llm-cli-gateway 1.0.1 → 1.4.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 +42 -0
- package/README.md +153 -9
- package/dist/approval-manager.d.ts +1 -1
- package/dist/approval-manager.js +7 -4
- package/dist/async-job-manager.d.ts +53 -4
- package/dist/async-job-manager.js +254 -27
- package/dist/claude-mcp-config.js +7 -4
- package/dist/cli-updater.d.ts +38 -0
- package/dist/cli-updater.js +145 -0
- package/dist/config.js +15 -9
- package/dist/db.js +4 -4
- package/dist/executor.js +20 -13
- package/dist/flight-recorder.d.ts +48 -0
- package/dist/flight-recorder.js +220 -0
- package/dist/health.js +3 -3
- package/dist/index.d.ts +28 -0
- package/dist/index.js +1456 -278
- package/dist/job-store.d.ts +84 -0
- package/dist/job-store.js +251 -0
- package/dist/logger.js +1 -1
- package/dist/metrics.js +9 -12
- package/dist/migrate-sessions.js +2 -2
- package/dist/model-registry.d.ts +14 -0
- package/dist/model-registry.js +448 -140
- package/dist/optimizer.js +9 -9
- package/dist/process-monitor.js +24 -8
- package/dist/request-helpers.d.ts +48 -0
- package/dist/request-helpers.js +64 -2
- package/dist/resources.js +76 -32
- package/dist/retry.js +6 -4
- package/dist/review-integrity.d.ts +6 -38
- package/dist/review-integrity.js +41 -275
- package/dist/session-manager-pg.js +7 -4
- package/dist/session-manager.d.ts +1 -1
- package/dist/session-manager.js +9 -5
- package/dist/stream-json-parser.js +8 -6
- package/package.json +7 -4
package/dist/config.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
// Zod schemas for configuration validation
|
|
3
|
-
const DatabaseUrlSchema = z
|
|
3
|
+
const DatabaseUrlSchema = z
|
|
4
|
+
.string()
|
|
5
|
+
.url()
|
|
6
|
+
.refine(url => url.startsWith("postgresql://") || url.startsWith("postgres://"), {
|
|
7
|
+
message: "Database URL must start with postgresql:// or postgres://",
|
|
8
|
+
});
|
|
4
9
|
const RedisUrlSchema = z.string().url().startsWith("redis://");
|
|
5
10
|
export const DEFAULT_SESSION_TTL_SECONDS = 2592000; // 30 days
|
|
6
11
|
/**
|
|
@@ -15,11 +20,12 @@ export function loadConfig() {
|
|
|
15
20
|
const cacheTtl = {
|
|
16
21
|
session: 3600, // 1 hour
|
|
17
22
|
activeSession: 1800, // 30 minutes
|
|
18
|
-
sessionList: 120 // 2 minutes
|
|
23
|
+
sessionList: 120, // 2 minutes
|
|
19
24
|
};
|
|
20
25
|
const rawSessionTtl = parseInt(process.env.SESSION_TTL || String(DEFAULT_SESSION_TTL_SECONDS), 10);
|
|
21
|
-
const sessionTtl =
|
|
22
|
-
? rawSessionTtl
|
|
26
|
+
const sessionTtl = Number.isFinite(rawSessionTtl) && rawSessionTtl > 0
|
|
27
|
+
? rawSessionTtl
|
|
28
|
+
: DEFAULT_SESSION_TTL_SECONDS;
|
|
23
29
|
// If no database config, return base config (file-based storage)
|
|
24
30
|
if (!databaseUrl || !redisUrl) {
|
|
25
31
|
return { cacheTtl, sessionTtl };
|
|
@@ -39,18 +45,18 @@ export function loadConfig() {
|
|
|
39
45
|
max: 10,
|
|
40
46
|
idleTimeoutMillis: 30000,
|
|
41
47
|
connectionTimeoutMillis: 5000,
|
|
42
|
-
statementTimeout: 10000
|
|
43
|
-
}
|
|
48
|
+
statementTimeout: 10000,
|
|
49
|
+
},
|
|
44
50
|
},
|
|
45
51
|
redis: {
|
|
46
52
|
url: redisUrl,
|
|
47
53
|
retryStrategy: {
|
|
48
54
|
maxRetries: 3,
|
|
49
55
|
initialDelay: 50,
|
|
50
|
-
maxDelay: 2000
|
|
51
|
-
}
|
|
56
|
+
maxDelay: 2000,
|
|
57
|
+
},
|
|
52
58
|
},
|
|
53
59
|
cacheTtl,
|
|
54
|
-
sessionTtl
|
|
60
|
+
sessionTtl,
|
|
55
61
|
};
|
|
56
62
|
}
|
package/dist/db.js
CHANGED
|
@@ -26,7 +26,7 @@ export class DatabaseConnection {
|
|
|
26
26
|
max: this.config.database.pool.max,
|
|
27
27
|
idleTimeoutMillis: this.config.database.pool.idleTimeoutMillis,
|
|
28
28
|
connectionTimeoutMillis: this.config.database.pool.connectionTimeoutMillis,
|
|
29
|
-
statement_timeout: this.config.database.pool.statementTimeout
|
|
29
|
+
statement_timeout: this.config.database.pool.statementTimeout,
|
|
30
30
|
};
|
|
31
31
|
this.pool = new Pool(poolConfig);
|
|
32
32
|
// Test PostgreSQL connection
|
|
@@ -54,7 +54,7 @@ export class DatabaseConnection {
|
|
|
54
54
|
// Reconnect on READONLY and ECONNRESET errors
|
|
55
55
|
const targetErrors = ["READONLY", "ECONNRESET"];
|
|
56
56
|
return targetErrors.some(targetError => err.message.includes(targetError));
|
|
57
|
-
}
|
|
57
|
+
},
|
|
58
58
|
};
|
|
59
59
|
this.redis = new Redis(this.config.redis.url, redisOptions);
|
|
60
60
|
// Test Redis connection
|
|
@@ -101,7 +101,7 @@ export class DatabaseConnection {
|
|
|
101
101
|
async healthCheck() {
|
|
102
102
|
const result = {
|
|
103
103
|
postgres: { connected: false, latency: 0 },
|
|
104
|
-
redis: { connected: false, latency: 0 }
|
|
104
|
+
redis: { connected: false, latency: 0 },
|
|
105
105
|
};
|
|
106
106
|
// Check PostgreSQL
|
|
107
107
|
if (this.pool) {
|
|
@@ -137,7 +137,7 @@ export class DatabaseConnection {
|
|
|
137
137
|
}
|
|
138
138
|
this.logger.debug("Health check completed", {
|
|
139
139
|
postgres: result.postgres.connected,
|
|
140
|
-
redis: result.redis.connected
|
|
140
|
+
redis: result.redis.connected,
|
|
141
141
|
});
|
|
142
142
|
return result;
|
|
143
143
|
}
|
package/dist/executor.js
CHANGED
|
@@ -28,7 +28,7 @@ function getNvmPath() {
|
|
|
28
28
|
try {
|
|
29
29
|
const versions = readdirSync(nvmVersionsDir);
|
|
30
30
|
cachedNvmPath = versions.length
|
|
31
|
-
? versions.map(
|
|
31
|
+
? versions.map(version => join(nvmVersionsDir, version, "bin")).join(":")
|
|
32
32
|
: null;
|
|
33
33
|
}
|
|
34
34
|
catch {
|
|
@@ -43,7 +43,7 @@ export function getExtendedPath() {
|
|
|
43
43
|
join(home, ".local/bin"),
|
|
44
44
|
dirname(process.execPath), // Current node's bin directory
|
|
45
45
|
"/usr/local/bin",
|
|
46
|
-
"/usr/bin"
|
|
46
|
+
"/usr/bin",
|
|
47
47
|
];
|
|
48
48
|
// Add all nvm node version bin directories
|
|
49
49
|
const nvmPath = getNvmPath();
|
|
@@ -75,7 +75,9 @@ export function killAllProcessGroups() {
|
|
|
75
75
|
try {
|
|
76
76
|
process.kill(-pid, "SIGTERM");
|
|
77
77
|
}
|
|
78
|
-
catch {
|
|
78
|
+
catch {
|
|
79
|
+
/* ESRCH ok */
|
|
80
|
+
}
|
|
79
81
|
}
|
|
80
82
|
return new Promise(resolve => {
|
|
81
83
|
setTimeout(() => {
|
|
@@ -83,7 +85,9 @@ export function killAllProcessGroups() {
|
|
|
83
85
|
try {
|
|
84
86
|
process.kill(-pid, "SIGKILL");
|
|
85
87
|
}
|
|
86
|
-
catch {
|
|
88
|
+
catch {
|
|
89
|
+
/* ESRCH ok */
|
|
90
|
+
}
|
|
87
91
|
}
|
|
88
92
|
activeProcessGroups.clear();
|
|
89
93
|
resolve();
|
|
@@ -129,7 +133,7 @@ export async function executeCli(command, args, options = {}) {
|
|
|
129
133
|
cwd,
|
|
130
134
|
detached: true,
|
|
131
135
|
stdio: ["ignore", "pipe", "pipe"],
|
|
132
|
-
env: { ...process.env, PATH: extendedPath }
|
|
136
|
+
env: { ...process.env, PATH: extendedPath },
|
|
133
137
|
});
|
|
134
138
|
if (proc.pid)
|
|
135
139
|
registerProcessGroup(proc.pid);
|
|
@@ -152,7 +156,9 @@ export async function executeCli(command, args, options = {}) {
|
|
|
152
156
|
if (proc.pid)
|
|
153
157
|
unregisterProcessGroup(proc.pid);
|
|
154
158
|
};
|
|
155
|
-
const timeoutMs = typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0
|
|
159
|
+
const timeoutMs = typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0
|
|
160
|
+
? timeout
|
|
161
|
+
: undefined;
|
|
156
162
|
const timeoutId = timeoutMs
|
|
157
163
|
? setTimeout(() => {
|
|
158
164
|
timedOut = true;
|
|
@@ -166,7 +172,8 @@ export async function executeCli(command, args, options = {}) {
|
|
|
166
172
|
: undefined;
|
|
167
173
|
// Idle timeout: kill process if no stdout/stderr activity for idleMs
|
|
168
174
|
const idleMs = typeof idleTimeout === "number" && Number.isFinite(idleTimeout) && idleTimeout > 0
|
|
169
|
-
? idleTimeout
|
|
175
|
+
? idleTimeout
|
|
176
|
+
: undefined;
|
|
170
177
|
let idleTimerId;
|
|
171
178
|
const resetIdleTimer = () => {
|
|
172
179
|
if (!idleMs)
|
|
@@ -222,19 +229,19 @@ export async function executeCli(command, args, options = {}) {
|
|
|
222
229
|
stderr += text;
|
|
223
230
|
}
|
|
224
231
|
};
|
|
225
|
-
proc.stdout.on("data",
|
|
232
|
+
proc.stdout.on("data", data => {
|
|
226
233
|
if (settled) {
|
|
227
234
|
return;
|
|
228
235
|
}
|
|
229
236
|
handleOutputChunk(data, "stdout");
|
|
230
237
|
});
|
|
231
|
-
proc.stderr.on("data",
|
|
238
|
+
proc.stderr.on("data", data => {
|
|
232
239
|
if (settled) {
|
|
233
240
|
return;
|
|
234
241
|
}
|
|
235
242
|
handleOutputChunk(data, "stderr");
|
|
236
243
|
});
|
|
237
|
-
proc.on("close",
|
|
244
|
+
proc.on("close", code => {
|
|
238
245
|
exited = true;
|
|
239
246
|
if (idleTimerId) {
|
|
240
247
|
clearTimeout(idleTimerId);
|
|
@@ -253,7 +260,7 @@ export async function executeCli(command, args, options = {}) {
|
|
|
253
260
|
const result = {
|
|
254
261
|
stdout,
|
|
255
262
|
stderr: stderr + `\nProcess timed out after ${timeoutMs}ms`,
|
|
256
|
-
code: 124 // Standard timeout exit code
|
|
263
|
+
code: 124, // Standard timeout exit code
|
|
257
264
|
};
|
|
258
265
|
const error = new Error(result.stderr);
|
|
259
266
|
error.code = 124;
|
|
@@ -265,7 +272,7 @@ export async function executeCli(command, args, options = {}) {
|
|
|
265
272
|
const result = {
|
|
266
273
|
stdout,
|
|
267
274
|
stderr: stderr + `\nProcess killed after ${idleMs}ms of inactivity`,
|
|
268
|
-
code: 125
|
|
275
|
+
code: 125,
|
|
269
276
|
};
|
|
270
277
|
const error = new Error(result.stderr);
|
|
271
278
|
error.code = 125;
|
|
@@ -283,7 +290,7 @@ export async function executeCli(command, args, options = {}) {
|
|
|
283
290
|
}
|
|
284
291
|
resolve(result);
|
|
285
292
|
});
|
|
286
|
-
proc.on("error",
|
|
293
|
+
proc.on("error", err => {
|
|
287
294
|
exited = true;
|
|
288
295
|
if (idleTimerId) {
|
|
289
296
|
clearTimeout(idleTimerId);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface FlightLogStart {
|
|
2
|
+
correlationId: string;
|
|
3
|
+
cli: "claude" | "codex" | "gemini" | "grok";
|
|
4
|
+
model: string;
|
|
5
|
+
prompt: string;
|
|
6
|
+
system?: string;
|
|
7
|
+
sessionId?: string;
|
|
8
|
+
asyncJobId?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface FlightLogResult {
|
|
11
|
+
response: string;
|
|
12
|
+
inputTokens?: number;
|
|
13
|
+
outputTokens?: number;
|
|
14
|
+
durationMs: number;
|
|
15
|
+
retryCount: number;
|
|
16
|
+
circuitBreakerState: string;
|
|
17
|
+
costUsd?: number;
|
|
18
|
+
approvalDecision?: string;
|
|
19
|
+
optimizationApplied: boolean;
|
|
20
|
+
thinkingBlocks?: string[];
|
|
21
|
+
exitCode: number;
|
|
22
|
+
errorMessage?: string;
|
|
23
|
+
status: "completed" | "failed";
|
|
24
|
+
}
|
|
25
|
+
interface LoggerLike {
|
|
26
|
+
info: (message: string, ...args: any[]) => void;
|
|
27
|
+
error: (message: string, ...args: any[]) => void;
|
|
28
|
+
}
|
|
29
|
+
export declare function resolveFlightRecorderDbPath(): string | null;
|
|
30
|
+
export declare class FlightRecorder {
|
|
31
|
+
private db;
|
|
32
|
+
private insertStartTxn;
|
|
33
|
+
private updateCompleteTxn;
|
|
34
|
+
constructor(dbPath: string);
|
|
35
|
+
logStart(entry: FlightLogStart): void;
|
|
36
|
+
logComplete(correlationId: string, result: FlightLogResult): void;
|
|
37
|
+
flush(): void;
|
|
38
|
+
close(): void;
|
|
39
|
+
}
|
|
40
|
+
export declare class NoopFlightRecorder {
|
|
41
|
+
logStart(_entry: FlightLogStart): void;
|
|
42
|
+
logComplete(_correlationId: string, _result: FlightLogResult): void;
|
|
43
|
+
flush(): void;
|
|
44
|
+
close(): void;
|
|
45
|
+
}
|
|
46
|
+
export type FlightRecorderLike = FlightRecorder | NoopFlightRecorder;
|
|
47
|
+
export declare function createFlightRecorder(logger: LoggerLike): FlightRecorderLike;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
const MAX_THINKING_BYTES = 1_000_000;
|
|
6
|
+
export function resolveFlightRecorderDbPath() {
|
|
7
|
+
const configured = process.env.LLM_GATEWAY_LOGS_DB;
|
|
8
|
+
if (configured !== undefined) {
|
|
9
|
+
const normalized = configured.trim().toLowerCase();
|
|
10
|
+
if (!normalized || normalized === "none") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return configured.trim();
|
|
14
|
+
}
|
|
15
|
+
return path.join(os.homedir(), ".llm-cli-gateway", "logs.db");
|
|
16
|
+
}
|
|
17
|
+
const TRUNCATION_SUFFIX = "[TRUNCATED]";
|
|
18
|
+
const TRUNCATION_SUFFIX_BYTES = Buffer.byteLength(TRUNCATION_SUFFIX, "utf8");
|
|
19
|
+
function truncateThinkingBlocks(blocks) {
|
|
20
|
+
const result = [];
|
|
21
|
+
let used = 0;
|
|
22
|
+
for (const block of blocks) {
|
|
23
|
+
const bytes = Buffer.byteLength(block, "utf8");
|
|
24
|
+
if (used + bytes <= MAX_THINKING_BYTES) {
|
|
25
|
+
result.push(block);
|
|
26
|
+
used += bytes;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
// Reserve space for the suffix so total stays within budget
|
|
30
|
+
const budget = Math.max(0, MAX_THINKING_BYTES - used - TRUNCATION_SUFFIX_BYTES);
|
|
31
|
+
if (budget > 0) {
|
|
32
|
+
// Truncate on code point boundaries by using string iteration
|
|
33
|
+
let charBytes = 0;
|
|
34
|
+
let safeEnd = 0;
|
|
35
|
+
for (const char of block) {
|
|
36
|
+
const charSize = Buffer.byteLength(char, "utf8");
|
|
37
|
+
if (charBytes + charSize > budget)
|
|
38
|
+
break;
|
|
39
|
+
charBytes += charSize;
|
|
40
|
+
safeEnd += char.length; // char.length handles surrogate pairs
|
|
41
|
+
}
|
|
42
|
+
const sliced = block.slice(0, safeEnd);
|
|
43
|
+
result.push(sliced ? `${sliced}${TRUNCATION_SUFFIX}` : TRUNCATION_SUFFIX);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
result.push(TRUNCATION_SUFFIX);
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
export class FlightRecorder {
|
|
53
|
+
db;
|
|
54
|
+
insertStartTxn;
|
|
55
|
+
updateCompleteTxn;
|
|
56
|
+
constructor(dbPath) {
|
|
57
|
+
const require = createRequire(import.meta.url);
|
|
58
|
+
const BetterSqlite3 = require("better-sqlite3");
|
|
59
|
+
const directory = path.dirname(dbPath);
|
|
60
|
+
if (!existsSync(directory)) {
|
|
61
|
+
mkdirSync(directory, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
this.db = new BetterSqlite3(dbPath);
|
|
64
|
+
this.db.pragma("journal_mode = WAL");
|
|
65
|
+
this.db.pragma("foreign_keys = ON");
|
|
66
|
+
this.db.exec(`
|
|
67
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
68
|
+
version INTEGER PRIMARY KEY,
|
|
69
|
+
applied_at TEXT NOT NULL
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE TABLE IF NOT EXISTS requests (
|
|
73
|
+
id TEXT PRIMARY KEY,
|
|
74
|
+
cli TEXT NOT NULL,
|
|
75
|
+
model TEXT NOT NULL,
|
|
76
|
+
prompt TEXT NOT NULL,
|
|
77
|
+
system TEXT,
|
|
78
|
+
response TEXT,
|
|
79
|
+
session_id TEXT,
|
|
80
|
+
duration_ms INTEGER,
|
|
81
|
+
datetime_utc TEXT NOT NULL,
|
|
82
|
+
input_tokens INTEGER,
|
|
83
|
+
output_tokens INTEGER
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE TABLE IF NOT EXISTS gateway_metadata (
|
|
87
|
+
request_id TEXT PRIMARY KEY REFERENCES requests(id),
|
|
88
|
+
retry_count INTEGER DEFAULT 0,
|
|
89
|
+
circuit_breaker_state TEXT,
|
|
90
|
+
cost_usd REAL,
|
|
91
|
+
approval_decision TEXT,
|
|
92
|
+
optimization_applied INTEGER DEFAULT 0,
|
|
93
|
+
thinking_blocks TEXT,
|
|
94
|
+
exit_code INTEGER,
|
|
95
|
+
error_message TEXT,
|
|
96
|
+
async_job_id TEXT,
|
|
97
|
+
status TEXT NOT NULL DEFAULT 'started'
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_requests_datetime ON requests(datetime_utc);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_requests_model ON requests(model);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_requests_cli ON requests(cli);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_metadata_status ON gateway_metadata(status);
|
|
105
|
+
`);
|
|
106
|
+
this.db
|
|
107
|
+
.prepare("INSERT OR IGNORE INTO _migrations(version, applied_at) VALUES(1, ?)")
|
|
108
|
+
.run(new Date().toISOString());
|
|
109
|
+
if (process.platform !== "win32") {
|
|
110
|
+
try {
|
|
111
|
+
chmodSync(dbPath, 0o600);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Best effort permissions hardening.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const insertRequest = this.db.prepare(`
|
|
118
|
+
INSERT INTO requests (id, cli, model, prompt, system, session_id, datetime_utc)
|
|
119
|
+
VALUES (@id, @cli, @model, @prompt, @system, @session_id, @datetime_utc)
|
|
120
|
+
`);
|
|
121
|
+
const insertMetadata = this.db.prepare(`
|
|
122
|
+
INSERT INTO gateway_metadata (request_id, async_job_id, status)
|
|
123
|
+
VALUES (@request_id, @async_job_id, 'started')
|
|
124
|
+
`);
|
|
125
|
+
this.insertStartTxn = this.db.transaction((entry) => {
|
|
126
|
+
insertRequest.run({
|
|
127
|
+
id: entry.correlationId,
|
|
128
|
+
cli: entry.cli,
|
|
129
|
+
model: entry.model,
|
|
130
|
+
prompt: entry.prompt,
|
|
131
|
+
system: entry.system || null,
|
|
132
|
+
session_id: entry.sessionId || null,
|
|
133
|
+
datetime_utc: new Date().toISOString(),
|
|
134
|
+
});
|
|
135
|
+
insertMetadata.run({
|
|
136
|
+
request_id: entry.correlationId,
|
|
137
|
+
async_job_id: entry.asyncJobId || null,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
const updateRequests = this.db.prepare(`
|
|
141
|
+
UPDATE requests
|
|
142
|
+
SET response = @response,
|
|
143
|
+
duration_ms = @duration_ms,
|
|
144
|
+
input_tokens = @input_tokens,
|
|
145
|
+
output_tokens = @output_tokens
|
|
146
|
+
WHERE id = @id
|
|
147
|
+
`);
|
|
148
|
+
const updateMetadata = this.db.prepare(`
|
|
149
|
+
UPDATE gateway_metadata
|
|
150
|
+
SET retry_count = @retry_count,
|
|
151
|
+
circuit_breaker_state = @circuit_breaker_state,
|
|
152
|
+
cost_usd = @cost_usd,
|
|
153
|
+
approval_decision = @approval_decision,
|
|
154
|
+
optimization_applied = @optimization_applied,
|
|
155
|
+
thinking_blocks = @thinking_blocks,
|
|
156
|
+
exit_code = @exit_code,
|
|
157
|
+
error_message = @error_message,
|
|
158
|
+
status = @status
|
|
159
|
+
WHERE request_id = @id AND status = 'started'
|
|
160
|
+
`);
|
|
161
|
+
this.updateCompleteTxn = this.db.transaction((correlationId, result) => {
|
|
162
|
+
const thinkingBlocks = result.thinkingBlocks && result.thinkingBlocks.length > 0
|
|
163
|
+
? JSON.stringify(truncateThinkingBlocks(result.thinkingBlocks))
|
|
164
|
+
: null;
|
|
165
|
+
updateRequests.run({
|
|
166
|
+
id: correlationId,
|
|
167
|
+
response: result.response,
|
|
168
|
+
duration_ms: result.durationMs,
|
|
169
|
+
input_tokens: result.inputTokens ?? null,
|
|
170
|
+
output_tokens: result.outputTokens ?? null,
|
|
171
|
+
});
|
|
172
|
+
updateMetadata.run({
|
|
173
|
+
id: correlationId,
|
|
174
|
+
retry_count: result.retryCount,
|
|
175
|
+
circuit_breaker_state: result.circuitBreakerState,
|
|
176
|
+
cost_usd: result.costUsd ?? null,
|
|
177
|
+
approval_decision: result.approvalDecision ?? null,
|
|
178
|
+
optimization_applied: result.optimizationApplied ? 1 : 0,
|
|
179
|
+
thinking_blocks: thinkingBlocks,
|
|
180
|
+
exit_code: result.exitCode,
|
|
181
|
+
error_message: result.errorMessage ?? null,
|
|
182
|
+
status: result.status,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
logStart(entry) {
|
|
187
|
+
this.insertStartTxn(entry);
|
|
188
|
+
}
|
|
189
|
+
logComplete(correlationId, result) {
|
|
190
|
+
this.updateCompleteTxn(correlationId, result);
|
|
191
|
+
}
|
|
192
|
+
flush() {
|
|
193
|
+
// No-op: better-sqlite3 writes synchronously.
|
|
194
|
+
}
|
|
195
|
+
close() {
|
|
196
|
+
this.db.close();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export class NoopFlightRecorder {
|
|
200
|
+
logStart(_entry) { }
|
|
201
|
+
logComplete(_correlationId, _result) { }
|
|
202
|
+
flush() { }
|
|
203
|
+
close() { }
|
|
204
|
+
}
|
|
205
|
+
export function createFlightRecorder(logger) {
|
|
206
|
+
const dbPath = resolveFlightRecorderDbPath();
|
|
207
|
+
if (!dbPath) {
|
|
208
|
+
logger.info("Flight recorder disabled (LLM_GATEWAY_LOGS_DB=none)");
|
|
209
|
+
return new NoopFlightRecorder();
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const recorder = new FlightRecorder(dbPath);
|
|
213
|
+
logger.info(`Flight recorder enabled at ${dbPath}`);
|
|
214
|
+
return recorder;
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
logger.error("Flight recorder unavailable; continuing without SQLite logging", error);
|
|
218
|
+
return new NoopFlightRecorder();
|
|
219
|
+
}
|
|
220
|
+
}
|
package/dist/health.js
CHANGED
|
@@ -10,13 +10,13 @@ export async function checkHealth(db) {
|
|
|
10
10
|
status: "unhealthy",
|
|
11
11
|
postgres: {
|
|
12
12
|
status: result.postgres.connected ? "up" : "down",
|
|
13
|
-
latency: result.postgres.latency
|
|
13
|
+
latency: result.postgres.latency,
|
|
14
14
|
},
|
|
15
15
|
redis: {
|
|
16
16
|
status: result.redis.connected ? "up" : "down",
|
|
17
|
-
latency: result.redis.latency
|
|
17
|
+
latency: result.redis.latency,
|
|
18
18
|
},
|
|
19
|
-
timestamp: new Date().toISOString()
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
20
|
};
|
|
21
21
|
// Determine overall health status
|
|
22
22
|
if (result.postgres.connected && result.redis.connected) {
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ type ExtendedToolResponse = {
|
|
|
12
12
|
isError?: boolean;
|
|
13
13
|
sessionId?: string;
|
|
14
14
|
resumable?: boolean;
|
|
15
|
+
structuredContent?: Record<string, unknown>;
|
|
15
16
|
approval?: ApprovalRecord | null;
|
|
16
17
|
mcpServers?: {
|
|
17
18
|
requested: ClaudeMcpServerName[];
|
|
@@ -36,6 +37,7 @@ export interface GeminiRequestParams {
|
|
|
36
37
|
optimizePrompt: boolean;
|
|
37
38
|
optimizeResponse?: boolean;
|
|
38
39
|
idleTimeoutMs?: number;
|
|
40
|
+
forceRefresh?: boolean;
|
|
39
41
|
}
|
|
40
42
|
export interface HandlerDeps {
|
|
41
43
|
sessionManager: ISessionManager;
|
|
@@ -50,6 +52,30 @@ export interface AsyncHandlerDeps extends HandlerDeps {
|
|
|
50
52
|
}
|
|
51
53
|
export declare function handleGeminiRequest(deps: HandlerDeps, params: GeminiRequestParams): Promise<ExtendedToolResponse>;
|
|
52
54
|
export declare function handleGeminiRequestAsync(deps: AsyncHandlerDeps, params: Omit<GeminiRequestParams, "optimizeResponse">): Promise<ExtendedToolResponse>;
|
|
55
|
+
export interface GrokRequestParams {
|
|
56
|
+
prompt: string;
|
|
57
|
+
model?: string;
|
|
58
|
+
outputFormat?: string;
|
|
59
|
+
sessionId?: string;
|
|
60
|
+
resumeLatest: boolean;
|
|
61
|
+
createNewSession: boolean;
|
|
62
|
+
alwaysApprove?: boolean;
|
|
63
|
+
permissionMode?: string;
|
|
64
|
+
effort?: string;
|
|
65
|
+
reasoningEffort?: string;
|
|
66
|
+
approvalStrategy: "legacy" | "mcp_managed";
|
|
67
|
+
approvalPolicy?: string;
|
|
68
|
+
mcpServers?: ClaudeMcpServerName[];
|
|
69
|
+
allowedTools?: string[];
|
|
70
|
+
disallowedTools?: string[];
|
|
71
|
+
correlationId?: string;
|
|
72
|
+
optimizePrompt: boolean;
|
|
73
|
+
optimizeResponse?: boolean;
|
|
74
|
+
idleTimeoutMs?: number;
|
|
75
|
+
forceRefresh?: boolean;
|
|
76
|
+
}
|
|
77
|
+
export declare function handleGrokRequest(deps: HandlerDeps, params: GrokRequestParams): Promise<ExtendedToolResponse>;
|
|
78
|
+
export declare function handleGrokRequestAsync(deps: AsyncHandlerDeps, params: Omit<GrokRequestParams, "optimizeResponse">): Promise<ExtendedToolResponse>;
|
|
53
79
|
export declare function handleCodexRequestAsync(deps: AsyncHandlerDeps, params: {
|
|
54
80
|
prompt: string;
|
|
55
81
|
model?: string;
|
|
@@ -59,9 +85,11 @@ export declare function handleCodexRequestAsync(deps: AsyncHandlerDeps, params:
|
|
|
59
85
|
approvalPolicy?: string;
|
|
60
86
|
mcpServers?: ClaudeMcpServerName[];
|
|
61
87
|
sessionId?: string;
|
|
88
|
+
resumeLatest?: boolean;
|
|
62
89
|
createNewSession: boolean;
|
|
63
90
|
correlationId?: string;
|
|
64
91
|
optimizePrompt: boolean;
|
|
65
92
|
idleTimeoutMs?: number;
|
|
93
|
+
forceRefresh?: boolean;
|
|
66
94
|
}): Promise<ExtendedToolResponse>;
|
|
67
95
|
export {};
|