llm-cli-gateway 2.16.0 → 2.17.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/.agents/skills/least-cost-routing/SKILL.md +123 -0
- package/CHANGELOG.md +28 -0
- 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 +41 -1
- package/dist/index.js +658 -30
- 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-definitions.d.ts +4 -0
- package/dist/provider-definitions.js +28 -0
- package/dist/request-helpers.d.ts +2 -2
- 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.js +1 -1
- 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 +9 -4
- 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) {
|
|
@@ -2981,6 +3005,7 @@ export async function handleApiProviderRequest(runtimeArg, providerRuntime, para
|
|
|
2981
3005
|
inputTokens: result.usage?.inputTokens,
|
|
2982
3006
|
outputTokens: result.usage?.outputTokens,
|
|
2983
3007
|
cacheReadTokens: result.usage?.cacheReadTokens,
|
|
3008
|
+
cacheCreationTokens: result.usage?.cacheCreationTokens,
|
|
2984
3009
|
costUsd: result.usage?.costUsd,
|
|
2985
3010
|
}, runtimeArg);
|
|
2986
3011
|
return buildApiSuccessResponse(text, corrId, providerRuntime.name, {
|
|
@@ -3252,7 +3277,7 @@ export async function handleGeminiRequest(deps, params) {
|
|
|
3252
3277
|
return createErrorResponse("gemini", code, stderr, corrId);
|
|
3253
3278
|
}
|
|
3254
3279
|
wasSuccessful = true;
|
|
3255
|
-
|
|
3280
|
+
const effectiveSessionId = effectiveSessionIdHint;
|
|
3256
3281
|
if (effectiveSessionId) {
|
|
3257
3282
|
const existing = await deps.sessionManager.getSession(effectiveSessionId);
|
|
3258
3283
|
if (!existing) {
|
|
@@ -3277,6 +3302,7 @@ export async function handleGeminiRequest(deps, params) {
|
|
|
3277
3302
|
}
|
|
3278
3303
|
}
|
|
3279
3304
|
const geminiUsage = extractUsageAndCost("gemini", stdout, params.outputFormat);
|
|
3305
|
+
const { costUsd: geminiCostUsd, costBasis: geminiCostBasis } = deriveCostBasis("gemini", prep.resolvedModel || "default", geminiUsage);
|
|
3280
3306
|
const geminiMeta = extractProviderOutputMetadata("gemini", stdout, params.outputFormat);
|
|
3281
3307
|
safeFlightComplete(corrId, {
|
|
3282
3308
|
response: stdout,
|
|
@@ -3291,7 +3317,8 @@ export async function handleGeminiRequest(deps, params) {
|
|
|
3291
3317
|
outputTokens: geminiUsage.outputTokens,
|
|
3292
3318
|
cacheReadTokens: geminiUsage.cacheReadTokens,
|
|
3293
3319
|
cacheCreationTokens: geminiUsage.cacheCreationTokens,
|
|
3294
|
-
costUsd:
|
|
3320
|
+
costUsd: geminiCostUsd,
|
|
3321
|
+
costBasis: geminiCostBasis,
|
|
3295
3322
|
providerSessionId: geminiMeta.sessionId,
|
|
3296
3323
|
stopReason: geminiMeta.stopReason,
|
|
3297
3324
|
}, runtime);
|
|
@@ -3352,7 +3379,7 @@ export async function handleGeminiRequestAsync(deps, params) {
|
|
|
3352
3379
|
resumeLatest: params.resumeLatest,
|
|
3353
3380
|
createNewSession: params.createNewSession,
|
|
3354
3381
|
});
|
|
3355
|
-
|
|
3382
|
+
const effectiveSessionId = sessionPlan.resumed ? params.sessionId : undefined;
|
|
3356
3383
|
const existingSession = await getExistingSessionForProvider(deps.sessionManager, effectiveSessionId, "gemini");
|
|
3357
3384
|
args.push(...sessionPlan.args);
|
|
3358
3385
|
if (effectiveSessionId) {
|
|
@@ -4730,14 +4757,8 @@ export async function handleCodexRequestAsync(deps, params) {
|
|
|
4730
4757
|
outputSchemaDeclared: params.outputSchema !== undefined,
|
|
4731
4758
|
});
|
|
4732
4759
|
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
|
-
}
|
|
4760
|
+
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);
|
|
4761
|
+
prepCleanupOwnedHere = false;
|
|
4741
4762
|
deps.logger.info(`[${corrId}] codex_request_async started job ${job.id}`);
|
|
4742
4763
|
const asyncResponse = {
|
|
4743
4764
|
success: true,
|
|
@@ -4766,6 +4787,439 @@ export async function handleCodexRequestAsync(deps, params) {
|
|
|
4766
4787
|
return createErrorResponse("codex_request_async", 1, "", corrId, error);
|
|
4767
4788
|
}
|
|
4768
4789
|
}
|
|
4790
|
+
function routedOutputFormat(provider) {
|
|
4791
|
+
return provider === "claude" || provider === "gemini" ? "stream-json" : "text";
|
|
4792
|
+
}
|
|
4793
|
+
function prepareRoutedCli(runtime, cli, params, outputFormat) {
|
|
4794
|
+
const base = {
|
|
4795
|
+
prompt: params.prompt,
|
|
4796
|
+
promptParts: params.promptParts,
|
|
4797
|
+
model: params.model,
|
|
4798
|
+
correlationId: params.correlationId,
|
|
4799
|
+
optimizePrompt: params.optimizePrompt,
|
|
4800
|
+
operation: "route_request",
|
|
4801
|
+
outputFormat,
|
|
4802
|
+
};
|
|
4803
|
+
switch (cli) {
|
|
4804
|
+
case "claude": {
|
|
4805
|
+
const p = {
|
|
4806
|
+
...base,
|
|
4807
|
+
dangerouslySkipPermissions: false,
|
|
4808
|
+
approvalStrategy: "legacy",
|
|
4809
|
+
strictMcpConfig: false,
|
|
4810
|
+
};
|
|
4811
|
+
return prepareClaudeRequest(p, runtime);
|
|
4812
|
+
}
|
|
4813
|
+
case "codex": {
|
|
4814
|
+
const p = {
|
|
4815
|
+
...base,
|
|
4816
|
+
outputFormat: outputFormat,
|
|
4817
|
+
fullAuto: false,
|
|
4818
|
+
dangerouslyBypassApprovalsAndSandbox: false,
|
|
4819
|
+
approvalStrategy: "legacy",
|
|
4820
|
+
};
|
|
4821
|
+
return prepareCodexRequest(p, runtime);
|
|
4822
|
+
}
|
|
4823
|
+
case "gemini": {
|
|
4824
|
+
const p = { ...base, approvalStrategy: "legacy" };
|
|
4825
|
+
return prepareGeminiRequest(p, runtime);
|
|
4826
|
+
}
|
|
4827
|
+
case "grok": {
|
|
4828
|
+
const p = { ...base, approvalStrategy: "legacy" };
|
|
4829
|
+
return prepareGrokRequest(p, runtime);
|
|
4830
|
+
}
|
|
4831
|
+
case "mistral": {
|
|
4832
|
+
const p = { ...base, approvalStrategy: "legacy" };
|
|
4833
|
+
return prepareMistralRequest(p, runtime);
|
|
4834
|
+
}
|
|
4835
|
+
case "devin": {
|
|
4836
|
+
const p = { ...base };
|
|
4837
|
+
return prepareDevinRequest(p, runtime);
|
|
4838
|
+
}
|
|
4839
|
+
case "cursor": {
|
|
4840
|
+
const p = { ...base };
|
|
4841
|
+
return prepareCursorRequest(p, runtime);
|
|
4842
|
+
}
|
|
4843
|
+
default: {
|
|
4844
|
+
const exhaustive = cli;
|
|
4845
|
+
return exhaustive;
|
|
4846
|
+
}
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
async function dispatchRoutedCli(runtime, cli, params) {
|
|
4850
|
+
const startTime = Date.now();
|
|
4851
|
+
let durationMs = 0;
|
|
4852
|
+
let wasSuccessful = false;
|
|
4853
|
+
const outputFormat = routedOutputFormat(cli);
|
|
4854
|
+
const prep = prepareRoutedCli(runtime, cli, params, outputFormat);
|
|
4855
|
+
if (!("args" in prep))
|
|
4856
|
+
return prep;
|
|
4857
|
+
const { corrId, args } = prep;
|
|
4858
|
+
const prepCleanup = "cleanup" in prep && typeof prep.cleanup === "function"
|
|
4859
|
+
? prep.cleanup
|
|
4860
|
+
: undefined;
|
|
4861
|
+
safeFlightStart({
|
|
4862
|
+
correlationId: corrId,
|
|
4863
|
+
cli,
|
|
4864
|
+
model: prep.resolvedModel || "default",
|
|
4865
|
+
prompt: prep.effectivePrompt,
|
|
4866
|
+
stablePrefixHash: prep.stablePrefixHash ?? undefined,
|
|
4867
|
+
stablePrefixTokens: prep.stablePrefixTokens ?? undefined,
|
|
4868
|
+
}, runtime);
|
|
4869
|
+
try {
|
|
4870
|
+
const effectiveCompress = resolveEffectiveCompression(runtime.compression, {
|
|
4871
|
+
compressResponse: params.compressResponse,
|
|
4872
|
+
outputFormat,
|
|
4873
|
+
outputSchemaDeclared: false,
|
|
4874
|
+
});
|
|
4875
|
+
const frHandoff = buildAsyncFlightRecorderHandoff(cli, prep, undefined, outputFormat, params.optimizePrompt);
|
|
4876
|
+
const result = await awaitJobOrDefer(cli, args, corrId, resolveIdleTimeout(cli, undefined), outputFormat, params.forceRefresh, runtime, undefined, prepCleanup, frHandoff.flightRecorderEntry, frHandoff.extractUsage, prep.stdinPayload, undefined, effectiveCompress);
|
|
4877
|
+
if (isDeferredResponse(result)) {
|
|
4878
|
+
return buildDeferredToolResponse(result, undefined);
|
|
4879
|
+
}
|
|
4880
|
+
const { stdout, stderr, code } = result;
|
|
4881
|
+
durationMs = Math.max(0, Date.now() - startTime);
|
|
4882
|
+
if (code !== 0) {
|
|
4883
|
+
safeFlightComplete(corrId, {
|
|
4884
|
+
response: stderr || "",
|
|
4885
|
+
durationMs,
|
|
4886
|
+
retryCount: 0,
|
|
4887
|
+
circuitBreakerState: "closed",
|
|
4888
|
+
optimizationApplied: params.optimizePrompt || params.optimizeResponse,
|
|
4889
|
+
exitCode: code,
|
|
4890
|
+
errorMessage: stderr || `Exit code ${code}`,
|
|
4891
|
+
status: "failed",
|
|
4892
|
+
}, runtime);
|
|
4893
|
+
return createErrorResponse(cli, code, stderr, corrId);
|
|
4894
|
+
}
|
|
4895
|
+
wasSuccessful = true;
|
|
4896
|
+
const claudeStream = cli === "claude" && outputFormat === "stream-json";
|
|
4897
|
+
const parsedClaude = claudeStream ? parseStreamJson(stdout) : null;
|
|
4898
|
+
const responseText = parsedClaude ? parsedClaude.text : stdout;
|
|
4899
|
+
const usage = parsedClaude
|
|
4900
|
+
? {
|
|
4901
|
+
inputTokens: parsedClaude.usage?.inputTokens,
|
|
4902
|
+
outputTokens: parsedClaude.usage?.outputTokens,
|
|
4903
|
+
cacheReadTokens: parsedClaude.usage?.cacheReadInputTokens || undefined,
|
|
4904
|
+
cacheCreationTokens: parsedClaude.usage?.cacheCreationInputTokens || undefined,
|
|
4905
|
+
costUsd: parsedClaude.costUsd ?? undefined,
|
|
4906
|
+
}
|
|
4907
|
+
: extractUsageAndCost(cli, stdout, outputFormat);
|
|
4908
|
+
const meta = extractProviderOutputMetadata(cli, stdout, outputFormat);
|
|
4909
|
+
const { costUsd: recordedCostUsd, costBasis } = deriveCostBasis(cli, prep.resolvedModel || "default", usage);
|
|
4910
|
+
safeFlightComplete(corrId, {
|
|
4911
|
+
response: cli === "codex" ? codexFrResponse(outputFormat, stdout) : responseText,
|
|
4912
|
+
durationMs,
|
|
4913
|
+
retryCount: 0,
|
|
4914
|
+
circuitBreakerState: "closed",
|
|
4915
|
+
optimizationApplied: params.optimizePrompt || params.optimizeResponse,
|
|
4916
|
+
exitCode: 0,
|
|
4917
|
+
status: "completed",
|
|
4918
|
+
inputTokens: usage.inputTokens,
|
|
4919
|
+
outputTokens: usage.outputTokens,
|
|
4920
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
4921
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
4922
|
+
costUsd: recordedCostUsd,
|
|
4923
|
+
costBasis,
|
|
4924
|
+
providerSessionId: parsedClaude ? (parsedClaude.sessionId ?? undefined) : meta.sessionId,
|
|
4925
|
+
stopReason: parsedClaude ? (parsedClaude.stopReason ?? undefined) : meta.stopReason,
|
|
4926
|
+
}, runtime);
|
|
4927
|
+
const response = buildCliResponse(cli, responseText, params.optimizeResponse, corrId, undefined, prep, durationMs, undefined, outputFormat, undefined, effectiveCompress);
|
|
4928
|
+
safeRecordCompression(corrId, response.compression, runtime);
|
|
4929
|
+
return response;
|
|
4930
|
+
}
|
|
4931
|
+
catch (error) {
|
|
4932
|
+
const elapsedMs = Math.max(0, Date.now() - startTime);
|
|
4933
|
+
safeFlightComplete(corrId, {
|
|
4934
|
+
response: "",
|
|
4935
|
+
durationMs: elapsedMs,
|
|
4936
|
+
retryCount: 0,
|
|
4937
|
+
circuitBreakerState: "closed",
|
|
4938
|
+
optimizationApplied: params.optimizePrompt || params.optimizeResponse,
|
|
4939
|
+
exitCode: 1,
|
|
4940
|
+
errorMessage: error.message,
|
|
4941
|
+
status: "failed",
|
|
4942
|
+
}, runtime);
|
|
4943
|
+
return createErrorResponse(cli, 1, "", corrId, error);
|
|
4944
|
+
}
|
|
4945
|
+
finally {
|
|
4946
|
+
const finalizedDurationMs = Math.max(0, durationMs || Date.now() - startTime);
|
|
4947
|
+
runtime.performanceMetrics.recordRequest(cli, finalizedDurationMs, wasSuccessful);
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
async function dispatchRoutedRequest(runtime, chosen, params) {
|
|
4951
|
+
if (isCliTypeValue(chosen.provider)) {
|
|
4952
|
+
return dispatchRoutedCli(runtime, chosen.provider, params);
|
|
4953
|
+
}
|
|
4954
|
+
const api = enabledApiProviders(runtime.providers).find(p => p.name === chosen.provider);
|
|
4955
|
+
if (!api) {
|
|
4956
|
+
return createErrorResponse("route_request", 1, `Routed provider '${chosen.provider}' is not an enabled API provider`, params.correlationId);
|
|
4957
|
+
}
|
|
4958
|
+
const apiParams = {
|
|
4959
|
+
prompt: params.prompt,
|
|
4960
|
+
promptParts: params.promptParts,
|
|
4961
|
+
model: chosen.model,
|
|
4962
|
+
correlationId: params.correlationId,
|
|
4963
|
+
optimizePrompt: params.optimizePrompt,
|
|
4964
|
+
optimizeResponse: params.optimizeResponse,
|
|
4965
|
+
compressResponse: params.compressResponse,
|
|
4966
|
+
};
|
|
4967
|
+
return handleApiProviderRequest(runtime, api, apiParams);
|
|
4968
|
+
}
|
|
4969
|
+
function isCliTypeValue(provider) {
|
|
4970
|
+
return CLI_TYPES.includes(provider);
|
|
4971
|
+
}
|
|
4972
|
+
function routeCandidateKey(c) {
|
|
4973
|
+
return `${c.provider}:${c.model}`;
|
|
4974
|
+
}
|
|
4975
|
+
export function isTransientRouteFailure(provider, response) {
|
|
4976
|
+
const breakerOpen = isCliTypeValue(provider)
|
|
4977
|
+
? cliBreakerState(provider) !== "CLOSED"
|
|
4978
|
+
: apiProviderBreakerState(provider) !== "CLOSED";
|
|
4979
|
+
if (breakerOpen)
|
|
4980
|
+
return true;
|
|
4981
|
+
const sc = response.structuredContent;
|
|
4982
|
+
if (isDefaultTransient({ code: sc?.exitCode }))
|
|
4983
|
+
return true;
|
|
4984
|
+
const haystack = `${response.content.map(c => c.text).join(" ")} ${sc?.errorCategory ?? ""}`;
|
|
4985
|
+
return /\b(ETIMEDOUT|ECONNRESET|ECONNREFUSED|EPIPE)\b/i.test(haystack);
|
|
4986
|
+
}
|
|
4987
|
+
function attachRouting(response, block) {
|
|
4988
|
+
response.routing = block;
|
|
4989
|
+
response.structuredContent = { ...(response.structuredContent ?? {}), routing: block };
|
|
4990
|
+
const chosen = block.chosen ? `${block.chosen.provider}/${block.chosen.model}` : "none";
|
|
4991
|
+
const cost = block.estCostUsd !== undefined ? `~$${block.estCostUsd.toFixed(6)}` : "unpriced";
|
|
4992
|
+
const basis = block.costBasis ? ` (${block.costBasis}, ${block.confidence})` : "";
|
|
4993
|
+
const banner = `[routing] chosen=${chosen} est=${cost}${basis} considered=${block.consideredCount} reroutes=${block.reroutes}`;
|
|
4994
|
+
const first = response.content[0];
|
|
4995
|
+
if (first && first.type === "text") {
|
|
4996
|
+
first.text = `${banner}\n${first.text}`;
|
|
4997
|
+
}
|
|
4998
|
+
else {
|
|
4999
|
+
response.content.unshift({ type: "text", text: banner });
|
|
5000
|
+
}
|
|
5001
|
+
return response;
|
|
5002
|
+
}
|
|
5003
|
+
function buildRoutingBlock(decision, reroutes) {
|
|
5004
|
+
return {
|
|
5005
|
+
chosen: decision.chosen ? { ...decision.chosen } : null,
|
|
5006
|
+
tier: decision.tier,
|
|
5007
|
+
estCostUsd: decision.estCostUsd,
|
|
5008
|
+
costBasis: decision.costBasis,
|
|
5009
|
+
confidence: decision.confidence,
|
|
5010
|
+
nearTie: decision.nearTie,
|
|
5011
|
+
estInputTokens: decision.estInputTokens,
|
|
5012
|
+
estOutputTokens: decision.estOutputTokens,
|
|
5013
|
+
priceAsOf: decision.priceAsOf,
|
|
5014
|
+
priceSource: decision.priceSource,
|
|
5015
|
+
consideredCount: decision.consideredCount,
|
|
5016
|
+
rejected: decision.rejected.map(r => ({ candidate: { ...r.candidate }, reason: r.reason })),
|
|
5017
|
+
reroutes,
|
|
5018
|
+
error: decision.error,
|
|
5019
|
+
};
|
|
5020
|
+
}
|
|
5021
|
+
export function deriveCostBasis(provider, resolvedModel, usage) {
|
|
5022
|
+
if (usage.costUsd != null) {
|
|
5023
|
+
return { costUsd: usage.costUsd, costBasis: "provider-reported" };
|
|
5024
|
+
}
|
|
5025
|
+
const hasCounts = usage.inputTokens != null || usage.outputTokens != null;
|
|
5026
|
+
if (hasCounts) {
|
|
5027
|
+
const modelCost = getModelCost(provider, resolvedModel);
|
|
5028
|
+
if (modelCost.source !== "unknown") {
|
|
5029
|
+
const counts = {
|
|
5030
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
5031
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
5032
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
5033
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
5034
|
+
};
|
|
5035
|
+
const composed = composeCost(counts, { estInputTokens: 0, estOutputTokens: 0 }, modelCost);
|
|
5036
|
+
return { costUsd: composed.costUsd, costBasis: composed.cost_basis };
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
return {};
|
|
5040
|
+
}
|
|
5041
|
+
function recordRoutingDecision(runtime, correlationId, decision, reroutes) {
|
|
5042
|
+
const reason = decision.chosen
|
|
5043
|
+
? `cheapest${decision.nearTie ? ":near-tie" : ""}`
|
|
5044
|
+
: (decision.error ?? "no-eligible");
|
|
5045
|
+
safeRecordRouting(correlationId, {
|
|
5046
|
+
estCostUsd: decision.estCostUsd ?? null,
|
|
5047
|
+
estConfidence: decision.confidence ?? null,
|
|
5048
|
+
reason,
|
|
5049
|
+
considered: decision.consideredCount,
|
|
5050
|
+
reroutes,
|
|
5051
|
+
}, runtime);
|
|
5052
|
+
}
|
|
5053
|
+
function withDispatchRejections(decision, dispatchRejections) {
|
|
5054
|
+
if (dispatchRejections.length === 0)
|
|
5055
|
+
return decision;
|
|
5056
|
+
const seen = new Set(decision.rejected.map(r => routeCandidateKey(r.candidate)));
|
|
5057
|
+
const extra = dispatchRejections.filter(r => !seen.has(routeCandidateKey(r.candidate)));
|
|
5058
|
+
return { ...decision, rejected: [...decision.rejected, ...extra] };
|
|
5059
|
+
}
|
|
5060
|
+
function routeErrorResponse(decision, reroutes, corrId, lastFailure) {
|
|
5061
|
+
const reasons = decision.rejected
|
|
5062
|
+
.map(r => `${routeCandidateKey(r.candidate)} (${r.reason})`)
|
|
5063
|
+
.join(", ");
|
|
5064
|
+
const kind = decision.error ?? "NoEligibleCandidate";
|
|
5065
|
+
const base = kind === "BudgetExceeded"
|
|
5066
|
+
? `route_request: no candidate fits the budget. Rejected: ${reasons || "none eligible"}.`
|
|
5067
|
+
: `route_request: no eligible candidate. Rejected: ${reasons || "empty pool"}.`;
|
|
5068
|
+
const diag = lastFailure?.content?.[0]?.text
|
|
5069
|
+
? `\nLast dispatch failure: ${lastFailure.content[0].text}`
|
|
5070
|
+
: "";
|
|
5071
|
+
const resp = createErrorResponse("route_request", 1, `${base}${diag}`, corrId);
|
|
5072
|
+
return attachRouting(resp, buildRoutingBlock({ ...decision, error: kind }, reroutes));
|
|
5073
|
+
}
|
|
5074
|
+
async function runRouteRequest(runtime, params) {
|
|
5075
|
+
const corrId = params.correlationId ?? randomUUID();
|
|
5076
|
+
const cfg = runtime.leastCost;
|
|
5077
|
+
const { env, routerConfig, req } = buildRouteContext(runtime, params);
|
|
5078
|
+
const excluded = new Set();
|
|
5079
|
+
const dispatchRejections = [];
|
|
5080
|
+
let reroutes = 0;
|
|
5081
|
+
let transientReroutes = 0;
|
|
5082
|
+
let lastFailure = null;
|
|
5083
|
+
let lastDecision = null;
|
|
5084
|
+
const maxIterations = env.providers().length * 8 + cfg.maxReroutes + 2;
|
|
5085
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
5086
|
+
const decision = selectCandidate(req, env, routerConfig, excluded);
|
|
5087
|
+
lastDecision = decision;
|
|
5088
|
+
if (!decision.chosen) {
|
|
5089
|
+
return routeErrorResponse(withDispatchRejections(decision, dispatchRejections), reroutes, corrId, lastFailure);
|
|
5090
|
+
}
|
|
5091
|
+
const chosen = decision.chosen;
|
|
5092
|
+
const attemptCorrId = i === 0 ? corrId : randomUUID();
|
|
5093
|
+
const dispatchParams = {
|
|
5094
|
+
prompt: params.prompt,
|
|
5095
|
+
promptParts: params.promptParts,
|
|
5096
|
+
model: chosen.model,
|
|
5097
|
+
correlationId: attemptCorrId,
|
|
5098
|
+
optimizePrompt: params.optimizePrompt ?? false,
|
|
5099
|
+
optimizeResponse: params.optimizeResponse ?? false,
|
|
5100
|
+
compressResponse: params.compressResponse,
|
|
5101
|
+
forceRefresh: params.forceRefresh,
|
|
5102
|
+
};
|
|
5103
|
+
const result = await dispatchRoutedRequest(runtime, chosen, dispatchParams);
|
|
5104
|
+
if (!result.isError) {
|
|
5105
|
+
recordRoutingDecision(runtime, attemptCorrId, decision, reroutes);
|
|
5106
|
+
return attachRouting(result, buildRoutingBlock(decision, reroutes));
|
|
5107
|
+
}
|
|
5108
|
+
lastFailure = result;
|
|
5109
|
+
const transient = isTransientRouteFailure(chosen.provider, result);
|
|
5110
|
+
excluded.add(routeCandidateKey(chosen));
|
|
5111
|
+
dispatchRejections.push({
|
|
5112
|
+
candidate: chosen,
|
|
5113
|
+
reason: transient ? "dispatch-failed:transient" : "dispatch-failed:non-transient",
|
|
5114
|
+
});
|
|
5115
|
+
reroutes++;
|
|
5116
|
+
if (transient) {
|
|
5117
|
+
transientReroutes++;
|
|
5118
|
+
if (transientReroutes > cfg.maxReroutes) {
|
|
5119
|
+
return attachRouting(result, buildRoutingBlock(decision, reroutes));
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5123
|
+
if (lastDecision) {
|
|
5124
|
+
return routeErrorResponse(withDispatchRejections(lastDecision, dispatchRejections), reroutes, corrId, lastFailure);
|
|
5125
|
+
}
|
|
5126
|
+
return createErrorResponse("route_request", 1, "route_request: routing did not converge", corrId);
|
|
5127
|
+
}
|
|
5128
|
+
function buildRouteContext(runtime, params) {
|
|
5129
|
+
return {
|
|
5130
|
+
env: buildRouterEnv({
|
|
5131
|
+
performanceMetrics: runtime.performanceMetrics,
|
|
5132
|
+
limiterSnapshot: runtime.asyncJobManager.getLimiterSnapshot(),
|
|
5133
|
+
apiProviders: enabledApiProviders(runtime.providers),
|
|
5134
|
+
preferCatalogPrice: runtime.leastCost.preferCatalogPrice,
|
|
5135
|
+
flightRecorder: runtime.flightRecorder,
|
|
5136
|
+
priorsScope: runtime.leastCost.priorsScope,
|
|
5137
|
+
ownerPrincipal: resolveOwnerPrincipal(getRequestContext()),
|
|
5138
|
+
}),
|
|
5139
|
+
routerConfig: toRouterConfig(runtime.leastCost),
|
|
5140
|
+
req: {
|
|
5141
|
+
prompt: params.prompt ?? "",
|
|
5142
|
+
candidates: params.candidates,
|
|
5143
|
+
minTier: params.minTier,
|
|
5144
|
+
maxCostUsd: params.maxCostUsd,
|
|
5145
|
+
expectedOutputTokens: params.expectedOutputTokens,
|
|
5146
|
+
maxOutputTokens: params.maxOutputTokens,
|
|
5147
|
+
requiredCapabilities: params.requiredCapabilities,
|
|
5148
|
+
allowUnpriced: params.allowUnpriced,
|
|
5149
|
+
budgetWaiver: params.budgetWaiver,
|
|
5150
|
+
fallback: params.fallback,
|
|
5151
|
+
},
|
|
5152
|
+
};
|
|
5153
|
+
}
|
|
5154
|
+
async function dispatchRoutedCliAsync(runtime, cli, params) {
|
|
5155
|
+
const outputFormat = routedOutputFormat(cli);
|
|
5156
|
+
const prep = prepareRoutedCli(runtime, cli, params, outputFormat);
|
|
5157
|
+
if (!("args" in prep))
|
|
5158
|
+
return prep;
|
|
5159
|
+
const { corrId, args } = prep;
|
|
5160
|
+
const prepCleanup = "cleanup" in prep && typeof prep.cleanup === "function"
|
|
5161
|
+
? prep.cleanup
|
|
5162
|
+
: undefined;
|
|
5163
|
+
try {
|
|
5164
|
+
const effectiveCompress = resolveEffectiveCompression(runtime.compression, {
|
|
5165
|
+
compressResponse: params.compressResponse,
|
|
5166
|
+
outputFormat,
|
|
5167
|
+
outputSchemaDeclared: false,
|
|
5168
|
+
});
|
|
5169
|
+
const frHandoff = buildAsyncFlightRecorderHandoff(cli, prep, undefined, outputFormat, params.optimizePrompt);
|
|
5170
|
+
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);
|
|
5171
|
+
return {
|
|
5172
|
+
content: [
|
|
5173
|
+
{
|
|
5174
|
+
type: "text",
|
|
5175
|
+
text: JSON.stringify({ success: true, job, sessionId: null }, null, 2),
|
|
5176
|
+
},
|
|
5177
|
+
],
|
|
5178
|
+
};
|
|
5179
|
+
}
|
|
5180
|
+
catch (error) {
|
|
5181
|
+
return createErrorResponse("route_request_async", 1, "", corrId, error);
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
async function runRouteRequestAsync(runtime, params) {
|
|
5185
|
+
const corrId = params.correlationId ?? randomUUID();
|
|
5186
|
+
const { env, routerConfig, req } = buildRouteContext(runtime, params);
|
|
5187
|
+
const decision = selectCandidate(req, env, routerConfig);
|
|
5188
|
+
if (!decision.chosen) {
|
|
5189
|
+
return routeErrorResponse(decision, 0, corrId);
|
|
5190
|
+
}
|
|
5191
|
+
const chosen = decision.chosen;
|
|
5192
|
+
const dispatchParams = {
|
|
5193
|
+
prompt: params.prompt,
|
|
5194
|
+
promptParts: params.promptParts,
|
|
5195
|
+
model: chosen.model,
|
|
5196
|
+
correlationId: corrId,
|
|
5197
|
+
optimizePrompt: params.optimizePrompt ?? false,
|
|
5198
|
+
optimizeResponse: params.optimizeResponse ?? false,
|
|
5199
|
+
compressResponse: params.compressResponse,
|
|
5200
|
+
forceRefresh: params.forceRefresh,
|
|
5201
|
+
};
|
|
5202
|
+
let result;
|
|
5203
|
+
if (isCliTypeValue(chosen.provider)) {
|
|
5204
|
+
result = await dispatchRoutedCliAsync(runtime, chosen.provider, dispatchParams);
|
|
5205
|
+
}
|
|
5206
|
+
else {
|
|
5207
|
+
const api = enabledApiProviders(runtime.providers).find(p => p.name === chosen.provider);
|
|
5208
|
+
if (!api) {
|
|
5209
|
+
return createErrorResponse("route_request_async", 1, `Routed provider '${chosen.provider}' is not an enabled API provider`, corrId);
|
|
5210
|
+
}
|
|
5211
|
+
result = await handleApiProviderRequestAsync(runtime, api, {
|
|
5212
|
+
prompt: params.prompt,
|
|
5213
|
+
promptParts: params.promptParts,
|
|
5214
|
+
model: chosen.model,
|
|
5215
|
+
correlationId: corrId,
|
|
5216
|
+
optimizePrompt: params.optimizePrompt ?? false,
|
|
5217
|
+
optimizeResponse: params.optimizeResponse ?? false,
|
|
5218
|
+
});
|
|
5219
|
+
}
|
|
5220
|
+
recordRoutingDecision(runtime, corrId, decision, 0);
|
|
5221
|
+
return attachRouting(result, buildRoutingBlock(decision, 0));
|
|
5222
|
+
}
|
|
4769
5223
|
export function createGatewayServer(deps = {}) {
|
|
4770
5224
|
const runtime = resolveGatewayServerRuntime(deps, { isolateState: true });
|
|
4771
5225
|
const { sessionManager, asyncJobManager, approvalManager, performanceMetrics, logger, persistence, flightRecorder, cacheAwareness, providers, } = runtime;
|
|
@@ -4779,6 +5233,9 @@ export function createGatewayServer(deps = {}) {
|
|
|
4779
5233
|
asyncJobManager,
|
|
4780
5234
|
apiProviders: enabledApiProviders(providers),
|
|
4781
5235
|
validationRunStore: asyncJobManager.getValidationRunStore(),
|
|
5236
|
+
leastCost: runtime.leastCost,
|
|
5237
|
+
performanceMetrics: runtime.performanceMetrics,
|
|
5238
|
+
flightRecorder: runtime.flightRecorder,
|
|
4782
5239
|
});
|
|
4783
5240
|
registerWorkspaceTools(server, runtime);
|
|
4784
5241
|
registerProviderAdminTools(server, {
|
|
@@ -4790,6 +5247,93 @@ export function createGatewayServer(deps = {}) {
|
|
|
4790
5247
|
if (apiProviderTools.length > 0) {
|
|
4791
5248
|
runtime.logger.info(`Registered API provider tools: ${apiProviderTools.join(", ")}`);
|
|
4792
5249
|
}
|
|
5250
|
+
if (runtime.leastCost.enabled) {
|
|
5251
|
+
const RouteCandidateShape = z.object({
|
|
5252
|
+
provider: z.string().min(1).describe("A CLI_TYPES member or an enabled API-provider name."),
|
|
5253
|
+
model: z.string().min(1).describe("A concrete model id/alias that provider can serve."),
|
|
5254
|
+
});
|
|
5255
|
+
const RouteRequiredCapabilitiesShape = z
|
|
5256
|
+
.object({
|
|
5257
|
+
images: z.boolean().optional(),
|
|
5258
|
+
attachments: z.boolean().optional(),
|
|
5259
|
+
toolCalling: z.boolean().optional(),
|
|
5260
|
+
jsonSchema: z.boolean().optional(),
|
|
5261
|
+
outputFormat: z.string().optional(),
|
|
5262
|
+
effort: z.string().optional(),
|
|
5263
|
+
})
|
|
5264
|
+
.describe("Hard capability constraints; a candidate missing any required capability is excluded.");
|
|
5265
|
+
const routeInputShape = {
|
|
5266
|
+
prompt: z
|
|
5267
|
+
.string()
|
|
5268
|
+
.min(1)
|
|
5269
|
+
.max(100000)
|
|
5270
|
+
.optional()
|
|
5271
|
+
.describe("Prompt text to route (mutually exclusive with promptParts)."),
|
|
5272
|
+
promptParts: PromptPartsSchema.optional().describe("Cache-aware structured prompt (mutually exclusive with prompt)."),
|
|
5273
|
+
candidates: z
|
|
5274
|
+
.array(RouteCandidateShape)
|
|
5275
|
+
.optional()
|
|
5276
|
+
.describe("Explicit (provider, model) pool to restrict routing to; also whitelists untiered / maintain-only candidates. Empty/omitted = all eligible."),
|
|
5277
|
+
minTier: z
|
|
5278
|
+
.enum(["economy", "standard", "frontier"])
|
|
5279
|
+
.optional()
|
|
5280
|
+
.describe("Minimum quality tier floor (default from config, usually 'standard'). No silent downgrade below it."),
|
|
5281
|
+
maxCostUsd: z
|
|
5282
|
+
.number()
|
|
5283
|
+
.positive()
|
|
5284
|
+
.optional()
|
|
5285
|
+
.describe("Per-request budget cap (USD). Over-budget fails closed unless waived via an explicit fallback + budgetWaiver."),
|
|
5286
|
+
maxOutputTokens: z
|
|
5287
|
+
.number()
|
|
5288
|
+
.int()
|
|
5289
|
+
.positive()
|
|
5290
|
+
.optional()
|
|
5291
|
+
.describe("Caller output cap; bounds the conservative budget estimate."),
|
|
5292
|
+
expectedOutputTokens: z
|
|
5293
|
+
.number()
|
|
5294
|
+
.int()
|
|
5295
|
+
.positive()
|
|
5296
|
+
.optional()
|
|
5297
|
+
.describe("Expected output tokens for the ranking estimate (default from config)."),
|
|
5298
|
+
requiredCapabilities: RouteRequiredCapabilitiesShape.optional(),
|
|
5299
|
+
allowUnpriced: z
|
|
5300
|
+
.boolean()
|
|
5301
|
+
.optional()
|
|
5302
|
+
.describe("Admit unpriced (source 'unknown') candidates. They still rank strictly last and cannot pass the budget gate without budgetWaiver."),
|
|
5303
|
+
budgetWaiver: z
|
|
5304
|
+
.boolean()
|
|
5305
|
+
.optional()
|
|
5306
|
+
.describe("Explicit acknowledgment to admit an unpriced or over-budget fallback candidate (with allowUnpriced / fallback)."),
|
|
5307
|
+
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)."),
|
|
5308
|
+
optimizePrompt: z
|
|
5309
|
+
.boolean()
|
|
5310
|
+
.optional()
|
|
5311
|
+
.describe("Apply prompt token optimization before dispatch."),
|
|
5312
|
+
optimizeResponse: z.boolean().optional().describe("Apply response token optimization."),
|
|
5313
|
+
compressResponse: z
|
|
5314
|
+
.boolean()
|
|
5315
|
+
.optional()
|
|
5316
|
+
.describe("Apply the native display-text compressor to the response."),
|
|
5317
|
+
correlationId: z.string().optional().describe("Caller-supplied correlation id for tracing."),
|
|
5318
|
+
forceRefresh: z
|
|
5319
|
+
.boolean()
|
|
5320
|
+
.optional()
|
|
5321
|
+
.describe("Bypass the async dedup window for this request."),
|
|
5322
|
+
};
|
|
5323
|
+
const routeAnnotations = {
|
|
5324
|
+
readOnlyHint: false,
|
|
5325
|
+
destructiveHint: true,
|
|
5326
|
+
idempotentHint: false,
|
|
5327
|
+
openWorldHint: true,
|
|
5328
|
+
};
|
|
5329
|
+
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));
|
|
5330
|
+
let registeredRouteTools = "route_request";
|
|
5331
|
+
if (asyncJobsEnabled) {
|
|
5332
|
+
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));
|
|
5333
|
+
registeredRouteTools += ", route_request_async";
|
|
5334
|
+
}
|
|
5335
|
+
runtime.logger.info(`Registered least-cost routing tools: ${registeredRouteTools}`);
|
|
5336
|
+
}
|
|
4793
5337
|
if (grokApiToolsEnabled) {
|
|
4794
5338
|
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
5339
|
prompt: z
|
|
@@ -5181,6 +5725,13 @@ export function createGatewayServer(deps = {}) {
|
|
|
5181
5725
|
if (parsed.costUsd !== null) {
|
|
5182
5726
|
logger.debug(`[${corrId}] stream-json cost=$${parsed.costUsd}, model=${parsed.model}, turns=${parsed.numTurns}`);
|
|
5183
5727
|
}
|
|
5728
|
+
const { costUsd: claudeCostUsd, costBasis: claudeCostBasis } = deriveCostBasis("claude", prep.resolvedModel || "default", {
|
|
5729
|
+
inputTokens: parsed.usage?.inputTokens,
|
|
5730
|
+
outputTokens: parsed.usage?.outputTokens,
|
|
5731
|
+
cacheReadTokens: parsed.usage?.cacheReadInputTokens || undefined,
|
|
5732
|
+
cacheCreationTokens: parsed.usage?.cacheCreationInputTokens || undefined,
|
|
5733
|
+
costUsd: parsed.costUsd ?? undefined,
|
|
5734
|
+
});
|
|
5184
5735
|
safeFlightComplete(corrId, {
|
|
5185
5736
|
response: parsed.text,
|
|
5186
5737
|
inputTokens: parsed.usage?.inputTokens,
|
|
@@ -5190,7 +5741,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
5190
5741
|
durationMs,
|
|
5191
5742
|
retryCount: 0,
|
|
5192
5743
|
circuitBreakerState: "closed",
|
|
5193
|
-
costUsd:
|
|
5744
|
+
costUsd: claudeCostUsd,
|
|
5745
|
+
costBasis: claudeCostBasis,
|
|
5194
5746
|
optimizationApplied: optimizePrompt || optimizeResponse,
|
|
5195
5747
|
exitCode: 0,
|
|
5196
5748
|
status: "completed",
|
|
@@ -5432,7 +5984,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
5432
5984
|
}, runtime);
|
|
5433
5985
|
logger.info(`[${corrId}] codex_request invoked with model=${prep.resolvedModel || "default"}, fullAuto=${fullAuto}, prompt length=${prep.effectivePrompt.length}`);
|
|
5434
5986
|
const prepCleanup = "cleanup" in prep && typeof prep.cleanup === "function" ? prep.cleanup : undefined;
|
|
5435
|
-
let worktreeResolution
|
|
5987
|
+
let worktreeResolution;
|
|
5436
5988
|
try {
|
|
5437
5989
|
worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
|
|
5438
5990
|
provider: "codex",
|
|
@@ -5505,6 +6057,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
5505
6057
|
}
|
|
5506
6058
|
logger.info(`[${corrId}] codex_request completed successfully in ${durationMs}ms`);
|
|
5507
6059
|
const codexUsage = extractUsageAndCost("codex", stdout, outputFormat);
|
|
6060
|
+
const { costUsd: codexCostUsd, costBasis: codexCostBasis } = deriveCostBasis("codex", prep.resolvedModel || "default", codexUsage);
|
|
5508
6061
|
const codexMeta = extractProviderOutputMetadata("codex", stdout, outputFormat);
|
|
5509
6062
|
safeFlightComplete(corrId, {
|
|
5510
6063
|
response: codexFrResponse(outputFormat, stdout),
|
|
@@ -5518,7 +6071,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
5518
6071
|
outputTokens: codexUsage.outputTokens,
|
|
5519
6072
|
cacheReadTokens: codexUsage.cacheReadTokens,
|
|
5520
6073
|
cacheCreationTokens: codexUsage.cacheCreationTokens,
|
|
5521
|
-
costUsd:
|
|
6074
|
+
costUsd: codexCostUsd,
|
|
6075
|
+
costBasis: codexCostBasis,
|
|
5522
6076
|
providerSessionId: codexMeta.sessionId,
|
|
5523
6077
|
stopReason: codexMeta.stopReason,
|
|
5524
6078
|
}, runtime);
|
|
@@ -7683,21 +8237,28 @@ export function createGatewayServer(deps = {}) {
|
|
|
7683
8237
|
}, async () => {
|
|
7684
8238
|
const health = asyncJobManager.getJobHealth();
|
|
7685
8239
|
const storeAttached = asyncJobManager.hasStore();
|
|
7686
|
-
const
|
|
8240
|
+
const durableAdmission = asyncJobManager.getDurableAdmissionHealth();
|
|
8241
|
+
const asyncJobsConfigured = persistence.backend !== "none" && persistence.asyncJobsEnabled;
|
|
8242
|
+
const asyncJobsEffective = asyncJobsConfigured && storeAttached && durableAdmission.admitting;
|
|
7687
8243
|
const persistenceBlock = {
|
|
7688
8244
|
backend: persistence.backend,
|
|
7689
8245
|
dbPath: persistence.path,
|
|
7690
8246
|
dsn: persistence.dsn ? "[redacted]" : null,
|
|
7691
8247
|
retentionDays: persistence.retentionDays,
|
|
7692
8248
|
dedupWindowMs: persistence.dedupWindowMs,
|
|
8249
|
+
asyncJobsConfigured,
|
|
8250
|
+
storeAttached,
|
|
7693
8251
|
asyncJobsEnabled: asyncJobsEffective,
|
|
8252
|
+
durableAdmission,
|
|
7694
8253
|
acknowledgeEphemeral: persistence.acknowledgeEphemeral,
|
|
7695
8254
|
sources: persistence.sources,
|
|
7696
8255
|
warning: asyncJobsEffective
|
|
7697
8256
|
? null
|
|
7698
8257
|
: persistence.backend === "none"
|
|
7699
8258
|
? "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
|
-
:
|
|
8259
|
+
: !storeAttached
|
|
8260
|
+
? `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.`
|
|
8261
|
+
: "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
8262
|
};
|
|
7702
8263
|
const outboundProviders = {
|
|
7703
8264
|
xai: providers.xai
|
|
@@ -7763,6 +8324,46 @@ export function createGatewayServer(deps = {}) {
|
|
|
7763
8324
|
external: mem.external,
|
|
7764
8325
|
},
|
|
7765
8326
|
};
|
|
8327
|
+
const leastCostBlock = (() => {
|
|
8328
|
+
const lc = runtime.leastCost;
|
|
8329
|
+
if (!lc.enabled)
|
|
8330
|
+
return { enabled: false };
|
|
8331
|
+
const env = buildRouterEnv({
|
|
8332
|
+
performanceMetrics: runtime.performanceMetrics,
|
|
8333
|
+
limiterSnapshot: runtime.asyncJobManager.getLimiterSnapshot(),
|
|
8334
|
+
apiProviders: enabledApiProviders(runtime.providers),
|
|
8335
|
+
preferCatalogPrice: lc.preferCatalogPrice,
|
|
8336
|
+
});
|
|
8337
|
+
const providerHealth = env.providers().map(provider => ({
|
|
8338
|
+
provider,
|
|
8339
|
+
telemetryTier: telemetryTierFor(provider),
|
|
8340
|
+
authed: env.isAuthed(provider),
|
|
8341
|
+
breakerState: env.breakerState(provider),
|
|
8342
|
+
atCapacity: env.atCapacity(provider),
|
|
8343
|
+
candidates: env.models(provider).map(model => {
|
|
8344
|
+
const mc = env.modelCost(provider, model);
|
|
8345
|
+
const family = modelIdToFamily(model);
|
|
8346
|
+
const tier = lc.tiers[`${provider}:${family}`];
|
|
8347
|
+
return {
|
|
8348
|
+
model,
|
|
8349
|
+
family: mc.family,
|
|
8350
|
+
priced: mc.source !== "unknown",
|
|
8351
|
+
priceSource: mc.source,
|
|
8352
|
+
tier: tier ?? null,
|
|
8353
|
+
};
|
|
8354
|
+
}),
|
|
8355
|
+
}));
|
|
8356
|
+
return {
|
|
8357
|
+
enabled: true,
|
|
8358
|
+
minTier: lc.minTier,
|
|
8359
|
+
maxCostUsd: lc.maxCostUsd,
|
|
8360
|
+
priorsScope: lc.priorsScope,
|
|
8361
|
+
preferCatalogPrice: lc.preferCatalogPrice,
|
|
8362
|
+
allowUnpriced: lc.allowUnpriced,
|
|
8363
|
+
maxReroutes: lc.maxReroutes,
|
|
8364
|
+
providers: providerHealth,
|
|
8365
|
+
};
|
|
8366
|
+
})();
|
|
7766
8367
|
return {
|
|
7767
8368
|
content: [
|
|
7768
8369
|
{
|
|
@@ -7773,6 +8374,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
7773
8374
|
backpressure,
|
|
7774
8375
|
persistence: persistenceBlock,
|
|
7775
8376
|
outboundProviders,
|
|
8377
|
+
leastCost: leastCostBlock,
|
|
7776
8378
|
}, null, 2),
|
|
7777
8379
|
},
|
|
7778
8380
|
],
|
|
@@ -8441,7 +9043,7 @@ async function initializeSessionManager() {
|
|
|
8441
9043
|
});
|
|
8442
9044
|
logger.info("File-based session manager initialized");
|
|
8443
9045
|
}
|
|
8444
|
-
resourceProvider = new ResourceProvider(sessionManager, performanceMetrics, getFlightRecorder(logger), getCacheAwarenessConfig(logger), getProvidersConfig(logger), undefined, getAcpConfig(logger));
|
|
9046
|
+
resourceProvider = new ResourceProvider(sessionManager, performanceMetrics, getFlightRecorder(logger), getCacheAwarenessConfig(logger), getProvidersConfig(logger), undefined, getAcpConfig(logger), getLeastCostConfig(logger));
|
|
8445
9047
|
}
|
|
8446
9048
|
function registerHealthResource(server) {
|
|
8447
9049
|
if (db) {
|
|
@@ -8468,20 +9070,26 @@ function registerHealthResource(server) {
|
|
|
8468
9070
|
description: "Async job health (CPU, memory, zombie detection)",
|
|
8469
9071
|
mimeType: "application/json",
|
|
8470
9072
|
}, async (uri) => {
|
|
8471
|
-
const
|
|
9073
|
+
const manager = getAsyncJobManager();
|
|
9074
|
+
const health = manager.getJobHealth();
|
|
8472
9075
|
return {
|
|
8473
9076
|
contents: [
|
|
8474
9077
|
{
|
|
8475
9078
|
uri: uri.href,
|
|
8476
9079
|
mimeType: "application/json",
|
|
8477
|
-
text: JSON.stringify(health, null, 2),
|
|
9080
|
+
text: JSON.stringify({ ...health, durableAdmission: manager.getDurableAdmissionHealth() }, null, 2),
|
|
8478
9081
|
},
|
|
8479
9082
|
],
|
|
8480
9083
|
};
|
|
8481
9084
|
});
|
|
8482
9085
|
logger.info("Process health resource registered");
|
|
8483
9086
|
}
|
|
8484
|
-
|
|
9087
|
+
let shutdownPromise = null;
|
|
9088
|
+
function shutdown(signal, exitCode = 0) {
|
|
9089
|
+
shutdownPromise ??= performShutdown(signal, exitCode);
|
|
9090
|
+
return shutdownPromise;
|
|
9091
|
+
}
|
|
9092
|
+
async function performShutdown(signal, exitCode) {
|
|
8485
9093
|
logger.info(`Received ${signal}, shutting down gracefully...`);
|
|
8486
9094
|
try {
|
|
8487
9095
|
if (asyncJobManager) {
|
|
@@ -8505,6 +9113,12 @@ async function shutdown(signal) {
|
|
|
8505
9113
|
logger.info("MCP server closed");
|
|
8506
9114
|
activeServer = null;
|
|
8507
9115
|
}
|
|
9116
|
+
if (jobStore) {
|
|
9117
|
+
jobStore.close();
|
|
9118
|
+
logger.info("Durable job store closed");
|
|
9119
|
+
jobStore = null;
|
|
9120
|
+
jobStoreInitialized = false;
|
|
9121
|
+
}
|
|
8508
9122
|
if (db) {
|
|
8509
9123
|
await db.disconnect();
|
|
8510
9124
|
logger.info("Database connections closed");
|
|
@@ -8513,7 +9127,7 @@ async function shutdown(signal) {
|
|
|
8513
9127
|
flightRecorder.close();
|
|
8514
9128
|
logger.info("Flight recorder closed");
|
|
8515
9129
|
}
|
|
8516
|
-
process.exit(
|
|
9130
|
+
process.exit(exitCode);
|
|
8517
9131
|
}
|
|
8518
9132
|
catch (error) {
|
|
8519
9133
|
logger.error("Error during shutdown:", error);
|
|
@@ -8907,15 +9521,23 @@ async function main() {
|
|
|
8907
9521
|
logger.info(`Starting llm-cli-gateway MCP server with ${transportMode} transport`);
|
|
8908
9522
|
await initializeSessionManager();
|
|
8909
9523
|
void warmProviderCapabilities({ logger }).catch(() => undefined);
|
|
9524
|
+
const persistence = getPersistenceConfig(logger);
|
|
9525
|
+
const runtimeAsyncJobManager = getAsyncJobManager(logger);
|
|
9526
|
+
if (persistence.backend !== "none" &&
|
|
9527
|
+
persistence.asyncJobsEnabled &&
|
|
9528
|
+
!runtimeAsyncJobManager.hasStore()) {
|
|
9529
|
+
throw new Error(`Configured ${persistence.backend} job store could not be opened; refusing to start without durable async persistence`);
|
|
9530
|
+
}
|
|
8910
9531
|
const serverDeps = {
|
|
8911
9532
|
sessionManager,
|
|
8912
9533
|
resourceProvider,
|
|
8913
9534
|
db,
|
|
8914
9535
|
performanceMetrics,
|
|
8915
|
-
asyncJobManager:
|
|
9536
|
+
asyncJobManager: runtimeAsyncJobManager,
|
|
8916
9537
|
approvalManager: getApprovalManager(logger),
|
|
8917
9538
|
flightRecorder: getFlightRecorder(logger),
|
|
8918
9539
|
logger,
|
|
9540
|
+
persistence,
|
|
8919
9541
|
};
|
|
8920
9542
|
if (transportMode === "http") {
|
|
8921
9543
|
activeHttpGateway = await startHttpGateway({
|
|
@@ -8932,6 +9554,12 @@ async function main() {
|
|
|
8932
9554
|
activeServer = createGatewayServer({
|
|
8933
9555
|
...serverDeps,
|
|
8934
9556
|
});
|
|
9557
|
+
activeServer.server.onclose = () => {
|
|
9558
|
+
void shutdown("stdio MCP connection closed");
|
|
9559
|
+
};
|
|
9560
|
+
process.stdin.once("end", () => {
|
|
9561
|
+
void shutdown("stdio stdin EOF");
|
|
9562
|
+
});
|
|
8935
9563
|
registerHealthResource(activeServer);
|
|
8936
9564
|
const transport = new StdioServerTransport();
|
|
8937
9565
|
await activeServer.connect(transport);
|
|
@@ -8941,6 +9569,6 @@ const __entryUrl = entrypointFileURL(process.argv[1]);
|
|
|
8941
9569
|
if (__entryUrl === import.meta.url) {
|
|
8942
9570
|
main().catch(error => {
|
|
8943
9571
|
logger.error("Fatal server error:", error);
|
|
8944
|
-
|
|
9572
|
+
void shutdown("fatal startup error", 1);
|
|
8945
9573
|
});
|
|
8946
9574
|
}
|