@absolutejs/ai 0.4.0 → 0.5.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,23 @@ This file is generated by `absolute-changelog` from the entries in
6
6
  `changelog/`. Edit an entry, not this file — and add new ones under
7
7
  `changelog/unreleased/`.
8
8
 
9
+ ## 0.5.0 — 2026-10-19
10
+
11
+ ### Breaking
12
+
13
+ - **AIToolDefinition.handler may return { text, images } as well as a string** (`AIToolDefinition`, `toolOutputText`)
14
+ _Migration:_ Code that calls a tool's handler itself (a wrapper, a test) now receives AIToolOutput, a string or { text, images }. Read its text with toolOutputText(output). Handlers that return a string need no change.
15
+
16
+ ### Added
17
+
18
+ - **Tools can return images for the model to see, such as a screenshot: a handler returns { text, images } and the tool loops send the images inside the tool result, mapped for Anthropic, OpenAI Responses, OpenAI Chat Completions, Gemini and Ollama** (`AIToolResult`, `AIToolOutput`, `toolOutputText`, `AIProviderContentBlock`, `anthropic`, `openai`, `openaiResponses`, `gemini`, `ollama`, `streamAIToSSE`, `streamAIWithTools`, `generateAIWithTools`, `hardenUntrustedTool`, `createAIStoredToolResultRecovery`)
19
+
20
+ ## 0.4.1 — 2026-10-17
21
+
22
+ ### Fixed
23
+
24
+ - **Preserve provider usage snapshots through stream interruptions, distinguish partial from final usage, and retain tool-input start events without application patches** (`AIUsageUpdateChunk`, `anthropic`, `openai`, `openaiResponses`, `AIChunk`, `StreamAIWithToolsEvent`, `AIToolInputStartChunk`)
25
+
9
26
  ## 0.4.0 — 2026-10-16
10
27
 
11
28
  ### Added
package/README.md CHANGED
@@ -560,3 +560,18 @@ are documented provider limits, not a substitute for runtime token budgeting.
560
560
  Regional features are omitted when they are not consistently supported.
561
561
 
562
562
  `@absolutejs/ai/research` separates an injected `SearchProvider` from evidence-only synthesis. It returns sources, findings, searches, limitations and explicit availability, and excludes invented source references/quotes. Quotation checks establish provenance; they are not a substitute for claim entailment or entity review. Hosts can set `maxRetries: 0` on one-shot or stream requests when they own attempt accounting, and independently bound `maxRepairAttempts` and disable context recovery.
563
+
564
+ ### Interrupted streams and usage
565
+
566
+ Provider streams can emit `usage_update` chunks before `done`. Each carries a
567
+ provider-reported cumulative snapshot: replace the previous snapshot for that
568
+ attempt; never sum snapshots. `final: true` means the provider reported complete
569
+ usage. An absent or false `final` means partial evidence, not a final bill.
570
+ Anthropic exposes message-start and message-delta usage; OpenAI Chat exposes its
571
+ usage chunk, and Responses preserves usage even on incomplete responses.
572
+
573
+ Persist snapshots while consuming the stream. Cancellation or transport failure
574
+ can prevent `done`, and no adapter can reconstruct usage the provider never sent.
575
+ `onUsage` still reports only complete usage, once per instrumented stream. Keep
576
+ credit reservations separate from charges and reconcile unknown outcomes in the
577
+ host application. The package does not invent token counts or decide billing policy.
package/changelog.json CHANGED
@@ -2,6 +2,63 @@
2
2
  "contract": 1,
3
3
  "name": "@absolutejs/ai",
