feat(llm): switch from Anthropic to OpenAI (gpt-4o-mini)
This commit is contained in:
parent
91ecb656e8
commit
72891c7a2d
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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("`")
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user