From c7feddaa1e83279248b5603796a5588a9d721c47 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Tue, 1 Sep 2026 18:03:52 +0100 Subject: [PATCH] Added SSE progress streaming support for Chat and Evaluate APIs --- openapi.yml | 10 +++ paths/chat/chat-health.yml | 6 ++ paths/chat/chat.yml | 64 ++++++++++++++++- paths/chat/schemas/ChatCapabilities.yml | 5 +- paths/chat/schemas/SseChatTerminalFrame.yml | 36 ++++++++++ paths/evaluate/evaluate-health.yml | 7 ++ paths/evaluate/evaluate.yml | 68 +++++++++++++++++-- .../evaluate/schemas/EvaluateCapabilities.yml | 2 + paths/evaluate/schemas/EvaluateResponse.yml | 4 ++ .../schemas/SseEvaluateTerminalFrame.yml | 32 +++++++++ paths/shared/schemas/LLMConfiguration.yml | 7 +- paths/shared/schemas/SseProgressStep.yml | 30 ++++++++ paths/shared/schemas/SseTerminalSteps.yml | 13 ++++ .../shared/schemas/StreamingCapabilities.yml | 26 +++++++ 14 files changed, 301 insertions(+), 9 deletions(-) create mode 100644 paths/chat/schemas/SseChatTerminalFrame.yml create mode 100644 paths/evaluate/schemas/EvaluateResponse.yml create mode 100644 paths/evaluate/schemas/SseEvaluateTerminalFrame.yml create mode 100644 paths/shared/schemas/SseProgressStep.yml create mode 100644 paths/shared/schemas/SseTerminalSteps.yml create mode 100644 paths/shared/schemas/StreamingCapabilities.yml diff --git a/openapi.yml b/openapi.yml index 909544c..03b5000 100644 --- a/openapi.yml +++ b/openapi.yml @@ -12,6 +12,16 @@ info: - **Evaluate Task**: automatic feedback and grading for student submissions.
- **Chat**: conversational interactions around tasks, submissions, or general learning questions. +

