@vedmalex/ai-connect 0.2.1 → 0.5.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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  It models routes as `provider + transport + account + credential + model`, so one client can combine:
6
6
 
7
7
  - direct APIs for `OpenAI`, `Anthropic`, and `Gemini`
8
- - local-only ACP harness routes for `Claude Code`, `Codex`, and `Gemini`
8
+ - local-only ACP harness routes for `Claude Code` and `Codex`
9
9
  - key rotation, account rotation, cooldowns, retries, and fallback chains
10
10
  - portable file, PDF document, and image inputs across direct API and ACP paths
11
11
  - cooperative cancellation, pause-with-partial, and per-operation timeouts
@@ -17,8 +17,8 @@ It models routes as `provider + transport + account + credential + model`, so on
17
17
  Implemented today:
18
18
 
19
19
  - `OpenAI API`, `Anthropic API`, `Gemini API`
20
- - `Claude Code ACP`, `Codex ACP`, `Gemini ACP`
21
- - `Gemini CLI`, `Qwen CLI`, `Claude/OpenClaude CLI`, `Codex CLI`
20
+ - `Claude Code ACP`, `Codex ACP`
21
+ - `agy CLI`, `pi CLI`, `Claude/OpenClaude CLI`, `Codex CLI`
22
22
  - `OpenCode Server`
23
23
  - browser and local client factories
24
24
  - env-backed key pools with delimiter-based rotation
@@ -219,51 +219,11 @@ Dedicated provider-specific ACP examples:
219
219
 
220
220
  - [examples/acp-claude.ts](/Users/vedmalex/work/ai-connect/examples/acp-claude.ts)
221
221
  - [examples/acp-codex.ts](/Users/vedmalex/work/ai-connect/examples/acp-codex.ts)
222
- - [examples/acp-gemini.ts](/Users/vedmalex/work/ai-connect/examples/acp-gemini.ts)
223
-
224
222
  Dedicated `clientTools` examples:
225
223
 
226
224
  - [examples/browser-client-tools.ts](/Users/vedmalex/work/ai-connect/examples/browser-client-tools.ts)
227
225
  - [examples/local-client-tools.ts](/Users/vedmalex/work/ai-connect/examples/local-client-tools.ts)
228
226
 
229
- For Gemini ACP you can choose the harness auth mode per connection by creating separate accounts:
230
-
231
- ```ts
232
- const client = createLocalClient(
233
- defineConfig({
234
- providers: {
235
- gemini: {
236
- accounts: [
237
- {
238
- id: "gemini-default",
239
- transport: { kind: "acp", id: "gemini-acp" },
240
- models: ["auto-gemini-3"],
241
- },
242
- {
243
- id: "gemini-oauth",
244
- transport: {
245
- kind: "acp",
246
- id: "gemini-acp",
247
- auth: {
248
- methodId: "oauth-personal",
249
- },
250
- },
251
- models: ["auto-gemini-3"],
252
- },
253
- ],
254
- },
255
- },
256
- routing: {
257
- operations: {
258
- text: ["gemini:gemini-acp:gemini-oauth:auto-gemini-3"],
259
- },
260
- },
261
- }),
262
- );
263
- ```
264
-
265
- This keeps ACP harness auth explicit at connection selection time without injecting provider keys or `baseUrl` from `ai-connect`.
266
-
267
227
  ## Local CLI And Server Presets
268
228
 
269
229
  Built-in local transport presets are available both as catalog entries and as exported preset metadata:
