@vedmalex/ai-connect 0.2.1 → 0.4.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.
@@ -219,11 +219,7 @@ function normalizeTransport(providerId, input) {
219
219
  ...selector.whereEquals !== void 0 ? { whereEquals: selector.whereEquals } : {}
220
220
  };
221
221
  };
222
- const parser = (() => {
223
- if (!descriptor.cli?.parser) {
224
- return void 0;
225
- }
226
- const cliParser = descriptor.cli.parser;
222
+ const normalizeCliParser = (cliParser) => {
227
223
  if (cliParser.kind === "json") {
228
224
  assert(
229
225
  cliParser.textPath.trim().length > 0,
@@ -236,13 +232,22 @@ function normalizeTransport(providerId, input) {
236
232
  ...cliParser.usagePath?.trim() ? { usagePath: cliParser.usagePath.trim() } : {}
237
233
  };
238
234
  }
235
+ if (cliParser.kind === "text") {
236
+ return {
237
+ kind: "text",
238
+ // Default trim=true, stripAnsi=false; only persist explicit overrides.
239
+ ...cliParser.trim === false ? { trim: false } : {},
240
+ ...cliParser.stripAnsi === true ? { stripAnsi: true } : {}
241
+ };
242
+ }
239
243
  return {
240
244
  kind: "jsonl",
241
245
  text: normalizeSelector(cliParser.text, "text"),
242
246
  ...cliParser.error ? { error: normalizeSelector(cliParser.error, "error") } : {},
243
247
  ...cliParser.usage ? { usage: normalizeSelector(cliParser.usage, "usage") } : {}
244
248
  };
245
- })();
249
+ };
250
+ const parser = descriptor.cli?.parser ? normalizeCliParser(descriptor.cli.parser) : void 0;
246
251
  const normalized = {
247
252
  ...descriptor.cli.preset ? { preset: descriptor.cli.preset } : {},
248
253
  ...descriptor.cli.argsTemplate ? {
@@ -254,11 +259,13 @@ function normalizeTransport(providerId, input) {
254
259
  if (!descriptor.cli?.discovery) {
255
260
  return void 0;
256
261
  }
257
- const via = descriptor.cli.discovery.via ?? "none";
258
- assert(
259
- via === "none" || via === "acp",
260
- `Unsupported CLI discovery mode "${String(via)}" for provider "${providerId}".`
261
- );
262
+ const explicitVia = descriptor.cli.discovery.via;
263
+ if (explicitVia !== void 0) {
264
+ assert(
265
+ explicitVia === "none" || explicitVia === "acp" || explicitVia === "command" || explicitVia === "static",
266
+ `Unsupported CLI discovery mode "${String(explicitVia)}" for provider "${providerId}".`
267
+ );
268
+ }
262
269
  const launch = descriptor.cli.discovery.acp?.launch ? (() => {
263
270
  const contextMode = descriptor.cli.discovery?.acp?.launch?.contextMode ?? "workspace";
264
271
  const skillsMode = descriptor.cli.discovery?.acp?.launch?.skillsMode ?? "default";
@@ -279,7 +286,7 @@ function normalizeTransport(providerId, input) {
279
286
  methodId: descriptor.cli.discovery.acp.auth.methodId.trim(),
280
287
  params: descriptor.cli.discovery.acp.auth.params ?? {}
281
288
  } : void 0;
282
- const acp = via === "acp" ? {
289
+ const acp = descriptor.cli.discovery.acp ? {
283
290
  ...descriptor.cli.discovery.acp?.providerId?.trim() ? { providerId: descriptor.cli.discovery.acp.providerId.trim() } : {},
284
291
  ...descriptor.cli.discovery.acp?.transportId?.trim() ? {
285
292
  transportId: descriptor.cli.discovery.acp.transportId.trim()
@@ -287,9 +294,55 @@ function normalizeTransport(providerId, input) {
287
294
  ...auth ? { auth } : {},
288
295
  ...launch ? { launch } : {}
289
296
  } : void 0;
297
+ const commandInput = descriptor.cli.discovery.command;
298
+ const command = commandInput ? (() => {
299
+ assert(
300
+ Array.isArray(commandInput.argsTemplate) && commandInput.argsTemplate.length > 0,
301
+ `CLI discovery command.argsTemplate must be a non-empty array for provider "${providerId}".`
302
+ );
303
+ const cmdParserInput = commandInput.parser ?? { kind: "text" };
304
+ const cmdParser = cmdParserInput.kind === "text" ? {
305
+ kind: "text",
306
+ ...cmdParserInput.trim === false ? { trim: false } : {},
307
+ ...cmdParserInput.stripAnsi === true ? { stripAnsi: true } : {}
308
+ } : { kind: cmdParserInput.kind };
309
+ if (cmdParser.kind !== "text") {
310
+ assert(
311
+ Boolean(commandInput.models?.idPath?.trim()),
312
+ `CLI discovery command.models.idPath is required for a ${cmdParser.kind} parser for provider "${providerId}".`
313
+ );
314
+ }
315
+ const m = commandInput.models;
316
+ const models = m?.idPath?.trim() ? {
317
+ ...m.path?.trim() ? { path: m.path.trim() } : {},
318
+ idPath: m.idPath.trim(),
319
+ ...m.namePath?.trim() ? { namePath: m.namePath.trim() } : {},
320
+ ...m.descriptionPath?.trim() ? { descriptionPath: m.descriptionPath.trim() } : {},
321
+ ...m.contextLengthPath?.trim() ? { contextLengthPath: m.contextLengthPath.trim() } : {}
322
+ } : void 0;
323
+ return {
324
+ ...commandInput.command?.trim() ? { command: commandInput.command.trim() } : {},
325
+ argsTemplate: commandInput.argsTemplate.map((part) => String(part)),
326
+ parser: cmdParser,
327
+ ...models ? { models } : {}
328
+ };
329
+ })() : void 0;
330
+ assert(
331
+ explicitVia !== "command" || command !== void 0,
332
+ `CLI discovery via:"command" requires a discovery.command block for provider "${providerId}".`
333
+ );
334
+ const fallback = descriptor.cli.discovery.fallback;
335
+ if (fallback !== void 0) {
336
+ assert(
337
+ fallback === "static" || fallback === "none",
338
+ `Unsupported CLI discovery fallback "${String(fallback)}" for provider "${providerId}".`
339
+ );
340
+ }
290
341
  return {
291
- via,
292
- ...acp ? { acp } : {}
342
+ ...explicitVia !== void 0 ? { via: explicitVia } : {},
343
+ ...acp ? { acp } : {},
344
+ ...command ? { command } : {},
345
+ ...fallback !== void 0 ? { fallback } : {}
293
346
  };
294
347
  })();
295
348
  return {
@@ -1730,6 +1783,25 @@ function resolveModelContextWindow(input) {
1730
1783
  const fallback = isUsableContextLength(input.defaultContextWindow) ? input.defaultContextWindow : DEFAULT_CONTEXT_WINDOW;
1731
1784
  return { contextWindow: fallback, source: "default" };
1732
1785
  }
1786
+ function fillConfiguredContextLength(route, models) {
1787
+ if (!isUsableContextLength(route.contextWindow)) {
1788
+ return models;
1789
+ }
1790
+ const configured = route.contextWindow;
1791
+ return models.map((entry) => {
1792
+ if (isUsableContextLength(entry.contextLength)) {
1793
+ return entry;
1794
+ }
1795
+ if (route.model !== void 0 && entry.modelId !== route.model) {
1796
+ return entry;
1797
+ }
1798
+ return {
1799
+ ...entry,
1800
+ contextLength: configured,
1801
+ metadata: { ...entry.metadata ?? {}, contextWindowSource: "configured" }
1802
+ };
1803
+ });
1804
+ }
1733
1805
  function modelContextCacheKey(input) {
1734
1806
  if (typeof input === "string") {
1735
1807
  return { key: `::${input}`, model: input };
@@ -4726,18 +4798,19 @@ function canonicalGeminiImageModelId(modelId) {
4726
4798
  }
4727
4799
  function buildModelCatalog(route, availableModels, currentModelId) {
4728
4800
  const requestedModelId = route.model;
4729
- const requestedModelAdvertised = availableModels.some(
4801
+ const filledModels = fillConfiguredContextLength(route, availableModels);
4802
+ const requestedModelAdvertised = filledModels.some(
4730
4803
  (model) => model.modelId === requestedModelId
4731
4804
  );
4732
4805
  const canonicalModelId = route.provider === "gemini" ? canonicalGeminiImageModelId(requestedModelId) : void 0;
4733
- const resolvedModelId = requestedModelAdvertised ? requestedModelId : canonicalModelId && availableModels.some((model) => model.modelId === canonicalModelId) ? canonicalModelId : void 0;
4806
+ const resolvedModelId = requestedModelAdvertised ? requestedModelId : canonicalModelId && filledModels.some((model) => model.modelId === canonicalModelId) ? canonicalModelId : void 0;
4734
4807
  return {
4735
4808
  requestedModelId,
4736
4809
  requestedModelAdvertised,
4737
4810
  ...canonicalModelId ? { canonicalModelId } : {},
4738
4811
  ...resolvedModelId ? { resolvedModelId } : {},
4739
4812
  ...currentModelId ? { currentModelId } : {},
4740
- availableModels
4813
+ availableModels: filledModels
4741
4814
  };
4742
4815
  }
4743
4816
  function parseOpenAiModelCatalog(route, payload) {
@@ -7966,6 +8039,10 @@ var AcpConnection = class {
7966
8039
  `ACP route "${context.route.id}" did not return an ACP model catalog in session/new.`
7967
8040
  );
7968
8041
  }
8042
+ catalog.availableModels = fillConfiguredContextLength(
8043
+ context.route,
8044
+ catalog.availableModels
8045
+ );
7969
8046
  return catalog;
7970
8047
  } finally {
7971
8048
  this.bumpIdleTimer();
@@ -8907,14 +8984,26 @@ function normalizeCliDiscoveryAcpSource(route, discovery) {
8907
8984
  };
8908
8985
  }
8909
8986
  function resolveCliDiscoverySource(route) {
8910
- const via = route.transport.cli?.discovery?.via ?? (defaultCliDiscoveryTransportIdForRoute(route) ? "acp" : "none");
8911
- if (via === "none") {
8912
- return { via: "none" };
8987
+ const discovery = route.transport.cli?.discovery;
8988
+ const hasModels = (route.advertisedModels?.length ?? 0) > 0;
8989
+ const hasAcpDefault = Boolean(defaultCliDiscoveryTransportIdForRoute(route));
8990
+ const via = discovery?.via ?? (discovery?.command ? "command" : hasAcpDefault ? "acp" : hasModels ? "static" : "none");
8991
+ switch (via) {
8992
+ case "none":
8993
+ return { via: "none" };
8994
+ case "static":
8995
+ return { via: "static" };
8996
+ case "command": {
8997
+ const command = discovery?.command;
8998
+ if (!command) {
8999
+ return { via: "none" };
9000
+ }
9001
+ const fallback = discovery?.fallback ?? (hasModels ? "static" : "none");
9002
+ return { via: "command", command, fallback };
9003
+ }
9004
+ default:
9005
+ return normalizeCliDiscoveryAcpSource(route, discovery?.acp);
8913
9006
  }
8914
- return normalizeCliDiscoveryAcpSource(
8915
- route,
8916
- route.transport.cli?.discovery?.acp
8917
- );
8918
9007
  }
8919
9008
  function createCliDiscoveryAcpRoute(route) {
8920
9009
  const discovery = resolveCliDiscoverySource(route);
@@ -9023,6 +9112,42 @@ async function buildCliInvocation(context, options) {
9023
9112
  parameterKeys
9024
9113
  };
9025
9114
  }
9115
+ function buildCliDiscoveryInvocation(route, command, options) {
9116
+ const commandLine = command.command?.trim() ? command.command : resolveCliCommand(route, options);
9117
+ const resolved = splitCommandLine2(commandLine);
9118
+ const parameterKeys = [];
9119
+ const args = [
9120
+ ...resolved.args,
9121
+ ...command.argsTemplate.map((part) => {
9122
+ if (part === "{model}") {
9123
+ parameterKeys.push("model");
9124
+ return route.model;
9125
+ }
9126
+ if (part.startsWith("--")) {
9127
+ parameterKeys.push(part);
9128
+ }
9129
+ return part;
9130
+ })
9131
+ ];
9132
+ return {
9133
+ command: resolved.command,
9134
+ args,
9135
+ cwd: path2.resolve(options?.cwd ?? process.cwd()),
9136
+ env: buildCliEnvironment(options),
9137
+ parameterKeys
9138
+ };
9139
+ }
9140
+ function buildStaticCliCatalog(route) {
9141
+ const models = route.advertisedModels.map((id) => ({
9142
+ modelId: id,
9143
+ name: id
9144
+ }));
9145
+ return buildModelCatalog(
9146
+ route,
9147
+ models,
9148
+ currentModelIdForRoute(route, route.advertisedModels)
9149
+ );
9150
+ }
9026
9151
  function statsToUsage(stats) {
9027
9152
  if (!stats || typeof stats !== "object") {
9028
9153
  return void 0;
@@ -9060,6 +9185,9 @@ function getValueByPath(value, dotPath) {
9060
9185
  }
9061
9186
  return current;
9062
9187
  }
9188
+ function splitJsonlLines(stdout) {
9189
+ return stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
9190
+ }
9063
9191
  function normalizeErrorMessage(value) {
9064
9192
  if (typeof value === "string" && value.trim()) {
9065
9193
  return value.trim();
@@ -9107,7 +9235,7 @@ function parseGenericJsonCli(stdout, parser) {
9107
9235
  };
9108
9236
  }
9109
9237
  function parseGenericJsonlCli(stdout, parser) {
9110
- const entries = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
9238
+ const entries = splitJsonlLines(stdout);
9111
9239
  const errorValue = parser.error ? findJsonlSelection(entries, parser.error) : void 0;
9112
9240
  const errorMessage = normalizeErrorMessage(errorValue);
9113
9241
  if (errorMessage) {
@@ -9128,6 +9256,70 @@ function parseGenericJsonlCli(stdout, parser) {
9128
9256
  data: entries
9129
9257
  };
9130
9258
  }
9259
+ var ANSI_ESCAPE_PATTERN = /\[[0-?]*[ -/]*[@-~]/g;
9260
+ function parseTextCli(stdout, parser) {
9261
+ let text = stdout;
9262
+ if (parser.stripAnsi) {
9263
+ text = text.replace(ANSI_ESCAPE_PATTERN, "");
9264
+ }
9265
+ if (parser.trim !== false) {
9266
+ text = text.trim();
9267
+ }
9268
+ if (!text) {
9269
+ throw new AiConnectError(
9270
+ "temporary_unavailable",
9271
+ "CLI text parser produced no output."
9272
+ );
9273
+ }
9274
+ return { text, data: stdout };
9275
+ }
9276
+ function modelInfoFromRecord(record, selector) {
9277
+ const rawId = getValueByPath(record, selector.idPath);
9278
+ const modelId = typeof rawId === "string" ? rawId.trim() : "";
9279
+ if (!modelId) {
9280
+ return void 0;
9281
+ }
9282
+ const rawName = selector.namePath ? getValueByPath(record, selector.namePath) : void 0;
9283
+ const name = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : modelId;
9284
+ const rawDescription = selector.descriptionPath ? getValueByPath(record, selector.descriptionPath) : void 0;
9285
+ const description = typeof rawDescription === "string" && rawDescription.trim().length > 0 ? rawDescription : void 0;
9286
+ const rawContext = selector.contextLengthPath ? getValueByPath(record, selector.contextLengthPath) : void 0;
9287
+ const contextLength = typeof rawContext === "number" && Number.isFinite(rawContext) && rawContext > 0 ? Math.floor(rawContext) : void 0;
9288
+ return {
9289
+ modelId,
9290
+ name,
9291
+ ...description ? { description } : {},
9292
+ ...contextLength !== void 0 ? { contextLength } : {}
9293
+ };
9294
+ }
9295
+ function parseCliModelList(stdout, parser, selector) {
9296
+ if (parser.kind === "text") {
9297
+ let text = stdout;
9298
+ if (parser.stripAnsi) {
9299
+ text = text.replace(ANSI_ESCAPE_PATTERN, "");
9300
+ }
9301
+ return text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => ({ modelId: line, name: line }));
9302
+ }
9303
+ if (!selector) {
9304
+ throw new AiConnectError(
9305
+ "validation_error",
9306
+ `CLI ${parser.kind} discovery parser requires a models selector with idPath.`
9307
+ );
9308
+ }
9309
+ const records = parser.kind === "jsonl" ? splitJsonlLines(stdout) : (() => {
9310
+ const payload = JSON.parse(stdout);
9311
+ const arr = selector.path ? getValueByPath(payload, selector.path) : payload;
9312
+ return Array.isArray(arr) ? arr : [];
9313
+ })();
9314
+ const models = [];
9315
+ for (const record of records) {
9316
+ const info = modelInfoFromRecord(record, selector);
9317
+ if (info) {
9318
+ models.push(info);
9319
+ }
9320
+ }
9321
+ return models;
9322
+ }
9131
9323
  function parseGeminiCli(stdout) {
9132
9324
  const payload = JSON.parse(stdout);
9133
9325
  if (typeof payload.error === "string") {
@@ -9199,8 +9391,7 @@ function parseClaudeCli(stdout) {
9199
9391
  };
9200
9392
  }
9201
9393
  function parseCodexCli(stdout, outputFileContent) {
9202
- const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
9203
- const events = lines.map((line) => JSON.parse(line));
9394
+ const events = splitJsonlLines(stdout);
9204
9395
  let text = outputFileContent?.trim() || void 0;
9205
9396
  let usage;
9206
9397
  for (const event of events) {
@@ -9258,7 +9449,14 @@ function parseCliResult(route, result, outputFileContent) {
9258
9449
  `CLI route "${route.id}" declared a parser override but normalization did not preserve it.`
9259
9450
  );
9260
9451
  }
9261
- return parser.kind === "json" ? parseGenericJsonCli(stdout, parser) : parseGenericJsonlCli(stdout, parser);
9452
+ switch (parser.kind) {
9453
+ case "text":
9454
+ return parseTextCli(result.stdout, parser);
9455
+ case "json":
9456
+ return parseGenericJsonCli(stdout, parser);
9457
+ default:
9458
+ return parseGenericJsonlCli(stdout, parser);
9459
+ }
9262
9460
  }
9263
9461
  switch (cliOptions.preset) {
9264
9462
  case "gemini":
@@ -9361,6 +9559,88 @@ async function cleanupCliInvocation(invocation) {
9361
9559
  }
9362
9560
  function createCliTransportManager(options) {
9363
9561
  return {
9562
+ async discoverModels(context) {
9563
+ const source = resolveCliDiscoverySource(context.route);
9564
+ if (source.via === "none") {
9565
+ throw new AiConnectError(
9566
+ "not_supported",
9567
+ `CLI transport "${context.route.transport.id}" does not support model discovery (no list command, no ACP sidecar, and no configured models[]).`
9568
+ );
9569
+ }
9570
+ if (source.via === "acp") {
9571
+ throw new AiConnectError(
9572
+ "not_supported",
9573
+ `CLI route "${context.route.id}" resolves discovery via acp; dispatch to the acp discovery route instead.`
9574
+ );
9575
+ }
9576
+ if (source.via === "static") {
9577
+ return buildStaticCliCatalog(context.route);
9578
+ }
9579
+ const phases = [];
9580
+ const invocation = buildCliDiscoveryInvocation(
9581
+ context.route,
9582
+ source.command,
9583
+ options
9584
+ );
9585
+ context.telemetry?.captureTransport({
9586
+ protocol: "cli",
9587
+ endpoint: invocation.command,
9588
+ method: "process",
9589
+ bodyKeys: ["argv"],
9590
+ parameterKeys: invocation.parameterKeys,
9591
+ phases,
9592
+ stream: false
9593
+ });
9594
+ try {
9595
+ let models;
9596
+ try {
9597
+ const execution = await executeCliInvocation(
9598
+ invocation,
9599
+ options?.timeoutMs ?? 6e4,
9600
+ phases,
9601
+ context.abort.signal
9602
+ );
9603
+ if (execution.exitCode !== 0 || !execution.stdout.trim()) {
9604
+ throw new AiConnectError(
9605
+ "temporary_unavailable",
9606
+ execution.stderr.trim() || `CLI discovery command for "${context.route.transport.id}" exited with code ${execution.exitCode ?? "null"}.`
9607
+ );
9608
+ }
9609
+ models = parseCliModelList(
9610
+ execution.stdout,
9611
+ source.command.parser,
9612
+ source.command.models
9613
+ );
9614
+ } catch (error) {
9615
+ if (error instanceof AiConnectError && error.code === "aborted") {
9616
+ throw error;
9617
+ }
9618
+ if (source.fallback === "static") {
9619
+ return buildStaticCliCatalog(context.route);
9620
+ }
9621
+ throw error;
9622
+ }
9623
+ if (models.length === 0) {
9624
+ if (source.fallback === "static") {
9625
+ return buildStaticCliCatalog(context.route);
9626
+ }
9627
+ throw new AiConnectError(
9628
+ "temporary_unavailable",
9629
+ `CLI discovery command for "${context.route.transport.id}" returned no models.`
9630
+ );
9631
+ }
9632
+ return buildModelCatalog(
9633
+ context.route,
9634
+ models,
9635
+ currentModelIdForRoute(
9636
+ context.route,
9637
+ models.map((model) => model.modelId)
9638
+ )
9639
+ );
9640
+ } finally {
9641
+ await cleanupCliInvocation(invocation);
9642
+ }
9643
+ },
9364
9644
  async runPrompt(context) {
9365
9645
  const invocation = await buildCliInvocation(context, options);
9366
9646
  const phases = [];
@@ -9880,17 +10160,21 @@ function createLocalRouteHandlers(options = {}) {
9880
10160
  return cliTransport.runPrompt(context);
9881
10161
  },
9882
10162
  async discoverModels(context) {
9883
- const discoveryRoute = createCliDiscoveryAcpRoute(context.route);
9884
- if (!discoveryRoute) {
9885
- throw new AiConnectError(
9886
- "not_supported",
9887
- `CLI route "${context.route.id}" does not define a model discovery backend.`
9888
- );
10163
+ const source = resolveCliDiscoverySource(context.route);
10164
+ if (source.via === "acp") {
10165
+ const discoveryRoute = createCliDiscoveryAcpRoute(context.route);
10166
+ if (!discoveryRoute) {
10167
+ throw new AiConnectError(
10168
+ "not_supported",
10169
+ `CLI route "${context.route.id}" does not define a model discovery backend.`
10170
+ );
10171
+ }
10172
+ return acpTransport.discoverModels({
10173
+ ...context,
10174
+ route: discoveryRoute
10175
+ });
9889
10176
  }
9890
- return acpTransport.discoverModels({
9891
- ...context,
9892
- route: discoveryRoute
9893
- });
10177
+ return cliTransport.discoverModels(context);
9894
10178
  },
9895
10179
  async verify({ route, runtime }) {
9896
10180
  try {