mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-24 17:21:40 +08:00
feat(llm): DeepSeek 适配器默认开启思考模式
请求体含 thinking+reasoning_effort,思考时不下发 temperature;未指定模型时开思考用 deepseek-v4-pro。DEEPSEEK_THINKING=0 可关。 Made-with: Cursor
This commit is contained in:
parent
e60e8170f9
commit
7823e8d6f8
@ -54,7 +54,11 @@ MA_LLM_TEXT_PROVIDER=crawler_openai_compatible
|
|||||||
# MA_LLM_TEXT_PROVIDER=deepseek
|
# MA_LLM_TEXT_PROVIDER=deepseek
|
||||||
# DEEPSEEK_API_KEY=sk-...
|
# DEEPSEEK_API_KEY=sk-...
|
||||||
# DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
|
# DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
|
||||||
# DEEPSEEK_TEXT_MODEL=deepseek-chat
|
# 思考模式(官方 Thinking Mode,默认开):关=0;力度 high|max(https://api-docs.deepseek.com/guides/thinking_mode)
|
||||||
|
# DEEPSEEK_THINKING=1
|
||||||
|
# DEEPSEEK_REASONING_EFFORT=high
|
||||||
|
# 未填 DEEPSEEK_TEXT_MODEL 时:开思考默认 deepseek-v4-pro,关思考默认 deepseek-chat
|
||||||
|
# DEEPSEEK_TEXT_MODEL=deepseek-v4-pro
|
||||||
# DEEPSEEK_CONTEXT_WINDOW=64000
|
# DEEPSEEK_CONTEXT_WINDOW=64000
|
||||||
# 别名:DEEPSEEK_MODEL
|
# 别名:DEEPSEEK_MODEL
|
||||||
# DEEPSEEK_TIMEOUT=600
|
# DEEPSEEK_TIMEOUT=600
|
||||||
|
|||||||
@ -5,7 +5,10 @@ DeepSeek 官方 OpenAI 兼容 `chat/completions`(`https://api.deepseek.com`)
|
|||||||
|
|
||||||
启用:`MA_LLM_TEXT_PROVIDER=deepseek`(或 `deep_seek`)。
|
启用:`MA_LLM_TEXT_PROVIDER=deepseek`(或 `deep_seek`)。
|
||||||
|
|
||||||
环境变量:见 `.env.example`(`DEEPSEEK_API_KEY`、基址、模型、上下文字数预检 等)。
|
**思考模式**(与官方「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
|
from __future__ import annotations
|
||||||
|
|
||||||
@ -20,7 +23,8 @@ from pipeline.openai_gateway.estimate import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
_DEFAULT_BASE = "https://api.deepseek.com/v1"
|
_DEFAULT_BASE = "https://api.deepseek.com/v1"
|
||||||
_DEFAULT_MODEL = "deepseek-chat"
|
_DEFAULT_MODEL_NO_THINK = "deepseek-chat"
|
||||||
|
_DEFAULT_MODEL_THINK = "deepseek-v4-pro"
|
||||||
# 常见 64k 级;若用长上下文/官方调整上限可改 DEEPSEEK_CONTEXT_WINDOW
|
# 常见 64k 级;若用长上下文/官方调整上限可改 DEEPSEEK_CONTEXT_WINDOW
|
||||||
_DEFAULT_CTX = 64_000
|
_DEFAULT_CTX = 64_000
|
||||||
|
|
||||||
@ -51,6 +55,28 @@ def _read_timeout() -> tuple[float, float]:
|
|||||||
return (conn, float(read))
|
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]:
|
def _resolve_deepseek_credentials() -> tuple[str, str, str]:
|
||||||
key = (os.environ.get("DEEPSEEK_API_KEY") or "").strip()
|
key = (os.environ.get("DEEPSEEK_API_KEY") or "").strip()
|
||||||
if not key:
|
if not key:
|
||||||
@ -59,9 +85,9 @@ def _resolve_deepseek_credentials() -> tuple[str, str, str]:
|
|||||||
"与配料/视觉所用 OPENAI_API_KEY 分开配置。"
|
"与配料/视觉所用 OPENAI_API_KEY 分开配置。"
|
||||||
)
|
)
|
||||||
base = (os.environ.get("DEEPSEEK_BASE_URL") or _DEFAULT_BASE).strip().rstrip("/")
|
base = (os.environ.get("DEEPSEEK_BASE_URL") or _DEFAULT_BASE).strip().rstrip("/")
|
||||||
model = (
|
model = (os.environ.get("DEEPSEEK_TEXT_MODEL") or os.environ.get("DEEPSEEK_MODEL") or "").strip()
|
||||||
(os.environ.get("DEEPSEEK_TEXT_MODEL") or os.environ.get("DEEPSEEK_MODEL") or _DEFAULT_MODEL)
|
if not model:
|
||||||
).strip()
|
model = _default_model()
|
||||||
return key, base, model
|
return key, base, model
|
||||||
|
|
||||||
|
|
||||||
@ -88,15 +114,21 @@ class DeepSeekTextLlm:
|
|||||||
temperature: float | None = None,
|
temperature: float | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
api_key, base, model = _resolve_deepseek_credentials()
|
api_key, base, model = _resolve_deepseek_credentials()
|
||||||
|
think = _thinking_enabled()
|
||||||
body: dict[str, Any] = {
|
body: dict[str, Any] = {
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "system", "content": system_prompt},
|
{"role": "system", "content": system_prompt},
|
||||||
{"role": "user", "content": user_prompt},
|
{"role": "user", "content": user_prompt},
|
||||||
],
|
],
|
||||||
"temperature": _default_temperature() if temperature is None else float(temperature),
|
|
||||||
"max_tokens": _WANT_MAX,
|
"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)
|
est = estimate_crawler_style_input_tokens(system_prompt, user_prompt)
|
||||||
context_window = _context_window()
|
context_window = _context_window()
|
||||||
if est >= context_window - _BUF - 256:
|
if est >= context_window - _BUF - 256:
|
||||||
|
|||||||
@ -65,8 +65,13 @@ def test_kimi_complete_text_uses_post_and_returns_content(monkeypatch: pytest.Mo
|
|||||||
assert out == "kimi_out"
|
assert out == "kimi_out"
|
||||||
|
|
||||||
|
|
||||||
def test_deepseek_complete_text_uses_post_and_returns_content(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_deepseek_complete_text_uses_post_and_thinking_by_default(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-ds-test")
|
monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-ds-test")
|
||||||
|
# 默认开思考,未显式设 DEEPSEEK_TEXT_MODEL 时用 deepseek-v4-pro
|
||||||
|
monkeypatch.delenv("DEEPSEEK_TEXT_MODEL", raising=False)
|
||||||
|
monkeypatch.delenv("DEEPSEEK_MODEL", raising=False)
|
||||||
|
|
||||||
def _fake_post(
|
def _fake_post(
|
||||||
url: str,
|
url: str,
|
||||||
@ -78,6 +83,10 @@ def test_deepseek_complete_text_uses_post_and_returns_content(monkeypatch: pytes
|
|||||||
assert "/chat/completions" in url
|
assert "/chat/completions" in url
|
||||||
assert json["messages"][0]["role"] == "system"
|
assert json["messages"][0]["role"] == "system"
|
||||||
assert headers.get("Authorization", "").startswith("Bearer sk-ds-")
|
assert headers.get("Authorization", "").startswith("Bearer sk-ds-")
|
||||||
|
assert json.get("model") == "deepseek-v4-pro"
|
||||||
|
assert json.get("thinking") == {"type": "enabled"}
|
||||||
|
assert json.get("reasoning_effort") == "high"
|
||||||
|
assert "temperature" not in json
|
||||||
r = MagicMock()
|
r = MagicMock()
|
||||||
r.json.return_value = {"choices": [{"message": {"content": "ds_out"}}]}
|
r.json.return_value = {"choices": [{"message": {"content": "ds_out"}}]}
|
||||||
r.raise_for_status = MagicMock()
|
r.raise_for_status = MagicMock()
|
||||||
@ -92,6 +101,34 @@ def test_deepseek_complete_text_uses_post_and_returns_content(monkeypatch: pytes
|
|||||||
assert out == "ds_out"
|
assert out == "ds_out"
|
||||||
|
|
||||||
|
|
||||||
|
def test_deepseek_thinking_off_sends_temperature(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-ds-test")
|
||||||
|
monkeypatch.setenv("DEEPSEEK_THINKING", "0")
|
||||||
|
monkeypatch.setenv("DEEPSEEK_TEXT_MODEL", "deepseek-chat")
|
||||||
|
|
||||||
|
def _fake_post(
|
||||||
|
url: str,
|
||||||
|
headers: dict,
|
||||||
|
json: dict,
|
||||||
|
timeout: object,
|
||||||
|
) -> MagicMock:
|
||||||
|
assert json.get("model") == "deepseek-chat"
|
||||||
|
assert "thinking" not in json
|
||||||
|
assert "reasoning_effort" not in json
|
||||||
|
assert "temperature" in json
|
||||||
|
r = MagicMock()
|
||||||
|
r.json.return_value = {"choices": [{"message": {"content": "plain"}}]}
|
||||||
|
r.raise_for_status = MagicMock()
|
||||||
|
return r
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"pipeline.llm.providers.adapters.deepseek_text.requests.post",
|
||||||
|
side_effect=_fake_post,
|
||||||
|
):
|
||||||
|
out = DeepSeekTextLlm().complete_text("S", "U", temperature=0.0)
|
||||||
|
assert out == "plain"
|
||||||
|
|
||||||
|
|
||||||
def test_factory_selects_deepseek_when_env_set(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_factory_selects_deepseek_when_env_set(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
reset_text_llm_client_for_tests()
|
reset_text_llm_client_for_tests()
|
||||||
monkeypatch.setenv("MA_LLM_TEXT_PROVIDER", "deepseek")
|
monkeypatch.setenv("MA_LLM_TEXT_PROVIDER", "deepseek")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user