@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.cjs CHANGED
@@ -392,9 +392,15 @@ var NoConsensus = class extends AxlError {
392
392
  }
393
393
  };
394
394
  var TimeoutError = class extends AxlError {
395
- constructor(operation, timeoutMs) {
396
- super("TIMEOUT", `${operation} exceeded timeout of ${timeoutMs}ms`);
395
+ /** Latency attribution for the timed-out operation, when measurable. */
396
+ breakdown;
397
+ constructor(operation, timeoutMs, breakdown) {
398
+ super(
399
+ "TIMEOUT",
400
+ `${operation} exceeded timeout of ${timeoutMs}ms` + (breakdown ? ` (elapsed ${breakdown.elapsedMs}ms: queued ${breakdown.queuedMs}ms, retries ${breakdown.retryMs}ms, wire ${breakdown.wireMs}ms, other ${breakdown.otherMs}ms)` : "")
401
+ );
397
402
  this.name = "TimeoutError";
403
+ if (breakdown) this.breakdown = breakdown;
398
404
  }
399
405
  };
400
406
  function formatBudgetCost(cost) {
@@ -941,6 +947,7 @@ var ProviderError = class extends AxlError {
941
947
  this.retryAfterMs = args.retryAfterMs;
942
948
  this.requestId = args.requestId;
943
949
  this.body = args.body;
950
+ if (args.timing) this.timing = args.timing;
944
951
  }
945
952
  };
946
953
  function isRetryableStatus(status) {
@@ -992,7 +999,8 @@ function buildProviderError(args) {
992
999
  message: args.message,
993
1000
  retryAfterMs: args.headers ? parseRetryAfter(args.headers) : void 0,
994
1001
  requestId: args.headers ? extractRequestId(args.headers) : void 0,
995
- body: args.body
1002
+ body: args.body,
1003
+ timing: args.timing
996
1004
  });
997
1005
  }
998
1006
 
