
    i+                    ,   U d Z ddlmZ ddlZddlZddlZddlZddlZddl	m
Z
 ddlmZ ddlmZ ddlmZ  ej        e          ZdZd	Zd
ZdZde Zdaded<   daded<   d/dZd0dZd1dZd2dZ d3d Z!d4d"Z"d#d$d5d&Z#d6d(Z$d6d)Z%d7d+Z&d8d-Z'd9d.Z(dS ):u  Remote model catalog fetcher.

The Hermes docs site hosts a JSON manifest of curated models for providers
we want to update without shipping a release (currently OpenRouter and
Nous Portal). This module fetches, validates, and caches that manifest,
falling back to the in-repo hardcoded lists when the network is unavailable.

Pipeline
--------
1. ``get_catalog()`` — returns a parsed manifest dict.
   - Checks in-process cache (invalidated by TTL).
   - Reads disk cache at ``~/.hermes/cache/model_catalog.json``.
   - Fetches the master URL if disk cache is stale or missing.
   - On any fetch failure, keeps using the stale cache (or empty dict).

2. ``get_curated_openrouter_models()`` / ``get_curated_nous_models()`` —
   thin accessors returning the shapes existing callers expect. Each
   falls back to the in-repo hardcoded list on any lookup failure.

Schema (version 1)
------------------
::

    {
      "version": 1,
      "updated_at": "2026-04-25T22:00:00Z",
      "metadata": {...},                # free-form
      "providers": {
        "openrouter": {
          "metadata": {...},            # free-form
          "models": [
            {"id": "vendor/model", "description": "recommended",
             "metadata": {...}}          # free-form, model-level
          ]
        },
        "nous": {...}
      }
    }

Unknown fields are ignored — extra metadata can be added at either level
without bumping ``version``. ``version`` bumps are reserved for
breaking changes (renaming ``providers``, changing ``models`` shape).
    )annotationsN)Path)Any)__version__)atomic_replacezAhttps://hermes-agent.nousresearch.com/docs/api/model-catalog.json   g       @   zhermes-cli/dict[str, Any] | None_catalog_cache        float_catalog_cache_source_mtimereturndict[str, Any]c                    	 ddl m}   |             pi }n# t          $ r i }Y nw xY w|                    d          }t	          |t
                    si }t          |                    dd                    t          |                    d          pt                    t          |                    d          pt                    t	          |                    d          t
                    r|                    d          ni d	S )
z@Load the ``model_catalog`` config block with defaults filled in.r   )load_configmodel_catalogenabledTurl	ttl_hours	providers)r   r   r   r   )hermes_cli.configr   	Exceptionget
isinstancedictboolstrDEFAULT_CATALOG_URLr   DEFAULT_TTL_HOURS)r   cfgraws      =/home/ubuntu/.hermes/hermes-agent/hermes_cli/model_catalog.py_load_catalog_configr$   U   s   111111kmm!r    ''/
"
"Cc4    	400113775>>8%899377;//D3DEE-78L8Ld-S-S[SWW[)))Y[	  s    $$r   c                 .    ddl m}   |             dz  dz  S )zHReturn the disk cache path. Import lazily so tests can monkeypatch home.r   get_hermes_homecachezmodel_catalog.json)hermes_constantsr'   r&   s    r#   _cache_pathr*   i   s-    000000?w&)===    r   r   timeoutc                   	 t           j                            | dt          d          }t           j                            ||          5 }t          j        |                                                                          }ddd           n# 1 swxY w Y   n# t           j	        j
        t          t
          j        t          f$ r'}t                              d| |           Y d}~dS d}~wt           $ r'}t                              d| |           Y d}~dS d}~ww xY wt#          |          st                              d|            dS |S )	zGHTTP GET the manifest URL and return a parsed dict, or None on failure.zapplication/json)Acceptz
User-Agent)headers)r,   Nz#model catalog fetch failed (%s): %sz$model catalog fetch errored (%s): %sz,model catalog at %s failed schema validation)urllibrequestRequest_HERMES_USER_AGENTurlopenjsonloadsreaddecodeerrorURLErrorTimeoutErrorJSONDecodeErrorOSErrorloggerinfor   _validate_manifest)r   r,   reqrespdataexcs         r#   _fetch_manifestrE   t   s   n$$,0  % 
 
 ^##C#99 	4T:diikk002233D	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4L!<1EwO   93DDDttttt   :CEEEttttt d## BCHHHtKsH   A
B 9BB BB BB ,D	C++D8DDrC   r   r   c                   t          | t                    sdS |                     d          }t          |t                    r|t          k    rdS |                     d          }t          |t                    sdS |                                D ]\  }}t          |t                    rt          |t                    s dS |                    d          }t          |t                    s dS |D ]a}t          |t                    s  dS t          |                    d          t                    r|d                                         s  dS bdS )z=Return True when ``data`` matches the minimum manifest shape.Fversionr   modelsidT)	r   r   r   intSUPPORTED_SCHEMA_VERSIONitemsr   liststrip)rC   rG   r   pnamepblockrH   ms          r#   r@   r@      sZ   dD!! uhhy!!Ggs## w1I'I'I u%%Ii&& u"** 
 
v%%% 	Z-E-E 	55H%%&$'' 	55 	 	Aa&& uuuaeeDkk3// qw}} uuu	
 4r+   #tuple[dict[str, Any] | None, float]c                 b   t                      } 	 |                                 j        }n# t          t          f$ r Y dS w xY w	 t          |           5 }t          j        |          }ddd           n# 1 swxY w Y   n# t          t          j        f$ r Y dS w xY wt          |          sdS ||fS )z@Return ``(data_or_none, mtime)``. mtime is 0 if file is missing.)Nr   N)
