Spaces:
Runtime error
Runtime error
| """ | |
| openai_adapter.py — Concrete LLMAdapter for OpenAI / OpenRouter / Ollama. | |
| V3 (Sidecar): adds stream_chat() for sentence-level streaming. | |
| Works with any OpenAI-compatible API. Configure via environment variables: | |
| OPENAI_API_KEY=sk-... | |
| OPENAI_BASE_URL=https://openrouter.ai/api/v1 | |
| OPENAI_MODEL=gpt-4o-mini | |
| """ | |
| import os | |
| from typing import AsyncGenerator, Optional | |
| from openai import AsyncOpenAI, OpenAI | |
| from llm_adapter import LLMAdapter | |
| class OpenAIAdapter(LLMAdapter): | |
| """ | |
| Wraps any OpenAI-compatible API as an LLMAdapter. | |
| Args: | |
| api_key: API key. Defaults to OPENAI_API_KEY env var. | |
| base_url: API base URL. Defaults to OPENAI_BASE_URL env var. | |
| model: Model name. Defaults to OPENAI_MODEL env var. | |
| system_prompt: Default system prompt for all calls. | |
| temperature: Sampling temperature (0 = deterministic). | |
| max_tokens: Max tokens in response. | |
| """ | |
| DEFAULT_BASE_URL = "https://api.openai.com/v1" | |
| DEFAULT_MODEL = "gpt-4o-mini" | |
| def __init__( | |
| self, | |
| api_key: Optional[str] = None, | |
| base_url: Optional[str] = None, | |
| model: Optional[str] = None, | |
| system_prompt: Optional[str] = None, | |
| temperature: float = 0.7, | |
| max_tokens: int = 1024, | |
| ): | |
| self._api_key = api_key or os.getenv("OPENAI_API_KEY", "") | |
| self._base_url = base_url or os.getenv("OPENAI_BASE_URL", self.DEFAULT_BASE_URL) | |
| self._model = model or os.getenv("OPENAI_MODEL", self.DEFAULT_MODEL) | |
| self._system_prompt = system_prompt or ( | |
| "You are a helpful AI assistant. Be concise and accurate." | |
| ) | |
| self._temperature = temperature | |
| self._max_tokens = max_tokens | |
| # Synchronous client (existing blocking interface) | |
| self._client = OpenAI(api_key=self._api_key, base_url=self._base_url) | |
| # Async client (streaming interface) | |
| self._async_client = AsyncOpenAI(api_key=self._api_key, base_url=self._base_url) | |
| # ------------------------------------------------------------------ | |
| # Blocking interface (existing — unchanged) | |
| # ------------------------------------------------------------------ | |
| def chat( | |
| self, | |
| prompt: str, | |
| system_prompt: Optional[str] = None, | |
| ) -> str: | |
| """Send prompt to LLM and return response text (blocking).""" | |
| sys_msg = system_prompt or self._system_prompt | |
| response = self._client.chat.completions.create( | |
| model = self._model, | |
| messages = [ | |
| {"role": "system", "content": sys_msg}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| temperature = self._temperature, | |
| max_tokens = self._max_tokens, | |
| ) | |
| return response.choices[0].message.content or "" | |
| # ------------------------------------------------------------------ | |
| # Streaming interface (NEW) | |
| # ------------------------------------------------------------------ | |
| async def stream_chat( | |
| self, | |
| prompt: str, | |
| system_prompt: Optional[str] = None, | |
| ) -> AsyncGenerator[str, None]: | |
| """ | |
| Yield token chunks from OpenAI as they arrive (true async streaming). | |
| """ | |
| sys_msg = system_prompt or self._system_prompt | |
| stream = await self._async_client.chat.completions.create( | |
| model = self._model, | |
| messages = [ | |
| {"role": "system", "content": sys_msg}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| temperature = self._temperature, | |
| max_tokens = self._max_tokens, | |
| stream = True, | |
| ) | |
| async for chunk in stream: | |
| delta = chunk.choices[0].delta | |
| if delta and delta.content: | |
| yield delta.content | |
| def get_model_name(self) -> str: | |
| return self._model | |