+
    E{iqT                         R t ^ RIHt ^ RIt^ RItRR ltR tR tR tR t	R t
R tR	 tR
 tR tR tR tR tR R ltR tR tR tR tR tR tR tR tR tR tR tR tR tR t R t!R# )!z
Tests for resolve_model_provider() model routing logic.
Verifies that model IDs are correctly resolved to (model, provider, base_url)
tuples for different provider configurations.
Nc                   \        \        P                  4      p/ pV'       d   WR&   V'       d   W%R&   V'       d   W5R&   V'       d   TM/ \        P                  R&    \        P                  ! V 4      \        P                  P	                  4        \        P                  P                  V4       #   \        P                  P	                  4        \        P                  P                  T4       i ; i)zHHelper: temporarily set config.cfg model section, call resolve, restore.providerbase_urldefaultmodeldictconfigcfgresolve_model_providerclearupdate)model_idr   r   r   old_cfg	model_cfgs   &&&&  6/home/ubuntu/hermes-webui/tests/test_model_resolver.py_resolve_with_configr      s    6::GI (* (*&)'0)bFJJw#,,X6



'" 	



'"s   B1 1?C0c                 ^    \        RRRR7      w  rpV R8X  g   Q RV  R24       hVR8X  g   Q hR# )zKopenrouter/free must NOT be stripped to 'free' when provider is openrouter.openrouter/free
openrouterhttps://openrouter.ai/api/v1r   r   z!Expected 'openrouter/free', got ''Nr   r   r   r   s      r   $test_openrouter_free_keeps_full_pathr      sH     4L/!EX %%S)J5'QR'SS%|###    c                 L    \        RRRR7      w  rpV R8X  g   Q hVR8X  g   Q hR# )z;anthropic/claude-sonnet-4.6 via openrouter keeps full path.anthropic/claude-sonnet-4.6r   r   r   Nr   r   s      r   *test_openrouter_model_with_provider_prefixr   )   s8     4%/!EX 1111|###r   c                 J    \        RRR7      w  rpV R8X  g   Q hVR8X  g   Q hR# )zEanthropic/claude-sonnet-4.6 strips prefix when provider is anthropic.r   	anthropicr   zclaude-sonnet-4.6Nr   r   s      r   -test_anthropic_prefix_stripped_for_direct_apir#   5   s5     4%!EX ''''{"""r   c                 J    \        RRR7      w  rpV R8X  g   Q hVR8X  g   Q hR# )z:openai/gpt-5.4-mini strips prefix when provider is openai.openai/gpt-5.4-miniopenair"   gpt-5.4-miniNr   r   s      r   *test_openai_prefix_stripped_for_direct_apir(   >   s4     4!EX N"""xr   c                 V    \        RRR7      w  rpV R8X  g   Q hVR8X  g   Q hVe   Q hR# )zDPicking openai model when config is anthropic routes via openrouter.r%   r!   r"   r   Nr   r   s      r   -test_cross_provider_routes_through_openrouterr*   I   sA     4!EX ))))|###r   c                 ^    \        RRRR7      w  rpV R8X  g   Q hVR8X  g   Q hVR8X  g   Q hR# )z=A model name without / uses the config provider and base_url.zgemma-4-26Bcustomzhttp://192.168.1.160:4000r   Nr   r   s      r   $test_bare_model_uses_config_providerr-   U   sF     4,!EX M!!!x2222r   c                 J    \        RRR7      w  rpV R8X  g   Q hVR8X  g   Q hR# )z8Empty model string returns config provider and base_url. r!   r"   Nr   r   s      r   (test_empty_model_returns_config_defaultsr0   `   s2     4
[!EX B;;{"""r   c                 V    \        RRR7      w  rpV R8X  g   Q hVR8X  g   Q hVe   Q hR# )z:@minimax:MiniMax-M2.7 routes to minimax provider directly.z@minimax:MiniMax-M2.7r!   r"   zMiniMax-M2.7minimaxNr   r   s      r   .test_provider_hint_routes_to_specific_providerr3   k   s@     4+!EX N"""y   r   c                 J    \        RRR7      w  rpV R8X  g   Q hVR8X  g   Q hR# )z+@zai:GLM-5 routes to zai provider directly.z
@zai:GLM-5r&   r"   zGLM-5zaiNr   r   s      r   test_provider_hint_zair6   u   s4     4x!EX Gur   c                 J    \        RRR7      w  rpV R8X  g   Q hVR8X  g   Q hR# )z4@deepseek:deepseek-chat routes to deepseek provider.z@deepseek:deepseek-chatr!   r"   deepseek-chatdeepseekNr   r   s      r   test_provider_hint_deepseekr:   ~   s4     4!K!EX O###z!!!r   c                 J    \        RRR7      w  rpV R8X  g   Q hVR8X  g   Q hR# )zBminimax/MiniMax-M2.7 (old format) still routes through openrouter.zminimax/MiniMax-M2.7r!   r"   r   Nr   r   s      r   5test_slash_prefix_non_default_still_routes_openrouterr<      s5     4!EX ****|###r   c                   \        \        P                  4      p\        P                  pRV /\        P                  R&   R \        n         \        P                  ! 4       V\        n        \        P                  P                  4        \        P                  P                  V4       #   T\        n        \        P                  P                  4        \        P                  P                  T4       i ; i)z2Helper: temporarily set active_provider in config.r   r   c                      / # N r@   r   r   <lambda>1_available_models_with_provider.<locals>.<lambda>   s    Br   )r   r	   r
   _load_repo_envget_available_modelsr   r   )r   r   old_repo_envs   &  r   _available_models_with_providerrF      s    6::G((L%x0FJJw&F#**, ,



'" !-



'"s   B) )A
C3c                  aaaaa \        \        P                  4      p\        V4      \        P                  R&   \        T;'       g    . 4      \        P                  R&   \        P
                  ! R4      p	. V	n        \        P
                  ! R4      p
R V
n        \        P
                  ! R4      pR Vn        V P                  \        P                  RV	4       V P                  \        P                  RV
4       V P                  \        P                  RV4       V P                  \        RV3R	 l4       V P                  \        R
V3R l4       V P                  \        RR 4       Sf3   \        P                   Uu. uF  pRVR,          RVR,          /NK  	  upoSf*   \        \        P                  P                  R. 4      4      oSf   / oV P                  \        RRV3R ll4       V P                  \        RV3R l4       V P                  \        RRV3R ll4        \        P                   ! 4       \        P                  P#                  4        \        P                  P%                  V4       # u upi   \        P                  P#                  4        \        P                  P%                  T4       i ; i)z?Helper: isolate provider discovery from the real machine state.r   custom_providers
hermes_clizhermes_cli.modelsc                      . # r?   r@   r@   r   r   rA   2_available_models_with_detection.<locals>.<lambda>   s    2r   zhermes_cli.authc                     / # r?   r@   )_pids   &r   rA   rK      s    Rr   _load_profile_envc                  .   < \        S ;'       g    / 4      # r?   r   envs   r   rA   rK      s    T#))_r   _load_auth_storec                  .   < \        S ;'       g    / 4      # r?   rP   )
auth_stores   r   rA   rK      s    DAQAQr<Rr   rC   c                      / # r?   r@   r@   r   r   rA   rK      s    "r   idlabelopenai-codex _discover_openrouter_free_modelsc                    < \        S4      # r?   list)all_envopenrouter_modelss   &r   rA   rK      s    Y]^oYpr   _discover_codex_model_entriesc                     < \        S 4      # r?   r\   )codex_modelss   r   rA   rK      s
    lI[r   (_discover_named_custom_provider_catalogsc                    < \        S4      # r?   rP   )r^   custom_catalogss   &r   rA   rK      s    aefuavr   r?   )r   r	   r
   r]   types
ModuleType__path__list_available_providersget_auth_statussetitemsysmodulessetattr_OPENROUTER_FREE_MODELS_PROVIDER_MODELSgetrD   r   r   )monkeypatchr   rR   rU   r_   rb   rH   re   r   fake_pkgfake_models	fake_authms   &&ffff&f     r    _available_models_with_detectionrw      s    6::Gy/FJJw%)*:*@*@b%AFJJ!"-HH""#67K+5K(  !23I /I\8<%8+F%6	B 35LM 24RS 0*= KQKiKijKiadAdGWajAKijF3377KL BDpq ?A[\ JLvw#**,



'" k 	



'"s   I<*J ?K c                    \        R4      p V R,           Uu/ uF  qR,          VR,          bK  	  ppRV9   dC   VR,           F3  pVR,          P                  R4      '       d   K#  Q RVR,          : 24       h	  R	# R	# u upi )
zIWith anthropic as default, minimax model IDs should use @minimax: prefix.r!   groupsr   modelsMiniMaxrW   z	@minimax:z Expected @minimax: prefix, got: N)rF   
startswith)resultgry   rv   s       r   0test_non_default_provider_models_use_hint_prefixr      s    ,[9F282BC2BQ
mQx[(2BFCF	""AT7%%k22 21T7+>2 #  Ds   A=c           	        \        V RRRR/4      pVR,           Uu/ uF  q"R,          VR,          bK  	  ppR pVR,           Uu. uF  qT! VR	,          4      NK  	  pp\        V4       Uu. uF  qvP                  V4      ^8  g   K  VNK  	  ppV'       d+   Q R
T RVR,           Uu. uF  qUR	,          NK  	  up 24       hR# u upi u upi u upi u upi )zIssue #147 Bug 2: 'anthropic/claude-opus-4.6' as default_model must not
inject a duplicate alongside the existing bare 'claude-opus-4.6' entry in
the same provider group.r   r!   r   zanthropic/claude-opus-4.6ry   provider_idrz   c                 F    R V 9   d   V P                  R ^4      R,          # T # )/)split)mids   &r   rA   Btest_no_duplicate_when_default_model_is_prefixed.<locals>.<lambda>   s"    s
syya(,CCr   rW   z:Anthropic group has duplicate models after normalization: z
Full group: N)rw   setcount)	rr   r}   r~   ry   normrv   bare_idsr   
duplicatess	   &        r   0test_no_duplicate_when_default_model_is_prefixedr      s     .2	
F 6<H5EF5E(+5EFFCD'-k':;':!QtW':H;!$XJ#..2E2I##JJ D,nvk7J%K7J!gg7J%K$L	N>z	 G;J &Ls   CC+CC'Cc                 v   ^ RI Hp  V P                  P                  R. 4       Uu0 uF  qR,          kK  	  pp\	        R4      pVR,           Uu/ uF  qDR,          VR,          bK  	  ppRV9   d;   VR,           Uu0 uF  qR,          kK  	  ppV F  pWv9   d   K  Q RV R	24       h	  R# R# u upi u upi u upi )
z<The active provider's models remain bare (no @prefix added).Nr!   rW   ry   r   rz   	Anthropicz_PROVIDER_MODELS entry 'z%' is missing from the Anthropic group)
api.configr	   rp   rq   rF   )_cfgrv   raw_anthropic_idsr}   r~   ry   returned_idsbare_ids           r   )test_default_provider_models_not_prefixedr      s    *.*?*?*C*CKQS*TU*TQ4*TU,[9F282BC2BQ
mQx[(2BFCf)/)<=)<A$)<=(G* *7)3XY* )  VC=s   B,B1;B6c           
        \        V RRRRRR/RRR	R
