
    sjT                    z   U d Z ddlmZ ddlZddlmZ i ddddd	dd
dddddddddddddddddddddddddddd dd!Zd"ed#<    eh d$          Z	d%ed&<    edh          Z
d%ed'<    eh d(          Zd%ed)<    ed*h          Zd%ed+<    eh d,          Zd%ed-<    edh          Zd%ed.<    edh          Zd%ed/<    eh d0          Zd%ed1<    ed2d3h          Zd%ed4<    ed5d6h          Zd%ed7<    ej        d8          ZdKd<ZdKd=ZdKd>ZdLd@ZdMdBZdNdDZdKdEZdOdGZdPdHZdQdJZdS )Ru  Per-provider model name normalization.

Different LLM providers expect model identifiers in different formats:

- **Aggregators** (OpenRouter, Nous, AI Gateway, Kilo Code) need
  ``vendor/model`` slugs like ``anthropic/claude-sonnet-4.6``.
- **Anthropic** native API expects bare names with dots replaced by
  hyphens: ``claude-sonnet-4-6``.
- **Copilot** expects bare names *with* dots preserved:
  ``claude-sonnet-4.6``.
- **OpenCode Zen** preserves dots for GPT/GLM/Gemini/Kimi/MiniMax-style
  model IDs, but Claude still uses hyphenated native names like
  ``claude-sonnet-4-6``.
- **OpenCode Go** preserves dots in model names: ``minimax-m2.7``.
- **DeepSeek** accepts only the first-class V-series IDs
  (``deepseek-v4-pro``, ``deepseek-v4-flash``, and any future
  ``deepseek-v<N>-*``).  The legacy aliases ``deepseek-chat`` and
  ``deepseek-reasoner`` were retired on 2026-07-24 and are remapped to
  ``deepseek-v4-flash`` (official non-thinking / thinking shims).  Older
  Hermes revisions folded every non-reasoner input into
  ``deepseek-chat``, which on aggregators routes to V3 — so a user
  picking V4 Pro was silently downgraded.
- **Custom** and remaining providers pass the name through as-is.

This module centralises that translation so callers can simply write::

    api_model = normalize_model_for_provider(user_input, provider)

Inspired by Clawdbot's ``normalizeAnthropicModelId`` pattern.
    )annotationsN)Optionalclaude	anthropicgptopenaio1o3o4geminigooglegemmadeepseekglmzz-aikimi
