Files
2026-07-13 12:49:17 +08:00

411 lines
14 KiB
Python

# 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