@@ -120,13 +120,11 @@ func Load(workingDir string, debug bool) (*Config, error) {
120120 }
121121
122122 applyDefaultValues()
123−
124123 defaultLevel := slog.LevelInfo
125124 if cfg.Debug {
126125 defaultLevel = slog.LevelDebug
127126 }
128− // if we are in debug mode make the writer a file
129− if cfg.Debug {
127+ if os.Getenv("OPENCODE_DEV_DEBUG") == "true" {
130128 loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log")
131129
132130 // if file does not exist create it
@@ -156,6 +154,11 @@ func Load(workingDir string, debug bool) (*Config, error) {
156154 slog.SetDefault(logger)
157155 }
158156
157+ // Validate configuration
158+ if err := Validate(); err != nil {
159+ return cfg, fmt.Errorf("config validation failed: %w", err)
160+ }
161+
159162 if cfg.Agents == nil {
160163 cfg.Agents = make(map[AgentName]Agent)
161164 }
@@ -302,6 +305,273 @@ func applyDefaultValues() {
302305 }
303306}
304307
308+// Validate checks if the configuration is valid and applies defaults where needed.
309+// It validates model IDs and providers, ensuring they are supported.
310+func Validate() error {
311+ if cfg == nil {
312+ return fmt.Errorf("config not loaded")
313+ }
314+
315+ // Validate agent models
316+ for name, agent := range cfg.Agents {
317+ // Check if model exists
318+ model, modelExists := models.SupportedModels[agent.Model]
319+ if !modelExists {
320+ logging.Warn("unsupported model configured, reverting to default",
321+ "agent", name,
322+ "configured_model", agent.Model)
323+
324+ // Set default model based on available providers
325+ if setDefaultModelForAgent(name) {
326+ logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
327+ } else {
328+ return fmt.Errorf("no valid provider available for agent %s", name)
329+ }
330+ continue
331+ }
332+
333+ // Check if provider for the model is configured
334+ provider := model.Provider
335+ providerCfg, providerExists := cfg.Providers[provider]
336+
337+ if !providerExists {
338+ // Provider not configured, check if we have environment variables
339+ apiKey := getProviderAPIKey(provider)
340+ if apiKey == "" {
341+ logging.Warn("provider not configured for model, reverting to default",
342+ "agent", name,
343+ "model", agent.Model,
344+ "provider", provider)
345+
346+ // Set default model based on available providers
347+ if setDefaultModelForAgent(name) {
348+ logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
349+ } else {
350+ return fmt.Errorf("no valid provider available for agent %s", name)
351+ }
352+ } else {
353+ // Add provider with API key from environment
354+ cfg.Providers[provider] = Provider{
355+ APIKey: apiKey,
356+ }
357+ logging.Info("added provider from environment", "provider", provider)
358+ }
359+ } else if providerCfg.Disabled || providerCfg.APIKey == "" {
360+ // Provider is disabled or has no API key
361+ logging.Warn("provider is disabled or has no API key, reverting to default",
362+ "agent", name,
363+ "model", agent.Model,
364+ "provider", provider)
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+ }
373+
374+ // Validate max tokens
375+ if agent.MaxTokens <= 0 {
376+ logging.Warn("invalid max tokens, setting to default",
377+ "agent", name,
378+ "model", agent.Model,
379+ "max_tokens", agent.MaxTokens)
380+
381+ // Update the agent with default max tokens
382+ updatedAgent := cfg.Agents[name]
383+ if model.DefaultMaxTokens > 0 {
384+ updatedAgent.MaxTokens = model.DefaultMaxTokens
385+ } else {
386+ updatedAgent.MaxTokens = 4096 // Fallback default
387+ }
388+ cfg.Agents[name] = updatedAgent
389+ } else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
390+ // Ensure max tokens doesn't exceed half the context window (reasonable limit)
391+ logging.Warn("max tokens exceeds half the context window, adjusting",
392+ "agent", name,
393+ "model", agent.Model,
394+ "max_tokens", agent.MaxTokens,
395+ "context_window", model.ContextWindow)
396+
397+ // Update the agent with adjusted max tokens
398+ updatedAgent := cfg.Agents[name]
399+ updatedAgent.MaxTokens = model.ContextWindow / 2
400+ cfg.Agents[name] = updatedAgent
401+ }
402+
403+ // Validate reasoning effort for models that support reasoning
404+ if model.CanReason && provider == models.ProviderOpenAI {
405+ if agent.ReasoningEffort == "" {
406+ // Set default reasoning effort for models that support it
407+ logging.Info("setting default reasoning effort for model that supports reasoning",
408+ "agent", name,
409+ "model", agent.Model)
410+
411+ // Update the agent with default reasoning effort
412+ updatedAgent := cfg.Agents[name]
413+ updatedAgent.ReasoningEffort = "medium"
414+ cfg.Agents[name] = updatedAgent
415+ } else {
416+ // Check if reasoning effort is valid (low, medium, high)
417+ effort := strings.ToLower(agent.ReasoningEffort)
418+ if effort != "low" && effort != "medium" && effort != "high" {
419+ logging.Warn("invalid reasoning effort, setting to medium",
420+ "agent", name,
421+ "model", agent.Model,
422+ "reasoning_effort", agent.ReasoningEffort)
423+
424+ // Update the agent with valid reasoning effort
425+ updatedAgent := cfg.Agents[name]
426+ updatedAgent.ReasoningEffort = "medium"
427+ cfg.Agents[name] = updatedAgent
428+ }
429+ }
430+ } else if !model.CanReason && agent.ReasoningEffort != "" {
431+ // Model doesn't support reasoning but reasoning effort is set
432+ logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
433+ "agent", name,
434+ "model", agent.Model,
435+ "reasoning_effort", agent.ReasoningEffort)
436+
437+ // Update the agent to remove reasoning effort
438+ updatedAgent := cfg.Agents[name]
439+ updatedAgent.ReasoningEffort = ""
440+ cfg.Agents[name] = updatedAgent
441+ }
442+ }
443+
444+ // Validate providers
445+ for provider, providerCfg := range cfg.Providers {
446+ if providerCfg.APIKey == "" && !providerCfg.Disabled {
447+ logging.Warn("provider has no API key, marking as disabled", "provider", provider)
448+ providerCfg.Disabled = true
449+ cfg.Providers[provider] = providerCfg
450+ }
451+ }
452+
453+ // Validate LSP configurations
454+ for language, lspConfig := range cfg.LSP {
455+ if lspConfig.Command == "" && !lspConfig.Disabled {
456+ logging.Warn("LSP configuration has no command, marking as disabled", "language", language)
457+ lspConfig.Disabled = true
458+ cfg.LSP[language] = lspConfig
459+ }
460+ }
461+
462+ return nil
463+}
464+
465+// getProviderAPIKey gets the API key for a provider from environment variables
466+func getProviderAPIKey(provider models.ModelProvider) string {
467+ switch provider {
468+ case models.ProviderAnthropic:
469+ return os.Getenv("ANTHROPIC_API_KEY")
470+ case models.ProviderOpenAI:
471+ return os.Getenv("OPENAI_API_KEY")
472+ case models.ProviderGemini:
473+ return os.Getenv("GEMINI_API_KEY")
474+ case models.ProviderGROQ:
475+ return os.Getenv("GROQ_API_KEY")
476+ case models.ProviderBedrock:
477+ if hasAWSCredentials() {
478+ return "aws-credentials-available"
479+ }
480+ }
481+ return ""
482+}
483+
484+// setDefaultModelForAgent sets a default model for an agent based on available providers
485+func setDefaultModelForAgent(agent AgentName) bool {
486+ // Check providers in order of preference
487+ if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
488+ maxTokens := int64(5000)
489+ if agent == AgentTitle {
490+ maxTokens = 80
491+ }
492+ cfg.Agents[agent] = Agent{
493+ Model: models.Claude37Sonnet,
494+ MaxTokens: maxTokens,
495+ }
496+ return true
497+ }
498+
499+ if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
500+ var model models.ModelID
501+ maxTokens := int64(5000)
502+ reasoningEffort := ""
503+
504+ switch agent {
505+ case AgentTitle:
506+ model = models.GPT41Mini
507+ maxTokens = 80
508+ case AgentTask:
509+ model = models.GPT41Mini
510+ default:
511+ model = models.GPT41
512+ }
513+
514+ // Check if model supports reasoning
515+ if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
516+ reasoningEffort = "medium"
517+ }
518+
519+ cfg.Agents[agent] = Agent{
520+ Model: model,
521+ MaxTokens: maxTokens,
522+ ReasoningEffort: reasoningEffort,
523+ }
524+ return true
525+ }
526+
527+ if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
528+ var model models.ModelID
529+ maxTokens := int64(5000)
530+
531+ if agent == AgentTitle {
532+ model = models.Gemini25Flash
533+ maxTokens = 80
534+ } else {
535+ model = models.Gemini25
536+ }
537+
538+ cfg.Agents[agent] = Agent{
539+ Model: model,
540+ MaxTokens: maxTokens,
541+ }
542+ return true
543+ }
544+
545+ if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
546+ maxTokens := int64(5000)
547+ if agent == AgentTitle {
548+ maxTokens = 80
549+ }
550+
551+ cfg.Agents[agent] = Agent{
552+ Model: models.QWENQwq,
553+ MaxTokens: maxTokens,
554+ }
555+ return true
556+ }
557+
558+ if hasAWSCredentials() {
559+ maxTokens := int64(5000)
560+ if agent == AgentTitle {
561+ maxTokens = 80
562+ }
563+
564+ cfg.Agents[agent] = Agent{
565+ Model: models.BedrockClaude37Sonnet,
566+ MaxTokens: maxTokens,
567+ ReasoningEffort: "medium", // Claude models support reasoning
568+ }
569+ return true
570+ }
571+
572+ return false
573+}
574+
305575// Get returns the current configuration.
306576// It's safe to call this function multiple times.
307577func Get() *Config {