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