llm-cli-gateway 2.14.1 → 2.16.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/CHANGELOG.md +61 -1
- package/README.md +35 -0
- package/dist/acp/process-manager.js +2 -1
- package/dist/acp/provider-registry.js +8 -8
- package/dist/async-job-manager.d.ts +4 -1
- package/dist/async-job-manager.js +15 -6
- 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 +89 -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 +16 -0
- package/dist/config.js +57 -0
- package/dist/executor.d.ts +1 -0
- package/dist/executor.js +5 -2
- package/dist/flight-recorder.d.ts +9 -0
- package/dist/flight-recorder.js +42 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.js +282 -88
- package/dist/job-store.d.ts +4 -0
- package/dist/job-store.js +16 -0
- package/dist/provider-definitions.d.ts +1 -0
- package/dist/provider-definitions.js +28 -10
- package/dist/provider-tool-capabilities.js +3 -3
- package/dist/request-helpers.js +1 -1
- package/dist/skill-loader.d.ts +24 -0
- package/dist/skill-loader.js +201 -0
- package/dist/spawn-env-isolation.d.ts +10 -0
- package/dist/spawn-env-isolation.js +55 -0
- package/dist/upstream-contracts.js +47 -27
- package/npm-shrinkwrap.json +2 -2
- package/package.json +7 -3
|
@@ -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
|
@@ -15,6 +15,13 @@ export interface Config {
|
|
|
15
15
|
database?: DatabaseConfig;
|
|
16
16
|
sessionTtl: number;
|
|
17
17
|
}
|
|
18
|
+
export interface SkillsConfig {
|
|
19
|
+
paths: string[];
|
|
20
|
+
sources: {
|
|
21
|
+
configFile: string | null;
|
|
22
|
+
envOverrides: string[];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
18
25
|
export declare function loadConfig(): Config;
|
|
19
26
|
export declare const PERSISTENCE_BACKENDS: readonly ["sqlite", "postgres", "memory", "none"];
|
|
20
27
|
export type PersistenceBackend = (typeof PERSISTENCE_BACKENDS)[number];
|
|
@@ -46,6 +53,7 @@ export interface PersistenceConfigSources {
|
|
|
46
53
|
envOverrides: string[];
|
|
47
54
|
}
|
|
48
55
|
export declare function defaultGatewayConfigPath(): string;
|
|
56
|
+
export declare function loadSkillsConfig(logger?: Logger): SkillsConfig;
|
|
49
57
|
export declare function loadPersistenceConfig(logger?: Logger): PersistenceConfig;
|
|
50
58
|
export declare const DEFAULT_HTTP_MAX_SESSIONS = 100;
|
|
51
59
|
export declare const DEFAULT_HTTP_SESSION_IDLE_TTL_MS: number;
|
|
@@ -103,6 +111,14 @@ export interface CacheAwarenessConfig {
|
|
|
103
111
|
};
|
|
104
112
|
}
|
|
105
113
|
export declare function loadCacheAwarenessConfig(logger?: Logger): CacheAwarenessConfig;
|
|
114
|
+
export interface CompressionConfig {
|
|
115
|
+
enabled: boolean;
|
|
116
|
+
sources: {
|
|
117
|
+
configFile: string | null;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export declare const DEFAULT_COMPRESSION_CONFIG: CompressionConfig;
|
|
121
|
+
export declare function loadCompressionConfig(logger?: Logger): CompressionConfig;
|
|
106
122
|
export declare function minStableTokensForModel(config: CacheAwarenessConfig, modelName: string): number;
|
|
107
123
|
export declare const DEFAULT_XAI_API_KEY_ENV = "XAI_API_KEY";
|
|
108
124
|
export declare const DEFAULT_XAI_BASE_URL = "https://api.x.ai/v1";
|
package/dist/config.js
CHANGED
|
@@ -14,6 +14,12 @@ const DatabaseUrlSchema = z
|
|
|
14
14
|
message: "Database URL must start with postgresql:// or postgres://",
|
|
15
15
|
});
|
|
16
16
|
export const DEFAULT_SESSION_TTL_SECONDS = 2592000;
|
|
17
|
+
const SkillsSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
paths: z.array(z.string().min(1)).default([]),
|
|
20
|
+
})
|
|
21
|
+
.strict()
|
|
22
|
+
.default({ paths: [] });
|
|
17
23
|
export function loadConfig() {
|
|
18
24
|
const databaseUrl = process.env.DATABASE_URL;
|
|
19
25
|
const rawSessionTtl = parseInt(process.env.SESSION_TTL || String(DEFAULT_SESSION_TTL_SECONDS), 10);
|
|
@@ -89,6 +95,32 @@ function defaultPersistenceConfigPath() {
|
|
|
89
95
|
export function defaultGatewayConfigPath() {
|
|
90
96
|
return defaultPersistenceConfigPath();
|
|
91
97
|
}
|
|
98
|
+
export function loadSkillsConfig(logger = noopLogger) {
|
|
99
|
+
const configPath = defaultGatewayConfigPath();
|
|
100
|
+
const { parsed, sourcePath } = readGatewayTomlFile(configPath, logger, "skills");
|
|
101
|
+
const rawSkills = parsed?.skills ?? {};
|
|
102
|
+
const sources = {
|
|
103
|
+
configFile: sourcePath,
|
|
104
|
+
envOverrides: [],
|
|
105
|
+
};
|
|
106
|
+
let skillsParsed;
|
|
107
|
+
try {
|
|
108
|
+
skillsParsed = SkillsSchema.parse(rawSkills);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
throw new Error(`Invalid [skills] config: ${err instanceof Error ? err.message : String(err)}`);
|
|
112
|
+
}
|
|
113
|
+
const paths = [...skillsParsed.paths];
|
|
114
|
+
const envPaths = process.env.LLM_GATEWAY_SKILLS_PATH;
|
|
115
|
+
if (envPaths !== undefined && envPaths.trim().length > 0) {
|
|
116
|
+
paths.push(...envPaths
|
|
117
|
+
.split(path.delimiter)
|
|
118
|
+
.map(part => part.trim())
|
|
119
|
+
.filter(Boolean));
|
|
120
|
+
sources.envOverrides.push("LLM_GATEWAY_SKILLS_PATH");
|
|
121
|
+
}
|
|
122
|
+
return { paths, sources };
|
|
123
|
+
}
|
|
92
124
|
function readPersistenceFile(configPath, logger) {
|
|
93
125
|
if (!existsSync(configPath)) {
|
|
94
126
|
return { raw: undefined, sourcePath: null };
|
|
@@ -381,6 +413,31 @@ export function loadCacheAwarenessConfig(logger = noopLogger) {
|
|
|
381
413
|
sources: { configFile: sourcePath },
|
|
382
414
|
};
|
|
383
415
|
}
|
|
416
|
+
const CompressionSchema = z
|
|
417
|
+
.object({
|
|
418
|
+
enabled: z.boolean().default(false),
|
|
419
|
+
})
|
|
420
|
+
.strict();
|
|
421
|
+
export const DEFAULT_COMPRESSION_CONFIG = {
|
|
422
|
+
enabled: false,
|
|
423
|
+
sources: { configFile: null },
|
|
424
|
+
};
|
|
425
|
+
export function loadCompressionConfig(logger = noopLogger) {
|
|
426
|
+
const configPath = defaultGatewayConfigPath();
|
|
427
|
+
const { parsed, sourcePath } = readGatewayTomlFile(configPath, logger, "compression");
|
|
428
|
+
const raw = parsed?.compression ?? {};
|
|
429
|
+
let parsedCompression;
|
|
430
|
+
try {
|
|
431
|
+
parsedCompression = CompressionSchema.parse(raw);
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
throw new Error(`Invalid [compression] config: ${err instanceof Error ? err.message : String(err)}`);
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
enabled: parsedCompression.enabled,
|
|
438
|
+
sources: { configFile: sourcePath },
|
|
439
|
+
};
|
|
440
|
+
}
|
|
384
441
|
export function minStableTokensForModel(config, modelName) {
|
|
385
442
|
const lower = modelName.toLowerCase();
|
|
386
443
|
const table = config.minStableTokensForCacheControl;
|
package/dist/executor.d.ts
CHANGED
|
@@ -35,5 +35,6 @@ export declare function spawnCliProcess(command: string, args: string[], options
|
|
|
35
35
|
cwd?: string;
|
|
36
36
|
env: NodeJS.ProcessEnv;
|
|
37
37
|
stdio: SpawnOptions["stdio"];
|
|
38
|
+
logger?: Logger;
|
|
38
39
|
}): ChildProcess;
|
|
39
40
|
export declare function executeCli(command: string, args: string[], options?: ExecuteOptions): Promise<ExecuteResult>;
|
package/dist/executor.js
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "os";
|
|
|
3
3
|
import { delimiter, join, dirname, extname, win32 } from "path";
|
|
4
4
|
import { readdirSync, existsSync } from "fs";
|
|
5
5
|
import { createCircuitBreaker, withRetry } from "./retry.js";
|
|
6
|
+
import { applySpawnEnvIsolation } from "./spawn-env-isolation.js";
|
|
6
7
|
export function providerCommandName(command) {
|
|
7
8
|
if (command === "gemini")
|
|
8
9
|
return "agy";
|
|
@@ -309,9 +310,10 @@ function killWindowsProcessTree(pid) {
|
|
|
309
310
|
return result.status === 0;
|
|
310
311
|
}
|
|
311
312
|
export function spawnCliProcess(command, args, options) {
|
|
313
|
+
const env = applySpawnEnvIsolation(options.env, options.logger);
|
|
312
314
|
const detached = shouldDetachProviderProcess();
|
|
313
315
|
const resolved = resolveCommandForSpawn(command, args, {
|
|
314
|
-
envPath: pathValueFromEnv(
|
|
316
|
+
envPath: pathValueFromEnv(env, process.platform),
|
|
315
317
|
});
|
|
316
318
|
const proc = spawn(resolved.command, resolved.args, {
|
|
317
319
|
cwd: options.cwd,
|
|
@@ -319,7 +321,7 @@ export function spawnCliProcess(command, args, options) {
|
|
|
319
321
|
windowsHide: true,
|
|
320
322
|
windowsVerbatimArguments: resolved.windowsVerbatimArguments,
|
|
321
323
|
stdio: options.stdio,
|
|
322
|
-
env
|
|
324
|
+
env,
|
|
323
325
|
});
|
|
324
326
|
if (proc.pid)
|
|
325
327
|
registerProcessGroup(proc.pid);
|
|
@@ -337,6 +339,7 @@ export async function executeCli(command, args, options = {}) {
|
|
|
337
339
|
cwd,
|
|
338
340
|
stdio,
|
|
339
341
|
env: { ...baseEnv, ...(extraEnv ?? {}) },
|
|
342
|
+
logger: options.logger,
|
|
340
343
|
});
|
|
341
344
|
if (stdin !== undefined && proc.stdin) {
|
|
342
345
|
proc.stdin.write(stdin);
|
|
@@ -37,6 +37,13 @@ interface LoggerLike {
|
|
|
37
37
|
info: (message: string, ...args: any[]) => void;
|
|
38
38
|
error: (message: string, ...args: any[]) => void;
|
|
39
39
|
}
|
|
40
|
+
export interface CompressionTelemetry {
|
|
41
|
+
route: string;
|
|
42
|
+
transforms: string[];
|
|
43
|
+
originalChars: number;
|
|
44
|
+
compressedChars: number;
|
|
45
|
+
estimatedTokensSaved: number;
|
|
46
|
+
}
|
|
40
47
|
export declare function resolveFlightRecorderDbPath(): string | null;
|
|
41
48
|
export declare class FlightRecorder {
|
|
42
49
|
private db;
|
|
@@ -51,6 +58,7 @@ export declare class FlightRecorder {
|
|
|
51
58
|
});
|
|
52
59
|
logStart(entry: FlightLogStart): void;
|
|
53
60
|
logComplete(correlationId: string, result: FlightLogResult): void;
|
|
61
|
+
recordCompressionTelemetry(correlationId: string, telemetry: CompressionTelemetry): void;
|
|
54
62
|
private redactStart;
|
|
55
63
|
private redactResult;
|
|
56
64
|
queryRequests<T = Record<string, unknown>>(sql: string, ...params: unknown[]): T[];
|
|
@@ -60,6 +68,7 @@ export declare class FlightRecorder {
|
|
|
60
68
|
export declare class NoopFlightRecorder {
|
|
61
69
|
logStart(_entry: FlightLogStart): void;
|
|
62
70
|
logComplete(_correlationId: string, _result: FlightLogResult): void;
|
|
71
|
+
recordCompressionTelemetry(_correlationId: string, _telemetry: CompressionTelemetry): void;
|
|
63
72
|
queryRequests<T = Record<string, unknown>>(_sql: string, ..._params: unknown[]): T[];
|
|
64
73
|
flush(): void;
|
|
65
74
|
close(): void;
|
package/dist/flight-recorder.js
CHANGED
|
@@ -64,6 +64,25 @@ function ensureMetadataProviderSessionColumns(db) {
|
|
|
64
64
|
db.exec("ALTER TABLE gateway_metadata ADD COLUMN stop_reason TEXT");
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
function ensureCompressionColumns(db) {
|
|
68
|
+
const rows = db.prepare("PRAGMA table_info(gateway_metadata)").all();
|
|
69
|
+
const names = new Set(rows.map((row) => (row && typeof row.name === "string" ? row.name : "")));
|
|
70
|
+
if (!names.has("compression_route")) {
|
|
71
|
+
db.exec("ALTER TABLE gateway_metadata ADD COLUMN compression_route TEXT");
|
|
72
|
+
}
|
|
73
|
+
if (!names.has("compression_transforms")) {
|
|
74
|
+
db.exec("ALTER TABLE gateway_metadata ADD COLUMN compression_transforms TEXT");
|
|
75
|
+
}
|
|
76
|
+
if (!names.has("compression_original_chars")) {
|
|
77
|
+
db.exec("ALTER TABLE gateway_metadata ADD COLUMN compression_original_chars INTEGER");
|
|
78
|
+
}
|
|
79
|
+
if (!names.has("compression_compressed_chars")) {
|
|
80
|
+
db.exec("ALTER TABLE gateway_metadata ADD COLUMN compression_compressed_chars INTEGER");
|
|
81
|
+
}
|
|
82
|
+
if (!names.has("compression_tokens_saved_est")) {
|
|
83
|
+
db.exec("ALTER TABLE gateway_metadata ADD COLUMN compression_tokens_saved_est INTEGER");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
67
86
|
export function resolveFlightRecorderDbPath() {
|
|
68
87
|
const configured = process.env.LLM_GATEWAY_LOGS_DB;
|
|
69
88
|
if (configured !== undefined) {
|
|
@@ -196,6 +215,10 @@ export class FlightRecorder {
|
|
|
196
215
|
this.db
|
|
197
216
|
.prepare("INSERT OR IGNORE INTO _migrations(version, applied_at) VALUES(7, ?)")
|
|
198
217
|
.run(new Date().toISOString());
|
|
218
|
+
ensureCompressionColumns(this.db);
|
|
219
|
+
this.db
|
|
220
|
+
.prepare("INSERT OR IGNORE INTO _migrations(version, applied_at) VALUES(8, ?)")
|
|
221
|
+
.run(new Date().toISOString());
|
|
199
222
|
if (process.platform !== "win32") {
|
|
200
223
|
try {
|
|
201
224
|
chmodSync(dbPath, 0o600);
|
|
@@ -297,6 +320,24 @@ export class FlightRecorder {
|
|
|
297
320
|
logComplete(correlationId, result) {
|
|
298
321
|
this.updateCompleteTxn(correlationId, this.redactEnabled ? this.redactResult(result) : result);
|
|
299
322
|
}
|
|
323
|
+
recordCompressionTelemetry(correlationId, telemetry) {
|
|
324
|
+
this.db
|
|
325
|
+
.prepare(`UPDATE gateway_metadata
|
|
326
|
+
SET compression_route = @route,
|
|
327
|
+
compression_transforms = @transforms,
|
|
328
|
+
compression_original_chars = @original_chars,
|
|
329
|
+
compression_compressed_chars = @compressed_chars,
|
|
330
|
+
compression_tokens_saved_est = @tokens_saved_est
|
|
331
|
+
WHERE request_id = @id AND compression_route IS NULL`)
|
|
332
|
+
.run({
|
|
333
|
+
id: correlationId,
|
|
334
|
+
route: telemetry.route,
|
|
335
|
+
transforms: telemetry.transforms.join(","),
|
|
336
|
+
original_chars: telemetry.originalChars,
|
|
337
|
+
compressed_chars: telemetry.compressedChars,
|
|
338
|
+
tokens_saved_est: telemetry.estimatedTokensSaved,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
300
341
|
redactStart(entry) {
|
|
301
342
|
return {
|
|
302
343
|
...entry,
|
|
@@ -330,6 +371,7 @@ export class FlightRecorder {
|
|
|
330
371
|
export class NoopFlightRecorder {
|
|
331
372
|
logStart(_entry) { }
|
|
332
373
|
logComplete(_correlationId, _result) { }
|
|
374
|
+
recordCompressionTelemetry(_correlationId, _telemetry) { }
|
|
333
375
|
queryRequests(_sql, ..._params) {
|
|
334
376
|
return [];
|
|
335
377
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { z } from "zod/v3";
|
|
|
4
4
|
import { ISessionManager, type CliType, type ProviderType } from "./session-manager.js";
|
|
5
5
|
import { ResourceProvider } from "./resources.js";
|
|
6
6
|
import { PerformanceMetrics } from "./metrics.js";
|
|
7
|
-
import { type
|
|
7
|
+
import { type CompressResult } from "./compressor/index.js";
|
|
8
|
+
import { type PersistenceConfig, type CacheAwarenessConfig, type CompressionConfig, type ProvidersConfig, type ApiProviderRuntime, type AcpConfig, type AdminConfig } from "./config.js";
|
|
8
9
|
import { DatabaseConnection } from "./db.js";
|
|
9
10
|
import { type DevinAcpAgentType } from "./provider-definitions.js";
|
|
10
11
|
import { AsyncJobManager } from "./async-job-manager.js";
|
|
@@ -38,6 +39,7 @@ type ExtendedToolResponse = {
|
|
|
38
39
|
};
|
|
39
40
|
reviewIntegrity?: ReviewIntegrityResult;
|
|
40
41
|
warnings?: WarningEntry[];
|
|
42
|
+
compression?: CompressResult;
|
|
41
43
|
};
|
|
42
44
|
export declare const logger: {
|
|
43
45
|
info: (message: string, ...args: unknown[]) => void;
|
|
@@ -76,6 +78,7 @@ export interface GatewayServerDeps {
|
|
|
76
78
|
logger?: GatewayLogger;
|
|
77
79
|
persistence?: PersistenceConfig;
|
|
78
80
|
cacheAwareness?: CacheAwarenessConfig;
|
|
81
|
+
compression?: CompressionConfig;
|
|
79
82
|
providers?: ProvidersConfig;
|
|
80
83
|
acpConfig?: AcpConfig;
|
|
81
84
|
adminConfig?: AdminConfig;
|
|
@@ -92,6 +95,7 @@ export interface GatewayServerRuntime {
|
|
|
92
95
|
logger: GatewayLogger;
|
|
93
96
|
persistence: PersistenceConfig;
|
|
94
97
|
cacheAwareness: CacheAwarenessConfig;
|
|
98
|
+
compression: CompressionConfig;
|
|
95
99
|
providers: ProvidersConfig;
|
|
96
100
|
acpConfig: AcpConfig;
|
|
97
101
|
adminConfig: AdminConfig;
|
|
@@ -169,6 +173,11 @@ export declare function extractUsageAndCost(cli: CliType, output: string, output
|
|
|
169
173
|
cacheCreationTokens?: number;
|
|
170
174
|
costUsd?: number;
|
|
171
175
|
};
|
|
176
|
+
export declare function resolveEffectiveCompression(compression: CompressionConfig, input: {
|
|
177
|
+
compressResponse?: boolean;
|
|
178
|
+
outputFormat?: string;
|
|
179
|
+
outputSchemaDeclared?: boolean;
|
|
180
|
+
}): boolean;
|
|
172
181
|
export declare function registerBaseResources(server: McpServer, runtime: GatewayServerRuntime): void;
|
|
173
182
|
interface CliRequestPrep {
|
|
174
183
|
corrId: string;
|
|
@@ -344,6 +353,7 @@ export declare function prepareGrokRequest(params: {
|
|
|
344
353
|
forkSession?: boolean;
|
|
345
354
|
jsonSchema?: string | Record<string, unknown>;
|
|
346
355
|
}, runtime?: GatewayServerRuntime): CliRequestPrep | ExtendedToolResponse;
|
|
356
|
+
export declare function resolveMistralAgentMode(approvalStrategy: "legacy" | "mcp_managed" | undefined, callerMode: MistralAgentMode | undefined): MistralAgentMode;
|
|
347
357
|
export declare function prepareMistralRequest(params: {
|
|
348
358
|
prompt?: string;
|
|
349
359
|
promptParts?: PromptParts;
|
|
@@ -374,7 +384,7 @@ export declare function buildMistralRetryPrep(params: Pick<MistralRequestParams,
|
|
|
374
384
|
env: Record<string, string>;
|
|
375
385
|
ignoredDisallowedTools: boolean;
|
|
376
386
|
};
|
|
377
|
-
export declare function buildCliResponse(cli: CliType, stdout: string, optimizeResponse: boolean, corrId: string, sessionId: string | undefined, prep: CliRequestPrep, durationMs: number, resumable?: boolean, outputFormat?: string, warnings?: WarningEntry[]): ExtendedToolResponse;
|
|
387
|
+
export declare function buildCliResponse(cli: CliType, stdout: string, optimizeResponse: boolean, corrId: string, sessionId: string | undefined, prep: CliRequestPrep, durationMs: number, resumable?: boolean, outputFormat?: string, warnings?: WarningEntry[], compressResponse?: boolean): ExtendedToolResponse;
|
|
378
388
|
export interface GrokApiRequestParams {
|
|
379
389
|
prompt?: string;
|
|
380
390
|
promptParts?: PromptParts;
|
|
@@ -427,6 +437,7 @@ export interface GeminiRequestParams {
|
|
|
427
437
|
correlationId?: string;
|
|
428
438
|
optimizePrompt: boolean;
|
|
429
439
|
optimizeResponse?: boolean;
|
|
440
|
+
compressResponse?: boolean;
|
|
430
441
|
idleTimeoutMs?: number;
|
|
431
442
|
forceRefresh?: boolean;
|
|
432
443
|
outputFormat?: "text" | "json" | "stream-json";
|
|
@@ -482,6 +493,7 @@ export interface GrokRequestParams {
|
|
|
482
493
|
correlationId?: string;
|
|
483
494
|
optimizePrompt: boolean;
|
|
484
495
|
optimizeResponse?: boolean;
|
|
496
|
+
compressResponse?: boolean;
|
|
485
497
|
idleTimeoutMs?: number;
|
|
486
498
|
forceRefresh?: boolean;
|
|
487
499
|
maxTurns?: number;
|
|
@@ -541,6 +553,7 @@ export interface DevinRequestParams {
|
|
|
541
553
|
correlationId?: string;
|
|
542
554
|
optimizePrompt: boolean;
|
|
543
555
|
optimizeResponse?: boolean;
|
|
556
|
+
compressResponse?: boolean;
|
|
544
557
|
idleTimeoutMs?: number;
|
|
545
558
|
forceRefresh?: boolean;
|
|
546
559
|
}
|
|
@@ -580,6 +593,7 @@ export interface CursorRequestParams {
|
|
|
580
593
|
correlationId?: string;
|
|
581
594
|
optimizePrompt: boolean;
|
|
582
595
|
optimizeResponse?: boolean;
|
|
596
|
+
compressResponse?: boolean;
|
|
583
597
|
idleTimeoutMs?: number;
|
|
584
598
|
forceRefresh?: boolean;
|
|
585
599
|
}
|
|
@@ -620,6 +634,7 @@ export interface MistralRequestParams {
|
|
|
620
634
|
correlationId?: string;
|
|
621
635
|
optimizePrompt: boolean;
|
|
622
636
|
optimizeResponse?: boolean;
|
|
637
|
+
compressResponse?: boolean;
|
|
623
638
|
idleTimeoutMs?: number;
|
|
624
639
|
forceRefresh?: boolean;
|
|
625
640
|
trust?: boolean;
|
|
@@ -653,6 +668,7 @@ export declare function handleCodexRequestAsync(deps: AsyncHandlerDeps, params:
|
|
|
653
668
|
createNewSession: boolean;
|
|
654
669
|
correlationId?: string;
|
|
655
670
|
optimizePrompt: boolean;
|
|
671
|
+
compressResponse?: boolean;
|
|
656
672
|
idleTimeoutMs?: number;
|
|
657
673
|
forceRefresh?: boolean;
|
|
658
674
|
outputFormat?: "text" | "json";
|