@f5-sales-demo/pi-ai 19.51.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 (128) hide show
  1. package/CHANGELOG.md +1997 -0
  2. package/README.md +1160 -0
  3. package/package.json +135 -0
  4. package/src/api-registry.ts +95 -0
  5. package/src/auth-storage.ts +2694 -0
  6. package/src/cli.ts +493 -0
  7. package/src/index.ts +42 -0
  8. package/src/model-cache.ts +97 -0
  9. package/src/model-manager.ts +349 -0
  10. package/src/model-thinking.ts +561 -0
  11. package/src/models.json +49439 -0
  12. package/src/models.json.d.ts +9 -0
  13. package/src/models.ts +56 -0
  14. package/src/prompts/turn-aborted-guidance.md +4 -0
  15. package/src/provider-details.ts +81 -0
  16. package/src/provider-models/descriptors.ts +285 -0
  17. package/src/provider-models/google.ts +90 -0
  18. package/src/provider-models/index.ts +4 -0
  19. package/src/provider-models/openai-compat.ts +2074 -0
  20. package/src/provider-models/special.ts +106 -0
  21. package/src/providers/amazon-bedrock.ts +706 -0
  22. package/src/providers/anthropic.ts +1682 -0
  23. package/src/providers/azure-openai-responses.ts +391 -0
  24. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  25. package/src/providers/cursor/proto/agent.proto +3526 -0
  26. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  27. package/src/providers/cursor/proto/buf.yaml +17 -0
  28. package/src/providers/cursor.ts +2218 -0
  29. package/src/providers/github-copilot-headers.ts +140 -0
  30. package/src/providers/gitlab-duo.ts +381 -0
  31. package/src/providers/google-gemini-cli.ts +1133 -0
  32. package/src/providers/google-shared.ts +354 -0
  33. package/src/providers/google-vertex.ts +436 -0
  34. package/src/providers/google.ts +381 -0
  35. package/src/providers/kimi.ts +151 -0
  36. package/src/providers/openai-codex/constants.ts +43 -0
  37. package/src/providers/openai-codex/request-transformer.ts +158 -0
  38. package/src/providers/openai-codex/response-handler.ts +81 -0
  39. package/src/providers/openai-codex-responses.ts +2345 -0
  40. package/src/providers/openai-completions-compat.ts +159 -0
  41. package/src/providers/openai-completions.ts +1290 -0
  42. package/src/providers/openai-responses-shared.ts +452 -0
  43. package/src/providers/openai-responses.ts +519 -0
  44. package/src/providers/register-builtins.ts +329 -0
  45. package/src/providers/synthetic.ts +154 -0
  46. package/src/providers/transform-messages.ts +234 -0
  47. package/src/rate-limit-utils.ts +84 -0
  48. package/src/stream.ts +728 -0
  49. package/src/types.ts +546 -0
  50. package/src/usage/claude.ts +337 -0
  51. package/src/usage/gemini.ts +248 -0
  52. package/src/usage/github-copilot.ts +421 -0
  53. package/src/usage/google-antigravity.ts +200 -0
  54. package/src/usage/kimi.ts +286 -0
  55. package/src/usage/minimax-code.ts +31 -0
  56. package/src/usage/openai-codex.ts +387 -0
  57. package/src/usage/zai.ts +247 -0
  58. package/src/usage.ts +130 -0
  59. package/src/utils/abort.ts +36 -0
  60. package/src/utils/anthropic-auth.ts +293 -0
  61. package/src/utils/discovery/antigravity.ts +261 -0
  62. package/src/utils/discovery/codex.ts +371 -0
  63. package/src/utils/discovery/cursor.ts +306 -0
  64. package/src/utils/discovery/gemini.ts +248 -0
  65. package/src/utils/discovery/index.ts +5 -0
  66. package/src/utils/discovery/openai-compatible.ts +224 -0
  67. package/src/utils/event-stream.ts +209 -0
  68. package/src/utils/http-inspector.ts +165 -0
  69. package/src/utils/idle-iterator.ts +176 -0
  70. package/src/utils/json-parse.ts +28 -0
  71. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  72. package/src/utils/oauth/anthropic.ts +134 -0
  73. package/src/utils/oauth/api-key-validation.ts +92 -0
  74. package/src/utils/oauth/callback-server.ts +276 -0
  75. package/src/utils/oauth/cerebras.ts +59 -0
  76. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  77. package/src/utils/oauth/cursor.ts +157 -0
  78. package/src/utils/oauth/github-copilot.ts +358 -0
  79. package/src/utils/oauth/gitlab-duo.ts +123 -0
  80. package/src/utils/oauth/google-antigravity.ts +275 -0
  81. package/src/utils/oauth/google-gemini-cli.ts +334 -0
  82. package/src/utils/oauth/huggingface.ts +62 -0
  83. package/src/utils/oauth/index.ts +512 -0
  84. package/src/utils/oauth/kagi.ts +47 -0
  85. package/src/utils/oauth/kilo.ts +87 -0
  86. package/src/utils/oauth/kimi.ts +251 -0
  87. package/src/utils/oauth/litellm.ts +81 -0
  88. package/src/utils/oauth/lm-studio.ts +40 -0
  89. package/src/utils/oauth/minimax-code.ts +78 -0
  90. package/src/utils/oauth/moonshot.ts +59 -0
  91. package/src/utils/oauth/nanogpt.ts +51 -0
  92. package/src/utils/oauth/nvidia.ts +70 -0
  93. package/src/utils/oauth/oauth.html +199 -0
  94. package/src/utils/oauth/ollama.ts +47 -0
  95. package/src/utils/oauth/openai-codex.ts +190 -0
  96. package/src/utils/oauth/opencode.ts +49 -0
  97. package/src/utils/oauth/parallel.ts +46 -0
  98. package/src/utils/oauth/perplexity.ts +200 -0
  99. package/src/utils/oauth/pkce.ts +18 -0
  100. package/src/utils/oauth/qianfan.ts +58 -0
  101. package/src/utils/oauth/qwen-portal.ts +60 -0
  102. package/src/utils/oauth/synthetic.ts +60 -0
  103. package/src/utils/oauth/tavily.ts +46 -0
  104. package/src/utils/oauth/together.ts +59 -0
  105. package/src/utils/oauth/types.ts +89 -0
  106. package/src/utils/oauth/venice.ts +59 -0
  107. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  108. package/src/utils/oauth/vllm.ts +40 -0
  109. package/src/utils/oauth/xiaomi.ts +88 -0
  110. package/src/utils/oauth/zai.ts +60 -0
  111. package/src/utils/oauth/zenmux.ts +51 -0
  112. package/src/utils/overflow.ts +134 -0
  113. package/src/utils/retry-after.ts +110 -0
  114. package/src/utils/retry.ts +93 -0
  115. package/src/utils/schema/CONSTRAINTS.md +160 -0
  116. package/src/utils/schema/adapt.ts +20 -0
  117. package/src/utils/schema/compatibility.ts +397 -0
  118. package/src/utils/schema/dereference.ts +93 -0
  119. package/src/utils/schema/equality.ts +93 -0
  120. package/src/utils/schema/fields.ts +147 -0
  121. package/src/utils/schema/index.ts +9 -0
  122. package/src/utils/schema/normalize-cca.ts +479 -0
  123. package/src/utils/schema/sanitize-google.ts +212 -0
  124. package/src/utils/schema/strict-mode.ts +385 -0
  125. package/src/utils/schema/types.ts +5 -0
  126. package/src/utils/tool-choice.ts +81 -0
  127. package/src/utils/validation.ts +664 -0
  128. package/src/utils.ts +147 -0
