@fleetagent/pi-ai 0.1.8 → 0.2.0

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 (148) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +29 -24
  3. package/dist/cli.js.map +1 -1
  4. package/dist/env-api-keys.d.ts.map +1 -1
  5. package/dist/env-api-keys.js +1 -1
  6. package/dist/env-api-keys.js.map +1 -1
  7. package/dist/image-models.generated.d.ts +225 -0
  8. package/dist/image-models.generated.d.ts.map +1 -1
  9. package/dist/image-models.generated.js +227 -2
  10. package/dist/image-models.generated.js.map +1 -1
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/models.d.ts +37 -2
  15. package/dist/models.d.ts.map +1 -1
  16. package/dist/models.generated.d.ts +6120 -2593
  17. package/dist/models.generated.d.ts.map +1 -1
  18. package/dist/models.generated.js +5347 -2110
  19. package/dist/models.generated.js.map +1 -1
  20. package/dist/models.js +259 -9
  21. package/dist/models.js.map +1 -1
  22. package/dist/oauth.d.ts +6 -1
  23. package/dist/oauth.d.ts.map +1 -1
  24. package/dist/oauth.js +4 -1
  25. package/dist/oauth.js.map +1 -1
  26. package/dist/providers/amazon-bedrock.d.ts +5 -4
  27. package/dist/providers/amazon-bedrock.d.ts.map +1 -1
  28. package/dist/providers/amazon-bedrock.js +303 -330
  29. package/dist/providers/amazon-bedrock.js.map +1 -1
  30. package/dist/providers/anthropic.d.ts +5 -4
  31. package/dist/providers/anthropic.d.ts.map +1 -1
  32. package/dist/providers/anthropic.js +492 -499
  33. package/dist/providers/anthropic.js.map +1 -1
  34. package/dist/providers/azure-openai-responses.d.ts +4 -2
  35. package/dist/providers/azure-openai-responses.d.ts.map +1 -1
  36. package/dist/providers/azure-openai-responses.js +52 -35
  37. package/dist/providers/azure-openai-responses.js.map +1 -1
  38. package/dist/providers/faux.d.ts +19 -22
  39. package/dist/providers/faux.d.ts.map +1 -1
  40. package/dist/providers/faux.js +106 -70
  41. package/dist/providers/faux.js.map +1 -1
  42. package/dist/providers/github-copilot-headers.d.ts +7 -4
  43. package/dist/providers/github-copilot-headers.d.ts.map +1 -1
  44. package/dist/providers/github-copilot-headers.js.map +1 -1
  45. package/dist/providers/google-shared.d.ts +24 -5
  46. package/dist/providers/google-shared.d.ts.map +1 -1
  47. package/dist/providers/google-shared.js +139 -140
  48. package/dist/providers/google-shared.js.map +1 -1
  49. package/dist/providers/google-vertex.d.ts +3 -7
  50. package/dist/providers/google-vertex.d.ts.map +1 -1
  51. package/dist/providers/google-vertex.js +216 -219
  52. package/dist/providers/google-vertex.js.map +1 -1
  53. package/dist/providers/google.d.ts +4 -7
  54. package/dist/providers/google.d.ts.map +1 -1
  55. package/dist/providers/google.js +214 -218
  56. package/dist/providers/google.js.map +1 -1
  57. package/dist/providers/images/openrouter.d.ts.map +1 -1
  58. package/dist/providers/images/openrouter.js +37 -27
  59. package/dist/providers/images/openrouter.js.map +1 -1
  60. package/dist/providers/mistral.d.ts +7 -6
  61. package/dist/providers/mistral.d.ts.map +1 -1
  62. package/dist/providers/mistral.js +287 -265
  63. package/dist/providers/mistral.js.map +1 -1
  64. package/dist/providers/openai-codex-responses.d.ts +8 -5
  65. package/dist/providers/openai-codex-responses.d.ts.map +1 -1
  66. package/dist/providers/openai-codex-responses.js +493 -391
  67. package/dist/providers/openai-codex-responses.js.map +1 -1
  68. package/dist/providers/openai-completions.d.ts +7 -10
  69. package/dist/providers/openai-completions.d.ts.map +1 -1
  70. package/dist/providers/openai-completions.js +670 -664
  71. package/dist/providers/openai-completions.js.map +1 -1
  72. package/dist/providers/openai-responses-shared.d.ts +5 -4
  73. package/dist/providers/openai-responses-shared.d.ts.map +1 -1
  74. package/dist/providers/openai-responses-shared.js +390 -360
  75. package/dist/providers/openai-responses-shared.js.map +1 -1
  76. package/dist/providers/openai-responses.d.ts +5 -5
  77. package/dist/providers/openai-responses.d.ts.map +1 -1
  78. package/dist/providers/openai-responses.js +134 -109
  79. package/dist/providers/openai-responses.js.map +1 -1
  80. package/dist/providers/simple-options.d.ts +4 -2
  81. package/dist/providers/simple-options.d.ts.map +1 -1
  82. package/dist/providers/simple-options.js.map +1 -1
  83. package/dist/providers/transform-messages.d.ts.map +1 -1
  84. package/dist/providers/transform-messages.js +112 -137
  85. package/dist/providers/transform-messages.js.map +1 -1
  86. package/dist/stream.d.ts +0 -1
  87. package/dist/stream.d.ts.map +1 -1
  88. package/dist/stream.js +0 -1
  89. package/dist/stream.js.map +1 -1
  90. package/dist/types.d.ts +91 -63
  91. package/dist/types.d.ts.map +1 -1
  92. package/dist/types.js.map +1 -1
  93. package/dist/utils/error-body.d.ts.map +1 -1
  94. package/dist/utils/error-body.js +15 -4
  95. package/dist/utils/error-body.js.map +1 -1
  96. package/dist/utils/estimate.d.ts +4 -0
  97. package/dist/utils/estimate.d.ts.map +1 -1
  98. package/dist/utils/estimate.js +3 -1
  99. package/dist/utils/estimate.js.map +1 -1
  100. package/dist/utils/headers.d.ts +3 -0
  101. package/dist/utils/headers.d.ts.map +1 -1
  102. package/dist/utils/headers.js +21 -0
  103. package/dist/utils/headers.js.map +1 -1
  104. package/dist/utils/json-parse.d.ts.map +1 -1
  105. package/dist/utils/json-parse.js +17 -19
  106. package/dist/utils/json-parse.js.map +1 -1
  107. package/dist/utils/oauth/anthropic.d.ts +4 -11
  108. package/dist/utils/oauth/anthropic.d.ts.map +1 -1
  109. package/dist/utils/oauth/anthropic.js +100 -117
  110. package/dist/utils/oauth/anthropic.js.map +1 -1
  111. package/dist/utils/oauth/device-code.d.ts.map +1 -1
  112. package/dist/utils/oauth/device-code.js +51 -38
  113. package/dist/utils/oauth/device-code.js.map +1 -1
  114. package/dist/utils/oauth/github-copilot.d.ts +6 -9
  115. package/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  116. package/dist/utils/oauth/github-copilot.js +47 -42
  117. package/dist/utils/oauth/github-copilot.js.map +1 -1
  118. package/dist/utils/oauth/index.d.ts +8 -17
  119. package/dist/utils/oauth/index.d.ts.map +1 -1
  120. package/dist/utils/oauth/index.js +27 -23
  121. package/dist/utils/oauth/index.js.map +1 -1
  122. package/dist/utils/oauth/oauth-page.d.ts.map +1 -1
  123. package/dist/utils/oauth/oauth-page.js.map +1 -1
  124. package/dist/utils/oauth/openai-codex.d.ts +7 -12
  125. package/dist/utils/oauth/openai-codex.d.ts.map +1 -1
  126. package/dist/utils/oauth/openai-codex.js +72 -89
  127. package/dist/utils/oauth/openai-codex.js.map +1 -1
  128. package/dist/utils/oauth/pkce.d.ts +5 -4
  129. package/dist/utils/oauth/pkce.d.ts.map +1 -1
  130. package/dist/utils/oauth/pkce.js.map +1 -1
  131. package/dist/utils/oauth/types.d.ts +7 -2
  132. package/dist/utils/oauth/types.d.ts.map +1 -1
  133. package/dist/utils/oauth/types.js.map +1 -1
  134. package/dist/utils/provider-retry.d.ts +16 -0
  135. package/dist/utils/provider-retry.d.ts.map +1 -0
  136. package/dist/utils/provider-retry.js +94 -0
  137. package/dist/utils/provider-retry.js.map +1 -0
  138. package/dist/utils/retry.d.ts +14 -0
  139. package/dist/utils/retry.d.ts.map +1 -1
  140. package/dist/utils/retry.js +84 -0
  141. package/dist/utils/retry.js.map +1 -1
  142. package/dist/utils/typebox-helpers.d.ts +5 -4
  143. package/dist/utils/typebox-helpers.d.ts.map +1 -1
  144. package/dist/utils/typebox-helpers.js.map +1 -1
  145. package/dist/utils/validation.d.ts.map +1 -1
  146. package/dist/utils/validation.js +102 -99
  147. package/dist/utils/validation.js.map +1 -1
  148. package/package.json +2 -2
