fix(sdk): preserve V2Event name for SSE streams (#34171)

This commit is contained in:
Dax
2026-06-27 21:05:47 -04:00
committed by GitHub
parent ae53163cad
commit 6446b8ae3b
7 changed files with 89 additions and 13 deletions

View File

@@ -1051,6 +1051,7 @@
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch", "@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",

View File

@@ -153,6 +153,7 @@
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch", "@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch" "@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch"
} }
} }

View File

@@ -10,6 +10,8 @@ type OpenApiSchema = {
readonly enum?: readonly unknown[] readonly enum?: readonly unknown[]
readonly properties?: Record<string, OpenApiSchema> readonly properties?: Record<string, OpenApiSchema>
readonly required?: readonly string[] readonly required?: readonly string[]
readonly contentSchema?: OpenApiSchema
readonly contentMediaType?: string
} }
type OpenApiResponse = { type OpenApiResponse = {
readonly description?: string readonly description?: string
@@ -99,6 +101,21 @@ describe("PublicApi OpenAPI v2 errors", () => {
}) })
}) })
test("names the v2 event union without the SSE string wrapper collision", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
expect(spec.components.schemas.V2Event1).toBeUndefined()
expect(spec.components.schemas.V2Event?.anyOf?.length).toBeGreaterThan(0)
expect(spec.components.schemas.V2EventStream).toMatchObject({
type: "string",
contentMediaType: "application/json",
contentSchema: { $ref: "#/components/schemas/V2Event" },
})
expect(spec.paths["/api/event"]?.get?.responses?.["200"]?.content?.["text/event-stream"]?.schema).toEqual({
$ref: "#/components/schemas/V2Event",
})
})
test("preserves /api auth responses", () => { test("preserves /api auth responses", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec

View File

@@ -2766,7 +2766,7 @@ export type SessionHistory = {
hasMore: boolean hasMore: boolean
} }
export type SessionDurableEvent1 = string export type SessionDurableEventStream = string
export type SessionMessagesResponse = { export type SessionMessagesResponse = {
data: Array<SessionMessage> data: Array<SessionMessage>
@@ -2847,7 +2847,7 @@ export type QuestionRejected2 = {
} }
} }
export type V2Event1 = export type V2Event =
| ModelsDevRefreshed | ModelsDevRefreshed
| IntegrationUpdated | IntegrationUpdated
| IntegrationConnectionUpdated | IntegrationConnectionUpdated
@@ -2937,7 +2937,7 @@ export type V2Event1 =
| ServerConnected | ServerConnected
| GlobalDisposed | GlobalDisposed
export type V2Event = string export type V2EventStream = string
export type ForbiddenError = { export type ForbiddenError = {
_tag: "ForbiddenError" _tag: "ForbiddenError"
@@ -11901,7 +11901,7 @@ export type V2SessionEventsResponses = {
200: { 200: {
id: string id: string
event: string event: string
data: SessionDurableEvent1 data: SessionDurableEventStream
} }
} }

View File

@@ -11260,7 +11260,7 @@
"type": "string" "type": "string"
}, },
"data": { "data": {
"$ref": "#/components/schemas/SessionDurableEvent1" "$ref": "#/components/schemas/SessionDurableEventStream"
} }
}, },
"required": ["id", "event", "data"], "required": ["id", "event", "data"],
@@ -23732,7 +23732,7 @@
"required": ["data", "hasMore"], "required": ["data", "hasMore"],
"additionalProperties": false "additionalProperties": false
}, },
"SessionDurableEvent1": { "SessionDurableEventStream": {
"type": "string", "type": "string",
"contentSchema": { "contentSchema": {
"$ref": "#/components/schemas/SessionDurableEvent" "$ref": "#/components/schemas/SessionDurableEvent"
@@ -23975,7 +23975,7 @@
"required": ["id", "type", "data"], "required": ["id", "type", "data"],
"additionalProperties": false "additionalProperties": false
}, },
"V2Event1": { "V2Event": {
"anyOf": [ "anyOf": [
{ {
"$ref": "#/components/schemas/Models-devRefreshed" "$ref": "#/components/schemas/Models-devRefreshed"
@@ -24243,10 +24243,10 @@
} }
] ]
}, },
"V2Event": { "V2EventStream": {
"type": "string", "type": "string",
"contentSchema": { "contentSchema": {
"$ref": "#/components/schemas/V2Event1" "$ref": "#/components/schemas/V2Event"
}, },
"contentMediaType": "application/json" "contentMediaType": "application/json"
}, },

View File

@@ -16,7 +16,7 @@ import type {
SessionMessageAssistantTool, SessionMessageAssistantTool,
SessionV2Info, SessionV2Info,
SkillV2Info, SkillV2Info,
V2Event1, V2Event,
} from "@opencode-ai/sdk/v2" } from "@opencode-ai/sdk/v2"
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "./helper" import { createSimpleContext } from "./helper"
@@ -121,7 +121,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
}, },
} }
function handleEvent(event: V2Event1) { function handleEvent(event: V2Event) {
switch (event.type) { switch (event.type) {
case "catalog.updated": case "catalog.updated":
void Promise.all([ void Promise.all([
@@ -408,7 +408,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
...event, ...event,
data: event.properties, data: event.properties,
location: { directory: metadata.directory, workspaceID: metadata.workspace }, location: { directory: metadata.directory, workspaceID: metadata.workspace },
} as V2Event1) } as V2Event)
}) })
onCleanup(unsub) onCleanup(unsub)
}) })

