@jeffusion/bungee-core 5.10.0 → 5.11.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.
Files changed (48) hide show
  1. package/dist/main.js +31344 -17334
  2. package/dist/plugins/ai-transformer/index.js.map +2 -2
  3. package/dist/plugins/anthropic-request-sanitizer/index.js.map +2 -2
  4. package/dist/plugins/anthropic-tool-name-transformer/index.js.map +2 -2
  5. package/dist/plugins/chatgpt-oauth/control.js +513 -1
  6. package/dist/plugins/chatgpt-oauth/control.js.map +12 -4
  7. package/dist/plugins/chatgpt-oauth/index.js +511 -4
  8. package/dist/plugins/chatgpt-oauth/index.js.map +13 -5
  9. package/dist/plugins/chatgpt-oauth/manifest.json +19 -0
  10. package/dist/plugins/deepseek-reasoning-fix/index.js.map +2 -2
  11. package/dist/plugins/key-access/index.js.map +2 -2
  12. package/dist/plugins/key-rate-limit/index.js.map +2 -2
  13. package/dist/plugins/local-accounts/control.js.map +2 -2
  14. package/dist/plugins/local-accounts/index.js.map +2 -2
  15. package/dist/plugins/model-mapping/control.js +72 -17741
  16. package/dist/plugins/model-mapping/control.js.map +6 -58
  17. package/dist/plugins/model-mapping/index.js +32 -17433
  18. package/dist/plugins/model-mapping/index.js.map +6 -58
  19. package/dist/plugins/model-mapping/manifest.json +30 -14
  20. package/dist/plugins/models-dev/control.js +1042 -0
  21. package/dist/plugins/models-dev/control.js.map +16 -0
  22. package/dist/plugins/models-dev/index.js +15418 -0
  23. package/dist/plugins/models-dev/index.js.map +134 -0
  24. package/dist/plugins/models-dev/manifest.json +199 -0
  25. package/dist/plugins/models-dev/ui/ModelsDevSettings.svelte +120 -0
  26. package/dist/plugins/openai-messages-to-chat/index.js.map +2 -2
  27. package/dist/plugins/signature-repair/index.js.map +2 -2
  28. package/dist/plugins/token-budget/control.js +524 -9
  29. package/dist/plugins/token-budget/control.js.map +12 -4
  30. package/dist/plugins/token-budget/index.js +519 -12
  31. package/dist/plugins/token-budget/index.js.map +13 -6
  32. package/dist/plugins/token-budget/manifest.json +16 -2
  33. package/dist/plugins/token-metering/index.js +93 -2
  34. package/dist/plugins/token-metering/index.js.map +12 -6
  35. package/dist/plugins/token-stats/control.js +174 -312
  36. package/dist/plugins/token-stats/control.js.map +7 -7
  37. package/dist/plugins/token-stats/index.js +283 -486
  38. package/dist/plugins/token-stats/index.js.map +16 -12
  39. package/dist/plugins/token-stats/manifest.json +99 -12
  40. package/dist/plugins/token-stats/ui/ClientModelPicker.svelte +76 -0
  41. package/dist/plugins/token-stats/ui/PriceModelPicker.svelte +87 -0
  42. package/dist/plugins/token-stats/ui/TokenStatsSettings.svelte +136 -80
  43. package/dist/plugins/token-stats/ui/client-model-search.test.ts +58 -0
  44. package/dist/plugins/token-stats/ui/client-model-search.ts +16 -0
  45. package/dist/plugins/token-stats/ui/model-search.ts +45 -0
  46. package/dist/plugins/token-stats/ui/pricing-catalog.test.ts +114 -0
  47. package/dist/plugins/token-stats/ui/pricing-catalog.ts +17 -0
  48. 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
- // node_modules/@tokenlens/fetch/dist/index.js
416
- class FetchModelsError extends Error {
417
- code;
418
- status;
419
- constructor(opts) {
420
- super(opts.message);
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
- if (this.stopped)
584
- return;
585
- this.schedule();
586
- await this.persist();
587
- }
588
- async configure(settings) {
589
- await this.start();
590
- if (this.stopped)
591
- throw new Error("disposed");
592
- const parsed = parsePriceSettings(settings);
593
- await this.storage.set(PRICE_SETTINGS_KEY, parsed);
594
- this.state.settings = parsed;
595
- this.schedule();
596
- await this.persist();
597
- return this.status();
598
- }
599
- refresh() {
600
- if (this.stopped)
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
- count(catalog) {
621
- this.state.providerCount = Object.keys(catalog).length;
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 load() {
625
- const controller = new AbortController;
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
- while (true) {
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 > 4096) {
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, options) {
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
- await this.pricing.start();
887
- return jsonResponse(this.pricing.status());
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/settings",
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: "configurePricing",
737
+ handler: "configurePricingMappings",
893
738
  invoke: invoke(async (context) => {
894
- let settings;
739
+ let mappings;
895
740
  try {
896
- settings = parsePriceSettings(await readSettings(context.request));
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
- return jsonResponse(await this.pricing.configure(settings));
750
+ await this.repository.configureMappings(mappings);
751
+ return jsonResponse({ mappings });
902
752
  })
903
753
  }, {
904
- path: "/pricing/refresh",
905
- methods: ["POST"],
906
- handler: "refreshPricing",
907
- invoke: invoke(async () => {
908
- await this.pricing.start();
909
- this.pricing.refresh();
910
- return jsonResponse(this.pricing.status(), 202);
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, options) {
927
- return new TokenStatsControl(context, options);
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=A06BDE7344854B3164756E2164756E21
797
+ //# debugId=9E6A0268F9DCEF3464756E2164756E21