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-...
ANTHROPIC_MODEL=claude-sonnet-4-6
ALLOWED_ORIGINS=https://sheetdoc.telecom.quest
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4o-mini
ALLOWED_ORIGINS=https://galymzhan.telecom.quest

View File

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

View File

@ -3,7 +3,7 @@ import json
from pathlib import Path
from typing import Optional
from anthropic import AsyncAnthropic, APIError
from openai import AsyncOpenAI, OpenAIError
from .config import settings
from .errors import LlmFailed
@ -18,20 +18,50 @@ def load_prompt(name: str) -> str:
class LlmClient:
def __init__(self) -> None:
self._client: Optional[AsyncAnthropic] = None
self._client: Optional[AsyncOpenAI] = None
@property
def client(self) -> AsyncAnthropic:
def client(self) -> AsyncOpenAI:
if self._client is None:
if not settings.anthropic_api_key:
if not settings.openai_api_key:
raise LlmFailed(
what="API-ключ не настроен.",
why="ANTHROPIC_API_KEY не задан.",
why="OPENAI_API_KEY не задан.",
next="Задайте переменную окружения и перезапустите сервис.",
)
self._client = AsyncAnthropic(api_key=settings.anthropic_api_key)
self._client = AsyncOpenAI(api_key=settings.openai_api_key)
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(
self,
system: str,
@ -39,31 +69,10 @@ class LlmClient:
max_tokens: int = 2048,
temperature: float = 0.4,
) -> str:
last_err: Optional[Exception] = None
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}")
return await self._chat(system, user, max_tokens, temperature, json_mode=False)
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()
if raw.startswith("```"):
raw = raw.strip("`")

View File

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

View File

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

View File

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