@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/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
- constructor(operation, timeoutMs) {
174
- super("TIMEOUT", `${operation} exceeded timeout of ${timeoutMs}ms`);
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 this.parseResponse(json, this.requestModel(body, options.model), body);
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* this.parseSSEStream(res.body, this.requestModel(body, options.model), body);
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
- async *parseSSEStream(body, model, request) {
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 reader.read();
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: 5 / M, cachedInput: 0.5 / M, cacheWrite: 6.25 / M, output: 30 / M },
1915
+ short: { input: 4 / M, cachedInput: 0.4 / M, cacheWrite: 5 / M, output: 20 / M },
1780
1916
  long: withLongContext({
1781
- input: 5 / M,
1782
- cachedInput: 0.5 / M,
1783
- cacheWrite: 6.25 / M,
1784
- output: 30 / M
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
- if (effort !== "max") return effort;
2125
- if (resolved.activeEffort === "max" && !warnedChatMaxDowngrades.has(model)) {
2126
- warnedChatMaxDowngrades.add(model);
2127
- console.warn(
2128
- `[axl] OpenAI Chat Completions does not accept effort "max" for ${model}; using "xhigh". Use the OpenAI Responses provider for native "max".`
2129
- );
2130
- }
2131
- return "xhigh";
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 this.parseResponse(json, this.requestModel(body, options.model), body);
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* this.parseSSEStream(res.body, this.requestModel(body, options.model), body);
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 reader.read();
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(eventType, data, model, callIdMap, request);
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: 3e-6, output: 15e-6, sonnet5Intro: true },
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, now = /* @__PURE__ */ new Date()) {
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.sonnet5Intro && now < /* @__PURE__ */ new Date("2026-09-01T00:00:00Z") ? { input: 2e-6, output: 1e-5 } : rate;
2853
- return usage.inputTokens * input + cacheRead * input * 0.1 + cacheWrite5m * input * 1.25 + cacheWrite1h * input * 2 + usage.outputTokens * output;
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* this.parseSSEStream(res.body, pricingContext, res.headers);
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
- if (systemText) {
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
- body.tools = options.tools.map((t) => this.mapToolDefinition(t));
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
- body.system = body.system ? `${body.system}
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 reader.read();
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: 15e-7, cached: 15e-8, output: 75e-7 }
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
- var _warned3xEffortNone = /* @__PURE__ */ new Set();
3932
- function warnGemini3xEffortNone(model) {
3933
- if (_warned3xEffortNone.has(model)) return;
3934
- _warned3xEffortNone.add(model);
3935
- console.warn(
3936
- `[axl] effort: 'none' on Gemini 3.x (${model}) maps to the model's minimum thinking level ('${minThinkingLevel(model)}'), not fully disabled. Gemini 3.x models cannot disable thinking entirely.`
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 this.parseInteractionResponse(await res.json());
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* this.parseInteractionSSEStream(res.body, res.headers);
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
- if (thinking.thinkingDisabled) {
4181
- generationConfig.thinking_level = isGemini3x(model) ? minThinkingLevel(model) : "minimal";
4182
- } else if (thinking.hasBudgetOverride) {
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 reader.read();
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
- effort,
4623
- thinkingBudget,
4624
- includeThoughts,
4625
- thinkingDisabled,
4626
- activeEffort,
4627
- hasBudgetOverride
4628
- } = resolveThinkingOptions(options);
4629
- if (thinkingDisabled) {
4630
- if (isGemini3x(requestModel)) {
4631
- if (effort === "none") {
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 reader.read();
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 warnSchemaDiagnosticOnce(key, message, silent) {
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 ${DOCS_URL}.`);
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: "system", content: feedbackMessage });
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
- if (Date.now() - startTime > timeoutMs) {
10364
- throw new TimeoutError("ctx.ask()", timeoutMs);
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: ${outputResult.reason ?? "Output blocked"}. Please provide a different response that complies with the guidelines.`;
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.emitEvent({
10833
- type: "pipeline",
10834
- agent: agent2._name,
10835
- status: "failed",
11263
+ const decision = await this.resolveRetryFeedback(options?.retryFeedback, {
10836
11264
  stage: "guardrail",
10837
- attempt: guardrailOutputRetries + 1,
10838
- maxAttempts: maxGuardrailRetries + 1,
10839
- reason: feedbackMessage
11265
+ attempt,
11266
+ maxAttempts,
11267
+ output: content,
11268
+ reason: blockReason,
11269
+ defaultMessage: feedbackMessage
10840
11270
  });
10841
- guardrailOutputRetries++;
10842
- appendRetryMessages(
10843
- currentMessages,
10844
- content,
10845
- feedbackMessage,
10846
- response.providerMetadata
10847
- );
10848
- pendingRetryReason = "guardrail";
10849
- continue;
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(outputResult.reason ?? "Output blocked by guardrail", {
11295
+ return onBlock(blockReason, {
10853
11296
  metadata: this.metadata
10854
11297
  });
10855
11298
  }
10856
- throw new GuardrailError("output", outputResult.reason ?? "Output blocked by guardrail");
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}. Please fix and try again.`;
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.emitEvent({
10899
- type: "pipeline",
10900
- agent: agent2._name,
10901
- status: "failed",
11341
+ const decision = await this.resolveRetryFeedback(options.retryFeedback, {
10902
11342
  stage: "schema",
10903
- attempt: schemaRetries + 1,
10904
- maxAttempts: (options.retries ?? 3) + 1,
10905
- reason: schemaFeedback
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
- schemaRetries++;
10908
- appendRetryMessages(
10909
- currentMessages,
10910
- content,
10911
- schemaFeedback,
10912
- response.providerMetadata
10913
- );
10914
- pendingRetryReason = "schema";
10915
- continue;
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 parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`;
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.emitEvent({
10965
- type: "pipeline",
10966
- agent: agent2._name,
10967
- status: "failed",
11421
+ const decision = await this.resolveRetryFeedback(options.retryFeedback, {
10968
11422
  stage: "validate",
10969
- attempt: validateRetries + 1,
10970
- maxAttempts: (options.validateRetries ?? 2) + 1,
10971
- reason: validateFeedback
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
- validateRetries++;
10974
- appendRetryMessages(
10975
- currentMessages,
10976
- content,
10977
- validateFeedback,
10978
- response.providerMetadata
10979
- );
10980
- pendingRetryReason = "validate";
10981
- continue;
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()],