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
7 changes: 7 additions & 0 deletions docs-site/src/content/docs/reference/proxy-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,13 @@ effort default to `reasoning.summary: "auto"` so thinking streams back as
`reasoning.summary: "none"`. An explicit `reasoning.summary` of `auto`, `concise`,
`detailed`, or `none` wins over `include_reasoning`.

Without reasoning options, raw reasoning already returned by the upstream is delivered as
`delta.reasoning_content` (streaming) or `message.reasoning_content` (buffered), without adding
reasoning options to the upstream request. Explicit thinking disable (`thinking.type: "disabled"`
or `reasoning.enabled: false`) and effort `none` also hide this raw text; an explicit enabled
boolean takes precedence over effort. Provider `hideRawReasoning` still applies. Provider-authored
summary visibility keeps its existing defaults; Responses and Anthropic Messages clients are unchanged.

When the model is Claude, the response also carries `reasoning_details` in the same shape
OpenRouter uses: `{"type": "reasoning.text", "text", "signature", "format": "anthropic-claude-v1"}`
for each signed thinking block and `{"type": "reasoning.encrypted", "data", ...}` for each redacted
Expand Down
1 change: 1 addition & 0 deletions scripts/test-layout/layout-chat.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"chat-raw-reasoning-delivery.test.ts": "responses",
"chat-incomplete-length-reason.test.ts": "responses",
"openai-chat-required-tool.test.ts": "adapters/openai",
"responses-required-chat-tool.test.ts": "responses"
Expand Down
4 changes: 3 additions & 1 deletion src/bridge/response-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ function buildResponseJSONWithBudget(
modelId: string,
options?: {
hideThinkingSummary?: boolean;
/** Internal Chat delivery override for raw reasoning only; summaries keep their policy. */
chatRawReasoningVisible?: boolean;
/** Provider policy: suppress raw content-channel reasoning, keep provider-authored summaries. */
hideRawReasoning?: boolean;
toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
Expand Down Expand Up @@ -297,7 +299,7 @@ function buildResponseJSONWithBudget(
const rawText = joinChunks(currentRawReasoning);
if (!rawText) return;
rawReasoningForNextToolCall = rawText;
if (options?.hideThinkingSummary === true || options?.hideRawReasoning === true) {
if (!(options?.chatRawReasoningVisible ?? !options?.hideThinkingSummary) || options?.hideRawReasoning === true) {
if (options?.omitHiddenReasoningEnvelope === true) {
budget?.releaseRetained(currentRawReasoning.bytes, { kind: "reasoning" });
currentRawReasoning = emptyChunks();
Expand Down
4 changes: 3 additions & 1 deletion src/bridge/sse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export function bridgeToResponsesSSE(
*/
localUpstream?: boolean;
hideThinkingSummary?: boolean;
/** Internal Chat delivery override for raw reasoning only; summaries keep their policy. */
chatRawReasoningVisible?: boolean;
/** Provider policy: suppress raw content-channel reasoning, keep provider-authored summaries. */
hideRawReasoning?: boolean;
/**
Expand Down Expand Up @@ -1036,7 +1038,7 @@ export function bridgeToResponsesSSE(
break;
}
case "reasoning_raw_delta": {
if (options?.hideThinkingSummary || options?.hideRawReasoning) {
if (!(options?.chatRawReasoningVisible ?? !options?.hideThinkingSummary) || options?.hideRawReasoning) {
hiddenRawReasoning = appendString(
hiddenRawReasoning,
event.text,
Expand Down
15 changes: 13 additions & 2 deletions src/chat/inbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,18 @@ function resolveReasoningSummary(raw: Rec): string | undefined {
return undefined;
}

function resolveThinkingEnabled(raw: Rec): boolean | undefined {
return isRec(raw.reasoning) && typeof raw.reasoning.enabled === "boolean" ? raw.reasoning.enabled
: isRec(raw.thinking) && ["enabled", "disabled"].includes(String(raw.thinking.type)) ? raw.thinking.type === "enabled" : undefined;
}

/** Client delivery only: never put this default in the upstream body or parsed options. */
export function chatRawReasoningVisible(raw: Rec): boolean {
if (resolveReasoningSummary(raw) === "none") return false;
const enabled = resolveThinkingEnabled(raw);
return enabled ?? resolveReasoningEffort(raw) !== "none";
}

/**
* Translate an OpenAI Chat Completions request body into a /v1/responses request body.
* Throws ChatCompletionsRequestError (-> 400) on malformed input.
Expand Down Expand Up @@ -672,8 +684,7 @@ function applyChatSettings(raw: ChatCompletionsRoutingBody, body: Rec): void {

const effort = resolveReasoningEffort(raw);
const summary = resolveReasoningSummary(raw);
const enabled = isRec(raw.reasoning) && typeof raw.reasoning.enabled === "boolean" ? raw.reasoning.enabled
: isRec(raw.thinking) && ["enabled", "disabled"].includes(String(raw.thinking.type)) ? raw.thinking.type === "enabled" : undefined;
const enabled = resolveThinkingEnabled(raw);
if (effort || summary !== undefined || enabled !== undefined) {
body.reasoning = {
...(effort ? { effort } : {}),
Expand Down
3 changes: 3 additions & 0 deletions src/images/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ function loopFailure(e: LoopError): SidecarLoopFailure {
export interface ImageBridgeDeps {
thinkingRecovery?: ReturnType<typeof createAnthropicThinkingRecovery>;
parsed: OcxParsedRequest;
/** Chat delivery only; never copied into parsed generation options. */
chatRawReasoningVisible?: boolean;
clientToolCatalogRestriction?: ClientToolCatalogRestriction;
adapter: ProviderAdapter;
incomingMeta: IncomingMeta;
Expand Down Expand Up @@ -1213,6 +1215,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
...(deps.forceEmptyResponseId ? { responseId: "" } : {}),
...(deps.terminalRefusalAsFailed ? { terminalRefusalAsFailed: true } : {}),
hideThinkingSummary: parsed.options.hideThinkingSummary,
chatRawReasoningVisible: deps.chatRawReasoningVisible,
hideRawReasoning: parsed.options.hideRawReasoning,
stallTimeoutSec: deps.stallTimeoutSec,
...(deps.onFirstOutput ? { onFirstOutput: deps.onFirstOutput } : {}),
Expand Down
4 changes: 3 additions & 1 deletion src/protocols/encoders/adapter-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ export interface ClientEncodeHooks {
export interface AdapterEventEncodeOptions {
translatorBudget: TranslatorBudget;
hideThinkingSummary?: boolean;
/** Chat client delivery only, independent of upstream thinking/summary configuration. */
chatRawReasoningVisible?: boolean;
/** Provider policy: suppress raw content-channel reasoning, keep provider-authored summaries. */
hideRawReasoning?: boolean;
toolNsMap?: ReadonlyMap<string, { namespace: string; name: string; freeform?: true }>;
Expand Down Expand Up @@ -640,7 +642,7 @@ export function encodeAdapterEventStream(
break;
}
case "reasoning_raw_delta": {
if (options.hideThinkingSummary || options.hideRawReasoning) {
if (!(options.chatRawReasoningVisible ?? !options.hideThinkingSummary) || options.hideRawReasoning) {
hiddenRawBytes += Buffer.byteLength(event.text);
break;
}
Expand Down
4 changes: 4 additions & 0 deletions src/server/chat-completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { FORWARD_HEADERS } from "../adapters/openai-responses";
import {
assertChatCompletionsRoutingBody,
chatRawReasoningVisible,
ChatCompletionsRequestError,
} from "../chat/inbound";
import { chatSettingsBody, chatToResponsesBody } from "../protocols/codecs/chat";
Expand Down Expand Up @@ -177,6 +178,8 @@ async function handleChatCompletionsWithBudget(
const droidDefaultEffort = effortRow
? undefined
: droidReasoningDefault(req.headers.get(DROID_DEFAULT_EFFORT_HEADER), chatBody);
// Read the caller's delivery choice before any generation default (Droid effort) is written.
const rawReasoningVisible = chatRawReasoningVisible(chatBody);
if (effortRow) chatBody.model = effortRow.baseId;
if (fastRow) {
chatBody.model = fastRow.baseId;
Expand Down Expand Up @@ -518,6 +521,7 @@ async function handleChatCompletionsWithBudget(
abortSignal: req.signal,
// Body is Responses-shaped by now, but the client spoke Chat Completions.
inboundWire: "chat",
chatRawReasoningVisible: rawReasoningVisible,
// A Chat caller may send an explicit empty `tools` array — a deny-all declaration the
// translation drops along with the whole catalog. Carried separately so the
// undeclared-tool guard can still enforce it; absent/partial catalogs stay deferred.
Expand Down
1 change: 1 addition & 0 deletions src/server/inference/client-encoder-delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export interface ClientEncodedDelivery {
fold: {
replayCacheScope?: OcxReasoningReplayScopeRef;
hideThinkingSummary?: boolean;
chatRawReasoningVisible?: boolean;
hideRawReasoning?: boolean;
toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
declaredToolNames?: ReadonlySet<string>;
Expand Down
3 changes: 3 additions & 0 deletions src/server/responses/adapter-delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export async function deliverAdapterResponse(
fold: {
replayCacheScope: parsed._reasoningReplayScope,
hideThinkingSummary: parsed.options.hideThinkingSummary,
chatRawReasoningVisible: options.chatRawReasoningVisible,
hideRawReasoning: parsed.options.hideRawReasoning,
toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, bareCustomToolNames, toolSearchToolNames,
},
Expand All @@ -258,6 +259,7 @@ export async function deliverAdapterResponse(
stallTimeoutSec: config.stallTimeoutSec,
localUpstream,
hideThinkingSummary: parsed.options.hideThinkingSummary,
chatRawReasoningVisible: options.chatRawReasoningVisible,
hideRawReasoning: parsed.options.hideRawReasoning,
declaredToolNames,
bareCustomToolNames,
Expand Down Expand Up @@ -365,6 +367,7 @@ export async function deliverAdapterResponse(
translatorBudget,
replayCacheScope: parsed._reasoningReplayScope,
hideThinkingSummary: parsed.options.hideThinkingSummary,
chatRawReasoningVisible: options.chatRawReasoningVisible,
hideRawReasoning: parsed.options.hideRawReasoning,
toolNsMap,
declaredToolNames,
Expand Down
2 changes: 2 additions & 0 deletions src/server/responses/core-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ export interface HandleResponsesOptions {
* returning a Responses body, which the ingress converts as before.
*/
clientEncoder?: ClientEncoderOption;
/** Chat ingress raw-channel delivery preference; never an upstream generation option. */
chatRawReasoningVisible?: boolean;
/** Set only by the Chat or Messages ingress on an eligible Antigravity route; see {@link DirectRequestOption}. */
directRequest?: DirectRequestOption;
}
Expand Down
2 changes: 2 additions & 0 deletions src/server/responses/run-turn-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,7 @@ export async function executeResponsesRunTurn(
...(options.forceEmptyResponseId ? { responseId: "" } : {}),
stallTimeoutSec,
hideThinkingSummary: parsed.options.hideThinkingSummary,
chatRawReasoningVisible: options.chatRawReasoningVisible,
hideRawReasoning: parsed.options.hideRawReasoning,
declaredToolNames,
bareCustomToolNames,
Expand Down Expand Up @@ -1055,6 +1056,7 @@ export async function executeResponsesRunTurn(
translatorBudget,
replayCacheScope: parsed._reasoningReplayScope,
hideThinkingSummary: parsed.options.hideThinkingSummary,
chatRawReasoningVisible: options.chatRawReasoningVisible,
hideRawReasoning: parsed.options.hideRawReasoning,
toolNsMap,
declaredToolNames,
Expand Down
2 changes: 2 additions & 0 deletions src/server/responses/sidecar-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ export async function executeResponsesSidecars(
);
const imgLoopResponse = await runWithImageBridge({
parsed, adapter: transportState.adapter,
chatRawReasoningVisible: options.chatRawReasoningVisible,
...(transportState.adapter.name === "anthropic" ? { thinkingRecovery } : {}),
clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
incomingMeta: { headers: requestState.selectedForwardHeaders, providerName: route.providerName, abortSignal: options.abortSignal, translatorBudget, ...(options.comboInitialSend ? { providerFetch: imageProviderFetch } : {}), ...sidecarBudget.incomingMeta },
Expand Down Expand Up @@ -662,6 +663,7 @@ export async function executeResponsesSidecars(
})(input, init)) as typeof globalThis.fetch;
const loopResponse = await runWithWebSearch({
parsed, adapter: transportState.adapter,
chatRawReasoningVisible: options.chatRawReasoningVisible,
...(transportState.adapter.name === "anthropic" ? { thinkingRecovery } : {}),
clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
onIterationEnd: sidecarBudget.releaseUnsentHop,
Expand Down
3 changes: 3 additions & 0 deletions src/web-search/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ function loopFailure(e: LoopError): SidecarLoopFailure {
*/
export interface WebSearchLoopDeps {
parsed: OcxParsedRequest;
/** Chat delivery only; never copied into parsed generation options. */
chatRawReasoningVisible?: boolean;
clientToolCatalogRestriction?: ClientToolCatalogRestriction;
adapter: ProviderAdapter;
incomingMeta: IncomingMeta;
Expand Down Expand Up @@ -1209,6 +1211,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
...(deps.forceEmptyResponseId ? { responseId: "" } : {}),
...(deps.terminalRefusalAsFailed ? { terminalRefusalAsFailed: true } : {}),
hideThinkingSummary: parsed.options.hideThinkingSummary,
chatRawReasoningVisible: deps.chatRawReasoningVisible,
hideRawReasoning: parsed.options.hideRawReasoning,
...(deps.stallTimeoutSec !== undefined ? { stallTimeoutSec: deps.stallTimeoutSec } : {}),
...(deps.onFirstOutput ? { onFirstOutput: deps.onFirstOutput } : {}),
Expand Down
2 changes: 1 addition & 1 deletion structure/data-planes/protocol-paths.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ attempt is traced with `markAttemptProtocolPath`: the request path is still the
side still decodes through the Responses projection, and the response path is
`[upstream, "ir", inbound]`. When the encoder option carries `requestLane: "direct"` (the
Antigravity direct lane) the request path is `[inbound, "ir", upstream]` and the mode
`translated`.
`translated`. Both paths separate Chat raw delivery from upstream thinking options under [reasoning display parity](../providers/chat-compat.md#reasoning-display-parity-hidethinkingsummary); web-search and image/video sidecar turns retain `responses-internal` delivery with that same preference even when direct encoders are enabled.

## Antigravity direct lane

Expand Down
15 changes: 14 additions & 1 deletion structure/providers/chat-compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ measurement rather than allocating a serialized copy just to measure it.

> Decision record: [ADR-0067](../decisions/ADR-0067-reasoning-display-parity-hidethinkingsummary.md)

`hideThinkingSummary` is set for explicit summary "none", or omitted summary without active thinking. An explicit `thinkingEnabled` boolean determines activity before effort is considered; otherwise accepted minimal/low/medium/high/xhigh/max (including ultra normalized to max)
For Responses delivery, `hideThinkingSummary` is set for explicit summary "none", or omitted summary without active thinking. An explicit `thinkingEnabled` boolean determines activity before effort is considered; otherwise accepted minimal/low/medium/high/xhigh/max (including ultra normalized to max)
allow raw visibility when summary is omitted; none and invalid efforts do not. Explicit "auto"
still permits raw visibility independently of effort. This flag is
honored by BOTH reasoning paths: anthropic `thinking_delta` AND raw `reasoning_raw_delta`
Expand All @@ -479,6 +479,19 @@ grouping evidence: `devlog/_fin/260709_native_response_pattern/`.
Provider policy `hideRawReasoning` hides only this raw channel (summaries keep streaming); it controls
display, not confidentiality ([Responses wire shapes](../transports/responses-wire-shapes.md)).

Chat ingress carries `chatRawReasoningVisible` separately from parsed options: raw reasoning already
produced upstream reaches `delta.reasoning_content` or buffered `message.reasoning_content` by default.
The existing explicit summary-over-`include_reasoning` precedence resolves display none first;
then an explicit thinking/enabled boolean wins over effort, and effort none hides raw text.
Provider `hideRawReasoning` still wins. Both bridge and direct encoders apply this only to the raw
channel; `thinking_delta` keeps its existing summary policy. No default reasoning field is injected
into the upstream body, and Responses and Messages ingress retain their existing behavior.
`src/server/responses/sidecar-execution.ts` forwards the same delivery-only flag through
`WebSearchLoopDeps` and `ImageBridgeDeps` to `src/web-search/loop.ts` and `src/images/loop.ts`.
These sidecar bridges use internal Responses SSE even with direct encoders enabled; Chat folds
that stream for buffered callers. Parsed generation options and upstream bytes remain unchanged.
`tests/responses/chat-raw-reasoning-delivery.test.ts` pins delivery and unchanged upstream bytes.

For models requiring a reasoning placeholder, a preserved thinking-only assistant receives it even without tools, keeping strict DeepSeek continuations valid (#5421). Chat replay policy in `src/adapters/openai-chat/reasoning-replay.ts` honors explicit lists and otherwise recognizes thinking model families (`isThinkingModelFamily`): DeepSeek Flash/Pro (`isDeepSeekThinkingModel`) across versions and gateways, and Reflection/Stealth families (`isReflectionThinkingModel`: `/(?:^|\/)(?:reflection|stealth)(?:$|[-_:/.])/i`). Default replay policy sets `preserve: true` for all thinking model families; operators may override preservation via `provider.preserveReasoningContentModels` or placeholder synthesis via `provider.requiresReasoningPlaceholderModels`. While DeepSeek models scope reasoning replay to tool-enabled turns (`hasTools: true`), Reflection and Stealth families require reasoning replay across all prior assistant turns even when no tools are present (`hasTools: false`), synthesizing whitespace placeholder `[" "]` when `placeholder: true` and cached reasoning is absent. In `src/adapters/openai-chat/messages.ts`, `messagesToChatFormat` checks `isReflectionThinkingModel` to synthesize thinking placeholders when `wireSerializesThinking` is true. Upstream reasoning capture in `src/server/chat-native-reasoning.ts` extracts thinking from both `reasoning_content` and `reasoning` in SSE stream deltas and non-streaming choices. In `src/adapters/openai-chat/passthrough.ts`, `baseBytes` is measured unconditionally against `maxSerializedBytes - replayPlan.serializedBytes` via `jsonUtf8Bytes(transformedBody(), ...)` for both unchanged bodies and changed replays; exceeding budget raises `TranslatorBudgetExceededError("request_copies", maxSerializedBytes)` (HTTP 413), guaranteeing non-replay requests also enforce `maxSerializedBytes` and reserve transient budget. [Native continuation](../data-planes/inbound-compat.md#native-chat-thinking-continuation) shares this replay and byte-accounting policy. Model-contract evidence and compatibility policy: Verification date: 2026-10-25. The Reflection/Stealth family default is a defensive compatibility fallback policy based on observed vendor behavior: live testing against Vercel AI Gateway (`reflection-beam`, 2026-10-23) observed rejection of tool-free multi-turn requests with HTTP 400 (`Missing required parameter: 'messages[12].reasoning_content'`). Because this destination-specific observation is not a vendor guarantee across all possible stealth or reflection hosts, OpenCodex applies this heuristic as a fallback policy while providing explicit operator overrides via `preserveReasoningContentModels` and `requiresReasoningPlaceholderModels`. Continuous CI verification exercises hermetic local fixtures in `tests/responses/chat-native-reasoning-scope.test.ts` (pinning multi-turn non-tool replay, `reasoning` delta capture, and passthrough size checks) without live network requests.

The process-local raw-reasoning fallback is fail-closed unless a request has an explicit client
Expand Down
Loading
Loading