§
    ÷žyj8)  ã                  ó„   — U d Z ddlmZ ddlZddlmZ dZded<   d„  eed	„ ¬
¦  «        D ¦   «         Z	ded<   dd„Z
dd„ZdS )u
  Per-reasoning-model stale-timeout floor for known reasoning models.

Reasoning models (those that emit extended thinking blocks before their
first content token) routinely exceed Hermes's default chat-model
stale detectors:

* Stream stale detector:   ``HERMES_STREAM_STALE_TIMEOUT``     default 180s
                           ``agent/chat_completion_helpers.py:2544``
* Non-stream stale detector: ``HERMES_API_CALL_STALE_TIMEOUT``  default 90s
                           ``run_agent.py:1140``

For NVIDIA Nemotron 3 Ultra on the hosted NIM gateway the empirical
upstream idle kill is ~120s (first-party reproduction at
NVIDIA/NemoClaw#4846 â€” TTFB ~31s, stream dies at 120s). The same
failure mode exists on OpenAI o1/o3, Anthropic Opus 4.x thinking,
DeepSeek R1, Qwen QwQ, xAI Grok reasoning â€” every cloud reasoning
model hits upstream-proxies / load-balancers with idle timeouts
shorter than the model's thinking phase. Result: the stale detector
kills the connection mid-think, surfacing as
``BrokenPipeError``/``RemoteProtocolError`` on the next read.

This module provides a floor that the existing stale-detector scaling
blocks consult via :func:`get_reasoning_stale_timeout_floor` and
apply as ``max(default, floor)``. It is a FLOOR:

* Never overrides explicit user config (``providers.<id>.models.<model>.stale_timeout_seconds``
  or ``request_timeout_seconds`` already wins â€” this code never runs
  in that branch).
* Never lowers an existing threshold.
* Has zero effect on non-reasoning models â€” they are not in the
  allowlist and the resolver returns ``None``.

Matching uses start-anchored regex on the slug-only component of
the model name (after stripping any aggregator prefix like
``openai/``, ``x-ai/``, ``anthropic/``).  The right-anchor matches
end-of-string or a ``-``/``.``/``_`` slug separator, so ``qwen3-235b``
matches the ``qwen3`` family entry (a future model slug would be
``qwen3-235b-instruct`` and would also match) but ``some-other-qwen3``
does NOT match ``qwen3`` (the ``-qwen3`` is not at start of slug).

The ``o1`` case is the most delicate: a model named
``llama-4-70b-o1-preview`` is a hypothetical community derivative that
should NOT trigger the reasoning-model floor for the user (the user
chose a non-OpenAI model, not a reasoning model).  The start-of-slug
anchor naturally excludes this â€” the matched ``o1-preview`` is at
position 11 of the slug, not at position 0.  The previous substring-
with-trailing-hyphen design would have over-matched here, which is
why start-of-slug anchoring is the right shape.

Fixes #52217.
é    )ÚannotationsN)ÚOptional))znemotron-3-ultraéX  )znemotron-3-superr   )znemotron-3-nanoé,  )zdeepseek-r1r   )zdeepseek-reasonerr   )zdeepseek-v4-flashr   )zdeepseek-v4-pror   )zqwq-32br   )Úqwen3é´   )Úo1r   )zo1-minir   )zo1-pror   )z
o1-previewr   )Úo3r   )zo3-pror   )zo3-minir   )zo4-minir   )zclaude-opus-4éð   )zclaude-opus-5r   )zclaude-sonnet-5r   )zclaude-sonnet-4.5r   )zclaude-sonnet-4.6r   )zclaude-fabler   )zgrok-4-fast-reasoningr   )zgrok-4.20-reasoningr   )zgrok-4.5r   )zgrok-4-fast-non-reasoningr   ztuple[tuple[str, int], ...]Ú_REASONING_STALE_TIMEOUT_FLOORSc           
     ór   — g | ]4\  }}||t          j        d t          j        |¦  «        z   dz   ¦  «        f‘Œ5S )ú^z(?:$|[\-._]))ÚreÚcompileÚescape)Ú.0ÚslugÚfloors      ú>/home/ragecks/.hermes/hermes-agent/agent/reasoning_timeouts.pyú
