fix(tui): include OpenCode runtime custom models in picker#358
fix(tui): include OpenCode runtime custom models in picker#358jatroconis wants to merge 1 commit intoGentleman-Programming:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes the OpenCode model picker in the TUI so that runtime-visible custom provider models (e.g., custom Google models returned by opencode models <provider>) are selectable even when they’re missing from the cached ~/.cache/opencode/models.json.
Changes:
- Add runtime model enrichment by invoking
opencode models <provider>and merging missing models into the cached provider catalog. - Update the TUI model picker initialization to enrich providers before filtering/rendering selectable models.
- Add a regression unit test ensuring missing custom Google models are merged and become SDD-selectable.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| internal/opencode/models.go | Adds runtime model listing via opencode models <provider> and merges missing runtime models into cached provider data. |
| internal/opencode/models_test.go | Adds a unit test validating enrichment merges missing custom models and they survive SDD filtering/sorting. |
| internal/tui/screens/model_picker.go | Calls provider enrichment during model picker state initialization so the picker reflects runtime-visible models. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var listModelsForProvider = func(providerID string) ([]string, error) { | ||
| cmd := exec.Command("opencode", "models", providerID) | ||
| output, err := cmd.Output() | ||
| if err != nil { | ||
| return nil, err | ||
| } |
There was a problem hiding this comment.
listModelsForProvider runs opencode models <provider> with exec.Command(...).Output() and no timeout/context. Because this is invoked during TUI initialization, a slow/hung opencode process can freeze the UI. Consider using exec.CommandContext with a short timeout (and optionally exec.LookPath upfront) and capturing stderr (e.g., via CombinedOutput) so failures return quickly and are diagnosable.
| // listModelsForProvider returns runtime-visible model IDs for a provider using | ||
| // the installed `opencode` binary. The CLI is the source of truth for custom | ||
| // models that may not appear in ~/.cache/opencode/models.json yet. | ||
| var listModelsForProvider = func(providerID string) ([]string, error) { | ||
| cmd := exec.Command("opencode", "models", providerID) | ||
| output, err := cmd.Output() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| var models []string | ||
| for _, line := range strings.Split(string(output), "\n") { | ||
| line = strings.TrimSpace(line) | ||
| if line == "" { | ||
| continue | ||
| } | ||
| parts := strings.SplitN(line, "/", 2) | ||
| if len(parts) != 2 || parts[0] != providerID || parts[1] == "" { | ||
| continue | ||
| } | ||
| models = append(models, parts[1]) | ||
| } | ||
|
|
||
| return models, nil | ||
| } | ||
|
|
There was a problem hiding this comment.
The parsing of opencode models output is embedded in listModelsForProvider and currently isn’t covered by tests (the added test stubs listModelsForProvider, so it doesn’t validate the output format assumptions). To prevent regressions if the CLI output changes (headers, extra columns, missing provider prefix, etc.), consider factoring the parsing into a small helper and adding table-driven tests for representative outputs.
| // listModelsForProvider returns runtime-visible model IDs for a provider using | |
| // the installed `opencode` binary. The CLI is the source of truth for custom | |
| // models that may not appear in ~/.cache/opencode/models.json yet. | |
| var listModelsForProvider = func(providerID string) ([]string, error) { | |
| cmd := exec.Command("opencode", "models", providerID) | |
| output, err := cmd.Output() | |
| if err != nil { | |
| return nil, err | |
| } | |
| var models []string | |
| for _, line := range strings.Split(string(output), "\n") { | |
| line = strings.TrimSpace(line) | |
| if line == "" { | |
| continue | |
| } | |
| parts := strings.SplitN(line, "/", 2) | |
| if len(parts) != 2 || parts[0] != providerID || parts[1] == "" { | |
| continue | |
| } | |
| models = append(models, parts[1]) | |
| } | |
| return models, nil | |
| } | |
| // parseModelsForProviderOutput extracts model IDs for a provider from | |
| // `opencode models` CLI output. Lines that are empty, malformed, for a | |
| // different provider, or have an empty model suffix are ignored. | |
| func parseModelsForProviderOutput(providerID, output string) []string { | |
| var models []string | |
| for _, line := range strings.Split(output, "\n") { | |
| line = strings.TrimSpace(line) | |
| if line == "" { | |
| continue | |
| } | |
| parts := strings.SplitN(line, "/", 2) | |
| if len(parts) != 2 || parts[0] != providerID || parts[1] == "" { | |
| continue | |
| } | |
| models = append(models, parts[1]) | |
| } | |
| return models | |
| } | |
| // listModelsForProvider returns runtime-visible model IDs for a provider using | |
| // the installed `opencode` binary. The CLI is the source of truth for custom | |
| // models that may not appear in ~/.cache/opencode/models.json yet. | |
| var listModelsForProvider = func(providerID string) ([]string, error) { | |
| cmd := exec.Command("opencode", "models", providerID) | |
| output, err := cmd.Output() | |
| if err != nil { | |
| return nil, err | |
| } | |
| return parseModelsForProviderOutput(providerID, string(output)), nil | |
| } |
| available := opencode.DetectAvailableProviders(providers) | ||
| opencode.EnrichProvidersWithRuntimeModels(providers, available) | ||
|
|
There was a problem hiding this comment.
NewModelPickerState now enriches providers by running the opencode CLI during state construction. This codepath is triggered synchronously on key navigation (see callers in internal/tui/model.go), so it can introduce noticeable latency. Consider making runtime enrichment lazy (only when entering a provider) or performing it asynchronously with a loading state/spinner, especially if multiple providers are available.
🔗 Linked Issue
Closes #357
🏷️ PR Type
What kind of change does this PR introduce?
type:bug— Bug fix (non-breaking change that fixes an issue)type:feature— New feature (non-breaking change that adds functionality)type:docs— Documentation onlytype:refactor— Code refactoring (no functional changes)type:chore— Build, CI, or tooling changestype:breaking-change— Breaking change (fix or feature that changes existing behavior)📝 Summary
opencode models <provider>output into the cached provider catalog before rendering the picker.antigravity-claude-*.📂 Changes
internal/opencode/models.goopencode models <provider>and merged missing models into the cached provider catalog.internal/opencode/models_test.gointernal/tui/screens/model_picker.go🧪 Test Plan
Unit Tests
go test ./...E2E Tests (Docker required)
go test ./...)cd e2e && ./docker-test.sh)Manual verification:
gentle-aiTUI before the fix🤖 Automated Checks
The following checks run automatically on this PR:
Closes/Fixes/Resolves #Nstatus:approvedtype:*Labeltype:*label must be appliedgo test ./...must passcd e2e && ./docker-test.shmust pass✅ Contributor Checklist
status:approvedtype:*label to this PRgo test ./...)cd e2e && ./docker-test.sh)Co-Authored-Bytrailers💬 Notes for Reviewers
status:approvedto the issue or apply thetype:buglabel on the PR.