{"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"}}}}