@genesislcap/foundation-ai 15.11.0 → 15.12.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.
@@ -2,6 +2,7 @@ import { __awaiter } from "tslib";
2
2
  import { SUPPORTED_ANTHROPIC_MODEL_IDS, } from '../types';
3
3
  import { logger } from '../utils/logger';
4
4
  import { scaleTemperature } from '../utils/temperature';
5
+ import { anthropicTokenCost } from '../utils/token-cost';
5
6
  import { enforceAnthropicToolSchema } from '../utils/tool-schema';
6
7
  import { VENDOR_LABELS } from './budget-exhausted-error';
7
8
  import { postWithRetry } from './post-with-retry';
@@ -52,25 +53,6 @@ function assertSupportedAnthropicModel(model) {
52
53
  throw new Error(`AnthropicTransport: unsupported model "${model}". Use one of: ${SUPPORTED_ANTHROPIC_MODEL_IDS.join(', ')}.`);
53
54
  }
54
55
  }
55
- /**
56
- * Standard tier pricing per million tokens — https://docs.claude.com/en/docs/about-claude/pricing
57
- */
58
- function estimatedAnthropicRatesUsdPerMillion(model) {
59
- if (model === 'claude-haiku-4-5-20251001') {
60
- return { promptPerMillion: 1, candidatePerMillion: 5 };
61
- }
62
- // Fable 5 — Anthropic's most capable widely-released model; priced above Opus tier.
63
- if (model === 'claude-fable-5') {
64
- return { promptPerMillion: 10, candidatePerMillion: 50 };
65
- }
66
- // Sonnet 5 and Sonnet 4.6 share the standard Sonnet tier ($3 / $15 per MTok). Sonnet 5's
67
- // introductory rate ($2 / $10 through 2026-08-31) is deliberately NOT used here — standard rates.
68
- if (model === 'claude-sonnet-5' || model === 'claude-sonnet-4-6') {
69
- return { promptPerMillion: 3, candidatePerMillion: 15 };
70
- }
71
- // Opus 4.7 / 4.8 — same $5 / $25 per MTok.
72
- return { promptPerMillion: 5, candidatePerMillion: 25 };
73
- }
74
56
  /**
75
57
  * Models that reject non-default sampling parameters (`temperature`/`top_p`/`top_k`) with a 400 —
76
58
  * the Opus 4.7+ / Sonnet 5 / Fable 5 generation. Sonnet 4.6 and Haiku 4.5 still accept them.
@@ -94,39 +76,74 @@ function supportsNativeStructuredOutput(model) {
94
76
  model === 'claude-haiku-4-5-20251001');
95
77
  }
96
78
  /**
97
- * Extended-thinking posture for a request. Sonnet 5 and Fable 5 run adaptive thinking with
98
- * `display:'summarized'` regardless of `tool_choice`. First-party has no forced-tool-vs-thinking
99
- * restriction (that's Bedrock-only), and "adaptive" self-regulates (the model thinks little on
100
- * trivial/mechanical turns on its own), so there is no reason to special-case forced tool calls.
101
- * `display:'summarized'` means the reasoning summary is always returned (billed regardless; the
102
- * host's toggle decides visibility).
79
+ * Whether a model accepts `thinking: {type:'adaptive'}` at all. Everything from the 4.6
80
+ * generation on does; Haiku 4.5 does not, and asking for it there is a 400 rather than a
81
+ * no-op — so a caller's `'auto'` degrades to the model default instead of failing the turn.
82
+ */
83
+ function supportsAdaptiveThinking(model) {
84
+ return model !== 'claude-haiku-4-5-20251001';
85
+ }
86
+ /**
87
+ * Models that think **unconditionally** — `{type:'disabled'}` is a 400, not a no-op. The same
88
+ * constraint exists on the other provider (Gemini 2.5 Pro), so this is a property of flagship
89
+ * reasoning models rather than an Anthropic quirk; see `ChatThinkingPolicy`.
90
+ */
91
+ function thinkingIsMandatory(model) {
92
+ return model === 'claude-fable-5';
93
+ }
94
+ /**
95
+ * Extended-thinking posture for a request, given the model and the caller's
96
+ * {@link ChatThinkingPolicy} for this turn.
103
97
  *
104
- * Fable 5 runs thinking *unconditionally* (it cannot be disabledan explicit
105
- * `{type:'disabled'}` is a 400), so it must never be sent `disabled`; `{type:'adaptive'}`
106
- * is the accepted posture and is what we return.
98
+ * **`policy === undefined` is the per-model default, and is a distinct third state** not a
99
+ * synonym for either value. It reproduces exactly what this function returned before the option
100
+ * existed, so an agent that leaves `thinkingPolicy` unset (or whose resolver returns `undefined`
101
+ * on a given turn) is priced and behaves identically to before:
107
102
  *
108
- * Every other model (Opus 4.8/4.7, Sonnet 4.6, Haiku) returns undefined `thinking` is omitted
109
- * it runs WITHOUT thinking on the chat path (Opus 4.8 adaptive thinking is opt-in). So the older
110
- * "forced tool + thinking is incompatible" restriction never applies to them (you can't hit it when
111
- * thinking is off). If thinking is ever enabled for one of those, revisit the forced-tool
112
- * interaction for that model then.
103
+ * - Sonnet 5 and Fable 5 adaptive thinking with `display:'summarized'`, regardless of
104
+ * `tool_choice`. First-party has no forced-tool-vs-thinking restriction (that's Bedrock-only),
105
+ * and "adaptive" self-regulates (the model thinks little on trivial turns on its own), so there
106
+ * is no reason to special-case forced tool calls. `display:'summarized'` means the reasoning
107
+ * summary is always returned billed regardless, with the host's toggle deciding only
108
+ * *visibility*. Hiding the steps saves no money; `'off'` is what saves money.
109
+ * - Every other model (Opus 4.8/4.7, Sonnet 4.6, Haiku) → `thinking` omitted, so it runs WITHOUT
110
+ * thinking on the chat path (their adaptive thinking is opt-in). The older "forced tool +
111
+ * thinking is incompatible" restriction therefore never applies to them — you cannot hit it
112
+ * when thinking is off.
113
+ *
114
+ * An explicit policy overrides that default, **clamped to what the model can actually honour**
115
+ * so a policy can never turn a turn into a 400:
116
+ *
117
+ * - `'off'` on a mandatory-thinking model (Fable 5) → left at its default. Requesting `disabled`
118
+ * there is a 400; the caller is told once via {@link AnthropicTransport} rather than losing
119
+ * the turn.
120
+ * - `'auto'` on a model without adaptive support (Haiku) → omitted, i.e. its default.
121
+ *
122
+ * `'off'` is expressed as an *omission* on models that already default to no thinking, rather
123
+ * than an explicit `{type:'disabled'}`: the two are equivalent on the wire there, and omitting
124
+ * keeps the request identical to the one those models have always received.
113
125
  */