moonshotaiminimaxgrokzx-aiqwenmimoxiaomitrinityzarcee-ainemotronnvidiallamaz
meta-llamastepfun)stepr   zdict[str, str]_VENDOR_PREFIXES>   
ai-gatewaynouskilocode
openrouterzfrozenset[str]_AGGREGATOR_PROVIDERS_DOT_TO_HYPHEN_PROVIDERS>   openai-codexcopilotcopilot-acp_STRIP_VENDOR_ONLY_PROVIDERShuggingface_AUTHORITATIVE_NATIVE_PROVIDERS>   
minimax-cn
qwen-oauthkimi-codingollama-cloudminimax-oauthkimi-coding-cnxaizaiarceecustomr   r   alibabar    _MATCHING_PREFIX_STRIP_PROVIDERS"_CATALOGUE_PREFIX_REPAIR_PROVIDERS_LOWERCASE_MODEL_PROVIDERS>   r1cotthinkreasoner	reasoning_DEEPSEEK_REASONER_KEYWORDSzdeepseek-chatzdeepseek-reasoner_DEEPSEEK_RETIRED_ALIASESzdeepseek-v4-prodeepseek-v4-flash_DEEPSEEK_CANONICAL_MODELSz^deepseek-v\d+([-.].+)?$
model_namestrreturnc                    t          |                                           }|t          v rdS |t          v r|S t                              |          r|S t          D ]	}||v r dS 
dS )a  Map a model input to a DeepSeek-accepted identifier.

    Rules:
    - Retired aliases ``deepseek-chat`` / ``deepseek-reasoner`` (cut off
      2026-07-24) -> ``deepseek-v4-flash``.
    - Already a known canonical (``deepseek-v4-pro``/``deepseek-v4-flash``)
      -> pass through.
    - Matches the V-series pattern ``deepseek-v<digit>...`` -> pass through
      (covers future ``deepseek-v5-*`` and dated variants without a release).
    - Contains a reasoner keyword (r1, think, reasoning, cot, reasoner)
      -> ``deepseek-v4-flash``.
    - Everything else -> ``deepseek-v4-flash``.

    Args:
        model_name: The bare model name (vendor prefix already stripped).

    Returns:
        A DeepSeek-accepted model identifier.
    r@   )_strip_vendor_prefixlowerr?   rA   _DEEPSEEK_V_SERIES_REmatchr>   )rB   barekeywords      >/home/agent/.hermes/hermes-agent/hermes_cli/model_normalize.py_normalize_for_deepseekrM      s    (  
++1133D (((""))) ""4((  / ' 'd??&&&      c                F    d| v r|                      dd          d         S | S )a@  Remove a ``vendor/`` prefix if present.

    Examples::

        >>> _strip_vendor_prefix("anthropic/claude-sonnet-4.6")
        'claude-sonnet-4.6'
        >>> _strip_vendor_prefix("claude-sonnet-4.6")
        'claude-sonnet-4.6'
        >>> _strip_vendor_prefix("meta-llama/llama-4-scout")
        'llama-4-scout'
    /   )splitrB   s    rL   rF   rF      s0     jQ''**rN   c                .    |                      dd          S )zReplace dots with hyphens in a model name.

    Anthropic's native API uses hyphens where marketing names use dots:
    ``claude-sonnet-4.6`` -> ``claude-sonnet-4-6``.
    .-)replacerS   s    rL   _dots_to_hyphensrX      s     c3'''rN   provider_namec                    | pd                                                                 }|s|S 	 ddlm}  ||          S # t          $ r |cY S w xY w)z2Resolve provider aliases to Hermes' canonical ids. r   )normalize_provider)striprG   hermes_cli.modelsr\   	Exception)rY   rawr\   s      rL   _normalize_provider_aliasra      s}    B
%
%
'
'
-
-
/
/C 
888888!!#&&&   


s   ? AAtarget_providerc                   d| vr| S |                      dd          \  }}|                                r|                                s| S t          |          }|dk    r@|                                                                dk    r|                                S | S t          |          }|r||k    r|                                S | S )a
  Strip ``provider/`` only when the prefix matches the target provider.

    This prevents arbitrary slash-bearing model IDs from being mangled on
    native providers while still repairing manual config values like
    ``zai/glm-5.1`` for the ``zai`` provider.

    ``custom`` is a generic bucket for arbitrary user-defined endpoints, not
    a vendor identity like ``zai``/``gemini``/``xai``. An alias that merely
    *resolves to* ``custom`` (e.g. ``ollama``, via ``_PROVIDER_ALIASES``)
    does not mean a ``ollama/`` prefix is redundant -- it may be the actual
    routing prefix a proxy in front of the custom endpoint (e.g. LiteLLM)
    requires, as in ``ollama/glm-5.2``. Only a literal ``custom/`` prefix --
    the bucket's own name -- is treated as redundant here.
    rP   rQ   r4   )rR   r]   ra   rG   )rB   rb   prefix	remaindernormalized_targetnormalized_prefixs         rL   _strip_matching_provider_prefixrh      s     *"((a00FI<<>> !2!2 1/BBH$$<<>>!!X--??$$$1&99 !.2CCC   rN   Optional[str]c                   |                                  }|sdS d|v r0|                    dd          d                                         pdS |                                }|                    d          d         }|t          v rt          |         S t                                          D ]\  }}|                    |          r|c S dS )a*  Detect the vendor slug from a bare model name.

    Uses the first hyphen-delimited token of the model name to look up
    the corresponding vendor in ``_VENDOR_PREFIXES``.  Also handles
    case-insensitive matching and special patterns.

    Args:
        model_name: A model name, optionally already including a
            ``vendor/`` prefix.  If a prefix is present it is used
            directly.

    Returns:
        The vendor slug (e.g. ``"anthropic"``, ``"openai"``) or ``None``
        if no vendor can be confidently detected.

    Examples::

        >>> detect_vendor("claude-sonnet-4.6")
        'anthropic'
        >>> detect_vendor("gpt-5.4-mini")
        'openai'
        >>> detect_vendor("anthropic/claude-sonnet-4.6")
        'anthropic'
        >>> detect_vendor("my-custom-model")
    NrP   rQ   r   rV   )r]   rR   rG   r   items
startswith)rB   name
name_lowerfirst_tokenrd   vendors         rL   detect_vendorrq   "  s    4 D t d{{zz#q!!!$**,,44J ""3''*K&&&,, +0022    (( 	MMM	 4rN   c                B    d| v r| S t          |           }|r| d|  S | S )aN  Prepend the detected ``vendor/`` prefix if missing.

    Used for aggregator providers that require ``vendor/model`` format.
    If the name already contains a ``/``, it is returned as-is.
    If no vendor can be detected, the name is returned unchanged
    (aggregators may still accept it or return an error).

    Examples::

        >>> _prepend_vendor("claude-sonnet-4.6")
        'anthropic/claude-sonnet-4.6'
        >>> _prepend_vendor("anthropic/claude-sonnet-4.6")
        'anthropic/claude-sonnet-4.6'
        >>> _prepend_vendor("my-custom-thing")
        'my-custom-thing'
    rP   )rq   )rB   rp   s     rL   _prepend_vendorrs   T  sD    " j:&&F ('':'''rN   providerc                ,   d| v r| S 	 ddl m} n# t          $ r | cY S w xY w|                    |          pg }|                                                                 fd|D             }t          |          dk    r|                                S | S )u  Restore a dropped ``vendor/`` prefix using the provider's catalogue.

    Unlike :func:`_prepend_vendor`, this never guesses from the model's name
    shape — it only repairs a bare id that matches **exactly one** curated
    entry for this provider modulo the prefix. That keeps self-hosted models
    behind the same provider id (local NIM containers, proxies) untouched,
    since they aren't in the catalogue.

    Examples::

        >>> _repair_prefix_from_catalogue("nemotron-3-ultra-550b-a55b", "nvidia")
        'nvidia/nemotron-3-ultra-550b-a55b'
        >>> _repair_prefix_from_catalogue("my-local-nim", "nvidia")
        'my-local-nim'
    rP   r   )_PROVIDER_MODELSc                    h | ]L}d |v |                     d d          d                                                                         k    J|MS )rP   rQ   )rR   r]   rG   ).0entryneedles     rL   	<setcomp>z0_repair_prefix_from_catalogue.<locals>.<setcomp>  sa       %<<EKKQ//288::@@BBfLL 	LLLrN   rQ   )r^   rv   r_   getr]   rG   lenpop)rB   rt   rv   	cataloguematchesrz   s        @rL   _repair_prefix_from_cataloguer   n  s      j6666666    !$$X..4"I %%''F     G
 7||q{{}}s    c                    |pd                                 }|rd|v rdS 	 t          |           }n# t          $ r Y dS w xY wt          ||          }||k    r|ndS )u  Return the prefixed catalogue id for a bare *model_name*, if unambiguous.

    The diagnostic counterpart to :func:`_repair_prefix_from_catalogue`: used
    to explain a provider's content-free 404 when the configured id lost its
    ``vendor/`` prefix. Returns ``None`` when the name already has a prefix,
    the provider has no curated catalogue, or nothing matches — so callers can
    stay silent rather than guess (#78796).
    r[   rP   N)r]   ra   r_   r   )rt   rB   rm   	canonicalrepaireds        rL   suggest_prefixed_model_idr     s     "##%%D 3$;;t-h77		   tt,T9==H4''88T1s   0 