<listcomp>r   ›   sU   € ð Fð Fð Fáˆˆeð 
ˆ5•"”*˜T¥B¤I¨d¡O¤OÑ3°oÑEÑFÔFÐGðFð Fð Fó    c                ó.   — t          | d         ¦  «         S )Nr   )Úlen)Úkvs    r   ú<lambda>r   ž   s   € ½¸RÀ¼U¹¼¸€ r   )Úkeyz(list[tuple[str, float, re.Pattern[str]]]Ú_SORTED_REASONING_FLOORSÚmodel_lowerÚstrÚreturnúOptional[float]c                ón   — t           D ],\  }}}|                     | ¦  «        rt          |¦  «        c S Œ-dS )a5  Return the floor for the first matching slug, else None.

    Each table entry is matched as a start-of-slug prefix with the
    slug-separator-or-end-of-string right-anchor.  Table iteration
    order is irrelevant: longest slug wins (so ``o3-mini`` beats
    ``o3`` on a model like ``openai/o3-mini``).
    N)r   ÚsearchÚfloat)r   Ú_slugr   Úpatterns       r   Ú
_match_anyr'   £   sJ   € õ ":ð  ð  Ñˆˆu�gØ�>Š>˜+Ñ&Ô&ð 	 Ý˜‘<”<ÐÐÐð	 àˆ4r   ÚmodelÚobjectc                óæ   — | rt          | t          ¦  «        sdS |                      ¦   «                              ¦   «         }|sdS d|v r|                     dd¦  «        d         }t          |¦  «        S )u  Return the stale-timeout floor (seconds) for a known reasoning model.

    Returns ``None`` when the model is not in the allowlist or the
    argument is empty / not a string.  Matching uses
    word-boundary-anchored regex on the lowercased model name, so
    ``openai/o3-mini`` matches the ``o3-mini`` slug but
    ``olmo-1`` does NOT match ``o1`` (the ``o1`` substring is not
    at a word boundary inside ``olmo-1``).

    Aggregator prefixes (``openai/``, ``x-ai/``, ``anthropic/`` etc.)
    are preserved through matching â€” the ``/`` is itself a word
    boundary, so ``openai/o3-mini`` matches ``o3-mini`` because the
    ``/`` before ``o3-mini`` satisfies the left-anchor alternation.

    This is a FLOOR â€” callers must apply it as ``max(default, floor)``
    and only when no explicit user-configured per-model
    ``stale_timeout_seconds`` exists.

    >>> get_reasoning_stale_timeout_floor("nvidia/nemotron-3-ultra-550b-a55b")
    600.0
    >>> get_reasoning_stale_timeout_floor("openai/o3-mini")
    300.0
    >>> get_reasoning_stale_timeout_floor("deepseek/deepseek-r1")
    600.0
    >>> get_reasoning_stale_timeout_floor("deepseek/deepseek-v4-flash")
    600.0
    >>> get_reasoning_stale_timeout_floor("deepseek/deepseek-v4-pro")
    600.0
    >>> get_reasoning_stale_timeout_floor("qwen/qwen3-235b-a22b-thinking")
    180.0
    >>> get_reasoning_stale_timeout_floor("x-ai/grok-4-fast-reasoning")
    300.0
    >>> get_reasoning_stale_timeout_floor("anthropic/claude-opus-4-6")
    240.0
    >>> get_reasoning_stale_timeout_floor("anthropic/claude-fable-5")
    600.0
    >>> get_reasoning_stale_timeout_floor("gpt-4o") is None
    True
    >>> get_reasoning_stale_timeout_floor("olmo-1") is None
    True
    >>> get_reasoning_stale_timeout_floor(None) is None
    True
    Nú/é   )Ú
isinstancer   ÚstripÚlowerÚrsplitr'   )r(   Únames     r   Ú!get_reasoning_stale_timeout_floorr2   ±   sz   € ðX ð �
 5­#Ñ.Ô.ð ØˆtØ�;Š;‰=Œ=×ÒÑ Ô €DØð Øˆtð ˆd€{€{Ø�{Š{˜3 Ñ"Ô" 1Ô%ˆÝ�dÑÔÐr   )r   r   r    r!   )r(   r)   r    r!   )Ú__doc__Ú
__future__r   r   Útypingr   r   Ú__annotations__Úsortedr   r'   r2   © r   r   ú<module>r9      së   ðð2ð 2ð 2ðh #Ð "Ð "Ð "Ð "Ð "à 	€	€	€	Ø Ð Ð Ð Ð Ð ð@@Ð ð @ð @ð @ñ @ðzFð Fà�vØ'Ð-CÐ-Cðñ ô ðFñ Fô FÐ ð ð ð ñ ðð ð ð ð6ð 6ð 6ð 6ð 6ð 6r   