@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,706 @@
1
+ import {
2
+ BedrockRuntimeClient,
3
+ type BedrockRuntimeClientConfig,
4
+ StopReason as BedrockStopReason,
5
+ type Tool as BedrockTool,
6
+ CachePointType,
7
+ CacheTTL,
8
+ type ContentBlock,
9
+ type ContentBlockDeltaEvent,
10
+ type ContentBlockStartEvent,
11
+ type ContentBlockStopEvent,
12
+ ConversationRole,
13
+ ConverseStreamCommand,
14
+ type ConverseStreamMetadataEvent,
15
+ ImageFormat,
16
+ type Message,
17
+ type SystemContentBlock,
18
+ type ToolChoice,
19
+ type ToolConfiguration,
20
+ ToolResultStatus,
21
+ } from "@aws-sdk/client-bedrock-runtime";
22
+ import { $env, $flag } from "@f5xc-salesdemos/pi-utils";
23
+ import { NodeHttpHandler } from "@smithy/node-http-handler";
24
+ import type { Effort } from "../model-thinking";
25
+ import { mapEffortToAnthropicAdaptiveEffort, requireSupportedEffort } from "../model-thinking";
26
+ import { calculateCost } from "../models";
27
+ import type {
28
+ Api,
29
+ AssistantMessage,
30
+ CacheRetention,
31
+ Context,
32
+ Model,
33
+ StopReason,
34
+ StreamFunction,
35
+ StreamOptions,
36
+ TextContent,
37
+ ThinkingBudgets,
38
+ ThinkingContent,
39
+ Tool,
40
+ ToolCall,
41
+ ToolResultMessage,
42
+ } from "../types";
43
+ import { normalizeToolCallId, resolveCacheRetention } from "../utils";
44
+ import { AssistantMessageEventStream } from "../utils/event-stream";
45
+ import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
46
+ import { parseStreamingJson } from "../utils/json-parse";
47
+ import { transformMessages } from "./transform-messages";
48
+
49
+ export interface BedrockOptions extends StreamOptions {
50
+ region?: string;
51
+ profile?: string;
52
+ toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
53
+ /* See https://docs.aws.amazon.com/bedrock/latest/userguide/inference-reasoning.html for supported models. */
54
+ reasoning?: Effort;
55
+ /* Custom token budgets per thinking level. Overrides default budgets. */
56
+ thinkingBudgets?: ThinkingBudgets;
57
+ /* Only supported by Claude 4.x models, see https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-extended-thinking.html#claude-messages-extended-thinking-tool-use-interleaved */
58
+ interleavedThinking?: boolean;
59
+ }
60
+
61
+ type Block = (TextContent | ThinkingContent | ToolCall) & { index?: number; partialJson?: string };
62
+
63
+ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
64
+ model: Model<"bedrock-converse-stream">,
65
+ context: Context,
66
+ options: BedrockOptions,
67
+ ): AssistantMessageEventStream => {
68
+ const stream = new AssistantMessageEventStream();
69
+
70
+ (async () => {
71
+ const startTime = Date.now();
72
+ let firstTokenTime: number | undefined;
73
+
74
+ const output: AssistantMessage = {
75
+ role: "assistant",
76
+ content: [],
77
+ api: "bedrock-converse-stream" as Api,
78
+ provider: model.provider,
79
+ model: model.id,
80
+ usage: {
81
+ input: 0,
82
+ output: 0,
83
+ cacheRead: 0,
84
+ cacheWrite: 0,
85
+ totalTokens: 0,
86
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
87
+ },
88
+ stopReason: "stop",
89
+ timestamp: Date.now(),
90
+ };
91
+
92
+ const blocks = output.content as Block[];
93
+ let rawRequestDump: RawHttpRequestDump | undefined;
94
+
95
+ const config: BedrockRuntimeClientConfig = {
96
+ region: options.region,
97
+ profile: options.profile,
98
+ };
99
+
100
+ // in Node.js/Bun environment only
101
+ if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
102
+ config.region = config.region || $env.AWS_REGION || $env.AWS_DEFAULT_REGION;
103
+
104
+ // Support proxies that don't need authentication
105
+ if ($flag("AWS_BEDROCK_SKIP_AUTH")) {
106
+ config.credentials = {
107
+ accessKeyId: "dummy-access-key",
108
+ secretAccessKey: "dummy-secret-key",
109
+ };
110
+ }
111
+
112
+ if ($flag("AWS_BEDROCK_FORCE_HTTP1")) {
113
+ config.requestHandler = new NodeHttpHandler();
114
+ }
115
+ }
116
+
117
+ config.region = config.region || "us-east-1";
118
+
119
+ try {
120
+ const client = new BedrockRuntimeClient(config);
121
+
122
+ const cacheRetention = resolveCacheRetention(options.cacheRetention);
123
+
124
+ const toolConfig = convertToolConfig(context.tools, options.toolChoice);
125
+ let additionalModelRequestFields = buildAdditionalModelRequestFields(model, options);
126
+
127
+ // Bedrock rejects thinking + forced tool_choice ("any" or specific tool).
128
+ // When tool_choice forces tool use, disable thinking to avoid API errors.
129
+ if (toolConfig?.toolChoice && additionalModelRequestFields) {
130
+ const tc = toolConfig.toolChoice;
131
+ if ("any" in tc || "tool" in tc) {
132
+ additionalModelRequestFields = undefined;
133
+ }
134
+ }
135
+
136
+ const commandInput = {
137
+ modelId: model.id,
138
+ messages: convertMessages(context, model, cacheRetention),
139
+ system: buildSystemPrompt(context.systemPrompt, model, cacheRetention),
140
+ inferenceConfig: { maxTokens: options.maxTokens, temperature: options.temperature, topP: options.topP },
141
+ toolConfig,
142
+ additionalModelRequestFields,
143
+ };
144
+ options?.onPayload?.(commandInput);
145
+ rawRequestDump = {
146
+ provider: model.provider,
147
+ api: output.api,
148
+ model: model.id,
149
+ method: "POST",
150
+ url: `https://bedrock-runtime.${config.region}.amazonaws.com/model/${model.id}/converse-stream`,
151
+ body: commandInput,
152
+ };
153
+ const command = new ConverseStreamCommand(commandInput);
154
+
155
+ const response = await client.send(command, { abortSignal: options.signal });
156
+
157
+ for await (const item of response.stream!) {
158
+ if (item.messageStart) {
159
+ if (item.messageStart.role !== ConversationRole.ASSISTANT) {
160
+ throw new Error("Unexpected assistant message start but got user message start instead");
161
+ }
162
+ stream.push({ type: "start", partial: output });
163
+ } else if (item.contentBlockStart) {
164
+ if (!firstTokenTime) firstTokenTime = Date.now();
165
+ handleContentBlockStart(item.contentBlockStart, blocks, output, stream);
166
+ } else if (item.contentBlockDelta) {
167
+ if (!firstTokenTime) firstTokenTime = Date.now();
168
+ handleContentBlockDelta(item.contentBlockDelta, blocks, output, stream);
169
+ } else if (item.contentBlockStop) {
170
+ handleContentBlockStop(item.contentBlockStop, blocks, output, stream);
171
+ } else if (item.messageStop) {
172
+ output.stopReason = mapStopReason(item.messageStop.stopReason);
173
+ } else if (item.metadata) {
174
+ handleMetadata(item.metadata, model, output);
175
+ } else if (item.internalServerException) {
176
+ throw new Error(`Internal server error: ${item.internalServerException.message}`);
177
+ } else if (item.modelStreamErrorException) {
178
+ throw new Error(`Model stream error: ${item.modelStreamErrorException.message}`);
179
+ } else if (item.validationException) {
180
+ throw withHttpStatus(new Error(`Validation error: ${item.validationException.message}`), 400);
181
+ } else if (item.throttlingException) {
182
+ throw new Error(`Throttling error: ${item.throttlingException.message}`);
183
+ } else if (item.serviceUnavailableException) {
184
+ throw new Error(`Service unavailable: ${item.serviceUnavailableException.message}`);
185
+ }
186
+ }
187
+
188
+ if (options.signal?.aborted) {
189
+ throw new Error("Request was aborted");
190
+ }
191
+
192
+ if (output.stopReason === "error" || output.stopReason === "aborted") {
193
+ throw new Error("An unknown error occurred");
194
+ }
195
+
196
+ output.duration = Date.now() - startTime;
197
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
198
+ stream.push({ type: "done", reason: output.stopReason, message: output });
199
+ stream.end();
200
+ } catch (error) {
201
+ for (const block of output.content) {
202
+ delete (block as Block).index;
203
+ delete (block as Block).partialJson;
204
+ }
205
+ output.stopReason = options.signal?.aborted ? "aborted" : "error";
206
+ const baseMessage = error instanceof Error ? error.message : JSON.stringify(error);
207
+ // Enrich error with thinking block diagnostics for signature-related failures
208
+ let diagnostics = "";
209
+ if (baseMessage.includes("signature") || baseMessage.includes("thinking")) {
210
+ const thinkingBlocks = context.messages
211
+ .filter((m): m is AssistantMessage => m.role === "assistant")
212
+ .flatMap((m, mi) =>
213
+ m.content
214
+ .filter(b => b.type === "thinking")
215
+ .map((b, bi) => ({
216
+ msg: mi,
217
+ block: bi,
218
+ stop: m.stopReason,
219
+ sigLen: b.thinkingSignature?.length ?? -1,
220
+ thinkLen: b.thinking.length,
221
+ })),
222
+ );
223
+ if (thinkingBlocks.length > 0) {
224
+ diagnostics = `\n[thinking-diag] ${JSON.stringify(thinkingBlocks)}`;
225
+ }
226
+ }
227
+ output.errorMessage = await appendRawHttpRequestDumpFor400(baseMessage + diagnostics, error, rawRequestDump);
228
+ output.duration = Date.now() - startTime;
229
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
230
+ stream.push({ type: "error", reason: output.stopReason, error: output });
231
+ stream.end();
232
+ }
233
+ })();
234
+
235
+ return stream;
236
+ };
237
+
238
+ function handleContentBlockStart(
239
+ event: ContentBlockStartEvent,
240
+ blocks: Block[],
241
+ output: AssistantMessage,
242
+ stream: AssistantMessageEventStream,
243
+ ): void {
244
+ const index = event.contentBlockIndex!;
245
+ const start = event.start;
246
+
247
+ if (start?.toolUse) {
248
+ const block: Block = {
249
+ type: "toolCall",
250
+ id: normalizeToolCallId(start.toolUse.toolUseId || ""),
251
+ name: start.toolUse.name || "",
252
+ arguments: {},
253
+ partialJson: "",
254
+ index,
255
+ };
256
+ output.content.push(block);
257
+ stream.push({ type: "toolcall_start", contentIndex: blocks.length - 1, partial: output });
258
+ }
259
+ }
260
+
261
+ function handleContentBlockDelta(
262
+ event: ContentBlockDeltaEvent,
263
+ blocks: Block[],
264
+ output: AssistantMessage,
265
+ stream: AssistantMessageEventStream,
266
+ ): void {
267
+ const contentBlockIndex = event.contentBlockIndex!;
268
+ const delta = event.delta;
269
+ let index = blocks.findIndex(b => b.index === contentBlockIndex);
270
+ let block = blocks[index];
271
+
272
+ if (delta?.text !== undefined) {
273
+ // If no text block exists yet, create one, as `handleContentBlockStart` is not sent for text blocks
274
+ if (!block) {
275
+ const newBlock: Block = { type: "text", text: "", index: contentBlockIndex };
276
+ output.content.push(newBlock);
277
+ index = blocks.length - 1;
278
+ block = blocks[index];
279
+ stream.push({ type: "text_start", contentIndex: index, partial: output });
280
+ }
281
+ if (block.type === "text") {
282
+ block.text += delta.text;
283
+ stream.push({ type: "text_delta", contentIndex: index, delta: delta.text, partial: output });
284
+ }
285
+ } else if (delta?.toolUse && block?.type === "toolCall") {
286
+ block.partialJson = (block.partialJson || "") + (delta.toolUse.input || "");
287
+ block.arguments = parseStreamingJson(block.partialJson);
288
+ stream.push({ type: "toolcall_delta", contentIndex: index, delta: delta.toolUse.input || "", partial: output });
289
+ } else if (delta?.reasoningContent) {
290
+ let thinkingBlock = block;
291
+ let thinkingIndex = index;
292
+
293
+ if (!thinkingBlock) {
294
+ const newBlock: Block = { type: "thinking", thinking: "", thinkingSignature: "", index: contentBlockIndex };
295
+ output.content.push(newBlock);
296
+ thinkingIndex = blocks.length - 1;
297
+ thinkingBlock = blocks[thinkingIndex];
298
+ stream.push({ type: "thinking_start", contentIndex: thinkingIndex, partial: output });
299
+ }
300
+
301
+ if (thinkingBlock?.type === "thinking") {
302
+ if (delta.reasoningContent.text) {
303
+ thinkingBlock.thinking += delta.reasoningContent.text;
304
+ stream.push({
305
+ type: "thinking_delta",
306
+ contentIndex: thinkingIndex,
307
+ delta: delta.reasoningContent.text,
308
+ partial: output,
309
+ });
310
+ }
311
+ if (delta.reasoningContent.signature) {
312
+ thinkingBlock.thinkingSignature =
313
+ (thinkingBlock.thinkingSignature || "") + delta.reasoningContent.signature;
314
+ }
315
+ }
316
+ }
317
+ }
318
+
319
+ function handleMetadata(
320
+ event: ConverseStreamMetadataEvent,
321
+ model: Model<"bedrock-converse-stream">,
322
+ output: AssistantMessage,
323
+ ): void {
324
+ if (event.usage) {
325
+ output.usage.input = event.usage.inputTokens || 0;
326
+ output.usage.output = event.usage.outputTokens || 0;
327
+ output.usage.cacheRead = event.usage.cacheReadInputTokens || 0;
328
+ output.usage.cacheWrite = event.usage.cacheWriteInputTokens || 0;
329
+ output.usage.totalTokens = event.usage.totalTokens || output.usage.input + output.usage.output;
330
+ calculateCost(model, output.usage);
331
+ }
332
+ }
333
+
334
+ function handleContentBlockStop(
335
+ event: ContentBlockStopEvent,
336
+ blocks: Block[],
337
+ output: AssistantMessage,
338
+ stream: AssistantMessageEventStream,
339
+ ): void {
340
+ const index = blocks.findIndex(b => b.index === event.contentBlockIndex);
341
+ const block = blocks[index];
342
+ if (!block) return;
343
+ delete (block as Block).index;
344
+
345
+ switch (block.type) {
346
+ case "text":
347
+ stream.push({ type: "text_end", contentIndex: index, content: block.text, partial: output });
348
+ break;
349
+ case "thinking":
350
+ stream.push({ type: "thinking_end", contentIndex: index, content: block.thinking, partial: output });
351
+ break;
352
+ case "toolCall":
353
+ block.arguments = parseStreamingJson(block.partialJson);
354
+ delete (block as Block).partialJson;
355
+ stream.push({ type: "toolcall_end", contentIndex: index, toolCall: block, partial: output });
356
+ break;
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Check if the model supports prompt caching.
362
+ * Supported: Claude 3.5 Haiku, Claude 3.7 Sonnet, Claude 4.x+ models, Haiku 4.5+
363
+ *
364
+ * For base models and system-defined inference profiles the model ID / ARN
365
+ * contains the model name, so we can decide locally.
366
+ *
367
+ * For application inference profiles (whose ARNs don't contain the model name),
368
+ * set AWS_BEDROCK_FORCE_CACHE=1 to enable cache points. Amazon Nova models
369
+ * have automatic caching and don't need explicit cache points.
370
+ */
371
+ function supportsPromptCaching(model: Model<"bedrock-converse-stream">): boolean {
372
+ if (model.cost.cacheRead || model.cost.cacheWrite) return true;
373
+ const id = model.id.toLowerCase();
374
+ // Claude 4.x models (opus-4, sonnet-4, haiku-4)
375
+ if (id.includes("claude") && (id.includes("-4-") || id.includes("-4."))) return true;
376
+ // Claude 3.5 Haiku, Claude 3.7 Sonnet (legacy naming)
377
+ if (id.includes("claude-3-7-sonnet") || id.includes("claude-3-5-haiku")) return true;
378
+ // Claude Haiku 4.5+ (new naming)
379
+ if (id.includes("claude-haiku")) return true;
380
+ // Application inference profiles don't contain the model name in the ARN.
381
+ // Allow users to force cache points via environment variable.
382
+ if (typeof process !== "undefined" && $flag("AWS_BEDROCK_FORCE_CACHE")) return true;
383
+ return false;
384
+ }
385
+
386
+ /**
387
+ * Check if the model supports thinking signatures in reasoningContent.
388
+ * Only Anthropic Claude models support the signature field.
389
+ * Other models (Nova, Titan, Mistral, Llama, etc.) reject it with:
390
+ * "This model doesn't support the reasoningContent.reasoningText.signature field"
391
+ */
392
+ function supportsThinkingSignature(model: Model<"bedrock-converse-stream">): boolean {
393
+ const id = model.id.toLowerCase();
394
+ return id.includes("anthropic.claude") || id.includes("anthropic/claude");
395
+ }
396
+
397
+ function buildSystemPrompt(
398
+ systemPrompt: string | undefined,
399
+ model: Model<"bedrock-converse-stream">,
400
+ cacheRetention: CacheRetention,
401
+ ): SystemContentBlock[] | undefined {
402
+ if (!systemPrompt) return undefined;
403
+
404
+ const blocks: SystemContentBlock[] = [{ text: systemPrompt.toWellFormed() }];
405
+
406
+ // Add cache point for supported Claude models
407
+ if (cacheRetention !== "none" && supportsPromptCaching(model)) {
408
+ blocks.push({
409
+ cachePoint: { type: CachePointType.DEFAULT, ...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}) },
410
+ });
411
+ }
412
+
413
+ return blocks;
414
+ }
415
+
416
+ function convertMessages(
417
+ context: Context,
418
+ model: Model<"bedrock-converse-stream">,
419
+ cacheRetention: CacheRetention,
420
+ ): Message[] {
421
+ const result: Message[] = [];
422
+ const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
423
+
424
+ for (let i = 0; i < transformedMessages.length; i++) {
425
+ const m = transformedMessages[i];
426
+
427
+ switch (m.role) {
428
+ case "developer":
429
+ case "user":
430
+ if (typeof m.content === "string") {
431
+ // Skip empty user messages
432
+ if (!m.content || m.content.trim() === "") continue;
433
+ result.push({
434
+ role: ConversationRole.USER,
435
+ content: [{ text: m.content.toWellFormed() }],
436
+ });
437
+ } else {
438
+ const contentBlocks = m.content
439
+ .map(c => {
440
+ switch (c.type) {
441
+ case "text":
442
+ return { text: c.text.toWellFormed() };
443
+ case "image":
444
+ return { image: createImageBlock(c.mimeType, c.data) };
445
+ default:
446
+ throw new Error("Unknown user content type");
447
+ }
448
+ })
449
+ .filter(block => {
450
+ // Filter out empty text blocks
451
+ if ("text" in block && block.text) {
452
+ return block.text.trim().length > 0;
453
+ }
454
+ return true; // Keep non-text blocks (images)
455
+ });
456
+ // Skip message if all blocks filtered out
457
+ if (contentBlocks.length === 0) continue;
458
+ result.push({
459
+ role: ConversationRole.USER,
460
+ content: contentBlocks,
461
+ });
462
+ }
463
+ break;
464
+ case "assistant": {
465
+ // Skip assistant messages with empty content (e.g., from aborted requests)
466
+ // Bedrock rejects messages with empty content arrays
467
+ if (m.content.length === 0) {
468
+ continue;
469
+ }
470
+ const contentBlocks: ContentBlock[] = [];
471
+ for (const c of m.content) {
472
+ switch (c.type) {
473
+ case "text":
474
+ // Skip empty text blocks
475
+ if (c.text.trim().length === 0) continue;
476
+ contentBlocks.push({ text: c.text.toWellFormed() });
477
+ break;
478
+ case "toolCall":
479
+ contentBlocks.push({
480
+ toolUse: {
481
+ toolUseId: normalizeToolCallId(c.id),
482
+ name: c.name,
483
+ input: c.arguments,
484
+ },
485
+ });
486
+ break;
487
+ case "thinking":
488
+ // Skip empty thinking blocks
489
+ if (c.thinking.trim().length === 0) continue;
490
+ // Thinking blocks require a valid signature when sent as reasoningContent.
491
+ // If the signature is missing (e.g., from an aborted stream), or the model
492
+ // doesn't support signatures, convert to plain text instead.
493
+ if (supportsThinkingSignature(model) && c.thinkingSignature) {
494
+ contentBlocks.push({
495
+ reasoningContent: {
496
+ reasoningText: { text: c.thinking.toWellFormed(), signature: c.thinkingSignature },
497
+ },
498
+ });
499
+ } else if (!supportsThinkingSignature(model)) {
500
+ // Model doesn't support signatures at all — send as unsigned reasoning
501
+ contentBlocks.push({
502
+ reasoningContent: {
503
+ reasoningText: { text: c.thinking.toWellFormed() },
504
+ },
505
+ });
506
+ } else {
507
+ // Model requires signature but we don't have one — demote to text
508
+ contentBlocks.push({ text: `[Thinking]: ${c.thinking.toWellFormed()}` });
509
+ }
510
+ break;
511
+ default:
512
+ throw new Error("Unknown assistant content type");
513
+ }
514
+ }
515
+ // Skip if all content blocks were filtered out
516
+ if (contentBlocks.length === 0) {
517
+ continue;
518
+ }
519
+ result.push({
520
+ role: ConversationRole.ASSISTANT,
521
+ content: contentBlocks,
522
+ });
523
+ break;
524
+ }
525
+ case "toolResult": {
526
+ // Collect all consecutive toolResult messages into a single user message
527
+ // Bedrock requires all tool results to be in one message
528
+ const toolResults: ContentBlock.ToolResultMember[] = [];
529
+
530
+ // Add current tool result with all content blocks combined
531
+ toolResults.push({
532
+ toolResult: {
533
+ toolUseId: normalizeToolCallId(m.toolCallId),
534
+ content: m.content.map(c =>
535
+ c.type === "image"
536
+ ? { image: createImageBlock(c.mimeType, c.data) }
537
+ : { text: c.text.toWellFormed() },
538
+ ),
539
+ status: m.isError ? ToolResultStatus.ERROR : ToolResultStatus.SUCCESS,
540
+ },
541
+ });
542
+
543
+ // Look ahead for consecutive toolResult messages
544
+ let j = i + 1;
545
+ while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
546
+ const nextMsg = transformedMessages[j] as ToolResultMessage;
547
+ toolResults.push({
548
+ toolResult: {
549
+ toolUseId: normalizeToolCallId(nextMsg.toolCallId),
550
+ content: nextMsg.content.map(c =>
551
+ c.type === "image"
552
+ ? { image: createImageBlock(c.mimeType, c.data) }
553
+ : { text: c.text.toWellFormed() },
554
+ ),
555
+ status: nextMsg.isError ? ToolResultStatus.ERROR : ToolResultStatus.SUCCESS,
556
+ },
557
+ });
558
+ j++;
559
+ }
560
+
561
+ // Skip the messages we've already processed
562
+ i = j - 1;
563
+
564
+ result.push({
565
+ role: ConversationRole.USER,
566
+ content: toolResults,
567
+ });
568
+ break;
569
+ }
570
+ default:
571
+ throw new Error("Unknown message role");
572
+ }
573
+ }
574
+
575
+ // Add cache point to the last user message for supported Claude models
576
+ if (cacheRetention !== "none" && supportsPromptCaching(model) && result.length > 0) {
577
+ const lastMessage = result[result.length - 1];
578
+ if (lastMessage.role === ConversationRole.USER && lastMessage.content) {
579
+ (lastMessage.content as ContentBlock[]).push({
580
+ cachePoint: {
581
+ type: CachePointType.DEFAULT,
582
+ ...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}),
583
+ },
584
+ });
585
+ }
586
+ }
587
+
588
+ return result;
589
+ }
590
+
591
+ function convertToolConfig(
592
+ tools: Tool[] | undefined,
593
+ toolChoice: BedrockOptions["toolChoice"],
594
+ ): ToolConfiguration | undefined {
595
+ if (!tools?.length || toolChoice === "none") return undefined;
596
+
597
+ const bedrockTools: BedrockTool[] = tools.map(tool => ({
598
+ toolSpec: {
599
+ name: tool.name,
600
+ description: tool.description || "",
601
+ inputSchema: { json: tool.parameters },
602
+ },
603
+ }));
604
+
605
+ let bedrockToolChoice: ToolChoice | undefined;
606
+ switch (toolChoice) {
607
+ case "auto":
608
+ bedrockToolChoice = { auto: {} };
609
+ break;
610
+ case "any":
611
+ bedrockToolChoice = { any: {} };
612
+ break;
613
+ default:
614
+ if (toolChoice?.type === "tool") {
615
+ bedrockToolChoice = { tool: { name: toolChoice.name } };
616
+ }
617
+ }
618
+
619
+ return { tools: bedrockTools, toolChoice: bedrockToolChoice };
620
+ }
621
+
622
+ function mapStopReason(reason: string | undefined): StopReason {
623
+ switch (reason) {
624
+ case BedrockStopReason.END_TURN:
625
+ case BedrockStopReason.STOP_SEQUENCE:
626
+ return "stop";
627
+ case BedrockStopReason.MAX_TOKENS:
628
+ case BedrockStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED:
629
+ return "length";
630
+ case BedrockStopReason.TOOL_USE:
631
+ return "toolUse";
632
+ default:
633
+ return "error";
634
+ }
635
+ }
636
+
637
+ function buildAdditionalModelRequestFields(
638
+ model: Model<"bedrock-converse-stream">,
639
+ options: BedrockOptions,
640
+ ): Record<string, any> | undefined {
641
+ const reasoning = options.reasoning;
642
+ if (!reasoning || !model.reasoning) {
643
+ return undefined;
644
+ }
645
+
646
+ const mode = model.thinking?.mode;
647
+ if (mode === "anthropic-adaptive") {
648
+ const effort = mapEffortToAnthropicAdaptiveEffort(model, reasoning);
649
+ return {
650
+ thinking: { type: "adaptive" },
651
+ output_config: { effort },
652
+ };
653
+ }
654
+
655
+ const level = requireSupportedEffort(model, reasoning);
656
+ const defaultBudgets: Record<Effort, number> = {
657
+ minimal: 1024,
658
+ low: 2048,
659
+ medium: 8192,
660
+ high: 16384,
661
+ xhigh: 32768,
662
+ };
663
+ const budget = options.thinkingBudgets?.[level] ?? defaultBudgets[level];
664
+
665
+ const result: Record<string, any> = {
666
+ thinking: {
667
+ type: "enabled",
668
+ budget_tokens: budget,
669
+ },
670
+ };
671
+
672
+ if (options.interleavedThinking) {
673
+ result.anthropic_beta = ["interleaved-thinking-2025-05-14"];
674
+ }
675
+
676
+ return result;
677
+ }
678
+
679
+ function createImageBlock(mimeType: string, data: string) {
680
+ let format: ImageFormat;
681
+ switch (mimeType) {
682
+ case "image/jpeg":
683
+ case "image/jpg":
684
+ format = ImageFormat.JPEG;
685
+ break;
686
+ case "image/png":
687
+ format = ImageFormat.PNG;
688
+ break;
689
+ case "image/gif":
690
+ format = ImageFormat.GIF;
691
+ break;
692
+ case "image/webp":
693
+ format = ImageFormat.WEBP;
694
+ break;
695
+ default:
696
+ throw new Error(`Unknown image type: ${mimeType}`);
697
+ }
698
+
699
+ const binaryString = atob(data);
700
+ const bytes = new Uint8Array(binaryString.length);
701
+ for (let i = 0; i < binaryString.length; i++) {
702
+ bytes[i] = binaryString.charCodeAt(i);
703
+ }
704
+
705
+ return { source: { bytes }, format };
706
+ }