Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion scripts/test-layout/seeds.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)-"
]
},
Expand Down
3 changes: 2 additions & 1 deletion src/adapters/openai-responses/passthrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/adapters/openai-responses/reasoning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion structure/transports/responses-wire-shapes.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +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.
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
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/test-layout-expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
42 changes: 42 additions & 0 deletions tests/responses/openai-responses-summary-none.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>): Record<string, unknown> | undefined {
const adapter = withTestTranslatorBudget(createResponsesPassthroughAdapter(target));
const request = adapter.buildRequest(parseRequest(body), { headers: new Headers() });
return (JSON.parse(request.body) as { reasoning?: Record<string, unknown> }).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 });
}
});
});
Loading