From 136ba9fe41cf2b6c65fe33885c909cc495700af5 Mon Sep 17 00:00:00 2001 From: Jiajun Li Date: Wed, 13 May 2026 11:09:12 -0700 Subject: [PATCH] fix raw id --- .../srt/entrypoints/openai/serving_chat.py | 17 +++++++++----- python/sglang/srt/function_call/core_types.py | 6 +++++ .../srt/function_call/function_call_parser.py | 6 ++++- .../srt/function_call/kimik2_detector.py | 22 +++++++++++++++++++ 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/python/sglang/srt/entrypoints/openai/serving_chat.py b/python/sglang/srt/entrypoints/openai/serving_chat.py index f236a26cd2bd..fb5d5d6f67d7 100644 --- a/python/sglang/srt/entrypoints/openai/serving_chat.py +++ b/python/sglang/srt/entrypoints/openai/serving_chat.py @@ -1138,11 +1138,7 @@ def _process_tool_call_id( history_tool_calls_cnt: int, ) -> str: """Process for generating a new and unique `tool_call_id`""" - if self.tool_call_parser != "kimi_k2": - # A simple uuid is sufficient for all models except for Kimi-K2. - tool_call_id = f"call_{uuid.uuid4().hex[:24]}" - return tool_call_id - else: + if self.tool_call_parser == "kimi_k2": # Align with Kimi-K2 format: functions.{name}:{index} # Kimi-K2 allows multiple tool_calls in one message; SGLang sets call_item.tool_index to the *local* position inside that message. # Therefore, the index must be corrected by using `history_tool_calls_cnt + call_item.tool_index` to ensure globally unique and properly ordered. @@ -1151,6 +1147,17 @@ def _process_tool_call_id( f"Process tool call idx, parser: {self.tool_call_parser}, tool_call_id: {tool_call_id}, history_cnt: {history_tool_calls_cnt}" ) return tool_call_id + if self.tool_call_parser == "kimi_k2_raw_id": + # RL training needs the model-emitted tool_call_id round-tripped verbatim, + # so we skip the history-based renumbering above and return whatever the + # detector captured. Fall back to the canonical Kimi-K2 reconstruction + # (without history offset) if for any reason the detector did not record + # a raw id — the raw id field is best-effort but the format is stable. + if call_item.tool_call_id: + return call_item.tool_call_id + return f"functions.{call_item.name}:{call_item.tool_index}" + # A simple uuid is sufficient for all other models. + return f"call_{uuid.uuid4().hex[:24]}" def _process_tool_calls( self, diff --git a/python/sglang/srt/function_call/core_types.py b/python/sglang/srt/function_call/core_types.py index 1ea87df798c8..297dd2712ef3 100644 --- a/python/sglang/srt/function_call/core_types.py +++ b/python/sglang/srt/function_call/core_types.py @@ -10,6 +10,12 @@ class ToolCallItem(BaseModel): tool_index: int name: Optional[str] = None parameters: str # JSON string + # The tool_call_id string emitted by the model, captured verbatim. + # Only populated by detectors whose downstream consumers need the exact + # model-emitted id (e.g. RL training trajectories). Existing detectors + # leave this as None and the serving layer falls back to its usual id + # generation strategy. + tool_call_id: Optional[str] = None class StreamingParseResult(BaseModel): diff --git a/python/sglang/srt/function_call/function_call_parser.py b/python/sglang/srt/function_call/function_call_parser.py index ca066e196d0f..ca8abc570434 100644 --- a/python/sglang/srt/function_call/function_call_parser.py +++ b/python/sglang/srt/function_call/function_call_parser.py @@ -20,7 +20,10 @@ from sglang.srt.function_call.gpt_oss_detector import GptOssDetector from sglang.srt.function_call.hermes_detector import HermesDetector from sglang.srt.function_call.internlm_detector import InternlmDetector -from sglang.srt.function_call.kimik2_detector import KimiK2Detector +from sglang.srt.function_call.kimik2_detector import ( + KimiK2Detector, + KimiK2RawIdDetector, +) from sglang.srt.function_call.lfm2_detector import Lfm2Detector from sglang.srt.function_call.llama32_detector import Llama32Detector from sglang.srt.function_call.mimo_detector import MiMoDetector @@ -54,6 +57,7 @@ class FunctionCallParser: "glm47": Glm47MoeDetector, "gpt-oss": GptOssDetector, "kimi_k2": KimiK2Detector, + "kimi_k2_raw_id": KimiK2RawIdDetector, "lfm2": Lfm2Detector, "llama3": Llama32Detector, "mimo": MiMoDetector, diff --git a/python/sglang/srt/function_call/kimik2_detector.py b/python/sglang/srt/function_call/kimik2_detector.py index 21cf46cb0103..747fdcdb9ed8 100644 --- a/python/sglang/srt/function_call/kimik2_detector.py +++ b/python/sglang/srt/function_call/kimik2_detector.py @@ -113,6 +113,7 @@ def detect_and_parse(self, text: str, tools: List[Tool]) -> StreamingParseResult tool_index=function_idx, name=function_name, parameters=function_args, + tool_call_id=function_id, ) ) @@ -177,6 +178,7 @@ def parse_streaming_increment( tool_index=self.current_tool_id, name=function_name, parameters="", + tool_call_id=function_id, ) ) self.current_tool_name_sent = True @@ -253,3 +255,23 @@ def get_info(name: str) -> StructureInfo: ) return get_info + + +class KimiK2RawIdDetector(KimiK2Detector): + """ + Variant of KimiK2Detector that preserves the model-emitted tool_call_id verbatim. + + The default kimi_k2 path renumbers ids via `history_tool_calls_cnt + tool_index` + in the serving layer so that multi-turn conversations get globally unique, + monotonically increasing ids (see PR #10600). That is the right behavior for + chat use cases. + + RL training has the opposite requirement: the trajectory must round-trip the + exact tool_call_id the model produced (e.g. `functions.foo:5`), so that the + follow-up tool result turn references the same id the policy emitted. This + subclass exists purely as a marker so the serving layer can branch on the + parser name and use `ToolCallItem.tool_call_id` directly. Parsing logic + is identical to KimiK2Detector. + """ + + pass