hub-gif e60e8170f9 feat(llm): 增加 DeepSeek 纯文本适配器与 DEEPSEEK_*
MA_LLM_TEXT_PROVIDER=deepseek|deep_seek;.env.example 增加 ②-D;单测与 Kimi 同形。

Made-with: Cursor
2026-04-27 11:12:05 +08:00

141 lines
4.6 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.

"""
DeepSeek 官方 OpenAI 兼容 `chat/completions``https://api.deepseek.com`**仅用于纯文本**`call_llm` / 报告 / 策略)。
与 `OPENAI_*` / 配料多模 分离,独立 `DEEPSEEK_*` 凭据。
启用:`MA_LLM_TEXT_PROVIDER=deepseek`(或 `deep_seek`)。
环境变量:见 `.env.example``DEEPSEEK_API_KEY`、基址、模型、上下文字数预检 等)。
"""
from __future__ import annotations
import os
from typing import Any
import requests
from pipeline.openai_gateway.chat_content import normalize_message_content
from pipeline.openai_gateway.estimate import (
estimate_chat_input_tokens as estimate_crawler_style_input_tokens,
)
_DEFAULT_BASE = "https://api.deepseek.com/v1"
_DEFAULT_MODEL = "deepseek-chat"
# 常见 64k 级;若用长上下文/官方调整上限可改 DEEPSEEK_CONTEXT_WINDOW
_DEFAULT_CTX = 64_000
_BUF = 256
_WANT_MAX = 8192
def _read_timeout() -> tuple[float, float]:
read = 600
raw = (
os.environ.get("DEEPSEEK_TIMEOUT")
or os.environ.get("LLM_CHAT_TIMEOUT")
or os.environ.get("OPENAI_TIMEOUT")
or ""
).strip()
if raw:
try:
read = max(60, int(raw))
except ValueError:
pass
conn = 30.0
raw_c = (os.environ.get("LLM_CHAT_CONNECT_TIMEOUT") or "").strip()
if raw_c:
try:
conn = max(5.0, float(raw_c))
except ValueError:
pass
return (conn, float(read))
def _resolve_deepseek_credentials() -> tuple[str, str, str]:
key = (os.environ.get("DEEPSEEK_API_KEY") or "").strip()
if not key:
raise ValueError(
"使用 deepseek 文本适配器需设置 DEEPSEEK_API_KEY"
"与配料/视觉所用 OPENAI_API_KEY 分开配置。"
)
base = (os.environ.get("DEEPSEEK_BASE_URL") or _DEFAULT_BASE).strip().rstrip("/")
model = (
(os.environ.get("DEEPSEEK_TEXT_MODEL") or os.environ.get("DEEPSEEK_MODEL") or _DEFAULT_MODEL)
).strip()
return key, base, model
def _context_window() -> int:
raw = (os.environ.get("DEEPSEEK_CONTEXT_WINDOW") or str(_DEFAULT_CTX)).strip()
try:
return max(4096, int(raw))
except ValueError:
return _DEFAULT_CTX
def _default_temperature() -> float:
return 0.2
class DeepSeekTextLlm:
"""DeepSeek `chat/completions`;与 `KimiMoonshotTextLlm` 同形max_tokens 预检)。"""
def complete_text(
self,
system_prompt: str,
user_prompt: str,
*,
temperature: float | None = None,
) -> str:
api_key, base, model = _resolve_deepseek_credentials()
body: dict[str, Any] = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"temperature": _default_temperature() if temperature is None else float(temperature),
"max_tokens": _WANT_MAX,
}
est = estimate_crawler_style_input_tokens(system_prompt, user_prompt)
context_window = _context_window()
if est >= context_window - _BUF - 256:
raise ValueError(
f"提示词过长(估算输入约 {est} tokensDEEPSEEK_CONTEXT_WINDOW={context_window}"
"请缩小输入或调大 DEEPSEEK_TEXT_MODEL / DEEPSEEK_CONTEXT_WINDOW。"
)
avail = context_window - est - _BUF
want = int(body.get("max_tokens") or _WANT_MAX)
body["max_tokens"] = max(256, min(want, max(avail, 256)))
r = requests.post(
f"{base}/chat/completions",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json=body,
timeout=_read_timeout(),
)
try:
r.raise_for_status()
except requests.HTTPError as e:
snippet = ""
if e.response is not None:
snippet = (e.response.text or "")[:1200].replace("\r\n", "\n").replace("\n", " ")
if snippet:
raise requests.HTTPError(
f"{e!s} | body: {snippet}",
response=e.response,
request=e.request,
) from e
raise
data = r.json()
msg = (data.get("choices") or [{}])[0].get("message") or {}
return normalize_message_content(msg.get("content"))
def estimate_input_tokens(self, system_prompt: str, user_prompt: str) -> int:
return estimate_crawler_style_input_tokens(system_prompt, user_prompt)
def context_window_tokens(self) -> int:
return _context_window()