-
Notifications
You must be signed in to change notification settings - Fork 1.6k
fix(frontend): derive reasoning default from SGLang instead of a local table #12350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
|
@@ -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: | ||
|
|
@@ -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( | ||
|
GavinZhu-GMI marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the repo-pinned 🤖 AI FixIn |
||
| _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" | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.