Fix opencode-go model sync — pass API key to CLI and strip prefix from model IDs#1425
Conversation
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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis 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. ChangesOpenCode Go API Key Propagation
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR fixes two bugs in OpenCode Go model sync: the saved API key was never forwarded to the spawned
Confidence Score: 5/5Safe 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
Sequence DiagramsequenceDiagram
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
Reviews (2): Last reviewed commit: "Address PR review comments" | Re-trigger Greptile |
| 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 }); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/cli/src/commands/dashboard.ts (1)
1767-1772: ⚖️ Poor tradeoffConsider 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
📒 Files selected for processing (4)
packages/cli/src/commands/daemon.tspackages/cli/src/commands/dashboard.tspackages/cli/src/commands/serve.tspackages/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
|
Thanks for these fixes! |
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.
Problem
Two bugs when using OpenCode Go as a provider in Fusion:
Model discovery only returns free models — Fusion saves the Go API key in its auth storage but never passes it to the
opencodeCLI when runningopencode models opencode --refresh. The CLI's internalOpencodePluginchecksprocess.env.OPENCODE_API_KEYand, when absent, disables all paid models (those withcost.input > 0). Only the 20 free models appear instead of all 67.Model IDs double-prefixed, API returns 401 —
normalizeOpencodeGoModelwas producing IDs likeopencode-go/deepseek-v4-flash. The Pi SDK sendsmodel.idverbatim 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.tsdiscoverOpencodeGoModels(apiKey?)— accepts optional API key and passes it asOPENCODE_API_KEYin the spawnedopencodeCLI process envrefreshOpencodeGoModels— added optionalapiKeyfield, forwards to discoverysyncStartupModels— reads key fromauthStorageand passes it throughnormalizeOpencodeGoModel— strips theopencode/oropencode-go/prefix from model IDs, registering bare model names (e.g.deepseek-v4-flash) instead of prefixed onespackages/cli/src/commands/serve.ts,daemon.ts,dashboard.tsonApiKeySavedhandler reads the saved key fromdashboardAuthStorageand passes it torefreshOpencodeGoModelsNote
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
Tests