@leanandmean/ai 0.74.1-scramjet.46 → 0.74.1-scramjet.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../src/providers/anthropic.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAW1C,OAAO,KAAK,EASX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAOb,MAAM,aAAa,CAAC;AAqJrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AAsBhE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;OAUG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AAiPD,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CA0RlF,CAAC;AAwCF,eAAO,MAAM,qBAAqB,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CA2C3F,CAAC","sourcesContent":["import Anthropic from \"@anthropic-ai/sdk\";\nimport type {\n\tCacheControlEphemeral,\n\tContentBlockParam,\n\tMessageCreateParamsStreaming,\n\tMessageParam,\n\tRawMessageStreamEvent,\n\tTextBlockParam,\n} from \"@anthropic-ai/sdk/resources/messages.js\";\nimport { getEnvApiKey } from \"../env-api-keys.js\";\nimport { calculateCost } from \"../models.js\";\nimport type {\n\tAnthropicMessagesCompat,\n\tApi,\n\tAssistantMessage,\n\tCacheRetention,\n\tContext,\n\tImageContent,\n\tMessage,\n\tModel,\n\tSimpleStreamOptions,\n\tStopReason,\n\tStreamFunction,\n\tStreamOptions,\n\tSystemPromptSection,\n\tTextContent,\n\tThinkingContent,\n\tTool,\n\tToolCall,\n\tToolResultMessage,\n} from \"../types.js\";\nimport { AssistantMessageEventStream } from \"../utils/event-stream.js\";\nimport {\n\tappendBuiltinFailure,\n\tfailureFromProviderError,\n\tinvokeProviderCallback,\n\tRequestFailureError,\n} from \"../utils/failure-evidence.js\";\nimport { headersToRecord } from \"../utils/headers.js\";\nimport { parseJsonWithRepair, parseStreamingJson } from \"../utils/json-parse.js\";\nimport { sanitizeSurrogates } from \"../utils/sanitize-unicode.js\";\nimport {\n\tADAPTIVE_THINKING_PATTERNS,\n\tNATIVE_XHIGH_EFFORT_PATTERNS,\n\tnormalizeForPatternMatch,\n} from \"./anthropic-model-patterns.js\";\n\nimport { resolveCloudflareBaseUrl } from \"./cloudflare.js\";\nimport { buildCopilotDynamicHeaders, hasCopilotVisionInput } from \"./github-copilot-headers.js\";\nimport { adjustMaxTokensForThinking, buildBaseOptions } from \"./simple-options.js\";\nimport { transformMessages } from \"./transform-messages.js\";\n\n/**\n * Resolve cache retention preference.\n *\n * Defaults to \"short\" and only honors PI_CACHE_RETENTION === \"long\" — this is\n * the low-level provider fallback for callers that hit the adapter directly.\n * In the coding-agent path this default rarely fires: the SDK's own\n * resolveCacheRetention (packages/coding-agent/src/core/sdk.ts, which defaults\n * to \"long\") resolves first and passes a concrete value down, so `cacheRetention`\n * is normally already set when it reaches here. Keep the two defaults in sync\n * intentionally — they differ on purpose (backward-compat vs. SDK default).\n */\nfunction resolveCacheRetention(cacheRetention?: CacheRetention): CacheRetention {\n\tif (cacheRetention) {\n\t\treturn cacheRetention;\n\t}\n\tif (typeof process !== \"undefined\" && process.env.PI_CACHE_RETENTION === \"long\") {\n\t\treturn \"long\";\n\t}\n\treturn \"short\";\n}\n\nfunction getCacheControl(\n\tmodel: Model<\"anthropic-messages\">,\n\tcacheRetention?: CacheRetention,\n): { retention: CacheRetention; cacheControl?: CacheControlEphemeral } {\n\tconst retention = resolveCacheRetention(cacheRetention);\n\tif (retention === \"none\") {\n\t\treturn { retention };\n\t}\n\tconst ttl = retention === \"long\" && getAnthropicCompat(model).supportsLongCacheRetention ? \"1h\" : undefined;\n\treturn {\n\t\tretention,\n\t\tcacheControl: { type: \"ephemeral\", ...(ttl && { ttl }) },\n\t};\n}\n\n// Stealth mode: Mimic Claude Code's tool naming exactly\nconst claudeCodeVersion = \"2.1.75\";\n\n// Claude Code 2.x tool names (canonical casing)\n// Source: https://cchistory.mariozechner.at/data/prompts-2.1.11.md\n// To update: https://github.com/badlogic/cchistory\nconst claudeCodeTools = [\n\t\"Read\",\n\t\"Write\",\n\t\"Edit\",\n\t\"Bash\",\n\t\"Grep\",\n\t\"Glob\",\n\t\"AskUserQuestion\",\n\t\"EnterPlanMode\",\n\t\"ExitPlanMode\",\n\t\"KillShell\",\n\t\"NotebookEdit\",\n\t\"Skill\",\n\t\"Task\",\n\t\"TaskOutput\",\n\t\"TodoWrite\",\n\t\"WebFetch\",\n\t\"WebSearch\",\n];\n\nconst ccToolLookup = new Map(claudeCodeTools.map((t) => [t.toLowerCase(), t]));\n\n// Convert tool name to CC canonical casing if it matches (case-insensitive)\nconst toClaudeCodeName = (name: string) => ccToolLookup.get(name.toLowerCase()) ?? name;\nconst fromClaudeCodeName = (name: string, tools?: Tool[]) => {\n\tif (tools && tools.length > 0) {\n\t\tconst lowerName = name.toLowerCase();\n\t\tconst matchedTool = tools.find((tool) => tool.name.toLowerCase() === lowerName);\n\t\tif (matchedTool) return matchedTool.name;\n\t}\n\treturn name;\n};\n\n/**\n * Convert content blocks to Anthropic API format\n */\nfunction convertContentBlocks(content: (TextContent | ImageContent)[]):\n\t| string\n\t| Array<\n\t\t\t| { type: \"text\"; text: string }\n\t\t\t| {\n\t\t\t\t\ttype: \"image\";\n\t\t\t\t\tsource: {\n\t\t\t\t\t\ttype: \"base64\";\n\t\t\t\t\t\tmedia_type: \"image/jpeg\" | \"image/png\" | \"image/gif\" | \"image/webp\";\n\t\t\t\t\t\tdata: string;\n\t\t\t\t\t};\n\t\t\t }\n\t > {\n\t// If only text blocks, return as concatenated string for simplicity\n\tconst hasImages = content.some((c) => c.type === \"image\");\n\tif (!hasImages) {\n\t\treturn sanitizeSurrogates(content.map((c) => (c as TextContent).text).join(\"\\n\"));\n\t}\n\n\t// If we have images, convert to content block array\n\tconst blocks = content.map((block) => {\n\t\tif (block.type === \"text\") {\n\t\t\treturn {\n\t\t\t\ttype: \"text\" as const,\n\t\t\t\ttext: sanitizeSurrogates(block.text),\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\ttype: \"image\" as const,\n\t\t\tsource: {\n\t\t\t\ttype: \"base64\" as const,\n\t\t\t\tmedia_type: block.mimeType as \"image/jpeg\" | \"image/png\" | \"image/gif\" | \"image/webp\",\n\t\t\t\tdata: block.data,\n\t\t\t},\n\t\t};\n\t});\n\n\t// If only images (no text), add placeholder text block\n\tconst hasText = blocks.some((b) => b.type === \"text\");\n\tif (!hasText) {\n\t\tblocks.unshift({\n\t\t\ttype: \"text\" as const,\n\t\t\ttext: \"(see attached image)\",\n\t\t});\n\t}\n\n\treturn blocks;\n}\n\nexport type AnthropicEffort = \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n\nexport type AnthropicThinkingDisplay = \"summarized\" | \"omitted\";\n\nconst FINE_GRAINED_TOOL_STREAMING_BETA = \"fine-grained-tool-streaming-2025-05-14\";\nconst INTERLEAVED_THINKING_BETA = \"interleaved-thinking-2025-05-14\";\n\nfunction getAnthropicCompat(model: Model<\"anthropic-messages\">): Required<AnthropicMessagesCompat> {\n\t// Auto-detect session affinity and cache control support from provider\n\tconst isFireworks = model.provider === \"fireworks\";\n\tconst isCloudflareAiGatewayAnthropic =\n\t\tmodel.provider === \"cloudflare-ai-gateway\" && model.baseUrl.includes(\"anthropic\");\n\t// SCRAMJET-DIVERGENCE: supportsTemperature and forceAdaptiveThinking defaults added for Opus 4.8/Fable 5/Sonnet 5 support\n\treturn {\n\t\tsupportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? !isFireworks,\n\t\tsupportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? !isFireworks,\n\t\tsendSessionAffinityHeaders:\n\t\t\tmodel.compat?.sendSessionAffinityHeaders ?? !!(isFireworks || isCloudflareAiGatewayAnthropic),\n\t\tsupportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? !isFireworks,\n\t\tsupportsTemperature: model.compat?.supportsTemperature ?? true,\n\t\tforceAdaptiveThinking: model.compat?.forceAdaptiveThinking ?? false,\n\t};\n}\n\nexport interface AnthropicOptions extends StreamOptions {\n\t/**\n\t * Enable extended thinking.\n\t * For adaptive thinking models (Opus 4.6+, Sonnet 4.6+, Fable 5): model decides when/how much to think.\n\t * For older models: uses budget-based thinking with thinkingBudgetTokens.\n\t */\n\tthinkingEnabled?: boolean;\n\t/**\n\t * Token budget for extended thinking (older models only).\n\t * Ignored for adaptive thinking models (Opus 4.6+, Sonnet 4.6+, Fable 5).\n\t */\n\tthinkingBudgetTokens?: number;\n\t/**\n\t * Effort level for adaptive thinking (Opus 4.6+, Sonnet 4.6+, Fable 5).\n\t * Controls how much thinking Claude allocates:\n\t * - \"max\": Always thinks with no constraints (Opus 4.6 only)\n\t * - \"xhigh\": Highest reasoning level (Opus 4.7+, Fable 5)\n\t * - \"high\": Always thinks, deep reasoning (default)\n\t * - \"medium\": Moderate thinking, may skip for simple queries\n\t * - \"low\": Minimal thinking, skips for simple tasks\n\t * Ignored for older models.\n\t */\n\teffort?: AnthropicEffort;\n\t/**\n\t * Controls how thinking content is returned in API responses.\n\t * - \"summarized\": Thinking blocks contain summarized thinking text (default here).\n\t * - \"omitted\": Thinking blocks return an empty thinking field; the encrypted\n\t * signature still travels back for multi-turn continuity. Use for faster\n\t * time-to-first-text-token when your UI does not surface thinking.\n\t *\n\t * Note: Anthropic's API default for Claude Opus 4.7 and Claude Mythos Preview\n\t * is \"omitted\". We default to \"summarized\" here to keep behavior consistent\n\t * with older Claude 4 models. Set this explicitly to \"omitted\" to opt in.\n\t */\n\tthinkingDisplay?: AnthropicThinkingDisplay;\n\tinterleavedThinking?: boolean;\n\ttoolChoice?: \"auto\" | \"any\" | \"none\" | { type: \"tool\"; name: string };\n\t/**\n\t * Pre-built Anthropic client instance. When provided, skips internal client\n\t * construction entirely. Use this to inject alternative SDK clients such as\n\t * `AnthropicVertex` that shares the same messaging API.\n\t */\n\tclient?: Anthropic;\n}\n\nfunction mergeHeaders(...headerSources: (Record<string, string | null> | undefined)[]): Record<string, string | null> {\n\tconst merged: Record<string, string | null> = {};\n\tfor (const headers of headerSources) {\n\t\tif (headers) {\n\t\t\tObject.assign(merged, headers);\n\t\t}\n\t}\n\treturn merged;\n}\n\ninterface ServerSentEvent {\n\tevent: string | null;\n\tdata: string;\n\traw: string[];\n}\n\ninterface SseDecoderState {\n\tevent: string | null;\n\tdata: string[];\n\traw: string[];\n}\n\nconst ANTHROPIC_MESSAGE_EVENTS: ReadonlySet<string> = new Set([\n\t\"message_start\",\n\t\"message_delta\",\n\t\"message_stop\",\n\t\"content_block_start\",\n\t\"content_block_delta\",\n\t\"content_block_stop\",\n]);\n\nfunction flushSseEvent(state: SseDecoderState): ServerSentEvent | null {\n\tif (!state.event && state.data.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst event: ServerSentEvent = {\n\t\tevent: state.event,\n\t\tdata: state.data.join(\"\\n\"),\n\t\traw: [...state.raw],\n\t};\n\tstate.event = null;\n\tstate.data = [];\n\tstate.raw = [];\n\treturn event;\n}\n\nfunction decodeSseLine(line: string, state: SseDecoderState): ServerSentEvent | null {\n\tif (line === \"\") {\n\t\treturn flushSseEvent(state);\n\t}\n\n\tstate.raw.push(line);\n\tif (line.startsWith(\":\")) {\n\t\treturn null;\n\t}\n\n\tconst delimiterIndex = line.indexOf(\":\");\n\tconst fieldName = delimiterIndex === -1 ? line : line.slice(0, delimiterIndex);\n\tlet value = delimiterIndex === -1 ? \"\" : line.slice(delimiterIndex + 1);\n\tif (value.startsWith(\" \")) {\n\t\tvalue = value.slice(1);\n\t}\n\n\tif (fieldName === \"event\") {\n\t\tstate.event = value;\n\t} else if (fieldName === \"data\") {\n\t\tstate.data.push(value);\n\t}\n\n\treturn null;\n}\n\nfunction nextLineBreakIndex(text: string): number {\n\tconst carriageReturnIndex = text.indexOf(\"\\r\");\n\tconst newlineIndex = text.indexOf(\"\\n\");\n\tif (carriageReturnIndex === -1) {\n\t\treturn newlineIndex;\n\t}\n\tif (newlineIndex === -1) {\n\t\treturn carriageReturnIndex;\n\t}\n\treturn Math.min(carriageReturnIndex, newlineIndex);\n}\n\nfunction consumeLine(text: string): { line: string; rest: string } | null {\n\tconst lineBreakIndex = nextLineBreakIndex(text);\n\tif (lineBreakIndex === -1) {\n\t\treturn null;\n\t}\n\n\tlet nextIndex = lineBreakIndex + 1;\n\tif (text[lineBreakIndex] === \"\\r\" && text[nextIndex] === \"\\n\") {\n\t\tnextIndex += 1;\n\t}\n\n\treturn {\n\t\tline: text.slice(0, lineBreakIndex),\n\t\trest: text.slice(nextIndex),\n\t};\n}\n\nasync function* iterateSseMessages(\n\tbody: ReadableStream<Uint8Array>,\n\tsignal?: AbortSignal,\n): AsyncGenerator<ServerSentEvent> {\n\tconst reader = body.getReader();\n\tconst decoder = new TextDecoder();\n\tconst state: SseDecoderState = { event: null, data: [], raw: [] };\n\tlet buffer = \"\";\n\n\ttry {\n\t\twhile (true) {\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\n\t\t\tconst { value, done } = await reader.read();\n\t\t\tif (done) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\tlet consumed = consumeLine(buffer);\n\t\t\twhile (consumed) {\n\t\t\t\tbuffer = consumed.rest;\n\t\t\t\tconst event = decodeSseLine(consumed.line, state);\n\t\t\t\tif (event) {\n\t\t\t\t\tyield event;\n\t\t\t\t}\n\t\t\t\tconsumed = consumeLine(buffer);\n\t\t\t}\n\t\t}\n\n\t\tbuffer += decoder.decode();\n\t\tlet consumed = consumeLine(buffer);\n\t\twhile (consumed) {\n\t\t\tbuffer = consumed.rest;\n\t\t\tconst event = decodeSseLine(consumed.line, state);\n\t\t\tif (event) {\n\t\t\t\tyield event;\n\t\t\t}\n\t\t\tconsumed = consumeLine(buffer);\n\t\t}\n\n\t\tif (buffer.length > 0) {\n\t\t\tconst event = decodeSseLine(buffer, state);\n\t\t\tif (event) {\n\t\t\t\tyield event;\n\t\t\t}\n\t\t}\n\n\t\tconst trailingEvent = flushSseEvent(state);\n\t\tif (trailingEvent) {\n\t\t\tyield trailingEvent;\n\t\t}\n\t} finally {\n\t\treader.releaseLock();\n\t}\n}\n\n// SCRAMJET-DIVERGENCE: Preserve provider and callback failure facts independently.\nasync function* iterateAnthropicEvents(\n\tresponse: Response,\n\tsignal?: AbortSignal,\n): AsyncGenerator<RawMessageStreamEvent> {\n\tif (!response.body) {\n\t\tthrow new RequestFailureError(\"Anthropic response has no body.\", {\n\t\t\tschemaVersion: 1,\n\t\t\tkind: \"stream\",\n\t\t\treason: \"missing_body\",\n\t\t});\n\t}\n\n\tlet sawMessageStart = false;\n\tlet sawMessageEnd = false;\n\n\tfor await (const sse of iterateSseMessages(response.body, signal)) {\n\t\tif (sse.event === \"error\") {\n\t\t\tlet value: unknown;\n\t\t\ttry {\n\t\t\t\tvalue = JSON.parse(sse.data);\n\t\t\t} catch {\n\t\t\t\tthrow new RequestFailureError(\"Malformed Anthropic error event.\", {\n\t\t\t\t\tschemaVersion: 1,\n\t\t\t\t\tkind: \"stream\",\n\t\t\t\t\treason: \"malformed_event\",\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (\n\t\t\t\t!value ||\n\t\t\t\ttypeof value !== \"object\" ||\n\t\t\t\tArray.isArray(value) ||\n\t\t\t\t!(\"error\" in value) ||\n\t\t\t\t!value.error ||\n\t\t\t\ttypeof value.error !== \"object\" ||\n\t\t\t\tArray.isArray(value.error)\n\t\t\t)\n\t\t\t\tthrow new RequestFailureError(\"Malformed Anthropic error event.\", {\n\t\t\t\t\tschemaVersion: 1,\n\t\t\t\t\tkind: \"stream\",\n\t\t\t\t\treason: \"malformed_event\",\n\t\t\t\t});\n\t\t\tthrow new RequestFailureError(\n\t\t\t\tsse.data,\n\t\t\t\tfailureFromProviderError(value) ?? { schemaVersion: 1, kind: \"provider\", category: \"unknown\" },\n\t\t\t);\n\t\t}\n\n\t\tif (!ANTHROPIC_MESSAGE_EVENTS.has(sse.event ?? \"\")) {\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst event = parseJsonWithRepair<RawMessageStreamEvent>(sse.data);\n\t\t\tif (event.type === \"message_start\") {\n\t\t\t\tsawMessageStart = true;\n\t\t\t} else if (event.type === \"message_stop\") {\n\t\t\t\tsawMessageEnd = true;\n\t\t\t}\n\t\t\tyield event;\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tthrow new RequestFailureError(\n\t\t\t\t`Could not parse Anthropic SSE event ${sse.event}: ${message}; data=${sse.data}; raw=${sse.raw.join(\"\\\\n\")}`,\n\t\t\t\t{ schemaVersion: 1, kind: \"stream\", reason: \"malformed_event\" },\n\t\t\t);\n\t\t}\n\t}\n\n\tif (sawMessageStart && !sawMessageEnd) {\n\t\tthrow new RequestFailureError(\"Anthropic stream ended before message_stop\", {\n\t\t\tschemaVersion: 1,\n\t\t\tkind: \"stream\",\n\t\t\treason: \"missing_terminal_event\",\n\t\t});\n\t}\n}\n\nexport const streamAnthropic: StreamFunction<\"anthropic-messages\", AnthropicOptions> = (\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: Context,\n\toptions?: AnthropicOptions,\n): AssistantMessageEventStream => {\n\tconst stream = new AssistantMessageEventStream();\n\n\t(async () => {\n\t\tconst output: AssistantMessage = {\n\t\t\trole: \"assistant\",\n\t\t\tcontent: [],\n\t\t\tapi: model.api as Api,\n\t\t\tprovider: model.provider,\n\t\t\tmodel: model.id,\n\t\t\tusage: {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\ttotalTokens: 0,\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t},\n\t\t\tstopReason: \"stop\",\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\n\t\tlet requestStarted = false;\n\t\ttry {\n\t\t\tlet client: Anthropic;\n\t\t\tlet isOAuth: boolean;\n\n\t\t\tif (options?.client) {\n\t\t\t\tclient = options.client;\n\t\t\t\tisOAuth = false;\n\t\t\t} else {\n\t\t\t\tconst apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? \"\";\n\n\t\t\t\tlet copilotDynamicHeaders: Record<string, string> | undefined;\n\t\t\t\tif (model.provider === \"github-copilot\") {\n\t\t\t\t\tconst hasImages = hasCopilotVisionInput(context.messages);\n\t\t\t\t\tcopilotDynamicHeaders = buildCopilotDynamicHeaders({\n\t\t\t\t\t\tmessages: context.messages,\n\t\t\t\t\t\thasImages,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tconst cacheRetention = options?.cacheRetention ?? resolveCacheRetention();\n\t\t\t\tconst cacheSessionId = cacheRetention === \"none\" ? undefined : options?.sessionId;\n\n\t\t\t\tconst created = createClient(\n\t\t\t\t\tmodel,\n\t\t\t\t\tapiKey,\n\t\t\t\t\toptions?.interleavedThinking ?? true,\n\t\t\t\t\tshouldUseFineGrainedToolStreamingBeta(model, context),\n\t\t\t\t\toptions?.headers,\n\t\t\t\t\tcopilotDynamicHeaders,\n\t\t\t\t\tcacheSessionId,\n\t\t\t\t\t(input, init) => {\n\t\t\t\t\t\trequestStarted = true;\n\t\t\t\t\t\treturn fetch(input, init);\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tclient = created.client;\n\t\t\t\tisOAuth = created.isOAuthToken;\n\t\t\t}\n\t\t\tlet params = buildParams(model, context, isOAuth, options);\n\t\t\tconst nextParams = await invokeProviderCallback(\"onPayload\", () => options?.onPayload?.(params, model));\n\t\t\tif (nextParams !== undefined) {\n\t\t\t\tparams = nextParams as MessageCreateParamsStreaming;\n\t\t\t}\n\t\t\tconst requestOptions = {\n\t\t\t\t...(options?.signal ? { signal: options.signal } : {}),\n\t\t\t\t...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),\n\t\t\t\t...(options?.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}),\n\t\t\t};\n\t\t\tif (options?.client) requestStarted = true;\n\t\t\tconst response = await client.messages.create({ ...params, stream: true }, requestOptions).asResponse();\n\t\t\tawait invokeProviderCallback(\"onResponse\", () =>\n\t\t\t\toptions?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model),\n\t\t\t);\n\t\t\tstream.push({ type: \"start\", partial: output });\n\n\t\t\ttype Block = (ThinkingContent | TextContent | (ToolCall & { partialJson: string })) & { index: number };\n\t\t\tconst blocks = output.content as Block[];\n\n\t\t\tfor await (const event of iterateAnthropicEvents(response, options?.signal)) {\n\t\t\t\tif (event.type === \"message_start\") {\n\t\t\t\t\toutput.responseId = event.message.id;\n\t\t\t\t\t// Capture initial token usage from message_start event\n\t\t\t\t\t// This ensures we have input token counts even if the stream is aborted early\n\t\t\t\t\toutput.usage.input = event.message.usage.input_tokens || 0;\n\t\t\t\t\toutput.usage.output = event.message.usage.output_tokens || 0;\n\t\t\t\t\toutput.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0;\n\t\t\t\t\toutput.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0;\n\t\t\t\t\t// Anthropic doesn't provide total_tokens, compute from components\n\t\t\t\t\toutput.usage.totalTokens =\n\t\t\t\t\t\toutput.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;\n\t\t\t\t\tcalculateCost(model, output.usage);\n\t\t\t\t} else if (event.type === \"content_block_start\") {\n\t\t\t\t\tif (event.content_block.type === \"text\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"\",\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"text_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t} else if (event.content_block.type === \"thinking\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\tthinking: \"\",\n\t\t\t\t\t\t\tthinkingSignature: \"\",\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"thinking_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t} else if (event.content_block.type === \"redacted_thinking\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\tthinking: \"[Reasoning redacted]\",\n\t\t\t\t\t\t\tthinkingSignature: event.content_block.data,\n\t\t\t\t\t\t\tredacted: true,\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"thinking_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t} else if (event.content_block.type === \"tool_use\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\t\t\tid: event.content_block.id,\n\t\t\t\t\t\t\tname: isOAuth\n\t\t\t\t\t\t\t\t? fromClaudeCodeName(event.content_block.name, context.tools)\n\t\t\t\t\t\t\t\t: event.content_block.name,\n\t\t\t\t\t\t\targuments: (event.content_block.input as Record<string, any>) ?? {},\n\t\t\t\t\t\t\tpartialJson: \"\",\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"toolcall_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t}\n\t\t\t\t} else if (event.type === \"content_block_delta\") {\n\t\t\t\t\tif (event.delta.type === \"text_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"text\") {\n\t\t\t\t\t\t\tblock.text += event.delta.text;\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"text_delta\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tdelta: event.delta.text,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (event.delta.type === \"thinking_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"thinking\") {\n\t\t\t\t\t\t\tblock.thinking += event.delta.thinking;\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"thinking_delta\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tdelta: event.delta.thinking,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (event.delta.type === \"input_json_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"toolCall\") {\n\t\t\t\t\t\t\tblock.partialJson += event.delta.partial_json;\n\t\t\t\t\t\t\tblock.arguments = parseStreamingJson(block.partialJson);\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"toolcall_delta\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tdelta: event.delta.partial_json,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (event.delta.type === \"signature_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"thinking\") {\n\t\t\t\t\t\t\tblock.thinkingSignature = block.thinkingSignature || \"\";\n\t\t\t\t\t\t\tblock.thinkingSignature += event.delta.signature;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (event.type === \"content_block_stop\") {\n\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\tif (block) {\n\t\t\t\t\t\tdelete (block as any).index;\n\t\t\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"text_end\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tcontent: block.text,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"thinking_end\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tcontent: block.thinking,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\t\t\tblock.arguments = parseStreamingJson(block.partialJson);\n\t\t\t\t\t\t\t// Finalize in-place and strip the scratch buffer so replay only\n\t\t\t\t\t\t\t// carries parsed arguments.\n\t\t\t\t\t\t\tdelete (block as { partialJson?: string }).partialJson;\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"toolcall_end\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\ttoolCall: block,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (event.type === \"message_delta\") {\n\t\t\t\t\tif (event.delta.stop_reason) {\n\t\t\t\t\t\tif (output.stopReason !== \"error\") output.stopReason = mapStopReason(event.delta.stop_reason);\n\t\t\t\t\t}\n\t\t\t\t\t// Only update usage fields if present (not null).\n\t\t\t\t\t// Preserves input_tokens from message_start when proxies omit it in message_delta.\n\t\t\t\t\tif (event.usage.input_tokens != null) {\n\t\t\t\t\t\toutput.usage.input = event.usage.input_tokens;\n\t\t\t\t\t}\n\t\t\t\t\tif (event.usage.output_tokens != null) {\n\t\t\t\t\t\toutput.usage.output = event.usage.output_tokens;\n\t\t\t\t\t}\n\t\t\t\t\tif (event.usage.cache_read_input_tokens != null) {\n\t\t\t\t\t\toutput.usage.cacheRead = event.usage.cache_read_input_tokens;\n\t\t\t\t\t}\n\t\t\t\t\tif (event.usage.cache_creation_input_tokens != null) {\n\t\t\t\t\t\toutput.usage.cacheWrite = event.usage.cache_creation_input_tokens;\n\t\t\t\t\t}\n\t\t\t\t\t// Anthropic doesn't provide total_tokens, compute from components\n\t\t\t\t\toutput.usage.totalTokens =\n\t\t\t\t\t\toutput.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;\n\t\t\t\t\tcalculateCost(model, output.usage);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (options?.signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\n\t\t\tif (output.stopReason === \"error\")\n\t\t\t\tthrow new RequestFailureError(\"Anthropic rejected the output.\", {\n\t\t\t\t\tschemaVersion: 1,\n\t\t\t\t\tkind: \"provider\",\n\t\t\t\t\tcategory: \"content_rejection\",\n\t\t\t\t});\n\t\t\tif (output.stopReason === \"aborted\") throw new Error(\"Request was aborted\");\n\n\t\t\tstream.push({ type: \"done\", reason: output.stopReason, message: output });\n\t\t\tstream.end();\n\t\t} catch (error) {\n\t\t\tfor (const block of output.content) {\n\t\t\t\tdelete (block as { index?: number }).index;\n\t\t\t\t// partialJson is only a streaming scratch buffer; never persist it.\n\t\t\t\tdelete (block as { partialJson?: string }).partialJson;\n\t\t\t}\n\t\t\toutput.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n\t\t\toutput.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);\n\t\t\tappendBuiltinFailure(\n\t\t\t\toutput,\n\t\t\t\trequestStarted && !options?.signal?.aborted && error instanceof Anthropic.APIConnectionError\n\t\t\t\t\t? new RequestFailureError(output.errorMessage, {\n\t\t\t\t\t\t\tschemaVersion: 1,\n\t\t\t\t\t\t\tkind: \"provider\",\n\t\t\t\t\t\t\tcategory: error instanceof Anthropic.APIConnectionTimeoutError ? \"timeout\" : \"transport\",\n\t\t\t\t\t\t})\n\t\t\t\t\t: error,\n\t\t\t\t!requestStarted,\n\t\t\t);\n\t\t\tstream.push({ type: \"error\", reason: output.stopReason, error: output });\n\t\t\tstream.end();\n\t\t}\n\t})();\n\n\treturn stream;\n};\n\n// SCRAMJET-DIVERGENCE: extended with opus-4-8, fable-5, sonnet-5 patterns for new model support\nfunction supportsAdaptiveThinking(modelId: string): boolean {\n\tconst normalized = normalizeForPatternMatch(modelId);\n\treturn ADAPTIVE_THINKING_PATTERNS.some((p) => normalized.includes(p));\n}\n\n// SCRAMJET-DIVERGENCE: native xhigh effort support for Opus 4.8/Fable 5 models\nfunction supportsNativeXhighEffort(modelId: string): boolean {\n\tconst normalized = normalizeForPatternMatch(modelId);\n\treturn NATIVE_XHIGH_EFFORT_PATTERNS.some((p) => normalized.includes(p));\n}\n\n/**\n * Map ThinkingLevel to Anthropic effort levels for adaptive thinking.\n * Note: effort \"max\" is only valid on Opus 4.6; Opus 4.7+ and Fable 5 support \"xhigh\".\n */\nfunction mapThinkingLevelToEffort(\n\tmodel: Model<\"anthropic-messages\">,\n\tlevel: SimpleStreamOptions[\"reasoning\"],\n): AnthropicEffort {\n\tconst mapped = level ? model.thinkingLevelMap?.[level] : undefined;\n\tif (typeof mapped === \"string\") return mapped as AnthropicEffort;\n\n\tswitch (level) {\n\t\tcase \"minimal\":\n\t\tcase \"low\":\n\t\t\treturn \"low\";\n\t\tcase \"medium\":\n\t\t\treturn \"medium\";\n\t\tcase \"high\":\n\t\t\treturn \"high\";\n\t\tcase \"xhigh\":\n\t\t\treturn supportsNativeXhighEffort(model.id) ? \"xhigh\" : \"high\";\n\t\tdefault:\n\t\t\treturn \"high\";\n\t}\n}\n\nexport const streamSimpleAnthropic: StreamFunction<\"anthropic-messages\", SimpleStreamOptions> = (\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream => {\n\tconst apiKey = options?.apiKey || getEnvApiKey(model.provider);\n\tif (!apiKey) {\n\t\tthrow new Error(`No API key for provider: ${model.provider}`);\n\t}\n\n\tconst base = buildBaseOptions(model, options, apiKey);\n\tif (!options?.reasoning) {\n\t\treturn streamAnthropic(model, context, { ...base, thinkingEnabled: false } satisfies AnthropicOptions);\n\t}\n\n\t// For adaptive-thinking models: use effort level.\n\t// For older models: use budget-based thinking.\n\tif (supportsAdaptiveThinking(model.id)) {\n\t\tconst effort = mapThinkingLevelToEffort(model, options.reasoning);\n\t\treturn streamAnthropic(model, context, {\n\t\t\t...base,\n\t\t\tthinkingEnabled: true,\n\t\t\teffort,\n\t\t} satisfies AnthropicOptions);\n\t}\n\n\tconst adjusted = adjustMaxTokensForThinking(\n\t\tbase.maxTokens || 0,\n\t\tMath.min(model.maxTokens, options.maxTokens ?? Infinity),\n\t\toptions.reasoning,\n\t\toptions.thinkingBudgets,\n\t);\n\n\t// SCRAMJET-DIVERGENCE: An explicit output ceiling includes thinking; never resurrect a zero budget.\n\tif (adjusted.thinkingBudget < 1024) {\n\t\tthrow new Error(\"Insufficient output space for Anthropic thinking; reduce input or request more output space.\");\n\t}\n\treturn streamAnthropic(model, context, {\n\t\t...base,\n\t\tmaxTokens: adjusted.maxTokens,\n\t\tthinkingEnabled: true,\n\t\tthinkingBudgetTokens: adjusted.thinkingBudget,\n\t} satisfies AnthropicOptions);\n};\n\nfunction isOAuthToken(apiKey: string): boolean {\n\treturn apiKey.includes(\"sk-ant-oat\");\n}\n\nfunction createClient(\n\tmodel: Model<\"anthropic-messages\">,\n\tapiKey: string,\n\tinterleavedThinking: boolean,\n\tuseFineGrainedToolStreamingBeta: boolean,\n\toptionsHeaders?: Record<string, string>,\n\tdynamicHeaders?: Record<string, string>,\n\tsessionId?: string,\n\tfetchImplementation?: typeof fetch,\n): { client: Anthropic; isOAuthToken: boolean } {\n\t// Adaptive-thinking models have interleaved thinking built in, so skip the beta header.\n\tconst needsInterleavedBeta = interleavedThinking && !supportsAdaptiveThinking(model.id);\n\tconst betaFeatures: string[] = [];\n\tif (useFineGrainedToolStreamingBeta) {\n\t\tbetaFeatures.push(FINE_GRAINED_TOOL_STREAMING_BETA);\n\t}\n\tif (needsInterleavedBeta) {\n\t\tbetaFeatures.push(INTERLEAVED_THINKING_BETA);\n\t}\n\n\tif (model.provider === \"cloudflare-ai-gateway\") {\n\t\tconst client = new Anthropic({\n\t\t\tapiKey: null,\n\t\t\tauthToken: null,\n\t\t\tbaseURL: resolveCloudflareBaseUrl(model),\n\t\t\tfetch: fetchImplementation,\n\t\t\tdangerouslyAllowBrowser: true,\n\t\t\tdefaultHeaders: mergeHeaders(\n\t\t\t\t{\n\t\t\t\t\taccept: \"application/json\",\n\t\t\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t\t\t\"cf-aig-authorization\": `Bearer ${apiKey}`,\n\t\t\t\t\t\"x-api-key\": null,\n\t\t\t\t\tAuthorization: null,\n\t\t\t\t\t...(betaFeatures.length > 0 ? { \"anthropic-beta\": betaFeatures.join(\",\") } : {}),\n\t\t\t\t},\n\t\t\t\tmodel.headers,\n\t\t\t\toptionsHeaders,\n\t\t\t),\n\t\t});\n\n\t\treturn { client, isOAuthToken: false };\n\t}\n\n\t// Copilot: Bearer auth, selective betas.\n\tif (model.provider === \"github-copilot\") {\n\t\tconst client = new Anthropic({\n\t\t\tfetch: fetchImplementation,\n\t\t\tapiKey: null,\n\t\t\tauthToken: apiKey,\n\t\t\tbaseURL: model.baseUrl,\n\t\t\tdangerouslyAllowBrowser: true,\n\t\t\tdefaultHeaders: mergeHeaders(\n\t\t\t\t{\n\t\t\t\t\taccept: \"application/json\",\n\t\t\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t\t\t...(betaFeatures.length > 0 ? { \"anthropic-beta\": betaFeatures.join(\",\") } : {}),\n\t\t\t\t},\n\t\t\t\tmodel.headers,\n\t\t\t\tdynamicHeaders,\n\t\t\t\toptionsHeaders,\n\t\t\t),\n\t\t});\n\n\t\treturn { client, isOAuthToken: false };\n\t}\n\n\t// OAuth: Bearer auth, Claude Code identity headers\n\tif (isOAuthToken(apiKey)) {\n\t\tconst client = new Anthropic({\n\t\t\tfetch: fetchImplementation,\n\t\t\tapiKey: null,\n\t\t\tauthToken: apiKey,\n\t\t\tbaseURL: model.baseUrl,\n\t\t\tdangerouslyAllowBrowser: true,\n\t\t\tdefaultHeaders: mergeHeaders(\n\t\t\t\t{\n\t\t\t\t\taccept: \"application/json\",\n\t\t\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t\t\t\"anthropic-beta\": [\"claude-code-20250219\", \"oauth-2025-04-20\", ...betaFeatures].join(\",\"),\n\t\t\t\t\t\"user-agent\": `claude-cli/${claudeCodeVersion}`,\n\t\t\t\t\t\"x-app\": \"cli\",\n\t\t\t\t},\n\t\t\t\tmodel.headers,\n\t\t\t\toptionsHeaders,\n\t\t\t),\n\t\t});\n\n\t\treturn { client, isOAuthToken: true };\n\t}\n\n\t// API key auth\n\tconst sessionAffinityHeaders: Record<string, string | null> =\n\t\tsessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { \"x-session-affinity\": sessionId } : {};\n\tconst client = new Anthropic({\n\t\tapiKey,\n\t\tbaseURL: model.baseUrl,\n\t\tfetch: fetchImplementation,\n\t\tdangerouslyAllowBrowser: true,\n\t\tdefaultHeaders: mergeHeaders(\n\t\t\t{\n\t\t\t\taccept: \"application/json\",\n\t\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t\t...(betaFeatures.length > 0 ? { \"anthropic-beta\": betaFeatures.join(\",\") } : {}),\n\t\t\t},\n\t\t\tsessionAffinityHeaders,\n\t\t\tmodel.headers,\n\t\t\toptionsHeaders,\n\t\t),\n\t});\n\n\treturn { client, isOAuthToken: false };\n}\n\nconst claudeCodeIdentity = \"You are Claude Code, Anthropic's official CLI for Claude.\";\n\n/**\n * Builds the system block array for a sectioned system prompt.\n *\n * Anthropic allows at most 4 `cache_control` breakpoints per request, and the\n * last tool and last user message already consume two, so the system prompt\n * spends at most one: it lands on a single stable block folding the leading\n * run of sections up to the first `cacheRetention: \"none\"` section (zero\n * breakpoints when retention is \"none\"; when the first section is volatile\n * the breakpoint moves to the OAuth identity block, or is omitted entirely\n * on API-key auth).\n * Everything from the first volatile section onward trails as uncached blocks\n * in array order — anything after the breakpoint is excluded from the cached\n * prefix, and preserving array order keeps the concatenated block text\n * byte-identical (for well-formed text; `sanitizeSurrogates` runs per block)\n * to the flattened prompt other providers send. On OAuth the\n * Claude Code identity block stays first; caching is prefix-based, so the\n * stable block's breakpoint covers it without a second one (unlike the legacy\n * string path, which marks both for back-compat).\n */\nfunction buildSystemBlocks(\n\tsections: SystemPromptSection[],\n\tisOAuthToken: boolean,\n\tcacheControl?: CacheControlEphemeral,\n): TextBlockParam[] {\n\tconst blocks: TextBlockParam[] = [];\n\tconst nonEmpty = sections.filter((s) => s.text);\n\tconst firstVolatile = nonEmpty.findIndex((s) => s.cacheRetention === \"none\");\n\tconst stable = firstVolatile === -1 ? nonEmpty : nonEmpty.slice(0, firstVolatile);\n\tconst tail = firstVolatile === -1 ? [] : nonEmpty.slice(firstVolatile);\n\n\tif (isOAuthToken) {\n\t\tblocks.push({\n\t\t\ttype: \"text\",\n\t\t\ttext: claudeCodeIdentity,\n\t\t\t// Without stable sections the identity block is the whole stable\n\t\t\t// prefix, so the breakpoint lands here (as on the string path).\n\t\t\t...(stable.length === 0 && cacheControl ? { cache_control: cacheControl } : {}),\n\t\t});\n\t}\n\tif (stable.length > 0) {\n\t\tblocks.push({\n\t\t\ttype: \"text\",\n\t\t\ttext: sanitizeSurrogates(stable.map((s) => s.text).join(\"\")),\n\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t});\n\t}\n\tfor (const section of tail) {\n\t\tblocks.push({ type: \"text\", text: sanitizeSurrogates(section.text) });\n\t}\n\treturn blocks;\n}\n\nfunction buildParams(\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: Context,\n\tisOAuthToken: boolean,\n\toptions?: AnthropicOptions,\n): MessageCreateParamsStreaming {\n\tconst { cacheControl } = getCacheControl(model, options?.cacheRetention);\n\tconst params: MessageCreateParamsStreaming = {\n\t\tmodel: model.id,\n\t\tmessages: convertMessages(context.messages, model, isOAuthToken, cacheControl),\n\t\tmax_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,\n\t\tstream: true,\n\t};\n\n\tif (Array.isArray(context.systemPrompt)) {\n\t\tconst blocks = buildSystemBlocks(context.systemPrompt, isOAuthToken, cacheControl);\n\t\tif (blocks.length > 0) {\n\t\t\tparams.system = blocks;\n\t\t}\n\t} else {\n\t\tconst systemPrompt = context.systemPrompt;\n\n\t\t// For OAuth tokens, we MUST include Claude Code identity\n\t\tif (isOAuthToken) {\n\t\t\tparams.system = [\n\t\t\t\t{\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: claudeCodeIdentity,\n\t\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t\t},\n\t\t\t];\n\t\t\tif (systemPrompt) {\n\t\t\t\tparams.system.push({\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: sanitizeSurrogates(systemPrompt),\n\t\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (systemPrompt) {\n\t\t\t// Add cache control to system prompt for non-OAuth tokens\n\t\t\tparams.system = [\n\t\t\t\t{\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: sanitizeSurrogates(systemPrompt),\n\t\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t\t},\n\t\t\t];\n\t\t}\n\t}\n\n\t// Temperature is incompatible with extended thinking (adaptive or budget-based).\n\t// SCRAMJET-DIVERGENCE: gate on supportsTemperature compat (opus-4-7+ rejects non-default temperature)\n\tif (\n\t\toptions?.temperature !== undefined &&\n\t\t!options?.thinkingEnabled &&\n\t\tgetAnthropicCompat(model).supportsTemperature\n\t) {\n\t\tparams.temperature = options.temperature;\n\t}\n\n\tif (context.tools && context.tools.length > 0) {\n\t\tconst compat = getAnthropicCompat(model);\n\t\tparams.tools = convertTools(\n\t\t\tcontext.tools,\n\t\t\tisOAuthToken,\n\t\t\tcompat.supportsEagerToolInputStreaming,\n\t\t\tcompat.supportsCacheControlOnTools ? cacheControl : undefined,\n\t\t);\n\t}\n\n\t// Configure thinking mode: adaptive-thinking models,\n\t// budget-based older models, or explicitly disabled.\n\tif (model.reasoning) {\n\t\tif (options?.thinkingEnabled) {\n\t\t\t// Default to \"summarized\" so Opus 4.7 and Mythos Preview behave like\n\t\t\t// older Claude 4 models (whose API default is also \"summarized\").\n\t\t\tconst display: AnthropicThinkingDisplay = options.thinkingDisplay ?? \"summarized\";\n\t\t\tif (supportsAdaptiveThinking(model.id)) {\n\t\t\t\t// Adaptive thinking: Claude decides when and how much to think.\n\t\t\t\tparams.thinking = { type: \"adaptive\", display };\n\t\t\t\tif (options.effort) {\n\t\t\t\t\t// The Anthropic SDK types can lag newly supported effort values such as \"xhigh\".\n\t\t\t\t\tparams.output_config =\n\t\t\t\t\t\toptions.effort === \"xhigh\"\n\t\t\t\t\t\t\t? ({ effort: options.effort } as unknown as NonNullable<\n\t\t\t\t\t\t\t\t\tMessageCreateParamsStreaming[\"output_config\"]\n\t\t\t\t\t\t\t\t>)\n\t\t\t\t\t\t\t: { effort: options.effort };\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Budget-based thinking for older models\n\t\t\t\tparams.thinking = {\n\t\t\t\t\ttype: \"enabled\",\n\t\t\t\t\tbudget_tokens: options.thinkingBudgetTokens || 1024,\n\t\t\t\t\tdisplay,\n\t\t\t\t};\n\t\t\t}\n\t\t} else if (options?.thinkingEnabled === false) {\n\t\t\tparams.thinking = { type: \"disabled\" };\n\t\t}\n\t}\n\n\tif (options?.metadata) {\n\t\tconst userId = options.metadata.user_id;\n\t\tif (typeof userId === \"string\") {\n\t\t\tparams.metadata = { user_id: userId };\n\t\t}\n\t}\n\n\tif (options?.toolChoice) {\n\t\tif (typeof options.toolChoice === \"string\") {\n\t\t\tparams.tool_choice = { type: options.toolChoice };\n\t\t} else {\n\t\t\tparams.tool_choice = options.toolChoice;\n\t\t}\n\t}\n\n\treturn params;\n}\n\n// Normalize tool call IDs to match Anthropic's required pattern and length\nfunction normalizeToolCallId(id: string): string {\n\treturn id.replace(/[^a-zA-Z0-9_-]/g, \"_\").slice(0, 64);\n}\n\nfunction convertMessages(\n\tmessages: Message[],\n\tmodel: Model<\"anthropic-messages\">,\n\tisOAuthToken: boolean,\n\tcacheControl?: CacheControlEphemeral,\n): MessageParam[] {\n\tconst params: MessageParam[] = [];\n\n\t// Transform messages for cross-provider compatibility\n\tconst transformedMessages = transformMessages(messages, model, normalizeToolCallId);\n\n\tfor (let i = 0; i < transformedMessages.length; i++) {\n\t\tconst msg = transformedMessages[i];\n\n\t\tif (msg.role === \"user\") {\n\t\t\tif (typeof msg.content === \"string\") {\n\t\t\t\tif (msg.content.trim().length > 0) {\n\t\t\t\t\tparams.push({\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: sanitizeSurrogates(msg.content),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst blocks: ContentBlockParam[] = msg.content.map((item) => {\n\t\t\t\t\tif (item.type === \"text\") {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: sanitizeSurrogates(item.text),\n\t\t\t\t\t\t};\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttype: \"image\",\n\t\t\t\t\t\t\tsource: {\n\t\t\t\t\t\t\t\ttype: \"base64\",\n\t\t\t\t\t\t\t\tmedia_type: item.mimeType as \"image/jpeg\" | \"image/png\" | \"image/gif\" | \"image/webp\",\n\t\t\t\t\t\t\t\tdata: item.data,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tconst filteredBlocks = blocks.filter((b) => {\n\t\t\t\t\tif (b.type === \"text\") {\n\t\t\t\t\t\treturn b.text.trim().length > 0;\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t});\n\t\t\t\tif (filteredBlocks.length === 0) continue;\n\t\t\t\tparams.push({\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: filteredBlocks,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (msg.role === \"assistant\") {\n\t\t\tconst blocks: ContentBlockParam[] = [];\n\n\t\t\tfor (const block of msg.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\tif (block.text.trim().length === 0) continue;\n\t\t\t\t\tblocks.push({\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: sanitizeSurrogates(block.text),\n\t\t\t\t\t});\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\t// Redacted thinking: pass the opaque payload back as redacted_thinking\n\t\t\t\t\tif (block.redacted) {\n\t\t\t\t\t\tblocks.push({\n\t\t\t\t\t\t\ttype: \"redacted_thinking\",\n\t\t\t\t\t\t\tdata: block.thinkingSignature!,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (block.thinking.trim().length === 0) continue;\n\t\t\t\t\t// If thinking signature is missing/empty (e.g., from aborted stream),\n\t\t\t\t\t// convert to plain text block without <thinking> tags to avoid API rejection\n\t\t\t\t\t// and prevent Claude from mimicking the tags in responses\n\t\t\t\t\tif (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {\n\t\t\t\t\t\tblocks.push({\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: sanitizeSurrogates(block.thinking),\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\tblocks.push({\n\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\tthinking: sanitizeSurrogates(block.thinking),\n\t\t\t\t\t\t\tsignature: block.thinkingSignature,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tblocks.push({\n\t\t\t\t\t\ttype: \"tool_use\",\n\t\t\t\t\t\t// SCRAMJET-DIVERGENCE: Sanitize tool-call IDs unconditionally on the outgoing\n\t\t\t\t\t\t// request. `transform-messages.ts` only normalizes IDs for cross-model replay\n\t\t\t\t\t\t// (`!isSameModel`), so a legacy session carrying a provider-invalid ID (e.g. one\n\t\t\t\t\t\t// embedding a raw model id like `gpt-5.5`) replayed to the SAME model would be sent\n\t\t\t\t\t\t// verbatim and rejected by Anthropic's `^[a-zA-Z0-9_-]+$` constraint. Applying the\n\t\t\t\t\t\t// idempotent `normalizeToolCallId` here (and at the matching tool_result sites below)\n\t\t\t\t\t\t// is harmless on already-clean IDs and preserves call/result correlation because the\n\t\t\t\t\t\t// same deterministic function maps both halves to the same value.\n\t\t\t\t\t\tid: normalizeToolCallId(block.id),\n\t\t\t\t\t\tname: isOAuthToken ? toClaudeCodeName(block.name) : block.name,\n\t\t\t\t\t\tinput: block.arguments ?? {},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (blocks.length === 0) continue;\n\t\t\tparams.push({\n\t\t\t\trole: \"assistant\",\n\t\t\t\tcontent: blocks,\n\t\t\t});\n\t\t} else if (msg.role === \"toolResult\") {\n\t\t\t// Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint\n\t\t\tconst toolResults: ContentBlockParam[] = [];\n\n\t\t\t// Add the current tool result\n\t\t\ttoolResults.push({\n\t\t\t\ttype: \"tool_result\",\n\t\t\t\t// SCRAMJET-DIVERGENCE: sanitize to keep correlation with the tool_use id above.\n\t\t\t\ttool_use_id: normalizeToolCallId(msg.toolCallId),\n\t\t\t\tcontent: convertContentBlocks(msg.content),\n\t\t\t\tis_error: msg.isError,\n\t\t\t});\n\n\t\t\t// Look ahead for consecutive toolResult messages\n\t\t\tlet j = i + 1;\n\t\t\twhile (j < transformedMessages.length && transformedMessages[j].role === \"toolResult\") {\n\t\t\t\tconst nextMsg = transformedMessages[j] as ToolResultMessage; // We know it's a toolResult\n\t\t\t\ttoolResults.push({\n\t\t\t\t\ttype: \"tool_result\",\n\t\t\t\t\t// SCRAMJET-DIVERGENCE: sanitize to keep correlation with the tool_use id above.\n\t\t\t\t\ttool_use_id: normalizeToolCallId(nextMsg.toolCallId),\n\t\t\t\t\tcontent: convertContentBlocks(nextMsg.content),\n\t\t\t\t\tis_error: nextMsg.isError,\n\t\t\t\t});\n\t\t\t\tj++;\n\t\t\t}\n\n\t\t\t// Skip the messages we've already processed\n\t\t\ti = j - 1;\n\n\t\t\t// Add a single user message with all tool results\n\t\t\tparams.push({\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: toolResults,\n\t\t\t});\n\t\t}\n\t}\n\n\t// Add cache_control to the last user message to cache conversation history\n\tif (cacheControl && params.length > 0) {\n\t\tconst lastMessage = params[params.length - 1];\n\t\tif (lastMessage.role === \"user\") {\n\t\t\tif (Array.isArray(lastMessage.content)) {\n\t\t\t\tconst lastBlock = lastMessage.content[lastMessage.content.length - 1];\n\t\t\t\tif (\n\t\t\t\t\tlastBlock &&\n\t\t\t\t\t(lastBlock.type === \"text\" || lastBlock.type === \"image\" || lastBlock.type === \"tool_result\")\n\t\t\t\t) {\n\t\t\t\t\t(lastBlock as any).cache_control = cacheControl;\n\t\t\t\t}\n\t\t\t} else if (typeof lastMessage.content === \"string\") {\n\t\t\t\tlastMessage.content = [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: lastMessage.content,\n\t\t\t\t\t\tcache_control: cacheControl,\n\t\t\t\t\t},\n\t\t\t\t] as any;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn params;\n}\n\nfunction shouldUseFineGrainedToolStreamingBeta(model: Model<\"anthropic-messages\">, context: Context): boolean {\n\treturn !!context.tools?.length && !getAnthropicCompat(model).supportsEagerToolInputStreaming;\n}\n\nfunction convertTools(\n\ttools: Tool[],\n\tisOAuthToken: boolean,\n\tsupportsEagerToolInputStreaming: boolean,\n\tcacheControl?: CacheControlEphemeral,\n): Anthropic.Messages.Tool[] {\n\tif (!tools) return [];\n\n\treturn tools.map((tool, index) => {\n\t\tconst schema = tool.parameters as { properties?: unknown; required?: string[] };\n\n\t\treturn {\n\t\t\tname: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name,\n\t\t\tdescription: tool.description,\n\t\t\t...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}),\n\t\t\tinput_schema: {\n\t\t\t\ttype: \"object\",\n\t\t\t\tproperties: schema.properties ?? {},\n\t\t\t\trequired: schema.required ?? [],\n\t\t\t},\n\t\t\t...(cacheControl && index === tools.length - 1 ? { cache_control: cacheControl } : {}),\n\t\t};\n\t});\n}\n\nfunction mapStopReason(reason: Anthropic.Messages.StopReason | string): StopReason {\n\tswitch (reason) {\n\t\tcase \"end_turn\":\n\t\t\treturn \"stop\";\n\t\tcase \"max_tokens\":\n\t\t\treturn \"length\";\n\t\tcase \"tool_use\":\n\t\t\treturn \"toolUse\";\n\t\tcase \"refusal\":\n\t\t\treturn \"error\";\n\t\tcase \"pause_turn\": // Stop is good enough -> resubmit\n\t\t\treturn \"stop\";\n\t\tcase \"stop_sequence\":\n\t\t\treturn \"stop\"; // We don't supply stop sequences, so this should never happen\n\t\tcase \"sensitive\": // Content flagged by safety filters (not yet in SDK types)\n\t\t\treturn \"error\";\n\t\tdefault:\n\t\t\t// Handle unknown stop reasons gracefully (API may add new values)\n\t\t\tthrow new Error(`Unhandled stop reason: ${reason}`);\n\t}\n}\n"]}
1
+ {"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../src/providers/anthropic.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAW1C,OAAO,KAAK,EASX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAOb,MAAM,aAAa,CAAC;AAsJrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AAsBhE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;OAUG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AAiPD,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CA8RlF,CAAC;AAwCF,eAAO,MAAM,qBAAqB,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CA2C3F,CAAC","sourcesContent":["import Anthropic from \"@anthropic-ai/sdk\";\nimport type {\n\tCacheControlEphemeral,\n\tContentBlockParam,\n\tMessageCreateParamsStreaming,\n\tMessageParam,\n\tRawMessageStreamEvent,\n\tTextBlockParam,\n} from \"@anthropic-ai/sdk/resources/messages.js\";\nimport { getEnvApiKey } from \"../env-api-keys.js\";\nimport { calculateCost } from \"../models.js\";\nimport type {\n\tAnthropicMessagesCompat,\n\tApi,\n\tAssistantMessage,\n\tCacheRetention,\n\tContext,\n\tImageContent,\n\tMessage,\n\tModel,\n\tSimpleStreamOptions,\n\tStopReason,\n\tStreamFunction,\n\tStreamOptions,\n\tSystemPromptSection,\n\tTextContent,\n\tThinkingContent,\n\tTool,\n\tToolCall,\n\tToolResultMessage,\n} from \"../types.js\";\nimport { AssistantMessageEventStream } from \"../utils/event-stream.js\";\nimport {\n\tappendBuiltinFailure,\n\tfailureFromProviderError,\n\tinvokeProviderCallback,\n\tRequestFailureError,\n} from \"../utils/failure-evidence.js\";\nimport { headersToRecord } from \"../utils/headers.js\";\nimport { parseJsonWithRepair, parseStreamingJson } from \"../utils/json-parse.js\";\nimport { createProviderAbortScope } from \"../utils/provider-abort-scope.js\";\nimport { sanitizeSurrogates } from \"../utils/sanitize-unicode.js\";\nimport {\n\tADAPTIVE_THINKING_PATTERNS,\n\tNATIVE_XHIGH_EFFORT_PATTERNS,\n\tnormalizeForPatternMatch,\n} from \"./anthropic-model-patterns.js\";\n\nimport { resolveCloudflareBaseUrl } from \"./cloudflare.js\";\nimport { buildCopilotDynamicHeaders, hasCopilotVisionInput } from \"./github-copilot-headers.js\";\nimport { adjustMaxTokensForThinking, buildBaseOptions } from \"./simple-options.js\";\nimport { transformMessages } from \"./transform-messages.js\";\n\n/**\n * Resolve cache retention preference.\n *\n * Defaults to \"short\" and only honors PI_CACHE_RETENTION === \"long\" — this is\n * the low-level provider fallback for callers that hit the adapter directly.\n * In the coding-agent path this default rarely fires: the SDK's own\n * resolveCacheRetention (packages/coding-agent/src/core/sdk.ts, which defaults\n * to \"long\") resolves first and passes a concrete value down, so `cacheRetention`\n * is normally already set when it reaches here. Keep the two defaults in sync\n * intentionally — they differ on purpose (backward-compat vs. SDK default).\n */\nfunction resolveCacheRetention(cacheRetention?: CacheRetention): CacheRetention {\n\tif (cacheRetention) {\n\t\treturn cacheRetention;\n\t}\n\tif (typeof process !== \"undefined\" && process.env.PI_CACHE_RETENTION === \"long\") {\n\t\treturn \"long\";\n\t}\n\treturn \"short\";\n}\n\nfunction getCacheControl(\n\tmodel: Model<\"anthropic-messages\">,\n\tcacheRetention?: CacheRetention,\n): { retention: CacheRetention; cacheControl?: CacheControlEphemeral } {\n\tconst retention = resolveCacheRetention(cacheRetention);\n\tif (retention === \"none\") {\n\t\treturn { retention };\n\t}\n\tconst ttl = retention === \"long\" && getAnthropicCompat(model).supportsLongCacheRetention ? \"1h\" : undefined;\n\treturn {\n\t\tretention,\n\t\tcacheControl: { type: \"ephemeral\", ...(ttl && { ttl }) },\n\t};\n}\n\n// Stealth mode: Mimic Claude Code's tool naming exactly\nconst claudeCodeVersion = \"2.1.75\";\n\n// Claude Code 2.x tool names (canonical casing)\n// Source: https://cchistory.mariozechner.at/data/prompts-2.1.11.md\n// To update: https://github.com/badlogic/cchistory\nconst claudeCodeTools = [\n\t\"Read\",\n\t\"Write\",\n\t\"Edit\",\n\t\"Bash\",\n\t\"Grep\",\n\t\"Glob\",\n\t\"AskUserQuestion\",\n\t\"EnterPlanMode\",\n\t\"ExitPlanMode\",\n\t\"KillShell\",\n\t\"NotebookEdit\",\n\t\"Skill\",\n\t\"Task\",\n\t\"TaskOutput\",\n\t\"TodoWrite\",\n\t\"WebFetch\",\n\t\"WebSearch\",\n];\n\nconst ccToolLookup = new Map(claudeCodeTools.map((t) => [t.toLowerCase(), t]));\n\n// Convert tool name to CC canonical casing if it matches (case-insensitive)\nconst toClaudeCodeName = (name: string) => ccToolLookup.get(name.toLowerCase()) ?? name;\nconst fromClaudeCodeName = (name: string, tools?: Tool[]) => {\n\tif (tools && tools.length > 0) {\n\t\tconst lowerName = name.toLowerCase();\n\t\tconst matchedTool = tools.find((tool) => tool.name.toLowerCase() === lowerName);\n\t\tif (matchedTool) return matchedTool.name;\n\t}\n\treturn name;\n};\n\n/**\n * Convert content blocks to Anthropic API format\n */\nfunction convertContentBlocks(content: (TextContent | ImageContent)[]):\n\t| string\n\t| Array<\n\t\t\t| { type: \"text\"; text: string }\n\t\t\t| {\n\t\t\t\t\ttype: \"image\";\n\t\t\t\t\tsource: {\n\t\t\t\t\t\ttype: \"base64\";\n\t\t\t\t\t\tmedia_type: \"image/jpeg\" | \"image/png\" | \"image/gif\" | \"image/webp\";\n\t\t\t\t\t\tdata: string;\n\t\t\t\t\t};\n\t\t\t }\n\t > {\n\t// If only text blocks, return as concatenated string for simplicity\n\tconst hasImages = content.some((c) => c.type === \"image\");\n\tif (!hasImages) {\n\t\treturn sanitizeSurrogates(content.map((c) => (c as TextContent).text).join(\"\\n\"));\n\t}\n\n\t// If we have images, convert to content block array\n\tconst blocks = content.map((block) => {\n\t\tif (block.type === \"text\") {\n\t\t\treturn {\n\t\t\t\ttype: \"text\" as const,\n\t\t\t\ttext: sanitizeSurrogates(block.text),\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\ttype: \"image\" as const,\n\t\t\tsource: {\n\t\t\t\ttype: \"base64\" as const,\n\t\t\t\tmedia_type: block.mimeType as \"image/jpeg\" | \"image/png\" | \"image/gif\" | \"image/webp\",\n\t\t\t\tdata: block.data,\n\t\t\t},\n\t\t};\n\t});\n\n\t// If only images (no text), add placeholder text block\n\tconst hasText = blocks.some((b) => b.type === \"text\");\n\tif (!hasText) {\n\t\tblocks.unshift({\n\t\t\ttype: \"text\" as const,\n\t\t\ttext: \"(see attached image)\",\n\t\t});\n\t}\n\n\treturn blocks;\n}\n\nexport type AnthropicEffort = \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n\nexport type AnthropicThinkingDisplay = \"summarized\" | \"omitted\";\n\nconst FINE_GRAINED_TOOL_STREAMING_BETA = \"fine-grained-tool-streaming-2025-05-14\";\nconst INTERLEAVED_THINKING_BETA = \"interleaved-thinking-2025-05-14\";\n\nfunction getAnthropicCompat(model: Model<\"anthropic-messages\">): Required<AnthropicMessagesCompat> {\n\t// Auto-detect session affinity and cache control support from provider\n\tconst isFireworks = model.provider === \"fireworks\";\n\tconst isCloudflareAiGatewayAnthropic =\n\t\tmodel.provider === \"cloudflare-ai-gateway\" && model.baseUrl.includes(\"anthropic\");\n\t// SCRAMJET-DIVERGENCE: supportsTemperature and forceAdaptiveThinking defaults added for Opus 4.8/Fable 5/Sonnet 5 support\n\treturn {\n\t\tsupportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? !isFireworks,\n\t\tsupportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? !isFireworks,\n\t\tsendSessionAffinityHeaders:\n\t\t\tmodel.compat?.sendSessionAffinityHeaders ?? !!(isFireworks || isCloudflareAiGatewayAnthropic),\n\t\tsupportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? !isFireworks,\n\t\tsupportsTemperature: model.compat?.supportsTemperature ?? true,\n\t\tforceAdaptiveThinking: model.compat?.forceAdaptiveThinking ?? false,\n\t};\n}\n\nexport interface AnthropicOptions extends StreamOptions {\n\t/**\n\t * Enable extended thinking.\n\t * For adaptive thinking models (Opus 4.6+, Sonnet 4.6+, Fable 5): model decides when/how much to think.\n\t * For older models: uses budget-based thinking with thinkingBudgetTokens.\n\t */\n\tthinkingEnabled?: boolean;\n\t/**\n\t * Token budget for extended thinking (older models only).\n\t * Ignored for adaptive thinking models (Opus 4.6+, Sonnet 4.6+, Fable 5).\n\t */\n\tthinkingBudgetTokens?: number;\n\t/**\n\t * Effort level for adaptive thinking (Opus 4.6+, Sonnet 4.6+, Fable 5).\n\t * Controls how much thinking Claude allocates:\n\t * - \"max\": Always thinks with no constraints (Opus 4.6 only)\n\t * - \"xhigh\": Highest reasoning level (Opus 4.7+, Fable 5)\n\t * - \"high\": Always thinks, deep reasoning (default)\n\t * - \"medium\": Moderate thinking, may skip for simple queries\n\t * - \"low\": Minimal thinking, skips for simple tasks\n\t * Ignored for older models.\n\t */\n\teffort?: AnthropicEffort;\n\t/**\n\t * Controls how thinking content is returned in API responses.\n\t * - \"summarized\": Thinking blocks contain summarized thinking text (default here).\n\t * - \"omitted\": Thinking blocks return an empty thinking field; the encrypted\n\t * signature still travels back for multi-turn continuity. Use for faster\n\t * time-to-first-text-token when your UI does not surface thinking.\n\t *\n\t * Note: Anthropic's API default for Claude Opus 4.7 and Claude Mythos Preview\n\t * is \"omitted\". We default to \"summarized\" here to keep behavior consistent\n\t * with older Claude 4 models. Set this explicitly to \"omitted\" to opt in.\n\t */\n\tthinkingDisplay?: AnthropicThinkingDisplay;\n\tinterleavedThinking?: boolean;\n\ttoolChoice?: \"auto\" | \"any\" | \"none\" | { type: \"tool\"; name: string };\n\t/**\n\t * Pre-built Anthropic client instance. When provided, skips internal client\n\t * construction entirely. Use this to inject alternative SDK clients such as\n\t * `AnthropicVertex` that shares the same messaging API.\n\t */\n\tclient?: Anthropic;\n}\n\nfunction mergeHeaders(...headerSources: (Record<string, string | null> | undefined)[]): Record<string, string | null> {\n\tconst merged: Record<string, string | null> = {};\n\tfor (const headers of headerSources) {\n\t\tif (headers) {\n\t\t\tObject.assign(merged, headers);\n\t\t}\n\t}\n\treturn merged;\n}\n\ninterface ServerSentEvent {\n\tevent: string | null;\n\tdata: string;\n\traw: string[];\n}\n\ninterface SseDecoderState {\n\tevent: string | null;\n\tdata: string[];\n\traw: string[];\n}\n\nconst ANTHROPIC_MESSAGE_EVENTS: ReadonlySet<string> = new Set([\n\t\"message_start\",\n\t\"message_delta\",\n\t\"message_stop\",\n\t\"content_block_start\",\n\t\"content_block_delta\",\n\t\"content_block_stop\",\n]);\n\nfunction flushSseEvent(state: SseDecoderState): ServerSentEvent | null {\n\tif (!state.event && state.data.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst event: ServerSentEvent = {\n\t\tevent: state.event,\n\t\tdata: state.data.join(\"\\n\"),\n\t\traw: [...state.raw],\n\t};\n\tstate.event = null;\n\tstate.data = [];\n\tstate.raw = [];\n\treturn event;\n}\n\nfunction decodeSseLine(line: string, state: SseDecoderState): ServerSentEvent | null {\n\tif (line === \"\") {\n\t\treturn flushSseEvent(state);\n\t}\n\n\tstate.raw.push(line);\n\tif (line.startsWith(\":\")) {\n\t\treturn null;\n\t}\n\n\tconst delimiterIndex = line.indexOf(\":\");\n\tconst fieldName = delimiterIndex === -1 ? line : line.slice(0, delimiterIndex);\n\tlet value = delimiterIndex === -1 ? \"\" : line.slice(delimiterIndex + 1);\n\tif (value.startsWith(\" \")) {\n\t\tvalue = value.slice(1);\n\t}\n\n\tif (fieldName === \"event\") {\n\t\tstate.event = value;\n\t} else if (fieldName === \"data\") {\n\t\tstate.data.push(value);\n\t}\n\n\treturn null;\n}\n\nfunction nextLineBreakIndex(text: string): number {\n\tconst carriageReturnIndex = text.indexOf(\"\\r\");\n\tconst newlineIndex = text.indexOf(\"\\n\");\n\tif (carriageReturnIndex === -1) {\n\t\treturn newlineIndex;\n\t}\n\tif (newlineIndex === -1) {\n\t\treturn carriageReturnIndex;\n\t}\n\treturn Math.min(carriageReturnIndex, newlineIndex);\n}\n\nfunction consumeLine(text: string): { line: string; rest: string } | null {\n\tconst lineBreakIndex = nextLineBreakIndex(text);\n\tif (lineBreakIndex === -1) {\n\t\treturn null;\n\t}\n\n\tlet nextIndex = lineBreakIndex + 1;\n\tif (text[lineBreakIndex] === \"\\r\" && text[nextIndex] === \"\\n\") {\n\t\tnextIndex += 1;\n\t}\n\n\treturn {\n\t\tline: text.slice(0, lineBreakIndex),\n\t\trest: text.slice(nextIndex),\n\t};\n}\n\nasync function* iterateSseMessages(\n\tbody: ReadableStream<Uint8Array>,\n\tsignal?: AbortSignal,\n): AsyncGenerator<ServerSentEvent> {\n\tconst reader = body.getReader();\n\tconst decoder = new TextDecoder();\n\tconst state: SseDecoderState = { event: null, data: [], raw: [] };\n\tlet buffer = \"\";\n\n\ttry {\n\t\twhile (true) {\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\n\t\t\tconst { value, done } = await reader.read();\n\t\t\tif (done) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\tlet consumed = consumeLine(buffer);\n\t\t\twhile (consumed) {\n\t\t\t\tbuffer = consumed.rest;\n\t\t\t\tconst event = decodeSseLine(consumed.line, state);\n\t\t\t\tif (event) {\n\t\t\t\t\tyield event;\n\t\t\t\t}\n\t\t\t\tconsumed = consumeLine(buffer);\n\t\t\t}\n\t\t}\n\n\t\tbuffer += decoder.decode();\n\t\tlet consumed = consumeLine(buffer);\n\t\twhile (consumed) {\n\t\t\tbuffer = consumed.rest;\n\t\t\tconst event = decodeSseLine(consumed.line, state);\n\t\t\tif (event) {\n\t\t\t\tyield event;\n\t\t\t}\n\t\t\tconsumed = consumeLine(buffer);\n\t\t}\n\n\t\tif (buffer.length > 0) {\n\t\t\tconst event = decodeSseLine(buffer, state);\n\t\t\tif (event) {\n\t\t\t\tyield event;\n\t\t\t}\n\t\t}\n\n\t\tconst trailingEvent = flushSseEvent(state);\n\t\tif (trailingEvent) {\n\t\t\tyield trailingEvent;\n\t\t}\n\t} finally {\n\t\treader.releaseLock();\n\t}\n}\n\n// SCRAMJET-DIVERGENCE: Preserve provider and callback failure facts independently.\nasync function* iterateAnthropicEvents(\n\tresponse: Response,\n\tsignal?: AbortSignal,\n): AsyncGenerator<RawMessageStreamEvent> {\n\tif (!response.body) {\n\t\tthrow new RequestFailureError(\"Anthropic response has no body.\", {\n\t\t\tschemaVersion: 1,\n\t\t\tkind: \"stream\",\n\t\t\treason: \"missing_body\",\n\t\t});\n\t}\n\n\tlet sawMessageStart = false;\n\tlet sawMessageEnd = false;\n\n\tfor await (const sse of iterateSseMessages(response.body, signal)) {\n\t\tif (sse.event === \"error\") {\n\t\t\tlet value: unknown;\n\t\t\ttry {\n\t\t\t\tvalue = JSON.parse(sse.data);\n\t\t\t} catch {\n\t\t\t\tthrow new RequestFailureError(\"Malformed Anthropic error event.\", {\n\t\t\t\t\tschemaVersion: 1,\n\t\t\t\t\tkind: \"stream\",\n\t\t\t\t\treason: \"malformed_event\",\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (\n\t\t\t\t!value ||\n\t\t\t\ttypeof value !== \"object\" ||\n\t\t\t\tArray.isArray(value) ||\n\t\t\t\t!(\"error\" in value) ||\n\t\t\t\t!value.error ||\n\t\t\t\ttypeof value.error !== \"object\" ||\n\t\t\t\tArray.isArray(value.error)\n\t\t\t)\n\t\t\t\tthrow new RequestFailureError(\"Malformed Anthropic error event.\", {\n\t\t\t\t\tschemaVersion: 1,\n\t\t\t\t\tkind: \"stream\",\n\t\t\t\t\treason: \"malformed_event\",\n\t\t\t\t});\n\t\t\tthrow new RequestFailureError(\n\t\t\t\tsse.data,\n\t\t\t\tfailureFromProviderError(value) ?? { schemaVersion: 1, kind: \"provider\", category: \"unknown\" },\n\t\t\t);\n\t\t}\n\n\t\tif (!ANTHROPIC_MESSAGE_EVENTS.has(sse.event ?? \"\")) {\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst event = parseJsonWithRepair<RawMessageStreamEvent>(sse.data);\n\t\t\tif (event.type === \"message_start\") {\n\t\t\t\tsawMessageStart = true;\n\t\t\t} else if (event.type === \"message_stop\") {\n\t\t\t\tsawMessageEnd = true;\n\t\t\t}\n\t\t\tyield event;\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tthrow new RequestFailureError(\n\t\t\t\t`Could not parse Anthropic SSE event ${sse.event}: ${message}; data=${sse.data}; raw=${sse.raw.join(\"\\\\n\")}`,\n\t\t\t\t{ schemaVersion: 1, kind: \"stream\", reason: \"malformed_event\" },\n\t\t\t);\n\t\t}\n\t}\n\n\tif (sawMessageStart && !sawMessageEnd) {\n\t\tthrow new RequestFailureError(\"Anthropic stream ended before message_stop\", {\n\t\t\tschemaVersion: 1,\n\t\t\tkind: \"stream\",\n\t\t\treason: \"missing_terminal_event\",\n\t\t});\n\t}\n}\n\nexport const streamAnthropic: StreamFunction<\"anthropic-messages\", AnthropicOptions> = (\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: Context,\n\toptions?: AnthropicOptions,\n): AssistantMessageEventStream => {\n\tconst stream = new AssistantMessageEventStream();\n\n\t(async () => {\n\t\tconst output: AssistantMessage = {\n\t\t\trole: \"assistant\",\n\t\t\tcontent: [],\n\t\t\tapi: model.api as Api,\n\t\t\tprovider: model.provider,\n\t\t\tmodel: model.id,\n\t\t\tusage: {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\ttotalTokens: 0,\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t},\n\t\t\tstopReason: \"stop\",\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\n\t\tlet requestStarted = false;\n\t\tconst abortScope = createProviderAbortScope(options?.signal);\n\t\ttry {\n\t\t\tlet client: Anthropic;\n\t\t\tlet isOAuth: boolean;\n\n\t\t\tif (options?.client) {\n\t\t\t\tclient = options.client;\n\t\t\t\tisOAuth = false;\n\t\t\t} else {\n\t\t\t\tconst apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? \"\";\n\n\t\t\t\tlet copilotDynamicHeaders: Record<string, string> | undefined;\n\t\t\t\tif (model.provider === \"github-copilot\") {\n\t\t\t\t\tconst hasImages = hasCopilotVisionInput(context.messages);\n\t\t\t\t\tcopilotDynamicHeaders = buildCopilotDynamicHeaders({\n\t\t\t\t\t\tmessages: context.messages,\n\t\t\t\t\t\thasImages,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tconst cacheRetention = options?.cacheRetention ?? resolveCacheRetention();\n\t\t\t\tconst cacheSessionId = cacheRetention === \"none\" ? undefined : options?.sessionId;\n\n\t\t\t\tconst created = createClient(\n\t\t\t\t\tmodel,\n\t\t\t\t\tapiKey,\n\t\t\t\t\toptions?.interleavedThinking ?? true,\n\t\t\t\t\tshouldUseFineGrainedToolStreamingBeta(model, context),\n\t\t\t\t\toptions?.headers,\n\t\t\t\t\tcopilotDynamicHeaders,\n\t\t\t\t\tcacheSessionId,\n\t\t\t\t\t(input, init) => {\n\t\t\t\t\t\trequestStarted = true;\n\t\t\t\t\t\treturn fetch(input, init);\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tclient = created.client;\n\t\t\t\tisOAuth = created.isOAuthToken;\n\t\t\t}\n\t\t\tlet params = buildParams(model, context, isOAuth, options);\n\t\t\tconst nextParams = await invokeProviderCallback(\"onPayload\", () => options?.onPayload?.(params, model));\n\t\t\tif (nextParams !== undefined) {\n\t\t\t\tparams = nextParams as MessageCreateParamsStreaming;\n\t\t\t}\n\t\t\tconst requestOptions = {\n\t\t\t\tsignal: abortScope.signal,\n\t\t\t\t...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),\n\t\t\t\t...(options?.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}),\n\t\t\t};\n\t\t\tif (options?.client) requestStarted = true;\n\t\t\tconst response = await client.messages.create({ ...params, stream: true }, requestOptions).asResponse();\n\t\t\tawait invokeProviderCallback(\"onResponse\", () =>\n\t\t\t\toptions?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model),\n\t\t\t);\n\t\t\tstream.push({ type: \"start\", partial: output });\n\n\t\t\ttype Block = (ThinkingContent | TextContent | (ToolCall & { partialJson: string })) & { index: number };\n\t\t\tconst blocks = output.content as Block[];\n\n\t\t\tfor await (const event of iterateAnthropicEvents(response, abortScope.signal)) {\n\t\t\t\tif (event.type === \"message_start\") {\n\t\t\t\t\toutput.responseId = event.message.id;\n\t\t\t\t\t// Capture initial token usage from message_start event\n\t\t\t\t\t// This ensures we have input token counts even if the stream is aborted early\n\t\t\t\t\toutput.usage.input = event.message.usage.input_tokens || 0;\n\t\t\t\t\toutput.usage.output = event.message.usage.output_tokens || 0;\n\t\t\t\t\toutput.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0;\n\t\t\t\t\toutput.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0;\n\t\t\t\t\t// Anthropic doesn't provide total_tokens, compute from components\n\t\t\t\t\toutput.usage.totalTokens =\n\t\t\t\t\t\toutput.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;\n\t\t\t\t\tcalculateCost(model, output.usage);\n\t\t\t\t} else if (event.type === \"content_block_start\") {\n\t\t\t\t\tif (event.content_block.type === \"text\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"\",\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"text_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t} else if (event.content_block.type === \"thinking\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\tthinking: \"\",\n\t\t\t\t\t\t\tthinkingSignature: \"\",\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"thinking_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t} else if (event.content_block.type === \"redacted_thinking\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\tthinking: \"[Reasoning redacted]\",\n\t\t\t\t\t\t\tthinkingSignature: event.content_block.data,\n\t\t\t\t\t\t\tredacted: true,\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"thinking_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t} else if (event.content_block.type === \"tool_use\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\t\t\tid: event.content_block.id,\n\t\t\t\t\t\t\tname: isOAuth\n\t\t\t\t\t\t\t\t? fromClaudeCodeName(event.content_block.name, context.tools)\n\t\t\t\t\t\t\t\t: event.content_block.name,\n\t\t\t\t\t\t\targuments: (event.content_block.input as Record<string, any>) ?? {},\n\t\t\t\t\t\t\tpartialJson: \"\",\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"toolcall_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t}\n\t\t\t\t} else if (event.type === \"content_block_delta\") {\n\t\t\t\t\tif (event.delta.type === \"text_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"text\") {\n\t\t\t\t\t\t\tblock.text += event.delta.text;\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"text_delta\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tdelta: event.delta.text,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (event.delta.type === \"thinking_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"thinking\") {\n\t\t\t\t\t\t\tblock.thinking += event.delta.thinking;\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"thinking_delta\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tdelta: event.delta.thinking,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (event.delta.type === \"input_json_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"toolCall\") {\n\t\t\t\t\t\t\tblock.partialJson += event.delta.partial_json;\n\t\t\t\t\t\t\tblock.arguments = parseStreamingJson(block.partialJson);\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"toolcall_delta\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tdelta: event.delta.partial_json,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (event.delta.type === \"signature_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"thinking\") {\n\t\t\t\t\t\t\tblock.thinkingSignature = block.thinkingSignature || \"\";\n\t\t\t\t\t\t\tblock.thinkingSignature += event.delta.signature;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (event.type === \"content_block_stop\") {\n\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\tif (block) {\n\t\t\t\t\t\tdelete (block as any).index;\n\t\t\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"text_end\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tcontent: block.text,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"thinking_end\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tcontent: block.thinking,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\t\t\tblock.arguments = parseStreamingJson(block.partialJson);\n\t\t\t\t\t\t\t// Finalize in-place and strip the scratch buffer so replay only\n\t\t\t\t\t\t\t// carries parsed arguments.\n\t\t\t\t\t\t\tdelete (block as { partialJson?: string }).partialJson;\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"toolcall_end\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\ttoolCall: block,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (event.type === \"message_delta\") {\n\t\t\t\t\tif (event.delta.stop_reason) {\n\t\t\t\t\t\tif (output.stopReason !== \"error\") output.stopReason = mapStopReason(event.delta.stop_reason);\n\t\t\t\t\t}\n\t\t\t\t\t// Only update usage fields if present (not null).\n\t\t\t\t\t// Preserves input_tokens from message_start when proxies omit it in message_delta.\n\t\t\t\t\tif (event.usage.input_tokens != null) {\n\t\t\t\t\t\toutput.usage.input = event.usage.input_tokens;\n\t\t\t\t\t}\n\t\t\t\t\tif (event.usage.output_tokens != null) {\n\t\t\t\t\t\toutput.usage.output = event.usage.output_tokens;\n\t\t\t\t\t}\n\t\t\t\t\tif (event.usage.cache_read_input_tokens != null) {\n\t\t\t\t\t\toutput.usage.cacheRead = event.usage.cache_read_input_tokens;\n\t\t\t\t\t}\n\t\t\t\t\tif (event.usage.cache_creation_input_tokens != null) {\n\t\t\t\t\t\toutput.usage.cacheWrite = event.usage.cache_creation_input_tokens;\n\t\t\t\t\t}\n\t\t\t\t\t// Anthropic doesn't provide total_tokens, compute from components\n\t\t\t\t\toutput.usage.totalTokens =\n\t\t\t\t\t\toutput.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;\n\t\t\t\t\tcalculateCost(model, output.usage);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (options?.signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\n\t\t\tif (output.stopReason === \"error\")\n\t\t\t\tthrow new RequestFailureError(\"Anthropic rejected the output.\", {\n\t\t\t\t\tschemaVersion: 1,\n\t\t\t\t\tkind: \"provider\",\n\t\t\t\t\tcategory: \"content_rejection\",\n\t\t\t\t});\n\t\t\tif (output.stopReason === \"aborted\") throw new Error(\"Request was aborted\");\n\n\t\t\tstream.push({ type: \"done\", reason: output.stopReason, message: output });\n\t\t\tstream.end();\n\t\t} catch (error) {\n\t\t\tfor (const block of output.content) {\n\t\t\t\tdelete (block as { index?: number }).index;\n\t\t\t\t// partialJson is only a streaming scratch buffer; never persist it.\n\t\t\t\tdelete (block as { partialJson?: string }).partialJson;\n\t\t\t}\n\t\t\toutput.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n\t\t\toutput.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);\n\t\t\tappendBuiltinFailure(\n\t\t\t\toutput,\n\t\t\t\trequestStarted && !options?.signal?.aborted && error instanceof Anthropic.APIConnectionError\n\t\t\t\t\t? new RequestFailureError(output.errorMessage, {\n\t\t\t\t\t\t\tschemaVersion: 1,\n\t\t\t\t\t\t\tkind: \"provider\",\n\t\t\t\t\t\t\tcategory: error instanceof Anthropic.APIConnectionTimeoutError ? \"timeout\" : \"transport\",\n\t\t\t\t\t\t})\n\t\t\t\t\t: error,\n\t\t\t\t!requestStarted,\n\t\t\t);\n\t\t\tstream.push({ type: \"error\", reason: output.stopReason, error: output });\n\t\t\tstream.end();\n\t\t} finally {\n\t\t\t// SCRAMJET-DIVERGENCE: finish cleanup synchronously before terminal consumers resume (#587).\n\t\t\tabortScope.dispose();\n\t\t}\n\t})();\n\n\treturn stream;\n};\n\n// SCRAMJET-DIVERGENCE: extended with opus-4-8, fable-5, sonnet-5 patterns for new model support\nfunction supportsAdaptiveThinking(modelId: string): boolean {\n\tconst normalized = normalizeForPatternMatch(modelId);\n\treturn ADAPTIVE_THINKING_PATTERNS.some((p) => normalized.includes(p));\n}\n\n// SCRAMJET-DIVERGENCE: native xhigh effort support for Opus 4.8/Fable 5 models\nfunction supportsNativeXhighEffort(modelId: string): boolean {\n\tconst normalized = normalizeForPatternMatch(modelId);\n\treturn NATIVE_XHIGH_EFFORT_PATTERNS.some((p) => normalized.includes(p));\n}\n\n/**\n * Map ThinkingLevel to Anthropic effort levels for adaptive thinking.\n * Note: effort \"max\" is only valid on Opus 4.6; Opus 4.7+ and Fable 5 support \"xhigh\".\n */\nfunction mapThinkingLevelToEffort(\n\tmodel: Model<\"anthropic-messages\">,\n\tlevel: SimpleStreamOptions[\"reasoning\"],\n): AnthropicEffort {\n\tconst mapped = level ? model.thinkingLevelMap?.[level] : undefined;\n\tif (typeof mapped === \"string\") return mapped as AnthropicEffort;\n\n\tswitch (level) {\n\t\tcase \"minimal\":\n\t\tcase \"low\":\n\t\t\treturn \"low\";\n\t\tcase \"medium\":\n\t\t\treturn \"medium\";\n\t\tcase \"high\":\n\t\t\treturn \"high\";\n\t\tcase \"xhigh\":\n\t\t\treturn supportsNativeXhighEffort(model.id) ? \"xhigh\" : \"high\";\n\t\tdefault:\n\t\t\treturn \"high\";\n\t}\n}\n\nexport const streamSimpleAnthropic: StreamFunction<\"anthropic-messages\", SimpleStreamOptions> = (\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream => {\n\tconst apiKey = options?.apiKey || getEnvApiKey(model.provider);\n\tif (!apiKey) {\n\t\tthrow new Error(`No API key for provider: ${model.provider}`);\n\t}\n\n\tconst base = buildBaseOptions(model, options, apiKey);\n\tif (!options?.reasoning) {\n\t\treturn streamAnthropic(model, context, { ...base, thinkingEnabled: false } satisfies AnthropicOptions);\n\t}\n\n\t// For adaptive-thinking models: use effort level.\n\t// For older models: use budget-based thinking.\n\tif (supportsAdaptiveThinking(model.id)) {\n\t\tconst effort = mapThinkingLevelToEffort(model, options.reasoning);\n\t\treturn streamAnthropic(model, context, {\n\t\t\t...base,\n\t\t\tthinkingEnabled: true,\n\t\t\teffort,\n\t\t} satisfies AnthropicOptions);\n\t}\n\n\tconst adjusted = adjustMaxTokensForThinking(\n\t\tbase.maxTokens || 0,\n\t\tMath.min(model.maxTokens, options.maxTokens ?? Infinity),\n\t\toptions.reasoning,\n\t\toptions.thinkingBudgets,\n\t);\n\n\t// SCRAMJET-DIVERGENCE: An explicit output ceiling includes thinking; never resurrect a zero budget.\n\tif (adjusted.thinkingBudget < 1024) {\n\t\tthrow new Error(\"Insufficient output space for Anthropic thinking; reduce input or request more output space.\");\n\t}\n\treturn streamAnthropic(model, context, {\n\t\t...base,\n\t\tmaxTokens: adjusted.maxTokens,\n\t\tthinkingEnabled: true,\n\t\tthinkingBudgetTokens: adjusted.thinkingBudget,\n\t} satisfies AnthropicOptions);\n};\n\nfunction isOAuthToken(apiKey: string): boolean {\n\treturn apiKey.includes(\"sk-ant-oat\");\n}\n\nfunction createClient(\n\tmodel: Model<\"anthropic-messages\">,\n\tapiKey: string,\n\tinterleavedThinking: boolean,\n\tuseFineGrainedToolStreamingBeta: boolean,\n\toptionsHeaders?: Record<string, string>,\n\tdynamicHeaders?: Record<string, string>,\n\tsessionId?: string,\n\tfetchImplementation?: typeof fetch,\n): { client: Anthropic; isOAuthToken: boolean } {\n\t// Adaptive-thinking models have interleaved thinking built in, so skip the beta header.\n\tconst needsInterleavedBeta = interleavedThinking && !supportsAdaptiveThinking(model.id);\n\tconst betaFeatures: string[] = [];\n\tif (useFineGrainedToolStreamingBeta) {\n\t\tbetaFeatures.push(FINE_GRAINED_TOOL_STREAMING_BETA);\n\t}\n\tif (needsInterleavedBeta) {\n\t\tbetaFeatures.push(INTERLEAVED_THINKING_BETA);\n\t}\n\n\tif (model.provider === \"cloudflare-ai-gateway\") {\n\t\tconst client = new Anthropic({\n\t\t\tapiKey: null,\n\t\t\tauthToken: null,\n\t\t\tbaseURL: resolveCloudflareBaseUrl(model),\n\t\t\tfetch: fetchImplementation,\n\t\t\tdangerouslyAllowBrowser: true,\n\t\t\tdefaultHeaders: mergeHeaders(\n\t\t\t\t{\n\t\t\t\t\taccept: \"application/json\",\n\t\t\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t\t\t\"cf-aig-authorization\": `Bearer ${apiKey}`,\n\t\t\t\t\t\"x-api-key\": null,\n\t\t\t\t\tAuthorization: null,\n\t\t\t\t\t...(betaFeatures.length > 0 ? { \"anthropic-beta\": betaFeatures.join(\",\") } : {}),\n\t\t\t\t},\n\t\t\t\tmodel.headers,\n\t\t\t\toptionsHeaders,\n\t\t\t),\n\t\t});\n\n\t\treturn { client, isOAuthToken: false };\n\t}\n\n\t// Copilot: Bearer auth, selective betas.\n\tif (model.provider === \"github-copilot\") {\n\t\tconst client = new Anthropic({\n\t\t\tfetch: fetchImplementation,\n\t\t\tapiKey: null,\n\t\t\tauthToken: apiKey,\n\t\t\tbaseURL: model.baseUrl,\n\t\t\tdangerouslyAllowBrowser: true,\n\t\t\tdefaultHeaders: mergeHeaders(\n\t\t\t\t{\n\t\t\t\t\taccept: \"application/json\",\n\t\t\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t\t\t...(betaFeatures.length > 0 ? { \"anthropic-beta\": betaFeatures.join(\",\") } : {}),\n\t\t\t\t},\n\t\t\t\tmodel.headers,\n\t\t\t\tdynamicHeaders,\n\t\t\t\toptionsHeaders,\n\t\t\t),\n\t\t});\n\n\t\treturn { client, isOAuthToken: false };\n\t}\n\n\t// OAuth: Bearer auth, Claude Code identity headers\n\tif (isOAuthToken(apiKey)) {\n\t\tconst client = new Anthropic({\n\t\t\tfetch: fetchImplementation,\n\t\t\tapiKey: null,\n\t\t\tauthToken: apiKey,\n\t\t\tbaseURL: model.baseUrl,\n\t\t\tdangerouslyAllowBrowser: true,\n\t\t\tdefaultHeaders: mergeHeaders(\n\t\t\t\t{\n\t\t\t\t\taccept: \"application/json\",\n\t\t\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t\t\t\"anthropic-beta\": [\"claude-code-20250219\", \"oauth-2025-04-20\", ...betaFeatures].join(\",\"),\n\t\t\t\t\t\"user-agent\": `claude-cli/${claudeCodeVersion}`,\n\t\t\t\t\t\"x-app\": \"cli\",\n\t\t\t\t},\n\t\t\t\tmodel.headers,\n\t\t\t\toptionsHeaders,\n\t\t\t),\n\t\t});\n\n\t\treturn { client, isOAuthToken: true };\n\t}\n\n\t// API key auth\n\tconst sessionAffinityHeaders: Record<string, string | null> =\n\t\tsessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { \"x-session-affinity\": sessionId } : {};\n\tconst client = new Anthropic({\n\t\tapiKey,\n\t\tbaseURL: model.baseUrl,\n\t\tfetch: fetchImplementation,\n\t\tdangerouslyAllowBrowser: true,\n\t\tdefaultHeaders: mergeHeaders(\n\t\t\t{\n\t\t\t\taccept: \"application/json\",\n\t\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t\t...(betaFeatures.length > 0 ? { \"anthropic-beta\": betaFeatures.join(\",\") } : {}),\n\t\t\t},\n\t\t\tsessionAffinityHeaders,\n\t\t\tmodel.headers,\n\t\t\toptionsHeaders,\n\t\t),\n\t});\n\n\treturn { client, isOAuthToken: false };\n}\n\nconst claudeCodeIdentity = \"You are Claude Code, Anthropic's official CLI for Claude.\";\n\n/**\n * Builds the system block array for a sectioned system prompt.\n *\n * Anthropic allows at most 4 `cache_control` breakpoints per request, and the\n * last tool and last user message already consume two, so the system prompt\n * spends at most one: it lands on a single stable block folding the leading\n * run of sections up to the first `cacheRetention: \"none\"` section (zero\n * breakpoints when retention is \"none\"; when the first section is volatile\n * the breakpoint moves to the OAuth identity block, or is omitted entirely\n * on API-key auth).\n * Everything from the first volatile section onward trails as uncached blocks\n * in array order — anything after the breakpoint is excluded from the cached\n * prefix, and preserving array order keeps the concatenated block text\n * byte-identical (for well-formed text; `sanitizeSurrogates` runs per block)\n * to the flattened prompt other providers send. On OAuth the\n * Claude Code identity block stays first; caching is prefix-based, so the\n * stable block's breakpoint covers it without a second one (unlike the legacy\n * string path, which marks both for back-compat).\n */\nfunction buildSystemBlocks(\n\tsections: SystemPromptSection[],\n\tisOAuthToken: boolean,\n\tcacheControl?: CacheControlEphemeral,\n): TextBlockParam[] {\n\tconst blocks: TextBlockParam[] = [];\n\tconst nonEmpty = sections.filter((s) => s.text);\n\tconst firstVolatile = nonEmpty.findIndex((s) => s.cacheRetention === \"none\");\n\tconst stable = firstVolatile === -1 ? nonEmpty : nonEmpty.slice(0, firstVolatile);\n\tconst tail = firstVolatile === -1 ? [] : nonEmpty.slice(firstVolatile);\n\n\tif (isOAuthToken) {\n\t\tblocks.push({\n\t\t\ttype: \"text\",\n\t\t\ttext: claudeCodeIdentity,\n\t\t\t// Without stable sections the identity block is the whole stable\n\t\t\t// prefix, so the breakpoint lands here (as on the string path).\n\t\t\t...(stable.length === 0 && cacheControl ? { cache_control: cacheControl } : {}),\n\t\t});\n\t}\n\tif (stable.length > 0) {\n\t\tblocks.push({\n\t\t\ttype: \"text\",\n\t\t\ttext: sanitizeSurrogates(stable.map((s) => s.text).join(\"\")),\n\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t});\n\t}\n\tfor (const section of tail) {\n\t\tblocks.push({ type: \"text\", text: sanitizeSurrogates(section.text) });\n\t}\n\treturn blocks;\n}\n\nfunction buildParams(\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: Context,\n\tisOAuthToken: boolean,\n\toptions?: AnthropicOptions,\n): MessageCreateParamsStreaming {\n\tconst { cacheControl } = getCacheControl(model, options?.cacheRetention);\n\tconst params: MessageCreateParamsStreaming = {\n\t\tmodel: model.id,\n\t\tmessages: convertMessages(context.messages, model, isOAuthToken, cacheControl),\n\t\tmax_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,\n\t\tstream: true,\n\t};\n\n\tif (Array.isArray(context.systemPrompt)) {\n\t\tconst blocks = buildSystemBlocks(context.systemPrompt, isOAuthToken, cacheControl);\n\t\tif (blocks.length > 0) {\n\t\t\tparams.system = blocks;\n\t\t}\n\t} else {\n\t\tconst systemPrompt = context.systemPrompt;\n\n\t\t// For OAuth tokens, we MUST include Claude Code identity\n\t\tif (isOAuthToken) {\n\t\t\tparams.system = [\n\t\t\t\t{\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: claudeCodeIdentity,\n\t\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t\t},\n\t\t\t];\n\t\t\tif (systemPrompt) {\n\t\t\t\tparams.system.push({\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: sanitizeSurrogates(systemPrompt),\n\t\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (systemPrompt) {\n\t\t\t// Add cache control to system prompt for non-OAuth tokens\n\t\t\tparams.system = [\n\t\t\t\t{\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: sanitizeSurrogates(systemPrompt),\n\t\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t\t},\n\t\t\t];\n\t\t}\n\t}\n\n\t// Temperature is incompatible with extended thinking (adaptive or budget-based).\n\t// SCRAMJET-DIVERGENCE: gate on supportsTemperature compat (opus-4-7+ rejects non-default temperature)\n\tif (\n\t\toptions?.temperature !== undefined &&\n\t\t!options?.thinkingEnabled &&\n\t\tgetAnthropicCompat(model).supportsTemperature\n\t) {\n\t\tparams.temperature = options.temperature;\n\t}\n\n\tif (context.tools && context.tools.length > 0) {\n\t\tconst compat = getAnthropicCompat(model);\n\t\tparams.tools = convertTools(\n\t\t\tcontext.tools,\n\t\t\tisOAuthToken,\n\t\t\tcompat.supportsEagerToolInputStreaming,\n\t\t\tcompat.supportsCacheControlOnTools ? cacheControl : undefined,\n\t\t);\n\t}\n\n\t// Configure thinking mode: adaptive-thinking models,\n\t// budget-based older models, or explicitly disabled.\n\tif (model.reasoning) {\n\t\tif (options?.thinkingEnabled) {\n\t\t\t// Default to \"summarized\" so Opus 4.7 and Mythos Preview behave like\n\t\t\t// older Claude 4 models (whose API default is also \"summarized\").\n\t\t\tconst display: AnthropicThinkingDisplay = options.thinkingDisplay ?? \"summarized\";\n\t\t\tif (supportsAdaptiveThinking(model.id)) {\n\t\t\t\t// Adaptive thinking: Claude decides when and how much to think.\n\t\t\t\tparams.thinking = { type: \"adaptive\", display };\n\t\t\t\tif (options.effort) {\n\t\t\t\t\t// The Anthropic SDK types can lag newly supported effort values such as \"xhigh\".\n\t\t\t\t\tparams.output_config =\n\t\t\t\t\t\toptions.effort === \"xhigh\"\n\t\t\t\t\t\t\t? ({ effort: options.effort } as unknown as NonNullable<\n\t\t\t\t\t\t\t\t\tMessageCreateParamsStreaming[\"output_config\"]\n\t\t\t\t\t\t\t\t>)\n\t\t\t\t\t\t\t: { effort: options.effort };\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Budget-based thinking for older models\n\t\t\t\tparams.thinking = {\n\t\t\t\t\ttype: \"enabled\",\n\t\t\t\t\tbudget_tokens: options.thinkingBudgetTokens || 1024,\n\t\t\t\t\tdisplay,\n\t\t\t\t};\n\t\t\t}\n\t\t} else if (options?.thinkingEnabled === false) {\n\t\t\tparams.thinking = { type: \"disabled\" };\n\t\t}\n\t}\n\n\tif (options?.metadata) {\n\t\tconst userId = options.metadata.user_id;\n\t\tif (typeof userId === \"string\") {\n\t\t\tparams.metadata = { user_id: userId };\n\t\t}\n\t}\n\n\tif (options?.toolChoice) {\n\t\tif (typeof options.toolChoice === \"string\") {\n\t\t\tparams.tool_choice = { type: options.toolChoice };\n\t\t} else {\n\t\t\tparams.tool_choice = options.toolChoice;\n\t\t}\n\t}\n\n\treturn params;\n}\n\n// Normalize tool call IDs to match Anthropic's required pattern and length\nfunction normalizeToolCallId(id: string): string {\n\treturn id.replace(/[^a-zA-Z0-9_-]/g, \"_\").slice(0, 64);\n}\n\nfunction convertMessages(\n\tmessages: Message[],\n\tmodel: Model<\"anthropic-messages\">,\n\tisOAuthToken: boolean,\n\tcacheControl?: CacheControlEphemeral,\n): MessageParam[] {\n\tconst params: MessageParam[] = [];\n\n\t// Transform messages for cross-provider compatibility\n\tconst transformedMessages = transformMessages(messages, model, normalizeToolCallId);\n\n\tfor (let i = 0; i < transformedMessages.length; i++) {\n\t\tconst msg = transformedMessages[i];\n\n\t\tif (msg.role === \"user\") {\n\t\t\tif (typeof msg.content === \"string\") {\n\t\t\t\tif (msg.content.trim().length > 0) {\n\t\t\t\t\tparams.push({\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: sanitizeSurrogates(msg.content),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst blocks: ContentBlockParam[] = msg.content.map((item) => {\n\t\t\t\t\tif (item.type === \"text\") {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: sanitizeSurrogates(item.text),\n\t\t\t\t\t\t};\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttype: \"image\",\n\t\t\t\t\t\t\tsource: {\n\t\t\t\t\t\t\t\ttype: \"base64\",\n\t\t\t\t\t\t\t\tmedia_type: item.mimeType as \"image/jpeg\" | \"image/png\" | \"image/gif\" | \"image/webp\",\n\t\t\t\t\t\t\t\tdata: item.data,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tconst filteredBlocks = blocks.filter((b) => {\n\t\t\t\t\tif (b.type === \"text\") {\n\t\t\t\t\t\treturn b.text.trim().length > 0;\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t});\n\t\t\t\tif (filteredBlocks.length === 0) continue;\n\t\t\t\tparams.push({\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: filteredBlocks,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (msg.role === \"assistant\") {\n\t\t\tconst blocks: ContentBlockParam[] = [];\n\n\t\t\tfor (const block of msg.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\tif (block.text.trim().length === 0) continue;\n\t\t\t\t\tblocks.push({\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: sanitizeSurrogates(block.text),\n\t\t\t\t\t});\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\t// Redacted thinking: pass the opaque payload back as redacted_thinking\n\t\t\t\t\tif (block.redacted) {\n\t\t\t\t\t\tblocks.push({\n\t\t\t\t\t\t\ttype: \"redacted_thinking\",\n\t\t\t\t\t\t\tdata: block.thinkingSignature!,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (block.thinking.trim().length === 0) continue;\n\t\t\t\t\t// If thinking signature is missing/empty (e.g., from aborted stream),\n\t\t\t\t\t// convert to plain text block without <thinking> tags to avoid API rejection\n\t\t\t\t\t// and prevent Claude from mimicking the tags in responses\n\t\t\t\t\tif (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {\n\t\t\t\t\t\tblocks.push({\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: sanitizeSurrogates(block.thinking),\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\tblocks.push({\n\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\tthinking: sanitizeSurrogates(block.thinking),\n\t\t\t\t\t\t\tsignature: block.thinkingSignature,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tblocks.push({\n\t\t\t\t\t\ttype: \"tool_use\",\n\t\t\t\t\t\t// SCRAMJET-DIVERGENCE: Sanitize tool-call IDs unconditionally on the outgoing\n\t\t\t\t\t\t// request. `transform-messages.ts` only normalizes IDs for cross-model replay\n\t\t\t\t\t\t// (`!isSameModel`), so a legacy session carrying a provider-invalid ID (e.g. one\n\t\t\t\t\t\t// embedding a raw model id like `gpt-5.5`) replayed to the SAME model would be sent\n\t\t\t\t\t\t// verbatim and rejected by Anthropic's `^[a-zA-Z0-9_-]+$` constraint. Applying the\n\t\t\t\t\t\t// idempotent `normalizeToolCallId` here (and at the matching tool_result sites below)\n\t\t\t\t\t\t// is harmless on already-clean IDs and preserves call/result correlation because the\n\t\t\t\t\t\t// same deterministic function maps both halves to the same value.\n\t\t\t\t\t\tid: normalizeToolCallId(block.id),\n\t\t\t\t\t\tname: isOAuthToken ? toClaudeCodeName(block.name) : block.name,\n\t\t\t\t\t\tinput: block.arguments ?? {},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (blocks.length === 0) continue;\n\t\t\tparams.push({\n\t\t\t\trole: \"assistant\",\n\t\t\t\tcontent: blocks,\n\t\t\t});\n\t\t} else if (msg.role === \"toolResult\") {\n\t\t\t// Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint\n\t\t\tconst toolResults: ContentBlockParam[] = [];\n\n\t\t\t// Add the current tool result\n\t\t\ttoolResults.push({\n\t\t\t\ttype: \"tool_result\",\n\t\t\t\t// SCRAMJET-DIVERGENCE: sanitize to keep correlation with the tool_use id above.\n\t\t\t\ttool_use_id: normalizeToolCallId(msg.toolCallId),\n\t\t\t\tcontent: convertContentBlocks(msg.content),\n\t\t\t\tis_error: msg.isError,\n\t\t\t});\n\n\t\t\t// Look ahead for consecutive toolResult messages\n\t\t\tlet j = i + 1;\n\t\t\twhile (j < transformedMessages.length && transformedMessages[j].role === \"toolResult\") {\n\t\t\t\tconst nextMsg = transformedMessages[j] as ToolResultMessage; // We know it's a toolResult\n\t\t\t\ttoolResults.push({\n\t\t\t\t\ttype: \"tool_result\",\n\t\t\t\t\t// SCRAMJET-DIVERGENCE: sanitize to keep correlation with the tool_use id above.\n\t\t\t\t\ttool_use_id: normalizeToolCallId(nextMsg.toolCallId),\n\t\t\t\t\tcontent: convertContentBlocks(nextMsg.content),\n\t\t\t\t\tis_error: nextMsg.isError,\n\t\t\t\t});\n\t\t\t\tj++;\n\t\t\t}\n\n\t\t\t// Skip the messages we've already processed\n\t\t\ti = j - 1;\n\n\t\t\t// Add a single user message with all tool results\n\t\t\tparams.push({\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: toolResults,\n\t\t\t});\n\t\t}\n\t}\n\n\t// Add cache_control to the last user message to cache conversation history\n\tif (cacheControl && params.length > 0) {\n\t\tconst lastMessage = params[params.length - 1];\n\t\tif (lastMessage.role === \"user\") {\n\t\t\tif (Array.isArray(lastMessage.content)) {\n\t\t\t\tconst lastBlock = lastMessage.content[lastMessage.content.length - 1];\n\t\t\t\tif (\n\t\t\t\t\tlastBlock &&\n\t\t\t\t\t(lastBlock.type === \"text\" || lastBlock.type === \"image\" || lastBlock.type === \"tool_result\")\n\t\t\t\t) {\n\t\t\t\t\t(lastBlock as any).cache_control = cacheControl;\n\t\t\t\t}\n\t\t\t} else if (typeof lastMessage.content === \"string\") {\n\t\t\t\tlastMessage.content = [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: lastMessage.content,\n\t\t\t\t\t\tcache_control: cacheControl,\n\t\t\t\t\t},\n\t\t\t\t] as any;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn params;\n}\n\nfunction shouldUseFineGrainedToolStreamingBeta(model: Model<\"anthropic-messages\">, context: Context): boolean {\n\treturn !!context.tools?.length && !getAnthropicCompat(model).supportsEagerToolInputStreaming;\n}\n\nfunction convertTools(\n\ttools: Tool[],\n\tisOAuthToken: boolean,\n\tsupportsEagerToolInputStreaming: boolean,\n\tcacheControl?: CacheControlEphemeral,\n): Anthropic.Messages.Tool[] {\n\tif (!tools) return [];\n\n\treturn tools.map((tool, index) => {\n\t\tconst schema = tool.parameters as { properties?: unknown; required?: string[] };\n\n\t\treturn {\n\t\t\tname: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name,\n\t\t\tdescription: tool.description,\n\t\t\t...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}),\n\t\t\tinput_schema: {\n\t\t\t\ttype: \"object\",\n\t\t\t\tproperties: schema.properties ?? {},\n\t\t\t\trequired: schema.required ?? [],\n\t\t\t},\n\t\t\t...(cacheControl && index === tools.length - 1 ? { cache_control: cacheControl } : {}),\n\t\t};\n\t});\n}\n\nfunction mapStopReason(reason: Anthropic.Messages.StopReason | string): StopReason {\n\tswitch (reason) {\n\t\tcase \"end_turn\":\n\t\t\treturn \"stop\";\n\t\tcase \"max_tokens\":\n\t\t\treturn \"length\";\n\t\tcase \"tool_use\":\n\t\t\treturn \"toolUse\";\n\t\tcase \"refusal\":\n\t\t\treturn \"error\";\n\t\tcase \"pause_turn\": // Stop is good enough -> resubmit\n\t\t\treturn \"stop\";\n\t\tcase \"stop_sequence\":\n\t\t\treturn \"stop\"; // We don't supply stop sequences, so this should never happen\n\t\tcase \"sensitive\": // Content flagged by safety filters (not yet in SDK types)\n\t\t\treturn \"error\";\n\t\tdefault:\n\t\t\t// Handle unknown stop reasons gracefully (API may add new values)\n\t\t\tthrow new Error(`Unhandled stop reason: ${reason}`);\n\t}\n}\n"]}
@@ -5,6 +5,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js";
5
5
  import { appendBuiltinFailure, failureFromProviderError, invokeProviderCallback, RequestFailureError, } from "../utils/failure-evidence.js";
6
6
  import { headersToRecord } from "../utils/headers.js";
7
7
  import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.js";
8
+ import { createProviderAbortScope } from "../utils/provider-abort-scope.js";
8
9
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
9
10
  import { ADAPTIVE_THINKING_PATTERNS, NATIVE_XHIGH_EFFORT_PATTERNS, normalizeForPatternMatch, } from "./anthropic-model-patterns.js";
10
11
  import { resolveCloudflareBaseUrl } from "./cloudflare.js";
@@ -343,6 +344,7 @@ export const streamAnthropic = (model, context, options) => {
343
344
  timestamp: Date.now(),
344
345
  };
345
346
  let requestStarted = false;
347
+ const abortScope = createProviderAbortScope(options?.signal);
346
348
  try {
347
349
  let client;
348
350
  let isOAuth;
@@ -375,7 +377,7 @@ export const streamAnthropic = (model, context, options) => {
375
377
  params = nextParams;
376
378
  }
377
379
  const requestOptions = {
378
- ...(options?.signal ? { signal: options.signal } : {}),
380
+ signal: abortScope.signal,
379
381
  ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
380
382
  ...(options?.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}),
381
383
  };
@@ -385,7 +387,7 @@ export const streamAnthropic = (model, context, options) => {
385
387
  await invokeProviderCallback("onResponse", () => options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model));
386
388
  stream.push({ type: "start", partial: output });
387
389
  const blocks = output.content;
388
- for await (const event of iterateAnthropicEvents(response, options?.signal)) {
390
+ for await (const event of iterateAnthropicEvents(response, abortScope.signal)) {
389
391
  if (event.type === "message_start") {
390
392
  output.responseId = event.message.id;
391
393
  // Capture initial token usage from message_start event
@@ -587,6 +589,10 @@ export const streamAnthropic = (model, context, options) => {
587
589
  stream.push({ type: "error", reason: output.stopReason, error: output });
588
590
  stream.end();
589
591
  }
592
+ finally {
593
+ // SCRAMJET-DIVERGENCE: finish cleanup synchronously before terminal consumers resume (#587).
594
+ abortScope.dispose();
595
+ }
590
596
  })();
591
597
  return stream;
592
598
  };