From 4842a640a4d53f51b4f4e5b231f9fbf2e9e74947 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 30 Sep 2026 02:39:05 +0900 Subject: [PATCH 1/3] fix(responses): strip internal summary none before upstream send Carry #6232 with its regression in a sibling file so the capped passthrough suite does not grow. Co-authored-by: cshyang --- scripts/test-layout/layout.json | 1 + scripts/test-layout/seeds.json | 3 +- src/adapters/openai-responses/passthrough.ts | 3 +- src/adapters/openai-responses/reasoning.ts | 15 +++++++ structure/transports/responses-wire-shapes.md | 2 + tests/fixtures/test-layout-expected.json | 1 + .../openai-responses-summary-none.test.ts | 42 +++++++++++++++++++ 7 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 tests/responses/openai-responses-summary-none.test.ts diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index fd19e2040b0..e9f2ed988cb 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1213,6 +1213,7 @@ "openai-provider-option-tooling.test.ts": "adapters/openai", "openai-provider-option.test.ts": "adapters/openai", "openai-responses-passthrough.test.ts": "responses", + "openai-responses-summary-none.test.ts": "responses", "responses-forward-output-cap.test.ts": "responses", "opencode-cli.test.ts": "providers", "opencode-free-provider.test.ts": "providers", diff --git a/scripts/test-layout/seeds.json b/scripts/test-layout/seeds.json index 394627e716b..806bdabba99 100644 --- a/scripts/test-layout/seeds.json +++ b/scripts/test-layout/seeds.json @@ -55,12 +55,13 @@ "^(?:anthropic)-" ], "openai": [ - "^(?:openai)-" + "^openai-(?!responses-summary-none\\.test\\.ts$)" ] } }, "responses": { "match": [ + "^openai-responses-summary-none\\.test\\.ts$", "^(?:apply|chat|citation|continuation|eventstream|legacy|namespace|passthrough|responses|sse|thought|ws)-" ] }, diff --git a/src/adapters/openai-responses/passthrough.ts b/src/adapters/openai-responses/passthrough.ts index a3e5a85e1ca..d4533405dd0 100644 --- a/src/adapters/openai-responses/passthrough.ts +++ b/src/adapters/openai-responses/passthrough.ts @@ -34,7 +34,7 @@ import { import { createAdapterTierMetadata, } from "../../providers/fastwire"; -import { dropResponsesReasoningInputItems, mapRoutedResponsesReasoningEffort, normalizeConfiguredReasoningSummaryDelivery, sanitizeReasoningInputContent, stripDisabledReasoningSummaries, stripDisabledVerbosity, stripUnsupportedReasoningSummaryDelivery } from "./reasoning"; +import { dropResponsesReasoningInputItems, mapRoutedResponsesReasoningEffort, normalizeConfiguredReasoningSummaryDelivery, sanitizeReasoningInputContent, stripDisabledReasoningSummaries, stripDisabledVerbosity, stripNoneReasoningSummary, stripUnsupportedReasoningSummaryDelivery } from "./reasoning"; import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripCanonicalOnlyTopLevelFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored, stripRejectedSamplingParams } from "./request-strips"; import { stripCanonicalForwardPromptCacheOptions, stripDeprecatedPromptCacheRetention } from "./prompt-cache"; import { isPlainObject } from "./internal"; @@ -352,6 +352,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): outBody = repairOversizedReplayCallIds(outBody); } outBody = stripUnsupportedReasoningSummaryDelivery(outBody, parsed.modelId); + outBody = stripNoneReasoningSummary(outBody); // #4587: on a bridged provider, hand the destination back the search call and result the // proxy executed on its behalf, in place of the hosted cell the caller replays. Scoped to // its exact conversation and serving identity and recorded by the bridge itself, so a diff --git a/src/adapters/openai-responses/reasoning.ts b/src/adapters/openai-responses/reasoning.ts index 5f19edec946..5b79f9d7cec 100644 --- a/src/adapters/openai-responses/reasoning.ts +++ b/src/adapters/openai-responses/reasoning.ts @@ -156,6 +156,21 @@ export function sanitizeReasoningInputContent( return changed ? { ...raw, input } : body; } +/** + * `reasoning.summary: "none"` is an internal "hide the thinking summary" marker: Claude's + * `thinking.display: "omitted"` maps to it, and the parser reads it into + * `options.hideThinkingSummary`. It is not a Responses API value (valid: auto, concise, + * detailed), so drop it from the body that is serialized to the upstream. + */ +export function stripNoneReasoningSummary(body: unknown): unknown { + if (!isPlainObject(body) || !isPlainObject(body.reasoning) || body.reasoning.summary !== "none") return body; + const { summary: _summary, ...rest } = body.reasoning; + const next = { ...body }; + if (Object.keys(rest).length > 0) next.reasoning = rest; + else delete next.reasoning; + return next; +} + export function stripUnsupportedReasoningSummaryDelivery(body: unknown, modelId: string): unknown { if (catalogModelSupportsReasoningSummaries(modelId) !== false) return body; if (!isPlainObject(body) || !isPlainObject(body.stream_options)) return body; diff --git a/structure/transports/responses-wire-shapes.md b/structure/transports/responses-wire-shapes.md index 7944955f31b..214caf79e06 100644 --- a/structure/transports/responses-wire-shapes.md +++ b/structure/transports/responses-wire-shapes.md @@ -561,6 +561,8 @@ summary choices remain intact. Raw display and hidden-envelope replay follow [reasoning display parity](../providers/chat-compat.md#reasoning-display-parity-hidethinkingsummary). Final-route normalization preserves visible raw reasoning when the parsed request has a validated active effort and omits summary; explicit `summary: "none"` still hides it. +The passthrough adapter removes that internal `none` marker at final outbound serialization; +valid summary values remain on the upstream Responses wire. The provider policy `hideRawReasoning` suppresses the raw `reasoning_raw_delta` channel only — openai-chat `reasoning_content`, kiro tags, and Gemini thought parts on routes that do not return thought summaries (direct and Vertex Gemini; a `cloud-code-assist` Gemini route emits its thought diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 459506dab5b..923b8a8b43c 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1225,6 +1225,7 @@ "openai-provider-option-tooling.test.ts": "adapters/openai", "openai-provider-option.test.ts": "adapters/openai", "openai-responses-passthrough.test.ts": "responses", + "openai-responses-summary-none.test.ts": "responses", "responses-forward-output-cap.test.ts": "responses", "opencode-cli.test.ts": "providers", "opencode-free-provider.test.ts": "providers", diff --git a/tests/responses/openai-responses-summary-none.test.ts b/tests/responses/openai-responses-summary-none.test.ts new file mode 100644 index 00000000000..f052c8fe07a --- /dev/null +++ b/tests/responses/openai-responses-summary-none.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, test } from "bun:test"; +import { createResponsesPassthroughAdapter } from "../../src/adapters/openai-responses"; +import { anthropicToResponsesBody } from "../../src/claude/inbound"; +import { parseRequest } from "../../src/responses/parser"; +import { withTestTranslatorBudget } from "../helpers/translator-budget"; + +const targets = [ + { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward" as const }, + { adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", authMode: "key" as const, apiKey: "sk-test" }, +]; + +function outboundReasoning(target: (typeof targets)[number], body: Record): Record | undefined { + const adapter = withTestTranslatorBudget(createResponsesPassthroughAdapter(target)); + const request = adapter.buildRequest(parseRequest(body), { headers: new Headers() }); + return (JSON.parse(request.body) as { reasoning?: Record }).reasoning; +} + +describe("Responses summary:none wire marker", () => { + test("Claude omitted thinking retains parser intent but removes the marker for both destinations", () => { + const claudeBody = anthropicToResponsesBody({ + model: "gpt-6-astra", + max_tokens: 10, + messages: [{ role: "user", content: "hi" }], + thinking: { type: "adaptive", display: "omitted" }, + output_config: { effort: "high" }, + }); + expect((claudeBody as { reasoning?: unknown }).reasoning).toEqual({ summary: "none", effort: "high" }); + expect(parseRequest(claudeBody).options.hideThinkingSummary).toBe(true); + + for (const target of targets) { + expect(outboundReasoning(target, claudeBody)).toEqual({ effort: "high" }); + expect(outboundReasoning(target, { model: "gpt-6-astra", input: "hi", reasoning: { summary: "none" } })).toBeUndefined(); + } + }); + + test.each(["auto", "concise", "detailed"])("preserves valid summary %s", summary => { + for (const target of targets) { + expect(outboundReasoning(target, { model: "gpt-6-astra", input: "hi", reasoning: { effort: "medium", summary } })) + .toEqual({ effort: "medium", summary }); + } + }); +}); From 09f491785f53adaec0232a213ec78444ccbf9e4c Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 30 Sep 2026 03:28:09 +0900 Subject: [PATCH 2/3] docs(structure): keep responses wire-shape doc within its line budget --- structure/transports/responses-wire-shapes.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/structure/transports/responses-wire-shapes.md b/structure/transports/responses-wire-shapes.md index 4a786331aef..008998fa775 100644 --- a/structure/transports/responses-wire-shapes.md +++ b/structure/transports/responses-wire-shapes.md @@ -569,9 +569,8 @@ An injected combo default supplies `summary: "auto"` only when no summary was sp summary choices remain intact. Raw display and hidden-envelope replay follow [reasoning display parity](../providers/chat-compat.md#reasoning-display-parity-hidethinkingsummary). Final-route normalization preserves visible raw reasoning when the parsed request has a validated -active effort and omits summary; explicit `summary: "none"` still hides it. -The passthrough adapter removes that internal `none` marker at final outbound serialization; -valid summary values remain on the upstream Responses wire. +active effort and omits summary; explicit `summary: "none"` still hides it, and the passthrough adapter +strips that internal marker at final outbound serialization (valid summary values stay on the wire). The provider policy `hideRawReasoning` suppresses the raw `reasoning_raw_delta` channel only — openai-chat `reasoning_content`, kiro tags, and Gemini thought parts on routes that do not return thought summaries (direct and Vertex Gemini; a `cloud-code-assist` Gemini route emits its thought From 22728b9e3fd7f3b21d130a6060f2fcd5220cf4ae Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 30 Sep 2026 03:28:23 +0900 Subject: [PATCH 3/3] docs(structure): fold the summary-none note into one line --- structure/transports/responses-wire-shapes.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/structure/transports/responses-wire-shapes.md b/structure/transports/responses-wire-shapes.md index 008998fa775..31f2610de7d 100644 --- a/structure/transports/responses-wire-shapes.md +++ b/structure/transports/responses-wire-shapes.md @@ -569,8 +569,7 @@ An injected combo default supplies `summary: "auto"` only when no summary was sp summary choices remain intact. Raw display and hidden-envelope replay follow [reasoning display parity](../providers/chat-compat.md#reasoning-display-parity-hidethinkingsummary). Final-route normalization preserves visible raw reasoning when the parsed request has a validated -active effort and omits summary; explicit `summary: "none"` still hides it, and the passthrough adapter -strips that internal marker at final outbound serialization (valid summary values stay on the wire). +active effort and omits summary; explicit `summary: "none"` still hides it (passthrough strips that internal marker before the upstream send). The provider policy `hideRawReasoning` suppresses the raw `reasoning_raw_delta` channel only — openai-chat `reasoning_content`, kiro tags, and Gemini thought parts on routes that do not return thought summaries (direct and Vertex Gemini; a `cloud-code-assist` Gemini route emits its thought