llm-cli-gateway 2.12.1 → 2.13.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 +46 -0
- package/README.md +177 -19
- package/dist/acp/provider-registry.js +13 -0
- package/dist/api-provider.d.ts +1 -0
- package/dist/api-provider.js +13 -0
- package/dist/approval-manager.d.ts +1 -1
- package/dist/async-job-manager.d.ts +35 -3
- package/dist/async-job-manager.js +344 -46
- package/dist/cli-updater.d.ts +1 -1
- package/dist/cli-updater.js +17 -9
- package/dist/config.d.ts +34 -0
- package/dist/config.js +93 -8
- package/dist/doctor.d.ts +32 -4
- package/dist/doctor.js +80 -13
- package/dist/endpoint-exposure.js +15 -3
- package/dist/executor.js +2 -0
- package/dist/http-transport.d.ts +3 -0
- package/dist/http-transport.js +138 -8
- package/dist/index.d.ts +62 -5
- package/dist/index.js +774 -83
- package/dist/model-registry.d.ts +1 -1
- package/dist/model-registry.js +12 -16
- package/dist/provider-login-guidance.d.ts +12 -0
- package/dist/provider-login-guidance.js +46 -0
- package/dist/provider-status.d.ts +13 -1
- package/dist/provider-status.js +22 -13
- package/dist/provider-tool-capabilities.d.ts +4 -1
- package/dist/provider-tool-capabilities.js +310 -11
- package/dist/provider-types.d.ts +4 -0
- package/dist/provider-types.js +10 -0
- package/dist/resources.d.ts +6 -2
- package/dist/resources.js +72 -8
- package/dist/session-manager.d.ts +3 -5
- package/dist/session-manager.js +4 -2
- package/dist/upstream-contracts.js +169 -0
- package/dist/validation-normalizer.js +5 -4
- package/dist/validation-orchestrator.js +4 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/setup/status.schema.json +68 -3
package/dist/http-transport.js
CHANGED
|
@@ -3,7 +3,7 @@ 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, resolveTrustedPrincipal, writeAuthFailure, } from "./auth.js";
|
|
6
|
-
import { loadRemoteOAuthConfig } from "./config.js";
|
|
6
|
+
import { loadRemoteOAuthConfig, loadLimitsConfig } from "./config.js";
|
|
7
7
|
import { OAuthServer, oauthBaseUrlFromRequest } from "./oauth.js";
|
|
8
8
|
import { runWithRequestContext } from "./request-context.js";
|
|
9
9
|
import { readCappedRawBody, maxHttpBodyBytes } from "./request-limits.js";
|
|
@@ -79,6 +79,8 @@ export async function startHttpGateway(options) {
|
|
|
79
79
|
const noAuthPaths = parseNoAuthPaths(process.env.LLM_GATEWAY_NO_AUTH_PATHS, path);
|
|
80
80
|
const logger = options.logger ?? noopLogger;
|
|
81
81
|
const sessions = new Map();
|
|
82
|
+
let pendingInitializes = 0;
|
|
83
|
+
const httpLimits = options.httpLimits ?? loadLimitsConfig(logger).http;
|
|
82
84
|
const token = getRequiredBearerToken();
|
|
83
85
|
const oauthConfig = loadRemoteOAuthConfig(logger);
|
|
84
86
|
if (oauthConfig.enabled &&
|
|
@@ -101,14 +103,33 @@ export async function startHttpGateway(options) {
|
|
|
101
103
|
.catch(error => logger.error("HTTP transport close failed", error));
|
|
102
104
|
await entry.server.close().catch(error => logger.error("HTTP MCP server close failed", error));
|
|
103
105
|
}
|
|
104
|
-
|
|
106
|
+
function touchSessionComplete(entry) {
|
|
107
|
+
entry.inFlight = Math.max(0, entry.inFlight - 1);
|
|
108
|
+
entry.lastActivityAt = Date.now();
|
|
109
|
+
}
|
|
110
|
+
async function createSession(releaseInitializeReservation) {
|
|
105
111
|
const gatewayServer = options.createGatewayServer(options.deps);
|
|
112
|
+
let entry;
|
|
106
113
|
const transport = new StreamableHTTPServerTransport({
|
|
107
114
|
sessionIdGenerator: () => randomUUID(),
|
|
108
115
|
onsessioninitialized: sessionId => {
|
|
109
|
-
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
releaseInitializeReservation();
|
|
118
|
+
entry.sessionId = sessionId;
|
|
119
|
+
entry.createdAt = now;
|
|
120
|
+
entry.lastActivityAt = now;
|
|
121
|
+
entry.inFlight++;
|
|
122
|
+
sessions.set(sessionId, entry);
|
|
110
123
|
},
|
|
111
124
|
});
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
entry = {
|
|
127
|
+
server: gatewayServer,
|
|
128
|
+
transport,
|
|
129
|
+
createdAt: now,
|
|
130
|
+
lastActivityAt: now,
|
|
131
|
+
inFlight: 0,
|
|
132
|
+
};
|
|
112
133
|
transport.onclose = () => {
|
|
113
134
|
if (transport.sessionId) {
|
|
114
135
|
sessions.delete(transport.sessionId);
|
|
@@ -116,7 +137,70 @@ export async function startHttpGateway(options) {
|
|
|
116
137
|
};
|
|
117
138
|
transport.onerror = error => logger.error("HTTP MCP transport error", error);
|
|
118
139
|
await gatewayServer.connect(transport);
|
|
119
|
-
return
|
|
140
|
+
return entry;
|
|
141
|
+
}
|
|
142
|
+
function reapIdleSessions() {
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
for (const [sessionId, entry] of sessions) {
|
|
145
|
+
if (entry.inFlight > 0)
|
|
146
|
+
continue;
|
|
147
|
+
if (now - entry.lastActivityAt < httpLimits.sessionIdleTtlMs)
|
|
148
|
+
continue;
|
|
149
|
+
logger.info(`Reaping idle HTTP MCP session (idle ${now - entry.lastActivityAt}ms >= ${httpLimits.sessionIdleTtlMs}ms)`);
|
|
150
|
+
void closeSession(sessionId);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const reaperTimer = setInterval(reapIdleSessions, httpLimits.sessionReaperIntervalMs);
|
|
154
|
+
if (reaperTimer.unref)
|
|
155
|
+
reaperTimer.unref();
|
|
156
|
+
function sessionHealth() {
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
let oldestAgeMs = 0;
|
|
159
|
+
for (const entry of sessions.values()) {
|
|
160
|
+
const age = now - entry.createdAt;
|
|
161
|
+
if (age > oldestAgeMs)
|
|
162
|
+
oldestAgeMs = age;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
current: sessions.size,
|
|
166
|
+
max: httpLimits.maxSessions,
|
|
167
|
+
oldestAgeMs,
|
|
168
|
+
idleTtlMs: httpLimits.sessionIdleTtlMs,
|
|
169
|
+
reaperIntervalMs: httpLimits.sessionReaperIntervalMs,
|
|
170
|
+
saturated: sessions.size + pendingInitializes >= httpLimits.maxSessions,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function healthPayload() {
|
|
174
|
+
const manager = options.deps?.asyncJobManager;
|
|
175
|
+
const mem = process.memoryUsage();
|
|
176
|
+
const payload = {
|
|
177
|
+
sessions: sessionHealth(),
|
|
178
|
+
memory: {
|
|
179
|
+
rss: mem.rss,
|
|
180
|
+
heapUsed: mem.heapUsed,
|
|
181
|
+
heapTotal: mem.heapTotal,
|
|
182
|
+
external: mem.external,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
if (manager) {
|
|
186
|
+
const limiter = manager.getLimiterSnapshot();
|
|
187
|
+
const limits = manager.getConfiguredLimits();
|
|
188
|
+
payload.jobs = {
|
|
189
|
+
running: limiter.running,
|
|
190
|
+
queued: limiter.queued,
|
|
191
|
+
runningByProvider: limiter.runningByProvider,
|
|
192
|
+
queuedByProvider: limiter.queuedByProvider,
|
|
193
|
+
maxRunning: limiter.maxRunning,
|
|
194
|
+
maxRunningPerProvider: limiter.maxRunningPerProvider,
|
|
195
|
+
maxQueued: limiter.maxQueued,
|
|
196
|
+
rejected: limiter.rejected,
|
|
197
|
+
timedOut: limiter.timedOut,
|
|
198
|
+
saturated: limiter.saturated,
|
|
199
|
+
completedJobMemoryTtlMs: limits.completedJobMemoryTtlMs,
|
|
200
|
+
maxJobOutputBytes: limits.maxJobOutputBytes,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return payload;
|
|
120
204
|
}
|
|
121
205
|
const httpServer = createServer(async (req, res) => {
|
|
122
206
|
try {
|
|
@@ -127,7 +211,7 @@ export async function startHttpGateway(options) {
|
|
|
127
211
|
const resourceMetadataUrl = oauthServer && oauthOrigin ? oauthServer.resourceMetadataUrl(oauthOrigin) : undefined;
|
|
128
212
|
if (url.pathname === "/healthz") {
|
|
129
213
|
res.writeHead(200, { "content-type": "application/json" });
|
|
130
|
-
res.end(JSON.stringify({ ok: true,
|
|
214
|
+
res.end(JSON.stringify({ ok: true, ...healthPayload() }));
|
|
131
215
|
return;
|
|
132
216
|
}
|
|
133
217
|
if (oauthServer) {
|
|
@@ -188,7 +272,14 @@ export async function startHttpGateway(options) {
|
|
|
188
272
|
return;
|
|
189
273
|
}
|
|
190
274
|
const body = req.method === "POST" ? await readBody(req) : undefined;
|
|
191
|
-
|
|
275
|
+
entry.lastActivityAt = Date.now();
|
|
276
|
+
entry.inFlight++;
|
|
277
|
+
try {
|
|
278
|
+
await runWithRequestContext(requestContext, () => entry.transport.handleRequest(req, res, body));
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
touchSessionComplete(entry);
|
|
282
|
+
}
|
|
192
283
|
return;
|
|
193
284
|
}
|
|
194
285
|
if (req.method !== "POST") {
|
|
@@ -204,8 +295,45 @@ export async function startHttpGateway(options) {
|
|
|
204
295
|
jsonError(res, 400, "First request must be initialize");
|
|
205
296
|
return;
|
|
206
297
|
}
|
|
207
|
-
const
|
|
208
|
-
|
|
298
|
+
const capacityInUse = sessions.size + pendingInitializes;
|
|
299
|
+
if (capacityInUse >= httpLimits.maxSessions) {
|
|
300
|
+
res.writeHead(429, {
|
|
301
|
+
"content-type": "application/json",
|
|
302
|
+
"retry-after": "5",
|
|
303
|
+
});
|
|
304
|
+
res.end(JSON.stringify({
|
|
305
|
+
error: "Gateway at session capacity",
|
|
306
|
+
code: "session_capacity",
|
|
307
|
+
retryable: true,
|
|
308
|
+
sessions: {
|
|
309
|
+
current: sessions.size,
|
|
310
|
+
pending: pendingInitializes,
|
|
311
|
+
max: httpLimits.maxSessions,
|
|
312
|
+
},
|
|
313
|
+
}));
|
|
314
|
+
logger.info(`Rejected new HTTP MCP session: at capacity (${capacityInUse}/${httpLimits.maxSessions})`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
pendingInitializes++;
|
|
318
|
+
let reservationReleased = false;
|
|
319
|
+
const releaseInitializeReservation = () => {
|
|
320
|
+
if (reservationReleased)
|
|
321
|
+
return;
|
|
322
|
+
reservationReleased = true;
|
|
323
|
+
pendingInitializes = Math.max(0, pendingInitializes - 1);
|
|
324
|
+
};
|
|
325
|
+
let entry;
|
|
326
|
+
try {
|
|
327
|
+
const created = await createSession(releaseInitializeReservation);
|
|
328
|
+
entry = created;
|
|
329
|
+
await runWithRequestContext(requestContext, () => created.transport.handleRequest(req, res, body));
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
releaseInitializeReservation();
|
|
333
|
+
if (entry?.sessionId && sessions.get(entry.sessionId) === entry && entry.inFlight > 0) {
|
|
334
|
+
touchSessionComplete(entry);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
209
337
|
}
|
|
210
338
|
catch (error) {
|
|
211
339
|
logger.error("HTTP transport request failed", error);
|
|
@@ -241,11 +369,13 @@ export async function startHttpGateway(options) {
|
|
|
241
369
|
server: httpServer,
|
|
242
370
|
url,
|
|
243
371
|
close: async () => {
|
|
372
|
+
clearInterval(reaperTimer);
|
|
244
373
|
await Promise.all([...sessions.keys()].map(closeSession));
|
|
245
374
|
await new Promise((resolve, reject) => {
|
|
246
375
|
httpServer.close(error => (error ? reject(error) : resolve()));
|
|
247
376
|
});
|
|
248
377
|
},
|
|
249
378
|
sessionCount: () => sessions.size,
|
|
379
|
+
sessionHealth,
|
|
250
380
|
};
|
|
251
381
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { PerformanceMetrics } from "./metrics.js";
|
|
|
7
7
|
import { type PersistenceConfig, type CacheAwarenessConfig, type ProvidersConfig, type ApiProviderRuntime, type AcpConfig } from "./config.js";
|
|
8
8
|
import { DatabaseConnection } from "./db.js";
|
|
9
9
|
import { AsyncJobManager } from "./async-job-manager.js";
|
|
10
|
-
import { ApprovalManager, ApprovalRecord } from "./approval-manager.js";
|
|
10
|
+
import { ApprovalManager, ApprovalPolicy, ApprovalRecord } from "./approval-manager.js";
|
|
11
11
|
import { ReviewIntegrityResult } from "./review-integrity.js";
|
|
12
12
|
import { ClaudeMcpConfigResult, ClaudeMcpServerName } from "./claude-mcp-config.js";
|
|
13
13
|
import { type MistralAgentMode, type ClaudePermissionMode, type CodexSandboxMode, type CodexAskForApproval, type ClaudeEffortLevel } from "./request-helpers.js";
|
|
@@ -60,8 +60,8 @@ export declare const WORKTREE_SCHEMA: z.ZodUnion<[z.ZodBoolean, z.ZodObject<{
|
|
|
60
60
|
ref?: string | undefined;
|
|
61
61
|
}>]>;
|
|
62
62
|
export declare const WORKSPACE_ALIAS_SCHEMA: z.ZodString;
|
|
63
|
-
export declare const SESSION_PROVIDER_VALUES: readonly ["claude", "codex", "gemini", "grok", "mistral", "devin", "grok-api"];
|
|
64
|
-
export declare const SESSION_PROVIDER_ENUM: z.ZodEnum<["claude", "codex", "gemini", "grok", "mistral", "devin", "grok-api"]>;
|
|
63
|
+
export declare const SESSION_PROVIDER_VALUES: readonly ["claude", "codex", "gemini", "grok", "mistral", "devin", "cursor", "grok-api"];
|
|
64
|
+
export declare const SESSION_PROVIDER_ENUM: z.ZodEnum<["claude", "codex", "gemini", "grok", "mistral", "devin", "cursor", "grok-api"]>;
|
|
65
65
|
export declare function sessionProviderValuesFor(providers: ProvidersConfig): string[];
|
|
66
66
|
export type SessionProvider = ProviderType;
|
|
67
67
|
export interface GatewayServerDeps {
|
|
@@ -124,6 +124,20 @@ export declare function createErrorResponse(cli: string, code: number, stderr: s
|
|
|
124
124
|
httpStatus?: number | null;
|
|
125
125
|
responseBody?: string;
|
|
126
126
|
}): {
|
|
127
|
+
content: {
|
|
128
|
+
type: "text";
|
|
129
|
+
text: string;
|
|
130
|
+
}[];
|
|
131
|
+
isError: boolean;
|
|
132
|
+
structuredContent: {
|
|
133
|
+
response: string;
|
|
134
|
+
correlationId: string | null;
|
|
135
|
+
cli: string;
|
|
136
|
+
exitCode: number;
|
|
137
|
+
errorCategory: string;
|
|
138
|
+
retryable: boolean;
|
|
139
|
+
};
|
|
140
|
+
} | {
|
|
127
141
|
content: {
|
|
128
142
|
type: "text";
|
|
129
143
|
text: string;
|
|
@@ -137,9 +151,10 @@ export declare function createErrorResponse(cli: string, code: number, stderr: s
|
|
|
137
151
|
cli: string;
|
|
138
152
|
exitCode: number;
|
|
139
153
|
errorCategory: string;
|
|
154
|
+
retryable?: undefined;
|
|
140
155
|
};
|
|
141
156
|
};
|
|
142
|
-
export declare function extractUsageAndCost(cli:
|
|
157
|
+
export declare function extractUsageAndCost(cli: CliType, output: string, outputFormat?: string, ctx?: {
|
|
143
158
|
sessionId?: string;
|
|
144
159
|
home?: string;
|
|
145
160
|
}): {
|
|
@@ -333,7 +348,7 @@ export declare function buildMistralRetryPrep(params: Pick<MistralRequestParams,
|
|
|
333
348
|
env: Record<string, string>;
|
|
334
349
|
ignoredDisallowedTools: boolean;
|
|
335
350
|
};
|
|
336
|
-
export declare function buildCliResponse(cli:
|
|
351
|
+
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;
|
|
337
352
|
export interface GrokApiRequestParams {
|
|
338
353
|
prompt?: string;
|
|
339
354
|
promptParts?: PromptParts;
|
|
@@ -507,6 +522,48 @@ export declare function prepareDevinRequest(params: {
|
|
|
507
522
|
}, _runtime: GatewayServerRuntime): CliRequestPrep | ExtendedToolResponse;
|
|
508
523
|
export declare function handleDevinRequest(deps: HandlerDeps, params: DevinRequestParams): Promise<ExtendedToolResponse>;
|
|
509
524
|
export declare function handleDevinRequestAsync(deps: AsyncHandlerDeps, params: Omit<DevinRequestParams, "optimizeResponse">): Promise<ExtendedToolResponse>;
|
|
525
|
+
export interface CursorRequestParams {
|
|
526
|
+
prompt?: string;
|
|
527
|
+
model?: string;
|
|
528
|
+
mode?: "plan" | "ask";
|
|
529
|
+
outputFormat?: "text" | "json" | "stream-json";
|
|
530
|
+
transport?: "cli" | "acp";
|
|
531
|
+
force?: boolean;
|
|
532
|
+
autoReview?: boolean;
|
|
533
|
+
sandbox?: "enabled" | "disabled";
|
|
534
|
+
trust?: boolean;
|
|
535
|
+
workspace?: string;
|
|
536
|
+
addDir?: string[];
|
|
537
|
+
sessionId?: string;
|
|
538
|
+
resumeLatest?: boolean;
|
|
539
|
+
createNewSession?: boolean;
|
|
540
|
+
approvalStrategy?: "legacy" | "mcp_managed";
|
|
541
|
+
approvalPolicy?: ApprovalPolicy;
|
|
542
|
+
correlationId?: string;
|
|
543
|
+
optimizePrompt: boolean;
|
|
544
|
+
optimizeResponse?: boolean;
|
|
545
|
+
idleTimeoutMs?: number;
|
|
546
|
+
forceRefresh?: boolean;
|
|
547
|
+
}
|
|
548
|
+
export declare function prepareCursorRequest(params: {
|
|
549
|
+
prompt?: string;
|
|
550
|
+
model?: string;
|
|
551
|
+
mode?: CursorRequestParams["mode"];
|
|
552
|
+
outputFormat?: CursorRequestParams["outputFormat"];
|
|
553
|
+
force?: boolean;
|
|
554
|
+
autoReview?: boolean;
|
|
555
|
+
sandbox?: CursorRequestParams["sandbox"];
|
|
556
|
+
trust?: boolean;
|
|
557
|
+
workspace?: string;
|
|
558
|
+
addDir?: string[];
|
|
559
|
+
approvalStrategy?: CursorRequestParams["approvalStrategy"];
|
|
560
|
+
approvalPolicy?: CursorRequestParams["approvalPolicy"];
|
|
561
|
+
correlationId?: string;
|
|
562
|
+
optimizePrompt: boolean;
|
|
563
|
+
operation: string;
|
|
564
|
+
}, runtime: GatewayServerRuntime): CliRequestPrep | ExtendedToolResponse;
|
|
565
|
+
export declare function handleCursorRequest(deps: HandlerDeps, params: CursorRequestParams): Promise<ExtendedToolResponse>;
|
|
566
|
+
export declare function handleCursorRequestAsync(deps: AsyncHandlerDeps, params: Omit<CursorRequestParams, "optimizeResponse">): Promise<ExtendedToolResponse>;
|
|
510
567
|
export interface MistralRequestParams {
|
|
511
568
|
prompt?: string;
|
|
512
569
|
promptParts?: PromptParts;
|