feat(server): enable reasoning_effort on /v1/chat/completions (#2)

* feat(server): enable reasoning_effort on /v1/chat/completions

* fix(server): glm thinking-off mislabeling and reasoning_effort mapping edge cases
This commit is contained in:
Xiaoze Fan
2026-08-14 00:02:32 -07:00
committed by GitHub
parent c28e93a8c0
commit 4f510f1ba6
7 changed files with 122 additions and 18 deletions
+1
View File
@@ -77,6 +77,7 @@ class ChatCompletionRequest(BaseModel):
presence_penalty: float = 0.0
frequency_penalty: float = 0.0
chat_template_kwargs: dict[str, Any] = Field(default_factory=dict)
reasoning_effort: str | None = None
ignore_eos: bool = False
tools: list[Tool] | None = None
tool_choice: Literal["none", "auto", "required"] | ToolChoiceObject | None = None
+4
View File
@@ -317,6 +317,10 @@ def _make_reasoning_parser(spec: GenSpec, state: Any) -> ReasoningParser | None:
# enable_thinking is explicitly false, so the model emits only the closing
# </think>. Mirror that default here, else the chain-of-thought leaks into content.
force_reasoning = (spec.chat_template_kwargs or {}).get("enable_thinking") is not False
elif parser_name == "glm":
# GLM's template honors enable_thinking (default on) even with tools; the
# generic fallback would force thinking and mislabel disabled output as reasoning.
force_reasoning = (spec.chat_template_kwargs or {}).get("enable_thinking") is not False
elif parser_name == "gemma4":
# Gemma4 defaults thinking off even when tools are present: its template injects an
# empty thought channel before generation. Do not let Codex tool definitions make all
+26
View File
@@ -67,6 +67,32 @@ def think_toggle_kwargs(reasoning_parser: str | None, enabled: bool) -> dict:
return think_chat_template_kwargs(reasoning_parser, gear)
_THINKING_KWARG_KEYS = ("enable_thinking", "thinking", "thinking_mode", "reasoning_effort")
_DISABLE_EFFORTS = ("none", "off")
def effort_toggle_kwargs(
reasoning_parser: str | None,
effort: str | None,
chat_template_kwargs: dict | None,
) -> dict:
"""Fold a protocol-level reasoning-effort request into the template kwargs.
An explicit thinking-related key wins wholesale; unrelated extras ride along.
Effort "none"/"off" (case-insensitive) disables thinking; any other or absent
effort enables it, forwarded for templates that grade it (gpt-oss)."""
ctk = dict(chat_template_kwargs or {})
if any(key in ctk for key in _THINKING_KWARG_KEYS):
return ctk
if isinstance(effort, str):
effort = effort.strip().lower()
disabled = effort in _DISABLE_EFFORTS
mapped = dict(think_toggle_kwargs(reasoning_parser, not disabled))
if effort and not disabled:
mapped.setdefault("reasoning_effort", effort)
mapped.update(ctk)
return mapped
def moe_total_experts(config: Any) -> int:
"""Total routed-expert slots the model has: experts per layer x MoE layers. Matches the
engine's own basis (``Engine._resolve_auto_moe_cache_size``), so a residency rate derived
+17 -4
View File
@@ -37,8 +37,17 @@ from .generation import (
)
def chat_request_to_genspec(req: ChatCompletionRequest, model_sampling: dict[str, Any]) -> GenSpec:
def chat_request_to_genspec(
req: ChatCompletionRequest,
model_sampling: dict[str, Any],
reasoning_parser: str | None = None,
) -> GenSpec:
"""OpenAI ChatCompletionRequest -> GenSpec (the OpenAI 'to_sampling_params')."""
from .model_meta import effort_toggle_kwargs
ctk = req.chat_template_kwargs
if req.reasoning_effort:
ctk = effort_toggle_kwargs(reasoning_parser, req.reasoning_effort, ctk)
return GenSpec(
messages=render_messages([m.model_dump(exclude_none=True) for m in req.messages]),
sampling_params=resolve_sampling(
@@ -50,7 +59,7 @@ def chat_request_to_genspec(req: ChatCompletionRequest, model_sampling: dict[str
model_sampling=model_sampling,
stop=req.stop,
),
chat_template_kwargs=req.chat_template_kwargs,
chat_template_kwargs=ctk,
template_tools=_tools_for_template(req),
parser_tools=(_all_tool_dicts(req.tools) if _should_parse_tools(req) else None),
)
@@ -133,7 +142,9 @@ async def handle_chat_completion(
return create_error_response("Only n=1 is supported", param="n")
try:
spec = chat_request_to_genspec(req, model_sampling)
spec = chat_request_to_genspec(
req, model_sampling, reasoning_parser=getattr(state.config, "reasoning_parser", None)
)
except ValueError as exc:
return create_error_response(str(exc))
uid = await submit_generation(spec, state)
@@ -182,7 +193,9 @@ async def stream_chat_completion_chunks(
) -> AsyncIterator[bytes]:
"""Format generate_events() into the OpenAI chat.completion.chunk SSE stream."""
if spec is None:
spec = chat_request_to_genspec(req, {})
spec = chat_request_to_genspec(
req, {}, reasoning_parser=getattr(state.config, "reasoning_parser", None)
)
yield _sse(
_chat_chunk(
req,
+3 -11
View File
@@ -230,19 +230,11 @@ def convert_responses_to_genspec(
else:
template_tools, parser_tools = split_tool_lists(raw_tools, selected)
# Thinking toggle: an explicit chat_template_kwargs extra field wins; else
# the protocol-native `reasoning` object drives the template through the
# per-family mapping in model_meta. effort "none" disables thinking (vLLM
# semantics); other efforts enable it and are forwarded for templates that
# grade them (gpt-oss).
from .model_meta import think_toggle_kwargs
from .model_meta import effort_toggle_kwargs
ctk = dict(getattr(req, "chat_template_kwargs", None) or {})
if req.reasoning and not ctk:
effort = req.reasoning.get("effort")
ctk = dict(think_toggle_kwargs(reasoning_parser, effort != "none"))
if effort and effort != "none":
ctk.setdefault("reasoning_effort", effort)
if req.reasoning:
ctk = effort_toggle_kwargs(reasoning_parser, req.reasoning.get("effort"), ctk)
return GenSpec(
messages=render_messages(messages),