r*   statst_mtimer=   FileNotFoundErroropenr5   loadr<   r@   )pathmtimefhrC   s       r#   _read_disk_cacher\      s   ==D		$&'   {{$ZZ 	!29R==D	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	!T)*   {{d## {%=sB   * ??A? A3'A? 3A77A? :A7;A? ?BBNonec                   t                      }	 |j                            dd           |                    |j        dz             }t          |d          5 }t          j        | |d           |                    d           d d d            n# 1 swxY w Y   t          ||           d S # t          $ r&}t                              d|           Y d }~d S d }~ww xY w)	NT)parentsexist_okz.tmpw   )indent
z$model catalog cache write failed: %s)r*   parentmkdirwith_suffixsuffixrW   r5   dumpwriter   r=   r>   r?   )rC   rY   tmpr[   rD   s        r#   _write_disk_cacherl      s.   ==DA$666t{V344#s^^ 	rIdBq))))HHTNNN	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	sD!!!!! A A A:C@@@@@@@@@As<   A	B/ -BB/ BB/ BB/ /
C9CCF)force_refreshrm   c                   t                      }|d         si S t          d|d         dz            }t                      \  }}t          j                    }|duo||z
  |k     }| st          ||t
          k    r	|rt          S | s
|r||a|a|S t          |d         t                    }|.t          |           t                      \  }}	||a|	a|S |a|a|S ||a|a|S i S )u   Return the parsed model catalog manifest, or an empty dict on failure.

    Callers should treat a missing provider/model as "use the in-repo fallback"
    — never raise from this function so the CLI keeps working offline.
    r   r   r   g      @Nr   )	r$   maxr\   timer   r   rE   DEFAULT_FETCH_TIMEOUTrl   )
rm   r!   ttl_seconds	disk_data
disk_mtimenow
disk_freshfetchednew_disk_data	new_mtimes
             r#   get_catalogrz      sC    
 
 Cy> 	c3{+f455K,..Iz
)++C$&KC*,<+KJ &!555 6   Z I,A"&0# c%j*?@@G'"""#3#5#5 y$*N*3'   &)#"&0#Ir+   providerc                d   t                      }|d         sdS |d                             |           }t          |t                    sdS |                    d          }t          |t                    r|                                sdS t          |                                t                    S )zEIf ``model_catalog.providers.<name>.url`` is set, fetch that instead.r   Nr   r   )r$   r   r   r   r   rN   rE   rq   )r{   r!   provider_cfgoverride_urls       r#   _fetch_provider_overrider     s    

 
 Cy> t{#''11LlD)) t##E**LlC(( 0B0B0D0D t <--//1FGGGr+   c                L   t          |           }|@|                    di                               |           }t          |t                    r|S t	                      }|sdS |                    di                               |           }t          |t                    r|ndS )zHReturn the provider's manifest block, respecting per-provider overrides.Nr   )r   r   r   r   rz   )r{   overrideblockcatalogs       r#   _get_provider_blockr     s    '11H["--11(;;eT"" 	LmmG tKKR((,,X66Eud++5555r+   list[tuple[str, str]] | Nonec                 N   t          d          } | sdS g }|                     dg           D ]v}t          |                    d          pd                                          }|s;t          |                    d          pd          }|                    ||f           w|pdS )zReturn OpenRouter's curated ``[(id, description), ...]`` from the manifest.

    Returns ``None`` when the manifest is unavailable, so callers can fall
    back to their hardcoded list.
    
openrouterNrH   rI    descriptionr   r   r   rN   append)r   outrQ   middescs        r#   get_curated_openrouter_modelsr   "  s      --E t!#CYYx$$    !%%++#$$**,, 	155''-2..

C;;$r+   list[str] | Nonec                     t          d          } | sdS g }|                     dg           D ]O}t          |                    d          pd                                          }|r|                    |           P|pdS )z~Return Nous Portal's curated list of model ids from the manifest.

    Returns ``None`` when the manifest is unavailable.
    nousNrH   rI   r   r   )r   r   rQ   r   s       r#   get_curated_nous_modelsr   5  s    
  ''E tCYYx$$  !%%++#$$**,, 	JJsOOO;$r+   c                     da dadS )zIClear the in-process cache. Used by tests and ``hermes model --refresh``.Nr   )r   r    r+   r#   reset_cacher   E  s     N"%r+   )r   r   )r   r   )r   r   r,   r   r   r
   )rC   r   r   r   )r   rR   )rC   r   r   r]   )rm   r   r   r   )r{   r   r   r
   )r   r   )r   r   )r   r]   ))__doc__
__future__r   r5   loggingrp   urllib.errorr0   urllib.requestpathlibr   typingr   
hermes_clir   _HERMES_VERSIONutilsr   	getLogger__name__r>   r   r    rq   rK   r3   r   __annotations__r   r$   r*   rE   r@   r\   rl   rz   r   r   r   r   r   r   r+   r#   <module>r      s  * * *X # " " " " "                        5 5 5 5 5 5            		8	$	$ H     4?44 
 )- , , , ,%(  ( ( ( (   (> > > >   4   4   "
A 
A 
A 
A$ */ 4 4 4 4 4 4nH H H H"6 6 6 6   &    & & & & & &r+   