4
4
  "releases": [
5
+ {
6
+ "changes": [
7
+ {
8
+ "kind": "breaking",
9
+ "migration": {
10
+ "instruction": "Code that calls a tool's handler itself (a wrapper, a test) now receives AIToolOutput, a string or { text, images }. Read its text with toolOutputText(output). Handlers that return a string need no change.",
11
+ "manual": true
12
+ },
13
+ "summary": "AIToolDefinition.handler may return { text, images } as well as a string",
14
+ "symbols": [
15
+ "AIToolDefinition",
16
+ "toolOutputText"
17
+ ]
18
+ },
19
+ {
20
+ "kind": "added",
21
+ "summary": "Tools can return images for the model to see, such as a screenshot: a handler returns { text, images } and the tool loops send the images inside the tool result, mapped for Anthropic, OpenAI Responses, OpenAI Chat Completions, Gemini and Ollama",
22
+ "symbols": [
23
+ "AIToolResult",
24
+ "AIToolOutput",
25
+ "toolOutputText",
26
+ "AIProviderContentBlock",
27
+ "anthropic",
28
+ "openai",
29
+ "openaiResponses",
30
+ "gemini",
31
+ "ollama",
32
+ "streamAIToSSE",
33
+ "streamAIWithTools",
34
+ "generateAIWithTools",
35
+ "hardenUntrustedTool",
36
+ "createAIStoredToolResultRecovery"
37
+ ]
38
+ }
39
+ ],
40
+ "date": "2026-10-19",
41
+ "version": "0.5.0"
42
+ },
43
+ {
44
+ "changes": [
45
+ {
46
+ "kind": "fixed",
47
+ "summary": "Preserve provider usage snapshots through stream interruptions, distinguish partial from final usage, and retain tool-input start events without application patches",
48
+ "symbols": [
49
+ "AIUsageUpdateChunk",
50
+ "anthropic",
51
+ "openai",
52
+ "openaiResponses",
53
+ "AIChunk",
54
+ "StreamAIWithToolsEvent",
55
+ "AIToolInputStartChunk"
56
+ ]
57
+ }
58
+ ],
59
+ "date": "2026-10-17",
60
+ "version": "0.4.1"
61
+ },
5
62
  {
6
63
  "changes": [
7
64
  {
package/dist/ai/index.js CHANGED
@@ -258,6 +258,14 @@ var parseAIMessage = (raw) => {
258
258
  };
259
259
  var serializeAIMessage = (message) => JSON.stringify(message);
260
260
 
261
+ // src/ai/toolOutput.ts
262
+ var toolOutputText = (output) => typeof output === "string" ? output : output.text;
263
+ var toolOutputImages = (output) => typeof output === "string" || !output.images?.length ? undefined : output.images;
264
+ var toolResultBlock = (toolUseId, output, content = toolOutputText(output)) => {
265
+ const images = toolOutputImages(output);
266
+ return images ? { content, images, tool_use_id: toolUseId, type: "tool_result" } : { content, tool_use_id: toolUseId, type: "tool_result" };
267
+ };
268
+
261
269
  // src/ai/inputCapacity.ts
262
270
  class AIInputError extends Error {
263
271
  code;
@@ -643,14 +651,11 @@ var processToolTurn = async (socket, options, state, messageId, conversationId,
643
651
  const toolResultBlocks = [];
644
652
  for (const toolCall of toolCalls) {
645
653
  await sendToolRunning(socket, toolCall.name, toolCall.input, messageId, conversationId);
646
- const result = await executeTool(options, toolCall.name, toolCall.input);
654
+ const output = await executeTool(options, toolCall.name, toolCall.input);
655
+ const result = toolOutputText(output);
647
656
  await sendToolComplete(socket, toolCall.name, result, messageId, conversationId);
648
657
  options.onToolUse?.(toolCall.name, toolCall.input, result);
649
- toolResultBlocks.push({
650
- content: result,
651
- tool_use_id: toolCall.id,
652
- type: "tool_result"
653
- });
658
+ toolResultBlocks.push(toolResultBlock(toolCall.id, output));
654
659
  state.executedToolKeys.add(serializeToolCall(toolCall.name, toolCall.input));
655
660
  }
656
661
  state.currentMessages.push({
@@ -1112,7 +1117,8 @@ var executeToolCalls = async function* (pendingToolCalls, options, renderers, tu
1112
1117
  turnState.allToolsHtml += renderers.toolRunning(toolCall.name, toolCall.input);
1113
1118
  yield { data: turnState.allToolsHtml, event: "tools" };
1114
1119
  }
1115
- const result = await executeTool2(options, toolCall.name, toolCall.input);
1120
+ const output = await executeTool2(options, toolCall.name, toolCall.input);
1121
+ const result = toolOutputText(output);
1116
1122
  if (options.structuredEvents) {
1117
1123
  yield {
1118
1124
  data: JSON.stringify({
@@ -1129,11 +1135,7 @@ var executeToolCalls = async function* (pendingToolCalls, options, renderers, tu
1129
1135
  }
1130
1136
  options.onToolUse?.(toolCall.name, toolCall.input, result);
1131
1137
  const resultContent = options.maxToolResultChars !== undefined ? truncateToolResult(result, options.maxToolResultChars) : result;
1132
- toolResultBlocks.push({
1133
- content: resultContent,
1134
- tool_use_id: toolCall.id,
1135
- type: "tool_result"
1136
- });
1138
+ toolResultBlocks.push(toolResultBlock(toolCall.id, output, resultContent));
1137
1139
  }
1138
1140
  return toolResultBlocks;
1139
1141
  };
@@ -1833,12 +1835,8 @@ var generateAIWithTools = async (options) => {
1833
1835
  const resultBlocks = await Promise.all(result.toolCalls.map(async (call) => {
1834
1836
  const definition = tools[call.name];
1835
1837
  const output = definition ? await Promise.resolve(definition.handler(call.input)).catch((err) => `Error: ${err instanceof Error ? err.message : String(err)}`) : `Error: unknown tool "${call.name}"`;
1836
- onToolUse?.(call.name, call.input, output);
1837
- return {
1838
- content: output,
1839
- tool_use_id: call.id,
1840
- type: "tool_result"
1841
- };
1838
+ onToolUse?.(call.name, call.input, toolOutputText(output));
1839
+ return toolResultBlock(call.id, output);
1842
1840
  }));
1843
1841
  return [
1844
1842
  ...result.requestMessages ?? messages,
@@ -1988,14 +1986,15 @@ var flushThinking3 = (blocks, thinking) => {
1988
1986
  var executeToolHandler = async (tools, call) => {
1989
1987
  const definition = tools[call.name];
1990
1988
  if (!definition) {
1991
- return { ok: false, result: `Error: unknown tool "${call.name}"` };
1989
+ return { ok: false, output: `Error: unknown tool "${call.name}"` };
1992
1990
  }
1993
1991
  try {
1994
- return { ok: true, result: await definition.handler(call.input) };
1992
+ const output = await definition.handler(call.input);
1993
+ return { ok: true, output };
1995
1994
  } catch (err) {
1996
1995
  return {
1997
1996
  ok: false,
1998
- result: `Error: ${err instanceof Error ? err.message : String(err)}`
1997
+ output: `Error: ${err instanceof Error ? err.message : String(err)}`
1999
1998
  };
2000
1999
  }
2001
2000
  };
@@ -2074,6 +2073,8 @@ var streamAIWithTools = async function* (options) {
2074
2073
  transcript: chunk.transcript,
2075
2074
  type: "audio"
2076
2075
  };
2076
+ } else if (chunk.type === "tool_input_start") {
2077
+ yield chunk;
2077
2078
  } else if (chunk.type === "tool_use") {
2078
2079
  thinking = flushThinking3(blocks, thinking);
2079
2080
  pending.push({ id: chunk.id, input: chunk.input, name: chunk.name });
@@ -2140,7 +2141,8 @@ var streamAIWithTools = async function* (options) {
2140
2141
  type: "tool_start"
2141
2142
  };
2142
2143
  const startedAt = Date.now();
2143
- const { ok, result } = await executeToolHandler(tools, call);
2144
+ const { ok, output } = await executeToolHandler(tools, call);
2145
+ const result = toolOutputText(output);
2144
2146
  executedKeys.add(toolCallKey(call));
2145
2147
  yield {
2146
2148
  id: call.id,
@@ -2151,11 +2153,7 @@ var streamAIWithTools = async function* (options) {
2151
2153
  result,
2152
2154
  type: "tool_result"
2153
2155
  };
2154
- resultBlocks.push({
2155
- content: result,
2156
- tool_use_id: call.id,
2157
- type: "tool_result"
2158
- });
2156
+ resultBlocks.push(toolResultBlock(call.id, output));
2159
2157
  if (ok && stopAfterTools?.includes(call.name)) {
2160
2158
  terminalToolSucceeded = true;
2161
2159
  break;
@@ -4874,7 +4872,7 @@ async function* tapStream(source, params, providerName) {
4874
4872
  let lastUsage;
4875
4873
  try {
4876
4874
  for await (const chunk of source) {
4877
- if (chunk.type === "done" && chunk.usage) {
4875
+ if ((chunk.type === "done" || chunk.type === "usage_update" && chunk.final) && chunk.usage) {
4878
4876
  lastUsage = chunk.usage;
4879
4877
  }
4880
4878
  yield chunk;
@@ -5177,7 +5175,10 @@ var convertToolBlock = (block) => {
5177
5175
  if (block.type === "tool_result") {
5178
5176
  return {
5179
5177
  call_id: block.tool_use_id,
5180
- output: typeof block.content === "string" ? block.content : "",
5178
+ output: block.images?.length ? [
5179
+ { text: block.content, type: "input_text" },
5180
+ ...block.images.map((source) => mapBlockToResponsesFormat({ source, type: "image" }))
5181
+ ] : block.content,
5181
5182
  type: "function_call_output"
5182
5183
  };
5183
5184
  }
@@ -5487,6 +5488,8 @@ var processCompleted = function* (parsed) {
5487
5488
  const { response } = parsed;
5488
5489
  const usage = extractUsage(response);
5489
5490
  const metadata = extractResponseMetadata(response);
5491
+ if (usage)
5492
+ yield { type: "usage_update", usage, metadata, final: true };
5490
5493
  if (isRecordArray(response.output)) {
5491
5494
  yield* extractCitationsFromOutput(response.output);
5492
5495
  yield* extractImageFromOutput(response.output);
@@ -5525,6 +5528,15 @@ var processSSEEvent = function* (eventType, parsed, pendingCalls, providerName)
5525
5528
  break;
5526
5529
  case "response.output_item.added":
5527
5530
  processOutputItemAdded(parsed, pendingCalls);
5531
+ if (isRecord2(parsed.item) && parsed.item.type === "function_call" && typeof parsed.item.name === "string") {
5532
+ const id = typeof parsed.item.call_id === "string" ? parsed.item.call_id : parsed.item.id;
5533
+ if (typeof id === "string")
5534
+ yield {
5535
+ type: "tool_input_start",
5536
+ id,
5537
+ name: parsed.item.name
5538
+ };
5539
+ }
5528
5540
  break;
5529
5541
  case "response.output_item.done":
5530
5542
  yield* processOutputItemDone(parsed);
@@ -5541,8 +5553,19 @@ var processSSEEvent = function* (eventType, parsed, pendingCalls, providerName)
5541
5553
  case "response.failed":
5542
5554
  case "response.incomplete":
5543
5555
  case "response.error":
5544
- case "error":
5556
+ case "error": {
5557
+ if (isRecord2(parsed.response)) {
5558
+ const usage = extractUsage(parsed.response);
5559
+ if (usage)
5560
+ yield {
5561
+ type: "usage_update",
5562
+ usage,
5563
+ metadata: extractResponseMetadata(parsed.response),
5564
+ final: false
5565
+ };
5566
+ }
5545
5567
  throw responseFailure(eventType, parsed, providerName);
5568
+ }
5546
5569
  }
5547
5570
  };
5548
5571
  var flushSSEBuffer = function* (state) {
@@ -5685,6 +5708,7 @@ var NOT_FOUND4 = -1;
5685
5708
  var isRecord3 = (value) => typeof value === "object" && value !== null;
5686
5709
  var isRecordArray2 = (value) => Array.isArray(value) && value.length > 0 && isRecord3(value[0]);
5687
5710
  var hasArrayContent = (msg) => typeof msg.content !== "string" && Array.isArray(msg.content);
5711
+ var imageUrl = (source) => source.type === "url" ? source.url : `data:${source.media_type};base64,${source.data}`;
5688
5712
  var buildToolMessages = (blocks) => {
5689
5713
  const toolUseBlocks = blocks.filter((block) => block.type === "tool_use");
5690
5714
  const toolResultBlocks = blocks.filter((block) => block.type === "tool_result");
@@ -5710,6 +5734,18 @@ var buildToolMessages = (blocks) => {
5710
5734
  tool_call_id: result.tool_use_id
5711
5735
  });
5712
5736
  }
5737
+ const images = toolResultBlocks.flatMap((result) => result.images ?? []);
5738
+ if (images.length > 0)
5739
+ messages.push({
5740
+ content: [
5741
+ { text: "Images returned by the tool calls above:", type: "text" },
5742
+ ...images.map((source) => ({
5743
+ image_url: { url: imageUrl(source) },
5744
+ type: "image_url"
5745
+ }))
5746
+ ],
5747
+ role: "user"
5748
+ });
5713
5749
  return messages;
5714
5750
  };
5715
5751
  var mapToolDefinitions = (tools) => tools.map((tool) => ({
@@ -5723,9 +5759,7 @@ var mapToolDefinitions = (tools) => tools.map((tool) => ({
5723
5759
  var mapContentBlockToOpenAI = (block) => {
5724
5760
  if (block.type === "image") {
5725
5761
  return {
5726
- image_url: {
5727
- url: block.source.type === "url" ? block.source.url : `data:${block.source.media_type};base64,${block.source.data}`
5728
- },
5762
+ image_url: { url: imageUrl(block.source) },
5729
5763
  type: "image_url"
5730
5764
  };
5731
5765
  }
@@ -5964,6 +5998,14 @@ var processDelta = function* (delta, pendingToolCalls) {
5964
5998
  }
5965
5999
  if (isRecordArray2(delta.tool_calls)) {
5966
6000
  processToolCallDeltas(delta.tool_calls, pendingToolCalls);
6001
+ for (const call of delta.tool_calls) {
6002
+ if (isRecord3(call.function) && typeof call.function.name === "string" && call.function.name)
6003
+ yield {
6004
+ type: "tool_input_start",
6005
+ id: typeof call.id === "string" ? call.id : String(call.index),
6006
+ name: call.function.name
6007
+ };
6008
+ }
5967
6009
  }
5968
6010
  if (Array.isArray(delta.annotations)) {
5969
6011
  for (const annotation of delta.annotations) {
@@ -6062,7 +6104,12 @@ var processSSELine2 = function* (line, pendingToolCalls, providerName) {
6062
6104
  }
6063
6105
  const usageUpdate = narrowUsageRecord(parsed);
6064
6106
  if (usageUpdate) {
6065
- yield { type: "usage_update", usage: usageUpdate };
6107
+ yield {
6108
+ type: "usage_update",
6109
+ usage: usageUpdate,
6110
+ final: Array.isArray(parsed.choices) && parsed.choices.length === 0,
6111
+ metadata: narrowResponseMetadata(parsed)
6112
+ };
6066
6113
  }
6067
6114
  const metadata = narrowResponseMetadata(parsed);
6068
6115
  if (metadata) {
@@ -6091,10 +6138,9 @@ var processSSELine2 = function* (line, pendingToolCalls, providerName) {
6091
6138
  }
6092
6139
  yield* processChoice(firstChoice, pendingToolCalls);
6093
6140
  };
6094
- var isUsageUpdate = (chunk) => chunk.type === "usage_update";
6095
6141
  var collectYieldableChunks = (line, pendingToolCalls, usageRef, metadataRef, providerName) => {
6096
6142
  const allChunks = Array.from(processSSELine2(line, pendingToolCalls, providerName));
6097
- const usageChunks = allChunks.filter(isUsageUpdate);
6143
+ const usageChunks = allChunks.filter((chunk) => chunk.type === "usage_update");
6098
6144
  const lastUsage = usageChunks.at(NOT_FOUND4);
6099
6145
  if (lastUsage) {
6100
6146
  usageRef.current = lastUsage.usage;
@@ -6111,7 +6157,7 @@ var collectYieldableChunks = (line, pendingToolCalls, usageRef, metadataRef, pro
6111
6157
  }
6112
6158
  };
6113
6159
  }
6114
- return allChunks.filter((chunk) => !isUsageUpdate(chunk) && chunk.type !== "response_metadata");
6160
+ return allChunks.filter((chunk) => chunk.type !== "response_metadata").map((chunk) => chunk.type === "usage_update" ? { ...chunk, metadata: metadataRef.current } : chunk);
6115
6161
  };
6116
6162
  var processSSELines2 = function* (lines, pendingToolCalls, usageRef, metadataRef, providerName) {
6117
6163
  for (const line of lines) {
@@ -6336,7 +6382,10 @@ var convertMessageContent = (content) => {
6336
6382
  if (typeof content === "string") {
6337
6383
  return [{ text: content }];
6338
6384
  }
6339
- return content.map(mapContentBlock).filter((mapped) => mapped !== null);
6385
+ return content.flatMap((block) => block.type === "tool_result" && block.images?.length ? [
6386
+ mapContentBlock(block),
6387
+ ...block.images.map((source) => mapContentBlock({ source, type: "image" }))
6388
+ ] : [mapContentBlock(block)]).filter((mapped) => mapped !== null);
6340
6389
  };
6341
6390
  var hasFunctionResponse = (content) => content.some((block) => block.type === "tool_result");
6342
6391
  var convertSingleMessage = (msg) => {
@@ -6613,7 +6662,6 @@ var API_VERSION = "2023-06-01";
6613
6662
  var DEFAULT_MAX_TOKENS = 32000;
6614
6663
  var EVENT_PREFIX_LENGTH2 = 7;
6615
6664
  var DATA_PREFIX_LENGTH2 = 6;
6616
- var EMPTY_CHUNKS = [];
6617
6665
  var isRecord5 = (val) => typeof val === "object" && val !== null;
6618
6666
  var mapContentBlock2 = (block) => {
6619
6667
  if (block.type === "thinking") {
@@ -6637,7 +6685,10 @@ var mapContentBlock2 = (block) => {
6637
6685
  }
6638
6686
  if (block.type === "tool_result") {
6639
6687
  return {
6640
- content: block.content,
6688
+ content: block.images?.length ? [
6689
+ { text: block.content, type: "text" },
6690
+ ...block.images.map((source) => ({ source, type: "image" }))
6691
+ ] : block.content,
6641
6692
  tool_use_id: block.tool_use_id,
6642
6693
  type: "tool_result"
6643
6694
  };
@@ -7141,11 +7192,21 @@ var processEvent = (eventType, parsed, state) => {
7141
7192
  case "message_delta": {
7142
7193
  handleMessageDelta(parsed, state);
7143
7194
  mergeMetadata(parsed, state);
7144
- return;
7195
+ return state.usage ? {
7196
+ type: "usage_update",
7197
+ usage: state.usage,
7198
+ metadata: state.metadata,
7199
+ final: false
7200
+ } : undefined;
7145
7201
  }
7146
7202
  case "message_start": {
7147
7203
  handleMessageStart(parsed, state);
7148
- return;
7204
+ return state.usage ? {
7205
+ type: "usage_update",
7206
+ usage: state.usage,
7207
+ metadata: state.metadata,
7208
+ final: false
7209
+ } : undefined;
7149
7210
  }
7150
7211
  case "message_stop": {
7151
7212
  mergeMetadata(parsed, state);
@@ -7179,49 +7240,6 @@ var processSingleEvent = (event, state) => {
7179
7240
  }
7180
7241
  return processEvent(eventType, parsed, state);
7181
7242
  };
7182
- var collectChunk = (event, state) => {
7183
- const chunk = processSingleEvent(event, state);
7184
- return chunk ? [chunk] : [];
7185
- };
7186
- var processBufferedEvents = (eventsText, state) => {
7187
- const events = eventsText.split(`
7188
-
7189
- `);
7190
- state.buffer = events.pop() ?? "";
7191
- return events.flatMap((event) => collectChunk(event, state));
7192
- };
7193
- var readNextChunks = async (reader, decoder, state, signal) => {
7194
- if (signal?.aborted) {
7195
- return { chunks: EMPTY_CHUNKS, done: true };
7196
- }
7197
- const { done, value } = await reader.read();
7198
- if (done) {
7199
- return { chunks: EMPTY_CHUNKS, done: true };
7200
- }
7201
- const rawText = state.buffer + decoder.decode(value, { stream: true });
7202
- const chunks = processBufferedEvents(rawText, state);
7203
- return { chunks, done: false };
7204
- };
7205
- var findDoneChunk = (chunks) => chunks.findIndex((c) => c.type === "done");
7206
- var sseStreamLoop = async (reader, decoder, state, signal) => {
7207
- const result = await readNextChunks(reader, decoder, state, signal);
7208
- if (result.done) {
7209
- return { chunks: result.chunks, finished: true };
7210
- }
7211
- const doneIdx = findDoneChunk(result.chunks);
7212
- if (doneIdx >= 0) {
7213
- return { chunks: result.chunks.slice(0, doneIdx + 1), finished: true };
7214
- }
7215
- return { chunks: result.chunks, finished: false };
7216
- };
7217
- async function* streamChunks(reader, decoder, state, signal) {
7218
- let finished = false;
7219
- while (!finished) {
7220
- const result = await sseStreamLoop(reader, decoder, state, signal);
7221
- ({ finished } = result);
7222
- yield* result.chunks;
7223
- }
7224
- }
7225
7243
  async function* parseSSEStream4(body, providerName, signal) {
7226
7244
  const reader = body.getReader();
7227
7245
  const decoder = new TextDecoder;
@@ -7239,8 +7257,34 @@ async function* parseSSEStream4(body, providerName, signal) {
7239
7257
  providerBlockInputJson: ""
7240
7258
  };
7241
7259
  try {
7242
- yield* streamChunks(reader, decoder, state, signal);
7260
+ while (true) {
7261
+ signal?.throwIfAborted();
7262
+ const { done, value } = await reader.read();
7263
+ if (done)
7264
+ return;
7265
+ const events = (state.buffer + decoder.decode(value, { stream: true })).split(`
7266
+
7267
+ `);
7268
+ state.buffer = events.pop() ?? "";
7269
+ for (const event of events) {
7270
+ const chunk = processSingleEvent(event, state);
7271
+ if (!chunk)
7272
+ continue;
7273
+ if (chunk.type === "tool_input" && chunk.receivedChars === 0)
7274
+ yield {
7275
+ type: "tool_input_start",
7276
+ id: chunk.id,
7277
+ name: chunk.name
7278
+ };
7279
+ yield chunk;
7280
+ if (chunk.type === "done")
7281
+ return;
7282
+ }
7283
+ }
7243
7284
  } finally {
7285
+ await reader.cancel().catch(() => {
7286
+ return;
7287
+ });
7244
7288
  reader.releaseLock();
7245
7289
  }
7246
7290
  }
@@ -7390,8 +7434,10 @@ var convertMessage2 = (msg) => {
7390
7434
  });
7391
7435
  }
7392
7436
  for (const block of toolResultBlocks) {
7437
+ const images = (block.images ?? []).flatMap((source) => source.type === "base64" ? [source.data] : []);
7393
7438
  results.push({
7394
7439
  content: typeof block.content === "string" ? block.content : "",
7440
+ ...images.length > 0 ? { images } : {},
7395
7441
  role: "tool"
7396
7442
  });
7397
7443
  }
@@ -8998,6 +9044,12 @@ var createAIStoredToolResultRecovery = (toolNames) => async ({ params }) => {
8998
9044
  if (block.type !== "tool_result" || !eligible.has(block.tool_use_id) || block === newest)
8999
9045
  return block;
9000
9046
  const marker = "[Earlier lookup removed from working context. Its immutable original remains saved. The original tool call and source/version arguments above are unchanged: repeat that lookup if needed to verify facts. Do not infer facts from this marker.]";
9047
+ if (block.images?.length)
9048
+ return {
9049
+ content: marker,
9050
+ tool_use_id: block.tool_use_id,
9051
+ type: block.type
9052
+ };
9001
9053
  return block.content.length > marker.length ? { ...block, content: marker } : block;
9002
9054
  })
9003
9055
  }));
@@ -9139,10 +9191,11 @@ export {
9139
9191
  streamAIWithTools,
9140
9192
  streamWithAIContext,
9141
9193
  tableCard,
9194
+ toolOutputText,
9142
9195
  verifyOpenRouterWebhookSignature,
9143
9196
  withResilience,
9144
9197
  xai
9145
9198
  };
9146
9199
 
9147
- //# debugId=4ED2ED351363579D64756E2164756E21
9200
+ //# debugId=94F48F9A5CBF872A64756E2164756E21
9148
9201
  //# sourceMappingURL=index.js.map