@@ -3,6 +3,7 @@ import { getEnvApiKey } from "../env-api-keys.js";
3
3
  import { calculateCost, clampThinkingLevel } from "../models.js";
4
4
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
5
5
  import { shortHash } from "../utils/hash.js";
6
+ import { providerHeadersToRecord } from "../utils/headers.js";
6
7
  import { parseStreamingJson } from "../utils/json-parse.js";
7
8
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
8
9
  import { buildBaseOptions } from "./simple-options.js";
@@ -17,47 +18,51 @@ export const streamMistral = (model, context, options) => {
17
18
  (async () => {
18
19
  const output = createOutput(model);
19
20
  try {
20
- const apiKey = options?.apiKey || getEnvApiKey(model.provider);
21
- if (!apiKey) {
22
- throw new Error(`No API key for provider: ${model.provider}`);
23
- }
24
- // Intentionally per-request: avoids shared SDK mutable state across concurrent consumers.
25
- const mistral = new Mistral({
26
- apiKey,
27
- serverURL: model.baseUrl,
28
- });
29
- const normalizeMistralToolCallId = createMistralToolCallIdNormalizer();
30
- const transformedMessages = transformMessages(context.messages, model, (id) => normalizeMistralToolCallId(id));
31
- let payload = buildChatPayload(model, context, transformedMessages, options);
32
- const nextPayload = await options?.onPayload?.(payload, model);
33
- if (nextPayload !== undefined) {
34
- payload = nextPayload;
35
- }
36
- const mistralStream = await mistral.chat.stream(payload, buildRequestOptions(model, options));
21
+ const mistralStream = await startMistralChatStream(model, context, options);
37
22
  stream.push({ type: "start", partial: output });
38
23
  await consumeChatStream(model, output, stream, mistralStream);
39
- if (options?.signal?.aborted) {
40
- throw new Error("Request was aborted");
41
- }
42
- if (output.stopReason === "aborted" || output.stopReason === "error") {
43
- throw new Error("An unknown error occurred");
44
- }
45
- stream.push({ type: "done", reason: output.stopReason, message: output });
46
- stream.end();
24
+ finishMistralChatStream(output, stream, options?.signal);
47
25
  }
48
26
  catch (error) {
49
- for (const block of output.content) {
50
- // partialArgs is only a streaming scratch buffer; never persist it.
51
- delete block.partialArgs;
52
- }
53
- output.stopReason = options?.signal?.aborted ? "aborted" : "error";
54
- output.errorMessage = formatMistralError(error);
55
- stream.push({ type: "error", reason: output.stopReason, error: output });
56
- stream.end();
27
+ failMistralChatStream(output, stream, error, options?.signal);
57
28
  }
58
29
  })();
59
30
  return stream;
60
31
  };
