@axlsdk/axl 0.22.3 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs 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
  },
@@ -2518,6 +2654,7 @@ var OpenAIResponsesProvider = class {
2518
2654
  async chat(messages, options) {
2519
2655
  const headers = this.buildHeaders(await this.resolveKey());
2520
2656
  const body = this.buildRequestBody(messages, options, false);
2657
+ const recorder = new CallTimingRecorder();
2521
2658
  const res = await fetchWithRetry(
2522
2659
  `${this.baseUrl}/responses`,
2523
2660
  {
@@ -2526,7 +2663,7 @@ var OpenAIResponsesProvider = class {
2526
2663
  body: JSON.stringify(body),
2527
2664
  signal: options.signal
2528
2665
  },
2529
- { governor: this.governor, provider: this.name }
2666
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
2530
2667
  );
2531
2668
  if (!res.ok) {
2532
2669
  const errorBody = await res.text();
@@ -2536,11 +2673,15 @@ var OpenAIResponsesProvider = class {
2536
2673
  status: res.status,
2537
2674
  headers: res.headers,
2538
2675
  message,
2539
- body: errorBody
2676
+ body: errorBody,
2677
+ timing: recorder.chatTiming()
2540
2678
  });
2541
2679
  }
2542
2680
  const json = await res.json();
2543
- 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
+ );
2544
2685
  }
2545
2686
  // ---------------------------------------------------------------------------
2546
2687
  // stream - SSE streaming
@@ -2548,6 +2689,7 @@ var OpenAIResponsesProvider = class {
2548
2689
  async *stream(messages, options) {
2549
2690
  const headers = this.buildHeaders(await this.resolveKey());
2550
2691
  const body = this.buildRequestBody(messages, options, true);
2692
+ const recorder = new CallTimingRecorder();
2551
2693
  const res = await fetchWithRetry(
2552
2694
  `${this.baseUrl}/responses`,
2553
2695
  {
@@ -2556,7 +2698,7 @@ var OpenAIResponsesProvider = class {
2556
2698
  body: JSON.stringify(body),
2557
2699
  signal: options.signal
2558
2700
  },
2559
- { governor: this.governor, provider: this.name }
2701
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
2560
2702
  );
2561
2703
  if (!res.ok) {
2562
2704
  const errorBody = await res.text();
@@ -2566,13 +2708,17 @@ var OpenAIResponsesProvider = class {
2566
2708
  status: res.status,
2567
2709
  headers: res.headers,
2568
2710
  message,
2569
- body: errorBody
2711
+ body: errorBody,
2712
+ timing: recorder.chatTiming()
2570
2713
  });
2571
2714
  }
2572
2715
  if (!res.body) {
2573
2716
  throw new Error("OpenAI Responses stream has no body");
2574
2717
  }
2575
- 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
+ );
2576
2722
  }
2577
2723
  // ---------------------------------------------------------------------------
2578
2724
  // Internal: build request body
