@juliantanx/aiusage 1.5.1 → 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 +8 -18
- package/dist/index.js +1982 -662
- package/dist/web/_app/immutable/assets/12.C1f-IaHK.css +1 -0
- 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/assets/_page.DO7aPEA-.css +1 -0
- package/dist/web/_app/immutable/chunks/{ToolSelector.CofvKVw2.js → ToolSelector.0iI1cXVj.js} +2 -2
- package/dist/web/_app/immutable/chunks/api.DVLAklIn.js +1 -0
- package/dist/web/_app/immutable/chunks/{entry.BdArkS4U.js → entry.DhBBmiYe.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.COyyqc3i.js → stores.CSRSkpU3.js} +1 -1
- package/dist/web/_app/immutable/entry/{app.fIWuRyD7.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.D19pLRbx.js → 0.DJfx41KV.js} +1 -1
- package/dist/web/_app/immutable/nodes/{1.Cr_OSeqa.js → 1.BLZj3WNW.js} +1 -1
- package/dist/web/_app/immutable/nodes/{10.DTaETIM6.js → 10.CnTuhaua.js} +1 -1
- package/dist/web/_app/immutable/nodes/{11.BbAcHr8g.js → 11.Bu1W8Jx8.js} +1 -1
- package/dist/web/_app/immutable/nodes/12.BCT8Dv1G.js +1 -0
- package/dist/web/_app/immutable/nodes/{13.BFaBQy6e.js → 13.Cu-n0wZ9.js} +1 -1
- package/dist/web/_app/immutable/nodes/{14.BgJEC28r.js → 14.u96Ucsue.js} +1 -1
- package/dist/web/_app/immutable/nodes/{15.DjuCO6OE.js → 15.CMhy_NGl.js} +1 -1
- package/dist/web/_app/immutable/nodes/{2.CoxR54dV.js → 2.JQynNxpe.js} +1 -1
- package/dist/web/_app/immutable/nodes/{3.Bua2oewW.js → 3.Chtgl9hh.js} +1 -1
- package/dist/web/_app/immutable/nodes/{4.DJwmYYKI.js → 4.BIau4H47.js} +1 -1
- package/dist/web/_app/immutable/nodes/{5.CXmtjVJL.js → 5.DtYG5yJy.js} +1 -1
- package/dist/web/_app/immutable/nodes/{6.kpxBlrHt.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.ByAI01dT.js → 8.Chp3dlzK.js} +1 -1
- package/dist/web/_app/immutable/nodes/{9.Ubt4Bfxd.js → 9.PMJrdCxq.js} +1 -1
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +7 -7
- package/package.json +7 -5
- package/dist/web/_app/immutable/assets/12.nNHFRI35.css +0 -1
- package/dist/web/_app/immutable/assets/7.oxd6PqWk.css +0 -1
- package/dist/web/_app/immutable/assets/_page.ZSDsIDry.css +0 -1
- package/dist/web/_app/immutable/assets/_page.oxd6PqWk.css +0 -1
- package/dist/web/_app/immutable/chunks/api.Mv_KcGhp.js +0 -1
- package/dist/web/_app/immutable/chunks/i18n.CwV3KceS.js +0 -1
- package/dist/web/_app/immutable/entry/start.B0xmplla.js +0 -1
- package/dist/web/_app/immutable/nodes/12.DeLNKhlw.js +0 -1
- package/dist/web/_app/immutable/nodes/7.LSfdx4vz.js +0 -9
package/dist/index.js
CHANGED
|
@@ -8,19 +8,21 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
10
10
|
import { Command } from "commander";
|
|
11
|
-
import { writeFileSync as
|
|
11
|
+
import { writeFileSync as writeFileSync7 } from "fs";
|
|
12
12
|
import { execSync as execSync5 } from "child_process";
|
|
13
13
|
|
|
14
14
|
// src/commands/serve.ts
|
|
15
15
|
import http2 from "http";
|
|
16
|
-
import { readFileSync as
|
|
17
|
-
import { join as
|
|
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 dirname4 } from "path";
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
19
19
|
|
|
20
20
|
// src/api/server.ts
|
|
21
21
|
import http from "http";
|
|
22
|
-
import
|
|
22
|
+
import path2 from "path";
|
|
23
|
+
import { hostname, platform as platform3, tmpdir } from "os";
|
|
23
24
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
25
|
+
import { mkdtempSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
24
26
|
|
|
25
27
|
// ../core/dist/index.js
|
|
26
28
|
import { createHash } from "crypto";
|
|
@@ -33,6 +35,7 @@ var TOOLS = [
|
|
|
33
35
|
"qoder",
|
|
34
36
|
"cursor",
|
|
35
37
|
"kilocode",
|
|
38
|
+
"kelivo",
|
|
36
39
|
"copilot",
|
|
37
40
|
"gemini",
|
|
38
41
|
"kimi",
|
|
@@ -109,204 +112,22 @@ async function fetchExchangeRate() {
|
|
|
109
112
|
function convertToUSD(cny, rate) {
|
|
110
113
|
return cny * rate;
|
|
111
114
|
}
|
|
112
|
-
var
|
|
113
|
-
// Anthropic — https://platform.claude.com/docs/en/about-claude/pricing
|
|
114
|
-
"claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
115
|
-
"claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
116
|
-
"claude-opus-4-6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
117
|
-
"claude-opus-4-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
118
|
-
"claude-opus-4-1": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
|
119
|
-
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
120
|
-
"claude-sonnet-4-5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
121
|
-
"claude-haiku-4-5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
|
122
|
-
"claude-haiku-3-5": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
|
|
123
|
-
"claude-sonnet-4": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
124
|
-
"claude-opus-4": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
|
125
|
-
"claude-3-5-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
126
|
-
"claude-3-5-haiku": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
|
|
127
|
-
// OpenAI — https://developers.openai.com/api/docs/pricing
|
|
128
|
-
"gpt-5.5-pro": { input: 30, output: 180 },
|
|
129
|
-
"gpt-5.5": { input: 5, output: 30, cacheRead: 0.5 },
|
|
130
|
-
"gpt-5.4-pro": { input: 30, output: 180 },
|
|
131
|
-
"gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075 },
|
|
132
|
-
"gpt-5.4-nano": { input: 0.2, output: 1.25, cacheRead: 0.02 },
|
|
133
|
-
"gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25 },
|
|
134
|
-
"gpt-5.3-codex": { input: 1.75, output: 14, cacheRead: 0.175 },
|
|
135
|
-
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
136
|
-
"gpt-4o": { input: 2.5, output: 10 },
|
|
137
|
-
"gpt-4.1": { input: 2, output: 8 },
|
|
138
|
-
"o4-mini": { input: 1.1, output: 4.4 },
|
|
139
|
-
"o4-mini-deep-research": { input: 1, output: 4 },
|
|
140
|
-
"o3-pro": { input: 20, output: 80 },
|
|
141
|
-
"o3": { input: 2, output: 8 },
|
|
142
|
-
"o3-mini": { input: 0.55, output: 2.2, cacheRead: 0.55 },
|
|
143
|
-
"o3-deep-research": { input: 5, output: 20 },
|
|
144
|
-
"o1": { input: 15, output: 60, cacheRead: 7.5 },
|
|
145
|
-
"o1-mini": { input: 0.55, output: 2.2, cacheRead: 0.55 },
|
|
146
|
-
"codex-mini": { input: 0.75, output: 3, cacheRead: 0.025 },
|
|
147
|
-
"gpt-5.2-codex": { input: 1.75, output: 14, cacheRead: 0.175 },
|
|
148
|
-
"gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175 },
|
|
149
|
-
"gpt-5.1-codex": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
150
|
-
"gpt-5.1": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
151
|
-
"gpt-5-codex": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
152
|
-
"gpt-5": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
153
|
-
"gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025 },
|
|
154
|
-
"gpt-5-nano": { input: 0.05, output: 0.4, cacheRead: 5e-3 },
|
|
155
|
-
"gpt-4.1-mini": { input: 0.4, output: 1.6, cacheRead: 0.1 },
|
|
156
|
-
"gpt-4.1-nano": { input: 0.1, output: 0.4, cacheRead: 0.025 },
|
|
157
|
-
// Google Gemini — https://ai.google.dev/gemini-api/docs/pricing
|
|
158
|
-
"gemini-3.5-flash": { input: 1.5, output: 9 },
|
|
159
|
-
"gemini-3.1-pro": { input: 2, output: 12 },
|
|
160
|
-
"gemini-3.1-flash-lite": { input: 0.25, output: 1.5 },
|
|
161
|
-
"gemini-3-pro-preview": { input: 2, output: 12 },
|
|
162
|
-
"gemini-3-flash-preview": { input: 0.5, output: 3 },
|
|
163
|
-
"gemini-3-flash": { input: 0.5, output: 3 },
|
|
164
|
-
"gemini-3-pro-image": { input: 2, output: 12 },
|
|
165
|
-
"gemini-3.1-pro-preview": { input: 2, output: 12 },
|
|
166
|
-
"gemini-3.1-flash-lite-preview": { input: 0.25, output: 1.5 },
|
|
167
|
-
"gemini-2.5-pro": { input: 1.25, output: 10 },
|
|
168
|
-
"gemini-2.5-flash-lite": { input: 0.1, output: 0.4 },
|
|
169
|
-
"gemini-2.5-flash": { input: 0.3, output: 2.5 },
|
|
170
|
-
"gemini-2.0-flash-lite": { input: 0.075, output: 0.3 },
|
|
171
|
-
"gemini-2.0-flash": { input: 0.1, output: 0.4 },
|
|
172
|
-
// DeepSeek — https://api-docs.deepseek.com/quick_start/pricing
|
|
173
|
-
"deepseek-v4-pro": { input: 3.13, output: 6.26, cacheRead: 0.026, currency: "CNY" },
|
|
174
|
-
"deepseek-v4-flash": { input: 1.01, output: 2.02, cacheRead: 0.02, currency: "CNY" },
|
|
175
|
-
"deepseek-v3.2": { input: 2.01, output: 3.01, cacheRead: 0.2, currency: "CNY" },
|
|
176
|
-
"deepseek-v3.1": { input: 3.94, output: 11.9, cacheRead: 0.39, currency: "CNY" },
|
|
177
|
-
"deepseek-v3": { input: 2.01, output: 7.94, cacheRead: 0.2, currency: "CNY" },
|
|
178
|
-
"deepseek-reasoner": { input: 3.94, output: 15.6, cacheRead: 1, currency: "CNY" },
|
|
179
|
-
"deepseek-chat": { input: 1.93, output: 7.87, cacheRead: 0.5, currency: "CNY" },
|
|
180
|
-
// Kimi (Moonshot AI) — https://platform.kimi.ai/docs/pricing/chat
|
|
181
|
-
"kimi-k2.6": { input: 6.9, output: 29, cacheRead: 1.15, currency: "CNY" },
|
|
182
|
-
"kimi-k2.5": { input: 4.3, output: 21.6, cacheRead: 0.72, currency: "CNY" },
|
|
183
|
-
"kimi-k2-turbo": { input: 8.3, output: 57.6, cacheRead: 1.08, currency: "CNY" },
|
|
184
|
-
"kimi-k2": { input: 4.3, output: 18, cacheRead: 1.08, currency: "CNY" },
|
|
185
|
-
"kimi-k2-thinking": { input: 3.94, output: 15.7, cacheRead: 0.71, currency: "CNY" },
|
|
186
|
-
"kimi-k2-0905": { input: 3.94, output: 15.7, cacheRead: 0.71, currency: "CNY" },
|
|
187
|
-
"moonshot-v1-128k": { input: 14.4, output: 36, currency: "CNY" },
|
|
188
|
-
"moonshot-v1-32k": { input: 7.2, output: 21.6, currency: "CNY" },
|
|
189
|
-
"moonshot-v1-8k": { input: 1.44, output: 14.4, currency: "CNY" },
|
|
190
|
-
// GLM (Z.ai / Zhipu AI) — https://docs.z.ai/guides/overview/pricing — TODO: verify against official pricing
|
|
191
|
-
"glm-5.1": { input: 10, output: 32, currency: "CNY" },
|
|
192
|
-
"glm-5p1": { input: 10, output: 32, currency: "CNY" },
|
|
193
|
-
// Fireworks alias (accounts/fireworks/models/glm-5p1)
|
|
194
|
-
"glm-5-turbo": { input: 8.5, output: 29, currency: "CNY" },
|
|
195
|
-
"glm-5": { input: 7, output: 23, currency: "CNY" },
|
|
196
|
-
"glm-4.7-flashx": { input: 0.5, output: 2.8, currency: "CNY" },
|
|
197
|
-
"glm-4.7": { input: 4, output: 16, currency: "CNY" },
|
|
198
|
-
"glm-4.6": { input: 4, output: 16, currency: "CNY" },
|
|
199
|
-
"glm-4.5-x": { input: 16, output: 64, currency: "CNY" },
|
|
200
|
-
"glm-4.5-airx": { input: 8, output: 32, currency: "CNY" },
|
|
201
|
-
"glm-4.5-air": { input: 1.4, output: 8, currency: "CNY" },
|
|
202
|
-
"glm-4.5": { input: 4, output: 16, currency: "CNY" },
|
|
203
|
-
// Qwen (Alibaba Cloud) — https://www.alibabacloud.com/help/en/model-studio/model-pricing
|
|
204
|
-
"qwen3.6-plus": { input: 2, output: 12, currency: "CNY" },
|
|
205
|
-
"qwen3-235b": { input: 5, output: 20, currency: "CNY" },
|
|
206
|
-
"qwen3-32b": { input: 1.12, output: 4.48, currency: "CNY" },
|
|
207
|
-
"qwen3-30b": { input: 1.4, output: 5.6, currency: "CNY" },
|
|
208
|
-
"qwen-max": { input: 11.5, output: 46, currency: "CNY" },
|
|
209
|
-
"qwen-plus": { input: 2.8, output: 8.4, currency: "CNY" },
|
|
210
|
-
"qwen-turbo": { input: 0.35, output: 1.4, currency: "CNY" },
|
|
211
|
-
"qwen-long": { input: 0.5, output: 2, currency: "CNY" },
|
|
212
|
-
"qwen2.5-72b": { input: 10, output: 40, currency: "CNY" },
|
|
213
|
-
"qwen2.5-7b": { input: 1.26, output: 5.04, currency: "CNY" },
|
|
214
|
-
"qwen3.5-plus": { input: 1.86, output: 11.2, currency: "CNY" },
|
|
215
|
-
"qwen3-max": { input: 5.59, output: 27.9, currency: "CNY" },
|
|
216
|
-
"qwen3-235b-a22b": { input: 5.01, output: 60.1, currency: "CNY" },
|
|
217
|
-
"qwen3-coder-plus": { input: 4.65, output: 23.3, currency: "CNY" },
|
|
218
|
-
"qwen3-coder-flash": { input: 1.4, output: 6.98, currency: "CNY" },
|
|
219
|
-
"qwen3-coder-next": { input: 0.86, output: 5.37, currency: "CNY" },
|
|
220
|
-
"qwq-plus": { input: 5.73, output: 17.2, currency: "CNY" },
|
|
221
|
-
"qwq-32b": { input: 1.43, output: 4.3, currency: "CNY" },
|
|
222
|
-
// Xiaomi MiMo — https://platform.xiaomimimo.com/docs/zh-CN/pricing
|
|
223
|
-
"mimo-v2.5-pro": { input: 3, output: 6, cacheRead: 0.025, currency: "CNY" },
|
|
224
|
-
"mimo-v2.5": { input: 1, output: 2, cacheRead: 0.02, currency: "CNY" },
|
|
225
|
-
"mimo-v2-flash": { input: 0.64, output: 2.08, cacheRead: 0.064, currency: "CNY" },
|
|
226
|
-
"mimo-v2-pro": { input: 7.17, output: 21.5, currency: "CNY" },
|
|
227
|
-
// MiniMax — https://platform.minimaxi.com/docs/guides/pricing-paygo
|
|
228
|
-
"minimax-m2.7": { input: 2.15, output: 8.59, cacheRead: 0.43, cacheWrite: 2.68, currency: "CNY" },
|
|
229
|
-
"minimax-m2.7-highspeed": { input: 4.3, output: 17.2, cacheRead: 0.43, cacheWrite: 2.68, currency: "CNY" },
|
|
230
|
-
"minimax-m2.5-lightning": { input: 2.15, output: 17.2, cacheRead: 0.21, currency: "CNY" },
|
|
231
|
-
"minimax-m2.5": { input: 0.86, output: 6.81, cacheRead: 0.21, currency: "CNY" },
|
|
232
|
-
"minimax-m2.1": { input: 1.93, output: 6.81, cacheRead: 0.21, currency: "CNY" },
|
|
233
|
-
"minimax-m2.1-lightning": { input: 1.93, output: 16.7, cacheRead: 0.21, currency: "CNY" },
|
|
234
|
-
"minimax-m2": { input: 1.93, output: 6.81, cacheRead: 0.21, currency: "CNY" },
|
|
235
|
-
// Mistral AI — https://mistral.ai/pricing
|
|
236
|
-
"mistral-large": { input: 0.5, output: 1.5 },
|
|
237
|
-
"mistral-large-3": { input: 0.5, output: 1.5 },
|
|
238
|
-
"mistral-medium": { input: 1.5, output: 7.5 },
|
|
239
|
-
"mistral-small": { input: 0.1, output: 0.3 },
|
|
240
|
-
"magistral-medium": { input: 2, output: 5 },
|
|
241
|
-
"magistral-small": { input: 0.5, output: 1.5 },
|
|
242
|
-
"devstral-medium": { input: 0.4, output: 2 },
|
|
243
|
-
"devstral-small": { input: 0.1, output: 0.3 },
|
|
244
|
-
"codestral": { input: 0.3, output: 0.9 },
|
|
245
|
-
"open-mistral-nemo": { input: 0.15, output: 0.15 },
|
|
246
|
-
"open-mixtral-8x22b": { input: 2, output: 6 },
|
|
247
|
-
"ministral-14b": { input: 0.2, output: 0.2 },
|
|
248
|
-
"ministral-8b": { input: 0.15, output: 0.15 },
|
|
249
|
-
"ministral-3b": { input: 0.1, output: 0.1 },
|
|
250
|
-
"pixtral-12b": { input: 0.1, output: 0.1 },
|
|
251
|
-
// xAI Grok — https://docs.x.ai/developers/models
|
|
252
|
-
"grok-4-1-fast": { input: 0.2, output: 0.5 },
|
|
253
|
-
"grok-4.3": { input: 1.25, output: 2.5 },
|
|
254
|
-
"grok-4.20-0309-reasoning": { input: 2, output: 6 },
|
|
255
|
-
"grok-4.20-0309-non-reasoning": { input: 2, output: 6 },
|
|
256
|
-
"grok-4": { input: 1.25, output: 2.5 },
|
|
257
|
-
"grok-build-0.1": { input: 1, output: 2 },
|
|
258
|
-
"grok-3": { input: 3, output: 15 },
|
|
259
|
-
"grok-3-mini": { input: 0.25, output: 0.5 },
|
|
260
|
-
// Cohere — https://cohere.com/pricing
|
|
261
|
-
"command-a": { input: 2.5, output: 10 },
|
|
262
|
-
"command-r-plus": { input: 2.5, output: 10 },
|
|
263
|
-
"command-r": { input: 0.15, output: 0.6 },
|
|
264
|
-
// Doubao (ByteDance) — https://www.volcengine.com/docs/82379/1544106
|
|
265
|
-
"doubao-seed-code": { input: 1.22, output: 7.95, currency: "CNY" },
|
|
266
|
-
"doubao-seed-2.0-pro": { input: 3.7, output: 18.5, currency: "CNY" },
|
|
267
|
-
"doubao-seed-2.0-code": { input: 3.36, output: 16.8, currency: "CNY" },
|
|
268
|
-
"doubao-seed-2.0-lite": { input: 0.63, output: 3.78, currency: "CNY" },
|
|
269
|
-
"doubao-seed-2.0-mini": { input: 0.21, output: 2.1, currency: "CNY" },
|
|
270
|
-
"doubao-seed-1.6-flash": { input: 0.16, output: 1.58, currency: "CNY" },
|
|
271
|
-
"doubao-seed-1.6-lite": { input: 0.32, output: 2.52, currency: "CNY" },
|
|
272
|
-
"doubao-seed-1.6": { input: 0.84, output: 8.4, currency: "CNY" },
|
|
273
|
-
"doubao-1.5-pro": { input: 0.84, output: 2.1, currency: "CNY" },
|
|
274
|
-
"doubao-1.5-lite": { input: 0.32, output: 0.63, currency: "CNY" },
|
|
275
|
-
// Hunyuan (Tencent) — https://cloud.tencent.com/document/product/1729 — TODO: verify against official pricing
|
|
276
|
-
"hunyuan-t1": { input: 0.48, output: 1.87, cacheRead: 0.21, currency: "CNY" },
|
|
277
|
-
"hunyuan-a13b": { input: 1, output: 4.1, currency: "CNY" },
|
|
278
|
-
// ERNIE (Baidu) — https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Blfmc9dlf — TODO: verify against official pricing
|
|
279
|
-
"ernie-4.5-300b": { input: 2, output: 6.5, currency: "CNY" },
|
|
280
|
-
"ernie-4.5-21b": { input: 0.5, output: 2, currency: "CNY" },
|
|
281
|
-
"ernie-x1": { input: 2, output: 8, currency: "CNY" },
|
|
282
|
-
// StepFun (阶跃星辰)
|
|
283
|
-
"step-3.5-flash": { input: 0.72, output: 2.15, currency: "CNY" },
|
|
284
|
-
// Qoder credits — https://docs.qoder.com/user-guide/chat/model-tier-selector
|
|
285
|
-
// Qoder publishes credit multipliers and a $0.02 / credit pack price, but
|
|
286
|
-
// not a full per-token tariff. Estimate 1.0x as 1 credit / 10K tokens.
|
|
287
|
-
// Ultimate uses the current official limited-time 0.8x discount.
|
|
288
|
-
"qoder-auto": { input: 2, output: 2, cacheRead: 2, cacheWrite: 2 },
|
|
289
|
-
"qoder-ultimate": { input: 1.6, output: 1.6, cacheRead: 1.6, cacheWrite: 1.6 },
|
|
290
|
-
"qoder-performance": { input: 2.2, output: 2.2, cacheRead: 2.2, cacheWrite: 2.2 },
|
|
291
|
-
"qoder-efficient": { input: 0.6, output: 0.6, cacheRead: 0.6, cacheWrite: 0.6 },
|
|
292
|
-
"qoder-lite": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
293
|
-
};
|
|
115
|
+
var basePriceTable = {};
|
|
294
116
|
var userOverrides = {};
|
|
295
|
-
var PRICE_TABLE = {
|
|
296
|
-
function
|
|
297
|
-
|
|
117
|
+
var PRICE_TABLE = {};
|
|
118
|
+
function setRuntimePriceTable(base, overrides = {}) {
|
|
119
|
+
basePriceTable = { ...base };
|
|
120
|
+
userOverrides = { ...overrides };
|
|
121
|
+
PRICE_TABLE = { ...basePriceTable, ...userOverrides };
|
|
298
122
|
}
|
|
299
123
|
function setPriceOverride(model, entry) {
|
|
300
124
|
userOverrides = { ...userOverrides, [model]: entry };
|
|
301
|
-
PRICE_TABLE = { ...
|
|
125
|
+
PRICE_TABLE = { ...basePriceTable, ...userOverrides };
|
|
302
126
|
}
|
|
303
127
|
function removePriceOverride(model) {
|
|
304
128
|
const { [model]: _, ...rest } = userOverrides;
|
|
305
129
|
userOverrides = rest;
|
|
306
|
-
PRICE_TABLE = { ...
|
|
307
|
-
}
|
|
308
|
-
function getUserOverrides() {
|
|
309
|
-
return { ...userOverrides };
|
|
130
|
+
PRICE_TABLE = { ...basePriceTable, ...userOverrides };
|
|
310
131
|
}
|
|
311
132
|
var PROVIDER_PREFIXES = [
|
|
312
133
|
"accounts/fireworks/models/",
|
|
@@ -363,6 +184,15 @@ function calculateCostWithResolver(model, tokens, exchangeRate, resolver) {
|
|
|
363
184
|
function calculateCost(model, tokens, exchangeRate) {
|
|
364
185
|
return calculateCostWithResolver(model, tokens, exchangeRate, resolvePrice);
|
|
365
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
|
+
}
|
|
366
196
|
var QODER_TIER_MODELS = /* @__PURE__ */ new Set([
|
|
367
197
|
"auto",
|
|
368
198
|
"ultimate",
|
|
@@ -1048,14 +878,14 @@ function usageFromAny(parsed) {
|
|
|
1048
878
|
const cachedFromDetails = num(details.cached_tokens);
|
|
1049
879
|
const rawInput = usage.input_tokens ?? usage.input ?? usage.prompt_tokens ?? usage.input_other ?? usage.tokensIn ?? 0;
|
|
1050
880
|
const cacheRead = num(
|
|
1051
|
-
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
|
|
1052
882
|
);
|
|
1053
883
|
return {
|
|
1054
884
|
inputTokens: Math.max(0, num(rawInput) - cachedFromDetails),
|
|
1055
885
|
outputTokens: num(usage.output_tokens ?? usage.output ?? usage.completion_tokens ?? usage.tokensOut),
|
|
1056
886
|
cacheReadTokens: cacheRead,
|
|
1057
887
|
cacheWriteTokens: num(
|
|
1058
|
-
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
|
|
1059
889
|
),
|
|
1060
890
|
thinkingTokens: num(
|
|
1061
891
|
usage.thinking_tokens ?? usage.reasoning_tokens ?? usage.reasoning_output_tokens ?? details.reasoning_tokens
|
|
@@ -1199,8 +1029,8 @@ var Aggregator = class {
|
|
|
1199
1029
|
};
|
|
1200
1030
|
|
|
1201
1031
|
// src/config.ts
|
|
1202
|
-
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
1203
|
-
import { join } from "path";
|
|
1032
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
1033
|
+
import { join, dirname } from "path";
|
|
1204
1034
|
import { homedir } from "os";
|
|
1205
1035
|
var AIUSAGE_DIR = join(homedir(), ".aiusage");
|
|
1206
1036
|
var CONFIG_PATH = join(AIUSAGE_DIR, "config.json");
|
|
@@ -1230,6 +1060,7 @@ function loadConfig() {
|
|
|
1230
1060
|
}
|
|
1231
1061
|
}
|
|
1232
1062
|
function saveConfig(config) {
|
|
1063
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 448 });
|
|
1233
1064
|
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
|
|
1234
1065
|
}
|
|
1235
1066
|
function buildConsentConfig(config) {
|
|
@@ -1257,7 +1088,7 @@ function loadCredential(key) {
|
|
|
1257
1088
|
}
|
|
1258
1089
|
|
|
1259
1090
|
// src/init.ts
|
|
1260
|
-
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";
|
|
1261
1092
|
import { join as join2 } from "path";
|
|
1262
1093
|
import { randomUUID } from "crypto";
|
|
1263
1094
|
function getState(aiusageDir) {
|
|
@@ -1720,6 +1551,13 @@ function probeKiloTasks(ctx) {
|
|
|
1720
1551
|
}
|
|
1721
1552
|
return null;
|
|
1722
1553
|
}
|
|
1554
|
+
function probeKelivo(ctx) {
|
|
1555
|
+
const override = envOverride("kelivo", ctx.env);
|
|
1556
|
+
if (override) return override;
|
|
1557
|
+
const legacy = ctx.legacySources?.["kelivo"];
|
|
1558
|
+
if (legacy) return legacy;
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1723
1561
|
function probeCopilot(ctx) {
|
|
1724
1562
|
const override = envOverride("copilot", ctx.env);
|
|
1725
1563
|
if (override) return override;
|
|
@@ -1905,6 +1743,7 @@ var TOOL_REGISTRY = [
|
|
|
1905
1743
|
{ tool: "cursor", sourceKey: "cursor", label: "Cursor", probe: probeCursor },
|
|
1906
1744
|
{ tool: "kilocode", sourceKey: "kilocode", label: "Kilo Code (extension)", probe: probeKiloTasks },
|
|
1907
1745
|
{ tool: "kilocode", sourceKey: "kilocode-db", label: "KiloCode", probe: probeKiloDb },
|
|
1746
|
+
{ tool: "kelivo", sourceKey: "kelivo", label: "Kelivo", probe: probeKelivo },
|
|
1908
1747
|
{ tool: "copilot", sourceKey: "copilot", label: "Copilot", probe: probeCopilot },
|
|
1909
1748
|
{ tool: "gemini", sourceKey: "gemini", label: "Gemini CLI", probe: probeGemini },
|
|
1910
1749
|
{ tool: "kimi", sourceKey: "kimi", label: "Kimi Code", probe: probeKimi },
|
|
@@ -1926,11 +1765,11 @@ function discoverTools(env = process.env) {
|
|
|
1926
1765
|
const legacySources = config?.sources;
|
|
1927
1766
|
const ctx = { home, env, legacySources };
|
|
1928
1767
|
return TOOL_REGISTRY.map((entry) => {
|
|
1929
|
-
const
|
|
1930
|
-
if (!
|
|
1768
|
+
const path3 = entry.probe(ctx);
|
|
1769
|
+
if (!path3) {
|
|
1931
1770
|
return { tool: entry.tool, sourceKey: entry.sourceKey, label: entry.label, path: "", fileCount: 0, status: "not_found" };
|
|
1932
1771
|
}
|
|
1933
|
-
const detectedPaths = entry.sourceKey === "opencode" && !envOverride("opencode", env) && !env.OPENCODE_DB && !legacySources?.["opencode"] ? existingOpenCodeDbs(env).filter((dbPath) => existsSync3(dbPath)) : entry.sourceKey === "codex" ? codexLogDirs(ctx,
|
|
1772
|
+
const detectedPaths = entry.sourceKey === "opencode" && !envOverride("opencode", env) && !env.OPENCODE_DB && !legacySources?.["opencode"] ? existingOpenCodeDbs(env).filter((dbPath) => existsSync3(dbPath)) : entry.sourceKey === "codex" ? codexLogDirs(ctx, path3) : [path3];
|
|
1934
1773
|
let fileCount = 0;
|
|
1935
1774
|
for (const detectedPath of detectedPaths) {
|
|
1936
1775
|
try {
|
|
@@ -1938,6 +1777,8 @@ function discoverTools(env = process.env) {
|
|
|
1938
1777
|
if (stat2.isDirectory()) {
|
|
1939
1778
|
if (entry.sourceKey === "roocode" || entry.sourceKey === "kilocode") {
|
|
1940
1779
|
fileCount += findJsonFiles(detectedPath).filter((p) => basename(p) === "ui_messages.json").length;
|
|
1780
|
+
} else if (entry.sourceKey === "kelivo") {
|
|
1781
|
+
fileCount += findJsonFiles(detectedPath).filter((p) => basename(p) === "chats.json").length + findZipFiles(detectedPath).length;
|
|
1941
1782
|
} else if (entry.sourceKey === "kiro") {
|
|
1942
1783
|
fileCount += unique([...findJsonlFiles(detectedPath), ...findJsonFiles(detectedPath)]).length;
|
|
1943
1784
|
} else {
|
|
@@ -1955,7 +1796,7 @@ function discoverTools(env = process.env) {
|
|
|
1955
1796
|
tool: entry.tool,
|
|
1956
1797
|
sourceKey: entry.sourceKey,
|
|
1957
1798
|
label: entry.label,
|
|
1958
|
-
path:
|
|
1799
|
+
path: path3,
|
|
1959
1800
|
paths: visiblePaths.length > 1 || entry.sourceKey === "codex" && visiblePaths.length > 0 ? visiblePaths : void 0,
|
|
1960
1801
|
fileCount,
|
|
1961
1802
|
status
|
|
@@ -2057,6 +1898,11 @@ function discoverLogFiles(env = process.env) {
|
|
|
2057
1898
|
const paths = findJsonFiles(kilocodeTaskPath).filter((p) => basename(p) === "ui_messages.json");
|
|
2058
1899
|
if (paths.length > 0) results.push({ tool: "kilocode", paths: unique(paths) });
|
|
2059
1900
|
}
|
|
1901
|
+
const kelivoPath = probeKelivo(ctx);
|
|
1902
|
+
if (kelivoPath && existsSync3(kelivoPath)) {
|
|
1903
|
+
const paths = statSync(kelivoPath).isDirectory() ? [...findJsonFiles(kelivoPath).filter((p) => basename(p) === "chats.json"), ...findZipFiles(kelivoPath)] : basename(kelivoPath) === "chats.json" || basename(kelivoPath).endsWith(".zip") ? [kelivoPath] : [];
|
|
1904
|
+
if (paths.length > 0) results.push({ tool: "kelivo", paths: unique(paths) });
|
|
1905
|
+
}
|
|
2060
1906
|
return results;
|
|
2061
1907
|
}
|
|
2062
1908
|
function findJsonFiles(dir) {
|
|
@@ -2075,6 +1921,22 @@ function findJsonFiles(dir) {
|
|
|
2075
1921
|
}
|
|
2076
1922
|
return results;
|
|
2077
1923
|
}
|
|
1924
|
+
function findZipFiles(dir) {
|
|
1925
|
+
const results = [];
|
|
1926
|
+
try {
|
|
1927
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
1928
|
+
for (const entry of entries) {
|
|
1929
|
+
const fullPath = join3(dir, entry.name);
|
|
1930
|
+
if (entry.isDirectory()) {
|
|
1931
|
+
results.push(...findZipFiles(fullPath));
|
|
1932
|
+
} else if (entry.isFile() && extname(entry.name) === ".zip") {
|
|
1933
|
+
results.push(fullPath);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
} catch {
|
|
1937
|
+
}
|
|
1938
|
+
return results;
|
|
1939
|
+
}
|
|
2078
1940
|
function normalizeQoderPath(source) {
|
|
2079
1941
|
const normalized = source.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2080
1942
|
if (normalized.endsWith("/logs/sessions") || normalized.endsWith("/sessions") || normalized.endsWith("/segments")) {
|
|
@@ -2594,8 +2456,8 @@ function generateNonce() {
|
|
|
2594
2456
|
function generateIdempotencyKey() {
|
|
2595
2457
|
return randomBytes(16).toString("hex");
|
|
2596
2458
|
}
|
|
2597
|
-
function buildCanonicalString(method,
|
|
2598
|
-
return [method,
|
|
2459
|
+
function buildCanonicalString(method, path3, bodyHash, timestamp, nonce, deviceId, idempotencyKey) {
|
|
2460
|
+
return [method, path3, bodyHash, timestamp, nonce, deviceId, idempotencyKey].join("\n");
|
|
2599
2461
|
}
|
|
2600
2462
|
function base64url(data) {
|
|
2601
2463
|
const buf = typeof data === "string" ? Buffer.from(data) : data;
|
|
@@ -2606,14 +2468,14 @@ function sha256Buffer(data) {
|
|
|
2606
2468
|
}
|
|
2607
2469
|
|
|
2608
2470
|
// src/leaderboard/credentials.ts
|
|
2609
|
-
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as
|
|
2610
|
-
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";
|
|
2611
2473
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
2612
2474
|
var CREDENTIALS_FILE = join5(AIUSAGE_DIR, "leaderboard-credentials.json");
|
|
2613
2475
|
function saveCredentials(creds) {
|
|
2614
|
-
const dir =
|
|
2476
|
+
const dir = dirname2(CREDENTIALS_FILE);
|
|
2615
2477
|
if (!existsSync6(dir)) {
|
|
2616
|
-
|
|
2478
|
+
mkdirSync3(dir, { recursive: true });
|
|
2617
2479
|
}
|
|
2618
2480
|
const tmpFile = `${CREDENTIALS_FILE}.${randomBytes2(4).toString("hex")}.tmp`;
|
|
2619
2481
|
try {
|
|
@@ -2668,8 +2530,8 @@ async function readJsonOrNull(response) {
|
|
|
2668
2530
|
if (!contentType.includes("application/json")) return null;
|
|
2669
2531
|
return response.json().catch(() => null);
|
|
2670
2532
|
}
|
|
2671
|
-
function endpointUnavailableMessage(
|
|
2672
|
-
return `Leaderboard API endpoint ${
|
|
2533
|
+
function endpointUnavailableMessage(path3, status) {
|
|
2534
|
+
return `Leaderboard API endpoint ${path3} is unavailable (HTTP ${status}). The site deployment is not routing this API request to the Node server.`;
|
|
2673
2535
|
}
|
|
2674
2536
|
async function fetchLeaderboard(serverUrl, options = {}) {
|
|
2675
2537
|
const params = new URLSearchParams();
|
|
@@ -2832,14 +2694,14 @@ async function fetchLeaderboardStatus(serverUrl) {
|
|
|
2832
2694
|
|
|
2833
2695
|
// src/db/index.ts
|
|
2834
2696
|
import Database from "better-sqlite3";
|
|
2835
|
-
import { mkdirSync as
|
|
2836
|
-
import { dirname as
|
|
2697
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
2698
|
+
import { dirname as dirname3 } from "path";
|
|
2837
2699
|
|
|
2838
2700
|
// src/db/schema.ts
|
|
2839
2701
|
function applyPragmas(db) {
|
|
2840
2702
|
db.pragma("journal_mode = WAL");
|
|
2841
2703
|
db.pragma("foreign_keys = ON");
|
|
2842
|
-
db.pragma("busy_timeout =
|
|
2704
|
+
db.pragma("busy_timeout = 30000");
|
|
2843
2705
|
}
|
|
2844
2706
|
function createReadonlyViews(db) {
|
|
2845
2707
|
db.exec(`
|
|
@@ -3132,6 +2994,79 @@ function migrateV9(db) {
|
|
|
3132
2994
|
`);
|
|
3133
2995
|
}
|
|
3134
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
|
+
|
|
3135
3070
|
// src/db/migrations/index.ts
|
|
3136
3071
|
var MIGRATIONS = [
|
|
3137
3072
|
{ version: 1, migrate: migrateV1 },
|
|
@@ -3142,7 +3077,9 @@ var MIGRATIONS = [
|
|
|
3142
3077
|
{ version: 6, migrate: migrateV6 },
|
|
3143
3078
|
{ version: 7, migrate: migrateV7 },
|
|
3144
3079
|
{ version: 8, migrate: migrateV8 },
|
|
3145
|
-
{ version: 9, migrate: migrateV9 }
|
|
3080
|
+
{ version: 9, migrate: migrateV9 },
|
|
3081
|
+
{ version: 10, migrate: migrateV10 },
|
|
3082
|
+
{ version: 11, migrate: migrateV11 }
|
|
3146
3083
|
];
|
|
3147
3084
|
function runMigrations(db) {
|
|
3148
3085
|
createSchemaVersionTable(db);
|
|
@@ -3159,14 +3096,592 @@ function runMigrations(db) {
|
|
|
3159
3096
|
}
|
|
3160
3097
|
}
|
|
3161
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
|
+
|
|
3162
3676
|
// src/db/index.ts
|
|
3163
3677
|
function initializeDatabase(db) {
|
|
3164
3678
|
applyPragmas(db);
|
|
3165
3679
|
runMigrations(db);
|
|
3680
|
+
loadPricingRuntime(db, loadConfig());
|
|
3166
3681
|
}
|
|
3167
|
-
function createDatabase(
|
|
3168
|
-
|
|
3169
|
-
const db = new Database(
|
|
3682
|
+
function createDatabase(path3) {
|
|
3683
|
+
mkdirSync4(dirname3(path3), { recursive: true, mode: 448 });
|
|
3684
|
+
const db = new Database(path3);
|
|
3170
3685
|
initializeDatabase(db);
|
|
3171
3686
|
return db;
|
|
3172
3687
|
}
|
|
@@ -3364,10 +3879,10 @@ function getClientPlatform() {
|
|
|
3364
3879
|
}
|
|
3365
3880
|
function getCliVersion() {
|
|
3366
3881
|
try {
|
|
3367
|
-
const { readFileSync:
|
|
3882
|
+
const { readFileSync: readFileSync12 } = __require("fs");
|
|
3368
3883
|
const { join: join16 } = __require("path");
|
|
3369
3884
|
const pkgPath = join16(__dirname, "../../package.json");
|
|
3370
|
-
const pkg = JSON.parse(
|
|
3885
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
3371
3886
|
return pkg.version || "0.0.0";
|
|
3372
3887
|
} catch {
|
|
3373
3888
|
return "0.0.0";
|
|
@@ -3645,102 +4160,475 @@ Summary: ${accepted} accepted, ${flagged} flagged, ${rejected} rejected`);
|
|
|
3645
4160
|
}
|
|
3646
4161
|
}
|
|
3647
4162
|
|
|
3648
|
-
// src/
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
while (true) {
|
|
3656
|
-
const records = db.prepare(
|
|
3657
|
-
"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 ?"
|
|
3658
|
-
).all(lastId, BATCH_SIZE2);
|
|
3659
|
-
if (records.length === 0) break;
|
|
3660
|
-
const updateStmt = db.prepare("UPDATE records SET model = ?, provider = ?, cost = ?, cost_source = ?, updated_at = ? WHERE id = ?");
|
|
3661
|
-
const tx = db.transaction((batch) => {
|
|
3662
|
-
for (const r of batch) {
|
|
3663
|
-
if (r.cost_source === "log") continue;
|
|
3664
|
-
const rawModel = r.tool === "qoder" ? normalizeQoderModel(r.model) : r.model;
|
|
3665
|
-
const model = rawModel === "unknown" ? r.tool === "qoder" ? "qoder-auto" : r.model : rawModel;
|
|
3666
|
-
const provider = model !== r.model ? inferProvider(model) : r.provider;
|
|
3667
|
-
const hasPrice = resolvePrice(model) != null;
|
|
3668
|
-
const cost = hasPrice ? calculateCost(model, {
|
|
3669
|
-
inputTokens: r.input_tokens,
|
|
3670
|
-
outputTokens: r.output_tokens,
|
|
3671
|
-
cacheReadTokens: r.cache_read_tokens,
|
|
3672
|
-
cacheWriteTokens: r.cache_write_tokens,
|
|
3673
|
-
thinkingTokens: r.thinking_tokens
|
|
3674
|
-
}, exchangeRate) : 0;
|
|
3675
|
-
const costSource = hasPrice ? "pricing" : "unknown";
|
|
3676
|
-
if (model === r.model && provider === r.provider && cost === r.cost && costSource === r.cost_source) continue;
|
|
3677
|
-
updateStmt.run(model, provider, cost, costSource, Date.now(), r.id);
|
|
3678
|
-
updated++;
|
|
3679
|
-
}
|
|
3680
|
-
});
|
|
3681
|
-
tx(records);
|
|
3682
|
-
lastId = records[records.length - 1].id;
|
|
3683
|
-
}
|
|
3684
|
-
return updated;
|
|
4163
|
+
// src/commands/parse-kelivo.ts
|
|
4164
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
4165
|
+
import { basename as basename2 } from "path";
|
|
4166
|
+
import * as yauzl from "yauzl";
|
|
4167
|
+
function toTokenCount(value) {
|
|
4168
|
+
const n = Number(value);
|
|
4169
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
3685
4170
|
}
|
|
3686
|
-
function
|
|
3687
|
-
|
|
3688
|
-
if (
|
|
3689
|
-
const
|
|
3690
|
-
|
|
3691
|
-
return { where: `AND ${ts3} >= @start AND ${ts3} < @end`, params: { start: startMs, end: endMs } };
|
|
3692
|
-
}
|
|
3693
|
-
const now = /* @__PURE__ */ new Date();
|
|
3694
|
-
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
3695
|
-
if (range === "week") {
|
|
3696
|
-
const dayOfWeek = today.getDay();
|
|
3697
|
-
const diff = (dayOfWeek - weekStart + 7) % 7;
|
|
3698
|
-
const startOfWeek = new Date(today);
|
|
3699
|
-
startOfWeek.setDate(today.getDate() - diff);
|
|
3700
|
-
return { where: `AND ${ts3} >= @start`, params: { start: startOfWeek.getTime() } };
|
|
3701
|
-
}
|
|
3702
|
-
if (range === "month") {
|
|
3703
|
-
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
3704
|
-
return { where: `AND ${ts3} >= @start`, params: { start: startOfMonth.getTime() } };
|
|
3705
|
-
}
|
|
3706
|
-
if (range === "last30") {
|
|
3707
|
-
const start = new Date(today);
|
|
3708
|
-
start.setDate(start.getDate() - 30);
|
|
3709
|
-
return { where: `AND ${ts3} >= @start`, params: { start: start.getTime() } };
|
|
3710
|
-
}
|
|
3711
|
-
if (range === "all") {
|
|
3712
|
-
return { where: "", params: {} };
|
|
4171
|
+
function parseTimestamp(value, fallback) {
|
|
4172
|
+
if (typeof value === "number" && Number.isFinite(value)) return value < 1e12 ? value * 1e3 : value;
|
|
4173
|
+
if (typeof value === "string" && value.trim()) {
|
|
4174
|
+
const parsed = new Date(value).getTime();
|
|
4175
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
3713
4176
|
}
|
|
3714
|
-
return
|
|
3715
|
-
}
|
|
3716
|
-
function json(res, data, status = 200) {
|
|
3717
|
-
res.writeHead(status, { "Content-Type": "application/json" });
|
|
3718
|
-
res.end(JSON.stringify(data));
|
|
3719
|
-
}
|
|
3720
|
-
async function readJsonBody(req) {
|
|
3721
|
-
let body = "";
|
|
3722
|
-
for await (const chunk of req) body += chunk;
|
|
3723
|
-
if (!body.trim()) return {};
|
|
3724
|
-
return JSON.parse(body);
|
|
4177
|
+
return fallback;
|
|
3725
4178
|
}
|
|
3726
|
-
function
|
|
3727
|
-
return
|
|
4179
|
+
async function readZipEntryText(filePath, entryName) {
|
|
4180
|
+
return new Promise((resolve2, reject) => {
|
|
4181
|
+
yauzl.open(filePath, { lazyEntries: true }, (openError, zipfile) => {
|
|
4182
|
+
if (openError) {
|
|
4183
|
+
reject(openError);
|
|
4184
|
+
return;
|
|
4185
|
+
}
|
|
4186
|
+
if (!zipfile) {
|
|
4187
|
+
reject(new Error("Unable to open zip file"));
|
|
4188
|
+
return;
|
|
4189
|
+
}
|
|
4190
|
+
let settled = false;
|
|
4191
|
+
zipfile.readEntry();
|
|
4192
|
+
zipfile.on("entry", (entry) => {
|
|
4193
|
+
if (basename2(entry.fileName) !== entryName) {
|
|
4194
|
+
zipfile.readEntry();
|
|
4195
|
+
return;
|
|
4196
|
+
}
|
|
4197
|
+
zipfile.openReadStream(entry, (streamError, stream) => {
|
|
4198
|
+
if (streamError) {
|
|
4199
|
+
settled = true;
|
|
4200
|
+
zipfile.close();
|
|
4201
|
+
reject(streamError);
|
|
4202
|
+
return;
|
|
4203
|
+
}
|
|
4204
|
+
if (!stream) {
|
|
4205
|
+
settled = true;
|
|
4206
|
+
zipfile.close();
|
|
4207
|
+
reject(new Error(`Missing ${entryName} stream`));
|
|
4208
|
+
return;
|
|
4209
|
+
}
|
|
4210
|
+
const chunks = [];
|
|
4211
|
+
stream.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
4212
|
+
stream.on("error", (e) => {
|
|
4213
|
+
settled = true;
|
|
4214
|
+
zipfile.close();
|
|
4215
|
+
reject(e);
|
|
4216
|
+
});
|
|
4217
|
+
stream.on("end", () => {
|
|
4218
|
+
settled = true;
|
|
4219
|
+
zipfile.close();
|
|
4220
|
+
resolve2(Buffer.concat(chunks).toString("utf-8"));
|
|
4221
|
+
});
|
|
4222
|
+
});
|
|
4223
|
+
});
|
|
4224
|
+
zipfile.on("end", () => {
|
|
4225
|
+
if (!settled) reject(new Error(`${entryName} not found in zip`));
|
|
4226
|
+
});
|
|
4227
|
+
zipfile.on("error", (e) => {
|
|
4228
|
+
if (!settled) reject(e);
|
|
4229
|
+
});
|
|
4230
|
+
});
|
|
4231
|
+
});
|
|
3728
4232
|
}
|
|
3729
|
-
function
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
next_cursor: null,
|
|
3733
|
-
current_user: null,
|
|
3734
|
-
period_type: periodType,
|
|
3735
|
-
period_start: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
3736
|
-
source_status: "unavailable"
|
|
3737
|
-
};
|
|
4233
|
+
async function readKelivoExport(filePath) {
|
|
4234
|
+
const text = filePath.endsWith(".zip") ? await readZipEntryText(filePath, "chats.json") : readFileSync6(filePath, "utf-8");
|
|
4235
|
+
return JSON.parse(text);
|
|
3738
4236
|
}
|
|
3739
|
-
async function
|
|
3740
|
-
const
|
|
3741
|
-
const
|
|
3742
|
-
const
|
|
3743
|
-
|
|
4237
|
+
async function runParseKelivo(options) {
|
|
4238
|
+
const { filePath, device, deviceInstanceId, platform: platform5, now, exchangeRate } = options;
|
|
4239
|
+
const records = [];
|
|
4240
|
+
const errors = [];
|
|
4241
|
+
let parsed;
|
|
4242
|
+
try {
|
|
4243
|
+
parsed = await readKelivoExport(filePath);
|
|
4244
|
+
} catch (e) {
|
|
4245
|
+
return { records, errors: [`${filePath}: ${e instanceof Error ? e.message : e}`] };
|
|
4246
|
+
}
|
|
4247
|
+
const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
|
|
4248
|
+
for (const [index, message] of messages.entries()) {
|
|
4249
|
+
if (message?.role !== "assistant") continue;
|
|
4250
|
+
const promptTokens = toTokenCount(message.promptTokens);
|
|
4251
|
+
const completionTokens = toTokenCount(message.completionTokens);
|
|
4252
|
+
const cachedTokens = toTokenCount(message.cachedTokens);
|
|
4253
|
+
const totalTokens = toTokenCount(message.totalTokens);
|
|
4254
|
+
const inputTokens = promptTokens > 0 ? Math.max(0, promptTokens - cachedTokens) : Math.max(0, totalTokens - completionTokens - cachedTokens);
|
|
4255
|
+
const outputTokens = completionTokens;
|
|
4256
|
+
const cacheReadTokens = cachedTokens;
|
|
4257
|
+
const cacheWriteTokens = 0;
|
|
4258
|
+
const thinkingTokens = 0;
|
|
4259
|
+
if (inputTokens + outputTokens + cacheReadTokens === 0) continue;
|
|
4260
|
+
const model = typeof message.modelId === "string" && message.modelId.trim() ? message.modelId.trim() : "unknown";
|
|
4261
|
+
const provider = typeof message.providerId === "string" && message.providerId.trim() ? message.providerId.trim() : inferProvider(model);
|
|
4262
|
+
const ts3 = parseTimestamp(message.timestamp, now);
|
|
4263
|
+
const sessionId = typeof message.conversationId === "string" && message.conversationId.trim() ? message.conversationId.trim() : "unknown";
|
|
4264
|
+
const messageId = typeof message.id === "string" && message.id.trim() ? message.id.trim() : `${sessionId}:${ts3}:${index}`;
|
|
4265
|
+
const sourceKey = `kelivo:${sessionId}:${messageId}`;
|
|
4266
|
+
const tokenArgs = { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, thinkingTokens };
|
|
4267
|
+
const cost = model !== "unknown" ? calculateCost(model, tokenArgs, exchangeRate) : 0;
|
|
4268
|
+
records.push({
|
|
4269
|
+
id: generateRecordId(deviceInstanceId, sourceKey, ts3),
|
|
4270
|
+
ts: ts3,
|
|
4271
|
+
ingestedAt: now,
|
|
4272
|
+
updatedAt: now,
|
|
4273
|
+
lineOffset: index,
|
|
4274
|
+
tool: "kelivo",
|
|
4275
|
+
model,
|
|
4276
|
+
provider,
|
|
4277
|
+
inputTokens,
|
|
4278
|
+
outputTokens,
|
|
4279
|
+
cacheReadTokens,
|
|
4280
|
+
cacheWriteTokens,
|
|
4281
|
+
thinkingTokens,
|
|
4282
|
+
cost,
|
|
4283
|
+
costSource: cost > 0 ? "pricing" : "unknown",
|
|
4284
|
+
sessionId,
|
|
4285
|
+
sourceFile: filePath,
|
|
4286
|
+
device,
|
|
4287
|
+
deviceInstanceId,
|
|
4288
|
+
platform: platform5
|
|
4289
|
+
});
|
|
4290
|
+
}
|
|
4291
|
+
return { records, errors };
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
// src/db/records.ts
|
|
4295
|
+
function insertRecord(db, record) {
|
|
4296
|
+
db.prepare(`
|
|
4297
|
+
INSERT OR REPLACE INTO records (
|
|
4298
|
+
id, ts, ingested_at, synced_at, updated_at, line_offset,
|
|
4299
|
+
tool, model, provider, input_tokens, output_tokens,
|
|
4300
|
+
cache_read_tokens, cache_write_tokens, thinking_tokens,
|
|
4301
|
+
cost, cost_source, session_id, source_file, cwd, device, device_instance_id, platform
|
|
4302
|
+
) VALUES (
|
|
4303
|
+
@id, @ts, @ingestedAt, @syncedAt, @updatedAt, @lineOffset,
|
|
4304
|
+
@tool, @model, @provider, @inputTokens, @outputTokens,
|
|
4305
|
+
@cacheReadTokens, @cacheWriteTokens, @thinkingTokens,
|
|
4306
|
+
@cost, @costSource, @sessionId, @sourceFile, @cwd, @device, @deviceInstanceId, @platform
|
|
4307
|
+
)
|
|
4308
|
+
`).run({
|
|
4309
|
+
id: record.id,
|
|
4310
|
+
ts: record.ts,
|
|
4311
|
+
ingestedAt: record.ingestedAt,
|
|
4312
|
+
syncedAt: record.syncedAt ?? null,
|
|
4313
|
+
updatedAt: record.updatedAt,
|
|
4314
|
+
lineOffset: record.lineOffset,
|
|
4315
|
+
tool: record.tool,
|
|
4316
|
+
model: record.model,
|
|
4317
|
+
provider: record.provider,
|
|
4318
|
+
inputTokens: record.inputTokens,
|
|
4319
|
+
outputTokens: record.outputTokens,
|
|
4320
|
+
cacheReadTokens: record.cacheReadTokens,
|
|
4321
|
+
cacheWriteTokens: record.cacheWriteTokens,
|
|
4322
|
+
thinkingTokens: record.thinkingTokens,
|
|
4323
|
+
cost: record.cost,
|
|
4324
|
+
costSource: record.costSource,
|
|
4325
|
+
sessionId: record.sessionId,
|
|
4326
|
+
sourceFile: record.sourceFile,
|
|
4327
|
+
cwd: record.cwd ?? "",
|
|
4328
|
+
device: record.device,
|
|
4329
|
+
deviceInstanceId: record.deviceInstanceId,
|
|
4330
|
+
platform: record.platform ?? ""
|
|
4331
|
+
});
|
|
4332
|
+
}
|
|
4333
|
+
function getUnsyncedRecords(db, target) {
|
|
4334
|
+
const rows = target ? db.prepare(`
|
|
4335
|
+
SELECT r.* FROM records r
|
|
4336
|
+
LEFT JOIN sync_record_state s
|
|
4337
|
+
ON s.record_id = r.id AND s.target = ?
|
|
4338
|
+
WHERE r.source_file NOT LIKE 'synced/%'
|
|
4339
|
+
AND (s.synced_at IS NULL OR r.updated_at > s.synced_at)
|
|
4340
|
+
`).all(target) : db.prepare(
|
|
4341
|
+
"SELECT * FROM records WHERE synced_at IS NULL OR updated_at > synced_at"
|
|
4342
|
+
).all();
|
|
4343
|
+
return rows.map(mapRowToRecord);
|
|
4344
|
+
}
|
|
4345
|
+
function markRecordsSynced(db, ids, syncedAt, target) {
|
|
4346
|
+
if (ids.length === 0) return;
|
|
4347
|
+
if (target) {
|
|
4348
|
+
const insertStmt = db.prepare(`
|
|
4349
|
+
INSERT INTO sync_record_state (record_id, target, synced_at)
|
|
4350
|
+
VALUES (?, ?, ?)
|
|
4351
|
+
ON CONFLICT(record_id, target) DO UPDATE SET synced_at = excluded.synced_at
|
|
4352
|
+
`);
|
|
4353
|
+
const legacyStmt = db.prepare("UPDATE records SET synced_at = ? WHERE id = ?");
|
|
4354
|
+
const tx2 = db.transaction((recordIds) => {
|
|
4355
|
+
for (const id of recordIds) {
|
|
4356
|
+
insertStmt.run(id, target, syncedAt);
|
|
4357
|
+
legacyStmt.run(syncedAt, id);
|
|
4358
|
+
}
|
|
4359
|
+
});
|
|
4360
|
+
tx2(ids);
|
|
4361
|
+
return;
|
|
4362
|
+
}
|
|
4363
|
+
const updateStmt = db.prepare("UPDATE records SET synced_at = ? WHERE id = ?");
|
|
4364
|
+
const tx = db.transaction((recordIds) => {
|
|
4365
|
+
for (const id of recordIds) updateStmt.run(syncedAt, id);
|
|
4366
|
+
});
|
|
4367
|
+
tx(ids);
|
|
4368
|
+
}
|
|
4369
|
+
function mapRowToRecord(row) {
|
|
4370
|
+
return {
|
|
4371
|
+
id: row.id,
|
|
4372
|
+
ts: row.ts,
|
|
4373
|
+
ingestedAt: row.ingested_at,
|
|
4374
|
+
syncedAt: row.synced_at != null ? row.synced_at : void 0,
|
|
4375
|
+
updatedAt: row.updated_at,
|
|
4376
|
+
lineOffset: row.line_offset,
|
|
4377
|
+
tool: row.tool,
|
|
4378
|
+
model: row.model,
|
|
4379
|
+
provider: row.provider,
|
|
4380
|
+
inputTokens: row.input_tokens,
|
|
4381
|
+
outputTokens: row.output_tokens,
|
|
4382
|
+
cacheReadTokens: row.cache_read_tokens,
|
|
4383
|
+
cacheWriteTokens: row.cache_write_tokens,
|
|
4384
|
+
thinkingTokens: row.thinking_tokens,
|
|
4385
|
+
cost: row.cost,
|
|
4386
|
+
costSource: row.cost_source,
|
|
4387
|
+
sessionId: row.session_id,
|
|
4388
|
+
sourceFile: row.source_file,
|
|
4389
|
+
cwd: row.cwd || void 0,
|
|
4390
|
+
device: row.device,
|
|
4391
|
+
deviceInstanceId: row.device_instance_id,
|
|
4392
|
+
platform: row.platform || void 0
|
|
4393
|
+
};
|
|
4394
|
+
}
|
|
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
|
+
|
|
4421
|
+
// src/api/server.ts
|
|
4422
|
+
var pendingLeaderboardAuth = /* @__PURE__ */ new Map();
|
|
4423
|
+
function getManualImportMetadata(db) {
|
|
4424
|
+
const rows = db.prepare(`
|
|
4425
|
+
SELECT tool, MAX(ingested_at) AS lastImportedAt
|
|
4426
|
+
FROM records
|
|
4427
|
+
WHERE tool IN ('kelivo')
|
|
4428
|
+
GROUP BY tool
|
|
4429
|
+
`).all();
|
|
4430
|
+
const metadata = {};
|
|
4431
|
+
for (const row of rows) {
|
|
4432
|
+
if (typeof row.lastImportedAt === "number") {
|
|
4433
|
+
metadata[row.tool] = { lastImportedAt: row.lastImportedAt };
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
return metadata;
|
|
4437
|
+
}
|
|
4438
|
+
function attachManualImportMetadata(tools, db) {
|
|
4439
|
+
const metadata = getManualImportMetadata(db);
|
|
4440
|
+
return tools.map((tool) => {
|
|
4441
|
+
const toolMetadata = metadata[tool.tool];
|
|
4442
|
+
return toolMetadata ? { ...tool, ...toolMetadata } : tool;
|
|
4443
|
+
});
|
|
4444
|
+
}
|
|
4445
|
+
function countExistingRecordIds(db, ids) {
|
|
4446
|
+
const uniqueIds = [...new Set(ids)];
|
|
4447
|
+
if (uniqueIds.length === 0) return 0;
|
|
4448
|
+
const stmt = db.prepare("SELECT 1 FROM records WHERE id = ? LIMIT 1");
|
|
4449
|
+
let count = 0;
|
|
4450
|
+
for (const id of uniqueIds) {
|
|
4451
|
+
if (stmt.get(id)) count++;
|
|
4452
|
+
}
|
|
4453
|
+
return count;
|
|
4454
|
+
}
|
|
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) {
|
|
4472
|
+
const BATCH_SIZE2 = 1e3;
|
|
4473
|
+
let updated = 0;
|
|
4474
|
+
let processed = 0;
|
|
4475
|
+
let skipped = 0;
|
|
4476
|
+
let lastId = "";
|
|
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 });
|
|
4488
|
+
while (true) {
|
|
4489
|
+
const records = db.prepare(
|
|
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 ?"
|
|
4491
|
+
).all(lastId, BATCH_SIZE2);
|
|
4492
|
+
if (records.length === 0) break;
|
|
4493
|
+
const updateStmt = db.prepare("UPDATE records SET model = ?, provider = ?, cost = ?, cost_source = ?, updated_at = ? WHERE id = ?");
|
|
4494
|
+
const tx = db.transaction((batch) => {
|
|
4495
|
+
for (const r of batch) {
|
|
4496
|
+
processed++;
|
|
4497
|
+
if (r.cost_source === "log") {
|
|
4498
|
+
skipped++;
|
|
4499
|
+
continue;
|
|
4500
|
+
}
|
|
4501
|
+
const rawModel = r.tool === "qoder" ? normalizeQoderModel(r.model) : r.model;
|
|
4502
|
+
const model = rawModel === "unknown" ? r.tool === "qoder" ? "qoder-auto" : r.model : rawModel;
|
|
4503
|
+
const provider = model !== r.model ? inferProvider(model) : r.provider;
|
|
4504
|
+
const price = resolveCachedPrice(model);
|
|
4505
|
+
const cost = price ? calculateCostForPrice(price, {
|
|
4506
|
+
inputTokens: r.input_tokens,
|
|
4507
|
+
outputTokens: r.output_tokens,
|
|
4508
|
+
cacheReadTokens: r.cache_read_tokens,
|
|
4509
|
+
cacheWriteTokens: r.cache_write_tokens,
|
|
4510
|
+
thinkingTokens: r.thinking_tokens
|
|
4511
|
+
}, exchangeRate) : 0;
|
|
4512
|
+
const costSource = price ? "pricing" : "unknown";
|
|
4513
|
+
if (model === r.model && provider === r.provider && cost === r.cost && costSource === r.cost_source) continue;
|
|
4514
|
+
updateStmt.run(model, provider, cost, costSource, Date.now(), r.id);
|
|
4515
|
+
updated++;
|
|
4516
|
+
}
|
|
4517
|
+
});
|
|
4518
|
+
tx(records);
|
|
4519
|
+
lastId = records[records.length - 1].id;
|
|
4520
|
+
onProgress?.({ total, processed, updated, skipped });
|
|
4521
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
4522
|
+
}
|
|
4523
|
+
return updated;
|
|
4524
|
+
}
|
|
4525
|
+
function getDateRangeFilter(range, from, to, prefix = "", weekStart = 1) {
|
|
4526
|
+
const ts3 = prefix ? `${prefix}.ts` : "ts";
|
|
4527
|
+
if (from && to) {
|
|
4528
|
+
const startMs = new Date(from).getTime();
|
|
4529
|
+
const endMs = (/* @__PURE__ */ new Date(to + "T23:59:59.999Z")).getTime();
|
|
4530
|
+
return { where: `AND ${ts3} >= @start AND ${ts3} < @end`, params: { start: startMs, end: endMs } };
|
|
4531
|
+
}
|
|
4532
|
+
const now = /* @__PURE__ */ new Date();
|
|
4533
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
4534
|
+
if (range === "week") {
|
|
4535
|
+
const dayOfWeek = today.getDay();
|
|
4536
|
+
const diff = (dayOfWeek - weekStart + 7) % 7;
|
|
4537
|
+
const startOfWeek = new Date(today);
|
|
4538
|
+
startOfWeek.setDate(today.getDate() - diff);
|
|
4539
|
+
return { where: `AND ${ts3} >= @start`, params: { start: startOfWeek.getTime() } };
|
|
4540
|
+
}
|
|
4541
|
+
if (range === "month") {
|
|
4542
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
4543
|
+
return { where: `AND ${ts3} >= @start`, params: { start: startOfMonth.getTime() } };
|
|
4544
|
+
}
|
|
4545
|
+
if (range === "last30") {
|
|
4546
|
+
const start = new Date(today);
|
|
4547
|
+
start.setDate(start.getDate() - 30);
|
|
4548
|
+
return { where: `AND ${ts3} >= @start`, params: { start: start.getTime() } };
|
|
4549
|
+
}
|
|
4550
|
+
if (range === "all") {
|
|
4551
|
+
return { where: "", params: {} };
|
|
4552
|
+
}
|
|
4553
|
+
return { where: `AND ${ts3} >= @start`, params: { start: today.getTime() } };
|
|
4554
|
+
}
|
|
4555
|
+
function json(res, data, status = 200) {
|
|
4556
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
4557
|
+
res.end(JSON.stringify(data));
|
|
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
|
+
}
|
|
4570
|
+
async function readJsonBody(req) {
|
|
4571
|
+
let body = "";
|
|
4572
|
+
for await (const chunk of req) body += chunk;
|
|
4573
|
+
if (!body.trim()) return {};
|
|
4574
|
+
return JSON.parse(body);
|
|
4575
|
+
}
|
|
4576
|
+
async function readBodyBuffer(req) {
|
|
4577
|
+
const chunks = [];
|
|
4578
|
+
for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
4579
|
+
return Buffer.concat(chunks);
|
|
4580
|
+
}
|
|
4581
|
+
function multipartBoundary(contentType) {
|
|
4582
|
+
const value = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
4583
|
+
if (!value) return null;
|
|
4584
|
+
const match = value.match(/(?:^|;)\s*boundary=("([^"]+)"|[^;]+)/i);
|
|
4585
|
+
if (!match) return null;
|
|
4586
|
+
return (match[2] ?? match[1]).replace(/^"|"$/g, "");
|
|
4587
|
+
}
|
|
4588
|
+
async function readMultipartFile(req) {
|
|
4589
|
+
const boundary = multipartBoundary(req.headers["content-type"]);
|
|
4590
|
+
if (!boundary) return null;
|
|
4591
|
+
const body = await readBodyBuffer(req);
|
|
4592
|
+
const marker = Buffer.from(`--${boundary}`);
|
|
4593
|
+
let position = body.indexOf(marker);
|
|
4594
|
+
while (position !== -1) {
|
|
4595
|
+
position += marker.length;
|
|
4596
|
+
if (body.subarray(position, position + 2).toString() === "--") return null;
|
|
4597
|
+
if (body.subarray(position, position + 2).toString() === "\r\n") position += 2;
|
|
4598
|
+
const headerEnd = body.indexOf(Buffer.from("\r\n\r\n"), position);
|
|
4599
|
+
if (headerEnd === -1) return null;
|
|
4600
|
+
const headers = body.subarray(position, headerEnd).toString("utf-8");
|
|
4601
|
+
const filenameMatch = headers.match(/Content-Disposition:[^\r\n]*\bname="file"[^\r\n]*\bfilename="([^"]+)"/i);
|
|
4602
|
+
const dataStart = headerEnd + 4;
|
|
4603
|
+
const nextBoundary = body.indexOf(Buffer.from(`\r
|
|
4604
|
+
--${boundary}`), dataStart);
|
|
4605
|
+
if (nextBoundary === -1) return null;
|
|
4606
|
+
if (filenameMatch) return { filename: path2.basename(filenameMatch[1]), data: body.subarray(dataStart, nextBoundary) };
|
|
4607
|
+
position = body.indexOf(marker, nextBoundary);
|
|
4608
|
+
}
|
|
4609
|
+
return null;
|
|
4610
|
+
}
|
|
4611
|
+
function isKelivoBackupFilename(filename) {
|
|
4612
|
+
return filename === "chats.json" || filename.toLowerCase().endsWith(".zip");
|
|
4613
|
+
}
|
|
4614
|
+
function getDeviceName() {
|
|
4615
|
+
return loadConfig()?.device || hostname() || `${platform3()}-${process.arch}`;
|
|
4616
|
+
}
|
|
4617
|
+
function getLeaderboardFallback(periodType) {
|
|
4618
|
+
return {
|
|
4619
|
+
entries: [],
|
|
4620
|
+
next_cursor: null,
|
|
4621
|
+
current_user: null,
|
|
4622
|
+
period_type: periodType,
|
|
4623
|
+
period_start: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
4624
|
+
source_status: "unavailable"
|
|
4625
|
+
};
|
|
4626
|
+
}
|
|
4627
|
+
async function proxyLeaderboard(res, url) {
|
|
4628
|
+
const periodType = url.searchParams.get("period_type") || "daily";
|
|
4629
|
+
const upstreamBase = getSiteUrl();
|
|
4630
|
+
const upstream = new URL("/api/leaderboard", upstreamBase);
|
|
4631
|
+
upstream.searchParams.set("period_type", periodType);
|
|
3744
4632
|
const periodStart = url.searchParams.get("period_start");
|
|
3745
4633
|
if (periodStart) upstream.searchParams.set("period_start", periodStart);
|
|
3746
4634
|
const cursor = url.searchParams.get("cursor");
|
|
@@ -3764,6 +4652,23 @@ async function proxyLeaderboard(res, url) {
|
|
|
3764
4652
|
json(res, getLeaderboardFallback(periodType));
|
|
3765
4653
|
}
|
|
3766
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
|
+
}
|
|
3767
4672
|
function getToolFilter(tool, prefix = "") {
|
|
3768
4673
|
if (!tool) return { where: "", params: {} };
|
|
3769
4674
|
const col = prefix ? `${prefix}.tool` : "tool";
|
|
@@ -3817,6 +4722,90 @@ function createApiServer(db, options) {
|
|
|
3817
4722
|
const cfg = loadConfig();
|
|
3818
4723
|
let weekStart = cfg?.weekStart ?? 1;
|
|
3819
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
|
+
};
|
|
3820
4809
|
const server = http.createServer(async (req, res) => {
|
|
3821
4810
|
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
3822
4811
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -3855,6 +4844,10 @@ function createApiServer(db, options) {
|
|
|
3855
4844
|
json(res, { ok: true });
|
|
3856
4845
|
return;
|
|
3857
4846
|
}
|
|
4847
|
+
if (url.pathname === "/api/cli/sync/status" && req.method === "GET") {
|
|
4848
|
+
await proxyCloudSyncStatus(res);
|
|
4849
|
+
return;
|
|
4850
|
+
}
|
|
3858
4851
|
if (dashboardPassword && shouldProtectApiPath(url.pathname) && !isAuthenticated(dashboardPassword, req.headers.cookie)) {
|
|
3859
4852
|
json(res, { error: { code: "UNAUTHORIZED", message: "Authentication required" } }, 401);
|
|
3860
4853
|
return;
|
|
@@ -4470,72 +5463,48 @@ function createApiServer(db, options) {
|
|
|
4470
5463
|
}
|
|
4471
5464
|
if (url.pathname === "/api/pricing") {
|
|
4472
5465
|
if (req.method === "GET") {
|
|
4473
|
-
|
|
4474
|
-
const overrides = getUserOverrides();
|
|
4475
|
-
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();
|
|
4476
|
-
const models = dbModels.map((r) => {
|
|
4477
|
-
const model = r.model;
|
|
4478
|
-
const exactPrice = table[model];
|
|
4479
|
-
const resolvedPrice = resolvePrice(model);
|
|
4480
|
-
const isOverride = model in overrides;
|
|
4481
|
-
const isDefault = model in DEFAULT_PRICE_TABLE && !isOverride;
|
|
4482
|
-
let matchedBy = null;
|
|
4483
|
-
if (!exactPrice && resolvedPrice) {
|
|
4484
|
-
for (const prefix of Object.keys(table)) {
|
|
4485
|
-
if (model.startsWith(prefix) || model.toLowerCase().startsWith(prefix)) {
|
|
4486
|
-
matchedBy = prefix;
|
|
4487
|
-
break;
|
|
4488
|
-
}
|
|
4489
|
-
}
|
|
4490
|
-
if (!matchedBy) {
|
|
4491
|
-
const stripped = model.replace(/^[^/]+\//, "").toLowerCase();
|
|
4492
|
-
for (const prefix of Object.keys(table)) {
|
|
4493
|
-
if (stripped.startsWith(prefix)) {
|
|
4494
|
-
matchedBy = prefix;
|
|
4495
|
-
break;
|
|
4496
|
-
}
|
|
4497
|
-
}
|
|
4498
|
-
}
|
|
4499
|
-
}
|
|
4500
|
-
return {
|
|
4501
|
-
model,
|
|
4502
|
-
price: resolvedPrice ?? null,
|
|
4503
|
-
currency: resolvedPrice?.currency ?? "USD",
|
|
4504
|
-
isDefault,
|
|
4505
|
-
isOverride,
|
|
4506
|
-
matchedBy
|
|
4507
|
-
};
|
|
4508
|
-
});
|
|
4509
|
-
json(res, { models, overrides });
|
|
5466
|
+
json(res, { models: listPricingModels(db), registry: getPricingRegistrySummary(db), targets: listPricingAliasTargets(db), aliasBindings: getUserAliasBindings(db), localBindings: listLocalModelBindings(db) });
|
|
4510
5467
|
return;
|
|
4511
5468
|
}
|
|
4512
5469
|
if (req.method === "PUT") {
|
|
4513
5470
|
let body = "";
|
|
4514
5471
|
for await (const chunk of req) body += chunk;
|
|
5472
|
+
let data;
|
|
4515
5473
|
try {
|
|
4516
|
-
|
|
4517
|
-
if (!data.model || typeof data.input !== "number" || typeof data.output !== "number") {
|
|
4518
|
-
json(res, { error: { code: "INVALID_PARAM", message: "model, input, output required" } }, 400);
|
|
4519
|
-
return;
|
|
4520
|
-
}
|
|
4521
|
-
const entry = {
|
|
4522
|
-
input: data.input,
|
|
4523
|
-
output: data.output,
|
|
4524
|
-
cacheRead: data.cacheRead,
|
|
4525
|
-
cacheWrite: data.cacheWrite
|
|
4526
|
-
};
|
|
4527
|
-
if (data.currency === "CNY") {
|
|
4528
|
-
entry.currency = "CNY";
|
|
4529
|
-
}
|
|
4530
|
-
setPriceOverride(data.model, entry);
|
|
4531
|
-
const cfg2 = loadConfig() ?? {};
|
|
4532
|
-
cfg2.priceOverrides = { ...cfg2.priceOverrides, [data.model]: entry };
|
|
4533
|
-
saveConfig(cfg2);
|
|
4534
|
-
const recalculated = recalcCosts(db);
|
|
4535
|
-
json(res, { ok: true, recalculated });
|
|
5474
|
+
data = JSON.parse(body);
|
|
4536
5475
|
} catch {
|
|
4537
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";
|
|
4538
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;
|
|
5506
|
+
}
|
|
5507
|
+
json(res, { ok: true, needsRecalc: true, needsRecalcSince: markPricingNeedsRecalc() });
|
|
4539
5508
|
return;
|
|
4540
5509
|
}
|
|
4541
5510
|
if (req.method === "DELETE") {
|
|
@@ -4544,20 +5513,94 @@ function createApiServer(db, options) {
|
|
|
4544
5513
|
json(res, { error: { code: "INVALID_PARAM", message: "model param required" } }, 400);
|
|
4545
5514
|
return;
|
|
4546
5515
|
}
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
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;
|
|
4552
5534
|
}
|
|
4553
|
-
const recalculated = recalcCosts(db);
|
|
4554
|
-
json(res, { ok: true, recalculated });
|
|
4555
5535
|
return;
|
|
4556
5536
|
}
|
|
4557
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
|
+
}
|
|
4558
5601
|
if (url.pathname === "/api/pricing/recalc" && req.method === "POST") {
|
|
4559
|
-
const
|
|
4560
|
-
json(res,
|
|
5602
|
+
const result = startPricingRecalc();
|
|
5603
|
+
json(res, result, result.accepted ? 202 : 200);
|
|
4561
5604
|
return;
|
|
4562
5605
|
}
|
|
4563
5606
|
if (url.pathname === "/api/sync") {
|
|
@@ -4579,8 +5622,16 @@ function createApiServer(db, options) {
|
|
|
4579
5622
|
json(res, { error: { code: "NOT_AVAILABLE", message: "Refresh not available" } }, 501);
|
|
4580
5623
|
return;
|
|
4581
5624
|
}
|
|
4582
|
-
|
|
4583
|
-
|
|
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
|
+
}
|
|
4584
5635
|
return;
|
|
4585
5636
|
}
|
|
4586
5637
|
if (url.pathname === "/api/quotas" && req.method === "GET") {
|
|
@@ -4954,141 +6005,86 @@ function createApiServer(db, options) {
|
|
|
4954
6005
|
}
|
|
4955
6006
|
}
|
|
4956
6007
|
if (url.pathname === "/api/detected-tools" && req.method === "GET") {
|
|
4957
|
-
const tools = discoverTools();
|
|
6008
|
+
const tools = attachManualImportMetadata(discoverTools(), db);
|
|
4958
6009
|
json(res, { tools });
|
|
4959
6010
|
return;
|
|
4960
6011
|
}
|
|
4961
|
-
if (url.pathname === "/api/
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
updatedAt: record.updatedAt,
|
|
5008
|
-
lineOffset: record.lineOffset,
|
|
5009
|
-
tool: record.tool,
|
|
5010
|
-
model: record.model,
|
|
5011
|
-
provider: record.provider,
|
|
5012
|
-
inputTokens: record.inputTokens,
|
|
5013
|
-
outputTokens: record.outputTokens,
|
|
5014
|
-
cacheReadTokens: record.cacheReadTokens,
|
|
5015
|
-
cacheWriteTokens: record.cacheWriteTokens,
|
|
5016
|
-
thinkingTokens: record.thinkingTokens,
|
|
5017
|
-
cost: record.cost,
|
|
5018
|
-
costSource: record.costSource,
|
|
5019
|
-
sessionId: record.sessionId,
|
|
5020
|
-
sourceFile: record.sourceFile,
|
|
5021
|
-
cwd: record.cwd ?? "",
|
|
5022
|
-
device: record.device,
|
|
5023
|
-
deviceInstanceId: record.deviceInstanceId,
|
|
5024
|
-
platform: record.platform ?? ""
|
|
5025
|
-
});
|
|
5026
|
-
}
|
|
5027
|
-
function getUnsyncedRecords(db, target) {
|
|
5028
|
-
const rows = target ? db.prepare(`
|
|
5029
|
-
SELECT r.* FROM records r
|
|
5030
|
-
LEFT JOIN sync_record_state s
|
|
5031
|
-
ON s.record_id = r.id AND s.target = ?
|
|
5032
|
-
WHERE r.source_file NOT LIKE 'synced/%'
|
|
5033
|
-
AND (s.synced_at IS NULL OR r.updated_at > s.synced_at)
|
|
5034
|
-
`).all(target) : db.prepare(
|
|
5035
|
-
"SELECT * FROM records WHERE synced_at IS NULL OR updated_at > synced_at"
|
|
5036
|
-
).all();
|
|
5037
|
-
return rows.map(mapRowToRecord);
|
|
5038
|
-
}
|
|
5039
|
-
function markRecordsSynced(db, ids, syncedAt, target) {
|
|
5040
|
-
if (ids.length === 0) return;
|
|
5041
|
-
if (target) {
|
|
5042
|
-
const insertStmt = db.prepare(`
|
|
5043
|
-
INSERT INTO sync_record_state (record_id, target, synced_at)
|
|
5044
|
-
VALUES (?, ?, ?)
|
|
5045
|
-
ON CONFLICT(record_id, target) DO UPDATE SET synced_at = excluded.synced_at
|
|
5046
|
-
`);
|
|
5047
|
-
const legacyStmt = db.prepare("UPDATE records SET synced_at = ? WHERE id = ?");
|
|
5048
|
-
const tx2 = db.transaction((recordIds) => {
|
|
5049
|
-
for (const id of recordIds) {
|
|
5050
|
-
insertStmt.run(id, target, syncedAt);
|
|
5051
|
-
legacyStmt.run(syncedAt, id);
|
|
6012
|
+
if (url.pathname === "/api/import/kelivo" && req.method === "POST") {
|
|
6013
|
+
let tempDir = null;
|
|
6014
|
+
try {
|
|
6015
|
+
const upload = await readMultipartFile(req);
|
|
6016
|
+
if (!upload) {
|
|
6017
|
+
json(res, { error: { code: "INVALID_UPLOAD", message: "Missing uploaded file" } }, 400);
|
|
6018
|
+
return;
|
|
6019
|
+
}
|
|
6020
|
+
if (!isKelivoBackupFilename(upload.filename)) {
|
|
6021
|
+
json(res, { error: { code: "INVALID_FILE", message: "Upload a Kelivo .zip backup or chats.json" } }, 400);
|
|
6022
|
+
return;
|
|
6023
|
+
}
|
|
6024
|
+
tempDir = mkdtempSync(path2.join(tmpdir(), "aiusage-kelivo-"));
|
|
6025
|
+
const tempPath = path2.join(tempDir, upload.filename);
|
|
6026
|
+
writeFileSync4(tempPath, upload.data);
|
|
6027
|
+
const cfg2 = loadConfig();
|
|
6028
|
+
const now = Date.now();
|
|
6029
|
+
const result = await runParseKelivo({
|
|
6030
|
+
filePath: tempPath,
|
|
6031
|
+
device: getDeviceName(),
|
|
6032
|
+
deviceInstanceId: options?.currentDeviceInstanceId ?? "unknown",
|
|
6033
|
+
platform: cfg2?.platform ?? platform3(),
|
|
6034
|
+
now,
|
|
6035
|
+
exchangeRate: resolveExchangeRate(cfg2 ?? {})
|
|
6036
|
+
});
|
|
6037
|
+
if (result.errors.length > 0 && result.records.length === 0) {
|
|
6038
|
+
json(res, { error: { code: "IMPORT_FAILED", message: result.errors[0] }, errors: result.errors }, 400);
|
|
6039
|
+
return;
|
|
6040
|
+
}
|
|
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
|
+
});
|
|
6046
|
+
const imported = result.records.length;
|
|
6047
|
+
const added = Math.max(0, new Set(result.records.map((record) => record.id)).size - existingCount);
|
|
6048
|
+
json(res, {
|
|
6049
|
+
imported,
|
|
6050
|
+
added,
|
|
6051
|
+
lastImportedAt: imported > 0 ? now : void 0,
|
|
6052
|
+
errors: result.errors
|
|
6053
|
+
});
|
|
6054
|
+
} finally {
|
|
6055
|
+
if (tempDir) rmSync(tempDir, { recursive: true, force: true });
|
|
6056
|
+
}
|
|
6057
|
+
return;
|
|
5052
6058
|
}
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
6059
|
+
if (url.pathname === "/api/exchange-rate/refresh" && req.method === "POST") {
|
|
6060
|
+
const rate = await fetchExchangeRate();
|
|
6061
|
+
if (rate == null) {
|
|
6062
|
+
json(res, { error: { code: "FETCH_FAILED", message: "Failed to fetch exchange rate" } }, 502);
|
|
6063
|
+
return;
|
|
6064
|
+
}
|
|
6065
|
+
const cfg2 = loadConfig() ?? {};
|
|
6066
|
+
cfg2.exchangeRateCache = { CNY_USD: rate, fetchedAt: Date.now() };
|
|
6067
|
+
saveConfig(cfg2);
|
|
6068
|
+
json(res, { rate, fetchedAt: cfg2.exchangeRateCache.fetchedAt });
|
|
6069
|
+
return;
|
|
6070
|
+
}
|
|
6071
|
+
json(res, { error: { code: "NOT_FOUND", message: "Endpoint not found" } }, 404);
|
|
6072
|
+
} catch (error) {
|
|
6073
|
+
console.error("API error:", error);
|
|
6074
|
+
json(res, { error: { code: "INTERNAL", message: "Internal server error" } }, 500);
|
|
6075
|
+
}
|
|
5060
6076
|
});
|
|
5061
|
-
|
|
5062
|
-
}
|
|
5063
|
-
function mapRowToRecord(row) {
|
|
5064
|
-
return {
|
|
5065
|
-
id: row.id,
|
|
5066
|
-
ts: row.ts,
|
|
5067
|
-
ingestedAt: row.ingested_at,
|
|
5068
|
-
syncedAt: row.synced_at != null ? row.synced_at : void 0,
|
|
5069
|
-
updatedAt: row.updated_at,
|
|
5070
|
-
lineOffset: row.line_offset,
|
|
5071
|
-
tool: row.tool,
|
|
5072
|
-
model: row.model,
|
|
5073
|
-
provider: row.provider,
|
|
5074
|
-
inputTokens: row.input_tokens,
|
|
5075
|
-
outputTokens: row.output_tokens,
|
|
5076
|
-
cacheReadTokens: row.cache_read_tokens,
|
|
5077
|
-
cacheWriteTokens: row.cache_write_tokens,
|
|
5078
|
-
thinkingTokens: row.thinking_tokens,
|
|
5079
|
-
cost: row.cost,
|
|
5080
|
-
costSource: row.cost_source,
|
|
5081
|
-
sessionId: row.session_id,
|
|
5082
|
-
sourceFile: row.source_file,
|
|
5083
|
-
cwd: row.cwd || void 0,
|
|
5084
|
-
device: row.device,
|
|
5085
|
-
deviceInstanceId: row.device_instance_id,
|
|
5086
|
-
platform: row.platform || void 0
|
|
5087
|
-
};
|
|
6077
|
+
return server;
|
|
5088
6078
|
}
|
|
5089
6079
|
|
|
6080
|
+
// src/commands/parse.ts
|
|
6081
|
+
import Database2 from "better-sqlite3";
|
|
6082
|
+
import { readFileSync as readFileSync8, statSync as statSync2, existsSync as existsSync8, openSync, readSync, closeSync } from "fs";
|
|
6083
|
+
import { join as join7 } from "path";
|
|
6084
|
+
import { hostname as hostname2 } from "os";
|
|
6085
|
+
|
|
5090
6086
|
// src/watermark.ts
|
|
5091
|
-
import { readFileSync as
|
|
6087
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
|
|
5092
6088
|
function defaultFileData() {
|
|
5093
6089
|
return {
|
|
5094
6090
|
"claude-code": {},
|
|
@@ -5099,6 +6095,7 @@ function defaultFileData() {
|
|
|
5099
6095
|
"qoder": {},
|
|
5100
6096
|
"cursor": {},
|
|
5101
6097
|
"kilocode": {},
|
|
6098
|
+
"kelivo": {},
|
|
5102
6099
|
"copilot": {},
|
|
5103
6100
|
"gemini": {},
|
|
5104
6101
|
"kimi": {},
|
|
@@ -5118,8 +6115,8 @@ function defaultFileData() {
|
|
|
5118
6115
|
var WatermarkManager = class {
|
|
5119
6116
|
data;
|
|
5120
6117
|
path;
|
|
5121
|
-
constructor(
|
|
5122
|
-
this.path =
|
|
6118
|
+
constructor(path3) {
|
|
6119
|
+
this.path = path3;
|
|
5123
6120
|
this.data = this.load();
|
|
5124
6121
|
}
|
|
5125
6122
|
load() {
|
|
@@ -5127,7 +6124,7 @@ var WatermarkManager = class {
|
|
|
5127
6124
|
return { files: defaultFileData() };
|
|
5128
6125
|
}
|
|
5129
6126
|
try {
|
|
5130
|
-
const content =
|
|
6127
|
+
const content = readFileSync7(this.path, "utf-8");
|
|
5131
6128
|
const parsed = JSON.parse(content);
|
|
5132
6129
|
if (parsed && typeof parsed === "object" && !("files" in parsed)) {
|
|
5133
6130
|
return { files: { ...defaultFileData(), ...parsed } };
|
|
@@ -5138,7 +6135,7 @@ var WatermarkManager = class {
|
|
|
5138
6135
|
}
|
|
5139
6136
|
}
|
|
5140
6137
|
save() {
|
|
5141
|
-
|
|
6138
|
+
writeFileSync5(this.path, JSON.stringify(this.data, null, 2), "utf-8");
|
|
5142
6139
|
}
|
|
5143
6140
|
getEntry(tool, filePath) {
|
|
5144
6141
|
return this.data.files[tool]?.[filePath] ?? null;
|
|
@@ -6011,6 +7008,11 @@ function extractSessionId2(filePath, tool) {
|
|
|
6011
7008
|
const parts = filePath.split("/");
|
|
6012
7009
|
return (parts.pop() ?? "").replace(".jsonl", "");
|
|
6013
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
|
+
}
|
|
6014
7016
|
return "unknown";
|
|
6015
7017
|
}
|
|
6016
7018
|
function toNumber2(value) {
|
|
@@ -6023,12 +7025,12 @@ function parseUiMessagesFile(options) {
|
|
|
6023
7025
|
const errors = [];
|
|
6024
7026
|
let messages;
|
|
6025
7027
|
try {
|
|
6026
|
-
const parsed = JSON.parse(
|
|
7028
|
+
const parsed = JSON.parse(readFileSync8(filePath, "utf-8"));
|
|
6027
7029
|
messages = Array.isArray(parsed) ? parsed : [];
|
|
6028
7030
|
} catch (e) {
|
|
6029
7031
|
return { records, errors: [`${filePath}: ${e instanceof Error ? e.message : e}`] };
|
|
6030
7032
|
}
|
|
6031
|
-
const taskId = filePath.split("/").slice(-2, -1)[0] || "unknown";
|
|
7033
|
+
const taskId = filePath.replace(/\\/g, "/").split("/").slice(-2, -1)[0] || "unknown";
|
|
6032
7034
|
for (const [index, message] of messages.entries()) {
|
|
6033
7035
|
if (message?.say !== "api_req_started" || typeof message.text !== "string") continue;
|
|
6034
7036
|
let payload;
|
|
@@ -6080,7 +7082,7 @@ function normalizeKiroModel(value) {
|
|
|
6080
7082
|
if (typeof value !== "string" || !value.trim()) return "kiro-agent";
|
|
6081
7083
|
return value.trim().toLowerCase().replace(/_/g, "-").replace(/^(anthropic\.|openai\.|aws\.)/, "").replace(/:\d+$/, "").replace(/-\d{8}-v\d+(?:-\d+)?$/i, "");
|
|
6082
7084
|
}
|
|
6083
|
-
function
|
|
7085
|
+
function parseTimestamp2(value, fallback) {
|
|
6084
7086
|
if (typeof value === "number" && Number.isFinite(value)) return value < 1e12 ? value * 1e3 : value;
|
|
6085
7087
|
if (typeof value === "string" && value.trim()) {
|
|
6086
7088
|
const parsed = new Date(value).getTime();
|
|
@@ -6101,7 +7103,7 @@ function parseKiroSessionFile(options) {
|
|
|
6101
7103
|
const errors = [];
|
|
6102
7104
|
let parsed;
|
|
6103
7105
|
try {
|
|
6104
|
-
parsed = JSON.parse(
|
|
7106
|
+
parsed = JSON.parse(readFileSync8(filePath, "utf-8"));
|
|
6105
7107
|
} catch (e) {
|
|
6106
7108
|
return { records, errors: [`${filePath}: ${e instanceof Error ? e.message : e}`] };
|
|
6107
7109
|
}
|
|
@@ -6114,7 +7116,7 @@ function parseKiroSessionFile(options) {
|
|
|
6114
7116
|
const outputTokens = toNumber2(turn?.output_token_count);
|
|
6115
7117
|
if (inputTokens + outputTokens === 0) continue;
|
|
6116
7118
|
const model = normalizeKiroModel(turn?.model_id ?? sessionModel);
|
|
6117
|
-
const recordTs =
|
|
7119
|
+
const recordTs = parseTimestamp2(
|
|
6118
7120
|
turn?.request_start_timestamp_ms ?? turn?.start_timestamp ?? turn?.end_timestamp,
|
|
6119
7121
|
now
|
|
6120
7122
|
);
|
|
@@ -6225,7 +7227,30 @@ async function runParse(db, filterTool, options) {
|
|
|
6225
7227
|
onProgress({ phase: "Parsing logs", tool, current: fileIndex, total: totalFiles, records: parsedCount, toolCalls: toolCallCount });
|
|
6226
7228
|
continue;
|
|
6227
7229
|
}
|
|
6228
|
-
|
|
7230
|
+
if (tool === "kelivo" && (filePath.endsWith("chats.json") || filePath.endsWith(".zip"))) {
|
|
7231
|
+
const result = await runParseKelivo({
|
|
7232
|
+
filePath,
|
|
7233
|
+
device,
|
|
7234
|
+
deviceInstanceId,
|
|
7235
|
+
platform: devicePlatform,
|
|
7236
|
+
now: Date.now(),
|
|
7237
|
+
exchangeRate
|
|
7238
|
+
});
|
|
7239
|
+
for (const record of result.records) {
|
|
7240
|
+
insertRecord(db, record);
|
|
7241
|
+
parsedCount++;
|
|
7242
|
+
}
|
|
7243
|
+
errors.push(...result.errors);
|
|
7244
|
+
wm.setEntry(tool, filePath, {
|
|
7245
|
+
offset: stat2.size,
|
|
7246
|
+
size: stat2.size,
|
|
7247
|
+
mtime: stat2.mtimeMs
|
|
7248
|
+
});
|
|
7249
|
+
wm.save();
|
|
7250
|
+
onProgress({ phase: "Parsing logs", tool, current: fileIndex, total: totalFiles, records: parsedCount, toolCalls: toolCallCount });
|
|
7251
|
+
continue;
|
|
7252
|
+
}
|
|
7253
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
6229
7254
|
const lines = content.split("\n");
|
|
6230
7255
|
let byteOffset = 0;
|
|
6231
7256
|
let fileCwd;
|
|
@@ -6680,7 +7705,7 @@ function backfillCodexModels(db) {
|
|
|
6680
7705
|
);
|
|
6681
7706
|
for (const [sourceFile, fileRows] of byFile) {
|
|
6682
7707
|
try {
|
|
6683
|
-
const content =
|
|
7708
|
+
const content = readFileSync8(sourceFile, "utf-8");
|
|
6684
7709
|
const lines = content.split("\n");
|
|
6685
7710
|
const offsetToModel = /* @__PURE__ */ new Map();
|
|
6686
7711
|
let currentModel = "";
|
|
@@ -6728,7 +7753,7 @@ function backfillMissingToolCalls(db, exchangeRate) {
|
|
|
6728
7753
|
}
|
|
6729
7754
|
for (const [sourceFile, fileRows] of rowsByFile) {
|
|
6730
7755
|
try {
|
|
6731
|
-
const content =
|
|
7756
|
+
const content = readFileSync8(sourceFile, "utf-8");
|
|
6732
7757
|
const lines = content.split("\n");
|
|
6733
7758
|
const aggregator = new Aggregator();
|
|
6734
7759
|
const recordIdByOffset = /* @__PURE__ */ new Map();
|
|
@@ -6794,7 +7819,7 @@ import { join as join9 } from "path";
|
|
|
6794
7819
|
|
|
6795
7820
|
// src/db/synced-records.ts
|
|
6796
7821
|
function insertSyncedRecord(db, record) {
|
|
6797
|
-
db.prepare(`
|
|
7822
|
+
const result = db.prepare(`
|
|
6798
7823
|
INSERT INTO synced_records (
|
|
6799
7824
|
id, ts, tool, model, provider, input_tokens, output_tokens,
|
|
6800
7825
|
cache_read_tokens, cache_write_tokens, thinking_tokens,
|
|
@@ -6847,6 +7872,7 @@ function insertSyncedRecord(db, record) {
|
|
|
6847
7872
|
sourceFile: record.sourceFile ?? "",
|
|
6848
7873
|
cwd: record.cwd ?? ""
|
|
6849
7874
|
});
|
|
7875
|
+
return result.changes > 0;
|
|
6850
7876
|
}
|
|
6851
7877
|
function mergeSyncedRecordsIntoRecords(db) {
|
|
6852
7878
|
const now = Date.now();
|
|
@@ -6907,6 +7933,7 @@ var RECORD_ID_SYNC_TOOLS = /* @__PURE__ */ new Set([
|
|
|
6907
7933
|
"qoder",
|
|
6908
7934
|
"cursor",
|
|
6909
7935
|
"kilocode",
|
|
7936
|
+
"kelivo",
|
|
6910
7937
|
"goose",
|
|
6911
7938
|
"zed",
|
|
6912
7939
|
"kiro",
|
|
@@ -7015,15 +8042,15 @@ var SyncOrchestrator = class {
|
|
|
7015
8042
|
});
|
|
7016
8043
|
if (paths.length === 0) return 0;
|
|
7017
8044
|
let totalPulled = 0;
|
|
7018
|
-
for (const [index,
|
|
8045
|
+
for (const [index, path3] of paths.entries()) {
|
|
7019
8046
|
this.options.onProgress?.({
|
|
7020
8047
|
phase: "pulling",
|
|
7021
|
-
currentPath:
|
|
8048
|
+
currentPath: path3,
|
|
7022
8049
|
completedFiles: index,
|
|
7023
8050
|
totalFiles: paths.length,
|
|
7024
8051
|
pulledCount: totalPulled
|
|
7025
8052
|
});
|
|
7026
|
-
const content = await this.backend.readFile(
|
|
8053
|
+
const content = await this.backend.readFile(path3);
|
|
7027
8054
|
if (!content) continue;
|
|
7028
8055
|
for (const line of content.split("\n").filter(Boolean)) {
|
|
7029
8056
|
try {
|
|
@@ -7034,14 +8061,14 @@ var SyncOrchestrator = class {
|
|
|
7034
8061
|
if (typeof record.updatedAt === "string") {
|
|
7035
8062
|
record.updatedAt = new Date(record.updatedAt).getTime();
|
|
7036
8063
|
}
|
|
7037
|
-
insertSyncedRecord(this.db, record);
|
|
7038
|
-
totalPulled++;
|
|
8064
|
+
const changed = insertSyncedRecord(this.db, record);
|
|
8065
|
+
if (changed) totalPulled++;
|
|
7039
8066
|
} catch {
|
|
7040
8067
|
}
|
|
7041
8068
|
}
|
|
7042
8069
|
this.options.onProgress?.({
|
|
7043
8070
|
phase: "pulling",
|
|
7044
|
-
currentPath:
|
|
8071
|
+
currentPath: path3,
|
|
7045
8072
|
completedFiles: index + 1,
|
|
7046
8073
|
totalFiles: paths.length,
|
|
7047
8074
|
pulledCount: totalPulled
|
|
@@ -7049,13 +8076,13 @@ var SyncOrchestrator = class {
|
|
|
7049
8076
|
}
|
|
7050
8077
|
return totalPulled;
|
|
7051
8078
|
}
|
|
7052
|
-
async uploadFile(
|
|
7053
|
-
const existingContent = await this.backend.readFile(
|
|
8079
|
+
async uploadFile(path3, localSyncRecords) {
|
|
8080
|
+
const existingContent = await this.backend.readFile(path3);
|
|
7054
8081
|
const existing = existingContent ? parseNdjson(existingContent) : /* @__PURE__ */ new Map();
|
|
7055
8082
|
const changedCount = mergeRecords(existing, localSyncRecords);
|
|
7056
8083
|
if (changedCount === 0) return 0;
|
|
7057
8084
|
const content = Array.from(existing.values()).map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
7058
|
-
await this.backend.writeFile(
|
|
8085
|
+
await this.backend.writeFile(path3, content);
|
|
7059
8086
|
return changedCount;
|
|
7060
8087
|
}
|
|
7061
8088
|
async upload() {
|
|
@@ -7063,9 +8090,9 @@ var SyncOrchestrator = class {
|
|
|
7063
8090
|
if (unsynced.length === 0) return 0;
|
|
7064
8091
|
const byPath = /* @__PURE__ */ new Map();
|
|
7065
8092
|
for (const record of unsynced) {
|
|
7066
|
-
const
|
|
7067
|
-
if (!byPath.has(
|
|
7068
|
-
byPath.get(
|
|
8093
|
+
const path3 = getSyncPath(record.ts, this.options.deviceInstanceId);
|
|
8094
|
+
if (!byPath.has(path3)) byPath.set(path3, []);
|
|
8095
|
+
byPath.get(path3).push(record);
|
|
7069
8096
|
}
|
|
7070
8097
|
let totalUploaded = 0;
|
|
7071
8098
|
const uploads = Array.from(byPath.entries());
|
|
@@ -7075,20 +8102,20 @@ var SyncOrchestrator = class {
|
|
|
7075
8102
|
totalFiles: uploads.length,
|
|
7076
8103
|
uploadedCount: 0
|
|
7077
8104
|
});
|
|
7078
|
-
for (const [index, [
|
|
8105
|
+
for (const [index, [path3, localRecords]] of uploads.entries()) {
|
|
7079
8106
|
this.options.onProgress?.({
|
|
7080
8107
|
phase: "uploading",
|
|
7081
|
-
currentPath:
|
|
8108
|
+
currentPath: path3,
|
|
7082
8109
|
completedFiles: index,
|
|
7083
8110
|
totalFiles: uploads.length,
|
|
7084
8111
|
uploadedCount: totalUploaded
|
|
7085
8112
|
});
|
|
7086
8113
|
const localSyncRecords = localRecords.map(mapStatsRecordToSyncRecord);
|
|
7087
|
-
await this.uploadFile(
|
|
8114
|
+
await this.uploadFile(path3, localSyncRecords);
|
|
7088
8115
|
totalUploaded += localRecords.length;
|
|
7089
8116
|
this.options.onProgress?.({
|
|
7090
8117
|
phase: "uploading",
|
|
7091
|
-
currentPath:
|
|
8118
|
+
currentPath: path3,
|
|
7092
8119
|
completedFiles: index + 1,
|
|
7093
8120
|
totalFiles: uploads.length,
|
|
7094
8121
|
uploadedCount: totalUploaded
|
|
@@ -7113,14 +8140,14 @@ var CloudSyncError = class extends Error {
|
|
|
7113
8140
|
code;
|
|
7114
8141
|
retryAfter;
|
|
7115
8142
|
};
|
|
7116
|
-
function buildHeaders(method,
|
|
8143
|
+
function buildHeaders(method, path3, body) {
|
|
7117
8144
|
const creds = loadCredentials();
|
|
7118
8145
|
if (!creds) throw new CloudSyncError("Not logged in. Run `aiusage login` first.", "not_logged_in");
|
|
7119
8146
|
const bodyHash = sha256(body);
|
|
7120
8147
|
const timestamp = Date.now().toString();
|
|
7121
8148
|
const nonce = generateNonce();
|
|
7122
8149
|
const idempotencyKey = generateIdempotencyKey();
|
|
7123
|
-
const canonical = buildCanonicalString(method,
|
|
8150
|
+
const canonical = buildCanonicalString(method, path3, bodyHash, timestamp, nonce, creds.device_id, idempotencyKey);
|
|
7124
8151
|
const signature = computeHmac(creds.device_secret, canonical);
|
|
7125
8152
|
return {
|
|
7126
8153
|
"Content-Type": "application/json",
|
|
@@ -7179,9 +8206,9 @@ async function cloudPull(cursor, limit = 1e3) {
|
|
|
7179
8206
|
if (cursor) params.set("cursor", cursor);
|
|
7180
8207
|
params.set("limit", String(limit));
|
|
7181
8208
|
const query = params.toString();
|
|
7182
|
-
const
|
|
8209
|
+
const path3 = `${SYNC_PULL_PATH}${query ? `?${query}` : ""}`;
|
|
7183
8210
|
const headers = buildHeaders("GET", SYNC_PULL_PATH, "");
|
|
7184
|
-
const response = await fetch(`${serverUrl}${
|
|
8211
|
+
const response = await fetch(`${serverUrl}${path3}`, { method: "GET", headers });
|
|
7185
8212
|
const data = await readJsonOrNull2(response);
|
|
7186
8213
|
if (!response.ok) {
|
|
7187
8214
|
const errObj = data?.error;
|
|
@@ -7198,12 +8225,32 @@ async function cloudPull(cursor, limit = 1e3) {
|
|
|
7198
8225
|
syncGeneration: data.sync_generation || 1
|
|
7199
8226
|
};
|
|
7200
8227
|
}
|
|
8228
|
+
async function cloudClear() {
|
|
8229
|
+
const serverUrl = getSiteUrl();
|
|
8230
|
+
const creds = loadCredentials();
|
|
8231
|
+
if (!creds) throw new CloudSyncError("Not logged in.", "not_logged_in");
|
|
8232
|
+
const SYNC_CLEAR_PATH = "/api/cli/sync/clear";
|
|
8233
|
+
const body = "{}";
|
|
8234
|
+
const headers = buildHeaders("POST", SYNC_CLEAR_PATH, body);
|
|
8235
|
+
const response = await fetch(`${serverUrl}${SYNC_CLEAR_PATH}`, { method: "POST", headers, body });
|
|
8236
|
+
const data = await readJsonOrNull2(response);
|
|
8237
|
+
if (!response.ok) {
|
|
8238
|
+
const errObj = data?.error;
|
|
8239
|
+
const errMsg = typeof errObj === "object" && errObj !== null ? errObj.message || `Clear failed (HTTP ${response.status})` : errObj || `Clear failed (HTTP ${response.status})`;
|
|
8240
|
+
const errCode = typeof errObj === "object" && errObj !== null ? errObj.code || "server_error" : data?.error_code || "server_error";
|
|
8241
|
+
throw new CloudSyncError(errMsg, errCode);
|
|
8242
|
+
}
|
|
8243
|
+
if (!data) throw new CloudSyncError("Invalid response from server", "invalid_response");
|
|
8244
|
+
return {
|
|
8245
|
+
syncGeneration: data.sync_generation || 1
|
|
8246
|
+
};
|
|
8247
|
+
}
|
|
7201
8248
|
function getClientVersion() {
|
|
7202
8249
|
try {
|
|
7203
|
-
const { readFileSync:
|
|
8250
|
+
const { readFileSync: readFileSync12 } = __require("fs");
|
|
7204
8251
|
const { join: join16 } = __require("path");
|
|
7205
8252
|
const pkgPath = join16(__dirname, "../../package.json");
|
|
7206
|
-
const pkg = JSON.parse(
|
|
8253
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
7207
8254
|
return pkg.version || "0.0.0";
|
|
7208
8255
|
} catch {
|
|
7209
8256
|
return "0.0.0";
|
|
@@ -7293,7 +8340,7 @@ var CloudSyncOrchestrator = class {
|
|
|
7293
8340
|
// src/sync/git.ts
|
|
7294
8341
|
import { execFile } from "child_process";
|
|
7295
8342
|
import { promisify } from "util";
|
|
7296
|
-
import { readFile, writeFile, mkdir, readdir, stat } from "fs/promises";
|
|
8343
|
+
import { readFile, writeFile, mkdir, readdir, stat, unlink, rm } from "fs/promises";
|
|
7297
8344
|
import { join as join8 } from "path";
|
|
7298
8345
|
var exec = promisify(execFile);
|
|
7299
8346
|
var GitSyncBackend = class _GitSyncBackend {
|
|
@@ -7353,16 +8400,16 @@ var GitSyncBackend = class _GitSyncBackend {
|
|
|
7353
8400
|
}
|
|
7354
8401
|
}
|
|
7355
8402
|
}
|
|
7356
|
-
async readFile(
|
|
8403
|
+
async readFile(path3) {
|
|
7357
8404
|
try {
|
|
7358
|
-
const fullPath = join8(this.dataDir,
|
|
8405
|
+
const fullPath = join8(this.dataDir, path3);
|
|
7359
8406
|
return await readFile(fullPath, "utf-8");
|
|
7360
8407
|
} catch {
|
|
7361
8408
|
return null;
|
|
7362
8409
|
}
|
|
7363
8410
|
}
|
|
7364
|
-
async writeFile(
|
|
7365
|
-
const fullPath = join8(this.dataDir,
|
|
8411
|
+
async writeFile(path3, content) {
|
|
8412
|
+
const fullPath = join8(this.dataDir, path3);
|
|
7366
8413
|
await mkdir(join8(fullPath, ".."), { recursive: true });
|
|
7367
8414
|
await writeFile(fullPath, content, "utf-8");
|
|
7368
8415
|
}
|
|
@@ -7373,6 +8420,22 @@ var GitSyncBackend = class _GitSyncBackend {
|
|
|
7373
8420
|
return [];
|
|
7374
8421
|
}
|
|
7375
8422
|
}
|
|
8423
|
+
async deleteFile(path3) {
|
|
8424
|
+
const fullPath = join8(this.dataDir, path3);
|
|
8425
|
+
try {
|
|
8426
|
+
await unlink(fullPath);
|
|
8427
|
+
} catch {
|
|
8428
|
+
}
|
|
8429
|
+
}
|
|
8430
|
+
async deleteAllData() {
|
|
8431
|
+
try {
|
|
8432
|
+
const files = await this.listFiles();
|
|
8433
|
+
await rm(this.dataDir, { recursive: true, force: true });
|
|
8434
|
+
return files.length;
|
|
8435
|
+
} catch {
|
|
8436
|
+
return 0;
|
|
8437
|
+
}
|
|
8438
|
+
}
|
|
7376
8439
|
static MAX_PUSH_RETRIES = 3;
|
|
7377
8440
|
/** Commit and push all changes. Returns true if anything was pushed. */
|
|
7378
8441
|
async flush() {
|
|
@@ -7407,7 +8470,7 @@ var GitSyncBackend = class _GitSyncBackend {
|
|
|
7407
8470
|
};
|
|
7408
8471
|
|
|
7409
8472
|
// src/sync/s3.ts
|
|
7410
|
-
import { S3Client, GetObjectCommand, PutObjectCommand, HeadObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
|
|
8473
|
+
import { S3Client, GetObjectCommand, PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, DeleteObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
7411
8474
|
var S3SyncBackend = class {
|
|
7412
8475
|
client;
|
|
7413
8476
|
bucket;
|
|
@@ -7424,11 +8487,11 @@ var S3SyncBackend = class {
|
|
|
7424
8487
|
}
|
|
7425
8488
|
});
|
|
7426
8489
|
}
|
|
7427
|
-
getObjectKey(
|
|
7428
|
-
return `${this.prefix}${
|
|
8490
|
+
getObjectKey(path3) {
|
|
8491
|
+
return `${this.prefix}${path3}`;
|
|
7429
8492
|
}
|
|
7430
|
-
async readFile(
|
|
7431
|
-
const key = this.getObjectKey(
|
|
8493
|
+
async readFile(path3) {
|
|
8494
|
+
const key = this.getObjectKey(path3);
|
|
7432
8495
|
try {
|
|
7433
8496
|
const command = new GetObjectCommand({
|
|
7434
8497
|
Bucket: this.bucket,
|
|
@@ -7444,8 +8507,8 @@ var S3SyncBackend = class {
|
|
|
7444
8507
|
throw error;
|
|
7445
8508
|
}
|
|
7446
8509
|
}
|
|
7447
|
-
async writeFile(
|
|
7448
|
-
const key = this.getObjectKey(
|
|
8510
|
+
async writeFile(path3, content) {
|
|
8511
|
+
const key = this.getObjectKey(path3);
|
|
7449
8512
|
const command = new PutObjectCommand({
|
|
7450
8513
|
Bucket: this.bucket,
|
|
7451
8514
|
Key: key,
|
|
@@ -7477,8 +8540,8 @@ var S3SyncBackend = class {
|
|
|
7477
8540
|
} while (continuationToken);
|
|
7478
8541
|
return files.sort();
|
|
7479
8542
|
}
|
|
7480
|
-
async fileExists(
|
|
7481
|
-
const key = this.getObjectKey(
|
|
8543
|
+
async fileExists(path3) {
|
|
8544
|
+
const key = this.getObjectKey(path3);
|
|
7482
8545
|
try {
|
|
7483
8546
|
const command = new HeadObjectCommand({
|
|
7484
8547
|
Bucket: this.bucket,
|
|
@@ -7493,6 +8556,31 @@ var S3SyncBackend = class {
|
|
|
7493
8556
|
throw error;
|
|
7494
8557
|
}
|
|
7495
8558
|
}
|
|
8559
|
+
async deleteFile(path3) {
|
|
8560
|
+
const key = this.getObjectKey(path3);
|
|
8561
|
+
const command = new DeleteObjectCommand({
|
|
8562
|
+
Bucket: this.bucket,
|
|
8563
|
+
Key: key
|
|
8564
|
+
});
|
|
8565
|
+
await this.client.send(command);
|
|
8566
|
+
}
|
|
8567
|
+
async deleteAllData() {
|
|
8568
|
+
const files = await this.listFiles();
|
|
8569
|
+
if (files.length === 0) return 0;
|
|
8570
|
+
const BATCH_SIZE2 = 1e3;
|
|
8571
|
+
for (let i = 0; i < files.length; i += BATCH_SIZE2) {
|
|
8572
|
+
const batch = files.slice(i, i + BATCH_SIZE2);
|
|
8573
|
+
const command = new DeleteObjectsCommand({
|
|
8574
|
+
Bucket: this.bucket,
|
|
8575
|
+
Delete: {
|
|
8576
|
+
Objects: batch.map((f) => ({ Key: this.getObjectKey(f) })),
|
|
8577
|
+
Quiet: true
|
|
8578
|
+
}
|
|
8579
|
+
});
|
|
8580
|
+
await this.client.send(command);
|
|
8581
|
+
}
|
|
8582
|
+
return files.length;
|
|
8583
|
+
}
|
|
7496
8584
|
};
|
|
7497
8585
|
|
|
7498
8586
|
// src/commands/sync.ts
|
|
@@ -7601,6 +8689,8 @@ async function runSync(db, options) {
|
|
|
7601
8689
|
}
|
|
7602
8690
|
|
|
7603
8691
|
// src/commands/clean.ts
|
|
8692
|
+
import { unlinkSync as unlinkSync2, existsSync as existsSync9 } from "fs";
|
|
8693
|
+
import { join as join10 } from "path";
|
|
7604
8694
|
function cleanOldData(db, days) {
|
|
7605
8695
|
const cutoff = Date.now() - days * 864e5;
|
|
7606
8696
|
const recordsResult = db.prepare("DELETE FROM records WHERE ts < ?").run(cutoff);
|
|
@@ -7615,6 +8705,172 @@ function cleanOldData(db, days) {
|
|
|
7615
8705
|
deletedOrphanToolCalls
|
|
7616
8706
|
};
|
|
7617
8707
|
}
|
|
8708
|
+
function cleanAll(db) {
|
|
8709
|
+
const recordsResult = db.prepare("DELETE FROM records").run();
|
|
8710
|
+
const toolCallsResult = db.prepare("DELETE FROM tool_calls").run();
|
|
8711
|
+
const syncedResult = db.prepare("DELETE FROM synced_records").run();
|
|
8712
|
+
const syncStateResult = db.prepare("DELETE FROM sync_record_state").run();
|
|
8713
|
+
const tombstonesResult = db.prepare("DELETE FROM sync_tombstones").run();
|
|
8714
|
+
const watermarkPath = join10(AIUSAGE_DIR, "watermark.json");
|
|
8715
|
+
let watermarkRemoved = false;
|
|
8716
|
+
if (existsSync9(watermarkPath)) {
|
|
8717
|
+
unlinkSync2(watermarkPath);
|
|
8718
|
+
watermarkRemoved = true;
|
|
8719
|
+
}
|
|
8720
|
+
return {
|
|
8721
|
+
deletedRecords: recordsResult.changes,
|
|
8722
|
+
deletedToolCalls: toolCallsResult.changes,
|
|
8723
|
+
deletedSyncedRecords: syncedResult.changes,
|
|
8724
|
+
deletedSyncRecordState: syncStateResult.changes,
|
|
8725
|
+
deletedTombstones: tombstonesResult.changes,
|
|
8726
|
+
watermarkRemoved
|
|
8727
|
+
};
|
|
8728
|
+
}
|
|
8729
|
+
function getRemoteBackends() {
|
|
8730
|
+
const config = loadConfig();
|
|
8731
|
+
if (!config?.sync) return [];
|
|
8732
|
+
const backends = [];
|
|
8733
|
+
if (config.sync.backend === "cloud") {
|
|
8734
|
+
backends.push({ type: "cloud", label: "AIUsage Cloud (cloud)" });
|
|
8735
|
+
} else if (config.sync.backend === "github" && config.sync.repo) {
|
|
8736
|
+
backends.push({ type: "github", label: `GitHub (${config.sync.repo})` });
|
|
8737
|
+
} else if (config.sync.backend === "s3" && config.sync.bucket) {
|
|
8738
|
+
backends.push({ type: "s3", label: `S3 (${config.sync.bucket})` });
|
|
8739
|
+
}
|
|
8740
|
+
return backends;
|
|
8741
|
+
}
|
|
8742
|
+
function createBackend2(config) {
|
|
8743
|
+
if (!config?.sync) return null;
|
|
8744
|
+
if (config.sync.backend === "github") {
|
|
8745
|
+
if (!config.sync.repo) return null;
|
|
8746
|
+
const token = loadCredential(`github/${config.sync.repo}/token`);
|
|
8747
|
+
if (!token) return null;
|
|
8748
|
+
return new GitSyncBackend({
|
|
8749
|
+
repo: config.sync.repo,
|
|
8750
|
+
token,
|
|
8751
|
+
cacheDir: join10(AIUSAGE_DIR, "sync-repo")
|
|
8752
|
+
});
|
|
8753
|
+
}
|
|
8754
|
+
if (config.sync.backend === "s3") {
|
|
8755
|
+
if (!config.sync.bucket) return null;
|
|
8756
|
+
const accessKeyId = loadCredential(`s3/${config.sync.bucket}/accessKeyId`);
|
|
8757
|
+
const secretAccessKey = loadCredential(`s3/${config.sync.bucket}/secretAccessKey`);
|
|
8758
|
+
if (!accessKeyId || !secretAccessKey) return null;
|
|
8759
|
+
return new S3SyncBackend({
|
|
8760
|
+
bucket: config.sync.bucket,
|
|
8761
|
+
prefix: config.sync.prefix ?? "aiusage/",
|
|
8762
|
+
accessKeyId,
|
|
8763
|
+
secretAccessKey,
|
|
8764
|
+
endpoint: config.sync.endpoint,
|
|
8765
|
+
region: config.sync.region
|
|
8766
|
+
});
|
|
8767
|
+
}
|
|
8768
|
+
return null;
|
|
8769
|
+
}
|
|
8770
|
+
async function propagateClean(options) {
|
|
8771
|
+
const config = loadConfig();
|
|
8772
|
+
if (!config?.sync) return { backends: [] };
|
|
8773
|
+
const results = [];
|
|
8774
|
+
if (config.sync.backend === "cloud") {
|
|
8775
|
+
if (!options.target || options.target === "cloud") {
|
|
8776
|
+
try {
|
|
8777
|
+
const result = await cloudClear();
|
|
8778
|
+
results.push({
|
|
8779
|
+
backend: { type: "cloud", label: "AIUsage Cloud" },
|
|
8780
|
+
status: "ok",
|
|
8781
|
+
detail: `generation updated to ${result.syncGeneration}`
|
|
8782
|
+
});
|
|
8783
|
+
} catch (err) {
|
|
8784
|
+
results.push({
|
|
8785
|
+
backend: { type: "cloud", label: "AIUsage Cloud" },
|
|
8786
|
+
status: "skipped",
|
|
8787
|
+
detail: err instanceof Error ? err.message : "unknown error"
|
|
8788
|
+
});
|
|
8789
|
+
}
|
|
8790
|
+
}
|
|
8791
|
+
return { backends: results };
|
|
8792
|
+
}
|
|
8793
|
+
if (options.target && options.target !== config.sync.backend) {
|
|
8794
|
+
return { backends: results };
|
|
8795
|
+
}
|
|
8796
|
+
const backend = createBackend2(config);
|
|
8797
|
+
if (!backend) {
|
|
8798
|
+
return { backends: [{
|
|
8799
|
+
backend: { type: config.sync.backend, label: config.sync.backend },
|
|
8800
|
+
status: "skipped",
|
|
8801
|
+
detail: "Could not create backend (missing credentials)"
|
|
8802
|
+
}] };
|
|
8803
|
+
}
|
|
8804
|
+
try {
|
|
8805
|
+
await backend.prepare?.();
|
|
8806
|
+
if (options.all) {
|
|
8807
|
+
const files = await backend.listFiles();
|
|
8808
|
+
const fileCount = files.length;
|
|
8809
|
+
if (fileCount > 0) {
|
|
8810
|
+
await backend.deleteAllData();
|
|
8811
|
+
}
|
|
8812
|
+
await backend.flush?.();
|
|
8813
|
+
results.push({
|
|
8814
|
+
backend: {
|
|
8815
|
+
type: config.sync.backend,
|
|
8816
|
+
label: config.sync.backend === "github" ? `GitHub (${config.sync.repo})` : `S3 (${config.sync.bucket})`
|
|
8817
|
+
},
|
|
8818
|
+
status: "ok",
|
|
8819
|
+
detail: `cleared ${fileCount} files`
|
|
8820
|
+
});
|
|
8821
|
+
} else if (options.beforeDays) {
|
|
8822
|
+
const cutoff = Date.now() - options.beforeDays * 864e5;
|
|
8823
|
+
const files = await backend.listFiles();
|
|
8824
|
+
let removedRecords = 0;
|
|
8825
|
+
let modifiedFiles = 0;
|
|
8826
|
+
for (const file of files) {
|
|
8827
|
+
const content = await backend.readFile(file);
|
|
8828
|
+
if (!content) continue;
|
|
8829
|
+
const lines = content.split("\n").filter(Boolean);
|
|
8830
|
+
const kept = [];
|
|
8831
|
+
for (const line of lines) {
|
|
8832
|
+
try {
|
|
8833
|
+
const record = JSON.parse(line);
|
|
8834
|
+
const ts3 = typeof record.ts === "string" ? new Date(record.ts).getTime() : record.ts;
|
|
8835
|
+
if (ts3 >= cutoff) {
|
|
8836
|
+
kept.push(line);
|
|
8837
|
+
} else {
|
|
8838
|
+
removedRecords++;
|
|
8839
|
+
}
|
|
8840
|
+
} catch {
|
|
8841
|
+
kept.push(line);
|
|
8842
|
+
}
|
|
8843
|
+
}
|
|
8844
|
+
if (kept.length === 0) {
|
|
8845
|
+
await backend.deleteFile?.(file);
|
|
8846
|
+
modifiedFiles++;
|
|
8847
|
+
} else if (kept.length < lines.length) {
|
|
8848
|
+
await backend.writeFile(file, kept.join("\n") + "\n");
|
|
8849
|
+
modifiedFiles++;
|
|
8850
|
+
}
|
|
8851
|
+
}
|
|
8852
|
+
await backend.flush?.();
|
|
8853
|
+
results.push({
|
|
8854
|
+
backend: {
|
|
8855
|
+
type: config.sync.backend,
|
|
8856
|
+
label: config.sync.backend === "github" ? `GitHub (${config.sync.repo})` : `S3 (${config.sync.bucket})`
|
|
8857
|
+
},
|
|
8858
|
+
status: "ok",
|
|
8859
|
+
detail: `removed ${removedRecords} records from ${modifiedFiles} files`
|
|
8860
|
+
});
|
|
8861
|
+
}
|
|
8862
|
+
} catch (err) {
|
|
8863
|
+
results.push({
|
|
8864
|
+
backend: {
|
|
8865
|
+
type: config.sync.backend,
|
|
8866
|
+
label: config.sync.backend === "github" ? `GitHub (${config.sync.repo})` : `S3 (${config.sync.bucket})`
|
|
8867
|
+
},
|
|
8868
|
+
status: "skipped",
|
|
8869
|
+
detail: err instanceof Error ? err.message : "unknown error"
|
|
8870
|
+
});
|
|
8871
|
+
}
|
|
8872
|
+
return { backends: results };
|
|
8873
|
+
}
|
|
7618
8874
|
|
|
7619
8875
|
// src/sync/runtime.ts
|
|
7620
8876
|
var SyncRuntimeController = class {
|
|
@@ -7712,6 +8968,7 @@ var RuntimeSettingsController = class {
|
|
|
7712
8968
|
leaderboardUploadTimer = null;
|
|
7713
8969
|
syncTimer = null;
|
|
7714
8970
|
parseInFlight = false;
|
|
8971
|
+
cleanupInFlight = false;
|
|
7715
8972
|
leaderboardUploadInFlight = false;
|
|
7716
8973
|
started = false;
|
|
7717
8974
|
constructor(options) {
|
|
@@ -7764,11 +9021,7 @@ var RuntimeSettingsController = class {
|
|
|
7764
9021
|
}
|
|
7765
9022
|
if (retentionDays > 0) {
|
|
7766
9023
|
this.cleanupTimer = setInterval(() => {
|
|
7767
|
-
|
|
7768
|
-
this.runCleanupFn(this.db, retentionDays);
|
|
7769
|
-
} catch (err) {
|
|
7770
|
-
console.error("[settings-controller] cleanup failed:", err);
|
|
7771
|
-
}
|
|
9024
|
+
void this.runCleanupSafely(retentionDays);
|
|
7772
9025
|
}, this.cleanupIntervalMs);
|
|
7773
9026
|
}
|
|
7774
9027
|
if (config?.leaderboardAutoUpload === true && leaderboardUploadInterval > 0 && this.runLeaderboardUploadFn) {
|
|
@@ -7801,6 +9054,17 @@ var RuntimeSettingsController = class {
|
|
|
7801
9054
|
this.parseInFlight = false;
|
|
7802
9055
|
}
|
|
7803
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
|
+
}
|
|
7804
9068
|
async runLeaderboardUploadSafely() {
|
|
7805
9069
|
if (this.leaderboardUploadInFlight) return;
|
|
7806
9070
|
this.leaderboardUploadInFlight = true;
|
|
@@ -7817,7 +9081,7 @@ var RuntimeSettingsController = class {
|
|
|
7817
9081
|
|
|
7818
9082
|
// src/commands/serve.ts
|
|
7819
9083
|
var MAX_PORT_ATTEMPTS = 10;
|
|
7820
|
-
var PORT_FILE =
|
|
9084
|
+
var PORT_FILE = join11(AIUSAGE_DIR, ".serve-port");
|
|
7821
9085
|
var MIME_TYPES = {
|
|
7822
9086
|
".html": "text/html",
|
|
7823
9087
|
".js": "application/javascript",
|
|
@@ -7832,11 +9096,8 @@ var MIME_TYPES = {
|
|
|
7832
9096
|
};
|
|
7833
9097
|
function serve(options) {
|
|
7834
9098
|
const config = loadConfig();
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
setPriceOverride(model, entry);
|
|
7838
|
-
}
|
|
7839
|
-
}
|
|
9099
|
+
const dbWriteQueue = new AsyncTaskQueue();
|
|
9100
|
+
const runDbWrite = (task) => dbWriteQueue.run(task);
|
|
7840
9101
|
if (config == null || config.exchangeRate == null) {
|
|
7841
9102
|
const cacheAge = config?.exchangeRateCache ? Date.now() - config.exchangeRateCache.fetchedAt : Infinity;
|
|
7842
9103
|
if (cacheAge >= CACHE_TTL_MS) {
|
|
@@ -7852,8 +9113,10 @@ function serve(options) {
|
|
|
7852
9113
|
}
|
|
7853
9114
|
const syncRuntime = new SyncRuntimeController({
|
|
7854
9115
|
runSync: async (runtimeOptions) => {
|
|
7855
|
-
await
|
|
7856
|
-
|
|
9116
|
+
await runDbWrite(async () => {
|
|
9117
|
+
await runParse(options.db);
|
|
9118
|
+
await runSync(options.db, runtimeOptions);
|
|
9119
|
+
});
|
|
7857
9120
|
},
|
|
7858
9121
|
getPersistedState: () => getState(AIUSAGE_DIR),
|
|
7859
9122
|
getCurrentTarget: () => getSyncTarget(loadConfig()?.sync)
|
|
@@ -7861,14 +9124,14 @@ function serve(options) {
|
|
|
7861
9124
|
const runtimeSettings = new RuntimeSettingsController({
|
|
7862
9125
|
db: options.db,
|
|
7863
9126
|
loadConfig,
|
|
7864
|
-
runParse,
|
|
7865
|
-
runCleanup: cleanOldData,
|
|
9127
|
+
runParse: (db) => runDbWrite(() => runParse(db)),
|
|
9128
|
+
runCleanup: (db, retentionDays) => runDbWrite(() => cleanOldData(db, retentionDays)),
|
|
7866
9129
|
runLeaderboardUpload: (db) => uploadLeaderboardData(db, getState(AIUSAGE_DIR)?.deviceInstanceId).then(() => void 0),
|
|
7867
9130
|
runSync: () => syncRuntime.start(),
|
|
7868
9131
|
onSyncScheduleChanged: (ts3) => syncRuntime.setNextSyncAt(ts3)
|
|
7869
9132
|
});
|
|
7870
9133
|
runtimeSettings.start();
|
|
7871
|
-
runParse(options.db).catch((err) => {
|
|
9134
|
+
runDbWrite(() => runParse(options.db)).catch((err) => {
|
|
7872
9135
|
console.error("[serve] initial parse failed:", err);
|
|
7873
9136
|
});
|
|
7874
9137
|
const apiServer = createApiServer(options.db, {
|
|
@@ -7876,14 +9139,16 @@ function serve(options) {
|
|
|
7876
9139
|
onRefresh: () => runParse(options.db),
|
|
7877
9140
|
onSyncStart: () => syncRuntime.start(),
|
|
7878
9141
|
getSyncStatus: () => syncRuntime.getStatus(),
|
|
7879
|
-
onConfigUpdated: () => runtimeSettings.reload()
|
|
9142
|
+
onConfigUpdated: () => runtimeSettings.reload(),
|
|
9143
|
+
runDbWrite,
|
|
9144
|
+
getDbWriteQueueStatus: () => dbWriteQueue.getStatus()
|
|
7880
9145
|
});
|
|
7881
9146
|
const webBuildDir = (() => {
|
|
7882
|
-
const prodDir =
|
|
7883
|
-
if (
|
|
7884
|
-
return
|
|
9147
|
+
const prodDir = join11(dirname4(fileURLToPath(import.meta.url)), "web");
|
|
9148
|
+
if (existsSync10(prodDir)) return prodDir;
|
|
9149
|
+
return join11(dirname4(fileURLToPath(import.meta.url)), "..", "..", "..", "web", "build");
|
|
7885
9150
|
})();
|
|
7886
|
-
if (!
|
|
9151
|
+
if (!existsSync10(webBuildDir)) {
|
|
7887
9152
|
console.error("Web dashboard not found. Reinstall the package: npm install -g @juliantanx/aiusage");
|
|
7888
9153
|
process.exit(1);
|
|
7889
9154
|
}
|
|
@@ -7893,19 +9158,19 @@ function serve(options) {
|
|
|
7893
9158
|
apiServer.emit("request", req, res);
|
|
7894
9159
|
return;
|
|
7895
9160
|
}
|
|
7896
|
-
if (
|
|
7897
|
-
let filePath =
|
|
9161
|
+
if (existsSync10(webBuildDir)) {
|
|
9162
|
+
let filePath = join11(webBuildDir, url.pathname);
|
|
7898
9163
|
try {
|
|
7899
9164
|
if (statSync3(filePath).isDirectory()) {
|
|
7900
|
-
filePath =
|
|
9165
|
+
filePath = join11(filePath, "index.html");
|
|
7901
9166
|
}
|
|
7902
9167
|
} catch {
|
|
7903
9168
|
}
|
|
7904
|
-
if (!
|
|
7905
|
-
filePath =
|
|
9169
|
+
if (!existsSync10(filePath)) {
|
|
9170
|
+
filePath = join11(webBuildDir, "index.html");
|
|
7906
9171
|
}
|
|
7907
9172
|
try {
|
|
7908
|
-
const content =
|
|
9173
|
+
const content = readFileSync9(filePath);
|
|
7909
9174
|
const ext = extname2(filePath);
|
|
7910
9175
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
7911
9176
|
res.writeHead(200, { "Content-Type": contentType });
|
|
@@ -7925,7 +9190,7 @@ function serve(options) {
|
|
|
7925
9190
|
};
|
|
7926
9191
|
server.on("listening", () => {
|
|
7927
9192
|
started = true;
|
|
7928
|
-
|
|
9193
|
+
writeFileSync6(PORT_FILE, String(currentPort), "utf-8");
|
|
7929
9194
|
console.log(`aiusage serve listening on http://localhost:${currentPort}`);
|
|
7930
9195
|
});
|
|
7931
9196
|
server.on("error", (error) => {
|
|
@@ -7943,7 +9208,7 @@ function serve(options) {
|
|
|
7943
9208
|
listenOnPort(options.port);
|
|
7944
9209
|
const cleanup = () => {
|
|
7945
9210
|
try {
|
|
7946
|
-
|
|
9211
|
+
unlinkSync3(PORT_FILE);
|
|
7947
9212
|
} catch {
|
|
7948
9213
|
}
|
|
7949
9214
|
};
|
|
@@ -8087,9 +9352,9 @@ Consent recorded for schema v1.`
|
|
|
8087
9352
|
}
|
|
8088
9353
|
|
|
8089
9354
|
// src/commands/status.ts
|
|
8090
|
-
import { statSync as statSync4, existsSync as
|
|
9355
|
+
import { statSync as statSync4, existsSync as existsSync11 } from "fs";
|
|
8091
9356
|
import { hostname as hostname3 } from "os";
|
|
8092
|
-
import { join as
|
|
9357
|
+
import { join as join12 } from "path";
|
|
8093
9358
|
function formatFileSize(bytes) {
|
|
8094
9359
|
if (bytes < 1024) return `${bytes} B`;
|
|
8095
9360
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -8107,9 +9372,9 @@ function generateStatus(db) {
|
|
|
8107
9372
|
const schemaVersion = db.prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get()?.version ?? 0;
|
|
8108
9373
|
const tableCount = db.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").get()?.count ?? 0;
|
|
8109
9374
|
const viewCount = db.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type = 'view'").get()?.count ?? 0;
|
|
8110
|
-
const sizePath =
|
|
9375
|
+
const sizePath = join12(AIUSAGE_DIR, "cache.db");
|
|
8111
9376
|
let databaseSize = "0 B";
|
|
8112
|
-
if (
|
|
9377
|
+
if (existsSync11(sizePath)) {
|
|
8113
9378
|
try {
|
|
8114
9379
|
const stat2 = statSync4(sizePath);
|
|
8115
9380
|
databaseSize = formatFileSize(stat2.size);
|
|
@@ -8186,27 +9451,6 @@ function exportData(db, format) {
|
|
|
8186
9451
|
return lines.join("\n");
|
|
8187
9452
|
}
|
|
8188
9453
|
|
|
8189
|
-
// src/commands/reset.ts
|
|
8190
|
-
import { unlinkSync as unlinkSync3, existsSync as existsSync11 } from "fs";
|
|
8191
|
-
import { join as join12 } from "path";
|
|
8192
|
-
function runReset(db) {
|
|
8193
|
-
const recordsResult = db.prepare("DELETE FROM records").run();
|
|
8194
|
-
const toolCallsResult = db.prepare("DELETE FROM tool_calls").run();
|
|
8195
|
-
const syncedResult = db.prepare("DELETE FROM synced_records").run();
|
|
8196
|
-
const watermarkPath = join12(AIUSAGE_DIR, "watermark.json");
|
|
8197
|
-
let watermarkRemoved = false;
|
|
8198
|
-
if (existsSync11(watermarkPath)) {
|
|
8199
|
-
unlinkSync3(watermarkPath);
|
|
8200
|
-
watermarkRemoved = true;
|
|
8201
|
-
}
|
|
8202
|
-
return {
|
|
8203
|
-
deletedRecords: recordsResult.changes,
|
|
8204
|
-
deletedToolCalls: toolCallsResult.changes,
|
|
8205
|
-
deletedSyncedRecords: syncedResult.changes,
|
|
8206
|
-
watermarkRemoved
|
|
8207
|
-
};
|
|
8208
|
-
}
|
|
8209
|
-
|
|
8210
9454
|
// src/commands/recalc.ts
|
|
8211
9455
|
var BATCH_SIZE = 1e3;
|
|
8212
9456
|
function recalcPricing(db) {
|
|
@@ -8227,15 +9471,15 @@ function recalcPricing(db) {
|
|
|
8227
9471
|
}
|
|
8228
9472
|
const model = record.tool === "qoder" ? normalizeQoderModel(record.model) : record.model;
|
|
8229
9473
|
const provider = model !== record.model ? inferProvider(model) : record.provider;
|
|
8230
|
-
const
|
|
8231
|
-
const newCost =
|
|
9474
|
+
const price = resolvePriceFromRegistry(db, model);
|
|
9475
|
+
const newCost = price ? calculateCostForPrice(price, {
|
|
8232
9476
|
inputTokens: record.input_tokens,
|
|
8233
9477
|
outputTokens: record.output_tokens,
|
|
8234
9478
|
cacheReadTokens: record.cache_read_tokens,
|
|
8235
9479
|
cacheWriteTokens: record.cache_write_tokens,
|
|
8236
9480
|
thinkingTokens: record.thinking_tokens
|
|
8237
9481
|
}, exchangeRate) : 0;
|
|
8238
|
-
const costSource =
|
|
9482
|
+
const costSource = price ? "pricing" : "unknown";
|
|
8239
9483
|
if (model !== record.model || provider !== record.provider || newCost !== record.cost || costSource !== record.cost_source) {
|
|
8240
9484
|
updateStmt.run(model, provider, newCost, costSource, Date.now(), record.id);
|
|
8241
9485
|
updatedCount++;
|
|
@@ -8319,7 +9563,7 @@ var SyncProgressReporter = class {
|
|
|
8319
9563
|
};
|
|
8320
9564
|
|
|
8321
9565
|
// src/commands/widget.ts
|
|
8322
|
-
import { existsSync as existsSync12, readFileSync as
|
|
9566
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
|
|
8323
9567
|
import { spawn, execSync as execSync2 } from "child_process";
|
|
8324
9568
|
import { join as join13 } from "path";
|
|
8325
9569
|
var WIDGET_PID_PATH = join13(AIUSAGE_DIR, "widget.pid");
|
|
@@ -8347,7 +9591,7 @@ function isWidgetRunning() {
|
|
|
8347
9591
|
if (!existsSync12(WIDGET_PID_PATH)) return false;
|
|
8348
9592
|
let pid;
|
|
8349
9593
|
try {
|
|
8350
|
-
pid = parseInt(
|
|
9594
|
+
pid = parseInt(readFileSync10(WIDGET_PID_PATH, "utf-8").trim(), 10);
|
|
8351
9595
|
} catch {
|
|
8352
9596
|
return false;
|
|
8353
9597
|
}
|
|
@@ -8391,10 +9635,10 @@ function getDeviceName2() {
|
|
|
8391
9635
|
}
|
|
8392
9636
|
function getCliVersion2() {
|
|
8393
9637
|
try {
|
|
8394
|
-
const { readFileSync:
|
|
9638
|
+
const { readFileSync: readFileSync12 } = __require("fs");
|
|
8395
9639
|
const { join: join16 } = __require("path");
|
|
8396
9640
|
const pkgPath = join16(__dirname, "../../package.json");
|
|
8397
|
-
const pkg = JSON.parse(
|
|
9641
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
8398
9642
|
return pkg.version || "0.0.0";
|
|
8399
9643
|
} catch {
|
|
8400
9644
|
return "0.0.0";
|
|
@@ -8638,7 +9882,7 @@ async function runLeaderboardView(options = {}) {
|
|
|
8638
9882
|
// src/commands/menu.ts
|
|
8639
9883
|
import { createInterface } from "readline";
|
|
8640
9884
|
import { spawn as spawn2, spawnSync, execSync as execSync4 } from "child_process";
|
|
8641
|
-
import { existsSync as existsSync13, readFileSync as
|
|
9885
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
|
|
8642
9886
|
import { join as join14 } from "path";
|
|
8643
9887
|
var DEFAULT_PORT = 3847;
|
|
8644
9888
|
var PORT_FILE2 = join14(AIUSAGE_DIR, ".serve-port");
|
|
@@ -8647,7 +9891,7 @@ function clearScreen() {
|
|
|
8647
9891
|
}
|
|
8648
9892
|
function getPort() {
|
|
8649
9893
|
if (existsSync13(PORT_FILE2)) {
|
|
8650
|
-
const raw =
|
|
9894
|
+
const raw = readFileSync11(PORT_FILE2, "utf-8").trim();
|
|
8651
9895
|
const n = parseInt(raw, 10);
|
|
8652
9896
|
if (!isNaN(n) && n > 0) return n;
|
|
8653
9897
|
}
|
|
@@ -8825,11 +10069,10 @@ async function dataSubmenu(rl) {
|
|
|
8825
10069
|
[2] Summary
|
|
8826
10070
|
[3] Export
|
|
8827
10071
|
[4] Clean
|
|
8828
|
-
[5]
|
|
8829
|
-
[6] Recalc
|
|
10072
|
+
[5] Recalc
|
|
8830
10073
|
[0] Back
|
|
8831
10074
|
`);
|
|
8832
|
-
const choice = await prompt(rl, " Select [0-
|
|
10075
|
+
const choice = await prompt(rl, " Select [0-5]: ");
|
|
8833
10076
|
switch (choice) {
|
|
8834
10077
|
case "1": {
|
|
8835
10078
|
const progress = await prompt(rl, " Show progress? [Y/N]: ");
|
|
@@ -8865,28 +10108,28 @@ async function dataSubmenu(rl) {
|
|
|
8865
10108
|
break;
|
|
8866
10109
|
}
|
|
8867
10110
|
case "4": {
|
|
8868
|
-
const
|
|
8869
|
-
const before = dur || "180d";
|
|
10111
|
+
const mode = await prompt(rl, " 1=Old data (180d) 2=Old data (custom) 3=All (=reset): ");
|
|
8870
10112
|
console.log("");
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
10113
|
+
if (mode === "3") {
|
|
10114
|
+
const confirm = await prompt(
|
|
10115
|
+
rl,
|
|
10116
|
+
' WARNING: This will delete ALL parsed data. Type "confirm" to proceed: '
|
|
10117
|
+
);
|
|
10118
|
+
if (confirm.toLowerCase() === "confirm") {
|
|
10119
|
+
runCommand(["clean", "--all", "--yes"]);
|
|
10120
|
+
} else {
|
|
10121
|
+
console.log(" Cancelled.");
|
|
10122
|
+
}
|
|
10123
|
+
} else if (mode === "2") {
|
|
10124
|
+
const dur = await prompt(rl, " Duration to keep (e.g. 30d, 90d): ");
|
|
10125
|
+
runCommand(["clean", "--before", dur || "180d", "--yes"]);
|
|
8883
10126
|
} else {
|
|
8884
|
-
|
|
10127
|
+
runCommand(["clean", "--before", "180d", "--yes"]);
|
|
8885
10128
|
}
|
|
8886
10129
|
await prompt(rl, "\n Press Enter to continue...");
|
|
8887
10130
|
break;
|
|
8888
10131
|
}
|
|
8889
|
-
case "
|
|
10132
|
+
case "5": {
|
|
8890
10133
|
console.log("");
|
|
8891
10134
|
runCommand(["recalc"]);
|
|
8892
10135
|
await prompt(rl, "\n Press Enter to continue...");
|
|
@@ -9101,7 +10344,7 @@ async function runMenu() {
|
|
|
9101
10344
|
${dashboardStatusLine()}
|
|
9102
10345
|
|
|
9103
10346
|
[1] Dashboard (serve/stop/restart/open)
|
|
9104
|
-
[2] Data (parse/summary/export/clean/
|
|
10347
|
+
[2] Data (parse/summary/export/clean/recalc)
|
|
9105
10348
|
[3] Sync (init/sync)
|
|
9106
10349
|
[4] Leaderboard (view/login/upload/status/logout)
|
|
9107
10350
|
[5] System (status/widget/pm2/update)
|
|
@@ -9140,7 +10383,7 @@ ${dashboardStatusLine()}
|
|
|
9140
10383
|
import { join as join15 } from "path";
|
|
9141
10384
|
var DB_PATH2 = join15(AIUSAGE_DIR, "cache.db");
|
|
9142
10385
|
var program = new Command();
|
|
9143
|
-
program.name("aiusage").version(true ? "1.5.
|
|
10386
|
+
program.name("aiusage").version(true ? "1.5.3" : "dev").description("CLI tool for AI usage statistics");
|
|
9144
10387
|
program.action(() => {
|
|
9145
10388
|
const unknownCommand = program.args.find((arg) => typeof arg === "string");
|
|
9146
10389
|
if (unknownCommand) {
|
|
@@ -9222,7 +10465,7 @@ program.command("export").description("Export data").requiredOption("--format <f
|
|
|
9222
10465
|
const db = createDatabase(DB_PATH2);
|
|
9223
10466
|
const data = exportData(db, format);
|
|
9224
10467
|
if (options.output) {
|
|
9225
|
-
|
|
10468
|
+
writeFileSync7(options.output, data, "utf-8");
|
|
9226
10469
|
console.log(`Exported to ${options.output}`);
|
|
9227
10470
|
} else {
|
|
9228
10471
|
console.log(data);
|
|
@@ -9249,32 +10492,109 @@ program.command("parse").description("Parse AI tool session logs").option("--too
|
|
|
9249
10492
|
db.close();
|
|
9250
10493
|
}
|
|
9251
10494
|
});
|
|
9252
|
-
program.command("clean").description("Clean old data").option("--before <duration>", "
|
|
9253
|
-
const
|
|
9254
|
-
|
|
9255
|
-
|
|
10495
|
+
program.command("clean").description("Clean data (old records or all data)").option("--before <duration>", "Delete data older than this (e.g., 30d, 180d)", "180d").option("--all", "Wipe all data (equivalent to former reset: all records, tool calls, watermark)").option("--local-only", "Only clean local data, do not propagate to remote backends").option("--target <backend>", "Target specific remote backend (github/s3/cloud)").option("--yes", "Skip local confirmation (remote deletion still requires confirmation)").action(async (options) => {
|
|
10496
|
+
const isAll = !!options.all;
|
|
10497
|
+
const localOnly = !!options.localOnly;
|
|
10498
|
+
let days = 0;
|
|
10499
|
+
if (isAll && options.before !== "180d") {
|
|
10500
|
+
console.error("Cannot use --all and --before together. Use one or the other.");
|
|
9256
10501
|
process.exit(1);
|
|
9257
10502
|
}
|
|
9258
|
-
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
}
|
|
9264
|
-
|
|
10503
|
+
if (!isAll) {
|
|
10504
|
+
const daysMatch = options.before.match(/^(\d+)d$/);
|
|
10505
|
+
if (!daysMatch) {
|
|
10506
|
+
console.error("Invalid duration format. Use e.g. 30d, 180d.");
|
|
10507
|
+
process.exit(1);
|
|
10508
|
+
}
|
|
10509
|
+
days = parseInt(daysMatch[1], 10);
|
|
10510
|
+
}
|
|
10511
|
+
const remoteBackends = localOnly ? [] : getRemoteBackends();
|
|
10512
|
+
if (isAll) {
|
|
10513
|
+
console.log("\x1B[33m\u26A0 WARNING: This will delete ALL parsed data, tool calls, synced records,\x1B[0m");
|
|
10514
|
+
console.log("\x1B[33m and the parse watermark. Next parse will re-import from scratch.\x1B[0m");
|
|
10515
|
+
} else {
|
|
10516
|
+
console.log(`This will delete local records older than ${days} days.`);
|
|
10517
|
+
}
|
|
10518
|
+
if (localOnly) {
|
|
10519
|
+
console.log("\x1B[36m (--local-only: remote data will NOT be affected)\x1B[0m");
|
|
10520
|
+
}
|
|
10521
|
+
if (remoteBackends.length > 0) {
|
|
10522
|
+
console.log("\nCloud sync is configured. The following remote backends will also be affected:");
|
|
10523
|
+
for (const rb of remoteBackends) {
|
|
10524
|
+
console.log(` - ${rb.label}`);
|
|
10525
|
+
}
|
|
10526
|
+
}
|
|
10527
|
+
const ask = async (question) => {
|
|
10528
|
+
if (!process.stdin.isTTY) {
|
|
10529
|
+
console.error("Non-interactive mode detected. Use --local-only to skip remote propagation, or run interactively.");
|
|
10530
|
+
process.exit(1);
|
|
10531
|
+
}
|
|
10532
|
+
const { createInterface: createInterface2 } = await import("readline");
|
|
10533
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
10534
|
+
return new Promise((resolve2) => {
|
|
10535
|
+
rl.question(question, (answer) => {
|
|
10536
|
+
rl.close();
|
|
10537
|
+
resolve2(answer.trim());
|
|
10538
|
+
});
|
|
10539
|
+
});
|
|
10540
|
+
};
|
|
9265
10541
|
if (!options.yes) {
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
10542
|
+
if (remoteBackends.length > 0) {
|
|
10543
|
+
const answer = await ask(`
|
|
10544
|
+
? Confirm deletion on local + ${remoteBackends.length} remote backend(s)? [y/N] `);
|
|
10545
|
+
if (answer.toLowerCase() !== "y") {
|
|
10546
|
+
console.log("Cancelled.");
|
|
10547
|
+
process.exit(0);
|
|
10548
|
+
}
|
|
10549
|
+
} else {
|
|
10550
|
+
const answer = await ask("\n? Confirm? [y/N] ");
|
|
10551
|
+
if (answer.toLowerCase() !== "y") {
|
|
10552
|
+
console.log("Cancelled.");
|
|
10553
|
+
process.exit(0);
|
|
10554
|
+
}
|
|
10555
|
+
}
|
|
10556
|
+
} else if (remoteBackends.length > 0) {
|
|
10557
|
+
const answer = await ask('\n? Type "confirm" to proceed with remote deletion: ');
|
|
10558
|
+
if (answer !== "confirm") {
|
|
10559
|
+
console.log("Cancelled.");
|
|
10560
|
+
process.exit(0);
|
|
10561
|
+
}
|
|
9270
10562
|
}
|
|
9271
10563
|
const db = createDatabase(DB_PATH2);
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
console.log(
|
|
10564
|
+
if (isAll) {
|
|
10565
|
+
console.log("\nDeleting all local data...");
|
|
10566
|
+
const result = cleanAll(db);
|
|
10567
|
+
console.log(` Local: deleted ${result.deletedRecords} records, ${result.deletedToolCalls} tool calls, ${result.deletedSyncedRecords} synced records`);
|
|
10568
|
+
if (result.watermarkRemoved) {
|
|
10569
|
+
console.log(" Watermark removed");
|
|
10570
|
+
}
|
|
10571
|
+
} else {
|
|
10572
|
+
console.log(`
|
|
10573
|
+
Cleaning local records older than ${days} days...`);
|
|
10574
|
+
const result = cleanOldData(db, days);
|
|
10575
|
+
console.log(` Local: deleted ${result.deletedCount} records, ${result.deletedSyncedCount} synced records, ${result.deletedOrphanToolCalls} orphan tool calls`);
|
|
9276
10576
|
}
|
|
9277
10577
|
db.close();
|
|
10578
|
+
if (remoteBackends.length > 0) {
|
|
10579
|
+
console.log("\nSyncing deletion to remote backends...");
|
|
10580
|
+
try {
|
|
10581
|
+
const propagation = await propagateClean({
|
|
10582
|
+
all: isAll,
|
|
10583
|
+
beforeDays: isAll ? void 0 : days,
|
|
10584
|
+
target: options.target
|
|
10585
|
+
});
|
|
10586
|
+
for (const r of propagation.backends) {
|
|
10587
|
+
if (r.status === "ok") {
|
|
10588
|
+
console.log(` ${r.backend.label}: ${r.detail}`);
|
|
10589
|
+
} else {
|
|
10590
|
+
console.log(` ${r.backend.label}: skipped \u2014 ${r.detail}`);
|
|
10591
|
+
}
|
|
10592
|
+
}
|
|
10593
|
+
} catch (err) {
|
|
10594
|
+
console.error(` Propagation failed: ${err instanceof Error ? err.message : err}`);
|
|
10595
|
+
}
|
|
10596
|
+
}
|
|
10597
|
+
console.log("\nDone.");
|
|
9278
10598
|
});
|
|
9279
10599
|
program.command("recalc").description("Recalculate costs").option("--pricing", "Recalculate using latest pricing").action(() => {
|
|
9280
10600
|
const db = createDatabase(DB_PATH2);
|
|
@@ -9351,7 +10671,7 @@ function hasWidget() {
|
|
|
9351
10671
|
}
|
|
9352
10672
|
function buildPm2Config(includeWidget) {
|
|
9353
10673
|
const wrapperPath = join15(AIUSAGE_DIR, "pm2-server.cjs");
|
|
9354
|
-
|
|
10674
|
+
writeFileSync7(wrapperPath, [
|
|
9355
10675
|
`// Auto-generated by aiusage pm2-start \u2014 do not edit`,
|
|
9356
10676
|
`const { execSync, spawn } = require('child_process')`,
|
|
9357
10677
|
`const { dirname, join } = require('path')`,
|
|
@@ -9398,7 +10718,7 @@ program.command("pm2-setup").description("Generate ecosystem.config.cjs for PM2
|
|
|
9398
10718
|
const includeWidget = options.serverOnly ? false : hasWidget();
|
|
9399
10719
|
const config = buildPm2Config(includeWidget);
|
|
9400
10720
|
const configPath = join15(AIUSAGE_DIR, "ecosystem.config.cjs");
|
|
9401
|
-
|
|
10721
|
+
writeFileSync7(configPath, config, "utf-8");
|
|
9402
10722
|
console.log(`Config created: ${configPath}`);
|
|
9403
10723
|
if (!includeWidget) {
|
|
9404
10724
|
console.log("Note: aiusage-widget not found, only server is configured.");
|
|
@@ -9408,7 +10728,7 @@ program.command("pm2-start").description("Setup and start PM2 background service
|
|
|
9408
10728
|
const includeWidget = options.serverOnly ? false : hasWidget();
|
|
9409
10729
|
const config = buildPm2Config(includeWidget);
|
|
9410
10730
|
const configPath = join15(AIUSAGE_DIR, "ecosystem.config.cjs");
|
|
9411
|
-
|
|
10731
|
+
writeFileSync7(configPath, config, "utf-8");
|
|
9412
10732
|
console.log(`Config: ${configPath}`);
|
|
9413
10733
|
try {
|
|
9414
10734
|
execSync5(`pm2 start "${configPath}"`, { stdio: "inherit" });
|