hub-gif 7823e8d6f8 feat(llm): DeepSeek 适配器默认开启思考模式
请求体含 thinking+reasoning_effort,思考时不下发 temperature;未指定模型时开思考用 deepseek-v4-pro。DEEPSEEK_THINKING=0 可关。

Made-with: Cursor
2026-04-27 11:14:38 +08:00

173 lines
5.9 KiB
Python
Raw Permalink 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`)。
**思考模式**与官方「Thinking Mode」一致默认可通过 `DEEPSEEK_THINKING=0` 关闭。开启时在请求中携带
`thinking.type=enabled` 与 `reasoning_effort`;不发送 `temperature`(官方在思考模式下忽略采样参数)。
未指定 `DEEPSEEK_TEXT_MODEL` 时,开启思考默认用 `deepseek-v4-pro`,关闭时默认 `deepseek-chat`。
详见 https://api-docs.deepseek.com/guides/thinking_mode
"""
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_NO_THINK = "deepseek-chat"
_DEFAULT_MODEL_THINK = "deepseek-v4-pro"
# 常见 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 _thinking_enabled() -> bool:
v = (os.environ.get("DEEPSEEK_THINKING") or "1").strip().lower()
if v in ("0", "false", "off", "no", "disabled"):
return False
return True
def _reasoning_effort() -> str:
raw = (os.environ.get("DEEPSEEK_REASONING_EFFORT") or "high").strip().lower()
if raw in ("max", "high", "low", "medium", "xhigh"):
if raw in ("low", "medium"):
return "high"
if raw == "xhigh":
return "max"
return raw
return "high"
def _default_model() -> str:
return _DEFAULT_MODEL_THINK if _thinking_enabled() else _DEFAULT_MODEL_NO_THINK
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 "").strip()
if not model:
model = _default_model()
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()
think = _thinking_enabled()
body: dict[str, Any] = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"max_tokens": _WANT_MAX,
}
if think:
# 与 OpenAI 官方 Python SDK 合并 extra_body 后一致:思考模式不依赖 temperature
body["thinking"] = {"type": "enabled"}
body["reasoning_effort"] = _reasoning_effort()
else:
body["temperature"] = _default_temperature() if temperature is None else float(temperature)
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()