>>model_inputc                   | pd                                 }|s|S t          |          }|t          v rt          |          S ddlm}  ||          }|qd|v r/|                    dd          \  }}|                                 p|}|dk    r6|                                                    d          rt          |          S |S |t          v r%t          ||          }d|v r|S t          |          S |d	v r'	 dd
lm}	  |	|          }
|
r|
S n# t          $ r Y nw xY w|t          v rIt          ||          }||k    r1|                    d          r|                    dd          d         S |S |dk    r%t          ||          }d|v r|S t          |          S |t           v r/t          ||          }|t"          v r|                                }|S |t$          v rt'          ||          S |t(          v r|S |S )a  Translate a model name into the format the target provider's API expects.

    This is the primary entry point for model name normalisation.  It
    accepts any user-facing model identifier and transforms it for the
    specific provider that will receive the API call.

    Args:
        model_input: The model name as provided by the user or config.
            Can be bare (``"claude-sonnet-4.6"``), vendor-prefixed
            (``"anthropic/claude-sonnet-4.6"``), or already in native
            format (``"claude-sonnet-4-6"``).
        target_provider: The canonical Hermes provider id, e.g.
            ``"openrouter"``, ``"anthropic"``, ``"copilot"``,
            ``"deepseek"``, ``"custom"``.  Should already be normalised
            via ``hermes_cli.models.normalize_provider()``.

    Returns:
        The model identifier string that the target provider's API
        expects.

    Raises:
        No exceptions -- always returns a best-effort string.

    Examples::

        >>> normalize_model_for_provider("claude-sonnet-4.6", "openrouter")
        'anthropic/claude-sonnet-4.6'

        >>> normalize_model_for_provider("anthropic/claude-sonnet-4.6", "anthropic")
        'claude-sonnet-4-6'

        >>> normalize_model_for_provider("anthropic/claude-sonnet-4.6", "copilot")
        'claude-sonnet-4.6'

        >>> normalize_model_for_provider("openai/gpt-5.4", "copilot")
        'gpt-5.4'

        >>> normalize_model_for_provider("claude-sonnet-4.6", "opencode-zen")
        'claude-sonnet-4-6'

        >>> normalize_model_for_provider("minimax-m2.5-free", "opencode-zen")
        'minimax-m2.5-free'

        >>> normalize_model_for_provider("deepseek-v3", "deepseek")
        'deepseek-v4-flash'

        >>> normalize_model_for_provider("deepseek-r1", "deepseek")
        'deepseek-v4-flash'

        >>> normalize_model_for_provider("deepseek-reasoner", "deepseek")
        'deepseek-v4-flash'

        >>> normalize_model_for_provider("my-model", "custom")
        'my-model'

        >>> normalize_model_for_provider("claude-sonnet-4.6", "zai")
        'claude-sonnet-4.6'

        >>> normalize_model_for_provider("MiMo-V2.5-Pro", "xiaomi")
        'mimo-v2.5-pro'
    r[   r   )opencode_provider_familyNrP   rQ   zopencode-zenzclaude->   r&   r'   )normalize_copilot_model_idzopenai/r   )r]   ra   r#   rs   r^   r   rR   rG   rl   rX   r$   rh   r   r_   r(   rM   r6   r8   r7   r   r*   )r   rb   rm   rt   r   
