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.
Files changed (61) hide show
  1. package/.agents/skills/least-cost-routing/SKILL.md +123 -0
  2. package/CHANGELOG.md +28 -0
  3. package/dist/acp/client.js +5 -0
  4. package/dist/acp/flight-redaction.d.ts +2 -0
  5. package/dist/acp/flight-redaction.js +2 -0
  6. package/dist/acp/runtime.d.ts +1 -0
  7. package/dist/acp/runtime.js +20 -0
  8. package/dist/acp/types.d.ts +52 -0
  9. package/dist/acp/types.js +8 -0
  10. package/dist/api-provider.d.ts +1 -0
  11. package/dist/api-provider.js +1 -0
  12. package/dist/async-job-manager.d.ts +16 -0
  13. package/dist/async-job-manager.js +70 -22
  14. package/dist/claude-mcp-config.js +1 -1
  15. package/dist/compressor/transforms/ansi.js +12 -2
  16. package/dist/config.d.ts +31 -0
  17. package/dist/config.js +142 -7
  18. package/dist/db.js +4 -4
  19. package/dist/doctor.d.ts +34 -1
  20. package/dist/doctor.js +85 -3
  21. package/dist/executor.d.ts +5 -0
  22. package/dist/executor.js +11 -1
  23. package/dist/flight-recorder.d.ts +10 -0
  24. package/dist/flight-recorder.js +68 -2
  25. package/dist/http-transport.js +19 -21
  26. package/dist/index.d.ts +41 -1
  27. package/dist/index.js +658 -30
  28. package/dist/job-store.d.ts +10 -2
  29. package/dist/job-store.js +154 -43
  30. package/dist/lcr-priors.d.ts +60 -0
  31. package/dist/lcr-priors.js +190 -0
  32. package/dist/lcr-router-env.d.ts +20 -0
  33. package/dist/lcr-router-env.js +133 -0
  34. package/dist/lcr-telemetry.d.ts +2 -0
  35. package/dist/lcr-telemetry.js +17 -0
  36. package/dist/least-cost-router.d.ts +86 -0
  37. package/dist/least-cost-router.js +296 -0
  38. package/dist/least-cost-types.d.ts +34 -0
  39. package/dist/least-cost-types.js +1 -0
  40. package/dist/migrate-sessions.js +1 -1
  41. package/dist/migrate.js +1 -1
  42. package/dist/model-registry.js +1 -1
  43. package/dist/postgres-job-store-worker.js +56 -13
  44. package/dist/pricing.d.ts +17 -0
  45. package/dist/pricing.js +167 -0
  46. package/dist/provider-definitions.d.ts +4 -0
  47. package/dist/provider-definitions.js +28 -0
  48. package/dist/request-helpers.d.ts +2 -2
  49. package/dist/resources.d.ts +37 -2
  50. package/dist/resources.js +96 -1
  51. package/dist/retry.d.ts +1 -0
  52. package/dist/retry.js +1 -1
  53. package/dist/token-estimator.d.ts +6 -0
  54. package/dist/token-estimator.js +59 -0
  55. package/dist/upstream-contracts.js +1 -1
  56. package/dist/validation-receipt.js +1 -1
  57. package/dist/validation-tools.d.ts +6 -0
  58. package/dist/validation-tools.js +174 -54
  59. package/npm-shrinkwrap.json +2 -2
  60. package/package.json +9 -4
  61. 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; async tools will be unavailable", err);
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: prep.resolvedModel || "default",
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) => extractUsageAndCost(cli, stdout, fmt, { sessionId: sid, home }),
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 (Boolean(params.alwaysApprove)) {
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
- let effectiveSessionId = effectiveSessionIdHint;
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: geminiUsage.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
- let effectiveSessionId = sessionPlan.resumed ? params.sessionId : undefined;
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
- let job;
4734
- try {
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: parsed.costUsd ?? undefined,
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: codexUsage.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 asyncJobsEffective = persistence.backend !== "none" && persistence.asyncJobsEnabled && storeAttached;
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
- : `Async job persistence is configured (backend = '${persistence.backend}') but the durable job store failed to open, so *_request_async tools are NOT registered on this gateway. Check gateway startup logs for the store-open error.`,
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 health = getAsyncJobManager().getJobHealth();
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
- async function shutdown(signal) {
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(0);
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: getAsyncJobManager(logger),
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
- process.exit(1);
9572
+ void shutdown("fatal startup error", 1);
8945
9573
  });
8946
9574
  }