@@ -2774,7 +2920,7 @@ var OpenAIResponsesProvider = class {
2774
2920
  // ---------------------------------------------------------------------------
2775
2921
  // Internal: parse SSE stream
2776
2922
  // ---------------------------------------------------------------------------
2777
- async *parseSSEStream(body, model, request) {
2923
+ async *parseSSEStream(body, model, request, timing) {
2778
2924
  const reader = body.getReader();
2779
2925
  const decoder = new TextDecoder();
2780
2926
  let buffer = "";
@@ -2782,7 +2928,7 @@ var OpenAIResponsesProvider = class {
2782
2928
  let eventType = "";
2783
2929
  try {
2784
2930
  while (true) {
2785
- const { done, value } = await reader.read();
2931
+ const { done, value } = await timing.read(reader);
2786
2932
  if (done) break;
2787
2933
  buffer += decoder.decode(value, { stream: true });
2788
2934
  const lines = buffer.split("\n");
@@ -2802,7 +2948,14 @@ var OpenAIResponsesProvider = class {
2802
2948
  } catch {
2803
2949
  continue;
2804
2950
  }
2805
- 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
+ );
2806
2959
  if (chunk) {
2807
2960
  yield chunk;
2808
2961
  if (chunk.type === "done") return;
@@ -2816,7 +2969,7 @@ var OpenAIResponsesProvider = class {
2816
2969
  reader.releaseLock();
2817
2970
  }
2818
2971
  }
2819
- handleStreamEvent(eventType, data, model, callIdMap, request) {
2972
+ handleStreamEvent(eventType, data, model, callIdMap, request, timing) {
2820
2973
  switch (eventType) {
2821
2974
  case "response.output_text.delta":
2822
2975
  return { type: "text_delta", content: data.delta ?? "" };
@@ -2872,7 +3025,8 @@ var OpenAIResponsesProvider = class {
2872
3025
  provider: this.name,
2873
3026
  status: 0,
2874
3027
  retryable: false,
2875
- message: `OpenAI Responses API error: ${errorMsg}`
3028
+ message: `OpenAI Responses API error: ${errorMsg}`,
3029
+ timing: timing.streamTiming()
2876
3030
  });
2877
3031
  }
2878
3032
  default:
@@ -2940,6 +3094,12 @@ function hasAnthropicProviderFileImage(messages) {
2940
3094
  );
2941
3095
  }
2942
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
+ },
2943
3103
  "claude-fable-5": {
2944
3104
  thinking: "adaptive-always-on",
2945
3105
  effortLevels: ["low", "medium", "high", "xhigh", "max"],
@@ -3030,10 +3190,18 @@ var PRE_INFERENCE_GEO_MODELS = /* @__PURE__ */ new Set([
3030
3190
  function resolveClaudeCapability(model) {
3031
3191
  return CLAUDE_CAPABILITIES[model] ?? (LEGACY_CLAUDE_MODELS.has(model) ? LEGACY_CLAUDE_CAPABILITY : void 0);
3032
3192
  }
3193
+ var DEFAULT_CACHE_READ_MULTIPLIER = 0.1;
3033
3194
  var ANTHROPIC_RATES = {
3195
+ "claude-fable-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
3034
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 },
3035
3203
  "claude-opus-5": { input: 5e-6, output: 25e-6 },
3036
- "claude-sonnet-5": { input: 3e-6, output: 15e-6, sonnet5Intro: true },
3204
+ "claude-sonnet-5": { input: 2e-6, output: 1e-5 },
3037
3205
  "claude-opus-4-8": { input: 5e-6, output: 25e-6 },
3038
3206
  "claude-opus-4-7": { input: 5e-6, output: 25e-6 },
3039
3207
  "claude-opus-4-6": { input: 5e-6, output: 25e-6 },
@@ -3072,7 +3240,7 @@ function anthropicStreamErrorStatus(type) {
3072
3240
  return 0;
3073
3241
  }
3074
3242
  }
3075
- function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
3243
+ function estimateAnthropicCost(model, usage) {
3076
3244
  if (!model || !isValidTokenCount(usage.inputTokens) || !isValidTokenCount(usage.outputTokens)) {
3077
3245
  return void 0;
3078
3246
  }
@@ -3091,8 +3259,9 @@ function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
3091
3259
  if (usage.aggregateCacheWriteTokens !== void 0 && (!isValidTokenCount(usage.aggregateCacheWriteTokens) || usage.aggregateCacheWriteTokens !== cacheWriteTotal)) {
3092
3260
  return void 0;
3093
3261
  }
3094
- const { input, output } = rate.sonnet5Intro && now < /* @__PURE__ */ new Date("2026-09-01T00:00:00Z") ? { input: 2e-6, output: 1e-5 } : rate;
3095
- 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;
3096
3265
  }
3097
3266
  var THINKING_BUDGETS = {
3098
3267
  low: 1024,
@@ -3333,6 +3502,14 @@ var AnthropicProvider = class {
3333
3502
  const resolved = resolveThinkingOptions(options);
3334
3503
  if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
3335
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
+ }
3336
3513
  const config = resolveClaudeThinking(model, resolved);
3337
3514
  const wireEffort = typeof config.outputConfig?.effort === "string" ? config.outputConfig.effort : void 0;
3338
3515
  const effective = config.clamp?.effective ?? wireEffort;
@@ -3388,6 +3565,10 @@ var AnthropicProvider = class {
3388
3565
  }
3389
3566
  /** Anthropic ignores native `json_schema` structurally — Axl uses a system
3390
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
+ }
3391
3572
  nativeStructuredOutputSupport() {
3392
3573
  return "unsupported";
3393
3574
  }
@@ -3429,6 +3610,7 @@ var AnthropicProvider = class {
3429
3610
  );
3430
3611
  const body = this.buildRequestBody(messages, options, false);
3431
3612
  const pricingContext = pricingContextFromBody(body);
3613
+ const recorder = new CallTimingRecorder();
3432
3614
  const res = await fetchWithRetry(
3433
3615
  `${this.baseUrl}/messages`,
3434
3616
  {
@@ -3437,7 +3619,7 @@ var AnthropicProvider = class {
3437
3619
  body: JSON.stringify(body),
3438
3620
  signal: options.signal
3439
3621
  },
3440
- { governor: this.governor, provider: this.name }
3622
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
3441
3623
  );
3442
3624
  if (!res.ok) {
3443
3625
  const errorBody = await res.text();
@@ -3447,11 +3629,12 @@ var AnthropicProvider = class {
3447
3629
  status: res.status,
3448
3630
  headers: res.headers,
3449
3631
  message,
3450
- body: errorBody
3632
+ body: errorBody,
3633
+ timing: recorder.chatTiming()
3451
3634
  });
3452
3635
  }
3453
3636
  const json = await res.json();
3454
- return this.parseResponse(json, pricingContext);
3637
+ return withChatTiming(recorder, this.parseResponse(json, pricingContext));
3455
3638
  }
3456
3639
  // ---------------------------------------------------------------------------
3457
3640
  // stream - SSE streaming completion
@@ -3463,6 +3646,7 @@ var AnthropicProvider = class {
3463
3646
  );
3464
3647
  const body = this.buildRequestBody(messages, options, true);
3465
3648
  const pricingContext = pricingContextFromBody(body);
3649
+ const recorder = new CallTimingRecorder();
3466
3650
  const res = await fetchWithRetry(
3467
3651
  `${this.baseUrl}/messages`,
3468
3652
  {
@@ -3471,7 +3655,7 @@ var AnthropicProvider = class {
3471
3655
  body: JSON.stringify(body),
3472
3656
  signal: options.signal
3473
3657
  },
3474
- { governor: this.governor, provider: this.name }
3658
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
3475
3659
  );
3476
3660
  if (!res.ok) {
3477
3661
  const errorBody = await res.text();
@@ -3481,13 +3665,17 @@ var AnthropicProvider = class {
3481
3665
  status: res.status,
3482
3666
  headers: res.headers,
3483
3667
  message,
3484
- body: errorBody
3668
+ body: errorBody,
3669
+ timing: recorder.chatTiming()
3485
3670
  });
3486
3671
  }
3487
3672
  if (!res.body) {
3488
3673
  throw new Error("Anthropic stream response has no body");
3489
3674
  }
3490
- yield* this.parseSSEStream(res.body, pricingContext, res.headers);
3675
+ yield* withCallTiming(
3676
+ recorder,
3677
+ this.parseSSEStream(res.body, pricingContext, res.headers, recorder)
3678
+ );
3491
3679
  }
3492
3680
  // ---------------------------------------------------------------------------
3493
3681
  // Internal: request building
@@ -3522,14 +3710,31 @@ var AnthropicProvider = class {
3522
3710
  max_tokens: options.maxTokens ?? 4096,
3523
3711
  stream
3524
3712
  };
3525
- 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) {
3526
3724
  body.system = systemText;
3527
3725
  }
3528
3726
  if (options.stop) {
3529
3727
  body.stop_sequences = options.stop;
3530
3728
  }
3531
3729
  if (options.tools && options.tools.length > 0) {
3532
- 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;
3533
3738
  }
3534
3739
  if (options.toolChoice !== void 0) {
3535
3740
  body.tool_choice = this.mapToolChoice(options.toolChoice);
@@ -3548,9 +3753,13 @@ var AnthropicProvider = class {
3548
3753
  }
3549
3754
  if (options.responseFormat && options.responseFormat.type !== "text") {
3550
3755
  const jsonInstruction = "You must respond with valid JSON only. No markdown fences, no extra text.";
3551
- 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}
3552
3760
 
3553
3761
  ${jsonInstruction}` : jsonInstruction;
3762
+ }
3554
3763
  }
3555
3764
  if (options.providerOptions) {
3556
3765
  Object.assign(body, options.providerOptions);
@@ -3730,7 +3939,7 @@ ${jsonInstruction}` : jsonInstruction;
3730
3939
  // ---------------------------------------------------------------------------
3731
3940
  // Internal: SSE stream parsing
3732
3941
  // ---------------------------------------------------------------------------
3733
- async *parseSSEStream(body, pricingContext, responseHeaders) {
3942
+ async *parseSSEStream(body, pricingContext, responseHeaders, timing) {
3734
3943
  const reader = body.getReader();
3735
3944
  const decoder = new TextDecoder();
3736
3945
  let buffer = "";
@@ -3759,7 +3968,7 @@ ${jsonInstruction}` : jsonInstruction;
3759
3968
  });
3760
3969
  try {
3761
3970
  while (true) {
3762
- const { done, value } = await reader.read();
3971
+ const { done, value } = await timing.read(reader);
3763
3972
  if (done) break;
3764
3973
  buffer += decoder.decode(value, { stream: true });
3765
3974
  const lines = buffer.split("\n");
@@ -3782,7 +3991,8 @@ ${jsonInstruction}` : jsonInstruction;
3782
3991
  provider: this.name,
3783
3992
  status,
3784
3993
  headers: responseHeaders,
3785
- message: event.error?.message ?? "Anthropic stream failed"
3994
+ message: event.error?.message ?? "Anthropic stream failed",
3995
+ timing: timing.streamTiming()
3786
3996
  });
3787
3997
  }
3788
3998
  case "content_block_start": {
@@ -4018,7 +4228,6 @@ var GEMINI_PRICING = {
4018
4228
  },
4019
4229
  "gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7 },
4020
4230
  "gemini-2.5-flash-lite": { input: 1e-7, cached: 1e-8, output: 4e-7 },
4021
- "gemini-3-flash-preview": { input: 5e-7, cached: 5e-8, output: 3e-6 },
4022
4231
  "gemini-3.1-pro-preview": {
4023
4232
  input: 2e-6,
4024
4233
  cached: 2e-7,
@@ -4034,7 +4243,9 @@ var GEMINI_PRICING = {
4034
4243
  "gemini-3.1-flash-lite": { input: 25e-8, cached: 25e-9, output: 15e-7 },
4035
4244
  "gemini-3.5-flash": { input: 15e-7, cached: 15e-8, output: 9e-6 },
4036
4245
  "gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
4037
- "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 }
4038
4249
  };
4039
4250
  function isValidTokenCount2(value) {
4040
4251
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
@@ -4379,6 +4590,7 @@ var GeminiProvider = class {
4379
4590
  const pricingContext = this.pricingContext(this.requestModel(options), body);
4380
4591
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
4381
4592
  const headers = this.buildHeaders(await this.resolveKey());
4593
+ const recorder = new CallTimingRecorder();
4382
4594
  const res = await fetchWithRetry(
4383
4595
  `${this.baseUrl}/models/${pricingContext.model}:generateContent`,
4384
4596
  {
@@ -4387,7 +4599,7 @@ var GeminiProvider = class {
4387
4599
  body: JSON.stringify(body),
4388
4600
  signal: options.signal
4389
4601
  },
4390
- { governor: this.governor, provider: this.name }
4602
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4391
4603
  );
4392
4604
  if (!res.ok) {
4393
4605
  const errorBody = await res.text();
@@ -4397,11 +4609,12 @@ var GeminiProvider = class {
4397
4609
  status: res.status,
4398
4610
  headers: res.headers,
4399
4611
  message,
4400
- body: errorBody
4612
+ body: errorBody,
4613
+ timing: recorder.chatTiming()
4401
4614
  });
4402
4615
  }
4403
4616
  const json = await res.json();
4404
- return this.parseResponse(json, pricingContext);
4617
+ return withChatTiming(recorder, this.parseResponse(json, pricingContext));
4405
4618
  }
4406
4619
  // ---------------------------------------------------------------------------
4407
4620
  // stream - SSE streaming completion
@@ -4415,6 +4628,7 @@ var GeminiProvider = class {
4415
4628
  const pricingContext = this.pricingContext(this.requestModel(options), body);
4416
4629
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
4417
4630
  const headers = this.buildHeaders(await this.resolveKey());
4631
+ const recorder = new CallTimingRecorder();
4418
4632
  const res = await fetchWithRetry(
4419
4633
  `${this.baseUrl}/models/${pricingContext.model}:streamGenerateContent?alt=sse`,
4420
4634
  {
@@ -4423,7 +4637,7 @@ var GeminiProvider = class {
4423
4637
  body: JSON.stringify(body),
4424
4638
  signal: options.signal
4425
4639
  },
4426
- { governor: this.governor, provider: this.name }
4640
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4427
4641
  );
4428
4642
  if (!res.ok) {
4429
4643
  const errorBody = await res.text();
@@ -4433,13 +4647,14 @@ var GeminiProvider = class {
4433
4647
  status: res.status,
4434
4648
  headers: res.headers,
4435
4649
  message,
4436
- body: errorBody
4650
+ body: errorBody,
4651
+ timing: recorder.chatTiming()
4437
4652
  });
4438
4653
  }
4439
4654
  if (!res.body) {
4440
4655
  throw new Error("Gemini stream response has no body");
4441
4656
  }
4442
- yield* this.parseSSEStream(res.body, pricingContext);
4657
+ yield* withCallTiming(recorder, this.parseSSEStream(res.body, pricingContext, recorder));
4443
4658
  }
4444
4659
  // ---------------------------------------------------------------------------
4445
4660
  // Rich image requests use Gemini Interactions. It is deliberately stateless:
@@ -4450,10 +4665,11 @@ var GeminiProvider = class {
4450
4665
  this.assertSafeInteractionOptions(options);
4451
4666
  const body = this.buildInteractionRequestBody(messages, options, false);
4452
4667
  const headers = this.buildHeaders(await this.resolveKey());
4668
+ const recorder = new CallTimingRecorder();
4453
4669
  const res = await fetchWithRetry(
4454
4670
  `${this.baseUrl}/interactions`,
4455
4671
  { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
4456
- { governor: this.governor, provider: this.name }
4672
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4457
4673
  );
4458
4674
  if (!res.ok) {
4459
4675
  const errorBody = await res.text();
@@ -4462,19 +4678,24 @@ var GeminiProvider = class {
4462
4678
  status: res.status,
4463
4679
  headers: res.headers,
4464
4680
  message: this.extractErrorMessage(errorBody, res.status),
4465
- body: errorBody
4681
+ body: errorBody,
4682
+ timing: recorder.chatTiming()
4466
4683
  });
4467
4684
  }
4468
- return this.parseInteractionResponse(await res.json());
4685
+ return withChatTiming(
4686
+ recorder,
4687
+ this.parseInteractionResponse(await res.json())
4688
+ );
4469
4689
  }
4470
4690
  async *streamInteraction(messages, options) {
4471
4691
  this.assertSafeInteractionOptions(options);
4472
4692
  const body = this.buildInteractionRequestBody(messages, options, true);
4473
4693
  const headers = this.buildHeaders(await this.resolveKey());
4694
+ const recorder = new CallTimingRecorder();
4474
4695
  const res = await fetchWithRetry(
4475
4696
  `${this.baseUrl}/interactions?alt=sse`,
4476
4697
  { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
4477
- { governor: this.governor, provider: this.name }
4698
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4478
4699
  );
4479
4700
  if (!res.ok) {
4480
4701
  const errorBody = await res.text();
@@ -4483,11 +4704,15 @@ var GeminiProvider = class {
4483
4704
  status: res.status,
4484
4705
  headers: res.headers,
4485
4706
  message: this.extractErrorMessage(errorBody, res.status),
4486
- body: errorBody
4707
+ body: errorBody,
4708
+ timing: recorder.chatTiming()
4487
4709
  });
4488
4710
  }
4489
4711
  if (!res.body) throw new Error("Gemini Interactions stream has no body");
4490
- yield* this.parseInteractionSSEStream(res.body, res.headers);
4712
+ yield* withCallTiming(
4713
+ recorder,
4714
+ this.parseInteractionSSEStream(res.body, res.headers, recorder)
4715
+ );
4491
4716
  }
4492
4717
  buildInteractionRequestBody(messages, options, stream) {
4493
4718
  const model = this.requestModel(options);
@@ -4643,7 +4868,7 @@ var GeminiProvider = class {
4643
4868
  providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
4644
4869
  };
4645
4870
  }
4646
- async *parseInteractionSSEStream(body, responseHeaders) {
4871
+ async *parseInteractionSSEStream(body, responseHeaders, timing) {
4647
4872
  const reader = body.getReader();
4648
4873
  const decoder = new TextDecoder();
4649
4874
  let buffer = "";
@@ -4653,7 +4878,7 @@ var GeminiProvider = class {
4653
4878
  let completed = false;
4654
4879
  try {
4655
4880
  while (true) {
4656
- const { done, value } = await reader.read();
4881
+ const { done, value } = await timing.read(reader);
4657
4882
  if (done) break;
4658
4883
  buffer += decoder.decode(value, { stream: true });
4659
4884
  const lines = buffer.split("\n");
@@ -4676,14 +4901,16 @@ var GeminiProvider = class {
4676
4901
  provider: this.name,
4677
4902
  status,
4678
4903
  headers: responseHeaders,
4679
- message
4904
+ message,
4905
+ timing: timing.streamTiming()
4680
4906
  });
4681
4907
  }
4682
4908
  throw new ProviderError({
4683
4909
  provider: this.name,
4684
4910
  status: 0,
4685
4911
  retryable: false,
4686
- message
4912
+ message,
4913
+ timing: timing.streamTiming()
4687
4914
  });
4688
4915
  }
4689
4916
  if (event.event_type === "step.start" && event.step) {
@@ -5161,7 +5388,7 @@ var GeminiProvider = class {
5161
5388
  // ---------------------------------------------------------------------------
5162
5389
  // Internal: SSE stream parsing
5163
5390
  // ---------------------------------------------------------------------------
5164
- async *parseSSEStream(body, pricingContext) {
5391
+ async *parseSSEStream(body, pricingContext, timing) {
5165
5392
  const reader = body.getReader();
5166
5393
  const decoder = new TextDecoder();
5167
5394
  let buffer = "";
@@ -5174,7 +5401,7 @@ var GeminiProvider = class {
5174
5401
  const accumulatedParts = [];
5175
5402
  try {
5176
5403
  while (true) {
5177
- const { done, value } = await reader.read();
5404
+ const { done, value } = await timing.read(reader);
5178
5405
  if (done) break;
5179
5406
  buffer += decoder.decode(value, { stream: true });
5180
5407
  const lines = buffer.split("\n");
@@ -9684,6 +9911,13 @@ function describeValue(value) {
9684
9911
  }
9685
9912
  return `a value of type ${typeof value}`;
9686
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;
9920
+ }
9687
9921
  function estimateMessagesTokens(messages) {
9688
9922
  let total = 0;
9689
9923
  let unmeasured = false;
@@ -9823,6 +10057,13 @@ var WorkflowContext = class _WorkflowContext {
9823
10057
  workflowLifecycleState;
9824
10058
  signal;
9825
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;
9826
10067
  workflowName;
9827
10068
  mcpManager;
9828
10069
  spanManager;
@@ -10355,6 +10596,17 @@ var WorkflowContext = class _WorkflowContext {
10355
10596
  const costAfter2 = this.budgetContext?.totalCost ?? 0;
10356
10597
  span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
10357
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
+ }
10358
10610
  if (usageCapture.value) {
10359
10611
  span.setAttribute(
10360
10612
  "axl.agent.prompt_tokens",
@@ -10562,6 +10814,14 @@ var WorkflowContext = class _WorkflowContext {
10562
10814
  const toolTokens = toolDefs.length > 0 ? estimateTokens(JSON.stringify(toolDefs)) : 0;
10563
10815
  const overhead = systemTokens + toolTokens + reserveTokens;
10564
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
+ }
10565
10825
  const historyEstimate = estimateMessagesTokens(sessionHistory);
10566
10826
  if (historyEstimate.unmeasured) {
10567
10827
  this.emitEvent({
@@ -10627,6 +10887,7 @@ ${appendedSchemaText}`;
10627
10887
  if (handoffContext.length > 0) {
10628
10888
  messages.push({
10629
10889
  role: "system",
10890
+ origin: "runtime",
10630
10891
  content: "The following is the conversation history from the previous agent that handed off to you:"
10631
10892
  });
10632
10893
  for (const msg of handoffContext) {
@@ -10676,6 +10937,7 @@ ${appendedSchemaText}`;
10676
10937
  const maxTurns = agent2._config.maxTurns ?? 25;
10677
10938
  const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
10678
10939
  const startTime = Date.now();
10940
+ const timingTotals = { turns: 0, queuedMs: 0, retryMs: 0, wireMs: 0 };
10679
10941
  const currentMessages = [...messages];
10680
10942
  let turns = 0;
10681
10943
  let guardrailOutputRetries = 0;
@@ -10689,8 +10951,22 @@ ${appendedSchemaText}`;
10689
10951
  let lastStartMaxAttempts = 1;
10690
10952
  while (turns < maxTurns) {
10691
10953
  this.currentSignal?.throwIfAborted();
10692
- if (Date.now() - startTime > timeoutMs) {
10693
- 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
+ );
10694
10970
  }
10695
10971
  turns++;
10696
10972
  const isFirstTurn = turns === 1;
@@ -10730,6 +11006,7 @@ ${appendedSchemaText}`;
10730
11006
  effort: options?.effort ?? agent2._config.effort,
10731
11007
  thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
10732
11008
  includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
11009
+ promptCache: options?.promptCache ?? agent2._config.promptCache,
10733
11010
  toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
10734
11011
  stop: options?.stop ?? agent2._config.stop,
10735
11012
  providerOptions,
@@ -10807,6 +11084,7 @@ ${appendedSchemaText}`;
10807
11084
  const toolCalls = [];
10808
11085
  const toolCallBuffers = /* @__PURE__ */ new Map();
10809
11086
  let streamProviderMetadata;
11087
+ let streamTiming;
10810
11088
  let thinkingContent = "";
10811
11089
  const streamingObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof import_zod2.z.ZodObject;
10812
11090
  const currentAttempt = schemaRetries + 1;
@@ -10858,6 +11136,7 @@ ${appendedSchemaText}`;
10858
11136
  if (chunk.arguments) buffer.arguments += chunk.arguments;
10859
11137
  } else if (chunk.type === "done") {
10860
11138
  streamProviderMetadata = chunk.providerMetadata;
11139
+ streamTiming = chunk.timing;
10861
11140
  if (chunk.usage) {
10862
11141
  response = {
10863
11142
  content: content2,
@@ -10885,6 +11164,9 @@ ${appendedSchemaText}`;
10885
11164
  if (streamProviderMetadata) {
10886
11165
  response.providerMetadata = streamProviderMetadata;
10887
11166
  }
11167
+ if (streamTiming) {
11168
+ response.timing = streamTiming;
11169
+ }
10888
11170
  if (thinkingContent) {
10889
11171
  response.thinking_content = thinkingContent;
10890
11172
  }
@@ -10893,12 +11175,15 @@ ${appendedSchemaText}`;
10893
11175
  }
10894
11176
  } catch (err) {
10895
11177
  if (richRequest) this.recordRichFailure(err);
11178
+ const failureTiming = err instanceof ProviderError ? err.timing : void 0;
10896
11179
  this.emitEvent({
10897
11180
  type: "agent_call_end",
10898
11181
  agent: agent2._name,
10899
11182
  model: effectiveModelUri,
10900
11183
  promptVersion: agent2._config.version,
10901
11184
  duration: Date.now() - turnStart,
11185
+ // Top-level beside `duration`, exactly as on the success path.
11186
+ ...failureTiming ? { timing: failureTiming } : {},
10902
11187
  data: {
10903
11188
  response: "",
10904
11189
  turn: turns,
@@ -10915,9 +11200,41 @@ ${appendedSchemaText}`;
10915
11200
  if (usageCapture && response.usage) {
10916
11201
  usageCapture.value = response.usage;
10917
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
+ }
10918
11229
  if (response.cost) {
10919
11230
  this._accumulateBudgetCost(response.cost);
10920
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
+ }
10921
11238
  this.emitEvent({
10922
11239
  type: "agent_call_end",
10923
11240
  agent: agent2._name,
@@ -10932,6 +11249,8 @@ ${appendedSchemaText}`;
10932
11249
  cacheWrite: response.usage.cache_write_tokens
10933
11250
  } : void 0,
10934
11251
  duration: Date.now() - turnStart,
11252
+ // Additive beside `duration`, which keeps its turn-wall-clock meaning.
11253
+ ...response.timing ? { timing: response.timing } : {},
10935
11254
  data: {
10936
11255
  response: response.content,
10937
11256
  ...response.thinking_content ? { thinking: response.thinking_content } : {},
@@ -11650,6 +11969,7 @@ ${appendedSchemaText}`;
11650
11969
  if (this.summaryCache) {
11651
11970
  const summaryMsg2 = {
11652
11971
  role: "system",
11972
+ origin: "runtime",
11653
11973
  content: `Summary of earlier conversation:
11654
11974
  ${this.summaryCache}`
11655
11975
  };
@@ -11663,6 +11983,7 @@ ${this.summaryCache}`
11663
11983
  recentTokens2 += msgTokens;
11664
11984
  splitIdx2 = i;
11665
11985
  }
11986
+ splitIdx2 = nextUserTurn(history, splitIdx2) ?? history.length;
11666
11987
  if (splitIdx2 < history.length) {
11667
11988
  return [summaryMsg2, ...history.slice(splitIdx2)];
11668
11989
  }
@@ -11676,6 +11997,19 @@ ${this.summaryCache}`
11676
11997
  recentTokens += msgTokens;
11677
11998
  splitIdx = i;
11678
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
+ }
11679
12013
  if (splitIdx === 0) return history;
11680
12014
  const oldMessages = history.slice(0, splitIdx);
11681
12015
  const summaryModelUri = this.config.contextManagement?.summaryModel;
@@ -11707,6 +12041,7 @@ ${this.summaryCache}`
11707
12041
  }
11708
12042
  const summaryMsg = {
11709
12043
  role: "system",
12044
+ origin: "runtime",
11710
12045
  content: `Summary of earlier conversation:
11711
12046
  ${summaryResponse.content}`
11712
12047
  };
@@ -16482,6 +16817,8 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
16482
16817
  parent: parentScope
16483
16818
  };
16484
16819
  const modelCalls = /* @__PURE__ */ new Map();
16820
+ const modelTiming = /* @__PURE__ */ new Map();
16821
+ const captureTimingSamples = options?.captureTimingSamples === true;
16485
16822
  const workflowCalls = /* @__PURE__ */ new Map();
16486
16823
  const tokens = { input: 0, output: 0, reasoning: 0 };
16487
16824
  let agentCalls = 0;
@@ -16499,6 +16836,30 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
16499
16836
  tokens.output += event.tokens.output ?? 0;
16500
16837
  tokens.reasoning += event.tokens.reasoning ?? 0;
16501
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
+ }
16502
16863
  }
16503
16864
  if (event.type === "workflow_start" && event.workflow) {
16504
16865
  workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
@@ -16529,6 +16890,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
16529
16890
  result,
16530
16891
  cost: scope.totalCost,
16531
16892
  unpriced,
16893
+ ...modelTiming.size > 0 ? { modelTiming: Object.fromEntries(modelTiming) } : {},
16532
16894
  ...capturedTraces ? { traces: capturedTraces } : {},
16533
16895
  metadata: {
16534
16896
  models: [...modelCalls.keys()],