32
+ async function startMistralChatStream(model, context, options) {
33
+ const apiKey = options?.apiKey || getEnvApiKey(model.provider);
34
+ if (!apiKey)
35
+ throw new Error(`No API key for provider: ${model.provider}`);
36
+ const mistral = new Mistral({ apiKey, serverURL: model.baseUrl });
37
+ const normalizeMistralToolCallId = createMistralToolCallIdNormalizer();
38
+ const transformedMessages = transformMessages(context.messages, model, normalizeMistralToolCallId);
39
+ let payload = buildChatPayload(model, context, transformedMessages, options);
40
+ const nextPayload = await options?.onPayload?.(payload, model);
41
+ if (nextPayload !== undefined)
42
+ payload = nextPayload;
43
+ return mistral.chat.stream(payload, buildRequestOptions(model, options));
44
+ }
45
+ function finishMistralChatStream(output, stream, signal) {
46
+ if (signal?.aborted)
47
+ throw new Error("Request was aborted");
48
+ if (output.stopReason === "pending")
49
+ throw new Error("Mistral stream ended without a finish reason");
50
+ if (output.stopReason === "aborted" || output.stopReason === "error") {
51
+ throw new Error("An unknown error occurred");
52
+ }
53
+ stream.push({ type: "done", reason: output.stopReason, message: output });
54
+ stream.end();
55
+ }
56
+ function failMistralChatStream(output, stream, error, signal) {
57
+ for (const block of output.content) {
58
+ // partialArgs is only a streaming scratch buffer; never persist it.
59
+ delete block.partialArgs;
60
+ }
61
+ output.stopReason = signal?.aborted ? "aborted" : "error";
62
+ output.errorMessage = formatMistralError(error);
63
+ stream.push({ type: "error", reason: output.stopReason, error: output });
64
+ stream.end();
65
+ }
61
66
  /**
62
67
  * Maps provider-agnostic `SimpleStreamOptions` to Mistral options.
63
68
  */
