@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.
- package/dist/dts/index.d.ts +4 -2
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/transports/anthropic-transport.d.ts +90 -0
- package/dist/dts/transports/anthropic-transport.d.ts.map +1 -1
- package/dist/dts/transports/gemini-transport.d.ts +16 -0
- package/dist/dts/transports/gemini-transport.d.ts.map +1 -1
- package/dist/dts/types/chat.types.d.ts +93 -2
- package/dist/dts/types/chat.types.d.ts.map +1 -1
- package/dist/dts/types/config.types.d.ts +26 -0
- package/dist/dts/types/config.types.d.ts.map +1 -1
- package/dist/dts/utils/token-cost.d.ts +193 -0
- package/dist/dts/utils/token-cost.d.ts.map +1 -0
- package/dist/esm/index.js +11 -1
- package/dist/esm/transports/anthropic-transport.js +357 -105
- package/dist/esm/transports/gemini-transport.js +84 -81
- package/dist/esm/types/config.types.js +32 -0
- package/dist/esm/utils/token-cost.js +238 -0
- package/dist/foundation-ai.api.json +1127 -73
- package/dist/foundation-ai.d.ts +432 -2
- package/package.json +11 -11
|
@@ -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
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* "
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
*
|
|
121
|
-
*
|
|
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
|
|
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
|
-
|
|
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 ((
|
|
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 ((
|
|
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
|
|
326
|
-
//
|
|
327
|
-
// mechanical turns.
|
|
328
|
-
|
|
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
|
|
333
|
-
//
|
|
334
|
-
|
|
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 ((
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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 (${
|
|
459
|
-
console.log(`Prompt Tokens: ${promptTokens} ($${
|
|
460
|
-
console.log(`Cache Read: ${cacheReadTokens} ($${
|
|
461
|
-
console.log(`Cache Write: ${cacheCreationTokens} ($${
|
|
462
|
-
console.log(`Candidate Tokens: ${candidateTokens} ($${
|
|
463
|
-
console.log(`Total Cost: $${
|
|
464
|
-
console.log(`Cache Saved: $${
|
|
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
|
|
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
|
|
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
|
-
|
|
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 = (
|
|
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
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|