@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.
- package/README.md +5 -5
- package/dist/index.js +1236 -319
- package/dist/web/_app/immutable/assets/7.CahA9iix.css +1 -0
- package/dist/web/_app/immutable/assets/_page.CahA9iix.css +1 -0
- package/dist/web/_app/immutable/chunks/{ToolSelector.DTPGX4zP.js → ToolSelector.0iI1cXVj.js} +2 -2
- package/dist/web/_app/immutable/chunks/api.DVLAklIn.js +1 -0
- package/dist/web/_app/immutable/chunks/{entry.Cxpjn2cB.js → entry.DhxonIJY.js} +2 -2
- package/dist/web/_app/immutable/chunks/i18n.DApJIXOT.js +1 -0
- package/dist/web/_app/immutable/chunks/{index.66Vu9Sio.js → index.BfJlbjrK.js} +1 -1
- package/dist/web/_app/immutable/chunks/{stores.CK2Vof2m.js → stores.BPMmgslX.js} +1 -1
- package/dist/web/_app/immutable/entry/{app.BzFxp-0D.js → app.0TSkMIPi.js} +2 -2
- package/dist/web/_app/immutable/entry/start.CnOi0uqd.js +1 -0
- package/dist/web/_app/immutable/nodes/{0.5419U2xS.js → 0.DrwtEJ21.js} +2 -2
- package/dist/web/_app/immutable/nodes/{1.C4em9nwH.js → 1.DQwsmuSX.js} +1 -1
- package/dist/web/_app/immutable/nodes/{10.DGXLHPeN.js → 10.CnTuhaua.js} +1 -1
- package/dist/web/_app/immutable/nodes/{11.SDWfdWaC.js → 11.B1soyRkK.js} +1 -1
- package/dist/web/_app/immutable/nodes/12.BCT8Dv1G.js +1 -0
- package/dist/web/_app/immutable/nodes/{13.9M-J-sT0.js → 13.Cu-n0wZ9.js} +1 -1
- package/dist/web/_app/immutable/nodes/{14.F_F2PTIe.js → 14.u96Ucsue.js} +1 -1
- package/dist/web/_app/immutable/nodes/{15.B0ylahpi.js → 15.CMhy_NGl.js} +1 -1
- package/dist/web/_app/immutable/nodes/{2.oTDdkLn9.js → 2.CJH1Fs0K.js} +2 -2
- package/dist/web/_app/immutable/nodes/{3.Oyghjrgn.js → 3.Chtgl9hh.js} +1 -1
- package/dist/web/_app/immutable/nodes/{4.DiPBEiKy.js → 4.BIau4H47.js} +1 -1
- package/dist/web/_app/immutable/nodes/{5.CtE-oF5k.js → 5.DtYG5yJy.js} +1 -1
- package/dist/web/_app/immutable/nodes/{6.tVPvf7jD.js → 6.B-ud4zCO.js} +1 -1
- package/dist/web/_app/immutable/nodes/7.7Fm6yzqA.js +9 -0
- package/dist/web/_app/immutable/nodes/{8.DJsWN3X2.js → 8.Chp3dlzK.js} +1 -1
- package/dist/web/_app/immutable/nodes/{9.30YXO0yd.js → 9.PMJrdCxq.js} +1 -1
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +7 -7
- package/package.json +3 -3
- package/dist/web/_app/immutable/assets/7.oxd6PqWk.css +0 -1
- package/dist/web/_app/immutable/assets/_page.oxd6PqWk.css +0 -1
- package/dist/web/_app/immutable/chunks/api.DC92Gfvo.js +0 -1
- package/dist/web/_app/immutable/chunks/i18n.yxU9Ieh-.js +0 -1
- package/dist/web/_app/immutable/entry/start.C2M6huNJ.js +0 -1
- package/dist/web/_app/immutable/nodes/12.B5loVhie.js +0 -1
- 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
|
|
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
|
|
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 = {
|
|
299
|
-
function
|
|
300
|
-
|
|
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 = { ...
|
|
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 = { ...
|
|
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
|
|
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 =
|
|
2491
|
+
const dir = dirname2(CREDENTIALS_FILE);
|
|
2649
2492
|
if (!existsSync6(dir)) {
|
|
2650
|
-
|
|
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
|
|
2870
|
-
import { dirname as
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
3967
|
-
const cost =
|
|
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 =
|
|
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
|
|
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} ${
|
|
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} ${
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
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
|
|
4897
|
-
json(res,
|
|
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
|
-
|
|
4920
|
-
|
|
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 =
|
|
5328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8369
|
-
|
|
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
|
|
8389
|
-
|
|
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(
|
|
9332
|
+
const prodDir = join11(dirname4(fileURLToPath(import.meta.url)), "web");
|
|
8416
9333
|
if (existsSync10(prodDir)) return prodDir;
|
|
8417
|
-
return join11(
|
|
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
|
|
8743
|
-
const newCost =
|
|
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 =
|
|
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.
|
|
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) {
|