"""DeepSeek / OpenAI-compatible LLM adapter — implements LLMProvider port.""" from collections.abc import AsyncIterator from langchain_openai import ChatOpenAI class DeepSeekLLMProvider: """LangChain-based LLM adapter for DeepSeek and OpenAI-compatible APIs.""" def __init__( self, api_key: str, base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat", temperature: float = 0.7, ): self._default_model = model self._default_temperature = temperature kwargs: dict = { "temperature": temperature, "model": model, "api_key": api_key, } if base_url: cleaned = base_url.rstrip("/") for suffix in ("/v1/chat/completions", "/v1"): if cleaned.endswith(suffix): cleaned = cleaned[: -len(suffix)] kwargs["base_url"] = cleaned self._llm = ChatOpenAI(**kwargs) @property def langchain_llm(self) -> ChatOpenAI: """Expose underlying ChatOpenAI for LangChain agent interop (Phase 2 will remove).""" return self._llm async def complete( self, messages: list[dict], *, temperature: float | None = None, model: str | None = None, ) -> str: llm = self._get_llm(temperature, model) lc_messages = _to_langchain_messages(messages) result = await llm.ainvoke(lc_messages) return str(result.content) async def stream( self, messages: list[dict], *, temperature: float | None = None, model: str | None = None, ) -> AsyncIterator[str]: llm = self._get_llm(temperature, model) lc_messages = _to_langchain_messages(messages) async for chunk in llm.astream(lc_messages): if chunk.content: yield str(chunk.content) def _get_llm(self, temperature: float | None, model: str | None): if temperature is None and model is None: return self._llm kwargs: dict = {} if temperature is not None: kwargs["temperature"] = temperature if model is not None: kwargs["model"] = model return self._llm.bind(**kwargs) if kwargs else self._llm def _to_langchain_messages(messages: list[dict]) -> list: from langchain_core.messages import AIMessage, HumanMessage, SystemMessage mapping = { "system": SystemMessage, "human": HumanMessage, "user": HumanMessage, "ai": AIMessage, "assistant": AIMessage, } result = [] for msg in messages: cls = mapping.get(msg.get("role", ""), HumanMessage) result.append(cls(content=msg.get("content", ""))) return result