agent-cli-proxy 0.1.0
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/LICENSE +21 -0
- package/README.md +348 -0
- package/dist/cli.js +4287 -0
- package/dist/data/plans.default.json +93 -0
- package/dist/index.js +4801 -0
- package/dist/migrations/001_init.sql +41 -0
- package/dist/migrations/002_agent_attribution.sql +25 -0
- package/dist/migrations/003_enhanced_logging.sql +3 -0
- package/dist/migrations/004_cliproxy_attribution.sql +40 -0
- package/dist/migrations/005_lifecycle_cost_subscription.sql +39 -0
- package/dist/migrations/006_account_subscriptions.sql +8 -0
- package/package.json +62 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,4287 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __returnValue = (v) => v;
|
|
5
|
+
function __exportSetter(name, newValue) {
|
|
6
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
+
}
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
set: __exportSetter.bind(all, name)
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
+
|
|
19
|
+
// data/plans.default.json
|
|
20
|
+
var plans_default_default;
|
|
21
|
+
var init_plans_default = __esm(() => {
|
|
22
|
+
plans_default_default = {
|
|
23
|
+
plans: [
|
|
24
|
+
{
|
|
25
|
+
code: "claude_pro",
|
|
26
|
+
provider: "anthropic",
|
|
27
|
+
display_name: "Anthropic Claude Pro",
|
|
28
|
+
monthly_price_usd: 20,
|
|
29
|
+
currency: "USD",
|
|
30
|
+
billing_period_days: 30,
|
|
31
|
+
vendor_url: "https://www.anthropic.com/claude/pricing",
|
|
32
|
+
notes: "Conservative estimate \u2014 verify with vendor \u2014 last updated 2026-05"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
code: "claude_max5",
|
|
36
|
+
provider: "anthropic",
|
|
37
|
+
display_name: "Anthropic Claude Max 5x",
|
|
38
|
+
monthly_price_usd: 100,
|
|
39
|
+
currency: "USD",
|
|
40
|
+
billing_period_days: 30,
|
|
41
|
+
vendor_url: "https://www.anthropic.com/claude/pricing",
|
|
42
|
+
notes: "Conservative estimate \u2014 verify with vendor \u2014 last updated 2026-05"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
code: "claude_max20",
|
|
46
|
+
provider: "anthropic",
|
|
47
|
+
display_name: "Anthropic Claude Max 20x",
|
|
48
|
+
monthly_price_usd: 200,
|
|
49
|
+
currency: "USD",
|
|
50
|
+
billing_period_days: 30,
|
|
51
|
+
vendor_url: "https://www.anthropic.com/claude/pricing",
|
|
52
|
+
notes: "Conservative estimate \u2014 verify with vendor \u2014 last updated 2026-05"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
code: "chatgpt_plus",
|
|
56
|
+
provider: "openai",
|
|
57
|
+
display_name: "OpenAI ChatGPT Plus",
|
|
58
|
+
monthly_price_usd: 20,
|
|
59
|
+
currency: "USD",
|
|
60
|
+
billing_period_days: 30,
|
|
61
|
+
vendor_url: "https://openai.com/chatgpt/pricing/",
|
|
62
|
+
notes: "Conservative estimate \u2014 verify with vendor \u2014 last updated 2026-05"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
code: "chatgpt_pro",
|
|
66
|
+
provider: "openai",
|
|
67
|
+
display_name: "OpenAI ChatGPT Pro",
|
|
68
|
+
monthly_price_usd: 200,
|
|
69
|
+
currency: "USD",
|
|
70
|
+
billing_period_days: 30,
|
|
71
|
+
vendor_url: "https://openai.com/chatgpt/pricing/",
|
|
72
|
+
notes: "Conservative estimate \u2014 verify with vendor \u2014 last updated 2026-05"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
code: "chatgpt_business",
|
|
76
|
+
provider: "openai",
|
|
77
|
+
display_name: "OpenAI ChatGPT Business",
|
|
78
|
+
monthly_price_usd: 25,
|
|
79
|
+
currency: "USD",
|
|
80
|
+
billing_period_days: 30,
|
|
81
|
+
vendor_url: "https://openai.com/chatgpt/pricing/",
|
|
82
|
+
notes: "Conservative estimate (per-seat); verify with vendor \u2014 last updated 2026-05"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
code: "kimi_pro",
|
|
86
|
+
provider: "moonshot",
|
|
87
|
+
display_name: "Moonshot Kimi",
|
|
88
|
+
monthly_price_usd: 15,
|
|
89
|
+
currency: "USD",
|
|
90
|
+
billing_period_days: 30,
|
|
91
|
+
vendor_url: "https://www.moonshot.cn/",
|
|
92
|
+
notes: "Conservative estimate \u2014 verify with vendor \u2014 last updated 2026-05"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
code: "glm_pro",
|
|
96
|
+
provider: "bigmodel",
|
|
97
|
+
display_name: "BigModel GLM",
|
|
98
|
+
monthly_price_usd: 10,
|
|
99
|
+
currency: "USD",
|
|
100
|
+
billing_period_days: 30,
|
|
101
|
+
vendor_url: "https://open.bigmodel.cn/",
|
|
102
|
+
notes: "Conservative estimate \u2014 verify with vendor \u2014 last updated 2026-05"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
code: "local_byok",
|
|
106
|
+
provider: "local",
|
|
107
|
+
display_name: "Bring Your Own Key (BYOK / Self-hosted)",
|
|
108
|
+
monthly_price_usd: 0,
|
|
109
|
+
currency: "USD",
|
|
110
|
+
billing_period_days: 30,
|
|
111
|
+
notes: "Self-hosted or BYOK provider. No vendor billing. verify with vendor \u2014 last updated 2026-05"
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// src/util/logger.ts
|
|
118
|
+
var Logger;
|
|
119
|
+
var init_logger = __esm(() => {
|
|
120
|
+
((Logger) => {
|
|
121
|
+
const LEVELS = {
|
|
122
|
+
debug: 10,
|
|
123
|
+
info: 20,
|
|
124
|
+
warn: 30,
|
|
125
|
+
error: 40
|
|
126
|
+
};
|
|
127
|
+
const REDACTED = "[REDACTED]";
|
|
128
|
+
const defaultSink = {
|
|
129
|
+
stdout(line) {
|
|
130
|
+
process.stdout.write(`${line}
|
|
131
|
+
`);
|
|
132
|
+
},
|
|
133
|
+
stderr(line) {
|
|
134
|
+
process.stderr.write(`${line}
|
|
135
|
+
`);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
function create(options = {}) {
|
|
139
|
+
return new StructuredLogger({
|
|
140
|
+
level: normalizeLevel(options.level),
|
|
141
|
+
format: normalizeFormat(options.format),
|
|
142
|
+
base: redact(options.base ?? {}),
|
|
143
|
+
sink: options.sink ?? defaultSink
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
Logger.create = create;
|
|
147
|
+
function fromConfig(options = {}) {
|
|
148
|
+
return create({
|
|
149
|
+
...options,
|
|
150
|
+
level: normalizeLevel(process.env.LOG_LEVEL),
|
|
151
|
+
format: normalizeFormat(process.env.LOG_FORMAT)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
Logger.fromConfig = fromConfig;
|
|
155
|
+
function redactValue(value) {
|
|
156
|
+
return redact(value);
|
|
157
|
+
}
|
|
158
|
+
Logger.redactValue = redactValue;
|
|
159
|
+
|
|
160
|
+
class StructuredLogger {
|
|
161
|
+
options;
|
|
162
|
+
constructor(options) {
|
|
163
|
+
this.options = options;
|
|
164
|
+
}
|
|
165
|
+
child(base) {
|
|
166
|
+
return new StructuredLogger({
|
|
167
|
+
...this.options,
|
|
168
|
+
base: {
|
|
169
|
+
...this.options.base,
|
|
170
|
+
...redact(base)
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
debug(msg, fields) {
|
|
175
|
+
this.write("debug", msg, fields);
|
|
176
|
+
}
|
|
177
|
+
info(msg, fields) {
|
|
178
|
+
this.write("info", msg, fields);
|
|
179
|
+
}
|
|
180
|
+
warn(msg, fields) {
|
|
181
|
+
this.write("warn", msg, fields);
|
|
182
|
+
}
|
|
183
|
+
error(msg, fields) {
|
|
184
|
+
this.write("error", msg, fields);
|
|
185
|
+
}
|
|
186
|
+
write(level, msg, fields = {}) {
|
|
187
|
+
if (LEVELS[level] < LEVELS[this.options.level])
|
|
188
|
+
return;
|
|
189
|
+
const record = {
|
|
190
|
+
ts: new Date().toISOString(),
|
|
191
|
+
level,
|
|
192
|
+
msg,
|
|
193
|
+
...this.options.base,
|
|
194
|
+
...redact(fields)
|
|
195
|
+
};
|
|
196
|
+
const line = this.options.format === "pretty" ? formatPretty(record) : JSON.stringify(record);
|
|
197
|
+
if (level === "error") {
|
|
198
|
+
this.options.sink.stderr(line);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.options.sink.stdout(line);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function normalizeLevel(value) {
|
|
205
|
+
if (value === "debug" || value === "info" || value === "warn" || value === "error")
|
|
206
|
+
return value;
|
|
207
|
+
return "info";
|
|
208
|
+
}
|
|
209
|
+
function normalizeFormat(value) {
|
|
210
|
+
if (value === "pretty")
|
|
211
|
+
return "pretty";
|
|
212
|
+
return "json";
|
|
213
|
+
}
|
|
214
|
+
function isSensitiveKey(key) {
|
|
215
|
+
return /authorization|x[-_]?api[-_]?key|api[-_]?key|token|password|secret/i.test(key);
|
|
216
|
+
}
|
|
217
|
+
function redact(value, seen = new WeakSet) {
|
|
218
|
+
if (value === null || value === undefined)
|
|
219
|
+
return value;
|
|
220
|
+
if (typeof value !== "object")
|
|
221
|
+
return value;
|
|
222
|
+
if (value instanceof Error) {
|
|
223
|
+
return {
|
|
224
|
+
name: value.name,
|
|
225
|
+
message: value.message,
|
|
226
|
+
stack: value.stack
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (seen.has(value))
|
|
230
|
+
return "[Circular]";
|
|
231
|
+
seen.add(value);
|
|
232
|
+
if (Array.isArray(value)) {
|
|
233
|
+
return value.map((item) => redact(item, seen));
|
|
234
|
+
}
|
|
235
|
+
if (value instanceof Headers) {
|
|
236
|
+
const out2 = {};
|
|
237
|
+
for (const [key, headerValue] of value.entries()) {
|
|
238
|
+
out2[key] = isSensitiveKey(key) ? REDACTED : headerValue;
|
|
239
|
+
}
|
|
240
|
+
return out2;
|
|
241
|
+
}
|
|
242
|
+
const out = {};
|
|
243
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
244
|
+
out[key] = isSensitiveKey(key) ? REDACTED : redact(entry, seen);
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
function formatPretty(record) {
|
|
249
|
+
const { ts, level, msg, ...fields } = record;
|
|
250
|
+
const suffix = Object.entries(fields).map(([key, value]) => `${key}=${formatPrettyValue(value)}`).join(" ");
|
|
251
|
+
return suffix ? `${ts} ${level.toUpperCase()} ${msg} ${suffix}` : `${ts} ${level.toUpperCase()} ${msg}`;
|
|
252
|
+
}
|
|
253
|
+
function formatPrettyValue(value) {
|
|
254
|
+
if (typeof value === "string")
|
|
255
|
+
return value.includes(" ") ? JSON.stringify(value) : value;
|
|
256
|
+
return JSON.stringify(value);
|
|
257
|
+
}
|
|
258
|
+
})(Logger ||= {});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// src/plans/index.ts
|
|
262
|
+
import { existsSync, readFileSync } from "fs";
|
|
263
|
+
import { join } from "path";
|
|
264
|
+
var Plans;
|
|
265
|
+
var init_plans = __esm(() => {
|
|
266
|
+
init_plans_default();
|
|
267
|
+
init_logger();
|
|
268
|
+
((Plans) => {
|
|
269
|
+
|
|
270
|
+
class SchemaError extends Error {
|
|
271
|
+
issues;
|
|
272
|
+
name = "PlansSchemaError";
|
|
273
|
+
code = "PLANS_SCHEMA_INVALID";
|
|
274
|
+
constructor(issues) {
|
|
275
|
+
super(`Plans schema validation failed: ${issues.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`);
|
|
276
|
+
this.issues = issues;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
Plans.SchemaError = SchemaError;
|
|
280
|
+
const logger = Logger.fromConfig().child({ component: "plans" });
|
|
281
|
+
let cache = null;
|
|
282
|
+
function load() {
|
|
283
|
+
return readCache().list;
|
|
284
|
+
}
|
|
285
|
+
Plans.load = load;
|
|
286
|
+
function list() {
|
|
287
|
+
return load();
|
|
288
|
+
}
|
|
289
|
+
Plans.list = list;
|
|
290
|
+
function byCode(code) {
|
|
291
|
+
return readCache().byCode.get(code) ?? null;
|
|
292
|
+
}
|
|
293
|
+
Plans.byCode = byCode;
|
|
294
|
+
function validateBindingInput(account, code) {
|
|
295
|
+
const normalizedAccount = account.trim();
|
|
296
|
+
if (!normalizedAccount)
|
|
297
|
+
throw new Error("Account must be a non-empty string");
|
|
298
|
+
const normalizedCode = code.trim();
|
|
299
|
+
if (!normalizedCode)
|
|
300
|
+
throw new Error("Plan code must be a non-empty string");
|
|
301
|
+
if (!byCode(normalizedCode))
|
|
302
|
+
throw new Error(`Unknown plan code: ${normalizedCode}`);
|
|
303
|
+
return { account: normalizedAccount, code: normalizedCode };
|
|
304
|
+
}
|
|
305
|
+
Plans.validateBindingInput = validateBindingInput;
|
|
306
|
+
function reload() {
|
|
307
|
+
cache = null;
|
|
308
|
+
return load();
|
|
309
|
+
}
|
|
310
|
+
Plans.reload = reload;
|
|
311
|
+
function readCache() {
|
|
312
|
+
if (cache)
|
|
313
|
+
return cache;
|
|
314
|
+
const source = resolveSource();
|
|
315
|
+
const plans = parseSourceOrFallback(source);
|
|
316
|
+
const frozenPlans = Object.freeze(plans.map((plan) => Object.freeze({ ...plan })));
|
|
317
|
+
cache = {
|
|
318
|
+
list: frozenPlans,
|
|
319
|
+
byCode: new Map(frozenPlans.map((plan) => [plan.code, plan]))
|
|
320
|
+
};
|
|
321
|
+
return cache;
|
|
322
|
+
}
|
|
323
|
+
function resolveSource() {
|
|
324
|
+
const inline = process.env.PLANS_JSON;
|
|
325
|
+
if (inline !== undefined && inline.trim() !== "")
|
|
326
|
+
return { kind: "PLANS_JSON", raw: inline };
|
|
327
|
+
const envPath = process.env.PLANS_PATH?.trim();
|
|
328
|
+
if (envPath)
|
|
329
|
+
return readPathSource("PLANS_PATH", envPath);
|
|
330
|
+
const xdgPath = xdgPlansPath();
|
|
331
|
+
if (xdgPath && existsSync(xdgPath))
|
|
332
|
+
return readPathSource("XDG_CONFIG_HOME", xdgPath);
|
|
333
|
+
return { kind: "default", value: plans_default_default };
|
|
334
|
+
}
|
|
335
|
+
function readPathSource(kind, configPath) {
|
|
336
|
+
try {
|
|
337
|
+
return { kind, raw: readFileSync(configPath, "utf-8"), configPath };
|
|
338
|
+
} catch (err) {
|
|
339
|
+
return { kind, raw: undefined, configPath, value: err };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function xdgPlansPath() {
|
|
343
|
+
const base = process.env.XDG_CONFIG_HOME?.trim() || (process.env.HOME?.trim() ? join(process.env.HOME.trim(), ".config") : "");
|
|
344
|
+
if (!base)
|
|
345
|
+
return null;
|
|
346
|
+
return join(base, "agent-cli-proxy", "plans.json");
|
|
347
|
+
}
|
|
348
|
+
function parseSourceOrFallback(source) {
|
|
349
|
+
if (source.kind === "default")
|
|
350
|
+
return validateDefault(plans_default_default);
|
|
351
|
+
const result = parseSource(source);
|
|
352
|
+
if (result.ok)
|
|
353
|
+
return result.plans;
|
|
354
|
+
logger.warn("plans config invalid; falling back to packaged defaults", {
|
|
355
|
+
event: "plans.config.invalid",
|
|
356
|
+
source: source.kind,
|
|
357
|
+
configPath: source.configPath,
|
|
358
|
+
path: result.issues[0]?.path ?? source.kind,
|
|
359
|
+
issues: result.issues
|
|
360
|
+
});
|
|
361
|
+
return validateDefault(plans_default_default);
|
|
362
|
+
}
|
|
363
|
+
function parseSource(source) {
|
|
364
|
+
if (source.raw === undefined) {
|
|
365
|
+
return {
|
|
366
|
+
ok: false,
|
|
367
|
+
issues: [{ path: source.kind, message: source.value instanceof Error ? source.value.message : "could not be read" }]
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
let parsed;
|
|
371
|
+
try {
|
|
372
|
+
parsed = JSON.parse(source.raw);
|
|
373
|
+
} catch (err) {
|
|
374
|
+
return {
|
|
375
|
+
ok: false,
|
|
376
|
+
issues: [{ path: "plans", message: err instanceof Error ? err.message : "must be valid JSON" }]
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const validation = parsePlanDocument(parsed);
|
|
380
|
+
if (validation.issues.length > 0)
|
|
381
|
+
return { ok: false, issues: validation.issues };
|
|
382
|
+
return { ok: true, plans: validation.plans };
|
|
383
|
+
}
|
|
384
|
+
function validateDefault(value) {
|
|
385
|
+
const validation = parsePlanDocument(value);
|
|
386
|
+
if (validation.issues.length > 0)
|
|
387
|
+
throw new SchemaError(validation.issues);
|
|
388
|
+
return validation.plans;
|
|
389
|
+
}
|
|
390
|
+
function parsePlanDocument(value) {
|
|
391
|
+
const issues = [];
|
|
392
|
+
const plans = [];
|
|
393
|
+
if (!isRecord(value)) {
|
|
394
|
+
issues.push({ path: "plans", message: "must be contained in an object" });
|
|
395
|
+
return { plans, issues };
|
|
396
|
+
}
|
|
397
|
+
if (!Array.isArray(value.plans)) {
|
|
398
|
+
issues.push({ path: "plans", message: "must be an array" });
|
|
399
|
+
return { plans, issues };
|
|
400
|
+
}
|
|
401
|
+
value.plans.forEach((entry, index) => {
|
|
402
|
+
const plan = parsePlan(entry, `plans[${index}]`, issues);
|
|
403
|
+
if (plan)
|
|
404
|
+
plans.push(plan);
|
|
405
|
+
});
|
|
406
|
+
const seen = new Set;
|
|
407
|
+
plans.forEach((plan, index) => {
|
|
408
|
+
if (seen.has(plan.code))
|
|
409
|
+
issues.push({ path: `plans[${index}].code`, message: "must be unique" });
|
|
410
|
+
seen.add(plan.code);
|
|
411
|
+
});
|
|
412
|
+
return { plans: issues.length === 0 ? plans : [], issues };
|
|
413
|
+
}
|
|
414
|
+
function parsePlan(value, path, issues) {
|
|
415
|
+
if (!isRecord(value)) {
|
|
416
|
+
issues.push({ path, message: "must be an object" });
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
const code = readRequiredString(value, "code", path, issues);
|
|
420
|
+
const provider = readRequiredString(value, "provider", path, issues);
|
|
421
|
+
const displayName = readRequiredString(value, "display_name", path, issues);
|
|
422
|
+
const monthlyPriceUsd = readRequiredNumber(value, "monthly_price_usd", path, issues);
|
|
423
|
+
const currency = readRequiredString(value, "currency", path, issues);
|
|
424
|
+
const billingPeriodDays = readRequiredPositiveInteger(value, "billing_period_days", path, issues);
|
|
425
|
+
const vendorUrl = readOptionalHttpUrl(value, "vendor_url", path, issues);
|
|
426
|
+
const notes = readOptionalString(value, "notes", path, issues);
|
|
427
|
+
if (!code || !provider || !displayName || monthlyPriceUsd === undefined || !currency || billingPeriodDays === undefined)
|
|
428
|
+
return null;
|
|
429
|
+
const plan = {
|
|
430
|
+
code,
|
|
431
|
+
provider,
|
|
432
|
+
display_name: displayName,
|
|
433
|
+
monthly_price_usd: monthlyPriceUsd,
|
|
434
|
+
currency,
|
|
435
|
+
billing_period_days: billingPeriodDays
|
|
436
|
+
};
|
|
437
|
+
if (vendorUrl !== undefined)
|
|
438
|
+
plan.vendor_url = vendorUrl;
|
|
439
|
+
if (notes !== undefined)
|
|
440
|
+
plan.notes = notes;
|
|
441
|
+
return plan;
|
|
442
|
+
}
|
|
443
|
+
function readRequiredString(record, key, path, issues) {
|
|
444
|
+
const value = record[key];
|
|
445
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
446
|
+
issues.push({ path: `${path}.${key}`, message: "must be a non-empty string" });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
return value.trim();
|
|
450
|
+
}
|
|
451
|
+
function readOptionalString(record, key, path, issues) {
|
|
452
|
+
const value = record[key];
|
|
453
|
+
if (value === undefined)
|
|
454
|
+
return;
|
|
455
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
456
|
+
issues.push({ path: `${path}.${key}`, message: "must be a non-empty string" });
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
return value.trim();
|
|
460
|
+
}
|
|
461
|
+
function readRequiredNumber(record, key, path, issues) {
|
|
462
|
+
const value = record[key];
|
|
463
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
464
|
+
issues.push({ path: `${path}.${key}`, message: "must be a non-negative finite number" });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
return value;
|
|
468
|
+
}
|
|
469
|
+
function readRequiredPositiveInteger(record, key, path, issues) {
|
|
470
|
+
const value = record[key];
|
|
471
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
472
|
+
issues.push({ path: `${path}.${key}`, message: "must be a positive integer" });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
return value;
|
|
476
|
+
}
|
|
477
|
+
function readOptionalHttpUrl(record, key, path, issues) {
|
|
478
|
+
const value = record[key];
|
|
479
|
+
if (value === undefined)
|
|
480
|
+
return;
|
|
481
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
482
|
+
issues.push({ path: `${path}.${key}`, message: "must be a non-empty http(s) URL string" });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
const url = new URL(value);
|
|
487
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
488
|
+
issues.push({ path: `${path}.${key}`, message: "must be an http(s) URL" });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
return url.toString();
|
|
492
|
+
} catch {
|
|
493
|
+
issues.push({ path: `${path}.${key}`, message: "must be a parseable http(s) URL" });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function isRecord(value) {
|
|
498
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
499
|
+
}
|
|
500
|
+
})(Plans ||= {});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// src/storage/account-subscriptions.ts
|
|
504
|
+
var AccountSubscriptionRepo;
|
|
505
|
+
var init_account_subscriptions = __esm(() => {
|
|
506
|
+
((AccountSubscriptionRepo) => {
|
|
507
|
+
function bind(db, cliproxyAccount, subscriptionCode) {
|
|
508
|
+
db.prepare(`
|
|
509
|
+
INSERT INTO account_subscriptions (
|
|
510
|
+
cliproxy_account, subscription_code, bound_at
|
|
511
|
+
) VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
512
|
+
ON CONFLICT(cliproxy_account) DO UPDATE SET
|
|
513
|
+
subscription_code = excluded.subscription_code,
|
|
514
|
+
bound_at = CURRENT_TIMESTAMP
|
|
515
|
+
`).run(cliproxyAccount, subscriptionCode);
|
|
516
|
+
}
|
|
517
|
+
AccountSubscriptionRepo.bind = bind;
|
|
518
|
+
function unbind(db, cliproxyAccount) {
|
|
519
|
+
db.prepare("DELETE FROM account_subscriptions WHERE cliproxy_account = ?").run(cliproxyAccount);
|
|
520
|
+
}
|
|
521
|
+
AccountSubscriptionRepo.unbind = unbind;
|
|
522
|
+
function get(db, cliproxyAccount) {
|
|
523
|
+
return db.prepare(`
|
|
524
|
+
SELECT cliproxy_account, subscription_code, bound_at
|
|
525
|
+
FROM account_subscriptions
|
|
526
|
+
WHERE cliproxy_account = ?
|
|
527
|
+
`).get(cliproxyAccount);
|
|
528
|
+
}
|
|
529
|
+
AccountSubscriptionRepo.get = get;
|
|
530
|
+
function list(db) {
|
|
531
|
+
return db.prepare(`
|
|
532
|
+
SELECT cliproxy_account, subscription_code, bound_at
|
|
533
|
+
FROM account_subscriptions
|
|
534
|
+
ORDER BY cliproxy_account ASC
|
|
535
|
+
`).all();
|
|
536
|
+
}
|
|
537
|
+
AccountSubscriptionRepo.list = list;
|
|
538
|
+
})(AccountSubscriptionRepo ||= {});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// src/storage/db.ts
|
|
542
|
+
var exports_db = {};
|
|
543
|
+
__export(exports_db, {
|
|
544
|
+
Storage: () => Storage,
|
|
545
|
+
STALE_PENDING_MAX_AGE_MS: () => STALE_PENDING_MAX_AGE_MS
|
|
546
|
+
});
|
|
547
|
+
import { Database } from "bun:sqlite";
|
|
548
|
+
import { readFileSync as readFileSync2, readdirSync } from "fs";
|
|
549
|
+
import { join as join2 } from "path";
|
|
550
|
+
function parseStalePendingMaxAgeMs(raw) {
|
|
551
|
+
if (raw === undefined)
|
|
552
|
+
return 600000;
|
|
553
|
+
const parsed = Number(raw);
|
|
554
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
555
|
+
return 600000;
|
|
556
|
+
return parsed;
|
|
557
|
+
}
|
|
558
|
+
var logger, STALE_PENDING_MAX_AGE_MS, Storage;
|
|
559
|
+
var init_db = __esm(() => {
|
|
560
|
+
init_logger();
|
|
561
|
+
logger = Logger.fromConfig().child({ component: "storage-db" });
|
|
562
|
+
STALE_PENDING_MAX_AGE_MS = parseStalePendingMaxAgeMs(process.env.STALE_PENDING_MAX_AGE_MS);
|
|
563
|
+
((Storage) => {
|
|
564
|
+
function splitStatements(sql) {
|
|
565
|
+
const stripped = sql.replace(/^\s*--.*$/gm, "");
|
|
566
|
+
return stripped.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
567
|
+
}
|
|
568
|
+
function execSafe(db, statement) {
|
|
569
|
+
try {
|
|
570
|
+
db.exec(statement);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
573
|
+
const ignorable = msg.includes("duplicate column name") || msg.includes("already exists") || msg.includes("no such column") || statement.toUpperCase().includes("ADD COLUMN") && msg.includes("syntax error");
|
|
574
|
+
if (!ignorable)
|
|
575
|
+
throw err;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function ensureColumn(db, table, column, typeDef) {
|
|
579
|
+
const cols = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
580
|
+
if (cols.some((c) => c.name === column))
|
|
581
|
+
return;
|
|
582
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${typeDef}`);
|
|
583
|
+
}
|
|
584
|
+
function initDb(dbPath) {
|
|
585
|
+
const db = new Database(dbPath);
|
|
586
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
587
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
588
|
+
db.exec(`
|
|
589
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
590
|
+
name TEXT PRIMARY KEY,
|
|
591
|
+
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
592
|
+
)
|
|
593
|
+
`);
|
|
594
|
+
const migrationsDir = join2(import.meta.dir, "migrations");
|
|
595
|
+
const files = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
|
|
596
|
+
for (const file of files) {
|
|
597
|
+
const applied = db.prepare("SELECT name FROM schema_migrations WHERE name = ?").get(file);
|
|
598
|
+
if (applied)
|
|
599
|
+
continue;
|
|
600
|
+
const sql = readFileSync2(join2(migrationsDir, file), "utf-8");
|
|
601
|
+
const txn = db.transaction(() => {
|
|
602
|
+
for (const stmt of splitStatements(sql)) {
|
|
603
|
+
execSafe(db, stmt);
|
|
604
|
+
}
|
|
605
|
+
db.prepare("INSERT INTO schema_migrations (name) VALUES (?)").run(file);
|
|
606
|
+
});
|
|
607
|
+
try {
|
|
608
|
+
txn();
|
|
609
|
+
} catch (err) {
|
|
610
|
+
logger.error("migration failed", { err, file });
|
|
611
|
+
throw err;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
ensureColumn(db, "request_logs", "cliproxy_account", "TEXT");
|
|
615
|
+
ensureColumn(db, "request_logs", "cliproxy_auth_index", "TEXT");
|
|
616
|
+
ensureColumn(db, "request_logs", "cliproxy_source", "TEXT");
|
|
617
|
+
ensureColumn(db, "request_logs", "request_id", "TEXT");
|
|
618
|
+
ensureColumn(db, "request_logs", "reasoning_tokens", "INTEGER DEFAULT 0");
|
|
619
|
+
ensureColumn(db, "request_logs", "actual_model", "TEXT");
|
|
620
|
+
ensureColumn(db, "request_logs", "user_agent", "TEXT");
|
|
621
|
+
ensureColumn(db, "request_logs", "source_ip", "TEXT");
|
|
622
|
+
ensureColumn(db, "request_logs", "correlated_at", "TEXT");
|
|
623
|
+
ensureColumn(db, "request_logs", "agent", "TEXT");
|
|
624
|
+
ensureColumn(db, "request_logs", "source", "TEXT DEFAULT 'proxy'");
|
|
625
|
+
ensureColumn(db, "request_logs", "msg_id", "TEXT");
|
|
626
|
+
ensureColumn(db, "request_logs", "lifecycle_status", "TEXT NOT NULL DEFAULT 'pending' CHECK(lifecycle_status IN ('pending', 'completed', 'error', 'aborted'))");
|
|
627
|
+
ensureColumn(db, "request_logs", "cost_status", "TEXT NOT NULL DEFAULT 'unresolved' CHECK(cost_status IN ('unresolved', 'ok', 'pending', 'unsupported'))");
|
|
628
|
+
ensureColumn(db, "request_logs", "subscription_code", "TEXT");
|
|
629
|
+
ensureColumn(db, "request_logs", "finalized_at", "TEXT");
|
|
630
|
+
ensureColumn(db, "request_logs", "error_message", "TEXT");
|
|
631
|
+
db.exec(`
|
|
632
|
+
UPDATE request_logs
|
|
633
|
+
SET lifecycle_status = CASE
|
|
634
|
+
WHEN incomplete = 1
|
|
635
|
+
OR error_code IS NOT NULL
|
|
636
|
+
OR status >= 400 THEN 'error'
|
|
637
|
+
ELSE 'completed'
|
|
638
|
+
END,
|
|
639
|
+
finalized_at = COALESCE(finalized_at, finished_at, started_at),
|
|
640
|
+
cost_status = CASE
|
|
641
|
+
WHEN cost_usd > 0 THEN 'ok'
|
|
642
|
+
ELSE 'pending'
|
|
643
|
+
END
|
|
644
|
+
WHERE lifecycle_status = 'pending'
|
|
645
|
+
AND (finalized_at IS NULL OR cost_status = 'unresolved')
|
|
646
|
+
AND (finished_at IS NOT NULL OR incomplete = 1 OR error_code IS NOT NULL OR status IS NOT NULL)
|
|
647
|
+
`);
|
|
648
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_request_logs_cliproxy_account ON request_logs(cliproxy_account)");
|
|
649
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_request_logs_cliproxy_auth_index ON request_logs(cliproxy_auth_index)");
|
|
650
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_request_logs_request_id ON request_logs(request_id)");
|
|
651
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_request_logs_msg_id ON request_logs(msg_id) WHERE msg_id IS NOT NULL");
|
|
652
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_request_logs_lifecycle_status ON request_logs(lifecycle_status)");
|
|
653
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_request_logs_cost_status ON request_logs(cost_status)");
|
|
654
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_request_logs_subscription_code ON request_logs(subscription_code) WHERE subscription_code IS NOT NULL");
|
|
655
|
+
db.exec(`
|
|
656
|
+
CREATE TABLE IF NOT EXISTS cost_audit (
|
|
657
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
658
|
+
request_log_id INTEGER,
|
|
659
|
+
model TEXT,
|
|
660
|
+
provider TEXT,
|
|
661
|
+
source TEXT,
|
|
662
|
+
base_cost_usd REAL,
|
|
663
|
+
calc_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
664
|
+
FOREIGN KEY (request_log_id) REFERENCES request_logs(id)
|
|
665
|
+
)
|
|
666
|
+
`);
|
|
667
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_cost_audit_request_log_id ON cost_audit(request_log_id)");
|
|
668
|
+
db.exec(`
|
|
669
|
+
CREATE TABLE IF NOT EXISTS quota_snapshots (
|
|
670
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
671
|
+
timestamp TEXT NOT NULL,
|
|
672
|
+
provider TEXT NOT NULL,
|
|
673
|
+
account TEXT NOT NULL,
|
|
674
|
+
quota_type TEXT NOT NULL,
|
|
675
|
+
used_pct REAL,
|
|
676
|
+
remaining REAL,
|
|
677
|
+
remaining_raw TEXT,
|
|
678
|
+
resets_at TEXT,
|
|
679
|
+
raw_json TEXT
|
|
680
|
+
)
|
|
681
|
+
`);
|
|
682
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_quota_snapshots_provider ON quota_snapshots(provider, account, timestamp)");
|
|
683
|
+
db.exec(`
|
|
684
|
+
CREATE TABLE IF NOT EXISTS daily_account_usage (
|
|
685
|
+
day TEXT NOT NULL,
|
|
686
|
+
provider TEXT NOT NULL,
|
|
687
|
+
model TEXT NOT NULL,
|
|
688
|
+
cliproxy_account TEXT NOT NULL,
|
|
689
|
+
cliproxy_auth_index TEXT,
|
|
690
|
+
request_count INTEGER DEFAULT 0,
|
|
691
|
+
prompt_tokens INTEGER DEFAULT 0,
|
|
692
|
+
completion_tokens INTEGER DEFAULT 0,
|
|
693
|
+
cache_creation_tokens INTEGER DEFAULT 0,
|
|
694
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
695
|
+
reasoning_tokens INTEGER DEFAULT 0,
|
|
696
|
+
total_tokens INTEGER DEFAULT 0,
|
|
697
|
+
cost_usd REAL DEFAULT 0,
|
|
698
|
+
PRIMARY KEY (day, provider, model, cliproxy_account)
|
|
699
|
+
)
|
|
700
|
+
`);
|
|
701
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_daily_account_usage_day ON daily_account_usage(day)");
|
|
702
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_daily_account_usage_account ON daily_account_usage(cliproxy_account)");
|
|
703
|
+
return db;
|
|
704
|
+
}
|
|
705
|
+
Storage.initDb = initDb;
|
|
706
|
+
function recoverStalePending(db, maxAgeMs = STALE_PENDING_MAX_AGE_MS) {
|
|
707
|
+
const now = new Date().toISOString();
|
|
708
|
+
const threshold = new Date(Date.now() - maxAgeMs).toISOString();
|
|
709
|
+
const stmt = db.prepare(`
|
|
710
|
+
UPDATE request_logs
|
|
711
|
+
SET lifecycle_status = 'aborted',
|
|
712
|
+
error_message = 'boot-recovery',
|
|
713
|
+
finalized_at = ?,
|
|
714
|
+
finished_at = COALESCE(finished_at, ?),
|
|
715
|
+
incomplete = 1,
|
|
716
|
+
cost_status = CASE
|
|
717
|
+
WHEN cost_status = 'unresolved' THEN 'pending'
|
|
718
|
+
ELSE cost_status
|
|
719
|
+
END
|
|
720
|
+
WHERE lifecycle_status = 'pending'
|
|
721
|
+
AND started_at < ?
|
|
722
|
+
`);
|
|
723
|
+
const result = stmt.run(now, now, threshold);
|
|
724
|
+
const recovered = result.changes;
|
|
725
|
+
if (recovered > 0) {
|
|
726
|
+
logger.warn("recovered stale pending request logs", {
|
|
727
|
+
event: "lifecycle.boot_recovery",
|
|
728
|
+
recovered,
|
|
729
|
+
max_age_ms: maxAgeMs,
|
|
730
|
+
threshold
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
return recovered;
|
|
734
|
+
}
|
|
735
|
+
Storage.recoverStalePending = recoverStalePending;
|
|
736
|
+
})(Storage ||= {});
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// src/provider/registry-schema.ts
|
|
740
|
+
function parseProviderInput(value, path = "provider") {
|
|
741
|
+
const issues = [];
|
|
742
|
+
const provider = normalizeProvider(value, path, issues);
|
|
743
|
+
return {
|
|
744
|
+
ok: issues.length === 0 && provider !== undefined,
|
|
745
|
+
provider: issues.length === 0 ? provider : undefined,
|
|
746
|
+
issues
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function validateProviderDocument(value) {
|
|
750
|
+
const issues = [];
|
|
751
|
+
const providers = [];
|
|
752
|
+
if (!isRecord(value)) {
|
|
753
|
+
issues.push({ path: "providers", message: "must be contained in an object" });
|
|
754
|
+
return { providers, issues };
|
|
755
|
+
}
|
|
756
|
+
if (!Array.isArray(value.providers)) {
|
|
757
|
+
issues.push({ path: "providers", message: "must be an array" });
|
|
758
|
+
return { providers, issues };
|
|
759
|
+
}
|
|
760
|
+
value.providers.forEach((entry, index) => {
|
|
761
|
+
const result = parseProviderInput(entry, `providers[${index}]`);
|
|
762
|
+
if (result.provider)
|
|
763
|
+
providers.push(result.provider);
|
|
764
|
+
issues.push(...result.issues);
|
|
765
|
+
});
|
|
766
|
+
return { providers, issues };
|
|
767
|
+
}
|
|
768
|
+
function normalizeProvider(value, path, issues) {
|
|
769
|
+
if (!isRecord(value)) {
|
|
770
|
+
issues.push({ path, message: "must be an object" });
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const id = readRequiredString(value, "id", path, issues);
|
|
774
|
+
const type = readProviderType(value.type, `${path}.type`, issues);
|
|
775
|
+
const paths = readRequiredStringArray(value, "paths", path, issues);
|
|
776
|
+
const upstreamBaseUrl = readRequiredHttpUrl(value.upstreamBaseUrl, `${path}.upstreamBaseUrl`, issues);
|
|
777
|
+
const upstreamPath = readOptionalString(value, "upstreamPath", path, issues);
|
|
778
|
+
const models = readOptionalStringArray(value, "models", path, issues);
|
|
779
|
+
const headers = readOptionalHeaders(value, path, issues);
|
|
780
|
+
const auth = readOptionalAuth(value.auth, `${path}.auth`, issues);
|
|
781
|
+
const stripProviderField = readOptionalBoolean(value, "stripProviderField", path, issues);
|
|
782
|
+
if (!id || !type || paths.length === 0 || !upstreamBaseUrl)
|
|
783
|
+
return;
|
|
784
|
+
const provider = {
|
|
785
|
+
id,
|
|
786
|
+
type,
|
|
787
|
+
paths,
|
|
788
|
+
upstreamBaseUrl
|
|
789
|
+
};
|
|
790
|
+
if (upstreamPath !== undefined)
|
|
791
|
+
provider.upstreamPath = upstreamPath;
|
|
792
|
+
if (models !== undefined)
|
|
793
|
+
provider.models = models;
|
|
794
|
+
if (headers !== undefined)
|
|
795
|
+
provider.headers = headers;
|
|
796
|
+
if (auth !== undefined)
|
|
797
|
+
provider.auth = auth;
|
|
798
|
+
if (stripProviderField !== undefined)
|
|
799
|
+
provider.stripProviderField = stripProviderField;
|
|
800
|
+
return provider;
|
|
801
|
+
}
|
|
802
|
+
function readRequiredString(record, key, path, issues) {
|
|
803
|
+
const value = record[key];
|
|
804
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
805
|
+
issues.push({ path: `${path}.${key}`, message: "must be a non-empty string" });
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
return value.trim();
|
|
809
|
+
}
|
|
810
|
+
function readProviderType(value, path, issues) {
|
|
811
|
+
if (typeof value !== "string" || !ALLOWED_PROVIDER_TYPES.has(value)) {
|
|
812
|
+
issues.push({ path, message: `must be one of ${Array.from(ALLOWED_PROVIDER_TYPES).join(", ")}` });
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
return value;
|
|
816
|
+
}
|
|
817
|
+
function readRequiredStringArray(record, key, path, issues) {
|
|
818
|
+
const value = record[key];
|
|
819
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
820
|
+
issues.push({ path: `${path}.${key}`, message: "must be a non-empty string array" });
|
|
821
|
+
return [];
|
|
822
|
+
}
|
|
823
|
+
const out = [];
|
|
824
|
+
value.forEach((entry, index) => {
|
|
825
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
826
|
+
issues.push({ path: `${path}.${key}[${index}]`, message: "must be a non-empty string" });
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
out.push(entry.trim());
|
|
830
|
+
});
|
|
831
|
+
return out;
|
|
832
|
+
}
|
|
833
|
+
function readRequiredHttpUrl(value, path, issues) {
|
|
834
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
835
|
+
issues.push({ path, message: "must be a non-empty http(s) URL string" });
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
return normalizeHttpUrl(value, path, issues);
|
|
839
|
+
}
|
|
840
|
+
function normalizeHttpUrl(raw, path, issues) {
|
|
841
|
+
try {
|
|
842
|
+
const parsed = new URL(raw);
|
|
843
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
844
|
+
issues.push({ path, message: "must be an http(s) URL" });
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
848
|
+
} catch {
|
|
849
|
+
issues.push({ path, message: "must be a parseable http(s) URL" });
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function readOptionalString(record, key, path, issues) {
|
|
854
|
+
const value = record[key];
|
|
855
|
+
if (value === undefined)
|
|
856
|
+
return;
|
|
857
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
858
|
+
issues.push({ path: `${path}.${key}`, message: "must be a non-empty string" });
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
return value.trim();
|
|
862
|
+
}
|
|
863
|
+
function readOptionalStringArray(record, key, path, issues) {
|
|
864
|
+
const value = record[key];
|
|
865
|
+
if (value === undefined)
|
|
866
|
+
return;
|
|
867
|
+
if (!Array.isArray(value)) {
|
|
868
|
+
issues.push({ path: `${path}.${key}`, message: "must be a string array" });
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
const out = [];
|
|
872
|
+
value.forEach((entry, index) => {
|
|
873
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
874
|
+
issues.push({ path: `${path}.${key}[${index}]`, message: "must be a non-empty string" });
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
out.push(entry.trim());
|
|
878
|
+
});
|
|
879
|
+
return out;
|
|
880
|
+
}
|
|
881
|
+
function readOptionalBoolean(record, key, path, issues) {
|
|
882
|
+
const value = record[key];
|
|
883
|
+
if (value === undefined)
|
|
884
|
+
return;
|
|
885
|
+
if (typeof value !== "boolean") {
|
|
886
|
+
issues.push({ path: `${path}.${key}`, message: "must be a boolean" });
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
return value;
|
|
890
|
+
}
|
|
891
|
+
function readOptionalHeaders(provider, path, issues) {
|
|
892
|
+
const value = provider.headers;
|
|
893
|
+
if (value === undefined)
|
|
894
|
+
return;
|
|
895
|
+
if (!isRecord(value)) {
|
|
896
|
+
issues.push({ path: `${path}.headers`, message: "must be an object" });
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const headers = {};
|
|
900
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
901
|
+
if (key.trim() === "") {
|
|
902
|
+
issues.push({ path: `${path}.headers`, message: "must not contain empty header names" });
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (typeof entry !== "string") {
|
|
906
|
+
issues.push({ path: `${path}.headers.${key}`, message: "must be a string" });
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
headers[key] = entry;
|
|
910
|
+
}
|
|
911
|
+
return headers;
|
|
912
|
+
}
|
|
913
|
+
function readOptionalAuth(auth, path, issues) {
|
|
914
|
+
if (auth === undefined)
|
|
915
|
+
return;
|
|
916
|
+
if (typeof auth === "string") {
|
|
917
|
+
if (!ALLOWED_AUTH_TYPES.has(auth)) {
|
|
918
|
+
issues.push({ path, message: `must be one of ${Array.from(ALLOWED_AUTH_TYPES).join(", ")}` });
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
return auth;
|
|
922
|
+
}
|
|
923
|
+
if (!isRecord(auth)) {
|
|
924
|
+
issues.push({ path, message: "must be a string or object" });
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
for (const key of Object.keys(auth)) {
|
|
928
|
+
if (!ALLOWED_AUTH_OBJECT_KEYS.has(key))
|
|
929
|
+
issues.push({ path: `${path}.${key}`, message: "is not supported" });
|
|
930
|
+
}
|
|
931
|
+
const type = auth.type;
|
|
932
|
+
if (typeof type !== "string" || type.trim() === "") {
|
|
933
|
+
issues.push({ path: `${path}.type`, message: "must be a non-empty string" });
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (!ALLOWED_AUTH_TYPES.has(type)) {
|
|
937
|
+
issues.push({ path: `${path}.type`, message: `must be one of ${Array.from(ALLOWED_AUTH_TYPES).join(", ")}` });
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const result = { type };
|
|
941
|
+
readAuthString(auth, "env", path, issues, result);
|
|
942
|
+
readAuthString(auth, "value", path, issues, result);
|
|
943
|
+
readAuthString(auth, "header", path, issues, result);
|
|
944
|
+
return result;
|
|
945
|
+
}
|
|
946
|
+
function readAuthString(auth, key, path, issues, result) {
|
|
947
|
+
const value = auth[key];
|
|
948
|
+
if (value === undefined)
|
|
949
|
+
return;
|
|
950
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
951
|
+
issues.push({ path: `${path}.${key}`, message: "must be a non-empty string" });
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
result[key] = value.trim();
|
|
955
|
+
}
|
|
956
|
+
function isRecord(value) {
|
|
957
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
958
|
+
}
|
|
959
|
+
var ALLOWED_PROVIDER_TYPES, ALLOWED_AUTH_TYPES, ALLOWED_AUTH_OBJECT_KEYS;
|
|
960
|
+
var init_registry_schema = __esm(() => {
|
|
961
|
+
ALLOWED_PROVIDER_TYPES = new Set(["openai-compatible", "anthropic"]);
|
|
962
|
+
ALLOWED_AUTH_TYPES = new Set(["none", "preserve", "bearer", "x-api-key"]);
|
|
963
|
+
ALLOWED_AUTH_OBJECT_KEYS = new Set(["type", "env", "value", "header"]);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// src/config/validate.ts
|
|
967
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
968
|
+
function readString(env, key, fallback) {
|
|
969
|
+
const value = env[key];
|
|
970
|
+
return value === undefined ? fallback : value;
|
|
971
|
+
}
|
|
972
|
+
function readPort(env, issues) {
|
|
973
|
+
const raw = env.PROXY_PORT ?? "3100";
|
|
974
|
+
const parsed = Number(raw);
|
|
975
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
976
|
+
issues.push({ path: "PROXY_PORT", message: "must be an integer from 1 to 65535" });
|
|
977
|
+
return 3100;
|
|
978
|
+
}
|
|
979
|
+
return parsed;
|
|
980
|
+
}
|
|
981
|
+
function readPositiveNumber(env, key, fallback, issues) {
|
|
982
|
+
const raw = env[key];
|
|
983
|
+
if (raw === undefined)
|
|
984
|
+
return fallback;
|
|
985
|
+
const parsed = Number(raw);
|
|
986
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
987
|
+
issues.push({ path: key, message: "must be a positive finite number" });
|
|
988
|
+
return fallback;
|
|
989
|
+
}
|
|
990
|
+
return parsed;
|
|
991
|
+
}
|
|
992
|
+
function readRequiredUrl(env, key, issues, warnings) {
|
|
993
|
+
const raw = env[key];
|
|
994
|
+
if (raw === undefined || raw.trim() === "") {
|
|
995
|
+
if (env.PROXY_LOCAL_OK === "1") {
|
|
996
|
+
warnings.push({
|
|
997
|
+
path: key,
|
|
998
|
+
message: `defaulted to ${DEFAULT_CLI_PROXY_API_URL} because PROXY_LOCAL_OK=1`
|
|
999
|
+
});
|
|
1000
|
+
return DEFAULT_CLI_PROXY_API_URL;
|
|
1001
|
+
}
|
|
1002
|
+
issues.push({ path: key, message: "is required unless PROXY_LOCAL_OK=1 permits the local default" });
|
|
1003
|
+
return DEFAULT_CLI_PROXY_API_URL;
|
|
1004
|
+
}
|
|
1005
|
+
return normalizeHttpUrl2(raw, key, issues) ?? raw;
|
|
1006
|
+
}
|
|
1007
|
+
function normalizeHttpUrl2(raw, path, issues) {
|
|
1008
|
+
try {
|
|
1009
|
+
const parsed = new URL(raw);
|
|
1010
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1011
|
+
issues.push({ path, message: "must be an http(s) URL" });
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
1015
|
+
} catch {
|
|
1016
|
+
issues.push({ path, message: "must be a parseable http(s) URL" });
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
function readCchPositions(env, issues) {
|
|
1021
|
+
const raw = env.CCH_POSITIONS ?? "[4,7,20]";
|
|
1022
|
+
let parsed;
|
|
1023
|
+
try {
|
|
1024
|
+
parsed = JSON.parse(raw);
|
|
1025
|
+
} catch {
|
|
1026
|
+
issues.push({ path: "CCH_POSITIONS", message: "must be JSON array of finite non-negative integers" });
|
|
1027
|
+
return [4, 7, 20];
|
|
1028
|
+
}
|
|
1029
|
+
if (!Array.isArray(parsed)) {
|
|
1030
|
+
issues.push({ path: "CCH_POSITIONS", message: "must be an array" });
|
|
1031
|
+
return [4, 7, 20];
|
|
1032
|
+
}
|
|
1033
|
+
parsed.forEach((value, index) => {
|
|
1034
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
1035
|
+
issues.push({ path: `CCH_POSITIONS[${index}]`, message: "must be a finite non-negative integer" });
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
return parsed.filter((value) => Number.isInteger(value) && value >= 0);
|
|
1039
|
+
}
|
|
1040
|
+
function readClientNameMapping(env, issues) {
|
|
1041
|
+
const mapping = new Map;
|
|
1042
|
+
const raw = env.CLIENT_NAME_MAPPING;
|
|
1043
|
+
if (raw === undefined || raw.trim() === "")
|
|
1044
|
+
return mapping;
|
|
1045
|
+
raw.split(",").forEach((entry, index) => {
|
|
1046
|
+
const pair = entry.trim();
|
|
1047
|
+
const splitAt = pair.indexOf("=");
|
|
1048
|
+
const key = splitAt >= 0 ? pair.slice(0, splitAt).trim() : "";
|
|
1049
|
+
const value = splitAt >= 0 ? pair.slice(splitAt + 1).trim() : "";
|
|
1050
|
+
if (!key || !value) {
|
|
1051
|
+
issues.push({ path: `CLIENT_NAME_MAPPING[${index}]`, message: "must be a non-empty key=value entry" });
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
mapping.set(key, value);
|
|
1055
|
+
});
|
|
1056
|
+
return mapping;
|
|
1057
|
+
}
|
|
1058
|
+
function validateProviderConfig(env, issues) {
|
|
1059
|
+
const inline = env.PROVIDERS_JSON;
|
|
1060
|
+
const filePath = env.PROVIDERS_CONFIG_PATH;
|
|
1061
|
+
if (inline !== undefined && inline.trim() !== "") {
|
|
1062
|
+
validateProviderJson(inline, "PROVIDERS_JSON", issues);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (filePath === undefined || filePath.trim() === "")
|
|
1066
|
+
return;
|
|
1067
|
+
try {
|
|
1068
|
+
validateProviderJson(readFileSync3(filePath, "utf-8"), "PROVIDERS_CONFIG_PATH", issues);
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
issues.push({
|
|
1071
|
+
path: "PROVIDERS_CONFIG_PATH",
|
|
1072
|
+
message: `could not be read: ${err instanceof Error ? err.message : String(err)}`
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
function validateProviderJson(raw, basePath, issues) {
|
|
1077
|
+
let parsed;
|
|
1078
|
+
try {
|
|
1079
|
+
parsed = JSON.parse(raw);
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
issues.push({
|
|
1082
|
+
path: basePath,
|
|
1083
|
+
message: `must be valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
1084
|
+
});
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
const result = validateProviderDocument(parsed);
|
|
1088
|
+
issues.push(...result.issues);
|
|
1089
|
+
}
|
|
1090
|
+
function isLoopbackHost(host) {
|
|
1091
|
+
return LOOPBACK_HOSTS.has(host.trim().toLowerCase());
|
|
1092
|
+
}
|
|
1093
|
+
var ConfigError, Config, DEFAULT_CLI_PROXY_API_URL = "http://localhost:8317", LOOPBACK_HOSTS;
|
|
1094
|
+
var init_validate = __esm(() => {
|
|
1095
|
+
init_registry_schema();
|
|
1096
|
+
ConfigError = class ConfigError extends Error {
|
|
1097
|
+
issues;
|
|
1098
|
+
name = "ConfigError";
|
|
1099
|
+
code = "CONFIG_INVALID";
|
|
1100
|
+
constructor(issues) {
|
|
1101
|
+
super(`Configuration validation failed: ${issues.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`);
|
|
1102
|
+
this.issues = issues;
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
((Config) => {
|
|
1106
|
+
function validate(env = process.env, options = {}) {
|
|
1107
|
+
const issues = [];
|
|
1108
|
+
const warnings = [];
|
|
1109
|
+
const host = readString(env, "PROXY_HOST", "127.0.0.1");
|
|
1110
|
+
const cliProxyApiUrl = readRequiredUrl(env, "CLI_PROXY_API_URL", issues, warnings);
|
|
1111
|
+
const config = {
|
|
1112
|
+
port: readPort(env, issues),
|
|
1113
|
+
host,
|
|
1114
|
+
adminApiKey: readString(env, "ADMIN_API_KEY", ""),
|
|
1115
|
+
cliProxyApiUrl,
|
|
1116
|
+
claudeCodeVersion: readString(env, "CLAUDE_CODE_VERSION", "2.1.87"),
|
|
1117
|
+
cchSalt: readString(env, "CCH_SALT", "59cf53e54c78"),
|
|
1118
|
+
cchPositions: readCchPositions(env, issues),
|
|
1119
|
+
toolPrefix: readString(env, "TOOL_PREFIX", "mcp_"),
|
|
1120
|
+
cliProxyApiKey: readString(env, "CLI_PROXY_API_KEY", "proxy"),
|
|
1121
|
+
dbPath: readString(env, "DB_PATH", "data/proxy.db"),
|
|
1122
|
+
pricingCacheTtlMs: readPositiveNumber(env, "PRICING_CACHE_TTL_MS", 3600000, issues),
|
|
1123
|
+
pricingCachePath: readString(env, "PRICING_CACHE_PATH", "data/pricing-cache.json"),
|
|
1124
|
+
readyPricingMaxAgeMs: readPositiveNumber(env, "READY_PRICING_MAX_AGE_MS", 86400000, issues),
|
|
1125
|
+
pricingRefreshIntervalMs: readPositiveNumber(env, "PRICING_REFRESH_INTERVAL_MS", 21600000, issues),
|
|
1126
|
+
costBackfillIntervalMs: readPositiveNumber(env, "COST_BACKFILL_INTERVAL_MS", 1800000, issues),
|
|
1127
|
+
costBackfillLookbackMs: readPositiveNumber(env, "COST_BACKFILL_LOOKBACK_MS", 604800000, issues),
|
|
1128
|
+
logLevel: readString(env, "LOG_LEVEL", "info"),
|
|
1129
|
+
clientNameMapping: readClientNameMapping(env, issues),
|
|
1130
|
+
cliproxyMgmtKey: readString(env, "CLIPROXY_MGMT_KEY", ""),
|
|
1131
|
+
cliproxyCorrelationIntervalMs: readPositiveNumber(env, "CLIPROXY_CORRELATION_INTERVAL_MS", 15000, issues),
|
|
1132
|
+
cliproxyCorrelationLookbackMs: readPositiveNumber(env, "CLIPROXY_CORRELATION_LOOKBACK_MS", 300000, issues),
|
|
1133
|
+
cliproxyAuthDir: readString(env, "CLIPROXY_AUTH_DIR", ""),
|
|
1134
|
+
quotaRefreshIntervalMs: readPositiveNumber(env, "QUOTA_REFRESH_INTERVAL_MS", 300000, issues),
|
|
1135
|
+
quotaRefreshTimeoutMs: readPositiveNumber(env, "QUOTA_REFRESH_TIMEOUT_MS", 15000, issues),
|
|
1136
|
+
upstreamTimeoutMs: readPositiveNumber(env, "UPSTREAM_TIMEOUT_MS", 300000, issues),
|
|
1137
|
+
upstreamConnectTimeoutMs: readPositiveNumber(env, "UPSTREAM_CONNECT_TIMEOUT_MS", 1e4, issues)
|
|
1138
|
+
};
|
|
1139
|
+
if (!isLoopbackHost(config.host) && !config.adminApiKey) {
|
|
1140
|
+
issues.push({
|
|
1141
|
+
path: "ADMIN_API_KEY",
|
|
1142
|
+
message: "is required when PROXY_HOST is not loopback"
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
validateProviderConfig(env, issues);
|
|
1146
|
+
if (issues.length > 0)
|
|
1147
|
+
throw new ConfigError(issues);
|
|
1148
|
+
for (const warning of warnings)
|
|
1149
|
+
options.onWarning?.(warning);
|
|
1150
|
+
return Object.freeze(config);
|
|
1151
|
+
}
|
|
1152
|
+
Config.validate = validate;
|
|
1153
|
+
})(Config ||= {});
|
|
1154
|
+
LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
// src/storage/repo.ts
|
|
1158
|
+
function parseLifecycleStatus(value) {
|
|
1159
|
+
if (value === "completed" || value === "error" || value === "aborted")
|
|
1160
|
+
return value;
|
|
1161
|
+
return "pending";
|
|
1162
|
+
}
|
|
1163
|
+
var RequestRepo, UsageRepo, QuotaRepo;
|
|
1164
|
+
var init_repo = __esm(() => {
|
|
1165
|
+
((RequestRepo) => {
|
|
1166
|
+
function insert(db, log) {
|
|
1167
|
+
const lifecycleStatus = log.lifecycle_status ?? (log.incomplete === 1 || log.error_code || (log.status ?? 0) >= 400 ? "error" : "completed");
|
|
1168
|
+
const costStatus = log.cost_status ?? (log.cost_usd > 0 ? "ok" : lifecycleStatus === "pending" ? "unresolved" : "pending");
|
|
1169
|
+
const finalizedAt = log.finalized_at ?? (lifecycleStatus === "pending" ? null : log.finished_at ?? log.started_at);
|
|
1170
|
+
const stmt = db.prepare(`
|
|
1171
|
+
INSERT INTO request_logs (
|
|
1172
|
+
request_id, provider, model, actual_model, tool, client_id, path,
|
|
1173
|
+
streamed, status, prompt_tokens, completion_tokens,
|
|
1174
|
+
cache_creation_tokens, cache_read_tokens, reasoning_tokens,
|
|
1175
|
+
total_tokens, cost_usd, incomplete, error_code, latency_ms,
|
|
1176
|
+
started_at, finished_at, meta_json, user_agent, source_ip,
|
|
1177
|
+
agent, source, msg_id, lifecycle_status, cost_status,
|
|
1178
|
+
subscription_code, finalized_at, error_message
|
|
1179
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1180
|
+
`);
|
|
1181
|
+
const result = stmt.run(log.request_id ?? null, log.provider, log.model, log.actual_model ?? null, log.tool, log.client_id, log.path, log.streamed, log.status ?? null, log.prompt_tokens, log.completion_tokens, log.cache_creation_tokens, log.cache_read_tokens, log.reasoning_tokens ?? 0, log.total_tokens, log.cost_usd, log.incomplete, log.error_code ?? null, log.latency_ms ?? null, log.started_at, log.finished_at ?? null, log.meta_json ?? null, log.user_agent ?? null, log.source_ip ?? null, log.agent ?? null, log.source ?? "proxy", log.msg_id ?? null, lifecycleStatus, costStatus, log.subscription_code ?? null, finalizedAt, log.error_message ?? null);
|
|
1182
|
+
return result.lastInsertRowid;
|
|
1183
|
+
}
|
|
1184
|
+
RequestRepo.insert = insert;
|
|
1185
|
+
function getRecent(db, limit, offset, tool, clientId) {
|
|
1186
|
+
let sql = `SELECT * FROM request_logs WHERE 1=1`;
|
|
1187
|
+
const params = [];
|
|
1188
|
+
if (tool) {
|
|
1189
|
+
sql += ` AND tool = ?`;
|
|
1190
|
+
params.push(tool);
|
|
1191
|
+
}
|
|
1192
|
+
if (clientId) {
|
|
1193
|
+
sql += ` AND client_id = ?`;
|
|
1194
|
+
params.push(clientId);
|
|
1195
|
+
}
|
|
1196
|
+
sql += ` ORDER BY started_at DESC LIMIT ? OFFSET ?`;
|
|
1197
|
+
params.push(limit, offset);
|
|
1198
|
+
const stmt = db.prepare(sql);
|
|
1199
|
+
return stmt.all(...params);
|
|
1200
|
+
}
|
|
1201
|
+
RequestRepo.getRecent = getRecent;
|
|
1202
|
+
function getById(db, id) {
|
|
1203
|
+
const stmt = db.prepare("SELECT * FROM request_logs WHERE id = ?");
|
|
1204
|
+
return stmt.get(id) || null;
|
|
1205
|
+
}
|
|
1206
|
+
RequestRepo.getById = getById;
|
|
1207
|
+
function aggregateByAccountForMonth(db, monthStart, monthEnd) {
|
|
1208
|
+
const stmt = db.prepare(`
|
|
1209
|
+
SELECT
|
|
1210
|
+
rl.cliproxy_account AS cliproxy_account,
|
|
1211
|
+
sub.subscription_code AS subscription_code,
|
|
1212
|
+
COUNT(*) AS total_requests,
|
|
1213
|
+
COALESCE(SUM(rl.cost_usd), 0) AS total_cost_usd
|
|
1214
|
+
FROM request_logs rl
|
|
1215
|
+
LEFT JOIN account_subscriptions sub
|
|
1216
|
+
ON sub.cliproxy_account = rl.cliproxy_account
|
|
1217
|
+
WHERE rl.lifecycle_status = 'completed'
|
|
1218
|
+
AND rl.started_at >= ?
|
|
1219
|
+
AND rl.started_at < ?
|
|
1220
|
+
AND rl.cliproxy_account IS NOT NULL
|
|
1221
|
+
AND rl.cliproxy_account <> ''
|
|
1222
|
+
GROUP BY rl.cliproxy_account, sub.subscription_code
|
|
1223
|
+
ORDER BY total_cost_usd DESC, rl.cliproxy_account ASC
|
|
1224
|
+
`);
|
|
1225
|
+
return stmt.all(monthStart, monthEnd).map((row) => {
|
|
1226
|
+
const record = row;
|
|
1227
|
+
return {
|
|
1228
|
+
cliproxy_account: String(record.cliproxy_account),
|
|
1229
|
+
subscription_code: typeof record.subscription_code === "string" ? record.subscription_code : null,
|
|
1230
|
+
total_requests: Number(record.total_requests ?? 0),
|
|
1231
|
+
total_cost_usd: Number(record.total_cost_usd ?? 0)
|
|
1232
|
+
};
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
RequestRepo.aggregateByAccountForMonth = aggregateByAccountForMonth;
|
|
1236
|
+
function getRecentByAccount(db, cliproxyAccount, limit) {
|
|
1237
|
+
const stmt = db.prepare(`
|
|
1238
|
+
SELECT started_at, model, total_tokens, cost_usd, lifecycle_status
|
|
1239
|
+
FROM request_logs
|
|
1240
|
+
WHERE cliproxy_account = ?
|
|
1241
|
+
ORDER BY started_at DESC
|
|
1242
|
+
LIMIT ?
|
|
1243
|
+
`);
|
|
1244
|
+
return stmt.all(cliproxyAccount, limit).map((row) => {
|
|
1245
|
+
const record = row;
|
|
1246
|
+
return {
|
|
1247
|
+
started_at: String(record.started_at),
|
|
1248
|
+
model: String(record.model),
|
|
1249
|
+
total_tokens: Number(record.total_tokens ?? 0),
|
|
1250
|
+
cost_usd: Number(record.cost_usd ?? 0),
|
|
1251
|
+
lifecycle_status: parseLifecycleStatus(record.lifecycle_status)
|
|
1252
|
+
};
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
RequestRepo.getRecentByAccount = getRecentByAccount;
|
|
1256
|
+
function getUncorrelated(db, sinceMs, limit) {
|
|
1257
|
+
const sinceIso = new Date(Date.now() - sinceMs).toISOString();
|
|
1258
|
+
const stmt = db.prepare(`
|
|
1259
|
+
SELECT * FROM request_logs
|
|
1260
|
+
WHERE cliproxy_account IS NULL
|
|
1261
|
+
AND status = 200
|
|
1262
|
+
AND started_at >= ?
|
|
1263
|
+
ORDER BY started_at DESC
|
|
1264
|
+
LIMIT ?
|
|
1265
|
+
`);
|
|
1266
|
+
return stmt.all(sinceIso, limit);
|
|
1267
|
+
}
|
|
1268
|
+
RequestRepo.getUncorrelated = getUncorrelated;
|
|
1269
|
+
function applyCorrelation(db, id, fields) {
|
|
1270
|
+
const stmt = db.prepare(`
|
|
1271
|
+
UPDATE request_logs
|
|
1272
|
+
SET cliproxy_account = COALESCE(?, cliproxy_account),
|
|
1273
|
+
cliproxy_auth_index = COALESCE(?, cliproxy_auth_index),
|
|
1274
|
+
cliproxy_source = COALESCE(?, cliproxy_source),
|
|
1275
|
+
reasoning_tokens = COALESCE(?, reasoning_tokens),
|
|
1276
|
+
actual_model = COALESCE(?, actual_model),
|
|
1277
|
+
correlated_at = ?
|
|
1278
|
+
WHERE id = ?
|
|
1279
|
+
`);
|
|
1280
|
+
stmt.run(fields.cliproxy_account ?? null, fields.cliproxy_auth_index ?? null, fields.cliproxy_source ?? null, fields.reasoning_tokens ?? null, fields.actual_model ?? null, new Date().toISOString(), id);
|
|
1281
|
+
}
|
|
1282
|
+
RequestRepo.applyCorrelation = applyCorrelation;
|
|
1283
|
+
function updateLifecycle(db, id, fields) {
|
|
1284
|
+
const stmt = db.prepare(`
|
|
1285
|
+
UPDATE request_logs
|
|
1286
|
+
SET lifecycle_status = COALESCE(?, lifecycle_status),
|
|
1287
|
+
finalized_at = COALESCE(?, finalized_at),
|
|
1288
|
+
error_message = COALESCE(?, error_message),
|
|
1289
|
+
cost_status = COALESCE(?, cost_status),
|
|
1290
|
+
subscription_code = COALESCE(?, subscription_code)
|
|
1291
|
+
WHERE id = ?
|
|
1292
|
+
`);
|
|
1293
|
+
stmt.run(fields.lifecycle_status ?? null, fields.finalized_at ?? null, fields.error_message ?? null, fields.cost_status ?? null, fields.subscription_code ?? null, id);
|
|
1294
|
+
}
|
|
1295
|
+
RequestRepo.updateLifecycle = updateLifecycle;
|
|
1296
|
+
function applySubscription(db, id, subscriptionCode) {
|
|
1297
|
+
db.prepare("UPDATE request_logs SET subscription_code = ? WHERE id = ?").run(subscriptionCode, id);
|
|
1298
|
+
}
|
|
1299
|
+
RequestRepo.applySubscription = applySubscription;
|
|
1300
|
+
function updateFinalize(db, id, fields) {
|
|
1301
|
+
const stmt = db.prepare(`
|
|
1302
|
+
UPDATE request_logs
|
|
1303
|
+
SET provider = COALESCE(?, provider),
|
|
1304
|
+
model = COALESCE(?, model),
|
|
1305
|
+
actual_model = COALESCE(?, actual_model),
|
|
1306
|
+
streamed = COALESCE(?, streamed),
|
|
1307
|
+
status = COALESCE(?, status),
|
|
1308
|
+
prompt_tokens = COALESCE(?, prompt_tokens),
|
|
1309
|
+
completion_tokens = COALESCE(?, completion_tokens),
|
|
1310
|
+
cache_creation_tokens = COALESCE(?, cache_creation_tokens),
|
|
1311
|
+
cache_read_tokens = COALESCE(?, cache_read_tokens),
|
|
1312
|
+
reasoning_tokens = COALESCE(?, reasoning_tokens),
|
|
1313
|
+
total_tokens = COALESCE(?, total_tokens),
|
|
1314
|
+
cost_usd = COALESCE(?, cost_usd),
|
|
1315
|
+
incomplete = COALESCE(?, incomplete),
|
|
1316
|
+
error_code = COALESCE(?, error_code),
|
|
1317
|
+
latency_ms = COALESCE(?, latency_ms),
|
|
1318
|
+
finished_at = COALESCE(?, finished_at),
|
|
1319
|
+
lifecycle_status = ?,
|
|
1320
|
+
finalized_at = ?,
|
|
1321
|
+
error_message = COALESCE(?, error_message),
|
|
1322
|
+
cost_status = ?,
|
|
1323
|
+
subscription_code = COALESCE(?, subscription_code)
|
|
1324
|
+
WHERE id = ? AND lifecycle_status = 'pending'
|
|
1325
|
+
`);
|
|
1326
|
+
const result = stmt.run(fields.provider ?? null, fields.model ?? null, fields.actual_model ?? null, fields.streamed ?? null, fields.status ?? null, fields.prompt_tokens ?? null, fields.completion_tokens ?? null, fields.cache_creation_tokens ?? null, fields.cache_read_tokens ?? null, fields.reasoning_tokens ?? null, fields.total_tokens ?? null, fields.cost_usd ?? null, fields.incomplete ?? null, fields.error_code ?? null, fields.latency_ms ?? null, fields.finished_at ?? null, fields.lifecycle_status, fields.finalized_at, fields.error_message ?? null, fields.cost_status, fields.subscription_code ?? null, id);
|
|
1327
|
+
return result.changes;
|
|
1328
|
+
}
|
|
1329
|
+
RequestRepo.updateFinalize = updateFinalize;
|
|
1330
|
+
function insertCostAudit(db, audit) {
|
|
1331
|
+
const stmt = db.prepare(`
|
|
1332
|
+
INSERT INTO cost_audit (
|
|
1333
|
+
request_log_id, model, provider, source, base_cost_usd, calc_at
|
|
1334
|
+
) VALUES (?, ?, ?, ?, ?, COALESCE(?, CURRENT_TIMESTAMP))
|
|
1335
|
+
`);
|
|
1336
|
+
const result = stmt.run(audit.request_log_id ?? null, audit.model ?? null, audit.provider ?? null, audit.source ?? null, audit.base_cost_usd ?? null, audit.calc_at ?? null);
|
|
1337
|
+
return result.lastInsertRowid;
|
|
1338
|
+
}
|
|
1339
|
+
RequestRepo.insertCostAudit = insertCostAudit;
|
|
1340
|
+
})(RequestRepo ||= {});
|
|
1341
|
+
((UsageRepo) => {
|
|
1342
|
+
function upsertDaily(db, usage) {
|
|
1343
|
+
const stmt = db.prepare(`
|
|
1344
|
+
INSERT INTO daily_usage (
|
|
1345
|
+
day, provider, model, request_count, prompt_tokens,
|
|
1346
|
+
completion_tokens, cache_creation_tokens, cache_read_tokens,
|
|
1347
|
+
total_tokens, cost_usd
|
|
1348
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1349
|
+
ON CONFLICT(day, provider, model) DO UPDATE SET
|
|
1350
|
+
request_count = request_count + excluded.request_count,
|
|
1351
|
+
prompt_tokens = prompt_tokens + excluded.prompt_tokens,
|
|
1352
|
+
completion_tokens = completion_tokens + excluded.completion_tokens,
|
|
1353
|
+
cache_creation_tokens = cache_creation_tokens + excluded.cache_creation_tokens,
|
|
1354
|
+
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
1355
|
+
total_tokens = total_tokens + excluded.total_tokens,
|
|
1356
|
+
cost_usd = cost_usd + excluded.cost_usd
|
|
1357
|
+
`);
|
|
1358
|
+
stmt.run(usage.day, usage.provider, usage.model, usage.request_count, usage.prompt_tokens, usage.completion_tokens, usage.cache_creation_tokens, usage.cache_read_tokens, usage.total_tokens, usage.cost_usd);
|
|
1359
|
+
}
|
|
1360
|
+
UsageRepo.upsertDaily = upsertDaily;
|
|
1361
|
+
function upsertDailyAccount(db, usage) {
|
|
1362
|
+
const stmt = db.prepare(`
|
|
1363
|
+
INSERT INTO daily_account_usage (
|
|
1364
|
+
day, provider, model, cliproxy_account, cliproxy_auth_index,
|
|
1365
|
+
request_count, prompt_tokens, completion_tokens,
|
|
1366
|
+
cache_creation_tokens, cache_read_tokens, reasoning_tokens,
|
|
1367
|
+
total_tokens, cost_usd
|
|
1368
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1369
|
+
ON CONFLICT(day, provider, model, cliproxy_account) DO UPDATE SET
|
|
1370
|
+
cliproxy_auth_index = COALESCE(excluded.cliproxy_auth_index, cliproxy_auth_index),
|
|
1371
|
+
request_count = request_count + excluded.request_count,
|
|
1372
|
+
prompt_tokens = prompt_tokens + excluded.prompt_tokens,
|
|
1373
|
+
completion_tokens = completion_tokens + excluded.completion_tokens,
|
|
1374
|
+
cache_creation_tokens = cache_creation_tokens + excluded.cache_creation_tokens,
|
|
1375
|
+
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
1376
|
+
reasoning_tokens = reasoning_tokens + excluded.reasoning_tokens,
|
|
1377
|
+
total_tokens = total_tokens + excluded.total_tokens,
|
|
1378
|
+
cost_usd = cost_usd + excluded.cost_usd
|
|
1379
|
+
`);
|
|
1380
|
+
stmt.run(usage.day, usage.provider, usage.model, usage.cliproxy_account, usage.cliproxy_auth_index ?? null, usage.request_count, usage.prompt_tokens, usage.completion_tokens, usage.cache_creation_tokens, usage.cache_read_tokens, usage.reasoning_tokens, usage.total_tokens, usage.cost_usd);
|
|
1381
|
+
}
|
|
1382
|
+
UsageRepo.upsertDailyAccount = upsertDailyAccount;
|
|
1383
|
+
function getDaily(db, day) {
|
|
1384
|
+
const stmt = db.prepare(`
|
|
1385
|
+
SELECT * FROM daily_usage
|
|
1386
|
+
WHERE day = ?
|
|
1387
|
+
ORDER BY provider, model
|
|
1388
|
+
`);
|
|
1389
|
+
return stmt.all(day);
|
|
1390
|
+
}
|
|
1391
|
+
UsageRepo.getDaily = getDaily;
|
|
1392
|
+
function getDailyByAccount(db, day) {
|
|
1393
|
+
const stmt = db.prepare(`
|
|
1394
|
+
SELECT * FROM daily_account_usage
|
|
1395
|
+
WHERE day = ?
|
|
1396
|
+
ORDER BY cliproxy_account, provider, model
|
|
1397
|
+
`);
|
|
1398
|
+
return stmt.all(day);
|
|
1399
|
+
}
|
|
1400
|
+
UsageRepo.getDailyByAccount = getDailyByAccount;
|
|
1401
|
+
function getRange(db, from, to) {
|
|
1402
|
+
const stmt = db.prepare(`
|
|
1403
|
+
SELECT * FROM daily_usage
|
|
1404
|
+
WHERE day >= ? AND day <= ?
|
|
1405
|
+
ORDER BY day DESC, provider, model
|
|
1406
|
+
`);
|
|
1407
|
+
return stmt.all(from, to);
|
|
1408
|
+
}
|
|
1409
|
+
UsageRepo.getRange = getRange;
|
|
1410
|
+
function getAccountRange(db, from, to) {
|
|
1411
|
+
const stmt = db.prepare(`
|
|
1412
|
+
SELECT * FROM daily_account_usage
|
|
1413
|
+
WHERE day >= ? AND day <= ?
|
|
1414
|
+
ORDER BY day DESC, cliproxy_account, provider, model
|
|
1415
|
+
`);
|
|
1416
|
+
return stmt.all(from, to);
|
|
1417
|
+
}
|
|
1418
|
+
UsageRepo.getAccountRange = getAccountRange;
|
|
1419
|
+
function getAccountSummary(db, from, to) {
|
|
1420
|
+
const stmt = db.prepare(`
|
|
1421
|
+
SELECT
|
|
1422
|
+
cliproxy_account,
|
|
1423
|
+
cliproxy_auth_index,
|
|
1424
|
+
provider,
|
|
1425
|
+
SUM(request_count) AS request_count,
|
|
1426
|
+
SUM(total_tokens) AS total_tokens,
|
|
1427
|
+
SUM(cost_usd) AS cost_usd
|
|
1428
|
+
FROM daily_account_usage
|
|
1429
|
+
WHERE day >= ? AND day <= ?
|
|
1430
|
+
GROUP BY cliproxy_account, cliproxy_auth_index, provider
|
|
1431
|
+
ORDER BY cost_usd DESC
|
|
1432
|
+
`);
|
|
1433
|
+
return stmt.all(from, to);
|
|
1434
|
+
}
|
|
1435
|
+
UsageRepo.getAccountSummary = getAccountSummary;
|
|
1436
|
+
})(UsageRepo ||= {});
|
|
1437
|
+
((QuotaRepo) => {
|
|
1438
|
+
function insertSnapshot(db, snapshot) {
|
|
1439
|
+
const stmt = db.prepare(`
|
|
1440
|
+
INSERT INTO quota_snapshots (
|
|
1441
|
+
timestamp, provider, account, quota_type, used_pct,
|
|
1442
|
+
remaining, remaining_raw, resets_at, raw_json
|
|
1443
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1444
|
+
`);
|
|
1445
|
+
const result = stmt.run(snapshot.timestamp, snapshot.provider, snapshot.account, snapshot.quota_type, snapshot.used_pct ?? null, snapshot.remaining ?? null, snapshot.remaining_raw ?? null, snapshot.resets_at ?? null, snapshot.raw_json ?? null);
|
|
1446
|
+
return result.lastInsertRowid;
|
|
1447
|
+
}
|
|
1448
|
+
QuotaRepo.insertSnapshot = insertSnapshot;
|
|
1449
|
+
function getLatest(db) {
|
|
1450
|
+
const stmt = db.prepare(`
|
|
1451
|
+
SELECT q.*
|
|
1452
|
+
FROM quota_snapshots q
|
|
1453
|
+
JOIN (
|
|
1454
|
+
SELECT provider, account, quota_type, MAX(timestamp) AS max_timestamp
|
|
1455
|
+
FROM quota_snapshots
|
|
1456
|
+
GROUP BY provider, account, quota_type
|
|
1457
|
+
) latest
|
|
1458
|
+
ON latest.provider = q.provider
|
|
1459
|
+
AND latest.account = q.account
|
|
1460
|
+
AND latest.quota_type = q.quota_type
|
|
1461
|
+
AND latest.max_timestamp = q.timestamp
|
|
1462
|
+
ORDER BY q.provider, q.account, q.quota_type
|
|
1463
|
+
`);
|
|
1464
|
+
return stmt.all();
|
|
1465
|
+
}
|
|
1466
|
+
QuotaRepo.getLatest = getLatest;
|
|
1467
|
+
function getLocalWindowUsage(db, provider, account, sinceIso) {
|
|
1468
|
+
const row = db.prepare(`
|
|
1469
|
+
SELECT
|
|
1470
|
+
COUNT(*) AS requests,
|
|
1471
|
+
COALESCE(SUM(total_tokens), 0) AS total_tokens,
|
|
1472
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd
|
|
1473
|
+
FROM request_logs
|
|
1474
|
+
WHERE provider = ?
|
|
1475
|
+
AND cliproxy_account = ?
|
|
1476
|
+
AND started_at >= ?
|
|
1477
|
+
`).get(provider, account, sinceIso);
|
|
1478
|
+
return {
|
|
1479
|
+
since: sinceIso,
|
|
1480
|
+
requests: Number(row.requests ?? 0),
|
|
1481
|
+
total_tokens: Number(row.total_tokens ?? 0),
|
|
1482
|
+
cost_usd: Number(row.cost_usd ?? 0)
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
QuotaRepo.getLocalWindowUsage = getLocalWindowUsage;
|
|
1486
|
+
})(QuotaRepo ||= {});
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
// src/config/index.ts
|
|
1490
|
+
var exports_config = {};
|
|
1491
|
+
__export(exports_config, {
|
|
1492
|
+
ConfigError: () => ConfigError,
|
|
1493
|
+
Config: () => Config2
|
|
1494
|
+
});
|
|
1495
|
+
var configLogger, validated, Config2;
|
|
1496
|
+
var init_config = __esm(() => {
|
|
1497
|
+
init_logger();
|
|
1498
|
+
init_validate();
|
|
1499
|
+
init_validate();
|
|
1500
|
+
configLogger = Logger.fromConfig().child({ component: "config" });
|
|
1501
|
+
validated = Config.validate(process.env, {
|
|
1502
|
+
onWarning(issue) {
|
|
1503
|
+
configLogger.warn("configuration warning", { event: "config.warning", ...issue });
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
Config2 = Object.freeze({
|
|
1507
|
+
...validated,
|
|
1508
|
+
validate: Config.validate
|
|
1509
|
+
});
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
// src/runtime/supervisor.ts
|
|
1513
|
+
var exports_supervisor = {};
|
|
1514
|
+
__export(exports_supervisor, {
|
|
1515
|
+
Supervisor: () => Supervisor
|
|
1516
|
+
});
|
|
1517
|
+
var Supervisor;
|
|
1518
|
+
var init_supervisor = __esm(() => {
|
|
1519
|
+
init_logger();
|
|
1520
|
+
((Supervisor) => {
|
|
1521
|
+
const DEFAULT_JITTER_RATIO = 0.1;
|
|
1522
|
+
const DEFAULT_MAX_BACKOFF_MS = 60000;
|
|
1523
|
+
const DEFAULT_STOP_TIMEOUT_MS = 2000;
|
|
1524
|
+
const registry = new Set;
|
|
1525
|
+
let logger2 = Logger.fromConfig().child({ component: "supervisor" });
|
|
1526
|
+
function run(name, fn, options) {
|
|
1527
|
+
validateOptions(name, options);
|
|
1528
|
+
const controller = new AbortController;
|
|
1529
|
+
const signal = controller.signal;
|
|
1530
|
+
const intervalMs = options.intervalMs;
|
|
1531
|
+
const jitterRatio = options.jitterRatio ?? DEFAULT_JITTER_RATIO;
|
|
1532
|
+
const maxBackoffMs = options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
|
|
1533
|
+
const runOnStart = options.runOnStart ?? true;
|
|
1534
|
+
const initialDelayMs = options.initialDelayMs ?? 0;
|
|
1535
|
+
let removeExternalAbort = null;
|
|
1536
|
+
if (options.signal) {
|
|
1537
|
+
if (options.signal.aborted)
|
|
1538
|
+
controller.abort();
|
|
1539
|
+
else {
|
|
1540
|
+
const abort = () => controller.abort();
|
|
1541
|
+
options.signal.addEventListener("abort", abort, { once: true });
|
|
1542
|
+
removeExternalAbort = () => options.signal?.removeEventListener("abort", abort);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
const state = {
|
|
1546
|
+
name,
|
|
1547
|
+
controller,
|
|
1548
|
+
done: Promise.resolve(),
|
|
1549
|
+
stopRequested: false,
|
|
1550
|
+
stopLogged: false,
|
|
1551
|
+
async stop(timeoutMs) {
|
|
1552
|
+
this.stopRequested = true;
|
|
1553
|
+
this.controller.abort();
|
|
1554
|
+
const stopped = await resolveWithin(this.done, timeoutMs);
|
|
1555
|
+
if (stopped) {
|
|
1556
|
+
logStopped(this);
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
registry.delete(this);
|
|
1560
|
+
logStopTimeout(this, timeoutMs);
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
state.done = loop({
|
|
1564
|
+
name,
|
|
1565
|
+
fn,
|
|
1566
|
+
signal,
|
|
1567
|
+
intervalMs,
|
|
1568
|
+
initialDelayMs,
|
|
1569
|
+
jitterRatio,
|
|
1570
|
+
maxBackoffMs,
|
|
1571
|
+
runOnStart
|
|
1572
|
+
}).finally(() => {
|
|
1573
|
+
removeExternalAbort?.();
|
|
1574
|
+
registry.delete(state);
|
|
1575
|
+
if (state.stopRequested || signal.aborted)
|
|
1576
|
+
logStopped(state);
|
|
1577
|
+
});
|
|
1578
|
+
registry.add(state);
|
|
1579
|
+
logger2.info("loop started", { name, event: "loop.started", interval_ms: intervalMs });
|
|
1580
|
+
return {
|
|
1581
|
+
stop() {
|
|
1582
|
+
return state.stop(DEFAULT_STOP_TIMEOUT_MS);
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
Supervisor.run = run;
|
|
1587
|
+
async function stopAll(timeoutMs = DEFAULT_STOP_TIMEOUT_MS) {
|
|
1588
|
+
const loops = Array.from(registry);
|
|
1589
|
+
await Promise.all(loops.map((loopState) => loopState.stop(timeoutMs)));
|
|
1590
|
+
}
|
|
1591
|
+
Supervisor.stopAll = stopAll;
|
|
1592
|
+
function list() {
|
|
1593
|
+
return Array.from(registry, (loopState) => loopState.name).sort();
|
|
1594
|
+
}
|
|
1595
|
+
Supervisor.list = list;
|
|
1596
|
+
function __setLoggerForTests(testLogger) {
|
|
1597
|
+
logger2 = testLogger ?? Logger.fromConfig().child({ component: "supervisor" });
|
|
1598
|
+
}
|
|
1599
|
+
Supervisor.__setLoggerForTests = __setLoggerForTests;
|
|
1600
|
+
async function loop(context) {
|
|
1601
|
+
let consecutiveFailures = 0;
|
|
1602
|
+
let nextDelayMs = context.runOnStart ? context.initialDelayMs : context.initialDelayMs > 0 ? context.initialDelayMs : context.intervalMs;
|
|
1603
|
+
while (!context.signal.aborted) {
|
|
1604
|
+
if (nextDelayMs > 0) {
|
|
1605
|
+
const slept = await sleep(nextDelayMs, context.signal);
|
|
1606
|
+
if (!slept)
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
if (context.signal.aborted)
|
|
1610
|
+
return;
|
|
1611
|
+
const startedAt = Date.now();
|
|
1612
|
+
try {
|
|
1613
|
+
await context.fn();
|
|
1614
|
+
const durationMs = Date.now() - startedAt;
|
|
1615
|
+
consecutiveFailures = 0;
|
|
1616
|
+
logger2.debug("loop tick", {
|
|
1617
|
+
name: context.name,
|
|
1618
|
+
event: "loop.tick",
|
|
1619
|
+
duration_ms: durationMs
|
|
1620
|
+
});
|
|
1621
|
+
nextDelayMs = applyJitter(context.intervalMs, context.jitterRatio);
|
|
1622
|
+
} catch (err) {
|
|
1623
|
+
consecutiveFailures += 1;
|
|
1624
|
+
const backoffMs = Math.min(context.intervalMs * 2 ** consecutiveFailures, context.maxBackoffMs);
|
|
1625
|
+
nextDelayMs = applyJitter(backoffMs, context.jitterRatio);
|
|
1626
|
+
logger2.error("loop error", {
|
|
1627
|
+
name: context.name,
|
|
1628
|
+
event: "loop.error",
|
|
1629
|
+
err,
|
|
1630
|
+
attempt: consecutiveFailures,
|
|
1631
|
+
next_delay_ms: nextDelayMs
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
function validateOptions(name, options) {
|
|
1637
|
+
if (!name.trim())
|
|
1638
|
+
throw new Error("Supervisor loop name is required");
|
|
1639
|
+
if (!Number.isFinite(options.intervalMs) || options.intervalMs <= 0) {
|
|
1640
|
+
throw new Error("Supervisor intervalMs must be a positive finite number");
|
|
1641
|
+
}
|
|
1642
|
+
if (options.initialDelayMs !== undefined && (!Number.isFinite(options.initialDelayMs) || options.initialDelayMs < 0)) {
|
|
1643
|
+
throw new Error("Supervisor initialDelayMs must be a non-negative finite number");
|
|
1644
|
+
}
|
|
1645
|
+
if (options.jitterRatio !== undefined && (!Number.isFinite(options.jitterRatio) || options.jitterRatio < 0)) {
|
|
1646
|
+
throw new Error("Supervisor jitterRatio must be a non-negative finite number");
|
|
1647
|
+
}
|
|
1648
|
+
if (options.maxBackoffMs !== undefined && (!Number.isFinite(options.maxBackoffMs) || options.maxBackoffMs <= 0)) {
|
|
1649
|
+
throw new Error("Supervisor maxBackoffMs must be a positive finite number");
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
function applyJitter(delayMs, jitterRatio) {
|
|
1653
|
+
if (delayMs <= 0 || jitterRatio <= 0)
|
|
1654
|
+
return Math.max(0, Math.round(delayMs));
|
|
1655
|
+
const spread = delayMs * jitterRatio;
|
|
1656
|
+
const offset = (Math.random() * 2 - 1) * spread;
|
|
1657
|
+
return Math.max(0, Math.round(delayMs + offset));
|
|
1658
|
+
}
|
|
1659
|
+
function sleep(delayMs, signal) {
|
|
1660
|
+
if (signal.aborted)
|
|
1661
|
+
return Promise.resolve(false);
|
|
1662
|
+
return new Promise((resolve) => {
|
|
1663
|
+
let timeout = null;
|
|
1664
|
+
const onAbort = () => {
|
|
1665
|
+
if (timeout)
|
|
1666
|
+
clearTimeout(timeout);
|
|
1667
|
+
resolve(false);
|
|
1668
|
+
};
|
|
1669
|
+
timeout = setTimeout(() => {
|
|
1670
|
+
signal.removeEventListener("abort", onAbort);
|
|
1671
|
+
resolve(true);
|
|
1672
|
+
}, delayMs);
|
|
1673
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
async function resolveWithin(promise, timeoutMs) {
|
|
1677
|
+
let timeout = null;
|
|
1678
|
+
try {
|
|
1679
|
+
return await Promise.race([
|
|
1680
|
+
promise.then(() => true),
|
|
1681
|
+
new Promise((resolve) => {
|
|
1682
|
+
timeout = setTimeout(() => resolve(false), timeoutMs);
|
|
1683
|
+
})
|
|
1684
|
+
]);
|
|
1685
|
+
} finally {
|
|
1686
|
+
if (timeout)
|
|
1687
|
+
clearTimeout(timeout);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
function logStopped(state) {
|
|
1691
|
+
if (state.stopLogged)
|
|
1692
|
+
return;
|
|
1693
|
+
state.stopLogged = true;
|
|
1694
|
+
logger2.info("loop stopped", { name: state.name, event: "loop.stopped" });
|
|
1695
|
+
}
|
|
1696
|
+
function logStopTimeout(state, timeoutMs) {
|
|
1697
|
+
if (state.stopLogged)
|
|
1698
|
+
return;
|
|
1699
|
+
state.stopLogged = true;
|
|
1700
|
+
logger2.warn("loop stop timeout", {
|
|
1701
|
+
name: state.name,
|
|
1702
|
+
event: "loop.stop_timeout",
|
|
1703
|
+
timeout_ms: timeoutMs
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
})(Supervisor ||= {});
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
// src/storage/pricing.ts
|
|
1710
|
+
var exports_pricing = {};
|
|
1711
|
+
__export(exports_pricing, {
|
|
1712
|
+
Pricing: () => Pricing
|
|
1713
|
+
});
|
|
1714
|
+
import { dirname } from "path";
|
|
1715
|
+
import { mkdir } from "fs/promises";
|
|
1716
|
+
var logger2, Pricing;
|
|
1717
|
+
var init_pricing = __esm(() => {
|
|
1718
|
+
init_config();
|
|
1719
|
+
init_logger();
|
|
1720
|
+
init_supervisor();
|
|
1721
|
+
logger2 = Logger.fromConfig().child({ component: "pricing" });
|
|
1722
|
+
((Pricing) => {
|
|
1723
|
+
const MODELS_DEV_URL = "https://models.dev/api.json";
|
|
1724
|
+
let cache = null;
|
|
1725
|
+
let inFlightFetch = null;
|
|
1726
|
+
let bypassDiskCacheForTests = false;
|
|
1727
|
+
async function fetchPricing(options = {}) {
|
|
1728
|
+
const now = Date.now();
|
|
1729
|
+
if (!options.force && cache && now - cache.fetchedAt < Config2.pricingCacheTtlMs) {
|
|
1730
|
+
return cache.data;
|
|
1731
|
+
}
|
|
1732
|
+
if (!options.force && inFlightFetch) {
|
|
1733
|
+
return inFlightFetch;
|
|
1734
|
+
}
|
|
1735
|
+
inFlightFetch = refreshPricing(options.force ?? false).finally(() => {
|
|
1736
|
+
inFlightFetch = null;
|
|
1737
|
+
});
|
|
1738
|
+
return inFlightFetch;
|
|
1739
|
+
}
|
|
1740
|
+
Pricing.fetchPricing = fetchPricing;
|
|
1741
|
+
function getPricing(model, provider) {
|
|
1742
|
+
return findPricing(model, provider)?.pricing ?? null;
|
|
1743
|
+
}
|
|
1744
|
+
Pricing.getPricing = getPricing;
|
|
1745
|
+
async function getPricingFreshness() {
|
|
1746
|
+
const entry = cache ?? await readDiskCache();
|
|
1747
|
+
if (!entry)
|
|
1748
|
+
return null;
|
|
1749
|
+
return { fetchedAt: entry.fetchedAt, ageMs: Date.now() - entry.fetchedAt };
|
|
1750
|
+
}
|
|
1751
|
+
Pricing.getPricingFreshness = getPricingFreshness;
|
|
1752
|
+
function startBackgroundRefresh(options = {}) {
|
|
1753
|
+
const intervalMs = options.intervalMs ?? Config2.pricingRefreshIntervalMs;
|
|
1754
|
+
return Supervisor.run("pricing-refresh", async () => {
|
|
1755
|
+
await fetchPricing();
|
|
1756
|
+
}, {
|
|
1757
|
+
intervalMs,
|
|
1758
|
+
runOnStart: false,
|
|
1759
|
+
signal: options.signal
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
Pricing.startBackgroundRefresh = startBackgroundRefresh;
|
|
1763
|
+
function __setPricingForTests(entries, fetchedAt = Date.now()) {
|
|
1764
|
+
bypassDiskCacheForTests = false;
|
|
1765
|
+
cache = { data: new Map(entries), fetchedAt };
|
|
1766
|
+
}
|
|
1767
|
+
Pricing.__setPricingForTests = __setPricingForTests;
|
|
1768
|
+
function __clearPricingForTests() {
|
|
1769
|
+
cache = null;
|
|
1770
|
+
inFlightFetch = null;
|
|
1771
|
+
bypassDiskCacheForTests = true;
|
|
1772
|
+
}
|
|
1773
|
+
Pricing.__clearPricingForTests = __clearPricingForTests;
|
|
1774
|
+
function findPricing(model, provider) {
|
|
1775
|
+
if (!cache)
|
|
1776
|
+
return null;
|
|
1777
|
+
const normalizedModel = normalizeKey(model);
|
|
1778
|
+
const normalizedProvider = provider ? normalizeKey(provider) : null;
|
|
1779
|
+
const candidates = buildLookupCandidates(model, provider);
|
|
1780
|
+
for (const key of candidates) {
|
|
1781
|
+
const pricing = cache.data.get(key);
|
|
1782
|
+
if (pricing)
|
|
1783
|
+
return { pricing, key, source: "exact" };
|
|
1784
|
+
}
|
|
1785
|
+
for (const [key, pricing] of cache.data) {
|
|
1786
|
+
if (normalizeKey(key) === normalizedModel) {
|
|
1787
|
+
return { pricing, key, source: "normalized" };
|
|
1788
|
+
}
|
|
1789
|
+
if (normalizedProvider && normalizeKey(key) === `${normalizedProvider}/${normalizedModel}`) {
|
|
1790
|
+
return { pricing, key, source: "normalized" };
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
const alias = aliasModel(normalizedModel);
|
|
1794
|
+
if (alias) {
|
|
1795
|
+
for (const key of buildLookupCandidates(alias, provider)) {
|
|
1796
|
+
const pricing = cache.data.get(key);
|
|
1797
|
+
if (pricing)
|
|
1798
|
+
return { pricing, key, source: "alias" };
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
const fuzzy = findFuzzyMatch(normalizedModel, normalizedProvider, cache.data);
|
|
1802
|
+
if (fuzzy)
|
|
1803
|
+
return fuzzy;
|
|
1804
|
+
return null;
|
|
1805
|
+
}
|
|
1806
|
+
Pricing.findPricing = findPricing;
|
|
1807
|
+
function calculateCost(usage, pricing, provider) {
|
|
1808
|
+
if (provider && normalizeKey(provider) === "openai") {
|
|
1809
|
+
const billableInputTokens = Math.max(usage.prompt_tokens - usage.cache_read_tokens, 0);
|
|
1810
|
+
return (billableInputTokens * pricing.input + usage.completion_tokens * pricing.output + usage.cache_read_tokens * (pricing.cache_read ?? pricing.input)) / 1e6;
|
|
1811
|
+
}
|
|
1812
|
+
return (usage.prompt_tokens * pricing.input + usage.completion_tokens * pricing.output + usage.cache_read_tokens * (pricing.cache_read ?? pricing.input) + usage.cache_creation_tokens * (pricing.cache_write ?? pricing.input) + (usage.reasoning_tokens ?? 0) * (pricing.reasoning ?? pricing.output)) / 1e6;
|
|
1813
|
+
}
|
|
1814
|
+
Pricing.calculateCost = calculateCost;
|
|
1815
|
+
async function refreshPricing(force) {
|
|
1816
|
+
const now = Date.now();
|
|
1817
|
+
if (!force && !bypassDiskCacheForTests) {
|
|
1818
|
+
const diskCache = await readDiskCache();
|
|
1819
|
+
if (diskCache && now - diskCache.fetchedAt < Config2.pricingCacheTtlMs) {
|
|
1820
|
+
cache = diskCache;
|
|
1821
|
+
return diskCache.data;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
try {
|
|
1825
|
+
const res = await fetch(MODELS_DEV_URL, { signal: AbortSignal.timeout(30000) });
|
|
1826
|
+
if (!res.ok)
|
|
1827
|
+
throw new Error(`models.dev returned HTTP ${res.status}`);
|
|
1828
|
+
const raw = await res.json();
|
|
1829
|
+
const map = buildPricingMap(raw);
|
|
1830
|
+
addLocalOverrides(map);
|
|
1831
|
+
cache = { data: map, fetchedAt: now };
|
|
1832
|
+
await writeDiskCache(cache);
|
|
1833
|
+
logger2.info("loaded pricing aliases", { aliases: map.size, source: "models.dev" });
|
|
1834
|
+
return map;
|
|
1835
|
+
} catch (err) {
|
|
1836
|
+
logger2.warn("pricing fetch failed, using cached data", { err, source: "models.dev" });
|
|
1837
|
+
if (cache)
|
|
1838
|
+
return cache.data;
|
|
1839
|
+
const diskCache = bypassDiskCacheForTests ? null : await readDiskCache();
|
|
1840
|
+
if (diskCache) {
|
|
1841
|
+
cache = diskCache;
|
|
1842
|
+
return diskCache.data;
|
|
1843
|
+
}
|
|
1844
|
+
const fallback = new Map;
|
|
1845
|
+
addLocalOverrides(fallback);
|
|
1846
|
+
cache = { data: fallback, fetchedAt: 0 };
|
|
1847
|
+
return fallback;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
function buildPricingMap(raw) {
|
|
1851
|
+
const map = new Map;
|
|
1852
|
+
for (const [provider, providerData] of Object.entries(raw)) {
|
|
1853
|
+
if (!providerData.models)
|
|
1854
|
+
continue;
|
|
1855
|
+
for (const [modelId, modelData] of Object.entries(providerData.models)) {
|
|
1856
|
+
if (!modelData.cost)
|
|
1857
|
+
continue;
|
|
1858
|
+
const pricing = toPricing(modelData.cost);
|
|
1859
|
+
if (!pricing)
|
|
1860
|
+
continue;
|
|
1861
|
+
setPricingAlias(map, modelId, pricing);
|
|
1862
|
+
setPricingAlias(map, `${provider}/${modelId}`, pricing);
|
|
1863
|
+
if (modelData.id)
|
|
1864
|
+
setPricingAlias(map, modelData.id, pricing);
|
|
1865
|
+
if (modelData.name)
|
|
1866
|
+
setPricingAlias(map, modelData.name, pricing);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
return map;
|
|
1870
|
+
}
|
|
1871
|
+
function toPricing(cost) {
|
|
1872
|
+
if (typeof cost.input !== "number" || typeof cost.output !== "number")
|
|
1873
|
+
return null;
|
|
1874
|
+
return {
|
|
1875
|
+
input: cost.input,
|
|
1876
|
+
output: cost.output,
|
|
1877
|
+
cache_read: typeof cost.cache_read === "number" ? cost.cache_read : undefined,
|
|
1878
|
+
cache_write: typeof cost.cache_write === "number" ? cost.cache_write : undefined,
|
|
1879
|
+
reasoning: typeof cost.reasoning === "number" ? cost.reasoning : undefined
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
function addLocalOverrides(map) {
|
|
1883
|
+
const overrides = {
|
|
1884
|
+
"gpt-5.4": { input: 2.5, output: 15, cache_read: 0.25 },
|
|
1885
|
+
"gpt-5.4-mini": { input: 0.75, output: 4.5, cache_read: 0.075 },
|
|
1886
|
+
"gpt-5.4-mini-2026-03-17": { input: 0.75, output: 4.5, cache_read: 0.075 },
|
|
1887
|
+
"kimi-for-coding": { input: 0.4, output: 2.5, cache_read: 0.4 },
|
|
1888
|
+
"kimi-k2": { input: 0.4, output: 2.5, cache_read: 0.4 },
|
|
1889
|
+
"kimi-k2.6": { input: 0.95, output: 4, cache_read: 0.16 }
|
|
1890
|
+
};
|
|
1891
|
+
for (const [model, pricing] of Object.entries(overrides)) {
|
|
1892
|
+
setPricingAlias(map, model, pricing);
|
|
1893
|
+
setPricingAlias(map, `openai/${model}`, pricing);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
function setPricingAlias(map, key, pricing) {
|
|
1897
|
+
map.set(key, pricing);
|
|
1898
|
+
map.set(normalizeKey(key), pricing);
|
|
1899
|
+
}
|
|
1900
|
+
function buildLookupCandidates(model, provider) {
|
|
1901
|
+
const candidates = new Set;
|
|
1902
|
+
candidates.add(model);
|
|
1903
|
+
candidates.add(normalizeKey(model));
|
|
1904
|
+
if (provider) {
|
|
1905
|
+
candidates.add(`${provider}/${model}`);
|
|
1906
|
+
candidates.add(`${normalizeKey(provider)}/${normalizeKey(model)}`);
|
|
1907
|
+
}
|
|
1908
|
+
return Array.from(candidates);
|
|
1909
|
+
}
|
|
1910
|
+
function aliasModel(normalizedModel) {
|
|
1911
|
+
if (normalizedModel === "kimi-for-coding")
|
|
1912
|
+
return "kimi-k2";
|
|
1913
|
+
if (normalizedModel.startsWith("gpt-5.4-mini"))
|
|
1914
|
+
return "gpt-5.4-mini";
|
|
1915
|
+
if (normalizedModel.startsWith("gpt-5.4"))
|
|
1916
|
+
return "gpt-5.4";
|
|
1917
|
+
return null;
|
|
1918
|
+
}
|
|
1919
|
+
function findFuzzyMatch(normalizedModel, normalizedProvider, map) {
|
|
1920
|
+
const eligible = Array.from(map.entries()).filter(([key, pricing]) => {
|
|
1921
|
+
if (pricing.input === 0 && pricing.output === 0)
|
|
1922
|
+
return false;
|
|
1923
|
+
const normalizedKey = normalizeKey(key);
|
|
1924
|
+
if (normalizedProvider && !normalizedKey.startsWith(`${normalizedProvider}/`) && normalizedKey.includes("/")) {
|
|
1925
|
+
return false;
|
|
1926
|
+
}
|
|
1927
|
+
return normalizedKey.endsWith(`/${normalizedModel}`) || normalizedKey === normalizedModel;
|
|
1928
|
+
});
|
|
1929
|
+
if (eligible.length > 0) {
|
|
1930
|
+
const [key, pricing] = eligible[0];
|
|
1931
|
+
return { key, pricing, source: "fuzzy" };
|
|
1932
|
+
}
|
|
1933
|
+
const broad = Array.from(map.entries()).find(([key, pricing]) => {
|
|
1934
|
+
if (pricing.input === 0 && pricing.output === 0)
|
|
1935
|
+
return false;
|
|
1936
|
+
const normalizedKey = normalizeKey(key);
|
|
1937
|
+
return normalizedKey.length >= 6 && normalizedModel.includes(normalizedKey);
|
|
1938
|
+
});
|
|
1939
|
+
if (!broad)
|
|
1940
|
+
return null;
|
|
1941
|
+
return { key: broad[0], pricing: broad[1], source: "fuzzy" };
|
|
1942
|
+
}
|
|
1943
|
+
function normalizeKey(key) {
|
|
1944
|
+
return key.trim().toLowerCase().replace(/[_\s]+/g, "-");
|
|
1945
|
+
}
|
|
1946
|
+
async function readDiskCache() {
|
|
1947
|
+
try {
|
|
1948
|
+
const file = Bun.file(Config2.pricingCachePath);
|
|
1949
|
+
if (!await file.exists())
|
|
1950
|
+
return null;
|
|
1951
|
+
const parsed = await file.json();
|
|
1952
|
+
if (typeof parsed.fetchedAt !== "number" || !Array.isArray(parsed.data))
|
|
1953
|
+
return null;
|
|
1954
|
+
return { fetchedAt: parsed.fetchedAt, data: new Map(parsed.data) };
|
|
1955
|
+
} catch (err) {
|
|
1956
|
+
logger2.warn("disk cache read failed", { err, path: Config2.pricingCachePath });
|
|
1957
|
+
return null;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
async function writeDiskCache(entry) {
|
|
1961
|
+
try {
|
|
1962
|
+
await mkdir(dirname(Config2.pricingCachePath), { recursive: true });
|
|
1963
|
+
await Bun.write(Config2.pricingCachePath, JSON.stringify({ fetchedAt: entry.fetchedAt, data: Array.from(entry.data.entries()) }));
|
|
1964
|
+
} catch (err) {
|
|
1965
|
+
logger2.warn("disk cache write failed", { err, path: Config2.pricingCachePath });
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
})(Pricing ||= {});
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
// src/storage/cost.ts
|
|
1972
|
+
var logger3, Cost;
|
|
1973
|
+
var init_cost = __esm(() => {
|
|
1974
|
+
init_pricing();
|
|
1975
|
+
init_logger();
|
|
1976
|
+
logger3 = Logger.fromConfig().child({ component: "cost" });
|
|
1977
|
+
((Cost) => {
|
|
1978
|
+
const SENTINEL_MODELS = new Set(["", "unknown", "undefined"]);
|
|
1979
|
+
let activeLogger = logger3;
|
|
1980
|
+
function __setLoggerForTests(nextLogger) {
|
|
1981
|
+
activeLogger = nextLogger;
|
|
1982
|
+
}
|
|
1983
|
+
Cost.__setLoggerForTests = __setLoggerForTests;
|
|
1984
|
+
function __resetLoggerForTests() {
|
|
1985
|
+
activeLogger = logger3;
|
|
1986
|
+
}
|
|
1987
|
+
Cost.__resetLoggerForTests = __resetLoggerForTests;
|
|
1988
|
+
function compute(inputs) {
|
|
1989
|
+
const model = inputs.model?.trim() ?? "";
|
|
1990
|
+
if (SENTINEL_MODELS.has(model.toLowerCase())) {
|
|
1991
|
+
return { cost_usd: 0, cost_status: "unsupported", source: "unsupported_model" };
|
|
1992
|
+
}
|
|
1993
|
+
const pricing = Pricing.getPricing(model, inputs.provider);
|
|
1994
|
+
if (!pricing) {
|
|
1995
|
+
return { cost_usd: 0, cost_status: "pending", source: "pricing" };
|
|
1996
|
+
}
|
|
1997
|
+
const rawCost = Pricing.calculateCost({
|
|
1998
|
+
prompt_tokens: inputs.usage.prompt_tokens ?? 0,
|
|
1999
|
+
completion_tokens: inputs.usage.completion_tokens ?? 0,
|
|
2000
|
+
cache_creation_tokens: inputs.usage.cache_creation_tokens ?? 0,
|
|
2001
|
+
cache_read_tokens: inputs.usage.cache_read_tokens ?? 0,
|
|
2002
|
+
reasoning_tokens: inputs.usage.reasoning_tokens ?? 0
|
|
2003
|
+
}, pricing, inputs.provider);
|
|
2004
|
+
if (!Number.isFinite(rawCost) || Number.isNaN(rawCost) || rawCost < 0) {
|
|
2005
|
+
activeLogger.warn("cost guard rejected computed cost", {
|
|
2006
|
+
event: "cost.guard",
|
|
2007
|
+
provider: inputs.provider,
|
|
2008
|
+
model,
|
|
2009
|
+
raw_cost: rawCost
|
|
2010
|
+
});
|
|
2011
|
+
return { cost_usd: 0, cost_status: "pending", source: "guard" };
|
|
2012
|
+
}
|
|
2013
|
+
if (rawCost === 0) {
|
|
2014
|
+
return { cost_usd: 0, cost_status: "pending", source: "guard" };
|
|
2015
|
+
}
|
|
2016
|
+
return { cost_usd: rawCost, cost_status: "ok", source: "pricing" };
|
|
2017
|
+
}
|
|
2018
|
+
Cost.compute = compute;
|
|
2019
|
+
function inputsFromLog(log) {
|
|
2020
|
+
return {
|
|
2021
|
+
provider: log.provider,
|
|
2022
|
+
model: log.model,
|
|
2023
|
+
usage: {
|
|
2024
|
+
prompt_tokens: log.prompt_tokens,
|
|
2025
|
+
completion_tokens: log.completion_tokens,
|
|
2026
|
+
cache_creation_tokens: log.cache_creation_tokens,
|
|
2027
|
+
cache_read_tokens: log.cache_read_tokens,
|
|
2028
|
+
reasoning_tokens: log.reasoning_tokens ?? 0
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
Cost.inputsFromLog = inputsFromLog;
|
|
2033
|
+
})(Cost ||= {});
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
// src/upstream/client.ts
|
|
2037
|
+
var exports_client = {};
|
|
2038
|
+
__export(exports_client, {
|
|
2039
|
+
UpstreamClient: () => UpstreamClient
|
|
2040
|
+
});
|
|
2041
|
+
var UpstreamClient;
|
|
2042
|
+
var init_client = __esm(() => {
|
|
2043
|
+
init_config();
|
|
2044
|
+
init_logger();
|
|
2045
|
+
((UpstreamClient) => {
|
|
2046
|
+
UpstreamClient.DEFAULT_UPSTREAM_TIMEOUT_MS = 300000;
|
|
2047
|
+
UpstreamClient.DEFAULT_UPSTREAM_CONNECT_TIMEOUT_MS = 1e4;
|
|
2048
|
+
const MAX_RETRIES = 2;
|
|
2049
|
+
const OPEN_AFTER_FAILURES = 5;
|
|
2050
|
+
const HALF_OPEN_AFTER_MS = 30000;
|
|
2051
|
+
const breakers = new Map;
|
|
2052
|
+
let logger4 = Logger.fromConfig().child({ component: "upstream-client" });
|
|
2053
|
+
let sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2054
|
+
let now = () => Date.now();
|
|
2055
|
+
let random = () => Math.random();
|
|
2056
|
+
async function fetch2(options) {
|
|
2057
|
+
const providerId = options.providerId || "unknown";
|
|
2058
|
+
const breaker = breakerFor(providerId);
|
|
2059
|
+
const breakerState = currentBreakerState(breaker);
|
|
2060
|
+
if (breakerState === "open") {
|
|
2061
|
+
const normalized = normalizeShortCircuit(providerId);
|
|
2062
|
+
logger4.warn("upstream circuit breaker open", {
|
|
2063
|
+
event: "upstream.short_circuit",
|
|
2064
|
+
...withoutCause(normalized)
|
|
2065
|
+
});
|
|
2066
|
+
logFailure(normalized, 0, false);
|
|
2067
|
+
return normalizedResponse(normalized);
|
|
2068
|
+
}
|
|
2069
|
+
const streaming = isStreamingRequest(options);
|
|
2070
|
+
const idempotent = options.idempotent === true;
|
|
2071
|
+
let attempt = 0;
|
|
2072
|
+
while (true) {
|
|
2073
|
+
const timeout = createTimeoutSignal(Config2.upstreamTimeoutMs, Config2.upstreamConnectTimeoutMs);
|
|
2074
|
+
const signal = composeSignals([timeout.signal, options.signal]);
|
|
2075
|
+
try {
|
|
2076
|
+
const response = await globalThis.fetch(options.url, {
|
|
2077
|
+
method: options.method,
|
|
2078
|
+
headers: options.headers,
|
|
2079
|
+
body: options.body,
|
|
2080
|
+
signal
|
|
2081
|
+
});
|
|
2082
|
+
timeout.clear();
|
|
2083
|
+
if (response.status >= 500) {
|
|
2084
|
+
const normalized = normalizeHttpFailure(response, providerId, canRetry(idempotent, streaming));
|
|
2085
|
+
const retrying = shouldRetry(normalized, attempt, streaming, idempotent);
|
|
2086
|
+
logFailure(normalized, attempt, retrying);
|
|
2087
|
+
if (retrying) {
|
|
2088
|
+
await discardResponse(response);
|
|
2089
|
+
await sleep(backoffMs(attempt));
|
|
2090
|
+
attempt += 1;
|
|
2091
|
+
continue;
|
|
2092
|
+
}
|
|
2093
|
+
recordFailure(breaker);
|
|
2094
|
+
return response;
|
|
2095
|
+
}
|
|
2096
|
+
recordSuccess(breaker);
|
|
2097
|
+
return response;
|
|
2098
|
+
} catch (err) {
|
|
2099
|
+
timeout.clear();
|
|
2100
|
+
const normalized = normalizeThrownFailure(err, providerId, canRetry(idempotent, streaming), timeout.kind);
|
|
2101
|
+
const retrying = shouldRetry(normalized, attempt, streaming, idempotent);
|
|
2102
|
+
logFailure(normalized, attempt, retrying);
|
|
2103
|
+
if (retrying) {
|
|
2104
|
+
await sleep(backoffMs(attempt));
|
|
2105
|
+
attempt += 1;
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
recordFailure(breaker);
|
|
2109
|
+
return normalizedResponse(normalized);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
UpstreamClient.fetch = fetch2;
|
|
2114
|
+
function __resetForTests() {
|
|
2115
|
+
breakers.clear();
|
|
2116
|
+
logger4 = Logger.fromConfig().child({ component: "upstream-client" });
|
|
2117
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2118
|
+
now = () => Date.now();
|
|
2119
|
+
random = () => Math.random();
|
|
2120
|
+
}
|
|
2121
|
+
UpstreamClient.__resetForTests = __resetForTests;
|
|
2122
|
+
function __setTestHooks(hooks) {
|
|
2123
|
+
if (hooks.logger)
|
|
2124
|
+
logger4 = hooks.logger;
|
|
2125
|
+
if (hooks.sleep)
|
|
2126
|
+
sleep = hooks.sleep;
|
|
2127
|
+
if (hooks.now)
|
|
2128
|
+
now = hooks.now;
|
|
2129
|
+
if (hooks.random)
|
|
2130
|
+
random = hooks.random;
|
|
2131
|
+
}
|
|
2132
|
+
UpstreamClient.__setTestHooks = __setTestHooks;
|
|
2133
|
+
function breakerFor(providerId) {
|
|
2134
|
+
const existing = breakers.get(providerId);
|
|
2135
|
+
if (existing)
|
|
2136
|
+
return existing;
|
|
2137
|
+
const created = { state: "closed", failures: 0, openedAt: 0 };
|
|
2138
|
+
breakers.set(providerId, created);
|
|
2139
|
+
return created;
|
|
2140
|
+
}
|
|
2141
|
+
function currentBreakerState(breaker) {
|
|
2142
|
+
if (breaker.state === "open" && now() - breaker.openedAt >= HALF_OPEN_AFTER_MS) {
|
|
2143
|
+
breaker.state = "half-open";
|
|
2144
|
+
}
|
|
2145
|
+
return breaker.state;
|
|
2146
|
+
}
|
|
2147
|
+
function recordFailure(breaker) {
|
|
2148
|
+
if (breaker.state === "half-open") {
|
|
2149
|
+
breaker.state = "open";
|
|
2150
|
+
breaker.openedAt = now();
|
|
2151
|
+
breaker.failures = OPEN_AFTER_FAILURES;
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
breaker.failures += 1;
|
|
2155
|
+
if (breaker.failures >= OPEN_AFTER_FAILURES) {
|
|
2156
|
+
breaker.state = "open";
|
|
2157
|
+
breaker.openedAt = now();
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
function recordSuccess(breaker) {
|
|
2161
|
+
breaker.state = "closed";
|
|
2162
|
+
breaker.failures = 0;
|
|
2163
|
+
breaker.openedAt = 0;
|
|
2164
|
+
}
|
|
2165
|
+
function canRetry(idempotent, streaming) {
|
|
2166
|
+
return idempotent && !streaming;
|
|
2167
|
+
}
|
|
2168
|
+
function shouldRetry(failure, attempt, streaming, idempotent) {
|
|
2169
|
+
if (!canRetry(idempotent, streaming))
|
|
2170
|
+
return false;
|
|
2171
|
+
if (attempt >= MAX_RETRIES)
|
|
2172
|
+
return false;
|
|
2173
|
+
return failure.code === "network" || failure.code === "5xx" || failure.code === "aborted-due-to-timeout";
|
|
2174
|
+
}
|
|
2175
|
+
function backoffMs(attempt) {
|
|
2176
|
+
const jitter = Math.floor(random() * 100);
|
|
2177
|
+
return Math.min(2 ** attempt * 200 + jitter, 5000);
|
|
2178
|
+
}
|
|
2179
|
+
function createTimeoutSignal(totalMs, connectMs) {
|
|
2180
|
+
const controller = new AbortController;
|
|
2181
|
+
let kind = null;
|
|
2182
|
+
const abort = (nextKind) => {
|
|
2183
|
+
if (controller.signal.aborted)
|
|
2184
|
+
return;
|
|
2185
|
+
kind = nextKind;
|
|
2186
|
+
controller.abort(new Error(`upstream ${nextKind} timeout`));
|
|
2187
|
+
};
|
|
2188
|
+
const connectTimer = setTimeout(() => abort("connect"), connectMs);
|
|
2189
|
+
const totalTimer = setTimeout(() => abort("total"), totalMs);
|
|
2190
|
+
return {
|
|
2191
|
+
signal: controller.signal,
|
|
2192
|
+
clear() {
|
|
2193
|
+
clearTimeout(connectTimer);
|
|
2194
|
+
clearTimeout(totalTimer);
|
|
2195
|
+
},
|
|
2196
|
+
get kind() {
|
|
2197
|
+
return kind;
|
|
2198
|
+
}
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
function composeSignals(signals) {
|
|
2202
|
+
const active = signals.filter((signal) => Boolean(signal));
|
|
2203
|
+
if (active.length === 1)
|
|
2204
|
+
return active[0];
|
|
2205
|
+
const abortSignal = AbortSignal;
|
|
2206
|
+
if (typeof abortSignal.any === "function")
|
|
2207
|
+
return abortSignal.any(active);
|
|
2208
|
+
const controller = new AbortController;
|
|
2209
|
+
const abort = (signal) => {
|
|
2210
|
+
if (!controller.signal.aborted)
|
|
2211
|
+
controller.abort(signal.reason);
|
|
2212
|
+
};
|
|
2213
|
+
for (const signal of active) {
|
|
2214
|
+
if (signal.aborted) {
|
|
2215
|
+
abort(signal);
|
|
2216
|
+
break;
|
|
2217
|
+
}
|
|
2218
|
+
signal.addEventListener("abort", () => abort(signal), { once: true });
|
|
2219
|
+
}
|
|
2220
|
+
return controller.signal;
|
|
2221
|
+
}
|
|
2222
|
+
function isStreamingRequest(options) {
|
|
2223
|
+
if (options.body instanceof ReadableStream)
|
|
2224
|
+
return true;
|
|
2225
|
+
const headers = new Headers(options.headers);
|
|
2226
|
+
const accept = headers.get("accept")?.toLowerCase() ?? "";
|
|
2227
|
+
const contentType = headers.get("content-type")?.toLowerCase() ?? "";
|
|
2228
|
+
return accept.includes("text/event-stream") || contentType.includes("text/event-stream");
|
|
2229
|
+
}
|
|
2230
|
+
function normalizeHttpFailure(response, providerId, retryable) {
|
|
2231
|
+
return {
|
|
2232
|
+
code: "5xx",
|
|
2233
|
+
status: response.status,
|
|
2234
|
+
providerId,
|
|
2235
|
+
retryable,
|
|
2236
|
+
cause: { statusText: response.statusText }
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
function normalizeThrownFailure(err, providerId, retryable, timeoutKind) {
|
|
2240
|
+
if (timeoutKind) {
|
|
2241
|
+
return {
|
|
2242
|
+
code: "aborted-due-to-timeout",
|
|
2243
|
+
status: 504,
|
|
2244
|
+
providerId,
|
|
2245
|
+
retryable,
|
|
2246
|
+
cause: { timeoutKind, error: serializeCause(err) }
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
if (isAbortError(err)) {
|
|
2250
|
+
return {
|
|
2251
|
+
code: "aborted",
|
|
2252
|
+
status: 499,
|
|
2253
|
+
providerId,
|
|
2254
|
+
retryable: false,
|
|
2255
|
+
cause: serializeCause(err)
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
return {
|
|
2259
|
+
code: "network",
|
|
2260
|
+
status: 503,
|
|
2261
|
+
providerId,
|
|
2262
|
+
retryable,
|
|
2263
|
+
cause: serializeCause(err)
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
function normalizeShortCircuit(providerId) {
|
|
2267
|
+
return {
|
|
2268
|
+
code: "short-circuit",
|
|
2269
|
+
status: 503,
|
|
2270
|
+
providerId,
|
|
2271
|
+
retryable: false,
|
|
2272
|
+
cause: "circuit breaker open"
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
function isAbortError(err) {
|
|
2276
|
+
return err instanceof DOMException && err.name === "AbortError";
|
|
2277
|
+
}
|
|
2278
|
+
function logFailure(failure, attempt, retrying) {
|
|
2279
|
+
logger4.error("upstream failure", {
|
|
2280
|
+
event: "upstream.error",
|
|
2281
|
+
...withoutCause(failure),
|
|
2282
|
+
cause: failure.cause,
|
|
2283
|
+
attempt,
|
|
2284
|
+
max_retries: MAX_RETRIES,
|
|
2285
|
+
retrying
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
function withoutCause(failure) {
|
|
2289
|
+
const { cause: _cause, ...rest } = failure;
|
|
2290
|
+
return rest;
|
|
2291
|
+
}
|
|
2292
|
+
function normalizedResponse(failure) {
|
|
2293
|
+
return new Response(JSON.stringify({ error: { ...withoutCause(failure), cause: failure.cause } }), {
|
|
2294
|
+
status: failure.status,
|
|
2295
|
+
headers: { "content-type": "application/json" }
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
function serializeCause(err) {
|
|
2299
|
+
if (err instanceof Error) {
|
|
2300
|
+
return { name: err.name, message: err.message };
|
|
2301
|
+
}
|
|
2302
|
+
return err;
|
|
2303
|
+
}
|
|
2304
|
+
async function discardResponse(response) {
|
|
2305
|
+
try {
|
|
2306
|
+
await response.body?.cancel();
|
|
2307
|
+
} catch {}
|
|
2308
|
+
}
|
|
2309
|
+
})(UpstreamClient ||= {});
|
|
2310
|
+
});
|
|
2311
|
+
|
|
2312
|
+
// src/cliproxy/quota.ts
|
|
2313
|
+
import { readdir, readFile } from "fs/promises";
|
|
2314
|
+
import { join as join3 } from "path";
|
|
2315
|
+
function normalizePercent(value) {
|
|
2316
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
2317
|
+
return;
|
|
2318
|
+
const pct = value <= 1 ? value * 100 : value;
|
|
2319
|
+
return Math.max(0, Math.min(100, pct));
|
|
2320
|
+
}
|
|
2321
|
+
function normalizeReset(value) {
|
|
2322
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2323
|
+
const ms = value < 1000000000000 ? value * 1000 : value;
|
|
2324
|
+
return new Date(ms).toISOString();
|
|
2325
|
+
}
|
|
2326
|
+
if (typeof value === "string" && value.trim()) {
|
|
2327
|
+
const parsed = Date.parse(value);
|
|
2328
|
+
if (Number.isFinite(parsed))
|
|
2329
|
+
return new Date(parsed).toISOString();
|
|
2330
|
+
}
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
function quotaTypeFromSeconds(seconds, fallback) {
|
|
2334
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds))
|
|
2335
|
+
return fallback;
|
|
2336
|
+
const hours = Math.round(seconds / 3600);
|
|
2337
|
+
if (hours >= 24 * 6)
|
|
2338
|
+
return "week";
|
|
2339
|
+
if (hours >= 24)
|
|
2340
|
+
return `${Math.round(hours / 24)}d`;
|
|
2341
|
+
return `${hours}h`;
|
|
2342
|
+
}
|
|
2343
|
+
async function fetchJson(url, init) {
|
|
2344
|
+
const controller = new AbortController;
|
|
2345
|
+
const timer = setTimeout(() => controller.abort(), Config2.quotaRefreshTimeoutMs);
|
|
2346
|
+
try {
|
|
2347
|
+
const res = await UpstreamClient.fetch({
|
|
2348
|
+
method: init.method ?? "GET",
|
|
2349
|
+
url,
|
|
2350
|
+
headers: init.headers,
|
|
2351
|
+
body: init.body ?? null,
|
|
2352
|
+
providerId: `quota:${new URL(url).hostname}`,
|
|
2353
|
+
idempotent: (init.method ?? "GET") === "GET" || (init.method ?? "GET") === "HEAD",
|
|
2354
|
+
signal: controller.signal
|
|
2355
|
+
});
|
|
2356
|
+
const text = await res.text();
|
|
2357
|
+
let data = null;
|
|
2358
|
+
try {
|
|
2359
|
+
data = text ? JSON.parse(text) : null;
|
|
2360
|
+
} catch {
|
|
2361
|
+
data = null;
|
|
2362
|
+
}
|
|
2363
|
+
return { ok: res.ok, status: res.status, data, text };
|
|
2364
|
+
} finally {
|
|
2365
|
+
clearTimeout(timer);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
function errorMessage(data, fallback) {
|
|
2369
|
+
if (data && typeof data === "object" && "error" in data) {
|
|
2370
|
+
const err = data.error;
|
|
2371
|
+
if (err && typeof err === "object" && "message" in err) {
|
|
2372
|
+
const message = err.message;
|
|
2373
|
+
if (typeof message === "string" && message.trim())
|
|
2374
|
+
return message;
|
|
2375
|
+
}
|
|
2376
|
+
if (typeof err === "string" && err.trim())
|
|
2377
|
+
return err;
|
|
2378
|
+
}
|
|
2379
|
+
return fallback;
|
|
2380
|
+
}
|
|
2381
|
+
async function probeClaude(auth) {
|
|
2382
|
+
const account = auth.email ?? "claude";
|
|
2383
|
+
if (!auth.access_token) {
|
|
2384
|
+
return {
|
|
2385
|
+
provider: "claude",
|
|
2386
|
+
account,
|
|
2387
|
+
status: "error",
|
|
2388
|
+
unavailable: true,
|
|
2389
|
+
disabled: auth.disabled === true,
|
|
2390
|
+
error: "missing access_token",
|
|
2391
|
+
windows: []
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
const res = await fetchJson("https://api.anthropic.com/api/oauth/usage", {
|
|
2395
|
+
method: "GET",
|
|
2396
|
+
headers: {
|
|
2397
|
+
Authorization: `Bearer ${auth.access_token}`,
|
|
2398
|
+
Accept: "application/json",
|
|
2399
|
+
"anthropic-version": "2023-06-01",
|
|
2400
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
2401
|
+
"User-Agent": "agent-cli-proxy"
|
|
2402
|
+
}
|
|
2403
|
+
});
|
|
2404
|
+
if (!res.ok) {
|
|
2405
|
+
return {
|
|
2406
|
+
provider: "claude",
|
|
2407
|
+
account,
|
|
2408
|
+
status: "error",
|
|
2409
|
+
unavailable: true,
|
|
2410
|
+
disabled: auth.disabled === true,
|
|
2411
|
+
error: errorMessage(res.data, `HTTP ${res.status}`),
|
|
2412
|
+
windows: []
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
const data = res.data;
|
|
2416
|
+
const windows = [];
|
|
2417
|
+
for (const [quotaType, window] of [
|
|
2418
|
+
["5h", data.five_hour],
|
|
2419
|
+
["week", data.seven_day],
|
|
2420
|
+
["week_sonnet", data.seven_day_sonnet],
|
|
2421
|
+
["week_opus", data.seven_day_opus]
|
|
2422
|
+
]) {
|
|
2423
|
+
if (!window)
|
|
2424
|
+
continue;
|
|
2425
|
+
const used = normalizePercent(window.utilization);
|
|
2426
|
+
if (used === undefined && !window.resets_at)
|
|
2427
|
+
continue;
|
|
2428
|
+
windows.push({
|
|
2429
|
+
quota_type: quotaType,
|
|
2430
|
+
used_pct: used,
|
|
2431
|
+
resets_at: normalizeReset(window.resets_at),
|
|
2432
|
+
raw: window
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
return {
|
|
2436
|
+
provider: "claude",
|
|
2437
|
+
account,
|
|
2438
|
+
status: "active",
|
|
2439
|
+
unavailable: false,
|
|
2440
|
+
disabled: auth.disabled === true,
|
|
2441
|
+
windows
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
async function probeCodex(auth) {
|
|
2445
|
+
const account = auth.email ?? "codex";
|
|
2446
|
+
if (!auth.access_token) {
|
|
2447
|
+
return {
|
|
2448
|
+
provider: "codex",
|
|
2449
|
+
account,
|
|
2450
|
+
status: "error",
|
|
2451
|
+
unavailable: true,
|
|
2452
|
+
disabled: auth.disabled === true,
|
|
2453
|
+
error: "missing access_token",
|
|
2454
|
+
windows: []
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
const headers = {
|
|
2458
|
+
Authorization: `Bearer ${auth.access_token}`,
|
|
2459
|
+
Accept: "application/json",
|
|
2460
|
+
"User-Agent": "codex_cli_rs/0.101.0 (Linux; x86_64) agent-cli-proxy"
|
|
2461
|
+
};
|
|
2462
|
+
if (auth.account_id)
|
|
2463
|
+
headers["ChatGPT-Account-Id"] = auth.account_id;
|
|
2464
|
+
const res = await fetchJson("https://chatgpt.com/backend-api/wham/usage", {
|
|
2465
|
+
method: "GET",
|
|
2466
|
+
headers
|
|
2467
|
+
});
|
|
2468
|
+
if (!res.ok) {
|
|
2469
|
+
const data2 = res.data;
|
|
2470
|
+
const resetsAt = normalizeReset(data2?.error?.resets_at);
|
|
2471
|
+
const usedWindow = resetsAt ? [
|
|
2472
|
+
{
|
|
2473
|
+
quota_type: "exhausted",
|
|
2474
|
+
used_pct: 100,
|
|
2475
|
+
resets_at: resetsAt,
|
|
2476
|
+
raw: data2
|
|
2477
|
+
}
|
|
2478
|
+
] : [];
|
|
2479
|
+
return {
|
|
2480
|
+
provider: "codex",
|
|
2481
|
+
account,
|
|
2482
|
+
status: data2?.error?.type ?? "error",
|
|
2483
|
+
unavailable: true,
|
|
2484
|
+
disabled: auth.disabled === true,
|
|
2485
|
+
plan: data2?.error?.plan_type,
|
|
2486
|
+
error: data2?.error?.message ?? `HTTP ${res.status}`,
|
|
2487
|
+
windows: usedWindow
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
const data = res.data;
|
|
2491
|
+
const windows = [];
|
|
2492
|
+
for (const [fallback, window] of [
|
|
2493
|
+
["5h", data.rate_limit?.primary_window],
|
|
2494
|
+
["week", data.rate_limit?.secondary_window]
|
|
2495
|
+
]) {
|
|
2496
|
+
if (!window)
|
|
2497
|
+
continue;
|
|
2498
|
+
const used = normalizePercent(window.used_percent);
|
|
2499
|
+
const reset = normalizeReset(window.reset_at);
|
|
2500
|
+
if (used === undefined && !reset)
|
|
2501
|
+
continue;
|
|
2502
|
+
windows.push({
|
|
2503
|
+
quota_type: quotaTypeFromSeconds(window.limit_window_seconds, fallback),
|
|
2504
|
+
used_pct: used,
|
|
2505
|
+
resets_at: reset,
|
|
2506
|
+
raw: window
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
let plan = data.plan_type;
|
|
2510
|
+
if (data.credits?.balance !== undefined && data.credits.balance !== null) {
|
|
2511
|
+
plan = plan ? `${plan}` : undefined;
|
|
2512
|
+
}
|
|
2513
|
+
return {
|
|
2514
|
+
provider: "codex",
|
|
2515
|
+
account,
|
|
2516
|
+
status: data.rate_limit?.limit_reached ? "limit_reached" : "active",
|
|
2517
|
+
unavailable: data.rate_limit?.limit_reached === true,
|
|
2518
|
+
disabled: auth.disabled === true,
|
|
2519
|
+
plan,
|
|
2520
|
+
windows
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
function readNumber(value) {
|
|
2524
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
2525
|
+
return value;
|
|
2526
|
+
if (typeof value === "string" && value.trim()) {
|
|
2527
|
+
const parsed = Number(value);
|
|
2528
|
+
if (Number.isFinite(parsed))
|
|
2529
|
+
return parsed;
|
|
2530
|
+
}
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
function kimiWindow(quotaType, detail, raw) {
|
|
2534
|
+
if (!detail)
|
|
2535
|
+
return null;
|
|
2536
|
+
const limit = readNumber(detail.limit);
|
|
2537
|
+
const remaining = readNumber(detail.remaining);
|
|
2538
|
+
const used = readNumber(detail.used) ?? (limit !== undefined && remaining !== undefined ? limit - remaining : undefined);
|
|
2539
|
+
const usedPct = limit && used !== undefined ? used / limit * 100 : undefined;
|
|
2540
|
+
const reset = normalizeReset(detail.resetTime);
|
|
2541
|
+
if (usedPct === undefined && remaining === undefined && !reset)
|
|
2542
|
+
return null;
|
|
2543
|
+
return {
|
|
2544
|
+
quota_type: quotaType,
|
|
2545
|
+
used_pct: normalizePercent(usedPct),
|
|
2546
|
+
resets_at: reset,
|
|
2547
|
+
raw
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
async function probeKimi(auth) {
|
|
2551
|
+
const account = "kimi";
|
|
2552
|
+
if (!auth.access_token) {
|
|
2553
|
+
return {
|
|
2554
|
+
provider: "kimi",
|
|
2555
|
+
account,
|
|
2556
|
+
status: "error",
|
|
2557
|
+
unavailable: true,
|
|
2558
|
+
disabled: auth.disabled === true,
|
|
2559
|
+
error: "missing access_token",
|
|
2560
|
+
windows: []
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
const headers = {
|
|
2564
|
+
Authorization: `Bearer ${auth.access_token}`,
|
|
2565
|
+
Accept: "application/json",
|
|
2566
|
+
"User-Agent": "KimiCLI/1.35 agent-cli-proxy"
|
|
2567
|
+
};
|
|
2568
|
+
let res = await fetchJson("https://api.kimi.com/coding/v1/usages", {
|
|
2569
|
+
method: "GET",
|
|
2570
|
+
headers
|
|
2571
|
+
});
|
|
2572
|
+
if (!res.ok) {
|
|
2573
|
+
res = await fetchJson("https://api.moonshot.ai/v1/usages", {
|
|
2574
|
+
method: "GET",
|
|
2575
|
+
headers
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
if (!res.ok) {
|
|
2579
|
+
return {
|
|
2580
|
+
provider: "kimi",
|
|
2581
|
+
account,
|
|
2582
|
+
status: "error",
|
|
2583
|
+
unavailable: true,
|
|
2584
|
+
disabled: auth.disabled === true,
|
|
2585
|
+
error: errorMessage(res.data, `HTTP ${res.status}`),
|
|
2586
|
+
windows: []
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
const data = res.data;
|
|
2590
|
+
const coding = data.usages?.find((u) => u.scope === "FEATURE_CODING") ?? data.usages?.[0];
|
|
2591
|
+
const windows = [];
|
|
2592
|
+
const weekly = kimiWindow("week", coding?.detail ?? data.usage, coding?.detail ?? data.usage);
|
|
2593
|
+
if (weekly)
|
|
2594
|
+
windows.push(weekly);
|
|
2595
|
+
for (const limit of coding?.limits ?? data.limits ?? []) {
|
|
2596
|
+
const duration = limit.window?.duration;
|
|
2597
|
+
const quotaType = duration === 300 ? "5h" : quotaTypeFromSeconds((duration ?? 0) * 60, "window");
|
|
2598
|
+
const window = kimiWindow(quotaType, limit.detail, limit);
|
|
2599
|
+
if (window)
|
|
2600
|
+
windows.push(window);
|
|
2601
|
+
}
|
|
2602
|
+
return {
|
|
2603
|
+
provider: "kimi",
|
|
2604
|
+
account,
|
|
2605
|
+
status: "active",
|
|
2606
|
+
unavailable: false,
|
|
2607
|
+
disabled: auth.disabled === true,
|
|
2608
|
+
windows
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
function unsupported(auth) {
|
|
2612
|
+
const provider = auth.type ?? "unknown";
|
|
2613
|
+
return {
|
|
2614
|
+
provider,
|
|
2615
|
+
account: auth.email ?? provider,
|
|
2616
|
+
status: "unsupported",
|
|
2617
|
+
unavailable: false,
|
|
2618
|
+
disabled: auth.disabled === true,
|
|
2619
|
+
error: "quota endpoint is not known for this provider",
|
|
2620
|
+
windows: []
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
async function readAuthFiles() {
|
|
2624
|
+
if (!Config2.cliproxyAuthDir)
|
|
2625
|
+
return [];
|
|
2626
|
+
const names = await readdir(Config2.cliproxyAuthDir);
|
|
2627
|
+
const out = [];
|
|
2628
|
+
for (const name of names) {
|
|
2629
|
+
if (!name.endsWith(".json"))
|
|
2630
|
+
continue;
|
|
2631
|
+
try {
|
|
2632
|
+
const raw = await readFile(join3(Config2.cliproxyAuthDir, name), "utf-8");
|
|
2633
|
+
const parsed = JSON.parse(raw);
|
|
2634
|
+
out.push(parsed);
|
|
2635
|
+
} catch (err) {
|
|
2636
|
+
logger4.warn("failed to read auth file", { err, name });
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
return out;
|
|
2640
|
+
}
|
|
2641
|
+
var logger4, QuotaProbe;
|
|
2642
|
+
var init_quota = __esm(() => {
|
|
2643
|
+
init_config();
|
|
2644
|
+
init_client();
|
|
2645
|
+
init_logger();
|
|
2646
|
+
logger4 = Logger.fromConfig().child({ component: "quota" });
|
|
2647
|
+
((QuotaProbe) => {
|
|
2648
|
+
async function refresh() {
|
|
2649
|
+
const timestamp = new Date().toISOString();
|
|
2650
|
+
const auths = await readAuthFiles();
|
|
2651
|
+
const accounts = [];
|
|
2652
|
+
for (const auth of auths) {
|
|
2653
|
+
let result;
|
|
2654
|
+
try {
|
|
2655
|
+
if (auth.type === "claude")
|
|
2656
|
+
result = await probeClaude(auth);
|
|
2657
|
+
else if (auth.type === "codex")
|
|
2658
|
+
result = await probeCodex(auth);
|
|
2659
|
+
else if (auth.type === "kimi")
|
|
2660
|
+
result = await probeKimi(auth);
|
|
2661
|
+
else
|
|
2662
|
+
result = unsupported(auth);
|
|
2663
|
+
} catch (err) {
|
|
2664
|
+
result = {
|
|
2665
|
+
provider: auth.type ?? "unknown",
|
|
2666
|
+
account: auth.email ?? auth.type ?? "unknown",
|
|
2667
|
+
status: "error",
|
|
2668
|
+
unavailable: true,
|
|
2669
|
+
disabled: auth.disabled === true,
|
|
2670
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2671
|
+
windows: []
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
const windows = result.windows.map((window) => ({
|
|
2675
|
+
timestamp,
|
|
2676
|
+
provider: result.provider,
|
|
2677
|
+
account: result.account,
|
|
2678
|
+
quota_type: window.quota_type,
|
|
2679
|
+
used_pct: window.used_pct ?? null,
|
|
2680
|
+
remaining: window.used_pct === undefined ? null : Math.max(0, 100 - window.used_pct),
|
|
2681
|
+
remaining_raw: window.used_pct === undefined ? null : `${Math.max(0, 100 - window.used_pct).toFixed(2)}%`,
|
|
2682
|
+
resets_at: window.resets_at ?? null,
|
|
2683
|
+
raw_json: JSON.stringify(window.raw)
|
|
2684
|
+
}));
|
|
2685
|
+
accounts.push({
|
|
2686
|
+
provider: result.provider,
|
|
2687
|
+
account: result.account,
|
|
2688
|
+
status: result.status,
|
|
2689
|
+
unavailable: result.unavailable,
|
|
2690
|
+
disabled: result.disabled,
|
|
2691
|
+
plan: result.plan,
|
|
2692
|
+
refreshed_at: timestamp,
|
|
2693
|
+
error: result.error,
|
|
2694
|
+
windows,
|
|
2695
|
+
local_usage: {
|
|
2696
|
+
five_hour: { since: "", requests: 0, total_tokens: 0, cost_usd: 0 },
|
|
2697
|
+
seven_day: { since: "", requests: 0, total_tokens: 0, cost_usd: 0 }
|
|
2698
|
+
}
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
return { timestamp, accounts, inserted: 0 };
|
|
2702
|
+
}
|
|
2703
|
+
QuotaProbe.refresh = refresh;
|
|
2704
|
+
})(QuotaProbe ||= {});
|
|
2705
|
+
});
|
|
2706
|
+
|
|
2707
|
+
// src/storage/service.ts
|
|
2708
|
+
var exports_service = {};
|
|
2709
|
+
__export(exports_service, {
|
|
2710
|
+
UsageService: () => UsageService
|
|
2711
|
+
});
|
|
2712
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
2713
|
+
var logger5, costBackfillLogger, unmappedSubscriptionWarnings, UsageService;
|
|
2714
|
+
var init_service = __esm(() => {
|
|
2715
|
+
init_account_subscriptions();
|
|
2716
|
+
init_repo();
|
|
2717
|
+
init_pricing();
|
|
2718
|
+
init_cost();
|
|
2719
|
+
init_quota();
|
|
2720
|
+
init_logger();
|
|
2721
|
+
init_config();
|
|
2722
|
+
init_supervisor();
|
|
2723
|
+
logger5 = Logger.fromConfig().child({ component: "usage-service" });
|
|
2724
|
+
costBackfillLogger = Logger.fromConfig().child({ component: "cost" });
|
|
2725
|
+
unmappedSubscriptionWarnings = new Map;
|
|
2726
|
+
((UsageService) => {
|
|
2727
|
+
function create(db, options = {}) {
|
|
2728
|
+
const serviceLogger = options.logger ?? logger5;
|
|
2729
|
+
const now = options.now ?? (() => new Date);
|
|
2730
|
+
function preLog(log) {
|
|
2731
|
+
return RequestRepo.insert(db, log);
|
|
2732
|
+
}
|
|
2733
|
+
async function finalizeUsage(id, log) {
|
|
2734
|
+
const cost = computeCost(log);
|
|
2735
|
+
const logWithCost = { ...log, cost_usd: cost.cost_usd, cost_status: cost.cost_status };
|
|
2736
|
+
const txn = db.transaction(() => {
|
|
2737
|
+
const updated = RequestRepo.updateFinalize(db, id, {
|
|
2738
|
+
provider: logWithCost.provider,
|
|
2739
|
+
model: logWithCost.model,
|
|
2740
|
+
actual_model: logWithCost.actual_model,
|
|
2741
|
+
streamed: logWithCost.streamed,
|
|
2742
|
+
status: logWithCost.status,
|
|
2743
|
+
prompt_tokens: logWithCost.prompt_tokens,
|
|
2744
|
+
completion_tokens: logWithCost.completion_tokens,
|
|
2745
|
+
cache_creation_tokens: logWithCost.cache_creation_tokens,
|
|
2746
|
+
cache_read_tokens: logWithCost.cache_read_tokens,
|
|
2747
|
+
reasoning_tokens: logWithCost.reasoning_tokens ?? 0,
|
|
2748
|
+
total_tokens: logWithCost.total_tokens,
|
|
2749
|
+
cost_usd: cost.cost_usd,
|
|
2750
|
+
incomplete: logWithCost.incomplete,
|
|
2751
|
+
error_code: logWithCost.error_code,
|
|
2752
|
+
latency_ms: logWithCost.latency_ms,
|
|
2753
|
+
finished_at: logWithCost.finished_at,
|
|
2754
|
+
lifecycle_status: logWithCost.lifecycle_status ?? "completed",
|
|
2755
|
+
finalized_at: logWithCost.finalized_at ?? logWithCost.finished_at ?? new Date().toISOString(),
|
|
2756
|
+
error_message: logWithCost.error_message,
|
|
2757
|
+
cost_status: cost.cost_status,
|
|
2758
|
+
subscription_code: logWithCost.subscription_code
|
|
2759
|
+
});
|
|
2760
|
+
if (updated === 0)
|
|
2761
|
+
return false;
|
|
2762
|
+
insertCostAudit(id, logWithCost, cost);
|
|
2763
|
+
const day = logWithCost.started_at.slice(0, 10);
|
|
2764
|
+
UsageRepo.upsertDaily(db, {
|
|
2765
|
+
day,
|
|
2766
|
+
provider: logWithCost.provider,
|
|
2767
|
+
model: logWithCost.model,
|
|
2768
|
+
request_count: 1,
|
|
2769
|
+
prompt_tokens: logWithCost.prompt_tokens,
|
|
2770
|
+
completion_tokens: logWithCost.completion_tokens,
|
|
2771
|
+
cache_creation_tokens: logWithCost.cache_creation_tokens,
|
|
2772
|
+
cache_read_tokens: logWithCost.cache_read_tokens,
|
|
2773
|
+
total_tokens: logWithCost.total_tokens,
|
|
2774
|
+
cost_usd: cost.cost_usd
|
|
2775
|
+
});
|
|
2776
|
+
return true;
|
|
2777
|
+
});
|
|
2778
|
+
return txn();
|
|
2779
|
+
}
|
|
2780
|
+
async function recordUsage(log) {
|
|
2781
|
+
const cost = computeCost(log);
|
|
2782
|
+
const logWithCost = { ...log, cost_usd: cost.cost_usd, cost_status: cost.cost_status };
|
|
2783
|
+
const txn = db.transaction(() => {
|
|
2784
|
+
const id = RequestRepo.insert(db, logWithCost);
|
|
2785
|
+
insertCostAudit(id, logWithCost, cost);
|
|
2786
|
+
const day = log.started_at.slice(0, 10);
|
|
2787
|
+
UsageRepo.upsertDaily(db, {
|
|
2788
|
+
day,
|
|
2789
|
+
provider: log.provider,
|
|
2790
|
+
model: log.model,
|
|
2791
|
+
request_count: 1,
|
|
2792
|
+
prompt_tokens: log.prompt_tokens,
|
|
2793
|
+
completion_tokens: log.completion_tokens,
|
|
2794
|
+
cache_creation_tokens: log.cache_creation_tokens,
|
|
2795
|
+
cache_read_tokens: log.cache_read_tokens,
|
|
2796
|
+
total_tokens: log.total_tokens,
|
|
2797
|
+
cost_usd: cost.cost_usd
|
|
2798
|
+
});
|
|
2799
|
+
return id;
|
|
2800
|
+
});
|
|
2801
|
+
return txn();
|
|
2802
|
+
}
|
|
2803
|
+
async function backfillCosts(options2 = {}) {
|
|
2804
|
+
try {
|
|
2805
|
+
await Pricing.fetchPricing({ force: true });
|
|
2806
|
+
} catch (err) {
|
|
2807
|
+
logger5.warn("pricing refresh failed before cost backfill", { err, event: "cost.backfill_pricing_failed" });
|
|
2808
|
+
}
|
|
2809
|
+
const lookbackMs = options2.lookbackMs ?? Config2.costBackfillLookbackMs;
|
|
2810
|
+
const limitClause = options2.limit && options2.limit > 0 ? " LIMIT ?" : "";
|
|
2811
|
+
const sinceClause = options2.all ? "" : "AND started_at >= ?";
|
|
2812
|
+
const sinceIso = new Date(Date.now() - lookbackMs).toISOString();
|
|
2813
|
+
const params = [];
|
|
2814
|
+
if (!options2.all)
|
|
2815
|
+
params.push(sinceIso);
|
|
2816
|
+
if (options2.limit && options2.limit > 0)
|
|
2817
|
+
params.push(options2.limit);
|
|
2818
|
+
const rows = db.query(`
|
|
2819
|
+
SELECT id, provider, model, prompt_tokens, completion_tokens,
|
|
2820
|
+
cache_creation_tokens, cache_read_tokens, reasoning_tokens, cost_usd, cost_status
|
|
2821
|
+
FROM request_logs
|
|
2822
|
+
WHERE lifecycle_status IN ('completed', 'error')
|
|
2823
|
+
AND cost_status IN ('pending', 'unresolved')
|
|
2824
|
+
${sinceClause}
|
|
2825
|
+
ORDER BY id ASC${limitClause}
|
|
2826
|
+
`).all(...params);
|
|
2827
|
+
let updated = 0;
|
|
2828
|
+
const statusCounts = { ok: 0, pending: 0, unsupported: 0 };
|
|
2829
|
+
const updateTxn = db.transaction(() => {
|
|
2830
|
+
const updateLog = db.prepare("UPDATE request_logs SET cost_usd = ?, cost_status = 'ok' WHERE id = ? AND cost_status IN ('pending', 'unresolved')");
|
|
2831
|
+
for (const row of rows) {
|
|
2832
|
+
const cost = computeCost(row);
|
|
2833
|
+
statusCounts[cost.cost_status] += 1;
|
|
2834
|
+
insertCostAudit(row.id, row, cost);
|
|
2835
|
+
if ((row.cost_status === "pending" || row.cost_status === "unresolved") && cost.cost_status === "ok") {
|
|
2836
|
+
const result = updateLog.run(cost.cost_usd, row.id);
|
|
2837
|
+
updated += result.changes;
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
db.exec(`
|
|
2841
|
+
DELETE FROM daily_usage;
|
|
2842
|
+
INSERT INTO daily_usage (
|
|
2843
|
+
day, provider, model, request_count, prompt_tokens,
|
|
2844
|
+
completion_tokens, cache_creation_tokens, cache_read_tokens,
|
|
2845
|
+
total_tokens, cost_usd
|
|
2846
|
+
)
|
|
2847
|
+
SELECT
|
|
2848
|
+
substr(started_at, 1, 10), provider, model, COUNT(*),
|
|
2849
|
+
SUM(prompt_tokens), SUM(completion_tokens), SUM(cache_creation_tokens),
|
|
2850
|
+
SUM(cache_read_tokens), SUM(total_tokens), SUM(cost_usd)
|
|
2851
|
+
FROM request_logs
|
|
2852
|
+
GROUP BY substr(started_at, 1, 10), provider, model;
|
|
2853
|
+
`);
|
|
2854
|
+
});
|
|
2855
|
+
updateTxn();
|
|
2856
|
+
return { scanned: rows.length, updated, ...statusCounts };
|
|
2857
|
+
}
|
|
2858
|
+
function computeCost(log) {
|
|
2859
|
+
return Cost.compute(Cost.inputsFromLog(log));
|
|
2860
|
+
}
|
|
2861
|
+
function insertCostAudit(requestLogId, log, cost) {
|
|
2862
|
+
RequestRepo.insertCostAudit(db, {
|
|
2863
|
+
request_log_id: requestLogId,
|
|
2864
|
+
model: log.model,
|
|
2865
|
+
provider: log.provider,
|
|
2866
|
+
source: cost.source,
|
|
2867
|
+
base_cost_usd: cost.cost_usd
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
function getToday() {
|
|
2871
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
2872
|
+
const breakdown = UsageRepo.getDaily(db, today);
|
|
2873
|
+
const totals = breakdown.reduce((acc, row) => ({
|
|
2874
|
+
requests: acc.requests + row.request_count,
|
|
2875
|
+
total_tokens: acc.total_tokens + row.total_tokens,
|
|
2876
|
+
cost_usd: acc.cost_usd + row.cost_usd
|
|
2877
|
+
}), { requests: 0, total_tokens: 0, cost_usd: 0 });
|
|
2878
|
+
return {
|
|
2879
|
+
date: today,
|
|
2880
|
+
requests: totals.requests,
|
|
2881
|
+
total_tokens: totals.total_tokens,
|
|
2882
|
+
cost_usd: totals.cost_usd,
|
|
2883
|
+
breakdown
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
function getDateRange(from, to) {
|
|
2887
|
+
const rows = UsageRepo.getRange(db, from, to);
|
|
2888
|
+
const byDay = new Map;
|
|
2889
|
+
for (const row of rows) {
|
|
2890
|
+
const existing = byDay.get(row.day) ?? [];
|
|
2891
|
+
existing.push(row);
|
|
2892
|
+
byDay.set(row.day, existing);
|
|
2893
|
+
}
|
|
2894
|
+
return Array.from(byDay.entries()).map(([day, breakdown]) => {
|
|
2895
|
+
const totals = breakdown.reduce((acc, row) => ({
|
|
2896
|
+
requests: acc.requests + row.request_count,
|
|
2897
|
+
total_tokens: acc.total_tokens + row.total_tokens,
|
|
2898
|
+
cost_usd: acc.cost_usd + row.cost_usd
|
|
2899
|
+
}), { requests: 0, total_tokens: 0, cost_usd: 0 });
|
|
2900
|
+
return { date: day, ...totals, breakdown };
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
function getModelBreakdown(day) {
|
|
2904
|
+
return UsageRepo.getDaily(db, day);
|
|
2905
|
+
}
|
|
2906
|
+
function getProviderBreakdown(day) {
|
|
2907
|
+
const rows = UsageRepo.getDaily(db, day);
|
|
2908
|
+
const byProvider = new Map;
|
|
2909
|
+
for (const row of rows) {
|
|
2910
|
+
const existing = byProvider.get(row.provider) ?? {
|
|
2911
|
+
provider: row.provider,
|
|
2912
|
+
request_count: 0,
|
|
2913
|
+
total_tokens: 0,
|
|
2914
|
+
cost_usd: 0
|
|
2915
|
+
};
|
|
2916
|
+
existing.request_count += row.request_count;
|
|
2917
|
+
existing.total_tokens += row.total_tokens;
|
|
2918
|
+
existing.cost_usd += row.cost_usd;
|
|
2919
|
+
byProvider.set(row.provider, existing);
|
|
2920
|
+
}
|
|
2921
|
+
return Array.from(byProvider.values());
|
|
2922
|
+
}
|
|
2923
|
+
function getTotalStats() {
|
|
2924
|
+
const stmt = db.prepare(`
|
|
2925
|
+
SELECT
|
|
2926
|
+
COUNT(*) as total_requests,
|
|
2927
|
+
SUM(total_tokens) as total_tokens,
|
|
2928
|
+
SUM(cost_usd) as total_cost_usd,
|
|
2929
|
+
MIN(started_at) as first_request_at,
|
|
2930
|
+
MAX(started_at) as last_request_at
|
|
2931
|
+
FROM request_logs
|
|
2932
|
+
`);
|
|
2933
|
+
const row = stmt.get();
|
|
2934
|
+
return {
|
|
2935
|
+
total_requests: Number(row.total_requests ?? 0),
|
|
2936
|
+
total_tokens: Number(row.total_tokens ?? 0),
|
|
2937
|
+
total_cost_usd: Number(row.total_cost_usd ?? 0),
|
|
2938
|
+
first_request_at: row.first_request_at ?? null,
|
|
2939
|
+
last_request_at: row.last_request_at ?? null
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
function getRecentLogs(limit, offset, tool, clientId) {
|
|
2943
|
+
return RequestRepo.getRecent(db, limit, offset, tool, clientId);
|
|
2944
|
+
}
|
|
2945
|
+
function getLogById(id) {
|
|
2946
|
+
return RequestRepo.getById(db, id);
|
|
2947
|
+
}
|
|
2948
|
+
function getUncorrelatedLogs(sinceMs, limit) {
|
|
2949
|
+
return RequestRepo.getUncorrelated(db, sinceMs, limit);
|
|
2950
|
+
}
|
|
2951
|
+
function applyCorrelation(id, log, fields) {
|
|
2952
|
+
const txn = db.transaction(() => {
|
|
2953
|
+
RequestRepo.applyCorrelation(db, id, fields);
|
|
2954
|
+
if (fields.cliproxy_account) {
|
|
2955
|
+
applySubscriptionAttribution(db, id, fields.cliproxy_account, serviceLogger, now);
|
|
2956
|
+
UsageRepo.upsertDailyAccount(db, {
|
|
2957
|
+
day: log.started_at.slice(0, 10),
|
|
2958
|
+
provider: log.provider,
|
|
2959
|
+
model: log.model,
|
|
2960
|
+
cliproxy_account: fields.cliproxy_account,
|
|
2961
|
+
cliproxy_auth_index: fields.cliproxy_auth_index,
|
|
2962
|
+
request_count: 1,
|
|
2963
|
+
prompt_tokens: log.prompt_tokens,
|
|
2964
|
+
completion_tokens: log.completion_tokens,
|
|
2965
|
+
cache_creation_tokens: log.cache_creation_tokens,
|
|
2966
|
+
cache_read_tokens: log.cache_read_tokens,
|
|
2967
|
+
reasoning_tokens: fields.reasoning_tokens ?? 0,
|
|
2968
|
+
total_tokens: log.total_tokens,
|
|
2969
|
+
cost_usd: log.cost_usd
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2973
|
+
txn();
|
|
2974
|
+
}
|
|
2975
|
+
function applySubscriptionAttribution(database, requestLogId, cliproxyAccount, targetLogger, currentDate) {
|
|
2976
|
+
const binding = AccountSubscriptionRepo.get(database, cliproxyAccount);
|
|
2977
|
+
if (binding) {
|
|
2978
|
+
RequestRepo.applySubscription(database, requestLogId, binding.subscription_code);
|
|
2979
|
+
return;
|
|
2980
|
+
}
|
|
2981
|
+
warnUnmappedSubscription(targetLogger, cliproxyAccount, currentDate());
|
|
2982
|
+
}
|
|
2983
|
+
function getAccountSummary(from, to) {
|
|
2984
|
+
return UsageRepo.getAccountSummary(db, from, to);
|
|
2985
|
+
}
|
|
2986
|
+
function getAccountDaily(day) {
|
|
2987
|
+
return UsageRepo.getDailyByAccount(db, day);
|
|
2988
|
+
}
|
|
2989
|
+
function getAccountRange(from, to) {
|
|
2990
|
+
return UsageRepo.getAccountRange(db, from, to);
|
|
2991
|
+
}
|
|
2992
|
+
function withLocalUsage(report) {
|
|
2993
|
+
const now2 = Date.now();
|
|
2994
|
+
const fiveHourSince = new Date(now2 - 5 * 60 * 60 * 1000).toISOString();
|
|
2995
|
+
const sevenDaySince = new Date(now2 - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
2996
|
+
const localProvider = report.provider === "claude" ? "anthropic" : "openai";
|
|
2997
|
+
return {
|
|
2998
|
+
...report,
|
|
2999
|
+
local_usage: {
|
|
3000
|
+
five_hour: QuotaRepo.getLocalWindowUsage(db, localProvider, report.account, fiveHourSince),
|
|
3001
|
+
seven_day: QuotaRepo.getLocalWindowUsage(db, localProvider, report.account, sevenDaySince)
|
|
3002
|
+
}
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
async function refreshQuotas() {
|
|
3006
|
+
const result = await QuotaProbe.refresh();
|
|
3007
|
+
let inserted = 0;
|
|
3008
|
+
const accounts = result.accounts.map(withLocalUsage);
|
|
3009
|
+
const txn = db.transaction(() => {
|
|
3010
|
+
for (const account of accounts) {
|
|
3011
|
+
for (const snapshot of account.windows) {
|
|
3012
|
+
QuotaRepo.insertSnapshot(db, snapshot);
|
|
3013
|
+
inserted += 1;
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
3017
|
+
txn();
|
|
3018
|
+
return { ...result, inserted, accounts };
|
|
3019
|
+
}
|
|
3020
|
+
async function startQuotaRefresh(options2 = {}) {
|
|
3021
|
+
if (!Config2.cliproxyAuthDir) {
|
|
3022
|
+
logger5.info("quota background refresh skipped", {
|
|
3023
|
+
event: "quota.refresh_skipped",
|
|
3024
|
+
reason: "missing_auth_dir"
|
|
3025
|
+
});
|
|
3026
|
+
return null;
|
|
3027
|
+
}
|
|
3028
|
+
let authFileNames;
|
|
3029
|
+
try {
|
|
3030
|
+
authFileNames = await readdir2(Config2.cliproxyAuthDir);
|
|
3031
|
+
} catch (err) {
|
|
3032
|
+
logger5.warn("quota background refresh skipped", {
|
|
3033
|
+
event: "quota.refresh_skipped",
|
|
3034
|
+
reason: "auth_dir_unreadable",
|
|
3035
|
+
err,
|
|
3036
|
+
path: Config2.cliproxyAuthDir
|
|
3037
|
+
});
|
|
3038
|
+
return null;
|
|
3039
|
+
}
|
|
3040
|
+
if (!authFileNames.some((name) => name.endsWith(".json"))) {
|
|
3041
|
+
logger5.info("quota background refresh skipped", {
|
|
3042
|
+
event: "quota.refresh_skipped",
|
|
3043
|
+
reason: "no_auth_files",
|
|
3044
|
+
path: Config2.cliproxyAuthDir
|
|
3045
|
+
});
|
|
3046
|
+
return null;
|
|
3047
|
+
}
|
|
3048
|
+
return Supervisor.run("quota-refresh", async () => {
|
|
3049
|
+
await refreshQuotas();
|
|
3050
|
+
}, {
|
|
3051
|
+
intervalMs: options2.intervalMs ?? Config2.quotaRefreshIntervalMs,
|
|
3052
|
+
signal: options2.signal
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
function getLatestQuotas() {
|
|
3056
|
+
return QuotaRepo.getLatest(db);
|
|
3057
|
+
}
|
|
3058
|
+
return {
|
|
3059
|
+
db,
|
|
3060
|
+
preLog,
|
|
3061
|
+
finalizeUsage,
|
|
3062
|
+
recordUsage,
|
|
3063
|
+
getToday,
|
|
3064
|
+
getDateRange,
|
|
3065
|
+
getModelBreakdown,
|
|
3066
|
+
getProviderBreakdown,
|
|
3067
|
+
getTotalStats,
|
|
3068
|
+
getRecentLogs,
|
|
3069
|
+
getLogById,
|
|
3070
|
+
backfillCosts,
|
|
3071
|
+
getUncorrelatedLogs,
|
|
3072
|
+
applyCorrelation,
|
|
3073
|
+
getAccountSummary,
|
|
3074
|
+
getAccountDaily,
|
|
3075
|
+
getAccountRange,
|
|
3076
|
+
refreshQuotas,
|
|
3077
|
+
startQuotaRefresh,
|
|
3078
|
+
getLatestQuotas
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
UsageService.create = create;
|
|
3082
|
+
function warnUnmappedSubscription(targetLogger, cliproxyAccount, date = new Date) {
|
|
3083
|
+
const day = date.toISOString().slice(0, 10);
|
|
3084
|
+
const key = `${cliproxyAccount}:${day}`;
|
|
3085
|
+
if (unmappedSubscriptionWarnings.has(key))
|
|
3086
|
+
return;
|
|
3087
|
+
unmappedSubscriptionWarnings.set(key, true);
|
|
3088
|
+
targetLogger.warn("plans unmapped", {
|
|
3089
|
+
event: "plans.unmapped",
|
|
3090
|
+
cliproxy_account: cliproxyAccount
|
|
3091
|
+
});
|
|
3092
|
+
}
|
|
3093
|
+
UsageService.warnUnmappedSubscription = warnUnmappedSubscription;
|
|
3094
|
+
function startCostBackfillLoop(service, options = {}) {
|
|
3095
|
+
return Supervisor.run("cost-backfill", async () => {
|
|
3096
|
+
const result = await service.backfillCosts();
|
|
3097
|
+
costBackfillLogger.info("cost backfill completed", { event: "cost.backfill", ...result });
|
|
3098
|
+
}, {
|
|
3099
|
+
intervalMs: options.intervalMs ?? Config2.costBackfillIntervalMs,
|
|
3100
|
+
signal: options.signal
|
|
3101
|
+
});
|
|
3102
|
+
}
|
|
3103
|
+
UsageService.startCostBackfillLoop = startCostBackfillLoop;
|
|
3104
|
+
})(UsageService ||= {});
|
|
3105
|
+
});
|
|
3106
|
+
|
|
3107
|
+
// src/provider/registry.ts
|
|
3108
|
+
var exports_registry = {};
|
|
3109
|
+
__export(exports_registry, {
|
|
3110
|
+
ProviderRegistry: () => ProviderRegistry
|
|
3111
|
+
});
|
|
3112
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
3113
|
+
var ProviderRegistry;
|
|
3114
|
+
var init_registry = __esm(() => {
|
|
3115
|
+
init_config();
|
|
3116
|
+
init_logger();
|
|
3117
|
+
init_registry_schema();
|
|
3118
|
+
((ProviderRegistry) => {
|
|
3119
|
+
const logger6 = Logger.fromConfig().child({ component: "provider-registry" });
|
|
3120
|
+
let cache = null;
|
|
3121
|
+
function loadProviders(options = {}) {
|
|
3122
|
+
if (cache && !options.force)
|
|
3123
|
+
return cache.providers;
|
|
3124
|
+
const customSource = readCustomConfig();
|
|
3125
|
+
const customProviders = customSource ? parseCustomProviders(customSource.raw) : [];
|
|
3126
|
+
const providers = mergeProviders([...builtInProviders(), ...customProviders]);
|
|
3127
|
+
const lastLoadedAt = new Date().toISOString();
|
|
3128
|
+
cache = {
|
|
3129
|
+
providers,
|
|
3130
|
+
source: customSource?.source ?? "built-in",
|
|
3131
|
+
configPath: customSource?.configPath,
|
|
3132
|
+
lastLoadedAt
|
|
3133
|
+
};
|
|
3134
|
+
return providers;
|
|
3135
|
+
}
|
|
3136
|
+
ProviderRegistry.loadProviders = loadProviders;
|
|
3137
|
+
function all() {
|
|
3138
|
+
return loadProviders();
|
|
3139
|
+
}
|
|
3140
|
+
ProviderRegistry.all = all;
|
|
3141
|
+
function handlesPath(path) {
|
|
3142
|
+
return loadProviders().some((provider) => provider.paths.includes(path));
|
|
3143
|
+
}
|
|
3144
|
+
ProviderRegistry.handlesPath = handlesPath;
|
|
3145
|
+
function resolve(input) {
|
|
3146
|
+
const providers = loadProviders();
|
|
3147
|
+
if (input.provider) {
|
|
3148
|
+
const explicit = providers.find((provider) => provider.id === input.provider);
|
|
3149
|
+
if (!explicit || !matchesPath(explicit, input.path) || !matchesModel(explicit, input.model))
|
|
3150
|
+
return null;
|
|
3151
|
+
return explicit;
|
|
3152
|
+
}
|
|
3153
|
+
const candidates = providers.filter((provider) => matchesPath(provider, input.path));
|
|
3154
|
+
if (input.model) {
|
|
3155
|
+
const modelMatch = candidates.find((provider) => matchesModel(provider, input.model));
|
|
3156
|
+
if (modelMatch)
|
|
3157
|
+
return modelMatch;
|
|
3158
|
+
}
|
|
3159
|
+
return candidates[0] ?? null;
|
|
3160
|
+
}
|
|
3161
|
+
ProviderRegistry.resolve = resolve;
|
|
3162
|
+
function forceReload() {
|
|
3163
|
+
return loadProviders({ force: true });
|
|
3164
|
+
}
|
|
3165
|
+
ProviderRegistry.forceReload = forceReload;
|
|
3166
|
+
function configPath() {
|
|
3167
|
+
return process.env.PROVIDERS_CONFIG_PATH?.trim() || undefined;
|
|
3168
|
+
}
|
|
3169
|
+
ProviderRegistry.configPath = configPath;
|
|
3170
|
+
function sourceInfo() {
|
|
3171
|
+
if (!cache) {
|
|
3172
|
+
return { source: "built-in", lastLoadedAt: null };
|
|
3173
|
+
}
|
|
3174
|
+
return {
|
|
3175
|
+
source: cache.source,
|
|
3176
|
+
configPath: cache.configPath,
|
|
3177
|
+
lastLoadedAt: cache.lastLoadedAt
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
ProviderRegistry.sourceInfo = sourceInfo;
|
|
3181
|
+
function builtInProviders() {
|
|
3182
|
+
return [
|
|
3183
|
+
{
|
|
3184
|
+
id: "anthropic",
|
|
3185
|
+
type: "anthropic",
|
|
3186
|
+
paths: ["/v1/messages"],
|
|
3187
|
+
upstreamBaseUrl: Config2.cliProxyApiUrl,
|
|
3188
|
+
upstreamPath: "/v1/messages",
|
|
3189
|
+
auth: "preserve"
|
|
3190
|
+
},
|
|
3191
|
+
{
|
|
3192
|
+
id: "openai",
|
|
3193
|
+
type: "openai-compatible",
|
|
3194
|
+
paths: ["/v1/chat/completions"],
|
|
3195
|
+
upstreamBaseUrl: Config2.cliProxyApiUrl,
|
|
3196
|
+
upstreamPath: "/v1/chat/completions",
|
|
3197
|
+
auth: "preserve"
|
|
3198
|
+
}
|
|
3199
|
+
];
|
|
3200
|
+
}
|
|
3201
|
+
function readCustomConfig() {
|
|
3202
|
+
const inline = process.env.PROVIDERS_JSON;
|
|
3203
|
+
if (inline !== undefined && inline.trim() !== "")
|
|
3204
|
+
return { source: "PROVIDERS_JSON", raw: inline };
|
|
3205
|
+
const path = configPath();
|
|
3206
|
+
if (!path)
|
|
3207
|
+
return null;
|
|
3208
|
+
try {
|
|
3209
|
+
return { source: "PROVIDERS_CONFIG_PATH", raw: readFileSync4(path, "utf-8"), configPath: path };
|
|
3210
|
+
} catch (err) {
|
|
3211
|
+
logger6.warn("provider config could not be read", {
|
|
3212
|
+
event: "provider.config.invalid",
|
|
3213
|
+
source: "PROVIDERS_CONFIG_PATH",
|
|
3214
|
+
path: "PROVIDERS_CONFIG_PATH",
|
|
3215
|
+
configPath: path,
|
|
3216
|
+
err
|
|
3217
|
+
});
|
|
3218
|
+
return null;
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
function parseCustomProviders(raw) {
|
|
3222
|
+
let parsed;
|
|
3223
|
+
try {
|
|
3224
|
+
parsed = JSON.parse(raw);
|
|
3225
|
+
} catch (err) {
|
|
3226
|
+
logger6.warn("provider config JSON is invalid", {
|
|
3227
|
+
event: "provider.config.invalid",
|
|
3228
|
+
path: "providers",
|
|
3229
|
+
err
|
|
3230
|
+
});
|
|
3231
|
+
return [];
|
|
3232
|
+
}
|
|
3233
|
+
if (!isRecord2(parsed) || !Array.isArray(parsed.providers)) {
|
|
3234
|
+
const result = validateProviderDocument(parsed);
|
|
3235
|
+
logger6.warn("provider config document is invalid", {
|
|
3236
|
+
event: "provider.config.invalid",
|
|
3237
|
+
path: result.issues[0]?.path ?? "providers",
|
|
3238
|
+
issues: result.issues
|
|
3239
|
+
});
|
|
3240
|
+
return [];
|
|
3241
|
+
}
|
|
3242
|
+
const providers = [];
|
|
3243
|
+
parsed.providers.forEach((entry, index) => {
|
|
3244
|
+
const result = parseProviderInput(entry, `providers[${index}]`);
|
|
3245
|
+
if (result.provider) {
|
|
3246
|
+
providers.push(result.provider);
|
|
3247
|
+
return;
|
|
3248
|
+
}
|
|
3249
|
+
warnInvalidEntry(entry, `providers[${index}]`, result.issues);
|
|
3250
|
+
});
|
|
3251
|
+
return providers;
|
|
3252
|
+
}
|
|
3253
|
+
function warnInvalidEntry(entry, path, issues) {
|
|
3254
|
+
logger6.warn("provider config entry is invalid", {
|
|
3255
|
+
event: "provider.config.invalid",
|
|
3256
|
+
providerId: providerIdForLog(entry),
|
|
3257
|
+
path,
|
|
3258
|
+
issues
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
function providerIdForLog(entry) {
|
|
3262
|
+
if (!isRecord2(entry) || typeof entry.id !== "string" || entry.id.trim() === "")
|
|
3263
|
+
return;
|
|
3264
|
+
return entry.id.trim();
|
|
3265
|
+
}
|
|
3266
|
+
function mergeProviders(providers) {
|
|
3267
|
+
const byId = new Map;
|
|
3268
|
+
for (const provider of providers)
|
|
3269
|
+
byId.set(provider.id, provider);
|
|
3270
|
+
return Array.from(byId.values());
|
|
3271
|
+
}
|
|
3272
|
+
function matchesPath(provider, path) {
|
|
3273
|
+
return provider.paths.includes(path);
|
|
3274
|
+
}
|
|
3275
|
+
function matchesModel(provider, model) {
|
|
3276
|
+
if (!model || !provider.models || provider.models.length === 0)
|
|
3277
|
+
return true;
|
|
3278
|
+
return provider.models.some((entry) => model === entry || model.startsWith(entry));
|
|
3279
|
+
}
|
|
3280
|
+
function isRecord2(value) {
|
|
3281
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3282
|
+
}
|
|
3283
|
+
})(ProviderRegistry ||= {});
|
|
3284
|
+
});
|
|
3285
|
+
|
|
3286
|
+
// src/cli.ts
|
|
3287
|
+
init_plans_default();
|
|
3288
|
+
init_plans();
|
|
3289
|
+
init_account_subscriptions();
|
|
3290
|
+
init_db();
|
|
3291
|
+
init_validate();
|
|
3292
|
+
init_logger();
|
|
3293
|
+
init_registry_schema();
|
|
3294
|
+
import { mkdir as mkdir2, chmod, copyFile, rm, cp, writeFile, open, rename, stat } from "fs/promises";
|
|
3295
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
3296
|
+
import { dirname as dirname2, join as join4, resolve } from "path";
|
|
3297
|
+
import { homedir, platform } from "os";
|
|
3298
|
+
import { createInterface } from "readline/promises";
|
|
3299
|
+
import { stdin as input, stdout as output } from "process";
|
|
3300
|
+
var APP_NAME = "agent-cli-proxy";
|
|
3301
|
+
var HOME = homedir();
|
|
3302
|
+
var XDG_CONFIG_HOME = process.env.XDG_CONFIG_HOME ?? join4(HOME, ".config");
|
|
3303
|
+
var XDG_DATA_HOME = process.env.XDG_DATA_HOME ?? join4(HOME, ".local", "share");
|
|
3304
|
+
var defaultConfigDir = join4(XDG_CONFIG_HOME, APP_NAME);
|
|
3305
|
+
var defaultDataDir = join4(XDG_DATA_HOME, APP_NAME);
|
|
3306
|
+
var defaultRuntimeDir = join4(XDG_DATA_HOME, APP_NAME, "runtime");
|
|
3307
|
+
var defaultEnvPath = join4(defaultConfigDir, ".env");
|
|
3308
|
+
var defaultDbPath = join4(defaultDataDir, "proxy.db");
|
|
3309
|
+
var defaultPricingCachePath = join4(defaultDataDir, "pricing-cache.json");
|
|
3310
|
+
var defaultPlansPath = join4(defaultConfigDir, "plans.json");
|
|
3311
|
+
var defaultProvidersPath = join4(defaultConfigDir, "providers.json");
|
|
3312
|
+
var packageRoot = resolve(dirname2(Bun.fileURLToPath(import.meta.url)), "..");
|
|
3313
|
+
var packagedDistDir = dirname2(Bun.fileURLToPath(import.meta.url));
|
|
3314
|
+
var logger6 = Logger.fromConfig().child({ component: "cli" });
|
|
3315
|
+
function writeOut(message = "") {
|
|
3316
|
+
process.stdout.write(`${message}
|
|
3317
|
+
`);
|
|
3318
|
+
}
|
|
3319
|
+
function writeErr(message = "") {
|
|
3320
|
+
process.stderr.write(`${message}
|
|
3321
|
+
`);
|
|
3322
|
+
}
|
|
3323
|
+
function parseArgs(argv) {
|
|
3324
|
+
const positional = [];
|
|
3325
|
+
const flags = new Map;
|
|
3326
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
3327
|
+
const arg = argv[index];
|
|
3328
|
+
if (!arg.startsWith("--")) {
|
|
3329
|
+
positional.push(arg);
|
|
3330
|
+
continue;
|
|
3331
|
+
}
|
|
3332
|
+
const eq = arg.indexOf("=");
|
|
3333
|
+
if (eq > 0) {
|
|
3334
|
+
const name = arg.slice(0, eq);
|
|
3335
|
+
const value = arg.slice(eq + 1);
|
|
3336
|
+
if (value.startsWith("--"))
|
|
3337
|
+
throw new Error(`${name} value must not start with --`);
|
|
3338
|
+
flags.set(name, value);
|
|
3339
|
+
continue;
|
|
3340
|
+
}
|
|
3341
|
+
const next = argv[index + 1];
|
|
3342
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
3343
|
+
flags.set(arg, next);
|
|
3344
|
+
index += 1;
|
|
3345
|
+
} else {
|
|
3346
|
+
flags.set(arg, true);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
return { positional, flags };
|
|
3350
|
+
}
|
|
3351
|
+
function getFlagValue(args, name) {
|
|
3352
|
+
const value = args.flags.get(name);
|
|
3353
|
+
if (value === undefined)
|
|
3354
|
+
return;
|
|
3355
|
+
if (value === true)
|
|
3356
|
+
throw new Error(`${name} requires a value`);
|
|
3357
|
+
if (value.startsWith("--"))
|
|
3358
|
+
throw new Error(`${name} value must not start with --`);
|
|
3359
|
+
return value;
|
|
3360
|
+
}
|
|
3361
|
+
function hasFlag(args, name) {
|
|
3362
|
+
return args.flags.has(name);
|
|
3363
|
+
}
|
|
3364
|
+
async function main() {
|
|
3365
|
+
const exitCode = await runCli(process.argv.slice(2));
|
|
3366
|
+
if (exitCode !== 0)
|
|
3367
|
+
process.exit(exitCode);
|
|
3368
|
+
}
|
|
3369
|
+
async function runCli(argv) {
|
|
3370
|
+
try {
|
|
3371
|
+
const args = parseArgs(argv);
|
|
3372
|
+
const [command = hasFlag(args, "--help") ? "help" : "help", subcommand = ""] = args.positional;
|
|
3373
|
+
const ctx = { args };
|
|
3374
|
+
switch (command) {
|
|
3375
|
+
case "init":
|
|
3376
|
+
await initCommand(ctx);
|
|
3377
|
+
return 0;
|
|
3378
|
+
case "doctor":
|
|
3379
|
+
return await doctorCommand(ctx);
|
|
3380
|
+
case "db":
|
|
3381
|
+
if (subcommand === "init") {
|
|
3382
|
+
await initDbCommand(ctx);
|
|
3383
|
+
return 0;
|
|
3384
|
+
}
|
|
3385
|
+
break;
|
|
3386
|
+
case "service":
|
|
3387
|
+
return await serviceCommand(ctx, subcommand);
|
|
3388
|
+
case "backfill-costs":
|
|
3389
|
+
await backfillCostsCommand(ctx);
|
|
3390
|
+
return 0;
|
|
3391
|
+
case "plans":
|
|
3392
|
+
await plansCommand(ctx, subcommand);
|
|
3393
|
+
return 0;
|
|
3394
|
+
case "providers":
|
|
3395
|
+
await providersCommand(ctx, subcommand);
|
|
3396
|
+
return 0;
|
|
3397
|
+
case "paths":
|
|
3398
|
+
printPaths();
|
|
3399
|
+
return 0;
|
|
3400
|
+
case "help":
|
|
3401
|
+
case "--help":
|
|
3402
|
+
case "-h":
|
|
3403
|
+
printHelp();
|
|
3404
|
+
return 0;
|
|
3405
|
+
}
|
|
3406
|
+
writeErr(`Unknown command: ${command}${subcommand ? ` ${subcommand}` : ""}`);
|
|
3407
|
+
printHelp();
|
|
3408
|
+
return 1;
|
|
3409
|
+
} catch (err) {
|
|
3410
|
+
if (err instanceof ConfigError || err instanceof Error && err.code === "CONFIG_INVALID") {
|
|
3411
|
+
logger6.error("configuration validation failed", { event: "config.error", err, issues: err.issues });
|
|
3412
|
+
writeErr(err instanceof Error ? err.message : String(err));
|
|
3413
|
+
return 1;
|
|
3414
|
+
}
|
|
3415
|
+
writeErr(err instanceof Error ? err.message : String(err));
|
|
3416
|
+
return 1;
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
async function initCommand(ctx) {
|
|
3420
|
+
if (hasFlag(ctx.args, "--non-interactive")) {
|
|
3421
|
+
await initNonInteractive(ctx);
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3424
|
+
await initInteractive(ctx);
|
|
3425
|
+
}
|
|
3426
|
+
async function initInteractive(ctx) {
|
|
3427
|
+
const rl = createInterface({ input, output });
|
|
3428
|
+
try {
|
|
3429
|
+
writeOut(`agent-cli-proxy installer
|
|
3430
|
+
`);
|
|
3431
|
+
const envPath = await ask(rl, "Config .env path", getFlagValue(ctx.args, "--env") ?? defaultEnvPath);
|
|
3432
|
+
const dataDir = await ask(rl, "Data directory", getFlagValue(ctx.args, "--data-dir") ?? defaultDataDir);
|
|
3433
|
+
const runtimeDir = await ask(rl, "Runtime directory", getFlagValue(ctx.args, "--runtime-dir") ?? defaultRuntimeDir);
|
|
3434
|
+
const host = await ask(rl, "Proxy host", "127.0.0.1");
|
|
3435
|
+
const port = await ask(rl, "Proxy port", "3100");
|
|
3436
|
+
const cliProxyApiUrl = await ask(rl, "CLIProxyAPI URL", "http://localhost:8317");
|
|
3437
|
+
const cliProxyApiKey = await askSecret(rl, "CLIProxyAPI proxy key", "proxy");
|
|
3438
|
+
const exposeAdmin = host !== "127.0.0.1" && host !== "localhost" && host !== "::1";
|
|
3439
|
+
const adminApiKey = exposeAdmin || await confirm(rl, "Generate ADMIN_API_KEY for admin API?", false) ? crypto.randomUUID().replaceAll("-", "") : "";
|
|
3440
|
+
const enableCliproxyCorrelation = await confirm(rl, "Enable CLIProxyAPI account correlation?", false);
|
|
3441
|
+
const cliproxyMgmtKey = enableCliproxyCorrelation ? await askSecret(rl, "CLIProxyAPI management key", "") : "";
|
|
3442
|
+
const enableQuotaRefresh = await confirm(rl, "Enable subscription quota checks from local CLIProxyAPI auth files?", false);
|
|
3443
|
+
const cliproxyAuthDir = enableQuotaRefresh ? await ask(rl, "CLIProxyAPI auth directory", join4(HOME, ".cli-proxy-api")) : "";
|
|
3444
|
+
const env = buildEnv({
|
|
3445
|
+
host,
|
|
3446
|
+
port,
|
|
3447
|
+
adminApiKey,
|
|
3448
|
+
cliProxyApiUrl,
|
|
3449
|
+
cliProxyApiKey,
|
|
3450
|
+
dataDir,
|
|
3451
|
+
cliproxyMgmtKey,
|
|
3452
|
+
cliproxyAuthDir
|
|
3453
|
+
});
|
|
3454
|
+
await completeInit(envPath, dataDir, runtimeDir, env, writeOptions(ctx));
|
|
3455
|
+
if (await confirm(rl, "Install user daemon now?", platform() === "linux" || platform() === "darwin")) {
|
|
3456
|
+
await installRuntime(runtimeDir, envPath);
|
|
3457
|
+
await installService(ctx, runtimeDir, envPath);
|
|
3458
|
+
}
|
|
3459
|
+
} finally {
|
|
3460
|
+
rl.close();
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
async function initNonInteractive(ctx) {
|
|
3464
|
+
logger6.info("non-interactive init started", { event: "cli.init.non_interactive" });
|
|
3465
|
+
const envPath = getFlagValue(ctx.args, "--env") ?? process.env.AGENT_CLI_PROXY_ENV ?? defaultEnvPath;
|
|
3466
|
+
const dataDir = getFlagValue(ctx.args, "--data-dir") ?? process.env.AGENT_CLI_PROXY_DATA_DIR ?? defaultDataDir;
|
|
3467
|
+
const runtimeDir = getFlagValue(ctx.args, "--runtime-dir") ?? process.env.AGENT_CLI_PROXY_RUNTIME_DIR ?? defaultRuntimeDir;
|
|
3468
|
+
const host = getFlagValue(ctx.args, "--host") ?? process.env.PROXY_HOST ?? "127.0.0.1";
|
|
3469
|
+
const port = getFlagValue(ctx.args, "--port") ?? process.env.PROXY_PORT ?? "3100";
|
|
3470
|
+
const cliProxyApiUrl = getFlagValue(ctx.args, "--cliproxy-api-url") ?? process.env.CLI_PROXY_API_URL;
|
|
3471
|
+
if (!cliProxyApiUrl && process.env.PROXY_LOCAL_OK !== "1") {
|
|
3472
|
+
throw new Error("CLI_PROXY_API_URL is required for init --non-interactive unless PROXY_LOCAL_OK=1");
|
|
3473
|
+
}
|
|
3474
|
+
const cliProxyApiKey = readSecretFromEnvFlag(ctx.args, "--cliproxy-api-key-env", "CLI_PROXY_API_KEY") ?? "proxy";
|
|
3475
|
+
const adminApiKey = getFlagValue(ctx.args, "--admin-token") ?? readNamedEnv(ctx.args, "--admin-token-env") ?? process.env.ADMIN_API_KEY ?? "";
|
|
3476
|
+
const cliproxyMgmtKey = readNamedEnv(ctx.args, "--cliproxy-mgmt-key-env") ?? process.env.CLIPROXY_MGMT_KEY ?? "";
|
|
3477
|
+
const cliproxyAuthDir = getFlagValue(ctx.args, "--cliproxy-auth-dir") ?? process.env.CLIPROXY_AUTH_DIR ?? "";
|
|
3478
|
+
const env = buildEnv({
|
|
3479
|
+
host,
|
|
3480
|
+
port,
|
|
3481
|
+
adminApiKey,
|
|
3482
|
+
cliProxyApiUrl: cliProxyApiUrl ?? "http://localhost:8317",
|
|
3483
|
+
cliProxyApiKey,
|
|
3484
|
+
dataDir,
|
|
3485
|
+
cliproxyMgmtKey,
|
|
3486
|
+
cliproxyAuthDir
|
|
3487
|
+
});
|
|
3488
|
+
await completeInit(envPath, dataDir, runtimeDir, env, writeOptions(ctx));
|
|
3489
|
+
}
|
|
3490
|
+
function readSecretFromEnvFlag(args, flagName, envName) {
|
|
3491
|
+
return readNamedEnv(args, flagName) ?? process.env[envName];
|
|
3492
|
+
}
|
|
3493
|
+
function readNamedEnv(args, flagName) {
|
|
3494
|
+
const name = getFlagValue(args, flagName);
|
|
3495
|
+
if (!name)
|
|
3496
|
+
return;
|
|
3497
|
+
const value = process.env[name];
|
|
3498
|
+
if (value === undefined || value === "")
|
|
3499
|
+
throw new Error(`${flagName} references missing environment variable ${name}`);
|
|
3500
|
+
return value;
|
|
3501
|
+
}
|
|
3502
|
+
function buildEnv(inputEnv) {
|
|
3503
|
+
return {
|
|
3504
|
+
PROXY_HOST: inputEnv.host,
|
|
3505
|
+
PROXY_PORT: inputEnv.port,
|
|
3506
|
+
ADMIN_API_KEY: inputEnv.adminApiKey,
|
|
3507
|
+
CLI_PROXY_API_URL: inputEnv.cliProxyApiUrl,
|
|
3508
|
+
CLI_PROXY_API_KEY: inputEnv.cliProxyApiKey,
|
|
3509
|
+
CLAUDE_CODE_VERSION: "2.1.87",
|
|
3510
|
+
DB_PATH: join4(inputEnv.dataDir, "proxy.db"),
|
|
3511
|
+
PRICING_CACHE_PATH: join4(inputEnv.dataDir, "pricing-cache.json"),
|
|
3512
|
+
PRICING_CACHE_TTL_MS: "3600000",
|
|
3513
|
+
CLIENT_NAME_MAPPING: "",
|
|
3514
|
+
CLIPROXY_MGMT_KEY: inputEnv.cliproxyMgmtKey,
|
|
3515
|
+
CLIPROXY_CORRELATION_INTERVAL_MS: "15000",
|
|
3516
|
+
CLIPROXY_CORRELATION_LOOKBACK_MS: "300000",
|
|
3517
|
+
CLIPROXY_AUTH_DIR: inputEnv.cliproxyAuthDir,
|
|
3518
|
+
QUOTA_REFRESH_TIMEOUT_MS: "15000"
|
|
3519
|
+
};
|
|
3520
|
+
}
|
|
3521
|
+
async function completeInit(envPath, dataDir, runtimeDir, env, options) {
|
|
3522
|
+
await mkdir2(dirname2(envPath), { recursive: true });
|
|
3523
|
+
await mkdir2(dataDir, { recursive: true });
|
|
3524
|
+
await mkdir2(runtimeDir, { recursive: true });
|
|
3525
|
+
await writeEnvAtomic(envPath, env, options);
|
|
3526
|
+
const persistedEnv = parseEnvFile(envPath);
|
|
3527
|
+
await initDbAt(persistedEnv.DB_PATH ?? env.DB_PATH);
|
|
3528
|
+
writeOut(`
|
|
3529
|
+
Created:`);
|
|
3530
|
+
writeOut(` env: ${envPath}`);
|
|
3531
|
+
writeOut(` db: ${persistedEnv.DB_PATH ?? env.DB_PATH}`);
|
|
3532
|
+
writeOut(` data: ${dataDir}`);
|
|
3533
|
+
writeOut(` runtime: ${runtimeDir}`);
|
|
3534
|
+
}
|
|
3535
|
+
function writeOptions(ctx) {
|
|
3536
|
+
return { force: hasFlag(ctx.args, "--force"), merge: hasFlag(ctx.args, "--merge") };
|
|
3537
|
+
}
|
|
3538
|
+
async function initDbCommand(ctx) {
|
|
3539
|
+
const envPath = getFlagValue(ctx.args, "--env") ?? defaultEnvPath;
|
|
3540
|
+
const env = parseEnvFile(envPath);
|
|
3541
|
+
await initDbAt(env.DB_PATH ?? defaultDbPath);
|
|
3542
|
+
writeOut(`Initialized DB at ${env.DB_PATH ?? defaultDbPath}`);
|
|
3543
|
+
}
|
|
3544
|
+
async function backfillCostsCommand(ctx) {
|
|
3545
|
+
const envPath = getFlagValue(ctx.args, "--env") ?? defaultEnvPath;
|
|
3546
|
+
const env = parseEnvFile(envPath);
|
|
3547
|
+
applyEnv(env);
|
|
3548
|
+
const { UsageService: UsageService2 } = await Promise.resolve().then(() => (init_service(), exports_service));
|
|
3549
|
+
Config.validate(process.env);
|
|
3550
|
+
const dbPath = env.DB_PATH ?? defaultDbPath;
|
|
3551
|
+
const db = Storage.initDb(dbPath);
|
|
3552
|
+
const usageService = UsageService2.create(db);
|
|
3553
|
+
const result = await usageService.backfillCosts({
|
|
3554
|
+
all: hasFlag(ctx.args, "--all"),
|
|
3555
|
+
limit: parsePositiveLimit(ctx.args)
|
|
3556
|
+
});
|
|
3557
|
+
writeOut(`[backfill-costs] scanned=${result.scanned} updated=${result.updated} ok=${result.ok} pending=${result.pending} unsupported=${result.unsupported}`);
|
|
3558
|
+
db.close();
|
|
3559
|
+
}
|
|
3560
|
+
async function doctorCommand(ctx) {
|
|
3561
|
+
logger6.info("doctor started", { event: "cli.doctor.started" });
|
|
3562
|
+
const report = await collectDoctorReport(ctx);
|
|
3563
|
+
if (hasFlag(ctx.args, "--json"))
|
|
3564
|
+
writeOut(JSON.stringify(report, null, 2));
|
|
3565
|
+
else
|
|
3566
|
+
printDoctorReport(report);
|
|
3567
|
+
return report.status === "PASS" ? 0 : 1;
|
|
3568
|
+
}
|
|
3569
|
+
async function collectDoctorReport(ctx) {
|
|
3570
|
+
const { envPath, env } = loadConfigEnv(ctx.args);
|
|
3571
|
+
applyEnv(env);
|
|
3572
|
+
const checks = {};
|
|
3573
|
+
let config = null;
|
|
3574
|
+
try {
|
|
3575
|
+
config = Config.validate(env);
|
|
3576
|
+
checks.config = { status: "PASS", issues: [], details: { envPath } };
|
|
3577
|
+
} catch (err) {
|
|
3578
|
+
const issues = err instanceof ConfigError ? err.issues.map(formatConfigIssue) : [errorMessage2(err)];
|
|
3579
|
+
checks.config = { status: "FAIL", issues, details: { envPath } };
|
|
3580
|
+
}
|
|
3581
|
+
const dbPath = config?.dbPath ?? env.DB_PATH ?? defaultDbPath;
|
|
3582
|
+
checks.database = databaseCheck(dbPath);
|
|
3583
|
+
checks.plans = plansCheck();
|
|
3584
|
+
checks.providers = await providersCheck(Boolean(config));
|
|
3585
|
+
checks.pricingCache = await pricingCacheCheck(config?.pricingCachePath ?? env.PRICING_CACHE_PATH ?? defaultPricingCachePath);
|
|
3586
|
+
checks.upstream = config ? await upstreamCheck(config) : { status: "FAIL", issues: ["skipped because config is invalid"] };
|
|
3587
|
+
checks.supervisor = await supervisorCheck();
|
|
3588
|
+
const status = Object.values(checks).every((check) => check.status === "PASS") ? "PASS" : "FAIL";
|
|
3589
|
+
return { status, checks };
|
|
3590
|
+
}
|
|
3591
|
+
function loadConfigEnv(args) {
|
|
3592
|
+
const envPath = getFlagValue(args, "--env") ?? defaultEnvPath;
|
|
3593
|
+
return { envPath, env: { ...envFromProcess(), ...parseEnvFile(envPath) } };
|
|
3594
|
+
}
|
|
3595
|
+
function envFromProcess() {
|
|
3596
|
+
const env = {};
|
|
3597
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
3598
|
+
if (value !== undefined)
|
|
3599
|
+
env[key] = value;
|
|
3600
|
+
}
|
|
3601
|
+
return env;
|
|
3602
|
+
}
|
|
3603
|
+
function databaseCheck(dbPath) {
|
|
3604
|
+
let db = null;
|
|
3605
|
+
try {
|
|
3606
|
+
db = Storage.initDb(dbPath);
|
|
3607
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all();
|
|
3608
|
+
const migrations = db.prepare("SELECT name FROM schema_migrations ORDER BY name DESC LIMIT 10").all();
|
|
3609
|
+
return {
|
|
3610
|
+
status: "PASS",
|
|
3611
|
+
issues: [],
|
|
3612
|
+
details: {
|
|
3613
|
+
path: dbPath,
|
|
3614
|
+
tablesCount: tables.length,
|
|
3615
|
+
tables: tables.map((table) => table.name),
|
|
3616
|
+
appliedMigrations: migrations.map((migration) => migration.name)
|
|
3617
|
+
}
|
|
3618
|
+
};
|
|
3619
|
+
} catch (err) {
|
|
3620
|
+
return { status: "FAIL", issues: [errorMessage2(err)], details: { path: dbPath } };
|
|
3621
|
+
} finally {
|
|
3622
|
+
db?.close();
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
function plansCheck() {
|
|
3626
|
+
try {
|
|
3627
|
+
Plans.reload();
|
|
3628
|
+
const plans = Plans.list();
|
|
3629
|
+
const source = resolvePlansSource();
|
|
3630
|
+
return {
|
|
3631
|
+
status: "PASS",
|
|
3632
|
+
issues: [],
|
|
3633
|
+
details: {
|
|
3634
|
+
count: plans.length,
|
|
3635
|
+
source: source.kind,
|
|
3636
|
+
path: source.path ?? null
|
|
3637
|
+
}
|
|
3638
|
+
};
|
|
3639
|
+
} catch (err) {
|
|
3640
|
+
return { status: "FAIL", issues: [errorMessage2(err)] };
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
async function providersCheck(canLoadRegistry) {
|
|
3644
|
+
const inspection = inspectProviderConfig(process.env);
|
|
3645
|
+
if (!canLoadRegistry) {
|
|
3646
|
+
return {
|
|
3647
|
+
status: "FAIL",
|
|
3648
|
+
issues: ["skipped because config is invalid", ...inspection.invalid.map((entry) => entry.reason)],
|
|
3649
|
+
details: { source: inspection.source, invalid: inspection.invalid }
|
|
3650
|
+
};
|
|
3651
|
+
}
|
|
3652
|
+
try {
|
|
3653
|
+
const { ProviderRegistry: ProviderRegistry2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
|
|
3654
|
+
const providers = ProviderRegistry2.forceReload();
|
|
3655
|
+
return {
|
|
3656
|
+
status: inspection.invalid.length === 0 ? "PASS" : "FAIL",
|
|
3657
|
+
issues: inspection.invalid.map((entry) => entry.reason),
|
|
3658
|
+
details: {
|
|
3659
|
+
count: providers.length,
|
|
3660
|
+
source: inspection.source,
|
|
3661
|
+
invalid: inspection.invalid
|
|
3662
|
+
}
|
|
3663
|
+
};
|
|
3664
|
+
} catch (err) {
|
|
3665
|
+
return { status: "FAIL", issues: [errorMessage2(err)], details: { invalid: inspection.invalid } };
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
async function pricingCacheCheck(path) {
|
|
3669
|
+
try {
|
|
3670
|
+
const info = await stat(path);
|
|
3671
|
+
return {
|
|
3672
|
+
status: "PASS",
|
|
3673
|
+
issues: [],
|
|
3674
|
+
details: {
|
|
3675
|
+
path,
|
|
3676
|
+
exists: true,
|
|
3677
|
+
size: info.size,
|
|
3678
|
+
ageMs: Date.now() - info.mtimeMs
|
|
3679
|
+
}
|
|
3680
|
+
};
|
|
3681
|
+
} catch (err) {
|
|
3682
|
+
return { status: "FAIL", issues: [errorMessage2(err)], details: { path, exists: false } };
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
async function upstreamCheck(config) {
|
|
3686
|
+
const controller = new AbortController;
|
|
3687
|
+
const timeout = setTimeout(() => controller.abort(new Error("doctor upstream timeout")), 2000);
|
|
3688
|
+
try {
|
|
3689
|
+
const { UpstreamClient: UpstreamClient2 } = await Promise.resolve().then(() => (init_client(), exports_client));
|
|
3690
|
+
const response = await UpstreamClient2.fetch({
|
|
3691
|
+
method: "HEAD",
|
|
3692
|
+
url: `${config.cliProxyApiUrl}/health`,
|
|
3693
|
+
providerId: "doctor",
|
|
3694
|
+
idempotent: true,
|
|
3695
|
+
signal: controller.signal
|
|
3696
|
+
});
|
|
3697
|
+
if (response.status >= 200 && response.status < 500) {
|
|
3698
|
+
return { status: "PASS", issues: [], details: { statusCode: response.status } };
|
|
3699
|
+
}
|
|
3700
|
+
return { status: "FAIL", issues: [`upstream returned HTTP ${response.status}`], details: { statusCode: response.status } };
|
|
3701
|
+
} catch (err) {
|
|
3702
|
+
return { status: "FAIL", issues: [errorMessage2(err)] };
|
|
3703
|
+
} finally {
|
|
3704
|
+
clearTimeout(timeout);
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
async function supervisorCheck() {
|
|
3708
|
+
try {
|
|
3709
|
+
const { Supervisor: Supervisor2 } = await Promise.resolve().then(() => (init_supervisor(), exports_supervisor));
|
|
3710
|
+
return { status: "PASS", issues: [], details: { loops: Supervisor2.list() } };
|
|
3711
|
+
} catch (err) {
|
|
3712
|
+
return { status: "FAIL", issues: [errorMessage2(err)] };
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
function printDoctorReport(report) {
|
|
3716
|
+
writeOut(`doctor: ${report.status}`);
|
|
3717
|
+
for (const [name, check] of Object.entries(report.checks)) {
|
|
3718
|
+
writeOut(`${check.status === "PASS" ? "PASS" : "FAIL"} ${name}`);
|
|
3719
|
+
if (check.issues.length > 0) {
|
|
3720
|
+
for (const issue of check.issues)
|
|
3721
|
+
writeOut(` - ${issue}`);
|
|
3722
|
+
}
|
|
3723
|
+
if (check.details !== undefined)
|
|
3724
|
+
writeOut(` ${JSON.stringify(check.details)}`);
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
async function plansCommand(ctx, subcommand) {
|
|
3728
|
+
const planArgs = ctx.args.positional.slice(2);
|
|
3729
|
+
const [accountArg = "", codeArg = ""] = planArgs;
|
|
3730
|
+
switch (subcommand) {
|
|
3731
|
+
case "show": {
|
|
3732
|
+
Plans.reload();
|
|
3733
|
+
const plans = Plans.list();
|
|
3734
|
+
if (hasFlag(ctx.args, "--json"))
|
|
3735
|
+
writeOut(JSON.stringify(plans, null, 2));
|
|
3736
|
+
else
|
|
3737
|
+
printPlansTable(plans);
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3740
|
+
case "path": {
|
|
3741
|
+
const source = resolvePlansSource();
|
|
3742
|
+
writeOut(source.path ?? source.label);
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
case "init": {
|
|
3746
|
+
await initPlansFile(hasFlag(ctx.args, "--force"));
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
case "edit": {
|
|
3750
|
+
const path = await ensureEditablePlansFile(hasFlag(ctx.args, "--force"));
|
|
3751
|
+
await openEditor(path);
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
case "bind": {
|
|
3755
|
+
const { account, code } = Plans.validateBindingInput(accountArg, codeArg);
|
|
3756
|
+
const db = await openConfiguredDb(ctx);
|
|
3757
|
+
try {
|
|
3758
|
+
AccountSubscriptionRepo.bind(db, account, code);
|
|
3759
|
+
} finally {
|
|
3760
|
+
db.close();
|
|
3761
|
+
}
|
|
3762
|
+
writeOut(`Bound ${account} \u2192 ${code}`);
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
case "unbind": {
|
|
3766
|
+
const account = accountArg.trim();
|
|
3767
|
+
if (!account)
|
|
3768
|
+
throw new Error("Account must be a non-empty string");
|
|
3769
|
+
const db = await openConfiguredDb(ctx);
|
|
3770
|
+
try {
|
|
3771
|
+
AccountSubscriptionRepo.unbind(db, account);
|
|
3772
|
+
} finally {
|
|
3773
|
+
db.close();
|
|
3774
|
+
}
|
|
3775
|
+
writeOut(`Unbound ${account}`);
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3778
|
+
case "list": {
|
|
3779
|
+
const db = await openConfiguredDb(ctx);
|
|
3780
|
+
try {
|
|
3781
|
+
writeOut("Plans:");
|
|
3782
|
+
for (const plan of Plans.list()) {
|
|
3783
|
+
writeOut(` ${plan.code} - ${plan.display_name}`);
|
|
3784
|
+
}
|
|
3785
|
+
writeOut("Bindings:");
|
|
3786
|
+
const bindings = AccountSubscriptionRepo.list(db);
|
|
3787
|
+
if (bindings.length === 0) {
|
|
3788
|
+
writeOut(" (none)");
|
|
3789
|
+
} else {
|
|
3790
|
+
for (const binding of bindings) {
|
|
3791
|
+
writeOut(` ${binding.cliproxy_account} \u2192 ${binding.subscription_code} (${binding.bound_at})`);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
} finally {
|
|
3795
|
+
db.close();
|
|
3796
|
+
}
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
writeErr("Usage: agent-cli-proxy plans <show|edit|path|init|bind|unbind|list>");
|
|
3801
|
+
throw new Error("invalid plans subcommand");
|
|
3802
|
+
}
|
|
3803
|
+
async function providersCommand(ctx, subcommand) {
|
|
3804
|
+
switch (subcommand) {
|
|
3805
|
+
case "show": {
|
|
3806
|
+
const { ProviderRegistry: ProviderRegistry2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
|
|
3807
|
+
const providers = ProviderRegistry2.all().map(maskProvider);
|
|
3808
|
+
if (hasFlag(ctx.args, "--json"))
|
|
3809
|
+
writeOut(JSON.stringify(providers, null, 2));
|
|
3810
|
+
else
|
|
3811
|
+
printProvidersTable(providers);
|
|
3812
|
+
return;
|
|
3813
|
+
}
|
|
3814
|
+
case "reload": {
|
|
3815
|
+
const { ProviderRegistry: ProviderRegistry2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
|
|
3816
|
+
const providers = ProviderRegistry2.forceReload();
|
|
3817
|
+
writeOut(`Reloaded providers: ${providers.length}`);
|
|
3818
|
+
return;
|
|
3819
|
+
}
|
|
3820
|
+
case "path": {
|
|
3821
|
+
writeOut(providerPathLabel(process.env));
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
case "init": {
|
|
3825
|
+
const path = getFlagValue(ctx.args, "--path") ?? process.env.PROVIDERS_CONFIG_PATH ?? defaultProvidersPath;
|
|
3826
|
+
await writeJsonAtomic(path, starterProvidersDocument(), { force: hasFlag(ctx.args, "--force") });
|
|
3827
|
+
writeOut(`Created providers config: ${path}`);
|
|
3828
|
+
return;
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
writeErr("Usage: agent-cli-proxy providers <show|reload|path|init>");
|
|
3832
|
+
throw new Error("invalid providers subcommand");
|
|
3833
|
+
}
|
|
3834
|
+
async function serviceCommand(ctx, subcommand) {
|
|
3835
|
+
const envPath = getFlagValue(ctx.args, "--env") ?? defaultEnvPath;
|
|
3836
|
+
const runtimeDir = getFlagValue(ctx.args, "--runtime-dir") ?? defaultRuntimeDir;
|
|
3837
|
+
switch (subcommand) {
|
|
3838
|
+
case "install":
|
|
3839
|
+
await installRuntime(runtimeDir, envPath);
|
|
3840
|
+
await installService(ctx, runtimeDir, envPath);
|
|
3841
|
+
return 0;
|
|
3842
|
+
case "start":
|
|
3843
|
+
case "stop":
|
|
3844
|
+
case "restart":
|
|
3845
|
+
case "status":
|
|
3846
|
+
return await controlService(subcommand);
|
|
3847
|
+
case "logs":
|
|
3848
|
+
return await serviceLogsCommand(hasFlag(ctx.args, "--follow"));
|
|
3849
|
+
}
|
|
3850
|
+
writeErr("Usage: agent-cli-proxy service <install|start|stop|restart|status|logs [--follow]>");
|
|
3851
|
+
return 1;
|
|
3852
|
+
}
|
|
3853
|
+
async function installRuntime(runtimeDir, envPath) {
|
|
3854
|
+
await mkdir2(runtimeDir, { recursive: true });
|
|
3855
|
+
const distDir = resolveDistDir();
|
|
3856
|
+
if (!existsSync2(join4(distDir, "index.js"))) {
|
|
3857
|
+
throw new Error("dist/index.js not found. Run `bun run build` first.");
|
|
3858
|
+
}
|
|
3859
|
+
await copyFile(join4(distDir, "index.js"), join4(runtimeDir, "index.js"));
|
|
3860
|
+
await copyGlobByPrefix(distDir, runtimeDir, "index-");
|
|
3861
|
+
await rm(join4(runtimeDir, "migrations"), { recursive: true, force: true });
|
|
3862
|
+
await cp(join4(distDir, "migrations"), join4(runtimeDir, "migrations"), { recursive: true });
|
|
3863
|
+
await writeFile(join4(runtimeDir, ".env.path"), `${envPath}
|
|
3864
|
+
`);
|
|
3865
|
+
}
|
|
3866
|
+
async function copyGlobByPrefix(fromDir, toDir, prefix) {
|
|
3867
|
+
const glob = new Bun.Glob(`${prefix}*`);
|
|
3868
|
+
for await (const file of glob.scan({ cwd: fromDir, onlyFiles: true })) {
|
|
3869
|
+
await copyFile(join4(fromDir, file), join4(toDir, file));
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
async function installService(ctx, runtimeDir, envPath) {
|
|
3873
|
+
if (platform() === "linux") {
|
|
3874
|
+
const unitDir = join4(XDG_CONFIG_HOME, "systemd", "user");
|
|
3875
|
+
const unitPath = getFlagValue(ctx.args, "--service-path") ?? join4(unitDir, `${APP_NAME}.service`);
|
|
3876
|
+
await mkdir2(dirname2(unitPath), { recursive: true });
|
|
3877
|
+
await writeFile(unitPath, renderSystemdUserService(runtimeDir, envPath));
|
|
3878
|
+
writeOut(`Installed systemd user service: ${unitPath}`);
|
|
3879
|
+
writeOut(`Run: systemctl --user daemon-reload && systemctl --user enable --now ${APP_NAME}`);
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
3882
|
+
if (platform() === "darwin") {
|
|
3883
|
+
const launchDir = join4(HOME, "Library", "LaunchAgents");
|
|
3884
|
+
const plistPath = getFlagValue(ctx.args, "--service-path") ?? join4(launchDir, `ai.agent-cli-proxy.plist`);
|
|
3885
|
+
await mkdir2(dirname2(plistPath), { recursive: true });
|
|
3886
|
+
await writeFile(plistPath, renderLaunchAgent(runtimeDir, envPath));
|
|
3887
|
+
writeOut(`Installed launchd agent: ${plistPath}`);
|
|
3888
|
+
writeOut(`Run: launchctl load ${plistPath}`);
|
|
3889
|
+
return;
|
|
3890
|
+
}
|
|
3891
|
+
throw new Error(`Unsupported service platform: ${platform()}`);
|
|
3892
|
+
}
|
|
3893
|
+
async function controlService(action) {
|
|
3894
|
+
if (platform() !== "linux") {
|
|
3895
|
+
writeOut("Service control is only automated for systemd user services. Use launchctl on macOS.");
|
|
3896
|
+
return 0;
|
|
3897
|
+
}
|
|
3898
|
+
const args = action === "status" ? ["--user", "--no-pager", "status", APP_NAME] : ["--user", action, APP_NAME];
|
|
3899
|
+
const proc = Bun.spawn(["systemctl", ...args], { stdout: "inherit", stderr: "inherit" });
|
|
3900
|
+
return await proc.exited;
|
|
3901
|
+
}
|
|
3902
|
+
async function serviceLogsCommand(follow) {
|
|
3903
|
+
const os = platform();
|
|
3904
|
+
if (os === "linux") {
|
|
3905
|
+
const args = ["--user", "-u", `${APP_NAME}.service`];
|
|
3906
|
+
if (follow)
|
|
3907
|
+
args.push("-f");
|
|
3908
|
+
const proc = Bun.spawn(["journalctl", ...args], { stdout: "inherit", stderr: "inherit" });
|
|
3909
|
+
return await proc.exited;
|
|
3910
|
+
}
|
|
3911
|
+
if (os === "darwin") {
|
|
3912
|
+
const command = follow ? "stream" : "show";
|
|
3913
|
+
const args = [command, "--predicate", `process == "${APP_NAME}"`, "--style", "compact"];
|
|
3914
|
+
const proc = Bun.spawn(["log", ...args], { stdout: "inherit", stderr: "inherit" });
|
|
3915
|
+
return await proc.exited;
|
|
3916
|
+
}
|
|
3917
|
+
writeErr("service logs not supported on this platform; check stderr/stdout");
|
|
3918
|
+
return 1;
|
|
3919
|
+
}
|
|
3920
|
+
function renderSystemdUserService(runtimeDir, envPath) {
|
|
3921
|
+
assertSafePath(runtimeDir, "runtimeDir");
|
|
3922
|
+
assertSafePath(envPath, "envPath");
|
|
3923
|
+
return `[Unit]
|
|
3924
|
+
Description=agent-cli-proxy - AI API Proxy with Usage Monitoring
|
|
3925
|
+
After=network-online.target
|
|
3926
|
+
|
|
3927
|
+
[Service]
|
|
3928
|
+
Type=simple
|
|
3929
|
+
WorkingDirectory=${systemdEscape(runtimeDir)}
|
|
3930
|
+
Environment=NODE_ENV=production
|
|
3931
|
+
Environment=PATH=${systemdEscape(`${HOME}/.bun/bin:/usr/local/bin:/usr/bin:/bin`)}
|
|
3932
|
+
EnvironmentFile=${systemdEscape(envPath)}
|
|
3933
|
+
ExecStart=${systemdEscape(`${HOME}/.bun/bin/bun`)} run index.js
|
|
3934
|
+
Restart=always
|
|
3935
|
+
RestartSec=5
|
|
3936
|
+
|
|
3937
|
+
[Install]
|
|
3938
|
+
WantedBy=default.target
|
|
3939
|
+
`;
|
|
3940
|
+
}
|
|
3941
|
+
function renderLaunchAgent(runtimeDir, envPath) {
|
|
3942
|
+
assertSafePath(runtimeDir, "runtimeDir");
|
|
3943
|
+
assertSafePath(envPath, "envPath");
|
|
3944
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3945
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3946
|
+
<plist version="1.0">
|
|
3947
|
+
<dict>
|
|
3948
|
+
<key>Label</key><string>ai.agent-cli-proxy</string>
|
|
3949
|
+
<key>WorkingDirectory</key><string>${xmlEscape(runtimeDir)}</string>
|
|
3950
|
+
<key>ProgramArguments</key>
|
|
3951
|
+
<array><string>${xmlEscape(`${HOME}/.bun/bin/bun`)}</string><string>--env-file</string><string>${xmlEscape(envPath)}</string><string>run</string><string>index.js</string></array>
|
|
3952
|
+
<key>EnvironmentVariables</key>
|
|
3953
|
+
<dict><key>NODE_ENV</key><string>production</string></dict>
|
|
3954
|
+
<key>RunAtLoad</key><true/>
|
|
3955
|
+
<key>KeepAlive</key><true/>
|
|
3956
|
+
</dict>
|
|
3957
|
+
</plist>
|
|
3958
|
+
`;
|
|
3959
|
+
}
|
|
3960
|
+
async function initDbAt(dbPath) {
|
|
3961
|
+
await mkdir2(dirname2(dbPath), { recursive: true });
|
|
3962
|
+
const db = Storage.initDb(dbPath);
|
|
3963
|
+
db.close();
|
|
3964
|
+
}
|
|
3965
|
+
async function openConfiguredDb(ctx) {
|
|
3966
|
+
const envPath = getFlagValue(ctx.args, "--env") ?? defaultEnvPath;
|
|
3967
|
+
const env = parseEnvFile(envPath);
|
|
3968
|
+
applyEnv(env);
|
|
3969
|
+
Config.validate(process.env);
|
|
3970
|
+
return Storage.initDb(env.DB_PATH ?? process.env.DB_PATH ?? defaultDbPath);
|
|
3971
|
+
}
|
|
3972
|
+
async function writeEnvAtomic(path, envMap, opts) {
|
|
3973
|
+
if (existsSync2(path) && !opts.force && !opts.merge) {
|
|
3974
|
+
throw new Error(`${path} already exists; use --force to overwrite, or --merge to preserve existing values`);
|
|
3975
|
+
}
|
|
3976
|
+
const existing = opts.merge ? parseEnvFile(path) : {};
|
|
3977
|
+
const finalEnv = opts.merge ? { ...envMap, ...existing } : envMap;
|
|
3978
|
+
await writeTextAtomic(path, renderEnv(finalEnv), 384);
|
|
3979
|
+
}
|
|
3980
|
+
async function writeJsonAtomic(path, value, opts) {
|
|
3981
|
+
if (existsSync2(path) && !opts.force)
|
|
3982
|
+
throw new Error(`${path} already exists; use --force to overwrite`);
|
|
3983
|
+
await writeTextAtomic(path, `${JSON.stringify(value, null, 2)}
|
|
3984
|
+
`, 384);
|
|
3985
|
+
}
|
|
3986
|
+
async function writeTextAtomic(path, text, mode) {
|
|
3987
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
3988
|
+
const temp = `${path}.tmp.${process.pid}.${crypto.randomUUID()}`;
|
|
3989
|
+
const handle = await open(temp, "w", mode);
|
|
3990
|
+
try {
|
|
3991
|
+
await handle.writeFile(text);
|
|
3992
|
+
} finally {
|
|
3993
|
+
await handle.close();
|
|
3994
|
+
}
|
|
3995
|
+
await chmod(temp, mode);
|
|
3996
|
+
await rename(temp, path);
|
|
3997
|
+
await chmod(path, mode);
|
|
3998
|
+
}
|
|
3999
|
+
function renderEnv(env) {
|
|
4000
|
+
const lines = Object.entries(env).map(([key, value]) => `${key}=${quoteEnv(value)}`);
|
|
4001
|
+
return `${lines.join(`
|
|
4002
|
+
`)}
|
|
4003
|
+
`;
|
|
4004
|
+
}
|
|
4005
|
+
function quoteEnv(value) {
|
|
4006
|
+
if (/^[A-Za-z0-9_./:@-]*$/.test(value))
|
|
4007
|
+
return value;
|
|
4008
|
+
return JSON.stringify(value);
|
|
4009
|
+
}
|
|
4010
|
+
function parseEnvFile(path) {
|
|
4011
|
+
if (!existsSync2(path))
|
|
4012
|
+
return {};
|
|
4013
|
+
const text = readFileSync5(path, "utf-8");
|
|
4014
|
+
const env = {};
|
|
4015
|
+
for (const line of text.split(`
|
|
4016
|
+
`)) {
|
|
4017
|
+
const trimmed = line.trim();
|
|
4018
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
4019
|
+
continue;
|
|
4020
|
+
const idx = trimmed.indexOf("=");
|
|
4021
|
+
if (idx === -1)
|
|
4022
|
+
continue;
|
|
4023
|
+
env[trimmed.slice(0, idx)] = parseEnvValue(trimmed.slice(idx + 1));
|
|
4024
|
+
}
|
|
4025
|
+
return env;
|
|
4026
|
+
}
|
|
4027
|
+
function parseEnvValue(raw) {
|
|
4028
|
+
if (!raw.startsWith('"'))
|
|
4029
|
+
return raw;
|
|
4030
|
+
try {
|
|
4031
|
+
const parsed = JSON.parse(raw);
|
|
4032
|
+
return typeof parsed === "string" ? parsed : raw.replace(/^"|"$/g, "");
|
|
4033
|
+
} catch {
|
|
4034
|
+
return raw.replace(/^"|"$/g, "");
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
function parsePositiveLimit(args) {
|
|
4038
|
+
const value = getFlagValue(args, "--limit");
|
|
4039
|
+
if (!value)
|
|
4040
|
+
return;
|
|
4041
|
+
const parsed = Number(value);
|
|
4042
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
4043
|
+
}
|
|
4044
|
+
async function ask(rl, question, fallback) {
|
|
4045
|
+
const answer = await rl.question(`${question} (${fallback}): `);
|
|
4046
|
+
return answer.trim() || fallback;
|
|
4047
|
+
}
|
|
4048
|
+
async function askSecret(rl, question, fallback) {
|
|
4049
|
+
if (!process.stdin.isTTY) {
|
|
4050
|
+
throw new Error(`${question} requires a TTY; use init --non-interactive with environment-backed secret flags`);
|
|
4051
|
+
}
|
|
4052
|
+
const mutableOutput = output;
|
|
4053
|
+
const originalWrite = mutableOutput.write.bind(mutableOutput);
|
|
4054
|
+
let muted = false;
|
|
4055
|
+
mutableOutput.write = (chunk, encoding, cb) => {
|
|
4056
|
+
if (muted)
|
|
4057
|
+
return true;
|
|
4058
|
+
return originalWrite(chunk, encoding, cb);
|
|
4059
|
+
};
|
|
4060
|
+
try {
|
|
4061
|
+
muted = true;
|
|
4062
|
+
const answer = await rl.question(`${question}${fallback ? " (configured)" : ""}: `);
|
|
4063
|
+
muted = false;
|
|
4064
|
+
originalWrite(`
|
|
4065
|
+
`);
|
|
4066
|
+
return answer.trim() || fallback;
|
|
4067
|
+
} finally {
|
|
4068
|
+
mutableOutput.write = originalWrite;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
function resolveDistDir() {
|
|
4072
|
+
if (existsSync2(join4(packagedDistDir, "index.js")))
|
|
4073
|
+
return packagedDistDir;
|
|
4074
|
+
const sourceTreeDist = join4(packageRoot, "dist");
|
|
4075
|
+
if (existsSync2(join4(sourceTreeDist, "index.js")))
|
|
4076
|
+
return sourceTreeDist;
|
|
4077
|
+
const cwdDist = resolve("dist");
|
|
4078
|
+
if (existsSync2(join4(cwdDist, "index.js")))
|
|
4079
|
+
return cwdDist;
|
|
4080
|
+
return packagedDistDir;
|
|
4081
|
+
}
|
|
4082
|
+
function assertSafePath(path, label) {
|
|
4083
|
+
if (/[\u0000-\u001f\u007f\n\r]/.test(path)) {
|
|
4084
|
+
throw new Error(`${label} contains unsupported control characters`);
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
function systemdEscape(value) {
|
|
4088
|
+
if (/^[A-Za-z0-9_/@%+=:,.-]+$/.test(value))
|
|
4089
|
+
return value;
|
|
4090
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
4091
|
+
}
|
|
4092
|
+
function xmlEscape(value) {
|
|
4093
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
4094
|
+
}
|
|
4095
|
+
async function confirm(rl, question, fallback) {
|
|
4096
|
+
const suffix = fallback ? "Y/n" : "y/N";
|
|
4097
|
+
const answer = (await rl.question(`${question} [${suffix}]: `)).trim().toLowerCase();
|
|
4098
|
+
if (!answer)
|
|
4099
|
+
return fallback;
|
|
4100
|
+
return answer === "y" || answer === "yes";
|
|
4101
|
+
}
|
|
4102
|
+
function printPaths() {
|
|
4103
|
+
writeOut(JSON.stringify({
|
|
4104
|
+
configDir: defaultConfigDir,
|
|
4105
|
+
dataDir: defaultDataDir,
|
|
4106
|
+
runtimeDir: defaultRuntimeDir,
|
|
4107
|
+
envPath: defaultEnvPath,
|
|
4108
|
+
dbPath: defaultDbPath,
|
|
4109
|
+
pricingCachePath: defaultPricingCachePath,
|
|
4110
|
+
plansPath: defaultPlansPath,
|
|
4111
|
+
providersPath: defaultProvidersPath
|
|
4112
|
+
}, null, 2));
|
|
4113
|
+
}
|
|
4114
|
+
function printHelp() {
|
|
4115
|
+
writeOut(`agent-cli-proxy
|
|
4116
|
+
|
|
4117
|
+
Usage:
|
|
4118
|
+
agent-cli-proxy init [--force|--merge]
|
|
4119
|
+
agent-cli-proxy init --non-interactive [--env PATH] [--data-dir PATH] [--runtime-dir PATH] [--admin-token VALUE|--admin-token-env NAME] [--cliproxy-mgmt-key-env NAME] [--force|--merge]
|
|
4120
|
+
agent-cli-proxy doctor [--env PATH] [--json]
|
|
4121
|
+
agent-cli-proxy db init [--env PATH]
|
|
4122
|
+
agent-cli-proxy service install [--env PATH] [--runtime-dir PATH] [--service-path PATH]
|
|
4123
|
+
agent-cli-proxy service start|stop|restart|status
|
|
4124
|
+
agent-cli-proxy service logs [--follow]
|
|
4125
|
+
agent-cli-proxy backfill-costs [--all] [--limit N]
|
|
4126
|
+
agent-cli-proxy plans show [--json]
|
|
4127
|
+
agent-cli-proxy plans edit|path|init [--force]
|
|
4128
|
+
agent-cli-proxy plans bind <account> <code> [--env PATH]
|
|
4129
|
+
agent-cli-proxy plans unbind <account> [--env PATH]
|
|
4130
|
+
agent-cli-proxy plans list [--env PATH]
|
|
4131
|
+
agent-cli-proxy providers show [--json]
|
|
4132
|
+
agent-cli-proxy providers reload|path|init [--force]
|
|
4133
|
+
agent-cli-proxy paths
|
|
4134
|
+
`);
|
|
4135
|
+
}
|
|
4136
|
+
function applyEnv(env) {
|
|
4137
|
+
for (const [key, value] of Object.entries(env))
|
|
4138
|
+
process.env[key] = value;
|
|
4139
|
+
}
|
|
4140
|
+
function formatConfigIssue(issue) {
|
|
4141
|
+
return `${issue.path} ${issue.message}`;
|
|
4142
|
+
}
|
|
4143
|
+
function errorMessage2(err) {
|
|
4144
|
+
return err instanceof Error ? err.message : String(err);
|
|
4145
|
+
}
|
|
4146
|
+
function resolvePlansSource() {
|
|
4147
|
+
if (process.env.PLANS_JSON?.trim())
|
|
4148
|
+
return { kind: "PLANS_JSON", label: "PLANS_JSON inline" };
|
|
4149
|
+
const envPath = process.env.PLANS_PATH?.trim();
|
|
4150
|
+
if (envPath)
|
|
4151
|
+
return { kind: "PLANS_PATH", path: envPath, label: envPath };
|
|
4152
|
+
if (existsSync2(defaultPlansPath))
|
|
4153
|
+
return { kind: "XDG_CONFIG_HOME", path: defaultPlansPath, label: defaultPlansPath };
|
|
4154
|
+
return { kind: "packaged", label: "packaged default" };
|
|
4155
|
+
}
|
|
4156
|
+
async function initPlansFile(force) {
|
|
4157
|
+
await writeJsonAtomic(defaultPlansPath, plans_default_default, { force });
|
|
4158
|
+
writeOut(`Created plans config: ${defaultPlansPath}`);
|
|
4159
|
+
}
|
|
4160
|
+
async function ensureEditablePlansFile(force) {
|
|
4161
|
+
const source = resolvePlansSource();
|
|
4162
|
+
if (source.path && source.kind !== "packaged")
|
|
4163
|
+
return source.path;
|
|
4164
|
+
if (existsSync2(defaultPlansPath) && !force)
|
|
4165
|
+
return defaultPlansPath;
|
|
4166
|
+
await writeJsonAtomic(defaultPlansPath, plans_default_default, { force: force || !existsSync2(defaultPlansPath) });
|
|
4167
|
+
return defaultPlansPath;
|
|
4168
|
+
}
|
|
4169
|
+
async function openEditor(path) {
|
|
4170
|
+
const editor = process.env.EDITOR?.trim() || "vi";
|
|
4171
|
+
const proc = Bun.spawn([editor, path], { stdout: "inherit", stderr: "inherit", stdin: "inherit" });
|
|
4172
|
+
const exitCode = await proc.exited;
|
|
4173
|
+
if (exitCode !== 0)
|
|
4174
|
+
throw new Error(`${editor} exited with ${exitCode}`);
|
|
4175
|
+
}
|
|
4176
|
+
function printPlansTable(plans) {
|
|
4177
|
+
writeOut("code\tprovider\tprice\tdisplay_name");
|
|
4178
|
+
for (const plan of plans) {
|
|
4179
|
+
writeOut(`${plan.code} ${plan.provider} ${plan.monthly_price_usd} ${plan.currency}/${plan.billing_period_days}d ${plan.display_name}`);
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
function printProvidersTable(providers) {
|
|
4183
|
+
writeOut("id\ttype\tpaths\tmodels\tauth");
|
|
4184
|
+
for (const provider of providers) {
|
|
4185
|
+
writeOut(`${provider.id} ${provider.type} ${provider.paths.join(",")} ${provider.models?.join(",") ?? ""} ${formatAuth(provider.auth)}`);
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
function maskProvider(provider) {
|
|
4189
|
+
if (typeof provider.auth !== "object" || provider.auth === null)
|
|
4190
|
+
return { ...provider };
|
|
4191
|
+
const auth = { ...provider.auth };
|
|
4192
|
+
if (auth.value !== undefined)
|
|
4193
|
+
auth.value = "[redacted]";
|
|
4194
|
+
return { ...provider, auth };
|
|
4195
|
+
}
|
|
4196
|
+
function formatAuth(auth) {
|
|
4197
|
+
if (auth === undefined)
|
|
4198
|
+
return "";
|
|
4199
|
+
if (typeof auth === "string")
|
|
4200
|
+
return auth;
|
|
4201
|
+
if (auth.env)
|
|
4202
|
+
return `${auth.type} env:${auth.env}`;
|
|
4203
|
+
if (auth.value)
|
|
4204
|
+
return `${auth.type} value:[redacted]`;
|
|
4205
|
+
return auth.type;
|
|
4206
|
+
}
|
|
4207
|
+
function providerPathLabel(env) {
|
|
4208
|
+
if (env.PROVIDERS_JSON?.trim())
|
|
4209
|
+
return "PROVIDERS_JSON inline";
|
|
4210
|
+
if (env.PROVIDERS_CONFIG_PATH?.trim())
|
|
4211
|
+
return env.PROVIDERS_CONFIG_PATH.trim();
|
|
4212
|
+
return "(no custom providers configured)";
|
|
4213
|
+
}
|
|
4214
|
+
function starterProvidersDocument() {
|
|
4215
|
+
return {
|
|
4216
|
+
providers: [
|
|
4217
|
+
{
|
|
4218
|
+
id: "local",
|
|
4219
|
+
type: "openai-compatible",
|
|
4220
|
+
paths: ["/v1/chat/completions"],
|
|
4221
|
+
upstreamBaseUrl: "http://localhost:11434",
|
|
4222
|
+
upstreamPath: "/v1/chat/completions",
|
|
4223
|
+
models: ["llama", "qwen"],
|
|
4224
|
+
auth: "none"
|
|
4225
|
+
},
|
|
4226
|
+
{
|
|
4227
|
+
id: "glm",
|
|
4228
|
+
type: "openai-compatible",
|
|
4229
|
+
paths: ["/v1/chat/completions"],
|
|
4230
|
+
upstreamBaseUrl: "https://open.bigmodel.cn/api/paas/v4",
|
|
4231
|
+
models: ["glm"],
|
|
4232
|
+
auth: { type: "bearer", env: "GLM_API_KEY" }
|
|
4233
|
+
}
|
|
4234
|
+
]
|
|
4235
|
+
};
|
|
4236
|
+
}
|
|
4237
|
+
function inspectProviderConfig(env) {
|
|
4238
|
+
const inline = env.PROVIDERS_JSON;
|
|
4239
|
+
if (inline !== undefined && inline.trim() !== "")
|
|
4240
|
+
return inspectProviderRaw("PROVIDERS_JSON", inline);
|
|
4241
|
+
const configPath = env.PROVIDERS_CONFIG_PATH?.trim();
|
|
4242
|
+
if (!configPath)
|
|
4243
|
+
return { source: "built-in", invalid: [] };
|
|
4244
|
+
try {
|
|
4245
|
+
return inspectProviderRaw(configPath, readFileSync5(configPath, "utf-8"));
|
|
4246
|
+
} catch (err) {
|
|
4247
|
+
return { source: configPath, invalid: [{ path: "PROVIDERS_CONFIG_PATH", reason: errorMessage2(err), issues: [{ path: "PROVIDERS_CONFIG_PATH", message: errorMessage2(err) }] }] };
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
function inspectProviderRaw(source, raw) {
|
|
4251
|
+
let parsed;
|
|
4252
|
+
try {
|
|
4253
|
+
parsed = JSON.parse(raw);
|
|
4254
|
+
} catch (err) {
|
|
4255
|
+
const issue = { path: source, message: errorMessage2(err) };
|
|
4256
|
+
return { source, invalid: [{ path: source, reason: `${source} must be valid JSON: ${issue.message}`, issues: [issue] }] };
|
|
4257
|
+
}
|
|
4258
|
+
const result = validateProviderDocument(parsed);
|
|
4259
|
+
const grouped = new Map;
|
|
4260
|
+
for (const issue of result.issues) {
|
|
4261
|
+
const match = /^providers\[(\d+)]/.exec(issue.path);
|
|
4262
|
+
const path = match ? `providers[${match[1]}]` : issue.path;
|
|
4263
|
+
const issues = grouped.get(path) ?? [];
|
|
4264
|
+
issues.push(issue);
|
|
4265
|
+
grouped.set(path, issues);
|
|
4266
|
+
}
|
|
4267
|
+
return {
|
|
4268
|
+
source,
|
|
4269
|
+
invalid: Array.from(grouped, ([path, issues]) => ({
|
|
4270
|
+
path,
|
|
4271
|
+
reason: issues.map((issue) => `${issue.path} ${issue.message}`).join("; "),
|
|
4272
|
+
issues
|
|
4273
|
+
}))
|
|
4274
|
+
};
|
|
4275
|
+
}
|
|
4276
|
+
if (import.meta.main) {
|
|
4277
|
+
main().catch((err) => {
|
|
4278
|
+
writeErr(err instanceof Error ? err.message : String(err));
|
|
4279
|
+
process.exit(1);
|
|
4280
|
+
});
|
|
4281
|
+
}
|
|
4282
|
+
export {
|
|
4283
|
+
writeEnvAtomic,
|
|
4284
|
+
runCli,
|
|
4285
|
+
parseArgs,
|
|
4286
|
+
getFlagValue
|
|
4287
|
+
};
|