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