llm-cli-gateway 2.16.0 → 2.17.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 (61) hide show
  1. package/.agents/skills/least-cost-routing/SKILL.md +123 -0
  2. package/CHANGELOG.md +28 -0
  3. package/dist/acp/client.js +5 -0
  4. package/dist/acp/flight-redaction.d.ts +2 -0
  5. package/dist/acp/flight-redaction.js +2 -0
  6. package/dist/acp/runtime.d.ts +1 -0
  7. package/dist/acp/runtime.js +20 -0
  8. package/dist/acp/types.d.ts +52 -0
  9. package/dist/acp/types.js +8 -0
  10. package/dist/api-provider.d.ts +1 -0
  11. package/dist/api-provider.js +1 -0
  12. package/dist/async-job-manager.d.ts +16 -0
  13. package/dist/async-job-manager.js +70 -22
  14. package/dist/claude-mcp-config.js +1 -1
  15. package/dist/compressor/transforms/ansi.js +12 -2
  16. package/dist/config.d.ts +31 -0
  17. package/dist/config.js +142 -7
  18. package/dist/db.js +4 -4
  19. package/dist/doctor.d.ts +34 -1
  20. package/dist/doctor.js +85 -3
  21. package/dist/executor.d.ts +5 -0
  22. package/dist/executor.js +11 -1
  23. package/dist/flight-recorder.d.ts +10 -0
  24. package/dist/flight-recorder.js +68 -2
  25. package/dist/http-transport.js +19 -21
  26. package/dist/index.d.ts +41 -1
  27. package/dist/index.js +658 -30
  28. package/dist/job-store.d.ts +10 -2
  29. package/dist/job-store.js +154 -43
  30. package/dist/lcr-priors.d.ts +60 -0
  31. package/dist/lcr-priors.js +190 -0
  32. package/dist/lcr-router-env.d.ts +20 -0
  33. package/dist/lcr-router-env.js +133 -0
  34. package/dist/lcr-telemetry.d.ts +2 -0
  35. package/dist/lcr-telemetry.js +17 -0
  36. package/dist/least-cost-router.d.ts +86 -0
  37. package/dist/least-cost-router.js +296 -0
  38. package/dist/least-cost-types.d.ts +34 -0
  39. package/dist/least-cost-types.js +1 -0
  40. package/dist/migrate-sessions.js +1 -1
  41. package/dist/migrate.js +1 -1
  42. package/dist/model-registry.js +1 -1
  43. package/dist/postgres-job-store-worker.js +56 -13
  44. package/dist/pricing.d.ts +17 -0
  45. package/dist/pricing.js +167 -0
  46. package/dist/provider-definitions.d.ts +4 -0
  47. package/dist/provider-definitions.js +28 -0
  48. package/dist/request-helpers.d.ts +2 -2
  49. package/dist/resources.d.ts +37 -2
  50. package/dist/resources.js +96 -1
  51. package/dist/retry.d.ts +1 -0
  52. package/dist/retry.js +1 -1
  53. package/dist/token-estimator.d.ts +6 -0
  54. package/dist/token-estimator.js +59 -0
  55. package/dist/upstream-contracts.js +1 -1
  56. package/dist/validation-receipt.js +1 -1
  57. package/dist/validation-tools.d.ts +6 -0
  58. package/dist/validation-tools.js +174 -54
  59. package/npm-shrinkwrap.json +2 -2
  60. package/package.json +9 -4
  61. package/setup/status.schema.json +68 -0
