
    6j                        d Z ddlmZ ddlZddlZddlmZ dZdZe G d d                      Z	 G d	 d
          Z
 e
            ZddZdS )u  
Hermes Web UI -- Streaming performance metering.

Tracks Tokens Per Second (TPS) across active WebUI streams.  Metering data is
emitted via SSE events so a streaming assistant message can update its own
header while the turn is running.

Architecture
────────────
Each streaming session is tracked independently.  TPS per stream is:

    stream_tps = total_stream_deltas / (last_delta_ts - first_delta_ts)

The global tps is the average of all currently active streams' TPS values.
This correctly represents the system's real-time capacity regardless of how
many sessions are running or how long each has been streaming.

For HIGH/LOW tracking, every stats snapshot records the current global tps
(only when > 0 — idle periods are skipped) into a rolling 60-minute history.
The max/min of that history gives the peak throughput observed over the past hour.

The ticker in streaming.py calls get_interval() — it returns 1.0 when streams
are actively receiving output deltas so message headers update at 1 Hz, and 10.0 when idle
so the ticker exits and no idle readings are emitted.

Usage from api/streaming.py
─────────────────────────────
  from api.metering import meter

  meter().begin_session(stream_id)                     # stream starts
  meter().record_token(stream_id, running_output_deltas)
  meter().record_reasoning(stream_id, running_reasoning_deltas)

The SSE `metering` event payload:
  {
    "tps": 47.3,              # omitted/null until a real reading exists
    "tps_available": true,    # frontend must hide TPS when false
    "estimated": false,       # never show byte/character-size estimates
    "high": 52.1,
    "low":  31.4,
    "active": 1,
  }
    )annotationsN)	dataclassg      @g      N@c                  X    e Zd ZU dZded<   dZded<   dZded<   dZded<   dd
ZddZ	dS )_SessionMeterr   intoutput_tokensreasoning_tokens        floatfirst_token_tslast_token_tsreturnc                     | j         | j        z   S N)r   r	   selfs    "/root/hermes-webui/api/metering.pytotal_tokensz_SessionMeter.total_tokens>   s    !D$999    float | Nonec                    | j         dk    s| j        | j         k    rd S |                                 | j        | j         z
  z  S Nr
   )r   r   r   r   s    r   tpsz_SessionMeter.tpsA   sG    #%%);t?R)R)R4  ""d&84;N&NOOr   N)r   r   )r   r   )
__name__
__module____qualname__r   __annotations__r	   r   r   r   r    r   r   r   r   7   s         MNM: : : :P P P P P Pr   r   c                  P    e Zd ZdZdZddZddZdd
ZddZddZ	dddZ
ddZdS )GlobalMeterzThread-safe global streaming meter.

    Tracks per-session TPS, averages them for a global tps, and maintains a
    60-minute rolling history of global tps snapshots for HIGH/LOW reporting.
    )_lock	_sessions	_readings_window_startr   Nonec                    t          j                    | _        i | _        g | _        t          j                    | _        d S r   )	threadingLockr!   r"   r#   time	monotonicr$   r   s    r   __init__zGlobalMeter.__init__U   s5    ^%%
3546$(N$4$4r   	stream_idstrc                r    | j         5  t                      | j        |<   d d d            d S # 1 swxY w Y   d S r   )r!   r   r"   )r   r,   s     r   begin_sessionzGlobalMeter.begin_session]   s~    Z 	8 	8(5DN9%	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8s   ,00r   c                    t          j                    | j        5  fd| j                                        D             }|rdndcddd           S # 1 swxY w Y   dS )zReturn 1.0 when sessions are actively receiving tokens, 10.0 when idle.

        Used by the streaming ticker to run at 1 Hz during work and exit when
        there is nothing to measure.
        c                V    h | ]%\  }}|j         d k    |j        z
  t          k    #|&S r   r   r   _STALE_SECS.0sidsnows      r   	<setcomp>z+GlobalMeter.get_interval.<locals>.<setcomp>j   sF       Q#a''S1?-B{,R,R ,R,R,Rr         ?g      $@N)r)   r*   r!   r"   items)r   active_sidsr9   s     @r   get_intervalzGlobalMeter.get_intervala   s     nZ 	0 	0   "&."6"6"8"8  K &/334	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0s   +AAArunning_output_tokensr   c                    t          j                    }| j        5  | j                            |          }|	 d d d            d S |j        dk    r||_        ||_        ||_        d d d            d S # 1 swxY w Y   d S r   )r)   r*   r!   r"   getr   r   r   )r   r,   r?   r9   r8   s        r   record_tokenzGlobalMeter.record_tokenp   s    nZ 	4 	4""9--Ay	4 	4 	4 	4 	4 	4 	4 	4 3&&#& !AO3AO	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4   A3 A33A7:A7running_reasoning_tokensc                    t          j                    }| j        5  | j                            |          }|	 d d d            d S |j        dk    r||_        ||_        ||_        d d d            d S # 1 swxY w Y   d S r   )r)   r*   r!   r"   rA   r   r   r	   )r   r,   rD   r9   r8   s        r   record_reasoningzGlobalMeter.record_reasoning{   s    nZ 	: 	:""9--Ay	: 	: 	: 	: 	: 	: 	: 	: 3&&#& !AO!9A	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	:rC   r   final_output_tokensinput_tokensc                |    | j         5  | j                            |d            d d d            d S # 1 swxY w Y   d S r   )r!   r"   pop)r   r,   rG   rH   s       r   end_sessionzGlobalMeter.end_session   s    Z 	0 	0Ny$///	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0 	0s   155dictc           	     Z  	
 t          j                    
