diff --git a/python/sglang/srt/entrypoints/openai/json_schema_utils.py b/python/sglang/srt/entrypoints/openai/json_schema_utils.py index 4e8039233b68..759624ac3f2b 100644 --- a/python/sglang/srt/entrypoints/openai/json_schema_utils.py +++ b/python/sglang/srt/entrypoints/openai/json_schema_utils.py @@ -39,6 +39,9 @@ def _enforce_no_additional_properties(schema: Dict[str, Any]) -> None: def normalize_json_schema(schema: Dict[str, Any], strict: bool) -> Dict[str, Any]: """Return a normalized copy of the schema with stricter defaults when requested.""" normalized = copy.deepcopy(schema) + properties = normalized.get("properties") + if isinstance(properties, dict) and "strict" in properties: + properties.pop("strict", None) if strict: _enforce_no_additional_properties(normalized) return normalized diff --git a/python/sglang/srt/entrypoints/openai/protocol.py b/python/sglang/srt/entrypoints/openai/protocol.py index d9025ef8a12e..db2228d63b1d 100644 --- a/python/sglang/srt/entrypoints/openai/protocol.py +++ b/python/sglang/srt/entrypoints/openai/protocol.py @@ -545,8 +545,25 @@ class ChatCompletionRequest(BaseModel): @model_validator(mode="before") @classmethod def set_tool_choice_default(cls, values): - if values.get("tool_choice") is None: - if values.get("tools") is None: + model_name = str(values.get("model") or "").lower() + tools = values.get("tools") + tool_choice = values.get("tool_choice") + + def _is_auto(choice): + return isinstance(choice, str) and choice.lower() == "auto" + + is_kimi_thinking = model_name == "moonshotai/kimi-k2-thinking" + + if ( + is_kimi_thinking + and tools is not None + and (tool_choice is None or _is_auto(tool_choice)) + ): + values["tool_choice"] = "required" + return values + + if tool_choice is None: + if tools is None: values["tool_choice"] = "none" else: values["tool_choice"] = "auto" diff --git a/python/sglang/srt/function_call/function_call_parser.py b/python/sglang/srt/function_call/function_call_parser.py index 0755448e97ad..c93f83fdf1ea 100644 --- a/python/sglang/srt/function_call/function_call_parser.py +++ b/python/sglang/srt/function_call/function_call_parser.py @@ -189,7 +189,7 @@ def get_structure_constraint( if self.detector.supports_structural_tag() and tool_choice == "auto": structural_tag = self.get_structure_tag() return ("structural_tag", structural_tag) - elif isinstance(self.detector, LongCatDetector): + elif isinstance(self.detector, (LongCatDetector, KimiK2Detector)): ebnf = self.get_ebnf(tool_choice) return ("ebnf", ebnf) if ebnf is not None else None elif tool_choice == "required" or isinstance(tool_choice, ToolChoice): diff --git a/test/srt/openai_server/basic/test_protocol.py b/test/srt/openai_server/basic/test_protocol.py index fbf1e3971dc0..602c25fbdd8a 100644 --- a/test/srt/openai_server/basic/test_protocol.py +++ b/test/srt/openai_server/basic/test_protocol.py @@ -190,6 +190,53 @@ def test_chat_completion_tool_choice_validation(self): ) self.assertEqual(request2.tool_choice, "auto") + def test_chat_completion_tool_choice_for_kimi_k2(self): + """Ensure Kimi K2 Thinking forces tool_choice to required when tools are present.""" + messages = [{"role": "user", "content": "Hello"}] + tools = [ + { + "type": "function", + "function": { + "name": "test_func", + "description": "Test function", + }, + } + ] + + request_default = ChatCompletionRequest( + model="moonshotai/Kimi-K2-Thinking", + messages=messages, + tools=tools, + ) + self.assertEqual(request_default.tool_choice, "required") + + request_auto = ChatCompletionRequest( + model="moonshotai/Kimi-K2-Thinking", + messages=messages, + tools=tools, + tool_choice="auto", + ) + self.assertEqual(request_auto.tool_choice, "required") + + request_function = ChatCompletionRequest( + model="moonshotai/Kimi-K2-Thinking", + messages=messages, + tools=tools, + tool_choice={ + "type": "function", + "function": {"name": "test_func"}, + }, + ) + self.assertEqual(request_function.tool_choice.function.name, "test_func") + + request_none = ChatCompletionRequest( + model="moonshotai/Kimi-K2-Thinking", + messages=messages, + tools=tools, + tool_choice="none", + ) + self.assertEqual(request_none.tool_choice, "none") + def test_chat_completion_sglang_extensions(self): """Test chat completion with SGLang extensions""" messages = [{"role": "user", "content": "Hello"}]