View File

@@ -0,0 +1,57 @@
diff --git a/dist/unstable/httpapi/HttpApiSchema.js b/dist/unstable/httpapi/HttpApiSchema.js
index ecb6603d1ee4e89e92174b3a1100e8c9c720b3f3..08b832b6b3e9e4d10df156eca36b16f571f67f6e 100644
--- a/dist/unstable/httpapi/HttpApiSchema.js
+++ b/dist/unstable/httpapi/HttpApiSchema.js
@@ -151,7 +151,7 @@ export const StreamSse = options => {
const events = options.events ?? (options.data === undefined ? undefined : Schema.Struct({
id: Schema.UndefinedOr(Schema.String),
event: Schema.String,
- data: Schema.fromJsonString(options.data)
+ data: sseDataJsonSchema(options.data)
}));
if (events === undefined) {
throw new Error("StreamSse requires either an events schema or a data schema");
@@ -166,6 +166,14 @@ export const StreamSse = options => {
error: options.error ?? Schema.Never
});
};
+const sseDataJsonSchema = data => {
+ const identifier = SchemaAST.resolveIdentifier(data.ast);
+ return identifier === undefined ? Schema.fromJsonString(data) : Schema.fromJsonString(data).annotate({
+ // The SSE transport field is a JSON string. Give that wrapper its own
+ // OpenAPI identifier so it does not claim the decoded data schema's name.
+ identifier: `${identifier}Stream`
+ });
+};
/**
* Creates a streaming `Uint8Array` success response schema.
*
diff --git a/src/unstable/httpapi/HttpApiSchema.ts b/src/unstable/httpapi/HttpApiSchema.ts
index f2920365cf0328146523ce2e2dba04d6a8809a4a..5482d635cdf01a72063ff683d244fab891910211 100644
--- a/src/unstable/httpapi/HttpApiSchema.ts
+++ b/src/unstable/httpapi/HttpApiSchema.ts
@@ -433,7 +433,7 @@ export const StreamSse: {
const events = options.events ?? (options.data === undefined ? undefined : Schema.Struct({
id: Schema.UndefinedOr(Schema.String),
event: Schema.String,
- data: Schema.fromJsonString(options.data)
+ data: sseDataJsonSchema(options.data)
}))
if (events === undefined) {
throw new Error("StreamSse requires either an events schema or a data schema")
@@ -449,6 +449,15 @@ export const StreamSse: {
})
}
+const sseDataJsonSchema = (data: Schema.Constraint) => {
+ const identifier = SchemaAST.resolveIdentifier(data.ast)
+ return identifier === undefined ? Schema.fromJsonString(data) : Schema.fromJsonString(data).annotate({
+ // The SSE transport field is a JSON string. Give that wrapper its own
+ // OpenAPI identifier so it does not claim the decoded data schema's name.
+ identifier: `${identifier}Stream`
+ })
+}
+
/**
* Creates a streaming `Uint8Array` success response schema.
*