@axlsdk/axl 0.22.2 → 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/README.md +1 -1
- package/dist/index.cjs +795 -204
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +365 -10
- package/dist/index.d.ts +365 -10
- package/dist/index.js +795 -204
- 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
|
},
|
|
@@ -2340,17 +2476,22 @@ function resolveOpenAIReasoningEffort(model, resolved) {
|
|
|
2340
2476
|
}
|
|
2341
2477
|
return resolved.thinkingDisabled ? clampReasoningEffort(model, "none") : void 0;
|
|
2342
2478
|
}
|
|
2343
|
-
var warnedChatMaxDowngrades = /* @__PURE__ */ new Set();
|
|
2344
2479
|
function resolveOpenAIChatReasoningEffort(model, resolved) {
|
|
2345
2480
|
const effort = resolveOpenAIReasoningEffort(model, resolved);
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
return
|
|
2481
|
+
return effort === "max" ? "xhigh" : effort;
|
|
2482
|
+
}
|
|
2483
|
+
function resolveOpenAIEffortResolution(options, resolveWireEffort, endpoint) {
|
|
2484
|
+
const resolved = resolveThinkingOptions(options);
|
|
2485
|
+
if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
|
|
2486
|
+
const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
|
|
2487
|
+
const effective = resolveWireEffort(model, resolved);
|
|
2488
|
+
if (effective === void 0 || effective === resolved.effort) return void 0;
|
|
2489
|
+
return {
|
|
2490
|
+
requested: resolved.effort,
|
|
2491
|
+
effective,
|
|
2492
|
+
clamped: true,
|
|
2493
|
+
cause: `${endpoint} does not accept reasoning effort '${resolved.effort}' for ${model}; using '${effective}'`
|
|
2494
|
+
};
|
|
2354
2495
|
}
|
|
2355
2496
|
var openaiReasoningEmit = (body, resolved, model) => {
|
|
2356
2497
|
const oSeries = isOSeriesModel(model);
|
|
@@ -2375,6 +2516,15 @@ var OpenAIProvider = class extends OpenAICompatibleProvider {
|
|
|
2375
2516
|
constructor(options = {}) {
|
|
2376
2517
|
super({ profile: OPENAI_PROFILE, ...options });
|
|
2377
2518
|
}
|
|
2519
|
+
/** Report a clamped `effort` — Chat Completions caps `'max'` at `'xhigh'` and
|
|
2520
|
+
* rejects `'none'` before gpt-5.1 (clamped to `'minimal'`). */
|
|
2521
|
+
effortResolution(options) {
|
|
2522
|
+
return resolveOpenAIEffortResolution(
|
|
2523
|
+
options,
|
|
2524
|
+
resolveOpenAIChatReasoningEffort,
|
|
2525
|
+
"OpenAI Chat Completions"
|
|
2526
|
+
);
|
|
2527
|
+
}
|
|
2378
2528
|
computeCost(model, usage, _reportedCost, context) {
|
|
2379
2529
|
return usage ? estimateDirectOpenAICost(model, usage, { ...context, baseUrl: this.baseUrl }) : void 0;
|
|
2380
2530
|
}
|
|
@@ -2421,6 +2571,12 @@ var OpenAIResponsesProvider = class {
|
|
|
2421
2571
|
inputCapabilities(model) {
|
|
2422
2572
|
return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
|
|
2423
2573
|
}
|
|
2574
|
+
/** Report a clamped `effort`. Responses accepts the native `'max'` tier that
|
|
2575
|
+
* Chat Completions caps, but still clamps `'none'` before gpt-5.1, `'xhigh'`
|
|
2576
|
+
* below gpt-5.2, and everything to `'high'` on gpt-5-pro. */
|
|
2577
|
+
effortResolution(options) {
|
|
2578
|
+
return resolveOpenAIEffortResolution(options, resolveOpenAIReasoningEffort, "OpenAI Responses");
|
|
2579
|
+
}
|
|
2424
2580
|
validateInput(request) {
|
|
2425
2581
|
const modelOverride = request.providerOptions?.model;
|
|
2426
2582
|
const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
|
|
@@ -2498,6 +2654,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2498
2654
|
async chat(messages, options) {
|
|
2499
2655
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
2500
2656
|
const body = this.buildRequestBody(messages, options, false);
|
|
2657
|
+
const recorder = new CallTimingRecorder();
|
|
2501
2658
|
const res = await fetchWithRetry(
|
|
2502
2659
|
`${this.baseUrl}/responses`,
|
|
2503
2660
|
{
|
|
@@ -2506,7 +2663,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2506
2663
|
body: JSON.stringify(body),
|
|
2507
2664
|
signal: options.signal
|
|
2508
2665
|
},
|
|
2509
|
-
{ governor: this.governor, provider: this.name }
|
|
2666
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
2510
2667
|
);
|
|
2511
2668
|
if (!res.ok) {
|
|
2512
2669
|
const errorBody = await res.text();
|
|
@@ -2516,11 +2673,15 @@ var OpenAIResponsesProvider = class {
|
|
|
2516
2673
|
status: res.status,
|
|
2517
2674
|
headers: res.headers,
|
|
2518
2675
|
message,
|
|
2519
|
-
body: errorBody
|
|
2676
|
+
body: errorBody,
|
|
2677
|
+
timing: recorder.chatTiming()
|
|
2520
2678
|
});
|
|
2521
2679
|
}
|
|
2522
2680
|
const json = await res.json();
|
|
2523
|
-
return
|
|
2681
|
+
return withChatTiming(
|
|
2682
|
+
recorder,
|
|
2683
|
+
this.parseResponse(json, this.requestModel(body, options.model), body)
|
|
2684
|
+
);
|
|
2524
2685
|
}
|
|
2525
2686
|
// ---------------------------------------------------------------------------
|
|
2526
2687
|
// stream - SSE streaming
|
|
@@ -2528,6 +2689,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2528
2689
|
async *stream(messages, options) {
|
|
2529
2690
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
2530
2691
|
const body = this.buildRequestBody(messages, options, true);
|
|
2692
|
+
const recorder = new CallTimingRecorder();
|
|
2531
2693
|
const res = await fetchWithRetry(
|
|
2532
2694
|
`${this.baseUrl}/responses`,
|
|
2533
2695
|
{
|
|
@@ -2536,7 +2698,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2536
2698
|
body: JSON.stringify(body),
|
|
2537
2699
|
signal: options.signal
|
|
2538
2700
|
},
|
|
2539
|
-
{ governor: this.governor, provider: this.name }
|
|
2701
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
2540
2702
|
);
|
|
2541
2703
|
if (!res.ok) {
|
|
2542
2704
|
const errorBody = await res.text();
|
|
@@ -2546,13 +2708,17 @@ var OpenAIResponsesProvider = class {
|
|
|
2546
2708
|
status: res.status,
|
|
2547
2709
|
headers: res.headers,
|
|
2548
2710
|
message,
|
|
2549
|
-
body: errorBody
|
|
2711
|
+
body: errorBody,
|
|
2712
|
+
timing: recorder.chatTiming()
|
|
2550
2713
|
});
|
|
2551
2714
|
}
|
|
2552
2715
|
if (!res.body) {
|
|
2553
2716
|
throw new Error("OpenAI Responses stream has no body");
|
|
2554
2717
|
}
|
|
2555
|
-
yield*
|
|
2718
|
+
yield* withCallTiming(
|
|
2719
|
+
recorder,
|
|
2720
|
+
this.parseSSEStream(res.body, this.requestModel(body, options.model), body, recorder)
|
|
2721
|
+
);
|
|
2556
2722
|
}
|
|
2557
2723
|
// ---------------------------------------------------------------------------
|
|
2558
2724
|
// Internal: build request body
|
|
@@ -2754,7 +2920,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2754
2920
|
// ---------------------------------------------------------------------------
|
|
2755
2921
|
// Internal: parse SSE stream
|
|
2756
2922
|
// ---------------------------------------------------------------------------
|
|
2757
|
-
async *parseSSEStream(body, model, request) {
|
|
2923
|
+
async *parseSSEStream(body, model, request, timing) {
|
|
2758
2924
|
const reader = body.getReader();
|
|
2759
2925
|
const decoder = new TextDecoder();
|
|
2760
2926
|
let buffer = "";
|
|
@@ -2762,7 +2928,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2762
2928
|
let eventType = "";
|
|
2763
2929
|
try {
|
|
2764
2930
|
while (true) {
|
|
2765
|
-
const { done, value } = await
|
|
2931
|
+
const { done, value } = await timing.read(reader);
|
|
2766
2932
|
if (done) break;
|
|
2767
2933
|
buffer += decoder.decode(value, { stream: true });
|
|
2768
2934
|
const lines = buffer.split("\n");
|
|
@@ -2782,7 +2948,14 @@ var OpenAIResponsesProvider = class {
|
|
|
2782
2948
|
} catch {
|
|
2783
2949
|
continue;
|
|
2784
2950
|
}
|
|
2785
|
-
const chunk = this.handleStreamEvent(
|
|
2951
|
+
const chunk = this.handleStreamEvent(
|
|
2952
|
+
eventType,
|
|
2953
|
+
data,
|
|
2954
|
+
model,
|
|
2955
|
+
callIdMap,
|
|
2956
|
+
request,
|
|
2957
|
+
timing
|
|
2958
|
+
);
|
|
2786
2959
|
if (chunk) {
|
|
2787
2960
|
yield chunk;
|
|
2788
2961
|
if (chunk.type === "done") return;
|
|
@@ -2796,7 +2969,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2796
2969
|
reader.releaseLock();
|
|
2797
2970
|
}
|
|
2798
2971
|
}
|
|
2799
|
-
handleStreamEvent(eventType, data, model, callIdMap, request) {
|
|
2972
|
+
handleStreamEvent(eventType, data, model, callIdMap, request, timing) {
|
|
2800
2973
|
switch (eventType) {
|
|
2801
2974
|
case "response.output_text.delta":
|
|
2802
2975
|
return { type: "text_delta", content: data.delta ?? "" };
|
|
@@ -2852,7 +3025,8 @@ var OpenAIResponsesProvider = class {
|
|
|
2852
3025
|
provider: this.name,
|
|
2853
3026
|
status: 0,
|
|
2854
3027
|
retryable: false,
|
|
2855
|
-
message: `OpenAI Responses API error: ${errorMsg}
|
|
3028
|
+
message: `OpenAI Responses API error: ${errorMsg}`,
|
|
3029
|
+
timing: timing.streamTiming()
|
|
2856
3030
|
});
|
|
2857
3031
|
}
|
|
2858
3032
|
default:
|
|
@@ -2920,6 +3094,12 @@ function hasAnthropicProviderFileImage(messages) {
|
|
|
2920
3094
|
);
|
|
2921
3095
|
}
|
|
2922
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
|
+
},
|
|
2923
3103
|
"claude-fable-5": {
|
|
2924
3104
|
thinking: "adaptive-always-on",
|
|
2925
3105
|
effortLevels: ["low", "medium", "high", "xhigh", "max"],
|
|
@@ -3010,10 +3190,18 @@ var PRE_INFERENCE_GEO_MODELS = /* @__PURE__ */ new Set([
|
|
|
3010
3190
|
function resolveClaudeCapability(model) {
|
|
3011
3191
|
return CLAUDE_CAPABILITIES[model] ?? (LEGACY_CLAUDE_MODELS.has(model) ? LEGACY_CLAUDE_CAPABILITY : void 0);
|
|
3012
3192
|
}
|
|
3193
|
+
var DEFAULT_CACHE_READ_MULTIPLIER = 0.1;
|
|
3013
3194
|
var ANTHROPIC_RATES = {
|
|
3195
|
+
"claude-fable-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
|
|
3014
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 },
|
|
3015
3203
|
"claude-opus-5": { input: 5e-6, output: 25e-6 },
|
|
3016
|
-
"claude-sonnet-5": { input:
|
|
3204
|
+
"claude-sonnet-5": { input: 2e-6, output: 1e-5 },
|
|
3017
3205
|
"claude-opus-4-8": { input: 5e-6, output: 25e-6 },
|
|
3018
3206
|
"claude-opus-4-7": { input: 5e-6, output: 25e-6 },
|
|
3019
3207
|
"claude-opus-4-6": { input: 5e-6, output: 25e-6 },
|
|
@@ -3052,7 +3240,7 @@ function anthropicStreamErrorStatus(type) {
|
|
|
3052
3240
|
return 0;
|
|
3053
3241
|
}
|
|
3054
3242
|
}
|
|
3055
|
-
function estimateAnthropicCost(model, usage
|
|
3243
|
+
function estimateAnthropicCost(model, usage) {
|
|
3056
3244
|
if (!model || !isValidTokenCount(usage.inputTokens) || !isValidTokenCount(usage.outputTokens)) {
|
|
3057
3245
|
return void 0;
|
|
3058
3246
|
}
|
|
@@ -3071,8 +3259,9 @@ function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
|
|
|
3071
3259
|
if (usage.aggregateCacheWriteTokens !== void 0 && (!isValidTokenCount(usage.aggregateCacheWriteTokens) || usage.aggregateCacheWriteTokens !== cacheWriteTotal)) {
|
|
3072
3260
|
return void 0;
|
|
3073
3261
|
}
|
|
3074
|
-
const { input, output } = rate
|
|
3075
|
-
|
|
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;
|
|
3076
3265
|
}
|
|
3077
3266
|
var THINKING_BUDGETS = {
|
|
3078
3267
|
low: 1024,
|
|
@@ -3096,14 +3285,6 @@ function budgetToClaudeEffort(budget) {
|
|
|
3096
3285
|
if (budget <= THINKING_BUDGETS.xhigh) return "xhigh";
|
|
3097
3286
|
return "max";
|
|
3098
3287
|
}
|
|
3099
|
-
var warnedFableDisable = /* @__PURE__ */ new Set();
|
|
3100
|
-
function warnFableDisable(model) {
|
|
3101
|
-
if (warnedFableDisable.has(model)) return;
|
|
3102
|
-
warnedFableDisable.add(model);
|
|
3103
|
-
console.warn(
|
|
3104
|
-
`[axl] thinking cannot be disabled on Anthropic ${model}; using adaptive thinking at effort 'low'.`
|
|
3105
|
-
);
|
|
3106
|
-
}
|
|
3107
3288
|
function resolveClaudeThinking(model, resolved) {
|
|
3108
3289
|
const capability = resolveClaudeCapability(model);
|
|
3109
3290
|
if (!capability) return { stripTemperature: false };
|
|
@@ -3123,11 +3304,14 @@ function resolveClaudeThinking(model, resolved) {
|
|
|
3123
3304
|
}
|
|
3124
3305
|
if (resolved.thinkingDisabled) {
|
|
3125
3306
|
if (capability.thinking === "adaptive-always-on") {
|
|
3126
|
-
warnFableDisable(model);
|
|
3127
3307
|
return {
|
|
3128
3308
|
thinking: { type: "adaptive" },
|
|
3129
3309
|
outputConfig: { effort: "low" },
|
|
3130
|
-
stripTemperature: true
|
|
3310
|
+
stripTemperature: true,
|
|
3311
|
+
clamp: {
|
|
3312
|
+
effective: "low",
|
|
3313
|
+
cause: `thinking cannot be disabled on Anthropic ${model}; using adaptive thinking at effort 'low'`
|
|
3314
|
+
}
|
|
3131
3315
|
};
|
|
3132
3316
|
}
|
|
3133
3317
|
if (activeEffort && capability.disableAt && !isEffortAtOrBelow(activeEffort, capability.disableAt)) {
|
|
@@ -3189,7 +3373,16 @@ function resolveClaudeThinking(model, resolved) {
|
|
|
3189
3373
|
return {
|
|
3190
3374
|
thinking: { type: "enabled", budget_tokens: budget },
|
|
3191
3375
|
manualBudget: budget,
|
|
3192
|
-
stripTemperature: true
|
|
3376
|
+
stripTemperature: true,
|
|
3377
|
+
// Legacy models take `budget_tokens`, not an effort level, and have no
|
|
3378
|
+
// `effortLevels` — so `clampAnthropicEffort` folds 'max'/'xhigh' down to
|
|
3379
|
+
// the 'high' tier and the request silently carries that tier's budget.
|
|
3380
|
+
// Report it: an unreported budget-tier downgrade is exactly the silent
|
|
3381
|
+
// clamp this capability exists to expose.
|
|
3382
|
+
clamp: activeEffort === resolved.activeEffort ? void 0 : {
|
|
3383
|
+
effective: activeEffort,
|
|
3384
|
+
cause: `Anthropic ${model} takes thinking budget_tokens rather than an effort level, and its ceiling tier is '${activeEffort}'; effort '${resolved.activeEffort}' sends budget_tokens ${budget}`
|
|
3385
|
+
}
|
|
3193
3386
|
};
|
|
3194
3387
|
}
|
|
3195
3388
|
return { stripTemperature: capability.stripTemperature ?? false };
|
|
@@ -3300,6 +3493,34 @@ function mergeAnthropicUsage(initial, update) {
|
|
|
3300
3493
|
}
|
|
3301
3494
|
var AnthropicProvider = class {
|
|
3302
3495
|
name = "anthropic";
|
|
3496
|
+
/** Report a clamped `effort`. Anthropic clamps to each model's supported
|
|
3497
|
+
* `output_config.effort` levels, and models that always think fall back to
|
|
3498
|
+
* adaptive `'low'` when asked to disable thinking. Only reported when the
|
|
3499
|
+
* effective level is what the request body actually carries; an explicit
|
|
3500
|
+
* `thinkingBudget` is documented to override `effort`, so it is not a clamp. */
|
|
3501
|
+
effortResolution(options) {
|
|
3502
|
+
const resolved = resolveThinkingOptions(options);
|
|
3503
|
+
if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
|
|
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
|
+
}
|
|
3513
|
+
const config = resolveClaudeThinking(model, resolved);
|
|
3514
|
+
const wireEffort = typeof config.outputConfig?.effort === "string" ? config.outputConfig.effort : void 0;
|
|
3515
|
+
const effective = config.clamp?.effective ?? wireEffort;
|
|
3516
|
+
if (effective === void 0 || effective === resolved.effort) return void 0;
|
|
3517
|
+
return {
|
|
3518
|
+
requested: resolved.effort,
|
|
3519
|
+
effective,
|
|
3520
|
+
clamped: true,
|
|
3521
|
+
cause: config.clamp?.cause ?? `Anthropic ${model} does not support effort '${resolved.effort}'; using '${effective}'`
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3303
3524
|
inputCapabilities(model) {
|
|
3304
3525
|
return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
|
|
3305
3526
|
}
|
|
@@ -3344,6 +3565,10 @@ var AnthropicProvider = class {
|
|
|
3344
3565
|
}
|
|
3345
3566
|
/** Anthropic ignores native `json_schema` structurally — Axl uses a system
|
|
3346
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
|
+
}
|
|
3347
3572
|
nativeStructuredOutputSupport() {
|
|
3348
3573
|
return "unsupported";
|
|
3349
3574
|
}
|
|
@@ -3385,6 +3610,7 @@ var AnthropicProvider = class {
|
|
|
3385
3610
|
);
|
|
3386
3611
|
const body = this.buildRequestBody(messages, options, false);
|
|
3387
3612
|
const pricingContext = pricingContextFromBody(body);
|
|
3613
|
+
const recorder = new CallTimingRecorder();
|
|
3388
3614
|
const res = await fetchWithRetry(
|
|
3389
3615
|
`${this.baseUrl}/messages`,
|
|
3390
3616
|
{
|
|
@@ -3393,7 +3619,7 @@ var AnthropicProvider = class {
|
|
|
3393
3619
|
body: JSON.stringify(body),
|
|
3394
3620
|
signal: options.signal
|
|
3395
3621
|
},
|
|
3396
|
-
{ governor: this.governor, provider: this.name }
|
|
3622
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
3397
3623
|
);
|
|
3398
3624
|
if (!res.ok) {
|
|
3399
3625
|
const errorBody = await res.text();
|
|
@@ -3403,11 +3629,12 @@ var AnthropicProvider = class {
|
|
|
3403
3629
|
status: res.status,
|
|
3404
3630
|
headers: res.headers,
|
|
3405
3631
|
message,
|
|
3406
|
-
body: errorBody
|
|
3632
|
+
body: errorBody,
|
|
3633
|
+
timing: recorder.chatTiming()
|
|
3407
3634
|
});
|
|
3408
3635
|
}
|
|
3409
3636
|
const json = await res.json();
|
|
3410
|
-
return this.parseResponse(json, pricingContext);
|
|
3637
|
+
return withChatTiming(recorder, this.parseResponse(json, pricingContext));
|
|
3411
3638
|
}
|
|
3412
3639
|
// ---------------------------------------------------------------------------
|
|
3413
3640
|
// stream - SSE streaming completion
|
|
@@ -3419,6 +3646,7 @@ var AnthropicProvider = class {
|
|
|
3419
3646
|
);
|
|
3420
3647
|
const body = this.buildRequestBody(messages, options, true);
|
|
3421
3648
|
const pricingContext = pricingContextFromBody(body);
|
|
3649
|
+
const recorder = new CallTimingRecorder();
|
|
3422
3650
|
const res = await fetchWithRetry(
|
|
3423
3651
|
`${this.baseUrl}/messages`,
|
|
3424
3652
|
{
|
|
@@ -3427,7 +3655,7 @@ var AnthropicProvider = class {
|
|
|
3427
3655
|
body: JSON.stringify(body),
|
|
3428
3656
|
signal: options.signal
|
|
3429
3657
|
},
|
|
3430
|
-
{ governor: this.governor, provider: this.name }
|
|
3658
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
3431
3659
|
);
|
|
3432
3660
|
if (!res.ok) {
|
|
3433
3661
|
const errorBody = await res.text();
|
|
@@ -3437,13 +3665,17 @@ var AnthropicProvider = class {
|
|
|
3437
3665
|
status: res.status,
|
|
3438
3666
|
headers: res.headers,
|
|
3439
3667
|
message,
|
|
3440
|
-
body: errorBody
|
|
3668
|
+
body: errorBody,
|
|
3669
|
+
timing: recorder.chatTiming()
|
|
3441
3670
|
});
|
|
3442
3671
|
}
|
|
3443
3672
|
if (!res.body) {
|
|
3444
3673
|
throw new Error("Anthropic stream response has no body");
|
|
3445
3674
|
}
|
|
3446
|
-
yield*
|
|
3675
|
+
yield* withCallTiming(
|
|
3676
|
+
recorder,
|
|
3677
|
+
this.parseSSEStream(res.body, pricingContext, res.headers, recorder)
|
|
3678
|
+
);
|
|
3447
3679
|
}
|
|
3448
3680
|
// ---------------------------------------------------------------------------
|
|
3449
3681
|
// Internal: request building
|
|
@@ -3478,14 +3710,31 @@ var AnthropicProvider = class {
|
|
|
3478
3710
|
max_tokens: options.maxTokens ?? 4096,
|
|
3479
3711
|
stream
|
|
3480
3712
|
};
|
|
3481
|
-
|
|
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) {
|
|
3482
3724
|
body.system = systemText;
|
|
3483
3725
|
}
|
|
3484
3726
|
if (options.stop) {
|
|
3485
3727
|
body.stop_sequences = options.stop;
|
|
3486
3728
|
}
|
|
3487
3729
|
if (options.tools && options.tools.length > 0) {
|
|
3488
|
-
|
|
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;
|
|
3489
3738
|
}
|
|
3490
3739
|
if (options.toolChoice !== void 0) {
|
|
3491
3740
|
body.tool_choice = this.mapToolChoice(options.toolChoice);
|
|
@@ -3504,9 +3753,13 @@ var AnthropicProvider = class {
|
|
|
3504
3753
|
}
|
|
3505
3754
|
if (options.responseFormat && options.responseFormat.type !== "text") {
|
|
3506
3755
|
const jsonInstruction = "You must respond with valid JSON only. No markdown fences, no extra text.";
|
|
3507
|
-
|
|
3756
|
+
if (Array.isArray(body.system)) {
|
|
3757
|
+
body.system = [...body.system, { type: "text", text: jsonInstruction }];
|
|
3758
|
+
} else {
|
|
3759
|
+
body.system = body.system ? `${body.system}
|
|
3508
3760
|
|
|
3509
3761
|
${jsonInstruction}` : jsonInstruction;
|
|
3762
|
+
}
|
|
3510
3763
|
}
|
|
3511
3764
|
if (options.providerOptions) {
|
|
3512
3765
|
Object.assign(body, options.providerOptions);
|
|
@@ -3686,7 +3939,7 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3686
3939
|
// ---------------------------------------------------------------------------
|
|
3687
3940
|
// Internal: SSE stream parsing
|
|
3688
3941
|
// ---------------------------------------------------------------------------
|
|
3689
|
-
async *parseSSEStream(body, pricingContext, responseHeaders) {
|
|
3942
|
+
async *parseSSEStream(body, pricingContext, responseHeaders, timing) {
|
|
3690
3943
|
const reader = body.getReader();
|
|
3691
3944
|
const decoder = new TextDecoder();
|
|
3692
3945
|
let buffer = "";
|
|
@@ -3715,7 +3968,7 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3715
3968
|
});
|
|
3716
3969
|
try {
|
|
3717
3970
|
while (true) {
|
|
3718
|
-
const { done, value } = await
|
|
3971
|
+
const { done, value } = await timing.read(reader);
|
|
3719
3972
|
if (done) break;
|
|
3720
3973
|
buffer += decoder.decode(value, { stream: true });
|
|
3721
3974
|
const lines = buffer.split("\n");
|
|
@@ -3738,7 +3991,8 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3738
3991
|
provider: this.name,
|
|
3739
3992
|
status,
|
|
3740
3993
|
headers: responseHeaders,
|
|
3741
|
-
message: event.error?.message ?? "Anthropic stream failed"
|
|
3994
|
+
message: event.error?.message ?? "Anthropic stream failed",
|
|
3995
|
+
timing: timing.streamTiming()
|
|
3742
3996
|
});
|
|
3743
3997
|
}
|
|
3744
3998
|
case "content_block_start": {
|
|
@@ -3974,7 +4228,6 @@ var GEMINI_PRICING = {
|
|
|
3974
4228
|
},
|
|
3975
4229
|
"gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7 },
|
|
3976
4230
|
"gemini-2.5-flash-lite": { input: 1e-7, cached: 1e-8, output: 4e-7 },
|
|
3977
|
-
"gemini-3-flash-preview": { input: 5e-7, cached: 5e-8, output: 3e-6 },
|
|
3978
4231
|
"gemini-3.1-pro-preview": {
|
|
3979
4232
|
input: 2e-6,
|
|
3980
4233
|
cached: 2e-7,
|
|
@@ -3990,7 +4243,9 @@ var GEMINI_PRICING = {
|
|
|
3990
4243
|
"gemini-3.1-flash-lite": { input: 25e-8, cached: 25e-9, output: 15e-7 },
|
|
3991
4244
|
"gemini-3.5-flash": { input: 15e-7, cached: 15e-8, output: 9e-6 },
|
|
3992
4245
|
"gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
|
|
3993
|
-
"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 }
|
|
3994
4249
|
};
|
|
3995
4250
|
function isValidTokenCount2(value) {
|
|
3996
4251
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
@@ -4150,13 +4405,71 @@ function budgetToThinkingLevel(budgetTokens) {
|
|
|
4150
4405
|
function minThinkingLevel(model) {
|
|
4151
4406
|
return GEMINI_3X_MIN_THINKING_LEVEL.get(model) ?? "minimal";
|
|
4152
4407
|
}
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
if (
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4408
|
+
function resolveGeminiThinking(model, resolved) {
|
|
4409
|
+
const is3x = isGemini3x(model);
|
|
4410
|
+
if (resolved.thinkingDisabled) {
|
|
4411
|
+
const minLevel = minThinkingLevel(model);
|
|
4412
|
+
if (is3x) {
|
|
4413
|
+
return {
|
|
4414
|
+
interactionLevel: minLevel,
|
|
4415
|
+
thinkingLevel: minLevel,
|
|
4416
|
+
thinkingBudget: void 0,
|
|
4417
|
+
attachThoughts: false,
|
|
4418
|
+
// A `thinkingBudget: 0` disable has no unified effort to report against;
|
|
4419
|
+
// only an explicit `effort: 'none'` is a clamp of a requested value.
|
|
4420
|
+
clamp: resolved.effort === "none" ? {
|
|
4421
|
+
effective: minLevel,
|
|
4422
|
+
cause: `Gemini 3.x models cannot disable thinking; using the model minimum thinking level '${minLevel}'`
|
|
4423
|
+
} : void 0
|
|
4424
|
+
};
|
|
4425
|
+
}
|
|
4426
|
+
return {
|
|
4427
|
+
interactionLevel: "minimal",
|
|
4428
|
+
thinkingLevel: void 0,
|
|
4429
|
+
thinkingBudget: 0,
|
|
4430
|
+
attachThoughts: false
|
|
4431
|
+
};
|
|
4432
|
+
}
|
|
4433
|
+
if (resolved.hasBudgetOverride) {
|
|
4434
|
+
const budget = resolved.thinkingBudget;
|
|
4435
|
+
const level = budgetToThinkingLevel(budget);
|
|
4436
|
+
return {
|
|
4437
|
+
interactionLevel: level,
|
|
4438
|
+
thinkingLevel: is3x ? level : void 0,
|
|
4439
|
+
thinkingBudget: is3x ? void 0 : budget,
|
|
4440
|
+
attachThoughts: resolved.includeThoughts
|
|
4441
|
+
};
|
|
4442
|
+
}
|
|
4443
|
+
if (resolved.activeEffort) {
|
|
4444
|
+
const effort = resolved.activeEffort;
|
|
4445
|
+
const budget = THINKING_BUDGETS2[effort] ?? 5e3;
|
|
4446
|
+
if (is3x) {
|
|
4447
|
+
const level = THINKING_LEVELS[effort] ?? "medium";
|
|
4448
|
+
return {
|
|
4449
|
+
interactionLevel: level,
|
|
4450
|
+
thinkingLevel: level,
|
|
4451
|
+
thinkingBudget: void 0,
|
|
4452
|
+
attachThoughts: resolved.includeThoughts,
|
|
4453
|
+
clamp: level === effort ? void 0 : {
|
|
4454
|
+
effective: level,
|
|
4455
|
+
cause: `Gemini 3.x thinking levels cap at 'high'; effort '${effort}' maps to '${level}'`
|
|
4456
|
+
}
|
|
4457
|
+
};
|
|
4458
|
+
}
|
|
4459
|
+
return {
|
|
4460
|
+
interactionLevel: budgetToThinkingLevel(budget),
|
|
4461
|
+
thinkingLevel: void 0,
|
|
4462
|
+
// 2.5 Pro supports a higher max budget (32768) than other 2.5 models (24576).
|
|
4463
|
+
thinkingBudget: effort === "max" && model === "gemini-2.5-pro" ? 32768 : budget,
|
|
4464
|
+
attachThoughts: resolved.includeThoughts
|
|
4465
|
+
};
|
|
4466
|
+
}
|
|
4467
|
+
return {
|
|
4468
|
+
interactionLevel: void 0,
|
|
4469
|
+
thinkingLevel: void 0,
|
|
4470
|
+
thinkingBudget: void 0,
|
|
4471
|
+
attachThoughts: resolved.includeThoughts
|
|
4472
|
+
};
|
|
4160
4473
|
}
|
|
4161
4474
|
var GeminiProvider = class {
|
|
4162
4475
|
name = "google";
|
|
@@ -4225,6 +4538,21 @@ var GeminiProvider = class {
|
|
|
4225
4538
|
nativeStructuredOutputSupport() {
|
|
4226
4539
|
return "lossy";
|
|
4227
4540
|
}
|
|
4541
|
+
/** Report a clamped `effort` (R7a). Gemini 3.x cannot disable thinking and
|
|
4542
|
+
* caps at `'high'`, so `'none'`, `'xhigh'` and `'max'` are not honored
|
|
4543
|
+
* verbatim there. Everything else — including 2.x disabled thinking — is
|
|
4544
|
+
* sent as asked, so there is nothing to report. */
|
|
4545
|
+
effortResolution(options) {
|
|
4546
|
+
const resolved = resolveThinkingOptions(options);
|
|
4547
|
+
if (resolved.effort === void 0) return void 0;
|
|
4548
|
+
const clamp = resolveGeminiThinking(this.requestModel(options), resolved).clamp;
|
|
4549
|
+
return clamp ? {
|
|
4550
|
+
requested: resolved.effort,
|
|
4551
|
+
effective: clamp.effective,
|
|
4552
|
+
clamped: true,
|
|
4553
|
+
cause: clamp.cause
|
|
4554
|
+
} : void 0;
|
|
4555
|
+
}
|
|
4228
4556
|
baseUrl;
|
|
4229
4557
|
apiKeySource;
|
|
4230
4558
|
callCounter = 0;
|
|
@@ -4262,6 +4590,7 @@ var GeminiProvider = class {
|
|
|
4262
4590
|
const pricingContext = this.pricingContext(this.requestModel(options), body);
|
|
4263
4591
|
this.assertSafeGemini3Continuation(messages, pricingContext.model);
|
|
4264
4592
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4593
|
+
const recorder = new CallTimingRecorder();
|
|
4265
4594
|
const res = await fetchWithRetry(
|
|
4266
4595
|
`${this.baseUrl}/models/${pricingContext.model}:generateContent`,
|
|
4267
4596
|
{
|
|
@@ -4270,7 +4599,7 @@ var GeminiProvider = class {
|
|
|
4270
4599
|
body: JSON.stringify(body),
|
|
4271
4600
|
signal: options.signal
|
|
4272
4601
|
},
|
|
4273
|
-
{ governor: this.governor, provider: this.name }
|
|
4602
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4274
4603
|
);
|
|
4275
4604
|
if (!res.ok) {
|
|
4276
4605
|
const errorBody = await res.text();
|
|
@@ -4280,11 +4609,12 @@ var GeminiProvider = class {
|
|
|
4280
4609
|
status: res.status,
|
|
4281
4610
|
headers: res.headers,
|
|
4282
4611
|
message,
|
|
4283
|
-
body: errorBody
|
|
4612
|
+
body: errorBody,
|
|
4613
|
+
timing: recorder.chatTiming()
|
|
4284
4614
|
});
|
|
4285
4615
|
}
|
|
4286
4616
|
const json = await res.json();
|
|
4287
|
-
return this.parseResponse(json, pricingContext);
|
|
4617
|
+
return withChatTiming(recorder, this.parseResponse(json, pricingContext));
|
|
4288
4618
|
}
|
|
4289
4619
|
// ---------------------------------------------------------------------------
|
|
4290
4620
|
// stream - SSE streaming completion
|
|
@@ -4298,6 +4628,7 @@ var GeminiProvider = class {
|
|
|
4298
4628
|
const pricingContext = this.pricingContext(this.requestModel(options), body);
|
|
4299
4629
|
this.assertSafeGemini3Continuation(messages, pricingContext.model);
|
|
4300
4630
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4631
|
+
const recorder = new CallTimingRecorder();
|
|
4301
4632
|
const res = await fetchWithRetry(
|
|
4302
4633
|
`${this.baseUrl}/models/${pricingContext.model}:streamGenerateContent?alt=sse`,
|
|
4303
4634
|
{
|
|
@@ -4306,7 +4637,7 @@ var GeminiProvider = class {
|
|
|
4306
4637
|
body: JSON.stringify(body),
|
|
4307
4638
|
signal: options.signal
|
|
4308
4639
|
},
|
|
4309
|
-
{ governor: this.governor, provider: this.name }
|
|
4640
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4310
4641
|
);
|
|
4311
4642
|
if (!res.ok) {
|
|
4312
4643
|
const errorBody = await res.text();
|
|
@@ -4316,13 +4647,14 @@ var GeminiProvider = class {
|
|
|
4316
4647
|
status: res.status,
|
|
4317
4648
|
headers: res.headers,
|
|
4318
4649
|
message,
|
|
4319
|
-
body: errorBody
|
|
4650
|
+
body: errorBody,
|
|
4651
|
+
timing: recorder.chatTiming()
|
|
4320
4652
|
});
|
|
4321
4653
|
}
|
|
4322
4654
|
if (!res.body) {
|
|
4323
4655
|
throw new Error("Gemini stream response has no body");
|
|
4324
4656
|
}
|
|
4325
|
-
yield* this.parseSSEStream(res.body, pricingContext);
|
|
4657
|
+
yield* withCallTiming(recorder, this.parseSSEStream(res.body, pricingContext, recorder));
|
|
4326
4658
|
}
|
|
4327
4659
|
// ---------------------------------------------------------------------------
|
|
4328
4660
|
// Rich image requests use Gemini Interactions. It is deliberately stateless:
|
|
@@ -4333,10 +4665,11 @@ var GeminiProvider = class {
|
|
|
4333
4665
|
this.assertSafeInteractionOptions(options);
|
|
4334
4666
|
const body = this.buildInteractionRequestBody(messages, options, false);
|
|
4335
4667
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4668
|
+
const recorder = new CallTimingRecorder();
|
|
4336
4669
|
const res = await fetchWithRetry(
|
|
4337
4670
|
`${this.baseUrl}/interactions`,
|
|
4338
4671
|
{ method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
|
|
4339
|
-
{ governor: this.governor, provider: this.name }
|
|
4672
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4340
4673
|
);
|
|
4341
4674
|
if (!res.ok) {
|
|
4342
4675
|
const errorBody = await res.text();
|
|
@@ -4345,19 +4678,24 @@ var GeminiProvider = class {
|
|
|
4345
4678
|
status: res.status,
|
|
4346
4679
|
headers: res.headers,
|
|
4347
4680
|
message: this.extractErrorMessage(errorBody, res.status),
|
|
4348
|
-
body: errorBody
|
|
4681
|
+
body: errorBody,
|
|
4682
|
+
timing: recorder.chatTiming()
|
|
4349
4683
|
});
|
|
4350
4684
|
}
|
|
4351
|
-
return
|
|
4685
|
+
return withChatTiming(
|
|
4686
|
+
recorder,
|
|
4687
|
+
this.parseInteractionResponse(await res.json())
|
|
4688
|
+
);
|
|
4352
4689
|
}
|
|
4353
4690
|
async *streamInteraction(messages, options) {
|
|
4354
4691
|
this.assertSafeInteractionOptions(options);
|
|
4355
4692
|
const body = this.buildInteractionRequestBody(messages, options, true);
|
|
4356
4693
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4694
|
+
const recorder = new CallTimingRecorder();
|
|
4357
4695
|
const res = await fetchWithRetry(
|
|
4358
4696
|
`${this.baseUrl}/interactions?alt=sse`,
|
|
4359
4697
|
{ method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
|
|
4360
|
-
{ governor: this.governor, provider: this.name }
|
|
4698
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4361
4699
|
);
|
|
4362
4700
|
if (!res.ok) {
|
|
4363
4701
|
const errorBody = await res.text();
|
|
@@ -4366,11 +4704,15 @@ var GeminiProvider = class {
|
|
|
4366
4704
|
status: res.status,
|
|
4367
4705
|
headers: res.headers,
|
|
4368
4706
|
message: this.extractErrorMessage(errorBody, res.status),
|
|
4369
|
-
body: errorBody
|
|
4707
|
+
body: errorBody,
|
|
4708
|
+
timing: recorder.chatTiming()
|
|
4370
4709
|
});
|
|
4371
4710
|
}
|
|
4372
4711
|
if (!res.body) throw new Error("Gemini Interactions stream has no body");
|
|
4373
|
-
yield*
|
|
4712
|
+
yield* withCallTiming(
|
|
4713
|
+
recorder,
|
|
4714
|
+
this.parseInteractionSSEStream(res.body, res.headers, recorder)
|
|
4715
|
+
);
|
|
4374
4716
|
}
|
|
4375
4717
|
buildInteractionRequestBody(messages, options, stream) {
|
|
4376
4718
|
const model = this.requestModel(options);
|
|
@@ -4399,12 +4741,9 @@ var GeminiProvider = class {
|
|
|
4399
4741
|
generationConfig.tool_choice = typeof options.toolChoice === "string" ? options.toolChoice === "required" ? "any" : options.toolChoice : { allowed_tools: { mode: "any", tools: [options.toolChoice.function.name] } };
|
|
4400
4742
|
}
|
|
4401
4743
|
const thinking = resolveThinkingOptions(options);
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
generationConfig.thinking_level = budgetToThinkingLevel(thinking.thinkingBudget);
|
|
4406
|
-
} else if (thinking.activeEffort) {
|
|
4407
|
-
generationConfig.thinking_level = isGemini3x(model) ? THINKING_LEVELS[thinking.activeEffort] ?? "medium" : budgetToThinkingLevel(THINKING_BUDGETS2[thinking.activeEffort] ?? 5e3);
|
|
4744
|
+
const thinkingPlan = resolveGeminiThinking(model, thinking);
|
|
4745
|
+
if (thinkingPlan.interactionLevel !== void 0) {
|
|
4746
|
+
generationConfig.thinking_level = thinkingPlan.interactionLevel;
|
|
4408
4747
|
}
|
|
4409
4748
|
if (thinking.includeThoughts) generationConfig.thinking_summaries = "auto";
|
|
4410
4749
|
if (Object.keys(generationConfig).length) body.generation_config = generationConfig;
|
|
@@ -4529,7 +4868,7 @@ var GeminiProvider = class {
|
|
|
4529
4868
|
providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
|
|
4530
4869
|
};
|
|
4531
4870
|
}
|
|
4532
|
-
async *parseInteractionSSEStream(body, responseHeaders) {
|
|
4871
|
+
async *parseInteractionSSEStream(body, responseHeaders, timing) {
|
|
4533
4872
|
const reader = body.getReader();
|
|
4534
4873
|
const decoder = new TextDecoder();
|
|
4535
4874
|
let buffer = "";
|
|
@@ -4539,7 +4878,7 @@ var GeminiProvider = class {
|
|
|
4539
4878
|
let completed = false;
|
|
4540
4879
|
try {
|
|
4541
4880
|
while (true) {
|
|
4542
|
-
const { done, value } = await
|
|
4881
|
+
const { done, value } = await timing.read(reader);
|
|
4543
4882
|
if (done) break;
|
|
4544
4883
|
buffer += decoder.decode(value, { stream: true });
|
|
4545
4884
|
const lines = buffer.split("\n");
|
|
@@ -4562,14 +4901,16 @@ var GeminiProvider = class {
|
|
|
4562
4901
|
provider: this.name,
|
|
4563
4902
|
status,
|
|
4564
4903
|
headers: responseHeaders,
|
|
4565
|
-
message
|
|
4904
|
+
message,
|
|
4905
|
+
timing: timing.streamTiming()
|
|
4566
4906
|
});
|
|
4567
4907
|
}
|
|
4568
4908
|
throw new ProviderError({
|
|
4569
4909
|
provider: this.name,
|
|
4570
4910
|
status: 0,
|
|
4571
4911
|
retryable: false,
|
|
4572
|
-
message
|
|
4912
|
+
message,
|
|
4913
|
+
timing: timing.streamTiming()
|
|
4573
4914
|
});
|
|
4574
4915
|
}
|
|
4575
4916
|
if (event.event_type === "step.start" && event.step) {
|
|
@@ -4840,50 +5181,17 @@ var GeminiProvider = class {
|
|
|
4840
5181
|
if (Object.keys(generationConfig).length > 0) {
|
|
4841
5182
|
body.generationConfig = generationConfig;
|
|
4842
5183
|
}
|
|
4843
|
-
const
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
}
|
|
4851
|
-
if (
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
warnGemini3xEffortNone(requestModel);
|
|
4855
|
-
}
|
|
4856
|
-
generationConfig.thinkingConfig = { thinkingLevel: minThinkingLevel(requestModel) };
|
|
4857
|
-
} else {
|
|
4858
|
-
generationConfig.thinkingConfig = { thinkingBudget: 0 };
|
|
4859
|
-
}
|
|
4860
|
-
if (!body.generationConfig) body.generationConfig = generationConfig;
|
|
4861
|
-
} else if (hasBudgetOverride) {
|
|
4862
|
-
const config = {};
|
|
4863
|
-
if (isGemini3x(requestModel)) {
|
|
4864
|
-
config.thinkingLevel = budgetToThinkingLevel(thinkingBudget);
|
|
4865
|
-
} else {
|
|
4866
|
-
config.thinkingBudget = thinkingBudget;
|
|
4867
|
-
}
|
|
4868
|
-
if (includeThoughts) config.includeThoughts = true;
|
|
4869
|
-
generationConfig.thinkingConfig = config;
|
|
4870
|
-
if (!body.generationConfig) body.generationConfig = generationConfig;
|
|
4871
|
-
} else if (activeEffort) {
|
|
4872
|
-
const config = {};
|
|
4873
|
-
if (isGemini3x(requestModel)) {
|
|
4874
|
-
config.thinkingLevel = THINKING_LEVELS[activeEffort] ?? "medium";
|
|
4875
|
-
} else {
|
|
4876
|
-
if (activeEffort === "max" && requestModel === "gemini-2.5-pro") {
|
|
4877
|
-
config.thinkingBudget = 32768;
|
|
4878
|
-
} else {
|
|
4879
|
-
config.thinkingBudget = THINKING_BUDGETS2[activeEffort] ?? 5e3;
|
|
4880
|
-
}
|
|
4881
|
-
}
|
|
4882
|
-
if (includeThoughts) config.includeThoughts = true;
|
|
4883
|
-
generationConfig.thinkingConfig = config;
|
|
4884
|
-
if (!body.generationConfig) body.generationConfig = generationConfig;
|
|
4885
|
-
} else if (includeThoughts) {
|
|
4886
|
-
generationConfig.thinkingConfig = { includeThoughts: true };
|
|
5184
|
+
const thinkingPlan = resolveGeminiThinking(requestModel, resolveThinkingOptions(options));
|
|
5185
|
+
const thinkingConfig = {};
|
|
5186
|
+
if (thinkingPlan.thinkingLevel !== void 0) {
|
|
5187
|
+
thinkingConfig.thinkingLevel = thinkingPlan.thinkingLevel;
|
|
5188
|
+
}
|
|
5189
|
+
if (thinkingPlan.thinkingBudget !== void 0) {
|
|
5190
|
+
thinkingConfig.thinkingBudget = thinkingPlan.thinkingBudget;
|
|
5191
|
+
}
|
|
5192
|
+
if (thinkingPlan.attachThoughts) thinkingConfig.includeThoughts = true;
|
|
5193
|
+
if (Object.keys(thinkingConfig).length > 0) {
|
|
5194
|
+
generationConfig.thinkingConfig = thinkingConfig;
|
|
4887
5195
|
if (!body.generationConfig) body.generationConfig = generationConfig;
|
|
4888
5196
|
}
|
|
4889
5197
|
if (options.toolChoice !== void 0) {
|
|
@@ -5080,7 +5388,7 @@ var GeminiProvider = class {
|
|
|
5080
5388
|
// ---------------------------------------------------------------------------
|
|
5081
5389
|
// Internal: SSE stream parsing
|
|
5082
5390
|
// ---------------------------------------------------------------------------
|
|
5083
|
-
async *parseSSEStream(body, pricingContext) {
|
|
5391
|
+
async *parseSSEStream(body, pricingContext, timing) {
|
|
5084
5392
|
const reader = body.getReader();
|
|
5085
5393
|
const decoder = new TextDecoder();
|
|
5086
5394
|
let buffer = "";
|
|
@@ -5093,7 +5401,7 @@ var GeminiProvider = class {
|
|
|
5093
5401
|
const accumulatedParts = [];
|
|
5094
5402
|
try {
|
|
5095
5403
|
while (true) {
|
|
5096
|
-
const { done, value } = await
|
|
5404
|
+
const { done, value } = await timing.read(reader);
|
|
5097
5405
|
if (done) break;
|
|
5098
5406
|
buffer += decoder.decode(value, { stream: true });
|
|
5099
5407
|
const lines = buffer.split("\n");
|
|
@@ -7518,6 +7826,9 @@ var AXL_EVENT_TYPES = [
|
|
|
7518
7826
|
// structured-output cliffs (oversized appended schema, dropped refinements,
|
|
7519
7827
|
// streaming disabled, zero-guidance `schemaPrompt:'none'`).
|
|
7520
7828
|
"schema_diagnostic",
|
|
7829
|
+
// Provider capability diagnostics — the resolved provider could not honor a
|
|
7830
|
+
// portable request knob verbatim (first kind: a clamped `effort`).
|
|
7831
|
+
"provider_diagnostic",
|
|
7521
7832
|
// Observability
|
|
7522
7833
|
"log",
|
|
7523
7834
|
"memory_remember",
|
|
@@ -7567,6 +7878,7 @@ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
|
|
|
7567
7878
|
"pipeline",
|
|
7568
7879
|
"verify",
|
|
7569
7880
|
"schema_diagnostic",
|
|
7881
|
+
"provider_diagnostic",
|
|
7570
7882
|
"workflow_start",
|
|
7571
7883
|
"workflow_end",
|
|
7572
7884
|
"transcription_start",
|
|
@@ -8495,6 +8807,10 @@ var LEGACY_REDACTION_RULES = {
|
|
|
8495
8807
|
// which is user content or PII (field paths are schema-shape, preserved like
|
|
8496
8808
|
// `string_delta.path`). Passthrough, as an explicit reviewed decision.
|
|
8497
8809
|
schema_diagnostic: passthrough,
|
|
8810
|
+
// `provider_diagnostic` carries only provider capability metadata — a provider
|
|
8811
|
+
// name, a model id, and two effort level strings. No application data or user
|
|
8812
|
+
// content ever reaches it. Passthrough, as an explicit reviewed decision.
|
|
8813
|
+
provider_diagnostic: passthrough,
|
|
8498
8814
|
log: (e) => {
|
|
8499
8815
|
if (!e.data || typeof e.data !== "object" || Array.isArray(e.data)) return e;
|
|
8500
8816
|
return { ...e, data: walkObjectOneLevel(e.data, LOG_PRESERVE_KEYS) };
|
|
@@ -8625,6 +8941,7 @@ var COMMON_V2_REDACTION_RULES = {
|
|
|
8625
8941
|
string_delta: reuseLegacyRule("string_delta"),
|
|
8626
8942
|
verify: reuseLegacyRule("verify"),
|
|
8627
8943
|
schema_diagnostic: reuseLegacyRule("schema_diagnostic"),
|
|
8944
|
+
provider_diagnostic: reuseLegacyRule("provider_diagnostic"),
|
|
8628
8945
|
log: reuseLegacyRule("log"),
|
|
8629
8946
|
memory_remember: reuseLegacyRule("memory_remember"),
|
|
8630
8947
|
memory_recall: reuseLegacyRule("memory_recall"),
|
|
@@ -8720,13 +9037,14 @@ function detectDroppedRefinements(schema) {
|
|
|
8720
9037
|
var DOCS_URL = "docs/observability.md#schema-diagnostics";
|
|
8721
9038
|
var MAX_WARNED_KEYS = 2048;
|
|
8722
9039
|
var warnedDiagnosticKeys = /* @__PURE__ */ new Set();
|
|
8723
|
-
function
|
|
9040
|
+
function warnDiagnosticOnce(key, message, silent, docs = DOCS_URL) {
|
|
8724
9041
|
if (silent || process.env.AXL_DIAGNOSTICS_SILENT === "true") return;
|
|
8725
9042
|
if (warnedDiagnosticKeys.has(key)) return;
|
|
8726
9043
|
if (warnedDiagnosticKeys.size >= MAX_WARNED_KEYS) warnedDiagnosticKeys.clear();
|
|
8727
9044
|
warnedDiagnosticKeys.add(key);
|
|
8728
|
-
console.warn(`[axl] ${message} See ${
|
|
9045
|
+
console.warn(`[axl] ${message} See ${docs}.`);
|
|
8729
9046
|
}
|
|
9047
|
+
var warnSchemaDiagnosticOnce = warnDiagnosticOnce;
|
|
8730
9048
|
|
|
8731
9049
|
// src/config.ts
|
|
8732
9050
|
function defineConfig(config) {
|
|
@@ -9576,7 +9894,29 @@ function appendRetryMessages(messages, content, feedbackMessage, providerMetadat
|
|
|
9576
9894
|
content,
|
|
9577
9895
|
...providerMetadata ? { providerMetadata } : {}
|
|
9578
9896
|
});
|
|
9579
|
-
messages.push({ role: "
|
|
9897
|
+
messages.push({ role: "user", content: feedbackMessage });
|
|
9898
|
+
}
|
|
9899
|
+
function isRetryAbort(value) {
|
|
9900
|
+
return typeof value === "object" && value !== null && value.retry === false;
|
|
9901
|
+
}
|
|
9902
|
+
function describeValue(value) {
|
|
9903
|
+
if (value === null) return "null";
|
|
9904
|
+
if (Array.isArray(value)) return `an array of length ${value.length}`;
|
|
9905
|
+
if (typeof value === "object") {
|
|
9906
|
+
const keys = Object.keys(value);
|
|
9907
|
+
return keys.length > 0 ? `an object with keys [${keys.join(", ")}]` : "an empty object";
|
|
9908
|
+
}
|
|
9909
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
9910
|
+
return `the ${typeof value} ${JSON.stringify(value)}`;
|
|
9911
|
+
}
|
|
9912
|
+
return `a value of type ${typeof value}`;
|
|
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;
|
|
9580
9920
|
}
|
|
9581
9921
|
function estimateMessagesTokens(messages) {
|
|
9582
9922
|
let total = 0;
|
|
@@ -9717,6 +10057,13 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
9717
10057
|
workflowLifecycleState;
|
|
9718
10058
|
signal;
|
|
9719
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;
|
|
9720
10067
|
workflowName;
|
|
9721
10068
|
mcpManager;
|
|
9722
10069
|
spanManager;
|
|
@@ -10249,6 +10596,17 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10249
10596
|
const costAfter2 = this.budgetContext?.totalCost ?? 0;
|
|
10250
10597
|
span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
|
|
10251
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
|
+
}
|
|
10252
10610
|
if (usageCapture.value) {
|
|
10253
10611
|
span.setAttribute(
|
|
10254
10612
|
"axl.agent.prompt_tokens",
|
|
@@ -10456,6 +10814,14 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10456
10814
|
const toolTokens = toolDefs.length > 0 ? estimateTokens(JSON.stringify(toolDefs)) : 0;
|
|
10457
10815
|
const overhead = systemTokens + toolTokens + reserveTokens;
|
|
10458
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
|
+
}
|
|
10459
10825
|
const historyEstimate = estimateMessagesTokens(sessionHistory);
|
|
10460
10826
|
if (historyEstimate.unmeasured) {
|
|
10461
10827
|
this.emitEvent({
|
|
@@ -10513,6 +10879,7 @@ ${appendedSchemaText}`;
|
|
|
10513
10879
|
provider,
|
|
10514
10880
|
model
|
|
10515
10881
|
});
|
|
10882
|
+
this.emitProviderDiagnostics(agent2, options, { provider, model, providerOptions });
|
|
10516
10883
|
if (handoffMessages && handoffMessages.length > 0) {
|
|
10517
10884
|
const handoffContext = handoffMessages.filter(
|
|
10518
10885
|
(m) => m.role === "assistant" || m.role === "tool"
|
|
@@ -10520,6 +10887,7 @@ ${appendedSchemaText}`;
|
|
|
10520
10887
|
if (handoffContext.length > 0) {
|
|
10521
10888
|
messages.push({
|
|
10522
10889
|
role: "system",
|
|
10890
|
+
origin: "runtime",
|
|
10523
10891
|
content: "The following is the conversation history from the previous agent that handed off to you:"
|
|
10524
10892
|
});
|
|
10525
10893
|
for (const msg of handoffContext) {
|
|
@@ -10569,6 +10937,7 @@ ${appendedSchemaText}`;
|
|
|
10569
10937
|
const maxTurns = agent2._config.maxTurns ?? 25;
|
|
10570
10938
|
const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
|
|
10571
10939
|
const startTime = Date.now();
|
|
10940
|
+
const timingTotals = { turns: 0, queuedMs: 0, retryMs: 0, wireMs: 0 };
|
|
10572
10941
|
const currentMessages = [...messages];
|
|
10573
10942
|
let turns = 0;
|
|
10574
10943
|
let guardrailOutputRetries = 0;
|
|
@@ -10582,8 +10951,22 @@ ${appendedSchemaText}`;
|
|
|
10582
10951
|
let lastStartMaxAttempts = 1;
|
|
10583
10952
|
while (turns < maxTurns) {
|
|
10584
10953
|
this.currentSignal?.throwIfAborted();
|
|
10585
|
-
|
|
10586
|
-
|
|
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
|
+
);
|
|
10587
10970
|
}
|
|
10588
10971
|
turns++;
|
|
10589
10972
|
const isFirstTurn = turns === 1;
|
|
@@ -10623,6 +11006,7 @@ ${appendedSchemaText}`;
|
|
|
10623
11006
|
effort: options?.effort ?? agent2._config.effort,
|
|
10624
11007
|
thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
|
|
10625
11008
|
includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
|
|
11009
|
+
promptCache: options?.promptCache ?? agent2._config.promptCache,
|
|
10626
11010
|
toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
|
|
10627
11011
|
stop: options?.stop ?? agent2._config.stop,
|
|
10628
11012
|
providerOptions,
|
|
@@ -10700,6 +11084,7 @@ ${appendedSchemaText}`;
|
|
|
10700
11084
|
const toolCalls = [];
|
|
10701
11085
|
const toolCallBuffers = /* @__PURE__ */ new Map();
|
|
10702
11086
|
let streamProviderMetadata;
|
|
11087
|
+
let streamTiming;
|
|
10703
11088
|
let thinkingContent = "";
|
|
10704
11089
|
const streamingObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof import_zod2.z.ZodObject;
|
|
10705
11090
|
const currentAttempt = schemaRetries + 1;
|
|
@@ -10751,6 +11136,7 @@ ${appendedSchemaText}`;
|
|
|
10751
11136
|
if (chunk.arguments) buffer.arguments += chunk.arguments;
|
|
10752
11137
|
} else if (chunk.type === "done") {
|
|
10753
11138
|
streamProviderMetadata = chunk.providerMetadata;
|
|
11139
|
+
streamTiming = chunk.timing;
|
|
10754
11140
|
if (chunk.usage) {
|
|
10755
11141
|
response = {
|
|
10756
11142
|
content: content2,
|
|
@@ -10778,6 +11164,9 @@ ${appendedSchemaText}`;
|
|
|
10778
11164
|
if (streamProviderMetadata) {
|
|
10779
11165
|
response.providerMetadata = streamProviderMetadata;
|
|
10780
11166
|
}
|
|
11167
|
+
if (streamTiming) {
|
|
11168
|
+
response.timing = streamTiming;
|
|
11169
|
+
}
|
|
10781
11170
|
if (thinkingContent) {
|
|
10782
11171
|
response.thinking_content = thinkingContent;
|
|
10783
11172
|
}
|
|
@@ -10786,12 +11175,15 @@ ${appendedSchemaText}`;
|
|
|
10786
11175
|
}
|
|
10787
11176
|
} catch (err) {
|
|
10788
11177
|
if (richRequest) this.recordRichFailure(err);
|
|
11178
|
+
const failureTiming = err instanceof ProviderError ? err.timing : void 0;
|
|
10789
11179
|
this.emitEvent({
|
|
10790
11180
|
type: "agent_call_end",
|
|
10791
11181
|
agent: agent2._name,
|
|
10792
11182
|
model: effectiveModelUri,
|
|
10793
11183
|
promptVersion: agent2._config.version,
|
|
10794
11184
|
duration: Date.now() - turnStart,
|
|
11185
|
+
// Top-level beside `duration`, exactly as on the success path.
|
|
11186
|
+
...failureTiming ? { timing: failureTiming } : {},
|
|
10795
11187
|
data: {
|
|
10796
11188
|
response: "",
|
|
10797
11189
|
turn: turns,
|
|
@@ -10808,9 +11200,41 @@ ${appendedSchemaText}`;
|
|
|
10808
11200
|
if (usageCapture && response.usage) {
|
|
10809
11201
|
usageCapture.value = response.usage;
|
|
10810
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
|
+
}
|
|
10811
11229
|
if (response.cost) {
|
|
10812
11230
|
this._accumulateBudgetCost(response.cost);
|
|
10813
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
|
+
}
|
|
10814
11238
|
this.emitEvent({
|
|
10815
11239
|
type: "agent_call_end",
|
|
10816
11240
|
agent: agent2._name,
|
|
@@ -10825,6 +11249,8 @@ ${appendedSchemaText}`;
|
|
|
10825
11249
|
cacheWrite: response.usage.cache_write_tokens
|
|
10826
11250
|
} : void 0,
|
|
10827
11251
|
duration: Date.now() - turnStart,
|
|
11252
|
+
// Additive beside `duration`, which keeps its turn-wall-clock meaning.
|
|
11253
|
+
...response.timing ? { timing: response.timing } : {},
|
|
10828
11254
|
data: {
|
|
10829
11255
|
response: response.content,
|
|
10830
11256
|
...response.thinking_content ? { thinking: response.thinking_content } : {},
|
|
@@ -10877,7 +11303,11 @@ ${appendedSchemaText}`;
|
|
|
10877
11303
|
retries: options.retries,
|
|
10878
11304
|
metadata: options.metadata,
|
|
10879
11305
|
validate: options.validate,
|
|
10880
|
-
validateRetries: options.validateRetries
|
|
11306
|
+
validateRetries: options.validateRetries,
|
|
11307
|
+
// This seam serves both callers of a handoff: a plain `ctx.ask()` on an
|
|
11308
|
+
// agent with `handoffs`, and the routed `ctx.delegate()` path, whose
|
|
11309
|
+
// terminal ask is the handoff target. The hook travels like `validate`.
|
|
11310
|
+
retryFeedback: options.retryFeedback
|
|
10881
11311
|
} : void 0;
|
|
10882
11312
|
const handoffFn = () => askStorage.run(targetFrame, async () => {
|
|
10883
11313
|
const delegateOriginal = agent2._delegateOriginalInput;
|
|
@@ -11026,9 +11456,10 @@ ${appendedSchemaText}`;
|
|
|
11026
11456
|
const attempt = guardrailOutputRetries + 1;
|
|
11027
11457
|
const maxAttempts = maxGuardrailRetries + 1;
|
|
11028
11458
|
const onBlock = guardrails.onBlock ?? "throw";
|
|
11459
|
+
const blockReason = outputResult.reason ?? "Output blocked by guardrail";
|
|
11029
11460
|
let feedbackMessage;
|
|
11030
11461
|
if (outputResult.block && onBlock === "retry" && guardrailOutputRetries < maxGuardrailRetries) {
|
|
11031
|
-
feedbackMessage = `Your previous response was blocked by a safety guardrail: ${
|
|
11462
|
+
feedbackMessage = `Your previous response was blocked by a safety guardrail: ${blockReason}. Please provide a different response that complies with the guidelines.`;
|
|
11032
11463
|
}
|
|
11033
11464
|
this.emitEvent({
|
|
11034
11465
|
type: "guardrail",
|
|
@@ -11051,31 +11482,43 @@ ${appendedSchemaText}`;
|
|
|
11051
11482
|
});
|
|
11052
11483
|
if (outputResult.block) {
|
|
11053
11484
|
if (feedbackMessage) {
|
|
11054
|
-
this.
|
|
11055
|
-
type: "pipeline",
|
|
11056
|
-
agent: agent2._name,
|
|
11057
|
-
status: "failed",
|
|
11485
|
+
const decision = await this.resolveRetryFeedback(options?.retryFeedback, {
|
|
11058
11486
|
stage: "guardrail",
|
|
11059
|
-
attempt
|
|
11060
|
-
maxAttempts
|
|
11061
|
-
|
|
11487
|
+
attempt,
|
|
11488
|
+
maxAttempts,
|
|
11489
|
+
output: content,
|
|
11490
|
+
reason: blockReason,
|
|
11491
|
+
defaultMessage: feedbackMessage
|
|
11062
11492
|
});
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
11066
|
-
|
|
11067
|
-
|
|
11068
|
-
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
|
|
11493
|
+
if (decision.retry) {
|
|
11494
|
+
this.emitEvent({
|
|
11495
|
+
type: "pipeline",
|
|
11496
|
+
agent: agent2._name,
|
|
11497
|
+
status: "failed",
|
|
11498
|
+
stage: "guardrail",
|
|
11499
|
+
attempt,
|
|
11500
|
+
maxAttempts,
|
|
11501
|
+
reason: decision.message
|
|
11502
|
+
});
|
|
11503
|
+
guardrailOutputRetries++;
|
|
11504
|
+
appendRetryMessages(
|
|
11505
|
+
currentMessages,
|
|
11506
|
+
content,
|
|
11507
|
+
decision.message,
|
|
11508
|
+
response.providerMetadata
|
|
11509
|
+
);
|
|
11510
|
+
pendingRetryReason = "guardrail";
|
|
11511
|
+
continue;
|
|
11512
|
+
}
|
|
11513
|
+
this.currentSignal?.throwIfAborted();
|
|
11514
|
+
throw new GuardrailError("output", blockReason);
|
|
11072
11515
|
}
|
|
11073
11516
|
if (typeof onBlock === "function") {
|
|
11074
|
-
return onBlock(
|
|
11517
|
+
return onBlock(blockReason, {
|
|
11075
11518
|
metadata: this.metadata
|
|
11076
11519
|
});
|
|
11077
11520
|
}
|
|
11078
|
-
throw new GuardrailError("output",
|
|
11521
|
+
throw new GuardrailError("output", blockReason);
|
|
11079
11522
|
}
|
|
11080
11523
|
}
|
|
11081
11524
|
let validated = void 0;
|
|
@@ -11095,7 +11538,7 @@ ${appendedSchemaText}`;
|
|
|
11095
11538
|
schemaErr = err;
|
|
11096
11539
|
schemaReason = err instanceof Error ? err.message : String(err);
|
|
11097
11540
|
if (schemaRetries < maxSchemaRetries) {
|
|
11098
|
-
schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}.
|
|
11541
|
+
schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Return a corrected response that matches the required schema.`;
|
|
11099
11542
|
}
|
|
11100
11543
|
}
|
|
11101
11544
|
this.emitEvent({
|
|
@@ -11117,24 +11560,36 @@ ${appendedSchemaText}`;
|
|
|
11117
11560
|
});
|
|
11118
11561
|
if (!schemaValid) {
|
|
11119
11562
|
if (schemaFeedback) {
|
|
11120
|
-
this.
|
|
11121
|
-
type: "pipeline",
|
|
11122
|
-
agent: agent2._name,
|
|
11123
|
-
status: "failed",
|
|
11563
|
+
const decision = await this.resolveRetryFeedback(options.retryFeedback, {
|
|
11124
11564
|
stage: "schema",
|
|
11125
|
-
attempt:
|
|
11126
|
-
maxAttempts:
|
|
11127
|
-
|
|
11565
|
+
attempt: schemaAttempt,
|
|
11566
|
+
maxAttempts: schemaMaxAttempts,
|
|
11567
|
+
output: content,
|
|
11568
|
+
reason: schemaReason ?? "Schema parse failed",
|
|
11569
|
+
...schemaErr !== void 0 ? { error: schemaErr } : {},
|
|
11570
|
+
defaultMessage: schemaFeedback
|
|
11128
11571
|
});
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11572
|
+
if (decision.retry) {
|
|
11573
|
+
this.emitEvent({
|
|
11574
|
+
type: "pipeline",
|
|
11575
|
+
agent: agent2._name,
|
|
11576
|
+
status: "failed",
|
|
11577
|
+
stage: "schema",
|
|
11578
|
+
attempt: schemaAttempt,
|
|
11579
|
+
maxAttempts: schemaMaxAttempts,
|
|
11580
|
+
reason: decision.message
|
|
11581
|
+
});
|
|
11582
|
+
schemaRetries++;
|
|
11583
|
+
appendRetryMessages(
|
|
11584
|
+
currentMessages,
|
|
11585
|
+
content,
|
|
11586
|
+
decision.message,
|
|
11587
|
+
response.providerMetadata
|
|
11588
|
+
);
|
|
11589
|
+
pendingRetryReason = "schema";
|
|
11590
|
+
continue;
|
|
11591
|
+
}
|
|
11592
|
+
this.currentSignal?.throwIfAborted();
|
|
11138
11593
|
}
|
|
11139
11594
|
const zodErr = schemaErr instanceof import_zod2.ZodError ? schemaErr : new import_zod2.ZodError([
|
|
11140
11595
|
{
|
|
@@ -11148,12 +11603,14 @@ ${appendedSchemaText}`;
|
|
|
11148
11603
|
}
|
|
11149
11604
|
if (options?.schema && options.validate) {
|
|
11150
11605
|
let validateResult;
|
|
11606
|
+
let validateErr;
|
|
11151
11607
|
try {
|
|
11152
11608
|
validateResult = await options.validate(validated, {
|
|
11153
11609
|
metadata: this.metadata
|
|
11154
11610
|
});
|
|
11155
11611
|
} catch (err) {
|
|
11156
11612
|
rethrowEventStreamOverflow(err);
|
|
11613
|
+
validateErr = err;
|
|
11157
11614
|
const reason = err instanceof Error ? err.message : String(err);
|
|
11158
11615
|
validateResult = { valid: false, reason: `Validator error: ${reason}` };
|
|
11159
11616
|
}
|
|
@@ -11162,7 +11619,7 @@ ${appendedSchemaText}`;
|
|
|
11162
11619
|
const validateMaxAttempts = maxValidateRetries + 1;
|
|
11163
11620
|
let validateFeedback;
|
|
11164
11621
|
if (!validateResult.valid && validateRetries < maxValidateRetries) {
|
|
11165
|
-
validateFeedback = `Your response
|
|
11622
|
+
validateFeedback = `Your previous response failed validation: ${validateResult.reason ?? "Validation failed"}. Return a corrected response that satisfies the required schema and this rule.`;
|
|
11166
11623
|
}
|
|
11167
11624
|
this.emitEvent({
|
|
11168
11625
|
type: "validate",
|
|
@@ -11183,24 +11640,37 @@ ${appendedSchemaText}`;
|
|
|
11183
11640
|
});
|
|
11184
11641
|
if (!validateResult.valid) {
|
|
11185
11642
|
if (validateFeedback) {
|
|
11186
|
-
this.
|
|
11187
|
-
type: "pipeline",
|
|
11188
|
-
agent: agent2._name,
|
|
11189
|
-
status: "failed",
|
|
11643
|
+
const decision = await this.resolveRetryFeedback(options.retryFeedback, {
|
|
11190
11644
|
stage: "validate",
|
|
11191
|
-
attempt:
|
|
11192
|
-
maxAttempts:
|
|
11193
|
-
|
|
11645
|
+
attempt: validateAttempt,
|
|
11646
|
+
maxAttempts: validateMaxAttempts,
|
|
11647
|
+
output: content,
|
|
11648
|
+
parsed: validated,
|
|
11649
|
+
reason: validateResult.reason ?? "Validation failed",
|
|
11650
|
+
...validateErr !== void 0 ? { error: validateErr } : {},
|
|
11651
|
+
defaultMessage: validateFeedback
|
|
11194
11652
|
});
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
|
|
11202
|
-
|
|
11203
|
-
|
|
11653
|
+
if (decision.retry) {
|
|
11654
|
+
this.emitEvent({
|
|
11655
|
+
type: "pipeline",
|
|
11656
|
+
agent: agent2._name,
|
|
11657
|
+
status: "failed",
|
|
11658
|
+
stage: "validate",
|
|
11659
|
+
attempt: validateAttempt,
|
|
11660
|
+
maxAttempts: validateMaxAttempts,
|
|
11661
|
+
reason: decision.message
|
|
11662
|
+
});
|
|
11663
|
+
validateRetries++;
|
|
11664
|
+
appendRetryMessages(
|
|
11665
|
+
currentMessages,
|
|
11666
|
+
content,
|
|
11667
|
+
decision.message,
|
|
11668
|
+
response.providerMetadata
|
|
11669
|
+
);
|
|
11670
|
+
pendingRetryReason = "validate";
|
|
11671
|
+
continue;
|
|
11672
|
+
}
|
|
11673
|
+
this.currentSignal?.throwIfAborted();
|
|
11204
11674
|
}
|
|
11205
11675
|
throw new ValidationError(
|
|
11206
11676
|
validated,
|
|
@@ -11222,6 +11692,31 @@ ${appendedSchemaText}`;
|
|
|
11222
11692
|
}
|
|
11223
11693
|
throw new MaxTurnsError("ctx.ask()", maxTurns);
|
|
11224
11694
|
}
|
|
11695
|
+
/**
|
|
11696
|
+
* Resolve the corrective feedback text for one rejected attempt, giving an optional
|
|
11697
|
+
* caller-supplied `retryFeedback` hook the chance to replace it or to stop retrying.
|
|
11698
|
+
*
|
|
11699
|
+
* Called by all three output gates, only while a retry is still permitted (never on the
|
|
11700
|
+
* exhausting attempt) and only after the gate's own event has been emitted, so a hook that
|
|
11701
|
+
* throws or aborts can never erase the evidence that the gate rejected. A non-empty string
|
|
11702
|
+
* replaces `defaultMessage` verbatim; `undefined` / `''` keeps it; `{ retry: false }` asks
|
|
11703
|
+
* the gate to throw its typed terminal error. Hook exceptions propagate — a bug in caller
|
|
11704
|
+
* code is not a model failure and must not be retried into silence, and neither is a
|
|
11705
|
+
* return value outside the contract: an unrecognized shape throws instead of silently
|
|
11706
|
+
* degrading to the default text, which would hide the defect behind extra token spend.
|
|
11707
|
+
*/
|
|
11708
|
+
async resolveRetryFeedback(hook, info) {
|
|
11709
|
+
if (!hook) return { retry: true, message: info.defaultMessage };
|
|
11710
|
+
const result = await hook(info, { metadata: this.metadata });
|
|
11711
|
+
if (result === void 0 || result === "") {
|
|
11712
|
+
return { retry: true, message: info.defaultMessage };
|
|
11713
|
+
}
|
|
11714
|
+
if (typeof result === "string") return { retry: true, message: result };
|
|
11715
|
+
if (isRetryAbort(result)) return { retry: false };
|
|
11716
|
+
throw new TypeError(
|
|
11717
|
+
`retryFeedback must return a string, undefined, or { retry: false }; received ${describeValue(result)}.`
|
|
11718
|
+
);
|
|
11719
|
+
}
|
|
11225
11720
|
/**
|
|
11226
11721
|
* Push the final assistant message into session history, preserving providerMetadata
|
|
11227
11722
|
* (e.g., Gemini thought signatures needed for multi-turn reasoning context) and
|
|
@@ -11377,6 +11872,57 @@ ${appendedSchemaText}`;
|
|
|
11377
11872
|
}
|
|
11378
11873
|
}
|
|
11379
11874
|
}
|
|
11875
|
+
/**
|
|
11876
|
+
* Emit a `provider_diagnostic` when the resolved provider reports that it
|
|
11877
|
+
* could not honor the unified `effort` verbatim (P3/R6/R7).
|
|
11878
|
+
*
|
|
11879
|
+
* The adapter only *resolves* — it returns the requested and effective levels
|
|
11880
|
+
* from the same pure resolver its request builder uses. The runtime decides
|
|
11881
|
+
* how to surface that, because only the runtime can see the event bus and
|
|
11882
|
+
* `AxlConfig.diagnostics.silent` (adapters receive `ProviderConfig` only).
|
|
11883
|
+
*
|
|
11884
|
+
* Called once per ask, so a tool loop with N provider round trips still emits
|
|
11885
|
+
* one event; the `console.warn` dedupes once per process per distinct clamp.
|
|
11886
|
+
* A throwing `effortResolution` is a provider bug and propagates.
|
|
11887
|
+
*/
|
|
11888
|
+
emitProviderDiagnostics(agent2, options, ctx) {
|
|
11889
|
+
if (!ctx.provider.effortResolution) return;
|
|
11890
|
+
const resolution = ctx.provider.effortResolution({
|
|
11891
|
+
model: ctx.model,
|
|
11892
|
+
effort: options?.effort ?? agent2._config.effort,
|
|
11893
|
+
thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
|
|
11894
|
+
includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
|
|
11895
|
+
providerOptions: ctx.providerOptions
|
|
11896
|
+
});
|
|
11897
|
+
if (!resolution?.clamped) return;
|
|
11898
|
+
if (typeof resolution.effective !== "string" || resolution.effective.length === 0 || typeof resolution.requested !== "string" || resolution.requested.length === 0) {
|
|
11899
|
+
throw new TypeError(
|
|
11900
|
+
`Provider '${ctx.provider.name ?? "unknown"}' returned a malformed EffortResolution for model '${ctx.model}': 'requested' and 'effective' must be non-empty strings when 'clamped' is true. Received ${JSON.stringify(resolution)}.`
|
|
11901
|
+
);
|
|
11902
|
+
}
|
|
11903
|
+
const cause = resolution.cause ?? `effort '${resolution.requested}' is not supported as asked`;
|
|
11904
|
+
this.emitEvent({
|
|
11905
|
+
type: "provider_diagnostic",
|
|
11906
|
+
agent: agent2._name,
|
|
11907
|
+
data: {
|
|
11908
|
+
kind: "effort_clamped",
|
|
11909
|
+
...ctx.provider.name ? { provider: ctx.provider.name } : {},
|
|
11910
|
+
model: ctx.model,
|
|
11911
|
+
requested: resolution.requested,
|
|
11912
|
+
effective: resolution.effective,
|
|
11913
|
+
cause
|
|
11914
|
+
}
|
|
11915
|
+
});
|
|
11916
|
+
warnDiagnosticOnce(
|
|
11917
|
+
// One warning per distinct clamp, not per ask: the same clamp across 500
|
|
11918
|
+
// asks is one process-level surprise, while a different requested/effective
|
|
11919
|
+
// pair is a genuinely new one.
|
|
11920
|
+
`effort_clamped\0${ctx.provider.name ?? ""}\0${ctx.model}\0${resolution.requested}\0${resolution.effective}`,
|
|
11921
|
+
`effort '${resolution.requested}' was clamped to '${resolution.effective}' on ${ctx.provider.name ? `${ctx.provider.name} ` : ""}${ctx.model} \u2014 ${cause}. A 'provider_diagnostic' event carries the same detail per ask.`,
|
|
11922
|
+
this.config.diagnostics?.silent,
|
|
11923
|
+
"docs/observability.md#provider-diagnostics"
|
|
11924
|
+
);
|
|
11925
|
+
}
|
|
11380
11926
|
buildToolDefs(agent2, resolvedHandoffs) {
|
|
11381
11927
|
const defs = [];
|
|
11382
11928
|
if (agent2._config.tools) {
|
|
@@ -11423,6 +11969,7 @@ ${appendedSchemaText}`;
|
|
|
11423
11969
|
if (this.summaryCache) {
|
|
11424
11970
|
const summaryMsg2 = {
|
|
11425
11971
|
role: "system",
|
|
11972
|
+
origin: "runtime",
|
|
11426
11973
|
content: `Summary of earlier conversation:
|
|
11427
11974
|
${this.summaryCache}`
|
|
11428
11975
|
};
|
|
@@ -11436,6 +11983,7 @@ ${this.summaryCache}`
|
|
|
11436
11983
|
recentTokens2 += msgTokens;
|
|
11437
11984
|
splitIdx2 = i;
|
|
11438
11985
|
}
|
|
11986
|
+
splitIdx2 = nextUserTurn(history, splitIdx2) ?? history.length;
|
|
11439
11987
|
if (splitIdx2 < history.length) {
|
|
11440
11988
|
return [summaryMsg2, ...history.slice(splitIdx2)];
|
|
11441
11989
|
}
|
|
@@ -11449,6 +11997,19 @@ ${this.summaryCache}`
|
|
|
11449
11997
|
recentTokens += msgTokens;
|
|
11450
11998
|
splitIdx = i;
|
|
11451
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
|
+
}
|
|
11452
12013
|
if (splitIdx === 0) return history;
|
|
11453
12014
|
const oldMessages = history.slice(0, splitIdx);
|
|
11454
12015
|
const summaryModelUri = this.config.contextManagement?.summaryModel;
|
|
@@ -11480,6 +12041,7 @@ ${this.summaryCache}`
|
|
|
11480
12041
|
}
|
|
11481
12042
|
const summaryMsg = {
|
|
11482
12043
|
role: "system",
|
|
12044
|
+
origin: "runtime",
|
|
11483
12045
|
content: `Summary of earlier conversation:
|
|
11484
12046
|
${summaryResponse.content}`
|
|
11485
12047
|
};
|
|
@@ -12657,7 +13219,8 @@ ${summaryResponse.content}`
|
|
|
12657
13219
|
retries: options?.retries,
|
|
12658
13220
|
metadata: options?.metadata,
|
|
12659
13221
|
validate: options?.validate,
|
|
12660
|
-
validateRetries: options?.validateRetries
|
|
13222
|
+
validateRetries: options?.validateRetries,
|
|
13223
|
+
retryFeedback: options?.retryFeedback
|
|
12661
13224
|
});
|
|
12662
13225
|
}
|
|
12663
13226
|
const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
|
|
@@ -12705,7 +13268,8 @@ ${summaryResponse.content}`
|
|
|
12705
13268
|
retries: options?.retries,
|
|
12706
13269
|
metadata: options?.metadata,
|
|
12707
13270
|
validate: options?.validate,
|
|
12708
|
-
validateRetries: options?.validateRetries
|
|
13271
|
+
validateRetries: options?.validateRetries,
|
|
13272
|
+
retryFeedback: options?.retryFeedback
|
|
12709
13273
|
});
|
|
12710
13274
|
}
|
|
12711
13275
|
/** Transcribe recorded audio through the dedicated transcription registry.
|
|
@@ -16253,6 +16817,8 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
16253
16817
|
parent: parentScope
|
|
16254
16818
|
};
|
|
16255
16819
|
const modelCalls = /* @__PURE__ */ new Map();
|
|
16820
|
+
const modelTiming = /* @__PURE__ */ new Map();
|
|
16821
|
+
const captureTimingSamples = options?.captureTimingSamples === true;
|
|
16256
16822
|
const workflowCalls = /* @__PURE__ */ new Map();
|
|
16257
16823
|
const tokens = { input: 0, output: 0, reasoning: 0 };
|
|
16258
16824
|
let agentCalls = 0;
|
|
@@ -16270,6 +16836,30 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
16270
16836
|
tokens.output += event.tokens.output ?? 0;
|
|
16271
16837
|
tokens.reasoning += event.tokens.reasoning ?? 0;
|
|
16272
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
|
+
}
|
|
16273
16863
|
}
|
|
16274
16864
|
if (event.type === "workflow_start" && event.workflow) {
|
|
16275
16865
|
workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
|
|
@@ -16300,6 +16890,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
16300
16890
|
result,
|
|
16301
16891
|
cost: scope.totalCost,
|
|
16302
16892
|
unpriced,
|
|
16893
|
+
...modelTiming.size > 0 ? { modelTiming: Object.fromEntries(modelTiming) } : {},
|
|
16303
16894
|
...capturedTraces ? { traces: capturedTraces } : {},
|
|
16304
16895
|
metadata: {
|
|
16305
16896
|
models: [...modelCalls.keys()],
|