LLM_Monitor / openai_adapter.py
potato-pzy
feat: remove powered-by line and integrate sidecar with sentence-level streaming
02422e3
Raw
History Blame Contribute Delete
4 kB
"""
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