Skip to content

feat(proxy): carry prior-turn reasoning through to the chat upstream - #158

Merged
orangeboyChen merged 3 commits into
mainfrom
feat/reasoning-roundtrip
Sep 17, 2026
Merged

orangeboyChen merged 3 commits into
mainfrom
feat/reasoning-roundtrip

Conversation

@orangeboyChen

@orangeboyChen orangeboyChen commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Anthropic clients echo thinking blocks back on later turns, and the signature on each block is what lets the server recover the reasoning behind it. Codex replays Responses reasoning items for the same reason. We dropped both, so a client following either contract lost its prior reasoning as soon as it sent a second request.

We are the API as far as our clients are concerned, so round-tripping reasoning is our job.

What changed

  • Anthropic (/v1/messages): inbound thinking / redacted_thinking blocks are recovered and sent upstream on the assistant message they belong to, as reasoning — the field the CodeBuddy chat upstream round-trips. Previously they were dropped. redacted_thinking is now matched explicitly; without that branch it fell through to stringifyContent and the model received a JSON dump of the opaque payload as if it were user prose.
  • Responses (/v1/responses): reasoning items no longer become empty user turns. They had neither role nor content, so the fallback produced {"role":"user","content":""} — a turn the user never sent, repeated and accumulated on every subsequent turn. Replayed reasoning now attaches to the assistant turn it accompanies.
  • Outbound: reasoning items are emitted with an id and encrypted_content, in both streaming and non-streaming paths. Previously the only reasoning we handed back was a transient reasoning_text.delta, which no client can replay because it has no id.
  • Sessions: previous_response_id continuations persist reasoning, so a client that does not replay output keeps its reasoning.

Reasoning is carried in the clear, not encrypted

The reasoning here is the upstream's own summary, and it already travels in the clear in the thinking field beside the signature. Encrypting a second copy would protect nothing while adding a module, a key dependency, and a failure mode (a deployment without the secret silently changing behaviour).

Values we minted are marked with a cbreason1: prefix. Not a security measure — it is what stops us reading a genuinely encrypted blob (an Anthropic signature, an OpenAI-issued blob) as reasoning text and forwarding ciphertext upstream. Unmarked values fall back to the summary, and are covered by tests.

Verified against the CodeBuddy client bundled in WorkBuddy.app: it sends prior-turn reasoning as a plain assistant.reasoning string and reads it back from message.reasoning, so the field name matches what the upstream already speaks.

Verification

  • tsc --noEmit and eslint clean.
  • Full suite: 736 passed / 32 files.
  • 14 new tests cover both paths end to end, asserting on the real upstream request body: emitted thinking blocks, recovered reasoning reaching the upstream, empty-turn and redacted_thinking regressions, streamed reasoning items and their ordering, session persistence, and the ciphertext-not-forwarded cases.

Caveat

Whether CodeBuddy accepts an inbound assistant reasoning field was not confirmed against the live endpoint — no credentials were available. It is inferred from the field its own client sends. Worth watching upstream errors after deploy.

Anthropic clients echo thinking blocks back on later turns, and the
`signature` on each block is what lets the server recover the reasoning
behind it. Codex replays Responses reasoning items for the same reason.
We dropped both, so a client following either contract lost its prior
reasoning as soon as it sent a second request.

We are the API as far as our clients are concerned, so mint and verify
these ourselves rather than waiting on Anthropic:

- Add `reasoning-seal`, sealing reasoning into an opaque, tamper-evident
  blob with aes-256-gcm. Reuses the storage encryption secret, and falls
  back to a plain encoding when it is unset so an unconfigured deployment
  still proxies successfully. Unsealing never throws — an unopenable
  value degrades to "no prior reasoning" instead of failing the request.

- Emit the signature on outbound thinking blocks (streamed as a
  `signature_delta` just before the block closes) and on outbound
  Responses reasoning items, so clients have something replayable.

- Recover inbound reasoning and send it upstream on the assistant
  message it belongs to, as `reasoning` — the field the CodeBuddy chat
  upstream round-trips, and the one its own client populates when it
  replays a response.

- Handle `redacted_thinking`, which previously fell through to
  `stringifyContent` and reached the model as a JSON dump of the opaque
  payload.

- Stop turning replayed Responses reasoning items into empty user turns.
  They had neither `role` nor `content`, so the fallback produced
  `{role:'user', content:''}` — a turn the user never sent, repeated and
  accumulated on every subsequent turn.

The signature is preferred but never a gate: a client may replay a
genuine Anthropic signature from a session started elsewhere, and that
should fall back to the summary rather than discard good reasoning.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f8e39b304

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/server/proxy/responses.ts
Comment thread lib/server/proxy/responses.ts Outdated
@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.01493% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.52%. Comparing base (8edfe8a) to head (c925528).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #158      +/-   ##
==========================================
+ Coverage   95.41%   95.52%   +0.11%     
==========================================
  Files          37       37              
  Lines        6585     6706     +121     
  Branches     1899     1933      +34     
==========================================
+ Hits         6283     6406     +123     
+ Misses        302      300       -2     
Flag Coverage Δ
unittests 95.52% <97.01%> (+0.11%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The reasoning we hand to clients is the upstream's own summary, and it
already travels in the clear in the `thinking` field beside the
signature. Encrypting a second copy protected nothing — the plaintext
was right there — while adding a module, a key dependency, and a failure
mode (a deployment without the secret silently changed behaviour).

Drop `reasoning-seal` and pass the reasoning through as-is:

- Anthropic path: thinking blocks carry their text in `thinking`, which
  is what clients echo back and what inbound handling reads. No
  signature is minted, because a replayable one would have to duplicate
  the text — and that duplication is not hypothetical: emitting it made
  the multi-hop server-tool tests see the reasoning twice.

- Responses path: `encrypted_content` carries the reasoning verbatim.
  Codex treats the field as an opaque string it never opens, so
  plaintext round-trips exactly as well.

- Values we minted are marked with a `cbreason1:` prefix. Not a
  security measure — it is what stops us reading a genuinely encrypted
  blob (an Anthropic signature, an OpenAI-issued blob) as reasoning text
  and forwarding ciphertext upstream. Unmarked values fall back to the
  summary.
@orangeboyChen
orangeboyChen enabled auto-merge (squash) September 17, 2026 11:57
…e merge

CI's patch-branch gate sits at 90%; the previous commit left the changed
lines at 89.06% (57/64). Cover the branches that were missing:

- Anthropic: several thinking blocks in one assistant turn (interleaved
  thinking puts one before each tool call) join in order.
- Responses: a reasoning item attaching to the following assistant
  message; summaries holding bare strings; summary entries with no text;
  a non-string `encrypted_content` falling back to the summary; and one
  reasoning item emitted for many reasoning deltas rather than one per
  delta.

Also drop the merge that combined a carried reasoning with a `reasoning`
field already on the assistant message. `ResponsesInputItem` has no such
field, so a client cannot send it and the branch was unreachable — the
mapper is the only thing that sets it. Replaced with a plain assignment.

Changed-branch coverage is now 98.44% (63/64).
@orangeboyChen
orangeboyChen merged commit 12f98c1 into main Sep 17, 2026
7 checks passed
@orangeboyChen
orangeboyChen deleted the feat/reasoning-roundtrip branch September 17, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant