Skip to content

fix(claude): carry a provider's own encrypted reasoning, bound to its minting route (carry #6741) - #6804

Merged
lidge-jun merged 6 commits into
devfrom
codex/l3-claude-native-reasoning
Oct 9, 2026
Merged

lidge-jun merged 6 commits into
devfrom
codex/l3-claude-native-reasoning

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

Maintainer carry of #6741 by @rhomat27, with a stricter replay boundary added before it can merge. Closes #6736.

On the translated Claude Messages → Responses route, a Responses provider's own encrypted_content was never requested (store: false and no include) and was dropped on the way back to Claude Code, because src/claude/outbound.ts only decodes ocxr1: envelopes. Claude Code replays only the thinking block, so the routed model lost its own reasoning every turn. With Muse Spark that shows as long reading phases before the first edit.

The contributor's design is kept. A new nat field in the reasoning envelope (src/responses/reasoning-envelope.ts) carries the provider's blob inside the thinking block's signature, outbound signs it in on both the streaming and JSON paths, and inbound turns it back into a reasoning item when the request names the same model; any other model gets the visible text only. Bodies that reason now send include: ["reasoning.encrypted_content"].

Maintainer changes make replay depend on where a blob came from, not on a model name, because one client-facing name can be served by a combo, a changed route, another account or another endpoint:

  • Each nat blob carries a tag: an HMAC under the installation's persisted replay salt over the minting route's owner (endpoint baseUrl + responsesPath, and a durable credential identity that includes the OAuth or Codex-pool token generation) and a hash of the blob itself. The owner is captured when the serving attempt's upstream accepts the turn, so a failed attempt never names a blob.
  • At every route binding (bindRouteReasoningReplayScope in src/server/responses/core-replay.ts) a restored blob is kept only when its tag verifies for the route about to send. Otherwise it is removed from both carriers, the raw-body reasoning item and the parser's thinking-signature copy, before any adapter builds a body. Removal never comes back on a later failover attempt.
  • Routes without a durable owner (direct-forward, header-only, no salt, no generation) never mint a tag, so their blobs never replay. A refreshed or relinked OAuth token starts a new owner: older blobs stop replaying instead of reaching a different account.
  • Native Anthropic sends (caller-forward passthrough and the managed native path) receive a projection of the Messages body without nat: blob-only thinking blocks are dropped, other proxy envelopes are re-encoded without nat, and undecodable ocxr1: blocks are dropped. The projection reserves its decode and encode copies against the request's translator budget and answers 413 translation_buffer_limit on overflow.
  • structure/clients/claude-desktop.md and structure/transports/responses.md describe the rule; the latter stays at its 600-line budget.

Co-authored-by: rhomat27 8294456+rhomat27@users.noreply.github.com

Verification

  • bun test tests/claude-integration/claude-native-reasoning-provenance.test.ts tests/claude-integration/claude-native-reasoning-continuity.test.ts tests/claude-integration/claude-outbound.test.ts tests/claude-integration/claude-inbound.test.ts tests/responses/reasoning-envelope.test.ts tests/responses/responses-account-change-scrub.test.ts tests/test-layout.test.ts tests/test-layout-tooling.test.ts tests/ci-workflows/file-size-ratchet.test.ts: 272 pass, 0 fail.
  • New tests/claude-integration/claude-native-reasoning-provenance.test.ts (registered in both layout registries) drives handleClaudeMessages with stubbed upstreams through: same route replays (also across a simulated restart); destination, credential and responsesPath switches strip before send; OAuth generation change strips; model switch and transplanted, altered or conflicting tags never replay; direct-forward never mints; streamed failover tags the serving attempt only; a block closing before commit gets no nat; an openai-chat destination gets no blob bytes; both native Anthropic branches get no blob bytes, including escaped-key and undecodable envelopes; budget rejection at the decode and encode boundaries; no include without reasoning. OAuth generation and late-commit cases run at their binding and converter seams.
  • bun run typecheck, bun run structure:check, bun run privacy:scan: pass. No log lines were added.
  • Independent gpt-6-sol security review: PASS after two fixes (translator-budget reservation for the native projection).
  • The full bun run test suite was not run locally (lane constraint: minimal local tests, several worktrees running in parallel). Exact-head hosted CI is the full-suite evidence. The contributor's live recall results in fix(claude): carry a provider's own encrypted reasoning through the Claude route #6741 predate these maintainer changes; the same-route behavior they measured is unchanged.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. Opaque provider state now replays only to the endpoint and credential generation that minted it; tags are HMACs under the existing owner-only replay salt and reveal no credential.

