@juliantanx/aiusage 1.5.2 → 1.5.3

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.
Files changed (39) hide show
  1. package/README.md +3 -3
  2. package/dist/index.js +1037 -305
  3. package/dist/web/_app/immutable/assets/7.CahA9iix.css +1 -0
  4. package/dist/web/_app/immutable/assets/_page.CahA9iix.css +1 -0
  5. package/dist/web/_app/immutable/chunks/{ToolSelector.DTPGX4zP.js → ToolSelector.0iI1cXVj.js} +2 -2
  6. package/dist/web/_app/immutable/chunks/api.DVLAklIn.js +1 -0
  7. package/dist/web/_app/immutable/chunks/entry.DhBBmiYe.js +3 -0
  8. package/dist/web/_app/immutable/chunks/i18n.DApJIXOT.js +1 -0
  9. package/dist/web/_app/immutable/chunks/{index.66Vu9Sio.js → index.BfJlbjrK.js} +1 -1
  10. package/dist/web/_app/immutable/chunks/{stores.CK2Vof2m.js → stores.CSRSkpU3.js} +1 -1
  11. package/dist/web/_app/immutable/entry/{app.BzFxp-0D.js → app.C_5glR-X.js} +2 -2
  12. package/dist/web/_app/immutable/entry/start.B0qYhR-d.js +1 -0
  13. package/dist/web/_app/immutable/nodes/{0.5419U2xS.js → 0.DJfx41KV.js} +2 -2
  14. package/dist/web/_app/immutable/nodes/{1.C4em9nwH.js → 1.BLZj3WNW.js} +1 -1
  15. package/dist/web/_app/immutable/nodes/{10.DGXLHPeN.js → 10.CnTuhaua.js} +1 -1
  16. package/dist/web/_app/immutable/nodes/{11.SDWfdWaC.js → 11.Bu1W8Jx8.js} +1 -1
  17. package/dist/web/_app/immutable/nodes/12.BCT8Dv1G.js +1 -0
  18. package/dist/web/_app/immutable/nodes/{13.9M-J-sT0.js → 13.Cu-n0wZ9.js} +1 -1
  19. package/dist/web/_app/immutable/nodes/{14.F_F2PTIe.js → 14.u96Ucsue.js} +1 -1
  20. package/dist/web/_app/immutable/nodes/{15.B0ylahpi.js → 15.CMhy_NGl.js} +1 -1
  21. package/dist/web/_app/immutable/nodes/{2.oTDdkLn9.js → 2.JQynNxpe.js} +1 -1
  22. package/dist/web/_app/immutable/nodes/{3.Oyghjrgn.js → 3.Chtgl9hh.js} +1 -1
  23. package/dist/web/_app/immutable/nodes/{4.DiPBEiKy.js → 4.BIau4H47.js} +1 -1
  24. package/dist/web/_app/immutable/nodes/{5.CtE-oF5k.js → 5.DtYG5yJy.js} +1 -1
  25. package/dist/web/_app/immutable/nodes/{6.tVPvf7jD.js → 6.B-ud4zCO.js} +1 -1
  26. package/dist/web/_app/immutable/nodes/7.7Fm6yzqA.js +9 -0
  27. package/dist/web/_app/immutable/nodes/{8.DJsWN3X2.js → 8.Chp3dlzK.js} +1 -1
  28. package/dist/web/_app/immutable/nodes/{9.30YXO0yd.js → 9.PMJrdCxq.js} +1 -1
  29. package/dist/web/_app/version.json +1 -1
  30. package/dist/web/index.html +7 -7
  31. package/package.json +3 -3
  32. package/dist/web/_app/immutable/assets/7.oxd6PqWk.css +0 -1
  33. package/dist/web/_app/immutable/assets/_page.oxd6PqWk.css +0 -1
  34. package/dist/web/_app/immutable/chunks/api.DC92Gfvo.js +0 -1
  35. package/dist/web/_app/immutable/chunks/entry.Cxpjn2cB.js +0 -3
  36. package/dist/web/_app/immutable/chunks/i18n.yxU9Ieh-.js +0 -1
  37. package/dist/web/_app/immutable/entry/start.C2M6huNJ.js +0 -1
  38. package/dist/web/_app/immutable/nodes/12.B5loVhie.js +0 -1
  39. package/dist/web/_app/immutable/nodes/7.CAfC8ItM.js +0 -9
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import { execSync as execSync5 } from "child_process";
14
14
  // src/commands/serve.ts
15
15
  import http2 from "http";
16
16
  import { readFileSync as readFileSync9, existsSync as existsSync10, statSync as statSync3, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3 } from "fs";
17
- import { join as join11, extname as extname2, dirname as dirname3 } from "path";
17
+ import { join as join11, extname as extname2, dirname as dirname4 } from "path";
18
18
  import { fileURLToPath } from "url";
19
19
 
20
20
  // src/api/server.ts
