llm-cli-gateway 2.12.0 → 2.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import { createWorktree, createWorktreeSessionCleanupHook, } from "./worktree-ma
18
18
  import { ResourceProvider } from "./resources.js";
19
19
  import { PerformanceMetrics } from "./metrics.js";
20
20
  import { estimateTokens, optimizePrompt as optimizePromptText, optimizeResponse as optimizeResponseText, } from "./optimizer.js";
21
- import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, loadProvidersConfig, loadAcpConfig, defaultGatewayConfigPath, isXaiProviderEnabled, enabledApiProviders, minStableTokensForModel, } from "./config.js";
21
+ import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, loadProvidersConfig, loadAcpConfig, loadLimitsConfig, defaultGatewayConfigPath, isXaiProviderEnabled, enabledApiProviders, minStableTokensForModel, } from "./config.js";
22
22
  import { runAcpRequest } from "./acp/runtime.js";
23
23
  import { isAcpError } from "./acp/errors.js";
24
24
  import { createApiProvider, runApiRequest, apiProviderBreakerState, } from "./api-provider.js";
@@ -26,7 +26,7 @@ import { prepareApiRequest, apiProviderCatalogEntry, ApiModelNotAllowedError, }
26
26
  import { checkHealth } from "./health.js";
27
27
  import { clearModelRegistryCache, getAvailableCliInfo, getCliInfo, resolveModelAlias, } from "./model-registry.js";
28
28
  import { getProviderToolCapabilities } from "./provider-tool-capabilities.js";
29
- import { AsyncJobManager, extractApiHttpStatus, extractApiErrorBody, } from "./async-job-manager.js";
29
+ import { AsyncJobManager, JobSaturationError, isAsyncJobInProgress, extractApiHttpStatus, extractApiErrorBody, } from "./async-job-manager.js";
30
30
  import { createJobStore } from "./job-store.js";
31
31
  import { ApprovalManager, bypassAllowedByOperator, } from "./approval-manager.js";
32
32
  import { checkReviewIntegrity } from "./review-integrity.js";
@@ -39,6 +39,7 @@ import { getCliVersions, runCliUpgrade } from "./cli-updater.js";
39
39
  import { startHttpGateway } from "./http-transport.js";
40
40
  import { getRequestContext, resolveOwnerPrincipal, principalCanAccess } from "./request-context.js";
41
41
  import { printDoctorJson } from "./doctor.js";
42
+ import { redactDiagnosticUrl } from "./endpoint-exposure.js";
42
43
  import { createWorkspace, describeWorkspace, getWorkspace, loadWorkspaceRegistry, registerExistingWorkspace, resolveWorkspaceForProvider, validatePathInsideWorkspace, } from "./workspace-registry.js";
43
44
  import { generateSecret, hashSecret } from "./oauth.js";
44
45
  import { registerValidationTools } from "./validation-tools.js";
@@ -165,12 +166,13 @@ Tools: claude_request, codex_request, gemini_request, grok_request, mistral_requ
165
166
  Validation: validate_with_models, second_opinion, compare_answers, red_team_review, consensus_check, ask_model, synthesize_validation, list_available_models | job_status/job_result (validation jobs)
166
167
  ${jobsLine}Sessions: session_create, session_list, session_set_active, session_get, session_delete, session_clear_all
167
168
  Other: list_models, provider_tool_capabilities, cli_versions, upstream_contracts, provider_subcommands_* (read-only subcommand contract/drift introspection), cli_upgrade, approval_list, llm_process_health, llm_request_result (read back any persisted request, sync or async, by correlationId)
168
- Workspaces: workspace_create, workspace_list, workspace_get, workspace_register_existing_repo
169
+ Workspaces: workspace_create, workspace_list, workspace_get, workspace_register_existing_repo (remote HTTP/OAuth workspace registry only; do not use workspace_* to fix stdio/local provider path access)
169
170
 
170
171
  Key behaviors:
171
172
  ${deferralLine}
