first commit
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""通过配置接入 OpenAI 兼容的大模型接口,不记录密钥或聊天正文。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlsplit
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
class LLMError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class LLMClient:
|
||||
def __init__(self, options):
|
||||
if not isinstance(options, dict):
|
||||
raise ValueError("大模型配置必须是 JSON 对象")
|
||||
base_url = options.get("base_url", "https://api.deepseek.com")
|
||||
if not isinstance(base_url, str):
|
||||
raise ValueError("大模型 base_url 必须是 HTTPS 地址字符串")
|
||||
self.base_url = base_url.rstrip("/")
|
||||
parsed = urlsplit(self.base_url)
|
||||
if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password or parsed.query or parsed.fragment:
|
||||
raise ValueError("大模型 base_url 应为不含认证信息的 HTTPS 地址")
|
||||
self.model = options.get("model", "deepseek-flash")
|
||||
self._api_key = options.get("api_key", "")
|
||||
if not isinstance(self.model, str) or not self.model.strip():
|
||||
raise ValueError("大模型 model 不能为空")
|
||||
if not isinstance(self._api_key, str) or not self._api_key.strip():
|
||||
raise ValueError("请在大模型配置文件中填写 api_key")
|
||||
self.timeout = options.get("timeout_seconds", 20)
|
||||
self.max_tokens = options.get("max_tokens", 512)
|
||||
if type(self.timeout) not in (int, float) or not math.isfinite(self.timeout) or not 1 <= self.timeout <= 60:
|
||||
raise ValueError("timeout_seconds 应为 1–60 秒")
|
||||
if type(self.max_tokens) is not int or not 128 <= self.max_tokens <= 4096:
|
||||
raise ValueError("max_tokens 应为 128–4096 的整数")
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path):
|
||||
return cls(json.loads(Path(path).read_text(encoding="utf-8-sig")))
|
||||
|
||||
def complete_json(self, system, user):
|
||||
payload = {"model": self.model, "messages": [{"role": "system", "content": system},
|
||||
{"role": "user", "content": user}],
|
||||
"response_format": {"type": "json_object"}, "max_tokens": self.max_tokens,
|
||||
"thinking": {"type": "disabled"}, "stream": False}
|
||||
request = Request(self.base_url + "/chat/completions",
|
||||
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
||||
headers={"Authorization": "Bearer " + self._api_key,
|
||||
"Content-Type": "application/json"}, method="POST")
|
||||
try:
|
||||
with urlopen(request, timeout=self.timeout) as response:
|
||||
raw = response.read(1024 * 1024 + 1)
|
||||
if len(raw) > 1024 * 1024:
|
||||
raise LLMError("大模型响应过大")
|
||||
result = json.loads(raw)
|
||||
choice = result["choices"][0]
|
||||
if choice.get("finish_reason") == "length":
|
||||
raise LLMError("大模型响应被截断,请增加 max_tokens")
|
||||
content = choice["message"]["content"]
|
||||
if not isinstance(content, str) or not content.strip():
|
||||
raise LLMError("大模型返回了空结果")
|
||||
data = json.loads(content)
|
||||
if not isinstance(data, dict):
|
||||
raise LLMError("大模型结果应为 JSON 对象")
|
||||
return data
|
||||
except HTTPError as error:
|
||||
# 不输出响应正文或请求头,避免服务器错误回显密钥。
|
||||
raise LLMError(f"大模型接口 HTTP {error.code};请核对密钥、余额和模型配置") from None
|
||||
except (URLError, TimeoutError, OSError):
|
||||
raise LLMError("大模型连接失败或超时,请稍后再试") from None
|
||||
except (ValueError, KeyError, IndexError, TypeError, AttributeError):
|
||||
raise LLMError("大模型响应格式不正确,请重试") from None
|
||||
Reference in New Issue
Block a user