@averyyy/pi-ai 0.85.1-piclient.9 → 0.86.1-piclient.2

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.
Files changed (180) hide show
  1. package/README.md +50 -6
  2. package/dist/api/anthropic-messages.d.ts.map +1 -1
  3. package/dist/api/anthropic-messages.js +130 -86
  4. package/dist/api/anthropic-messages.js.map +1 -1
  5. package/dist/api/azure-openai-responses.d.ts.map +1 -1
  6. package/dist/api/azure-openai-responses.js +17 -5
  7. package/dist/api/azure-openai-responses.js.map +1 -1
  8. package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  9. package/dist/api/bedrock-converse-stream.js +13 -12
  10. package/dist/api/bedrock-converse-stream.js.map +1 -1
  11. package/dist/api/devin-context-map.d.ts +2 -2
  12. package/dist/api/devin-context-map.d.ts.map +1 -1
  13. package/dist/api/devin-context-map.js +4 -2
  14. package/dist/api/devin-context-map.js.map +1 -1
  15. package/dist/api/devin-stream.d.ts +2 -2
  16. package/dist/api/devin-stream.d.ts.map +1 -1
  17. package/dist/api/devin-stream.js.map +1 -1
  18. package/dist/api/google-generative-ai.d.ts.map +1 -1
  19. package/dist/api/google-generative-ai.js +21 -73
  20. package/dist/api/google-generative-ai.js.map +1 -1
  21. package/dist/api/google-shared.d.ts +13 -4
  22. package/dist/api/google-shared.d.ts.map +1 -1
  23. package/dist/api/google-shared.js +55 -4
  24. package/dist/api/google-shared.js.map +1 -1
  25. package/dist/api/google-vertex.d.ts.map +1 -1
  26. package/dist/api/google-vertex.js +25 -65
  27. package/dist/api/google-vertex.js.map +1 -1
  28. package/dist/api/mistral-conversations.d.ts.map +1 -1
  29. package/dist/api/mistral-conversations.js +19 -12
  30. package/dist/api/mistral-conversations.js.map +1 -1
  31. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  32. package/dist/api/openai-codex-responses.js +26 -23
  33. package/dist/api/openai-codex-responses.js.map +1 -1
  34. package/dist/api/openai-completions.d.ts +6 -4
  35. package/dist/api/openai-completions.d.ts.map +1 -1
  36. package/dist/api/openai-completions.js +40 -58
  37. package/dist/api/openai-completions.js.map +1 -1
  38. package/dist/api/openai-responses-shared.d.ts +7 -5
  39. package/dist/api/openai-responses-shared.d.ts.map +1 -1
  40. package/dist/api/openai-responses-shared.js +56 -56
  41. package/dist/api/openai-responses-shared.js.map +1 -1
  42. package/dist/api/openai-responses.d.ts.map +1 -1
  43. package/dist/api/openai-responses.js +14 -19
  44. package/dist/api/openai-responses.js.map +1 -1
  45. package/dist/api/pi-messages.d.ts +3 -3
  46. package/dist/api/pi-messages.d.ts.map +1 -1
  47. package/dist/api/pi-messages.js +2 -2
  48. package/dist/api/pi-messages.js.map +1 -1
  49. package/dist/api/simple-options.d.ts +3 -3
  50. package/dist/api/simple-options.d.ts.map +1 -1
  51. package/dist/api/simple-options.js.map +1 -1
  52. package/dist/api/transform-messages.d.ts.map +1 -1
  53. package/dist/api/transform-messages.js +21 -7
  54. package/dist/api/transform-messages.js.map +1 -1
  55. package/dist/auth/oauth/load.d.ts +2 -0
  56. package/dist/auth/oauth/load.d.ts.map +1 -1
  57. package/dist/auth/oauth/load.js +5 -0
  58. package/dist/auth/oauth/load.js.map +1 -1
  59. package/dist/auth/oauth/meta.d.ts +17 -0
  60. package/dist/auth/oauth/meta.d.ts.map +1 -0
  61. package/dist/auth/oauth/meta.js +190 -0
  62. package/dist/auth/oauth/meta.js.map +1 -0
  63. package/dist/bun-oauth.d.ts.map +1 -1
  64. package/dist/bun-oauth.js +2 -0
  65. package/dist/bun-oauth.js.map +1 -1
  66. package/dist/compat.d.ts +3 -3
  67. package/dist/compat.d.ts.map +1 -1
  68. package/dist/compat.js +9 -6
  69. package/dist/compat.js.map +1 -1
  70. package/dist/env-api-keys.d.ts.map +1 -1
  71. package/dist/env-api-keys.js +1 -0
  72. package/dist/env-api-keys.js.map +1 -1
  73. package/dist/image-models.generated.d.ts +30 -0
  74. package/dist/image-models.generated.d.ts.map +1 -1
  75. package/dist/image-models.generated.js +34 -4
  76. package/dist/image-models.generated.js.map +1 -1
  77. package/dist/index.d.ts +2 -1
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +2 -1
  80. package/dist/index.js.map +1 -1
  81. package/dist/models.d.ts +4 -3
  82. package/dist/models.d.ts.map +1 -1
  83. package/dist/models.generated.d.ts +4 -0
  84. package/dist/models.generated.d.ts.map +1 -1
  85. package/dist/models.generated.js +4 -0
  86. package/dist/models.generated.js.map +1 -1
  87. package/dist/models.js +5 -2
  88. package/dist/models.js.map +1 -1
  89. package/dist/providers/all.d.ts.map +1 -1
  90. package/dist/providers/all.js +2 -0
  91. package/dist/providers/all.js.map +1 -1
  92. package/dist/providers/data/.manifest.json +1 -1
  93. package/dist/providers/data/anthropic.json +1 -1
  94. package/dist/providers/data/baseten.json +1 -1
  95. package/dist/providers/data/cerebras.json +1 -1
  96. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  97. package/dist/providers/data/deepseek.json +1 -1
  98. package/dist/providers/data/fireworks.json +1 -1
  99. package/dist/providers/data/github-copilot.json +1 -1
  100. package/dist/providers/data/google-vertex.json +1 -1
  101. package/dist/providers/data/google.json +1 -1
  102. package/dist/providers/data/meta.json +1 -0
  103. package/dist/providers/data/mistral.json +1 -1
  104. package/dist/providers/data/moonshotai-cn.json +1 -1
  105. package/dist/providers/data/moonshotai.json +1 -1
  106. package/dist/providers/data/nvidia.json +1 -1
  107. package/dist/providers/data/openai-codex.json +1 -1
  108. package/dist/providers/data/openai.json +1 -1
  109. package/dist/providers/data/opencode-go.json +1 -1
  110. package/dist/providers/data/opencode.json +1 -1
  111. package/dist/providers/data/openrouter.json +1 -1
  112. package/dist/providers/data/qwen-token-plan-cn.json +1 -1
  113. package/dist/providers/data/qwen-token-plan.json +1 -1
  114. package/dist/providers/data/radius.json +1 -0
  115. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  116. package/dist/providers/data/zai-coding-cn.json +1 -1
  117. package/dist/providers/data/zai.json +1 -1
  118. package/dist/providers/faux.d.ts +2 -2
  119. package/dist/providers/faux.d.ts.map +1 -1
  120. package/dist/providers/faux.js +11 -11
  121. package/dist/providers/faux.js.map +1 -1
  122. package/dist/providers/meta.d.ts +3 -0
  123. package/dist/providers/meta.d.ts.map +1 -0
  124. package/dist/providers/meta.js +24 -0
  125. package/dist/providers/meta.js.map +1 -0
  126. package/dist/providers/meta.models.d.ts +4 -0
  127. package/dist/providers/meta.models.d.ts.map +1 -0
  128. package/dist/providers/meta.models.js +6 -0
  129. package/dist/providers/meta.models.js.map +1 -0
  130. package/dist/providers/opencode-go.d.ts.map +1 -1
  131. package/dist/providers/opencode-go.js +4 -3
  132. package/dist/providers/opencode-go.js.map +1 -1
  133. package/dist/providers/opencode-headers.d.ts +4 -0
  134. package/dist/providers/opencode-headers.d.ts.map +1 -0
  135. package/dist/providers/opencode-headers.js +22 -0
  136. package/dist/providers/opencode-headers.js.map +1 -0
  137. package/dist/providers/opencode.d.ts.map +1 -1
  138. package/dist/providers/opencode.js +5 -4
  139. package/dist/providers/opencode.js.map +1 -1
  140. package/dist/providers/radius.d.ts.map +1 -1
  141. package/dist/providers/radius.js +19 -5
  142. package/dist/providers/radius.js.map +1 -1
  143. package/dist/providers/radius.models.d.ts +4 -0
  144. package/dist/providers/radius.models.d.ts.map +1 -0
  145. package/dist/providers/radius.models.js +6 -0
  146. package/dist/providers/radius.models.js.map +1 -0
  147. package/dist/types.d.ts +98 -27
  148. package/dist/types.d.ts.map +1 -1
  149. package/dist/types.js.map +1 -1
  150. package/dist/utils/diagnostics.d.ts +3 -2
  151. package/dist/utils/diagnostics.d.ts.map +1 -1
  152. package/dist/utils/diagnostics.js.map +1 -1
  153. package/dist/utils/estimate.d.ts +2 -2
  154. package/dist/utils/estimate.d.ts.map +1 -1
  155. package/dist/utils/estimate.js +8 -29
  156. package/dist/utils/estimate.js.map +1 -1
  157. package/dist/utils/event-stream.d.ts.map +1 -1
  158. package/dist/utils/event-stream.js +25 -7
  159. package/dist/utils/event-stream.js.map +1 -1
  160. package/dist/utils/overflow.d.ts +1 -0
  161. package/dist/utils/overflow.d.ts.map +1 -1
  162. package/dist/utils/overflow.js +11 -5
  163. package/dist/utils/overflow.js.map +1 -1
  164. package/dist/utils/retry.d.ts +6 -1
  165. package/dist/utils/retry.d.ts.map +1 -1
  166. package/dist/utils/retry.js +40 -6
  167. package/dist/utils/retry.js.map +1 -1
  168. package/dist/utils/text.d.ts +9 -1
  169. package/dist/utils/text.d.ts.map +1 -1
  170. package/dist/utils/text.js +26 -0
  171. package/dist/utils/text.js.map +1 -1
  172. package/dist/utils/transcript.d.ts +84 -0
  173. package/dist/utils/transcript.d.ts.map +1 -0
  174. package/dist/utils/transcript.js +204 -0
  175. package/dist/utils/transcript.js.map +1 -0
  176. package/package.json +9 -9
  177. package/dist/utils/deferred-tools.d.ts +0 -9
  178. package/dist/utils/deferred-tools.d.ts.map +0 -1
  179. package/dist/utils/deferred-tools.js +0 -36
  180. package/dist/utils/deferred-tools.js.map +0 -1
