Skip to content

Commit b3c0285

Browse files
authored
feat: model selection for given provider (#57)
* feat: model selection for given provider * tweak: adjust cfg validation func, remove duplicated logic, consolidate agent updating into agent.go * tweak: make the model dialog scrollable, adjust padding slightly for modal" * feat: add provider selection, add hints, simplify some logic, add horizontal scrolling support, additional scroll indicators" * remove nav help * update docs * increase number of visible models, make horizontal scroll "wrap" * add provider popularity rankings
1 parent 805aeff commit b3c0285

File tree

8 files changed

+622
-109
lines changed

8 files changed

+622
-109
lines changed

README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ OpenCode supports a variety of AI models from different providers:
168168

169169
### Groq
170170

171-
- Llama 4 Maverick (17b-128e-instruct)
171+
- Llama 4 Maverick (17b-128e-instruct)
172172
- Llama 4 Scout (17b-16e-instruct)
173173
- QWEN QWQ-32b
174174
- Deepseek R1 distill Llama 70b
@@ -216,6 +216,7 @@ opencode -c /path/to/project
216216
| `Ctrl+L` | View logs |
217217
| `Ctrl+A` | Switch session |
218218
| `Ctrl+K` | Command dialog |
219+
| `Ctrl+O` | Toggle model selection dialog |
219220
| `Esc` | Close current overlay/dialog or return to previous mode |
220221

221222
### Chat Page Shortcuts
@@ -245,6 +246,16 @@ opencode -c /path/to/project
245246
| `Enter` | Select session |
246247
| `Esc` | Close dialog |
247248

249+
### Model Dialog Shortcuts
250+
251+
| Shortcut | Action |
252+
| ---------- | ----------------- |
253+
| `` or `k` | Move up |
254+
| `` or `j` | Move down |
255+
| `` or `h` | Previous provider |
256+
| `` or `l` | Next provider |
257+
| `Esc` | Close dialog |
258+
248259
### Permission Dialog Shortcuts
249260

250261
| Shortcut | Action |

internal/app/app.go

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
7373
return app, nil
7474
}
7575