_oc_family_bare_after_slashrJ   r   
normalizedstrippedresults                rL   normalize_model_for_providerr     s   | 2$$&&D (99H (((t$$$ ;:::::))(33J$;;"&**S!"4"4A#))++3tD''DJJLL,C,CI,N,N'#D))) +++.tX>>$;;K%%% ---		DDDDDD33D99J "!!" 	 	 	 D	 ///24BBt	 : :::c1%%a(( :.tX>>$;;K&t,,, 3330x@@ 111\\^^F
 555,T8<<< 222 Ks   9D 
DD)rB   rC   rD   rC   )rY   rC   rD   rC   )rB   rC   rb   rC   rD   rC   )rB   rC   rD   ri   )rB   rC   rt   rC   rD   rC   )rt   rC   rB   rC   rD   ri   )r   rC   rb   rC   rD   rC   )__doc__
__future__r   retypingr   r   __annotations__	frozensetr#   r$   r(   r*   r6   r7   r8   r>   r?   rA   compilerH   rM   rF   rX   ra   rh   rq   rs   r   r   r    rN   rL   <module>r      s2    > # " " " " " 				      $k$	8$ 	($ 	(	$
 	($ h$ X$ 
$ 
6$ L$ y$ F$ F$ H$ z$  !$" \#$$ '$ $ $     . )2	 3 3 3 ) )      ,596 , ,     
 09y : : : 0 0      3<)= 3 3      4=9 > > > 4 4      4 6?Y@ 6 6 "     .7Y8 . .      /8i 9 9 9 / /      -6I7 - -     
 .7Y8 . .      #
#>?? ' ' ' '\   "( ( ( (
 
 
 
   D/ / / /d   4" " " "J2 2 2 20X X X X X XrN   