@graphorin/pricing 0.6.1 → 0.7.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/CHANGELOG.md +21 -0
- package/README.md +1 -1
- package/dist/convert-genai-prices.d.ts +28 -0
- package/dist/convert-genai-prices.d.ts.map +1 -0
- package/dist/convert-genai-prices.js +90 -0
- package/dist/convert-genai-prices.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lookup.d.ts +5 -0
- package/dist/lookup.d.ts.map +1 -1
- package/dist/lookup.js +5 -0
- package/dist/lookup.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/dist/refresh.d.ts +7 -0
- package/dist/refresh.d.ts.map +1 -1
- package/dist/refresh.js +35 -3
- package/dist/refresh.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -5
- package/src/config.ts +39 -0
- package/src/convert-genai-prices.ts +164 -0
- package/src/diff.ts +73 -0
- package/src/index.ts +61 -0
- package/src/lookup.ts +164 -0
- package/src/missing.ts +73 -0
- package/src/refresh.ts +197 -0
- package/src/snapshot/bundled.ts +306 -0
- package/src/snapshot/index.ts +7 -0
- package/src/types.ts +122 -0
package/src/refresh.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `refreshPricing(...)` - opt-in network call. The framework never
|
|
3
|
+
* invokes this automatically; it exists so the CLI (`graphorin
|
|
4
|
+
* pricing refresh`) and operator scripts can pull a fresh snapshot
|
|
5
|
+
* without re-installing the package.
|
|
6
|
+
*
|
|
7
|
+
* The function fetches the supplied URL as JSON and validates the
|
|
8
|
+
* shape against the {@link PricingSnapshot} contract. The bundled
|
|
9
|
+
* snapshot is **not** mutated; callers persist the refreshed
|
|
10
|
+
* snapshot to disk and reload it explicitly.
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { convertGenaiPrices, isGenaiPricesShape } from './convert-genai-prices.js';
|
|
16
|
+
import { computeEntriesDigest } from './snapshot/bundled.js';
|
|
17
|
+
import type { ModelPrice, PricingSnapshot } from './types.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Configuration shape for {@link refreshPricing}. The `fetchImpl`
|
|
21
|
+
* override exists so tests can exercise the function without making
|
|
22
|
+
* a real network call.
|
|
23
|
+
*
|
|
24
|
+
* @stable
|
|
25
|
+
*/
|
|
26
|
+
export interface RefreshPricingOptions {
|
|
27
|
+
/** Snapshot URL - typically the upstream pricing JSON. */
|
|
28
|
+
readonly url: string;
|
|
29
|
+
/** Optional headers (auth, conditional GET, etc.). */
|
|
30
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
31
|
+
/** Optional fetch override - useful in tests. */
|
|
32
|
+
readonly fetchImpl?: typeof fetch;
|
|
33
|
+
/** Override the snapshot date stamped on the result. Defaults to today. */
|
|
34
|
+
readonly snapshotDate?: string;
|
|
35
|
+
/** Override the snapshot version string. Defaults to `'graphorin/0.1+refreshed'`. */
|
|
36
|
+
readonly version?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Hard timeout for the network fetch in milliseconds. Default
|
|
39
|
+
* `30000`. Aborts the request (and throws) if the upstream is slow
|
|
40
|
+
* or unreachable so `graphorin pricing refresh` cannot hang. Pass an
|
|
41
|
+
* explicit {@link RefreshPricingOptions.signal} to manage
|
|
42
|
+
* cancellation yourself; the two are combined.
|
|
43
|
+
*/
|
|
44
|
+
readonly timeoutMs?: number;
|
|
45
|
+
/** Caller-supplied abort signal, combined with the timeout. */
|
|
46
|
+
readonly signal?: AbortSignal;
|
|
47
|
+
/**
|
|
48
|
+
* W-097: accepted body format. `'auto'` (default) tries the native
|
|
49
|
+
* graphorin shape, then auto-detects + converts the
|
|
50
|
+
* `@pydantic/genai-prices` dataset; the explicit values pin one
|
|
51
|
+
* format and fail fast on anything else.
|
|
52
|
+
*/
|
|
53
|
+
readonly format?: 'auto' | 'graphorin' | 'genai-prices';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Pull a fresh snapshot from the supplied URL and return it. Network
|
|
58
|
+
* failures and shape mismatches surface as thrown errors so the CLI
|
|
59
|
+
* can surface them to the operator.
|
|
60
|
+
*
|
|
61
|
+
* @stable
|
|
62
|
+
*/
|
|
63
|
+
export async function refreshPricing(opts: RefreshPricingOptions): Promise<PricingSnapshot> {
|
|
64
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
65
|
+
if (typeof fetchImpl !== 'function') {
|
|
66
|
+
throw new TypeError(
|
|
67
|
+
'refreshPricing: no fetch implementation available. Provide `fetchImpl` ' +
|
|
68
|
+
'explicitly or run on a Node.js version with global fetch (>= 18).',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const timeoutMs = opts.timeoutMs ?? 30_000;
|
|
72
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
73
|
+
const signal =
|
|
74
|
+
opts.signal === undefined ? timeoutSignal : AbortSignal.any([opts.signal, timeoutSignal]);
|
|
75
|
+
let res: Awaited<ReturnType<typeof fetchImpl>>;
|
|
76
|
+
try {
|
|
77
|
+
res = await fetchImpl(opts.url, { headers: opts.headers ?? {}, signal });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (timeoutSignal.aborted) {
|
|
80
|
+
throw new Error(`refreshPricing: timed out after ${timeoutMs} ms fetching ${opts.url}`, {
|
|
81
|
+
cause: err,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
throw new Error(`refreshPricing: HTTP ${res.status} ${res.statusText} from ${opts.url}`);
|
|
88
|
+
}
|
|
89
|
+
const body = (await res.json()) as unknown;
|
|
90
|
+
const format = opts.format ?? 'auto';
|
|
91
|
+
let entries: ReadonlyArray<ModelPrice>;
|
|
92
|
+
let conversion: PricingSnapshot['conversion'];
|
|
93
|
+
if (format === 'graphorin') {
|
|
94
|
+
entries = parseEntries(body);
|
|
95
|
+
} else if (format === 'genai-prices') {
|
|
96
|
+
({ entries, conversion } = convertForeign(body, opts.url));
|
|
97
|
+
} else {
|
|
98
|
+
// 'auto': native first; a shape miss falls through to detection so
|
|
99
|
+
// the operator error names the NATIVE expectation when neither fits.
|
|
100
|
+
try {
|
|
101
|
+
entries = parseEntries(body);
|
|
102
|
+
} catch (nativeError) {
|
|
103
|
+
if (!isGenaiPricesShape(body)) throw nativeError;
|
|
104
|
+
({ entries, conversion } = convertForeign(body, opts.url));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const snapshotDate = opts.snapshotDate ?? new Date().toISOString().slice(0, 10);
|
|
108
|
+
const version =
|
|
109
|
+
opts.version ??
|
|
110
|
+
(conversion !== undefined ? 'genai-prices+converted' : 'graphorin/0.1+refreshed');
|
|
111
|
+
return Object.freeze<PricingSnapshot>({
|
|
112
|
+
version,
|
|
113
|
+
source: opts.url,
|
|
114
|
+
snapshotDate,
|
|
115
|
+
currency: 'USD',
|
|
116
|
+
sha256: computeEntriesDigest(entries),
|
|
117
|
+
entries,
|
|
118
|
+
...(conversion !== undefined ? { conversion } : {}),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* W-097: convert a genai-prices dataset body, guarding the currency -
|
|
124
|
+
* `PricingSnapshot.currency` is a `'USD'` literal, so a body that
|
|
125
|
+
* DECLARES another currency must fail loudly instead of being stamped
|
|
126
|
+
* dollars.
|
|
127
|
+
*/
|
|
128
|
+
function convertForeign(
|
|
129
|
+
body: unknown,
|
|
130
|
+
url: string,
|
|
131
|
+
): { entries: ReadonlyArray<ModelPrice>; conversion: NonNullable<PricingSnapshot['conversion']> } {
|
|
132
|
+
const declared = (body as { currency?: unknown }).currency;
|
|
133
|
+
if (typeof declared === 'string' && declared.toUpperCase() !== 'USD') {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`refreshPricing: dataset at ${url} declares currency '${declared}' - graphorin snapshots are USD-only and no conversion rate is available`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const { entries, skipped } = convertGenaiPrices(body);
|
|
139
|
+
if (entries.length === 0) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`refreshPricing: the genai-prices dataset at ${url} converted to zero usable entries (${skipped} skipped) - the supported subset may have drifted; see convert-genai-prices.ts`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return { entries, conversion: { format: 'genai-prices', skipped } };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseEntries(body: unknown): ReadonlyArray<ModelPrice> {
|
|
148
|
+
if (body === null || typeof body !== 'object') {
|
|
149
|
+
throw new Error('refreshPricing: response body is not an object');
|
|
150
|
+
}
|
|
151
|
+
// Two accepted shapes:
|
|
152
|
+
// - `{ entries: ModelPrice[] }`
|
|
153
|
+
// - `ModelPrice[]`
|
|
154
|
+
let raw: unknown;
|
|
155
|
+
if (Array.isArray(body)) {
|
|
156
|
+
raw = body;
|
|
157
|
+
} else {
|
|
158
|
+
raw = (body as { entries?: unknown }).entries;
|
|
159
|
+
if (!Array.isArray(raw)) {
|
|
160
|
+
throw new Error('refreshPricing: response object is missing the `entries` array');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const entries = (raw as ReadonlyArray<unknown>).map(parseEntry);
|
|
164
|
+
return Object.freeze(entries);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseEntry(raw: unknown): ModelPrice {
|
|
168
|
+
if (raw === null || typeof raw !== 'object') {
|
|
169
|
+
throw new Error('refreshPricing: pricing entry is not an object');
|
|
170
|
+
}
|
|
171
|
+
const e = raw as Partial<ModelPrice>;
|
|
172
|
+
if (typeof e.provider !== 'string' || typeof e.model !== 'string') {
|
|
173
|
+
throw new Error('refreshPricing: pricing entry is missing provider / model');
|
|
174
|
+
}
|
|
175
|
+
if (typeof e.inputUsdPerToken !== 'number' || typeof e.outputUsdPerToken !== 'number') {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`refreshPricing: pricing entry ${e.provider}/${e.model} is missing input / output prices`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return Object.freeze({
|
|
181
|
+
provider: e.provider,
|
|
182
|
+
model: e.model,
|
|
183
|
+
inputUsdPerToken: e.inputUsdPerToken,
|
|
184
|
+
outputUsdPerToken: e.outputUsdPerToken,
|
|
185
|
+
...(typeof e.cachedReadUsdPerToken === 'number'
|
|
186
|
+
? { cachedReadUsdPerToken: e.cachedReadUsdPerToken }
|
|
187
|
+
: {}),
|
|
188
|
+
...(typeof e.cacheWriteUsdPerToken === 'number'
|
|
189
|
+
? { cacheWriteUsdPerToken: e.cacheWriteUsdPerToken }
|
|
190
|
+
: {}),
|
|
191
|
+
...(typeof e.reasoningUsdPerToken === 'number'
|
|
192
|
+
? { reasoningUsdPerToken: e.reasoningUsdPerToken }
|
|
193
|
+
: {}),
|
|
194
|
+
...(typeof e.region === 'string' ? { region: e.region } : {}),
|
|
195
|
+
...(typeof e.notes === 'string' ? { notes: e.notes } : {}),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled pricing snapshot. The catalogue is intentionally small -
|
|
3
|
+
* operators wanting the full upstream catalogue should run the opt-in
|
|
4
|
+
* `refreshPricing(...)` flow at build time and persist the result to
|
|
5
|
+
* a custom snapshot file.
|
|
6
|
+
*
|
|
7
|
+
* Prices are stored in **USD per token** (i.e. divide the per-million
|
|
8
|
+
* upstream prices by 1_000_000). Numbers are accurate to the snapshot
|
|
9
|
+
* date; consumers are expected to refresh on a regular cadence.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
|
|
16
|
+
import type { ModelPrice, PricingSnapshot } from '../types.js';
|
|
17
|
+
|
|
18
|
+
/** @internal - used for `lookupPrice` defaults. */
|
|
19
|
+
export const SNAPSHOT_DATE = '2026-07-04';
|
|
20
|
+
|
|
21
|
+
const ENTRIES: ReadonlyArray<ModelPrice> = Object.freeze([
|
|
22
|
+
// -----------------------------------------------------------------
|
|
23
|
+
// Anthropic - current families (core-provider-03). Dateless alias ids;
|
|
24
|
+
// dated ids (`claude-haiku-4-5-20251001`) resolve through the lookup's
|
|
25
|
+
// date-suffix fallback. Cache-write = 1.25x input (5-minute cache).
|
|
26
|
+
// NOTE: the Claude 5 family (fable/mythos) and any tier released after
|
|
27
|
+
// this snapshot date have NO entry on purpose - cost tracking reports
|
|
28
|
+
// null + one WARN instead of an invented number. Refresh via
|
|
29
|
+
// `refreshPricing(...)` or contribute the entry when pricing is public.
|
|
30
|
+
// -----------------------------------------------------------------
|
|
31
|
+
{
|
|
32
|
+
provider: 'anthropic',
|
|
33
|
+
model: 'claude-opus-4-5',
|
|
34
|
+
inputUsdPerToken: 5 / 1_000_000,
|
|
35
|
+
outputUsdPerToken: 25 / 1_000_000,
|
|
36
|
+
cachedReadUsdPerToken: 0.5 / 1_000_000,
|
|
37
|
+
cacheWriteUsdPerToken: 6.25 / 1_000_000,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
provider: 'anthropic',
|
|
41
|
+
model: 'claude-opus-4-1',
|
|
42
|
+
inputUsdPerToken: 15 / 1_000_000,
|
|
43
|
+
outputUsdPerToken: 75 / 1_000_000,
|
|
44
|
+
cachedReadUsdPerToken: 1.5 / 1_000_000,
|
|
45
|
+
cacheWriteUsdPerToken: 18.75 / 1_000_000,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
provider: 'anthropic',
|
|
49
|
+
model: 'claude-sonnet-4-5',
|
|
50
|
+
inputUsdPerToken: 3 / 1_000_000,
|
|
51
|
+
outputUsdPerToken: 15 / 1_000_000,
|
|
52
|
+
cachedReadUsdPerToken: 0.3 / 1_000_000,
|
|
53
|
+
cacheWriteUsdPerToken: 3.75 / 1_000_000,
|
|
54
|
+
notes: 'Prompts <= 200k tokens; the long-context tier is priced higher.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
provider: 'anthropic',
|
|
58
|
+
model: 'claude-haiku-4-5',
|
|
59
|
+
inputUsdPerToken: 1 / 1_000_000,
|
|
60
|
+
outputUsdPerToken: 5 / 1_000_000,
|
|
61
|
+
cachedReadUsdPerToken: 0.1 / 1_000_000,
|
|
62
|
+
cacheWriteUsdPerToken: 1.25 / 1_000_000,
|
|
63
|
+
},
|
|
64
|
+
// Retired / legacy ids retained for historical cost attribution.
|
|
65
|
+
{
|
|
66
|
+
provider: 'anthropic',
|
|
67
|
+
model: 'claude-3-5-sonnet-20241022',
|
|
68
|
+
inputUsdPerToken: 3 / 1_000_000,
|
|
69
|
+
outputUsdPerToken: 15 / 1_000_000,
|
|
70
|
+
cachedReadUsdPerToken: 0.3 / 1_000_000,
|
|
71
|
+
cacheWriteUsdPerToken: 3.75 / 1_000_000,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
provider: 'anthropic',
|
|
75
|
+
model: 'claude-3-5-haiku-20241022',
|
|
76
|
+
inputUsdPerToken: 0.8 / 1_000_000,
|
|
77
|
+
outputUsdPerToken: 4 / 1_000_000,
|
|
78
|
+
cachedReadUsdPerToken: 0.08 / 1_000_000,
|
|
79
|
+
cacheWriteUsdPerToken: 1 / 1_000_000,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
provider: 'anthropic',
|
|
83
|
+
model: 'claude-3-opus-20240229',
|
|
84
|
+
inputUsdPerToken: 15 / 1_000_000,
|
|
85
|
+
outputUsdPerToken: 75 / 1_000_000,
|
|
86
|
+
cachedReadUsdPerToken: 1.5 / 1_000_000,
|
|
87
|
+
cacheWriteUsdPerToken: 18.75 / 1_000_000,
|
|
88
|
+
},
|
|
89
|
+
// -----------------------------------------------------------------
|
|
90
|
+
// OpenAI - cache reads are automatic (no breakpoints) and there is no
|
|
91
|
+
// cache-write charge, so entries carry only `cachedReadUsdPerToken`.
|
|
92
|
+
// -----------------------------------------------------------------
|
|
93
|
+
{
|
|
94
|
+
provider: 'openai',
|
|
95
|
+
model: 'gpt-5',
|
|
96
|
+
inputUsdPerToken: 1.25 / 1_000_000,
|
|
97
|
+
outputUsdPerToken: 10 / 1_000_000,
|
|
98
|
+
cachedReadUsdPerToken: 0.125 / 1_000_000,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
provider: 'openai',
|
|
102
|
+
model: 'gpt-5-mini',
|
|
103
|
+
inputUsdPerToken: 0.25 / 1_000_000,
|
|
104
|
+
outputUsdPerToken: 2 / 1_000_000,
|
|
105
|
+
cachedReadUsdPerToken: 0.025 / 1_000_000,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
provider: 'openai',
|
|
109
|
+
model: 'gpt-5-nano',
|
|
110
|
+
inputUsdPerToken: 0.05 / 1_000_000,
|
|
111
|
+
outputUsdPerToken: 0.4 / 1_000_000,
|
|
112
|
+
cachedReadUsdPerToken: 0.005 / 1_000_000,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
provider: 'openai',
|
|
116
|
+
model: 'gpt-4.1',
|
|
117
|
+
inputUsdPerToken: 2 / 1_000_000,
|
|
118
|
+
outputUsdPerToken: 8 / 1_000_000,
|
|
119
|
+
cachedReadUsdPerToken: 0.5 / 1_000_000,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
provider: 'openai',
|
|
123
|
+
model: 'gpt-4.1-mini',
|
|
124
|
+
inputUsdPerToken: 0.4 / 1_000_000,
|
|
125
|
+
outputUsdPerToken: 1.6 / 1_000_000,
|
|
126
|
+
cachedReadUsdPerToken: 0.1 / 1_000_000,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
provider: 'openai',
|
|
130
|
+
model: 'gpt-4.1-nano',
|
|
131
|
+
inputUsdPerToken: 0.1 / 1_000_000,
|
|
132
|
+
outputUsdPerToken: 0.4 / 1_000_000,
|
|
133
|
+
cachedReadUsdPerToken: 0.025 / 1_000_000,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
provider: 'openai',
|
|
137
|
+
model: 'o3',
|
|
138
|
+
inputUsdPerToken: 2 / 1_000_000,
|
|
139
|
+
outputUsdPerToken: 8 / 1_000_000,
|
|
140
|
+
cachedReadUsdPerToken: 0.5 / 1_000_000,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
provider: 'openai',
|
|
144
|
+
model: 'o4-mini',
|
|
145
|
+
inputUsdPerToken: 1.1 / 1_000_000,
|
|
146
|
+
outputUsdPerToken: 4.4 / 1_000_000,
|
|
147
|
+
cachedReadUsdPerToken: 0.275 / 1_000_000,
|
|
148
|
+
},
|
|
149
|
+
// Retired / legacy ids retained for historical cost attribution.
|
|
150
|
+
{
|
|
151
|
+
provider: 'openai',
|
|
152
|
+
model: 'gpt-4o-2024-11-20',
|
|
153
|
+
inputUsdPerToken: 2.5 / 1_000_000,
|
|
154
|
+
outputUsdPerToken: 10 / 1_000_000,
|
|
155
|
+
cachedReadUsdPerToken: 1.25 / 1_000_000,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
provider: 'openai',
|
|
159
|
+
model: 'gpt-4o-mini-2024-07-18',
|
|
160
|
+
inputUsdPerToken: 0.15 / 1_000_000,
|
|
161
|
+
outputUsdPerToken: 0.6 / 1_000_000,
|
|
162
|
+
cachedReadUsdPerToken: 0.075 / 1_000_000,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
provider: 'openai',
|
|
166
|
+
model: 'o1-2024-12-17',
|
|
167
|
+
inputUsdPerToken: 15 / 1_000_000,
|
|
168
|
+
outputUsdPerToken: 60 / 1_000_000,
|
|
169
|
+
cachedReadUsdPerToken: 7.5 / 1_000_000,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
provider: 'openai',
|
|
173
|
+
model: 'o3-mini-2025-01-31',
|
|
174
|
+
inputUsdPerToken: 1.1 / 1_000_000,
|
|
175
|
+
outputUsdPerToken: 4.4 / 1_000_000,
|
|
176
|
+
cachedReadUsdPerToken: 0.55 / 1_000_000,
|
|
177
|
+
},
|
|
178
|
+
// -----------------------------------------------------------------
|
|
179
|
+
// Google - implicit-caching read discount; context-cache storage is
|
|
180
|
+
// billed per hour upstream and is not modelled here.
|
|
181
|
+
// -----------------------------------------------------------------
|
|
182
|
+
{
|
|
183
|
+
provider: 'google',
|
|
184
|
+
model: 'gemini-2.5-pro',
|
|
185
|
+
inputUsdPerToken: 1.25 / 1_000_000,
|
|
186
|
+
outputUsdPerToken: 10 / 1_000_000,
|
|
187
|
+
cachedReadUsdPerToken: 0.3125 / 1_000_000,
|
|
188
|
+
notes: 'Prompts <= 200k tokens; the long-context tier is priced higher.',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
provider: 'google',
|
|
192
|
+
model: 'gemini-2.5-flash',
|
|
193
|
+
inputUsdPerToken: 0.3 / 1_000_000,
|
|
194
|
+
outputUsdPerToken: 2.5 / 1_000_000,
|
|
195
|
+
cachedReadUsdPerToken: 0.075 / 1_000_000,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
provider: 'google',
|
|
199
|
+
model: 'gemini-2.5-flash-lite',
|
|
200
|
+
inputUsdPerToken: 0.1 / 1_000_000,
|
|
201
|
+
outputUsdPerToken: 0.4 / 1_000_000,
|
|
202
|
+
cachedReadUsdPerToken: 0.025 / 1_000_000,
|
|
203
|
+
},
|
|
204
|
+
// Retired / legacy ids retained for historical cost attribution.
|
|
205
|
+
{
|
|
206
|
+
provider: 'google',
|
|
207
|
+
model: 'gemini-1.5-pro-002',
|
|
208
|
+
inputUsdPerToken: 1.25 / 1_000_000,
|
|
209
|
+
outputUsdPerToken: 5 / 1_000_000,
|
|
210
|
+
cachedReadUsdPerToken: 0.3125 / 1_000_000,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
provider: 'google',
|
|
214
|
+
model: 'gemini-1.5-flash-002',
|
|
215
|
+
inputUsdPerToken: 0.075 / 1_000_000,
|
|
216
|
+
outputUsdPerToken: 0.3 / 1_000_000,
|
|
217
|
+
cachedReadUsdPerToken: 0.01875 / 1_000_000,
|
|
218
|
+
},
|
|
219
|
+
// -----------------------------------------------------------------
|
|
220
|
+
// Mistral
|
|
221
|
+
// -----------------------------------------------------------------
|
|
222
|
+
{
|
|
223
|
+
provider: 'mistral',
|
|
224
|
+
model: 'mistral-large-2411',
|
|
225
|
+
inputUsdPerToken: 2 / 1_000_000,
|
|
226
|
+
outputUsdPerToken: 6 / 1_000_000,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
provider: 'mistral',
|
|
230
|
+
model: 'mistral-small-2503',
|
|
231
|
+
inputUsdPerToken: 0.2 / 1_000_000,
|
|
232
|
+
outputUsdPerToken: 0.6 / 1_000_000,
|
|
233
|
+
},
|
|
234
|
+
// -----------------------------------------------------------------
|
|
235
|
+
// Cohere
|
|
236
|
+
// -----------------------------------------------------------------
|
|
237
|
+
{
|
|
238
|
+
provider: 'cohere',
|
|
239
|
+
model: 'command-r-plus-08-2024',
|
|
240
|
+
inputUsdPerToken: 2.5 / 1_000_000,
|
|
241
|
+
outputUsdPerToken: 10 / 1_000_000,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
provider: 'cohere',
|
|
245
|
+
model: 'command-r-08-2024',
|
|
246
|
+
inputUsdPerToken: 0.15 / 1_000_000,
|
|
247
|
+
outputUsdPerToken: 0.6 / 1_000_000,
|
|
248
|
+
},
|
|
249
|
+
// -----------------------------------------------------------------
|
|
250
|
+
// Local providers - documented as zero-cost so cost rollups still
|
|
251
|
+
// include the call counts.
|
|
252
|
+
// -----------------------------------------------------------------
|
|
253
|
+
{
|
|
254
|
+
provider: 'ollama',
|
|
255
|
+
model: '*',
|
|
256
|
+
inputUsdPerToken: 0,
|
|
257
|
+
outputUsdPerToken: 0,
|
|
258
|
+
notes: 'Local Ollama deployment - operator-priced.',
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
provider: 'graphorin-llamacpp',
|
|
262
|
+
model: '*',
|
|
263
|
+
inputUsdPerToken: 0,
|
|
264
|
+
outputUsdPerToken: 0,
|
|
265
|
+
notes: 'Local llama.cpp deployment - operator-priced.',
|
|
266
|
+
},
|
|
267
|
+
] as const);
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Compute a deterministic SHA-256 of the entries. Sorting the entry
|
|
271
|
+
* keys before serialisation keeps the digest stable across Node
|
|
272
|
+
* versions / object-property-iteration orderings.
|
|
273
|
+
*
|
|
274
|
+
* @internal
|
|
275
|
+
*/
|
|
276
|
+
export function computeEntriesDigest(entries: ReadonlyArray<ModelPrice>): string {
|
|
277
|
+
const canonical = JSON.stringify(entries.map((entry) => sortKeys(entry as unknown)));
|
|
278
|
+
return createHash('sha256').update(canonical, 'utf8').digest('hex');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function sortKeys(value: unknown): unknown {
|
|
282
|
+
if (value === null || typeof value !== 'object') return value;
|
|
283
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
284
|
+
const out: Record<string, unknown> = {};
|
|
285
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
286
|
+
out[key] = sortKeys((value as Record<string, unknown>)[key]);
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The bundled snapshot. The `sha256` digest is computed over the
|
|
293
|
+
* canonical JSON form of `entries` at module load time so consumers
|
|
294
|
+
* can verify integrity without trusting the package metadata.
|
|
295
|
+
*
|
|
296
|
+
* @stable
|
|
297
|
+
*/
|
|
298
|
+
export const BUNDLED_SNAPSHOT: PricingSnapshot = Object.freeze({
|
|
299
|
+
version: 'graphorin/0.1',
|
|
300
|
+
source:
|
|
301
|
+
'https://github.com/o-stepper/graphorin/tree/main/packages/pricing/src/snapshot/bundled.ts',
|
|
302
|
+
snapshotDate: SNAPSHOT_DATE,
|
|
303
|
+
currency: 'USD',
|
|
304
|
+
sha256: computeEntriesDigest(ENTRIES),
|
|
305
|
+
entries: ENTRIES,
|
|
306
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the pricing package.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Cost } from '@graphorin/core';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Per-model pricing entry. All amounts are in **USD per token** unless
|
|
11
|
+
* the snapshot declares an alternative currency. Reasoning tokens
|
|
12
|
+
* (when supported) follow the same pricing as completion tokens unless
|
|
13
|
+
* the entry declares an explicit `reasoningUsdPerToken`.
|
|
14
|
+
*
|
|
15
|
+
* @stable
|
|
16
|
+
*/
|
|
17
|
+
export interface ModelPrice {
|
|
18
|
+
/** Lower-case provider id (e.g. `'anthropic'`, `'openai'`, `'ollama'`). */
|
|
19
|
+
readonly provider: string;
|
|
20
|
+
/** Lower-case model id, e.g. `'claude-3-5-sonnet-20241022'`. */
|
|
21
|
+
readonly model: string;
|
|
22
|
+
/** Price per input token, in USD. */
|
|
23
|
+
readonly inputUsdPerToken: number;
|
|
24
|
+
/** Price per output token, in USD. */
|
|
25
|
+
readonly outputUsdPerToken: number;
|
|
26
|
+
/** Optional cached-read price (Anthropic / OpenAI prompt caching). */
|
|
27
|
+
readonly cachedReadUsdPerToken?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Optional cache-write (cache-creation) price. Anthropic bills prompt
|
|
30
|
+
* tokens written to the 5-minute cache at 1.25x the input rate; OpenAI
|
|
31
|
+
* does not charge (or report) cache writes, so its entries omit this.
|
|
32
|
+
*/
|
|
33
|
+
readonly cacheWriteUsdPerToken?: number;
|
|
34
|
+
/** Optional reasoning-token price (OpenAI o1 / Gemini 2 thinking). */
|
|
35
|
+
readonly reasoningUsdPerToken?: number;
|
|
36
|
+
/** Optional region label (e.g. `'us-east-1'`). */
|
|
37
|
+
readonly region?: string;
|
|
38
|
+
/** Free-form notes for tooling / docs. */
|
|
39
|
+
readonly notes?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Single bundled snapshot.
|
|
44
|
+
*
|
|
45
|
+
* @stable
|
|
46
|
+
*/
|
|
47
|
+
export interface PricingSnapshot {
|
|
48
|
+
readonly version: string;
|
|
49
|
+
readonly source: string;
|
|
50
|
+
readonly snapshotDate: string;
|
|
51
|
+
readonly currency: 'USD';
|
|
52
|
+
readonly sha256: string;
|
|
53
|
+
readonly entries: ReadonlyArray<ModelPrice>;
|
|
54
|
+
/**
|
|
55
|
+
* W-097: present when `refreshPricing` converted a foreign dataset
|
|
56
|
+
* (today: `@pydantic/genai-prices`) instead of consuming the native
|
|
57
|
+
* shape. `skipped` counts model entries the supported subset could
|
|
58
|
+
* not represent (tiered / conditional pricing).
|
|
59
|
+
*/
|
|
60
|
+
readonly conversion?: {
|
|
61
|
+
readonly format: 'genai-prices';
|
|
62
|
+
readonly skipped: number;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Lookup criteria.
|
|
68
|
+
*
|
|
69
|
+
* @stable
|
|
70
|
+
*/
|
|
71
|
+
export interface LookupPriceArgs {
|
|
72
|
+
readonly provider: string;
|
|
73
|
+
readonly model: string;
|
|
74
|
+
readonly region?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Result of {@link lookupPrice} when the model is known.
|
|
79
|
+
*
|
|
80
|
+
* @stable
|
|
81
|
+
*/
|
|
82
|
+
export interface LookupPriceResult {
|
|
83
|
+
readonly inputUsdPerToken: number;
|
|
84
|
+
readonly outputUsdPerToken: number;
|
|
85
|
+
readonly cachedReadUsdPerToken?: number;
|
|
86
|
+
readonly cacheWriteUsdPerToken?: number;
|
|
87
|
+
readonly reasoningUsdPerToken?: number;
|
|
88
|
+
readonly source: string;
|
|
89
|
+
readonly snapshotDate: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Result row reported by {@link diffPricing}.
|
|
94
|
+
*
|
|
95
|
+
* @stable
|
|
96
|
+
*/
|
|
97
|
+
export interface PricingDiffEntry {
|
|
98
|
+
readonly provider: string;
|
|
99
|
+
readonly model: string;
|
|
100
|
+
readonly kind: 'added' | 'removed' | 'changed';
|
|
101
|
+
readonly before?: ModelPrice;
|
|
102
|
+
readonly after?: ModelPrice;
|
|
103
|
+
readonly changedFields?: ReadonlyArray<keyof ModelPrice>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Span-shape input accepted by {@link listMissingModels}. Lightweight
|
|
108
|
+
* subset of `SpanRecord` from `@graphorin/observability` so the
|
|
109
|
+
* pricing package stays free of an observability dependency.
|
|
110
|
+
*
|
|
111
|
+
* @stable
|
|
112
|
+
*/
|
|
113
|
+
export interface PricingTraceSpanLike {
|
|
114
|
+
readonly attributes: Readonly<Record<string, unknown>>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Re-export the `@graphorin/core` `Cost` shape for caller convenience.
|
|
119
|
+
*
|
|
120
|
+
* @stable
|
|
121
|
+
*/
|
|
122
|
+
export type { Cost };
|