@absolutejs/ai 0.3.3 → 0.4.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 +18 -0
- package/changelog.json +37 -0
- package/dist/ai/index.js +97 -9
- package/dist/ai/index.js.map +4 -4
- package/dist/ai/providers/anthropic.js +79 -5
- package/dist/ai/providers/anthropic.js.map +3 -3
- package/dist/ai/providers/openrouter.js +79 -5
- package/dist/ai/providers/openrouter.js.map +3 -3
- package/dist/angular/ai/index.js +7 -9
- package/dist/angular/ai/index.js.map +2 -2
- package/dist/types/ai.d.ts +15 -1
- package/dist/types/anthropic.d.ts +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ 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.0 — 2026-10-16
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Report a tool call's input while it streams: a `tool_input` chunk, and a `preparing` tools event from streamAIToSSE** (`AIChunk`, `AIToolInputChunk`, `streamAIToSSE`)
|
|
14
|
+
A tool call with a long input, such as a whole file, takes the model a minute or more to write, and until now nothing arrived meanwhile. The Anthropic provider now yields a `tool_input` chunk when a tool call starts and every 2 KB of its input, with the tool name, the characters received and the top-level string fields already complete (a path, a name; at most 200 characters each, never read from inside other values). With structured events, streamAIToSSE turns it into a `tools` event with status `preparing`. Consumers that do not know the chunk can ignore it.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **streamAIToSSE sends a turn's text back as one block, not one per streamed delta, so agent loops stop re-writing their prompt cache** (`streamAIToSSE`)
|
|
19
|
+
streamAIToSSE stored every streamed text delta as its own content block, so an agent loop sent each turn's text back as dozens of blocks. Anthropic looks back only 20 blocks for an earlier cache entry, and a turn with a few sentences of text pushed the previous request's entry out of reach: the next request re-wrote the whole history at the cache-write price. A run of text is now one block, as generateAI and streamAIWithTools already kept it.
|
|
20
|
+
|
|
21
|
+
## 0.3.4 — 2026-10-15
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **Keep a prompt-cache breakpoint on the previous turn's user message as well as the last message, so an agent loop's next request reads its cached history by exact position instead of re-writing it; a production Studio build re-wrote its whole history on 7 of 48 requests and those writes were about half its cost.** (`anthropic`)
|
|
26
|
+
|
|
9
27
|
## 0.3.3 — 2026-10-10
|
|
10
28
|
|
|
11
29
|
### Fixed
|
package/changelog.json
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
"contract": 1,
|
|
3
3
|
"name": "@absolutejs/ai",
|
|
4
4
|
"releases": [
|
|
5
|
+
{
|
|
6
|
+
"changes": [
|
|
7
|
+
{
|
|
8
|
+
"detail": "streamAIToSSE stored every streamed text delta as its own content block, so an agent loop sent each turn's text back as dozens of blocks. Anthropic looks back only 20 blocks for an earlier cache entry, and a turn with a few sentences of text pushed the previous request's entry out of reach: the next request re-wrote the whole history at the cache-write price. A run of text is now one block, as generateAI and streamAIWithTools already kept it.",
|
|
9
|
+
"kind": "fixed",
|
|
10
|
+
"summary": "streamAIToSSE sends a turn's text back as one block, not one per streamed delta, so agent loops stop re-writing their prompt cache",
|
|
11
|
+
"symbols": [
|
|
12
|
+
"streamAIToSSE"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"detail": "A tool call with a long input, such as a whole file, takes the model a minute or more to write, and until now nothing arrived meanwhile. The Anthropic provider now yields a `tool_input` chunk when a tool call starts and every 2 KB of its input, with the tool name, the characters received and the top-level string fields already complete (a path, a name; at most 200 characters each, never read from inside other values). With structured events, streamAIToSSE turns it into a `tools` event with status `preparing`. Consumers that do not know the chunk can ignore it.",
|
|
17
|
+
"kind": "added",
|
|
18
|
+
"summary": "Report a tool call's input while it streams: a `tool_input` chunk, and a `preparing` tools event from streamAIToSSE",
|
|
19
|
+
"symbols": [
|
|
20
|
+
"AIChunk",
|
|
21
|
+
"AIToolInputChunk",
|
|
22
|
+
"streamAIToSSE"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"date": "2026-10-16",
|
|
27
|
+
"version": "0.4.0"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"changes": [
|
|
31
|
+
{
|
|
32
|
+
"kind": "fixed",
|
|
33
|
+
"summary": "Keep a prompt-cache breakpoint on the previous turn's user message as well as the last message, so an agent loop's next request reads its cached history by exact position instead of re-writing it; a production Studio build re-wrote its whole history on 7 of 48 requests and those writes were about half its cost.",
|
|
34
|
+
"symbols": [
|
|
35
|
+
"anthropic"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"date": "2026-10-15",
|
|
40
|
+
"version": "0.3.4"
|
|
41
|
+
},
|
|
5
42
|
{
|
|
6
43
|
"changes": [
|
|
7
44
|
{
|
package/dist/ai/index.js
CHANGED
|
@@ -999,10 +999,11 @@ var maybeFlushThinking = (chunkState) => {
|
|
|
999
999
|
};
|
|
1000
1000
|
var processTextChunk = function* (content, chunkState, renderers, fullResponse, options) {
|
|
1001
1001
|
maybeFlushThinking(chunkState);
|
|
1002
|
-
chunkState.contentBlocks.
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1002
|
+
const last = chunkState.contentBlocks.at(-1);
|
|
1003
|
+
if (last?.type === "text")
|
|
1004
|
+
last.content += content;
|
|
1005
|
+
else
|
|
1006
|
+
chunkState.contentBlocks.push({ content, type: "text" });
|
|
1006
1007
|
yield contentEvent(options, renderers, content, fullResponse + content);
|
|
1007
1008
|
};
|
|
1008
1009
|
var processImageChunk = function* (chunk, renderers, options) {
|
|
@@ -1056,6 +1057,19 @@ var processChunk = function* (chunk, chunkState, renderers, options, fullRespons
|
|
|
1056
1057
|
case "tool_use":
|
|
1057
1058
|
processToolUseChunk(chunk, chunkState);
|
|
1058
1059
|
break;
|
|
1060
|
+
case "tool_input":
|
|
1061
|
+
if (options.structuredEvents)
|
|
1062
|
+
yield {
|
|
1063
|
+
data: JSON.stringify({
|
|
1064
|
+
fields: chunk.fields,
|
|
1065
|
+
id: chunk.id,
|
|
1066
|
+
name: chunk.name,
|
|
1067
|
+
receivedChars: chunk.receivedChars,
|
|
1068
|
+
status: "preparing"
|
|
1069
|
+
}),
|
|
1070
|
+
event: "tools"
|
|
1071
|
+
};
|
|
1072
|
+
break;
|
|
1059
1073
|
case "provider_event":
|
|
1060
1074
|
maybeFlushThinking(chunkState);
|
|
1061
1075
|
chunkState.contentBlocks.push({
|
|
@@ -6735,9 +6749,15 @@ var buildRequestBody3 = (params, configuredMax, configCaching) => {
|
|
|
6735
6749
|
const options = requestOptionsFor(params);
|
|
6736
6750
|
const messages = params.messages.filter((msg) => msg.role !== "system").map(mapMessage);
|
|
6737
6751
|
if (caching && messages.length > 1) {
|
|
6738
|
-
const
|
|
6752
|
+
const lastIndex = messages.length - 1;
|
|
6753
|
+
const last = messages[lastIndex];
|
|
6739
6754
|
if (last) {
|
|
6740
|
-
messages[
|
|
6755
|
+
messages[lastIndex] = cacheLastContentBlock(last);
|
|
6756
|
+
}
|
|
6757
|
+
const previousUserIndex = messages.findLastIndex((message, index) => index < lastIndex - 1 && message.role === "user");
|
|
6758
|
+
const previous = messages[previousUserIndex];
|
|
6759
|
+
if (previous) {
|
|
6760
|
+
messages[previousUserIndex] = cacheLastContentBlock(previous);
|
|
6741
6761
|
}
|
|
6742
6762
|
}
|
|
6743
6763
|
const max = typeof params.maxTokens === "number" ? params.maxTokens : configuredMax;
|
|
@@ -6872,14 +6892,79 @@ var getNumber = (obj, key) => {
|
|
|
6872
6892
|
}
|
|
6873
6893
|
return 0;
|
|
6874
6894
|
};
|
|
6895
|
+
var TOOL_INPUT_PROGRESS_CHARS = 2048;
|
|
6896
|
+
var TOOL_INPUT_FIELD_MAX_CHARS = 200;
|
|
6897
|
+
var readJsonString = (json, start) => {
|
|
6898
|
+
for (let index = start + 1;index < json.length; index += 1) {
|
|
6899
|
+
if (json[index] === "\\")
|
|
6900
|
+
index += 1;
|
|
6901
|
+
else if (json[index] === '"')
|
|
6902
|
+
return { end: index + 1, raw: json.slice(start + 1, index) };
|
|
6903
|
+
}
|
|
6904
|
+
return;
|
|
6905
|
+
};
|
|
6906
|
+
var decodeJsonString = (raw) => {
|
|
6907
|
+
try {
|
|
6908
|
+
const value = JSON.parse(`"${raw}"`);
|
|
6909
|
+
return typeof value === "string" ? value : undefined;
|
|
6910
|
+
} catch {
|
|
6911
|
+
return;
|
|
6912
|
+
}
|
|
6913
|
+
};
|
|
6914
|
+
var completeStringFields = (partialJson) => {
|
|
6915
|
+
const fields = {};
|
|
6916
|
+
let depth = 0;
|
|
6917
|
+
let key;
|
|
6918
|
+
let index = 0;
|
|
6919
|
+
while (index < partialJson.length) {
|
|
6920
|
+
const character = partialJson[index];
|
|
6921
|
+
if (character === '"') {
|
|
6922
|
+
const text = readJsonString(partialJson, index);
|
|
6923
|
+
if (!text)
|
|
6924
|
+
break;
|
|
6925
|
+
if (depth === 1 && key === undefined) {
|
|
6926
|
+
const after = partialJson.slice(text.end).trimStart();
|
|
6927
|
+
if (after.startsWith(":"))
|
|
6928
|
+
key = text.raw;
|
|
6929
|
+
} else if (depth === 1 && key !== undefined) {
|
|
6930
|
+
const value = decodeJsonString(text.raw);
|
|
6931
|
+
if (value !== undefined && value.length <= TOOL_INPUT_FIELD_MAX_CHARS)
|
|
6932
|
+
fields[key] = value;
|
|
6933
|
+
key = undefined;
|
|
6934
|
+
}
|
|
6935
|
+
index = text.end;
|
|
6936
|
+
continue;
|
|
6937
|
+
}
|
|
6938
|
+
if (character === "{" || character === "[")
|
|
6939
|
+
depth += 1;
|
|
6940
|
+
else if (character === "}" || character === "]")
|
|
6941
|
+
depth -= 1;
|
|
6942
|
+
else if (character === "," && depth === 1)
|
|
6943
|
+
key = undefined;
|
|
6944
|
+
index += 1;
|
|
6945
|
+
}
|
|
6946
|
+
return fields;
|
|
6947
|
+
};
|
|
6948
|
+
var toolInputProgress = (state) => {
|
|
6949
|
+
state.toolInputReported = state.toolInputJson.length;
|
|
6950
|
+
return {
|
|
6951
|
+
fields: completeStringFields(state.toolInputJson),
|
|
6952
|
+
id: state.currentToolId,
|
|
6953
|
+
name: state.currentToolName,
|
|
6954
|
+
receivedChars: state.toolInputJson.length,
|
|
6955
|
+
type: "tool_input"
|
|
6956
|
+
};
|
|
6957
|
+
};
|
|
6875
6958
|
var handleContentBlockStart = (parsed, state) => {
|
|
6876
6959
|
const block = getRecord(parsed, "content_block");
|
|
6877
6960
|
if (block && block.type === "tool_use") {
|
|
6878
6961
|
state.currentToolId = getString(block, "id");
|
|
6879
6962
|
state.currentToolName = getString(block, "name");
|
|
6880
6963
|
state.toolInputJson = "";
|
|
6964
|
+
state.toolInputReported = 0;
|
|
6881
6965
|
state.isThinkingBlock = false;
|
|
6882
6966
|
state.currentProviderBlock = undefined;
|
|
6967
|
+
return toolInputProgress(state);
|
|
6883
6968
|
} else if (block && block.type === "thinking") {
|
|
6884
6969
|
state.isThinkingBlock = true;
|
|
6885
6970
|
state.thinkingSignature = "";
|
|
@@ -6889,6 +6974,7 @@ var handleContentBlockStart = (parsed, state) => {
|
|
|
6889
6974
|
state.currentProviderBlock = block && block.type !== "text" ? { ...block } : undefined;
|
|
6890
6975
|
state.providerBlockInputJson = "";
|
|
6891
6976
|
}
|
|
6977
|
+
return;
|
|
6892
6978
|
};
|
|
6893
6979
|
var handleContentBlockDelta = (parsed, state) => {
|
|
6894
6980
|
const delta = getRecord(parsed, "delta");
|
|
@@ -6923,6 +7009,8 @@ var handleContentBlockDelta = (parsed, state) => {
|
|
|
6923
7009
|
state.providerBlockInputJson += getString(delta, "partial_json");
|
|
6924
7010
|
} else {
|
|
6925
7011
|
state.toolInputJson += getString(delta, "partial_json");
|
|
7012
|
+
if (state.toolInputJson.length - state.toolInputReported >= TOOL_INPUT_PROGRESS_CHARS)
|
|
7013
|
+
return toolInputProgress(state);
|
|
6926
7014
|
}
|
|
6927
7015
|
}
|
|
6928
7016
|
if (delta.type === "signature_delta") {
|
|
@@ -7042,8 +7130,7 @@ var handleError = (parsed, state) => {
|
|
|
7042
7130
|
var processEvent = (eventType, parsed, state) => {
|
|
7043
7131
|
switch (eventType) {
|
|
7044
7132
|
case "content_block_start": {
|
|
7045
|
-
handleContentBlockStart(parsed, state);
|
|
7046
|
-
return;
|
|
7133
|
+
return handleContentBlockStart(parsed, state);
|
|
7047
7134
|
}
|
|
7048
7135
|
case "content_block_delta": {
|
|
7049
7136
|
return handleContentBlockDelta(parsed, state);
|
|
@@ -7146,6 +7233,7 @@ async function* parseSSEStream4(body, providerName, signal) {
|
|
|
7146
7233
|
stopReason: "",
|
|
7147
7234
|
thinkingSignature: "",
|
|
7148
7235
|
toolInputJson: "",
|
|
7236
|
+
toolInputReported: 0,
|
|
7149
7237
|
usage: undefined,
|
|
7150
7238
|
providerName,
|
|
7151
7239
|
providerBlockInputJson: ""
|
|
@@ -9056,5 +9144,5 @@ export {
|
|
|
9056
9144
|
xai
|
|
9057
9145
|
};
|
|
9058
9146
|
|
|
9059
|
-
//# debugId=
|
|
9147
|
+
//# debugId=4ED2ED351363579D64756E2164756E21
|
|
9060
9148
|
//# sourceMappingURL=index.js.map
|