| j        5  
fd| j                                        D             }|D ]}| j                            |d            | j        s
| _        d | j                                        D             }d |D             }|r t          |          t          |          z  }nd }
t          z
  		fd| j        D             | _        |"|dk    r| j                            
|f           d | j        D             }|rt          |          nd}|rt          |          nd}|t          |d          nd |d ud	|rt          |d          nd |rt          |d          nd t          | j                  d
cd d d            S # 1 swxY w Y   d S )Nc                V    g | ]%\  }}|j         d k    |j        z
  t          k    #|&S r2   r3   r5   s      r   
<listcomp>z)GlobalMeter.get_stats.<locals>.<listcomp>   sF       Q#a''S1?-Bk,Q,Q ,Q,Q,Qr   c                (    g | ]}|j         d k    |S r2   )r   )r6   r8   s     r   rO   z)GlobalMeter.get_stats.<locals>.<listcomp>   s%    QQQAA<Lq<P<Pa<P<P<Pr   c                J    g | ] }|                                 }||dk    |!S )Nr   )r   )r6   r8   vs      r   rO   z)GlobalMeter.get_stats.<locals>.<listcomp>   s4    ZZZaeegg!-TUXYTYTY!TYTYTYr   c                *    g | ]\  }}|k    ||fS r   r   )r6   tsrR   cutoffs      r   rO   z)GlobalMeter.get_stats.<locals>.<listcomp>   s&    QQQ%"aR&[[r1g[[[r   r   c                $    g | ]\  }}|d k    |S )r;   r   )r6   _rR   s      r   rO   z)GlobalMeter.get_stats.<locals>.<listcomp>   s!    HHHTQqCxxqxxxr   r
      F)r   tps_available	estimatedhighlowactive)r)   r*   r!   r"   r<   rJ   r$   valuessumlen
_HOUR_SECSr#   appendmaxminround)r   staler7   r]   
active_tps
global_tpsactive_readingsr[   r\   rU   r9   s            @@r   	get_statszGlobalMeter.get_stats   sM   nZ -	 -	   "&."6"6"8"8  E  . .""3---- > )%("
 RQ!6!6!8!8QQQFZZVZZZJ " __s:>

!
 :%FQQQQ4>QQQDN
 %*q..%%sJ&7888 IHT^HHHO+:C3'''D*9B#o&&&sC 0:/EuZ+++4!+4!7"*.8dAD(+5uS!}}}dn-- M-	 -	 -	 -	 -	 -	 -	 -	 -	 -	 -	 -	 -	 -	 -	 -	 -	 -	s   E6F  F$'F$N)r   r%   )r,   r-   r   r%   )r   r   )r,   r-   r?   r   r   r%   )r,   r-   rD   r   r   r%   r2   )r,   r-   rG   r   rH   r   r   r%   )r   rL   )r   r   r   __doc__	__slots__r+   r/   r>   rB   rF   rK   rj   r   r   r   r    r    G   s         I5 5 5 58 8 8 80 0 0 0	4 	4 	4 	4	: 	: 	: 	:0 0 0 0 0/ / / / / /r   r    r   c                     t           S r   )_meterr   r   r   meterro      s    Mr   )r   r    )rk   
__future__r   r'   r)   dataclassesr   ra   r4   r   r    rn   ro   r   r   r   <module>rr      s   * *X # " " " " "      ! ! ! ! ! !
 P P P P P P P Pr r r r r r r rn 
     r   