@elvatis_com/openclaw-cli-bridge-elvatis 1.9.1 → 2.0.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.
@@ -1,55 +1,58 @@
1
1
  # STATUS — openclaw-cli-bridge-elvatis
2
2
 
3
- ## Current Version: 1.7.3
3
+ ## Current Version: 1.9.2
4
4
 
5
- ## All 4 Providers Available persistent Chromium profiles
6
- | Provider | Status | Models | Login Cmd | Cookie Expiry |
7
- |---|---|---|---|---|
8
- | Grok | ✅ | web-grok/grok-3, grok-3-fast, grok-3-mini, grok-3-mini-fast | /grok-login | ~178d |
9
- | Gemini | | web-gemini/gemini-2-5-pro, gemini-2-5-flash, gemini-3-pro, gemini-3-flash | /gemini-login | ~398d |
10
- | Claude.ai | | web-claude/claude-sonnet, claude-opus, claude-haiku | /claude-login | ~364d |
11
- | ChatGPT | ✅ | web-chatgpt/gpt-4o, gpt-4o-mini, gpt-4.1, o3, o4-mini, gpt-5, gpt-5-mini | /chatgpt-login | ~6d (re-run /chatgpt-login to refresh) |
5
+ - **npm:** @elvatis_com/openclaw-cli-bridge-elvatis (not yet published to npm)
6
+ - **ClawHub:** openclaw-cli-bridge-elvatis@1.9.2
7
+ - **GitHub:** https://github.com/elvatis/openclaw-cli-bridge-elvatis/releases/tag/v1.9.2
8
+
9
+ ## CLI Model Token Limits (corrected in v1.9.2)
10
+ | Model | Context Window | Max Output |
11
+ |---|---|---|
12
+ | Claude Opus 4.6 (CLI) | 1,000,000 | 128,000 |
13
+ | Claude Sonnet 4.6 (CLI) | 1,000,000 | 64,000 |
14
+ | Claude Haiku 4.5 (CLI) | 200,000 | 64,000 |
15
+ | Gemini 2.5 Pro (CLI) | 1,048,576 | 65,535 |
16
+ | Gemini 2.5 Flash (CLI) | 1,048,576 | 65,535 |
17
+ | Gemini 3 Pro Preview (CLI) | 1,048,576 | 65,536 |
18
+ | Gemini 3 Flash Preview (CLI) | 1,048,576 | 65,536 |
19
+
20
+ ## Architecture
21
+ - **Proxy server:** `http://127.0.0.1:31337/v1` (OpenAI-compatible)
22
+ - **OpenClaw connects via** `vllm` provider with `api: openai-completions`
23
+ - **CLI models** (`cli-claude/*`, `cli-gemini/*`): plain text completions only — NO tool/function call support
24
+ - **Web-session models** (`web-grok/*`, `web-gemini/*`): browser-based, require `/xxx-login`
25
+ - **Codex models** (`openai-codex/*`): OAuth auth bridge
26
+ - **BitNet** (`local-bitnet/*`): local CPU inference
27
+
28
+ ## Tool Support Limitation
29
+ CLI models explicitly reject tool/function call requests (HTTP 400):
30
+ ```
31
+ Model cli-claude/claude-opus-4-6 does not support tool/function calls.
32
+ Use a native API model (e.g. github-copilot/gpt-5-mini) for agents that need tools.
33
+ ```
34
+ This is by design — CLI tools output plain text only.
35
+
36
+ ## All 4 Browser Providers
37
+ | Provider | Models | Login Cmd | Profile Dir |
38
+ |---|---|---|---|
39
+ | Grok | web-grok/grok-3, grok-3-fast, grok-3-mini, grok-3-mini-fast | /grok-login | ~/.openclaw/grok-profile/ |
40
+ | Gemini | web-gemini/gemini-2-5-pro, gemini-2-5-flash, gemini-3-pro, gemini-3-flash | /gemini-login | ~/.openclaw/gemini-profile/ |
41
+ | Claude | web-claude/* (removed in v1.6.x) | /claude-login | ~/.openclaw/claude-profile/ |
42
+ | ChatGPT | web-chatgpt/* (removed in v1.6.x) | /chatgpt-login | ~/.openclaw/chatgpt-profile/ |
12
43
 
13
44
  ## Stats
14
- - 22 total models (6 CLI + 16 web-session)
15
- - 96/96 tests green (8 test files)
16
- - All 4 providers use launchPersistentContext — sessions survive gateway restarts
17
- - /bridge-status shows cookie-based status (independent of in-memory context)
18
-
19
- ## Architecture: Browser Lifecycle
20
- - **Profile dirs:** `~/.openclaw/{grok,gemini,claude,chatgpt}-profile/`
21
- - **On plugin start:** startup restore attempts headless reconnect from saved profiles (5s delay)
22
- - **On /xxx-login:** headed browser, user logs in, cookies baked to profile automatically
23
- - **On request (no in-memory ctx):** proxy lazy-launches persistent context on first request
24
- - **On /xxx-logout:** closes context + deletes profile + clears expiry file
25
- - **bridge-status:** uses cookie expiry files as source of truth (not in-memory state)
26
- - activebrowser connected and verified
27
- - 🟡 logged in, browser not loaded cookies valid, lazy-loads on first request
28
- - 🔴 session expired — needs /xxx-login
29
- - ⚪ never logged in
30
-
31
- ## Cookie Expiry Tracking (fixed in v1.7.3)
32
- All 4 providers now track the **longest-lived** auth cookie instead of the shortest:
33
- - Claude: `sessionKey` (~1 year) — was `__cf_bm` (Cloudflare, ~30 min) causing false alerts
34
- - ChatGPT: longest of `__Secure-next-auth.session-token` / `_puid` / `oai-did`
35
- - Gemini: longest of `__Secure-1PSID` / `__Secure-3PSID` / `SID`
36
- - Grok: longest of `sso` / `sso-rw`
37
-
38
- ## Release History
39
- - v1.7.3 (2026-03-13): Fix cookie expiry tracking — use longest-lived auth cookie for all 4 providers
40
- - v1.7.2 (2026-03-13): Cookie-first startup restore (skip fragile browser selector check)
41
- - v1.7.1 (2026-03-13): /status HTML dashboard at :31337
42
- - v1.7.0 (2026-03-13): Startup restore timeout fix, auto-relogin, keep-alive verification, vitest suite
43
- - v1.6.1 (2026-03-13): Fix /bridge-status — use cookie expiry as source of truth
44
- - v1.6.0 (2026-03-13): Persistent Chromium profiles for all 4 providers (Claude web + ChatGPT)
45
- - v1.5.1 (2026-03-12): Fix hardcoded plugin version
46
- - v1.5.0 (2026-03-12): Remove /claude-login and /chatgpt-login (pre-v1.6.0 interim)
47
- - v1.4.0 (2026-03-12): Persistent browser fallback for Claude/Gemini/ChatGPT (no CDP required)
48
- - v1.3.5 (2026-03-12): Startup restore guard (SIGUSR1 OOM fix)
49
- - v1.3.0 (2026-03-11): Browser auto-reconnect after gateway restart
50
- - v1.0.0 (2026-03-11): All 4 providers headless — 96/96 tests
51
-
52
- ## Next Steps
53
- - /chatgpt-login should be re-run soon (~6d left on _puid cookie)
54
- - Gemini model switching via UI (2.5 Pro vs Flash vs 3)
55
- - Context-window management for long conversations
45
+ - 22+ total models (7 CLI + 5 Codex + 4 Grok + 4 Gemini + 1 BitNet)
46
+ - Persistent Chromium profiles survive gateway restarts
47
+ - /bridge-status shows cookie-based status
48
+
49
+ ## Release History (recent)
50
+ - v1.9.2 (2026-03-15): Fix maxTokens/contextWindow for all CLI_MODELS (were 8192, now correct per vendor specs)
51
+ - v1.9.1: Previous stable
52
+ - v1.7.3 (2026-03-13): Fix cookie expiry tracking
53
+ - v1.7.0 (2026-03-13): Startup restore timeout fix, auto-relogin, vitest suite
54
+ - v1.6.0 (2026-03-13): Persistent Chromium profiles for all 4 providers
55
+
56
+ ## Known Issues
57
+ - CLI models cannot do tool calls (by design plain text proxy)
58
+ - Opus via CLI proxy may halluzinate XML tool-call tags when maxTokens was too low (fixed in v1.9.2)
package/CONTRIBUTING.md CHANGED
@@ -57,6 +57,24 @@ clawhub publish "$TMPDIR" --slug openclaw-cli-bridge-elvatis --version X.Y.Z \
57
57
 
58
58
  ---
59
59
 
60
+ ## 🚨 Doku-Regel (PFLICHT)
61
+
62
+ **Wenn ein Feature hinzukommt, geändert oder entfernt wird → SOFORT in ALLEN Doku-Dateien aktualisieren.**
63
+
64
+ Gilt für: `README.md`, `SKILL.md`, `CONTRIBUTING.md`, `openclaw.plugin.json`, ClawHub, npm, GitHub.
65
+
66
+ **Konkret für Slash Commands:**
67
+ - Neuer Command (`/cli-xyz`) → sofort in README Utility-Tabelle + ASCII-Beispielblock eintragen
68
+ - Command entfernt → sofort aus README + SKILL.md entfernen, nicht beim nächsten Release
69
+ - Command umbenannt → beide Stellen gleichzeitig ändern
70
+
71
+ **Tested Badge:**
72
+ - Neues Feature getestet → sofort `✅ Tested` Badge in README setzen
73
+
74
+ Kein "machen wir beim nächsten Release" — immer sofort, im gleichen Commit.
75
+
76
+ ---
77
+
60
78
  ## Versionsstellen — alle prüfen vor Release
61
79
 
62
80
  ```bash
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > OpenClaw plugin that bridges locally installed AI CLIs (Codex, Gemini, Claude Code) as model providers — with slash commands for instant model switching, restore, health testing, and model listing.
4
4
 
5
- **Current version:** `1.9.1`
5
+ **Current version:** `1.9.2`
6
6
 
7
7
  ---
8
8
 
@@ -70,6 +70,7 @@ All commands use gateway-level `commands.allowFrom` for authorization (`requireA
70
70
  | `/cli-back` | Restore the model active **before** the last `/cli-*` switch |
71
71
  | `/cli-test [model]` | One-shot proxy health check — **does NOT switch your active model** |
72
72
  | `/cli-list` | Show all registered CLI bridge models with commands |
73
+ | `/cli-help` | Full reference card — CLI/Codex/Web/BitNet sections, expiry info, quick examples, dashboard links |
73
74
 
74
75
  **`/cli-back` details:**
75
76
  - Before every `/cli-*` switch the current model is saved to `~/.openclaw/cli-bridge-state.json`
@@ -217,7 +218,8 @@ openclaw models auth login --provider openai-codex
217
218
  Utility
218
219
  /cli-back Restore previous model
219
220
  /cli-test [model] Health check (no model switch)
220
- /cli-list This overview
221
+ /cli-list All models with slash commands + dashboard URL
222
+ /cli-help Full reference card (sections, expiry, examples, links)
221
223
 
222
224
  Proxy: 127.0.0.1:31337
223
225
  ```
@@ -356,6 +358,15 @@ npm run ci # lint + typecheck + test
356
358
 
357
359
  ## Changelog
358
360
 
361
+ ### v1.9.2
362
+ - **fix:** Correct `maxTokens` and `contextWindow` for all CLI_MODELS — were hardcoded to 8192 output tokens
363
+ - Claude Opus 4.6: 1M context / 128k output (was 200k/8k)
364
+ - Claude Sonnet 4.6: 1M context / 64k output (was 200k/8k)
365
+ - Claude Haiku 4.5: 200k context / 64k output (was 200k/8k)
366
+ - Gemini 2.5 Pro/Flash: 1M context / 65k output (was 1M/8k)
367
+ - Gemini 3 Pro/Flash Preview: 1M context / 65k output (was 1M/8k)
368
+ - Web-session Gemini models: same corrections
369
+
359
370
  ### v1.9.1
360
371
  - **feat:** Full slash command mapping on status page — all models now show their /cli-* command
361
372
  - **fix:** Register missing slash commands: /cli-codex-spark, /cli-codex52, /cli-codex-mini, /cli-gemini3-flash (documented but never registered)
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-cli-bridge-elvatis",
3
3
  "slug": "openclaw-cli-bridge-elvatis",
4
4
  "name": "OpenClaw CLI Bridge",
5
- "version": "1.9.1",
5
+ "version": "2.0.0",
6
6
  "license": "MIT",
7
7
  "description": "Phase 1: openai-codex auth bridge. Phase 2: local HTTP proxy routing model calls through gemini/claude CLIs (vllm provider).",
8
8
  "providers": [
@@ -38,4 +38,4 @@
38
38
  }
39
39
  }
40
40
  }
41
- }
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elvatis_com/openclaw-cli-bridge-elvatis",
3
- "version": "1.9.1",
3
+ "version": "2.0.0",
4
4
  "description": "Bridges gemini, claude, and codex CLI tools as OpenClaw model providers. Reads existing CLI auth without re-login.",
5
5
  "type": "module",
6
6
  "openclaw": {
package/src/cli-runner.ts CHANGED
@@ -1,19 +1,25 @@
1
1
  /**
2
2
  * cli-runner.ts
3
3
  *
4
- * Spawns CLI subprocesses (gemini, claude) and captures their output.
5
- * Input: OpenAI-format messages → formatted prompt string → CLI stdin.
4
+ * Spawns CLI subprocesses (gemini, claude, codex, opencode, pi) and captures their output.
5
+ * Input: OpenAI-format messages → formatted prompt string → CLI stdin (or CLI arg).
6
6
  *
7
- * Both Gemini and Claude receive the prompt via stdin to avoid:
8
- * - E2BIG (arg list too long) for large conversation histories
9
- * - Gemini agentic mode (triggered by @file syntax + workspace cwd)
7
+ * Prompt delivery:
8
+ * - Gemini/Claude/Codex receive the prompt via stdin to avoid E2BIG and agentic mode.
9
+ * - OpenCode receives the prompt as a CLI argument (`opencode run "prompt"`).
10
+ * - Pi receives the prompt via `-p "prompt"` flag.
10
11
  *
11
- * Gemini is always spawned with cwd = tmpdir() so it doesn't scan the
12
- * workspace and enter agentic mode.
12
+ * Workdir isolation:
13
+ * - Gemini: defaults to tmpdir() (prevents agentic workspace scanning).
14
+ * - Claude/Codex: defaults to homedir().
15
+ * - OpenCode/Pi: defaults to homedir().
16
+ * - All runners accept an explicit `workdir` override via RouteOptions.
13
17
  */