172
173
  - Sessions: Claude --continue, Gemini (Antigravity) --conversation <id>/--continue, Grok --resume/--continue, Mistral --resume/--continue (current Vibe defaults session logging on; doctor flags explicit session_logging.enabled=false), Codex \`exec resume <ID>\` / \`exec resume --last\`, Devin --resume <id>/--continue (all real CLI continuity). For Codex, sessionId must be a real Codex UUID (from ~/.codex/sessions/); gateway-generated gw-* IDs are rejected.
173
174
  - Approval gates: opt-in via approvalStrategy:"mcp_managed".
175
+ - Stdio/local provider calls may pass local workingDir/addDir/includeDirs directly. Workspace aliases are for remote HTTP/OAuth calls and must not be used as a fallback after a stdio provider path error.
174
176
  - Upstream drift detection: After upgrading any provider CLI (especially grok), use upstream_contracts with probeInstalled:true and provider_subcommand_drift for declared subcommand help surfaces. Probes are safe, read-only --help checks.
175
177
  - Idle timeout kills stuck processes (default 10min, configurable via idleTimeoutMs).
176
178
 
@@ -189,6 +191,7 @@ let persistenceConfig = null;
189
191
  let cacheAwarenessConfig = null;
190
192
  let providersConfig = null;
191
193
  let acpConfig = null;
194
+ let limitsConfig = null;
192
195
  let jobStore = null;
193
196
  let jobStoreInitialized = false;
194
197
  let asyncJobManager = null;
@@ -201,6 +204,10 @@ function getPersistenceConfig(runtimeLogger = logger) {
201
204
  persistenceConfig ??= loadPersistenceConfig(runtimeLogger);
202
205
  return persistenceConfig;
203
206
  }
207
+ function getLimitsConfig(runtimeLogger = logger) {
208
+ limitsConfig ??= loadLimitsConfig(runtimeLogger);
209
+ return limitsConfig;
210
+ }
204
211
  function getAcpConfig(runtimeLogger = logger) {
205
212
  acpConfig ??= loadAcpConfig(runtimeLogger);
206
213
  return acpConfig;
@@ -229,7 +236,7 @@ function getJobStore(runtimeLogger = logger) {
229
236
  function newAsyncJobManager(metrics, runtimeLogger, store = getJobStore(runtimeLogger), fr = getFlightRecorder(runtimeLogger)) {
230
237
  return new AsyncJobManager(runtimeLogger, (cli, durationMs, success) => {
231
238
  metrics.recordRequest(cli, durationMs, success);
232
- }, store, fr);
239
+ }, store, fr, getLimitsConfig(runtimeLogger).jobs);
233
240
  }
234
241
  function getAsyncJobManager(runtimeLogger = logger) {
235
242
  asyncJobManager ??= newAsyncJobManager(performanceMetrics, runtimeLogger);
@@ -278,7 +285,14 @@ export const WORKSPACE_ALIAS_SCHEMA = z
278
285
  .min(1)
279
286
  .max(64)
280
287
  .regex(/^[A-Za-z][A-Za-z0-9._-]{0,63}$/)
281
- .describe("Registered workspace alias. Remote clients use aliases, not absolute paths.");
288
+ .describe("Registered workspace alias for remote HTTP/OAuth provider calls. Stdio/local callers should not use workspace_* tools to fix provider path access; pass local workingDir/addDir/includeDirs directly.");
289
+ const PROVIDER_WORKSPACE_FIELD_DESCRIPTION = "Registered workspace alias for remote HTTP/OAuth provider calls. Do not use this field, workspace_list, or workspace_register_existing_repo as a fallback for stdio/local provider path access; pass workingDir/addDir/includeDirs directly instead.";
290
+ const LOCAL_WORKING_DIR_FIELD_SUFFIX = " Stdio/local callers may pass local paths directly. Remote HTTP/OAuth callers must use relative paths inside a selected registered workspace. Do not call workspace_* tools to fix stdio/local provider path access.";
291
+ const LOCAL_ADD_DIR_FIELD_SUFFIX = " Stdio/local callers may pass local paths directly. Remote HTTP/OAuth callers must use relative paths inside a selected registered workspace. Do not call workspace_* tools to fix stdio/local provider path access.";
292
+ const LOCAL_INCLUDE_DIRS_FIELD_SUFFIX = " Stdio/local callers may pass local paths directly. Remote HTTP/OAuth callers must use relative paths inside a selected registered workspace. Do not call workspace_* tools to fix stdio/local provider path access.";
293
+ function providerWorkspaceAliasSchema() {
294
+ return WORKSPACE_ALIAS_SCHEMA.describe(PROVIDER_WORKSPACE_FIELD_DESCRIPTION).optional();
295
+ }
282
296
  export const SESSION_PROVIDER_VALUES = PROVIDER_TYPES;
283
297
  export const SESSION_PROVIDER_ENUM = z.enum(SESSION_PROVIDER_VALUES);
284
298
  export function sessionProviderValuesFor(providers) {
@@ -307,7 +321,7 @@ export function resolveGatewayServerRuntime(deps = {}, options = {}) {
307
321
  sessionManager: runtimeSessionManager,
308
322
  resourceProvider: deps.resourceProvider ??
309
323
  (options.isolateState
310
- ? new ResourceProvider(runtimeSessionManager, runtimePerformanceMetrics, runtimeFlightRecorder, deps.cacheAwareness ?? getCacheAwarenessConfig(runtimeLogger))
324
+ ? new ResourceProvider(runtimeSessionManager, runtimePerformanceMetrics, runtimeFlightRecorder, deps.cacheAwareness ?? getCacheAwarenessConfig(runtimeLogger), deps.providers ?? getProvidersConfig(runtimeLogger))
311
325
  : resourceProvider),
312
326
  db: "db" in deps ? (deps.db ?? null) : db,
313
327
  performanceMetrics: runtimePerformanceMetrics,
@@ -402,6 +416,14 @@ async function awaitJobOrDefer(cli, args, corrId, idleTimeoutMs, outputFormat, f
402
416
  runtime.asyncJobManager.hasStore();
403
417
  if (SYNC_DEADLINE_MS === 0 || !deferralAvailable) {
404
418
  const command = providerCommandName(cli);
419
+ let slot;
420
+ try {
421
+ slot = await runtime.asyncJobManager.acquireProcessSlot(cli);
422
+ }
423
+ catch (err) {
424
+ consumeOnComplete();
425
+ throw err;
426
+ }
405
427
  try {
406
428
  return await executeCli(command, args, {
407
429
  idleTimeout: idleTimeoutMs,
@@ -412,6 +434,7 @@ async function awaitJobOrDefer(cli, args, corrId, idleTimeoutMs, outputFormat, f
412
434
  });
413
435
  }
414
436
  finally {
437
+ slot.release();
415
438
  consumeOnComplete();
416
439
  }
417
440
  }
@@ -441,7 +464,7 @@ async function awaitJobOrDefer(cli, args, corrId, idleTimeoutMs, outputFormat, f
441
464
  const deadline = Date.now() + SYNC_DEADLINE_MS;
442
465
  while (Date.now() < deadline) {
443
466
  const snapshot = runtime.asyncJobManager.getJobSnapshot(job.id);
444
- if (snapshot && snapshot.status !== "running") {
467
+ if (snapshot && !isAsyncJobInProgress(snapshot.status)) {
445
468
  const result = runtime.asyncJobManager.getJobResult(job.id);
446
469
  if (!result) {
447
470
  return { stdout: "", stderr: "Job result unavailable", code: 1 };
@@ -481,6 +504,14 @@ async function awaitApiJobOrDefer(provider, apiRequest, corrId, runtime = resolv
481
504
  runtime.persistence.asyncJobsEnabled &&
482
505
  runtime.asyncJobManager.hasStore();
483
506
  if (SYNC_DEADLINE_MS === 0 || !deferralAvailable || forceInline) {
507
+ let slot;
508
+ try {
509
+ slot = await runtime.asyncJobManager.acquireProcessSlot(provider.name);
510
+ }
511
+ catch (err) {
512
+ consumeOnComplete();
513
+ throw err;
514
+ }
484
515
  try {
485
516
  const result = await runApiRequest(provider, apiRequest, runtime.logger);
486
517
  return {
@@ -504,6 +535,7 @@ async function awaitApiJobOrDefer(provider, apiRequest, corrId, runtime = resolv
504
535
  };
505
536
  }
506
537
  finally {
538
+ slot.release();
507
539
  consumeOnComplete();
508
540
  }
509
541
  }
@@ -531,7 +563,7 @@ async function awaitApiJobOrDefer(provider, apiRequest, corrId, runtime = resolv
531
563
  const deadline = Date.now() + SYNC_DEADLINE_MS;
532
564
  while (Date.now() < deadline) {
533
565
  const snapshot = runtime.asyncJobManager.getJobSnapshot(job.id);
534
- if (snapshot && snapshot.status !== "running") {
566
+ if (snapshot && !isAsyncJobInProgress(snapshot.status)) {
535
567
  const result = runtime.asyncJobManager.getJobResult(job.id);
536
568
  if (!result)
537
569
  return { stdout: "", stderr: "Job result unavailable", code: 1 };
@@ -678,38 +710,50 @@ function workspaceAdminEnabled() {
678
710
  const scopes = getRequestContext()?.authScopes ?? [];
679
711
  return process.env.LLM_GATEWAY_WORKSPACE_ADMIN === "1" && scopes.includes("workspace:admin");
680
712
  }
713
+ function assertWorkspaceToolCaller(toolName) {
714
+ const context = getRequestContext();
715
+ if (context?.transport === "http" || context?.authKind === "oauth")
716
+ return;
717
+ throw new Error(`${toolName} is only for remote HTTP/OAuth workspace clients. Stdio/local provider calls must not use workspace_* tools for path access; pass workingDir/addDir/includeDirs directly on the provider request instead.`);
718
+ }
681
719
  function registerWorkspaceTools(server, runtime) {
682
- server.tool("workspace_list", "List registered workspace aliases and summary metadata. Does not browse files.", {}, {
720
+ server.tool("workspace_list", "List registered workspace aliases for remote HTTP/OAuth provider calls. Does not browse files. Stdio/local callers should not use workspace_* tools to fix provider path access; pass local workingDir/addDir/includeDirs directly.", {}, {
683
721
  title: "List workspaces",
684
722
  readOnlyHint: true,
685
723
  destructiveHint: false,
686
724
  idempotentHint: true,
687
725
  openWorldHint: false,
688
726
  }, async () => {
689
- const registry = loadWorkspaceRegistry(runtime.logger);
690
- return {
691
- content: [
692
- {
693
- type: "text",
694
- text: JSON.stringify({
695
- success: true,
696
- enabled: registry.enabled,
697
- default: registry.defaultAlias,
698
- workspaces: registry.repos.map(describeWorkspace),
699
- allowed_roots: registry.allowedRoots.map(root => ({
700
- alias: root.alias,
701
- path: root.path,
702
- allow_register_existing_git_repos: root.allowRegisterExistingGitRepos,
703
- allow_create_directories: root.allowCreateDirectories,
704
- allow_init_git_repos: root.allowInitGitRepos,
705
- max_create_depth: root.maxCreateDepth,
706
- })),
707
- }, null, 2),
708
- },
709
- ],
710
- };
727
+ try {
728
+ assertWorkspaceToolCaller("workspace_list");
729
+ const registry = loadWorkspaceRegistry(runtime.logger);
730
+ return {
731
+ content: [
732
+ {
733
+ type: "text",
734
+ text: JSON.stringify({
735
+ success: true,
736
+ enabled: registry.enabled,
737
+ default: registry.defaultAlias,
738
+ workspaces: registry.repos.map(describeWorkspace),
739
+ allowed_roots: registry.allowedRoots.map(root => ({
740
+ alias: root.alias,
741
+ path: root.path,
742
+ allow_register_existing_git_repos: root.allowRegisterExistingGitRepos,
743
+ allow_create_directories: root.allowCreateDirectories,
744
+ allow_init_git_repos: root.allowInitGitRepos,
745
+ max_create_depth: root.maxCreateDepth,
746
+ })),
747
+ }, null, 2),
748
+ },
749
+ ],
750
+ };
751
+ }
752
+ catch (error) {
753
+ return createErrorResponse("workspace_list", 1, "", undefined, error);
754
+ }
711
755
  });
712
- server.tool("workspace_get", "Inspect a registered workspace alias. Does not list files.", { alias: WORKSPACE_ALIAS_SCHEMA }, {
756
+ server.tool("workspace_get", "Inspect a registered remote HTTP/OAuth workspace alias. Does not list files. Not needed for stdio/local provider calls; do not use workspace_* tools to fix local path access.", { alias: WORKSPACE_ALIAS_SCHEMA }, {
713
757
  title: "Get workspace",
714
758
  readOnlyHint: true,
715
759
  destructiveHint: false,
@@ -717,6 +761,7 @@ function registerWorkspaceTools(server, runtime) {
717
761
  openWorldHint: false,
718
762
  }, async ({ alias }) => {
719
763
  try {
764
+ assertWorkspaceToolCaller("workspace_get");
720
765
  const registry = loadWorkspaceRegistry(runtime.logger);
721
766
  return {
722
767
  content: [
@@ -731,7 +776,7 @@ function registerWorkspaceTools(server, runtime) {
731
776
  return createErrorResponse("workspace_get", 1, "", undefined, error);
732
777
  }
733
778
  });
734
- server.tool("workspace_create", "Create a new local folder or git repo under a configured allowed root. Requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin.", {
779
+ server.tool("workspace_create", "Create a remote HTTP/OAuth workspace alias by creating a new local folder or git repo under a configured allowed root. Not for stdio/local provider path access. Requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin.", {
735
780
  alias: WORKSPACE_ALIAS_SCHEMA,
736
781
  root: WORKSPACE_ALIAS_SCHEMA.describe("Allowed-root alias from workspace_list."),
737
782
  slug: z.string().min(1).max(255).describe("Safe relative path under the allowed root."),
@@ -745,6 +790,7 @@ function registerWorkspaceTools(server, runtime) {
745
790
  openWorldHint: false,
746
791
  }, async ({ alias, root, slug, kind, setDefault }) => {
747
792
  try {
793
+ assertWorkspaceToolCaller("workspace_create");
748
794
  if (!workspaceAdminEnabled()) {
749
795
  throw new Error("workspace_create requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin");
750
796
  }
@@ -769,7 +815,7 @@ function registerWorkspaceTools(server, runtime) {
769
815
  return createErrorResponse("workspace_create", 1, "", undefined, error);
770
816
  }
771
817
  });
772
- server.tool("workspace_register_existing_repo", "Register an existing local Git repo under an allowed root. Requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin.", {
818
+ server.tool("workspace_register_existing_repo", "Register an existing local Git repo as a remote HTTP/OAuth workspace alias. Not for stdio/local provider path access. Requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin.", {
773
819
  alias: WORKSPACE_ALIAS_SCHEMA,
774
820
  path: z
775
821
  .string()
@@ -784,6 +830,7 @@ function registerWorkspaceTools(server, runtime) {
784
830
  openWorldHint: false,
785
831
  }, async ({ alias, path, setDefault }) => {
786
832
  try {
833
+ assertWorkspaceToolCaller("workspace_register_existing_repo");
787
834
  if (!workspaceAdminEnabled()) {
788
835
  throw new Error("workspace_register_existing_repo requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin");
789
836
  }
@@ -808,6 +855,23 @@ function registerWorkspaceTools(server, runtime) {
808
855
  });
809
856
  }
810
857
  export function createErrorResponse(cli, code, stderr, correlationId, error, apiError) {
858
+ if (error instanceof JobSaturationError) {
859
+ const satMessage = `${error.message}. The gateway is protecting the host from process/request ` +
860
+ `overload; this is transient. Retry after a short delay, or reduce concurrent requests.`;
861
+ logger.error(`[${correlationId || "unknown"}] ${cli} rejected: gateway at capacity`);
862
+ return {
863
+ content: [{ type: "text", text: satMessage }],
864
+ isError: true,
865
+ structuredContent: {
866
+ response: satMessage,
867
+ correlationId: correlationId || null,
868
+ cli,
869
+ exitCode: code,
870
+ errorCategory: "saturated",
871
+ retryable: true,
872
+ },
873
+ };
874
+ }
811
875
  let errorMessage = `Error executing ${cli} CLI`;
812
876
  const isLaunchExit = code === 127 || code === -4058;
813
877
  const isOutputOverflow = Boolean(error) && /Output exceeded maximum size/i.test(error.message);
@@ -4162,7 +4226,8 @@ export function createGatewayServer(deps = {}) {
4162
4226
  addDir: z
4163
4227
  .array(z.string())
4164
4228
  .optional()
4165
- .describe("Claude --add-dir: additional directories the CLI is allowed to read/write beyond the process cwd. Each entry is emitted as its own --add-dir instance."),
4229
+ .describe("Claude --add-dir: additional directories the CLI is allowed to read/write beyond the process cwd. Each entry is emitted as its own --add-dir instance." +
4230
+ LOCAL_ADD_DIR_FIELD_SUFFIX),
4166
4231
  noSessionPersistence: z
4167
4232
  .boolean()
4168
4233
  .optional()
@@ -4181,7 +4246,7 @@ export function createGatewayServer(deps = {}) {
4181
4246
  .array(z.string())
4182
4247
  .optional()
4183
4248
  .describe('Claude --tools: restrict the available built-in tool set (distinct from allowedTools permission gating). Pass [""] to disable all tools.'),
4184
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4249
+ workspace: providerWorkspaceAliasSchema(),
4185
4250
  worktree: WORKTREE_SCHEMA.optional(),
4186
4251
  approvalStrategy: z
4187
4252
  .enum(["legacy", "mcp_managed"])
@@ -4525,12 +4590,14 @@ export function createGatewayServer(deps = {}) {
4525
4590
  .string()
4526
4591
  .min(1)
4527
4592
  .optional()
4528
- .describe("Codex -C/--cd <DIR>: working root for this session. Emitted on new sessions only; resume inherits the original session's cwd via CODEX_RESUME_FILTERED_FLAGS."),
4593
+ .describe("Codex -C/--cd <DIR>: working root for this session. Emitted on new sessions only; resume inherits the original session's cwd via CODEX_RESUME_FILTERED_FLAGS." +
4594
+ LOCAL_WORKING_DIR_FIELD_SUFFIX),
4529
4595
  addDir: z
4530
4596
  .array(z.string())
4531
4597
  .optional()
4532
- .describe("Codex --add-dir <DIR>: additional writable workspace directories. Emitted once per entry on new sessions only; resume inherits the original session's writable-dir policy."),
4533
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4598
+ .describe("Codex --add-dir <DIR>: additional writable workspace directories. Emitted once per entry on new sessions only; resume inherits the original session's writable-dir policy." +
4599
+ LOCAL_ADD_DIR_FIELD_SUFFIX),
4600
+ workspace: providerWorkspaceAliasSchema(),
4534
4601
  worktree: WORKTREE_SCHEMA.optional(),
4535
4602
  }, {
4536
4603
  title: "Codex request",
@@ -4729,7 +4796,7 @@ export function createGatewayServer(deps = {}) {
4729
4796
  .max(3_600_000)
4730
4797
  .optional()
4731
4798
  .describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
4732
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4799
+ workspace: providerWorkspaceAliasSchema(),
4733
4800
  }, {
4734
4801
  title: "Fork Codex session",
4735
4802
  readOnlyHint: false,
@@ -4853,7 +4920,7 @@ export function createGatewayServer(deps = {}) {
4853
4920
  includeDirs: z
4854
4921
  .array(z.string())
4855
4922
  .optional()
4856
- .describe("Additional workspace directories passed as --add-dir"),
4923
+ .describe("Additional workspace directories passed as --add-dir." + LOCAL_INCLUDE_DIRS_FIELD_SUFFIX),
4857
4924
  correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
4858
4925
  optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
4859
4926
  optimizeResponse: z.boolean().default(false).describe("Optimize response output"),
@@ -4893,7 +4960,7 @@ export function createGatewayServer(deps = {}) {
4893
4960
  .boolean()
4894
4961
  .optional()
4895
4962
  .describe("Antigravity 1.0.13 --new-project: create a new project for this session. Mutually exclusive with project."),
4896
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4963
+ workspace: providerWorkspaceAliasSchema(),
4897
4964
  worktree: WORKTREE_SCHEMA.optional(),
4898
4965
  }, {
4899
4966
  title: "Gemini request",
@@ -5015,7 +5082,7 @@ export function createGatewayServer(deps = {}) {
5015
5082
  .union([z.string().min(1), z.record(z.string(), z.unknown())])
5016
5083
  .optional()
5017
5084
  .describe("Grok 0.2.73 --json-schema: constrain output to a JSON Schema (string literal or object; implies json output). Set outputFormat:json so the gateway treats the reply as structured output (skips response optimization and warning injection); the default output format is not json, so pass it explicitly."),
5018
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
5085
+ workspace: providerWorkspaceAliasSchema(),
5019
5086
  worktree: WORKTREE_SCHEMA.optional(),
5020
5087
  }, {
5021
5088
  title: "Grok request",
@@ -5226,12 +5293,14 @@ export function createGatewayServer(deps = {}) {
5226
5293
  .string()
5227
5294
  .min(1)
5228
5295
  .optional()
5229
- .describe("Vibe --workdir <DIR>: change to this directory before running. Single value (Vibe accepts one --workdir per invocation)."),
5296
+ .describe("Vibe --workdir <DIR>: change to this directory before running. Single value (Vibe accepts one --workdir per invocation)." +
5297
+ LOCAL_WORKING_DIR_FIELD_SUFFIX),
5230
5298
  addDir: z
5231
5299
  .array(z.string())
5232
5300
  .optional()
5233
- .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance (Vibe states this flag may be specified multiple times)."),
5234
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
5301
+ .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance (Vibe states this flag may be specified multiple times)." +
5302
+ LOCAL_ADD_DIR_FIELD_SUFFIX),
5303
+ workspace: providerWorkspaceAliasSchema(),
5235
5304
  worktree: WORKTREE_SCHEMA.optional(),
5236
5305
  }, {
5237
5306
  title: "Mistral Vibe request",
@@ -5360,7 +5429,8 @@ export function createGatewayServer(deps = {}) {
5360
5429
  addDir: z
5361
5430
  .array(z.string())
5362
5431
  .optional()
5363
- .describe("Claude --add-dir: additional directories the CLI is allowed to read/write beyond the process cwd. Each entry is emitted as its own --add-dir instance."),
5432
+ .describe("Claude --add-dir: additional directories the CLI is allowed to read/write beyond the process cwd. Each entry is emitted as its own --add-dir instance." +
5433
+ LOCAL_ADD_DIR_FIELD_SUFFIX),
5364
5434
  noSessionPersistence: z
5365
5435
  .boolean()
5366
5436
  .optional()
@@ -5379,7 +5449,7 @@ export function createGatewayServer(deps = {}) {
5379
5449
  .array(z.string())
5380
5450
  .optional()
5381
5451
  .describe('Claude --tools: restrict the available built-in tool set (distinct from allowedTools permission gating). Pass [""] to disable all tools.'),
5382
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
5452
+ workspace: providerWorkspaceAliasSchema(),
5383
5453
  worktree: WORKTREE_SCHEMA.optional(),
5384
5454
  approvalStrategy: z
5385
5455
  .enum(["legacy", "mcp_managed"])
@@ -5629,12 +5699,14 @@ export function createGatewayServer(deps = {}) {
5629
5699
  .string()
5630
5700
  .min(1)
5631
5701
  .optional()
5632
- .describe("Codex -C/--cd <DIR>: working root for this session. New sessions only; resume inherits the original session's cwd."),
5702
+ .describe("Codex -C/--cd <DIR>: working root for this session. New sessions only; resume inherits the original session's cwd." +
5703
+ LOCAL_WORKING_DIR_FIELD_SUFFIX),
5633
5704
  addDir: z
5634
5705
  .array(z.string())
5635
5706
  .optional()
5636
- .describe("Codex --add-dir <DIR>: additional writable workspace directories (repeat per entry). New sessions only."),
5637
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
5707
+ .describe("Codex --add-dir <DIR>: additional writable workspace directories (repeat per entry). New sessions only." +
5708
+ LOCAL_ADD_DIR_FIELD_SUFFIX),
5709
+ workspace: providerWorkspaceAliasSchema(),
5638
5710
  worktree: WORKTREE_SCHEMA.optional(),
5639
5711
  }, {
5640
5712
  title: "Codex request (async job)",
@@ -5721,7 +5793,8 @@ export function createGatewayServer(deps = {}) {
5721
5793
  includeDirs: z
5722
5794
  .array(z.string())
5723
5795
  .optional()
5724
- .describe("Additional workspace directories passed as --add-dir"),
5796
+ .describe("Additional workspace directories passed as --add-dir." +
5797
+ LOCAL_INCLUDE_DIRS_FIELD_SUFFIX),
5725
5798
  correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
5726
5799
  optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
5727
5800
  idleTimeoutMs: z
@@ -5760,7 +5833,7 @@ export function createGatewayServer(deps = {}) {
5760
5833
  .boolean()
5761
5834
  .optional()
5762
5835
  .describe("Antigravity 1.0.13 --new-project: create a new project for this session. Mutually exclusive with project."),
5763
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
5836
+ workspace: providerWorkspaceAliasSchema(),
5764
5837
  worktree: WORKTREE_SCHEMA.optional(),
5765
5838
  }, {
5766
5839
  title: "Gemini request (async job)",
@@ -5872,7 +5945,8 @@ export function createGatewayServer(deps = {}) {
5872
5945
  .string()
5873
5946
  .min(1)
5874
5947
  .optional()
5875
- .describe("Grok --cwd <DIR>: working directory for this invocation. Lets headless callers run Grok against a directory other than the gateway process's cwd."),
5948
+ .describe("Grok --cwd <DIR>: working directory for this invocation. Lets headless callers run Grok against a directory other than the gateway process's cwd." +
5949
+ LOCAL_WORKING_DIR_FIELD_SUFFIX),
5876
5950
  sandbox: z
5877
5951
  .string()
5878
5952
  .min(1)
@@ -5988,7 +6062,7 @@ export function createGatewayServer(deps = {}) {
5988
6062
  .union([z.string().min(1), z.record(z.string(), z.unknown())])
5989
6063
  .optional()
5990
6064
  .describe("Grok 0.2.73 --json-schema: constrain output to a JSON Schema (string literal or object; implies json output). Set outputFormat:json so the gateway treats the reply as structured output (skips response optimization and warning injection); the default output format is not json, so pass it explicitly."),
5991
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
6065
+ workspace: providerWorkspaceAliasSchema(),
5992
6066
  worktree: WORKTREE_SCHEMA.optional(),
5993
6067
  }, {
5994
6068
  title: "Grok request (async job)",
@@ -6185,12 +6259,14 @@ export function createGatewayServer(deps = {}) {
6185
6259
  .string()
6186
6260
  .min(1)
6187
6261
  .optional()
6188
- .describe("Vibe --workdir <DIR>: change to this directory before running. Single value per invocation."),
6262
+ .describe("Vibe --workdir <DIR>: change to this directory before running. Single value per invocation." +
6263
+ LOCAL_WORKING_DIR_FIELD_SUFFIX),
6189
6264
  addDir: z
6190
6265
  .array(z.string())
6191
6266
  .optional()
6192
- .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance."),
6193
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
6267
+ .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance." +
6268
+ LOCAL_ADD_DIR_FIELD_SUFFIX),
6269
+ workspace: providerWorkspaceAliasSchema(),
6194
6270
  worktree: WORKTREE_SCHEMA.optional(),
6195
6271
  }, {
6196
6272
  title: "Mistral Vibe request (async job)",
@@ -6227,7 +6303,7 @@ export function createGatewayServer(deps = {}) {
6227
6303
  worktree,
6228
6304
  });
6229
6305
  });
6230
- server.tool("llm_job_status", "Check lifecycle status (running|completed|failed|canceled|orphaned) of a gateway async or deferred-sync job by jobId.", {
6306
+ server.tool("llm_job_status", "Check lifecycle status (queued|running|completed|failed|canceled|orphaned) of a gateway async or deferred-sync job by jobId.", {
6231
6307
  jobId: z.string().describe("Async job ID from *_request_async"),
6232
6308
  }, {
6233
6309
  title: "Async job status",
@@ -6447,18 +6523,22 @@ export function createGatewayServer(deps = {}) {
6447
6523
  openWorldHint: false,
6448
6524
  }, async () => {
6449
6525
  const health = asyncJobManager.getJobHealth();
6526
+ const storeAttached = asyncJobManager.hasStore();
6527
+ const asyncJobsEffective = persistence.backend !== "none" && persistence.asyncJobsEnabled && storeAttached;
6450
6528
  const persistenceBlock = {
6451
6529
  backend: persistence.backend,
6452
6530
  dbPath: persistence.path,
6453
6531
  dsn: persistence.dsn ? "[redacted]" : null,
6454
6532
  retentionDays: persistence.retentionDays,
6455
6533
  dedupWindowMs: persistence.dedupWindowMs,
6456
- asyncJobsEnabled: persistence.asyncJobsEnabled,
6534
+ asyncJobsEnabled: asyncJobsEffective,
6457
6535
  acknowledgeEphemeral: persistence.acknowledgeEphemeral,
6458
6536
  sources: persistence.sources,
6459
- warning: persistence.asyncJobsEnabled
6537
+ warning: asyncJobsEffective
6460
6538
  ? null
6461
- : "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.",
6539
+ : persistence.backend === "none"
6540
+ ? "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."
6541
+ : `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.`,
6462
6542
  };
6463
6543
  const outboundProviders = {
6464
6544
  xai: providers.xai
@@ -6467,7 +6547,7 @@ export function createGatewayServer(deps = {}) {
6467
6547
  enabled: isXaiProviderEnabled(providers),
6468
6548
  apiKeyEnv: providers.xai.apiKeyEnv,
6469
6549
  apiKeyPresent: isXaiProviderEnabled(providers),
6470
- baseUrl: providers.xai.baseUrl,
6550
+ baseUrl: redactDiagnosticUrl(providers.xai.baseUrl) ?? providers.xai.baseUrl,
6471
6551
  defaultModel: providers.xai.defaultModel,
6472
6552
  mode: isXaiProviderEnabled(providers) ? "sync" : "configured-missing-key",
6473
6553
  }
@@ -6482,16 +6562,59 @@ export function createGatewayServer(deps = {}) {
6482
6562
  },
6483
6563
  apiProviders: enabledApiProviders(providers).map(p => ({
6484
6564
  ...apiProviderCatalogEntry(p),
6485
- baseUrl: p.baseUrl,
6565
+ baseUrl: redactDiagnosticUrl(p.baseUrl) ?? p.baseUrl,
6486
6566
  breakerState: apiProviderBreakerState(p.name),
6487
6567
  })),
6488
6568
  sources: providers.sources,
6489
6569
  };
6570
+ const limiter = asyncJobManager.getLimiterSnapshot();
6571
+ const jobLimits = asyncJobManager.getConfiguredLimits();
6572
+ const httpLimits = getLimitsConfig().http;
6573
+ const mem = process.memoryUsage();
6574
+ const backpressure = {
6575
+ jobs: {
6576
+ running: limiter.running,
6577
+ queued: limiter.queued,
6578
+ runningByProvider: limiter.runningByProvider,
6579
+ queuedByProvider: limiter.queuedByProvider,
6580
+ maxRunning: limiter.maxRunning,
6581
+ maxRunningPerProvider: limiter.maxRunningPerProvider,
6582
+ maxQueued: limiter.maxQueued,
6583
+ rejected: limiter.rejected,
6584
+ timedOut: limiter.timedOut,
6585
+ saturated: limiter.saturated,
6586
+ completedJobMemoryTtlMs: jobLimits.completedJobMemoryTtlMs,
6587
+ maxJobOutputBytes: jobLimits.maxJobOutputBytes,
6588
+ },
6589
+ httpSessions: activeHttpGateway
6590
+ ? { active: true, ...activeHttpGateway.sessionHealth() }
6591
+ : {
6592
+ active: false,
6593
+ current: 0,
6594
+ max: httpLimits.maxSessions,
6595
+ oldestAgeMs: 0,
6596
+ idleTtlMs: httpLimits.sessionIdleTtlMs,
6597
+ reaperIntervalMs: httpLimits.sessionReaperIntervalMs,
6598
+ saturated: false,
6599
+ },
6600
+ memory: {
6601
+ rss: mem.rss,
6602
+ heapUsed: mem.heapUsed,
6603
+ heapTotal: mem.heapTotal,
6604
+ external: mem.external,
6605
+ },
6606
+ };
6490
6607
  return {
6491
6608
  content: [
6492
6609
  {
6493
6610
  type: "text",
6494
- text: JSON.stringify({ success: true, ...health, persistence: persistenceBlock, outboundProviders }, null, 2),
6611
+ text: JSON.stringify({
6612
+ success: true,
6613
+ ...health,
6614
+ backpressure,
6615
+ persistence: persistenceBlock,
6616
+ outboundProviders,
6617
+ }, null, 2),
6495
6618
  },
6496
6619
  ],
6497
6620
  };
@@ -6529,10 +6652,13 @@ export function createGatewayServer(deps = {}) {
6529
6652
  ],
6530
6653
  };
6531
6654
  });
6532
- server.tool("list_models", "List models, aliases, and defaults for one provider CLI (claude|codex|gemini|grok|mistral|devin), or omit cli to list all providers.", {
6655
+ const listModelsFilterValues = [
6656
+ ...new Set([...CLI_TYPES, ...enabledApiProviders(providers).map(p => p.name)]),
6657
+ ];
6658
+ server.tool("list_models", "List models, aliases, and defaults for one provider (claude|codex|gemini|grok|mistral|devin, or an enabled API provider name), or omit cli to list all providers. API providers are returned under an `apiProviders` array.", {
6533
6659
  cli: z
6534
- .preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
6535
- .describe("CLI filter (claude|codex|gemini|grok|mistral|devin)"),
6660
+ .preprocess(value => (value === "" || value === null ? undefined : value), z.enum(listModelsFilterValues).optional())
6661
+ .describe("Provider filter (claude|codex|gemini|grok|mistral|devin, or an enabled API provider name)"),
6536
6662
  }, {
6537
6663
  title: "Provider models",
6538
6664
  readOnlyHint: true,
@@ -6541,13 +6667,32 @@ export function createGatewayServer(deps = {}) {
6541
6667
  openWorldHint: false,
6542
6668
  }, async ({ cli }) => {
6543
6669
  const cliInfo = getAvailableCliInfo();
6544
- const result = cli ? { [cli]: cliInfo[cli] } : cliInfo;
6670
+ const apiRuntimes = enabledApiProviders(providers);
6671
+ let result;
6672
+ if (cli && CLI_TYPES.includes(cli)) {
6673
+ result = { [cli]: cliInfo[cli] };
6674
+ }
6675
+ else if (cli) {
6676
+ const runtime = apiRuntimes.find(p => p.name === cli);
6677
+ result = { apiProviders: runtime ? [apiProviderCatalogEntry(runtime)] : [] };
6678
+ }
6679
+ else {
6680
+ const apiProviders = apiRuntimes.map(apiProviderCatalogEntry);
6681
+ result = { ...cliInfo, ...(apiProviders.length > 0 ? { apiProviders } : {}) };
6682
+ }
6545
6683
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6546
6684
  });
6547
- server.tool("provider_tool_capabilities", "Report provider tool/feature capabilities and discovered local skill/tool integrations for claude|codex|gemini|grok|mistral|devin|grok_api.", {
6685
+ const providerToolCapabilitiesFilterValues = [
6686
+ ...new Set([
6687
+ ...CLI_TYPES,
6688
+ "grok_api",
6689
+ ...enabledApiProviders(providers).map(p => p.name),
6690
+ ]),
6691
+ ];
6692
+ server.tool("provider_tool_capabilities", "Report provider tool/feature capabilities and discovered local skill/tool integrations for claude|codex|gemini|grok|mistral|devin|grok_api, or an enabled API provider name.", {
6548
6693
  cli: z
6549
- .preprocess(value => (value === "" || value === null ? undefined : value), z.enum(["claude", "codex", "gemini", "grok", "mistral", "devin", "grok_api"]).optional())
6550
- .describe("Provider filter (claude|codex|gemini|grok|mistral|devin|grok_api)"),
6694
+ .preprocess(value => (value === "" || value === null ? undefined : value), z.enum(providerToolCapabilitiesFilterValues).optional())
6695
+ .describe("Provider filter (claude|codex|gemini|grok|mistral|devin|grok_api, or an enabled API provider name)"),
6551
6696
  includeSkills: z
6552
6697
  .boolean()
6553
6698
  .default(true)
@@ -6579,6 +6724,7 @@ export function createGatewayServer(deps = {}) {
6579
6724
  includeUnsupported,
6580
6725
  includePaths,
6581
6726
  refresh,
6727
+ providersConfig: providers,
6582
6728
  });
6583
6729
  return { content: [{ type: "text", text: JSON.stringify(capabilities, null, 2) }] };
6584
6730
  });
@@ -7124,7 +7270,7 @@ async function initializeSessionManager() {
7124
7270
  });
7125
7271
  logger.info("File-based session manager initialized");
7126
7272
  }
7127
- resourceProvider = new ResourceProvider(sessionManager, performanceMetrics, getFlightRecorder(logger), getCacheAwarenessConfig(logger));
7273
+ resourceProvider = new ResourceProvider(sessionManager, performanceMetrics, getFlightRecorder(logger), getCacheAwarenessConfig(logger), getProvidersConfig(logger));
7128
7274
  }
7129
7275
  function registerHealthResource(server) {
7130
7276
  if (db) {
@@ -7430,7 +7576,8 @@ async function main() {
7430
7576
  if (args[0] === "doctor") {
7431
7577
  if (args.includes("--json")) {
7432
7578
  const probeUpstream = args.includes("--probe-upstream") || args.includes("--probe-installed");
7433
- printDoctorJson({ probeUpstream });
7579
+ const probeApiProviders = args.includes("--probe-api-providers");
7580
+ await printDoctorJson({ probeUpstream, probeApiProviders });
7434
7581
  return;
7435
7582
  }
7436
7583
  process.stderr.write("Only doctor --json is supported in this layer.\n");