@juliantanx/aiusage 1.5.2 → 1.5.4

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 (38) hide show
  1. package/README.md +5 -5
  2. package/dist/index.js +1236 -319
  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.Cxpjn2cB.js → entry.DhxonIJY.js} +2 -2
  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.BPMmgslX.js} +1 -1
  11. package/dist/web/_app/immutable/entry/{app.BzFxp-0D.js → app.0TSkMIPi.js} +2 -2
  12. package/dist/web/_app/immutable/entry/start.CnOi0uqd.js +1 -0
  13. package/dist/web/_app/immutable/nodes/{0.5419U2xS.js → 0.DrwtEJ21.js} +2 -2
  14. package/dist/web/_app/immutable/nodes/{1.C4em9nwH.js → 1.DQwsmuSX.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.B1soyRkK.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.CJH1Fs0K.js} +2 -2
  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/i18n.yxU9Ieh-.js +0 -1
  36. package/dist/web/_app/immutable/entry/start.C2M6huNJ.js +0 -1
  37. package/dist/web/_app/immutable/nodes/12.B5loVhie.js +0 -1
  38. 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
@@ -49,7 +49,8 @@ var TOOLS = [
49
49
  "omp",
50
50
  "pi",
51
51
  "craft",
52
- "droid"
52
+ "droid",
53
+ "zcode"
53
54
  ];
