Repository navigation
fix(claude): carry a provider's own encrypted reasoning, bound to its minting route (carry #6741) #6804
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(claude): carry a provider's own encrypted reasoning, bound to its minting route (carry #6741) #6804
Changes from all commits
db0a7dd
30f075a
7bd7a66
aad6f48
1ef3b1f
4b6cf53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,7 @@ import { | |
| type TranslatorBudget, | ||
| } from "../lib/translator-budget"; | ||
| import { sseFieldOffset, sseFieldValue } from "../lib/sse-decoder"; | ||
| import { decodeReasoningEnvelope, encodeReasoningEnvelope } from "../responses/reasoning-envelope"; | ||
| import { decodeReasoningEnvelope, encodeReasoningEnvelope, OCX_REASONING_PREFIX } from "../responses/reasoning-envelope"; | ||
|
|
||
| type Rec = Record<string, unknown>; | ||
|
|
||
|
|
@@ -339,6 +339,13 @@ interface OpenBlock { | |
| thinkingBuf?: string; | ||
| thinkingBufBytes?: number; | ||
| reasoningSig?: string; | ||
| /** The provider's own encrypted reasoning for this item; signed into the block on close. */ | ||
| reasoningNative?: { enc: string; id?: string }; | ||
| } | ||
|
|
||
| /** A provider-minted (not ocxr1) `encrypted_content`, or `""`. */ | ||
| function nativeEncryptedReasoning(encrypted: string): string { | ||
| return encrypted.length > 0 && !encrypted.startsWith(OCX_REASONING_PREFIX) ? encrypted : ""; | ||
| } | ||
|
|
||
| /** Streaming: Responses SSE bytes -> Anthropic Messages SSE bytes. */ | ||
|
|
@@ -353,6 +360,7 @@ export function responsesSseToAnthropicSse( | |
| * upstream sent no confirmed usage before the first frame. See `messageSnapshot` (#4857). | ||
| */ | ||
| inputTokenFloor?: number; | ||
| nativeReasoningTagFor?: (blob: string) => string | undefined; | ||
| }, | ||
| ): ReadableStream<Uint8Array> { | ||
| const translatorBudget = opts.translatorBudget; | ||
|
|
@@ -441,6 +449,13 @@ export function responsesSseToAnthropicSse( | |
| open.webSearchArgsEmitted = true; | ||
| } | ||
| if (open.kind === "thinking") { | ||
| const native = open.reasoningNative; | ||
| const tag = native && opts.nativeReasoningTagFor?.(native.enc); | ||
| if (!open.thinkingBuf && !open.reasoningSig && !tag) { | ||
| releaseThinkingBuffer(open); | ||
| open = null; | ||
| return; | ||
| } | ||
| // Delay the index and all thinking frames until closure so a matching | ||
| // done envelope can put its redacted blocks first. The existing buffer | ||
| // remains charged through signature emission, including queued frames. | ||
|
|
@@ -455,7 +470,10 @@ export function responsesSseToAnthropicSse( | |
| delta: { type: "thinking_delta", thinking: open.thinkingBuf }, | ||
| }); | ||
| } | ||
| const signature = open.reasoningSig ?? encodeReasoningEnvelope({ txt: open.thinkingBuf ?? "" }, translatorBudget); | ||
| const signature = open.reasoningSig ?? encodeReasoningEnvelope({ | ||
| txt: open.thinkingBuf ?? "", | ||
| ...(tag ? { nat: { ...native, model, tag } } : {}), | ||
| }, translatorBudget); | ||
|
Comment on lines
+473
to
+476
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The envelope records the client-facing Useful? React with 👍 / 👎. |
||
| emit("content_block_delta", { | ||
| type: "content_block_delta", index: open.index, | ||
| delta: { type: "signature_delta", signature }, | ||
|
|
@@ -743,11 +761,15 @@ export function responsesSseToAnthropicSse( | |
| emit("content_block_start", { type: "content_block_start", index: idx, content_block: { type: "redacted_thinking", data } }); | ||
| emit("content_block_stop", { type: "content_block_stop", index: idx }); | ||
| } | ||
| if (env?.sig && open?.kind !== "thinking") { | ||
| // A provider's own blob is the only copy of its reasoning the client can hold, so | ||
| // it gets a thinking block even when no summary text arrived. | ||
| const native = nativeEncryptedReasoning(encrypted); | ||
| if ((env?.sig || native) && open?.kind !== "thinking") { | ||
| ensureBlock("thinking"); | ||
| } | ||
| if (open?.kind === "thinking") { | ||
| if (env?.sig) open.reasoningSig = env.sig; | ||
| if (native) open.reasoningNative = typeof item.id === "string" && item.id.length > 0 ? { enc: native, id: item.id } : { enc: native }; | ||
| closeOpenBlock(); | ||
| } | ||
| } | ||
|
|
@@ -940,7 +962,7 @@ export function responsesSseToAnthropicSse( | |
| } | ||
|
|
||
| /** Non-streaming: /v1/responses JSON -> Anthropic message JSON. */ | ||
| export function responsesJsonToAnthropicMessage(json: unknown, model: string, translatorBudget?: TranslatorBudget): Rec { | ||
| export function responsesJsonToAnthropicMessage(json: unknown, model: string, translatorBudget?: TranslatorBudget, nativeReasoningTagFor?: (blob: string) => string | undefined): Rec { | ||
| const body = isRec(json) ? json : {}; | ||
| const output = Array.isArray(body.output) ? body.output : []; | ||
| const content: Rec[] = []; | ||
|
|
@@ -976,10 +998,14 @@ export function responsesJsonToAnthropicMessage(json: unknown, model: string, tr | |
| // Legacy combined envelopes place redacted blocks before the signed block, | ||
| // matching the Anthropic adapter. New bridge output uses separate items. | ||
| for (const data of env?.red ?? []) content.push({ type: "redacted_thinking", data }); | ||
| // env.txt may be locally hidden text. Do not expose it here or manufacture | ||
| // a new signed continuity carrier; hidden-summary replay remains limited. | ||
| if (parts.length > 0 || env?.sig) { | ||
| content.push({ type: "thinking", thinking: parts.join("\n\n"), signature: env?.sig ?? encodeReasoningEnvelope({ txt: parts.join("\n\n") }, translatorBudget) }); | ||
| // env.txt may be locally hidden text. Do not expose it here. A provider's own blob is | ||
| // carried in the envelope so the next request can hand it back (see inbound.ts). | ||
| const native = nativeEncryptedReasoning(encrypted); | ||
| const tag = native && nativeReasoningTagFor?.(native); | ||
| if (parts.length > 0 || env?.sig || tag) { | ||
| const text = parts.join("\n\n"); | ||
| const nat = tag ? { enc: native, model, tag, ...(typeof raw.id === "string" && raw.id.length > 0 ? { id: raw.id } : {}) } : undefined; | ||
| content.push({ type: "thinking", thinking: text, signature: env?.sig ?? encodeReasoningEnvelope({ txt: text, ...(nat ? { nat } : {}) }, translatorBudget) }); | ||
| } | ||
| break; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the selected model has an explicitly empty effort ladder,
claude-messages.ts:1158-1161deletesinternalBody.reasoningas a safety valve, but this newincluderemains on the raw body and is forwarded by anopenai-responsesroute. The resulting request asks forreasoning.encrypted_contentdespite 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 👍 / 👎.