Skip to content
Closed
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
138 changes: 135 additions & 3 deletions components/src/dynamo/frontend/sglang_prepost.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,118 @@ def _normalize_sglang_parser_name(parser_name: str | None) -> str | None:
return _SGLANG_PARSER_NAME_ALIASES.get(parser_name, parser_name)


# SGLang ``reasoning_default`` modes this frontend implements, mirroring the
# dispatch in ``serving_chat._get_reasoning_from_request``. A mode outside this
# set is reported rather than silently ignored -- see _sglang_reasoning_default.
_SGLANG_REASONING_MODES = frozenset(
{
"always",
"mistral",
"thinking",
"enable_thinking",
"explicit_thinking",
"explicit_enable_thinking",
}
)


@lru_cache(maxsize=64)
def _sglang_reasoning_default(parser_name: str) -> str | None:
"""Return SGLang's own ``reasoning_default`` for ``parser_name``.

``_THINKING_BY_DEFAULT`` / ``_THINKING_OPT_IN`` below duplicate a table that
SGLang already publishes: every reasoning detector declares a
``reasoning_default`` and ``serving_chat._get_reasoning_from_request`` reads
it. Duplicating it means each new SGLang model silently gets the wrong
answer here until someone remembers to update the sets -- and the failure is
quiet, because an unlisted parser falls through to ``template_default``,
which is ``False`` for any model that ships no Jinja chat template.

Kimi-K3 hit exactly that: ``reasoning_default='thinking'`` (thinking on
unless ``chat_template_kwargs.thinking is False``), but it was in neither
set and has no chat template, so reasoning was never enabled -- the parser
stayed off, ``reasoning_content`` came back null, and the raw
``<|close|>think<|sep|>`` marker leaked into ``content``.

Ask SGLang instead. Returns None when the parser is unknown to this SGLang
build, or declares a mode this dispatch does not implement, in which case
the caller falls back to the static tables.
"""
try:
detector = ReasoningParser(model_type=parser_name).detector
except ValueError:
# The parser is unknown to this SGLang build: ReasoningParser raises
# ValueError("Unsupported model type: ...") for a name outside its
# DetectorMap. That is the expected fallback path, so it is not logged.
# Any other exception is a genuine SGLang failure and must propagate.
return None

# Deliberately a direct attribute access, not getattr(..., None): every
# detector inherits reasoning_default from BaseReasoningFormatDetector, so
# its absence means the detector contract has changed and we want the
# AttributeError rather than a silent fallback.
mode = detector.reasoning_default

if mode not in _SGLANG_REASONING_MODES:
# SGLang knows this parser but declares a mode we do not implement --
# a new mode added upstream. Falling back to the static tables here
# would silently reintroduce exactly the miss this function exists to
# prevent, so say so. lru_cache keeps this to once per parser.
logger.warning(
"sglang reasoning parser %r declares reasoning_default=%r, which "
"this frontend does not implement; falling back to the static "
"thinking tables. Reasoning enablement may be wrong for this model.",
parser_name,
mode,
)
return None
return mode


def _request_reasoning_effort(
request: dict[str, Any], kwargs: dict[str, Any]
) -> str | None:
"""``reasoning_effort`` for this request, top-level winning over kwargs.

SGLang reads only the top-level field; the ``chat_template_kwargs`` fallback
is pre-existing frontend behaviour, kept so the ``mistral`` and ``hunyuan``
gates agree on where the value comes from.
"""
reasoning_effort = request.get("reasoning_effort")
if reasoning_effort is None:
reasoning_effort = kwargs.get("reasoning_effort")
return reasoning_effort


