
    JjM              
          d Z ddlmZ ddlmZmZ ddlmZ  ed           G d d                      Zd*d
Z	dddddddddd	d+dZ
d,dZd-d Zd,d!Zd.d"Zdd#d/d$Zd0d1d)ZdS )2u  Provider/model inventory context — shared substrate for the dashboard
``/api/model/options``, the TUI ``model.options``/``model.save_key``
JSON-RPC handlers, and the interactive picker.

Before this module the three call-sites each duplicated:

1. The 17-LOC config-slice that pulls ``model.{default,name,provider,base_url}``,
   ``providers:``, and ``custom_providers:`` out of ``load_config()``;
2. The call into ``list_authenticated_providers`` with the resulting kwargs;
3. (TUI only) a 45-LOC post-pass that merges authenticated rows with
   unconfigured ``CANONICAL_PROVIDERS`` rows and emits ``authenticated``/
   ``auth_type``/``key_env``/``warning`` hints for the picker UI.

Consolidating those three steps into one entry point eliminates two bugs
the duplicates were hiding:

- The dashboard read ``cfg.get("custom_providers")`` directly, missing the
  v12+ keyed ``providers:`` form (which the TUI handled via
  ``get_compatible_custom_providers``).
- The TUI's canonical-merge keyed on ``is_user_defined`` to decide
  ordering. Section 3 of ``list_authenticated_providers`` sets
  ``is_user_defined=True`` even for canonical slugs that appear in the
  ``providers:`` config dict, which silently demoted them to the tail of
  the picker. ``_reorder_canonical`` keys on slug membership instead.

Substrate facts (verified May 2026):
- ``list_authenticated_providers`` already populates each row's
  ``models`` from the curated catalog (same source as the picker). Do
  NOT call ``provider_model_ids()`` per row to "freshen" — that bypasses
  curation and pulls in non-agentic models (Nous /models returns ~400
  IDs including TTS, embeddings, rerankers, image/video generators).
    )annotations)	dataclassreplace)OptionalT)frozenc                  X    e Zd ZU dZded<   ded<   ded<   ded<   ded	<   d
d
d
dddZd
S )ConfigContextzSnapshot of the model + provider config every inventory caller
    needs. Built once via ``load_picker_context()``; the TUI overlays
    live agent state via ``with_overrides()`` before passing through.
    strcurrent_providercurrent_modelcurrent_base_urldictuser_providerslistcustom_providersN)r   r   r   Optional[str]return'ConfigContext'c               R    i }|r||d<   |r||d<   |r||d<   |rt          | fi |n| S )u   Return a copy with truthy overrides applied.

        Truthy-only because the TUI reads agent attributes that may be
        empty strings before an agent is spawned — empties must NOT
        clobber the disk-config values.
        r   r   r   )r   )selfr   r   r   kws        :/home/rurouni/.hermes/hermes-agent/hermes_cli/inventory.pywith_overrideszConfigContext.with_overrides8   sa      	6%5B!" 	0"/B 	6%5B!"&(2wt""r"""d2    )r   r   r   r   r   r   r   r   )__name__
__module____qualname____doc____annotations__r    r   r   r	   r	   +   s          
 
 +/'+*.3 3 3 3 3 3 3 3r   r	   r   c            	        ddl m} m}  |            }|                    di           }t	          |t
                    r]|                    d|                    dd                    pd}|                    dd          pd}|                    dd          pd}n|rt          |          nd}d}d}|                    d	          }t          |||t	          |t
                    r|ni  | |          
          S )u   Load the disk-config snapshot every consumer needs.

    Replaces the inline 17-LOC config-slice that ``web_server.py`` and
    ``tui_gateway/server.py`` (×2 sites) used to do.
    r   )get_compatible_custom_providersload_configmodeldefaultname providerbase_url	providers)r   r   r   r   r   )hermes_cli.configr"   r#   get
isinstancer   r
   r	   )r"   r#   cfg	model_cfgr   r   r   raws           r   load_picker_contextr1   O   s    ONNNNNNN
+--C$$I)T"" !ivr1J1JKKQr$==R88>B$==R88>B +4;I
''+

C)#)(d33;ss88==   r   FN)	include_unconfiguredpicker_hintscanonical_orderpricingcapabilitiesforce_fresh_nous_tierrefreshprobe_custom_providers
max_modelsctxr2   boolr3   r4   r5   r6   r7   r8   r9   r:   
int | Noner   c       	           ddl m}
  |
