[Model] Add native IFM K2 Horizon serving support (#37654)

Co-authored-by: Xiaoyu Zhang <1182563586@qq.com>
Co-authored-by: Xinyuan Tong <xinyuantong.cs@gmail.com>
This commit is contained in:
Yash Akhauri
2026-09-03 16:39:43 +08:00
committed by GitHub
co-authored by Xiaoyu Zhang Xinyuan Tong
parent 02d9b3060a
commit 3bac084d4e
34 changed files with 4487 additions and 16 deletions
@@ -43,6 +43,9 @@ from sglang.srt.parser.jinja_template_utils import (
jinja_template_may_reorder_tool_results,
)
from sglang.srt.parser.template_detection import ReasoningToggleConfig
from sglang.srt.sampling.sampling_params import (
REQUEST_REASONING_END_TOKEN_IDS_KEY,
)
from sglang.srt.utils import get_or_create_event_loop
from sglang.test.ci.ci_register import register_cpu_ci
@@ -794,6 +797,33 @@ class ServingChatTestCase(unittest.TestCase):
self.assertEqual(req.reasoning_effort, "high")
def test_k2_selected_terminator_reaches_sampling_params(self):
self.tm._config_overrides["reasoning_parser"] = "k2_horizon"
self.chat = OpenAIServingChat(self.tm, self.template_manager)
self.template_manager.chat_template_name = None
self.template_manager.jinja_template_content_format = "string"
self.tm.tokenizer.apply_chat_template.return_value = [1, 2, 3]
self.tm.tokenizer.encode.side_effect = lambda text, **_: (
[8, 9] if text == "</ifm|think_fast>" else [1, 2, 3]
)
req = ChatCompletionRequest(
model="IFM/K2-Horizon-7B",
messages=[{"role": "user", "content": "hi"}],
chat_template_kwargs={"reasoning_effort": "medium"},
)
processed = self.chat._process_messages(req, is_multimodal=False)
self.assertEqual(processed.reasoning_end_token_ids, [8, 9])
with patch.object(self.chat, "_process_messages", return_value=processed):
adapted, _ = self.chat._convert_to_internal_request(req)
self.assertEqual(
adapted.sampling_params["custom_params"][
REQUEST_REASONING_END_TOKEN_IDS_KEY
],
[8, 9],
)
def test_kimi_tool_call_keeps_template_default_thinking(self):
self.template_manager.chat_template_name = None
self.template_manager.jinja_template_content_format = "string"
@@ -23,6 +23,9 @@ from sglang.srt.entrypoints.openai.serving_responses import (
)
from sglang.srt.function_call.core_types import ToolCallItem
from sglang.srt.parser.template_detection import ReasoningToggleConfig
from sglang.srt.sampling.sampling_params import (
REQUEST_REASONING_END_TOKEN_IDS_KEY,
)
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
@@ -246,6 +249,38 @@ class ChatToolForwardingTestCase(CustomTestCase):
self.assertEqual(request_prompts, [[4, 5, 6]])
self.assertEqual(engine_prompts, [[4, 5, 6]])
def test_k2_output_parser_reuses_effective_template_default(self):
serving = make_serving()
serving.reasoning_parser = "k2_horizon"
serving.default_chat_template_kwargs = {"reasoning_effort": "low"}
serving.template_manager.chat_template_name = None
serving.tokenizer_manager.tokenizer.apply_chat_template.return_value = [4, 5, 6]
request = ResponsesRequest(
model="IFM/K2-Horizon-7B",
input="hi",
# Template kwargs are the final render inputs, so the server default
# below takes precedence over this API convenience field.
reasoning={"effort": "medium"},
store=False,
)
asyncio.run(
serving._make_request(request, None, serving.tokenizer_manager.tokenizer)
)
render_call = serving.tokenizer_manager.tokenizer.apply_chat_template.call_args
self.assertEqual(render_call.kwargs["reasoning_effort"], "low")
self.assertEqual(request.chat_template_kwargs["reasoning_effort"], "low")
output_items = serving._make_response_output_items(
request,
"work</ifm|think_faster>\nanswer",
tokenizer=Mock(),
require_reasoning=True,
)
self.assertEqual(output_items[0].content[0].text, "work")
self.assertEqual(output_items[1].content[0].text, "\nanswer")
class ReasoningRequestForwardingTestCase(unittest.TestCase):
def test_create_responses_uses_processed_reasoning_state(self):
@@ -263,6 +298,7 @@ class ReasoningRequestForwardingTestCase(unittest.TestCase):
video_data=None,
modalities=[],
stop=[],
reasoning_end_token_ids=[41, 42],
)
captured = {}
@@ -308,6 +344,12 @@ class ReasoningRequestForwardingTestCase(unittest.TestCase):
self.assertEqual(response.status, "completed")
self.assertFalse(captured["adapted_request"].require_reasoning)
self.assertEqual(
captured["adapted_request"].sampling_params["custom_params"][
REQUEST_REASONING_END_TOKEN_IDS_KEY
],
[41, 42],
)
self.assertFalse(parser_cls.call_args.kwargs["force_reasoning"])
@@ -32,6 +32,37 @@ class NonHarmonyStreamTestCase(CustomTestCase):
self.assertTrue(parser_cls.call_args.kwargs["force_reasoning"])
def test_k2_nested_effort_selects_streaming_reasoning_delimiter(self):
serving = make_serving()
serving.reasoning_parser = "k2_horizon"
serving.tool_call_parser = None
request = ResponsesRequest(
model="IFM/K2-Horizon-7B",
input="hi",
reasoning={"effort": "medium"},
stream=True,
store=False,
)
events = StreamFixture(serving, request, require_reasoning=True).run(
[engine_chunk("work</ifm|think_fast>\nanswer", 4, finish=True)]
)
types = event_types(events)
payloads = event_payloads(events)
reasoning = "".join(
payload["delta"]
for event_type, payload in zip(types, payloads)
if event_type == "response.reasoning_text.delta"
)
answer = "".join(
payload["delta"]
for event_type, payload in zip(types, payloads)
if event_type == "response.output_text.delta"
)
self.assertEqual(reasoning, "work")
self.assertEqual(answer, "\nanswer")
def test_emits_typed_sse_events_in_order(self):
serving = make_serving()
serving.reasoning_parser = None