llm-cli-gateway 2.13.2 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +68 -29
  3. package/dist/acp/client.d.ts +29 -1
  4. package/dist/acp/client.js +78 -4
  5. package/dist/acp/errors.d.ts +9 -1
  6. package/dist/acp/errors.js +19 -0
  7. package/dist/acp/event-normalizer.d.ts +12 -0
  8. package/dist/acp/event-normalizer.js +16 -0
  9. package/dist/acp/flight-redaction.d.ts +3 -0
  10. package/dist/acp/flight-redaction.js +3 -0
  11. package/dist/acp/permission-bridge.js +11 -5
  12. package/dist/acp/process-manager.d.ts +2 -1
  13. package/dist/acp/process-manager.js +43 -4
  14. package/dist/acp/provider-registry.js +19 -11
  15. package/dist/acp/runtime.d.ts +8 -0
  16. package/dist/acp/runtime.js +47 -4
  17. package/dist/acp/types.d.ts +3083 -55
  18. package/dist/acp/types.js +242 -5
  19. package/dist/async-job-manager.d.ts +38 -1
  20. package/dist/async-job-manager.js +287 -20
  21. package/dist/codex-json-parser.d.ts +1 -0
  22. package/dist/codex-json-parser.js +6 -0
  23. package/dist/config.d.ts +19 -0
  24. package/dist/config.js +84 -1
  25. package/dist/flight-recorder.d.ts +2 -0
  26. package/dist/flight-recorder.js +20 -0
  27. package/dist/gemini-json-parser.d.ts +2 -0
  28. package/dist/gemini-json-parser.js +45 -8
  29. package/dist/grok-json-parser.d.ts +14 -0
  30. package/dist/grok-json-parser.js +156 -0
  31. package/dist/index.d.ts +47 -2
  32. package/dist/index.js +504 -122
  33. package/dist/job-store.d.ts +119 -11
  34. package/dist/job-store.js +372 -42
  35. package/dist/model-registry.d.ts +1 -0
  36. package/dist/model-registry.js +46 -0
  37. package/dist/oauth.js +2 -2
  38. package/dist/postgres-job-store-worker.d.ts +1 -0
  39. package/dist/postgres-job-store-worker.js +444 -0
  40. package/dist/pricing.d.ts +3 -2
  41. package/dist/provider-acp-capabilities.d.ts +52 -0
  42. package/dist/provider-acp-capabilities.js +101 -0
  43. package/dist/provider-admin-tools.d.ts +100 -0
  44. package/dist/provider-admin-tools.js +572 -0
  45. package/dist/provider-capability-cache.d.ts +46 -0
  46. package/dist/provider-capability-cache.js +248 -0
  47. package/dist/provider-capability-discovery.d.ts +85 -0
  48. package/dist/provider-capability-discovery.js +461 -0
  49. package/dist/provider-capability-resolver.d.ts +29 -0
  50. package/dist/provider-capability-resolver.js +92 -0
  51. package/dist/provider-definition-assertions.d.ts +9 -0
  52. package/dist/provider-definition-assertions.js +147 -0
  53. package/dist/provider-definitions.d.ts +127 -0
  54. package/dist/provider-definitions.js +758 -0
  55. package/dist/provider-help-parser.d.ts +34 -0
  56. package/dist/provider-help-parser.js +203 -0
  57. package/dist/provider-model-discovery.d.ts +30 -0
  58. package/dist/provider-model-discovery.js +229 -0
  59. package/dist/provider-output-metadata.d.ts +7 -0
  60. package/dist/provider-output-metadata.js +68 -0
  61. package/dist/provider-schema-builder.d.ts +22 -0
  62. package/dist/provider-schema-builder.js +55 -0
  63. package/dist/provider-surface-generator.d.ts +98 -0
  64. package/dist/provider-surface-generator.js +140 -0
  65. package/dist/provider-tool-capabilities.d.ts +3 -2
  66. package/dist/provider-tool-capabilities.js +77 -62
  67. package/dist/request-helpers.d.ts +37 -4
  68. package/dist/request-helpers.js +134 -0
  69. package/dist/resources.d.ts +6 -1
  70. package/dist/resources.js +64 -192
  71. package/dist/session-manager.js +18 -11
  72. package/dist/sqlite-driver.d.ts +1 -1
  73. package/dist/sqlite-driver.js +2 -1
  74. package/dist/stream-json-parser.d.ts +1 -0
  75. package/dist/stream-json-parser.js +3 -0
  76. package/dist/upstream-contracts.d.ts +17 -1
  77. package/dist/upstream-contracts.js +209 -36
  78. package/npm-shrinkwrap.json +2 -2
  79. package/package.json +8 -3
