Skip to content
Closed
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
Binary file added .github/pr-assets/raw-reasoning-band.mov
Binary file not shown.
32 changes: 32 additions & 0 deletions devlog/_fin/260911_raw_reasoning_content_channel/010_record.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 260911 — Raw reasoning rides the content channel

## Problem

Codex desktop renders the one-line animated thinking band from the Responses summary channel.
Commit 318315450 (issue #45) routed every raw `reasoning_raw_delta` into that summary channel so
non-OpenAI providers got an "expandable" trace, but the summary text there is the model's raw
chain of thought — GLM/DeepSeek/Grok chat streams scrolled unsummarized CoT through the band,
which only looks right for native OpenAI providers that author real summaries.

## Change

- `src/bridge.ts`: visible raw reasoning now streams on the CONTENT channel
(`response.reasoning_text.delta`, `content_index: 0`) and the final reasoning item carries
`content: [{type: "reasoning_text", text}]` with an empty `summary` — the native gpt-oss shape
documented in `100_codex-native-parity/51_raw-reasoning-bridge`. Codex applies its own display
policy: the desktop band shows the "Thinking…" placeholder, and the CLI still gates raw display
behind `show_raw_agent_reasoning`.
- Deleted the content-to-summary payload rewrite
(`src/server/responses-reasoning-summary-rewrite.ts`). Its only purpose was that display;
native Responses passthrough (DeepSeek) now round-trips content-channel reasoning unchanged,
which the upstream already accepts.
- Hidden mode (`hideThinkingSummary`, summary absent/"none") is unchanged: envelope-only item with
txt-only `ocxr1:` round-trip for `preserveReasoningContentModels` replay.
- Claude/kiro SIGNED `thinking_delta` visible mode is intentionally unchanged (summary channel);
the same content-channel treatment is a possible follow-up.

## Verification

- `bun run typecheck`; focused bridge, raw-reasoning, replay, xAI, web-search, and layout tests.
- The two web-search tests that fail only in multi-file batch runs fail identically on the
pre-change tree (pre-existing cross-file contamination, not caused here).
1 change: 0 additions & 1 deletion scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -1091,7 +1091,6 @@
"responses-pool-401-refresh.test.ts": "responses",
"responses-pool-refresh-attribution.test.ts": "responses",
"responses-reasoning-summary-passthrough.test.ts": "responses",
"responses-reasoning-summary-rewrite.test.ts": "responses",
"responses-routed-web-search-fields.test.ts": "responses",
"responses-self-named-namespace-scrub.test.ts": "responses",
"responses-shadow-intercept.test.ts": "responses",
Expand Down
26 changes: 12 additions & 14 deletions src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,16 +663,13 @@ export function bridgeToResponsesSSE(
const closeCurrentRawReasoning = () => {
if (!currentRawReasoning) return;
rawReasoningForNextToolCall = currentRawReasoning.text;
emit("response.reasoning_summary_text.done", {
item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0, text: currentRawReasoning.text,
});
emit("response.reasoning_summary_part.done", {
item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0,
part: { type: "summary_text", text: currentRawReasoning.text },
emit("response.reasoning_text.done", {
item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, content_index: 0, text: currentRawReasoning.text,
});
const item = {
type: "reasoning", id: currentRawReasoning.itemId,
summary: [{ type: "summary_text", text: currentRawReasoning.text }],
summary: [] as never[],
content: [{ type: "reasoning_text", text: currentRawReasoning.text }],
};
emit("response.output_item.done", { output_index: currentRawReasoning.outputIndex, item });
retainFinishedItem(item as OutputItem, currentRawReasoning.textBytes, "reasoning");
Expand Down Expand Up @@ -1111,10 +1108,6 @@ export function bridgeToResponsesSSE(
const itemId = `rs_${uuid()}`;
const item = { type: "reasoning", id: itemId, summary: [] as { type: string; text: string }[] };
emit("response.output_item.added", { output_index: outputIndex, item });
emit("response.reasoning_summary_part.added", {
item_id: itemId, output_index: outputIndex, summary_index: 0,
part: { type: "summary_text", text: "" },
});
currentRawReasoning = { itemId, outputIndex, text: "", textBytes: 0 };
}
({ value: currentRawReasoning.text, bytes: currentRawReasoning.textBytes } = appendString(
Expand All @@ -1123,9 +1116,13 @@ export function bridgeToResponsesSSE(
event.text,
"reasoning",
));
emit("response.reasoning_summary_text.delta", {
// Raw reasoning (openai-chat reasoning_content, kiro tags) rides the CONTENT
// channel, matching native gpt-oss passthrough: Codex applies its own display
// policy, so the desktop band shows the "Thinking…" placeholder instead of the
// raw CoT (the #45 summary-channel display intent is intentionally reverted).
emit("response.reasoning_text.delta", {
item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex,
summary_index: 0, delta: event.text,
content_index: 0, delta: event.text,
});
break;
}
Expand Down Expand Up @@ -1780,7 +1777,8 @@ function buildResponseJSONWithBudget(
}
pushOutput({
type: "reasoning", id: `rs_${uuid()}`,
summary: [{ type: "summary_text", text: currentRawReasoning }],
summary: [],
content: [{ type: "reasoning_text", text: currentRawReasoning }],
}, currentRawReasoningBytes, "reasoning");
currentRawReasoning = "";
currentRawReasoningBytes = 0;
Expand Down
178 changes: 0 additions & 178 deletions src/server/responses-reasoning-summary-rewrite.ts

This file was deleted.

23 changes: 2 additions & 21 deletions src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,12 +362,6 @@ import {
hasResponsesItemIdRepair,
repairResponsesJsonItemIds,
} from "../responses-item-id-repair";
import {
createReasoningSummaryChannelPayloadRewrite,
rewriteReasoningSummaryInJson,
rewriteReasoningSummaryInJsonString,
routeUsesContentChannelReasoning,
} from "../responses-reasoning-summary-rewrite";
import {
createImageGenCallRestoreRewrite,
imageGenToolCallAliases,
Expand Down Expand Up @@ -4978,10 +4972,7 @@ async function handleResponsesInner(
? JSON.parse(normalizeFunctionCompletionJson(JSON.stringify(restored)))
: restored) as { id?: unknown; output?: unknown; status?: unknown };
// Replay overlap compares the items the client echoes, including visible reasoning shape.
const replayResponse = parsed.options.hideThinkingSummary !== true
&& routeUsesContentChannelReasoning(route.provider, route.modelId)
? rewriteReasoningSummaryInJson(restoredResponse) as typeof restoredResponse
: restoredResponse;
const replayResponse = restoredResponse;
if (
undeclaredToolGuardActive
&& undeclaredToolCallNameInResponse(
Expand Down Expand Up @@ -5975,10 +5966,6 @@ async function handleResponsesInner(
? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
: undefined,
responseModelRewrite,
parsed.options.hideThinkingSummary !== true
&& routeUsesContentChannelReasoning(route.provider, route.modelId)
? createReasoningSummaryChannelPayloadRewrite()
: undefined,
].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
// #893: sparse-snapshot gateways get field backfills AND lifecycle event
// injection at the block level, after payload rewrites. Defaults come
Expand Down Expand Up @@ -6249,13 +6236,7 @@ async function handleResponsesInner(
const modelRewritten = parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId
? rewriteResponsesModelJson(repaired, parsed._responseModelId)
: repaired;
// The bounded-JSON answer bypasses the SSE payload rewrite, so content-
// channel reasoning needs the same normalization here for the plain
// JSON answer and every reframed-SSE variant built from clientJson.
return parsed.options.hideThinkingSummary !== true
&& routeUsesContentChannelReasoning(route.provider, route.modelId)
? rewriteReasoningSummaryInJsonString(modelRewritten)
: modelRewritten;
return modelRewritten;
})();
// #1700: same fail-closed policy as the SSE relay above. Both the plain JSON answer and
// the reframed-SSE branch below are built from this body, so one check covers them. This
Expand Down
19 changes: 7 additions & 12 deletions structure/providers/chat-compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,18 +215,13 @@ honored by BOTH reasoning paths: anthropic `thinking_delta` AND raw `reasoning_r
item (`summary: []`, txt-only `ocxr1:` `encrypted_content`, no text deltas) — invisible in the
Codex app, so tool cells group like native models — while the text still round-trips for
`preserveReasoningContentModels` replay. Visible mode (summary "auto") keeps the raw
`content[reasoning_text]` shape. Diagnosis and codex-rs grouping evidence:
`devlog/_fin/260709_native_response_pattern/`.

The content-to-summary channel rewrite skips any reasoning item that carries a native
`encrypted_content` blob. The blob is opaque, state-bearing provider data, so the item must
round-trip unchanged unless that backend has an explicit replay contract permitting a rewrite.
This defensively protects providers that issue blobs and later join the route through
`preserveReasoningContentModels`. The rewrite's round trip was verified against DeepSeek, which is
`statelessResponses` and issues no blob. Grok is unaffected in practice because it natively emits
summary-channel reasoning and no `reasoning_text` events, so this content-to-summary item rewrite
does not engage on its route. Only the stored item is exempt — `reasoning_text` delta events carry
no blob and still route to the summary channel, so the live expandable trace is unchanged.
`content[reasoning_text]` shape: raw deltas stream as `response.reasoning_text.delta` and the final
item carries `content: [{type: "reasoning_text", text}]`, so Codex applies its own display policy —
the desktop thinking band shows the "Thinking…" placeholder, and raw text appears only when
`show_raw_agent_reasoning` is enabled. Routing raw CoT through the summary channel instead (the
#45 display intent, intentionally reverted 260911) put unsummarized thinking in the desktop band,
which only fits native OpenAI providers that author real summaries. Diagnosis and codex-rs
grouping evidence: `devlog/_fin/260709_native_response_pattern/`.

The process-local raw-reasoning fallback is fail-closed unless a request has an explicit client
thread plus an exact provider destination, wire adapter, final model, and physical credential
Expand Down
Loading
Loading