config.go

  1// Package config manages application configuration from various sources.
  2package config
  3
  4import (
  5	"fmt"
  6	"log/slog"
  7	"os"
  8	"strings"
  9
 10	"github.com/opencode-ai/opencode/internal/llm/models"
 11	"github.com/opencode-ai/opencode/internal/logging"
 12	"github.com/spf13/viper"
 13)
 14
 15// MCPType defines the type of MCP (Model Control Protocol) server.
 16type MCPType string
 17
 18// Supported MCP types
 19const (
 20	MCPStdio MCPType = "stdio"
 21	MCPSse   MCPType = "sse"
 22)
 23
 24// MCPServer defines the configuration for a Model Control Protocol server.
 25type MCPServer struct {
 26	Command string            `json:"command"`
 27	Env     []string          `json:"env"`
 28	Args    []string          `json:"args"`
 29	Type    MCPType           `json:"type"`
 30	URL     string            `json:"url"`
 31	Headers map[string]string `json:"headers"`
 32}
 33
 34type AgentName string
 35
 36const (
 37	AgentCoder AgentName = "coder"
 38	AgentTask  AgentName = "task"
 39	AgentTitle AgentName = "title"
 40)
 41
 42// Agent defines configuration for different LLM models and their token limits.
 43type Agent struct {
 44	Model           models.ModelID `json:"model"`
 45	MaxTokens       int64          `json:"maxTokens"`
 46	ReasoningEffort string         `json:"reasoningEffort"` // For openai models low,medium,heigh
 47}
 48
 49// Provider defines configuration for an LLM provider.
 50type Provider struct {
 51	APIKey   string `json:"apiKey"`
 52	Disabled bool   `json:"disabled"`
 53}
 54
 55// Data defines storage configuration.
 56type Data struct {
 57	Directory string `json:"directory"`
 58}
 59
 60// LSPConfig defines configuration for Language Server Protocol integration.
 61type LSPConfig struct {
 62	Disabled bool     `json:"enabled"`
 63	Command  string   `json:"command"`
 64	Args     []string `json:"args"`
 65	Options  any      `json:"options"`
 66}
 67
 68// Config is the main configuration structure for the application.
 69type Config struct {
 70	Data         Data                              `json:"data"`
 71	WorkingDir   string                            `json:"wd,omitempty"`
 72	MCPServers   map[string]MCPServer              `json:"mcpServers,omitempty"`
 73	Providers    map[models.ModelProvider]Provider `json:"providers,omitempty"`
 74	LSP          map[string]LSPConfig              `json:"lsp,omitempty"`
 75	Agents       map[AgentName]Agent               `json:"agents"`
 76	Debug        bool                              `json:"debug,omitempty"`
 77	DebugLSP     bool                              `json:"debugLSP,omitempty"`
 78	ContextPaths []string                          `json:"contextPaths,omitempty"`
 79}
 80
 81// Application constants
 82const (
 83	defaultDataDirectory = ".opencode"
 84	defaultLogLevel      = "info"
 85	appName              = "opencode"
 86
 87	MaxTokensFallbackDefault = 4096
 88)
 89
 90var defaultContextPaths = []string{
 91	".github/copilot-instructions.md",
 92	".cursorrules",
 93	".cursor/rules/",
 94	"CLAUDE.md",
 95	"CLAUDE.local.md",
 96	"opencode.md",
 97	"opencode.local.md",
 98	"OpenCode.md",
 99	"OpenCode.local.md",
100	"OPENCODE.md",
101	"OPENCODE.local.md",
102}
103
104// Global configuration instance
105var cfg *Config
106
107// Load initializes the configuration from environment variables and config files.
108// If debug is true, debug mode is enabled and log level is set to debug.
109// It returns an error if configuration loading fails.
110func Load(workingDir string, debug bool) (*Config, error) {
111	if cfg != nil {
112		return cfg, nil
113	}
114
115	cfg = &Config{
116		WorkingDir: workingDir,
117		MCPServers: make(map[string]MCPServer),
118		Providers:  make(map[models.ModelProvider]Provider),
119		LSP:        make(map[string]LSPConfig),
120	}
121
122	configureViper()
123	setDefaults(debug)
124	setProviderDefaults()
125
126	// Read global config
127	if err := readConfig(viper.ReadInConfig()); err != nil {
128		return cfg, err
129	}
130
131	// Load and merge local config
132	mergeLocalConfig(workingDir)
133
134	// Apply configuration to the struct
135	if err := viper.Unmarshal(cfg); err != nil {
136		return cfg, fmt.Errorf("failed to unmarshal config: %w", err)
137	}
138
139	applyDefaultValues()
140	defaultLevel := slog.LevelInfo
141	if cfg.Debug {
142		defaultLevel = slog.LevelDebug
143	}
144	if os.Getenv("OPENCODE_DEV_DEBUG") == "true" {
145		loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log")
146
147		// if file does not exist create it
148		if _, err := os.Stat(loggingFile); os.IsNotExist(err) {
149			if err := os.MkdirAll(cfg.Data.Directory, 0o755); err != nil {
150				return cfg, fmt.Errorf("failed to create directory: %w", err)
151			}
152			if _, err := os.Create(loggingFile); err != nil {
153				return cfg, fmt.Errorf("failed to create log file: %w", err)
154			}
155		}
156
157		sloggingFileWriter, err := os.OpenFile(loggingFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
158		if err != nil {
159			return cfg, fmt.Errorf("failed to open log file: %w", err)
160		}
161		// Configure logger
162		logger := slog.New(slog.NewTextHandler(sloggingFileWriter, &slog.HandlerOptions{
163			Level: defaultLevel,
164		}))
165		slog.SetDefault(logger)
166	} else {
167		// Configure logger
168		logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
169			Level: defaultLevel,
170		}))
171		slog.SetDefault(logger)
172	}
173
174	// Validate configuration
175	if err := Validate(); err != nil {
176		return cfg, fmt.Errorf("config validation failed: %w", err)
177	}
178
179	if cfg.Agents == nil {
180		cfg.Agents = make(map[AgentName]Agent)
181	}
182
183	// Override the max tokens for title agent
184	cfg.Agents[AgentTitle] = Agent{
185		Model:     cfg.Agents[AgentTitle].Model,
186		MaxTokens: 80,
187	}
188	return cfg, nil
189}
190
191// configureViper sets up viper's configuration paths and environment variables.
192func configureViper() {
193	viper.SetConfigName(fmt.Sprintf(".%s", appName))
194	viper.SetConfigType("json")
195	viper.AddConfigPath("$HOME")
196	viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
197	viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
198	viper.SetEnvPrefix(strings.ToUpper(appName))
199	viper.AutomaticEnv()
200}
201
202// setDefaults configures default values for configuration options.
203func setDefaults(debug bool) {
204	viper.SetDefault("data.directory", defaultDataDirectory)
205	viper.SetDefault("contextPaths", defaultContextPaths)
206
207	if debug {
208		viper.SetDefault("debug", true)
209		viper.Set("log.level", "debug")
210	} else {
211		viper.SetDefault("debug", false)
212		viper.SetDefault("log.level", defaultLogLevel)
213	}
214}
215
216// setProviderDefaults configures LLM provider defaults based on environment variables.
217func setProviderDefaults() {
218	// Set all API keys we can find in the environment
219	if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
220		viper.SetDefault("providers.anthropic.apiKey", apiKey)
221	}
222	if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
223		viper.SetDefault("providers.openai.apiKey", apiKey)
224	}
225	if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
226		viper.SetDefault("providers.gemini.apiKey", apiKey)
227	}
228	if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
229		viper.SetDefault("providers.groq.apiKey", apiKey)
230	}
231
232	// Use this order to set the default models
233	// 1. Anthropic
234	// 2. OpenAI
235	// 3. Google Gemini
236	// 4. Groq
237	// 5. AWS Bedrock
238	// Anthropic configuration
239	if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
240		viper.SetDefault("agents.coder.model", models.Claude37Sonnet)
241		viper.SetDefault("agents.task.model", models.Claude37Sonnet)
242		viper.SetDefault("agents.title.model", models.Claude37Sonnet)
243		return
244	}
245
246	// OpenAI configuration
247	if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
248		viper.SetDefault("agents.coder.model", models.GPT41)
249		viper.SetDefault("agents.task.model", models.GPT41Mini)
250		viper.SetDefault("agents.title.model", models.GPT41Mini)
251		return
252	}
253
254	// Google Gemini configuration
255	if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
256		viper.SetDefault("agents.coder.model", models.Gemini25)
257		viper.SetDefault("agents.task.model", models.Gemini25Flash)
258		viper.SetDefault("agents.title.model", models.Gemini25Flash)
259		return
260	}
261
262	// Groq configuration
263	if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
264		viper.SetDefault("agents.coder.model", models.QWENQwq)
265		viper.SetDefault("agents.task.model", models.QWENQwq)
266		viper.SetDefault("agents.title.model", models.QWENQwq)
267		return
268	}
269
270	// OpenRouter configuration
271	if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
272		viper.SetDefault("providers.openrouter.apiKey", apiKey)
273		viper.SetDefault("agents.coder.model", models.OpenRouterClaude37Sonnet)
274		viper.SetDefault("agents.task.model", models.OpenRouterClaude37Sonnet)
275		viper.SetDefault("agents.title.model", models.OpenRouterClaude35Haiku)
276		return
277	}
278
279	// AWS Bedrock configuration
280	if hasAWSCredentials() {
281		viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
282		viper.SetDefault("agents.task.model", models.BedrockClaude37Sonnet)
283		viper.SetDefault("agents.title.model", models.BedrockClaude37Sonnet)
284		return
285	}
286
287	if os.Getenv("AZURE_OPENAI_ENDPOINT") != "" {
288		// api-key may be empty when using Entra ID credentials – that's okay
289		viper.SetDefault("providers.azure.apiKey", os.Getenv("AZURE_OPENAI_API_KEY"))
290		viper.SetDefault("agents.coder.model", models.AzureGPT41)
291		viper.SetDefault("agents.task.model", models.AzureGPT41Mini)
292		viper.SetDefault("agents.title.model", models.AzureGPT41Mini)
293		return
294	}
295}
296
297// hasAWSCredentials checks if AWS credentials are available in the environment.
298func hasAWSCredentials() bool {
299	// Check for explicit AWS credentials
300	if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
301		return true
302	}
303
304	// Check for AWS profile
305	if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
306		return true
307	}
308
309	// Check for AWS region
310	if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
311		return true
312	}
313
314	// Check if running on EC2 with instance profile
315	if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
316		os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
317		return true
318	}
319
320	return false
321}
322
323// readConfig handles the result of reading a configuration file.
324func readConfig(err error) error {
325	if err == nil {
326		return nil
327	}
328
329	// It's okay if the config file doesn't exist
330	if _, ok := err.(viper.ConfigFileNotFoundError); ok {
331		return nil
332	}
333
334	return fmt.Errorf("failed to read config: %w", err)
335}
336
337// mergeLocalConfig loads and merges configuration from the local directory.
338func mergeLocalConfig(workingDir string) {
339	local := viper.New()
340	local.SetConfigName(fmt.Sprintf(".%s", appName))
341	local.SetConfigType("json")
342	local.AddConfigPath(workingDir)
343
344	// Merge local config if it exists
345	if err := local.ReadInConfig(); err == nil {
346		viper.MergeConfigMap(local.AllSettings())
347	}
348}
349
350// applyDefaultValues sets default values for configuration fields that need processing.
351func applyDefaultValues() {
352	// Set default MCP type if not specified
353	for k, v := range cfg.MCPServers {
354		if v.Type == "" {
355			v.Type = MCPStdio
356			cfg.MCPServers[k] = v
357		}
358	}
359}
360
361// It validates model IDs and providers, ensuring they are supported.
362func validateAgent(cfg *Config, name AgentName, agent Agent) error {
363	// Check if model exists
364	model, modelExists := models.SupportedModels[agent.Model]
365	if !modelExists {
366		logging.Warn("unsupported model configured, reverting to default",
367			"agent", name,
368			"configured_model", agent.Model)
369
370		// Set default model based on available providers
371		if setDefaultModelForAgent(name) {
372			logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
373		} else {
374			return fmt.Errorf("no valid provider available for agent %s", name)
375		}
376		return nil
377	}
378
379	// Check if provider for the model is configured
380	provider := model.Provider
381	providerCfg, providerExists := cfg.Providers[provider]
382
383	if !providerExists {
384		// Provider not configured, check if we have environment variables
385		apiKey := getProviderAPIKey(provider)
386		if apiKey == "" {
387			logging.Warn("provider not configured for model, reverting to default",
388				"agent", name,
389				"model", agent.Model,
390				"provider", provider)
391
392			// Set default model based on available providers
393			if setDefaultModelForAgent(name) {
394				logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
395			} else {
396				return fmt.Errorf("no valid provider available for agent %s", name)
397			}
398		} else {
399			// Add provider with API key from environment
400			cfg.Providers[provider] = Provider{
401				APIKey: apiKey,
402			}
403			logging.Info("added provider from environment", "provider", provider)
404		}
405	} else if providerCfg.Disabled || providerCfg.APIKey == "" {
406		// Provider is disabled or has no API key
407		logging.Warn("provider is disabled or has no API key, reverting to default",
408			"agent", name,
409			"model", agent.Model,
410			"provider", provider)
411
412		// Set default model based on available providers
413		if setDefaultModelForAgent(name) {
414			logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
415		} else {
416			return fmt.Errorf("no valid provider available for agent %s", name)
417		}
418	}
419
420	// Validate max tokens
421	if agent.MaxTokens <= 0 {
422		logging.Warn("invalid max tokens, setting to default",
423			"agent", name,
424			"model", agent.Model,
425			"max_tokens", agent.MaxTokens)
426
427		// Update the agent with default max tokens
428		updatedAgent := cfg.Agents[name]
429		if model.DefaultMaxTokens > 0 {
430			updatedAgent.MaxTokens = model.DefaultMaxTokens
431		} else {
432			updatedAgent.MaxTokens = MaxTokensFallbackDefault
433		}
434		cfg.Agents[name] = updatedAgent
435	} else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
436		// Ensure max tokens doesn't exceed half the context window (reasonable limit)
437		logging.Warn("max tokens exceeds half the context window, adjusting",
438			"agent", name,
439			"model", agent.Model,
440			"max_tokens", agent.MaxTokens,
441			"context_window", model.ContextWindow)
442
443		// Update the agent with adjusted max tokens
444		updatedAgent := cfg.Agents[name]
445		updatedAgent.MaxTokens = model.ContextWindow / 2
446		cfg.Agents[name] = updatedAgent
447	}
448
449	// Validate reasoning effort for models that support reasoning
450	if model.CanReason && provider == models.ProviderOpenAI {
451		if agent.ReasoningEffort == "" {
452			// Set default reasoning effort for models that support it
453			logging.Info("setting default reasoning effort for model that supports reasoning",
454				"agent", name,
455				"model", agent.Model)
456
457			// Update the agent with default reasoning effort
458			updatedAgent := cfg.Agents[name]
459			updatedAgent.ReasoningEffort = "medium"
460			cfg.Agents[name] = updatedAgent
461		} else {
462			// Check if reasoning effort is valid (low, medium, high)
463			effort := strings.ToLower(agent.ReasoningEffort)
464			if effort != "low" && effort != "medium" && effort != "high" {
465				logging.Warn("invalid reasoning effort, setting to medium",
466					"agent", name,
467					"model", agent.Model,
468					"reasoning_effort", agent.ReasoningEffort)
469
470				// Update the agent with valid reasoning effort
471				updatedAgent := cfg.Agents[name]
472				updatedAgent.ReasoningEffort = "medium"
473				cfg.Agents[name] = updatedAgent
474			}
475		}
476	} else if !model.CanReason && agent.ReasoningEffort != "" {
477		// Model doesn't support reasoning but reasoning effort is set
478		logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
479			"agent", name,
480			"model", agent.Model,
481			"reasoning_effort", agent.ReasoningEffort)
482
483		// Update the agent to remove reasoning effort
484		updatedAgent := cfg.Agents[name]
485		updatedAgent.ReasoningEffort = ""
486		cfg.Agents[name] = updatedAgent
487	}
488
489	return nil
490}
491
492// Validate checks if the configuration is valid and applies defaults where needed.
493func Validate() error {
494	if cfg == nil {
495		return fmt.Errorf("config not loaded")
496	}
497
498	// Validate agent models
499	for name, agent := range cfg.Agents {
500		if err := validateAgent(cfg, name, agent); err != nil {
501			return err
502		}
503	}
504
505	// Validate providers
506	for provider, providerCfg := range cfg.Providers {
507		if providerCfg.APIKey == "" && !providerCfg.Disabled {
508			logging.Warn("provider has no API key, marking as disabled", "provider", provider)
509			providerCfg.Disabled = true
510			cfg.Providers[provider] = providerCfg
511		}
512	}
513
514	// Validate LSP configurations
515	for language, lspConfig := range cfg.LSP {
516		if lspConfig.Command == "" && !lspConfig.Disabled {
517			logging.Warn("LSP configuration has no command, marking as disabled", "language", language)
518			lspConfig.Disabled = true
519			cfg.LSP[language] = lspConfig
520		}
521	}
522
523	return nil
524}
525
526// getProviderAPIKey gets the API key for a provider from environment variables
527func getProviderAPIKey(provider models.ModelProvider) string {
528	switch provider {
529	case models.ProviderAnthropic:
530		return os.Getenv("ANTHROPIC_API_KEY")
531	case models.ProviderOpenAI:
532		return os.Getenv("OPENAI_API_KEY")
533	case models.ProviderGemini:
534		return os.Getenv("GEMINI_API_KEY")
535	case models.ProviderGROQ:
536		return os.Getenv("GROQ_API_KEY")
537	case models.ProviderAzure:
538		return os.Getenv("AZURE_OPENAI_API_KEY")
539	case models.ProviderOpenRouter:
540		return os.Getenv("OPENROUTER_API_KEY")
541	case models.ProviderBedrock:
542		if hasAWSCredentials() {
543			return "aws-credentials-available"
544		}
545	}
546	return ""
547}
548
549// setDefaultModelForAgent sets a default model for an agent based on available providers
550func setDefaultModelForAgent(agent AgentName) bool {
551	// Check providers in order of preference
552	if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
553		maxTokens := int64(5000)
554		if agent == AgentTitle {
555			maxTokens = 80
556		}
557		cfg.Agents[agent] = Agent{
558			Model:     models.Claude37Sonnet,
559			MaxTokens: maxTokens,
560		}
561		return true
562	}
563
564	if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
565		var model models.ModelID
566		maxTokens := int64(5000)
567		reasoningEffort := ""
568
569		switch agent {
570		case AgentTitle:
571			model = models.GPT41Mini
572			maxTokens = 80
573		case AgentTask:
574			model = models.GPT41Mini
575		default:
576			model = models.GPT41
577		}
578
579		// Check if model supports reasoning
580		if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
581			reasoningEffort = "medium"
582		}
583
584		cfg.Agents[agent] = Agent{
585			Model:           model,
586			MaxTokens:       maxTokens,
587			ReasoningEffort: reasoningEffort,
588		}
589		return true
590	}
591
592	if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
593		var model models.ModelID
594		maxTokens := int64(5000)
595		reasoningEffort := ""
596
597		switch agent {
598		case AgentTitle:
599			model = models.OpenRouterClaude35Haiku
600			maxTokens = 80
601		case AgentTask:
602			model = models.OpenRouterClaude37Sonnet
603		default:
604			model = models.OpenRouterClaude37Sonnet
605		}
606
607		// Check if model supports reasoning
608		if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
609			reasoningEffort = "medium"
610		}
611
612		cfg.Agents[agent] = Agent{
613			Model:           model,
614			MaxTokens:       maxTokens,
615			ReasoningEffort: reasoningEffort,
616		}
617		return true
618	}
619
620	if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
621		var model models.ModelID
622		maxTokens := int64(5000)
623
624		if agent == AgentTitle {
625			model = models.Gemini25Flash
626			maxTokens = 80
627		} else {
628			model = models.Gemini25
629		}
630
631		cfg.Agents[agent] = Agent{
632			Model:     model,
633			MaxTokens: maxTokens,
634		}
635		return true
636	}
637
638	if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
639		maxTokens := int64(5000)
640		if agent == AgentTitle {
641			maxTokens = 80
642		}
643
644		cfg.Agents[agent] = Agent{
645			Model:     models.QWENQwq,
646			MaxTokens: maxTokens,
647		}
648		return true
649	}
650
651	if hasAWSCredentials() {
652		maxTokens := int64(5000)
653		if agent == AgentTitle {
654			maxTokens = 80
655		}
656
657		cfg.Agents[agent] = Agent{
658			Model:           models.BedrockClaude37Sonnet,
659			MaxTokens:       maxTokens,
660			ReasoningEffort: "medium", // Claude models support reasoning
661		}
662		return true
663	}
664
665	return false
666}
667
668// Get returns the current configuration.
669// It's safe to call this function multiple times.
670func Get() *Config {
671	return cfg
672}
673
674// WorkingDirectory returns the current working directory from the configuration.
675func WorkingDirectory() string {
676	if cfg == nil {
677		panic("config not loaded")
678	}
679	return cfg.WorkingDir
680}
681
682func UpdateAgentModel(agentName AgentName, modelID models.ModelID) error {
683	if cfg == nil {
684		panic("config not loaded")
685	}
686
687	existingAgentCfg := cfg.Agents[agentName]
688
689	model, ok := models.SupportedModels[modelID]
690	if !ok {
691		return fmt.Errorf("model %s not supported", modelID)
692	}
693
694	maxTokens := existingAgentCfg.MaxTokens
695	if model.DefaultMaxTokens > 0 {
696		maxTokens = model.DefaultMaxTokens
697	}
698
699	newAgentCfg := Agent{
700		Model:           modelID,
701		MaxTokens:       maxTokens,
702		ReasoningEffort: existingAgentCfg.ReasoningEffort,
703	}
704	cfg.Agents[agentName] = newAgentCfg
705
706	if err := validateAgent(cfg, agentName, newAgentCfg); err != nil {
707		// revert config update on failure
708		cfg.Agents[agentName] = existingAgentCfg
709		return fmt.Errorf("failed to update agent model: %w", err)
710	}
711
712	return nil
713}