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
7 changes: 7 additions & 0 deletions src/server/responses-reasoning-summary-rewrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ function reasoningTextOf(item: Record<string, unknown>): string {
/** Move a reasoning item's content channel into the summary channel. */
function reasoningItemToSummaryShape(item: Record<string, unknown>): Record<string, unknown> {
if (item.type !== "reasoning") return item;
// `encrypted_content` is opaque, state-bearing provider data, so the entire item must retain its
// upstream shape unless that backend has an explicit replay contract permitting a rewrite. This
// defensively protects content-channel backends that do issue blobs when the client replays the
// stored item. The delta rewrite can still provide the expandable trace for the live turn.
// DeepSeek — the provider this rewrite was verified against — is `statelessResponses` and issues
// no blob, so it is unaffected.
if (typeof item.encrypted_content === "string" && item.encrypted_content.length > 0) return item;
const text = reasoningTextOf(item);
// Items that already use the summary channel (or carry no content text at
// all) are left untouched: rewriting them could clear a valid summary.
Expand Down
10 changes: 10 additions & 0 deletions structure/04_transports-and-sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,16 @@ Codex app, so tool cells group like native models — while the text still round
`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.

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
identity. API-key material is represented only by a process-keyed HMAC; OAuth replay is bound to the
Expand Down
46 changes: 46 additions & 0 deletions tests/responses-reasoning-summary-rewrite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,52 @@ describe("responses reasoning summary channel rewrite", () => {
expect(rewrite("not json")).toBe("not json");
expect(rewrite("[1,2]")).toBe("[1,2]");
});

// `encrypted_content` is opaque, state-bearing provider data, so preserve the complete item
// shape defensively when the client replays it. This rewrite's round-trip was verified against
// DeepSeek, which is stateless and issues no blob; providers that do issue one joined later
// through `preserveReasoningContentModels`.
describe("items carrying encrypted_content", () => {
const blobItem = {
type: "reasoning",
id: "rs_1",
status: "completed",
encrypted_content: "gAAAAAB-upstream-issued-blob",
content: [{ type: "reasoning_text", text: "thinking" }],
summary: [],
};

test("are returned byte-for-byte on output_item.done", () => {
const payload = { type: "response.output_item.done", output_index: 0, item: blobItem };
expect(apply(payload)).toEqual(payload);
});

test("are returned byte-for-byte inside response.completed output", () => {
const payload = {
type: "response.completed",
response: { id: "resp_1", output: [blobItem] },
};
expect(apply(payload)).toEqual(payload);
});

test("are returned byte-for-byte through the non-streaming document rewrite", () => {
const doc = { id: "resp_1", object: "response", output: [blobItem] };
expect(rewriteReasoningSummaryInJson(doc)).toBe(doc);
const json = JSON.stringify(doc);
expect(rewriteReasoningSummaryInJsonString(json)).toBe(json);
});

// Only the stored item is protected: the live trace Codex renders comes from the delta events,
// which carry no blob and are still routed to the summary channel.
test("do not disable the delta rewrite that renders the live trace", () => {
expect(apply({
type: "response.reasoning_text.delta",
delta: "think",
item_id: "rs_1",
output_index: 0,
})).toMatchObject({ type: "response.reasoning_summary_text.delta", delta: "think" });
});
});
});

describe("routeUsesContentChannelReasoning", () => {
Expand Down
Loading