{"repo": "abdrsl/vibebridge", "files": {"src/vibebridge/providers/openrouter.py": {"b9d8d20882fa994eccef9e11cdfd3edbb9d4a645": {"t": 1785746501.6504695, "n": 11709, "h": "09a13f35d989448f", "c": "\"\"\"OpenRouter provider for VibeBridge.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nimport os\nfrom typing import AsyncIterator\n\nimport httpx\n\nfrom .base import BaseProvider, StreamEvent, StreamEventType\n\nlogger = logging.getLogger(__name__)\n\n\nclass OpenRouterProvider(BaseProvider):\n \"\"\"OpenRouter provider using their API.\"\"\"\n\n name = \"openrouter\"\n display_name = \"OpenRouter\"\n\n def __init__(\n self,\n api_key: str | None = None,\n default_model: str = \"openai/gpt-4o\",\n base_url: str = \"https://openrouter.ai/api/v1\",\n ):\n self.api_key = api_key or os.getenv(\"OPENROUTER_API_KEY\", \"\")\n self.default_model = default_model\n self.base_url = base_url\n self._client: httpx.AsyncClient | None = None\n self._tasks: dict[str, asyncio.Task] = {}\n self._task_contexts: dict[str, dict[str, str]] = {}\n\n async def _get_client(self) -> httpx.AsyncClient:\n \"\"\"Get or create HTTP client.\"\"\"\n if self._client is None:\n headers = {\n \"Authorization\": f\"Bearer {self.api_key}\",\n \"Content-Type\": \"application/json\",\n \"HTTP-Referer\": \"https://vibebridge.ai\",\n \"X-Title\": \"VibeBridge\",\n }\n self._client = httpx.AsyncClient(\n headers=headers, timeout=httpx.Timeout(300.0, connect=10.0)\n )\n return self._client\n\n async def health_check(self) -> tuple[bool, str]:\n \"\"\"Check if OpenRouter API is accessible.\"\"\"\n if not self.api_key:\n return False, \"OpenRouter API key not configured\"\n\n try:\n client = await self._get_client()\n response = await client.get(f\"{self.base_url}/models\")\n if response.status_code == 200:\n return True, \"OpenRouter API is accessible\"\n else:\n return False, f\"OpenRouter API returned status {response.status_code}\"\n except Exception as e:\n return False, f\"OpenRouter API error: {str(e)}\"\n\n async def create_task(\n self,\n prompt: str,\n workdir: str,\n session_id: str,\n chat_id: str | None = None,\n ) -> str:\n \"\"\"Create a task and return task_id.\"\"\"\n task_id = f\"openrouter_{session_id}_{hash(prompt) & 0xFFFFFFFF}\"\n \n # Store task context so stream_task can access the original prompt\n self._task_contexts[task_id] = {\n \"prompt\": prompt,\n \"workdir\": workdir,\n \"session_id\": session_id,\n \"chat_id\": chat_id or \"\",\n }\n \n # Start the task in background\n task = asyncio.create_task(self._execute_task(task_id, prompt, workdir))\n self._tasks[task_id] = task\n \n return task_id\n\n async def _execute_task(self, task_id: str, prompt: str, workdir: str):\n \"\"\"Execute the task and store result.\"\"\"\n try:\n logger.info(f\"OpenRouter task {task_id} created for prompt: {prompt[:100]}...\")\n except Exception as e:\n logger.error(f\"Error in OpenRouter task {task_id}: {e}\")\n\n async def stream_task(self, task_id: str) -> AsyncIterator[StreamEvent]:\n \"\"\"Stream task execution using OpenRouter API.\"\"\"\n # Retrieve the original prompt from task context\n ctx = self._task_contexts.get(task_id, {})\n prompt = ctx.get(\"prompt\", \"\")\n if not prompt:\n logger.warning(f\"OpenRouter task {task_id}: no prompt found in context, using fallback\")\n prompt = \"Execute the user's request\"\n \n try:\n client = await self._get_client()\n \n # Send initial status\n yield StreamEvent(\n type=StreamEventType.STATUS,\n content=\"Starting OpenRouter task...\",\n task_id=task_id,\n )\n \n # Prepare the request\n messages = [\n {\n \"role\": \"system\",\n \"content\": \"You are an AI assistant that helps with coding tasks. Provide clear, executable solutions.\"\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ]\n \n data = {\n \"model\": self.default_model,\n \"messages\": messages,\n \"stream\": True,\n \"temperature\": 0.7,\n \"max_tokens\": 4000,\n }\n \n # Make streaming request\n async with client.stream(\n \"POST\",\n f\"{self.base_url}/chat/completions\",\n json=data,\n ) as response:\n response.raise_for_status()\n \n buffer = \"\"\n async for chunk in response.aiter_bytes():\n chunk_str = chunk.decode(\"utf-8\")\n lines = chunk_str.split(\"\\n\")\n \n for line in lines:\n line = line.strip()\n if not line or line == \"data: [DONE]\":\n continue\n \n if line.startswith(\"data: \"):\n data_str = line[6:]\n try:\n data_obj = json.loads(data_str)\n choices = data_obj.get(\"choices\", [])\n if choices:\n delta = choices[0].get(\"delta\", {})\n content = delta.get(\"content\", \"\")\n if content:\n buffer += content\n # Yield text in chunks\n if len(buffer) > 100 or \"\\n\" in content:\n yield StreamEvent(\n type=StreamEventType.TEXT,\n content=buffer,\n task_id=task_id,\n )\n buffer = \"\"\n except json.JSONDecodeError:\n continue\n \n # Yield any remaining buffer\n if buffer:\n yield StreamEvent(\n type=StreamEventType.TEXT,\n content=buffer,\n task_id=task_id,\n )\n \n # Yield completion\n yield StreamEvent(\n type=StreamEventType.DONE,\n content=\"Task completed successfully\",\n task_id=task_id,\n )\n \n except Exception as e:\n logger.error(f\"Error streaming OpenRouter task {task_id}: {e}\")\n yield StreamEvent(\n type=StreamEventType.ERROR,\n content=f\"OpenRouter API error: {str(e)}\",\n task_id=task_id,\n )\n finally:\n # Clean up task and context\n if task_id in self._tasks:\n del self._tasks[task_id]\n if task_id in self._task_contexts:\n del self._task_contexts[task_id]\n\n async def cancel_task(self, task_id: str) -> bool:\n \"\"\"Cancel a running task.\"\"\"\n if task_id in self._tasks:\n task = self._tasks[task_id]\n task.cancel()\n try:\n await task\n except asyncio.CancelledError:\n pass\n del self._tasks[task_id]\n return True\n return False\n\n def default_workdir(self) -> str:\n \"\"\"Return the suggested default working directory.\"\"\"\n return os.path.expanduser(\"~/workspace\")\n\n async def close(self):\n \"\"\"Close the HTTP client.\"\"\"\n if self._client:\n await self._client.aclose()\n self._client = None\n\n async def test_all_models(self) -> dict:\n \"\"\"Test all available models on OpenRouter.\"\"\"\n if not self.api_key:\n return {\"error\": \"OpenRouter API key not configured\"}\n \n try:\n client = await self._get_client()\n response = await client.get(f\"{self.base_url}/models\")\n \n if response.status_code != 200:\n return {\"error\": f\"Failed to fetch models: {response.status_code}\"}\n \n models_data = response.json()\n models = models_data.get(\"data\", [])\n \n results = {\n \"total_models\": len(models),\n \"available_models\": [],\n \"test_results\": {}\n }\n \n # Test a subset of popular models\n popular_models = [\n \"openai/gpt-4o\",\n \"openai/gpt-4-turbo\",\n \"anthropic/claude-3.5-sonnet\",\n \"anthropic/claude-3-opus\",\n \"google/gemini-2.0-flash-exp\",\n \"meta-llama/llama-3.3-70b-instruct\",\n \"mistralai/mistral-large-2411\",\n \"deepseek/deepseek-chat\",\n \"deepseek/deepseek-reasoner\",\n \"qwen/qwen-2.5-72b-instruct\",\n ]\n \n for model in popular_models:\n # Check if model is available\n model_available = any(m.get(\"id\") == model for m in models)\n \n if model_available:\n # Test the model with a simple prompt\n test_result = await self._test_model(client, model)\n results[\"test_results\"][model] = test_result\n results[\"available_models\"].append(model)\n else:\n results[\"test_results\"][model] = {\"available\": False, \"error\": \"Model not found\"}\n \n return results\n \n except Exception as e:\n return {\"error\": f\"Error testing models: {str(e)}\"}\n\n async def _test_model(self, client: httpx.AsyncClient, model: str) -> dict:\n \"\"\"Test a specific model with a simple prompt.\"\"\"\n try:\n messages = [\n {\n \"role\": \"user\",\n \"content\": \"Hello! Please respond with 'OK' if you can hear me.\"\n }\n ]\n \n data = {\n \"model\": model,\n \"messages\": messages,\n \"max_tokens\": 10,\n \"temperature\": 0.1,\n }\n \n response = await client.post(\n f\"{self.base_url}/chat/completions\",\n json=data,\n timeout=10.0\n )\n \n if response.status_code == 200:\n result = response.json()\n content = result[\"choices\"][0][\"message\"][\"content\"].strip()\n return {\n \"available\": True,\n \"response\": content,\n \"tokens\": result.get(\"usage\", {}).get(\"total_tokens\", 0)\n }\n else:\n return {\n \"available\": False,\n \"error\": f\"API error: {response.status_code}\",\n \"details\": response.text[:200]\n }\n \n except Exception as e:\n return {\n \"available\": False,\n \"error\": f\"Test failed: {str(e)}\"\n }"}, "4fde75c0641efce8bbaccb9cc1214dd542db7dfe": {"t": 1785765004.5854676, "n": 11709, "h": "09a13f35d989448f", "c": "\"\"\"OpenRouter provider for VibeBridge.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nimport os\nfrom typing import AsyncIterator\n\nimport httpx\n\nfrom .base import BaseProvider, StreamEvent, StreamEventType\n\nlogger = logging.getLogger(__name__)\n\n\nclass OpenRouterProvider(BaseProvider):\n \"\"\"OpenRouter provider using their API.\"\"\"\n\n name = \"openrouter\"\n display_name = \"OpenRouter\"\n\n def __init__(\n self,\n api_key: str | None = None,\n default_model: str = \"openai/gpt-4o\",\n base_url: str = \"https://openrouter.ai/api/v1\",\n ):\n self.api_key = api_key or os.getenv(\"OPENROUTER_API_KEY\", \"\")\n self.default_model = default_model\n self.base_url = base_url\n self._client: httpx.AsyncClient | None = None\n self._tasks: dict[str, asyncio.Task] = {}\n self._task_contexts: dict[str, dict[str, str]] = {}\n\n async def _get_client(self) -> httpx.AsyncClient:\n \"\"\"Get or create HTTP client.\"\"\"\n if self._client is None:\n headers = {\n \"Authorization\": f\"Bearer {self.api_key}\",\n \"Content-Type\": \"application/json\",\n \"HTTP-Referer\": \"https://vibebridge.ai\",\n \"X-Title\": \"VibeBridge\",\n }\n self._client = httpx.AsyncClient(\n headers=headers, timeout=httpx.Timeout(300.0, connect=10.0)\n )\n return self._client\n\n async def health_check(self) -> tuple[bool, str]:\n \"\"\"Check if OpenRouter API is accessible.\"\"\"\n if not self.api_key:\n return False, \"OpenRouter API key not configured\"\n\n try:\n client = await self._get_client()\n response = await client.get(f\"{self.base_url}/models\")\n if response.status_code == 200:\n return True, \"OpenRouter API is accessible\"\n else:\n return False, f\"OpenRouter API returned status {response.status_code}\"\n except Exception as e:\n return False, f\"OpenRouter API error: {str(e)}\"\n\n async def create_task(\n self,\n prompt: str,\n workdir: str,\n session_id: str,\n chat_id: str | None = None,\n ) -> str:\n \"\"\"Create a task and return task_id.\"\"\"\n task_id = f\"openrouter_{session_id}_{hash(prompt) & 0xFFFFFFFF}\"\n \n # Store task context so stream_task can access the original prompt\n self._task_contexts[task_id] = {\n \"prompt\": prompt,\n \"workdir\": workdir,\n \"session_id\": session_id,\n \"chat_id\": chat_id or \"\",\n }\n \n # Start the task in background\n task = asyncio.create_task(self._execute_task(task_id, prompt, workdir))\n self._tasks[task_id] = task\n \n return task_id\n\n async def _execute_task(self, task_id: str, prompt: str, workdir: str):\n \"\"\"Execute the task and store result.\"\"\"\n try:\n logger.info(f\"OpenRouter task {task_id} created for prompt: {prompt[:100]}...\")\n except Exception as e:\n logger.error(f\"Error in OpenRouter task {task_id}: {e}\")\n\n async def stream_task(self, task_id: str) -> AsyncIterator[StreamEvent]:\n \"\"\"Stream task execution using OpenRouter API.\"\"\"\n # Retrieve the original prompt from task context\n ctx = self._task_contexts.get(task_id, {})\n prompt = ctx.get(\"prompt\", \"\")\n if not prompt:\n logger.warning(f\"OpenRouter task {task_id}: no prompt found in context, using fallback\")\n prompt = \"Execute the user's request\"\n \n try:\n client = await self._get_client()\n \n # Send initial status\n yield StreamEvent(\n type=StreamEventType.STATUS,\n content=\"Starting OpenRouter task...\",\n task_id=task_id,\n )\n \n # Prepare the request\n messages = [\n {\n \"role\": \"system\",\n \"content\": \"You are an AI assistant that helps with coding tasks. Provide clear, executable solutions.\"\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ]\n \n data = {\n \"model\": self.default_model,\n \"messages\": messages,\n \"stream\": True,\n \"temperature\": 0.7,\n \"max_tokens\": 4000,\n }\n \n # Make streaming request\n async with client.stream(\n \"POST\",\n f\"{self.base_url}/chat/completions\",\n json=data,\n ) as response:\n response.raise_for_status()\n \n buffer = \"\"\n async for chunk in response.aiter_bytes():\n chunk_str = chunk.decode(\"utf-8\")\n lines = chunk_str.split(\"\\n\")\n \n for line in lines:\n line = line.strip()\n if not line or line == \"data: [DONE]\":\n continue\n \n if line.startswith(\"data: \"):\n data_str = line[6:]\n try:\n data_obj = json.loads(data_str)\n choices = data_obj.get(\"choices\", [])\n if choices:\n delta = choices[0].get(\"delta\", {})\n content = delta.get(\"content\", \"\")\n if content:\n buffer += content\n # Yield text in chunks\n if len(buffer) > 100 or \"\\n\" in content:\n yield StreamEvent(\n type=StreamEventType.TEXT,\n content=buffer,\n task_id=task_id,\n )\n buffer = \"\"\n except json.JSONDecodeError:\n continue\n \n # Yield any remaining buffer\n if buffer:\n yield StreamEvent(\n type=StreamEventType.TEXT,\n content=buffer,\n task_id=task_id,\n )\n \n # Yield completion\n yield StreamEvent(\n type=StreamEventType.DONE,\n content=\"Task completed successfully\",\n task_id=task_id,\n )\n \n except Exception as e:\n logger.error(f\"Error streaming OpenRouter task {task_id}: {e}\")\n yield StreamEvent(\n type=StreamEventType.ERROR,\n content=f\"OpenRouter API error: {str(e)}\",\n task_id=task_id,\n )\n finally:\n # Clean up task and context\n if task_id in self._tasks:\n del self._tasks[task_id]\n if task_id in self._task_contexts:\n del self._task_contexts[task_id]\n\n async def cancel_task(self, task_id: str) -> bool:\n \"\"\"Cancel a running task.\"\"\"\n if task_id in self._tasks:\n task = self._tasks[task_id]\n task.cancel()\n try:\n await task\n except asyncio.CancelledError:\n pass\n del self._tasks[task_id]\n return True\n return False\n\n def default_workdir(self) -> str:\n \"\"\"Return the suggested default working directory.\"\"\"\n return os.path.expanduser(\"~/workspace\")\n\n async def close(self):\n \"\"\"Close the HTTP client.\"\"\"\n if self._client:\n await self._client.aclose()\n self._client = None\n\n async def test_all_models(self) -> dict:\n \"\"\"Test all available models on OpenRouter.\"\"\"\n if not self.api_key:\n return {\"error\": \"OpenRouter API key not configured\"}\n \n try:\n client = await self._get_client()\n response = await client.get(f\"{self.base_url}/models\")\n \n if response.status_code != 200:\n return {\"error\": f\"Failed to fetch models: {response.status_code}\"}\n \n models_data = response.json()\n models = models_data.get(\"data\", [])\n \n results = {\n \"total_models\": len(models),\n \"available_models\": [],\n \"test_results\": {}\n }\n \n # Test a subset of popular models\n popular_models = [\n \"openai/gpt-4o\",\n \"openai/gpt-4-turbo\",\n \"anthropic/claude-3.5-sonnet\",\n \"anthropic/claude-3-opus\",\n \"google/gemini-2.0-flash-exp\",\n \"meta-llama/llama-3.3-70b-instruct\",\n \"mistralai/mistral-large-2411\",\n \"deepseek/deepseek-chat\",\n \"deepseek/deepseek-reasoner\",\n \"qwen/qwen-2.5-72b-instruct\",\n ]\n \n for model in popular_models:\n # Check if model is available\n model_available = any(m.get(\"id\") == model for m in models)\n \n if model_available:\n # Test the model with a simple prompt\n test_result = await self._test_model(client, model)\n results[\"test_results\"][model] = test_result\n results[\"available_models\"].append(model)\n else:\n results[\"test_results\"][model] = {\"available\": False, \"error\": \"Model not found\"}\n \n return results\n \n except Exception as e:\n return {\"error\": f\"Error testing models: {str(e)}\"}\n\n async def _test_model(self, client: httpx.AsyncClient, model: str) -> dict:\n \"\"\"Test a specific model with a simple prompt.\"\"\"\n try:\n messages = [\n {\n \"role\": \"user\",\n \"content\": \"Hello! Please respond with 'OK' if you can hear me.\"\n }\n ]\n \n data = {\n \"model\": model,\n \"messages\": messages,\n \"max_tokens\": 10,\n \"temperature\": 0.1,\n }\n \n response = await client.post(\n f\"{self.base_url}/chat/completions\",\n json=data,\n timeout=10.0\n )\n \n if response.status_code == 200:\n result = response.json()\n content = result[\"choices\"][0][\"message\"][\"content\"].strip()\n return {\n \"available\": True,\n \"response\": content,\n \"tokens\": result.get(\"usage\", {}).get(\"total_tokens\", 0)\n }\n else:\n return {\n \"available\": False,\n \"error\": f\"API error: {response.status_code}\",\n \"details\": response.text[:200]\n }\n \n except Exception as e:\n return {\n \"available\": False,\n \"error\": f\"Test failed: {str(e)}\"\n }"}, "609080ba9d33964707f6ffe15534c2ad8908c71a": {"t": 1785765005.8294215, "n": 11203, "h": "057f1d926e3d1ada", "c": "\"\"\"OpenRouter provider for VibeBridge.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nimport os\nfrom typing import AsyncIterator\n\nimport httpx\n\nfrom .base import BaseProvider, StreamEvent, StreamEventType\n\nlogger = logging.getLogger(__name__)\n\n\nclass OpenRouterProvider(BaseProvider):\n \"\"\"OpenRouter provider using their API.\"\"\"\n\n name = \"openrouter\"\n display_name = \"OpenRouter\"\n\n def __init__(\n self,\n api_key: str | None = None,\n default_model: str = \"openai/gpt-4o\",\n base_url: str = \"https://openrouter.ai/api/v1\",\n ):\n self.api_key = api_key or os.getenv(\"OPENROUTER_API_KEY\", \"\")\n self.default_model = default_model\n self.base_url = base_url\n self._client: httpx.AsyncClient | None = None\n self._tasks: dict[str, asyncio.Task] = {}\n\n async def _get_client(self) -> httpx.AsyncClient:\n \"\"\"Get or create HTTP client.\"\"\"\n if self._client is None:\n headers = {\n \"Authorization\": f\"Bearer {self.api_key}\",\n \"Content-Type\": \"application/json\",\n \"HTTP-Referer\": \"https://vibebridge.ai\",\n \"X-Title\": \"VibeBridge\",\n }\n self._client = httpx.AsyncClient(\n headers=headers, timeout=httpx.Timeout(300.0, connect=10.0)\n )\n return self._client\n\n async def health_check(self) -> tuple[bool, str]:\n \"\"\"Check if OpenRouter API is accessible.\"\"\"\n if not self.api_key:\n return False, \"OpenRouter API key not configured\"\n\n try:\n client = await self._get_client()\n response = await client.get(f\"{self.base_url}/models\")\n if response.status_code == 200:\n return True, \"OpenRouter API is accessible\"\n else:\n return False, f\"OpenRouter API returned status {response.status_code}\"\n except Exception as e:\n return False, f\"OpenRouter API error: {str(e)}\"\n\n async def create_task(\n self,\n prompt: str,\n workdir: str,\n session_id: str,\n chat_id: str | None = None,\n ) -> str:\n \"\"\"Create a task and return task_id.\"\"\"\n task_id = f\"openrouter_{session_id}_{hash(prompt) & 0xFFFFFFFF}\"\n \n # Start the task in background\n task = asyncio.create_task(self._execute_task(task_id, prompt, workdir))\n self._tasks[task_id] = task\n \n return task_id\n\n async def _execute_task(self, task_id: str, prompt: str, workdir: str):\n \"\"\"Execute the task and store result.\"\"\"\n try:\n # This is a placeholder - actual execution would happen in stream_task\n logger.info(f\"OpenRouter task {task_id} created for prompt: {prompt[:100]}...\")\n except Exception as e:\n logger.error(f\"Error in OpenRouter task {task_id}: {e}\")\n\n async def stream_task(self, task_id: str) -> AsyncIterator[StreamEvent]:\n \"\"\"Stream task execution using OpenRouter API.\"\"\"\n # Get the prompt from task context (in real implementation, we'd store it)\n # For now, we'll use a dummy prompt\n prompt = \"Execute the user's request\"\n \n try:\n client = await self._get_client()\n \n # Send initial status\n yield StreamEvent(\n type=StreamEventType.STATUS,\n content=\"Starting OpenRouter task...\",\n task_id=task_id,\n )\n \n # Prepare the request\n messages = [\n {\n \"role\": \"system\",\n \"content\": \"You are an AI assistant that helps with coding tasks. Provide clear, executable solutions.\"\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ]\n \n data = {\n \"model\": self.default_model,\n \"messages\": messages,\n \"stream\": True,\n \"temperature\": 0.7,\n \"max_tokens\": 4000,\n }\n \n # Make streaming request\n async with client.stream(\n \"POST\",\n f\"{self.base_url}/chat/completions\",\n json=data,\n ) as response:\n response.raise_for_status()\n \n buffer = \"\"\n async for chunk in response.aiter_bytes():\n chunk_str = chunk.decode(\"utf-8\")\n lines = chunk_str.split(\"\\n\")\n \n for line in lines:\n line = line.strip()\n if not line or line == \"data: [DONE]\":\n continue\n \n if line.startswith(\"data: \"):\n data_str = line[6:]\n try:\n data_obj = json.loads(data_str)\n choices = data_obj.get(\"choices\", [])\n if choices:\n delta = choices[0].get(\"delta\", {})\n content = delta.get(\"content\", \"\")\n if content:\n buffer += content\n # Yield text in chunks\n if len(buffer) > 100 or \"\\n\" in content:\n yield StreamEvent(\n type=StreamEventType.TEXT,\n content=buffer,\n task_id=task_id,\n )\n buffer = \"\"\n except json.JSONDecodeError:\n continue\n \n # Yield any remaining buffer\n if buffer:\n yield StreamEvent(\n type=StreamEventType.TEXT,\n content=buffer,\n task_id=task_id,\n )\n \n # Yield completion\n yield StreamEvent(\n type=StreamEventType.DONE,\n content=\"Task completed successfully\",\n task_id=task_id,\n )\n \n except Exception as e:\n logger.error(f\"Error streaming OpenRouter task {task_id}: {e}\")\n yield StreamEvent(\n type=StreamEventType.ERROR,\n content=f\"OpenRouter API error: {str(e)}\",\n task_id=task_id,\n )\n finally:\n # Clean up task\n if task_id in self._tasks:\n del self._tasks[task_id]\n\n async def cancel_task(self, task_id: str) -> bool:\n \"\"\"Cancel a running task.\"\"\"\n if task_id in self._tasks:\n task = self._tasks[task_id]\n task.cancel()\n try:\n await task\n except asyncio.CancelledError:\n pass\n del self._tasks[task_id]\n return True\n return False\n\n def default_workdir(self) -> str:\n \"\"\"Return the suggested default working directory.\"\"\"\n return os.path.expanduser(\"~/workspace\")\n\n async def close(self):\n \"\"\"Close the HTTP client.\"\"\"\n if self._client:\n await self._client.aclose()\n self._client = None\n\n async def test_all_models(self) -> dict:\n \"\"\"Test all available models on OpenRouter.\"\"\"\n if not self.api_key:\n return {\"error\": \"OpenRouter API key not configured\"}\n \n try:\n client = await self._get_client()\n response = await client.get(f\"{self.base_url}/models\")\n \n if response.status_code != 200:\n return {\"error\": f\"Failed to fetch models: {response.status_code}\"}\n \n models_data = response.json()\n models = models_data.get(\"data\", [])\n \n results = {\n \"total_models\": len(models),\n \"available_models\": [],\n \"test_results\": {}\n }\n \n # Test a subset of popular models\n popular_models = [\n \"openai/gpt-4o\",\n \"openai/gpt-4-turbo\",\n \"anthropic/claude-3.5-sonnet\",\n \"anthropic/claude-3-opus\",\n \"google/gemini-2.0-flash-exp\",\n \"meta-llama/llama-3.3-70b-instruct\",\n \"mistralai/mistral-large-2411\",\n \"deepseek/deepseek-chat\",\n \"deepseek/deepseek-reasoner\",\n \"qwen/qwen-2.5-72b-instruct\",\n ]\n \n for model in popular_models:\n # Check if model is available\n model_available = any(m.get(\"id\") == model for m in models)\n \n if model_available:\n # Test the model with a simple prompt\n test_result = await self._test_model(client, model)\n results[\"test_results\"][model] = test_result\n results[\"available_models\"].append(model)\n else:\n results[\"test_results\"][model] = {\"available\": False, \"error\": \"Model not found\"}\n \n return results\n \n except Exception as e:\n return {\"error\": f\"Error testing models: {str(e)}\"}\n\n async def _test_model(self, client: httpx.AsyncClient, model: str) -> dict:\n \"\"\"Test a specific model with a simple prompt.\"\"\"\n try:\n messages = [\n {\n \"role\": \"user\",\n \"content\": \"Hello! Please respond with 'OK' if you can hear me.\"\n }\n ]\n \n data = {\n \"model\": model,\n \"messages\": messages,\n \"max_tokens\": 10,\n \"temperature\": 0.1,\n }\n \n response = await client.post(\n f\"{self.base_url}/chat/completions\",\n json=data,\n timeout=10.0\n )\n \n if response.status_code == 200:\n result = response.json()\n content = result[\"choices\"][0][\"message\"][\"content\"].strip()\n return {\n \"available\": True,\n \"response\": content,\n \"tokens\": result.get(\"usage\", {}).get(\"total_tokens\", 0)\n }\n else:\n return {\n \"available\": False,\n \"error\": f\"API error: {response.status_code}\",\n \"details\": response.text[:200]\n }\n \n except Exception as e:\n return {\n \"available\": False,\n \"error\": f\"Test failed: {str(e)}\"\n }"}}, "src/vibebridge/legacy/secure_config.py": {"b9d8d20882fa994eccef9e11cdfd3edbb9d4a645": {"t": 1785746896.8498871, "n": 10283, "h": "954aa47d912a2321", "c": "\"\"\"\nSecure configuration management with optional encryption for sensitive values.\n\nUsage:\n1. Set a master key via environment variable: AI_MASTER_KEY or in ~/.ai-product-lab/master.key\n2. Encrypt sensitive values using the encrypt_value() function\n3. Store encrypted values in .env with prefix ENC_\n4. Use get_secret() to automatically decrypt values\n\nExample:\n Before: DEEPSEEK_API_KEY=sk-abc123\n After: DEEPSEEK_API_KEY_ENC=ENC:gAAAAABm... (encrypted)\n\n In code: api_key = get_secret(\"DEEPSEEK_API_KEY\")\n\"\"\"\n\nimport base64\nimport binascii\nimport hashlib\nimport os\nfrom pathlib import Path\nfrom typing import Optional\n\nimport os\nfrom cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes\nfrom cryptography.hazmat.primitives import padding\nfrom cryptography.hazmat.backends import default_backend\n\n# Constants\nMASTER_KEY_ENV_VAR = \"AI_MASTER_KEY\"\nMASTER_KEY_FILE = Path.home() / \".ai-product-lab\" / \"master.key\"\nENCRYPTED_PREFIX = \"ENC:\"\nDEFAULT_SENSITIVE_KEYS = [\n \"DEEPSEEK_API_KEY\",\n \"MOONSHOT_API_KEY\",\n \"FEISHU_APP_SECRET\",\n \"FEISHU_ENCRYPT_KEY\",\n \"FEISHU_VERIFICATION_TOKEN\",\n \"NGROK_AUTHTOKEN\",\n \"OPENAI_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"GEMINI_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"LITELLM_MASTER_KEY\",\n \"POSTGRES_PASSWORD\",\n \"REDIS_PASSWORD\",\n \"QDRANT_API_KEY\",\n]\n\n\nclass SecureConfigError(Exception):\n \"\"\"Secure configuration error.\"\"\"\n\n pass\n\n\ndef _get_master_key() -> bytes:\n \"\"\"Get master key from environment variable or file.\"\"\"\n # Try environment variable first\n master_key = os.getenv(MASTER_KEY_ENV_VAR)\n if master_key:\n # Ensure it's 32 bytes for AES-256\n key_hash = hashlib.sha256(master_key.encode()).digest()\n return key_hash\n\n # Try key file\n if MASTER_KEY_FILE.exists():\n try:\n key_data = MASTER_KEY_FILE.read_bytes()\n # File can contain raw bytes or base64 encoded string\n if len(key_data) == 32:\n return key_data\n else:\n # Try to decode as base64\n try:\n decoded = base64.b64decode(key_data)\n if len(decoded) == 32:\n return decoded\n else:\n # Hash to 32 bytes\n return hashlib.sha256(decoded).digest()[:32]\n except (binascii.Error, ValueError):\n # Hash the raw bytes\n return hashlib.sha256(key_data).digest()[:32]\n except Exception as e:\n raise SecureConfigError(f\"Failed to read master key file: {e}\")\n\n # No master key found - in development, we can fall back to a default\n # but warn loudly. In production, this should fail.\n if os.getenv(\"APP_ENV\") == \"dev\":\n print(\"⚠️ WARNING: No master key found. Using default development key.\")\n print(f\" Set {MASTER_KEY_ENV_VAR} or create {MASTER_KEY_FILE}\")\n # Generate deterministic key from string for development\n dev_key = \"dev-master-key-insecure-change-in-production\"\n return hashlib.sha256(dev_key.encode()).digest()\n\n raise SecureConfigError(\n f\"Master key required. Set {MASTER_KEY_ENV_VAR} environment variable \"\n f\"or create {MASTER_KEY_FILE} with 32-byte key.\"\n )\n\n\ndef _create_cipher(key: bytes, iv: bytes) -> Cipher:\n \"\"\"Create AES cipher in CBC mode.\"\"\"\n return Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())\n\n\ndef encrypt_value(plaintext: str) -> str:\n \"\"\"\n Encrypt a plaintext string.\n\n Returns:\n Base64 encoded string with format: ENC:\n \"\"\"\n if not plaintext:\n return \"\"\n\n master_key = _get_master_key()\n iv = os.urandom(16)\n cipher = _create_cipher(master_key, iv)\n encryptor = cipher.encryptor()\n\n # Encrypt with PKCS7 padding\n padder = padding.PKCS7(128).padder()\n padded_data = padder.update(plaintext.encode(\"utf-8\")) + padder.finalize()\n ciphertext = encryptor.update(padded_data) + encryptor.finalize()\n\n # Combine IV and ciphertext\n encrypted = iv + ciphertext\n encoded = base64.b64encode(encrypted).decode(\"utf-8\")\n\n return f\"{ENCRYPTED_PREFIX}{encoded}\"\n\n\ndef decrypt_value(encrypted_str: str) -> str:\n \"\"\"\n Decrypt an encrypted string.\n\n Args:\n encrypted_str: String with format ENC:\n\n Returns:\n Decrypted plaintext string\n \"\"\"\n if not encrypted_str:\n return \"\"\n\n if not encrypted_str.startswith(ENCRYPTED_PREFIX):\n # Not encrypted, return as-is (for backwards compatibility)\n return encrypted_str\n\n encoded = encrypted_str[len(ENCRYPTED_PREFIX) :]\n\n try:\n encrypted = base64.b64decode(encoded)\n except Exception as e:\n raise SecureConfigError(f\"Invalid base64 encoding: {e}\")\n\n if len(encrypted) < 16:\n raise SecureConfigError(\"Encrypted data too short\")\n\n iv = encrypted[:16]\n ciphertext = encrypted[16:]\n\n master_key = _get_master_key()\n cipher = _create_cipher(master_key, iv)\n\n try:\n decryptor = cipher.decryptor()\n decrypted = decryptor.update(ciphertext) + decryptor.finalize()\n unpadder = padding.PKCS7(128).unpadder()\n plaintext = unpadder.update(decrypted) + unpadder.finalize()\n return plaintext.decode(\"utf-8\")\n except Exception as e:\n raise SecureConfigError(f\"Decryption failed: {e}\")\n\n\ndef get_secret(key_name: str, default: Optional[str] = None) -> Optional[str]:\n \"\"\"\n Get a secret value, automatically decrypting if encrypted.\n\n Checks for encrypted version first (key_name + \"_ENC\"), then falls back\n to plain version. Returns None if neither exists.\n \"\"\"\n # Try encrypted version first\n enc_key = f\"{key_name}_ENC\"\n enc_value = os.getenv(enc_key)\n\n if enc_value:\n try:\n return decrypt_value(enc_value)\n except SecureConfigError as e:\n print(f\"⚠️ Failed to decrypt {key_name}: {e}\")\n # Fall back to plain version\n\n # Try plain version\n plain_value = os.getenv(key_name)\n if plain_value:\n # Warn if sensitive key is stored in plaintext\n if key_name in DEFAULT_SENSITIVE_KEYS and os.getenv(\"APP_ENV\") != \"test\":\n print(\n f\"⚠️ WARNING: {key_name} is stored in plaintext. Consider encrypting it.\"\n )\n return plain_value\n\n return default\n\n\ndef get_config(key_name: str, default: Optional[str] = None) -> Optional[str]:\n \"\"\"\n Get configuration value (non-sensitive). Same as os.getenv but with logging.\n \"\"\"\n value = os.getenv(key_name, default)\n return value\n\n\ndef migrate_env_file(input_path: str = \".env\", output_path: str = \".env.encrypted\"):\n \"\"\"\n Migrate .env file to use encrypted values for sensitive keys.\n\n Creates a new file with encrypted values for sensitive keys.\n Original keys are commented out and new _ENC keys are added.\n \"\"\"\n input_file = Path(input_path)\n if not input_file.exists():\n raise FileNotFoundError(f\"Input file not found: {input_path}\")\n\n lines = input_file.read_text().splitlines()\n output_lines = []\n\n for line in lines:\n line = line.strip()\n\n # Skip empty lines and comments\n if not line or line.startswith(\"#\"):\n output_lines.append(line)\n continue\n\n # Parse key=value\n if \"=\" not in line:\n output_lines.append(line)\n continue\n\n key, value = line.split(\"=\", 1)\n key = key.strip()\n value = value.strip()\n\n # Remove quotes if present\n if value.startswith(('\"', \"'\")) and value.endswith(('\"', \"'\")):\n value = value[1:-1]\n\n # Check if this is a sensitive key\n if key in DEFAULT_SENSITIVE_KEYS and value:\n # Encrypt the value\n try:\n encrypted = encrypt_value(value)\n # Comment out original line\n output_lines.append(f\"# {line}\")\n # Add encrypted version\n output_lines.append(f\"{key}_ENC={encrypted}\")\n print(f\"✓ Encrypted {key}\")\n except Exception as e:\n print(f\"✗ Failed to encrypt {key}: {e}\")\n output_lines.append(line)\n else:\n output_lines.append(line)\n\n # Write output file\n output_file = Path(output_path)\n output_file.write_text(\"\\n\".join(output_lines))\n print(f\"\\n✅ Created encrypted environment file: {output_path}\")\n print(\" Review the file and replace your original .env if ready.\")\n print(\" Don't forget to set your master key!\")\n print(f\" Export: export {MASTER_KEY_ENV_VAR}=$(cat ~/.ai-product-lab/master.key)\")\n\n\ndef init_master_key(key_path: Optional[str] = None) -> str:\n \"\"\"\n Initialize a new random master key.\n\n Args:\n key_path: Path to save the key (default: ~/.ai-product-lab/master.key)\n\n Returns:\n Base64 encoded key (for display only)\n \"\"\"\n if key_path:\n key_file = Path(key_path)\n else:\n key_file = MASTER_KEY_FILE\n\n # Create parent directory if needed\n key_file.parent.mkdir(parents=True, exist_ok=True)\n\n # Generate random 32-byte key\n random_key = get_random_bytes(32)\n\n # Save as base64\n encoded_key = base64.b64encode(random_key).decode(\"utf-8\")\n key_file.write_text(encoded_key)\n\n # Set restrictive permissions (owner read/write only)\n key_file.chmod(0o600)\n\n print(f\"✅ Generated new master key at: {key_file}\")\n print(f\" Key (base64): {encoded_key}\")\n print(f\"\\n To use immediately: export {MASTER_KEY_ENV_VAR}='{encoded_key}'\")\n print(f\" Or ensure the key file is at: {MASTER_KEY_FILE}\")\n\n return encoded_key\n\n\n# Convenience functions for common secrets\ndef get_deepseek_key() -> Optional[str]:\n return get_secret(\"DEEPSEEK_API_KEY\")\n\n\ndef get_feishu_app_secret() -> Optional[str]:\n return get_secret(\"FEISHU_APP_SECRET\")\n\n\ndef get_feishu_encrypt_key() -> Optional[str]:\n return get_secret(\"FEISHU_ENCRYPT_KEY\")\n\n\ndef get_feishu_verification_token() -> Optional[str]:\n return get_secret(\"FEISHU_VERIFICATION_TOKEN\")\n\n\ndef get_ngrok_token() -> Optional[str]:\n return get_secret(\"NGROK_AUTHTOKEN\")\n"}, "eec1b4fcb63813f73c442c9463f5bbb553612f9b": {"t": 1785765006.0588107, "n": 10283, "h": "954aa47d912a2321", "c": "\"\"\"\nSecure configuration management with optional encryption for sensitive values.\n\nUsage:\n1. Set a master key via environment variable: AI_MASTER_KEY or in ~/.ai-product-lab/master.key\n2. Encrypt sensitive values using the encrypt_value() function\n3. Store encrypted values in .env with prefix ENC_\n4. Use get_secret() to automatically decrypt values\n\nExample:\n Before: DEEPSEEK_API_KEY=sk-abc123\n After: DEEPSEEK_API_KEY_ENC=ENC:gAAAAABm... (encrypted)\n\n In code: api_key = get_secret(\"DEEPSEEK_API_KEY\")\n\"\"\"\n\nimport base64\nimport binascii\nimport hashlib\nimport os\nfrom pathlib import Path\nfrom typing import Optional\n\nimport os\nfrom cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes\nfrom cryptography.hazmat.primitives import padding\nfrom cryptography.hazmat.backends import default_backend\n\n# Constants\nMASTER_KEY_ENV_VAR = \"AI_MASTER_KEY\"\nMASTER_KEY_FILE = Path.home() / \".ai-product-lab\" / \"master.key\"\nENCRYPTED_PREFIX = \"ENC:\"\nDEFAULT_SENSITIVE_KEYS = [\n \"DEEPSEEK_API_KEY\",\n \"MOONSHOT_API_KEY\",\n \"FEISHU_APP_SECRET\",\n \"FEISHU_ENCRYPT_KEY\",\n \"FEISHU_VERIFICATION_TOKEN\",\n \"NGROK_AUTHTOKEN\",\n \"OPENAI_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"GEMINI_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"LITELLM_MASTER_KEY\",\n \"POSTGRES_PASSWORD\",\n \"REDIS_PASSWORD\",\n \"QDRANT_API_KEY\",\n]\n\n\nclass SecureConfigError(Exception):\n \"\"\"Secure configuration error.\"\"\"\n\n pass\n\n\ndef _get_master_key() -> bytes:\n \"\"\"Get master key from environment variable or file.\"\"\"\n # Try environment variable first\n master_key = os.getenv(MASTER_KEY_ENV_VAR)\n if master_key:\n # Ensure it's 32 bytes for AES-256\n key_hash = hashlib.sha256(master_key.encode()).digest()\n return key_hash\n\n # Try key file\n if MASTER_KEY_FILE.exists():\n try:\n key_data = MASTER_KEY_FILE.read_bytes()\n # File can contain raw bytes or base64 encoded string\n if len(key_data) == 32:\n return key_data\n else:\n # Try to decode as base64\n try:\n decoded = base64.b64decode(key_data)\n if len(decoded) == 32:\n return decoded\n else:\n # Hash to 32 bytes\n return hashlib.sha256(decoded).digest()[:32]\n except (binascii.Error, ValueError):\n # Hash the raw bytes\n return hashlib.sha256(key_data).digest()[:32]\n except Exception as e:\n raise SecureConfigError(f\"Failed to read master key file: {e}\")\n\n # No master key found - in development, we can fall back to a default\n # but warn loudly. In production, this should fail.\n if os.getenv(\"APP_ENV\") == \"dev\":\n print(\"⚠️ WARNING: No master key found. Using default development key.\")\n print(f\" Set {MASTER_KEY_ENV_VAR} or create {MASTER_KEY_FILE}\")\n # Generate deterministic key from string for development\n dev_key = \"dev-master-key-insecure-change-in-production\"\n return hashlib.sha256(dev_key.encode()).digest()\n\n raise SecureConfigError(\n f\"Master key required. Set {MASTER_KEY_ENV_VAR} environment variable \"\n f\"or create {MASTER_KEY_FILE} with 32-byte key.\"\n )\n\n\ndef _create_cipher(key: bytes, iv: bytes) -> Cipher:\n \"\"\"Create AES cipher in CBC mode.\"\"\"\n return Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())\n\n\ndef encrypt_value(plaintext: str) -> str:\n \"\"\"\n Encrypt a plaintext string.\n\n Returns:\n Base64 encoded string with format: ENC:\n \"\"\"\n if not plaintext:\n return \"\"\n\n master_key = _get_master_key()\n iv = os.urandom(16)\n cipher = _create_cipher(master_key, iv)\n encryptor = cipher.encryptor()\n\n # Encrypt with PKCS7 padding\n padder = padding.PKCS7(128).padder()\n padded_data = padder.update(plaintext.encode(\"utf-8\")) + padder.finalize()\n ciphertext = encryptor.update(padded_data) + encryptor.finalize()\n\n # Combine IV and ciphertext\n encrypted = iv + ciphertext\n encoded = base64.b64encode(encrypted).decode(\"utf-8\")\n\n return f\"{ENCRYPTED_PREFIX}{encoded}\"\n\n\ndef decrypt_value(encrypted_str: str) -> str:\n \"\"\"\n Decrypt an encrypted string.\n\n Args:\n encrypted_str: String with format ENC:\n\n Returns:\n Decrypted plaintext string\n \"\"\"\n if not encrypted_str:\n return \"\"\n\n if not encrypted_str.startswith(ENCRYPTED_PREFIX):\n # Not encrypted, return as-is (for backwards compatibility)\n return encrypted_str\n\n encoded = encrypted_str[len(ENCRYPTED_PREFIX) :]\n\n try:\n encrypted = base64.b64decode(encoded)\n except Exception as e:\n raise SecureConfigError(f\"Invalid base64 encoding: {e}\")\n\n if len(encrypted) < 16:\n raise SecureConfigError(\"Encrypted data too short\")\n\n iv = encrypted[:16]\n ciphertext = encrypted[16:]\n\n master_key = _get_master_key()\n cipher = _create_cipher(master_key, iv)\n\n try:\n decryptor = cipher.decryptor()\n decrypted = decryptor.update(ciphertext) + decryptor.finalize()\n unpadder = padding.PKCS7(128).unpadder()\n plaintext = unpadder.update(decrypted) + unpadder.finalize()\n return plaintext.decode(\"utf-8\")\n except Exception as e:\n raise SecureConfigError(f\"Decryption failed: {e}\")\n\n\ndef get_secret(key_name: str, default: Optional[str] = None) -> Optional[str]:\n \"\"\"\n Get a secret value, automatically decrypting if encrypted.\n\n Checks for encrypted version first (key_name + \"_ENC\"), then falls back\n to plain version. Returns None if neither exists.\n \"\"\"\n # Try encrypted version first\n enc_key = f\"{key_name}_ENC\"\n enc_value = os.getenv(enc_key)\n\n if enc_value:\n try:\n return decrypt_value(enc_value)\n except SecureConfigError as e:\n print(f\"⚠️ Failed to decrypt {key_name}: {e}\")\n # Fall back to plain version\n\n # Try plain version\n plain_value = os.getenv(key_name)\n if plain_value:\n # Warn if sensitive key is stored in plaintext\n if key_name in DEFAULT_SENSITIVE_KEYS and os.getenv(\"APP_ENV\") != \"test\":\n print(\n f\"⚠️ WARNING: {key_name} is stored in plaintext. Consider encrypting it.\"\n )\n return plain_value\n\n return default\n\n\ndef get_config(key_name: str, default: Optional[str] = None) -> Optional[str]:\n \"\"\"\n Get configuration value (non-sensitive). Same as os.getenv but with logging.\n \"\"\"\n value = os.getenv(key_name, default)\n return value\n\n\ndef migrate_env_file(input_path: str = \".env\", output_path: str = \".env.encrypted\"):\n \"\"\"\n Migrate .env file to use encrypted values for sensitive keys.\n\n Creates a new file with encrypted values for sensitive keys.\n Original keys are commented out and new _ENC keys are added.\n \"\"\"\n input_file = Path(input_path)\n if not input_file.exists():\n raise FileNotFoundError(f\"Input file not found: {input_path}\")\n\n lines = input_file.read_text().splitlines()\n output_lines = []\n\n for line in lines:\n line = line.strip()\n\n # Skip empty lines and comments\n if not line or line.startswith(\"#\"):\n output_lines.append(line)\n continue\n\n # Parse key=value\n if \"=\" not in line:\n output_lines.append(line)\n continue\n\n key, value = line.split(\"=\", 1)\n key = key.strip()\n value = value.strip()\n\n # Remove quotes if present\n if value.startswith(('\"', \"'\")) and value.endswith(('\"', \"'\")):\n value = value[1:-1]\n\n # Check if this is a sensitive key\n if key in DEFAULT_SENSITIVE_KEYS and value:\n # Encrypt the value\n try:\n encrypted = encrypt_value(value)\n # Comment out original line\n output_lines.append(f\"# {line}\")\n # Add encrypted version\n output_lines.append(f\"{key}_ENC={encrypted}\")\n print(f\"✓ Encrypted {key}\")\n except Exception as e:\n print(f\"✗ Failed to encrypt {key}: {e}\")\n output_lines.append(line)\n else:\n output_lines.append(line)\n\n # Write output file\n output_file = Path(output_path)\n output_file.write_text(\"\\n\".join(output_lines))\n print(f\"\\n✅ Created encrypted environment file: {output_path}\")\n print(\" Review the file and replace your original .env if ready.\")\n print(\" Don't forget to set your master key!\")\n print(f\" Export: export {MASTER_KEY_ENV_VAR}=$(cat ~/.ai-product-lab/master.key)\")\n\n\ndef init_master_key(key_path: Optional[str] = None) -> str:\n \"\"\"\n Initialize a new random master key.\n\n Args:\n key_path: Path to save the key (default: ~/.ai-product-lab/master.key)\n\n Returns:\n Base64 encoded key (for display only)\n \"\"\"\n if key_path:\n key_file = Path(key_path)\n else:\n key_file = MASTER_KEY_FILE\n\n # Create parent directory if needed\n key_file.parent.mkdir(parents=True, exist_ok=True)\n\n # Generate random 32-byte key\n random_key = get_random_bytes(32)\n\n # Save as base64\n encoded_key = base64.b64encode(random_key).decode(\"utf-8\")\n key_file.write_text(encoded_key)\n\n # Set restrictive permissions (owner read/write only)\n key_file.chmod(0o600)\n\n print(f\"✅ Generated new master key at: {key_file}\")\n print(f\" Key (base64): {encoded_key}\")\n print(f\"\\n To use immediately: export {MASTER_KEY_ENV_VAR}='{encoded_key}'\")\n print(f\" Or ensure the key file is at: {MASTER_KEY_FILE}\")\n\n return encoded_key\n\n\n# Convenience functions for common secrets\ndef get_deepseek_key() -> Optional[str]:\n return get_secret(\"DEEPSEEK_API_KEY\")\n\n\ndef get_feishu_app_secret() -> Optional[str]:\n return get_secret(\"FEISHU_APP_SECRET\")\n\n\ndef get_feishu_encrypt_key() -> Optional[str]:\n return get_secret(\"FEISHU_ENCRYPT_KEY\")\n\n\ndef get_feishu_verification_token() -> Optional[str]:\n return get_secret(\"FEISHU_VERIFICATION_TOKEN\")\n\n\ndef get_ngrok_token() -> Optional[str]:\n return get_secret(\"NGROK_AUTHTOKEN\")\n"}}}}