@asm-agent/ai 0.8.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 (210) hide show
  1. package/README.md +1333 -0
  2. package/dist/api-registry.d.ts +20 -0
  3. package/dist/api-registry.d.ts.map +1 -0
  4. package/dist/api-registry.js +44 -0
  5. package/dist/api-registry.js.map +1 -0
  6. package/dist/bedrock-provider.d.ts +5 -0
  7. package/dist/bedrock-provider.d.ts.map +1 -0
  8. package/dist/bedrock-provider.js +6 -0
  9. package/dist/bedrock-provider.js.map +1 -0
  10. package/dist/cache-pricing.d.ts +13 -0
  11. package/dist/cache-pricing.d.ts.map +1 -0
  12. package/dist/cache-pricing.js +38 -0
  13. package/dist/cache-pricing.js.map +1 -0
  14. package/dist/cli.d.ts +3 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +116 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/env-api-keys.d.ts +19 -0
  19. package/dist/env-api-keys.d.ts.map +1 -0
  20. package/dist/env-api-keys.js +180 -0
  21. package/dist/env-api-keys.js.map +1 -0
  22. package/dist/index.d.ts +30 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +18 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/log.d.ts +27 -0
  27. package/dist/log.d.ts.map +1 -0
  28. package/dist/log.js +69 -0
  29. package/dist/log.js.map +1 -0
  30. package/dist/mcp/catalog.d.ts +19 -0
  31. package/dist/mcp/catalog.d.ts.map +1 -0
  32. package/dist/mcp/catalog.js +42 -0
  33. package/dist/mcp/catalog.js.map +1 -0
  34. package/dist/mcp/index.d.ts +5 -0
  35. package/dist/mcp/index.d.ts.map +1 -0
  36. package/dist/mcp/index.js +3 -0
  37. package/dist/mcp/index.js.map +1 -0
  38. package/dist/mcp/oauth.d.ts +15 -0
  39. package/dist/mcp/oauth.d.ts.map +1 -0
  40. package/dist/mcp/oauth.js +489 -0
  41. package/dist/mcp/oauth.js.map +1 -0
  42. package/dist/mcp.d.ts +2 -0
  43. package/dist/mcp.d.ts.map +1 -0
  44. package/dist/mcp.js +2 -0
  45. package/dist/mcp.js.map +1 -0
  46. package/dist/models.d.ts +18 -0
  47. package/dist/models.d.ts.map +1 -0
  48. package/dist/models.generated.d.ts +26696 -0
  49. package/dist/models.generated.d.ts.map +1 -0
  50. package/dist/models.generated.js +22192 -0
  51. package/dist/models.generated.js.map +1 -0
  52. package/dist/models.js +72 -0
  53. package/dist/models.js.map +1 -0
  54. package/dist/oauth.d.ts +2 -0
  55. package/dist/oauth.d.ts.map +1 -0
  56. package/dist/oauth.js +2 -0
  57. package/dist/oauth.js.map +1 -0
  58. package/dist/openrouter-reasoning.d.ts +11 -0
  59. package/dist/openrouter-reasoning.d.ts.map +1 -0
  60. package/dist/openrouter-reasoning.js +52 -0
  61. package/dist/openrouter-reasoning.js.map +1 -0
  62. package/dist/providers/amazon-bedrock.d.ts +38 -0
  63. package/dist/providers/amazon-bedrock.d.ts.map +1 -0
  64. package/dist/providers/amazon-bedrock.js +767 -0
  65. package/dist/providers/amazon-bedrock.js.map +1 -0
  66. package/dist/providers/anthropic.d.ts +54 -0
  67. package/dist/providers/anthropic.d.ts.map +1 -0
  68. package/dist/providers/anthropic.js +999 -0
  69. package/dist/providers/anthropic.js.map +1 -0
  70. package/dist/providers/azure-openai-responses.d.ts +12 -0
  71. package/dist/providers/azure-openai-responses.d.ts.map +1 -0
  72. package/dist/providers/azure-openai-responses.js +206 -0
  73. package/dist/providers/azure-openai-responses.js.map +1 -0
  74. package/dist/providers/cloudflare.d.ts +10 -0
  75. package/dist/providers/cloudflare.d.ts.map +1 -0
  76. package/dist/providers/cloudflare.js +23 -0
  77. package/dist/providers/cloudflare.js.map +1 -0
  78. package/dist/providers/faux.d.ts +56 -0
  79. package/dist/providers/faux.d.ts.map +1 -0
  80. package/dist/providers/faux.js +368 -0
  81. package/dist/providers/faux.js.map +1 -0
  82. package/dist/providers/github-copilot-headers.d.ts +8 -0
  83. package/dist/providers/github-copilot-headers.d.ts.map +1 -0
  84. package/dist/providers/github-copilot-headers.js +26 -0
  85. package/dist/providers/github-copilot-headers.js.map +1 -0
  86. package/dist/providers/google-shared.d.ts +55 -0
  87. package/dist/providers/google-shared.d.ts.map +1 -0
  88. package/dist/providers/google-shared.js +312 -0
  89. package/dist/providers/google-shared.js.map +1 -0
  90. package/dist/providers/google-vertex.d.ts +15 -0
  91. package/dist/providers/google-vertex.d.ts.map +1 -0
  92. package/dist/providers/google-vertex.js +421 -0
  93. package/dist/providers/google-vertex.js.map +1 -0
  94. package/dist/providers/google.d.ts +13 -0
  95. package/dist/providers/google.d.ts.map +1 -0
  96. package/dist/providers/google.js +370 -0
  97. package/dist/providers/google.js.map +1 -0
  98. package/dist/providers/mistral.d.ts +20 -0
  99. package/dist/providers/mistral.d.ts.map +1 -0
  100. package/dist/providers/mistral.js +536 -0
  101. package/dist/providers/mistral.js.map +1 -0
  102. package/dist/providers/openai-codex-responses.d.ts +30 -0
  103. package/dist/providers/openai-codex-responses.d.ts.map +1 -0
  104. package/dist/providers/openai-codex-responses.js +1013 -0
  105. package/dist/providers/openai-codex-responses.js.map +1 -0
  106. package/dist/providers/openai-completions.d.ts +21 -0
  107. package/dist/providers/openai-completions.d.ts.map +1 -0
  108. package/dist/providers/openai-completions.js +1019 -0
  109. package/dist/providers/openai-completions.js.map +1 -0
  110. package/dist/providers/openai-responses-shared.d.ts +18 -0
  111. package/dist/providers/openai-responses-shared.d.ts.map +1 -0
  112. package/dist/providers/openai-responses-shared.js +488 -0
  113. package/dist/providers/openai-responses-shared.js.map +1 -0
  114. package/dist/providers/openai-responses.d.ts +10 -0
  115. package/dist/providers/openai-responses.d.ts.map +1 -0
  116. package/dist/providers/openai-responses.js +218 -0
  117. package/dist/providers/openai-responses.js.map +1 -0
  118. package/dist/providers/register-builtins.d.ts +35 -0
  119. package/dist/providers/register-builtins.d.ts.map +1 -0
  120. package/dist/providers/register-builtins.js +243 -0
  121. package/dist/providers/register-builtins.js.map +1 -0
  122. package/dist/providers/simple-options.d.ts +8 -0
  123. package/dist/providers/simple-options.d.ts.map +1 -0
  124. package/dist/providers/simple-options.js +40 -0
  125. package/dist/providers/simple-options.js.map +1 -0
  126. package/dist/providers/transform-messages.d.ts +8 -0
  127. package/dist/providers/transform-messages.d.ts.map +1 -0
  128. package/dist/providers/transform-messages.js +174 -0
  129. package/dist/providers/transform-messages.js.map +1 -0
  130. package/dist/session-resources.d.ts +4 -0
  131. package/dist/session-resources.d.ts.map +1 -0
  132. package/dist/session-resources.js +22 -0
  133. package/dist/session-resources.js.map +1 -0
  134. package/dist/stream.d.ts +8 -0
  135. package/dist/stream.d.ts.map +1 -0
  136. package/dist/stream.js +27 -0
  137. package/dist/stream.js.map +1 -0
  138. package/dist/types.d.ts +411 -0
  139. package/dist/types.d.ts.map +1 -0
  140. package/dist/types.js +2 -0
  141. package/dist/types.js.map +1 -0
  142. package/dist/utils/diagnostics.d.ts +19 -0
  143. package/dist/utils/diagnostics.d.ts.map +1 -0
  144. package/dist/utils/diagnostics.js +25 -0
  145. package/dist/utils/diagnostics.js.map +1 -0
  146. package/dist/utils/event-stream.d.ts +21 -0
  147. package/dist/utils/event-stream.d.ts.map +1 -0
  148. package/dist/utils/event-stream.js +78 -0
  149. package/dist/utils/event-stream.js.map +1 -0
  150. package/dist/utils/hash.d.ts +2 -0
  151. package/dist/utils/hash.d.ts.map +1 -0
  152. package/dist/utils/hash.js +13 -0
  153. package/dist/utils/hash.js.map +1 -0
  154. package/dist/utils/headers.d.ts +2 -0
  155. package/dist/utils/headers.d.ts.map +1 -0
  156. package/dist/utils/headers.js +8 -0
  157. package/dist/utils/headers.js.map +1 -0
  158. package/dist/utils/json-parse.d.ts +16 -0
  159. package/dist/utils/json-parse.d.ts.map +1 -0
  160. package/dist/utils/json-parse.js +113 -0
  161. package/dist/utils/json-parse.js.map +1 -0
  162. package/dist/utils/oauth/anthropic.d.ts +25 -0
  163. package/dist/utils/oauth/anthropic.d.ts.map +1 -0
  164. package/dist/utils/oauth/anthropic.js +335 -0
  165. package/dist/utils/oauth/anthropic.js.map +1 -0
  166. package/dist/utils/oauth/github-copilot.d.ts +24 -0
  167. package/dist/utils/oauth/github-copilot.d.ts.map +1 -0
  168. package/dist/utils/oauth/github-copilot.js +279 -0
  169. package/dist/utils/oauth/github-copilot.js.map +1 -0
  170. package/dist/utils/oauth/index.d.ts +45 -0
  171. package/dist/utils/oauth/index.d.ts.map +1 -0
  172. package/dist/utils/oauth/index.js +99 -0
  173. package/dist/utils/oauth/index.js.map +1 -0
  174. package/dist/utils/oauth/oauth-page.d.ts +3 -0
  175. package/dist/utils/oauth/oauth-page.d.ts.map +1 -0
  176. package/dist/utils/oauth/oauth-page.js +134 -0
  177. package/dist/utils/oauth/oauth-page.js.map +1 -0
  178. package/dist/utils/oauth/openai-codex.d.ts +34 -0
  179. package/dist/utils/oauth/openai-codex.d.ts.map +1 -0
  180. package/dist/utils/oauth/openai-codex.js +385 -0
  181. package/dist/utils/oauth/openai-codex.js.map +1 -0
  182. package/dist/utils/oauth/pkce.d.ts +13 -0
  183. package/dist/utils/oauth/pkce.d.ts.map +1 -0
  184. package/dist/utils/oauth/pkce.js +26 -0
  185. package/dist/utils/oauth/pkce.js.map +1 -0
  186. package/dist/utils/oauth/types.d.ts +57 -0
  187. package/dist/utils/oauth/types.d.ts.map +1 -0
  188. package/dist/utils/oauth/types.js +2 -0
  189. package/dist/utils/oauth/types.js.map +1 -0
  190. package/dist/utils/overflow.d.ts +55 -0
  191. package/dist/utils/overflow.d.ts.map +1 -0
  192. package/dist/utils/overflow.js +144 -0
  193. package/dist/utils/overflow.js.map +1 -0
  194. package/dist/utils/sanitize-unicode.d.ts +22 -0
  195. package/dist/utils/sanitize-unicode.d.ts.map +1 -0
  196. package/dist/utils/sanitize-unicode.js +26 -0
  197. package/dist/utils/sanitize-unicode.js.map +1 -0
  198. package/dist/utils/stream-failure.d.ts +55 -0
  199. package/dist/utils/stream-failure.d.ts.map +1 -0
  200. package/dist/utils/stream-failure.js +179 -0
  201. package/dist/utils/stream-failure.js.map +1 -0
  202. package/dist/utils/typebox-helpers.d.ts +17 -0
  203. package/dist/utils/typebox-helpers.d.ts.map +1 -0
  204. package/dist/utils/typebox-helpers.js +21 -0
  205. package/dist/utils/typebox-helpers.js.map +1 -0
  206. package/dist/utils/validation.d.ts +18 -0
  207. package/dist/utils/validation.d.ts.map +1 -0
  208. package/dist/utils/validation.js +281 -0
  209. package/dist/utils/validation.js.map +1 -0
  210. package/package.json +113 -0
