@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_GEMINI_MODEL_IDS, } from '../types';
|
|
3
3
|
import { logger } from '../utils/logger';
|
|
4
4
|
import { scaleTemperature } from '../utils/temperature';
|
|
5
|
+
import { geminiTokenCost } from '../utils/token-cost';
|
|
5
6
|
import { toGeminiSchema } from '../utils/tool-schema';
|
|
6
7
|
import { ResponseTruncatedError } from './anthropic-transport';
|
|
7
8
|
import { VENDOR_LABELS } from './budget-exhausted-error';
|
|
@@ -44,57 +45,6 @@ function assertSupportedGeminiModel(model) {
|
|
|
44
45
|
throw new Error(`GeminiTransport: unsupported model "${model}". Use one of: ${SUPPORTED_GEMINI_MODEL_IDS.join(', ')}.`);
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Prompt size (tokens) at or below which the standard pricing tier applies.
|
|
49
|
-
* Above it, Gemini's long-context tier kicks in. Google applies a single tier
|
|
50
|
-
* to the whole request based on prompt size — it is not a marginal/blended rate.
|
|
51
|
-
*/
|
|
52
|
-
const GEMINI_LONG_CONTEXT_THRESHOLD = 200000;
|
|
53
|
-
/**
|
|
54
|
-
* Cached / context-cache input tokens bill at a flat ~10% of the model's normal input
|
|
55
|
-
* rate — consistent across models (flash $0.30→$0.03, flash-lite $0.10→$0.01,
|
|
56
|
-
* pro $1.25→$0.125). Source: https://ai.google.dev/gemini-api/docs/pricing.
|
|
57
|
-
* (Explicit context caching also has an hourly storage fee; implicit caching — what we
|
|
58
|
-
* rely on — has none, so only this read discount applies.)
|
|
59
|
-
*/
|
|
60
|
-
const GEMINI_CACHED_INPUT_MULTIPLIER = 0.1;
|
|
61
|
-
/**
|
|
62
|
-
* Paid Standard tier rates (USD per million tokens) — https://ai.google.dev/gemini-api/docs/pricing
|
|
63
|
-
*/
|
|
64
|
-
const GEMINI_PRICING = {
|
|
65
|
-
'gemini-2.5-flash-lite': {
|
|
66
|
-
kind: 'flat',
|
|
67
|
-
rate: { promptPerMillion: 0.1, candidatePerMillion: 0.4 },
|
|
68
|
-
},
|
|
69
|
-
'gemini-2.5-flash': { kind: 'flat', rate: { promptPerMillion: 0.3, candidatePerMillion: 2.5 } },
|
|
70
|
-
'gemini-2.5-pro': {
|
|
71
|
-
kind: 'tiered',
|
|
72
|
-
standard: { promptPerMillion: 1.25, candidatePerMillion: 10.0 },
|
|
73
|
-
longContext: { promptPerMillion: 2.5, candidatePerMillion: 15.0 },
|
|
74
|
-
},
|
|
75
|
-
'gemini-3.1-flash-lite': {
|
|
76
|
-
kind: 'flat',
|
|
77
|
-
rate: { promptPerMillion: 0.25, candidatePerMillion: 1.5 },
|
|
78
|
-
},
|
|
79
|
-
'gemini-3.5-flash': { kind: 'flat', rate: { promptPerMillion: 1.5, candidatePerMillion: 9.0 } },
|
|
80
|
-
'gemini-3.1-pro-preview': {
|
|
81
|
-
kind: 'tiered',
|
|
82
|
-
standard: { promptPerMillion: 2.0, candidatePerMillion: 12.0 },
|
|
83
|
-
longContext: { promptPerMillion: 4.0, candidatePerMillion: 18.0 },
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
/**
|
|
87
|
-
* Resolves the per-million-token rates for a model given the request's prompt
|
|
88
|
-
* size, selecting the long-context tier for tiered models when the prompt
|
|
89
|
-
* exceeds {@link GEMINI_LONG_CONTEXT_THRESHOLD}.
|
|
90
|
-
*/
|
|
91
|
-
function estimatedGeminiPaidRatesUsdPerMillion(model, promptTokens) {
|
|
92
|
-
const pricing = GEMINI_PRICING[model];
|
|
93
|
-
if (pricing.kind === 'flat') {
|
|
94
|
-
return pricing.rate;
|
|
95
|
-
}
|
|
96
|
-
return promptTokens > GEMINI_LONG_CONTEXT_THRESHOLD ? pricing.longContext : pricing.standard;
|
|
97
|
-
}
|
|
98
48
|
/**
|
|
99
49
|
* Per-model warnings emitted on construction. flash-lite tiers are the cheap
|
|
100
50
|
* default and warn-free; higher-cost, frontier, and preview models flag their
|
|
@@ -106,6 +56,41 @@ const GEMINI_MODEL_WARNINGS = {
|
|
|
106
56
|
'gemini-3.5-flash': 'GeminiTransport: using gemini-3.5-flash — frontier model, materially higher cost than flash-lite; use for harder reasoning or agent tasks.',
|
|
107
57
|
'gemini-3.1-pro-preview': 'GeminiTransport: using gemini-3.1-pro-preview — PREVIEW model (no stability guarantee, may be withdrawn) with significantly higher, prompt-size-tiered cost; reserve for tasks where flash reliability is insufficient.',
|
|
108
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* Gemini's `thinkingConfig` for a turn, given the model and the caller's
|
|
61
|
+
* {@link ChatThinkingPolicy}.
|
|
62
|
+
*
|
|
63
|
+
* `includeThoughts` is about what is **returned**, not what is billed, and stays on for every
|
|
64
|
+
* posture that thinks — a thinking-only turn would otherwise surface as blank, and the thought
|
|
65
|
+
* summary is what `logTokenUsage` reconciles its thinking-token accounting against.
|
|
66
|
+
*
|
|
67
|
+
* `thinkingBudget` is the posture itself: `0` off, `-1` model-decides. **Omitting it is the third
|
|
68
|
+
* state** and is what an undefined policy sends — the model's own default, exactly the request
|
|
69
|
+
* shape this transport issued before the option existed.
|
|
70
|
+
*
|
|
71
|
+
* Only the flash tiers can actually be turned off. `gemini-2.5-pro` always thinks (its minimum
|
|
72
|
+
* budget is 128; `0` is rejected), which is the same constraint Anthropic's Fable 5 has — so a
|
|
73
|
+
* clamp here is the norm across providers, not a Gemini quirk. The 3.x tiers moved to a
|
|
74
|
+
* `thinkingLevel` enum and do not take a numeric budget on every tier, so they are deliberately
|
|
75
|
+
* left on their default rather than sent a value that may not apply: a clamp costs money, but a
|
|
76
|
+
* guess costs a 400 on a turn the user is waiting for. Wire `thinkingLevel` through when the
|
|
77
|
+
* per-tier support is confirmed.
|
|
78
|
+
*/
|
|
79
|
+
const GEMINI_THINKING_DISABLEABLE = [
|
|
80
|
+
'gemini-2.5-flash',
|
|
81
|
+
'gemini-2.5-flash-lite',
|
|
82
|
+
];
|
|
83
|
+
function geminiThinkingConfig(model, policy) {
|
|
84
|
+
if (policy === 'off' && GEMINI_THINKING_DISABLEABLE.includes(model)) {
|
|
85
|
+
// No reasoning to return once there is none to bill for.
|
|
86
|
+
return { includeThoughts: false, thinkingBudget: 0 };
|
|
87
|
+
}
|
|
88
|
+
if (policy === 'auto' && GEMINI_THINKING_DISABLEABLE.includes(model)) {
|
|
89
|
+
return { includeThoughts: true, thinkingBudget: -1 };
|
|
90
|
+
}
|
|
91
|
+
// Undefined policy, or a model whose posture we cannot safely set: the default.
|
|
92
|
+
return { includeThoughts: true };
|
|
93
|
+
}
|
|
109
94
|
/**
|
|
110
95
|
* Token accepted by the Gemini 3 API in place of a real `thoughtSignature` to
|
|
111
96
|
* skip signature validation. Used when replaying a tool call whose signature
|
|
@@ -144,6 +129,25 @@ export class MalformedFunctionCallError extends Error {
|
|
|
144
129
|
* @beta
|
|
145
130
|
*/
|
|
146
131
|
export class GeminiTransport {
|
|
132
|
+
/**
|
|
133
|
+
* Warn once when a requested policy is silently clamped. Turning thinking off is a *cost*
|
|
134
|
+
* decision, so a caller who asked for it and kept paying for reasoning tokens needs to hear
|
|
135
|
+
* about it — but only once, not per turn.
|
|
136
|
+
*
|
|
137
|
+
* Only `'off'` can actually be denied. `'auto'` is already what an omitted budget produces on
|
|
138
|
+
* every model here — dynamic thinking is the documented default — so warning that it was
|
|
139
|
+
* "ignored" would be false, and latching on it would spend the one warning the genuinely
|
|
140
|
+
* unhonourable `'off'` needs.
|
|
141
|
+
*/
|
|
142
|
+
warnIfThinkingUnclampable(policy) {
|
|
143
|
+
if (policy !== 'off' ||
|
|
144
|
+
GEMINI_THINKING_DISABLEABLE.includes(this.model) ||
|
|
145
|
+
this.warnedThinkingClamped) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.warnedThinkingClamped = true;
|
|
149
|
+
logger.warn(`GeminiTransport: thinkingPolicy 'off' ignored — ${this.model} runs its default thinking posture and takes no budget we can safely set. Reasoning tokens are still billed at the candidate rate; use a flash tier if you need them gone.`);
|
|
150
|
+
}
|
|
147
151
|
constructor(config = {}) {
|
|
148
152
|
var _a, _b, _c;
|
|
149
153
|
/**
|
|
@@ -159,6 +163,11 @@ export class GeminiTransport {
|
|
|
159
163
|
* Surfaced alongside `getLifetimeCost`.
|
|
160
164
|
*/
|
|
161
165
|
this.lifetimeSavingsUsd = 0;
|
|
166
|
+
/**
|
|
167
|
+
* Whether we have already told the caller their `thinkingPolicy` cannot be honoured on this
|
|
168
|
+
* model. Latched, because it would otherwise fire on every turn of a tool loop.
|
|
169
|
+
*/
|
|
170
|
+
this.warnedThinkingClamped = false;
|
|
162
171
|
const model = (_a = config.model) !== null && _a !== void 0 ? _a : DEFAULT_MODEL;
|
|
163
172
|
assertSupportedGeminiModel(model);
|
|
164
173
|
this.model = model;
|
|
@@ -248,12 +257,13 @@ export class GeminiTransport {
|
|
|
248
257
|
// it can confidently parse the call (see `repairMalformedFunctionCall`),
|
|
249
258
|
// falling back to the caller's retry otherwise.
|
|
250
259
|
const toolConfig = tools ? toGeminiToolConfig(options === null || options === void 0 ? void 0 : options.toolChoice) : undefined;
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
260
|
+
// Thinking posture for this turn. By default (no `thinkingPolicy`) this asks only for thought
|
|
261
|
+
// summaries, so the model's official reasoning is returned and a thinking-only turn surfaces
|
|
262
|
+
// *something* rather than going silently blank — see `logTokenUsage` for the thinking-token
|
|
263
|
+
// accounting it lets us capture. A policy adds an explicit `thinkingBudget` where the model
|
|
264
|
+
// supports one; Gemini 2.5 Pro always thinks regardless. See `geminiThinkingConfig`.
|
|
265
|
+
this.warnIfThinkingUnclampable(options === null || options === void 0 ? void 0 : options.thinkingPolicy);
|
|
266
|
+
const generationConfig = { thinkingConfig: geminiThinkingConfig(this.model, options === null || options === void 0 ? void 0 : options.thinkingPolicy) };
|
|
257
267
|
// Normalized [0,1] temperature → Gemini's native range, anchored so 0.5
|
|
258
268
|
// maps to its default (native 1) and 1 to its ceiling (native 2).
|
|
259
269
|
if ((options === null || options === void 0 ? void 0 : options.temperature) != null) {
|
|
@@ -288,34 +298,27 @@ export class GeminiTransport {
|
|
|
288
298
|
* message.
|
|
289
299
|
*/
|
|
290
300
|
logTokenUsage(promptTokens, candidateTokens, thoughtTokens, cachedTokens) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const candidateCost = ((candidateTokens + thoughtTokens) / m) * candidatePerMillion;
|
|
304
|
-
const totalCost = promptCost + cacheReadCost + candidateCost;
|
|
305
|
-
this.lifetimeCostUsd += totalCost;
|
|
306
|
-
// Savings vs no caching: the cached tokens would have cost full input price; we paid ~0.1×.
|
|
307
|
-
// Implicit caching has no write premium, so this is always ≥ 0.
|
|
308
|
-
const saved = (cachedTokens / m) * promptPerMillion - cacheReadCost;
|
|
309
|
-
this.lifetimeSavingsUsd += saved;
|
|
301
|
+
// The arithmetic, the rate table and the long-context tiering rule live in
|
|
302
|
+
// `utils/token-cost` so a host pricing its own requests uses the same numbers
|
|
303
|
+
// instead of a hand-copied table that drifts. This method keeps only what is
|
|
304
|
+
// instance-scoped: the lifetime accumulators and the log.
|
|
305
|
+
const { costUsd, savedUsd, breakdown } = geminiTokenCost(this.model, {
|
|
306
|
+
promptTokens,
|
|
307
|
+
candidateTokens,
|
|
308
|
+
thoughtTokens,
|
|
309
|
+
cachedTokens,
|
|
310
|
+
});
|
|
311
|
+
this.lifetimeCostUsd += costUsd;
|
|
312
|
+
this.lifetimeSavingsUsd += savedUsd;
|
|
310
313
|
const dp = GeminiTransport.COST_DECIMAL_PLACES;
|
|
311
314
|
console.log(`--- Gemini Token Usage (${this.model}) ---`);
|
|
312
|
-
console.log(`Prompt Tokens: ${promptTokens} (${cachedTokens} cached) ($${
|
|
313
|
-
console.log(`Candidate Tokens: ${candidateTokens} (+${thoughtTokens} thinking) ($${
|
|
314
|
-
console.log(`Total Cost: $${
|
|
315
|
-
console.log(`Cache Saved: $${
|
|
315
|
+
console.log(`Prompt Tokens: ${promptTokens} (${cachedTokens} cached) ($${breakdown.promptUsd.toFixed(dp)} + $${breakdown.cacheReadUsd.toFixed(dp)})`);
|
|
316
|
+
console.log(`Candidate Tokens: ${candidateTokens} (+${thoughtTokens} thinking) ($${breakdown.candidateUsd.toFixed(dp)})`);
|
|
317
|
+
console.log(`Total Cost: $${costUsd.toFixed(dp)}`);
|
|
318
|
+
console.log(`Cache Saved: $${savedUsd.toFixed(dp)} (lifetime $${this.lifetimeSavingsUsd.toFixed(dp)})`);
|
|
316
319
|
console.log(`Lifetime Cost: $${this.lifetimeCostUsd.toFixed(dp)}`);
|
|
317
320
|
console.log('--------------------------');
|
|
318
|
-
return
|
|
321
|
+
return costUsd;
|
|
319
322
|
}
|
|
320
323
|
toGeminiContents(history, userMessage, attachments) {
|
|
321
324
|
var _a, _b, _c;
|
|
@@ -16,3 +16,35 @@ export const SUPPORTED_ANTHROPIC_MODEL_IDS = [
|
|
|
16
16
|
'claude-sonnet-4-6',
|
|
17
17
|
'claude-haiku-4-5-20251001',
|
|
18
18
|
];
|
|
19
|
+
/**
|
|
20
|
+
* Which vendor a model id belongs to, or `undefined` when no supported provider claims it.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* Derived from {@link SUPPORTED_ANTHROPIC_MODEL_IDS} and {@link SUPPORTED_GEMINI_MODEL_IDS},
|
|
24
|
+
* so it cannot drift from them the way a hand-maintained map does — adding a model to an
|
|
25
|
+
* allowlist is enough to teach this too.
|
|
26
|
+
*
|
|
27
|
+
* Exists because callers hold a **model id**, not a vendor. A usage ledger reads a message's
|
|
28
|
+
* `model` and then has to pick the matching pricing function — `anthropicTokenCost` vs
|
|
29
|
+
* `geminiTokenCost` — whose usage records are deliberately not interchangeable, because the
|
|
30
|
+
* two providers disagree about whether the prompt figure includes the cached part. Without
|
|
31
|
+
* this, every such caller writes its own model→vendor map, which is the same duplication that
|
|
32
|
+
* exporting the pricing removes.
|
|
33
|
+
*
|
|
34
|
+
* Returns `undefined` rather than falling back to a default vendor so an unrecognised model
|
|
35
|
+
* is a decision the caller has to make out loud. Silently defaulting is precisely how an
|
|
36
|
+
* unlisted model gets priced at some other tier's rates — a consumer has already had Haiku
|
|
37
|
+
* billed at Sonnet rates that way.
|
|
38
|
+
*
|
|
39
|
+
* Takes a plain `string`, not a union: the interesting callers are reading a model id back
|
|
40
|
+
* off a stored message or a config file, where it is untrusted text.
|
|
41
|
+
*
|
|
42
|
+
* @beta
|
|
43
|
+
*/
|
|
44
|
+
export function vendorOfModel(modelId) {
|
|
45
|
+
if (SUPPORTED_ANTHROPIC_MODEL_IDS.includes(modelId))
|
|
46
|
+
return 'anthropic';
|
|
47
|
+
if (SUPPORTED_GEMINI_MODEL_IDS.includes(modelId))
|
|
48
|
+
return 'gemini';
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request cost pricing for the models this package can call.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Extracted from the two transports' private cost paths so a host that prices
|
|
6
|
+
* requests of its own — a server proxy, a usage ledger, a benchmark — computes the
|
|
7
|
+
* same numbers instead of hand-copying the rate tables. Duplicated tables drift:
|
|
8
|
+
* an unlisted model silently prices at whatever the copy's fallback is, and a
|
|
9
|
+
* missing multiplier misprices every cached request.
|
|
10
|
+
*
|
|
11
|
+
* **Typed over raw provider usage, deliberately.** These take the token counts a
|
|
12
|
+
* provider response reports, NOT `ChatMessage`/`AggregateUsage`. Two reasons:
|
|
13
|
+
*
|
|
14
|
+
* 1. `ChatMessage.cacheWriteTokens` is a single total, but Anthropic bills cache
|
|
15
|
+
* writes per TTL (5-minute at 1.25×, 1-hour at 2×). The split exists only in
|
|
16
|
+
* the provider response, so pricing from a message cannot be exact.
|
|
17
|
+
* 2. The path that actually needs to compute a cost is the one where no transport
|
|
18
|
+
* stamped one — a proxied or server-side call holding a raw usage block.
|
|
19
|
+
*
|
|
20
|
+
* When a `ChatMessage` already carries `cost`, use that; it was computed here at
|
|
21
|
+
* request time with information the message no longer has. Re-deriving a cost
|
|
22
|
+
* from a message's token fields is wrong in the expensive direction, because
|
|
23
|
+
* `inputTokens` is total prompt size *including* the cached part.
|
|
24
|
+
*
|
|
25
|
+
* **What this model cannot express, by construction.** Everything here is
|
|
26
|
+
* *per-request*: a function of the tokens one call consumed. Caching priced by
|
|
27
|
+
* **elapsed time** does not fit, and no additional bucket would make it fit —
|
|
28
|
+
* Gemini's explicit context caching, for instance, charges an hourly storage fee
|
|
29
|
+
* that accrues while you are making no requests at all. Nothing to attribute it to.
|
|
30
|
+
*
|
|
31
|
+
* A provider whose caching is billed that way needs a **separate ledger entry**,
|
|
32
|
+
* recorded against a cache's lifetime rather than against a request, and summed
|
|
33
|
+
* alongside these figures rather than inside them. Do not fold it into
|
|
34
|
+
* {@link TokenCostBreakdown} — a storage fee divided across whatever requests
|
|
35
|
+
* happened to occur is an invented number, and it would make two runs with
|
|
36
|
+
* identical token usage report different costs for reasons neither run caused.
|
|
37
|
+
*
|
|
38
|
+
* The vendors also differ in shape more than they differ in rates, which is why
|
|
39
|
+
* there is a function per provider rather than one with a normalised record:
|
|
40
|
+
* Anthropic charges a premium for the cache *write* and lets you choose the TTL;
|
|
41
|
+
* Gemini's implicit caching has no write charge at all; a provider with automatic,
|
|
42
|
+
* non-configurable caching has neither a write bucket nor a TTL to record. The
|
|
43
|
+
* shared vocabulary is the RESULT ({@link TokenCost}), not the input.
|
|
44
|
+
*/
|
|
45
|
+
/** One million — the unit every published rate is quoted in. */
|
|
46
|
+
const TOKENS_PER_MILLION = 1000000;
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Anthropic
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
/**
|
|
51
|
+
* Standard tier pricing per million tokens —
|
|
52
|
+
* https://docs.claude.com/en/docs/about-claude/pricing
|
|
53
|
+
*
|
|
54
|
+
* @beta
|
|
55
|
+
*/
|
|
56
|
+
const ANTHROPIC_PRICING = {
|
|
57
|
+
'claude-haiku-4-5-20251001': { promptPerMillion: 1, candidatePerMillion: 5 },
|
|
58
|
+
// Fable 5 — Anthropic's most capable widely-released model; priced above Opus tier.
|
|
59
|
+
'claude-fable-5': { promptPerMillion: 10, candidatePerMillion: 50 },
|
|
60
|
+
// Opus 4.7 / 4.8 — same $5 / $25 per MTok. Stated per model rather than shared through a
|
|
61
|
+
// fall-through, so neither is ever "whatever was left over".
|
|
62
|
+
'claude-opus-4-8': { promptPerMillion: 5, candidatePerMillion: 25 },
|
|
63
|
+
'claude-opus-4-7': { promptPerMillion: 5, candidatePerMillion: 25 },
|
|
64
|
+
// Sonnet 5 is $2 / $10 — its OWN tier, and CHEAPER than the older Sonnet 4.6 below.
|
|
65
|
+
//
|
|
66
|
+
// This launched as an introductory rate through 2026-08-31, and an earlier version of this
|
|
67
|
+
// table deliberately charged the $3 / $15 standard rate instead, on the reasoning that the
|
|
68
|
+
// introductory period would lapse. Anthropic has since confirmed $2 / $10 as the permanent
|
|
69
|
+
// standard price and cancelled the scheduled increase, so that reasoning is dead and the old
|
|
70
|
+
// figure over-charged every Sonnet 5 request by 50%.
|
|
71
|
+
// https://platform.claude.com/docs/en/about-claude/pricing#model-pricing
|
|
72
|
+
'claude-sonnet-5': { promptPerMillion: 2, candidatePerMillion: 10 },
|
|
73
|
+
// Sonnet 4.6 remains on the older $3 / $15 Sonnet tier — the newer model is the cheaper one.
|
|
74
|
+
'claude-sonnet-4-6': { promptPerMillion: 3, candidatePerMillion: 15 },
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Standard tier pricing per million tokens —
|
|
78
|
+
* https://docs.claude.com/en/docs/about-claude/pricing
|
|
79
|
+
*
|
|
80
|
+
* @remarks
|
|
81
|
+
* Backed by a total {@link https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type | Record}
|
|
82
|
+
* over `AnthropicModelId` rather than a chain of checks with a default. That is the whole
|
|
83
|
+
* point: adding a model to `SUPPORTED_ANTHROPIC_MODEL_IDS` without pricing it becomes a
|
|
84
|
+
* COMPILE error instead of a silent charge at whatever the fall-through happened to be — the
|
|
85
|
+
* exact failure this module's header warns about, and one a consumer has already been bitten
|
|
86
|
+
* by. The Gemini half of this file has always had the guarantee; this half now matches.
|
|
87
|
+
*
|
|
88
|
+
* @beta
|
|
89
|
+
*/
|
|
90
|
+
export function anthropicRatesFor(model) {
|
|
91
|
+
return ANTHROPIC_PRICING[model];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Prompt-cache pricing multipliers, applied to the model's base input rate
|
|
95
|
+
* (`promptPerMillion`) — https://docs.claude.com/en/docs/build-with-claude/prompt-caching
|
|
96
|
+
* Reads bill at ~0.1× base input; writes bill by TTL — 5-minute at ~1.25× and 1-hour at 2×.
|
|
97
|
+
* Both write TTLs are reachable (`CachePolicy.ttl` is `'5m' | '1h'`), so each TTL bucket is
|
|
98
|
+
* costed from the response's per-TTL `cache_creation` breakdown rather than assuming one rate.
|
|
99
|
+
*
|
|
100
|
+
* @beta
|
|
101
|
+
*/
|
|
102
|
+
export const ANTHROPIC_CACHE_READ_MULTIPLIER = 0.1;
|
|
103
|
+
/** @beta */
|
|
104
|
+
export const ANTHROPIC_CACHE_WRITE_5M_MULTIPLIER = 1.25;
|
|
105
|
+
/** @beta */
|
|
106
|
+
export const ANTHROPIC_CACHE_WRITE_1H_MULTIPLIER = 2;
|
|
107
|
+
/**
|
|
108
|
+
* Cost one Anthropic request.
|
|
109
|
+
*
|
|
110
|
+
* @beta
|
|
111
|
+
*/
|
|
112
|
+
export function anthropicTokenCost(model, usage) {
|
|
113
|
+
const { promptPerMillion, candidatePerMillion } = anthropicRatesFor(model);
|
|
114
|
+
const m = TOKENS_PER_MILLION;
|
|
115
|
+
// Split the creation total into its 1-hour portion and the 5-minute remainder and
|
|
116
|
+
// cost each at its own rate. All buckets are multiples of the same base input rate,
|
|
117
|
+
// so summing them stays correct whether or not caching was active (every cache
|
|
118
|
+
// field is 0 when it wasn't).
|
|
119
|
+
const cacheWrite5mTokens = Math.max(0, usage.cacheWriteTokens - usage.cacheWrite1hTokens);
|
|
120
|
+
const promptCost = (usage.uncachedInputTokens / m) * promptPerMillion;
|
|
121
|
+
const cacheReadCost = (usage.cacheReadTokens / m) * promptPerMillion * ANTHROPIC_CACHE_READ_MULTIPLIER;
|
|
122
|
+
const cacheWriteCost = (cacheWrite5mTokens / m) * promptPerMillion * ANTHROPIC_CACHE_WRITE_5M_MULTIPLIER +
|
|
123
|
+
(usage.cacheWrite1hTokens / m) * promptPerMillion * ANTHROPIC_CACHE_WRITE_1H_MULTIPLIER;
|
|
124
|
+
const candidateCost = (usage.outputTokens / m) * candidatePerMillion;
|
|
125
|
+
// Net saving vs no caching: the cache-read tokens would have cost full input price
|
|
126
|
+
// (we paid ~0.1× of that), while the write premium is an upfront cost that pays back
|
|
127
|
+
// on later reads — so the net dips negative on a write-heavy request and climbs
|
|
128
|
+
// positive as reads accrue.
|
|
129
|
+
const cacheReadFull = (usage.cacheReadTokens / m) * promptPerMillion;
|
|
130
|
+
const cacheWriteFull = (usage.cacheWriteTokens / m) * promptPerMillion;
|
|
131
|
+
return {
|
|
132
|
+
costUsd: promptCost + cacheReadCost + cacheWriteCost + candidateCost,
|
|
133
|
+
savedUsd: cacheReadFull - cacheReadCost + (cacheWriteFull - cacheWriteCost),
|
|
134
|
+
breakdown: {
|
|
135
|
+
promptUsd: promptCost,
|
|
136
|
+
cacheReadUsd: cacheReadCost,
|
|
137
|
+
cacheWriteUsd: cacheWriteCost,
|
|
138
|
+
candidateUsd: candidateCost,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Gemini
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
/**
|
|
146
|
+
* Prompt size (tokens) at or below which the standard pricing tier applies.
|
|
147
|
+
* Above it, Gemini's long-context tier kicks in. Google applies a single tier
|
|
148
|
+
* to the whole request based on prompt size — it is not a marginal/blended rate.
|
|
149
|
+
*
|
|
150
|
+
* @beta
|
|
151
|
+
*/
|
|
152
|
+
export const GEMINI_LONG_CONTEXT_THRESHOLD = 200000;
|
|
153
|
+
/**
|
|
154
|
+
* Cached / context-cache input tokens bill at a flat ~10% of the model's normal input
|
|
155
|
+
* rate — consistent across models (flash $0.30→$0.03, flash-lite $0.10→$0.01,
|
|
156
|
+
* pro $1.25→$0.125). Source: https://ai.google.dev/gemini-api/docs/pricing.
|
|
157
|
+
* (Explicit context caching also has an hourly storage fee; implicit caching — what the
|
|
158
|
+
* transport relies on — has none, so only this read discount applies. There is no write
|
|
159
|
+
* premium on this provider, which is why its savings figure is never negative.)
|
|
160
|
+
*
|
|
161
|
+
* @beta
|
|
162
|
+
*/
|
|
163
|
+
export const GEMINI_CACHED_INPUT_MULTIPLIER = 0.1;
|
|
164
|
+
/**
|
|
165
|
+
* Paid Standard tier rates (USD per million tokens) —
|
|
166
|
+
* https://ai.google.dev/gemini-api/docs/pricing
|
|
167
|
+
*/
|
|
168
|
+
const GEMINI_PRICING = {
|
|
169
|
+
'gemini-2.5-flash-lite': {
|
|
170
|
+
kind: 'flat',
|
|
171
|
+
rate: { promptPerMillion: 0.1, candidatePerMillion: 0.4 },
|
|
172
|
+
},
|
|
173
|
+
'gemini-2.5-flash': { kind: 'flat', rate: { promptPerMillion: 0.3, candidatePerMillion: 2.5 } },
|
|
174
|
+
'gemini-2.5-pro': {
|
|
175
|
+
kind: 'tiered',
|
|
176
|
+
standard: { promptPerMillion: 1.25, candidatePerMillion: 10.0 },
|
|
177
|
+
longContext: { promptPerMillion: 2.5, candidatePerMillion: 15.0 },
|
|
178
|
+
},
|
|
179
|
+
'gemini-3.1-flash-lite': {
|
|
180
|
+
kind: 'flat',
|
|
181
|
+
rate: { promptPerMillion: 0.25, candidatePerMillion: 1.5 },
|
|
182
|
+
},
|
|
183
|
+
'gemini-3.5-flash': { kind: 'flat', rate: { promptPerMillion: 1.5, candidatePerMillion: 9.0 } },
|
|
184
|
+
'gemini-3.1-pro-preview': {
|
|
185
|
+
kind: 'tiered',
|
|
186
|
+
standard: { promptPerMillion: 2.0, candidatePerMillion: 12.0 },
|
|
187
|
+
longContext: { promptPerMillion: 4.0, candidatePerMillion: 18.0 },
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Resolve the per-million-token rates for a model given the request's prompt size,
|
|
192
|
+
* selecting the long-context tier for tiered models when the prompt exceeds
|
|
193
|
+
* {@link GEMINI_LONG_CONTEXT_THRESHOLD}.
|
|
194
|
+
*
|
|
195
|
+
* @remarks
|
|
196
|
+
* `promptTokens` must be the **full** prompt size, cached slice included — the tier
|
|
197
|
+
* is chosen on what was sent, not on what was billed at full rate. A mostly-cached
|
|
198
|
+
* long prompt is still a long prompt.
|
|
199
|
+
*
|
|
200
|
+
* Unlike Anthropic's, this accessor is not a pure function of the model, which is
|
|
201
|
+
* why there is no single `ratesFor(model)` across both providers.
|
|
202
|
+
*
|
|
203
|
+
* @beta
|
|
204
|
+
*/
|
|
205
|
+
export function geminiRatesFor(model, promptTokens) {
|
|
206
|
+
const pricing = GEMINI_PRICING[model];
|
|
207
|
+
if (pricing.kind === 'flat') {
|
|
208
|
+
return pricing.rate;
|
|
209
|
+
}
|
|
210
|
+
return promptTokens > GEMINI_LONG_CONTEXT_THRESHOLD ? pricing.longContext : pricing.standard;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Cost one Gemini request.
|
|
214
|
+
*
|
|
215
|
+
* @beta
|
|
216
|
+
*/
|
|
217
|
+
export function geminiTokenCost(model, usage) {
|
|
218
|
+
// The pricing TIER is chosen on the full prompt size, BEFORE the cache split.
|
|
219
|
+
const { promptPerMillion, candidatePerMillion } = geminiRatesFor(model, usage.promptTokens);
|
|
220
|
+
const m = TOKENS_PER_MILLION;
|
|
221
|
+
const uncachedPromptTokens = Math.max(0, usage.promptTokens - usage.cachedTokens);
|
|
222
|
+
const promptCost = (uncachedPromptTokens / m) * promptPerMillion;
|
|
223
|
+
const cacheReadCost = (usage.cachedTokens / m) * promptPerMillion * GEMINI_CACHED_INPUT_MULTIPLIER;
|
|
224
|
+
// Thinking tokens bill at the output rate, and are incurred whenever the model
|
|
225
|
+
// thinks (always, on 2.5 Pro).
|
|
226
|
+
const candidateCost = ((usage.candidateTokens + usage.thoughtTokens) / m) * candidatePerMillion;
|
|
227
|
+
return {
|
|
228
|
+
costUsd: promptCost + cacheReadCost + candidateCost,
|
|
229
|
+
// Implicit caching has no write premium, so this is always >= 0.
|
|
230
|
+
savedUsd: (usage.cachedTokens / m) * promptPerMillion - cacheReadCost,
|
|
231
|
+
breakdown: {
|
|
232
|
+
promptUsd: promptCost,
|
|
233
|
+
cacheReadUsd: cacheReadCost,
|
|
234
|
+
cacheWriteUsd: 0,
|
|
235
|
+
candidateUsd: candidateCost,
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|