burner-cc 0.1.2 → 0.3.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/README.md +71 -21
- package/bin/burner.js +2 -0
- package/package.json +11 -8
- package/src/analyze.js +241 -0
- package/src/build-receipt.js +118 -11
- package/src/classifier.js +140 -0
- package/src/cli.js +181 -84
- package/src/config.js +222 -0
- package/src/discover.js +10 -7
- package/src/install.js +215 -0
- package/src/model/prior.json +1 -0
- package/src/model/prior.meta.json +320 -0
- package/src/parse.js +234 -42
- package/src/pricing-fallback.json +199 -6
- package/src/pricing.js +176 -98
- package/src/receipt.js +351 -0
- package/src/report.js +873 -0
- package/src/server.js +261 -0
- package/src/settings-page.js +752 -0
- package/src/tag-end.js +94 -0
- package/src/tagging.js +122 -0
- package/src/tags.js +79 -0
- package/src/themes.js +122 -0
- package/src/yaml.js +163 -0
- package/web/receipt.css +0 -298
- package/web/receipt.html +0 -32
- package/web/receipt.js +0 -315
- /package/{web → assets}/logo-mark.png +0 -0
- /package/{web → assets}/logo-with-text.png +0 -0
- /package/{web/vendor → vendor}/html2canvas.min.js +0 -0
|
@@ -1,9 +1,202 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
"_comment": "Last-resort snapshot used only when offline with no cache. Live prices come from LiteLLM (src/pricing.js). USD per million tokens.",
|
|
3
|
+
"capturedAt": 1792919926365,
|
|
4
|
+
"models": {
|
|
5
|
+
"claude-3-5-haiku": {
|
|
6
|
+
"input": 0.8,
|
|
7
|
+
"output": 4,
|
|
8
|
+
"cacheWrite5m": 1,
|
|
9
|
+
"cacheWrite1h": 1.6,
|
|
10
|
+
"cacheRead": 0.08
|
|
11
|
+
},
|
|
12
|
+
"claude-3-5-sonnet": {
|
|
13
|
+
"input": 3,
|
|
14
|
+
"output": 15,
|
|
15
|
+
"cacheWrite5m": 3.75,
|
|
16
|
+
"cacheWrite1h": 6,
|
|
17
|
+
"cacheRead": 0.3
|
|
18
|
+
},
|
|
19
|
+
"claude-3-7-sonnet": {
|
|
20
|
+
"input": 3.6,
|
|
21
|
+
"output": 18,
|
|
22
|
+
"cacheWrite5m": 4.5,
|
|
23
|
+
"cacheWrite1h": 7.2,
|
|
24
|
+
"cacheRead": 0.36
|
|
25
|
+
},
|
|
26
|
+
"claude-3-haiku": {
|
|
27
|
+
"input": 0.25,
|
|
28
|
+
"output": 1.25,
|
|
29
|
+
"cacheWrite5m": 0.3125,
|
|
30
|
+
"cacheWrite1h": 0.5,
|
|
31
|
+
"cacheRead": 0.025
|
|
32
|
+
},
|
|
33
|
+
"claude-3-opus": {
|
|
34
|
+
"input": 15,
|
|
35
|
+
"output": 75,
|
|
36
|
+
"cacheWrite5m": 18.75,
|
|
37
|
+
"cacheWrite1h": 30,
|
|
38
|
+
"cacheRead": 1.5
|
|
39
|
+
},
|
|
40
|
+
"claude-3-sonnet": {
|
|
41
|
+
"input": 3,
|
|
42
|
+
"output": 15,
|
|
43
|
+
"cacheWrite5m": 3.75,
|
|
44
|
+
"cacheWrite1h": 6,
|
|
45
|
+
"cacheRead": 0.3
|
|
46
|
+
},
|
|
47
|
+
"claude-fable-5": {
|
|
48
|
+
"input": 10,
|
|
49
|
+
"output": 50,
|
|
50
|
+
"cacheWrite5m": 12.5,
|
|
51
|
+
"cacheWrite1h": 20,
|
|
52
|
+
"cacheRead": 1
|
|
53
|
+
},
|
|
54
|
+
"claude-fable-5-1": {
|
|
55
|
+
"input": 10,
|
|
56
|
+
"output": 50,
|
|
57
|
+
"cacheWrite5m": 12.5,
|
|
58
|
+
"cacheWrite1h": 20,
|
|
59
|
+
"cacheRead": 0.25
|
|
60
|
+
},
|
|
61
|
+
"claude-haiku-4-5": {
|
|
62
|
+
"input": 1,
|
|
63
|
+
"output": 5,
|
|
64
|
+
"cacheWrite5m": 1.25,
|
|
65
|
+
"cacheWrite1h": 2,
|
|
66
|
+
"cacheRead": 0.1
|
|
67
|
+
},
|
|
68
|
+
"claude-haiku-5-5": {
|
|
69
|
+
"input": 0.1,
|
|
70
|
+
"output": 0.5,
|
|
71
|
+
"cacheWrite5m": 0.125,
|
|
72
|
+
"cacheWrite1h": 0.2,
|
|
73
|
+
"cacheRead": 0.01
|
|
74
|
+
},
|
|
75
|
+
"claude-instant": {
|
|
76
|
+
"input": 0.8,
|
|
77
|
+
"output": 2.4,
|
|
78
|
+
"cacheWrite5m": 1,
|
|
79
|
+
"cacheWrite1h": 1.6,
|
|
80
|
+
"cacheRead": 0.08
|
|
81
|
+
},
|
|
82
|
+
"claude-mythos-5": {
|
|
83
|
+
"input": 10,
|
|
84
|
+
"output": 50,
|
|
85
|
+
"cacheWrite5m": 12.5,
|
|
86
|
+
"cacheWrite1h": 20,
|
|
87
|
+
"cacheRead": 1
|
|
88
|
+
},
|
|
89
|
+
"claude-mythos-5-1": {
|
|
90
|
+
"input": 10,
|
|
91
|
+
"output": 50,
|
|
92
|
+
"cacheWrite5m": 12.5,
|
|
93
|
+
"cacheWrite1h": 20,
|
|
94
|
+
"cacheRead": 0.25
|
|
95
|
+
},
|
|
96
|
+
"claude-mythos-preview": {
|
|
97
|
+
"input": 10,
|
|
98
|
+
"output": 50,
|
|
99
|
+
"cacheWrite5m": 12.5,
|
|
100
|
+
"cacheWrite1h": 20,
|
|
101
|
+
"cacheRead": 1
|
|
102
|
+
},
|
|
103
|
+
"claude-opus-4": {
|
|
104
|
+
"input": 15,
|
|
105
|
+
"output": 75,
|
|
106
|
+
"cacheWrite5m": 18.75,
|
|
107
|
+
"cacheWrite1h": 30,
|
|
108
|
+
"cacheRead": 1.5
|
|
109
|
+
},
|
|
110
|
+
"claude-opus-4-1": {
|
|
111
|
+
"input": 15,
|
|
112
|
+
"output": 75,
|
|
113
|
+
"cacheWrite5m": 18.75,
|
|
114
|
+
"cacheWrite1h": 30,
|
|
115
|
+
"cacheRead": 1.5
|
|
116
|
+
},
|
|
117
|
+
"claude-opus-4-5": {
|
|
118
|
+
"input": 5,
|
|
119
|
+
"output": 25,
|
|
120
|
+
"cacheWrite5m": 6.25,
|
|
121
|
+
"cacheWrite1h": 10,
|
|
122
|
+
"cacheRead": 0.5
|
|
123
|
+
},
|
|
124
|
+
"claude-opus-4-6": {
|
|
125
|
+
"input": 5,
|
|
126
|
+
"output": 25,
|
|
127
|
+
"cacheWrite5m": 6.25,
|
|
128
|
+
"cacheWrite1h": 10,
|
|
129
|
+
"cacheRead": 0.5
|
|
130
|
+
},
|
|
131
|
+
"claude-opus-4-7": {
|
|
132
|
+
"input": 5,
|
|
133
|
+
"output": 25,
|
|
134
|
+
"cacheWrite5m": 6.25,
|
|
135
|
+
"cacheWrite1h": 10,
|
|
136
|
+
"cacheRead": 0.5
|
|
137
|
+
},
|
|
138
|
+
"claude-opus-4-8": {
|
|
139
|
+
"input": 5,
|
|
140
|
+
"output": 25,
|
|
141
|
+
"cacheWrite5m": 6.25,
|
|
142
|
+
"cacheWrite1h": 10,
|
|
143
|
+
"cacheRead": 0.5
|
|
144
|
+
},
|
|
145
|
+
"claude-opus-5": {
|
|
146
|
+
"input": 5,
|
|
147
|
+
"output": 25,
|
|
148
|
+
"cacheWrite5m": 6.25,
|
|
149
|
+
"cacheWrite1h": 10,
|
|
150
|
+
"cacheRead": 0.5
|
|
151
|
+
},
|
|
152
|
+
"claude-opus-5-5": {
|
|
153
|
+
"input": 4,
|
|
154
|
+
"output": 20,
|
|
155
|
+
"cacheWrite5m": 5,
|
|
156
|
+
"cacheWrite1h": 8,
|
|
157
|
+
"cacheRead": 0.2
|
|
158
|
+
},
|
|
159
|
+
"claude-opus-5-5-fast": {
|
|
160
|
+
"input": 8,
|
|
161
|
+
"output": 40,
|
|
162
|
+
"cacheWrite5m": 10,
|
|
163
|
+
"cacheWrite1h": 16,
|
|
164
|
+
"cacheRead": 0.4
|
|
165
|
+
},
|
|
166
|
+
"claude-sonnet-4": {
|
|
167
|
+
"input": 3,
|
|
168
|
+
"output": 15,
|
|
169
|
+
"cacheWrite5m": 3.75,
|
|
170
|
+
"cacheWrite1h": 6,
|
|
171
|
+
"cacheRead": 0.3
|
|
172
|
+
},
|
|
173
|
+
"claude-sonnet-4-5": {
|
|
174
|
+
"input": 3,
|
|
175
|
+
"output": 15,
|
|
176
|
+
"cacheWrite5m": 3.75,
|
|
177
|
+
"cacheWrite1h": 6,
|
|
178
|
+
"cacheRead": 0.3
|
|
179
|
+
},
|
|
180
|
+
"claude-sonnet-4-6": {
|
|
181
|
+
"input": 3,
|
|
182
|
+
"output": 15,
|
|
183
|
+
"cacheWrite5m": 3.75,
|
|
184
|
+
"cacheWrite1h": 6,
|
|
185
|
+
"cacheRead": 0.3
|
|
186
|
+
},
|
|
187
|
+
"claude-sonnet-5": {
|
|
188
|
+
"input": 2,
|
|
189
|
+
"output": 10,
|
|
190
|
+
"cacheWrite5m": 2.5,
|
|
191
|
+
"cacheWrite1h": 4,
|
|
192
|
+
"cacheRead": 0.2
|
|
193
|
+
},
|
|
194
|
+
"claude-sonnet-5-5": {
|
|
195
|
+
"input": 2,
|
|
196
|
+
"output": 10,
|
|
197
|
+
"cacheWrite5m": 2.5,
|
|
198
|
+
"cacheWrite1h": 4,
|
|
199
|
+
"cacheRead": 0.1
|
|
8
200
|
}
|
|
201
|
+
}
|
|
9
202
|
}
|
package/src/pricing.js
CHANGED
|
@@ -1,143 +1,221 @@
|
|
|
1
|
-
import { readFileSync
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { FILES, readJson, writeJson, loadPricingOverrides } from './config.js';
|
|
5
6
|
|
|
6
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const cacheDir = path.join(os.homedir(), '.burner-cc-cache');
|
|
8
|
-
const cacheFile = path.join(cacheDir, 'litellm-pricing.json');
|
|
9
8
|
const fallbackFile = path.join(__dirname, 'pricing-fallback.json');
|
|
10
9
|
|
|
11
|
-
const SOURCE_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
|
|
12
|
-
const TTL_MS =
|
|
10
|
+
export const SOURCE_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
|
|
11
|
+
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
const RATE_KEYS = ['input', 'output', 'cacheWrite5m', 'cacheWrite1h', 'cacheRead'];
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
14
|
+
async function fetchText(url, timeoutMs = 15000) {
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
17
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
18
|
+
return await res.text();
|
|
19
|
+
} catch (err) {
|
|
20
|
+
// Node's fetch ignores HTTP(S)_PROXY; curl honours it.
|
|
21
|
+
const proxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
|
|
22
|
+
const args = ['--silent', '--show-error', '--fail', '--location', '--max-time', String(Math.ceil(timeoutMs / 1000)), '--compressed'];
|
|
23
|
+
if (proxy) args.push('--proxy', proxy);
|
|
24
|
+
args.push(url);
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
execFile('curl', args, { maxBuffer: 64 * 1024 * 1024 }, (cerr, stdout, stderr) => {
|
|
27
|
+
if (cerr) reject(new Error(`${err.message}; curl: ${(stderr || cerr.message).trim()}`));
|
|
28
|
+
else resolve(stdout);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
const PROVIDER_PREFIX = /^(anthropic|bedrock|bedrock_converse|vertex_ai|vertex_ai_beta|openrouter|azure|azure_ai|openai|google|gemini|deepseek|mistral|groq|together_ai|fireworks_ai|xai|ollama|ollama_chat|zai|moonshot|dashscope|databricks|litellm_proxy)\//;
|
|
35
|
+
|
|
36
|
+
// Collapses the many spellings of one model to a lookup key:
|
|
37
|
+
// "us.anthropic.claude-sonnet-4-5-20250929-v1:0" → "claude-sonnet-4-5",
|
|
38
|
+
// "claude-opus-4-1@20250805" → "claude-opus-4-1", "claude-sonnet-4[1m]" → "claude-sonnet-4".
|
|
39
|
+
export function normalizeModelId(raw) {
|
|
40
|
+
let s = String(raw ?? '').trim().toLowerCase();
|
|
41
|
+
for (let i = 0; i < 3 && PROVIDER_PREFIX.test(s); i++) s = s.replace(PROVIDER_PREFIX, '');
|
|
42
|
+
s = s.replace(/\[[^\]]*\]$/, '');
|
|
43
|
+
s = s.replace(/^(us|eu|apac|global|au|jp)\./, '').replace(/^anthropic\./, '');
|
|
44
|
+
s = s.replace(/-v\d+(:\d+)?$/, '').replace(/:\d+$/, '');
|
|
45
|
+
s = s.replace(/@[a-z0-9-]+$/, '').replace(/-\d{8}$/, '').replace(/-latest$/, '');
|
|
46
|
+
return s;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isClaudeModel(raw) {
|
|
50
|
+
const n = normalizeModelId(raw);
|
|
51
|
+
return n.startsWith('claude-') || /^(opus|sonnet|haiku|fable|mythos)(-|$)/.test(n);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toRates(entry) {
|
|
55
|
+
const perM = (v) => Math.round(v * 1_000_000 * 1e6) / 1e6;
|
|
56
|
+
const inp = entry.input_cost_per_token;
|
|
20
57
|
return {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
cacheRead: (entry.cache_read_input_token_cost ?? 0) * mtok,
|
|
58
|
+
input: perM(inp),
|
|
59
|
+
output: perM(entry.output_cost_per_token),
|
|
60
|
+
cacheWrite5m: perM(entry.cache_creation_input_token_cost ?? inp * 1.25),
|
|
61
|
+
cacheWrite1h: perM(entry.cache_creation_input_token_cost_above_1hr ?? inp * 2),
|
|
62
|
+
cacheRead: perM(entry.cache_read_input_token_cost ?? inp * 0.1),
|
|
27
63
|
};
|
|
28
64
|
}
|
|
29
65
|
|
|
30
|
-
|
|
66
|
+
const PROVIDER_RANK = { anthropic: 0, openai: 1, gemini: 2, vertex_ai: 3, deepseek: 4, mistral: 5, xai: 6, zai: 7, moonshot: 8, dashscope: 9 };
|
|
67
|
+
|
|
68
|
+
// Keyed by normalized id. For each id, the first-party provider's price wins
|
|
69
|
+
// over resellers (OpenRouter, Bedrock regional markups, …).
|
|
70
|
+
export function buildTable(raw) {
|
|
31
71
|
const models = {};
|
|
72
|
+
const rank = {};
|
|
32
73
|
for (const [key, entry] of Object.entries(raw)) {
|
|
33
|
-
if (entry
|
|
74
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
75
|
+
if (entry.mode && entry.mode !== 'chat' && entry.mode !== 'responses') continue;
|
|
34
76
|
if (typeof entry.input_cost_per_token !== 'number' || typeof entry.output_cost_per_token !== 'number') continue;
|
|
35
|
-
|
|
77
|
+
const id = normalizeModelId(key);
|
|
78
|
+
if (!id) continue;
|
|
79
|
+
const r = PROVIDER_RANK[entry.litellm_provider] ?? 50;
|
|
80
|
+
const hasPrefix = PROVIDER_PREFIX.test(key.toLowerCase()) || /^(us|eu|apac|global)\./.test(key);
|
|
81
|
+
const score = r * 2 + (hasPrefix ? 1 : 0);
|
|
82
|
+
if (rank[id] != null && rank[id] <= score) continue;
|
|
83
|
+
rank[id] = score;
|
|
84
|
+
models[id] = { ...toRates(entry), provider: entry.litellm_provider ?? null, sourceKey: key };
|
|
36
85
|
}
|
|
37
86
|
return models;
|
|
38
87
|
}
|
|
39
88
|
|
|
40
|
-
function
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
89
|
+
export async function refreshPricing() {
|
|
90
|
+
const text = await fetchText(SOURCE_URL);
|
|
91
|
+
const models = buildTable(JSON.parse(text));
|
|
92
|
+
if (Object.keys(models).length < 50) throw new Error('pricing source looked truncated');
|
|
93
|
+
const fetchedAt = Date.now();
|
|
94
|
+
writeJson(FILES.pricingCache, { fetchedAt, url: SOURCE_URL, models });
|
|
95
|
+
return { models, source: 'live', fetchedAt };
|
|
46
96
|
}
|
|
47
97
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (!
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
98
|
+
// Resolved once per run: fresh cache, else live fetch, else stale cache,
|
|
99
|
+
// else the bundled snapshot. User overrides from pricing.yaml are layered
|
|
100
|
+
// on top by lookupPricing.
|
|
101
|
+
export async function loadPricingTable({ force = false, offline = false } = {}) {
|
|
102
|
+
const overrides = loadPricingOverrides();
|
|
103
|
+
const cache = readJson(FILES.pricingCache, null);
|
|
104
|
+
const fresh = cache?.models && Date.now() - cache.fetchedAt < TTL_MS;
|
|
105
|
+
if (fresh && !force) return { models: cache.models, overrides, source: 'cache', fetchedAt: cache.fetchedAt };
|
|
106
|
+
if (!offline) {
|
|
107
|
+
try {
|
|
108
|
+
return { ...(await refreshPricing()), overrides };
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (cache?.models) return { models: cache.models, overrides, source: 'stale-cache', fetchedAt: cache.fetchedAt, error: err.message };
|
|
111
|
+
const fb = JSON.parse(readFileSync(fallbackFile, 'utf-8'));
|
|
112
|
+
return { models: fb.models, overrides, source: 'bundled-fallback', fetchedAt: fb.capturedAt ?? null, error: err.message };
|
|
113
|
+
}
|
|
61
114
|
}
|
|
115
|
+
if (cache?.models) return { models: cache.models, overrides, source: 'cache', fetchedAt: cache.fetchedAt };
|
|
116
|
+
const fb = JSON.parse(readFileSync(fallbackFile, 'utf-8'));
|
|
117
|
+
return { models: fb.models, overrides, source: 'bundled-fallback', fetchedAt: fb.capturedAt ?? null };
|
|
62
118
|
}
|
|
63
119
|
|
|
64
|
-
|
|
65
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
66
|
-
writeFileSync(cacheFile, JSON.stringify({ fetchedAt: Date.now(), models }, null, 2), 'utf-8');
|
|
67
|
-
}
|
|
120
|
+
const ALIAS_FAMILIES = ['opus', 'sonnet', 'haiku', 'fable', 'mythos'];
|
|
68
121
|
|
|
69
|
-
function
|
|
70
|
-
return
|
|
122
|
+
function versionOf(id) {
|
|
123
|
+
return id.replace(/^claude-[a-z]+-?/, '').split(/[-.]/).map(Number);
|
|
71
124
|
}
|
|
72
125
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const models = await fetchLiteLLMTable();
|
|
85
|
-
writeCache(models);
|
|
86
|
-
return { models, source: 'live', fetchedAt: Date.now() };
|
|
87
|
-
} catch (err) {
|
|
88
|
-
if (cache) {
|
|
89
|
-
return { models: cache.models, source: 'stale-cache', fetchedAt: cache.fetchedAt, error: err.message };
|
|
126
|
+
function newestInFamily(family, models) {
|
|
127
|
+
const ids = Object.keys(models).filter((k) => new RegExp(`^claude-${family}(-[\\d.]+)*(-\\d)?$`).test(k));
|
|
128
|
+
ids.sort((a, b) => {
|
|
129
|
+
const va = versionOf(a);
|
|
130
|
+
const vb = versionOf(b);
|
|
131
|
+
for (let i = 0; i < Math.max(va.length, vb.length); i++) {
|
|
132
|
+
const d = (vb[i] ?? 0) - (va[i] ?? 0);
|
|
133
|
+
if (d) return d;
|
|
90
134
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
135
|
+
return 0;
|
|
136
|
+
});
|
|
137
|
+
return ids[0] ?? null;
|
|
94
138
|
}
|
|
95
139
|
|
|
96
|
-
function
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// "claude-<family>-<N>" ids (no date suffix) and take the highest one.
|
|
101
|
-
const candidates = Object.keys(models).filter((k) => k.startsWith(`claude-${family}-`) && /^claude-[a-z]+-[\d.]+$/.test(k));
|
|
102
|
-
candidates.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
103
|
-
return candidates[0] ?? null;
|
|
140
|
+
function cleanRates(r) {
|
|
141
|
+
const out = {};
|
|
142
|
+
for (const k of RATE_KEYS) out[k] = Number(r?.[k]) || 0;
|
|
143
|
+
return out;
|
|
104
144
|
}
|
|
105
145
|
|
|
106
|
-
|
|
107
|
-
if (!
|
|
108
|
-
|
|
146
|
+
function findOverride(rawModel, overrides) {
|
|
147
|
+
if (!overrides) return null;
|
|
148
|
+
if (overrides[rawModel]) return overrides[rawModel];
|
|
149
|
+
const n = normalizeModelId(rawModel);
|
|
150
|
+
for (const [k, v] of Object.entries(overrides)) if (normalizeModelId(k) === n) return v;
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
109
153
|
|
|
110
|
-
|
|
111
|
-
|
|
154
|
+
// Returns { rates, fidelity, suggested }
|
|
155
|
+
// fidelity: override | measured | approximate-alias | unpriced
|
|
156
|
+
// unpriced → rates are all zero; `suggested` carries the public list price
|
|
157
|
+
// when one exists so the Pricing page can offer it.
|
|
158
|
+
export function lookupPricing(rawModel, table) {
|
|
159
|
+
if (!rawModel) return { fidelity: 'unpriced', rates: cleanRates(null), suggested: null };
|
|
160
|
+
const models = table.models ?? {};
|
|
161
|
+
const override = findOverride(rawModel, table.overrides);
|
|
162
|
+
const n = normalizeModelId(rawModel);
|
|
163
|
+
const claude = isClaudeModel(rawModel);
|
|
164
|
+
|
|
165
|
+
let publicRates = models[n] ?? models[rawModel] ?? null;
|
|
166
|
+
let fidelity = 'measured';
|
|
167
|
+
if (!publicRates && claude) {
|
|
168
|
+
const family = ALIAS_FAMILIES.find((f) => n === f || n.startsWith(f + '-') || n === `claude-${f}`);
|
|
169
|
+
const target = family ? newestInFamily(family, models) : null;
|
|
170
|
+
if (target) {
|
|
171
|
+
publicRates = models[target];
|
|
172
|
+
fidelity = 'approximate-alias';
|
|
173
|
+
}
|
|
112
174
|
}
|
|
113
175
|
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
return { fidelity: 'approximate-alias', rates, displayName: `${rates.displayName} (via alias "${rawModel}")` };
|
|
176
|
+
if (override) return { fidelity: 'override', rates: cleanRates(override), suggested: publicRates ? cleanRates(publicRates) : null };
|
|
177
|
+
if (publicRates && claude) {
|
|
178
|
+
return { fidelity, rates: cleanRates(publicRates), suggested: null };
|
|
118
179
|
}
|
|
180
|
+
// Non-Claude model behind the Claude Code harness (gateway, on-prem, local):
|
|
181
|
+
// we can't know what it costs this user, so it's $0 until they say otherwise.
|
|
182
|
+
return { fidelity: 'unpriced', rates: cleanRates(null), suggested: publicRates ? cleanRates(publicRates) : null };
|
|
183
|
+
}
|
|
119
184
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
185
|
+
// Looks a model id up in the public price list: exact (after normalizing
|
|
186
|
+
// provider prefixes / dates), else the newest Claude of the same family,
|
|
187
|
+
// else close matches the user can pick from.
|
|
188
|
+
export function lookupPublic(rawModel, table) {
|
|
189
|
+
const models = table.models ?? {};
|
|
190
|
+
const n = normalizeModelId(rawModel);
|
|
191
|
+
const exact = models[n] ?? models[String(rawModel).trim().toLowerCase()];
|
|
192
|
+
if (exact) {
|
|
193
|
+
const id = models[n] ? n : String(rawModel).trim().toLowerCase();
|
|
194
|
+
return { found: true, match: 'exact', id, rates: cleanRates(exact), provider: exact.provider ?? null, sourceKey: exact.sourceKey ?? id };
|
|
128
195
|
}
|
|
129
|
-
|
|
130
|
-
|
|
196
|
+
const q = n.replace(/[^a-z0-9]/g, '');
|
|
197
|
+
const common = (k) => { let i = 0; while (i < q.length && i < k.length && q[i] === k[i]) i++; return i; };
|
|
198
|
+
const scored = [];
|
|
199
|
+
for (const id of Object.keys(models)) {
|
|
200
|
+
const k = id.replace(/[^a-z0-9]/g, '');
|
|
201
|
+
const score = q.length >= 3 && (k.includes(q) || q.includes(k)) ? 100 : common(k);
|
|
202
|
+
if (score >= 5) scored.push([id, score]);
|
|
203
|
+
}
|
|
204
|
+
scored.sort((x, y) => y[1] - x[1] || x[0].length - y[0].length);
|
|
205
|
+
const candidates = scored.slice(0, 8).map(([id]) => ({ id, rates: cleanRates(models[id]), provider: models[id].provider ?? null }));
|
|
206
|
+
return { found: false, match: 'none', id: n, candidates };
|
|
131
207
|
}
|
|
132
208
|
|
|
133
209
|
export function costFor(usage, rates) {
|
|
134
210
|
if (!rates) return null;
|
|
135
211
|
const mtok = 1_000_000;
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const cacheWrite = (cacheWrite5m / mtok) * rates.cacheWrite5m + (cacheWrite1h / mtok) * rates.cacheWrite1h;
|
|
141
|
-
const cacheRead = (usage.cacheRead / mtok) * rates.cacheRead;
|
|
212
|
+
const input = ((usage.input ?? 0) / mtok) * rates.input;
|
|
213
|
+
const output = ((usage.output ?? 0) / mtok) * rates.output;
|
|
214
|
+
const cacheWrite = ((usage.cacheWrite5m ?? 0) / mtok) * rates.cacheWrite5m + ((usage.cacheWrite1h ?? 0) / mtok) * rates.cacheWrite1h;
|
|
215
|
+
const cacheRead = ((usage.cacheRead ?? 0) / mtok) * rates.cacheRead;
|
|
142
216
|
return { input, output, cacheWrite, cacheRead, total: input + output + cacheWrite + cacheRead };
|
|
143
217
|
}
|
|
218
|
+
|
|
219
|
+
export function pricingInfo(table) {
|
|
220
|
+
return { source: table.source, fetchedAt: table.fetchedAt ?? null, error: table.error ?? null, url: SOURCE_URL, modelCount: Object.keys(table.models ?? {}).length };
|
|
221
|
+
}
|