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.
- package/.agents/skills/least-cost-routing/SKILL.md +123 -0
- package/CHANGELOG.md +28 -0
- package/dist/acp/client.js +5 -0
- package/dist/acp/flight-redaction.d.ts +2 -0
- package/dist/acp/flight-redaction.js +2 -0
- package/dist/acp/runtime.d.ts +1 -0
- package/dist/acp/runtime.js +20 -0
- package/dist/acp/types.d.ts +52 -0
- package/dist/acp/types.js +8 -0
- package/dist/api-provider.d.ts +1 -0
- package/dist/api-provider.js +1 -0
- package/dist/async-job-manager.d.ts +16 -0
- package/dist/async-job-manager.js +70 -22
- package/dist/claude-mcp-config.js +1 -1
- package/dist/compressor/transforms/ansi.js +12 -2
- package/dist/config.d.ts +31 -0
- package/dist/config.js +142 -7
- package/dist/db.js +4 -4
- package/dist/doctor.d.ts +34 -1
- package/dist/doctor.js +85 -3
- package/dist/executor.d.ts +5 -0
- package/dist/executor.js +11 -1
- package/dist/flight-recorder.d.ts +10 -0
- package/dist/flight-recorder.js +68 -2
- package/dist/http-transport.js +19 -21
- package/dist/index.d.ts +41 -1
- package/dist/index.js +658 -30
- package/dist/job-store.d.ts +10 -2
- package/dist/job-store.js +154 -43
- package/dist/lcr-priors.d.ts +60 -0
- package/dist/lcr-priors.js +190 -0
- package/dist/lcr-router-env.d.ts +20 -0
- package/dist/lcr-router-env.js +133 -0
- package/dist/lcr-telemetry.d.ts +2 -0
- package/dist/lcr-telemetry.js +17 -0
- package/dist/least-cost-router.d.ts +86 -0
- package/dist/least-cost-router.js +296 -0
- package/dist/least-cost-types.d.ts +34 -0
- package/dist/least-cost-types.js +1 -0
- package/dist/migrate-sessions.js +1 -1
- package/dist/migrate.js +1 -1
- package/dist/model-registry.js +1 -1
- package/dist/postgres-job-store-worker.js +56 -13
- package/dist/pricing.d.ts +17 -0
- package/dist/pricing.js +167 -0
- package/dist/provider-definitions.d.ts +4 -0
- package/dist/provider-definitions.js +28 -0
- package/dist/request-helpers.d.ts +2 -2
- package/dist/resources.d.ts +37 -2
- package/dist/resources.js +96 -1
- package/dist/retry.d.ts +1 -0
- package/dist/retry.js +1 -1
- package/dist/token-estimator.d.ts +6 -0
- package/dist/token-estimator.js +59 -0
- package/dist/upstream-contracts.js +1 -1
- package/dist/validation-receipt.js +1 -1
- package/dist/validation-tools.d.ts +6 -0
- package/dist/validation-tools.js +174 -54
- package/npm-shrinkwrap.json +2 -2
- package/package.json +9 -4
- package/setup/status.schema.json +68 -0
package/dist/job-store.d.ts
CHANGED
|
@@ -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
|
|
284
|
-
private
|
|
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
|
|
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
|
-
|
|
762
|
-
|
|
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.
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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.
|
|
782
|
+
this.ensureWorker();
|
|
783
783
|
}
|
|
784
784
|
catch (err) {
|
|
785
785
|
this.closed = true;
|
|
786
|
-
|
|
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
|
|
821
|
+
if (this.closed) {
|
|
793
822
|
throw new Error("PostgresJobStore is closed");
|
|
794
823
|
}
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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
|
-
|
|
804
|
-
|
|
835
|
+
const worker = this.createWorker();
|
|
836
|
+
this.worker = worker;
|
|
837
|
+
try {
|
|
838
|
+
this.callWorker(worker, "init", []);
|
|
839
|
+
return worker;
|
|
805
840
|
}
|
|
806
|
-
|
|
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
|
-
|
|
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
|
-
|
|
884
|
+
responsePort.close();
|
|
812
885
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|