@@ -159,14 +164,12 @@ function buildRequestOptions(model, options) {
159
164
  };
160
165
  if (options?.signal)
161
166
  requestOptions.signal = options.signal;
162
- const headers = {};
163
- if (model.headers)
164
- Object.assign(headers, model.headers);
165
- if (options?.headers)
166
- Object.assign(headers, options.headers);
167
+ const explicitHeaders = { ...model.headers, ...options?.headers };
168
+ const hasExplicitAffinity = Object.keys(explicitHeaders).some((name) => name.toLowerCase() === "x-affinity");
169
+ const headers = providerHeadersToRecord(explicitHeaders) ?? {};
167
170
  // Mistral infrastructure uses `x-affinity` for KV-cache reuse (prefix caching).
168
171
  // Respect explicit caller-provided header values.
169
- if (options?.sessionId && !headers["x-affinity"]) {
172
+ if (options?.sessionId && !hasExplicitAffinity) {
170
173
  headers["x-affinity"] = options.sessionId;
171
174
  }
172
175
  if (Object.keys(headers).length > 0) {
@@ -200,174 +203,188 @@ function buildChatPayload(model, context, messages, options) {
200
203
  }
201
204
  return payload;
202
205
  }
203
- async function consumeChatStream(model, output, stream, mistralStream) {
204
- let currentBlock = null;
205
- const blocks = output.content;
206
- const blockIndex = () => blocks.length - 1;
207
- const toolBlocksByKey = new Map();
208
- const finishCurrentBlock = (block) => {
209
- if (!block)
210
- return;
211
- if (block.type === "text") {
212
- stream.push({
213
- type: "text_end",
214
- contentIndex: blockIndex(),
215
- content: block.text,
216
- partial: output,
217
- });
218
- return;
219
- }
220
- if (block.type === "thinking") {
221
- stream.push({
222
- type: "thinking_end",
223
- contentIndex: blockIndex(),
224
- content: block.thinking,
225
- partial: output,
226
- });
227
- }
228
- };
229
- for await (const event of mistralStream) {
230
- const chunk = event.data;
231
- // Mistral's streamed CompletionChunk carries an id field. Keep the first non-empty one,
232
- // mirroring how OpenAI-style streaming exposes a stable response identifier per stream.
233
- output.responseId ||= chunk.id;
234
- if (chunk.usage) {
235
- output.usage.input = chunk.usage.promptTokens || 0;
236
- output.usage.output = chunk.usage.completionTokens || 0;
237
- output.usage.cacheRead = 0;
238
- output.usage.cacheWrite = 0;
239
- output.usage.totalTokens = chunk.usage.totalTokens || output.usage.input + output.usage.output;
240
- calculateCost(model, output.usage);
241
- }
242
- const choice = chunk.choices[0];
243
- if (!choice)
206
+ function currentMistralBlockIndex(state) {
207
+ return state.output.content.length - 1;
208
+ }
209
+ function finishCurrentMistralBlock(state) {
210
+ const block = state.currentBlock;
211
+ if (!block)
212
+ return;
213
+ if (block.type === "text") {
214
+ state.stream.push({
215
+ type: "text_end",
216
+ contentIndex: currentMistralBlockIndex(state),
217
+ content: block.text,
218
+ partial: state.output,
219
+ });
220
+ }
221
+ else {
222
+ state.stream.push({
223
+ type: "thinking_end",
224
+ contentIndex: currentMistralBlockIndex(state),
225
+ content: block.thinking,
226
+ partial: state.output,
227
+ });
228
+ }
229
+ state.currentBlock = null;
230
+ }
231
+ function appendMistralTextDelta(state, delta) {
232
+ let block = state.currentBlock;
233
+ if (block?.type !== "text") {
234
+ finishCurrentMistralBlock(state);
235
+ block = { type: "text", text: "" };
236
+ state.currentBlock = block;
237
+ state.output.content.push(block);
238
+ state.stream.push({
239
+ type: "text_start",
240
+ contentIndex: currentMistralBlockIndex(state),
241
+ partial: state.output,
242
+ });
243
+ }
244
+ block.text += delta;
245
+ state.stream.push({
246
+ type: "text_delta",
247
+ contentIndex: currentMistralBlockIndex(state),
248
+ delta,
249
+ partial: state.output,
250
+ });
251
+ }
252
+ function appendMistralThinkingDelta(state, delta) {
253
+ let block = state.currentBlock;
254
+ if (block?.type !== "thinking") {
255
+ finishCurrentMistralBlock(state);
256
+ block = { type: "thinking", thinking: "" };
257
+ state.currentBlock = block;
258
+ state.output.content.push(block);
259
+ state.stream.push({
260
+ type: "thinking_start",
261
+ contentIndex: currentMistralBlockIndex(state),
262
+ partial: state.output,
263
+ });
264
+ }
265
+ block.thinking += delta;
266
+ state.stream.push({
267
+ type: "thinking_delta",
268
+ contentIndex: currentMistralBlockIndex(state),
269
+ delta,
270
+ partial: state.output,
271
+ });
272
+ }
273
+ function consumeMistralContentDelta(state, content) {
274
+ if (content === null || content === undefined)
275
+ return;
276
+ const items = typeof content === "string" ? [content] : content;
277
+ for (const item of items) {
278
+ if (typeof item === "string") {
279
+ appendMistralTextDelta(state, sanitizeSurrogates(item));
244
280
  continue;
245
- if (choice.finishReason) {
246
- output.stopReason = mapChatStopReason(choice.finishReason);
247
281
  }
248
- const delta = choice.delta;
249
- if (delta.content !== null && delta.content !== undefined) {
250
- const contentItems = typeof delta.content === "string" ? [delta.content] : delta.content;
251
- for (const item of contentItems) {
252
- if (typeof item === "string") {
253
- const textDelta = sanitizeSurrogates(item);
254
- if (!currentBlock || currentBlock.type !== "text") {
255
- finishCurrentBlock(currentBlock);
256
- currentBlock = { type: "text", text: "" };
257
- output.content.push(currentBlock);
258
- stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
259
- }
260
- currentBlock.text += textDelta;
261
- stream.push({
262
- type: "text_delta",
263
- contentIndex: blockIndex(),
264
- delta: textDelta,
265
- partial: output,
266
- });
267
- continue;
268
- }
269
- if (item.type === "thinking") {
270
- const deltaText = item.thinking
271
- .map((part) => ("text" in part ? part.text : ""))
272
- .filter((text) => text.length > 0)
273
- .join("");
274
- const thinkingDelta = sanitizeSurrogates(deltaText);
275
- if (!thinkingDelta)
276
- continue;
277
- if (!currentBlock || currentBlock.type !== "thinking") {
278
- finishCurrentBlock(currentBlock);
279
- currentBlock = { type: "thinking", thinking: "" };
280
- output.content.push(currentBlock);
281
- stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
282
- }
283
- currentBlock.thinking += thinkingDelta;
284
- stream.push({
285
- type: "thinking_delta",
286
- contentIndex: blockIndex(),
287
- delta: thinkingDelta,
288
- partial: output,
289
- });
290
- continue;
291
- }
292
- if (item.type === "text") {
293
- const textDelta = sanitizeSurrogates(item.text);
294
- if (!currentBlock || currentBlock.type !== "text") {
295
- finishCurrentBlock(currentBlock);
296
- currentBlock = { type: "text", text: "" };
297
- output.content.push(currentBlock);
298
- stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
299
- }
300
- currentBlock.text += textDelta;
301
- stream.push({
302
- type: "text_delta",
303
- contentIndex: blockIndex(),
304
- delta: textDelta,
305
- partial: output,
306
- });
307
- }
308
- }
282
+ if (item.type === "thinking") {
283
+ const delta = item.thinking
284
+ .map((part) => ("text" in part ? part.text : ""))
285
+ .filter((text) => text.length > 0)
286
+ .join("");
287
+ const sanitized = sanitizeSurrogates(delta);
288
+ if (sanitized)
289
+ appendMistralThinkingDelta(state, sanitized);
290
+ continue;
309
291
  }
310
- const toolCalls = delta.toolCalls || [];
311
- for (const toolCall of toolCalls) {
312
- if (currentBlock) {
313
- finishCurrentBlock(currentBlock);
314
- currentBlock = null;
315
- }
316
- const callId = toolCall.id && toolCall.id !== "null"
317
- ? toolCall.id
318
- : deriveMistralToolCallId(`toolcall:${toolCall.index ?? 0}`, 0);
319
- const key = `${callId}:${toolCall.index || 0}`;
320
- const existingIndex = toolBlocksByKey.get(key);
321
- let block;
322
- if (existingIndex !== undefined) {
323
- const existing = output.content[existingIndex];
324
- if (existing?.type === "toolCall") {
325
- block = existing;
326
- }
327
- }
328
- if (!block) {
329
- block = {
330
- type: "toolCall",
331
- id: callId,
332
- name: toolCall.function.name,
333
- arguments: {},
334
- partialArgs: "",
335
- };
336
- output.content.push(block);
337
- toolBlocksByKey.set(key, output.content.length - 1);
338
- stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
339
- }
340
- const argsDelta = typeof toolCall.function.arguments === "string"
341
- ? toolCall.function.arguments
342
- : JSON.stringify(toolCall.function.arguments || {});
343
- block.partialArgs = (block.partialArgs || "") + argsDelta;
344
- block.arguments = parseStreamingJson(block.partialArgs);
345
- stream.push({
346
- type: "toolcall_delta",
347
- contentIndex: toolBlocksByKey.get(key),
348
- delta: argsDelta,
349
- partial: output,
350
- });
292
+ if (item.type === "text")
293
+ appendMistralTextDelta(state, sanitizeSurrogates(item.text));
294
+ }
295
+ }
296
+ function updateMistralChunkMetadata(state, chunk) {
297
+ state.output.responseId ||= chunk.id;
298
+ if (!chunk.usage)
299
+ return;
300
+ state.output.usage.input = chunk.usage.promptTokens || 0;
301
+ state.output.usage.output = chunk.usage.completionTokens || 0;
302
+ state.output.usage.cacheRead = 0;
303
+ state.output.usage.cacheWrite = 0;
304
+ state.output.usage.totalTokens = chunk.usage.totalTokens || state.output.usage.input + state.output.usage.output;
305
+ calculateCost(state.model, state.output.usage);
306
+ }
307
+ function resolveMistralToolBlock(state, toolCall) {
308
+ const callId = toolCall.id && toolCall.id !== "null"
309
+ ? toolCall.id
310
+ : deriveMistralToolCallId(`toolcall:${toolCall.index ?? 0}`, 0);
311
+ const key = `${callId}:${toolCall.index || 0}`;
312
+ const existingIndex = state.toolBlocksByKey.get(key);
313
+ if (existingIndex !== undefined) {
314
+ const existing = state.output.content[existingIndex];
315
+ if (existing?.type === "toolCall") {
316
+ return { block: existing, index: existingIndex };
351
317
  }
352
318
  }
353
- finishCurrentBlock(currentBlock);
354
- for (const index of toolBlocksByKey.values()) {
355
- const block = output.content[index];
319
+ const block = {
320
+ type: "toolCall",
321
+ id: callId,
322
+ name: toolCall.function.name,
323
+ arguments: {},
324
+ partialArgs: "",
325
+ };
326
+ state.output.content.push(block);
327
+ const index = state.output.content.length - 1;
328
+ state.toolBlocksByKey.set(key, index);
329
+ state.stream.push({ type: "toolcall_start", contentIndex: index, partial: state.output });
330
+ return { block, index };
331
+ }
332
+ function consumeMistralToolCall(state, toolCall) {
333
+ finishCurrentMistralBlock(state);
334
+ const { block, index } = resolveMistralToolBlock(state, toolCall);
335
+ const argsDelta = typeof toolCall.function.arguments === "string"
336
+ ? toolCall.function.arguments
337
+ : JSON.stringify(toolCall.function.arguments || {});
338
+ block.partialArgs = (block.partialArgs || "") + argsDelta;
339
+ block.arguments = parseStreamingJson(block.partialArgs);
340
+ state.stream.push({
341
+ type: "toolcall_delta",
342
+ contentIndex: index,
343
+ delta: argsDelta,
344
+ partial: state.output,
345
+ });
346
+ }
347
+ function consumeMistralChoice(state, choice) {
348
+ if (choice.finishReason)
349
+ state.output.stopReason = mapChatStopReason(choice.finishReason);
350
+ consumeMistralContentDelta(state, choice.delta.content);
351
+ for (const toolCall of choice.delta.toolCalls || [])
352
+ consumeMistralToolCall(state, toolCall);
353
+ }
354
+ function finalizeMistralToolCalls(state) {
355
+ for (const index of state.toolBlocksByKey.values()) {
356
+ const block = state.output.content[index];
356
357
  if (block.type !== "toolCall")
357
358
  continue;
358
359
  const toolBlock = block;
359
360
  toolBlock.arguments = parseStreamingJson(toolBlock.partialArgs);
360
- // Finalize in-place and strip the scratch buffer so replay only
361
- // carries parsed arguments.
362
361
  delete toolBlock.partialArgs;
363
- stream.push({
362
+ state.stream.push({
364
363
  type: "toolcall_end",
365
364
  contentIndex: index,
366
365
  toolCall: toolBlock,
367
- partial: output,
366
+ partial: state.output,
368
367
  });
369
368
  }
370
369
  }
370
+ async function consumeChatStream(model, output, stream, mistralStream) {
371
+ const state = {
372
+ model,
373
+ output,
374
+ stream,
375
+ currentBlock: null,
376
+ toolBlocksByKey: new Map(),
377
+ };
378
+ for await (const event of mistralStream) {
379
+ const chunk = event.data;
380
+ updateMistralChunkMetadata(state, chunk);
381
+ const choice = chunk.choices[0];
382
+ if (choice)
383
+ consumeMistralChoice(state, choice);
384
+ }
385
+ finishCurrentMistralBlock(state);
386
+ finalizeMistralToolCalls(state);
387
+ }
371
388
  function toFunctionTools(tools) {
372
389
  return tools.map((tool) => ({
373
390
  type: "function",
@@ -392,90 +409,95 @@ function stripSymbolKeys(value) {
392
409
  }
393
410
  return value;
394
411
  }
395
- function toChatMessages(messages, supportsImages) {
396
- const result = [];
397
- for (const msg of messages) {
398
- if (msg.role === "user") {
399
- if (typeof msg.content === "string") {
400
- result.push({ role: "user", content: sanitizeSurrogates(msg.content) });
401
- continue;
402
- }
403
- const hadImages = msg.content.some((item) => item.type === "image");
404
- const content = msg.content
405
- .filter((item) => item.type === "text" || supportsImages)
406
- .map((item) => {
407
- if (item.type === "text")
408
- return { type: "text", text: sanitizeSurrogates(item.text) };
409
- return { type: "image_url", imageUrl: `data:${item.mimeType};base64,${item.data}` };
410
- });
411
- if (content.length > 0) {
412
- result.push({ role: "user", content });
413
- continue;
414
- }
415
- if (hadImages && !supportsImages) {
416
- result.push({ role: "user", content: "(image omitted: model does not support images)" });
417
- }
418
- continue;
419
- }
420
- if (msg.role === "assistant") {
421
- const contentParts = [];
422
- const toolCalls = [];
423
- for (const block of msg.content) {
424
- if (block.type === "text") {
425
- if (block.text.trim().length > 0) {
426
- contentParts.push({ type: "text", text: sanitizeSurrogates(block.text) });
427
- }
428
- continue;
412
+ function convertMistralUserMessage(message, supportsImages) {
413
+ if (typeof message.content === "string") {
414
+ return { role: "user", content: sanitizeSurrogates(message.content) };
415
+ }
416
+ const hadImages = message.content.some((item) => item.type === "image");
417
+ const content = message.content
418
+ .filter((item) => item.type === "text" || supportsImages)
419
+ .map((item) => {
420
+ if (item.type === "text")
421
+ return { type: "text", text: sanitizeSurrogates(item.text) };
422
+ return { type: "image_url", imageUrl: `data:${item.mimeType};base64,${item.data}` };
423
+ });
424
+ if (content.length > 0)
425
+ return { role: "user", content };
426
+ return hadImages && !supportsImages
427
+ ? { role: "user", content: "(image omitted: model does not support images)" }
428
+ : undefined;
429
+ }
430
+ function convertMistralAssistantMessage(message) {
431
+ const contentParts = [];
432
+ const toolCalls = [];
433
+ for (const block of message.content) {
434
+ switch (block.type) {
435
+ case "text":
436
+ if (block.text.trim().length > 0) {
437
+ contentParts.push({ type: "text", text: sanitizeSurrogates(block.text) });
429
438
  }
430
- if (block.type === "thinking") {
431
- if (block.thinking.trim().length > 0) {
432
- contentParts.push({
433
- type: "thinking",
434
- thinking: [{ type: "text", text: sanitizeSurrogates(block.thinking) }],
435
- });
436
- }
437
- continue;
439
+ break;
440
+ case "thinking":
441
+ if (block.thinking.trim().length > 0) {
442
+ contentParts.push({
443
+ type: "thinking",
444
+ thinking: [{ type: "text", text: sanitizeSurrogates(block.thinking) }],
445
+ });
438
446
  }
447
+ break;
448
+ case "toolCall":
439
449
  toolCalls.push({
440
450
  id: block.id,
441
451
  type: "function",
442
452
  function: { name: block.name, arguments: JSON.stringify(block.arguments || {}) },
443
453
  });
444
- }
445
- const assistantMessage = { role: "assistant" };
446
- if (contentParts.length > 0)
447
- assistantMessage.content = contentParts;
448
- if (toolCalls.length > 0)
449
- assistantMessage.toolCalls = toolCalls;
450
- if (contentParts.length > 0 || toolCalls.length > 0)
451
- result.push(assistantMessage);
452
- continue;
453
- }
454
- const toolContent = [];
455
- const textResult = msg.content
456
- .filter((part) => part.type === "text")
457
- .map((part) => (part.type === "text" ? sanitizeSurrogates(part.text) : ""))
458
- .join("\n");
459
- const hasImages = msg.content.some((part) => part.type === "image");
460
- const toolText = buildToolResultText(textResult, hasImages, supportsImages, msg.isError);
461
- toolContent.push({ type: "text", text: toolText });
462
- for (const part of msg.content) {
463
- if (!supportsImages)
464
- continue;
465
- if (part.type !== "image")
466
- continue;
467
- toolContent.push({
468
- type: "image_url",
469
- imageUrl: `data:${part.mimeType};base64,${part.data}`,
470
- });
454
+ break;
471
455
  }
472
- result.push({
473
- role: "tool",
474
- toolCallId: msg.toolCallId,
475
- name: msg.toolName,
476
- content: toolContent,
456
+ }
457
+ const assistantMessage = { role: "assistant" };
458
+ if (contentParts.length > 0)
459
+ assistantMessage.content = contentParts;
460
+ if (toolCalls.length > 0)
461
+ assistantMessage.toolCalls = toolCalls;
462
+ return contentParts.length > 0 || toolCalls.length > 0 ? assistantMessage : undefined;
463
+ }
464
+ function convertMistralToolResultMessage(message, supportsImages) {
465
+ const toolContent = [];
466
+ const textResult = message.content
467
+ .filter((part) => part.type === "text")
468
+ .map((part) => (part.type === "text" ? sanitizeSurrogates(part.text) : ""))
469
+ .join("\n");
470
+ const hasImages = message.content.some((part) => part.type === "image");
471
+ const toolText = buildToolResultText(textResult, hasImages, supportsImages, message.isError);
472
+ toolContent.push({ type: "text", text: toolText });
473
+ for (const part of message.content) {
474
+ if (!supportsImages)
475
+ continue;
476
+ if (part.type !== "image")
477
+ continue;
478
+ toolContent.push({
479
+ type: "image_url",
480
+ imageUrl: `data:${part.mimeType};base64,${part.data}`,
477
481
  });
478
482
  }
483
+ return {
484
+ role: "tool",
485
+ toolCallId: message.toolCallId,
486
+ name: message.toolName,
487
+ content: toolContent,
488
+ };
489
+ }
490
+ function toChatMessages(messages, supportsImages) {
491
+ const result = [];
492
+ for (const message of messages) {
493
+ const converted = message.role === "user"
494
+ ? convertMistralUserMessage(message, supportsImages)
495
+ : message.role === "assistant"
496
+ ? convertMistralAssistantMessage(message)
497
+ : convertMistralToolResultMessage(message, supportsImages);
498
+ if (converted)
499
+ result.push(converted);
500
+ }
479
501
  return result;
480
502
  }
481
503
  function buildToolResultText(text, hasImages, supportsImages, isError) {