package/README.md CHANGED
@@ -44,6 +44,7 @@ Unified LLM API with provider collections, automatic auth resolution, token and
44
44
  - [OpenAI Compatibility Settings](#openai-compatibility-settings)
45
45
  - [Faux Provider for Tests](#faux-provider-for-tests)
46
46
  - [Cross-Provider Handoffs](#cross-provider-handoffs)
47
+ - [System Messages](#system-messages)
47
48
  - [Context Serialization](#context-serialization)
48
49
  - [Browser Usage](#browser-usage)
49
50
  - [Bundling and Tree Shaking](#bundling-and-tree-shaking)
@@ -61,6 +62,7 @@ Unified LLM API with provider collections, automatic auth resolution, token and
61
62
  - **Ant Ling**
62
63
  - **Azure OpenAI (Responses)**
63
64
  - **OpenAI Codex** (ChatGPT Plus/Pro subscription, requires OAuth, see below)
65
+ - **Radius** (API key or OAuth, with a dynamically refreshed gateway catalog)
64
66
  - **DeepSeek**
65
67
  - **NVIDIA NIM**
66
68
  - **Anthropic**
@@ -86,6 +88,7 @@ Unified LLM API with provider collections, automatic auth resolution, token and
86
88
  - **OpenCode Go**
87
89
  - **Fireworks** (uses OpenAI- and Anthropic-compatible APIs)
88
90
  - **Kimi For Coding** (Moonshot AI subscription endpoint, uses Anthropic-compatible API)
91
+ - **Meta** (Model API, uses OpenAI Responses-compatible API)
89
92
  - **Qwen Token Plan** (separate Individual and existing catalogs, with a separate China provider)
90
93
  - **Xiaomi MiMo** (defaults to API billing endpoint, with separate Token Plan providers for `cn`/`ams`/`sgp` regions)
91
94
  - **Any OpenAI-compatible API**: Ollama, vLLM, LM Studio, etc.
@@ -305,6 +308,7 @@ For tooling that wants the generated built-in catalog with full literal typing (
305
308
  import { getBuiltinModel, getBuiltinModels, getBuiltinProviders } from '@earendil-works/pi-ai/providers/all';
306
309
 
307
310
  const model = getBuiltinModel('openai', 'gpt-4o-mini'); // typed Model<'openai-responses'>
311
+ const radius = getBuiltinModel('radius', 'balanced'); // typed Model<'pi-messages'>
308
312
  const providers = getBuiltinProviders();
309
313
  const anthropic = getBuiltinModels('anthropic');
310
314
  ```
@@ -320,7 +324,7 @@ await models.refresh(); // refresh all providers conc
320
324
  const fresh = models.getModel('llamacpp', 'qwen3-30b');
321
325
  ```
322
326
 
323
- Static built-in providers are no-ops for `refresh()`. See [createProvider()](#createprovider) for building a dynamic provider.
327
+ Static built-in providers are no-ops for `refresh()`. Radius is both static and dynamic: it ships the public `radius.pi.dev` catalog for synchronous API lookup, then overlays cached and freshly fetched `/v1/config` models when refreshed with configured auth. See [createProvider()](#createprovider) for building a dynamic provider.
324
328
 
325
329
  ## Auth
326
330
 
@@ -417,6 +421,7 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex
417
421
  | Ant Ling | `ANT_LING_API_KEY` |
418
422
  | Azure OpenAI | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_BASE_URL` (e.g. `https://{resource}.ai.azure.com`) or `AZURE_OPENAI_RESOURCE_NAME`. Supports `*.openai.azure.com`, `*.cognitiveservices.azure.com` and `*.ai.azure.com`; root endpoints auto-normalize to `/openai/v1`. Optional: `AZURE_OPENAI_API_VERSION` (default `v1`), `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`. |
419
423
  | Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` |
424
+ | Radius | `RADIUS_API_KEY` |
420
425
  | DeepSeek | `DEEPSEEK_API_KEY` |
421
426
  | NVIDIA NIM | `NVIDIA_API_KEY` |
422
427
  | Google | `GEMINI_API_KEY` |
@@ -440,6 +445,7 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex
440
445
  | Hugging Face | `HF_TOKEN` |
441
446
  | OpenCode Zen / OpenCode Go | `OPENCODE_API_KEY` |
442
447
  | Kimi For Coding | `KIMI_API_KEY` |
448
+ | Meta | `META_API_KEY` |
443
449
  | Qwen Token Plan (existing catalog) | `QWEN_TOKEN_PLAN_API_KEY` |
444
450
  | Qwen Token Plan (Individual) | `QWEN_TOKEN_PLAN_API_KEY` |
445
451
  | Qwen Token Plan (China) | `QWEN_TOKEN_PLAN_CN_API_KEY` |
@@ -1168,12 +1174,13 @@ const ollamaReasoningModel: Model<'openai-completions'> = {
1168
1174
 
1169
1175
  ### Calling API Implementations Directly
1170
1176
 
1171
- The API implementations are importable on their own. Each module exports exactly `stream` and `streamSimple` with that API's full option typing. Direct calls bypass provider auth — pass `apiKey` explicitly:
1177
+ The API implementations are importable on their own. Each module exports exactly `stream` and `streamSimple` with that API's full option typing. Direct calls bypass provider auth and context normalization — pass `apiKey` explicitly and wrap the context in `normalizeContext()`:
1172
1178
 
1173
1179
  ```typescript
1180
+ import { normalizeContext } from '@earendil-works/pi-ai';
1174
1181
  import { stream } from '@earendil-works/pi-ai/api/anthropic-messages';
1175
1182
 
1176
- const s = stream(claudeModel, context, {
1183
+ const s = stream(claudeModel, normalizeContext(context), {
1177
1184
  apiKey: process.env.ANTHROPIC_API_KEY,
1178
1185
  thinkingEnabled: true,
1179
1186
  thinkingBudgetTokens: 2048,
@@ -1208,7 +1215,9 @@ interface OpenAICompletionsCompat {
1208
1215
  supportsUsageInStreaming?: boolean; // Whether provider supports `stream_options: { include_usage: true }` (default: true)
1209
1216
  supportsStrictMode?: boolean; // Whether provider supports `strict` in tool definitions (default: true)
1210
1217
  supportsOpenAIGrammarTools?: boolean; // Whether to emit OpenAI custom Lark/regex grammar tools; false falls back to normal function tools (default: false; the generated catalog enables it for capable models)
1211
- sendSessionAffinityHeaders?: boolean; // Send session-affinity data from `sessionId` (default: false)
1218
+ supportsMidConvoSystemMessages?: boolean; // Whether the model accepts system messages after the conversation started; false folds them into the leading prompt (default: false; the generated catalog enables it for verified models)
1219
+ supportsMidConvoToolAdditions?: boolean; // Whether system messages can add tools mid-conversation via Kimi-style `tools` system messages; requires supportsMidConvoSystemMessages (default: false)
1220
+ sendSessionAffinityHeaders?: boolean; // Send session-affinity data from `sessionId` (default: true for OpenRouter, false otherwise)
1212
1221
  sessionAffinityFormat?: 'openai' | 'openai-nosession' | 'openrouter'; // Format for session affinity: 'openai' uses `prompt_cache_key`, `session_id`, `x-client-request-id`, and `x-session-affinity`; 'openai-nosession' uses `prompt_cache_key`, `x-client-request-id`, and `x-session-affinity`; 'openrouter' uses `x-session-id` (default: auto-detected)
1213
1222
  maxTokensField?: 'max_completion_tokens' | 'max_tokens'; // Which field name to use (default: max_completion_tokens)
1214
1223
  requiresToolResultName?: boolean; // Whether tool results require the `name` field (default: false)
@@ -1234,6 +1243,8 @@ interface OpenAIResponsesCompat {
1234
1243
  }
1235
1244
  ```
1236
1245
 
1246
+ OpenRouter requests send `x-session-id` from `sessionId` when prompt caching is enabled. Chat Completions and Anthropic Messages both auto-detect OpenRouter endpoints unless `sendSessionAffinityHeaders` is explicitly false. On Anthropic-compatible models, `sessionAffinityFormat: "openrouter"` selects `x-session-id`; when unset, the existing `x-session-affinity` format is used. Explicit request headers take precedence over generated headers.
1247
+
1237
1248
  If `compat` is not set, the library falls back to URL-based detection. If `compat` is partially set, unspecified fields use the detected defaults. This is useful for:
1238
1249
 
1239
1250
  - **LiteLLM proxies**: May not support `store` field
@@ -1369,6 +1380,39 @@ const geminiResponse = await models.complete(gemini, context);
1369
1380
 
1370
1381
  All providers can handle messages from other providers — text, tool calls and results (including images), thinking blocks (transformed to tagged text), and aborted messages with partial content. This enables flexible workflows: start with a fast model, switch to a more capable one for complex reasoning, or maintain continuity across provider outages.
1371
1382
 
1383
+ ## System Messages
1384
+
1385
+ `Context.systemPrompt` and `Context.tools` are shorthand for a leading system message. The public entry points (`Models.stream()`, `streamSimple()`, `complete()`, `completeSimple()`) accept a `Context` and call `normalizeContext()` once; everything below them, including `Provider.stream()`, `ProviderStreams`, and the API implementation modules, receives the resulting `TranscriptContext`, which only has `messages`. The transcript can also carry system messages later in the conversation to change the prompt or the tool set without rewriting the history:
1386
+
1387
+ ```typescript
1388
+ interface SystemMessage {
1389
+ role: "system";
1390
+ content: string | TextContent[]; // leading: base prompt; later: added instructions
1391
+ sections?: Record<string, string | null>; // named prompt sections; later messages patch by name, null removes
1392
+ toolsAdded?: Tool[]; // tools that become available here
1393
+ toolsRemoved?: ToolReference[]; // tools that stop being available here
1394
+ timestamp: number;
1395
+ }
1396
+ ```
1397
+
1398
+ Sections are opaque text rendered verbatim after `content`, joined by blank lines. Keep each one self-delimiting (a tag, a heading) so the model can relate an update to the original. Replaying every system message in order yields the current prompt and tools; the replay helpers take the message list:
1399
+
1400
+ ```typescript
1401
+ import { getCurrentSystemPrompt, getCurrentTools } from "@earendil-works/pi-ai";
1402
+
1403
+ const messages: Message[] = [
1404
+ { role: "system", content: "You are helpful.", sections: { rules: "<rules>Be brief.</rules>" }, toolsAdded: [readTool], timestamp: 1 },
1405
+ { role: "user", content: "hi", timestamp: 2 },
1406
+ { role: "system", content: "", sections: { rules: "<rules>Be thorough.</rules>" }, toolsRemoved: [{ name: "read" }], timestamp: 3 },
1407
+ ];
1408
+ getCurrentSystemPrompt(messages); // "You are helpful.\n\n<rules>Be thorough.</rules>"
1409
+ getCurrentTools(messages); // []
1410
+ ```
1411
+
1412
+ A custom `Provider` or `ProviderStreams` implementation reads the prompt and tools the same way from `context.messages`; `context.systemPrompt` and `context.tools` do not exist at that layer.
1413
+
1414
+ Models that accept system messages mid-conversation (`supportsMidConvoSystemMessages` in the model's compat settings, set by the generated catalog for verified models) receive each later system message in place, so the cached prefix stays intact; section changes are framed by name for the model. Every other model receives `collapseSystemMessages(transcript)`: the replayed prompt and current tools as the leading system message, with later system messages dropped. Anthropic models that also set `supportsMidConvoToolChanges` send tool changes as native `tool_addition`/`tool_removal` blocks: the initial tools stay active at the top level, every later declaration is sent with `defer_loading` (plus a stable deferred placeholder from the first request, which keeps Anthropic's deferred-tool scaffolding in the cached prefix), and removed tools stay declared, so tool changes do not invalidate the prompt cache. That needs at least one initial tool and no same-name redefinition; otherwise the current tool list is sent at the top level with the system text only. OpenAI Responses models with `supportsAdditionalTools` or `supportsToolSearch` anchor additive tool changes at their message; everything else sends the current tool list at the top level.
1415
+
1372
1416
  ## Context Serialization
1373
1417
 
1374
1418
  The `Context` object can be easily serialized and deserialized using standard JSON methods, making it simple to persist conversations, implement chat history, or transfer contexts between services:
@@ -1589,7 +1633,7 @@ Built-in login and refresh flows are private provider implementations. Use provi
1589
1633
 
1590
1634
  Provider notes:
1591
1635
 
1592
- **OpenAI Codex**: Requires a ChatGPT Plus or Pro subscription. Provides access to GPT-5.x Codex models with extended context windows and reasoning capabilities. The library automatically handles session-based prompt caching when `sessionId` is provided in stream options unless `cacheRetention` is `"none"`. You can set `transport` in stream options to `"sse"`, `"websocket"`, or `"auto"` for Codex Responses transport selection. When using WebSocket with a `sessionId` and cache retention enabled, connections are reused per session and expire after 5 minutes of inactivity.
1636
+ **OpenAI Codex**: Requires a ChatGPT Plus or Pro subscription. Provides access to GPT-5.x Codex models with extended context windows and reasoning capabilities. The library automatically handles session-based prompt caching when `sessionId` is provided in stream options unless `cacheRetention` is `"none"`. You can set `transport` in stream options to `"sse"`, `"websocket"`, or `"auto"` for Codex Responses transport selection. When using WebSocket with a `sessionId` and cache retention enabled, connections are reused per session and expire after 5 minutes of inactivity. Call `cleanupSessionResources(sessionId)` when finished so the pooled connection does not keep the process alive.
1593
1637
 
1594
1638
  **Azure OpenAI (Responses)**: Uses the Responses API only. Set `AZURE_OPENAI_API_KEY` and either `AZURE_OPENAI_BASE_URL` or `AZURE_OPENAI_RESOURCE_NAME`. `AZURE_OPENAI_BASE_URL` supports both `https://<resource>.openai.azure.com` and `https://<resource>.cognitiveservices.azure.com`; root endpoints are normalized to `.../openai/v1` automatically. Use `AZURE_OPENAI_API_VERSION` (defaults to `v1`) to override the API version if needed. Deployment names are treated as model IDs by default, override with `azureDeploymentName` or `AZURE_OPENAI_DEPLOYMENT_NAME_MAP` using comma-separated `model-id=deployment` pairs (for example `gpt-4o-mini=my-deployment,gpt-4o=prod`). Legacy deployment-based URLs are intentionally unsupported.
1595
1639
 
@@ -1636,7 +1680,7 @@ Adding a new LLM provider requires changes across multiple files. The layered la
1636
1680
  Create a new API implementation file (for example `bedrock-converse-stream.ts`) that exports exactly `stream` and `streamSimple`, plus:
1637
1681
 
1638
1682
  - An options interface extending `StreamOptions` (for example `BedrockOptions`)
1639
- - Message conversion functions to transform `Context` to provider format
1683
+ - Message conversion functions to transform the `TranscriptContext` messages to provider format; read the prompt and tools from the transcript with `getInitialSystemMessage()`, `getCurrentTools()`, and `resolveTranscript()`
1640
1684
  - Tool conversion if the provider supports tools
1641
1685
  - Response parsing to emit standardized events (`text`, `tool_call`, `thinking`, `usage`, `stop`)
1642
1686
 
@@ -1 +1 @@
1
- {"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAa1C,OAAO,KAAK,EAWX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAMb,MAAM,aAAa,CAAC;AAyIrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AA2ChE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,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;AAqOD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CA6TzE,CAAC;AA2BF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CA8ClF,CAAC","sourcesContent":["import Anthropic from \"@anthropic-ai/sdk\";\nimport type {\n\tBetaStopReason,\n\tBetaThinkingDroppedInputTransformation,\n\tBetaTool,\n\tBetaCacheControlEphemeral as CacheControlEphemeral,\n\tBetaContentBlockParam as ContentBlockParam,\n\tMessageCreateParamsStreaming,\n\tBetaMessageParam as MessageParam,\n\tBetaRawMessageStreamEvent as RawMessageStreamEvent,\n\tBetaRefusalStopDetails as RefusalStopDetails,\n} from \"@anthropic-ai/sdk/resources/beta/messages/messages.js\";\nimport { calculateCost } from \"../models.ts\";\nimport type {\n\tAnthropicMessagesCompat,\n\tApi,\n\tAssistantMessage,\n\tCacheRetention,\n\tContext,\n\tImageContent,\n\tMessage,\n\tModel,\n\tProviderEnv,\n\tProviderHeaders,\n\tSimpleStreamOptions,\n\tStopReason,\n\tStreamFunction,\n\tStreamOptions,\n\tTextContent,\n\tThinkingContent,\n\tTool,\n\tToolCall,\n\tToolResultMessage,\n} from \"../types.ts\";\nimport { splitDeferredTools } from \"../utils/deferred-tools.ts\";\nimport { appendAssistantMessageDiagnostic } from \"../utils/diagnostics.ts\";\nimport { AssistantMessageEventStream } from \"../utils/event-stream.ts\";\nimport { headersToRecord } from \"../utils/headers.ts\";\nimport { parseJsonWithRepair, parseStreamingJson } from \"../utils/json-parse.ts\";\nimport { getPiUserAgent } from \"../utils/pi-user-agent.ts\";\nimport { getProviderEnvValue } from \"../utils/provider-env.ts\";\nimport { retryProviderRequest } from \"../utils/provider-retry.ts\";\nimport { sanitizeSurrogates } from \"../utils/sanitize-unicode.ts\";\n\nimport { getJsonSchemaToolParameters, resolveJsonSchemaStrictSampling } from \"./constrained-sampling.ts\";\nimport { buildCopilotDynamicHeaders, hasCopilotVisionInput } from \"./github-copilot-headers.ts\";\nimport { adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext } from \"./simple-options.ts\";\nimport { transformMessages } from \"./transform-messages.ts\";\n\n/**\n * Resolve cache retention preference.\n * Defaults to \"short\" and uses PI_CACHE_RETENTION for backward compatibility.\n */\nfunction resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEnv): CacheRetention {\n\tif (cacheRetention) {\n\t\treturn cacheRetention;\n\t}\n\tif (getProviderEnvValue(\"PI_CACHE_RETENTION\", env) === \"long\") {\n\t\treturn \"long\";\n\t}\n\treturn \"short\";\n}\n\nfunction getCacheControl(\n\tmodel: Model<\"anthropic-messages\">,\n\tcacheRetention?: CacheRetention,\n\tenv?: ProviderEnv,\n): { retention: CacheRetention; cacheControl?: CacheControlEphemeral } {\n\tconst retention = resolveCacheRetention(cacheRetention, env);\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.251\";\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\";\nconst SERVER_SIDE_FALLBACK_BETA = \"server-side-fallback-2026-07-01\";\nconst MID_CONVERSATION_OUTPUT_CONFIG_BETA = \"mid-conversation-output-config-2026-07-01\";\nconst THINKING_BINDING_CONTROLS_BETA = \"thinking-binding-controls-2026-08-01\";\n\nfunction shouldUseServerSideFallbackBeta(model: Model<\"anthropic-messages\">): boolean {\n\treturn (model.compat?.allowedFallbackModels?.length ?? 0) > 0;\n}\n\nfunction getAnthropicCompat(\n\tmodel: Model<\"anthropic-messages\">,\n): Required<\n\tOmit<AnthropicMessagesCompat, \"forceAdaptiveThinking\" | \"allowedFallbackModels\" | \"supportsMidConvoEffort\">\n> {\n\treturn {\n\t\tsupportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true,\n\t\tsupportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,\n\t\tsendSessionAffinityHeaders: model.compat?.sendSessionAffinityHeaders ?? false,\n\t\tsupportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? true,\n\t\tsupportsTemperature: model.compat?.supportsTemperature ?? true,\n\t\tallowEmptySignature: model.compat?.allowEmptySignature ?? false,\n\t\tsupportsStrictTools: model.compat?.supportsStrictTools ?? false,\n\t\tsupportsToolReferences: model.compat?.supportsToolReferences ?? defaultSupportsToolReferences(model),\n\t};\n}\n\n/**\n * Default for `supportsToolReferences`: first-party Anthropic models except\n * Haiku (rejects client-side tool_reference blocks) and models that predate\n * tool search (Claude 3.x, Opus/Sonnet 4.0, Opus 4.1).\n */\nfunction defaultSupportsToolReferences(model: Model<\"anthropic-messages\">): boolean {\n\tif (model.provider !== \"anthropic\" || model.id.includes(\"haiku\")) return false;\n\tconst version = model.id.match(/^claude-(?:opus|sonnet|fable)-(\\d+)(?:-(\\d+))?(?:-|$)/);\n\tif (!version) return false;\n\tconst major = Number(version[1]);\n\tconst minor = version[2] && version[2].length < 8 ? Number(version[2]) : 0;\n\treturn major > 4 || (major === 4 && minor >= 5);\n}\n\nexport interface AnthropicOptions extends StreamOptions {\n\t/**\n\t * Enable extended thinking.\n\t * For adaptive thinking models: the model decides when/how much to think.\n\t * For older models: uses budget-based thinking with thinkingBudgetTokens.\n\t * Default: undefined (thinking is omitted unless `streamSimple()` maps\n\t * a simple reasoning level to this option, or callers set it explicitly).\n\t */\n\tthinkingEnabled?: boolean;\n\t/**\n\t * Token budget for extended thinking (older models only).\n\t * Ignored for adaptive thinking models.\n\t * Default: 1024 when `thinkingEnabled` is true and no budget is provided.\n\t */\n\tthinkingBudgetTokens?: number;\n\t/**\n\t * Effort level for adaptive thinking models.\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\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 * Default: omitted unless `streamSimple()` maps a simple reasoning\n\t * level to this option.\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.\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 * Default: \"summarized\" when thinking is enabled.\n\t */\n\tthinkingDisplay?: AnthropicThinkingDisplay;\n\t/**\n\t * Whether to request the interleaved thinking beta header for non-adaptive\n\t * thinking models. Adaptive thinking models have interleaved thinking built in,\n\t * so the header is skipped for them regardless of this setting.\n\t * Default: true.\n\t */\n\tinterleavedThinking?: boolean;\n\t/**\n\t * Anthropic tool choice behavior. String values map to Anthropic's built-in\n\t * choices; `{ type: \"tool\", name }` forces a specific tool.\n\t * Default: omitted (Anthropic default behavior, currently equivalent to auto).\n\t */\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: (ProviderHeaders | undefined)[]): ProviderHeaders {\n\tconst merged: ProviderHeaders = {};\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\nfunction mergeClientHeaders(...headerSources: (ProviderHeaders | undefined)[]): ProviderHeaders {\n\treturn mergeHeaders({ \"User-Agent\": getPiUserAgent() }, ...headerSources);\n}\n\nfunction hasHeader(headers: ProviderHeaders | undefined, name: string): boolean {\n\tif (!headers) return false;\n\tconst expected = name.toLowerCase();\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tif (key.toLowerCase() === expected && value !== null && value.trim().length > 0) return true;\n\t}\n\treturn false;\n}\n\nfunction assertRequestAuth(provider: string, apiKey: string | undefined, headers: ProviderHeaders | undefined): void {\n\tif (apiKey) return;\n\tif (\n\t\thasHeader(headers, \"authorization\") ||\n\t\thasHeader(headers, \"x-api-key\") ||\n\t\thasHeader(headers, \"cf-aig-authorization\")\n\t) {\n\t\treturn;\n\t}\n\tthrow new Error(`No API key for provider: ${provider}`);\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\nasync function* iterateAnthropicEvents(\n\tresponse: Response,\n\tsignal?: AbortSignal,\n): AsyncGenerator<RawMessageStreamEvent> {\n\tif (!response.body) {\n\t\tthrow new Error(\"Attempted to iterate over an Anthropic response with no body\");\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\tthrow new Error(sse.data);\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 Error(\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);\n\t\t}\n\t}\n\n\tif (sawMessageStart && !sawMessageEnd) {\n\t\tthrow new Error(\"Anthropic stream ended before message_stop\");\n\t}\n}\n\nexport const stream: 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 providerThinkingLevel = model.compat?.supportsMidConvoEffort ? (options?.effort ?? \"high\") : undefined;\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\t...(providerThinkingLevel === undefined ? {} : { providerThinkingLevel }),\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: \"pending\",\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\n\t\ttry {\n\t\t\tlet client: Anthropic;\n\t\t\tlet isOAuth: boolean;\n\t\t\tlet usageModel = model;\n\t\t\tlet inputTransformations: BetaThinkingDroppedInputTransformation[] | undefined;\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;\n\t\t\t\tassertRequestAuth(model.provider, apiKey, options?.headers);\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 = resolveCacheRetention(options?.cacheRetention, options?.env);\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?.headers,\n\t\t\t\t\toptions?.fetch,\n\t\t\t\t\tcopilotDynamicHeaders,\n\t\t\t\t\tcacheSessionId,\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 options?.onPayload?.(params, model);\n\t\t\tif (nextParams !== undefined) {\n\t\t\t\tparams = { ...(nextParams as MessageCreateParamsStreaming), stream: true };\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\tmaxRetries: 0,\n\t\t\t};\n\t\t\tconst response = await retryProviderRequest(\n\t\t\t\t() => client.beta.messages.create(params, requestOptions).asResponse(),\n\t\t\t\t{\n\t\t\t\t\tmaxRetries: options?.maxRetries,\n\t\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs,\n\t\t\t\t\tsignal: options?.signal,\n\t\t\t\t},\n\t\t\t);\n\t\t\tawait options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);\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\tconst transformations = event.message.input_transformations;\n\t\t\t\t\tif (Array.isArray(transformations)) inputTransformations = transformations;\n\t\t\t\t\toutput.model = event.message.model;\n\t\t\t\t\tconst fallbackCost =\n\t\t\t\t\t\toutput.model === model.id\n\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t: model.compat?.allowedFallbackModels?.find(\n\t\t\t\t\t\t\t\t\t(fallback) => fallback.provider === model.provider && fallback.model === output.model,\n\t\t\t\t\t\t\t\t)?.cost;\n\t\t\t\t\tusageModel = fallbackCost ? { ...model, id: output.model, cost: fallbackCost } : model;\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\toutput.usage.cacheWrite1h = event.message.usage.cache_creation?.ephemeral_1h_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(usageModel, output.usage);\n\t\t\t\t} else if (event.type === \"content_block_start\") {\n\t\t\t\t\tif (event.content_block.type === \"fallback\") {\n\t\t\t\t\t\tif (output.content.length > 0) {\n\t\t\t\t\t\t\tthrow new Error(\"Anthropic performed an unsupported mid-output model fallback\");\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 (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: event.content_block.text ?? \"\",\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: event.content_block.thinking ?? \"\",\n\t\t\t\t\t\t\tthinkingSignature: event.content_block.signature ?? \"\",\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\tconst transformations = event.input_transformations;\n\t\t\t\t\tif (Array.isArray(transformations)) inputTransformations = transformations;\n\t\t\t\t\tif (event.delta.stop_reason) {\n\t\t\t\t\t\toutput.rawStopReason = event.delta.stop_reason;\n\t\t\t\t\t\tconst stopReasonResult = mapStopReason(event.delta.stop_reason, event.delta.stop_details);\n\t\t\t\t\t\toutput.stopReason = stopReasonResult.stopReason;\n\t\t\t\t\t\tif (stopReasonResult.errorMessage) {\n\t\t\t\t\t\t\toutput.errorMessage = stopReasonResult.errorMessage;\n\t\t\t\t\t\t}\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) {\n\t\t\t\t\t\tif (event.usage.input_tokens != null) {\n\t\t\t\t\t\t\toutput.usage.input = event.usage.input_tokens;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (event.usage.output_tokens != null) {\n\t\t\t\t\t\t\toutput.usage.output = event.usage.output_tokens;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (event.usage.cache_read_input_tokens != null) {\n\t\t\t\t\t\t\toutput.usage.cacheRead = event.usage.cache_read_input_tokens;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (event.usage.cache_creation_input_tokens != null) {\n\t\t\t\t\t\t\toutput.usage.cacheWrite = event.usage.cache_creation_input_tokens;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Anthropic reports reasoning tokens as a subset of output tokens.\n\t\t\t\t\t\tconst thinkingTokens = event.usage.output_tokens_details?.thinking_tokens;\n\t\t\t\t\t\tif (thinkingTokens != null) {\n\t\t\t\t\t\t\toutput.usage.reasoning = thinkingTokens;\n\t\t\t\t\t\t}\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(usageModel, 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 === \"pending\") {\n\t\t\t\tthrow new Error(\"Anthropic stream ended without a stop reason\");\n\t\t\t}\n\t\t\tif (output.stopReason === \"aborted\" || output.stopReason === \"error\") {\n\t\t\t\tthrow new Error(output.errorMessage || \"An unknown error occurred\");\n\t\t\t}\n\t\t\tif (inputTransformations && inputTransformations.length > 0) {\n\t\t\t\tappendAssistantMessageDiagnostic(output, {\n\t\t\t\t\ttype: \"anthropic_input_transformations\",\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\ttransformations: inputTransformations.map((transformation) => ({\n\t\t\t\t\t\t\ttype: transformation.type ?? undefined,\n\t\t\t\t\t\t\tpath: transformation.path ?? undefined,\n\t\t\t\t\t\t\treason: transformation.reason ?? undefined,\n\t\t\t\t\t\t})),\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\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\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/**\n * Map ThinkingLevel to Anthropic effort levels for adaptive thinking.\n * Note: effort \"max\" is available on all adaptive-thinking Claude models, while native\n * \"xhigh\" is only available on Opus 4.7/4.8, Sonnet 5, and Fable 5.\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\tdefault:\n\t\t\treturn \"high\";\n\t}\n}\n\nexport const streamSimple: StreamFunction<\"anthropic-messages\", SimpleStreamOptions> = (\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream => {\n\tassertRequestAuth(model.provider, options?.apiKey, options?.headers);\n\n\tconst base = {\n\t\t...buildBaseOptions(model, context, options, options?.apiKey),\n\t\ttoolChoice: options?.toolChoice,\n\t} satisfies AnthropicOptions;\n\tif (!options?.reasoning) {\n\t\treturn stream(model, context, {\n\t\t\t...base,\n\t\t\tthinkingEnabled: false,\n\t\t} satisfies AnthropicOptions);\n\t}\n\n\t// For models with adaptive thinking: use an effort level.\n\t// For older models: use budget-based thinking.\n\tif (model.compat?.forceAdaptiveThinking === true) {\n\t\tconst effort = mapThinkingLevelToEffort(model, options.reasoning);\n\t\treturn stream(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\t// Undefined means the caller did not request an output cap; let the helper use the model cap.\n\t// Do not coerce to 0 here, or the thinking budget would become the entire max_tokens value.\n\tconst adjusted = adjustMaxTokensForThinking(\n\t\tbase.maxTokens,\n\t\tmodel.maxTokens,\n\t\toptions.reasoning,\n\t\toptions.thinkingBudgets,\n\t);\n\n\tconst maxTokens = clampMaxTokensToContext(model, context, adjusted.maxTokens);\n\n\treturn stream(model, context, {\n\t\t...base,\n\t\tmaxTokens,\n\t\tthinkingEnabled: true,\n\t\tthinkingBudgetTokens: Math.min(adjusted.thinkingBudget, Math.max(0, maxTokens - 1024)),\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 | undefined,\n\toptionsHeaders?: ProviderHeaders,\n\tfetch?: typeof globalThis.fetch,\n\tdynamicHeaders?: Record<string, string>,\n\tsessionId?: string,\n): { client: Anthropic; isOAuthToken: boolean } {\n\t// Copilot: Bearer auth.\n\tif (model.provider === \"github-copilot\") {\n\t\tconst client = new Anthropic({\n\t\t\tapiKey: null,\n\t\t\tauthToken: apiKey ?? null,\n\t\t\tbaseURL: model.baseUrl,\n\t\t\tdangerouslyAllowBrowser: true,\n\t\t\tfetch,\n\t\t\tdefaultHeaders: mergeClientHeaders(\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},\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 (apiKey && isOAuthToken(apiKey)) {\n\t\tconst client = new Anthropic({\n\t\t\tapiKey: null,\n\t\t\tauthToken: apiKey,\n\t\t\tbaseURL: model.baseUrl,\n\t\t\tdangerouslyAllowBrowser: true,\n\t\t\tfetch,\n\t\t\tdefaultHeaders: mergeClientHeaders(\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\"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 or header-owned auth.\n\tconst sessionAffinityHeaders: ProviderHeaders =\n\t\tsessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { \"x-session-affinity\": sessionId } : {};\n\tconst defaultHeaders = mergeClientHeaders(\n\t\t{\n\t\t\taccept: \"application/json\",\n\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t},\n\t\tsessionAffinityHeaders,\n\t\tmodel.headers,\n\t\toptionsHeaders,\n\t);\n\tconst client = new Anthropic({\n\t\tapiKey: apiKey ?? null,\n\t\tauthToken: null,\n\t\tbaseURL: model.baseUrl,\n\t\tdangerouslyAllowBrowser: true,\n\t\tfetch,\n\t\tdefaultHeaders,\n\t});\n\n\treturn { client, isOAuthToken: false };\n}\n\nfunction getBetaFeatures(\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: Context,\n\tisOAuthToken: boolean,\n\toptions?: AnthropicOptions,\n): NonNullable<MessageCreateParamsStreaming[\"betas\"]> {\n\tlet configuredFeatures: string | null | undefined;\n\tfor (const headers of [model.headers, options?.headers]) {\n\t\tfor (const [name, value] of Object.entries(headers ?? {})) {\n\t\t\tif (name.toLowerCase() === \"anthropic-beta\") configuredFeatures = value;\n\t\t}\n\t}\n\tif (configuredFeatures === null) return [];\n\tif (configuredFeatures !== undefined) {\n\t\treturn [\n\t\t\t...new Set(\n\t\t\t\tconfiguredFeatures\n\t\t\t\t\t.split(\",\")\n\t\t\t\t\t.map((feature) => feature.trim())\n\t\t\t\t\t.filter((feature) => feature.length > 0),\n\t\t\t),\n\t\t];\n\t}\n\n\tconst features: NonNullable<MessageCreateParamsStreaming[\"betas\"]> = [];\n\tif (isOAuthToken) features.push(\"claude-code-20250219\", \"oauth-2025-04-20\");\n\tif (shouldUseFineGrainedToolStreamingBeta(model, context)) features.push(FINE_GRAINED_TOOL_STREAMING_BETA);\n\tif (\n\t\tmodel.reasoning &&\n\t\toptions?.thinkingEnabled === true &&\n\t\t(options.interleavedThinking ?? true) &&\n\t\tmodel.compat?.forceAdaptiveThinking !== true\n\t) {\n\t\tfeatures.push(INTERLEAVED_THINKING_BETA);\n\t}\n\tif (shouldUseServerSideFallbackBeta(model)) features.push(SERVER_SIDE_FALLBACK_BETA);\n\tif (model.compat?.supportsMidConvoEffort === true) {\n\t\tfeatures.push(MID_CONVERSATION_OUTPUT_CONFIG_BETA, THINKING_BINDING_CONTROLS_BETA);\n\t}\n\treturn [...new Set(features)];\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, options?.env);\n\tconst compat = getAnthropicCompat(model);\n\tconst transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);\n\tconst normalizeToolName = isOAuthToken ? toClaudeCodeName : (name: string) => name;\n\tconst toolPlacement = splitDeferredTools(\n\t\t{ ...context, messages: transformedMessages },\n\t\tcompat.supportsToolReferences,\n\t\tnormalizeToolName,\n\t);\n\tlet immediateTools = toolPlacement.immediate;\n\tlet deferredTools = [...toolPlacement.deferred.values()];\n\tif (immediateTools.length === 0 && deferredTools.length > 0) {\n\t\timmediateTools = deferredTools;\n\t\tdeferredTools = [];\n\t}\n\tconst deferredToolNames = new Set(deferredTools.map((tool) => normalizeToolName(tool.name)));\n\tconst converted = convertMessages(\n\t\ttransformedMessages,\n\t\tisOAuthToken,\n\t\tcacheControl,\n\t\tcompat.allowEmptySignature,\n\t\tdeferredToolNames,\n\t\tnormalizeToolName,\n\t\tmodel.compat?.supportsMidConvoEffort === true ? model.provider : undefined,\n\t);\n\tconst activeEffort = options?.effort ?? \"high\";\n\tconst betaFeatures = getBetaFeatures(model, context, isOAuthToken, options);\n\tconst params: MessageCreateParamsStreaming = {\n\t\tmodel: model.id,\n\t\tmessages:\n\t\t\tmodel.compat?.supportsMidConvoEffort === true\n\t\t\t\t? insertThinkingLevelMessages(converted, activeEffort)\n\t\t\t\t: converted.messages,\n\t\tmax_tokens: options?.maxTokens ?? model.maxTokens,\n\t\tstream: true,\n\t\t...(betaFeatures.length > 0 ? { betas: betaFeatures } : {}),\n\t};\n\n\t// For OAuth tokens, we MUST include Claude Code identity\n\tif (isOAuthToken) {\n\t\tparams.system = [\n\t\t\t{\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: \"You are Claude Code, Anthropic's official CLI for Claude.\",\n\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t},\n\t\t];\n\t\tif (context.systemPrompt) {\n\t\t\tparams.system.push({\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: sanitizeSurrogates(context.systemPrompt),\n\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t});\n\t\t}\n\t} else if (context.systemPrompt) {\n\t\t// Add cache control to system prompt for non-OAuth tokens\n\t\tparams.system = [\n\t\t\t{\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: sanitizeSurrogates(context.systemPrompt),\n\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t},\n\t\t];\n\t}\n\n\t// Temperature is incompatible with extended thinking and unsupported on Claude Opus 4.7+.\n\tif (\n\t\toptions?.temperature !== undefined &&\n\t\t!options?.thinkingEnabled &&\n\t\tmodel.compat?.supportsMidConvoEffort !== true &&\n\t\tcompat.supportsTemperature\n\t) {\n\t\tparams.temperature = options.temperature;\n\t}\n\n\tif (immediateTools.length > 0 || deferredTools.length > 0) {\n\t\tparams.tools = [\n\t\t\t...convertTools(\n\t\t\t\timmediateTools,\n\t\t\t\tisOAuthToken,\n\t\t\t\tcompat.supportsEagerToolInputStreaming,\n\t\t\t\tcompat.supportsStrictTools,\n\t\t\t\tcompat.supportsCacheControlOnTools ? cacheControl : undefined,\n\t\t\t),\n\t\t\t...convertTools(\n\t\t\t\tdeferredTools,\n\t\t\t\tisOAuthToken,\n\t\t\t\tcompat.supportsEagerToolInputStreaming,\n\t\t\t\tcompat.supportsStrictTools,\n\t\t\t\tundefined,\n\t\t\t\ttrue,\n\t\t\t),\n\t\t];\n\t}\n\n\t// Managed effort models always use adaptive thinking so prefix mismatches can\n\t// be dropped instead of surfacing as persistent 400 responses.\n\tif (model.compat?.supportsMidConvoEffort === true) {\n\t\tparams.thinking = {\n\t\t\ttype: \"adaptive\",\n\t\t\tdisplay: options?.thinkingDisplay ?? \"summarized\",\n\t\t\tblock_binding: { prefix_mismatch_behavior: \"drop_block\" },\n\t\t};\n\t\tparams.output_config = { effort: \"high\" };\n\t} else if (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 (model.compat?.forceAdaptiveThinking === true) {\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\tparams.output_config = { 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 && model.thinkingLevelMap?.off !== null) {\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\tconst allowedFallbackModels = model.compat?.allowedFallbackModels;\n\tif (allowedFallbackModels && allowedFallbackModels.length > 0) {\n\t\tparams.fallbacks = allowedFallbackModels.map((fallback) => ({ model: fallback.model }));\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 convertToolResult(\n\tmsg: ToolResultMessage,\n\tisOAuthToken: boolean,\n\tdeferredToolNames: ReadonlySet<string>,\n\tloadedToolNames: Set<string>,\n\tnormalizeToolName: (name: string) => string,\n): { toolResult: ContentBlockParam; siblingContent: ContentBlockParam[] } {\n\tconst references: Array<{ type: \"tool_reference\"; tool_name: string }> = [];\n\tfor (const name of msg.addedToolNames ?? []) {\n\t\tconst normalizedName = normalizeToolName(name);\n\t\tif (!deferredToolNames.has(normalizedName) || loadedToolNames.has(normalizedName)) continue;\n\t\tloadedToolNames.add(normalizedName);\n\t\treferences.push({\n\t\t\ttype: \"tool_reference\",\n\t\t\ttool_name: isOAuthToken ? toClaudeCodeName(name) : name,\n\t\t});\n\t}\n\tconst convertedContent = convertContentBlocks(msg.content);\n\t// Anthropic rejects tool references mixed with ordinary tool-result content.\n\treturn {\n\t\ttoolResult: {\n\t\t\ttype: \"tool_result\",\n\t\t\ttool_use_id: msg.toolCallId,\n\t\t\tcontent: references.length > 0 ? references : convertedContent,\n\t\t\tis_error: msg.isError,\n\t\t},\n\t\tsiblingContent:\n\t\t\treferences.length === 0\n\t\t\t\t? []\n\t\t\t\t: typeof convertedContent === \"string\"\n\t\t\t\t\t? [{ type: \"text\", text: convertedContent }]\n\t\t\t\t\t: convertedContent,\n\t};\n}\n\ninterface ConvertedAnthropicMessages {\n\tmessages: MessageParam[];\n\tassistantLevels: Map<number, AnthropicEffort>;\n}\n\nfunction convertMessages(\n\ttransformedMessages: Message[],\n\tisOAuthToken: boolean,\n\tcacheControl?: CacheControlEphemeral,\n\tallowEmptySignature = false,\n\tdeferredToolNames: ReadonlySet<string> = new Set(),\n\tnormalizeToolName: (name: string) => string = (name) => name,\n\tmanagedProvider?: string,\n): ConvertedAnthropicMessages {\n\tconst params: MessageParam[] = [];\n\tconst assistantLevels = new Map<number, AnthropicEffort>();\n\tconst loadedToolNames = new Set<string>();\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\tconst thinkingSignature = block.thinkingSignature;\n\t\t\t\t\tconst hasThinkingSignature = !!thinkingSignature && thinkingSignature.trim().length > 0;\n\t\t\t\t\tif (block.thinking.trim().length === 0 && !hasThinkingSignature) 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 for Anthropic. Some compatible providers emit\n\t\t\t\t\t// and accept empty signatures, so let marked models preserve the block.\n\t\t\t\t\tif (!hasThinkingSignature) {\n\t\t\t\t\t\tblocks.push(\n\t\t\t\t\t\t\tallowEmptySignature\n\t\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\t\t\t\tthinking: sanitizeSurrogates(block.thinking),\n\t\t\t\t\t\t\t\t\t\tsignature: \"\",\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: sanitizeSurrogates(block.thinking),\n\t\t\t\t\t\t\t\t\t},\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: 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\tid: 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\tconst messageIndex = params.length;\n\t\t\tparams.push({\n\t\t\t\trole: \"assistant\",\n\t\t\t\tcontent: blocks,\n\t\t\t});\n\t\t\tif (\n\t\t\t\tmanagedProvider !== undefined &&\n\t\t\t\tmsg.api === \"anthropic-messages\" &&\n\t\t\t\tmsg.provider === managedProvider &&\n\t\t\t\tisAnthropicEffort(msg.providerThinkingLevel)\n\t\t\t) {\n\t\t\t\tassistantLevels.set(messageIndex, msg.providerThinkingLevel);\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\t\t\tconst siblingContent: ContentBlockParam[] = [];\n\t\t\tlet j = i;\n\t\t\twhile (j < transformedMessages.length && transformedMessages[j].role === \"toolResult\") {\n\t\t\t\tconst converted = convertToolResult(\n\t\t\t\t\ttransformedMessages[j] as ToolResultMessage,\n\t\t\t\t\tisOAuthToken,\n\t\t\t\t\tdeferredToolNames,\n\t\t\t\t\tloadedToolNames,\n\t\t\t\t\tnormalizeToolName,\n\t\t\t\t);\n\t\t\t\ttoolResults.push(converted.toolResult);\n\t\t\t\tsiblingContent.push(...converted.siblingContent);\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// Displaced reference-bearing results must follow every tool_result block.\n\t\t\tparams.push({\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: [...toolResults, ...siblingContent],\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 { messages: params, assistantLevels };\n}\n\nfunction isAnthropicEffort(value: unknown): value is AnthropicEffort {\n\treturn value === \"low\" || value === \"medium\" || value === \"high\" || value === \"xhigh\" || value === \"max\";\n}\n\nfunction insertThinkingLevelMessages(\n\tconverted: ConvertedAnthropicMessages,\n\tactiveEffort: AnthropicEffort,\n): MessageParam[] {\n\tconst messages: MessageParam[] = [];\n\tfor (let index = 0; index < converted.messages.length; index++) {\n\t\tconst historicalEffort = converted.assistantLevels.get(index);\n\t\tif (historicalEffort !== undefined) {\n\t\t\tmessages.push({ role: \"system\", content: [], output_config: { effort: historicalEffort } });\n\t\t}\n\t\tmessages.push(converted.messages[index]);\n\t}\n\tmessages.push({ role: \"system\", content: [], output_config: { effort: activeEffort } });\n\treturn messages;\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\tsupportsStrictTools: boolean,\n\tcacheControl?: CacheControlEphemeral,\n\tdeferLoading = false,\n): BetaTool[] {\n\tif (!tools) return [];\n\n\treturn tools.map((tool, index) => {\n\t\tconst strict = resolveJsonSchemaStrictSampling(tool, supportsStrictTools);\n\t\tconst parameters = getJsonSchemaToolParameters(tool, strict);\n\t\tconst schema = parameters as { properties?: unknown; required?: string[] };\n\t\tconst legacyInputSchema = {\n\t\t\ttype: \"object\" as const,\n\t\t\tproperties: schema.properties ?? {},\n\t\t\trequired: schema.required ?? [],\n\t\t};\n\t\tconst inputSchema =\n\t\t\tstrict === true\n\t\t\t\t? {\n\t\t\t\t\t\t...(parameters as Record<string, unknown>),\n\t\t\t\t\t\t...legacyInputSchema,\n\t\t\t\t\t}\n\t\t\t\t: legacyInputSchema;\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\t...(strict === true ? { strict: true } : {}),\n\t\t\tinput_schema: inputSchema,\n\t\t\t...(deferLoading ? { defer_loading: true } : {}),\n\t\t\t...(cacheControl && index === tools.length - 1 ? { cache_control: cacheControl } : {}),\n\t\t};\n\t});\n}\n\nfunction mapStopReason(\n\treason: BetaStopReason | string,\n\tstopDetails?: RefusalStopDetails | null,\n): { stopReason: StopReason; errorMessage?: string } {\n\tswitch (reason) {\n\t\tcase \"end_turn\":\n\t\t\treturn { stopReason: \"stop\" };\n\t\tcase \"max_tokens\":\n\t\t\treturn { stopReason: \"length\" };\n\t\tcase \"tool_use\":\n\t\t\treturn { stopReason: \"toolUse\" };\n\t\tcase \"refusal\":\n\t\t\treturn {\n\t\t\t\tstopReason: \"error\",\n\t\t\t\terrorMessage: stopDetails?.explanation || `The model refused to complete the request`,\n\t\t\t};\n\t\tcase \"pause_turn\": // Stop is good enough -> resubmit\n\t\t\treturn { stopReason: \"stop\" };\n\t\tcase \"stop_sequence\":\n\t\t\treturn { stopReason: \"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 { stopReason: \"error\", errorMessage: \"Provider stopped with: sensitive\" };\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-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAa1C,OAAO,KAAK,EASX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAMb,MAAM,aAAa,CAAC;AAiJrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AA2ChE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,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;AAqOD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CAgUzE,CAAC;AA2BF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CA8ClF,CAAC","sourcesContent":["import Anthropic from \"@anthropic-ai/sdk\";\nimport type {\n\tBetaStopReason,\n\tBetaThinkingDroppedInputTransformation,\n\tBetaTool,\n\tBetaCacheControlEphemeral as CacheControlEphemeral,\n\tBetaContentBlockParam as ContentBlockParam,\n\tMessageCreateParamsStreaming,\n\tBetaMessageParam as MessageParam,\n\tBetaRawMessageStreamEvent as RawMessageStreamEvent,\n\tBetaRefusalStopDetails as RefusalStopDetails,\n} from \"@anthropic-ai/sdk/resources/beta/messages/messages.js\";\nimport { calculateCost } from \"../models.ts\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tCacheRetention,\n\tImageContent,\n\tMessage,\n\tModel,\n\tProviderEnv,\n\tProviderHeaders,\n\tSimpleStreamOptions,\n\tStopReason,\n\tStreamFunction,\n\tStreamOptions,\n\tTextContent,\n\tThinkingContent,\n\tTool,\n\tToolCall,\n\tToolResultMessage,\n} from \"../types.ts\";\nimport { appendAssistantMessageDiagnostic } from \"../utils/diagnostics.ts\";\nimport { AssistantMessageEventStream } from \"../utils/event-stream.ts\";\nimport { headersToRecord } from \"../utils/headers.ts\";\nimport { parseJsonWithRepair, parseStreamingJson } from \"../utils/json-parse.ts\";\nimport { getPiUserAgent } from \"../utils/pi-user-agent.ts\";\nimport { getProviderEnvValue } from \"../utils/provider-env.ts\";\nimport { retryProviderRequest } from \"../utils/provider-retry.ts\";\nimport { sanitizeSurrogates } from \"../utils/sanitize-unicode.ts\";\nimport { getSystemMessageText, renderSystemMessageUpdate } from \"../utils/text.ts\";\nimport {\n\tgetCurrentTools,\n\tgetDeclaredTools,\n\tgetInitialSystemMessage,\n\thasToolRedefinitions,\n\tresolveTranscript,\n\ttype TranscriptContext,\n} from \"../utils/transcript.ts\";\n\nimport { getJsonSchemaToolParameters, resolveJsonSchemaStrictSampling } from \"./constrained-sampling.ts\";\nimport { buildCopilotDynamicHeaders, hasCopilotVisionInput } from \"./github-copilot-headers.ts\";\nimport { adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext } from \"./simple-options.ts\";\nimport { transformMessages } from \"./transform-messages.ts\";\n\n/**\n * Resolve cache retention preference.\n * Defaults to \"short\" and uses PI_CACHE_RETENTION for backward compatibility.\n */\nfunction resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEnv): CacheRetention {\n\tif (cacheRetention) {\n\t\treturn cacheRetention;\n\t}\n\tif (getProviderEnvValue(\"PI_CACHE_RETENTION\", env) === \"long\") {\n\t\treturn \"long\";\n\t}\n\treturn \"short\";\n}\n\nfunction getCacheControl(\n\tmodel: Model<\"anthropic-messages\">,\n\tcacheRetention?: CacheRetention,\n\tenv?: ProviderEnv,\n): { retention: CacheRetention; cacheControl?: CacheControlEphemeral } {\n\tconst retention = resolveCacheRetention(cacheRetention, env);\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.251\";\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\";\nconst SERVER_SIDE_FALLBACK_BETA = \"server-side-fallback-2026-07-01\";\nconst MID_CONVERSATION_OUTPUT_CONFIG_BETA = \"mid-conversation-output-config-2026-07-01\";\nconst THINKING_BINDING_CONTROLS_BETA = \"thinking-binding-controls-2026-08-01\";\nconst MID_CONVERSATION_TOOL_CHANGES_BETA = \"mid-conversation-tool-changes-2026-07-01\";\n\n/**\n * Stable deferred tool declared whenever native tool changes are in use. Anthropic adds\n * hidden prompt scaffolding as soon as any tool has `defer_loading`; declaring this\n * placeholder from the first request keeps that scaffolding in the cached prefix, so the\n * first real late tool does not invalidate the cache (measured: full miss without it).\n * It is never activated and the model cannot see it.\n */\nconst DEFERRED_TOOL_PLACEHOLDER: BetaTool = {\n\tname: \"__pi_deferred_placeholder__\",\n\tdescription: \"Reserved placeholder. Never available. Never call this.\",\n\tinput_schema: { type: \"object\", properties: {}, required: [] },\n\tdefer_loading: true,\n};\n\nfunction shouldUseServerSideFallbackBeta(model: Model<\"anthropic-messages\">): boolean {\n\treturn (model.compat?.allowedFallbackModels?.length ?? 0) > 0;\n}\n\nfunction getAnthropicCompat(model: Model<\"anthropic-messages\">) {\n\tconst isOpenRouter = model.provider === \"openrouter\" || model.baseUrl.includes(\"openrouter.ai\");\n\treturn {\n\t\tsupportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true,\n\t\tsupportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,\n\t\tsendSessionAffinityHeaders: model.compat?.sendSessionAffinityHeaders ?? isOpenRouter,\n\t\tsessionAffinityFormat: model.compat?.sessionAffinityFormat ?? (isOpenRouter ? \"openrouter\" : undefined),\n\t\tsupportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? true,\n\t\tsupportsTemperature: model.compat?.supportsTemperature ?? true,\n\t\tallowEmptySignature: model.compat?.allowEmptySignature ?? false,\n\t\tsupportsStrictTools: model.compat?.supportsStrictTools ?? false,\n\t\tsupportsMidConvoSystemMessages: model.compat?.supportsMidConvoSystemMessages ?? false,\n\t\tsupportsMidConvoToolChanges: model.compat?.supportsMidConvoToolChanges ?? false,\n\t};\n}\n\nexport interface AnthropicOptions extends StreamOptions {\n\t/**\n\t * Enable extended thinking.\n\t * For adaptive thinking models: the model decides when/how much to think.\n\t * For older models: uses budget-based thinking with thinkingBudgetTokens.\n\t * Default: undefined (thinking is omitted unless `streamSimple()` maps\n\t * a simple reasoning level to this option, or callers set it explicitly).\n\t */\n\tthinkingEnabled?: boolean;\n\t/**\n\t * Token budget for extended thinking (older models only).\n\t * Ignored for adaptive thinking models.\n\t * Default: 1024 when `thinkingEnabled` is true and no budget is provided.\n\t */\n\tthinkingBudgetTokens?: number;\n\t/**\n\t * Effort level for adaptive thinking models.\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\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 * Default: omitted unless `streamSimple()` maps a simple reasoning\n\t * level to this option.\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.\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 * Default: \"summarized\" when thinking is enabled.\n\t */\n\tthinkingDisplay?: AnthropicThinkingDisplay;\n\t/**\n\t * Whether to request the interleaved thinking beta header for non-adaptive\n\t * thinking models. Adaptive thinking models have interleaved thinking built in,\n\t * so the header is skipped for them regardless of this setting.\n\t * Default: true.\n\t */\n\tinterleavedThinking?: boolean;\n\t/**\n\t * Anthropic tool choice behavior. String values map to Anthropic's built-in\n\t * choices; `{ type: \"tool\", name }` forces a specific tool.\n\t * Default: omitted (Anthropic default behavior, currently equivalent to auto).\n\t */\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: (ProviderHeaders | undefined)[]): ProviderHeaders {\n\tconst merged: ProviderHeaders = {};\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\nfunction mergeClientHeaders(...headerSources: (ProviderHeaders | undefined)[]): ProviderHeaders {\n\treturn mergeHeaders({ \"User-Agent\": getPiUserAgent() }, ...headerSources);\n}\n\nfunction hasHeader(headers: ProviderHeaders | undefined, name: string): boolean {\n\tif (!headers) return false;\n\tconst expected = name.toLowerCase();\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tif (key.toLowerCase() === expected && value !== null && value.trim().length > 0) return true;\n\t}\n\treturn false;\n}\n\nfunction assertRequestAuth(provider: string, apiKey: string | undefined, headers: ProviderHeaders | undefined): void {\n\tif (apiKey) return;\n\tif (\n\t\thasHeader(headers, \"authorization\") ||\n\t\thasHeader(headers, \"x-api-key\") ||\n\t\thasHeader(headers, \"cf-aig-authorization\")\n\t) {\n\t\treturn;\n\t}\n\tthrow new Error(`No API key for provider: ${provider}`);\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\nasync function* iterateAnthropicEvents(\n\tresponse: Response,\n\tsignal?: AbortSignal,\n): AsyncGenerator<RawMessageStreamEvent> {\n\tif (!response.body) {\n\t\tthrow new Error(\"Attempted to iterate over an Anthropic response with no body\");\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\tthrow new Error(sse.data);\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 Error(\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);\n\t\t}\n\t}\n\n\tif (sawMessageStart && !sawMessageEnd) {\n\t\tthrow new Error(\"Anthropic stream ended before message_stop\");\n\t}\n}\n\nexport const stream: StreamFunction<\"anthropic-messages\", AnthropicOptions> = (\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: TranscriptContext,\n\toptions?: AnthropicOptions,\n): AssistantMessageEventStream => {\n\tconst stream = new AssistantMessageEventStream();\n\tconst normalizedContext = resolveTranscript(context, getAnthropicCompat(model).supportsMidConvoSystemMessages);\n\tconst currentTools = getCurrentTools(normalizedContext.messages);\n\n\t(async () => {\n\t\tconst providerThinkingLevel = model.compat?.supportsMidConvoEffort ? (options?.effort ?? \"high\") : undefined;\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\t...(providerThinkingLevel === undefined ? {} : { providerThinkingLevel }),\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: \"pending\",\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\n\t\ttry {\n\t\t\tlet client: Anthropic;\n\t\t\tlet isOAuth: boolean;\n\t\t\tlet usageModel = model;\n\t\t\tlet inputTransformations: BetaThinkingDroppedInputTransformation[] | undefined;\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;\n\t\t\t\tassertRequestAuth(model.provider, apiKey, options?.headers);\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(normalizedContext.messages);\n\t\t\t\t\tcopilotDynamicHeaders = buildCopilotDynamicHeaders({\n\t\t\t\t\t\tmessages: normalizedContext.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 = resolveCacheRetention(options?.cacheRetention, options?.env);\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?.headers,\n\t\t\t\t\toptions?.fetch,\n\t\t\t\t\tcopilotDynamicHeaders,\n\t\t\t\t\tcacheSessionId,\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, normalizedContext, isOAuth, options);\n\t\t\tconst nextParams = await options?.onPayload?.(params, model);\n\t\t\tif (nextParams !== undefined) {\n\t\t\t\tparams = { ...(nextParams as MessageCreateParamsStreaming), stream: true };\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\tmaxRetries: 0,\n\t\t\t};\n\t\t\tconst response = await retryProviderRequest(\n\t\t\t\t() => client.beta.messages.create(params, requestOptions).asResponse(),\n\t\t\t\t{\n\t\t\t\t\tmaxRetries: options?.maxRetries,\n\t\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs,\n\t\t\t\t\tsignal: options?.signal,\n\t\t\t\t},\n\t\t\t);\n\t\t\tawait options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);\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\tconst transformations = event.message.input_transformations;\n\t\t\t\t\tif (Array.isArray(transformations)) inputTransformations = transformations;\n\t\t\t\t\tconst responseModel = event.message.model;\n\t\t\t\t\tif (responseModel !== model.id) output.responseModel = responseModel;\n\t\t\t\t\tconst fallbackCost =\n\t\t\t\t\t\tresponseModel === model.id\n\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t: model.compat?.allowedFallbackModels?.find(\n\t\t\t\t\t\t\t\t\t(fallback) => fallback.provider === model.provider && fallback.model === responseModel,\n\t\t\t\t\t\t\t\t)?.cost;\n\t\t\t\t\tusageModel = fallbackCost ? { ...model, id: responseModel, cost: fallbackCost } : model;\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\toutput.usage.cacheWrite1h = event.message.usage.cache_creation?.ephemeral_1h_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(usageModel, output.usage);\n\t\t\t\t} else if (event.type === \"content_block_start\") {\n\t\t\t\t\tif (event.content_block.type === \"fallback\") {\n\t\t\t\t\t\tif (output.content.length > 0) {\n\t\t\t\t\t\t\tthrow new Error(\"Anthropic performed an unsupported mid-output model fallback\");\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 (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: event.content_block.text ?? \"\",\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: event.content_block.thinking ?? \"\",\n\t\t\t\t\t\t\tthinkingSignature: event.content_block.signature ?? \"\",\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, currentTools)\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\tconst transformations = event.input_transformations;\n\t\t\t\t\tif (Array.isArray(transformations)) inputTransformations = transformations;\n\t\t\t\t\tif (event.delta.stop_reason) {\n\t\t\t\t\t\toutput.rawStopReason = event.delta.stop_reason;\n\t\t\t\t\t\tconst stopReasonResult = mapStopReason(event.delta.stop_reason, event.delta.stop_details);\n\t\t\t\t\t\toutput.stopReason = stopReasonResult.stopReason;\n\t\t\t\t\t\tif (stopReasonResult.errorMessage) {\n\t\t\t\t\t\t\toutput.errorMessage = stopReasonResult.errorMessage;\n\t\t\t\t\t\t}\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) {\n\t\t\t\t\t\tif (event.usage.input_tokens != null) {\n\t\t\t\t\t\t\toutput.usage.input = event.usage.input_tokens;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (event.usage.output_tokens != null) {\n\t\t\t\t\t\t\toutput.usage.output = event.usage.output_tokens;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (event.usage.cache_read_input_tokens != null) {\n\t\t\t\t\t\t\toutput.usage.cacheRead = event.usage.cache_read_input_tokens;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (event.usage.cache_creation_input_tokens != null) {\n\t\t\t\t\t\t\toutput.usage.cacheWrite = event.usage.cache_creation_input_tokens;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Anthropic reports reasoning tokens as a subset of output tokens.\n\t\t\t\t\t\tconst thinkingTokens = event.usage.output_tokens_details?.thinking_tokens;\n\t\t\t\t\t\tif (thinkingTokens != null) {\n\t\t\t\t\t\t\toutput.usage.reasoning = thinkingTokens;\n\t\t\t\t\t\t}\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(usageModel, 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 === \"pending\") {\n\t\t\t\tthrow new Error(\"Anthropic stream ended without a stop reason\");\n\t\t\t}\n\t\t\tif (output.stopReason === \"aborted\" || output.stopReason === \"error\") {\n\t\t\t\tthrow new Error(output.errorMessage || \"An unknown error occurred\");\n\t\t\t}\n\t\t\tif (inputTransformations && inputTransformations.length > 0) {\n\t\t\t\tappendAssistantMessageDiagnostic(output, {\n\t\t\t\t\ttype: \"anthropic_input_transformations\",\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\ttransformations: inputTransformations.map((transformation) => ({\n\t\t\t\t\t\t\ttype: transformation.type ?? undefined,\n\t\t\t\t\t\t\tpath: transformation.path ?? undefined,\n\t\t\t\t\t\t\treason: transformation.reason ?? undefined,\n\t\t\t\t\t\t})),\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\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\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/**\n * Map ThinkingLevel to Anthropic effort levels for adaptive thinking.\n * Note: effort \"max\" is available on all adaptive-thinking Claude models, while native\n * \"xhigh\" is only available on Opus 4.7/4.8, Sonnet 5, and Fable 5.\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\tdefault:\n\t\t\treturn \"high\";\n\t}\n}\n\nexport const streamSimple: StreamFunction<\"anthropic-messages\", SimpleStreamOptions> = (\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: TranscriptContext,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream => {\n\tassertRequestAuth(model.provider, options?.apiKey, options?.headers);\n\n\tconst base = {\n\t\t...buildBaseOptions(model, context, options, options?.apiKey),\n\t\ttoolChoice: options?.toolChoice,\n\t} satisfies AnthropicOptions;\n\tif (!options?.reasoning) {\n\t\treturn stream(model, context, {\n\t\t\t...base,\n\t\t\tthinkingEnabled: false,\n\t\t} satisfies AnthropicOptions);\n\t}\n\n\t// For models with adaptive thinking: use an effort level.\n\t// For older models: use budget-based thinking.\n\tif (model.compat?.forceAdaptiveThinking === true) {\n\t\tconst effort = mapThinkingLevelToEffort(model, options.reasoning);\n\t\treturn stream(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\t// Undefined means the caller did not request an output cap; let the helper use the model cap.\n\t// Do not coerce to 0 here, or the thinking budget would become the entire max_tokens value.\n\tconst adjusted = adjustMaxTokensForThinking(\n\t\tbase.maxTokens,\n\t\tmodel.maxTokens,\n\t\toptions.reasoning,\n\t\toptions.thinkingBudgets,\n\t);\n\n\tconst maxTokens = clampMaxTokensToContext(model, context, adjusted.maxTokens);\n\n\treturn stream(model, context, {\n\t\t...base,\n\t\tmaxTokens,\n\t\tthinkingEnabled: true,\n\t\tthinkingBudgetTokens: Math.min(adjusted.thinkingBudget, Math.max(0, maxTokens - 1024)),\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 | undefined,\n\toptionsHeaders?: ProviderHeaders,\n\tfetch?: typeof globalThis.fetch,\n\tdynamicHeaders?: Record<string, string>,\n\tsessionId?: string,\n): { client: Anthropic; isOAuthToken: boolean } {\n\t// Copilot: Bearer auth.\n\tif (model.provider === \"github-copilot\") {\n\t\tconst client = new Anthropic({\n\t\t\tapiKey: null,\n\t\t\tauthToken: apiKey ?? null,\n\t\t\tbaseURL: model.baseUrl,\n\t\t\tdangerouslyAllowBrowser: true,\n\t\t\tfetch,\n\t\t\tdefaultHeaders: mergeClientHeaders(\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},\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 (apiKey && isOAuthToken(apiKey)) {\n\t\tconst client = new Anthropic({\n\t\t\tapiKey: null,\n\t\t\tauthToken: apiKey,\n\t\t\tbaseURL: model.baseUrl,\n\t\t\tdangerouslyAllowBrowser: true,\n\t\t\tfetch,\n\t\t\tdefaultHeaders: mergeClientHeaders(\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\"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 or header-owned auth.\n\tconst compat = getAnthropicCompat(model);\n\tconst sessionAffinityHeaders: ProviderHeaders = {};\n\tif (sessionId && compat.sendSessionAffinityHeaders) {\n\t\tconst header = compat.sessionAffinityFormat === \"openrouter\" ? \"x-session-id\" : \"x-session-affinity\";\n\t\tsessionAffinityHeaders[header] = sessionId;\n\t}\n\tconst defaultHeaders = mergeClientHeaders(\n\t\t{\n\t\t\taccept: \"application/json\",\n\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t},\n\t\tsessionAffinityHeaders,\n\t\tmodel.headers,\n\t\toptionsHeaders,\n\t);\n\tconst client = new Anthropic({\n\t\tapiKey: apiKey ?? null,\n\t\tauthToken: null,\n\t\tbaseURL: model.baseUrl,\n\t\tdangerouslyAllowBrowser: true,\n\t\tfetch,\n\t\tdefaultHeaders,\n\t});\n\n\treturn { client, isOAuthToken: false };\n}\n\nfunction getBetaFeatures(\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: TranscriptContext,\n\tisOAuthToken: boolean,\n\tnativeToolChanges: boolean,\n\toptions?: AnthropicOptions,\n): NonNullable<MessageCreateParamsStreaming[\"betas\"]> {\n\tlet configuredFeatures: string | null | undefined;\n\tfor (const headers of [model.headers, options?.headers]) {\n\t\tfor (const [name, value] of Object.entries(headers ?? {})) {\n\t\t\tif (name.toLowerCase() === \"anthropic-beta\") configuredFeatures = value;\n\t\t}\n\t}\n\tif (configuredFeatures === null) return [];\n\tif (configuredFeatures !== undefined) {\n\t\treturn [\n\t\t\t...new Set(\n\t\t\t\tconfiguredFeatures\n\t\t\t\t\t.split(\",\")\n\t\t\t\t\t.map((feature) => feature.trim())\n\t\t\t\t\t.filter((feature) => feature.length > 0),\n\t\t\t),\n\t\t];\n\t}\n\n\tconst features: NonNullable<MessageCreateParamsStreaming[\"betas\"]> = [];\n\tif (isOAuthToken) features.push(\"claude-code-20250219\", \"oauth-2025-04-20\");\n\tif (shouldUseFineGrainedToolStreamingBeta(model, context)) features.push(FINE_GRAINED_TOOL_STREAMING_BETA);\n\tif (\n\t\tmodel.reasoning &&\n\t\toptions?.thinkingEnabled === true &&\n\t\t(options.interleavedThinking ?? true) &&\n\t\tmodel.compat?.forceAdaptiveThinking !== true\n\t) {\n\t\tfeatures.push(INTERLEAVED_THINKING_BETA);\n\t}\n\tif (shouldUseServerSideFallbackBeta(model)) features.push(SERVER_SIDE_FALLBACK_BETA);\n\tif (model.compat?.supportsMidConvoEffort === true) {\n\t\tfeatures.push(MID_CONVERSATION_OUTPUT_CONFIG_BETA, THINKING_BINDING_CONTROLS_BETA);\n\t}\n\tif (nativeToolChanges) features.push(MID_CONVERSATION_TOOL_CHANGES_BETA);\n\treturn [...new Set(features)];\n}\n\nfunction buildParams(\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: TranscriptContext,\n\tisOAuthToken: boolean,\n\toptions?: AnthropicOptions,\n): MessageCreateParamsStreaming {\n\tconst { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env);\n\tconst compat = getAnthropicCompat(model);\n\tconst initialSystemMessage = getInitialSystemMessage(context.messages);\n\tconst initialSystemText = initialSystemMessage ? getSystemMessageText(initialSystemMessage) : \"\";\n\tconst transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);\n\tconst conversationMessages = initialSystemMessage ? transformedMessages.slice(1) : transformedMessages;\n\t// Native tool changes reference tools by name, so a redefined name cannot be expressed,\n\t// and Anthropic rejects a tool list where every tool is deferred, so there must be an\n\t// initial active tool to anchor the deferred ones. Otherwise the current tool list is sent.\n\tconst initialTools = initialSystemMessage?.toolsAdded ?? [];\n\tconst nativeToolChanges =\n\t\tcompat.supportsMidConvoSystemMessages &&\n\t\tcompat.supportsMidConvoToolChanges &&\n\t\tinitialTools.length > 0 &&\n\t\t!hasToolRedefinitions(context.messages);\n\tconst converted = convertMessages(\n\t\tconversationMessages,\n\t\tisOAuthToken,\n\t\tcacheControl,\n\t\tcompat.allowEmptySignature,\n\t\tmodel.compat?.supportsMidConvoEffort === true ? model.provider : undefined,\n\t\tnativeToolChanges,\n\t);\n\tconst activeEffort = options?.effort ?? \"high\";\n\tconst betaFeatures = getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, options);\n\tconst params: MessageCreateParamsStreaming = {\n\t\tmodel: model.id,\n\t\tmessages:\n\t\t\tmodel.compat?.supportsMidConvoEffort === true\n\t\t\t\t? insertThinkingLevelMessages(converted, activeEffort)\n\t\t\t\t: converted.messages,\n\t\tmax_tokens: options?.maxTokens ?? model.maxTokens,\n\t\tstream: true,\n\t\t...(betaFeatures.length > 0 ? { betas: betaFeatures } : {}),\n\t};\n\n\t// For OAuth tokens, we MUST include Claude Code identity\n\tif (isOAuthToken) {\n\t\tparams.system = [\n\t\t\t{\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: \"You are Claude Code, Anthropic's official CLI for Claude.\",\n\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t},\n\t\t];\n\t\tif (initialSystemText) {\n\t\t\tparams.system.push({\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: sanitizeSurrogates(initialSystemText),\n\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t});\n\t\t}\n\t} else if (initialSystemText) {\n\t\t// Add cache control to system prompt for non-OAuth tokens\n\t\tparams.system = [\n\t\t\t{\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: sanitizeSurrogates(initialSystemText),\n\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t},\n\t\t];\n\t}\n\n\t// Temperature is incompatible with extended thinking and unsupported on Claude Opus 4.7+.\n\tif (\n\t\toptions?.temperature !== undefined &&\n\t\t!options?.thinkingEnabled &&\n\t\tmodel.compat?.supportsMidConvoEffort !== true &&\n\t\tcompat.supportsTemperature\n\t) {\n\t\tparams.temperature = options.temperature;\n\t}\n\n\tconst toolCacheControl = compat.supportsCacheControlOnTools ? cacheControl : undefined;\n\tif (nativeToolChanges) {\n\t\t// Initial tools stay active with the cache breakpoint on the last one. Every later\n\t\t// declaration is deferred and only surfaced by its `tool_addition` block; removed\n\t\t// tools stay declared and are withdrawn by `tool_removal`. The request-level list\n\t\t// therefore only grows, keeping the cached prefix intact across tool changes.\n\t\tconst initialNames = new Set(initialTools.map((tool) => tool.name));\n\t\tconst laterTools = getDeclaredTools(context.messages).filter((tool) => !initialNames.has(tool.name));\n\t\tparams.tools = [\n\t\t\t...convertTools(\n\t\t\t\tinitialTools,\n\t\t\t\tisOAuthToken,\n\t\t\t\tcompat.supportsEagerToolInputStreaming,\n\t\t\t\tcompat.supportsStrictTools,\n\t\t\t\ttoolCacheControl,\n\t\t\t),\n\t\t\tDEFERRED_TOOL_PLACEHOLDER,\n\t\t\t...convertTools(\n\t\t\t\tlaterTools,\n\t\t\t\tisOAuthToken,\n\t\t\t\tcompat.supportsEagerToolInputStreaming,\n\t\t\t\tcompat.supportsStrictTools,\n\t\t\t).map((tool) => ({ ...tool, defer_loading: true })),\n\t\t];\n\t} else {\n\t\tconst tools = getCurrentTools(context.messages);\n\t\tif (tools.length > 0) {\n\t\t\tparams.tools = convertTools(\n\t\t\t\ttools,\n\t\t\t\tisOAuthToken,\n\t\t\t\tcompat.supportsEagerToolInputStreaming,\n\t\t\t\tcompat.supportsStrictTools,\n\t\t\t\ttoolCacheControl,\n\t\t\t);\n\t\t}\n\t}\n\n\t// Managed effort models always use adaptive thinking so prefix mismatches can\n\t// be dropped instead of surfacing as persistent 400 responses.\n\tif (model.compat?.supportsMidConvoEffort === true) {\n\t\tparams.thinking = {\n\t\t\ttype: \"adaptive\",\n\t\t\tdisplay: options?.thinkingDisplay ?? \"summarized\",\n\t\t\tblock_binding: { prefix_mismatch_behavior: \"drop_block\" },\n\t\t};\n\t\tparams.output_config = { effort: \"high\" };\n\t} else if (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 (model.compat?.forceAdaptiveThinking === true) {\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\tparams.output_config = { 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 && model.thinkingLevelMap?.off !== null) {\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\tconst allowedFallbackModels = model.compat?.allowedFallbackModels;\n\tif (allowedFallbackModels && allowedFallbackModels.length > 0) {\n\t\tparams.fallbacks = allowedFallbackModels.map((fallback) => ({ model: fallback.model }));\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 convertToolResult(msg: ToolResultMessage): ContentBlockParam {\n\treturn {\n\t\ttype: \"tool_result\",\n\t\ttool_use_id: msg.toolCallId,\n\t\tcontent: convertContentBlocks(msg.content),\n\t\tis_error: msg.isError,\n\t};\n}\n\ninterface ConvertedAnthropicMessages {\n\tmessages: MessageParam[];\n\tassistantLevels: Map<number, AnthropicEffort>;\n}\n\nfunction convertMessages(\n\ttransformedMessages: Message[],\n\tisOAuthToken: boolean,\n\tcacheControl?: CacheControlEphemeral,\n\tallowEmptySignature = false,\n\tmanagedProvider?: string,\n\tnativeToolChanges = false,\n): ConvertedAnthropicMessages {\n\tconst params: MessageParam[] = [];\n\tconst assistantLevels = new Map<number, AnthropicEffort>();\n\t// Later system messages are held back and emitted directly before the next assistant\n\t// message (or at the end of the transcript). Anthropic requires `tool_result` blocks to\n\t// immediately follow their `tool_use`, so a system message between them is rejected; this\n\t// also mirrors where the managed-effort system messages are inserted. As a result an\n\t// update placed before a user message in the transcript lands after it on the wire.\n\tconst pendingSystemMessages: MessageParam[] = [];\n\tconst flushPendingSystemMessages = (): void => {\n\t\tparams.push(...pendingSystemMessages);\n\t\tpendingSystemMessages.length = 0;\n\t};\n\n\tfor (let i = 0; i < transformedMessages.length; i++) {\n\t\tconst msg = transformedMessages[i];\n\n\t\tif (msg.role === \"system\") {\n\t\t\t// Later system messages only reach this point when the model accepts them natively;\n\t\t\t// otherwise the transcript was collapsed into the leading message before conversion.\n\t\t\tconst text = renderSystemMessageUpdate(msg);\n\t\t\tconst blocks: ContentBlockParam[] = [];\n\t\t\tif (text.length > 0) blocks.push({ type: \"text\", text: sanitizeSurrogates(text) });\n\t\t\tif (nativeToolChanges) {\n\t\t\t\tfor (const tool of msg.toolsRemoved ?? []) {\n\t\t\t\t\tblocks.push({\n\t\t\t\t\t\ttype: \"tool_removal\",\n\t\t\t\t\t\ttool: { type: \"tool_reference\", name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tfor (const tool of msg.toolsAdded ?? []) {\n\t\t\t\t\tblocks.push({\n\t\t\t\t\t\ttype: \"tool_addition\",\n\t\t\t\t\t\ttool: { type: \"tool_reference\", name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (blocks.length > 0) pendingSystemMessages.push({ role: \"system\", content: blocks });\n\t\t} else if (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\tflushPendingSystemMessages();\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\tconst thinkingSignature = block.thinkingSignature;\n\t\t\t\t\tconst hasThinkingSignature = !!thinkingSignature && thinkingSignature.trim().length > 0;\n\t\t\t\t\tif (block.thinking.trim().length === 0 && !hasThinkingSignature) 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 for Anthropic. Some compatible providers emit\n\t\t\t\t\t// and accept empty signatures, so let marked models preserve the block.\n\t\t\t\t\tif (!hasThinkingSignature) {\n\t\t\t\t\t\tblocks.push(\n\t\t\t\t\t\t\tallowEmptySignature\n\t\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\t\t\t\tthinking: sanitizeSurrogates(block.thinking),\n\t\t\t\t\t\t\t\t\t\tsignature: \"\",\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: sanitizeSurrogates(block.thinking),\n\t\t\t\t\t\t\t\t\t},\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: 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\tid: 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\tconst messageIndex = params.length;\n\t\t\tparams.push({\n\t\t\t\trole: \"assistant\",\n\t\t\t\tcontent: blocks,\n\t\t\t});\n\t\t\tif (\n\t\t\t\tmanagedProvider !== undefined &&\n\t\t\t\tmsg.api === \"anthropic-messages\" &&\n\t\t\t\tmsg.provider === managedProvider &&\n\t\t\t\tisAnthropicEffort(msg.providerThinkingLevel)\n\t\t\t) {\n\t\t\t\tassistantLevels.set(messageIndex, msg.providerThinkingLevel);\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\t\t\tlet j = i;\n\t\t\twhile (j < transformedMessages.length && transformedMessages[j].role === \"toolResult\") {\n\t\t\t\ttoolResults.push(convertToolResult(transformedMessages[j] as ToolResultMessage));\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\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\tflushPendingSystemMessages();\n\n\t// Add cache_control to the last user or system 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\" || lastMessage.role === \"system\") {\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\" ||\n\t\t\t\t\t\tlastBlock.type === \"image\" ||\n\t\t\t\t\t\tlastBlock.type === \"tool_result\" ||\n\t\t\t\t\t\tlastBlock.type === \"tool_addition\" ||\n\t\t\t\t\t\tlastBlock.type === \"tool_removal\")\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 { messages: params, assistantLevels };\n}\n\nfunction isAnthropicEffort(value: unknown): value is AnthropicEffort {\n\treturn value === \"low\" || value === \"medium\" || value === \"high\" || value === \"xhigh\" || value === \"max\";\n}\n\nfunction insertThinkingLevelMessages(\n\tconverted: ConvertedAnthropicMessages,\n\tactiveEffort: AnthropicEffort,\n): MessageParam[] {\n\tconst messages: MessageParam[] = [];\n\tfor (let index = 0; index < converted.messages.length; index++) {\n\t\tconst historicalEffort = converted.assistantLevels.get(index);\n\t\tif (historicalEffort !== undefined) {\n\t\t\tmessages.push({ role: \"system\", content: [], output_config: { effort: historicalEffort } });\n\t\t}\n\t\tmessages.push(converted.messages[index]);\n\t}\n\tmessages.push({ role: \"system\", content: [], output_config: { effort: activeEffort } });\n\treturn messages;\n}\n\nfunction shouldUseFineGrainedToolStreamingBeta(\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: TranscriptContext,\n): boolean {\n\treturn getCurrentTools(context.messages).length > 0 && !getAnthropicCompat(model).supportsEagerToolInputStreaming;\n}\n\nfunction convertTools(\n\ttools: Tool[],\n\tisOAuthToken: boolean,\n\tsupportsEagerToolInputStreaming: boolean,\n\tsupportsStrictTools: boolean,\n\tcacheControl?: CacheControlEphemeral,\n): BetaTool[] {\n\tif (!tools) return [];\n\n\treturn tools.map((tool, index) => {\n\t\tconst strict = resolveJsonSchemaStrictSampling(tool, supportsStrictTools);\n\t\tconst parameters = getJsonSchemaToolParameters(tool, strict);\n\t\tconst schema = parameters as { properties?: unknown; required?: string[] };\n\t\tconst legacyInputSchema = {\n\t\t\ttype: \"object\" as const,\n\t\t\tproperties: schema.properties ?? {},\n\t\t\trequired: schema.required ?? [],\n\t\t};\n\t\tconst inputSchema =\n\t\t\tstrict === true\n\t\t\t\t? {\n\t\t\t\t\t\t...(parameters as Record<string, unknown>),\n\t\t\t\t\t\t...legacyInputSchema,\n\t\t\t\t\t}\n\t\t\t\t: legacyInputSchema;\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\t...(strict === true ? { strict: true } : {}),\n\t\t\tinput_schema: inputSchema,\n\t\t\t...(cacheControl && index === tools.length - 1 ? { cache_control: cacheControl } : {}),\n\t\t};\n\t});\n}\n\nfunction mapStopReason(\n\treason: BetaStopReason | string,\n\tstopDetails?: RefusalStopDetails | null,\n): { stopReason: StopReason; errorMessage?: string } {\n\tswitch (reason) {\n\t\tcase \"end_turn\":\n\t\t\treturn { stopReason: \"stop\" };\n\t\tcase \"max_tokens\":\n\t\t\treturn { stopReason: \"length\" };\n\t\tcase \"tool_use\":\n\t\t\treturn { stopReason: \"toolUse\" };\n\t\tcase \"refusal\":\n\t\t\treturn {\n\t\t\t\tstopReason: \"error\",\n\t\t\t\terrorMessage: stopDetails?.explanation || `The model refused to complete the request`,\n\t\t\t};\n\t\tcase \"pause_turn\": // Stop is good enough -> resubmit\n\t\t\treturn { stopReason: \"stop\" };\n\t\tcase \"stop_sequence\":\n\t\t\treturn { stopReason: \"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 { stopReason: \"error\", errorMessage: \"Provider stopped with: sensitive\" };\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"]}