@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
package/src/stream.ts ADDED
@@ -0,0 +1,728 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { $env, $pickenv } from "@f5xc-salesdemos/pi-utils";
5
+ import { getCustomApi } from "./api-registry";
6
+ import type { Effort } from "./model-thinking";
7
+ import {
8
+ mapEffortToAnthropicAdaptiveEffort,
9
+ mapEffortToGoogleThinkingLevel,
10
+ requireSupportedEffort,
11
+ } from "./model-thinking";
12
+ import { type BedrockOptions, streamBedrock } from "./providers/amazon-bedrock";
13
+ import { type AnthropicOptions, streamAnthropic } from "./providers/anthropic";
14
+ import { streamAzureOpenAIResponses } from "./providers/azure-openai-responses";
15
+ import { type CursorOptions, streamCursor } from "./providers/cursor";
16
+ import { isGitLabDuoModel, streamGitLabDuo } from "./providers/gitlab-duo";
17
+ import { type GoogleOptions, streamGoogle } from "./providers/google";
18
+ import { type GoogleGeminiCliOptions, streamGoogleGeminiCli } from "./providers/google-gemini-cli";
19
+ import { type GoogleVertexOptions, streamGoogleVertex } from "./providers/google-vertex";
20
+ import { isKimiModel, streamKimi } from "./providers/kimi";
21
+ import { streamOpenAICodexResponses } from "./providers/openai-codex-responses";
22
+ import { type OpenAICompletionsOptions, streamOpenAICompletions } from "./providers/openai-completions";
23
+ import { streamOpenAIResponses } from "./providers/openai-responses";
24
+ import { isSyntheticModel, streamSynthetic } from "./providers/synthetic";
25
+ import type {
26
+ Api,
27
+ AssistantMessage,
28
+ AssistantMessageEventStream,
29
+ Context,
30
+ Model,
31
+ OptionsForApi,
32
+ SimpleStreamOptions,
33
+ StreamOptions,
34
+ ThinkingBudgets,
35
+ ToolChoice,
36
+ } from "./types";
37
+
38
+ let cachedVertexAdcCredentialsExists: boolean | null = null;
39
+
40
+ function hasVertexAdcCredentials(): boolean {
41
+ if (cachedVertexAdcCredentialsExists === null) {
42
+ const gacPath = $env.GOOGLE_APPLICATION_CREDENTIALS;
43
+ if (gacPath) {
44
+ cachedVertexAdcCredentialsExists = fs.existsSync(gacPath);
45
+ } else {
46
+ cachedVertexAdcCredentialsExists = fs.existsSync(
47
+ path.join(os.homedir(), ".config", "gcloud", "application_default_credentials.json"),
48
+ );
49
+ }
50
+ }
51
+ return cachedVertexAdcCredentialsExists;
52
+ }
53
+
54
+ type KeyResolver = string | (() => string | undefined);
55
+
56
+ function isFoundryEnabled(): boolean {
57
+ const value = $env.CLAUDE_CODE_USE_FOUNDRY;
58
+ if (!value) return false;
59
+ const normalized = value.trim().toLowerCase();
60
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
61
+ }
62
+
63
+ const serviceProviderMap: Record<string, KeyResolver> = {
64
+ "alibaba-coding-plan": "ALIBABA_CODING_PLAN_API_KEY",
65
+ openai: "OPENAI_API_KEY",
66
+ google: "GEMINI_API_KEY",
67
+ groq: "GROQ_API_KEY",
68
+ cerebras: "CEREBRAS_API_KEY",
69
+ xai: "XAI_API_KEY",
70
+ openrouter: "OPENROUTER_API_KEY",
71
+ kilo: "KILO_API_KEY",
72
+ "vercel-ai-gateway": "AI_GATEWAY_API_KEY",
73
+ zai: "ZAI_API_KEY",
74
+ mistral: "MISTRAL_API_KEY",
75
+ minimax: "MINIMAX_API_KEY",
76
+ "minimax-code": "MINIMAX_CODE_API_KEY",
77
+ "minimax-code-cn": "MINIMAX_CODE_CN_API_KEY",
78
+ "opencode-go": "OPENCODE_API_KEY",
79
+ "opencode-zen": "OPENCODE_API_KEY",
80
+ cursor: "CURSOR_ACCESS_TOKEN",
81
+ "openai-codex": "OPENAI_CODEX_OAUTH_TOKEN",
82
+ "azure-openai-responses": "AZURE_OPENAI_API_KEY",
83
+ exa: "EXA_API_KEY",
84
+ jina: "JINA_API_KEY",
85
+ brave: "BRAVE_API_KEY",
86
+ perplexity: "PERPLEXITY_API_KEY",
87
+ tavily: "TAVILY_API_KEY",
88
+ parallel: "PARALLEL_API_KEY",
89
+ kagi: "KAGI_API_KEY",
90
+ // GitHub Copilot uses GitHub personal access token
91
+ "github-copilot": () => $pickenv("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"),
92
+ // Foundry mode optionally switches Anthropic auth to enterprise gateway credentials.
93
+ anthropic: () =>
94
+ isFoundryEnabled()
95
+ ? $pickenv("ANTHROPIC_FOUNDRY_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY")
96
+ : $pickenv("ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"),
97
+ "gitlab-duo": "GITLAB_TOKEN",
98
+ // Vertex AI supports either GOOGLE_CLOUD_API_KEY or Application Default Credentials.
99
+ "google-vertex": () => {
100
+ if ($env.GOOGLE_CLOUD_API_KEY) {
101
+ return $env.GOOGLE_CLOUD_API_KEY;
102
+ }
103
+ const hasCredentials = hasVertexAdcCredentials();
104
+ const hasProject = !!($env.GOOGLE_CLOUD_PROJECT || $env.GCLOUD_PROJECT);
105
+ const hasLocation = !!$env.GOOGLE_CLOUD_LOCATION;
106
+ if (hasCredentials && hasProject && hasLocation) {
107
+ return "<authenticated>";
108
+ }
109
+ },
110
+ // Amazon Bedrock supports multiple credential sources:
111
+ // 1. AWS_PROFILE - named profile from ~/.aws/credentials
112
+ // 2. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY - standard IAM keys
113
+ // 3. AWS_BEARER_TOKEN_BEDROCK - Bedrock API keys (bearer token)
114
+ // 4. AWS_CONTAINER_CREDENTIALS_* - ECS/Task IAM role credentials
115
+ // 5. AWS_WEB_IDENTITY_TOKEN_FILE + AWS_ROLE_ARN - IRSA (EKS) web identity
116
+ "amazon-bedrock": () => {
117
+ const hasEcsCredentials =
118
+ !!$env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || !!$env.AWS_CONTAINER_CREDENTIALS_FULL_URI;
119
+ const hasWebIdentity = !!$env.AWS_WEB_IDENTITY_TOKEN_FILE && !!$env.AWS_ROLE_ARN;
120
+ if (
121
+ $env.AWS_PROFILE ||
122
+ ($env.AWS_ACCESS_KEY_ID && $env.AWS_SECRET_ACCESS_KEY) ||
123
+ $env.AWS_BEARER_TOKEN_BEDROCK ||
124
+ hasEcsCredentials ||
125
+ hasWebIdentity
126
+ ) {
127
+ return "<authenticated>";
128
+ }
129
+ },
130
+ synthetic: "SYNTHETIC_API_KEY",
131
+ "cloudflare-ai-gateway": "CLOUDFLARE_AI_GATEWAY_API_KEY",
132
+ huggingface: () => $pickenv("HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"),
133
+ litellm: "LITELLM_API_KEY",
134
+ moonshot: "MOONSHOT_API_KEY",
135
+ nvidia: "NVIDIA_API_KEY",
136
+ nanogpt: "NANO_GPT_API_KEY",
137
+ "lm-studio": "LM_STUDIO_API_KEY",
138
+ ollama: "OLLAMA_API_KEY",
139
+ "llama.cpp": "LLAMA_CPP_API_KEY",
140
+ qianfan: "QIANFAN_API_KEY",
141
+ "qwen-portal": () => $pickenv("QWEN_OAUTH_TOKEN", "QWEN_PORTAL_API_KEY"),
142
+ together: "TOGETHER_API_KEY",
143
+ zenmux: "ZENMUX_API_KEY",
144
+ venice: "VENICE_API_KEY",
145
+ vllm: "VLLM_API_KEY",
146
+ xiaomi: "XIAOMI_API_KEY",
147
+ };
148
+
149
+ /**
150
+ * Get API key for provider from known environment variables, e.g. OPENAI_API_KEY.
151
+ *
152
+ * Will not return API keys for providers that require OAuth tokens.
153
+ * Checks Bun.env, then cwd/.env, then ~/.env.
154
+ */
155
+ export function getEnvApiKey(provider: string): string | undefined {
156
+ const resolver = serviceProviderMap[provider];
157
+ if (typeof resolver === "string") {
158
+ return $env[resolver];
159
+ }
160
+ return resolver?.();
161
+ }
162
+
163
+ export function stream<TApi extends Api>(
164
+ model: Model<TApi>,
165
+ context: Context,
166
+ options?: OptionsForApi<TApi>,
167
+ ): AssistantMessageEventStream {
168
+ // Check custom API registry first (extension-provided APIs like "vertex-claude-api")
169
+ const customApiProvider = getCustomApi(model.api);
170
+ if (customApiProvider) {
171
+ return customApiProvider.stream(model, context, options as StreamOptions);
172
+ }
173
+
174
+ if (isGitLabDuoModel(model)) {
175
+ const apiKey = (options as StreamOptions | undefined)?.apiKey || getEnvApiKey(model.provider);
176
+ if (!apiKey) {
177
+ throw new Error(`No API key for provider: ${model.provider}`);
178
+ }
179
+ return streamGitLabDuo(model, context, {
180
+ ...(options as SimpleStreamOptions | undefined),
181
+ apiKey,
182
+ });
183
+ }
184
+
185
+ // Vertex AI uses Application Default Credentials, not API keys
186
+ if (model.api === "google-vertex") {
187
+ return streamGoogleVertex(model as Model<"google-vertex">, context, options as GoogleVertexOptions);
188
+ } else if (model.api === "bedrock-converse-stream") {
189
+ // Bedrock doesn't have any API keys instead it sources credentials from standard AWS env variables or from given AWS profile.
190
+ return streamBedrock(model as Model<"bedrock-converse-stream">, context, (options || {}) as BedrockOptions);
191
+ }
192
+
193
+ const apiKey = options?.apiKey || getEnvApiKey(model.provider);
194
+ if (!apiKey) {
195
+ throw new Error(`No API key for provider: ${model.provider}`);
196
+ }
197
+ const providerOptions = { ...options, apiKey };
198
+
199
+ const api: Api = model.api;
200
+ switch (api) {
201
+ case "anthropic-messages":
202
+ return streamAnthropic(model as Model<"anthropic-messages">, context, providerOptions);
203
+
204
+ case "openai-completions":
205
+ return streamOpenAICompletions(model as Model<"openai-completions">, context, providerOptions as any);
206
+
207
+ case "openai-responses":
208
+ return streamOpenAIResponses(model as Model<"openai-responses">, context, providerOptions as any);
209
+
210
+ case "azure-openai-responses":
211
+ return streamAzureOpenAIResponses(model as Model<"azure-openai-responses">, context, providerOptions as any);
212
+
213
+ case "openai-codex-responses":
214
+ return streamOpenAICodexResponses(model as Model<"openai-codex-responses">, context, providerOptions as any);
215
+
216
+ case "google-generative-ai":
217
+ return streamGoogle(model as Model<"google-generative-ai">, context, providerOptions);
218
+
219
+ case "google-gemini-cli":
220
+ return streamGoogleGeminiCli(
221
+ model as Model<"google-gemini-cli">,
222
+ context,
223
+ providerOptions as GoogleGeminiCliOptions,
224
+ );
225
+
226
+ case "cursor-agent":
227
+ return streamCursor(model as Model<"cursor-agent">, context, providerOptions as CursorOptions);
228
+
229
+ default:
230
+ throw new Error(`Unhandled API: ${api}`);
231
+ }
232
+ }
233
+
234
+ export async function complete<TApi extends Api>(
235
+ model: Model<TApi>,
236
+ context: Context,
237
+ options?: OptionsForApi<TApi>,
238
+ ): Promise<AssistantMessage> {
239
+ const s = stream(model, context, options);
240
+ return s.result();
241
+ }
242
+
243
+ export function streamSimple<TApi extends Api>(
244
+ model: Model<TApi>,
245
+ context: Context,
246
+ options?: SimpleStreamOptions,
247
+ ): AssistantMessageEventStream {
248
+ // Check custom API registry first (extension-provided APIs)
249
+ const customApiProvider = getCustomApi(model.api);
250
+ if (customApiProvider) {
251
+ return customApiProvider.streamSimple(model, context, options);
252
+ }
253
+
254
+ // Vertex AI uses Application Default Credentials, not API keys
255
+ if (model.api === "google-vertex") {
256
+ const providerOptions = mapOptionsForApi(model, options, undefined);
257
+ return stream(model, context, providerOptions);
258
+ } else if (model.api === "bedrock-converse-stream") {
259
+ // Bedrock doesn't have any API keys instead it sources credentials from standard AWS env variables or from given AWS profile.
260
+ const providerOptions = mapOptionsForApi(model, options, undefined);
261
+ return stream(model, context, providerOptions);
262
+ }
263
+
264
+ const apiKey = options?.apiKey || getEnvApiKey(model.provider);
265
+ if (!apiKey) {
266
+ throw new Error(`No API key for provider: ${model.provider}`);
267
+ }
268
+
269
+ // GitLab Duo - wraps Anthropic/OpenAI behind GitLab AI Gateway direct access tokens
270
+ if (isGitLabDuoModel(model)) {
271
+ return streamGitLabDuo(model, context, {
272
+ ...options,
273
+ apiKey,
274
+ });
275
+ }
276
+
277
+ // Kimi Code - route to dedicated handler that wraps OpenAI or Anthropic API
278
+ if (isKimiModel(model)) {
279
+ // Pass raw SimpleStreamOptions - streamKimi handles mapping internally
280
+ return streamKimi(model as Model<"openai-completions">, context, {
281
+ ...options,
282
+ apiKey,
283
+ format: options?.kimiApiFormat ?? "anthropic",
284
+ });
285
+ }
286
+
287
+ // Synthetic - route to dedicated handler that wraps OpenAI or Anthropic API
288
+ if (isSyntheticModel(model)) {
289
+ // Pass raw SimpleStreamOptions - streamSynthetic handles mapping internally
290
+ return streamSynthetic(model as Model<"openai-completions">, context, {
291
+ ...options,
292
+ apiKey,
293
+ format: options?.syntheticApiFormat ?? "openai", // Default to OpenAI format
294
+ });
295
+ }
296
+
297
+ const providerOptions = mapOptionsForApi(model, options, apiKey);
298
+ return stream(model, context, providerOptions);
299
+ }
300
+
301
+ export async function completeSimple<TApi extends Api>(
302
+ model: Model<TApi>,
303
+ context: Context,
304
+ options?: SimpleStreamOptions,
305
+ ): Promise<AssistantMessage> {
306
+ const s = streamSimple(model, context, options);
307
+ return s.result();
308
+ }
309
+
310
+ const MIN_OUTPUT_TOKENS = 1024;
311
+ export const OUTPUT_FALLBACK_BUFFER = 4000;
312
+ const ANTHROPIC_USE_INTERLEAVED_THINKING = Bun.env.PI_NO_INTERLEAVED_THINKING !== "1";
313
+
314
+ export const ANTHROPIC_THINKING: Record<Effort, number> = {
315
+ minimal: 1024,
316
+ low: 4096,
317
+ medium: 8192,
318
+ high: 16384,
319
+ xhigh: 32768,
320
+ };
321
+
322
+ const GOOGLE_THINKING: Record<Effort, number> = {
323
+ minimal: 1024,
324
+ low: 4096,
325
+ medium: 8192,
326
+ high: 16384,
327
+ xhigh: 24575,
328
+ };
329
+
330
+ const BEDROCK_CLAUDE_THINKING: Record<Effort, number> = {
331
+ minimal: 1024,
332
+ low: 2048,
333
+ medium: 8192,
334
+ high: 16384,
335
+ xhigh: 16384,
336
+ };
337
+
338
+ function resolveBedrockThinkingBudget(
339
+ model: Model<"bedrock-converse-stream">,
340
+ options?: SimpleStreamOptions,
341
+ ): { budget: number; level: Effort } | null {
342
+ if (!options?.reasoning || !model.reasoning) return null;
343
+ const level = requireSupportedEffort(model, options.reasoning);
344
+ const budget = options.thinkingBudgets?.[level] ?? BEDROCK_CLAUDE_THINKING[level];
345
+ return { budget, level };
346
+ }
347
+
348
+ export function mapAnthropicToolChoice(choice?: ToolChoice): AnthropicOptions["toolChoice"] {
349
+ if (!choice) return undefined;
350
+ if (typeof choice === "string") {
351
+ if (choice === "required") return "any";
352
+ if (choice === "auto" || choice === "none" || choice === "any") return choice;
353
+ return undefined;
354
+ }
355
+ if (choice.type === "tool") {
356
+ return choice.name ? { type: "tool", name: choice.name } : undefined;
357
+ }
358
+ if (choice.type === "function") {
359
+ const name = "function" in choice ? choice.function?.name : choice.name;
360
+ return name ? { type: "tool", name } : undefined;
361
+ }
362
+ return undefined;
363
+ }
364
+
365
+ function mapGoogleToolChoice(
366
+ choice?: ToolChoice,
367
+ ): GoogleOptions["toolChoice"] | GoogleGeminiCliOptions["toolChoice"] | GoogleVertexOptions["toolChoice"] {
368
+ if (!choice) return undefined;
369
+ if (typeof choice === "string") {
370
+ if (choice === "required") return "any";
371
+ if (choice === "auto" || choice === "none" || choice === "any") return choice;
372
+ return undefined;
373
+ }
374
+ return "any";
375
+ }
376
+
377
+ function mapOpenAiToolChoice(choice?: ToolChoice): OpenAICompletionsOptions["toolChoice"] {
378
+ if (!choice) return undefined;
379
+ if (typeof choice === "string") {
380
+ if (choice === "any") return "required";
381
+ if (choice === "auto" || choice === "none" || choice === "required") return choice;
382
+ return undefined;
383
+ }
384
+ if (choice.type === "tool") {
385
+ return choice.name ? { type: "function", function: { name: choice.name } } : undefined;
386
+ }
387
+ if (choice.type === "function") {
388
+ const name = "function" in choice ? choice.function?.name : choice.name;
389
+ return name ? { type: "function", function: { name } } : undefined;
390
+ }
391
+ return undefined;
392
+ }
393
+
394
+ function resolveOpenAiReasoningEffort<TApi extends Api>(
395
+ model: Model<TApi>,
396
+ options?: SimpleStreamOptions,
397
+ ): Effort | undefined {
398
+ const reasoning = options?.reasoning;
399
+ if (!reasoning || !model.reasoning) return undefined;
400
+ return requireSupportedEffort(model, reasoning);
401
+ }
402
+
403
+ const castApi = <TApi extends Api>(api: OptionsForApi<TApi>): OptionsForApi<Api> => api as OptionsForApi<Api>;
404
+
405
+ function mapOptionsForApi<TApi extends Api>(
406
+ model: Model<TApi>,
407
+ options?: SimpleStreamOptions,
408
+ apiKey?: string,
409
+ ): OptionsForApi<TApi> {
410
+ const base = {
411
+ temperature: options?.temperature,
412
+ topP: options?.topP,
413
+ topK: options?.topK,
414
+ minP: options?.minP,
415
+ presencePenalty: options?.presencePenalty,
416
+ repetitionPenalty: options?.repetitionPenalty,
417
+ maxTokens: options?.maxTokens || Math.min(model.maxTokens, 32000),
418
+ signal: options?.signal,
419
+ apiKey: apiKey || options?.apiKey,
420
+ cacheRetention: options?.cacheRetention,
421
+ headers: options?.headers,
422
+ initiatorOverride: options?.initiatorOverride,
423
+ maxRetryDelayMs: options?.maxRetryDelayMs,
424
+ metadata: options?.metadata,
425
+ sessionId: options?.sessionId,
426
+ providerSessionState: options?.providerSessionState,
427
+ onPayload: options?.onPayload,
428
+ execHandlers: options?.execHandlers,
429
+ };
430
+
431
+ switch (model.api) {
432
+ case "anthropic-messages": {
433
+ // Explicitly disable thinking when reasoning is not specified or model doesn't support it
434
+ const reasoning = options?.reasoning;
435
+ if (!reasoning || !model.reasoning) {
436
+ return castApi<"anthropic-messages">({
437
+ ...base,
438
+ thinkingEnabled: false,
439
+ toolChoice: mapAnthropicToolChoice(options?.toolChoice),
440
+ });
441
+ }
442
+
443
+ let thinkingBudget = options.thinkingBudgets?.[reasoning] ?? ANTHROPIC_THINKING[reasoning];
444
+ if (thinkingBudget <= 0) {
445
+ return castApi<"anthropic-messages">({
446
+ ...base,
447
+ thinkingEnabled: false,
448
+ toolChoice: mapAnthropicToolChoice(options?.toolChoice),
449
+ });
450
+ }
451
+
452
+ // For Opus 4.6+ and Sonnet 4.6+: use adaptive thinking with effort level
453
+ // For older models: use budget-based thinking
454
+ if (model.thinking?.mode === "anthropic-adaptive") {
455
+ const effort = mapEffortToAnthropicAdaptiveEffort(model, reasoning);
456
+ return castApi<"anthropic-messages">({
457
+ ...base,
458
+ thinkingEnabled: true,
459
+ effort,
460
+ toolChoice: mapAnthropicToolChoice(options?.toolChoice),
461
+ });
462
+ }
463
+
464
+ if (ANTHROPIC_USE_INTERLEAVED_THINKING) {
465
+ return castApi<"anthropic-messages">({
466
+ ...base,
467
+ thinkingEnabled: true,
468
+ thinkingBudgetTokens: thinkingBudget,
469
+ toolChoice: mapAnthropicToolChoice(options?.toolChoice),
470
+ });
471
+ }
472
+
473
+ // Caller's maxTokens is the desired output; add thinking budget on top, capped at model limit
474
+ const maxTokens = Math.min((base.maxTokens || 0) + thinkingBudget, model.maxTokens);
475
+
476
+ // If not enough room for thinking + output, reduce thinking budget
477
+ if (maxTokens <= thinkingBudget) {
478
+ thinkingBudget = maxTokens - MIN_OUTPUT_TOKENS;
479
+ }
480
+
481
+ // If thinking budget is too low, disable thinking
482
+ if (thinkingBudget <= 0) {
483
+ return castApi<"anthropic-messages">({
484
+ ...base,
485
+ thinkingEnabled: false,
486
+ toolChoice: mapAnthropicToolChoice(options?.toolChoice),
487
+ });
488
+ } else {
489
+ return castApi<"anthropic-messages">({
490
+ ...base,
491
+ maxTokens,
492
+ thinkingEnabled: true,
493
+ thinkingBudgetTokens: thinkingBudget,
494
+ toolChoice: mapAnthropicToolChoice(options?.toolChoice),
495
+ });
496
+ }
497
+ }
498
+
499
+ case "bedrock-converse-stream": {
500
+ const bedrockBase: BedrockOptions = {
501
+ ...base,
502
+ reasoning: options?.reasoning,
503
+ thinkingBudgets: options?.thinkingBudgets,
504
+ toolChoice: mapAnthropicToolChoice(options?.toolChoice),
505
+ };
506
+ // Adaptive mode sends effort directly, no budget_tokens — skip budget inflation.
507
+ if (model.thinking?.mode === "anthropic-adaptive") {
508
+ return castApi<"bedrock-converse-stream">(bedrockBase);
509
+ }
510
+ const budgetInfo = resolveBedrockThinkingBudget(model as Model<"bedrock-converse-stream">, options);
511
+ if (!budgetInfo) return bedrockBase as OptionsForApi<TApi>;
512
+ let maxTokens = bedrockBase.maxTokens ?? model.maxTokens;
513
+ let thinkingBudgets = bedrockBase.thinkingBudgets;
514
+ if (maxTokens <= budgetInfo.budget) {
515
+ const desiredMaxTokens = Math.min(model.maxTokens, budgetInfo.budget + MIN_OUTPUT_TOKENS);
516
+ if (desiredMaxTokens > maxTokens) {
517
+ maxTokens = desiredMaxTokens;
518
+ }
519
+ }
520
+ if (maxTokens <= budgetInfo.budget) {
521
+ const adjustedBudget = Math.max(0, maxTokens - MIN_OUTPUT_TOKENS);
522
+ thinkingBudgets = { ...(thinkingBudgets ?? {}), [budgetInfo.level]: adjustedBudget };
523
+ }
524
+ return castApi<"bedrock-converse-stream">({ ...bedrockBase, maxTokens, thinkingBudgets });
525
+ }
526
+
527
+ case "openai-completions":
528
+ return castApi<"openai-completions">({
529
+ ...base,
530
+ reasoning: resolveOpenAiReasoningEffort(model, options),
531
+ toolChoice: mapOpenAiToolChoice(options?.toolChoice),
532
+ serviceTier: options?.serviceTier,
533
+ });
534
+
535
+ case "openai-responses":
536
+ return castApi<"openai-responses">({
537
+ ...base,
538
+ reasoning: resolveOpenAiReasoningEffort(model, options),
539
+ toolChoice: mapOpenAiToolChoice(options?.toolChoice),
540
+ serviceTier: options?.serviceTier,
541
+ });
542
+
543
+ case "azure-openai-responses":
544
+ return castApi<"azure-openai-responses">({
545
+ ...base,
546
+ reasoning: resolveOpenAiReasoningEffort(model, options),
547
+ toolChoice: mapOpenAiToolChoice(options?.toolChoice),
548
+ serviceTier: options?.serviceTier,
549
+ });
550
+
551
+ case "openai-codex-responses":
552
+ return castApi<"openai-codex-responses">({
553
+ ...base,
554
+ reasoning: resolveOpenAiReasoningEffort(model, options),
555
+ toolChoice: mapOpenAiToolChoice(options?.toolChoice),
556
+ serviceTier: options?.serviceTier,
557
+ preferWebsockets: options?.preferWebsockets,
558
+ });
559
+
560
+ case "google-generative-ai": {
561
+ // Explicitly disable thinking when reasoning is not specified or model doesn't support it
562
+ // This is needed because Gemini has "dynamic thinking" enabled by default
563
+ const reasoning = options?.reasoning;
564
+ if (!reasoning || !model.reasoning) {
565
+ return castApi<"google-generative-ai">({
566
+ ...base,
567
+ thinking: { enabled: false },
568
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
569
+ });
570
+ }
571
+
572
+ const googleModel = model as Model<"google-generative-ai">;
573
+ const effort = requireSupportedEffort(googleModel, reasoning);
574
+
575
+ // Gemini 3+ models use thinkingLevel exclusively instead of thinkingBudget.
576
+ // https://ai.google.dev/gemini-api/docs/thinking#set-budget
577
+ if (googleModel.thinking?.mode === "google-level") {
578
+ return castApi<"google-generative-ai">({
579
+ ...base,
580
+ thinking: {
581
+ enabled: true,
582
+ level: mapEffortToGoogleThinkingLevel(googleModel, effort),
583
+ },
584
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
585
+ });
586
+ }
587
+
588
+ return castApi<"google-gemini-cli">({
589
+ ...base,
590
+ thinking: {
591
+ enabled: true,
592
+ budgetTokens: getGoogleBudget(googleModel, effort, options?.thinkingBudgets),
593
+ },
594
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
595
+ });
596
+ }
597
+
598
+ case "google-gemini-cli": {
599
+ const reasoning = options?.reasoning;
600
+ if (!reasoning || !model.reasoning) {
601
+ return castApi<"google-gemini-cli">({
602
+ ...base,
603
+ thinking: { enabled: false },
604
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
605
+ });
606
+ }
607
+
608
+ const effort = requireSupportedEffort(model, reasoning);
609
+
610
+ // Gemini 3+ models use thinkingLevel instead of thinkingBudget
611
+ if (model.thinking?.mode === "google-level") {
612
+ return castApi<"google-gemini-cli">({
613
+ ...base,
614
+ thinking: {
615
+ enabled: true,
616
+ level: mapEffortToGoogleThinkingLevel(model, effort),
617
+ },
618
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
619
+ });
620
+ }
621
+
622
+ let thinkingBudget = options.thinkingBudgets?.[effort] ?? GOOGLE_THINKING[effort];
623
+
624
+ // Caller's maxTokens is the desired output; add thinking budget on top, capped at model limit
625
+ const maxTokens = Math.min((base.maxTokens || 0) + thinkingBudget, model.maxTokens);
626
+
627
+ // If not enough room for thinking + output, reduce thinking budget
628
+ if (maxTokens <= thinkingBudget) {
629
+ thinkingBudget = Math.max(0, maxTokens - MIN_OUTPUT_TOKENS) ?? 0;
630
+ }
631
+
632
+ // If thinking budget is too low, disable thinking
633
+ if (thinkingBudget <= 0) {
634
+ return castApi<"google-gemini-cli">({
635
+ ...base,
636
+ thinking: { enabled: false },
637
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
638
+ });
639
+ } else {
640
+ return castApi<"google-gemini-cli">({
641
+ ...base,
642
+ maxTokens,
643
+ thinking: { enabled: true, budgetTokens: thinkingBudget },
644
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
645
+ });
646
+ }
647
+ }
648
+
649
+ case "google-vertex": {
650
+ // Explicitly disable thinking when reasoning is not specified or model doesn't support it
651
+ const reasoning = options?.reasoning;
652
+ if (!reasoning || !model.reasoning) {
653
+ return castApi<"google-vertex">({
654
+ ...base,
655
+ thinking: { enabled: false },
656
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
657
+ });
658
+ }
659
+
660
+ const vertexModel = model as Model<"google-vertex">;
661
+ const effort = requireSupportedEffort(vertexModel, reasoning);
662
+ const geminiModel = vertexModel as unknown as Model<"google-generative-ai">;
663
+
664
+ if (geminiModel.thinking?.mode === "google-level") {
665
+ return castApi<"google-vertex">({
666
+ ...base,
667
+ thinking: {
668
+ enabled: true,
669
+ level: mapEffortToGoogleThinkingLevel(geminiModel, effort),
670
+ },
671
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
672
+ });
673
+ }
674
+
675
+ return castApi<"google-vertex">({
676
+ ...base,
677
+ thinking: {
678
+ enabled: true,
679
+ budgetTokens: getGoogleBudget(geminiModel, effort, options?.thinkingBudgets),
680
+ },
681
+ toolChoice: mapGoogleToolChoice(options?.toolChoice),
682
+ });
683
+ }
684
+
685
+ case "cursor-agent": {
686
+ const execHandlers = options?.cursorExecHandlers ?? options?.execHandlers;
687
+ const onToolResult = options?.cursorOnToolResult ?? execHandlers?.onToolResult;
688
+ return castApi<"cursor-agent">({
689
+ ...base,
690
+ execHandlers,
691
+ onToolResult,
692
+ });
693
+ }
694
+
695
+ default:
696
+ throw new Error(`Unhandled API in mapOptionsForApi: ${model.api}`);
697
+ }
698
+ }
699
+
700
+ function getGoogleBudget(
701
+ model: Model<"google-generative-ai">,
702
+ effort: Effort,
703
+ customBudgets?: ThinkingBudgets,
704
+ ): number {
705
+ requireSupportedEffort(model, effort);
706
+
707
+ // Custom budgets take precedence if provided for this level
708
+ if (customBudgets?.[effort] !== undefined) {
709
+ return customBudgets[effort]!;
710
+ }
711
+
712
+ // See https://ai.google.dev/gemini-api/docs/thinking#set-budget
713
+ if (model.id.includes("2.5-")) {
714
+ switch (effort) {
715
+ case "minimal":
716
+ return 128;
717
+ case "low":
718
+ return 2048;
719
+ case "medium":
720
+ return 8192;
721
+ default:
722
+ return model.id.includes("2.5-flash") ? 24576 : 32768;
723
+ }
724
+ }
725
+
726
+ // Unknown model - use dynamic
727
+ return -1;
728
+ }