llm-cli-gateway 2.15.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 +78 -1
- 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/process-manager.js +2 -1
- package/dist/acp/provider-registry.js +8 -8
- 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 +20 -1
- package/dist/async-job-manager.js +85 -28
- package/dist/claude-mcp-config.js +1 -1
- package/dist/compressor/estimate.d.ts +5 -0
- package/dist/compressor/estimate.js +21 -0
- package/dist/compressor/index.d.ts +23 -0
- package/dist/compressor/index.js +77 -0
- package/dist/compressor/router.d.ts +2 -0
- package/dist/compressor/router.js +57 -0
- package/dist/compressor/transforms/ansi.d.ts +3 -0
- package/dist/compressor/transforms/ansi.js +99 -0
- package/dist/compressor/transforms/json.d.ts +1 -0
- package/dist/compressor/transforms/json.js +156 -0
- package/dist/compressor/transforms/log.d.ts +12 -0
- package/dist/compressor/transforms/log.js +55 -0
- package/dist/compressor/transforms/whitespace.d.ts +8 -0
- package/dist/compressor/transforms/whitespace.js +79 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.js +166 -6
- package/dist/db.js +4 -4
- package/dist/doctor.d.ts +34 -1
- package/dist/doctor.js +85 -3
- package/dist/executor.d.ts +6 -0
- package/dist/executor.js +16 -3
- package/dist/flight-recorder.d.ts +19 -0
- package/dist/flight-recorder.js +110 -2
- package/dist/http-transport.js +19 -21
- package/dist/index.d.ts +58 -2
- package/dist/index.js +929 -94
- package/dist/job-store.d.ts +14 -2
- package/dist/job-store.js +170 -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 +5 -0
- package/dist/provider-definitions.js +56 -10
- package/dist/provider-tool-capabilities.js +3 -3
- package/dist/request-helpers.d.ts +2 -2
- package/dist/request-helpers.js +1 -1
- 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/spawn-env-isolation.d.ts +10 -0
- package/dist/spawn-env-isolation.js +55 -0
- package/dist/token-estimator.d.ts +6 -0
- package/dist/token-estimator.js +59 -0
- package/dist/upstream-contracts.js +48 -28
- 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 +15 -6
- package/setup/status.schema.json +68 -0
package/dist/job-store.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface JobRecord {
|
|
|
10
10
|
cli: string;
|
|
11
11
|
argsJson: string;
|
|
12
12
|
outputFormat?: string | null;
|
|
13
|
+
compressResponse?: boolean | null;
|
|
13
14
|
status: JobStoreStatus;
|
|
14
15
|
exitCode: number | null;
|
|
15
16
|
stdout: string;
|
|
@@ -52,6 +53,7 @@ export interface JobStore {
|
|
|
52
53
|
cli: string;
|
|
53
54
|
args: string[];
|
|
54
55
|
outputFormat?: string;
|
|
56
|
+
compressResponse?: boolean;
|
|
55
57
|
startedAt: string;
|
|
56
58
|
pid: number | null;
|
|
57
59
|
ownerPrincipal?: string | null;
|
|
@@ -173,6 +175,7 @@ export declare class SqliteJobStore implements JobStore, ValidationRunStore {
|
|
|
173
175
|
cli: string;
|
|
174
176
|
args: string[];
|
|
175
177
|
outputFormat?: string;
|
|
178
|
+
compressResponse?: boolean;
|
|
176
179
|
startedAt: string;
|
|
177
180
|
pid: number | null;
|
|
178
181
|
ownerPrincipal?: string | null;
|
|
@@ -236,6 +239,7 @@ export declare class MemoryJobStore implements JobStore {
|
|
|
236
239
|
cli: string;
|
|
237
240
|
args: string[];
|
|
238
241
|
outputFormat?: string;
|
|
242
|
+
compressResponse?: boolean;
|
|
239
243
|
startedAt: string;
|
|
240
244
|
pid: number | null;
|
|
241
245
|
ownerPrincipal?: string | null;
|
|
@@ -276,15 +280,23 @@ export declare class MemoryJobStore implements JobStore {
|
|
|
276
280
|
export declare class PostgresJobStore implements JobStore, ValidationRunStore {
|
|
277
281
|
private logger;
|
|
278
282
|
private worker;
|
|
279
|
-
private
|
|
280
|
-
private
|
|
283
|
+
private retiringWorker;
|
|
284
|
+
private readonly intentionallyRetiredWorkers;
|
|
285
|
+
private workerTerminationPending;
|
|
281
286
|
private closed;
|
|
287
|
+
private readonly workerData;
|
|
282
288
|
constructor(dsn: string, logger?: Logger, options?: {
|
|
283
289
|
retentionMs?: number;
|
|
284
290
|
dedupWindowMs?: number;
|
|
285
291
|
leaseTtlMs?: number;
|
|
286
292
|
});
|
|
293
|
+
private createWorker;
|
|
287
294
|
private syncCall;
|
|
295
|
+
private ensureWorker;
|
|
296
|
+
private callWorker;
|
|
297
|
+
private retireAfterBridgeFailure;
|
|
298
|
+
private retireWorker;
|
|
299
|
+
private markWorkerRetired;
|
|
288
300
|
recordStart(input: {
|
|
289
301
|
id: string;
|
|
290
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)
|
|
@@ -51,6 +53,9 @@ function rowToRecord(row) {
|
|
|
51
53
|
cli: row.cli,
|
|
52
54
|
argsJson: row.args_json,
|
|
53
55
|
outputFormat: row.output_format ?? null,
|
|
56
|
+
compressResponse: row.compress_response === null || row.compress_response === undefined
|
|
57
|
+
? null
|
|
58
|
+
: Boolean(row.compress_response),
|
|
54
59
|
status: row.status,
|
|
55
60
|
exitCode: row.exit_code,
|
|
56
61
|
stdout: row.stdout ?? "",
|
|
@@ -99,6 +104,13 @@ function ensureJobsLeaseColumns(db) {
|
|
|
99
104
|
db.exec("ALTER TABLE jobs ADD COLUMN lease_deadline INTEGER");
|
|
100
105
|
}
|
|
101
106
|
}
|
|
107
|
+
function ensureJobsCompressResponseColumn(db) {
|
|
108
|
+
const cols = db.prepare("PRAGMA table_info(jobs)").all();
|
|
109
|
+
const names = new Set(cols.map(col => col?.name));
|
|
110
|
+
if (!names.has("compress_response")) {
|
|
111
|
+
db.exec("ALTER TABLE jobs ADD COLUMN compress_response INTEGER");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
102
114
|
export function isValidationRunStore(store) {
|
|
103
115
|
return (typeof store === "object" &&
|
|
104
116
|
store !== null &&
|
|
@@ -143,6 +155,7 @@ export class SqliteJobStore {
|
|
|
143
155
|
cli TEXT NOT NULL,
|
|
144
156
|
args_json TEXT NOT NULL,
|
|
145
157
|
output_format TEXT,
|
|
158
|
+
compress_response INTEGER,
|
|
146
159
|
status TEXT NOT NULL,
|
|
147
160
|
exit_code INTEGER,
|
|
148
161
|
stdout TEXT,
|
|
@@ -216,6 +229,7 @@ export class SqliteJobStore {
|
|
|
216
229
|
ensureJobsTransportColumns(this.db);
|
|
217
230
|
ensureJobsLeaseColumns(this.db);
|
|
218
231
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_jobs_owner_status ON jobs(owner_instance, status)");
|
|
232
|
+
ensureJobsCompressResponseColumn(this.db);
|
|
219
233
|
if (process.platform !== "win32") {
|
|
220
234
|
try {
|
|
221
235
|
chmodSync(dbPath, 0o600);
|
|
@@ -228,10 +242,12 @@ export class SqliteJobStore {
|
|
|
228
242
|
this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_INSTANCE_LEASE_TTL_MS;
|
|
229
243
|
this.insertStmt = this.db.prepare(`
|
|
230
244
|
INSERT INTO jobs (id, correlation_id, request_key, cli, args_json, output_format,
|
|
245
|
+
compress_response,
|
|
231
246
|
status, exit_code, stdout, stderr, output_truncated, error,
|
|
232
247
|
started_at, finished_at, pid, expires_at, owner_principal,
|
|
233
248
|
transport, http_status, payload_json, owner_instance, lease_deadline)
|
|
234
249
|
VALUES (@id, @correlation_id, @request_key, @cli, @args_json, @output_format,
|
|
250
|
+
@compress_response,
|
|
235
251
|
'queued', @exit_code, @stdout, @stderr, @output_truncated, @error,
|
|
236
252
|
@started_at, @finished_at, @pid, @expires_at, @owner_principal,
|
|
237
253
|
@transport, @http_status, @payload_json, @owner_instance,
|
|
@@ -332,6 +348,7 @@ export class SqliteJobStore {
|
|
|
332
348
|
cli: input.cli,
|
|
333
349
|
args_json: JSON.stringify(input.args),
|
|
334
350
|
output_format: input.outputFormat ?? null,
|
|
351
|
+
compress_response: input.compressResponse === undefined ? null : input.compressResponse ? 1 : 0,
|
|
335
352
|
exit_code: null,
|
|
336
353
|
stdout: "",
|
|
337
354
|
stderr: "",
|
|
@@ -624,6 +641,7 @@ export class MemoryJobStore {
|
|
|
624
641
|
cli: input.cli,
|
|
625
642
|
argsJson: JSON.stringify(input.args),
|
|
626
643
|
outputFormat: input.outputFormat ?? null,
|
|
644
|
+
compressResponse: input.compressResponse ?? null,
|
|
627
645
|
status: "queued",
|
|
628
646
|
exitCode: null,
|
|
629
647
|
stdout: "",
|
|
@@ -741,66 +759,153 @@ export class MemoryJobStore {
|
|
|
741
759
|
}
|
|
742
760
|
export class PostgresJobStore {
|
|
743
761
|
logger;
|
|
744
|
-
worker;
|
|
745
|
-
|
|
746
|
-
|
|
762
|
+
worker = null;
|
|
763
|
+
retiringWorker = null;
|
|
764
|
+
intentionallyRetiredWorkers = new WeakSet();
|
|
765
|
+
workerTerminationPending = false;
|
|
747
766
|
closed = false;
|
|
767
|
+
workerData;
|
|
748
768
|
constructor(dsn, logger = noopLogger, options = {}) {
|
|
749
769
|
this.logger = logger;
|
|
750
770
|
if (!dsn) {
|
|
751
771
|
throw new Error("PostgresJobStore requires a non-empty DSN");
|
|
752
772
|
}
|
|
753
|
-
this.
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
farFutureIso: FAR_FUTURE_ISO,
|
|
762
|
-
connectionTimeoutMillis: 5000,
|
|
763
|
-
},
|
|
764
|
-
});
|
|
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
|
+
};
|
|
765
781
|
try {
|
|
766
|
-
this.
|
|
782
|
+
this.ensureWorker();
|
|
767
783
|
}
|
|
768
784
|
catch (err) {
|
|
769
785
|
this.closed = true;
|
|
770
|
-
|
|
771
|
-
rmSync(this.tmpDir, { recursive: true, force: true });
|
|
786
|
+
this.retireWorker(this.worker);
|
|
772
787
|
throw err;
|
|
773
788
|
}
|
|
774
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
|
+
}
|
|
775
820
|
syncCall(method, ...args) {
|
|
776
|
-
if (this.closed
|
|
821
|
+
if (this.closed) {
|
|
777
822
|
throw new Error("PostgresJobStore is closed");
|
|
778
823
|
}
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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");
|
|
834
|
+
}
|
|
835
|
+
const worker = this.createWorker();
|
|
836
|
+
this.worker = worker;
|
|
837
|
+
try {
|
|
838
|
+
this.callWorker(worker, "init", []);
|
|
839
|
+
return worker;
|
|
786
840
|
}
|
|
787
|
-
|
|
788
|
-
|
|
841
|
+
catch (err) {
|
|
842
|
+
this.retireWorker(worker);
|
|
843
|
+
throw err;
|
|
789
844
|
}
|
|
790
|
-
|
|
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();
|
|
791
850
|
try {
|
|
792
|
-
|
|
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;
|
|
793
882
|
}
|
|
794
883
|
finally {
|
|
795
|
-
|
|
884
|
+
responsePort.close();
|
|
796
885
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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;
|
|
804
909
|
}
|
|
805
910
|
recordStart(input) {
|
|
806
911
|
this.syncCall("recordStart", input);
|
|
@@ -883,19 +988,41 @@ export class PostgresJobStore {
|
|
|
883
988
|
close() {
|
|
884
989
|
if (this.closed)
|
|
885
990
|
return;
|
|
991
|
+
const worker = this.worker;
|
|
886
992
|
try {
|
|
887
|
-
|
|
993
|
+
if (worker)
|
|
994
|
+
this.callWorker(worker, "close", []);
|
|
888
995
|
}
|
|
889
996
|
catch (err) {
|
|
890
997
|
this.logger.error("PostgresJobStore close failed", err);
|
|
891
998
|
}
|
|
892
999
|
finally {
|
|
893
1000
|
this.closed = true;
|
|
894
|
-
|
|
895
|
-
rmSync(this.tmpDir, { recursive: true, force: true });
|
|
1001
|
+
this.retireWorker(worker);
|
|
896
1002
|
}
|
|
897
1003
|
}
|
|
898
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
|
+
}
|
|
899
1026
|
function resolvePostgresWorkerUrl() {
|
|
900
1027
|
const sibling = new URL("./postgres-job-store-worker.js", import.meta.url);
|
|
901
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;
|