14
18
 
15
- import { spawn } from "node:child_process";
19
+ import { spawn, execSync } from "node:child_process";
16
20
  import { tmpdir, homedir } from "node:os";
21
+ import { existsSync } from "node:fs";
22
+ import { join } from "node:path";
17
23
  import { ensureClaudeToken, refreshClaudeToken } from "./claude-auth.js";
18
24
 
19
25
  /** Max messages to include in the prompt sent to the CLI. */
@@ -198,6 +204,41 @@ export function runCli(
198
204
  });
199
205
  }
200
206
 
207
+ /**
208
+ * Spawn a CLI with the prompt delivered as a CLI argument (not stdin).
209
+ * Used by OpenCode which expects `opencode run "prompt"`.
210
+ */
211
+ export function runCliWithArg(
212
+ cmd: string,
213
+ args: string[],
214
+ timeoutMs = 120_000,
215
+ opts: RunCliOptions = {}
216
+ ): Promise<CliRunResult> {
217
+ const cwd = opts.cwd ?? homedir();
218
+
219
+ return new Promise((resolve, reject) => {
220
+ const proc = spawn(cmd, args, {
221
+ timeout: timeoutMs,
222
+ env: buildMinimalEnv(),
223
+ cwd,
224
+ });
225
+
226
+ let stdout = "";
227
+ let stderr = "";
228
+
229
+ proc.stdout.on("data", (d: Buffer) => { stdout += d.toString(); });
230
+ proc.stderr.on("data", (d: Buffer) => { stderr += d.toString(); });
231
+
232
+ proc.on("close", (code) => {
233
+ resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code ?? 0 });
234
+ });
235
+
236
+ proc.on("error", (err) => {
237
+ reject(new Error(`Failed to spawn '${cmd}': ${err.message}`));
238
+ });
239
+ });
240
+ }
241
+
201
242
  // ──────────────────────────────────────────────────────────────────────────────
