@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 +203 -0
- package/dist/__tests__/calcPrice.test.d.ts +1 -0
- package/dist/async/calcPriceAsync.d.ts +8 -0
- package/dist/async/calcPriceAsync.js +22 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +149 -0
- package/dist/data.d.ts +2 -0
- package/dist/data.js +10414 -0
- package/dist/dataLoader.d.ts +18 -0
- package/dist/dataLoader.js +146 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/index.umd.cjs +10697 -0
- package/dist/matcher.d.ts +3 -0
- package/dist/matcher.js +37 -0
- package/dist/priceCalc.d.ts +3 -0
- package/dist/priceCalc.js +68 -0
- package/dist/sync/calcPriceSync.d.ts +8 -0
- package/dist/sync/calcPriceSync.js +22 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
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