MiniMax M2.x reasoning models emit reasoning_content blocks before their first content token (#17924). During extended thinking phases, they routinely exceed the default 180s chat-model stale-stream timeout, causing the stale-stream detector to kill the connection mid-think. This adds minimax-m2 to _REASONING_STALE_TIMEOUT_FLOORS with a 300s floor — generous enough to cover the documented 240s stall (test_streaming.py:1270-1278) with margin. Fixes #62353 (cherry picked from commit d66b2a75dee78f583edb75b699ba055ffcb93013)
83 lines
4.4 KiB
Python
83 lines
4.4 KiB
Python
"""Per-model stale-timeout FLOOR for known reasoning models.
|
|
|
|
Reasoning models routinely exceed the default chat-model stale detectors (stream 180s,
|
|
non-stream 90s): upstream proxies idle-kill the stream mid-think, surfacing as
|
|
``BrokenPipeError``/``RemoteProtocolError``. The stale-detector scaling applies
|
|
``max(default, floor)`` from :func:`get_reasoning_stale_timeout_floor`, so this never
|
|
overrides explicit per-model ``stale_timeout_seconds``/``request_timeout_seconds`` (that
|
|
branch never calls it), never lowers a threshold, and is ``None`` for non-allowlisted models.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Optional
|
|
|
|
|
|
# floor_seconds -> slugs. Order irrelevant — longest slug wins at match time.
|
|
_REASONING_STALE_TIMEOUT_FLOORS: dict[int, tuple[str, ...]] = {
|
|
600: (
|
|
# NVIDIA Nemotron behind hosted NIM: documented 60-180s upstream idle kill.
|
|
"nemotron-3-ultra", "nemotron-3-super",
|
|
# DeepSeek R1 / V4 (reasoning_content streamed before final content).
|
|
# ``deepseek-flash`` is the version-less canonical Flash id (2026-09 Flash refresh);
|
|
# ``deepseek-v4-flash`` still aliases onto it server-side.
|
|
"deepseek-r1", "deepseek-reasoner", "deepseek-flash", "deepseek-v4-flash", "deepseek-v4.1-flash", "deepseek-v4-pro",
|
|
# OpenAI o-series: each variant enumerated so bare ``o1`` cannot over-match ``olmo-1``.
|
|
"o1", "o1-mini", "o1-pro", "o1-preview", "o3", "o3-pro",
|
|
# OpenAI named reasoning lines (gpt-5.6-sol/-terra/-luna, gpt-6-astra, their -pro/-900k
|
|
# variants): minutes-long thinking at xhigh/max/ultra; sub-10k-token requests sit below the
|
|
# Codex context-size floor, so this is their only protection. Anchored so gpt-5.5 and the
|
|
# gpt-4.x / gpt-5.1-chat lines keep the effort-tier defaults (#112909).
|
|
"gpt-5.6", "gpt-6",
|
|
# Mythos-class named models (claude-fable-5): 1M ctx + 128K output, a heavier thinking
|
|
# phase than the numbered line — otherwise the stale detector trips the circuit breaker.
|
|
"claude-fable",
|
|
),
|
|
300: (
|
|
"nemotron-3-nano", "nemotron-3.5-lightning", "qwq-32b", "o3-mini", "o4-mini",
|
|
# xAI Grok: explicit reasoning pairs only, so bare ``grok-3``/``grok-4`` fast variants
|
|
# don't inherit the floor.
|
|
"grok-4-fast-reasoning", "grok-4.20-reasoning", "grok-4.5", "grok-4.6",
|
|
# "Ox Alpha" stealth reasoning model (OpenRouter / OpenCode Zen slugs); Thinking
|
|
# Machines Inkling (covers inkling-small and :free SKUs).
|
|
"ox-alpha", "x-preview-f-free", "inkling",
|
|
# MiniMax M2.x (m2.5/m2.7): reasoning_content before first content token; 240s
|
|
# mid-think stalls observed (#62353).
|
|
"minimax-m2",
|
|
),
|
|
# Anthropic Claude 4.x+ thinking variants (anchored so 3.x never matches).
|
|
240: ("claude-opus-4", "claude-opus-5"),
|
|
# qwen3 family: instruct variants also match — a slightly longer wait on a hung provider
|
|
# beats a pattern (``qwen3-.*-thinking``) that breaks on the next naming shape.
|
|
180: ("qwen3", "claude-sonnet-5", "claude-sonnet-4.5", "claude-sonnet-4.6", "grok-4-fast-non-reasoning"),
|
|
}
|
|
|
|
|
|
# Pre-compiled once at import (immutable afterwards — safe under free-threaded Python).
|
|
# Right anchor: end-of-string or a slug separator; ``:`` because OpenRouter routing suffixes
|
|
# (``:free``, ``:nitro``) attach directly to the slug. Longest-first so ``o3-mini`` beats ``o3``.
|
|
_SORTED_REASONING_FLOORS: list[tuple[str, float, re.Pattern[str]]] = [
|
|
(slug, floor, re.compile(r"^" + re.escape(slug) + r"(?:$|[\-._:])"))
|
|
for slug, floor in sorted(
|
|
((slug, floor) for floor, slugs in _REASONING_STALE_TIMEOUT_FLOORS.items() for slug in slugs),
|
|
key=lambda kv: -len(kv[0]),
|
|
)
|
|
]
|
|
|
|
|
|
def get_reasoning_stale_timeout_floor(model: object) -> Optional[float]:
|
|
"""Stale-timeout floor (seconds) for a known reasoning model, else ``None``.
|
|
|
|
The aggregator prefix (up to the last ``/``) is stripped and the slug matched
|
|
start-anchored with an end-or-separator right anchor, so ``qwen3-235b`` matches ``qwen3``
|
|
but ``some-other-qwen3`` and ``llama-4-70b-o1-preview`` do not.
|
|
"""
|
|
if not model or not isinstance(model, str):
|
|
return None
|
|
name = model.strip().lower().rsplit("/", 1)[-1]
|
|
for _slug, floor, pattern in _SORTED_REASONING_FLOORS:
|
|
if pattern.search(name):
|
|
return float(floor)
|
|
return None
|