From 8a8b572e9f8864eddec2c6bd3342be067a4530f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=93=9B=F0=9D=93=B2=F0=9D=93=BD=F0=9D=93=BD?= =?UTF-8?q?=F0=9D=93=B5=F0=9D=93=AE=20=F0=9D=93=95=F0=9D=93=BB=F0=9D=93=AA?= =?UTF-8?q?=F0=9D=93=B7=F0=9D=93=B4?= Date: Fri, 26 Jun 2026 20:37:53 +0000 Subject: [PATCH] fix(sdk): preserve SSE event payload types --- .../server/routes/instance/httpapi/public.ts | 23 +++++++++++++++++++ .../server/httpapi-public-openapi.test.ts | 16 +++++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 8 ++----- packages/sdk/openapi.json | 18 ++------------- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/httpapi/public.ts b/packages/opencode/src/server/routes/instance/httpapi/public.ts index 2a7266c511..288ff7df4c 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/public.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/public.ts @@ -34,6 +34,8 @@ type OpenApiSchema = { additionalProperties?: OpenApiSchema | boolean allOf?: OpenApiSchema[] anyOf?: OpenApiSchema[] + contentMediaType?: string + contentSchema?: OpenApiSchema description?: string enum?: Array items?: OpenApiSchema @@ -88,6 +90,7 @@ function matchLegacyOpenApi(input: Record) { // payload and inside an annotated union arm. Resolve these by inlining the // actual schema from any parent union that references them. fixSelfReferencingComponents(spec) + collapseJsonSseComponents(spec) // Effect's Schema.optional emits `anyOf: [T, {type:"null"}]` in OpenAPI, // but the legacy SDK expected plain `T` for optional fields. Strip null @@ -229,6 +232,26 @@ function collapseDuplicateComponents(spec: OpenApiSpec) { } } +function collapseJsonSseComponents(spec: OpenApiSpec) { + const schemas = spec.components?.schemas + if (!schemas) return + for (const [name, schema] of Object.entries(schemas)) { + const ref = schema.contentMediaType === "application/json" ? schema.contentSchema?.$ref : undefined + const target = ref?.replace("#/components/schemas/", "") + if (schema.type !== "string" || !target || !schemas[target]) continue + // Effect represents JSON SSE payloads as encoded string schemas, but the + // generated SDK's SSE client already decodes them. Keep the payload union + // under its stable, unsuffixed component name instead of exposing `string`. + const canonical = name.replace(/\d+$/, "") + if (target.replace(/\d+$/, "") !== canonical) continue + schemas[canonical] = schemas[target] + rewriteRefs(spec, name, canonical) + rewriteRefs(spec, target, canonical) + if (name !== canonical) delete schemas[name] + if (target !== canonical) delete schemas[target] + } +} + function normalizeComponentNames(spec: OpenApiSpec) { const schemas = spec.components?.schemas if (!schemas) return diff --git a/packages/opencode/test/server/httpapi-public-openapi.test.ts b/packages/opencode/test/server/httpapi-public-openapi.test.ts index a8f6f8d1c8..7175d0a828 100644 --- a/packages/opencode/test/server/httpapi-public-openapi.test.ts +++ b/packages/opencode/test/server/httpapi-public-openapi.test.ts @@ -99,6 +99,22 @@ describe("PublicApi OpenAPI v2 errors", () => { }) }) + test("exposes decoded SSE payload schemas", () => { + const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec + + expect(spec.paths["/api/event"]?.get?.responses?.["200"]?.content?.["text/event-stream"]?.schema?.$ref).toBe( + "#/components/schemas/V2Event", + ) + expect(spec.components.schemas.V2Event?.anyOf?.length).toBeGreaterThan(0) + expect(spec.components.schemas.V2Event1).toBeUndefined() + + expect( + spec.paths["/api/session/{sessionID}/event"]?.get?.responses?.["200"]?.content?.["text/event-stream"]?.schema + ?.properties?.data?.$ref, + ).toBe("#/components/schemas/SessionDurableEvent") + expect(spec.components.schemas.SessionDurableEvent1).toBeUndefined() + }) + test("preserves /api auth responses", () => { const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 3ac6ea713d..3ce42e218c 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2766,8 +2766,6 @@ export type SessionHistory = { hasMore: boolean } -export type SessionDurableEvent1 = string - export type SessionMessagesResponse = { data: Array cursor: { @@ -2847,7 +2845,7 @@ export type QuestionRejected2 = { } } -export type V2Event1 = +export type V2Event = | ModelsDevRefreshed | IntegrationUpdated | IntegrationConnectionUpdated @@ -2937,8 +2935,6 @@ export type V2Event1 = | ServerConnected | GlobalDisposed -export type V2Event = string - export type ForbiddenError = { _tag: "ForbiddenError" message: string @@ -11897,7 +11893,7 @@ export type V2SessionEventsResponses = { 200: { id: string event: string - data: SessionDurableEvent1 + data: SessionDurableEvent } } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 1c833aa64b..140edf86bd 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -11260,7 +11260,7 @@ "type": "string" }, "data": { - "$ref": "#/components/schemas/SessionDurableEvent1" + "$ref": "#/components/schemas/SessionDurableEvent" } }, "required": ["id", "event", "data"], @@ -23732,13 +23732,6 @@ "required": ["data", "hasMore"], "additionalProperties": false }, - "SessionDurableEvent1": { - "type": "string", - "contentSchema": { - "$ref": "#/components/schemas/SessionDurableEvent" - }, - "contentMediaType": "application/json" - }, "SessionMessagesResponse": { "type": "object", "properties": { @@ -23975,7 +23968,7 @@ "required": ["id", "type", "data"], "additionalProperties": false }, - "V2Event1": { + "V2Event": { "anyOf": [ { "$ref": "#/components/schemas/Models-devRefreshed" @@ -24243,13 +24236,6 @@ } ] }, - "V2Event": { - "type": "string", - "contentSchema": { - "$ref": "#/components/schemas/V2Event1" - }, - "contentMediaType": "application/json" - }, "ForbiddenError": { "type": "object", "properties": {