openuse 0.1.0 → 1.0.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 +1 -34
- package/dist/index.js +19 -0
- package/package.json +31 -6
- package/pnpm-workspace.yaml +0 -3
- package/src/index.ts +0 -281
- package/tsconfig.json +0 -11
package/README.md
CHANGED
|
@@ -1,34 +1 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
Estimate OpenCode token costs per day and per model by combining:
|
|
4
|
-
|
|
5
|
-
- local OpenCode usage from SQLite (`part` + `message` tables)
|
|
6
|
-
- live OpenRouter model pricing from `https://openrouter.ai/api/v1/models`
|
|
7
|
-
|
|
8
|
-
## Run
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
npm install
|
|
12
|
-
npm start
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Use a custom DB path:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm start -- "C:\\Users\\Sayad\\.local\\share\\opencode\\opencode.db"
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Or with env var:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
OPENCODE_DB_PATH="C:\\Users\\Sayad\\.local\\share\\opencode\\opencode.db" npm start
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Output
|
|
28
|
-
|
|
29
|
-
The script prints:
|
|
30
|
-
|
|
31
|
-
- per day/model table with input/output/cache tokens and estimated cost
|
|
32
|
-
- per day totals table
|
|
33
|
-
|
|
34
|
-
Rows that cannot be matched to an OpenRouter model are marked as `unmatched` and excluded from cost totals.
|
|
1
|
+
# npm package
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import e from"node:os";import t from"node:path";import n from"better-sqlite3";function r(e,t,n,r,i,a,o){try{var s=e[a](o),c=s.value}catch(e){n(e);return}s.done?t(c):Promise.resolve(c).then(r,i)}function i(e){return function(){var t=this,n=arguments;return new Promise(function(i,a){var o=e.apply(t,n);function s(e){r(o,i,a,s,c,`next`,e)}function c(e){r(o,i,a,s,c,`throw`,e)}s(void 0)})}}function a(e){"@babel/helpers - typeof";return a=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},a(e)}function o(e,t){if(a(e)!=`object`||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||`default`);if(a(r)!=`object`)return r;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function s(e){var t=o(e,`string`);return a(t)==`symbol`?t:t+``}function c(e,t,n){return(t=s(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function l(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function u(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]==null?{}:arguments[t];t%2?l(Object(n),!0).forEach(function(t){c(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):l(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}function d(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,``)}function f(e){return e.toLowerCase().replace(/([a-z])([0-9])/g,`$1 $2`).replace(/([0-9])([a-z])/g,`$1 $2`).replace(/[^a-z0-9]+/g,` `).trim().split(/\s+/).filter(Boolean)}function p(e){let t=new n(e,{readonly:!0}),r=t.prepare(`
|
|
2
|
+
SELECT
|
|
3
|
+
date(datetime(json_extract(m.data, '$.time.created') / 1000, 'unixepoch', 'localtime')) AS day,
|
|
4
|
+
COALESCE(json_extract(m.data, '$.modelID'), 'unknown') AS model,
|
|
5
|
+
SUM(COALESCE(json_extract(p.data, '$.tokens.input'), 0)) AS input_tokens,
|
|
6
|
+
SUM(COALESCE(json_extract(p.data, '$.tokens.output'), 0)) AS output_tokens,
|
|
7
|
+
SUM(COALESCE(json_extract(p.data, '$.tokens.reasoning'), 0)) AS reasoning_tokens,
|
|
8
|
+
SUM(COALESCE(json_extract(p.data, '$.tokens.cache.read'), 0)) AS cache_read_tokens,
|
|
9
|
+
SUM(COALESCE(json_extract(p.data, '$.tokens.cache.write'), 0)) AS cache_write_tokens,
|
|
10
|
+
SUM(COALESCE(json_extract(p.data, '$.tokens.total'), 0)) AS total_tokens,
|
|
11
|
+
COUNT(*) AS steps
|
|
12
|
+
FROM part p
|
|
13
|
+
JOIN message m ON m.id = p.message_id
|
|
14
|
+
WHERE json_extract(p.data, '$.type') = 'step-finish'
|
|
15
|
+
AND json_extract(m.data, '$.time.created') IS NOT NULL
|
|
16
|
+
GROUP BY day, model
|
|
17
|
+
ORDER BY day DESC, total_tokens DESC
|
|
18
|
+
`).all();return t.close(),r}function m(){return h.apply(this,arguments)}function h(){return h=i(function*(){var e;let t=yield fetch(`https://openrouter.ai/api/v1/models`);if(!t.ok)throw Error(`OpenRouter API failed with ${t.status}`);return(e=(yield t.json()).data)==null?[]:e}),h.apply(this,arguments)}function g(e){let t=new Map,n=new Map,r=[];for(let l of e){var i,a,o,s,c;t.set(l.id,l);let e=[l.id,(i=l.name)==null?``:i,(a=l.id.split(`/`).at(-1))==null?``:a,(o=(s=((c=l.name)==null?``:c).split(`:`).at(-1))==null?void 0:s.trim())==null?``:o],u=[],p=[];for(let t of e){if(!t)continue;let e=d(t);u.push(e),p.push(f(t)),n.has(e)||n.set(e,l)}r.push({model:l,normalizedKeys:u,tokenKeys:p})}return{byId:t,byNormalized:n,candidates:r}}function _(e,t){if(t.byId.has(e)){var n;return(n=t.byId.get(e))==null?null:n}let r=d(e);if(t.byNormalized.has(r)){var i;return(i=t.byNormalized.get(r))==null?null:i}let a=null,o=new Set(f(e));for(let e of t.candidates){let t=0;for(let n of e.normalizedKeys)if(n&&(n.includes(r)||r.includes(n))){let e=Math.min(n.length,r.length)/Math.max(n.length,r.length);e>t&&(t=e)}for(let n of e.tokenKeys){if(n.length===0||o.size===0)continue;let e=0;for(let t of n)o.has(t)&&(e+=1);let r=e/Math.max(n.length,o.size);r>t&&(t=r)}(!a||t>a.score)&&(a={model:e.model,score:t})}return a&&a.score>=.7?a.model:null}function v(e){if(!e)return 0;let t=Number(e);return Number.isFinite(t)?t:0}function y(e,t){let n=g(t);return e.map(e=>{var t;let r=_(e.model,n);if(!r)return u(u({},e),{},{matched_model_id:null,prompt_rate:0,completion_rate:0,cache_read_rate:0,cache_write_rate:0,cost_usd:null});let i=(t=r.pricing)==null?{}:t,a=v(i.prompt),o=v(i.completion),s=v(i.input_cache_read),c=v(i.input_cache_write||i.input_cache_creation||i.cache_write),l=e.input_tokens*a+e.output_tokens*o+e.cache_read_tokens*s+e.cache_write_tokens*c;return u(u({},e),{},{matched_model_id:r.id,prompt_rate:a,completion_rate:o,cache_read_rate:s,cache_write_rate:c,cost_usd:l})})}function b(e){return e===null?null:Number(e.toFixed(6))}function x(e,t){let n=e.map(e=>{var t;return{day:e.day,model:e.model,matched:(t=e.matched_model_id)==null?`unmatched`:t,input:e.input_tokens,output:e.output_tokens,cache_read:e.cache_read_tokens,cache_write:e.cache_write_tokens,total_tokens:e.total_tokens,cost_usd:b(e.cost_usd)}}),r=new Map;for(let t of e){var i;let e=(i=r.get(t.day))==null?{cost:0,totalTokens:0,unmatchedRows:0}:i;e.totalTokens+=t.total_tokens,t.cost_usd===null?e.unmatchedRows+=1:e.cost+=t.cost_usd,r.set(t.day,e)}let a=[...r.entries()].sort((e,t)=>t[0].localeCompare(e[0])).map(([e,t])=>({day:e,total_tokens:t.totalTokens,estimated_cost_usd:Number(t.cost.toFixed(6)),unmatched_models:t.unmatchedRows}));console.log(`Database: ${t}`),console.log(`
|
|
19
|
+
Per day/model:`),console.table(n),console.log(`Daily totals:`),console.table(a)}var S,C;const w=(S=(C=process.argv[2])==null?process.env.OPENCODE_DB_PATH:C)==null?t.join(e.homedir(),`.local`,`share`,`opencode`,`opencode.db`):S;function T(){return E.apply(this,arguments)}function E(){return E=i(function*(){let e=p(w);if(e.length===0){console.log(`No usage rows found in ${w}`);return}x(y(e,yield m()),w)}),E.apply(this,arguments)}T().catch(e=>{let t=e instanceof Error?e.message:String(e);console.error(`Failed: ${t}`),process.exit(1)});export{};
|
package/package.json
CHANGED
|
@@ -1,18 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openuse",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"
|
|
7
|
-
"
|
|
6
|
+
"test": "bun test",
|
|
7
|
+
"lint": "eslint .",
|
|
8
|
+
"fix": "eslint . --fix",
|
|
9
|
+
"tc": "tsc --noEmit --watch",
|
|
10
|
+
"build": "tsdown --config ./tsdown.config.ts",
|
|
11
|
+
"dev": "concurrently --names \"R,T\" --prefix-colors \"blue.dim,magenta.dim\" \"tsdown --config ./tsdown.config.ts --watch\" \"tsc --noEmit --watch\"",
|
|
12
|
+
"start": "tsx --watch ./src/index.ts"
|
|
8
13
|
},
|
|
14
|
+
"bin": "./dist/index.js",
|
|
9
15
|
"dependencies": {
|
|
10
|
-
"better-sqlite3": "^12.
|
|
16
|
+
"better-sqlite3": "^12.8.0"
|
|
11
17
|
},
|
|
12
18
|
"devDependencies": {
|
|
19
|
+
"@eslint/compat": "^1.4.0",
|
|
20
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
21
|
+
"@eslint/js": "^9.38.0",
|
|
13
22
|
"@types/better-sqlite3": "^7.6.13",
|
|
14
|
-
"@types/
|
|
23
|
+
"@types/bun": "^1.3.0",
|
|
24
|
+
"@types/node": "^24.9.1",
|
|
25
|
+
"@typescript-eslint/eslint-plugin": "^8.46.1",
|
|
26
|
+
"@typescript-eslint/parser": "^8.46.1",
|
|
27
|
+
"concurrently": "^9.2.1",
|
|
28
|
+
"eslint": "^9.38.0",
|
|
29
|
+
"eslint-config-prettier": "^10.1.8",
|
|
30
|
+
"eslint-plugin-check-file": "^3.3.0",
|
|
31
|
+
"eslint-plugin-import": "^2.32.0",
|
|
32
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
33
|
+
"husky": "^9.1.7",
|
|
34
|
+
"prettier": "^3.6.2",
|
|
35
|
+
"prettier-plugin-organize-imports": "^4.3.0",
|
|
36
|
+
"rolldown": "1.0.0-rc.13",
|
|
37
|
+
"tsdown": "^0.15.9",
|
|
38
|
+
"tslib": "^2.8.1",
|
|
15
39
|
"tsx": "^4.20.6",
|
|
16
|
-
"typescript": "^5.9.3"
|
|
40
|
+
"typescript": "^5.9.3",
|
|
41
|
+
"typescript-eslint": "^8.46.2"
|
|
17
42
|
}
|
|
18
43
|
}
|
package/pnpm-workspace.yaml
DELETED
package/src/index.ts
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import Database from "better-sqlite3";
|
|
4
|
-
|
|
5
|
-
type UsageRow = {
|
|
6
|
-
day: string;
|
|
7
|
-
model: string;
|
|
8
|
-
input_tokens: number;
|
|
9
|
-
output_tokens: number;
|
|
10
|
-
reasoning_tokens: number;
|
|
11
|
-
cache_read_tokens: number;
|
|
12
|
-
cache_write_tokens: number;
|
|
13
|
-
total_tokens: number;
|
|
14
|
-
steps: number;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
type OpenRouterModel = {
|
|
18
|
-
id: string;
|
|
19
|
-
name?: string;
|
|
20
|
-
pricing?: Record<string, string>;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type PricedRow = UsageRow & {
|
|
24
|
-
matched_model_id: string | null;
|
|
25
|
-
prompt_rate: number;
|
|
26
|
-
completion_rate: number;
|
|
27
|
-
cache_read_rate: number;
|
|
28
|
-
cache_write_rate: number;
|
|
29
|
-
cost_usd: number | null;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const dbPath = process.argv[2] ?? process.env.OPENCODE_DB_PATH ?? path.join(os.homedir(), ".local", "share", "opencode", "opencode.db");
|
|
33
|
-
|
|
34
|
-
function normalize(value: string) {
|
|
35
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function tokenize(value: string) {
|
|
39
|
-
return value
|
|
40
|
-
.toLowerCase()
|
|
41
|
-
.replace(/([a-z])([0-9])/g, "$1 $2")
|
|
42
|
-
.replace(/([0-9])([a-z])/g, "$1 $2")
|
|
43
|
-
.replace(/[^a-z0-9]+/g, " ")
|
|
44
|
-
.trim()
|
|
45
|
-
.split(/\s+/)
|
|
46
|
-
.filter(Boolean);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function readUsage(file: string) {
|
|
50
|
-
const db = new Database(file, { readonly: true });
|
|
51
|
-
const sql = `
|
|
52
|
-
SELECT
|
|
53
|
-
date(datetime(json_extract(m.data, '$.time.created') / 1000, 'unixepoch', 'localtime')) AS day,
|
|
54
|
-
COALESCE(json_extract(m.data, '$.modelID'), 'unknown') AS model,
|
|
55
|
-
SUM(COALESCE(json_extract(p.data, '$.tokens.input'), 0)) AS input_tokens,
|
|
56
|
-
SUM(COALESCE(json_extract(p.data, '$.tokens.output'), 0)) AS output_tokens,
|
|
57
|
-
SUM(COALESCE(json_extract(p.data, '$.tokens.reasoning'), 0)) AS reasoning_tokens,
|
|
58
|
-
SUM(COALESCE(json_extract(p.data, '$.tokens.cache.read'), 0)) AS cache_read_tokens,
|
|
59
|
-
SUM(COALESCE(json_extract(p.data, '$.tokens.cache.write'), 0)) AS cache_write_tokens,
|
|
60
|
-
SUM(COALESCE(json_extract(p.data, '$.tokens.total'), 0)) AS total_tokens,
|
|
61
|
-
COUNT(*) AS steps
|
|
62
|
-
FROM part p
|
|
63
|
-
JOIN message m ON m.id = p.message_id
|
|
64
|
-
WHERE json_extract(p.data, '$.type') = 'step-finish'
|
|
65
|
-
AND json_extract(m.data, '$.time.created') IS NOT NULL
|
|
66
|
-
GROUP BY day, model
|
|
67
|
-
ORDER BY day DESC, total_tokens DESC
|
|
68
|
-
`;
|
|
69
|
-
const rows = db.prepare(sql).all() as UsageRow[];
|
|
70
|
-
db.close();
|
|
71
|
-
return rows;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function fetchOpenRouterModels() {
|
|
75
|
-
const response = await fetch("https://openrouter.ai/api/v1/models");
|
|
76
|
-
if (!response.ok) {
|
|
77
|
-
throw new Error(`OpenRouter API failed with ${response.status}`);
|
|
78
|
-
}
|
|
79
|
-
const payload = (await response.json()) as { data?: OpenRouterModel[] };
|
|
80
|
-
return payload.data ?? [];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function buildModelIndex(models: OpenRouterModel[]) {
|
|
84
|
-
const byId = new Map<string, OpenRouterModel>();
|
|
85
|
-
const byNormalized = new Map<string, OpenRouterModel>();
|
|
86
|
-
const candidates: { model: OpenRouterModel; normalizedKeys: string[]; tokenKeys: string[][] }[] = [];
|
|
87
|
-
|
|
88
|
-
for (const model of models) {
|
|
89
|
-
byId.set(model.id, model);
|
|
90
|
-
const keys = [
|
|
91
|
-
model.id,
|
|
92
|
-
model.name ?? "",
|
|
93
|
-
model.id.split("/").at(-1) ?? "",
|
|
94
|
-
(model.name ?? "").split(":").at(-1)?.trim() ?? ""
|
|
95
|
-
];
|
|
96
|
-
const normalizedKeys: string[] = [];
|
|
97
|
-
const tokenKeys: string[][] = [];
|
|
98
|
-
|
|
99
|
-
for (const key of keys) {
|
|
100
|
-
if (!key) {
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
const normalizedKey = normalize(key);
|
|
104
|
-
normalizedKeys.push(normalizedKey);
|
|
105
|
-
tokenKeys.push(tokenize(key));
|
|
106
|
-
if (!byNormalized.has(normalizedKey)) {
|
|
107
|
-
byNormalized.set(normalizedKey, model);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
candidates.push({ model, normalizedKeys, tokenKeys });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return { byId, byNormalized, candidates };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function matchModel(inputModel: string, index: ReturnType<typeof buildModelIndex>) {
|
|
118
|
-
if (index.byId.has(inputModel)) {
|
|
119
|
-
return index.byId.get(inputModel) ?? null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const normalized = normalize(inputModel);
|
|
123
|
-
if (index.byNormalized.has(normalized)) {
|
|
124
|
-
return index.byNormalized.get(normalized) ?? null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
let best: { model: OpenRouterModel; score: number } | null = null;
|
|
128
|
-
const inputTokens = new Set(tokenize(inputModel));
|
|
129
|
-
|
|
130
|
-
for (const candidate of index.candidates) {
|
|
131
|
-
let score = 0;
|
|
132
|
-
|
|
133
|
-
for (const key of candidate.normalizedKeys) {
|
|
134
|
-
if (!key) {
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (key.includes(normalized) || normalized.includes(key)) {
|
|
138
|
-
const ratio = Math.min(key.length, normalized.length) / Math.max(key.length, normalized.length);
|
|
139
|
-
if (ratio > score) {
|
|
140
|
-
score = ratio;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
for (const keyTokens of candidate.tokenKeys) {
|
|
146
|
-
if (keyTokens.length === 0 || inputTokens.size === 0) {
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
let overlap = 0;
|
|
150
|
-
for (const token of keyTokens) {
|
|
151
|
-
if (inputTokens.has(token)) {
|
|
152
|
-
overlap += 1;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const tokenScore = overlap / Math.max(keyTokens.length, inputTokens.size);
|
|
156
|
-
if (tokenScore > score) {
|
|
157
|
-
score = tokenScore;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (!best || score > best.score) {
|
|
162
|
-
best = { model: candidate.model, score };
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return best && best.score >= 0.7 ? best.model : null;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function toNumber(value: string | undefined) {
|
|
170
|
-
if (!value) {
|
|
171
|
-
return 0;
|
|
172
|
-
}
|
|
173
|
-
const parsed = Number(value);
|
|
174
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function priceRows(rows: UsageRow[], models: OpenRouterModel[]) {
|
|
178
|
-
const index = buildModelIndex(models);
|
|
179
|
-
|
|
180
|
-
return rows.map((row) => {
|
|
181
|
-
const matched = matchModel(row.model, index);
|
|
182
|
-
if (!matched) {
|
|
183
|
-
return {
|
|
184
|
-
...row,
|
|
185
|
-
matched_model_id: null,
|
|
186
|
-
prompt_rate: 0,
|
|
187
|
-
completion_rate: 0,
|
|
188
|
-
cache_read_rate: 0,
|
|
189
|
-
cache_write_rate: 0,
|
|
190
|
-
cost_usd: null
|
|
191
|
-
} satisfies PricedRow;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const pricing = matched.pricing ?? {};
|
|
195
|
-
const promptRate = toNumber(pricing.prompt);
|
|
196
|
-
const completionRate = toNumber(pricing.completion);
|
|
197
|
-
const cacheReadRate = toNumber(pricing.input_cache_read);
|
|
198
|
-
const cacheWriteRate = toNumber(pricing.input_cache_write || pricing.input_cache_creation || pricing.cache_write);
|
|
199
|
-
|
|
200
|
-
const costUsd =
|
|
201
|
-
row.input_tokens * promptRate +
|
|
202
|
-
row.output_tokens * completionRate +
|
|
203
|
-
row.cache_read_tokens * cacheReadRate +
|
|
204
|
-
row.cache_write_tokens * cacheWriteRate;
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
...row,
|
|
208
|
-
matched_model_id: matched.id,
|
|
209
|
-
prompt_rate: promptRate,
|
|
210
|
-
completion_rate: completionRate,
|
|
211
|
-
cache_read_rate: cacheReadRate,
|
|
212
|
-
cache_write_rate: cacheWriteRate,
|
|
213
|
-
cost_usd: costUsd
|
|
214
|
-
} satisfies PricedRow;
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function formatMoney(value: number | null) {
|
|
219
|
-
if (value === null) {
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
return Number(value.toFixed(6));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function printReport(rows: PricedRow[]) {
|
|
226
|
-
const detail = rows.map((row) => ({
|
|
227
|
-
day: row.day,
|
|
228
|
-
model: row.model,
|
|
229
|
-
matched: row.matched_model_id ?? "unmatched",
|
|
230
|
-
input: row.input_tokens,
|
|
231
|
-
output: row.output_tokens,
|
|
232
|
-
cache_read: row.cache_read_tokens,
|
|
233
|
-
cache_write: row.cache_write_tokens,
|
|
234
|
-
total_tokens: row.total_tokens,
|
|
235
|
-
cost_usd: formatMoney(row.cost_usd)
|
|
236
|
-
}));
|
|
237
|
-
|
|
238
|
-
const totalsByDay = new Map<string, { cost: number; totalTokens: number; unmatchedRows: number }>();
|
|
239
|
-
for (const row of rows) {
|
|
240
|
-
const current = totalsByDay.get(row.day) ?? { cost: 0, totalTokens: 0, unmatchedRows: 0 };
|
|
241
|
-
current.totalTokens += row.total_tokens;
|
|
242
|
-
if (row.cost_usd === null) {
|
|
243
|
-
current.unmatchedRows += 1;
|
|
244
|
-
} else {
|
|
245
|
-
current.cost += row.cost_usd;
|
|
246
|
-
}
|
|
247
|
-
totalsByDay.set(row.day, current);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const daily = [...totalsByDay.entries()]
|
|
251
|
-
.sort((a, b) => b[0].localeCompare(a[0]))
|
|
252
|
-
.map(([day, value]) => ({
|
|
253
|
-
day,
|
|
254
|
-
total_tokens: value.totalTokens,
|
|
255
|
-
estimated_cost_usd: Number(value.cost.toFixed(6)),
|
|
256
|
-
unmatched_models: value.unmatchedRows
|
|
257
|
-
}));
|
|
258
|
-
|
|
259
|
-
console.log(`Database: ${dbPath}`);
|
|
260
|
-
console.log("\nPer day/model:");
|
|
261
|
-
console.table(detail);
|
|
262
|
-
console.log("Daily totals:");
|
|
263
|
-
console.table(daily);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
async function main() {
|
|
267
|
-
const usageRows = readUsage(dbPath);
|
|
268
|
-
if (usageRows.length === 0) {
|
|
269
|
-
console.log(`No usage rows found in ${dbPath}`);
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
const models = await fetchOpenRouterModels();
|
|
273
|
-
const pricedRows = priceRows(usageRows, models);
|
|
274
|
-
printReport(pricedRows);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
main().catch((error: unknown) => {
|
|
278
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
279
|
-
console.error(`Failed: ${message}`);
|
|
280
|
-
process.exit(1);
|
|
281
|
-
});
|