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-...
|
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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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("`")
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user