Skip to content

Fix opencode-go model sync — pass API key to CLI and strip prefix from model IDs#1425

Merged
gsxdsm merged 3 commits into
Runfusion:mainfrom
tomdurrant:fix/opencode-go-api-key-env
Jun 5, 2026
Merged

Fix opencode-go model sync — pass API key to CLI and strip prefix from model IDs#1425
gsxdsm merged 3 commits into
Runfusion:mainfrom
tomdurrant:fix/opencode-go-api-key-env

Conversation

@tomdurrant

@tomdurrant tomdurrant commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Problem

Two bugs when using OpenCode Go as a provider in Fusion:

  1. Model discovery only returns free models — Fusion saves the Go API key in its auth storage but never passes it to the opencode CLI when running opencode models opencode --refresh. The CLI's internal OpencodePlugin checks process.env.OPENCODE_API_KEY and, when absent, disables all paid models (those with cost.input > 0). Only the 20 free models appear instead of all 67.

  2. Model IDs double-prefixed, API returns 401normalizeOpencodeGoModel was producing IDs like opencode-go/deepseek-v4-flash. The Pi SDK sends model.id verbatim in API requests, so the OpenCode API received a prefixed model name it didn't recognize → 401 Model opencode-go/deepseek-v4-flash is not supported. The bare model name (e.g. deepseek-v4-flash) is what the API expects.

Changes

packages/cli/src/commands/startup-model-sync.ts

  • discoverOpencodeGoModels(apiKey?) — accepts optional API key and passes it as OPENCODE_API_KEY in the spawned opencode CLI process env
  • refreshOpencodeGoModels — added optional apiKey field, forwards to discovery
  • syncStartupModels — reads key from authStorage and passes it through
  • normalizeOpencodeGoModel — strips the opencode/ or opencode-go/ prefix from model IDs, registering bare model names (e.g. deepseek-v4-flash) instead of prefixed ones

packages/cli/src/commands/serve.ts, daemon.ts, dashboard.ts

  • Each onApiKeySaved handler reads the saved key from dashboardAuthStorage and passes it to refreshOpencodeGoModels

Note

After this change, users must re-select their opencode-go model in Settings because the model IDs have changed from prefixed (e.g. opencode-go/deepseek-v4-flash) to bare names (e.g. deepseek-v4-flash).

Summary by CodeRabbit

  • Bug Fixes

    • OpenCode API key is now correctly applied when fetching models, ensuring paid models appear.
    • Model identifiers are normalized to bare names (prefixes removed) and duplicates are deduplicated.
    • Users may need to re-select OpenCode models in Settings due to ID normalization.
  • Tests

    • Added tests for normalization, deduplication, and API-key propagation.

discoverOpencodeGoModels() spawns 'opencode models opencode --refresh' but
never passed the saved API key as OPENCODE_API_KEY. The opencode CLI's
internal OpencodePlugin checks this env var to decide whether to show
paid models; without it, only free (cost.input === 0) models appear.

Now threads the apiKey from auth storage through to the spawned process
environment, so the CLI sees the user's Go subscription and returns the
full model catalog including paid models like Claude, GPT-5.x, Gemini, etc.

Callers in serve.ts, daemon.ts, and dashboard.ts all updated to read
the key from dashboardAuthStorage and pass it to refreshOpencodeGoModels.
syncStartupModels reads from authStorage in StartupSyncOptions.
normalizeOpencodeGoModel was prefixing model IDs with 'opencode-go/'
(e.g. 'opencode-go/deepseek-v4-flash'), but the Pi SDK sends the model
id field as the model name in API requests. The OpenCode API expects
bare model names (e.g. 'deepseek-v4-flash'), not prefixed ones.

Now strips the 'opencode/' or 'opencode-go/' prefix entirely so the
registered model ID matches what the API expects.
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 65c1d0a6-4743-408f-9eb1-de1cc8b70224