rhomat27 and others added 6 commits October 9, 2026 11:08
…laude route

On the translated Claude Messages route a Responses provider's native
encrypted_content was discarded on the way out, and nothing could carry it
back: the body is store:false and Claude Code replays only the thinking
block. The routed model lost its own reasoning every turn (#6736).

Keep the native blob in the thinking block's ocxr1 envelope as `nat`, with
the client-facing model and the provider's item id, and emit that block even
when no summary arrived. Inbound restores it as encrypted_content and id
only for a request naming the same model; any other model gets the visible
text alone. Translated bodies that reason now request
include: ["reasoning.encrypted_content"], as Codex does.

Refs #6736

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner October 9, 2026 02:27
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-09T02:34:16.867015Z 4b6cf53 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 1 minute.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 578ac7fd-4256-4727-a274-0d4d1a9ddd4b
📥 Commits

Reviewing files that changed from the base of the PR and between fd20320 and 4b6cf53.

📒 Files selected for processing (16)
  • scripts/test-layout/layout.json
  • src/claude/inbound.ts
  • src/claude/outbound.ts
  • src/responses/reasoning-envelope.ts
  • src/responses/reasoning-replay-cache.ts
  • src/server/claude-messages.ts
  • src/server/responses/core-options.ts
  • src/server/responses/core-replay.ts
  • src/server/responses/request-prepare.ts
  • src/server/responses/response-effects.ts
  • src/types/request.ts
  • structure/clients/claude-desktop.md
  • structure/transports/responses.md
  • tests/claude-integration/claude-native-reasoning-continuity.test.ts
  • tests/claude-integration/claude-native-reasoning-provenance.test.ts
  • tests/fixtures/test-layout-expected.json
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@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: 4b6cf535a8

ℹ️ 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 src/claude/outbound.ts
Comment on lines +473 to +476
const signature = open.reasoningSig ?? encodeReasoningEnvelope({
txt: open.thinkingBuf ?? "",
...(tag ? { nat: { ...native, model, tag } } : {}),
}, translatorBudget);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bind native reasoning to the effective routed model

The envelope records the client-facing model, not the model that actually produced the ciphertext. For synthetic effort/Fast rows, handleClaudeMessages saves the suffixed selector in requestedModel and then replaces anthropicBody.model with the base ID before translation, so the next turn compares this stored value with the normalized ID in inbound.ts:320 and always drops otherwise-valid continuity. Conversely, a combo or policy can retain the same client selector while switching between two physical models at the same endpoint and credential, allowing the tag and model checks to pass and forwarding one model's blob to another. Carry the effective routed model identity through minting and route verification instead of using the response-display selector.

Useful? React with 👍 / 👎.

Comment thread src/claude/inbound.ts
Comment on lines 553 to +556
body.reasoning = reasoning;
// store:false returns a provider's encrypted reasoning only on request. The outbound
// envelope (`nat`) carries it to the client so the next turn can hand it back.
body.include = ["reasoning.encrypted_content"];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove encrypted-content include when reasoning is stripped

When the selected model has an explicitly empty effort ladder, claude-messages.ts:1158-1161 deletes internalBody.reasoning as a safety valve, but this new include remains on the raw body and is forwarded by an openai-responses route. The resulting request asks for reasoning.encrypted_content despite no longer requesting reasoning, defeating the no-reasoning-model guard and potentially causing strict Responses-compatible endpoints to reject the unsupported parameter. Delete the include alongside reasoning, or add it only after final route capability resolution.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants