@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.js
CHANGED
|
@@ -170,9 +170,15 @@ var NoConsensus = class extends AxlError {
|
|
|
170
170
|
}
|
|
171
171
|
};
|
|
172
172
|
var TimeoutError = class extends AxlError {
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
/** Latency attribution for the timed-out operation, when measurable. */
|
|
174
|
+
breakdown;
|
|
175
|
+
constructor(operation, timeoutMs, breakdown) {
|
|
176
|
+
super(
|
|
177
|
+
"TIMEOUT",
|
|
178
|
+
`${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)` : "")
|
|
179
|
+
);
|
|
175
180
|
this.name = "TimeoutError";
|
|
181
|
+
if (breakdown) this.breakdown = breakdown;
|
|
176
182
|
}
|
|
177
183
|
};
|
|
178
184
|
function formatBudgetCost(cost) {
|
|
@@ -719,6 +725,7 @@ var ProviderError = class extends AxlError {
|
|
|
719
725
|
this.retryAfterMs = args.retryAfterMs;
|
|
720
726
|
this.requestId = args.requestId;
|
|
721
727
|
this.body = args.body;
|
|
728
|
+
if (args.timing) this.timing = args.timing;
|
|
722
729
|
}
|
|
723
730
|
};
|
|
724
731
|
function isRetryableStatus(status) {
|
|
@@ -770,7 +777,8 @@ function buildProviderError(args) {
|
|
|
770
777
|
message: args.message,
|
|
771
778
|
retryAfterMs: args.headers ? parseRetryAfter(args.headers) : void 0,
|
|
772
779
|
requestId: args.headers ? extractRequestId(args.headers) : void 0,
|
|
773
|
-
body: args.body
|
|
780
|
+
body: args.body,
|
|
781
|
+
timing: args.timing
|
|
774
782
|
});
|
|
775
783
|
}
|
|
776
784
|
|
|
@@ -807,14 +815,33 @@ async function fetchWithRetry(input, init, opts) {
|
|
|
807
815
|
const maxRetries = opts?.maxRetries ?? MAX_RETRIES;
|
|
808
816
|
const governor = opts?.governor;
|
|
809
817
|
const provider = opts?.provider ?? "unknown";
|
|
818
|
+
const observer = opts?.timing;
|
|
810
819
|
let acquired = false;
|
|
820
|
+
let queuedMs = 0;
|
|
811
821
|
if (governor) {
|
|
822
|
+
const acquireStart = Date.now();
|
|
812
823
|
await governor.acquire(init?.signal ?? void 0);
|
|
813
824
|
acquired = true;
|
|
814
|
-
|
|
825
|
+
queuedMs = Date.now() - acquireStart;
|
|
826
|
+
}
|
|
827
|
+
let firstDispatchedAt = 0;
|
|
828
|
+
let dispatchedAt = 0;
|
|
829
|
+
let headersAt = 0;
|
|
830
|
+
const reportComplete = (attempts) => {
|
|
831
|
+
observer?.onComplete?.({
|
|
832
|
+
queuedMs,
|
|
833
|
+
attempts,
|
|
834
|
+
retryMs: dispatchedAt - firstDispatchedAt,
|
|
835
|
+
dispatchedAt,
|
|
836
|
+
headersAt
|
|
837
|
+
});
|
|
838
|
+
};
|
|
815
839
|
try {
|
|
816
840
|
for (let attempt = 0; ; attempt++) {
|
|
817
841
|
let res;
|
|
842
|
+
dispatchedAt = Date.now();
|
|
843
|
+
if (attempt === 0) firstDispatchedAt = dispatchedAt;
|
|
844
|
+
observer?.onDispatch?.(attempt + 1, dispatchedAt);
|
|
818
845
|
try {
|
|
819
846
|
res = await fetch(input, { ...init, redirect: "manual" });
|
|
820
847
|
} catch (err) {
|
|
@@ -829,11 +856,14 @@ async function fetchWithRetry(input, init, opts) {
|
|
|
829
856
|
await sleep2(jitter(BASE_DELAY_MS * 2 ** attempt), init?.signal ?? void 0);
|
|
830
857
|
continue;
|
|
831
858
|
}
|
|
859
|
+
headersAt = Date.now();
|
|
832
860
|
governor?.observe(res);
|
|
833
861
|
if (res.ok || !RETRYABLE_STATUS_CODES.has(res.status) || attempt >= maxRetries) {
|
|
862
|
+
reportComplete(attempt + 1);
|
|
834
863
|
return res;
|
|
835
864
|
}
|
|
836
865
|
if (init?.signal?.aborted) {
|
|
866
|
+
reportComplete(attempt + 1);
|
|
837
867
|
return res;
|
|
838
868
|
}
|
|
839
869
|
const retryAfterMs = parseRetryAfter(res.headers);
|
|
@@ -845,6 +875,86 @@ async function fetchWithRetry(input, init, opts) {
|
|
|
845
875
|
}
|
|
846
876
|
}
|
|
847
877
|
|
|
878
|
+
// src/providers/call-timing.ts
|
|
879
|
+
var CallTimingRecorder = class {
|
|
880
|
+
fetchTiming;
|
|
881
|
+
firstTokenAt;
|
|
882
|
+
/** Cumulative time awaiting `reader.read()`, i.e. transport, not consumer. */
|
|
883
|
+
readWaitMs = 0;
|
|
884
|
+
/** Pass as `timing` in the `fetchWithRetry` options object. */
|
|
885
|
+
observer = {
|
|
886
|
+
onComplete: (timing) => {
|
|
887
|
+
this.fetchTiming = timing;
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
/**
|
|
891
|
+
* Await one body read, charging only the wait itself to `wireMs`. Time the
|
|
892
|
+
* consumer spends between reads is excluded because it elapses outside this
|
|
893
|
+
* call.
|
|
894
|
+
*/
|
|
895
|
+
async read(reader) {
|
|
896
|
+
const start = Date.now();
|
|
897
|
+
try {
|
|
898
|
+
return await reader.read();
|
|
899
|
+
} finally {
|
|
900
|
+
this.readWaitMs += Date.now() - start;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
/** Stamp the first content delta. Idempotent — only the first call counts. */
|
|
904
|
+
markFirstToken() {
|
|
905
|
+
this.firstTokenAt ??= Date.now();
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Timing for a non-streaming call. Call once the response body is parsed.
|
|
909
|
+
*
|
|
910
|
+
* Also the right reading at an adapter's `!res.ok` throw site on BOTH
|
|
911
|
+
* transports: headers have arrived, no body will be streamed, so
|
|
912
|
+
* dispatch → now is the whole of what the provider cost us.
|
|
913
|
+
*/
|
|
914
|
+
chatTiming() {
|
|
915
|
+
const t = this.fetchTiming;
|
|
916
|
+
if (!t) return void 0;
|
|
917
|
+
return {
|
|
918
|
+
queuedMs: t.queuedMs,
|
|
919
|
+
attempts: t.attempts,
|
|
920
|
+
retryMs: t.retryMs,
|
|
921
|
+
ttfbMs: Math.max(0, t.headersAt - t.dispatchedAt),
|
|
922
|
+
wireMs: Math.max(0, Date.now() - t.dispatchedAt)
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
/** Timing for a streamed call. Call when building the terminal `done` chunk. */
|
|
926
|
+
streamTiming() {
|
|
927
|
+
const t = this.fetchTiming;
|
|
928
|
+
if (!t) return void 0;
|
|
929
|
+
const ttfbMs = Math.max(0, t.headersAt - t.dispatchedAt);
|
|
930
|
+
return {
|
|
931
|
+
queuedMs: t.queuedMs,
|
|
932
|
+
attempts: t.attempts,
|
|
933
|
+
retryMs: t.retryMs,
|
|
934
|
+
ttfbMs,
|
|
935
|
+
...this.firstTokenAt !== void 0 ? { firstTokenMs: Math.max(0, this.firstTokenAt - t.dispatchedAt) } : {},
|
|
936
|
+
wireMs: ttfbMs + this.readWaitMs
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
async function* withCallTiming(recorder, source) {
|
|
941
|
+
for await (const chunk of source) {
|
|
942
|
+
if (chunk.type === "text_delta" || chunk.type === "thinking_delta") {
|
|
943
|
+
recorder.markFirstToken();
|
|
944
|
+
yield chunk;
|
|
945
|
+
} else if (chunk.type === "done") {
|
|
946
|
+
const timing = recorder.streamTiming();
|
|
947
|
+
yield timing ? { ...chunk, timing } : chunk;
|
|
948
|
+
} else {
|
|
949
|
+
yield chunk;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function withChatTiming(recorder, response) {
|
|
954
|
+
const timing = recorder.chatTiming();
|
|
955
|
+
return timing ? { ...response, timing } : response;
|
|
956
|
+
}
|
|
957
|
+
|
|
848
958
|
// src/providers/rate-limiter.ts
|
|
849
959
|
function abortError() {
|
|
850
960
|
return Object.assign(new Error("The operation was aborted"), { name: "AbortError" });
|
|
@@ -1297,6 +1407,7 @@ var OpenAICompatibleProvider = class {
|
|
|
1297
1407
|
async chat(messages, options) {
|
|
1298
1408
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
1299
1409
|
const body = this.buildRequestBody(messages, options, false);
|
|
1410
|
+
const recorder = new CallTimingRecorder();
|
|
1300
1411
|
const res = await fetchWithRetry(
|
|
1301
1412
|
`${this.baseUrl}/chat/completions`,
|
|
1302
1413
|
{
|
|
@@ -1305,7 +1416,7 @@ var OpenAICompatibleProvider = class {
|
|
|
1305
1416
|
body: JSON.stringify(body),
|
|
1306
1417
|
signal: options.signal
|
|
1307
1418
|
},
|
|
1308
|
-
{ governor: this.governor, provider: this.name }
|
|
1419
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
1309
1420
|
);
|
|
1310
1421
|
if (!res.ok) {
|
|
1311
1422
|
const errorBody = await res.text();
|
|
@@ -1314,15 +1425,20 @@ var OpenAICompatibleProvider = class {
|
|
|
1314
1425
|
status: res.status,
|
|
1315
1426
|
headers: res.headers,
|
|
1316
1427
|
message: this.extractErrorMessage(errorBody, res.status),
|
|
1317
|
-
body: errorBody
|
|
1428
|
+
body: errorBody,
|
|
1429
|
+
timing: recorder.chatTiming()
|
|
1318
1430
|
});
|
|
1319
1431
|
}
|
|
1320
1432
|
const json = await res.json();
|
|
1321
|
-
return
|
|
1433
|
+
return withChatTiming(
|
|
1434
|
+
recorder,
|
|
1435
|
+
this.parseResponse(json, this.requestModel(body, options.model), body)
|
|
1436
|
+
);
|
|
1322
1437
|
}
|
|
1323
1438
|
async *stream(messages, options) {
|
|
1324
1439
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
1325
1440
|
const body = this.buildRequestBody(messages, options, true);
|
|
1441
|
+
const recorder = new CallTimingRecorder();
|
|
1326
1442
|
const res = await fetchWithRetry(
|
|
1327
1443
|
`${this.baseUrl}/chat/completions`,
|
|
1328
1444
|
{
|
|
@@ -1331,7 +1447,7 @@ var OpenAICompatibleProvider = class {
|
|
|
1331
1447
|
body: JSON.stringify(body),
|
|
1332
1448
|
signal: options.signal
|
|
1333
1449
|
},
|
|
1334
|
-
{ governor: this.governor, provider: this.name }
|
|
1450
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
1335
1451
|
);
|
|
1336
1452
|
if (!res.ok) {
|
|
1337
1453
|
const errorBody = await res.text();
|
|
@@ -1340,13 +1456,17 @@ var OpenAICompatibleProvider = class {
|
|
|
1340
1456
|
status: res.status,
|
|
1341
1457
|
headers: res.headers,
|
|
1342
1458
|
message: this.extractErrorMessage(errorBody, res.status),
|
|
1343
|
-
body: errorBody
|
|
1459
|
+
body: errorBody,
|
|
1460
|
+
timing: recorder.chatTiming()
|
|
1344
1461
|
});
|
|
1345
1462
|
}
|
|
1346
1463
|
if (!res.body) {
|
|
1347
1464
|
throw new Error(`${this.profile.label ?? this.profile.name} stream response has no body`);
|
|
1348
1465
|
}
|
|
1349
|
-
yield*
|
|
1466
|
+
yield* withCallTiming(
|
|
1467
|
+
recorder,
|
|
1468
|
+
this.parseSSEStream(res.body, this.requestModel(body, options.model), body, recorder)
|
|
1469
|
+
);
|
|
1350
1470
|
}
|
|
1351
1471
|
// ---------------------------------------------------------------------------
|
|
1352
1472
|
// Request building
|
|
@@ -1573,7 +1693,21 @@ var OpenAICompatibleProvider = class {
|
|
|
1573
1693
|
// ---------------------------------------------------------------------------
|
|
1574
1694
|
// SSE streaming
|
|
1575
1695
|
// ---------------------------------------------------------------------------
|
|
1576
|
-
|
|
1696
|
+
/**
|
|
1697
|
+
* PRIVATE, and deliberately not an extension point. It was `protected`, but
|
|
1698
|
+
* nothing in or out of this repo overrides it, and `protected` could not
|
|
1699
|
+
* carry the guarantee it appeared to: TypeScript checks an override for
|
|
1700
|
+
* assignability, and a function type with fewer parameters is assignable to
|
|
1701
|
+
* one with more — so an override that simply dropped the recorder still
|
|
1702
|
+
* compiled and then reported `wireMs === ttfbMs` for the whole subclass. Its
|
|
1703
|
+
* type is not exported from the barrel either, so a subclass author could not
|
|
1704
|
+
* name what they were required to accept.
|
|
1705
|
+
*
|
|
1706
|
+
* A provider with a different wire format implements `Provider` (see
|
|
1707
|
+
* docs/providers.md); an OpenAI-compatible one adds a `ProviderProfile`.
|
|
1708
|
+
* Neither needs to reach in here.
|
|
1709
|
+
*/
|
|
1710
|
+
async *parseSSEStream(body, model, request, timing) {
|
|
1577
1711
|
const reader = body.getReader();
|
|
1578
1712
|
const decoder = new TextDecoder();
|
|
1579
1713
|
let buffer = "";
|
|
@@ -1633,7 +1767,7 @@ var OpenAICompatibleProvider = class {
|
|
|
1633
1767
|
};
|
|
1634
1768
|
try {
|
|
1635
1769
|
for (; ; ) {
|
|
1636
|
-
const { done, value } = await
|
|
1770
|
+
const { done, value } = await timing.read(reader);
|
|
1637
1771
|
if (done) break;
|
|
1638
1772
|
buffer += decoder.decode(value, { stream: true });
|
|
1639
1773
|
const lines = buffer.split("\n");
|
|
@@ -1665,7 +1799,8 @@ var OpenAICompatibleProvider = class {
|
|
|
1665
1799
|
status: 0,
|
|
1666
1800
|
retryable: false,
|
|
1667
1801
|
message,
|
|
1668
|
-
body: JSON.stringify(parsed.error)
|
|
1802
|
+
body: JSON.stringify(parsed.error),
|
|
1803
|
+
timing: timing.streamTiming()
|
|
1669
1804
|
});
|
|
1670
1805
|
}
|
|
1671
1806
|
if (parsed.model !== void 0 || parsed.service_tier !== void 0 || parsed.serviceTier !== void 0) {
|
|
@@ -1737,7 +1872,8 @@ var OpenAICompatibleProvider = class {
|
|
|
1737
1872
|
provider: this.name,
|
|
1738
1873
|
status: 0,
|
|
1739
1874
|
retryable: true,
|
|
1740
|
-
message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]
|
|
1875
|
+
message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]`,
|
|
1876
|
+
timing: timing.streamTiming()
|
|
1741
1877
|
});
|
|
1742
1878
|
} finally {
|
|
1743
1879
|
reader.releaseLock();
|
|
@@ -1776,12 +1912,12 @@ var DIRECT_OPENAI_CATALOG = [
|
|
|
1776
1912
|
{
|
|
1777
1913
|
aliases: ["gpt-5.6", "gpt-5.6-sol"],
|
|
1778
1914
|
snapshotBase: "gpt-5.6-sol",
|
|
1779
|
-
short: { input:
|
|
1915
|
+
short: { input: 4 / M, cachedInput: 0.4 / M, cacheWrite: 5 / M, output: 20 / M },
|
|
1780
1916
|
long: withLongContext({
|
|
1781
|
-
input:
|
|
1782
|
-
cachedInput: 0.
|
|
1783
|
-
cacheWrite:
|
|
1784
|
-
output:
|
|
1917
|
+
input: 4 / M,
|
|
1918
|
+
cachedInput: 0.4 / M,
|
|
1919
|
+
cacheWrite: 5 / M,
|
|
1920
|
+
output: 20 / M
|
|
1785
1921
|
}),
|
|
1786
1922
|
contextBoundary: LONG_CONTEXT_BOUNDARY
|
|
1787
1923
|
},
|
|
@@ -2296,6 +2432,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2296
2432
|
async chat(messages, options) {
|
|
2297
2433
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
2298
2434
|
const body = this.buildRequestBody(messages, options, false);
|
|
2435
|
+
const recorder = new CallTimingRecorder();
|
|
2299
2436
|
const res = await fetchWithRetry(
|
|
2300
2437
|
`${this.baseUrl}/responses`,
|
|
2301
2438
|
{
|
|
@@ -2304,7 +2441,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2304
2441
|
body: JSON.stringify(body),
|
|
2305
2442
|
signal: options.signal
|
|
2306
2443
|
},
|
|
2307
|
-
{ governor: this.governor, provider: this.name }
|
|
2444
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
2308
2445
|
);
|
|
2309
2446
|
if (!res.ok) {
|
|
2310
2447
|
const errorBody = await res.text();
|
|
@@ -2314,11 +2451,15 @@ var OpenAIResponsesProvider = class {
|
|
|
2314
2451
|
status: res.status,
|
|
2315
2452
|
headers: res.headers,
|
|
2316
2453
|
message,
|
|
2317
|
-
body: errorBody
|
|
2454
|
+
body: errorBody,
|
|
2455
|
+
timing: recorder.chatTiming()
|
|
2318
2456
|
});
|
|
2319
2457
|
}
|
|
2320
2458
|
const json = await res.json();
|
|
2321
|
-
return
|
|
2459
|
+
return withChatTiming(
|
|
2460
|
+
recorder,
|
|
2461
|
+
this.parseResponse(json, this.requestModel(body, options.model), body)
|
|
2462
|
+
);
|
|
2322
2463
|
}
|
|
2323
2464
|
// ---------------------------------------------------------------------------
|
|
2324
2465
|
// stream - SSE streaming
|
|
@@ -2326,6 +2467,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2326
2467
|
async *stream(messages, options) {
|
|
2327
2468
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
2328
2469
|
const body = this.buildRequestBody(messages, options, true);
|
|
2470
|
+
const recorder = new CallTimingRecorder();
|
|
2329
2471
|
const res = await fetchWithRetry(
|
|
2330
2472
|
`${this.baseUrl}/responses`,
|
|
2331
2473
|
{
|
|
@@ -2334,7 +2476,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2334
2476
|
body: JSON.stringify(body),
|
|
2335
2477
|
signal: options.signal
|
|
2336
2478
|
},
|
|
2337
|
-
{ governor: this.governor, provider: this.name }
|
|
2479
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
2338
2480
|
);
|
|
2339
2481
|
if (!res.ok) {
|
|
2340
2482
|
const errorBody = await res.text();
|
|
@@ -2344,13 +2486,17 @@ var OpenAIResponsesProvider = class {
|
|
|
2344
2486
|
status: res.status,
|
|
2345
2487
|
headers: res.headers,
|
|
2346
2488
|
message,
|
|
2347
|
-
body: errorBody
|
|
2489
|
+
body: errorBody,
|
|
2490
|
+
timing: recorder.chatTiming()
|
|
2348
2491
|
});
|
|
2349
2492
|
}
|
|
2350
2493
|
if (!res.body) {
|
|
2351
2494
|
throw new Error("OpenAI Responses stream has no body");
|
|
2352
2495
|
}
|
|
2353
|
-
yield*
|
|
2496
|
+
yield* withCallTiming(
|
|
2497
|
+
recorder,
|
|
2498
|
+
this.parseSSEStream(res.body, this.requestModel(body, options.model), body, recorder)
|
|
2499
|
+
);
|
|
2354
2500
|
}
|
|
2355
2501
|
// ---------------------------------------------------------------------------
|
|
2356
2502
|
// Internal: build request body
|
|
@@ -2552,7 +2698,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2552
2698
|
// ---------------------------------------------------------------------------
|
|
2553
2699
|
// Internal: parse SSE stream
|
|
2554
2700
|
// ---------------------------------------------------------------------------
|
|
2555
|
-
async *parseSSEStream(body, model, request) {
|
|
2701
|
+
async *parseSSEStream(body, model, request, timing) {
|
|
2556
2702
|
const reader = body.getReader();
|
|
2557
2703
|
const decoder = new TextDecoder();
|
|
2558
2704
|
let buffer = "";
|
|
@@ -2560,7 +2706,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2560
2706
|
let eventType = "";
|
|
2561
2707
|
try {
|
|
2562
2708
|
while (true) {
|
|
2563
|
-
const { done, value } = await
|
|
2709
|
+
const { done, value } = await timing.read(reader);
|
|
2564
2710
|
if (done) break;
|
|
2565
2711
|
buffer += decoder.decode(value, { stream: true });
|
|
2566
2712
|
const lines = buffer.split("\n");
|
|
@@ -2580,7 +2726,14 @@ var OpenAIResponsesProvider = class {
|
|
|
2580
2726
|
} catch {
|
|
2581
2727
|
continue;
|
|
2582
2728
|
}
|
|
2583
|
-
const chunk = this.handleStreamEvent(
|
|
2729
|
+
const chunk = this.handleStreamEvent(
|
|
2730
|
+
eventType,
|
|
2731
|
+
data,
|
|
2732
|
+
model,
|
|
2733
|
+
callIdMap,
|
|
2734
|
+
request,
|
|
2735
|
+
timing
|
|
2736
|
+
);
|
|
2584
2737
|
if (chunk) {
|
|
2585
2738
|
yield chunk;
|
|
2586
2739
|
if (chunk.type === "done") return;
|
|
@@ -2594,7 +2747,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2594
2747
|
reader.releaseLock();
|
|
2595
2748
|
}
|
|
2596
2749
|
}
|
|
2597
|
-
handleStreamEvent(eventType, data, model, callIdMap, request) {
|
|
2750
|
+
handleStreamEvent(eventType, data, model, callIdMap, request, timing) {
|
|
2598
2751
|
switch (eventType) {
|
|
2599
2752
|
case "response.output_text.delta":
|
|
2600
2753
|
return { type: "text_delta", content: data.delta ?? "" };
|
|
@@ -2650,7 +2803,8 @@ var OpenAIResponsesProvider = class {
|
|
|
2650
2803
|
provider: this.name,
|
|
2651
2804
|
status: 0,
|
|
2652
2805
|
retryable: false,
|
|
2653
|
-
message: `OpenAI Responses API error: ${errorMsg}
|
|
2806
|
+
message: `OpenAI Responses API error: ${errorMsg}`,
|
|
2807
|
+
timing: timing.streamTiming()
|
|
2654
2808
|
});
|
|
2655
2809
|
}
|
|
2656
2810
|
default:
|
|
@@ -2718,6 +2872,12 @@ function hasAnthropicProviderFileImage(messages) {
|
|
|
2718
2872
|
);
|
|
2719
2873
|
}
|
|
2720
2874
|
var CLAUDE_CAPABILITIES = {
|
|
2875
|
+
"claude-fable-5-1": {
|
|
2876
|
+
thinking: "adaptive-always-on",
|
|
2877
|
+
effortLevels: ["low", "medium", "high", "xhigh", "max"],
|
|
2878
|
+
manualBudget: false,
|
|
2879
|
+
stripTemperature: true
|
|
2880
|
+
},
|
|
2721
2881
|
"claude-fable-5": {
|
|
2722
2882
|
thinking: "adaptive-always-on",
|
|
2723
2883
|
effortLevels: ["low", "medium", "high", "xhigh", "max"],
|
|
@@ -2808,10 +2968,18 @@ var PRE_INFERENCE_GEO_MODELS = /* @__PURE__ */ new Set([
|
|
|
2808
2968
|
function resolveClaudeCapability(model) {
|
|
2809
2969
|
return CLAUDE_CAPABILITIES[model] ?? (LEGACY_CLAUDE_MODELS.has(model) ? LEGACY_CLAUDE_CAPABILITY : void 0);
|
|
2810
2970
|
}
|
|
2971
|
+
var DEFAULT_CACHE_READ_MULTIPLIER = 0.1;
|
|
2811
2972
|
var ANTHROPIC_RATES = {
|
|
2973
|
+
"claude-fable-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
|
|
2812
2974
|
"claude-fable-5": { input: 1e-5, output: 5e-5 },
|
|
2975
|
+
// Limited availability (Glasswing). Priced from the public pricing table; no
|
|
2976
|
+
// CLAUDE_CAPABILITIES entry, because their thinking semantics are not
|
|
2977
|
+
// documented per-model and an unverified capability would send the wrong
|
|
2978
|
+
// request shape. Unknown IDs pass through unmodified, which is the safe side.
|
|
2979
|
+
"claude-mythos-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
|
|
2980
|
+
"claude-mythos-5": { input: 1e-5, output: 5e-5 },
|
|
2813
2981
|
"claude-opus-5": { input: 5e-6, output: 25e-6 },
|
|
2814
|
-
"claude-sonnet-5": { input:
|
|
2982
|
+
"claude-sonnet-5": { input: 2e-6, output: 1e-5 },
|
|
2815
2983
|
"claude-opus-4-8": { input: 5e-6, output: 25e-6 },
|
|
2816
2984
|
"claude-opus-4-7": { input: 5e-6, output: 25e-6 },
|
|
2817
2985
|
"claude-opus-4-6": { input: 5e-6, output: 25e-6 },
|
|
@@ -2850,7 +3018,7 @@ function anthropicStreamErrorStatus(type) {
|
|
|
2850
3018
|
return 0;
|
|
2851
3019
|
}
|
|
2852
3020
|
}
|
|
2853
|
-
function estimateAnthropicCost(model, usage
|
|
3021
|
+
function estimateAnthropicCost(model, usage) {
|
|
2854
3022
|
if (!model || !isValidTokenCount(usage.inputTokens) || !isValidTokenCount(usage.outputTokens)) {
|
|
2855
3023
|
return void 0;
|
|
2856
3024
|
}
|
|
@@ -2869,8 +3037,9 @@ function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
|
|
|
2869
3037
|
if (usage.aggregateCacheWriteTokens !== void 0 && (!isValidTokenCount(usage.aggregateCacheWriteTokens) || usage.aggregateCacheWriteTokens !== cacheWriteTotal)) {
|
|
2870
3038
|
return void 0;
|
|
2871
3039
|
}
|
|
2872
|
-
const { input, output } = rate
|
|
2873
|
-
|
|
3040
|
+
const { input, output } = rate;
|
|
3041
|
+
const cacheReadRate = input * (rate.cacheReadMultiplier ?? DEFAULT_CACHE_READ_MULTIPLIER);
|
|
3042
|
+
return usage.inputTokens * input + cacheRead * cacheReadRate + cacheWrite5m * input * 1.25 + cacheWrite1h * input * 2 + usage.outputTokens * output;
|
|
2874
3043
|
}
|
|
2875
3044
|
var THINKING_BUDGETS = {
|
|
2876
3045
|
low: 1024,
|
|
@@ -3111,6 +3280,14 @@ var AnthropicProvider = class {
|
|
|
3111
3280
|
const resolved = resolveThinkingOptions(options);
|
|
3112
3281
|
if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
|
|
3113
3282
|
const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
|
|
3283
|
+
if (ANTHROPIC_RATES[model] && !resolveClaudeCapability(model)) {
|
|
3284
|
+
return {
|
|
3285
|
+
requested: resolved.effort,
|
|
3286
|
+
effective: "unset",
|
|
3287
|
+
clamped: true,
|
|
3288
|
+
cause: `Axl has no capability entry for Anthropic ${model}, so no reasoning control was sent and effort '${resolved.effort}' had no effect`
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3114
3291
|
const config = resolveClaudeThinking(model, resolved);
|
|
3115
3292
|
const wireEffort = typeof config.outputConfig?.effort === "string" ? config.outputConfig.effort : void 0;
|
|
3116
3293
|
const effective = config.clamp?.effective ?? wireEffort;
|
|
@@ -3166,6 +3343,10 @@ var AnthropicProvider = class {
|
|
|
3166
3343
|
}
|
|
3167
3344
|
/** Anthropic ignores native `json_schema` structurally — Axl uses a system
|
|
3168
3345
|
* prompt JSON instruction + client-side Zod validation (see buildRequest). */
|
|
3346
|
+
/** The only built-in adapter that honors `promptCache` (see buildBody). */
|
|
3347
|
+
realizesPromptCache() {
|
|
3348
|
+
return true;
|
|
3349
|
+
}
|
|
3169
3350
|
nativeStructuredOutputSupport() {
|
|
3170
3351
|
return "unsupported";
|
|
3171
3352
|
}
|
|
@@ -3207,6 +3388,7 @@ var AnthropicProvider = class {
|
|
|
3207
3388
|
);
|
|
3208
3389
|
const body = this.buildRequestBody(messages, options, false);
|
|
3209
3390
|
const pricingContext = pricingContextFromBody(body);
|
|
3391
|
+
const recorder = new CallTimingRecorder();
|
|
3210
3392
|
const res = await fetchWithRetry(
|
|
3211
3393
|
`${this.baseUrl}/messages`,
|
|
3212
3394
|
{
|
|
@@ -3215,7 +3397,7 @@ var AnthropicProvider = class {
|
|
|
3215
3397
|
body: JSON.stringify(body),
|
|
3216
3398
|
signal: options.signal
|
|
3217
3399
|
},
|
|
3218
|
-
{ governor: this.governor, provider: this.name }
|
|
3400
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
3219
3401
|
);
|
|
3220
3402
|
if (!res.ok) {
|
|
3221
3403
|
const errorBody = await res.text();
|
|
@@ -3225,11 +3407,12 @@ var AnthropicProvider = class {
|
|
|
3225
3407
|
status: res.status,
|
|
3226
3408
|
headers: res.headers,
|
|
3227
3409
|
message,
|
|
3228
|
-
body: errorBody
|
|
3410
|
+
body: errorBody,
|
|
3411
|
+
timing: recorder.chatTiming()
|
|
3229
3412
|
});
|
|
3230
3413
|
}
|
|
3231
3414
|
const json = await res.json();
|
|
3232
|
-
return this.parseResponse(json, pricingContext);
|
|
3415
|
+
return withChatTiming(recorder, this.parseResponse(json, pricingContext));
|
|
3233
3416
|
}
|
|
3234
3417
|
// ---------------------------------------------------------------------------
|
|
3235
3418
|
// stream - SSE streaming completion
|
|
@@ -3241,6 +3424,7 @@ var AnthropicProvider = class {
|
|
|
3241
3424
|
);
|
|
3242
3425
|
const body = this.buildRequestBody(messages, options, true);
|
|
3243
3426
|
const pricingContext = pricingContextFromBody(body);
|
|
3427
|
+
const recorder = new CallTimingRecorder();
|
|
3244
3428
|
const res = await fetchWithRetry(
|
|
3245
3429
|
`${this.baseUrl}/messages`,
|
|
3246
3430
|
{
|
|
@@ -3249,7 +3433,7 @@ var AnthropicProvider = class {
|
|
|
3249
3433
|
body: JSON.stringify(body),
|
|
3250
3434
|
signal: options.signal
|
|
3251
3435
|
},
|
|
3252
|
-
{ governor: this.governor, provider: this.name }
|
|
3436
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
3253
3437
|
);
|
|
3254
3438
|
if (!res.ok) {
|
|
3255
3439
|
const errorBody = await res.text();
|
|
@@ -3259,13 +3443,17 @@ var AnthropicProvider = class {
|
|
|
3259
3443
|
status: res.status,
|
|
3260
3444
|
headers: res.headers,
|
|
3261
3445
|
message,
|
|
3262
|
-
body: errorBody
|
|
3446
|
+
body: errorBody,
|
|
3447
|
+
timing: recorder.chatTiming()
|
|
3263
3448
|
});
|
|
3264
3449
|
}
|
|
3265
3450
|
if (!res.body) {
|
|
3266
3451
|
throw new Error("Anthropic stream response has no body");
|
|
3267
3452
|
}
|
|
3268
|
-
yield*
|
|
3453
|
+
yield* withCallTiming(
|
|
3454
|
+
recorder,
|
|
3455
|
+
this.parseSSEStream(res.body, pricingContext, res.headers, recorder)
|
|
3456
|
+
);
|
|
3269
3457
|
}
|
|
3270
3458
|
// ---------------------------------------------------------------------------
|
|
3271
3459
|
// Internal: request building
|
|
@@ -3300,14 +3488,31 @@ var AnthropicProvider = class {
|
|
|
3300
3488
|
max_tokens: options.maxTokens ?? 4096,
|
|
3301
3489
|
stream
|
|
3302
3490
|
};
|
|
3303
|
-
|
|
3491
|
+
const po = options.providerOptions;
|
|
3492
|
+
const callerOwnsPrefix = po !== void 0 && (po.cache_control !== void 0 || po.system !== void 0 || po.tools !== void 0);
|
|
3493
|
+
const promptCache = options.promptCache === true && !callerOwnsPrefix;
|
|
3494
|
+
const anchorOnSystem = promptCache && systemMessages.length > 0 && systemMessages[0].origin !== "runtime";
|
|
3495
|
+
if (anchorOnSystem) {
|
|
3496
|
+
body.system = systemMessages.map((m, index) => ({
|
|
3497
|
+
type: "text",
|
|
3498
|
+
text: typeof m.content === "string" ? m.content : "",
|
|
3499
|
+
...index === 0 ? { cache_control: { type: "ephemeral" } } : {}
|
|
3500
|
+
}));
|
|
3501
|
+
} else if (systemText) {
|
|
3304
3502
|
body.system = systemText;
|
|
3305
3503
|
}
|
|
3306
3504
|
if (options.stop) {
|
|
3307
3505
|
body.stop_sequences = options.stop;
|
|
3308
3506
|
}
|
|
3309
3507
|
if (options.tools && options.tools.length > 0) {
|
|
3310
|
-
|
|
3508
|
+
const tools = options.tools.map((t) => this.mapToolDefinition(t));
|
|
3509
|
+
if (promptCache && !anchorOnSystem) {
|
|
3510
|
+
tools[tools.length - 1] = {
|
|
3511
|
+
...tools[tools.length - 1],
|
|
3512
|
+
cache_control: { type: "ephemeral" }
|
|
3513
|
+
};
|
|
3514
|
+
}
|
|
3515
|
+
body.tools = tools;
|
|
3311
3516
|
}
|
|
3312
3517
|
if (options.toolChoice !== void 0) {
|
|
3313
3518
|
body.tool_choice = this.mapToolChoice(options.toolChoice);
|
|
@@ -3326,9 +3531,13 @@ var AnthropicProvider = class {
|
|
|
3326
3531
|
}
|
|
3327
3532
|
if (options.responseFormat && options.responseFormat.type !== "text") {
|
|
3328
3533
|
const jsonInstruction = "You must respond with valid JSON only. No markdown fences, no extra text.";
|
|
3329
|
-
|
|
3534
|
+
if (Array.isArray(body.system)) {
|
|
3535
|
+
body.system = [...body.system, { type: "text", text: jsonInstruction }];
|
|
3536
|
+
} else {
|
|
3537
|
+
body.system = body.system ? `${body.system}
|
|
3330
3538
|
|
|
3331
3539
|
${jsonInstruction}` : jsonInstruction;
|
|
3540
|
+
}
|
|
3332
3541
|
}
|
|
3333
3542
|
if (options.providerOptions) {
|
|
3334
3543
|
Object.assign(body, options.providerOptions);
|
|
@@ -3508,7 +3717,7 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3508
3717
|
// ---------------------------------------------------------------------------
|
|
3509
3718
|
// Internal: SSE stream parsing
|
|
3510
3719
|
// ---------------------------------------------------------------------------
|
|
3511
|
-
async *parseSSEStream(body, pricingContext, responseHeaders) {
|
|
3720
|
+
async *parseSSEStream(body, pricingContext, responseHeaders, timing) {
|
|
3512
3721
|
const reader = body.getReader();
|
|
3513
3722
|
const decoder = new TextDecoder();
|
|
3514
3723
|
let buffer = "";
|
|
@@ -3537,7 +3746,7 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3537
3746
|
});
|
|
3538
3747
|
try {
|
|
3539
3748
|
while (true) {
|
|
3540
|
-
const { done, value } = await
|
|
3749
|
+
const { done, value } = await timing.read(reader);
|
|
3541
3750
|
if (done) break;
|
|
3542
3751
|
buffer += decoder.decode(value, { stream: true });
|
|
3543
3752
|
const lines = buffer.split("\n");
|
|
@@ -3560,7 +3769,8 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3560
3769
|
provider: this.name,
|
|
3561
3770
|
status,
|
|
3562
3771
|
headers: responseHeaders,
|
|
3563
|
-
message: event.error?.message ?? "Anthropic stream failed"
|
|
3772
|
+
message: event.error?.message ?? "Anthropic stream failed",
|
|
3773
|
+
timing: timing.streamTiming()
|
|
3564
3774
|
});
|
|
3565
3775
|
}
|
|
3566
3776
|
case "content_block_start": {
|
|
@@ -3796,7 +4006,6 @@ var GEMINI_PRICING = {
|
|
|
3796
4006
|
},
|
|
3797
4007
|
"gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7 },
|
|
3798
4008
|
"gemini-2.5-flash-lite": { input: 1e-7, cached: 1e-8, output: 4e-7 },
|
|
3799
|
-
"gemini-3-flash-preview": { input: 5e-7, cached: 5e-8, output: 3e-6 },
|
|
3800
4009
|
"gemini-3.1-pro-preview": {
|
|
3801
4010
|
input: 2e-6,
|
|
3802
4011
|
cached: 2e-7,
|
|
@@ -3812,7 +4021,9 @@ var GEMINI_PRICING = {
|
|
|
3812
4021
|
"gemini-3.1-flash-lite": { input: 25e-8, cached: 25e-9, output: 15e-7 },
|
|
3813
4022
|
"gemini-3.5-flash": { input: 15e-7, cached: 15e-8, output: 9e-6 },
|
|
3814
4023
|
"gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
|
|
3815
|
-
"gemini-3.6-flash": { input:
|
|
4024
|
+
"gemini-3.6-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
|
|
4025
|
+
"gemini-3.7-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
|
|
4026
|
+
"gemini-3.8-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 }
|
|
3816
4027
|
};
|
|
3817
4028
|
function isValidTokenCount2(value) {
|
|
3818
4029
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
@@ -4157,6 +4368,7 @@ var GeminiProvider = class {
|
|
|
4157
4368
|
const pricingContext = this.pricingContext(this.requestModel(options), body);
|
|
4158
4369
|
this.assertSafeGemini3Continuation(messages, pricingContext.model);
|
|
4159
4370
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4371
|
+
const recorder = new CallTimingRecorder();
|
|
4160
4372
|
const res = await fetchWithRetry(
|
|
4161
4373
|
`${this.baseUrl}/models/${pricingContext.model}:generateContent`,
|
|
4162
4374
|
{
|
|
@@ -4165,7 +4377,7 @@ var GeminiProvider = class {
|
|
|
4165
4377
|
body: JSON.stringify(body),
|
|
4166
4378
|
signal: options.signal
|
|
4167
4379
|
},
|
|
4168
|
-
{ governor: this.governor, provider: this.name }
|
|
4380
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4169
4381
|
);
|
|
4170
4382
|
if (!res.ok) {
|
|
4171
4383
|
const errorBody = await res.text();
|
|
@@ -4175,11 +4387,12 @@ var GeminiProvider = class {
|
|
|
4175
4387
|
status: res.status,
|
|
4176
4388
|
headers: res.headers,
|
|
4177
4389
|
message,
|
|
4178
|
-
body: errorBody
|
|
4390
|
+
body: errorBody,
|
|
4391
|
+
timing: recorder.chatTiming()
|
|
4179
4392
|
});
|
|
4180
4393
|
}
|
|
4181
4394
|
const json = await res.json();
|
|
4182
|
-
return this.parseResponse(json, pricingContext);
|
|
4395
|
+
return withChatTiming(recorder, this.parseResponse(json, pricingContext));
|
|
4183
4396
|
}
|
|
4184
4397
|
// ---------------------------------------------------------------------------
|
|
4185
4398
|
// stream - SSE streaming completion
|
|
@@ -4193,6 +4406,7 @@ var GeminiProvider = class {
|
|
|
4193
4406
|
const pricingContext = this.pricingContext(this.requestModel(options), body);
|
|
4194
4407
|
this.assertSafeGemini3Continuation(messages, pricingContext.model);
|
|
4195
4408
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4409
|
+
const recorder = new CallTimingRecorder();
|
|
4196
4410
|
const res = await fetchWithRetry(
|
|
4197
4411
|
`${this.baseUrl}/models/${pricingContext.model}:streamGenerateContent?alt=sse`,
|
|
4198
4412
|
{
|
|
@@ -4201,7 +4415,7 @@ var GeminiProvider = class {
|
|
|
4201
4415
|
body: JSON.stringify(body),
|
|
4202
4416
|
signal: options.signal
|
|
4203
4417
|
},
|
|
4204
|
-
{ governor: this.governor, provider: this.name }
|
|
4418
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4205
4419
|
);
|
|
4206
4420
|
if (!res.ok) {
|
|
4207
4421
|
const errorBody = await res.text();
|
|
@@ -4211,13 +4425,14 @@ var GeminiProvider = class {
|
|
|
4211
4425
|
status: res.status,
|
|
4212
4426
|
headers: res.headers,
|
|
4213
4427
|
message,
|
|
4214
|
-
body: errorBody
|
|
4428
|
+
body: errorBody,
|
|
4429
|
+
timing: recorder.chatTiming()
|
|
4215
4430
|
});
|
|
4216
4431
|
}
|
|
4217
4432
|
if (!res.body) {
|
|
4218
4433
|
throw new Error("Gemini stream response has no body");
|
|
4219
4434
|
}
|
|
4220
|
-
yield* this.parseSSEStream(res.body, pricingContext);
|
|
4435
|
+
yield* withCallTiming(recorder, this.parseSSEStream(res.body, pricingContext, recorder));
|
|
4221
4436
|
}
|
|
4222
4437
|
// ---------------------------------------------------------------------------
|
|
4223
4438
|
// Rich image requests use Gemini Interactions. It is deliberately stateless:
|
|
@@ -4228,10 +4443,11 @@ var GeminiProvider = class {
|
|
|
4228
4443
|
this.assertSafeInteractionOptions(options);
|
|
4229
4444
|
const body = this.buildInteractionRequestBody(messages, options, false);
|
|
4230
4445
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4446
|
+
const recorder = new CallTimingRecorder();
|
|
4231
4447
|
const res = await fetchWithRetry(
|
|
4232
4448
|
`${this.baseUrl}/interactions`,
|
|
4233
4449
|
{ method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
|
|
4234
|
-
{ governor: this.governor, provider: this.name }
|
|
4450
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4235
4451
|
);
|
|
4236
4452
|
if (!res.ok) {
|
|
4237
4453
|
const errorBody = await res.text();
|
|
@@ -4240,19 +4456,24 @@ var GeminiProvider = class {
|
|
|
4240
4456
|
status: res.status,
|
|
4241
4457
|
headers: res.headers,
|
|
4242
4458
|
message: this.extractErrorMessage(errorBody, res.status),
|
|
4243
|
-
body: errorBody
|
|
4459
|
+
body: errorBody,
|
|
4460
|
+
timing: recorder.chatTiming()
|
|
4244
4461
|
});
|
|
4245
4462
|
}
|
|
4246
|
-
return
|
|
4463
|
+
return withChatTiming(
|
|
4464
|
+
recorder,
|
|
4465
|
+
this.parseInteractionResponse(await res.json())
|
|
4466
|
+
);
|
|
4247
4467
|
}
|
|
4248
4468
|
async *streamInteraction(messages, options) {
|
|
4249
4469
|
this.assertSafeInteractionOptions(options);
|
|
4250
4470
|
const body = this.buildInteractionRequestBody(messages, options, true);
|
|
4251
4471
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4472
|
+
const recorder = new CallTimingRecorder();
|
|
4252
4473
|
const res = await fetchWithRetry(
|
|
4253
4474
|
`${this.baseUrl}/interactions?alt=sse`,
|
|
4254
4475
|
{ method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
|
|
4255
|
-
{ governor: this.governor, provider: this.name }
|
|
4476
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4256
4477
|
);
|
|
4257
4478
|
if (!res.ok) {
|
|
4258
4479
|
const errorBody = await res.text();
|
|
@@ -4261,11 +4482,15 @@ var GeminiProvider = class {
|
|
|
4261
4482
|
status: res.status,
|
|
4262
4483
|
headers: res.headers,
|
|
4263
4484
|
message: this.extractErrorMessage(errorBody, res.status),
|
|
4264
|
-
body: errorBody
|
|
4485
|
+
body: errorBody,
|
|
4486
|
+
timing: recorder.chatTiming()
|
|
4265
4487
|
});
|
|
4266
4488
|
}
|
|
4267
4489
|
if (!res.body) throw new Error("Gemini Interactions stream has no body");
|
|
4268
|
-
yield*
|
|
4490
|
+
yield* withCallTiming(
|
|
4491
|
+
recorder,
|
|
4492
|
+
this.parseInteractionSSEStream(res.body, res.headers, recorder)
|
|
4493
|
+
);
|
|
4269
4494
|
}
|
|
4270
4495
|
buildInteractionRequestBody(messages, options, stream) {
|
|
4271
4496
|
const model = this.requestModel(options);
|
|
@@ -4421,7 +4646,7 @@ var GeminiProvider = class {
|
|
|
4421
4646
|
providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
|
|
4422
4647
|
};
|
|
4423
4648
|
}
|
|
4424
|
-
async *parseInteractionSSEStream(body, responseHeaders) {
|
|
4649
|
+
async *parseInteractionSSEStream(body, responseHeaders, timing) {
|
|
4425
4650
|
const reader = body.getReader();
|
|
4426
4651
|
const decoder = new TextDecoder();
|
|
4427
4652
|
let buffer = "";
|
|
@@ -4431,7 +4656,7 @@ var GeminiProvider = class {
|
|
|
4431
4656
|
let completed = false;
|
|
4432
4657
|
try {
|
|
4433
4658
|
while (true) {
|
|
4434
|
-
const { done, value } = await
|
|
4659
|
+
const { done, value } = await timing.read(reader);
|
|
4435
4660
|
if (done) break;
|
|
4436
4661
|
buffer += decoder.decode(value, { stream: true });
|
|
4437
4662
|
const lines = buffer.split("\n");
|
|
@@ -4454,14 +4679,16 @@ var GeminiProvider = class {
|
|
|
4454
4679
|
provider: this.name,
|
|
4455
4680
|
status,
|
|
4456
4681
|
headers: responseHeaders,
|
|
4457
|
-
message
|
|
4682
|
+
message,
|
|
4683
|
+
timing: timing.streamTiming()
|
|
4458
4684
|
});
|
|
4459
4685
|
}
|
|
4460
4686
|
throw new ProviderError({
|
|
4461
4687
|
provider: this.name,
|
|
4462
4688
|
status: 0,
|
|
4463
4689
|
retryable: false,
|
|
4464
|
-
message
|
|
4690
|
+
message,
|
|
4691
|
+
timing: timing.streamTiming()
|
|
4465
4692
|
});
|
|
4466
4693
|
}
|
|
4467
4694
|
if (event.event_type === "step.start" && event.step) {
|
|
@@ -4939,7 +5166,7 @@ var GeminiProvider = class {
|
|
|
4939
5166
|
// ---------------------------------------------------------------------------
|
|
4940
5167
|
// Internal: SSE stream parsing
|
|
4941
5168
|
// ---------------------------------------------------------------------------
|
|
4942
|
-
async *parseSSEStream(body, pricingContext) {
|
|
5169
|
+
async *parseSSEStream(body, pricingContext, timing) {
|
|
4943
5170
|
const reader = body.getReader();
|
|
4944
5171
|
const decoder = new TextDecoder();
|
|
4945
5172
|
let buffer = "";
|
|
@@ -4952,7 +5179,7 @@ var GeminiProvider = class {
|
|
|
4952
5179
|
const accumulatedParts = [];
|
|
4953
5180
|
try {
|
|
4954
5181
|
while (true) {
|
|
4955
|
-
const { done, value } = await
|
|
5182
|
+
const { done, value } = await timing.read(reader);
|
|
4956
5183
|
if (done) break;
|
|
4957
5184
|
buffer += decoder.decode(value, { stream: true });
|
|
4958
5185
|
const lines = buffer.split("\n");
|
|
@@ -9462,6 +9689,13 @@ function describeValue(value) {
|
|
|
9462
9689
|
}
|
|
9463
9690
|
return `a value of type ${typeof value}`;
|
|
9464
9691
|
}
|
|
9692
|
+
var PROMPT_CACHE_COLD_WRITE_WARN_AFTER = 2;
|
|
9693
|
+
function nextUserTurn(history, from) {
|
|
9694
|
+
for (let i = Math.max(0, from); i < history.length; i++) {
|
|
9695
|
+
if (history[i].role === "user") return i;
|
|
9696
|
+
}
|
|
9697
|
+
return void 0;
|
|
9698
|
+
}
|
|
9465
9699
|
function estimateMessagesTokens(messages) {
|
|
9466
9700
|
let total = 0;
|
|
9467
9701
|
let unmeasured = false;
|
|
@@ -9601,6 +9835,13 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
9601
9835
|
workflowLifecycleState;
|
|
9602
9836
|
signal;
|
|
9603
9837
|
summaryCache;
|
|
9838
|
+
/** Consecutive provider calls with promptCache on that wrote to the cache
|
|
9839
|
+
* without reading from it (prefix changes per call), and consecutive calls
|
|
9840
|
+
* with no cache activity at all (prefix below the model's minimum, which
|
|
9841
|
+
* Anthropic silently ignores). Either reports once per context. */
|
|
9842
|
+
promptCacheColdWrites = 0;
|
|
9843
|
+
promptCacheInactive = 0;
|
|
9844
|
+
promptCacheWarned = false;
|
|
9604
9845
|
workflowName;
|
|
9605
9846
|
mcpManager;
|
|
9606
9847
|
spanManager;
|
|
@@ -10133,6 +10374,17 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10133
10374
|
const costAfter2 = this.budgetContext?.totalCost ?? 0;
|
|
10134
10375
|
span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
|
|
10135
10376
|
span.setAttribute("axl.agent.duration", Date.now() - askStart);
|
|
10377
|
+
if (usageCapture.timing) {
|
|
10378
|
+
const t = usageCapture.timing;
|
|
10379
|
+
span.setAttribute("axl.agent.queued_ms", t.queuedMs);
|
|
10380
|
+
span.setAttribute("axl.agent.retry_ms", t.retryMs);
|
|
10381
|
+
span.setAttribute("axl.agent.attempts", t.attempts);
|
|
10382
|
+
span.setAttribute("axl.agent.ttfb_ms", t.ttfbMs);
|
|
10383
|
+
span.setAttribute("axl.agent.wire_ms", t.wireMs);
|
|
10384
|
+
if (t.firstTokenMs !== void 0) {
|
|
10385
|
+
span.setAttribute("axl.agent.first_token_ms", t.firstTokenMs);
|
|
10386
|
+
}
|
|
10387
|
+
}
|
|
10136
10388
|
if (usageCapture.value) {
|
|
10137
10389
|
span.setAttribute(
|
|
10138
10390
|
"axl.agent.prompt_tokens",
|
|
@@ -10340,6 +10592,14 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10340
10592
|
const toolTokens = toolDefs.length > 0 ? estimateTokens(JSON.stringify(toolDefs)) : 0;
|
|
10341
10593
|
const overhead = systemTokens + toolTokens + reserveTokens;
|
|
10342
10594
|
const availableForHistory = maxContext - overhead;
|
|
10595
|
+
if (availableForHistory <= 0) {
|
|
10596
|
+
this.emitEvent({
|
|
10597
|
+
type: "log",
|
|
10598
|
+
data: {
|
|
10599
|
+
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.`
|
|
10600
|
+
}
|
|
10601
|
+
});
|
|
10602
|
+
}
|
|
10343
10603
|
const historyEstimate = estimateMessagesTokens(sessionHistory);
|
|
10344
10604
|
if (historyEstimate.unmeasured) {
|
|
10345
10605
|
this.emitEvent({
|
|
@@ -10405,6 +10665,7 @@ ${appendedSchemaText}`;
|
|
|
10405
10665
|
if (handoffContext.length > 0) {
|
|
10406
10666
|
messages.push({
|
|
10407
10667
|
role: "system",
|
|
10668
|
+
origin: "runtime",
|
|
10408
10669
|
content: "The following is the conversation history from the previous agent that handed off to you:"
|
|
10409
10670
|
});
|
|
10410
10671
|
for (const msg of handoffContext) {
|
|
@@ -10454,6 +10715,7 @@ ${appendedSchemaText}`;
|
|
|
10454
10715
|
const maxTurns = agent2._config.maxTurns ?? 25;
|
|
10455
10716
|
const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
|
|
10456
10717
|
const startTime = Date.now();
|
|
10718
|
+
const timingTotals = { turns: 0, queuedMs: 0, retryMs: 0, wireMs: 0 };
|
|
10457
10719
|
const currentMessages = [...messages];
|
|
10458
10720
|
let turns = 0;
|
|
10459
10721
|
let guardrailOutputRetries = 0;
|
|
@@ -10467,8 +10729,22 @@ ${appendedSchemaText}`;
|
|
|
10467
10729
|
let lastStartMaxAttempts = 1;
|
|
10468
10730
|
while (turns < maxTurns) {
|
|
10469
10731
|
this.currentSignal?.throwIfAborted();
|
|
10470
|
-
|
|
10471
|
-
|
|
10732
|
+
const elapsedMs = Date.now() - startTime;
|
|
10733
|
+
if (elapsedMs > timeoutMs) {
|
|
10734
|
+
throw new TimeoutError(
|
|
10735
|
+
"ctx.ask()",
|
|
10736
|
+
timeoutMs,
|
|
10737
|
+
timingTotals.turns > 0 ? {
|
|
10738
|
+
elapsedMs,
|
|
10739
|
+
queuedMs: timingTotals.queuedMs,
|
|
10740
|
+
retryMs: timingTotals.retryMs,
|
|
10741
|
+
wireMs: timingTotals.wireMs,
|
|
10742
|
+
otherMs: Math.max(
|
|
10743
|
+
0,
|
|
10744
|
+
elapsedMs - timingTotals.queuedMs - timingTotals.retryMs - timingTotals.wireMs
|
|
10745
|
+
)
|
|
10746
|
+
} : void 0
|
|
10747
|
+
);
|
|
10472
10748
|
}
|
|
10473
10749
|
turns++;
|
|
10474
10750
|
const isFirstTurn = turns === 1;
|
|
@@ -10508,6 +10784,7 @@ ${appendedSchemaText}`;
|
|
|
10508
10784
|
effort: options?.effort ?? agent2._config.effort,
|
|
10509
10785
|
thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
|
|
10510
10786
|
includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
|
|
10787
|
+
promptCache: options?.promptCache ?? agent2._config.promptCache,
|
|
10511
10788
|
toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
|
|
10512
10789
|
stop: options?.stop ?? agent2._config.stop,
|
|
10513
10790
|
providerOptions,
|
|
@@ -10585,6 +10862,7 @@ ${appendedSchemaText}`;
|
|
|
10585
10862
|
const toolCalls = [];
|
|
10586
10863
|
const toolCallBuffers = /* @__PURE__ */ new Map();
|
|
10587
10864
|
let streamProviderMetadata;
|
|
10865
|
+
let streamTiming;
|
|
10588
10866
|
let thinkingContent = "";
|
|
10589
10867
|
const streamingObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof z.ZodObject;
|
|
10590
10868
|
const currentAttempt = schemaRetries + 1;
|
|
@@ -10636,6 +10914,7 @@ ${appendedSchemaText}`;
|
|
|
10636
10914
|
if (chunk.arguments) buffer.arguments += chunk.arguments;
|
|
10637
10915
|
} else if (chunk.type === "done") {
|
|
10638
10916
|
streamProviderMetadata = chunk.providerMetadata;
|
|
10917
|
+
streamTiming = chunk.timing;
|
|
10639
10918
|
if (chunk.usage) {
|
|
10640
10919
|
response = {
|
|
10641
10920
|
content: content2,
|
|
@@ -10663,6 +10942,9 @@ ${appendedSchemaText}`;
|
|
|
10663
10942
|
if (streamProviderMetadata) {
|
|
10664
10943
|
response.providerMetadata = streamProviderMetadata;
|
|
10665
10944
|
}
|
|
10945
|
+
if (streamTiming) {
|
|
10946
|
+
response.timing = streamTiming;
|
|
10947
|
+
}
|
|
10666
10948
|
if (thinkingContent) {
|
|
10667
10949
|
response.thinking_content = thinkingContent;
|
|
10668
10950
|
}
|
|
@@ -10671,12 +10953,15 @@ ${appendedSchemaText}`;
|
|
|
10671
10953
|
}
|
|
10672
10954
|
} catch (err) {
|
|
10673
10955
|
if (richRequest) this.recordRichFailure(err);
|
|
10956
|
+
const failureTiming = err instanceof ProviderError ? err.timing : void 0;
|
|
10674
10957
|
this.emitEvent({
|
|
10675
10958
|
type: "agent_call_end",
|
|
10676
10959
|
agent: agent2._name,
|
|
10677
10960
|
model: effectiveModelUri,
|
|
10678
10961
|
promptVersion: agent2._config.version,
|
|
10679
10962
|
duration: Date.now() - turnStart,
|
|
10963
|
+
// Top-level beside `duration`, exactly as on the success path.
|
|
10964
|
+
...failureTiming ? { timing: failureTiming } : {},
|
|
10680
10965
|
data: {
|
|
10681
10966
|
response: "",
|
|
10682
10967
|
turn: turns,
|
|
@@ -10693,9 +10978,41 @@ ${appendedSchemaText}`;
|
|
|
10693
10978
|
if (usageCapture && response.usage) {
|
|
10694
10979
|
usageCapture.value = response.usage;
|
|
10695
10980
|
}
|
|
10981
|
+
if (usageCapture && response.timing) {
|
|
10982
|
+
usageCapture.timing = response.timing;
|
|
10983
|
+
}
|
|
10984
|
+
if (chatOptions.promptCache && provider.realizesPromptCache?.(model) === true && response.usage && !this.promptCacheWarned) {
|
|
10985
|
+
const wrote = (response.usage.cache_write_tokens ?? 0) > 0;
|
|
10986
|
+
const read = (response.usage.cached_tokens ?? 0) > 0;
|
|
10987
|
+
if (read) {
|
|
10988
|
+
this.promptCacheColdWrites = 0;
|
|
10989
|
+
this.promptCacheInactive = 0;
|
|
10990
|
+
} else if (wrote) {
|
|
10991
|
+
this.promptCacheColdWrites += 1;
|
|
10992
|
+
this.promptCacheInactive = 0;
|
|
10993
|
+
} else {
|
|
10994
|
+
this.promptCacheInactive += 1;
|
|
10995
|
+
}
|
|
10996
|
+
let warning;
|
|
10997
|
+
if (this.promptCacheColdWrites >= PROMPT_CACHE_COLD_WRITE_WARN_AFTER) {
|
|
10998
|
+
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.`;
|
|
10999
|
+
} else if (this.promptCacheInactive >= PROMPT_CACHE_COLD_WRITE_WARN_AFTER) {
|
|
11000
|
+
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.`;
|
|
11001
|
+
}
|
|
11002
|
+
if (warning) {
|
|
11003
|
+
this.promptCacheWarned = true;
|
|
11004
|
+
this.emitEvent({ type: "log", agent: agent2._name, data: { warning } });
|
|
11005
|
+
}
|
|
11006
|
+
}
|
|
10696
11007
|
if (response.cost) {
|
|
10697
11008
|
this._accumulateBudgetCost(response.cost);
|
|
10698
11009
|
}
|
|
11010
|
+
if (response.timing) {
|
|
11011
|
+
timingTotals.turns += 1;
|
|
11012
|
+
timingTotals.queuedMs += response.timing.queuedMs;
|
|
11013
|
+
timingTotals.retryMs += response.timing.retryMs;
|
|
11014
|
+
timingTotals.wireMs += response.timing.wireMs;
|
|
11015
|
+
}
|
|
10699
11016
|
this.emitEvent({
|
|
10700
11017
|
type: "agent_call_end",
|
|
10701
11018
|
agent: agent2._name,
|
|
@@ -10710,6 +11027,8 @@ ${appendedSchemaText}`;
|
|
|
10710
11027
|
cacheWrite: response.usage.cache_write_tokens
|
|
10711
11028
|
} : void 0,
|
|
10712
11029
|
duration: Date.now() - turnStart,
|
|
11030
|
+
// Additive beside `duration`, which keeps its turn-wall-clock meaning.
|
|
11031
|
+
...response.timing ? { timing: response.timing } : {},
|
|
10713
11032
|
data: {
|
|
10714
11033
|
response: response.content,
|
|
10715
11034
|
...response.thinking_content ? { thinking: response.thinking_content } : {},
|
|
@@ -11428,6 +11747,7 @@ ${appendedSchemaText}`;
|
|
|
11428
11747
|
if (this.summaryCache) {
|
|
11429
11748
|
const summaryMsg2 = {
|
|
11430
11749
|
role: "system",
|
|
11750
|
+
origin: "runtime",
|
|
11431
11751
|
content: `Summary of earlier conversation:
|
|
11432
11752
|
${this.summaryCache}`
|
|
11433
11753
|
};
|
|
@@ -11441,6 +11761,7 @@ ${this.summaryCache}`
|
|
|
11441
11761
|
recentTokens2 += msgTokens;
|
|
11442
11762
|
splitIdx2 = i;
|
|
11443
11763
|
}
|
|
11764
|
+
splitIdx2 = nextUserTurn(history, splitIdx2) ?? history.length;
|
|
11444
11765
|
if (splitIdx2 < history.length) {
|
|
11445
11766
|
return [summaryMsg2, ...history.slice(splitIdx2)];
|
|
11446
11767
|
}
|
|
@@ -11454,6 +11775,19 @@ ${this.summaryCache}`
|
|
|
11454
11775
|
recentTokens += msgTokens;
|
|
11455
11776
|
splitIdx = i;
|
|
11456
11777
|
}
|
|
11778
|
+
const anchored = nextUserTurn(history, splitIdx);
|
|
11779
|
+
if (anchored !== void 0) {
|
|
11780
|
+
splitIdx = anchored;
|
|
11781
|
+
} else if (splitIdx === history.length && history.length > 1) {
|
|
11782
|
+
for (let i = history.length - 1; i > 0; i--) {
|
|
11783
|
+
if (history[i].role === "user") {
|
|
11784
|
+
splitIdx = i;
|
|
11785
|
+
break;
|
|
11786
|
+
}
|
|
11787
|
+
}
|
|
11788
|
+
} else {
|
|
11789
|
+
splitIdx = history.length;
|
|
11790
|
+
}
|
|
11457
11791
|
if (splitIdx === 0) return history;
|
|
11458
11792
|
const oldMessages = history.slice(0, splitIdx);
|
|
11459
11793
|
const summaryModelUri = this.config.contextManagement?.summaryModel;
|
|
@@ -11485,6 +11819,7 @@ ${this.summaryCache}`
|
|
|
11485
11819
|
}
|
|
11486
11820
|
const summaryMsg = {
|
|
11487
11821
|
role: "system",
|
|
11822
|
+
origin: "runtime",
|
|
11488
11823
|
content: `Summary of earlier conversation:
|
|
11489
11824
|
${summaryResponse.content}`
|
|
11490
11825
|
};
|
|
@@ -16260,6 +16595,8 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
16260
16595
|
parent: parentScope
|
|
16261
16596
|
};
|
|
16262
16597
|
const modelCalls = /* @__PURE__ */ new Map();
|
|
16598
|
+
const modelTiming = /* @__PURE__ */ new Map();
|
|
16599
|
+
const captureTimingSamples = options?.captureTimingSamples === true;
|
|
16263
16600
|
const workflowCalls = /* @__PURE__ */ new Map();
|
|
16264
16601
|
const tokens = { input: 0, output: 0, reasoning: 0 };
|
|
16265
16602
|
let agentCalls = 0;
|
|
@@ -16277,6 +16614,30 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
16277
16614
|
tokens.output += event.tokens.output ?? 0;
|
|
16278
16615
|
tokens.reasoning += event.tokens.reasoning ?? 0;
|
|
16279
16616
|
}
|
|
16617
|
+
const failed2 = event.data?.error != null;
|
|
16618
|
+
if (event.model && event.timing && !failed2) {
|
|
16619
|
+
const t = event.timing;
|
|
16620
|
+
let bucket = modelTiming.get(event.model);
|
|
16621
|
+
if (!bucket) {
|
|
16622
|
+
bucket = {
|
|
16623
|
+
calls: 0,
|
|
16624
|
+
queuedMs: 0,
|
|
16625
|
+
retryMs: 0,
|
|
16626
|
+
wireMs: 0,
|
|
16627
|
+
...captureTimingSamples ? { samples: [] } : {}
|
|
16628
|
+
};
|
|
16629
|
+
modelTiming.set(event.model, bucket);
|
|
16630
|
+
}
|
|
16631
|
+
bucket.calls++;
|
|
16632
|
+
bucket.samples?.push(t);
|
|
16633
|
+
bucket.queuedMs += t.queuedMs;
|
|
16634
|
+
bucket.retryMs += t.retryMs;
|
|
16635
|
+
bucket.wireMs += t.wireMs;
|
|
16636
|
+
if (t.firstTokenMs != null) {
|
|
16637
|
+
bucket.firstTokenMs = (bucket.firstTokenMs ?? 0) + t.firstTokenMs;
|
|
16638
|
+
bucket.firstTokenCalls = (bucket.firstTokenCalls ?? 0) + 1;
|
|
16639
|
+
}
|
|
16640
|
+
}
|
|
16280
16641
|
}
|
|
16281
16642
|
if (event.type === "workflow_start" && event.workflow) {
|
|
16282
16643
|
workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
|
|
@@ -16307,6 +16668,7 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
16307
16668
|
result,
|
|
16308
16669
|
cost: scope.totalCost,
|
|
16309
16670
|
unpriced,
|
|
16671
|
+
...modelTiming.size > 0 ? { modelTiming: Object.fromEntries(modelTiming) } : {},
|
|
16310
16672
|
...capturedTraces ? { traces: capturedTraces } : {},
|
|
16311
16673
|
metadata: {
|
|
16312
16674
|
models: [...modelCalls.keys()],
|