@@ -83,6 +83,8 @@ const (
8383 defaultDataDirectory = ".opencode"
8484 defaultLogLevel = "info"
8585 appName = "opencode"
86+
87+ MaxTokensFallbackDefault = 4096
8688)
8789
8890var 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+}