Files
item_bank/app/services/llm/client.py
T
2026-08-01 16:50:55 +08:00

104 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""统一的 OpenAI 兼容 LLM client:文本 + 多模态。
配置来自用户的 UserSettingsbase_url / api_key / 模型名)。api_key 在库里
是 Fernet 加密的,这里解密后使用。
"""
import base64
from dataclasses import dataclass
from openai import AsyncOpenAI
from app.models.user import UserSettings
from app.services.crypto import decrypt
class LLMConfigError(RuntimeError):
"""用户尚未正确配置 AI 接口。"""
@dataclass
class LLMResult:
content: str
model: str | None
prompt_tokens: int | None
completion_tokens: int | None
class LLMClient:
def __init__(self, settings: UserSettings):
if not settings or not settings.llm_base_url or not settings.llm_api_key_encrypted:
raise LLMConfigError("请先在设置里配置 AI 接口(base_url 与 api_key")
api_key = decrypt(settings.llm_api_key_encrypted)
if not api_key:
raise LLMConfigError("api_key 解密失败,请在设置里重新填写")
self._client = AsyncOpenAI(base_url=settings.llm_base_url, api_key=api_key)
self._text_model = settings.llm_text_model
self._vision_model = settings.llm_vision_model or settings.llm_text_model
async def chat_text(
self,
system: str,
user: str,
*,
json_mode: bool = False,
model: str | None = None,
) -> LLMResult:
m = model or self._text_model
if not m:
raise LLMConfigError("未配置文本模型名")
kwargs = {}
if json_mode:
kwargs["response_format"] = {"type": "json_object"}
resp = await self._client.chat.completions.create(
model=m,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user},
],
**kwargs,
)
return self._to_result(resp, m)
async def chat_vision(
self,
system: str,
user_text: str,
image_bytes: bytes,
mime: str = "image/jpeg",
*,
json_mode: bool = False,
model: str | None = None,
) -> LLMResult:
m = model or self._vision_model
if not m:
raise LLMConfigError("未配置多模态模型名")
data_url = f"data:{mime};base64,{base64.b64encode(image_bytes).decode()}"
kwargs = {}
if json_mode:
kwargs["response_format"] = {"type": "json_object"}
resp = await self._client.chat.completions.create(
model=m,
messages=[
{"role": "system", "content": system},
{
"role": "user",
"content": [
{"type": "text", "text": user_text},
{"type": "image_url", "image_url": {"url": data_url}},
],
},
],
**kwargs,
)
return self._to_result(resp, m)
@staticmethod
def _to_result(resp, model: str) -> LLMResult:
usage = getattr(resp, "usage", None)
return LLMResult(
content=resp.choices[0].message.content or "",
model=model,
prompt_tokens=getattr(usage, "prompt_tokens", None),
completion_tokens=getattr(usage, "completion_tokens", None),
)