@@ -1029,14 +1037,33 @@ async function fetchWithRetry(input, init, opts) {
1029
1037
  const maxRetries = opts?.maxRetries ?? MAX_RETRIES;
1030
1038
  const governor = opts?.governor;
1031
1039
  const provider = opts?.provider ?? "unknown";
1040
+ const observer = opts?.timing;
1032
1041
  let acquired = false;
1042
+ let queuedMs = 0;
1033
1043
  if (governor) {
1044
+ const acquireStart = Date.now();
1034
1045
  await governor.acquire(init?.signal ?? void 0);
1035
1046
  acquired = true;
1036
- }
1047
+ queuedMs = Date.now() - acquireStart;
1048
+ }
1049
+ let firstDispatchedAt = 0;
1050
+ let dispatchedAt = 0;
1051
+ let headersAt = 0;
1052
+ const reportComplete = (attempts) => {
1053
+ observer?.onComplete?.({
1054
+ queuedMs,
1055
+ attempts,
1056
+ retryMs: dispatchedAt - firstDispatchedAt,
1057
+ dispatchedAt,
1058
+ headersAt
1059
+ });
1060
+ };
1037
1061
  try {
1038
1062
  for (let attempt = 0; ; attempt++) {
1039
1063
  let res;
1064
+ dispatchedAt = Date.now();
1065
+ if (attempt === 0) firstDispatchedAt = dispatchedAt;
1066
+ observer?.onDispatch?.(attempt + 1, dispatchedAt);
1040
1067
  try {
1041
1068
  res = await fetch(input, { ...init, redirect: "manual" });
1042
1069
  } catch (err) {
@@ -1051,11 +1078,14 @@ async function fetchWithRetry(input, init, opts) {
1051
1078
  await sleep2(jitter(BASE_DELAY_MS * 2 ** attempt), init?.signal ?? void 0);
1052
1079
  continue;
1053
1080
  }
1081
+ headersAt = Date.now();
1054
1082
  governor?.observe(res);
1055
1083
  if (res.ok || !RETRYABLE_STATUS_CODES.has(res.status) || attempt >= maxRetries) {
1084
+ reportComplete(attempt + 1);
1056
1085
  return res;
1057
1086
  }
1058
1087
  if (init?.signal?.aborted) {
1088
+ reportComplete(attempt + 1);
1059
1089
  return res;
1060
1090
  }
1061
1091
  const retryAfterMs = parseRetryAfter(res.headers);
@@ -1067,6 +1097,86 @@ async function fetchWithRetry(input, init, opts) {
1067
1097
  }
1068
1098
  }
1069
1099
 
1100
+ // src/providers/call-timing.ts
1101
+ var CallTimingRecorder = class {
1102
+ fetchTiming;
1103
+ firstTokenAt;
1104
+ /** Cumulative time awaiting `reader.read()`, i.e. transport, not consumer. */
1105
+ readWaitMs = 0;
1106
+ /** Pass as `timing` in the `fetchWithRetry` options object. */
1107
+ observer = {
1108
+ onComplete: (timing) => {
1109
+ this.fetchTiming = timing;
1110
+ }
1111
+ };
1112
+ /**
1113
+ * Await one body read, charging only the wait itself to `wireMs`. Time the
1114
+ * consumer spends between reads is excluded because it elapses outside this
1115
+ * call.
1116
+ */
1117
+ async read(reader) {
1118
+ const start = Date.now();
1119
+ try {
1120
+ return await reader.read();
1121
+ } finally {
1122
+ this.readWaitMs += Date.now() - start;
1123
+ }
1124
+ }
1125
+ /** Stamp the first content delta. Idempotent — only the first call counts. */
1126
+ markFirstToken() {
1127
+ this.firstTokenAt ??= Date.now();
1128
+ }
1129
+ /**
1130
+ * Timing for a non-streaming call. Call once the response body is parsed.
1131
+ *
1132
+ * Also the right reading at an adapter's `!res.ok` throw site on BOTH
1133
+ * transports: headers have arrived, no body will be streamed, so
1134
+ * dispatch → now is the whole of what the provider cost us.
1135
+ */
1136
+ chatTiming() {
1137
+ const t = this.fetchTiming;
1138
+ if (!t) return void 0;
1139
+ return {
1140
+ queuedMs: t.queuedMs,
1141
+ attempts: t.attempts,
1142
+ retryMs: t.retryMs,
1143
+ ttfbMs: Math.max(0, t.headersAt - t.dispatchedAt),
1144
+ wireMs: Math.max(0, Date.now() - t.dispatchedAt)
1145
+ };
1146
+ }
1147
+ /** Timing for a streamed call. Call when building the terminal `done` chunk. */
1148
+ streamTiming() {
1149
+ const t = this.fetchTiming;
1150
+ if (!t) return void 0;
1151
+ const ttfbMs = Math.max(0, t.headersAt - t.dispatchedAt);
1152
+ return {
1153
+ queuedMs: t.queuedMs,
1154
+ attempts: t.attempts,
1155
+ retryMs: t.retryMs,
1156
+ ttfbMs,
1157
+ ...this.firstTokenAt !== void 0 ? { firstTokenMs: Math.max(0, this.firstTokenAt - t.dispatchedAt) } : {},
1158
+ wireMs: ttfbMs + this.readWaitMs
1159
+ };
1160
+ }
1161
+ };
1162
+ async function* withCallTiming(recorder, source) {
1163
+ for await (const chunk of source) {
1164
+ if (chunk.type === "text_delta" || chunk.type === "thinking_delta") {
1165
+ recorder.markFirstToken();
1166
+ yield chunk;
1167
+ } else if (chunk.type === "done") {
1168
+ const timing = recorder.streamTiming();
1169
+ yield timing ? { ...chunk, timing } : chunk;
1170
+ } else {
1171
+ yield chunk;
1172
+ }
1173
+ }
1174
+ }
1175
+ function withChatTiming(recorder, response) {
1176
+ const timing = recorder.chatTiming();
1177
+ return timing ? { ...response, timing } : response;
1178
+ }
1179
+
1070
1180
  // src/providers/rate-limiter.ts
1071
1181
  function abortError() {
1072
1182
  return Object.assign(new Error("The operation was aborted"), { name: "AbortError" });
@@ -1519,6 +1629,7 @@ var OpenAICompatibleProvider = class {
1519
1629
  async chat(messages, options) {
1520
1630
  const headers = this.buildHeaders(await this.resolveKey());
1521
1631
  const body = this.buildRequestBody(messages, options, false);
1632
+ const recorder = new CallTimingRecorder();
1522
1633
  const res = await fetchWithRetry(
1523
1634
  `${this.baseUrl}/chat/completions`,
1524
1635
  {
@@ -1527,7 +1638,7 @@ var OpenAICompatibleProvider = class {
1527
1638
  body: JSON.stringify(body),
1528
1639
  signal: options.signal
1529
1640
  },
1530
- { governor: this.governor, provider: this.name }
1641
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
1531
1642
  );
1532
1643
  if (!res.ok) {
1533
1644
  const errorBody = await res.text();
@@ -1536,15 +1647,20 @@ var OpenAICompatibleProvider = class {
1536
1647
  status: res.status,
1537
1648
  headers: res.headers,
1538
1649
  message: this.extractErrorMessage(errorBody, res.status),
1539
- body: errorBody
1650
+ body: errorBody,
1651
+ timing: recorder.chatTiming()
1540
1652
  });
1541
1653
  }
1542
1654
  const json = await res.json();
1543
- return this.parseResponse(json, this.requestModel(body, options.model), body);
1655
+ return withChatTiming(
1656
+ recorder,
1657
+ this.parseResponse(json, this.requestModel(body, options.model), body)
1658
+ );
1544
1659
  }
1545
1660
  async *stream(messages, options) {
1546
1661
  const headers = this.buildHeaders(await this.resolveKey());
1547
1662
  const body = this.buildRequestBody(messages, options, true);
1663
+ const recorder = new CallTimingRecorder();
1548
1664
  const res = await fetchWithRetry(
1549
1665
  `${this.baseUrl}/chat/completions`,
1550
1666
  {
@@ -1553,7 +1669,7 @@ var OpenAICompatibleProvider = class {
1553
1669
  body: JSON.stringify(body),
1554
1670
  signal: options.signal
1555
1671
  },
1556
- { governor: this.governor, provider: this.name }
1672
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
1557
1673
  );
1558
1674
  if (!res.ok) {
1559
1675
  const errorBody = await res.text();
@@ -1562,13 +1678,17 @@ var OpenAICompatibleProvider = class {
1562
1678
  status: res.status,
1563
1679
  headers: res.headers,
1564
1680
  message: this.extractErrorMessage(errorBody, res.status),
1565
- body: errorBody
1681
+ body: errorBody,
1682
+ timing: recorder.chatTiming()
1566
1683
  });
1567
1684
  }
1568
1685
  if (!res.body) {
1569
1686
  throw new Error(`${this.profile.label ?? this.profile.name} stream response has no body`);
1570
1687
  }
1571
- yield* this.parseSSEStream(res.body, this.requestModel(body, options.model), body);
1688
+ yield* withCallTiming(
1689
+ recorder,
1690
+ this.parseSSEStream(res.body, this.requestModel(body, options.model), body, recorder)
1691
+ );
1572
1692
  }
1573
1693
  // ---------------------------------------------------------------------------
1574
1694
  // Request building
@@ -1795,7 +1915,21 @@ var OpenAICompatibleProvider = class {
1795
1915
  // ---------------------------------------------------------------------------
1796
1916
  // SSE streaming
1797
1917
  // ---------------------------------------------------------------------------
1798
- async *parseSSEStream(body, model, request) {
1918
+ /**
1919
+ * PRIVATE, and deliberately not an extension point. It was `protected`, but
1920
+ * nothing in or out of this repo overrides it, and `protected` could not
1921
+ * carry the guarantee it appeared to: TypeScript checks an override for
1922
+ * assignability, and a function type with fewer parameters is assignable to
1923
+ * one with more — so an override that simply dropped the recorder still
1924
+ * compiled and then reported `wireMs === ttfbMs` for the whole subclass. Its
1925
+ * type is not exported from the barrel either, so a subclass author could not
1926
+ * name what they were required to accept.
1927
+ *
1928
+ * A provider with a different wire format implements `Provider` (see
1929
+ * docs/providers.md); an OpenAI-compatible one adds a `ProviderProfile`.
1930
+ * Neither needs to reach in here.
1931
+ */
1932
+ async *parseSSEStream(body, model, request, timing) {
1799
1933
  const reader = body.getReader();
1800
1934
  const decoder = new TextDecoder();
1801
1935
  let buffer = "";
@@ -1855,7 +1989,7 @@ var OpenAICompatibleProvider = class {
1855
1989
  };
1856
1990
  try {
1857
1991
  for (; ; ) {
1858
- const { done, value } = await reader.read();
1992
+ const { done, value } = await timing.read(reader);
1859
1993
  if (done) break;
1860
1994
  buffer += decoder.decode(value, { stream: true });
1861
1995
  const lines = buffer.split("\n");
@@ -1887,7 +2021,8 @@ var OpenAICompatibleProvider = class {
1887
2021
  status: 0,
1888
2022
  retryable: false,
1889
2023
  message,
1890
- body: JSON.stringify(parsed.error)
2024
+ body: JSON.stringify(parsed.error),
2025
+ timing: timing.streamTiming()
1891
2026
  });
1892
2027
  }
1893
2028
  if (parsed.model !== void 0 || parsed.service_tier !== void 0 || parsed.serviceTier !== void 0) {
@@ -1959,7 +2094,8 @@ var OpenAICompatibleProvider = class {
1959
2094
  provider: this.name,
1960
2095
  status: 0,
1961
2096
  retryable: true,
1962
- message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]`
2097
+ message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]`,
2098
+ timing: timing.streamTiming()
1963
2099
  });
1964
2100
  } finally {
1965
2101
  reader.releaseLock();
@@ -1998,12 +2134,12 @@ var DIRECT_OPENAI_CATALOG = [
1998
2134
  {
1999
2135
  aliases: ["gpt-5.6", "gpt-5.6-sol"],
2000
2136
  snapshotBase: "gpt-5.6-sol",
2001
- short: { input: 5 / M, cachedInput: 0.5 / M, cacheWrite: 6.25 / M, output: 30 / M },
2137
+ short: { input: 4 / M, cachedInput: 0.4 / M, cacheWrite: 5 / M, output: 20 / M },
2002
2138
  long: withLongContext({
2003
- input: 5 / M,
2004
- cachedInput: 0.5 / M,
2005
- cacheWrite: 6.25 / M,
2006
- output: 30 / M
2139
+ input: 4 / M,
2140
+ cachedInput: 0.4 / M,
2141
+ cacheWrite: 5 / M,
2142
+ output: 20 / M
2007
2143
  }),
2008
2144
  contextBoundary: LONG_CONTEXT_BOUNDARY
2009
2145
  },
@@ -2340,17 +2476,22 @@ function resolveOpenAIReasoningEffort(model, resolved) {
2340
2476
  }
2341
2477
  return resolved.thinkingDisabled ? clampReasoningEffort(model, "none") : void 0;
2342
2478
  }
2343
- var warnedChatMaxDowngrades = /* @__PURE__ */ new Set();
2344
2479
  function resolveOpenAIChatReasoningEffort(model, resolved) {
2345
2480
  const effort = resolveOpenAIReasoningEffort(model, resolved);
2346
- if (effort !== "max") return effort;
2347
- if (resolved.activeEffort === "max" && !warnedChatMaxDowngrades.has(model)) {
2348
- warnedChatMaxDowngrades.add(model);
2349
- console.warn(
2350
- `[axl] OpenAI Chat Completions does not accept effort "max" for ${model}; using "xhigh". Use the OpenAI Responses provider for native "max".`
2351
- );
2352
- }
2353
- return "xhigh";
2481
+ return effort === "max" ? "xhigh" : effort;
2482
+ }
2483
+ function resolveOpenAIEffortResolution(options, resolveWireEffort, endpoint) {
2484
+ const resolved = resolveThinkingOptions(options);
2485
+ if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
2486
+ const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
2487
+ const effective = resolveWireEffort(model, resolved);
2488
+ if (effective === void 0 || effective === resolved.effort) return void 0;
2489
+ return {
2490
+ requested: resolved.effort,
2491
+ effective,
2492
+ clamped: true,
2493
+ cause: `${endpoint} does not accept reasoning effort '${resolved.effort}' for ${model}; using '${effective}'`
2494
+ };
2354
2495
  }
2355
2496
  var openaiReasoningEmit = (body, resolved, model) => {
2356
2497
  const oSeries = isOSeriesModel(model);
@@ -2375,6 +2516,15 @@ var OpenAIProvider = class extends OpenAICompatibleProvider {
2375
2516
  constructor(options = {}) {
2376
2517
  super({ profile: OPENAI_PROFILE, ...options });
2377
2518
  }
2519
+ /** Report a clamped `effort` — Chat Completions caps `'max'` at `'xhigh'` and
2520
+ * rejects `'none'` before gpt-5.1 (clamped to `'minimal'`). */
2521
+ effortResolution(options) {
2522
+ return resolveOpenAIEffortResolution(
2523
+ options,
2524
+ resolveOpenAIChatReasoningEffort,
2525
+ "OpenAI Chat Completions"
2526
+ );
2527
+ }
2378
2528
  computeCost(model, usage, _reportedCost, context) {
2379
2529
  return usage ? estimateDirectOpenAICost(model, usage, { ...context, baseUrl: this.baseUrl }) : void 0;
2380
2530
  }
@@ -2421,6 +2571,12 @@ var OpenAIResponsesProvider = class {
2421
2571
  inputCapabilities(model) {
2422
2572
  return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
2423
2573
  }
2574
+ /** Report a clamped `effort`. Responses accepts the native `'max'` tier that
2575
+ * Chat Completions caps, but still clamps `'none'` before gpt-5.1, `'xhigh'`
2576
+ * below gpt-5.2, and everything to `'high'` on gpt-5-pro. */
2577
+ effortResolution(options) {
2578
+ return resolveOpenAIEffortResolution(options, resolveOpenAIReasoningEffort, "OpenAI Responses");
2579
+ }
2424
2580
  validateInput(request) {
2425
2581
  const modelOverride = request.providerOptions?.model;
2426
2582
  const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
@@ -2498,6 +2654,7 @@ var OpenAIResponsesProvider = class {
2498
2654
  async chat(messages, options) {
2499
2655
  const headers = this.buildHeaders(await this.resolveKey());
2500
2656
  const body = this.buildRequestBody(messages, options, false);
2657
+ const recorder = new CallTimingRecorder();
2501
2658
  const res = await fetchWithRetry(
2502
2659
  `${this.baseUrl}/responses`,
2503
2660
  {
@@ -2506,7 +2663,7 @@ var OpenAIResponsesProvider = class {
2506
2663
  body: JSON.stringify(body),
2507
2664
  signal: options.signal
2508
2665
  },
2509
- { governor: this.governor, provider: this.name }
2666
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
2510
2667
  );
2511
2668
  if (!res.ok) {
2512
2669
  const errorBody = await res.text();
@@ -2516,11 +2673,15 @@ var OpenAIResponsesProvider = class {
2516
2673
  status: res.status,
2517
2674
  headers: res.headers,
2518
2675
  message,
2519
- body: errorBody
2676
+ body: errorBody,
2677
+ timing: recorder.chatTiming()
2520
2678
  });
2521
2679
  }
2522
2680
  const json = await res.json();
2523
- return this.parseResponse(json, this.requestModel(body, options.model), body);
2681
+ return withChatTiming(
2682
+ recorder,
2683
+ this.parseResponse(json, this.requestModel(body, options.model), body)
2684
+ );
2524
2685
  }
2525
2686
  // ---------------------------------------------------------------------------
2526
2687
  // stream - SSE streaming
@@ -2528,6 +2689,7 @@ var OpenAIResponsesProvider = class {
2528
2689
  async *stream(messages, options) {
2529
2690
  const headers = this.buildHeaders(await this.resolveKey());
2530
2691
  const body = this.buildRequestBody(messages, options, true);
2692
+ const recorder = new CallTimingRecorder();
2531
2693
  const res = await fetchWithRetry(
2532
2694
  `${this.baseUrl}/responses`,
2533
2695
  {
@@ -2536,7 +2698,7 @@ var OpenAIResponsesProvider = class {
2536
2698
  body: JSON.stringify(body),
2537
2699
  signal: options.signal
2538
2700
  },
2539
- { governor: this.governor, provider: this.name }
2701
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
2540
2702
  );
2541
2703
  if (!res.ok) {
2542
2704
  const errorBody = await res.text();
@@ -2546,13 +2708,17 @@ var OpenAIResponsesProvider = class {
2546
2708
  status: res.status,
2547
2709
  headers: res.headers,
2548
2710
  message,
2549
- body: errorBody
2711
+ body: errorBody,
2712
+ timing: recorder.chatTiming()
2550
2713
  });
2551
2714
  }
2552
2715
  if (!res.body) {
2553
2716
  throw new Error("OpenAI Responses stream has no body");
2554
2717
  }
2555
- yield* this.parseSSEStream(res.body, this.requestModel(body, options.model), body);
2718
+ yield* withCallTiming(
2719
+ recorder,
2720
+ this.parseSSEStream(res.body, this.requestModel(body, options.model), body, recorder)
2721
+ );
2556
2722
  }
2557
2723
  // ---------------------------------------------------------------------------
2558
2724
  // Internal: build request body
@@ -2754,7 +2920,7 @@ var OpenAIResponsesProvider = class {
2754
2920
  // ---------------------------------------------------------------------------
2755
2921
  // Internal: parse SSE stream
2756
2922
  // ---------------------------------------------------------------------------
2757
- async *parseSSEStream(body, model, request) {
2923
+ async *parseSSEStream(body, model, request, timing) {
2758
2924
  const reader = body.getReader();
2759
2925
  const decoder = new TextDecoder();
2760
2926
  let buffer = "";
@@ -2762,7 +2928,7 @@ var OpenAIResponsesProvider = class {
2762
2928
  let eventType = "";
2763
2929
  try {
2764
2930
  while (true) {
2765
- const { done, value } = await reader.read();
2931
+ const { done, value } = await timing.read(reader);
2766
2932
  if (done) break;
2767
2933
  buffer += decoder.decode(value, { stream: true });
2768
2934
  const lines = buffer.split("\n");
@@ -2782,7 +2948,14 @@ var OpenAIResponsesProvider = class {
2782
2948
  } catch {
2783
2949
  continue;
2784
2950
  }
2785
- const chunk = this.handleStreamEvent(eventType, data, model, callIdMap, request);
2951
+ const chunk = this.handleStreamEvent(
2952
+ eventType,
2953
+ data,
2954
+ model,
2955
+ callIdMap,
2956
+ request,
2957
+ timing
2958
+ );
2786
2959
  if (chunk) {
2787
2960
  yield chunk;
2788
2961
  if (chunk.type === "done") return;
@@ -2796,7 +2969,7 @@ var OpenAIResponsesProvider = class {
2796
2969
  reader.releaseLock();
2797
2970
  }
2798
2971
  }
2799
- handleStreamEvent(eventType, data, model, callIdMap, request) {
2972
+ handleStreamEvent(eventType, data, model, callIdMap, request, timing) {
2800
2973
  switch (eventType) {
2801
2974
  case "response.output_text.delta":
2802
2975
  return { type: "text_delta", content: data.delta ?? "" };
@@ -2852,7 +3025,8 @@ var OpenAIResponsesProvider = class {
2852
3025
  provider: this.name,
2853
3026
  status: 0,
2854
3027
  retryable: false,
2855
- message: `OpenAI Responses API error: ${errorMsg}`
3028
+ message: `OpenAI Responses API error: ${errorMsg}`,
3029
+ timing: timing.streamTiming()
2856
3030
  });
2857
3031
  }
2858
3032
  default:
@@ -2920,6 +3094,12 @@ function hasAnthropicProviderFileImage(messages) {
2920
3094
  );
2921
3095
  }
2922
3096
  var CLAUDE_CAPABILITIES = {
3097
+ "claude-fable-5-1": {
3098
+ thinking: "adaptive-always-on",
3099
+ effortLevels: ["low", "medium", "high", "xhigh", "max"],
3100
+ manualBudget: false,
3101
+ stripTemperature: true
3102
+ },
2923
3103
  "claude-fable-5": {
2924
3104
  thinking: "adaptive-always-on",
2925
3105
  effortLevels: ["low", "medium", "high", "xhigh", "max"],
@@ -3010,10 +3190,18 @@ var PRE_INFERENCE_GEO_MODELS = /* @__PURE__ */ new Set([
3010
3190
  function resolveClaudeCapability(model) {
3011
3191
  return CLAUDE_CAPABILITIES[model] ?? (LEGACY_CLAUDE_MODELS.has(model) ? LEGACY_CLAUDE_CAPABILITY : void 0);
3012
3192
  }
3193
+ var DEFAULT_CACHE_READ_MULTIPLIER = 0.1;
3013
3194
  var ANTHROPIC_RATES = {
3195
+ "claude-fable-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
3014
3196
  "claude-fable-5": { input: 1e-5, output: 5e-5 },
3197
+ // Limited availability (Glasswing). Priced from the public pricing table; no
3198
+ // CLAUDE_CAPABILITIES entry, because their thinking semantics are not
3199
+ // documented per-model and an unverified capability would send the wrong
3200
+ // request shape. Unknown IDs pass through unmodified, which is the safe side.
3201
+ "claude-mythos-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
3202
+ "claude-mythos-5": { input: 1e-5, output: 5e-5 },
3015
3203
  "claude-opus-5": { input: 5e-6, output: 25e-6 },
3016
- "claude-sonnet-5": { input: 3e-6, output: 15e-6, sonnet5Intro: true },
3204
+ "claude-sonnet-5": { input: 2e-6, output: 1e-5 },
3017
3205
  "claude-opus-4-8": { input: 5e-6, output: 25e-6 },
3018
3206
  "claude-opus-4-7": { input: 5e-6, output: 25e-6 },
3019
3207
  "claude-opus-4-6": { input: 5e-6, output: 25e-6 },
@@ -3052,7 +3240,7 @@ function anthropicStreamErrorStatus(type) {
3052
3240
  return 0;
3053
3241
  }
3054
3242
  }
3055
- function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
3243
+ function estimateAnthropicCost(model, usage) {
3056
3244
  if (!model || !isValidTokenCount(usage.inputTokens) || !isValidTokenCount(usage.outputTokens)) {
3057
3245
  return void 0;
3058
3246
  }
@@ -3071,8 +3259,9 @@ function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
3071
3259
  if (usage.aggregateCacheWriteTokens !== void 0 && (!isValidTokenCount(usage.aggregateCacheWriteTokens) || usage.aggregateCacheWriteTokens !== cacheWriteTotal)) {
3072
3260
  return void 0;
3073
3261
  }
3074
- const { input, output } = rate.sonnet5Intro && now < /* @__PURE__ */ new Date("2026-09-01T00:00:00Z") ? { input: 2e-6, output: 1e-5 } : rate;
3075
- return usage.inputTokens * input + cacheRead * input * 0.1 + cacheWrite5m * input * 1.25 + cacheWrite1h * input * 2 + usage.outputTokens * output;
3262
+ const { input, output } = rate;
3263
+ const cacheReadRate = input * (rate.cacheReadMultiplier ?? DEFAULT_CACHE_READ_MULTIPLIER);
3264
+ return usage.inputTokens * input + cacheRead * cacheReadRate + cacheWrite5m * input * 1.25 + cacheWrite1h * input * 2 + usage.outputTokens * output;
3076
3265
  }
3077
3266
  var THINKING_BUDGETS = {
3078
3267
  low: 1024,
@@ -3096,14 +3285,6 @@ function budgetToClaudeEffort(budget) {
3096
3285
  if (budget <= THINKING_BUDGETS.xhigh) return "xhigh";
3097
3286
  return "max";
3098
3287
  }
3099
- var warnedFableDisable = /* @__PURE__ */ new Set();
3100
- function warnFableDisable(model) {
3101
- if (warnedFableDisable.has(model)) return;
3102
- warnedFableDisable.add(model);
3103
- console.warn(
3104
- `[axl] thinking cannot be disabled on Anthropic ${model}; using adaptive thinking at effort 'low'.`
3105
- );
3106
- }
3107
3288
  function resolveClaudeThinking(model, resolved) {
3108
3289
  const capability = resolveClaudeCapability(model);
3109
3290
  if (!capability) return { stripTemperature: false };
@@ -3123,11 +3304,14 @@ function resolveClaudeThinking(model, resolved) {
3123
3304
  }
3124
3305
  if (resolved.thinkingDisabled) {
3125
3306
  if (capability.thinking === "adaptive-always-on") {
3126
- warnFableDisable(model);
3127
3307
  return {
3128
3308
  thinking: { type: "adaptive" },
3129
3309
  outputConfig: { effort: "low" },
3130
- stripTemperature: true
3310
+ stripTemperature: true,
3311
+ clamp: {
3312
+ effective: "low",
3313
+ cause: `thinking cannot be disabled on Anthropic ${model}; using adaptive thinking at effort 'low'`
3314
+ }
3131
3315
  };
3132
3316
  }
3133
3317
  if (activeEffort && capability.disableAt && !isEffortAtOrBelow(activeEffort, capability.disableAt)) {
@@ -3189,7 +3373,16 @@ function resolveClaudeThinking(model, resolved) {
3189
3373
  return {
3190
3374
  thinking: { type: "enabled", budget_tokens: budget },
3191
3375
  manualBudget: budget,
3192
- stripTemperature: true
3376
+ stripTemperature: true,
3377
+ // Legacy models take `budget_tokens`, not an effort level, and have no
3378
+ // `effortLevels` — so `clampAnthropicEffort` folds 'max'/'xhigh' down to
3379
+ // the 'high' tier and the request silently carries that tier's budget.
3380
+ // Report it: an unreported budget-tier downgrade is exactly the silent
3381
+ // clamp this capability exists to expose.
3382
+ clamp: activeEffort === resolved.activeEffort ? void 0 : {
3383
+ effective: activeEffort,
3384
+ cause: `Anthropic ${model} takes thinking budget_tokens rather than an effort level, and its ceiling tier is '${activeEffort}'; effort '${resolved.activeEffort}' sends budget_tokens ${budget}`
3385
+ }
3193
3386
  };
3194
3387
  }
3195
3388
  return { stripTemperature: capability.stripTemperature ?? false };
@@ -3300,6 +3493,34 @@ function mergeAnthropicUsage(initial, update) {
3300
3493
  }
3301
3494
  var AnthropicProvider = class {
3302
3495
  name = "anthropic";
3496
+ /** Report a clamped `effort`. Anthropic clamps to each model's supported
3497
+ * `output_config.effort` levels, and models that always think fall back to
3498
+ * adaptive `'low'` when asked to disable thinking. Only reported when the
3499
+ * effective level is what the request body actually carries; an explicit
3500
+ * `thinkingBudget` is documented to override `effort`, so it is not a clamp. */
3501
+ effortResolution(options) {
3502
+ const resolved = resolveThinkingOptions(options);
3503
+ if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
3504
+ const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
3505
+ if (ANTHROPIC_RATES[model] && !resolveClaudeCapability(model)) {
3506
+ return {
3507
+ requested: resolved.effort,
3508
+ effective: "unset",
3509
+ clamped: true,
3510
+ cause: `Axl has no capability entry for Anthropic ${model}, so no reasoning control was sent and effort '${resolved.effort}' had no effect`
3511
+ };
3512
+ }
3513
+ const config = resolveClaudeThinking(model, resolved);
3514
+ const wireEffort = typeof config.outputConfig?.effort === "string" ? config.outputConfig.effort : void 0;
3515
+ const effective = config.clamp?.effective ?? wireEffort;
3516
+ if (effective === void 0 || effective === resolved.effort) return void 0;
3517
+ return {
3518
+ requested: resolved.effort,
3519
+ effective,
3520
+ clamped: true,
3521
+ cause: config.clamp?.cause ?? `Anthropic ${model} does not support effort '${resolved.effort}'; using '${effective}'`
3522
+ };
3523
+ }
3303
3524
  inputCapabilities(model) {
3304
3525
  return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
3305
3526
  }
@@ -3344,6 +3565,10 @@ var AnthropicProvider = class {
3344
3565
  }
3345
3566
  /** Anthropic ignores native `json_schema` structurally — Axl uses a system
3346
3567
  * prompt JSON instruction + client-side Zod validation (see buildRequest). */
3568
+ /** The only built-in adapter that honors `promptCache` (see buildBody). */
3569
+ realizesPromptCache() {
3570
+ return true;
3571
+ }
3347
3572
  nativeStructuredOutputSupport() {
3348
3573
  return "unsupported";
3349
3574
  }
@@ -3385,6 +3610,7 @@ var AnthropicProvider = class {
3385
3610
  );
3386
3611
  const body = this.buildRequestBody(messages, options, false);
3387
3612
  const pricingContext = pricingContextFromBody(body);
3613
+ const recorder = new CallTimingRecorder();
3388
3614
  const res = await fetchWithRetry(
3389
3615
  `${this.baseUrl}/messages`,
3390
3616
  {
@@ -3393,7 +3619,7 @@ var AnthropicProvider = class {
3393
3619
  body: JSON.stringify(body),
3394
3620
  signal: options.signal
3395
3621
  },
3396
- { governor: this.governor, provider: this.name }
3622
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
3397
3623
  );
3398
3624
  if (!res.ok) {
3399
3625
  const errorBody = await res.text();
@@ -3403,11 +3629,12 @@ var AnthropicProvider = class {
3403
3629
  status: res.status,
3404
3630
  headers: res.headers,
3405
3631
  message,
3406
- body: errorBody
3632
+ body: errorBody,
3633
+ timing: recorder.chatTiming()
3407
3634
  });
3408
3635
  }
3409
3636
  const json = await res.json();
3410
- return this.parseResponse(json, pricingContext);
3637
+ return withChatTiming(recorder, this.parseResponse(json, pricingContext));
3411
3638
  }
3412
3639
  // ---------------------------------------------------------------------------
3413
3640
  // stream - SSE streaming completion
@@ -3419,6 +3646,7 @@ var AnthropicProvider = class {
3419
3646
  );
3420
3647
  const body = this.buildRequestBody(messages, options, true);
3421
3648
  const pricingContext = pricingContextFromBody(body);
3649
+ const recorder = new CallTimingRecorder();
3422
3650
  const res = await fetchWithRetry(
3423
3651
  `${this.baseUrl}/messages`,
3424
3652
  {
@@ -3427,7 +3655,7 @@ var AnthropicProvider = class {
3427
3655
  body: JSON.stringify(body),
3428
3656
  signal: options.signal
3429
3657
  },
3430
- { governor: this.governor, provider: this.name }
3658
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
3431
3659
  );
3432
3660
  if (!res.ok) {
3433
3661
  const errorBody = await res.text();
@@ -3437,13 +3665,17 @@ var AnthropicProvider = class {
3437
3665
  status: res.status,
3438
3666
  headers: res.headers,
3439
3667
  message,
3440
- body: errorBody
3668
+ body: errorBody,
3669
+ timing: recorder.chatTiming()
3441
3670
  });
3442
3671
  }
3443
3672
  if (!res.body) {
3444
3673
  throw new Error("Anthropic stream response has no body");
3445
3674
  }
3446
- yield* this.parseSSEStream(res.body, pricingContext, res.headers);
3675
+ yield* withCallTiming(
3676
+ recorder,
3677
+ this.parseSSEStream(res.body, pricingContext, res.headers, recorder)
3678
+ );
3447
3679
  }
3448
3680
  // ---------------------------------------------------------------------------
3449
3681
  // Internal: request building
@@ -3478,14 +3710,31 @@ var AnthropicProvider = class {
3478
3710
  max_tokens: options.maxTokens ?? 4096,
3479
3711
  stream
3480
3712
  };
3481
- if (systemText) {
3713
+ const po = options.providerOptions;
3714
+ const callerOwnsPrefix = po !== void 0 && (po.cache_control !== void 0 || po.system !== void 0 || po.tools !== void 0);
3715
+ const promptCache = options.promptCache === true && !callerOwnsPrefix;
3716
+ const anchorOnSystem = promptCache && systemMessages.length > 0 && systemMessages[0].origin !== "runtime";
3717
+ if (anchorOnSystem) {
3718
+ body.system = systemMessages.map((m, index) => ({
3719
+ type: "text",
3720
+ text: typeof m.content === "string" ? m.content : "",
3721
+ ...index === 0 ? { cache_control: { type: "ephemeral" } } : {}
3722
+ }));
3723
+ } else if (systemText) {
3482
3724
  body.system = systemText;
3483
3725
  }
3484
3726
  if (options.stop) {
3485
3727
  body.stop_sequences = options.stop;
3486
3728
  }
3487
3729
  if (options.tools && options.tools.length > 0) {
3488
- body.tools = options.tools.map((t) => this.mapToolDefinition(t));
3730
+ const tools = options.tools.map((t) => this.mapToolDefinition(t));
3731
+ if (promptCache && !anchorOnSystem) {
3732
+ tools[tools.length - 1] = {
3733
+ ...tools[tools.length - 1],
3734
+ cache_control: { type: "ephemeral" }
3735
+ };
3736
+ }
3737
+ body.tools = tools;
3489
3738
  }
3490
3739
  if (options.toolChoice !== void 0) {
3491
3740
  body.tool_choice = this.mapToolChoice(options.toolChoice);
@@ -3504,9 +3753,13 @@ var AnthropicProvider = class {
3504
3753
  }
3505
3754
  if (options.responseFormat && options.responseFormat.type !== "text") {
3506
3755
  const jsonInstruction = "You must respond with valid JSON only. No markdown fences, no extra text.";
3507
- body.system = body.system ? `${body.system}
3756
+ if (Array.isArray(body.system)) {
3757
+ body.system = [...body.system, { type: "text", text: jsonInstruction }];
3758
+ } else {
3759
+ body.system = body.system ? `${body.system}
3508
3760
 
3509
3761
  ${jsonInstruction}` : jsonInstruction;
3762
+ }
3510
3763
  }
3511
3764
  if (options.providerOptions) {
3512
3765
  Object.assign(body, options.providerOptions);
@@ -3686,7 +3939,7 @@ ${jsonInstruction}` : jsonInstruction;
3686
3939
  // ---------------------------------------------------------------------------
3687
3940
  // Internal: SSE stream parsing
3688
3941
  // ---------------------------------------------------------------------------
3689
- async *parseSSEStream(body, pricingContext, responseHeaders) {
3942
+ async *parseSSEStream(body, pricingContext, responseHeaders, timing) {
3690
3943
  const reader = body.getReader();
3691
3944
  const decoder = new TextDecoder();
3692
3945
  let buffer = "";
@@ -3715,7 +3968,7 @@ ${jsonInstruction}` : jsonInstruction;
3715
3968
  });
3716
3969
  try {
3717
3970
  while (true) {
3718
- const { done, value } = await reader.read();
3971
+ const { done, value } = await timing.read(reader);
3719
3972
  if (done) break;
3720
3973
  buffer += decoder.decode(value, { stream: true });
3721
3974
  const lines = buffer.split("\n");
@@ -3738,7 +3991,8 @@ ${jsonInstruction}` : jsonInstruction;
3738
3991
  provider: this.name,
3739
3992
  status,
3740
3993
  headers: responseHeaders,
3741
- message: event.error?.message ?? "Anthropic stream failed"
3994
+ message: event.error?.message ?? "Anthropic stream failed",
3995
+ timing: timing.streamTiming()
3742
3996
  });
3743
3997
  }
3744
3998
  case "content_block_start": {
@@ -3974,7 +4228,6 @@ var GEMINI_PRICING = {
3974
4228
  },
3975
4229
  "gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7 },
3976
4230
  "gemini-2.5-flash-lite": { input: 1e-7, cached: 1e-8, output: 4e-7 },
3977
- "gemini-3-flash-preview": { input: 5e-7, cached: 5e-8, output: 3e-6 },
3978
4231
  "gemini-3.1-pro-preview": {
3979
4232
  input: 2e-6,
3980
4233
  cached: 2e-7,
@@ -3990,7 +4243,9 @@ var GEMINI_PRICING = {
3990
4243
  "gemini-3.1-flash-lite": { input: 25e-8, cached: 25e-9, output: 15e-7 },
3991
4244
  "gemini-3.5-flash": { input: 15e-7, cached: 15e-8, output: 9e-6 },
3992
4245
  "gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
3993
- "gemini-3.6-flash": { input: 15e-7, cached: 15e-8, output: 75e-7 }
4246
+ "gemini-3.6-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
4247
+ "gemini-3.7-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
4248
+ "gemini-3.8-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 }
3994
4249
  };
3995
4250
  function isValidTokenCount2(value) {
3996
4251
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
@@ -4150,13 +4405,71 @@ function budgetToThinkingLevel(budgetTokens) {
4150
4405
  function minThinkingLevel(model) {
4151
4406
  return GEMINI_3X_MIN_THINKING_LEVEL.get(model) ?? "minimal";
4152
4407
  }
4153
- var _warned3xEffortNone = /* @__PURE__ */ new Set();
4154
- function warnGemini3xEffortNone(model) {
4155
- if (_warned3xEffortNone.has(model)) return;
4156
- _warned3xEffortNone.add(model);
4157
- console.warn(
4158
- `[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.`
4159
- );
4408
+ function resolveGeminiThinking(model, resolved) {
4409
+ const is3x = isGemini3x(model);
4410
+ if (resolved.thinkingDisabled) {
4411
+ const minLevel = minThinkingLevel(model);
4412
+ if (is3x) {
4413
+ return {
4414
+ interactionLevel: minLevel,
4415
+ thinkingLevel: minLevel,
4416
+ thinkingBudget: void 0,
4417
+ attachThoughts: false,
4418
+ // A `thinkingBudget: 0` disable has no unified effort to report against;
4419
+ // only an explicit `effort: 'none'` is a clamp of a requested value.
4420
+ clamp: resolved.effort === "none" ? {
4421
+ effective: minLevel,
4422
+ cause: `Gemini 3.x models cannot disable thinking; using the model minimum thinking level '${minLevel}'`
4423
+ } : void 0
4424
+ };
4425
+ }
4426
+ return {
4427
+ interactionLevel: "minimal",
4428
+ thinkingLevel: void 0,
4429
+ thinkingBudget: 0,
4430
+ attachThoughts: false
4431
+ };
4432
+ }
4433
+ if (resolved.hasBudgetOverride) {
4434
+ const budget = resolved.thinkingBudget;
4435
+ const level = budgetToThinkingLevel(budget);
4436
+ return {
4437
+ interactionLevel: level,
4438
+ thinkingLevel: is3x ? level : void 0,
4439
+ thinkingBudget: is3x ? void 0 : budget,
4440
+ attachThoughts: resolved.includeThoughts
4441
+ };
4442
+ }
4443
+ if (resolved.activeEffort) {
4444
+ const effort = resolved.activeEffort;
4445
+ const budget = THINKING_BUDGETS2[effort] ?? 5e3;
4446
+ if (is3x) {
4447
+ const level = THINKING_LEVELS[effort] ?? "medium";
4448
+ return {
4449
+ interactionLevel: level,
4450
+ thinkingLevel: level,
4451
+ thinkingBudget: void 0,
4452
+ attachThoughts: resolved.includeThoughts,
4453
+ clamp: level === effort ? void 0 : {
4454
+ effective: level,
4455
+ cause: `Gemini 3.x thinking levels cap at 'high'; effort '${effort}' maps to '${level}'`
4456
+ }
4457
+ };
4458
+ }
4459
+ return {
4460
+ interactionLevel: budgetToThinkingLevel(budget),
4461
+ thinkingLevel: void 0,
4462
+ // 2.5 Pro supports a higher max budget (32768) than other 2.5 models (24576).
4463
+ thinkingBudget: effort === "max" && model === "gemini-2.5-pro" ? 32768 : budget,
4464
+ attachThoughts: resolved.includeThoughts
4465
+ };
4466
+ }
4467
+ return {
4468
+ interactionLevel: void 0,
4469
+ thinkingLevel: void 0,
4470
+ thinkingBudget: void 0,
4471
+ attachThoughts: resolved.includeThoughts
4472
+ };
4160
4473
  }
4161
4474
  var GeminiProvider = class {
4162
4475
  name = "google";
@@ -4225,6 +4538,21 @@ var GeminiProvider = class {
4225
4538
  nativeStructuredOutputSupport() {
4226
4539
  return "lossy";
4227
4540
  }
4541
+ /** Report a clamped `effort` (R7a). Gemini 3.x cannot disable thinking and
4542
+ * caps at `'high'`, so `'none'`, `'xhigh'` and `'max'` are not honored
4543
+ * verbatim there. Everything else — including 2.x disabled thinking — is
4544
+ * sent as asked, so there is nothing to report. */
4545
+ effortResolution(options) {
4546
+ const resolved = resolveThinkingOptions(options);
4547
+ if (resolved.effort === void 0) return void 0;
4548
+ const clamp = resolveGeminiThinking(this.requestModel(options), resolved).clamp;
4549
+ return clamp ? {
4550
+ requested: resolved.effort,
4551
+ effective: clamp.effective,
4552
+ clamped: true,
4553
+ cause: clamp.cause
4554
+ } : void 0;
4555
+ }
4228
4556
  baseUrl;
4229
4557
  apiKeySource;
4230
4558
  callCounter = 0;
@@ -4262,6 +4590,7 @@ var GeminiProvider = class {
4262
4590
  const pricingContext = this.pricingContext(this.requestModel(options), body);
4263
4591
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
4264
4592
  const headers = this.buildHeaders(await this.resolveKey());
4593
+ const recorder = new CallTimingRecorder();
4265
4594
  const res = await fetchWithRetry(
4266
4595
  `${this.baseUrl}/models/${pricingContext.model}:generateContent`,
4267
4596
  {
@@ -4270,7 +4599,7 @@ var GeminiProvider = class {
4270
4599
  body: JSON.stringify(body),
4271
4600
  signal: options.signal
4272
4601
  },
4273
- { governor: this.governor, provider: this.name }
4602
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4274
4603
  );
4275
4604
  if (!res.ok) {
4276
4605
  const errorBody = await res.text();
@@ -4280,11 +4609,12 @@ var GeminiProvider = class {
4280
4609
  status: res.status,
4281
4610
  headers: res.headers,
4282
4611
  message,
4283
- body: errorBody
4612
+ body: errorBody,
4613
+ timing: recorder.chatTiming()
4284
4614
  });
4285
4615
  }
4286
4616
  const json = await res.json();
4287
- return this.parseResponse(json, pricingContext);
4617
+ return withChatTiming(recorder, this.parseResponse(json, pricingContext));
4288
4618
  }
4289
4619
  // ---------------------------------------------------------------------------
4290
4620
  // stream - SSE streaming completion
@@ -4298,6 +4628,7 @@ var GeminiProvider = class {
4298
4628
  const pricingContext = this.pricingContext(this.requestModel(options), body);
4299
4629
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
4300
4630
  const headers = this.buildHeaders(await this.resolveKey());
4631
+ const recorder = new CallTimingRecorder();
4301
4632
  const res = await fetchWithRetry(
4302
4633
  `${this.baseUrl}/models/${pricingContext.model}:streamGenerateContent?alt=sse`,
4303
4634
  {
@@ -4306,7 +4637,7 @@ var GeminiProvider = class {
4306
4637
  body: JSON.stringify(body),
4307
4638
  signal: options.signal
4308
4639
  },
4309
- { governor: this.governor, provider: this.name }
4640
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4310
4641
  );
4311
4642
  if (!res.ok) {
4312
4643
  const errorBody = await res.text();
@@ -4316,13 +4647,14 @@ var GeminiProvider = class {
4316
4647
  status: res.status,
4317
4648
  headers: res.headers,
4318
4649
  message,
4319
- body: errorBody
4650
+ body: errorBody,
4651
+ timing: recorder.chatTiming()
4320
4652
  });
4321
4653
  }
4322
4654
  if (!res.body) {
4323
4655
  throw new Error("Gemini stream response has no body");
4324
4656
  }
4325
- yield* this.parseSSEStream(res.body, pricingContext);
4657
+ yield* withCallTiming(recorder, this.parseSSEStream(res.body, pricingContext, recorder));
4326
4658
  }
4327
4659
  // ---------------------------------------------------------------------------
4328
4660
  // Rich image requests use Gemini Interactions. It is deliberately stateless:
@@ -4333,10 +4665,11 @@ var GeminiProvider = class {
4333
4665
  this.assertSafeInteractionOptions(options);
4334
4666
  const body = this.buildInteractionRequestBody(messages, options, false);
4335
4667
  const headers = this.buildHeaders(await this.resolveKey());
4668
+ const recorder = new CallTimingRecorder();
4336
4669
  const res = await fetchWithRetry(
4337
4670
  `${this.baseUrl}/interactions`,
4338
4671
  { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
4339
- { governor: this.governor, provider: this.name }
4672
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4340
4673
  );
4341
4674
  if (!res.ok) {
4342
4675
  const errorBody = await res.text();
@@ -4345,19 +4678,24 @@ var GeminiProvider = class {
4345
4678
  status: res.status,
4346
4679
  headers: res.headers,
4347
4680
  message: this.extractErrorMessage(errorBody, res.status),
4348
- body: errorBody
4681
+ body: errorBody,
4682
+ timing: recorder.chatTiming()
4349
4683
  });
4350
4684
  }
4351
- return this.parseInteractionResponse(await res.json());
4685
+ return withChatTiming(
4686
+ recorder,
4687
+ this.parseInteractionResponse(await res.json())
4688
+ );
4352
4689
  }
4353
4690
  async *streamInteraction(messages, options) {
4354
4691
  this.assertSafeInteractionOptions(options);
4355
4692
  const body = this.buildInteractionRequestBody(messages, options, true);
4356
4693
  const headers = this.buildHeaders(await this.resolveKey());
4694
+ const recorder = new CallTimingRecorder();
4357
4695
  const res = await fetchWithRetry(
4358
4696
  `${this.baseUrl}/interactions?alt=sse`,
4359
4697
  { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
4360
- { governor: this.governor, provider: this.name }
4698
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4361
4699
  );
4362
4700
  if (!res.ok) {
4363
4701
  const errorBody = await res.text();
@@ -4366,11 +4704,15 @@ var GeminiProvider = class {
4366
4704
  status: res.status,
4367
4705
  headers: res.headers,
4368
4706
  message: this.extractErrorMessage(errorBody, res.status),
4369
- body: errorBody
4707
+ body: errorBody,
4708
+ timing: recorder.chatTiming()
4370
4709
  });
4371
4710
  }
4372
4711
  if (!res.body) throw new Error("Gemini Interactions stream has no body");
4373
- yield* this.parseInteractionSSEStream(res.body, res.headers);
4712
+ yield* withCallTiming(
4713
+ recorder,
4714
+ this.parseInteractionSSEStream(res.body, res.headers, recorder)
4715
+ );
4374
4716
  }
4375
4717
  buildInteractionRequestBody(messages, options, stream) {
4376
4718
  const model = this.requestModel(options);
@@ -4399,12 +4741,9 @@ var GeminiProvider = class {
4399
4741
  generationConfig.tool_choice = typeof options.toolChoice === "string" ? options.toolChoice === "required" ? "any" : options.toolChoice : { allowed_tools: { mode: "any", tools: [options.toolChoice.function.name] } };
4400
4742
  }
4401
4743
  const thinking = resolveThinkingOptions(options);
4402
- if (thinking.thinkingDisabled) {
4403
- generationConfig.thinking_level = isGemini3x(model) ? minThinkingLevel(model) : "minimal";
4404
- } else if (thinking.hasBudgetOverride) {
4405
- generationConfig.thinking_level = budgetToThinkingLevel(thinking.thinkingBudget);
4406
- } else if (thinking.activeEffort) {
4407
- generationConfig.thinking_level = isGemini3x(model) ? THINKING_LEVELS[thinking.activeEffort] ?? "medium" : budgetToThinkingLevel(THINKING_BUDGETS2[thinking.activeEffort] ?? 5e3);
4744
+ const thinkingPlan = resolveGeminiThinking(model, thinking);
4745
+ if (thinkingPlan.interactionLevel !== void 0) {
4746
+ generationConfig.thinking_level = thinkingPlan.interactionLevel;
4408
4747
  }
4409
4748
  if (thinking.includeThoughts) generationConfig.thinking_summaries = "auto";
4410
4749
  if (Object.keys(generationConfig).length) body.generation_config = generationConfig;
@@ -4529,7 +4868,7 @@ var GeminiProvider = class {
4529
4868
  providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
4530
4869
  };
4531
4870
  }
4532
- async *parseInteractionSSEStream(body, responseHeaders) {
4871
+ async *parseInteractionSSEStream(body, responseHeaders, timing) {
4533
4872
  const reader = body.getReader();
4534
4873
  const decoder = new TextDecoder();
4535
4874
  let buffer = "";
@@ -4539,7 +4878,7 @@ var GeminiProvider = class {
4539
4878
  let completed = false;
4540
4879
  try {
4541
4880
  while (true) {
4542
- const { done, value } = await reader.read();
4881
+ const { done, value } = await timing.read(reader);
4543
4882
  if (done) break;
4544
4883
  buffer += decoder.decode(value, { stream: true });
4545
4884
  const lines = buffer.split("\n");
@@ -4562,14 +4901,16 @@ var GeminiProvider = class {
4562
4901
  provider: this.name,
4563
4902
  status,
4564
4903
  headers: responseHeaders,
4565
- message
4904
+ message,
4905
+ timing: timing.streamTiming()
4566
4906
  });
4567
4907
  }
4568
4908
  throw new ProviderError({
4569
4909
  provider: this.name,
4570
4910
  status: 0,
4571
4911
  retryable: false,
4572
- message
4912
+ message,
4913
+ timing: timing.streamTiming()
4573
4914
  });
4574
4915
  }
4575
4916
  if (event.event_type === "step.start" && event.step) {
@@ -4840,50 +5181,17 @@ var GeminiProvider = class {
4840
5181
  if (Object.keys(generationConfig).length > 0) {
4841
5182
  body.generationConfig = generationConfig;
4842
5183
  }
4843
- const {
4844
- effort,
4845
- thinkingBudget,
4846
- includeThoughts,
4847
- thinkingDisabled,
4848
- activeEffort,
4849
- hasBudgetOverride
4850
- } = resolveThinkingOptions(options);
4851
- if (thinkingDisabled) {
4852
- if (isGemini3x(requestModel)) {
4853
- if (effort === "none") {
4854
- warnGemini3xEffortNone(requestModel);
4855
- }
4856
- generationConfig.thinkingConfig = { thinkingLevel: minThinkingLevel(requestModel) };
4857
- } else {
4858
- generationConfig.thinkingConfig = { thinkingBudget: 0 };
4859
- }
4860
- if (!body.generationConfig) body.generationConfig = generationConfig;
4861
- } else if (hasBudgetOverride) {
4862
- const config = {};
4863
- if (isGemini3x(requestModel)) {
4864
- config.thinkingLevel = budgetToThinkingLevel(thinkingBudget);
4865
- } else {
4866
- config.thinkingBudget = thinkingBudget;
4867
- }
4868
- if (includeThoughts) config.includeThoughts = true;
4869
- generationConfig.thinkingConfig = config;
4870
- if (!body.generationConfig) body.generationConfig = generationConfig;
4871
- } else if (activeEffort) {
4872
- const config = {};
4873
- if (isGemini3x(requestModel)) {
4874
- config.thinkingLevel = THINKING_LEVELS[activeEffort] ?? "medium";
4875
- } else {
4876
- if (activeEffort === "max" && requestModel === "gemini-2.5-pro") {
4877
- config.thinkingBudget = 32768;
4878
- } else {
4879
- config.thinkingBudget = THINKING_BUDGETS2[activeEffort] ?? 5e3;
4880
- }
4881
- }
4882
- if (includeThoughts) config.includeThoughts = true;
4883
- generationConfig.thinkingConfig = config;
4884
- if (!body.generationConfig) body.generationConfig = generationConfig;
4885
- } else if (includeThoughts) {
4886
- generationConfig.thinkingConfig = { includeThoughts: true };
5184
+ const thinkingPlan = resolveGeminiThinking(requestModel, resolveThinkingOptions(options));
5185
+ const thinkingConfig = {};
5186
+ if (thinkingPlan.thinkingLevel !== void 0) {
5187
+ thinkingConfig.thinkingLevel = thinkingPlan.thinkingLevel;
5188
+ }
5189
+ if (thinkingPlan.thinkingBudget !== void 0) {
5190
+ thinkingConfig.thinkingBudget = thinkingPlan.thinkingBudget;
5191
+ }
5192
+ if (thinkingPlan.attachThoughts) thinkingConfig.includeThoughts = true;
5193
+ if (Object.keys(thinkingConfig).length > 0) {
5194
+ generationConfig.thinkingConfig = thinkingConfig;
4887
5195
  if (!body.generationConfig) body.generationConfig = generationConfig;
4888
5196
  }
4889
5197
  if (options.toolChoice !== void 0) {
@@ -5080,7 +5388,7 @@ var GeminiProvider = class {
5080
5388
  // ---------------------------------------------------------------------------
5081
5389
  // Internal: SSE stream parsing
5082
5390
  // ---------------------------------------------------------------------------
5083
- async *parseSSEStream(body, pricingContext) {
5391
+ async *parseSSEStream(body, pricingContext, timing) {
5084
5392
  const reader = body.getReader();
5085
5393
  const decoder = new TextDecoder();
5086
5394
  let buffer = "";
@@ -5093,7 +5401,7 @@ var GeminiProvider = class {
5093
5401
  const accumulatedParts = [];
5094
5402
  try {
5095
5403
  while (true) {
5096
- const { done, value } = await reader.read();
5404
+ const { done, value } = await timing.read(reader);
5097
5405
  if (done) break;
5098
5406
  buffer += decoder.decode(value, { stream: true });
5099
5407
  const lines = buffer.split("\n");
@@ -7518,6 +7826,9 @@ var AXL_EVENT_TYPES = [
7518
7826
  // structured-output cliffs (oversized appended schema, dropped refinements,
7519
7827
  // streaming disabled, zero-guidance `schemaPrompt:'none'`).
7520
7828
  "schema_diagnostic",
7829
+ // Provider capability diagnostics — the resolved provider could not honor a
7830
+ // portable request knob verbatim (first kind: a clamped `effort`).
7831
+ "provider_diagnostic",
7521
7832
  // Observability
7522
7833
  "log",
7523
7834
  "memory_remember",
@@ -7567,6 +7878,7 @@ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
7567
7878
  "pipeline",
7568
7879
  "verify",
7569
7880
  "schema_diagnostic",
7881
+ "provider_diagnostic",
7570
7882
  "workflow_start",
7571
7883
  "workflow_end",
7572
7884
  "transcription_start",
@@ -8495,6 +8807,10 @@ var LEGACY_REDACTION_RULES = {
8495
8807
  // which is user content or PII (field paths are schema-shape, preserved like
8496
8808
  // `string_delta.path`). Passthrough, as an explicit reviewed decision.
8497
8809
  schema_diagnostic: passthrough,
8810
+ // `provider_diagnostic` carries only provider capability metadata — a provider
8811
+ // name, a model id, and two effort level strings. No application data or user
8812
+ // content ever reaches it. Passthrough, as an explicit reviewed decision.
8813
+ provider_diagnostic: passthrough,
8498
8814
  log: (e) => {
8499
8815
  if (!e.data || typeof e.data !== "object" || Array.isArray(e.data)) return e;
8500
8816
  return { ...e, data: walkObjectOneLevel(e.data, LOG_PRESERVE_KEYS) };
@@ -8625,6 +8941,7 @@ var COMMON_V2_REDACTION_RULES = {
8625
8941
  string_delta: reuseLegacyRule("string_delta"),
8626
8942
  verify: reuseLegacyRule("verify"),
8627
8943
  schema_diagnostic: reuseLegacyRule("schema_diagnostic"),
8944
+ provider_diagnostic: reuseLegacyRule("provider_diagnostic"),
8628
8945
  log: reuseLegacyRule("log"),
8629
8946
  memory_remember: reuseLegacyRule("memory_remember"),
8630
8947
  memory_recall: reuseLegacyRule("memory_recall"),
@@ -8720,13 +9037,14 @@ function detectDroppedRefinements(schema) {
8720
9037
  var DOCS_URL = "docs/observability.md#schema-diagnostics";
8721
9038
  var MAX_WARNED_KEYS = 2048;
8722
9039
  var warnedDiagnosticKeys = /* @__PURE__ */ new Set();
8723
- function warnSchemaDiagnosticOnce(key, message, silent) {
9040
+ function warnDiagnosticOnce(key, message, silent, docs = DOCS_URL) {
8724
9041
  if (silent || process.env.AXL_DIAGNOSTICS_SILENT === "true") return;
8725
9042
  if (warnedDiagnosticKeys.has(key)) return;
8726
9043
  if (warnedDiagnosticKeys.size >= MAX_WARNED_KEYS) warnedDiagnosticKeys.clear();
8727
9044
  warnedDiagnosticKeys.add(key);
8728
- console.warn(`[axl] ${message} See ${DOCS_URL}.`);
9045
+ console.warn(`[axl] ${message} See ${docs}.`);
8729
9046
  }
9047
+ var warnSchemaDiagnosticOnce = warnDiagnosticOnce;
8730
9048
 
8731
9049
  // src/config.ts
8732
9050
  function defineConfig(config) {
@@ -9576,7 +9894,29 @@ function appendRetryMessages(messages, content, feedbackMessage, providerMetadat
9576
9894
  content,
9577
9895
  ...providerMetadata ? { providerMetadata } : {}
9578
9896
  });
9579
- messages.push({ role: "system", content: feedbackMessage });
9897
+ messages.push({ role: "user", content: feedbackMessage });
9898
+ }
9899
+ function isRetryAbort(value) {
9900
+ return typeof value === "object" && value !== null && value.retry === false;
9901
+ }
9902
+ function describeValue(value) {
9903
+ if (value === null) return "null";
9904
+ if (Array.isArray(value)) return `an array of length ${value.length}`;
9905
+ if (typeof value === "object") {
9906
+ const keys = Object.keys(value);
9907
+ return keys.length > 0 ? `an object with keys [${keys.join(", ")}]` : "an empty object";
9908
+ }
9909
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
9910
+ return `the ${typeof value} ${JSON.stringify(value)}`;
9911
+ }
9912
+ return `a value of type ${typeof value}`;
9913
+ }
9914
+ var PROMPT_CACHE_COLD_WRITE_WARN_AFTER = 2;
9915
+ function nextUserTurn(history, from) {
9916
+ for (let i = Math.max(0, from); i < history.length; i++) {
9917
+ if (history[i].role === "user") return i;
9918
+ }
9919
+ return void 0;
9580
9920
  }
9581
9921
  function estimateMessagesTokens(messages) {
9582
9922
  let total = 0;
@@ -9717,6 +10057,13 @@ var WorkflowContext = class _WorkflowContext {
9717
10057
  workflowLifecycleState;
9718
10058
  signal;
9719
10059
  summaryCache;
10060
+ /** Consecutive provider calls with promptCache on that wrote to the cache
10061
+ * without reading from it (prefix changes per call), and consecutive calls
10062
+ * with no cache activity at all (prefix below the model's minimum, which
10063
+ * Anthropic silently ignores). Either reports once per context. */
10064
+ promptCacheColdWrites = 0;
10065
+ promptCacheInactive = 0;
10066
+ promptCacheWarned = false;
9720
10067
  workflowName;
9721
10068
  mcpManager;
9722
10069
  spanManager;
@@ -10249,6 +10596,17 @@ var WorkflowContext = class _WorkflowContext {
10249
10596
  const costAfter2 = this.budgetContext?.totalCost ?? 0;
10250
10597
  span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
10251
10598
  span.setAttribute("axl.agent.duration", Date.now() - askStart);
10599
+ if (usageCapture.timing) {
10600
+ const t = usageCapture.timing;
10601
+ span.setAttribute("axl.agent.queued_ms", t.queuedMs);
10602
+ span.setAttribute("axl.agent.retry_ms", t.retryMs);
10603
+ span.setAttribute("axl.agent.attempts", t.attempts);
10604
+ span.setAttribute("axl.agent.ttfb_ms", t.ttfbMs);
10605
+ span.setAttribute("axl.agent.wire_ms", t.wireMs);
10606
+ if (t.firstTokenMs !== void 0) {
10607
+ span.setAttribute("axl.agent.first_token_ms", t.firstTokenMs);
10608
+ }
10609
+ }
10252
10610
  if (usageCapture.value) {
10253
10611
  span.setAttribute(
10254
10612
  "axl.agent.prompt_tokens",
@@ -10456,6 +10814,14 @@ var WorkflowContext = class _WorkflowContext {
10456
10814
  const toolTokens = toolDefs.length > 0 ? estimateTokens(JSON.stringify(toolDefs)) : 0;
10457
10815
  const overhead = systemTokens + toolTokens + reserveTokens;
10458
10816
  const availableForHistory = maxContext - overhead;
10817
+ if (availableForHistory <= 0) {
10818
+ this.emitEvent({
10819
+ type: "log",
10820
+ data: {
10821
+ warning: `Agent '${agent2._name}' maxContext (${maxContext}) is at or below its fixed overhead (${overhead} = system ${systemTokens} + tools ${toolTokens} + reserve ${reserveTokens}); no session history can fit, so every ask summarizes, retaining at most the most recent exchange and sometimes none of it verbatim. The current input is always sent. Raise maxContext or lower contextManagement.reserveTokens.`
10822
+ }
10823
+ });
10824
+ }
10459
10825
  const historyEstimate = estimateMessagesTokens(sessionHistory);
10460
10826
  if (historyEstimate.unmeasured) {
10461
10827
  this.emitEvent({
@@ -10513,6 +10879,7 @@ ${appendedSchemaText}`;
10513
10879
  provider,
10514
10880
  model
10515
10881
  });
10882
+ this.emitProviderDiagnostics(agent2, options, { provider, model, providerOptions });
10516
10883
  if (handoffMessages && handoffMessages.length > 0) {
10517
10884
  const handoffContext = handoffMessages.filter(
10518
10885
  (m) => m.role === "assistant" || m.role === "tool"
@@ -10520,6 +10887,7 @@ ${appendedSchemaText}`;
10520
10887
  if (handoffContext.length > 0) {
10521
10888
  messages.push({
10522
10889
  role: "system",
10890
+ origin: "runtime",
10523
10891
  content: "The following is the conversation history from the previous agent that handed off to you:"
10524
10892
  });
10525
10893
  for (const msg of handoffContext) {
@@ -10569,6 +10937,7 @@ ${appendedSchemaText}`;
10569
10937
  const maxTurns = agent2._config.maxTurns ?? 25;
10570
10938
  const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
10571
10939
  const startTime = Date.now();
10940
+ const timingTotals = { turns: 0, queuedMs: 0, retryMs: 0, wireMs: 0 };
10572
10941
  const currentMessages = [...messages];
10573
10942
  let turns = 0;
10574
10943
  let guardrailOutputRetries = 0;
@@ -10582,8 +10951,22 @@ ${appendedSchemaText}`;
10582
10951
  let lastStartMaxAttempts = 1;
10583
10952
  while (turns < maxTurns) {
10584
10953
  this.currentSignal?.throwIfAborted();
10585
- if (Date.now() - startTime > timeoutMs) {
10586
- throw new TimeoutError("ctx.ask()", timeoutMs);
10954
+ const elapsedMs = Date.now() - startTime;
10955
+ if (elapsedMs > timeoutMs) {
10956
+ throw new TimeoutError(
10957
+ "ctx.ask()",
10958
+ timeoutMs,
10959
+ timingTotals.turns > 0 ? {
10960
+ elapsedMs,
10961
+ queuedMs: timingTotals.queuedMs,
10962
+ retryMs: timingTotals.retryMs,
10963
+ wireMs: timingTotals.wireMs,
10964
+ otherMs: Math.max(
10965
+ 0,
10966
+ elapsedMs - timingTotals.queuedMs - timingTotals.retryMs - timingTotals.wireMs
10967
+ )
10968
+ } : void 0
10969
+ );
10587
10970
  }
10588
10971
  turns++;
10589
10972
  const isFirstTurn = turns === 1;
@@ -10623,6 +11006,7 @@ ${appendedSchemaText}`;
10623
11006
  effort: options?.effort ?? agent2._config.effort,
10624
11007
  thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
10625
11008
  includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
11009
+ promptCache: options?.promptCache ?? agent2._config.promptCache,
10626
11010
  toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
10627
11011
  stop: options?.stop ?? agent2._config.stop,
10628
11012
  providerOptions,
@@ -10700,6 +11084,7 @@ ${appendedSchemaText}`;
10700
11084
  const toolCalls = [];
10701
11085
  const toolCallBuffers = /* @__PURE__ */ new Map();
10702
11086
  let streamProviderMetadata;
11087
+ let streamTiming;
10703
11088
  let thinkingContent = "";
10704
11089
  const streamingObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof import_zod2.z.ZodObject;
10705
11090
  const currentAttempt = schemaRetries + 1;
@@ -10751,6 +11136,7 @@ ${appendedSchemaText}`;
10751
11136
  if (chunk.arguments) buffer.arguments += chunk.arguments;
10752
11137
  } else if (chunk.type === "done") {
10753
11138
  streamProviderMetadata = chunk.providerMetadata;
11139
+ streamTiming = chunk.timing;
10754
11140
  if (chunk.usage) {
10755
11141
  response = {
10756
11142
  content: content2,
@@ -10778,6 +11164,9 @@ ${appendedSchemaText}`;
10778
11164
  if (streamProviderMetadata) {
10779
11165
  response.providerMetadata = streamProviderMetadata;
10780
11166
  }
11167
+ if (streamTiming) {
11168
+ response.timing = streamTiming;
11169
+ }
10781
11170
  if (thinkingContent) {
10782
11171
  response.thinking_content = thinkingContent;
10783
11172
  }
@@ -10786,12 +11175,15 @@ ${appendedSchemaText}`;
10786
11175
  }
10787
11176
  } catch (err) {
10788
11177
  if (richRequest) this.recordRichFailure(err);
11178
+ const failureTiming = err instanceof ProviderError ? err.timing : void 0;
10789
11179
  this.emitEvent({
10790
11180
  type: "agent_call_end",
10791
11181
  agent: agent2._name,
10792
11182
  model: effectiveModelUri,
10793
11183
  promptVersion: agent2._config.version,
10794
11184
  duration: Date.now() - turnStart,
11185
+ // Top-level beside `duration`, exactly as on the success path.
11186
+ ...failureTiming ? { timing: failureTiming } : {},
10795
11187
  data: {
10796
11188
  response: "",
10797
11189
  turn: turns,
@@ -10808,9 +11200,41 @@ ${appendedSchemaText}`;
10808
11200
  if (usageCapture && response.usage) {
10809
11201
  usageCapture.value = response.usage;
10810
11202
  }
11203
+ if (usageCapture && response.timing) {
11204
+ usageCapture.timing = response.timing;
11205
+ }
11206
+ if (chatOptions.promptCache && provider.realizesPromptCache?.(model) === true && response.usage && !this.promptCacheWarned) {
11207
+ const wrote = (response.usage.cache_write_tokens ?? 0) > 0;
11208
+ const read = (response.usage.cached_tokens ?? 0) > 0;
11209
+ if (read) {
11210
+ this.promptCacheColdWrites = 0;
11211
+ this.promptCacheInactive = 0;
11212
+ } else if (wrote) {
11213
+ this.promptCacheColdWrites += 1;
11214
+ this.promptCacheInactive = 0;
11215
+ } else {
11216
+ this.promptCacheInactive += 1;
11217
+ }
11218
+ let warning;
11219
+ if (this.promptCacheColdWrites >= PROMPT_CACHE_COLD_WRITE_WARN_AFTER) {
11220
+ warning = `Agent '${agent2._name}' has promptCache on, but its prefix was written to the cache ${this.promptCacheColdWrites} times in a row with no cache reads. Each write bills 1.25x input. If this agent's system prompt or tool set changes per call, set promptCache: false for it.`;
11221
+ } else if (this.promptCacheInactive >= PROMPT_CACHE_COLD_WRITE_WARN_AFTER) {
11222
+ warning = `Agent '${agent2._name}' has promptCache on, but ${this.promptCacheInactive} calls in a row showed no cache writes or reads. Anthropic silently ignores a breakpoint below the model's minimum cacheable length (4,096 tokens on Haiku 4.5; 1,024 or 512 on others) -- the system prompt plus tools may be too short to cache on this model, or the provider may not support prompt caching.`;
11223
+ }
11224
+ if (warning) {
11225
+ this.promptCacheWarned = true;
11226
+ this.emitEvent({ type: "log", agent: agent2._name, data: { warning } });
11227
+ }
11228
+ }
10811
11229
  if (response.cost) {
10812
11230
  this._accumulateBudgetCost(response.cost);
10813
11231
  }
11232
+ if (response.timing) {
11233
+ timingTotals.turns += 1;
11234
+ timingTotals.queuedMs += response.timing.queuedMs;
11235
+ timingTotals.retryMs += response.timing.retryMs;
11236
+ timingTotals.wireMs += response.timing.wireMs;
11237
+ }
10814
11238
  this.emitEvent({
10815
11239
  type: "agent_call_end",
10816
11240
  agent: agent2._name,
@@ -10825,6 +11249,8 @@ ${appendedSchemaText}`;
10825
11249
  cacheWrite: response.usage.cache_write_tokens
10826
11250
  } : void 0,
10827
11251
  duration: Date.now() - turnStart,
11252
+ // Additive beside `duration`, which keeps its turn-wall-clock meaning.
11253
+ ...response.timing ? { timing: response.timing } : {},
10828
11254
  data: {
10829
11255
  response: response.content,
10830
11256
  ...response.thinking_content ? { thinking: response.thinking_content } : {},
@@ -10877,7 +11303,11 @@ ${appendedSchemaText}`;
10877
11303
  retries: options.retries,
10878
11304
  metadata: options.metadata,
10879
11305
  validate: options.validate,
10880
- validateRetries: options.validateRetries
11306
+ validateRetries: options.validateRetries,
11307
+ // This seam serves both callers of a handoff: a plain `ctx.ask()` on an
11308
+ // agent with `handoffs`, and the routed `ctx.delegate()` path, whose
11309
+ // terminal ask is the handoff target. The hook travels like `validate`.
11310
+ retryFeedback: options.retryFeedback
10881
11311
  } : void 0;
10882
11312
  const handoffFn = () => askStorage.run(targetFrame, async () => {
10883
11313
  const delegateOriginal = agent2._delegateOriginalInput;
@@ -11026,9 +11456,10 @@ ${appendedSchemaText}`;
11026
11456
  const attempt = guardrailOutputRetries + 1;
11027
11457
  const maxAttempts = maxGuardrailRetries + 1;
11028
11458
  const onBlock = guardrails.onBlock ?? "throw";
11459
+ const blockReason = outputResult.reason ?? "Output blocked by guardrail";
11029
11460
  let feedbackMessage;
11030
11461
  if (outputResult.block && onBlock === "retry" && guardrailOutputRetries < maxGuardrailRetries) {
11031
- feedbackMessage = `Your previous response was blocked by a safety guardrail: ${outputResult.reason ?? "Output blocked"}. Please provide a different response that complies with the guidelines.`;
11462
+ feedbackMessage = `Your previous response was blocked by a safety guardrail: ${blockReason}. Please provide a different response that complies with the guidelines.`;
11032
11463
  }
11033
11464
  this.emitEvent({
11034
11465
  type: "guardrail",
@@ -11051,31 +11482,43 @@ ${appendedSchemaText}`;
11051
11482
  });
11052
11483
  if (outputResult.block) {
11053
11484
  if (feedbackMessage) {
11054
- this.emitEvent({
11055
- type: "pipeline",
11056
- agent: agent2._name,
11057
- status: "failed",
11485
+ const decision = await this.resolveRetryFeedback(options?.retryFeedback, {
11058
11486
  stage: "guardrail",
11059
- attempt: guardrailOutputRetries + 1,
11060
- maxAttempts: maxGuardrailRetries + 1,
11061
- reason: feedbackMessage
11487
+ attempt,
11488
+ maxAttempts,
11489
+ output: content,
11490
+ reason: blockReason,
11491
+ defaultMessage: feedbackMessage
11062
11492
  });
11063
- guardrailOutputRetries++;
11064
- appendRetryMessages(
11065
- currentMessages,
11066
- content,
11067
- feedbackMessage,
11068
- response.providerMetadata
11069
- );
11070
- pendingRetryReason = "guardrail";
11071
- continue;
11493
+ if (decision.retry) {
11494
+ this.emitEvent({
11495
+ type: "pipeline",
11496
+ agent: agent2._name,
11497
+ status: "failed",
11498
+ stage: "guardrail",
11499
+ attempt,
11500
+ maxAttempts,
11501
+ reason: decision.message
11502
+ });
11503
+ guardrailOutputRetries++;
11504
+ appendRetryMessages(
11505
+ currentMessages,
11506
+ content,
11507
+ decision.message,
11508
+ response.providerMetadata
11509
+ );
11510
+ pendingRetryReason = "guardrail";
11511
+ continue;
11512
+ }
11513
+ this.currentSignal?.throwIfAborted();
11514
+ throw new GuardrailError("output", blockReason);
11072
11515
  }
11073
11516
  if (typeof onBlock === "function") {
11074
- return onBlock(outputResult.reason ?? "Output blocked by guardrail", {
11517
+ return onBlock(blockReason, {
11075
11518
  metadata: this.metadata
11076
11519
  });
11077
11520
  }
11078
- throw new GuardrailError("output", outputResult.reason ?? "Output blocked by guardrail");
11521
+ throw new GuardrailError("output", blockReason);
11079
11522
  }
11080
11523
  }
11081
11524
  let validated = void 0;
@@ -11095,7 +11538,7 @@ ${appendedSchemaText}`;
11095
11538
  schemaErr = err;
11096
11539
  schemaReason = err instanceof Error ? err.message : String(err);
11097
11540
  if (schemaRetries < maxSchemaRetries) {
11098
- schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Please fix and try again.`;
11541
+ schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Return a corrected response that matches the required schema.`;
11099
11542
  }
11100
11543
  }
11101
11544
  this.emitEvent({
@@ -11117,24 +11560,36 @@ ${appendedSchemaText}`;
11117
11560
  });
11118
11561
  if (!schemaValid) {
11119
11562
  if (schemaFeedback) {
11120
- this.emitEvent({
11121
- type: "pipeline",
11122
- agent: agent2._name,
11123
- status: "failed",
11563
+ const decision = await this.resolveRetryFeedback(options.retryFeedback, {
11124
11564
  stage: "schema",
11125
- attempt: schemaRetries + 1,
11126
- maxAttempts: (options.retries ?? 3) + 1,
11127
- reason: schemaFeedback
11565
+ attempt: schemaAttempt,
11566
+ maxAttempts: schemaMaxAttempts,
11567
+ output: content,
11568
+ reason: schemaReason ?? "Schema parse failed",
11569
+ ...schemaErr !== void 0 ? { error: schemaErr } : {},
11570
+ defaultMessage: schemaFeedback
11128
11571
  });
11129
- schemaRetries++;
11130
- appendRetryMessages(
11131
- currentMessages,
11132
- content,
11133
- schemaFeedback,
11134
- response.providerMetadata
11135
- );
11136
- pendingRetryReason = "schema";
11137
- continue;
11572
+ if (decision.retry) {
11573
+ this.emitEvent({
11574
+ type: "pipeline",
11575
+ agent: agent2._name,
11576
+ status: "failed",
11577
+ stage: "schema",
11578
+ attempt: schemaAttempt,
11579
+ maxAttempts: schemaMaxAttempts,
11580
+ reason: decision.message
11581
+ });
11582
+ schemaRetries++;
11583
+ appendRetryMessages(
11584
+ currentMessages,
11585
+ content,
11586
+ decision.message,
11587
+ response.providerMetadata
11588
+ );
11589
+ pendingRetryReason = "schema";
11590
+ continue;
11591
+ }
11592
+ this.currentSignal?.throwIfAborted();
11138
11593
  }
11139
11594
  const zodErr = schemaErr instanceof import_zod2.ZodError ? schemaErr : new import_zod2.ZodError([
11140
11595
  {
@@ -11148,12 +11603,14 @@ ${appendedSchemaText}`;
11148
11603
  }
11149
11604
  if (options?.schema && options.validate) {
11150
11605
  let validateResult;
11606
+ let validateErr;
11151
11607
  try {
11152
11608
  validateResult = await options.validate(validated, {
11153
11609
  metadata: this.metadata
11154
11610
  });
11155
11611
  } catch (err) {
11156
11612
  rethrowEventStreamOverflow(err);
11613
+ validateErr = err;
11157
11614
  const reason = err instanceof Error ? err.message : String(err);
11158
11615
  validateResult = { valid: false, reason: `Validator error: ${reason}` };
11159
11616
  }
@@ -11162,7 +11619,7 @@ ${appendedSchemaText}`;
11162
11619
  const validateMaxAttempts = maxValidateRetries + 1;
11163
11620
  let validateFeedback;
11164
11621
  if (!validateResult.valid && validateRetries < maxValidateRetries) {
11165
- validateFeedback = `Your response parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`;
11622
+ validateFeedback = `Your previous response failed validation: ${validateResult.reason ?? "Validation failed"}. Return a corrected response that satisfies the required schema and this rule.`;
11166
11623
  }
11167
11624
  this.emitEvent({
11168
11625
  type: "validate",
@@ -11183,24 +11640,37 @@ ${appendedSchemaText}`;
11183
11640
  });
11184
11641
  if (!validateResult.valid) {
11185
11642
  if (validateFeedback) {
11186
- this.emitEvent({
11187
- type: "pipeline",
11188
- agent: agent2._name,
11189
- status: "failed",
11643
+ const decision = await this.resolveRetryFeedback(options.retryFeedback, {
11190
11644
  stage: "validate",
11191
- attempt: validateRetries + 1,
11192
- maxAttempts: (options.validateRetries ?? 2) + 1,
11193
- reason: validateFeedback
11645
+ attempt: validateAttempt,
11646
+ maxAttempts: validateMaxAttempts,
11647
+ output: content,
11648
+ parsed: validated,
11649
+ reason: validateResult.reason ?? "Validation failed",
11650
+ ...validateErr !== void 0 ? { error: validateErr } : {},
11651
+ defaultMessage: validateFeedback
11194
11652
  });
11195
- validateRetries++;
11196
- appendRetryMessages(
11197
- currentMessages,
11198
- content,
11199
- validateFeedback,
11200
- response.providerMetadata
11201
- );
11202
- pendingRetryReason = "validate";
11203
- continue;
11653
+ if (decision.retry) {
11654
+ this.emitEvent({
11655
+ type: "pipeline",
11656
+ agent: agent2._name,
11657
+ status: "failed",
11658
+ stage: "validate",
11659
+ attempt: validateAttempt,
11660
+ maxAttempts: validateMaxAttempts,
11661
+ reason: decision.message
11662
+ });
11663
+ validateRetries++;
11664
+ appendRetryMessages(
11665
+ currentMessages,
11666
+ content,
11667
+ decision.message,
11668
+ response.providerMetadata
11669
+ );
11670
+ pendingRetryReason = "validate";
11671
+ continue;
11672
+ }
11673
+ this.currentSignal?.throwIfAborted();
11204
11674
  }
11205
11675
  throw new ValidationError(
11206
11676
  validated,
@@ -11222,6 +11692,31 @@ ${appendedSchemaText}`;
11222
11692
  }
11223
11693
  throw new MaxTurnsError("ctx.ask()", maxTurns);
11224
11694
  }
11695
+ /**
11696
+ * Resolve the corrective feedback text for one rejected attempt, giving an optional
11697
+ * caller-supplied `retryFeedback` hook the chance to replace it or to stop retrying.
11698
+ *
11699
+ * Called by all three output gates, only while a retry is still permitted (never on the
11700
+ * exhausting attempt) and only after the gate's own event has been emitted, so a hook that
11701
+ * throws or aborts can never erase the evidence that the gate rejected. A non-empty string
11702
+ * replaces `defaultMessage` verbatim; `undefined` / `''` keeps it; `{ retry: false }` asks
11703
+ * the gate to throw its typed terminal error. Hook exceptions propagate — a bug in caller
11704
+ * code is not a model failure and must not be retried into silence, and neither is a
11705
+ * return value outside the contract: an unrecognized shape throws instead of silently
11706
+ * degrading to the default text, which would hide the defect behind extra token spend.
11707
+ */
11708
+ async resolveRetryFeedback(hook, info) {
11709
+ if (!hook) return { retry: true, message: info.defaultMessage };
11710
+ const result = await hook(info, { metadata: this.metadata });
11711
+ if (result === void 0 || result === "") {
11712
+ return { retry: true, message: info.defaultMessage };
11713
+ }
11714
+ if (typeof result === "string") return { retry: true, message: result };
11715
+ if (isRetryAbort(result)) return { retry: false };
11716
+ throw new TypeError(
11717
+ `retryFeedback must return a string, undefined, or { retry: false }; received ${describeValue(result)}.`
11718
+ );
11719
+ }
11225
11720
  /**
11226
11721
  * Push the final assistant message into session history, preserving providerMetadata
11227
11722
  * (e.g., Gemini thought signatures needed for multi-turn reasoning context) and
@@ -11377,6 +11872,57 @@ ${appendedSchemaText}`;
11377
11872
  }
11378
11873
  }
11379
11874
  }
11875
+ /**
11876
+ * Emit a `provider_diagnostic` when the resolved provider reports that it
11877
+ * could not honor the unified `effort` verbatim (P3/R6/R7).
11878
+ *
11879
+ * The adapter only *resolves* — it returns the requested and effective levels
11880
+ * from the same pure resolver its request builder uses. The runtime decides
11881
+ * how to surface that, because only the runtime can see the event bus and
11882
+ * `AxlConfig.diagnostics.silent` (adapters receive `ProviderConfig` only).
11883
+ *
11884
+ * Called once per ask, so a tool loop with N provider round trips still emits
11885
+ * one event; the `console.warn` dedupes once per process per distinct clamp.
11886
+ * A throwing `effortResolution` is a provider bug and propagates.
11887
+ */
11888
+ emitProviderDiagnostics(agent2, options, ctx) {
11889
+ if (!ctx.provider.effortResolution) return;
11890
+ const resolution = ctx.provider.effortResolution({
11891
+ model: ctx.model,
11892
+ effort: options?.effort ?? agent2._config.effort,
11893
+ thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
11894
+ includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
11895
+ providerOptions: ctx.providerOptions
11896
+ });
11897
+ if (!resolution?.clamped) return;
11898
+ if (typeof resolution.effective !== "string" || resolution.effective.length === 0 || typeof resolution.requested !== "string" || resolution.requested.length === 0) {
11899
+ throw new TypeError(
11900
+ `Provider '${ctx.provider.name ?? "unknown"}' returned a malformed EffortResolution for model '${ctx.model}': 'requested' and 'effective' must be non-empty strings when 'clamped' is true. Received ${JSON.stringify(resolution)}.`
11901
+ );
11902
+ }
11903
+ const cause = resolution.cause ?? `effort '${resolution.requested}' is not supported as asked`;
11904
+ this.emitEvent({
11905
+ type: "provider_diagnostic",
11906
+ agent: agent2._name,
11907
+ data: {
11908
+ kind: "effort_clamped",
11909
+ ...ctx.provider.name ? { provider: ctx.provider.name } : {},
11910
+ model: ctx.model,
11911
+ requested: resolution.requested,
11912
+ effective: resolution.effective,
11913
+ cause
11914
+ }
11915
+ });
11916
+ warnDiagnosticOnce(
11917
+ // One warning per distinct clamp, not per ask: the same clamp across 500
11918
+ // asks is one process-level surprise, while a different requested/effective
11919
+ // pair is a genuinely new one.
11920
+ `effort_clamped\0${ctx.provider.name ?? ""}\0${ctx.model}\0${resolution.requested}\0${resolution.effective}`,
11921
+ `effort '${resolution.requested}' was clamped to '${resolution.effective}' on ${ctx.provider.name ? `${ctx.provider.name} ` : ""}${ctx.model} \u2014 ${cause}. A 'provider_diagnostic' event carries the same detail per ask.`,
11922
+ this.config.diagnostics?.silent,
11923
+ "docs/observability.md#provider-diagnostics"
11924
+ );
11925
+ }
11380
11926
  buildToolDefs(agent2, resolvedHandoffs) {
11381
11927
  const defs = [];
11382
11928
  if (agent2._config.tools) {
@@ -11423,6 +11969,7 @@ ${appendedSchemaText}`;
11423
11969
  if (this.summaryCache) {
11424
11970
  const summaryMsg2 = {
11425
11971
  role: "system",
11972
+ origin: "runtime",
11426
11973
  content: `Summary of earlier conversation:
11427
11974
  ${this.summaryCache}`
11428
11975
  };
@@ -11436,6 +11983,7 @@ ${this.summaryCache}`
11436
11983
  recentTokens2 += msgTokens;
11437
11984
  splitIdx2 = i;
11438
11985
  }
11986
+ splitIdx2 = nextUserTurn(history, splitIdx2) ?? history.length;
11439
11987
  if (splitIdx2 < history.length) {
11440
11988
  return [summaryMsg2, ...history.slice(splitIdx2)];
11441
11989
  }
@@ -11449,6 +11997,19 @@ ${this.summaryCache}`
11449
11997
  recentTokens += msgTokens;
11450
11998
  splitIdx = i;
11451
11999
  }
12000
+ const anchored = nextUserTurn(history, splitIdx);
12001
+ if (anchored !== void 0) {
12002
+ splitIdx = anchored;
12003
+ } else if (splitIdx === history.length && history.length > 1) {
12004
+ for (let i = history.length - 1; i > 0; i--) {
12005
+ if (history[i].role === "user") {
12006
+ splitIdx = i;
12007
+ break;
12008
+ }
12009
+ }
12010
+ } else {
12011
+ splitIdx = history.length;
12012
+ }
11452
12013
  if (splitIdx === 0) return history;
11453
12014
  const oldMessages = history.slice(0, splitIdx);
11454
12015
  const summaryModelUri = this.config.contextManagement?.summaryModel;
@@ -11480,6 +12041,7 @@ ${this.summaryCache}`
11480
12041
  }
11481
12042
  const summaryMsg = {
11482
12043
  role: "system",
12044
+ origin: "runtime",
11483
12045
  content: `Summary of earlier conversation:
11484
12046
  ${summaryResponse.content}`
11485
12047
  };
@@ -12657,7 +13219,8 @@ ${summaryResponse.content}`
12657
13219
  retries: options?.retries,
12658
13220
  metadata: options?.metadata,
12659
13221
  validate: options?.validate,
12660
- validateRetries: options?.validateRetries
13222
+ validateRetries: options?.validateRetries,
13223
+ retryFeedback: options?.retryFeedback
12661
13224
  });
12662
13225
  }
12663
13226
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
@@ -12705,7 +13268,8 @@ ${summaryResponse.content}`
12705
13268
  retries: options?.retries,
12706
13269
  metadata: options?.metadata,
12707
13270
  validate: options?.validate,
12708
- validateRetries: options?.validateRetries
13271
+ validateRetries: options?.validateRetries,
13272
+ retryFeedback: options?.retryFeedback
12709
13273
  });
12710
13274
  }
12711
13275
  /** Transcribe recorded audio through the dedicated transcription registry.
@@ -16253,6 +16817,8 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
16253
16817
  parent: parentScope
16254
16818
  };
16255
16819
  const modelCalls = /* @__PURE__ */ new Map();
16820
+ const modelTiming = /* @__PURE__ */ new Map();
16821
+ const captureTimingSamples = options?.captureTimingSamples === true;
16256
16822
  const workflowCalls = /* @__PURE__ */ new Map();
16257
16823
  const tokens = { input: 0, output: 0, reasoning: 0 };
16258
16824
  let agentCalls = 0;
@@ -16270,6 +16836,30 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
16270
16836
  tokens.output += event.tokens.output ?? 0;
16271
16837
  tokens.reasoning += event.tokens.reasoning ?? 0;
16272
16838
  }
16839
+ const failed2 = event.data?.error != null;
16840
+ if (event.model && event.timing && !failed2) {
16841
+ const t = event.timing;
16842
+ let bucket = modelTiming.get(event.model);
16843
+ if (!bucket) {
16844
+ bucket = {
16845
+ calls: 0,
16846
+ queuedMs: 0,
16847
+ retryMs: 0,
16848
+ wireMs: 0,
16849
+ ...captureTimingSamples ? { samples: [] } : {}
16850
+ };
16851
+ modelTiming.set(event.model, bucket);
16852
+ }
16853
+ bucket.calls++;
16854
+ bucket.samples?.push(t);
16855
+ bucket.queuedMs += t.queuedMs;
16856
+ bucket.retryMs += t.retryMs;
16857
+ bucket.wireMs += t.wireMs;
16858
+ if (t.firstTokenMs != null) {
16859
+ bucket.firstTokenMs = (bucket.firstTokenMs ?? 0) + t.firstTokenMs;
16860
+ bucket.firstTokenCalls = (bucket.firstTokenCalls ?? 0) + 1;
16861
+ }
16862
+ }
16273
16863
  }
16274
16864
  if (event.type === "workflow_start" && event.workflow) {
16275
16865
  workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
@@ -16300,6 +16890,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
16300
16890
  result,
16301
16891
  cost: scope.totalCost,
16302
16892
  unpriced,
16893
+ ...modelTiming.size > 0 ? { modelTiming: Object.fromEntries(modelTiming) } : {},
16303
16894
  ...capturedTraces ? { traces: capturedTraces } : {},
16304
16895
  metadata: {
16305
16896
  models: [...modelCalls.keys()],