@jeffusion/bungee-core 5.10.0 → 5.12.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/dist/main.js +31436 -17327
- package/dist/plugins/ai-transformer/index.js.map +2 -2
- package/dist/plugins/anthropic-request-sanitizer/index.js.map +2 -2
- package/dist/plugins/anthropic-tool-name-transformer/index.js.map +2 -2
- package/dist/plugins/chatgpt-oauth/control.js +850 -47
- package/dist/plugins/chatgpt-oauth/control.js.map +16 -7
- package/dist/plugins/chatgpt-oauth/index.js +691 -16
- package/dist/plugins/chatgpt-oauth/index.js.map +15 -7
- package/dist/plugins/chatgpt-oauth/manifest.json +117 -7
- package/dist/plugins/chatgpt-oauth/ui/AccountsPage.svelte +25 -15
- package/dist/plugins/chatgpt-oauth/ui/ChatgptQuotaWidget.svelte +7 -4
- package/dist/plugins/chatgpt-oauth/ui/account-model.js +16 -7
- package/dist/plugins/deepseek-reasoning-fix/index.js.map +2 -2
- package/dist/plugins/key-access/index.js.map +2 -2
- package/dist/plugins/key-rate-limit/index.js.map +2 -2
- package/dist/plugins/local-accounts/control.js.map +2 -2
- package/dist/plugins/local-accounts/index.js.map +2 -2
- package/dist/plugins/model-mapping/control.js +72 -17741
- package/dist/plugins/model-mapping/control.js.map +6 -58
- package/dist/plugins/model-mapping/index.js +32 -17433
- package/dist/plugins/model-mapping/index.js.map +6 -58
- package/dist/plugins/model-mapping/manifest.json +30 -14
- package/dist/plugins/models-dev/control.js +1042 -0
- package/dist/plugins/models-dev/control.js.map +16 -0
- package/dist/plugins/models-dev/index.js +15418 -0
- package/dist/plugins/models-dev/index.js.map +134 -0
- package/dist/plugins/models-dev/manifest.json +199 -0
- package/dist/plugins/models-dev/ui/ModelsDevSettings.svelte +120 -0
- package/dist/plugins/openai-messages-to-chat/index.js.map +2 -2
- package/dist/plugins/signature-repair/index.js.map +2 -2
- package/dist/plugins/token-budget/control.js +524 -9
- package/dist/plugins/token-budget/control.js.map +12 -4
- package/dist/plugins/token-budget/index.js +519 -12
- package/dist/plugins/token-budget/index.js.map +13 -6
- package/dist/plugins/token-budget/manifest.json +16 -2
- package/dist/plugins/token-metering/index.js +93 -2
- package/dist/plugins/token-metering/index.js.map +12 -6
- package/dist/plugins/token-stats/control.js +174 -312
- package/dist/plugins/token-stats/control.js.map +7 -7
- package/dist/plugins/token-stats/index.js +283 -486
- package/dist/plugins/token-stats/index.js.map +16 -12
- package/dist/plugins/token-stats/manifest.json +99 -12
- package/dist/plugins/token-stats/ui/ClientModelPicker.svelte +76 -0
- package/dist/plugins/token-stats/ui/PriceModelPicker.svelte +87 -0
- package/dist/plugins/token-stats/ui/TokenStatsSettings.svelte +136 -80
- package/dist/plugins/token-stats/ui/client-model-search.test.ts +58 -0
- package/dist/plugins/token-stats/ui/client-model-search.ts +16 -0
- package/dist/plugins/token-stats/ui/model-search.ts +45 -0
- package/dist/plugins/token-stats/ui/pricing-catalog.test.ts +114 -0
- package/dist/plugins/token-stats/ui/pricing-catalog.ts +17 -0
- package/package.json +2 -2
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// plugins/models-dev/contract.ts
|
|
3
|
+
var MODELS_DEV_SOURCE_URL = "https://models.dev/api.json";
|
|
4
|
+
var MODELS_DEV_CATALOG_SERVICE_ID = "models-dev.catalog.v1";
|
|
5
|
+
var MODELS_DEV_CATALOG_CONTRACT_VERSION = 1;
|
|
6
|
+
var MODELS_DEV_CATALOG_SNAPSHOT_ID = "models-dev.catalog.snapshot.v1";
|
|
7
|
+
var MODELS_DEV_CATALOG_SNAPSHOT_CONTRACT_VERSION = 1;
|
|
8
|
+
var MODELS_DEV_CATALOG_SNAPSHOT_SCHEMA = Object.freeze({
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
version: { type: "number", integer: true, minimum: 1 },
|
|
12
|
+
fetchedAt: { type: "number", minimum: 0 },
|
|
13
|
+
catalog: { type: "json", maxBytes: 32 * 1024 * 1024 }
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
var MODELS_DEV_CATALOG_SNAPSHOT_CONTRACT = Object.freeze({
|
|
17
|
+
id: MODELS_DEV_CATALOG_SNAPSHOT_ID,
|
|
18
|
+
version: MODELS_DEV_CATALOG_SNAPSHOT_CONTRACT_VERSION,
|
|
19
|
+
content: MODELS_DEV_CATALOG_SNAPSHOT_SCHEMA
|
|
20
|
+
});
|
|
21
|
+
|
|
2
22
|
// packages/core/src/token-stats-window.ts
|
|
3
23
|
var HOUR = 3600000;
|
|
4
24
|
var TOKEN_STATS_RETENTION_MS = 31 * 24 * HOUR;
|
|
@@ -259,6 +279,41 @@ function withReportingWriteTimeout(db, operation) {
|
|
|
259
279
|
}
|
|
260
280
|
}
|
|
261
281
|
|
|
282
|
+
// plugins/token-stats/server/model-mappings.ts
|
|
283
|
+
var PRICE_MODEL_MAPPINGS_KEY = "pricing:model-mappings:v1";
|
|
284
|
+
var MAX_PRICE_MODEL_MAPPINGS = 100;
|
|
285
|
+
async function readPriceModelMappings(storage) {
|
|
286
|
+
if (!storage.observation)
|
|
287
|
+
return parsePriceModelMappings(await storage.get(PRICE_MODEL_MAPPINGS_KEY) ?? []);
|
|
288
|
+
return storage.observation.withDatabase((db) => {
|
|
289
|
+
const row = db.query("SELECT value, ttl FROM plugin_storage WHERE plugin_name = ? AND key = ?").get("token-stats", PRICE_MODEL_MAPPINGS_KEY);
|
|
290
|
+
if (!row || row.ttl !== null && row.ttl < Math.floor(Date.now() / 1000))
|
|
291
|
+
return [];
|
|
292
|
+
return parsePriceModelMappings(JSON.parse(row.value));
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function isUnchangedPriceModelMapping(mapping, existing) {
|
|
296
|
+
return existing.some((item) => item.source === mapping.source && item.provider === mapping.provider && item.model === mapping.model);
|
|
297
|
+
}
|
|
298
|
+
function parsePriceModelMappings(value) {
|
|
299
|
+
if (!Array.isArray(value) || value.length > MAX_PRICE_MODEL_MAPPINGS)
|
|
300
|
+
throw new Error("invalid_input");
|
|
301
|
+
const sources = new Set;
|
|
302
|
+
return value.map((item) => {
|
|
303
|
+
if (!item || typeof item !== "object" || Array.isArray(item) || Object.keys(item).some((key) => !["source", "provider", "model"].includes(key)))
|
|
304
|
+
throw new Error("invalid_input");
|
|
305
|
+
const { source, provider, model } = item;
|
|
306
|
+
for (const [text, limit] of [[source, 256], [provider, 128], [model, 256]]) {
|
|
307
|
+
if (typeof text !== "string" || !text.trim() || text !== text.trim() || text.length > limit || /[\u0000-\u001f\u007f]/.test(text))
|
|
308
|
+
throw new Error("invalid_input");
|
|
309
|
+
}
|
|
310
|
+
if (sources.has(source))
|
|
311
|
+
throw new Error("invalid_input");
|
|
312
|
+
sources.add(source);
|
|
313
|
+
return { source, provider, model };
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
262
317
|
// plugins/token-stats/server/repository.ts
|
|
263
318
|
var TOKEN_ACCOUNTING_AUTHORITIES = ["official", "local", "heuristic", "partial", "none"];
|
|
264
319
|
|
|
@@ -271,6 +326,9 @@ class TokenStatsRepositoryError extends Error {
|
|
|
271
326
|
|
|
272
327
|
class TokenStatsRepositoryLimitError extends TokenStatsRepositoryError {
|
|
273
328
|
}
|
|
329
|
+
var MAX_CLIENT_MODEL_PAGE_SIZE = 100;
|
|
330
|
+
var MAX_CLIENT_MODEL_PAGE = 400;
|
|
331
|
+
var CLIENT_MODEL_KEYWORD_MAX = 256;
|
|
274
332
|
var REPORTING_INCOMPLETE_KEY = "reporting:incomplete";
|
|
275
333
|
var MAX_PENDING_ATTEMPTS = 256;
|
|
276
334
|
var LOG_INTERVAL_MS = 60000;
|
|
@@ -410,296 +468,40 @@ class TokenStatsRepository {
|
|
|
410
468
|
})).sort(groupBy === "model" ? (a, b) => b.inputTokens + b.outputTokens - a.inputTokens - a.outputTokens || b.upstreamAttempts - a.upstreamAttempts || a.dimension.localeCompare(b.dimension) : (a, b) => a.bucketStartMs - b.bucketStartMs || a.dimension.localeCompare(b.dimension))
|
|
411
469
|
};
|
|
412
470
|
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
this.name = "FetchModelsError";
|
|
422
|
-
this.code = opts.code;
|
|
423
|
-
this.status = opts.status;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
async function fetchModels(opts = {}) {
|
|
427
|
-
if (typeof opts === "string") {
|
|
428
|
-
return fetchModels({ provider: opts });
|
|
429
|
-
}
|
|
430
|
-
const url = opts.baseUrl ?? "https://models.dev/api.json";
|
|
431
|
-
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
432
|
-
if (typeof fetchImpl !== "function") {
|
|
433
|
-
throw new FetchModelsError({
|
|
434
|
-
code: "UNAVAILABLE",
|
|
435
|
-
message: "No fetch implementation found. Pass a custom `fetch` in options or run on a platform with global fetch (Node 18+, modern browsers)."
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
let res;
|
|
439
|
-
try {
|
|
440
|
-
res = await fetchImpl(url, opts.signal ? { signal: opts.signal } : undefined);
|
|
441
|
-
} catch (err) {
|
|
442
|
-
const message = err instanceof Error ? err.message : "Unknown network error";
|
|
443
|
-
throw new FetchModelsError({ code: "NETWORK", message });
|
|
444
|
-
}
|
|
445
|
-
if (!res.ok) {
|
|
446
|
-
let body = "";
|
|
447
|
-
try {
|
|
448
|
-
body = await res.text();
|
|
449
|
-
} catch {}
|
|
450
|
-
const snippet = body ? ` Body: ${body.slice(0, 256)}${body.length > 256 ? "\u2026" : ""}` : "";
|
|
451
|
-
throw new FetchModelsError({
|
|
452
|
-
code: "HTTP",
|
|
453
|
-
status: res.status,
|
|
454
|
-
message: `Failed to fetch models.dev API (${res.status} ${res.statusText}).${snippet}`
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
let data;
|
|
458
|
-
try {
|
|
459
|
-
data = await res.json();
|
|
460
|
-
} catch (err) {
|
|
461
|
-
const message = err instanceof Error ? err.message : "Invalid JSON";
|
|
462
|
-
throw new FetchModelsError({ code: "PARSE", message });
|
|
463
|
-
}
|
|
464
|
-
const catalog = data;
|
|
465
|
-
const { provider, model } = opts;
|
|
466
|
-
if (!provider && !model)
|
|
467
|
-
return catalog;
|
|
468
|
-
if (provider && !model)
|
|
469
|
-
return catalog[provider];
|
|
470
|
-
if (provider && model)
|
|
471
|
-
return catalog[provider]?.models?.[model];
|
|
472
|
-
const matches = [];
|
|
473
|
-
if (!model)
|
|
474
|
-
return matches;
|
|
475
|
-
for (const [provKey, prov] of Object.entries(catalog)) {
|
|
476
|
-
const m = prov?.models?.[model];
|
|
477
|
-
if (m)
|
|
478
|
-
matches.push({ provider: provKey, model: m });
|
|
479
|
-
}
|
|
480
|
-
return matches;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// plugins/token-stats/server/price-catalog.ts
|
|
484
|
-
var PRICE_SETTINGS_KEY = "pricing:settings:v1";
|
|
485
|
-
var PRICE_CACHE_KEY = "pricing:catalog:v1";
|
|
486
|
-
var PRICE_STATUS_KEY = "pricing:status:v1";
|
|
487
|
-
var PRICE_SOURCE = "https://models.dev/api.json";
|
|
488
|
-
var DEFAULT_PRICE_SETTINGS = { autoRefresh: true, intervalMinutes: 60, timeoutSeconds: 15 };
|
|
489
|
-
function parsePriceSettings(value) {
|
|
490
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
491
|
-
throw new Error("invalid_input");
|
|
492
|
-
const v = value;
|
|
493
|
-
if (Object.keys(v).some((key) => !["autoRefresh", "intervalMinutes", "timeoutSeconds"].includes(key)) || typeof v.autoRefresh !== "boolean" || !Number.isInteger(v.intervalMinutes) || v.intervalMinutes < 1 || v.intervalMinutes > 1440 || !Number.isInteger(v.timeoutSeconds) || v.timeoutSeconds < 5 || v.timeoutSeconds > 60) {
|
|
494
|
-
throw new Error("invalid_input");
|
|
495
|
-
}
|
|
496
|
-
return { autoRefresh: v.autoRefresh, intervalMinutes: v.intervalMinutes, timeoutSeconds: v.timeoutSeconds };
|
|
497
|
-
}
|
|
498
|
-
function pricedCatalog(value) {
|
|
499
|
-
if (!value || typeof value !== "object")
|
|
500
|
-
throw new Error("invalid_catalog");
|
|
501
|
-
const input = value;
|
|
502
|
-
const output = {};
|
|
503
|
-
let count = 0;
|
|
504
|
-
for (const id of ["openai", "anthropic", "google", "xai"]) {
|
|
505
|
-
const provider = input[id];
|
|
506
|
-
if (!provider?.models || typeof provider.models !== "object")
|
|
507
|
-
continue;
|
|
508
|
-
const models = {};
|
|
509
|
-
for (const [modelId, model] of Object.entries(provider.models)) {
|
|
510
|
-
const cost = model?.cost;
|
|
511
|
-
if (!cost || typeof cost.input !== "number" || !Number.isFinite(cost.input) || cost.input < 0 || typeof cost.output !== "number" || !Number.isFinite(cost.output) || cost.output < 0)
|
|
512
|
-
continue;
|
|
513
|
-
if (++count > 1e4)
|
|
514
|
-
throw new Error("invalid_catalog");
|
|
515
|
-
Object.defineProperty(models, modelId, { value: { id: modelId, name: model.name, cost }, enumerable: true });
|
|
516
|
-
}
|
|
517
|
-
if (Object.keys(models).length)
|
|
518
|
-
output[id] = { id, name: provider.name, models };
|
|
519
|
-
}
|
|
520
|
-
if (!count)
|
|
521
|
-
throw new Error("invalid_catalog");
|
|
522
|
-
return output;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
class PriceCatalogManager {
|
|
526
|
-
storage;
|
|
527
|
-
options;
|
|
528
|
-
state = {
|
|
529
|
-
settings: { ...DEFAULT_PRICE_SETTINGS },
|
|
530
|
-
source: PRICE_SOURCE,
|
|
531
|
-
refreshing: false,
|
|
532
|
-
lastAttemptAt: null,
|
|
533
|
-
lastSuccessAt: null,
|
|
534
|
-
nextRefreshAt: null,
|
|
535
|
-
lastError: null,
|
|
536
|
-
consecutiveFailures: 0,
|
|
537
|
-
modelCount: 0,
|
|
538
|
-
providerCount: 0
|
|
539
|
-
};
|
|
540
|
-
timer;
|
|
541
|
-
controller;
|
|
542
|
-
inFlight;
|
|
543
|
-
initialization;
|
|
544
|
-
stopped = false;
|
|
545
|
-
now;
|
|
546
|
-
constructor(storage, options = {}) {
|
|
547
|
-
this.storage = storage;
|
|
548
|
-
this.options = options;
|
|
549
|
-
this.now = options.now ?? Date.now;
|
|
550
|
-
}
|
|
551
|
-
async start() {
|
|
552
|
-
await (this.initialization ??= this.restore());
|
|
553
|
-
}
|
|
554
|
-
status() {
|
|
555
|
-
return structuredClone(this.state);
|
|
556
|
-
}
|
|
557
|
-
async restore() {
|
|
558
|
-
const settings = await this.storage.get(PRICE_SETTINGS_KEY);
|
|
559
|
-
if (settings !== null) {
|
|
560
|
-
try {
|
|
561
|
-
this.state.settings = parsePriceSettings(settings);
|
|
562
|
-
} catch {}
|
|
563
|
-
}
|
|
564
|
-
const previous = await this.storage.get(PRICE_STATUS_KEY);
|
|
565
|
-
if (previous) {
|
|
566
|
-
if (typeof previous.lastAttemptAt === "number" && Number.isFinite(previous.lastAttemptAt))
|
|
567
|
-
this.state.lastAttemptAt = previous.lastAttemptAt;
|
|
568
|
-
if (["timeout", "network", "invalid_catalog", "storage"].includes(previous.lastError))
|
|
569
|
-
this.state.lastError = previous.lastError;
|
|
570
|
-
if (Number.isSafeInteger(previous.consecutiveFailures) && previous.consecutiveFailures >= 0)
|
|
571
|
-
this.state.consecutiveFailures = previous.consecutiveFailures;
|
|
572
|
-
}
|
|
573
|
-
const cache = await this.storage.get(PRICE_CACHE_KEY);
|
|
574
|
-
if (cache?.version === 1 && Number.isFinite(cache.fetchedAt)) {
|
|
575
|
-
try {
|
|
576
|
-
const catalog = pricedCatalog(cache.catalog);
|
|
577
|
-
this.state.lastSuccessAt = cache.fetchedAt;
|
|
578
|
-
this.count(catalog);
|
|
579
|
-
} catch {
|
|
580
|
-
this.state.lastError = "invalid_catalog";
|
|
581
|
-
}
|
|
471
|
+
async listClientModels(input = {}) {
|
|
472
|
+
const pageSize = Math.max(1, Math.min(MAX_CLIENT_MODEL_PAGE_SIZE, Math.trunc(input.pageSize ?? 50)));
|
|
473
|
+
const page = Math.max(1, Math.trunc(input.page ?? 1));
|
|
474
|
+
if (!Number.isSafeInteger(page) || page > MAX_CLIENT_MODEL_PAGE)
|
|
475
|
+
throw new TokenStatsRepositoryError("invalid token-stats page");
|
|
476
|
+
const keyword = input.keyword;
|
|
477
|
+
if (keyword !== undefined && (typeof keyword !== "string" || keyword.length > CLIENT_MODEL_KEYWORD_MAX)) {
|
|
478
|
+
throw new TokenStatsRepositoryError("invalid token-stats keyword");
|
|
582
479
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
return Promise.resolve();
|
|
602
|
-
if (this.inFlight)
|
|
603
|
-
return this.inFlight;
|
|
604
|
-
this.clearTimer();
|
|
605
|
-
this.state.nextRefreshAt = null;
|
|
606
|
-
this.state.refreshing = true;
|
|
607
|
-
this.state.lastAttemptAt = this.now();
|
|
608
|
-
const task = this.load().finally(() => {
|
|
609
|
-
if (this.inFlight === task)
|
|
610
|
-
this.inFlight = undefined;
|
|
480
|
+
const observation = this.storage.observation;
|
|
481
|
+
if (observation === undefined)
|
|
482
|
+
throw new TokenStatsRepositoryError("token-stats observation storage is required");
|
|
483
|
+
const since = Date.now() - TOKEN_STATS_RETENTION_MS;
|
|
484
|
+
const hasKeyword = keyword !== undefined && keyword.length > 0;
|
|
485
|
+
const escaped = hasKeyword ? keyword.replace(/[\\%_]/g, (character) => `\\${character}`) : null;
|
|
486
|
+
return observation.withDatabase((db) => {
|
|
487
|
+
const countSql = hasKeyword ? "SELECT COUNT(DISTINCT model) AS total FROM token_stats_attempts WHERE finished_at_ms >= ? AND model LIKE ? ESCAPE '\\'" : "SELECT COUNT(DISTINCT model) AS total FROM token_stats_attempts WHERE finished_at_ms >= ?";
|
|
488
|
+
const totalRow = db.query(countSql).get(...hasKeyword ? [since, `%${escaped}%`] : [since]);
|
|
489
|
+
const total = Number(totalRow?.total ?? 0);
|
|
490
|
+
const pageSql = hasKeyword ? "SELECT DISTINCT model FROM token_stats_attempts WHERE finished_at_ms >= ? AND model LIKE ? ESCAPE '\\' ORDER BY model ASC LIMIT ? OFFSET ?" : "SELECT DISTINCT model FROM token_stats_attempts WHERE finished_at_ms >= ? ORDER BY model ASC LIMIT ? OFFSET ?";
|
|
491
|
+
const rows = db.query(pageSql).all(...hasKeyword ? [since, `%${escaped}%`, pageSize, (page - 1) * pageSize] : [since, pageSize, (page - 1) * pageSize]);
|
|
492
|
+
return {
|
|
493
|
+
models: rows.map((row) => String(row.model)),
|
|
494
|
+
total: Number.isSafeInteger(total) && total >= 0 ? total : 0,
|
|
495
|
+
page,
|
|
496
|
+
pageSize
|
|
497
|
+
};
|
|
611
498
|
});
|
|
612
|
-
this.inFlight = task;
|
|
613
|
-
return task;
|
|
614
|
-
}
|
|
615
|
-
stop() {
|
|
616
|
-
this.stopped = true;
|
|
617
|
-
this.clearTimer();
|
|
618
|
-
this.controller?.abort();
|
|
619
499
|
}
|
|
620
|
-
|
|
621
|
-
this.
|
|
622
|
-
this.state.modelCount = Object.values(catalog).reduce((total, provider) => total + Object.keys(provider.models).length, 0);
|
|
500
|
+
async mappings() {
|
|
501
|
+
return readPriceModelMappings(this.storage);
|
|
623
502
|
}
|
|
624
|
-
async
|
|
625
|
-
|
|
626
|
-
this.controller = controller;
|
|
627
|
-
let timeout;
|
|
628
|
-
let onAbort;
|
|
629
|
-
let timedOut = false;
|
|
630
|
-
let phase = "storage";
|
|
631
|
-
try {
|
|
632
|
-
await this.persist();
|
|
633
|
-
if (this.stopped)
|
|
634
|
-
return;
|
|
635
|
-
phase = "network";
|
|
636
|
-
const failure = new Promise((_, reject) => {
|
|
637
|
-
onAbort = () => reject(new Error("aborted"));
|
|
638
|
-
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
639
|
-
timeout = setTimeout(() => {
|
|
640
|
-
timedOut = true;
|
|
641
|
-
controller.abort();
|
|
642
|
-
}, this.state.settings.timeoutSeconds * 1000);
|
|
643
|
-
});
|
|
644
|
-
const fetched = await Promise.race([
|
|
645
|
-
fetchModels({ ...this.options.fetch ? { fetch: this.options.fetch } : {}, signal: controller.signal }),
|
|
646
|
-
failure
|
|
647
|
-
]);
|
|
648
|
-
if (this.stopped)
|
|
649
|
-
return;
|
|
650
|
-
phase = "invalid_catalog";
|
|
651
|
-
const catalog = pricedCatalog(fetched);
|
|
652
|
-
const fetchedAt = Math.max(this.now(), (this.state.lastSuccessAt ?? -1) + 1);
|
|
653
|
-
phase = "storage";
|
|
654
|
-
await this.storage.set(PRICE_CACHE_KEY, { version: 1, fetchedAt, catalog });
|
|
655
|
-
if (this.stopped)
|
|
656
|
-
return;
|
|
657
|
-
this.state.lastSuccessAt = fetchedAt;
|
|
658
|
-
this.count(catalog);
|
|
659
|
-
this.state.lastError = null;
|
|
660
|
-
this.state.consecutiveFailures = 0;
|
|
661
|
-
} catch {
|
|
662
|
-
if (this.stopped)
|
|
663
|
-
return;
|
|
664
|
-
this.state.lastError = timedOut ? "timeout" : phase;
|
|
665
|
-
this.state.consecutiveFailures++;
|
|
666
|
-
} finally {
|
|
667
|
-
if (timeout !== undefined)
|
|
668
|
-
clearTimeout(timeout);
|
|
669
|
-
if (onAbort)
|
|
670
|
-
controller.signal.removeEventListener("abort", onAbort);
|
|
671
|
-
if (this.controller === controller)
|
|
672
|
-
this.controller = undefined;
|
|
673
|
-
this.state.refreshing = false;
|
|
674
|
-
if (!this.stopped) {
|
|
675
|
-
this.schedule();
|
|
676
|
-
try {
|
|
677
|
-
await this.persist();
|
|
678
|
-
} catch {
|
|
679
|
-
this.state.lastError = "storage";
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
clearTimer() {
|
|
685
|
-
if (this.timer !== undefined)
|
|
686
|
-
(this.options.cancel ?? clearTimeout)(this.timer);
|
|
687
|
-
this.timer = undefined;
|
|
688
|
-
}
|
|
689
|
-
schedule() {
|
|
690
|
-
this.clearTimer();
|
|
691
|
-
this.state.nextRefreshAt = null;
|
|
692
|
-
if (this.stopped || this.state.refreshing || !this.state.settings.autoRefresh)
|
|
693
|
-
return;
|
|
694
|
-
const interval = this.state.settings.intervalMinutes * 60000;
|
|
695
|
-
const due = this.state.consecutiveFailures ? this.now() + Math.min(interval, 60000 * Math.min(15, 2 ** Math.min(this.state.consecutiveFailures - 1, 4))) : this.state.lastSuccessAt === null ? this.now() : Math.max(this.now(), this.state.lastSuccessAt + interval);
|
|
696
|
-
this.state.nextRefreshAt = due;
|
|
697
|
-
this.timer = (this.options.schedule ?? setTimeout)(() => {
|
|
698
|
-
this.refresh();
|
|
699
|
-
}, Math.max(0, due - this.now()));
|
|
700
|
-
}
|
|
701
|
-
persist() {
|
|
702
|
-
return this.storage.set(PRICE_STATUS_KEY, this.status());
|
|
503
|
+
async configureMappings(mappings) {
|
|
504
|
+
await this.storage.set(PRICE_MODEL_MAPPINGS_KEY, mappings);
|
|
703
505
|
}
|
|
704
506
|
}
|
|
705
507
|
|
|
@@ -731,22 +533,22 @@ function jsonResponse(value, status = 200, maxBytes = MAX_RESPONSE_BYTES) {
|
|
|
731
533
|
}
|
|
732
534
|
function errorResponse(error) {
|
|
733
535
|
const code = error instanceof ControlError ? error.code : error instanceof TokenStatsRepositoryLimitError ? "response_limit" : error instanceof TokenStatsRepositoryError ? "invalid_persisted_value" : "internal_error";
|
|
734
|
-
const status = code === "request_cancelled" || code === "invalid_input" ? 400 : code === "disposed" ? 409 : 500;
|
|
536
|
+
const status = code === "request_cancelled" || code === "invalid_input" ? 400 : code === "disposed" ? 409 : code === "catalog_unavailable" || code === "storage_unavailable" ? 503 : 500;
|
|
735
537
|
return jsonResponse({ error: code }, status);
|
|
736
538
|
}
|
|
737
|
-
async function readSettings(request) {
|
|
539
|
+
async function readSettings(request, maxBytes = 4096) {
|
|
738
540
|
const reader = request.body?.getReader();
|
|
739
541
|
if (!reader)
|
|
740
542
|
throw new ControlError("invalid_input");
|
|
741
543
|
const chunks = [];
|
|
742
544
|
let size = 0;
|
|
743
545
|
try {
|
|
744
|
-
|
|
546
|
+
for (;; ) {
|
|
745
547
|
const { done, value } = await reader.read();
|
|
746
548
|
if (done)
|
|
747
549
|
break;
|
|
748
550
|
size += value.byteLength;
|
|
749
|
-
if (size >
|
|
551
|
+
if (size > maxBytes) {
|
|
750
552
|
await reader.cancel();
|
|
751
553
|
throw new ControlError("invalid_input");
|
|
752
554
|
}
|
|
@@ -805,19 +607,30 @@ function abortable(promise, requestSignal, hostSignal) {
|
|
|
805
607
|
});
|
|
806
608
|
});
|
|
807
609
|
}
|
|
610
|
+
function parseModelQuery(request) {
|
|
611
|
+
const params = new URL(request.url).searchParams;
|
|
612
|
+
for (const key of ["provider", "search", "page", "pageSize"])
|
|
613
|
+
if (params.getAll(key).length > 1)
|
|
614
|
+
return null;
|
|
615
|
+
const provider = params.get("provider") ?? undefined;
|
|
616
|
+
const search = params.get("search") ?? undefined;
|
|
617
|
+
const page = params.get("page") === null ? 1 : Number(params.get("page"));
|
|
618
|
+
const pageSize = params.get("pageSize") === null ? 50 : Number(params.get("pageSize"));
|
|
619
|
+
if (!Number.isSafeInteger(page) || page < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > MAX_CLIENT_MODEL_PAGE_SIZE)
|
|
620
|
+
return null;
|
|
621
|
+
return { ...provider === undefined ? {} : { provider }, ...search === undefined ? {} : { search }, page, pageSize };
|
|
622
|
+
}
|
|
808
623
|
|
|
809
624
|
class TokenStatsControl {
|
|
810
625
|
host;
|
|
811
626
|
api;
|
|
812
627
|
rpc = [];
|
|
813
628
|
repository;
|
|
814
|
-
pricing;
|
|
815
629
|
disposed = false;
|
|
816
630
|
abortListener;
|
|
817
|
-
constructor(host
|
|
631
|
+
constructor(host) {
|
|
818
632
|
this.host = host;
|
|
819
633
|
this.repository = new TokenStatsRepository(host.storage);
|
|
820
|
-
this.pricing = new PriceCatalogManager(host.storage.uncached?.() ?? host.storage, options);
|
|
821
634
|
this.abortListener = () => {
|
|
822
635
|
this.dispose();
|
|
823
636
|
};
|
|
@@ -833,6 +646,16 @@ class TokenStatsControl {
|
|
|
833
646
|
if (signal?.aborted)
|
|
834
647
|
throw new ControlError("request_cancelled");
|
|
835
648
|
}
|
|
649
|
+
catalogService() {
|
|
650
|
+
const services = this.host.services;
|
|
651
|
+
if (services === undefined)
|
|
652
|
+
throw new ControlError("catalog_unavailable");
|
|
653
|
+
try {
|
|
654
|
+
return services.consume("models-dev", MODELS_DEV_CATALOG_SERVICE_ID, MODELS_DEV_CATALOG_CONTRACT_VERSION);
|
|
655
|
+
} catch {
|
|
656
|
+
throw new ControlError("catalog_unavailable");
|
|
657
|
+
}
|
|
658
|
+
}
|
|
836
659
|
buildApi() {
|
|
837
660
|
const invoke = (handler) => async (context) => {
|
|
838
661
|
try {
|
|
@@ -883,48 +706,87 @@ class TokenStatsControl {
|
|
|
883
706
|
methods: ["GET"],
|
|
884
707
|
handler: "getPricing",
|
|
885
708
|
invoke: invoke(async () => {
|
|
886
|
-
|
|
887
|
-
return jsonResponse(
|
|
709
|
+
const status = this.catalogService().status();
|
|
710
|
+
return jsonResponse({ ...status, source: MODELS_DEV_SOURCE_URL });
|
|
711
|
+
})
|
|
712
|
+
}, {
|
|
713
|
+
path: "/pricing/models",
|
|
714
|
+
methods: ["GET"],
|
|
715
|
+
handler: "getPricingModels",
|
|
716
|
+
invoke: invoke(async (context) => {
|
|
717
|
+
const query = parseModelQuery(context.request);
|
|
718
|
+
if (query === null)
|
|
719
|
+
return jsonResponse({ error: "invalid_input" }, 400);
|
|
720
|
+
const result = this.catalogService().modelOptions(query);
|
|
721
|
+
const models = result.models.map((model) => ({
|
|
722
|
+
provider: model.provider,
|
|
723
|
+
providerName: model.providerName,
|
|
724
|
+
model: model.model,
|
|
725
|
+
name: model.name
|
|
726
|
+
}));
|
|
727
|
+
return jsonResponse({ models, total: result.total, page: result.page, pageSize: result.pageSize }, 200, MAX_PAGE_STATS_RESPONSE_BYTES);
|
|
888
728
|
})
|
|
889
729
|
}, {
|
|
890
|
-
path: "/pricing/
|
|
730
|
+
path: "/pricing/mappings",
|
|
731
|
+
methods: ["GET"],
|
|
732
|
+
handler: "getPricingMappings",
|
|
733
|
+
invoke: invoke(async () => jsonResponse({ mappings: await this.repository.mappings() }))
|
|
734
|
+
}, {
|
|
735
|
+
path: "/pricing/mappings",
|
|
891
736
|
methods: ["PUT"],
|
|
892
|
-
handler: "
|
|
737
|
+
handler: "configurePricingMappings",
|
|
893
738
|
invoke: invoke(async (context) => {
|
|
894
|
-
let
|
|
739
|
+
let mappings;
|
|
895
740
|
try {
|
|
896
|
-
|
|
741
|
+
mappings = parsePriceModelMappings(await readSettings(context.request, MAX_RESPONSE_BYTES));
|
|
897
742
|
} catch {
|
|
898
743
|
throw new ControlError("invalid_input");
|
|
899
744
|
}
|
|
745
|
+
const service = this.catalogService();
|
|
746
|
+
const existing = await this.repository.mappings();
|
|
747
|
+
if (mappings.some((mapping) => service.resolveModel({ model: mapping.model, pricingProvider: mapping.provider }) === null && !isUnchangedPriceModelMapping(mapping, existing)))
|
|
748
|
+
throw new ControlError("invalid_input");
|
|
900
749
|
this.assertAlive(context.requestSignal);
|
|
901
|
-
|
|
750
|
+
await this.repository.configureMappings(mappings);
|
|
751
|
+
return jsonResponse({ mappings });
|
|
902
752
|
})
|
|
903
753
|
}, {
|
|
904
|
-
path: "/
|
|
905
|
-
methods: ["
|
|
906
|
-
handler: "
|
|
907
|
-
invoke: invoke(async () => {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
754
|
+
path: "/models",
|
|
755
|
+
methods: ["GET"],
|
|
756
|
+
handler: "getClientModels",
|
|
757
|
+
invoke: invoke(async (context) => {
|
|
758
|
+
const query = parseModelQuery(context.request);
|
|
759
|
+
if (query === null)
|
|
760
|
+
return jsonResponse({ error: "invalid_input" }, 400);
|
|
761
|
+
let page;
|
|
762
|
+
try {
|
|
763
|
+
page = await this.repository.listClientModels({
|
|
764
|
+
...query.search === undefined ? {} : { keyword: query.search },
|
|
765
|
+
page: query.page,
|
|
766
|
+
pageSize: query.pageSize
|
|
767
|
+
});
|
|
768
|
+
} catch (error) {
|
|
769
|
+
if (error instanceof TokenStatsRepositoryError)
|
|
770
|
+
throw new ControlError("storage_unavailable");
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
this.assertAlive(context.requestSignal);
|
|
774
|
+
return jsonResponse(page);
|
|
911
775
|
})
|
|
912
776
|
}];
|
|
913
777
|
}
|
|
914
778
|
async start() {
|
|
915
779
|
this.assertAlive();
|
|
916
|
-
await this.pricing.start();
|
|
917
780
|
}
|
|
918
781
|
dispose() {
|
|
919
782
|
if (this.disposed)
|
|
920
783
|
return;
|
|
921
784
|
this.disposed = true;
|
|
922
|
-
this.pricing.stop();
|
|
923
785
|
this.host.signal.removeEventListener("abort", this.abortListener);
|
|
924
786
|
}
|
|
925
787
|
}
|
|
926
|
-
function createControl(context
|
|
927
|
-
return new TokenStatsControl(context
|
|
788
|
+
function createControl(context) {
|
|
789
|
+
return new TokenStatsControl(context);
|
|
928
790
|
}
|
|
929
791
|
var control_default = { createControl };
|
|
930
792
|
export {
|
|
@@ -932,4 +794,4 @@ export {
|
|
|
932
794
|
control_default as default
|
|
933
795
|
};
|
|
934
796
|
|
|
935
|
-
//# debugId=
|
|
797
|
+
//# debugId=9E6A0268F9DCEF3464756E2164756E21
|