@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/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
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Bundled pricing snapshot surface.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ export { BUNDLED_SNAPSHOT, computeEntriesDigest, SNAPSHOT_DATE } from './bundled.js';
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 };