202
243
  // Gemini CLI
203
244
  // ──────────────────────────────────────────────────────────────────────────────
@@ -215,17 +256,20 @@ export function runCli(
215
256
  * Gemini CLI: -p "" triggers headless mode; stdin content is the actual prompt
216
257
  * (per Gemini docs: "prompt is appended to input on stdin (if any)").
217
258
  *
218
- * cwd = tmpdir() — neutral empty-ish dir, prevents workspace context scanning.
259
+ * cwd = tmpdir() by default — neutral empty-ish dir, prevents workspace context scanning.
260
+ * Override with explicit workdir.
219
261
  */
220
262
  export async function runGemini(
221
263
  prompt: string,
222
264
  modelId: string,
223
- timeoutMs: number
265
+ timeoutMs: number,
266
+ workdir?: string
224
267
  ): Promise<string> {
225
268
  const model = stripPrefix(modelId);
226
269
  // -p "" = headless mode trigger; actual prompt arrives via stdin
227
270
  const args = ["-m", model, "-p", ""];
228
- const result = await runCli("gemini", args, prompt, timeoutMs, { cwd: tmpdir() });
271
+ const cwd = workdir ?? tmpdir();
272
+ const result = await runCli("gemini", args, prompt, timeoutMs, { cwd });
229
273
 
230
274
  // Filter out [WARN] lines from stderr (Gemini emits noisy permission warnings)
231
275
  const cleanStderr = result.stderr
@@ -248,11 +292,13 @@ export async function runGemini(
248
292
  /**
249
293
  * Run Claude Code CLI in headless mode with prompt delivered via stdin.
250
294
  * Strips the model prefix ("cli-claude/claude-opus-4-6" → "claude-opus-4-6").
295
+ * cwd = homedir() by default. Override with explicit workdir.
251
296
  */
252
297
  export async function runClaude(
253
298
  prompt: string,
254
299
  modelId: string,
255
- timeoutMs: number
300
+ timeoutMs: number,
301
+ workdir?: string
256
302
  ): Promise<string> {
257
303
  // Proactively refresh OAuth token if it's about to expire (< 5 min remaining).
258
304
  // No-op for API-key users.
@@ -267,7 +313,8 @@ export async function runClaude(
267
313
  "--model", model,
268
314
  ];
269
315
 
270
- const result = await runCli("claude", args, prompt, timeoutMs);
316
+ const cwd = workdir ?? homedir();
317
+ const result = await runCli("claude", args, prompt, timeoutMs, { cwd });
271
318
 
272
319
  // On 401: attempt one token refresh + retry before giving up.
273
320
  if (result.exitCode !== 0 && result.stdout.length === 0) {
@@ -275,7 +322,7 @@ export async function runClaude(
275
322
  if (stderr.includes("401") || stderr.includes("Invalid authentication credentials") || stderr.includes("authentication_error")) {
276
323
  // Refresh and retry once
277
324
  await refreshClaudeToken();
278
- const retry = await runCli("claude", args, prompt, timeoutMs);
325
+ const retry = await runCli("claude", args, prompt, timeoutMs, { cwd });
279
326
  if (retry.exitCode !== 0 && retry.stdout.length === 0) {
280
327
  const retryStderr = retry.stderr || "(no output)";
281
328
  if (retryStderr.includes("401") || retryStderr.includes("authentication_error") || retryStderr.includes("Invalid authentication credentials")) {
@@ -294,6 +341,97 @@ export async function runClaude(
294
341
  return result.stdout;
295
342
  }
296
343
 
344
+ // ──────────────────────────────────────────────────────────────────────────────
345
+ // Codex CLI
346
+ // ──────────────────────────────────────────────────────────────────────────────
347
+
348
+ /**
349
+ * Ensure the workdir is a git repository. Codex CLI requires a git repo.
350
+ * If the directory exists but is not a git repo, run `git init`.
351
+ */
352
+ function ensureGitRepo(dir: string): void {
353
+ if (!existsSync(join(dir, ".git"))) {
354
+ execSync("git init", { cwd: dir, stdio: "ignore" });
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Run Codex CLI in non-interactive mode with prompt via stdin.
360
+ * cwd = homedir() by default. Override with explicit workdir.
361
+ * Auto-initializes git if workdir is not already a git repo.
362
+ */
363
+ export async function runCodex(
364
+ prompt: string,
365
+ modelId: string,
366
+ timeoutMs: number,
367
+ workdir?: string
368
+ ): Promise<string> {
369
+ const model = stripPrefix(modelId);
370
+ const args = ["--model", model, "--quiet", "--full-auto"];
371
+ const cwd = workdir ?? homedir();
372
+
373
+ // Codex requires a git repo in the working directory
374
+ ensureGitRepo(cwd);
375
+
376
+ const result = await runCli("codex", args, prompt, timeoutMs, { cwd });
377
+
378
+ if (result.exitCode !== 0 && result.stdout.length === 0) {
379
+ throw new Error(`codex exited ${result.exitCode}: ${result.stderr || "(no output)"}`);
380
+ }
381
+
382
+ return result.stdout || result.stderr;
383
+ }
384
+
385
+ // ──────────────────────────────────────────────────────────────────────────────
386
+ // OpenCode CLI
387
+ // ──────────────────────────────────────────────────────────────────────────────
388
+
389
+ /**
390
+ * Run OpenCode CLI. Prompt is passed as a CLI argument: `opencode run "prompt"`.
391
+ * cwd = homedir() by default. Override with explicit workdir.
392
+ */
393
+ export async function runOpenCode(
394
+ prompt: string,
395
+ _modelId: string,
396
+ timeoutMs: number,
397
+ workdir?: string
398
+ ): Promise<string> {
399
+ const args = ["run", prompt];
400
+ const cwd = workdir ?? homedir();
401
+ const result = await runCliWithArg("opencode", args, timeoutMs, { cwd });
402
+
403
+ if (result.exitCode !== 0 && result.stdout.length === 0) {
404
+ throw new Error(`opencode exited ${result.exitCode}: ${result.stderr || "(no output)"}`);
405
+ }
406
+
407
+ return result.stdout || result.stderr;
408
+ }
409
+
410
+ // ──────────────────────────────────────────────────────────────────────────────
411
+ // Pi CLI
412
+ // ──────────────────────────────────────────────────────────────────────────────
413
+
414
+ /**
415
+ * Run Pi CLI in non-interactive mode: `pi -p "prompt"`.
416
+ * cwd = homedir() by default. Override with explicit workdir.
417
+ */
418
+ export async function runPi(
419
+ prompt: string,
420
+ _modelId: string,
421
+ timeoutMs: number,
422
+ workdir?: string
423
+ ): Promise<string> {
424
+ const args = ["-p", prompt];
425
+ const cwd = workdir ?? homedir();
426
+ const result = await runCliWithArg("pi", args, timeoutMs, { cwd });
427
+
428
+ if (result.exitCode !== 0 && result.stdout.length === 0) {
429
+ throw new Error(`pi exited ${result.exitCode}: ${result.stderr || "(no output)"}`);
430
+ }
431
+
432
+ return result.stdout || result.stderr;
433
+ }
434
+
297
435
  // ──────────────────────────────────────────────────────────────────────────────
298
436
  // Model allowlist (T-103)
299
437
  // ──────────────────────────────────────────────────────────────────────────────
@@ -319,6 +457,16 @@ export const DEFAULT_ALLOWED_CLI_MODELS: ReadonlySet<string> = new Set([
319
457
  // Aliases (map to preview variants internally)
320
458
  "cli-gemini/gemini-3-pro", // alias → gemini-3-pro-preview
321
459
  "cli-gemini/gemini-3-flash", // alias → gemini-3-flash-preview
460
+ // Codex CLI
461
+ "openai-codex/gpt-5.3-codex",
462
+ "openai-codex/gpt-5.3-codex-spark",
463
+ "openai-codex/gpt-5.2-codex",
464
+ "openai-codex/gpt-5.4",
465
+ "openai-codex/gpt-5.1-codex-mini",
466
+ // OpenCode CLI
467
+ "opencode/default",
468
+ // Pi CLI
469
+ "pi/default",
322
470
  ]);
323
471
 
324
472
  /** Normalize model aliases to their canonical CLI model names. */
@@ -341,12 +489,20 @@ export interface RouteOptions {
341
489
  * Defaults to DEFAULT_ALLOWED_CLI_MODELS.
342
490
  */
343
491
  allowedModels?: ReadonlySet<string> | null;
492
+ /**
493
+ * Working directory for the CLI subprocess.
494
+ * Overrides the per-runner default (tmpdir for gemini, homedir for others).
495
+ */
496
+ workdir?: string;
344
497
  }
345
498
 
346
499
  /**
347
500
  * Route a chat completion to the correct CLI based on model prefix.
348
- * cli-gemini/<id> → gemini CLI
349
- * cli-claude/<id> → claude CLI
501
+ * cli-gemini/<id> → gemini CLI
502
+ * cli-claude/<id> → claude CLI
503
+ * openai-codex/<id> → codex CLI
504
+ * opencode/<id> → opencode CLI
505
+ * pi/<id> → pi CLI
350
506
  *
351
507
  * Enforces DEFAULT_ALLOWED_CLI_MODELS by default (T-103).
352
508
  * Pass `allowedModels: null` to skip the allowlist check.
@@ -379,11 +535,14 @@ export async function routeToCliRunner(
379
535
  // Resolve aliases (e.g. gemini-3-pro → gemini-3-pro-preview) after allowlist check
380
536
  const resolved = normalizeModelAlias(normalized);
381
537
 
382
- if (resolved.startsWith("cli-gemini/")) return runGemini(prompt, resolved, timeoutMs);
383
- if (resolved.startsWith("cli-claude/")) return runClaude(prompt, resolved, timeoutMs);
538
+ if (resolved.startsWith("cli-gemini/")) return runGemini(prompt, resolved, timeoutMs, opts.workdir);
539
+ if (resolved.startsWith("cli-claude/")) return runClaude(prompt, resolved, timeoutMs, opts.workdir);
540
+ if (resolved.startsWith("openai-codex/")) return runCodex(prompt, resolved, timeoutMs, opts.workdir);
541
+ if (resolved.startsWith("opencode/")) return runOpenCode(prompt, resolved, timeoutMs, opts.workdir);
542
+ if (resolved.startsWith("pi/")) return runPi(prompt, resolved, timeoutMs, opts.workdir);
384
543
 
385
544
  throw new Error(
386
- `Unknown CLI bridge model: "${model}". Use "vllm/cli-gemini/<model>" or "vllm/cli-claude/<model>".`
545
+ `Unknown CLI bridge model: "${model}". Use "vllm/cli-gemini/<model>", "vllm/cli-claude/<model>", "openai-codex/<model>", "opencode/<model>", or "pi/<model>".`
387
546
  );
388
547
  }
389
548
 
@@ -51,7 +51,7 @@ const STABLE_INTERVAL_MS = 500; // ms between stability checks
51
51
 
52
52
  function resolveModel(m?: string): string {
53
53
  const clean = (m ?? "grok-3").replace("web-grok/", "");
54
- const allowed = ["grok-3", "grok-3-fast", "grok-3-mini", "grok-3-mini-fast"];
54
+ const allowed = ["grok-4", "grok-3", "grok-3-fast", "grok-3-mini", "grok-3-mini-fast"];
55
55
  return allowed.includes(clean) ? clean : "grok-3";
56
56
  }
57
57