From bab609b2d42c4722d51260a6c2192bbf16ab9139 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Fri, 4 Sep 2026 12:50:50 +0800 Subject: [PATCH] =?UTF-8?q?fix(ai):=20DeepSeek=20thinking=20=E7=A6=81?= =?UTF-8?q?=E7=94=A8=E5=8F=82=E6=95=B0=E8=A2=AB=20400=20=E6=8B=92=E7=BB=9D?= =?UTF-8?q?=E6=97=B6=E5=8E=BB=E5=8F=82=E9=87=8D=E8=AF=95=20(#240=20?= =?UTF-8?q?=E8=B7=9F=E8=BF=9B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _openai_kwargs 对 DeepSeek V4 官方域名注入的 thinking 禁用参数 (extra_body) 属外部 API 契约, 模型/接口版本差异可能报 400。照 temperature / reasoning_effort 既有模式补第三条定向回退: 400 且 错误信息指向 thinking 时移除 extra_body 重建请求 (回退默认思考 模式; 若正文因此被推理挤占, 由 _iter_openai_text 显式报错指引)。 与 thinking 无关的 400 不触发回退, 直接抛出。 --- backend/app/services/ai_provider.py | 16 +++++++++++++++ backend/tests/test_ai_provider.py | 30 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/backend/app/services/ai_provider.py b/backend/app/services/ai_provider.py index e7aae8c..1e71bb0 100644 --- a/backend/app/services/ai_provider.py +++ b/backend/app/services/ai_provider.py @@ -504,6 +504,7 @@ def _openai_client(api_key: str, timeout: float): # 只在 400 明确指出对应参数时移除该参数并重试; 每个参数最多移除一次。 _TEMP_REJECT_HINTS = ("temperature", "only 1 is allowed") _REASONING_EFFORT_REJECT_HINTS = ("reasoning_effort", "reasoning effort") +_THINKING_BODY_REJECT_HINTS = ("thinking",) def _is_temperature_rejected(exc: Exception) -> bool: @@ -526,6 +527,16 @@ def _is_reasoning_effort_rejected(exc: Exception) -> bool: ) +def _is_thinking_body_rejected(exc: Exception) -> bool: + """True if the upstream 400 specifically rejects the thinking extra_body.""" + if getattr(exc, "status_code", None) != 400: + return False + text = _openai_error_detail(exc) or str(exc) + return _openai_error_param(exc) == "thinking" or any( + h in text.lower() for h in _THINKING_BODY_REJECT_HINTS + ) + + def _openai_error_param(exc: Exception) -> str: body = getattr(exc, "body", None) if not isinstance(body, dict): @@ -545,6 +556,11 @@ def _openai_retry_kwargs(exc: Exception, kwargs: dict) -> dict | None: if "reasoning_effort" in retry_kwargs and _is_reasoning_effort_rejected(exc): retry_kwargs.pop("reasoning_effort") return retry_kwargs + if "extra_body" in retry_kwargs and _is_thinking_body_rejected(exc): + # DeepSeek thinking 禁用参数被拒 (模型/API 版本差异): 回退默认思考模式 + # 重试; 报告若因此被推理挤占正文, 由 _iter_openai_text 显式报错。 + retry_kwargs.pop("extra_body") + return retry_kwargs return None diff --git a/backend/tests/test_ai_provider.py b/backend/tests/test_ai_provider.py index 7995e56..98eb8fe 100644 --- a/backend/tests/test_ai_provider.py +++ b/backend/tests/test_ai_provider.py @@ -198,6 +198,36 @@ def test_is_temperature_rejected_false_for_other_400(): assert _is_temperature_rejected(exc) is False +def test_thinking_body_rejected_falls_back_to_default_mode(): + """DeepSeek thinking 禁用参数被 400 拒绝时, 去参重试而非直接失败。""" + response = httpx.Response( + 400, + json={"error": {"message": "unknown parameter: thinking"}}, + request=httpx.Request("POST", "https://api.deepseek.com/v1/chat/completions"), + ) + exc = openai.BadRequestError( + "bad request", response=response, + body={"error": {"message": "unknown parameter: thinking"}}, + ) + kwargs = {"max_tokens": None, "extra_body": {"thinking": {"type": "disabled"}}} + retry = ai_provider._openai_retry_kwargs(exc, kwargs) + assert retry == {"max_tokens": None} + # 原 kwargs 不被修改 + assert kwargs["extra_body"] == {"thinking": {"type": "disabled"}} + + # 与 thinking 无关的 400 不触发该回退 + response_other = httpx.Response( + 400, + json={"error": {"message": "model not found"}}, + request=httpx.Request("POST", "https://api.deepseek.com/v1/chat/completions"), + ) + exc_other = openai.BadRequestError( + "bad request", response=response_other, + body={"error": {"message": "model not found"}}, + ) + assert ai_provider._openai_retry_kwargs(exc_other, kwargs) is None + + def test_is_temperature_rejected_false_for_non_400(): response = httpx.Response( 401,