@vedmalex/ai-connect 0.2.0 → 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/README.md +137 -67
- package/dist/browser/index.js +113 -17
- package/dist/browser/index.js.map +2 -2
- package/dist/bun/index.js +345 -38
- package/dist/bun/index.js.map +2 -2
- package/dist/bun/local.js +345 -38
- package/dist/bun/local.js.map +2 -2
- package/dist/node/index.js +345 -38
- package/dist/node/index.js.map +2 -2
- package/dist/node/local.js +345 -38
- package/dist/node/local.js.map +2 -2
- package/dist/types/acp.d.ts.map +1 -1
- package/dist/types/cli.d.ts +8 -1
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/default-handlers.d.ts +3 -1
- package/dist/types/default-handlers.d.ts.map +1 -1
- package/dist/types/local-handlers.d.ts.map +1 -1
- package/dist/types/model-reference.d.ts +19 -1
- package/dist/types/model-reference.d.ts.map +1 -1
- package/dist/types/types.d.ts +90 -4
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +3 -2
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
|
|
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
|
|
21
|
-
- `
|
|
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: "
|
|
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-
|
|
258
|
+
id: "my-codex-wrapper",
|
|
300
259
|
cli: {
|
|
301
|
-
preset: "
|
|
260
|
+
preset: "codex",
|
|
302
261
|
},
|
|
303
262
|
}
|
|
304
263
|
```
|
|
@@ -316,10 +275,6 @@ Known local presets now include:
|
|
|
316
275
|
- `anthropic:claude-cli`
|
|
317
276
|
- `openclaude:openclaude-cli`
|
|
318
277
|
- `anthropic:claude-code-acp`
|
|
319
|
-
- `gemini:gemini-cli`
|
|
320
|
-
- `gemini:gemini-acp`
|
|
321
|
-
- `qwen:qwen-cli`
|
|
322
|
-
- `qwen:qwen-acp`
|
|
323
278
|
- `opencode:opencode-server`
|
|
324
279
|
- `opencode:opencode-acp`
|
|
325
280
|
|
|
@@ -368,6 +323,76 @@ const client = createLocalClient(
|
|
|
368
323
|
);
|
|
369
324
|
```
|
|
370
325
|
|
|
326
|
+
The parser supports three kinds:
|
|
327
|
+
|
|
328
|
+
- `kind: "json"` — parse stdout as a single JSON object; read the answer from `textPath` (plus optional `usagePath` / `errorPath`).
|
|
329
|
+
- `kind: "jsonl"` — parse stdout as newline-delimited JSON; select the answer/usage/error lines with `{ path, wherePath, whereEquals }` selectors.
|
|
330
|
+
- `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:
|
|
331
|
+
- `trim` (default `true`) — trim leading/trailing whitespace.
|
|
332
|
+
- `stripAnsi` (default `false`) — strip ANSI escape sequences (spinner/color noise) before returning.
|
|
333
|
+
|
|
334
|
+
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`.
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { createLocalClient, defineConfig } from "@vedmalex/ai-connect";
|
|
338
|
+
|
|
339
|
+
// A custom print-mode coding-agent CLI ("agy") that writes the answer as raw text.
|
|
340
|
+
const client = createLocalClient(
|
|
341
|
+
defineConfig({
|
|
342
|
+
providers: {
|
|
343
|
+
agy: {
|
|
344
|
+
accounts: [
|
|
345
|
+
{
|
|
346
|
+
id: "local",
|
|
347
|
+
transport: {
|
|
348
|
+
kind: "cli",
|
|
349
|
+
id: "agy-cli",
|
|
350
|
+
command: "agy",
|
|
351
|
+
cli: {
|
|
352
|
+
argsTemplate: ["-p", "{prompt}", "--model", "{model}"],
|
|
353
|
+
parser: { kind: "text" }, // raw stdout -> result.text
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
models: ["default"],
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Another print-mode coding-agent CLI example — `pi` (invoked with `pi --print`; no ACP sidecar):
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
import { createLocalClient, defineConfig } from "@vedmalex/ai-connect";
|
|
369
|
+
|
|
370
|
+
const client = createLocalClient(
|
|
371
|
+
defineConfig({
|
|
372
|
+
providers: {
|
|
373
|
+
pi: {
|
|
374
|
+
accounts: [
|
|
375
|
+
{
|
|
376
|
+
id: "local",
|
|
377
|
+
transport: {
|
|
378
|
+
kind: "cli",
|
|
379
|
+
id: "pi-cli",
|
|
380
|
+
command: "pi",
|
|
381
|
+
cli: {
|
|
382
|
+
argsTemplate: ["--print", "--model", "{model}", "{prompt}"],
|
|
383
|
+
parser: { kind: "text" }, // raw stdout -> result.text
|
|
384
|
+
discovery: { via: "none" }, // no ACP sidecar for pi
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
models: ["gemini-3.1-pro-low", "gemini-3-flash"],
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
```
|
|
395
|
+
|
|
371
396
|
Supported placeholders in `argsTemplate`:
|
|
372
397
|
|
|
373
398
|
- `{prompt}`
|
|
@@ -376,9 +401,40 @@ Supported placeholders in `argsTemplate`:
|
|
|
376
401
|
|
|
377
402
|
`{output_file}` is useful for CLIs like `codex exec` that stream JSONL to stdout but write the final assistant message to a file.
|
|
378
403
|
|
|
404
|
+
### CLI Model Discovery
|
|
405
|
+
|
|
406
|
+
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`**:
|
|
407
|
+
|
|
408
|
+
- **`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):
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
transport: {
|
|
412
|
+
kind: "cli",
|
|
413
|
+
command: "agy",
|
|
414
|
+
cli: {
|
|
415
|
+
argsTemplate: ["-p", "{prompt}", "--model", "{model}"],
|
|
416
|
+
parser: { kind: "text" },
|
|
417
|
+
discovery: {
|
|
418
|
+
command: {
|
|
419
|
+
argsTemplate: ["models", "list", "--json"],
|
|
420
|
+
parser: { kind: "json" },
|
|
421
|
+
models: { path: "data", idPath: "id", namePath: "display_name", contextLengthPath: "context_window" },
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
models: ["agy-pro", { id: "agy-fast", contextWindow: 200_000 }],
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
- **`acp`** — delegate discovery to an ACP sidecar (the default for the built-in coding-agent presets).
|
|
430
|
+
- **`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" }`.
|
|
431
|
+
- **`none`** — no discovery.
|
|
432
|
+
|
|
433
|
+
**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.
|
|
434
|
+
|
|
379
435
|
Current local transport scope:
|
|
380
436
|
|
|
381
|
-
- `cli`: text generation, plus
|
|
437
|
+
- `cli`: text generation, plus model discovery via a list `command`, a `static` config catalog, or an ACP sidecar
|
|
382
438
|
- `server`: text generation plus provider-native model discovery
|
|
383
439
|
|
|
384
440
|
## Mock Gateway
|
|
@@ -933,7 +989,7 @@ When consuming `ai-connect` from `bs-search`:
|
|
|
933
989
|
| Context-window resolution | yes | yes | yes |
|
|
934
990
|
| Local file paths | no | yes | yes |
|
|
935
991
|
| Local command/session verification | no | yes | yes |
|
|
936
|
-
| Claude/Codex
|
|
992
|
+
| Claude/Codex ACP | no | yes | yes |
|
|
937
993
|
|
|
938
994
|
## Runtime Entry Points
|
|
939
995
|
|
|
@@ -1017,8 +1073,6 @@ Current discovery support matrix:
|
|
|
1017
1073
|
|
|
1018
1074
|
Built-in CLI discovery defaults:
|
|
1019
1075
|
|
|
1020
|
-
- `gemini-cli` -> `gemini-acp`
|
|
1021
|
-
- `qwen-cli` -> `qwen-acp`
|
|
1022
1076
|
- `claude-cli` -> `claude-code-acp`
|
|
1023
1077
|
- `codex-cli` -> `codex-acp`
|
|
1024
1078
|
- `openclaude-cli` -> no default discovery bridge
|
|
@@ -1029,22 +1083,38 @@ CLI discovery through ACP adds ACP-side prerequisites:
|
|
|
1029
1083
|
- the ACP harness must be authenticated if that provider requires auth
|
|
1030
1084
|
- `verify()` checks route plausibility and handler presence, but it does not perform a live discovery/auth handshake up front
|
|
1031
1085
|
|
|
1032
|
-
For custom CLI wrappers you can make the public API stay uniform by delegating discovery to ACP
|
|
1086
|
+
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
1087
|
|
|
1034
1088
|
```ts
|
|
1035
1089
|
transport: {
|
|
1036
1090
|
kind: "cli",
|
|
1037
|
-
id: "my-
|
|
1038
|
-
command: "/opt/bin/
|
|
1091
|
+
id: "my-codex-wrapper",
|
|
1092
|
+
command: "/opt/bin/codex-wrapper",
|
|
1039
1093
|
cli: {
|
|
1040
1094
|
discovery: {
|
|
1041
1095
|
via: "acp",
|
|
1042
1096
|
acp: {
|
|
1043
|
-
providerId: "
|
|
1044
|
-
transportId: "
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1097
|
+
providerId: "openai",
|
|
1098
|
+
transportId: "codex-acp",
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
}
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
Or a Claude wrapper delegating to `claude-code-acp`:
|
|
1106
|
+
|
|
1107
|
+
```ts
|
|
1108
|
+
transport: {
|
|
1109
|
+
kind: "cli",
|
|
1110
|
+
id: "my-claude-wrapper",
|
|
1111
|
+
command: "/opt/bin/claude-wrapper",
|
|
1112
|
+
cli: {
|
|
1113
|
+
discovery: {
|
|
1114
|
+
via: "acp",
|
|
1115
|
+
acp: {
|
|
1116
|
+
providerId: "anthropic",
|
|
1117
|
+
transportId: "claude-code-acp",
|
|
1048
1118
|
},
|
|
1049
1119
|
},
|
|
1050
1120
|
},
|
|
@@ -1156,9 +1226,7 @@ Server routes:
|
|
|
1156
1226
|
|
|
1157
1227
|
ACP usage statistics are exposed on `result.usage` when the harness provides them. `ai-connect` currently normalizes:
|
|
1158
1228
|
|
|
1159
|
-
- Gemini ACP `_meta.quota.token_count` and `_meta.quota.model_usage`
|
|
1160
1229
|
- OpenCode ACP `usage_update` (`used`, `size`, `cost`)
|
|
1161
|
-
- Qwen ACP `_meta.usage` (`inputTokens`, `outputTokens`, `totalTokens`, `thoughtTokens`, `cachedReadTokens`)
|
|
1162
1230
|
|
|
1163
1231
|
## Examples
|
|
1164
1232
|
|
|
@@ -1166,7 +1234,6 @@ See:
|
|
|
1166
1234
|
|
|
1167
1235
|
- [examples/acp-claude.ts](/Users/vedmalex/work/ai-connect/examples/acp-claude.ts)
|
|
1168
1236
|
- [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
1237
|
- [examples/browser-basic.ts](/Users/vedmalex/work/ai-connect/examples/browser-basic.ts)
|
|
1171
1238
|
- [examples/local-acp.ts](/Users/vedmalex/work/ai-connect/examples/local-acp.ts)
|
|
1172
1239
|
- [examples/local-test-server.ts](/Users/vedmalex/work/ai-connect/examples/local-test-server.ts)
|
|
@@ -1189,7 +1256,8 @@ If you are targeting the local gateway at `127.0.0.1:8045`, configure direct API
|
|
|
1189
1256
|
```ts
|
|
1190
1257
|
import { createLocalClient, defineConfig } from "@vedmalex/ai-connect";
|
|
1191
1258
|
|
|
1192
|
-
|
|
1259
|
+
// Read the local gateway key from the environment — never hardcode a key.
|
|
1260
|
+
const LOCAL_TEST_API_KEY = process.env.LOCAL_TEST_API_KEY ?? "";
|
|
1193
1261
|
|
|
1194
1262
|
const client = createLocalClient(
|
|
1195
1263
|
defineConfig({
|
|
@@ -1248,6 +1316,8 @@ console.log(
|
|
|
1248
1316
|
|
|
1249
1317
|
## Publishing
|
|
1250
1318
|
|
|
1319
|
+
> Full release runbook (OIDC trusted publishing, cutting a release, caveats): [`docs/publishing.md`](./docs/publishing.md).
|
|
1320
|
+
|
|
1251
1321
|
`@vedmalex/ai-connect` ships as a public scoped npm package. The package metadata enforces the publish boundary:
|
|
1252
1322
|
|
|
1253
1323
|
- `publishConfig.access` is `public`, which is required for a scoped name to publish without an explicit `--access public` flag.
|
package/dist/browser/index.js
CHANGED
|
@@ -219,11 +219,7 @@ function normalizeTransport(providerId, input) {
|
|
|
219
219
|
...selector.whereEquals !== void 0 ? { whereEquals: selector.whereEquals } : {}
|
|
220
220
|
};
|
|
221
221
|
};
|
|
222
|
-
const
|
|
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
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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 =
|
|
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 };
|
|
@@ -2172,6 +2244,7 @@ function normalizeResult(route, output, attempts) {
|
|
|
2172
2244
|
attempts,
|
|
2173
2245
|
...output.toolCalls ? { toolCalls: output.toolCalls } : {},
|
|
2174
2246
|
...output.text !== void 0 ? { text: output.text } : {},
|
|
2247
|
+
...output.reasoning !== void 0 ? { reasoning: output.reasoning } : {},
|
|
2175
2248
|
...output.data !== void 0 ? { data: output.data } : {},
|
|
2176
2249
|
...output.usage !== void 0 ? { usage: output.usage } : {}
|
|
2177
2250
|
};
|
|
@@ -4722,18 +4795,19 @@ function canonicalGeminiImageModelId(modelId) {
|
|
|
4722
4795
|
}
|
|
4723
4796
|
function buildModelCatalog(route, availableModels, currentModelId) {
|
|
4724
4797
|
const requestedModelId = route.model;
|
|
4725
|
-
const
|
|
4798
|
+
const filledModels = fillConfiguredContextLength(route, availableModels);
|
|
4799
|
+
const requestedModelAdvertised = filledModels.some(
|
|
4726
4800
|
(model) => model.modelId === requestedModelId
|
|
4727
4801
|
);
|
|
4728
4802
|
const canonicalModelId = route.provider === "gemini" ? canonicalGeminiImageModelId(requestedModelId) : void 0;
|
|
4729
|
-
const resolvedModelId = requestedModelAdvertised ? requestedModelId : canonicalModelId &&
|
|
4803
|
+
const resolvedModelId = requestedModelAdvertised ? requestedModelId : canonicalModelId && filledModels.some((model) => model.modelId === canonicalModelId) ? canonicalModelId : void 0;
|
|
4730
4804
|
return {
|
|
4731
4805
|
requestedModelId,
|
|
4732
4806
|
requestedModelAdvertised,
|
|
4733
4807
|
...canonicalModelId ? { canonicalModelId } : {},
|
|
4734
4808
|
...resolvedModelId ? { resolvedModelId } : {},
|
|
4735
4809
|
...currentModelId ? { currentModelId } : {},
|
|
4736
|
-
availableModels
|
|
4810
|
+
availableModels: filledModels
|
|
4737
4811
|
};
|
|
4738
4812
|
}
|
|
4739
4813
|
function parseOpenAiModelCatalog(route, payload) {
|
|
@@ -5548,6 +5622,9 @@ function extractText(value) {
|
|
|
5548
5622
|
return [part];
|
|
5549
5623
|
}
|
|
5550
5624
|
if (part && typeof part === "object") {
|
|
5625
|
+
if (part.thought === true) {
|
|
5626
|
+
return [];
|
|
5627
|
+
}
|
|
5551
5628
|
const candidate = part.text;
|
|
5552
5629
|
if (typeof candidate === "string") {
|
|
5553
5630
|
return [candidate];
|
|
@@ -5558,6 +5635,22 @@ function extractText(value) {
|
|
|
5558
5635
|
}
|
|
5559
5636
|
return "";
|
|
5560
5637
|
}
|
|
5638
|
+
function extractThoughts(value) {
|
|
5639
|
+
if (!Array.isArray(value)) {
|
|
5640
|
+
return void 0;
|
|
5641
|
+
}
|
|
5642
|
+
const thoughts = value.flatMap((part) => {
|
|
5643
|
+
if (part && typeof part === "object" && part.thought === true) {
|
|
5644
|
+
const candidate = part.text;
|
|
5645
|
+
if (typeof candidate === "string") {
|
|
5646
|
+
return [candidate];
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5649
|
+
return [];
|
|
5650
|
+
});
|
|
5651
|
+
const joined = thoughts.join("\n").trim();
|
|
5652
|
+
return joined.length > 0 ? joined : void 0;
|
|
5653
|
+
}
|
|
5561
5654
|
function imageAttachmentsFromPayload(payload, ...sources) {
|
|
5562
5655
|
return extractImagePayloads(payload, ...sources).map(
|
|
5563
5656
|
(item) => preparePortableFile(item)
|
|
@@ -6100,8 +6193,11 @@ async function runGemini(fetchImpl, context) {
|
|
|
6100
6193
|
throw classifyApiError(provider, response, payload);
|
|
6101
6194
|
}
|
|
6102
6195
|
const data = payload;
|
|
6196
|
+
const geminiThoughts = extractThoughts(data.candidates?.[0]?.content?.parts);
|
|
6103
6197
|
return {
|
|
6104
6198
|
text: extractText(data.candidates?.[0]?.content?.parts),
|
|
6199
|
+
// The model's `{ thought: true }` parts, separated from the answer (consumers route this to a thinking UI).
|
|
6200
|
+
...geminiThoughts ? { reasoning: geminiThoughts } : {},
|
|
6105
6201
|
data,
|
|
6106
6202
|
...geminiToolCallsFromPayload(data).length > 0 ? { toolCalls: geminiToolCallsFromPayload(data) } : {},
|
|
6107
6203
|
...(() => {
|