llm-cli-gateway 2.3.0 → 2.5.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 +79 -9
- package/README.md +3 -1
- package/dist/auth.d.ts +44 -1
- package/dist/auth.js +60 -13
- package/dist/config.d.ts +19 -0
- package/dist/config.js +235 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +22 -11
- package/dist/executor.js +17 -21
- package/dist/flight-recorder.d.ts +2 -1
- package/dist/http-transport.js +74 -12
- package/dist/index.d.ts +42 -7
- package/dist/index.js +1161 -82
- package/dist/metrics.d.ts +3 -3
- package/dist/metrics.js +8 -8
- package/dist/oauth.d.ts +38 -0
- package/dist/oauth.js +441 -0
- package/dist/request-context.d.ts +7 -0
- package/dist/request-context.js +8 -0
- package/dist/request-helpers.d.ts +8 -8
- package/dist/resources.js +56 -7
- package/dist/session-manager-pg.d.ts +6 -6
- package/dist/session-manager-pg.js +1 -0
- package/dist/session-manager.d.ts +16 -12
- package/dist/session-manager.js +4 -1
- package/dist/upstream-contracts.d.ts +84 -0
- package/dist/upstream-contracts.js +714 -6
- package/dist/workspace-registry.d.ts +63 -0
- package/dist/workspace-registry.js +417 -0
- package/dist/xai-api-provider.d.ts +43 -0
- package/dist/xai-api-provider.js +191 -0
- package/migrations/001_initial_schema.sql +65 -0
- package/migrations/002_session_ids_as_text.sql +26 -0
- package/migrations/003_provider_type_sessions.sql +20 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
- package/setup/status.schema.json +42 -1
package/dist/executor.js
CHANGED
|
@@ -269,30 +269,26 @@ export function killAllProcessGroups() {
|
|
|
269
269
|
});
|
|
270
270
|
}
|
|
271
271
|
export function killProcessGroup(proc, signal) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return true;
|
|
279
|
-
}
|
|
280
|
-
catch (err) {
|
|
281
|
-
if (err.code !== "ESRCH") {
|
|
282
|
-
try {
|
|
283
|
-
return proc.kill(signal);
|
|
284
|
-
}
|
|
285
|
-
catch {
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
return false;
|
|
290
|
-
}
|
|
272
|
+
const pid = proc.pid;
|
|
273
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
if (process.platform === "win32") {
|
|
277
|
+
return killWindowsProcessTree(pid);
|
|
291
278
|
}
|
|
292
279
|
try {
|
|
293
|
-
|
|
280
|
+
process.kill(-pid, signal);
|
|
281
|
+
return true;
|
|
294
282
|
}
|
|
295
|
-
catch {
|
|
283
|
+
catch (err) {
|
|
284
|
+
if (err.code !== "ESRCH") {
|
|
285
|
+
try {
|
|
286
|
+
return proc.kill(signal);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
296
292
|
return false;
|
|
297
293
|
}
|
|
298
294
|
}
|
package/dist/http-transport.js
CHANGED
|
@@ -3,31 +3,42 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
4
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { authorizeBearerRequest, getRequiredBearerToken, writeAuthFailure } from "./auth.js";
|
|
6
|
+
import { loadRemoteOAuthConfig } from "./config.js";
|
|
7
|
+
import { OAuthServer, oauthBaseUrlFromRequest } from "./oauth.js";
|
|
8
|
+
import { runWithRequestContext } from "./request-context.js";
|
|
6
9
|
const noopLogger = {
|
|
7
10
|
info: (..._args) => { },
|
|
8
11
|
error: (..._args) => { },
|
|
9
12
|
debug: (..._args) => { },
|
|
10
13
|
};
|
|
11
|
-
function
|
|
14
|
+
function firstHeader(value) {
|
|
15
|
+
return Array.isArray(value) ? value[0] : value;
|
|
16
|
+
}
|
|
17
|
+
function readRawBody(req) {
|
|
12
18
|
return new Promise((resolve, reject) => {
|
|
13
19
|
const chunks = [];
|
|
14
20
|
req.on("data", chunk => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
15
21
|
req.on("error", reject);
|
|
16
22
|
req.on("end", () => {
|
|
17
23
|
if (chunks.length === 0) {
|
|
18
|
-
resolve(
|
|
24
|
+
resolve("");
|
|
19
25
|
return;
|
|
20
26
|
}
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
resolve(JSON.parse(raw));
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
reject(error);
|
|
27
|
-
}
|
|
27
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
+
async function readBody(req) {
|
|
32
|
+
const raw = await readRawBody(req);
|
|
33
|
+
if (!raw)
|
|
34
|
+
return undefined;
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
31
42
|
function methodNotAllowed(res) {
|
|
32
43
|
res.writeHead(405, { allow: "GET, POST, DELETE", "content-type": "application/json" });
|
|
33
44
|
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
@@ -36,6 +47,10 @@ function jsonError(res, status, message) {
|
|
|
36
47
|
res.writeHead(status, { "content-type": "application/json" });
|
|
37
48
|
res.end(JSON.stringify({ error: message }));
|
|
38
49
|
}
|
|
50
|
+
function jsonResponse(res, status, body) {
|
|
51
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
52
|
+
res.end(JSON.stringify(body));
|
|
53
|
+
}
|
|
39
54
|
function parseNoAuthPaths(raw, protectedPath) {
|
|
40
55
|
const paths = new Set();
|
|
41
56
|
for (const value of (raw ?? "").split(/[,;\s]+/)) {
|
|
@@ -51,6 +66,25 @@ function parseNoAuthPaths(raw, protectedPath) {
|
|
|
51
66
|
}
|
|
52
67
|
return paths;
|
|
53
68
|
}
|
|
69
|
+
function isLocalHost(host) {
|
|
70
|
+
const hostname = host.split(":")[0]?.toLowerCase() ?? "";
|
|
71
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
72
|
+
}
|
|
73
|
+
function requestBaseUrl(req) {
|
|
74
|
+
const configured = process.env.LLM_GATEWAY_PUBLIC_URL;
|
|
75
|
+
if (configured) {
|
|
76
|
+
try {
|
|
77
|
+
return new URL(configured).origin;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const host = firstHeader(req.headers.host) ?? "127.0.0.1:3333";
|
|
83
|
+
const forwardedProto = firstHeader(req.headers["x-forwarded-proto"]);
|
|
84
|
+
const proto = forwardedProto ??
|
|
85
|
+
(host.startsWith("127.0.0.1") || host.startsWith("localhost") ? "http" : "https");
|
|
86
|
+
return `${proto}://${host}`;
|
|
87
|
+
}
|
|
54
88
|
export async function startHttpGateway(options) {
|
|
55
89
|
const host = options.host ?? process.env.LLM_GATEWAY_HTTP_HOST ?? "127.0.0.1";
|
|
56
90
|
const port = options.port ?? Number(process.env.LLM_GATEWAY_HTTP_PORT ?? 3333);
|
|
@@ -59,6 +93,10 @@ export async function startHttpGateway(options) {
|
|
|
59
93
|
const logger = options.logger ?? noopLogger;
|
|
60
94
|
const sessions = new Map();
|
|
61
95
|
const token = getRequiredBearerToken();
|
|
96
|
+
const oauthConfig = loadRemoteOAuthConfig(logger);
|
|
97
|
+
const oauthServer = oauthConfig.enabled
|
|
98
|
+
? new OAuthServer({ protectedPath: path, config: oauthConfig, logger })
|
|
99
|
+
: null;
|
|
62
100
|
async function closeSession(sessionId) {
|
|
63
101
|
const entry = sessions.get(sessionId);
|
|
64
102
|
if (!entry)
|
|
@@ -89,22 +127,46 @@ export async function startHttpGateway(options) {
|
|
|
89
127
|
const httpServer = createServer(async (req, res) => {
|
|
90
128
|
try {
|
|
91
129
|
const url = new URL(req.url || "/", `http://${req.headers.host || `${host}:${port}`}`);
|
|
130
|
+
const baseUrl = requestBaseUrl(req);
|
|
131
|
+
const oauthOrigin = oauthServer ? oauthBaseUrlFromRequest(req, oauthConfig) : null;
|
|
132
|
+
const effectiveOAuthBaseUrl = oauthOrigin ?? baseUrl;
|
|
133
|
+
const resourceMetadataUrl = oauthServer && oauthOrigin ? oauthServer.resourceMetadataUrl(oauthOrigin) : undefined;
|
|
92
134
|
if (url.pathname === "/healthz") {
|
|
93
135
|
res.writeHead(200, { "content-type": "application/json" });
|
|
94
136
|
res.end(JSON.stringify({ ok: true, sessions: sessions.size }));
|
|
95
137
|
return;
|
|
96
138
|
}
|
|
139
|
+
if (oauthServer) {
|
|
140
|
+
if (oauthServer.isOAuthPath(url.pathname) && !oauthOrigin) {
|
|
141
|
+
jsonError(res, 503, "LLM_GATEWAY_PUBLIC_URL is required for public OAuth issuer metadata");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (await oauthServer.handle({
|
|
145
|
+
req,
|
|
146
|
+
res,
|
|
147
|
+
url,
|
|
148
|
+
baseUrl: effectiveOAuthBaseUrl,
|
|
149
|
+
})) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
97
153
|
const noAuthPath = noAuthPaths.has(url.pathname);
|
|
98
154
|
if (url.pathname !== path && !noAuthPath) {
|
|
99
155
|
jsonError(res, 404, "Not found");
|
|
100
156
|
return;
|
|
101
157
|
}
|
|
158
|
+
let requestContext = { authScopes: [] };
|
|
102
159
|
if (!noAuthPath) {
|
|
103
160
|
const auth = authorizeBearerRequest(req, token);
|
|
104
161
|
if (!auth.ok) {
|
|
105
|
-
writeAuthFailure(res, auth);
|
|
162
|
+
writeAuthFailure(res, auth, resourceMetadataUrl ? { resourceMetadataUrl } : {});
|
|
106
163
|
return;
|
|
107
164
|
}
|
|
165
|
+
requestContext = {
|
|
166
|
+
authKind: auth.kind,
|
|
167
|
+
authScopes: auth.scopes ?? [],
|
|
168
|
+
authClientId: auth.clientId,
|
|
169
|
+
};
|
|
108
170
|
}
|
|
109
171
|
if (req.method !== "GET" && req.method !== "POST" && req.method !== "DELETE") {
|
|
110
172
|
methodNotAllowed(res);
|
|
@@ -129,7 +191,7 @@ export async function startHttpGateway(options) {
|
|
|
129
191
|
return;
|
|
130
192
|
}
|
|
131
193
|
const body = req.method === "POST" ? await readBody(req) : undefined;
|
|
132
|
-
await entry.transport.handleRequest(req, res, body);
|
|
194
|
+
await runWithRequestContext(requestContext, () => entry.transport.handleRequest(req, res, body));
|
|
133
195
|
return;
|
|
134
196
|
}
|
|
135
197
|
if (req.method !== "POST") {
|
|
@@ -146,7 +208,7 @@ export async function startHttpGateway(options) {
|
|
|
146
208
|
return;
|
|
147
209
|
}
|
|
148
210
|
const entry = await createSession();
|
|
149
|
-
await entry.transport.handleRequest(req, res, body);
|
|
211
|
+
await runWithRequestContext(requestContext, () => entry.transport.handleRequest(req, res, body));
|
|
150
212
|
}
|
|
151
213
|
catch (error) {
|
|
152
214
|
logger.error("HTTP transport request failed", error);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { z } from "zod/v3";
|
|
4
|
-
import { ISessionManager } from "./session-manager.js";
|
|
4
|
+
import { ISessionManager, type ProviderType } from "./session-manager.js";
|
|
5
5
|
import { ResourceProvider } from "./resources.js";
|
|
6
6
|
import { PerformanceMetrics } from "./metrics.js";
|
|
7
|
-
import { type PersistenceConfig, type CacheAwarenessConfig } from "./config.js";
|
|
7
|
+
import { type PersistenceConfig, type CacheAwarenessConfig, type ProvidersConfig } from "./config.js";
|
|
8
|
+
import { type XaiReasoningEffort } from "./xai-api-provider.js";
|
|
8
9
|
import { DatabaseConnection } from "./db.js";
|
|
9
10
|
import { AsyncJobManager } from "./async-job-manager.js";
|
|
10
11
|
import { ApprovalManager, ApprovalRecord } from "./approval-manager.js";
|
|
@@ -13,6 +14,7 @@ import { ClaudeMcpConfigResult, ClaudeMcpServerName } from "./claude-mcp-config.
|
|
|
13
14
|
import { type MistralAgentMode, type ClaudePermissionMode, type CodexSandboxMode, type CodexAskForApproval, type ClaudeEffortLevel } from "./request-helpers.js";
|
|
14
15
|
import { FlightRecorderLike } from "./flight-recorder.js";
|
|
15
16
|
import { type PromptParts } from "./prompt-parts.js";
|
|
17
|
+
import { type WorkspaceRegistry } from "./workspace-registry.js";
|
|
16
18
|
export interface WarningEntry {
|
|
17
19
|
code: string;
|
|
18
20
|
message?: string;
|
|
@@ -44,7 +46,7 @@ declare const logger: {
|
|
|
44
46
|
debug: (message: string, ...args: any[]) => void;
|
|
45
47
|
};
|
|
46
48
|
type GatewayLogger = typeof logger;
|
|
47
|
-
export declare function buildServerInstructions(asyncJobsEnabled: boolean): string;
|
|
49
|
+
export declare function buildServerInstructions(asyncJobsEnabled: boolean, grokApiToolsEnabled?: boolean): string;
|
|
48
50
|
export declare const MAX_TURNS_SCHEMA: z.ZodNumber;
|
|
49
51
|
export declare const MAX_TOKENS_SCHEMA: z.ZodNumber;
|
|
50
52
|
export declare const MAX_PRICE_SCHEMA: z.ZodNumber;
|
|
@@ -58,9 +60,10 @@ export declare const WORKTREE_SCHEMA: z.ZodUnion<[z.ZodBoolean, z.ZodObject<{
|
|
|
58
60
|
name?: string | undefined;
|
|
59
61
|
ref?: string | undefined;
|
|
60
62
|
}>]>;
|
|
61
|
-
export declare const
|
|
62
|
-
export declare const
|
|
63
|
-
export
|
|
63
|
+
export declare const WORKSPACE_ALIAS_SCHEMA: z.ZodString;
|
|
64
|
+
export declare const SESSION_PROVIDER_VALUES: readonly ["claude", "codex", "gemini", "grok", "mistral", "grok-api"];
|
|
65
|
+
export declare const SESSION_PROVIDER_ENUM: z.ZodEnum<["claude", "codex", "gemini", "grok", "mistral", "grok-api"]>;
|
|
66
|
+
export type SessionProvider = ProviderType;
|
|
64
67
|
export interface GatewayServerDeps {
|
|
65
68
|
sessionManager?: ISessionManager;
|
|
66
69
|
resourceProvider?: ResourceProvider;
|
|
@@ -72,6 +75,8 @@ export interface GatewayServerDeps {
|
|
|
72
75
|
logger?: GatewayLogger;
|
|
73
76
|
persistence?: PersistenceConfig;
|
|
74
77
|
cacheAwareness?: CacheAwarenessConfig;
|
|
78
|
+
providers?: ProvidersConfig;
|
|
79
|
+
workspaces?: WorkspaceRegistry;
|
|
75
80
|
}
|
|
76
81
|
export interface GatewayServerRuntime {
|
|
77
82
|
sessionManager: ISessionManager;
|
|
@@ -84,18 +89,27 @@ export interface GatewayServerRuntime {
|
|
|
84
89
|
logger: GatewayLogger;
|
|
85
90
|
persistence: PersistenceConfig;
|
|
86
91
|
cacheAwareness: CacheAwarenessConfig;
|
|
92
|
+
providers: ProvidersConfig;
|
|
93
|
+
workspaces: WorkspaceRegistry;
|
|
87
94
|
}
|
|
88
95
|
export declare function resolveGatewayServerRuntime(deps?: GatewayServerDeps, options?: {
|
|
89
96
|
isolateState?: boolean;
|
|
90
97
|
}): GatewayServerRuntime;
|
|
98
|
+
export declare function shouldRegisterGrokApiTools(providers: ProvidersConfig): boolean;
|
|
91
99
|
export interface ResolvedWorktree {
|
|
92
100
|
cwd?: string;
|
|
93
101
|
worktreePath?: string;
|
|
102
|
+
workspaceAlias?: string;
|
|
103
|
+
workspaceRoot?: string;
|
|
94
104
|
}
|
|
95
105
|
export declare function resolveWorktreeForRequest(worktreeOpt: boolean | {
|
|
96
106
|
name?: string;
|
|
97
107
|
ref?: string;
|
|
98
|
-
} | undefined, sessionId: string | undefined, runtime: GatewayServerRuntime
|
|
108
|
+
} | undefined, sessionId: string | undefined, runtime: GatewayServerRuntime, options?: {
|
|
109
|
+
repoRoot?: string;
|
|
110
|
+
workspaceAlias?: string;
|
|
111
|
+
workspaceRoot?: string;
|
|
112
|
+
}): Promise<ResolvedWorktree>;
|
|
99
113
|
export declare function formatWorktreePrefix(worktreePath?: string): string;
|
|
100
114
|
export declare function extractUsageAndCost(cli: "claude" | "codex" | "gemini" | "grok" | "mistral", output: string, outputFormat?: string, ctx?: {
|
|
101
115
|
sessionId?: string;
|
|
@@ -285,6 +299,22 @@ export declare function buildMistralRetryPrep(params: Pick<MistralRequestParams,
|
|
|
285
299
|
env: Record<string, string>;
|
|
286
300
|
ignoredDisallowedTools: boolean;
|
|
287
301
|
};
|
|
302
|
+
export interface GrokApiRequestParams {
|
|
303
|
+
prompt?: string;
|
|
304
|
+
promptParts?: PromptParts;
|
|
305
|
+
model?: string;
|
|
306
|
+
sessionId?: string;
|
|
307
|
+
createNewSession?: boolean;
|
|
308
|
+
correlationId?: string;
|
|
309
|
+
optimizePrompt: boolean;
|
|
310
|
+
optimizeResponse?: boolean;
|
|
311
|
+
maxOutputTokens?: number;
|
|
312
|
+
temperature?: number;
|
|
313
|
+
topP?: number;
|
|
314
|
+
reasoningEffort?: XaiReasoningEffort;
|
|
315
|
+
timeoutMs?: number;
|
|
316
|
+
}
|
|
317
|
+
export declare function handleGrokApiRequest(deps: HandlerDeps, params: GrokApiRequestParams): Promise<ExtendedToolResponse>;
|
|
288
318
|
export interface GeminiRequestParams {
|
|
289
319
|
prompt?: string;
|
|
290
320
|
promptParts?: PromptParts;
|
|
@@ -310,6 +340,7 @@ export interface GeminiRequestParams {
|
|
|
310
340
|
attachments?: string[];
|
|
311
341
|
skipTrust?: boolean;
|
|
312
342
|
yolo?: boolean;
|
|
343
|
+
workspace?: string;
|
|
313
344
|
worktree?: boolean | {
|
|
314
345
|
name?: string;
|
|
315
346
|
ref?: string;
|
|
@@ -323,6 +354,7 @@ export interface HandlerDeps {
|
|
|
323
354
|
error: (...args: any[]) => void;
|
|
324
355
|
debug: (...args: any[]) => void;
|
|
325
356
|
};
|
|
357
|
+
workspaces?: WorkspaceRegistry;
|
|
326
358
|
runtime?: GatewayServerRuntime;
|
|
327
359
|
}
|
|
328
360
|
export interface AsyncHandlerDeps extends HandlerDeps {
|
|
@@ -380,6 +412,7 @@ export interface GrokRequestParams {
|
|
|
380
412
|
restoreCode?: boolean;
|
|
381
413
|
leaderSocket?: string;
|
|
382
414
|
nativeWorktree?: boolean | string;
|
|
415
|
+
workspace?: string;
|
|
383
416
|
worktree?: boolean | {
|
|
384
417
|
name?: string;
|
|
385
418
|
ref?: string;
|
|
@@ -412,6 +445,7 @@ export interface MistralRequestParams {
|
|
|
412
445
|
maxTokens?: number;
|
|
413
446
|
workingDir?: string;
|
|
414
447
|
addDir?: string[];
|
|
448
|
+
workspace?: string;
|
|
415
449
|
worktree?: boolean | {
|
|
416
450
|
name?: string;
|
|
417
451
|
ref?: string;
|
|
@@ -449,6 +483,7 @@ export declare function handleCodexRequestAsync(deps: AsyncHandlerDeps, params:
|
|
|
449
483
|
ignoreRules?: boolean;
|
|
450
484
|
workingDir?: string;
|
|
451
485
|
addDir?: string[];
|
|
486
|
+
workspace?: string;
|
|
452
487
|
worktree?: boolean | {
|
|
453
488
|
name?: string;
|
|
454
489
|
ref?: string;
|