+ **Progress streaming.** Any operation MAY support an opt-in Server-Sent + Events progress stream, selected per request with the + `Accept: text/event-stream` header. The stream is a sequence of + `SseProgressStep` frames, optional keep-alive comments, and exactly one + terminal frame carrying the operation's normal `200` body (on success) or an + `ErrorResponse` (on failure); the HTTP status stays `200` throughout. + Services advertise support via `supportsStreaming` in their `/health` + capabilities. This is distinct from `configuration.llm.stream`, which + governs token-level streaming from the LLM provider. tags: - name: evaluate diff --git a/paths/chat/chat-health.yml b/paths/chat/chat-health.yml index 4d3ec90..8401390 100644 --- a/paths/chat/chat-health.yml +++ b/paths/chat/chat-health.yml @@ -37,6 +37,12 @@ get: supportsChat: true supportsUserPreferences: true supportsStreaming: true + supportedProgressStages: + - "preparing" + - "starting" + - "thinking" + - "completed" + - "failed" "supportsDataPolicy": "NOT_SUPPORTED" supportedLanguages: - "en" diff --git a/paths/chat/chat.yml b/paths/chat/chat.yml index ff3dd0c..a475199 100644 --- a/paths/chat/chat.yml +++ b/paths/chat/chat.yml @@ -139,7 +139,35 @@ post: temperature: 0.5 responses: "200": - description: Successful chat response. + description: > + Successful chat response. + + + ### Streaming variant (opt-in) + + + If the request sends `Accept: text/event-stream`, the response is a + Server-Sent Events progress stream instead of a single JSON body. The + HTTP status stays `200` for the entire stream, including failures. It + consists of: + + + - zero or more progress frames — the SSE `event:` field is the stage + name and `data:` is an `SseProgressStep` JSON object; + + - optional keep-alive comment lines (each starting with `:`); + + - exactly one terminal frame — `event: completed` with `data:` an + `SseChatTerminalFrame` (the `200` body as `output` / `metadata`, plus + a `steps` replay), or `event: failed` with `data:` an + `SseChatTerminalFrame` whose `error` is an `ErrorResponse`. + + + Failures are always delivered as a terminal `failed` frame, never as an + HTTP error status. `X-Request-Id` and `X-Api-Version` are sent once, as + response headers, when the stream opens. This is independent of + `configuration.llm.stream`, which governs token-level streaming from the + LLM provider. headers: X-Request-Id: description: Request id for tracing this request across services. @@ -213,6 +241,40 @@ post: model: "gpt-5.2" temperature: 0.5 outputTokens: 143 + text/event-stream: + schema: + $ref: "./schemas/SseChatTerminalFrame.yml" + examples: + completedFrame: + summary: > + Terminal `event: completed` frame `data` (preceded on the wire by + `event: preparing` / `event: thinking` progress frames whose + `data` is an SseProgressStep, and optional `:` keep-alive lines). + value: + output: + role: ASSISTANT + content: "Polymorphism lets one interface represent many underlying forms." + metadata: + responseTimeMs: 1800 + steps: + - stage: "preparing" + message: "Preparing worker" + timestamp: "2025-12-10T11:15:00Z" + - stage: "thinking" + message: "Composing response" + timestamp: "2025-12-10T11:15:02Z" + failedFrame: + summary: "Terminal `event: failed` frame `data` (HTTP status is still 200)." + value: + output: null + error: + title: "Chat failed" + message: "The LLM provider connection failed." + code: "LLM_PROVIDER_ERROR" + steps: + - stage: "preparing" + message: "Preparing worker" + timestamp: "2025-12-10T11:15:00Z" "400": $ref: "./responses/400-BadRequest.yml" "403": diff --git a/paths/chat/schemas/ChatCapabilities.yml b/paths/chat/schemas/ChatCapabilities.yml index 08cf660..d8ffbb7 100644 --- a/paths/chat/schemas/ChatCapabilities.yml +++ b/paths/chat/schemas/ChatCapabilities.yml @@ -4,6 +4,8 @@ required: - supportsChat - supportsDataPolicy additionalProperties: true +allOf: + - $ref: "../../shared/schemas/StreamingCapabilities.yml" properties: supportsChat: type: boolean @@ -11,9 +13,6 @@ properties: supportsUserPreferences: type: boolean description: Indicates whether the service supports adapting to user preferences. - supportsStreaming: - type: boolean - description: Indicates whether the service supports streaming responses. supportsDataPolicy: $ref: "../../shared/enums/DataPolicySupport.yml" supportedLanguages: diff --git a/paths/chat/schemas/SseChatTerminalFrame.yml b/paths/chat/schemas/SseChatTerminalFrame.yml new file mode 100644 index 0000000..a852b6a --- /dev/null +++ b/paths/chat/schemas/SseChatTerminalFrame.yml @@ -0,0 +1,36 @@ +type: object +description: > + Data payload of the single terminal frame of a `POST /chat` SSE progress + stream. + + On `event: completed`, `output` / `metadata` hold exactly the endpoint's + normal `200 application/json` body (`ChatResponse`) and `error` is absent. + + On `event: failed`, `output` is null and `error` holds an `ErrorResponse` with + the same `title` / `code` / `details` shape clients already handle for `400` / + `500` responses. The HTTP status stays `200` regardless. + + `steps` is always present: the ordered replay of every progress step emitted + during the stream. +allOf: + - $ref: "../../shared/schemas/SseTerminalSteps.yml" + - type: object + properties: + output: + description: The generated assistant response; null on a `failed` frame. + anyOf: + - $ref: "./Message.yml" + - type: "null" + metadata: + type: + - object + - "null" + description: Optional metadata about response generation. + additionalProperties: true + error: + type: + - object + - "null" + description: Present only on a `failed` frame. + allOf: + - $ref: "../../shared/schemas/ErrorResponse.yml" diff --git a/paths/evaluate/evaluate-health.yml b/paths/evaluate/evaluate-health.yml index 36cacdc..e224d37 100644 --- a/paths/evaluate/evaluate-health.yml +++ b/paths/evaluate/evaluate-health.yml @@ -40,6 +40,13 @@ get: supportsFormativeFeedback: true supportsSummativeFeedback: true supportsDataPolicy: "PARTIAL" + supportsStreaming: true + supportedProgressStages: + - "preparing" + - "starting" + - "evaluating" + - "completed" + - "failed" supportedArtefactProfiles: - type: "TEXT" supportedFormats: ["plain", "markdown"] diff --git a/paths/evaluate/evaluate.yml b/paths/evaluate/evaluate.yml index 77818a0..57d2ff6 100644 --- a/paths/evaluate/evaluate.yml +++ b/paths/evaluate/evaluate.yml @@ -339,7 +339,35 @@ post: version: 1 responses: "200": - description: Successfully generated feedback. + description: > + Successfully generated feedback. + + + ### Streaming variant (opt-in) + + + If the request sends `Accept: text/event-stream`, the response is a + Server-Sent Events progress stream instead of a single JSON body. The + HTTP status stays `200` for the entire stream, including failures. It + consists of: + + + - zero or more progress frames — the SSE `event:` field is the stage + name and `data:` is an `SseProgressStep` JSON object; + + - optional keep-alive comment lines (each starting with `:`); + + - exactly one terminal frame — `event: completed` with `data:` an + `SseEvaluateTerminalFrame` (the `200` body under `feedback`, plus a + `steps` replay), or `event: failed` with `data:` an + `SseEvaluateTerminalFrame` whose `error` is an `ErrorResponse`. + + + Failures are always delivered as a terminal `failed` frame, never as an + HTTP error status. `X-Request-Id` and `X-Api-Version` are sent once, as + response headers, when the stream opens. This is independent of + `configuration.llm.stream`, which governs token-level streaming from the + LLM provider. headers: X-Request-Id: description: Request id for tracing this request across services. @@ -352,9 +380,7 @@ post: content: application/json: schema: - type: array - items: - $ref: "./schemas/Feedback.yml" + $ref: "./schemas/EvaluateResponse.yml" examples: exampleResponse: summary: Example feedback response @@ -379,6 +405,40 @@ post: - feedbackId: "fb-2" title: "Overall structure" message: "The overall structure of your answer is clear and easy to follow." + text/event-stream: + schema: + $ref: "./schemas/SseEvaluateTerminalFrame.yml" + examples: + completedFrame: + summary: > + Terminal `event: completed` frame `data` (preceded on the wire by + `event: preparing` / `event: evaluating` progress frames whose + `data` is an SseProgressStep, and optional `:` keep-alive lines). + value: + feedback: + - feedbackId: "fb-1" + title: "Clarify your definition" + message: "Distinguish subtype from parametric polymorphism." + awardedPoints: 2.5 + steps: + - stage: "preparing" + message: "Preparing worker" + timestamp: "2025-12-16T09:30:01Z" + - stage: "evaluating" + message: "Ran 3/10 checks" + timestamp: "2025-12-16T09:30:04Z" + failedFrame: + summary: "Terminal `event: failed` frame `data` (HTTP status is still 200)." + value: + feedback: null + error: + title: "Evaluation failed" + message: "The worker exited before returning feedback." + code: "LLM_PROVIDER_ERROR" + steps: + - stage: "preparing" + message: "Preparing worker" + timestamp: "2025-12-16T09:30:01Z" "202": $ref: "./responses/202-Accepted.yml" "400": diff --git a/paths/evaluate/schemas/EvaluateCapabilities.yml b/paths/evaluate/schemas/EvaluateCapabilities.yml index 92796e9..cb7d3b5 100644 --- a/paths/evaluate/schemas/EvaluateCapabilities.yml +++ b/paths/evaluate/schemas/EvaluateCapabilities.yml @@ -7,6 +7,8 @@ required: - supportsSummativeFeedback - supportsDataPolicy additionalProperties: true +allOf: + - $ref: "../../shared/schemas/StreamingCapabilities.yml" properties: supportsEvaluate: type: boolean diff --git a/paths/evaluate/schemas/EvaluateResponse.yml b/paths/evaluate/schemas/EvaluateResponse.yml new file mode 100644 index 0000000..0303b1a --- /dev/null +++ b/paths/evaluate/schemas/EvaluateResponse.yml @@ -0,0 +1,4 @@ +type: array +description: Ordered list of feedback items generated for the submission. +items: + $ref: "./Feedback.yml" diff --git a/paths/evaluate/schemas/SseEvaluateTerminalFrame.yml b/paths/evaluate/schemas/SseEvaluateTerminalFrame.yml new file mode 100644 index 0000000..8319adc --- /dev/null +++ b/paths/evaluate/schemas/SseEvaluateTerminalFrame.yml @@ -0,0 +1,32 @@ +type: object +description: > + Data payload of the single terminal frame of a `POST /evaluate` SSE progress + stream. + + On `event: completed`, `feedback` holds exactly the endpoint's normal + `200 application/json` body and `error` is absent. + + On `event: failed`, `feedback` is null and `error` holds an `ErrorResponse` + with the same `title` / `code` / `details` shape clients already handle for + `400` / `500` responses. The HTTP status stays `200` regardless. + + `steps` is always present: the ordered replay of every progress step emitted + during the stream. +allOf: + - $ref: "../../shared/schemas/SseTerminalSteps.yml" + - type: object + properties: + feedback: + type: + - array + - "null" + description: The generated feedback items; null on a `failed` frame. + items: + $ref: "./Feedback.yml" + error: + type: + - object + - "null" + description: Present only on a `failed` frame. + allOf: + - $ref: "../../shared/schemas/ErrorResponse.yml" diff --git a/paths/shared/schemas/LLMConfiguration.yml b/paths/shared/schemas/LLMConfiguration.yml index 7829e1e..0d07dea 100644 --- a/paths/shared/schemas/LLMConfiguration.yml +++ b/paths/shared/schemas/LLMConfiguration.yml @@ -23,7 +23,12 @@ properties: type: - boolean - "null" - description: Optional flag indicating whether streaming responses are requested. + description: > + Optional flag requesting token-level streaming from the underlying LLM + provider. This is distinct from API-layer progress streaming, which is + opt-in per request via the `Accept: text/event-stream` header and is + advertised by `supportsStreaming` in a service's `/health` capabilities. + A service MAY support either, both, or neither. credentials: type: - object diff --git a/paths/shared/schemas/SseProgressStep.yml b/paths/shared/schemas/SseProgressStep.yml new file mode 100644 index 0000000..6acce1b --- /dev/null +++ b/paths/shared/schemas/SseProgressStep.yml @@ -0,0 +1,30 @@ +type: object +description: > + A single progress step emitted while an operation runs. Carried as the SSE + `data:` payload of an intermediate progress frame (the SSE `event:` field + carries the stage name), and replayed in the terminal frame's `steps` array. +additionalProperties: true +required: + - stage + - timestamp +properties: + stage: + type: string + description: > + Lifecycle stage this step reports. Informative, not a fixed enum: + implementations MAY introduce new stages without a spec change, and clients + MUST tolerate values they do not recognise. Each service advertises the + stage values it emits via `supportedProgressStages` in its `/health` + capabilities. Common values: "preparing", "starting", "evaluating" + (evaluate), "thinking" (chat), and the terminal "completed" / "failed". + message: + type: + - string + - "null" + description: > + Short, human-readable description of the step, suitable for display to a + learner or teacher. + timestamp: + type: string + format: date-time + description: Time this step was generated (RFC 3339 / ISO 8601). diff --git a/paths/shared/schemas/SseTerminalSteps.yml b/paths/shared/schemas/SseTerminalSteps.yml new file mode 100644 index 0000000..9a9240d --- /dev/null +++ b/paths/shared/schemas/SseTerminalSteps.yml @@ -0,0 +1,13 @@ +type: object +description: > + Shared fragment for the terminal SSE frame: the ordered replay of every + progress step emitted during the stream, so a client that connected late or + dropped intermediate frames still receives the full trace. +required: + - steps +properties: + steps: + type: array + description: Ordered list of every SseProgressStep emitted during the stream. + items: + $ref: "./SseProgressStep.yml" diff --git a/paths/shared/schemas/StreamingCapabilities.yml b/paths/shared/schemas/StreamingCapabilities.yml new file mode 100644 index 0000000..54422fd --- /dev/null +++ b/paths/shared/schemas/StreamingCapabilities.yml @@ -0,0 +1,26 @@ +type: object +description: > + Shared capability fragment describing an operation's support for opt-in + Server-Sent Events (SSE) progress streaming. Composed via `allOf` into each + service's capabilities schema so every streaming-capable endpoint advertises + streaming the same way. +additionalProperties: true +properties: + supportsStreaming: + type: boolean + description: > + Indicates whether this operation supports opt-in SSE progress streaming, + selected per request with the `Accept: text/event-stream` header. This is + distinct from `configuration.llm.stream`, which governs token-level + streaming from the underlying LLM provider. A service MAY support either, + both, or neither. + supportedProgressStages: + type: + - array + - "null" + description: > + Optional list of `SseProgressStep.stage` values this service may emit + (e.g. ["preparing", "starting", "evaluating", "completed", "failed"]). + Informative only; clients MUST tolerate stages that are not listed. + items: + type: string