Skip to content

feat(cli): OpenCode setup commands (openai-compatible provider + remote-aware plugin) - #4277

Merged
diegosouzapw merged 1 commit into
release/v3.8.30from
feat/opencode-cli
Jun 19, 2026
Merged

diegosouzapw merged 1 commit into
release/v3.8.30from
feat/opencode-cli

Conversation

@diegosouzapw

Copy link
Copy Markdown
Collaborator

What

OpenCode parity for remote mode — both approaches (per owner decision):

  1. omniroute setup-opencode (new, openai-compatible): generates the omniroute provider in ~/.config/opencode/opencode.json from the live /v1/models catalog (local or --remote VPS). Run opencode -m omniroute/<model>.
    • Reuses the proven config-generator/opencode.ts (catalog fetch + merge, preserves existing config) via a deferred tsx import.
    • API key referenced as {env:OMNIROUTE_API_KEY} — never written to disk.
    • --only <substrings> filters the model map; --model sets the default; --dry-run previews.
  2. 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-model limit.context/output.
  • /v1/chat/completions (OpenCode's wire) → streamed "OK" ✅.
  • opencode binary run hangs 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 (postProcessOpencodeConfig env-ref + --only filter + $schema/npm preserved; 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

…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.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +125 to +128
.action(async (opts) => {
const code = await runSetupOpencodeCommand(opts);
if (code !== 0) process.exit(code);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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);
    });

Comment on lines +259 to +268
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";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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(/\/+$/, "");

Comment on lines +23 to +24
if (opts.remote) {
baseUrl = String(opts.remote).replace(/\/+$/, "");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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;

Comment on lines +105 to +106
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
writeFileSync(configPath, json, "utf8");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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;
  }

@diegosouzapw
diegosouzapw merged commit 1696944 into release/v3.8.30 Jun 19, 2026
4 checks passed
@diegosouzapw
diegosouzapw deleted the feat/opencode-cli branch June 19, 2026 14:13
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant