llm-cli-gateway 2.10.0 → 2.11.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 (64) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/README.md +46 -14
  3. package/dist/acp/event-normalizer.d.ts +42 -0
  4. package/dist/acp/event-normalizer.js +71 -0
  5. package/dist/acp/flight-redaction.d.ts +25 -0
  6. package/dist/acp/flight-redaction.js +40 -0
  7. package/dist/acp/host-services.d.ts +16 -0
  8. package/dist/acp/host-services.js +29 -0
  9. package/dist/acp/permission-bridge.d.ts +15 -0
  10. package/dist/acp/permission-bridge.js +90 -0
  11. package/dist/acp/process-manager.js +7 -1
  12. package/dist/acp/provider-registry.d.ts +1 -1
  13. package/dist/acp/provider-registry.js +18 -5
  14. package/dist/acp/runtime.d.ts +35 -0
  15. package/dist/acp/runtime.js +125 -0
  16. package/dist/acp/session-map.d.ts +42 -0
  17. package/dist/acp/session-map.js +67 -0
  18. package/dist/acp/smoke-harness.d.ts +28 -0
  19. package/dist/acp/smoke-harness.js +90 -0
  20. package/dist/api-http.d.ts +18 -0
  21. package/dist/api-http.js +122 -0
  22. package/dist/api-provider.d.ts +83 -0
  23. package/dist/api-provider.js +258 -0
  24. package/dist/api-request.d.ts +30 -0
  25. package/dist/api-request.js +51 -0
  26. package/dist/approval-manager.d.ts +1 -1
  27. package/dist/approval-manager.js +6 -7
  28. package/dist/async-job-manager.d.ts +19 -4
  29. package/dist/async-job-manager.js +211 -35
  30. package/dist/claude-mcp-config.d.ts +2 -2
  31. package/dist/claude-mcp-config.js +42 -52
  32. package/dist/cli-updater.js +16 -1
  33. package/dist/config.d.ts +20 -0
  34. package/dist/config.js +93 -35
  35. package/dist/doctor.d.ts +1 -1
  36. package/dist/flight-recorder.d.ts +1 -0
  37. package/dist/flight-recorder.js +11 -0
  38. package/dist/index.d.ts +56 -5
  39. package/dist/index.js +639 -38
  40. package/dist/job-store.d.ts +15 -0
  41. package/dist/job-store.js +39 -5
  42. package/dist/mcp-registry.d.ts +17 -0
  43. package/dist/mcp-registry.js +5 -0
  44. package/dist/metrics.js +7 -2
  45. package/dist/model-registry.js +11 -0
  46. package/dist/prompt-parts.d.ts +6 -6
  47. package/dist/provider-login-guidance.js +21 -0
  48. package/dist/provider-status.js +4 -1
  49. package/dist/provider-tool-capabilities.d.ts +8 -3
  50. package/dist/provider-tool-capabilities.js +107 -17
  51. package/dist/request-helpers.d.ts +6 -6
  52. package/dist/request-helpers.js +1 -4
  53. package/dist/session-manager-pg.js +2 -9
  54. package/dist/session-manager.d.ts +9 -4
  55. package/dist/session-manager.js +13 -4
  56. package/dist/upstream-contracts.js +184 -24
  57. package/dist/validation-normalizer.d.ts +2 -2
  58. package/dist/validation-orchestrator.d.ts +2 -0
  59. package/dist/validation-orchestrator.js +28 -7
  60. package/dist/validation-tools.d.ts +61 -0
  61. package/dist/validation-tools.js +36 -21
  62. package/migrations/005_provider_type_open_api_names.sql +28 -0
  63. package/npm-shrinkwrap.json +6 -5
  64. package/package.json +12 -9
@@ -6,6 +6,18 @@ import { computeRequestKey } from "./job-store.js";
6
6
  import { NoopFlightRecorder, } from "./flight-recorder.js";
7
7
  import { codexFrResponse } from "./codex-json-parser.js";
8
8
  import { getRequestContext, resolveOwnerPrincipal } from "./request-context.js";
9
+ import { runApiRequest, ApiHttpError, } from "./api-provider.js";
10
+ function extractApiHttpStatus(error) {
11
+ for (const candidate of [error, error?.cause]) {
12
+ if (candidate instanceof ApiHttpError && typeof candidate.status === "number") {
13
+ return candidate.status;
14
+ }
15
+ const status = candidate?.status;
16
+ if (typeof status === "number")
17
+ return status;
18
+ }
19
+ return null;
20
+ }
9
21
  const MAX_OUTPUT_SIZE = 50 * 1024 * 1024;
