| """OpenAI-compatible LLM client (QuantaAlpha style).""" |
|
|
| from __future__ import annotations |
|
|
| import json |
| import os |
| from dataclasses import dataclass |
| from pathlib import Path |
| from typing import Any |
|
|
| import yaml |
|
|
| from config.settings import PROJECT_ROOT |
|
|
|
|
| @dataclass |
| class LLMConfig: |
| api_key: str |
| base_url: str |
| chat_model: str |
| reasoning_model: str | None = None |
| temperature: float = 0.3 |
| max_tokens: int = 4000 |
| max_retry: int = 3 |
|
|
|
|
| def load_llm_config(config_path: str | None = None) -> LLMConfig: |
| path = PROJECT_ROOT / "config" / "quantaalpha.yaml" |
| if config_path: |
| path = Path(config_path) if Path(config_path).is_absolute() else PROJECT_ROOT / config_path |
|
|
| raw: dict[str, Any] = {} |
| if path.exists(): |
| with open(path, encoding="utf-8") as f: |
| raw = yaml.safe_load(f) or {} |
|
|
| llm = raw.get("llm", {}) |
| return LLMConfig( |
| api_key=os.environ.get("OPENAI_API_KEY", llm.get("api_key", "")), |
| base_url=os.environ.get("OPENAI_BASE_URL", llm.get("base_url", "https://api.openai.com/v1")), |
| chat_model=os.environ.get("CHAT_MODEL", llm.get("chat_model", "gpt-4o-mini")), |
| reasoning_model=os.environ.get("REASONING_MODEL", llm.get("reasoning_model")), |
| temperature=float(os.environ.get("CHAT_TEMPERATURE", llm.get("temperature", 0.3))), |
| max_tokens=int(os.environ.get("CHAT_MAX_TOKENS", llm.get("max_tokens", 4000))), |
| max_retry=int(os.environ.get("MAX_RETRY", llm.get("max_retry", 3))), |
| ) |
|
|
|
|
| class QuantaAlphaLLMClient: |
| """Thin wrapper over OpenAI-compatible chat completions API.""" |
|
|
| def __init__(self, config: LLMConfig | None = None): |
| self.config = config or load_llm_config() |
| if not self.config.api_key: |
| raise ValueError( |
| "OPENAI_API_KEY not set. Configure config/quantaalpha.yaml or export OPENAI_API_KEY." |
| ) |
|
|
| try: |
| from openai import OpenAI |
| except ImportError as exc: |
| raise ImportError("Install openai: pip install openai") from exc |
|
|
| self._client = OpenAI(api_key=self.config.api_key, base_url=self.config.base_url) |
|
|
| def chat( |
| self, |
| messages: list[dict[str, str]], |
| model: str | None = None, |
| temperature: float | None = None, |
| response_json: bool = False, |
| ) -> str: |
| model = model or self.config.chat_model |
| temperature = self.config.temperature if temperature is None else temperature |
|
|
| kwargs: dict[str, Any] = { |
| "model": model, |
| "messages": messages, |
| "temperature": temperature, |
| "max_tokens": self.config.max_tokens, |
| } |
| if response_json: |
| kwargs["response_format"] = {"type": "json_object"} |
|
|
| last_err = None |
| for _ in range(self.config.max_retry): |
| try: |
| resp = self._client.chat.completions.create(**kwargs) |
| return resp.choices[0].message.content or "" |
| except Exception as exc: |
| last_err = exc |
| raise RuntimeError(f"LLM request failed after retries: {last_err}") |
|
|
| def chat_json(self, messages: list[dict[str, str]], **kwargs) -> dict[str, Any]: |
| text = self.chat(messages, response_json=True, **kwargs) |
| return json.loads(text) |
|
|