Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions python/sglang/srt/entrypoints/openai/json_schema_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 19 additions & 2 deletions python/sglang/srt/entrypoints/openai/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion python/sglang/srt/function_call/function_call_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
47 changes: 47 additions & 0 deletions test/srt/openai_server/basic/test_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]
Expand Down