@@ -112,204 +112,22 @@ async function fetchExchangeRate() {
112
112
  function convertToUSD(cny, rate) {
113
113
  return cny * rate;
114
114
  }
115
- var DEFAULT_PRICE_TABLE = {
116
- // Anthropic — https://platform.claude.com/docs/en/about-claude/pricing
117
- "claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
118
- "claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
119
- "claude-opus-4-6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
120
- "claude-opus-4-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
121
- "claude-opus-4-1": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
122
- "claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
123
- "claude-sonnet-4-5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
124
- "claude-haiku-4-5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
125
- "claude-haiku-3-5": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
126
- "claude-sonnet-4": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
127
- "claude-opus-4": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
128
- "claude-3-5-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
129
- "claude-3-5-haiku": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
130
- // OpenAI — https://developers.openai.com/api/docs/pricing
131
- "gpt-5.5-pro": { input: 30, output: 180 },
132
- "gpt-5.5": { input: 5, output: 30, cacheRead: 0.5 },
133
- "gpt-5.4-pro": { input: 30, output: 180 },
134
- "gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075 },
135
- "gpt-5.4-nano": { input: 0.2, output: 1.25, cacheRead: 0.02 },
136
- "gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25 },
137
- "gpt-5.3-codex": { input: 1.75, output: 14, cacheRead: 0.175 },
138
- "gpt-4o-mini": { input: 0.15, output: 0.6 },
139
- "gpt-4o": { input: 2.5, output: 10 },
140
- "gpt-4.1": { input: 2, output: 8 },
141
- "o4-mini": { input: 1.1, output: 4.4 },
142
- "o4-mini-deep-research": { input: 1, output: 4 },
143
- "o3-pro": { input: 20, output: 80 },
144
- "o3": { input: 2, output: 8 },
145
- "o3-mini": { input: 0.55, output: 2.2, cacheRead: 0.55 },
146
- "o3-deep-research": { input: 5, output: 20 },
147
- "o1": { input: 15, output: 60, cacheRead: 7.5 },
148
- "o1-mini": { input: 0.55, output: 2.2, cacheRead: 0.55 },
149
- "codex-mini": { input: 0.75, output: 3, cacheRead: 0.025 },
150
- "gpt-5.2-codex": { input: 1.75, output: 14, cacheRead: 0.175 },
151
- "gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175 },
152
- "gpt-5.1-codex": { input: 1.25, output: 10, cacheRead: 0.125 },
153
- "gpt-5.1": { input: 1.25, output: 10, cacheRead: 0.125 },
154
- "gpt-5-codex": { input: 1.25, output: 10, cacheRead: 0.125 },
155
- "gpt-5": { input: 1.25, output: 10, cacheRead: 0.125 },
156
- "gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025 },
157
- "gpt-5-nano": { input: 0.05, output: 0.4, cacheRead: 5e-3 },
158
- "gpt-4.1-mini": { input: 0.4, output: 1.6, cacheRead: 0.1 },
159
- "gpt-4.1-nano": { input: 0.1, output: 0.4, cacheRead: 0.025 },
160
- // Google Gemini — https://ai.google.dev/gemini-api/docs/pricing
161
- "gemini-3.5-flash": { input: 1.5, output: 9 },
162
- "gemini-3.1-pro": { input: 2, output: 12 },
163
- "gemini-3.1-flash-lite": { input: 0.25, output: 1.5 },
164
- "gemini-3-pro-preview": { input: 2, output: 12 },
165
- "gemini-3-flash-preview": { input: 0.5, output: 3 },
166
- "gemini-3-flash": { input: 0.5, output: 3 },
167
- "gemini-3-pro-image": { input: 2, output: 12 },
168
- "gemini-3.1-pro-preview": { input: 2, output: 12 },
169
- "gemini-3.1-flash-lite-preview": { input: 0.25, output: 1.5 },
170
- "gemini-2.5-pro": { input: 1.25, output: 10 },
171
- "gemini-2.5-flash-lite": { input: 0.1, output: 0.4 },
172
- "gemini-2.5-flash": { input: 0.3, output: 2.5 },
173
- "gemini-2.0-flash-lite": { input: 0.075, output: 0.3 },
174
- "gemini-2.0-flash": { input: 0.1, output: 0.4 },
175
- // DeepSeek — https://api-docs.deepseek.com/quick_start/pricing
176
- "deepseek-v4-pro": { input: 3.13, output: 6.26, cacheRead: 0.026, currency: "CNY" },
177
- "deepseek-v4-flash": { input: 1.01, output: 2.02, cacheRead: 0.02, currency: "CNY" },
178
- "deepseek-v3.2": { input: 2.01, output: 3.01, cacheRead: 0.2, currency: "CNY" },
179
- "deepseek-v3.1": { input: 3.94, output: 11.9, cacheRead: 0.39, currency: "CNY" },
180
- "deepseek-v3": { input: 2.01, output: 7.94, cacheRead: 0.2, currency: "CNY" },
181
- "deepseek-reasoner": { input: 3.94, output: 15.6, cacheRead: 1, currency: "CNY" },
182
- "deepseek-chat": { input: 1.93, output: 7.87, cacheRead: 0.5, currency: "CNY" },
183
- // Kimi (Moonshot AI) — https://platform.kimi.ai/docs/pricing/chat
184
- "kimi-k2.6": { input: 6.9, output: 29, cacheRead: 1.15, currency: "CNY" },
185
- "kimi-k2.5": { input: 4.3, output: 21.6, cacheRead: 0.72, currency: "CNY" },
186
- "kimi-k2-turbo": { input: 8.3, output: 57.6, cacheRead: 1.08, currency: "CNY" },
187
- "kimi-k2": { input: 4.3, output: 18, cacheRead: 1.08, currency: "CNY" },
188
- "kimi-k2-thinking": { input: 3.94, output: 15.7, cacheRead: 0.71, currency: "CNY" },
189
- "kimi-k2-0905": { input: 3.94, output: 15.7, cacheRead: 0.71, currency: "CNY" },
190
- "moonshot-v1-128k": { input: 14.4, output: 36, currency: "CNY" },
191
- "moonshot-v1-32k": { input: 7.2, output: 21.6, currency: "CNY" },
192
- "moonshot-v1-8k": { input: 1.44, output: 14.4, currency: "CNY" },
193
- // GLM (Z.ai / Zhipu AI) — https://docs.z.ai/guides/overview/pricing — TODO: verify against official pricing
194
- "glm-5.1": { input: 10, output: 32, currency: "CNY" },
195
- "glm-5p1": { input: 10, output: 32, currency: "CNY" },
196
- // Fireworks alias (accounts/fireworks/models/glm-5p1)
197
- "glm-5-turbo": { input: 8.5, output: 29, currency: "CNY" },
198
- "glm-5": { input: 7, output: 23, currency: "CNY" },
199
- "glm-4.7-flashx": { input: 0.5, output: 2.8, currency: "CNY" },
200
- "glm-4.7": { input: 4, output: 16, currency: "CNY" },
201
- "glm-4.6": { input: 4, output: 16, currency: "CNY" },
202
- "glm-4.5-x": { input: 16, output: 64, currency: "CNY" },
203
- "glm-4.5-airx": { input: 8, output: 32, currency: "CNY" },
204
- "glm-4.5-air": { input: 1.4, output: 8, currency: "CNY" },
205
- "glm-4.5": { input: 4, output: 16, currency: "CNY" },
206
- // Qwen (Alibaba Cloud) — https://www.alibabacloud.com/help/en/model-studio/model-pricing
207
- "qwen3.6-plus": { input: 2, output: 12, currency: "CNY" },
208
- "qwen3-235b": { input: 5, output: 20, currency: "CNY" },
209
- "qwen3-32b": { input: 1.12, output: 4.48, currency: "CNY" },
210
- "qwen3-30b": { input: 1.4, output: 5.6, currency: "CNY" },
211
- "qwen-max": { input: 11.5, output: 46, currency: "CNY" },
212
- "qwen-plus": { input: 2.8, output: 8.4, currency: "CNY" },
213
- "qwen-turbo": { input: 0.35, output: 1.4, currency: "CNY" },
214
- "qwen-long": { input: 0.5, output: 2, currency: "CNY" },
215
- "qwen2.5-72b": { input: 10, output: 40, currency: "CNY" },
216
- "qwen2.5-7b": { input: 1.26, output: 5.04, currency: "CNY" },
217
- "qwen3.5-plus": { input: 1.86, output: 11.2, currency: "CNY" },
218
- "qwen3-max": { input: 5.59, output: 27.9, currency: "CNY" },
219
- "qwen3-235b-a22b": { input: 5.01, output: 60.1, currency: "CNY" },
220
- "qwen3-coder-plus": { input: 4.65, output: 23.3, currency: "CNY" },
221
- "qwen3-coder-flash": { input: 1.4, output: 6.98, currency: "CNY" },
222
- "qwen3-coder-next": { input: 0.86, output: 5.37, currency: "CNY" },
223
- "qwq-plus": { input: 5.73, output: 17.2, currency: "CNY" },
224
- "qwq-32b": { input: 1.43, output: 4.3, currency: "CNY" },
225
- // Xiaomi MiMo — https://platform.xiaomimimo.com/docs/zh-CN/pricing
226
- "mimo-v2.5-pro": { input: 3, output: 6, cacheRead: 0.025, currency: "CNY" },
227
- "mimo-v2.5": { input: 1, output: 2, cacheRead: 0.02, currency: "CNY" },
228
- "mimo-v2-flash": { input: 0.64, output: 2.08, cacheRead: 0.064, currency: "CNY" },
229
- "mimo-v2-pro": { input: 7.17, output: 21.5, currency: "CNY" },
230
- // MiniMax — https://platform.minimaxi.com/docs/guides/pricing-paygo
231
- "minimax-m2.7": { input: 2.15, output: 8.59, cacheRead: 0.43, cacheWrite: 2.68, currency: "CNY" },
232
- "minimax-m2.7-highspeed": { input: 4.3, output: 17.2, cacheRead: 0.43, cacheWrite: 2.68, currency: "CNY" },
233
- "minimax-m2.5-lightning": { input: 2.15, output: 17.2, cacheRead: 0.21, currency: "CNY" },
234
- "minimax-m2.5": { input: 0.86, output: 6.81, cacheRead: 0.21, currency: "CNY" },
235
- "minimax-m2.1": { input: 1.93, output: 6.81, cacheRead: 0.21, currency: "CNY" },
236
- "minimax-m2.1-lightning": { input: 1.93, output: 16.7, cacheRead: 0.21, currency: "CNY" },
237
- "minimax-m2": { input: 1.93, output: 6.81, cacheRead: 0.21, currency: "CNY" },
238
- // Mistral AI — https://mistral.ai/pricing
239
- "mistral-large": { input: 0.5, output: 1.5 },
240
- "mistral-large-3": { input: 0.5, output: 1.5 },
241
- "mistral-medium": { input: 1.5, output: 7.5 },
242
- "mistral-small": { input: 0.1, output: 0.3 },
243
- "magistral-medium": { input: 2, output: 5 },
244
- "magistral-small": { input: 0.5, output: 1.5 },
245
- "devstral-medium": { input: 0.4, output: 2 },
246
- "devstral-small": { input: 0.1, output: 0.3 },
247
- "codestral": { input: 0.3, output: 0.9 },
248
- "open-mistral-nemo": { input: 0.15, output: 0.15 },
249
- "open-mixtral-8x22b": { input: 2, output: 6 },
250
- "ministral-14b": { input: 0.2, output: 0.2 },
251
- "ministral-8b": { input: 0.15, output: 0.15 },
252
- "ministral-3b": { input: 0.1, output: 0.1 },
253
- "pixtral-12b": { input: 0.1, output: 0.1 },
254
- // xAI Grok — https://docs.x.ai/developers/models
255
- "grok-4-1-fast": { input: 0.2, output: 0.5 },
256
- "grok-4.3": { input: 1.25, output: 2.5 },
257
- "grok-4.20-0309-reasoning": { input: 2, output: 6 },
258
- "grok-4.20-0309-non-reasoning": { input: 2, output: 6 },
259
- "grok-4": { input: 1.25, output: 2.5 },
260
- "grok-build-0.1": { input: 1, output: 2 },
261
- "grok-3": { input: 3, output: 15 },
262
- "grok-3-mini": { input: 0.25, output: 0.5 },
263
- // Cohere — https://cohere.com/pricing
264
- "command-a": { input: 2.5, output: 10 },
265
- "command-r-plus": { input: 2.5, output: 10 },
266
- "command-r": { input: 0.15, output: 0.6 },
267
- // Doubao (ByteDance) — https://www.volcengine.com/docs/82379/1544106
268
- "doubao-seed-code": { input: 1.22, output: 7.95, currency: "CNY" },
269
- "doubao-seed-2.0-pro": { input: 3.7, output: 18.5, currency: "CNY" },
270
- "doubao-seed-2.0-code": { input: 3.36, output: 16.8, currency: "CNY" },
271
- "doubao-seed-2.0-lite": { input: 0.63, output: 3.78, currency: "CNY" },
272
- "doubao-seed-2.0-mini": { input: 0.21, output: 2.1, currency: "CNY" },
273
- "doubao-seed-1.6-flash": { input: 0.16, output: 1.58, currency: "CNY" },
274
- "doubao-seed-1.6-lite": { input: 0.32, output: 2.52, currency: "CNY" },
275
- "doubao-seed-1.6": { input: 0.84, output: 8.4, currency: "CNY" },
276
- "doubao-1.5-pro": { input: 0.84, output: 2.1, currency: "CNY" },
277
- "doubao-1.5-lite": { input: 0.32, output: 0.63, currency: "CNY" },
278
- // Hunyuan (Tencent) — https://cloud.tencent.com/document/product/1729 — TODO: verify against official pricing
279
- "hunyuan-t1": { input: 0.48, output: 1.87, cacheRead: 0.21, currency: "CNY" },
280
- "hunyuan-a13b": { input: 1, output: 4.1, currency: "CNY" },
281
- // ERNIE (Baidu) — https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Blfmc9dlf — TODO: verify against official pricing
282
- "ernie-4.5-300b": { input: 2, output: 6.5, currency: "CNY" },
283
- "ernie-4.5-21b": { input: 0.5, output: 2, currency: "CNY" },
284
- "ernie-x1": { input: 2, output: 8, currency: "CNY" },
285
- // StepFun (阶跃星辰)
286
- "step-3.5-flash": { input: 0.72, output: 2.15, currency: "CNY" },
287
- // Qoder credits — https://docs.qoder.com/user-guide/chat/model-tier-selector
288
- // Qoder publishes credit multipliers and a $0.02 / credit pack price, but
289
- // not a full per-token tariff. Estimate 1.0x as 1 credit / 10K tokens.
290
- // Ultimate uses the current official limited-time 0.8x discount.
291
- "qoder-auto": { input: 2, output: 2, cacheRead: 2, cacheWrite: 2 },
292
- "qoder-ultimate": { input: 1.6, output: 1.6, cacheRead: 1.6, cacheWrite: 1.6 },
293
- "qoder-performance": { input: 2.2, output: 2.2, cacheRead: 2.2, cacheWrite: 2.2 },
294
- "qoder-efficient": { input: 0.6, output: 0.6, cacheRead: 0.6, cacheWrite: 0.6 },
295
- "qoder-lite": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
296
- };
115
+ var basePriceTable = {};
297
116
  var userOverrides = {};
298
- var PRICE_TABLE = { ...DEFAULT_PRICE_TABLE };
299
- function getPriceTable() {
300
- return { ...DEFAULT_PRICE_TABLE, ...userOverrides };
117
+ var PRICE_TABLE = {};
118
+ function setRuntimePriceTable(base, overrides = {}) {
119
+ basePriceTable = { ...base };
120
+ userOverrides = { ...overrides };
121
+ PRICE_TABLE = { ...basePriceTable, ...userOverrides };
301
122
  }
302
123
  function setPriceOverride(model, entry) {
303
124
  userOverrides = { ...userOverrides, [model]: entry };
304
- PRICE_TABLE = { ...DEFAULT_PRICE_TABLE, ...userOverrides };
125
+ PRICE_TABLE = { ...basePriceTable, ...userOverrides };
305
126
  }
306
127
  function removePriceOverride(model) {
307
128
  const { [model]: _, ...rest } = userOverrides;
308
129
  userOverrides = rest;
309
- PRICE_TABLE = { ...DEFAULT_PRICE_TABLE, ...userOverrides };
310
- }
311
- function getUserOverrides() {
312
- return { ...userOverrides };
130
+ PRICE_TABLE = { ...basePriceTable, ...userOverrides };
313
131
  }
314
132
  var PROVIDER_PREFIXES = [
315
133
  "accounts/fireworks/models/",
@@ -366,6 +184,15 @@ function calculateCostWithResolver(model, tokens, exchangeRate, resolver) {
366
184
  function calculateCost(model, tokens, exchangeRate) {
367
185
  return calculateCostWithResolver(model, tokens, exchangeRate, resolvePrice);
368
186
  }
187
+ function calculateCostForPrice(price, tokens, exchangeRate) {
188
+ const inputCost = tokens.inputTokens / 1e6 * price.input;
189
+ const outputCost = tokens.outputTokens / 1e6 * price.output;
190
+ const cacheReadCost = tokens.cacheReadTokens / 1e6 * (price.cacheRead ?? 0);
191
+ const cacheWriteCost = tokens.cacheWriteTokens / 1e6 * (price.cacheWrite ?? 0);
192
+ const thinkingCost = tokens.thinkingTokens / 1e6 * price.output;
193
+ const rawCost = inputCost + outputCost + cacheReadCost + cacheWriteCost + thinkingCost;
194
+ return price.currency === "CNY" ? convertToUSD(rawCost, exchangeRate ?? FALLBACK_RATE) : rawCost;
195
+ }
369
196
  var QODER_TIER_MODELS = /* @__PURE__ */ new Set([
370
197
  "auto",
371
198
  "ultimate",
@@ -1051,14 +878,14 @@ function usageFromAny(parsed) {
1051
878
  const cachedFromDetails = num(details.cached_tokens);
1052
879
  const rawInput = usage.input_tokens ?? usage.input ?? usage.prompt_tokens ?? usage.input_other ?? usage.tokensIn ?? 0;
1053
880
  const cacheRead = num(
1054
- usage.cache_read_input_tokens ?? usage.cache_read_tokens ?? usage.input_cache_read ?? usage.cacheReads ?? cachedFromDetails
881
+ usage.cache_read_input_tokens ?? usage.cache_read_tokens ?? usage.input_cache_read ?? usage.cacheReads ?? usage.cacheRead ?? cachedFromDetails
1055
882
  );
1056
883
  return {
1057
884
  inputTokens: Math.max(0, num(rawInput) - cachedFromDetails),
1058
885
  outputTokens: num(usage.output_tokens ?? usage.output ?? usage.completion_tokens ?? usage.tokensOut),
1059
886
  cacheReadTokens: cacheRead,
1060
887
  cacheWriteTokens: num(
1061
- usage.cache_creation_input_tokens ?? usage.cache_write_input_tokens ?? usage.cache_write_tokens ?? usage.input_cache_creation ?? usage.cacheWrites
888
+ usage.cache_creation_input_tokens ?? usage.cache_write_input_tokens ?? usage.cache_write_tokens ?? usage.input_cache_creation ?? usage.cacheWrites ?? usage.cacheWrite
1062
889
  ),
1063
890
  thinkingTokens: num(
1064
891
  usage.thinking_tokens ?? usage.reasoning_tokens ?? usage.reasoning_output_tokens ?? details.reasoning_tokens
@@ -1202,8 +1029,8 @@ var Aggregator = class {
1202
1029
  };
1203
1030
 
1204
1031
  // src/config.ts
1205
- import { readFileSync, writeFileSync, existsSync } from "fs";
1206
- import { join } from "path";
1032
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
1033
+ import { join, dirname } from "path";
1207
1034
  import { homedir } from "os";
1208
1035
  var AIUSAGE_DIR = join(homedir(), ".aiusage");
1209
1036
  var CONFIG_PATH = join(AIUSAGE_DIR, "config.json");
@@ -1233,6 +1060,7 @@ function loadConfig() {
1233
1060
  }
1234
1061
  }
1235
1062
  function saveConfig(config) {
1063
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 448 });
1236
1064
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
1237
1065
  }
1238
1066
  function buildConsentConfig(config) {
@@ -1260,7 +1088,7 @@ function loadCredential(key) {
1260
1088
  }
1261
1089
 
1262
1090
  // src/init.ts
1263
- import { mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
1091
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
1264
1092
  import { join as join2 } from "path";
1265
1093
  import { randomUUID } from "crypto";
1266
1094
  function getState(aiusageDir) {
@@ -2640,14 +2468,14 @@ function sha256Buffer(data) {
2640
2468
  }
2641
2469
 
2642
2470
  // src/leaderboard/credentials.ts
2643
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, unlinkSync, existsSync as existsSync6, renameSync } from "fs";
2644
- import { join as join5, dirname } from "path";
2471
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync, existsSync as existsSync6, renameSync } from "fs";
2472
+ import { join as join5, dirname as dirname2 } from "path";
2645
2473
  import { randomBytes as randomBytes2 } from "crypto";
2646
2474
  var CREDENTIALS_FILE = join5(AIUSAGE_DIR, "leaderboard-credentials.json");
2647
2475
  function saveCredentials(creds) {
2648
- const dir = dirname(CREDENTIALS_FILE);
2476
+ const dir = dirname2(CREDENTIALS_FILE);
2649
2477
  if (!existsSync6(dir)) {
2650
- mkdirSync2(dir, { recursive: true });
2478
+ mkdirSync3(dir, { recursive: true });
2651
2479
  }
2652
2480
  const tmpFile = `${CREDENTIALS_FILE}.${randomBytes2(4).toString("hex")}.tmp`;
2653
2481
  try {
@@ -2866,14 +2694,14 @@ async function fetchLeaderboardStatus(serverUrl) {
2866
2694
 
2867
2695
  // src/db/index.ts
2868
2696
  import Database from "better-sqlite3";
2869
- import { mkdirSync as mkdirSync3 } from "fs";
2870
- import { dirname as dirname2 } from "path";
2697
+ import { mkdirSync as mkdirSync4 } from "fs";
2698
+ import { dirname as dirname3 } from "path";
2871
2699
 
2872
2700
  // src/db/schema.ts
2873
2701
  function applyPragmas(db) {
2874
2702
  db.pragma("journal_mode = WAL");
2875
2703
  db.pragma("foreign_keys = ON");
2876
- db.pragma("busy_timeout = 5000");
2704
+ db.pragma("busy_timeout = 30000");
2877
2705
  }
2878
2706
  function createReadonlyViews(db) {
2879
2707
  db.exec(`
@@ -3166,6 +2994,79 @@ function migrateV9(db) {
3166
2994
  `);
3167
2995
  }
3168
2996
 
2997
+ // src/db/migrations/v10.ts
2998
+ function migrateV10(db) {
2999
+ db.exec(`
3000
+ CREATE TABLE IF NOT EXISTS model_prices (
3001
+ model_key TEXT PRIMARY KEY,
3002
+ provider TEXT NOT NULL DEFAULT '',
3003
+ input REAL NOT NULL,
3004
+ output REAL NOT NULL,
3005
+ cache_read REAL,
3006
+ cache_write REAL,
3007
+ currency TEXT NOT NULL DEFAULT 'USD',
3008
+ source TEXT NOT NULL DEFAULT 'manual',
3009
+ source_model_id TEXT,
3010
+ source_url TEXT,
3011
+ origin TEXT NOT NULL DEFAULT 'user',
3012
+ status TEXT NOT NULL DEFAULT 'active',
3013
+ last_synced_at INTEGER,
3014
+ created_at INTEGER NOT NULL,
3015
+ updated_at INTEGER NOT NULL
3016
+ );
3017
+
3018
+ CREATE TABLE IF NOT EXISTS model_price_aliases (
3019
+ alias TEXT PRIMARY KEY,
3020
+ model_key TEXT NOT NULL REFERENCES model_prices(model_key) ON DELETE CASCADE,
3021
+ match_type TEXT NOT NULL DEFAULT 'exact',
3022
+ provider TEXT NOT NULL DEFAULT '',
3023
+ priority INTEGER NOT NULL DEFAULT 100,
3024
+ source TEXT NOT NULL DEFAULT 'manual',
3025
+ origin TEXT NOT NULL DEFAULT 'user',
3026
+ enabled INTEGER NOT NULL DEFAULT 1,
3027
+ created_at INTEGER NOT NULL,
3028
+ updated_at INTEGER NOT NULL
3029
+ );
3030
+
3031
+ CREATE INDEX IF NOT EXISTS idx_model_prices_origin ON model_prices(origin);
3032
+ CREATE INDEX IF NOT EXISTS idx_model_prices_source ON model_prices(source);
3033
+ CREATE INDEX IF NOT EXISTS idx_model_price_aliases_model ON model_price_aliases(model_key);
3034
+
3035
+ INSERT INTO schema_version (version) VALUES (10);
3036
+ `);
3037
+ }
3038
+
3039
+ // src/db/migrations/v11.ts
3040
+ function migrateV11(db) {
3041
+ db.exec(`
3042
+ CREATE TABLE IF NOT EXISTS model_price_sync_baselines (
3043
+ model_key TEXT PRIMARY KEY,
3044
+ provider TEXT NOT NULL DEFAULT '',
3045
+ input REAL NOT NULL,
3046
+ output REAL NOT NULL,
3047
+ cache_read REAL,
3048
+ cache_write REAL,
3049
+ currency TEXT NOT NULL DEFAULT 'USD',
3050
+ source TEXT NOT NULL DEFAULT 'litellm',
3051
+ source_model_id TEXT,
3052
+ source_url TEXT,
3053
+ last_synced_at INTEGER NOT NULL,
3054
+ updated_at INTEGER NOT NULL
3055
+ );
3056
+
3057
+ INSERT INTO model_price_sync_baselines (
3058
+ model_key, provider, input, output, cache_read, cache_write, currency, source,
3059
+ source_model_id, source_url, last_synced_at, updated_at
3060
+ )
3061
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source,
3062
+ source_model_id, source_url, COALESCE(last_synced_at, updated_at), updated_at
3063
+ FROM model_prices
3064
+ WHERE origin = 'builtin' AND source != 'manual';
3065
+
3066
+ INSERT INTO schema_version (version) VALUES (11);
3067
+ `);
3068
+ }
3069
+
3169
3070
  // src/db/migrations/index.ts
3170
3071
  var MIGRATIONS = [
3171
3072
  { version: 1, migrate: migrateV1 },
@@ -3176,7 +3077,9 @@ var MIGRATIONS = [
3176
3077
  { version: 6, migrate: migrateV6 },
3177
3078
  { version: 7, migrate: migrateV7 },
3178
3079
  { version: 8, migrate: migrateV8 },
3179
- { version: 9, migrate: migrateV9 }
3080
+ { version: 9, migrate: migrateV9 },
3081
+ { version: 10, migrate: migrateV10 },
3082
+ { version: 11, migrate: migrateV11 }
3180
3083
  ];
3181
3084
  function runMigrations(db) {
3182
3085
  createSchemaVersionTable(db);
@@ -3193,13 +3096,591 @@ function runMigrations(db) {
3193
3096
  }
3194
3097
  }
3195
3098
 
3099
+ // src/pricing-registry.ts
3100
+ var LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
3101
+ function rowToPrice(row) {
3102
+ return {
3103
+ input: Number(row.input),
3104
+ output: Number(row.output),
3105
+ cacheRead: row.cache_read == null ? void 0 : Number(row.cache_read),
3106
+ cacheWrite: row.cache_write == null ? void 0 : Number(row.cache_write),
3107
+ currency: row.currency
3108
+ };
3109
+ }
3110
+ function normalizeProvider(provider) {
3111
+ return (provider ?? "").trim().toLowerCase();
3112
+ }
3113
+ function normalizeModelKey(sourceModelId) {
3114
+ const trimmed = sourceModelId.trim();
3115
+ const slash = trimmed.indexOf("/");
3116
+ return slash > 0 ? trimmed.slice(slash + 1) : trimmed;
3117
+ }
3118
+ function aliasesFor(sourceModelId, modelKey) {
3119
+ const aliases = /* @__PURE__ */ new Set([modelKey, sourceModelId]);
3120
+ if (modelKey.startsWith("claude-")) aliases.add(modelKey.replace(/-\d{8}$/, ""));
3121
+ return [...aliases].filter(Boolean);
3122
+ }
3123
+ function litellmEntryToPrice(sourceModelId, entry) {
3124
+ if (entry.mode && entry.mode !== "chat" && entry.mode !== "completion" && entry.mode !== "responses") return null;
3125
+ if (entry.input_cost_per_second != null || entry.output_cost_per_second != null) return null;
3126
+ if (typeof entry.input_cost_per_token !== "number" || typeof entry.output_cost_per_token !== "number") return null;
3127
+ const modelKey = normalizeModelKey(sourceModelId);
3128
+ if (!modelKey || modelKey.includes("*")) return null;
3129
+ return {
3130
+ modelKey,
3131
+ provider: normalizeProvider(entry.litellm_provider),
3132
+ price: {
3133
+ input: entry.input_cost_per_token * 1e6,
3134
+ output: entry.output_cost_per_token * 1e6,
3135
+ cacheRead: typeof entry.cache_read_input_token_cost === "number" ? entry.cache_read_input_token_cost * 1e6 : void 0,
3136
+ cacheWrite: typeof entry.cache_creation_input_token_cost === "number" ? entry.cache_creation_input_token_cost * 1e6 : void 0,
3137
+ currency: "USD"
3138
+ }
3139
+ };
3140
+ }
3141
+ function loadBuiltinRows(db) {
3142
+ return db.prepare(`
3143
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3144
+ source_url, origin, status, last_synced_at, updated_at
3145
+ FROM model_prices
3146
+ WHERE origin = 'builtin' AND status = 'active'
3147
+ `).all();
3148
+ }
3149
+ function loadUserRows(db) {
3150
+ return db.prepare(`
3151
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3152
+ source_url, origin, status, last_synced_at, updated_at
3153
+ FROM model_prices
3154
+ WHERE origin = 'user' AND status = 'active'
3155
+ `).all();
3156
+ }
3157
+ function loadSyncedBaselineRows(db) {
3158
+ return db.prepare(`
3159
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3160
+ source_url, last_synced_at, updated_at
3161
+ FROM model_price_sync_baselines
3162
+ `).all();
3163
+ }
3164
+ function loadAliasRows(db) {
3165
+ return db.prepare(`
3166
+ SELECT alias, model_key, match_type, provider, priority, source, origin, enabled
3167
+ FROM model_price_aliases
3168
+ WHERE enabled = 1
3169
+ ORDER BY priority ASC
3170
+ `).all();
3171
+ }
3172
+ function loadPricingRuntime(db, config) {
3173
+ const builtin = {};
3174
+ for (const row of loadBuiltinRows(db)) builtin[row.model_key] = rowToPrice(row);
3175
+ const overrides = {};
3176
+ const userRows = loadUserRows(db);
3177
+ const configOverrides = config?.priceOverrides ?? {};
3178
+ const activePrices = priceTableFromDb(db);
3179
+ for (const alias of loadAliasRows(db)) {
3180
+ const price = activePrices[alias.model_key];
3181
+ if (!price) continue;
3182
+ if (alias.origin === "user") overrides[alias.alias] = price;
3183
+ else builtin[alias.alias] = price;
3184
+ }
3185
+ for (const row of userRows) overrides[row.model_key] = rowToPrice(row);
3186
+ for (const [model, entry] of Object.entries(configOverrides)) overrides[model] = entry;
3187
+ setRuntimePriceTable(builtin, overrides);
3188
+ }
3189
+ function setUserPrice(db, modelKey, entry) {
3190
+ const now = Date.now();
3191
+ const baseline = db.prepare(`
3192
+ SELECT provider, source, source_model_id, source_url, last_synced_at
3193
+ FROM model_price_sync_baselines
3194
+ WHERE model_key = ?
3195
+ `).get(modelKey);
3196
+ db.prepare(`
3197
+ INSERT INTO model_prices (
3198
+ model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3199
+ source_url, origin, status, last_synced_at, created_at, updated_at
3200
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'manual', ?, ?, 'user', 'active', ?, ?, ?)
3201
+ ON CONFLICT(model_key) DO UPDATE SET
3202
+ provider = excluded.provider,
3203
+ input = excluded.input,
3204
+ output = excluded.output,
3205
+ cache_read = excluded.cache_read,
3206
+ cache_write = excluded.cache_write,
3207
+ currency = excluded.currency,
3208
+ source = 'manual',
3209
+ source_model_id = excluded.source_model_id,
3210
+ source_url = excluded.source_url,
3211
+ origin = 'user',
3212
+ status = 'active',
3213
+ last_synced_at = excluded.last_synced_at,
3214
+ updated_at = excluded.updated_at
3215
+ `).run(
3216
+ modelKey,
3217
+ baseline?.provider ?? "",
3218
+ entry.input,
3219
+ entry.output,
3220
+ entry.cacheRead ?? null,
3221
+ entry.cacheWrite ?? null,
3222
+ entry.currency ?? "USD",
3223
+ baseline?.source_model_id ?? modelKey,
3224
+ baseline?.source_url ?? null,
3225
+ baseline?.last_synced_at ?? null,
3226
+ now,
3227
+ now
3228
+ );
3229
+ setPriceOverride(modelKey, entry);
3230
+ }
3231
+ function resetUserPriceToSynced(db, modelKey) {
3232
+ const baseline = db.prepare(`
3233
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3234
+ source_url, last_synced_at, updated_at
3235
+ FROM model_price_sync_baselines
3236
+ WHERE model_key = ?
3237
+ `).get(modelKey);
3238
+ if (!baseline) {
3239
+ removeUserPrice(db, modelKey);
3240
+ return;
3241
+ }
3242
+ const now = Date.now();
3243
+ db.prepare("DELETE FROM model_price_aliases WHERE alias = ? AND model_key = ? AND origin = 'user'").run(modelKey, modelKey);
3244
+ db.prepare(`
3245
+ INSERT INTO model_prices (
3246
+ model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3247
+ source_url, origin, status, last_synced_at, created_at, updated_at
3248
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'builtin', 'active', ?, ?, ?)
3249
+ ON CONFLICT(model_key) DO UPDATE SET
3250
+ provider = excluded.provider,
3251
+ input = excluded.input,
3252
+ output = excluded.output,
3253
+ cache_read = excluded.cache_read,
3254
+ cache_write = excluded.cache_write,
3255
+ currency = excluded.currency,
3256
+ source = excluded.source,
3257
+ source_model_id = excluded.source_model_id,
3258
+ source_url = excluded.source_url,
3259
+ origin = 'builtin',
3260
+ status = 'active',
3261
+ last_synced_at = excluded.last_synced_at,
3262
+ updated_at = excluded.updated_at
3263
+ `).run(
3264
+ baseline.model_key,
3265
+ baseline.provider,
3266
+ baseline.input,
3267
+ baseline.output,
3268
+ baseline.cache_read,
3269
+ baseline.cache_write,
3270
+ baseline.currency,
3271
+ baseline.source,
3272
+ baseline.source_model_id,
3273
+ baseline.source_url,
3274
+ baseline.last_synced_at,
3275
+ now,
3276
+ now
3277
+ );
3278
+ }
3279
+ function setUserPricingAlias(db, alias, modelKey) {
3280
+ const aliasKey = alias.trim();
3281
+ const targetKey = modelKey.trim();
3282
+ if (!aliasKey || !targetKey) throw new Error("alias and modelKey required");
3283
+ const target = db.prepare(`
3284
+ SELECT provider
3285
+ FROM model_prices
3286
+ WHERE model_key = ? AND status = 'active'
3287
+ `).get(targetKey);
3288
+ if (!target) throw new Error("Pricing model not found");
3289
+ const now = Date.now();
3290
+ db.prepare(`
3291
+ INSERT INTO model_price_aliases (alias, model_key, match_type, provider, priority, source, origin, enabled, created_at, updated_at)
3292
+ VALUES (?, ?, 'exact', ?, 200, 'manual', 'user', 1, ?, ?)
3293
+ ON CONFLICT(alias) DO UPDATE SET
3294
+ model_key = excluded.model_key,
3295
+ match_type = 'exact',
3296
+ provider = excluded.provider,
3297
+ priority = excluded.priority,
3298
+ source = 'manual',
3299
+ origin = 'user',
3300
+ enabled = 1,
3301
+ updated_at = excluded.updated_at
3302
+ `).run(aliasKey, targetKey, target.provider ?? "", now, now);
3303
+ }
3304
+ function removeUserPricingAlias(db, alias) {
3305
+ const aliasKey = alias.trim();
3306
+ if (!aliasKey) throw new Error("alias required");
3307
+ db.prepare("DELETE FROM model_price_aliases WHERE alias = ? AND origin = 'user'").run(aliasKey);
3308
+ }
3309
+ function removeUserPrice(db, modelKey) {
3310
+ db.prepare("DELETE FROM model_price_aliases WHERE alias = ? AND model_key = ? AND origin = 'user'").run(modelKey, modelKey);
3311
+ db.prepare("DELETE FROM model_prices WHERE model_key = ? AND origin = 'user'").run(modelKey);
3312
+ }
3313
+ function upsertBuiltinPrice(db, entry, now) {
3314
+ db.prepare(`
3315
+ INSERT INTO model_price_sync_baselines (
3316
+ model_key, provider, input, output, cache_read, cache_write, currency, source,
3317
+ source_model_id, source_url, last_synced_at, updated_at
3318
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'litellm', ?, ?, ?, ?)
3319
+ ON CONFLICT(model_key) DO UPDATE SET
3320
+ provider = excluded.provider,
3321
+ input = excluded.input,
3322
+ output = excluded.output,
3323
+ cache_read = excluded.cache_read,
3324
+ cache_write = excluded.cache_write,
3325
+ currency = excluded.currency,
3326
+ source = excluded.source,
3327
+ source_model_id = excluded.source_model_id,
3328
+ source_url = excluded.source_url,
3329
+ last_synced_at = excluded.last_synced_at,
3330
+ updated_at = excluded.updated_at
3331
+ `).run(
3332
+ entry.modelKey,
3333
+ entry.provider,
3334
+ entry.price.input,
3335
+ entry.price.output,
3336
+ entry.price.cacheRead ?? null,
3337
+ entry.price.cacheWrite ?? null,
3338
+ entry.price.currency ?? "USD",
3339
+ entry.sourceModelId,
3340
+ entry.sourceUrl,
3341
+ now,
3342
+ now
3343
+ );
3344
+ const existing = db.prepare("SELECT origin, input, output, cache_read, cache_write, currency FROM model_prices WHERE model_key = ?").get(entry.modelKey);
3345
+ if (existing?.origin === "user") return "user_preserved";
3346
+ const next = [entry.price.input, entry.price.output, entry.price.cacheRead ?? null, entry.price.cacheWrite ?? null, entry.price.currency ?? "USD"];
3347
+ if (!existing) {
3348
+ db.prepare(`
3349
+ INSERT INTO model_prices (
3350
+ model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3351
+ source_url, origin, status, last_synced_at, created_at, updated_at
3352
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'litellm', ?, ?, 'builtin', 'active', ?, ?, ?)
3353
+ `).run(entry.modelKey, entry.provider, ...next, entry.sourceModelId, entry.sourceUrl, now, now, now);
3354
+ return "added";
3355
+ }
3356
+ const current = [Number(existing.input), Number(existing.output), existing.cache_read == null ? null : Number(existing.cache_read), existing.cache_write == null ? null : Number(existing.cache_write), existing.currency];
3357
+ const changed = current.some((value, index) => value !== next[index]);
3358
+ if (!changed) {
3359
+ db.prepare(`
3360
+ UPDATE model_prices SET provider = ?, source = 'litellm', source_model_id = ?, source_url = ?, last_synced_at = ?, updated_at = ?
3361
+ WHERE model_key = ? AND origin = 'builtin'
3362
+ `).run(entry.provider, entry.sourceModelId, entry.sourceUrl, now, now, entry.modelKey);
3363
+ return "unchanged";
3364
+ }
3365
+ db.prepare(`
3366
+ UPDATE model_prices
3367
+ SET provider = ?, input = ?, output = ?, cache_read = ?, cache_write = ?, currency = ?, source = 'litellm',
3368
+ source_model_id = ?, source_url = ?, status = 'active', last_synced_at = ?, updated_at = ?
3369
+ WHERE model_key = ? AND origin = 'builtin'
3370
+ `).run(entry.provider, ...next, entry.sourceModelId, entry.sourceUrl, now, now, entry.modelKey);
3371
+ return "updated";
3372
+ }
3373
+ function upsertBuiltinAlias(db, alias, modelKey, provider, source, now) {
3374
+ const existing = db.prepare("SELECT origin, model_key, enabled FROM model_price_aliases WHERE alias = ?").get(alias);
3375
+ if (existing?.origin === "user") return "user_preserved";
3376
+ if (!existing) {
3377
+ db.prepare(`
3378
+ INSERT INTO model_price_aliases (alias, model_key, match_type, provider, priority, source, origin, enabled, created_at, updated_at)
3379
+ VALUES (?, ?, 'exact', ?, 100, ?, 'builtin', 1, ?, ?)
3380
+ `).run(alias, modelKey, provider, source, now, now);
3381
+ return "added";
3382
+ }
3383
+ if (existing.model_key === modelKey && existing.enabled === 1) return "unchanged";
3384
+ db.prepare(`
3385
+ UPDATE model_price_aliases SET model_key = ?, provider = ?, source = ?, enabled = 1, updated_at = ?
3386
+ WHERE alias = ? AND origin = 'builtin'
3387
+ `).run(modelKey, provider, source, now, alias);
3388
+ return "updated";
3389
+ }
3390
+ async function syncPricingFromLitellm(db) {
3391
+ const response = await fetch(LITELLM_PRICING_URL, { headers: { Accept: "application/json" } });
3392
+ if (!response.ok) throw new Error(`LiteLLM pricing fetch failed: HTTP ${response.status}`);
3393
+ const data = await response.json();
3394
+ const now = Date.now();
3395
+ const summary = {
3396
+ source: "litellm",
3397
+ added: 0,
3398
+ updated: 0,
3399
+ unchanged: 0,
3400
+ skipped: 0,
3401
+ aliasesAdded: 0,
3402
+ aliasesUpdated: 0,
3403
+ userPreserved: 0,
3404
+ dryRun: { totalModels: 0, matched: 0, unresolved: [] }
3405
+ };
3406
+ const tx = db.transaction(() => {
3407
+ for (const [sourceModelId, rawEntry] of Object.entries(data)) {
3408
+ const parsed = litellmEntryToPrice(sourceModelId, rawEntry);
3409
+ if (!parsed) {
3410
+ summary.skipped++;
3411
+ continue;
3412
+ }
3413
+ const result = upsertBuiltinPrice(db, {
3414
+ modelKey: parsed.modelKey,
3415
+ provider: parsed.provider,
3416
+ price: parsed.price,
3417
+ sourceModelId,
3418
+ sourceUrl: LITELLM_PRICING_URL
3419
+ }, now);
3420
+ if (result === "user_preserved") summary.userPreserved++;
3421
+ else summary[result]++;
3422
+ for (const alias of aliasesFor(sourceModelId, parsed.modelKey)) {
3423
+ const aliasResult = upsertBuiltinAlias(db, alias, parsed.modelKey, parsed.provider, "litellm", now);
3424
+ if (aliasResult === "added") summary.aliasesAdded++;
3425
+ else if (aliasResult === "updated") summary.aliasesUpdated++;
3426
+ else if (aliasResult === "user_preserved") summary.userPreserved++;
3427
+ }
3428
+ }
3429
+ });
3430
+ tx();
3431
+ summary.dryRun = dryRunLocalModels(db);
3432
+ return summary;
3433
+ }
3434
+ function priceTableFromDb(db) {
3435
+ const table = {};
3436
+ const rows = db.prepare(`
3437
+ SELECT model_key, input, output, cache_read, cache_write, currency
3438
+ FROM model_prices WHERE status = 'active'
3439
+ `).all();
3440
+ for (const row of rows) table[row.model_key] = rowToPrice(row);
3441
+ return table;
3442
+ }
3443
+ function resolvePriceFromRegistry(db, model) {
3444
+ const directUserPrice = db.prepare(`
3445
+ SELECT input, output, cache_read, cache_write, currency
3446
+ FROM model_prices
3447
+ WHERE model_key = ? AND origin = 'user' AND status = 'active'
3448
+ `).get(model);
3449
+ if (directUserPrice) return rowToPrice(directUserPrice);
3450
+ const exactAlias = db.prepare(`
3451
+ SELECT p.input, p.output, p.cache_read, p.cache_write, p.currency
3452
+ FROM model_price_aliases a
3453
+ JOIN model_prices p ON p.model_key = a.model_key
3454
+ WHERE a.alias = ? AND a.enabled = 1 AND p.status = 'active'
3455
+ ORDER BY a.priority DESC LIMIT 1
3456
+ `).get(model);
3457
+ if (exactAlias) return rowToPrice(exactAlias);
3458
+ return resolvePriceFromTable(model, priceTableFromDb(db));
3459
+ }
3460
+ function findMatch(db, model) {
3461
+ const directUserMatch = db.prepare(`
3462
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3463
+ source_url, origin, status, last_synced_at, updated_at
3464
+ FROM model_prices
3465
+ WHERE model_key = ? AND origin = 'user' AND status = 'active'
3466
+ `).get(model);
3467
+ if (directUserMatch) return { price: rowToPrice(directUserMatch), row: directUserMatch, matchedBy: null };
3468
+ const aliasMatch = db.prepare(`
3469
+ SELECT p.model_key, p.provider, p.input, p.output, p.cache_read, p.cache_write, p.currency, p.source,
3470
+ p.source_model_id, p.source_url, p.origin, p.status, p.last_synced_at, p.updated_at, a.alias
3471
+ FROM model_price_aliases a
3472
+ JOIN model_prices p ON p.model_key = a.model_key
3473
+ WHERE a.alias = ? AND a.enabled = 1 AND p.status = 'active'
3474
+ ORDER BY a.priority DESC LIMIT 1
3475
+ `).get(model);
3476
+ if (aliasMatch) {
3477
+ const matchedBy = aliasMatch.alias === model ? aliasMatch.model_key === model ? null : aliasMatch.model_key : aliasMatch.alias;
3478
+ return { price: rowToPrice(aliasMatch), row: aliasMatch, matchedBy };
3479
+ }
3480
+ const rows = db.prepare(`
3481
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3482
+ source_url, origin, status, last_synced_at, updated_at
3483
+ FROM model_prices WHERE status = 'active'
3484
+ `).all();
3485
+ let best = null;
3486
+ for (const row of rows) {
3487
+ if (model === row.model_key) {
3488
+ best = row;
3489
+ break;
3490
+ }
3491
+ if (model.startsWith(row.model_key) && (!best || row.model_key.length > best.model_key.length)) best = row;
3492
+ }
3493
+ if (!best) return null;
3494
+ return { price: rowToPrice(best), row: best, matchedBy: best.model_key === model ? null : best.model_key };
3495
+ }
3496
+ function listPricingModels(db) {
3497
+ const dbModels = db.prepare("SELECT model, COUNT(*) as usage_count FROM records WHERE model != 'unknown' GROUP BY model ORDER BY usage_count DESC, model ASC").all();
3498
+ const knownModels = new Set(dbModels.map((row) => row.model));
3499
+ const userRows = loadUserRows(db);
3500
+ for (const row of userRows) knownModels.add(row.model_key);
3501
+ const syncedBaselines = new Set(loadSyncedBaselineRows(db).map((row) => row.model_key));
3502
+ return [...knownModels].map((model) => {
3503
+ const match = findMatch(db, model);
3504
+ if (!match) {
3505
+ return {
3506
+ model,
3507
+ provider: null,
3508
+ price: null,
3509
+ currency: "USD",
3510
+ origin: null,
3511
+ source: null,
3512
+ sourceModelId: null,
3513
+ lastSyncedAt: null,
3514
+ isBuiltin: false,
3515
+ isOverride: false,
3516
+ isDefault: false,
3517
+ hasSyncedBaseline: syncedBaselines.has(model),
3518
+ matchedBy: null
3519
+ };
3520
+ }
3521
+ return {
3522
+ model,
3523
+ provider: match.row.provider || null,
3524
+ price: match.price,
3525
+ currency: match.price.currency ?? "USD",
3526
+ origin: match.row.origin,
3527
+ source: match.row.source,
3528
+ sourceModelId: match.row.source_model_id,
3529
+ lastSyncedAt: match.row.last_synced_at,
3530
+ isBuiltin: match.row.origin === "builtin",
3531
+ isOverride: match.row.origin === "user",
3532
+ isDefault: match.row.origin === "builtin",
3533
+ hasSyncedBaseline: syncedBaselines.has(match.row.model_key),
3534
+ matchedBy: match.matchedBy
3535
+ };
3536
+ });
3537
+ }
3538
+ function getPricingRegistrySummary(db) {
3539
+ const priceCounts = db.prepare(`
3540
+ SELECT
3541
+ COUNT(*) AS totalPrices,
3542
+ SUM(CASE WHEN origin = 'builtin' THEN 1 ELSE 0 END) AS builtinPrices,
3543
+ SUM(CASE WHEN origin = 'user' THEN 1 ELSE 0 END) AS userPrices,
3544
+ MAX(last_synced_at) AS lastSyncedAt
3545
+ FROM model_prices
3546
+ WHERE status = 'active'
3547
+ `).get();
3548
+ const aliasCounts = db.prepare(`
3549
+ SELECT COUNT(*) AS activeAliases
3550
+ FROM model_price_aliases
3551
+ WHERE enabled = 1
3552
+ `).get();
3553
+ const dryRun = dryRunLocalModels(db);
3554
+ return {
3555
+ totalPrices: Number(priceCounts.totalPrices ?? 0),
3556
+ builtinPrices: Number(priceCounts.builtinPrices ?? 0),
3557
+ userPrices: Number(priceCounts.userPrices ?? 0),
3558
+ activeAliases: Number(aliasCounts.activeAliases ?? 0),
3559
+ lastSyncedAt: priceCounts.lastSyncedAt == null ? null : Number(priceCounts.lastSyncedAt),
3560
+ localModels: dryRun.totalModels,
3561
+ matchedLocalModels: dryRun.matched,
3562
+ unresolvedLocalModels: dryRun.unresolved
3563
+ };
3564
+ }
3565
+ function listPricingAliasTargets(db) {
3566
+ const rows = db.prepare(`
3567
+ SELECT model_key, provider, origin, source, source_model_id, currency
3568
+ FROM model_prices
3569
+ WHERE status = 'active'
3570
+ ORDER BY origin DESC, provider ASC, model_key ASC
3571
+ `).all();
3572
+ return rows.map((row) => ({
3573
+ model: row.model_key,
3574
+ provider: row.provider,
3575
+ origin: row.origin,
3576
+ source: row.source,
3577
+ sourceModelId: row.source_model_id,
3578
+ currency: row.currency
3579
+ }));
3580
+ }
3581
+ function listLocalModelBindings(db) {
3582
+ const rows = db.prepare("SELECT DISTINCT model FROM records WHERE model != 'unknown' ORDER BY model ASC").all();
3583
+ const priceRows = db.prepare(`
3584
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3585
+ source_url, origin, status, last_synced_at, updated_at
3586
+ FROM model_prices WHERE status = 'active'
3587
+ `).all();
3588
+ return rows.map(({ model }) => {
3589
+ const directUserPrice = db.prepare(`
3590
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3591
+ source_url, origin, status, last_synced_at, updated_at
3592
+ FROM model_prices
3593
+ WHERE model_key = ? AND origin = 'user' AND status = 'active'
3594
+ `).get(model);
3595
+ const aliasMatch = db.prepare(`
3596
+ SELECT a.alias, a.model_key AS modelKey, a.origin, a.source, p.origin AS priceOrigin
3597
+ FROM model_price_aliases a
3598
+ JOIN model_prices p ON p.model_key = a.model_key
3599
+ WHERE a.alias = ? AND a.enabled = 1 AND p.status = 'active'
3600
+ ORDER BY a.priority DESC LIMIT 1
3601
+ `).get(model);
3602
+ if (directUserPrice) {
3603
+ const hasManualBinding = Boolean(aliasMatch && aliasMatch.origin === "user" && !(aliasMatch.alias === model && aliasMatch.modelKey === model));
3604
+ return {
3605
+ model,
3606
+ modelKey: hasManualBinding ? aliasMatch.modelKey : directUserPrice.model_key,
3607
+ origin: hasManualBinding ? aliasMatch.origin : directUserPrice.origin,
3608
+ priceOrigin: directUserPrice.origin,
3609
+ source: directUserPrice.source,
3610
+ matchType: "exact",
3611
+ bindingType: "custom",
3612
+ hasManualBinding,
3613
+ hasPrice: true
3614
+ };
3615
+ }
3616
+ if (aliasMatch) {
3617
+ const selfAlias = aliasMatch.alias === model && aliasMatch.modelKey === model;
3618
+ const hasManualBinding = aliasMatch.origin === "user" && !selfAlias;
3619
+ return {
3620
+ model,
3621
+ modelKey: aliasMatch.modelKey,
3622
+ origin: aliasMatch.origin,
3623
+ priceOrigin: aliasMatch.priceOrigin,
3624
+ source: aliasMatch.source,
3625
+ matchType: selfAlias ? "exact" : "alias",
3626
+ bindingType: aliasMatch.priceOrigin === "user" ? "custom" : hasManualBinding ? "manual" : "automatic",
3627
+ hasManualBinding,
3628
+ hasPrice: true
3629
+ };
3630
+ }
3631
+ let best = null;
3632
+ let matchType = null;
3633
+ for (const row of priceRows) {
3634
+ if (model === row.model_key) {
3635
+ best = row;
3636
+ matchType = "exact";
3637
+ break;
3638
+ }
3639
+ if (model.startsWith(row.model_key) && (!best || row.model_key.length > best.model_key.length)) {
3640
+ best = row;
3641
+ matchType = "prefix";
3642
+ }
3643
+ }
3644
+ return {
3645
+ model,
3646
+ modelKey: best?.model_key ?? null,
3647
+ origin: best?.origin ?? null,
3648
+ priceOrigin: best?.origin ?? null,
3649
+ source: best?.source ?? null,
3650
+ matchType,
3651
+ bindingType: best?.origin === "user" ? "custom" : best ? "automatic" : "none",
3652
+ hasManualBinding: false,
3653
+ hasPrice: Boolean(best)
3654
+ };
3655
+ });
3656
+ }
3657
+ function dryRunLocalModels(db) {
3658
+ const models = db.prepare("SELECT DISTINCT model FROM records WHERE model != 'unknown' ORDER BY model ASC").all();
3659
+ const unresolved = [];
3660
+ let matched = 0;
3661
+ for (const row of models) {
3662
+ if (resolvePriceFromRegistry(db, row.model)) matched++;
3663
+ else unresolved.push(row.model);
3664
+ }
3665
+ return { totalModels: models.length, matched, unresolved: unresolved.slice(0, 50) };
3666
+ }
3667
+ function getUserAliasBindings(db) {
3668
+ return db.prepare(`
3669
+ SELECT alias, model_key AS modelKey, origin, source
3670
+ FROM model_price_aliases
3671
+ WHERE origin = 'user' AND enabled = 1
3672
+ ORDER BY alias ASC
3673
+ `).all();
3674
+ }
3675
+
3196
3676
  // src/db/index.ts
3197
3677
  function initializeDatabase(db) {
3198
3678
  applyPragmas(db);
3199
3679
  runMigrations(db);
3680
+ loadPricingRuntime(db, loadConfig());
3200
3681
  }
3201
3682
  function createDatabase(path3) {
3202
- mkdirSync3(dirname2(path3), { recursive: true, mode: 448 });
3683
+ mkdirSync4(dirname3(path3), { recursive: true, mode: 448 });
3203
3684
  const db = new Database(path3);
3204
3685
  initializeDatabase(db);
3205
3686
  return db;
@@ -3912,6 +4393,31 @@ function mapRowToRecord(row) {
3912
4393
  };
3913
4394
  }
3914
4395
 
4396
+ // src/db/write-queue.ts
4397
+ var AsyncTaskQueue = class {
4398
+ tail = Promise.resolve();
4399
+ running = false;
4400
+ pending = 0;
4401
+ run(task) {
4402
+ this.pending++;
4403
+ const runTask = async () => {
4404
+ this.pending--;
4405
+ this.running = true;
4406
+ try {
4407
+ return await task();
4408
+ } finally {
4409
+ this.running = false;
4410
+ }
4411
+ };
4412
+ const result = this.tail.then(runTask, runTask);
4413
+ this.tail = result.catch(() => void 0);
4414
+ return result;
4415
+ }
4416
+ getStatus() {
4417
+ return { running: this.running, pending: this.pending };
4418
+ }
4419
+ };
4420
+
3915
4421
  // src/api/server.ts
3916
4422
  var pendingLeaderboardAuth = /* @__PURE__ */ new Map();
3917
4423
  function getManualImportMetadata(db) {
@@ -3946,11 +4452,39 @@ function countExistingRecordIds(db, ids) {
3946
4452
  }
3947
4453
  return count;
3948
4454
  }
3949
- function recalcCosts(db) {
4455
+ function emptyPricingRecalcStatus() {
4456
+ return {
4457
+ state: "idle",
4458
+ needsRecalc: false,
4459
+ needsRecalcSince: null,
4460
+ total: 0,
4461
+ processed: 0,
4462
+ updated: 0,
4463
+ skipped: 0,
4464
+ queueRunning: false,
4465
+ queuePending: 0,
4466
+ startedAt: null,
4467
+ finishedAt: null,
4468
+ error: null
4469
+ };
4470
+ }
4471
+ async function recalcCosts(db, onProgress) {
3950
4472
  const BATCH_SIZE2 = 1e3;
3951
4473
  let updated = 0;
4474
+ let processed = 0;
4475
+ let skipped = 0;
3952
4476
  let lastId = "";
3953
4477
  const exchangeRate = resolveExchangeRate(loadConfig() ?? {});
4478
+ const priceCache = /* @__PURE__ */ new Map();
4479
+ const resolveCachedPrice = (model) => {
4480
+ if (priceCache.has(model)) return priceCache.get(model) ?? void 0;
4481
+ const price = resolvePriceFromRegistry(db, model) ?? null;
4482
+ priceCache.set(model, price);
4483
+ return price ?? void 0;
4484
+ };
4485
+ const totalRow = db.prepare("SELECT COUNT(*) AS total FROM records").get();
4486
+ const total = totalRow.total;
4487
+ onProgress?.({ total, processed, updated, skipped });
3954
4488
  while (true) {
3955
4489
  const records = db.prepare(
3956
4490
  "SELECT id, tool, model, provider, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, thinking_tokens, cost, cost_source FROM records WHERE id > ? ORDER BY id LIMIT ?"
@@ -3959,19 +4493,23 @@ function recalcCosts(db) {
3959
4493
  const updateStmt = db.prepare("UPDATE records SET model = ?, provider = ?, cost = ?, cost_source = ?, updated_at = ? WHERE id = ?");
3960
4494
  const tx = db.transaction((batch) => {
3961
4495
  for (const r of batch) {
3962
- if (r.cost_source === "log") continue;
4496
+ processed++;
4497
+ if (r.cost_source === "log") {
4498
+ skipped++;
4499
+ continue;
4500
+ }
3963
4501
  const rawModel = r.tool === "qoder" ? normalizeQoderModel(r.model) : r.model;
3964
4502
  const model = rawModel === "unknown" ? r.tool === "qoder" ? "qoder-auto" : r.model : rawModel;
3965
4503
  const provider = model !== r.model ? inferProvider(model) : r.provider;
3966
- const hasPrice = resolvePrice(model) != null;
3967
- const cost = hasPrice ? calculateCost(model, {
4504
+ const price = resolveCachedPrice(model);
4505
+ const cost = price ? calculateCostForPrice(price, {
3968
4506
  inputTokens: r.input_tokens,
3969
4507
  outputTokens: r.output_tokens,
3970
4508
  cacheReadTokens: r.cache_read_tokens,
3971
4509
  cacheWriteTokens: r.cache_write_tokens,
3972
4510
  thinkingTokens: r.thinking_tokens
3973
4511
  }, exchangeRate) : 0;
3974
- const costSource = hasPrice ? "pricing" : "unknown";
4512
+ const costSource = price ? "pricing" : "unknown";
3975
4513
  if (model === r.model && provider === r.provider && cost === r.cost && costSource === r.cost_source) continue;
3976
4514
  updateStmt.run(model, provider, cost, costSource, Date.now(), r.id);
3977
4515
  updated++;
@@ -3979,6 +4517,8 @@ function recalcCosts(db) {
3979
4517
  });
3980
4518
  tx(records);
3981
4519
  lastId = records[records.length - 1].id;
4520
+ onProgress?.({ total, processed, updated, skipped });
4521
+ await new Promise((resolve2) => setImmediate(resolve2));
3982
4522
  }
3983
4523
  return updated;
3984
4524
  }
@@ -4016,6 +4556,17 @@ function json(res, data, status = 200) {
4016
4556
  res.writeHead(status, { "Content-Type": "application/json" });
4017
4557
  res.end(JSON.stringify(data));
4018
4558
  }
4559
+ function isDatabaseLockedError(error) {
4560
+ return error instanceof Error && /database is locked|SQLITE_BUSY/i.test(error.message);
4561
+ }
4562
+ function databaseBusy(res) {
4563
+ json(res, {
4564
+ error: {
4565
+ code: "DATABASE_BUSY",
4566
+ message: "Database is busy. Wait for the current refresh, sync, or import to finish and retry."
4567
+ }
4568
+ }, 503);
4569
+ }
4019
4570
  async function readJsonBody(req) {
4020
4571
  let body = "";
4021
4572
  for await (const chunk of req) body += chunk;
@@ -4101,6 +4652,23 @@ async function proxyLeaderboard(res, url) {
4101
4652
  json(res, getLeaderboardFallback(periodType));
4102
4653
  }
4103
4654
  }
4655
+ async function proxyCloudSyncStatus(res) {
4656
+ const upstream = new URL("/api/cli/sync/status", getSiteUrl());
4657
+ try {
4658
+ const response = await fetch(upstream, {
4659
+ headers: { Accept: "application/json" }
4660
+ });
4661
+ const contentType = response.headers.get("content-type") || "";
4662
+ if (!response.ok || !contentType.includes("application/json")) {
4663
+ json(res, { enabled: false });
4664
+ return;
4665
+ }
4666
+ const data = await response.json().catch(() => null);
4667
+ json(res, data && typeof data.enabled === "boolean" ? data : { enabled: false });
4668
+ } catch {
4669
+ json(res, { enabled: false });
4670
+ }
4671
+ }
4104
4672
  function getToolFilter(tool, prefix = "") {
4105
4673
  if (!tool) return { where: "", params: {} };
4106
4674
  const col = prefix ? `${prefix}.tool` : "tool";
@@ -4154,6 +4722,90 @@ function createApiServer(db, options) {
4154
4722
  const cfg = loadConfig();
4155
4723
  let weekStart = cfg?.weekStart ?? 1;
4156
4724
  const dashboardPassword = getDashboardPassword();
4725
+ const localWriteQueue = new AsyncTaskQueue();
4726
+ const runDbWrite = options?.runDbWrite ?? ((task) => localWriteQueue.run(task));
4727
+ const getDbWriteQueueStatus = options?.getDbWriteQueueStatus ?? (() => localWriteQueue.getStatus());
4728
+ let pricingRecalcStatus = emptyPricingRecalcStatus();
4729
+ let pricingNeedsRecalcSince = null;
4730
+ let pricingRecalcGeneration = 0;
4731
+ const currentPricingRecalcStatus = () => {
4732
+ const status = {
4733
+ ...pricingRecalcStatus,
4734
+ needsRecalc: pricingNeedsRecalcSince != null,
4735
+ needsRecalcSince: pricingNeedsRecalcSince
4736
+ };
4737
+ if (status.state !== "queued") return status;
4738
+ const queueStatus = getDbWriteQueueStatus();
4739
+ return {
4740
+ ...status,
4741
+ queueRunning: queueStatus.running,
4742
+ queuePending: queueStatus.pending
4743
+ };
4744
+ };
4745
+ const markPricingNeedsRecalc = () => {
4746
+ pricingRecalcGeneration += 1;
4747
+ pricingNeedsRecalcSince = Date.now();
4748
+ return pricingNeedsRecalcSince;
4749
+ };
4750
+ const startPricingRecalc = () => {
4751
+ if (pricingRecalcStatus.state === "queued" || pricingRecalcStatus.state === "running") {
4752
+ return { accepted: false, status: currentPricingRecalcStatus() };
4753
+ }
4754
+ const startedAt = Date.now();
4755
+ const startedGeneration = pricingRecalcGeneration;
4756
+ pricingRecalcStatus = {
4757
+ ...emptyPricingRecalcStatus(),
4758
+ state: "running",
4759
+ startedAt
4760
+ };
4761
+ const runRecalc = async () => {
4762
+ pricingRecalcStatus = {
4763
+ ...pricingRecalcStatus,
4764
+ state: "running",
4765
+ queueRunning: false,
4766
+ queuePending: 0,
4767
+ startedAt: pricingRecalcStatus.startedAt ?? Date.now()
4768
+ };
4769
+ try {
4770
+ await recalcCosts(db, (progress) => {
4771
+ pricingRecalcStatus = {
4772
+ ...pricingRecalcStatus,
4773
+ ...progress,
4774
+ state: "running"
4775
+ };
4776
+ });
4777
+ if (pricingNeedsRecalcSince != null && pricingRecalcGeneration === startedGeneration) {
4778
+ pricingNeedsRecalcSince = null;
4779
+ }
4780
+ pricingRecalcStatus = {
4781
+ ...pricingRecalcStatus,
4782
+ state: "done",
4783
+ needsRecalc: pricingNeedsRecalcSince != null,
4784
+ needsRecalcSince: pricingNeedsRecalcSince,
4785
+ finishedAt: Date.now(),
4786
+ error: null
4787
+ };
4788
+ } catch (error) {
4789
+ pricingRecalcStatus = {
4790
+ ...pricingRecalcStatus,
4791
+ state: "error",
4792
+ finishedAt: Date.now(),
4793
+ error: error instanceof Error ? error.message : String(error)
4794
+ };
4795
+ }
4796
+ };
4797
+ setImmediate(() => {
4798
+ void runRecalc().catch((error) => {
4799
+ pricingRecalcStatus = {
4800
+ ...pricingRecalcStatus,
4801
+ state: "error",
4802
+ finishedAt: Date.now(),
4803
+ error: error instanceof Error ? error.message : String(error)
4804
+ };
4805
+ });
4806
+ });
4807
+ return { accepted: true, status: currentPricingRecalcStatus() };
4808
+ };
4157
4809
  const server = http.createServer(async (req, res) => {
4158
4810
  const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
4159
4811
  res.setHeader("Access-Control-Allow-Origin", "*");
@@ -4192,6 +4844,10 @@ function createApiServer(db, options) {
4192
4844
  json(res, { ok: true });
4193
4845
  return;
4194
4846
  }
4847
+ if (url.pathname === "/api/cli/sync/status" && req.method === "GET") {
4848
+ await proxyCloudSyncStatus(res);
4849
+ return;
4850
+ }
4195
4851
  if (dashboardPassword && shouldProtectApiPath(url.pathname) && !isAuthenticated(dashboardPassword, req.headers.cookie)) {
4196
4852
  json(res, { error: { code: "UNAUTHORIZED", message: "Authentication required" } }, 401);
4197
4853
  return;
@@ -4807,72 +5463,48 @@ function createApiServer(db, options) {
4807
5463
  }
4808
5464
  if (url.pathname === "/api/pricing") {
4809
5465
  if (req.method === "GET") {
4810
- const table = getPriceTable();
4811
- const overrides = getUserOverrides();
4812
- const dbModels = db.prepare("SELECT model, COUNT(*) as usage_count FROM records WHERE model != 'unknown' GROUP BY model ORDER BY usage_count DESC, model ASC").all();
4813
- const models = dbModels.map((r) => {
4814
- const model = r.model;
4815
- const exactPrice = table[model];
4816
- const resolvedPrice = resolvePrice(model);
4817
- const isOverride = model in overrides;
4818
- const isDefault = model in DEFAULT_PRICE_TABLE && !isOverride;
4819
- let matchedBy = null;
4820
- if (!exactPrice && resolvedPrice) {
4821
- for (const prefix of Object.keys(table)) {
4822
- if (model.startsWith(prefix) || model.toLowerCase().startsWith(prefix)) {
4823
- matchedBy = prefix;
4824
- break;
4825
- }
4826
- }
4827
- if (!matchedBy) {
4828
- const stripped = model.replace(/^[^/]+\//, "").toLowerCase();
4829
- for (const prefix of Object.keys(table)) {
4830
- if (stripped.startsWith(prefix)) {
4831
- matchedBy = prefix;
4832
- break;
4833
- }
4834
- }
4835
- }
4836
- }
4837
- return {
4838
- model,
4839
- price: resolvedPrice ?? null,
4840
- currency: resolvedPrice?.currency ?? "USD",
4841
- isDefault,
4842
- isOverride,
4843
- matchedBy
4844
- };
4845
- });
4846
- json(res, { models, overrides });
5466
+ json(res, { models: listPricingModels(db), registry: getPricingRegistrySummary(db), targets: listPricingAliasTargets(db), aliasBindings: getUserAliasBindings(db), localBindings: listLocalModelBindings(db) });
4847
5467
  return;
4848
5468
  }
4849
5469
  if (req.method === "PUT") {
4850
5470
  let body = "";
4851
5471
  for await (const chunk of req) body += chunk;
5472
+ let data;
4852
5473
  try {
4853
- const data = JSON.parse(body);
4854
- if (!data.model || typeof data.input !== "number" || typeof data.output !== "number") {
4855
- json(res, { error: { code: "INVALID_PARAM", message: "model, input, output required" } }, 400);
4856
- return;
4857
- }
4858
- const entry = {
4859
- input: data.input,
4860
- output: data.output,
4861
- cacheRead: data.cacheRead,
4862
- cacheWrite: data.cacheWrite
4863
- };
4864
- if (data.currency === "CNY") {
4865
- entry.currency = "CNY";
4866
- }
4867
- setPriceOverride(data.model, entry);
4868
- const cfg2 = loadConfig() ?? {};
4869
- cfg2.priceOverrides = { ...cfg2.priceOverrides, [data.model]: entry };
4870
- saveConfig(cfg2);
4871
- const recalculated = recalcCosts(db);
4872
- json(res, { ok: true, recalculated });
5474
+ data = JSON.parse(body);
4873
5475
  } catch {
4874
5476
  json(res, { error: { code: "INVALID_JSON", message: "Invalid JSON body" } }, 400);
5477
+ return;
5478
+ }
5479
+ if (!data.model || typeof data.input !== "number" || typeof data.output !== "number") {
5480
+ json(res, { error: { code: "INVALID_PARAM", message: "model, input, output required" } }, 400);
5481
+ return;
5482
+ }
5483
+ const entry = {
5484
+ input: data.input,
5485
+ output: data.output,
5486
+ cacheRead: data.cacheRead,
5487
+ cacheWrite: data.cacheWrite
5488
+ };
5489
+ if (data.currency === "CNY") {
5490
+ entry.currency = "CNY";
5491
+ }
5492
+ try {
5493
+ await runDbWrite(() => {
5494
+ setUserPrice(db, data.model, entry);
5495
+ const cfg2 = loadConfig() ?? {};
5496
+ cfg2.priceOverrides = { ...cfg2.priceOverrides, [data.model]: entry };
5497
+ saveConfig(cfg2);
5498
+ loadPricingRuntime(db, cfg2);
5499
+ });
5500
+ } catch (error) {
5501
+ if (isDatabaseLockedError(error)) {
5502
+ databaseBusy(res);
5503
+ return;
5504
+ }
5505
+ throw error;
4875
5506
  }
5507
+ json(res, { ok: true, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
4876
5508
  return;
4877
5509
  }
4878
5510
  if (req.method === "DELETE") {
@@ -4881,20 +5513,94 @@ function createApiServer(db, options) {
4881
5513
  json(res, { error: { code: "INVALID_PARAM", message: "model param required" } }, 400);
4882
5514
  return;
4883
5515
  }
4884
- removePriceOverride(model);
4885
- const cfg2 = loadConfig() ?? {};
4886
- if (cfg2.priceOverrides) {
4887
- delete cfg2.priceOverrides[model];
4888
- saveConfig(cfg2);
5516
+ try {
5517
+ await runDbWrite(() => {
5518
+ resetUserPriceToSynced(db, model);
5519
+ removePriceOverride(model);
5520
+ const cfg2 = loadConfig() ?? {};
5521
+ if (cfg2.priceOverrides) {
5522
+ delete cfg2.priceOverrides[model];
5523
+ saveConfig(cfg2);
5524
+ }
5525
+ loadPricingRuntime(db, cfg2);
5526
+ });
5527
+ json(res, { ok: true, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
5528
+ } catch (error) {
5529
+ if (isDatabaseLockedError(error)) {
5530
+ databaseBusy(res);
5531
+ return;
5532
+ }
5533
+ throw error;
4889
5534
  }
4890
- const recalculated = recalcCosts(db);
4891
- json(res, { ok: true, recalculated });
4892
5535
  return;
4893
5536
  }
4894
5537
  }
5538
+ if (url.pathname === "/api/pricing/alias" && req.method === "POST") {
5539
+ try {
5540
+ const data = await readJsonBody(req);
5541
+ if (typeof data.alias !== "string" || typeof data.modelKey !== "string") {
5542
+ json(res, { error: { code: "INVALID_PARAM", message: "alias and modelKey required" } }, 400);
5543
+ return;
5544
+ }
5545
+ await runDbWrite(() => {
5546
+ setUserPricingAlias(db, data.alias, data.modelKey);
5547
+ loadPricingRuntime(db, loadConfig());
5548
+ });
5549
+ json(res, { ok: true, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
5550
+ } catch (error) {
5551
+ if (isDatabaseLockedError(error)) {
5552
+ databaseBusy(res);
5553
+ return;
5554
+ }
5555
+ json(res, { error: { code: "INVALID_PARAM", message: error instanceof Error ? error.message : "Invalid pricing alias" } }, 400);
5556
+ }
5557
+ return;
5558
+ }
5559
+ if (url.pathname === "/api/pricing/alias" && req.method === "DELETE") {
5560
+ const alias = url.searchParams.get("alias");
5561
+ if (!alias) {
5562
+ json(res, { error: { code: "INVALID_PARAM", message: "alias param required" } }, 400);
5563
+ return;
5564
+ }
5565
+ try {
5566
+ await runDbWrite(() => {
5567
+ removeUserPricingAlias(db, alias);
5568
+ loadPricingRuntime(db, loadConfig());
5569
+ });
5570
+ json(res, { ok: true, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
5571
+ } catch (error) {
5572
+ if (isDatabaseLockedError(error)) {
5573
+ databaseBusy(res);
5574
+ return;
5575
+ }
5576
+ json(res, { error: { code: "INVALID_PARAM", message: error instanceof Error ? error.message : "Invalid pricing alias" } }, 400);
5577
+ }
5578
+ return;
5579
+ }
5580
+ if (url.pathname === "/api/pricing/sync" && req.method === "POST") {
5581
+ try {
5582
+ const summary = await runDbWrite(async () => {
5583
+ const summary2 = await syncPricingFromLitellm(db);
5584
+ loadPricingRuntime(db, loadConfig());
5585
+ return summary2;
5586
+ });
5587
+ json(res, { ok: true, summary, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
5588
+ } catch (error) {
5589
+ if (isDatabaseLockedError(error)) {
5590
+ databaseBusy(res);
5591
+ return;
5592
+ }
5593
+ json(res, { error: { code: "SYNC_FAILED", message: error instanceof Error ? error.message : "Pricing sync failed" } }, 502);
5594
+ }
5595
+ return;
5596
+ }
5597
+ if (url.pathname === "/api/pricing/recalc" && req.method === "GET") {
5598
+ json(res, currentPricingRecalcStatus());
5599
+ return;
5600
+ }
4895
5601
  if (url.pathname === "/api/pricing/recalc" && req.method === "POST") {
4896
- const updated = recalcCosts(db);
4897
- json(res, { updated });
5602
+ const result = startPricingRecalc();
5603
+ json(res, result, result.accepted ? 202 : 200);
4898
5604
  return;
4899
5605
  }
4900
5606
  if (url.pathname === "/api/sync") {
@@ -4916,8 +5622,16 @@ function createApiServer(db, options) {
4916
5622
  json(res, { error: { code: "NOT_AVAILABLE", message: "Refresh not available" } }, 501);
4917
5623
  return;
4918
5624
  }
4919
- const result = await options.onRefresh();
4920
- json(res, result);
5625
+ try {
5626
+ const result = await runDbWrite(() => options.onRefresh());
5627
+ json(res, result);
5628
+ } catch (error) {
5629
+ if (isDatabaseLockedError(error)) {
5630
+ databaseBusy(res);
5631
+ return;
5632
+ }
5633
+ throw error;
5634
+ }
4921
5635
  return;
4922
5636
  }
4923
5637
  if (url.pathname === "/api/quotas" && req.method === "GET") {
@@ -5324,8 +6038,11 @@ function createApiServer(db, options) {
5324
6038
  json(res, { error: { code: "IMPORT_FAILED", message: result.errors[0] }, errors: result.errors }, 400);
5325
6039
  return;
5326
6040
  }
5327
- const existingCount = countExistingRecordIds(db, result.records.map((record) => record.id));
5328
- for (const record of result.records) insertRecord(db, record);
6041
+ const existingCount = await runDbWrite(() => {
6042
+ const existingCount2 = countExistingRecordIds(db, result.records.map((record) => record.id));
6043
+ for (const record of result.records) insertRecord(db, record);
6044
+ return existingCount2;
6045
+ });
5329
6046
  const imported = result.records.length;
5330
6047
  const added = Math.max(0, new Set(result.records.map((record) => record.id)).size - existingCount);
5331
6048
  json(res, {
@@ -6291,6 +7008,11 @@ function extractSessionId2(filePath, tool) {
6291
7008
  const parts = filePath.split("/");
6292
7009
  return (parts.pop() ?? "").replace(".jsonl", "");
6293
7010
  }
7011
+ if (tool === "pi") {
7012
+ const filename = filePath.split("/").pop() ?? "";
7013
+ const match = filename.match(/_([^_]+)\.jsonl$/);
7014
+ return match ? match[1] : filename.replace(".jsonl", "");
7015
+ }
6294
7016
  return "unknown";
6295
7017
  }
6296
7018
  function toNumber2(value) {
@@ -7097,7 +7819,7 @@ import { join as join9 } from "path";
7097
7819
 
7098
7820
  // src/db/synced-records.ts
7099
7821
  function insertSyncedRecord(db, record) {
7100
- db.prepare(`
7822
+ const result = db.prepare(`
7101
7823
  INSERT INTO synced_records (
7102
7824
  id, ts, tool, model, provider, input_tokens, output_tokens,
7103
7825
  cache_read_tokens, cache_write_tokens, thinking_tokens,
@@ -7150,6 +7872,7 @@ function insertSyncedRecord(db, record) {
7150
7872
  sourceFile: record.sourceFile ?? "",
7151
7873
  cwd: record.cwd ?? ""
7152
7874
  });
7875
+ return result.changes > 0;
7153
7876
  }
7154
7877
  function mergeSyncedRecordsIntoRecords(db) {
7155
7878
  const now = Date.now();
@@ -7338,8 +8061,8 @@ var SyncOrchestrator = class {
7338
8061
  if (typeof record.updatedAt === "string") {
7339
8062
  record.updatedAt = new Date(record.updatedAt).getTime();
7340
8063
  }
7341
- insertSyncedRecord(this.db, record);
7342
- totalPulled++;
8064
+ const changed = insertSyncedRecord(this.db, record);
8065
+ if (changed) totalPulled++;
7343
8066
  } catch {
7344
8067
  }
7345
8068
  }
@@ -8245,6 +8968,7 @@ var RuntimeSettingsController = class {
8245
8968
  leaderboardUploadTimer = null;
8246
8969
  syncTimer = null;
8247
8970
  parseInFlight = false;
8971
+ cleanupInFlight = false;
8248
8972
  leaderboardUploadInFlight = false;
8249
8973
  started = false;
8250
8974
  constructor(options) {
@@ -8297,11 +9021,7 @@ var RuntimeSettingsController = class {
8297
9021
  }
8298
9022
  if (retentionDays > 0) {
8299
9023
  this.cleanupTimer = setInterval(() => {
8300
- try {
8301
- this.runCleanupFn(this.db, retentionDays);
8302
- } catch (err) {
8303
- console.error("[settings-controller] cleanup failed:", err);
8304
- }
9024
+ void this.runCleanupSafely(retentionDays);
8305
9025
  }, this.cleanupIntervalMs);
8306
9026
  }
8307
9027
  if (config?.leaderboardAutoUpload === true && leaderboardUploadInterval > 0 && this.runLeaderboardUploadFn) {
@@ -8334,6 +9054,17 @@ var RuntimeSettingsController = class {
8334
9054
  this.parseInFlight = false;
8335
9055
  }
8336
9056
  }
9057
+ async runCleanupSafely(retentionDays) {
9058
+ if (this.cleanupInFlight) return;
9059
+ this.cleanupInFlight = true;
9060
+ try {
9061
+ await this.runCleanupFn(this.db, retentionDays);
9062
+ } catch (err) {
9063
+ console.error("[settings-controller] cleanup failed:", err);
9064
+ } finally {
9065
+ this.cleanupInFlight = false;
9066
+ }
9067
+ }
8337
9068
  async runLeaderboardUploadSafely() {
8338
9069
  if (this.leaderboardUploadInFlight) return;
8339
9070
  this.leaderboardUploadInFlight = true;
@@ -8365,11 +9096,8 @@ var MIME_TYPES = {
8365
9096
  };
8366
9097
  function serve(options) {
8367
9098
  const config = loadConfig();
8368
- if (config?.priceOverrides) {
8369
- for (const [model, entry] of Object.entries(config.priceOverrides)) {
8370
- setPriceOverride(model, entry);
8371
- }
8372
- }
9099
+ const dbWriteQueue = new AsyncTaskQueue();
9100
+ const runDbWrite = (task) => dbWriteQueue.run(task);
8373
9101
  if (config == null || config.exchangeRate == null) {
8374
9102
  const cacheAge = config?.exchangeRateCache ? Date.now() - config.exchangeRateCache.fetchedAt : Infinity;
8375
9103
  if (cacheAge >= CACHE_TTL_MS) {
@@ -8385,8 +9113,10 @@ function serve(options) {
8385
9113
  }
8386
9114
  const syncRuntime = new SyncRuntimeController({
8387
9115
  runSync: async (runtimeOptions) => {
8388
- await runParse(options.db);
8389
- await runSync(options.db, runtimeOptions);
9116
+ await runDbWrite(async () => {
9117
+ await runParse(options.db);
9118
+ await runSync(options.db, runtimeOptions);
9119
+ });
8390
9120
  },
8391
9121
  getPersistedState: () => getState(AIUSAGE_DIR),
8392
9122
  getCurrentTarget: () => getSyncTarget(loadConfig()?.sync)
@@ -8394,14 +9124,14 @@ function serve(options) {
8394
9124
  const runtimeSettings = new RuntimeSettingsController({
8395
9125
  db: options.db,
8396
9126
  loadConfig,
8397
- runParse,
8398
- runCleanup: cleanOldData,
9127
+ runParse: (db) => runDbWrite(() => runParse(db)),
9128
+ runCleanup: (db, retentionDays) => runDbWrite(() => cleanOldData(db, retentionDays)),
8399
9129
  runLeaderboardUpload: (db) => uploadLeaderboardData(db, getState(AIUSAGE_DIR)?.deviceInstanceId).then(() => void 0),
8400
9130
  runSync: () => syncRuntime.start(),
8401
9131
  onSyncScheduleChanged: (ts3) => syncRuntime.setNextSyncAt(ts3)
8402
9132
  });
8403
9133
  runtimeSettings.start();
8404
- runParse(options.db).catch((err) => {
9134
+ runDbWrite(() => runParse(options.db)).catch((err) => {
8405
9135
  console.error("[serve] initial parse failed:", err);
8406
9136
  });
8407
9137
  const apiServer = createApiServer(options.db, {
@@ -8409,12 +9139,14 @@ function serve(options) {
8409
9139
  onRefresh: () => runParse(options.db),
8410
9140
  onSyncStart: () => syncRuntime.start(),
8411
9141
  getSyncStatus: () => syncRuntime.getStatus(),
8412
- onConfigUpdated: () => runtimeSettings.reload()
9142
+ onConfigUpdated: () => runtimeSettings.reload(),
9143
+ runDbWrite,
9144
+ getDbWriteQueueStatus: () => dbWriteQueue.getStatus()
8413
9145
  });
8414
9146
  const webBuildDir = (() => {
8415
- const prodDir = join11(dirname3(fileURLToPath(import.meta.url)), "web");
9147
+ const prodDir = join11(dirname4(fileURLToPath(import.meta.url)), "web");
8416
9148
  if (existsSync10(prodDir)) return prodDir;
8417
- return join11(dirname3(fileURLToPath(import.meta.url)), "..", "..", "..", "web", "build");
9149
+ return join11(dirname4(fileURLToPath(import.meta.url)), "..", "..", "..", "web", "build");
8418
9150
  })();
8419
9151
  if (!existsSync10(webBuildDir)) {
8420
9152
  console.error("Web dashboard not found. Reinstall the package: npm install -g @juliantanx/aiusage");
@@ -8739,15 +9471,15 @@ function recalcPricing(db) {
8739
9471
  }
8740
9472
  const model = record.tool === "qoder" ? normalizeQoderModel(record.model) : record.model;
8741
9473
  const provider = model !== record.model ? inferProvider(model) : record.provider;
8742
- const hasPrice = resolvePrice(model) != null;
8743
- const newCost = hasPrice ? calculateCost(model, {
9474
+ const price = resolvePriceFromRegistry(db, model);
9475
+ const newCost = price ? calculateCostForPrice(price, {
8744
9476
  inputTokens: record.input_tokens,
8745
9477
  outputTokens: record.output_tokens,
8746
9478
  cacheReadTokens: record.cache_read_tokens,
8747
9479
  cacheWriteTokens: record.cache_write_tokens,
8748
9480
  thinkingTokens: record.thinking_tokens
8749
9481
  }, exchangeRate) : 0;
8750
- const costSource = hasPrice ? "pricing" : "unknown";
9482
+ const costSource = price ? "pricing" : "unknown";
8751
9483
  if (model !== record.model || provider !== record.provider || newCost !== record.cost || costSource !== record.cost_source) {
8752
9484
  updateStmt.run(model, provider, newCost, costSource, Date.now(), record.id);
8753
9485
  updatedCount++;
@@ -9651,7 +10383,7 @@ ${dashboardStatusLine()}
9651
10383
  import { join as join15 } from "path";
9652
10384
  var DB_PATH2 = join15(AIUSAGE_DIR, "cache.db");
9653
10385
  var program = new Command();
9654
- program.name("aiusage").version(true ? "1.5.2" : "dev").description("CLI tool for AI usage statistics");
10386
+ program.name("aiusage").version(true ? "1.5.3" : "dev").description("CLI tool for AI usage statistics");
9655
10387
  program.action(() => {
9656
10388
  const unknownCommand = program.args.find((arg) => typeof arg === "string");
9657
10389
  if (unknownCommand) {