📥 Commits

Reviewing files that changed from the base of the PR and between 1ae2555 and 978d07c.

📒 Files selected for processing (6)
  • .changeset/fix-opencode-go-api-key-env.md
  • packages/cli/src/commands/__tests__/startup-model-sync.test.ts
  • packages/cli/src/commands/daemon.ts
  • packages/cli/src/commands/dashboard.ts
  • packages/cli/src/commands/serve.ts
  • packages/cli/src/commands/startup-model-sync.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/fix-opencode-go-api-key-env.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/commands/dashboard.ts

📝 Walkthrough

Walkthrough

This PR adds optional API key support to OpenCode Go model discovery and refresh functions, then integrates that capability into command handlers (daemon, dashboard, and serve) by fetching and passing the opencode-go API key through the discovery chain with fallback to the opencode key.

Changes

OpenCode Go API Key Propagation

Layer / File(s) Summary
Model normalization, discovery, and refresh
packages/cli/src/commands/startup-model-sync.ts
normalizeOpencodeGoModel strips opencode/ and opencode-go/ prefixes and errors on empty names; discoverOpencodeGoModels(apiKey?) injects OPENCODE_API_KEY into the spawned CLI when provided; refreshOpencodeGoModels forwards apiKey; syncStartupModels resolves opencode-goopencode API key and calls refresh.
Command handler delegation for API-key saves
packages/cli/src/commands/daemon.ts, packages/cli/src/commands/dashboard.ts, packages/cli/src/commands/serve.ts
Handlers now call handleOpencodeGoApiKeySaved(dashboardAuthStorage, store, modelRegistry, log) in onApiKeySaved instead of performing inline settings checks and directly invoking refreshOpencodeGoModels.
Tests and release notes
packages/cli/src/commands/__tests__/startup-model-sync.test.ts, .changeset/fix-opencode-go-api-key-env.md
Tests updated to expect unprefixed model IDs, added tests for deduplication, empty-name error, and OPENCODE_API_KEY propagation; changeset documents the fix and notes model-id presentation change.

Sequence Diagram

sequenceDiagram
  participant Handler as Handler (daemon/dashboard/serve)
  participant Auth as Auth Storage
  participant Refresh as refreshOpencodeGoModels
  participant Discover as discoverOpencodeGoModels
  participant CLI as opencode CLI
  Handler->>Auth: fetch apiKey (opencode-go, fallback opencode)
  Handler->>Refresh: call with {apiKey, modelRegistry, log}
  Refresh->>Discover: pass apiKey
  Discover->>CLI: spawn with OPENCODE_API_KEY env
  CLI-->>Discover: model list
  Discover-->>Refresh: discovered models
  Refresh-->>Handler: refresh result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • gsxdsm

Poem

A rabbit hops where models bloom and glide,
Keys tucked in pockets, no prefix to hide,
Discovery hums with envvars set right,
Daemon, dashboard, serve — all sing through the night. 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the two main bug fixes: passing the API key to the CLI and stripping prefixes from model IDs, which are the core changes throughout the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two bugs in OpenCode Go model sync: the saved API key was never forwarded to the spawned opencode CLI process (so only free models were discovered), and model IDs were being registered with a provider prefix that the OpenCode API does not accept (causing 401s). A new shared helper handleOpencodeGoApiKeySaved consolidates the duplicated onApiKeySaved handlers in serve, daemon, and dashboard.

  • API key forwarding: discoverOpencodeGoModels now accepts an optional key and injects it as OPENCODE_API_KEY in the child-process env; syncStartupModels and all onApiKeySaved call-sites resolve the key from auth storage and pass it through.
  • Model ID normalisation: normalizeOpencodeGoModel now strips the opencode/ / opencode-go/ prefix so the bare model name (e.g. deepseek-v4-flash) is registered, matching what the OpenCode API expects; deduplication is added to guard against the CLI emitting both prefix forms.
  • Refactor: The three identical onApiKeySaved blocks in serve/daemon/dashboard are replaced by a single handleOpencodeGoApiKeySaved export, and a patch changeset is included.

