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__) def _resolve_timeout_seconds() -> float: raw = os.getenv("DEEPSEEK_TIMEOUT_SECONDS") if raw is None: return 60.0 try: return float(raw) except ValueError: logger.warning( "Invalid value for DEEPSEEK_TIMEOUT_SECONDS=%r, falling back to 60 seconds", raw, ) return 60.0 DEEPSEEK_TIMEOUT_SECONDS = _resolve_timeout_seconds() class DeepSeekProvider(LLMProviderClient): name = LLMProvider.DEEPSEEK.value api_key_env = "DEEPSEEK_API_KEY" supports_stream = True base_url = "https://api.deepseek.com/v1/chat/completions" 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", } timeout = httpx.Timeout(DEEPSEEK_TIMEOUT_SECONDS) try: response = await client.post( self.base_url, json=payload, headers=headers, timeout=timeout ) response.raise_for_status() except httpx.HTTPStatusError as exc: status_code = exc.response.status_code body = exc.response.text logger.error("DeepSeek upstream returned %s: %s", status_code, body, exc_info=True) raise ProviderAPICallError( f"DeepSeek request failed with status {status_code}", status_code=status_code, response_text=body, ) from exc except httpx.HTTPError as exc: logger.error("DeepSeek transport error: %s", exc, exc_info=True) raise ProviderAPICallError(f"DeepSeek request failed: {exc}") from exc data: Dict[str, Any] = response.json() choices = self._build_choices(data.get("choices", [])) return LLMResponse( provider=LLMProvider.DEEPSEEK, 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