[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
@@ -28,6 +28,8 @@ register_cpu_ci(est_time=1, suite="base-a-test-cpu")
class _FakeOpenAIServingChat:
native_reasoning_history = False
def __init__(self, stream_lines=None, chat_template=None):
self.stream_lines = stream_lines or []
self.apply_reasoning_calls: list[bool] = []
@@ -35,6 +37,9 @@ class _FakeOpenAIServingChat:
tokenizer=SimpleNamespace(chat_template=chat_template)
)
def supports_native_reasoning_history(self):
return self.native_reasoning_history
def _generate_chat_stream(self, adapted_request, processed_request, raw_request):
async def _gen():
for line in self.stream_lines:
@@ -960,6 +965,63 @@ class TestAnthropicServing(unittest.TestCase):
self.assertIn("ponder", joined)
self.assertNotIn("<think>\nponder\n</think>\nponder", joined)
def test_assistant_thinking_history_uses_native_reasoning_content(self):
"""Channel-framing encoders take thinking history as ``reasoning_content``.
Splicing the detector's markers into content would nest a reasoning block
inside the content channel and leave the real one empty, training the
model to emit raw markers as visible text.
"""
class _NativeOpenAI(_FakeOpenAIServingChat):
native_reasoning_history = True
def wrap_reasoning_history(self, text):
raise AssertionError("must not rewrap for a native-history encoder")
serving = AnthropicServing(_NativeOpenAI())
request = self._anthropic_request(
stream=False,
messages=[
{"role": "user", "content": "hi"},
{
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "ponder"},
{"type": "text", "text": "hello"},
],
},
{"role": "user", "content": "again"},
],
)
chat_request = serving._convert_to_chat_completion_request(request)
assistant_msg = next(m for m in chat_request.messages if m.role == "assistant")
self.assertEqual(assistant_msg.reasoning_content, "ponder")
self.assertEqual(assistant_msg.content, "hello")
def test_thinking_only_turn_keeps_native_reasoning_content(self):
"""An assistant turn that is only thinking still carries its reasoning."""
class _NativeOpenAI(_FakeOpenAIServingChat):
native_reasoning_history = True
serving = AnthropicServing(_NativeOpenAI())
request = self._anthropic_request(
stream=False,
messages=[
{"role": "user", "content": "hi"},
{
"role": "assistant",
"content": [{"type": "thinking", "thinking": "ponder"}],
},
{"role": "user", "content": "again"},
],
)
chat_request = serving._convert_to_chat_completion_request(request)
assistant_msg = next(m for m in chat_request.messages if m.role == "assistant")
self.assertEqual(assistant_msg.reasoning_content, "ponder")
self.assertEqual(assistant_msg.content, "")
def test_redacted_thinking_history_is_rejected(self):
"""``redacted_thinking`` cannot be rendered by local parsers."""
serving = self._serving()
@@ -11,6 +11,7 @@ from sglang.test.test_utils import maybe_stub_sgl_kernel
maybe_stub_sgl_kernel() # must precede any import that pulls in sgl_kernel
import json
import re
import tempfile
import unittest
import uuid
@@ -21,6 +22,7 @@ from unittest.mock import Mock, patch
from fastapi import Request
from sglang.srt.entrypoints.openai import chat_encoding
from sglang.srt.entrypoints.openai.chat_encoding import (
resolve_dsv4_reasoning_effort_profile,
)
@@ -43,6 +45,9 @@ from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=11, suite="base-a-test-cpu")
# Every spec resolve_chat_encoding_spec can return; pinned by the guard below.
_ALL_CHAT_ENCODING_SPECS = ("dsv4", "dsv32", "inkling", "kimi_k3")
def _spec_result(index):
return {
@@ -1924,6 +1929,34 @@ class ServingChatTestCase(unittest.TestCase):
serving_chat = OpenAIServingChat(tm, TemplateManager())
self.assertEqual(serving_chat.chat_encoding_spec, "kimi_k3")
def test_custom_encoders_own_reasoning_history(self):
"""Every custom encoding spec takes reasoning history natively.
The alternative splices a detector's markers into content, which an
encoder that frames its own channels turns into visible raw markers.
A new spec must not silently default to that path.
"""
for spec in _ALL_CHAT_ENCODING_SPECS:
with self.subTest(chat_encoding_spec=spec):
self.chat.chat_encoding_spec = spec
self.assertTrue(self.chat.supports_native_reasoning_history())
# The HF chat-template path keeps the wrap-into-content behaviour.
self.chat.chat_encoding_spec = None
self.assertFalse(self.chat.supports_native_reasoning_history())
def test_all_chat_encoding_specs_are_enumerated(self):
"""Guard the spec list this file asserts capabilities over."""
source = Path(chat_encoding.__file__).read_text()
returned = set(
re.findall(
r'^\s+return "(\w+)"$',
source[source.index("def resolve_chat_encoding_spec") :],
re.MULTILINE,
)
)
self.assertEqual(returned, set(_ALL_CHAT_ENCODING_SPECS))
# ------------- dsv4 task + latest_reminder -------------
def test_dsv4_task_field_schema(self):
"""Top-level `task` accepts the 6 DS task tokens and rejects others."""