@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.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
|
},
|
|
@@ -2118,17 +2254,22 @@ function resolveOpenAIReasoningEffort(model, resolved) {
|
|
|
2118
2254
|
}
|
|
2119
2255
|
return resolved.thinkingDisabled ? clampReasoningEffort(model, "none") : void 0;
|
|
2120
2256
|
}
|
|
2121
|
-
var warnedChatMaxDowngrades = /* @__PURE__ */ new Set();
|
|
2122
2257
|
function resolveOpenAIChatReasoningEffort(model, resolved) {
|
|
2123
2258
|
const effort = resolveOpenAIReasoningEffort(model, resolved);
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
return
|
|
2259
|
+
return effort === "max" ? "xhigh" : effort;
|
|
2260
|
+
}
|
|
2261
|
+
function resolveOpenAIEffortResolution(options, resolveWireEffort, endpoint) {
|
|
2262
|
+
const resolved = resolveThinkingOptions(options);
|
|
2263
|
+
if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
|
|
2264
|
+
const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
|
|
2265
|
+
const effective = resolveWireEffort(model, resolved);
|
|
2266
|
+
if (effective === void 0 || effective === resolved.effort) return void 0;
|
|
2267
|
+
return {
|
|
2268
|
+
requested: resolved.effort,
|
|
2269
|
+
effective,
|
|
2270
|
+
clamped: true,
|
|
2271
|
+
cause: `${endpoint} does not accept reasoning effort '${resolved.effort}' for ${model}; using '${effective}'`
|
|
2272
|
+
};
|
|
2132
2273
|
}
|
|
2133
2274
|
var openaiReasoningEmit = (body, resolved, model) => {
|
|
2134
2275
|
const oSeries = isOSeriesModel(model);
|
|
@@ -2153,6 +2294,15 @@ var OpenAIProvider = class extends OpenAICompatibleProvider {
|
|
|
2153
2294
|
constructor(options = {}) {
|
|
2154
2295
|
super({ profile: OPENAI_PROFILE, ...options });
|
|
2155
2296
|
}
|
|
2297
|
+
/** Report a clamped `effort` — Chat Completions caps `'max'` at `'xhigh'` and
|
|
2298
|
+
* rejects `'none'` before gpt-5.1 (clamped to `'minimal'`). */
|
|
2299
|
+
effortResolution(options) {
|
|
2300
|
+
return resolveOpenAIEffortResolution(
|
|
2301
|
+
options,
|
|
2302
|
+
resolveOpenAIChatReasoningEffort,
|
|
2303
|
+
"OpenAI Chat Completions"
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2156
2306
|
computeCost(model, usage, _reportedCost, context) {
|
|
2157
2307
|
return usage ? estimateDirectOpenAICost(model, usage, { ...context, baseUrl: this.baseUrl }) : void 0;
|
|
2158
2308
|
}
|
|
@@ -2199,6 +2349,12 @@ var OpenAIResponsesProvider = class {
|
|
|
2199
2349
|
inputCapabilities(model) {
|
|
2200
2350
|
return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
|
|
2201
2351
|
}
|
|
2352
|
+
/** Report a clamped `effort`. Responses accepts the native `'max'` tier that
|
|
2353
|
+
* Chat Completions caps, but still clamps `'none'` before gpt-5.1, `'xhigh'`
|
|
2354
|
+
* below gpt-5.2, and everything to `'high'` on gpt-5-pro. */
|
|
2355
|
+
effortResolution(options) {
|
|
2356
|
+
return resolveOpenAIEffortResolution(options, resolveOpenAIReasoningEffort, "OpenAI Responses");
|
|
2357
|
+
}
|
|
2202
2358
|
validateInput(request) {
|
|
2203
2359
|
const modelOverride = request.providerOptions?.model;
|
|
2204
2360
|
const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
|
|
@@ -2276,6 +2432,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2276
2432
|
async chat(messages, options) {
|
|
2277
2433
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
2278
2434
|
const body = this.buildRequestBody(messages, options, false);
|
|
2435
|
+
const recorder = new CallTimingRecorder();
|
|
2279
2436
|
const res = await fetchWithRetry(
|
|
2280
2437
|
`${this.baseUrl}/responses`,
|
|
2281
2438
|
{
|
|
@@ -2284,7 +2441,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2284
2441
|
body: JSON.stringify(body),
|
|
2285
2442
|
signal: options.signal
|
|
2286
2443
|
},
|
|
2287
|
-
{ governor: this.governor, provider: this.name }
|
|
2444
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
2288
2445
|
);
|
|
2289
2446
|
if (!res.ok) {
|
|
2290
2447
|
const errorBody = await res.text();
|
|
@@ -2294,11 +2451,15 @@ var OpenAIResponsesProvider = class {
|
|
|
2294
2451
|
status: res.status,
|
|
2295
2452
|
headers: res.headers,
|
|
2296
2453
|
message,
|
|
2297
|
-
body: errorBody
|
|
2454
|
+
body: errorBody,
|
|
2455
|
+
timing: recorder.chatTiming()
|
|
2298
2456
|
});
|
|
2299
2457
|
}
|
|
2300
2458
|
const json = await res.json();
|
|
2301
|
-
return
|
|
2459
|
+
return withChatTiming(
|
|
2460
|
+
recorder,
|
|
2461
|
+
this.parseResponse(json, this.requestModel(body, options.model), body)
|
|
2462
|
+
);
|
|
2302
2463
|
}
|
|
2303
2464
|
// ---------------------------------------------------------------------------
|
|
2304
2465
|
// stream - SSE streaming
|
|
@@ -2306,6 +2467,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2306
2467
|
async *stream(messages, options) {
|
|
2307
2468
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
2308
2469
|
const body = this.buildRequestBody(messages, options, true);
|
|
2470
|
+
const recorder = new CallTimingRecorder();
|
|
2309
2471
|
const res = await fetchWithRetry(
|
|
2310
2472
|
`${this.baseUrl}/responses`,
|
|
2311
2473
|
{
|
|
@@ -2314,7 +2476,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2314
2476
|
body: JSON.stringify(body),
|
|
2315
2477
|
signal: options.signal
|
|
2316
2478
|
},
|
|
2317
|
-
{ governor: this.governor, provider: this.name }
|
|
2479
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
2318
2480
|
);
|
|
2319
2481
|
if (!res.ok) {
|
|
2320
2482
|
const errorBody = await res.text();
|
|
@@ -2324,13 +2486,17 @@ var OpenAIResponsesProvider = class {
|
|
|
2324
2486
|
status: res.status,
|
|
2325
2487
|
headers: res.headers,
|
|
2326
2488
|
message,
|
|
2327
|
-
body: errorBody
|
|
2489
|
+
body: errorBody,
|
|
2490
|
+
timing: recorder.chatTiming()
|
|
2328
2491
|
});
|
|
2329
2492
|
}
|
|
2330
2493
|
if (!res.body) {
|
|
2331
2494
|
throw new Error("OpenAI Responses stream has no body");
|
|
2332
2495
|
}
|
|
2333
|
-
yield*
|
|
2496
|
+
yield* withCallTiming(
|
|
2497
|
+
recorder,
|
|
2498
|
+
this.parseSSEStream(res.body, this.requestModel(body, options.model), body, recorder)
|
|
2499
|
+
);
|
|
2334
2500
|
}
|
|
2335
2501
|
// ---------------------------------------------------------------------------
|
|
2336
2502
|
// Internal: build request body
|
|
@@ -2532,7 +2698,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2532
2698
|
// ---------------------------------------------------------------------------
|
|
2533
2699
|
// Internal: parse SSE stream
|
|
2534
2700
|
// ---------------------------------------------------------------------------
|
|
2535
|
-
async *parseSSEStream(body, model, request) {
|
|
2701
|
+
async *parseSSEStream(body, model, request, timing) {
|
|
2536
2702
|
const reader = body.getReader();
|
|
2537
2703
|
const decoder = new TextDecoder();
|
|
2538
2704
|
let buffer = "";
|
|
@@ -2540,7 +2706,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2540
2706
|
let eventType = "";
|
|
2541
2707
|
try {
|
|
2542
2708
|
while (true) {
|
|
2543
|
-
const { done, value } = await
|
|
2709
|
+
const { done, value } = await timing.read(reader);
|
|
2544
2710
|
if (done) break;
|
|
2545
2711
|
buffer += decoder.decode(value, { stream: true });
|
|
2546
2712
|
const lines = buffer.split("\n");
|
|
@@ -2560,7 +2726,14 @@ var OpenAIResponsesProvider = class {
|
|
|
2560
2726
|
} catch {
|
|
2561
2727
|
continue;
|
|
2562
2728
|
}
|
|
2563
|
-
const chunk = this.handleStreamEvent(
|
|
2729
|
+
const chunk = this.handleStreamEvent(
|
|
2730
|
+
eventType,
|
|
2731
|
+
data,
|
|
2732
|
+
model,
|
|
2733
|
+
callIdMap,
|
|
2734
|
+
request,
|
|
2735
|
+
timing
|
|
2736
|
+
);
|
|
2564
2737
|
if (chunk) {
|
|
2565
2738
|
yield chunk;
|
|
2566
2739
|
if (chunk.type === "done") return;
|
|
@@ -2574,7 +2747,7 @@ var OpenAIResponsesProvider = class {
|
|
|
2574
2747
|
reader.releaseLock();
|
|
2575
2748
|
}
|
|
2576
2749
|
}
|
|
2577
|
-
handleStreamEvent(eventType, data, model, callIdMap, request) {
|
|
2750
|
+
handleStreamEvent(eventType, data, model, callIdMap, request, timing) {
|
|
2578
2751
|
switch (eventType) {
|
|
2579
2752
|
case "response.output_text.delta":
|
|
2580
2753
|
return { type: "text_delta", content: data.delta ?? "" };
|
|
@@ -2630,7 +2803,8 @@ var OpenAIResponsesProvider = class {
|
|
|
2630
2803
|
provider: this.name,
|
|
2631
2804
|
status: 0,
|
|
2632
2805
|
retryable: false,
|
|
2633
|
-
message: `OpenAI Responses API error: ${errorMsg}
|
|
2806
|
+
message: `OpenAI Responses API error: ${errorMsg}`,
|
|
2807
|
+
timing: timing.streamTiming()
|
|
2634
2808
|
});
|
|
2635
2809
|
}
|
|
2636
2810
|
default:
|
|
@@ -2698,6 +2872,12 @@ function hasAnthropicProviderFileImage(messages) {
|
|
|
2698
2872
|
);
|
|
2699
2873
|
}
|
|
2700
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
|
+
},
|
|
2701
2881
|
"claude-fable-5": {
|
|
2702
2882
|
thinking: "adaptive-always-on",
|
|
2703
2883
|
effortLevels: ["low", "medium", "high", "xhigh", "max"],
|
|
@@ -2788,10 +2968,18 @@ var PRE_INFERENCE_GEO_MODELS = /* @__PURE__ */ new Set([
|
|
|
2788
2968
|
function resolveClaudeCapability(model) {
|
|
2789
2969
|
return CLAUDE_CAPABILITIES[model] ?? (LEGACY_CLAUDE_MODELS.has(model) ? LEGACY_CLAUDE_CAPABILITY : void 0);
|
|
2790
2970
|
}
|
|
2971
|
+
var DEFAULT_CACHE_READ_MULTIPLIER = 0.1;
|
|
2791
2972
|
var ANTHROPIC_RATES = {
|
|
2973
|
+
"claude-fable-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
|
|
2792
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 },
|
|
2793
2981
|
"claude-opus-5": { input: 5e-6, output: 25e-6 },
|
|
2794
|
-
"claude-sonnet-5": { input:
|
|
2982
|
+
"claude-sonnet-5": { input: 2e-6, output: 1e-5 },
|
|
2795
2983
|
"claude-opus-4-8": { input: 5e-6, output: 25e-6 },
|
|
2796
2984
|
"claude-opus-4-7": { input: 5e-6, output: 25e-6 },
|
|
2797
2985
|
"claude-opus-4-6": { input: 5e-6, output: 25e-6 },
|
|
@@ -2830,7 +3018,7 @@ function anthropicStreamErrorStatus(type) {
|
|
|
2830
3018
|
return 0;
|
|
2831
3019
|
}
|
|
2832
3020
|
}
|
|
2833
|
-
function estimateAnthropicCost(model, usage
|
|
3021
|
+
function estimateAnthropicCost(model, usage) {
|
|
2834
3022
|
if (!model || !isValidTokenCount(usage.inputTokens) || !isValidTokenCount(usage.outputTokens)) {
|
|
2835
3023
|
return void 0;
|
|
2836
3024
|
}
|
|
@@ -2849,8 +3037,9 @@ function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
|
|
|
2849
3037
|
if (usage.aggregateCacheWriteTokens !== void 0 && (!isValidTokenCount(usage.aggregateCacheWriteTokens) || usage.aggregateCacheWriteTokens !== cacheWriteTotal)) {
|
|
2850
3038
|
return void 0;
|
|
2851
3039
|
}
|
|
2852
|
-
const { input, output } = rate
|
|
2853
|
-
|
|
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;
|
|
2854
3043
|
}
|
|
2855
3044
|
var THINKING_BUDGETS = {
|
|
2856
3045
|
low: 1024,
|
|
@@ -2874,14 +3063,6 @@ function budgetToClaudeEffort(budget) {
|
|
|
2874
3063
|
if (budget <= THINKING_BUDGETS.xhigh) return "xhigh";
|
|
2875
3064
|
return "max";
|
|
2876
3065
|
}
|
|
2877
|
-
var warnedFableDisable = /* @__PURE__ */ new Set();
|
|
2878
|
-
function warnFableDisable(model) {
|
|
2879
|
-
if (warnedFableDisable.has(model)) return;
|
|
2880
|
-
warnedFableDisable.add(model);
|
|
2881
|
-
console.warn(
|
|
2882
|
-
`[axl] thinking cannot be disabled on Anthropic ${model}; using adaptive thinking at effort 'low'.`
|
|
2883
|
-
);
|
|
2884
|
-
}
|
|
2885
3066
|
function resolveClaudeThinking(model, resolved) {
|
|
2886
3067
|
const capability = resolveClaudeCapability(model);
|
|
2887
3068
|
if (!capability) return { stripTemperature: false };
|
|
@@ -2901,11 +3082,14 @@ function resolveClaudeThinking(model, resolved) {
|
|
|
2901
3082
|
}
|
|
2902
3083
|
if (resolved.thinkingDisabled) {
|
|
2903
3084
|
if (capability.thinking === "adaptive-always-on") {
|
|
2904
|
-
warnFableDisable(model);
|
|
2905
3085
|
return {
|
|
2906
3086
|
thinking: { type: "adaptive" },
|
|
2907
3087
|
outputConfig: { effort: "low" },
|
|
2908
|
-
stripTemperature: true
|
|
3088
|
+
stripTemperature: true,
|
|
3089
|
+
clamp: {
|
|
3090
|
+
effective: "low",
|
|
3091
|
+
cause: `thinking cannot be disabled on Anthropic ${model}; using adaptive thinking at effort 'low'`
|
|
3092
|
+
}
|
|
2909
3093
|
};
|
|
2910
3094
|
}
|
|
2911
3095
|
if (activeEffort && capability.disableAt && !isEffortAtOrBelow(activeEffort, capability.disableAt)) {
|
|
@@ -2967,7 +3151,16 @@ function resolveClaudeThinking(model, resolved) {
|
|
|
2967
3151
|
return {
|
|
2968
3152
|
thinking: { type: "enabled", budget_tokens: budget },
|
|
2969
3153
|
manualBudget: budget,
|
|
2970
|
-
stripTemperature: true
|
|
3154
|
+
stripTemperature: true,
|
|
3155
|
+
// Legacy models take `budget_tokens`, not an effort level, and have no
|
|
3156
|
+
// `effortLevels` — so `clampAnthropicEffort` folds 'max'/'xhigh' down to
|
|
3157
|
+
// the 'high' tier and the request silently carries that tier's budget.
|
|
3158
|
+
// Report it: an unreported budget-tier downgrade is exactly the silent
|
|
3159
|
+
// clamp this capability exists to expose.
|
|
3160
|
+
clamp: activeEffort === resolved.activeEffort ? void 0 : {
|
|
3161
|
+
effective: activeEffort,
|
|
3162
|
+
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}`
|
|
3163
|
+
}
|
|
2971
3164
|
};
|
|
2972
3165
|
}
|
|
2973
3166
|
return { stripTemperature: capability.stripTemperature ?? false };
|
|
@@ -3078,6 +3271,34 @@ function mergeAnthropicUsage(initial, update) {
|
|
|
3078
3271
|
}
|
|
3079
3272
|
var AnthropicProvider = class {
|
|
3080
3273
|
name = "anthropic";
|
|
3274
|
+
/** Report a clamped `effort`. Anthropic clamps to each model's supported
|
|
3275
|
+
* `output_config.effort` levels, and models that always think fall back to
|
|
3276
|
+
* adaptive `'low'` when asked to disable thinking. Only reported when the
|
|
3277
|
+
* effective level is what the request body actually carries; an explicit
|
|
3278
|
+
* `thinkingBudget` is documented to override `effort`, so it is not a clamp. */
|
|
3279
|
+
effortResolution(options) {
|
|
3280
|
+
const resolved = resolveThinkingOptions(options);
|
|
3281
|
+
if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
|
|
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
|
+
}
|
|
3291
|
+
const config = resolveClaudeThinking(model, resolved);
|
|
3292
|
+
const wireEffort = typeof config.outputConfig?.effort === "string" ? config.outputConfig.effort : void 0;
|
|
3293
|
+
const effective = config.clamp?.effective ?? wireEffort;
|
|
3294
|
+
if (effective === void 0 || effective === resolved.effort) return void 0;
|
|
3295
|
+
return {
|
|
3296
|
+
requested: resolved.effort,
|
|
3297
|
+
effective,
|
|
3298
|
+
clamped: true,
|
|
3299
|
+
cause: config.clamp?.cause ?? `Anthropic ${model} does not support effort '${resolved.effort}'; using '${effective}'`
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3081
3302
|
inputCapabilities(model) {
|
|
3082
3303
|
return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
|
|
3083
3304
|
}
|
|
@@ -3122,6 +3343,10 @@ var AnthropicProvider = class {
|
|
|
3122
3343
|
}
|
|
3123
3344
|
/** Anthropic ignores native `json_schema` structurally — Axl uses a system
|
|
3124
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
|
+
}
|
|
3125
3350
|
nativeStructuredOutputSupport() {
|
|
3126
3351
|
return "unsupported";
|
|
3127
3352
|
}
|
|
@@ -3163,6 +3388,7 @@ var AnthropicProvider = class {
|
|
|
3163
3388
|
);
|
|
3164
3389
|
const body = this.buildRequestBody(messages, options, false);
|
|
3165
3390
|
const pricingContext = pricingContextFromBody(body);
|
|
3391
|
+
const recorder = new CallTimingRecorder();
|
|
3166
3392
|
const res = await fetchWithRetry(
|
|
3167
3393
|
`${this.baseUrl}/messages`,
|
|
3168
3394
|
{
|
|
@@ -3171,7 +3397,7 @@ var AnthropicProvider = class {
|
|
|
3171
3397
|
body: JSON.stringify(body),
|
|
3172
3398
|
signal: options.signal
|
|
3173
3399
|
},
|
|
3174
|
-
{ governor: this.governor, provider: this.name }
|
|
3400
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
3175
3401
|
);
|
|
3176
3402
|
if (!res.ok) {
|
|
3177
3403
|
const errorBody = await res.text();
|
|
@@ -3181,11 +3407,12 @@ var AnthropicProvider = class {
|
|
|
3181
3407
|
status: res.status,
|
|
3182
3408
|
headers: res.headers,
|
|
3183
3409
|
message,
|
|
3184
|
-
body: errorBody
|
|
3410
|
+
body: errorBody,
|
|
3411
|
+
timing: recorder.chatTiming()
|
|
3185
3412
|
});
|
|
3186
3413
|
}
|
|
3187
3414
|
const json = await res.json();
|
|
3188
|
-
return this.parseResponse(json, pricingContext);
|
|
3415
|
+
return withChatTiming(recorder, this.parseResponse(json, pricingContext));
|
|
3189
3416
|
}
|
|
3190
3417
|
// ---------------------------------------------------------------------------
|
|
3191
3418
|
// stream - SSE streaming completion
|
|
@@ -3197,6 +3424,7 @@ var AnthropicProvider = class {
|
|
|
3197
3424
|
);
|
|
3198
3425
|
const body = this.buildRequestBody(messages, options, true);
|
|
3199
3426
|
const pricingContext = pricingContextFromBody(body);
|
|
3427
|
+
const recorder = new CallTimingRecorder();
|
|
3200
3428
|
const res = await fetchWithRetry(
|
|
3201
3429
|
`${this.baseUrl}/messages`,
|
|
3202
3430
|
{
|
|
@@ -3205,7 +3433,7 @@ var AnthropicProvider = class {
|
|
|
3205
3433
|
body: JSON.stringify(body),
|
|
3206
3434
|
signal: options.signal
|
|
3207
3435
|
},
|
|
3208
|
-
{ governor: this.governor, provider: this.name }
|
|
3436
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
3209
3437
|
);
|
|
3210
3438
|
if (!res.ok) {
|
|
3211
3439
|
const errorBody = await res.text();
|
|
@@ -3215,13 +3443,17 @@ var AnthropicProvider = class {
|
|
|
3215
3443
|
status: res.status,
|
|
3216
3444
|
headers: res.headers,
|
|
3217
3445
|
message,
|
|
3218
|
-
body: errorBody
|
|
3446
|
+
body: errorBody,
|
|
3447
|
+
timing: recorder.chatTiming()
|
|
3219
3448
|
});
|
|
3220
3449
|
}
|
|
3221
3450
|
if (!res.body) {
|
|
3222
3451
|
throw new Error("Anthropic stream response has no body");
|
|
3223
3452
|
}
|
|
3224
|
-
yield*
|
|
3453
|
+
yield* withCallTiming(
|
|
3454
|
+
recorder,
|
|
3455
|
+
this.parseSSEStream(res.body, pricingContext, res.headers, recorder)
|
|
3456
|
+
);
|
|
3225
3457
|
}
|
|
3226
3458
|
// ---------------------------------------------------------------------------
|
|
3227
3459
|
// Internal: request building
|
|
@@ -3256,14 +3488,31 @@ var AnthropicProvider = class {
|
|
|
3256
3488
|
max_tokens: options.maxTokens ?? 4096,
|
|
3257
3489
|
stream
|
|
3258
3490
|
};
|
|
3259
|
-
|
|
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) {
|
|
3260
3502
|
body.system = systemText;
|
|
3261
3503
|
}
|
|
3262
3504
|
if (options.stop) {
|
|
3263
3505
|
body.stop_sequences = options.stop;
|
|
3264
3506
|
}
|
|
3265
3507
|
if (options.tools && options.tools.length > 0) {
|
|
3266
|
-
|
|
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;
|
|
3267
3516
|
}
|
|
3268
3517
|
if (options.toolChoice !== void 0) {
|
|
3269
3518
|
body.tool_choice = this.mapToolChoice(options.toolChoice);
|
|
@@ -3282,9 +3531,13 @@ var AnthropicProvider = class {
|
|
|
3282
3531
|
}
|
|
3283
3532
|
if (options.responseFormat && options.responseFormat.type !== "text") {
|
|
3284
3533
|
const jsonInstruction = "You must respond with valid JSON only. No markdown fences, no extra text.";
|
|
3285
|
-
|
|
3534
|
+
if (Array.isArray(body.system)) {
|
|
3535
|
+
body.system = [...body.system, { type: "text", text: jsonInstruction }];
|
|
3536
|
+
} else {
|
|
3537
|
+
body.system = body.system ? `${body.system}
|
|
3286
3538
|
|
|
3287
3539
|
${jsonInstruction}` : jsonInstruction;
|
|
3540
|
+
}
|
|
3288
3541
|
}
|
|
3289
3542
|
if (options.providerOptions) {
|
|
3290
3543
|
Object.assign(body, options.providerOptions);
|
|
@@ -3464,7 +3717,7 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3464
3717
|
// ---------------------------------------------------------------------------
|
|
3465
3718
|
// Internal: SSE stream parsing
|
|
3466
3719
|
// ---------------------------------------------------------------------------
|
|
3467
|
-
async *parseSSEStream(body, pricingContext, responseHeaders) {
|
|
3720
|
+
async *parseSSEStream(body, pricingContext, responseHeaders, timing) {
|
|
3468
3721
|
const reader = body.getReader();
|
|
3469
3722
|
const decoder = new TextDecoder();
|
|
3470
3723
|
let buffer = "";
|
|
@@ -3493,7 +3746,7 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3493
3746
|
});
|
|
3494
3747
|
try {
|
|
3495
3748
|
while (true) {
|
|
3496
|
-
const { done, value } = await
|
|
3749
|
+
const { done, value } = await timing.read(reader);
|
|
3497
3750
|
if (done) break;
|
|
3498
3751
|
buffer += decoder.decode(value, { stream: true });
|
|
3499
3752
|
const lines = buffer.split("\n");
|
|
@@ -3516,7 +3769,8 @@ ${jsonInstruction}` : jsonInstruction;
|
|
|
3516
3769
|
provider: this.name,
|
|
3517
3770
|
status,
|
|
3518
3771
|
headers: responseHeaders,
|
|
3519
|
-
message: event.error?.message ?? "Anthropic stream failed"
|
|
3772
|
+
message: event.error?.message ?? "Anthropic stream failed",
|
|
3773
|
+
timing: timing.streamTiming()
|
|
3520
3774
|
});
|
|
3521
3775
|
}
|
|
3522
3776
|
case "content_block_start": {
|
|
@@ -3752,7 +4006,6 @@ var GEMINI_PRICING = {
|
|
|
3752
4006
|
},
|
|
3753
4007
|
"gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7 },
|
|
3754
4008
|
"gemini-2.5-flash-lite": { input: 1e-7, cached: 1e-8, output: 4e-7 },
|
|
3755
|
-
"gemini-3-flash-preview": { input: 5e-7, cached: 5e-8, output: 3e-6 },
|
|
3756
4009
|
"gemini-3.1-pro-preview": {
|
|
3757
4010
|
input: 2e-6,
|
|
3758
4011
|
cached: 2e-7,
|
|
@@ -3768,7 +4021,9 @@ var GEMINI_PRICING = {
|
|
|
3768
4021
|
"gemini-3.1-flash-lite": { input: 25e-8, cached: 25e-9, output: 15e-7 },
|
|
3769
4022
|
"gemini-3.5-flash": { input: 15e-7, cached: 15e-8, output: 9e-6 },
|
|
3770
4023
|
"gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
|
|
3771
|
-
"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 }
|
|
3772
4027
|
};
|
|
3773
4028
|
function isValidTokenCount2(value) {
|
|
3774
4029
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
@@ -3928,13 +4183,71 @@ function budgetToThinkingLevel(budgetTokens) {
|
|
|
3928
4183
|
function minThinkingLevel(model) {
|
|
3929
4184
|
return GEMINI_3X_MIN_THINKING_LEVEL.get(model) ?? "minimal";
|
|
3930
4185
|
}
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
if (
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
4186
|
+
function resolveGeminiThinking(model, resolved) {
|
|
4187
|
+
const is3x = isGemini3x(model);
|
|
4188
|
+
if (resolved.thinkingDisabled) {
|
|
4189
|
+
const minLevel = minThinkingLevel(model);
|
|
4190
|
+
if (is3x) {
|
|
4191
|
+
return {
|
|
4192
|
+
interactionLevel: minLevel,
|
|
4193
|
+
thinkingLevel: minLevel,
|
|
4194
|
+
thinkingBudget: void 0,
|
|
4195
|
+
attachThoughts: false,
|
|
4196
|
+
// A `thinkingBudget: 0` disable has no unified effort to report against;
|
|
4197
|
+
// only an explicit `effort: 'none'` is a clamp of a requested value.
|
|
4198
|
+
clamp: resolved.effort === "none" ? {
|
|
4199
|
+
effective: minLevel,
|
|
4200
|
+
cause: `Gemini 3.x models cannot disable thinking; using the model minimum thinking level '${minLevel}'`
|
|
4201
|
+
} : void 0
|
|
4202
|
+
};
|
|
4203
|
+
}
|
|
4204
|
+
return {
|
|
4205
|
+
interactionLevel: "minimal",
|
|
4206
|
+
thinkingLevel: void 0,
|
|
4207
|
+
thinkingBudget: 0,
|
|
4208
|
+
attachThoughts: false
|
|
4209
|
+
};
|
|
4210
|
+
}
|
|
4211
|
+
if (resolved.hasBudgetOverride) {
|
|
4212
|
+
const budget = resolved.thinkingBudget;
|
|
4213
|
+
const level = budgetToThinkingLevel(budget);
|
|
4214
|
+
return {
|
|
4215
|
+
interactionLevel: level,
|
|
4216
|
+
thinkingLevel: is3x ? level : void 0,
|
|
4217
|
+
thinkingBudget: is3x ? void 0 : budget,
|
|
4218
|
+
attachThoughts: resolved.includeThoughts
|
|
4219
|
+
};
|
|
4220
|
+
}
|
|
4221
|
+
if (resolved.activeEffort) {
|
|
4222
|
+
const effort = resolved.activeEffort;
|
|
4223
|
+
const budget = THINKING_BUDGETS2[effort] ?? 5e3;
|
|
4224
|
+
if (is3x) {
|
|
4225
|
+
const level = THINKING_LEVELS[effort] ?? "medium";
|
|
4226
|
+
return {
|
|
4227
|
+
interactionLevel: level,
|
|
4228
|
+
thinkingLevel: level,
|
|
4229
|
+
thinkingBudget: void 0,
|
|
4230
|
+
attachThoughts: resolved.includeThoughts,
|
|
4231
|
+
clamp: level === effort ? void 0 : {
|
|
4232
|
+
effective: level,
|
|
4233
|
+
cause: `Gemini 3.x thinking levels cap at 'high'; effort '${effort}' maps to '${level}'`
|
|
4234
|
+
}
|
|
4235
|
+
};
|
|
4236
|
+
}
|
|
4237
|
+
return {
|
|
4238
|
+
interactionLevel: budgetToThinkingLevel(budget),
|
|
4239
|
+
thinkingLevel: void 0,
|
|
4240
|
+
// 2.5 Pro supports a higher max budget (32768) than other 2.5 models (24576).
|
|
4241
|
+
thinkingBudget: effort === "max" && model === "gemini-2.5-pro" ? 32768 : budget,
|
|
4242
|
+
attachThoughts: resolved.includeThoughts
|
|
4243
|
+
};
|
|
4244
|
+
}
|
|
4245
|
+
return {
|
|
4246
|
+
interactionLevel: void 0,
|
|
4247
|
+
thinkingLevel: void 0,
|
|
4248
|
+
thinkingBudget: void 0,
|
|
4249
|
+
attachThoughts: resolved.includeThoughts
|
|
4250
|
+
};
|
|
3938
4251
|
}
|
|
3939
4252
|
var GeminiProvider = class {
|
|
3940
4253
|
name = "google";
|
|
@@ -4003,6 +4316,21 @@ var GeminiProvider = class {
|
|
|
4003
4316
|
nativeStructuredOutputSupport() {
|
|
4004
4317
|
return "lossy";
|
|
4005
4318
|
}
|
|
4319
|
+
/** Report a clamped `effort` (R7a). Gemini 3.x cannot disable thinking and
|
|
4320
|
+
* caps at `'high'`, so `'none'`, `'xhigh'` and `'max'` are not honored
|
|
4321
|
+
* verbatim there. Everything else — including 2.x disabled thinking — is
|
|
4322
|
+
* sent as asked, so there is nothing to report. */
|
|
4323
|
+
effortResolution(options) {
|
|
4324
|
+
const resolved = resolveThinkingOptions(options);
|
|
4325
|
+
if (resolved.effort === void 0) return void 0;
|
|
4326
|
+
const clamp = resolveGeminiThinking(this.requestModel(options), resolved).clamp;
|
|
4327
|
+
return clamp ? {
|
|
4328
|
+
requested: resolved.effort,
|
|
4329
|
+
effective: clamp.effective,
|
|
4330
|
+
clamped: true,
|
|
4331
|
+
cause: clamp.cause
|
|
4332
|
+
} : void 0;
|
|
4333
|
+
}
|
|
4006
4334
|
baseUrl;
|
|
4007
4335
|
apiKeySource;
|
|
4008
4336
|
callCounter = 0;
|
|
@@ -4040,6 +4368,7 @@ var GeminiProvider = class {
|
|
|
4040
4368
|
const pricingContext = this.pricingContext(this.requestModel(options), body);
|
|
4041
4369
|
this.assertSafeGemini3Continuation(messages, pricingContext.model);
|
|
4042
4370
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4371
|
+
const recorder = new CallTimingRecorder();
|
|
4043
4372
|
const res = await fetchWithRetry(
|
|
4044
4373
|
`${this.baseUrl}/models/${pricingContext.model}:generateContent`,
|
|
4045
4374
|
{
|
|
@@ -4048,7 +4377,7 @@ var GeminiProvider = class {
|
|
|
4048
4377
|
body: JSON.stringify(body),
|
|
4049
4378
|
signal: options.signal
|
|
4050
4379
|
},
|
|
4051
|
-
{ governor: this.governor, provider: this.name }
|
|
4380
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4052
4381
|
);
|
|
4053
4382
|
if (!res.ok) {
|
|
4054
4383
|
const errorBody = await res.text();
|
|
@@ -4058,11 +4387,12 @@ var GeminiProvider = class {
|
|
|
4058
4387
|
status: res.status,
|
|
4059
4388
|
headers: res.headers,
|
|
4060
4389
|
message,
|
|
4061
|
-
body: errorBody
|
|
4390
|
+
body: errorBody,
|
|
4391
|
+
timing: recorder.chatTiming()
|
|
4062
4392
|
});
|
|
4063
4393
|
}
|
|
4064
4394
|
const json = await res.json();
|
|
4065
|
-
return this.parseResponse(json, pricingContext);
|
|
4395
|
+
return withChatTiming(recorder, this.parseResponse(json, pricingContext));
|
|
4066
4396
|
}
|
|
4067
4397
|
// ---------------------------------------------------------------------------
|
|
4068
4398
|
// stream - SSE streaming completion
|
|
@@ -4076,6 +4406,7 @@ var GeminiProvider = class {
|
|
|
4076
4406
|
const pricingContext = this.pricingContext(this.requestModel(options), body);
|
|
4077
4407
|
this.assertSafeGemini3Continuation(messages, pricingContext.model);
|
|
4078
4408
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4409
|
+
const recorder = new CallTimingRecorder();
|
|
4079
4410
|
const res = await fetchWithRetry(
|
|
4080
4411
|
`${this.baseUrl}/models/${pricingContext.model}:streamGenerateContent?alt=sse`,
|
|
4081
4412
|
{
|
|
@@ -4084,7 +4415,7 @@ var GeminiProvider = class {
|
|
|
4084
4415
|
body: JSON.stringify(body),
|
|
4085
4416
|
signal: options.signal
|
|
4086
4417
|
},
|
|
4087
|
-
{ governor: this.governor, provider: this.name }
|
|
4418
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4088
4419
|
);
|
|
4089
4420
|
if (!res.ok) {
|
|
4090
4421
|
const errorBody = await res.text();
|
|
@@ -4094,13 +4425,14 @@ var GeminiProvider = class {
|
|
|
4094
4425
|
status: res.status,
|
|
4095
4426
|
headers: res.headers,
|
|
4096
4427
|
message,
|
|
4097
|
-
body: errorBody
|
|
4428
|
+
body: errorBody,
|
|
4429
|
+
timing: recorder.chatTiming()
|
|
4098
4430
|
});
|
|
4099
4431
|
}
|
|
4100
4432
|
if (!res.body) {
|
|
4101
4433
|
throw new Error("Gemini stream response has no body");
|
|
4102
4434
|
}
|
|
4103
|
-
yield* this.parseSSEStream(res.body, pricingContext);
|
|
4435
|
+
yield* withCallTiming(recorder, this.parseSSEStream(res.body, pricingContext, recorder));
|
|
4104
4436
|
}
|
|
4105
4437
|
// ---------------------------------------------------------------------------
|
|
4106
4438
|
// Rich image requests use Gemini Interactions. It is deliberately stateless:
|
|
@@ -4111,10 +4443,11 @@ var GeminiProvider = class {
|
|
|
4111
4443
|
this.assertSafeInteractionOptions(options);
|
|
4112
4444
|
const body = this.buildInteractionRequestBody(messages, options, false);
|
|
4113
4445
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4446
|
+
const recorder = new CallTimingRecorder();
|
|
4114
4447
|
const res = await fetchWithRetry(
|
|
4115
4448
|
`${this.baseUrl}/interactions`,
|
|
4116
4449
|
{ method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
|
|
4117
|
-
{ governor: this.governor, provider: this.name }
|
|
4450
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4118
4451
|
);
|
|
4119
4452
|
if (!res.ok) {
|
|
4120
4453
|
const errorBody = await res.text();
|
|
@@ -4123,19 +4456,24 @@ var GeminiProvider = class {
|
|
|
4123
4456
|
status: res.status,
|
|
4124
4457
|
headers: res.headers,
|
|
4125
4458
|
message: this.extractErrorMessage(errorBody, res.status),
|
|
4126
|
-
body: errorBody
|
|
4459
|
+
body: errorBody,
|
|
4460
|
+
timing: recorder.chatTiming()
|
|
4127
4461
|
});
|
|
4128
4462
|
}
|
|
4129
|
-
return
|
|
4463
|
+
return withChatTiming(
|
|
4464
|
+
recorder,
|
|
4465
|
+
this.parseInteractionResponse(await res.json())
|
|
4466
|
+
);
|
|
4130
4467
|
}
|
|
4131
4468
|
async *streamInteraction(messages, options) {
|
|
4132
4469
|
this.assertSafeInteractionOptions(options);
|
|
4133
4470
|
const body = this.buildInteractionRequestBody(messages, options, true);
|
|
4134
4471
|
const headers = this.buildHeaders(await this.resolveKey());
|
|
4472
|
+
const recorder = new CallTimingRecorder();
|
|
4135
4473
|
const res = await fetchWithRetry(
|
|
4136
4474
|
`${this.baseUrl}/interactions?alt=sse`,
|
|
4137
4475
|
{ method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
|
|
4138
|
-
{ governor: this.governor, provider: this.name }
|
|
4476
|
+
{ governor: this.governor, provider: this.name, timing: recorder.observer }
|
|
4139
4477
|
);
|
|
4140
4478
|
if (!res.ok) {
|
|
4141
4479
|
const errorBody = await res.text();
|
|
@@ -4144,11 +4482,15 @@ var GeminiProvider = class {
|
|
|
4144
4482
|
status: res.status,
|
|
4145
4483
|
headers: res.headers,
|
|
4146
4484
|
message: this.extractErrorMessage(errorBody, res.status),
|
|
4147
|
-
body: errorBody
|
|
4485
|
+
body: errorBody,
|
|
4486
|
+
timing: recorder.chatTiming()
|
|
4148
4487
|
});
|
|
4149
4488
|
}
|
|
4150
4489
|
if (!res.body) throw new Error("Gemini Interactions stream has no body");
|
|
4151
|
-
yield*
|
|
4490
|
+
yield* withCallTiming(
|
|
4491
|
+
recorder,
|
|
4492
|
+
this.parseInteractionSSEStream(res.body, res.headers, recorder)
|
|
4493
|
+
);
|
|
4152
4494
|
}
|
|
4153
4495
|
buildInteractionRequestBody(messages, options, stream) {
|
|
4154
4496
|
const model = this.requestModel(options);
|
|
@@ -4177,12 +4519,9 @@ var GeminiProvider = class {
|
|
|
4177
4519
|
generationConfig.tool_choice = typeof options.toolChoice === "string" ? options.toolChoice === "required" ? "any" : options.toolChoice : { allowed_tools: { mode: "any", tools: [options.toolChoice.function.name] } };
|
|
4178
4520
|
}
|
|
4179
4521
|
const thinking = resolveThinkingOptions(options);
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
generationConfig.thinking_level = budgetToThinkingLevel(thinking.thinkingBudget);
|
|
4184
|
-
} else if (thinking.activeEffort) {
|
|
4185
|
-
generationConfig.thinking_level = isGemini3x(model) ? THINKING_LEVELS[thinking.activeEffort] ?? "medium" : budgetToThinkingLevel(THINKING_BUDGETS2[thinking.activeEffort] ?? 5e3);
|
|
4522
|
+
const thinkingPlan = resolveGeminiThinking(model, thinking);
|
|
4523
|
+
if (thinkingPlan.interactionLevel !== void 0) {
|
|
4524
|
+
generationConfig.thinking_level = thinkingPlan.interactionLevel;
|
|
4186
4525
|
}
|
|
4187
4526
|
if (thinking.includeThoughts) generationConfig.thinking_summaries = "auto";
|
|
4188
4527
|
if (Object.keys(generationConfig).length) body.generation_config = generationConfig;
|
|
@@ -4307,7 +4646,7 @@ var GeminiProvider = class {
|
|
|
4307
4646
|
providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
|
|
4308
4647
|
};
|
|
4309
4648
|
}
|
|
4310
|
-
async *parseInteractionSSEStream(body, responseHeaders) {
|
|
4649
|
+
async *parseInteractionSSEStream(body, responseHeaders, timing) {
|
|
4311
4650
|
const reader = body.getReader();
|
|
4312
4651
|
const decoder = new TextDecoder();
|
|
4313
4652
|
let buffer = "";
|
|
@@ -4317,7 +4656,7 @@ var GeminiProvider = class {
|
|
|
4317
4656
|
let completed = false;
|
|
4318
4657
|
try {
|
|
4319
4658
|
while (true) {
|
|
4320
|
-
const { done, value } = await
|
|
4659
|
+
const { done, value } = await timing.read(reader);
|
|
4321
4660
|
if (done) break;
|
|
4322
4661
|
buffer += decoder.decode(value, { stream: true });
|
|
4323
4662
|
const lines = buffer.split("\n");
|
|
@@ -4340,14 +4679,16 @@ var GeminiProvider = class {
|
|
|
4340
4679
|
provider: this.name,
|
|
4341
4680
|
status,
|
|
4342
4681
|
headers: responseHeaders,
|
|
4343
|
-
message
|
|
4682
|
+
message,
|
|
4683
|
+
timing: timing.streamTiming()
|
|
4344
4684
|
});
|
|
4345
4685
|
}
|
|
4346
4686
|
throw new ProviderError({
|
|
4347
4687
|
provider: this.name,
|
|
4348
4688
|
status: 0,
|
|
4349
4689
|
retryable: false,
|
|
4350
|
-
message
|
|
4690
|
+
message,
|
|
4691
|
+
timing: timing.streamTiming()
|
|
4351
4692
|
});
|
|
4352
4693
|
}
|
|
4353
4694
|
if (event.event_type === "step.start" && event.step) {
|
|
@@ -4618,50 +4959,17 @@ var GeminiProvider = class {
|
|
|
4618
4959
|
if (Object.keys(generationConfig).length > 0) {
|
|
4619
4960
|
body.generationConfig = generationConfig;
|
|
4620
4961
|
}
|
|
4621
|
-
const
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
}
|
|
4629
|
-
if (
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
warnGemini3xEffortNone(requestModel);
|
|
4633
|
-
}
|
|
4634
|
-
generationConfig.thinkingConfig = { thinkingLevel: minThinkingLevel(requestModel) };
|
|
4635
|
-
} else {
|
|
4636
|
-
generationConfig.thinkingConfig = { thinkingBudget: 0 };
|
|
4637
|
-
}
|
|
4638
|
-
if (!body.generationConfig) body.generationConfig = generationConfig;
|
|
4639
|
-
} else if (hasBudgetOverride) {
|
|
4640
|
-
const config = {};
|
|
4641
|
-
if (isGemini3x(requestModel)) {
|
|
4642
|
-
config.thinkingLevel = budgetToThinkingLevel(thinkingBudget);
|
|
4643
|
-
} else {
|
|
4644
|
-
config.thinkingBudget = thinkingBudget;
|
|
4645
|
-
}
|
|
4646
|
-
if (includeThoughts) config.includeThoughts = true;
|
|
4647
|
-
generationConfig.thinkingConfig = config;
|
|
4648
|
-
if (!body.generationConfig) body.generationConfig = generationConfig;
|
|
4649
|
-
} else if (activeEffort) {
|
|
4650
|
-
const config = {};
|
|
4651
|
-
if (isGemini3x(requestModel)) {
|
|
4652
|
-
config.thinkingLevel = THINKING_LEVELS[activeEffort] ?? "medium";
|
|
4653
|
-
} else {
|
|
4654
|
-
if (activeEffort === "max" && requestModel === "gemini-2.5-pro") {
|
|
4655
|
-
config.thinkingBudget = 32768;
|
|
4656
|
-
} else {
|
|
4657
|
-
config.thinkingBudget = THINKING_BUDGETS2[activeEffort] ?? 5e3;
|
|
4658
|
-
}
|
|
4659
|
-
}
|
|
4660
|
-
if (includeThoughts) config.includeThoughts = true;
|
|
4661
|
-
generationConfig.thinkingConfig = config;
|
|
4662
|
-
if (!body.generationConfig) body.generationConfig = generationConfig;
|
|
4663
|
-
} else if (includeThoughts) {
|
|
4664
|
-
generationConfig.thinkingConfig = { includeThoughts: true };
|
|
4962
|
+
const thinkingPlan = resolveGeminiThinking(requestModel, resolveThinkingOptions(options));
|
|
4963
|
+
const thinkingConfig = {};
|
|
4964
|
+
if (thinkingPlan.thinkingLevel !== void 0) {
|
|
4965
|
+
thinkingConfig.thinkingLevel = thinkingPlan.thinkingLevel;
|
|
4966
|
+
}
|
|
4967
|
+
if (thinkingPlan.thinkingBudget !== void 0) {
|
|
4968
|
+
thinkingConfig.thinkingBudget = thinkingPlan.thinkingBudget;
|
|
4969
|
+
}
|
|
4970
|
+
if (thinkingPlan.attachThoughts) thinkingConfig.includeThoughts = true;
|
|
4971
|
+
if (Object.keys(thinkingConfig).length > 0) {
|
|
4972
|
+
generationConfig.thinkingConfig = thinkingConfig;
|
|
4665
4973
|
if (!body.generationConfig) body.generationConfig = generationConfig;
|
|
4666
4974
|
}
|
|
4667
4975
|
if (options.toolChoice !== void 0) {
|
|
@@ -4858,7 +5166,7 @@ var GeminiProvider = class {
|
|
|
4858
5166
|
// ---------------------------------------------------------------------------
|
|
4859
5167
|
// Internal: SSE stream parsing
|
|
4860
5168
|
// ---------------------------------------------------------------------------
|
|
4861
|
-
async *parseSSEStream(body, pricingContext) {
|
|
5169
|
+
async *parseSSEStream(body, pricingContext, timing) {
|
|
4862
5170
|
const reader = body.getReader();
|
|
4863
5171
|
const decoder = new TextDecoder();
|
|
4864
5172
|
let buffer = "";
|
|
@@ -4871,7 +5179,7 @@ var GeminiProvider = class {
|
|
|
4871
5179
|
const accumulatedParts = [];
|
|
4872
5180
|
try {
|
|
4873
5181
|
while (true) {
|
|
4874
|
-
const { done, value } = await
|
|
5182
|
+
const { done, value } = await timing.read(reader);
|
|
4875
5183
|
if (done) break;
|
|
4876
5184
|
buffer += decoder.decode(value, { stream: true });
|
|
4877
5185
|
const lines = buffer.split("\n");
|
|
@@ -7296,6 +7604,9 @@ var AXL_EVENT_TYPES = [
|
|
|
7296
7604
|
// structured-output cliffs (oversized appended schema, dropped refinements,
|
|
7297
7605
|
// streaming disabled, zero-guidance `schemaPrompt:'none'`).
|
|
7298
7606
|
"schema_diagnostic",
|
|
7607
|
+
// Provider capability diagnostics — the resolved provider could not honor a
|
|
7608
|
+
// portable request knob verbatim (first kind: a clamped `effort`).
|
|
7609
|
+
"provider_diagnostic",
|
|
7299
7610
|
// Observability
|
|
7300
7611
|
"log",
|
|
7301
7612
|
"memory_remember",
|
|
@@ -7345,6 +7656,7 @@ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
|
|
|
7345
7656
|
"pipeline",
|
|
7346
7657
|
"verify",
|
|
7347
7658
|
"schema_diagnostic",
|
|
7659
|
+
"provider_diagnostic",
|
|
7348
7660
|
"workflow_start",
|
|
7349
7661
|
"workflow_end",
|
|
7350
7662
|
"transcription_start",
|
|
@@ -8273,6 +8585,10 @@ var LEGACY_REDACTION_RULES = {
|
|
|
8273
8585
|
// which is user content or PII (field paths are schema-shape, preserved like
|
|
8274
8586
|
// `string_delta.path`). Passthrough, as an explicit reviewed decision.
|
|
8275
8587
|
schema_diagnostic: passthrough,
|
|
8588
|
+
// `provider_diagnostic` carries only provider capability metadata — a provider
|
|
8589
|
+
// name, a model id, and two effort level strings. No application data or user
|
|
8590
|
+
// content ever reaches it. Passthrough, as an explicit reviewed decision.
|
|
8591
|
+
provider_diagnostic: passthrough,
|
|
8276
8592
|
log: (e) => {
|
|
8277
8593
|
if (!e.data || typeof e.data !== "object" || Array.isArray(e.data)) return e;
|
|
8278
8594
|
return { ...e, data: walkObjectOneLevel(e.data, LOG_PRESERVE_KEYS) };
|
|
@@ -8403,6 +8719,7 @@ var COMMON_V2_REDACTION_RULES = {
|
|
|
8403
8719
|
string_delta: reuseLegacyRule("string_delta"),
|
|
8404
8720
|
verify: reuseLegacyRule("verify"),
|
|
8405
8721
|
schema_diagnostic: reuseLegacyRule("schema_diagnostic"),
|
|
8722
|
+
provider_diagnostic: reuseLegacyRule("provider_diagnostic"),
|
|
8406
8723
|
log: reuseLegacyRule("log"),
|
|
8407
8724
|
memory_remember: reuseLegacyRule("memory_remember"),
|
|
8408
8725
|
memory_recall: reuseLegacyRule("memory_recall"),
|
|
@@ -8498,13 +8815,14 @@ function detectDroppedRefinements(schema) {
|
|
|
8498
8815
|
var DOCS_URL = "docs/observability.md#schema-diagnostics";
|
|
8499
8816
|
var MAX_WARNED_KEYS = 2048;
|
|
8500
8817
|
var warnedDiagnosticKeys = /* @__PURE__ */ new Set();
|
|
8501
|
-
function
|
|
8818
|
+
function warnDiagnosticOnce(key, message, silent, docs = DOCS_URL) {
|
|
8502
8819
|
if (silent || process.env.AXL_DIAGNOSTICS_SILENT === "true") return;
|
|
8503
8820
|
if (warnedDiagnosticKeys.has(key)) return;
|
|
8504
8821
|
if (warnedDiagnosticKeys.size >= MAX_WARNED_KEYS) warnedDiagnosticKeys.clear();
|
|
8505
8822
|
warnedDiagnosticKeys.add(key);
|
|
8506
|
-
console.warn(`[axl] ${message} See ${
|
|
8823
|
+
console.warn(`[axl] ${message} See ${docs}.`);
|
|
8507
8824
|
}
|
|
8825
|
+
var warnSchemaDiagnosticOnce = warnDiagnosticOnce;
|
|
8508
8826
|
|
|
8509
8827
|
// src/config.ts
|
|
8510
8828
|
function defineConfig(config) {
|
|
@@ -9354,7 +9672,29 @@ function appendRetryMessages(messages, content, feedbackMessage, providerMetadat
|
|
|
9354
9672
|
content,
|
|
9355
9673
|
...providerMetadata ? { providerMetadata } : {}
|
|
9356
9674
|
});
|
|
9357
|
-
messages.push({ role: "
|
|
9675
|
+
messages.push({ role: "user", content: feedbackMessage });
|
|
9676
|
+
}
|
|
9677
|
+
function isRetryAbort(value) {
|
|
9678
|
+
return typeof value === "object" && value !== null && value.retry === false;
|
|
9679
|
+
}
|
|
9680
|
+
function describeValue(value) {
|
|
9681
|
+
if (value === null) return "null";
|
|
9682
|
+
if (Array.isArray(value)) return `an array of length ${value.length}`;
|
|
9683
|
+
if (typeof value === "object") {
|
|
9684
|
+
const keys = Object.keys(value);
|
|
9685
|
+
return keys.length > 0 ? `an object with keys [${keys.join(", ")}]` : "an empty object";
|
|
9686
|
+
}
|
|
9687
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
9688
|
+
return `the ${typeof value} ${JSON.stringify(value)}`;
|
|
9689
|
+
}
|
|
9690
|
+
return `a value of type ${typeof value}`;
|
|
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;
|
|
9358
9698
|
}
|
|
9359
9699
|
function estimateMessagesTokens(messages) {
|
|
9360
9700
|
let total = 0;
|
|
@@ -9495,6 +9835,13 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
9495
9835
|
workflowLifecycleState;
|
|
9496
9836
|
signal;
|
|
9497
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;
|
|
9498
9845
|
workflowName;
|
|
9499
9846
|
mcpManager;
|
|
9500
9847
|
spanManager;
|
|
@@ -10027,6 +10374,17 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10027
10374
|
const costAfter2 = this.budgetContext?.totalCost ?? 0;
|
|
10028
10375
|
span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
|
|
10029
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
|
+
}
|
|
10030
10388
|
if (usageCapture.value) {
|
|
10031
10389
|
span.setAttribute(
|
|
10032
10390
|
"axl.agent.prompt_tokens",
|
|
@@ -10234,6 +10592,14 @@ var WorkflowContext = class _WorkflowContext {
|
|
|
10234
10592
|
const toolTokens = toolDefs.length > 0 ? estimateTokens(JSON.stringify(toolDefs)) : 0;
|
|
10235
10593
|
const overhead = systemTokens + toolTokens + reserveTokens;
|
|
10236
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
|
+
}
|
|
10237
10603
|
const historyEstimate = estimateMessagesTokens(sessionHistory);
|
|
10238
10604
|
if (historyEstimate.unmeasured) {
|
|
10239
10605
|
this.emitEvent({
|
|
@@ -10291,6 +10657,7 @@ ${appendedSchemaText}`;
|
|
|
10291
10657
|
provider,
|
|
10292
10658
|
model
|
|
10293
10659
|
});
|
|
10660
|
+
this.emitProviderDiagnostics(agent2, options, { provider, model, providerOptions });
|
|
10294
10661
|
if (handoffMessages && handoffMessages.length > 0) {
|
|
10295
10662
|
const handoffContext = handoffMessages.filter(
|
|
10296
10663
|
(m) => m.role === "assistant" || m.role === "tool"
|
|
@@ -10298,6 +10665,7 @@ ${appendedSchemaText}`;
|
|
|
10298
10665
|
if (handoffContext.length > 0) {
|
|
10299
10666
|
messages.push({
|
|
10300
10667
|
role: "system",
|
|
10668
|
+
origin: "runtime",
|
|
10301
10669
|
content: "The following is the conversation history from the previous agent that handed off to you:"
|
|
10302
10670
|
});
|
|
10303
10671
|
for (const msg of handoffContext) {
|
|
@@ -10347,6 +10715,7 @@ ${appendedSchemaText}`;
|
|
|
10347
10715
|
const maxTurns = agent2._config.maxTurns ?? 25;
|
|
10348
10716
|
const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
|
|
10349
10717
|
const startTime = Date.now();
|
|
10718
|
+
const timingTotals = { turns: 0, queuedMs: 0, retryMs: 0, wireMs: 0 };
|
|
10350
10719
|
const currentMessages = [...messages];
|
|
10351
10720
|
let turns = 0;
|
|
10352
10721
|
let guardrailOutputRetries = 0;
|
|
@@ -10360,8 +10729,22 @@ ${appendedSchemaText}`;
|
|
|
10360
10729
|
let lastStartMaxAttempts = 1;
|
|
10361
10730
|
while (turns < maxTurns) {
|
|
10362
10731
|
this.currentSignal?.throwIfAborted();
|
|
10363
|
-
|
|
10364
|
-
|
|
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
|
+
);
|
|
10365
10748
|
}
|
|
10366
10749
|
turns++;
|
|
10367
10750
|
const isFirstTurn = turns === 1;
|
|
@@ -10401,6 +10784,7 @@ ${appendedSchemaText}`;
|
|
|
10401
10784
|
effort: options?.effort ?? agent2._config.effort,
|
|
10402
10785
|
thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
|
|
10403
10786
|
includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
|
|
10787
|
+
promptCache: options?.promptCache ?? agent2._config.promptCache,
|
|
10404
10788
|
toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
|
|
10405
10789
|
stop: options?.stop ?? agent2._config.stop,
|
|
10406
10790
|
providerOptions,
|
|
@@ -10478,6 +10862,7 @@ ${appendedSchemaText}`;
|
|
|
10478
10862
|
const toolCalls = [];
|
|
10479
10863
|
const toolCallBuffers = /* @__PURE__ */ new Map();
|
|
10480
10864
|
let streamProviderMetadata;
|
|
10865
|
+
let streamTiming;
|
|
10481
10866
|
let thinkingContent = "";
|
|
10482
10867
|
const streamingObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof z.ZodObject;
|
|
10483
10868
|
const currentAttempt = schemaRetries + 1;
|
|
@@ -10529,6 +10914,7 @@ ${appendedSchemaText}`;
|
|
|
10529
10914
|
if (chunk.arguments) buffer.arguments += chunk.arguments;
|
|
10530
10915
|
} else if (chunk.type === "done") {
|
|
10531
10916
|
streamProviderMetadata = chunk.providerMetadata;
|
|
10917
|
+
streamTiming = chunk.timing;
|
|
10532
10918
|
if (chunk.usage) {
|
|
10533
10919
|
response = {
|
|
10534
10920
|
content: content2,
|
|
@@ -10556,6 +10942,9 @@ ${appendedSchemaText}`;
|
|
|
10556
10942
|
if (streamProviderMetadata) {
|
|
10557
10943
|
response.providerMetadata = streamProviderMetadata;
|
|
10558
10944
|
}
|
|
10945
|
+
if (streamTiming) {
|
|
10946
|
+
response.timing = streamTiming;
|
|
10947
|
+
}
|
|
10559
10948
|
if (thinkingContent) {
|
|
10560
10949
|
response.thinking_content = thinkingContent;
|
|
10561
10950
|
}
|
|
@@ -10564,12 +10953,15 @@ ${appendedSchemaText}`;
|
|
|
10564
10953
|
}
|
|
10565
10954
|
} catch (err) {
|
|
10566
10955
|
if (richRequest) this.recordRichFailure(err);
|
|
10956
|
+
const failureTiming = err instanceof ProviderError ? err.timing : void 0;
|
|
10567
10957
|
this.emitEvent({
|
|
10568
10958
|
type: "agent_call_end",
|
|
10569
10959
|
agent: agent2._name,
|
|
10570
10960
|
model: effectiveModelUri,
|
|
10571
10961
|
promptVersion: agent2._config.version,
|
|
10572
10962
|
duration: Date.now() - turnStart,
|
|
10963
|
+
// Top-level beside `duration`, exactly as on the success path.
|
|
10964
|
+
...failureTiming ? { timing: failureTiming } : {},
|
|
10573
10965
|
data: {
|
|
10574
10966
|
response: "",
|
|
10575
10967
|
turn: turns,
|
|
@@ -10586,9 +10978,41 @@ ${appendedSchemaText}`;
|
|
|
10586
10978
|
if (usageCapture && response.usage) {
|
|
10587
10979
|
usageCapture.value = response.usage;
|
|
10588
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
|
+
}
|
|
10589
11007
|
if (response.cost) {
|
|
10590
11008
|
this._accumulateBudgetCost(response.cost);
|
|
10591
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
|
+
}
|
|
10592
11016
|
this.emitEvent({
|
|
10593
11017
|
type: "agent_call_end",
|
|
10594
11018
|
agent: agent2._name,
|
|
@@ -10603,6 +11027,8 @@ ${appendedSchemaText}`;
|
|
|
10603
11027
|
cacheWrite: response.usage.cache_write_tokens
|
|
10604
11028
|
} : void 0,
|
|
10605
11029
|
duration: Date.now() - turnStart,
|
|
11030
|
+
// Additive beside `duration`, which keeps its turn-wall-clock meaning.
|
|
11031
|
+
...response.timing ? { timing: response.timing } : {},
|
|
10606
11032
|
data: {
|
|
10607
11033
|
response: response.content,
|
|
10608
11034
|
...response.thinking_content ? { thinking: response.thinking_content } : {},
|
|
@@ -10655,7 +11081,11 @@ ${appendedSchemaText}`;
|
|
|
10655
11081
|
retries: options.retries,
|
|
10656
11082
|
metadata: options.metadata,
|
|
10657
11083
|
validate: options.validate,
|
|
10658
|
-
validateRetries: options.validateRetries
|
|
11084
|
+
validateRetries: options.validateRetries,
|
|
11085
|
+
// This seam serves both callers of a handoff: a plain `ctx.ask()` on an
|
|
11086
|
+
// agent with `handoffs`, and the routed `ctx.delegate()` path, whose
|
|
11087
|
+
// terminal ask is the handoff target. The hook travels like `validate`.
|
|
11088
|
+
retryFeedback: options.retryFeedback
|
|
10659
11089
|
} : void 0;
|
|
10660
11090
|
const handoffFn = () => askStorage.run(targetFrame, async () => {
|
|
10661
11091
|
const delegateOriginal = agent2._delegateOriginalInput;
|
|
@@ -10804,9 +11234,10 @@ ${appendedSchemaText}`;
|
|
|
10804
11234
|
const attempt = guardrailOutputRetries + 1;
|
|
10805
11235
|
const maxAttempts = maxGuardrailRetries + 1;
|
|
10806
11236
|
const onBlock = guardrails.onBlock ?? "throw";
|
|
11237
|
+
const blockReason = outputResult.reason ?? "Output blocked by guardrail";
|
|
10807
11238
|
let feedbackMessage;
|
|
10808
11239
|
if (outputResult.block && onBlock === "retry" && guardrailOutputRetries < maxGuardrailRetries) {
|
|
10809
|
-
feedbackMessage = `Your previous response was blocked by a safety guardrail: ${
|
|
11240
|
+
feedbackMessage = `Your previous response was blocked by a safety guardrail: ${blockReason}. Please provide a different response that complies with the guidelines.`;
|
|
10810
11241
|
}
|
|
10811
11242
|
this.emitEvent({
|
|
10812
11243
|
type: "guardrail",
|
|
@@ -10829,31 +11260,43 @@ ${appendedSchemaText}`;
|
|
|
10829
11260
|
});
|
|
10830
11261
|
if (outputResult.block) {
|
|
10831
11262
|
if (feedbackMessage) {
|
|
10832
|
-
this.
|
|
10833
|
-
type: "pipeline",
|
|
10834
|
-
agent: agent2._name,
|
|
10835
|
-
status: "failed",
|
|
11263
|
+
const decision = await this.resolveRetryFeedback(options?.retryFeedback, {
|
|
10836
11264
|
stage: "guardrail",
|
|
10837
|
-
attempt
|
|
10838
|
-
maxAttempts
|
|
10839
|
-
|
|
11265
|
+
attempt,
|
|
11266
|
+
maxAttempts,
|
|
11267
|
+
output: content,
|
|
11268
|
+
reason: blockReason,
|
|
11269
|
+
defaultMessage: feedbackMessage
|
|
10840
11270
|
});
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
|
|
10847
|
-
|
|
10848
|
-
|
|
10849
|
-
|
|
11271
|
+
if (decision.retry) {
|
|
11272
|
+
this.emitEvent({
|
|
11273
|
+
type: "pipeline",
|
|
11274
|
+
agent: agent2._name,
|
|
11275
|
+
status: "failed",
|
|
11276
|
+
stage: "guardrail",
|
|
11277
|
+
attempt,
|
|
11278
|
+
maxAttempts,
|
|
11279
|
+
reason: decision.message
|
|
11280
|
+
});
|
|
11281
|
+
guardrailOutputRetries++;
|
|
11282
|
+
appendRetryMessages(
|
|
11283
|
+
currentMessages,
|
|
11284
|
+
content,
|
|
11285
|
+
decision.message,
|
|
11286
|
+
response.providerMetadata
|
|
11287
|
+
);
|
|
11288
|
+
pendingRetryReason = "guardrail";
|
|
11289
|
+
continue;
|
|
11290
|
+
}
|
|
11291
|
+
this.currentSignal?.throwIfAborted();
|
|
11292
|
+
throw new GuardrailError("output", blockReason);
|
|
10850
11293
|
}
|
|
10851
11294
|
if (typeof onBlock === "function") {
|
|
10852
|
-
return onBlock(
|
|
11295
|
+
return onBlock(blockReason, {
|
|
10853
11296
|
metadata: this.metadata
|
|
10854
11297
|
});
|
|
10855
11298
|
}
|
|
10856
|
-
throw new GuardrailError("output",
|
|
11299
|
+
throw new GuardrailError("output", blockReason);
|
|
10857
11300
|
}
|
|
10858
11301
|
}
|
|
10859
11302
|
let validated = void 0;
|
|
@@ -10873,7 +11316,7 @@ ${appendedSchemaText}`;
|
|
|
10873
11316
|
schemaErr = err;
|
|
10874
11317
|
schemaReason = err instanceof Error ? err.message : String(err);
|
|
10875
11318
|
if (schemaRetries < maxSchemaRetries) {
|
|
10876
|
-
schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}.
|
|
11319
|
+
schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Return a corrected response that matches the required schema.`;
|
|
10877
11320
|
}
|
|
10878
11321
|
}
|
|
10879
11322
|
this.emitEvent({
|
|
@@ -10895,24 +11338,36 @@ ${appendedSchemaText}`;
|
|
|
10895
11338
|
});
|
|
10896
11339
|
if (!schemaValid) {
|
|
10897
11340
|
if (schemaFeedback) {
|
|
10898
|
-
this.
|
|
10899
|
-
type: "pipeline",
|
|
10900
|
-
agent: agent2._name,
|
|
10901
|
-
status: "failed",
|
|
11341
|
+
const decision = await this.resolveRetryFeedback(options.retryFeedback, {
|
|
10902
11342
|
stage: "schema",
|
|
10903
|
-
attempt:
|
|
10904
|
-
maxAttempts:
|
|
10905
|
-
|
|
11343
|
+
attempt: schemaAttempt,
|
|
11344
|
+
maxAttempts: schemaMaxAttempts,
|
|
11345
|
+
output: content,
|
|
11346
|
+
reason: schemaReason ?? "Schema parse failed",
|
|
11347
|
+
...schemaErr !== void 0 ? { error: schemaErr } : {},
|
|
11348
|
+
defaultMessage: schemaFeedback
|
|
10906
11349
|
});
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
|
|
11350
|
+
if (decision.retry) {
|
|
11351
|
+
this.emitEvent({
|
|
11352
|
+
type: "pipeline",
|
|
11353
|
+
agent: agent2._name,
|
|
11354
|
+
status: "failed",
|
|
11355
|
+
stage: "schema",
|
|
11356
|
+
attempt: schemaAttempt,
|
|
11357
|
+
maxAttempts: schemaMaxAttempts,
|
|
11358
|
+
reason: decision.message
|
|
11359
|
+
});
|
|
11360
|
+
schemaRetries++;
|
|
11361
|
+
appendRetryMessages(
|
|
11362
|
+
currentMessages,
|
|
11363
|
+
content,
|
|
11364
|
+
decision.message,
|
|
11365
|
+
response.providerMetadata
|
|
11366
|
+
);
|
|
11367
|
+
pendingRetryReason = "schema";
|
|
11368
|
+
continue;
|
|
11369
|
+
}
|
|
11370
|
+
this.currentSignal?.throwIfAborted();
|
|
10916
11371
|
}
|
|
10917
11372
|
const zodErr = schemaErr instanceof ZodError2 ? schemaErr : new ZodError2([
|
|
10918
11373
|
{
|
|
@@ -10926,12 +11381,14 @@ ${appendedSchemaText}`;
|
|
|
10926
11381
|
}
|
|
10927
11382
|
if (options?.schema && options.validate) {
|
|
10928
11383
|
let validateResult;
|
|
11384
|
+
let validateErr;
|
|
10929
11385
|
try {
|
|
10930
11386
|
validateResult = await options.validate(validated, {
|
|
10931
11387
|
metadata: this.metadata
|
|
10932
11388
|
});
|
|
10933
11389
|
} catch (err) {
|
|
10934
11390
|
rethrowEventStreamOverflow(err);
|
|
11391
|
+
validateErr = err;
|
|
10935
11392
|
const reason = err instanceof Error ? err.message : String(err);
|
|
10936
11393
|
validateResult = { valid: false, reason: `Validator error: ${reason}` };
|
|
10937
11394
|
}
|
|
@@ -10940,7 +11397,7 @@ ${appendedSchemaText}`;
|
|
|
10940
11397
|
const validateMaxAttempts = maxValidateRetries + 1;
|
|
10941
11398
|
let validateFeedback;
|
|
10942
11399
|
if (!validateResult.valid && validateRetries < maxValidateRetries) {
|
|
10943
|
-
validateFeedback = `Your response
|
|
11400
|
+
validateFeedback = `Your previous response failed validation: ${validateResult.reason ?? "Validation failed"}. Return a corrected response that satisfies the required schema and this rule.`;
|
|
10944
11401
|
}
|
|
10945
11402
|
this.emitEvent({
|
|
10946
11403
|
type: "validate",
|
|
@@ -10961,24 +11418,37 @@ ${appendedSchemaText}`;
|
|
|
10961
11418
|
});
|
|
10962
11419
|
if (!validateResult.valid) {
|
|
10963
11420
|
if (validateFeedback) {
|
|
10964
|
-
this.
|
|
10965
|
-
type: "pipeline",
|
|
10966
|
-
agent: agent2._name,
|
|
10967
|
-
status: "failed",
|
|
11421
|
+
const decision = await this.resolveRetryFeedback(options.retryFeedback, {
|
|
10968
11422
|
stage: "validate",
|
|
10969
|
-
attempt:
|
|
10970
|
-
maxAttempts:
|
|
10971
|
-
|
|
11423
|
+
attempt: validateAttempt,
|
|
11424
|
+
maxAttempts: validateMaxAttempts,
|
|
11425
|
+
output: content,
|
|
11426
|
+
parsed: validated,
|
|
11427
|
+
reason: validateResult.reason ?? "Validation failed",
|
|
11428
|
+
...validateErr !== void 0 ? { error: validateErr } : {},
|
|
11429
|
+
defaultMessage: validateFeedback
|
|
10972
11430
|
});
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
|
|
10979
|
-
|
|
10980
|
-
|
|
10981
|
-
|
|
11431
|
+
if (decision.retry) {
|
|
11432
|
+
this.emitEvent({
|
|
11433
|
+
type: "pipeline",
|
|
11434
|
+
agent: agent2._name,
|
|
11435
|
+
status: "failed",
|
|
11436
|
+
stage: "validate",
|
|
11437
|
+
attempt: validateAttempt,
|
|
11438
|
+
maxAttempts: validateMaxAttempts,
|
|
11439
|
+
reason: decision.message
|
|
11440
|
+
});
|
|
11441
|
+
validateRetries++;
|
|
11442
|
+
appendRetryMessages(
|
|
11443
|
+
currentMessages,
|
|
11444
|
+
content,
|
|
11445
|
+
decision.message,
|
|
11446
|
+
response.providerMetadata
|
|
11447
|
+
);
|
|
11448
|
+
pendingRetryReason = "validate";
|
|
11449
|
+
continue;
|
|
11450
|
+
}
|
|
11451
|
+
this.currentSignal?.throwIfAborted();
|
|
10982
11452
|
}
|
|
10983
11453
|
throw new ValidationError(
|
|
10984
11454
|
validated,
|
|
@@ -11000,6 +11470,31 @@ ${appendedSchemaText}`;
|
|
|
11000
11470
|
}
|
|
11001
11471
|
throw new MaxTurnsError("ctx.ask()", maxTurns);
|
|
11002
11472
|
}
|
|
11473
|
+
/**
|
|
11474
|
+
* Resolve the corrective feedback text for one rejected attempt, giving an optional
|
|
11475
|
+
* caller-supplied `retryFeedback` hook the chance to replace it or to stop retrying.
|
|
11476
|
+
*
|
|
11477
|
+
* Called by all three output gates, only while a retry is still permitted (never on the
|
|
11478
|
+
* exhausting attempt) and only after the gate's own event has been emitted, so a hook that
|
|
11479
|
+
* throws or aborts can never erase the evidence that the gate rejected. A non-empty string
|
|
11480
|
+
* replaces `defaultMessage` verbatim; `undefined` / `''` keeps it; `{ retry: false }` asks
|
|
11481
|
+
* the gate to throw its typed terminal error. Hook exceptions propagate — a bug in caller
|
|
11482
|
+
* code is not a model failure and must not be retried into silence, and neither is a
|
|
11483
|
+
* return value outside the contract: an unrecognized shape throws instead of silently
|
|
11484
|
+
* degrading to the default text, which would hide the defect behind extra token spend.
|
|
11485
|
+
*/
|
|
11486
|
+
async resolveRetryFeedback(hook, info) {
|
|
11487
|
+
if (!hook) return { retry: true, message: info.defaultMessage };
|
|
11488
|
+
const result = await hook(info, { metadata: this.metadata });
|
|
11489
|
+
if (result === void 0 || result === "") {
|
|
11490
|
+
return { retry: true, message: info.defaultMessage };
|
|
11491
|
+
}
|
|
11492
|
+
if (typeof result === "string") return { retry: true, message: result };
|
|
11493
|
+
if (isRetryAbort(result)) return { retry: false };
|
|
11494
|
+
throw new TypeError(
|
|
11495
|
+
`retryFeedback must return a string, undefined, or { retry: false }; received ${describeValue(result)}.`
|
|
11496
|
+
);
|
|
11497
|
+
}
|
|
11003
11498
|
/**
|
|
11004
11499
|
* Push the final assistant message into session history, preserving providerMetadata
|
|
11005
11500
|
* (e.g., Gemini thought signatures needed for multi-turn reasoning context) and
|
|
@@ -11155,6 +11650,57 @@ ${appendedSchemaText}`;
|
|
|
11155
11650
|
}
|
|
11156
11651
|
}
|
|
11157
11652
|
}
|
|
11653
|
+
/**
|
|
11654
|
+
* Emit a `provider_diagnostic` when the resolved provider reports that it
|
|
11655
|
+
* could not honor the unified `effort` verbatim (P3/R6/R7).
|
|
11656
|
+
*
|
|
11657
|
+
* The adapter only *resolves* — it returns the requested and effective levels
|
|
11658
|
+
* from the same pure resolver its request builder uses. The runtime decides
|
|
11659
|
+
* how to surface that, because only the runtime can see the event bus and
|
|
11660
|
+
* `AxlConfig.diagnostics.silent` (adapters receive `ProviderConfig` only).
|
|
11661
|
+
*
|
|
11662
|
+
* Called once per ask, so a tool loop with N provider round trips still emits
|
|
11663
|
+
* one event; the `console.warn` dedupes once per process per distinct clamp.
|
|
11664
|
+
* A throwing `effortResolution` is a provider bug and propagates.
|
|
11665
|
+
*/
|
|
11666
|
+
emitProviderDiagnostics(agent2, options, ctx) {
|
|
11667
|
+
if (!ctx.provider.effortResolution) return;
|
|
11668
|
+
const resolution = ctx.provider.effortResolution({
|
|
11669
|
+
model: ctx.model,
|
|
11670
|
+
effort: options?.effort ?? agent2._config.effort,
|
|
11671
|
+
thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
|
|
11672
|
+
includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
|
|
11673
|
+
providerOptions: ctx.providerOptions
|
|
11674
|
+
});
|
|
11675
|
+
if (!resolution?.clamped) return;
|
|
11676
|
+
if (typeof resolution.effective !== "string" || resolution.effective.length === 0 || typeof resolution.requested !== "string" || resolution.requested.length === 0) {
|
|
11677
|
+
throw new TypeError(
|
|
11678
|
+
`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)}.`
|
|
11679
|
+
);
|
|
11680
|
+
}
|
|
11681
|
+
const cause = resolution.cause ?? `effort '${resolution.requested}' is not supported as asked`;
|
|
11682
|
+
this.emitEvent({
|
|
11683
|
+
type: "provider_diagnostic",
|
|
11684
|
+
agent: agent2._name,
|
|
11685
|
+
data: {
|
|
11686
|
+
kind: "effort_clamped",
|
|
11687
|
+
...ctx.provider.name ? { provider: ctx.provider.name } : {},
|
|
11688
|
+
model: ctx.model,
|
|
11689
|
+
requested: resolution.requested,
|
|
11690
|
+
effective: resolution.effective,
|
|
11691
|
+
cause
|
|
11692
|
+
}
|
|
11693
|
+
});
|
|
11694
|
+
warnDiagnosticOnce(
|
|
11695
|
+
// One warning per distinct clamp, not per ask: the same clamp across 500
|
|
11696
|
+
// asks is one process-level surprise, while a different requested/effective
|
|
11697
|
+
// pair is a genuinely new one.
|
|
11698
|
+
`effort_clamped\0${ctx.provider.name ?? ""}\0${ctx.model}\0${resolution.requested}\0${resolution.effective}`,
|
|
11699
|
+
`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.`,
|
|
11700
|
+
this.config.diagnostics?.silent,
|
|
11701
|
+
"docs/observability.md#provider-diagnostics"
|
|
11702
|
+
);
|
|
11703
|
+
}
|
|
11158
11704
|
buildToolDefs(agent2, resolvedHandoffs) {
|
|
11159
11705
|
const defs = [];
|
|
11160
11706
|
if (agent2._config.tools) {
|
|
@@ -11201,6 +11747,7 @@ ${appendedSchemaText}`;
|
|
|
11201
11747
|
if (this.summaryCache) {
|
|
11202
11748
|
const summaryMsg2 = {
|
|
11203
11749
|
role: "system",
|
|
11750
|
+
origin: "runtime",
|
|
11204
11751
|
content: `Summary of earlier conversation:
|
|
11205
11752
|
${this.summaryCache}`
|
|
11206
11753
|
};
|
|
@@ -11214,6 +11761,7 @@ ${this.summaryCache}`
|
|
|
11214
11761
|
recentTokens2 += msgTokens;
|
|
11215
11762
|
splitIdx2 = i;
|
|
11216
11763
|
}
|
|
11764
|
+
splitIdx2 = nextUserTurn(history, splitIdx2) ?? history.length;
|
|
11217
11765
|
if (splitIdx2 < history.length) {
|
|
11218
11766
|
return [summaryMsg2, ...history.slice(splitIdx2)];
|
|
11219
11767
|
}
|
|
@@ -11227,6 +11775,19 @@ ${this.summaryCache}`
|
|
|
11227
11775
|
recentTokens += msgTokens;
|
|
11228
11776
|
splitIdx = i;
|
|
11229
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
|
+
}
|
|
11230
11791
|
if (splitIdx === 0) return history;
|
|
11231
11792
|
const oldMessages = history.slice(0, splitIdx);
|
|
11232
11793
|
const summaryModelUri = this.config.contextManagement?.summaryModel;
|
|
@@ -11258,6 +11819,7 @@ ${this.summaryCache}`
|
|
|
11258
11819
|
}
|
|
11259
11820
|
const summaryMsg = {
|
|
11260
11821
|
role: "system",
|
|
11822
|
+
origin: "runtime",
|
|
11261
11823
|
content: `Summary of earlier conversation:
|
|
11262
11824
|
${summaryResponse.content}`
|
|
11263
11825
|
};
|
|
@@ -12435,7 +12997,8 @@ ${summaryResponse.content}`
|
|
|
12435
12997
|
retries: options?.retries,
|
|
12436
12998
|
metadata: options?.metadata,
|
|
12437
12999
|
validate: options?.validate,
|
|
12438
|
-
validateRetries: options?.validateRetries
|
|
13000
|
+
validateRetries: options?.validateRetries,
|
|
13001
|
+
retryFeedback: options?.retryFeedback
|
|
12439
13002
|
});
|
|
12440
13003
|
}
|
|
12441
13004
|
const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
|
|
@@ -12483,7 +13046,8 @@ ${summaryResponse.content}`
|
|
|
12483
13046
|
retries: options?.retries,
|
|
12484
13047
|
metadata: options?.metadata,
|
|
12485
13048
|
validate: options?.validate,
|
|
12486
|
-
validateRetries: options?.validateRetries
|
|
13049
|
+
validateRetries: options?.validateRetries,
|
|
13050
|
+
retryFeedback: options?.retryFeedback
|
|
12487
13051
|
});
|
|
12488
13052
|
}
|
|
12489
13053
|
/** Transcribe recorded audio through the dedicated transcription registry.
|
|
@@ -16031,6 +16595,8 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
16031
16595
|
parent: parentScope
|
|
16032
16596
|
};
|
|
16033
16597
|
const modelCalls = /* @__PURE__ */ new Map();
|
|
16598
|
+
const modelTiming = /* @__PURE__ */ new Map();
|
|
16599
|
+
const captureTimingSamples = options?.captureTimingSamples === true;
|
|
16034
16600
|
const workflowCalls = /* @__PURE__ */ new Map();
|
|
16035
16601
|
const tokens = { input: 0, output: 0, reasoning: 0 };
|
|
16036
16602
|
let agentCalls = 0;
|
|
@@ -16048,6 +16614,30 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
16048
16614
|
tokens.output += event.tokens.output ?? 0;
|
|
16049
16615
|
tokens.reasoning += event.tokens.reasoning ?? 0;
|
|
16050
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
|
+
}
|
|
16051
16641
|
}
|
|
16052
16642
|
if (event.type === "workflow_start" && event.workflow) {
|
|
16053
16643
|
workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
|
|
@@ -16078,6 +16668,7 @@ var AxlRuntime = class extends EventEmitter2 {
|
|
|
16078
16668
|
result,
|
|
16079
16669
|
cost: scope.totalCost,
|
|
16080
16670
|
unpriced,
|
|
16671
|
+
...modelTiming.size > 0 ? { modelTiming: Object.fromEntries(modelTiming) } : {},
|
|
16081
16672
|
...capturedTraces ? { traces: capturedTraces } : {},
|
|
16082
16673
|
metadata: {
|
|
16083
16674
|
models: [...modelCalls.keys()],
|