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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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",
"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",
"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",
"@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",

View file

@ -153,6 +153,7 @@
"@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",
"@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 properties?: Record<string, OpenApiSchema>
readonly required?: readonly string[]
readonly contentSchema?: OpenApiSchema
readonly contentMediaType?: string
}
type OpenApiResponse = {
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", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec

View file

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

View file

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

View file

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