def _force_reasoning_from_sglang_default(
mode: str | None, kwargs: dict[str, Any], request: dict[str, Any]
) -> bool | None:
"""Apply SGLang's ``reasoning_default`` semantics; None if not applicable.

Mirrors the mode dispatch in
``serving_chat._get_reasoning_from_request``. ``mode`` is None when the
parser is unknown to this SGLang build or declares an unimplemented mode;
both mean "fall back to the static tables".
"""
if mode is None:
return None
if mode == "always":
return True
if mode == "mistral":
reasoning_effort = _request_reasoning_effort(request, kwargs)
return reasoning_effort is not None and reasoning_effort != "none"
if mode in ("thinking", "enable_thinking"):
# on by default; the matching kwarg set to False opts out
return kwargs.get(mode) is not False
if mode in ("explicit_thinking", "explicit_enable_thinking"):
toggle = mode.replace("explicit_", "")
return kwargs.get(toggle) is True
# Unreachable: _sglang_reasoning_default only returns a mode in
# _SGLANG_REASONING_MODES, and every member is handled above. Kept so the
# two stay honest if a mode is ever added to the set without a branch here.
raise AssertionError(f"unhandled sglang reasoning_default mode: {mode!r}")


def resolve_request_force_reasoning(
request: dict[str, Any],
reasoning_parser_name: str | None,
Expand All @@ -137,6 +249,13 @@ def resolve_request_force_reasoning(
* opt-in families (``deepseek-v3``/``gemma4``): off by default,
enabled by ``chat_template_kwargs.{thinking,enable_thinking}=True``.
* anything else: follow the statically-detected template default.

The opt-out/opt-in families are resolved from SGLang's own
``reasoning_default`` where this SGLang build exposes the parser, so a model
SGLang knows about does not need to be added to the tables below. The tables
remain as the fallback for parsers this build does not expose. ``minimax-m3``
and ``mistral`` keep their explicit handling above, so their behaviour is
unaffected.
"""
reasoning_parser_name = _normalize_sglang_parser_name(reasoning_parser_name)
if not reasoning_parser_name:
Expand All @@ -150,11 +269,24 @@ def resolve_request_force_reasoning(
return kwargs.get("thinking_mode") != "disabled"

if reasoning_parser_name == "mistral":
reasoning_effort = request.get("reasoning_effort")
if reasoning_effort is None:
reasoning_effort = kwargs.get("reasoning_effort")
reasoning_effort = _request_reasoning_effort(request, kwargs)
return reasoning_effort is not None and reasoning_effort != "none"

if reasoning_parser_name == "hunyuan":
# SGLang special-cases hunyuan ahead of its own reasoning_default
# dispatch, and must be mirrored here for the same reason: the detector
# declares reasoning_default="always", but the Hy3-preview template
# emits no <think> unless reasoning_effort asks for it, so honouring
# "always" would route the entire response into reasoning_content.
reasoning_effort = _request_reasoning_effort(request, kwargs)
return reasoning_effort not in (None, "none", "no_think")

resolved = _force_reasoning_from_sglang_default(
Comment thread
GavinZhu-GMI marked this conversation as resolved.
Comment thread
GavinZhu-GMI marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the repo-pinned sglang[diffusion]==0.5.15, ReasoningParser.DetectorMap does not contain kimi_k3, so this lookup returns None and the reported no-template Kimi-K3 case still falls through to template_default=False. Fix: keep kimi_k3 in the static fallback as a thinking-on-by-default parser until the SGLang dependency is bumped to a version that exposes it.

🤖 AI Fix

In components/src/dynamo/frontend/sglang_prepost.py, add "kimi_k3" to _THINKING_BY_DEFAULT and change the fallback flag_key selection in resolve_request_force_reasoning so both "kimi_k2" and "kimi_k3" use "thinking"; in components/src/dynamo/frontend/tests/test_sglang_reasoning_default.py, add a non-skipped absent-parser fallback test asserting _resolve("kimi_k3", {}, template_default=False) is True and _resolve("kimi_k3", {"thinking": False}, template_default=False) is False.

_sglang_reasoning_default(reasoning_parser_name), kwargs, request
)
if resolved is not None:
return resolved

if reasoning_parser_name in _THINKING_BY_DEFAULT:
flag_key = (
"thinking" if reasoning_parser_name == "kimi_k2" else "enable_thinking"
Expand Down
Loading
Loading