# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates # SPDX-License-Identifier: MIT import os from dataclasses import dataclass, field import yaml from trae_agent.utils.legacy_config import LegacyConfig class ConfigError(Exception): pass @dataclass class ModelProvider: """ Model provider configuration. For official model providers such as OpenAI and Anthropic, the base_url is optional. api_version is required for Azure. """ api_key: str provider: str base_url: str | None = None api_version: str | None = None @dataclass class ModelConfig: """ Model configuration. """ model: str model_provider: ModelProvider temperature: float top_p: float top_k: int parallel_tool_calls: bool max_retries: int max_tokens: int | None = None # Legacy max_tokens parameter, optional supports_tool_calling: bool = True candidate_count: int | None = None # Gemini specific field stop_sequences: list[str] | None = None max_completion_tokens: int | None = None # Azure OpenAI specific field def get_max_tokens_param(self) -> int: """Get the maximum tokens parameter value.Prioritizes max_completion_tokens, falls back to max_tokens if not available.""" if self.max_completion_tokens is not None: return self.max_completion_tokens elif self.max_tokens is not None: return self.max_tokens else: # Return default value if neither is set return 4096 def should_use_max_completion_tokens(self) -> bool: """Determine whether to use the max_completion_tokens parameter.Primarily used for Azure OpenAI's newer models (e.g., gpt-5).""" return ( self.max_completion_tokens is not None and self.model_provider.provider == "azure" and ("gpt-5" in self.model or "o3" in self.model or "o4-mini" in self.model) ) def resolve_config_values( self, *, model_providers: dict[str, ModelProvider] | None = None, provider: str | None = None, model: str | None = None, model_base_url: str | None = None, api_key: str | None = None, ): """ When some config values are provided through CLI or environment variables, they will override the values in the config file. """ self.model = str(resolve_config_value(cli_value=model, config_value=self.model)) # If the user wants to change the model provider, they should either: # * Make sure the provider name is available in the model_providers dict; # * If not, base url and api key should be provided to register a new model provider. if provider: if model_providers and provider in model_providers: self.model_provider = model_providers[provider] elif api_key is None: raise ConfigError("To register a new model provider, an api_key should be provided") else: self.model_provider = ModelProvider( api_key=api_key, provider=provider, base_url=model_base_url, ) # Map providers to their environment variable names env_var_api_key = str(self.model_provider.provider).upper() + "_API_KEY" env_var_api_base_url = str(self.model_provider.provider).upper() + "_BASE_URL" resolved_api_key = resolve_config_value( cli_value=api_key, config_value=self.model_provider.api_key, env_var=env_var_api_key, ) resolved_api_base_url = resolve_config_value( cli_value=model_base_url, config_value=self.model_provider.base_url, env_var=env_var_api_base_url, ) if resolved_api_key: self.model_provider.api_key = str(resolved_api_key) if resolved_api_base_url: self.model_provider.base_url = str(resolved_api_base_url) @dataclass class MCPServerConfig: # For stdio transport command: str | None = None args: list[str] | None = None env: dict[str, str] | None = None cwd: str | None = None # For sse transport url: str | None = None # For streamable http transport http_url: str | None = None headers: dict[str, str] | None = None # For websocket transport tcp: str | None = None # Common timeout: int | None = None trust: bool | None = None # Metadata description: str | None = None @dataclass class AgentConfig: """ Base class for agent configurations. """ allow_mcp_servers: list[str] mcp_servers_config: dict[str, MCPServerConfig] max_steps: int model: ModelConfig tools: list[str] @dataclass class TraeAgentConfig(AgentConfig): """ Trae agent configuration. """ enable_lakeview: bool = True tools: list[str] = field( default_factory=lambda: [ "bash", "str_replace_based_edit_tool", "sequentialthinking", "task_done", ] ) def resolve_config_values( self, *, max_steps: int | None = None, ): resolved_value = resolve_config_value(cli_value=max_steps, config_value=self.max_steps) if resolved_value: self.max_steps = int(resolved_value) @dataclass class LakeviewConfig: """ Lakeview configuration. """ model: ModelConfig @dataclass class Config: """ Configuration class for agents, models and model providers. """ lakeview: LakeviewConfig | None = None model_providers: dict[str, ModelProvider] | None = None models: dict[str, ModelConfig] | None = None trae_agent: TraeAgentConfig | None = None @classmethod def create( cls, *, config_file: str | None = None, config_string: str | None = None, ) -> "Config": if config_file and config_string: raise ConfigError("Only one of config_file or config_string should be provided") # Parse YAML config from file or string try: if config_file is not None: if config_file.endswith(".json"): return cls.create_from_legacy_config(config_file=config_file) with open(config_file, "r") as f: yaml_config = yaml.safe_load(f) elif config_string is not None: yaml_config = yaml.safe_load(config_string) else: raise ConfigError("No config file or config string provided") except yaml.YAMLError as e: raise ConfigError(f"Error parsing YAML config: {e}") from e config = cls() # Parse model providers model_providers = yaml_config.get("model_providers", None) if model_providers is not None and len(model_providers.keys()) > 0: config_model_providers: dict[str, ModelProvider] = {} for model_provider_name, model_provider_config in model_providers.items(): config_model_providers[model_provider_name] = ModelProvider(**model_provider_config) config.model_providers = config_model_providers else: raise ConfigError("No model providers provided") # Parse models and populate model_provider fields models = yaml_config.get("models", None) if models is not None and len(models.keys()) > 0: config_models: dict[str, ModelConfig] = {} for model_name, model_config in models.items(): if model_config["model_provider"] not in config_model_providers: raise ConfigError(f"Model provider {model_config['model_provider']} not found") config_models[model_name] = ModelConfig(**model_config) config_models[model_name].model_provider = config_model_providers[ model_config["model_provider"] ] config.models = config_models else: raise ConfigError("No models provided") # Parse lakeview config lakeview = yaml_config.get("lakeview", None) if lakeview is not None: lakeview_model_name = lakeview.get("model", None) if lakeview_model_name is None: raise ConfigError("No model provided for lakeview") lakeview_model = config_models[lakeview_model_name] config.lakeview = LakeviewConfig( model=lakeview_model, ) else: config.lakeview = None mcp_servers_config = { k: MCPServerConfig(**v) for k, v in yaml_config.get("mcp_servers", {}).items() } allow_mcp_servers = yaml_config.get("allow_mcp_servers", []) # Parse agents agents = yaml_config.get("agents", None) if agents is not None and len(agents.keys()) > 0: for agent_name, agent_config in agents.items(): agent_model_name = agent_config.get("model", None) if agent_model_name is None: raise ConfigError(f"No model provided for {agent_name}") try: agent_model = config_models[agent_model_name] except KeyError as e: raise ConfigError(f"Model {agent_model_name} not found") from e match agent_name: case "trae_agent": trae_agent_config = TraeAgentConfig( **agent_config, mcp_servers_config=mcp_servers_config, allow_mcp_servers=allow_mcp_servers, ) trae_agent_config.model = agent_model if trae_agent_config.enable_lakeview and config.lakeview is None: raise ConfigError("Lakeview is enabled but no lakeview config provided") config.trae_agent = trae_agent_config case _: raise ConfigError(f"Unknown agent: {agent_name}") else: raise ConfigError("No agent configs provided") return config def resolve_config_values( self, *, provider: str | None = None, model: str | None = None, model_base_url: str | None = None, api_key: str | None = None, max_steps: int | None = None, ): if self.trae_agent: self.trae_agent.resolve_config_values( max_steps=max_steps, ) self.trae_agent.model.resolve_config_values( model_providers=self.model_providers, provider=provider, model=model, model_base_url=model_base_url, api_key=api_key, ) return self @classmethod def create_from_legacy_config( cls, *, legacy_config: LegacyConfig | None = None, config_file: str | None = None, ) -> "Config": if legacy_config and config_file: raise ConfigError("Only one of legacy_config or config_file should be provided") if config_file: legacy_config = LegacyConfig(config_file) elif not legacy_config: raise ConfigError("No legacy_config or config_file provided") model_provider = ModelProvider( api_key=legacy_config.model_providers[legacy_config.default_provider].api_key, base_url=legacy_config.model_providers[legacy_config.default_provider].base_url, api_version=legacy_config.model_providers[legacy_config.default_provider].api_version, provider=legacy_config.default_provider, ) model_config = ModelConfig( model=legacy_config.model_providers[legacy_config.default_provider].model, model_provider=model_provider, max_tokens=legacy_config.model_providers[legacy_config.default_provider].max_tokens, temperature=legacy_config.model_providers[legacy_config.default_provider].temperature, top_p=legacy_config.model_providers[legacy_config.default_provider].top_p, top_k=legacy_config.model_providers[legacy_config.default_provider].top_k, parallel_tool_calls=legacy_config.model_providers[ legacy_config.default_provider ].parallel_tool_calls, max_retries=legacy_config.model_providers[legacy_config.default_provider].max_retries, candidate_count=legacy_config.model_providers[ legacy_config.default_provider ].candidate_count, stop_sequences=legacy_config.model_providers[ legacy_config.default_provider ].stop_sequences, ) mcp_servers_config = { k: MCPServerConfig(**vars(v)) for k, v in legacy_config.mcp_servers.items() } trae_agent_config = TraeAgentConfig( max_steps=legacy_config.max_steps, enable_lakeview=legacy_config.enable_lakeview, model=model_config, allow_mcp_servers=legacy_config.allow_mcp_servers, mcp_servers_config=mcp_servers_config, ) if trae_agent_config.enable_lakeview: lakeview_config = LakeviewConfig( model=model_config, ) else: lakeview_config = None return cls( trae_agent=trae_agent_config, lakeview=lakeview_config, model_providers={ legacy_config.default_provider: model_provider, }, models={ "default_model": model_config, }, ) def resolve_config_value( *, cli_value: int | str | float | None, config_value: int | str | float | None, env_var: str | None = None, ) -> int | str | float | None: """Resolve configuration value with priority: CLI > ENV > Config > Default.""" if cli_value is not None: return cli_value if env_var and os.getenv(env_var): return os.getenv(env_var) if config_value is not None: return config_value return None