@@ -0,0 +1,1019 @@
1
+ import OpenAI from "openai";
2
+ import { getAnthropicCacheWriteCost, hasStandardAnthropicCachePricing } from "../cache-pricing.js";
3
+ import { getEnvApiKey, getPrimeTeamId } from "../env-api-keys.js";
4
+ import { calculateCost, clampThinkingLevel } from "../models.js";
5
+ import { AssistantMessageEventStream } from "../utils/event-stream.js";
6
+ import { headersToRecord } from "../utils/headers.js";
7
+ import { parseStreamingJson } from "../utils/json-parse.js";
8
+ import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
9
+ import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js";
10
+ import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
11
+ import { buildBaseOptions } from "./simple-options.js";
12
+ import { transformMessages } from "./transform-messages.js";
13
+ /**
14
+ * Check if conversation messages contain tool calls or tool results.
15
+ * This is needed because Anthropic (via proxy) requires the tools param
16
+ * to be present when messages include tool_calls or tool role messages.
17
+ */
18
+ function hasToolHistory(messages) {
19
+ for (const msg of messages) {
20
+ if (msg.role === "toolResult") {
21
+ return true;
22
+ }
23
+ if (msg.role === "assistant") {
24
+ if (msg.content.some((block) => block.type === "toolCall")) {
25
+ return true;
26
+ }
27
+ }
28
+ }
29
+ return false;
30
+ }
31
+ function isTextContentBlock(block) {
32
+ return block.type === "text";
33
+ }
34
+ function isThinkingContentBlock(block) {
35
+ return block.type === "thinking";
36
+ }
37
+ function isToolCallBlock(block) {
38
+ return block.type === "toolCall";
39
+ }
40
+ function isImageContentBlock(block) {
41
+ return block.type === "image";
42
+ }
43
+ const REASONING_DETAILS_SIGNATURE_TYPE = "openai-completions.reasoning_details.v1";
44
+ function encodeReasoningDetails(details) {
45
+ return JSON.stringify({ type: REASONING_DETAILS_SIGNATURE_TYPE, details });
46
+ }
47
+ function decodeReasoningDetails(signature) {
48
+ if (!signature?.startsWith("{"))
49
+ return undefined;
50
+ try {
51
+ const parsed = JSON.parse(signature);
52
+ if (parsed.type !== REASONING_DETAILS_SIGNATURE_TYPE || !Array.isArray(parsed.details))
53
+ return undefined;
54
+ if (parsed.details.some((detail) => !detail || typeof detail !== "object" || Array.isArray(detail))) {
55
+ return undefined;
56
+ }
57
+ return parsed.details;
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
63
+ function resolveCacheRetention(cacheRetention) {
64
+ if (cacheRetention) {
65
+ return cacheRetention;
66
+ }
67
+ if (typeof process !== "undefined" && process.env.PI_CACHE_RETENTION === "long") {
68
+ return "long";
69
+ }
70
+ return "short";
71
+ }
72
+ export const streamOpenAICompletions = (model, context, options) => {
73
+ const stream = new AssistantMessageEventStream();
74
+ (async () => {
75
+ const output = {
76
+ role: "assistant",
77
+ content: [],
78
+ api: model.api,
79
+ provider: model.provider,
80
+ model: model.id,
81
+ usage: {
82
+ input: 0,
83
+ output: 0,
84
+ cacheRead: 0,
85
+ cacheWrite: 0,
86
+ totalTokens: 0,
87
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
88
+ },
89
+ stopReason: "stop",
90
+ timestamp: Date.now(),
91
+ };
92
+ try {
93
+ const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
94
+ const compat = getCompat(model);
95
+ const cacheRetention = resolveCacheRetention(options?.cacheRetention);
96
+ const cacheControl = getCompatCacheControl(compat, cacheRetention);
97
+ const cacheWriteCost = cacheControl && hasStandardAnthropicCachePricing(model)
98
+ ? getAnthropicCacheWriteCost(model.cost.input, cacheControl.ttl === "1h" ? "1h" : "5m")
99
+ : undefined;
100
+ const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
101
+ const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, compat);
102
+ let params = buildParams(model, context, options, compat, cacheRetention, cacheControl);
103
+ const nextParams = await options?.onPayload?.(params, model);
104
+ if (nextParams !== undefined) {
105
+ params = nextParams;
106
+ }
107
+ const requestOptions = {
108
+ ...(options?.signal ? { signal: options.signal } : {}),
109
+ ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
110
+ ...(options?.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}),
111
+ };
112
+ const { data: openaiStream, response } = await client.chat.completions
113
+ .create(params, requestOptions)
114
+ .withResponse();
115
+ await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
116
+ stream.push({ type: "start", partial: output });
117
+ let textBlock = null;
118
+ let thinkingBlock = null;
119
+ const toolCallBlocksByIndex = new Map();
120
+ const toolCallBlocksById = new Map();
121
+ const reasoningDetailsByIndex = new Map();
122
+ let nextReasoningDetailsIndex = 0;
123
+ let reasoningDetailsBlock = null;
124
+ const blocks = output.content;
125
+ const getContentIndex = (block) => blocks.indexOf(block);
126
+ const finishBlock = (block) => {
127
+ const contentIndex = getContentIndex(block);
128
+ if (contentIndex === -1) {
129
+ return;
130
+ }
131
+ if (block.type === "text") {
132
+ stream.push({
133
+ type: "text_end",
134
+ contentIndex,
135
+ content: block.text,
136
+ partial: output,
137
+ });
138
+ }
139
+ else if (block.type === "thinking") {
140
+ stream.push({
141
+ type: "thinking_end",
142
+ contentIndex,
143
+ content: block.thinking,
144
+ partial: output,
145
+ });
146
+ }
147
+ else if (block.type === "toolCall") {
148
+ block.arguments = parseStreamingJson(block.partialArgs);
149
+ // Finalize in-place and strip the scratch buffers so replay only
150
+ // carries parsed arguments.
151
+ delete block.partialArgs;
152
+ delete block.streamIndex;
153
+ stream.push({
154
+ type: "toolcall_end",
155
+ contentIndex,
156
+ toolCall: block,
157
+ partial: output,
158
+ });
159
+ }
160
+ };
161
+ const ensureTextBlock = () => {
162
+ if (!textBlock) {
163
+ textBlock = { type: "text", text: "" };
164
+ blocks.push(textBlock);
165
+ stream.push({ type: "text_start", contentIndex: getContentIndex(textBlock), partial: output });
166
+ }
167
+ return textBlock;
168
+ };
169
+ const ensureThinkingBlock = (thinkingSignature) => {
170
+ if (!thinkingBlock) {
171
+ thinkingBlock = {
172
+ type: "thinking",
173
+ thinking: "",
174
+ thinkingSignature,
175
+ };
176
+ blocks.push(thinkingBlock);
177
+ stream.push({ type: "thinking_start", contentIndex: getContentIndex(thinkingBlock), partial: output });
178
+ }
179
+ return thinkingBlock;
180
+ };
181
+ const ensureToolCallBlock = (toolCall) => {
182
+ const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
183
+ let block = streamIndex !== undefined ? toolCallBlocksByIndex.get(streamIndex) : undefined;
184
+ if (!block && toolCall.id) {
185
+ block = toolCallBlocksById.get(toolCall.id);
186
+ }
187
+ if (!block) {
188
+ block = {
189
+ type: "toolCall",
190
+ id: toolCall.id || "",
191
+ name: toolCall.function?.name || "",
192
+ arguments: {},
193
+ partialArgs: "",
194
+ streamIndex,
195
+ };
196
+ if (streamIndex !== undefined) {
197
+ toolCallBlocksByIndex.set(streamIndex, block);
198
+ }
199
+ if (toolCall.id) {
200
+ toolCallBlocksById.set(toolCall.id, block);
201
+ }
202
+ blocks.push(block);
203
+ stream.push({
204
+ type: "toolcall_start",
205
+ contentIndex: getContentIndex(block),
206
+ partial: output,
207
+ });
208
+ }
209
+ if (streamIndex !== undefined && block.streamIndex === undefined) {
210
+ block.streamIndex = streamIndex;
211
+ toolCallBlocksByIndex.set(streamIndex, block);
212
+ }
213
+ if (toolCall.id) {
214
+ toolCallBlocksById.set(toolCall.id, block);
215
+ }
216
+ return block;
217
+ };
218
+ for await (const chunk of openaiStream) {
219
+ if (!chunk || typeof chunk !== "object")
220
+ continue;
221
+ // OpenAI documents ChatCompletionChunk.id as the unique chat completion identifier,
222
+ // and each chunk in a streamed completion carries the same id.
223
+ output.responseId ||= chunk.id;
224
+ if (typeof chunk.model === "string" && chunk.model.length > 0 && chunk.model !== model.id) {
225
+ output.responseModel ||= chunk.model;
226
+ }
227
+ if (chunk.usage) {
228
+ output.usage = parseChunkUsage(chunk.usage, model, cacheWriteCost);
229
+ }
230
+ const choice = Array.isArray(chunk.choices) ? chunk.choices[0] : undefined;
231
+ if (!choice)
232
+ continue;
233
+ // Fallback: some providers (e.g., Moonshot) return usage
234
+ // in choice.usage instead of the standard chunk.usage
235
+ if (!chunk.usage && choice.usage) {
236
+ output.usage = parseChunkUsage(choice.usage, model, cacheWriteCost);
237
+ }
238
+ if (choice.finish_reason) {
239
+ const finishReasonResult = mapStopReason(choice.finish_reason);
240
+ output.stopReason = finishReasonResult.stopReason;
241
+ if (finishReasonResult.errorMessage) {
242
+ output.errorMessage = finishReasonResult.errorMessage;
243
+ }
244
+ }
245
+ if (choice.delta) {
246
+ if (choice.delta.content !== null &&
247
+ choice.delta.content !== undefined &&
248
+ choice.delta.content.length > 0) {
249
+ const block = ensureTextBlock();
250
+ block.text += choice.delta.content;
251
+ stream.push({
252
+ type: "text_delta",
253
+ contentIndex: getContentIndex(block),
254
+ delta: choice.delta.content,
255
+ partial: output,
256
+ });
257
+ }
258
+ // Some endpoints return reasoning in reasoning_content (llama.cpp),
259
+ // or reasoning (other openai compatible endpoints)
260
+ // Use the first non-empty reasoning field to avoid duplication
261
+ // (e.g., chutes.ai returns both reasoning_content and reasoning with same content)
262
+ const reasoningFields = ["reasoning_content", "reasoning", "reasoning_text"];
263
+ const deltaFields = choice.delta;
264
+ let foundReasoningField = null;
265
+ for (const field of reasoningFields) {
266
+ const value = deltaFields[field];
267
+ if (typeof value === "string" && value.length > 0) {
268
+ foundReasoningField = field;
269
+ break;
270
+ }
271
+ }
272
+ if (foundReasoningField) {
273
+ const delta = deltaFields[foundReasoningField];
274
+ if (typeof delta === "string" && delta.length > 0) {
275
+ const block = ensureThinkingBlock(foundReasoningField);
276
+ block.thinking += delta;
277
+ stream.push({
278
+ type: "thinking_delta",
279
+ contentIndex: getContentIndex(block),
280
+ delta,
281
+ partial: output,
282
+ });
283
+ }
284
+ }
285
+ if (choice?.delta?.tool_calls) {
286
+ for (const toolCall of choice.delta.tool_calls) {
287
+ const block = ensureToolCallBlock(toolCall);
288
+ if (!block.id && toolCall.id) {
289
+ block.id = toolCall.id;
290
+ toolCallBlocksById.set(toolCall.id, block);
291
+ }
292
+ if (!block.name && toolCall.function?.name) {
293
+ block.name = toolCall.function.name;
294
+ }
295
+ let delta = "";
296
+ if (toolCall.function?.arguments) {
297
+ delta = toolCall.function.arguments;
298
+ block.partialArgs = (block.partialArgs ?? "") + toolCall.function.arguments;
299
+ block.arguments = parseStreamingJson(block.partialArgs);
300
+ }
301
+ stream.push({
302
+ type: "toolcall_delta",
303
+ contentIndex: getContentIndex(block),
304
+ delta,
305
+ partial: output,
306
+ });
307
+ }
308
+ }
309
+ const reasoningDetails = choice.delta.reasoning_details;
310
+ if (reasoningDetails && Array.isArray(reasoningDetails)) {
311
+ for (const detail of reasoningDetails) {
312
+ if (!detail || typeof detail !== "object" || Array.isArray(detail))
313
+ continue;
314
+ const detailRecord = detail;
315
+ const explicitIndex = typeof detailRecord.index === "number" ? detailRecord.index : undefined;
316
+ const index = explicitIndex ?? nextReasoningDetailsIndex;
317
+ nextReasoningDetailsIndex = Math.max(nextReasoningDetailsIndex, index + 1);
318
+ const previousDetail = reasoningDetailsByIndex.get(index);
319
+ const mergedDetail = { ...previousDetail, ...detailRecord };
320
+ for (const field of ["text", "summary"]) {
321
+ const previousFragment = previousDetail?.[field];
322
+ const fragment = detailRecord[field];
323
+ if (typeof previousFragment === "string" && typeof fragment === "string") {
324
+ mergedDetail[field] = previousFragment + fragment;
325
+ }
326
+ }
327
+ reasoningDetailsByIndex.set(index, mergedDetail);
328
+ if (detailRecord.type === "reasoning.encrypted" &&
329
+ typeof detailRecord.id === "string" &&
330
+ detailRecord.data) {
331
+ const matchingToolCall = output.content.find((b) => b.type === "toolCall" && b.id === detailRecord.id);
332
+ if (matchingToolCall) {
333
+ matchingToolCall.thoughtSignature = JSON.stringify(detailRecord);
334
+ }
335
+ }
336
+ }
337
+ if (reasoningDetailsByIndex.size > 0) {
338
+ if (!reasoningDetailsBlock) {
339
+ reasoningDetailsBlock = { type: "thinking", thinking: "", redacted: true };
340
+ blocks.push(reasoningDetailsBlock);
341
+ stream.push({
342
+ type: "thinking_start",
343
+ contentIndex: getContentIndex(reasoningDetailsBlock),
344
+ partial: output,
345
+ });
346
+ }
347
+ reasoningDetailsBlock.thinkingSignature = encodeReasoningDetails([...reasoningDetailsByIndex.entries()]
348
+ .sort(([left], [right]) => left - right)
349
+ .map(([, detail]) => detail));
350
+ }
351
+ }
352
+ }
353
+ }
354
+ for (const block of blocks) {
355
+ finishBlock(block);
356
+ }
357
+ if (options?.signal?.aborted) {
358
+ throw new Error("Request was aborted");
359
+ }
360
+ if (output.stopReason === "aborted") {
361
+ throw new Error("Request was aborted");
362
+ }
363
+ if (output.stopReason === "error") {
364
+ throw new Error(output.errorMessage || "Provider returned an error stop reason");
365
+ }
366
+ stream.push({ type: "done", reason: output.stopReason, message: output });
367
+ stream.end();
368
+ }
369
+ catch (error) {
370
+ for (const block of output.content) {
371
+ delete block.index;
372
+ // Streaming scratch buffers are only used during parsing; never persist them.
373
+ delete block.partialArgs;
374
+ delete block.streamIndex;
375
+ }
376
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
377
+ output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
378
+ // Some providers via OpenRouter give additional information in this field.
379
+ const rawMetadata = error?.error?.metadata?.raw;
380
+ if (rawMetadata)
381
+ output.errorMessage += `\n${rawMetadata}`;
382
+ stream.push({ type: "error", reason: output.stopReason, error: output });
383
+ stream.end();
384
+ }
385
+ })();
386
+ return stream;
387
+ };
388
+ export const streamSimpleOpenAICompletions = (model, context, options) => {
389
+ const apiKey = options?.apiKey || getEnvApiKey(model.provider);
390
+ if (!apiKey) {
391
+ throw new Error(`No API key for provider: ${model.provider}`);
392
+ }
393
+ const base = buildBaseOptions(model, options, apiKey);
394
+ const requestedReasoning = options?.reasoning;
395
+ const reasoningSpecified = requestedReasoning !== undefined;
396
+ const clampedReasoning = reasoningSpecified ? clampThinkingLevel(model, requestedReasoning) : undefined;
397
+ const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
398
+ const toolChoice = options?.toolChoice;
399
+ return streamOpenAICompletions(model, context, {
400
+ ...base,
401
+ reasoningEffort,
402
+ reasoningEnabled: reasoningSpecified ? clampedReasoning !== "off" : undefined,
403
+ toolChoice,
404
+ });
405
+ };
406
+ function createClient(model, context, apiKey, optionsHeaders, sessionId, compat = getCompat(model)) {
407
+ if (!apiKey) {
408
+ if (!process.env.OPENAI_API_KEY) {
409
+ throw new Error("OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it as an argument.");
410
+ }
411
+ apiKey = process.env.OPENAI_API_KEY;
412
+ }
413
+ const headers = { ...model.headers };
414
+ if (model.provider === "github-copilot") {
415
+ const hasImages = hasCopilotVisionInput(context.messages);
416
+ const copilotHeaders = buildCopilotDynamicHeaders({
417
+ messages: context.messages,
418
+ hasImages,
419
+ });
420
+ Object.assign(headers, copilotHeaders);
421
+ }
422
+ if (model.provider === "prime-inference") {
423
+ const teamId = getPrimeTeamId();
424
+ if (teamId)
425
+ headers["X-Prime-Team-ID"] = teamId;
426
+ }
427
+ if (sessionId && compat.sendSessionAffinityHeaders) {
428
+ headers.session_id = sessionId;
429
+ headers["x-client-request-id"] = sessionId;
430
+ headers["x-session-affinity"] = sessionId;
431
+ }
432
+ if (optionsHeaders) {
433
+ Object.assign(headers, optionsHeaders);
434
+ }
435
+ const defaultHeaders = model.provider === "cloudflare-ai-gateway"
436
+ ? {
437
+ ...headers,
438
+ Authorization: headers.Authorization ?? null,
439
+ "cf-aig-authorization": `Bearer ${apiKey}`,
440
+ }
441
+ : headers;
442
+ return new OpenAI({
443
+ apiKey,
444
+ baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model) : model.baseUrl,
445
+ dangerouslyAllowBrowser: true,
446
+ defaultHeaders,
447
+ });
448
+ }
449
+ function buildParams(model, context, options, compat = getCompat(model), cacheRetention = resolveCacheRetention(options?.cacheRetention), cacheControl = getCompatCacheControl(compat, cacheRetention)) {
450
+ const messages = convertMessages(model, context, compat);
451
+ const params = {
452
+ model: model.id,
453
+ messages,
454
+ stream: true,
455
+ prompt_cache_key: (model.baseUrl.includes("api.openai.com") && cacheRetention !== "none") ||
456
+ (cacheRetention === "long" && compat.supportsLongCacheRetention)
457
+ ? options?.sessionId
458
+ : undefined,
459
+ prompt_cache_retention: cacheRetention === "long" && compat.supportsLongCacheRetention ? "24h" : undefined,
460
+ };
461
+ if (compat.supportsUsageInStreaming !== false) {
462
+ params.stream_options = { include_usage: true };
463
+ }
464
+ if (compat.supportsStore) {
465
+ params.store = false;
466
+ }
467
+ if (options?.maxTokens) {
468
+ if (compat.maxTokensField === "max_tokens") {
469
+ params.max_tokens = options.maxTokens;
470
+ }
471
+ else {
472
+ params.max_completion_tokens = options.maxTokens;
473
+ }
474
+ }
475
+ if (options?.temperature !== undefined) {
476
+ params.temperature = options.temperature;
477
+ }
478
+ if (context.tools && context.tools.length > 0) {
479
+ params.tools = convertTools(context.tools, compat);
480
+ if (compat.zaiToolStream) {
481
+ params.tool_stream = true;
482
+ }
483
+ }
484
+ else if (hasToolHistory(context.messages)) {
485
+ // Anthropic (via LiteLLM/proxy) requires tools param when conversation has tool_calls/tool_results
486
+ params.tools = [];
487
+ }
488
+ if (cacheControl) {
489
+ applyAnthropicCacheControl(messages, params.tools, cacheControl);
490
+ }
491
+ if (options?.toolChoice) {
492
+ params.tool_choice = options.toolChoice;
493
+ }
494
+ if (compat.thinkingFormat === "zai" && model.reasoning) {
495
+ params.enable_thinking = !!options?.reasoningEffort;
496
+ }
497
+ else if (compat.thinkingFormat === "qwen" && model.reasoning) {
498
+ params.enable_thinking = !!options?.reasoningEffort;
499
+ }
500
+ else if (compat.thinkingFormat === "qwen-chat-template" && model.reasoning) {
501
+ params.chat_template_kwargs = {
502
+ enable_thinking: !!options?.reasoningEffort,
503
+ preserve_thinking: true,
504
+ };
505
+ }
506
+ else if (compat.thinkingFormat === "deepseek" && model.reasoning) {
507
+ params.thinking = { type: options?.reasoningEffort ? "enabled" : "disabled" };
508
+ if (options?.reasoningEffort) {
509
+ params.reasoning_effort =
510
+ model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
511
+ }
512
+ }
513
+ else if (compat.thinkingFormat === "openrouter" && model.reasoning) {
514
+ // OpenRouter distinguishes an omitted reasoning preference (use the model
515
+ // default), an explicit toggle, and an explicit effort selection.
516
+ const openRouterParams = params;
517
+ if (options?.reasoningEffort && compat.supportsReasoningEffort) {
518
+ openRouterParams.reasoning = {
519
+ effort: model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort,
520
+ };
521
+ }
522
+ else if (options?.reasoningEnabled === true) {
523
+ openRouterParams.reasoning = { enabled: true };
524
+ }
525
+ else if (options?.reasoningEnabled === false && model.thinkingLevelMap?.off !== null) {
526
+ openRouterParams.reasoning = compat.supportsReasoningEffort
527
+ ? { effort: model.thinkingLevelMap?.off ?? "none" }
528
+ : { enabled: false };
529
+ }
530
+ }
531
+ else if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) {
532
+ params.reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
533
+ }
534
+ else if (options?.reasoningEnabled === false && model.reasoning && compat.supportsReasoningEffort) {
535
+ const offValue = model.thinkingLevelMap?.off;
536
+ if (offValue !== null) {
537
+ params.reasoning_effort = offValue ?? "none";
538
+ }
539
+ }
540
+ if (model.baseUrl.includes("openrouter.ai") && model.compat?.openRouterRouting) {
541
+ params.provider = model.compat.openRouterRouting;
542
+ }
543
+ if (model.baseUrl.includes("ai-gateway.vercel.sh") && model.compat?.vercelGatewayRouting) {
544
+ const routing = model.compat.vercelGatewayRouting;
545
+ if (routing.only || routing.order) {
546
+ const gatewayOptions = {};
547
+ if (routing.only)
548
+ gatewayOptions.only = routing.only;
549
+ if (routing.order)
550
+ gatewayOptions.order = routing.order;
551
+ params.providerOptions = { gateway: gatewayOptions };
552
+ }
553
+ }
554
+ return params;
555
+ }
556
+ function getCompatCacheControl(compat, cacheRetention) {
557
+ if (compat.cacheControlFormat !== "anthropic" || cacheRetention === "none") {
558
+ return undefined;
559
+ }
560
+ const ttl = cacheRetention === "long" && compat.supportsLongCacheRetention ? "1h" : undefined;
561
+ return { type: "ephemeral", ...(ttl ? { ttl } : {}) };
562
+ }
563
+ function applyAnthropicCacheControl(messages, tools, cacheControl) {
564
+ addCacheControlToSystemPrompt(messages, cacheControl);
565
+ addCacheControlToLastTool(tools, cacheControl);
566
+ addCacheControlToLastConversationMessage(messages, cacheControl);
567
+ }
568
+ function addCacheControlToSystemPrompt(messages, cacheControl) {
569
+ for (const message of messages) {
570
+ if (message.role === "system" || message.role === "developer") {
571
+ addCacheControlToInstructionMessage(message, cacheControl);
572
+ return;
573
+ }
574
+ }
575
+ }
576
+ function addCacheControlToLastConversationMessage(messages, cacheControl) {
577
+ for (let i = messages.length - 1; i >= 0; i--) {
578
+ const message = messages[i];
579
+ if (message.role === "user" || message.role === "assistant") {
580
+ if (addCacheControlToMessage(message, cacheControl)) {
581
+ return;
582
+ }
583
+ }
584
+ }
585
+ }
586
+ function addCacheControlToLastTool(tools, cacheControl) {
587
+ if (!tools || tools.length === 0) {
588
+ return;
589
+ }
590
+ const lastTool = tools[tools.length - 1];
591
+ lastTool.cache_control = cacheControl;
592
+ }
593
+ function addCacheControlToInstructionMessage(message, cacheControl) {
594
+ return addCacheControlToTextContent(message, cacheControl);
595
+ }
596
+ function addCacheControlToMessage(message, cacheControl) {
597
+ if (message.role === "user" || message.role === "assistant") {
598
+ return addCacheControlToTextContent(message, cacheControl);
599
+ }
600
+ return false;
601
+ }
602
+ function addCacheControlToTextContent(message, cacheControl) {
603
+ const content = message.content;
604
+ if (typeof content === "string") {
605
+ if (content.length === 0) {
606
+ return false;
607
+ }
608
+ message.content = [
609
+ {
610
+ type: "text",
611
+ text: content,
612
+ cache_control: cacheControl,
613
+ },
614
+ ];
615
+ return true;
616
+ }
617
+ if (!Array.isArray(content)) {
618
+ return false;
619
+ }
620
+ for (let i = content.length - 1; i >= 0; i--) {
621
+ const part = content[i];
622
+ if (part?.type === "text") {
623
+ const textPart = part;
624
+ textPart.cache_control = cacheControl;
625
+ return true;
626
+ }
627
+ }
628
+ return false;
629
+ }
630
+ export function convertMessages(model, context, compat) {
631
+ const params = [];
632
+ const normalizeToolCallId = (id) => {
633
+ // Handle pipe-separated IDs from OpenAI Responses API
634
+ // Format: {call_id}|{id} where {id} can be 400+ chars with special chars (+, /, =)
635
+ // These come from providers like github-copilot, openai-codex, opencode
636
+ // Extract just the call_id part and normalize it
637
+ if (id.includes("|")) {
638
+ const [callId] = id.split("|");
639
+ // Sanitize to allowed chars and truncate to 40 chars (OpenAI limit)
640
+ return callId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
641
+ }
642
+ if (model.provider === "openai")
643
+ return id.length > 40 ? id.slice(0, 40) : id;
644
+ return id;
645
+ };
646
+ const transformedMessages = transformMessages(context.messages, model, (id) => normalizeToolCallId(id));
647
+ if (context.systemPrompt) {
648
+ const useDeveloperRole = model.reasoning && compat.supportsDeveloperRole;
649
+ const role = useDeveloperRole ? "developer" : "system";
650
+ params.push({ role: role, content: sanitizeSurrogates(context.systemPrompt) });
651
+ }
652
+ let lastRole = null;
653
+ for (let i = 0; i < transformedMessages.length; i++) {
654
+ const msg = transformedMessages[i];
655
+ // Some providers don't allow user messages directly after tool results
656
+ // Insert a synthetic assistant message to bridge the gap
657
+ if (compat.requiresAssistantAfterToolResult && lastRole === "toolResult" && msg.role === "user") {
658
+ params.push({
659
+ role: "assistant",
660
+ content: "I have processed the tool results.",
661
+ });
662
+ }
663
+ if (msg.role === "user") {
664
+ if (typeof msg.content === "string") {
665
+ params.push({
666
+ role: "user",
667
+ content: sanitizeSurrogates(msg.content),
668
+ });
669
+ }
670
+ else {
671
+ const content = msg.content.map((item) => {
672
+ if (item.type === "text") {
673
+ return {
674
+ type: "text",
675
+ text: sanitizeSurrogates(item.text),
676
+ };
677
+ }
678
+ else {
679
+ return {
680
+ type: "image_url",
681
+ image_url: {
682
+ url: `data:${item.mimeType};base64,${item.data}`,
683
+ },
684
+ };
685
+ }
686
+ });
687
+ if (content.length === 0)
688
+ continue;
689
+ params.push({
690
+ role: "user",
691
+ content,
692
+ });
693
+ }
694
+ }
695
+ else if (msg.role === "assistant") {
696
+ // Some providers don't accept null content, use empty string instead
697
+ const assistantMsg = {
698
+ role: "assistant",
699
+ content: compat.requiresAssistantAfterToolResult ? "" : null,
700
+ };
701
+ const assistantTextParts = msg.content
702
+ .filter(isTextContentBlock)
703
+ .filter((block) => block.text.trim().length > 0)
704
+ .map((block) => ({
705
+ type: "text",
706
+ text: sanitizeSurrogates(block.text),
707
+ }));
708
+ const assistantText = assistantTextParts.map((part) => part.text).join("");
709
+ const replayReasoningDetails = msg.content
710
+ .filter(isThinkingContentBlock)
711
+ .flatMap((block) => decodeReasoningDetails(block.thinkingSignature) ?? []);
712
+ if (replayReasoningDetails.length > 0) {
713
+ assistantMsg.reasoning_details = replayReasoningDetails;
714
+ }
715
+ const nonEmptyThinkingBlocks = msg.content
716
+ .filter(isThinkingContentBlock)
717
+ .filter((block) => decodeReasoningDetails(block.thinkingSignature) === undefined)
718
+ .filter((block) => block.thinking.trim().length > 0);
719
+ if (nonEmptyThinkingBlocks.length > 0) {
720
+ if (compat.requiresThinkingAsText) {
721
+ // Convert thinking blocks to plain text (no tags to avoid model mimicking them)
722
+ const thinkingText = nonEmptyThinkingBlocks
723
+ .map((block) => sanitizeSurrogates(block.thinking))
724
+ .join("\n\n");
725
+ assistantMsg.content = [{ type: "text", text: thinkingText }, ...assistantTextParts];
726
+ }
727
+ else {
728
+ // Always send assistant content as a plain string (OpenAI Chat Completions
729
+ // API standard format). Sending as an array of {type:"text", text:"..."}
730
+ // objects is non-standard and causes some models (e.g. DeepSeek V3.2 via
731
+ // NVIDIA NIM) to mirror the content-block structure literally in their
732
+ // output, producing recursive nesting like [{'type':'text','text':'[{...}]'}].
733
+ if (assistantText.length > 0) {
734
+ assistantMsg.content = assistantText;
735
+ }
736
+ // thinkingSignature holds the field the provider streamed reasoning in
737
+ // (reasoning_content / reasoning / reasoning_text), not a crypto signature.
738
+ // Prefer reasoning_content when the provider requires it (otherwise the
739
+ // reasoning_content="" default below would clobber the trace); else round-trip
740
+ // into the recorded field; with neither, keep the trace as text rather than
741
+ // inventing an unsupported field.
742
+ const reasoningText = nonEmptyThinkingBlocks
743
+ .map((block) => sanitizeSurrogates(block.thinking))
744
+ .join("\n");
745
+ const reasoningField = compat.requiresReasoningContentOnAssistantMessages
746
+ ? "reasoning_content"
747
+ : nonEmptyThinkingBlocks[0].thinkingSignature || undefined;
748
+ if (reasoningField) {
749
+ assistantMsg[reasoningField] = reasoningText;
750
+ }
751
+ else {
752
+ assistantMsg.content =
753
+ assistantText.length > 0 ? `${reasoningText}\n\n${assistantText}` : reasoningText;
754
+ }
755
+ }
756
+ }
757
+ else if (assistantText.length > 0) {
758
+ // Always send assistant content as a plain string (OpenAI Chat Completions
759
+ // API standard format). Sending as an array of {type:"text", text:"..."}
760
+ // objects is non-standard and causes some models (e.g. DeepSeek V3.2 via
761
+ // NVIDIA NIM) to mirror the content-block structure literally in their
762
+ // output, producing recursive nesting like [{'type':'text','text':'[{...}]'}].
763
+ assistantMsg.content = assistantText;
764
+ }
765
+ const toolCalls = msg.content.filter(isToolCallBlock);
766
+ if (toolCalls.length > 0) {
767
+ assistantMsg.tool_calls = toolCalls.map((tc) => ({
768
+ id: tc.id,
769
+ type: "function",
770
+ function: {
771
+ name: tc.name,
772
+ arguments: JSON.stringify(tc.arguments),
773
+ },
774
+ }));
775
+ const reasoningDetails = toolCalls
776
+ .filter((tc) => tc.thoughtSignature)
777
+ .map((tc) => {
778
+ try {
779
+ return JSON.parse(tc.thoughtSignature);
780
+ }
781
+ catch {
782
+ return null;
783
+ }
784
+ })
785
+ .filter(Boolean);
786
+ if (reasoningDetails.length > 0 && replayReasoningDetails.length === 0) {
787
+ assistantMsg.reasoning_details = reasoningDetails;
788
+ }
789
+ }
790
+ if (compat.requiresReasoningContentOnAssistantMessages &&
791
+ model.reasoning &&
792
+ assistantMsg.reasoning_content === undefined) {
793
+ assistantMsg.reasoning_content = "";
794
+ }
795
+ if (replayReasoningDetails.length > 0 && assistantMsg.content === null && !assistantMsg.tool_calls) {
796
+ assistantMsg.content = "";
797
+ }
798
+ // Skip assistant messages that have no content and no tool calls.
799
+ // Some providers require "either content or tool_calls, but not none".
800
+ // Other providers also don't accept empty assistant messages.
801
+ // This handles aborted assistant responses that got no content.
802
+ const content = assistantMsg.content;
803
+ const hasContent = content !== null &&
804
+ content !== undefined &&
805
+ (typeof content === "string" ? content.length > 0 : content.length > 0);
806
+ if (!hasContent && !assistantMsg.tool_calls && replayReasoningDetails.length === 0) {
807
+ continue;
808
+ }
809
+ params.push(assistantMsg);
810
+ }
811
+ else if (msg.role === "toolResult") {
812
+ const imageBlocks = [];
813
+ let j = i;
814
+ for (; j < transformedMessages.length && transformedMessages[j].role === "toolResult"; j++) {
815
+ const toolMsg = transformedMessages[j];
816
+ const textResult = toolMsg.content
817
+ .filter(isTextContentBlock)
818
+ .map((block) => block.text)
819
+ .join("\n");
820
+ const hasImages = toolMsg.content.some((c) => c.type === "image");
821
+ // Always send tool result with text (or placeholder if only images)
822
+ const hasText = textResult.length > 0;
823
+ const toolResultMsg = {
824
+ role: "tool",
825
+ content: sanitizeSurrogates(hasText ? textResult : hasImages ? "(see attached image)" : ""),
826
+ tool_call_id: toolMsg.toolCallId,
827
+ };
828
+ if (compat.requiresToolResultName && toolMsg.toolName) {
829
+ toolResultMsg.name = toolMsg.toolName;
830
+ }
831
+ params.push(toolResultMsg);
832
+ if (hasImages && model.input.includes("image")) {
833
+ for (const block of toolMsg.content) {
834
+ if (isImageContentBlock(block)) {
835
+ imageBlocks.push({
836
+ type: "image_url",
837
+ image_url: {
838
+ url: `data:${block.mimeType};base64,${block.data}`,
839
+ },
840
+ });
841
+ }
842
+ }
843
+ }
844
+ }
845
+ i = j - 1;
846
+ if (imageBlocks.length > 0) {
847
+ if (compat.requiresAssistantAfterToolResult) {
848
+ params.push({
849
+ role: "assistant",
850
+ content: "I have processed the tool results.",
851
+ });
852
+ }
853
+ params.push({
854
+ role: "user",
855
+ content: [
856
+ {
857
+ type: "text",
858
+ text: "Attached image(s) from tool result:",
859
+ },
860
+ ...imageBlocks,
861
+ ],
862
+ });
863
+ lastRole = "user";
864
+ }
865
+ else {
866
+ lastRole = "toolResult";
867
+ }
868
+ continue;
869
+ }
870
+ lastRole = msg.role;
871
+ }
872
+ return params;
873
+ }
874
+ function convertTools(tools, compat) {
875
+ return tools.map((tool) => ({
876
+ type: "function",
877
+ function: {
878
+ name: tool.name,
879
+ description: tool.description,
880
+ parameters: tool.parameters, // TypeBox already generates JSON Schema
881
+ // Only include strict if provider supports it. Some reject unknown fields.
882
+ ...(compat.supportsStrictMode !== false && { strict: false }),
883
+ },
884
+ }));
885
+ }
886
+ function parseChunkUsage(rawUsage, model, cacheWriteCost) {
887
+ const promptTokens = rawUsage.prompt_tokens || 0;
888
+ const reportedCachedTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? 0;
889
+ const cacheWriteTokens = rawUsage.prompt_tokens_details?.cache_write_tokens || 0;
890
+ // Normalize to pi-ai semantics:
891
+ // - cacheRead: hits from cache created by previous requests only
892
+ // - cacheWrite: tokens written to cache in this request
893
+ // Some OpenAI-compatible providers (observed on OpenRouter) report cached_tokens
894
+ // as (previous hits + current writes). In that case, remove cacheWrite from cacheRead.
895
+ const cacheReadTokens = cacheWriteTokens > 0 ? Math.max(0, reportedCachedTokens - cacheWriteTokens) : reportedCachedTokens;
896
+ const input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
897
+ // OpenAI completion_tokens already includes reasoning_tokens.
898
+ const outputTokens = rawUsage.completion_tokens || 0;
899
+ const usage = {
900
+ input,
901
+ output: outputTokens,
902
+ cacheRead: cacheReadTokens,
903
+ cacheWrite: cacheWriteTokens,
904
+ totalTokens: input + outputTokens + cacheReadTokens + cacheWriteTokens,
905
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
906
+ };
907
+ calculateCost(model, usage, cacheWriteCost === undefined ? undefined : { cacheWrite: cacheWriteCost });
908
+ return usage;
909
+ }
910
+ function mapStopReason(reason) {
911
+ if (reason === null)
912
+ return { stopReason: "stop" };
913
+ switch (reason) {
914
+ case "stop":
915
+ case "end":
916
+ return { stopReason: "stop" };
917
+ case "length":
918
+ return { stopReason: "length" };
919
+ case "function_call":
920
+ case "tool_calls":
921
+ return { stopReason: "toolUse" };
922
+ case "content_filter":
923
+ return { stopReason: "error", errorMessage: "Provider finish_reason: content_filter" };
924
+ case "network_error":
925
+ return { stopReason: "error", errorMessage: "Provider finish_reason: network_error" };
926
+ default:
927
+ return {
928
+ stopReason: "error",
929
+ errorMessage: `Provider finish_reason: ${reason}`,
930
+ };
931
+ }
932
+ }
933
+ /**
934
+ * Detect compatibility settings from provider and baseUrl for known providers.
935
+ * Provider takes precedence over URL-based detection since it's explicitly configured.
936
+ * Returns a fully resolved OpenAICompletionsCompat object with all fields set.
937
+ */
938
+ function detectCompat(model) {
939
+ const provider = model.provider;
940
+ const baseUrl = model.baseUrl;
941
+ const isZai = provider === "zai" || baseUrl.includes("api.z.ai");
942
+ const isMoonshot = provider === "moonshotai" || provider === "moonshotai-cn" || baseUrl.includes("api.moonshot.");
943
+ const isCloudflareWorkersAI = provider === "cloudflare-workers-ai" || baseUrl.includes("api.cloudflare.com");
944
+ const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com");
945
+ const isPrimeInference = provider === "prime-inference" || baseUrl.includes("api.pinference.ai");
946
+ const isNonStandard = provider === "cerebras" ||
947
+ baseUrl.includes("cerebras.ai") ||
948
+ provider === "xai" ||
949
+ baseUrl.includes("api.x.ai") ||
950
+ baseUrl.includes("chutes.ai") ||
951
+ baseUrl.includes("deepseek.com") ||
952
+ isZai ||
953
+ isMoonshot ||
954
+ provider === "opencode" ||
955
+ baseUrl.includes("opencode.ai") ||
956
+ isCloudflareWorkersAI ||
957
+ isCloudflareAiGateway ||
958
+ isPrimeInference;
959
+ const useMaxTokens = baseUrl.includes("chutes.ai") || isMoonshot || isCloudflareAiGateway || isPrimeInference;
960
+ const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
961
+ const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com");
962
+ const isAnthropicModel = model.id.startsWith("anthropic/");
963
+ const cacheControlFormat = isAnthropicModel && (provider === "openrouter" || isPrimeInference) ? "anthropic" : undefined;
964
+ return {
965
+ supportsStore: !isNonStandard,
966
+ supportsDeveloperRole: !isNonStandard,
967
+ supportsReasoningEffort: !isGrok && !isZai && !isMoonshot && !isCloudflareAiGateway,
968
+ supportsUsageInStreaming: true,
969
+ maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
970
+ requiresToolResultName: false,
971
+ requiresAssistantAfterToolResult: false,
972
+ requiresThinkingAsText: false,
973
+ requiresReasoningContentOnAssistantMessages: isDeepSeek,
974
+ thinkingFormat: isDeepSeek
975
+ ? "deepseek"
976
+ : isZai
977
+ ? "zai"
978
+ : provider === "openrouter" || baseUrl.includes("openrouter.ai")
979
+ ? "openrouter"
980
+ : "openai",
981
+ openRouterRouting: {},
982
+ vercelGatewayRouting: {},
983
+ zaiToolStream: false,
984
+ supportsStrictMode: !isMoonshot && !isCloudflareAiGateway && !isPrimeInference,
985
+ cacheControlFormat,
986
+ sendSessionAffinityHeaders: false,
987
+ supportsLongCacheRetention: !(isCloudflareWorkersAI || isCloudflareAiGateway),
988
+ };
989
+ }
990
+ /**
991
+ * Get resolved compatibility settings for a model.
992
+ * Uses explicit model.compat if provided, otherwise auto-detects from provider/URL.
993
+ */
994
+ function getCompat(model) {
995
+ const detected = detectCompat(model);
996
+ if (!model.compat)
997
+ return detected;
998
+ return {
999
+ supportsStore: model.compat.supportsStore ?? detected.supportsStore,
1000
+ supportsDeveloperRole: model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole,
1001
+ supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort,
1002
+ supportsUsageInStreaming: model.compat.supportsUsageInStreaming ?? detected.supportsUsageInStreaming,
1003
+ maxTokensField: model.compat.maxTokensField ?? detected.maxTokensField,
1004
+ requiresToolResultName: model.compat.requiresToolResultName ?? detected.requiresToolResultName,
1005
+ requiresAssistantAfterToolResult: model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult,
1006
+ requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
1007
+ requiresReasoningContentOnAssistantMessages: model.compat.requiresReasoningContentOnAssistantMessages ??
1008
+ detected.requiresReasoningContentOnAssistantMessages,
1009
+ thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
1010
+ openRouterRouting: model.compat.openRouterRouting ?? {},
1011
+ vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,
1012
+ zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream,
1013
+ supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
1014
+ cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat,
1015
+ sendSessionAffinityHeaders: model.compat.sendSessionAffinityHeaders ?? detected.sendSessionAffinityHeaders,
1016
+ supportsLongCacheRetention: model.compat.supportsLongCacheRetention ?? detected.supportsLongCacheRetention,
1017
+ };
1018
+ }
1019
+ //# sourceMappingURL=openai-completions.js.map