fix(ai): DeepSeek thinking 禁用参数被 400 拒绝时去参重试 (#240 跟进)

_openai_kwargs 对 DeepSeek V4 官方域名注入的 thinking 禁用参数
(extra_body) 属外部 API 契约, 模型/接口版本差异可能报 400。照
temperature / reasoning_effort 既有模式补第三条定向回退: 400 且
错误信息指向 thinking 时移除 extra_body 重建请求 (回退默认思考
模式; 若正文因此被推理挤占, 由 _iter_openai_text 显式报错指引)。

与 thinking 无关的 400 不触发回退, 直接抛出。
This commit is contained in:
shy3130
2026-09-04 12:50:50 +08:00
parent 3fee4ecba5
commit bab609b2d4
2 changed files with 46 additions and 0 deletions
+16
View File
@@ -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
+30
View File
@@ -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,