| j        | j        | j        | j        | j        ||	||	  	        }t          | j                  }||gd |D             z   }	 ddlm	} n# t          $ r d}Y nw xY w|t                      |D ]K}|                    d          r4                    d |                    d	          pg D                        Lr|D ]}|                    d          r|                    d
d          } ||          s:|                    d	          pg }fd|D             }t          |          t          |          k     r||d	<   t          |          |d<   |r*t          |          d t!          ||           D             z   }|rt#          |           |rt%          |          }|rt'          ||           |rt)          |           || j        | j        dS )uf  Build the ``{providers, model, provider}`` shape every consumer
    needs from a single substrate call.

    Flags:
    - ``include_unconfigured``: append ``CANONICAL_PROVIDERS`` rows that
      ``list_authenticated_providers`` didn't emit (TUI uses this to show
      the full provider universe in the picker).
    - ``picker_hints``: add ``authenticated``/``auth_type``/``key_env``/
      ``warning`` per row (TUI ``ModelPickerDialog`` shape).
    - ``canonical_order``: reorder canonical-slug rows to
      ``CANONICAL_PROVIDERS`` declaration order; truly-custom rows go
      last (TUI display order).
    - ``pricing``: enrich each row with formatted per-model pricing and,
      for Nous, ``free_tier``/``unavailable_models`` so the GUI picker can
      show $/Mtok columns and gate paid models on free accounts —
      mirroring the ``hermes model`` CLI picker. Adds network calls
      (pricing fetch + Nous tier check); only set for interactive pickers.
    - ``capabilities``: add a per-row ``capabilities`` map
      ``{model: {fast, reasoning}}`` so pickers can gate the model-options
      controls (fast toggle / reasoning) to what each model actually
      supports, instead of offering knobs the backend would reject.
    - ``force_fresh_nous_tier``: bypass the short Nous free-tier cache when
      selecting Portal-recommended Nous models and applying tier gating. Keep
      this false for UI picker opens; explicit auth/model flows can opt in
      when they need freshly-purchased credits to show up immediately.
    - ``refresh``: bust the per-provider model-id disk cache so every row
      re-fetches its live catalog. Set only for an explicit user-triggered
      "refresh models" action; normal picker opens leave it false to stay
      snappy on the 1h cache.
    - ``probe_custom_providers``: allow saved custom/provider endpoints to
      run live ``/models`` discovery while building the payload. GUI picker
      opens should leave this false unless the user explicitly refreshes; the
      row can still render its configured model immediately, and slow/offline
      local endpoints no longer block the dialog.
    r   )list_authenticated_providers)	r   r   r   r   r   r7   r:   r8   r9   Nc                    g | ]=}t          |                    d d                                                    dk    ;|>S slugr'   moar
   r,   lower.0rs     r   
<listcomp>z(build_models_payload.<locals>.<listcomp>   sE    WWW!s1553D3D/E/E/K/K/M/MQV/V/VA/V/V/Vr   )is_routing_aggregatoris_user_definedc              3  >   K   | ]}|                                 V  d S )NrE   )rG   ms     r   	<genexpr>z'build_models_payload.<locals>.<genexpr>   s*      "P"P17799"P"P"P"P"P"Pr   modelsrB   r'   c                @    g | ]}|                                 v|S r    rM   )rG   rN   user_modelss     r   rI   z(build_models_payload.<locals>.<listcomp>   s+    PPP!17799K3O3OA3O3O3Or   total_modelsc                    g | ]=}t          |                    d d                                                    dk    ;|>S rA   rD   rF   s     r   rI   z(build_models_payload.<locals>.<listcomp>   sK    xxx1PSTUTYTYZ`bdTeTePfPfPlPlPnPnrwPwPwQPwPwPwr   r7   )r*   r$   r(   )hermes_cli.model_switchr?   r   r   r   r   r   _moa_provider_rowhermes_cli.providersrJ   	Exceptionsetr,   updatelenr   _append_unconfigured_rows_apply_picker_hints_reorder_canonical_apply_pricing_apply_capabilities)r;   r2   r3   r4   r5   r6   r7   r8   r9   r:   r?   rowsmoa_row_is_routing_aggregatorrowrB   originalfilteredrR   s                     @r   build_models_payloadrh   o   s   ` EDDDDD''--')-35
 
 
D   455GyWWtWWWW&XXXXXXX & & &!%& ) # 	Q 	QCww()) Q"""P"Pswwx7H7H7NB"P"P"PPPP 	8 8 8 77,-- wwvr** .-d33 778,,2PPPPxPPPx==3x==00$,CM*-h--C' yDzzxx(A$(L(Lxxxx "D!!! (!$'' Jt3HIIII "D!!! "(  s   A# #A21A2rb   
list[dict]Nonec                t   ddl m} 	 ddlm} n# t          $ r d}Y nw xY w| D ]}|                    d          pd}i }|                    d          pg D ]\}d}|8|r6	  |||          }|t          |j                  }n# t          $ r d}Y nw xY wt           ||                    |d	||<   ]||d
<   dS )u  Attach a ``{model: {fast, reasoning}}`` map to each provider row.

    `fast` mirrors ``model_supports_fast_mode`` (the same gate the runtime
    enforces). `reasoning` comes from the models.dev catalog when known and
    defaults to True otherwise — the effort dial is broadly accepted and a
    no-op on models that ignore it, whereas hiding it from a capable-but-
    uncatalogued model is the worse failure.
    r   )model_supports_fast_mode)get_model_capabilitiesNrB   r'   rP   T)fast	reasoningr6   )hermes_cli.modelsrl   agent.models_devrm   rY   r,   r<   supports_reasoning)	rb   rl   rm   re   rB   capsr$   ro   metas	            r   ra   ra      sJ    ;:::::&;;;;;;; & & &!%&  # #wwv$"+-WWX&&," 	 	EI%1d1%11$>>D'$()@$A$A	  % % % $III% 55e<<==& DKK
 #N'# #s    "BBBc                `   ddl m}m} d | D             }|j        pd                                }g }|D ]y}|j                                        |v r|                    |j        |                    |j        |j                  |j                                        |k    dg ddd           z|S )zBBuild skeleton rows for canonical providers missing from ``rows``.r   )CANONICAL_PROVIDERS_PROVIDER_LABELSc                B    h | ]}|d                                           S rB   rM   rF   s     r   	<setcomp>z,_append_unconfigured_rows.<locals>.<setcomp>  s&    ,,,!AfIOO,,,r   r'   F	canonical)rB   r&   