Confidence Score: 5/5

Safe to merge — the changes are well-scoped bug fixes with no regressions to existing flows.

Both fixes are straightforward and well-tested: the API key is now correctly forwarded to the child process env, and model IDs are normalised to the bare form the OpenCode API expects. The shared helper eliminates copy-paste drift across three entry points. Deduplication and the empty-ID guard are clean defensive additions. The changeset is present and correctly typed as a patch.

No files require special attention.

Important Files Changed

Filename Overview
packages/cli/src/commands/startup-model-sync.ts Core fix: strips provider prefix from model IDs, forwards API key to child process, deduplicates normalized models, and introduces the shared handleOpencodeGoApiKeySaved helper. Logic is correct and well-guarded.
packages/cli/src/commands/tests/startup-model-sync.test.ts Adds three targeted tests: deduplication of both prefix forms, throw on empty bare ID, and API key env-var forwarding to spawn. Updated existing model ID assertions to expect bare names.
packages/cli/src/commands/serve.ts Replaces duplicated onApiKeySaved implementation with a call to handleOpencodeGoApiKeySaved; the provider-ID guard remains in place.
packages/cli/src/commands/daemon.ts Same refactor as serve.ts: delegates onApiKeySaved to the new shared helper.
packages/cli/src/commands/dashboard.ts Two identical onApiKeySaved blocks (lines ~1760 and ~2079) both updated to use the shared helper.
.changeset/fix-opencode-go-api-key-env.md Patch changeset correctly targets @runfusion/fusion and documents both bugs, the deduplication improvement, and the user-facing ID migration note.

Sequence Diagram

sequenceDiagram
    participant User as User (saves API key)
    participant Handler as onApiKeySaved handler (serve/daemon/dashboard)
    participant Helper as handleOpencodeGoApiKeySaved
    participant Storage as dashboardAuthStorage
    participant Settings as store.getSettings()
    participant Refresh as refreshOpencodeGoModels
    participant CLI as opencode CLI (child process)
    participant Registry as modelRegistry

    User->>Handler: "providerId = opencode or opencode-go"
    Handler->>Helper: dashboardAuthStorage, store, modelRegistry, log
    Helper->>Settings: getSettings()
    Settings-->>Helper: "{ opencodeGoModelSync }"
    alt "opencodeGoModelSync === false"
        Helper-->>Handler: "{ registeredCount: 0, reason: disabled-by-settings }"
    else enabled
        Helper->>Storage: getApiKey(opencode-go)
        Storage-->>Helper: key or undefined
        Helper->>Storage: getApiKey(opencode) fallback if needed
        Storage-->>Helper: key
        Helper->>Refresh: "{ modelRegistry, log, apiKey }"
        Refresh->>CLI: spawn opencode models opencode --refresh with OPENCODE_API_KEY in env
        CLI-->>Refresh: stdout prefixed model IDs
        Note over Refresh: parseOpencodeModelsOutput, normalizeOpencodeGoModel strips prefix, deduplicate by bare ID
        Refresh->>Registry: registerProvider opencode-go with bare model IDs
        Refresh-->>Helper: "{ registeredCount: N }"
        Helper-->>Handler: "{ registeredCount: N }"
    end
Loading

Reviews (2): Last reviewed commit: "Address PR review comments" | Re-trigger Greptile

