-
Notifications
You must be signed in to change notification settings - Fork 351
[Feat] OpenAPI - add support for SSE import and emission #8887
Description
Clear and concise description of the problem
Now that we have support for OpenAPI 3.2.0 (#8828 ) both in emissions and in importing descriptions to TypeSpec, it'd be nice to have support for importing SSE (server-sent events).
@timotheeguerin and I spent the better part of an hour combing through multiple specifications to understand what needs to happen here, so strap in, this is going to be a long issue.
Relevant resources
- Define streaming APIs(Server side events, log stream, etc.) #154 as the initial specification for sse in typespec
- [Feat] Add support for terminating events in streaming APIs OAI/OpenAPI-Specification#5096 ongoing discussion about terminal events
- https://github.com/Azure/azure-sdk-for-python/blob/894d166350f0ccbe2fae467e4093ed0c3b428213/sdk/ai/azure-ai-agents/azure/ai/agents/operations/_patch.py#L564 manual implementation of SSE for Azure AI Foundry: Agents threads run API
- https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-agents/tests/assets/send_email_stream_response.txt an example of the payloads sent by this API
- WHATWG specification for SSE
- OAI 3.2.0 example on how to describe SSE
- OpenAI terminal event description
- OpenAI streaming responses documentation
- Blog post about using SSE APIs with OpenAI
- TypeSpec very sparse documentation for SSE
Import
Scenario 1 : no terminal event
Given the following OpenAPI Response object.
content:
description: A request body to add a stream of typed data.
required: true
content:
text/event-stream:
itemSchema:
type: object
properties:
event:
type: string
data:
type: string
required: [event]
# Define event types and specific schemas for the corresponding data
oneOf:
- properties:
event:
const: userconnect
data:
contentMediaType: application/json
contentSchema:
type: object
required: [username]
properties:
username:
type: string
- properties:
event:
const: usermessage
data:
contentMediaType: application/json
contentSchema:
type: object
required: [text]
properties:
text:
type: stringI'd expect aresulting TypeSpec description looking like this.
import "@typespec/streams";
import "@typespec/sse";
import "@typespec/events";
using SSE;
model UserConnect {
username: string;
}
model UserMessage {
text: string;
}
@TypeSpec.Events.events
union ChannelEvents {
userconnect: UserConnect,
usermessage: UserMessage,
}
op subscribeToChannel(): SSEStream<ChannelEvents>;Note that the terminal event is NOT present, this is ok because it's an invention of some APIs, and is not part of the WHATWG spec. It should remain optional.
Here a couple of things are worth noting:
- imports for typespec streams/sse/events are added
- a using for SSE is added
- a
@TypeSpec.Events.eventsdecorator is added to the union type - the return type of the operation is now
SSEStream<ChannelEvents>(instead of ChannelEvents) - The union type discriminator values are obtained by conventions based on the event properties in the schema (name is a convention)
- the union type member types are defined by convention by the schema of the data property, and the fact the content media type is application/json
Scenario 2: with terminal events
Given the following OpenAPI Response object.
content:
description: A request body to add a stream of typed data.
required: true
content:
text/event-stream:
itemSchema:
type: object
properties:
event:
type: string
data:
type: string
required: [event]
# Define event types and specific schemas for the corresponding data
oneOf:
- properties:
data:
contentMediaType: text/plain
const: "[done]"
"x-ms-sse-terminal-event": true
- properties:
event:
const: userconnect
data:
contentMediaType: application/json
contentSchema:
type: object
required: [username]
properties:
username:
type: string
- properties:
event:
const: usermessage
data:
contentMediaType: application/json
contentSchema:
type: object
required: [text]
properties:
text:
type: stringI'd expect aresulting TypeSpec description looking like this.
import "@typespec/streams";
import "@typespec/sse";
import "@typespec/events";
using SSE;
model UserConnect {
username: string;
}
model UserMessage {
text: string;
}
@TypeSpec.Events.events
union ChannelEvents {
userconnect: UserConnect,
usermessage: UserMessage,
@TypeSpec.Events.contentType("text/plain")
@TypeSpec.SSE.terminalEvent
@extension("x-ms-sse-terminal-event", true)
"[done]",
}
op subscribeToChannel(): SSEStream<ChannelEvents>;Terminal events are used to circumvent the fact that the only way to terminal an SSE "session" from the service is to terminal the connection, which may cause "errors" on the client side, and is costly (maybe the connection could have been re-used for a subsequent request).
Here a couple of additions are important to note:
- An extension is introduced in the description to "flag" the terminal event value as special. This is because today there's no placeholder for that information in the OpenAPI specification (see the discussion in the resources)
- That extension is ALSO maintained as a decorator during the import. This is because we DO NOT want to specialize the terminal event decorator to emit it to avoid basing the emission behaviour on an extension. So the extension decorator is used for the round trip.
- The contentType decorator value is taken from the contentMediaType in the description.
Emission
We also need to implement emission from those decorators to enable round-trips. The implementation design need to be similar to what's currently done for xml to enable decoupling of the packages:
See the tryImportXml implementation as an example.
When the @events decorator is encountered by the OpenAPI emitter, it should behave similarly to the @discriminator one. A couple of key differences to keep in mind:
- the implicit discriminator property name is "event" (conventions from SSE specification)
- the each model of the union should be emitted as a schema for the data sub-property (see examples above)
When the @terminalEvent decorator is encountered by the OpenAPI emitter, no special behavior should happen at this time.
When the @contentType decorator is encountered, it should be used to specify the value of the contentMediaType property (see example above)
When an operation has a return type which is SSEStream<T>, the response media type should be text/event-stream and itemSchema should be used with the information above instead of schema.
Checklist
- Follow our Code of Conduct
- Read the docs.
- Check that there isn't already an issue that request the same feature to avoid creating a duplicate.