54
55
  var MODEL_PROVIDER_MAP = [
55
56
  ["claude-", "anthropic"],
@@ -112,204 +113,22 @@ async function fetchExchangeRate() {
112
113
  function convertToUSD(cny, rate) {
113
114
  return cny * rate;
114
115
  }
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
- };
116
+ var basePriceTable = {};
297
117
  var userOverrides = {};
298
- var PRICE_TABLE = { ...DEFAULT_PRICE_TABLE };
299
- function getPriceTable() {
300
- return { ...DEFAULT_PRICE_TABLE, ...userOverrides };
118
+ var PRICE_TABLE = {};
119
+ function setRuntimePriceTable(base, overrides = {}) {
120
+ basePriceTable = { ...base };
121
+ userOverrides = { ...overrides };
122
+ PRICE_TABLE = { ...basePriceTable, ...userOverrides };
301
123
  }
302
124
  function setPriceOverride(model, entry) {
303
125
  userOverrides = { ...userOverrides, [model]: entry };
304
- PRICE_TABLE = { ...DEFAULT_PRICE_TABLE, ...userOverrides };
126
+ PRICE_TABLE = { ...basePriceTable, ...userOverrides };
305
127
  }
306
128
  function removePriceOverride(model) {
307
129
  const { [model]: _, ...rest } = userOverrides;
308
130
  userOverrides = rest;
309
- PRICE_TABLE = { ...DEFAULT_PRICE_TABLE, ...userOverrides };
310
- }
311
- function getUserOverrides() {
312
- return { ...userOverrides };
131
+ PRICE_TABLE = { ...basePriceTable, ...userOverrides };
313
132
  }
314
133
  var PROVIDER_PREFIXES = [
315
134
  "accounts/fireworks/models/",
@@ -366,6 +185,15 @@ function calculateCostWithResolver(model, tokens, exchangeRate, resolver) {
366
185
  function calculateCost(model, tokens, exchangeRate) {
367
186
  return calculateCostWithResolver(model, tokens, exchangeRate, resolvePrice);
368
187
  }
188
+ function calculateCostForPrice(price, tokens, exchangeRate) {
189
+ const inputCost = tokens.inputTokens / 1e6 * price.input;
190
+ const outputCost = tokens.outputTokens / 1e6 * price.output;
191
+ const cacheReadCost = tokens.cacheReadTokens / 1e6 * (price.cacheRead ?? 0);
192
+ const cacheWriteCost = tokens.cacheWriteTokens / 1e6 * (price.cacheWrite ?? 0);
193
+ const thinkingCost = tokens.thinkingTokens / 1e6 * price.output;
194
+ const rawCost = inputCost + outputCost + cacheReadCost + cacheWriteCost + thinkingCost;
195
+ return price.currency === "CNY" ? convertToUSD(rawCost, exchangeRate ?? FALLBACK_RATE) : rawCost;
196
+ }
369
197
  var QODER_TIER_MODELS = /* @__PURE__ */ new Set([
370
198
  "auto",
371
199
  "ultimate",
@@ -430,8 +258,9 @@ var ClaudeCodeParser = class {
430
258
  }, context.exchangeRate);
431
259
  const costSource = model === "unknown" ? "unknown" : "pricing";
432
260
  const provider = inferProvider(model);
261
+ const messageId = parsed.message.id;
433
262
  const record = {
434
- id: generateRecordId(context.deviceInstanceId, context.sourceFile, context.lineOffset),
263
+ id: messageId ? generateRecordId(context.deviceInstanceId, messageId, 0) : generateRecordId(context.deviceInstanceId, context.sourceFile, context.lineOffset),
435
264
  ts: ts22,
436
265
  ingestedAt: context.now,
437
266
  updatedAt: context.now,
@@ -1051,14 +880,14 @@ function usageFromAny(parsed) {
1051
880
  const cachedFromDetails = num(details.cached_tokens);
1052
881
  const rawInput = usage.input_tokens ?? usage.input ?? usage.prompt_tokens ?? usage.input_other ?? usage.tokensIn ?? 0;
1053
882
  const cacheRead = num(
1054
- usage.cache_read_input_tokens ?? usage.cache_read_tokens ?? usage.input_cache_read ?? usage.cacheReads ?? cachedFromDetails
883
+ usage.cache_read_input_tokens ?? usage.cache_read_tokens ?? usage.input_cache_read ?? usage.cacheReads ?? usage.cacheRead ?? cachedFromDetails
1055
884
  );
1056
885
  return {
1057
886
  inputTokens: Math.max(0, num(rawInput) - cachedFromDetails),
1058
887
  outputTokens: num(usage.output_tokens ?? usage.output ?? usage.completion_tokens ?? usage.tokensOut),
1059
888
  cacheReadTokens: cacheRead,
1060
889
  cacheWriteTokens: num(
1061
- usage.cache_creation_input_tokens ?? usage.cache_write_input_tokens ?? usage.cache_write_tokens ?? usage.input_cache_creation ?? usage.cacheWrites
890
+ usage.cache_creation_input_tokens ?? usage.cache_write_input_tokens ?? usage.cache_write_tokens ?? usage.input_cache_creation ?? usage.cacheWrites ?? usage.cacheWrite
1062
891
  ),
1063
892
  thinkingTokens: num(
1064
893
  usage.thinking_tokens ?? usage.reasoning_tokens ?? usage.reasoning_output_tokens ?? details.reasoning_tokens
@@ -1202,8 +1031,8 @@ var Aggregator = class {
1202
1031
  };
1203
1032
 
1204
1033
  // src/config.ts
1205
- import { readFileSync, writeFileSync, existsSync } from "fs";
1206
- import { join } from "path";
1034
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
1035
+ import { join, dirname } from "path";
1207
1036
  import { homedir } from "os";
1208
1037
  var AIUSAGE_DIR = join(homedir(), ".aiusage");
1209
1038
  var CONFIG_PATH = join(AIUSAGE_DIR, "config.json");
@@ -1233,6 +1062,7 @@ function loadConfig() {
1233
1062
  }
1234
1063
  }
1235
1064
  function saveConfig(config) {
1065
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 448 });
1236
1066
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
1237
1067
  }
1238
1068
  function buildConsentConfig(config) {
@@ -1260,7 +1090,7 @@ function loadCredential(key) {
1260
1090
  }
1261
1091
 
1262
1092
  // src/init.ts
1263
- import { mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
1093
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
1264
1094
  import { join as join2 } from "path";
1265
1095
  import { randomUUID } from "crypto";
1266
1096
  function getState(aiusageDir) {
@@ -1632,6 +1462,9 @@ function defaultZedDbPath() {
1632
1462
  }
1633
1463
  return join3(xdgDataDir(home, "zed"), "threads", "threads.db");
1634
1464
  }
1465
+ function defaultZcodeDbPath() {
1466
+ return join3(homedir2(), ".zcode", "cli", "db", "db.sqlite");
1467
+ }
1635
1468
  function probeClaudeCode(ctx) {
1636
1469
  const override = envOverride("claude-code", ctx.env);
1637
1470
  if (override) return override;
@@ -1843,6 +1676,15 @@ function probeZed(ctx) {
1843
1676
  const dbPath = defaultZedDbPath();
1844
1677
  return existsSync3(dbPath) ? dbPath : null;
1845
1678
  }
1679
+ function probeZcode(ctx) {
1680
+ const override = envOverride("zcode", ctx.env);
1681
+ if (override) return override;
1682
+ if (ctx.env.ZCODE_HOME) return join3(ctx.env.ZCODE_HOME, "cli", "db", "db.sqlite");
1683
+ const legacy = ctx.legacySources?.["zcode"];
1684
+ if (legacy) return legacy;
1685
+ const dbPath = defaultZcodeDbPath();
1686
+ return existsSync3(dbPath) ? dbPath : null;
1687
+ }
1846
1688
  function probeGoose(ctx) {
1847
1689
  const override = envOverride("goose", ctx.env);
1848
1690
  if (override) return override;
@@ -1925,6 +1767,7 @@ var TOOL_REGISTRY = [
1925
1767
  { tool: "antigravity", sourceKey: "antigravity", label: "Antigravity", probe: probeAntigravity },
1926
1768
  { tool: "roocode", sourceKey: "roocode", label: "Roo Code", probe: probeRooCode },
1927
1769
  { tool: "zed", sourceKey: "zed", label: "Zed", probe: probeZed },
1770
+ { tool: "zcode", sourceKey: "zcode", label: "ZCode", probe: probeZcode },
1928
1771
  { tool: "goose", sourceKey: "goose", label: "Goose", probe: probeGoose },
1929
1772
  { tool: "omp", sourceKey: "omp", label: "oh-my-pi", probe: probeOmp },
1930
1773
  { tool: "pi", sourceKey: "pi", label: "pi", probe: probePi },
@@ -2640,14 +2483,14 @@ function sha256Buffer(data) {
2640
2483
  }
2641
2484
 
2642
2485
  // 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";
2486
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync, existsSync as existsSync6, renameSync } from "fs";
2487
+ import { join as join5, dirname as dirname2 } from "path";
2645
2488
  import { randomBytes as randomBytes2 } from "crypto";
2646
2489
  var CREDENTIALS_FILE = join5(AIUSAGE_DIR, "leaderboard-credentials.json");
2647
2490
  function saveCredentials(creds) {
2648
- const dir = dirname(CREDENTIALS_FILE);
2491
+ const dir = dirname2(CREDENTIALS_FILE);
2649
2492
  if (!existsSync6(dir)) {
2650
- mkdirSync2(dir, { recursive: true });
2493
+ mkdirSync3(dir, { recursive: true });
2651
2494
  }
2652
2495
  const tmpFile = `${CREDENTIALS_FILE}.${randomBytes2(4).toString("hex")}.tmp`;
2653
2496
  try {
@@ -2866,14 +2709,14 @@ async function fetchLeaderboardStatus(serverUrl) {
2866
2709
 
2867
2710
  // src/db/index.ts
2868
2711
  import Database from "better-sqlite3";
2869
- import { mkdirSync as mkdirSync3 } from "fs";
2870
- import { dirname as dirname2 } from "path";
2712
+ import { mkdirSync as mkdirSync4 } from "fs";
2713
+ import { dirname as dirname3 } from "path";
2871
2714
 
2872
2715
  // src/db/schema.ts
2873
2716
  function applyPragmas(db) {
2874
2717
  db.pragma("journal_mode = WAL");
2875
2718
  db.pragma("foreign_keys = ON");
2876
- db.pragma("busy_timeout = 5000");
2719
+ db.pragma("busy_timeout = 30000");
2877
2720
  }
2878
2721
  function createReadonlyViews(db) {
2879
2722
  db.exec(`
@@ -3166,6 +3009,79 @@ function migrateV9(db) {
3166
3009
  `);
3167
3010
  }
3168
3011
 
3012
+ // src/db/migrations/v10.ts
3013
+ function migrateV10(db) {
3014
+ db.exec(`
3015
+ CREATE TABLE IF NOT EXISTS model_prices (
3016
+ model_key TEXT PRIMARY KEY,
3017
+ provider TEXT NOT NULL DEFAULT '',
3018
+ input REAL NOT NULL,
3019
+ output REAL NOT NULL,
3020
+ cache_read REAL,
3021
+ cache_write REAL,
3022
+ currency TEXT NOT NULL DEFAULT 'USD',
3023
+ source TEXT NOT NULL DEFAULT 'manual',
3024
+ source_model_id TEXT,
3025
+ source_url TEXT,
3026
+ origin TEXT NOT NULL DEFAULT 'user',
3027
+ status TEXT NOT NULL DEFAULT 'active',
3028
+ last_synced_at INTEGER,
3029
+ created_at INTEGER NOT NULL,
3030
+ updated_at INTEGER NOT NULL
3031
+ );
3032
+
3033
+ CREATE TABLE IF NOT EXISTS model_price_aliases (
3034
+ alias TEXT PRIMARY KEY,
3035
+ model_key TEXT NOT NULL REFERENCES model_prices(model_key) ON DELETE CASCADE,
3036
+ match_type TEXT NOT NULL DEFAULT 'exact',
3037
+ provider TEXT NOT NULL DEFAULT '',
3038
+ priority INTEGER NOT NULL DEFAULT 100,
3039
+ source TEXT NOT NULL DEFAULT 'manual',
3040
+ origin TEXT NOT NULL DEFAULT 'user',
3041
+ enabled INTEGER NOT NULL DEFAULT 1,
3042
+ created_at INTEGER NOT NULL,
3043
+ updated_at INTEGER NOT NULL
3044
+ );
3045
+
3046
+ CREATE INDEX IF NOT EXISTS idx_model_prices_origin ON model_prices(origin);
3047
+ CREATE INDEX IF NOT EXISTS idx_model_prices_source ON model_prices(source);
3048
+ CREATE INDEX IF NOT EXISTS idx_model_price_aliases_model ON model_price_aliases(model_key);
3049
+
3050
+ INSERT INTO schema_version (version) VALUES (10);
3051
+ `);
3052
+ }
3053
+
3054
+ // src/db/migrations/v11.ts
3055
+ function migrateV11(db) {
3056
+ db.exec(`
3057
+ CREATE TABLE IF NOT EXISTS model_price_sync_baselines (
3058
+ model_key TEXT PRIMARY KEY,
3059
+ provider TEXT NOT NULL DEFAULT '',
3060
+ input REAL NOT NULL,
3061
+ output REAL NOT NULL,
3062
+ cache_read REAL,
3063
+ cache_write REAL,
3064
+ currency TEXT NOT NULL DEFAULT 'USD',
3065
+ source TEXT NOT NULL DEFAULT 'litellm',
3066
+ source_model_id TEXT,
3067
+ source_url TEXT,
3068
+ last_synced_at INTEGER NOT NULL,
3069
+ updated_at INTEGER NOT NULL
3070
+ );
3071
+
3072
+ INSERT INTO model_price_sync_baselines (
3073
+ model_key, provider, input, output, cache_read, cache_write, currency, source,
3074
+ source_model_id, source_url, last_synced_at, updated_at
3075
+ )
3076
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source,
3077
+ source_model_id, source_url, COALESCE(last_synced_at, updated_at), updated_at
3078
+ FROM model_prices
3079
+ WHERE origin = 'builtin' AND source != 'manual';
3080
+
3081
+ INSERT INTO schema_version (version) VALUES (11);
3082
+ `);
3083
+ }
3084
+
3169
3085
  // src/db/migrations/index.ts
3170
3086
  var MIGRATIONS = [
3171
3087
  { version: 1, migrate: migrateV1 },
@@ -3176,7 +3092,9 @@ var MIGRATIONS = [
3176
3092
  { version: 6, migrate: migrateV6 },
3177
3093
  { version: 7, migrate: migrateV7 },
3178
3094
  { version: 8, migrate: migrateV8 },
3179
- { version: 9, migrate: migrateV9 }
3095
+ { version: 9, migrate: migrateV9 },
3096
+ { version: 10, migrate: migrateV10 },
3097
+ { version: 11, migrate: migrateV11 }
3180
3098
  ];
3181
3099
  function runMigrations(db) {
3182
3100
  createSchemaVersionTable(db);
@@ -3193,13 +3111,591 @@ function runMigrations(db) {
3193
3111
  }
3194
3112
  }
3195
3113
 
3114
+ // src/pricing-registry.ts
3115
+ var LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
3116
+ function rowToPrice(row) {
3117
+ return {
3118
+ input: Number(row.input),
3119
+ output: Number(row.output),
3120
+ cacheRead: row.cache_read == null ? void 0 : Number(row.cache_read),
3121
+ cacheWrite: row.cache_write == null ? void 0 : Number(row.cache_write),
3122
+ currency: row.currency
3123
+ };
3124
+ }
3125
+ function normalizeProvider(provider) {
3126
+ return (provider ?? "").trim().toLowerCase();
3127
+ }
3128
+ function normalizeModelKey(sourceModelId) {
3129
+ const trimmed = sourceModelId.trim();
3130
+ const slash = trimmed.indexOf("/");
3131
+ return slash > 0 ? trimmed.slice(slash + 1) : trimmed;
3132
+ }
3133
+ function aliasesFor(sourceModelId, modelKey) {
3134
+ const aliases = /* @__PURE__ */ new Set([modelKey, sourceModelId]);
3135
+ if (modelKey.startsWith("claude-")) aliases.add(modelKey.replace(/-\d{8}$/, ""));
3136
+ return [...aliases].filter(Boolean);
3137
+ }
3138
+ function litellmEntryToPrice(sourceModelId, entry) {
3139
+ if (entry.mode && entry.mode !== "chat" && entry.mode !== "completion" && entry.mode !== "responses") return null;
3140
+ if (entry.input_cost_per_second != null || entry.output_cost_per_second != null) return null;
3141
+ if (typeof entry.input_cost_per_token !== "number" || typeof entry.output_cost_per_token !== "number") return null;
3142
+ const modelKey = normalizeModelKey(sourceModelId);
3143
+ if (!modelKey || modelKey.includes("*")) return null;
3144
+ return {
3145
+ modelKey,
3146
+ provider: normalizeProvider(entry.litellm_provider),
3147
+ price: {
3148
+ input: entry.input_cost_per_token * 1e6,
3149
+ output: entry.output_cost_per_token * 1e6,
3150
+ cacheRead: typeof entry.cache_read_input_token_cost === "number" ? entry.cache_read_input_token_cost * 1e6 : void 0,
3151
+ cacheWrite: typeof entry.cache_creation_input_token_cost === "number" ? entry.cache_creation_input_token_cost * 1e6 : void 0,
3152
+ currency: "USD"
3153
+ }
3154
+ };
3155
+ }
3156
+ function loadBuiltinRows(db) {
3157
+ return db.prepare(`
3158
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3159
+ source_url, origin, status, last_synced_at, updated_at
3160
+ FROM model_prices
3161
+ WHERE origin = 'builtin' AND status = 'active'
3162
+ `).all();
3163
+ }
3164
+ function loadUserRows(db) {
3165
+ return db.prepare(`
3166
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3167
+ source_url, origin, status, last_synced_at, updated_at
3168
+ FROM model_prices
3169
+ WHERE origin = 'user' AND status = 'active'
3170
+ `).all();
3171
+ }
3172
+ function loadSyncedBaselineRows(db) {
3173
+ return db.prepare(`
3174
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3175
+ source_url, last_synced_at, updated_at
3176
+ FROM model_price_sync_baselines
3177
+ `).all();
3178
+ }
3179
+ function loadAliasRows(db) {
3180
+ return db.prepare(`
3181
+ SELECT alias, model_key, match_type, provider, priority, source, origin, enabled
3182
+ FROM model_price_aliases
3183
+ WHERE enabled = 1
3184
+ ORDER BY priority ASC
3185
+ `).all();
3186
+ }
3187
+ function loadPricingRuntime(db, config) {
3188
+ const builtin = {};
3189
+ for (const row of loadBuiltinRows(db)) builtin[row.model_key] = rowToPrice(row);
3190
+ const overrides = {};
3191
+ const userRows = loadUserRows(db);
3192
+ const configOverrides = config?.priceOverrides ?? {};
3193
+ const activePrices = priceTableFromDb(db);
3194
+ for (const alias of loadAliasRows(db)) {
3195
+ const price = activePrices[alias.model_key];
3196
+ if (!price) continue;
3197
+ if (alias.origin === "user") overrides[alias.alias] = price;
3198
+ else builtin[alias.alias] = price;
3199
+ }
3200
+ for (const row of userRows) overrides[row.model_key] = rowToPrice(row);
3201
+ for (const [model, entry] of Object.entries(configOverrides)) overrides[model] = entry;
3202
+ setRuntimePriceTable(builtin, overrides);
3203
+ }
3204
+ function setUserPrice(db, modelKey, entry) {
3205
+ const now = Date.now();
3206
+ const baseline = db.prepare(`
3207
+ SELECT provider, source, source_model_id, source_url, last_synced_at
3208
+ FROM model_price_sync_baselines
3209
+ WHERE model_key = ?
3210
+ `).get(modelKey);
3211
+ db.prepare(`
3212
+ INSERT INTO model_prices (
3213
+ model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3214
+ source_url, origin, status, last_synced_at, created_at, updated_at
3215
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'manual', ?, ?, 'user', 'active', ?, ?, ?)
3216
+ ON CONFLICT(model_key) DO UPDATE SET
3217
+ provider = excluded.provider,
3218
+ input = excluded.input,
3219
+ output = excluded.output,
3220
+ cache_read = excluded.cache_read,
3221
+ cache_write = excluded.cache_write,
3222
+ currency = excluded.currency,
3223
+ source = 'manual',
3224
+ source_model_id = excluded.source_model_id,
3225
+ source_url = excluded.source_url,
3226
+ origin = 'user',
3227
+ status = 'active',
3228
+ last_synced_at = excluded.last_synced_at,
3229
+ updated_at = excluded.updated_at
3230
+ `).run(
3231
+ modelKey,
3232
+ baseline?.provider ?? "",
3233
+ entry.input,
3234
+ entry.output,
3235
+ entry.cacheRead ?? null,
3236
+ entry.cacheWrite ?? null,
3237
+ entry.currency ?? "USD",
3238
+ baseline?.source_model_id ?? modelKey,
3239
+ baseline?.source_url ?? null,
3240
+ baseline?.last_synced_at ?? null,
3241
+ now,
3242
+ now
3243
+ );
3244
+ setPriceOverride(modelKey, entry);
3245
+ }
3246
+ function resetUserPriceToSynced(db, modelKey) {
3247
+ const baseline = db.prepare(`
3248
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3249
+ source_url, last_synced_at, updated_at
3250
+ FROM model_price_sync_baselines
3251
+ WHERE model_key = ?
3252
+ `).get(modelKey);
3253
+ if (!baseline) {
3254
+ removeUserPrice(db, modelKey);
3255
+ return;
3256
+ }
3257
+ const now = Date.now();
3258
+ db.prepare("DELETE FROM model_price_aliases WHERE alias = ? AND model_key = ? AND origin = 'user'").run(modelKey, modelKey);
3259
+ db.prepare(`
3260
+ INSERT INTO model_prices (
3261
+ model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3262
+ source_url, origin, status, last_synced_at, created_at, updated_at
3263
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'builtin', 'active', ?, ?, ?)
3264
+ ON CONFLICT(model_key) DO UPDATE SET
3265
+ provider = excluded.provider,
3266
+ input = excluded.input,
3267
+ output = excluded.output,
3268
+ cache_read = excluded.cache_read,
3269
+ cache_write = excluded.cache_write,
3270
+ currency = excluded.currency,
3271
+ source = excluded.source,
3272
+ source_model_id = excluded.source_model_id,
3273
+ source_url = excluded.source_url,
3274
+ origin = 'builtin',
3275
+ status = 'active',
3276
+ last_synced_at = excluded.last_synced_at,
3277
+ updated_at = excluded.updated_at
3278
+ `).run(
3279
+ baseline.model_key,
3280
+ baseline.provider,
3281
+ baseline.input,
3282
+ baseline.output,
3283
+ baseline.cache_read,
3284
+ baseline.cache_write,
3285
+ baseline.currency,
3286
+ baseline.source,
3287
+ baseline.source_model_id,
3288
+ baseline.source_url,
3289
+ baseline.last_synced_at,
3290
+ now,
3291
+ now
3292
+ );
3293
+ }
3294
+ function setUserPricingAlias(db, alias, modelKey) {
3295
+ const aliasKey = alias.trim();
3296
+ const targetKey = modelKey.trim();
3297
+ if (!aliasKey || !targetKey) throw new Error("alias and modelKey required");
3298
+ const target = db.prepare(`
3299
+ SELECT provider
3300
+ FROM model_prices
3301
+ WHERE model_key = ? AND status = 'active'
3302
+ `).get(targetKey);
3303
+ if (!target) throw new Error("Pricing model not found");
3304
+ const now = Date.now();
3305
+ db.prepare(`
3306
+ INSERT INTO model_price_aliases (alias, model_key, match_type, provider, priority, source, origin, enabled, created_at, updated_at)
3307
+ VALUES (?, ?, 'exact', ?, 200, 'manual', 'user', 1, ?, ?)
3308
+ ON CONFLICT(alias) DO UPDATE SET
3309
+ model_key = excluded.model_key,
3310
+ match_type = 'exact',
3311
+ provider = excluded.provider,
3312
+ priority = excluded.priority,
3313
+ source = 'manual',
3314
+ origin = 'user',
3315
+ enabled = 1,
3316
+ updated_at = excluded.updated_at
3317
+ `).run(aliasKey, targetKey, target.provider ?? "", now, now);
3318
+ }
3319
+ function removeUserPricingAlias(db, alias) {
3320
+ const aliasKey = alias.trim();
3321
+ if (!aliasKey) throw new Error("alias required");
3322
+ db.prepare("DELETE FROM model_price_aliases WHERE alias = ? AND origin = 'user'").run(aliasKey);
3323
+ }
3324
+ function removeUserPrice(db, modelKey) {
3325
+ db.prepare("DELETE FROM model_price_aliases WHERE alias = ? AND model_key = ? AND origin = 'user'").run(modelKey, modelKey);
3326
+ db.prepare("DELETE FROM model_prices WHERE model_key = ? AND origin = 'user'").run(modelKey);
3327
+ }
3328
+ function upsertBuiltinPrice(db, entry, now) {
3329
+ db.prepare(`
3330
+ INSERT INTO model_price_sync_baselines (
3331
+ model_key, provider, input, output, cache_read, cache_write, currency, source,
3332
+ source_model_id, source_url, last_synced_at, updated_at
3333
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'litellm', ?, ?, ?, ?)
3334
+ ON CONFLICT(model_key) DO UPDATE SET
3335
+ provider = excluded.provider,
3336
+ input = excluded.input,
3337
+ output = excluded.output,
3338
+ cache_read = excluded.cache_read,
3339
+ cache_write = excluded.cache_write,
3340
+ currency = excluded.currency,
3341
+ source = excluded.source,
3342
+ source_model_id = excluded.source_model_id,
3343
+ source_url = excluded.source_url,
3344
+ last_synced_at = excluded.last_synced_at,
3345
+ updated_at = excluded.updated_at
3346
+ `).run(
3347
+ entry.modelKey,
3348
+ entry.provider,
3349
+ entry.price.input,
3350
+ entry.price.output,
3351
+ entry.price.cacheRead ?? null,
3352
+ entry.price.cacheWrite ?? null,
3353
+ entry.price.currency ?? "USD",
3354
+ entry.sourceModelId,
3355
+ entry.sourceUrl,
3356
+ now,
3357
+ now
3358
+ );
3359
+ const existing = db.prepare("SELECT origin, input, output, cache_read, cache_write, currency FROM model_prices WHERE model_key = ?").get(entry.modelKey);
3360
+ if (existing?.origin === "user") return "user_preserved";
3361
+ const next = [entry.price.input, entry.price.output, entry.price.cacheRead ?? null, entry.price.cacheWrite ?? null, entry.price.currency ?? "USD"];
3362
+ if (!existing) {
3363
+ db.prepare(`
3364
+ INSERT INTO model_prices (
3365
+ model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3366
+ source_url, origin, status, last_synced_at, created_at, updated_at
3367
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'litellm', ?, ?, 'builtin', 'active', ?, ?, ?)
3368
+ `).run(entry.modelKey, entry.provider, ...next, entry.sourceModelId, entry.sourceUrl, now, now, now);
3369
+ return "added";
3370
+ }
3371
+ 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];
3372
+ const changed = current.some((value, index) => value !== next[index]);
3373
+ if (!changed) {
3374
+ db.prepare(`
3375
+ UPDATE model_prices SET provider = ?, source = 'litellm', source_model_id = ?, source_url = ?, last_synced_at = ?, updated_at = ?
3376
+ WHERE model_key = ? AND origin = 'builtin'
3377
+ `).run(entry.provider, entry.sourceModelId, entry.sourceUrl, now, now, entry.modelKey);
3378
+ return "unchanged";
3379
+ }
3380
+ db.prepare(`
3381
+ UPDATE model_prices
3382
+ SET provider = ?, input = ?, output = ?, cache_read = ?, cache_write = ?, currency = ?, source = 'litellm',
3383
+ source_model_id = ?, source_url = ?, status = 'active', last_synced_at = ?, updated_at = ?
3384
+ WHERE model_key = ? AND origin = 'builtin'
3385
+ `).run(entry.provider, ...next, entry.sourceModelId, entry.sourceUrl, now, now, entry.modelKey);
3386
+ return "updated";
3387
+ }
3388
+ function upsertBuiltinAlias(db, alias, modelKey, provider, source, now) {
3389
+ const existing = db.prepare("SELECT origin, model_key, enabled FROM model_price_aliases WHERE alias = ?").get(alias);
3390
+ if (existing?.origin === "user") return "user_preserved";
3391
+ if (!existing) {
3392
+ db.prepare(`
3393
+ INSERT INTO model_price_aliases (alias, model_key, match_type, provider, priority, source, origin, enabled, created_at, updated_at)
3394
+ VALUES (?, ?, 'exact', ?, 100, ?, 'builtin', 1, ?, ?)
3395
+ `).run(alias, modelKey, provider, source, now, now);
3396
+ return "added";
3397
+ }
3398
+ if (existing.model_key === modelKey && existing.enabled === 1) return "unchanged";
3399
+ db.prepare(`
3400
+ UPDATE model_price_aliases SET model_key = ?, provider = ?, source = ?, enabled = 1, updated_at = ?
3401
+ WHERE alias = ? AND origin = 'builtin'
3402
+ `).run(modelKey, provider, source, now, alias);
3403
+ return "updated";
3404
+ }
3405
+ async function syncPricingFromLitellm(db) {
3406
+ const response = await fetch(LITELLM_PRICING_URL, { headers: { Accept: "application/json" } });
3407
+ if (!response.ok) throw new Error(`LiteLLM pricing fetch failed: HTTP ${response.status}`);
3408
+ const data = await response.json();
3409
+ const now = Date.now();
3410
+ const summary = {
3411
+ source: "litellm",
3412
+ added: 0,
3413
+ updated: 0,
3414
+ unchanged: 0,
3415
+ skipped: 0,
3416
+ aliasesAdded: 0,
3417
+ aliasesUpdated: 0,
3418
+ userPreserved: 0,
3419
+ dryRun: { totalModels: 0, matched: 0, unresolved: [] }
3420
+ };
3421
+ const tx = db.transaction(() => {
3422
+ for (const [sourceModelId, rawEntry] of Object.entries(data)) {
3423
+ const parsed = litellmEntryToPrice(sourceModelId, rawEntry);
3424
+ if (!parsed) {
3425
+ summary.skipped++;
3426
+ continue;
3427
+ }
3428
+ const result = upsertBuiltinPrice(db, {
3429
+ modelKey: parsed.modelKey,
3430
+ provider: parsed.provider,
3431
+ price: parsed.price,
3432
+ sourceModelId,
3433
+ sourceUrl: LITELLM_PRICING_URL
3434
+ }, now);
3435
+ if (result === "user_preserved") summary.userPreserved++;
3436
+ else summary[result]++;
3437
+ for (const alias of aliasesFor(sourceModelId, parsed.modelKey)) {
3438
+ const aliasResult = upsertBuiltinAlias(db, alias, parsed.modelKey, parsed.provider, "litellm", now);
3439
+ if (aliasResult === "added") summary.aliasesAdded++;
3440
+ else if (aliasResult === "updated") summary.aliasesUpdated++;
3441
+ else if (aliasResult === "user_preserved") summary.userPreserved++;
3442
+ }
3443
+ }
3444
+ });
3445
+ tx();
3446
+ summary.dryRun = dryRunLocalModels(db);
3447
+ return summary;
3448
+ }
3449
+ function priceTableFromDb(db) {
3450
+ const table = {};
3451
+ const rows = db.prepare(`
3452
+ SELECT model_key, input, output, cache_read, cache_write, currency
3453
+ FROM model_prices WHERE status = 'active'
3454
+ `).all();
3455
+ for (const row of rows) table[row.model_key] = rowToPrice(row);
3456
+ return table;
3457
+ }
3458
+ function resolvePriceFromRegistry(db, model) {
3459
+ const directUserPrice = db.prepare(`
3460
+ SELECT input, output, cache_read, cache_write, currency
3461
+ FROM model_prices
3462
+ WHERE model_key = ? AND origin = 'user' AND status = 'active'
3463
+ `).get(model);
3464
+ if (directUserPrice) return rowToPrice(directUserPrice);
3465
+ const exactAlias = db.prepare(`
3466
+ SELECT p.input, p.output, p.cache_read, p.cache_write, p.currency
3467
+ FROM model_price_aliases a
3468
+ JOIN model_prices p ON p.model_key = a.model_key
3469
+ WHERE a.alias = ? AND a.enabled = 1 AND p.status = 'active'
3470
+ ORDER BY a.priority DESC LIMIT 1
3471
+ `).get(model);
3472
+ if (exactAlias) return rowToPrice(exactAlias);
3473
+ return resolvePriceFromTable(model, priceTableFromDb(db));
3474
+ }
3475
+ function findMatch(db, model) {
3476
+ const directUserMatch = db.prepare(`
3477
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3478
+ source_url, origin, status, last_synced_at, updated_at
3479
+ FROM model_prices
3480
+ WHERE model_key = ? AND origin = 'user' AND status = 'active'
3481
+ `).get(model);
3482
+ if (directUserMatch) return { price: rowToPrice(directUserMatch), row: directUserMatch, matchedBy: null };
3483
+ const aliasMatch = db.prepare(`
3484
+ SELECT p.model_key, p.provider, p.input, p.output, p.cache_read, p.cache_write, p.currency, p.source,
3485
+ p.source_model_id, p.source_url, p.origin, p.status, p.last_synced_at, p.updated_at, a.alias
3486
+ FROM model_price_aliases a
3487
+ JOIN model_prices p ON p.model_key = a.model_key
3488
+ WHERE a.alias = ? AND a.enabled = 1 AND p.status = 'active'
3489
+ ORDER BY a.priority DESC LIMIT 1
3490
+ `).get(model);
3491
+ if (aliasMatch) {
3492
+ const matchedBy = aliasMatch.alias === model ? aliasMatch.model_key === model ? null : aliasMatch.model_key : aliasMatch.alias;
3493
+ return { price: rowToPrice(aliasMatch), row: aliasMatch, matchedBy };
3494
+ }
3495
+ const rows = db.prepare(`
3496
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3497
+ source_url, origin, status, last_synced_at, updated_at
3498
+ FROM model_prices WHERE status = 'active'
3499
+ `).all();
3500
+ let best = null;
3501
+ for (const row of rows) {
3502
+ if (model === row.model_key) {
3503
+ best = row;
3504
+ break;
3505
+ }
3506
+ if (model.startsWith(row.model_key) && (!best || row.model_key.length > best.model_key.length)) best = row;
3507
+ }
3508
+ if (!best) return null;
3509
+ return { price: rowToPrice(best), row: best, matchedBy: best.model_key === model ? null : best.model_key };
3510
+ }
3511
+ function listPricingModels(db) {
3512
+ 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();
3513
+ const knownModels = new Set(dbModels.map((row) => row.model));
3514
+ const userRows = loadUserRows(db);
3515
+ for (const row of userRows) knownModels.add(row.model_key);
3516
+ const syncedBaselines = new Set(loadSyncedBaselineRows(db).map((row) => row.model_key));
3517
+ return [...knownModels].map((model) => {
3518
+ const match = findMatch(db, model);
3519
+ if (!match) {
3520
+ return {
3521
+ model,
3522
+ provider: null,
3523
+ price: null,
3524
+ currency: "USD",
3525
+ origin: null,
3526
+ source: null,
3527
+ sourceModelId: null,
3528
+ lastSyncedAt: null,
3529
+ isBuiltin: false,
3530
+ isOverride: false,
3531
+ isDefault: false,
3532
+ hasSyncedBaseline: syncedBaselines.has(model),
3533
+ matchedBy: null
3534
+ };
3535
+ }
3536
+ return {
3537
+ model,
3538
+ provider: match.row.provider || null,
3539
+ price: match.price,
3540
+ currency: match.price.currency ?? "USD",
3541
+ origin: match.row.origin,
3542
+ source: match.row.source,
3543
+ sourceModelId: match.row.source_model_id,
3544
+ lastSyncedAt: match.row.last_synced_at,
3545
+ isBuiltin: match.row.origin === "builtin",
3546
+ isOverride: match.row.origin === "user",
3547
+ isDefault: match.row.origin === "builtin",
3548
+ hasSyncedBaseline: syncedBaselines.has(match.row.model_key),
3549
+ matchedBy: match.matchedBy
3550
+ };
3551
+ });
3552
+ }
3553
+ function getPricingRegistrySummary(db) {
3554
+ const priceCounts = db.prepare(`
3555
+ SELECT
3556
+ COUNT(*) AS totalPrices,
3557
+ SUM(CASE WHEN origin = 'builtin' THEN 1 ELSE 0 END) AS builtinPrices,
3558
+ SUM(CASE WHEN origin = 'user' THEN 1 ELSE 0 END) AS userPrices,
3559
+ MAX(last_synced_at) AS lastSyncedAt
3560
+ FROM model_prices
3561
+ WHERE status = 'active'
3562
+ `).get();
3563
+ const aliasCounts = db.prepare(`
3564
+ SELECT COUNT(*) AS activeAliases
3565
+ FROM model_price_aliases
3566
+ WHERE enabled = 1
3567
+ `).get();
3568
+ const dryRun = dryRunLocalModels(db);
3569
+ return {
3570
+ totalPrices: Number(priceCounts.totalPrices ?? 0),
3571
+ builtinPrices: Number(priceCounts.builtinPrices ?? 0),
3572
+ userPrices: Number(priceCounts.userPrices ?? 0),
3573
+ activeAliases: Number(aliasCounts.activeAliases ?? 0),
3574
+ lastSyncedAt: priceCounts.lastSyncedAt == null ? null : Number(priceCounts.lastSyncedAt),
3575
+ localModels: dryRun.totalModels,
3576
+ matchedLocalModels: dryRun.matched,
3577
+ unresolvedLocalModels: dryRun.unresolved
3578
+ };
3579
+ }
3580
+ function listPricingAliasTargets(db) {
3581
+ const rows = db.prepare(`
3582
+ SELECT model_key, provider, origin, source, source_model_id, currency
3583
+ FROM model_prices
3584
+ WHERE status = 'active'
3585
+ ORDER BY origin DESC, provider ASC, model_key ASC
3586
+ `).all();
3587
+ return rows.map((row) => ({
3588
+ model: row.model_key,
3589
+ provider: row.provider,
3590
+ origin: row.origin,
3591
+ source: row.source,
3592
+ sourceModelId: row.source_model_id,
3593
+ currency: row.currency
3594
+ }));
3595
+ }
3596
+ function listLocalModelBindings(db) {
3597
+ const rows = db.prepare("SELECT DISTINCT model FROM records WHERE model != 'unknown' ORDER BY model ASC").all();
3598
+ const priceRows = db.prepare(`
3599
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3600
+ source_url, origin, status, last_synced_at, updated_at
3601
+ FROM model_prices WHERE status = 'active'
3602
+ `).all();
3603
+ return rows.map(({ model }) => {
3604
+ const directUserPrice = db.prepare(`
3605
+ SELECT model_key, provider, input, output, cache_read, cache_write, currency, source, source_model_id,
3606
+ source_url, origin, status, last_synced_at, updated_at
3607
+ FROM model_prices
3608
+ WHERE model_key = ? AND origin = 'user' AND status = 'active'
3609
+ `).get(model);
3610
+ const aliasMatch = db.prepare(`
3611
+ SELECT a.alias, a.model_key AS modelKey, a.origin, a.source, p.origin AS priceOrigin
3612
+ FROM model_price_aliases a
3613
+ JOIN model_prices p ON p.model_key = a.model_key
3614
+ WHERE a.alias = ? AND a.enabled = 1 AND p.status = 'active'
3615
+ ORDER BY a.priority DESC LIMIT 1
3616
+ `).get(model);
3617
+ if (directUserPrice) {
3618
+ const hasManualBinding = Boolean(aliasMatch && aliasMatch.origin === "user" && !(aliasMatch.alias === model && aliasMatch.modelKey === model));
3619
+ return {
3620
+ model,
3621
+ modelKey: hasManualBinding ? aliasMatch.modelKey : directUserPrice.model_key,
3622
+ origin: hasManualBinding ? aliasMatch.origin : directUserPrice.origin,
3623
+ priceOrigin: directUserPrice.origin,
3624
+ source: directUserPrice.source,
3625
+ matchType: "exact",
3626
+ bindingType: "custom",
3627
+ hasManualBinding,
3628
+ hasPrice: true
3629
+ };
3630
+ }
3631
+ if (aliasMatch) {
3632
+ const selfAlias = aliasMatch.alias === model && aliasMatch.modelKey === model;
3633
+ const hasManualBinding = aliasMatch.origin === "user" && !selfAlias;
3634
+ return {
3635
+ model,
3636
+ modelKey: aliasMatch.modelKey,
3637
+ origin: aliasMatch.origin,
3638
+ priceOrigin: aliasMatch.priceOrigin,
3639
+ source: aliasMatch.source,
3640
+ matchType: selfAlias ? "exact" : "alias",
3641
+ bindingType: aliasMatch.priceOrigin === "user" ? "custom" : hasManualBinding ? "manual" : "automatic",
3642
+ hasManualBinding,
3643
+ hasPrice: true
3644
+ };
3645
+ }
3646
+ let best = null;
3647
+ let matchType = null;
3648
+ for (const row of priceRows) {
3649
+ if (model === row.model_key) {
3650
+ best = row;
3651
+ matchType = "exact";
3652
+ break;
3653
+ }
3654
+ if (model.startsWith(row.model_key) && (!best || row.model_key.length > best.model_key.length)) {
3655
+ best = row;
3656
+ matchType = "prefix";
3657
+ }
3658
+ }
3659
+ return {
3660
+ model,
3661
+ modelKey: best?.model_key ?? null,
3662
+ origin: best?.origin ?? null,
3663
+ priceOrigin: best?.origin ?? null,
3664
+ source: best?.source ?? null,
3665
+ matchType,
3666
+ bindingType: best?.origin === "user" ? "custom" : best ? "automatic" : "none",
3667
+ hasManualBinding: false,
3668
+ hasPrice: Boolean(best)
3669
+ };
3670
+ });
3671
+ }
3672
+ function dryRunLocalModels(db) {
3673
+ const models = db.prepare("SELECT DISTINCT model FROM records WHERE model != 'unknown' ORDER BY model ASC").all();
3674
+ const unresolved = [];
3675
+ let matched = 0;
3676
+ for (const row of models) {
3677
+ if (resolvePriceFromRegistry(db, row.model)) matched++;
3678
+ else unresolved.push(row.model);
3679
+ }
3680
+ return { totalModels: models.length, matched, unresolved: unresolved.slice(0, 50) };
3681
+ }
3682
+ function getUserAliasBindings(db) {
3683
+ return db.prepare(`
3684
+ SELECT alias, model_key AS modelKey, origin, source
3685
+ FROM model_price_aliases
3686
+ WHERE origin = 'user' AND enabled = 1
3687
+ ORDER BY alias ASC
3688
+ `).all();
3689
+ }
3690
+
3196
3691
  // src/db/index.ts
3197
3692
  function initializeDatabase(db) {
3198
3693
  applyPragmas(db);
3199
3694
  runMigrations(db);
3695
+ loadPricingRuntime(db, loadConfig());
3200
3696
  }
3201
3697
  function createDatabase(path3) {
3202
- mkdirSync3(dirname2(path3), { recursive: true, mode: 448 });
3698
+ mkdirSync4(dirname3(path3), { recursive: true, mode: 448 });
3203
3699
  const db = new Database(path3);
3204
3700
  initializeDatabase(db);
3205
3701
  return db;
@@ -3912,6 +4408,31 @@ function mapRowToRecord(row) {
3912
4408
  };
3913
4409
  }
3914
4410
 
4411
+ // src/db/write-queue.ts
4412
+ var AsyncTaskQueue = class {
4413
+ tail = Promise.resolve();
4414
+ running = false;
4415
+ pending = 0;
4416
+ run(task) {
4417
+ this.pending++;
4418
+ const runTask = async () => {
4419
+ this.pending--;
4420
+ this.running = true;
4421
+ try {
4422
+ return await task();
4423
+ } finally {
4424
+ this.running = false;
4425
+ }
4426
+ };
4427
+ const result = this.tail.then(runTask, runTask);
4428
+ this.tail = result.catch(() => void 0);
4429
+ return result;
4430
+ }
4431
+ getStatus() {
4432
+ return { running: this.running, pending: this.pending };
4433
+ }
4434
+ };
4435
+
3915
4436
  // src/api/server.ts
3916
4437
  var pendingLeaderboardAuth = /* @__PURE__ */ new Map();
3917
4438
  function getManualImportMetadata(db) {
@@ -3946,11 +4467,39 @@ function countExistingRecordIds(db, ids) {
3946
4467
  }
3947
4468
  return count;
3948
4469
  }
3949
- function recalcCosts(db) {
4470
+ function emptyPricingRecalcStatus() {
4471
+ return {
4472
+ state: "idle",
4473
+ needsRecalc: false,
4474
+ needsRecalcSince: null,
4475
+ total: 0,
4476
+ processed: 0,
4477
+ updated: 0,
4478
+ skipped: 0,
4479
+ queueRunning: false,
4480
+ queuePending: 0,
4481
+ startedAt: null,
4482
+ finishedAt: null,
4483
+ error: null
4484
+ };
4485
+ }
4486
+ async function recalcCosts(db, onProgress) {
3950
4487
  const BATCH_SIZE2 = 1e3;
3951
4488
  let updated = 0;
4489
+ let processed = 0;
4490
+ let skipped = 0;
3952
4491
  let lastId = "";
3953
4492
  const exchangeRate = resolveExchangeRate(loadConfig() ?? {});
4493
+ const priceCache = /* @__PURE__ */ new Map();
4494
+ const resolveCachedPrice = (model) => {
4495
+ if (priceCache.has(model)) return priceCache.get(model) ?? void 0;
4496
+ const price = resolvePriceFromRegistry(db, model) ?? null;
4497
+ priceCache.set(model, price);
4498
+ return price ?? void 0;
4499
+ };
4500
+ const totalRow = db.prepare("SELECT COUNT(*) AS total FROM records").get();
4501
+ const total = totalRow.total;
4502
+ onProgress?.({ total, processed, updated, skipped });
3954
4503
  while (true) {
3955
4504
  const records = db.prepare(
3956
4505
  "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 +4508,23 @@ function recalcCosts(db) {
3959
4508
  const updateStmt = db.prepare("UPDATE records SET model = ?, provider = ?, cost = ?, cost_source = ?, updated_at = ? WHERE id = ?");
3960
4509
  const tx = db.transaction((batch) => {
3961
4510
  for (const r of batch) {
3962
- if (r.cost_source === "log") continue;
4511
+ processed++;
4512
+ if (r.cost_source === "log") {
4513
+ skipped++;
4514
+ continue;
4515
+ }
3963
4516
  const rawModel = r.tool === "qoder" ? normalizeQoderModel(r.model) : r.model;
3964
4517
  const model = rawModel === "unknown" ? r.tool === "qoder" ? "qoder-auto" : r.model : rawModel;
3965
4518
  const provider = model !== r.model ? inferProvider(model) : r.provider;
3966
- const hasPrice = resolvePrice(model) != null;
3967
- const cost = hasPrice ? calculateCost(model, {
4519
+ const price = resolveCachedPrice(model);
4520
+ const cost = price ? calculateCostForPrice(price, {
3968
4521
  inputTokens: r.input_tokens,
3969
4522
  outputTokens: r.output_tokens,
3970
4523
  cacheReadTokens: r.cache_read_tokens,
3971
4524
  cacheWriteTokens: r.cache_write_tokens,
3972
4525
  thinkingTokens: r.thinking_tokens
3973
4526
  }, exchangeRate) : 0;
3974
- const costSource = hasPrice ? "pricing" : "unknown";
4527
+ const costSource = price ? "pricing" : "unknown";
3975
4528
  if (model === r.model && provider === r.provider && cost === r.cost && costSource === r.cost_source) continue;
3976
4529
  updateStmt.run(model, provider, cost, costSource, Date.now(), r.id);
3977
4530
  updated++;
@@ -3979,6 +4532,8 @@ function recalcCosts(db) {
3979
4532
  });
3980
4533
  tx(records);
3981
4534
  lastId = records[records.length - 1].id;
4535
+ onProgress?.({ total, processed, updated, skipped });
4536
+ await new Promise((resolve2) => setImmediate(resolve2));
3982
4537
  }
3983
4538
  return updated;
3984
4539
  }
@@ -4016,6 +4571,17 @@ function json(res, data, status = 200) {
4016
4571
  res.writeHead(status, { "Content-Type": "application/json" });
4017
4572
  res.end(JSON.stringify(data));
4018
4573
  }
4574
+ function isDatabaseLockedError(error) {
4575
+ return error instanceof Error && /database is locked|SQLITE_BUSY/i.test(error.message);
4576
+ }
4577
+ function databaseBusy(res) {
4578
+ json(res, {
4579
+ error: {
4580
+ code: "DATABASE_BUSY",
4581
+ message: "Database is busy. Wait for the current refresh, sync, or import to finish and retry."
4582
+ }
4583
+ }, 503);
4584
+ }
4019
4585
  async function readJsonBody(req) {
4020
4586
  let body = "";
4021
4587
  for await (const chunk of req) body += chunk;
@@ -4101,6 +4667,23 @@ async function proxyLeaderboard(res, url) {
4101
4667
  json(res, getLeaderboardFallback(periodType));
4102
4668
  }
4103
4669
  }
4670
+ async function proxyCloudSyncStatus(res) {
4671
+ const upstream = new URL("/api/cli/sync/status", getSiteUrl());
4672
+ try {
4673
+ const response = await fetch(upstream, {
4674
+ headers: { Accept: "application/json" }
4675
+ });
4676
+ const contentType = response.headers.get("content-type") || "";
4677
+ if (!response.ok || !contentType.includes("application/json")) {
4678
+ json(res, { enabled: false });
4679
+ return;
4680
+ }
4681
+ const data = await response.json().catch(() => null);
4682
+ json(res, data && typeof data.enabled === "boolean" ? data : { enabled: false });
4683
+ } catch {
4684
+ json(res, { enabled: false });
4685
+ }
4686
+ }
4104
4687
  function getToolFilter(tool, prefix = "") {
4105
4688
  if (!tool) return { where: "", params: {} };
4106
4689
  const col = prefix ? `${prefix}.tool` : "tool";
@@ -4154,6 +4737,90 @@ function createApiServer(db, options) {
4154
4737
  const cfg = loadConfig();
4155
4738
  let weekStart = cfg?.weekStart ?? 1;
4156
4739
  const dashboardPassword = getDashboardPassword();
4740
+ const localWriteQueue = new AsyncTaskQueue();
4741
+ const runDbWrite = options?.runDbWrite ?? ((task) => localWriteQueue.run(task));
4742
+ const getDbWriteQueueStatus = options?.getDbWriteQueueStatus ?? (() => localWriteQueue.getStatus());
4743
+ let pricingRecalcStatus = emptyPricingRecalcStatus();
4744
+ let pricingNeedsRecalcSince = null;
4745
+ let pricingRecalcGeneration = 0;
4746
+ const currentPricingRecalcStatus = () => {
4747
+ const status = {
4748
+ ...pricingRecalcStatus,
4749
+ needsRecalc: pricingNeedsRecalcSince != null,
4750
+ needsRecalcSince: pricingNeedsRecalcSince
4751
+ };
4752
+ if (status.state !== "queued") return status;
4753
+ const queueStatus = getDbWriteQueueStatus();
4754
+ return {
4755
+ ...status,
4756
+ queueRunning: queueStatus.running,
4757
+ queuePending: queueStatus.pending
4758
+ };
4759
+ };
4760
+ const markPricingNeedsRecalc = () => {
4761
+ pricingRecalcGeneration += 1;
4762
+ pricingNeedsRecalcSince = Date.now();
4763
+ return pricingNeedsRecalcSince;
4764
+ };
4765
+ const startPricingRecalc = () => {
4766
+ if (pricingRecalcStatus.state === "queued" || pricingRecalcStatus.state === "running") {
4767
+ return { accepted: false, status: currentPricingRecalcStatus() };
4768
+ }
4769
+ const startedAt = Date.now();
4770
+ const startedGeneration = pricingRecalcGeneration;
4771
+ pricingRecalcStatus = {
4772
+ ...emptyPricingRecalcStatus(),
4773
+ state: "running",
4774
+ startedAt
4775
+ };
4776
+ const runRecalc = async () => {
4777
+ pricingRecalcStatus = {
4778
+ ...pricingRecalcStatus,
4779
+ state: "running",
4780
+ queueRunning: false,
4781
+ queuePending: 0,
4782
+ startedAt: pricingRecalcStatus.startedAt ?? Date.now()
4783
+ };
4784
+ try {
4785
+ await recalcCosts(db, (progress) => {
4786
+ pricingRecalcStatus = {
4787
+ ...pricingRecalcStatus,
4788
+ ...progress,
4789
+ state: "running"
4790
+ };
4791
+ });
4792
+ if (pricingNeedsRecalcSince != null && pricingRecalcGeneration === startedGeneration) {
4793
+ pricingNeedsRecalcSince = null;
4794
+ }
4795
+ pricingRecalcStatus = {
4796
+ ...pricingRecalcStatus,
4797
+ state: "done",
4798
+ needsRecalc: pricingNeedsRecalcSince != null,
4799
+ needsRecalcSince: pricingNeedsRecalcSince,
4800
+ finishedAt: Date.now(),
4801
+ error: null
4802
+ };
4803
+ } catch (error) {
4804
+ pricingRecalcStatus = {
4805
+ ...pricingRecalcStatus,
4806
+ state: "error",
4807
+ finishedAt: Date.now(),
4808
+ error: error instanceof Error ? error.message : String(error)
4809
+ };
4810
+ }
4811
+ };
4812
+ setImmediate(() => {
4813
+ void runRecalc().catch((error) => {
4814
+ pricingRecalcStatus = {
4815
+ ...pricingRecalcStatus,
4816
+ state: "error",
4817
+ finishedAt: Date.now(),
4818
+ error: error instanceof Error ? error.message : String(error)
4819
+ };
4820
+ });
4821
+ });
4822
+ return { accepted: true, status: currentPricingRecalcStatus() };
4823
+ };
4157
4824
  const server = http.createServer(async (req, res) => {
4158
4825
  const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
4159
4826
  res.setHeader("Access-Control-Allow-Origin", "*");
@@ -4192,6 +4859,10 @@ function createApiServer(db, options) {
4192
4859
  json(res, { ok: true });
4193
4860
  return;
4194
4861
  }
4862
+ if (url.pathname === "/api/cli/sync/status" && req.method === "GET") {
4863
+ await proxyCloudSyncStatus(res);
4864
+ return;
4865
+ }
4195
4866
  if (dashboardPassword && shouldProtectApiPath(url.pathname) && !isAuthenticated(dashboardPassword, req.headers.cookie)) {
4196
4867
  json(res, { error: { code: "UNAUTHORIZED", message: "Authentication required" } }, 401);
4197
4868
  return;
@@ -4301,22 +4972,28 @@ function createApiServer(db, options) {
4301
4972
  byTool[row.tool] = { tokens: row.tokens, cost: row.cost };
4302
4973
  }
4303
4974
  const drJoin = getDateRangeFilter(range, from, to, "r", weekStart);
4304
- const dfJoin = df.where ? df.where.replace(/device_instance_id/g, "r.device_instance_id") : "";
4975
+ const drJoinO = drJoin.where.replaceAll("r.ts", "COALESCE(r.ts, tc.ts)");
4976
+ const dfJoinRaw = df.where ? df.where.replace(/device_instance_id/g, "r.device_instance_id") : "";
4977
+ const dfJoin = dfJoinRaw.replace(
4978
+ /r\.device_instance_id\s*=\s*@\w+/,
4979
+ "(r.device_instance_id IS NULL OR r.device_instance_id = @deviceId)"
4980
+ );
4305
4981
  const tfJoin = getToolFilter(tool, "r");
4982
+ const tfJoinO = tfJoin.where.replaceAll("r.tool", "COALESCE(r.tool, tc.tool)");
4306
4983
  const topToolCalls = db.prepare(`
4307
4984
  SELECT tc.name, COUNT(*) AS count
4308
4985
  FROM tool_calls tc
4309
- JOIN records r ON r.id = tc.record_id
4310
- WHERE 1=1 ${dfJoin} ${drJoin.where} ${tfJoin.where}
4986
+ LEFT JOIN records r ON r.id = tc.record_id
4987
+ WHERE 1=1 ${dfJoin} ${drJoinO} ${tfJoinO}
4311
4988
  GROUP BY tc.name ORDER BY count DESC LIMIT 10
4312
4989
  `).all({ ...drJoin.params, ...df.params, ...tfJoin.params });
4313
4990
  const topMcpServersRaw = db.prepare(`
4314
4991
  SELECT tc.name, COUNT(*) AS count
4315
4992
  FROM tool_calls tc
4316
- JOIN records r ON r.id = tc.record_id
4993
+ LEFT JOIN records r ON r.id = tc.record_id
4317
4994
  WHERE tc.name LIKE 'mcp\\_\\_%' ESCAPE '\\'
4318
4995
  AND INSTR(SUBSTR(tc.name, 6), '__') > 0
4319
- ${dfJoin} ${drJoin.where} ${tfJoin.where}
4996
+ ${dfJoin} ${drJoinO} ${tfJoinO}
4320
4997
  GROUP BY tc.name ORDER BY count DESC
4321
4998
  `).all({ ...drJoin.params, ...df.params, ...tfJoin.params });
4322
4999
  const mcpServerMap = /* @__PURE__ */ new Map();
@@ -4570,9 +5247,17 @@ function createApiServer(db, options) {
4570
5247
  json(res, { toolCalls: [] });
4571
5248
  return;
4572
5249
  }
4573
- const dr = getDateRangeFilter(range, from, to, "r", weekStart);
5250
+ const drRaw = getDateRangeFilter(range, from, to, "r", weekStart);
5251
+ const dr = {
5252
+ where: drRaw.where.replaceAll("r.ts", "COALESCE(r.ts, tc.ts)"),
5253
+ params: drRaw.params
5254
+ };
4574
5255
  const tool = url.searchParams.get("tool");
4575
- const tf = getToolFilter(tool, "r");
5256
+ const tfRaw = getToolFilter(tool, "r");
5257
+ const tf = {
5258
+ where: tfRaw.where.replaceAll("r.tool", "COALESCE(r.tool, tc.tool)"),
5259
+ params: tfRaw.params
5260
+ };
4576
5261
  const toolType = url.searchParams.get("toolType");
4577
5262
  if (toolType && !["mcp", "skill", "builtin"].includes(toolType)) {
4578
5263
  json(res, { error: { code: "INVALID_PARAM", message: "Invalid toolType" } }, 400);
@@ -4581,14 +5266,14 @@ function createApiServer(db, options) {
4581
5266
  const ttf = getToolTypeFilter(toolType);
4582
5267
  const totalRow = db.prepare(`
4583
5268
  SELECT COUNT(*) AS total FROM tool_calls tc
4584
- JOIN records r ON r.id = tc.record_id
5269
+ LEFT JOIN records r ON r.id = tc.record_id
4585
5270
  WHERE 1=1 ${dr.where} ${tf.where} ${ttf}
4586
5271
  `).get({ ...dr.params, ...tf.params });
4587
5272
  const total = totalRow.total || 1;
4588
5273
  const rows = db.prepare(`
4589
5274
  SELECT tc.name, COUNT(*) AS count
4590
5275
  FROM tool_calls tc
4591
- JOIN records r ON r.id = tc.record_id
5276
+ LEFT JOIN records r ON r.id = tc.record_id
4592
5277
  WHERE 1=1 ${dr.where} ${tf.where} ${ttf}
4593
5278
  GROUP BY tc.name ORDER BY count DESC
4594
5279
  `).all({ ...dr.params, ...tf.params });
@@ -4807,72 +5492,48 @@ function createApiServer(db, options) {
4807
5492
  }
4808
5493
  if (url.pathname === "/api/pricing") {
4809
5494
  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 });
5495
+ json(res, { models: listPricingModels(db), registry: getPricingRegistrySummary(db), targets: listPricingAliasTargets(db), aliasBindings: getUserAliasBindings(db), localBindings: listLocalModelBindings(db) });
4847
5496
  return;
4848
5497
  }
4849
5498
  if (req.method === "PUT") {
4850
5499
  let body = "";
4851
5500
  for await (const chunk of req) body += chunk;
5501
+ let data;
4852
5502
  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 });
5503
+ data = JSON.parse(body);
4873
5504
  } catch {
4874
5505
  json(res, { error: { code: "INVALID_JSON", message: "Invalid JSON body" } }, 400);
5506
+ return;
5507
+ }
5508
+ if (!data.model || typeof data.input !== "number" || typeof data.output !== "number") {
5509
+ json(res, { error: { code: "INVALID_PARAM", message: "model, input, output required" } }, 400);
5510
+ return;
5511
+ }
5512
+ const entry = {
5513
+ input: data.input,
5514
+ output: data.output,
5515
+ cacheRead: data.cacheRead,
5516
+ cacheWrite: data.cacheWrite
5517
+ };
5518
+ if (data.currency === "CNY") {
5519
+ entry.currency = "CNY";
4875
5520
  }
5521
+ try {
5522
+ await runDbWrite(() => {
5523
+ setUserPrice(db, data.model, entry);
5524
+ const cfg2 = loadConfig() ?? {};
5525
+ cfg2.priceOverrides = { ...cfg2.priceOverrides, [data.model]: entry };
5526
+ saveConfig(cfg2);
5527
+ loadPricingRuntime(db, cfg2);
5528
+ });
5529
+ } catch (error) {
5530
+ if (isDatabaseLockedError(error)) {
5531
+ databaseBusy(res);
5532
+ return;
5533
+ }
5534
+ throw error;
5535
+ }
5536
+ json(res, { ok: true, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
4876
5537
  return;
4877
5538
  }
4878
5539
  if (req.method === "DELETE") {
@@ -4881,20 +5542,94 @@ function createApiServer(db, options) {
4881
5542
  json(res, { error: { code: "INVALID_PARAM", message: "model param required" } }, 400);
4882
5543
  return;
4883
5544
  }
4884
- removePriceOverride(model);
4885
- const cfg2 = loadConfig() ?? {};
4886
- if (cfg2.priceOverrides) {
4887
- delete cfg2.priceOverrides[model];
4888
- saveConfig(cfg2);
5545
+ try {
5546
+ await runDbWrite(() => {
5547
+ resetUserPriceToSynced(db, model);
5548
+ removePriceOverride(model);
5549
+ const cfg2 = loadConfig() ?? {};
5550
+ if (cfg2.priceOverrides) {
5551
+ delete cfg2.priceOverrides[model];
5552
+ saveConfig(cfg2);
5553
+ }
5554
+ loadPricingRuntime(db, cfg2);
5555
+ });
5556
+ json(res, { ok: true, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
5557
+ } catch (error) {
5558
+ if (isDatabaseLockedError(error)) {
5559
+ databaseBusy(res);
5560
+ return;
5561
+ }
5562
+ throw error;
4889
5563
  }
4890
- const recalculated = recalcCosts(db);
4891
- json(res, { ok: true, recalculated });
4892
5564
  return;
4893
5565
  }
4894
5566
  }
5567
+ if (url.pathname === "/api/pricing/alias" && req.method === "POST") {
5568
+ try {
5569
+ const data = await readJsonBody(req);
5570
+ if (typeof data.alias !== "string" || typeof data.modelKey !== "string") {
5571
+ json(res, { error: { code: "INVALID_PARAM", message: "alias and modelKey required" } }, 400);
5572
+ return;
5573
+ }
5574
+ await runDbWrite(() => {
5575
+ setUserPricingAlias(db, data.alias, data.modelKey);
5576
+ loadPricingRuntime(db, loadConfig());
5577
+ });
5578
+ json(res, { ok: true, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
5579
+ } catch (error) {
5580
+ if (isDatabaseLockedError(error)) {
5581
+ databaseBusy(res);
5582
+ return;
5583
+ }
5584
+ json(res, { error: { code: "INVALID_PARAM", message: error instanceof Error ? error.message : "Invalid pricing alias" } }, 400);
5585
+ }
5586
+ return;
5587
+ }
5588
+ if (url.pathname === "/api/pricing/alias" && req.method === "DELETE") {
5589
+ const alias = url.searchParams.get("alias");
5590
+ if (!alias) {
5591
+ json(res, { error: { code: "INVALID_PARAM", message: "alias param required" } }, 400);
5592
+ return;
5593
+ }
5594
+ try {
5595
+ await runDbWrite(() => {
5596
+ removeUserPricingAlias(db, alias);
5597
+ loadPricingRuntime(db, loadConfig());
5598
+ });
5599
+ json(res, { ok: true, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
5600
+ } catch (error) {
5601
+ if (isDatabaseLockedError(error)) {
5602
+ databaseBusy(res);
5603
+ return;
5604
+ }
5605
+ json(res, { error: { code: "INVALID_PARAM", message: error instanceof Error ? error.message : "Invalid pricing alias" } }, 400);
5606
+ }
5607
+ return;
5608
+ }
5609
+ if (url.pathname === "/api/pricing/sync" && req.method === "POST") {
5610
+ try {
5611
+ const summary = await runDbWrite(async () => {
5612
+ const summary2 = await syncPricingFromLitellm(db);
5613
+ loadPricingRuntime(db, loadConfig());
5614
+ return summary2;
5615
+ });
5616
+ json(res, { ok: true, summary, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
5617
+ } catch (error) {
5618
+ if (isDatabaseLockedError(error)) {
5619
+ databaseBusy(res);
5620
+ return;
5621
+ }
5622
+ json(res, { error: { code: "SYNC_FAILED", message: error instanceof Error ? error.message : "Pricing sync failed" } }, 502);
5623
+ }
5624
+ return;
5625
+ }
5626
+ if (url.pathname === "/api/pricing/recalc" && req.method === "GET") {
5627
+ json(res, currentPricingRecalcStatus());
5628
+ return;
5629
+ }
4895
5630
  if (url.pathname === "/api/pricing/recalc" && req.method === "POST") {
4896
- const updated = recalcCosts(db);
4897
- json(res, { updated });
5631
+ const result = startPricingRecalc();
5632
+ json(res, result, result.accepted ? 202 : 200);
4898
5633
  return;
4899
5634
  }
4900
5635
  if (url.pathname === "/api/sync") {
@@ -4916,8 +5651,16 @@ function createApiServer(db, options) {
4916
5651
  json(res, { error: { code: "NOT_AVAILABLE", message: "Refresh not available" } }, 501);
4917
5652
  return;
4918
5653
  }
4919
- const result = await options.onRefresh();
4920
- json(res, result);
5654
+ try {
5655
+ const result = await runDbWrite(() => options.onRefresh());
5656
+ json(res, result);
5657
+ } catch (error) {
5658
+ if (isDatabaseLockedError(error)) {
5659
+ databaseBusy(res);
5660
+ return;
5661
+ }
5662
+ throw error;
5663
+ }
4921
5664
  return;
4922
5665
  }
4923
5666
  if (url.pathname === "/api/quotas" && req.method === "GET") {
@@ -5324,8 +6067,11 @@ function createApiServer(db, options) {
5324
6067
  json(res, { error: { code: "IMPORT_FAILED", message: result.errors[0] }, errors: result.errors }, 400);
5325
6068
  return;
5326
6069
  }
5327
- const existingCount = countExistingRecordIds(db, result.records.map((record) => record.id));
5328
- for (const record of result.records) insertRecord(db, record);
6070
+ const existingCount = await runDbWrite(() => {
6071
+ const existingCount2 = countExistingRecordIds(db, result.records.map((record) => record.id));
6072
+ for (const record of result.records) insertRecord(db, record);
6073
+ return existingCount2;
6074
+ });
5329
6075
  const imported = result.records.length;
5330
6076
  const added = Math.max(0, new Set(result.records.map((record) => record.id)).size - existingCount);
5331
6077
  json(res, {
@@ -5392,7 +6138,8 @@ function defaultFileData() {
5392
6138
  "omp": {},
5393
6139
  "pi": {},
5394
6140
  "craft": {},
5395
- "droid": {}
6141
+ "droid": {},
6142
+ "zcode": {}
5396
6143
  };
5397
6144
  }
5398
6145
  var WatermarkManager = class {
@@ -5412,7 +6159,7 @@ var WatermarkManager = class {
5412
6159
  if (parsed && typeof parsed === "object" && !("files" in parsed)) {
5413
6160
  return { files: { ...defaultFileData(), ...parsed } };
5414
6161
  }
5415
- return { files: { ...defaultFileData(), ...parsed.files ?? {} }, opencode: parsed.opencode ?? null, hermes: parsed.hermes ?? null, qoder: parsed.qoder ?? null, cursor: parsed.cursor ?? null, goose: parsed.goose ?? null, zed: parsed.zed ?? null, kiro: parsed.kiro ?? null };
6162
+ return { files: { ...defaultFileData(), ...parsed.files ?? {} }, opencode: parsed.opencode ?? null, hermes: parsed.hermes ?? null, qoder: parsed.qoder ?? null, cursor: parsed.cursor ?? null, goose: parsed.goose ?? null, zed: parsed.zed ?? null, kiro: parsed.kiro ?? null, zcode: parsed.zcode ?? null, zcodeTools: parsed.zcodeTools ?? null };
5416
6163
  } catch {
5417
6164
  return { files: defaultFileData() };
5418
6165
  }
@@ -5481,6 +6228,18 @@ var WatermarkManager = class {
5481
6228
  setKiroCursor(cursor) {
5482
6229
  this.data.kiro = cursor;
5483
6230
  }
6231
+ getZcodeCursor() {
6232
+ return this.data.zcode ?? null;
6233
+ }
6234
+ setZcodeCursor(cursor) {
6235
+ this.data.zcode = cursor;
6236
+ }
6237
+ getZcodeToolCursor() {
6238
+ return this.data.zcodeTools ?? null;
6239
+ }
6240
+ setZcodeToolCursor(cursor) {
6241
+ this.data.zcodeTools = cursor;
6242
+ }
5484
6243
  };
5485
6244
 
5486
6245
  // src/commands/parse-opencode.ts
@@ -6255,6 +7014,109 @@ function runParseKiro(db, options) {
6255
7014
  return { records: [], nextCursor: null, errors: ["no supported Kiro tables found"] };
6256
7015
  }
6257
7016
 
7017
+ // src/commands/parse-zcode.ts
7018
+ function normalizeZcodeModel(value) {
7019
+ if (typeof value !== "string") return "unknown";
7020
+ const trimmed = value.trim();
7021
+ if (!trimmed) return "unknown";
7022
+ return trimmed.toLowerCase();
7023
+ }
7024
+ function runParseZcode(db, options) {
7025
+ const { dbPath, device, deviceInstanceId, platform: platform5, now, cursor, toolCursor, exchangeRate } = options;
7026
+ const records = [];
7027
+ const toolCalls = [];
7028
+ const errors = [];
7029
+ let nextCursor = null;
7030
+ let nextToolCursor = null;
7031
+ const modelRows = db.prepare(`
7032
+ SELECT m.id, m.session_id, m.model_id, m.started_at,
7033
+ m.input_tokens, m.output_tokens, m.reasoning_tokens,
7034
+ m.cache_creation_input_tokens, m.cache_read_input_tokens,
7035
+ s.directory
7036
+ FROM model_usage m
7037
+ LEFT JOIN session s ON s.id = m.session_id
7038
+ WHERE m.status = 'completed'
7039
+ AND (m.started_at > ? OR (m.started_at = ? AND m.id > ?))
7040
+ ORDER BY m.started_at, m.id
7041
+ `).all(
7042
+ cursor?.lastStartedAt ?? 0,
7043
+ cursor?.lastStartedAt ?? 0,
7044
+ cursor?.lastId ?? ""
7045
+ );
7046
+ for (const row of modelRows) {
7047
+ nextCursor = { lastStartedAt: Number(row.started_at), lastId: String(row.id) };
7048
+ const inputTokens = Number(row.input_tokens) || 0;
7049
+ const outputTokens = Number(row.output_tokens) || 0;
7050
+ const thinkingTokens = Number(row.reasoning_tokens) || 0;
7051
+ const cacheReadTokens = Number(row.cache_read_input_tokens) || 0;
7052
+ const cacheWriteTokens = Number(row.cache_creation_input_tokens) || 0;
7053
+ if (inputTokens + outputTokens + thinkingTokens + cacheReadTokens + cacheWriteTokens === 0) continue;
7054
+ const model = normalizeZcodeModel(row.model_id);
7055
+ const provider = inferProvider(model);
7056
+ const ts3 = Number(row.started_at) || now;
7057
+ const recordId = generateRecordId(deviceInstanceId, `${dbPath}:${row.id}`, ts3);
7058
+ const tokenArgs = { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, thinkingTokens };
7059
+ const calculatedCost = model !== "unknown" ? calculateCost(model, tokenArgs, exchangeRate) : 0;
7060
+ records.push({
7061
+ id: recordId,
7062
+ ts: ts3,
7063
+ ingestedAt: now,
7064
+ updatedAt: now,
7065
+ lineOffset: 0,
7066
+ tool: "zcode",
7067
+ model,
7068
+ provider,
7069
+ inputTokens,
7070
+ outputTokens,
7071
+ cacheReadTokens,
7072
+ cacheWriteTokens,
7073
+ thinkingTokens,
7074
+ cost: calculatedCost,
7075
+ costSource: calculatedCost > 0 ? "pricing" : "unknown",
7076
+ sessionId: String(row.session_id ?? "unknown"),
7077
+ sourceFile: dbPath,
7078
+ device,
7079
+ deviceInstanceId,
7080
+ platform: platform5,
7081
+ cwd: typeof row.directory === "string" && row.directory ? row.directory : ""
7082
+ });
7083
+ }
7084
+ const toolRows = db.prepare(`
7085
+ SELECT id, tool_name, started_at
7086
+ FROM tool_usage
7087
+ WHERE status = 'completed'
7088
+ AND (started_at > ? OR (started_at = ? AND id > ?))
7089
+ ORDER BY started_at, id
7090
+ `).all(
7091
+ toolCursor?.lastStartedAt ?? 0,
7092
+ toolCursor?.lastStartedAt ?? 0,
7093
+ toolCursor?.lastId ?? ""
7094
+ );
7095
+ let callIndex = 0;
7096
+ for (const row of toolRows) {
7097
+ nextToolCursor = { lastStartedAt: Number(row.started_at), lastId: String(row.id) };
7098
+ const name = typeof row.tool_name === "string" && row.tool_name.trim() ? row.tool_name.trim() : "unknown";
7099
+ const ts3 = Number(row.started_at) || now;
7100
+ toolCalls.push({
7101
+ id: generateOrphanToolCallId("zcode", name, ts3, callIndex),
7102
+ recordId: null,
7103
+ name,
7104
+ ts: ts3,
7105
+ callIndex,
7106
+ // insertToolCall reads (tc as any).tool for orphan calls
7107
+ tool: "zcode"
7108
+ });
7109
+ callIndex++;
7110
+ }
7111
+ return {
7112
+ records,
7113
+ toolCalls,
7114
+ nextCursor: records.length > 0 ? nextCursor : null,
7115
+ nextToolCursor: toolCalls.length > 0 ? nextToolCursor : null,
7116
+ errors
7117
+ };
7118
+ }
7119
+
6258
7120
  // src/commands/parse.ts
6259
7121
  function extractCwdFromJson(data) {
6260
7122
  if (typeof data.cwd === "string" && data.cwd) return data.cwd;
@@ -6291,6 +7153,11 @@ function extractSessionId2(filePath, tool) {
6291
7153
  const parts = filePath.split("/");
6292
7154
  return (parts.pop() ?? "").replace(".jsonl", "");
6293
7155
  }
7156
+ if (tool === "pi") {
7157
+ const filename = filePath.split("/").pop() ?? "";
7158
+ const match = filename.match(/_([^_]+)\.jsonl$/);
7159
+ return match ? match[1] : filename.replace(".jsonl", "");
7160
+ }
6294
7161
  return "unknown";
6295
7162
  }
6296
7163
  function toNumber2(value) {
@@ -6856,6 +7723,45 @@ async function runParse(db, filterTool, options) {
6856
7723
  errors.push(`${zedDbPath}: ${e instanceof Error ? e.message : e}`);
6857
7724
  }
6858
7725
  }
7726
+ if (!filterTool || filterTool === "zcode") {
7727
+ const zcodeDbPath = getDbPath("zcode") ?? "";
7728
+ if (existsSync8(zcodeDbPath)) {
7729
+ try {
7730
+ const zcodeDb = new Database2(zcodeDbPath, { readonly: true });
7731
+ try {
7732
+ const result = runParseZcode(zcodeDb, {
7733
+ dbPath: zcodeDbPath,
7734
+ device,
7735
+ deviceInstanceId,
7736
+ platform: devicePlatform,
7737
+ now: Date.now(),
7738
+ cursor: wm.getZcodeCursor(),
7739
+ toolCursor: wm.getZcodeToolCursor(),
7740
+ exchangeRate
7741
+ });
7742
+ for (const record of result.records) insertRecord(db, record);
7743
+ for (const tc of result.toolCalls) insertToolCall(db, tc);
7744
+ if (result.nextCursor) {
7745
+ wm.setZcodeCursor(result.nextCursor);
7746
+ }
7747
+ if (result.nextToolCursor) {
7748
+ wm.setZcodeToolCursor(result.nextToolCursor);
7749
+ }
7750
+ if (result.nextCursor || result.nextToolCursor) {
7751
+ wm.save();
7752
+ }
7753
+ parsedCount += result.records.length;
7754
+ toolCallCount += result.toolCalls.length;
7755
+ errors.push(...result.errors);
7756
+ onProgress({ phase: "Parsing SQLite", tool: "zcode", current: 1, total: 1, records: parsedCount, toolCalls: toolCallCount });
7757
+ } finally {
7758
+ zcodeDb.close();
7759
+ }
7760
+ } catch (e) {
7761
+ errors.push(`${zcodeDbPath}: ${e instanceof Error ? e.message : e}`);
7762
+ }
7763
+ }
7764
+ }
6859
7765
  if (deviceInstanceId !== "unknown") {
6860
7766
  db.prepare(
6861
7767
  `UPDATE records SET device_instance_id = ?, device = ? WHERE device_instance_id = 'unknown'`
@@ -7097,7 +8003,7 @@ import { join as join9 } from "path";
7097
8003
 
7098
8004
  // src/db/synced-records.ts
7099
8005
  function insertSyncedRecord(db, record) {
7100
- db.prepare(`
8006
+ const result = db.prepare(`
7101
8007
  INSERT INTO synced_records (
7102
8008
  id, ts, tool, model, provider, input_tokens, output_tokens,
7103
8009
  cache_read_tokens, cache_write_tokens, thinking_tokens,
@@ -7150,6 +8056,7 @@ function insertSyncedRecord(db, record) {
7150
8056
  sourceFile: record.sourceFile ?? "",
7151
8057
  cwd: record.cwd ?? ""
7152
8058
  });
8059
+ return result.changes > 0;
7153
8060
  }
7154
8061
  function mergeSyncedRecordsIntoRecords(db) {
7155
8062
  const now = Date.now();
@@ -7214,7 +8121,8 @@ var RECORD_ID_SYNC_TOOLS = /* @__PURE__ */ new Set([
7214
8121
  "goose",
7215
8122
  "zed",
7216
8123
  "kiro",
7217
- "roocode"
8124
+ "roocode",
8125
+ "zcode"
7218
8126
  ]);
7219
8127
  function mapStatsRecordToSyncRecord(record) {
7220
8128
  const id = record.sourceFile.startsWith("synced/") ? record.id : RECORD_ID_SYNC_TOOLS.has(record.tool) ? record.id : generateSyncRecordId(record.deviceInstanceId, record.sourceFile, record.lineOffset);
@@ -7338,8 +8246,8 @@ var SyncOrchestrator = class {
7338
8246
  if (typeof record.updatedAt === "string") {
7339
8247
  record.updatedAt = new Date(record.updatedAt).getTime();
7340
8248
  }
7341
- insertSyncedRecord(this.db, record);
7342
- totalPulled++;
8249
+ const changed = insertSyncedRecord(this.db, record);
8250
+ if (changed) totalPulled++;
7343
8251
  } catch {
7344
8252
  }
7345
8253
  }
@@ -8245,6 +9153,7 @@ var RuntimeSettingsController = class {
8245
9153
  leaderboardUploadTimer = null;
8246
9154
  syncTimer = null;
8247
9155
  parseInFlight = false;
9156
+ cleanupInFlight = false;
8248
9157
  leaderboardUploadInFlight = false;
8249
9158
  started = false;
8250
9159
  constructor(options) {
@@ -8297,11 +9206,7 @@ var RuntimeSettingsController = class {
8297
9206
  }
8298
9207
  if (retentionDays > 0) {
8299
9208
  this.cleanupTimer = setInterval(() => {
8300
- try {
8301
- this.runCleanupFn(this.db, retentionDays);
8302
- } catch (err) {
8303
- console.error("[settings-controller] cleanup failed:", err);
8304
- }
9209
+ void this.runCleanupSafely(retentionDays);
8305
9210
  }, this.cleanupIntervalMs);
8306
9211
  }
8307
9212
  if (config?.leaderboardAutoUpload === true && leaderboardUploadInterval > 0 && this.runLeaderboardUploadFn) {
@@ -8334,6 +9239,17 @@ var RuntimeSettingsController = class {
8334
9239
  this.parseInFlight = false;
8335
9240
  }
8336
9241
  }
9242
+ async runCleanupSafely(retentionDays) {
9243
+ if (this.cleanupInFlight) return;
9244
+ this.cleanupInFlight = true;
9245
+ try {
9246
+ await this.runCleanupFn(this.db, retentionDays);
9247
+ } catch (err) {
9248
+ console.error("[settings-controller] cleanup failed:", err);
9249
+ } finally {
9250
+ this.cleanupInFlight = false;
9251
+ }
9252
+ }
8337
9253
  async runLeaderboardUploadSafely() {
8338
9254
  if (this.leaderboardUploadInFlight) return;
8339
9255
  this.leaderboardUploadInFlight = true;
@@ -8365,11 +9281,8 @@ var MIME_TYPES = {
8365
9281
  };
8366
9282
  function serve(options) {
8367
9283
  const config = loadConfig();
8368
- if (config?.priceOverrides) {
8369
- for (const [model, entry] of Object.entries(config.priceOverrides)) {
8370
- setPriceOverride(model, entry);
8371
- }
8372
- }
9284
+ const dbWriteQueue = new AsyncTaskQueue();
9285
+ const runDbWrite = (task) => dbWriteQueue.run(task);
8373
9286
  if (config == null || config.exchangeRate == null) {
8374
9287
  const cacheAge = config?.exchangeRateCache ? Date.now() - config.exchangeRateCache.fetchedAt : Infinity;
8375
9288
  if (cacheAge >= CACHE_TTL_MS) {
@@ -8385,8 +9298,10 @@ function serve(options) {
8385
9298
  }
8386
9299
  const syncRuntime = new SyncRuntimeController({
8387
9300
  runSync: async (runtimeOptions) => {
8388
- await runParse(options.db);
8389
- await runSync(options.db, runtimeOptions);
9301
+ await runDbWrite(async () => {
9302
+ await runParse(options.db);
9303
+ await runSync(options.db, runtimeOptions);
9304
+ });
8390
9305
  },
8391
9306
  getPersistedState: () => getState(AIUSAGE_DIR),
8392
9307
  getCurrentTarget: () => getSyncTarget(loadConfig()?.sync)
@@ -8394,14 +9309,14 @@ function serve(options) {
8394
9309
  const runtimeSettings = new RuntimeSettingsController({
8395
9310
  db: options.db,
8396
9311
  loadConfig,
8397
- runParse,
8398
- runCleanup: cleanOldData,
9312
+ runParse: (db) => runDbWrite(() => runParse(db)),
9313
+ runCleanup: (db, retentionDays) => runDbWrite(() => cleanOldData(db, retentionDays)),
8399
9314
  runLeaderboardUpload: (db) => uploadLeaderboardData(db, getState(AIUSAGE_DIR)?.deviceInstanceId).then(() => void 0),
8400
9315
  runSync: () => syncRuntime.start(),
8401
9316
  onSyncScheduleChanged: (ts3) => syncRuntime.setNextSyncAt(ts3)
8402
9317
  });
8403
9318
  runtimeSettings.start();
8404
- runParse(options.db).catch((err) => {
9319
+ runDbWrite(() => runParse(options.db)).catch((err) => {
8405
9320
  console.error("[serve] initial parse failed:", err);
8406
9321
  });
8407
9322
  const apiServer = createApiServer(options.db, {
@@ -8409,12 +9324,14 @@ function serve(options) {
8409
9324
  onRefresh: () => runParse(options.db),
8410
9325
  onSyncStart: () => syncRuntime.start(),
8411
9326
  getSyncStatus: () => syncRuntime.getStatus(),
8412
- onConfigUpdated: () => runtimeSettings.reload()
9327
+ onConfigUpdated: () => runtimeSettings.reload(),
9328
+ runDbWrite,
9329
+ getDbWriteQueueStatus: () => dbWriteQueue.getStatus()
8413
9330
  });
8414
9331
  const webBuildDir = (() => {
8415
- const prodDir = join11(dirname3(fileURLToPath(import.meta.url)), "web");
9332
+ const prodDir = join11(dirname4(fileURLToPath(import.meta.url)), "web");
8416
9333
  if (existsSync10(prodDir)) return prodDir;
8417
- return join11(dirname3(fileURLToPath(import.meta.url)), "..", "..", "..", "web", "build");
9334
+ return join11(dirname4(fileURLToPath(import.meta.url)), "..", "..", "..", "web", "build");
8418
9335
  })();
8419
9336
  if (!existsSync10(webBuildDir)) {
8420
9337
  console.error("Web dashboard not found. Reinstall the package: npm install -g @juliantanx/aiusage");
@@ -8739,15 +9656,15 @@ function recalcPricing(db) {
8739
9656
  }
8740
9657
  const model = record.tool === "qoder" ? normalizeQoderModel(record.model) : record.model;
8741
9658
  const provider = model !== record.model ? inferProvider(model) : record.provider;
8742
- const hasPrice = resolvePrice(model) != null;
8743
- const newCost = hasPrice ? calculateCost(model, {
9659
+ const price = resolvePriceFromRegistry(db, model);
9660
+ const newCost = price ? calculateCostForPrice(price, {
8744
9661
  inputTokens: record.input_tokens,
8745
9662
  outputTokens: record.output_tokens,
8746
9663
  cacheReadTokens: record.cache_read_tokens,
8747
9664
  cacheWriteTokens: record.cache_write_tokens,
8748
9665
  thinkingTokens: record.thinking_tokens
8749
9666
  }, exchangeRate) : 0;
8750
- const costSource = hasPrice ? "pricing" : "unknown";
9667
+ const costSource = price ? "pricing" : "unknown";
8751
9668
  if (model !== record.model || provider !== record.provider || newCost !== record.cost || costSource !== record.cost_source) {
8752
9669
  updateStmt.run(model, provider, newCost, costSource, Date.now(), record.id);
8753
9670
  updatedCount++;
@@ -9651,7 +10568,7 @@ ${dashboardStatusLine()}
9651
10568
  import { join as join15 } from "path";
9652
10569
  var DB_PATH2 = join15(AIUSAGE_DIR, "cache.db");
9653
10570
  var program = new Command();
9654
- program.name("aiusage").version(true ? "1.5.2" : "dev").description("CLI tool for AI usage statistics");
10571
+ program.name("aiusage").version(true ? "1.5.4" : "dev").description("CLI tool for AI usage statistics");
9655
10572
  program.action(() => {
9656
10573
  const unknownCommand = program.args.find((arg) => typeof arg === "string");
9657
10574
  if (unknownCommand) {