@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.js CHANGED
@@ -170,9 +170,15 @@ var NoConsensus = class extends AxlError {
170
170
  }
171
171
  };
172
172
  var TimeoutError = class extends AxlError {
173
- constructor(operation, timeoutMs) {
174
- super("TIMEOUT", `${operation} exceeded timeout of ${timeoutMs}ms`);
173
+ /** Latency attribution for the timed-out operation, when measurable. */
174
+ breakdown;
175
+ constructor(operation, timeoutMs, breakdown) {
176
+ super(
177
+ "TIMEOUT",
178
+ `${operation} exceeded timeout of ${timeoutMs}ms` + (breakdown ? ` (elapsed ${breakdown.elapsedMs}ms: queued ${breakdown.queuedMs}ms, retries ${breakdown.retryMs}ms, wire ${breakdown.wireMs}ms, other ${breakdown.otherMs}ms)` : "")
179
+ );
175
180
  this.name = "TimeoutError";
181
+ if (breakdown) this.breakdown = breakdown;
176
182
  }
177
183
  };
178
184
  function formatBudgetCost(cost) {
@@ -719,6 +725,7 @@ var ProviderError = class extends AxlError {
719
725
  this.retryAfterMs = args.retryAfterMs;
720
726
  this.requestId = args.requestId;
721
727
  this.body = args.body;
728
+ if (args.timing) this.timing = args.timing;
722
729
  }
723
730
  };
724
731
  function isRetryableStatus(status) {
@@ -770,7 +777,8 @@ function buildProviderError(args) {
770
777
  message: args.message,
771
778
  retryAfterMs: args.headers ? parseRetryAfter(args.headers) : void 0,
772
779
  requestId: args.headers ? extractRequestId(args.headers) : void 0,
773
- body: args.body
780
+ body: args.body,
781
+ timing: args.timing
774
782
  });
775
783
  }
776
784
 
