@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 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
  ```
@@ -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 optional model discovery via an ACP sidecar
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/Gemini ACP | no | yes | yes |
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-gemini-wrapper",
1038
- command: "/opt/bin/gemini-wrapper",
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: "gemini",
1044
- transportId: "gemini-acp",
1045
- auth: {
1046
- methodId: "oauth-personal",
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
- const LOCAL_TEST_API_KEY = "sk-8181e6a4a59b4ec5a9931f3ae0f359c4";
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.
@@ -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 };
@@ -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 requestedModelAdvertised = availableModels.some(
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 && availableModels.some((model) => model.modelId === canonicalModelId) ? canonicalModelId : void 0;
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
  ...(() => {