10
22
  const JOB_TTL_MS = 60 * 60 * 1000;
11
23
  const EVICTION_INTERVAL_MS = 5 * 60 * 1000;
@@ -120,6 +132,7 @@ export class AsyncJobManager {
120
132
  circuitBreakerState: "closed",
121
133
  optimizationApplied: false,
122
134
  exitCode: orphan.exitCode ?? 1,
135
+ httpStatus: orphan.transport === "http" ? (orphan.httpStatus ?? undefined) : undefined,
123
136
  errorMessage: "orphaned after gateway restart",
124
137
  status: "failed",
125
138
  };
@@ -244,6 +257,171 @@ export class AsyncJobManager {
244
257
  const extra = cli === "codex" ? `${withCwd}|fmt:${outputFormat ?? "text"}` : withCwd;
245
258
  return computeRequestKey(cli, args, extra);
246
259
  }
260
+ buildHttpRequestKey(providerName, req) {
261
+ const canonical = {
262
+ transport: "http",
263
+ provider: providerName,
264
+ baseUrl: req.baseUrl,
265
+ model: req.model,
266
+ messages: req.messages,
267
+ temperature: req.temperature ?? null,
268
+ topP: req.topP ?? null,
269
+ reasoningEffort: req.reasoningEffort ?? null,
270
+ maxOutputTokens: req.maxOutputTokens ?? null,
271
+ previousResponseId: req.previousResponseId ?? null,
272
+ };
273
+ return computeRequestKey(`http:${providerName}`, [], JSON.stringify(canonical));
274
+ }
275
+ tryReuseDedupedJob(requestKey, correlationId, label, onComplete) {
276
+ if (!this.store)
277
+ return null;
278
+ try {
279
+ const existing = this.store.findByRequestKey(requestKey);
280
+ if (!existing)
281
+ return null;
282
+ let record = this.jobs.get(existing.id);
283
+ if (!record)
284
+ record = this.hydrateFromStore(existing.id) ?? undefined;
285
+ if (!record)
286
+ return null;
287
+ this.logger.info(`Job ${existing.id} reused via dedup for ${label}`, {
288
+ correlationId,
289
+ originalCorrelationId: record.correlationId,
290
+ status: record.status,
291
+ });
292
+ if (onComplete) {
293
+ try {
294
+ onComplete();
295
+ }
296
+ catch (err) {
297
+ this.logger.error("dedup onComplete cleanup threw", err);
298
+ }
299
+ }
300
+ return {
301
+ snapshot: this.snapshot(record),
302
+ deduped: true,
303
+ originalCorrelationId: record.correlationId,
304
+ };
305
+ }
306
+ catch (err) {
307
+ this.logger.error("dedup lookup failed; proceeding with fresh run", err);
308
+ return null;
309
+ }
310
+ }
311
+ startHttpJob(params) {
312
+ const { provider, apiRequest, correlationId, forceRefresh, onComplete, writeFlightStart, flightRecorderEntry, extractUsage, } = params;
313
+ const requestKey = this.buildHttpRequestKey(provider.name, apiRequest);
314
+ if (!forceRefresh) {
315
+ const reused = this.tryReuseDedupedJob(requestKey, correlationId, provider.name, onComplete);
316
+ if (reused)
317
+ return reused;
318
+ }
319
+ const id = randomUUID();
320
+ const startedAt = new Date().toISOString();
321
+ const abort = new AbortController();
322
+ const ownerPrincipal = resolveOwnerPrincipal(getRequestContext());
323
+ const payloadJson = JSON.stringify({
324
+ baseUrl: apiRequest.baseUrl,
325
+ model: apiRequest.model,
326
+ messages: apiRequest.messages,
327
+ maxOutputTokens: apiRequest.maxOutputTokens,
328
+ temperature: apiRequest.temperature,
329
+ topP: apiRequest.topP,
330
+ reasoningEffort: apiRequest.reasoningEffort,
331
+ previousResponseId: apiRequest.previousResponseId,
332
+ });
333
+ const job = {
334
+ id,
335
+ cli: provider.name,
336
+ args: [],
337
+ requestKey,
338
+ correlationId,
339
+ status: "running",
340
+ startedAt,
341
+ finishedAt: null,
342
+ exitCode: null,
343
+ stdout: "",
344
+ stderr: "",
345
+ outputTruncated: false,
346
+ canceled: false,
347
+ error: null,
348
+ process: null,
349
+ transport: "http",
350
+ abort,
351
+ httpStatus: null,
352
+ payloadJson,
353
+ exited: false,
354
+ metricsRecorded: false,
355
+ ownerPrincipal,
356
+ onComplete,
357
+ onCompleteFired: false,
358
+ outputDirty: false,
359
+ lastOutputFlushAt: Date.now(),
360
+ flightRecorderEntry,
361
+ extractUsage,
362
+ flightRecorderComplete: false,
363
+ flightCompleteArmed: writeFlightStart === true,
364
+ };
365
+ this.jobs.set(id, job);
366
+ this.safeStoreCall("recordStart", () => this.store.recordStart({
367
+ id,
368
+ correlationId,
369
+ requestKey,
370
+ cli: provider.name,
371
+ args: [],
372
+ startedAt,
373
+ pid: null,
374
+ ownerPrincipal,
375
+ transport: "http",
376
+ payloadJson,
377
+ }));
378
+ if (writeFlightStart && flightRecorderEntry) {
379
+ try {
380
+ this.flightRecorder.logStart({
381
+ correlationId,
382
+ cli: provider.name,
383
+ model: flightRecorderEntry.model,
384
+ prompt: flightRecorderEntry.prompt,
385
+ sessionId: flightRecorderEntry.sessionId,
386
+ asyncJobId: id,
387
+ });
388
+ }
389
+ catch (err) {
390
+ this.logger.error("Async-path flight recorder logStart failed", err);
391
+ }
392
+ }
393
+ this.logger.info(`Job ${id} started for ${provider.name} (http)`, { correlationId });
394
+ runApiRequest(provider, apiRequest, this.logger, { signal: abort.signal })
395
+ .then(result => this.finalizeHttpJob(job, result, null))
396
+ .catch(error => this.finalizeHttpJob(job, null, error));
397
+ return { snapshot: this.snapshot(job), deduped: false };
398
+ }
399
+ finalizeHttpJob(job, result, error) {
400
+ if (job.status !== "running")
401
+ return;
402
+ if (result) {
403
+ job.status = "completed";
404
+ job.stdout = result.text;
405
+ job.httpStatus = result.httpStatus;
406
+ job.exitCode = 0;
407
+ }
408
+ else {
409
+ job.status = "failed";
410
+ const status = extractApiHttpStatus(error);
411
+ job.httpStatus = status;
412
+ const message = error?.message ?? "API request failed";
413
+ job.stderr = message;
414
+ job.error = message;
415
+ job.exitCode = 1;
416
+ }
417
+ job.finishedAt = new Date().toISOString();
418
+ job.exited = true;
419
+ job.abort = null;
420
+ this.emitMetrics(job);
421
+ this.persistComplete(job);
422
+ this.writeFlightComplete(job, job.status === "completed" ? "completed" : "failed");
423
+ this.fireOnComplete(job);
424
+ }
247
425
  fireOnComplete(job) {
248
426
  if (job.onCompleteFired)
249
427
  return;
@@ -268,7 +446,7 @@ export class AsyncJobManager {
268
446
  const usage = finalStatus === "completed" && job.extractUsage ? this.safeExtractUsage(job) : {};
269
447
  const isFailure = finalStatus === "failed";
270
448
  let response;
271
- if (job.cli === "codex") {
449
+ if (job.transport === "process" && job.cli === "codex") {
272
450
  const codexText = codexFrResponse(job.outputFormat, job.stdout);
273
451
  response = isFailure ? job.stderr || codexText : codexText;
274
452
  }
@@ -287,6 +465,7 @@ export class AsyncJobManager {
287
465
  circuitBreakerState: "closed",
288
466
  optimizationApplied: false,
289
467
  exitCode,
468
+ httpStatus: job.transport === "http" ? (job.httpStatus ?? undefined) : undefined,
290
469
  errorMessage,
291
470
  status: finalStatus,
292
471
  inputTokens: usage.inputTokens,
@@ -364,6 +543,7 @@ export class AsyncJobManager {
364
543
  outputTruncated: job.outputTruncated,
365
544
  error: job.error,
366
545
  finishedAt: job.finishedAt,
546
+ httpStatus: job.httpStatus,
367
547
  }));
368
548
  }
369
549
  hydrateFromStore(jobId) {
@@ -391,7 +571,7 @@ export class AsyncJobManager {
391
571
  const reconstituted = {
392
572
  id: row.id,
393
573
  cli: row.cli,
394
- args,
574
+ args: row.transport === "http" ? [] : args,
395
575
  requestKey: row.requestKey,
396
576
  correlationId: row.correlationId,
397
577
  status: row.status,
@@ -404,6 +584,10 @@ export class AsyncJobManager {
404
584
  canceled: row.status === "canceled",
405
585
  error: row.error,
406
586
  process: null,
587
+ transport: row.transport,
588
+ abort: null,
589
+ httpStatus: row.httpStatus,
590
+ payloadJson: row.payloadJson,
407
591
  exited: row.status !== "running",
408
592
  metricsRecorded: true,
409
593
  outputFormat: row.outputFormat ?? undefined,
@@ -437,39 +621,10 @@ export class AsyncJobManager {
437
621
  startJobWithDedup(cli, args, correlationId, opts = {}) {
438
622
  const { cwd, idleTimeoutMs, outputFormat, forceRefresh, env: extraEnv, stdin, onComplete, flightRecorderEntry, extractUsage, writeFlightStart, } = opts;
439
623
  const requestKey = this.buildRequestKey(cli, args, extraEnv, stdin, cwd, outputFormat);
440
- if (!forceRefresh && this.store) {
441
- try {
442
- const existing = this.store.findByRequestKey(requestKey);
443
- if (existing) {
444
- let record = this.jobs.get(existing.id);
445
- if (!record) {
446
- record = this.hydrateFromStore(existing.id) ?? undefined;
447
- }
448
- if (record) {
449
- this.logger.info(`Job ${existing.id} reused via dedup for ${cli}`, {
450
- correlationId,
451
- originalCorrelationId: record.correlationId,
452
- status: record.status,
453
- });
454
- if (onComplete) {
455
- try {
456
- onComplete();
457
- }
458
- catch (err) {
459
- this.logger.error("dedup onComplete cleanup threw", err);
460
- }
461
- }
462
- return {
463
- snapshot: this.snapshot(record),
464
- deduped: true,
465
- originalCorrelationId: record.correlationId,
466
- };
467
- }
468
- }
469
- }
470
- catch (err) {
471
- this.logger.error("dedup lookup failed; proceeding with fresh run", err);
472
- }
624
+ if (!forceRefresh) {
625
+ const reused = this.tryReuseDedupedJob(requestKey, correlationId, cli, onComplete);
626
+ if (reused)
627
+ return reused;
473
628
  }
474
629
  const id = randomUUID();
475
630
  const startedAt = new Date().toISOString();
@@ -515,6 +670,9 @@ export class AsyncJobManager {
515
670
  canceled: false,
516
671
  error: null,
517
672
  process: child,
673
+ transport: "process",
674
+ abort: null,
675
+ httpStatus: null,
518
676
  exited: false,
519
677
  metricsRecorded: false,
520
678
  outputFormat,
@@ -698,6 +856,24 @@ export class AsyncJobManager {
698
856
  if (job.status !== "running") {
699
857
  return { canceled: false, reason: `Job is already ${job.status}` };
700
858
  }
859
+ if (job.transport === "http") {
860
+ if (!job.abort) {
861
+ return {
862
+ canceled: false,
863
+ reason: "Job has no live request (orphaned from prior gateway run)",
864
+ };
865
+ }
866
+ job.canceled = true;
867
+ job.status = "canceled";
868
+ job.finishedAt = new Date().toISOString();
869
+ job.abort.abort();
870
+ this.logger.info(`Job ${jobId} canceled (http)`, { correlationId: job.correlationId });
871
+ this.emitMetrics(job);
872
+ this.persistComplete(job);
873
+ this.writeFlightComplete(job, "failed", "canceled by caller");
874
+ this.fireOnComplete(job);
875
+ return { canceled: true };
876
+ }
701
877
  if (!job.process) {
702
878
  return {
703
879
  canceled: false,
@@ -1,5 +1,5 @@
1
- export declare const CLAUDE_MCP_SERVER_NAMES: readonly ["sqry", "exa", "ref_tools", "trstr"];
2
- export type ClaudeMcpServerName = (typeof CLAUDE_MCP_SERVER_NAMES)[number];
1
+ export { CLAUDE_MCP_SERVER_NAMES } from "./mcp-registry.js";
2
+ export type ClaudeMcpServerName = string;
3
3
  export interface ClaudeMcpConfigResult {
4
4
  path: string;
5
5
  enabled: ClaudeMcpServerName[];
@@ -1,8 +1,9 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, renameSync, openSync, fsyncSync, closeSync, chmodSync, } from "fs";
1
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, openSync, fsyncSync, closeSync, chmodSync, } from "fs";
2
2
  import { homedir } from "os";
3
- import { dirname, join } from "path";
3
+ import { delimiter, dirname, join } from "path";
4
4
  import { parse as parseToml } from "smol-toml";
5
- export const CLAUDE_MCP_SERVER_NAMES = ["sqry", "exa", "ref_tools", "trstr"];
5
+ import { INTERNAL_MCP_REGISTRY } from "./mcp-registry.js";
6
+ export { CLAUDE_MCP_SERVER_NAMES } from "./mcp-registry.js";
6
7
  function asStringArray(value) {
7
8
  if (!Array.isArray(value)) {
8
9
  return undefined;
@@ -52,69 +53,58 @@ function readCodexServerConfig(server) {
52
53
  return {};
53
54
  }
54
55
  }
55
- function findInstalledExaEntrypoint() {
56
- const nvmVersionsDir = join(homedir(), ".nvm", "versions", "node");
57
- if (!existsSync(nvmVersionsDir)) {
58
- return null;
59
- }
60
- let versions = [];
61
- try {
62
- versions = readdirSync(nvmVersionsDir);
63
- }
64
- catch {
65
- return null;
66
- }
67
- const candidates = [];
68
- for (const version of versions) {
69
- const entrypoint = join(nvmVersionsDir, version, "lib", "node_modules", "exa-mcp-server", ".smithery", "stdio", "index.cjs");
70
- if (existsSync(entrypoint)) {
71
- candidates.push(entrypoint);
56
+ function commandExists(command) {
57
+ if (command.includes("/") || command.includes("\\")) {
58
+ try {
59
+ accessSync(command, constants.X_OK);
60
+ return true;
61
+ }
62
+ catch {
63
+ return false;
72
64
  }
73
65
  }
74
- if (candidates.length === 0) {
75
- return null;
76
- }
77
- candidates.sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }));
78
- return candidates[0];
79
- }
80
- function defaultServerDef(server) {
81
- switch (server) {
82
- case "sqry":
83
- return { command: join(homedir(), ".local", "bin", "sqry-mcp"), args: [] };
84
- case "trstr":
85
- return { command: join(homedir(), ".local", "bin", "trstr-mcp"), args: [] };
86
- case "exa": {
87
- const exaEntrypoint = findInstalledExaEntrypoint();
88
- if (exaEntrypoint) {
89
- return { command: "node", args: [exaEntrypoint] };
66
+ const pathEnv = process.env.PATH || "";
67
+ const extensions = process.platform === "win32" ? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";") : [""];
68
+ for (const dir of pathEnv.split(delimiter).filter(Boolean)) {
69
+ for (const ext of extensions) {
70
+ try {
71
+ accessSync(join(dir, `${command}${ext}`), constants.X_OK);
72
+ return true;
73
+ }
74
+ catch {
90
75
  }
91
- return { command: "npx", args: ["-y", "exa-mcp-server"] };
92
- }
93
- case "ref_tools":
94
- return { command: "npx", args: ["-y", "ref-tools-mcp"] };
95
- default: {
96
- const _exhaustive = server;
97
- throw new Error(`Unknown MCP server: ${_exhaustive}`);
98
76
  }
99
77
  }
78
+ return false;
100
79
  }
101
80
  function toClaudeServerDef(server) {
81
+ const entry = INTERNAL_MCP_REGISTRY[server];
102
82
  const codexDef = readCodexServerConfig(server);
103
- const fallback = defaultServerDef(server);
83
+ const fallback = entry ? entry.defaultDef() : {};
104
84
  const command = codexDef.command || fallback.command;
85
+ if (!command) {
86
+ return null;
87
+ }
105
88
  const args = codexDef.args || fallback.args || [];
106
89
  const env = {};
107
90
  if (codexDef.env) {
108
91
  Object.assign(env, codexDef.env);
109
92
  }
110
- if (server === "exa" && process.env.EXA_API_KEY) {
111
- env.EXA_API_KEY = process.env.EXA_API_KEY;
112
- }
113
- if (server === "ref_tools" && process.env.REF_API_KEY) {
114
- env.REF_API_KEY = process.env.REF_API_KEY;
115
- }
116
- if ((server === "exa" && !env.EXA_API_KEY) || (server === "ref_tools" && !env.REF_API_KEY)) {
117
- return null;
93
+ if (entry) {
94
+ for (const key of entry.forwardEnv ?? []) {
95
+ const value = process.env[key];
96
+ if (value) {
97
+ env[key] = value;
98
+ }
99
+ }
100
+ for (const key of entry.requireEnv ?? []) {
101
+ if (!env[key]) {
102
+ return null;
103
+ }
104
+ }
105
+ if (entry.requireCommandOnPath && !codexDef.command && !commandExists(command)) {
106
+ return null;
107
+ }
118
108
  }
119
109
  return {
120
110
  command,
@@ -34,6 +34,7 @@ const VERSION_ARGS = {
34
34
  gemini: ["--version"],
35
35
  grok: ["--version"],
36
36
  mistral: ["--version"],
37
+ devin: ["--version"],
37
38
  };
38
39
  const CODEX_NPM_PACKAGE = "@openai/codex";
39
40
  export function buildCliUpgradePlan(cli, target = "latest", detectMistral = detectMistralInstallMethod) {
@@ -93,6 +94,20 @@ export function buildCliUpgradePlan(cli, target = "latest", detectMistral = dete
93
94
  requiresNetwork: true,
94
95
  };
95
96
  }
97
+ if (cli === "devin") {
98
+ if (normalizedTarget !== "latest") {
99
+ throw new Error("Devin CLI upgrades support only the 'latest' target via 'devin update'.");
100
+ }
101
+ return {
102
+ cli,
103
+ target: normalizedTarget,
104
+ command: "devin",
105
+ args: ["update"],
106
+ strategy: "self-update",
107
+ requiresNetwork: true,
108
+ note: "Devin CLI self-updates via 'devin update' (use --force to reinstall the latest).",
109
+ };
110
+ }
96
111
  if (cli === "gemini") {
97
112
  if (normalizedTarget !== "latest") {
98
113
  throw new Error("Antigravity CLI upgrades support only the 'latest' target via 'agy update'.");
@@ -150,7 +165,7 @@ export async function getCliVersion(cli) {
150
165
  }
151
166
  }
152
167
  export async function getCliVersions(cli) {
153
- const clis = cli ? [cli] : ["claude", "codex", "gemini", "grok", "mistral"];
168
+ const clis = cli ? [cli] : ["claude", "codex", "gemini", "grok", "mistral", "devin"];
154
169
  return Promise.all(clis.map(item => getCliVersion(item)));
155
170
  }
156
171
  function buildMistralUpgradePlan(normalizedTarget, detectMistral) {
package/dist/config.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Logger } from "./logger.js";
2
2
  import type { RemoteOAuthConfig } from "./auth.js";
3
+ import type { ApiProviderKind } from "./api-provider.js";
3
4
  export interface DatabaseConfig {
4
5
  connectionString: string;
5
6
  pool: {
@@ -68,13 +69,32 @@ export interface XaiProviderConfig {
68
69
  baseUrl: string;
69
70
  defaultModel: string;
70
71
  }
72
+ export interface ApiProviderConfig {
73
+ name: string;
74
+ kind: ApiProviderKind;
75
+ apiKeyEnv: string | null;
76
+ baseUrl: string;
77
+ defaultModel: string;
78
+ models?: string[];
79
+ }
80
+ export interface ApiProviderRuntime {
81
+ name: string;
82
+ kind: ApiProviderKind;
83
+ baseUrl: string;
84
+ defaultModel: string;
85
+ models?: string[];
86
+ apiKey: string;
87
+ }
71
88
  export interface ProvidersConfig {
72
89
  xai: XaiProviderConfig | null;
90
+ providers: Record<string, ApiProviderConfig>;
73
91
  sources: {
74
92
  configFile: string | null;
75
93
  };
76
94
  }
77
95
  export declare function loadProvidersConfig(logger?: Logger): ProvidersConfig;
96
+ export declare function isApiProviderEnabled(provider: ApiProviderConfig, env?: NodeJS.ProcessEnv): boolean;
97
+ export declare function enabledApiProviders(config: ProvidersConfig, env?: NodeJS.ProcessEnv): ApiProviderRuntime[];
78
98
  export declare function isXaiProviderEnabled(config: ProvidersConfig, env?: NodeJS.ProcessEnv): boolean;
79
99
  export declare const ACP_TRANSPORTS: readonly ["cli", "acp"];
80
100
  export type AcpTransport = (typeof ACP_TRANSPORTS)[number];