is_currentrK   rP   rS   source)	rp   rv   rw   r   rE   rB   appendr,   label)rb   r;   rv   rw   seencurextrasentrys           r   r]   r]     s    GGGGGGGG,,t,,,D%2
,
,
.
.CF$ 
 
:%%
(,,UZEE#j..00C7#( !% 
	
 
	
 
	
 
	
 Mr   c                ~   ddl m} | D ]}d|v r|                    d          dk    o|                    d           }| |d<   |r|                    d          rT|                    |d                   }|r|j        nd	}|r|j        r|j        d         nd
}||d<   ||d<   |d	k    r|rd| dnd| d|d<   dS )a0  Add ``authenticated``/``auth_type``/``key_env``/``warning`` per row.

    Mutates ``rows`` in-place. Rows already from
    ``list_authenticated_providers`` are marked ``authenticated=True``;
    the unconfigured skeleton rows from ``_append_unconfigured_rows`` get
    the picker's setup-hint shape.
    r   )PROVIDER_REGISTRYauthenticatedr}   r{   rP   rK   rB   api_keyr'   	auth_typekey_envzpaste z to activatez!run `hermes model` to configure ()warningN)hermes_cli.authr   r,   r   api_key_env_vars)rb   r   re   is_skeletonr.   r   r   s          r   r^   r^   1  s8    211111 
 
c!! ggh'';6Pswwx?P?P;P#.O 	cgg&788 	##CK00%(7CMMi	 ,C ## 	
 %K I I%%'% +W****AYAAA 	I+
 
r   c                    ddl m} d t          |          D             t          fd| D             fd          }fd| D             }||z   S )u  Canonical slugs in ``CANONICAL_PROVIDERS`` declaration order;
    truly-custom rows last.

    Keys on slug membership, NOT ``is_user_defined`` — section 3 of
    ``list_authenticated_providers`` sets ``is_user_defined=True`` on
    rows from the ``providers:`` config dict even when the slug is
    canonical. Keying on the flag would silently demote canonical
    providers configured via the new keyed schema.
    r   )rv   c                $    i | ]\  }}|j         |S r    ry   )rG   ies      r   
