This commit is contained in:
@@ -449,59 +449,6 @@ class OpenAICompatProvider(LLMProvider):
|
||||
clean["content"] = self._coerce_content_to_string(clean.get("content"))
|
||||
return self._enforce_role_alternation(sanitized)
|
||||
|
||||
def _drop_deepseek_incomplete_reasoning_history(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
model_name: str,
|
||||
reasoning_effort: str | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
if (
|
||||
not self._spec
|
||||
or self._spec.name != "deepseek"
|
||||
):
|
||||
return messages
|
||||
|
||||
semantic_effort = reasoning_effort.lower() if isinstance(reasoning_effort, str) else None
|
||||
if semantic_effort in {"none", "minimal", "minimum"}:
|
||||
return messages
|
||||
|
||||
# DeepSeek-V4 can require reasoning_content even when the config did
|
||||
# not explicitly request reasoning_effort. Keep that implicit-thinking
|
||||
# cleanup scoped to known thinking-capable DeepSeek models so normal
|
||||
# deepseek-chat history is not trimmed.
|
||||
if semantic_effort is None:
|
||||
model_lower = model_name.lower()
|
||||
if not any(token in model_lower for token in ("deepseek-v4", "deepseek-reasoner")):
|
||||
return messages
|
||||
|
||||
bad_idx = None
|
||||
for idx, msg in enumerate(messages):
|
||||
if (
|
||||
msg.get("role") == "assistant"
|
||||
and msg.get("tool_calls")
|
||||
and not msg.get("reasoning_content")
|
||||
):
|
||||
bad_idx = idx
|
||||
if bad_idx is None:
|
||||
return messages
|
||||
|
||||
keep_from = None
|
||||
for idx in range(bad_idx + 1, len(messages)):
|
||||
if messages[idx].get("role") == "user":
|
||||
keep_from = idx
|
||||
break
|
||||
|
||||
if keep_from is None:
|
||||
trimmed = messages[:bad_idx]
|
||||
else:
|
||||
prefix = [msg for msg in messages[:keep_from] if msg.get("role") == "system"]
|
||||
trimmed = prefix + messages[keep_from:]
|
||||
logger.warning(
|
||||
"Dropped {} DeepSeek thinking history message(s) with incomplete reasoning_content",
|
||||
len(messages) - len(trimmed),
|
||||
)
|
||||
return trimmed
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Build kwargs
|
||||
# ------------------------------------------------------------------
|
||||
@@ -542,11 +489,6 @@ class OpenAICompatProvider(LLMProvider):
|
||||
if spec and spec.strip_model_prefix:
|
||||
model_name = model_name.split("/")[-1]
|
||||
|
||||
messages = self._drop_deepseek_incomplete_reasoning_history(
|
||||
messages,
|
||||
model_name,
|
||||
reasoning_effort,
|
||||
)
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": model_name,
|
||||
"messages": self._sanitize_messages(self._sanitize_empty_content(messages)),
|
||||
@@ -611,22 +553,22 @@ class OpenAICompatProvider(LLMProvider):
|
||||
kwargs["tools"] = tools
|
||||
kwargs["tool_choice"] = tool_choice or "auto"
|
||||
|
||||
# Backfill reasoning_content on legacy assistant messages.
|
||||
# DeepSeek V4 (and potentially others) rejects thinking-mode
|
||||
# requests that contain assistant messages without reasoning_content
|
||||
# — even on turns that had no tool calls. This happens when a
|
||||
# session was started with a non-thinking model or without
|
||||
# reasoning_effort, then the user switches thinking mode on
|
||||
# mid-session. Injecting an empty string satisfies the API
|
||||
# without altering semantics (the model treats it as "no
|
||||
# thinking happened on that turn").
|
||||
thinking_active = (
|
||||
(spec and spec.thinking_style and reasoning_effort is not None
|
||||
and semantic_effort not in ("none", "minimal"))
|
||||
or (reasoning_effort is not None and _is_kimi_thinking_model(model_name)
|
||||
and semantic_effort not in ("none", "minimal"))
|
||||
# Backfill reasoning_content="" on assistants missing it: DeepSeek
|
||||
# thinking mode rejects history otherwise (#3554, #3584); "" reads
|
||||
# as "no thinking that turn". DeepSeek-V4/reasoner reason natively,
|
||||
# so backfill even without explicit reasoning_effort.
|
||||
explicit_thinking = (
|
||||
reasoning_effort is not None
|
||||
and semantic_effort not in ("none", "minimal")
|
||||
and ((spec and spec.thinking_style) or _is_kimi_thinking_model(model_name))
|
||||
)
|
||||
if thinking_active:
|
||||
implicit_deepseek_thinking = (
|
||||
spec is not None
|
||||
and spec.name == "deepseek"
|
||||
and semantic_effort not in ("none", "minimal", "minimum")
|
||||
and any(t in model_name.lower() for t in ("deepseek-v4", "deepseek-reasoner"))
|
||||
)
|
||||
if explicit_thinking or implicit_deepseek_thinking:
|
||||
for msg in kwargs["messages"]:
|
||||
if msg.get("role") == "assistant" and "reasoning_content" not in msg:
|
||||
msg["reasoning_content"] = ""
|
||||
|
||||
Reference in New Issue
Block a user