@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.
package/dist/bun/local.js CHANGED
@@ -220,11 +220,7 @@ function normalizeTransport(providerId, input) {
220
220
  ...selector.whereEquals !== void 0 ? { whereEquals: selector.whereEquals } : {}
221
221
  };
222
222
  };
223
- const parser = (() => {
224
- if (!descriptor.cli?.parser) {
225
- return void 0;
226
- }
227
- const cliParser = descriptor.cli.parser;
223
+ const normalizeCliParser = (cliParser) => {
228
224
  if (cliParser.kind === "json") {
229
225
  assert(
230
226
  cliParser.textPath.trim().length > 0,
@@ -237,13 +233,22 @@ function normalizeTransport(providerId, input) {
237
233
  ...cliParser.usagePath?.trim() ? { usagePath: cliParser.usagePath.trim() } : {}
238
234
  };
239
235
  }
236
+ if (cliParser.kind === "text") {
237
+ return {
238
+ kind: "text",
239
+ // Default trim=true, stripAnsi=false; only persist explicit overrides.
240
+ ...cliParser.trim === false ? { trim: false } : {},
241
+ ...cliParser.stripAnsi === true ? { stripAnsi: true } : {}
242
+ };
243
+ }
240
244
  return {
241
245
  kind: "jsonl",
242
246
  text: normalizeSelector(cliParser.text, "text"),
243
247
  ...cliParser.error ? { error: normalizeSelector(cliParser.error, "error") } : {},
244
248
  ...cliParser.usage ? { usage: normalizeSelector(cliParser.usage, "usage") } : {}
245
249
  };
246
- })();
250
+ };
251
+ const parser = descriptor.cli?.parser ? normalizeCliParser(descriptor.cli.parser) : void 0;
247
252
  const normalized = {
248
253
  ...descriptor.cli.preset ? { preset: descriptor.cli.preset } : {},
249
254
  ...descriptor.cli.argsTemplate ? {
@@ -255,11 +260,13 @@ function normalizeTransport(providerId, input) {
255
260
  if (!descriptor.cli?.discovery) {
256
261
  return void 0;
257
262
  }
258
- const via = descriptor.cli.discovery.via ?? "none";
259
- assert(
260
- via === "none" || via === "acp",
261
- `Unsupported CLI discovery mode "${String(via)}" for provider "${providerId}".`
262
- );
263
+ const explicitVia = descriptor.cli.discovery.via;
264
+ if (explicitVia !== void 0) {
265
+ assert(
266
+ explicitVia === "none" || explicitVia === "acp" || explicitVia === "command" || explicitVia === "static",
267
+ `Unsupported CLI discovery mode "${String(explicitVia)}" for provider "${providerId}".`
268
+ );
269
+ }
263
270
  const launch = descriptor.cli.discovery.acp?.launch ? (() => {
264
271
  const contextMode = descriptor.cli.discovery?.acp?.launch?.contextMode ?? "workspace";
265
272
  const skillsMode = descriptor.cli.discovery?.acp?.launch?.skillsMode ?? "default";
@@ -280,7 +287,7 @@ function normalizeTransport(providerId, input) {
280
287
  methodId: descriptor.cli.discovery.acp.auth.methodId.trim(),
281
288
  params: descriptor.cli.discovery.acp.auth.params ?? {}
282
289
  } : void 0;
283
- const acp = via === "acp" ? {
290
+ const acp = descriptor.cli.discovery.acp ? {
284
291
  ...descriptor.cli.discovery.acp?.providerId?.trim() ? { providerId: descriptor.cli.discovery.acp.providerId.trim() } : {},
285
292
  ...descriptor.cli.discovery.acp?.transportId?.trim() ? {
286
293
  transportId: descriptor.cli.discovery.acp.transportId.trim()
@@ -288,9 +295,55 @@ function normalizeTransport(providerId, input) {
288
295
  ...auth ? { auth } : {},
289
296
  ...launch ? { launch } : {}
290
297
  } : void 0;
298
+ const commandInput = descriptor.cli.discovery.command;
299
+ const command = commandInput ? (() => {
300
+ assert(
301
+ Array.isArray(commandInput.argsTemplate) && commandInput.argsTemplate.length > 0,
302
+ `CLI discovery command.argsTemplate must be a non-empty array for provider "${providerId}".`
303
+ );
304
+ const cmdParserInput = commandInput.parser ?? { kind: "text" };
305
+ const cmdParser = cmdParserInput.kind === "text" ? {
306
+ kind: "text",
307
+ ...cmdParserInput.trim === false ? { trim: false } : {},
308
+ ...cmdParserInput.stripAnsi === true ? { stripAnsi: true } : {}
309
+ } : { kind: cmdParserInput.kind };
310
+ if (cmdParser.kind !== "text") {
311
+ assert(
312
+ Boolean(commandInput.models?.idPath?.trim()),
313
+ `CLI discovery command.models.idPath is required for a ${cmdParser.kind} parser for provider "${providerId}".`
314
+ );
315
+ }
316
+ const m = commandInput.models;
317
+ const models = m?.idPath?.trim() ? {
318
+ ...m.path?.trim() ? { path: m.path.trim() } : {},
319
+ idPath: m.idPath.trim(),
320
+ ...m.namePath?.trim() ? { namePath: m.namePath.trim() } : {},
321
+ ...m.descriptionPath?.trim() ? { descriptionPath: m.descriptionPath.trim() } : {},
322
+ ...m.contextLengthPath?.trim() ? { contextLengthPath: m.contextLengthPath.trim() } : {}
323
+ } : void 0;
324
+ return {
325
+ ...commandInput.command?.trim() ? { command: commandInput.command.trim() } : {},
326
+ argsTemplate: commandInput.argsTemplate.map((part) => String(part)),
327
+ parser: cmdParser,
328
+ ...models ? { models } : {}
329
+ };
330
+ })() : void 0;
331
+ assert(
332
+ explicitVia !== "command" || command !== void 0,
333
+ `CLI discovery via:"command" requires a discovery.command block for provider "${providerId}".`
334
+ );
335
+ const fallback = descriptor.cli.discovery.fallback;
336
+ if (fallback !== void 0) {
337
+ assert(
338
+ fallback === "static" || fallback === "none",
339
+ `Unsupported CLI discovery fallback "${String(fallback)}" for provider "${providerId}".`
340
+ );
341
+ }
291
342
  return {
292
- via,
293
- ...acp ? { acp } : {}
343
+ ...explicitVia !== void 0 ? { via: explicitVia } : {},
344
+ ...acp ? { acp } : {},
345
+ ...command ? { command } : {},
346
+ ...fallback !== void 0 ? { fallback } : {}
294
347
  };
295
348
  })();
296
349
  return {
@@ -1721,6 +1774,25 @@ function resolveModelContextWindow(input) {
1721
1774
  const fallback = isUsableContextLength(input.defaultContextWindow) ? input.defaultContextWindow : DEFAULT_CONTEXT_WINDOW;
1722
1775
  return { contextWindow: fallback, source: "default" };
1723
1776
  }
1777
+ function fillConfiguredContextLength(route, models) {
1778
+ if (!isUsableContextLength(route.contextWindow)) {
1779
+ return models;
1780
+ }
1781
+ const configured = route.contextWindow;
1782
+ return models.map((entry) => {
1783
+ if (isUsableContextLength(entry.contextLength)) {
1784
+ return entry;
1785
+ }
1786
+ if (route.model !== void 0 && entry.modelId !== route.model) {
1787
+ return entry;
1788
+ }
1789
+ return {
1790
+ ...entry,
1791
+ contextLength: configured,
1792
+ metadata: { ...entry.metadata ?? {}, contextWindowSource: "configured" }
1793
+ };
1794
+ });
1795
+ }
1724
1796
  function modelContextCacheKey(input) {
1725
1797
  if (typeof input === "string") {
1726
1798
  return { key: `::${input}`, model: input };
@@ -4696,18 +4768,19 @@ function canonicalGeminiImageModelId(modelId) {
4696
4768
  }
4697
4769
  function buildModelCatalog(route, availableModels, currentModelId) {
4698
4770
  const requestedModelId = route.model;
4699
- const requestedModelAdvertised = availableModels.some(
4771
+ const filledModels = fillConfiguredContextLength(route, availableModels);
4772
+ const requestedModelAdvertised = filledModels.some(
4700
4773
  (model) => model.modelId === requestedModelId
4701
4774
  );
4702
4775
  const canonicalModelId = route.provider === "gemini" ? canonicalGeminiImageModelId(requestedModelId) : void 0;
4703
- const resolvedModelId = requestedModelAdvertised ? requestedModelId : canonicalModelId && availableModels.some((model) => model.modelId === canonicalModelId) ? canonicalModelId : void 0;
4776
+ const resolvedModelId = requestedModelAdvertised ? requestedModelId : canonicalModelId && filledModels.some((model) => model.modelId === canonicalModelId) ? canonicalModelId : void 0;
4704
4777
  return {
4705
4778
  requestedModelId,
4706
4779
  requestedModelAdvertised,
4707
4780
  ...canonicalModelId ? { canonicalModelId } : {},
4708
4781
  ...resolvedModelId ? { resolvedModelId } : {},
4709
4782
  ...currentModelId ? { currentModelId } : {},
4710
- availableModels
4783
+ availableModels: filledModels
4711
4784
  };
4712
4785
  }
4713
4786
  function parseOpenAiModelCatalog(route, payload) {
@@ -7553,6 +7626,10 @@ var AcpConnection = class {
7553
7626
  `ACP route "${context.route.id}" did not return an ACP model catalog in session/new.`
7554
7627
  );
7555
7628
  }
7629
+ catalog.availableModels = fillConfiguredContextLength(
7630
+ context.route,
7631
+ catalog.availableModels
7632
+ );
7556
7633
  return catalog;
7557
7634
  } finally {
7558
7635
  this.bumpIdleTimer();
@@ -8629,14 +8706,26 @@ function normalizeCliDiscoveryAcpSource(route, discovery) {
8629
8706
  };
8630
8707
  }
8631
8708
  function resolveCliDiscoverySource(route) {
8632
- const via = route.transport.cli?.discovery?.via ?? (defaultCliDiscoveryTransportIdForRoute(route) ? "acp" : "none");
8633
- if (via === "none") {
8634
- return { via: "none" };
8709
+ const discovery = route.transport.cli?.discovery;
8710
+ const hasModels = (route.advertisedModels?.length ?? 0) > 0;
8711
+ const hasAcpDefault = Boolean(defaultCliDiscoveryTransportIdForRoute(route));
8712
+ const via = discovery?.via ?? (discovery?.command ? "command" : hasAcpDefault ? "acp" : hasModels ? "static" : "none");
8713
+ switch (via) {
8714
+ case "none":
8715
+ return { via: "none" };
8716
+ case "static":
8717
+ return { via: "static" };
8718
+ case "command": {
8719
+ const command = discovery?.command;
8720
+ if (!command) {
8721
+ return { via: "none" };
8722
+ }
8723
+ const fallback = discovery?.fallback ?? (hasModels ? "static" : "none");
8724
+ return { via: "command", command, fallback };
8725
+ }
8726
+ default:
8727
+ return normalizeCliDiscoveryAcpSource(route, discovery?.acp);
8635
8728
  }
8636
- return normalizeCliDiscoveryAcpSource(
8637
- route,
8638
- route.transport.cli?.discovery?.acp
8639
- );
8640
8729
  }
8641
8730
  function createCliDiscoveryAcpRoute(route) {
8642
8731
  const discovery = resolveCliDiscoverySource(route);
@@ -8745,6 +8834,42 @@ async function buildCliInvocation(context, options) {
8745
8834
  parameterKeys
8746
8835
  };
8747
8836
  }
8837
+ function buildCliDiscoveryInvocation(route, command, options) {
8838
+ const commandLine = command.command?.trim() ? command.command : resolveCliCommand(route, options);
8839
+ const resolved = splitCommandLine2(commandLine);
8840
+ const parameterKeys = [];
8841
+ const args = [
8842
+ ...resolved.args,
8843
+ ...command.argsTemplate.map((part) => {
8844
+ if (part === "{model}") {
8845
+ parameterKeys.push("model");
8846
+ return route.model;
8847
+ }
8848
+ if (part.startsWith("--")) {
8849
+ parameterKeys.push(part);
8850
+ }
8851
+ return part;
8852
+ })
8853
+ ];
8854
+ return {
8855
+ command: resolved.command,
8856
+ args,
8857
+ cwd: path2.resolve(options?.cwd ?? process.cwd()),
8858
+ env: buildCliEnvironment(options),
8859
+ parameterKeys
8860
+ };
8861
+ }
8862
+ function buildStaticCliCatalog(route) {
8863
+ const models = route.advertisedModels.map((id) => ({
8864
+ modelId: id,
8865
+ name: id
8866
+ }));
8867
+ return buildModelCatalog(
8868
+ route,
8869
+ models,
8870
+ currentModelIdForRoute(route, route.advertisedModels)
8871
+ );
8872
+ }
8748
8873
  function statsToUsage(stats) {
8749
8874
  if (!stats || typeof stats !== "object") {
8750
8875
  return void 0;
@@ -8782,6 +8907,9 @@ function getValueByPath(value, dotPath) {
8782
8907
  }
8783
8908
  return current;
8784
8909
  }
8910
+ function splitJsonlLines(stdout) {
8911
+ return stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
8912
+ }
8785
8913
  function normalizeErrorMessage(value) {
8786
8914
  if (typeof value === "string" && value.trim()) {
8787
8915
  return value.trim();
@@ -8829,7 +8957,7 @@ function parseGenericJsonCli(stdout, parser) {
8829
8957
  };
8830
8958
  }
8831
8959
  function parseGenericJsonlCli(stdout, parser) {
8832
- const entries = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
8960
+ const entries = splitJsonlLines(stdout);
8833
8961
  const errorValue = parser.error ? findJsonlSelection(entries, parser.error) : void 0;
8834
8962
  const errorMessage = normalizeErrorMessage(errorValue);
8835
8963
  if (errorMessage) {
@@ -8850,6 +8978,70 @@ function parseGenericJsonlCli(stdout, parser) {
8850
8978
  data: entries
8851
8979
  };
8852
8980
  }
8981
+ var ANSI_ESCAPE_PATTERN = /\[[0-?]*[ -/]*[@-~]/g;
8982
+ function parseTextCli(stdout, parser) {
8983
+ let text = stdout;
8984
+ if (parser.stripAnsi) {
8985
+ text = text.replace(ANSI_ESCAPE_PATTERN, "");
8986
+ }
8987
+ if (parser.trim !== false) {
8988
+ text = text.trim();
8989
+ }
8990
+ if (!text) {
8991
+ throw new AiConnectError(
8992
+ "temporary_unavailable",
8993
+ "CLI text parser produced no output."
8994
+ );
8995
+ }
8996
+ return { text, data: stdout };
8997
+ }
8998
+ function modelInfoFromRecord(record, selector) {
8999
+ const rawId = getValueByPath(record, selector.idPath);
9000
+ const modelId = typeof rawId === "string" ? rawId.trim() : "";
9001
+ if (!modelId) {
9002
+ return void 0;
9003
+ }
9004
+ const rawName = selector.namePath ? getValueByPath(record, selector.namePath) : void 0;
9005
+ const name = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : modelId;
9006
+ const rawDescription = selector.descriptionPath ? getValueByPath(record, selector.descriptionPath) : void 0;
9007
+ const description = typeof rawDescription === "string" && rawDescription.trim().length > 0 ? rawDescription : void 0;
9008
+ const rawContext = selector.contextLengthPath ? getValueByPath(record, selector.contextLengthPath) : void 0;
9009
+ const contextLength = typeof rawContext === "number" && Number.isFinite(rawContext) && rawContext > 0 ? Math.floor(rawContext) : void 0;
9010
+ return {
9011
+ modelId,
9012
+ name,
9013
+ ...description ? { description } : {},
9014
+ ...contextLength !== void 0 ? { contextLength } : {}
9015
+ };
9016
+ }
9017
+ function parseCliModelList(stdout, parser, selector) {
9018
+ if (parser.kind === "text") {
9019
+ let text = stdout;
9020
+ if (parser.stripAnsi) {
9021
+ text = text.replace(ANSI_ESCAPE_PATTERN, "");
9022
+ }
9023
+ return text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => ({ modelId: line, name: line }));
9024
+ }
9025
+ if (!selector) {
9026
+ throw new AiConnectError(
9027
+ "validation_error",
9028
+ `CLI ${parser.kind} discovery parser requires a models selector with idPath.`
9029
+ );
9030
+ }
9031
+ const records = parser.kind === "jsonl" ? splitJsonlLines(stdout) : (() => {
9032
+ const payload = JSON.parse(stdout);
9033
+ const arr = selector.path ? getValueByPath(payload, selector.path) : payload;
9034
+ return Array.isArray(arr) ? arr : [];
9035
+ })();
9036
+ const models = [];
9037
+ for (const record of records) {
9038
+ const info = modelInfoFromRecord(record, selector);
9039
+ if (info) {
9040
+ models.push(info);
9041
+ }
9042
+ }
9043
+ return models;
9044
+ }
8853
9045
  function parseGeminiCli(stdout) {
8854
9046
  const payload = JSON.parse(stdout);
8855
9047
  if (typeof payload.error === "string") {
@@ -8921,8 +9113,7 @@ function parseClaudeCli(stdout) {
8921
9113
  };
8922
9114
  }
8923
9115
  function parseCodexCli(stdout, outputFileContent) {
8924
- const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
8925
- const events = lines.map((line) => JSON.parse(line));
9116
+ const events = splitJsonlLines(stdout);
8926
9117
  let text = outputFileContent?.trim() || void 0;
8927
9118
  let usage;
8928
9119
  for (const event of events) {
@@ -8980,7 +9171,14 @@ function parseCliResult(route, result, outputFileContent) {
8980
9171
  `CLI route "${route.id}" declared a parser override but normalization did not preserve it.`
8981
9172
  );
8982
9173
  }
8983
- return parser.kind === "json" ? parseGenericJsonCli(stdout, parser) : parseGenericJsonlCli(stdout, parser);
9174
+ switch (parser.kind) {
9175
+ case "text":
9176
+ return parseTextCli(result.stdout, parser);
9177
+ case "json":
9178
+ return parseGenericJsonCli(stdout, parser);
9179
+ default:
9180
+ return parseGenericJsonlCli(stdout, parser);
9181
+ }
8984
9182
  }
8985
9183
  switch (cliOptions.preset) {
8986
9184
  case "gemini":
@@ -9083,6 +9281,88 @@ async function cleanupCliInvocation(invocation) {
9083
9281
  }
9084
9282
  function createCliTransportManager(options) {
9085
9283
  return {
9284
+ async discoverModels(context) {
9285
+ const source = resolveCliDiscoverySource(context.route);
9286
+ if (source.via === "none") {
9287
+ throw new AiConnectError(
9288
+ "not_supported",
9289
+ `CLI transport "${context.route.transport.id}" does not support model discovery (no list command, no ACP sidecar, and no configured models[]).`
9290
+ );
9291
+ }
9292
+ if (source.via === "acp") {
9293
+ throw new AiConnectError(
9294
+ "not_supported",
9295
+ `CLI route "${context.route.id}" resolves discovery via acp; dispatch to the acp discovery route instead.`
9296
+ );
9297
+ }
9298
+ if (source.via === "static") {
9299
+ return buildStaticCliCatalog(context.route);
9300
+ }
9301
+ const phases = [];
9302
+ const invocation = buildCliDiscoveryInvocation(
9303
+ context.route,
9304
+ source.command,
9305
+ options
9306
+ );
9307
+ context.telemetry?.captureTransport({
9308
+ protocol: "cli",
9309
+ endpoint: invocation.command,
9310
+ method: "process",
9311
+ bodyKeys: ["argv"],
9312
+ parameterKeys: invocation.parameterKeys,
9313
+ phases,
9314
+ stream: false
9315
+ });
9316
+ try {
9317
+ let models;
9318
+ try {
9319
+ const execution = await executeCliInvocation(
9320
+ invocation,
9321
+ options?.timeoutMs ?? 6e4,
9322
+ phases,
9323
+ context.abort.signal
9324
+ );
9325
+ if (execution.exitCode !== 0 || !execution.stdout.trim()) {
9326
+ throw new AiConnectError(
9327
+ "temporary_unavailable",
9328
+ execution.stderr.trim() || `CLI discovery command for "${context.route.transport.id}" exited with code ${execution.exitCode ?? "null"}.`
9329
+ );
9330
+ }
9331
+ models = parseCliModelList(
9332
+ execution.stdout,
9333
+ source.command.parser,
9334
+ source.command.models
9335
+ );
9336
+ } catch (error) {
9337
+ if (error instanceof AiConnectError && error.code === "aborted") {
9338
+ throw error;
9339
+ }
9340
+ if (source.fallback === "static") {
9341
+ return buildStaticCliCatalog(context.route);
9342
+ }
9343
+ throw error;
9344
+ }
9345
+ if (models.length === 0) {
9346
+ if (source.fallback === "static") {
9347
+ return buildStaticCliCatalog(context.route);
9348
+ }
9349
+ throw new AiConnectError(
9350
+ "temporary_unavailable",
9351
+ `CLI discovery command for "${context.route.transport.id}" returned no models.`
9352
+ );
9353
+ }
9354
+ return buildModelCatalog(
9355
+ context.route,
9356
+ models,
9357
+ currentModelIdForRoute(
9358
+ context.route,
9359
+ models.map((model) => model.modelId)
9360
+ )
9361
+ );
9362
+ } finally {
9363
+ await cleanupCliInvocation(invocation);
9364
+ }
9365
+ },
9086
9366
  async runPrompt(context) {
9087
9367
  const invocation = await buildCliInvocation(context, options);
9088
9368
  const phases = [];
@@ -9617,17 +9897,21 @@ function createLocalRouteHandlers(options = {}) {
9617
9897
  return cliTransport.runPrompt(context);
9618
9898
  },
9619
9899
  async discoverModels(context) {
9620
- const discoveryRoute = createCliDiscoveryAcpRoute(context.route);
9621
- if (!discoveryRoute) {
9622
- throw new AiConnectError(
9623
- "not_supported",
9624
- `CLI route "${context.route.id}" does not define a model discovery backend.`
9625
- );
9900
+ const source = resolveCliDiscoverySource(context.route);
9901
+ if (source.via === "acp") {
9902
+ const discoveryRoute = createCliDiscoveryAcpRoute(context.route);
9903
+ if (!discoveryRoute) {
9904
+ throw new AiConnectError(
9905
+ "not_supported",
9906
+ `CLI route "${context.route.id}" does not define a model discovery backend.`
9907
+ );
9908
+ }
9909
+ return acpTransport.discoverModels({
9910
+ ...context,
9911
+ route: discoveryRoute
9912
+ });
9626
9913
  }
9627
- return acpTransport.discoverModels({
9628
- ...context,
9629
- route: discoveryRoute
9630
- });
9914
+ return cliTransport.discoverModels(context);
9631
9915
  },
9632
9916
  async verify({ route, runtime }) {
9633
9917
  try {