llm-cli-gateway 2.16.0 → 2.17.1
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 +37 -0
- package/README.md +12 -12
- 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/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 +16 -0
- package/dist/async-job-manager.js +70 -22
- package/dist/claude-mcp-config.js +1 -1
- package/dist/compressor/transforms/ansi.js +12 -2
- package/dist/config.d.ts +31 -0
- package/dist/config.js +142 -7
- package/dist/db.js +4 -4
- package/dist/doctor.d.ts +34 -1
- package/dist/doctor.js +85 -3
- package/dist/executor.d.ts +5 -0
- package/dist/executor.js +11 -1
- package/dist/flight-recorder.d.ts +10 -0
- package/dist/flight-recorder.js +68 -2
- package/dist/http-transport.js +19 -21
- package/dist/index.d.ts +42 -3
- package/dist/index.js +666 -41
- package/dist/job-store.d.ts +10 -2
- package/dist/job-store.js +154 -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-admin-tools.js +12 -4
- package/dist/provider-definitions.d.ts +4 -0
- package/dist/provider-definitions.js +33 -5
- package/dist/provider-tool-capabilities.js +6 -9
- package/dist/request-helpers.d.ts +3 -4
- package/dist/request-helpers.js +9 -10
- 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/token-estimator.d.ts +6 -0
- package/dist/token-estimator.js +59 -0
- package/dist/upstream-contracts.d.ts +4 -1
- package/dist/upstream-contracts.js +255 -71
- 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 +14 -5
- package/setup/status.schema.json +68 -0
package/dist/index.js
CHANGED
|
@@ -7,7 +7,8 @@ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileS
|
|
|
7
7
|
import { basename, dirname, isAbsolute, join, relative } from "path";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
import { z } from "zod/v3";
|
|
10
|
-
import { executeCli, killAllProcessGroups, providerCommandName } from "./executor.js";
|
|
10
|
+
import { executeCli, killAllProcessGroups, providerCommandName, cliBreakerState, } from "./executor.js";
|
|
11
|
+
import { isDefaultTransient } from "./retry.js";
|
|
11
12
|
import { parseStreamJson } from "./stream-json-parser.js";
|
|
12
13
|
import { parseCodexJsonStream, codexDisplayText, codexFrResponse } from "./codex-json-parser.js";
|
|
13
14
|
import { grokDisplayText } from "./grok-json-parser.js";
|
|
@@ -22,7 +23,11 @@ import { generateResourceDescriptors, generateProviderAcpDescriptors, } from "./
|
|
|
22
23
|
import { PerformanceMetrics } from "./metrics.js";
|
|
23
24
|
import { estimateTokens, optimizePrompt as optimizePromptText, optimizeResponse as optimizeResponseText, } from "./optimizer.js";
|
|
24
25
|
import { compressDisplayText } from "./compressor/index.js";
|
|
25
|
-
import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, loadProvidersConfig, loadAcpConfig, loadAdminConfig, loadLimitsConfig, loadCompressionConfig, loadSkillsConfig, defaultGatewayConfigPath, isXaiProviderEnabled, enabledApiProviders, minStableTokensForModel, } from "./config.js";
|
|
26
|
+
import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, loadProvidersConfig, loadAcpConfig, loadAdminConfig, loadLimitsConfig, loadCompressionConfig, loadLeastCostConfig, loadSkillsConfig, defaultGatewayConfigPath, isXaiProviderEnabled, enabledApiProviders, minStableTokensForModel, } from "./config.js";
|
|
27
|
+
import { buildRouterEnv, toRouterConfig } from "./lcr-router-env.js";
|
|
28
|
+
import { getModelCost, composeCost, modelIdToFamily } from "./pricing.js";
|
|
29
|
+
import { telemetryTierFor } from "./lcr-telemetry.js";
|
|
30
|
+
import { selectCandidate, } from "./least-cost-router.js";
|
|
26
31
|
import { loadGatewaySkills } from "./skill-loader.js";
|
|
27
32
|
import { runAcpRequest } from "./acp/runtime.js";
|
|
28
33
|
import { isAcpError } from "./acp/errors.js";
|
|
@@ -230,6 +235,7 @@ let flightRecorder = null;
|
|
|
230
235
|
let persistenceConfig = null;
|
|
231
236
|
let cacheAwarenessConfig = null;
|
|
232
237
|
let compressionConfig = null;
|
|
238
|
+
let leastCostConfig = null;
|
|
233
239
|
let providersConfig = null;
|
|
234
240
|
let acpConfig = null;
|
|
235
241
|
let adminConfig = null;
|
|
@@ -266,6 +272,10 @@ function getCompressionConfig(runtimeLogger = logger) {
|
|
|
266
272
|
compressionConfig ??= loadCompressionConfig(runtimeLogger);
|
|
267
273
|
return compressionConfig;
|
|
268
274
|
}
|
|
275
|
+
function getLeastCostConfig(runtimeLogger = logger) {
|
|
276
|
+
leastCostConfig ??= loadLeastCostConfig(runtimeLogger);
|
|
277
|
+
return leastCostConfig;
|
|
278
|
+
}
|
|
269
279
|
function getProvidersConfig(runtimeLogger = logger) {
|
|
270
280
|
providersConfig ??= loadProvidersConfig(runtimeLogger);
|
|
271
281
|
return providersConfig;
|
|
@@ -278,7 +288,7 @@ function getJobStore(runtimeLogger = logger) {
|
|
|
278
288
|
jobStore = createJobStore(getPersistenceConfig(runtimeLogger), runtimeLogger);
|
|
279
289
|
}
|
|
280
290
|
catch (err) {
|
|
281
|
-
runtimeLogger.error("Failed to open durable job store
|
|
291
|
+
runtimeLogger.error("Failed to open configured durable job store", err);
|
|
282
292
|
jobStore = null;
|
|
283
293
|
}
|
|
284
294
|
return jobStore;
|
|
@@ -464,7 +474,7 @@ export function resolveGatewayServerRuntime(deps = {}, options = {}) {
|
|
|
464
474
|
sessionManager: runtimeSessionManager,
|
|
465
475
|
resourceProvider: deps.resourceProvider ??
|
|
466
476
|
(options.isolateState
|
|
467
|
-
? new ResourceProvider(runtimeSessionManager, runtimePerformanceMetrics, runtimeFlightRecorder, deps.cacheAwareness ?? getCacheAwarenessConfig(runtimeLogger), deps.providers ?? getProvidersConfig(runtimeLogger), undefined, deps.acpConfig ?? getAcpConfig(runtimeLogger))
|
|
477
|
+
? new ResourceProvider(runtimeSessionManager, runtimePerformanceMetrics, runtimeFlightRecorder, deps.cacheAwareness ?? getCacheAwarenessConfig(runtimeLogger), deps.providers ?? getProvidersConfig(runtimeLogger), undefined, deps.acpConfig ?? getAcpConfig(runtimeLogger), deps.leastCost ?? getLeastCostConfig(runtimeLogger))
|
|
468
478
|
: resourceProvider),
|
|
469
479
|
db: "db" in deps ? (deps.db ?? null) : db,
|
|
470
480
|
performanceMetrics: runtimePerformanceMetrics,
|
|
@@ -479,6 +489,7 @@ export function resolveGatewayServerRuntime(deps = {}, options = {}) {
|
|
|
479
489
|
acpConfig: deps.acpConfig ?? getAcpConfig(runtimeLogger),
|
|
480
490
|
adminConfig: deps.adminConfig ?? getAdminConfig(runtimeLogger),
|
|
481
491
|
workspaces: deps.workspaces ?? loadWorkspaceRegistry(runtimeLogger),
|
|
492
|
+
leastCost: deps.leastCost ?? getLeastCostConfig(runtimeLogger),
|
|
482
493
|
};
|
|
483
494
|
}
|
|
484
495
|
export function shouldRegisterGrokApiTools(providers) {
|
|
@@ -1179,9 +1190,10 @@ function buildAsyncFlightRecorderHandoff(cliName, prep, sessionId, outputFormat,
|
|
|
1179
1190
|
const fmt = outputFormat;
|
|
1180
1191
|
const sid = sessionId;
|
|
1181
1192
|
const home = homedir();
|
|
1193
|
+
const model = prep.resolvedModel || "default";
|
|
1182
1194
|
return {
|
|
1183
1195
|
flightRecorderEntry: {
|
|
1184
|
-
model
|
|
1196
|
+
model,
|
|
1185
1197
|
prompt: prep.effectivePrompt,
|
|
1186
1198
|
sessionId,
|
|
1187
1199
|
optimizationApplied,
|
|
@@ -1190,7 +1202,11 @@ function buildAsyncFlightRecorderHandoff(cliName, prep, sessionId, outputFormat,
|
|
|
1190
1202
|
cacheControlBlocks: prep.cacheControlBlocks,
|
|
1191
1203
|
cacheControlTtlSeconds: prep.cacheControlTtlSeconds,
|
|
1192
1204
|
},
|
|
1193
|
-
extractUsage: (stdout) =>
|
|
1205
|
+
extractUsage: (stdout) => {
|
|
1206
|
+
const usage = extractUsageAndCost(cli, stdout, fmt, { sessionId: sid, home });
|
|
1207
|
+
const { costUsd, costBasis } = deriveCostBasis(cli, model, usage);
|
|
1208
|
+
return { ...usage, costUsd, costBasis };
|
|
1209
|
+
},
|
|
1194
1210
|
};
|
|
1195
1211
|
}
|
|
1196
1212
|
function safeFlightStart(entry, runtime = resolveGatewayServerRuntime()) {
|
|
@@ -1235,6 +1251,14 @@ function safeRecordCompression(correlationId, compression, runtime) {
|
|
|
1235
1251
|
runtime.logger.error("Flight recorder recordCompressionTelemetry failed", error);
|
|
1236
1252
|
}
|
|
1237
1253
|
}
|
|
1254
|
+
function safeRecordRouting(correlationId, routing, runtime) {
|
|
1255
|
+
try {
|
|
1256
|
+
runtime.flightRecorder.recordRouting(correlationId, routing);
|
|
1257
|
+
}
|
|
1258
|
+
catch (error) {
|
|
1259
|
+
runtime.logger.error("Flight recorder recordRouting failed", error);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1238
1262
|
function createApprovalDeniedResponse(operation, decision) {
|
|
1239
1263
|
return {
|
|
1240
1264
|
content: [
|
|
@@ -2177,7 +2201,7 @@ export function prepareGrokRequest(params, runtime = resolveGatewayServerRuntime
|
|
|
2177
2201
|
args.push("--permission-mode", "acceptEdits");
|
|
2178
2202
|
}
|
|
2179
2203
|
}
|
|
2180
|
-
else if (
|
|
2204
|
+
else if (params.alwaysApprove) {
|
|
2181
2205
|
args.push("--always-approve");
|
|
2182
2206
|
}
|
|
2183
2207
|
else if (params.permissionMode) {
|
|
@@ -2324,9 +2348,6 @@ export function prepareMistralRequest(params, runtime = resolveGatewayServerRunt
|
|
|
2324
2348
|
workingDir: params.workingDir,
|
|
2325
2349
|
addDir: params.addDir,
|
|
2326
2350
|
});
|
|
2327
|
-
if (prep.ignoredDisallowedTools) {
|
|
2328
|
-
runtime.logger.info(`[${corrId}] Mistral does not support disallowedTools; ignoring (caller passed ${params.disallowedTools?.length ?? 0} entries)`);
|
|
2329
|
-
}
|
|
2330
2351
|
return {
|
|
2331
2352
|
corrId,
|
|
2332
2353
|
effectivePrompt,
|
|
@@ -2981,6 +3002,7 @@ export async function handleApiProviderRequest(runtimeArg, providerRuntime, para
|
|
|
2981
3002
|
inputTokens: result.usage?.inputTokens,
|
|
2982
3003
|
outputTokens: result.usage?.outputTokens,
|
|
2983
3004
|
cacheReadTokens: result.usage?.cacheReadTokens,
|
|
3005
|
+
cacheCreationTokens: result.usage?.cacheCreationTokens,
|
|
2984
3006
|
costUsd: result.usage?.costUsd,
|
|
2985
3007
|
}, runtimeArg);
|
|
2986
3008
|
return buildApiSuccessResponse(text, corrId, providerRuntime.name, {
|
|
@@ -3252,7 +3274,7 @@ export async function handleGeminiRequest(deps, params) {
|
|
|
3252
3274
|
return createErrorResponse("gemini", code, stderr, corrId);
|
|
3253
3275
|
}
|
|
3254
3276
|
wasSuccessful = true;
|
|
3255
|
-
|
|
3277
|
+
const effectiveSessionId = effectiveSessionIdHint;
|
|
3256
3278
|
if (effectiveSessionId) {
|
|
3257
3279
|
const existing = await deps.sessionManager.getSession(effectiveSessionId);
|
|
3258
3280
|
if (!existing) {
|
|
@@ -3277,6 +3299,7 @@ export async function handleGeminiRequest(deps, params) {
|
|
|
3277
3299
|
}
|
|
3278
3300
|
}
|
|
3279
3301
|
const geminiUsage = extractUsageAndCost("gemini", stdout, params.outputFormat);
|
|
3302
|
+
const { costUsd: geminiCostUsd, costBasis: geminiCostBasis } = deriveCostBasis("gemini", prep.resolvedModel || "default", geminiUsage);
|
|
3280
3303
|
const geminiMeta = extractProviderOutputMetadata("gemini", stdout, params.outputFormat);
|
|
3281
3304
|
safeFlightComplete(corrId, {
|
|
3282
3305
|
response: stdout,
|
|
@@ -3291,7 +3314,8 @@ export async function handleGeminiRequest(deps, params) {
|
|
|
3291
3314
|
outputTokens: geminiUsage.outputTokens,
|
|
3292
3315
|
cacheReadTokens: geminiUsage.cacheReadTokens,
|
|
3293
3316
|
cacheCreationTokens: geminiUsage.cacheCreationTokens,
|
|
3294
|
-
costUsd:
|
|
3317
|
+
costUsd: geminiCostUsd,
|
|
3318
|
+
costBasis: geminiCostBasis,
|
|
3295
3319
|
providerSessionId: geminiMeta.sessionId,
|
|
3296
3320
|
stopReason: geminiMeta.stopReason,
|
|
3297
3321
|
}, runtime);
|
|
@@ -3352,7 +3376,7 @@ export async function handleGeminiRequestAsync(deps, params) {
|
|
|
3352
3376
|
resumeLatest: params.resumeLatest,
|
|
3353
3377
|
createNewSession: params.createNewSession,
|
|
3354
3378
|
});
|
|
3355
|
-
|
|
3379
|
+
const effectiveSessionId = sessionPlan.resumed ? params.sessionId : undefined;
|
|
3356
3380
|
const existingSession = await getExistingSessionForProvider(deps.sessionManager, effectiveSessionId, "gemini");
|
|
3357
3381
|
args.push(...sessionPlan.args);
|
|
3358
3382
|
if (effectiveSessionId) {
|
|
@@ -4730,14 +4754,8 @@ export async function handleCodexRequestAsync(deps, params) {
|
|
|
4730
4754
|
outputSchemaDeclared: params.outputSchema !== undefined,
|
|
4731
4755
|
});
|
|
4732
4756
|
const codexAsyncFrHandoff = buildAsyncFlightRecorderHandoff("codex", prep, effectiveSessionId, params.outputFormat, params.optimizePrompt);
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
job = deps.asyncJobManager.startJob("codex", args, corrId, worktreeResolution.cwd, resolveIdleTimeout("codex", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, undefined, prepCleanup, codexAsyncFrHandoff.flightRecorderEntry, codexAsyncFrHandoff.extractUsage, true, undefined, effectiveCompress);
|
|
4736
|
-
prepCleanupOwnedHere = false;
|
|
4737
|
-
}
|
|
4738
|
-
catch (startErr) {
|
|
4739
|
-
throw startErr;
|
|
4740
|
-
}
|
|
4757
|
+
const job = deps.asyncJobManager.startJob("codex", args, corrId, worktreeResolution.cwd, resolveIdleTimeout("codex", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, undefined, prepCleanup, codexAsyncFrHandoff.flightRecorderEntry, codexAsyncFrHandoff.extractUsage, true, undefined, effectiveCompress);
|
|
4758
|
+
prepCleanupOwnedHere = false;
|
|
4741
4759
|
deps.logger.info(`[${corrId}] codex_request_async started job ${job.id}`);
|
|
4742
4760
|
const asyncResponse = {
|
|
4743
4761
|
success: true,
|
|
@@ -4766,6 +4784,439 @@ export async function handleCodexRequestAsync(deps, params) {
|
|
|
4766
4784
|
return createErrorResponse("codex_request_async", 1, "", corrId, error);
|
|
4767
4785
|
}
|
|
4768
4786
|
}
|
|
4787
|
+
function routedOutputFormat(provider) {
|
|
4788
|
+
return provider === "claude" || provider === "gemini" ? "stream-json" : "text";
|
|
4789
|
+
}
|
|
4790
|
+
function prepareRoutedCli(runtime, cli, params, outputFormat) {
|
|
4791
|
+
const base = {
|
|
4792
|
+
prompt: params.prompt,
|
|
4793
|
+
promptParts: params.promptParts,
|
|
4794
|
+
model: params.model,
|
|
4795
|
+
correlationId: params.correlationId,
|
|
4796
|
+
optimizePrompt: params.optimizePrompt,
|
|
4797
|
+
operation: "route_request",
|
|
4798
|
+
outputFormat,
|
|
4799
|
+
};
|
|
4800
|
+
switch (cli) {
|
|
4801
|
+
case "claude": {
|
|
4802
|
+
const p = {
|
|
4803
|
+
...base,
|
|
4804
|
+
dangerouslySkipPermissions: false,
|
|
4805
|
+
approvalStrategy: "legacy",
|
|
4806
|
+
strictMcpConfig: false,
|
|
4807
|
+
};
|
|
4808
|
+
return prepareClaudeRequest(p, runtime);
|
|
4809
|
+
}
|
|
4810
|
+
case "codex": {
|
|
4811
|
+
const p = {
|
|
4812
|
+
...base,
|
|
4813
|
+
outputFormat: outputFormat,
|
|
4814
|
+
fullAuto: false,
|
|
4815
|
+
dangerouslyBypassApprovalsAndSandbox: false,
|
|
4816
|
+
approvalStrategy: "legacy",
|
|
4817
|
+
};
|
|
4818
|
+
return prepareCodexRequest(p, runtime);
|
|
4819
|
+
}
|
|
4820
|
+
case "gemini": {
|
|
4821
|
+
const p = { ...base, approvalStrategy: "legacy" };
|
|
4822
|
+
return prepareGeminiRequest(p, runtime);
|
|
4823
|
+
}
|
|
4824
|
+
case "grok": {
|
|
4825
|
+
const p = { ...base, approvalStrategy: "legacy" };
|
|
4826
|
+
return prepareGrokRequest(p, runtime);
|
|
4827
|
+
}
|
|
4828
|
+
case "mistral": {
|
|
4829
|
+
const p = { ...base, approvalStrategy: "legacy" };
|
|
4830
|
+
return prepareMistralRequest(p, runtime);
|
|
4831
|
+
}
|
|
4832
|
+
case "devin": {
|
|
4833
|
+
const p = { ...base };
|
|
4834
|
+
return prepareDevinRequest(p, runtime);
|
|
4835
|
+
}
|
|
4836
|
+
case "cursor": {
|
|
4837
|
+
const p = { ...base };
|
|
4838
|
+
return prepareCursorRequest(p, runtime);
|
|
4839
|
+
}
|
|
4840
|
+
default: {
|
|
4841
|
+
const exhaustive = cli;
|
|
4842
|
+
return exhaustive;
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
}
|
|
4846
|
+
async function dispatchRoutedCli(runtime, cli, params) {
|
|
4847
|
+
const startTime = Date.now();
|
|
4848
|
+
let durationMs = 0;
|
|
4849
|
+
let wasSuccessful = false;
|
|
4850
|
+
const outputFormat = routedOutputFormat(cli);
|
|
4851
|
+
const prep = prepareRoutedCli(runtime, cli, params, outputFormat);
|
|
4852
|
+
if (!("args" in prep))
|
|
4853
|
+
return prep;
|
|
4854
|
+
const { corrId, args } = prep;
|
|
4855
|
+
const prepCleanup = "cleanup" in prep && typeof prep.cleanup === "function"
|
|
4856
|
+
? prep.cleanup
|
|
4857
|
+
: undefined;
|
|
4858
|
+
safeFlightStart({
|
|
4859
|
+
correlationId: corrId,
|
|
4860
|
+
cli,
|
|
4861
|
+
model: prep.resolvedModel || "default",
|
|
4862
|
+
prompt: prep.effectivePrompt,
|
|
4863
|
+
stablePrefixHash: prep.stablePrefixHash ?? undefined,
|
|
4864
|
+
stablePrefixTokens: prep.stablePrefixTokens ?? undefined,
|
|
4865
|
+
}, runtime);
|
|
4866
|
+
try {
|
|
4867
|
+
const effectiveCompress = resolveEffectiveCompression(runtime.compression, {
|
|
4868
|
+
compressResponse: params.compressResponse,
|
|
4869
|
+
outputFormat,
|
|
4870
|
+
outputSchemaDeclared: false,
|
|
4871
|
+
});
|
|
4872
|
+
const frHandoff = buildAsyncFlightRecorderHandoff(cli, prep, undefined, outputFormat, params.optimizePrompt);
|
|
4873
|
+
const result = await awaitJobOrDefer(cli, args, corrId, resolveIdleTimeout(cli, undefined), outputFormat, params.forceRefresh, runtime, undefined, prepCleanup, frHandoff.flightRecorderEntry, frHandoff.extractUsage, prep.stdinPayload, undefined, effectiveCompress);
|
|
4874
|
+
if (isDeferredResponse(result)) {
|
|
4875
|
+
return buildDeferredToolResponse(result, undefined);
|
|
4876
|
+
}
|
|
4877
|
+
const { stdout, stderr, code } = result;
|
|
4878
|
+
durationMs = Math.max(0, Date.now() - startTime);
|
|
4879
|
+
if (code !== 0) {
|
|
4880
|
+
safeFlightComplete(corrId, {
|
|
4881
|
+
response: stderr || "",
|
|
4882
|
+
durationMs,
|
|
4883
|
+
retryCount: 0,
|
|
4884
|
+
circuitBreakerState: "closed",
|
|
4885
|
+
optimizationApplied: params.optimizePrompt || params.optimizeResponse,
|
|
4886
|
+
exitCode: code,
|
|
4887
|
+
errorMessage: stderr || `Exit code ${code}`,
|
|
4888
|
+
status: "failed",
|
|
4889
|
+
}, runtime);
|
|
4890
|
+
return createErrorResponse(cli, code, stderr, corrId);
|
|
4891
|
+
}
|
|
4892
|
+
wasSuccessful = true;
|
|
4893
|
+
const claudeStream = cli === "claude" && outputFormat === "stream-json";
|
|
4894
|
+
const parsedClaude = claudeStream ? parseStreamJson(stdout) : null;
|
|
4895
|
+
const responseText = parsedClaude ? parsedClaude.text : stdout;
|
|
4896
|
+
const usage = parsedClaude
|
|
4897
|
+
? {
|
|
4898
|
+
inputTokens: parsedClaude.usage?.inputTokens,
|
|
4899
|
+
outputTokens: parsedClaude.usage?.outputTokens,
|
|
4900
|
+
cacheReadTokens: parsedClaude.usage?.cacheReadInputTokens || undefined,
|
|
4901
|
+
cacheCreationTokens: parsedClaude.usage?.cacheCreationInputTokens || undefined,
|
|
4902
|
+
costUsd: parsedClaude.costUsd ?? undefined,
|
|
4903
|
+
}
|
|
4904
|
+
: extractUsageAndCost(cli, stdout, outputFormat);
|
|
4905
|
+
const meta = extractProviderOutputMetadata(cli, stdout, outputFormat);
|
|
4906
|
+
const { costUsd: recordedCostUsd, costBasis } = deriveCostBasis(cli, prep.resolvedModel || "default", usage);
|
|
4907
|
+
safeFlightComplete(corrId, {
|
|
4908
|
+
response: cli === "codex" ? codexFrResponse(outputFormat, stdout) : responseText,
|
|
4909
|
+
durationMs,
|
|
4910
|
+
retryCount: 0,
|
|
4911
|
+
circuitBreakerState: "closed",
|
|
4912
|
+
optimizationApplied: params.optimizePrompt || params.optimizeResponse,
|
|
4913
|
+
exitCode: 0,
|
|
4914
|
+
status: "completed",
|
|
4915
|
+
inputTokens: usage.inputTokens,
|
|
4916
|
+
outputTokens: usage.outputTokens,
|
|
4917
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
4918
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
4919
|
+
costUsd: recordedCostUsd,
|
|
4920
|
+
costBasis,
|
|
4921
|
+
providerSessionId: parsedClaude ? (parsedClaude.sessionId ?? undefined) : meta.sessionId,
|
|
4922
|
+
stopReason: parsedClaude ? (parsedClaude.stopReason ?? undefined) : meta.stopReason,
|
|
4923
|
+
}, runtime);
|
|
4924
|
+
const response = buildCliResponse(cli, responseText, params.optimizeResponse, corrId, undefined, prep, durationMs, undefined, outputFormat, undefined, effectiveCompress);
|
|
4925
|
+
safeRecordCompression(corrId, response.compression, runtime);
|
|
4926
|
+
return response;
|
|
4927
|
+
}
|
|
4928
|
+
catch (error) {
|
|
4929
|
+
const elapsedMs = Math.max(0, Date.now() - startTime);
|
|
4930
|
+
safeFlightComplete(corrId, {
|
|
4931
|
+
response: "",
|
|
4932
|
+
durationMs: elapsedMs,
|
|
4933
|
+
retryCount: 0,
|
|
4934
|
+
circuitBreakerState: "closed",
|
|
4935
|
+
optimizationApplied: params.optimizePrompt || params.optimizeResponse,
|
|
4936
|
+
exitCode: 1,
|
|
4937
|
+
errorMessage: error.message,
|
|
4938
|
+
status: "failed",
|
|
4939
|
+
}, runtime);
|
|
4940
|
+
return createErrorResponse(cli, 1, "", corrId, error);
|
|
4941
|
+
}
|
|
4942
|
+
finally {
|
|
4943
|
+
const finalizedDurationMs = Math.max(0, durationMs || Date.now() - startTime);
|
|
4944
|
+
runtime.performanceMetrics.recordRequest(cli, finalizedDurationMs, wasSuccessful);
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
async function dispatchRoutedRequest(runtime, chosen, params) {
|
|
4948
|
+
if (isCliTypeValue(chosen.provider)) {
|
|
4949
|
+
return dispatchRoutedCli(runtime, chosen.provider, params);
|
|
4950
|
+
}
|
|
4951
|
+
const api = enabledApiProviders(runtime.providers).find(p => p.name === chosen.provider);
|
|
4952
|
+
if (!api) {
|
|
4953
|
+
return createErrorResponse("route_request", 1, `Routed provider '${chosen.provider}' is not an enabled API provider`, params.correlationId);
|
|
4954
|
+
}
|
|
4955
|
+
const apiParams = {
|
|
4956
|
+
prompt: params.prompt,
|
|
4957
|
+
promptParts: params.promptParts,
|
|
4958
|
+
model: chosen.model,
|
|
4959
|
+
correlationId: params.correlationId,
|
|
4960
|
+
optimizePrompt: params.optimizePrompt,
|
|
4961
|
+
optimizeResponse: params.optimizeResponse,
|
|
4962
|
+
compressResponse: params.compressResponse,
|
|
4963
|
+
};
|
|
4964
|
+
return handleApiProviderRequest(runtime, api, apiParams);
|
|
4965
|
+
}
|
|
4966
|
+
function isCliTypeValue(provider) {
|
|
4967
|
+
return CLI_TYPES.includes(provider);
|
|
4968
|
+
}
|
|
4969
|
+
function routeCandidateKey(c) {
|
|
4970
|
+
return `${c.provider}:${c.model}`;
|
|
4971
|
+
}
|
|
4972
|
+
export function isTransientRouteFailure(provider, response) {
|
|
4973
|
+
const breakerOpen = isCliTypeValue(provider)
|
|
4974
|
+
? cliBreakerState(provider) !== "CLOSED"
|
|
4975
|
+
: apiProviderBreakerState(provider) !== "CLOSED";
|
|
4976
|
+
if (breakerOpen)
|
|
4977
|
+
return true;
|
|
4978
|
+
const sc = response.structuredContent;
|
|
4979
|
+
if (isDefaultTransient({ code: sc?.exitCode }))
|
|
4980
|
+
return true;
|
|
4981
|
+
const haystack = `${response.content.map(c => c.text).join(" ")} ${sc?.errorCategory ?? ""}`;
|
|
4982
|
+
return /\b(ETIMEDOUT|ECONNRESET|ECONNREFUSED|EPIPE)\b/i.test(haystack);
|
|
4983
|
+
}
|
|
4984
|
+
function attachRouting(response, block) {
|
|
4985
|
+
response.routing = block;
|
|
4986
|
+
response.structuredContent = { ...(response.structuredContent ?? {}), routing: block };
|
|
4987
|
+
const chosen = block.chosen ? `${block.chosen.provider}/${block.chosen.model}` : "none";
|
|
4988
|
+
const cost = block.estCostUsd !== undefined ? `~$${block.estCostUsd.toFixed(6)}` : "unpriced";
|
|
4989
|
+
const basis = block.costBasis ? ` (${block.costBasis}, ${block.confidence})` : "";
|
|
4990
|
+
const banner = `[routing] chosen=${chosen} est=${cost}${basis} considered=${block.consideredCount} reroutes=${block.reroutes}`;
|
|
4991
|
+
const first = response.content[0];
|
|
4992
|
+
if (first && first.type === "text") {
|
|
4993
|
+
first.text = `${banner}\n${first.text}`;
|
|
4994
|
+
}
|
|
4995
|
+
else {
|
|
4996
|
+
response.content.unshift({ type: "text", text: banner });
|
|
4997
|
+
}
|
|
4998
|
+
return response;
|
|
4999
|
+
}
|
|
5000
|
+
function buildRoutingBlock(decision, reroutes) {
|
|
5001
|
+
return {
|
|
5002
|
+
chosen: decision.chosen ? { ...decision.chosen } : null,
|
|
5003
|
+
tier: decision.tier,
|
|
5004
|
+
estCostUsd: decision.estCostUsd,
|
|
5005
|
+
costBasis: decision.costBasis,
|
|
5006
|
+
confidence: decision.confidence,
|
|
5007
|
+
nearTie: decision.nearTie,
|
|
5008
|
+
estInputTokens: decision.estInputTokens,
|
|
5009
|
+
estOutputTokens: decision.estOutputTokens,
|
|
5010
|
+
priceAsOf: decision.priceAsOf,
|
|
5011
|
+
priceSource: decision.priceSource,
|
|
5012
|
+
consideredCount: decision.consideredCount,
|
|
5013
|
+
rejected: decision.rejected.map(r => ({ candidate: { ...r.candidate }, reason: r.reason })),
|
|
5014
|
+
reroutes,
|
|
5015
|
+
error: decision.error,
|
|
5016
|
+
};
|
|
5017
|
+
}
|
|
5018
|
+
export function deriveCostBasis(provider, resolvedModel, usage) {
|
|
5019
|
+
if (usage.costUsd != null) {
|
|
5020
|
+
return { costUsd: usage.costUsd, costBasis: "provider-reported" };
|
|
5021
|
+
}
|
|
5022
|
+
const hasCounts = usage.inputTokens != null || usage.outputTokens != null;
|
|
5023
|
+
if (hasCounts) {
|
|
5024
|
+
const modelCost = getModelCost(provider, resolvedModel);
|
|
5025
|
+
if (modelCost.source !== "unknown") {
|
|
5026
|
+
const counts = {
|
|
5027
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
5028
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
5029
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
5030
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
5031
|
+
};
|
|
5032
|
+
const composed = composeCost(counts, { estInputTokens: 0, estOutputTokens: 0 }, modelCost);
|
|
5033
|
+
return { costUsd: composed.costUsd, costBasis: composed.cost_basis };
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
return {};
|
|
5037
|
+
}
|
|
5038
|
+
function recordRoutingDecision(runtime, correlationId, decision, reroutes) {
|
|
5039
|
+
const reason = decision.chosen
|
|
5040
|
+
? `cheapest${decision.nearTie ? ":near-tie" : ""}`
|
|
5041
|
+
: (decision.error ?? "no-eligible");
|
|
5042
|
+
safeRecordRouting(correlationId, {
|
|
5043
|
+
estCostUsd: decision.estCostUsd ?? null,
|
|
5044
|
+
estConfidence: decision.confidence ?? null,
|
|
5045
|
+
reason,
|
|
5046
|
+
considered: decision.consideredCount,
|
|
5047
|
+
reroutes,
|
|
5048
|
+
}, runtime);
|
|
5049
|
+
}
|
|
5050
|
+
function withDispatchRejections(decision, dispatchRejections) {
|
|
5051
|
+
if (dispatchRejections.length === 0)
|
|
5052
|
+
return decision;
|
|
5053
|
+
const seen = new Set(decision.rejected.map(r => routeCandidateKey(r.candidate)));
|
|
5054
|
+
const extra = dispatchRejections.filter(r => !seen.has(routeCandidateKey(r.candidate)));
|
|
5055
|
+
return { ...decision, rejected: [...decision.rejected, ...extra] };
|
|
5056
|
+
}
|
|
5057
|
+
function routeErrorResponse(decision, reroutes, corrId, lastFailure) {
|
|
5058
|
+
const reasons = decision.rejected
|
|
5059
|
+
.map(r => `${routeCandidateKey(r.candidate)} (${r.reason})`)
|
|
5060
|
+
.join(", ");
|
|
5061
|
+
const kind = decision.error ?? "NoEligibleCandidate";
|
|
5062
|
+
const base = kind === "BudgetExceeded"
|
|
5063
|
+
? `route_request: no candidate fits the budget. Rejected: ${reasons || "none eligible"}.`
|
|
5064
|
+
: `route_request: no eligible candidate. Rejected: ${reasons || "empty pool"}.`;
|
|
5065
|
+
const diag = lastFailure?.content?.[0]?.text
|
|
5066
|
+
? `\nLast dispatch failure: ${lastFailure.content[0].text}`
|
|
5067
|
+
: "";
|
|
5068
|
+
const resp = createErrorResponse("route_request", 1, `${base}${diag}`, corrId);
|
|
5069
|
+
return attachRouting(resp, buildRoutingBlock({ ...decision, error: kind }, reroutes));
|
|
5070
|
+
}
|
|
5071
|
+
async function runRouteRequest(runtime, params) {
|
|
5072
|
+
const corrId = params.correlationId ?? randomUUID();
|
|
5073
|
+
const cfg = runtime.leastCost;
|
|
5074
|
+
const { env, routerConfig, req } = buildRouteContext(runtime, params);
|
|
5075
|
+
const excluded = new Set();
|
|
5076
|
+
const dispatchRejections = [];
|
|
5077
|
+
let reroutes = 0;
|
|
5078
|
+
let transientReroutes = 0;
|
|
5079
|
+
let lastFailure = null;
|
|
5080
|
+
let lastDecision = null;
|
|
5081
|
+
const maxIterations = env.providers().length * 8 + cfg.maxReroutes + 2;
|
|
5082
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
5083
|
+
const decision = selectCandidate(req, env, routerConfig, excluded);
|
|
5084
|
+
lastDecision = decision;
|
|
5085
|
+
if (!decision.chosen) {
|
|
5086
|
+
return routeErrorResponse(withDispatchRejections(decision, dispatchRejections), reroutes, corrId, lastFailure);
|
|
5087
|
+
}
|
|
5088
|
+
const chosen = decision.chosen;
|
|
5089
|
+
const attemptCorrId = i === 0 ? corrId : randomUUID();
|
|
5090
|
+
const dispatchParams = {
|
|
5091
|
+
prompt: params.prompt,
|
|
5092
|
+
promptParts: params.promptParts,
|
|
5093
|
+
model: chosen.model,
|
|
5094
|
+
correlationId: attemptCorrId,
|
|
5095
|
+
optimizePrompt: params.optimizePrompt ?? false,
|
|
5096
|
+
optimizeResponse: params.optimizeResponse ?? false,
|
|
5097
|
+
compressResponse: params.compressResponse,
|
|
5098
|
+
forceRefresh: params.forceRefresh,
|
|
5099
|
+
};
|
|
5100
|
+
const result = await dispatchRoutedRequest(runtime, chosen, dispatchParams);
|
|
5101
|
+
if (!result.isError) {
|
|
5102
|
+
recordRoutingDecision(runtime, attemptCorrId, decision, reroutes);
|
|
5103
|
+
return attachRouting(result, buildRoutingBlock(decision, reroutes));
|
|
5104
|
+
}
|
|
5105
|
+
lastFailure = result;
|
|
5106
|
+
const transient = isTransientRouteFailure(chosen.provider, result);
|
|
5107
|
+
excluded.add(routeCandidateKey(chosen));
|
|
5108
|
+
dispatchRejections.push({
|
|
5109
|
+
candidate: chosen,
|
|
5110
|
+
reason: transient ? "dispatch-failed:transient" : "dispatch-failed:non-transient",
|
|
5111
|
+
});
|
|
5112
|
+
reroutes++;
|
|
5113
|
+
if (transient) {
|
|
5114
|
+
transientReroutes++;
|
|
5115
|
+
if (transientReroutes > cfg.maxReroutes) {
|
|
5116
|
+
return attachRouting(result, buildRoutingBlock(decision, reroutes));
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
if (lastDecision) {
|
|
5121
|
+
return routeErrorResponse(withDispatchRejections(lastDecision, dispatchRejections), reroutes, corrId, lastFailure);
|
|
5122
|
+
}
|
|
5123
|
+
return createErrorResponse("route_request", 1, "route_request: routing did not converge", corrId);
|
|
5124
|
+
}
|
|
5125
|
+
function buildRouteContext(runtime, params) {
|
|
5126
|
+
return {
|
|
5127
|
+
env: buildRouterEnv({
|
|
5128
|
+
performanceMetrics: runtime.performanceMetrics,
|
|
5129
|
+
limiterSnapshot: runtime.asyncJobManager.getLimiterSnapshot(),
|
|
5130
|
+
apiProviders: enabledApiProviders(runtime.providers),
|
|
5131
|
+
preferCatalogPrice: runtime.leastCost.preferCatalogPrice,
|
|
5132
|
+
flightRecorder: runtime.flightRecorder,
|
|
5133
|
+
priorsScope: runtime.leastCost.priorsScope,
|
|
5134
|
+
ownerPrincipal: resolveOwnerPrincipal(getRequestContext()),
|
|
5135
|
+
}),
|
|
5136
|
+
routerConfig: toRouterConfig(runtime.leastCost),
|
|
5137
|
+
req: {
|
|
5138
|
+
prompt: params.prompt ?? "",
|
|
5139
|
+
candidates: params.candidates,
|
|
5140
|
+
minTier: params.minTier,
|
|
5141
|
+
maxCostUsd: params.maxCostUsd,
|
|
5142
|
+
expectedOutputTokens: params.expectedOutputTokens,
|
|
5143
|
+
maxOutputTokens: params.maxOutputTokens,
|
|
5144
|
+
requiredCapabilities: params.requiredCapabilities,
|
|
5145
|
+
allowUnpriced: params.allowUnpriced,
|
|
5146
|
+
budgetWaiver: params.budgetWaiver,
|
|
5147
|
+
fallback: params.fallback,
|
|
5148
|
+
},
|
|
5149
|
+
};
|
|
5150
|
+
}
|
|
5151
|
+
async function dispatchRoutedCliAsync(runtime, cli, params) {
|
|
5152
|
+
const outputFormat = routedOutputFormat(cli);
|
|
5153
|
+
const prep = prepareRoutedCli(runtime, cli, params, outputFormat);
|
|
5154
|
+
if (!("args" in prep))
|
|
5155
|
+
return prep;
|
|
5156
|
+
const { corrId, args } = prep;
|
|
5157
|
+
const prepCleanup = "cleanup" in prep && typeof prep.cleanup === "function"
|
|
5158
|
+
? prep.cleanup
|
|
5159
|
+
: undefined;
|
|
5160
|
+
try {
|
|
5161
|
+
const effectiveCompress = resolveEffectiveCompression(runtime.compression, {
|
|
5162
|
+
compressResponse: params.compressResponse,
|
|
5163
|
+
outputFormat,
|
|
5164
|
+
outputSchemaDeclared: false,
|
|
5165
|
+
});
|
|
5166
|
+
const frHandoff = buildAsyncFlightRecorderHandoff(cli, prep, undefined, outputFormat, params.optimizePrompt);
|
|
5167
|
+
const job = runtime.asyncJobManager.startJob(cli, args, corrId, undefined, resolveIdleTimeout(cli, undefined), outputFormat, params.forceRefresh, undefined, prepCleanup, frHandoff.flightRecorderEntry, frHandoff.extractUsage, true, prep.stdinPayload, effectiveCompress);
|
|
5168
|
+
return {
|
|
5169
|
+
content: [
|
|
5170
|
+
{
|
|
5171
|
+
type: "text",
|
|
5172
|
+
text: JSON.stringify({ success: true, job, sessionId: null }, null, 2),
|
|
5173
|
+
},
|
|
5174
|
+
],
|
|
5175
|
+
};
|
|
5176
|
+
}
|
|
5177
|
+
catch (error) {
|
|
5178
|
+
return createErrorResponse("route_request_async", 1, "", corrId, error);
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
async function runRouteRequestAsync(runtime, params) {
|
|
5182
|
+
const corrId = params.correlationId ?? randomUUID();
|
|
5183
|
+
const { env, routerConfig, req } = buildRouteContext(runtime, params);
|
|
5184
|
+
const decision = selectCandidate(req, env, routerConfig);
|
|
5185
|
+
if (!decision.chosen) {
|
|
5186
|
+
return routeErrorResponse(decision, 0, corrId);
|
|
5187
|
+
}
|
|
5188
|
+
const chosen = decision.chosen;
|
|
5189
|
+
const dispatchParams = {
|
|
5190
|
+
prompt: params.prompt,
|
|
5191
|
+
promptParts: params.promptParts,
|
|
5192
|
+
model: chosen.model,
|
|
5193
|
+
correlationId: corrId,
|
|
5194
|
+
optimizePrompt: params.optimizePrompt ?? false,
|
|
5195
|
+
optimizeResponse: params.optimizeResponse ?? false,
|
|
5196
|
+
compressResponse: params.compressResponse,
|
|
5197
|
+
forceRefresh: params.forceRefresh,
|
|
5198
|
+
};
|
|
5199
|
+
let result;
|
|
5200
|
+
if (isCliTypeValue(chosen.provider)) {
|
|
5201
|
+
result = await dispatchRoutedCliAsync(runtime, chosen.provider, dispatchParams);
|
|
5202
|
+
}
|
|
5203
|
+
else {
|
|
5204
|
+
const api = enabledApiProviders(runtime.providers).find(p => p.name === chosen.provider);
|
|
5205
|
+
if (!api) {
|
|
5206
|
+
return createErrorResponse("route_request_async", 1, `Routed provider '${chosen.provider}' is not an enabled API provider`, corrId);
|
|
5207
|
+
}
|
|
5208
|
+
result = await handleApiProviderRequestAsync(runtime, api, {
|
|
5209
|
+
prompt: params.prompt,
|
|
5210
|
+
promptParts: params.promptParts,
|
|
5211
|
+
model: chosen.model,
|
|
5212
|
+
correlationId: corrId,
|
|
5213
|
+
optimizePrompt: params.optimizePrompt ?? false,
|
|
5214
|
+
optimizeResponse: params.optimizeResponse ?? false,
|
|
5215
|
+
});
|
|
5216
|
+
}
|
|
5217
|
+
recordRoutingDecision(runtime, corrId, decision, 0);
|
|
5218
|
+
return attachRouting(result, buildRoutingBlock(decision, 0));
|
|
5219
|
+
}
|
|
4769
5220
|
export function createGatewayServer(deps = {}) {
|
|
4770
5221
|
const runtime = resolveGatewayServerRuntime(deps, { isolateState: true });
|
|
4771
5222
|
const { sessionManager, asyncJobManager, approvalManager, performanceMetrics, logger, persistence, flightRecorder, cacheAwareness, providers, } = runtime;
|
|
@@ -4779,6 +5230,9 @@ export function createGatewayServer(deps = {}) {
|
|
|
4779
5230
|
asyncJobManager,
|
|
4780
5231
|
apiProviders: enabledApiProviders(providers),
|
|
4781
5232
|
validationRunStore: asyncJobManager.getValidationRunStore(),
|
|
5233
|
+
leastCost: runtime.leastCost,
|
|
5234
|
+
performanceMetrics: runtime.performanceMetrics,
|
|
5235
|
+
flightRecorder: runtime.flightRecorder,
|
|
4782
5236
|
});
|
|
4783
5237
|
registerWorkspaceTools(server, runtime);
|
|
4784
5238
|
registerProviderAdminTools(server, {
|
|
@@ -4790,6 +5244,93 @@ export function createGatewayServer(deps = {}) {
|
|
|
4790
5244
|
if (apiProviderTools.length > 0) {
|
|
4791
5245
|
runtime.logger.info(`Registered API provider tools: ${apiProviderTools.join(", ")}`);
|
|
4792
5246
|
}
|
|
5247
|
+
if (runtime.leastCost.enabled) {
|
|
5248
|
+
const RouteCandidateShape = z.object({
|
|
5249
|
+
provider: z.string().min(1).describe("A CLI_TYPES member or an enabled API-provider name."),
|
|
5250
|
+
model: z.string().min(1).describe("A concrete model id/alias that provider can serve."),
|
|
5251
|
+
});
|
|
5252
|
+
const RouteRequiredCapabilitiesShape = z
|
|
5253
|
+
.object({
|
|
5254
|
+
images: z.boolean().optional(),
|
|
5255
|
+
attachments: z.boolean().optional(),
|
|
5256
|
+
toolCalling: z.boolean().optional(),
|
|
5257
|
+
jsonSchema: z.boolean().optional(),
|
|
5258
|
+
outputFormat: z.string().optional(),
|
|
5259
|
+
effort: z.string().optional(),
|
|
5260
|
+
})
|
|
5261
|
+
.describe("Hard capability constraints; a candidate missing any required capability is excluded.");
|
|
5262
|
+
const routeInputShape = {
|
|
5263
|
+
prompt: z
|
|
5264
|
+
.string()
|
|
5265
|
+
.min(1)
|
|
5266
|
+
.max(100000)
|
|
5267
|
+
.optional()
|
|
5268
|
+
.describe("Prompt text to route (mutually exclusive with promptParts)."),
|
|
5269
|
+
promptParts: PromptPartsSchema.optional().describe("Cache-aware structured prompt (mutually exclusive with prompt)."),
|
|
5270
|
+
candidates: z
|
|
5271
|
+
.array(RouteCandidateShape)
|
|
5272
|
+
.optional()
|
|
5273
|
+
.describe("Explicit (provider, model) pool to restrict routing to; also whitelists untiered / maintain-only candidates. Empty/omitted = all eligible."),
|
|
5274
|
+
minTier: z
|
|
5275
|
+
.enum(["economy", "standard", "frontier"])
|
|
5276
|
+
.optional()
|
|
5277
|
+
.describe("Minimum quality tier floor (default from config, usually 'standard'). No silent downgrade below it."),
|
|
5278
|
+
maxCostUsd: z
|
|
5279
|
+
.number()
|
|
5280
|
+
.positive()
|
|
5281
|
+
.optional()
|
|
5282
|
+
.describe("Per-request budget cap (USD). Over-budget fails closed unless waived via an explicit fallback + budgetWaiver."),
|
|
5283
|
+
maxOutputTokens: z
|
|
5284
|
+
.number()
|
|
5285
|
+
.int()
|
|
5286
|
+
.positive()
|
|
5287
|
+
.optional()
|
|
5288
|
+
.describe("Caller output cap; bounds the conservative budget estimate."),
|
|
5289
|
+
expectedOutputTokens: z
|
|
5290
|
+
.number()
|
|
5291
|
+
.int()
|
|
5292
|
+
.positive()
|
|
5293
|
+
.optional()
|
|
5294
|
+
.describe("Expected output tokens for the ranking estimate (default from config)."),
|
|
5295
|
+
requiredCapabilities: RouteRequiredCapabilitiesShape.optional(),
|
|
5296
|
+
allowUnpriced: z
|
|
5297
|
+
.boolean()
|
|
5298
|
+
.optional()
|
|
5299
|
+
.describe("Admit unpriced (source 'unknown') candidates. They still rank strictly last and cannot pass the budget gate without budgetWaiver."),
|
|
5300
|
+
budgetWaiver: z
|
|
5301
|
+
.boolean()
|
|
5302
|
+
.optional()
|
|
5303
|
+
.describe("Explicit acknowledgment to admit an unpriced or over-budget fallback candidate (with allowUnpriced / fallback)."),
|
|
5304
|
+
fallback: RouteCandidateShape.optional().describe("A pinned (provider, model) used only when the eligible pool is empty; bypasses cost ranking but still passes eligibility + budget (unless budgetWaiver)."),
|
|
5305
|
+
optimizePrompt: z
|
|
5306
|
+
.boolean()
|
|
5307
|
+
.optional()
|
|
5308
|
+
.describe("Apply prompt token optimization before dispatch."),
|
|
5309
|
+
optimizeResponse: z.boolean().optional().describe("Apply response token optimization."),
|
|
5310
|
+
compressResponse: z
|
|
5311
|
+
.boolean()
|
|
5312
|
+
.optional()
|
|
5313
|
+
.describe("Apply the native display-text compressor to the response."),
|
|
5314
|
+
correlationId: z.string().optional().describe("Caller-supplied correlation id for tracing."),
|
|
5315
|
+
forceRefresh: z
|
|
5316
|
+
.boolean()
|
|
5317
|
+
.optional()
|
|
5318
|
+
.describe("Bypass the async dedup window for this request."),
|
|
5319
|
+
};
|
|
5320
|
+
const routeAnnotations = {
|
|
5321
|
+
readOnlyHint: false,
|
|
5322
|
+
destructiveHint: true,
|
|
5323
|
+
idempotentHint: false,
|
|
5324
|
+
openWorldHint: true,
|
|
5325
|
+
};
|
|
5326
|
+
server.tool("route_request", "Least-cost routing: pick the cheapest eligible (provider, model) candidate that meets the caller's quality tier, capability, and budget constraints, then dispatch it through the normal provider path. Returns the provider response plus a `routing` block (chosen candidate, cost estimate + basis/confidence, rejections, reroutes). Registered only when [least_cost].enabled is true.", routeInputShape, { title: "Least-cost route (sync)", ...routeAnnotations }, async (params) => runRouteRequest(runtime, params));
|
|
5327
|
+
let registeredRouteTools = "route_request";
|
|
5328
|
+
if (asyncJobsEnabled) {
|
|
5329
|
+
server.tool("route_request_async", "Async least-cost routing: select the cheapest eligible (provider, model) candidate synchronously, enqueue it for background execution, and return a pollable job handle plus the `routing` block. Poll with llm_job_status, collect with llm_job_result. Registered only when [least_cost].enabled is true and async jobs are enabled.", routeInputShape, { title: "Least-cost route (async)", ...routeAnnotations }, async (params) => runRouteRequestAsync(runtime, params));
|
|
5330
|
+
registeredRouteTools += ", route_request_async";
|
|
5331
|
+
}
|
|
5332
|
+
runtime.logger.info(`Registered least-cost routing tools: ${registeredRouteTools}`);
|
|
5333
|
+
}
|
|
4793
5334
|
if (grokApiToolsEnabled) {
|
|
4794
5335
|
server.tool("grok_api_request", "Run an xAI Grok API request synchronously through the Responses API. Requires exactly one of prompt or promptParts. Registered only when [providers.xai] is configured and its API-key env var is present.", {
|
|
4795
5336
|
prompt: z
|
|
@@ -4902,7 +5443,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
4902
5443
|
permissionMode: z
|
|
4903
5444
|
.enum(CLAUDE_PERMISSION_MODES)
|
|
4904
5445
|
.optional()
|
|
4905
|
-
.describe("Claude --permission-mode: default|acceptEdits|
|
|
5446
|
+
.describe("Claude --permission-mode: default|acceptEdits|auto|bypassPermissions|manual|dontAsk|plan. `default` is a no-op (no flag emitted)."),
|
|
4906
5447
|
agent: z
|
|
4907
5448
|
.string()
|
|
4908
5449
|
.optional()
|
|
@@ -5181,6 +5722,13 @@ export function createGatewayServer(deps = {}) {
|
|
|
5181
5722
|
if (parsed.costUsd !== null) {
|
|
5182
5723
|
logger.debug(`[${corrId}] stream-json cost=$${parsed.costUsd}, model=${parsed.model}, turns=${parsed.numTurns}`);
|
|
5183
5724
|
}
|
|
5725
|
+
const { costUsd: claudeCostUsd, costBasis: claudeCostBasis } = deriveCostBasis("claude", prep.resolvedModel || "default", {
|
|
5726
|
+
inputTokens: parsed.usage?.inputTokens,
|
|
5727
|
+
outputTokens: parsed.usage?.outputTokens,
|
|
5728
|
+
cacheReadTokens: parsed.usage?.cacheReadInputTokens || undefined,
|
|
5729
|
+
cacheCreationTokens: parsed.usage?.cacheCreationInputTokens || undefined,
|
|
5730
|
+
costUsd: parsed.costUsd ?? undefined,
|
|
5731
|
+
});
|
|
5184
5732
|
safeFlightComplete(corrId, {
|
|
5185
5733
|
response: parsed.text,
|
|
5186
5734
|
inputTokens: parsed.usage?.inputTokens,
|
|
@@ -5190,7 +5738,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
5190
5738
|
durationMs,
|
|
5191
5739
|
retryCount: 0,
|
|
5192
5740
|
circuitBreakerState: "closed",
|
|
5193
|
-
costUsd:
|
|
5741
|
+
costUsd: claudeCostUsd,
|
|
5742
|
+
costBasis: claudeCostBasis,
|
|
5194
5743
|
optimizationApplied: optimizePrompt || optimizeResponse,
|
|
5195
5744
|
exitCode: 0,
|
|
5196
5745
|
status: "completed",
|
|
@@ -5432,7 +5981,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
5432
5981
|
}, runtime);
|
|
5433
5982
|
logger.info(`[${corrId}] codex_request invoked with model=${prep.resolvedModel || "default"}, fullAuto=${fullAuto}, prompt length=${prep.effectivePrompt.length}`);
|
|
5434
5983
|
const prepCleanup = "cleanup" in prep && typeof prep.cleanup === "function" ? prep.cleanup : undefined;
|
|
5435
|
-
let worktreeResolution
|
|
5984
|
+
let worktreeResolution;
|
|
5436
5985
|
try {
|
|
5437
5986
|
worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
|
|
5438
5987
|
provider: "codex",
|
|
@@ -5505,6 +6054,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
5505
6054
|
}
|
|
5506
6055
|
logger.info(`[${corrId}] codex_request completed successfully in ${durationMs}ms`);
|
|
5507
6056
|
const codexUsage = extractUsageAndCost("codex", stdout, outputFormat);
|
|
6057
|
+
const { costUsd: codexCostUsd, costBasis: codexCostBasis } = deriveCostBasis("codex", prep.resolvedModel || "default", codexUsage);
|
|
5508
6058
|
const codexMeta = extractProviderOutputMetadata("codex", stdout, outputFormat);
|
|
5509
6059
|
safeFlightComplete(corrId, {
|
|
5510
6060
|
response: codexFrResponse(outputFormat, stdout),
|
|
@@ -5518,7 +6068,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
5518
6068
|
outputTokens: codexUsage.outputTokens,
|
|
5519
6069
|
cacheReadTokens: codexUsage.cacheReadTokens,
|
|
5520
6070
|
cacheCreationTokens: codexUsage.cacheCreationTokens,
|
|
5521
|
-
costUsd:
|
|
6071
|
+
costUsd: codexCostUsd,
|
|
6072
|
+
costBasis: codexCostBasis,
|
|
5522
6073
|
providerSessionId: codexMeta.sessionId,
|
|
5523
6074
|
stopReason: codexMeta.stopReason,
|
|
5524
6075
|
}, runtime);
|
|
@@ -5968,9 +6519,9 @@ export function createGatewayServer(deps = {}) {
|
|
|
5968
6519
|
.default("cli")
|
|
5969
6520
|
.describe("Transport: 'cli' (default) runs the Devin CLI; 'acp' routes through `devin acp` when [acp].enabled and the provider's runtime_enabled are set (fails closed otherwise)."),
|
|
5970
6521
|
permissionMode: z
|
|
5971
|
-
.enum(["auto", "smart", "dangerous"])
|
|
6522
|
+
.enum(["auto", "accept-edits", "smart", "dangerous"])
|
|
5972
6523
|
.optional()
|
|
5973
|
-
.describe("Devin CLI permission mode (--permission-mode). auto auto-approves read-only tools; smart additionally auto-runs actions a fast model judges safe; dangerous auto-approves all. When omitted, Devin uses its own headless default;
|
|
6524
|
+
.describe("Devin CLI permission mode (--permission-mode). auto auto-approves read-only tools; accept-edits also auto-approves workspace edits; smart additionally auto-runs actions a fast model judges safe; dangerous auto-approves all. When omitted, Devin uses its own headless default; choose an explicit mode for unattended runs."),
|
|
5974
6525
|
promptFile: z
|
|
5975
6526
|
.string()
|
|
5976
6527
|
.optional()
|
|
@@ -6210,7 +6761,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
6210
6761
|
disallowedTools: z
|
|
6211
6762
|
.array(z.string())
|
|
6212
6763
|
.optional()
|
|
6213
|
-
.describe("
|
|
6764
|
+
.describe("Denylist of built-in tools, each emitted as a separate --disabled-tools <tool> flag"),
|
|
6214
6765
|
correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
|
|
6215
6766
|
optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
|
|
6216
6767
|
optimizeResponse: z.boolean().default(false).describe("Optimize response output"),
|
|
@@ -6324,7 +6875,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
6324
6875
|
permissionMode: z
|
|
6325
6876
|
.enum(CLAUDE_PERMISSION_MODES)
|
|
6326
6877
|
.optional()
|
|
6327
|
-
.describe("Claude --permission-mode: default|acceptEdits|
|
|
6878
|
+
.describe("Claude --permission-mode: default|acceptEdits|auto|bypassPermissions|manual|dontAsk|plan. `default` is a no-op."),
|
|
6328
6879
|
agent: z
|
|
6329
6880
|
.string()
|
|
6330
6881
|
.optional()
|
|
@@ -7134,9 +7685,9 @@ export function createGatewayServer(deps = {}) {
|
|
|
7134
7685
|
.describe("Prompt text for Devin CLI. Required in practice; promptFile is additive (loads an initial prompt from a file)."),
|
|
7135
7686
|
model: z.string().optional().describe("Model name or alias (e.g. opus, latest)"),
|
|
7136
7687
|
permissionMode: z
|
|
7137
|
-
.enum(["auto", "smart", "dangerous"])
|
|
7688
|
+
.enum(["auto", "accept-edits", "smart", "dangerous"])
|
|
7138
7689
|
.optional()
|
|
7139
|
-
.describe("Devin CLI permission mode (--permission-mode). auto, smart, or dangerous. When omitted, Devin uses its own headless default;
|
|
7690
|
+
.describe("Devin CLI permission mode (--permission-mode). auto, accept-edits, smart, or dangerous. When omitted, Devin uses its own headless default; choose an explicit mode for unattended runs."),
|
|
7140
7691
|
promptFile: z
|
|
7141
7692
|
.string()
|
|
7142
7693
|
.optional()
|
|
@@ -7359,7 +7910,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
7359
7910
|
disallowedTools: z
|
|
7360
7911
|
.array(z.string())
|
|
7361
7912
|
.optional()
|
|
7362
|
-
.describe("
|
|
7913
|
+
.describe("Denylist of built-in tools, each emitted as a separate --disabled-tools <tool> flag"),
|
|
7363
7914
|
correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
|
|
7364
7915
|
optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
|
|
7365
7916
|
compressResponse: z
|
|
@@ -7683,21 +8234,28 @@ export function createGatewayServer(deps = {}) {
|
|
|
7683
8234
|
}, async () => {
|
|
7684
8235
|
const health = asyncJobManager.getJobHealth();
|
|
7685
8236
|
const storeAttached = asyncJobManager.hasStore();
|
|
7686
|
-
const
|
|
8237
|
+
const durableAdmission = asyncJobManager.getDurableAdmissionHealth();
|
|
8238
|
+
const asyncJobsConfigured = persistence.backend !== "none" && persistence.asyncJobsEnabled;
|
|
8239
|
+
const asyncJobsEffective = asyncJobsConfigured && storeAttached && durableAdmission.admitting;
|
|
7687
8240
|
const persistenceBlock = {
|
|
7688
8241
|
backend: persistence.backend,
|
|
7689
8242
|
dbPath: persistence.path,
|
|
7690
8243
|
dsn: persistence.dsn ? "[redacted]" : null,
|
|
7691
8244
|
retentionDays: persistence.retentionDays,
|
|
7692
8245
|
dedupWindowMs: persistence.dedupWindowMs,
|
|
8246
|
+
asyncJobsConfigured,
|
|
8247
|
+
storeAttached,
|
|
7693
8248
|
asyncJobsEnabled: asyncJobsEffective,
|
|
8249
|
+
durableAdmission,
|
|
7694
8250
|
acknowledgeEphemeral: persistence.acknowledgeEphemeral,
|
|
7695
8251
|
sources: persistence.sources,
|
|
7696
8252
|
warning: asyncJobsEffective
|
|
7697
8253
|
? null
|
|
7698
8254
|
: persistence.backend === "none"
|
|
7699
8255
|
? "Async job persistence is disabled (backend = 'none'). *_request_async tools are NOT registered on this gateway. Set [persistence].backend = 'sqlite' (or 'memory' + acknowledgeEphemeral = true) to enable them."
|
|
7700
|
-
:
|
|
8256
|
+
: !storeAttached
|
|
8257
|
+
? `Async job persistence is configured (backend = '${persistence.backend}') but the durable job store failed to open, so *_request_async / llm_job_* tools are NOT registered on this gateway. Check gateway startup logs for the store-open error.`
|
|
8258
|
+
: "Async job persistence is attached but durable admission is temporarily disabled while its heartbeat lease recovers. Existing async tools fail closed until admission is restored.",
|
|
7701
8259
|
};
|
|
7702
8260
|
const outboundProviders = {
|
|
7703
8261
|
xai: providers.xai
|
|
@@ -7763,6 +8321,46 @@ export function createGatewayServer(deps = {}) {
|
|
|
7763
8321
|
external: mem.external,
|
|
7764
8322
|
},
|
|
7765
8323
|
};
|
|
8324
|
+
const leastCostBlock = (() => {
|
|
8325
|
+
const lc = runtime.leastCost;
|
|
8326
|
+
if (!lc.enabled)
|
|
8327
|
+
return { enabled: false };
|
|
8328
|
+
const env = buildRouterEnv({
|
|
8329
|
+
performanceMetrics: runtime.performanceMetrics,
|
|
8330
|
+
limiterSnapshot: runtime.asyncJobManager.getLimiterSnapshot(),
|
|
8331
|
+
apiProviders: enabledApiProviders(runtime.providers),
|
|
8332
|
+
preferCatalogPrice: lc.preferCatalogPrice,
|
|
8333
|
+
});
|
|
8334
|
+
const providerHealth = env.providers().map(provider => ({
|
|
8335
|
+
provider,
|
|
8336
|
+
telemetryTier: telemetryTierFor(provider),
|
|
8337
|
+
authed: env.isAuthed(provider),
|
|
8338
|
+
breakerState: env.breakerState(provider),
|
|
8339
|
+
atCapacity: env.atCapacity(provider),
|
|
8340
|
+
candidates: env.models(provider).map(model => {
|
|
8341
|
+
const mc = env.modelCost(provider, model);
|
|
8342
|
+
const family = modelIdToFamily(model);
|
|
8343
|
+
const tier = lc.tiers[`${provider}:${family}`];
|
|
8344
|
+
return {
|
|
8345
|
+
model,
|
|
8346
|
+
family: mc.family,
|
|
8347
|
+
priced: mc.source !== "unknown",
|
|
8348
|
+
priceSource: mc.source,
|
|
8349
|
+
tier: tier ?? null,
|
|
8350
|
+
};
|
|
8351
|
+
}),
|
|
8352
|
+
}));
|
|
8353
|
+
return {
|
|
8354
|
+
enabled: true,
|
|
8355
|
+
minTier: lc.minTier,
|
|
8356
|
+
maxCostUsd: lc.maxCostUsd,
|
|
8357
|
+
priorsScope: lc.priorsScope,
|
|
8358
|
+
preferCatalogPrice: lc.preferCatalogPrice,
|
|
8359
|
+
allowUnpriced: lc.allowUnpriced,
|
|
8360
|
+
maxReroutes: lc.maxReroutes,
|
|
8361
|
+
providers: providerHealth,
|
|
8362
|
+
};
|
|
8363
|
+
})();
|
|
7766
8364
|
return {
|
|
7767
8365
|
content: [
|
|
7768
8366
|
{
|
|
@@ -7773,6 +8371,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
7773
8371
|
backpressure,
|
|
7774
8372
|
persistence: persistenceBlock,
|
|
7775
8373
|
outboundProviders,
|
|
8374
|
+
leastCost: leastCostBlock,
|
|
7776
8375
|
}, null, 2),
|
|
7777
8376
|
},
|
|
7778
8377
|
],
|
|
@@ -8441,7 +9040,7 @@ async function initializeSessionManager() {
|
|
|
8441
9040
|
});
|
|
8442
9041
|
logger.info("File-based session manager initialized");
|
|
8443
9042
|
}
|
|
8444
|
-
resourceProvider = new ResourceProvider(sessionManager, performanceMetrics, getFlightRecorder(logger), getCacheAwarenessConfig(logger), getProvidersConfig(logger), undefined, getAcpConfig(logger));
|
|
9043
|
+
resourceProvider = new ResourceProvider(sessionManager, performanceMetrics, getFlightRecorder(logger), getCacheAwarenessConfig(logger), getProvidersConfig(logger), undefined, getAcpConfig(logger), getLeastCostConfig(logger));
|
|
8445
9044
|
}
|
|
8446
9045
|
function registerHealthResource(server) {
|
|
8447
9046
|
if (db) {
|
|
@@ -8468,20 +9067,26 @@ function registerHealthResource(server) {
|
|
|
8468
9067
|
description: "Async job health (CPU, memory, zombie detection)",
|
|
8469
9068
|
mimeType: "application/json",
|
|
8470
9069
|
}, async (uri) => {
|
|
8471
|
-
const
|
|
9070
|
+
const manager = getAsyncJobManager();
|
|
9071
|
+
const health = manager.getJobHealth();
|
|
8472
9072
|
return {
|
|
8473
9073
|
contents: [
|
|
8474
9074
|
{
|
|
8475
9075
|
uri: uri.href,
|
|
8476
9076
|
mimeType: "application/json",
|
|
8477
|
-
text: JSON.stringify(health, null, 2),
|
|
9077
|
+
text: JSON.stringify({ ...health, durableAdmission: manager.getDurableAdmissionHealth() }, null, 2),
|
|
8478
9078
|
},
|
|
8479
9079
|
],
|
|
8480
9080
|
};
|
|
8481
9081
|
});
|
|
8482
9082
|
logger.info("Process health resource registered");
|
|
8483
9083
|
}
|
|
8484
|
-
|
|
9084
|
+
let shutdownPromise = null;
|
|
9085
|
+
function shutdown(signal, exitCode = 0) {
|
|
9086
|
+
shutdownPromise ??= performShutdown(signal, exitCode);
|
|
9087
|
+
return shutdownPromise;
|
|
9088
|
+
}
|
|
9089
|
+
async function performShutdown(signal, exitCode) {
|
|
8485
9090
|
logger.info(`Received ${signal}, shutting down gracefully...`);
|
|
8486
9091
|
try {
|
|
8487
9092
|
if (asyncJobManager) {
|
|
@@ -8505,6 +9110,12 @@ async function shutdown(signal) {
|
|
|
8505
9110
|
logger.info("MCP server closed");
|
|
8506
9111
|
activeServer = null;
|
|
8507
9112
|
}
|
|
9113
|
+
if (jobStore) {
|
|
9114
|
+
jobStore.close();
|
|
9115
|
+
logger.info("Durable job store closed");
|
|
9116
|
+
jobStore = null;
|
|
9117
|
+
jobStoreInitialized = false;
|
|
9118
|
+
}
|
|
8508
9119
|
if (db) {
|
|
8509
9120
|
await db.disconnect();
|
|
8510
9121
|
logger.info("Database connections closed");
|
|
@@ -8513,7 +9124,7 @@ async function shutdown(signal) {
|
|
|
8513
9124
|
flightRecorder.close();
|
|
8514
9125
|
logger.info("Flight recorder closed");
|
|
8515
9126
|
}
|
|
8516
|
-
process.exit(
|
|
9127
|
+
process.exit(exitCode);
|
|
8517
9128
|
}
|
|
8518
9129
|
catch (error) {
|
|
8519
9130
|
logger.error("Error during shutdown:", error);
|
|
@@ -8907,15 +9518,23 @@ async function main() {
|
|
|
8907
9518
|
logger.info(`Starting llm-cli-gateway MCP server with ${transportMode} transport`);
|
|
8908
9519
|
await initializeSessionManager();
|
|
8909
9520
|
void warmProviderCapabilities({ logger }).catch(() => undefined);
|
|
9521
|
+
const persistence = getPersistenceConfig(logger);
|
|
9522
|
+
const runtimeAsyncJobManager = getAsyncJobManager(logger);
|
|
9523
|
+
if (persistence.backend !== "none" &&
|
|
9524
|
+
persistence.asyncJobsEnabled &&
|
|
9525
|
+
!runtimeAsyncJobManager.hasStore()) {
|
|
9526
|
+
throw new Error(`Configured ${persistence.backend} job store could not be opened; refusing to start without durable async persistence`);
|
|
9527
|
+
}
|
|
8910
9528
|
const serverDeps = {
|
|
8911
9529
|
sessionManager,
|
|
8912
9530
|
resourceProvider,
|
|
8913
9531
|
db,
|
|
8914
9532
|
performanceMetrics,
|
|
8915
|
-
asyncJobManager:
|
|
9533
|
+
asyncJobManager: runtimeAsyncJobManager,
|
|
8916
9534
|
approvalManager: getApprovalManager(logger),
|
|
8917
9535
|
flightRecorder: getFlightRecorder(logger),
|
|
8918
9536
|
logger,
|
|
9537
|
+
persistence,
|
|
8919
9538
|
};
|
|
8920
9539
|
if (transportMode === "http") {
|
|
8921
9540
|
activeHttpGateway = await startHttpGateway({
|
|
@@ -8932,6 +9551,12 @@ async function main() {
|
|
|
8932
9551
|
activeServer = createGatewayServer({
|
|
8933
9552
|
...serverDeps,
|
|
8934
9553
|
});
|
|
9554
|
+
activeServer.server.onclose = () => {
|
|
9555
|
+
void shutdown("stdio MCP connection closed");
|
|
9556
|
+
};
|
|
9557
|
+
process.stdin.once("end", () => {
|
|
9558
|
+
void shutdown("stdio stdin EOF");
|
|
9559
|
+
});
|
|
8935
9560
|
registerHealthResource(activeServer);
|
|
8936
9561
|
const transport = new StdioServerTransport();
|
|
8937
9562
|
await activeServer.connect(transport);
|
|
@@ -8941,6 +9566,6 @@ const __entryUrl = entrypointFileURL(process.argv[1]);
|
|
|
8941
9566
|
if (__entryUrl === import.meta.url) {
|
|
8942
9567
|
main().catch(error => {
|
|
8943
9568
|
logger.error("Fatal server error:", error);
|
|
8944
|
-
|
|
9569
|
+
void shutdown("fatal startup error", 1);
|
|
8945
9570
|
});
|
|
8946
9571
|
}
|