@absolutejs/ai 0.4.0 → 0.4.1

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,12 @@ 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.4.1 — 2026-10-17
10
+
11
+ ### Fixed
12
+
13
+ - **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`)
14
+
9
15
  ## 0.4.0 — 2026-10-16
10
16
 
11
17
  ### 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,25 @@
2
2
  "contract": 1,
3
3
  "name": "@absolutejs/ai",
4
4
  "releases": [
5
+ {
6
+ "changes": [
7
+ {
8
+ "kind": "fixed",
9
+ "summary": "Preserve provider usage snapshots through stream interruptions, distinguish partial from final usage, and retain tool-input start events without application patches",
10
+ "symbols": [
11
+ "AIUsageUpdateChunk",
12
+ "anthropic",
13
+ "openai",
14
+ "openaiResponses",
15
+ "AIChunk",
16
+ "StreamAIWithToolsEvent",
17
+ "AIToolInputStartChunk"
18
+ ]
19
+ }
20
+ ],
21
+ "date": "2026-10-17",
22
+ "version": "0.4.1"
23
+ },
5
24
  {
6
25
  "changes": [
7
26
  {
package/dist/ai/index.js CHANGED
@@ -2074,6 +2074,8 @@ var streamAIWithTools = async function* (options) {
2074
2074
  transcript: chunk.transcript,
2075
2075
  type: "audio"
2076
2076
  };
2077
+ } else if (chunk.type === "tool_input_start") {
2078
+ yield chunk;
2077
2079
  } else if (chunk.type === "tool_use") {
2078
2080
  thinking = flushThinking3(blocks, thinking);
2079
2081
  pending.push({ id: chunk.id, input: chunk.input, name: chunk.name });
@@ -4874,7 +4876,7 @@ async function* tapStream(source, params, providerName) {
4874
4876
  let lastUsage;
4875
4877
  try {
4876
4878
  for await (const chunk of source) {
4877
- if (chunk.type === "done" && chunk.usage) {
4879
+ if ((chunk.type === "done" || chunk.type === "usage_update" && chunk.final) && chunk.usage) {
4878
4880
  lastUsage = chunk.usage;
4879
4881
  }
4880
4882
  yield chunk;
@@ -5487,6 +5489,8 @@ var processCompleted = function* (parsed) {
5487
5489
  const { response } = parsed;
5488
5490
  const usage = extractUsage(response);
5489
5491
  const metadata = extractResponseMetadata(response);
5492
+ if (usage)
5493
+ yield { type: "usage_update", usage, metadata, final: true };
5490
5494
  if (isRecordArray(response.output)) {
5491
5495
  yield* extractCitationsFromOutput(response.output);
5492
5496
  yield* extractImageFromOutput(response.output);
@@ -5525,6 +5529,15 @@ var processSSEEvent = function* (eventType, parsed, pendingCalls, providerName)
5525
5529
  break;
5526
5530
  case "response.output_item.added":
5527
5531
  processOutputItemAdded(parsed, pendingCalls);
5532
+ if (isRecord2(parsed.item) && parsed.item.type === "function_call" && typeof parsed.item.name === "string") {
5533
+ const id = typeof parsed.item.call_id === "string" ? parsed.item.call_id : parsed.item.id;
5534
+ if (typeof id === "string")
5535
+ yield {
5536
+ type: "tool_input_start",
5537
+ id,
5538
+ name: parsed.item.name
5539
+ };
5540
+ }
5528
5541
  break;
5529
5542
  case "response.output_item.done":
5530
5543
  yield* processOutputItemDone(parsed);
@@ -5541,8 +5554,19 @@ var processSSEEvent = function* (eventType, parsed, pendingCalls, providerName)
5541
5554
  case "response.failed":
5542
5555
  case "response.incomplete":
5543
5556
  case "response.error":
5544
- case "error":
5557
+ case "error": {
5558
+ if (isRecord2(parsed.response)) {
5559
+ const usage = extractUsage(parsed.response);
5560
+ if (usage)
5561
+ yield {
5562
+ type: "usage_update",
5563
+ usage,
5564
+ metadata: extractResponseMetadata(parsed.response),
5565
+ final: false
5566
+ };
5567
+ }
5545
5568
  throw responseFailure(eventType, parsed, providerName);
5569
+ }
5546
5570
  }
5547
5571
  };
5548
5572
  var flushSSEBuffer = function* (state) {
@@ -5964,6 +5988,14 @@ var processDelta = function* (delta, pendingToolCalls) {
5964
5988
  }
5965
5989
  if (isRecordArray2(delta.tool_calls)) {
5966
5990
  processToolCallDeltas(delta.tool_calls, pendingToolCalls);
5991
+ for (const call of delta.tool_calls) {
5992
+ if (isRecord3(call.function) && typeof call.function.name === "string" && call.function.name)
5993
+ yield {
5994
+ type: "tool_input_start",
5995
+ id: typeof call.id === "string" ? call.id : String(call.index),
5996
+ name: call.function.name
5997
+ };
5998
+ }
5967
5999
  }
5968
6000
  if (Array.isArray(delta.annotations)) {
5969
6001
  for (const annotation of delta.annotations) {
@@ -6062,7 +6094,12 @@ var processSSELine2 = function* (line, pendingToolCalls, providerName) {
6062
6094
  }
6063
6095
  const usageUpdate = narrowUsageRecord(parsed);
6064
6096
  if (usageUpdate) {
6065
- yield { type: "usage_update", usage: usageUpdate };
6097
+ yield {
6098
+ type: "usage_update",
6099
+ usage: usageUpdate,
6100
+ final: Array.isArray(parsed.choices) && parsed.choices.length === 0,
6101
+ metadata: narrowResponseMetadata(parsed)
6102
+ };
6066
6103
  }
6067
6104
  const metadata = narrowResponseMetadata(parsed);
6068
6105
  if (metadata) {
@@ -6091,10 +6128,9 @@ var processSSELine2 = function* (line, pendingToolCalls, providerName) {
6091
6128
  }
6092
6129
  yield* processChoice(firstChoice, pendingToolCalls);
6093
6130
  };
6094
- var isUsageUpdate = (chunk) => chunk.type === "usage_update";
6095
6131
  var collectYieldableChunks = (line, pendingToolCalls, usageRef, metadataRef, providerName) => {
6096
6132
  const allChunks = Array.from(processSSELine2(line, pendingToolCalls, providerName));
6097
- const usageChunks = allChunks.filter(isUsageUpdate);
6133
+ const usageChunks = allChunks.filter((chunk) => chunk.type === "usage_update");
6098
6134
  const lastUsage = usageChunks.at(NOT_FOUND4);
6099
6135
  if (lastUsage) {
6100
6136
  usageRef.current = lastUsage.usage;
@@ -6111,7 +6147,7 @@ var collectYieldableChunks = (line, pendingToolCalls, usageRef, metadataRef, pro
6111
6147
  }
6112
6148
  };
6113
6149
  }
6114
- return allChunks.filter((chunk) => !isUsageUpdate(chunk) && chunk.type !== "response_metadata");
6150
+ return allChunks.filter((chunk) => chunk.type !== "response_metadata").map((chunk) => chunk.type === "usage_update" ? { ...chunk, metadata: metadataRef.current } : chunk);
6115
6151
  };
6116
6152
  var processSSELines2 = function* (lines, pendingToolCalls, usageRef, metadataRef, providerName) {
6117
6153
  for (const line of lines) {
@@ -6613,7 +6649,6 @@ var API_VERSION = "2023-06-01";
6613
6649
  var DEFAULT_MAX_TOKENS = 32000;
6614
6650
  var EVENT_PREFIX_LENGTH2 = 7;
6615
6651
  var DATA_PREFIX_LENGTH2 = 6;
6616
- var EMPTY_CHUNKS = [];
6617
6652
  var isRecord5 = (val) => typeof val === "object" && val !== null;
6618
6653
  var mapContentBlock2 = (block) => {
6619
6654
  if (block.type === "thinking") {
@@ -7141,11 +7176,21 @@ var processEvent = (eventType, parsed, state) => {
7141
7176
  case "message_delta": {
7142
7177
  handleMessageDelta(parsed, state);
7143
7178
  mergeMetadata(parsed, state);
7144
- return;
7179
+ return state.usage ? {
7180
+ type: "usage_update",
7181
+ usage: state.usage,
7182
+ metadata: state.metadata,
7183
+ final: false
7184
+ } : undefined;
7145
7185
  }
7146
7186
  case "message_start": {
7147
7187
  handleMessageStart(parsed, state);
7148
- return;
7188
+ return state.usage ? {
7189
+ type: "usage_update",
7190
+ usage: state.usage,
7191
+ metadata: state.metadata,
7192
+ final: false
7193
+ } : undefined;
7149
7194
  }
7150
7195
  case "message_stop": {
7151
7196
  mergeMetadata(parsed, state);
@@ -7179,49 +7224,6 @@ var processSingleEvent = (event, state) => {
7179
7224
  }
7180
7225
  return processEvent(eventType, parsed, state);
7181
7226
  };
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
7227
  async function* parseSSEStream4(body, providerName, signal) {
7226
7228
  const reader = body.getReader();
7227
7229
  const decoder = new TextDecoder;
@@ -7239,8 +7241,34 @@ async function* parseSSEStream4(body, providerName, signal) {
7239
7241
  providerBlockInputJson: ""
7240
7242
  };
7241
7243
  try {
7242
- yield* streamChunks(reader, decoder, state, signal);
7244
+ while (true) {
7245
+ signal?.throwIfAborted();
7246
+ const { done, value } = await reader.read();
7247
+ if (done)
7248
+ return;
7249
+ const events = (state.buffer + decoder.decode(value, { stream: true })).split(`
7250
+
7251
+ `);
7252
+ state.buffer = events.pop() ?? "";
7253
+ for (const event of events) {
7254
+ const chunk = processSingleEvent(event, state);
7255
+ if (!chunk)
7256
+ continue;
7257
+ if (chunk.type === "tool_input" && chunk.receivedChars === 0)
7258
+ yield {
7259
+ type: "tool_input_start",
7260
+ id: chunk.id,
7261
+ name: chunk.name
7262
+ };
7263
+ yield chunk;
7264
+ if (chunk.type === "done")
7265
+ return;
7266
+ }
7267
+ }
7243
7268
  } finally {
7269
+ await reader.cancel().catch(() => {
7270
+ return;
7271
+ });
7244
7272
  reader.releaseLock();
7245
7273
  }
7246
7274
  }
@@ -9144,5 +9172,5 @@ export {
9144
9172
  xai
9145
9173
  };
9146
9174
 
9147
- //# debugId=4ED2ED351363579D64756E2164756E21
9175
+ //# debugId=A58174807DA8A73664756E2164756E21
9148
9176
  //# sourceMappingURL=index.js.map