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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const WS = new Set([" ", "\t", "\n", "\r"]);
|
|
2
|
+
function skipWs(c) {
|
|
3
|
+
while (c.pos < c.text.length && WS.has(c.text[c.pos]))
|
|
4
|
+
c.pos += 1;
|
|
5
|
+
}
|
|
6
|
+
function fail() {
|
|
7
|
+
throw new SyntaxError("json-minify: not valid JSON");
|
|
8
|
+
}
|
|
9
|
+
const SIMPLE_ESCAPES = new Set(['"', "\\", "/", "b", "f", "n", "r", "t"]);
|
|
10
|
+
const HEX = /^[0-9a-fA-F]{4}$/;
|
|
11
|
+
function copyString(c) {
|
|
12
|
+
const start = c.pos;
|
|
13
|
+
if (c.text[c.pos] !== '"')
|
|
14
|
+
fail();
|
|
15
|
+
c.pos += 1;
|
|
16
|
+
while (c.pos < c.text.length) {
|
|
17
|
+
const ch = c.text[c.pos];
|
|
18
|
+
if (ch === "\\") {
|
|
19
|
+
const esc = c.text[c.pos + 1];
|
|
20
|
+
if (esc === undefined)
|
|
21
|
+
fail();
|
|
22
|
+
if (esc === "u") {
|
|
23
|
+
if (!HEX.test(c.text.slice(c.pos + 2, c.pos + 6)))
|
|
24
|
+
fail();
|
|
25
|
+
c.pos += 6;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!SIMPLE_ESCAPES.has(esc))
|
|
29
|
+
fail();
|
|
30
|
+
c.pos += 2;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (ch === '"') {
|
|
34
|
+
c.pos += 1;
|
|
35
|
+
c.out.push(c.text.slice(start, c.pos));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (ch.charCodeAt(0) < 0x20)
|
|
39
|
+
fail();
|
|
40
|
+
c.pos += 1;
|
|
41
|
+
}
|
|
42
|
+
fail();
|
|
43
|
+
}
|
|
44
|
+
const NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/;
|
|
45
|
+
function copyNumber(c) {
|
|
46
|
+
const match = NUMBER.exec(c.text.slice(c.pos));
|
|
47
|
+
if (!match || match[0].length === 0)
|
|
48
|
+
fail();
|
|
49
|
+
c.out.push(match[0]);
|
|
50
|
+
c.pos += match[0].length;
|
|
51
|
+
}
|
|
52
|
+
function copyLiteral(c, word) {
|
|
53
|
+
if (c.text.startsWith(word, c.pos)) {
|
|
54
|
+
c.out.push(word);
|
|
55
|
+
c.pos += word.length;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
fail();
|
|
59
|
+
}
|
|
60
|
+
function copyValue(c) {
|
|
61
|
+
skipWs(c);
|
|
62
|
+
const ch = c.text[c.pos];
|
|
63
|
+
if (ch === undefined)
|
|
64
|
+
fail();
|
|
65
|
+
if (ch === "{") {
|
|
66
|
+
c.out.push("{");
|
|
67
|
+
c.pos += 1;
|
|
68
|
+
skipWs(c);
|
|
69
|
+
if (c.text[c.pos] === "}") {
|
|
70
|
+
c.out.push("}");
|
|
71
|
+
c.pos += 1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (;;) {
|
|
75
|
+
skipWs(c);
|
|
76
|
+
copyString(c);
|
|
77
|
+
skipWs(c);
|
|
78
|
+
if (c.text[c.pos] !== ":")
|
|
79
|
+
fail();
|
|
80
|
+
c.out.push(":");
|
|
81
|
+
c.pos += 1;
|
|
82
|
+
copyValue(c);
|
|
83
|
+
skipWs(c);
|
|
84
|
+
if (c.text[c.pos] === ",") {
|
|
85
|
+
c.out.push(",");
|
|
86
|
+
c.pos += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (c.text[c.pos] === "}") {
|
|
90
|
+
c.out.push("}");
|
|
91
|
+
c.pos += 1;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
fail();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (ch === "[") {
|
|
98
|
+
c.out.push("[");
|
|
99
|
+
c.pos += 1;
|
|
100
|
+
skipWs(c);
|
|
101
|
+
if (c.text[c.pos] === "]") {
|
|
102
|
+
c.out.push("]");
|
|
103
|
+
c.pos += 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
for (;;) {
|
|
107
|
+
copyValue(c);
|
|
108
|
+
skipWs(c);
|
|
109
|
+
if (c.text[c.pos] === ",") {
|
|
110
|
+
c.out.push(",");
|
|
111
|
+
c.pos += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (c.text[c.pos] === "]") {
|
|
115
|
+
c.out.push("]");
|
|
116
|
+
c.pos += 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
fail();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (ch === '"') {
|
|
123
|
+
copyString(c);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (ch === "t") {
|
|
127
|
+
copyLiteral(c, "true");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (ch === "f") {
|
|
131
|
+
copyLiteral(c, "false");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (ch === "n") {
|
|
135
|
+
copyLiteral(c, "null");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (ch === "-" || (ch >= "0" && ch <= "9")) {
|
|
139
|
+
copyNumber(c);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
fail();
|
|
143
|
+
}
|
|
144
|
+
export function minifyJson(text) {
|
|
145
|
+
const c = { text, pos: 0, out: [] };
|
|
146
|
+
try {
|
|
147
|
+
copyValue(c);
|
|
148
|
+
skipWs(c);
|
|
149
|
+
if (c.pos !== text.length)
|
|
150
|
+
return null;
|
|
151
|
+
return c.out.join("");
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const SENTINEL_PREFIX = "[[gateway-";
|
|
2
|
+
export declare const LIT_MARKER = "[[gateway-lit:v1]] ";
|
|
3
|
+
export declare function repeatMarker(lines: number, count: number): string;
|
|
4
|
+
export declare function crMarker(frames: number): string;
|
|
5
|
+
export declare function noteLine(folded: number, escaped: number): string;
|
|
6
|
+
export interface MarkerCounts {
|
|
7
|
+
folded: number;
|
|
8
|
+
escaped: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function escapeSentinelLikeLines(text: string, counts: MarkerCounts): string;
|
|
11
|
+
export declare const MIN_RUN = 3;
|
|
12
|
+
export declare function dedupAdjacentLines(text: string, counts: MarkerCounts): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mapUnfenced } from "./whitespace.js";
|
|
2
|
+
export const SENTINEL_PREFIX = "[[gateway-";
|
|
3
|
+
export const LIT_MARKER = "[[gateway-lit:v1]] ";
|
|
4
|
+
export function repeatMarker(lines, count) {
|
|
5
|
+
return `[[gateway-repeat:v1 lines=${lines} count=${count}]]`;
|
|
6
|
+
}
|
|
7
|
+
export function crMarker(frames) {
|
|
8
|
+
return `[[gateway-cr:v1 frames=${frames}]]`;
|
|
9
|
+
}
|
|
10
|
+
export function noteLine(folded, escaped) {
|
|
11
|
+
return (`[[gateway-note:v1 folded=${folded} escaped=${escaped}]] ` +
|
|
12
|
+
"Gateway compressor markers follow: [[gateway-repeat:v1 ...]] folds byte-identical repeated lines (count included), " +
|
|
13
|
+
"[[gateway-cr:v1 ...]] keeps the final frame of carriage-return-rewritten lines, " +
|
|
14
|
+
"and lines opening with [[gateway-lit:v1]] are verbatim input with that prefix added.");
|
|
15
|
+
}
|
|
16
|
+
export function escapeSentinelLikeLines(text, counts) {
|
|
17
|
+
return mapUnfenced(text, segment => segment
|
|
18
|
+
.split("\n")
|
|
19
|
+
.map(line => {
|
|
20
|
+
if (line.trimStart().startsWith(SENTINEL_PREFIX)) {
|
|
21
|
+
counts.escaped += 1;
|
|
22
|
+
return LIT_MARKER + line;
|
|
23
|
+
}
|
|
24
|
+
return line;
|
|
25
|
+
})
|
|
26
|
+
.join("\n"));
|
|
27
|
+
}
|
|
28
|
+
export const MIN_RUN = 3;
|
|
29
|
+
export function dedupAdjacentLines(text, counts) {
|
|
30
|
+
return mapUnfenced(text, segment => {
|
|
31
|
+
const lines = segment.split("\n");
|
|
32
|
+
const out = [];
|
|
33
|
+
let i = 0;
|
|
34
|
+
while (i < lines.length) {
|
|
35
|
+
const line = lines[i];
|
|
36
|
+
let runEnd = i + 1;
|
|
37
|
+
if (line.trim() !== "") {
|
|
38
|
+
while (runEnd < lines.length && lines[runEnd] === line)
|
|
39
|
+
runEnd += 1;
|
|
40
|
+
}
|
|
41
|
+
const runLength = runEnd - i;
|
|
42
|
+
if (runLength >= MIN_RUN) {
|
|
43
|
+
out.push(line);
|
|
44
|
+
out.push(repeatMarker(1, runLength));
|
|
45
|
+
counts.folded += 1;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
for (let j = i; j < runEnd; j += 1)
|
|
49
|
+
out.push(lines[j]);
|
|
50
|
+
}
|
|
51
|
+
i = runEnd;
|
|
52
|
+
}
|
|
53
|
+
return out.join("\n");
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface FenceSegment {
|
|
2
|
+
text: string;
|
|
3
|
+
fenced: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function splitFences(text: string): FenceSegment[];
|
|
6
|
+
export declare function joinFences(segments: FenceSegment[]): string;
|
|
7
|
+
export declare function mapUnfenced(text: string, op: (segment: string) => string): string;
|
|
8
|
+
export declare function normalizeWhitespace(text: string): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
|
|
2
|
+
export function splitFences(text) {
|
|
3
|
+
const segments = [];
|
|
4
|
+
const lines = text.split("\n");
|
|
5
|
+
let current = [];
|
|
6
|
+
let fenced = false;
|
|
7
|
+
let fenceMarker = "";
|
|
8
|
+
const flush = (nextFenced) => {
|
|
9
|
+
if (current.length > 0) {
|
|
10
|
+
segments.push({ text: current.join("\n"), fenced });
|
|
11
|
+
current = [];
|
|
12
|
+
}
|
|
13
|
+
fenced = nextFenced;
|
|
14
|
+
};
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
if (!fenced) {
|
|
17
|
+
const open = FENCE_OPEN.exec(line);
|
|
18
|
+
if (open) {
|
|
19
|
+
flush(true);
|
|
20
|
+
fenceMarker = open[1];
|
|
21
|
+
current.push(line);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
current.push(line);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
current.push(line);
|
|
28
|
+
const marker = FENCE_OPEN.exec(line);
|
|
29
|
+
if (marker &&
|
|
30
|
+
marker[1][0] === fenceMarker[0] &&
|
|
31
|
+
marker[1].length >= fenceMarker.length &&
|
|
32
|
+
line.trim() === marker[1]) {
|
|
33
|
+
flush(false);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
flush(false);
|
|
38
|
+
return segments;
|
|
39
|
+
}
|
|
40
|
+
export function joinFences(segments) {
|
|
41
|
+
return segments.map(s => s.text).join("\n");
|
|
42
|
+
}
|
|
43
|
+
export function mapUnfenced(text, op) {
|
|
44
|
+
const segments = splitFences(text);
|
|
45
|
+
return joinFences(segments.map(s => (s.fenced ? s : { ...s, text: op(s.text) })));
|
|
46
|
+
}
|
|
47
|
+
export function normalizeWhitespace(text) {
|
|
48
|
+
return mapUnfenced(text, segment => {
|
|
49
|
+
const lines = segment.split("\n").map(line => {
|
|
50
|
+
if (line.includes("`"))
|
|
51
|
+
return line;
|
|
52
|
+
const crlf = line.endsWith("\r");
|
|
53
|
+
const body = crlf ? line.slice(0, -1) : line;
|
|
54
|
+
const stripped = body.replace(/[ \t]+$/, "");
|
|
55
|
+
return crlf ? `${stripped}\r` : stripped;
|
|
56
|
+
});
|
|
57
|
+
const out = [];
|
|
58
|
+
let blankRun = 0;
|
|
59
|
+
const collapse = () => {
|
|
60
|
+
if (blankRun < 3)
|
|
61
|
+
return;
|
|
62
|
+
const keep = out[out.length - blankRun];
|
|
63
|
+
out.splice(out.length - blankRun, blankRun, keep);
|
|
64
|
+
};
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
const isBlank = line === "" || line === "\r";
|
|
67
|
+
if (isBlank) {
|
|
68
|
+
blankRun += 1;
|
|
69
|
+
out.push(line);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
collapse();
|
|
73
|
+
blankRun = 0;
|
|
74
|
+
out.push(line);
|
|
75
|
+
}
|
|
76
|
+
collapse();
|
|
77
|
+
return out.join("\n");
|
|
78
|
+
});
|
|
79
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Logger } from "./logger.js";
|
|
2
2
|
import type { RemoteOAuthConfig } from "./auth.js";
|
|
3
3
|
import type { ApiProviderKind } from "./api-provider.js";
|
|
4
|
+
import type { QualityTier } from "./least-cost-types.js";
|
|
4
5
|
export interface DatabaseConfig {
|
|
5
6
|
connectionString: string;
|
|
6
7
|
pool: {
|
|
@@ -111,6 +112,44 @@ export interface CacheAwarenessConfig {
|
|
|
111
112
|
};
|
|
112
113
|
}
|
|
113
114
|
export declare function loadCacheAwarenessConfig(logger?: Logger): CacheAwarenessConfig;
|
|
115
|
+
export interface CompressionConfig {
|
|
116
|
+
enabled: boolean;
|
|
117
|
+
sources: {
|
|
118
|
+
configFile: string | null;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export declare const DEFAULT_COMPRESSION_CONFIG: CompressionConfig;
|
|
122
|
+
export declare function loadCompressionConfig(logger?: Logger): CompressionConfig;
|
|
123
|
+
export declare const LEAST_COST_PRIORS_SCOPES: readonly ["global", "principal", "off"];
|
|
124
|
+
export type LeastCostPriorsScope = (typeof LEAST_COST_PRIORS_SCOPES)[number];
|
|
125
|
+
export declare const DEFAULT_LEAST_COST_MAX_COST_USD = 0.5;
|
|
126
|
+
export declare const DEFAULT_LEAST_COST_EXPECTED_OUTPUT_TOKENS = 800;
|
|
127
|
+
export declare const DEFAULT_LEAST_COST_BUDGET_SAFETY_FACTOR = 1.5;
|
|
128
|
+
export declare const DEFAULT_LEAST_COST_MAX_REROUTES = 2;
|
|
129
|
+
export declare const DEFAULT_LEAST_COST_TIERS: Readonly<Record<string, QualityTier>>;
|
|
130
|
+
export interface LeastCostConfig {
|
|
131
|
+
enabled: boolean;
|
|
132
|
+
minTier: QualityTier;
|
|
133
|
+
maxCostUsd: number;
|
|
134
|
+
defaultExpectedOutputTokens: number;
|
|
135
|
+
budgetOutputSafetyFactor: number;
|
|
136
|
+
priorsScope: LeastCostPriorsScope;
|
|
137
|
+
allowUnpriced: boolean;
|
|
138
|
+
maxReroutes: number;
|
|
139
|
+
preferCatalogPrice: boolean;
|
|
140
|
+
preferenceOrder: string[];
|
|
141
|
+
tiers: Record<string, QualityTier>;
|
|
142
|
+
candidates: {
|
|
143
|
+
allow: string[];
|
|
144
|
+
deny: string[];
|
|
145
|
+
};
|
|
146
|
+
sources: {
|
|
147
|
+
configFile: string | null;
|
|
148
|
+
envOverrides: string[];
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export declare function defaultLeastCostConfig(sourcePath?: string | null): LeastCostConfig;
|
|
152
|
+
export declare function loadLeastCostConfig(logger?: Logger): LeastCostConfig;
|
|
114
153
|
export declare function minStableTokensForModel(config: CacheAwarenessConfig, modelName: string): number;
|
|
115
154
|
export declare const DEFAULT_XAI_API_KEY_ENV = "XAI_API_KEY";
|
|
116
155
|
export declare const DEFAULT_XAI_BASE_URL = "https://api.x.ai/v1";
|
package/dist/config.js
CHANGED
|
@@ -33,7 +33,7 @@ export function loadConfig() {
|
|
|
33
33
|
DatabaseUrlSchema.parse(databaseUrl);
|
|
34
34
|
}
|
|
35
35
|
catch (error) {
|
|
36
|
-
throw new Error(`Invalid database URL: ${error instanceof Error ? error.message : String(error)}
|
|
36
|
+
throw new Error(`Invalid database URL: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
37
37
|
}
|
|
38
38
|
return {
|
|
39
39
|
database: {
|
|
@@ -87,6 +87,13 @@ const PersistenceSchema = z
|
|
|
87
87
|
message: `httpJobGraceMs (${cfg.httpJobGraceMs}) must be >= instanceLeaseTtlMs (${cfg.instanceLeaseTtlMs})`,
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
|
+
if (cfg.instanceGcMs < cfg.instanceLeaseTtlMs) {
|
|
91
|
+
ctx.addIssue({
|
|
92
|
+
code: z.ZodIssueCode.custom,
|
|
93
|
+
path: ["instanceGcMs"],
|
|
94
|
+
message: `instanceGcMs (${cfg.instanceGcMs}) must be >= instanceLeaseTtlMs (${cfg.instanceLeaseTtlMs})`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
90
97
|
});
|
|
91
98
|
const DEFAULT_SQLITE_PATH = path.join(os.homedir(), ".llm-cli-gateway", "logs.db");
|
|
92
99
|
function defaultPersistenceConfigPath() {
|
|
@@ -108,7 +115,9 @@ export function loadSkillsConfig(logger = noopLogger) {
|
|
|
108
115
|
skillsParsed = SkillsSchema.parse(rawSkills);
|
|
109
116
|
}
|
|
110
117
|
catch (err) {
|
|
111
|
-
throw new Error(`Invalid [skills] config: ${err instanceof Error ? err.message : String(err)}
|
|
118
|
+
throw new Error(`Invalid [skills] config: ${err instanceof Error ? err.message : String(err)}`, {
|
|
119
|
+
cause: err,
|
|
120
|
+
});
|
|
112
121
|
}
|
|
113
122
|
const paths = [...skillsParsed.paths];
|
|
114
123
|
const envPaths = process.env.LLM_GATEWAY_SKILLS_PATH;
|
|
@@ -220,7 +229,7 @@ export function loadPersistenceConfig(logger = noopLogger) {
|
|
|
220
229
|
parsed = PersistenceSchema.parse(merged);
|
|
221
230
|
}
|
|
222
231
|
catch (err) {
|
|
223
|
-
throw new Error(`Invalid [persistence] config: ${err instanceof Error ? err.message : String(err)}
|
|
232
|
+
throw new Error(`Invalid [persistence] config: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
224
233
|
}
|
|
225
234
|
const backend = parsed.backend;
|
|
226
235
|
const resolvedPath = backend === "sqlite" ? expandHome(parsed.path ?? DEFAULT_SQLITE_PATH) : null;
|
|
@@ -315,14 +324,16 @@ export function loadLimitsConfig(logger = noopLogger) {
|
|
|
315
324
|
httpParsed = HttpSessionLimitsSchema.parse(rawHttp);
|
|
316
325
|
}
|
|
317
326
|
catch (err) {
|
|
318
|
-
throw new Error(`Invalid [http] session-limit config: ${err instanceof Error ? err.message : String(err)}
|
|
327
|
+
throw new Error(`Invalid [http] session-limit config: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
319
328
|
}
|
|
320
329
|
let limitsParsed;
|
|
321
330
|
try {
|
|
322
331
|
limitsParsed = JobLimitsSchema.parse(rawLimits);
|
|
323
332
|
}
|
|
324
333
|
catch (err) {
|
|
325
|
-
throw new Error(`Invalid [limits] config: ${err instanceof Error ? err.message : String(err)}
|
|
334
|
+
throw new Error(`Invalid [limits] config: ${err instanceof Error ? err.message : String(err)}`, {
|
|
335
|
+
cause: err,
|
|
336
|
+
});
|
|
326
337
|
}
|
|
327
338
|
return {
|
|
328
339
|
http: {
|
|
@@ -398,7 +409,7 @@ export function loadCacheAwarenessConfig(logger = noopLogger) {
|
|
|
398
409
|
parsed = CacheAwarenessSchema.parse(raw ?? {});
|
|
399
410
|
}
|
|
400
411
|
catch (err) {
|
|
401
|
-
throw new Error(`Invalid [cache_awareness] config: ${err instanceof Error ? err.message : String(err)}
|
|
412
|
+
throw new Error(`Invalid [cache_awareness] config: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
402
413
|
}
|
|
403
414
|
return {
|
|
404
415
|
emitAnthropicCacheControl: parsed.emit_anthropic_cache_control,
|
|
@@ -413,6 +424,155 @@ export function loadCacheAwarenessConfig(logger = noopLogger) {
|
|
|
413
424
|
sources: { configFile: sourcePath },
|
|
414
425
|
};
|
|
415
426
|
}
|
|
427
|
+
const CompressionSchema = z
|
|
428
|
+
.object({
|
|
429
|
+
enabled: z.boolean().default(false),
|
|
430
|
+
})
|
|
431
|
+
.strict();
|
|
432
|
+
export const DEFAULT_COMPRESSION_CONFIG = {
|
|
433
|
+
enabled: false,
|
|
434
|
+
sources: { configFile: null },
|
|
435
|
+
};
|
|
436
|
+
export function loadCompressionConfig(logger = noopLogger) {
|
|
437
|
+
const configPath = defaultGatewayConfigPath();
|
|
438
|
+
const { parsed, sourcePath } = readGatewayTomlFile(configPath, logger, "compression");
|
|
439
|
+
const raw = parsed?.compression ?? {};
|
|
440
|
+
let parsedCompression;
|
|
441
|
+
try {
|
|
442
|
+
parsedCompression = CompressionSchema.parse(raw);
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
throw new Error(`Invalid [compression] config: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
enabled: parsedCompression.enabled,
|
|
449
|
+
sources: { configFile: sourcePath },
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
export const LEAST_COST_PRIORS_SCOPES = ["global", "principal", "off"];
|
|
453
|
+
export const DEFAULT_LEAST_COST_MAX_COST_USD = 0.5;
|
|
454
|
+
export const DEFAULT_LEAST_COST_EXPECTED_OUTPUT_TOKENS = 800;
|
|
455
|
+
export const DEFAULT_LEAST_COST_BUDGET_SAFETY_FACTOR = 1.5;
|
|
456
|
+
export const DEFAULT_LEAST_COST_MAX_REROUTES = 2;
|
|
457
|
+
export const DEFAULT_LEAST_COST_TIERS = {
|
|
458
|
+
"claude:claude-haiku": "economy",
|
|
459
|
+
"claude:claude-sonnet": "standard",
|
|
460
|
+
"claude:claude-opus": "frontier",
|
|
461
|
+
"codex:openai-gpt5": "standard",
|
|
462
|
+
"gemini:gemini-2.5-flash": "economy",
|
|
463
|
+
"gemini:gemini-2.5-pro": "standard",
|
|
464
|
+
"gemini:gemini-3-pro": "frontier",
|
|
465
|
+
"grok:grok-build": "economy",
|
|
466
|
+
"grok:grok-4": "standard",
|
|
467
|
+
"mistral:mistral-devstral": "economy",
|
|
468
|
+
"mistral:mistral-medium": "standard",
|
|
469
|
+
};
|
|
470
|
+
const QualityTierSchema = z.enum(["economy", "standard", "frontier"]);
|
|
471
|
+
const LeastCostCandidatesSchema = z
|
|
472
|
+
.object({
|
|
473
|
+
allow: z.array(z.string().min(1)).default([]),
|
|
474
|
+
deny: z.array(z.string().min(1)).default([]),
|
|
475
|
+
})
|
|
476
|
+
.strict()
|
|
477
|
+
.default({ allow: [], deny: [] });
|
|
478
|
+
const LeastCostSchema = z
|
|
479
|
+
.object({
|
|
480
|
+
enabled: z.boolean().default(false),
|
|
481
|
+
min_tier: QualityTierSchema.default("standard"),
|
|
482
|
+
max_cost_usd: z.number().positive().default(DEFAULT_LEAST_COST_MAX_COST_USD),
|
|
483
|
+
default_expected_output_tokens: z
|
|
484
|
+
.number()
|
|
485
|
+
.int()
|
|
486
|
+
.positive()
|
|
487
|
+
.default(DEFAULT_LEAST_COST_EXPECTED_OUTPUT_TOKENS),
|
|
488
|
+
budget_output_safety_factor: z
|
|
489
|
+
.number()
|
|
490
|
+
.positive()
|
|
491
|
+
.default(DEFAULT_LEAST_COST_BUDGET_SAFETY_FACTOR),
|
|
492
|
+
priors_scope: z.enum(LEAST_COST_PRIORS_SCOPES).default("global"),
|
|
493
|
+
allow_unpriced: z.boolean().default(false),
|
|
494
|
+
max_reroutes: z.number().int().nonnegative().default(DEFAULT_LEAST_COST_MAX_REROUTES),
|
|
495
|
+
prefer_catalog_price: z.boolean().default(true),
|
|
496
|
+
preference_order: z.array(z.string().min(1)).default([]),
|
|
497
|
+
tiers: z.record(z.string(), QualityTierSchema).default({}),
|
|
498
|
+
candidates: LeastCostCandidatesSchema,
|
|
499
|
+
})
|
|
500
|
+
.strict();
|
|
501
|
+
export function defaultLeastCostConfig(sourcePath = null) {
|
|
502
|
+
return {
|
|
503
|
+
enabled: false,
|
|
504
|
+
minTier: "standard",
|
|
505
|
+
maxCostUsd: DEFAULT_LEAST_COST_MAX_COST_USD,
|
|
506
|
+
defaultExpectedOutputTokens: DEFAULT_LEAST_COST_EXPECTED_OUTPUT_TOKENS,
|
|
507
|
+
budgetOutputSafetyFactor: DEFAULT_LEAST_COST_BUDGET_SAFETY_FACTOR,
|
|
508
|
+
priorsScope: "global",
|
|
509
|
+
allowUnpriced: false,
|
|
510
|
+
maxReroutes: DEFAULT_LEAST_COST_MAX_REROUTES,
|
|
511
|
+
preferCatalogPrice: true,
|
|
512
|
+
preferenceOrder: [],
|
|
513
|
+
tiers: { ...DEFAULT_LEAST_COST_TIERS },
|
|
514
|
+
candidates: { allow: [], deny: [] },
|
|
515
|
+
sources: { configFile: sourcePath, envOverrides: [] },
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function readLeastCostFile(configPath, logger) {
|
|
519
|
+
if (!existsSync(configPath)) {
|
|
520
|
+
return { raw: undefined, sourcePath: null };
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const require = createRequire(import.meta.url);
|
|
524
|
+
const TOML = require("smol-toml");
|
|
525
|
+
const text = readFileSync(configPath, "utf-8");
|
|
526
|
+
const parsed = TOML.parse(text);
|
|
527
|
+
return { raw: parsed?.least_cost, sourcePath: configPath };
|
|
528
|
+
}
|
|
529
|
+
catch (err) {
|
|
530
|
+
logger.error(`Failed to parse gateway config at ${configPath}; using least_cost defaults`, err);
|
|
531
|
+
return { raw: undefined, sourcePath: null };
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
export function loadLeastCostConfig(logger = noopLogger) {
|
|
535
|
+
const configPath = defaultGatewayConfigPath();
|
|
536
|
+
const { raw, sourcePath } = readLeastCostFile(configPath, logger);
|
|
537
|
+
const envOverrides = [];
|
|
538
|
+
const envSentinel = process.env.LLM_GATEWAY_LEAST_COST;
|
|
539
|
+
if (envSentinel !== undefined && envSentinel.length > 0) {
|
|
540
|
+
envOverrides.push("LLM_GATEWAY_LEAST_COST");
|
|
541
|
+
logWarn(logger, "LLM_GATEWAY_LEAST_COST is not supported and is ignored; configure [least_cost] in " +
|
|
542
|
+
"~/.llm-cli-gateway/config.toml (routing cannot be enabled via an environment variable).");
|
|
543
|
+
}
|
|
544
|
+
if (raw === undefined) {
|
|
545
|
+
const cfg = defaultLeastCostConfig(sourcePath);
|
|
546
|
+
cfg.sources.envOverrides = envOverrides;
|
|
547
|
+
return cfg;
|
|
548
|
+
}
|
|
549
|
+
let parsed;
|
|
550
|
+
try {
|
|
551
|
+
parsed = LeastCostSchema.parse(raw);
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
throw new Error(`Invalid [least_cost] config: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
555
|
+
}
|
|
556
|
+
const tiers = { ...DEFAULT_LEAST_COST_TIERS, ...parsed.tiers };
|
|
557
|
+
return {
|
|
558
|
+
enabled: parsed.enabled,
|
|
559
|
+
minTier: parsed.min_tier,
|
|
560
|
+
maxCostUsd: parsed.max_cost_usd,
|
|
561
|
+
defaultExpectedOutputTokens: parsed.default_expected_output_tokens,
|
|
562
|
+
budgetOutputSafetyFactor: parsed.budget_output_safety_factor,
|
|
563
|
+
priorsScope: parsed.priors_scope,
|
|
564
|
+
allowUnpriced: parsed.allow_unpriced,
|
|
565
|
+
maxReroutes: parsed.max_reroutes,
|
|
566
|
+
preferCatalogPrice: parsed.prefer_catalog_price,
|
|
567
|
+
preferenceOrder: [...parsed.preference_order],
|
|
568
|
+
tiers,
|
|
569
|
+
candidates: {
|
|
570
|
+
allow: [...parsed.candidates.allow],
|
|
571
|
+
deny: [...parsed.candidates.deny],
|
|
572
|
+
},
|
|
573
|
+
sources: { configFile: sourcePath, envOverrides },
|
|
574
|
+
};
|
|
575
|
+
}
|
|
416
576
|
export function minStableTokensForModel(config, modelName) {
|
|
417
577
|
const lower = modelName.toLowerCase();
|
|
418
578
|
const table = config.minStableTokensForCacheControl;
|
package/dist/db.js
CHANGED
|
@@ -28,7 +28,7 @@ export class DatabaseConnection {
|
|
|
28
28
|
}
|
|
29
29
|
catch (error) {
|
|
30
30
|
this.logger.error("Failed to connect to PostgreSQL", { error });
|
|
31
|
-
throw new Error(`Failed to connect to PostgreSQL: ${error instanceof Error ? error.message : String(error)}
|
|
31
|
+
throw new Error(`Failed to connect to PostgreSQL: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
async disconnect() {
|
|
@@ -40,7 +40,7 @@ export class DatabaseConnection {
|
|
|
40
40
|
this.pool = null;
|
|
41
41
|
}
|
|
42
42
|
catch (error) {
|
|
43
|
-
errors.push(new Error(`PostgreSQL disconnect error: ${error instanceof Error ? error.message : String(error)}
|
|
43
|
+
errors.push(new Error(`PostgreSQL disconnect error: ${error instanceof Error ? error.message : String(error)}`, { cause: error }));
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
if (errors.length > 0) {
|
|
@@ -60,7 +60,7 @@ export class DatabaseConnection {
|
|
|
60
60
|
result.postgres.connected = true;
|
|
61
61
|
result.postgres.latency = Date.now() - pgStart;
|
|
62
62
|
}
|
|
63
|
-
catch
|
|
63
|
+
catch {
|
|
64
64
|
result.postgres.connected = false;
|
|
65
65
|
}
|
|
66
66
|
finally {
|
|
@@ -87,7 +87,7 @@ async function importOptionalPg() {
|
|
|
87
87
|
}
|
|
88
88
|
catch (error) {
|
|
89
89
|
if (error?.code === "ERR_MODULE_NOT_FOUND" || error?.code === "MODULE_NOT_FOUND") {
|
|
90
|
-
throw new Error("PostgreSQL sessions require optional peer dependency 'pg'. Install it alongside llm-cli-gateway to use DATABASE_URL-backed sessions.");
|
|
90
|
+
throw new Error("PostgreSQL sessions require optional peer dependency 'pg'. Install it alongside llm-cli-gateway to use DATABASE_URL-backed sessions.", { cause: error });
|
|
91
91
|
}
|
|
92
92
|
throw error;
|
|
93
93
|
}
|
package/dist/doctor.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { type EndpointExposureReport } from "./endpoint-exposure.js";
|
|
|
2
2
|
import { type ProviderLoginStatus } from "./provider-status.js";
|
|
3
3
|
import { type ApiProviderLoginGuidance } from "./provider-login-guidance.js";
|
|
4
4
|
import type { FlightRecorderQuery } from "./flight-recorder.js";
|
|
5
|
-
import { type CacheAwarenessConfig, type ProvidersConfig, type RemoteOAuthConfigDiagnostics } from "./config.js";
|
|
5
|
+
import { type CacheAwarenessConfig, type LeastCostConfig, type ProvidersConfig, type RemoteOAuthConfigDiagnostics } from "./config.js";
|
|
6
|
+
import { type TelemetryTier } from "./lcr-telemetry.js";
|
|
7
|
+
import type { Confidence, PriceSource, QualityTier } from "./least-cost-types.js";
|
|
6
8
|
import type { AuthConfig } from "./auth.js";
|
|
7
9
|
import { type RemoteSafeWorkspaceSummary } from "./workspace-registry.js";
|
|
8
10
|
import { type ProviderCapabilityId, type ProviderKind } from "./provider-tool-capabilities.js";
|
|
@@ -79,6 +81,35 @@ export interface GeminiConfigStatus {
|
|
|
79
81
|
}
|
|
80
82
|
export declare function checkVibeSessionLogging(home?: string): VibeSessionLoggingStatus;
|
|
81
83
|
export declare function checkGeminiConfig(cwd?: string, home?: string, whitelist?: readonly string[]): GeminiConfigStatus;
|
|
84
|
+
export interface LeastCostCandidateReport {
|
|
85
|
+
model: string;
|
|
86
|
+
family: string;
|
|
87
|
+
priced: boolean;
|
|
88
|
+
priceSource: PriceSource;
|
|
89
|
+
tier: QualityTier | null;
|
|
90
|
+
}
|
|
91
|
+
export interface LeastCostProviderReport {
|
|
92
|
+
provider: CliType;
|
|
93
|
+
telemetryTier: TelemetryTier;
|
|
94
|
+
candidates: LeastCostCandidateReport[];
|
|
95
|
+
}
|
|
96
|
+
export interface LeastCostCalibrationBucketReport {
|
|
97
|
+
bucket: string;
|
|
98
|
+
k: number;
|
|
99
|
+
samples: number;
|
|
100
|
+
confidence: Confidence;
|
|
101
|
+
}
|
|
102
|
+
export interface LeastCostReport {
|
|
103
|
+
enabled: boolean;
|
|
104
|
+
pricing: {
|
|
105
|
+
tableAsOf: string;
|
|
106
|
+
apiCatalogAsOf: string;
|
|
107
|
+
staleDays: number;
|
|
108
|
+
};
|
|
109
|
+
providers: LeastCostProviderReport[];
|
|
110
|
+
untieredModels: string[];
|
|
111
|
+
calibrationQuality: LeastCostCalibrationBucketReport[];
|
|
112
|
+
}
|
|
82
113
|
export interface DoctorReport {
|
|
83
114
|
schema_version: "1.0";
|
|
84
115
|
ok: boolean;
|
|
@@ -158,6 +189,7 @@ export interface DoctorReport {
|
|
|
158
189
|
};
|
|
159
190
|
cache_awareness: CacheAwarenessReport;
|
|
160
191
|
provider_capabilities: ProviderCapabilitySummaryReport;
|
|
192
|
+
least_cost: LeastCostReport;
|
|
161
193
|
api_providers?: ApiProviderHealthReport;
|
|
162
194
|
upstream: {
|
|
163
195
|
note: string;
|
|
@@ -215,6 +247,7 @@ export interface CreateDoctorReportOptions {
|
|
|
215
247
|
reachable: boolean;
|
|
216
248
|
error?: string;
|
|
217
249
|
}>;
|
|
250
|
+
leastCost?: LeastCostConfig;
|
|
218
251
|
}
|
|
219
252
|
export declare function createDoctorReport(envOrOptions?: NodeJS.ProcessEnv | CreateDoctorReportOptions): DoctorReport;
|
|
220
253
|
export declare function printDoctorJson(opts?: {
|