@@ -280,15 +280,23 @@ export declare class MemoryJobStore implements JobStore {
280
280
  export declare class PostgresJobStore implements JobStore, ValidationRunStore {
281
281
  private logger;
282
282
  private worker;
283
- private tmpDir;
284
- private nextRequestId;
283
+ private retiringWorker;
284
+ private readonly intentionallyRetiredWorkers;
285
+ private workerTerminationPending;
285
286
  private closed;
287
+ private readonly workerData;
286
288
  constructor(dsn: string, logger?: Logger, options?: {
287
289
  retentionMs?: number;
288
290
  dedupWindowMs?: number;
289
291
  leaseTtlMs?: number;
290
292
  });
293
+ private createWorker;
291
294
  private syncCall;
295
+ private ensureWorker;
296
+ private callWorker;
297
+ private retireAfterBridgeFailure;
298
+ private retireWorker;
299
+ private markWorkerRetired;
292
300
  recordStart(input: {
293
301
  id: string;
294
302
  correlationId: string;
package/dist/job-store.js CHANGED
@@ -1,8 +1,8 @@
1
- import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync } from "fs";
1
+ import { chmodSync, existsSync } from "fs";
2
2
  import os from "os";
3
3
  import path from "path";
4
4
  import { createHash } from "crypto";
5
- import { Worker } from "worker_threads";
5
+ import { MessageChannel, receiveMessageOnPort, Worker } from "worker_threads";
6
6
  import { fileURLToPath } from "url";
7
7
  import { openDatabase } from "./sqlite-driver.js";
8
8
  import { noopLogger } from "./logger.js";
@@ -30,6 +30,8 @@ export function resolveJobRetentionMs() {
30
30
  return days * 24 * 60 * 60 * 1000;
31
31
  }
32
32
  const DEFAULT_DEDUP_WINDOW_MS = 60 * 60 * 1000;
33
+ const POSTGRES_WORKER_OPERATION_TIMEOUT_MS = 35_000;
34
+ const POSTGRES_WORKER_INITIALIZATION_TIMEOUT_MS = 45_000;
33
35
  export function resolveDedupWindowMs() {
34
36
  const raw = process.env.LLM_GATEWAY_DEDUP_WINDOW_MS;
35
37
  if (raw === undefined)
@@ -757,66 +759,153 @@ export class MemoryJobStore {
757
759
  }
758
760
  export class PostgresJobStore {
759
761
  logger;
760
- worker;
761
- tmpDir;
762
- nextRequestId = 0;
762
+ worker = null;
763
+ retiringWorker = null;
764
+ intentionallyRetiredWorkers = new WeakSet();
765
+ workerTerminationPending = false;
763
766
  closed = false;
767
+ workerData;
764
768
  constructor(dsn, logger = noopLogger, options = {}) {
765
769
  this.logger = logger;
766
770
  if (!dsn) {
767
771
  throw new Error("PostgresJobStore requires a non-empty DSN");
768
772
  }
769
- this.tmpDir = mkdtempSync(path.join(os.tmpdir(), "llm-gateway-pg-job-store-"));
770
- this.worker = new Worker(resolvePostgresWorkerUrl(), {
771
- execArgv: [],
772
- workerData: {
773
- dsn,
774
- retentionMs: options.retentionMs ?? resolveJobRetentionMs(),
775
- dedupWindowMs: options.dedupWindowMs ?? resolveDedupWindowMs(),
776
- leaseTtlMs: options.leaseTtlMs ?? DEFAULT_INSTANCE_LEASE_TTL_MS,
777
- farFutureIso: FAR_FUTURE_ISO,
778
- connectionTimeoutMillis: 5000,
779
- },
780
- });
773
+ this.workerData = {
774
+ dsn,
775
+ retentionMs: options.retentionMs ?? resolveJobRetentionMs(),
776
+ dedupWindowMs: options.dedupWindowMs ?? resolveDedupWindowMs(),
777
+ leaseTtlMs: options.leaseTtlMs ?? DEFAULT_INSTANCE_LEASE_TTL_MS,
778
+ farFutureIso: FAR_FUTURE_ISO,
779
+ connectionTimeoutMillis: 5000,
780
+ };
781
781
  try {
782
- this.syncCall("init");
782
+ this.ensureWorker();
783
783
  }
784
784
  catch (err) {
785
785
  this.closed = true;
786
- void this.worker.terminate();
787
- rmSync(this.tmpDir, { recursive: true, force: true });
786
+ this.retireWorker(this.worker);
788
787
  throw err;
789
788
  }
790
789
  }
790
+ createWorker() {
791
+ const worker = new Worker(resolvePostgresWorkerUrl(), {
792
+ execArgv: [],
793
+ workerData: this.workerData,
794
+ });
795
+ worker.on("message", message => {
796
+ if (!isPostgresWorkerDiagnostic(message))
797
+ return;
798
+ const error = new Error(message.message);
799
+ if (message.stack)
800
+ error.stack = message.stack;
801
+ this.logger.error(`PostgresJobStore worker ${message.kind}`, error);
802
+ });
803
+ worker.on("error", error => {
804
+ this.logger.error("PostgresJobStore worker crashed", error);
805
+ if (!this.closed && this.worker === worker)
806
+ this.retireWorker(worker);
807
+ });
808
+ worker.on("exit", code => {
809
+ const wasRetiring = this.retiringWorker === worker || this.intentionallyRetiredWorkers.has(worker);
810
+ if (this.worker === worker)
811
+ this.worker = null;
812
+ if (wasRetiring)
813
+ this.markWorkerRetired(worker);
814
+ if (this.closed || wasRetiring || code === 0)
815
+ return;
816
+ this.logger.error("PostgresJobStore worker exited unexpectedly", new Error(`PostgresJobStore worker exited with code ${code}`));
817
+ });
818
+ return worker;
819
+ }
791
820
  syncCall(method, ...args) {
792
- if (this.closed && method !== "close") {
821
+ if (this.closed) {
793
822
  throw new Error("PostgresJobStore is closed");
794
823
  }
795
- const shared = new SharedArrayBuffer(4);
796
- const state = new Int32Array(shared);
797
- const resultPath = path.join(this.tmpDir, `result-${process.pid}-${++this.nextRequestId}.json`);
798
- this.worker.postMessage({ method, args, resultPath, shared });
799
- const wait = Atomics.wait(state, 0, 0, method === "init" ? 10_000 : 30_000);
800
- if (wait !== "ok" && wait !== "not-equal") {
801
- throw new Error(`PostgresJobStore ${method} wait failed: ${wait}`);
824
+ const worker = this.ensureWorker();
825
+ return this.callWorker(worker, method, args);
826
+ }
827
+ ensureWorker() {
828
+ if (this.closed)
829
+ throw new Error("PostgresJobStore is closed");
830
+ if (this.worker)
831
+ return this.worker;
832
+ if (this.workerTerminationPending) {
833
+ throw new Error("PostgresJobStore is waiting for a failed worker to terminate before it can recover");
802
834
  }
803
- if (Atomics.load(state, 0) === 2) {
804
- throw new Error(`PostgresJobStore ${method} worker could not write its result`);
835
+ const worker = this.createWorker();
836
+ this.worker = worker;
837
+ try {
838
+ this.callWorker(worker, "init", []);
839
+ return worker;
805
840
  }
806
- let payload;
841
+ catch (err) {
842
+ this.retireWorker(worker);
843
+ throw err;
844
+ }
845
+ }
846
+ callWorker(worker, method, args) {
847
+ const shared = new SharedArrayBuffer(4);
848
+ const state = new Int32Array(shared);
849
+ const { port1: workerPort, port2: responsePort } = new MessageChannel();
807
850
  try {
808
- payload = JSON.parse(readFileSync(resultPath, "utf8"));
851
+ try {
852
+ worker.postMessage({ method, args, shared, responsePort: workerPort }, [workerPort]);
853
+ }
854
+ catch (err) {
855
+ throw this.retireAfterBridgeFailure(worker, `PostgresJobStore ${method} could not dispatch work to its worker`, err);
856
+ }
857
+ const timeoutMs = method === "init"
858
+ ? POSTGRES_WORKER_INITIALIZATION_TIMEOUT_MS
859
+ : POSTGRES_WORKER_OPERATION_TIMEOUT_MS;
860
+ const wait = Atomics.wait(state, 0, 0, timeoutMs);
861
+ if (wait === "timed-out") {
862
+ throw this.retireAfterBridgeFailure(worker, `PostgresJobStore ${method} timed out after ${timeoutMs}ms; the operation outcome is unknown`);
863
+ }
864
+ if (wait !== "ok" && wait !== "not-equal") {
865
+ throw this.retireAfterBridgeFailure(worker, `PostgresJobStore ${method} wait failed: ${wait}`);
866
+ }
867
+ if (Atomics.load(state, 0) !== 1) {
868
+ throw this.retireAfterBridgeFailure(worker, `PostgresJobStore ${method} worker response transport failed`);
869
+ }
870
+ const received = receiveMessageOnPort(responsePort);
871
+ if (!received || !isPostgresWorkerPayload(received.message)) {
872
+ throw this.retireAfterBridgeFailure(worker, `PostgresJobStore ${method} worker signalled without a valid response`);
873
+ }
874
+ const payload = received.message;
875
+ if (!payload.ok) {
876
+ const err = new Error(payload.error.message);
877
+ if (payload.error.stack)
878
+ err.stack = payload.error.stack;
879
+ throw err;
880
+ }
881
+ return payload.value;
809
882
  }
810
883
  finally {
811
- rmSync(resultPath, { force: true });
884
+ responsePort.close();
812
885
  }
813
- if (!payload.ok) {
814
- const err = new Error(payload.error.message);
815
- if (payload.error.stack)
816
- err.stack = payload.error.stack;
817
- throw err;
818
- }
819
- return payload.value;
886
+ }
887
+ retireAfterBridgeFailure(worker, message, cause) {
888
+ this.retireWorker(worker);
889
+ return cause === undefined ? new Error(message) : new Error(message, { cause });
890
+ }
891
+ retireWorker(worker) {
892
+ if (!worker || this.retiringWorker === worker)
893
+ return;
894
+ if (this.worker === worker)
895
+ this.worker = null;
896
+ this.retiringWorker = worker;
897
+ this.intentionallyRetiredWorkers.add(worker);
898
+ this.workerTerminationPending = true;
899
+ void worker.terminate().then(() => this.markWorkerRetired(worker), error => {
900
+ this.logger.error("PostgresJobStore worker termination failed", error);
901
+ this.markWorkerRetired(worker);
902
+ });
903
+ }
904
+ markWorkerRetired(worker) {
905
+ if (this.retiringWorker !== worker)
906
+ return;
907
+ this.retiringWorker = null;
908
+ this.workerTerminationPending = false;
820
909
  }
821
910
  recordStart(input) {
822
911
  this.syncCall("recordStart", input);
@@ -899,19 +988,41 @@ export class PostgresJobStore {
899
988
  close() {
900
989
  if (this.closed)
901
990
  return;
991
+ const worker = this.worker;
902
992
  try {
903
- this.syncCall("close");
993
+ if (worker)
994
+ this.callWorker(worker, "close", []);
904
995
  }
905
996
  catch (err) {
906
997
  this.logger.error("PostgresJobStore close failed", err);
907
998
  }
908
999
  finally {
909
1000
  this.closed = true;
910
- void this.worker.terminate();
911
- rmSync(this.tmpDir, { recursive: true, force: true });
1001
+ this.retireWorker(worker);
912
1002
  }
913
1003
  }
914
1004
  }
1005
+ function isPostgresWorkerPayload(value) {
1006
+ if (!value || typeof value !== "object")
1007
+ return false;
1008
+ const candidate = value;
1009
+ if (candidate.ok === true)
1010
+ return "value" in candidate;
1011
+ if (!candidate.error || typeof candidate.error !== "object" || candidate.ok !== false)
1012
+ return false;
1013
+ const error = candidate.error;
1014
+ return (typeof error.message === "string" &&
1015
+ (error.stack === undefined || typeof error.stack === "string"));
1016
+ }
1017
+ function isPostgresWorkerDiagnostic(value) {
1018
+ if (!value || typeof value !== "object")
1019
+ return false;
1020
+ const candidate = value;
1021
+ return (candidate.type === "postgres-job-store-diagnostic" &&
1022
+ typeof candidate.kind === "string" &&
1023
+ typeof candidate.message === "string" &&
1024
+ (candidate.stack === undefined || typeof candidate.stack === "string"));
1025
+ }
915
1026
  function resolvePostgresWorkerUrl() {
916
1027
  const sibling = new URL("./postgres-job-store-worker.js", import.meta.url);
917
1028
  if (existsSync(fileURLToPath(sibling)))
@@ -0,0 +1,60 @@
1
+ import type { FlightRecorderQuery } from "./flight-recorder.js";
2
+ import type { Confidence } from "./least-cost-types.js";
3
+ import type { ContentType } from "./token-estimator.js";
4
+ export type PriorsScope = "global" | "principal" | "off";
5
+ export declare const CALIBRATION_K_MIN_SAMPLES = 5;
6
+ export declare const CONFIDENCE_HIGH_MIN_SAMPLES = 30;
7
+ export declare const CONFIDENCE_HIGH_MAX_SPREAD = 1.5;
8
+ export declare const CONFIDENCE_MEDIUM_MIN_SAMPLES = 10;
9
+ export declare const CONFIDENCE_MEDIUM_MAX_SPREAD = 3;
10
+ export interface LcrPriorRow {
11
+ provider: string;
12
+ model: string;
13
+ prompt: string;
14
+ inputTokens: number | null;
15
+ outputTokens: number | null;
16
+ cacheReadTokens: number | null;
17
+ cacheCreationTokens: number | null;
18
+ costUsd: number | null;
19
+ costBasis: string | null;
20
+ routeEstCostUsd: number | null;
21
+ ownerPrincipal: string | null;
22
+ sessionContinued: boolean;
23
+ }
24
+ export interface OutputPrior {
25
+ median: number;
26
+ p90: number;
27
+ samples: number;
28
+ }
29
+ export interface CalibrationBucket {
30
+ k: number;
31
+ samples: number;
32
+ p10: number;
33
+ p50: number;
34
+ p90: number;
35
+ confidence: Confidence;
36
+ }
37
+ export interface AccuracyBucket {
38
+ provider: string;
39
+ model: string;
40
+ costBasis: string;
41
+ medianAccuracy: number;
42
+ samples: number;
43
+ p10: number;
44
+ p50: number;
45
+ p90: number;
46
+ }
47
+ export interface LcrPriors {
48
+ outputPriors: Map<string, OutputPrior>;
49
+ calibration: Map<string, CalibrationBucket>;
50
+ accuracyByBasis: Map<string, AccuracyBucket>;
51
+ }
52
+ export interface ComputeLcrPriorsOptions {
53
+ priorsScope: PriorsScope;
54
+ ownerPrincipal?: string;
55
+ }
56
+ export declare function confidenceFromQuality(samples: number, p10: number, p90: number): Confidence;
57
+ export declare function lookupCalibrationK(priors: LcrPriors, contentType: ContentType, family: string): number;
58
+ export declare function computeLcrPriors(rows: LcrPriorRow[], opts: ComputeLcrPriorsOptions): LcrPriors;
59
+ export declare function loadLcrPriorRows(db: FlightRecorderQuery): LcrPriorRow[];
60
+ export declare function computeLcrPriorsFromDb(db: FlightRecorderQuery, opts: ComputeLcrPriorsOptions): LcrPriors;
@@ -0,0 +1,190 @@
1
+ import { estimateInputTokens, classifyContent } from "./token-estimator.js";
2
+ import { modelIdToFamily } from "./pricing.js";
3
+ export const CALIBRATION_K_MIN_SAMPLES = 5;
4
+ export const CONFIDENCE_HIGH_MIN_SAMPLES = 30;
5
+ export const CONFIDENCE_HIGH_MAX_SPREAD = 1.5;
6
+ export const CONFIDENCE_MEDIUM_MIN_SAMPLES = 10;
7
+ export const CONFIDENCE_MEDIUM_MAX_SPREAD = 3.0;
8
+ function isFiniteNumber(n) {
9
+ return typeof n === "number" && Number.isFinite(n);
10
+ }
11
+ function num(n) {
12
+ return isFiniteNumber(n) ? n : 0;
13
+ }
14
+ function percentile(sortedAsc, q) {
15
+ const n = sortedAsc.length;
16
+ if (n === 0)
17
+ return 0;
18
+ if (n === 1)
19
+ return sortedAsc[0];
20
+ const idx = (n - 1) * q;
21
+ const lo = Math.floor(idx);
22
+ const hi = Math.ceil(idx);
23
+ const loVal = sortedAsc.at(lo) ?? 0;
24
+ const hiVal = sortedAsc.at(hi) ?? 0;
25
+ if (lo === hi)
26
+ return loVal;
27
+ return loVal + (hiVal - loVal) * (idx - lo);
28
+ }
29
+ const outputKey = (provider, model) => `${provider}:${model}`;
30
+ const calibrationKey = (contentType, family) => `${contentType}:${family}`;
31
+ const accuracyKey = (provider, model, costBasis) => `${provider}:${model}::${costBasis}`;
32
+ function reconstructActualInput(row, family) {
33
+ const input = num(row.inputTokens);
34
+ if (family.startsWith("claude-")) {
35
+ return input + num(row.cacheReadTokens) + num(row.cacheCreationTokens);
36
+ }
37
+ return input;
38
+ }
39
+ export function confidenceFromQuality(samples, p10, p90) {
40
+ if (p10 <= 0)
41
+ return "low";
42
+ const spread = p90 / p10;
43
+ if (samples >= CONFIDENCE_HIGH_MIN_SAMPLES && spread <= CONFIDENCE_HIGH_MAX_SPREAD) {
44
+ return "high";
45
+ }
46
+ if (samples >= CONFIDENCE_MEDIUM_MIN_SAMPLES && spread <= CONFIDENCE_MEDIUM_MAX_SPREAD) {
47
+ return "medium";
48
+ }
49
+ return "low";
50
+ }
51
+ export function lookupCalibrationK(priors, contentType, family) {
52
+ const bucket = priors.calibration.get(calibrationKey(contentType, family));
53
+ if (!bucket)
54
+ return 1;
55
+ if (bucket.samples < CALIBRATION_K_MIN_SAMPLES)
56
+ return 1;
57
+ return bucket.k;
58
+ }
59
+ export function computeLcrPriors(rows, opts) {
60
+ const empty = {
61
+ outputPriors: new Map(),
62
+ calibration: new Map(),
63
+ accuracyByBasis: new Map(),
64
+ };
65
+ if (opts.priorsScope === "off")
66
+ return empty;
67
+ let scoped = rows;
68
+ if (opts.priorsScope === "principal") {
69
+ const owner = opts.ownerPrincipal;
70
+ if (owner === undefined)
71
+ return empty;
72
+ scoped = rows.filter(r => r.ownerPrincipal === owner);
73
+ }
74
+ const outputSamples = new Map();
75
+ const calibrationRatios = new Map();
76
+ const accuracyRatios = new Map();
77
+ for (const row of scoped) {
78
+ if (isFiniteNumber(row.outputTokens) && row.outputTokens >= 0) {
79
+ const key = outputKey(row.provider, row.model);
80
+ const arr = outputSamples.get(key) ?? [];
81
+ arr.push(row.outputTokens);
82
+ outputSamples.set(key, arr);
83
+ }
84
+ const family = modelIdToFamily(row.model);
85
+ if (family !== "unknown" && !row.sessionContinued && isFiniteNumber(row.inputTokens)) {
86
+ const base = estimateInputTokens(row.prompt, { family });
87
+ if (base > 0) {
88
+ const actual = reconstructActualInput(row, family);
89
+ const contentType = classifyContent(row.prompt);
90
+ const key = calibrationKey(contentType, family);
91
+ const arr = calibrationRatios.get(key) ?? [];
92
+ arr.push(actual / base);
93
+ calibrationRatios.set(key, arr);
94
+ }
95
+ }
96
+ if (isFiniteNumber(row.routeEstCostUsd) &&
97
+ isFiniteNumber(row.costUsd) &&
98
+ row.costUsd > 0 &&
99
+ row.costBasis) {
100
+ const key = accuracyKey(row.provider, row.model, row.costBasis);
101
+ const arr = accuracyRatios.get(key) ?? [];
102
+ arr.push(row.routeEstCostUsd / row.costUsd);
103
+ accuracyRatios.set(key, arr);
104
+ }
105
+ }
106
+ const outputPriors = new Map();
107
+ for (const [key, values] of outputSamples) {
108
+ values.sort((a, b) => a - b);
109
+ outputPriors.set(key, {
110
+ median: percentile(values, 0.5),
111
+ p90: percentile(values, 0.9),
112
+ samples: values.length,
113
+ });
114
+ }
115
+ const calibration = new Map();
116
+ for (const [key, values] of calibrationRatios) {
117
+ values.sort((a, b) => a - b);
118
+ const p10 = percentile(values, 0.1);
119
+ const p50 = percentile(values, 0.5);
120
+ const p90 = percentile(values, 0.9);
121
+ calibration.set(key, {
122
+ k: p50,
123
+ samples: values.length,
124
+ p10,
125
+ p50,
126
+ p90,
127
+ confidence: confidenceFromQuality(values.length, p10, p90),
128
+ });
129
+ }
130
+ const accuracyByBasis = new Map();
131
+ for (const [key, values] of accuracyRatios) {
132
+ values.sort((a, b) => a - b);
133
+ const sep = key.lastIndexOf("::");
134
+ const pm = key.slice(0, sep);
135
+ const costBasis = key.slice(sep + 2);
136
+ const colon = pm.indexOf(":");
137
+ const provider = pm.slice(0, colon);
138
+ const model = pm.slice(colon + 1);
139
+ accuracyByBasis.set(key, {
140
+ provider,
141
+ model,
142
+ costBasis,
143
+ medianAccuracy: percentile(values, 0.5),
144
+ samples: values.length,
145
+ p10: percentile(values, 0.1),
146
+ p50: percentile(values, 0.5),
147
+ p90: percentile(values, 0.9),
148
+ });
149
+ }
150
+ return { outputPriors, calibration, accuracyByBasis };
151
+ }
152
+ export function loadLcrPriorRows(db) {
153
+ const raw = db.queryRequests(`SELECT r.cli, r.model, r.prompt,
154
+ r.input_tokens, r.output_tokens,
155
+ r.cache_read_tokens, r.cache_creation_tokens,
156
+ r.cost_basis, r.owner_principal, r.session_id, r.datetime_utc,
157
+ m.cost_usd, m.route_est_cost_usd
158
+ FROM requests r
159
+ LEFT JOIN gateway_metadata m ON m.request_id = r.id
160
+ ORDER BY r.datetime_utc ASC`);
161
+ const seenMistralSessions = new Set();
162
+ return raw.map(row => {
163
+ let sessionContinued = false;
164
+ if (row.cli === "mistral" && row.session_id) {
165
+ if (seenMistralSessions.has(row.session_id)) {
166
+ sessionContinued = true;
167
+ }
168
+ else {
169
+ seenMistralSessions.add(row.session_id);
170
+ }
171
+ }
172
+ return {
173
+ provider: row.cli,
174
+ model: row.model,
175
+ prompt: row.prompt ?? "",
176
+ inputTokens: row.input_tokens,
177
+ outputTokens: row.output_tokens,
178
+ cacheReadTokens: row.cache_read_tokens,
179
+ cacheCreationTokens: row.cache_creation_tokens,
180
+ costUsd: row.cost_usd,
181
+ costBasis: row.cost_basis,
182
+ routeEstCostUsd: row.route_est_cost_usd,
183
+ ownerPrincipal: row.owner_principal,
184
+ sessionContinued,
185
+ };
186
+ });
187
+ }
188
+ export function computeLcrPriorsFromDb(db, opts) {
189
+ return computeLcrPriors(loadLcrPriorRows(db), opts);
190
+ }
@@ -0,0 +1,20 @@
1
+ import { type JobLimiterSnapshot } from "./async-job-manager.js";
2
+ import { type LcrPriors, type PriorsScope } from "./lcr-priors.js";
3
+ import type { FlightRecorderQuery } from "./flight-recorder.js";
4
+ import type { PerformanceMetrics } from "./metrics.js";
5
+ import type { ApiProviderRuntime, LeastCostConfig } from "./config.js";
6
+ import type { RouterEnv, RouterConfig } from "./least-cost-router.js";
7
+ export interface RouterEnvDeps {
8
+ performanceMetrics: PerformanceMetrics;
9
+ limiterSnapshot: JobLimiterSnapshot;
10
+ apiProviders: readonly ApiProviderRuntime[];
11
+ isAuthed?: (provider: string) => boolean;
12
+ preferCatalogPrice?: boolean;
13
+ flightRecorder?: FlightRecorderQuery;
14
+ priorsScope?: PriorsScope;
15
+ ownerPrincipal?: string;
16
+ priors?: LcrPriors;
17
+ }
18
+ export declare function resetLcrPriorsCacheForTest(): void;
19
+ export declare function buildRouterEnv(deps: RouterEnvDeps): RouterEnv;
20
+ export declare function toRouterConfig(cfg: LeastCostConfig): RouterConfig;
@@ -0,0 +1,133 @@
1
+ import { CLI_TYPES } from "./provider-types.js";
2
+ import { getCliInfo } from "./model-registry.js";
3
+ import { getProviderDefinition } from "./provider-definitions.js";
4
+ import { cliBreakerState } from "./executor.js";
5
+ import { apiProviderBreakerState } from "./api-provider.js";
6
+ import { providerAtCapacity } from "./async-job-manager.js";
7
+ import { getModelCost } from "./pricing.js";
8
+ import { classifyContent } from "./token-estimator.js";
9
+ import { computeLcrPriorsFromDb, lookupCalibrationK, } from "./lcr-priors.js";
10
+ const CLI_TYPE_SET = new Set(CLI_TYPES);
11
+ function isCliType(provider) {
12
+ return CLI_TYPE_SET.has(provider);
13
+ }
14
+ const API_PROVIDER_CAPABILITIES = {
15
+ acceptsImages: false,
16
+ acceptsAttachments: false,
17
+ toolCalling: true,
18
+ jsonSchema: true,
19
+ outputFormats: ["text", "json"],
20
+ capabilityScope: "full",
21
+ effortLevels: [],
22
+ };
23
+ const EMPTY_PRIORS = {
24
+ outputPriors: new Map(),
25
+ calibration: new Map(),
26
+ accuracyByBasis: new Map(),
27
+ };
28
+ const PRIORS_TTL_MS = 60_000;
29
+ let priorsCacheEntry = null;
30
+ function resolvePriors(deps) {
31
+ if (deps.priors)
32
+ return deps.priors;
33
+ const scope = deps.priorsScope ?? "global";
34
+ if (scope === "off" || !deps.flightRecorder)
35
+ return EMPTY_PRIORS;
36
+ const key = `${scope}:${deps.ownerPrincipal ?? ""}`;
37
+ const now = Date.now();
38
+ if (priorsCacheEntry &&
39
+ priorsCacheEntry.key === key &&
40
+ now - priorsCacheEntry.at < PRIORS_TTL_MS) {
41
+ return priorsCacheEntry.priors;
42
+ }
43
+ const priors = computeLcrPriorsFromDb(deps.flightRecorder, {
44
+ priorsScope: scope,
45
+ ownerPrincipal: deps.ownerPrincipal,
46
+ });
47
+ priorsCacheEntry = { at: now, key, priors };
48
+ return priors;
49
+ }
50
+ export function resetLcrPriorsCacheForTest() {
51
+ priorsCacheEntry = null;
52
+ }
53
+ function candidateCapabilities(provider) {
54
+ if (isCliType(provider)) {
55
+ const def = getProviderDefinition(provider);
56
+ return {
57
+ acceptsImages: def.requestSurface.acceptsImages,
58
+ acceptsAttachments: def.requestSurface.acceptsAttachments,
59
+ toolCalling: def.requestSurface.toolCalling,
60
+ jsonSchema: def.requestSurface.jsonSchema,
61
+ outputFormats: def.outputFormats,
62
+ capabilityScope: def.capabilityScope,
63
+ effortLevels: def.discovery.modelDiscovery.facts.effortLevels,
64
+ };
65
+ }
66
+ return API_PROVIDER_CAPABILITIES;
67
+ }
68
+ export function buildRouterEnv(deps) {
69
+ const apiProviderByName = new Map(deps.apiProviders.map(p => [p.name, p]));
70
+ const providerNames = [...CLI_TYPES, ...deps.apiProviders.map(p => p.name)];
71
+ const metricsByTool = deps.performanceMetrics.snapshot().byTool;
72
+ const priors = resolvePriors(deps);
73
+ const isAuthed = deps.isAuthed ??
74
+ ((provider) => {
75
+ return isCliType(provider) || apiProviderByName.has(provider);
76
+ });
77
+ return {
78
+ providers() {
79
+ return providerNames;
80
+ },
81
+ models(provider) {
82
+ if (isCliType(provider)) {
83
+ return Object.keys(getCliInfo()[provider].models);
84
+ }
85
+ const api = apiProviderByName.get(provider);
86
+ if (!api)
87
+ return [];
88
+ return api.models && api.models.length > 0 ? api.models : [api.defaultModel];
89
+ },
90
+ isAuthed,
91
+ breakerState(provider) {
92
+ return isCliType(provider) ? cliBreakerState(provider) : apiProviderBreakerState(provider);
93
+ },
94
+ atCapacity(provider) {
95
+ return providerAtCapacity(deps.limiterSnapshot, provider);
96
+ },
97
+ capabilities(provider) {
98
+ return candidateCapabilities(provider);
99
+ },
100
+ modelCost(provider, model) {
101
+ return getModelCost(provider, model, { preferCatalog: deps.preferCatalogPrice });
102
+ },
103
+ successRate(provider) {
104
+ return metricsByTool[provider]?.successRate ?? 0;
105
+ },
106
+ meanLatencyMs(provider) {
107
+ return metricsByTool[provider]?.averageResponseTimeMs ?? 0;
108
+ },
109
+ calibrationK(prompt, family) {
110
+ return lookupCalibrationK(priors, classifyContent(prompt), family);
111
+ },
112
+ outputPrior(provider, model) {
113
+ const p = priors.outputPriors.get(`${provider}:${model}`);
114
+ return p ? { median: p.median, p90: p.p90 } : null;
115
+ },
116
+ confidenceBand(prompt, family) {
117
+ const bucket = priors.calibration.get(`${classifyContent(prompt)}:${family}`);
118
+ return bucket?.confidence ?? "low";
119
+ },
120
+ };
121
+ }
122
+ export function toRouterConfig(cfg) {
123
+ return {
124
+ minTier: cfg.minTier,
125
+ maxCostUsd: cfg.maxCostUsd,
126
+ defaultExpectedOutputTokens: cfg.defaultExpectedOutputTokens,
127
+ budgetOutputSafetyFactor: cfg.budgetOutputSafetyFactor,
128
+ allowUnpriced: cfg.allowUnpriced,
129
+ tiers: cfg.tiers,
130
+ candidates: cfg.candidates,
131
+ preferenceOrder: cfg.preferenceOrder.length > 0 ? cfg.preferenceOrder : undefined,
132
+ };
133
+ }