omnirush 0.7.1 → 0.8.1

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.
Files changed (34) hide show
  1. package/assets/collect-once.ts +72 -141
  2. package/assets/extensions/omnirush/agents-lib.ts +23 -7
  3. package/assets/extensions/omnirush/agents.ts +13 -9
  4. package/assets/extensions/omnirush/capture/UPSTREAM +21 -0
  5. package/assets/extensions/omnirush/capture/collect-upload-budget.ts +39 -0
  6. package/assets/extensions/omnirush/capture/omnirush-swarm.ts +6 -0
  7. package/assets/extensions/omnirush/capture/server-fetch.ts +8 -0
  8. package/assets/extensions/omnirush/capture/session-archive/detect.ts +410 -0
  9. package/assets/extensions/omnirush/capture/session-archive/files.ts +85 -0
  10. package/assets/extensions/omnirush/capture/session-archive/ignored.ts +123 -0
  11. package/assets/extensions/omnirush/capture/session-archive/index.ts +1468 -0
  12. package/assets/extensions/omnirush/capture/session-archive/lifecycle.ts +606 -0
  13. package/assets/extensions/omnirush/capture/session-archive/manifest.ts +827 -0
  14. package/assets/extensions/omnirush/capture/session-archive/pack.ts +481 -0
  15. package/assets/extensions/omnirush/capture/session-archive/policy.ts +21 -0
  16. package/assets/extensions/omnirush/capture/session-archive/seal.ts +481 -0
  17. package/assets/extensions/omnirush/capture/session-archive/touched.ts +550 -0
  18. package/assets/extensions/omnirush/capture/session-archive/upload.ts +601 -0
  19. package/assets/extensions/omnirush/capture/turn-diff.ts +661 -0
  20. package/assets/extensions/omnirush/capture/vendor/minimatch.js +4 -0
  21. package/assets/extensions/omnirush/capture/vendor/zod.js +60 -0
  22. package/assets/extensions/omnirush/capture/workspace-collector.ts +5021 -0
  23. package/assets/extensions/omnirush/collector.ts +559 -171
  24. package/assets/extensions/omnirush/commands.ts +1 -1
  25. package/assets/extensions/omnirush/pi-engine.ts +666 -0
  26. package/assets/extensions/omnirush/plan.ts +6 -7
  27. package/assets/extensions/omnirush/sota.ts +11 -1
  28. package/assets/models.json +1 -1
  29. package/package.json +5 -2
  30. package/scripts/build-capture-vendor.js +55 -0
  31. package/src/bin.js +91 -15
  32. package/src/lib.js +73 -6
  33. package/assets/extensions/omnirush/collector-lib.ts +0 -2433
  34. package/assets/extensions/omnirush/trace-format.ts +0 -311
@@ -3,10 +3,10 @@
3
3
  // The model maintains an ordered task list (items with pending /
4
4
  // in-progress / done statuses) while it works. Every update:
5
5
  // - persists to <cwd>/__agent__/plan.json (the workspace collector
6
- // ships it, so buyers see how the agent structured the work),
7
- // - appends an `omnirush.plan` custom entry to the session JSONL —
8
- // trace-format.ts carries those as additive `plan_records` on the
9
- // trace artifact,
6
+ // ships it with the workspace snapshots),
7
+ // - appends an `omnirush.plan` custom entry to the session JSONL (the
8
+ // tool call itself, with the whole list, reaches the trace as a tool
9
+ // part of the session's engine messages),
10
10
  // - renders one line in the footer via ctx.ui.setStatus,
11
11
  // - returns the full list as the tool result.
12
12
  //
