Files

1 line
292 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "browser-use/browser-use-rsi", "files": {"browser_use/config.py": {"122feb91f41f66cdc17a54e3c6b6e807f317eb91": {"t": 1785745432.610027, "n": 16330, "h": "33ea455257cd95b3", "c": "\"\"\"Configuration system for browser-use with automatic migration support.\"\"\"\n\nimport json\nimport logging\nimport os\nfrom datetime import datetime\nfrom functools import cache\nfrom pathlib import Path\nfrom typing import Any\nfrom uuid import uuid4\n\nimport psutil\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass OldConfig:\n\t\"\"\"Original lazy-loading configuration class for environment variables.\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_API_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\t@property\n\tdef BROWSER_USE_EXTENSIONS_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'extensions'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'extensions').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef DEFAULT_LLM(self) -> str:\n\t\treturn os.getenv('DEFAULT_LLM', '')\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\nclass FlatEnvConfig(BaseSettings):\n\t\"\"\"All environment variables in a flat namespace.\"\"\"\n\n\tmodel_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True, extra='allow')\n\n\t# Logging and telemetry\n\tBROWSER_USE_LOGGING_LEVEL: str = Field(default='info')\n\tCDP_LOGGING_LEVEL: str = Field(default='WARNING')\n\tBROWSER_USE_DEBUG_LOG_FILE: str | None = Field(default=None)\n\tBROWSER_USE_INFO_LOG_FILE: str | None = Field(default=None)\n\tANONYMIZED_TELEMETRY: bool = Field(default=True)\n\tBROWSER_USE_CLOUD_SYNC: bool | None = Field(default=None)\n\tBROWSER_USE_CLOUD_API_URL: str = Field(default='https://api.browser-use.com')\n\tBROWSER_USE_CLOUD_UI_URL: str = Field(default='')\n\n\t# Path configuration\n\tXDG_CACHE_HOME: str = Field(default='~/.cache')\n\tXDG_CONFIG_HOME: str = Field(default='~/.config')\n\tBROWSER_USE_CONFIG_DIR: str | None = Field(default=None)\n\n\t# LLM API keys\n\tOPENAI_API_KEY: str = Field(default='')\n\tANTHROPIC_API_KEY: str = Field(default='')\n\tGOOGLE_API_KEY: str = Field(default='')\n\tDEEPSEEK_API_KEY: str = Field(default='')\n\tGROK_API_KEY: str = Field(default='')\n\tNOVITA_API_KEY: str = Field(default='')\n\tAZURE_OPENAI_ENDPOINT: str = Field(default='')\n\tAZURE_OPENAI_KEY: str = Field(default='')\n\tSKIP_LLM_API_KEY_VERIFICATION: bool = Field(default=False)\n\tDEFAULT_LLM: str = Field(default='')\n\n\t# Runtime hints\n\tIN_DOCKER: bool | None = Field(default=None)\n\tIS_IN_EVALS: bool = Field(default=False)\n\tWIN_FONT_DIR: str = Field(default='C:\\\\Windows\\\\Fonts')\n\n\t# MCP-specific env vars\n\tBROWSER_USE_CONFIG_PATH: str | None = Field(default=None)\n\tBROWSER_USE_HEADLESS: bool | None = Field(default=None)\n\tBROWSER_USE_ALLOWED_DOMAINS: str | None = Field(default=None)\n\tBROWSER_USE_LLM_MODEL: str | None = Field(default=None)\n\n\t# Proxy env vars\n\tBROWSER_USE_PROXY_URL: str | None = Field(default=None)\n\tBROWSER_USE_NO_PROXY: str | None = Field(default=None)\n\tBROWSER_USE_PROXY_USERNAME: str | None = Field(default=None)\n\tBROWSER_USE_PROXY_PASSWORD: str | None = Field(default=None)\n\n\nclass DBStyleEntry(BaseModel):\n\t\"\"\"Database-style entry with UUID and metadata.\"\"\"\n\n\tid: str = Field(default_factory=lambda: str(uuid4()))\n\tdefault: bool = Field(default=False)\n\tcreated_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n\n\nclass BrowserProfileEntry(DBStyleEntry):\n\t\"\"\"Browser profile configuration entry - accepts any BrowserProfile fields.\"\"\"\n\n\tmodel_config = ConfigDict(extra='allow')\n\n\t# Common browser profile fields for reference\n\theadless: bool | None = None\n\tuser_data_dir: str | None = None\n\tallowed_domains: list[str] | None = None\n\tdownloads_path: str | None = None\n\n\nclass LLMEntry(DBStyleEntry):\n\t\"\"\"LLM configuration entry.\"\"\"\n\n\tapi_key: str | None = None\n\tmodel: str | None = None\n\ttemperature: float | None = None\n\tmax_tokens: int | None = None\n\n\nclass AgentEntry(DBStyleEntry):\n\t\"\"\"Agent configuration entry.\"\"\"\n\n\tmax_steps: int | None = None\n\tuse_vision: bool | None = None\n\tsystem_prompt: str | None = None\n\n\nclass DBStyleConfigJSON(BaseModel):\n\t\"\"\"New database-style configuration format.\"\"\"\n\n\tbrowser_profile: dict[str, BrowserProfileEntry] = Field(default_factory=dict)\n\tllm: dict[str, LLMEntry] = Field(default_factory=dict)\n\tagent: dict[str, AgentEntry] = Field(default_factory=dict)\n\n\ndef create_default_config() -> DBStyleConfigJSON:\n\t\"\"\"Create a fresh default configuration.\"\"\"\n\tlogger.debug('Creating fresh default config.json')\n\n\tnew_config = DBStyleConfigJSON()\n\n\t# Generate default IDs\n\tprofile_id = str(uuid4())\n\tllm_id = str(uuid4())\n\tagent_id = str(uuid4())\n\n\t# Create default browser profile entry\n\tnew_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)\n\n\t# Create default LLM entry\n\tnew_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4o', api_key='your-openai-api-key-here')\n\n\t# Create default agent entry\n\tnew_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)\n\n\treturn new_config\n\n\ndef load_and_migrate_config(config_path: Path) -> DBStyleConfigJSON:\n\t\"\"\"Load config.json or create fresh one if old format detected.\"\"\"\n\tif not config_path.exists():\n\t\t# Create fresh config with defaults\n\t\tconfig_path.parent.mkdir(parents=True, exist_ok=True)\n\t\tnew_config = create_default_config()\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\treturn new_config\n\n\ttry:\n\t\twith open(config_path) as f:\n\t\t\tdata = json.load(f)\n\n\t\t# Check if it's already in DB-style format\n\t\tif all(key in data for key in ['browser_profile', 'llm', 'agent']) and all(\n\t\t\tisinstance(data.get(key, {}), dict) for key in ['browser_profile', 'llm', 'agent']\n\t\t):\n\t\t\t# Check if the values are DB-style entries (have UUIDs as keys)\n\t\t\tif data.get('browser_profile') and all(isinstance(v, dict) and 'id' in v for v in data['browser_profile'].values()):\n\t\t\t\t# Already in new format\n\t\t\t\treturn DBStyleConfigJSON(**data)\n\n\t\t# Old format detected - delete it and create fresh config\n\t\tlogger.debug(f'Old config format detected at {config_path}, creating fresh config')\n\t\tnew_config = create_default_config()\n\n\t\t# Overwrite with new config\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\n\t\tlogger.debug(f'Created fresh config.json at {config_path}')\n\t\treturn new_config\n\n\texcept Exception as e:\n\t\tlogger.error(f'Failed to load config from {config_path}: {e}, creating fresh config')\n\t\t# On any error, create fresh config\n\t\tnew_config = create_default_config()\n\t\ttry:\n\t\t\twith open(config_path, 'w') as f:\n\t\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\texcept Exception as write_error:\n\t\t\tlogger.error(f'Failed to write fresh config: {write_error}')\n\t\treturn new_config\n\n\nclass Config:\n\t\"\"\"Backward-compatible configuration class that merges all config sources.\n\n\tRe-reads environment variables on every access to maintain compatibility.\n\t\"\"\"\n\n\tdef __init__(self):\n\t\t# Cache for directory creation tracking only\n\t\tself._dirs_created = False\n\n\tdef __getattr__(self, name: str) -> Any:\n\t\t\"\"\"Dynamically proxy all attributes to fresh instances.\n\n\t\tThis ensures env vars are re-read on every access.\n\t\t\"\"\"\n\t\t# Special handling for internal attributes\n\t\tif name.startswith('_'):\n\t\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\t\t# Create fresh instances on every access\n\t\told_config = OldConfig()\n\n\t\t# Always use old config for all attributes (it handles env vars with proper transformations)\n\t\tif hasattr(old_config, name):\n\t\t\treturn getattr(old_config, name)\n\n\t\t# For new MCP-specific attributes not in old config\n\t\tenv_config = FlatEnvConfig()\n\t\tif hasattr(env_config, name):\n\t\t\treturn getattr(env_config, name)\n\n\t\t# Handle special methods\n\t\tif name == 'get_default_profile':\n\t\t\treturn lambda: self._get_default_profile()\n\t\telif name == 'get_default_llm':\n\t\t\treturn lambda: self._get_default_llm()\n\t\telif name == 'get_default_agent':\n\t\t\treturn lambda: self._get_default_agent()\n\t\telif name == 'load_config':\n\t\t\treturn lambda: self._load_config()\n\t\telif name == '_ensure_dirs':\n\t\t\treturn lambda: old_config._ensure_dirs()\n\n\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\tdef _get_config_path(self) -> Path:\n\t\t\"\"\"Get config path from fresh env config.\"\"\"\n\t\tenv_config = FlatEnvConfig()\n\t\tif env_config.BROWSER_USE_CONFIG_PATH:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_PATH).expanduser()\n\t\telif env_config.BROWSER_USE_CONFIG_DIR:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_DIR).expanduser() / 'config.json'\n\t\telse:\n\t\t\txdg_config = Path(env_config.XDG_CONFIG_HOME).expanduser()\n\t\t\treturn xdg_config / 'browseruse' / 'config.json'\n\n\tdef _get_db_config(self) -> DBStyleConfigJSON:\n\t\t\"\"\"Load and migrate config.json.\"\"\"\n\t\tconfig_path = self._get_config_path()\n\t\treturn load_and_migrate_config(config_path)\n\n\tdef _get_default_profile(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default browser profile configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor profile in db_config.browser_profile.values():\n\t\t\tif profile.default:\n\t\t\t\treturn profile.model_dump(exclude_none=True)\n\n\t\t# Return first profile if no default\n\t\tif db_config.browser_profile:\n\t\t\treturn next(iter(db_config.browser_profile.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_llm(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default LLM configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor llm in db_config.llm.values():\n\t\t\tif llm.default:\n\t\t\t\treturn llm.model_dump(exclude_none=True)\n\n\t\t# Return first LLM if no default\n\t\tif db_config.llm:\n\t\t\treturn next(iter(db_config.llm.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_agent(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default agent configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor agent in db_config.agent.values():\n\t\t\tif agent.default:\n\t\t\t\treturn agent.model_dump(exclude_none=True)\n\n\t\t# Return first agent if no default\n\t\tif db_config.agent:\n\t\t\treturn next(iter(db_config.agent.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _load_config(self) -> dict[str, Any]:\n\t\t\"\"\"Load configuration with env var overrides for MCP components.\"\"\"\n\t\tconfig = {\n\t\t\t'browser_profile': self._get_default_profile(),\n\t\t\t'llm': self._get_default_llm(),\n\t\t\t'agent': self._get_default_agent(),\n\t\t}\n\n\t\t# Fresh env config for overrides\n\t\tenv_config = FlatEnvConfig()\n\n\t\t# Apply MCP-specific env var overrides\n\t\tif env_config.BROWSER_USE_HEADLESS is not None:\n\t\t\tconfig['browser_profile']['headless'] = env_config.BROWSER_USE_HEADLESS\n\n\t\tif env_config.BROWSER_USE_ALLOWED_DOMAINS:\n\t\t\tdomains = [d.strip() for d in env_config.BROWSER_USE_ALLOWED_DOMAINS.split(',') if d.strip()]\n\t\t\tconfig['browser_profile']['allowed_domains'] = domains\n\n\t\t# Proxy settings (Chromium) -> consolidated `proxy` dict\n\t\tproxy_dict: dict[str, Any] = {}\n\t\tif env_config.BROWSER_USE_PROXY_URL:\n\t\t\tproxy_dict['server'] = env_config.BROWSER_USE_PROXY_URL\n\t\tif env_config.BROWSER_USE_NO_PROXY:\n\t\t\t# store bypass as comma-separated string to match Chrome flag\n\t\t\tproxy_dict['bypass'] = ','.join([d.strip() for d in env_config.BROWSER_USE_NO_PROXY.split(',') if d.strip()])\n\t\tif env_config.BROWSER_USE_PROXY_USERNAME:\n\t\t\tproxy_dict['username'] = env_config.BROWSER_USE_PROXY_USERNAME\n\t\tif env_config.BROWSER_USE_PROXY_PASSWORD:\n\t\t\tproxy_dict['password'] = env_config.BROWSER_USE_PROXY_PASSWORD\n\t\tif proxy_dict:\n\t\t\t# ensure section exists\n\t\t\tconfig.setdefault('browser_profile', {})\n\t\t\tconfig['browser_profile']['proxy'] = proxy_dict\n\n\t\tif env_config.OPENAI_API_KEY:\n\t\t\tconfig['llm']['api_key'] = env_config.OPENAI_API_KEY\n\n\t\tif env_config.BROWSER_USE_LLM_MODEL:\n\t\t\tconfig['llm']['model'] = env_config.BROWSER_USE_LLM_MODEL\n\n\t\treturn config\n\n\n# Create singleton instance\nCONFIG = Config()\n\n\n# Helper functions for MCP components\ndef load_browser_use_config() -> dict[str, Any]:\n\t\"\"\"Load browser-use configuration for MCP components.\"\"\"\n\treturn CONFIG.load_config()\n\n\ndef get_default_profile(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default browser profile from config dict.\"\"\"\n\treturn config.get('browser_profile', {})\n\n\ndef get_default_llm(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default LLM config from config dict.\"\"\"\n\treturn config.get('llm', {})\n"}, "570b15d26ae7df0b73c0250befb95172a77f24a3": {"t": 1785753137.3114514, "n": 16330, "h": "33ea455257cd95b3", "c": "\"\"\"Configuration system for browser-use with automatic migration support.\"\"\"\n\nimport json\nimport logging\nimport os\nfrom datetime import datetime\nfrom functools import cache\nfrom pathlib import Path\nfrom typing import Any\nfrom uuid import uuid4\n\nimport psutil\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass OldConfig:\n\t\"\"\"Original lazy-loading configuration class for environment variables.\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_API_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\t@property\n\tdef BROWSER_USE_EXTENSIONS_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'extensions'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'extensions').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef DEFAULT_LLM(self) -> str:\n\t\treturn os.getenv('DEFAULT_LLM', '')\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\nclass FlatEnvConfig(BaseSettings):\n\t\"\"\"All environment variables in a flat namespace.\"\"\"\n\n\tmodel_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True, extra='allow')\n\n\t# Logging and telemetry\n\tBROWSER_USE_LOGGING_LEVEL: str = Field(default='info')\n\tCDP_LOGGING_LEVEL: str = Field(default='WARNING')\n\tBROWSER_USE_DEBUG_LOG_FILE: str | None = Field(default=None)\n\tBROWSER_USE_INFO_LOG_FILE: str | None = Field(default=None)\n\tANONYMIZED_TELEMETRY: bool = Field(default=True)\n\tBROWSER_USE_CLOUD_SYNC: bool | None = Field(default=None)\n\tBROWSER_USE_CLOUD_API_URL: str = Field(default='https://api.browser-use.com')\n\tBROWSER_USE_CLOUD_UI_URL: str = Field(default='')\n\n\t# Path configuration\n\tXDG_CACHE_HOME: str = Field(default='~/.cache')\n\tXDG_CONFIG_HOME: str = Field(default='~/.config')\n\tBROWSER_USE_CONFIG_DIR: str | None = Field(default=None)\n\n\t# LLM API keys\n\tOPENAI_API_KEY: str = Field(default='')\n\tANTHROPIC_API_KEY: str = Field(default='')\n\tGOOGLE_API_KEY: str = Field(default='')\n\tDEEPSEEK_API_KEY: str = Field(default='')\n\tGROK_API_KEY: str = Field(default='')\n\tNOVITA_API_KEY: str = Field(default='')\n\tAZURE_OPENAI_ENDPOINT: str = Field(default='')\n\tAZURE_OPENAI_KEY: str = Field(default='')\n\tSKIP_LLM_API_KEY_VERIFICATION: bool = Field(default=False)\n\tDEFAULT_LLM: str = Field(default='')\n\n\t# Runtime hints\n\tIN_DOCKER: bool | None = Field(default=None)\n\tIS_IN_EVALS: bool = Field(default=False)\n\tWIN_FONT_DIR: str = Field(default='C:\\\\Windows\\\\Fonts')\n\n\t# MCP-specific env vars\n\tBROWSER_USE_CONFIG_PATH: str | None = Field(default=None)\n\tBROWSER_USE_HEADLESS: bool | None = Field(default=None)\n\tBROWSER_USE_ALLOWED_DOMAINS: str | None = Field(default=None)\n\tBROWSER_USE_LLM_MODEL: str | None = Field(default=None)\n\n\t# Proxy env vars\n\tBROWSER_USE_PROXY_URL: str | None = Field(default=None)\n\tBROWSER_USE_NO_PROXY: str | None = Field(default=None)\n\tBROWSER_USE_PROXY_USERNAME: str | None = Field(default=None)\n\tBROWSER_USE_PROXY_PASSWORD: str | None = Field(default=None)\n\n\nclass DBStyleEntry(BaseModel):\n\t\"\"\"Database-style entry with UUID and metadata.\"\"\"\n\n\tid: str = Field(default_factory=lambda: str(uuid4()))\n\tdefault: bool = Field(default=False)\n\tcreated_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n\n\nclass BrowserProfileEntry(DBStyleEntry):\n\t\"\"\"Browser profile configuration entry - accepts any BrowserProfile fields.\"\"\"\n\n\tmodel_config = ConfigDict(extra='allow')\n\n\t# Common browser profile fields for reference\n\theadless: bool | None = None\n\tuser_data_dir: str | None = None\n\tallowed_domains: list[str] | None = None\n\tdownloads_path: str | None = None\n\n\nclass LLMEntry(DBStyleEntry):\n\t\"\"\"LLM configuration entry.\"\"\"\n\n\tapi_key: str | None = None\n\tmodel: str | None = None\n\ttemperature: float | None = None\n\tmax_tokens: int | None = None\n\n\nclass AgentEntry(DBStyleEntry):\n\t\"\"\"Agent configuration entry.\"\"\"\n\n\tmax_steps: int | None = None\n\tuse_vision: bool | None = None\n\tsystem_prompt: str | None = None\n\n\nclass DBStyleConfigJSON(BaseModel):\n\t\"\"\"New database-style configuration format.\"\"\"\n\n\tbrowser_profile: dict[str, BrowserProfileEntry] = Field(default_factory=dict)\n\tllm: dict[str, LLMEntry] = Field(default_factory=dict)\n\tagent: dict[str, AgentEntry] = Field(default_factory=dict)\n\n\ndef create_default_config() -> DBStyleConfigJSON:\n\t\"\"\"Create a fresh default configuration.\"\"\"\n\tlogger.debug('Creating fresh default config.json')\n\n\tnew_config = DBStyleConfigJSON()\n\n\t# Generate default IDs\n\tprofile_id = str(uuid4())\n\tllm_id = str(uuid4())\n\tagent_id = str(uuid4())\n\n\t# Create default browser profile entry\n\tnew_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)\n\n\t# Create default LLM entry\n\tnew_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4o', api_key='your-openai-api-key-here')\n\n\t# Create default agent entry\n\tnew_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)\n\n\treturn new_config\n\n\ndef load_and_migrate_config(config_path: Path) -> DBStyleConfigJSON:\n\t\"\"\"Load config.json or create fresh one if old format detected.\"\"\"\n\tif not config_path.exists():\n\t\t# Create fresh config with defaults\n\t\tconfig_path.parent.mkdir(parents=True, exist_ok=True)\n\t\tnew_config = create_default_config()\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\treturn new_config\n\n\ttry:\n\t\twith open(config_path) as f:\n\t\t\tdata = json.load(f)\n\n\t\t# Check if it's already in DB-style format\n\t\tif all(key in data for key in ['browser_profile', 'llm', 'agent']) and all(\n\t\t\tisinstance(data.get(key, {}), dict) for key in ['browser_profile', 'llm', 'agent']\n\t\t):\n\t\t\t# Check if the values are DB-style entries (have UUIDs as keys)\n\t\t\tif data.get('browser_profile') and all(isinstance(v, dict) and 'id' in v for v in data['browser_profile'].values()):\n\t\t\t\t# Already in new format\n\t\t\t\treturn DBStyleConfigJSON(**data)\n\n\t\t# Old format detected - delete it and create fresh config\n\t\tlogger.debug(f'Old config format detected at {config_path}, creating fresh config')\n\t\tnew_config = create_default_config()\n\n\t\t# Overwrite with new config\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\n\t\tlogger.debug(f'Created fresh config.json at {config_path}')\n\t\treturn new_config\n\n\texcept Exception as e:\n\t\tlogger.error(f'Failed to load config from {config_path}: {e}, creating fresh config')\n\t\t# On any error, create fresh config\n\t\tnew_config = create_default_config()\n\t\ttry:\n\t\t\twith open(config_path, 'w') as f:\n\t\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\texcept Exception as write_error:\n\t\t\tlogger.error(f'Failed to write fresh config: {write_error}')\n\t\treturn new_config\n\n\nclass Config:\n\t\"\"\"Backward-compatible configuration class that merges all config sources.\n\n\tRe-reads environment variables on every access to maintain compatibility.\n\t\"\"\"\n\n\tdef __init__(self):\n\t\t# Cache for directory creation tracking only\n\t\tself._dirs_created = False\n\n\tdef __getattr__(self, name: str) -> Any:\n\t\t\"\"\"Dynamically proxy all attributes to fresh instances.\n\n\t\tThis ensures env vars are re-read on every access.\n\t\t\"\"\"\n\t\t# Special handling for internal attributes\n\t\tif name.startswith('_'):\n\t\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\t\t# Create fresh instances on every access\n\t\told_config = OldConfig()\n\n\t\t# Always use old config for all attributes (it handles env vars with proper transformations)\n\t\tif hasattr(old_config, name):\n\t\t\treturn getattr(old_config, name)\n\n\t\t# For new MCP-specific attributes not in old config\n\t\tenv_config = FlatEnvConfig()\n\t\tif hasattr(env_config, name):\n\t\t\treturn getattr(env_config, name)\n\n\t\t# Handle special methods\n\t\tif name == 'get_default_profile':\n\t\t\treturn lambda: self._get_default_profile()\n\t\telif name == 'get_default_llm':\n\t\t\treturn lambda: self._get_default_llm()\n\t\telif name == 'get_default_agent':\n\t\t\treturn lambda: self._get_default_agent()\n\t\telif name == 'load_config':\n\t\t\treturn lambda: self._load_config()\n\t\telif name == '_ensure_dirs':\n\t\t\treturn lambda: old_config._ensure_dirs()\n\n\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\tdef _get_config_path(self) -> Path:\n\t\t\"\"\"Get config path from fresh env config.\"\"\"\n\t\tenv_config = FlatEnvConfig()\n\t\tif env_config.BROWSER_USE_CONFIG_PATH:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_PATH).expanduser()\n\t\telif env_config.BROWSER_USE_CONFIG_DIR:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_DIR).expanduser() / 'config.json'\n\t\telse:\n\t\t\txdg_config = Path(env_config.XDG_CONFIG_HOME).expanduser()\n\t\t\treturn xdg_config / 'browseruse' / 'config.json'\n\n\tdef _get_db_config(self) -> DBStyleConfigJSON:\n\t\t\"\"\"Load and migrate config.json.\"\"\"\n\t\tconfig_path = self._get_config_path()\n\t\treturn load_and_migrate_config(config_path)\n\n\tdef _get_default_profile(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default browser profile configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor profile in db_config.browser_profile.values():\n\t\t\tif profile.default:\n\t\t\t\treturn profile.model_dump(exclude_none=True)\n\n\t\t# Return first profile if no default\n\t\tif db_config.browser_profile:\n\t\t\treturn next(iter(db_config.browser_profile.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_llm(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default LLM configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor llm in db_config.llm.values():\n\t\t\tif llm.default:\n\t\t\t\treturn llm.model_dump(exclude_none=True)\n\n\t\t# Return first LLM if no default\n\t\tif db_config.llm:\n\t\t\treturn next(iter(db_config.llm.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_agent(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default agent configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor agent in db_config.agent.values():\n\t\t\tif agent.default:\n\t\t\t\treturn agent.model_dump(exclude_none=True)\n\n\t\t# Return first agent if no default\n\t\tif db_config.agent:\n\t\t\treturn next(iter(db_config.agent.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _load_config(self) -> dict[str, Any]:\n\t\t\"\"\"Load configuration with env var overrides for MCP components.\"\"\"\n\t\tconfig = {\n\t\t\t'browser_profile': self._get_default_profile(),\n\t\t\t'llm': self._get_default_llm(),\n\t\t\t'agent': self._get_default_agent(),\n\t\t}\n\n\t\t# Fresh env config for overrides\n\t\tenv_config = FlatEnvConfig()\n\n\t\t# Apply MCP-specific env var overrides\n\t\tif env_config.BROWSER_USE_HEADLESS is not None:\n\t\t\tconfig['browser_profile']['headless'] = env_config.BROWSER_USE_HEADLESS\n\n\t\tif env_config.BROWSER_USE_ALLOWED_DOMAINS:\n\t\t\tdomains = [d.strip() for d in env_config.BROWSER_USE_ALLOWED_DOMAINS.split(',') if d.strip()]\n\t\t\tconfig['browser_profile']['allowed_domains'] = domains\n\n\t\t# Proxy settings (Chromium) -> consolidated `proxy` dict\n\t\tproxy_dict: dict[str, Any] = {}\n\t\tif env_config.BROWSER_USE_PROXY_URL:\n\t\t\tproxy_dict['server'] = env_config.BROWSER_USE_PROXY_URL\n\t\tif env_config.BROWSER_USE_NO_PROXY:\n\t\t\t# store bypass as comma-separated string to match Chrome flag\n\t\t\tproxy_dict['bypass'] = ','.join([d.strip() for d in env_config.BROWSER_USE_NO_PROXY.split(',') if d.strip()])\n\t\tif env_config.BROWSER_USE_PROXY_USERNAME:\n\t\t\tproxy_dict['username'] = env_config.BROWSER_USE_PROXY_USERNAME\n\t\tif env_config.BROWSER_USE_PROXY_PASSWORD:\n\t\t\tproxy_dict['password'] = env_config.BROWSER_USE_PROXY_PASSWORD\n\t\tif proxy_dict:\n\t\t\t# ensure section exists\n\t\t\tconfig.setdefault('browser_profile', {})\n\t\t\tconfig['browser_profile']['proxy'] = proxy_dict\n\n\t\tif env_config.OPENAI_API_KEY:\n\t\t\tconfig['llm']['api_key'] = env_config.OPENAI_API_KEY\n\n\t\tif env_config.BROWSER_USE_LLM_MODEL:\n\t\t\tconfig['llm']['model'] = env_config.BROWSER_USE_LLM_MODEL\n\n\t\treturn config\n\n\n# Create singleton instance\nCONFIG = Config()\n\n\n# Helper functions for MCP components\ndef load_browser_use_config() -> dict[str, Any]:\n\t\"\"\"Load browser-use configuration for MCP components.\"\"\"\n\treturn CONFIG.load_config()\n\n\ndef get_default_profile(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default browser profile from config dict.\"\"\"\n\treturn config.get('browser_profile', {})\n\n\ndef get_default_llm(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default LLM config from config dict.\"\"\"\n\treturn config.get('llm', {})\n"}, "146b7ff423ff30df7841b3642483bde236d87b6f": {"t": 1785753138.6742945, "n": 16211, "h": "5ed654ac7a4ad01f", "c": "\"\"\"Configuration system for browser-use with automatic migration support.\"\"\"\n\nimport json\nimport logging\nimport os\nfrom datetime import datetime\nfrom functools import cache\nfrom pathlib import Path\nfrom typing import Any\nfrom uuid import uuid4\n\nimport psutil\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass OldConfig:\n\t\"\"\"Original lazy-loading configuration class for environment variables.\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_API_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\t@property\n\tdef BROWSER_USE_EXTENSIONS_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'extensions'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'extensions').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\nclass FlatEnvConfig(BaseSettings):\n\t\"\"\"All environment variables in a flat namespace.\"\"\"\n\n\tmodel_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True, extra='allow')\n\n\t# Logging and telemetry\n\tBROWSER_USE_LOGGING_LEVEL: str = Field(default='info')\n\tCDP_LOGGING_LEVEL: str = Field(default='WARNING')\n\tBROWSER_USE_DEBUG_LOG_FILE: str | None = Field(default=None)\n\tBROWSER_USE_INFO_LOG_FILE: str | None = Field(default=None)\n\tANONYMIZED_TELEMETRY: bool = Field(default=True)\n\tBROWSER_USE_CLOUD_SYNC: bool | None = Field(default=None)\n\tBROWSER_USE_CLOUD_API_URL: str = Field(default='https://api.browser-use.com')\n\tBROWSER_USE_CLOUD_UI_URL: str = Field(default='')\n\n\t# Path configuration\n\tXDG_CACHE_HOME: str = Field(default='~/.cache')\n\tXDG_CONFIG_HOME: str = Field(default='~/.config')\n\tBROWSER_USE_CONFIG_DIR: str | None = Field(default=None)\n\n\t# LLM API keys\n\tOPENAI_API_KEY: str = Field(default='')\n\tANTHROPIC_API_KEY: str = Field(default='')\n\tGOOGLE_API_KEY: str = Field(default='')\n\tDEEPSEEK_API_KEY: str = Field(default='')\n\tGROK_API_KEY: str = Field(default='')\n\tNOVITA_API_KEY: str = Field(default='')\n\tAZURE_OPENAI_ENDPOINT: str = Field(default='')\n\tAZURE_OPENAI_KEY: str = Field(default='')\n\tSKIP_LLM_API_KEY_VERIFICATION: bool = Field(default=False)\n\n\t# Runtime hints\n\tIN_DOCKER: bool | None = Field(default=None)\n\tIS_IN_EVALS: bool = Field(default=False)\n\tWIN_FONT_DIR: str = Field(default='C:\\\\Windows\\\\Fonts')\n\n\t# MCP-specific env vars\n\tBROWSER_USE_CONFIG_PATH: str | None = Field(default=None)\n\tBROWSER_USE_HEADLESS: bool | None = Field(default=None)\n\tBROWSER_USE_ALLOWED_DOMAINS: str | None = Field(default=None)\n\tBROWSER_USE_LLM_MODEL: str | None = Field(default=None)\n\n\t# Proxy env vars\n\tBROWSER_USE_PROXY_URL: str | None = Field(default=None)\n\tBROWSER_USE_NO_PROXY: str | None = Field(default=None)\n\tBROWSER_USE_PROXY_USERNAME: str | None = Field(default=None)\n\tBROWSER_USE_PROXY_PASSWORD: str | None = Field(default=None)\n\n\nclass DBStyleEntry(BaseModel):\n\t\"\"\"Database-style entry with UUID and metadata.\"\"\"\n\n\tid: str = Field(default_factory=lambda: str(uuid4()))\n\tdefault: bool = Field(default=False)\n\tcreated_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n\n\nclass BrowserProfileEntry(DBStyleEntry):\n\t\"\"\"Browser profile configuration entry - accepts any BrowserProfile fields.\"\"\"\n\n\tmodel_config = ConfigDict(extra='allow')\n\n\t# Common browser profile fields for reference\n\theadless: bool | None = None\n\tuser_data_dir: str | None = None\n\tallowed_domains: list[str] | None = None\n\tdownloads_path: str | None = None\n\n\nclass LLMEntry(DBStyleEntry):\n\t\"\"\"LLM configuration entry.\"\"\"\n\n\tapi_key: str | None = None\n\tmodel: str | None = None\n\ttemperature: float | None = None\n\tmax_tokens: int | None = None\n\n\nclass AgentEntry(DBStyleEntry):\n\t\"\"\"Agent configuration entry.\"\"\"\n\n\tmax_steps: int | None = None\n\tuse_vision: bool | None = None\n\tsystem_prompt: str | None = None\n\n\nclass DBStyleConfigJSON(BaseModel):\n\t\"\"\"New database-style configuration format.\"\"\"\n\n\tbrowser_profile: dict[str, BrowserProfileEntry] = Field(default_factory=dict)\n\tllm: dict[str, LLMEntry] = Field(default_factory=dict)\n\tagent: dict[str, AgentEntry] = Field(default_factory=dict)\n\n\ndef create_default_config() -> DBStyleConfigJSON:\n\t\"\"\"Create a fresh default configuration.\"\"\"\n\tlogger.debug('Creating fresh default config.json')\n\n\tnew_config = DBStyleConfigJSON()\n\n\t# Generate default IDs\n\tprofile_id = str(uuid4())\n\tllm_id = str(uuid4())\n\tagent_id = str(uuid4())\n\n\t# Create default browser profile entry\n\tnew_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)\n\n\t# Create default LLM entry\n\tnew_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4o', api_key='your-openai-api-key-here')\n\n\t# Create default agent entry\n\tnew_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)\n\n\treturn new_config\n\n\ndef load_and_migrate_config(config_path: Path) -> DBStyleConfigJSON:\n\t\"\"\"Load config.json or create fresh one if old format detected.\"\"\"\n\tif not config_path.exists():\n\t\t# Create fresh config with defaults\n\t\tconfig_path.parent.mkdir(parents=True, exist_ok=True)\n\t\tnew_config = create_default_config()\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\treturn new_config\n\n\ttry:\n\t\twith open(config_path) as f:\n\t\t\tdata = json.load(f)\n\n\t\t# Check if it's already in DB-style format\n\t\tif all(key in data for key in ['browser_profile', 'llm', 'agent']) and all(\n\t\t\tisinstance(data.get(key, {}), dict) for key in ['browser_profile', 'llm', 'agent']\n\t\t):\n\t\t\t# Check if the values are DB-style entries (have UUIDs as keys)\n\t\t\tif data.get('browser_profile') and all(isinstance(v, dict) and 'id' in v for v in data['browser_profile'].values()):\n\t\t\t\t# Already in new format\n\t\t\t\treturn DBStyleConfigJSON(**data)\n\n\t\t# Old format detected - delete it and create fresh config\n\t\tlogger.debug(f'Old config format detected at {config_path}, creating fresh config')\n\t\tnew_config = create_default_config()\n\n\t\t# Overwrite with new config\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\n\t\tlogger.debug(f'Created fresh config.json at {config_path}')\n\t\treturn new_config\n\n\texcept Exception as e:\n\t\tlogger.error(f'Failed to load config from {config_path}: {e}, creating fresh config')\n\t\t# On any error, create fresh config\n\t\tnew_config = create_default_config()\n\t\ttry:\n\t\t\twith open(config_path, 'w') as f:\n\t\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\texcept Exception as write_error:\n\t\t\tlogger.error(f'Failed to write fresh config: {write_error}')\n\t\treturn new_config\n\n\nclass Config:\n\t\"\"\"Backward-compatible configuration class that merges all config sources.\n\n\tRe-reads environment variables on every access to maintain compatibility.\n\t\"\"\"\n\n\tdef __init__(self):\n\t\t# Cache for directory creation tracking only\n\t\tself._dirs_created = False\n\n\tdef __getattr__(self, name: str) -> Any:\n\t\t\"\"\"Dynamically proxy all attributes to fresh instances.\n\n\t\tThis ensures env vars are re-read on every access.\n\t\t\"\"\"\n\t\t# Special handling for internal attributes\n\t\tif name.startswith('_'):\n\t\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\t\t# Create fresh instances on every access\n\t\told_config = OldConfig()\n\n\t\t# Always use old config for all attributes (it handles env vars with proper transformations)\n\t\tif hasattr(old_config, name):\n\t\t\treturn getattr(old_config, name)\n\n\t\t# For new MCP-specific attributes not in old config\n\t\tenv_config = FlatEnvConfig()\n\t\tif hasattr(env_config, name):\n\t\t\treturn getattr(env_config, name)\n\n\t\t# Handle special methods\n\t\tif name == 'get_default_profile':\n\t\t\treturn lambda: self._get_default_profile()\n\t\telif name == 'get_default_llm':\n\t\t\treturn lambda: self._get_default_llm()\n\t\telif name == 'get_default_agent':\n\t\t\treturn lambda: self._get_default_agent()\n\t\telif name == 'load_config':\n\t\t\treturn lambda: self._load_config()\n\t\telif name == '_ensure_dirs':\n\t\t\treturn lambda: old_config._ensure_dirs()\n\n\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\tdef _get_config_path(self) -> Path:\n\t\t\"\"\"Get config path from fresh env config.\"\"\"\n\t\tenv_config = FlatEnvConfig()\n\t\tif env_config.BROWSER_USE_CONFIG_PATH:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_PATH).expanduser()\n\t\telif env_config.BROWSER_USE_CONFIG_DIR:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_DIR).expanduser() / 'config.json'\n\t\telse:\n\t\t\txdg_config = Path(env_config.XDG_CONFIG_HOME).expanduser()\n\t\t\treturn xdg_config / 'browseruse' / 'config.json'\n\n\tdef _get_db_config(self) -> DBStyleConfigJSON:\n\t\t\"\"\"Load and migrate config.json.\"\"\"\n\t\tconfig_path = self._get_config_path()\n\t\treturn load_and_migrate_config(config_path)\n\n\tdef _get_default_profile(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default browser profile configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor profile in db_config.browser_profile.values():\n\t\t\tif profile.default:\n\t\t\t\treturn profile.model_dump(exclude_none=True)\n\n\t\t# Return first profile if no default\n\t\tif db_config.browser_profile:\n\t\t\treturn next(iter(db_config.browser_profile.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_llm(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default LLM configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor llm in db_config.llm.values():\n\t\t\tif llm.default:\n\t\t\t\treturn llm.model_dump(exclude_none=True)\n\n\t\t# Return first LLM if no default\n\t\tif db_config.llm:\n\t\t\treturn next(iter(db_config.llm.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_agent(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default agent configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor agent in db_config.agent.values():\n\t\t\tif agent.default:\n\t\t\t\treturn agent.model_dump(exclude_none=True)\n\n\t\t# Return first agent if no default\n\t\tif db_config.agent:\n\t\t\treturn next(iter(db_config.agent.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _load_config(self) -> dict[str, Any]:\n\t\t\"\"\"Load configuration with env var overrides for MCP components.\"\"\"\n\t\tconfig = {\n\t\t\t'browser_profile': self._get_default_profile(),\n\t\t\t'llm': self._get_default_llm(),\n\t\t\t'agent': self._get_default_agent(),\n\t\t}\n\n\t\t# Fresh env config for overrides\n\t\tenv_config = FlatEnvConfig()\n\n\t\t# Apply MCP-specific env var overrides\n\t\tif env_config.BROWSER_USE_HEADLESS is not None:\n\t\t\tconfig['browser_profile']['headless'] = env_config.BROWSER_USE_HEADLESS\n\n\t\tif env_config.BROWSER_USE_ALLOWED_DOMAINS:\n\t\t\tdomains = [d.strip() for d in env_config.BROWSER_USE_ALLOWED_DOMAINS.split(',') if d.strip()]\n\t\t\tconfig['browser_profile']['allowed_domains'] = domains\n\n\t\t# Proxy settings (Chromium) -> consolidated `proxy` dict\n\t\tproxy_dict: dict[str, Any] = {}\n\t\tif env_config.BROWSER_USE_PROXY_URL:\n\t\t\tproxy_dict['server'] = env_config.BROWSER_USE_PROXY_URL\n\t\tif env_config.BROWSER_USE_NO_PROXY:\n\t\t\t# store bypass as comma-separated string to match Chrome flag\n\t\t\tproxy_dict['bypass'] = ','.join([d.strip() for d in env_config.BROWSER_USE_NO_PROXY.split(',') if d.strip()])\n\t\tif env_config.BROWSER_USE_PROXY_USERNAME:\n\t\t\tproxy_dict['username'] = env_config.BROWSER_USE_PROXY_USERNAME\n\t\tif env_config.BROWSER_USE_PROXY_PASSWORD:\n\t\t\tproxy_dict['password'] = env_config.BROWSER_USE_PROXY_PASSWORD\n\t\tif proxy_dict:\n\t\t\t# ensure section exists\n\t\t\tconfig.setdefault('browser_profile', {})\n\t\t\tconfig['browser_profile']['proxy'] = proxy_dict\n\n\t\tif env_config.OPENAI_API_KEY:\n\t\t\tconfig['llm']['api_key'] = env_config.OPENAI_API_KEY\n\n\t\tif env_config.BROWSER_USE_LLM_MODEL:\n\t\t\tconfig['llm']['model'] = env_config.BROWSER_USE_LLM_MODEL\n\n\t\treturn config\n\n\n# Create singleton instance\nCONFIG = Config()\n\n\n# Helper functions for MCP components\ndef load_browser_use_config() -> dict[str, Any]:\n\t\"\"\"Load browser-use configuration for MCP components.\"\"\"\n\treturn CONFIG.load_config()\n\n\ndef get_default_profile(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default browser profile from config dict.\"\"\"\n\treturn config.get('browser_profile', {})\n\n\ndef get_default_llm(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default LLM config from config dict.\"\"\"\n\treturn config.get('llm', {})\n"}, "1173e2c3ab2fcc2e9831a8f8afa12401ab90e3d0": {"t": 1785753139.9655848, "n": 16088, "h": "c38ec64def7d019b", "c": "\"\"\"Configuration system for browser-use with automatic migration support.\"\"\"\n\nimport json\nimport logging\nimport os\nfrom datetime import datetime\nfrom functools import cache\nfrom pathlib import Path\nfrom typing import Any\nfrom uuid import uuid4\n\nimport psutil\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass OldConfig:\n\t\"\"\"Original lazy-loading configuration class for environment variables.\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_API_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\t@property\n\tdef BROWSER_USE_EXTENSIONS_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'extensions'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'extensions').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\nclass FlatEnvConfig(BaseSettings):\n\t\"\"\"All environment variables in a flat namespace.\"\"\"\n\n\tmodel_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True, extra='allow')\n\n\t# Logging and telemetry\n\tBROWSER_USE_LOGGING_LEVEL: str = Field(default='info')\n\tCDP_LOGGING_LEVEL: str = Field(default='WARNING')\n\tANONYMIZED_TELEMETRY: bool = Field(default=True)\n\tBROWSER_USE_CLOUD_SYNC: bool | None = Field(default=None)\n\tBROWSER_USE_CLOUD_API_URL: str = Field(default='https://api.browser-use.com')\n\tBROWSER_USE_CLOUD_UI_URL: str = Field(default='')\n\n\t# Path configuration\n\tXDG_CACHE_HOME: str = Field(default='~/.cache')\n\tXDG_CONFIG_HOME: str = Field(default='~/.config')\n\tBROWSER_USE_CONFIG_DIR: str | None = Field(default=None)\n\n\t# LLM API keys\n\tOPENAI_API_KEY: str = Field(default='')\n\tANTHROPIC_API_KEY: str = Field(default='')\n\tGOOGLE_API_KEY: str = Field(default='')\n\tDEEPSEEK_API_KEY: str = Field(default='')\n\tGROK_API_KEY: str = Field(default='')\n\tNOVITA_API_KEY: str = Field(default='')\n\tAZURE_OPENAI_ENDPOINT: str = Field(default='')\n\tAZURE_OPENAI_KEY: str = Field(default='')\n\tSKIP_LLM_API_KEY_VERIFICATION: bool = Field(default=False)\n\n\t# Runtime hints\n\tIN_DOCKER: bool | None = Field(default=None)\n\tIS_IN_EVALS: bool = Field(default=False)\n\tWIN_FONT_DIR: str = Field(default='C:\\\\Windows\\\\Fonts')\n\n\t# MCP-specific env vars\n\tBROWSER_USE_CONFIG_PATH: str | None = Field(default=None)\n\tBROWSER_USE_HEADLESS: bool | None = Field(default=None)\n\tBROWSER_USE_ALLOWED_DOMAINS: str | None = Field(default=None)\n\tBROWSER_USE_LLM_MODEL: str | None = Field(default=None)\n\n\t# Proxy env vars\n\tBROWSER_USE_PROXY_URL: str | None = Field(default=None)\n\tBROWSER_USE_NO_PROXY: str | None = Field(default=None)\n\tBROWSER_USE_PROXY_USERNAME: str | None = Field(default=None)\n\tBROWSER_USE_PROXY_PASSWORD: str | None = Field(default=None)\n\n\nclass DBStyleEntry(BaseModel):\n\t\"\"\"Database-style entry with UUID and metadata.\"\"\"\n\n\tid: str = Field(default_factory=lambda: str(uuid4()))\n\tdefault: bool = Field(default=False)\n\tcreated_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n\n\nclass BrowserProfileEntry(DBStyleEntry):\n\t\"\"\"Browser profile configuration entry - accepts any BrowserProfile fields.\"\"\"\n\n\tmodel_config = ConfigDict(extra='allow')\n\n\t# Common browser profile fields for reference\n\theadless: bool | None = None\n\tuser_data_dir: str | None = None\n\tallowed_domains: list[str] | None = None\n\tdownloads_path: str | None = None\n\n\nclass LLMEntry(DBStyleEntry):\n\t\"\"\"LLM configuration entry.\"\"\"\n\n\tapi_key: str | None = None\n\tmodel: str | None = None\n\ttemperature: float | None = None\n\tmax_tokens: int | None = None\n\n\nclass AgentEntry(DBStyleEntry):\n\t\"\"\"Agent configuration entry.\"\"\"\n\n\tmax_steps: int | None = None\n\tuse_vision: bool | None = None\n\tsystem_prompt: str | None = None\n\n\nclass DBStyleConfigJSON(BaseModel):\n\t\"\"\"New database-style configuration format.\"\"\"\n\n\tbrowser_profile: dict[str, BrowserProfileEntry] = Field(default_factory=dict)\n\tllm: dict[str, LLMEntry] = Field(default_factory=dict)\n\tagent: dict[str, AgentEntry] = Field(default_factory=dict)\n\n\ndef create_default_config() -> DBStyleConfigJSON:\n\t\"\"\"Create a fresh default configuration.\"\"\"\n\tlogger.debug('Creating fresh default config.json')\n\n\tnew_config = DBStyleConfigJSON()\n\n\t# Generate default IDs\n\tprofile_id = str(uuid4())\n\tllm_id = str(uuid4())\n\tagent_id = str(uuid4())\n\n\t# Create default browser profile entry\n\tnew_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)\n\n\t# Create default LLM entry\n\tnew_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4o', api_key='your-openai-api-key-here')\n\n\t# Create default agent entry\n\tnew_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)\n\n\treturn new_config\n\n\ndef load_and_migrate_config(config_path: Path) -> DBStyleConfigJSON:\n\t\"\"\"Load config.json or create fresh one if old format detected.\"\"\"\n\tif not config_path.exists():\n\t\t# Create fresh config with defaults\n\t\tconfig_path.parent.mkdir(parents=True, exist_ok=True)\n\t\tnew_config = create_default_config()\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\treturn new_config\n\n\ttry:\n\t\twith open(config_path) as f:\n\t\t\tdata = json.load(f)\n\n\t\t# Check if it's already in DB-style format\n\t\tif all(key in data for key in ['browser_profile', 'llm', 'agent']) and all(\n\t\t\tisinstance(data.get(key, {}), dict) for key in ['browser_profile', 'llm', 'agent']\n\t\t):\n\t\t\t# Check if the values are DB-style entries (have UUIDs as keys)\n\t\t\tif data.get('browser_profile') and all(isinstance(v, dict) and 'id' in v for v in data['browser_profile'].values()):\n\t\t\t\t# Already in new format\n\t\t\t\treturn DBStyleConfigJSON(**data)\n\n\t\t# Old format detected - delete it and create fresh config\n\t\tlogger.debug(f'Old config format detected at {config_path}, creating fresh config')\n\t\tnew_config = create_default_config()\n\n\t\t# Overwrite with new config\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\n\t\tlogger.debug(f'Created fresh config.json at {config_path}')\n\t\treturn new_config\n\n\texcept Exception as e:\n\t\tlogger.error(f'Failed to load config from {config_path}: {e}, creating fresh config')\n\t\t# On any error, create fresh config\n\t\tnew_config = create_default_config()\n\t\ttry:\n\t\t\twith open(config_path, 'w') as f:\n\t\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\texcept Exception as write_error:\n\t\t\tlogger.error(f'Failed to write fresh config: {write_error}')\n\t\treturn new_config\n\n\nclass Config:\n\t\"\"\"Backward-compatible configuration class that merges all config sources.\n\n\tRe-reads environment variables on every access to maintain compatibility.\n\t\"\"\"\n\n\tdef __init__(self):\n\t\t# Cache for directory creation tracking only\n\t\tself._dirs_created = False\n\n\tdef __getattr__(self, name: str) -> Any:\n\t\t\"\"\"Dynamically proxy all attributes to fresh instances.\n\n\t\tThis ensures env vars are re-read on every access.\n\t\t\"\"\"\n\t\t# Special handling for internal attributes\n\t\tif name.startswith('_'):\n\t\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\t\t# Create fresh instances on every access\n\t\told_config = OldConfig()\n\n\t\t# Always use old config for all attributes (it handles env vars with proper transformations)\n\t\tif hasattr(old_config, name):\n\t\t\treturn getattr(old_config, name)\n\n\t\t# For new MCP-specific attributes not in old config\n\t\tenv_config = FlatEnvConfig()\n\t\tif hasattr(env_config, name):\n\t\t\treturn getattr(env_config, name)\n\n\t\t# Handle special methods\n\t\tif name == 'get_default_profile':\n\t\t\treturn lambda: self._get_default_profile()\n\t\telif name == 'get_default_llm':\n\t\t\treturn lambda: self._get_default_llm()\n\t\telif name == 'get_default_agent':\n\t\t\treturn lambda: self._get_default_agent()\n\t\telif name == 'load_config':\n\t\t\treturn lambda: self._load_config()\n\t\telif name == '_ensure_dirs':\n\t\t\treturn lambda: old_config._ensure_dirs()\n\n\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\tdef _get_config_path(self) -> Path:\n\t\t\"\"\"Get config path from fresh env config.\"\"\"\n\t\tenv_config = FlatEnvConfig()\n\t\tif env_config.BROWSER_USE_CONFIG_PATH:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_PATH).expanduser()\n\t\telif env_config.BROWSER_USE_CONFIG_DIR:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_DIR).expanduser() / 'config.json'\n\t\telse:\n\t\t\txdg_config = Path(env_config.XDG_CONFIG_HOME).expanduser()\n\t\t\treturn xdg_config / 'browseruse' / 'config.json'\n\n\tdef _get_db_config(self) -> DBStyleConfigJSON:\n\t\t\"\"\"Load and migrate config.json.\"\"\"\n\t\tconfig_path = self._get_config_path()\n\t\treturn load_and_migrate_config(config_path)\n\n\tdef _get_default_profile(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default browser profile configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor profile in db_config.browser_profile.values():\n\t\t\tif profile.default:\n\t\t\t\treturn profile.model_dump(exclude_none=True)\n\n\t\t# Return first profile if no default\n\t\tif db_config.browser_profile:\n\t\t\treturn next(iter(db_config.browser_profile.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_llm(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default LLM configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor llm in db_config.llm.values():\n\t\t\tif llm.default:\n\t\t\t\treturn llm.model_dump(exclude_none=True)\n\n\t\t# Return first LLM if no default\n\t\tif db_config.llm:\n\t\t\treturn next(iter(db_config.llm.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_agent(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default agent configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor agent in db_config.agent.values():\n\t\t\tif agent.default:\n\t\t\t\treturn agent.model_dump(exclude_none=True)\n\n\t\t# Return first agent if no default\n\t\tif db_config.agent:\n\t\t\treturn next(iter(db_config.agent.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _load_config(self) -> dict[str, Any]:\n\t\t\"\"\"Load configuration with env var overrides for MCP components.\"\"\"\n\t\tconfig = {\n\t\t\t'browser_profile': self._get_default_profile(),\n\t\t\t'llm': self._get_default_llm(),\n\t\t\t'agent': self._get_default_agent(),\n\t\t}\n\n\t\t# Fresh env config for overrides\n\t\tenv_config = FlatEnvConfig()\n\n\t\t# Apply MCP-specific env var overrides\n\t\tif env_config.BROWSER_USE_HEADLESS is not None:\n\t\t\tconfig['browser_profile']['headless'] = env_config.BROWSER_USE_HEADLESS\n\n\t\tif env_config.BROWSER_USE_ALLOWED_DOMAINS:\n\t\t\tdomains = [d.strip() for d in env_config.BROWSER_USE_ALLOWED_DOMAINS.split(',') if d.strip()]\n\t\t\tconfig['browser_profile']['allowed_domains'] = domains\n\n\t\t# Proxy settings (Chromium) -> consolidated `proxy` dict\n\t\tproxy_dict: dict[str, Any] = {}\n\t\tif env_config.BROWSER_USE_PROXY_URL:\n\t\t\tproxy_dict['server'] = env_config.BROWSER_USE_PROXY_URL\n\t\tif env_config.BROWSER_USE_NO_PROXY:\n\t\t\t# store bypass as comma-separated string to match Chrome flag\n\t\t\tproxy_dict['bypass'] = ','.join([d.strip() for d in env_config.BROWSER_USE_NO_PROXY.split(',') if d.strip()])\n\t\tif env_config.BROWSER_USE_PROXY_USERNAME:\n\t\t\tproxy_dict['username'] = env_config.BROWSER_USE_PROXY_USERNAME\n\t\tif env_config.BROWSER_USE_PROXY_PASSWORD:\n\t\t\tproxy_dict['password'] = env_config.BROWSER_USE_PROXY_PASSWORD\n\t\tif proxy_dict:\n\t\t\t# ensure section exists\n\t\t\tconfig.setdefault('browser_profile', {})\n\t\t\tconfig['browser_profile']['proxy'] = proxy_dict\n\n\t\tif env_config.OPENAI_API_KEY:\n\t\t\tconfig['llm']['api_key'] = env_config.OPENAI_API_KEY\n\n\t\tif env_config.BROWSER_USE_LLM_MODEL:\n\t\t\tconfig['llm']['model'] = env_config.BROWSER_USE_LLM_MODEL\n\n\t\treturn config\n\n\n# Create singleton instance\nCONFIG = Config()\n\n\n# Helper functions for MCP components\ndef load_browser_use_config() -> dict[str, Any]:\n\t\"\"\"Load browser-use configuration for MCP components.\"\"\"\n\treturn CONFIG.load_config()\n\n\ndef get_default_profile(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default browser profile from config dict.\"\"\"\n\treturn config.get('browser_profile', {})\n\n\ndef get_default_llm(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default LLM config from config dict.\"\"\"\n\treturn config.get('llm', {})\n"}, "4871add4a19421fe80710a7bf7b4612b3558e889": {"t": 1785753141.7382123, "n": 16097, "h": "b5f6f6d96d19616b", "c": "\"\"\"Configuration system for browser-use with automatic migration support.\"\"\"\n\nimport json\nimport logging\nimport os\nfrom datetime import datetime\nfrom functools import cache\nfrom pathlib import Path\nfrom typing import Any\nfrom uuid import uuid4\n\nimport psutil\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass OldConfig:\n\t\"\"\"Original lazy-loading configuration class for environment variables.\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_API_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\t@property\n\tdef BROWSER_USE_EXTENSIONS_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'extensions'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'extensions').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\nclass FlatEnvConfig(BaseSettings):\n\t\"\"\"All environment variables in a flat namespace.\"\"\"\n\n\tmodel_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True, extra='allow')\n\n\t# Logging and telemetry\n\tBROWSER_USE_LOGGING_LEVEL: str = Field(default='info')\n\tCDP_LOGGING_LEVEL: str = Field(default='WARNING')\n\tANONYMIZED_TELEMETRY: bool = Field(default=True)\n\tBROWSER_USE_CLOUD_SYNC: bool | None = Field(default=None)\n\tBROWSER_USE_CLOUD_API_URL: str = Field(default='https://api.browser-use.com')\n\tBROWSER_USE_CLOUD_UI_URL: str = Field(default='')\n\n\t# Path configuration\n\tXDG_CACHE_HOME: str = Field(default='~/.cache')\n\tXDG_CONFIG_HOME: str = Field(default='~/.config')\n\tBROWSER_USE_CONFIG_DIR: str | None = Field(default=None)\n\n\t# LLM API keys\n\tOPENAI_API_KEY: str = Field(default='')\n\tANTHROPIC_API_KEY: str = Field(default='')\n\tGOOGLE_API_KEY: str = Field(default='')\n\tDEEPSEEK_API_KEY: str = Field(default='')\n\tGROK_API_KEY: str = Field(default='')\n\tNOVITA_API_KEY: str = Field(default='')\n\tAZURE_OPENAI_ENDPOINT: str = Field(default='')\n\tAZURE_OPENAI_KEY: str = Field(default='')\n\tSKIP_LLM_API_KEY_VERIFICATION: bool = Field(default=False)\n\n\t# Runtime hints\n\tIN_DOCKER: bool | None = Field(default=None)\n\tIS_IN_EVALS: bool = Field(default=False)\n\tWIN_FONT_DIR: str = Field(default='C:\\\\Windows\\\\Fonts')\n\n\t# MCP-specific env vars\n\tBROWSER_USE_CONFIG_PATH: str | None = Field(default=None)\n\tBROWSER_USE_HEADLESS: bool | None = Field(default=None)\n\tBROWSER_USE_ALLOWED_DOMAINS: str | None = Field(default=None)\n\tBROWSER_USE_LLM_MODEL: str | None = Field(default=None)\n\n\t# Proxy env vars\n\tBROWSER_USE_PROXY_URL: str | None = Field(default=None)\n\tBROWSER_USE_NO_PROXY: str | None = Field(default=None)\n\tBROWSER_USE_PROXY_USERNAME: str | None = Field(default=None)\n\tBROWSER_USE_PROXY_PASSWORD: str | None = Field(default=None)\n\n\nclass DBStyleEntry(BaseModel):\n\t\"\"\"Database-style entry with UUID and metadata.\"\"\"\n\n\tid: str = Field(default_factory=lambda: str(uuid4()))\n\tdefault: bool = Field(default=False)\n\tcreated_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n\n\nclass BrowserProfileEntry(DBStyleEntry):\n\t\"\"\"Browser profile configuration entry - accepts any BrowserProfile fields.\"\"\"\n\n\tmodel_config = ConfigDict(extra='allow')\n\n\t# Common browser profile fields for reference\n\theadless: bool | None = None\n\tuser_data_dir: str | None = None\n\tallowed_domains: list[str] | None = None\n\tdownloads_path: str | None = None\n\n\nclass LLMEntry(DBStyleEntry):\n\t\"\"\"LLM configuration entry.\"\"\"\n\n\tapi_key: str | None = None\n\tmodel: str | None = None\n\ttemperature: float | None = None\n\tmax_tokens: int | None = None\n\n\nclass AgentEntry(DBStyleEntry):\n\t\"\"\"Agent configuration entry.\"\"\"\n\n\tmax_steps: int | None = None\n\tuse_vision: bool | None = None\n\tsystem_prompt: str | None = None\n\n\nclass DBStyleConfigJSON(BaseModel):\n\t\"\"\"New database-style configuration format.\"\"\"\n\n\tbrowser_profile: dict[str, BrowserProfileEntry] = Field(default_factory=dict)\n\tllm: dict[str, LLMEntry] = Field(default_factory=dict)\n\tagent: dict[str, AgentEntry] = Field(default_factory=dict)\n\n\ndef create_default_config() -> DBStyleConfigJSON:\n\t\"\"\"Create a fresh default configuration.\"\"\"\n\tlogger.debug('Creating fresh default config.json')\n\n\tnew_config = DBStyleConfigJSON()\n\n\t# Generate default IDs\n\tprofile_id = str(uuid4())\n\tllm_id = str(uuid4())\n\tagent_id = str(uuid4())\n\n\t# Create default browser profile entry\n\tnew_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)\n\n\t# Create default LLM entry\n\tnew_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4o', api_key='your-openai-api-key-here')\n\n\t# Create default agent entry\n\tnew_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)\n\n\treturn new_config\n\n\ndef load_and_migrate_config(config_path: Path) -> DBStyleConfigJSON:\n\t\"\"\"Load config.json or create fresh one if old format detected.\"\"\"\n\tif not config_path.exists():\n\t\t# Create fresh config with defaults\n\t\tconfig_path.parent.mkdir(parents=True, exist_ok=True)\n\t\tnew_config = create_default_config()\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\treturn new_config\n\n\ttry:\n\t\twith open(config_path) as f:\n\t\t\tdata = json.load(f)\n\n\t\t# Check if it's already in DB-style format\n\t\tif all(key in data for key in ['browser_profile', 'llm', 'agent']) and all(\n\t\t\tisinstance(data.get(key, {}), dict) for key in ['browser_profile', 'llm', 'agent']\n\t\t):\n\t\t\t# Check if the values are DB-style entries (have UUIDs as keys)\n\t\t\tif data.get('browser_profile') and all(isinstance(v, dict) and 'id' in v for v in data['browser_profile'].values()):\n\t\t\t\t# Already in new format\n\t\t\t\treturn DBStyleConfigJSON(**data)\n\n\t\t# Old format detected - delete it and create fresh config\n\t\tlogger.debug(f'Old config format detected at {config_path}, creating fresh config')\n\t\tnew_config = create_default_config()\n\n\t\t# Overwrite with new config\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\n\t\tlogger.debug(f'Created fresh config.json at {config_path}')\n\t\treturn new_config\n\n\texcept Exception as e:\n\t\tlogger.error(f'Failed to load config from {config_path}: {e}, creating fresh config')\n\t\t# On any error, create fresh config\n\t\tnew_config = create_default_config()\n\t\ttry:\n\t\t\twith open(config_path, 'w') as f:\n\t\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\texcept Exception as write_error:\n\t\t\tlogger.error(f'Failed to write fresh config: {write_error}')\n\t\treturn new_config\n\n\nclass Config:\n\t\"\"\"Backward-compatible configuration class that merges all config sources.\n\n\tRe-reads environment variables on every access to maintain compatibility.\n\t\"\"\"\n\n\tdef __init__(self):\n\t\t# Cache for directory creation tracking only\n\t\tself._dirs_created = False\n\n\tdef __getattr__(self, name: str) -> Any:\n\t\t\"\"\"Dynamically proxy all attributes to fresh instances.\n\n\t\tThis ensures env vars are re-read on every access.\n\t\t\"\"\"\n\t\t# Special handling for internal attributes\n\t\tif name.startswith('_'):\n\t\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\t\t# Create fresh instances on every access\n\t\told_config = OldConfig()\n\n\t\t# Always use old config for all attributes (it handles env vars with proper transformations)\n\t\tif hasattr(old_config, name):\n\t\t\treturn getattr(old_config, name)\n\n\t\t# For new MCP-specific attributes not in old config\n\t\tenv_config = FlatEnvConfig()\n\t\tif hasattr(env_config, name):\n\t\t\treturn getattr(env_config, name)\n\n\t\t# Handle special methods\n\t\tif name == 'get_default_profile':\n\t\t\treturn lambda: self._get_default_profile()\n\t\telif name == 'get_default_llm':\n\t\t\treturn lambda: self._get_default_llm()\n\t\telif name == 'get_default_agent':\n\t\t\treturn lambda: self._get_default_agent()\n\t\telif name == 'load_config':\n\t\t\treturn lambda: self._load_config()\n\t\telif name == '_ensure_dirs':\n\t\t\treturn lambda: old_config._ensure_dirs()\n\n\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\tdef _get_config_path(self) -> Path:\n\t\t\"\"\"Get config path from fresh env config.\"\"\"\n\t\tenv_config = FlatEnvConfig()\n\t\tif env_config.BROWSER_USE_CONFIG_PATH:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_PATH).expanduser()\n\t\telif env_config.BROWSER_USE_CONFIG_DIR:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_DIR).expanduser() / 'config.json'\n\t\telse:\n\t\t\txdg_config = Path(env_config.XDG_CONFIG_HOME).expanduser()\n\t\t\treturn xdg_config / 'browseruse' / 'config.json'\n\n\tdef _get_db_config(self) -> DBStyleConfigJSON:\n\t\t\"\"\"Load and migrate config.json.\"\"\"\n\t\tconfig_path = self._get_config_path()\n\t\treturn load_and_migrate_config(config_path)\n\n\tdef _get_default_profile(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default browser profile configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor profile in db_config.browser_profile.values():\n\t\t\tif profile.default:\n\t\t\t\treturn profile.model_dump(exclude_none=True)\n\n\t\t# Return first profile if no default\n\t\tif db_config.browser_profile:\n\t\t\treturn next(iter(db_config.browser_profile.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_llm(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default LLM configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor llm in db_config.llm.values():\n\t\t\tif llm.default:\n\t\t\t\treturn llm.model_dump(exclude_none=True)\n\n\t\t# Return first LLM if no default\n\t\tif db_config.llm:\n\t\t\treturn next(iter(db_config.llm.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_agent(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default agent configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor agent in db_config.agent.values():\n\t\t\tif agent.default:\n\t\t\t\treturn agent.model_dump(exclude_none=True)\n\n\t\t# Return first agent if no default\n\t\tif db_config.agent:\n\t\t\treturn next(iter(db_config.agent.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _load_config(self) -> dict[str, Any]:\n\t\t\"\"\"Load configuration with env var overrides for MCP components.\"\"\"\n\t\tconfig = {\n\t\t\t'browser_profile': self._get_default_profile(),\n\t\t\t'llm': self._get_default_llm(),\n\t\t\t'agent': self._get_default_agent(),\n\t\t}\n\n\t\t# Fresh env config for overrides\n\t\tenv_config = FlatEnvConfig()\n\n\t\t# Apply MCP-specific env var overrides\n\t\tif env_config.BROWSER_USE_HEADLESS is not None:\n\t\t\tconfig['browser_profile']['headless'] = env_config.BROWSER_USE_HEADLESS\n\n\t\tif env_config.BROWSER_USE_ALLOWED_DOMAINS:\n\t\t\tdomains = [d.strip() for d in env_config.BROWSER_USE_ALLOWED_DOMAINS.split(',') if d.strip()]\n\t\t\tconfig['browser_profile']['allowed_domains'] = domains\n\n\t\t# Proxy settings (Chromium) -> consolidated `proxy` dict\n\t\tproxy_dict: dict[str, Any] = {}\n\t\tif env_config.BROWSER_USE_PROXY_URL:\n\t\t\tproxy_dict['server'] = env_config.BROWSER_USE_PROXY_URL\n\t\tif env_config.BROWSER_USE_NO_PROXY:\n\t\t\t# store bypass as comma-separated string to match Chrome flag\n\t\t\tproxy_dict['bypass'] = ','.join(\n\t\t\t\t[d.strip() for d in env_config.BROWSER_USE_NO_PROXY.split(',') if d.strip()]\n\t\t\t)\n\t\tif env_config.BROWSER_USE_PROXY_USERNAME:\n\t\t\tproxy_dict['username'] = env_config.BROWSER_USE_PROXY_USERNAME\n\t\tif env_config.BROWSER_USE_PROXY_PASSWORD:\n\t\t\tproxy_dict['password'] = env_config.BROWSER_USE_PROXY_PASSWORD\n\t\tif proxy_dict:\n\t\t\t# ensure section exists\n\t\t\tconfig.setdefault('browser_profile', {})\n\t\t\tconfig['browser_profile']['proxy'] = proxy_dict\n\n\t\tif env_config.OPENAI_API_KEY:\n\t\t\tconfig['llm']['api_key'] = env_config.OPENAI_API_KEY\n\n\t\tif env_config.BROWSER_USE_LLM_MODEL:\n\t\t\tconfig['llm']['model'] = env_config.BROWSER_USE_LLM_MODEL\n\n\t\treturn config\n\n\n# Create singleton instance\nCONFIG = Config()\n\n\n# Helper functions for MCP components\ndef load_browser_use_config() -> dict[str, Any]:\n\t\"\"\"Load browser-use configuration for MCP components.\"\"\"\n\treturn CONFIG.load_config()\n\n\ndef get_default_profile(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default browser profile from config dict.\"\"\"\n\treturn config.get('browser_profile', {})\n\n\ndef get_default_llm(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default LLM config from config dict.\"\"\"\n\treturn config.get('llm', {})\n"}, "284a2e8a50e0c974bbd9502e4fb2f21d959b702c": {"t": 1785753143.2037673, "n": 16095, "h": "ad4cce50511a1a22", "c": "\"\"\"Configuration system for browser-use with automatic migration support.\"\"\"\n\nimport json\nimport logging\nimport os\nfrom datetime import datetime\nfrom functools import cache\nfrom pathlib import Path\nfrom typing import Any\nfrom uuid import uuid4\n\nimport psutil\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass OldConfig:\n\t\"\"\"Original lazy-loading configuration class for environment variables.\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_API_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\t@property\n\tdef BROWSER_USE_EXTENSIONS_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'extensions'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'extensions').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\nclass FlatEnvConfig(BaseSettings):\n\t\"\"\"All environment variables in a flat namespace.\"\"\"\n\n\tmodel_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True, extra='allow')\n\n\t# Logging and telemetry\n\tBROWSER_USE_LOGGING_LEVEL: str = Field(default='info')\n\tCDP_LOGGING_LEVEL: str = Field(default='error')\n\tANONYMIZED_TELEMETRY: bool = Field(default=True)\n\tBROWSER_USE_CLOUD_SYNC: bool | None = Field(default=None)\n\tBROWSER_USE_CLOUD_API_URL: str = Field(default='https://api.browser-use.com')\n\tBROWSER_USE_CLOUD_UI_URL: str = Field(default='')\n\n\t# Path configuration\n\tXDG_CACHE_HOME: str = Field(default='~/.cache')\n\tXDG_CONFIG_HOME: str = Field(default='~/.config')\n\tBROWSER_USE_CONFIG_DIR: str | None = Field(default=None)\n\n\t# LLM API keys\n\tOPENAI_API_KEY: str = Field(default='')\n\tANTHROPIC_API_KEY: str = Field(default='')\n\tGOOGLE_API_KEY: str = Field(default='')\n\tDEEPSEEK_API_KEY: str = Field(default='')\n\tGROK_API_KEY: str = Field(default='')\n\tNOVITA_API_KEY: str = Field(default='')\n\tAZURE_OPENAI_ENDPOINT: str = Field(default='')\n\tAZURE_OPENAI_KEY: str = Field(default='')\n\tSKIP_LLM_API_KEY_VERIFICATION: bool = Field(default=False)\n\n\t# Runtime hints\n\tIN_DOCKER: bool | None = Field(default=None)\n\tIS_IN_EVALS: bool = Field(default=False)\n\tWIN_FONT_DIR: str = Field(default='C:\\\\Windows\\\\Fonts')\n\n\t# MCP-specific env vars\n\tBROWSER_USE_CONFIG_PATH: str | None = Field(default=None)\n\tBROWSER_USE_HEADLESS: bool | None = Field(default=None)\n\tBROWSER_USE_ALLOWED_DOMAINS: str | None = Field(default=None)\n\tBROWSER_USE_LLM_MODEL: str | None = Field(default=None)\n\n\t# Proxy env vars\n\tBROWSER_USE_PROXY_URL: str | None = Field(default=None)\n\tBROWSER_USE_NO_PROXY: str | None = Field(default=None)\n\tBROWSER_USE_PROXY_USERNAME: str | None = Field(default=None)\n\tBROWSER_USE_PROXY_PASSWORD: str | None = Field(default=None)\n\n\nclass DBStyleEntry(BaseModel):\n\t\"\"\"Database-style entry with UUID and metadata.\"\"\"\n\n\tid: str = Field(default_factory=lambda: str(uuid4()))\n\tdefault: bool = Field(default=False)\n\tcreated_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n\n\nclass BrowserProfileEntry(DBStyleEntry):\n\t\"\"\"Browser profile configuration entry - accepts any BrowserProfile fields.\"\"\"\n\n\tmodel_config = ConfigDict(extra='allow')\n\n\t# Common browser profile fields for reference\n\theadless: bool | None = None\n\tuser_data_dir: str | None = None\n\tallowed_domains: list[str] | None = None\n\tdownloads_path: str | None = None\n\n\nclass LLMEntry(DBStyleEntry):\n\t\"\"\"LLM configuration entry.\"\"\"\n\n\tapi_key: str | None = None\n\tmodel: str | None = None\n\ttemperature: float | None = None\n\tmax_tokens: int | None = None\n\n\nclass AgentEntry(DBStyleEntry):\n\t\"\"\"Agent configuration entry.\"\"\"\n\n\tmax_steps: int | None = None\n\tuse_vision: bool | None = None\n\tsystem_prompt: str | None = None\n\n\nclass DBStyleConfigJSON(BaseModel):\n\t\"\"\"New database-style configuration format.\"\"\"\n\n\tbrowser_profile: dict[str, BrowserProfileEntry] = Field(default_factory=dict)\n\tllm: dict[str, LLMEntry] = Field(default_factory=dict)\n\tagent: dict[str, AgentEntry] = Field(default_factory=dict)\n\n\ndef create_default_config() -> DBStyleConfigJSON:\n\t\"\"\"Create a fresh default configuration.\"\"\"\n\tlogger.debug('Creating fresh default config.json')\n\n\tnew_config = DBStyleConfigJSON()\n\n\t# Generate default IDs\n\tprofile_id = str(uuid4())\n\tllm_id = str(uuid4())\n\tagent_id = str(uuid4())\n\n\t# Create default browser profile entry\n\tnew_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)\n\n\t# Create default LLM entry\n\tnew_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4o', api_key='your-openai-api-key-here')\n\n\t# Create default agent entry\n\tnew_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)\n\n\treturn new_config\n\n\ndef load_and_migrate_config(config_path: Path) -> DBStyleConfigJSON:\n\t\"\"\"Load config.json or create fresh one if old format detected.\"\"\"\n\tif not config_path.exists():\n\t\t# Create fresh config with defaults\n\t\tconfig_path.parent.mkdir(parents=True, exist_ok=True)\n\t\tnew_config = create_default_config()\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\treturn new_config\n\n\ttry:\n\t\twith open(config_path) as f:\n\t\t\tdata = json.load(f)\n\n\t\t# Check if it's already in DB-style format\n\t\tif all(key in data for key in ['browser_profile', 'llm', 'agent']) and all(\n\t\t\tisinstance(data.get(key, {}), dict) for key in ['browser_profile', 'llm', 'agent']\n\t\t):\n\t\t\t# Check if the values are DB-style entries (have UUIDs as keys)\n\t\t\tif data.get('browser_profile') and all(isinstance(v, dict) and 'id' in v for v in data['browser_profile'].values()):\n\t\t\t\t# Already in new format\n\t\t\t\treturn DBStyleConfigJSON(**data)\n\n\t\t# Old format detected - delete it and create fresh config\n\t\tlogger.debug(f'Old config format detected at {config_path}, creating fresh config')\n\t\tnew_config = create_default_config()\n\n\t\t# Overwrite with new config\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\n\t\tlogger.debug(f'Created fresh config.json at {config_path}')\n\t\treturn new_config\n\n\texcept Exception as e:\n\t\tlogger.error(f'Failed to load config from {config_path}: {e}, creating fresh config')\n\t\t# On any error, create fresh config\n\t\tnew_config = create_default_config()\n\t\ttry:\n\t\t\twith open(config_path, 'w') as f:\n\t\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\texcept Exception as write_error:\n\t\t\tlogger.error(f'Failed to write fresh config: {write_error}')\n\t\treturn new_config\n\n\nclass Config:\n\t\"\"\"Backward-compatible configuration class that merges all config sources.\n\n\tRe-reads environment variables on every access to maintain compatibility.\n\t\"\"\"\n\n\tdef __init__(self):\n\t\t# Cache for directory creation tracking only\n\t\tself._dirs_created = False\n\n\tdef __getattr__(self, name: str) -> Any:\n\t\t\"\"\"Dynamically proxy all attributes to fresh instances.\n\n\t\tThis ensures env vars are re-read on every access.\n\t\t\"\"\"\n\t\t# Special handling for internal attributes\n\t\tif name.startswith('_'):\n\t\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\t\t# Create fresh instances on every access\n\t\told_config = OldConfig()\n\n\t\t# Always use old config for all attributes (it handles env vars with proper transformations)\n\t\tif hasattr(old_config, name):\n\t\t\treturn getattr(old_config, name)\n\n\t\t# For new MCP-specific attributes not in old config\n\t\tenv_config = FlatEnvConfig()\n\t\tif hasattr(env_config, name):\n\t\t\treturn getattr(env_config, name)\n\n\t\t# Handle special methods\n\t\tif name == 'get_default_profile':\n\t\t\treturn lambda: self._get_default_profile()\n\t\telif name == 'get_default_llm':\n\t\t\treturn lambda: self._get_default_llm()\n\t\telif name == 'get_default_agent':\n\t\t\treturn lambda: self._get_default_agent()\n\t\telif name == 'load_config':\n\t\t\treturn lambda: self._load_config()\n\t\telif name == '_ensure_dirs':\n\t\t\treturn lambda: old_config._ensure_dirs()\n\n\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\tdef _get_config_path(self) -> Path:\n\t\t\"\"\"Get config path from fresh env config.\"\"\"\n\t\tenv_config = FlatEnvConfig()\n\t\tif env_config.BROWSER_USE_CONFIG_PATH:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_PATH).expanduser()\n\t\telif env_config.BROWSER_USE_CONFIG_DIR:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_DIR).expanduser() / 'config.json'\n\t\telse:\n\t\t\txdg_config = Path(env_config.XDG_CONFIG_HOME).expanduser()\n\t\t\treturn xdg_config / 'browseruse' / 'config.json'\n\n\tdef _get_db_config(self) -> DBStyleConfigJSON:\n\t\t\"\"\"Load and migrate config.json.\"\"\"\n\t\tconfig_path = self._get_config_path()\n\t\treturn load_and_migrate_config(config_path)\n\n\tdef _get_default_profile(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default browser profile configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor profile in db_config.browser_profile.values():\n\t\t\tif profile.default:\n\t\t\t\treturn profile.model_dump(exclude_none=True)\n\n\t\t# Return first profile if no default\n\t\tif db_config.browser_profile:\n\t\t\treturn next(iter(db_config.browser_profile.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_llm(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default LLM configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor llm in db_config.llm.values():\n\t\t\tif llm.default:\n\t\t\t\treturn llm.model_dump(exclude_none=True)\n\n\t\t# Return first LLM if no default\n\t\tif db_config.llm:\n\t\t\treturn next(iter(db_config.llm.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_agent(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default agent configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor agent in db_config.agent.values():\n\t\t\tif agent.default:\n\t\t\t\treturn agent.model_dump(exclude_none=True)\n\n\t\t# Return first agent if no default\n\t\tif db_config.agent:\n\t\t\treturn next(iter(db_config.agent.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _load_config(self) -> dict[str, Any]:\n\t\t\"\"\"Load configuration with env var overrides for MCP components.\"\"\"\n\t\tconfig = {\n\t\t\t'browser_profile': self._get_default_profile(),\n\t\t\t'llm': self._get_default_llm(),\n\t\t\t'agent': self._get_default_agent(),\n\t\t}\n\n\t\t# Fresh env config for overrides\n\t\tenv_config = FlatEnvConfig()\n\n\t\t# Apply MCP-specific env var overrides\n\t\tif env_config.BROWSER_USE_HEADLESS is not None:\n\t\t\tconfig['browser_profile']['headless'] = env_config.BROWSER_USE_HEADLESS\n\n\t\tif env_config.BROWSER_USE_ALLOWED_DOMAINS:\n\t\t\tdomains = [d.strip() for d in env_config.BROWSER_USE_ALLOWED_DOMAINS.split(',') if d.strip()]\n\t\t\tconfig['browser_profile']['allowed_domains'] = domains\n\n\t\t# Proxy settings (Chromium) -> consolidated `proxy` dict\n\t\tproxy_dict: dict[str, Any] = {}\n\t\tif env_config.BROWSER_USE_PROXY_URL:\n\t\t\tproxy_dict['server'] = env_config.BROWSER_USE_PROXY_URL\n\t\tif env_config.BROWSER_USE_NO_PROXY:\n\t\t\t# store bypass as comma-separated string to match Chrome flag\n\t\t\tproxy_dict['bypass'] = ','.join(\n\t\t\t\t[d.strip() for d in env_config.BROWSER_USE_NO_PROXY.split(',') if d.strip()]\n\t\t\t)\n\t\tif env_config.BROWSER_USE_PROXY_USERNAME:\n\t\t\tproxy_dict['username'] = env_config.BROWSER_USE_PROXY_USERNAME\n\t\tif env_config.BROWSER_USE_PROXY_PASSWORD:\n\t\t\tproxy_dict['password'] = env_config.BROWSER_USE_PROXY_PASSWORD\n\t\tif proxy_dict:\n\t\t\t# ensure section exists\n\t\t\tconfig.setdefault('browser_profile', {})\n\t\t\tconfig['browser_profile']['proxy'] = proxy_dict\n\n\t\tif env_config.OPENAI_API_KEY:\n\t\t\tconfig['llm']['api_key'] = env_config.OPENAI_API_KEY\n\n\t\tif env_config.BROWSER_USE_LLM_MODEL:\n\t\t\tconfig['llm']['model'] = env_config.BROWSER_USE_LLM_MODEL\n\n\t\treturn config\n\n\n# Create singleton instance\nCONFIG = Config()\n\n\n# Helper functions for MCP components\ndef load_browser_use_config() -> dict[str, Any]:\n\t\"\"\"Load browser-use configuration for MCP components.\"\"\"\n\treturn CONFIG.load_config()\n\n\ndef get_default_profile(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default browser profile from config dict.\"\"\"\n\treturn config.get('browser_profile', {})\n\n\ndef get_default_llm(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default LLM config from config dict.\"\"\"\n\treturn config.get('llm', {})\n"}, "4e0ce78f4338a9cfc771b354dded1c3fce5fbd5b": {"t": 1785753144.6479087, "n": 15917, "h": "d7be5b21035dd060", "c": "\"\"\"Configuration system for browser-use with automatic migration support.\"\"\"\n\nimport json\nimport logging\nimport os\nfrom datetime import datetime\nfrom functools import cache\nfrom pathlib import Path\nfrom typing import Any\nfrom uuid import uuid4\n\nimport psutil\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass OldConfig:\n\t\"\"\"Original lazy-loading configuration class for environment variables.\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_API_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\t@property\n\tdef BROWSER_USE_EXTENSIONS_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'extensions'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'extensions').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\nclass FlatEnvConfig(BaseSettings):\n\t\"\"\"All environment variables in a flat namespace.\"\"\"\n\n\tmodel_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True, extra='allow')\n\n\t# Logging and telemetry\n\tBROWSER_USE_LOGGING_LEVEL: str = Field(default='info')\n\tCDP_LOGGING_LEVEL: str = Field(default='error')\n\tANONYMIZED_TELEMETRY: bool = Field(default=True)\n\tBROWSER_USE_CLOUD_SYNC: bool | None = Field(default=None)\n\tBROWSER_USE_CLOUD_API_URL: str = Field(default='https://api.browser-use.com')\n\tBROWSER_USE_CLOUD_UI_URL: str = Field(default='')\n\n\t# Path configuration\n\tXDG_CACHE_HOME: str = Field(default='~/.cache')\n\tXDG_CONFIG_HOME: str = Field(default='~/.config')\n\tBROWSER_USE_CONFIG_DIR: str | None = Field(default=None)\n\n\t# LLM API keys\n\tOPENAI_API_KEY: str = Field(default='')\n\tANTHROPIC_API_KEY: str = Field(default='')\n\tGOOGLE_API_KEY: str = Field(default='')\n\tDEEPSEEK_API_KEY: str = Field(default='')\n\tGROK_API_KEY: str = Field(default='')\n\tNOVITA_API_KEY: str = Field(default='')\n\tAZURE_OPENAI_ENDPOINT: str = Field(default='')\n\tAZURE_OPENAI_KEY: str = Field(default='')\n\tSKIP_LLM_API_KEY_VERIFICATION: bool = Field(default=False)\n\n\t# Runtime hints\n\tIN_DOCKER: bool | None = Field(default=None)\n\tIS_IN_EVALS: bool = Field(default=False)\n\tWIN_FONT_DIR: str = Field(default='C:\\\\Windows\\\\Fonts')\n\n\t# MCP-specific env vars\n\tBROWSER_USE_CONFIG_PATH: str | None = Field(default=None)\n\tBROWSER_USE_HEADLESS: bool | None = Field(default=None)\n\tBROWSER_USE_ALLOWED_DOMAINS: str | None = Field(default=None)\n\tBROWSER_USE_LLM_MODEL: str | None = Field(default=None)\n\n\t# Proxy env vars\n\tBROWSER_USE_PROXY_URL: str | None = Field(default=None)\n\tBROWSER_USE_NO_PROXY: str | None = Field(default=None)\n\tBROWSER_USE_PROXY_USERNAME: str | None = Field(default=None)\n\tBROWSER_USE_PROXY_PASSWORD: str | None = Field(default=None)\n\n\nclass DBStyleEntry(BaseModel):\n\t\"\"\"Database-style entry with UUID and metadata.\"\"\"\n\n\tid: str = Field(default_factory=lambda: str(uuid4()))\n\tdefault: bool = Field(default=False)\n\tcreated_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n\n\nclass BrowserProfileEntry(DBStyleEntry):\n\t\"\"\"Browser profile configuration entry - accepts any BrowserProfile fields.\"\"\"\n\n\tmodel_config = ConfigDict(extra='allow')\n\n\t# Common browser profile fields for reference\n\theadless: bool | None = None\n\tuser_data_dir: str | None = None\n\tallowed_domains: list[str] | None = None\n\tdownloads_path: str | None = None\n\n\nclass LLMEntry(DBStyleEntry):\n\t\"\"\"LLM configuration entry.\"\"\"\n\n\tapi_key: str | None = None\n\tmodel: str | None = None\n\ttemperature: float | None = None\n\tmax_tokens: int | None = None\n\n\nclass AgentEntry(DBStyleEntry):\n\t\"\"\"Agent configuration entry.\"\"\"\n\n\tmax_steps: int | None = None\n\tuse_vision: bool | None = None\n\tsystem_prompt: str | None = None\n\n\nclass DBStyleConfigJSON(BaseModel):\n\t\"\"\"New database-style configuration format.\"\"\"\n\n\tbrowser_profile: dict[str, BrowserProfileEntry] = Field(default_factory=dict)\n\tllm: dict[str, LLMEntry] = Field(default_factory=dict)\n\tagent: dict[str, AgentEntry] = Field(default_factory=dict)\n\n\ndef create_default_config() -> DBStyleConfigJSON:\n\t\"\"\"Create a fresh default configuration.\"\"\"\n\tlogger.debug('Creating fresh default config.json')\n\n\tnew_config = DBStyleConfigJSON()\n\n\t# Generate default IDs\n\tprofile_id = str(uuid4())\n\tllm_id = str(uuid4())\n\tagent_id = str(uuid4())\n\n\t# Create default browser profile entry\n\tnew_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)\n\n\t# Create default LLM entry\n\tnew_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4o', api_key='your-openai-api-key-here')\n\n\t# Create default agent entry\n\tnew_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)\n\n\treturn new_config\n\n\ndef load_and_migrate_config(config_path: Path) -> DBStyleConfigJSON:\n\t\"\"\"Load config.json or create fresh one if old format detected.\"\"\"\n\tif not config_path.exists():\n\t\t# Create fresh config with defaults\n\t\tconfig_path.parent.mkdir(parents=True, exist_ok=True)\n\t\tnew_config = create_default_config()\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\treturn new_config\n\n\ttry:\n\t\twith open(config_path) as f:\n\t\t\tdata = json.load(f)\n\n\t\t# Check if it's already in DB-style format\n\t\tif all(key in data for key in ['browser_profile', 'llm', 'agent']) and all(\n\t\t\tisinstance(data.get(key, {}), dict) for key in ['browser_profile', 'llm', 'agent']\n\t\t):\n\t\t\t# Check if the values are DB-style entries (have UUIDs as keys)\n\t\t\tif data.get('browser_profile') and all(isinstance(v, dict) and 'id' in v for v in data['browser_profile'].values()):\n\t\t\t\t# Already in new format\n\t\t\t\treturn DBStyleConfigJSON(**data)\n\n\t\t# Old format detected - delete it and create fresh config\n\t\tlogger.debug(f'Old config format detected at {config_path}, creating fresh config')\n\t\tnew_config = create_default_config()\n\n\t\t# Overwrite with new config\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\n\t\tlogger.debug(f'Created fresh config.json at {config_path}')\n\t\treturn new_config\n\n\texcept Exception as e:\n\t\tlogger.error(f'Failed to load config from {config_path}: {e}, creating fresh config')\n\t\t# On any error, create fresh config\n\t\tnew_config = create_default_config()\n\t\ttry:\n\t\t\twith open(config_path, 'w') as f:\n\t\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\texcept Exception as write_error:\n\t\t\tlogger.error(f'Failed to write fresh config: {write_error}')\n\t\treturn new_config\n\n\nclass Config:\n\t\"\"\"Backward-compatible configuration class that merges all config sources.\n\n\tRe-reads environment variables on every access to maintain compatibility.\n\t\"\"\"\n\n\tdef __init__(self):\n\t\t# Cache for directory creation tracking only\n\t\tself._dirs_created = False\n\n\tdef __getattr__(self, name: str) -> Any:\n\t\t\"\"\"Dynamically proxy all attributes to fresh instances.\n\n\t\tThis ensures env vars are re-read on every access.\n\t\t\"\"\"\n\t\t# Special handling for internal attributes\n\t\tif name.startswith('_'):\n\t\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\t\t# Create fresh instances on every access\n\t\told_config = OldConfig()\n\n\t\t# Always use old config for all attributes (it handles env vars with proper transformations)\n\t\tif hasattr(old_config, name):\n\t\t\treturn getattr(old_config, name)\n\n\t\t# For new MCP-specific attributes not in old config\n\t\tenv_config = FlatEnvConfig()\n\t\tif hasattr(env_config, name):\n\t\t\treturn getattr(env_config, name)\n\n\t\t# Handle special methods\n\t\tif name == 'get_default_profile':\n\t\t\treturn lambda: self._get_default_profile()\n\t\telif name == 'get_default_llm':\n\t\t\treturn lambda: self._get_default_llm()\n\t\telif name == 'get_default_agent':\n\t\t\treturn lambda: self._get_default_agent()\n\t\telif name == 'load_config':\n\t\t\treturn lambda: self._load_config()\n\t\telif name == '_ensure_dirs':\n\t\t\treturn lambda: old_config._ensure_dirs()\n\n\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\tdef _get_config_path(self) -> Path:\n\t\t\"\"\"Get config path from fresh env config.\"\"\"\n\t\tenv_config = FlatEnvConfig()\n\t\tif env_config.BROWSER_USE_CONFIG_PATH:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_PATH).expanduser()\n\t\telif env_config.BROWSER_USE_CONFIG_DIR:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_DIR).expanduser() / 'config.json'\n\t\telse:\n\t\t\txdg_config = Path(env_config.XDG_CONFIG_HOME).expanduser()\n\t\t\treturn xdg_config / 'browseruse' / 'config.json'\n\n\tdef _get_db_config(self) -> DBStyleConfigJSON:\n\t\t\"\"\"Load and migrate config.json.\"\"\"\n\t\tconfig_path = self._get_config_path()\n\t\treturn load_and_migrate_config(config_path)\n\n\tdef _get_default_profile(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default browser profile configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor profile in db_config.browser_profile.values():\n\t\t\tif profile.default:\n\t\t\t\treturn profile.model_dump(exclude_none=True)\n\n\t\t# Return first profile if no default\n\t\tif db_config.browser_profile:\n\t\t\treturn next(iter(db_config.browser_profile.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_llm(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default LLM configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor llm in db_config.llm.values():\n\t\t\tif llm.default:\n\t\t\t\treturn llm.model_dump(exclude_none=True)\n\n\t\t# Return first LLM if no default\n\t\tif db_config.llm:\n\t\t\treturn next(iter(db_config.llm.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_agent(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default agent configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor agent in db_config.agent.values():\n\t\t\tif agent.default:\n\t\t\t\treturn agent.model_dump(exclude_none=True)\n\n\t\t# Return first agent if no default\n\t\tif db_config.agent:\n\t\t\treturn next(iter(db_config.agent.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _load_config(self) -> dict[str, Any]:\n\t\t\"\"\"Load configuration with env var overrides for MCP components.\"\"\"\n\t\tconfig = {\n\t\t\t'browser_profile': self._get_default_profile(),\n\t\t\t'llm': self._get_default_llm(),\n\t\t\t'agent': self._get_default_agent(),\n\t\t}\n\n\t\t# Fresh env config for overrides\n\t\tenv_config = FlatEnvConfig()\n\n\t\t# Apply MCP-specific env var overrides\n\t\tif env_config.BROWSER_USE_HEADLESS is not None:\n\t\t\tconfig['browser_profile']['headless'] = env_config.BROWSER_USE_HEADLESS\n\n\t\tif env_config.BROWSER_USE_ALLOWED_DOMAINS:\n\t\t\tdomains = [d.strip() for d in env_config.BROWSER_USE_ALLOWED_DOMAINS.split(',') if d.strip()]\n\t\t\tconfig['browser_profile']['allowed_domains'] = domains\n\n\t\t# Proxy settings (Chromium)\n\t\tif env_config.BROWSER_USE_PROXY_URL:\n\t\t\tconfig['browser_profile']['proxy_server'] = env_config.BROWSER_USE_PROXY_URL\n\t\tif env_config.BROWSER_USE_NO_PROXY:\n\t\t\tbypass = [d.strip() for d in env_config.BROWSER_USE_NO_PROXY.split(',') if d.strip()]\n\t\t\tconfig['browser_profile']['proxy_bypass_list'] = bypass\n\t\tif env_config.BROWSER_USE_PROXY_USERNAME:\n\t\t\tconfig['browser_profile']['proxy_username'] = env_config.BROWSER_USE_PROXY_USERNAME\n\t\tif env_config.BROWSER_USE_PROXY_PASSWORD:\n\t\t\tconfig['browser_profile']['proxy_password'] = env_config.BROWSER_USE_PROXY_PASSWORD\n\n\t\tif env_config.OPENAI_API_KEY:\n\t\t\tconfig['llm']['api_key'] = env_config.OPENAI_API_KEY\n\n\t\tif env_config.BROWSER_USE_LLM_MODEL:\n\t\t\tconfig['llm']['model'] = env_config.BROWSER_USE_LLM_MODEL\n\n\t\treturn config\n\n\n# Create singleton instance\nCONFIG = Config()\n\n\n# Helper functions for MCP components\ndef load_browser_use_config() -> dict[str, Any]:\n\t\"\"\"Load browser-use configuration for MCP components.\"\"\"\n\treturn CONFIG.load_config()\n\n\ndef get_default_profile(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default browser profile from config dict.\"\"\"\n\treturn config.get('browser_profile', {})\n\n\ndef get_default_llm(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default LLM config from config dict.\"\"\"\n\treturn config.get('llm', {})\n"}, "13dce77c98d9bdab823e70f9154a1a06dd8c1380": {"t": 1785753146.0411527, "n": 15063, "h": "f1b419fdba1e0b79", "c": "\"\"\"Configuration system for browser-use with automatic migration support.\"\"\"\n\nimport json\nimport logging\nimport os\nfrom datetime import datetime\nfrom functools import cache\nfrom pathlib import Path\nfrom typing import Any\nfrom uuid import uuid4\n\nimport psutil\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass OldConfig:\n\t\"\"\"Original lazy-loading configuration class for environment variables.\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_API_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\t@property\n\tdef BROWSER_USE_EXTENSIONS_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'extensions'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'extensions').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\nclass FlatEnvConfig(BaseSettings):\n\t\"\"\"All environment variables in a flat namespace.\"\"\"\n\n\tmodel_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True, extra='allow')\n\n\t# Logging and telemetry\n\tBROWSER_USE_LOGGING_LEVEL: str = Field(default='info')\n\tCDP_LOGGING_LEVEL: str = Field(default='error')\n\tANONYMIZED_TELEMETRY: bool = Field(default=True)\n\tBROWSER_USE_CLOUD_SYNC: bool | None = Field(default=None)\n\tBROWSER_USE_CLOUD_API_URL: str = Field(default='https://api.browser-use.com')\n\tBROWSER_USE_CLOUD_UI_URL: str = Field(default='')\n\n\t# Path configuration\n\tXDG_CACHE_HOME: str = Field(default='~/.cache')\n\tXDG_CONFIG_HOME: str = Field(default='~/.config')\n\tBROWSER_USE_CONFIG_DIR: str | None = Field(default=None)\n\n\t# LLM API keys\n\tOPENAI_API_KEY: str = Field(default='')\n\tANTHROPIC_API_KEY: str = Field(default='')\n\tGOOGLE_API_KEY: str = Field(default='')\n\tDEEPSEEK_API_KEY: str = Field(default='')\n\tGROK_API_KEY: str = Field(default='')\n\tNOVITA_API_KEY: str = Field(default='')\n\tAZURE_OPENAI_ENDPOINT: str = Field(default='')\n\tAZURE_OPENAI_KEY: str = Field(default='')\n\tSKIP_LLM_API_KEY_VERIFICATION: bool = Field(default=False)\n\n\t# Runtime hints\n\tIN_DOCKER: bool | None = Field(default=None)\n\tIS_IN_EVALS: bool = Field(default=False)\n\tWIN_FONT_DIR: str = Field(default='C:\\\\Windows\\\\Fonts')\n\n\t# MCP-specific env vars\n\tBROWSER_USE_CONFIG_PATH: str | None = Field(default=None)\n\tBROWSER_USE_HEADLESS: bool | None = Field(default=None)\n\tBROWSER_USE_ALLOWED_DOMAINS: str | None = Field(default=None)\n\tBROWSER_USE_LLM_MODEL: str | None = Field(default=None)\n\n\nclass DBStyleEntry(BaseModel):\n\t\"\"\"Database-style entry with UUID and metadata.\"\"\"\n\n\tid: str = Field(default_factory=lambda: str(uuid4()))\n\tdefault: bool = Field(default=False)\n\tcreated_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n\n\nclass BrowserProfileEntry(DBStyleEntry):\n\t\"\"\"Browser profile configuration entry - accepts any BrowserProfile fields.\"\"\"\n\n\tmodel_config = ConfigDict(extra='allow')\n\n\t# Common browser profile fields for reference\n\theadless: bool | None = None\n\tuser_data_dir: str | None = None\n\tallowed_domains: list[str] | None = None\n\tdownloads_path: str | None = None\n\n\nclass LLMEntry(DBStyleEntry):\n\t\"\"\"LLM configuration entry.\"\"\"\n\n\tapi_key: str | None = None\n\tmodel: str | None = None\n\ttemperature: float | None = None\n\tmax_tokens: int | None = None\n\n\nclass AgentEntry(DBStyleEntry):\n\t\"\"\"Agent configuration entry.\"\"\"\n\n\tmax_steps: int | None = None\n\tuse_vision: bool | None = None\n\tsystem_prompt: str | None = None\n\n\nclass DBStyleConfigJSON(BaseModel):\n\t\"\"\"New database-style configuration format.\"\"\"\n\n\tbrowser_profile: dict[str, BrowserProfileEntry] = Field(default_factory=dict)\n\tllm: dict[str, LLMEntry] = Field(default_factory=dict)\n\tagent: dict[str, AgentEntry] = Field(default_factory=dict)\n\n\ndef create_default_config() -> DBStyleConfigJSON:\n\t\"\"\"Create a fresh default configuration.\"\"\"\n\tlogger.debug('Creating fresh default config.json')\n\n\tnew_config = DBStyleConfigJSON()\n\n\t# Generate default IDs\n\tprofile_id = str(uuid4())\n\tllm_id = str(uuid4())\n\tagent_id = str(uuid4())\n\n\t# Create default browser profile entry\n\tnew_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)\n\n\t# Create default LLM entry\n\tnew_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4o', api_key='your-openai-api-key-here')\n\n\t# Create default agent entry\n\tnew_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)\n\n\treturn new_config\n\n\ndef load_and_migrate_config(config_path: Path) -> DBStyleConfigJSON:\n\t\"\"\"Load config.json or create fresh one if old format detected.\"\"\"\n\tif not config_path.exists():\n\t\t# Create fresh config with defaults\n\t\tconfig_path.parent.mkdir(parents=True, exist_ok=True)\n\t\tnew_config = create_default_config()\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\treturn new_config\n\n\ttry:\n\t\twith open(config_path) as f:\n\t\t\tdata = json.load(f)\n\n\t\t# Check if it's already in DB-style format\n\t\tif all(key in data for key in ['browser_profile', 'llm', 'agent']) and all(\n\t\t\tisinstance(data.get(key, {}), dict) for key in ['browser_profile', 'llm', 'agent']\n\t\t):\n\t\t\t# Check if the values are DB-style entries (have UUIDs as keys)\n\t\t\tif data.get('browser_profile') and all(isinstance(v, dict) and 'id' in v for v in data['browser_profile'].values()):\n\t\t\t\t# Already in new format\n\t\t\t\treturn DBStyleConfigJSON(**data)\n\n\t\t# Old format detected - delete it and create fresh config\n\t\tlogger.debug(f'Old config format detected at {config_path}, creating fresh config')\n\t\tnew_config = create_default_config()\n\n\t\t# Overwrite with new config\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\n\t\tlogger.debug(f'Created fresh config.json at {config_path}')\n\t\treturn new_config\n\n\texcept Exception as e:\n\t\tlogger.error(f'Failed to load config from {config_path}: {e}, creating fresh config')\n\t\t# On any error, create fresh config\n\t\tnew_config = create_default_config()\n\t\ttry:\n\t\t\twith open(config_path, 'w') as f:\n\t\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\texcept Exception as write_error:\n\t\t\tlogger.error(f'Failed to write fresh config: {write_error}')\n\t\treturn new_config\n\n\nclass Config:\n\t\"\"\"Backward-compatible configuration class that merges all config sources.\n\n\tRe-reads environment variables on every access to maintain compatibility.\n\t\"\"\"\n\n\tdef __init__(self):\n\t\t# Cache for directory creation tracking only\n\t\tself._dirs_created = False\n\n\tdef __getattr__(self, name: str) -> Any:\n\t\t\"\"\"Dynamically proxy all attributes to fresh instances.\n\n\t\tThis ensures env vars are re-read on every access.\n\t\t\"\"\"\n\t\t# Special handling for internal attributes\n\t\tif name.startswith('_'):\n\t\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\t\t# Create fresh instances on every access\n\t\told_config = OldConfig()\n\n\t\t# Always use old config for all attributes (it handles env vars with proper transformations)\n\t\tif hasattr(old_config, name):\n\t\t\treturn getattr(old_config, name)\n\n\t\t# For new MCP-specific attributes not in old config\n\t\tenv_config = FlatEnvConfig()\n\t\tif hasattr(env_config, name):\n\t\t\treturn getattr(env_config, name)\n\n\t\t# Handle special methods\n\t\tif name == 'get_default_profile':\n\t\t\treturn lambda: self._get_default_profile()\n\t\telif name == 'get_default_llm':\n\t\t\treturn lambda: self._get_default_llm()\n\t\telif name == 'get_default_agent':\n\t\t\treturn lambda: self._get_default_agent()\n\t\telif name == 'load_config':\n\t\t\treturn lambda: self._load_config()\n\t\telif name == '_ensure_dirs':\n\t\t\treturn lambda: old_config._ensure_dirs()\n\n\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\tdef _get_config_path(self) -> Path:\n\t\t\"\"\"Get config path from fresh env config.\"\"\"\n\t\tenv_config = FlatEnvConfig()\n\t\tif env_config.BROWSER_USE_CONFIG_PATH:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_PATH).expanduser()\n\t\telif env_config.BROWSER_USE_CONFIG_DIR:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_DIR).expanduser() / 'config.json'\n\t\telse:\n\t\t\txdg_config = Path(env_config.XDG_CONFIG_HOME).expanduser()\n\t\t\treturn xdg_config / 'browseruse' / 'config.json'\n\n\tdef _get_db_config(self) -> DBStyleConfigJSON:\n\t\t\"\"\"Load and migrate config.json.\"\"\"\n\t\tconfig_path = self._get_config_path()\n\t\treturn load_and_migrate_config(config_path)\n\n\tdef _get_default_profile(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default browser profile configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor profile in db_config.browser_profile.values():\n\t\t\tif profile.default:\n\t\t\t\treturn profile.model_dump(exclude_none=True)\n\n\t\t# Return first profile if no default\n\t\tif db_config.browser_profile:\n\t\t\treturn next(iter(db_config.browser_profile.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_llm(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default LLM configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor llm in db_config.llm.values():\n\t\t\tif llm.default:\n\t\t\t\treturn llm.model_dump(exclude_none=True)\n\n\t\t# Return first LLM if no default\n\t\tif db_config.llm:\n\t\t\treturn next(iter(db_config.llm.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_agent(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default agent configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor agent in db_config.agent.values():\n\t\t\tif agent.default:\n\t\t\t\treturn agent.model_dump(exclude_none=True)\n\n\t\t# Return first agent if no default\n\t\tif db_config.agent:\n\t\t\treturn next(iter(db_config.agent.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _load_config(self) -> dict[str, Any]:\n\t\t\"\"\"Load configuration with env var overrides for MCP components.\"\"\"\n\t\tconfig = {\n\t\t\t'browser_profile': self._get_default_profile(),\n\t\t\t'llm': self._get_default_llm(),\n\t\t\t'agent': self._get_default_agent(),\n\t\t}\n\n\t\t# Fresh env config for overrides\n\t\tenv_config = FlatEnvConfig()\n\n\t\t# Apply MCP-specific env var overrides\n\t\tif env_config.BROWSER_USE_HEADLESS is not None:\n\t\t\tconfig['browser_profile']['headless'] = env_config.BROWSER_USE_HEADLESS\n\n\t\tif env_config.BROWSER_USE_ALLOWED_DOMAINS:\n\t\t\tdomains = [d.strip() for d in env_config.BROWSER_USE_ALLOWED_DOMAINS.split(',') if d.strip()]\n\t\t\tconfig['browser_profile']['allowed_domains'] = domains\n\n\t\tif env_config.OPENAI_API_KEY:\n\t\t\tconfig['llm']['api_key'] = env_config.OPENAI_API_KEY\n\n\t\tif env_config.BROWSER_USE_LLM_MODEL:\n\t\t\tconfig['llm']['model'] = env_config.BROWSER_USE_LLM_MODEL\n\n\t\treturn config\n\n\n# Create singleton instance\nCONFIG = Config()\n\n\n# Helper functions for MCP components\ndef load_browser_use_config() -> dict[str, Any]:\n\t\"\"\"Load browser-use configuration for MCP components.\"\"\"\n\treturn CONFIG.load_config()\n\n\ndef get_default_profile(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default browser profile from config dict.\"\"\"\n\treturn config.get('browser_profile', {})\n\n\ndef get_default_llm(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default LLM config from config dict.\"\"\"\n\treturn config.get('llm', {})\n"}, "5530e2485e60d22d7474e422a6fbbcb97d39cb88": {"t": 1785753147.2180822, "n": 15060, "h": "eefb8bad00e376dd", "c": "\"\"\"Configuration system for browser-use with automatic migration support.\"\"\"\n\nimport json\nimport logging\nimport os\nfrom datetime import datetime\nfrom functools import cache\nfrom pathlib import Path\nfrom typing import Any\nfrom uuid import uuid4\n\nimport psutil\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass OldConfig:\n\t\"\"\"Original lazy-loading configuration class for environment variables.\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_API_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\t@property\n\tdef BROWSER_USE_EXTENSIONS_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'extensions'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'extensions').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\nclass FlatEnvConfig(BaseSettings):\n\t\"\"\"All environment variables in a flat namespace.\"\"\"\n\n\tmodel_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True, extra='allow')\n\n\t# Logging and telemetry\n\tBROWSER_USE_LOGGING_LEVEL: str = Field(default='info')\n\tCDP_LOGGING_LEVEL: str = Field(default='error')\n\tANONYMIZED_TELEMETRY: bool = Field(default=True)\n\tBROWSER_USE_CLOUD_SYNC: bool | None = Field(default=None)\n\tBROWSER_USE_CLOUD_API_URL: str = Field(default='https://api.browser-use.com')\n\tBROWSER_USE_CLOUD_UI_URL: str = Field(default='')\n\n\t# Path configuration\n\tXDG_CACHE_HOME: str = Field(default='~/.cache')\n\tXDG_CONFIG_HOME: str = Field(default='~/.config')\n\tBROWSER_USE_CONFIG_DIR: str | None = Field(default=None)\n\n\t# LLM API keys\n\tOPENAI_API_KEY: str = Field(default='')\n\tANTHROPIC_API_KEY: str = Field(default='')\n\tGOOGLE_API_KEY: str = Field(default='')\n\tDEEPSEEK_API_KEY: str = Field(default='')\n\tGROK_API_KEY: str = Field(default='')\n\tNOVITA_API_KEY: str = Field(default='')\n\tAZURE_OPENAI_ENDPOINT: str = Field(default='')\n\tAZURE_OPENAI_KEY: str = Field(default='')\n\tSKIP_LLM_API_KEY_VERIFICATION: bool = Field(default=False)\n\n\t# Runtime hints\n\tIN_DOCKER: bool | None = Field(default=None)\n\tIS_IN_EVALS: bool = Field(default=False)\n\tWIN_FONT_DIR: str = Field(default='C:\\\\Windows\\\\Fonts')\n\n\t# MCP-specific env vars\n\tBROWSER_USE_CONFIG_PATH: str | None = Field(default=None)\n\tBROWSER_USE_HEADLESS: bool | None = Field(default=None)\n\tBROWSER_USE_ALLOWED_DOMAINS: str | None = Field(default=None)\n\tBROWSER_USE_LLM_MODEL: str | None = Field(default=None)\n\n\nclass DBStyleEntry(BaseModel):\n\t\"\"\"Database-style entry with UUID and metadata.\"\"\"\n\n\tid: str = Field(default_factory=lambda: str(uuid4()))\n\tdefault: bool = Field(default=False)\n\tcreated_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n\n\nclass BrowserProfileEntry(DBStyleEntry):\n\t\"\"\"Browser profile configuration entry - accepts any BrowserProfile fields.\"\"\"\n\n\tmodel_config = ConfigDict(extra='allow')\n\n\t# Common browser profile fields for reference\n\theadless: bool | None = None\n\tuser_data_dir: str | None = None\n\tallowed_domains: list[str] | None = None\n\tdownloads_path: str | None = None\n\n\nclass LLMEntry(DBStyleEntry):\n\t\"\"\"LLM configuration entry.\"\"\"\n\n\tapi_key: str | None = None\n\tmodel: str | None = None\n\ttemperature: float | None = None\n\tmax_tokens: int | None = None\n\n\nclass AgentEntry(DBStyleEntry):\n\t\"\"\"Agent configuration entry.\"\"\"\n\n\tmax_steps: int | None = None\n\tuse_vision: bool | None = None\n\tsystem_prompt: str | None = None\n\n\nclass DBStyleConfigJSON(BaseModel):\n\t\"\"\"New database-style configuration format.\"\"\"\n\n\tbrowser_profile: dict[str, BrowserProfileEntry] = Field(default_factory=dict)\n\tllm: dict[str, LLMEntry] = Field(default_factory=dict)\n\tagent: dict[str, AgentEntry] = Field(default_factory=dict)\n\n\ndef create_default_config() -> DBStyleConfigJSON:\n\t\"\"\"Create a fresh default configuration.\"\"\"\n\tlogger.info('Creating fresh default config.json')\n\n\tnew_config = DBStyleConfigJSON()\n\n\t# Generate default IDs\n\tprofile_id = str(uuid4())\n\tllm_id = str(uuid4())\n\tagent_id = str(uuid4())\n\n\t# Create default browser profile entry\n\tnew_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)\n\n\t# Create default LLM entry\n\tnew_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4o', api_key='your-openai-api-key-here')\n\n\t# Create default agent entry\n\tnew_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)\n\n\treturn new_config\n\n\ndef load_and_migrate_config(config_path: Path) -> DBStyleConfigJSON:\n\t\"\"\"Load config.json or create fresh one if old format detected.\"\"\"\n\tif not config_path.exists():\n\t\t# Create fresh config with defaults\n\t\tconfig_path.parent.mkdir(parents=True, exist_ok=True)\n\t\tnew_config = create_default_config()\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\treturn new_config\n\n\ttry:\n\t\twith open(config_path) as f:\n\t\t\tdata = json.load(f)\n\n\t\t# Check if it's already in DB-style format\n\t\tif all(key in data for key in ['browser_profile', 'llm', 'agent']) and all(\n\t\t\tisinstance(data.get(key, {}), dict) for key in ['browser_profile', 'llm', 'agent']\n\t\t):\n\t\t\t# Check if the values are DB-style entries (have UUIDs as keys)\n\t\t\tif data.get('browser_profile') and all(isinstance(v, dict) and 'id' in v for v in data['browser_profile'].values()):\n\t\t\t\t# Already in new format\n\t\t\t\treturn DBStyleConfigJSON(**data)\n\n\t\t# Old format detected - delete it and create fresh config\n\t\tlogger.info(f'Old config format detected at {config_path}, creating fresh config')\n\t\tnew_config = create_default_config()\n\n\t\t# Overwrite with new config\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\n\t\tlogger.info(f'Created fresh config.json at {config_path}')\n\t\treturn new_config\n\n\texcept Exception as e:\n\t\tlogger.error(f'Failed to load config from {config_path}: {e}, creating fresh config')\n\t\t# On any error, create fresh config\n\t\tnew_config = create_default_config()\n\t\ttry:\n\t\t\twith open(config_path, 'w') as f:\n\t\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\texcept Exception as write_error:\n\t\t\tlogger.error(f'Failed to write fresh config: {write_error}')\n\t\treturn new_config\n\n\nclass Config:\n\t\"\"\"Backward-compatible configuration class that merges all config sources.\n\n\tRe-reads environment variables on every access to maintain compatibility.\n\t\"\"\"\n\n\tdef __init__(self):\n\t\t# Cache for directory creation tracking only\n\t\tself._dirs_created = False\n\n\tdef __getattr__(self, name: str) -> Any:\n\t\t\"\"\"Dynamically proxy all attributes to fresh instances.\n\n\t\tThis ensures env vars are re-read on every access.\n\t\t\"\"\"\n\t\t# Special handling for internal attributes\n\t\tif name.startswith('_'):\n\t\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\t\t# Create fresh instances on every access\n\t\told_config = OldConfig()\n\n\t\t# Always use old config for all attributes (it handles env vars with proper transformations)\n\t\tif hasattr(old_config, name):\n\t\t\treturn getattr(old_config, name)\n\n\t\t# For new MCP-specific attributes not in old config\n\t\tenv_config = FlatEnvConfig()\n\t\tif hasattr(env_config, name):\n\t\t\treturn getattr(env_config, name)\n\n\t\t# Handle special methods\n\t\tif name == 'get_default_profile':\n\t\t\treturn lambda: self._get_default_profile()\n\t\telif name == 'get_default_llm':\n\t\t\treturn lambda: self._get_default_llm()\n\t\telif name == 'get_default_agent':\n\t\t\treturn lambda: self._get_default_agent()\n\t\telif name == 'load_config':\n\t\t\treturn lambda: self._load_config()\n\t\telif name == '_ensure_dirs':\n\t\t\treturn lambda: old_config._ensure_dirs()\n\n\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\tdef _get_config_path(self) -> Path:\n\t\t\"\"\"Get config path from fresh env config.\"\"\"\n\t\tenv_config = FlatEnvConfig()\n\t\tif env_config.BROWSER_USE_CONFIG_PATH:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_PATH).expanduser()\n\t\telif env_config.BROWSER_USE_CONFIG_DIR:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_DIR).expanduser() / 'config.json'\n\t\telse:\n\t\t\txdg_config = Path(env_config.XDG_CONFIG_HOME).expanduser()\n\t\t\treturn xdg_config / 'browseruse' / 'config.json'\n\n\tdef _get_db_config(self) -> DBStyleConfigJSON:\n\t\t\"\"\"Load and migrate config.json.\"\"\"\n\t\tconfig_path = self._get_config_path()\n\t\treturn load_and_migrate_config(config_path)\n\n\tdef _get_default_profile(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default browser profile configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor profile in db_config.browser_profile.values():\n\t\t\tif profile.default:\n\t\t\t\treturn profile.model_dump(exclude_none=True)\n\n\t\t# Return first profile if no default\n\t\tif db_config.browser_profile:\n\t\t\treturn next(iter(db_config.browser_profile.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_llm(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default LLM configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor llm in db_config.llm.values():\n\t\t\tif llm.default:\n\t\t\t\treturn llm.model_dump(exclude_none=True)\n\n\t\t# Return first LLM if no default\n\t\tif db_config.llm:\n\t\t\treturn next(iter(db_config.llm.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_agent(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default agent configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor agent in db_config.agent.values():\n\t\t\tif agent.default:\n\t\t\t\treturn agent.model_dump(exclude_none=True)\n\n\t\t# Return first agent if no default\n\t\tif db_config.agent:\n\t\t\treturn next(iter(db_config.agent.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _load_config(self) -> dict[str, Any]:\n\t\t\"\"\"Load configuration with env var overrides for MCP components.\"\"\"\n\t\tconfig = {\n\t\t\t'browser_profile': self._get_default_profile(),\n\t\t\t'llm': self._get_default_llm(),\n\t\t\t'agent': self._get_default_agent(),\n\t\t}\n\n\t\t# Fresh env config for overrides\n\t\tenv_config = FlatEnvConfig()\n\n\t\t# Apply MCP-specific env var overrides\n\t\tif env_config.BROWSER_USE_HEADLESS is not None:\n\t\t\tconfig['browser_profile']['headless'] = env_config.BROWSER_USE_HEADLESS\n\n\t\tif env_config.BROWSER_USE_ALLOWED_DOMAINS:\n\t\t\tdomains = [d.strip() for d in env_config.BROWSER_USE_ALLOWED_DOMAINS.split(',') if d.strip()]\n\t\t\tconfig['browser_profile']['allowed_domains'] = domains\n\n\t\tif env_config.OPENAI_API_KEY:\n\t\t\tconfig['llm']['api_key'] = env_config.OPENAI_API_KEY\n\n\t\tif env_config.BROWSER_USE_LLM_MODEL:\n\t\t\tconfig['llm']['model'] = env_config.BROWSER_USE_LLM_MODEL\n\n\t\treturn config\n\n\n# Create singleton instance\nCONFIG = Config()\n\n\n# Helper functions for MCP components\ndef load_browser_use_config() -> dict[str, Any]:\n\t\"\"\"Load browser-use configuration for MCP components.\"\"\"\n\treturn CONFIG.load_config()\n\n\ndef get_default_profile(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default browser profile from config dict.\"\"\"\n\treturn config.get('browser_profile', {})\n\n\ndef get_default_llm(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default LLM config from config dict.\"\"\"\n\treturn config.get('llm', {})\n"}, "02a1f452557611dc74371ff755970082dbb1a353": {"t": 1785753148.4177215, "n": 15062, "h": "85499851ba269d1d", "c": "\"\"\"Configuration system for browser-use with automatic migration support.\"\"\"\n\nimport json\nimport logging\nimport os\nfrom datetime import datetime\nfrom functools import cache\nfrom pathlib import Path\nfrom typing import Any\nfrom uuid import uuid4\n\nimport psutil\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass OldConfig:\n\t\"\"\"Original lazy-loading configuration class for environment variables.\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_API_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\t@property\n\tdef BROWSER_USE_EXTENSIONS_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'extensions'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'extensions').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\nclass FlatEnvConfig(BaseSettings):\n\t\"\"\"All environment variables in a flat namespace.\"\"\"\n\n\tmodel_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True, extra='allow')\n\n\t# Logging and telemetry\n\tBROWSER_USE_LOGGING_LEVEL: str = Field(default='info')\n\tCDP_LOGGING_LEVEL: str = Field(default='warning')\n\tANONYMIZED_TELEMETRY: bool = Field(default=True)\n\tBROWSER_USE_CLOUD_SYNC: bool | None = Field(default=None)\n\tBROWSER_USE_CLOUD_API_URL: str = Field(default='https://api.browser-use.com')\n\tBROWSER_USE_CLOUD_UI_URL: str = Field(default='')\n\n\t# Path configuration\n\tXDG_CACHE_HOME: str = Field(default='~/.cache')\n\tXDG_CONFIG_HOME: str = Field(default='~/.config')\n\tBROWSER_USE_CONFIG_DIR: str | None = Field(default=None)\n\n\t# LLM API keys\n\tOPENAI_API_KEY: str = Field(default='')\n\tANTHROPIC_API_KEY: str = Field(default='')\n\tGOOGLE_API_KEY: str = Field(default='')\n\tDEEPSEEK_API_KEY: str = Field(default='')\n\tGROK_API_KEY: str = Field(default='')\n\tNOVITA_API_KEY: str = Field(default='')\n\tAZURE_OPENAI_ENDPOINT: str = Field(default='')\n\tAZURE_OPENAI_KEY: str = Field(default='')\n\tSKIP_LLM_API_KEY_VERIFICATION: bool = Field(default=False)\n\n\t# Runtime hints\n\tIN_DOCKER: bool | None = Field(default=None)\n\tIS_IN_EVALS: bool = Field(default=False)\n\tWIN_FONT_DIR: str = Field(default='C:\\\\Windows\\\\Fonts')\n\n\t# MCP-specific env vars\n\tBROWSER_USE_CONFIG_PATH: str | None = Field(default=None)\n\tBROWSER_USE_HEADLESS: bool | None = Field(default=None)\n\tBROWSER_USE_ALLOWED_DOMAINS: str | None = Field(default=None)\n\tBROWSER_USE_LLM_MODEL: str | None = Field(default=None)\n\n\nclass DBStyleEntry(BaseModel):\n\t\"\"\"Database-style entry with UUID and metadata.\"\"\"\n\n\tid: str = Field(default_factory=lambda: str(uuid4()))\n\tdefault: bool = Field(default=False)\n\tcreated_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n\n\nclass BrowserProfileEntry(DBStyleEntry):\n\t\"\"\"Browser profile configuration entry - accepts any BrowserProfile fields.\"\"\"\n\n\tmodel_config = ConfigDict(extra='allow')\n\n\t# Common browser profile fields for reference\n\theadless: bool | None = None\n\tuser_data_dir: str | None = None\n\tallowed_domains: list[str] | None = None\n\tdownloads_path: str | None = None\n\n\nclass LLMEntry(DBStyleEntry):\n\t\"\"\"LLM configuration entry.\"\"\"\n\n\tapi_key: str | None = None\n\tmodel: str | None = None\n\ttemperature: float | None = None\n\tmax_tokens: int | None = None\n\n\nclass AgentEntry(DBStyleEntry):\n\t\"\"\"Agent configuration entry.\"\"\"\n\n\tmax_steps: int | None = None\n\tuse_vision: bool | None = None\n\tsystem_prompt: str | None = None\n\n\nclass DBStyleConfigJSON(BaseModel):\n\t\"\"\"New database-style configuration format.\"\"\"\n\n\tbrowser_profile: dict[str, BrowserProfileEntry] = Field(default_factory=dict)\n\tllm: dict[str, LLMEntry] = Field(default_factory=dict)\n\tagent: dict[str, AgentEntry] = Field(default_factory=dict)\n\n\ndef create_default_config() -> DBStyleConfigJSON:\n\t\"\"\"Create a fresh default configuration.\"\"\"\n\tlogger.info('Creating fresh default config.json')\n\n\tnew_config = DBStyleConfigJSON()\n\n\t# Generate default IDs\n\tprofile_id = str(uuid4())\n\tllm_id = str(uuid4())\n\tagent_id = str(uuid4())\n\n\t# Create default browser profile entry\n\tnew_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)\n\n\t# Create default LLM entry\n\tnew_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4o', api_key='your-openai-api-key-here')\n\n\t# Create default agent entry\n\tnew_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)\n\n\treturn new_config\n\n\ndef load_and_migrate_config(config_path: Path) -> DBStyleConfigJSON:\n\t\"\"\"Load config.json or create fresh one if old format detected.\"\"\"\n\tif not config_path.exists():\n\t\t# Create fresh config with defaults\n\t\tconfig_path.parent.mkdir(parents=True, exist_ok=True)\n\t\tnew_config = create_default_config()\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\treturn new_config\n\n\ttry:\n\t\twith open(config_path) as f:\n\t\t\tdata = json.load(f)\n\n\t\t# Check if it's already in DB-style format\n\t\tif all(key in data for key in ['browser_profile', 'llm', 'agent']) and all(\n\t\t\tisinstance(data.get(key, {}), dict) for key in ['browser_profile', 'llm', 'agent']\n\t\t):\n\t\t\t# Check if the values are DB-style entries (have UUIDs as keys)\n\t\t\tif data.get('browser_profile') and all(isinstance(v, dict) and 'id' in v for v in data['browser_profile'].values()):\n\t\t\t\t# Already in new format\n\t\t\t\treturn DBStyleConfigJSON(**data)\n\n\t\t# Old format detected - delete it and create fresh config\n\t\tlogger.info(f'Old config format detected at {config_path}, creating fresh config')\n\t\tnew_config = create_default_config()\n\n\t\t# Overwrite with new config\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\n\t\tlogger.info(f'Created fresh config.json at {config_path}')\n\t\treturn new_config\n\n\texcept Exception as e:\n\t\tlogger.error(f'Failed to load config from {config_path}: {e}, creating fresh config')\n\t\t# On any error, create fresh config\n\t\tnew_config = create_default_config()\n\t\ttry:\n\t\t\twith open(config_path, 'w') as f:\n\t\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\texcept Exception as write_error:\n\t\t\tlogger.error(f'Failed to write fresh config: {write_error}')\n\t\treturn new_config\n\n\nclass Config:\n\t\"\"\"Backward-compatible configuration class that merges all config sources.\n\n\tRe-reads environment variables on every access to maintain compatibility.\n\t\"\"\"\n\n\tdef __init__(self):\n\t\t# Cache for directory creation tracking only\n\t\tself._dirs_created = False\n\n\tdef __getattr__(self, name: str) -> Any:\n\t\t\"\"\"Dynamically proxy all attributes to fresh instances.\n\n\t\tThis ensures env vars are re-read on every access.\n\t\t\"\"\"\n\t\t# Special handling for internal attributes\n\t\tif name.startswith('_'):\n\t\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\t\t# Create fresh instances on every access\n\t\told_config = OldConfig()\n\n\t\t# Always use old config for all attributes (it handles env vars with proper transformations)\n\t\tif hasattr(old_config, name):\n\t\t\treturn getattr(old_config, name)\n\n\t\t# For new MCP-specific attributes not in old config\n\t\tenv_config = FlatEnvConfig()\n\t\tif hasattr(env_config, name):\n\t\t\treturn getattr(env_config, name)\n\n\t\t# Handle special methods\n\t\tif name == 'get_default_profile':\n\t\t\treturn lambda: self._get_default_profile()\n\t\telif name == 'get_default_llm':\n\t\t\treturn lambda: self._get_default_llm()\n\t\telif name == 'get_default_agent':\n\t\t\treturn lambda: self._get_default_agent()\n\t\telif name == 'load_config':\n\t\t\treturn lambda: self._load_config()\n\t\telif name == '_ensure_dirs':\n\t\t\treturn lambda: old_config._ensure_dirs()\n\n\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\tdef _get_config_path(self) -> Path:\n\t\t\"\"\"Get config path from fresh env config.\"\"\"\n\t\tenv_config = FlatEnvConfig()\n\t\tif env_config.BROWSER_USE_CONFIG_PATH:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_PATH).expanduser()\n\t\telif env_config.BROWSER_USE_CONFIG_DIR:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_DIR).expanduser() / 'config.json'\n\t\telse:\n\t\t\txdg_config = Path(env_config.XDG_CONFIG_HOME).expanduser()\n\t\t\treturn xdg_config / 'browseruse' / 'config.json'\n\n\tdef _get_db_config(self) -> DBStyleConfigJSON:\n\t\t\"\"\"Load and migrate config.json.\"\"\"\n\t\tconfig_path = self._get_config_path()\n\t\treturn load_and_migrate_config(config_path)\n\n\tdef _get_default_profile(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default browser profile configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor profile in db_config.browser_profile.values():\n\t\t\tif profile.default:\n\t\t\t\treturn profile.model_dump(exclude_none=True)\n\n\t\t# Return first profile if no default\n\t\tif db_config.browser_profile:\n\t\t\treturn next(iter(db_config.browser_profile.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_llm(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default LLM configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor llm in db_config.llm.values():\n\t\t\tif llm.default:\n\t\t\t\treturn llm.model_dump(exclude_none=True)\n\n\t\t# Return first LLM if no default\n\t\tif db_config.llm:\n\t\t\treturn next(iter(db_config.llm.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_agent(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default agent configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor agent in db_config.agent.values():\n\t\t\tif agent.default:\n\t\t\t\treturn agent.model_dump(exclude_none=True)\n\n\t\t# Return first agent if no default\n\t\tif db_config.agent:\n\t\t\treturn next(iter(db_config.agent.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _load_config(self) -> dict[str, Any]:\n\t\t\"\"\"Load configuration with env var overrides for MCP components.\"\"\"\n\t\tconfig = {\n\t\t\t'browser_profile': self._get_default_profile(),\n\t\t\t'llm': self._get_default_llm(),\n\t\t\t'agent': self._get_default_agent(),\n\t\t}\n\n\t\t# Fresh env config for overrides\n\t\tenv_config = FlatEnvConfig()\n\n\t\t# Apply MCP-specific env var overrides\n\t\tif env_config.BROWSER_USE_HEADLESS is not None:\n\t\t\tconfig['browser_profile']['headless'] = env_config.BROWSER_USE_HEADLESS\n\n\t\tif env_config.BROWSER_USE_ALLOWED_DOMAINS:\n\t\t\tdomains = [d.strip() for d in env_config.BROWSER_USE_ALLOWED_DOMAINS.split(',') if d.strip()]\n\t\t\tconfig['browser_profile']['allowed_domains'] = domains\n\n\t\tif env_config.OPENAI_API_KEY:\n\t\t\tconfig['llm']['api_key'] = env_config.OPENAI_API_KEY\n\n\t\tif env_config.BROWSER_USE_LLM_MODEL:\n\t\t\tconfig['llm']['model'] = env_config.BROWSER_USE_LLM_MODEL\n\n\t\treturn config\n\n\n# Create singleton instance\nCONFIG = Config()\n\n\n# Helper functions for MCP components\ndef load_browser_use_config() -> dict[str, Any]:\n\t\"\"\"Load browser-use configuration for MCP components.\"\"\"\n\treturn CONFIG.load_config()\n\n\ndef get_default_profile(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default browser profile from config dict.\"\"\"\n\treturn config.get('browser_profile', {})\n\n\ndef get_default_llm(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default LLM config from config dict.\"\"\"\n\treturn config.get('llm', {})\n"}, "2f3a19bf7e61c4aa2c070c32f01e34a65178d12b": {"t": 1785753149.8997076, "n": 15062, "h": "41c2838c388abade", "c": "\"\"\"Configuration system for browser-use with automatic migration support.\"\"\"\n\nimport json\nimport logging\nimport os\nfrom datetime import datetime\nfrom functools import cache\nfrom pathlib import Path\nfrom typing import Any\nfrom uuid import uuid4\n\nimport psutil\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass OldConfig:\n\t\"\"\"Original lazy-loading configuration class for environment variables.\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_API_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\t@property\n\tdef BROWSER_USE_EXTENSIONS_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'extensions'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'extensions').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\nclass FlatEnvConfig(BaseSettings):\n\t\"\"\"All environment variables in a flat namespace.\"\"\"\n\n\tmodel_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True, extra='allow')\n\n\t# Logging and telemetry\n\tBROWSER_USE_LOGGING_LEVEL: str = Field(default='info')\n\tCDP_LOGGING_LEVEL: str = Field(default='WARNING')\n\tANONYMIZED_TELEMETRY: bool = Field(default=True)\n\tBROWSER_USE_CLOUD_SYNC: bool | None = Field(default=None)\n\tBROWSER_USE_CLOUD_API_URL: str = Field(default='https://api.browser-use.com')\n\tBROWSER_USE_CLOUD_UI_URL: str = Field(default='')\n\n\t# Path configuration\n\tXDG_CACHE_HOME: str = Field(default='~/.cache')\n\tXDG_CONFIG_HOME: str = Field(default='~/.config')\n\tBROWSER_USE_CONFIG_DIR: str | None = Field(default=None)\n\n\t# LLM API keys\n\tOPENAI_API_KEY: str = Field(default='')\n\tANTHROPIC_API_KEY: str = Field(default='')\n\tGOOGLE_API_KEY: str = Field(default='')\n\tDEEPSEEK_API_KEY: str = Field(default='')\n\tGROK_API_KEY: str = Field(default='')\n\tNOVITA_API_KEY: str = Field(default='')\n\tAZURE_OPENAI_ENDPOINT: str = Field(default='')\n\tAZURE_OPENAI_KEY: str = Field(default='')\n\tSKIP_LLM_API_KEY_VERIFICATION: bool = Field(default=False)\n\n\t# Runtime hints\n\tIN_DOCKER: bool | None = Field(default=None)\n\tIS_IN_EVALS: bool = Field(default=False)\n\tWIN_FONT_DIR: str = Field(default='C:\\\\Windows\\\\Fonts')\n\n\t# MCP-specific env vars\n\tBROWSER_USE_CONFIG_PATH: str | None = Field(default=None)\n\tBROWSER_USE_HEADLESS: bool | None = Field(default=None)\n\tBROWSER_USE_ALLOWED_DOMAINS: str | None = Field(default=None)\n\tBROWSER_USE_LLM_MODEL: str | None = Field(default=None)\n\n\nclass DBStyleEntry(BaseModel):\n\t\"\"\"Database-style entry with UUID and metadata.\"\"\"\n\n\tid: str = Field(default_factory=lambda: str(uuid4()))\n\tdefault: bool = Field(default=False)\n\tcreated_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n\n\nclass BrowserProfileEntry(DBStyleEntry):\n\t\"\"\"Browser profile configuration entry - accepts any BrowserProfile fields.\"\"\"\n\n\tmodel_config = ConfigDict(extra='allow')\n\n\t# Common browser profile fields for reference\n\theadless: bool | None = None\n\tuser_data_dir: str | None = None\n\tallowed_domains: list[str] | None = None\n\tdownloads_path: str | None = None\n\n\nclass LLMEntry(DBStyleEntry):\n\t\"\"\"LLM configuration entry.\"\"\"\n\n\tapi_key: str | None = None\n\tmodel: str | None = None\n\ttemperature: float | None = None\n\tmax_tokens: int | None = None\n\n\nclass AgentEntry(DBStyleEntry):\n\t\"\"\"Agent configuration entry.\"\"\"\n\n\tmax_steps: int | None = None\n\tuse_vision: bool | None = None\n\tsystem_prompt: str | None = None\n\n\nclass DBStyleConfigJSON(BaseModel):\n\t\"\"\"New database-style configuration format.\"\"\"\n\n\tbrowser_profile: dict[str, BrowserProfileEntry] = Field(default_factory=dict)\n\tllm: dict[str, LLMEntry] = Field(default_factory=dict)\n\tagent: dict[str, AgentEntry] = Field(default_factory=dict)\n\n\ndef create_default_config() -> DBStyleConfigJSON:\n\t\"\"\"Create a fresh default configuration.\"\"\"\n\tlogger.info('Creating fresh default config.json')\n\n\tnew_config = DBStyleConfigJSON()\n\n\t# Generate default IDs\n\tprofile_id = str(uuid4())\n\tllm_id = str(uuid4())\n\tagent_id = str(uuid4())\n\n\t# Create default browser profile entry\n\tnew_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)\n\n\t# Create default LLM entry\n\tnew_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4o', api_key='your-openai-api-key-here')\n\n\t# Create default agent entry\n\tnew_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)\n\n\treturn new_config\n\n\ndef load_and_migrate_config(config_path: Path) -> DBStyleConfigJSON:\n\t\"\"\"Load config.json or create fresh one if old format detected.\"\"\"\n\tif not config_path.exists():\n\t\t# Create fresh config with defaults\n\t\tconfig_path.parent.mkdir(parents=True, exist_ok=True)\n\t\tnew_config = create_default_config()\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\treturn new_config\n\n\ttry:\n\t\twith open(config_path) as f:\n\t\t\tdata = json.load(f)\n\n\t\t# Check if it's already in DB-style format\n\t\tif all(key in data for key in ['browser_profile', 'llm', 'agent']) and all(\n\t\t\tisinstance(data.get(key, {}), dict) for key in ['browser_profile', 'llm', 'agent']\n\t\t):\n\t\t\t# Check if the values are DB-style entries (have UUIDs as keys)\n\t\t\tif data.get('browser_profile') and all(isinstance(v, dict) and 'id' in v for v in data['browser_profile'].values()):\n\t\t\t\t# Already in new format\n\t\t\t\treturn DBStyleConfigJSON(**data)\n\n\t\t# Old format detected - delete it and create fresh config\n\t\tlogger.info(f'Old config format detected at {config_path}, creating fresh config')\n\t\tnew_config = create_default_config()\n\n\t\t# Overwrite with new config\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\n\t\tlogger.info(f'Created fresh config.json at {config_path}')\n\t\treturn new_config\n\n\texcept Exception as e:\n\t\tlogger.error(f'Failed to load config from {config_path}: {e}, creating fresh config')\n\t\t# On any error, create fresh config\n\t\tnew_config = create_default_config()\n\t\ttry:\n\t\t\twith open(config_path, 'w') as f:\n\t\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\texcept Exception as write_error:\n\t\t\tlogger.error(f'Failed to write fresh config: {write_error}')\n\t\treturn new_config\n\n\nclass Config:\n\t\"\"\"Backward-compatible configuration class that merges all config sources.\n\n\tRe-reads environment variables on every access to maintain compatibility.\n\t\"\"\"\n\n\tdef __init__(self):\n\t\t# Cache for directory creation tracking only\n\t\tself._dirs_created = False\n\n\tdef __getattr__(self, name: str) -> Any:\n\t\t\"\"\"Dynamically proxy all attributes to fresh instances.\n\n\t\tThis ensures env vars are re-read on every access.\n\t\t\"\"\"\n\t\t# Special handling for internal attributes\n\t\tif name.startswith('_'):\n\t\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\t\t# Create fresh instances on every access\n\t\told_config = OldConfig()\n\n\t\t# Always use old config for all attributes (it handles env vars with proper transformations)\n\t\tif hasattr(old_config, name):\n\t\t\treturn getattr(old_config, name)\n\n\t\t# For new MCP-specific attributes not in old config\n\t\tenv_config = FlatEnvConfig()\n\t\tif hasattr(env_config, name):\n\t\t\treturn getattr(env_config, name)\n\n\t\t# Handle special methods\n\t\tif name == 'get_default_profile':\n\t\t\treturn lambda: self._get_default_profile()\n\t\telif name == 'get_default_llm':\n\t\t\treturn lambda: self._get_default_llm()\n\t\telif name == 'get_default_agent':\n\t\t\treturn lambda: self._get_default_agent()\n\t\telif name == 'load_config':\n\t\t\treturn lambda: self._load_config()\n\t\telif name == '_ensure_dirs':\n\t\t\treturn lambda: old_config._ensure_dirs()\n\n\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\tdef _get_config_path(self) -> Path:\n\t\t\"\"\"Get config path from fresh env config.\"\"\"\n\t\tenv_config = FlatEnvConfig()\n\t\tif env_config.BROWSER_USE_CONFIG_PATH:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_PATH).expanduser()\n\t\telif env_config.BROWSER_USE_CONFIG_DIR:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_DIR).expanduser() / 'config.json'\n\t\telse:\n\t\t\txdg_config = Path(env_config.XDG_CONFIG_HOME).expanduser()\n\t\t\treturn xdg_config / 'browseruse' / 'config.json'\n\n\tdef _get_db_config(self) -> DBStyleConfigJSON:\n\t\t\"\"\"Load and migrate config.json.\"\"\"\n\t\tconfig_path = self._get_config_path()\n\t\treturn load_and_migrate_config(config_path)\n\n\tdef _get_default_profile(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default browser profile configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor profile in db_config.browser_profile.values():\n\t\t\tif profile.default:\n\t\t\t\treturn profile.model_dump(exclude_none=True)\n\n\t\t# Return first profile if no default\n\t\tif db_config.browser_profile:\n\t\t\treturn next(iter(db_config.browser_profile.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_llm(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default LLM configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor llm in db_config.llm.values():\n\t\t\tif llm.default:\n\t\t\t\treturn llm.model_dump(exclude_none=True)\n\n\t\t# Return first LLM if no default\n\t\tif db_config.llm:\n\t\t\treturn next(iter(db_config.llm.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_agent(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default agent configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor agent in db_config.agent.values():\n\t\t\tif agent.default:\n\t\t\t\treturn agent.model_dump(exclude_none=True)\n\n\t\t# Return first agent if no default\n\t\tif db_config.agent:\n\t\t\treturn next(iter(db_config.agent.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _load_config(self) -> dict[str, Any]:\n\t\t\"\"\"Load configuration with env var overrides for MCP components.\"\"\"\n\t\tconfig = {\n\t\t\t'browser_profile': self._get_default_profile(),\n\t\t\t'llm': self._get_default_llm(),\n\t\t\t'agent': self._get_default_agent(),\n\t\t}\n\n\t\t# Fresh env config for overrides\n\t\tenv_config = FlatEnvConfig()\n\n\t\t# Apply MCP-specific env var overrides\n\t\tif env_config.BROWSER_USE_HEADLESS is not None:\n\t\t\tconfig['browser_profile']['headless'] = env_config.BROWSER_USE_HEADLESS\n\n\t\tif env_config.BROWSER_USE_ALLOWED_DOMAINS:\n\t\t\tdomains = [d.strip() for d in env_config.BROWSER_USE_ALLOWED_DOMAINS.split(',') if d.strip()]\n\t\t\tconfig['browser_profile']['allowed_domains'] = domains\n\n\t\tif env_config.OPENAI_API_KEY:\n\t\t\tconfig['llm']['api_key'] = env_config.OPENAI_API_KEY\n\n\t\tif env_config.BROWSER_USE_LLM_MODEL:\n\t\t\tconfig['llm']['model'] = env_config.BROWSER_USE_LLM_MODEL\n\n\t\treturn config\n\n\n# Create singleton instance\nCONFIG = Config()\n\n\n# Helper functions for MCP components\ndef load_browser_use_config() -> dict[str, Any]:\n\t\"\"\"Load browser-use configuration for MCP components.\"\"\"\n\treturn CONFIG.load_config()\n\n\ndef get_default_profile(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default browser profile from config dict.\"\"\"\n\treturn config.get('browser_profile', {})\n\n\ndef get_default_llm(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default LLM config from config dict.\"\"\"\n\treturn config.get('llm', {})\n"}, "f5925b1080c76bc25ac6d2285b6cd5dc17e84ac1": {"t": 1785753151.0607285, "n": 15011, "h": "efd459d030932f26", "c": "\"\"\"Configuration system for browser-use with automatic migration support.\"\"\"\n\nimport json\nimport logging\nimport os\nfrom datetime import datetime\nfrom functools import cache\nfrom pathlib import Path\nfrom typing import Any\nfrom uuid import uuid4\n\nimport psutil\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass OldConfig:\n\t\"\"\"Original lazy-loading configuration class for environment variables.\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_API_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\t@property\n\tdef BROWSER_USE_EXTENSIONS_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'extensions'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'extensions').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\nclass FlatEnvConfig(BaseSettings):\n\t\"\"\"All environment variables in a flat namespace.\"\"\"\n\n\tmodel_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True, extra='allow')\n\n\t# Logging and telemetry\n\tBROWSER_USE_LOGGING_LEVEL: str = Field(default='info')\n\tANONYMIZED_TELEMETRY: bool = Field(default=True)\n\tBROWSER_USE_CLOUD_SYNC: bool | None = Field(default=None)\n\tBROWSER_USE_CLOUD_API_URL: str = Field(default='https://api.browser-use.com')\n\tBROWSER_USE_CLOUD_UI_URL: str = Field(default='')\n\n\t# Path configuration\n\tXDG_CACHE_HOME: str = Field(default='~/.cache')\n\tXDG_CONFIG_HOME: str = Field(default='~/.config')\n\tBROWSER_USE_CONFIG_DIR: str | None = Field(default=None)\n\n\t# LLM API keys\n\tOPENAI_API_KEY: str = Field(default='')\n\tANTHROPIC_API_KEY: str = Field(default='')\n\tGOOGLE_API_KEY: str = Field(default='')\n\tDEEPSEEK_API_KEY: str = Field(default='')\n\tGROK_API_KEY: str = Field(default='')\n\tNOVITA_API_KEY: str = Field(default='')\n\tAZURE_OPENAI_ENDPOINT: str = Field(default='')\n\tAZURE_OPENAI_KEY: str = Field(default='')\n\tSKIP_LLM_API_KEY_VERIFICATION: bool = Field(default=False)\n\n\t# Runtime hints\n\tIN_DOCKER: bool | None = Field(default=None)\n\tIS_IN_EVALS: bool = Field(default=False)\n\tWIN_FONT_DIR: str = Field(default='C:\\\\Windows\\\\Fonts')\n\n\t# MCP-specific env vars\n\tBROWSER_USE_CONFIG_PATH: str | None = Field(default=None)\n\tBROWSER_USE_HEADLESS: bool | None = Field(default=None)\n\tBROWSER_USE_ALLOWED_DOMAINS: str | None = Field(default=None)\n\tBROWSER_USE_LLM_MODEL: str | None = Field(default=None)\n\n\nclass DBStyleEntry(BaseModel):\n\t\"\"\"Database-style entry with UUID and metadata.\"\"\"\n\n\tid: str = Field(default_factory=lambda: str(uuid4()))\n\tdefault: bool = Field(default=False)\n\tcreated_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n\n\nclass BrowserProfileEntry(DBStyleEntry):\n\t\"\"\"Browser profile configuration entry - accepts any BrowserProfile fields.\"\"\"\n\n\tmodel_config = ConfigDict(extra='allow')\n\n\t# Common browser profile fields for reference\n\theadless: bool | None = None\n\tuser_data_dir: str | None = None\n\tallowed_domains: list[str] | None = None\n\tdownloads_path: str | None = None\n\n\nclass LLMEntry(DBStyleEntry):\n\t\"\"\"LLM configuration entry.\"\"\"\n\n\tapi_key: str | None = None\n\tmodel: str | None = None\n\ttemperature: float | None = None\n\tmax_tokens: int | None = None\n\n\nclass AgentEntry(DBStyleEntry):\n\t\"\"\"Agent configuration entry.\"\"\"\n\n\tmax_steps: int | None = None\n\tuse_vision: bool | None = None\n\tsystem_prompt: str | None = None\n\n\nclass DBStyleConfigJSON(BaseModel):\n\t\"\"\"New database-style configuration format.\"\"\"\n\n\tbrowser_profile: dict[str, BrowserProfileEntry] = Field(default_factory=dict)\n\tllm: dict[str, LLMEntry] = Field(default_factory=dict)\n\tagent: dict[str, AgentEntry] = Field(default_factory=dict)\n\n\ndef create_default_config() -> DBStyleConfigJSON:\n\t\"\"\"Create a fresh default configuration.\"\"\"\n\tlogger.info('Creating fresh default config.json')\n\n\tnew_config = DBStyleConfigJSON()\n\n\t# Generate default IDs\n\tprofile_id = str(uuid4())\n\tllm_id = str(uuid4())\n\tagent_id = str(uuid4())\n\n\t# Create default browser profile entry\n\tnew_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)\n\n\t# Create default LLM entry\n\tnew_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4o', api_key='your-openai-api-key-here')\n\n\t# Create default agent entry\n\tnew_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)\n\n\treturn new_config\n\n\ndef load_and_migrate_config(config_path: Path) -> DBStyleConfigJSON:\n\t\"\"\"Load config.json or create fresh one if old format detected.\"\"\"\n\tif not config_path.exists():\n\t\t# Create fresh config with defaults\n\t\tconfig_path.parent.mkdir(parents=True, exist_ok=True)\n\t\tnew_config = create_default_config()\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\treturn new_config\n\n\ttry:\n\t\twith open(config_path) as f:\n\t\t\tdata = json.load(f)\n\n\t\t# Check if it's already in DB-style format\n\t\tif all(key in data for key in ['browser_profile', 'llm', 'agent']) and all(\n\t\t\tisinstance(data.get(key, {}), dict) for key in ['browser_profile', 'llm', 'agent']\n\t\t):\n\t\t\t# Check if the values are DB-style entries (have UUIDs as keys)\n\t\t\tif data.get('browser_profile') and all(isinstance(v, dict) and 'id' in v for v in data['browser_profile'].values()):\n\t\t\t\t# Already in new format\n\t\t\t\treturn DBStyleConfigJSON(**data)\n\n\t\t# Old format detected - delete it and create fresh config\n\t\tlogger.info(f'Old config format detected at {config_path}, creating fresh config')\n\t\tnew_config = create_default_config()\n\n\t\t# Overwrite with new config\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\n\t\tlogger.info(f'Created fresh config.json at {config_path}')\n\t\treturn new_config\n\n\texcept Exception as e:\n\t\tlogger.error(f'Failed to load config from {config_path}: {e}, creating fresh config')\n\t\t# On any error, create fresh config\n\t\tnew_config = create_default_config()\n\t\ttry:\n\t\t\twith open(config_path, 'w') as f:\n\t\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\texcept Exception as write_error:\n\t\t\tlogger.error(f'Failed to write fresh config: {write_error}')\n\t\treturn new_config\n\n\nclass Config:\n\t\"\"\"Backward-compatible configuration class that merges all config sources.\n\n\tRe-reads environment variables on every access to maintain compatibility.\n\t\"\"\"\n\n\tdef __init__(self):\n\t\t# Cache for directory creation tracking only\n\t\tself._dirs_created = False\n\n\tdef __getattr__(self, name: str) -> Any:\n\t\t\"\"\"Dynamically proxy all attributes to fresh instances.\n\n\t\tThis ensures env vars are re-read on every access.\n\t\t\"\"\"\n\t\t# Special handling for internal attributes\n\t\tif name.startswith('_'):\n\t\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\t\t# Create fresh instances on every access\n\t\told_config = OldConfig()\n\n\t\t# Always use old config for all attributes (it handles env vars with proper transformations)\n\t\tif hasattr(old_config, name):\n\t\t\treturn getattr(old_config, name)\n\n\t\t# For new MCP-specific attributes not in old config\n\t\tenv_config = FlatEnvConfig()\n\t\tif hasattr(env_config, name):\n\t\t\treturn getattr(env_config, name)\n\n\t\t# Handle special methods\n\t\tif name == 'get_default_profile':\n\t\t\treturn lambda: self._get_default_profile()\n\t\telif name == 'get_default_llm':\n\t\t\treturn lambda: self._get_default_llm()\n\t\telif name == 'get_default_agent':\n\t\t\treturn lambda: self._get_default_agent()\n\t\telif name == 'load_config':\n\t\t\treturn lambda: self._load_config()\n\t\telif name == '_ensure_dirs':\n\t\t\treturn lambda: old_config._ensure_dirs()\n\n\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\tdef _get_config_path(self) -> Path:\n\t\t\"\"\"Get config path from fresh env config.\"\"\"\n\t\tenv_config = FlatEnvConfig()\n\t\tif env_config.BROWSER_USE_CONFIG_PATH:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_PATH).expanduser()\n\t\telif env_config.BROWSER_USE_CONFIG_DIR:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_DIR).expanduser() / 'config.json'\n\t\telse:\n\t\t\txdg_config = Path(env_config.XDG_CONFIG_HOME).expanduser()\n\t\t\treturn xdg_config / 'browseruse' / 'config.json'\n\n\tdef _get_db_config(self) -> DBStyleConfigJSON:\n\t\t\"\"\"Load and migrate config.json.\"\"\"\n\t\tconfig_path = self._get_config_path()\n\t\treturn load_and_migrate_config(config_path)\n\n\tdef _get_default_profile(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default browser profile configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor profile in db_config.browser_profile.values():\n\t\t\tif profile.default:\n\t\t\t\treturn profile.model_dump(exclude_none=True)\n\n\t\t# Return first profile if no default\n\t\tif db_config.browser_profile:\n\t\t\treturn next(iter(db_config.browser_profile.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_llm(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default LLM configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor llm in db_config.llm.values():\n\t\t\tif llm.default:\n\t\t\t\treturn llm.model_dump(exclude_none=True)\n\n\t\t# Return first LLM if no default\n\t\tif db_config.llm:\n\t\t\treturn next(iter(db_config.llm.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_agent(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default agent configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor agent in db_config.agent.values():\n\t\t\tif agent.default:\n\t\t\t\treturn agent.model_dump(exclude_none=True)\n\n\t\t# Return first agent if no default\n\t\tif db_config.agent:\n\t\t\treturn next(iter(db_config.agent.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _load_config(self) -> dict[str, Any]:\n\t\t\"\"\"Load configuration with env var overrides for MCP components.\"\"\"\n\t\tconfig = {\n\t\t\t'browser_profile': self._get_default_profile(),\n\t\t\t'llm': self._get_default_llm(),\n\t\t\t'agent': self._get_default_agent(),\n\t\t}\n\n\t\t# Fresh env config for overrides\n\t\tenv_config = FlatEnvConfig()\n\n\t\t# Apply MCP-specific env var overrides\n\t\tif env_config.BROWSER_USE_HEADLESS is not None:\n\t\t\tconfig['browser_profile']['headless'] = env_config.BROWSER_USE_HEADLESS\n\n\t\tif env_config.BROWSER_USE_ALLOWED_DOMAINS:\n\t\t\tdomains = [d.strip() for d in env_config.BROWSER_USE_ALLOWED_DOMAINS.split(',') if d.strip()]\n\t\t\tconfig['browser_profile']['allowed_domains'] = domains\n\n\t\tif env_config.OPENAI_API_KEY:\n\t\t\tconfig['llm']['api_key'] = env_config.OPENAI_API_KEY\n\n\t\tif env_config.BROWSER_USE_LLM_MODEL:\n\t\t\tconfig['llm']['model'] = env_config.BROWSER_USE_LLM_MODEL\n\n\t\treturn config\n\n\n# Create singleton instance\nCONFIG = Config()\n\n\n# Helper functions for MCP components\ndef load_browser_use_config() -> dict[str, Any]:\n\t\"\"\"Load browser-use configuration for MCP components.\"\"\"\n\treturn CONFIG.load_config()\n\n\ndef get_default_profile(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default browser profile from config dict.\"\"\"\n\treturn config.get('browser_profile', {})\n\n\ndef get_default_llm(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default LLM config from config dict.\"\"\"\n\treturn config.get('llm', {})\n"}, "6dcf479fc6d2f2b26ed2c0a4c0f161b0254c3396": {"t": 1785753152.4318542, "n": 14798, "h": "59342f2f6e1ec974", "c": "\"\"\"Configuration system for browser-use with automatic migration support.\"\"\"\n\nimport json\nimport logging\nimport os\nfrom datetime import datetime\nfrom functools import cache\nfrom pathlib import Path\nfrom typing import Any\nfrom uuid import uuid4\n\nimport psutil\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass OldConfig:\n\t\"\"\"Original lazy-loading configuration class for environment variables.\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_API_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\nclass FlatEnvConfig(BaseSettings):\n\t\"\"\"All environment variables in a flat namespace.\"\"\"\n\n\tmodel_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True, extra='allow')\n\n\t# Logging and telemetry\n\tBROWSER_USE_LOGGING_LEVEL: str = Field(default='info')\n\tANONYMIZED_TELEMETRY: bool = Field(default=True)\n\tBROWSER_USE_CLOUD_SYNC: bool | None = Field(default=None)\n\tBROWSER_USE_CLOUD_API_URL: str = Field(default='https://api.browser-use.com')\n\tBROWSER_USE_CLOUD_UI_URL: str = Field(default='')\n\n\t# Path configuration\n\tXDG_CACHE_HOME: str = Field(default='~/.cache')\n\tXDG_CONFIG_HOME: str = Field(default='~/.config')\n\tBROWSER_USE_CONFIG_DIR: str | None = Field(default=None)\n\n\t# LLM API keys\n\tOPENAI_API_KEY: str = Field(default='')\n\tANTHROPIC_API_KEY: str = Field(default='')\n\tGOOGLE_API_KEY: str = Field(default='')\n\tDEEPSEEK_API_KEY: str = Field(default='')\n\tGROK_API_KEY: str = Field(default='')\n\tNOVITA_API_KEY: str = Field(default='')\n\tAZURE_OPENAI_ENDPOINT: str = Field(default='')\n\tAZURE_OPENAI_KEY: str = Field(default='')\n\tSKIP_LLM_API_KEY_VERIFICATION: bool = Field(default=False)\n\n\t# Runtime hints\n\tIN_DOCKER: bool | None = Field(default=None)\n\tIS_IN_EVALS: bool = Field(default=False)\n\tWIN_FONT_DIR: str = Field(default='C:\\\\Windows\\\\Fonts')\n\n\t# MCP-specific env vars\n\tBROWSER_USE_CONFIG_PATH: str | None = Field(default=None)\n\tBROWSER_USE_HEADLESS: bool | None = Field(default=None)\n\tBROWSER_USE_ALLOWED_DOMAINS: str | None = Field(default=None)\n\tBROWSER_USE_LLM_MODEL: str | None = Field(default=None)\n\n\nclass DBStyleEntry(BaseModel):\n\t\"\"\"Database-style entry with UUID and metadata.\"\"\"\n\n\tid: str = Field(default_factory=lambda: str(uuid4()))\n\tdefault: bool = Field(default=False)\n\tcreated_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n\n\nclass BrowserProfileEntry(DBStyleEntry):\n\t\"\"\"Browser profile configuration entry - accepts any BrowserProfile fields.\"\"\"\n\n\tmodel_config = ConfigDict(extra='allow')\n\n\t# Common browser profile fields for reference\n\theadless: bool | None = None\n\tuser_data_dir: str | None = None\n\tallowed_domains: list[str] | None = None\n\tdownloads_path: str | None = None\n\n\nclass LLMEntry(DBStyleEntry):\n\t\"\"\"LLM configuration entry.\"\"\"\n\n\tapi_key: str | None = None\n\tmodel: str | None = None\n\ttemperature: float | None = None\n\tmax_tokens: int | None = None\n\n\nclass AgentEntry(DBStyleEntry):\n\t\"\"\"Agent configuration entry.\"\"\"\n\n\tmax_steps: int | None = None\n\tuse_vision: bool | None = None\n\tsystem_prompt: str | None = None\n\n\nclass DBStyleConfigJSON(BaseModel):\n\t\"\"\"New database-style configuration format.\"\"\"\n\n\tbrowser_profile: dict[str, BrowserProfileEntry] = Field(default_factory=dict)\n\tllm: dict[str, LLMEntry] = Field(default_factory=dict)\n\tagent: dict[str, AgentEntry] = Field(default_factory=dict)\n\n\ndef create_default_config() -> DBStyleConfigJSON:\n\t\"\"\"Create a fresh default configuration.\"\"\"\n\tlogger.info('Creating fresh default config.json')\n\n\tnew_config = DBStyleConfigJSON()\n\n\t# Generate default IDs\n\tprofile_id = str(uuid4())\n\tllm_id = str(uuid4())\n\tagent_id = str(uuid4())\n\n\t# Create default browser profile entry\n\tnew_config.browser_profile[profile_id] = BrowserProfileEntry(id=profile_id, default=True, headless=False, user_data_dir=None)\n\n\t# Create default LLM entry\n\tnew_config.llm[llm_id] = LLMEntry(id=llm_id, default=True, model='gpt-4o', api_key='your-openai-api-key-here')\n\n\t# Create default agent entry\n\tnew_config.agent[agent_id] = AgentEntry(id=agent_id, default=True)\n\n\treturn new_config\n\n\ndef load_and_migrate_config(config_path: Path) -> DBStyleConfigJSON:\n\t\"\"\"Load config.json or create fresh one if old format detected.\"\"\"\n\tif not config_path.exists():\n\t\t# Create fresh config with defaults\n\t\tconfig_path.parent.mkdir(parents=True, exist_ok=True)\n\t\tnew_config = create_default_config()\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\treturn new_config\n\n\ttry:\n\t\twith open(config_path) as f:\n\t\t\tdata = json.load(f)\n\n\t\t# Check if it's already in DB-style format\n\t\tif all(key in data for key in ['browser_profile', 'llm', 'agent']) and all(\n\t\t\tisinstance(data.get(key, {}), dict) for key in ['browser_profile', 'llm', 'agent']\n\t\t):\n\t\t\t# Check if the values are DB-style entries (have UUIDs as keys)\n\t\t\tif data.get('browser_profile') and all(isinstance(v, dict) and 'id' in v for v in data['browser_profile'].values()):\n\t\t\t\t# Already in new format\n\t\t\t\treturn DBStyleConfigJSON(**data)\n\n\t\t# Old format detected - delete it and create fresh config\n\t\tlogger.info(f'Old config format detected at {config_path}, creating fresh config')\n\t\tnew_config = create_default_config()\n\n\t\t# Overwrite with new config\n\t\twith open(config_path, 'w') as f:\n\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\n\t\tlogger.info(f'Created fresh config.json at {config_path}')\n\t\treturn new_config\n\n\texcept Exception as e:\n\t\tlogger.error(f'Failed to load config from {config_path}: {e}, creating fresh config')\n\t\t# On any error, create fresh config\n\t\tnew_config = create_default_config()\n\t\ttry:\n\t\t\twith open(config_path, 'w') as f:\n\t\t\t\tjson.dump(new_config.model_dump(), f, indent=2)\n\t\texcept Exception as write_error:\n\t\t\tlogger.error(f'Failed to write fresh config: {write_error}')\n\t\treturn new_config\n\n\nclass Config:\n\t\"\"\"Backward-compatible configuration class that merges all config sources.\n\n\tRe-reads environment variables on every access to maintain compatibility.\n\t\"\"\"\n\n\tdef __init__(self):\n\t\t# Cache for directory creation tracking only\n\t\tself._dirs_created = False\n\n\tdef __getattr__(self, name: str) -> Any:\n\t\t\"\"\"Dynamically proxy all attributes to fresh instances.\n\n\t\tThis ensures env vars are re-read on every access.\n\t\t\"\"\"\n\t\t# Special handling for internal attributes\n\t\tif name.startswith('_'):\n\t\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\t\t# Create fresh instances on every access\n\t\told_config = OldConfig()\n\n\t\t# Always use old config for all attributes (it handles env vars with proper transformations)\n\t\tif hasattr(old_config, name):\n\t\t\treturn getattr(old_config, name)\n\n\t\t# For new MCP-specific attributes not in old config\n\t\tenv_config = FlatEnvConfig()\n\t\tif hasattr(env_config, name):\n\t\t\treturn getattr(env_config, name)\n\n\t\t# Handle special methods\n\t\tif name == 'get_default_profile':\n\t\t\treturn lambda: self._get_default_profile()\n\t\telif name == 'get_default_llm':\n\t\t\treturn lambda: self._get_default_llm()\n\t\telif name == 'get_default_agent':\n\t\t\treturn lambda: self._get_default_agent()\n\t\telif name == 'load_config':\n\t\t\treturn lambda: self._load_config()\n\t\telif name == '_ensure_dirs':\n\t\t\treturn lambda: old_config._ensure_dirs()\n\n\t\traise AttributeError(f\"'{self.__class__.__name__}' object has no attribute '{name}'\")\n\n\tdef _get_config_path(self) -> Path:\n\t\t\"\"\"Get config path from fresh env config.\"\"\"\n\t\tenv_config = FlatEnvConfig()\n\t\tif env_config.BROWSER_USE_CONFIG_PATH:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_PATH).expanduser()\n\t\telif env_config.BROWSER_USE_CONFIG_DIR:\n\t\t\treturn Path(env_config.BROWSER_USE_CONFIG_DIR).expanduser() / 'config.json'\n\t\telse:\n\t\t\txdg_config = Path(env_config.XDG_CONFIG_HOME).expanduser()\n\t\t\treturn xdg_config / 'browseruse' / 'config.json'\n\n\tdef _get_db_config(self) -> DBStyleConfigJSON:\n\t\t\"\"\"Load and migrate config.json.\"\"\"\n\t\tconfig_path = self._get_config_path()\n\t\treturn load_and_migrate_config(config_path)\n\n\tdef _get_default_profile(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default browser profile configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor profile in db_config.browser_profile.values():\n\t\t\tif profile.default:\n\t\t\t\treturn profile.model_dump(exclude_none=True)\n\n\t\t# Return first profile if no default\n\t\tif db_config.browser_profile:\n\t\t\treturn next(iter(db_config.browser_profile.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_llm(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default LLM configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor llm in db_config.llm.values():\n\t\t\tif llm.default:\n\t\t\t\treturn llm.model_dump(exclude_none=True)\n\n\t\t# Return first LLM if no default\n\t\tif db_config.llm:\n\t\t\treturn next(iter(db_config.llm.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _get_default_agent(self) -> dict[str, Any]:\n\t\t\"\"\"Get the default agent configuration.\"\"\"\n\t\tdb_config = self._get_db_config()\n\t\tfor agent in db_config.agent.values():\n\t\t\tif agent.default:\n\t\t\t\treturn agent.model_dump(exclude_none=True)\n\n\t\t# Return first agent if no default\n\t\tif db_config.agent:\n\t\t\treturn next(iter(db_config.agent.values())).model_dump(exclude_none=True)\n\n\t\treturn {}\n\n\tdef _load_config(self) -> dict[str, Any]:\n\t\t\"\"\"Load configuration with env var overrides for MCP components.\"\"\"\n\t\tconfig = {\n\t\t\t'browser_profile': self._get_default_profile(),\n\t\t\t'llm': self._get_default_llm(),\n\t\t\t'agent': self._get_default_agent(),\n\t\t}\n\n\t\t# Fresh env config for overrides\n\t\tenv_config = FlatEnvConfig()\n\n\t\t# Apply MCP-specific env var overrides\n\t\tif env_config.BROWSER_USE_HEADLESS is not None:\n\t\t\tconfig['browser_profile']['headless'] = env_config.BROWSER_USE_HEADLESS\n\n\t\tif env_config.BROWSER_USE_ALLOWED_DOMAINS:\n\t\t\tdomains = [d.strip() for d in env_config.BROWSER_USE_ALLOWED_DOMAINS.split(',') if d.strip()]\n\t\t\tconfig['browser_profile']['allowed_domains'] = domains\n\n\t\tif env_config.OPENAI_API_KEY:\n\t\t\tconfig['llm']['api_key'] = env_config.OPENAI_API_KEY\n\n\t\tif env_config.BROWSER_USE_LLM_MODEL:\n\t\t\tconfig['llm']['model'] = env_config.BROWSER_USE_LLM_MODEL\n\n\t\treturn config\n\n\n# Create singleton instance\nCONFIG = Config()\n\n\n# Helper functions for MCP components\ndef load_browser_use_config() -> dict[str, Any]:\n\t\"\"\"Load browser-use configuration for MCP components.\"\"\"\n\treturn CONFIG.load_config()\n\n\ndef get_default_profile(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default browser profile from config dict.\"\"\"\n\treturn config.get('browser_profile', {})\n\n\ndef get_default_llm(config: dict[str, Any]) -> dict[str, Any]:\n\t\"\"\"Get default LLM config from config dict.\"\"\"\n\treturn config.get('llm', {})\n"}, "42579c05a48ef91f6c52cf63de85f99dee60e8ed": {"t": 1785753153.6827781, "n": 4679, "h": "971b178a037803d9", "c": "\"\"\"Lazy-loading configuration system for browser-use environment variables.\"\"\"\n\nimport os\nfrom functools import cache\nfrom pathlib import Path\n\nimport psutil\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass Config:\n\t\"\"\"Lazy-loading configuration class for environment variables (env vars can change at runtime so we need to get them fresh on every access)\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_API_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_API_URL', 'https://api.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_API_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\n# Create a singleton instance\nCONFIG = Config()\n"}, "875c8fc831d96a79c7cc75303d7cb811c6476507": {"t": 1785753156.0443218, "n": 4669, "h": "65dbd411cc0a6be3", "c": "\"\"\"Lazy-loading configuration system for browser-use environment variables.\"\"\"\n\nimport os\nfrom functools import cache\nfrom pathlib import Path\n\nimport psutil\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass Config:\n\t\"\"\"Lazy-loading configuration class for environment variables (env vars can change at runtime so we need to get them fresh on every access)\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_URL', 'https://cloud.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\t# Allow empty string as default, only validate if set\n\t\tif url and '://' not in url:\n\t\t\traise AssertionError('BROWSER_USE_CLOUD_UI_URL must be a valid URL if set')\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\n# Create a singleton instance\nCONFIG = Config()\n"}, "3cf9f3410c8b4f6eee0d2d5c3896550e05f04b4c": {"t": 1785753157.4120243, "n": 4573, "h": "9c7924cf23e93325", "c": "\"\"\"Lazy-loading configuration system for browser-use environment variables.\"\"\"\n\nimport os\nfrom functools import cache\nfrom pathlib import Path\n\nimport psutil\n\n\n@cache\ndef is_running_in_docker() -> bool:\n\t\"\"\"Detect if we are running in a docker container, for the purpose of optimizing chrome launch flags (dev shm usage, gpu settings, etc.)\"\"\"\n\ttry:\n\t\tif Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if init proc (PID 1) looks like uvicorn/python/uv/etc. then we're in Docker\n\t\t# if init proc (PID 1) looks like bash/systemd/init/etc. then we're probably NOT in Docker\n\t\tinit_cmd = ' '.join(psutil.Process(1).cmdline())\n\t\tif ('py' in init_cmd) or ('uv' in init_cmd) or ('app' in init_cmd):\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\ttry:\n\t\t# if less than 10 total running procs, then we're almost certainly in a container\n\t\tif len(psutil.pids()) < 10:\n\t\t\treturn True\n\texcept Exception:\n\t\tpass\n\n\treturn False\n\n\nclass Config:\n\t\"\"\"Lazy-loading configuration class for environment variables (env vars can change at runtime so we need to get them fresh on every access)\"\"\"\n\n\t# Cache for directory creation tracking\n\t_dirs_created = False\n\n\t@property\n\tdef BROWSER_USE_LOGGING_LEVEL(self) -> str:\n\t\treturn os.getenv('BROWSER_USE_LOGGING_LEVEL', 'info').lower()\n\n\t@property\n\tdef ANONYMIZED_TELEMETRY(self) -> bool:\n\t\treturn os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_SYNC(self) -> bool:\n\t\treturn os.getenv('BROWSER_USE_CLOUD_SYNC', str(self.ANONYMIZED_TELEMETRY)).lower()[:1] in 'ty1'\n\n\t@property\n\tdef BROWSER_USE_CLOUD_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_URL', 'https://cloud.browser-use.com')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_URL must be a valid URL'\n\t\treturn url\n\n\t@property\n\tdef BROWSER_USE_CLOUD_UI_URL(self) -> str:\n\t\turl = os.getenv('BROWSER_USE_CLOUD_UI_URL', '')\n\t\tassert '://' in url, 'BROWSER_USE_CLOUD_UI_URL must be a valid URL'\n\t\treturn url\n\n\t# Path configuration\n\t@property\n\tdef XDG_CACHE_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser().resolve()\n\n\t@property\n\tdef XDG_CONFIG_HOME(self) -> Path:\n\t\treturn Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser().resolve()\n\n\t@property\n\tdef BROWSER_USE_CONFIG_DIR(self) -> Path:\n\t\tpath = Path(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_CONFIG_FILE(self) -> Path:\n\t\treturn self.BROWSER_USE_CONFIG_DIR / 'config.json'\n\n\t@property\n\tdef BROWSER_USE_PROFILES_DIR(self) -> Path:\n\t\tpath = self.BROWSER_USE_CONFIG_DIR / 'profiles'\n\t\tself._ensure_dirs()\n\t\treturn path\n\n\t@property\n\tdef BROWSER_USE_DEFAULT_USER_DATA_DIR(self) -> Path:\n\t\treturn self.BROWSER_USE_PROFILES_DIR / 'default'\n\n\tdef _ensure_dirs(self) -> None:\n\t\t\"\"\"Create directories if they don't exist (only once)\"\"\"\n\t\tif not self._dirs_created:\n\t\t\tconfig_dir = (\n\t\t\t\tPath(os.getenv('BROWSER_USE_CONFIG_DIR', str(self.XDG_CONFIG_HOME / 'browseruse'))).expanduser().resolve()\n\t\t\t)\n\t\t\tconfig_dir.mkdir(parents=True, exist_ok=True)\n\t\t\t(config_dir / 'profiles').mkdir(parents=True, exist_ok=True)\n\t\t\tself._dirs_created = True\n\n\t# LLM API key configuration\n\t@property\n\tdef OPENAI_API_KEY(self) -> str:\n\t\treturn os.getenv('OPENAI_API_KEY', '')\n\n\t@property\n\tdef ANTHROPIC_API_KEY(self) -> str:\n\t\treturn os.getenv('ANTHROPIC_API_KEY', '')\n\n\t@property\n\tdef GOOGLE_API_KEY(self) -> str:\n\t\treturn os.getenv('GOOGLE_API_KEY', '')\n\n\t@property\n\tdef DEEPSEEK_API_KEY(self) -> str:\n\t\treturn os.getenv('DEEPSEEK_API_KEY', '')\n\n\t@property\n\tdef GROK_API_KEY(self) -> str:\n\t\treturn os.getenv('GROK_API_KEY', '')\n\n\t@property\n\tdef NOVITA_API_KEY(self) -> str:\n\t\treturn os.getenv('NOVITA_API_KEY', '')\n\n\t@property\n\tdef AZURE_OPENAI_ENDPOINT(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_ENDPOINT', '')\n\n\t@property\n\tdef AZURE_OPENAI_KEY(self) -> str:\n\t\treturn os.getenv('AZURE_OPENAI_KEY', '')\n\n\t@property\n\tdef SKIP_LLM_API_KEY_VERIFICATION(self) -> bool:\n\t\treturn os.getenv('SKIP_LLM_API_KEY_VERIFICATION', 'false').lower()[:1] in 'ty1'\n\n\t# Runtime hints\n\t@property\n\tdef IN_DOCKER(self) -> bool:\n\t\treturn os.getenv('IN_DOCKER', 'false').lower()[:1] in 'ty1' or is_running_in_docker()\n\n\t@property\n\tdef IS_IN_EVALS(self) -> bool:\n\t\treturn os.getenv('IS_IN_EVALS', 'false').lower()[:1] in 'ty1'\n\n\t@property\n\tdef WIN_FONT_DIR(self) -> str:\n\t\treturn os.getenv('WIN_FONT_DIR', 'C:\\\\Windows\\\\Fonts')\n\n\n# Create a singleton instance\nCONFIG = Config()\n"}}, "browser_use/llm/deepseek/chat.py": {"122feb91f41f66cdc17a54e3c6b6e807f317eb91": {"t": 1785745982.0359807, "n": 6774, "h": "5fbe3de955e4e230", "c": "from __future__ import annotations\n\nimport json\nfrom dataclasses import dataclass\nfrom typing import Any, TypeVar, overload\n\nimport httpx\nfrom openai import (\n\tAPIConnectionError,\n\tAPIError,\n\tAPIStatusError,\n\tAPITimeoutError,\n\tAsyncOpenAI,\n\tRateLimitError,\n)\nfrom pydantic import BaseModel\n\nfrom browser_use.llm.base import BaseChatModel\nfrom browser_use.llm.deepseek.serializer import DeepSeekMessageSerializer\nfrom browser_use.llm.exceptions import ModelProviderError, ModelRateLimitError\nfrom browser_use.llm.messages import BaseMessage\nfrom browser_use.llm.schema import SchemaOptimizer\nfrom browser_use.llm.views import ChatInvokeCompletion\n\nT = TypeVar('T', bound=BaseModel)\n\n\n@dataclass\nclass ChatDeepSeek(BaseChatModel):\n\t\"\"\"DeepSeek /chat/completions wrapper (OpenAI-compatible).\"\"\"\n\n\tmodel: str = 'deepseek-chat'\n\n\t# Generation parameters\n\tmax_tokens: int | None = None\n\ttemperature: float | None = None\n\ttop_p: float | None = None\n\tseed: int | None = None\n\n\t# Connection parameters\n\tapi_key: str | None = None\n\tbase_url: str | httpx.URL | None = 'https://api.deepseek.com/v1'\n\ttimeout: float | httpx.Timeout | None = None\n\tclient_params: dict[str, Any] | None = None\n\n\t@property\n\tdef provider(self) -> str:\n\t\treturn 'deepseek'\n\n\tdef _client(self) -> AsyncOpenAI:\n\t\treturn AsyncOpenAI(\n\t\t\tapi_key=self.api_key,\n\t\t\tbase_url=self.base_url,\n\t\t\ttimeout=self.timeout,\n\t\t\t**(self.client_params or {}),\n\t\t)\n\n\t@property\n\tdef name(self) -> str:\n\t\treturn self.model\n\n\t@overload\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: None = None,\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[str]: ...\n\n\t@overload\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: type[T],\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[T]: ...\n\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: type[T] | None = None,\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:\n\t\t\"\"\"\n\t\tDeepSeek ainvoke supports:\n\t\t1. Regular text/multi-turn conversation\n\t\t2. Function Calling\n\t\t3. JSON Output (response_format)\n\t\t4. Conversation prefix continuation (beta, prefix, stop)\n\t\t\"\"\"\n\t\tclient = self._client()\n\t\tds_messages = DeepSeekMessageSerializer.serialize_messages(messages)\n\t\tcommon: dict[str, Any] = {}\n\n\t\tif self.temperature is not None:\n\t\t\tcommon['temperature'] = self.temperature\n\t\tif self.max_tokens is not None:\n\t\t\tcommon['max_tokens'] = self.max_tokens\n\t\tif self.top_p is not None:\n\t\t\tcommon['top_p'] = self.top_p\n\t\tif self.seed is not None:\n\t\t\tcommon['seed'] = self.seed\n\n\t\t# Beta conversation prefix continuation (see official documentation)\n\t\tif self.base_url and str(self.base_url).endswith('/beta'):\n\t\t\t# The last assistant message must have prefix\n\t\t\tif ds_messages and isinstance(ds_messages[-1], dict) and ds_messages[-1].get('role') == 'assistant':\n\t\t\t\tds_messages[-1]['prefix'] = True\n\t\t\tif stop:\n\t\t\t\tcommon['stop'] = stop\n\n\t\t# ① Regular multi-turn conversation/text output\n\t\tif output_format is None and not tools:\n\t\t\ttry:\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\tcompletion=resp.choices[0].message.content or '',\n\t\t\t\t\tusage=None,\n\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\t# ② Function Calling path (with tools or output_format)\n\t\tif tools or (output_format is not None and hasattr(output_format, 'model_json_schema')):\n\t\t\ttry:\n\t\t\t\tcall_tools = tools\n\t\t\t\ttool_choice = None\n\t\t\t\tif output_format is not None and hasattr(output_format, 'model_json_schema'):\n\t\t\t\t\ttool_name = output_format.__name__\n\t\t\t\t\tschema = SchemaOptimizer.create_optimized_json_schema(output_format)\n\t\t\t\t\tschema.pop('title', None)\n\t\t\t\t\tcall_tools = [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t'type': 'function',\n\t\t\t\t\t\t\t'function': {\n\t\t\t\t\t\t\t\t'name': tool_name,\n\t\t\t\t\t\t\t\t'description': f'Return a JSON object of type {tool_name}',\n\t\t\t\t\t\t\t\t'parameters': schema,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t\ttool_choice = {'type': 'function', 'function': {'name': tool_name}}\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\ttools=call_tools, # type: ignore\n\t\t\t\t\ttool_choice=tool_choice, # type: ignore\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\tmsg = resp.choices[0].message\n\t\t\t\tif not msg.tool_calls:\n\t\t\t\t\traise ValueError('Expected tool_calls in response but got none')\n\t\t\t\traw_args = msg.tool_calls[0].function.arguments\n\t\t\t\tif isinstance(raw_args, str):\n\t\t\t\t\tparsed = json.loads(raw_args)\n\t\t\t\telse:\n\t\t\t\t\tparsed = raw_args\n\t\t\t\t# --------- Fix: only use model_validate when output_format is not None ----------\n\t\t\t\tif output_format is not None:\n\t\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\t\tcompletion=output_format.model_validate(parsed),\n\t\t\t\t\t\tusage=None,\n\t\t\t\t\t)\n\t\t\t\telse:\n\t\t\t\t\t# If no output_format, return dict directly\n\t\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\t\tcompletion=parsed,\n\t\t\t\t\t\tusage=None,\n\t\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\t# ③ JSON Output path (official response_format)\n\t\tif output_format is not None and hasattr(output_format, 'model_json_schema'):\n\t\t\ttry:\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\tresponse_format={'type': 'json_object'},\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\tcontent = resp.choices[0].message.content\n\t\t\t\tif not content:\n\t\t\t\t\traise ModelProviderError('Empty JSON content in DeepSeek response', model=self.name)\n\t\t\t\tparsed = output_format.model_validate_json(content)\n\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\tcompletion=parsed,\n\t\t\t\t\tusage=None,\n\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\traise ModelProviderError('No valid ainvoke execution path for DeepSeek LLM', model=self.name)\n"}, "a5eae475a12f39020a6142a33142189da18b1d5e": {"t": 1785753138.4310415, "n": 6774, "h": "5fbe3de955e4e230", "c": "from __future__ import annotations\n\nimport json\nfrom dataclasses import dataclass\nfrom typing import Any, TypeVar, overload\n\nimport httpx\nfrom openai import (\n\tAPIConnectionError,\n\tAPIError,\n\tAPIStatusError,\n\tAPITimeoutError,\n\tAsyncOpenAI,\n\tRateLimitError,\n)\nfrom pydantic import BaseModel\n\nfrom browser_use.llm.base import BaseChatModel\nfrom browser_use.llm.deepseek.serializer import DeepSeekMessageSerializer\nfrom browser_use.llm.exceptions import ModelProviderError, ModelRateLimitError\nfrom browser_use.llm.messages import BaseMessage\nfrom browser_use.llm.schema import SchemaOptimizer\nfrom browser_use.llm.views import ChatInvokeCompletion\n\nT = TypeVar('T', bound=BaseModel)\n\n\n@dataclass\nclass ChatDeepSeek(BaseChatModel):\n\t\"\"\"DeepSeek /chat/completions wrapper (OpenAI-compatible).\"\"\"\n\n\tmodel: str = 'deepseek-chat'\n\n\t# Generation parameters\n\tmax_tokens: int | None = None\n\ttemperature: float | None = None\n\ttop_p: float | None = None\n\tseed: int | None = None\n\n\t# Connection parameters\n\tapi_key: str | None = None\n\tbase_url: str | httpx.URL | None = 'https://api.deepseek.com/v1'\n\ttimeout: float | httpx.Timeout | None = None\n\tclient_params: dict[str, Any] | None = None\n\n\t@property\n\tdef provider(self) -> str:\n\t\treturn 'deepseek'\n\n\tdef _client(self) -> AsyncOpenAI:\n\t\treturn AsyncOpenAI(\n\t\t\tapi_key=self.api_key,\n\t\t\tbase_url=self.base_url,\n\t\t\ttimeout=self.timeout,\n\t\t\t**(self.client_params or {}),\n\t\t)\n\n\t@property\n\tdef name(self) -> str:\n\t\treturn self.model\n\n\t@overload\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: None = None,\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[str]: ...\n\n\t@overload\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: type[T],\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[T]: ...\n\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: type[T] | None = None,\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:\n\t\t\"\"\"\n\t\tDeepSeek ainvoke supports:\n\t\t1. Regular text/multi-turn conversation\n\t\t2. Function Calling\n\t\t3. JSON Output (response_format)\n\t\t4. Conversation prefix continuation (beta, prefix, stop)\n\t\t\"\"\"\n\t\tclient = self._client()\n\t\tds_messages = DeepSeekMessageSerializer.serialize_messages(messages)\n\t\tcommon: dict[str, Any] = {}\n\n\t\tif self.temperature is not None:\n\t\t\tcommon['temperature'] = self.temperature\n\t\tif self.max_tokens is not None:\n\t\t\tcommon['max_tokens'] = self.max_tokens\n\t\tif self.top_p is not None:\n\t\t\tcommon['top_p'] = self.top_p\n\t\tif self.seed is not None:\n\t\t\tcommon['seed'] = self.seed\n\n\t\t# Beta conversation prefix continuation (see official documentation)\n\t\tif self.base_url and str(self.base_url).endswith('/beta'):\n\t\t\t# The last assistant message must have prefix\n\t\t\tif ds_messages and isinstance(ds_messages[-1], dict) and ds_messages[-1].get('role') == 'assistant':\n\t\t\t\tds_messages[-1]['prefix'] = True\n\t\t\tif stop:\n\t\t\t\tcommon['stop'] = stop\n\n\t\t# ① Regular multi-turn conversation/text output\n\t\tif output_format is None and not tools:\n\t\t\ttry:\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\tcompletion=resp.choices[0].message.content or '',\n\t\t\t\t\tusage=None,\n\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\t# ② Function Calling path (with tools or output_format)\n\t\tif tools or (output_format is not None and hasattr(output_format, 'model_json_schema')):\n\t\t\ttry:\n\t\t\t\tcall_tools = tools\n\t\t\t\ttool_choice = None\n\t\t\t\tif output_format is not None and hasattr(output_format, 'model_json_schema'):\n\t\t\t\t\ttool_name = output_format.__name__\n\t\t\t\t\tschema = SchemaOptimizer.create_optimized_json_schema(output_format)\n\t\t\t\t\tschema.pop('title', None)\n\t\t\t\t\tcall_tools = [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t'type': 'function',\n\t\t\t\t\t\t\t'function': {\n\t\t\t\t\t\t\t\t'name': tool_name,\n\t\t\t\t\t\t\t\t'description': f'Return a JSON object of type {tool_name}',\n\t\t\t\t\t\t\t\t'parameters': schema,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t\ttool_choice = {'type': 'function', 'function': {'name': tool_name}}\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\ttools=call_tools, # type: ignore\n\t\t\t\t\ttool_choice=tool_choice, # type: ignore\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\tmsg = resp.choices[0].message\n\t\t\t\tif not msg.tool_calls:\n\t\t\t\t\traise ValueError('Expected tool_calls in response but got none')\n\t\t\t\traw_args = msg.tool_calls[0].function.arguments\n\t\t\t\tif isinstance(raw_args, str):\n\t\t\t\t\tparsed = json.loads(raw_args)\n\t\t\t\telse:\n\t\t\t\t\tparsed = raw_args\n\t\t\t\t# --------- Fix: only use model_validate when output_format is not None ----------\n\t\t\t\tif output_format is not None:\n\t\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\t\tcompletion=output_format.model_validate(parsed),\n\t\t\t\t\t\tusage=None,\n\t\t\t\t\t)\n\t\t\t\telse:\n\t\t\t\t\t# If no output_format, return dict directly\n\t\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\t\tcompletion=parsed,\n\t\t\t\t\t\tusage=None,\n\t\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\t# ③ JSON Output path (official response_format)\n\t\tif output_format is not None and hasattr(output_format, 'model_json_schema'):\n\t\t\ttry:\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\tresponse_format={'type': 'json_object'},\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\tcontent = resp.choices[0].message.content\n\t\t\t\tif not content:\n\t\t\t\t\traise ModelProviderError('Empty JSON content in DeepSeek response', model=self.name)\n\t\t\t\tparsed = output_format.model_validate_json(content)\n\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\tcompletion=parsed,\n\t\t\t\t\tusage=None,\n\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\traise ModelProviderError('No valid ainvoke execution path for DeepSeek LLM', model=self.name)\n"}, "63cb5613a5a316f3c7413c5085fee40486001615": {"t": 1785753139.67177, "n": 6545, "h": "66e3a4cbb1ed4eb3", "c": "from __future__ import annotations\n\nimport json\nfrom dataclasses import dataclass\nfrom typing import Any, TypeVar, overload\n\nimport httpx\nfrom openai import (\n\tAPIConnectionError,\n\tAPIError,\n\tAPIStatusError,\n\tAPITimeoutError,\n\tAsyncOpenAI,\n\tRateLimitError,\n)\nfrom pydantic import BaseModel\n\nfrom browser_use.llm.base import BaseChatModel\nfrom browser_use.llm.deepseek.serializer import DeepSeekMessageSerializer\nfrom browser_use.llm.exceptions import ModelProviderError, ModelRateLimitError\nfrom browser_use.llm.messages import BaseMessage\nfrom browser_use.llm.schema import SchemaOptimizer\nfrom browser_use.llm.views import ChatInvokeCompletion\n\nT = TypeVar('T', bound=BaseModel)\n\n\n@dataclass\nclass ChatDeepSeek(BaseChatModel):\n\t\"\"\"DeepSeek /chat/completions 封装(OpenAI-compatible)。\"\"\"\n\n\tmodel: str = 'deepseek-chat'\n\n\t# 生成参数\n\tmax_tokens: int | None = None\n\ttemperature: float | None = None\n\ttop_p: float | None = None\n\tseed: int | None = None\n\n\t# 连接参数\n\tapi_key: str | None = None\n\tbase_url: str | httpx.URL | None = 'https://api.deepseek.com/v1'\n\ttimeout: float | httpx.Timeout | None = None\n\tclient_params: dict[str, Any] | None = None\n\n\t@property\n\tdef provider(self) -> str:\n\t\treturn 'deepseek'\n\n\tdef _client(self) -> AsyncOpenAI:\n\t\treturn AsyncOpenAI(\n\t\t\tapi_key=self.api_key,\n\t\t\tbase_url=self.base_url,\n\t\t\ttimeout=self.timeout,\n\t\t\t**(self.client_params or {}),\n\t\t)\n\n\t@property\n\tdef name(self) -> str:\n\t\treturn self.model\n\n\t@overload\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: None = None,\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[str]: ...\n\n\t@overload\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: type[T],\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[T]: ...\n\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: type[T] | None = None,\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:\n\t\t\"\"\"\n\t\tDeepSeek ainvoke 支持:\n\t\t1. 普通文本/多轮对话\n\t\t2. Function Calling\n\t\t3. JSON Output (response_format)\n\t\t4. 对话前缀续写 (beta, prefix, stop)\n\t\t\"\"\"\n\t\tclient = self._client()\n\t\tds_messages = DeepSeekMessageSerializer.serialize_messages(messages)\n\t\tcommon: dict[str, Any] = {}\n\n\t\tif self.temperature is not None:\n\t\t\tcommon['temperature'] = self.temperature\n\t\tif self.max_tokens is not None:\n\t\t\tcommon['max_tokens'] = self.max_tokens\n\t\tif self.top_p is not None:\n\t\t\tcommon['top_p'] = self.top_p\n\t\tif self.seed is not None:\n\t\t\tcommon['seed'] = self.seed\n\n\t\t# Beta 对话前缀续写(见官方文档)\n\t\tif self.base_url and str(self.base_url).endswith('/beta'):\n\t\t\t# 最后一个 assistant 必须 prefix\n\t\t\tif ds_messages and isinstance(ds_messages[-1], dict) and ds_messages[-1].get('role') == 'assistant':\n\t\t\t\tds_messages[-1]['prefix'] = True\n\t\t\tif stop:\n\t\t\t\tcommon['stop'] = stop\n\n\t\t# ① 普通多轮对话/文本输出\n\t\tif output_format is None and not tools:\n\t\t\ttry:\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\tcompletion=resp.choices[0].message.content or '',\n\t\t\t\t\tusage=None,\n\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\t# ② Function Calling 路径(有 tools 或 output_format)\n\t\tif tools or (output_format is not None and hasattr(output_format, 'model_json_schema')):\n\t\t\ttry:\n\t\t\t\tcall_tools = tools\n\t\t\t\ttool_choice = None\n\t\t\t\tif output_format is not None and hasattr(output_format, 'model_json_schema'):\n\t\t\t\t\ttool_name = output_format.__name__\n\t\t\t\t\tschema = SchemaOptimizer.create_optimized_json_schema(output_format)\n\t\t\t\t\tschema.pop('title', None)\n\t\t\t\t\tcall_tools = [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t'type': 'function',\n\t\t\t\t\t\t\t'function': {\n\t\t\t\t\t\t\t\t'name': tool_name,\n\t\t\t\t\t\t\t\t'description': f'Return a JSON object of type {tool_name}',\n\t\t\t\t\t\t\t\t'parameters': schema,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t\ttool_choice = {'type': 'function', 'function': {'name': tool_name}}\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\ttools=call_tools, # type: ignore\n\t\t\t\t\ttool_choice=tool_choice, # type: ignore\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\tmsg = resp.choices[0].message\n\t\t\t\tif not msg.tool_calls:\n\t\t\t\t\traise ValueError('Expected tool_calls in response but got none')\n\t\t\t\traw_args = msg.tool_calls[0].function.arguments\n\t\t\t\tif isinstance(raw_args, str):\n\t\t\t\t\tparsed = json.loads(raw_args)\n\t\t\t\telse:\n\t\t\t\t\tparsed = raw_args\n\t\t\t\t# --------- 修复点: 只有 output_format 不为 None 才能用 model_validate ----------\n\t\t\t\tif output_format is not None:\n\t\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\t\tcompletion=output_format.model_validate(parsed),\n\t\t\t\t\t\tusage=None,\n\t\t\t\t\t)\n\t\t\t\telse:\n\t\t\t\t\t# 若无 output_format,直接返回 dict\n\t\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\t\tcompletion=parsed,\n\t\t\t\t\t\tusage=None,\n\t\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\t# ③ JSON Output 路径(官方 response_format)\n\t\tif output_format is not None and hasattr(output_format, 'model_json_schema'):\n\t\t\ttry:\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\tresponse_format={'type': 'json_object'},\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\tcontent = resp.choices[0].message.content\n\t\t\t\tif not content:\n\t\t\t\t\traise ModelProviderError('Empty JSON content in DeepSeek response', model=self.name)\n\t\t\t\tparsed = output_format.model_validate_json(content)\n\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\tcompletion=parsed,\n\t\t\t\t\tusage=None,\n\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\t# 所有路径兜底\n\t\traise ModelProviderError('No valid ainvoke execution path for DeepSeek LLM', model=self.name)\n"}, "39bd56250f75b099201fa61f10525f92bf50d13a": {"t": 1785753140.8057594, "n": 6372, "h": "990fca93208a0dc4", "c": "from __future__ import annotations\n\nimport json\nfrom dataclasses import dataclass\nfrom typing import Any, TypeVar, overload\n\nimport httpx\nfrom openai import (\n\tAPIConnectionError,\n\tAPIError,\n\tAPIStatusError,\n\tAPITimeoutError,\n\tAsyncOpenAI,\n\tRateLimitError,\n)\nfrom pydantic import BaseModel\n\nfrom browser_use.llm.base import BaseChatModel\nfrom browser_use.llm.deepseek.serializer import DeepSeekMessageSerializer\nfrom browser_use.llm.exceptions import ModelProviderError, ModelRateLimitError\nfrom browser_use.llm.messages import BaseMessage\nfrom browser_use.llm.schema import SchemaOptimizer\nfrom browser_use.llm.views import ChatInvokeCompletion\n\nT = TypeVar('T', bound=BaseModel)\n\n\n@dataclass\nclass ChatDeepSeek(BaseChatModel):\n\t\"\"\"DeepSeek /chat/completions 封装(OpenAI-compatible)。\"\"\"\n\n\tmodel: str = 'deepseek-chat'\n\n\t# 生成参数\n\tmax_tokens: int | None = None\n\ttemperature: float | None = None\n\n\t# 连接参数\n\tapi_key: str | None = None\n\tbase_url: str | httpx.URL | None = 'https://api.deepseek.com/v1'\n\ttimeout: float | httpx.Timeout | None = None\n\tclient_params: dict[str, Any] | None = None\n\n\t@property\n\tdef provider(self) -> str:\n\t\treturn 'deepseek'\n\n\tdef _client(self) -> AsyncOpenAI:\n\t\treturn AsyncOpenAI(\n\t\t\tapi_key=self.api_key,\n\t\t\tbase_url=self.base_url,\n\t\t\ttimeout=self.timeout,\n\t\t\t**(self.client_params or {}),\n\t\t)\n\n\t@property\n\tdef name(self) -> str:\n\t\treturn self.model\n\n\t@overload\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: None = None,\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[str]: ...\n\n\t@overload\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: type[T],\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[T]: ...\n\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: type[T] | None = None,\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:\n\t\t\"\"\"\n\t\tDeepSeek ainvoke 支持:\n\t\t1. 普通文本/多轮对话\n\t\t2. Function Calling\n\t\t3. JSON Output (response_format)\n\t\t4. 对话前缀续写 (beta, prefix, stop)\n\t\t\"\"\"\n\t\tclient = self._client()\n\t\tds_messages = DeepSeekMessageSerializer.serialize_messages(messages)\n\t\tcommon: dict[str, Any] = {}\n\t\tif self.temperature is not None:\n\t\t\tcommon['temperature'] = self.temperature\n\t\tif self.max_tokens is not None:\n\t\t\tcommon['max_tokens'] = self.max_tokens\n\n\t\t# Beta 对话前缀续写(见官方文档)\n\t\tif self.base_url and str(self.base_url).endswith('/beta'):\n\t\t\t# 最后一个 assistant 必须 prefix\n\t\t\tif ds_messages and isinstance(ds_messages[-1], dict) and ds_messages[-1].get('role') == 'assistant':\n\t\t\t\tds_messages[-1]['prefix'] = True\n\t\t\tif stop:\n\t\t\t\tcommon['stop'] = stop\n\n\t\t# ① 普通多轮对话/文本输出\n\t\tif output_format is None and not tools:\n\t\t\ttry:\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\tcompletion=resp.choices[0].message.content or '',\n\t\t\t\t\tusage=None,\n\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\t# ② Function Calling 路径(有 tools 或 output_format)\n\t\tif tools or (output_format is not None and hasattr(output_format, 'model_json_schema')):\n\t\t\ttry:\n\t\t\t\tcall_tools = tools\n\t\t\t\ttool_choice = None\n\t\t\t\tif output_format is not None and hasattr(output_format, 'model_json_schema'):\n\t\t\t\t\ttool_name = output_format.__name__\n\t\t\t\t\tschema = SchemaOptimizer.create_optimized_json_schema(output_format)\n\t\t\t\t\tschema.pop('title', None)\n\t\t\t\t\tcall_tools = [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t'type': 'function',\n\t\t\t\t\t\t\t'function': {\n\t\t\t\t\t\t\t\t'name': tool_name,\n\t\t\t\t\t\t\t\t'description': f'Return a JSON object of type {tool_name}',\n\t\t\t\t\t\t\t\t'parameters': schema,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t\ttool_choice = {'type': 'function', 'function': {'name': tool_name}}\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\ttools=call_tools, # type: ignore\n\t\t\t\t\ttool_choice=tool_choice, # type: ignore\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\tmsg = resp.choices[0].message\n\t\t\t\tif not msg.tool_calls:\n\t\t\t\t\traise ValueError('Expected tool_calls in response but got none')\n\t\t\t\traw_args = msg.tool_calls[0].function.arguments\n\t\t\t\tif isinstance(raw_args, str):\n\t\t\t\t\tparsed = json.loads(raw_args)\n\t\t\t\telse:\n\t\t\t\t\tparsed = raw_args\n\t\t\t\t# --------- 修复点: 只有 output_format 不为 None 才能用 model_validate ----------\n\t\t\t\tif output_format is not None:\n\t\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\t\tcompletion=output_format.model_validate(parsed),\n\t\t\t\t\t\tusage=None,\n\t\t\t\t\t)\n\t\t\t\telse:\n\t\t\t\t\t# 若无 output_format,直接返回 dict\n\t\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\t\tcompletion=parsed,\n\t\t\t\t\t\tusage=None,\n\t\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\t# ③ JSON Output 路径(官方 response_format)\n\t\tif output_format is not None and hasattr(output_format, 'model_json_schema'):\n\t\t\ttry:\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\tresponse_format={'type': 'json_object'},\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\tcontent = resp.choices[0].message.content\n\t\t\t\tif not content:\n\t\t\t\t\traise ModelProviderError('Empty JSON content in DeepSeek response', model=self.name)\n\t\t\t\tparsed = output_format.model_validate_json(content)\n\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\tcompletion=parsed,\n\t\t\t\t\tusage=None,\n\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\t# 所有路径兜底\n\t\traise ModelProviderError('No valid ainvoke execution path for DeepSeek LLM', model=self.name)\n"}, "0d9e6e3a2fcd6fb3773bd8212f2f1810ec139eec": {"t": 1785753142.3101544, "n": 6372, "h": "1539b7290888d1be", "c": "from __future__ import annotations\n\nimport json\nfrom dataclasses import dataclass\nfrom typing import Any, TypeVar, overload\n\nimport httpx\nfrom openai import (\n\tAsyncOpenAI,\n\tAPIError,\n\tAPIConnectionError,\n\tAPITimeoutError,\n\tAPIStatusError,\n\tRateLimitError,\n)\nfrom pydantic import BaseModel\n\nfrom browser_use.llm.base import BaseChatModel\nfrom browser_use.llm.deepseek.serializer import DeepSeekMessageSerializer\nfrom browser_use.llm.exceptions import ModelProviderError, ModelRateLimitError\nfrom browser_use.llm.schema import SchemaOptimizer\nfrom browser_use.llm.messages import BaseMessage\nfrom browser_use.llm.views import ChatInvokeCompletion\n\nT = TypeVar(\"T\", bound=BaseModel)\n\n\n@dataclass\nclass ChatDeepSeek(BaseChatModel):\n\t\"\"\"DeepSeek /chat/completions 封装(OpenAI-compatible)。\"\"\"\n\n\tmodel: str = \"deepseek-chat\"\n\n\t# 生成参数\n\tmax_tokens: int | None = None\n\ttemperature: float | None = None\n\n\t# 连接参数\n\tapi_key: str | None = None\n\tbase_url: str | httpx.URL | None = \"https://api.deepseek.com/v1\"\n\ttimeout: float | httpx.Timeout | None = None\n\tclient_params: dict[str, Any] | None = None\n\n\t@property\n\tdef provider(self) -> str:\n\t\treturn \"deepseek\"\n\n\tdef _client(self) -> AsyncOpenAI:\n\t\treturn AsyncOpenAI(\n\t\t\tapi_key=self.api_key,\n\t\t\tbase_url=self.base_url,\n\t\t\ttimeout=self.timeout,\n\t\t\t**(self.client_params or {}),\n\t\t)\n\n\t@property\n\tdef name(self) -> str:\n\t\treturn self.model\n\n\t@overload\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: None = None,\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[str]: ...\n\n\t@overload\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: type[T],\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[T]: ...\n\n\tasync def ainvoke(\n\t\tself,\n\t\tmessages: list[BaseMessage],\n\t\toutput_format: type[T] | None = None,\n\t\ttools: list[dict[str, Any]] | None = None,\n\t\tstop: list[str] | None = None,\n\t) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:\n\t\t\"\"\"\n\t\tDeepSeek ainvoke 支持:\n\t\t1. 普通文本/多轮对话\n\t\t2. Function Calling\n\t\t3. JSON Output (response_format)\n\t\t4. 对话前缀续写 (beta, prefix, stop)\n\t\t\"\"\"\n\t\tclient = self._client()\n\t\tds_messages = DeepSeekMessageSerializer.serialize_messages(messages)\n\t\tcommon: dict[str, Any] = {}\n\t\tif self.temperature is not None:\n\t\t\tcommon[\"temperature\"] = self.temperature\n\t\tif self.max_tokens is not None:\n\t\t\tcommon[\"max_tokens\"] = self.max_tokens\n\n\t\t# Beta 对话前缀续写(见官方文档)\n\t\tif self.base_url and str(self.base_url).endswith(\"/beta\"):\n\t\t\t# 最后一个 assistant 必须 prefix\n\t\t\tif ds_messages and isinstance(ds_messages[-1], dict) and ds_messages[-1].get(\"role\") == \"assistant\":\n\t\t\t\tds_messages[-1][\"prefix\"] = True\n\t\t\tif stop:\n\t\t\t\tcommon[\"stop\"] = stop\n\n\t\t# ① 普通多轮对话/文本输出\n\t\tif output_format is None and not tools:\n\t\t\ttry:\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\tcompletion=resp.choices[0].message.content or \"\",\n\t\t\t\t\tusage=None,\n\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\t# ② Function Calling 路径(有 tools 或 output_format)\n\t\tif tools or (output_format is not None and hasattr(output_format, \"model_json_schema\")):\n\t\t\ttry:\n\t\t\t\tcall_tools = tools\n\t\t\t\ttool_choice = None\n\t\t\t\tif output_format is not None and hasattr(output_format, \"model_json_schema\"):\n\t\t\t\t\ttool_name = output_format.__name__\n\t\t\t\t\tschema = SchemaOptimizer.create_optimized_json_schema(output_format)\n\t\t\t\t\tschema.pop(\"title\", None)\n\t\t\t\t\tcall_tools = [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"type\": \"function\",\n\t\t\t\t\t\t\t\"function\": {\n\t\t\t\t\t\t\t\t\"name\": tool_name,\n\t\t\t\t\t\t\t\t\"description\": f\"Return a JSON object of type {tool_name}\",\n\t\t\t\t\t\t\t\t\"parameters\": schema,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t\ttool_choice = {\"type\": \"function\", \"function\": {\"name\": tool_name}}\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\ttools=call_tools, # type: ignore\n\t\t\t\t\ttool_choice=tool_choice, # type: ignore\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\tmsg = resp.choices[0].message\n\t\t\t\tif not msg.tool_calls:\n\t\t\t\t\traise ValueError(\"Expected tool_calls in response but got none\")\n\t\t\t\traw_args = msg.tool_calls[0].function.arguments\n\t\t\t\tif isinstance(raw_args, str):\n\t\t\t\t\tparsed = json.loads(raw_args)\n\t\t\t\telse:\n\t\t\t\t\tparsed = raw_args\n\t\t\t\t# --------- 修复点: 只有 output_format 不为 None 才能用 model_validate ----------\n\t\t\t\tif output_format is not None:\n\t\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\t\tcompletion=output_format.model_validate(parsed),\n\t\t\t\t\t\tusage=None,\n\t\t\t\t\t)\n\t\t\t\telse:\n\t\t\t\t\t# 若无 output_format,直接返回 dict\n\t\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\t\tcompletion=parsed,\n\t\t\t\t\t\tusage=None,\n\t\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\t# ③ JSON Output 路径(官方 response_format)\n\t\tif output_format is not None and hasattr(output_format, \"model_json_schema\"):\n\t\t\ttry:\n\t\t\t\tresp = await client.chat.completions.create( # type: ignore\n\t\t\t\t\tmodel=self.model,\n\t\t\t\t\tmessages=ds_messages, # type: ignore\n\t\t\t\t\tresponse_format={\"type\": \"json_object\"},\n\t\t\t\t\t**common,\n\t\t\t\t)\n\t\t\t\tcontent = resp.choices[0].message.content\n\t\t\t\tif not content:\n\t\t\t\t\traise ModelProviderError(\"Empty JSON content in DeepSeek response\", model=self.name)\n\t\t\t\tparsed = output_format.model_validate_json(content)\n\t\t\t\treturn ChatInvokeCompletion(\n\t\t\t\t\tcompletion=parsed,\n\t\t\t\t\tusage=None,\n\t\t\t\t)\n\t\t\texcept RateLimitError as e:\n\t\t\t\traise ModelRateLimitError(str(e), model=self.name) from e\n\t\t\texcept (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\t\t\texcept Exception as e:\n\t\t\t\traise ModelProviderError(str(e), model=self.name) from e\n\n\t\t# 所有路径兜底\n\t\traise ModelProviderError(\"No valid ainvoke execution path for DeepSeek LLM\", model=self.name)\n"}, "d990ab15df3a7b4d27e0e7e162421092c46c9be3": {"t": 1785753143.434644, "n": 8022, "h": "9863ecc2499ab885", "c": "from __future__ import annotations\n\nimport json\nfrom dataclasses import dataclass\nfrom typing import Any, TypeVar, overload\n\nimport httpx\nfrom openai import (\n AsyncOpenAI,\n APIError,\n APIConnectionError,\n APITimeoutError,\n APIStatusError,\n RateLimitError,\n)\nfrom pydantic import BaseModel\n\nfrom browser_use.llm.base import BaseChatModel\nfrom browser_use.llm.deepseek.serializer import DeepSeekMessageSerializer\nfrom browser_use.llm.exceptions import ModelProviderError, ModelRateLimitError\nfrom browser_use.llm.schema import SchemaOptimizer\nfrom browser_use.llm.messages import BaseMessage\nfrom browser_use.llm.views import ChatInvokeCompletion\n\nT = TypeVar(\"T\", bound=BaseModel)\n\n\n@dataclass\nclass ChatDeepSeek(BaseChatModel):\n \"\"\"DeepSeek /chat/completions 封装(OpenAI-compatible)。\"\"\"\n\n model: str = \"deepseek-chat\"\n\n # 生成参数\n max_tokens: int | None = None\n temperature: float | None = None\n\n # 连接参数\n api_key: str | None = None\n base_url: str | httpx.URL | None = \"https://api.deepseek.com/v1\"\n timeout: float | httpx.Timeout | None = None\n client_params: dict[str, Any] | None = None\n\n @property\n def provider(self) -> str:\n return \"deepseek\"\n\n def _client(self) -> AsyncOpenAI:\n return AsyncOpenAI(\n api_key=self.api_key,\n base_url=self.base_url,\n timeout=self.timeout,\n **(self.client_params or {}),\n )\n\n @property\n def name(self) -> str:\n return self.model\n\n @overload\n async def ainvoke(\n self,\n messages: list[BaseMessage],\n output_format: None = None,\n tools: list[dict[str, Any]] | None = None,\n stop: list[str] | None = None,\n ) -> ChatInvokeCompletion[str]: ...\n\n @overload\n async def ainvoke(\n self,\n messages: list[BaseMessage],\n output_format: type[T],\n tools: list[dict[str, Any]] | None = None,\n stop: list[str] | None = None,\n ) -> ChatInvokeCompletion[T]: ...\n\n async def ainvoke(\n self,\n messages: list[BaseMessage],\n output_format: type[T] | None = None,\n tools: list[dict[str, Any]] | None = None,\n stop: list[str] | None = None,\n ) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:\n \"\"\"\n DeepSeek ainvoke 支持:\n 1. 普通文本/多轮对话\n 2. Function Calling\n 3. JSON Output (response_format)\n 4. 对话前缀续写 (beta, prefix, stop)\n \"\"\"\n client = self._client()\n ds_messages = DeepSeekMessageSerializer.serialize_messages(messages)\n common: dict[str, Any] = {}\n if self.temperature is not None:\n common[\"temperature\"] = self.temperature\n if self.max_tokens is not None:\n common[\"max_tokens\"] = self.max_tokens\n\n # Beta 对话前缀续写(见官方文档)\n if self.base_url and str(self.base_url).endswith(\"/beta\"):\n # 最后一个 assistant 必须 prefix\n if ds_messages and isinstance(ds_messages[-1], dict) and ds_messages[-1].get(\"role\") == \"assistant\":\n ds_messages[-1][\"prefix\"] = True\n if stop:\n common[\"stop\"] = stop\n\n # ① 普通多轮对话/文本输出\n if output_format is None and not tools:\n try:\n resp = await client.chat.completions.create( # type: ignore\n model=self.model,\n messages=ds_messages, # type: ignore\n **common,\n )\n return ChatInvokeCompletion(\n completion=resp.choices[0].message.content or \"\",\n usage=None,\n )\n except RateLimitError as e:\n raise ModelRateLimitError(str(e), model=self.name) from e\n except (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n raise ModelProviderError(str(e), model=self.name) from e\n except Exception as e:\n raise ModelProviderError(str(e), model=self.name) from e\n\n # ② Function Calling 路径(有 tools 或 output_format)\n if tools or (output_format is not None and hasattr(output_format, \"model_json_schema\")):\n try:\n call_tools = tools\n tool_choice = None\n if output_format is not None and hasattr(output_format, \"model_json_schema\"):\n tool_name = output_format.__name__\n schema = SchemaOptimizer.create_optimized_json_schema(output_format)\n schema.pop(\"title\", None)\n call_tools = [\n {\n \"type\": \"function\",\n \"function\": {\n \"name\": tool_name,\n \"description\": f\"Return a JSON object of type {tool_name}\",\n \"parameters\": schema,\n },\n }\n ]\n tool_choice = {\"type\": \"function\", \"function\": {\"name\": tool_name}}\n resp = await client.chat.completions.create( # type: ignore\n model=self.model,\n messages=ds_messages, # type: ignore\n tools=call_tools, # type: ignore\n tool_choice=tool_choice, # type: ignore\n **common,\n )\n msg = resp.choices[0].message\n if not msg.tool_calls:\n raise ValueError(\"Expected tool_calls in response but got none\")\n raw_args = msg.tool_calls[0].function.arguments\n if isinstance(raw_args, str):\n parsed = json.loads(raw_args)\n else:\n parsed = raw_args\n # --------- 修复点: 只有 output_format 不为 None 才能用 model_validate ----------\n if output_format is not None:\n return ChatInvokeCompletion(\n completion=output_format.model_validate(parsed),\n usage=None,\n )\n else:\n # 若无 output_format,直接返回 dict\n return ChatInvokeCompletion(\n completion=parsed,\n usage=None,\n )\n except RateLimitError as e:\n raise ModelRateLimitError(str(e), model=self.name) from e\n except (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n raise ModelProviderError(str(e), model=self.name) from e\n except Exception as e:\n raise ModelProviderError(str(e), model=self.name) from e\n\n # ③ JSON Output 路径(官方 response_format)\n if output_format is not None and hasattr(output_format, \"model_json_schema\"):\n try:\n resp = await client.chat.completions.create( # type: ignore\n model=self.model,\n messages=ds_messages, # type: ignore\n response_format={\"type\": \"json_object\"},\n **common,\n )\n content = resp.choices[0].message.content\n if not content:\n raise ModelProviderError(\"Empty JSON content in DeepSeek response\", model=self.name)\n parsed = output_format.model_validate_json(content)\n return ChatInvokeCompletion(\n completion=parsed,\n usage=None,\n )\n except RateLimitError as e:\n raise ModelRateLimitError(str(e), model=self.name) from e\n except (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n raise ModelProviderError(str(e), model=self.name) from e\n except Exception as e:\n raise ModelProviderError(str(e), model=self.name) from e\n\n # 所有路径兜底\n raise ModelProviderError(\"No valid ainvoke execution path for DeepSeek LLM\", model=self.name)\n"}, "e9f539a714af5f5388c10c3bfcaae4fba11cf9fb": {"t": 1785753144.7474368, "n": 7614, "h": "62bcce0e0e400951", "c": "from __future__ import annotations\n\nimport json\nfrom dataclasses import dataclass\nfrom typing import Any, TypeVar, overload\n\nimport httpx\nfrom openai import AsyncOpenAI, APIError, APIConnectionError, APITimeoutError, APIStatusError, RateLimitError\nfrom pydantic import BaseModel\n\nfrom browser_use.llm.base import BaseChatModel\nfrom browser_use.llm.deepseek.serializer import DeepSeekMessageSerializer\nfrom browser_use.llm.exceptions import ModelProviderError, ModelRateLimitError\nfrom browser_use.llm.schema import SchemaOptimizer\nfrom browser_use.llm.messages import BaseMessage\nfrom browser_use.llm.views import ChatInvokeCompletion\n\nT = TypeVar(\"T\", bound=BaseModel)\n\n\n@dataclass\nclass ChatDeepSeek(BaseChatModel):\n \"\"\"DeepSeek /chat/completions 封装(OpenAI-compatible)。\"\"\"\n\n model: str = \"deepseek-chat\"\n\n # 生成参数\n max_tokens: int | None = None\n temperature: float | None = None\n\n # 连接参数\n api_key: str | None = None\n base_url: str | httpx.URL | None = \"https://api.deepseek.com/v1\"\n timeout: float | httpx.Timeout | None = None\n client_params: dict[str, Any] | None = None\n\n @property\n def provider(self) -> str:\n return \"deepseek\"\n\n def _client(self) -> AsyncOpenAI:\n return AsyncOpenAI(\n api_key=self.api_key,\n base_url=self.base_url,\n timeout=self.timeout,\n **(self.client_params or {}),\n )\n\n @property\n def name(self) -> str:\n return self.model\n\n @overload\n async def ainvoke(\n self,\n messages: list[BaseMessage],\n output_format: None = None,\n tools: list[dict[str, Any]] | None = None,\n stop: list[str] | None = None,\n ) -> ChatInvokeCompletion[str]: ...\n\n @overload\n async def ainvoke(\n self,\n messages: list[BaseMessage],\n output_format: type[T],\n tools: list[dict[str, Any]] | None = None,\n stop: list[str] | None = None,\n ) -> ChatInvokeCompletion[T]: ...\n\n async def ainvoke(\n self,\n messages: list[BaseMessage],\n output_format: type[T] | None = None,\n tools: list[dict[str, Any]] | None = None,\n stop: list[str] | None = None,\n ) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:\n \"\"\"\n DeepSeek ainvoke 支持:\n 1. 普通文本/多轮对话\n 2. Function Calling\n 3. JSON Output (response_format)\n 4. 对话前缀续写 (beta, prefix, stop)\n \"\"\"\n client = self._client()\n ds_messages = DeepSeekMessageSerializer.serialize_messages(messages)\n common: dict[str, Any] = {}\n if self.temperature is not None:\n common[\"temperature\"] = self.temperature\n if self.max_tokens is not None:\n common[\"max_tokens\"] = self.max_tokens\n\n # Beta 对话前缀续写(见官方文档)\n if self.base_url and str(self.base_url).endswith(\"/beta\"):\n # 最后一个 assistant 必须 prefix\n if ds_messages and isinstance(ds_messages[-1], dict) and ds_messages[-1].get(\"role\") == \"assistant\":\n ds_messages[-1][\"prefix\"] = True\n if stop:\n common[\"stop\"] = stop\n\n # ① 普通多轮对话/文本输出\n if output_format is None and (not tools):\n try:\n resp = await client.chat.completions.create( # type: ignore\n model=self.model,\n messages=ds_messages, # type: ignore\n **common,\n )\n return ChatInvokeCompletion(\n completion=resp.choices[0].message.content or \"\",\n usage=None,\n )\n except RateLimitError as e:\n raise ModelRateLimitError(str(e), model=self.name) from e\n except (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n raise ModelProviderError(str(e), model=self.name) from e\n except Exception as e:\n raise ModelProviderError(str(e), model=self.name) from e\n\n # ② Function Calling 路径(有 tools 或 output_format 为 pydantic 模型)\n if tools or (output_format and hasattr(output_format, \"model_json_schema\")):\n try:\n call_tools = tools\n tool_choice = None\n if output_format and hasattr(output_format, \"model_json_schema\"):\n tool_name = output_format.__name__\n schema = SchemaOptimizer.create_optimized_json_schema(output_format)\n schema.pop(\"title\", None)\n call_tools = [\n {\n \"type\": \"function\",\n \"function\": {\n \"name\": tool_name,\n \"description\": f\"Return a JSON object of type {tool_name}\",\n \"parameters\": schema,\n },\n }\n ]\n tool_choice = {\"type\": \"function\", \"function\": {\"name\": tool_name}}\n resp = await client.chat.completions.create( # type: ignore\n model=self.model,\n messages=ds_messages, # type: ignore\n tools=call_tools, # type: ignore\n tool_choice=tool_choice, # type: ignore\n **common,\n )\n msg = resp.choices[0].message\n if not msg.tool_calls:\n raise ValueError(\"Expected tool_calls in response but got none\")\n raw_args = msg.tool_calls[0].function.arguments\n if isinstance(raw_args, str):\n parsed = json.loads(raw_args)\n else:\n parsed = raw_args\n return ChatInvokeCompletion(\n completion=output_format.model_validate(parsed),\n usage=None,\n )\n except RateLimitError as e:\n raise ModelRateLimitError(str(e), model=self.name) from e\n except (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n raise ModelProviderError(str(e), model=self.name) from e\n except Exception as e:\n raise ModelProviderError(str(e), model=self.name) from e\n\n # ③ JSON Output 路径(官方 response_format)\n if output_format is not None and hasattr(output_format, \"model_json_schema\"):\n try:\n resp = await client.chat.completions.create( # type: ignore\n model=self.model,\n messages=ds_messages, # type: ignore\n response_format={\"type\": \"json_object\"},\n **common,\n )\n content = resp.choices[0].message.content\n if not content:\n raise ModelProviderError(\"Empty JSON content in DeepSeek response\", model=self.name)\n parsed = output_format.model_validate_json(content)\n return ChatInvokeCompletion(\n completion=parsed,\n usage=None,\n )\n except RateLimitError as e:\n raise ModelRateLimitError(str(e), model=self.name) from e\n except (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n raise ModelProviderError(str(e), model=self.name) from e\n except Exception as e:\n raise ModelProviderError(str(e), model=self.name) from e\n\n # 所有路径兜底\n raise ModelProviderError(\"No valid ainvoke execution path for DeepSeek LLM\", model=self.name)\n"}, "36e50a739e77531dce1edc6cd84357bd9616e4f9": {"t": 1785753146.4416246, "n": 7564, "h": "18c44e3564711aba", "c": "from __future__ import annotations\n\nimport json\nfrom dataclasses import dataclass\nfrom typing import Any, TypeVar, overload\n\nimport httpx\nfrom openai import AsyncOpenAI, APIError, APIConnectionError, APITimeoutError, APIStatusError, RateLimitError\nfrom pydantic import BaseModel\n\nfrom browser_use.llm.base import BaseChatModel\nfrom browser_use.llm.deepseek.serializer import DeepSeekMessageSerializer\nfrom browser_use.llm.exceptions import ModelProviderError, ModelRateLimitError\nfrom browser_use.llm.schema import SchemaOptimizer # ✅ 与 Anthropic 共用\nfrom browser_use.llm.messages import BaseMessage\nfrom browser_use.llm.views import ChatInvokeCompletion\n\nT = TypeVar(\"T\", bound=BaseModel)\n\n\n@dataclass\nclass ChatDeepSeek(BaseChatModel):\n \"\"\"DeepSeek /chat/completions 封装(OpenAI-compatible)。\"\"\"\n\n model: str = \"deepseek-chat\"\n\n # 生成参数\n max_tokens: int | None = None\n temperature: float | None = None\n\n # 连接参数\n api_key: str | None = None\n base_url: str | httpx.URL | None = \"https://api.deepseek.com/v1\"\n timeout: float | httpx.Timeout | None = None\n client_params: dict[str, Any] | None = None\n\n # ------------------------------------------------------------------ #\n @property\n def provider(self) -> str: # 供 browser-use 判断\n return \"deepseek\"\n\n def _client(self) -> AsyncOpenAI:\n return AsyncOpenAI(\n api_key=self.api_key,\n base_url=self.base_url,\n timeout=self.timeout,\n **(self.client_params or {}),\n )\n\n @property\n def name(self) -> str:\n return self.model\n\n # ---------------------- 核心调用接口 -------------------------------- #\n @overload\n async def ainvoke(\n self,\n messages: list[BaseMessage],\n output_format: None = None,\n tools: list[dict[str, Any]] | None = None,\n ) -> ChatInvokeCompletion[str]: ...\n\n @overload\n async def ainvoke(\n self,\n messages: list[BaseMessage],\n output_format: type[T],\n tools: list[dict[str, Any]] | None = None,\n ) -> ChatInvokeCompletion[T]: ...\n\n async def ainvoke(\n self,\n messages: list[BaseMessage],\n output_format: type[T] | None = None,\n tools: list[dict[str, Any]] | None = None,\n stop: list[str] | None = None, # beta 对话前缀支持\n ) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:\n \"\"\"\n DeepSeek ainvoke 支持:\n 1. 普通文本/多轮对话\n 2. Function Calling\n 3. JSON Output (response_format)\n 4. 对话前缀续写 (beta, prefix, stop)\n \"\"\"\n client = self._client()\n ds_messages = DeepSeekMessageSerializer.serialize_messages(messages)\n common: dict[str, Any] = {}\n if self.temperature is not None:\n common[\"temperature\"] = self.temperature\n if self.max_tokens is not None:\n common[\"max_tokens\"] = self.max_tokens\n\n # 若检测到 beta 对话前缀续写\n if self.base_url and str(self.base_url).endswith(\"/beta\"):\n # 最后一个 assistant 必须 prefix\n if ds_messages and ds_messages[-1].get(\"role\") == \"assistant\":\n ds_messages[-1][\"prefix\"] = True\n if stop:\n common[\"stop\"] = stop\n\n # 普通 Function Calling 路径\n if output_format is None and (not tools):\n try:\n resp = await client.chat.completions.create(\n model=self.model,\n messages=ds_messages,\n **common,\n )\n return ChatInvokeCompletion(\n completion=resp.choices[0].message.content or \"\",\n usage=None,\n )\n except RateLimitError as e:\n raise ModelRateLimitError(str(e), model=self.name) from e\n except (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n raise ModelProviderError(str(e), model=self.name) from e\n except Exception as e:\n raise ModelProviderError(str(e), model=self.name) from e\n\n # 若 Function Calling 路径 (有 tools/自动工具推理)\n if tools or (output_format and hasattr(output_format, \"model_json_schema\")):\n try:\n call_tools = tools\n tool_choice = None\n if output_format and hasattr(output_format, \"model_json_schema\"):\n # output_format 存在,构造 function 工具 (自动推理工具名和 schema)\n tool_name = output_format.__name__\n schema = SchemaOptimizer.create_optimized_json_schema(output_format)\n schema.pop(\"title\", None)\n call_tools = [\n {\n \"type\": \"function\",\n \"function\": {\n \"name\": tool_name,\n \"description\": f\"Return a JSON object of type {tool_name}\",\n \"parameters\": schema,\n },\n }\n ]\n tool_choice = {\"type\": \"function\", \"function\": {\"name\": tool_name}}\n resp = await client.chat.completions.create(\n model=self.model,\n messages=ds_messages,\n tools=call_tools,\n tool_choice=tool_choice,\n **common,\n )\n msg = resp.choices[0].message\n if not msg.tool_calls:\n raise ValueError(\"Expected tool_calls in response but got none\")\n raw_args = msg.tool_calls[0].function.arguments\n if isinstance(raw_args, str):\n parsed = json.loads(raw_args)\n else:\n parsed = raw_args\n return ChatInvokeCompletion(\n completion=output_format.model_validate(parsed),\n usage=None,\n )\n except RateLimitError as e:\n raise ModelRateLimitError(str(e), model=self.name) from e\n except (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n raise ModelProviderError(str(e), model=self.name) from e\n except Exception as e:\n raise ModelProviderError(str(e), model=self.name) from e\n\n # JSON Output 路径 (官方 response_format)\n if output_format is not None and hasattr(output_format, \"model_json_schema\"):\n # 符合 deepseek 官方 JSON Output\n try:\n resp = await client.chat.completions.create(\n model=self.model,\n messages=ds_messages,\n response_format={\"type\": \"json_object\"},\n **common,\n )\n content = resp.choices[0].message.content\n if not content:\n raise ModelProviderError(\"Empty JSON content in DeepSeek response\", model=self.name)\n parsed = output_format.model_validate_json(content)\n return ChatInvokeCompletion(\n completion=parsed,\n usage=None,\n )\n except RateLimitError as e:\n raise ModelRateLimitError(str(e), model=self.name) from e\n except (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:\n raise ModelProviderError(str(e), model=self.name) from e\n except Exception as e:\n raise ModelProviderError(str(e), model=self.name) from e\n"}}}}