feat(llm): switch from Anthropic to OpenAI (gpt-4o-mini)

This commit is contained in:
galymzhan 2026-05-22 14:23:54 +05:00
parent 91ecb656e8
commit 72891c7a2d
6 changed files with 47 additions and 38 deletions

View File

@ -1,3 +1,3 @@
ANTHROPIC_API_KEY=sk-ant-... OPENAI_API_KEY=sk-...
ANTHROPIC_MODEL=claude-sonnet-4-6 OPENAI_MODEL=gpt-4o-mini
ALLOWED_ORIGINS=https://sheetdoc.telecom.quest ALLOWED_ORIGINS=https://galymzhan.telecom.quest

View File

@ -4,8 +4,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings): class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore") model_config = SettingsConfigDict(env_file=".env", extra="ignore")
anthropic_api_key: str = "" openai_api_key: str = ""
anthropic_model: str = "claude-sonnet-4-6" openai_model: str = "gpt-4o-mini"
allowed_origins: str = "*" allowed_origins: str = "*"
max_csv_bytes: int = 5 * 1024 * 1024 max_csv_bytes: int = 5 * 1024 * 1024
max_rows: int = 1000 max_rows: int = 1000

View File

@ -3,7 +3,7 @@ import json
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from anthropic import AsyncAnthropic, APIError from openai import AsyncOpenAI, OpenAIError
from .config import settings from .config import settings
from .errors import LlmFailed from .errors import LlmFailed
@ -18,20 +18,50 @@ def load_prompt(name: str) -> str:
class LlmClient: class LlmClient:
def __init__(self) -> None: def __init__(self) -> None:
self._client: Optional[AsyncAnthropic] = None self._client: Optional[AsyncOpenAI] = None
@property @property
def client(self) -> AsyncAnthropic: def client(self) -> AsyncOpenAI:
if self._client is None: if self._client is None:
if not settings.anthropic_api_key: if not settings.openai_api_key:
raise LlmFailed( raise LlmFailed(
what="API-ключ не настроен.", what="API-ключ не настроен.",
why="ANTHROPIC_API_KEY не задан.", why="OPENAI_API_KEY не задан.",
next="Задайте переменную окружения и перезапустите сервис.", next="Задайте переменную окружения и перезапустите сервис.",
) )
self._client = AsyncAnthropic(api_key=settings.anthropic_api_key) self._client = AsyncOpenAI(api_key=settings.openai_api_key)
return self._client return self._client
async def _chat(
self,
system: str,
user: str,
max_tokens: int,
temperature: float,
json_mode: bool,
) -> str:
last_err: Optional[Exception] = None
for attempt in range(3):
try:
kwargs = {
"model": settings.openai_model,
"max_tokens": max_tokens,
"temperature": temperature,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": user},
],
}
if json_mode:
kwargs["response_format"] = {"type": "json_object"}
resp = await self.client.chat.completions.create(**kwargs)
content = resp.choices[0].message.content or ""
return content.strip()
except OpenAIError as e:
last_err = e
await asyncio.sleep(2 ** attempt)
raise LlmFailed(why=f"OpenAI API: {last_err}")
async def complete( async def complete(
self, self,
system: str, system: str,
@ -39,31 +69,10 @@ class LlmClient:
max_tokens: int = 2048, max_tokens: int = 2048,
temperature: float = 0.4, temperature: float = 0.4,
) -> str: ) -> str:
last_err: Optional[Exception] = None return await self._chat(system, user, max_tokens, temperature, json_mode=False)
for attempt in range(3):
try:
msg = await self.client.messages.create(
model=settings.anthropic_model,
max_tokens=max_tokens,
temperature=temperature,
system=[
{
"type": "text",
"text": system,
"cache_control": {"type": "ephemeral"},
}
],
messages=[{"role": "user", "content": user}],
)
parts = [b.text for b in msg.content if getattr(b, "text", None)]
return "".join(parts).strip()
except APIError as e:
last_err = e
await asyncio.sleep(2 ** attempt)
raise LlmFailed(why=f"Anthropic API: {last_err}")
async def complete_json(self, system: str, user: str, max_tokens: int = 2048) -> dict: async def complete_json(self, system: str, user: str, max_tokens: int = 2048) -> dict:
raw = await self.complete(system, user, max_tokens=max_tokens, temperature=0.2) raw = await self._chat(system, user, max_tokens, temperature=0.2, json_mode=True)
raw = raw.strip() raw = raw.strip()
if raw.startswith("```"): if raw.startswith("```"):
raw = raw.strip("`") raw = raw.strip("`")

View File

@ -41,7 +41,7 @@ limiter = Limiter(key_func=get_remote_address)
@asynccontextmanager @asynccontextmanager
async def lifespan(_: FastAPI): async def lifespan(_: FastAPI):
setup_logging() setup_logging()
log.info("startup", model=settings.anthropic_model) log.info("startup", model=settings.openai_model)
yield yield

View File

@ -3,7 +3,7 @@ uvicorn[standard]==0.30.6
httpx==0.27.2 httpx==0.27.2
pydantic==2.9.2 pydantic==2.9.2
pydantic-settings==2.5.2 pydantic-settings==2.5.2
anthropic==0.39.0 openai==1.54.4
python-dateutil==2.9.0 python-dateutil==2.9.0
structlog==24.4.0 structlog==24.4.0
cachetools==5.5.0 cachetools==5.5.0

View File

@ -4,8 +4,8 @@ services:
image: sheetdoc:latest image: sheetdoc:latest
restart: unless-stopped restart: unless-stopped
environment: environment:
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} OPENAI_API_KEY: ${OPENAI_API_KEY}
ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-claude-sonnet-4-6} OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4o-mini}
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-https://galymzhan.telecom.quest} ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-https://galymzhan.telecom.quest}
networks: networks:
- traefik_default - traefik_default