@axlsdk/axl 0.22.3 → 0.23.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/dist/index.cjs +430 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +202 -8
- package/dist/index.d.ts +202 -8
- package/dist/index.js +430 -68
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -392,9 +392,15 @@ var NoConsensus = class extends AxlError {
|
|
|
392
392
|
}
|
|
393
393
|
};
|
|
394
394
|
var TimeoutError = class extends AxlError {
|
|
395
|
-
|
|
396
|
-
|
|
395
|
+
/** Latency attribution for the timed-out operation, when measurable. */
|
|
396
|
+
breakdown;
|
|
397
|
+
constructor(operation, timeoutMs, breakdown) {
|
|
398
|
+
super(
|
|
399
|
+
"TIMEOUT",
|
|
400
|
+
`${operation} exceeded timeout of ${timeoutMs}ms` + (breakdown ? ` (elapsed ${breakdown.elapsedMs}ms: queued ${breakdown.queuedMs}ms, retries ${breakdown.retryMs}ms, wire ${breakdown.wireMs}ms, other ${breakdown.otherMs}ms)` : "")
|
|
401
|
+
);
|
|
397
402
|
this.name = "TimeoutError";
|
|
403
|
+
if (breakdown) this.breakdown = breakdown;
|
|
398
404
|
}
|
|
399
405
|
};
|
|
400
406
|
function formatBudgetCost(cost) {
|
|
@@ -941,6 +947,7 @@ var ProviderError = class extends AxlError {
|
|
|
941
947
|
this.retryAfterMs = args.retryAfterMs;
|
|
942
948
|
this.requestId = args.requestId;
|
|
943
949
|
this.body = args.body;
|
|
950
|
+
if (args.timing) this.timing = args.timing;
|
|
944
951
|
}
|
|
945
952
|
};
|
|
946
953
|
function isRetryableStatus(status) {
|
|
@@ -992,7 +999,8 @@ function buildProviderError(args) {
|
|
|
992
999
|
message: args.message,
|
|
993
1000
|
retryAfterMs: args.headers ? parseRetryAfter(args.headers) : void 0,
|
|
994
1001
|
requestId: args.headers ? extractRequestId(args.headers) : void 0,
|
|
995
|
-
body: args.body
|
|
1002
|
+
body: args.body,
|
|
1003
|
+
timing: args.timing
|
|
996
1004
|
});
|
|
997
1005
|
}
|
|
998
1006
|
|
|
@@ -1029,14 +1037,33 @@ async function fetchWithRetry(input, init, opts) {
|
|
|
1029
1037
|
const maxRetries = opts?.maxRetries ?? MAX_RETRIES;
|
|
1030
1038
|
const governor = opts?.governor;
|
|
1031
1039
|
const provider = opts?.provider ?? "unknown";
|
|
1040
|
+
const observer = opts?.timing;
|
|
1032
1041
|
let acquired = false;
|
|
1042
|
+
let queuedMs = 0;
|
|
1033
1043
|
if (governor) {
|
|
1044
|
+
const acquireStart = Date.now();
|
|
1034
1045
|
await governor.acquire(init?.signal ?? void 0);
|
|
1035
1046
|
acquired = true;
|
|
1036
|
-
|
|
1047
|
+
queuedMs = Date.now() - acquireStart;
|
|
1048
|
+
}
|
|
1049
|
+
let firstDispatchedAt = 0;
|
|
1050
|
+
let dispatchedAt = 0;
|
|
1051
|
+
let headersAt = 0;
|
|
1052
|
+
const reportComplete = (attempts) => {
|
|
1053
|
+
observer?.onComplete?.({
|
|
1054
|
+
queuedMs,
|
|
1055
|
+
attempts,
|
|
1056
|
+
retryMs: dispatchedAt - firstDispatchedAt,
|
|
1057
|
+
dispatchedAt,
|
|
1058
|
+
headersAt
|
|
1059
|
+
});
|
|
1060
|
+
};
|
|
1037
1061
|
try {
|
|
1038
1062
|
for (let attempt = 0; ; attempt++) {
|
|
1039
1063
|
let res;
|
|
1064
|
+
dispatchedAt = Date.now();
|
|
1065
|
+
if (attempt === 0) firstDispatchedAt = dispatchedAt;
|
|
1066
|
+
observer?.onDispatch?.(attempt + 1, dispatchedAt);
|
|
1040
1067
|
try {
|
|
1041
1068
|
res = await fetch(input, { ...init, redirect: "manual" });
|
|
1042
1069
|
} catch (err) {
|
|
@@ -1051,11 +1078,14 @@ async function fetchWithRetry(input, init, opts) {
|
|
|
1051
1078
|
await sleep2(jitter(BASE_DELAY_MS * 2 ** attempt), init?.signal ?? void 0);
|
|
1052
1079
|
continue;
|
|
1053
1080
|
}
|
|
1081
|
+
headersAt = Date.now();
|
|
1054
1082
|
governor?.observe(res);
|
|
1055
1083
|
if (res.ok || !RETRYABLE_STATUS_CODES.has(res.status) || attempt >= maxRetries) {
|
|
1084
|
+
reportComplete(attempt + 1);
|
|
1056
1085
|
return res;
|
|
1057
1086
|
}
|
|
1058
1087
|
if (init?.signal?.aborted) {
|
|
1088
|
+
reportComplete(attempt + 1);
|
|
1059
1089
|
return res;
|
|
1060
1090
|
}
|
|
1061
1091
|
const retryAfterMs = parseRetryAfter(res.headers);
|
|
@@ -1067,6 +1097,86 @@ async function fetchWithRetry(input, init, opts) {
|
|
|
1067
1097
|
}
|
|
1068
1098
|
}
|
|
1069
1099
|
|
|
1100
|
+
// src/providers/call-timing.ts
|
|
1101
|
+
var CallTimingRecorder = class {
|
|
1102
|
+
fetchTiming;
|
|
1103
|
+
firstTokenAt;
|
|
1104
|
+
/** Cumulative time awaiting `reader.read()`, i.e. transport, not consumer. */
|
|
1105
|
+
readWaitMs = 0;
|
|
1106
|
+
/** Pass as `timing` in the `fetchWithRetry` options object. */
|
|
1107
|
+
observer = {
|
|
1108
|
+
onComplete: (timing) => {
|
|
1109
|
+
this.fetchTiming = timing;
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
/**
|
|
1113
|
+
* Await one body read, charging only the wait itself to `wireMs`. Time the
|
|
1114
|
+
* consumer spends between reads is excluded because it elapses outside this
|
|
1115
|
+
* call.
|
|
1116
|
+
*/
|
|
1117
|
+
async read(reader) {
|
|
1118
|
+
const start = Date.now();
|
|
1119
|
+
try {
|
|
1120
|
+
return await reader.read();
|
|
1121
|
+
} finally {
|
|
1122
|
+
this.readWaitMs += Date.now() - start;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
/** Stamp the first content delta. Idempotent — only the first call counts. */
|
|
1126
|
+
markFirstToken() {
|
|
1127
|
+
this.firstTokenAt ??= Date.now();
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Timing for a non-streaming call. Call once the response body is parsed.
|
|
1131
|
+
*
|
|
1132
|
+
* Also the right reading at an adapter's `!res.ok` throw site on BOTH
|
|
1133
|
+
* transports: headers have arrived, no body will be streamed, so
|
|
1134
|
+
* dispatch → now is the whole of what the provider cost us.
|
|
1135
|
+
*/
|
|
1136
|
+
chatTiming() {
|
|
1137
|
+
const t = this.fetchTiming;
|
|
1138
|
+
if (!t) return void 0;
|
|
1139
|
+
return {
|
|
1140
|
+
queuedMs: t.queuedMs,
|
|
1141
|
+
attempts: t.attempts,
|
|
1142
|
+
retryMs: t.retryMs,
|
|
1143
|
+
ttfbMs: Math.max(0, t.headersAt - t.dispatchedAt),
|
|
1144
|
+
wireMs: Math.max(0, Date.now() - t.dispatchedAt)
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
/** Timing for a streamed call. Call when building the terminal `done` chunk. */
|
|
1148
|
+
streamTiming() {
|
|
1149
|
+
const t = this.fetchTiming;
|
|
1150
|
+
if (!t) return void 0;
|
|
1151
|
+
const ttfbMs = Math.max(0, t.headersAt - t.dispatchedAt);
|
|
1152
|
+
return {
|
|
1153
|
+
queuedMs: t.queuedMs,
|
|
1154
|
+
attempts: t.attempts,
|
|
1155
|
+
retryMs: t.retryMs,
|
|
1156
|
+
ttfbMs,
|
|
1157
|
+
...this.firstTokenAt !== void 0 ? { firstTokenMs: Math.max(0, this.firstTokenAt - t.dispatchedAt) } : {},
|
|
1158
|
+
wireMs: ttfbMs + this.readWaitMs
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
async function* withCallTiming(recorder, source) {
|
|
1163
|
+
for await (const chunk of source) {
|
|
1164
|
+
if (chunk.type === "text_delta" || chunk.type === "thinking_delta") {
|
|
1165
|
+
recorder.markFirstToken();
|
|
1166
|
+
yield chunk;
|
|
1167
|
+
} else if (chunk.type === "done") {
|
|
1168
|
+
const timing = recorder.streamTiming();
|
|
1169
|
+
yield timing ? { ...chunk, timing } : chunk;
|
|
1170
|
+
} else {
|
|
1171
|
+
yield chunk;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function withChatTiming(recorder, response) {
|
|
1176
|
+
const timing = recorder.chatTiming();
|
|
1177
|
+
return timing ? { ...response, timing } : response;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1070
1180
|
// src/providers/rate-limiter.ts
|
|
1071
1181
|
function abortError() {
|
|
1072
1182
|
return Object.assign(new Error("The operation was aborted"), { name: "AbortError" });
|
|
@@ -1519,6 +1629,7 @@ var OpenAICompatibleProvider = class {
|
|
|
1519
1629
|
async chat(messages, options) {
|
|
1520
1630
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
1521
1631
|
const body = this.buildRequestBody(messages, options, false);
|
|
1632
|
+
const recorder = new CallTimingRecorder();
|
|
1522
1633
|
const res = await fetchWithRetry(
|
|
1523
1634
|
`${this.baseUrl}/chat/completions`,
|
|
1524
1635
|
{
|
|
@@ -1527,7 +1638,7 @@ var OpenAICompatibleProvider = class {
|
|
|
1527
1638
|
body: JSON.stringify(body),
|
|
1528
1639
|
signal: options.signal
|
|
1529
1640
|
},
|
|
1530
|
-
{ governor: this.governor, provider: this.name }
|
|
1641
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
1531
1642
|
);
|
|
1532
1643
|
if (!res.ok) {
|
|
1533
1644
|
const errorBody = await res.text();
|
|
@@ -1536,15 +1647,20 @@ var OpenAICompatibleProvider = class {
|
|
|
1536
1647
|
status: res.status,
|
|
1537
1648
|
headers: res.headers,
|
|
1538
1649
|
message: this.extractErrorMessage(errorBody, res.status),
|
|
1539
|
-
body: errorBody
|
|
1650
|
+
body: errorBody,
|
|
1651
|
+
timing: recorder.chatTiming()
|
|
1540
1652
|
});
|
|
1541
1653
|
}
|
|
1542
1654
|
const json = await res.json();
|
|
1543
|
-
return
|
|
1655
|
+
return withChatTiming(
|
|
1656
|
+
recorder,
|
|
1657
|
+
this.parseResponse(json, this.requestModel(body, options.model), body)
|
|
1658
|
+
);
|
|
1544
1659
|
}
|
|
1545
1660
|
async *stream(messages, options) {
|
|
1546
1661
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
1547
1662
|
const body = this.buildRequestBody(messages, options, true);
|
|
1663
|
+
const recorder = new CallTimingRecorder();
|
|
1548
1664
|
const res = await fetchWithRetry(
|
|
1549
1665
|
`${this.baseUrl}/chat/completions`,
|
|
1550
1666
|
{
|
|
@@ -1553,7 +1669,7 @@ var OpenAICompatibleProvider = class {
|
|
|
1553
1669
|
body: JSON.stringify(body),
|
|
1554
1670
|
signal: options.signal
|
|
1555
1671
|
},
|
|
1556
|
-
{ governor: this.governor, provider: this.name }
|
|
1672
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
1557
1673
|
);
|
|
1558
1674
|
if (!res.ok) {
|
|
1559
1675
|
const errorBody = await res.text();
|
|
@@ -1562,13 +1678,17 @@ var OpenAICompatibleProvider = class {
|
|
|
1562
1678
|
status: res.status,
|
|
1563
1679
|
headers: res.headers,
|
|
1564
1680
|
message: this.extractErrorMessage(errorBody, res.status),
|
|
1565
|
-
body: errorBody
|
|
1681
|
+
body: errorBody,
|
|
1682
|
+
timing: recorder.chatTiming()
|
|
1566
1683
|
});
|
|
1567
1684
|
}
|
|
1568
1685
|
if (!res.body) {
|
|
1569
1686
|
throw new Error(`${this.profile.label ?? this.profile.name} stream response has no body`);
|
|
1570
1687
|
}
|
|
1571
|
-
yield*
|
|
1688
|
+
yield* withCallTiming(
|
|
1689
|
+
recorder,
|
|
1690
|
+
this.parseSSEStream(res.body, this.requestModel(body, options.model), body, recorder)
|
|
1691
|
+
);
|
|
1572
1692
|
}
|
|
1573
1693
|
// ---------------------------------------------------------------------------
|
|
1574
1694
|
// Request building
|
|
@@ -1795,7 +1915,21 @@ var OpenAICompatibleProvider = class {
|
|
|
1795
1915
|
// ---------------------------------------------------------------------------
|
|
1796
1916
|
// SSE streaming
|
|
1797
1917
|
// ---------------------------------------------------------------------------
|
|
1798
|
-
|
|
1918
|
+
/**
|
|
1919
|
+
* PRIVATE, and deliberately not an extension point. It was `protected`, but
|
|
1920
|
+
* nothing in or out of this repo overrides it, and `protected` could not
|
|
1921
|
+
* carry the guarantee it appeared to: TypeScript checks an override for
|
|
1922
|
+
* assignability, and a function type with fewer parameters is assignable to
|
|
1923
|
+
* one with more — so an override that simply dropped the recorder still
|
|
1924
|
+
* compiled and then reported `wireMs === ttfbMs` for the whole subclass. Its
|
|
1925
|
+
* type is not exported from the barrel either, so a subclass author could not
|
|
1926
|
+
* name what they were required to accept.
|
|
1927
|
+
*
|
|
1928
|
+
* A provider with a different wire format implements `Provider` (see
|
|
1929
|
+
* docs/providers.md); an OpenAI-compatible one adds a `ProviderProfile`.
|
|
1930
|
+
* Neither needs to reach in here.
|
|
1931
|
+
*/
|
|
1932
|
+
async *parseSSEStream(body, model, request, timing) {
|
|
1799
1933
|
const reader = body.getReader();
|
|
1800
1934
|
const decoder = new TextDecoder();
|
|
1801
1935
|
let buffer = "";
|
|
@@ -1855,7 +1989,7 @@ var OpenAICompatibleProvider = class {
|
|
|
1855
1989
|
};
|
|
1856
1990
|
try {
|
|
1857
1991
|
for (; ; ) {
|
|
1858
|
-
const { done, value } = await
|
|
1992
|
+
const { done, value } = await timing.read(reader);
|
|
1859
1993
|
if (done) break;
|
|
1860
1994
|
buffer += decoder.decode(value, { stream: true });
|
|
1861
1995
|
const lines = buffer.split("\n");
|
|
@@ -1887,7 +2021,8 @@ var OpenAICompatibleProvider = class {
|
|
|
1887
2021
|
status: 0,
|
|
1888
2022
|
retryable: false,
|
|
1889
2023
|
message,
|
|
1890
|
-
body: JSON.stringify(parsed.error)
|
|
2024
|
+
body: JSON.stringify(parsed.error),
|
|
2025
|
+
timing: timing.streamTiming()
|
|
1891
2026
|
});
|
|
1892
2027
|
}
|
|
1893
2028
|
if (parsed.model !== void 0 || parsed.service_tier !== void 0 || parsed.serviceTier !== void 0) {
|
|
@@ -1959,7 +2094,8 @@ var OpenAICompatibleProvider = class {
|
|
|
1959
2094
|
provider: this.name,
|
|
1960
2095
|
status: 0,
|
|
1961
2096
|
retryable: true,
|
|
1962
|
-
message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]
|
|
2097
|
+
message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]`,
|
|
2098
|
+
timing: timing.streamTiming()
|
|
1963
2099
|
});
|
|
1964
2100
|
} finally {
|
|
1965
2101
|
reader.releaseLock();
|
|
@@ -1998,12 +2134,12 @@ var DIRECT_OPENAI_CATALOG = [
|
|
|
1998
2134
|
{
|
|
1999
2135
|
aliases: ["gpt-5.6", "gpt-5.6-sol"],
|
|
2000
2136
|
snapshotBase: "gpt-5.6-sol",
|
|
2001
|
-
short: { input:
|
|
2137
|
+
short: { input: 4 / M, cachedInput: 0.4 / M, cacheWrite: 5 / M, output: 20 / M },
|
|
2002
2138
|
long: withLongContext({
|
|
2003
|
-
input:
|
|
2004
|
-
cachedInput: 0.
|
|
2005
|
-
cacheWrite:
|
|
2006
|
-
output:
|
|
2139
|
+
input: 4 / M,
|
|
2140
|
+
cachedInput: 0.4 / M,
|
|
2141
|
+
cacheWrite: 5 / M,
|
|
2142
|
+
output: 20 / M
|
|
2007
2143
|
}),
|
|
2008
2144
|
contextBoundary: LONG_CONTEXT_BOUNDARY
|
|
2009
2145
|
},
|
|
@@ -2518,6 +2654,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2518
2654
|
async chat(messages, options) {
|
|
2519
2655
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
2520
2656
|
const body = this.buildRequestBody(messages, options, false);
|
|
2657
|
+
const recorder = new CallTimingRecorder();
|
|
2521
2658
|
const res = await fetchWithRetry(
|
|
2522
2659
|
`${this.baseUrl}/responses`,
|
|
2523
2660
|
{
|
|
@@ -2526,7 +2663,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2526
2663
|
body: JSON.stringify(body),
|
|
2527
2664
|
signal: options.signal
|
|
2528
2665
|
},
|
|
2529
|
-
{ governor: this.governor, provider: this.name }
|
|
2666
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
2530
2667
|
);
|
|
2531
2668
|
if (!res.ok) {
|
|
2532
2669
|
const errorBody = await res.text();
|
|
@@ -2536,11 +2673,15 @@ var OpenAIResponsesProvider = class {
|
|
|
2536
2673
|
status: res.status,
|
|
2537
2674
|
headers: res.headers,
|
|
2538
2675
|
message,
|
|
2539
|
-
body: errorBody
|
|
2676
|
+
body: errorBody,
|
|
2677
|
+
timing: recorder.chatTiming()
|
|
2540
2678
|
});
|
|
2541
2679
|
}
|
|
2542
2680
|
const json = await res.json();
|
|
2543
|
-
return
|
|
2681
|
+
return withChatTiming(
|
|
2682
|
+
recorder,
|
|
2683
|
+
this.parseResponse(json, this.requestModel(body, options.model), body)
|
|
2684
|
+
);
|
|
2544
2685
|
}
|
|
2545
2686
|
// ---------------------------------------------------------------------------
|
|
2546
2687
|
// stream - SSE streaming
|
|
@@ -2548,6 +2689,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2548
2689
|
async *stream(messages, options) {
|
|
2549
2690
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
2550
2691
|
const body = this.buildRequestBody(messages, options, true);
|
|
2692
|
+
const recorder = new CallTimingRecorder();
|
|
2551
2693
|
const res = await fetchWithRetry(
|
|
2552
2694
|
`${this.baseUrl}/responses`,
|
|
2553
2695
|
{
|
|
@@ -2556,7 +2698,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2556
2698
|
body: JSON.stringify(body),
|
|
2557
2699
|
signal: options.signal
|
|
2558
2700
|
},
|
|
2559
|
-
{ governor: this.governor, provider: this.name }
|
|
2701
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
2560
2702
|
);
|
|
2561
2703
|
if (!res.ok) {
|
|
2562
2704
|
const errorBody = await res.text();
|
|
@@ -2566,13 +2708,17 @@ var OpenAIResponsesProvider = class {
|
|
|
2566
2708
|
status: res.status,
|
|
2567
2709
|
headers: res.headers,
|
|
2568
2710
|
message,
|
|
2569
|
-
body: errorBody
|
|
2711
|
+
body: errorBody,
|
|
2712
|
+
timing: recorder.chatTiming()
|
|
2570
2713
|
});
|
|
2571
2714
|
}
|
|
2572
2715
|
if (!res.body) {
|
|
2573
2716
|
throw new Error("OpenAI Responses stream has no body");
|
|
2574
2717
|
}
|
|
2575
|
-
yield*
|
|
2718
|
+
yield* withCallTiming(
|
|
2719
|
+
recorder,
|
|
2720
|
+
this.parseSSEStream(res.body, this.requestModel(body, options.model), body, recorder)
|
|
2721
|
+
);
|
|
2576
2722
|
}
|
|
2577
2723
|
// ---------------------------------------------------------------------------
|
|
2578
2724
|
// Internal: build request body
|
|
@@ -2774,7 +2920,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2774
2920
|
// ---------------------------------------------------------------------------
|
|
2775
2921
|
// Internal: parse SSE stream
|
|
2776
2922
|
// ---------------------------------------------------------------------------
|
|
2777
|
-
async *parseSSEStream(body, model, request) {
|
|
2923
|
+
async *parseSSEStream(body, model, request, timing) {
|
|
2778
2924
|
const reader = body.getReader();
|
|
2779
2925
|
const decoder = new TextDecoder();
|
|
2780
2926
|
let buffer = "";
|
|
@@ -2782,7 +2928,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2782
2928
|
let eventType = "";
|
|
2783
2929
|
try {
|
|
2784
2930
|
while (true) {
|
|
2785
|
-
const { done, value } = await
|
|
2931
|
+
const { done, value } = await timing.read(reader);
|
|
2786
2932
|
if (done) break;
|
|
2787
2933
|
buffer += decoder.decode(value, { stream: true });
|
|
2788
2934
|
const lines = buffer.split("\n");
|
|
@@ -2802,7 +2948,14 @@ var OpenAIResponsesProvider = class {
|
|
|
2802
2948
|
} catch {
|
|
2803
2949
|
continue;
|
|
2804
2950
|
}
|
|
2805
|
-
const chunk = this.handleStreamEvent(
|
|
2951
|
+
const chunk = this.handleStreamEvent(
|
|
2952
|
+
eventType,
|
|
2953
|
+
data,
|
|
2954
|
+
model,
|
|
2955
|
+
callIdMap,
|
|
2956
|
+
request,
|
|
2957
|
+
timing
|
|
2958
|
+
);
|
|
2806
2959
|
if (chunk) {
|
|
2807
2960
|
yield chunk;
|
|
2808
2961
|
if (chunk.type === "done") return;
|
|
@@ -2816,7 +2969,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2816
2969
|
reader.releaseLock();
|
|
2817
2970
|
}
|
|
2818
2971
|
}
|
|
2819
|
-
handleStreamEvent(eventType, data, model, callIdMap, request) {
|
|
2972
|
+
handleStreamEvent(eventType, data, model, callIdMap, request, timing) {
|
|
2820
2973
|
switch (eventType) {
|
|
2821
2974
|
case "response.output_text.delta":
|
|
2822
2975
|
return { type: "text_delta", content: data.delta ?? "" };
|
|
@@ -2872,7 +3025,8 @@ var OpenAIResponsesProvider = class {
|
|
|
2872
3025
|
provider: this.name,
|
|
2873
3026
|
status: 0,
|
|
2874
3027
|
retryable: false,
|
|
2875
|
-
message: `OpenAI Responses API error: ${errorMsg}
|
|
3028
|
+
message: `OpenAI Responses API error: ${errorMsg}`,
|
|
3029
|
+
timing: timing.streamTiming()
|
|
2876
3030
|
});
|
|
2877
3031
|
}
|
|
2878
3032
|
default:
|
|
@@ -2940,6 +3094,12 @@ function hasAnthropicProviderFileImage(messages) {
|
|
|
2940
3094
|
);
|
|
2941
3095
|
}
|
|
2942
3096
|
var CLAUDE_CAPABILITIES = {
|
|
3097
|
+
"claude-fable-5-1": {
|
|
3098
|
+
thinking: "adaptive-always-on",
|
|
3099
|
+
effortLevels: ["low", "medium", "high", "xhigh", "max"],
|
|
3100
|
+
manualBudget: false,
|
|
3101
|
+
stripTemperature: true
|
|
3102
|
+
},
|
|
2943
3103
|
"claude-fable-5": {
|
|
2944
3104
|
thinking: "adaptive-always-on",
|
|
2945
3105
|
effortLevels: ["low", "medium", "high", "xhigh", "max"],
|
|
@@ -3030,10 +3190,18 @@ var PRE_INFERENCE_GEO_MODELS = /* @__PURE__ */ new Set([
|
|
|
3030
3190
|
function resolveClaudeCapability(model) {
|
|
3031
3191
|
return CLAUDE_CAPABILITIES[model] ?? (LEGACY_CLAUDE_MODELS.has(model) ? LEGACY_CLAUDE_CAPABILITY : void 0);
|
|
3032
3192
|
}
|
|
3193
|
+
var DEFAULT_CACHE_READ_MULTIPLIER = 0.1;
|
|
3033
3194
|
var ANTHROPIC_RATES = {
|
|
3195
|
+
"claude-fable-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
|
|
3034
3196
|
"claude-fable-5": { input: 1e-5, output: 5e-5 },
|
|
3197
|
+
// Limited availability (Glasswing). Priced from the public pricing table; no
|
|
3198
|
+
// CLAUDE_CAPABILITIES entry, because their thinking semantics are not
|
|
3199
|
+
// documented per-model and an unverified capability would send the wrong
|
|
3200
|
+
// request shape. Unknown IDs pass through unmodified, which is the safe side.
|
|
3201
|
+
"claude-mythos-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
|
|
3202
|
+
"claude-mythos-5": { input: 1e-5, output: 5e-5 },
|
|
3035
3203
|
"claude-opus-5": { input: 5e-6, output: 25e-6 },
|
|
3036
|
-
"claude-sonnet-5": { input:
|
|
3204
|
+
"claude-sonnet-5": { input: 2e-6, output: 1e-5 },
|
|
3037
3205
|
"claude-opus-4-8": { input: 5e-6, output: 25e-6 },
|
|
3038
3206
|
"claude-opus-4-7": { input: 5e-6, output: 25e-6 },
|
|
3039
3207
|
"claude-opus-4-6": { input: 5e-6, output: 25e-6 },
|
|
@@ -3072,7 +3240,7 @@ function anthropicStreamErrorStatus(type) {
|
|
|
3072
3240
|
return 0;
|
|
3073
3241
|
}
|
|
3074
3242
|
}
|
|
3075
|
-
function estimateAnthropicCost(model, usage
|
|
3243
|
+
function estimateAnthropicCost(model, usage) {
|
|
3076
3244
|
if (!model || !isValidTokenCount(usage.inputTokens) || !isValidTokenCount(usage.outputTokens)) {
|
|
3077
3245
|
return void 0;
|
|
3078
3246
|
}
|
|
@@ -3091,8 +3259,9 @@ function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
|
|
|
3091
3259
|
if (usage.aggregateCacheWriteTokens !== void 0 && (!isValidTokenCount(usage.aggregateCacheWriteTokens) || usage.aggregateCacheWriteTokens !== cacheWriteTotal)) {
|
|
3092
3260
|
return void 0;
|
|
3093
3261
|
}
|
|
3094
|
-
const { input, output } = rate
|
|
3095
|
-
|
|
3262
|
+
const { input, output } = rate;
|
|
3263
|
+
const cacheReadRate = input * (rate.cacheReadMultiplier ?? DEFAULT_CACHE_READ_MULTIPLIER);
|
|
3264
|
+
return usage.inputTokens * input + cacheRead * cacheReadRate + cacheWrite5m * input * 1.25 + cacheWrite1h * input * 2 + usage.outputTokens * output;
|
|
3096
3265
|
}
|
|
3097
3266
|
var THINKING_BUDGETS = {
|
|
3098
3267
|
low: 1024,
|
|
@@ -3333,6 +3502,14 @@ var AnthropicProvider = class {
|
|
|
3333
3502
|
const resolved = resolveThinkingOptions(options);
|
|
3334
3503
|
if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
|
|
3335
3504
|
const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
|
|
3505
|
+
if (ANTHROPIC_RATES[model] && !resolveClaudeCapability(model)) {
|
|
3506
|
+
return {
|
|
3507
|
+
requested: resolved.effort,
|
|
3508
|
+
effective: "unset",
|
|
3509
|
+
clamped: true,
|
|
3510
|
+
cause: `Axl has no capability entry for Anthropic ${model}, so no reasoning control was sent and effort '${resolved.effort}' had no effect`
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3336
3513
|
const config = resolveClaudeThinking(model, resolved);
|
|
3337
3514
|
const wireEffort = typeof config.outputConfig?.effort === "string" ? config.outputConfig.effort : void 0;
|
|
3338
3515
|
const effective = config.clamp?.effective ?? wireEffort;
|
|
@@ -3388,6 +3565,10 @@ var AnthropicProvider = class {
|
|
|
3388
3565
|
}
|
|
3389
3566
|
/** Anthropic ignores native `json_schema` structurally — Axl uses a system
|
|
3390
3567
|
* prompt JSON instruction + client-side Zod validation (see buildRequest). */
|
|
3568
|
+
/** The only built-in adapter that honors `promptCache` (see buildBody). */
|
|
3569
|
+
realizesPromptCache() {
|
|
3570
|
+
return true;
|
|
3571
|
+
}
|
|
3391
3572
|
nativeStructuredOutputSupport() {
|
|
3392
3573
|
return "unsupported";
|
|
3393
3574
|
}
|
|
@@ -3429,6 +3610,7 @@ var AnthropicProvider = class {
|
|
|
3429
3610
|
);
|
|
3430
3611
|
const body = this.buildRequestBody(messages, options, false);
|
|
3431
3612
|
const pricingContext = pricingContextFromBody(body);
|
|
3613
|
+
const recorder = new CallTimingRecorder();
|
|
3432
3614
|
const res = await fetchWithRetry(
|
|
3433
3615
|
`${this.baseUrl}/messages`,
|
|
3434
3616
|
{
|
|
@@ -3437,7 +3619,7 @@ var AnthropicProvider = class {
|
|
|
3437
3619
|
body: JSON.stringify(body),
|
|
3438
3620
|
signal: options.signal
|
|
3439
3621
|
},
|
|
3440
|
-
{ governor: this.governor, provider: this.name }
|
|
3622
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
3441
3623
|
);
|
|
3442
3624
|
if (!res.ok) {
|
|
3443
3625
|
const errorBody = await res.text();
|
|
@@ -3447,11 +3629,12 @@ var AnthropicProvider = class {
|
|
|
3447
3629
|
status: res.status,
|
|
3448
3630
|
headers: res.headers,
|
|
3449
3631
|
message,
|
|
3450
|
-
body: errorBody
|
|
3632
|
+
body: errorBody,
|
|
3633
|
+
timing: recorder.chatTiming()
|
|
3451
3634
|
});
|
|
3452
3635
|
}
|
|
3453
3636
|
const json = await res.json();
|
|
3454
|
-
return this.parseResponse(json, pricingContext);
|
|
3637
|
+
return withChatTiming(recorder, this.parseResponse(json, pricingContext));
|
|
3455
3638
|
}
|
|
3456
3639
|
// ---------------------------------------------------------------------------
|
|
3457
3640
|
// stream - SSE streaming completion
|
|
@@ -3463,6 +3646,7 @@ var AnthropicProvider = class {
|
|
|
3463
3646
|
);
|
|
3464
3647
|
const body = this.buildRequestBody(messages, options, true);
|
|
3465
3648
|
const pricingContext = pricingContextFromBody(body);
|
|
3649
|
+
const recorder = new CallTimingRecorder();
|
|
3466
3650
|
const res = await fetchWithRetry(
|
|
3467
3651
|
`${this.baseUrl}/messages`,
|
|
3468
3652
|
{
|
|
@@ -3471,7 +3655,7 @@ var AnthropicProvider = class {
|
|
|
3471
3655
|
body: JSON.stringify(body),
|
|
3472
3656
|
signal: options.signal
|
|
3473
3657
|
},
|
|
3474
|
-
{ governor: this.governor, provider: this.name }
|
|
3658
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
3475
3659
|
);
|
|
3476
3660
|
if (!res.ok) {
|
|
3477
3661
|
const errorBody = await res.text();
|
|
@@ -3481,13 +3665,17 @@ var AnthropicProvider = class {
|
|
|
3481
3665
|
status: res.status,
|
|
3482
3666
|
headers: res.headers,
|
|
3483
3667
|
message,
|
|
3484
|
-
body: errorBody
|
|
3668
|
+
body: errorBody,
|
|
3669
|
+
timing: recorder.chatTiming()
|
|
3485
3670
|
});
|
|
3486
3671
|
}
|
|
3487
3672
|
if (!res.body) {
|
|
3488
3673
|
throw new Error("Anthropic stream response has no body");
|
|
3489
3674
|
}
|
|
3490
|
-
yield*
|
|
3675
|
+
yield* withCallTiming(
|
|
3676
|
+
recorder,
|
|
3677
|
+
this.parseSSEStream(res.body, pricingContext, res.headers, recorder)
|
|
3678
|
+
);
|
|
3491
3679
|
}
|
|
3492
3680
|
// ---------------------------------------------------------------------------
|
|
3493
3681
|
// Internal: request building
|
|
@@ -3522,14 +3710,31 @@ var AnthropicProvider = class {
|
|
|
3522
3710
|
max_tokens: options.maxTokens ?? 4096,
|
|
3523
3711
|
stream
|
|
3524
3712
|
};
|
|
3525
|
-
|
|
3713
|
+
const po = options.providerOptions;
|
|
3714
|
+
const callerOwnsPrefix = po !== void 0 && (po.cache_control !== void 0 || po.system !== void 0 || po.tools !== void 0);
|
|
3715
|
+
const promptCache = options.promptCache === true && !callerOwnsPrefix;
|
|
3716
|
+
const anchorOnSystem = promptCache && systemMessages.length > 0 && systemMessages[0].origin !== "runtime";
|
|
3717
|
+
if (anchorOnSystem) {
|
|
3718
|
+
body.system = systemMessages.map((m, index) => ({
|
|
3719
|
+
type: "text",
|
|
3720
|
+
text: typeof m.content === "string" ? m.content : "",
|
|
3721
|
+
...index === 0 ? { cache_control: { type: "ephemeral" } } : {}
|
|
3722
|
+
}));
|
|
3723
|
+
} else if (systemText) {
|
|
3526
3724
|
body.system = systemText;
|
|
3527
3725
|
}
|
|
3528
3726
|
if (options.stop) {
|
|
3529
3727
|
body.stop_sequences = options.stop;
|
|
3530
3728
|
}
|
|
3531
3729
|
if (options.tools && options.tools.length > 0) {
|
|
3532
|
-
|
|
3730
|
+
const tools = options.tools.map((t) => this.mapToolDefinition(t));
|
|
3731
|
+
if (promptCache && !anchorOnSystem) {
|
|
3732
|
+
tools[tools.length - 1] = {
|
|
3733
|
+
...tools[tools.length - 1],
|
|
3734
|
+
cache_control: { type: "ephemeral" }
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3737
|
+
body.tools = tools;
|
|
3533
3738
|
}
|
|
3534
3739
|
if (options.toolChoice !== void 0) {
|
|
3535
3740
|
body.tool_choice = this.mapToolChoice(options.toolChoice);
|
|
@@ -3548,9 +3753,13 @@ var AnthropicProvider = class {
|
|
|
3548
3753
|
}
|
|
3549
3754
|
if (options.responseFormat && options.responseFormat.type !== "text") {
|
|
3550
3755
|
const jsonInstruction = "You must respond with valid JSON only. No markdown fences, no extra text.";
|
|
3551
|
-
|
|
3756
|
+
if (Array.isArray(body.system)) {
|
|
3757
|
+
body.system = [...body.system, { type: "text", text: jsonInstruction }];
|
|
3758
|
+
} else {
|
|
3759
|
+
body.system = body.system ? `${body.system}
|
|
3552
3760
|
|
|
3553
3761
|
${jsonInstruction}` : jsonInstruction;
|
|
3762
|
+
}
|
|
3554
3763
|
}
|
|
3555
3764
|
if (options.providerOptions) {
|
|
3556
3765
|
Object.assign(body, options.providerOptions);
|
|
@@ -3730,7 +3939,7 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3730
3939
|
// ---------------------------------------------------------------------------
|
|
3731
3940
|
// Internal: SSE stream parsing
|
|
3732
3941
|
// ---------------------------------------------------------------------------
|
|
3733
|
-
async *parseSSEStream(body, pricingContext, responseHeaders) {
|
|
3942
|
+
async *parseSSEStream(body, pricingContext, responseHeaders, timing) {
|
|
3734
3943
|
const reader = body.getReader();
|
|
3735
3944
|
const decoder = new TextDecoder();
|
|
3736
3945
|
let buffer = "";
|
|
@@ -3759,7 +3968,7 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3759
3968
|
});
|
|
3760
3969
|
try {
|
|
3761
3970
|
while (true) {
|
|
3762
|
-
const { done, value } = await
|
|
3971
|
+
const { done, value } = await timing.read(reader);
|
|
3763
3972
|
if (done) break;
|
|
3764
3973
|
buffer += decoder.decode(value, { stream: true });
|
|
3765
3974
|
const lines = buffer.split("\n");
|
|
@@ -3782,7 +3991,8 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3782
3991
|
provider: this.name,
|
|
3783
3992
|
status,
|
|
3784
3993
|
headers: responseHeaders,
|
|
3785
|
-
message: event.error?.message ?? "Anthropic stream failed"
|
|
3994
|
+
message: event.error?.message ?? "Anthropic stream failed",
|
|
3995
|
+
timing: timing.streamTiming()
|
|
3786
3996
|
});
|
|
3787
3997
|
}
|
|
3788
3998
|
case "content_block_start": {
|
|
@@ -4018,7 +4228,6 @@ var GEMINI_PRICING = {
|
|
|
4018
4228
|
},
|
|
4019
4229
|
"gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7 },
|
|
4020
4230
|
"gemini-2.5-flash-lite": { input: 1e-7, cached: 1e-8, output: 4e-7 },
|
|
4021
|
-
"gemini-3-flash-preview": { input: 5e-7, cached: 5e-8, output: 3e-6 },
|
|
4022
4231
|
"gemini-3.1-pro-preview": {
|
|
4023
4232
|
input: 2e-6,
|
|
4024
4233
|
cached: 2e-7,
|
|
@@ -4034,7 +4243,9 @@ var GEMINI_PRICING = {
|
|
|
4034
4243
|
"gemini-3.1-flash-lite": { input: 25e-8, cached: 25e-9, output: 15e-7 },
|
|
4035
4244
|
"gemini-3.5-flash": { input: 15e-7, cached: 15e-8, output: 9e-6 },
|
|
4036
4245
|
"gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
|
|
4037
|
-
"gemini-3.6-flash": { input:
|
|
4246
|
+
"gemini-3.6-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
|
|
4247
|
+
"gemini-3.7-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
|
|
4248
|
+
"gemini-3.8-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 }
|
|
4038
4249
|
};
|
|
4039
4250
|
function isValidTokenCount2(value) {
|
|
4040
4251
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
@@ -4379,6 +4590,7 @@ var GeminiProvider = class {
|
|
|
4379
4590
|
const pricingContext = this.pricingContext(this.requestModel(options), body);
|
|
4380
4591
|
this.assertSafeGemini3Continuation(messages, pricingContext.model);
|
|
4381
4592
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4593
|
+
const recorder = new CallTimingRecorder();
|
|
4382
4594
|
const res = await fetchWithRetry(
|
|
4383
4595
|
`${this.baseUrl}/models/${pricingContext.model}:generateContent`,
|
|
4384
4596
|
{
|
|
@@ -4387,7 +4599,7 @@ var GeminiProvider = class {
|
|
|
4387
4599
|
body: JSON.stringify(body),
|
|
4388
4600
|
signal: options.signal
|
|
4389
4601
|
},
|
|
4390
|
-
{ governor: this.governor, provider: this.name }
|
|
4602
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4391
4603
|
);
|
|
4392
4604
|
if (!res.ok) {
|
|
4393
4605
|
const errorBody = await res.text();
|
|
@@ -4397,11 +4609,12 @@ var GeminiProvider = class {
|
|
|
4397
4609
|
status: res.status,
|
|
4398
4610
|
headers: res.headers,
|
|
4399
4611
|
message,
|
|
4400
|
-
body: errorBody
|
|
4612
|
+
body: errorBody,
|
|
4613
|
+
timing: recorder.chatTiming()
|
|
4401
4614
|
});
|
|
4402
4615
|
}
|
|
4403
4616
|
const json = await res.json();
|
|
4404
|
-
return this.parseResponse(json, pricingContext);
|
|
4617
|
+
return withChatTiming(recorder, this.parseResponse(json, pricingContext));
|
|
4405
4618
|
}
|
|
4406
4619
|
// ---------------------------------------------------------------------------
|
|
4407
4620
|
// stream - SSE streaming completion
|
|
@@ -4415,6 +4628,7 @@ var GeminiProvider = class {
|
|
|
4415
4628
|
const pricingContext = this.pricingContext(this.requestModel(options), body);
|
|
4416
4629
|
this.assertSafeGemini3Continuation(messages, pricingContext.model);
|
|
4417
4630
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4631
|
+
const recorder = new CallTimingRecorder();
|
|
4418
4632
|
const res = await fetchWithRetry(
|
|
4419
4633
|
`${this.baseUrl}/models/${pricingContext.model}:streamGenerateContent?alt=sse`,
|
|
4420
4634
|
{
|
|
@@ -4423,7 +4637,7 @@ var GeminiProvider = class {
|
|
|
4423
4637
|
body: JSON.stringify(body),
|
|
4424
4638
|
signal: options.signal
|
|
4425
4639
|
},
|
|
4426
|
-
{ governor: this.governor, provider: this.name }
|
|
4640
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4427
4641
|
);
|
|
4428
4642
|
if (!res.ok) {
|
|
4429
4643
|
const errorBody = await res.text();
|
|
@@ -4433,13 +4647,14 @@ var GeminiProvider = class {
|
|
|
4433
4647
|
status: res.status,
|
|
4434
4648
|
headers: res.headers,
|
|
4435
4649
|
message,
|
|
4436
|
-
body: errorBody
|
|
4650
|
+
body: errorBody,
|
|
4651
|
+
timing: recorder.chatTiming()
|
|
4437
4652
|
});
|
|
4438
4653
|
}
|
|
4439
4654
|
if (!res.body) {
|
|
4440
4655
|
throw new Error("Gemini stream response has no body");
|
|
4441
4656
|
}
|
|
4442
|
-
yield* this.parseSSEStream(res.body, pricingContext);
|
|
4657
|
+
yield* withCallTiming(recorder, this.parseSSEStream(res.body, pricingContext, recorder));
|
|
4443
4658
|
}
|
|
4444
4659
|
// ---------------------------------------------------------------------------
|
|
4445
4660
|
// Rich image requests use Gemini Interactions. It is deliberately stateless:
|
|
@@ -4450,10 +4665,11 @@ var GeminiProvider = class {
|
|
|
4450
4665
|
this.assertSafeInteractionOptions(options);
|
|
4451
4666
|
const body = this.buildInteractionRequestBody(messages, options, false);
|
|
4452
4667
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4668
|
+
const recorder = new CallTimingRecorder();
|
|
4453
4669
|
const res = await fetchWithRetry(
|
|
4454
4670
|
`${this.baseUrl}/interactions`,
|
|
4455
4671
|
{ method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
|
|
4456
|
-
{ governor: this.governor, provider: this.name }
|
|
4672
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4457
4673
|
);
|
|
4458
4674
|
if (!res.ok) {
|
|
4459
4675
|
const errorBody = await res.text();
|
|
@@ -4462,19 +4678,24 @@ var GeminiProvider = class {
|
|
|
4462
4678
|
status: res.status,
|
|
4463
4679
|
headers: res.headers,
|
|
4464
4680
|
message: this.extractErrorMessage(errorBody, res.status),
|
|
4465
|
-
body: errorBody
|
|
4681
|
+
body: errorBody,
|
|
4682
|
+
timing: recorder.chatTiming()
|
|
4466
4683
|
});
|
|
4467
4684
|
}
|
|
4468
|
-
return
|
|
4685
|
+
return withChatTiming(
|
|
4686
|
+
recorder,
|
|
4687
|
+
this.parseInteractionResponse(await res.json())
|
|
4688
|
+
);
|
|
4469
4689
|
}
|
|
4470
4690
|
async *streamInteraction(messages, options) {
|
|
4471
4691
|
this.assertSafeInteractionOptions(options);
|
|
4472
4692
|
const body = this.buildInteractionRequestBody(messages, options, true);
|
|
4473
4693
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4694
|
+
const recorder = new CallTimingRecorder();
|
|
4474
4695
|
const res = await fetchWithRetry(
|
|
4475
4696
|
`${this.baseUrl}/interactions?alt=sse`,
|
|
4476
4697
|
{ method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
|
|
4477
|
-
{ governor: this.governor, provider: this.name }
|
|
4698
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4478
4699
|
);
|
|
4479
4700
|
if (!res.ok) {
|
|
4480
4701
|
const errorBody = await res.text();
|
|
@@ -4483,11 +4704,15 @@ var GeminiProvider = class {
|
|
|
4483
4704
|
status: res.status,
|
|
4484
4705
|
headers: res.headers,
|
|
4485
4706
|
message: this.extractErrorMessage(errorBody, res.status),
|
|
4486
|
-
body: errorBody
|
|
4707
|
+
body: errorBody,
|
|
4708
|
+
timing: recorder.chatTiming()
|
|
4487
4709
|
});
|
|
4488
4710
|
}
|
|
4489
4711
|
if (!res.body) throw new Error("Gemini Interactions stream has no body");
|
|
4490
|
-
yield*
|
|
4712
|
+
yield* withCallTiming(
|
|
4713
|
+
recorder,
|
|
4714
|
+
this.parseInteractionSSEStream(res.body, res.headers, recorder)
|
|
4715
|
+
);
|
|
4491
4716
|
}
|
|
4492
4717
|
buildInteractionRequestBody(messages, options, stream) {
|
|
4493
4718
|
const model = this.requestModel(options);
|
|
@@ -4643,7 +4868,7 @@ var GeminiProvider = class {
|
|
|
4643
4868
|
providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
|
|
4644
4869
|
};
|
|
4645
4870
|
}
|
|
4646
|
-
async *parseInteractionSSEStream(body, responseHeaders) {
|
|
4871
|
+
async *parseInteractionSSEStream(body, responseHeaders, timing) {
|
|
4647
4872
|
const reader = body.getReader();
|
|
4648
4873
|
const decoder = new TextDecoder();
|
|
4649
4874
|
let buffer = "";
|
|
@@ -4653,7 +4878,7 @@ var GeminiProvider = class {
|
|
|
4653
4878
|
let completed = false;
|
|
4654
4879
|
try {
|
|
4655
4880
|
while (true) {
|
|
4656
|
-
const { done, value } = await
|
|
4881
|
+
const { done, value } = await timing.read(reader);
|
|
4657
4882
|
if (done) break;
|
|
4658
4883
|
buffer += decoder.decode(value, { stream: true });
|
|
4659
4884
|
const lines = buffer.split("\n");
|
|
@@ -4676,14 +4901,16 @@ var GeminiProvider = class {
|
|
|
4676
4901
|
provider: this.name,
|
|
4677
4902
|
status,
|
|
4678
4903
|
headers: responseHeaders,
|
|
4679
|
-
message
|
|
4904
|
+
message,
|
|
4905
|
+
timing: timing.streamTiming()
|
|
4680
4906
|
});
|
|
4681
4907
|
}
|
|
4682
4908
|
throw new ProviderError({
|
|
4683
4909
|
provider: this.name,
|
|
4684
4910
|
status: 0,
|
|
4685
4911
|
retryable: false,
|
|
4686
|
-
message
|
|
4912
|
+
message,
|
|
4913
|
+
timing: timing.streamTiming()
|
|
4687
4914
|
});
|
|
4688
4915
|
}
|
|
4689
4916
|
if (event.event_type === "step.start" && event.step) {
|
|
@@ -5161,7 +5388,7 @@ var GeminiProvider = class {
|
|
|
5161
5388
|
// ---------------------------------------------------------------------------
|
|
5162
5389
|
// Internal: SSE stream parsing
|
|
5163
5390
|
// ---------------------------------------------------------------------------
|
|
5164
|
-
async *parseSSEStream(body, pricingContext) {
|
|
5391
|
+
async *parseSSEStream(body, pricingContext, timing) {
|
|
5165
5392
|
const reader = body.getReader();
|
|
5166
5393
|
const decoder = new TextDecoder();
|
|
5167
5394
|
let buffer = "";
|
|
@@ -5174,7 +5401,7 @@ var GeminiProvider = class {
|
|
|
5174
5401
|
const accumulatedParts = [];
|
|
5175
5402
|
try {
|
|
5176
5403
|
while (true) {
|
|
5177
|
-
const { done, value } = await
|
|
5404
|
+
const { done, value } = await timing.read(reader);
|
|
5178
5405
|
if (done) break;
|
|
5179
5406
|
buffer += decoder.decode(value, { stream: true });
|
|
5180
5407
|
const lines = buffer.split("\n");
|
|
@@ -9684,6 +9911,13 @@ function describeValue(value) {
|
|
|
9684
9911
|
}
|
|
9685
9912
|
return `a value of type ${typeof value}`;
|
|
9686
9913
|
}
|
|
9914
|
+
var PROMPT_CACHE_COLD_WRITE_WARN_AFTER = 2;
|
|
9915
|
+
function nextUserTurn(history, from) {
|
|
9916
|
+
for (let i = Math.max(0, from); i < history.length; i++) {
|
|
9917
|
+
if (history[i].role === "user") return i;
|
|
9918
|
+
}
|
|
9919
|
+
return void 0;
|
|
9920
|
+
}
|
|
9687
9921
|
function estimateMessagesTokens(messages) {
|
|
9688
9922
|
let total = 0;
|
|
9689
9923
|
let unmeasured = false;
|
|
@@ -9823,6 +10057,13 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
9823
10057
|
workflowLifecycleState;
|
|
9824
10058
|
signal;
|
|
9825
10059
|
summaryCache;
|
|
10060
|
+
/** Consecutive provider calls with promptCache on that wrote to the cache
|
|
10061
|
+
* without reading from it (prefix changes per call), and consecutive calls
|
|
10062
|
+
* with no cache activity at all (prefix below the model's minimum, which
|
|
10063
|
+
* Anthropic silently ignores). Either reports once per context. */
|
|
10064
|
+
promptCacheColdWrites = 0;
|
|
10065
|
+
promptCacheInactive = 0;
|
|
10066
|
+
promptCacheWarned = false;
|
|
9826
10067
|
workflowName;
|
|
9827
10068
|
mcpManager;
|
|
9828
10069
|
spanManager;
|
|
@@ -10355,6 +10596,17 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10355
10596
|
const costAfter2 = this.budgetContext?.totalCost ?? 0;
|
|
10356
10597
|
span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
|
|
10357
10598
|
span.setAttribute("axl.agent.duration", Date.now() - askStart);
|
|
10599
|
+
if (usageCapture.timing) {
|
|
10600
|
+
const t = usageCapture.timing;
|
|
10601
|
+
span.setAttribute("axl.agent.queued_ms", t.queuedMs);
|
|
10602
|
+
span.setAttribute("axl.agent.retry_ms", t.retryMs);
|
|
10603
|
+
span.setAttribute("axl.agent.attempts", t.attempts);
|
|
10604
|
+
span.setAttribute("axl.agent.ttfb_ms", t.ttfbMs);
|
|
10605
|
+
span.setAttribute("axl.agent.wire_ms", t.wireMs);
|
|
10606
|
+
if (t.firstTokenMs !== void 0) {
|
|
10607
|
+
span.setAttribute("axl.agent.first_token_ms", t.firstTokenMs);
|
|
10608
|
+
}
|
|
10609
|
+
}
|
|
10358
10610
|
if (usageCapture.value) {
|
|
10359
10611
|
span.setAttribute(
|
|
10360
10612
|
"axl.agent.prompt_tokens",
|
|
@@ -10562,6 +10814,14 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10562
10814
|
const toolTokens = toolDefs.length > 0 ? estimateTokens(JSON.stringify(toolDefs)) : 0;
|
|
10563
10815
|
const overhead = systemTokens + toolTokens + reserveTokens;
|
|
10564
10816
|
const availableForHistory = maxContext - overhead;
|
|
10817
|
+
if (availableForHistory <= 0) {
|
|
10818
|
+
this.emitEvent({
|
|
10819
|
+
type: "log",
|
|
10820
|
+
data: {
|
|
10821
|
+
warning: `Agent '${agent2._name}' maxContext (${maxContext}) is at or below its fixed overhead (${overhead} = system ${systemTokens} + tools ${toolTokens} + reserve ${reserveTokens}); no session history can fit, so every ask summarizes, retaining at most the most recent exchange and sometimes none of it verbatim. The current input is always sent. Raise maxContext or lower contextManagement.reserveTokens.`
|
|
10822
|
+
}
|
|
10823
|
+
});
|
|
10824
|
+
}
|
|
10565
10825
|
const historyEstimate = estimateMessagesTokens(sessionHistory);
|
|
10566
10826
|
if (historyEstimate.unmeasured) {
|
|
10567
10827
|
this.emitEvent({
|
|
@@ -10627,6 +10887,7 @@ ${appendedSchemaText}`;
|
|
|
10627
10887
|
if (handoffContext.length > 0) {
|
|
10628
10888
|
messages.push({
|
|
10629
10889
|
role: "system",
|
|
10890
|
+
origin: "runtime",
|
|
10630
10891
|
content: "The following is the conversation history from the previous agent that handed off to you:"
|
|
10631
10892
|
});
|
|
10632
10893
|
for (const msg of handoffContext) {
|
|
@@ -10676,6 +10937,7 @@ ${appendedSchemaText}`;
|
|
|
10676
10937
|
const maxTurns = agent2._config.maxTurns ?? 25;
|
|
10677
10938
|
const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
|
|
10678
10939
|
const startTime = Date.now();
|
|
10940
|
+
const timingTotals = { turns: 0, queuedMs: 0, retryMs: 0, wireMs: 0 };
|
|
10679
10941
|
const currentMessages = [...messages];
|
|
10680
10942
|
let turns = 0;
|
|
10681
10943
|
let guardrailOutputRetries = 0;
|
|
@@ -10689,8 +10951,22 @@ ${appendedSchemaText}`;
|
|
|
10689
10951
|
let lastStartMaxAttempts = 1;
|
|
10690
10952
|
while (turns < maxTurns) {
|
|
10691
10953
|
this.currentSignal?.throwIfAborted();
|
|
10692
|
-
|
|
10693
|
-
|
|
10954
|
+
const elapsedMs = Date.now() - startTime;
|
|
10955
|
+
if (elapsedMs > timeoutMs) {
|
|
10956
|
+
throw new TimeoutError(
|
|
10957
|
+
"ctx.ask()",
|
|
10958
|
+
timeoutMs,
|
|
10959
|
+
timingTotals.turns > 0 ? {
|
|
10960
|
+
elapsedMs,
|
|
10961
|
+
queuedMs: timingTotals.queuedMs,
|
|
10962
|
+
retryMs: timingTotals.retryMs,
|
|
10963
|
+
wireMs: timingTotals.wireMs,
|
|
10964
|
+
otherMs: Math.max(
|
|
10965
|
+
0,
|
|
10966
|
+
elapsedMs - timingTotals.queuedMs - timingTotals.retryMs - timingTotals.wireMs
|
|
10967
|
+
)
|
|
10968
|
+
} : void 0
|
|
10969
|
+
);
|
|
10694
10970
|
}
|
|
10695
10971
|
turns++;
|
|
10696
10972
|
const isFirstTurn = turns === 1;
|
|
@@ -10730,6 +11006,7 @@ ${appendedSchemaText}`;
|
|
|
10730
11006
|
effort: options?.effort ?? agent2._config.effort,
|
|
10731
11007
|
thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
|
|
10732
11008
|
includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
|
|
11009
|
+
promptCache: options?.promptCache ?? agent2._config.promptCache,
|
|
10733
11010
|
toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
|
|
10734
11011
|
stop: options?.stop ?? agent2._config.stop,
|
|
10735
11012
|
providerOptions,
|
|
@@ -10807,6 +11084,7 @@ ${appendedSchemaText}`;
|
|
|
10807
11084
|
const toolCalls = [];
|
|
10808
11085
|
const toolCallBuffers = /* @__PURE__ */ new Map();
|
|
10809
11086
|
let streamProviderMetadata;
|
|
11087
|
+
let streamTiming;
|
|
10810
11088
|
let thinkingContent = "";
|
|
10811
11089
|
const streamingObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof import_zod2.z.ZodObject;
|
|
10812
11090
|
const currentAttempt = schemaRetries + 1;
|
|
@@ -10858,6 +11136,7 @@ ${appendedSchemaText}`;
|
|
|
10858
11136
|
if (chunk.arguments) buffer.arguments += chunk.arguments;
|
|
10859
11137
|
} else if (chunk.type === "done") {
|
|
10860
11138
|
streamProviderMetadata = chunk.providerMetadata;
|
|
11139
|
+
streamTiming = chunk.timing;
|
|
10861
11140
|
if (chunk.usage) {
|
|
10862
11141
|
response = {
|
|
10863
11142
|
content: content2,
|
|
@@ -10885,6 +11164,9 @@ ${appendedSchemaText}`;
|
|
|
10885
11164
|
if (streamProviderMetadata) {
|
|
10886
11165
|
response.providerMetadata = streamProviderMetadata;
|
|
10887
11166
|
}
|
|
11167
|
+
if (streamTiming) {
|
|
11168
|
+
response.timing = streamTiming;
|
|
11169
|
+
}
|
|
10888
11170
|
if (thinkingContent) {
|
|
10889
11171
|
response.thinking_content = thinkingContent;
|
|
10890
11172
|
}
|
|
@@ -10893,12 +11175,15 @@ ${appendedSchemaText}`;
|
|
|
10893
11175
|
}
|
|
10894
11176
|
} catch (err) {
|
|
10895
11177
|
if (richRequest) this.recordRichFailure(err);
|
|
11178
|
+
const failureTiming = err instanceof ProviderError ? err.timing : void 0;
|
|
10896
11179
|
this.emitEvent({
|
|
10897
11180
|
type: "agent_call_end",
|
|
10898
11181
|
agent: agent2._name,
|
|
10899
11182
|
model: effectiveModelUri,
|
|
10900
11183
|
promptVersion: agent2._config.version,
|
|
10901
11184
|
duration: Date.now() - turnStart,
|
|
11185
|
+
// Top-level beside `duration`, exactly as on the success path.
|
|
11186
|
+
...failureTiming ? { timing: failureTiming } : {},
|
|
10902
11187
|
data: {
|
|
10903
11188
|
response: "",
|
|
10904
11189
|
turn: turns,
|
|
@@ -10915,9 +11200,41 @@ ${appendedSchemaText}`;
|
|
|
10915
11200
|
if (usageCapture && response.usage) {
|
|
10916
11201
|
usageCapture.value = response.usage;
|
|
10917
11202
|
}
|
|
11203
|
+
if (usageCapture && response.timing) {
|
|
11204
|
+
usageCapture.timing = response.timing;
|
|
11205
|
+
}
|
|
11206
|
+
if (chatOptions.promptCache && provider.realizesPromptCache?.(model) === true && response.usage && !this.promptCacheWarned) {
|
|
11207
|
+
const wrote = (response.usage.cache_write_tokens ?? 0) > 0;
|
|
11208
|
+
const read = (response.usage.cached_tokens ?? 0) > 0;
|
|
11209
|
+
if (read) {
|
|
11210
|
+
this.promptCacheColdWrites = 0;
|
|
11211
|
+
this.promptCacheInactive = 0;
|
|
11212
|
+
} else if (wrote) {
|
|
11213
|
+
this.promptCacheColdWrites += 1;
|
|
11214
|
+
this.promptCacheInactive = 0;
|
|
11215
|
+
} else {
|
|
11216
|
+
this.promptCacheInactive += 1;
|
|
11217
|
+
}
|
|
11218
|
+
let warning;
|
|
11219
|
+
if (this.promptCacheColdWrites >= PROMPT_CACHE_COLD_WRITE_WARN_AFTER) {
|
|
11220
|
+
warning = `Agent '${agent2._name}' has promptCache on, but its prefix was written to the cache ${this.promptCacheColdWrites} times in a row with no cache reads. Each write bills 1.25x input. If this agent's system prompt or tool set changes per call, set promptCache: false for it.`;
|
|
11221
|
+
} else if (this.promptCacheInactive >= PROMPT_CACHE_COLD_WRITE_WARN_AFTER) {
|
|
11222
|
+
warning = `Agent '${agent2._name}' has promptCache on, but ${this.promptCacheInactive} calls in a row showed no cache writes or reads. Anthropic silently ignores a breakpoint below the model's minimum cacheable length (4,096 tokens on Haiku 4.5; 1,024 or 512 on others) -- the system prompt plus tools may be too short to cache on this model, or the provider may not support prompt caching.`;
|
|
11223
|
+
}
|
|
11224
|
+
if (warning) {
|
|
11225
|
+
this.promptCacheWarned = true;
|
|
11226
|
+
this.emitEvent({ type: "log", agent: agent2._name, data: { warning } });
|
|
11227
|
+
}
|
|
11228
|
+
}
|
|
10918
11229
|
if (response.cost) {
|
|
10919
11230
|
this._accumulateBudgetCost(response.cost);
|
|
10920
11231
|
}
|
|
11232
|
+
if (response.timing) {
|
|
11233
|
+
timingTotals.turns += 1;
|
|
11234
|
+
timingTotals.queuedMs += response.timing.queuedMs;
|
|
11235
|
+
timingTotals.retryMs += response.timing.retryMs;
|
|
11236
|
+
timingTotals.wireMs += response.timing.wireMs;
|
|
11237
|
+
}
|
|
10921
11238
|
this.emitEvent({
|
|
10922
11239
|
type: "agent_call_end",
|
|
10923
11240
|
agent: agent2._name,
|
|
@@ -10932,6 +11249,8 @@ ${appendedSchemaText}`;
|
|
|
10932
11249
|
cacheWrite: response.usage.cache_write_tokens
|
|
10933
11250
|
} : void 0,
|
|
10934
11251
|
duration: Date.now() - turnStart,
|
|
11252
|
+
// Additive beside `duration`, which keeps its turn-wall-clock meaning.
|
|
11253
|
+
...response.timing ? { timing: response.timing } : {},
|
|
10935
11254
|
data: {
|
|
10936
11255
|
response: response.content,
|
|
10937
11256
|
...response.thinking_content ? { thinking: response.thinking_content } : {},
|
|
@@ -11650,6 +11969,7 @@ ${appendedSchemaText}`;
|
|
|
11650
11969
|
if (this.summaryCache) {
|
|
11651
11970
|
const summaryMsg2 = {
|
|
11652
11971
|
role: "system",
|
|
11972
|
+
origin: "runtime",
|
|
11653
11973
|
content: `Summary of earlier conversation:
|
|
11654
11974
|
${this.summaryCache}`
|
|
11655
11975
|
};
|
|
@@ -11663,6 +11983,7 @@ ${this.summaryCache}`
|
|
|
11663
11983
|
recentTokens2 += msgTokens;
|
|
11664
11984
|
splitIdx2 = i;
|
|
11665
11985
|
}
|
|
11986
|
+
splitIdx2 = nextUserTurn(history, splitIdx2) ?? history.length;
|
|
11666
11987
|
if (splitIdx2 < history.length) {
|
|
11667
11988
|
return [summaryMsg2, ...history.slice(splitIdx2)];
|
|
11668
11989
|
}
|
|
@@ -11676,6 +11997,19 @@ ${this.summaryCache}`
|
|
|
11676
11997
|
recentTokens += msgTokens;
|
|
11677
11998
|
splitIdx = i;
|
|
11678
11999
|
}
|
|
12000
|
+
const anchored = nextUserTurn(history, splitIdx);
|
|
12001
|
+
if (anchored !== void 0) {
|
|
12002
|
+
splitIdx = anchored;
|
|
12003
|
+
} else if (splitIdx === history.length && history.length > 1) {
|
|
12004
|
+
for (let i = history.length - 1; i > 0; i--) {
|
|
12005
|
+
if (history[i].role === "user") {
|
|
12006
|
+
splitIdx = i;
|
|
12007
|
+
break;
|
|
12008
|
+
}
|
|
12009
|
+
}
|
|
12010
|
+
} else {
|
|
12011
|
+
splitIdx = history.length;
|
|
12012
|
+
}
|
|
11679
12013
|
if (splitIdx === 0) return history;
|
|
11680
12014
|
const oldMessages = history.slice(0, splitIdx);
|
|
11681
12015
|
const summaryModelUri = this.config.contextManagement?.summaryModel;
|
|
@@ -11707,6 +12041,7 @@ ${this.summaryCache}`
|
|
|
11707
12041
|
}
|
|
11708
12042
|
const summaryMsg = {
|
|
11709
12043
|
role: "system",
|
|
12044
|
+
origin: "runtime",
|
|
11710
12045
|
content: `Summary of earlier conversation:
|
|
11711
12046
|
${summaryResponse.content}`
|
|
11712
12047
|
};
|
|
@@ -16482,6 +16817,8 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
16482
16817
|
parent: parentScope
|
|
16483
16818
|
};
|
|
16484
16819
|
const modelCalls = /* @__PURE__ */ new Map();
|
|
16820
|
+
const modelTiming = /* @__PURE__ */ new Map();
|
|
16821
|
+
const captureTimingSamples = options?.captureTimingSamples === true;
|
|
16485
16822
|
const workflowCalls = /* @__PURE__ */ new Map();
|
|
16486
16823
|
const tokens = { input: 0, output: 0, reasoning: 0 };
|
|
16487
16824
|
let agentCalls = 0;
|
|
@@ -16499,6 +16836,30 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
16499
16836
|
tokens.output += event.tokens.output ?? 0;
|
|
16500
16837
|
tokens.reasoning += event.tokens.reasoning ?? 0;
|
|
16501
16838
|
}
|
|
16839
|
+
const failed2 = event.data?.error != null;
|
|
16840
|
+
if (event.model && event.timing && !failed2) {
|
|
16841
|
+
const t = event.timing;
|
|
16842
|
+
let bucket = modelTiming.get(event.model);
|
|
16843
|
+
if (!bucket) {
|
|
16844
|
+
bucket = {
|
|
16845
|
+
calls: 0,
|
|
16846
|
+
queuedMs: 0,
|
|
16847
|
+
retryMs: 0,
|
|
16848
|
+
wireMs: 0,
|
|
16849
|
+
...captureTimingSamples ? { samples: [] } : {}
|
|
16850
|
+
};
|
|
16851
|
+
modelTiming.set(event.model, bucket);
|
|
16852
|
+
}
|
|
16853
|
+
bucket.calls++;
|
|
16854
|
+
bucket.samples?.push(t);
|
|
16855
|
+
bucket.queuedMs += t.queuedMs;
|
|
16856
|
+
bucket.retryMs += t.retryMs;
|
|
16857
|
+
bucket.wireMs += t.wireMs;
|
|
16858
|
+
if (t.firstTokenMs != null) {
|
|
16859
|
+
bucket.firstTokenMs = (bucket.firstTokenMs ?? 0) + t.firstTokenMs;
|
|
16860
|
+
bucket.firstTokenCalls = (bucket.firstTokenCalls ?? 0) + 1;
|
|
16861
|
+
}
|
|
16862
|
+
}
|
|
16502
16863
|
}
|
|
16503
16864
|
if (event.type === "workflow_start" && event.workflow) {
|
|
16504
16865
|
workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
|
|
@@ -16529,6 +16890,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
16529
16890
|
result,
|
|
16530
16891
|
cost: scope.totalCost,
|
|
16531
16892
|
unpriced,
|
|
16893
|
+
...modelTiming.size > 0 ? { modelTiming: Object.fromEntries(modelTiming) } : {},
|
|
16532
16894
|
...capturedTraces ? { traces: capturedTraces } : {},
|
|
16533
16895
|
metadata: {
|
|
16534
16896
|
models: [...modelCalls.keys()],
|