@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 +6 -0
- package/README.md +15 -0
- package/changelog.json +19 -0
- package/dist/ai/index.js +82 -54
- package/dist/ai/index.js.map +8 -8
- package/dist/ai/providers/anthropic.js +41 -49
- package/dist/ai/providers/anthropic.js.map +4 -4
- package/dist/ai/providers/gemini.js +2 -2
- package/dist/ai/providers/gemini.js.map +3 -3
- package/dist/ai/providers/ollama.js +2 -2
- package/dist/ai/providers/ollama.js.map +4 -4
- package/dist/ai/providers/openai.js +41 -7
- package/dist/ai/providers/openai.js.map +5 -5
- package/dist/ai/providers/openaiCompatible.js +41 -7
- package/dist/ai/providers/openaiCompatible.js.map +5 -5
- package/dist/ai/providers/openaiResponses.js +25 -3
- package/dist/ai/providers/openaiResponses.js.map +4 -4
- package/dist/ai/providers/openrouter.js +80 -54
- package/dist/ai/providers/openrouter.js.map +6 -6
- package/dist/angular/ai/index.js +9 -7
- package/dist/angular/ai/index.js.map +2 -2
- package/dist/src/ai/streamAIWithTools.d.ts +4 -0
- package/dist/types/ai.d.ts +12 -1
- package/package.json +1 -1
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 {
|
|
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(
|
|
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) =>
|
|
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
|
-
|
|
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=
|
|
9175
|
+
//# debugId=A58174807DA8A73664756E2164756E21
|
|
9148
9176
|
//# sourceMappingURL=index.js.map
|