
    IjxH              	          U d Z ddlZddlZddlZddlZddl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 ej                            ej                            e                    Zeej        vrej                            de           ddlmZ ej                            d          Zej                            d          Zej                            d          Zej                            d          Zej                            d	          Zej                            ed
          Z ej        ed            ej        eej        dd            ej         d          Z!d Z"da#e$dz  e%d<   de&fdZ'd Z(ej                            d          Z)de*de+fdZ,d8de*de*de*de*fdZ-de+fdZ.de/fdZ0d e*de*fd!Z1d"e*de&fd#Z2de+dz  fd$Z3d e*de&fd%Z4de+de&fd&Z5de+de&fd'Z6d9d)e+d*e*fd+Z7d:d-e$d.e$de/e&e8f         fd/Z9d)e+de&fd0Z:d1 Z;d2 Z<e=d3k    rU	  e;             dS # e>$ r@Z?e!@                    d4d5            e"d6e?             ejA        d7           Y dZ?[?dS dZ?[?ww xY wdS );a  
Auto-switch llama-server service when Hermes config model changes.

Triggered by systemd path unit (hermes-config.path) on config.yaml changes.
Uses flock-based lock file to prevent concurrent invocations from racing.
Uses state file guard to prevent re-trigger loops.
Performs rollback if the target service fails to start.
Logs to ~/.hermes/logs/model-switch.log for troubleshooting.

Single source of truth for model definitions: models.py
    N)datetime)MODELSz~/.hermes/config.yamlz~/.hermes/.last-model-statez~/.hermes/.model-switch-notifyz~/.hermes/.model-switch.lockz~/.hermes/logszmodel-switch.logTexist_okz'%(asctime)s [%(levelname)s] %(message)sz%Y-%m-%d %H:%M:%S)filenamelevelformatdatefmtzmodel-switchc                 $    t          | d          S )NT)flush)print)msgs    2/home/rurouni/.hermes/scripts/auto-model-switch.py<lambda>r   2   s    E#T***     _LOCK_FDreturnc                     	 t          j        t          t           j        t           j        z  d          } t          j        | t
          j        t
          j        z             | a	t          j
        | d           t          j        | t          j                     d                                           dS # t          t          f$ r/ t          $dt!                      v rt          j        |            Y dS w xY w)zAAcquire an exclusive lock on LOCK_FILE. Returns True if acquired.i  r   
