llm-cli-gateway 2.16.0 → 2.17.1

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