@@ -807,14 +815,33 @@ async function fetchWithRetry(input, init, opts) {
807
815
  const maxRetries = opts?.maxRetries ?? MAX_RETRIES;
808
816
  const governor = opts?.governor;
809
817
  const provider = opts?.provider ?? "unknown";
818
+ const observer = opts?.timing;
810
819
  let acquired = false;
820
+ let queuedMs = 0;
811
821
  if (governor) {
822
+ const acquireStart = Date.now();
812
823
  await governor.acquire(init?.signal ?? void 0);
813
824
  acquired = true;
814
- }
825
+ queuedMs = Date.now() - acquireStart;
826
+ }
827
+ let firstDispatchedAt = 0;
828
+ let dispatchedAt = 0;
829
+ let headersAt = 0;
830
+ const reportComplete = (attempts) => {
831
+ observer?.onComplete?.({
832
+ queuedMs,
833
+ attempts,
834
+ retryMs: dispatchedAt - firstDispatchedAt,
835
+ dispatchedAt,
836
+ headersAt
837
+ });
838
+ };
815
839
  try {
816
840
  for (let attempt = 0; ; attempt++) {
817
841
  let res;
842
+ dispatchedAt = Date.now();
843
+ if (attempt === 0) firstDispatchedAt = dispatchedAt;
844
+ observer?.onDispatch?.(attempt + 1, dispatchedAt);
818
845
  try {
819
846
  res = await fetch(input, { ...init, redirect: "manual" });
820
847
  } catch (err) {
@@ -829,11 +856,14 @@ async function fetchWithRetry(input, init, opts) {
829
856
  await sleep2(jitter(BASE_DELAY_MS * 2 ** attempt), init?.signal ?? void 0);
830
857
  continue;
831
858
  }
859
+ headersAt = Date.now();
832
860
  governor?.observe(res);
833
861
  if (res.ok || !RETRYABLE_STATUS_CODES.has(res.status) || attempt >= maxRetries) {
862
+ reportComplete(attempt + 1);
834
863
  return res;
835
864
  }
836
865
  if (init?.signal?.aborted) {
866
+ reportComplete(attempt + 1);
837
867
  return res;
838
868
  }
839
869
  const retryAfterMs = parseRetryAfter(res.headers);
@@ -845,6 +875,86 @@ async function fetchWithRetry(input, init, opts) {
845
875
  }
846
876
  }
847
877
 
878
+ // src/providers/call-timing.ts
879
+ var CallTimingRecorder = class {
880
+ fetchTiming;
881
+ firstTokenAt;
882
+ /** Cumulative time awaiting `reader.read()`, i.e. transport, not consumer. */
883
+ readWaitMs = 0;
884
+ /** Pass as `timing` in the `fetchWithRetry` options object. */
885
+ observer = {
886
+ onComplete: (timing) => {
887
+ this.fetchTiming = timing;
888
+ }
889
+ };
890
+ /**
891
+ * Await one body read, charging only the wait itself to `wireMs`. Time the
892
+ * consumer spends between reads is excluded because it elapses outside this
893
+ * call.
894
+ */
895
+ async read(reader) {
896
+ const start = Date.now();
897
+ try {
898
+ return await reader.read();
899
+ } finally {
900
+ this.readWaitMs += Date.now() - start;
901
+ }
902
+ }
903
+ /** Stamp the first content delta. Idempotent — only the first call counts. */
904
+ markFirstToken() {
905
+ this.firstTokenAt ??= Date.now();
906
+ }
907
+ /**
908
+ * Timing for a non-streaming call. Call once the response body is parsed.
909
+ *
910
+ * Also the right reading at an adapter's `!res.ok` throw site on BOTH
911
+ * transports: headers have arrived, no body will be streamed, so
912
+ * dispatch → now is the whole of what the provider cost us.
913
+ */
914
+ chatTiming() {
915
+ const t = this.fetchTiming;
916
+ if (!t) return void 0;
917
+ return {
918
+ queuedMs: t.queuedMs,
919
+ attempts: t.attempts,
920
+ retryMs: t.retryMs,
921
+ ttfbMs: Math.max(0, t.headersAt - t.dispatchedAt),
922
+ wireMs: Math.max(0, Date.now() - t.dispatchedAt)
923
+ };
924
+ }
925
+ /** Timing for a streamed call. Call when building the terminal `done` chunk. */
926
+ streamTiming() {
927
+ const t = this.fetchTiming;
928
+ if (!t) return void 0;
929
+ const ttfbMs = Math.max(0, t.headersAt - t.dispatchedAt);
930
+ return {
931
+ queuedMs: t.queuedMs,
932
+ attempts: t.attempts,
933
+ retryMs: t.retryMs,
934
+ ttfbMs,
935
+ ...this.firstTokenAt !== void 0 ? { firstTokenMs: Math.max(0, this.firstTokenAt - t.dispatchedAt) } : {},
936
+ wireMs: ttfbMs + this.readWaitMs
937
+ };
938
+ }
939
+ };
940
+ async function* withCallTiming(recorder, source) {
941
+ for await (const chunk of source) {
942
+ if (chunk.type === "text_delta" || chunk.type === "thinking_delta") {
943
+ recorder.markFirstToken();
944
+ yield chunk;
945
+ } else if (chunk.type === "done") {
946
+ const timing = recorder.streamTiming();
947
+ yield timing ? { ...chunk, timing } : chunk;
948
+ } else {
949
+ yield chunk;
950
+ }
951
+ }
952
+ }
953
+ function withChatTiming(recorder, response) {
954
+ const timing = recorder.chatTiming();
955
+ return timing ? { ...response, timing } : response;
956
+ }
957
+
848
958
  // src/providers/rate-limiter.ts
849
959
  function abortError() {
850
960
  return Object.assign(new Error("The operation was aborted"), { name: "AbortError" });
@@ -1297,6 +1407,7 @@ var OpenAICompatibleProvider = class {
1297
1407
  async chat(messages, options) {
1298
1408
  const headers = this.buildHeaders(await this.resolveKey());
1299
1409
  const body = this.buildRequestBody(messages, options, false);
1410
+ const recorder = new CallTimingRecorder();
1300
1411
  const res = await fetchWithRetry(
1301
1412
  `${this.baseUrl}/chat/completions`,
1302
1413
  {
@@ -1305,7 +1416,7 @@ var OpenAICompatibleProvider = class {
1305
1416
  body: JSON.stringify(body),
1306
1417
  signal: options.signal
1307
1418
  },
1308
- { governor: this.governor, provider: this.name }
1419
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
1309
1420
  );
1310
1421
  if (!res.ok) {
1311
1422
  const errorBody = await res.text();
@@ -1314,15 +1425,20 @@ var OpenAICompatibleProvider = class {
1314
1425
  status: res.status,
1315
1426
  headers: res.headers,
1316
1427
  message: this.extractErrorMessage(errorBody, res.status),
1317
- body: errorBody
1428
+ body: errorBody,
1429
+ timing: recorder.chatTiming()
1318
1430
  });
1319
1431
  }
1320
1432
  const json = await res.json();
1321
- return this.parseResponse(json, this.requestModel(body, options.model), body);
1433
+ return withChatTiming(
1434
+ recorder,
1435
+ this.parseResponse(json, this.requestModel(body, options.model), body)
1436
+ );
1322
1437
  }
1323
1438
  async *stream(messages, options) {
1324
1439
  const headers = this.buildHeaders(await this.resolveKey());
1325
1440
  const body = this.buildRequestBody(messages, options, true);
1441
+ const recorder = new CallTimingRecorder();
1326
1442
  const res = await fetchWithRetry(
1327
1443
  `${this.baseUrl}/chat/completions`,
1328
1444
  {
@@ -1331,7 +1447,7 @@ var OpenAICompatibleProvider = class {
1331
1447
  body: JSON.stringify(body),
1332
1448
  signal: options.signal
1333
1449
  },
1334
- { governor: this.governor, provider: this.name }
1450
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
1335
1451
  );
1336
1452
  if (!res.ok) {
1337
1453
  const errorBody = await res.text();
@@ -1340,13 +1456,17 @@ var OpenAICompatibleProvider = class {
1340
1456
  status: res.status,
1341
1457
  headers: res.headers,
1342
1458
  message: this.extractErrorMessage(errorBody, res.status),
1343
- body: errorBody
1459
+ body: errorBody,
1460
+ timing: recorder.chatTiming()
1344
1461
  });
1345
1462
  }
1346
1463
  if (!res.body) {
1347
1464
  throw new Error(`${this.profile.label ?? this.profile.name} stream response has no body`);
1348
1465
  }
1349
- yield* this.parseSSEStream(res.body, this.requestModel(body, options.model), body);
1466
+ yield* withCallTiming(
1467
+ recorder,
1468
+ this.parseSSEStream(res.body, this.requestModel(body, options.model), body, recorder)
1469
+ );
1350
1470
  }
1351
1471
  // ---------------------------------------------------------------------------
1352
1472
  // Request building
@@ -1573,7 +1693,21 @@ var OpenAICompatibleProvider = class {
1573
1693
  // ---------------------------------------------------------------------------
1574
1694
  // SSE streaming
1575
1695
  // ---------------------------------------------------------------------------
1576
- async *parseSSEStream(body, model, request) {
1696
+ /**
1697
+ * PRIVATE, and deliberately not an extension point. It was `protected`, but
1698
+ * nothing in or out of this repo overrides it, and `protected` could not
1699
+ * carry the guarantee it appeared to: TypeScript checks an override for
1700
+ * assignability, and a function type with fewer parameters is assignable to
1701
+ * one with more — so an override that simply dropped the recorder still
1702
+ * compiled and then reported `wireMs === ttfbMs` for the whole subclass. Its
1703
+ * type is not exported from the barrel either, so a subclass author could not
1704
+ * name what they were required to accept.
1705
+ *
1706
+ * A provider with a different wire format implements `Provider` (see
1707
+ * docs/providers.md); an OpenAI-compatible one adds a `ProviderProfile`.
1708
+ * Neither needs to reach in here.
1709
+ */
1710
+ async *parseSSEStream(body, model, request, timing) {
1577
1711
  const reader = body.getReader();
1578
1712
  const decoder = new TextDecoder();
1579
1713
  let buffer = "";
@@ -1633,7 +1767,7 @@ var OpenAICompatibleProvider = class {
1633
1767
  };
1634
1768
  try {
1635
1769
  for (; ; ) {
1636
- const { done, value } = await reader.read();
1770
+ const { done, value } = await timing.read(reader);
1637
1771
  if (done) break;
1638
1772
  buffer += decoder.decode(value, { stream: true });
1639
1773
  const lines = buffer.split("\n");
@@ -1665,7 +1799,8 @@ var OpenAICompatibleProvider = class {
1665
1799
  status: 0,
1666
1800
  retryable: false,
1667
1801
  message,
1668
- body: JSON.stringify(parsed.error)
1802
+ body: JSON.stringify(parsed.error),
1803
+ timing: timing.streamTiming()
1669
1804
  });
1670
1805
  }
1671
1806
  if (parsed.model !== void 0 || parsed.service_tier !== void 0 || parsed.serviceTier !== void 0) {
@@ -1737,7 +1872,8 @@ var OpenAICompatibleProvider = class {
1737
1872
  provider: this.name,
1738
1873
  status: 0,
1739
1874
  retryable: true,
1740
- message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]`
1875
+ message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]`,
1876
+ timing: timing.streamTiming()
1741
1877
  });
1742
1878
  } finally {
1743
1879
  reader.releaseLock();
@@ -1776,12 +1912,12 @@ var DIRECT_OPENAI_CATALOG = [
1776
1912
  {
1777
1913
  aliases: ["gpt-5.6", "gpt-5.6-sol"],
1778
1914
  snapshotBase: "gpt-5.6-sol",
1779
- short: { input: 5 / M, cachedInput: 0.5 / M, cacheWrite: 6.25 / M, output: 30 / M },
1915
+ short: { input: 4 / M, cachedInput: 0.4 / M, cacheWrite: 5 / M, output: 20 / M },
1780
1916
  long: withLongContext({
1781
- input: 5 / M,
1782
- cachedInput: 0.5 / M,
1783
- cacheWrite: 6.25 / M,
1784
- output: 30 / M
1917
+ input: 4 / M,
1918
+ cachedInput: 0.4 / M,
1919
+ cacheWrite: 5 / M,
1920
+ output: 20 / M
1785
1921
  }),
1786
1922
  contextBoundary: LONG_CONTEXT_BOUNDARY
1787
1923
  },
@@ -2296,6 +2432,7 @@ var OpenAIResponsesProvider = class {
2296
2432
  async chat(messages, options) {
2297
2433
  const headers = this.buildHeaders(await this.resolveKey());
2298
2434
  const body = this.buildRequestBody(messages, options, false);
2435
+ const recorder = new CallTimingRecorder();
2299
2436
  const res = await fetchWithRetry(
2300
2437
  `${this.baseUrl}/responses`,
2301
2438
  {
@@ -2304,7 +2441,7 @@ var OpenAIResponsesProvider = class {
2304
2441
  body: JSON.stringify(body),
2305
2442
  signal: options.signal
2306
2443
  },
2307
- { governor: this.governor, provider: this.name }
2444
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
2308
2445
  );
2309
2446
  if (!res.ok) {
2310
2447
  const errorBody = await res.text();
@@ -2314,11 +2451,15 @@ var OpenAIResponsesProvider = class {
2314
2451
  status: res.status,
2315
2452
  headers: res.headers,
2316
2453
  message,
2317
- body: errorBody
2454
+ body: errorBody,
2455
+ timing: recorder.chatTiming()
2318
2456
  });
2319
2457
  }
2320
2458
  const json = await res.json();
2321
- return this.parseResponse(json, this.requestModel(body, options.model), body);
2459
+ return withChatTiming(
2460
+ recorder,
2461
+ this.parseResponse(json, this.requestModel(body, options.model), body)
2462
+ );
2322
2463
  }
2323
2464
  // ---------------------------------------------------------------------------
2324
2465
  // stream - SSE streaming
@@ -2326,6 +2467,7 @@ var OpenAIResponsesProvider = class {
2326
2467
  async *stream(messages, options) {
2327
2468
  const headers = this.buildHeaders(await this.resolveKey());
2328
2469
  const body = this.buildRequestBody(messages, options, true);
2470
+ const recorder = new CallTimingRecorder();
2329
2471
  const res = await fetchWithRetry(
2330
2472
  `${this.baseUrl}/responses`,
2331
2473
  {
@@ -2334,7 +2476,7 @@ var OpenAIResponsesProvider = class {
2334
2476
  body: JSON.stringify(body),
2335
2477
  signal: options.signal
2336
2478
  },
2337
- { governor: this.governor, provider: this.name }
2479
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
2338
2480
  );
2339
2481
  if (!res.ok) {
2340
2482
  const errorBody = await res.text();
@@ -2344,13 +2486,17 @@ var OpenAIResponsesProvider = class {
2344
2486
  status: res.status,
2345
2487
  headers: res.headers,
2346
2488
  message,
2347
- body: errorBody
2489
+ body: errorBody,
2490
+ timing: recorder.chatTiming()
2348
2491
  });
2349
2492
  }
2350
2493
  if (!res.body) {
2351
2494
  throw new Error("OpenAI Responses stream has no body");
2352
2495
  }
2353
- yield* this.parseSSEStream(res.body, this.requestModel(body, options.model), body);
2496
+ yield* withCallTiming(
2497
+ recorder,
2498
+ this.parseSSEStream(res.body, this.requestModel(body, options.model), body, recorder)
2499
+ );
2354
2500
  }
2355
2501
  // ---------------------------------------------------------------------------
2356
2502
  // Internal: build request body
@@ -2552,7 +2698,7 @@ var OpenAIResponsesProvider = class {
2552
2698
  // ---------------------------------------------------------------------------
2553
2699
  // Internal: parse SSE stream
2554
2700
  // ---------------------------------------------------------------------------
2555
- async *parseSSEStream(body, model, request) {
2701
+ async *parseSSEStream(body, model, request, timing) {
2556
2702
  const reader = body.getReader();
2557
2703
  const decoder = new TextDecoder();
2558
2704
  let buffer = "";
@@ -2560,7 +2706,7 @@ var OpenAIResponsesProvider = class {
2560
2706
  let eventType = "";
2561
2707
  try {
2562
2708
  while (true) {
2563
- const { done, value } = await reader.read();
2709
+ const { done, value } = await timing.read(reader);
2564
2710
  if (done) break;
2565
2711
  buffer += decoder.decode(value, { stream: true });
2566
2712
  const lines = buffer.split("\n");
@@ -2580,7 +2726,14 @@ var OpenAIResponsesProvider = class {
2580
2726
  } catch {
2581
2727
  continue;
2582
2728
  }
2583
- const chunk = this.handleStreamEvent(eventType, data, model, callIdMap, request);
2729
+ const chunk = this.handleStreamEvent(
2730
+ eventType,
2731
+ data,
2732
+ model,
2733
+ callIdMap,
2734
+ request,
2735
+ timing
2736
+ );
2584
2737
  if (chunk) {
2585
2738
  yield chunk;
2586
2739
  if (chunk.type === "done") return;
@@ -2594,7 +2747,7 @@ var OpenAIResponsesProvider = class {
2594
2747
  reader.releaseLock();
2595
2748
  }
2596
2749
  }
2597
- handleStreamEvent(eventType, data, model, callIdMap, request) {
2750
+ handleStreamEvent(eventType, data, model, callIdMap, request, timing) {
2598
2751
  switch (eventType) {
2599
2752
  case "response.output_text.delta":
2600
2753
  return { type: "text_delta", content: data.delta ?? "" };
@@ -2650,7 +2803,8 @@ var OpenAIResponsesProvider = class {
2650
2803
  provider: this.name,
2651
2804
  status: 0,
2652
2805
  retryable: false,
2653
- message: `OpenAI Responses API error: ${errorMsg}`
2806
+ message: `OpenAI Responses API error: ${errorMsg}`,
2807
+ timing: timing.streamTiming()
2654
2808
  });
2655
2809
  }
2656
2810
  default:
@@ -2718,6 +2872,12 @@ function hasAnthropicProviderFileImage(messages) {
2718
2872
  );
2719
2873
  }
2720
2874
  var CLAUDE_CAPABILITIES = {
2875
+ "claude-fable-5-1": {
2876
+ thinking: "adaptive-always-on",
2877
+ effortLevels: ["low", "medium", "high", "xhigh", "max"],
2878
+ manualBudget: false,
2879
+ stripTemperature: true
2880
+ },
2721
2881
  "claude-fable-5": {
2722
2882
  thinking: "adaptive-always-on",
2723
2883
  effortLevels: ["low", "medium", "high", "xhigh", "max"],
@@ -2808,10 +2968,18 @@ var PRE_INFERENCE_GEO_MODELS = /* @__PURE__ */ new Set([
2808
2968
  function resolveClaudeCapability(model) {
2809
2969
  return CLAUDE_CAPABILITIES[model] ?? (LEGACY_CLAUDE_MODELS.has(model) ? LEGACY_CLAUDE_CAPABILITY : void 0);
2810
2970
  }
2971
+ var DEFAULT_CACHE_READ_MULTIPLIER = 0.1;
2811
2972
  var ANTHROPIC_RATES = {
2973
+ "claude-fable-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
2812
2974
  "claude-fable-5": { input: 1e-5, output: 5e-5 },
2975
+ // Limited availability (Glasswing). Priced from the public pricing table; no
2976
+ // CLAUDE_CAPABILITIES entry, because their thinking semantics are not
2977
+ // documented per-model and an unverified capability would send the wrong
2978
+ // request shape. Unknown IDs pass through unmodified, which is the safe side.
2979
+ "claude-mythos-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
2980
+ "claude-mythos-5": { input: 1e-5, output: 5e-5 },
2813
2981
  "claude-opus-5": { input: 5e-6, output: 25e-6 },
2814
- "claude-sonnet-5": { input: 3e-6, output: 15e-6, sonnet5Intro: true },
2982
+ "claude-sonnet-5": { input: 2e-6, output: 1e-5 },
2815
2983
  "claude-opus-4-8": { input: 5e-6, output: 25e-6 },
2816
2984
  "claude-opus-4-7": { input: 5e-6, output: 25e-6 },
2817
2985
  "claude-opus-4-6": { input: 5e-6, output: 25e-6 },
@@ -2850,7 +3018,7 @@ function anthropicStreamErrorStatus(type) {
2850
3018
  return 0;
2851
3019
  }
2852
3020
  }
2853
- function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
3021
+ function estimateAnthropicCost(model, usage) {
2854
3022
  if (!model || !isValidTokenCount(usage.inputTokens) || !isValidTokenCount(usage.outputTokens)) {
2855
3023
  return void 0;
2856
3024
  }
@@ -2869,8 +3037,9 @@ function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
2869
3037
  if (usage.aggregateCacheWriteTokens !== void 0 && (!isValidTokenCount(usage.aggregateCacheWriteTokens) || usage.aggregateCacheWriteTokens !== cacheWriteTotal)) {
2870
3038
  return void 0;
2871
3039
  }
2872
- const { input, output } = rate.sonnet5Intro && now < /* @__PURE__ */ new Date("2026-09-01T00:00:00Z") ? { input: 2e-6, output: 1e-5 } : rate;
2873
- return usage.inputTokens * input + cacheRead * input * 0.1 + cacheWrite5m * input * 1.25 + cacheWrite1h * input * 2 + usage.outputTokens * output;
3040
+ const { input, output } = rate;
3041
+ const cacheReadRate = input * (rate.cacheReadMultiplier ?? DEFAULT_CACHE_READ_MULTIPLIER);
3042
+ return usage.inputTokens * input + cacheRead * cacheReadRate + cacheWrite5m * input * 1.25 + cacheWrite1h * input * 2 + usage.outputTokens * output;
2874
3043
  }
2875
3044
  var THINKING_BUDGETS = {
2876
3045
  low: 1024,
@@ -3111,6 +3280,14 @@ var AnthropicProvider = class {
3111
3280
  const resolved = resolveThinkingOptions(options);
3112
3281
  if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
3113
3282
  const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
3283
+ if (ANTHROPIC_RATES[model] && !resolveClaudeCapability(model)) {
3284
+ return {
3285
+ requested: resolved.effort,
3286
+ effective: "unset",
3287
+ clamped: true,
3288
+ cause: `Axl has no capability entry for Anthropic ${model}, so no reasoning control was sent and effort '${resolved.effort}' had no effect`
3289
+ };
3290
+ }
3114
3291
  const config = resolveClaudeThinking(model, resolved);
3115
3292
  const wireEffort = typeof config.outputConfig?.effort === "string" ? config.outputConfig.effort : void 0;
3116
3293
  const effective = config.clamp?.effective ?? wireEffort;
@@ -3166,6 +3343,10 @@ var AnthropicProvider = class {
3166
3343
  }
3167
3344
  /** Anthropic ignores native `json_schema` structurally — Axl uses a system
3168
3345
  * prompt JSON instruction + client-side Zod validation (see buildRequest). */
3346
+ /** The only built-in adapter that honors `promptCache` (see buildBody). */
3347
+ realizesPromptCache() {
3348
+ return true;
3349
+ }
3169
3350
  nativeStructuredOutputSupport() {
3170
3351
  return "unsupported";
3171
3352
  }
@@ -3207,6 +3388,7 @@ var AnthropicProvider = class {
3207
3388
  );
3208
3389
  const body = this.buildRequestBody(messages, options, false);
3209
3390
  const pricingContext = pricingContextFromBody(body);
3391
+ const recorder = new CallTimingRecorder();
3210
3392
  const res = await fetchWithRetry(
3211
3393
  `${this.baseUrl}/messages`,
3212
3394
  {
@@ -3215,7 +3397,7 @@ var AnthropicProvider = class {
3215
3397
  body: JSON.stringify(body),
3216
3398
  signal: options.signal
3217
3399
  },
3218
- { governor: this.governor, provider: this.name }
3400
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
3219
3401
  );
3220
3402
  if (!res.ok) {
3221
3403
  const errorBody = await res.text();
@@ -3225,11 +3407,12 @@ var AnthropicProvider = class {
3225
3407
  status: res.status,
3226
3408
  headers: res.headers,
3227
3409
  message,
3228
- body: errorBody
3410
+ body: errorBody,
3411
+ timing: recorder.chatTiming()
3229
3412
  });
3230
3413
  }
3231
3414
  const json = await res.json();
3232
- return this.parseResponse(json, pricingContext);
3415
+ return withChatTiming(recorder, this.parseResponse(json, pricingContext));
3233
3416
  }
3234
3417
  // ---------------------------------------------------------------------------
3235
3418
  // stream - SSE streaming completion
@@ -3241,6 +3424,7 @@ var AnthropicProvider = class {
3241
3424
  );
3242
3425
  const body = this.buildRequestBody(messages, options, true);
3243
3426
  const pricingContext = pricingContextFromBody(body);
3427
+ const recorder = new CallTimingRecorder();
3244
3428
  const res = await fetchWithRetry(
3245
3429
  `${this.baseUrl}/messages`,
3246
3430
  {
@@ -3249,7 +3433,7 @@ var AnthropicProvider = class {
3249
3433
  body: JSON.stringify(body),
3250
3434
  signal: options.signal
3251
3435
  },
3252
- { governor: this.governor, provider: this.name }
3436
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
3253
3437
  );
3254
3438
  if (!res.ok) {
3255
3439
  const errorBody = await res.text();
@@ -3259,13 +3443,17 @@ var AnthropicProvider = class {
3259
3443
  status: res.status,
3260
3444
  headers: res.headers,
3261
3445
  message,
3262
- body: errorBody
3446
+ body: errorBody,
3447
+ timing: recorder.chatTiming()
3263
3448
  });
3264
3449
  }
3265
3450
  if (!res.body) {
3266
3451
  throw new Error("Anthropic stream response has no body");
3267
3452
  }
3268
- yield* this.parseSSEStream(res.body, pricingContext, res.headers);
3453
+ yield* withCallTiming(
3454
+ recorder,
3455
+ this.parseSSEStream(res.body, pricingContext, res.headers, recorder)
3456
+ );
3269
3457
  }
3270
3458
  // ---------------------------------------------------------------------------
3271
3459
  // Internal: request building
@@ -3300,14 +3488,31 @@ var AnthropicProvider = class {
3300
3488
  max_tokens: options.maxTokens ?? 4096,
3301
3489
  stream
3302
3490
  };
3303
- if (systemText) {
3491
+ const po = options.providerOptions;
3492
+ const callerOwnsPrefix = po !== void 0 && (po.cache_control !== void 0 || po.system !== void 0 || po.tools !== void 0);
3493
+ const promptCache = options.promptCache === true && !callerOwnsPrefix;
3494
+ const anchorOnSystem = promptCache && systemMessages.length > 0 && systemMessages[0].origin !== "runtime";
3495
+ if (anchorOnSystem) {
3496
+ body.system = systemMessages.map((m, index) => ({
3497
+ type: "text",
3498
+ text: typeof m.content === "string" ? m.content : "",
3499
+ ...index === 0 ? { cache_control: { type: "ephemeral" } } : {}
3500
+ }));
3501
+ } else if (systemText) {
3304
3502
  body.system = systemText;
3305
3503
  }
3306
3504
  if (options.stop) {
3307
3505
  body.stop_sequences = options.stop;
3308
3506
  }
3309
3507
  if (options.tools && options.tools.length > 0) {
3310
- body.tools = options.tools.map((t) => this.mapToolDefinition(t));
3508
+ const tools = options.tools.map((t) => this.mapToolDefinition(t));
3509
+ if (promptCache && !anchorOnSystem) {
3510
+ tools[tools.length - 1] = {
3511
+ ...tools[tools.length - 1],
3512
+ cache_control: { type: "ephemeral" }
3513
+ };
3514
+ }
3515
+ body.tools = tools;
3311
3516
  }
3312
3517
  if (options.toolChoice !== void 0) {
3313
3518
  body.tool_choice = this.mapToolChoice(options.toolChoice);
@@ -3326,9 +3531,13 @@ var AnthropicProvider = class {
3326
3531
  }
3327
3532
  if (options.responseFormat && options.responseFormat.type !== "text") {
3328
3533
  const jsonInstruction = "You must respond with valid JSON only. No markdown fences, no extra text.";
3329
- body.system = body.system ? `${body.system}
3534
+ if (Array.isArray(body.system)) {
3535
+ body.system = [...body.system, { type: "text", text: jsonInstruction }];
3536
+ } else {
3537
+ body.system = body.system ? `${body.system}
3330
3538
 
3331
3539
  ${jsonInstruction}` : jsonInstruction;
3540
+ }
3332
3541
  }
3333
3542
  if (options.providerOptions) {
3334
3543
  Object.assign(body, options.providerOptions);
@@ -3508,7 +3717,7 @@ ${jsonInstruction}` : jsonInstruction;
3508
3717
  // ---------------------------------------------------------------------------
3509
3718
  // Internal: SSE stream parsing
3510
3719
  // ---------------------------------------------------------------------------
3511
- async *parseSSEStream(body, pricingContext, responseHeaders) {
3720
+ async *parseSSEStream(body, pricingContext, responseHeaders, timing) {
3512
3721
  const reader = body.getReader();
3513
3722
  const decoder = new TextDecoder();
3514
3723
  let buffer = "";
@@ -3537,7 +3746,7 @@ ${jsonInstruction}` : jsonInstruction;
3537
3746
  });
3538
3747
  try {
3539
3748
  while (true) {
3540
- const { done, value } = await reader.read();
3749
+ const { done, value } = await timing.read(reader);
3541
3750
  if (done) break;
3542
3751
  buffer += decoder.decode(value, { stream: true });
3543
3752
  const lines = buffer.split("\n");
@@ -3560,7 +3769,8 @@ ${jsonInstruction}` : jsonInstruction;
3560
3769
  provider: this.name,
3561
3770
  status,
3562
3771
  headers: responseHeaders,
3563
- message: event.error?.message ?? "Anthropic stream failed"
3772
+ message: event.error?.message ?? "Anthropic stream failed",
3773
+ timing: timing.streamTiming()
3564
3774
  });
3565
3775
  }
3566
3776
  case "content_block_start": {
@@ -3796,7 +4006,6 @@ var GEMINI_PRICING = {
3796
4006
  },
3797
4007
  "gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7 },
3798
4008
  "gemini-2.5-flash-lite": { input: 1e-7, cached: 1e-8, output: 4e-7 },
3799
- "gemini-3-flash-preview": { input: 5e-7, cached: 5e-8, output: 3e-6 },
3800
4009
  "gemini-3.1-pro-preview": {
3801
4010
  input: 2e-6,
3802
4011
  cached: 2e-7,
@@ -3812,7 +4021,9 @@ var GEMINI_PRICING = {
3812
4021
  "gemini-3.1-flash-lite": { input: 25e-8, cached: 25e-9, output: 15e-7 },
3813
4022
  "gemini-3.5-flash": { input: 15e-7, cached: 15e-8, output: 9e-6 },
3814
4023
  "gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
3815
- "gemini-3.6-flash": { input: 15e-7, cached: 15e-8, output: 75e-7 }
4024
+ "gemini-3.6-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
4025
+ "gemini-3.7-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
4026
+ "gemini-3.8-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 }
3816
4027
  };
3817
4028
  function isValidTokenCount2(value) {
3818
4029
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
@@ -4157,6 +4368,7 @@ var GeminiProvider = class {
4157
4368
  const pricingContext = this.pricingContext(this.requestModel(options), body);
4158
4369
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
4159
4370
  const headers = this.buildHeaders(await this.resolveKey());
4371
+ const recorder = new CallTimingRecorder();
4160
4372
  const res = await fetchWithRetry(
4161
4373
  `${this.baseUrl}/models/${pricingContext.model}:generateContent`,
4162
4374
  {
@@ -4165,7 +4377,7 @@ var GeminiProvider = class {
4165
4377
  body: JSON.stringify(body),
4166
4378
  signal: options.signal
4167
4379
  },
4168
- { governor: this.governor, provider: this.name }
4380
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4169
4381
  );
4170
4382
  if (!res.ok) {
4171
4383
  const errorBody = await res.text();
@@ -4175,11 +4387,12 @@ var GeminiProvider = class {
4175
4387
  status: res.status,
4176
4388
  headers: res.headers,
4177
4389
  message,
4178
- body: errorBody
4390
+ body: errorBody,
4391
+ timing: recorder.chatTiming()
4179
4392
  });
4180
4393
  }
4181
4394
  const json = await res.json();
4182
- return this.parseResponse(json, pricingContext);
4395
+ return withChatTiming(recorder, this.parseResponse(json, pricingContext));
4183
4396
  }
4184
4397
  // ---------------------------------------------------------------------------
4185
4398
  // stream - SSE streaming completion
@@ -4193,6 +4406,7 @@ var GeminiProvider = class {
4193
4406
  const pricingContext = this.pricingContext(this.requestModel(options), body);
4194
4407
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
4195
4408
  const headers = this.buildHeaders(await this.resolveKey());
4409
+ const recorder = new CallTimingRecorder();
4196
4410
  const res = await fetchWithRetry(
4197
4411
  `${this.baseUrl}/models/${pricingContext.model}:streamGenerateContent?alt=sse`,
4198
4412
  {
@@ -4201,7 +4415,7 @@ var GeminiProvider = class {
4201
4415
  body: JSON.stringify(body),
4202
4416
  signal: options.signal
4203
4417
  },
4204
- { governor: this.governor, provider: this.name }
4418
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4205
4419
  );
4206
4420
  if (!res.ok) {
4207
4421
  const errorBody = await res.text();
@@ -4211,13 +4425,14 @@ var GeminiProvider = class {
4211
4425
  status: res.status,
4212
4426
  headers: res.headers,
4213
4427
  message,
4214
- body: errorBody
4428
+ body: errorBody,
4429
+ timing: recorder.chatTiming()
4215
4430
  });
4216
4431
  }
4217
4432
  if (!res.body) {
4218
4433
  throw new Error("Gemini stream response has no body");
4219
4434
  }
4220
- yield* this.parseSSEStream(res.body, pricingContext);
4435
+ yield* withCallTiming(recorder, this.parseSSEStream(res.body, pricingContext, recorder));
4221
4436
  }
4222
4437
  // ---------------------------------------------------------------------------
4223
4438
  // Rich image requests use Gemini Interactions. It is deliberately stateless:
@@ -4228,10 +4443,11 @@ var GeminiProvider = class {
4228
4443
  this.assertSafeInteractionOptions(options);
4229
4444
  const body = this.buildInteractionRequestBody(messages, options, false);
4230
4445
  const headers = this.buildHeaders(await this.resolveKey());
4446
+ const recorder = new CallTimingRecorder();
4231
4447
  const res = await fetchWithRetry(
4232
4448
  `${this.baseUrl}/interactions`,
4233
4449
  { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
4234
- { governor: this.governor, provider: this.name }
4450
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4235
4451
  );
4236
4452
  if (!res.ok) {
4237
4453
  const errorBody = await res.text();
@@ -4240,19 +4456,24 @@ var GeminiProvider = class {
4240
4456
  status: res.status,
4241
4457
  headers: res.headers,
4242
4458
  message: this.extractErrorMessage(errorBody, res.status),
4243
- body: errorBody
4459
+ body: errorBody,
4460
+ timing: recorder.chatTiming()
4244
4461
  });
4245
4462
  }
4246
- return this.parseInteractionResponse(await res.json());
4463
+ return withChatTiming(
4464
+ recorder,
4465
+ this.parseInteractionResponse(await res.json())
4466
+ );
4247
4467
  }
4248
4468
  async *streamInteraction(messages, options) {
4249
4469
  this.assertSafeInteractionOptions(options);
4250
4470
  const body = this.buildInteractionRequestBody(messages, options, true);
4251
4471
  const headers = this.buildHeaders(await this.resolveKey());
4472
+ const recorder = new CallTimingRecorder();
4252
4473
  const res = await fetchWithRetry(
4253
4474
  `${this.baseUrl}/interactions?alt=sse`,
4254
4475
  { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
4255
- { governor: this.governor, provider: this.name }
4476
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4256
4477
  );
4257
4478
  if (!res.ok) {
4258
4479
  const errorBody = await res.text();
@@ -4261,11 +4482,15 @@ var GeminiProvider = class {
4261
4482
  status: res.status,
4262
4483
  headers: res.headers,
4263
4484
  message: this.extractErrorMessage(errorBody, res.status),
4264
- body: errorBody
4485
+ body: errorBody,
4486
+ timing: recorder.chatTiming()
4265
4487
  });
4266
4488
  }
4267
4489
  if (!res.body) throw new Error("Gemini Interactions stream has no body");
4268
- yield* this.parseInteractionSSEStream(res.body, res.headers);
4490
+ yield* withCallTiming(
4491
+ recorder,
4492
+ this.parseInteractionSSEStream(res.body, res.headers, recorder)
4493
+ );
4269
4494
  }
4270
4495
  buildInteractionRequestBody(messages, options, stream) {
4271
4496
  const model = this.requestModel(options);
@@ -4421,7 +4646,7 @@ var GeminiProvider = class {
4421
4646
  providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
4422
4647
  };
4423
4648
  }
4424
- async *parseInteractionSSEStream(body, responseHeaders) {
4649
+ async *parseInteractionSSEStream(body, responseHeaders, timing) {
4425
4650
  const reader = body.getReader();
4426
4651
  const decoder = new TextDecoder();
4427
4652
  let buffer = "";
@@ -4431,7 +4656,7 @@ var GeminiProvider = class {
4431
4656
  let completed = false;
4432
4657
  try {
4433
4658
  while (true) {
4434
- const { done, value } = await reader.read();
4659
+ const { done, value } = await timing.read(reader);
4435
4660
  if (done) break;
4436
4661
  buffer += decoder.decode(value, { stream: true });
4437
4662
  const lines = buffer.split("\n");
@@ -4454,14 +4679,16 @@ var GeminiProvider = class {
4454
4679
  provider: this.name,
4455
4680
  status,
4456
4681
  headers: responseHeaders,
4457
- message
4682
+ message,
4683
+ timing: timing.streamTiming()
4458
4684
  });
4459
4685
  }
4460
4686
  throw new ProviderError({
4461
4687
  provider: this.name,
4462
4688
  status: 0,
4463
4689
  retryable: false,
4464
- message
4690
+ message,
4691
+ timing: timing.streamTiming()
4465
4692
  });
4466
4693
  }
4467
4694
  if (event.event_type === "step.start" && event.step) {
@@ -4939,7 +5166,7 @@ var GeminiProvider = class {
4939
5166
  // ---------------------------------------------------------------------------
4940
5167
  // Internal: SSE stream parsing
4941
5168
  // ---------------------------------------------------------------------------
4942
- async *parseSSEStream(body, pricingContext) {
5169
+ async *parseSSEStream(body, pricingContext, timing) {
4943
5170
  const reader = body.getReader();
4944
5171
  const decoder = new TextDecoder();
4945
5172
  let buffer = "";
@@ -4952,7 +5179,7 @@ var GeminiProvider = class {
4952
5179
  const accumulatedParts = [];
4953
5180
  try {
4954
5181
  while (true) {
4955
- const { done, value } = await reader.read();
5182
+ const { done, value } = await timing.read(reader);
4956
5183
  if (done) break;
4957
5184
  buffer += decoder.decode(value, { stream: true });
4958
5185
  const lines = buffer.split("\n");
@@ -9462,6 +9689,13 @@ function describeValue(value) {
9462
9689
  }
9463
9690
  return `a value of type ${typeof value}`;
9464
9691
  }
9692
+ var PROMPT_CACHE_COLD_WRITE_WARN_AFTER = 2;
9693
+ function nextUserTurn(history, from) {
9694
+ for (let i = Math.max(0, from); i < history.length; i++) {
9695
+ if (history[i].role === "user") return i;
9696
+ }
9697
+ return void 0;
9698
+ }
9465
9699
  function estimateMessagesTokens(messages) {
9466
9700
  let total = 0;
9467
9701
  let unmeasured = false;
@@ -9601,6 +9835,13 @@ var WorkflowContext = class _WorkflowContext {
9601
9835
  workflowLifecycleState;
9602
9836
  signal;
9603
9837
  summaryCache;
9838
+ /** Consecutive provider calls with promptCache on that wrote to the cache
9839
+ * without reading from it (prefix changes per call), and consecutive calls
9840
+ * with no cache activity at all (prefix below the model's minimum, which
9841
+ * Anthropic silently ignores). Either reports once per context. */
9842
+ promptCacheColdWrites = 0;
9843
+ promptCacheInactive = 0;
9844
+ promptCacheWarned = false;
9604
9845
  workflowName;
9605
9846
  mcpManager;
9606
9847
  spanManager;
@@ -10133,6 +10374,17 @@ var WorkflowContext = class _WorkflowContext {
10133
10374
  const costAfter2 = this.budgetContext?.totalCost ?? 0;
10134
10375
  span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
10135
10376
  span.setAttribute("axl.agent.duration", Date.now() - askStart);
10377
+ if (usageCapture.timing) {
10378
+ const t = usageCapture.timing;
10379
+ span.setAttribute("axl.agent.queued_ms", t.queuedMs);
10380
+ span.setAttribute("axl.agent.retry_ms", t.retryMs);
10381
+ span.setAttribute("axl.agent.attempts", t.attempts);
10382
+ span.setAttribute("axl.agent.ttfb_ms", t.ttfbMs);
10383
+ span.setAttribute("axl.agent.wire_ms", t.wireMs);
10384
+ if (t.firstTokenMs !== void 0) {
10385
+ span.setAttribute("axl.agent.first_token_ms", t.firstTokenMs);
10386
+ }
10387
+ }
10136
10388
  if (usageCapture.value) {
10137
10389
  span.setAttribute(
10138
10390
  "axl.agent.prompt_tokens",
@@ -10340,6 +10592,14 @@ var WorkflowContext = class _WorkflowContext {
10340
10592
  const toolTokens = toolDefs.length > 0 ? estimateTokens(JSON.stringify(toolDefs)) : 0;
10341
10593
  const overhead = systemTokens + toolTokens + reserveTokens;
10342
10594
  const availableForHistory = maxContext - overhead;
10595
+ if (availableForHistory <= 0) {
10596
+ this.emitEvent({
10597
+ type: "log",
10598
+ data: {
10599
+ warning: `Agent '${agent2._name}' maxContext (${maxContext}) is at or below its fixed overhead (${overhead} = system ${systemTokens} + tools ${toolTokens} + reserve ${reserveTokens}); no session history can fit, so every ask summarizes, retaining at most the most recent exchange and sometimes none of it verbatim. The current input is always sent. Raise maxContext or lower contextManagement.reserveTokens.`
10600
+ }
10601
+ });
10602
+ }
10343
10603
  const historyEstimate = estimateMessagesTokens(sessionHistory);
10344
10604
  if (historyEstimate.unmeasured) {
10345
10605
  this.emitEvent({
@@ -10405,6 +10665,7 @@ ${appendedSchemaText}`;
10405
10665
  if (handoffContext.length > 0) {
10406
10666
  messages.push({
10407
10667
  role: "system",
10668
+ origin: "runtime",
10408
10669
  content: "The following is the conversation history from the previous agent that handed off to you:"
10409
10670
  });
10410
10671
  for (const msg of handoffContext) {
@@ -10454,6 +10715,7 @@ ${appendedSchemaText}`;
10454
10715
  const maxTurns = agent2._config.maxTurns ?? 25;
10455
10716
  const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
10456
10717
  const startTime = Date.now();
10718
+ const timingTotals = { turns: 0, queuedMs: 0, retryMs: 0, wireMs: 0 };
10457
10719
  const currentMessages = [...messages];
10458
10720
  let turns = 0;
10459
10721
  let guardrailOutputRetries = 0;
@@ -10467,8 +10729,22 @@ ${appendedSchemaText}`;
10467
10729
  let lastStartMaxAttempts = 1;
10468
10730
  while (turns < maxTurns) {
10469
10731
  this.currentSignal?.throwIfAborted();
10470
- if (Date.now() - startTime > timeoutMs) {
10471
- throw new TimeoutError("ctx.ask()", timeoutMs);
10732
+ const elapsedMs = Date.now() - startTime;
10733
+ if (elapsedMs > timeoutMs) {
10734
+ throw new TimeoutError(
10735
+ "ctx.ask()",
10736
+ timeoutMs,
10737
+ timingTotals.turns > 0 ? {
10738
+ elapsedMs,
10739
+ queuedMs: timingTotals.queuedMs,
10740
+ retryMs: timingTotals.retryMs,
10741
+ wireMs: timingTotals.wireMs,
10742
+ otherMs: Math.max(
10743
+ 0,
10744
+ elapsedMs - timingTotals.queuedMs - timingTotals.retryMs - timingTotals.wireMs
10745
+ )
10746
+ } : void 0
10747
+ );
10472
10748
  }
10473
10749
  turns++;
10474
10750
  const isFirstTurn = turns === 1;
@@ -10508,6 +10784,7 @@ ${appendedSchemaText}`;
10508
10784
  effort: options?.effort ?? agent2._config.effort,
10509
10785
  thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
10510
10786
  includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
10787
+ promptCache: options?.promptCache ?? agent2._config.promptCache,
10511
10788
  toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
10512
10789
  stop: options?.stop ?? agent2._config.stop,
10513
10790
  providerOptions,
@@ -10585,6 +10862,7 @@ ${appendedSchemaText}`;
10585
10862
  const toolCalls = [];
10586
10863
  const toolCallBuffers = /* @__PURE__ */ new Map();
10587
10864
  let streamProviderMetadata;
10865
+ let streamTiming;
10588
10866
  let thinkingContent = "";
10589
10867
  const streamingObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof z.ZodObject;
10590
10868
  const currentAttempt = schemaRetries + 1;
@@ -10636,6 +10914,7 @@ ${appendedSchemaText}`;
10636
10914
  if (chunk.arguments) buffer.arguments += chunk.arguments;
10637
10915
  } else if (chunk.type === "done") {
10638
10916
  streamProviderMetadata = chunk.providerMetadata;
10917
+ streamTiming = chunk.timing;
10639
10918
  if (chunk.usage) {
10640
10919
  response = {
10641
10920
  content: content2,
@@ -10663,6 +10942,9 @@ ${appendedSchemaText}`;
10663
10942
  if (streamProviderMetadata) {
10664
10943
  response.providerMetadata = streamProviderMetadata;
10665
10944
  }
10945
+ if (streamTiming) {
10946
+ response.timing = streamTiming;
10947
+ }
10666
10948
  if (thinkingContent) {
10667
10949
  response.thinking_content = thinkingContent;
10668
10950
  }
@@ -10671,12 +10953,15 @@ ${appendedSchemaText}`;
10671
10953
  }
10672
10954
  } catch (err) {
10673
10955
  if (richRequest) this.recordRichFailure(err);
10956
+ const failureTiming = err instanceof ProviderError ? err.timing : void 0;
10674
10957
  this.emitEvent({
10675
10958
  type: "agent_call_end",
10676
10959
  agent: agent2._name,
10677
10960
  model: effectiveModelUri,
10678
10961
  promptVersion: agent2._config.version,
10679
10962
  duration: Date.now() - turnStart,
10963
+ // Top-level beside `duration`, exactly as on the success path.
10964
+ ...failureTiming ? { timing: failureTiming } : {},
10680
10965
  data: {
10681
10966
  response: "",
10682
10967
  turn: turns,
@@ -10693,9 +10978,41 @@ ${appendedSchemaText}`;
10693
10978
  if (usageCapture && response.usage) {
10694
10979
  usageCapture.value = response.usage;
10695
10980
  }
10981
+ if (usageCapture && response.timing) {
10982
+ usageCapture.timing = response.timing;
10983
+ }
10984
+ if (chatOptions.promptCache && provider.realizesPromptCache?.(model) === true && response.usage && !this.promptCacheWarned) {
10985
+ const wrote = (response.usage.cache_write_tokens ?? 0) > 0;
10986
+ const read = (response.usage.cached_tokens ?? 0) > 0;
10987
+ if (read) {
10988
+ this.promptCacheColdWrites = 0;
10989
+ this.promptCacheInactive = 0;
10990
+ } else if (wrote) {
10991
+ this.promptCacheColdWrites += 1;
10992
+ this.promptCacheInactive = 0;
10993
+ } else {
10994
+ this.promptCacheInactive += 1;
10995
+ }
10996
+ let warning;
10997
+ if (this.promptCacheColdWrites >= PROMPT_CACHE_COLD_WRITE_WARN_AFTER) {
10998
+ warning = `Agent '${agent2._name}' has promptCache on, but its prefix was written to the cache ${this.promptCacheColdWrites} times in a row with no cache reads. Each write bills 1.25x input. If this agent's system prompt or tool set changes per call, set promptCache: false for it.`;
10999
+ } else if (this.promptCacheInactive >= PROMPT_CACHE_COLD_WRITE_WARN_AFTER) {
11000
+ warning = `Agent '${agent2._name}' has promptCache on, but ${this.promptCacheInactive} calls in a row showed no cache writes or reads. Anthropic silently ignores a breakpoint below the model's minimum cacheable length (4,096 tokens on Haiku 4.5; 1,024 or 512 on others) -- the system prompt plus tools may be too short to cache on this model, or the provider may not support prompt caching.`;
11001
+ }
11002
+ if (warning) {
11003
+ this.promptCacheWarned = true;
11004
+ this.emitEvent({ type: "log", agent: agent2._name, data: { warning } });
11005
+ }
11006
+ }
10696
11007
  if (response.cost) {
10697
11008
  this._accumulateBudgetCost(response.cost);
10698
11009
  }
11010
+ if (response.timing) {
11011
+ timingTotals.turns += 1;
11012
+ timingTotals.queuedMs += response.timing.queuedMs;
11013
+ timingTotals.retryMs += response.timing.retryMs;
11014
+ timingTotals.wireMs += response.timing.wireMs;
11015
+ }
10699
11016
  this.emitEvent({
10700
11017
  type: "agent_call_end",
10701
11018
  agent: agent2._name,
@@ -10710,6 +11027,8 @@ ${appendedSchemaText}`;
10710
11027
  cacheWrite: response.usage.cache_write_tokens
10711
11028
  } : void 0,
10712
11029
  duration: Date.now() - turnStart,
11030
+ // Additive beside `duration`, which keeps its turn-wall-clock meaning.
11031
+ ...response.timing ? { timing: response.timing } : {},
10713
11032
  data: {
10714
11033
  response: response.content,
10715
11034
  ...response.thinking_content ? { thinking: response.thinking_content } : {},
@@ -11428,6 +11747,7 @@ ${appendedSchemaText}`;
11428
11747
  if (this.summaryCache) {
11429
11748
  const summaryMsg2 = {
11430
11749
  role: "system",
11750
+ origin: "runtime",
11431
11751
  content: `Summary of earlier conversation:
11432
11752
  ${this.summaryCache}`
11433
11753
  };
@@ -11441,6 +11761,7 @@ ${this.summaryCache}`
11441
11761
  recentTokens2 += msgTokens;
11442
11762
  splitIdx2 = i;
11443
11763
  }
11764
+ splitIdx2 = nextUserTurn(history, splitIdx2) ?? history.length;
11444
11765
  if (splitIdx2 < history.length) {
11445
11766
  return [summaryMsg2, ...history.slice(splitIdx2)];
11446
11767
  }
@@ -11454,6 +11775,19 @@ ${this.summaryCache}`
11454
11775
  recentTokens += msgTokens;
11455
11776
  splitIdx = i;
11456
11777
  }
11778
+ const anchored = nextUserTurn(history, splitIdx);
11779
+ if (anchored !== void 0) {
11780
+ splitIdx = anchored;
11781
+ } else if (splitIdx === history.length && history.length > 1) {
11782
+ for (let i = history.length - 1; i > 0; i--) {
11783
+ if (history[i].role === "user") {
11784
+ splitIdx = i;
11785
+ break;
11786
+ }
11787
+ }
11788
+ } else {
11789
+ splitIdx = history.length;
11790
+ }
11457
11791
  if (splitIdx === 0) return history;
11458
11792
  const oldMessages = history.slice(0, splitIdx);
11459
11793
  const summaryModelUri = this.config.contextManagement?.summaryModel;
@@ -11485,6 +11819,7 @@ ${this.summaryCache}`
11485
11819
  }
11486
11820
  const summaryMsg = {
11487
11821
  role: "system",
11822
+ origin: "runtime",
11488
11823
  content: `Summary of earlier conversation:
11489
11824
  ${summaryResponse.content}`
11490
11825
  };
@@ -16260,6 +16595,8 @@ var AxlRuntime = class extends EventEmitter2 {
16260
16595
  parent: parentScope
16261
16596
  };
16262
16597
  const modelCalls = /* @__PURE__ */ new Map();
16598
+ const modelTiming = /* @__PURE__ */ new Map();
16599
+ const captureTimingSamples = options?.captureTimingSamples === true;
16263
16600
  const workflowCalls = /* @__PURE__ */ new Map();
16264
16601
  const tokens = { input: 0, output: 0, reasoning: 0 };
16265
16602
  let agentCalls = 0;
@@ -16277,6 +16614,30 @@ var AxlRuntime = class extends EventEmitter2 {
16277
16614
  tokens.output += event.tokens.output ?? 0;
16278
16615
  tokens.reasoning += event.tokens.reasoning ?? 0;
16279
16616
  }
16617
+ const failed2 = event.data?.error != null;
16618
+ if (event.model && event.timing && !failed2) {
16619
+ const t = event.timing;
16620
+ let bucket = modelTiming.get(event.model);
16621
+ if (!bucket) {
16622
+ bucket = {
16623
+ calls: 0,
16624
+ queuedMs: 0,
16625
+ retryMs: 0,
16626
+ wireMs: 0,
16627
+ ...captureTimingSamples ? { samples: [] } : {}
16628
+ };
16629
+ modelTiming.set(event.model, bucket);
16630
+ }
16631
+ bucket.calls++;
16632
+ bucket.samples?.push(t);
16633
+ bucket.queuedMs += t.queuedMs;
16634
+ bucket.retryMs += t.retryMs;
16635
+ bucket.wireMs += t.wireMs;
16636
+ if (t.firstTokenMs != null) {
16637
+ bucket.firstTokenMs = (bucket.firstTokenMs ?? 0) + t.firstTokenMs;
16638
+ bucket.firstTokenCalls = (bucket.firstTokenCalls ?? 0) + 1;
16639
+ }
16640
+ }
16280
16641
  }
16281
16642
  if (event.type === "workflow_start" && event.workflow) {
16282
16643
  workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
@@ -16307,6 +16668,7 @@ var AxlRuntime = class extends EventEmitter2 {
16307
16668
  result,
16308
16669
  cost: scope.totalCost,
16309
16670
  unpriced,
16671
+ ...modelTiming.size > 0 ? { modelTiming: Object.fromEntries(modelTiming) } : {},
16310
16672
  ...capturedTraces ? { traces: capturedTraces } : {},
16311
16673
  metadata: {
16312
16674
  models: [...modelCalls.keys()],