TNfdF)osopen	LOCK_FILEO_CREATO_RDWRfcntlflockLOCK_EXLOCK_NBr   	ftruncatewritegetpidencodeIOErrorOSErrorlocalsclose)r   s    r   acquire_lockr(   9   s    WY
RY 6>>B5666
R
	'''..00111tW    0 0HRLLLuus   B0B4 4<C43C4c                     t           	 t          j        t           t          j                   t	          j        t                      n# t          t          f$ r Y nw xY wda 	 t	          j        t                     dS # t          $ r Y dS w xY wdS )zRelease the lock file.N)
r   r   r   LOCK_UNr   r'   r$   r%   unlinkr    r   r   release_lockr-   J   s     	K%-000HX! 	 	 	D		Ii      	 	 	DD	 s#   =A AA!A< <
B
	B
z(~/.hermes/cache/openrouter_override.jsonfilepathc                    i }d}d}t          |           5 }|D ]}|                                }|r|                    d          r/t          |          t          |                                          z
  }d|v r|                    d          \  }}	}
|                                }|
                                                    d          }|dk    r|dk    r|}d}|dk    r|dk    r|}|}|r||k    r|r|nd|| d| <   |r
|r|nd||<   |}|                                ||<   	 ddd           n# 1 swxY w Y   |S )u   Ultra-simple YAML parser for ~/.hermes/config.yaml — returns flat-ish dict.
    Only handles basic indented key-value pairs and leaf values.Nr   #:z'" .)r   rstrip
startswithlenlstrip	partitionstrip)r.   resultcurrent_keycurrent_indentflinestrippedindentkey_part_val_partkeyvals                r   parse_yaml_simplerF   ^   s    FKN	h 71 	7 	7D{{}}H x22377 YYT[[]]!3!33Fh(0(:(:3(?(?%!Xnn&&nn&&,,U33Q;;3"99"%K%&NNaZZC2II"%K%+NN  &Vn%<%<<?5GSSRFk11C1122 &),"4##"F3KK"%KK&.nn&6&6{##-	77 7 7 7 7 7 7 7 7 7 7 7 7 7 70 Ms   DE  EEr2   providermodelbase_urlc                    t          | d          5 }t          j        |          pi }ddd           n# 1 swxY w Y   t          |t                    si }|                    di           }t          |t                    si }||d<   ||d<   ||d<   |r||d<   t          j                            |           pd}t          j
        d	d
|d          \  }}		 t          j        |dd          5 }t          j        ||ddd           |                                 t          j        |                                           ddd           n# 1 swxY w Y   t          j        |	|            t          j                            |	          rt          j        |	           dS dS # t          j                            |	          rt          j        |	           w w xY w)zBAtomically update only the top-level model section in config.yaml.zutf-8)encodingNrH   rG   defaultrI   r3   z.config.yaml.z.tmpT)prefixsuffixdirtextwF)default_flow_style	sort_keysallow_unicode)r   yaml	safe_load
isinstancedict
setdefaultr   pathdirnametempfilemkstempfdopen	safe_dumpr   fsyncfilenoreplaceexistsr+   )
r.   rG   rH   rI   r=   cfg	model_cfg	directoryr   tmp_paths
             r   write_model_yamlrh      sK   	h	)	)	) &QnQ%2& & & & & & & & & & & & & & &c4   w++Ii&& !	 G$Ij Ii ) (	*))0SI#?6y_cdddLB Yr3111 	!QN3eu\`aaaaGGIIIHQXXZZ   	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	
8X&&&7>>(## 	 Ih	  	 27>>(## 	 Ih	 s=   599F% .AEF% EF% EF% %6Gc                     t           j                            t                    s%t                              dt                     dddS t          t                    } |                     dd          |                     dd          dS )z7Read model.provider and model.default from config.yaml.zConfig file not found: %sN)rG   rH   zmodel.providerr2   zmodel.default)r   rZ   rc   CONFIGlogwarningrF   get)parseds    r   get_config_settingsro      sz    7>>&!! 1/888 4000v&&FJJ/44OR00  r   c                  r   t           j                            t                    sdS t	          t                    5 } |                                                                                     d          }ddd           n# 1 swxY w Y   |r|d         nd}t          |          dk    r|d         nd}||fS )z+Read last-known-GOOD state from state file.)NNr   Nr      )	r   rZ   rc   
STATE_FILEr   readr9   splitr6   )r=   linesrH   rG   s       r   get_last_good_staterv      s    7>>*%% |	j		 -Q  &&t,,- - - - - - - - - - - - - - -'E!HH4Eu::>>uQxxtH8s   :BBB
model_namec                     |  d| d}t          t          d          5 }|                    |           ddd           n# 1 swxY w Y   t                              d||            dS )z%Persist the last good model+provider.r   rQ   Nz&Good state saved: provider=%s model=%s)r   rr   r!   rk   debug)rw   rG   contentr=   s       r   save_good_stater{      s    ++x+++G	j#		 !	              II6*MMMMMs   A  AAmodel_idc                    t           j                            t                    st                              d           dS 	 t          t                    5 }t          j        |          }ddd           n# 1 swxY w Y   d |d         d         d         D             }| |v S # t          j	        t          f$ r&}t                              d|           Y d}~dS d}~ww xY w)	z=Check if an OpenRouter model is in the curated override list.u6   OpenRouter override file not found — cannot validateTNc                     g | ]
}|d          S )idr,   ).0ms     r   
<listcomp>z/is_allowed_openrouter_model.<locals>.<listcomp>   s    NNNq1T7NNNr   	providers
openroutermodelsz'Failed to parse OpenRouter override: %s)r   rZ   rc   OPENROUTER_OVERRIDErk   rl   r   jsonloadJSONDecodeErrorKeyErrorerror)r|   r=   dataallowedes        r   is_allowed_openrouter_modelr      s   7>>-.. LMMMt%&& 	 !9Q<<D	  	  	  	  	  	  	  	  	  	  	  	  	  	  	 NND$5l$CH$MNNN7"" (+   		;Q???ttttts<   B$ A7+B$ 7A;;B$ >A;?$B$ $C :CC c                      t           D ]Z} |                     d          rt          j        dd| d         gddd          }|j                                        dk    r| c S [d	S )
zPFind which model service is currently active (systemd), skipping preserved ones.preserve	systemctlz	is-activeserviceT   capture_outputrP   timeoutactiveN)r   rm   
subprocessrunstdoutr9   )r   rs     r   get_active_servicer      s      55 	N+q|4dA
 
 
 8>>x''HHH (4r   c                 F     t           fdt          D                        S )zCCheck if a model_name refers to a cloud model (not in MODELS list).c              3   0   K   | ]}|d          k    V  dS rw   Nr,   )r   r   rw   s     r   	<genexpr>z!is_cloud_model.<locals>.<genexpr>   s,      AAQ1\?j0AAAAAAr   )anyr   )rw   s   `r   is_cloud_modelr      s)    AAAA&AAAAAAAr   c                    t                               d| d         | d                    t          j        ddd| d         gddd	          }|j        d
k    r"t                               d| d                    nGt                               d| d         |j        |j                                        dd                    |j        d
k    S )z?Stop a single model's systemd service. Returns True on success.zStopping %s (%s)...namer   sudor   stopT   r   r   u     ✅ Stopped %su!     ⚠️  Stop %s returned %d: %sN   )rk   infor   r   
returncoderl   stderrr9   rH   r   s     r   stop_servicer      s    HH"E&M53CDDD	feI&67$	 	 	A 	|q#U6]333376]AL!(..2B2B4C42H	J 	J 	J<1r   c                    t                               d| d         | d                    t          j        ddd| d         gddd	          }|j        d
k    r"t                               d| d                    nGt                               d| d         |j        |j                                        dd                    |j        d
k    S )z9Start a model's systemd service. Returns True on success.zStarting %s (%s)...r   r   r   r   startTi,  r   r   u     ✅ Started %su     ❌ Start %s failed (%d): %sN)rk   r   r   r   r   r   r   r9   r   s     r   start_servicer      s    HH"E&M53CDDD	guY'78$	 	 	A 	|q#U6]3333		2=!,0@0@#0F	H 	H 	H<1r   switchedtargetactionc                    d}	 t          j        g dddd          }|j        dk    rS|j                                                            d          }t          |          dk    r|d          d	|d
          d}n# t          $ r Y nw xY wt          j	                    
                    d          }d| d| d          d| d          d| d|                     dd           d| d}t          j        t          j                            t                     d           t#          t           d          5 }|                    |dz              ddd           dS # 1 swxY w Y   dS )z?Write a one-shot notification file for the watchdog to deliver.?)z
nvidia-smiz$--query-gpu=memory.used,memory.totalz--format=csv,noheader,nounitsTr   r   r   z,    z MB / rq   z MBz%I:%M:%S %puO   🔀 **Model Switch**
━━━━━━━━━━━━━━━
**Event:** z
**Model:** r   z
**Port:** portz
**VRAM:** z
**Context:** contextzK
**Time:** z PDTr   rQ   r   N)r   r   r   r   r9   rt   r6   	Exceptionr   nowstrftimerm   r   makedirsrZ   r[   NOTIFY_FILEr   r!   )r   r   vramr   partstsr   r=   s           r   write_notificationr      s   DN. . .dA
 
 

 <1HNN$$**400E5zzQ(77%(777   		 	 	/	/B		 	 Vn	 	 F^		 	
 	 	 

9c22	 	 	 	 	  K,,t<<<<	k3		 1	d
                 s$   A8A= =
B
	B
5EE"E<   r   timeout_secc                    d|  d}t          j                     }t          |          D ]}	 t          j                            t          j                            |          d          }|j        dk    rdt          j                     |z
  fc S n# t          $ r Y nw xY wt          j        d           dt          j                     |z
  fS )	zEWait for the model API to respond. Returns (success, seconds_waited).http://127.0.0.1:z
/v1/modelsr   )r   r   Trq   F)	timerangeurllibrequesturlopenRequeststatusr   sleep)r   r   urlr   rB   resps         r   wait_for_modelr     s    
.d
.
.
.CIKKE;  	>))&.*@*@*E*Eq)QQD{c!!TY[[500000 " 	 	 	D	
1$)++%%%s   A B
BBc                    t                      }|r5|d         | d         k    r#t                              d| d                    dS |}|r|d         dk    r| d         dk    r{|                    d          rt	          d|d          d           nht	          d	|d          d
           t          |           t	          d           t          j        d           n|st                              d           | d         dk    ret	          d| d          d
           t          |           }|rt	          d| d          d           t          | d         d          \  }}|rt	          d| d          d|dd           nt	          d| d          d|dd           t                              d|r|d         nd| d         |rdnd|           d | d          d!}t          t          |                     dd"          | d#         |           t          | d$           dS t	          d%| d                     t                              d&           |rt	          d'|d          d
           t          |          }|rt                              d(|d                    t          t          |                    dd"          |d#         d |d          d!           t          |d)           t	          d*|d                     n:t                              d+|d                    t	          d,|d          d-           d.S t                              d/           t	          d0           d.S t	          d1| d#          d2           dS )3zStop the currently active service, start target.
    Does NOT write config.yaml (caller already did). Only rollback writes config.
    r   u!   Already on %s — nothing to swapTrG   localr   u     🛡  Preserving z (background service)u     ⏹  Stopping ...u#     💤 Waiting 3s for VRAM drain...   zNo active service to stopu     ▶️  Starting u     ⏳ Waiting for z to respond...r   r   )r   u     ✅ z responded in z.1fsu
     ⚠️  z API not responding after z.0fu   s — continuingu3   Switch complete: %s → %s (API ready: %s in %.1fs)z(none)yesnor   /v1custom:localrw   r   u     ❌ Failed to start u%   Switch FAILED — attempting rollbacku     ↩️  Rolling back to z!Rollback successful: restarted %srollbacku     ✅ Rolled back to u.   Rollback FAILED — %s could not be restarted!u     ❌ Rollback failed! z not running!Fz#No previous service to roll back tou-     ⚠️  No previous service to roll back tou     ☁️  Cloud model u    — no service swap needed)r   rk   r   rm   _echor   r   r   r   r   rh   rj   r   r   critical)r   r   previousokloadedelapsedcorrect_base_urlrollback_oks           r   swap_tor   (  s     !!F  &.F6N224fVnEEEtH  	.&$//F:4F'4Q4Q::j!! 	MvMMMNNNN8VF^888999   7888JqMMMM .,--- jW$$7F6N7778886"" 	Evf~EEEFFF,VF^LLLOFG lKvf~KKWKKKKLLLLj6&>jjWjjjjkkkHHJ)1?Xf%%x$.UU$9 9 9  G6&>FFFVVZZ
N%K%K#L13CE E Evz2224 	7vf~77888		9::: 	Dx/?DDDEEE'11K 	Q<hv>NOOO j.)Q)Q!),!7!JXf5E!J!J!JL L L #8Z888@hv.>@@AAAAMxX^O_```O0@OOOPPP5II;<<<ABBB5 

T6,#7
T
T
TUUU4r   c                      t                      s.t                              d           t          j        d           	 t                       t                       d S # t                       w xY w)Nu(   Lock held by another process — exitingr   )r(   rk   ry   sysexit
_main_implr-   r,   r   r   mainr   q  sW    >> 		<===s   A A,c                      t                      } | d         | d         }st                              d           d S |dk    r:t                      \  }}t	                    st          d d           t                              d           |rL|k    rFt          d|            t                              d	|           t          t          |pd|           nEd
}t          d|            t                              d|           t          t          d|           t                      } t          | d         | d                    d S t          |           t                              d           d S t                    r-t                              d           t          |           d S t          fdt          D             d           }|st                              d           d S t                      \  }}|k    rt          fdt          D             d           }|rEt                      }|r/|d         |d         k    rt                              d           d S |r)t                              d|d         |d                    nt                              d|d                    t          d|d          d           t          |          }|rvd|d          d}	t          t          |                    dd          |d         |	           t#          |d           t          |           t          d|d                     nt          d|d                     d S t                              d            d S t                              d!|d                    t          d"|d          d#           t%          |          }
|
r*t          |           t          d$|d                     d S d S )%NrH   rG   u   No model in config — exitingr   u     ❌ 'z/' is NOT in your curated OpenRouter model list!z2BLOCKED: OpenRouter model '%s' not in allowed listu     ↩️  Reverting to z'Reverting config to last good model: %szQwen3.6-35B-A3B-UD-Q4_K_S.ggufu5     ↩️  No last good state — reverting to local: z&Reverting config to local fallback: %sr   zOpenRouter model validated: %su4   Config set to cloud model: %s — no service to swapc              3   4   K   | ]}|d          k    |V  dS r   r,   r   r   config_models     r   r   z_main_impl.<locals>.<genexpr>  s1      HH,<(G(G1(G(G(G(GHHr   u(   Unknown model in config: %s — ignoringc              3   4   K   | ]}|d          k    |V  dS r   r,   r   s     r   r   z_main_impl.<locals>.<genexpr>  s1      LLQAlO|,K,Kq,K,K,K,KLLr   r   u2   State unchanged (%s) — service healthy, skippingu2   State says %s but %s is active — fixing mismatchu0   State says %s but service is dead — restartingu    🔄 u    is dead — restarting...r   r   r   rw   	restartedu    ✅ Restarted u     ❌ Failed to restart u!   State unchanged (%s) — skippingz!Config changed to local model: %su     🔄 Auto-switching to r   u     ✅ Switched to )ro   rk   r   rv   r   r   rl   rh   rj   r{   r   nextr   r   ry   r   rm   r   r   )settingsconfig_providerlast_good_modellast_good_providerfallbackr   rB   r   r   correct_urlr:   r   s              @r   r   r   {  sf   "$$HG$Lz*O 1222 ,&&.A.C.C++*<88 	YLYYYZZZKKLl[[[ C?l#B#BAAABBBBOTTT );)K|_]]]];XhXXYYYA8LLL BBB*,,HHW-x
/CDDDFo6661<@@@ l## GVVVo666 HHHHfHHH$OOF >MMM -..OQ,&&LLLL&LLLdSS 	'))F &.F6N::		NP\]]] P"6NF6N< < < < NPVW]P^___Ivf~IIIJJJ"6** G"IfVn"I"I"IK$VVZZ
N-S-S%+L%9;H H H&v{;;;#L/BBB<F6N<<====EVF^EEFFFII9<HHHF HH0&.AAA	
9fVn
9
9
9:::V__F 5o66636&>33444445 5r   __main__z$Unhandled error in auto-model-switch)exc_infou     ❌ Auto-switch error: rq   )r2   )r   )r   )B__doc__r   r   r   r   loggingr   r\   urllib.requestr   r   rU   r   rZ   r[   abspath__file___script_dirinsertr   r   
expanduserrj   rr   r   r   LOG_DIRjoinLOG_FILEr   basicConfigINFO	getLoggerrk   r   r   int__annotations__boolr(   r-   r   strrX   rF   rh   ro   tuplerv   r{   r   r   r   r   r   r   floatr   r   r   r   __name__r   r   r   r   r,   r   r   <module>r     s  
 
 
 
			 



                     goobgooh7788chHOOA{###       
		3	4	4W =>>
g  !ABBG=>>	
'

-
.
.7<<!344 Gd # # # #  
,4	    g'' 	+* #*   d    "  " g(()STT      B   s  c  #          6	T 	 	 	 	U    N Ns N N N N# $    D4K    Bs Bt B B B B
      $     t S    >& & &3 &dEk8J & & & & ED ET E E E ER  P5 P5 P5f z   		84	HHH-!--... s   
H% %I**5I%%I*