Comment on lines 321 to +323
if (settings.opencodeGoModelSync !== false) {
await refreshOpencodeGoModels({ modelRegistry: options.modelRegistry, log: options.log });
const opencodeGoApiKey = await options.authStorage.getApiKey("opencode-go") ?? await options.authStorage.getApiKey("opencode");
await refreshOpencodeGoModels({ modelRegistry: options.modelRegistry, log: options.log, apiKey: opencodeGoApiKey });

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.

P1 Missing changeset for published package change

packages/cli is part of @runfusion/fusion, which is the published package. Per the project's AGENTS.md rules, every bug fix that affects @runfusion/fusion requires a patch changeset (e.g. .changeset/<name>.md with "@runfusion/fusion": patch). No changeset file was added in this PR, so the fix will ship without a version bump on the next release.

Context Used: AGENTS.md (source)

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/cli/src/commands/dashboard.ts (1)

1767-1772: ⚖️ Poor tradeoff

Consider extracting the duplicated onApiKeySaved logic.

The opencode-go API key resolution and refresh logic is duplicated across four locations: daemon.ts (727-732), dashboard.ts non-dev (1767-1772), dashboard.ts dev (2090-2095), and serve.ts (834-839). If the fallback order or refresh behavior needs to change, all four must be updated consistently.

♻️ Suggested refactor to extract shared logic

In startup-model-sync.ts:

export async function handleOpencodeGoApiKeySaved(
  dashboardAuthStorage: AuthStorageLike,
  store: { getSettings: () => Promise<SettingsLike> },
  modelRegistry: ModelRegistryLike,
  log: (scope: string, message: string) => void,
): Promise<OpencodeGoRefreshResult | undefined> {
  const settings = await store.getSettings();
  if (settings.opencodeGoModelSync === false) {
    return { registeredCount: 0, reason: "disabled-by-settings" };
  }
  const opencodeGoKey = await dashboardAuthStorage.getApiKey("opencode-go") ?? await dashboardAuthStorage.getApiKey("opencode");
  return await refreshOpencodeGoModels({ modelRegistry, log, apiKey: opencodeGoKey });
}

Then replace the four duplicated blocks with:

onApiKeySaved: async (providerId: string) => {
  if (providerId !== "opencode" && providerId !== "opencode-go") {
    return undefined;
  }
  return await handleOpencodeGoApiKeySaved(dashboardAuthStorage, store, modelRegistry, (scope, message) => console.log(`[${scope}] ${message}`));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/dashboard.ts` around lines 1767 - 1772, Extract the
duplicated opencode-go API key resolution and refresh logic into a single helper
(e.g., handleOpencodeGoApiKeySaved) and call that helper from each onApiKeySaved
hook instead of repeating the block; the helper should accept
dashboardAuthStorage, a store/getSettings accessor, modelRegistry and a log
function, check settings.opencodeGoModelSync and return early if disabled,
resolve apiKey via dashboardAuthStorage.getApiKey("opencode-go") ??
dashboardAuthStorage.getApiKey("opencode"), then call refreshOpencodeGoModels({
modelRegistry, log, apiKey }) and return its result; replace the four inline
implementations in daemon.ts, dashboard.ts (dev and non-dev) and serve.ts with a
small onApiKeySaved wrapper that filters providerId ("opencode" or
"opencode-go") and forwards to handleOpencodeGoApiKeySaved.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/startup-model-sync.ts`:
- Around line 211-219: The function normalizeOpencodeGoModel currently strips
"opencode-go/" or "opencode/" prefixes into bareModel and may return an empty id
when input is exactly "opencode/" or "opencode-go/"; update
normalizeOpencodeGoModel to validate that bareModel is non-empty after stripping
(check the trimmed variable or bareModel) and if empty either throw a clear
error (e.g., "invalid model id: no model name after prefix") or return a
sanitized fallback, ensuring any downstream code cannot receive an empty
id/name; reference the normalizeOpencodeGoModel function and the
bareModel/trimmed symbols when making the change.

---

Nitpick comments:
In `@packages/cli/src/commands/dashboard.ts`:
- Around line 1767-1772: Extract the duplicated opencode-go API key resolution
and refresh logic into a single helper (e.g., handleOpencodeGoApiKeySaved) and
call that helper from each onApiKeySaved hook instead of repeating the block;
the helper should accept dashboardAuthStorage, a store/getSettings accessor,
modelRegistry and a log function, check settings.opencodeGoModelSync and return
early if disabled, resolve apiKey via
dashboardAuthStorage.getApiKey("opencode-go") ??
dashboardAuthStorage.getApiKey("opencode"), then call refreshOpencodeGoModels({
modelRegistry, log, apiKey }) and return its result; replace the four inline
implementations in daemon.ts, dashboard.ts (dev and non-dev) and serve.ts with a
small onApiKeySaved wrapper that filters providerId ("opencode" or
"opencode-go") and forwards to handleOpencodeGoApiKeySaved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e83d197-8e9e-4226-8d96-ac4a3f5be7b5

📥 Commits

Reviewing files that changed from the base of the PR and between 8f3d8a2 and 1ae2555.

📒 Files selected for processing (4)
  • packages/cli/src/commands/daemon.ts
  • packages/cli/src/commands/dashboard.ts
  • packages/cli/src/commands/serve.ts
  • packages/cli/src/commands/startup-model-sync.ts

Comment thread packages/cli/src/commands/startup-model-sync.ts
- Update test assertions for bare model IDs
- Add deduplication guard for models with same bare ID
- Add validation for empty model IDs after prefix stripping
- Add test for API key forwarded as env var to spawn
- Add test for deduplication and empty model ID guard
- Extract shared handleOpencodeGoApiKeySaved helper
- Add changeset for the published package
@gsxdsm

gsxdsm commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Thanks for these fixes!

@gsxdsm
gsxdsm merged commit 02e53b2 into Runfusion:main Jun 5, 2026
8 checks passed
gsxdsm added a commit that referenced this pull request Jun 26, 2026
Addresses P1 findings from the migration review:

Performance:
- Add missing index on tasks.source_parent_task_id (#18): the lineage gate
  (findLiveLineageChildren/removeLineageReferences, run on every archive/delete)
  was a full tasks-table scan.
- Add partial index idxTasksLiveColumn for WHERE deleted_at IS NULL AND column=?
  kanban/board read (#1425 P2) so the planner avoids a bitmap-AND.
- Fix N+1 in merge-queue lease acquire (#19): cleanupStaleMergeQueueRows now
  batches the per-row DELETE + audit INSERT into one bulk DELETE ... WHERE IN
  and one bulk INSERT ... VALUES (was 2 round-trips per stale row).
- Push LIMIT into SQL for audit/activity-log queries (#20): getRunAuditEvents
  and getActivityLog now use Drizzle .limit() instead of fetching the whole set
  then .slice() in JS.
- Bound readLiveTaskRows payload (#21): add excludeLog option that drops the
  heavy log jsonb column (~99% of row payload) from the board-list hydration
  in slim mode, matching the SQLite getTaskSelectClause(slim) projection.

Correctness / security:
- Fix broken backend discriminator (#17): monitor-store, activity-analytics,
  and plugin-activation-analytics used 'transactionImmediate' in db which is
  true for BOTH SQLite Database and AsyncDataLayer. Switched to 'ping' in db
  (unique to AsyncDataLayer). Verified Database has no ping; has
  transactionImmediate.
- Await floating Promise in register-signal-routes (#16): resolveIncident
  became async but the caller did not await it; resolution errors were silently
  dropped. Now awaited and the db handle resolves to getAsyncLayer() in
  backend mode.
- Redact ?password= query-param URLs (#22): redactUrlPassword and
  redactCredentialsFromMessage now also cover the ?password=/&password=
  query-param form, not just the userinfo form.

Standards:
- Convert embedded-postgres-lifecycle.md changeset to labeled summary/category/
  dev format so check:changesets --strict passes.
- Add credential-redact unit tests covering both userinfo and query-param forms.

All gate, typecheck, lint, and changeset checks pass.
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.

2 participants