RR/RRRRR////R7      pVR,           Uu/ uF  q"R,          VR,          bK  	  pp0 RmP                  VP                  4       4      '       g   Q h\        ;QJ d#    R VR,           4       F  '       g   K   RM	  RM! R VR,           4       4      '       g   Q h\        ;QJ d#    R VR,           4       F  '       g   K   RM	  RM! R VR,           4       4      '       g   Q h\        ;QJ d#    R VR,           4       F  '       g   K   RM	  RM! R VR,           4       4      '       g   Q hR# u upi )zIOpenAI Codex OAuth and the local custom env aliases should still surface.r   r   r   qwen/qwen3.6-plus:freer   r   OPENROUTER_API_KEYor-keyCUSTOM_API_MOONSHOT_AI_API_KEYmoonshot-keyCUSTOM_API_DEEPSEEK_COM_API_KEYdeepseek-key	providersrY   tokensaccess_tokencodex-tokenrR   rU   ry   r   rz   kimi-codingr9   c              3   P   "   T F  qR ,          P                  R4      x  K  	  R# 5i)rW   z@openai-codex:Nr|   .0rv   s   & r   	<genexpr>Btest_detects_codex_oauth_and_custom_env_aliases.<locals>.<genexpr>  s#     T=Sw!!"233=S   $&TFc              3   P   "   T F  qR ,          P                  R4      x  K  	  R# 5i)rW   z@kimi-coding:Nr   r   s   & r   r   r     s"     R<Qqw!!/22<Qr   c              3   P   "   T F  qR ,          P                  R4      x  K  	  R# 5i)rW   z
@deepseek:Nr   r   s   & r   r   r     s"     L9KAw!!,//9Kr   N   r9   r   r   rY   )rw   issubsetkeysany)rr   r}   r~   ry   s   &   r   /test_detects_codex_oauth_and_custom_env_aliasesr      s8   -/6	
 !(,n-~
 NM+J K
F& 6<H5EF5E(+5EFFDMMfkkm\\\\3TVN=ST333TVN=STTTTT3RF=<QR333RF=<QRRRRR3L
9KL333L
9KLLLLL	 Gs   Ec           	        \        V RRRRRR/RR/R	7      pVR
,           Uu/ uF  q"R,          VbK  	  ppRV9  g   Q hVR,          R,           Uu0 uF  qDR,          kK  	  ppRV9  g   Q hRV9  g   Q hRV9   g   Q hR# u upi u upi )zLStandard OpenRouter config should not produce a second giant 'custom' group.r   r   r   r   r   r   r   r   rQ   ry   r   r,   rz   rW   r8   deepseek-reasonerr   Nrw   rr   r}   r~   ry   rv   openrouter_idss   &     r   @test_openrouter_group_stays_free_and_no_duplicate_custom_catalogr     s    -/6	

 "8,F ,2(+;<+;a!+;F<6!!!'-l';H'EF'E!gg'ENF.000n444... =Fs   BBc                    V P                  RR4       \        V RRRRRR/R	R
RRRRRR/RRRRR////R7      pVR,           Uu0 uF  q"R,          kK  	  ppV0 Rm8X  g   Q hR# u upi )zCRepo/env allowlist should hide providers outside the requested set.HERMES_WEBUI_ALLOWED_PROVIDERSz,openai-codex,kimi-coding,deepseek,openrouterr   r   r   r   r   r   r   r   r   r   r   r   ANTHROPIC_API_KEYanthropic-keyr   rY   r   r   r   r   ry   r   Nr   )setenvrw   rr   r}   r~   provider_idss   &   r   %test_allowed_providers_filters_groupsr   +  s    79gh-/6	
 !(,n-~	
 NM+J K
F& /5X.>?.>m$$.>L?TTTT @s   A#c                (   V P                  RRR7       V P                  RR4       \        V RRRR	R
R/RRRRRRRR/RRRRR////R7      pVR,           Uu0 uF  q"R,          kK  	  ppRV9  g   Q h0 RmP                  V4      '       g   Q hR# u upi )zBHidden-provider config should remove only the requested providers.r   F)raisingHERMES_WEBUI_HIDDEN_PROVIDERSr!   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   rY   r   r   r   r   ry   r   Nr   )delenvr   rw   r   r   s   &   r   $test_hidden_providers_filters_groupsr   E  s    7G6D-/6	
 !(,n-~	
 NM+J K
F& /5X.>?.>m$$.>L?l***DMMl[[[[ @s   Bc           
        \        V RRRR/RRRRR	////R
RRR/R
RRR/.R7      pVR,           Uu/ uF  q"R,          VR,          bK  	  ppVR,           Uu0 uF  qDR
,          kK  	  ppRV9   g   Q hRV9   g   Q hR# u upi u upi )z[OpenAI Codex should expose the live/discovered catalog instead of only the old static list.r   r   r   r   r   rY   r   r   r   rW   gpt-5.4rX   GPT-5.4r'   zGPT-5.4 MinirU   rb   ry   r   rz   @openai-codex:gpt-5.4z@openai-codex:gpt-5.4-miniNr   )rr   r}   r~   ry   rv   	codex_idss   &     r   %test_codex_group_uses_dynamic_catalogr   a  s    -	\9.FG.8nm=\2]!^_9gy1>7N;
	F 6<H5EF5E(+5EFF"("89"8Q4"8I9"i///'9444 G9s   A>Bc                   \        V RRRRRR/RR/R	RR
R/R	RR
R/R	RR
R/.R7      pVR,           Uu/ uF  q"R,          VR,          bK  	  ppVR,           Uu. uF  qDR	,          NK  	  ppV. RO8X  g   Q hR# u upi u upi )zHOpenRouter group should come from the live/discovered free-text catalog.r   r   r   r   r   r   r   r   rW   rX   zAuto (Free Router)openai/gpt-oss-120b:freezOpenAI: gpt-oss-120b (free)qwen/qwen3-coder:freez"Qwen: Qwen3 Coder 480B A35B (free))rR   r_   ry   r   rz   N)r   r   r   r   r   s   &     r   /test_openrouter_group_uses_dynamic_free_catalogr   r  s    -(6	

 "8,$g/CD-w8UV*G5YZ
F 6<H5EF5E(+5EFF'-l';<';!gg';N<     G<s   A8A=c                    \        V RRRRRR/RR/R	R
RRRR/.R
RRRR/RRRR/./R7      pVR,           Uu/ uF  q"R,          VR,          bK  	  ppR
V9   g   Q hVR
,           Uu. uF  qDR,          NK  	  ppVRR.8X  g   Q hR# u upi u upi )zXNamed custom providers from config.yaml should appear with provider hints when inactive.r   r   r   r   r   r   r   r   nameblockrunhttp://127.0.0.1:8402/v1api_modechat_completionsrW   autorX   openai/gpt-5.4)rR   rH   re   ry   r   rz   z@blockrun:autoz@blockrun:openai/gpt-5.4Nr   )rr   r}   r~   ry   rv   blockrun_idss   &     r   8test_named_custom_provider_group_is_exposed_and_prefixedr     s    -(6	

 "8,Z-GUgh
 vw/'2BC
F& 6<H5EF5E(+5EFF%+J%78%7dGG%7L8,.HIIII G8s   B&Bc                   \        V RRRRRR/RRRRRR	/.RR
RRR/R
RRR/./R7      pVR,           Uu/ uF  q"R,          VR,          bK  	  ppVR,           Uu. uF  qDR
,          NK  	  upRR.8X  g   Q hR# u upi u upi )zGWhen a named custom provider is active, its catalog should remain bare.r   r   r   r   r   r   r   r   r   rW   rX   r   rH   re   ry   r   rz   Nr   )rr   r}   r~   ry   rv   s   &    r   5test_named_custom_provider_active_keeps_raw_model_idsr     s    -
v2	
 Z-GUgh
 vw/'2BC
F$ 6<H5EF5E(+5EFF#J/0/dGG/0V=M4NNNN G0s   A9A>c                    \        \        P                  4      p RRRR/\        P                  R&   RRRRRR/.\        P                  R	&    \        P                  ! R
4      w  rpVR
8X  g   Q hVR8X  g   Q hVR8X  g   Q h \        P                  P	                  4        \        P                  P                  V 4       R#   \        P                  P	                  4        \        P                  P                  T 4       i ; i)zRSlash-form models on a named custom provider should not be rerouted to OpenRouter.r   r   r   r   r   r   r   r   rH   r   Nr   )r   r   r   r   s       r   ?test_named_custom_provider_keeps_slash_model_on_active_providerr     s    6::GJ.FJJw
 
Z)CZQcd&FJJ!"#$*$A$ABR$S!((((:%%%5555



'" 	



'"s   4C   ?C?c           
        \        V RRRR/RRRRR	////R
RRR/R
RRR/.R7      pVR,           Uu/ uF  q"R,          VR,          bK  	  pp\        R VR,           4       4      p\        R VR,           4       4      pVR,          R,          R8X  g   Q hVR,          R,          . RO8X  g   Q hVR,          R,          RJ g   Q hVR,          R,          R8X  g   Q hVR,          R,          RJ g   Q hR# u upi ) zHGPT-5 Codex models should expose only the effort values Hermes supports.r   r   r   r   r   rY   r   r   r   rW   r   rX   r   zgpt-4ozGPT-4or   ry   r   rz   c              3   D   "   T F  qR ,          R8X  g   K  Vx  K  	  R# 5i)rW   r   Nr@   r   s   & r   r   Itest_codex_reasoning_metadata_tracks_supported_efforts.<locals>.<genexpr>  s     Y2qgAX6X2    
 c              3   D   "   T F  qR ,          R8X  g   K  Vx  K  	  R# 5i)rW   z@openai-codex:gpt-4oNr@   r   s   & r   r   r     s     X2qgAW6W2r   	reasoningmodeeffortoptionsdisabledFunsupportedTN)r/   noneminimallowmediumhighrw   next)rr   r}   r~   ry   gpt54gpt4os   &     r   6test_codex_reasoning_metadata_tracks_supported_effortsr     s"   -	\9.?@.8nm=\2]!^_9gy18Wh/
	F 6<H5EF5E(+5EFFYF>2YYEXF>2XXEf%111i(,\\\\j)U222f%666j)T111 Gs   C3c                n   \        V RRRR/RR/R7      pVR,           Uu/ uF  q"R	,          VR
,          bK  	  pp\        R VR,           4       4      pVR,          R,          R8X  g   Q hVR,          R,          R.8X  g   Q hVR,          R,          RJ g   Q hRVR,          R,          9   g   Q hR# u upi )zWDeepSeek should disable the effort dropdown and point users to Chat vs Thinking models.r   r9   r   r8   r   r   rQ   ry   r   rz   c              3   D   "   T F  qR ,          R8X  g   K  Vx  K  	  R# 5i)rW   r   Nr@   r   s   & r   r   >test_deepseek_reasoning_is_model_controlled.<locals>.<genexpr>  s     T1!tW@S5SAA1r   r   r   r   r   r/   r   TzChat and Thinking modelsnoteNr   )rr   r}   r~   ry   reasoners   &    r   +test_deepseek_reasoning_is_model_controlledr     s    -	ZO<.?F 6<H5EF5E(+5EFFTvj1TTHK (G333K +t333K ,444%+)>v)FFFF Gs   B2c                
   \        V RRRR/RRRRRR	/.RR
RRRRRRRR//R
RRRRRRRR//./R7      pVR,           Uu/ uF  q"R,          VR,          bK  	  pp\        R VR,           4       4      p\        R VR,           4       4      pVR,          R,          R8X  g   Q hVR,          R,          RR.8X  g   Q hVR,          R,          RJ g   Q hVR,          R,          R8X  g   Q hVR,          R,          RJ g   Q hR# u upi ) z\Custom providers like Mistral should surface toggle-only reasoning when the catalog says so.r   mistralr   mistral-small-latestr   r   zhttps://api.mistral.ai/v1r   r   rW   rX   capabilitiescompletion_chatTr   mistral-medium-latestFr   ry   r   rz   c              3   D   "   T F  qR ,          R8X  g   K  Vx  K  	  R# 5i)rW   r   Nr@   r   s   & r   r   Ztest_named_custom_provider_reasoning_metadata_uses_capabilities_boolean.<locals>.<genexpr>  s     S-q4<R1R-r   c              3   D   "   T F  qR ,          R8X  g   K  Vx  K  	  R# 5i)rW   r   Nr@   r   s   & r   r   r    s     U.D'=T2T!!.r   r   toggler   r/   r   r   r   Nr   )rr   r}   r~   ry   smallr   s   &     r   Gtest_named_custom_provider_reasoning_metadata_uses_capabilities_booleanr    sY   -	Y	+ABY
,GUgh
 03"%6k4$P 14"%6k5$Q
F, 6<H5EF5E(+5EFFSF9-SSEUVI.UUFf%111i(RL888j)U222+v&-777+z*d222 Gs   D )NNN)NNNNNN)"__doc__r   r	   rl   rf   r   r   r   r#   r(   r*   r-   r0   r3   r6   r:   r<   rF   rw   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r@   r   r   <module>r     s   
  
 #($$# 3#"$#*#Z*M8/(U4\85"2J6O0#(20G"!3r   