@@ -0,0 +1,461 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { InitializeResponseSchema } from "./acp/types.js";
6
+ import { envWithExtendedPath, getExtendedPath, resolveCommandForSpawn } from "./executor.js";
7
+ import { noopLogger } from "./logger.js";
8
+ import { getAllProviderDefinitions, } from "./provider-definitions.js";
9
+ import { checksumText, parseHelpText, } from "./provider-help-parser.js";
10
+ import { computeDiscoveryContractDrift, } from "./upstream-contracts.js";
11
+ export const KNOWN_ACP_METHODS = [
12
+ "initialize",
13
+ "authenticate",
14
+ "session/new",
15
+ "session/load",
16
+ "session/resume",
17
+ "session/list",
18
+ "session/prompt",
19
+ "session/cancel",
20
+ "session/close",
21
+ "session/delete",
22
+ "session/update",
23
+ "session/set_mode",
24
+ "session/set_config_option",
25
+ "session/request_permission",
26
+ "fs/read_text_file",
27
+ "fs/write_text_file",
28
+ "terminal/create",
29
+ "terminal/output",
30
+ "terminal/release",
31
+ ];
32
+ let cachedGatewayVersion;
33
+ export function gatewayVersion() {
34
+ if (cachedGatewayVersion !== undefined)
35
+ return cachedGatewayVersion;
36
+ const here = dirname(fileURLToPath(import.meta.url));
37
+ const candidates = [join(here, "..", "package.json"), join(here, "..", "..", "package.json")];
38
+ for (const candidate of candidates) {
39
+ try {
40
+ const parsed = JSON.parse(readFileSync(candidate, "utf8"));
41
+ if (parsed.version) {
42
+ cachedGatewayVersion = parsed.version;
43
+ return cachedGatewayVersion;
44
+ }
45
+ }
46
+ catch {
47
+ }
48
+ }
49
+ cachedGatewayVersion = "unknown";
50
+ return cachedGatewayVersion;
51
+ }
52
+ export function resolveExecutableAbsolutePath(exe) {
53
+ const extendedPath = getExtendedPath();
54
+ const resolved = resolveCommandForSpawn(exe, [], { envPath: extendedPath });
55
+ if (resolved.command !== exe)
56
+ return resolved.command;
57
+ const result = spawnSync(process.platform === "win32" ? "where" : "command", ["-v", exe], {
58
+ encoding: "utf8",
59
+ timeout: 3000,
60
+ windowsHide: true,
61
+ shell: process.platform !== "win32",
62
+ });
63
+ const out = (result.stdout ?? "").split(/\r?\n/).find(line => line.trim().length > 0);
64
+ return out?.trim() || exe;
65
+ }
66
+ export function createDefaultProbeRunner(timeoutMs = 5000) {
67
+ return async (exe, argv) => {
68
+ const extendedPath = getExtendedPath();
69
+ const env = envWithExtendedPath(process.env, extendedPath);
70
+ const resolved = resolveCommandForSpawn(exe, [...argv], { envPath: extendedPath });
71
+ const result = spawnSync(resolved.command, resolved.args, {
72
+ encoding: "utf8",
73
+ timeout: timeoutMs,
74
+ maxBuffer: 8 * 1024 * 1024,
75
+ env,
76
+ windowsHide: true,
77
+ windowsVerbatimArguments: resolved.windowsVerbatimArguments,
78
+ });
79
+ if (result.error)
80
+ throw result.error;
81
+ return {
82
+ stdout: result.stdout ?? "",
83
+ stderr: result.stderr ?? "",
84
+ code: result.status ?? 0,
85
+ };
86
+ };
87
+ }
88
+ export const defaultProbeRunner = createDefaultProbeRunner();
89
+ function probeExecutable(def, probe) {
90
+ return probe.executable ?? def.primaryExecutable;
91
+ }
92
+ function subcommandProbeKey(def, probe) {
93
+ const exe = probeExecutable(def, probe);
94
+ const prefix = exe === def.primaryExecutable ? "" : `${exe} `;
95
+ return `${prefix}${probe.argv.join(" ")}`;
96
+ }
97
+ function presentCapabilityTokens(bag) {
98
+ if (!bag || typeof bag !== "object")
99
+ return [];
100
+ return Object.entries(bag)
101
+ .filter(([key]) => key !== "_meta")
102
+ .filter(([, value]) => value !== false && value !== null && value !== undefined)
103
+ .map(([key]) => key);
104
+ }
105
+ function authMethodTokens(value) {
106
+ if (!Array.isArray(value))
107
+ return [];
108
+ return value
109
+ .map(entry => {
110
+ if (typeof entry === "string")
111
+ return entry;
112
+ if (entry && typeof entry === "object") {
113
+ const record = entry;
114
+ if (typeof record.id === "string")
115
+ return record.id;
116
+ if (typeof record.name === "string")
117
+ return record.name;
118
+ }
119
+ return null;
120
+ })
121
+ .filter((entry) => typeof entry === "string");
122
+ }
123
+ function methodListToStrings(value) {
124
+ if (!Array.isArray(value))
125
+ return [];
126
+ return value
127
+ .map(entry => typeof entry === "string"
128
+ ? entry
129
+ : entry &&
130
+ typeof entry === "object" &&
131
+ typeof entry.name === "string"
132
+ ? entry.name
133
+ : null)
134
+ .filter((entry) => typeof entry === "string");
135
+ }
136
+ export function parseAcpInitialize(text) {
137
+ const trimmed = text.trim();
138
+ if (trimmed.length === 0 || !(trimmed.startsWith("{") || trimmed.startsWith("["))) {
139
+ return { kind: "none" };
140
+ }
141
+ let parsed;
142
+ try {
143
+ parsed = JSON.parse(trimmed);
144
+ }
145
+ catch {
146
+ return { kind: "invalid", reason: "ACP initialize output is not valid JSON" };
147
+ }
148
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
149
+ return { kind: "invalid", reason: "ACP initialize output is not a JSON object" };
150
+ }
151
+ const root = parsed;
152
+ const candidate = root.result !== undefined && typeof root.result === "object" && root.result !== null
153
+ ? root.result
154
+ : root;
155
+ const validated = InitializeResponseSchema.safeParse(candidate);
156
+ if (!validated.success) {
157
+ return {
158
+ kind: "invalid",
159
+ reason: `ACP initialize response failed validation: ${validated.error.message}`,
160
+ };
161
+ }
162
+ const result = validated.data;
163
+ const caps = result.agentCapabilities ?? null;
164
+ const agentInfo = result.agentInfo
165
+ ? {
166
+ ...(typeof result.agentInfo.name === "string" ? { name: result.agentInfo.name } : {}),
167
+ ...(typeof result.agentInfo.version === "string"
168
+ ? { version: result.agentInfo.version }
169
+ : {}),
170
+ }
171
+ : null;
172
+ const advertisedMethods = methodListToStrings(candidate.methods ??
173
+ candidate.availableMethods);
174
+ const knownMethods = advertisedMethods.filter(method => KNOWN_ACP_METHODS.includes(method));
175
+ const extensionMethods = advertisedMethods.filter(method => !KNOWN_ACP_METHODS.includes(method));
176
+ const protocolVersion = typeof result.protocolVersion === "number" ? result.protocolVersion : null;
177
+ const sessionTokens = presentCapabilityTokens(caps?.sessionCapabilities);
178
+ if (caps?.loadSession === true)
179
+ sessionTokens.push("load");
180
+ return {
181
+ kind: "ok",
182
+ value: {
183
+ protocolVersion,
184
+ agentInfo,
185
+ authMethods: authMethodTokens(result.authMethods),
186
+ promptCapabilities: presentCapabilityTokens(caps?.promptCapabilities),
187
+ mcpCapabilities: presentCapabilityTokens(caps?.mcpCapabilities),
188
+ sessionCapabilities: sessionTokens,
189
+ agentCapabilities: caps,
190
+ knownMethods,
191
+ extensionMethods,
192
+ checksum: checksumText(trimmed),
193
+ },
194
+ };
195
+ }
196
+ const EMPTY_HELP = {
197
+ flags: [],
198
+ subcommands: [],
199
+ discoveredUnmapped: [],
200
+ checksum: checksumText(""),
201
+ };
202
+ export async function discoverProviderCapabilities(def, options = {}) {
203
+ const runner = options.runner ?? defaultProbeRunner;
204
+ const resolvePath = options.resolveExecutablePath ?? resolveExecutableAbsolutePath;
205
+ const version = options.gatewayVersion ?? gatewayVersion();
206
+ const logger = options.logger ?? noopLogger;
207
+ const evidence = [];
208
+ const unmapped = [];
209
+ const discoveredAt = new Date().toISOString();
210
+ let executablePath;
211
+ try {
212
+ executablePath = resolvePath(def.primaryExecutable);
213
+ }
214
+ catch {
215
+ executablePath = def.primaryExecutable;
216
+ }
217
+ let versionString;
218
+ try {
219
+ const result = await runner(def.primaryExecutable, def.discovery.version.argv);
220
+ versionString = (result.stdout || result.stderr).split(/\r?\n/)[0]?.trim() ?? "";
221
+ evidence.push(`version: ${def.primaryExecutable} ${def.discovery.version.argv.join(" ")}`);
222
+ }
223
+ catch (err) {
224
+ const reason = err instanceof Error ? err.message : String(err);
225
+ logger.debug(`capability discovery: ${def.id} version probe failed`, { reason });
226
+ return {
227
+ providerId: def.id,
228
+ executable: def.primaryExecutable,
229
+ executablePath,
230
+ version: "",
231
+ rootHelp: EMPTY_HELP,
232
+ subcommandHelp: {},
233
+ modelCatalog: {
234
+ strategy: def.discovery.modelDiscovery.strategy,
235
+ argv: def.discovery.modelDiscovery.argv,
236
+ raw: null,
237
+ checksum: checksumText(def.discovery.modelDiscovery.evidence),
238
+ evidence: def.discovery.modelDiscovery.evidence,
239
+ },
240
+ acpInitialize: null,
241
+ checksums: {
242
+ version: checksumText(""),
243
+ rootHelp: EMPTY_HELP.checksum,
244
+ subcommandHelp: {},
245
+ modelCatalog: checksumText(def.discovery.modelDiscovery.evidence),
246
+ acpInitialize: null,
247
+ },
248
+ sourceEvidence: evidence,
249
+ discoveredUnmapped: unmapped,
250
+ status: "error",
251
+ degradedReason: `version probe failed: ${reason}`,
252
+ gatewayVersion: version,
253
+ discoveredAt,
254
+ };
255
+ }
256
+ let degraded = false;
257
+ let degradedReason;
258
+ let rootHelp = EMPTY_HELP;
259
+ try {
260
+ const result = await runner(def.primaryExecutable, def.discovery.rootHelp.argv);
261
+ rootHelp = parseHelpText(result.stdout || result.stderr);
262
+ unmapped.push(...rootHelp.discoveredUnmapped);
263
+ evidence.push(`root-help: ${def.primaryExecutable} ${def.discovery.rootHelp.argv.join(" ")}`);
264
+ }
265
+ catch (err) {
266
+ degraded = true;
267
+ degradedReason = `root help probe failed: ${err instanceof Error ? err.message : String(err)}`;
268
+ logger.debug(`capability discovery: ${def.id} root help probe failed`, { degradedReason });
269
+ }
270
+ const subcommandHelp = {};
271
+ const subcommandChecksums = {};
272
+ for (const probe of def.discovery.subcommandHelp) {
273
+ const key = subcommandProbeKey(def, probe);
274
+ const exe = probeExecutable(def, probe);
275
+ try {
276
+ const result = await runner(exe, probe.argv);
277
+ const parsed = parseHelpText(result.stdout || result.stderr);
278
+ subcommandHelp[key] = parsed;
279
+ subcommandChecksums[key] = parsed.checksum;
280
+ unmapped.push(...parsed.discoveredUnmapped);
281
+ evidence.push(`subcommand-help: ${exe} ${probe.argv.join(" ")}`);
282
+ }
283
+ catch (err) {
284
+ degraded = true;
285
+ degradedReason = `subcommand help probe failed (${key}): ${err instanceof Error ? err.message : String(err)}`;
286
+ logger.debug(`capability discovery: ${def.id} subcommand help probe failed`, {
287
+ key,
288
+ degradedReason,
289
+ });
290
+ }
291
+ }
292
+ const md = def.discovery.modelDiscovery;
293
+ let modelRaw = null;
294
+ let modelChecksum = checksumText(md.evidence);
295
+ if (md.strategy === "native-command" && md.argv.length > 0) {
296
+ try {
297
+ const result = await runner(def.primaryExecutable, md.argv);
298
+ const stdout = result.stdout ?? "";
299
+ if (result.code === 0 && stdout.trim().length > 0) {
300
+ modelRaw = stdout;
301
+ modelChecksum = checksumText(modelRaw);
302
+ evidence.push(`model-catalog: ${def.primaryExecutable} ${md.argv.join(" ")}`);
303
+ }
304
+ else {
305
+ degraded = true;
306
+ const why = stdout.trim().length === 0 ? "empty stdout" : "stderr-only output";
307
+ degradedReason = `model catalog probe unusable (exit ${result.code}, ${why}); not treated as a catalog`;
308
+ evidence.push(`model-catalog: ${def.primaryExecutable} ${md.argv.join(" ")} (exit ${result.code}; ignored)`);
309
+ logger.debug(`capability discovery: ${def.id} model catalog probe unusable`, {
310
+ degradedReason,
311
+ });
312
+ }
313
+ }
314
+ catch (err) {
315
+ degraded = true;
316
+ degradedReason = `model catalog probe failed: ${err instanceof Error ? err.message : String(err)}`;
317
+ logger.debug(`capability discovery: ${def.id} model catalog probe failed`, {
318
+ degradedReason,
319
+ });
320
+ }
321
+ }
322
+ else {
323
+ evidence.push(`model-catalog: ${md.strategy} (no read-only listing command)`);
324
+ }
325
+ const modelCatalog = {
326
+ strategy: md.strategy,
327
+ argv: md.argv,
328
+ raw: modelRaw,
329
+ checksum: modelChecksum,
330
+ evidence: md.evidence,
331
+ };
332
+ let acpInitialize = null;
333
+ let acpChecksum = null;
334
+ if (def.acp.classification === "native" && def.acp.probeArgv.length > 0) {
335
+ const acpProbe = def.acp.probeArgv[0];
336
+ const exe = def.acp.entrypoint?.command ?? def.primaryExecutable;
337
+ try {
338
+ const result = await runner(exe, acpProbe);
339
+ const parsed = parseAcpInitialize(result.stdout || result.stderr);
340
+ if (parsed.kind === "ok") {
341
+ acpInitialize = parsed.value;
342
+ acpChecksum = parsed.value.checksum;
343
+ evidence.push(`acp-initialize: ${exe} ${acpProbe.join(" ")}`);
344
+ for (const method of parsed.value.extensionMethods) {
345
+ unmapped.push({
346
+ kind: "acp-method",
347
+ raw: method,
348
+ checksum: checksumText(method),
349
+ reason: "ACP method advertised by initialize is not known to the ACP spec and has no extension-namespace normalizer",
350
+ });
351
+ }
352
+ }
353
+ else if (parsed.kind === "invalid") {
354
+ degraded = true;
355
+ degradedReason = `acp initialize invalid: ${parsed.reason}`;
356
+ evidence.push(`acp-probe: ${exe} ${acpProbe.join(" ")} (invalid initialize response)`);
357
+ logger.debug(`capability discovery: ${def.id} acp initialize invalid`, { degradedReason });
358
+ }
359
+ else {
360
+ evidence.push(`acp-probe: ${exe} ${acpProbe.join(" ")} (no initialize response)`);
361
+ }
362
+ }
363
+ catch (err) {
364
+ degraded = true;
365
+ degradedReason = `acp probe failed: ${err instanceof Error ? err.message : String(err)}`;
366
+ logger.debug(`capability discovery: ${def.id} acp probe failed`, { degradedReason });
367
+ }
368
+ }
369
+ return {
370
+ providerId: def.id,
371
+ executable: def.primaryExecutable,
372
+ executablePath,
373
+ version: versionString,
374
+ rootHelp,
375
+ subcommandHelp,
376
+ modelCatalog,
377
+ acpInitialize,
378
+ checksums: {
379
+ version: checksumText(versionString),
380
+ rootHelp: rootHelp.checksum,
381
+ subcommandHelp: subcommandChecksums,
382
+ modelCatalog: modelChecksum,
383
+ acpInitialize: acpChecksum,
384
+ },
385
+ sourceEvidence: evidence,
386
+ discoveredUnmapped: unmapped,
387
+ status: degraded ? "degraded" : "ok",
388
+ ...(degraded ? { degradedReason } : {}),
389
+ gatewayVersion: version,
390
+ discoveredAt,
391
+ };
392
+ }
393
+ export async function discoverAllProviders(options = {}) {
394
+ const out = new Map();
395
+ const results = await Promise.all(getAllProviderDefinitions().map(async (def) => {
396
+ try {
397
+ return await discoverProviderCapabilities(def, options);
398
+ }
399
+ catch (err) {
400
+ const reason = err instanceof Error ? err.message : String(err);
401
+ const version = options.gatewayVersion ?? gatewayVersion();
402
+ const set = {
403
+ providerId: def.id,
404
+ executable: def.primaryExecutable,
405
+ executablePath: def.primaryExecutable,
406
+ version: "",
407
+ rootHelp: EMPTY_HELP,
408
+ subcommandHelp: {},
409
+ modelCatalog: {
410
+ strategy: def.discovery.modelDiscovery.strategy,
411
+ argv: def.discovery.modelDiscovery.argv,
412
+ raw: null,
413
+ checksum: checksumText(def.discovery.modelDiscovery.evidence),
414
+ evidence: def.discovery.modelDiscovery.evidence,
415
+ },
416
+ acpInitialize: null,
417
+ checksums: {
418
+ version: checksumText(""),
419
+ rootHelp: EMPTY_HELP.checksum,
420
+ subcommandHelp: {},
421
+ modelCatalog: checksumText(def.discovery.modelDiscovery.evidence),
422
+ acpInitialize: null,
423
+ },
424
+ sourceEvidence: [],
425
+ discoveredUnmapped: [],
426
+ status: "error",
427
+ degradedReason: `discovery threw: ${reason}`,
428
+ gatewayVersion: version,
429
+ discoveredAt: new Date().toISOString(),
430
+ };
431
+ return set;
432
+ }
433
+ }));
434
+ for (const set of results)
435
+ out.set(set.providerId, set);
436
+ return out;
437
+ }
438
+ export function acpMethodAvailability(set) {
439
+ if (!set.acpInitialize) {
440
+ return { acpAvailable: false, knownMethods: [], extensionMethods: [] };
441
+ }
442
+ return {
443
+ acpAvailable: true,
444
+ knownMethods: set.acpInitialize.knownMethods,
445
+ extensionMethods: set.acpInitialize.extensionMethods,
446
+ };
447
+ }
448
+ export function discoveryContractDrift(set) {
449
+ const discoveredFlagNames = new Set();
450
+ for (const flag of set.rootHelp.flags) {
451
+ discoveredFlagNames.add(flag.name);
452
+ for (const alias of flag.aliases)
453
+ discoveredFlagNames.add(alias);
454
+ }
455
+ return computeDiscoveryContractDrift(set.providerId, {
456
+ version: set.version,
457
+ discoveredFlagNames: [...discoveredFlagNames],
458
+ discoveredUnmappedCount: set.discoveredUnmapped.length,
459
+ status: set.status,
460
+ });
461
+ }
@@ -0,0 +1,29 @@
1
+ import type { CliType } from "./provider-definitions.js";
2
+ import { type ProviderDefinition } from "./provider-definitions.js";
3
+ import { type DiscoveredCapabilitySet, type ProbeRunner } from "./provider-capability-discovery.js";
4
+ import { type DiscoveredModelListing } from "./provider-model-discovery.js";
5
+ import type { CliInfo } from "./model-registry.js";
6
+ import { type Logger } from "./logger.js";
7
+ export type CapabilityResolutionSource = "live" | "cache" | "static-fallback";
8
+ export interface ResolvedProviderCapability {
9
+ readonly set: DiscoveredCapabilitySet;
10
+ readonly source: "live" | "cache";
11
+ readonly degraded: boolean;
12
+ }
13
+ export interface ResolveCapabilityOptions {
14
+ readonly runner?: ProbeRunner;
15
+ readonly inject?: (def: ProviderDefinition) => Promise<ResolvedProviderCapability | null> | ResolvedProviderCapability | null;
16
+ readonly forceRefresh?: boolean;
17
+ readonly logger?: Logger;
18
+ }
19
+ export declare function __resetCapabilityResolverMemoForTest(): void;
20
+ export declare function __seedCapabilityResolverMemoForTest(id: CliType, resolution: ResolvedProviderCapability): void;
21
+ export declare function peekProviderCapabilitySet(id: CliType): ResolvedProviderCapability | null;
22
+ export declare function resolveProviderCapabilitySet(def: ProviderDefinition, options?: ResolveCapabilityOptions): Promise<ResolvedProviderCapability | null>;
23
+ export declare function warmProviderCapabilities(options?: ResolveCapabilityOptions): Promise<void>;
24
+ export interface ProviderDiscoveredView {
25
+ readonly source: CapabilityResolutionSource;
26
+ readonly degraded: boolean;
27
+ readonly listing: DiscoveredModelListing | null;
28
+ }
29
+ export declare function buildProviderDiscoveredView(def: ProviderDefinition, registryInfo: CliInfo | undefined, peek?: (id: CliType) => ResolvedProviderCapability | null): ProviderDiscoveredView;
@@ -0,0 +1,92 @@
1
+ import { getAllProviderDefinitions } from "./provider-definitions.js";
2
+ import { defaultProbeRunner, discoverProviderCapabilities, } from "./provider-capability-discovery.js";
3
+ import { readCapabilityCache, resolveCapabilitySet } from "./provider-capability-cache.js";
4
+ import { discoverProviderModels } from "./provider-model-discovery.js";
5
+ import { noopLogger } from "./logger.js";
6
+ const DEFAULT_CAPABILITY_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
7
+ function capabilityCacheTtlMs() {
8
+ const raw = process.env.LLM_GATEWAY_CAPABILITY_CACHE_TTL_MS;
9
+ if (raw === undefined)
10
+ return DEFAULT_CAPABILITY_CACHE_TTL_MS;
11
+ const parsed = Number(raw);
12
+ return Number.isFinite(parsed) ? parsed : DEFAULT_CAPABILITY_CACHE_TTL_MS;
13
+ }
14
+ function isCacheSeedFresh(cachedAt) {
15
+ const ttl = capabilityCacheTtlMs();
16
+ if (ttl <= 0)
17
+ return false;
18
+ const cachedAtMs = Date.parse(cachedAt);
19
+ if (!Number.isFinite(cachedAtMs))
20
+ return false;
21
+ return Date.now() - cachedAtMs <= ttl;
22
+ }
23
+ const memo = new Map();
24
+ export function __resetCapabilityResolverMemoForTest() {
25
+ memo.clear();
26
+ }
27
+ export function __seedCapabilityResolverMemoForTest(id, resolution) {
28
+ memo.set(id, resolution);
29
+ }
30
+ export function peekProviderCapabilitySet(id) {
31
+ return memo.get(id) ?? null;
32
+ }
33
+ export async function resolveProviderCapabilitySet(def, options = {}) {
34
+ const logger = options.logger ?? noopLogger;
35
+ if (!options.forceRefresh) {
36
+ const memoized = memo.get(def.id);
37
+ if (memoized)
38
+ return memoized;
39
+ }
40
+ try {
41
+ if (options.inject) {
42
+ const injected = await options.inject(def);
43
+ if (injected)
44
+ memo.set(def.id, injected);
45
+ return injected;
46
+ }
47
+ if (!options.forceRefresh) {
48
+ const cached = readCapabilityCache(def.id);
49
+ if (cached && cached.capabilitySet.status !== "error" && isCacheSeedFresh(cached.cachedAt)) {
50
+ const resolution = {
51
+ set: cached.capabilitySet,
52
+ source: "cache",
53
+ degraded: cached.capabilitySet.status === "degraded",
54
+ };
55
+ memo.set(def.id, resolution);
56
+ return resolution;
57
+ }
58
+ }
59
+ const fresh = await discoverProviderCapabilities(def, {
60
+ runner: options.runner ?? defaultProbeRunner,
61
+ logger,
62
+ });
63
+ const resolved = resolveCapabilitySet(fresh);
64
+ if (resolved.source === "minimal") {
65
+ return null;
66
+ }
67
+ const resolution = {
68
+ set: resolved.set,
69
+ source: resolved.source === "discovery" ? "live" : "cache",
70
+ degraded: resolved.degraded,
71
+ };
72
+ memo.set(def.id, resolution);
73
+ return resolution;
74
+ }
75
+ catch (err) {
76
+ logger.debug(`capability resolve failed for ${def.id}`, {
77
+ reason: err instanceof Error ? err.message : String(err),
78
+ });
79
+ return null;
80
+ }
81
+ }
82
+ export async function warmProviderCapabilities(options = {}) {
83
+ await Promise.all(getAllProviderDefinitions().map(def => resolveProviderCapabilitySet(def, options).catch(() => null)));
84
+ }
85
+ export function buildProviderDiscoveredView(def, registryInfo, peek = peekProviderCapabilitySet) {
86
+ const resolved = peek(def.id);
87
+ if (!resolved) {
88
+ return { source: "static-fallback", degraded: false, listing: null };
89
+ }
90
+ const listing = discoverProviderModels(def, resolved.set, { registryInfo });
91
+ return { source: resolved.source, degraded: resolved.degraded, listing };
92
+ }
@@ -0,0 +1,9 @@
1
+ import { type CliType } from "./provider-types.js";
2
+ import { type ProviderDefinition } from "./provider-definitions.js";
3
+ export declare function assertNever(x: never, context?: string): never;
4
+ export declare function providerScopeLabel(id: CliType): string;
5
+ export declare const REQUIRED_PROVIDER_PROJECTIONS: readonly ["syncRequestTool", "asyncRequestTool", "modelsResource", "sessionsResource", "modelDiscovery", "sessionContinuity", "capabilityRow", "adminDescriptor", "upstreamContractRow", "docsSummary"];
6
+ export type RequiredProviderProjection = (typeof REQUIRED_PROVIDER_PROJECTIONS)[number];
7
+ export declare function assertExhaustiveProviderCoverage(defs?: readonly ProviderDefinition[]): void;
8
+ export declare function assertProviderProjectionsProducible(def: ProviderDefinition): ReadonlySet<RequiredProviderProjection>;
9
+ export declare function assertRegistryIntegrity(defs?: readonly ProviderDefinition[]): void;