from pathlib import Path import toml from pydantic import BaseModel, Field from openhands.cli.tui import ( UsageMetrics, ) from openhands.events.event import Event from openhands.llm.metrics import Metrics _LOCAL_CONFIG_FILE_PATH = Path.home() / '.openhands' / 'config.toml' _DEFAULT_CONFIG: dict[str, dict[str, list[str]]] = {'sandbox': {'trusted_dirs': []}} def get_local_config_trusted_dirs() -> list[str]: if _LOCAL_CONFIG_FILE_PATH.exists(): with open(_LOCAL_CONFIG_FILE_PATH, 'r') as f: try: config = toml.load(f) except Exception: config = _DEFAULT_CONFIG if 'sandbox' in config and 'trusted_dirs' in config['sandbox']: return config['sandbox']['trusted_dirs'] return [] def add_local_config_trusted_dir(folder_path: str) -> None: config = _DEFAULT_CONFIG if _LOCAL_CONFIG_FILE_PATH.exists(): try: with open(_LOCAL_CONFIG_FILE_PATH, 'r') as f: config = toml.load(f) except Exception: config = _DEFAULT_CONFIG else: _LOCAL_CONFIG_FILE_PATH.parent.mkdir(parents=True, exist_ok=True) if 'sandbox' not in config: config['sandbox'] = {} if 'trusted_dirs' not in config['sandbox']: config['sandbox']['trusted_dirs'] = [] if folder_path not in config['sandbox']['trusted_dirs']: config['sandbox']['trusted_dirs'].append(folder_path) with open(_LOCAL_CONFIG_FILE_PATH, 'w') as f: toml.dump(config, f) def update_usage_metrics(event: Event, usage_metrics: UsageMetrics) -> None: if not hasattr(event, 'llm_metrics'): return llm_metrics: Metrics | None = event.llm_metrics if not llm_metrics: return usage_metrics.metrics = llm_metrics class ModelInfo(BaseModel): """Information about a model and its provider.""" provider: str = Field(description='The provider of the model') model: str = Field(description='The model identifier') separator: str = Field(description='The separator used in the model identifier') def __getitem__(self, key: str) -> str: """Allow dictionary-like access to fields.""" if key == 'provider': return self.provider elif key == 'model': return self.model elif key == 'separator': return self.separator raise KeyError(f'ModelInfo has no key {key}') def extract_model_and_provider(model: str) -> ModelInfo: """Extract provider and model information from a model identifier. Args: model: The model identifier string Returns: A ModelInfo object containing provider, model, and separator information """ separator = '/' split = model.split(separator) if len(split) == 1: # no "/" separator found, try with "." separator = '.' split = model.split(separator) if split_is_actually_version(split): split = [separator.join(split)] # undo the split if len(split) == 1: # no "/" or "." separator found if split[0] in VERIFIED_OPENAI_MODELS: return ModelInfo(provider='openai', model=split[0], separator='/') if split[0] in VERIFIED_ANTHROPIC_MODELS: return ModelInfo(provider='anthropic', model=split[0], separator='/') # return as model only return ModelInfo(provider='', model=model, separator='') provider = split[0] model_id = separator.join(split[1:]) return ModelInfo(provider=provider, model=model_id, separator=separator) def organize_models_and_providers( models: list[str], ) -> dict[str, 'ProviderInfo']: """Organize a list of model identifiers by provider. Args: models: List of model identifiers Returns: A mapping of providers to their information and models """ result_dict: dict[str, ProviderInfo] = {} for model in models: extracted = extract_model_and_provider(model) separator = extracted.separator provider = extracted.provider model_id = extracted.model # Ignore "anthropic" providers with a separator of "." # These are outdated and incompatible providers. if provider == 'anthropic' and separator == '.': continue key = provider or 'other' if key not in result_dict: result_dict[key] = ProviderInfo(separator=separator, models=[]) result_dict[key].models.append(model_id) return result_dict VERIFIED_PROVIDERS = ['openai', 'azure', 'anthropic', 'deepseek'] VERIFIED_OPENAI_MODELS = [ 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4', 'gpt-4-32k', 'o1-mini', 'o1', 'o3-mini', 'o3-mini-2025-01-31', ] VERIFIED_ANTHROPIC_MODELS = [ 'claude-2', 'claude-2.1', 'claude-3-5-sonnet-20240620', 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-haiku-20240307', 'claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-7-sonnet-20250219', 'claude-sonnet-4-20250514', 'claude-opus-4-20250514', ] class ProviderInfo(BaseModel): """Information about a provider and its models.""" separator: str = Field(description='The separator used in model identifiers') models: list[str] = Field( default_factory=list, description='List of model identifiers' ) def __getitem__(self, key: str) -> str | list[str]: """Allow dictionary-like access to fields.""" if key == 'separator': return self.separator elif key == 'models': return self.models raise KeyError(f'ProviderInfo has no key {key}') def get(self, key: str, default: None = None) -> str | list[str] | None: """Dictionary-like get method with default value.""" try: return self[key] except KeyError: return default def is_number(char: str) -> bool: return char.isdigit() def split_is_actually_version(split: list[str]) -> bool: return ( len(split) > 1 and bool(split[1]) and bool(split[1][0]) and is_number(split[1][0]) ) def read_file(file_path: str | Path) -> str: with open(file_path, 'r') as f: return f.read() def write_to_file(file_path: str | Path, content: str) -> None: with open(file_path, 'w') as f: f.write(content)