pi-cost 0.1.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 +46 -0
- package/extensions/cost.ts +181 -0
- package/extensions/package.json +1 -0
- package/extensions/tsconfig.json +16 -0
- package/lib/pi-parsers-cost.js +297 -0
- package/lib/pi-pricing.js +197 -0
- package/package.json +61 -0
- package/public/app.js +1290 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon-svg.svg +7 -0
- package/public/index.html +109 -0
- package/public/manifest.json +26 -0
- package/public/style.css +420 -0
- package/public/sw.js +60 -0
- package/server.js +673 -0
- package/themes/classic-dark.json +21 -0
- package/themes/classic-light.json +21 -0
- package/themes/pi-dark.json +21 -0
- package/themes/pi-light.json +21 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const LITELLM_PRICING_URL =
|
|
4
|
+
'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
|
|
5
|
+
|
|
6
|
+
const TIERED_THRESHOLD = 200_000;
|
|
7
|
+
const PRICING_REFRESH_MS = 6 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
const PI_ROUTING_PROVIDERS = new Set(['dial', 'exa', 'github-copilot', 'github_copilot', 'auto']);
|
|
10
|
+
|
|
11
|
+
// Strip pi-side decoration so we can match LiteLLM canonical keys.
|
|
12
|
+
// Examples: claude-sonnet-4.6 -> claude-sonnet-4-5 (pi uses dotted minor)
|
|
13
|
+
function piIdNormalizations(modelId) {
|
|
14
|
+
if (!modelId) return [];
|
|
15
|
+
const lower = String(modelId).toLowerCase().trim();
|
|
16
|
+
const out = new Set();
|
|
17
|
+
out.add(lower);
|
|
18
|
+
const dotToDash = lower.replace(/(\d)\.(\d)/g, '$1-$2');
|
|
19
|
+
out.add(dotToDash);
|
|
20
|
+
// claude-sonnet-4.6 -> claude-sonnet-4-5 (pi numbering, LiteLLM uses prior step)
|
|
21
|
+
const stripMinor = lower.replace(/(claude-(?:sonnet|haiku|opus)-\d+)\.\d+/g, '$1');
|
|
22
|
+
out.add(stripMinor);
|
|
23
|
+
return [...out];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const OFFLINE_PRICING = {
|
|
27
|
+
'anthropic/claude-sonnet-4-5': {
|
|
28
|
+
input_cost_per_token: 3e-6, output_cost_per_token: 1.5e-5,
|
|
29
|
+
cache_creation_input_token_cost: 3.75e-6, cache_read_input_token_cost: 3e-7,
|
|
30
|
+
input_cost_per_token_above_200k_tokens: 6e-6, output_cost_per_token_above_200k_tokens: 2.25e-5,
|
|
31
|
+
cache_creation_input_token_cost_above_200k_tokens: 7.5e-6, cache_read_input_token_cost_above_200k_tokens: 6e-7,
|
|
32
|
+
},
|
|
33
|
+
'anthropic/claude-haiku-4-5': {
|
|
34
|
+
input_cost_per_token: 8e-7, output_cost_per_token: 4e-6,
|
|
35
|
+
cache_creation_input_token_cost: 1e-6, cache_read_input_token_cost: 8e-8,
|
|
36
|
+
},
|
|
37
|
+
'anthropic/claude-opus-4-1': {
|
|
38
|
+
input_cost_per_token: 1.5e-5, output_cost_per_token: 7.5e-5,
|
|
39
|
+
cache_creation_input_token_cost: 1.875e-5, cache_read_input_token_cost: 1.5e-6,
|
|
40
|
+
},
|
|
41
|
+
'openai/gpt-5': { input_cost_per_token: 2.5e-6, output_cost_per_token: 1e-5, cache_read_input_token_cost: 2.5e-7 },
|
|
42
|
+
'openai/gpt-5-mini': { input_cost_per_token: 2.5e-7, output_cost_per_token: 2e-6, cache_read_input_token_cost: 2.5e-8 },
|
|
43
|
+
'openai/gpt-4': { input_cost_per_token: 3e-5, output_cost_per_token: 6e-5 },
|
|
44
|
+
'openai/gpt-4o': { input_cost_per_token: 2.5e-6, output_cost_per_token: 1e-5 },
|
|
45
|
+
'openai/gpt-4o-mini': { input_cost_per_token: 1.5e-7, output_cost_per_token: 6e-7 },
|
|
46
|
+
'gemini/gemini-2.5-pro': { input_cost_per_token: 1.25e-6, output_cost_per_token: 1e-5 },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
let cachedPricing = null;
|
|
50
|
+
let pricingFetchedAt = 0;
|
|
51
|
+
let pricingSource = 'none';
|
|
52
|
+
|
|
53
|
+
async function fetchPricing() {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
if (cachedPricing && (now - pricingFetchedAt) < PRICING_REFRESH_MS) return cachedPricing;
|
|
56
|
+
try {
|
|
57
|
+
const resp = await fetch(LITELLM_PRICING_URL);
|
|
58
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
59
|
+
const data = await resp.json();
|
|
60
|
+
const pricing = new Map();
|
|
61
|
+
for (const [name, info] of Object.entries(data)) {
|
|
62
|
+
if (typeof info !== 'object' || info == null) continue;
|
|
63
|
+
if (info.input_cost_per_token != null || info.output_cost_per_token != null) {
|
|
64
|
+
pricing.set(name, info);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
cachedPricing = pricing;
|
|
68
|
+
pricingFetchedAt = now;
|
|
69
|
+
pricingSource = 'litellm';
|
|
70
|
+
console.log(`[pi-cost pricing] loaded ${pricing.size} models from LiteLLM`);
|
|
71
|
+
return pricing;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.warn('[pi-cost pricing] fetch failed, using offline fallback:', err.message);
|
|
74
|
+
if (!cachedPricing) {
|
|
75
|
+
cachedPricing = new Map(Object.entries(OFFLINE_PRICING));
|
|
76
|
+
pricingFetchedAt = now;
|
|
77
|
+
pricingSource = 'offline';
|
|
78
|
+
}
|
|
79
|
+
return cachedPricing;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function refresh() {
|
|
84
|
+
cachedPricing = null;
|
|
85
|
+
pricingFetchedAt = 0;
|
|
86
|
+
pricingSource = 'none';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getMeta() {
|
|
90
|
+
return {
|
|
91
|
+
fetchedAt: pricingFetchedAt ? new Date(pricingFetchedAt).toISOString() : null,
|
|
92
|
+
source: pricingSource,
|
|
93
|
+
modelCount: cachedPricing ? cachedPricing.size : 0,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function lookupExact(pricing, key) {
|
|
98
|
+
if (!key) return null;
|
|
99
|
+
const direct = pricing.get(key);
|
|
100
|
+
if (direct) return { key, info: direct };
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveKey(pricing, provider, modelId) {
|
|
105
|
+
if (!pricing || !modelId) return null;
|
|
106
|
+
const candidates = piIdNormalizations(modelId);
|
|
107
|
+
const providerLower = provider ? String(provider).toLowerCase() : null;
|
|
108
|
+
const isRouting = providerLower ? PI_ROUTING_PROVIDERS.has(providerLower) : false;
|
|
109
|
+
|
|
110
|
+
// 1. Exact match on every normalized id.
|
|
111
|
+
for (const c of candidates) {
|
|
112
|
+
const hit = lookupExact(pricing, c);
|
|
113
|
+
if (hit) return hit;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. provider/model when the provider is a real upstream (not a pi billing route).
|
|
117
|
+
if (providerLower && !isRouting) {
|
|
118
|
+
for (const c of candidates) {
|
|
119
|
+
const hit = lookupExact(pricing, `${providerLower}/${c}`);
|
|
120
|
+
if (hit) return hit;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 3. Anthropic / openai / gemini prefixes for unrouted pi model IDs.
|
|
125
|
+
const knownPrefixes = ['anthropic/', 'openai/', 'gemini/', 'mistral/', 'groq/'];
|
|
126
|
+
for (const c of candidates) {
|
|
127
|
+
for (const p of knownPrefixes) {
|
|
128
|
+
const hit = lookupExact(pricing, `${p}${c}`);
|
|
129
|
+
if (hit) return hit;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 4. Fuzzy: longest LiteLLM key whose tail-segment matches any candidate.
|
|
134
|
+
let best = null;
|
|
135
|
+
let bestScore = 0;
|
|
136
|
+
for (const c of candidates) {
|
|
137
|
+
for (const [key, info] of pricing) {
|
|
138
|
+
const kLower = key.toLowerCase();
|
|
139
|
+
const tail = kLower.split('/').pop();
|
|
140
|
+
// exact tail match wins
|
|
141
|
+
if (tail === c && c.length > bestScore) {
|
|
142
|
+
best = { key, info }; bestScore = c.length + 10;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (tail && (tail.includes(c) || c.includes(tail))) {
|
|
146
|
+
const score = Math.min(tail.length, c.length);
|
|
147
|
+
if (score > bestScore) { best = { key, info }; bestScore = score; }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return best;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function calculateTieredCost(tokens, basePrice, tieredPrice, threshold = TIERED_THRESHOLD) {
|
|
155
|
+
if (!tokens || tokens <= 0) return 0;
|
|
156
|
+
if (tokens > threshold && tieredPrice != null) {
|
|
157
|
+
const below = Math.min(tokens, threshold);
|
|
158
|
+
const above = Math.max(0, tokens - threshold);
|
|
159
|
+
let cost = above * tieredPrice;
|
|
160
|
+
if (basePrice != null) cost += below * basePrice;
|
|
161
|
+
return cost;
|
|
162
|
+
}
|
|
163
|
+
if (basePrice != null) return tokens * basePrice;
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function computeCost(info, usage) {
|
|
168
|
+
const input = calculateTieredCost(
|
|
169
|
+
usage.input || 0, info.input_cost_per_token,
|
|
170
|
+
info.input_cost_per_token_above_200k_tokens);
|
|
171
|
+
const output = calculateTieredCost(
|
|
172
|
+
usage.output || 0, info.output_cost_per_token,
|
|
173
|
+
info.output_cost_per_token_above_200k_tokens);
|
|
174
|
+
const cacheWrite = calculateTieredCost(
|
|
175
|
+
usage.cacheWrite || 0, info.cache_creation_input_token_cost,
|
|
176
|
+
info.cache_creation_input_token_cost_above_200k_tokens);
|
|
177
|
+
const cacheRead = calculateTieredCost(
|
|
178
|
+
usage.cacheRead || 0, info.cache_read_input_token_cost,
|
|
179
|
+
info.cache_read_input_token_cost_above_200k_tokens);
|
|
180
|
+
return input + output + cacheWrite + cacheRead;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Pi-shaped usage: { input, output, cacheRead, cacheWrite, cost: { total } }
|
|
184
|
+
// Returns { cost, source, modelKey }
|
|
185
|
+
function estimate(pricing, provider, modelId, usage) {
|
|
186
|
+
if (!usage) return { cost: 0, source: 'none', modelKey: null };
|
|
187
|
+
const jsonlTotal = usage.cost && typeof usage.cost.total === 'number' ? usage.cost.total : 0;
|
|
188
|
+
if (jsonlTotal > 0) {
|
|
189
|
+
return { cost: jsonlTotal, source: 'jsonl', modelKey: null };
|
|
190
|
+
}
|
|
191
|
+
const match = resolveKey(pricing, provider, modelId);
|
|
192
|
+
if (!match) return { cost: 0, source: 'none', modelKey: null };
|
|
193
|
+
const cost = computeCost(match.info, usage);
|
|
194
|
+
return { cost, source: 'pricing', modelKey: match.key };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { fetchPricing, refresh, getMeta, estimate, resolveKey };
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-cost",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cost dashboard for the pi coding agent",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"cost",
|
|
9
|
+
"dashboard",
|
|
10
|
+
"litellm",
|
|
11
|
+
"observability"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "nikiforovall",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/NikiforovAll/pi-cost.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/NikiforovAll/pi-cost/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://nikiforovall.github.io/pi-cost/",
|
|
23
|
+
"bin": { "pi-cost": "./server.js" },
|
|
24
|
+
"main": "server.js",
|
|
25
|
+
"files": [
|
|
26
|
+
"server.js",
|
|
27
|
+
"lib/",
|
|
28
|
+
"public/",
|
|
29
|
+
"extensions/",
|
|
30
|
+
"themes/",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"start": "node server.js",
|
|
35
|
+
"dev": "node server.js --open",
|
|
36
|
+
"docs:dev": "vitepress dev docs",
|
|
37
|
+
"docs:build": "vitepress build docs",
|
|
38
|
+
"docs:preview": "vitepress preview docs"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"express": "^4.19.2",
|
|
42
|
+
"open": "^10.1.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"vitepress": "^1.6.4"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"@earendil-works/pi-coding-agent": { "optional": true }
|
|
52
|
+
},
|
|
53
|
+
"pi": {
|
|
54
|
+
"extensions": ["./extensions"],
|
|
55
|
+
"image": "https://raw.githubusercontent.com/NikiforovAll/pi-cost/main/assets/overview.png",
|
|
56
|
+
"video": "https://raw.githubusercontent.com/NikiforovAll/pi-cost/main/assets/demo.mp4"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=20.0.0"
|
|
60
|
+
}
|
|
61
|
+
}
|