@@ -33,9 +33,8 @@ import {
33
33
  const PLAN_TOOL_NAME = "plan";
34
34
  const PLAN_ENTRY_TYPE = "omnirush.plan";
35
35
  const PLAN_STATUS_KEY = "omnirush-plan";
36
- // Same directory the collector journals under — it ships with the
37
- // workspace trace. (Mirrors collector-lib's AGENT_DIR; kept local so
38
- // this module stays independent of the collector's implementation.)
36
+ // The plan file's folder in the workspace (the CLI's earlier collector
37
+ // kept its journal there too).
39
38
  const AGENT_DIR = "__agent__";
40
39
 
41
40
  const PlanParams = Type.Object({
@@ -57,7 +57,7 @@ const STRICT_EXIT_CODE = 3;
57
57
  const CATALOG_MODELS = [
58
58
  {
59
59
  id: "gpt-6-astra",
60
- name: "GPT-6 Astra",
60
+ name: "GPT 6 Astra",
61
61
  api: "openai-responses",
62
62
  reasoning: true,
63
63
  input: ["text", "image"],
@@ -65,6 +65,16 @@ const CATALOG_MODELS = [
65
65
  contextWindow: 400000,
66
66
  compat: { supportsMaxOutputTokens: false },
67
67
  },
68
+ {
69
+ id: "gpt-6-sol",
70
+ name: "GPT 6 Sol",
71
+ api: "openai-responses",
72
+ reasoning: true,
73
+ input: ["text", "image"],
74
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
75
+ contextWindow: 400000,
76
+ compat: { supportsMaxOutputTokens: false },
77
+ },
68
78
  {
69
79
  id: "gpt-5.6-sol",
70
80
  name: "GPT-5.6 Sol",
@@ -8,7 +8,7 @@
8
8
  "models": [
9
9
  {
10
10
  "id": "gpt-6-astra",
11
- "name": "GPT-6 Astra",
11
+ "name": "GPT 6 Astra",
12
12
  "reasoning": true,
13
13
  "input": ["text", "image"],
14
14
  "cost": { "input": 10, "output": 50, "cacheRead": 2.5, "cacheWrite": 12.5 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnirush",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "Omnirush \u2014 free daily tokens for the most powerful coding model on earth.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -14,6 +14,7 @@
14
14
  "test": "node --test",
15
15
  "test:bun": "if [ -x .runtime/bin/bun ]; then .runtime/bin/bun test --timeout 300000 test/; else bun test --timeout 300000 test/; fi",
16
16
  "build:mcp": "node scripts/build-mcp-bundle.js",
17
+ "build:capture-vendor": "node scripts/build-capture-vendor.js",
17
18
  "postinstall": "node scripts/patch-pi-branding.js && node scripts/postinstall.js"
18
19
  },
19
20
  "dependencies": {
@@ -36,6 +37,8 @@
36
37
  ],
37
38
  "devDependencies": {
38
39
  "@modelcontextprotocol/sdk": "^1.30.0",
39
- "esbuild": "^0.28.2"
40
+ "esbuild": "^0.28.2",
41
+ "minimatch": "^10.2.6",
42
+ "zod": "^4.6.5"
40
43
  }
41
44
  }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ // Bundle the two npm libraries the ported capture modules use (zod for the
3
+ // project archive's state/queue/API schemas, minimatch for the .gitignore
4
+ // fallback matcher) into self-contained ESM files committed at
5
+ // assets/extensions/omnirush/capture/vendor/.
6
+ //
7
+ // Same reason as the MCP bundle (scripts/build-mcp-bundle.js): the extension
8
+ // directory is copied to ~/.pi/agent/extensions/omnirush/ where there is no
9
+ // npm resolution, and the published package ships no node_modules. The
10
+ // versions are the devDependencies in package.json, kept in step with the
11
+ // desktop app's (omnirush-gui apps/server/package.json). Regenerate with:
12
+ //
13
+ // npm run build:capture-vendor
14
+
15
+ import { build } from "esbuild";
16
+ import { readFileSync } from "node:fs";
17
+ import path from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
21
+ const outdir = path.join(root, "assets", "extensions", "omnirush", "capture", "vendor");
22
+ const version = (name) => JSON.parse(readFileSync(path.join(root, "node_modules", name, "package.json"), "utf8")).version;
23
+
24
+ for (const [name, source] of [
25
+ // Only the schema builders the archive modules call (tree-shaken: the whole
26
+ // zod namespace with its locales is ~450 KiB minified, this is ~95 KiB).
27
+ // `z.infer` is type-only and never reaches the runtime.
28
+ [
29
+ "zod",
30
+ 'import { object, string, number, literal, enum as zenum, array, record, boolean } from "zod";\n' +
31
+ "export const z = { object, string, number, literal, enum: zenum, array, record, boolean };",
32
+ ],
33
+ ["minimatch", 'export { minimatch } from "minimatch";'],
34
+ ]) {
35
+ await build({
36
+ stdin: { contents: source, resolveDir: root, loader: "js" },
37
+ outfile: path.join(outdir, `${name}.js`),
38
+ bundle: true,
39
+ platform: "node",
40
+ format: "esm",
41
+ target: "node18",
42
+ external: ["node:*"],
43
+ minify: true,
44
+ sourcemap: false,
45
+ legalComments: "none",
46
+ logLevel: "info",
47
+ banner: {
48
+ js: [
49
+ "// GENERATED FILE — do not edit by hand.",
50
+ "// Regenerate with: npm run build:capture-vendor",
51
+ `// Bundled from ${name}@${version(name)} (devDependency) via esbuild.`,
52
+ ].join("\n"),
53
+ },
54
+ });
55
+ }
package/src/bin.js CHANGED
@@ -18,12 +18,14 @@
18
18
  // OMNIRUSH_GATEWAY_URL provider base URL override ({origin}/v1)
19
19
  // OMNIRUSH_DIR state dir override (default ~/.omnirush)
20
20
  // OMNIRUSH_MODEL model override for plain runs (default
21
- // gpt-6-astra; gpt-5.6-sol also available; user
21
+ // gpt-6-astra; gpt-6-sol, gpt-5.6-sol and the Muse
22
+ // models also available; user
22
23
  // --model still wins)
23
24
  //
24
25
  // Agent runs default to `--provider omnirush --model gpt-6-astra`; the
25
- // catalog also carries gpt-5.6-sol (`--model gpt-5.6-sol` or
26
- // OMNIRUSH_MODEL=gpt-5.6-sol). The sota extension watches every response,
26
+ // catalog also carries gpt-6-sol, gpt-5.6-sol, meta-muse-spark and
27
+ // muse-spark-1.1 (`--model gpt-6-sol` or OMNIRUSH_MODEL=gpt-6-sol), refreshed
28
+ // from the gateway's GET /v1/models at launch. The sota extension watches every response,
27
29
  // refreshes the device token single-flight on 401 (retry once), and
28
30
  // warns on stderr if the gateway serves a different model than
29
31
  // requested; `--strict-sota` makes that violation fatal (exit 3).
@@ -39,6 +41,7 @@ import { fileURLToPath } from "node:url";
39
41
 
40
42
  import {
41
43
  DEFAULT_ORIGIN,
44
+ catalogFromGateway,
42
45
  modelsConfig,
43
46
  resolveGatewayUrl,
44
47
  withModelDefaults,
@@ -147,7 +150,53 @@ function installDefaultThinkingLevel() {
147
150
  // Merge our provider into the user's models.json without clobbering
148
151
  // unrelated providers. Ours is authoritative: this file is how the
149
152
  // product ships its provider. Other providers untouched.
150
- function installModelsJson(gatewayUrl) {
153
+ /**
154
+ * The model list for this run, as the desktop app gets it: the gateway's
155
+ * catalog (GET <gateway>/models, bounded to a couple of seconds), cached in
156
+ * the state dir so an offline start still shows the last list; the shipped
157
+ * list (src/lib.js MODELS) when neither is available. New models then
158
+ * appear without a CLI release. OMNIRUSH_STATIC_MODELS=1 skips the fetch.
159
+ */
160
+ async function launchModels(gatewayUrl, accessToken) {
161
+ const cacheFile = path.join(OMNI_DIR, "model-catalog.json");
162
+ const base = String(gatewayUrl).replace(/\/+$/, "");
163
+ if (process.env.OMNIRUSH_STATIC_MODELS !== "1" && accessToken) {
164
+ try {
165
+ const response = await fetch(`${base}/models`, {
166
+ headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
167
+ signal: AbortSignal.timeout(2500),
168
+ });
169
+ if (response.ok) {
170
+ const payload = await response.json();
171
+ const models = catalogFromGateway(payload);
172
+ if (models) {
173
+ try {
174
+ fs.writeFileSync(cacheFile, JSON.stringify({ gateway: base, fetched_at: new Date().toISOString(), payload }), { mode: 0o600 });
175
+ } catch {
176
+ /* the cache is a convenience */
177
+ }
178
+ return models;
179
+ }
180
+ } else {
181
+ await response.body?.cancel().catch(() => undefined);
182
+ }
183
+ } catch {
184
+ /* offline, slow or refused: the cached or shipped list */
185
+ }
186
+ }
187
+ try {
188
+ const cached = JSON.parse(fs.readFileSync(cacheFile, "utf8"));
189
+ if (cached && cached.gateway === base) {
190
+ const models = catalogFromGateway(cached.payload);
191
+ if (models) return models;
192
+ }
193
+ } catch {
194
+ /* no cache yet */
195
+ }
196
+ return undefined;
197
+ }
198
+
199
+ function installModelsJson(gatewayUrl, models) {
151
200
  let current = {};
152
201
  try {
153
202
  current = JSON.parse(fs.readFileSync(PI_MODELS_JSON, "utf8"));
@@ -157,18 +206,30 @@ function installModelsJson(gatewayUrl) {
157
206
  if (typeof current !== "object" || current === null) current = {};
158
207
  current.providers = {
159
208
  ...(current.providers || {}),
160
- ...modelsConfig({ gatewayUrl }).providers,
209
+ ...modelsConfig({ gatewayUrl, ...(models ? { models } : {}) }).providers,
161
210
  };
162
211
  fs.writeFileSync(PI_MODELS_JSON, JSON.stringify(current, null, 2) + "\n");
163
212
  }
164
213
 
165
214
  function installExtensions() {
166
215
  const src = path.join(__dirname, "..", "assets", "extensions", "omnirush");
167
- for (const f of fs.readdirSync(src)) {
168
- // pi loads extensions/<dir>/index.ts only; that aggregator imports the
169
- // sibling .ts modules and the shared auth.js — copy all loadable files.
170
- if (!f.endsWith(".ts") && !f.endsWith(".js")) continue;
171
- fs.copyFileSync(path.join(src, f), path.join(PI_EXT_DIR, f));
216
+ // pi loads extensions/<dir>/index.ts only; that aggregator imports the
217
+ // sibling .ts modules, the shared auth.js and the capture/ modules (the
218
+ // desktop's collector and project archive, in subfolders) — copy every
219
+ // loadable file, keeping the folder layout.
220
+ const copy = (from, to) => {
221
+ fs.mkdirSync(to, { recursive: true });
222
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
223
+ if (entry.isDirectory()) copy(path.join(from, entry.name), path.join(to, entry.name));
224
+ else if (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) {
225
+ fs.copyFileSync(path.join(from, entry.name), path.join(to, entry.name));
226
+ }
227
+ }
228
+ };
229
+ copy(src, PI_EXT_DIR);
230
+ // Modules of the earlier collector: nothing imports them any more.
231
+ for (const stale of ["collector-lib.ts", "trace-format.ts"]) {
232
+ fs.rmSync(path.join(PI_EXT_DIR, stale), { force: true });
172
233
  }
173
234
  }
174
235
 
@@ -493,7 +554,7 @@ async function cmdDoctor() {
493
554
  /* update check is advisory only */
494
555
  }
495
556
 
496
- installModelsJson(gateway);
557
+ installModelsJson(gateway, await launchModels(gateway, null));
497
558
  installExtensions();
498
559
  try {
499
560
  installManagedTools({ agentDir: PI_AGENT_DIR, runtimeRoot: path.resolve(__dirname, "..", ".runtime") });
@@ -621,6 +682,17 @@ function bunVersionOf(bunPath) {
621
682
  }
622
683
  }
623
684
 
685
+ /** The agent core release the launcher pins (its package.json reports ours after the branding patch). */
686
+ function engineVersion() {
687
+ try {
688
+ const pinned = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"))
689
+ .dependencies?.["@earendil-works/pi-coding-agent"];
690
+ return typeof pinned === "string" ? pinned.replace(/^[^\d]*/, "") : "";
691
+ } catch {
692
+ return "";
693
+ }
694
+ }
695
+
624
696
  function ownVersion() {
625
697
  try {
626
698
  return JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version;
@@ -642,6 +714,8 @@ function childEnv(auth) {
642
714
  // /status shows the CLI version; extensions have no package.json of
643
715
  // their own in ~/.pi, so the launcher hands it over.
644
716
  OMNIRUSH_VERSION: ownVersion(),
717
+ // The collector reports it as the environment's engine_version.
718
+ OMNIRUSH_ENGINE_VERSION: engineVersion(),
645
719
  // The agent resolves its config dir from <APP>_CODING_AGENT_DIR. The
646
720
  // branding patch renames the app to omnirush, so set the omnirush
647
721
  // spelling; the pi spelling stays for unpatched cores.
@@ -707,7 +781,7 @@ function spawnAgent(cmd, args, opts) {
707
781
  async function cmdRun() {
708
782
  ensureDirs();
709
783
  const auth = requireAuth();
710
- installModelsJson(agentGatewayUrl(auth));
784
+ installModelsJson(agentGatewayUrl(auth), await launchModels(agentGatewayUrl(auth), auth.accessToken));
711
785
  installExtensions();
712
786
  const entry = piEntry();
713
787
  const { cmd, args } = await runtimeFor(entry);
@@ -749,7 +823,8 @@ Usage:
749
823
  omnirush --version show the omnirush version
750
824
 
751
825
  Options:
752
- --model <model[:effort]> model override (gpt-6-astra, gpt-5.6-sol;
826
+ --model <model[:effort]> model override (gpt-6-astra, gpt-6-sol,
827
+ gpt-5.6-sol, meta-muse-spark, muse-spark-1.1;
753
828
  effort: minimal..max)
754
829
  --thinking <level> reasoning effort override
755
830
  --strict-sota exit non-zero if the served model is not
@@ -767,7 +842,8 @@ Session commands (inside the agent):
767
842
 
768
843
  Environment:
769
844
  OMNIRUSH_ORIGIN manager origin override
770
- OMNIRUSH_MODEL default model override (e.g. gpt-5.6-sol)
845
+ OMNIRUSH_MODEL default model override (e.g. gpt-6-sol)
846
+ OMNIRUSH_STATIC_MODELS=1 use the shipped model list (no GET /v1/models)
771
847
  OMNIRUSH_DIR state dir override (default ~/.omnirush)
772
848
  OMNIRUSH_DEBUG=1 verbose diagnostics (raw gateway errors on failure)
773
849
 
@@ -791,7 +867,7 @@ else {
791
867
  // duplicated and pi admin subcommands pass through untouched.
792
868
  ensureDirs();
793
869
  const auth = requireAuth();
794
- installModelsJson(agentGatewayUrl(auth));
870
+ installModelsJson(agentGatewayUrl(auth), await launchModels(agentGatewayUrl(auth), auth.accessToken));
795
871
  installExtensions();
796
872
  const entry = piEntry();
797
873
  const { cmd: bin, args } = await runtimeFor(entry);
package/src/lib.js CHANGED
@@ -68,7 +68,7 @@ const VALUE_FLAGS = new Set([
68
68
  /**
69
69
  * Model defaults for plain `omnirush` runs. OMNIRUSH_MODEL overrides the
70
70
  * default model — any catalog id below, optionally with a thinking level
71
- * like "gpt-6-astra:high" or "gpt-5.6-sol:xhigh".
71
+ * like "gpt-6-astra:high" or "gpt-6-sol:xhigh".
72
72
  */
73
73
  export function resolveModelDefaults(env = process.env) {
74
74
  return {
@@ -77,8 +77,11 @@ export function resolveModelDefaults(env = process.env) {
77
77
  };
78
78
  }
79
79
 
80
- // The product's shipped catalog: gpt-6-astra (default), gpt-5.6-sol and
81
- // the Muse models. Live-verified 2026-09-22 against the production
80
+ // The product's shipped catalog, in the desktop app's order (the backend
81
+ // catalog, GET /v1/models): gpt-6-astra (default), gpt-6-sol, gpt-5.6-sol,
82
+ // then the Muse models. gpt-6-sol added 2026-09-26 (the gateway serves it
83
+ // as gpt-6-sol). At launch the list is refreshed from GET /v1/models
84
+ // (catalogFromGateway); this one is the fallback. Live-verified 2026-09-22 against the production
82
85
  // gateway: GET /v1/models lists both ids and POST /v1/responses with
83
86
  // model "gpt-5.6-sol" is served as gpt-5.6-sol (x-omnirush-model
84
87
  // response header + the model named in every SSE frame), so the client
@@ -87,7 +90,7 @@ export function resolveModelDefaults(env = process.env) {
87
90
  export const MODELS = [
88
91
  {
89
92
  id: "gpt-6-astra",
90
- name: "GPT-6 Astra",
93
+ name: "GPT 6 Astra",
91
94
  reasoning: true,
92
95
  input: ["text", "image"],
93
96
  cost: { input: 10, output: 50, cacheRead: 2.5, cacheWrite: 12.5 },
@@ -105,6 +108,25 @@ export const MODELS = [
105
108
  max: "max",
106
109
  },
107
110
  },
111
+ {
112
+ id: "gpt-6-sol",
113
+ name: "GPT 6 Sol",
114
+ reasoning: true,
115
+ input: ["text", "image"],
116
+ // No published list pricing: pi shows zeros (as for gpt-5.6-sol).
117
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
118
+ contextWindow: 400000,
119
+ compat: { supportsMaxOutputTokens: false },
120
+ // Same Codex route as astra: the gateway accepts every effort level.
121
+ thinkingLevelMap: {
122
+ minimal: "minimal",
123
+ low: "low",
124
+ medium: "medium",
125
+ high: "high",
126
+ xhigh: "xhigh",
127
+ max: "max",
128
+ },
129
+ },
108
130
  {
109
131
  id: "gpt-5.6-sol",
110
132
  name: "GPT-5.6 Sol",
@@ -205,6 +227,51 @@ export const MODELS = [
205
227
  },
206
228
  ];
207
229
 
230
+ /** Every effort the gateway accepts from a client (backend catalog ACCEPTED_EFFORTS, pi spellings). */
231
+ const GATEWAY_EFFORTS = ["minimal", "low", "medium", "high", "xhigh", "max"];
232
+
233
+ /**
234
+ * The model list from the gateway's catalog (GET /v1/models, the list the
235
+ * desktop app shows), in its order, for pi's models.json: a model the CLI
236
+ * ships keeps its entry (pricing, effort map) under the gateway's display
237
+ * name; a model it does not know gets a generic entry from the catalog's
238
+ * fields (context limit, image input, reasoning). Shipped models the
239
+ * gateway does not list (sub-agent-only Muse ids) follow, so
240
+ * `spawn_agents` can still name them. Null when the payload is not a
241
+ * usable catalog: the caller keeps the shipped list.
242
+ */
243
+ export function catalogFromGateway(payload, shipped = MODELS) {
244
+ const data = payload && Array.isArray(payload.data) ? payload.data : null;
245
+ if (!data) return null;
246
+ const known = new Map(shipped.map((model) => [model.id, model]));
247
+ const listed = [];
248
+ for (const entry of data) {
249
+ const id = entry && typeof entry.id === "string" ? entry.id.trim() : "";
250
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id) || listed.some((model) => model.id === id)) continue;
251
+ const name = typeof entry.display_name === "string" && entry.display_name.trim() ? entry.display_name.trim() : null;
252
+ const base = known.get(id);
253
+ if (base) {
254
+ listed.push({ ...base, ...(name ? { name } : {}) });
255
+ continue;
256
+ }
257
+ const caps = entry.capabilities && typeof entry.capabilities === "object" ? entry.capabilities : {};
258
+ const context = entry.limits && Number.isSafeInteger(entry.limits.context) && entry.limits.context > 0 ? entry.limits.context : 400000;
259
+ const reasoning = caps.reasoning !== false;
260
+ listed.push({
261
+ id,
262
+ name: name ?? id,
263
+ reasoning,
264
+ input: caps.image_input === false ? ["text"] : ["text", "image"],
265
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
266
+ contextWindow: context,
267
+ compat: { supportsMaxOutputTokens: false },
268
+ ...(reasoning ? { thinkingLevelMap: Object.fromEntries(GATEWAY_EFFORTS.map((level) => [level, level])) } : {}),
269
+ });
270
+ }
271
+ if (listed.length === 0) return null;
272
+ return [...listed, ...shipped.filter((model) => !listed.some((entry) => entry.id === model.id)).map((model) => ({ ...model }))];
273
+ }
274
+
208
275
  /**
209
276
  * Prepend `--provider`/`--model` defaults for agent runs, without
210
277
  * duplicating flags the user already specified. Consumption rules mirror
@@ -284,7 +351,7 @@ export function withModelDefaults(args, defaults) {
284
351
  * `compat.supportsMaxOutputTokens: false` and ships no maxTokens — pi would
285
352
  * otherwise clamp a default and break every request.
286
353
  */
287
- export function modelsConfig({ gatewayUrl } = {}) {
354
+ export function modelsConfig({ gatewayUrl, models = MODELS } = {}) {
288
355
  return {
289
356
  providers: {
290
357
  [DEFAULT_PROVIDER]: {
@@ -292,7 +359,7 @@ export function modelsConfig({ gatewayUrl } = {}) {
292
359
  baseUrl: gatewayUrl ?? DEFAULT_GATEWAY_URL,
293
360
  api: "openai-responses",
294
361
  apiKey: "$OMNIRUSH_TOKEN",
295
- models: MODELS.map((model) => ({ ...model })),
362
+ models: models.map((model) => ({ ...model })),
296
363
  },
297
364
  },
298
365
  };