[Fix] Pass Anthropic thinking history as reasoning_content for custom chat encoders (#35480)

Co-authored-by: Mohammad Angkad <mohammad.angkad@radixark.ai>
This commit is contained in:
Mohammad Miadh Angkad
2026-08-22 02:22:11 +08:00
committed by GitHub
co-authored by Mohammad Angkad
parent 70983bd7db
commit 61c2da42bb
5 changed files with 137 additions and 8 deletions
@@ -368,8 +368,12 @@ class AnthropicServing:
def _convert_assistant_thinking_blocks(
blocks: list[AnthropicContentBlock],
) -> Optional[str]:
"""Re-wrap prior-turn thinking blocks in the parser's own tokens.
) -> tuple[Optional[str], Optional[str]]:
"""Reconstruct prior-turn thinking as ``(reasoning_content, text)``.
At most one is set: encoders that frame the reasoning channel take
it as ``reasoning_content``, everything else gets it re-wrapped and
spliced into content.
``redacted_thinking`` carries encrypted bytes that no local
parser can interpret, so we raise rather than silently drop it.
@@ -387,11 +391,15 @@ class AnthropicServing:
if block.type == "thinking" and block.thinking
]
if not thinking_parts:
return None
return None, None
reasoning_text = "\n".join(thinking_parts)
if self.openai_serving_chat.supports_native_reasoning_history():
return reasoning_text, None
try:
return self.openai_serving_chat.wrap_reasoning_history(
"\n".join(thinking_parts)
return None, self.openai_serving_chat.wrap_reasoning_history(
reasoning_text
)
except ValueError as e:
logger.warning(
@@ -399,7 +407,7 @@ class AnthropicServing:
len(thinking_parts),
e,
)
return None
return None, None
system_parts: list[str] = []
if anthropic_request.system:
@@ -454,7 +462,11 @@ class AnthropicServing:
tool_calls: list[dict] = []
if msg.role == "assistant":
reasoning_history = _convert_assistant_thinking_blocks(msg.content)
reasoning_content, reasoning_history = (
_convert_assistant_thinking_blocks(msg.content)
)
if reasoning_content is not None:
openai_msg["reasoning_content"] = reasoning_content
if reasoning_history is not None:
content_parts.append({"type": "text", "text": reasoning_history})
@@ -113,7 +113,8 @@ def resolve_chat_encoding_spec(
) -> Optional[str]:
"""Return the chat encoding spec for a model.
None means the default path (HF chat template).
None means the default path (HF chat template); any non-None spec also owns
reasoning-history rendering (:func:`spec_owns_reasoning_history`).
"""
if tool_call_parser == "deepseekv4":
return "dsv4"
@@ -142,6 +143,20 @@ def resolve_chat_encoding_spec(
return None
def spec_owns_reasoning_history(spec: Optional[str]) -> bool:
"""Whether the encoder for ``spec`` renders assistant reasoning history itself.
Custom encoders frame the reasoning and content channels, so history must be
passed as assistant ``reasoning_content``. Splicing a detector's markers into
content instead nests a reasoning block inside the content channel and leaves
the real one empty, teaching the model to emit raw markers as visible text.
Answered for the whole family rather than a list of specs, so a new spec gets
the safe default: worst case is dropped history, not a leak.
"""
return spec is not None
def encode_simple_chat(
*,
tokenizer: Any,
@@ -2272,6 +2272,13 @@ class OpenAIServingChat(OpenAIServingBase):
elif self.reasoning_parser == "muse":
request.skip_special_tokens = False
def supports_native_reasoning_history(self) -> bool:
"""Whether the chat encoder takes history as ``reasoning_content`` rather
than via :meth:`wrap_reasoning_history`; see
:func:`chat_encoding.spec_owns_reasoning_history` for why.
"""
return chat_encoding.spec_owns_reasoning_history(self.chat_encoding_spec)
def wrap_reasoning_history(self, reasoning_text: str) -> str:
"""Wrap prior-turn reasoning in the detector's own start/end tokens.