@@ -0,0 +1,1682 @@
1
+ import * as nodeCrypto from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as tls from "node:tls";
4
+ import Anthropic, { type ClientOptions as AnthropicSdkClientOptions } from "@anthropic-ai/sdk";
5
+ import type {
6
+ ContentBlockParam,
7
+ MessageCreateParamsStreaming,
8
+ MessageParam,
9
+ } from "@anthropic-ai/sdk/resources/messages";
10
+ import { $env, abortableSleep, isEnoent } from "@f5xc-salesdemos/pi-utils";
11
+ import { mapEffortToAnthropicAdaptiveEffort } from "../model-thinking";
12
+ import { calculateCost } from "../models";
13
+ import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
14
+ import type {
15
+ Api,
16
+ AssistantMessage,
17
+ CacheRetention,
18
+ Context,
19
+ ImageContent,
20
+ Message,
21
+ Model,
22
+ RedactedThinkingContent,
23
+ SimpleStreamOptions,
24
+ StopReason,
25
+ StreamFunction,
26
+ StreamOptions,
27
+ TextContent,
28
+ ThinkingContent,
29
+ Tool,
30
+ ToolCall,
31
+ ToolResultMessage,
32
+ Usage,
33
+ } from "../types";
34
+ import { isAnthropicOAuthToken, normalizeToolCallId, resolveCacheRetention } from "../utils";
35
+ import { createAbortSourceTracker } from "../utils/abort";
36
+ import { AssistantMessageEventStream } from "../utils/event-stream";
37
+ import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotAuthError } from "../utils/http-inspector";
38
+ import { createFirstEventWatchdog, getStreamFirstEventTimeoutMs, markFirstStreamEvent } from "../utils/idle-iterator";
39
+ import { parseStreamingJson } from "../utils/json-parse";
40
+ import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
41
+ import {
42
+ buildCopilotDynamicHeaders,
43
+ hasCopilotVisionInput,
44
+ resolveGitHubCopilotBaseUrl,
45
+ } from "./github-copilot-headers";
46
+ import { transformMessages } from "./transform-messages";
47
+
48
+ export type AnthropicHeaderOptions = {
49
+ apiKey: string;
50
+ baseUrl?: string;
51
+ isOAuth?: boolean;
52
+ extraBetas?: string[];
53
+ stream?: boolean;
54
+ modelHeaders?: Record<string, string>;
55
+ };
56
+
57
+ export function normalizeAnthropicBaseUrl(baseUrl?: string): string | undefined {
58
+ const trimmed = baseUrl?.trim();
59
+ if (!trimmed) {
60
+ return undefined;
61
+ }
62
+ const withoutTrailingSlashes = trimmed.replace(/\/+$/, "");
63
+ return withoutTrailingSlashes.endsWith("/v1") ? withoutTrailingSlashes.slice(0, -3) : withoutTrailingSlashes;
64
+ }
65
+
66
+ // Build deduplicated beta header string
67
+ export function buildBetaHeader(baseBetas: string[], extraBetas: string[]): string {
68
+ const seen = new Set<string>();
69
+ const result: string[] = [];
70
+ for (const beta of [...baseBetas, ...extraBetas]) {
71
+ const trimmed = beta.trim();
72
+ if (trimmed && !seen.has(trimmed)) {
73
+ seen.add(trimmed);
74
+ result.push(trimmed);
75
+ }
76
+ }
77
+ return result.join(",");
78
+ }
79
+
80
+ const claudeCodeBetaDefaults = [
81
+ "claude-code-20250219",
82
+ "oauth-2025-04-20",
83
+ "interleaved-thinking-2025-05-14",
84
+ "context-management-2025-06-27",
85
+ "prompt-caching-scope-2026-01-05",
86
+ ];
87
+ function getHeaderCaseInsensitive(headers: Record<string, string> | undefined, headerName: string): string | undefined {
88
+ if (!headers) return undefined;
89
+ const normalizedName = headerName.toLowerCase();
90
+ for (const [key, value] of Object.entries(headers)) {
91
+ if (key.toLowerCase() === normalizedName) return value;
92
+ }
93
+ return undefined;
94
+ }
95
+
96
+ function isClaudeCodeClientUserAgent(userAgent: string | undefined): userAgent is string {
97
+ if (!userAgent) return false;
98
+ return userAgent.toLowerCase().startsWith("claude-cli");
99
+ }
100
+
101
+ function isAnthropicApiBaseUrl(baseUrl?: string): boolean {
102
+ if (!baseUrl) return true;
103
+ try {
104
+ const url = new URL(baseUrl);
105
+ return url.protocol.toLowerCase() === "https:" && url.hostname.toLowerCase() === "api.anthropic.com";
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ const sharedHeaders = {
112
+ "Accept-Encoding": "gzip, deflate, br, zstd",
113
+ Connection: "keep-alive",
114
+ "Content-Type": "application/json",
115
+ "Anthropic-Version": "2023-06-01",
116
+ "Anthropic-Dangerous-Direct-Browser-Access": "true",
117
+ "X-App": "cli",
118
+ };
119
+
120
+ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<string, string> {
121
+ const oauthToken = options.isOAuth ?? isAnthropicOAuthToken(options.apiKey);
122
+ const extraBetas = options.extraBetas ?? [];
123
+ const stream = options.stream ?? false;
124
+ const betaHeader = buildBetaHeader(claudeCodeBetaDefaults, extraBetas);
125
+ const acceptHeader = stream ? "text/event-stream" : "application/json";
126
+ const modelHeaders = Object.fromEntries(
127
+ Object.entries(options.modelHeaders ?? {}).filter(([key]) => !enforcedHeaderKeys.has(key.toLowerCase())),
128
+ );
129
+
130
+ if (oauthToken) {
131
+ const incomingUserAgent = getHeaderCaseInsensitive(options.modelHeaders, "User-Agent");
132
+ const userAgent = isClaudeCodeClientUserAgent(incomingUserAgent)
133
+ ? incomingUserAgent
134
+ : `claude-cli/${claudeCodeVersion} (external, cli)`;
135
+ return {
136
+ ...modelHeaders,
137
+ ...claudeCodeHeaders,
138
+ Accept: acceptHeader,
139
+ Authorization: `Bearer ${options.apiKey}`,
140
+ ...sharedHeaders,
141
+ "Anthropic-Beta": betaHeader,
142
+ "User-Agent": userAgent,
143
+ };
144
+ } else if (!isAnthropicApiBaseUrl(options.baseUrl)) {
145
+ return {
146
+ ...modelHeaders,
147
+ Accept: acceptHeader,
148
+ Authorization: `Bearer ${options.apiKey}`,
149
+ ...sharedHeaders,
150
+ "Anthropic-Beta": betaHeader,
151
+ };
152
+ } else {
153
+ return {
154
+ ...modelHeaders,
155
+ Accept: acceptHeader,
156
+ ...sharedHeaders,
157
+ "Anthropic-Beta": betaHeader,
158
+ "X-Api-Key": options.apiKey,
159
+ };
160
+ }
161
+ }
162
+
163
+ type AnthropicCacheControl = { type: "ephemeral"; ttl?: "1h" | "5m" };
164
+
165
+ type AnthropicSamplingParams = MessageCreateParamsStreaming & {
166
+ top_p?: number;
167
+ top_k?: number;
168
+ };
169
+ function getCacheControl(
170
+ baseUrl: string,
171
+ cacheRetention?: CacheRetention,
172
+ ): { retention: CacheRetention; cacheControl?: AnthropicCacheControl } {
173
+ const retention = resolveCacheRetention(cacheRetention);
174
+ if (retention === "none") {
175
+ return { retention };
176
+ }
177
+ const ttl = retention === "long" && baseUrl.includes("api.anthropic.com") ? "1h" : undefined;
178
+ return {
179
+ retention,
180
+ cacheControl: { type: "ephemeral", ...(ttl && { ttl }) },
181
+ };
182
+ }
183
+
184
+ // Stealth mode: Mimic Claude Code headers and tool prefixing.
185
+ export const claudeCodeVersion = "2.1.63";
186
+ export const claudeToolPrefix: string = "proxy_";
187
+ export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
188
+
189
+ export function mapStainlessOs(platform: string): "MacOS" | "Windows" | "Linux" | "FreeBSD" | `Other::${string}` {
190
+ switch (platform.toLowerCase()) {
191
+ case "darwin":
192
+ return "MacOS";
193
+ case "windows":
194
+ case "win32":
195
+ return "Windows";
196
+ case "linux":
197
+ return "Linux";
198
+ case "freebsd":
199
+ return "FreeBSD";
200
+ default:
201
+ return `Other::${platform.toLowerCase()}`;
202
+ }
203
+ }
204
+
205
+ export function mapStainlessArch(arch: string): "x64" | "arm64" | "x86" | `other::${string}` {
206
+ switch (arch.toLowerCase()) {
207
+ case "amd64":
208
+ case "x64":
209
+ return "x64";
210
+ case "arm64":
211
+ case "aarch64":
212
+ return "arm64";
213
+ case "386":
214
+ case "x86":
215
+ case "ia32":
216
+ return "x86";
217
+ default:
218
+ return `other::${arch.toLowerCase()}`;
219
+ }
220
+ }
221
+
222
+ export const claudeCodeHeaders = {
223
+ "X-Stainless-Retry-Count": "0",
224
+ "X-Stainless-Runtime-Version": "v24.3.0",
225
+ "X-Stainless-Package-Version": "0.74.0",
226
+ "X-Stainless-Runtime": "node",
227
+ "X-Stainless-Lang": "js",
228
+ "X-Stainless-Arch": mapStainlessArch(process.arch),
229
+ "X-Stainless-Os": mapStainlessOs(process.platform),
230
+ "X-Stainless-Timeout": "600",
231
+ } as const;
232
+
233
+ const enforcedHeaderKeys = new Set(
234
+ [
235
+ ...Object.keys(claudeCodeHeaders),
236
+ "Accept",
237
+ "Accept-Encoding",
238
+ "Connection",
239
+ "Content-Type",
240
+ "Anthropic-Version",
241
+ "Anthropic-Dangerous-Direct-Browser-Access",
242
+ "Anthropic-Beta",
243
+ "User-Agent",
244
+ "X-App",
245
+ "Authorization",
246
+ "X-Api-Key",
247
+ ].map(key => key.toLowerCase()),
248
+ );
249
+
250
+ const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
251
+
252
+ function createClaudeBillingHeader(payload: unknown): string {
253
+ const payloadJson = JSON.stringify(payload) ?? "";
254
+ const cch = nodeCrypto.createHash("sha256").update(payloadJson).digest("hex").slice(0, 5);
255
+ const randomBytes = new Uint8Array(2);
256
+ crypto.getRandomValues(randomBytes);
257
+ const buildHash = Array.from(randomBytes, byte => byte.toString(16).padStart(2, "0"))
258
+ .join("")
259
+ .slice(0, 3);
260
+ return `${CLAUDE_BILLING_HEADER_PREFIX} cc_version=${claudeCodeVersion}.${buildHash}; cc_entrypoint=cli; cch=${cch};`;
261
+ }
262
+
263
+ const CLAUDE_CLOAKING_USER_ID_REGEX =
264
+ /^user_[0-9a-fA-F]{64}_account_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_session_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
265
+
266
+ export function isClaudeCloakingUserId(userId: string): boolean {
267
+ return CLAUDE_CLOAKING_USER_ID_REGEX.test(userId);
268
+ }
269
+
270
+ export function generateClaudeCloakingUserId(): string {
271
+ const userHash = nodeCrypto.randomBytes(32).toString("hex");
272
+ const accountId = nodeCrypto.randomUUID().toLowerCase();
273
+ const sessionId = nodeCrypto.randomUUID().toLowerCase();
274
+ return `user_${userHash}_account_${accountId}_session_${sessionId}`;
275
+ }
276
+
277
+ function resolveAnthropicMetadataUserId(userId: unknown, isOAuthToken: boolean): string | undefined {
278
+ if (typeof userId === "string") {
279
+ if (!isOAuthToken || isClaudeCloakingUserId(userId)) {
280
+ return userId;
281
+ }
282
+ }
283
+
284
+ if (!isOAuthToken) return undefined;
285
+ return generateClaudeCloakingUserId();
286
+ }
287
+ const ANTHROPIC_BUILTIN_TOOL_NAMES = new Set(["web_search", "code_execution", "text_editor", "computer"]);
288
+ export const applyClaudeToolPrefix = (name: string, prefixOverride: string = claudeToolPrefix) => {
289
+ if (!prefixOverride) return name;
290
+ if (ANTHROPIC_BUILTIN_TOOL_NAMES.has(name.toLowerCase())) return name;
291
+ const prefix = prefixOverride.toLowerCase();
292
+ if (name.toLowerCase().startsWith(prefix)) return name;
293
+ return `${prefixOverride}${name}`;
294
+ };
295
+
296
+ export const stripClaudeToolPrefix = (name: string, prefixOverride: string = claudeToolPrefix) => {
297
+ if (!prefixOverride) return name;
298
+ const prefix = prefixOverride.toLowerCase();
299
+ if (!name.toLowerCase().startsWith(prefix)) return name;
300
+ return name.slice(prefixOverride.length);
301
+ };
302
+
303
+ /**
304
+ * Convert content blocks to Anthropic API format
305
+ */
306
+ function convertContentBlocks(content: (TextContent | ImageContent)[]):
307
+ | string
308
+ | Array<
309
+ | { type: "text"; text: string }
310
+ | {
311
+ type: "image";
312
+ source: {
313
+ type: "base64";
314
+ media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
315
+ data: string;
316
+ };
317
+ }
318
+ > {
319
+ // If only text blocks, return as concatenated string for simplicity
320
+ const hasImages = content.some(c => c.type === "image");
321
+ if (!hasImages) {
322
+ return content
323
+ .map(c => (c as TextContent).text)
324
+ .join("\n")
325
+ .toWellFormed();
326
+ }
327
+
328
+ // If we have images, convert to content block array
329
+ const blocks = content.map(block => {
330
+ if (block.type === "text") {
331
+ return {
332
+ type: "text" as const,
333
+ text: block.text.toWellFormed(),
334
+ };
335
+ }
336
+ return {
337
+ type: "image" as const,
338
+ source: {
339
+ type: "base64" as const,
340
+ media_type: block.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
341
+ data: block.data,
342
+ },
343
+ };
344
+ });
345
+
346
+ // If only images (no text), add placeholder text block
347
+ const hasText = blocks.some(b => b.type === "text");
348
+ if (!hasText) {
349
+ blocks.unshift({
350
+ type: "text" as const,
351
+ text: "(see attached image)",
352
+ });
353
+ }
354
+
355
+ return blocks;
356
+ }
357
+
358
+ export type AnthropicEffort = "low" | "medium" | "high" | "max";
359
+
360
+ export interface AnthropicOptions extends StreamOptions {
361
+ /**
362
+ * Enable extended thinking.
363
+ * For Opus 4.6+: uses adaptive thinking (Claude decides when/how much to think).
364
+ * For older models: uses budget-based thinking with thinkingBudgetTokens.
365
+ */
366
+ thinkingEnabled?: boolean;
367
+ /**
368
+ * Token budget for extended thinking (older models only).
369
+ * Ignored for Opus 4.6+ which uses adaptive thinking.
370
+ */
371
+ thinkingBudgetTokens?: number;
372
+ /**
373
+ * Effort level for adaptive thinking (Opus 4.6+ only).
374
+ * Controls how much thinking Claude allocates:
375
+ * - "max": Always thinks with no constraints
376
+ * - "high": Always thinks, deep reasoning (default)
377
+ * - "medium": Moderate thinking, may skip for simple queries
378
+ * - "low": Minimal thinking, skips for simple tasks
379
+ * Ignored for older models.
380
+ */
381
+ effort?: AnthropicEffort;
382
+ /**
383
+ * Optional reasoning level fallback for direct Anthropic provider usage.
384
+ * Converted to adaptive effort when effort is not explicitly provided.
385
+ */
386
+ reasoning?: SimpleStreamOptions["reasoning"];
387
+ interleavedThinking?: boolean;
388
+ toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
389
+ betas?: string[] | string;
390
+ /** Force OAuth bearer auth mode for proxy tokens that don't match Anthropic token prefixes. */
391
+ isOAuth?: boolean;
392
+ /**
393
+ * Pre-built Anthropic client instance. When provided, skips internal client
394
+ * construction entirely. Use this to inject alternative SDK clients such as
395
+ * `AnthropicVertex` that shares the same messaging API.
396
+ */
397
+ client?: Anthropic;
398
+ }
399
+
400
+ export type AnthropicClientOptionsArgs = {
401
+ model: Model<"anthropic-messages">;
402
+ apiKey: string;
403
+ extraBetas?: string[];
404
+ stream?: boolean;
405
+ interleavedThinking?: boolean;
406
+ headers?: Record<string, string>;
407
+ dynamicHeaders?: Record<string, string>;
408
+ isOAuth?: boolean;
409
+ };
410
+
411
+ export type AnthropicClientOptionsResult = {
412
+ isOAuthToken: boolean;
413
+ apiKey: string | null;
414
+ authToken?: string;
415
+ baseURL?: string;
416
+ maxRetries: number;
417
+ dangerouslyAllowBrowser: boolean;
418
+ defaultHeaders: Record<string, string>;
419
+ logLevel: AnthropicSdkClientOptions["logLevel"];
420
+ fetchOptions?: AnthropicSdkClientOptions["fetchOptions"];
421
+ };
422
+
423
+ const CLAUDE_CODE_TLS_CIPHERS = tls.DEFAULT_CIPHERS;
424
+
425
+ type FoundryTlsOptions = {
426
+ ca?: string | string[];
427
+ cert?: string;
428
+ key?: string;
429
+ };
430
+
431
+ function isFoundryEnabled(): boolean {
432
+ const value = $env.CLAUDE_CODE_USE_FOUNDRY;
433
+ if (!value) return false;
434
+ const normalized = value.trim().toLowerCase();
435
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
436
+ }
437
+
438
+ function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: string): string | undefined {
439
+ if (model.provider === "github-copilot") {
440
+ return normalizeAnthropicBaseUrl(resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl);
441
+ }
442
+ if (model.provider === "anthropic" && isFoundryEnabled()) {
443
+ const foundryBaseUrl = normalizeAnthropicBaseUrl($env.FOUNDRY_BASE_URL);
444
+ if (foundryBaseUrl) {
445
+ return foundryBaseUrl;
446
+ }
447
+ }
448
+ if (model.provider === "anthropic") {
449
+ return normalizeAnthropicBaseUrl(model.baseUrl) ?? "https://api.anthropic.com";
450
+ }
451
+ return normalizeAnthropicBaseUrl(model.baseUrl);
452
+ }
453
+
454
+ function parseAnthropicCustomHeaders(rawHeaders: string | undefined): Record<string, string> | undefined {
455
+ const source = rawHeaders?.trim();
456
+ if (!source) return undefined;
457
+
458
+ const parsed: Record<string, string> = {};
459
+ for (const token of source.split(/\r?\n|,/)) {
460
+ const entry = token.trim();
461
+ if (!entry) continue;
462
+ const separatorIndex = entry.indexOf(":");
463
+ if (separatorIndex <= 0) continue;
464
+ const key = entry.slice(0, separatorIndex).trim();
465
+ const value = entry.slice(separatorIndex + 1).trim();
466
+ if (!key || !value) continue;
467
+ parsed[key] = value;
468
+ }
469
+
470
+ return Object.keys(parsed).length > 0 ? parsed : undefined;
471
+ }
472
+
473
+ function resolveAnthropicCustomHeaders(model: Model<"anthropic-messages">): Record<string, string> | undefined {
474
+ if (model.provider !== "anthropic") return undefined;
475
+ if (!isFoundryEnabled()) return undefined;
476
+ return parseAnthropicCustomHeaders($env.ANTHROPIC_CUSTOM_HEADERS);
477
+ }
478
+
479
+ function looksLikeFilePath(value: string): boolean {
480
+ return value.includes("/") || value.includes("\\") || /\.(pem|crt|cer|key)$/i.test(value);
481
+ }
482
+
483
+ function resolvePemValue(value: string | undefined, name: string): string | undefined {
484
+ const trimmed = value?.trim();
485
+ if (!trimmed) return undefined;
486
+
487
+ const inline = trimmed.replace(/\\n/g, "\n");
488
+ if (inline.includes("-----BEGIN")) {
489
+ return inline;
490
+ }
491
+
492
+ if (looksLikeFilePath(trimmed)) {
493
+ try {
494
+ return fs.readFileSync(trimmed, "utf8");
495
+ } catch (error) {
496
+ if (isEnoent(error)) {
497
+ throw new Error(`${name} path does not exist: ${trimmed}`);
498
+ }
499
+ throw error;
500
+ }
501
+ }
502
+
503
+ return inline;
504
+ }
505
+
506
+ function resolveFoundryTlsOptions(model: Model<"anthropic-messages">): FoundryTlsOptions | undefined {
507
+ if (model.provider !== "anthropic") return undefined;
508
+ if (!isFoundryEnabled()) return undefined;
509
+
510
+ const ca = resolvePemValue($env.NODE_EXTRA_CA_CERTS, "NODE_EXTRA_CA_CERTS");
511
+ const cert = resolvePemValue($env.CLAUDE_CODE_CLIENT_CERT, "CLAUDE_CODE_CLIENT_CERT");
512
+ const key = resolvePemValue($env.CLAUDE_CODE_CLIENT_KEY, "CLAUDE_CODE_CLIENT_KEY");
513
+
514
+ if ((cert && !key) || (!cert && key)) {
515
+ throw new Error("Both CLAUDE_CODE_CLIENT_CERT and CLAUDE_CODE_CLIENT_KEY must be set for mTLS.");
516
+ }
517
+
518
+ const options: FoundryTlsOptions = {};
519
+ if (ca) options.ca = [...tls.rootCertificates, ca];
520
+ if (cert) options.cert = cert;
521
+ if (key) options.key = key;
522
+ return Object.keys(options).length > 0 ? options : undefined;
523
+ }
524
+
525
+ function buildClaudeCodeTlsFetchOptions(
526
+ model: Model<"anthropic-messages">,
527
+ baseUrl: string | undefined,
528
+ ): AnthropicSdkClientOptions["fetchOptions"] | undefined {
529
+ if (model.provider !== "anthropic") return undefined;
530
+ if (!baseUrl) return undefined;
531
+
532
+ let serverName: string;
533
+ try {
534
+ serverName = new URL(baseUrl).hostname;
535
+ } catch {
536
+ return undefined;
537
+ }
538
+
539
+ if (!serverName) return undefined;
540
+
541
+ const foundryTlsOptions = resolveFoundryTlsOptions(model);
542
+
543
+ return {
544
+ tls: {
545
+ rejectUnauthorized: true,
546
+ serverName,
547
+ ...(CLAUDE_CODE_TLS_CIPHERS ? { ciphers: CLAUDE_CODE_TLS_CIPHERS } : {}),
548
+ ...(foundryTlsOptions ?? {}),
549
+ },
550
+ };
551
+ }
552
+ function mergeHeaders(...headerSources: (Record<string, string> | undefined)[]): Record<string, string> {
553
+ const merged: Record<string, string> = {};
554
+ for (const headers of headerSources) {
555
+ if (headers) {
556
+ Object.assign(merged, headers);
557
+ }
558
+ }
559
+ return merged;
560
+ }
561
+
562
+ // The Anthropic SDK logs malformed SSE frames directly before rethrowing them.
563
+ // We surface the resulting provider error ourselves, so keep the SDK quiet.
564
+ const ANTHROPIC_SDK_LOG_LEVEL = "off" as const;
565
+
566
+ const PROVIDER_MAX_RETRIES = 3;
567
+ const PROVIDER_BASE_DELAY_MS = 2000;
568
+
569
+ /**
570
+ * Check if an error from the Anthropic SDK is a rate-limit/transient error that
571
+ * should be retried before any content has been emitted.
572
+ *
573
+ * Includes malformed JSON stream-envelope parse errors seen from some
574
+ * Anthropic-compatible proxy endpoints.
575
+ */
576
+ /** Transient stream corruption errors where the response was truncated mid-JSON. */
577
+ function isTransientStreamParseError(error: unknown): boolean {
578
+ if (!(error instanceof Error)) return false;
579
+ return /json parse error|unterminated string|unexpected end of json input/i.test(error.message);
580
+ }
581
+
582
+ const ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX = "Anthropic stream envelope error:";
583
+
584
+ function createAnthropicStreamEnvelopeError(message: string): Error {
585
+ return new Error(`${ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX} ${message}`);
586
+ }
587
+
588
+ const ANTHROPIC_PRE_MESSAGE_START_EVENT_TYPES = new Set([
589
+ "content_block_start",
590
+ "content_block_delta",
591
+ "content_block_stop",
592
+ "message_delta",
593
+ "message_stop",
594
+ "message_start",
595
+ ]);
596
+
597
+ function shouldIgnoreAnthropicPreambleEvent(eventType: unknown): boolean {
598
+ if (typeof eventType !== "string") return false;
599
+ if (eventType === "ping") return true;
600
+ return !ANTHROPIC_PRE_MESSAGE_START_EVENT_TYPES.has(eventType);
601
+ }
602
+
603
+ function isTransientStreamEnvelopeError(error: unknown): boolean {
604
+ if (!(error instanceof Error)) return false;
605
+ return (
606
+ error.message.includes(ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX) ||
607
+ /stream event order|before message_start|before terminal stop signal/i.test(error.message)
608
+ );
609
+ }
610
+
611
+ function isProviderRetryableStreamEnvelopeError(error: unknown): boolean {
612
+ if (!(error instanceof Error)) return false;
613
+ return /stream event order|before message_start/i.test(error.message);
614
+ }
615
+
616
+ export function isProviderRetryableError(error: unknown): boolean {
617
+ if (!(error instanceof Error)) return false;
618
+ const msg = error.message.toLowerCase();
619
+ return (
620
+ /rate.?limit|too many requests|overloaded|service.?unavailable|internal_error|stream error.*received from peer|1302|timed?\s*out while waiting for the first event|timeout waiting for first/i.test(
621
+ msg,
622
+ ) ||
623
+ isTransientStreamParseError(error) ||
624
+ isProviderRetryableStreamEnvelopeError(error)
625
+ );
626
+ }
627
+
628
+ function createEmptyUsage(premiumRequests?: number): Usage {
629
+ return {
630
+ input: 0,
631
+ output: 0,
632
+ cacheRead: 0,
633
+ cacheWrite: 0,
634
+ totalTokens: 0,
635
+ ...(premiumRequests === undefined ? {} : { premiumRequests }),
636
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
637
+ };
638
+ }
639
+
640
+ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
641
+ model: Model<"anthropic-messages">,
642
+ context: Context,
643
+ options?: AnthropicOptions,
644
+ ): AssistantMessageEventStream => {
645
+ const stream = new AssistantMessageEventStream();
646
+
647
+ (async () => {
648
+ const startTime = Date.now();
649
+ let firstTokenTime: number | undefined;
650
+
651
+ const copilotDynamicHeaders =
652
+ model.provider === "github-copilot"
653
+ ? buildCopilotDynamicHeaders({
654
+ messages: context.messages,
655
+ hasImages: hasCopilotVisionInput(context.messages),
656
+ premiumMultiplier: model.premiumMultiplier,
657
+ headers: { ...(model.headers ?? {}), ...(options?.headers ?? {}) },
658
+ initiatorOverride: options?.initiatorOverride,
659
+ })
660
+ : undefined;
661
+ const output: AssistantMessage = {
662
+ role: "assistant",
663
+ content: [],
664
+ api: model.api as Api,
665
+ provider: model.provider,
666
+ model: model.id,
667
+ usage: createEmptyUsage(copilotDynamicHeaders?.premiumRequests),
668
+ stopReason: "stop",
669
+ timestamp: Date.now(),
670
+ };
671
+ let rawRequestDump: RawHttpRequestDump | undefined;
672
+ let activeAbortTracker = createAbortSourceTracker(options?.signal);
673
+
674
+ try {
675
+ let client: Anthropic;
676
+ let isOAuthToken: boolean;
677
+
678
+ if (options?.client) {
679
+ client = options.client;
680
+ isOAuthToken = false;
681
+ } else {
682
+ const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
683
+
684
+ const created = createClient(model, {
685
+ model,
686
+ apiKey,
687
+ extraBetas: normalizeExtraBetas(options?.betas),
688
+ stream: true,
689
+ interleavedThinking: options?.interleavedThinking ?? true,
690
+ headers: options?.headers,
691
+ dynamicHeaders: copilotDynamicHeaders?.headers,
692
+ isOAuth: options?.isOAuth,
693
+ });
694
+ client = created.client;
695
+ isOAuthToken = created.isOAuthToken;
696
+ }
697
+ const baseUrl =
698
+ resolveAnthropicBaseUrl(model, options?.apiKey ?? getEnvApiKey(model.provider) ?? "") ??
699
+ "https://api.anthropic.com";
700
+ let params = buildParams(model, baseUrl, context, isOAuthToken, options);
701
+ const replacementPayload = await options?.onPayload?.(params, model);
702
+ if (replacementPayload !== undefined) {
703
+ params = replacementPayload as typeof params;
704
+ }
705
+ rawRequestDump = {
706
+ provider: model.provider,
707
+ api: output.api,
708
+ model: model.id,
709
+ method: "POST",
710
+ url: `${baseUrl}/v1/messages`,
711
+ body: params,
712
+ };
713
+
714
+ type Block = (
715
+ | ThinkingContent
716
+ | RedactedThinkingContent
717
+ | TextContent
718
+ | (ToolCall & { partialJson: string })
719
+ ) & { index: number };
720
+ const blocks = output.content as Block[];
721
+ const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs();
722
+ stream.push({ type: "start", partial: output });
723
+ // Retry loop for transient errors from the stream.
724
+ // Provider-level transport/rate-limit failures: only before any streamed content starts.
725
+ // Malformed envelopes/JSON: only before replay-unsafe text/tool events are visible on this stream.
726
+ let providerRetryAttempt = 0;
727
+ while (true) {
728
+ activeAbortTracker = createAbortSourceTracker(options?.signal);
729
+ const firstEventTimeoutAbortError = new Error(
730
+ "Anthropic stream timed out while waiting for the first event",
731
+ );
732
+ const { requestSignal } = activeAbortTracker;
733
+ const anthropicRequest = client.messages.create({ ...params, stream: true }, { signal: requestSignal });
734
+ let streamedReplayUnsafeContent = false;
735
+
736
+ try {
737
+ const { data: anthropicStream } = await anthropicRequest.withResponse();
738
+ const firstEventWatchdog = createFirstEventWatchdog(firstEventTimeoutMs, () =>
739
+ activeAbortTracker.abortLocally(firstEventTimeoutAbortError),
740
+ );
741
+ let sawEvent = false;
742
+ let sawMessageStart = false;
743
+ let sawTerminalEnvelope = false;
744
+
745
+ for await (const event of markFirstStreamEvent(anthropicStream, firstEventWatchdog)) {
746
+ sawEvent = true;
747
+
748
+ if (event.type === "message_start") {
749
+ if (sawMessageStart) {
750
+ continue;
751
+ }
752
+ sawMessageStart = true;
753
+ output.responseId = event.message.id;
754
+ output.usage.input = event.message.usage.input_tokens || 0;
755
+ output.usage.output = event.message.usage.output_tokens || 0;
756
+ output.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0;
757
+ output.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0;
758
+ output.usage.totalTokens =
759
+ output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
760
+ calculateCost(model, output.usage);
761
+ continue;
762
+ }
763
+
764
+ if (!sawMessageStart) {
765
+ if (shouldIgnoreAnthropicPreambleEvent(event.type)) {
766
+ continue;
767
+ }
768
+ throw createAnthropicStreamEnvelopeError(`received ${event.type} before message_start`);
769
+ }
770
+
771
+ if (event.type === "content_block_start") {
772
+ if (!firstTokenTime) firstTokenTime = Date.now();
773
+ if (event.content_block.type === "text") {
774
+ streamedReplayUnsafeContent = true;
775
+ const block: Block = {
776
+ type: "text",
777
+ text: "",
778
+ index: event.index,
779
+ };
780
+ output.content.push(block);
781
+ stream.push({
782
+ type: "text_start",
783
+ contentIndex: output.content.length - 1,
784
+ partial: output,
785
+ });
786
+ } else if (event.content_block.type === "thinking") {
787
+ const block: Block = {
788
+ type: "thinking",
789
+ thinking: "",
790
+ thinkingSignature: "",
791
+ index: event.index,
792
+ };
793
+ output.content.push(block);
794
+ stream.push({
795
+ type: "thinking_start",
796
+ contentIndex: output.content.length - 1,
797
+ partial: output,
798
+ });
799
+ } else if (event.content_block.type === "redacted_thinking") {
800
+ const block: Block = {
801
+ type: "redactedThinking",
802
+ data: event.content_block.data,
803
+ index: event.index,
804
+ };
805
+ output.content.push(block);
806
+ } else if (event.content_block.type === "tool_use") {
807
+ streamedReplayUnsafeContent = true;
808
+ const block: Block = {
809
+ type: "toolCall",
810
+ id: event.content_block.id,
811
+ name: isOAuthToken
812
+ ? stripClaudeToolPrefix(event.content_block.name)
813
+ : event.content_block.name,
814
+ arguments: (event.content_block.input as Record<string, unknown>) ?? {},
815
+ partialJson: "",
816
+ index: event.index,
817
+ };
818
+ output.content.push(block);
819
+ stream.push({
820
+ type: "toolcall_start",
821
+ contentIndex: output.content.length - 1,
822
+ partial: output,
823
+ });
824
+ }
825
+ } else if (event.type === "content_block_delta") {
826
+ if (event.delta.type === "text_delta") {
827
+ const index = blocks.findIndex(b => b.index === event.index);
828
+ const block = blocks[index];
829
+ if (block && block.type === "text") {
830
+ block.text += event.delta.text;
831
+ stream.push({
832
+ type: "text_delta",
833
+ contentIndex: index,
834
+ delta: event.delta.text,
835
+ partial: output,
836
+ });
837
+ }
838
+ } else if (event.delta.type === "thinking_delta") {
839
+ const index = blocks.findIndex(b => b.index === event.index);
840
+ const block = blocks[index];
841
+ if (block && block.type === "thinking") {
842
+ block.thinking += event.delta.thinking;
843
+ stream.push({
844
+ type: "thinking_delta",
845
+ contentIndex: index,
846
+ delta: event.delta.thinking,
847
+ partial: output,
848
+ });
849
+ }
850
+ } else if (event.delta.type === "input_json_delta") {
851
+ const index = blocks.findIndex(b => b.index === event.index);
852
+ const block = blocks[index];
853
+ if (block && block.type === "toolCall") {
854
+ block.partialJson += event.delta.partial_json;
855
+ block.arguments = parseStreamingJson(block.partialJson);
856
+ stream.push({
857
+ type: "toolcall_delta",
858
+ contentIndex: index,
859
+ delta: event.delta.partial_json,
860
+ partial: output,
861
+ });
862
+ }
863
+ } else if (event.delta.type === "signature_delta") {
864
+ const index = blocks.findIndex(b => b.index === event.index);
865
+ const block = blocks[index];
866
+ if (block && block.type === "thinking") {
867
+ block.thinkingSignature = block.thinkingSignature || "";
868
+ block.thinkingSignature += event.delta.signature;
869
+ }
870
+ }
871
+ } else if (event.type === "content_block_stop") {
872
+ const index = blocks.findIndex(b => b.index === event.index);
873
+ const block = blocks[index];
874
+ if (block) {
875
+ delete (block as { index?: number }).index;
876
+ if (block.type === "text") {
877
+ stream.push({
878
+ type: "text_end",
879
+ contentIndex: index,
880
+ content: block.text,
881
+ partial: output,
882
+ });
883
+ } else if (block.type === "thinking") {
884
+ stream.push({
885
+ type: "thinking_end",
886
+ contentIndex: index,
887
+ content: block.thinking,
888
+ partial: output,
889
+ });
890
+ } else if (block.type === "toolCall") {
891
+ block.arguments = parseStreamingJson(block.partialJson);
892
+ delete (block as { partialJson?: string }).partialJson;
893
+ stream.push({
894
+ type: "toolcall_end",
895
+ contentIndex: index,
896
+ toolCall: block,
897
+ partial: output,
898
+ });
899
+ }
900
+ }
901
+ } else if (event.type === "message_delta") {
902
+ if (event.delta.stop_reason) {
903
+ output.stopReason = mapStopReason(event.delta.stop_reason);
904
+ sawTerminalEnvelope = true;
905
+ }
906
+ if (event.usage.input_tokens != null) {
907
+ output.usage.input = event.usage.input_tokens;
908
+ }
909
+ if (event.usage.output_tokens != null) {
910
+ output.usage.output = event.usage.output_tokens;
911
+ }
912
+ if (event.usage.cache_read_input_tokens != null) {
913
+ output.usage.cacheRead = event.usage.cache_read_input_tokens;
914
+ }
915
+ if (event.usage.cache_creation_input_tokens != null) {
916
+ output.usage.cacheWrite = event.usage.cache_creation_input_tokens;
917
+ }
918
+ output.usage.totalTokens =
919
+ output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
920
+ calculateCost(model, output.usage);
921
+ } else if (event.type === "message_stop") {
922
+ sawTerminalEnvelope = true;
923
+ }
924
+ }
925
+
926
+ const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
927
+ if (firstEventTimeoutError) {
928
+ throw firstEventTimeoutError;
929
+ }
930
+ if (activeAbortTracker.wasCallerAbort()) {
931
+ throw new Error("Request was aborted");
932
+ }
933
+ if (!sawEvent || !sawMessageStart) {
934
+ throw createAnthropicStreamEnvelopeError("stream ended before message_start");
935
+ }
936
+ if (!sawTerminalEnvelope) {
937
+ throw createAnthropicStreamEnvelopeError("stream ended before terminal stop signal");
938
+ }
939
+
940
+ if (output.stopReason === "aborted" || output.stopReason === "error") {
941
+ throw new Error("An unknown error occurred");
942
+ }
943
+ break;
944
+ } catch (streamError) {
945
+ const streamFailure = activeAbortTracker.getLocalAbortReason() ?? streamError;
946
+ const isTransientEnvelopeFailure =
947
+ isTransientStreamParseError(streamFailure) || isTransientStreamEnvelopeError(streamFailure);
948
+ const canRetryTransientEnvelopeFailure = isTransientEnvelopeFailure && !streamedReplayUnsafeContent;
949
+ const canRetryProviderFailure = firstTokenTime === undefined && isProviderRetryableError(streamFailure);
950
+ if (
951
+ activeAbortTracker.wasCallerAbort() ||
952
+ providerRetryAttempt >= PROVIDER_MAX_RETRIES ||
953
+ (!canRetryTransientEnvelopeFailure && !canRetryProviderFailure)
954
+ ) {
955
+ throw streamFailure;
956
+ }
957
+ providerRetryAttempt++;
958
+ const delayMs = PROVIDER_BASE_DELAY_MS * 2 ** (providerRetryAttempt - 1);
959
+ await abortableSleep(delayMs, options?.signal);
960
+ output.content.length = 0;
961
+ output.responseId = undefined;
962
+ output.errorMessage = undefined;
963
+ output.providerPayload = undefined;
964
+ output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
965
+ output.stopReason = "stop";
966
+ firstTokenTime = undefined;
967
+ }
968
+ }
969
+
970
+ output.duration = Date.now() - startTime;
971
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
972
+ stream.push({ type: "done", reason: output.stopReason, message: output });
973
+ stream.end();
974
+ } catch (error) {
975
+ for (const block of output.content) {
976
+ delete (block as { index?: number }).index;
977
+ delete (block as { partialJson?: string }).partialJson;
978
+ }
979
+ const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
980
+ output.stopReason = activeAbortTracker.wasCallerAbort() ? "aborted" : "error";
981
+ output.errorMessage = firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
982
+ output.errorMessage = rewriteCopilotAuthError(output.errorMessage, error, model.provider);
983
+ output.duration = Date.now() - startTime;
984
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
985
+ stream.push({ type: "error", reason: output.stopReason, error: output });
986
+ stream.end();
987
+ }
988
+ })();
989
+
990
+ return stream;
991
+ };
992
+
993
+ export type AnthropicSystemBlock = {
994
+ type: "text";
995
+ text: string;
996
+ cache_control?: AnthropicCacheControl;
997
+ };
998
+ type SystemBlockOptions = {
999
+ includeClaudeCodeInstruction?: boolean;
1000
+ extraInstructions?: string[];
1001
+ billingPayload?: unknown;
1002
+ cacheControl?: AnthropicCacheControl;
1003
+ };
1004
+
1005
+ export function buildAnthropicSystemBlocks(
1006
+ systemPrompt: string | undefined,
1007
+ options: SystemBlockOptions = {},
1008
+ ): AnthropicSystemBlock[] | undefined {
1009
+ const { includeClaudeCodeInstruction = false, extraInstructions = [], billingPayload, cacheControl } = options;
1010
+ const blocks: AnthropicSystemBlock[] = [];
1011
+ const sanitizedPrompt = systemPrompt ? systemPrompt.toWellFormed() : "";
1012
+ const trimmedInstructions = extraInstructions.map(instruction => instruction.trim()).filter(Boolean);
1013
+ const hasBillingHeader = sanitizedPrompt.includes(CLAUDE_BILLING_HEADER_PREFIX);
1014
+
1015
+ if (includeClaudeCodeInstruction && !hasBillingHeader) {
1016
+ const payloadSeed = billingPayload ?? {
1017
+ system: sanitizedPrompt,
1018
+ extraInstructions: trimmedInstructions,
1019
+ };
1020
+ blocks.push(
1021
+ { type: "text", text: createClaudeBillingHeader(payloadSeed) },
1022
+ {
1023
+ type: "text",
1024
+ text: claudeCodeSystemInstruction,
1025
+ },
1026
+ );
1027
+ }
1028
+
1029
+ for (const instruction of trimmedInstructions) {
1030
+ blocks.push({
1031
+ type: "text",
1032
+ text: instruction,
1033
+ ...(cacheControl ? { cache_control: cacheControl } : {}),
1034
+ });
1035
+ }
1036
+
1037
+ if (systemPrompt) {
1038
+ blocks.push({
1039
+ type: "text",
1040
+ text: sanitizedPrompt,
1041
+ ...(cacheControl ? { cache_control: cacheControl } : {}),
1042
+ });
1043
+ }
1044
+
1045
+ return blocks.length > 0 ? blocks : undefined;
1046
+ }
1047
+
1048
+ export function normalizeExtraBetas(betas?: string[] | string): string[] {
1049
+ if (!betas) return [];
1050
+ const raw = Array.isArray(betas) ? betas : betas.split(",");
1051
+ return raw.map(beta => beta.trim()).filter(beta => beta.length > 0);
1052
+ }
1053
+
1054
+ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): AnthropicClientOptionsResult {
1055
+ const {
1056
+ model,
1057
+ apiKey,
1058
+ extraBetas = [],
1059
+ stream = true,
1060
+ interleavedThinking = true,
1061
+ headers,
1062
+ dynamicHeaders,
1063
+ isOAuth,
1064
+ } = args;
1065
+ const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey);
1066
+ const baseUrl = resolveAnthropicBaseUrl(model, apiKey);
1067
+ const foundryCustomHeaders = resolveAnthropicCustomHeaders(model);
1068
+ const tlsFetchOptions = buildClaudeCodeTlsFetchOptions(model, baseUrl);
1069
+ if (model.provider === "github-copilot") {
1070
+ const copilotApiKey = parseGitHubCopilotApiKey(apiKey).accessToken;
1071
+ const betaFeatures = [...extraBetas];
1072
+ if (interleavedThinking) {
1073
+ betaFeatures.push("interleaved-thinking-2025-05-14");
1074
+ }
1075
+ const defaultHeaders = mergeHeaders(
1076
+ {
1077
+ Accept: stream ? "text/event-stream" : "application/json",
1078
+ "Anthropic-Dangerous-Direct-Browser-Access": "true",
1079
+ Authorization: `Bearer ${copilotApiKey}`,
1080
+ ...(betaFeatures.length > 0 ? { "anthropic-beta": buildBetaHeader([], betaFeatures) } : {}),
1081
+ },
1082
+ model.headers,
1083
+ dynamicHeaders,
1084
+ headers,
1085
+ );
1086
+
1087
+ return {
1088
+ isOAuthToken: false,
1089
+ apiKey: null,
1090
+ authToken: copilotApiKey,
1091
+ baseURL: baseUrl,
1092
+ maxRetries: 5,
1093
+ dangerouslyAllowBrowser: true,
1094
+ defaultHeaders,
1095
+ logLevel: ANTHROPIC_SDK_LOG_LEVEL,
1096
+ ...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
1097
+ };
1098
+ }
1099
+
1100
+ const betaFeatures = [...extraBetas];
1101
+ if (interleavedThinking) {
1102
+ betaFeatures.push("interleaved-thinking-2025-05-14");
1103
+ }
1104
+
1105
+ const defaultHeaders = buildAnthropicHeaders({
1106
+ apiKey,
1107
+ baseUrl,
1108
+ isOAuth: oauthToken,
1109
+ extraBetas: betaFeatures,
1110
+ stream,
1111
+ modelHeaders: mergeHeaders(model.headers, foundryCustomHeaders, headers, dynamicHeaders),
1112
+ });
1113
+
1114
+ return {
1115
+ isOAuthToken: oauthToken,
1116
+ apiKey: oauthToken ? null : apiKey,
1117
+ authToken: oauthToken ? apiKey : undefined,
1118
+ baseURL: baseUrl,
1119
+ maxRetries: 5,
1120
+ dangerouslyAllowBrowser: true,
1121
+ defaultHeaders,
1122
+ logLevel: ANTHROPIC_SDK_LOG_LEVEL,
1123
+ ...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
1124
+ };
1125
+ }
1126
+
1127
+ function createClient(
1128
+ model: Model<"anthropic-messages">,
1129
+ args: AnthropicClientOptionsArgs,
1130
+ ): { client: Anthropic; isOAuthToken: boolean } {
1131
+ const { isOAuthToken: oauthToken, ...clientOptions } = buildAnthropicClientOptions({ ...args, model });
1132
+ const client = new Anthropic(clientOptions);
1133
+ return { client, isOAuthToken: oauthToken };
1134
+ }
1135
+
1136
+ function disableThinkingIfToolChoiceForced(params: MessageCreateParamsStreaming): void {
1137
+ const toolChoice = params.tool_choice;
1138
+ if (!toolChoice) return;
1139
+ if (toolChoice.type === "any" || toolChoice.type === "tool") {
1140
+ delete params.thinking;
1141
+ delete params.output_config;
1142
+ }
1143
+ }
1144
+
1145
+ function ensureMaxTokensForThinking(params: MessageCreateParamsStreaming, model: Model<"anthropic-messages">): void {
1146
+ const thinking = params.thinking;
1147
+ if (thinking?.type !== "enabled") return;
1148
+
1149
+ const budgetTokens = thinking.budget_tokens ?? 0;
1150
+ if (budgetTokens <= 0) return;
1151
+
1152
+ const maxTokens = params.max_tokens ?? 0;
1153
+ const requiredMaxTokens = budgetTokens + OUTPUT_FALLBACK_BUFFER;
1154
+ if (maxTokens < requiredMaxTokens) {
1155
+ params.max_tokens = Math.min(requiredMaxTokens, model.maxTokens);
1156
+ }
1157
+ }
1158
+
1159
+ type CacheControlBlock = {
1160
+ cache_control?: AnthropicCacheControl | null;
1161
+ };
1162
+
1163
+ function applyCacheControlToLastBlock<T extends CacheControlBlock>(
1164
+ blocks: T[],
1165
+ cacheControl: AnthropicCacheControl,
1166
+ ): void {
1167
+ if (blocks.length === 0) return;
1168
+ const lastIndex = blocks.length - 1;
1169
+ blocks[lastIndex] = { ...blocks[lastIndex], cache_control: cacheControl };
1170
+ }
1171
+
1172
+ function applyCacheControlToLastTextBlock(
1173
+ blocks: Array<ContentBlockParam & CacheControlBlock>,
1174
+ cacheControl: AnthropicCacheControl,
1175
+ ): void {
1176
+ if (blocks.length === 0) return;
1177
+ for (let i = blocks.length - 1; i >= 0; i--) {
1178
+ if (blocks[i].type === "text") {
1179
+ blocks[i] = { ...blocks[i], cache_control: cacheControl };
1180
+ return;
1181
+ }
1182
+ }
1183
+ applyCacheControlToLastBlock(blocks, cacheControl);
1184
+ }
1185
+
1186
+ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?: AnthropicCacheControl): void {
1187
+ if (!cacheControl) return;
1188
+
1189
+ // Skip if cache_control breakpoints were already placed externally on messages.
1190
+ for (const message of params.messages) {
1191
+ if (Array.isArray(message.content)) {
1192
+ if ((message.content as Array<ContentBlockParam & CacheControlBlock>).some(b => b.cache_control != null))
1193
+ return;
1194
+ }
1195
+ }
1196
+
1197
+ const MAX_CACHE_BREAKPOINTS = 4;
1198
+ let cacheBreakpointsUsed = 0;
1199
+
1200
+ if (params.tools && params.tools.length > 0) {
1201
+ applyCacheControlToLastBlock(params.tools as Array<CacheControlBlock>, cacheControl);
1202
+ cacheBreakpointsUsed++;
1203
+ }
1204
+
1205
+ if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;
1206
+
1207
+ if (params.system && Array.isArray(params.system) && params.system.length > 0) {
1208
+ applyCacheControlToLastBlock(params.system, cacheControl);
1209
+ cacheBreakpointsUsed++;
1210
+ }
1211
+
1212
+ if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;
1213
+
1214
+ const userIndexes = params.messages
1215
+ .map((message, index) => (message.role === "user" ? index : -1))
1216
+ .filter(index => index >= 0);
1217
+
1218
+ if (userIndexes.length >= 2) {
1219
+ const penultimateUserIndex = userIndexes[userIndexes.length - 2];
1220
+ const penultimateUser = params.messages[penultimateUserIndex];
1221
+ if (penultimateUser) {
1222
+ if (typeof penultimateUser.content === "string") {
1223
+ const contentBlock: ContentBlockParam & CacheControlBlock = {
1224
+ type: "text",
1225
+ text: penultimateUser.content,
1226
+ cache_control: cacheControl,
1227
+ };
1228
+ penultimateUser.content = [contentBlock];
1229
+ cacheBreakpointsUsed++;
1230
+ } else if (Array.isArray(penultimateUser.content) && penultimateUser.content.length > 0) {
1231
+ applyCacheControlToLastTextBlock(
1232
+ penultimateUser.content as Array<ContentBlockParam & CacheControlBlock>,
1233
+ cacheControl,
1234
+ );
1235
+ cacheBreakpointsUsed++;
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;
1241
+
1242
+ if (userIndexes.length >= 1) {
1243
+ const lastUserIndex = userIndexes[userIndexes.length - 1];
1244
+ const lastUser = params.messages[lastUserIndex];
1245
+ if (lastUser) {
1246
+ if (typeof lastUser.content === "string") {
1247
+ const contentBlock: ContentBlockParam & CacheControlBlock = {
1248
+ type: "text",
1249
+ text: lastUser.content,
1250
+ cache_control: cacheControl,
1251
+ };
1252
+ lastUser.content = [contentBlock];
1253
+ } else if (Array.isArray(lastUser.content) && lastUser.content.length > 0) {
1254
+ applyCacheControlToLastTextBlock(
1255
+ lastUser.content as Array<ContentBlockParam & CacheControlBlock>,
1256
+ cacheControl,
1257
+ );
1258
+ }
1259
+ }
1260
+ }
1261
+ }
1262
+
1263
+ function normalizeCacheControlBlockTtl(block: CacheControlBlock, seenFiveMinute: { value: boolean }): void {
1264
+ const cacheControl = block.cache_control;
1265
+ if (!cacheControl) return;
1266
+ if (cacheControl.ttl !== "1h") {
1267
+ seenFiveMinute.value = true;
1268
+ return;
1269
+ }
1270
+ if (seenFiveMinute.value) {
1271
+ delete cacheControl.ttl;
1272
+ }
1273
+ }
1274
+
1275
+ function normalizeCacheControlTtlOrdering(params: MessageCreateParamsStreaming): void {
1276
+ const seenFiveMinute = { value: false };
1277
+ if (params.tools) {
1278
+ for (const tool of params.tools as Array<Anthropic.Messages.Tool & CacheControlBlock>) {
1279
+ normalizeCacheControlBlockTtl(tool, seenFiveMinute);
1280
+ }
1281
+ }
1282
+ if (params.system && Array.isArray(params.system)) {
1283
+ for (const block of params.system as Array<AnthropicSystemBlock & CacheControlBlock>) {
1284
+ normalizeCacheControlBlockTtl(block, seenFiveMinute);
1285
+ }
1286
+ }
1287
+ for (const message of params.messages) {
1288
+ if (!Array.isArray(message.content)) continue;
1289
+ for (const block of message.content as Array<ContentBlockParam & CacheControlBlock>) {
1290
+ normalizeCacheControlBlockTtl(block, seenFiveMinute);
1291
+ }
1292
+ }
1293
+ }
1294
+
1295
+ function findLastCacheControlIndex<T extends CacheControlBlock>(blocks: T[]): number {
1296
+ for (let index = blocks.length - 1; index >= 0; index--) {
1297
+ if (blocks[index]?.cache_control != null) return index;
1298
+ }
1299
+ return -1;
1300
+ }
1301
+
1302
+ function stripCacheControlExceptIndex<T extends CacheControlBlock>(
1303
+ blocks: T[],
1304
+ preserveIndex: number,
1305
+ excessCounter: { value: number },
1306
+ ): void {
1307
+ for (let index = 0; index < blocks.length && excessCounter.value > 0; index++) {
1308
+ if (index === preserveIndex) continue;
1309
+ if (!blocks[index]?.cache_control) continue;
1310
+ delete blocks[index].cache_control;
1311
+ excessCounter.value--;
1312
+ }
1313
+ }
1314
+
1315
+ function stripAllCacheControl<T extends CacheControlBlock>(blocks: T[], excessCounter: { value: number }): void {
1316
+ for (const block of blocks) {
1317
+ if (excessCounter.value <= 0) return;
1318
+ if (!block.cache_control) continue;
1319
+ delete block.cache_control;
1320
+ excessCounter.value--;
1321
+ }
1322
+ }
1323
+
1324
+ function stripMessageCacheControl(
1325
+ messages: MessageCreateParamsStreaming["messages"],
1326
+ excessCounter: { value: number },
1327
+ ): void {
1328
+ for (const message of messages) {
1329
+ if (excessCounter.value <= 0) return;
1330
+ if (!Array.isArray(message.content)) continue;
1331
+ for (const block of message.content as Array<ContentBlockParam & CacheControlBlock>) {
1332
+ if (excessCounter.value <= 0) return;
1333
+ if (!block.cache_control) continue;
1334
+ delete block.cache_control;
1335
+ excessCounter.value--;
1336
+ }
1337
+ }
1338
+ }
1339
+
1340
+ function countCacheControlBreakpoints(params: MessageCreateParamsStreaming): number {
1341
+ let total = 0;
1342
+ if (params.tools) {
1343
+ for (const tool of params.tools as Array<Anthropic.Messages.Tool & CacheControlBlock>) {
1344
+ if (tool.cache_control) total++;
1345
+ }
1346
+ }
1347
+ if (params.system && Array.isArray(params.system)) {
1348
+ for (const block of params.system as Array<AnthropicSystemBlock & CacheControlBlock>) {
1349
+ if (block.cache_control) total++;
1350
+ }
1351
+ }
1352
+ for (const message of params.messages) {
1353
+ if (!Array.isArray(message.content)) continue;
1354
+ for (const block of message.content as Array<ContentBlockParam & CacheControlBlock>) {
1355
+ if (block.cache_control) total++;
1356
+ }
1357
+ }
1358
+ return total;
1359
+ }
1360
+
1361
+ function enforceCacheControlLimit(params: MessageCreateParamsStreaming, maxBreakpoints: number): void {
1362
+ const total = countCacheControlBreakpoints(params);
1363
+ if (total <= maxBreakpoints) return;
1364
+ const excessCounter = { value: total - maxBreakpoints };
1365
+ const systemBlocks =
1366
+ params.system && Array.isArray(params.system)
1367
+ ? (params.system as Array<AnthropicSystemBlock & CacheControlBlock>)
1368
+ : [];
1369
+ const toolBlocks = (params.tools ?? []) as Array<Anthropic.Messages.Tool & CacheControlBlock>;
1370
+ const lastSystemIndex = findLastCacheControlIndex(systemBlocks);
1371
+ const lastToolIndex = findLastCacheControlIndex(toolBlocks);
1372
+ if (systemBlocks.length > 0) {
1373
+ stripCacheControlExceptIndex(systemBlocks, lastSystemIndex, excessCounter);
1374
+ }
1375
+ if (excessCounter.value <= 0) return;
1376
+ if (toolBlocks.length > 0) {
1377
+ stripCacheControlExceptIndex(toolBlocks, lastToolIndex, excessCounter);
1378
+ }
1379
+ if (excessCounter.value <= 0) return;
1380
+ stripMessageCacheControl(params.messages, excessCounter);
1381
+ if (excessCounter.value <= 0) return;
1382
+ if (systemBlocks.length > 0) {
1383
+ stripAllCacheControl(systemBlocks, excessCounter);
1384
+ }
1385
+ if (excessCounter.value <= 0) return;
1386
+ if (toolBlocks.length > 0) {
1387
+ stripAllCacheControl(toolBlocks, excessCounter);
1388
+ }
1389
+ }
1390
+ function buildParams(
1391
+ model: Model<"anthropic-messages">,
1392
+ baseUrl: string,
1393
+ context: Context,
1394
+ isOAuthToken: boolean,
1395
+ options?: AnthropicOptions,
1396
+ ): MessageCreateParamsStreaming {
1397
+ const { cacheControl } = getCacheControl(baseUrl, options?.cacheRetention);
1398
+ const params: AnthropicSamplingParams = {
1399
+ model: model.id,
1400
+ messages: convertAnthropicMessages(context.messages, model, isOAuthToken),
1401
+ max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,
1402
+ stream: true,
1403
+ };
1404
+
1405
+ if (options?.temperature !== undefined) {
1406
+ params.temperature = options.temperature;
1407
+ }
1408
+ if (options?.topP !== undefined) {
1409
+ params.top_p = options.topP;
1410
+ }
1411
+ if (options?.topK !== undefined) {
1412
+ params.top_k = options.topK;
1413
+ }
1414
+
1415
+ if (context.tools) {
1416
+ params.tools = convertTools(context.tools, isOAuthToken);
1417
+ }
1418
+
1419
+ if (options?.thinkingEnabled && model.reasoning) {
1420
+ const mode = model.thinking?.mode;
1421
+ const requestedEffort = options.reasoning;
1422
+ const effort =
1423
+ options.effort ?? (requestedEffort ? mapEffortToAnthropicAdaptiveEffort(model, requestedEffort) : undefined);
1424
+
1425
+ if (mode === "anthropic-adaptive") {
1426
+ params.thinking = { type: "adaptive" };
1427
+ if (effort) {
1428
+ params.output_config = { effort };
1429
+ }
1430
+ } else {
1431
+ params.thinking = {
1432
+ type: "enabled",
1433
+ budget_tokens: options.thinkingBudgetTokens || 1024,
1434
+ };
1435
+ if (mode === "anthropic-budget-effort" && effort) {
1436
+ params.output_config = { effort };
1437
+ }
1438
+ }
1439
+ // Anthropic requires temperature=1 when thinking is enabled; override any
1440
+ // caller-supplied value (e.g. temperature=0 set for deterministic non-interactive mode).
1441
+ params.temperature = 1;
1442
+ }
1443
+
1444
+ const metadataUserId = resolveAnthropicMetadataUserId(options?.metadata?.user_id, isOAuthToken);
1445
+ if (metadataUserId) {
1446
+ params.metadata = { user_id: metadataUserId };
1447
+ }
1448
+
1449
+ if (options?.toolChoice) {
1450
+ if (typeof options.toolChoice === "string") {
1451
+ params.tool_choice = { type: options.toolChoice };
1452
+ } else if (isOAuthToken && options.toolChoice.name) {
1453
+ params.tool_choice = {
1454
+ ...options.toolChoice,
1455
+ name: applyClaudeToolPrefix(options.toolChoice.name),
1456
+ };
1457
+ } else {
1458
+ params.tool_choice = options.toolChoice;
1459
+ }
1460
+ }
1461
+
1462
+ const shouldInjectClaudeCodeInstruction = isOAuthToken && !model.id.startsWith("claude-3-5-haiku");
1463
+ const billingPayload = shouldInjectClaudeCodeInstruction
1464
+ ? {
1465
+ ...params,
1466
+ ...(context.systemPrompt ? { system: context.systemPrompt.toWellFormed() } : {}),
1467
+ }
1468
+ : undefined;
1469
+ const systemBlocks = buildAnthropicSystemBlocks(context.systemPrompt, {
1470
+ includeClaudeCodeInstruction: shouldInjectClaudeCodeInstruction,
1471
+ billingPayload,
1472
+ });
1473
+ if (systemBlocks) {
1474
+ params.system = systemBlocks;
1475
+ }
1476
+ disableThinkingIfToolChoiceForced(params);
1477
+ ensureMaxTokensForThinking(params, model);
1478
+ applyPromptCaching(params, cacheControl);
1479
+ enforceCacheControlLimit(params, 4);
1480
+ normalizeCacheControlTtlOrdering(params);
1481
+
1482
+ return params;
1483
+ }
1484
+
1485
+ export function convertAnthropicMessages(
1486
+ messages: Message[],
1487
+ model: Model<"anthropic-messages">,
1488
+ isOAuthToken: boolean,
1489
+ ): MessageParam[] {
1490
+ const params: MessageParam[] = [];
1491
+
1492
+ const transformedMessages = transformMessages(messages, model, normalizeToolCallId);
1493
+
1494
+ for (let i = 0; i < transformedMessages.length; i++) {
1495
+ const msg = transformedMessages[i];
1496
+
1497
+ if (msg.role === "user" || msg.role === "developer") {
1498
+ if (!msg.content) continue;
1499
+
1500
+ if (typeof msg.content === "string") {
1501
+ if (msg.content.trim().length > 0) {
1502
+ params.push({
1503
+ role: "user",
1504
+ content: msg.content.toWellFormed(),
1505
+ });
1506
+ }
1507
+ } else {
1508
+ const blocks: ContentBlockParam[] = msg.content.map(item => {
1509
+ if (item.type === "text") {
1510
+ return {
1511
+ type: "text",
1512
+ text: item.text.toWellFormed(),
1513
+ };
1514
+ }
1515
+ return {
1516
+ type: "image",
1517
+ source: {
1518
+ type: "base64",
1519
+ media_type: item.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
1520
+ data: item.data,
1521
+ },
1522
+ };
1523
+ });
1524
+ let filteredBlocks = !model?.input.includes("image") ? blocks.filter(b => b.type !== "image") : blocks;
1525
+ filteredBlocks = filteredBlocks.filter(b => {
1526
+ if (b.type === "text") {
1527
+ return b.text.trim().length > 0;
1528
+ }
1529
+ return true;
1530
+ });
1531
+ if (filteredBlocks.length === 0) continue;
1532
+ params.push({
1533
+ role: "user",
1534
+ content: filteredBlocks,
1535
+ });
1536
+ }
1537
+ } else if (msg.role === "assistant") {
1538
+ const blocks: ContentBlockParam[] = [];
1539
+ const hasSignedThinking = msg.content.some(
1540
+ block =>
1541
+ block.type === "thinking" && !!block.thinkingSignature && block.thinkingSignature.trim().length > 0,
1542
+ );
1543
+
1544
+ for (const block of msg.content) {
1545
+ if (block.type === "text") {
1546
+ if (block.text.trim().length === 0) continue;
1547
+ blocks.push({
1548
+ type: "text",
1549
+ text: block.text.toWellFormed(),
1550
+ });
1551
+ } else if (block.type === "thinking") {
1552
+ if (hasSignedThinking) {
1553
+ if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {
1554
+ if (block.thinking.trim().length === 0) continue;
1555
+ blocks.push({
1556
+ type: "text",
1557
+ text: block.thinking.toWellFormed(),
1558
+ });
1559
+ continue;
1560
+ }
1561
+ blocks.push({
1562
+ type: "thinking",
1563
+ thinking: block.thinking,
1564
+ signature: block.thinkingSignature,
1565
+ });
1566
+ continue;
1567
+ }
1568
+ if (block.thinking.trim().length === 0) continue;
1569
+ if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {
1570
+ blocks.push({
1571
+ type: "text",
1572
+ text: block.thinking.toWellFormed(),
1573
+ });
1574
+ } else {
1575
+ blocks.push({
1576
+ type: "thinking",
1577
+ thinking: block.thinking.toWellFormed(),
1578
+ signature: block.thinkingSignature,
1579
+ });
1580
+ }
1581
+ } else if (block.type === "redactedThinking") {
1582
+ if (block.data.trim().length === 0) continue;
1583
+ blocks.push({
1584
+ type: "redacted_thinking",
1585
+ data: block.data,
1586
+ });
1587
+ } else if (block.type === "toolCall") {
1588
+ blocks.push({
1589
+ type: "tool_use",
1590
+ id: block.id,
1591
+ name: isOAuthToken ? applyClaudeToolPrefix(block.name) : block.name,
1592
+ input: block.arguments ?? {},
1593
+ });
1594
+ }
1595
+ }
1596
+ if (blocks.length === 0) continue;
1597
+ params.push({
1598
+ role: "assistant",
1599
+ content: blocks,
1600
+ });
1601
+ } else if (msg.role === "toolResult") {
1602
+ // Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint
1603
+ const toolResults: ContentBlockParam[] = [];
1604
+
1605
+ // Add the current tool result
1606
+ toolResults.push({
1607
+ type: "tool_result",
1608
+ tool_use_id: msg.toolCallId,
1609
+ content: convertContentBlocks(msg.content),
1610
+ is_error: msg.isError,
1611
+ });
1612
+
1613
+ // Look ahead for consecutive toolResult messages
1614
+ let j = i + 1;
1615
+ while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
1616
+ const nextMsg = transformedMessages[j] as ToolResultMessage; // We know it's a toolResult
1617
+ toolResults.push({
1618
+ type: "tool_result",
1619
+ tool_use_id: nextMsg.toolCallId,
1620
+ content: convertContentBlocks(nextMsg.content),
1621
+ is_error: nextMsg.isError,
1622
+ });
1623
+ j++;
1624
+ }
1625
+
1626
+ // Skip the messages we've already processed
1627
+ i = j - 1;
1628
+
1629
+ // Add a single user message with all tool results
1630
+ params.push({
1631
+ role: "user",
1632
+ content: toolResults,
1633
+ });
1634
+ }
1635
+ }
1636
+
1637
+ if (params.length > 0 && params[params.length - 1]?.role === "assistant") {
1638
+ params.push({ role: "user", content: "Continue." });
1639
+ }
1640
+
1641
+ return params;
1642
+ }
1643
+
1644
+ function convertTools(tools: Tool[], isOAuthToken: boolean): Anthropic.Messages.Tool[] {
1645
+ if (!tools) return [];
1646
+
1647
+ return tools.map(tool => {
1648
+ const jsonSchema = tool.parameters as any; // TypeBox already generates JSON Schema
1649
+
1650
+ return {
1651
+ name: isOAuthToken ? applyClaudeToolPrefix(tool.name) : tool.name,
1652
+ description: tool.description || "",
1653
+ input_schema: {
1654
+ type: "object" as const,
1655
+ properties: jsonSchema.properties || {},
1656
+ required: jsonSchema.required || [],
1657
+ },
1658
+ };
1659
+ });
1660
+ }
1661
+
1662
+ function mapStopReason(reason: Anthropic.Messages.StopReason | string): StopReason {
1663
+ switch (reason) {
1664
+ case "end_turn":
1665
+ return "stop";
1666
+ case "max_tokens":
1667
+ return "length";
1668
+ case "tool_use":
1669
+ return "toolUse";
1670
+ case "refusal":
1671
+ return "error";
1672
+ case "pause_turn": // Stop is good enough -> resubmit
1673
+ return "stop";
1674
+ case "stop_sequence":
1675
+ return "stop"; // We don't supply stop sequences, so this should never happen
1676
+ case "sensitive": // Content flagged by safety filters (not yet in SDK types)
1677
+ return "error";
1678
+ default:
1679
+ // Handle unknown stop reasons gracefully (API may add new values)
1680
+ throw new Error(`Unhandled stop reason: ${reason}`);
1681
+ }
1682
+ }