@pydantic/genai-prices 0.0.6

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/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # genai-prices package (JS/TS)
2
+
3
+ Library and CLI for calculating LLM API prices, supporting browser, Node.js and other environments.
4
+
5
+ ## Features
6
+
7
+ - **Sync API**: Fast, local price calculation using embedded data. Works in browser, Node.js, Cloudflare, Deno, etc.
8
+ - **Async API**: Fetches and caches price data from GitHub, works in browser, Node.js, Cloudflare, Deno, etc.
9
+ - **Environment-agnostic design**: Sync/async distinction is about API style, not environment.
10
+ - **Smart provider and model matching** with flexible options.
11
+ - **CLI** for quick price calculations with auto-update support.
12
+ - **Browser support** with a single bundle and test page.
13
+
14
+ ## API Usage
15
+
16
+ ### Node.js & Browser (Library)
17
+
18
+ ```js
19
+ import { calcPriceSync, calcPriceAsync } from '@pydantic/genai-prices'
20
+
21
+ const usage = { input_tokens: 1000, output_tokens: 100 }
22
+
23
+ // Sync (works everywhere, including browser)
24
+ const result = calcPriceSync(usage, 'gpt-3.5-turbo', { providerId: 'openai' })
25
+ if (result) {
26
+ console.log(result.price, result.provider.name, result.model.name)
27
+ } else {
28
+ console.log('No price found for this model/provider combination')
29
+ }
30
+
31
+ // Async (works everywhere)
32
+ const asyncResult = await calcPriceAsync(usage, 'gpt-3.5-turbo', { providerId: 'openai' })
33
+ if (asyncResult) {
34
+ console.log(asyncResult.price, asyncResult.provider.name, asyncResult.model.name)
35
+ } else {
36
+ console.log('No price found for this model/provider combination')
37
+ }
38
+ ```
39
+
40
+ ### Browser (Direct Bundle)
41
+
42
+ ```js
43
+ import { calcPriceSync, calcPriceAsync } from './dist/index.js'
44
+ const usage = { input_tokens: 1000, output_tokens: 100 }
45
+ const result = calcPriceSync(usage, 'gpt-3.5-turbo', { providerId: 'openai' })
46
+ if (result) {
47
+ console.log(result.price, result.provider.name, result.model.name)
48
+ }
49
+ ```
50
+
51
+ ### Global CLI Installation
52
+
53
+ You can install the CLI globally to use the `genai-prices` command from anywhere:
54
+
55
+ ```bash
56
+ npm install -g @pydantic/genai-prices
57
+ ```
58
+
59
+ After installing globally, you can run:
60
+
61
+ ```bash
62
+ genai-prices calc gpt-4 --input-tokens 1000 --output-tokens 500
63
+ genai-prices list
64
+ ```
65
+
66
+ ### CLI
67
+
68
+ After global installation, you can use the CLI as follows:
69
+
70
+ ```bash
71
+ # Basic usage
72
+ genai-prices gpt-3.5-turbo --input-tokens 1000 --output-tokens 100
73
+
74
+ # With auto-update (fetches latest prices from GitHub)
75
+ genai-prices gpt-3.5-turbo --input-tokens 1000 --output-tokens 100 --auto-update
76
+
77
+ # Specify provider explicitly
78
+ genai-prices openai:gpt-3.5-turbo --input-tokens 1000 --output-tokens 100
79
+
80
+ # List available providers and models
81
+ genai-prices list
82
+ genai-prices list openai
83
+ ```
84
+
85
+ ### Provider Matching
86
+
87
+ The library uses intelligent provider matching:
88
+
89
+ 1. **Explicit provider**: Use `providerId` parameter or `provider:model` format
90
+ 2. **Model-based matching**: Uses provider's `model_match` logic (e.g., OpenAI matches models starting with "gpt-")
91
+ 3. **Fallback**: Tries to match based on model name patterns
92
+
93
+ **Best practices:**
94
+
95
+ - Always specify `providerId` if you know it (e.g., `openai`, `google`, etc.) for best results
96
+ - Use `provider:model` format in CLI for explicit provider selection
97
+ - The async API with `--auto-update` provides the most up-to-date pricing
98
+
99
+ ### Error Handling
100
+
101
+ The library returns `null` when a model or provider is not found, rather than throwing errors. This makes it easier to handle cases where pricing information might not be available:
102
+
103
+ ```js
104
+ import { calcPriceSync, calcPriceAsync } from '@pydantic/genai-prices'
105
+
106
+ const usage = { input_tokens: 1000, output_tokens: 100 }
107
+
108
+ // Returns null if model/provider not found
109
+ const result = calcPriceSync(usage, 'non-existent-model')
110
+ if (result === null) {
111
+ console.log('No pricing information available for this model')
112
+ } else {
113
+ console.log(`Price: $${result.price}`)
114
+ }
115
+
116
+ // Async version also returns null
117
+ const asyncResult = await calcPriceAsync(usage, 'non-existent-model', { providerId: 'unknown-provider' })
118
+ if (asyncResult === null) {
119
+ console.log('No pricing information available for this model/provider combination')
120
+ } else {
121
+ console.log(`Price: $${asyncResult.price}`)
122
+ }
123
+ ```
124
+
125
+ **TypeScript users**: The return type is `PriceCalculation | null` (exported as `PriceCalculationResult`).
126
+
127
+ ## Testing
128
+
129
+ ### Node.js Test
130
+
131
+ Run:
132
+
133
+ ```bash
134
+ node tests/test-error-handling.js
135
+ ```
136
+
137
+ This tests error handling, sync/async API, and providerId usage.
138
+
139
+ ### Browser Test
140
+
141
+ 1. Build the package: `npm run build`
142
+ 2. Serve the directory: `npx serve .` or `python3 -m http.server`
143
+ 3. Open `tests/test-browser.html` in your browser.
144
+ 4. Enter a provider (e.g., `openai`) and model (e.g., `gpt-3.5-turbo`) and run the test.
145
+
146
+ ## Architecture
147
+
148
+ ### Folder Structure
149
+
150
+ ```
151
+ src/
152
+ ├── sync/
153
+ │ └── calcPriceSync.ts # Sync API implementation
154
+ ├── async/
155
+ │ └── calcPriceAsync.ts # Async API implementation
156
+ ├── dataLoader.ts # Data loader (sync + async)
157
+ ├── index.ts # Entry point (exports both sync + async)
158
+ ├── cli.ts # CLI tool
159
+ ├── types.ts # Shared types (snake_case, matches JSON)
160
+ ├── matcher.ts # Shared matching logic
161
+ ├── priceCalc.ts # Shared price calculation
162
+ └── __tests__/ # Tests
163
+ ```
164
+
165
+ ### Design Principles
166
+
167
+ - **Environment-agnostic APIs**: Sync/async is about API style, not environment
168
+ - **Single data loader**: Handles all environments with embedded data for sync and remote fetch for async
169
+ - **Cross-environment compatibility**: Both sync and async APIs can be used in Node.js, browser, Cloudflare, etc.
170
+ - **No mapping needed**: All types and data use snake_case, matching the JSON schema
171
+
172
+ ## Troubleshooting
173
+
174
+ ### Common Issues
175
+
176
+ - **No price found (returns null)**:
177
+ - Make sure you specify the correct `providerId` (e.g., `openai`)
178
+ - Try using `provider:model` format in CLI
179
+ - Use `--auto-update` flag to fetch latest data
180
+ - Check that the model name is correct and supported by the provider
181
+ - **Build errors**: Ensure you have run the build and that your data is up to date.
182
+
183
+ ### Provider Matching Examples
184
+
185
+ ```bash
186
+ # These should work with auto-update
187
+ genai-prices gpt-3.5-turbo --auto-update
188
+ genai-prices claude-3-5-sonnet --auto-update
189
+ genai-prices gemini-1.5-pro --auto-update
190
+
191
+ # Explicit provider specification
192
+ genai-prices openai:gpt-3.5-turbo
193
+ genai-prices anthropic:claude-3-5-sonnet
194
+ genai-prices google:gemini-1.5-pro
195
+ ```
196
+
197
+ ## Maintainers
198
+
199
+ - When adding new features, keep sync and async logic in separate files
200
+ - Only import Node.js built-ins in Node-only files if absolutely necessary
201
+ - Use the main entry for all environments
202
+ - All types and data should use snake_case to match the JSON schema
203
+ - Provider matching logic is in `matcher.ts` and should be environment-agnostic
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { Usage, PriceCalculationResult } from '../types.js';
2
+ export type { Usage, PriceCalculation, PriceCalculationResult } from '../types.js';
3
+ export interface CalcPriceOptions {
4
+ providerId?: string;
5
+ providerApiUrl?: string;
6
+ timestamp?: Date;
7
+ }
8
+ export declare function calcPriceAsync(usage: Usage, modelRef: string, options?: CalcPriceOptions): Promise<PriceCalculationResult>;
@@ -0,0 +1,22 @@
1
+ import { getProvidersAsync } from '../dataLoader.js';
2
+ import { matchProvider, matchModel } from '../matcher.js';
3
+ import { calcPrice as calcModelPrice, getActiveModelPrice } from '../priceCalc.js';
4
+ export async function calcPriceAsync(usage, modelRef, options = {}) {
5
+ const providers = await getProvidersAsync();
6
+ const provider = matchProvider(providers, modelRef, options.providerId, options.providerApiUrl);
7
+ if (!provider)
8
+ return null;
9
+ const model = matchModel(provider.models, modelRef);
10
+ if (!model)
11
+ return null;
12
+ const timestamp = options.timestamp || new Date();
13
+ const model_price = getActiveModelPrice(model, timestamp);
14
+ const price = calcModelPrice(usage, model_price);
15
+ return {
16
+ price,
17
+ provider,
18
+ model,
19
+ model_price,
20
+ auto_update_timestamp: undefined,
21
+ };
22
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ import yargs from 'yargs';
3
+ import { hideBin } from 'yargs/helpers';
4
+ import { calcPriceSync, calcPriceAsync, enableAutoUpdate } from './index.js';
5
+ const argv = yargs(hideBin(process.argv))
6
+ .scriptName('genai-prices')
7
+ .command('list [provider]', 'List providers and models', (y) => y.positional('provider', { type: 'string', describe: 'Provider ID to filter' }))
8
+ .command('calc <model...>', 'Calculate price', (y) => y
9
+ .positional('model', { type: 'string', describe: 'Model(s) (optionally provider:model)', array: true })
10
+ .option('input-tokens', { type: 'number' })
11
+ .option('cache-write-tokens', { type: 'number' })
12
+ .option('cache-read-tokens', { type: 'number' })
13
+ .option('output-tokens', { type: 'number' })
14
+ .option('input-audio-tokens', { type: 'number' })
15
+ .option('cache-audio-read-tokens', { type: 'number' })
16
+ .option('output-audio-tokens', { type: 'number' })
17
+ .option('requests', { type: 'number' })
18
+ .option('provider', { type: 'string' })
19
+ .option('auto-update', { type: 'boolean', default: false })
20
+ .option('timestamp', { type: 'string', describe: 'RFC3339 timestamp' }))
21
+ .option('auto-update', { type: 'boolean', describe: 'Enable auto-update from GitHub' })
22
+ .option('input-tokens', { type: 'number' })
23
+ .option('cache-write-tokens', { type: 'number' })
24
+ .option('cache-read-tokens', { type: 'number' })
25
+ .option('output-tokens', { type: 'number' })
26
+ .option('input-audio-tokens', { type: 'number' })
27
+ .option('cache-audio-read-tokens', { type: 'number' })
28
+ .option('output-audio-tokens', { type: 'number' })
29
+ .option('requests', { type: 'number' })
30
+ .option('provider', { type: 'string' })
31
+ .option('timestamp', { type: 'string', describe: 'RFC3339 timestamp' })
32
+ .version('0.1.0')
33
+ .help()
34
+ .parseSync();
35
+ async function main() {
36
+ if (argv['auto-update'])
37
+ enableAutoUpdate();
38
+ // Handle list command
39
+ if (argv._[0] === 'list') {
40
+ if (argv['auto-update']) {
41
+ const { getProvidersAsync } = await import('./dataLoader.js');
42
+ const providers = await getProvidersAsync();
43
+ if (argv.provider) {
44
+ const p = providers.find((p) => p.id === argv.provider);
45
+ if (!p) {
46
+ console.error(`Provider ${argv.provider} not found.`);
47
+ process.exit(1);
48
+ }
49
+ console.log(`${p.name}: (${p.models.length} models)`);
50
+ for (const m of p.models) {
51
+ console.log(` ${p.id}:${m.id}${m.name ? ': ' + m.name : ''}`);
52
+ }
53
+ }
54
+ else {
55
+ for (const p of providers) {
56
+ console.log(`${p.name}: (${p.models.length} models)`);
57
+ for (const m of p.models) {
58
+ console.log(` ${p.id}:${m.id}${m.name ? ': ' + m.name : ''}`);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ else {
64
+ const { getProvidersSync } = await import('./dataLoader.js');
65
+ const providers = getProvidersSync();
66
+ if (argv.provider) {
67
+ const p = providers.find((p) => p.id === argv.provider);
68
+ if (!p) {
69
+ console.error(`Provider ${argv.provider} not found.`);
70
+ process.exit(1);
71
+ }
72
+ console.log(`${p.name}: (${p.models.length} models)`);
73
+ for (const m of p.models) {
74
+ console.log(` ${p.id}:${m.id}${m.name ? ': ' + m.name : ''}`);
75
+ }
76
+ }
77
+ else {
78
+ for (const p of providers) {
79
+ console.log(`${p.name}: (${p.models.length} models)`);
80
+ for (const m of p.models) {
81
+ console.log(` ${p.id}:${m.id}${m.name ? ': ' + m.name : ''}`);
82
+ }
83
+ }
84
+ }
85
+ }
86
+ process.exit(0);
87
+ }
88
+ // Handle calc command or direct model names
89
+ const isCalcCommand = argv._[0] === 'calc';
90
+ const models = isCalcCommand
91
+ ? Array.isArray(argv.model)
92
+ ? argv.model
93
+ : [argv.model]
94
+ : argv._.filter((arg) => typeof arg === 'string');
95
+ if (models.length > 0) {
96
+ const usage = {
97
+ input_tokens: argv['input-tokens'] !== undefined ? Number(argv['input-tokens']) : undefined,
98
+ cache_write_tokens: argv['cache-write-tokens'] !== undefined ? Number(argv['cache-write-tokens']) : undefined,
99
+ cache_read_tokens: argv['cache-read-tokens'] !== undefined ? Number(argv['cache-read-tokens']) : undefined,
100
+ output_tokens: argv['output-tokens'] !== undefined ? Number(argv['output-tokens']) : undefined,
101
+ input_audio_tokens: argv['input-audio-tokens'] !== undefined ? Number(argv['input-audio-tokens']) : undefined,
102
+ cache_audio_read_tokens: argv['cache-audio-read-tokens'] !== undefined ? Number(argv['cache-audio-read-tokens']) : undefined,
103
+ output_audio_tokens: argv['output-audio-tokens'] !== undefined ? Number(argv['output-audio-tokens']) : undefined,
104
+ requests: argv['requests'] !== undefined ? Number(argv['requests']) : undefined,
105
+ };
106
+ const timestamp = argv.timestamp ? new Date(String(argv.timestamp)) : undefined;
107
+ const fn = argv['auto-update'] ? calcPriceAsync : calcPriceSync;
108
+ let hadError = false;
109
+ for (const modelArg of models) {
110
+ let providerId;
111
+ let modelRef = modelArg;
112
+ if (modelRef.includes(':')) {
113
+ ;
114
+ [providerId, modelRef] = modelRef.split(':', 2);
115
+ }
116
+ try {
117
+ const result = await fn(usage, modelRef, { providerId, timestamp });
118
+ if (!result) {
119
+ hadError = true;
120
+ console.error(`No price found for model ${modelArg}`);
121
+ continue;
122
+ }
123
+ const w = result.model.context_window;
124
+ const output = [
125
+ ['Provider', result.provider.name],
126
+ ['Model', result.model.name || result.model.id],
127
+ ['Model Prices', JSON.stringify(result.model_price)],
128
+ ['Context Window', w !== undefined ? w.toLocaleString() : undefined],
129
+ ['Price', `$${result.price}`],
130
+ ];
131
+ for (const [key, value] of output) {
132
+ if (value !== undefined) {
133
+ console.log(`${key.padStart(14)}: ${value}`);
134
+ }
135
+ }
136
+ console.log('');
137
+ }
138
+ catch (e) {
139
+ hadError = true;
140
+ console.error(`Error for model ${modelArg}:`, e.message);
141
+ }
142
+ }
143
+ process.exit(hadError ? 1 : 0);
144
+ }
145
+ // If no command matched
146
+ yargs().showHelp();
147
+ process.exit(1);
148
+ }
149
+ main();
package/dist/data.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { Provider } from './types.js';
2
+ export declare const data: Provider[];