114
- function anthropicThinking(model) {
115
- if (model !== 'claude-sonnet-5' && model !== 'claude-fable-5')
116
- return undefined;
117
- return { type: 'adaptive', display: 'summarized' };
126
+ function anthropicThinking(model, policy) {
127
+ const defaultsToThinking = model === 'claude-sonnet-5' || thinkingIsMandatory(model);
128
+ const adaptive = { type: 'adaptive', display: 'summarized' };
129
+ if (policy === 'off') {
130
+ // Cannot be turned off — keep the model's default rather than 400 the turn.
131
+ if (thinkingIsMandatory(model))
132
+ return adaptive;
133
+ // Already off by default: omitting and disabling are the same request.
134
+ return defaultsToThinking ? { type: 'disabled' } : undefined;
135
+ }
136
+ if (policy === 'auto') {
137
+ return supportsAdaptiveThinking(model) ? adaptive : undefined;
138
+ }
139
+ // policy === undefined → the per-model default, unchanged.
140
+ return defaultsToThinking ? adaptive : undefined;
118
141
  }
119
142
  /**
120
- * Prompt-cache pricing multipliers, applied to the model's base input rate
121
- * (`promptPerMillion`) https://docs.claude.com/en/docs/build-with-claude/prompt-caching
122
- * Reads bill at ~0.1× base input; writes bill by TTL — 5-minute at ~1.25× and 1-hour at 2×.
123
- * Both write TTLs are reachable (`CachePolicy.ttl` is `'5m' | '1h'` and `applyCacheControl`
124
- * emits either), so the cost path costs each TTL bucket from the response's per-TTL
125
- * `cache_creation` breakdown rather than assuming a single rate.
143
+ * Key under which this transport stashes its round-trip state on a tool call's
144
+ * provider-neutral `providerMetadata` bag. Private to AnthropicTransport.
126
145
  */
127
- const ANTHROPIC_CACHE_READ_MULTIPLIER = 0.1;
128
- const ANTHROPIC_CACHE_WRITE_5M_MULTIPLIER = 1.25;
129
- const ANTHROPIC_CACHE_WRITE_1H_MULTIPLIER = 2;
146
+ const ANTHROPIC_PROVIDER_KEY = 'anthropic';
130
147
  /**
131
148
  * Thrown when a response was cut off at the output-token limit (Anthropic
132
149
  * `stop_reason: 'max_tokens'`, Gemini `finishReason: 'MAX_TOKENS'`) in a way
@@ -184,6 +201,27 @@ export class ResponseTruncatedError extends Error {
184
201
  * @beta
185
202
  */
186
203
  export class AnthropicTransport {
204
+ /**
205
+ * Warn once when a requested policy is silently clamped, in EITHER direction — which is what
206
+ * `ChatThinkingPolicy` promises callers. Both directions cost the caller something they asked
207
+ * for and would otherwise get no signal about: `'off'` on a model that always thinks keeps
208
+ * billing reasoning as output, and `'auto'` on a model without adaptive support means an agent
209
+ * that asked to reason quietly does not. (Gemini's twin deliberately stays silent on `'auto'`,
210
+ * but only because dynamic thinking is already the default there; Haiku defaults to none, so
211
+ * the same silence would hide a real difference.)
212
+ */
213
+ warnIfThinkingUnclampable(policy) {
214
+ if (this.warnedThinkingClamped)
215
+ return;
216
+ const clamped = (policy === 'off' && thinkingIsMandatory(this.model)) ||
217
+ (policy === 'auto' && !supportsAdaptiveThinking(this.model));
218
+ if (!clamped)
219
+ return;
220
+ this.warnedThinkingClamped = true;
221
+ logger.warn(policy === 'off'
222
+ ? `AnthropicTransport: thinkingPolicy 'off' ignored — ${this.model} always thinks and rejects an explicit disable. Reasoning tokens are still billed as output; switch model if you need them gone.`
223
+ : `AnthropicTransport: thinkingPolicy 'auto' ignored — ${this.model} does not support adaptive thinking, so this turn runs without reasoning. Use a Sonnet or Opus tier if the agent needs it.`);
224
+ }
187
225
  constructor(config = {}) {
188
226
  var _a, _b, _c, _d;
189
227
  /**
@@ -199,6 +237,16 @@ export class AnthropicTransport {
199
237
  * accrue. Surfaced alongside `getLifetimeCost`.
200
238
  */
201
239
  this.lifetimeSavingsUsd = 0;
240
+ /**
241
+ * Whether we have already told the caller their `thinkingPolicy: 'off'` cannot be honoured
242
+ * on this model. Latched, because it would otherwise fire on every turn of a tool loop.
243
+ */
244
+ this.warnedThinkingClamped = false;
245
+ /**
246
+ * Serving-model ids already reported as unrecognised. Per-id rather than a single flag, so a
247
+ * second unknown model is still announced. See {@link AnthropicTransport.servingModel}.
248
+ */
249
+ this.warnedUnknownServingModels = new Set();
202
250
  const model = (_a = config.model) !== null && _a !== void 0 ? _a : DEFAULT_MODEL;
203
251
  assertSupportedAnthropicModel(model);
204
252
  this.model = model;
@@ -276,7 +324,9 @@ export class AnthropicTransport {
276
324
  }
277
325
  // Sonnet 5 runs adaptive thinking by default; disable it for one-shot prompts — the reasoning
278
326
  // would be billed but discarded. Fable 5 runs thinking unconditionally (cannot be disabled — a
279
- // 400), so it is left on; its summary is simply unused here.
327
+ // 400), so it is left on; its summary is simply unused here. Not routed through
328
+ // `anthropicThinking`: this path wants the summary suppressed rather than returned, so a
329
+ // clamped Fable 5 must keep omitting the field rather than opting into `display:'summarized'`.
280
330
  if (this.model === 'claude-sonnet-5')
281
331
  body.thinking = { type: 'disabled' };
282
332
  const response = yield this.post(body);
@@ -294,8 +344,16 @@ export class AnthropicTransport {
294
344
  // ── ChatTransport (multi-turn chat) ────────────────────────────────────
295
345
  sendChatMessage(history, userMessage, options) {
296
346
  return __awaiter(this, void 0, void 0, function* () {
297
- var _a, _b, _c;
298
- const messages = this.toAnthropicMessages(history, userMessage, options === null || options === void 0 ? void 0 : options.attachments);
347
+ var _a, _b, _c, _d;
348
+ // The models this request could actually be served by the configured one plus any
349
+ // fallback target. Needed when replaying reasoning: a signature is only valid for the
350
+ // model that produced it, so stored blocks are replayed only when their producer is
351
+ // still reachable on THIS request. See `reasoningToReplay`.
352
+ const reachableModels = new Set([
353
+ this.model,
354
+ ...((_a = options === null || options === void 0 ? void 0 : options.fallbacks) !== null && _a !== void 0 ? _a : []).map((f) => f.model),
355
+ ]);
356
+ const messages = this.toAnthropicMessages(history, userMessage, options === null || options === void 0 ? void 0 : options.attachments, reachableModels);
299
357
  const body = {
300
358
  model: this.model,
301
359
  max_tokens: this.maxTokens,
@@ -303,7 +361,7 @@ export class AnthropicTransport {
303
361
  };
304
362
  if (options === null || options === void 0 ? void 0 : options.systemPrompt)
305
363
  body.system = options.systemPrompt;
306
- if ((_a = options === null || options === void 0 ? void 0 : options.tools) === null || _a === void 0 ? void 0 : _a.length) {
364
+ if ((_b = options === null || options === void 0 ? void 0 : options.tools) === null || _b === void 0 ? void 0 : _b.length) {
307
365
  // `enforceSchema` is a per-tool opt-in, and the provider compiles per tool:
308
366
  // an unenforced tool is never compiled and costs nothing against the
309
367
  // compilation limits, so marking one tool in a large surface is legitimate.
@@ -317,21 +375,31 @@ export class AnthropicTransport {
317
375
  // Map the requested tool-call mode to Anthropic's tool_choice — only
318
376
  // meaningful when tools exist. `'required'`/`{ tool }` force a tool call so
319
377
  // a turn can only end via one (e.g. a sub-agent's completion tool).
320
- if ((_b = body.tools) === null || _b === void 0 ? void 0 : _b.length) {
378
+ if ((_c = body.tools) === null || _c === void 0 ? void 0 : _c.length) {
321
379
  const toolChoice = toAnthropicToolChoice(options === null || options === void 0 ? void 0 : options.toolChoice);
322
380
  if (toolChoice)
323
381
  body.tool_choice = toolChoice;
324
382
  }
325
- // Extended thinking (Sonnet 5). Always adaptive regardless of `tool_choice` — first-party has
326
- // no forced-tool-vs-thinking restriction (that's Bedrock-only) and adaptive self-regulates on
327
- // mechanical turns. Older models omit it (see `anthropicThinking`).
328
- const thinking = anthropicThinking(this.model);
383
+ // Extended thinking. Adaptive regardless of `tool_choice` — first-party has no
384
+ // forced-tool-vs-thinking restriction (that's Bedrock-only) and adaptive self-regulates on
385
+ // mechanical turns. The per-turn `thinkingPolicy` overrides the per-model default; an
386
+ // undefined policy reproduces it exactly (see `anthropicThinking`).
387
+ this.warnIfThinkingUnclampable(options === null || options === void 0 ? void 0 : options.thinkingPolicy);
388
+ const thinking = anthropicThinking(this.model, options === null || options === void 0 ? void 0 : options.thinkingPolicy);
329
389
  if (thinking)
330
390
  body.thinking = thinking;
331
391
  // Normalized [0,1] temperature → Anthropic's native range, anchored so 0.5 maps to its default.
332
- // (Default == max here, so the upper half is flat.) Skipped on models that reject sampling
333
- // params (Opus 4.7+ / Sonnet 5), where sending `temperature` is a 400.
334
- if ((options === null || options === void 0 ? void 0 : options.temperature) != null && !rejectsSamplingParams(this.model)) {
392
+ // (Default == max here, so the upper half is flat.) Skipped in two cases:
393
+ //
394
+ // - models that reject sampling params outright (Opus 4.7+ / Sonnet 5 / Fable 5), where
395
+ // sending `temperature` is a 400 whether or not thinking is on;
396
+ // - ANY model with thinking enabled, because temperature is incompatible with thinking on
397
+ // the models that still accept sampling params. Sonnet 4.6 is the live case: it takes a
398
+ // temperature happily until `thinkingPolicy: 'auto'` turns thinking on, at which point
399
+ // `{ thinkingPolicy: 'auto', temperature: 0 }` — an entirely reasonable agent config —
400
+ // becomes an invalid request. The thinking posture wins; temperature is dropped.
401
+ const thinkingEnabled = (thinking === null || thinking === void 0 ? void 0 : thinking.type) === 'adaptive';
402
+ if ((options === null || options === void 0 ? void 0 : options.temperature) != null && !rejectsSamplingParams(this.model) && !thinkingEnabled) {
335
403
  body.temperature = scaleTemperature(options.temperature, {
336
404
  defaultTemp: ANTHROPIC_DEFAULT_TEMPERATURE,
337
405
  maxTemp: ANTHROPIC_MAX_TEMPERATURE,
@@ -350,7 +418,7 @@ export class AnthropicTransport {
350
418
  // Refusal fallback chain (e.g. Fable 5 → Opus 4.8). Sent as the server-side `fallbacks`
351
419
  // param; `post` adds the required beta header when this is present. A refused turn is
352
420
  // re-run on the next model in one round trip.
353
- if ((_c = options === null || options === void 0 ? void 0 : options.fallbacks) === null || _c === void 0 ? void 0 : _c.length) {
421
+ if ((_d = options === null || options === void 0 ? void 0 : options.fallbacks) === null || _d === void 0 ? void 0 : _d.length) {
354
422
  body.fallbacks = options.fallbacks.map((f) => f.maxTokens != null ? { model: f.model, max_tokens: f.maxTokens } : { model: f.model });
355
423
  }
356
424
  // Place prompt-cache breakpoints per the resolved policy (no-op for `'default'`/absent).
@@ -429,42 +497,31 @@ export class AnthropicTransport {
429
497
  * and returns the per-call total so the caller can attach it to the response
430
498
  * message.
431
499
  */
432
- logTokenUsage(promptTokens, candidateTokens, cacheReadTokens, cacheCreationTokens, cacheCreation1hTokens) {
433
- const { promptPerMillion, candidatePerMillion } = estimatedAnthropicRatesUsdPerMillion(this.model);
434
- const m = AnthropicTransport.TOKENS_PER_MILLION;
435
- // `promptTokens` is `input_tokens` the UNCACHED remainder, billed at the full input
436
- // rate. Cache reads bill at ~0.1×. Cache writes bill by TTL — 5-min at ~1.25×, 1-hour at
437
- // so split the creation total into its 1-hour portion (from the response breakdown)
438
- // and the 5-min remainder, costing each at its own rate. All buckets are off the same base
439
- // input rate. Summing them keeps the cost correct whether or not caching was active (all
440
- // cache fields are 0 when it wasn't).
441
- const cacheCreation5mTokens = Math.max(0, cacheCreationTokens - cacheCreation1hTokens);
442
- const promptCost = (promptTokens / m) * promptPerMillion;
443
- const cacheReadCost = (cacheReadTokens / m) * promptPerMillion * ANTHROPIC_CACHE_READ_MULTIPLIER;
444
- const cacheWriteCost = (cacheCreation5mTokens / m) * promptPerMillion * ANTHROPIC_CACHE_WRITE_5M_MULTIPLIER +
445
- (cacheCreation1hTokens / m) * promptPerMillion * ANTHROPIC_CACHE_WRITE_1H_MULTIPLIER;
446
- const candidateCost = (candidateTokens / m) * candidatePerMillion;
447
- const totalCost = promptCost + cacheReadCost + cacheWriteCost + candidateCost;
448
- this.lifetimeCostUsd += totalCost;
449
- // Net savings vs no caching: the cache-read tokens would have cost full input price
450
- // (we paid ~0.1× of that), while the write premium (~0.25× over full) is an upfront cost
451
- // that pays back on later reads — so the net can dip negative on a write-heavy request and
452
- // climb positive as reads accrue. Zero when caching was inactive.
453
- const cacheReadFull = (cacheReadTokens / m) * promptPerMillion;
454
- const cacheWriteFull = (cacheCreationTokens / m) * promptPerMillion;
455
- const saved = cacheReadFull - cacheReadCost + (cacheWriteFull - cacheWriteCost);
456
- this.lifetimeSavingsUsd += saved;
500
+ logTokenUsage(model, promptTokens, candidateTokens, cacheReadTokens, cacheCreationTokens, cacheCreation1hTokens) {
501
+ // The arithmetic and the rate table live in `utils/token-cost` so a host pricing
502
+ // its own requests — a server proxy, a usage ledger — uses the same numbers
503
+ // instead of a hand-copied table that drifts. This method keeps only what is
504
+ // instance-scoped: the lifetime accumulators and the log.
505
+ const { costUsd, savedUsd, breakdown } = anthropicTokenCost(model, {
506
+ uncachedInputTokens: promptTokens,
507
+ outputTokens: candidateTokens,
508
+ cacheReadTokens,
509
+ cacheWriteTokens: cacheCreationTokens,
510
+ cacheWrite1hTokens: cacheCreation1hTokens,
511
+ });
512
+ this.lifetimeCostUsd += costUsd;
513
+ this.lifetimeSavingsUsd += savedUsd;
457
514
  const dp = AnthropicTransport.COST_DECIMAL_PLACES;
458
- console.log(`--- Anthropic Token Usage (${this.model}) ---`);
459
- console.log(`Prompt Tokens: ${promptTokens} ($${promptCost.toFixed(dp)})`);
460
- console.log(`Cache Read: ${cacheReadTokens} ($${cacheReadCost.toFixed(dp)})`);
461
- console.log(`Cache Write: ${cacheCreationTokens} ($${cacheWriteCost.toFixed(dp)})`);
462
- console.log(`Candidate Tokens: ${candidateTokens} ($${candidateCost.toFixed(dp)})`);
463
- console.log(`Total Cost: $${totalCost.toFixed(dp)}`);
464
- console.log(`Cache Saved: $${saved.toFixed(dp)} (lifetime $${this.lifetimeSavingsUsd.toFixed(dp)})`);
515
+ console.log(`--- Anthropic Token Usage (${model}) ---`);
516
+ console.log(`Prompt Tokens: ${promptTokens} ($${breakdown.promptUsd.toFixed(dp)})`);
517
+ console.log(`Cache Read: ${cacheReadTokens} ($${breakdown.cacheReadUsd.toFixed(dp)})`);
518
+ console.log(`Cache Write: ${cacheCreationTokens} ($${breakdown.cacheWriteUsd.toFixed(dp)})`);
519
+ console.log(`Candidate Tokens: ${candidateTokens} ($${breakdown.candidateUsd.toFixed(dp)})`);
520
+ console.log(`Total Cost: $${costUsd.toFixed(dp)}`);
521
+ console.log(`Cache Saved: $${savedUsd.toFixed(dp)} (lifetime $${this.lifetimeSavingsUsd.toFixed(dp)})`);
465
522
  console.log(`Lifetime Cost: $${this.lifetimeCostUsd.toFixed(dp)}`);
466
523
  console.log('--------------------------');
467
- return totalCost;
524
+ return costUsd;
468
525
  }
469
526
  /**
470
527
  * Convert the internal `ChatMessage[]` history into Anthropic's message format.
@@ -475,7 +532,7 @@ export class AnthropicTransport {
475
532
  * Consecutive same-role turns are merged by the API but we merge here to keep
476
533
  * the payload tidy.
477
534
  */
478
- toAnthropicMessages(history, userMessage, attachments) {
535
+ toAnthropicMessages(history, userMessage, attachments, reachableModels = new Set([this.model])) {
479
536
  var _a, _b, _c;
480
537
  const messages = [];
481
538
  const pushBlock = (role, block) => {
@@ -505,6 +562,14 @@ export class AnthropicTransport {
505
562
  continue;
506
563
  }
507
564
  if ((_a = msg.toolCalls) === null || _a === void 0 ? void 0 : _a.length) {
565
+ // Reasoning first, then text, then the calls — the order the model produced them,
566
+ // which is the order the API validates. A tool-use loop is one assistant turn, so
567
+ // resuming it requires the thinking that led to the call to still be present and
568
+ // byte-identical. Replayed from the first call's stashed blocks (see
569
+ // `AnthropicToolCallState`), never rebuilt from the display summary.
570
+ for (const block of this.reasoningToReplay(msg.toolCalls[0], reachableModels)) {
571
+ pushBlock('assistant', block);
572
+ }
508
573
  if (msg.content) {
509
574
  pushBlock('assistant', { type: 'text', text: msg.content });
510
575
  }
@@ -539,23 +604,165 @@ export class AnthropicTransport {
539
604
  }
540
605
  return messages;
541
606
  }
607
+ /**
608
+ * The blocks to replay ahead of a tool call — fallback boundaries then reasoning, or none.
609
+ *
610
+ * A `signature` is only valid for the model that produced it, so reasoning captured under a
611
+ * *different* model is normally dropped: an agent that varies `provider` by state can switch
612
+ * models mid-loop, and replaying the old model's signatures would send blocks the new one
613
+ * cannot verify.
614
+ *
615
+ * Two ways the producer can be the model that will validate:
616
+ *
617
+ * 1. **It is the model we are asking for.** `state.model === this.model` — the ordinary case.
618
+ * 2. **Sticky routing will send this conversation back to it.** After a conversation falls
619
+ * back, later requests carrying `fallbacks` go straight to the model that served, without
620
+ * re-running the one that declined. That is what makes a fallback producer's reasoning
621
+ * replayable at all (Fable 5 configured, Opus 4.8 serving — the pairing this transport's
622
+ * own constructor warning recommends). But it holds only while the conversation continues
623
+ * under the SAME request configuration, which is why `requestedModel` is compared rather
624
+ * than just checking the chain for the producer.
625
+ *
626
+ * That second condition is deliberately narrow. Chain membership alone is too weak: a
627
+ * fallback target is only *contingently* the server, so an agent that switches `provider` to
628
+ * a model whose own chain happens to contain the old producer would replay foreign
629
+ * signatures to whichever model actually answers. Requiring the requested model to be
630
+ * unchanged separates "this conversation is still going" from "we are somewhere else now".
631
+ *
632
+ * State captured before `requestedModel` existed falls back to condition 1 alone, which is
633
+ * the conservative branch — it can drop reasoning, never misdirect it.
634
+ *
635
+ * Boundaries themselves are always echoed: keeping them in place is the documented rule, and
636
+ * with no thinking blocks around them they are inert rather than harmful.
637
+ */
638
+ reasoningToReplay(firstCall, reachableModels) {
639
+ var _a, _b, _c;
640
+ const state = (_a = firstCall.providerMetadata) === null || _a === void 0 ? void 0 : _a[ANTHROPIC_PROVIDER_KEY];
641
+ if (!state)
642
+ return [];
643
+ const boundaries = (_b = state.fallbacks) !== null && _b !== void 0 ? _b : [];
644
+ const stickyToProducer = state.requestedModel === this.model && reachableModels.has(state.model);
645
+ const vouched = state.model === this.model || stickyToProducer;
646
+ return [...boundaries, ...(vouched ? ((_c = state.reasoning) !== null && _c !== void 0 ? _c : []) : [])];
647
+ }
648
+ /**
649
+ * The model that actually served this response.
650
+ *
651
+ * A server-side `fallbacks` chain re-runs a declined request on another model and
652
+ * names it in the response's top-level `model`. Pricing must follow that, not the
653
+ * model we asked for: a Fable 5 request served by Opus 4.8 costed at Fable's
654
+ * $10/$50 instead of $5/$25 doubles that part of the bill.
655
+ *
656
+ * An unrecognised model id is NOT priced at a guessed rate — that is precisely how
657
+ * a consumer ended up billing Haiku at Sonnet rates. It warns and falls back to the
658
+ * configured model, which is at least a figure someone chose.
659
+ */
660
+ servingModel(response) {
661
+ const served = response.model;
662
+ if (!served || served === this.model)
663
+ return this.model;
664
+ if (SUPPORTED_ANTHROPIC_MODEL_IDS.includes(served)) {
665
+ return served;
666
+ }
667
+ // Latched per id: this is reached once per response AND once per fallback-chain attempt, so
668
+ // an unrecognised model would otherwise repeat on every iteration of every turn for the
669
+ // session's life — burying the one message the operator needs at exactly the moment cost
670
+ // reporting has gone wrong. Keyed by id so a second unknown model still gets its own line.
671
+ if (!this.warnedUnknownServingModels.has(served)) {
672
+ this.warnedUnknownServingModels.add(served);
673
+ logger.warn(`AnthropicTransport: response was served by "${served}", which is not a known model — ` +
674
+ `pricing it at the configured "${this.model}" rate instead of guessing. The reported ` +
675
+ `cost for this request may be wrong; add the model to SUPPORTED_ANTHROPIC_MODEL_IDS.`);
676
+ }
677
+ return this.model;
678
+ }
679
+ /**
680
+ * Whether a fallback-chain attempt is a refusal that was **not billed**.
681
+ *
682
+ * Anthropic does not charge for a refusal that arrives before any output: the token counts
683
+ * still appear in `usage`, but they are not on the bill. Pricing them anyway turns the
684
+ * fallback undercount this reducer was written to fix into an overcount — the documented
685
+ * example declines with `input_tokens: 535, output_tokens: 0`, which is real money at
686
+ * Fable 5's prompt rate.
687
+ *
688
+ * A **mid-output** refusal *is* billed for the input and whatever it streamed, so output
689
+ * tokens are the discriminator rather than the refusal itself.
690
+ *
691
+ * Declined attempts appear as `type: 'message'`; the attempt that served the turn is
692
+ * `type: 'fallback_message'`. The serving entry is normally billable — except when every
693
+ * model in the chain declined, where the last entry is both the serving one and a refusal,
694
+ * which `lastAndRefused` covers.
695
+ *
696
+ * Also called for a response with no chain at all, as `isUnbilledRefusal({}, usage, refused)`:
697
+ * a direct request that was declined has no `iterations` array, and the same rule applies to
698
+ * it. That is in fact the common case — `iterations` only appears when `fallbacks` was
699
+ * configured.
700
+ */
701
+ isUnbilledRefusal(attempt, usage, lastAndRefused) {
702
+ var _a;
703
+ if (((_a = usage.output_tokens) !== null && _a !== void 0 ? _a : 0) > 0)
704
+ return false;
705
+ return attempt.type === 'message' || lastAndRefused;
706
+ }
707
+ /** Cost one attempt's usage block at `model`'s rates, logging and banking it. */
708
+ costAttempt(model, usage) {
709
+ var _a, _b, _c, _d, _e, _f;
710
+ const cacheRead = (_a = usage.cache_read_input_tokens) !== null && _a !== void 0 ? _a : 0;
711
+ // 1-hour writes bill at 2× vs 1.25× for 5-min, so read the per-TTL breakdown when present
712
+ // and let the cost path split the buckets. Fall back to the flat total (treated as 5-min)
713
+ // when the breakdown is absent.
714
+ const breakdown = usage.cache_creation;
715
+ const cacheCreation1h = (_b = breakdown === null || breakdown === void 0 ? void 0 : breakdown.ephemeral_1h_input_tokens) !== null && _b !== void 0 ? _b : 0;
716
+ const cacheCreation = (_c = usage.cache_creation_input_tokens) !== null && _c !== void 0 ? _c : (breakdown ? ((_d = breakdown.ephemeral_5m_input_tokens) !== null && _d !== void 0 ? _d : 0) + cacheCreation1h : 0);
717
+ return this.logTokenUsage(model, (_e = usage.input_tokens) !== null && _e !== void 0 ? _e : 0, (_f = usage.output_tokens) !== null && _f !== void 0 ? _f : 0, cacheRead, cacheCreation, cacheCreation1h);
718
+ }
542
719
  fromAnthropicResponse(response) {
543
- var _a, _b, _c, _d, _e, _f, _g, _h;
720
+ var _a, _b, _c, _d, _e, _f, _g;
544
721
  let inputTokens;
545
722
  let outputTokens;
546
723
  let cacheReadTokens;
547
724
  let cacheWriteTokens;
548
725
  let cost;
726
+ const servingModel = this.servingModel(response);
549
727
  if (response.usage) {
550
728
  const uncachedInput = (_a = response.usage.input_tokens) !== null && _a !== void 0 ? _a : 0;
551
729
  const cacheRead = (_b = response.usage.cache_read_input_tokens) !== null && _b !== void 0 ? _b : 0;
552
- // 1-hour writes bill at 2× vs 1.25× for 5-min, so read the per-TTL breakdown when present
553
- // and let the cost path split the buckets. Fall back to the flat total (treated as 5-min)
554
- // when the breakdown is absent.
555
730
  const breakdown = response.usage.cache_creation;
556
731
  const cacheCreation1h = (_c = breakdown === null || breakdown === void 0 ? void 0 : breakdown.ephemeral_1h_input_tokens) !== null && _c !== void 0 ? _c : 0;
557
732
  const cacheCreation = (_d = response.usage.cache_creation_input_tokens) !== null && _d !== void 0 ? _d : (breakdown ? ((_e = breakdown.ephemeral_5m_input_tokens) !== null && _e !== void 0 ? _e : 0) + cacheCreation1h : 0);
558
- cost = this.logTokenUsage(uncachedInput, (_f = response.usage.output_tokens) !== null && _f !== void 0 ? _f : 0, cacheRead, cacheCreation, cacheCreation1h);
733
+ // Top-level `usage` describes only the attempt that produced this message. When a
734
+ // fallback chain ran, the ATTEMPTS BEFORE IT were billed too — a mid-output decline
735
+ // bills for what it streamed — and they may have run on a different model at a
736
+ // different rate. Cost each attempt at its own model and sum; the token fields
737
+ // below still describe the serving attempt, which is what the message's content is.
738
+ const attempts = response.usage.iterations;
739
+ if (attempts === null || attempts === void 0 ? void 0 : attempts.length) {
740
+ const refused = response.stop_reason === 'refusal';
741
+ cost = attempts.reduce((total, attempt, index) => {
742
+ var _a;
743
+ const usage = (_a = attempt.usage) !== null && _a !== void 0 ? _a : attempt;
744
+ if (this.isUnbilledRefusal(attempt, usage, index === attempts.length - 1 && refused)) {
745
+ return total;
746
+ }
747
+ // An omitted `model` means the attempt ran on the model we ASKED for — which on a
748
+ // chain turn is precisely not the one that served it. Defaulting to `servingModel`
749
+ // here would price a declining Fable 5 attempt at Opus 4.8's $5/$25 instead of
750
+ // $10/$50: half its real cost, in the same silent-undercount direction this reducer
751
+ // exists to fix.
752
+ const model = attempt.model ? this.servingModel({ model: attempt.model }) : this.model;
753
+ return total + this.costAttempt(model, usage);
754
+ }, 0);
755
+ }
756
+ else if (this.isUnbilledRefusal({}, response.usage, response.stop_reason === 'refusal')) {
757
+ // A refusal with no chain behind it — the model was called directly, declined before
758
+ // producing anything, and was not charged. The `iterations` branch above only sees
759
+ // this when `fallbacks` was configured; most refusals arrive here instead, so pricing
760
+ // this path is the difference between an occasional phantom charge and a routine one.
761
+ cost = 0;
762
+ }
763
+ else {
764
+ cost = this.costAttempt(servingModel, response.usage);
765
+ }
559
766
  // Report TOTAL prompt size (uncached + cache read + cache write) so `inputTokens` means
560
767
  // "prompt tokens" consistently whether or not caching was active — matching Gemini's
561
768
  // `promptTokenCount`, which already includes cached tokens. Cost (above) reflects the
@@ -571,25 +778,65 @@ export class AnthropicTransport {
571
778
  outputTokens = response.usage.output_tokens;
572
779
  }
573
780
  }
574
- const blocks = (_g = response.content) !== null && _g !== void 0 ? _g : [];
781
+ const blocks = (_f = response.content) !== null && _f !== void 0 ? _f : [];
575
782
  const toolCalls = [];
576
783
  const thoughtParts = [];
577
784
  const textParts = [];
578
- for (const block of blocks) {
785
+ // Raw reasoning blocks, kept verbatim for wire replay. Distinct from `thoughtParts`,
786
+ // which is the readable text for display — that copy is lossy (no signature) and must
787
+ // never be what goes back to the API.
788
+ const reasoningBlocks = [];
789
+ // A server-side fallback splits the turn: everything before the LAST `fallback` block was
790
+ // produced by a model that then declined. Its reasoning and client tool calls must be
791
+ // dropped when echoing the turn — they belong to a model that is no longer answering, and
792
+ // their signatures are not valid for the one that is. Replaying blocks from both sides of
793
+ // the boundary is a rejected request, not a degraded one. Absent a fallback (the normal
794
+ // case) this is -1, so every block counts as post-boundary and nothing changes.
795
+ const fallbackIndex = blocks.map((b) => b.type).lastIndexOf('fallback');
796
+ const fallbackBlocks = blocks.filter((b) => b.type === 'fallback');
797
+ for (const [index, block] of blocks.entries()) {
798
+ const postBoundary = index > fallbackIndex;
579
799
  if (block.type === 'tool_use') {
580
- toolCalls.push({
581
- id: block.id,
582
- name: block.name,
583
- args: (_h = block.input) !== null && _h !== void 0 ? _h : {},
584
- });
800
+ // A pre-boundary client tool call was abandoned with the declined attempt; echoing it
801
+ // would ask the serving model to own a call it never made.
802
+ if (postBoundary) {
803
+ toolCalls.push({
804
+ id: block.id,
805
+ name: block.name,
806
+ args: (_g = block.input) !== null && _g !== void 0 ? _g : {},
807
+ });
808
+ }
585
809
  }
586
810
  else if (block.type === 'thinking') {
811
+ if (postBoundary)
812
+ reasoningBlocks.push(block);
587
813
  thoughtParts.push(block.thinking);
588
814
  }
815
+ else if (block.type === 'redacted_thinking') {
816
+ // No readable text by design — replayable but never displayable.
817
+ if (postBoundary)
818
+ reasoningBlocks.push(block);
819
+ }
589
820
  else if (block.type === 'text') {
590
821
  textParts.push(block.text);
591
822
  }
592
823
  }
824
+ // Attach the turn's reasoning to the FIRST tool call, which is where it is replayed
825
+ // from: the blocks precede the whole `tool_use` group in the assistant message, not
826
+ // each call individually. Stored only when a tool call exists — without one the turn
827
+ // ends and there is nothing to resume, so the blocks have no further wire role. The
828
+ // boundary marker travels with them so it can be re-emitted in position, which is what
829
+ // the API validates the surrounding thinking blocks against.
830
+ if (toolCalls.length > 0 && (reasoningBlocks.length > 0 || fallbackBlocks.length > 0)) {
831
+ const state = {
832
+ reasoning: reasoningBlocks,
833
+ model: servingModel,
834
+ requestedModel: this.model,
835
+ };
836
+ if (fallbackBlocks.length > 0)
837
+ state.fallbacks = fallbackBlocks;
838
+ toolCalls[0].providerMetadata = Object.assign(Object.assign({}, toolCalls[0].providerMetadata), { [ANTHROPIC_PROVIDER_KEY]: state });
839
+ }
593
840
  // A `max_tokens` stop means the response was cut off mid-stream. For a turn
594
841
  // carrying tool calls this is fatal: the final tool call's argument JSON was
595
842
  // truncated before it closed, so its args are incomplete and the call cannot
@@ -617,6 +864,11 @@ export class AnthropicTransport {
617
864
  base.cacheWriteTokens = cacheWriteTokens;
618
865
  if (cost != null)
619
866
  base.cost = cost;
867
+ // The model that actually answered — which is NOT the configured one when a
868
+ // fallback chain served the request. Stamped here rather than left to the driver,
869
+ // which only knows what was asked for; a usage ledger attributing spend to the
870
+ // requested model would misattribute every fallback-served turn.
871
+ base.model = servingModel;
620
872
  // Surface a non-standard stop reason for observability and parity with the
621
873
  // Gemini transport's `responseMeta.finishReason`. A text-only `max_tokens`
622
874
  // stop yields partial-but-legible content, so it is returned (not raised) with