<dictcomp>z&_reorder_canonical.<locals>.<dictcomp>c  s     BBB41aQVQBBBr   c              3  0   K   | ]}|d          v |V  dS )rB   Nr    rG   rH   orders     r   rO   z%_reorder_canonical.<locals>.<genexpr>e  s1      //qAfI......//r   c                     | d                  S )NrB   r    )rH   r   s    r   <lambda>z$_reorder_canonical.<locals>.<lambda>f  s    eAfI& r   )keyc                (    g | ]}|d          v|S ry   r    r   s     r   rI   z&_reorder_canonical.<locals>.<listcomp>h  s'    888A6%!7!7a!7!7!7r   )rp   rv   	enumeratesorted)rb   rv   canonr   r   s       @r   r_   r_   W  s     655555BB9-@#A#ABBBE////D///&&&&  E 9888888F6>r   rU   c               f   ddl m}m}m}m} d}| D ]}t          |                    dd                                                    }|                    d          pg }	|	sR	  ||          pi }
n# t          $ r i }
Y nw xY w|
svi }|	D ]}|
                    |          }|s|                    dd          }|                    dd          }|                    d	d          }|dk    r ||          nd}|dk    r ||          nd}|r ||          nd}|d
k    o|d
k    p|dk    }||||d||<   |r||d<   |dk    ri	 | ||          }t          |          |d<   |r$ |t          |	          |
d          \  }}||d<   ng |d<   # t          $ r d|d<   g |d<   Y w xY wdS )u  Enrich each provider row with per-model pricing + Nous tier gating.

    Mutates ``rows`` in-place. For every row whose provider supports live
    pricing (openrouter / nous / novita) adds::

        row["pricing"] = {model_id: {"input": "$3.00", "output": "$15.00",
                                     "cache": "$0.30" | None, "free": bool}}

    For Nous additionally adds::

        row["free_tier"] = bool            # current account is free-tier
        row["unavailable_models"] = [...]  # paid models a free user can't pick

    Prices are pre-formatted via ``_format_price_per_mtok`` so the GUI just
    renders strings — identical formatting to the CLI picker. All failures
    are swallowed (best-effort): a row simply gets no ``pricing`` key.
    r   )_format_price_per_mtokcheck_nous_free_tierget_pricing_for_providerpartition_nous_models_by_tierNrB   r'   rP   prompt
completioninput_cache_readfree)inputoutputcacher   r5   nous)force_fresh	free_tierT)r   unavailable_modelsF)rp   r   r   r   r   r
   r,   rE   rY   r<   r   )rb   r7   r   r   r   r   nous_free_tierre   rB   rP   raw_pricing	formattedmidpinp_rawout_raw	cache_rawinpoutr   is_free_selectableunavailables                          r   r`   r`   l  s   ,            &*N 5/ 5/37762&&''--//""(b 		22488>BKK 	 	 	KKK	 	%'	 	 	C$$A eeHb))GeeL"--G0"55I5<]]((111C5<]]((111C9BL**9555EVmD)C#)G	 IcNN  	'&C	N6>>/!)%9%9$9& & &N $(#7#7K ! 3/L/LVkT0 0 0,K 1<C,--02C,- / / / $)K ,.()))	/ G5/ 5/s%   $A22B BAFF,+F,r'   r   r
   dict | Nonec                d   	 ddl m} ddlm}  | |                                d          pi           }t          |                    di                                                     }|sdS dd| pd                                dk    d	|t          |          d
dd
dd
S # t          $ r Y dS w xY w)a+  Build the virtual ``moa`` provider row for model pickers.

    Shared by the CLI inventory (:func:`build_models_payload`) and the gateway
    picker path (:func:`hermes_cli.model_switch.list_picker_providers`) so the
    row shape stays in one place. Returns ``None`` when no MoA presets exist.
    r   )r#   )normalize_moa_configrC   presetsNzMixture of Agentsr'   FvirtualTzTAggregator acts as the selected model; references provide analysis before each call.)
rB   r&   r|   rK   rP   rS   r}   r   r   r   )
r+   r#   hermes_cli.moa_configr   r,   r   keysrE   r\   rY   )r   r#   r   r.   rP   s        r   rW   rW     s    111111>>>>>>"";;==#4#4U#;#;#ArBBcggi,,113344 	4'+1r88::eC$KK!"m
 
 	
    tts   A+B! /1B! !
B/.B/)r   r	   )r;   r	   r2   r<   r3   r<   r4   r<   r5   r<   r6   r<   r7   r<   r8   r<   r9   r<   r:   r=   r   r   )rb   ri   r   rj   )rb   ri   r;   r	   r   ri   )rb   ri   r   ri   )rb   ri   r7   r<   r   rj   )r'   )r   r
   r   r   )r   
__future__r   dataclassesr   r   typingr   r	   r1   rh   ra   r]   r^   r_   r`   rW   r    r   r   <module>r      s   B # " " " " " * * * * * * * *       $ 3  3  3  3  3  3  3  3F   F "'!"'#'!~ ~ ~ ~ ~ ~B## ## ## ##R   0#
 #
 #
 #
L   0 #(U/ U/ U/ U/ U/ U/p      r   