from __future__ import annotations import logging import os from typing import Any, Dict, List import httpx from app.exceptions import ProviderAPICallError from app.models import LLMChoice, LLMMessage, LLMProvider, LLMRequest, LLMResponse from app.providers.base import LLMProviderClient logger = logging.getLogger(__name__) class OpenRouterProvider(LLMProviderClient): name = LLMProvider.OPENROUTER.value api_key_env = "OPENROUTER_API_KEY" supports_stream = True base_url = "https://openrouter.ai/api/v1/chat/completions" def __init__(self, api_key: str | None): super().__init__(api_key) self.site_url = os.getenv("OPENROUTER_SITE_URL") self.app_name = os.getenv("OPENROUTER_APP_NAME") async def chat( self, request: LLMRequest, client: httpx.AsyncClient ) -> LLMResponse: self.ensure_stream_supported(request.stream) payload = self.merge_payload( { "model": request.model, "messages": [msg.model_dump() for msg in request.messages], "temperature": request.temperature, "top_p": request.top_p, "max_tokens": request.max_tokens, "stream": request.stream, }, request.extra_params, ) headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } if self.site_url: headers["HTTP-Referer"] = self.site_url if self.app_name: headers["X-Title"] = self.app_name try: response = await client.post(self.base_url, json=payload, headers=headers) response.raise_for_status() except httpx.HTTPStatusError as exc: status_code = exc.response.status_code body = exc.response.text logger.error( "OpenRouter upstream returned %s: %s", status_code, body, exc_info=True ) raise ProviderAPICallError( f"OpenRouter request failed with status {status_code}", status_code=status_code, response_text=body, ) from exc except httpx.HTTPError as exc: logger.error("OpenRouter transport error: %s", exc, exc_info=True) raise ProviderAPICallError(f"OpenRouter request failed: {exc}") from exc data: Dict[str, Any] = response.json() choices = self._build_choices(data.get("choices", [])) return LLMResponse( provider=LLMProvider.OPENROUTER, model=data.get("model", request.model), choices=choices, raw=data, ) @staticmethod def _build_choices(choices: List[dict[str, Any]]) -> List[LLMChoice]: built: List[LLMChoice] = [] for choice in choices: message_data = choice.get("message") or {} message = LLMMessage( role=message_data.get("role", "assistant"), content=message_data.get("content", ""), ) built.append(LLMChoice(index=choice.get("index", len(built)), message=message)) return built