76+
7677
// Shutdown performs a clean shutdown of the application
7778
func (app *App) Shutdown() {
7879
// Cancel all watcher goroutines

internal/config/config.go

+148-105
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ const (
8383
defaultDataDirectory = ".opencode"
8484
defaultLogLevel = "info"
8585
appName = "opencode"
86+
87+
MaxTokensFallbackDefault = 4096
8688
)
8789

8890
var defaultContextPaths = []string{
@@ -347,60 +349,33 @@ func applyDefaultValues() {
347349
}
348350
}
349351

350-
// Validate checks if the configuration is valid and applies defaults where needed.
351352
// It validates model IDs and providers, ensuring they are supported.
352-
func Validate() error {
353-
if cfg == nil {
354-
return fmt.Errorf("config not loaded")
355-
}
356-
357-
// Validate agent models
358-
for name, agent := range cfg.Agents {
359-
// Check if model exists
360-
model, modelExists := models.SupportedModels[agent.Model]
361-
if !modelExists {
362-
logging.Warn("unsupported model configured, reverting to default",
363-
"agent", name,
364-
"configured_model", agent.Model)
365-
366-
// Set default model based on available providers
367-
if setDefaultModelForAgent(name) {
368-
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
369-
} else {
370-
return fmt.Errorf("no valid provider available for agent %s", name)
371-
}
372-
continue
353+
func validateAgent(cfg *Config, name AgentName, agent Agent) error {
354+
// Check if model exists
355+
model, modelExists := models.SupportedModels[agent.Model]
356+
if !modelExists {
357+
logging.Warn("unsupported model configured, reverting to default",
358+
"agent", name,
359+
"configured_model", agent.Model)
360+
361+
// Set default model based on available providers
362+
if setDefaultModelForAgent(name) {
363+
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
364+
} else {
365+
return fmt.Errorf("no valid provider available for agent %s", name)
373366
}
367+
return nil
368+
}
374369

375-
// Check if provider for the model is configured
376-
provider := model.Provider
377-
providerCfg, providerExists := cfg.Providers[provider]
370+
// Check if provider for the model is configured
371+
provider := model.Provider
372+
providerCfg, providerExists := cfg.Providers[provider]
378373

379-
if !providerExists {
380-
// Provider not configured, check if we have environment variables
381-
apiKey := getProviderAPIKey(provider)
382-
if apiKey == "" {
383-
logging.Warn("provider not configured for model, reverting to default",
384-
"agent", name,
385-
"model", agent.Model,
386-
"provider", provider)
387-
388-
// Set default model based on available providers
389-
if setDefaultModelForAgent(name) {
390-
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
391-
} else {
392-
return fmt.Errorf("no valid provider available for agent %s", name)
393-
}
394-
} else {
395-
// Add provider with API key from environment
396-
cfg.Providers[provider] = Provider{
397-
APIKey: apiKey,
398-
}
399-
logging.Info("added provider from environment", "provider", provider)
400-
}
401-
} else if providerCfg.Disabled || providerCfg.APIKey == "" {
402-
// Provider is disabled or has no API key
403-
logging.Warn("provider is disabled or has no API key, reverting to default",
374+
if !providerExists {
375+
// Provider not configured, check if we have environment variables
376+
apiKey := getProviderAPIKey(provider)
377+
if apiKey == "" {
378+
logging.Warn("provider not configured for model, reverting to default",
404379
"agent", name,
405380
"model", agent.Model,
406381
"provider", provider)
@@ -411,75 +386,110 @@ func Validate() error {
411386
} else {
412387
return fmt.Errorf("no valid provider available for agent %s", name)
413388
}
389+
} else {
390+
// Add provider with API key from environment
391+
cfg.Providers[provider] = Provider{
392+
APIKey: apiKey,
393+
}
394+
logging.Info("added provider from environment", "provider", provider)
395+
}
396+
} else if providerCfg.Disabled || providerCfg.APIKey == "" {
397+
// Provider is disabled or has no API key
398+
logging.Warn("provider is disabled or has no API key, reverting to default",
399+
"agent", name,
400+
"model", agent.Model,
401+
"provider", provider)
402+
403+
// Set default model based on available providers
404+
if setDefaultModelForAgent(name) {
405+
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
406+
} else {
407+
return fmt.Errorf("no valid provider available for agent %s", name)
414408
}
409+
}
415410

416-
// Validate max tokens
417-
if agent.MaxTokens <= 0 {
418-
logging.Warn("invalid max tokens, setting to default",
419-
"agent", name,
420-
"model", agent.Model,
421-
"max_tokens", agent.MaxTokens)
411+
// Validate max tokens
412+
if agent.MaxTokens <= 0 {
413+
logging.Warn("invalid max tokens, setting to default",
414+
"agent", name,
415+
"model", agent.Model,
416+
"max_tokens", agent.MaxTokens)
422417

423-
// Update the agent with default max tokens
424-
updatedAgent := cfg.Agents[name]
425-
if model.DefaultMaxTokens > 0 {
426-
updatedAgent.MaxTokens = model.DefaultMaxTokens
427-
} else {
428-
updatedAgent.MaxTokens = 4096 // Fallback default
429-
}
430-
cfg.Agents[name] = updatedAgent
431-
} else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
432-
// Ensure max tokens doesn't exceed half the context window (reasonable limit)
433-
logging.Warn("max tokens exceeds half the context window, adjusting",
418+
// Update the agent with default max tokens
419+
updatedAgent := cfg.Agents[name]
420+
if model.DefaultMaxTokens > 0 {
421+
updatedAgent.MaxTokens = model.DefaultMaxTokens
422+
} else {
423+
updatedAgent.MaxTokens = MaxTokensFallbackDefault
424+
}
425+
cfg.Agents[name] = updatedAgent
426+
} else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
427+
// Ensure max tokens doesn't exceed half the context window (reasonable limit)
428+
logging.Warn("max tokens exceeds half the context window, adjusting",
429+
"agent", name,
430+
"model", agent.Model,
431+
"max_tokens", agent.MaxTokens,
432+
"context_window", model.ContextWindow)
433+
434+
// Update the agent with adjusted max tokens
435+
updatedAgent := cfg.Agents[name]
436+
updatedAgent.MaxTokens = model.ContextWindow / 2
437+
cfg.Agents[name] = updatedAgent
438+
}
439+
440+
// Validate reasoning effort for models that support reasoning
441+
if model.CanReason && provider == models.ProviderOpenAI {
442+
if agent.ReasoningEffort == "" {
443+
// Set default reasoning effort for models that support it
444+
logging.Info("setting default reasoning effort for model that supports reasoning",
434445
"agent", name,
435-
"model", agent.Model,
436-
"max_tokens", agent.MaxTokens,
437-
"context_window", model.ContextWindow)
446+
"model", agent.Model)
438447

439-
// Update the agent with adjusted max tokens
448+
// Update the agent with default reasoning effort
440449
updatedAgent := cfg.Agents[name]
441-
updatedAgent.MaxTokens = model.ContextWindow / 2
450+
updatedAgent.ReasoningEffort = "medium"
442451
cfg.Agents[name] = updatedAgent
443-
}
444-
445-
// Validate reasoning effort for models that support reasoning
446-
if model.CanReason && provider == models.ProviderOpenAI {
447-
if agent.ReasoningEffort == "" {
448-
// Set default reasoning effort for models that support it
449-
logging.Info("setting default reasoning effort for model that supports reasoning",
452+
} else {
453+
// Check if reasoning effort is valid (low, medium, high)
454+
effort := strings.ToLower(agent.ReasoningEffort)
455+
if effort != "low" && effort != "medium" && effort != "high" {
456+
logging.Warn("invalid reasoning effort, setting to medium",
450457
"agent", name,
451-
"model", agent.Model)
458+
"model", agent.Model,
459+
"reasoning_effort", agent.ReasoningEffort)
452460

453-
// Update the agent with default reasoning effort
461+
// Update the agent with valid reasoning effort
454462
updatedAgent := cfg.Agents[name]
455463
updatedAgent.ReasoningEffort = "medium"
456464
cfg.Agents[name] = updatedAgent
457-
} else {
458-
// Check if reasoning effort is valid (low, medium, high)
459-
effort := strings.ToLower(agent.ReasoningEffort)
460-
if effort != "low" && effort != "medium" && effort != "high" {
461-
logging.Warn("invalid reasoning effort, setting to medium",
462-
"agent", name,
463-
"model", agent.Model,
464-
"reasoning_effort", agent.ReasoningEffort)
465-
466-
// Update the agent with valid reasoning effort
467-
updatedAgent := cfg.Agents[name]
468-
updatedAgent.ReasoningEffort = "medium"
469-
cfg.Agents[name] = updatedAgent
470-
}
471465
}
472-
} else if !model.CanReason && agent.ReasoningEffort != "" {
473-
// Model doesn't support reasoning but reasoning effort is set
474-
logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
475-
"agent", name,
476-
"model", agent.Model,
477-
"reasoning_effort", agent.ReasoningEffort)
466+
}
467+
} else if !model.CanReason && agent.ReasoningEffort != "" {
468+
// Model doesn't support reasoning but reasoning effort is set
469+
logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
470+
"agent", name,
471+
"model", agent.Model,
472+
"reasoning_effort", agent.ReasoningEffort)
478473

479-
// Update the agent to remove reasoning effort
480-
updatedAgent := cfg.Agents[name]
481-
updatedAgent.ReasoningEffort = ""
482-
cfg.Agents[name] = updatedAgent
474+
// Update the agent to remove reasoning effort
475+
updatedAgent := cfg.Agents[name]
476+
updatedAgent.ReasoningEffort = ""
477+
cfg.Agents[name] = updatedAgent
478+
}
479+
480+
return nil
481+
}
482+
483+
// Validate checks if the configuration is valid and applies defaults where needed.
484+
func Validate() error {
485+
if cfg == nil {
486+
return fmt.Errorf("config not loaded")
487+
}
488+
489+
// Validate agent models
490+
for name, agent := range cfg.Agents {
491+
if err := validateAgent(cfg, name, agent); err != nil {
492+
return err
483493
}
484494
}
485495

@@ -629,3 +639,36 @@ func WorkingDirectory() string {
629639
}
630640
return cfg.WorkingDir
631641
}
642+
643+
func UpdateAgentModel(agentName AgentName, modelID models.ModelID) error {
644+
if cfg == nil {
645+
panic("config not loaded")
646+
}
647+
648+
existingAgentCfg := cfg.Agents[agentName]
649+
650+
model, ok := models.SupportedModels[modelID]
651+
if !ok {
652+
return fmt.Errorf("model %s not supported", modelID)
653+
}
654+
655+
maxTokens := existingAgentCfg.MaxTokens
656+
if model.DefaultMaxTokens > 0 {
657+
maxTokens = model.DefaultMaxTokens
658+
}
659+
660+
newAgentCfg := Agent{
661+
Model: modelID,
662+
MaxTokens: maxTokens,
663+
ReasoningEffort: existingAgentCfg.ReasoningEffort,
664+
}
665+
cfg.Agents[agentName] = newAgentCfg
666+
667+
if err := validateAgent(cfg, agentName, newAgentCfg); err != nil {
668+
// revert config update on failure
669+
cfg.Agents[agentName] = existingAgentCfg
670+
return fmt.Errorf("failed to update agent model: %w", err)
671+
}
672+
673+
return nil
674+
}

internal/llm/agent/agent.go

+20
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type Service interface {
4242
Cancel(sessionID string)
4343
IsSessionBusy(sessionID string) bool
4444
IsBusy() bool
45+
Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error)
4546
}
4647

4748
type agent struct {
@@ -436,6 +437,25 @@ func (a *agent) TrackUsage(ctx context.Context, sessionID string, model models.M
436437
return nil
437438
}
438439

440+
func (a *agent) Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error) {
441+
if a.IsBusy() {
442+
return models.Model{}, fmt.Errorf("cannot change model while processing requests")
443+
}
444+
445+
if err := config.UpdateAgentModel(agentName, modelID); err != nil {
446+
return models.Model{}, fmt.Errorf("failed to update config: %w", err)
447+
}
448+
449+
provider, err := createAgentProvider(agentName)
450+
if err != nil {
451+
return models.Model{}, fmt.Errorf("failed to create provider for model %s: %w", modelID, err)
452+
}
453+
454+
a.provider = provider
455+
456+
return a.provider.Model(), nil
457+
}
458+
439459
func createAgentProvider(agentName config.AgentName) (provider.Provider, error) {
440460
cfg := config.Get()
441461
agentConfig, ok := cfg.Agents[agentName]

internal/llm/models/anthropic.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ const (
1111
Claude3Opus ModelID = "claude-3-opus"
1212
)
1313

14+
// https://docs.anthropic.com/en/docs/about-claude/models/all-models
1415
var AnthropicModels = map[ModelID]Model{
15-
// Anthropic
1616
Claude35Sonnet: {
1717
ID: Claude35Sonnet,
1818
Name: "Claude 3.5 Sonnet",
@@ -29,13 +29,13 @@ var AnthropicModels = map[ModelID]Model{
2929
ID: Claude3Haiku,
3030
Name: "Claude 3 Haiku",
3131
Provider: ProviderAnthropic,
32-
APIModel: "claude-3-haiku-latest",
32+
APIModel: "claude-3-haiku-20240307", // doesn't support "-latest"
3333
CostPer1MIn: 0.25,
3434
CostPer1MInCached: 0.30,
3535
CostPer1MOutCached: 0.03,
3636
CostPer1MOut: 1.25,
3737
ContextWindow: 200000,
38-
DefaultMaxTokens: 5000,
38+
DefaultMaxTokens: 4096,
3939
},
4040
Claude37Sonnet: {
4141
ID: Claude37Sonnet,

0 commit comments

Comments
 (0)