Repository navigation
feat(cli): OpenCode setup commands (openai-compatible provider + remote-aware plugin) - #4277
Conversation
…te-aware plugin) OpenCode parity for remote mode (CLI #3 of the series), both approaches: - `omniroute setup-opencode` (new): generates the `omniroute` openai-compatible provider in ~/.config/opencode/opencode.json from the live /v1/models catalog (local or --remote VPS). Reuses the proven config-generator/opencode.ts (catalog fetch + merge, preserves existing config) via a deferred tsx import; references the key as {env:OMNIROUTE_API_KEY} (never on disk). `--only` filters the model map; `--model` sets the default. Run: `opencode -m omniroute/<model>`. - `omniroute setup opencode` (plugin) is now remote-aware: new `--remote` + resolves baseURL from the active context (was localhost-only via --base-url). Docs: REMOTE-MODE.md per-CLI table + OpenCode section. Tests: postProcess (env-ref + --only filter, preserves $schema/npm), resolveOpencodeTarget. Validated remotely vs VPS v3.8.30: setup-opencode --remote → correct provider config from 1119-model catalog; /v1/chat/completions (OpenCode's wire) → "OK". check:cli-i18n green; 6 unit tests pass.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Code Review
This pull request introduces a new setup-opencode CLI command to generate an OpenAI-compatible provider configuration for OpenCode, updates the existing setup-open-code command to support remote contexts, and adds corresponding unit tests and documentation. The reviewer's feedback focuses on enhancing CLI robustness and error handling, including merging global options in the new command, trimming trailing slashes from base URLs, adding type checks to prevent crashes from boolean flags, and wrapping filesystem writes in try-catch blocks.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| .action(async (opts) => { | ||
| const code = await runSetupOpencodeCommand(opts); | ||
| if (code !== 0) process.exit(code); | ||
| }); |
There was a problem hiding this comment.
The action callback only receives opts (local options) and does not merge global options (like --context, global --api-key, or --base-url). This means global options will be completely ignored by the setup-opencode command, breaking remote-mode parity. Using cmd.optsWithGlobals() ensures all options are correctly merged.
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals?.() ?? {};
const merged = {
...globalOpts,
...opts,
};
const code = await runSetupOpencodeCommand(merged);
if (code !== 0) process.exit(code);
});| let baseURL = opts.remote || opts.baseURL || opts.baseUrl; | ||
| if (!baseURL) { | ||
| try { | ||
| const ctx = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT); | ||
| baseURL = ctx?.baseUrl; | ||
| } catch { | ||
| /* no context */ | ||
| } | ||
| } | ||
| if (!baseURL) baseURL = "http://localhost:20128"; |
There was a problem hiding this comment.
Trimming trailing slashes from baseURL is recommended to prevent potential double-slash issues in subsequent network requests or when registering the plugin in opencode.json.
| let baseURL = opts.remote || opts.baseURL || opts.baseUrl; | |
| if (!baseURL) { | |
| try { | |
| const ctx = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT); | |
| baseURL = ctx?.baseUrl; | |
| } catch { | |
| /* no context */ | |
| } | |
| } | |
| if (!baseURL) baseURL = "http://localhost:20128"; | |
| let baseURL = opts.remote || opts.baseURL || opts.baseUrl; | |
| if (!baseURL) { | |
| try { | |
| const ctx = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT); | |
| baseURL = ctx?.baseUrl; | |
| } catch { | |
| /* no context */ | |
| } | |
| } | |
| if (!baseURL) baseURL = "http://localhost:20128"; | |
| baseURL = String(baseURL).replace(/\/+$/, ""); |
| if (opts.remote) { | ||
| baseUrl = String(opts.remote).replace(/\/+$/, ""); |
There was a problem hiding this comment.
If opts.remote is passed as a boolean (e.g., if the flag is present but empty, or programmatically), String(opts.remote) will evaluate to "true", which is an invalid URL. Checking if it is a string prevents this.
| if (opts.remote) { | |
| baseUrl = String(opts.remote).replace(/\/+$/, ""); | |
| if (typeof opts.remote === "string" && opts.remote.trim()) { | |
| baseUrl = opts.remote.trim().replace(/\/+$/, ""); |
| export async function runSetupOpencodeCommand(opts = {}) { | ||
| const { baseUrl, apiKey } = resolveOpencodeTarget(opts); | ||
| const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]); | ||
| const only = opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : null; |
There was a problem hiding this comment.
If opts.only is not a string (e.g., if it is parsed as a boolean true when the flag is present but empty, or passed programmatically), calling .split() will throw a TypeError and crash the CLI. Checking the type first ensures defensive safety.
| const only = opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : null; | |
| const only = typeof opts.only === "string" ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : null; |
| if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true }); | ||
| writeFileSync(configPath, json, "utf8"); |
There was a problem hiding this comment.
If directory creation or file writing fails (e.g., due to permission issues or disk full), it will throw an unhandled exception and crash the CLI with a raw stack trace. Wrapping this in a try-catch block and printing a user-friendly error message is safer.
try {
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
writeFileSync(configPath, json, "utf8");
} catch (err) {
printError(`Failed to write opencode.json: ${err?.message || err}`);
return 1;
}…te-aware plugin) (cheaper-inference#4277) setup-opencode: remote-aware openai-compatible provider generator; API key referenced by env var (never on disk). 6 tests.
What
OpenCode parity for remote mode — both approaches (per owner decision):
omniroute setup-opencode(new, openai-compatible): generates theomnirouteprovider in~/.config/opencode/opencode.jsonfrom the live/v1/modelscatalog (local or--remoteVPS). Runopencode -m omniroute/<model>.config-generator/opencode.ts(catalog fetch + merge, preserves existing config) via a deferredtsximport.{env:OMNIROUTE_API_KEY}— never written to disk.--only <substrings>filters the model map;--modelsets the default;--dry-runpreviews.omniroute setup opencode(the existing plugin command) is now remote-aware: new--remote+ resolves baseURL from the active context (was localhost-only).OpenCode is special — one provider, many models, no native profiles and no model auto-discovery — so this declares the full catalog statically (the correct approach per the OpenCode docs).
Validation (remote, VPS v3.8.30 via Tailscale)
setup-opencode --remote --dry-run→ correct provider config generated from the 1119-model catalog, with{env:...}key ref + per-modellimit.context/output./v1/chat/completions(OpenCode's wire) → streamed "OK" ✅.opencodebinaryrunhangs non-interactively in the sandbox (interactive-tool limitation, not a code defect) — the generated config + the wire are validated.check:cli-i18n✓ · 6 unit tests (postProcessOpencodeConfigenv-ref +--onlyfilter +$schema/npmpreserved;resolveOpencodeTarget).Series
CLI #3 (after Codex #4270, Claude Code #4274). Next, one at a time: Cline, Kilo Code, Continue, then the research-heavy ones (Cursor, Roo, Crush, Goose, Qwen Code, Gemini CLI, Aider).
🤖 Generated with Claude Code