@@ -279,7 +239,6 @@ import {
279
239
  const localCatalog = listTextProviderCatalog({ runtime: "local" });
280
240
  const codexCli = getTextTransportPresetById("openai", "codex-cli");
281
241
  const opencodeServer = AI_CONNECT_DEFAULT_SERVER_PRESETS.opencode;
282
- const geminiCli = AI_CONNECT_DEFAULT_CLI_PRESETS.gemini;
283
242
  ```
284
243
 
285
244
  For built-in CLI routes the shortest form is still the route `id`:
@@ -287,7 +246,7 @@ For built-in CLI routes the shortest form is still the route `id`:
287
246
  ```ts
288
247
  transport: {
289
248
  kind: "cli",
290
- id: "gemini-cli",
249
+ id: "codex-cli",
291
250
  }
292
251
  ```
293
252
 
@@ -296,9 +255,9 @@ If you want a custom route id but still want the built-in argv/parser/command de
296
255
  ```ts
297
256
  transport: {
298
257
  kind: "cli",
299
- id: "my-gemini-wrapper",
258
+ id: "my-codex-wrapper",
300
259
  cli: {
301
- preset: "gemini",
260
+ preset: "codex",
302
261
  },
303
262
  }
304
263
  ```
@@ -315,11 +274,8 @@ Known local presets now include:
315
274
  - `openai:codex-cli`
316
275
  - `anthropic:claude-cli`
317
276
  - `openclaude:openclaude-cli`
277
+ - `pi:pi-cli`
318
278
  - `anthropic:claude-code-acp`
319
- - `gemini:gemini-cli`
320
- - `gemini:gemini-acp`
321
- - `qwen:qwen-cli`
322
- - `qwen:qwen-acp`
323
279
  - `opencode:opencode-server`
324
280
  - `opencode:opencode-acp`
325
281
 
@@ -368,6 +324,70 @@ const client = createLocalClient(
368
324
  );
369
325
  ```
370
326
 
327
+ The parser supports three kinds:
328
+
329
+ - `kind: "json"` — parse stdout as a single JSON object; read the answer from `textPath` (plus optional `usagePath` / `errorPath`).
330
+ - `kind: "jsonl"` — parse stdout as newline-delimited JSON; select the answer/usage/error lines with `{ path, wherePath, whereEquals }` selectors.
331
+ - `kind: "text"` — treat stdout as **raw plain text** and return it as `result.text`. For **print-mode coding-agent CLIs that emit plain text, not JSON** (no `--output-format json` flag, no ACP mode). Options:
332
+ - `trim` (default `true`) — trim leading/trailing whitespace.
333
+ - `stripAnsi` (default `false`) — strip ANSI escape sequences (spinner/color noise) before returning.
334
+
335
+ Print-mode plain text carries no token information, so `result.usage` is absent (none is fabricated). An empty stdout on a non-zero exit still rejects with `temporary_unavailable`.
336
+
337
+ ```ts
338
+ import { createLocalClient, defineConfig } from "@vedmalex/ai-connect";
339
+
340
+ // A custom print-mode coding-agent CLI ("agy") that writes the answer as raw text.
341
+ const client = createLocalClient(
342
+ defineConfig({
343
+ providers: {
344
+ agy: {
345
+ accounts: [
346
+ {
347
+ id: "local",
348
+ transport: {
349
+ kind: "cli",
350
+ id: "agy-cli",
351
+ command: "agy",
352
+ cli: {
353
+ argsTemplate: ["-p", "{prompt}", "--model", "{model}"],
354
+ parser: { kind: "text" }, // raw stdout -> result.text
355
+ },
356
+ },
357
+ models: ["default"],
358
+ },
359
+ ],
360
+ },
361
+ },
362
+ }),
363
+ );
364
+ ```
365
+
366
+ `pi` has a built-in CLI preset (`pi-cli`). The preset supplies the default command (`pi`), argsTemplate (`["--print","--model","{model}","{prompt}"]`), parser (`kind: "text"`), and discovery (`via: "none"`). The minimal config is therefore:
367
+
368
+ ```ts
369
+ import { createLocalClient, defineConfig } from "@vedmalex/ai-connect";
370
+
371
+ const client = createLocalClient(
372
+ defineConfig({
373
+ providers: {
374
+ pi: {
375
+ accounts: [
376
+ {
377
+ id: "local",
378
+ transport: {
379
+ kind: "cli",
380
+ id: "pi-cli", // selects the built-in pi-cli preset
381
+ },
382
+ models: ["gemini-3.1-pro-low"],
383
+ },
384
+ ],
385
+ },
386
+ },
387
+ }),
388
+ );
389
+ ```
390
+
371
391
  Supported placeholders in `argsTemplate`:
372
392
 
373
393
  - `{prompt}`
@@ -376,9 +396,40 @@ Supported placeholders in `argsTemplate`:
376
396
 
377
397
  `{output_file}` is useful for CLIs like `codex exec` that stream JSONL to stdout but write the final assistant message to a file.
378
398
 
399
+ ### CLI Model Discovery
400
+
401
+ A `cli` route exposes the same management interfaces as other providers — `discoverModels` / `checkHealth` / `probeModels` / `listCandidateModels`. The discovery **source** is resolved from `transport.cli.discovery.via`; when `via` is omitted it is chosen by the chain **`command` → `acp` → `static` → `none`**:
402
+
403
+ - **`command`** — run a configured CLI sub-command that lists models and parse its stdout. Reuses the same `json | jsonl | text` formats; model fields are mapped with a `models` selector. Falls back to the static source on empty/failed output (`fallback: "static"` by default when `models[]` is present; set `fallback: "none"` to fail loud):
404
+
405
+ ```ts
406
+ transport: {
407
+ kind: "cli",
408
+ command: "agy",
409
+ cli: {
410
+ argsTemplate: ["-p", "{prompt}", "--model", "{model}"],
411
+ parser: { kind: "text" },
412
+ discovery: {
413
+ command: {
414
+ argsTemplate: ["models", "list", "--json"],
415
+ parser: { kind: "json" },
416
+ models: { path: "data", idPath: "id", namePath: "display_name", contextLengthPath: "context_window" },
417
+ },
418
+ },
419
+ },
420
+ },
421
+ models: ["agy-pro", { id: "agy-fast", contextWindow: 200_000 }],
422
+ ```
423
+
424
+ - **`acp`** — delegate discovery to an ACP sidecar (the default for the built-in coding-agent presets).
425
+ - **`static`** — build the catalog from the account's configured `models[]` (+ `contextWindow`). This is the default for a preset-less custom CLI that declares `models[]` (it previously reported `not_supported`); opt out with `discovery: { via: "none" }`.
426
+ - **`none`** — no discovery.
427
+
428
+ **Configured context window (GAP-A).** A model entry's `contextLength` is surfaced from the route's configured `contextWindow` only when discovery did not already report one (monotonic — a live discovered value always wins). Such entries are tagged `metadata.contextWindowSource: "configured"`. A consumer mapping `catalog.contextLength` into `resolveModelContextWindow`'s `discovered` slot should gate on `metadata.contextWindowSource !== "configured"` (otherwise feed it as the `configured` input) so the documented precedence `discovered > reference > configured > default` stays honest.
429
+
379
430
  Current local transport scope:
380
431
 
381
- - `cli`: text generation, plus optional model discovery via an ACP sidecar
432
+ - `cli`: text generation, plus model discovery via a list `command`, a `static` config catalog, or an ACP sidecar
382
433
  - `server`: text generation plus provider-native model discovery
383
434
 
384
435
  ## Mock Gateway
@@ -933,7 +984,7 @@ When consuming `ai-connect` from `bs-search`:
933
984
  | Context-window resolution | yes | yes | yes |
934
985
  | Local file paths | no | yes | yes |
935
986
  | Local command/session verification | no | yes | yes |
936
- | Claude/Codex/Gemini ACP | no | yes | yes |
987
+ | Claude/Codex ACP | no | yes | yes |
937
988
 
938
989
  ## Runtime Entry Points
939
990
 
@@ -1017,8 +1068,6 @@ Current discovery support matrix:
1017
1068
 
1018
1069
  Built-in CLI discovery defaults:
1019
1070
 
1020
- - `gemini-cli` -> `gemini-acp`
1021
- - `qwen-cli` -> `qwen-acp`
1022
1071
  - `claude-cli` -> `claude-code-acp`
1023
1072
  - `codex-cli` -> `codex-acp`
1024
1073
  - `openclaude-cli` -> no default discovery bridge
@@ -1029,22 +1078,38 @@ CLI discovery through ACP adds ACP-side prerequisites:
1029
1078
  - the ACP harness must be authenticated if that provider requires auth
1030
1079
  - `verify()` checks route plausibility and handler presence, but it does not perform a live discovery/auth handshake up front
1031
1080
 
1032
- For custom CLI wrappers you can make the public API stay uniform by delegating discovery to ACP:
1081
+ For custom CLI wrappers you can make the public API stay uniform by delegating discovery to an ACP sidecar. For example, a Codex wrapper that delegates to `codex-acp`:
1033
1082
 
1034
1083
  ```ts
1035
1084
  transport: {
1036
1085
  kind: "cli",
1037
- id: "my-gemini-wrapper",
1038
- command: "/opt/bin/gemini-wrapper",
1086
+ id: "my-codex-wrapper",
1087
+ command: "/opt/bin/codex-wrapper",
1039
1088
  cli: {
1040
1089
  discovery: {
1041
1090
  via: "acp",
1042
1091
  acp: {
1043
- providerId: "gemini",
1044
- transportId: "gemini-acp",
1045
- auth: {
1046
- methodId: "oauth-personal",
1047
- },
1092
+ providerId: "openai",
1093
+ transportId: "codex-acp",
1094
+ },
1095
+ },
1096
+ },
1097
+ }
1098
+ ```
1099
+
1100
+ Or a Claude wrapper delegating to `claude-code-acp`:
1101
+
1102
+ ```ts
1103
+ transport: {
1104
+ kind: "cli",
1105
+ id: "my-claude-wrapper",
1106
+ command: "/opt/bin/claude-wrapper",
1107
+ cli: {
1108
+ discovery: {
1109
+ via: "acp",
1110
+ acp: {
1111
+ providerId: "anthropic",
1112
+ transportId: "claude-code-acp",
1048
1113
  },
1049
1114
  },
1050
1115
  },
@@ -1156,9 +1221,7 @@ Server routes:
1156
1221
 
1157
1222
  ACP usage statistics are exposed on `result.usage` when the harness provides them. `ai-connect` currently normalizes:
1158
1223
 
1159
- - Gemini ACP `_meta.quota.token_count` and `_meta.quota.model_usage`
1160
1224
  - OpenCode ACP `usage_update` (`used`, `size`, `cost`)
1161
- - Qwen ACP `_meta.usage` (`inputTokens`, `outputTokens`, `totalTokens`, `thoughtTokens`, `cachedReadTokens`)
1162
1225
 
1163
1226
  ## Examples
1164
1227
 
@@ -1166,7 +1229,6 @@ See:
1166
1229
 
1167
1230
  - [examples/acp-claude.ts](/Users/vedmalex/work/ai-connect/examples/acp-claude.ts)
1168
1231
  - [examples/acp-codex.ts](/Users/vedmalex/work/ai-connect/examples/acp-codex.ts)
1169
- - [examples/acp-gemini.ts](/Users/vedmalex/work/ai-connect/examples/acp-gemini.ts)
1170
1232
  - [examples/browser-basic.ts](/Users/vedmalex/work/ai-connect/examples/browser-basic.ts)
1171
1233
  - [examples/local-acp.ts](/Users/vedmalex/work/ai-connect/examples/local-acp.ts)
1172
1234
  - [examples/local-test-server.ts](/Users/vedmalex/work/ai-connect/examples/local-test-server.ts)
@@ -1189,7 +1251,8 @@ If you are targeting the local gateway at `127.0.0.1:8045`, configure direct API
1189
1251
  ```ts
1190
1252
  import { createLocalClient, defineConfig } from "@vedmalex/ai-connect";
1191
1253
 
1192
- const LOCAL_TEST_API_KEY = "sk-8181e6a4a59b4ec5a9931f3ae0f359c4";
1254
+ // Read the local gateway key from the environment — never hardcode a key.
1255
+ const LOCAL_TEST_API_KEY = process.env.LOCAL_TEST_API_KEY ?? "";
1193
1256
 
1194
1257
  const client = createLocalClient(
1195
1258
  defineConfig({
@@ -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 {
@@ -330,7 +383,7 @@ function normalizeTransport(providerId, input) {
330
383
  };
331
384
  }
332
385
  if (descriptor.kind === "cli") {
333
- const inferredId2 = providerId === "anthropic" ? "claude-cli" : providerId === "openclaude" ? "openclaude-cli" : providerId === "openai" ? "codex-cli" : providerId === "gemini" ? "gemini-cli" : providerId === "qwen" ? "qwen-cli" : "cli";
386
+ const inferredId2 = providerId === "anthropic" ? "claude-cli" : providerId === "openclaude" ? "openclaude-cli" : providerId === "openai" ? "codex-cli" : providerId === "pi" ? "pi-cli" : "cli";
334
387
  return {
335
388
  kind: "cli",
336
389
  id: descriptor.id?.trim() || inferredId2,
@@ -347,7 +400,7 @@ function normalizeTransport(providerId, input) {
347
400
  ...descriptor.baseUrl?.trim() ? { baseUrl: descriptor.baseUrl.trim() } : {}
348
401
  };
349
402
  }
350
- const inferredId = providerId === "anthropic" ? "claude-code-acp" : providerId === "openai" ? "codex-acp" : providerId === "gemini" ? "gemini-acp" : "acp";
403
+ const inferredId = providerId === "anthropic" ? "claude-code-acp" : providerId === "openai" ? "codex-acp" : "acp";
351
404
  const normalizedLaunch = descriptor.launch ? (() => {
352
405
  const contextMode = descriptor.launch?.contextMode ?? "workspace";
353
406
  const skillsMode = descriptor.launch?.skillsMode ?? "default";
@@ -1585,18 +1638,14 @@ var MODEL_REFERENCE = {
1585
1638
  key: "gemini-3.1-flash-lite",
1586
1639
  contextLength: 1048576
1587
1640
  },
1641
+ "gemini-3.1-pro": { key: "gemini-3.1-pro", contextLength: 1048576 },
1588
1642
  "gemini-3-pro": { key: "gemini-3-pro", contextLength: 2097152 },
1589
1643
  "auto-gemini-3": { key: "auto-gemini-3", contextLength: 1048576 },
1590
1644
  "gemini-2.5-flash": { key: "gemini-2.5-flash", contextLength: 1048576 },
1591
1645
  "gemini-2.5-pro": { key: "gemini-2.5-pro", contextLength: 2097152 },
1592
1646
  "gemini-2.0-flash": { key: "gemini-2.0-flash", contextLength: 1048576 },
1593
1647
  "gemini-1.5-flash": { key: "gemini-1.5-flash", contextLength: 1048576 },
1594
- "gemini-1.5-pro": { key: "gemini-1.5-pro", contextLength: 2097152 },
1595
- // --- Qwen (catalog: qwen3-coder-plus) ---
1596
- "qwen3-coder-plus": { key: "qwen3-coder-plus", contextLength: 1048576 },
1597
- "qwen3-coder": { key: "qwen3-coder", contextLength: 262144 },
1598
- "qwen-max": { key: "qwen-max", contextLength: 32768 },
1599
- "qwen-plus": { key: "qwen-plus", contextLength: 131072 }
1648
+ "gemini-1.5-pro": { key: "gemini-1.5-pro", contextLength: 2097152 }
1600
1649
  };
1601
1650
  function normalizeModelKey(model) {
1602
1651
  let key = model.trim().toLowerCase();
@@ -1730,6 +1779,25 @@ function resolveModelContextWindow(input) {
1730
1779
  const fallback = isUsableContextLength(input.defaultContextWindow) ? input.defaultContextWindow : DEFAULT_CONTEXT_WINDOW;
1731
1780
  return { contextWindow: fallback, source: "default" };
1732
1781
  }
1782
+ function fillConfiguredContextLength(route, models) {
1783
+ if (!isUsableContextLength(route.contextWindow)) {
1784
+ return models;
1785
+ }
1786
+ const configured = route.contextWindow;
1787
+ return models.map((entry) => {
1788
+ if (isUsableContextLength(entry.contextLength)) {
1789
+ return entry;
1790
+ }
1791
+ if (route.model !== void 0 && entry.modelId !== route.model) {
1792
+ return entry;
1793
+ }
1794
+ return {
1795
+ ...entry,
1796
+ contextLength: configured,
1797
+ metadata: { ...entry.metadata ?? {}, contextWindowSource: "configured" }
1798
+ };
1799
+ });
1800
+ }
1733
1801
  function modelContextCacheKey(input) {
1734
1802
  if (typeof input === "string") {
1735
1803
  return { key: `::${input}`, model: input };
@@ -4723,18 +4791,19 @@ function canonicalGeminiImageModelId(modelId) {
4723
4791
  }
4724
4792
  function buildModelCatalog(route, availableModels, currentModelId) {
4725
4793
  const requestedModelId = route.model;
4726
- const requestedModelAdvertised = availableModels.some(
4794
+ const filledModels = fillConfiguredContextLength(route, availableModels);
4795
+ const requestedModelAdvertised = filledModels.some(
4727
4796
  (model) => model.modelId === requestedModelId
4728
4797
  );
4729
4798
  const canonicalModelId = route.provider === "gemini" ? canonicalGeminiImageModelId(requestedModelId) : void 0;
4730
- const resolvedModelId = requestedModelAdvertised ? requestedModelId : canonicalModelId && availableModels.some((model) => model.modelId === canonicalModelId) ? canonicalModelId : void 0;
4799
+ const resolvedModelId = requestedModelAdvertised ? requestedModelId : canonicalModelId && filledModels.some((model) => model.modelId === canonicalModelId) ? canonicalModelId : void 0;
4731
4800
  return {
4732
4801
  requestedModelId,
4733
4802
  requestedModelAdvertised,
4734
4803
  ...canonicalModelId ? { canonicalModelId } : {},
4735
4804
  ...resolvedModelId ? { resolvedModelId } : {},
4736
4805
  ...currentModelId ? { currentModelId } : {},
4737
- availableModels
4806
+ availableModels: filledModels
4738
4807
  };
4739
4808
  }
4740
4809
  function parseOpenAiModelCatalog(route, payload) {
@@ -6267,54 +6336,26 @@ function createDefaultRouteHandlers(options = {}) {
6267
6336
  var AI_CONNECT_DEFAULT_ACP_COMMANDS = {
6268
6337
  "anthropic:claude-code-acp": "npx -y @agentclientprotocol/claude-agent-acp@^0.25.0",
6269
6338
  "openai:codex-acp": "npx @zed-industries/codex-acp@^0.11.1",
6270
- "gemini:gemini-acp": "gemini --acp",
6271
- "qwen:qwen-acp": "qwen --acp",
6272
6339
  "opencode:opencode-acp": "opencode acp"
6273
6340
  };
6274
6341
 
6275
6342
  // src/cli-presets.ts
6276
6343
  var AI_CONNECT_DEFAULT_CLI_PRESETS = {
6277
- gemini: {
6278
- id: "gemini",
6279
- label: "Gemini CLI",
6280
- command: "gemini",
6281
- transportId: "gemini-cli",
6344
+ pi: {
6345
+ id: "pi",
6346
+ label: "pi (coding agent)",
6347
+ command: "pi",
6348
+ transportId: "pi-cli",
6282
6349
  options: {
6283
- preset: "gemini",
6284
- argsTemplate: ["-p", "{prompt}", "--output-format", "json", "--model", "{model}"],
6350
+ preset: "pi",
6351
+ // pi print mode emits the answer as plain text on stdout (no JSON wrapper),
6352
+ // and has no ACP mode — so it uses the raw `text` parser and no discovery sidecar.
6353
+ argsTemplate: ["--print", "--model", "{model}", "{prompt}"],
6285
6354
  discovery: {
6286
- via: "acp",
6287
- acp: {
6288
- transportId: "gemini-acp"
6289
- }
6290
- },
6291
- parser: {
6292
- kind: "json",
6293
- textPath: "response",
6294
- usagePath: "stats",
6295
- errorPath: "error"
6296
- }
6297
- }
6298
- },
6299
- qwen: {
6300
- id: "qwen",
6301
- label: "Qwen CLI",
6302
- command: "qwen",
6303
- transportId: "qwen-cli",
6304
- options: {
6305
- preset: "qwen",
6306
- argsTemplate: ["-p", "{prompt}", "--output-format", "json", "--model", "{model}"],
6307
- discovery: {
6308
- via: "acp",
6309
- acp: {
6310
- transportId: "qwen-acp"
6311
- }
6355
+ via: "none"
6312
6356
  },
6313
6357
  parser: {
6314
- kind: "json",
6315
- textPath: "__preset__:qwen.result",
6316
- usagePath: "__preset__:qwen.stats",
6317
- errorPath: "__preset__:qwen.error"
6358
+ kind: "text"
6318
6359
  }
6319
6360
  }
6320
6361
  },
@@ -6398,8 +6439,7 @@ var AI_CONNECT_DEFAULT_CLI_COMMANDS = {
6398
6439
  "anthropic:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
6399
6440
  "openclaude:openclaude-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.openclaude.command,
6400
6441
  "openai:codex-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.codex.command,
6401
- "gemini:gemini-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.gemini.command,
6402
- "qwen:qwen-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.qwen.command
6442
+ "pi:pi-cli": AI_CONNECT_DEFAULT_CLI_PRESETS.pi.command
6403
6443
  };
6404
6444
  function getCliTransportPreset(presetId) {
6405
6445
  return AI_CONNECT_DEFAULT_CLI_PRESETS[presetId];
@@ -6521,52 +6561,22 @@ var TEXT_PROVIDER_CATALOG = [
6521
6561
  runtime: "universal",
6522
6562
  defaultModel: "gemini-3.1-flash-lite",
6523
6563
  defaultBaseUrl: ""
6524
- },
6525
- {
6526
- providerId: "gemini",
6527
- providerLabel: "Gemini",
6528
- transportKind: "cli",
6529
- transportId: "gemini-cli",
6530
- transportLabel: "Gemini CLI",
6531
- runtime: "local",
6532
- defaultModel: "gemini-2.5-flash",
6533
- defaultCommand: AI_CONNECT_DEFAULT_CLI_COMMANDS["gemini:gemini-cli"]
6534
- },
6535
- {
6536
- providerId: "gemini",
6537
- providerLabel: "Gemini",
6538
- transportKind: "acp",
6539
- transportId: "gemini-acp",
6540
- transportLabel: "Gemini ACP",
6541
- runtime: "local",
6542
- defaultModel: "auto-gemini-3",
6543
- defaultCommand: AI_CONNECT_DEFAULT_ACP_COMMANDS["gemini:gemini-acp"]
6544
6564
  }
6545
6565
  ]
6546
6566
  },
6547
6567
  {
6548
- providerId: "qwen",
6549
- label: "Qwen",
6568
+ providerId: "pi",
6569
+ label: "pi",
6550
6570
  transports: [
6551
6571
  {
6552
- providerId: "qwen",
6553
- providerLabel: "Qwen",
6572
+ providerId: "pi",
6573
+ providerLabel: "pi",
6554
6574
  transportKind: "cli",
6555
- transportId: "qwen-cli",
6556
- transportLabel: "Qwen CLI",
6575
+ transportId: "pi-cli",
6576
+ transportLabel: "pi CLI",
6557
6577
  runtime: "local",
6558
- defaultModel: "qwen3-coder-plus",
6559
- defaultCommand: AI_CONNECT_DEFAULT_CLI_COMMANDS["qwen:qwen-cli"]
6560
- },
6561
- {
6562
- providerId: "qwen",
6563
- providerLabel: "Qwen",
6564
- transportKind: "acp",
6565
- transportId: "qwen-acp",
6566
- transportLabel: "Qwen ACP",
6567
- runtime: "local",
6568
- defaultModel: "default",
6569
- defaultCommand: AI_CONNECT_DEFAULT_ACP_COMMANDS["qwen:qwen-acp"]
6578
+ defaultModel: "gemini-3.1-pro-low",
6579
+ defaultCommand: AI_CONNECT_DEFAULT_CLI_COMMANDS["pi:pi-cli"]
6570
6580
  }
6571
6581
  ]
6572
6582
  },