Ë
    âß²jÂa  ã                  óÜ   — 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Zddl	m
Z
mZmZmZmZ  ej                  d«      ZdZdd„Z ed¬	«      Z ed
¬	«      ZdZ ed«      Z G d„ d«      Zy)z¯Token/usage accounting mixin for SessionDB: the coalescing background token writer,
per-model usage rows, and billing-route columns. Writer thread state lives on the instance.é    )ÚannotationsN)ÚAnyÚDictÚListÚOptionalÚTupleÚhermes_state)Úinput_tokensÚoutput_tokensÚcache_read_tokensÚcache_write_tokensÚreasoning_tokensc           	     ó¢   ‡ ‡— dˆ fd„Šdˆ fd„}dj                  ˆfd„t        D «       «      }‰ rdnd}d|z   d|› d	 |d
«      › d |d«      › d�z   S )zú``UPDATE sessions`` for one usage report: *delta* adds to the stored counters (CLI
    per-call path), otherwise sets them (gateway cumulative path). Cost/route columns
    COALESCE-fill either way (statement text is pinned by the SQL trace harness).c                ó   •— ‰r| › d�S dS )Nz + ?ú?© ©ÚcolÚdeltas    €ú7/home/ubuntu/.hermes/hermes-agent/hermes_state_usage.pyÚaddz_token_update_sql.<locals>.add   s   ø€ Ù$�#��dˆ|Ð-¨#Ð-ó    c                ó   •— ‰rd| › d�S dS )Nz	COALESCE(z, 0) + ?r   r   r   s    €r   Úadd0z_token_update_sql.<locals>.add0   s   ø€ Ù,1�˜3˜%˜xÐ(Ð:°sÐ:r   Ú c              3  ó<   •K  — | ]  }d |› d ‰|«      › d�–— Œ y­w)z                   z = z,
Nr   )Ú.0Úcr   s     €r   ú	<genexpr>z$_token_update_sql.<locals>.<genexpr>   s%   øè ø€ ÒXÀ1Ð,¨Q¨C¨s±3°q³6°(¸#Ô>ÑXùs   ƒz0COALESCE(estimated_cost_usd, 0) + COALESCE(?, 0)zCOALESCE(?, 0)zUPDATE sessions SET
z(                   estimated_cost_usd = zƒ,
                   actual_cost_usd = CASE
                       WHEN ? IS NULL THEN actual_cost_usd
                       ELSE Úactual_cost_usdaì  
                   END,
                   cost_status = COALESCE(?, cost_status),
                   cost_source = COALESCE(?, cost_source),
                   pricing_version = COALESCE(?, pricing_version),
                   billing_provider = COALESCE(billing_provider, ?),
                   billing_base_url = COALESCE(billing_base_url, ?),
                   billing_mode = COALESCE(billing_mode, ?),
                   model = COALESCE(model, ?),
                   api_call_count = Úapi_call_countz 
                   WHERE id = ?)r   ÚstrÚreturnr"   )ÚjoinÚ_TOKEN_COUNTERS)r   r   ÚcountersÚ	estimatedr   s   `   @r   Ú_token_update_sqlr(      sv   ù€ õ.õ;à�w‰wÓXÌÔXÓX€HÙFKÑBÐQa€Ià (Ñ*Ø6°y°kð Bñ "Ð"3Ó4Ð5ð 	6%ñ &*Ð*:Ó%;Ð$<ð = ð#ñ	#ðr   F)r   Ta·  INSERT INTO session_model_usage (
                   session_id, model, billing_provider, billing_base_url, billing_mode,
                   task, api_call_count, input_tokens, output_tokens,
                   cache_read_tokens, cache_write_tokens, reasoning_tokens,
                   estimated_cost_usd, actual_cost_usd, cost_status, cost_source,
                   first_seen, last_seen
               ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
               ON CONFLICT(session_id, model, billing_provider, billing_base_url, billing_mode, task)
               DO UPDATE SET
                   api_call_count = api_call_count + excluded.api_call_count,
                   input_tokens = input_tokens + excluded.input_tokens,
                   output_tokens = output_tokens + excluded.output_tokens,
                   cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
                   cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
                   reasoning_tokens = reasoning_tokens + excluded.reasoning_tokens,
                   estimated_cost_usd = estimated_cost_usd + excluded.estimated_cost_usd,
                   actual_cost_usd = actual_cost_usd + excluded.actual_cost_usd,
                   cost_status = COALESCE(excluded.cost_status, cost_status),
                   cost_source = COALESCE(excluded.cost_source, cost_source),
                   last_seen = excluded.last_seen)ÚmodelÚbilling_providerÚbilling_base_urlÚbilling_moder
   r   r   r   r   Úestimated_cost_usdr    Úcost_statusÚcost_sourcer!   c                  óª  — e Zd ZdZddœ	 	 	 	 	 	 	 	 	 dd„Zdd„Zdd„Zddd„Zdd„Zdd	„Z	dd
„Z
d d!d„Zdd„Z	 	 	 	 	 	 d"	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d#d„Zddddddddddddddddœ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d$d„Zdddddddddddœ
	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d%d„Zd&d„Zdddœd'd„Zy)(ÚSessionUsageMixinz<Coalesced token writer, per-model usage rows, billing route.N)r,   c               ób   ‡ ‡‡‡‡— ‰ j                  «        ˆˆˆˆ ˆfd„}‰ j                  |«       y)a
  Unconditionally set the billing route (``update_token_counts`` only COALESCE-fills
        NULLs) so the dashboard reflects the latest /model switch; also nulls
        ``system_prompt`` so the cached snapshot header is rebuilt.

        See #48173, #48248.
        c                óT   •— | j                  d‰‰‰‰f«       ‰j                  | «       y )Na  UPDATE sessions SET
                   billing_provider = ?,
                   billing_base_url = ?,
                   billing_mode = COALESCE(?, billing_mode),
                   system_prompt = NULL,
                   system_prompt_hash = NULL
                   WHERE id = ?)ÚexecuteÚ#_delete_unreferenced_system_prompts)ÚconnÚbase_urlr,   ÚproviderÚselfÚ
session_ids    €€€€€r   Ú_doz;SessionUsageMixin.update_session_billing_route.<locals>._doa   s3   ø€ Ø�L‰Lð #ð &.¨x¸ÀzÐ$RôTð ×4Ñ4°TÕ:r   N)Úflush_token_countsÚ_execute_write)r9   r:   r8   r7   r,   r;   s   ````` r   Úupdate_session_billing_routez.SessionUsageMixin.update_session_billing_routeU   s+   ü€ ð 	×ÑÔ!÷	;ð 	;ð 	×Ñ˜CÕ r   c                óH  ‡— | j                   5  | j                  }|duxr |j                  «       }| j                  xr | }|sµ| j                  j                  ||f«       |s|t        j                  | j                  dd¬«      }|| _        |j                  «        | j                  €7t        j                  | «      Šdˆfd„}|| _
        t        j                  |«       | j                   j                  «        ddd«       r | j                   |fi |¤Ž yy# 1 sw Y   Œ xY w)zÆEnqueue a token/cost delta for the background writer (same kwargs as
        :meth:`update_token_counts`). After close() stopped the writer, falls back to the
        synchronous path and may raise.Nzsession-db-token-writerT)ÚtargetÚnameÚdaemonc                 ó:   •—  ‰«       } | �| j                  «        y y ©N)Ú_drain_token_queue_at_exit)ÚdbÚself_refs    €r   Ú_drain_at_exitz<SessionUsageMixin.queue_token_counts.<locals>._drain_at_exit€   s    ø€ Ù!)£˜BØ!˜~Ø "× =Ñ =Õ ?ð  .r   ©r#   ÚNone)Ú_token_queue_condÚ_token_writer_threadÚis_aliveÚ_token_writer_stopÚ_token_queueÚappendÚ	threadingÚThreadÚ_token_writer_loopÚstartÚ_token_atexit_hookÚweakrefÚrefÚatexitÚregisterÚ
notify_allÚupdate_token_counts)r9   r:   ÚkwargsÚthreadÚwriter_aliveÚwriter_stoppedrH   rG   s          @r   Úqueue_token_countsz$SessionUsageMixin.queue_token_countsl   s  ø€ ð ×#Ñ#ñ 	4Ø×.Ñ.ˆFØ!¨Ð-ÒC°&·/±/Ó2CˆLØ!×4Ñ4ÒI¸\Ð9IˆNÙ!Ø×!Ñ!×(Ñ(¨*°fÐ)=Ô>Ù#ô '×-Ñ-Ø#×6Ñ6Ð=VÐ_côe�Fà06�DÔ-Ø—L‘L”NØ×.Ñ.Ð6Ü#*§;¡;¨tÓ#4˜õ@ð
 3A˜Ô/ÜŸ™¨Ô7Ø×&Ñ&×1Ñ1Ô3÷/	4ñ0 à$ˆD×$Ñ$ ZÑ:°6Ó:ð ÷1	4ð 	4ús   ŽC+DÄD!c                ó:  — 	 | j                  |«       | j                  5  d| _        | j                  j                  «        ddd«       y# 1 sw Y   yxY w# | j                  5  d| _        | j                  j                  «        ddd«       w # 1 sw Y   w xY wxY w)zKApply a batch whose ``busy`` flag the caller already claimed, then release.FN)Ú_apply_token_batchrK   Ú_token_writer_busyrZ   )r9   Úbatchs     r   Ú_apply_claimed_batchz&SessionUsageMixin._apply_claimed_batchŒ   s„   € ð	4Ø×#Ñ# EÔ*à×'Ñ'ñ 4Ø*/�Ô'Ø×&Ñ&×1Ñ1Ô3÷4÷ 4ñ 4û�×'Ñ'ñ 4Ø*/�Ô'Ø×&Ñ&×1Ñ1Ô3÷4÷ 4ñ 4ýs.   ‚A Ÿ"A
Á
AÁBÁ#"BÂ	BÂBÂBc                óx  — | j                   s| j                  syd}| j                  5  t        j                  «       |z   }| j                   s| j                  r¼| j
                  }|�|j                  «       sC| j                  s7d| _        t        | j                   «      }| j                   j                  «        n[|t        j                  «       z
  }|dk  r
	 ddd«       y| j                  j                  |«       | j                   rŒ¯| j                  rŒ¼ddd«       |r| j                  |«       y# 1 sw Y   ŒxY w)z”Block until every queued token delta has been applied. False on timeout (callers
        then read totals stale by the queued deltas). Never raises.TNr   F)rO   rc   rK   ÚtimeÚ	monotonicrL   rM   ÚlistÚclearÚwaitre   )r9   Útimeoutrd   Údeadliner]   Ú	remainings         r   r<   z$SessionUsageMixin.flush_token_counts•   s  € ð × Ò ¨×)@Ò)@ØØˆØ×#Ñ#ñ 	7Ü—~‘~Ó'¨'Ñ1ˆHØ×#Ò# t×'>Ò'>ð
 ×2Ñ2�Ø�N¨&¯/©/Ô*;ÀT×E\ÒE\Ø.2�DÔ+Ü  ×!2Ñ!2Ó3�EØ×%Ñ%×+Ñ+Ô-ØØ$¤t§~¡~Ó'7Ñ7�	Ø ’>Ø ÷	7ð 	7ð  ×&Ñ&×+Ñ+¨IÔ6ð ×#Ó# t×'>Ó'>÷	7ñ" Ø×%Ñ% eÔ,Ø÷'	7ð 	7ús   ¨B.D0Ã'D0ÄD0Ä0D9c                óF  — 	 | j                   5  t        j                  «       | j                  z   }| j                  sm| j
                  sa|t        j                  «       z
  }|dk  rd | _        	 d d d «       y | j                   j                  |«       | j                  s| j
                  sŒa| j                  sd | _        	 d d d «       y d| _        t        | j                  «      }| j                  j                  «        d d d «       | j                  «       �Œ# 1 sw Y   ŒxY w)NTr   )rK   rg   rh   Ú_TOKEN_WRITER_IDLE_SECONDSrO   rN   rL   rk   rc   ri   rj   re   )r9   Úidle_deadlinern   rd   s       r   rS   z$SessionUsageMixin._token_writer_loop±   s  € ØØ×'Ñ'ñ *Ü $§¡Ó 0°4×3RÑ3RÑ R�Ø×+Ò+°D×4KÒ4KØ -´·±Ó0@Ñ @�IØ  A’~ð 59˜Ô1Ø÷*ð *ð ×*Ñ*×/Ñ/°	Ô:ð ×+Ò+°D×4KÓ4Kð ×(Ò(Ø04�DÔ-Ø÷*ð *ð +/�Ô'Ü˜T×.Ñ.Ó/�Ø×!Ñ!×'Ñ'Ô)÷#*ð$ ×%Ñ% eÔ,ñ' ÷*ð *ús   ŽADÁ53DÂ)DÃ6DÄD c                ó  — 	 | j                  |«      }|D ]  \  }}	  | j                  |fi |¤Ž Œ y# t        $ r"}t        j                  d|«       |}Y d}~ŒEd}~ww xY w# t        $ r!}t        j                  d||«       Y d}~Œpd}~ww xY w)zBApply queued deltas in order, coalescing where safe. Never raises.z?async token accounting: coalesce failed, applying raw batch: %sNz5async token accounting: apply failed (session=%s): %s)Ú_coalesce_token_deltasÚ	ExceptionÚloggerÚwarningr[   )r9   rd   Ú	coalescedÚexcr:   r\   s         r   rb   z$SessionUsageMixin._apply_token_batchÇ   sš   € ð	Ø×3Ñ3°EÓ:ˆIð
 #,ò 	iÑˆJ˜ðiØ(�×(Ñ(¨Ñ>°vÓ>ñ	iøô	 ò 	ä�N‰NÐ\Ð^aÔbØ�Iûð	ûô ò iä—‘ÐVÐXbÐdg×hÑhûðiús+   ‚2 œA ²	A»AÁAÁ 	B
Á)BÂB
c                ó  ‡— g }|D ]ß  \  }Šd}‰j                  d«      s|gˆfd„| j                  D «       ¢­}|r�|��|d   d   |k(  r‚|d   d   }| j                  D ]*  }|j                  |d«      ‰j                  |d«      z   ||<   Œ, | j                  D ]1  }‰j                  |«      }|€Œ|j                  |«      xs d|z   ||<   Œ3 ŒÃ|j	                  ||t        ‰«      f«       Œá |D ��	�
cg c]
  \  }}	}
|	|
f‘Œ c}
}	}S c c}
}	}w )z¤Merge adjacent incremental deltas with an identical route, so ordering across
        sessions and /model switches is preserved exactly. absolute=True never merges.NÚabsolutec              3  ó@   •K  — | ]  }‰j                  |«      –— Œ y ­wrD   )Úget)r   Úfr\   s     €r   r   z;SessionUsageMixin._coalesce_token_deltas.<locals>.<genexpr>Ý   s   øè ø€ Ò$[°q V§Z¡Z°§]Ñ$[ùs   ƒéÿÿÿÿr   é   ç        )r|   Ú_TOKEN_DELTA_ROUTE_FIELDSÚ_TOKEN_DELTA_SUM_FIELDSÚ_TOKEN_DELTA_COST_FIELDSrP   Údict)r9   rd   Úgroupsr:   ÚkeyÚmergedr}   ÚvalueÚ_ÚsidÚkwr\   s              @r   rs   z(SessionUsageMixin._coalesce_token_deltasÖ   s)  ø€ ð EGˆØ"'ò 	?ÑˆJ˜ØˆCØ—:‘:˜jÔ)Ø!Ð\Ó$[¸D×<ZÑ<ZÔ$[Ñ\�Ù˜#˜/¨f°R©j¸©m¸sÒ.BØ ™ A™�Ø×5Ñ5ò D�AØ &§
¡
¨1¨aÓ 0°6·:±:¸aÀÓ3CÑ C�F˜1’IðDà×6Ñ6ò C�AØ"ŸJ™J q›M�EØÑ(à%+§Z¡Z°£]Ò%9°c¸UÑ$B˜˜qš	ñ	Cð —‘˜s J´°V³Ð=Õ>ð	?ð -3×3Ð3™j˜a  b��b’	Ô3Ð3ùÔ3s   Ã.Dc                óp  — | j                   5  d| _        | j                   j                  «        | j                  }ddd«       �]|j	                  «       rM|j                  |¬«       |j	                  «       r+t        j                  d|t        | j                  «      «       y| j                   5  t        j                  «       |z   }| j                  rx|t        j                  «       z
  }|dk  r4t        j                  d|t        | j                  «      «       	 ddd«       y| j                   j                  |«       | j                  rŒxt        | j                  «      }|r!d| _        | j                  j                  «        ddd«       r| j!                  |«       yy# 1 sw Y   �ŒfxY w# 1 sw Y   Œ+xY w)z@Stop the writer thread and drain remaining deltas. Never raises.TN)rl   zZasync token accounting: writer did not stop within %.0fs; %d queued delta(s) not persistedr   zfasync token accounting: concurrent drain did not finish within %.0fs; %d queued delta(s) not persisted)rK   rN   rZ   rL   rM   r$   ru   rv   ÚlenrO   rg   rh   rc   rk   ri   rj   re   )r9   Újoin_timeoutr]   rm   rn   rd   s         r   Ú_stop_token_writerz$SessionUsageMixin._stop_token_writerë   s€  € à×#Ñ#ñ 	/Ø&*ˆDÔ#Ø×"Ñ"×-Ñ-Ô/Ø×.Ñ.ˆF÷	/ð Ð &§/¡/Ô"3Ø�K‰K ˆKÔ-Ø�‰Ô ä—‘ð7Ø8DÄcÈ$×J[ÑJ[ÓF\ô^ð ð ×#Ñ#ñ 	*Ü—~‘~Ó'¨,Ñ6ˆHØ×)Ò)Ø$¤t§~¡~Ó'7Ñ7�	Ø ’>Ü—N‘NðPà$¤c¨$×*;Ñ*;Ó&<ô>ð ÷	*ð 	*ð ×&Ñ&×+Ñ+¨IÔ6ð ×)Ó)ô ˜×*Ñ*Ó+ˆEÙØ*.�Ô'Ø×!Ñ!×'Ñ'Ô)÷	*ñ  Ø×%Ñ% eÕ,ð ÷?	/ñ 	/ú÷	*ð 	*ús$   �.FÂ.A+F,Ä"'F,Å
8F,ÆF)Æ,F5c                ó€   — t        j                  t        «      5  | j                  «        d d d «       y # 1 sw Y   y xY wrD   )Ú
contextlibÚsuppressrt   r�   )r9   s    r   rE   z,SessionUsageMixin._drain_token_queue_at_exit  s1   € Ü× Ñ ¤Ó+ñ 	&Ø×#Ñ#Ô%÷	&÷ 	&ñ 	&ús   š4´=r   Fc                óÔ  ‡ ‡‡‡‡‡‡‡‡‡‡— t        «       j                  «       D ��ci c]  \  }}|t        v sŒ||“Œ c}}Š‰ j                  ‰|xs d‰¬«       |rt        nt
        Št        |xs |xs |xs |xs
 |xs |xs |«      }t        |xs |	«      Š|||||||	|	|
||‰r‰nd‰r‰nd‰r‰nd‰r‰nd|‰fŠ| xr |Šˆˆˆˆˆˆˆˆ ˆˆˆfd„}‰ j                  |«       yc c}}w )a]  Update token counters and backfill model if unset. *absolute*=False increments
        (per-API-call deltas, CLI path); *absolute*=True sets directly (gateway path,
        where the cached agent holds cumulative totals). ``source`` is the session's real surface
        for the row-existence guard; callers that don't know it leave the placeholder.Úunknown)r)   Nc                óÀ  •— | j                  d‰f«      j                  «       }|�t        |«      ni }t        |j	                  d«      xs d«      dk(  xrH ‰xrD t        ‰«      xr7 t        ‰«      xr* |j	                  d«      ‰k7  xs |j	                  d«      ‰k7  }|r| j                  d‰‰‰‰‰f«       | j                  ‰‰	«       ‰
r ‰j                  | ‰fi ‰¤Ž y y )NzISELECT model, billing_provider, api_call_count FROM sessions WHERE id = ?r!   r   r)   r*   z­UPDATE sessions
                       SET model = ?, billing_provider = ?,
                       billing_base_url = ?, billing_mode = ?
                       WHERE id = ?)r4   Úfetchoner„   Úintr|   ÚboolÚ_record_model_usage)r6   ÚrowÚexistingÚfirst_accounted_router+   r,   r*   Úhas_accounted_usager)   ÚparamsÚrecord_model_usager9   r:   ÚsqlÚusages       €€€€€€€€€€€r   r;   z2SessionUsageMixin.update_token_counts.<locals>._do<  s  ø€ Ø—,‘,Ø[Ð^hÐ]jóç‰h‹jð ð %( O”t˜C”y¸ˆHô
 �H—L‘LÐ!1Ó2Ò7°aÓ8¸AÑ=ò mÐBUò mÔZ^Ð_dÓZeò mÜÐ)Ó*òmà—\‘\ 'Ó*¨eÑ3Òk°x·|±|ÐDVÓ7WÐ[kÑ7kð "ñ
 %Ø—‘ð 'ð */Ð0@ÐBRÐT`ÐblÐ(môoð �L‰L˜˜fÔ%Ù!Ø(�×(Ñ(¨¨zÑC¸UÓCð "r   )ÚlocalsÚitemsÚ_MODEL_USAGE_FIELDSÚ_insert_session_rowÚ_TOKEN_UPDATE_ABSOLUTE_SQLÚ_TOKEN_UPDATE_DELTA_SQLr˜   r=   )r9   r:   r
   r   r)   r   r   r   r-   r    r.   r/   Úpricing_versionr*   r+   r,   r!   rz   ÚsourceÚkÚvÚ	has_usager;   r�   rž   rŸ   r    r¡   s   ``  `        ```       @@@@@r   r[   z%SessionUsageMixin.update_token_counts  s!  ÿú€ ô #)£(§.¡.Ó"2×O™$˜!˜Q°aÔ;NÒ6N��A‘ÓOˆð 	× Ñ  ¨VÒ-@°yÈÐ ÔNÙ,4Õ(Ô:QˆÜ˜ò A¨ò AÐ:Kò AÐOaò AÐeuò AØ*òAØ.@óBˆ	ä" 9Ò#?°Ó@Ðà˜-Ð):Ð<NÐP`Ø °À+È{Ð\kÙ 3Ñ¸Ù 3Ñ¸Ù/‰L°TÑDW¹5Ð]aØ˜Jð(ˆð  #+˜lÒ9°	Ð÷	Dö 	Dð* 	×Ñ˜CÕ ùóe Ps
   §C$¸C$r   )r)   r*   r+   r,   r
   r   r   r   r   r-   r    r.   r/   r!   Útaskc               ó  — |j                  d|f«      j                  «       }|�|st        |«      ni }|||	|
|fD �cg c]  }|xs d‘Œ
 }}t        j                  «       }|j                  t        ||xs |j                  d«      xs d|xs |j                  d«      xs d|xs |j                  d«      xs d|xs |j                  d	«      xs d|xs d|xs dg|¢t        |xs d
«      ‘t        |xs d
«      ‘|‘|‘|‘|‘­«       yc c}w )u›  Accumulate a per-API-call usage delta into session_model_usage, inside the caller's
        write txn after the ``sessions`` UPDATE. A missing model/provider falls back to
        the session row â€” except for aux rows (``task`` set), which must NOT inherit the
        main-loop route (vision on gemini while the main loop runs anthropic): missing
        info stays 'unknown'/empty.

        ``task`` distinguishes what kind of work consumed the tokens: ``''`` (empty) is the main agent loop;
        auxiliary calls record their task name (``vision``, ``compression``, ``title_generation``, ...) via
        :meth:`record_auxiliary_usage` (issue #23270).
        zYSELECT model, billing_provider, billing_base_url, billing_mode FROM sessions WHERE id = ?Nr   r)   r”   r*   r   r+   r,   r€   )r4   r–   r„   rg   Ú_MODEL_USAGE_UPSERT_SQLr|   Úfloat)r9   r6   r:   r)   r*   r+   r,   r
   r   r   r   r   r-   r    r.   r/   r!   r­   rš   Úsessr«   ÚcountsÚnows                          r   r™   z%SessionUsageMixin._record_model_usageS  s\  € ð" �l‰lØgÐjtÐivó
ç
‰(‹*ð 	ð !˜_±TŒt�CŒyÀˆØ#/°Ð@QÐSeÐgwÐ"xÖy˜Q�!’&�q‘&ÐyˆÐyÜ�i‰i‹kˆØ�‰Ô,Ø˜Ò? §¡¨'Ó!2Ò?°iØÒB §¡Ð);Ó <ÒBÀØÒB §¡Ð);Ó <ÒBÀØÒ:˜DŸH™H ^Ó4Ò:¸¸DºJÀBÈÒH[ÐZ[ð	/qð _eð	/qô
 Ð$Ò+¨Ó,ð/qô
 /4°OÒ4JÀsÓ.Kð/qð
 NYð/qð
 [fð/qð
 hkð/qð
 mpñ/qõ 	rùò zs   ¼D
é   )
r)   r*   r+   r
   r   r   r   r   r-   r!   c       
        ó  ‡ ‡‡‡— t        «       j                  «       D ��ci c]  \  }}|t        v sŒ||“Œ c}}Š‰r‰sy|€dn
t        |«      ‰d<   ‰ j	                  ‰d«       ‰ j                  ˆ ˆˆˆfd„«       yc c}}w )a  Record an auxiliary LLM call's usage (vision, compression, title generation, ...)
        as a per-(model, provider, task) delta in ``session_model_usage`` WITHOUT touching
        the ``sessions`` summary row (the gateway overwrites those counters with absolute
        main-loop totals). ``api_call_count`` may aggregate N calls. Best-effort.

        See #23270.
        Background-review forks record an aggregate of N fork API calls in one write with
        ``task='background_review'`` (issue #87250).
        Nr´   r!   r”   c                ó0   •—  ‰j                   | ‰fd‰i‰¤ŽS )Nr­   )r™   )r6   r9   r:   r­   r¡   s    €€€€r   ú<lambda>z:SessionUsageMixin.record_auxiliary_usage.<locals>.<lambda>‡  s#   ø€ Ð)A¨×)AÑ)AÀ$È
Ñ)gÐY]Ð)gÐafÑ)g€ r   )r¢   r£   r¤   r—   r¥   r=   )r9   r:   r­   r)   r*   r+   r
   r   r   r   r   r-   r!   rª   r«   r¡   s   ```            @r   Úrecord_auxiliary_usagez(SessionUsageMixin.record_auxiliary_usageq  st   û€ ô #)£(§.¡.Ó"2×O™$˜!˜Q°aÔ;NÒ6N��A‘ÓOˆÙ¡ØØ'5Ð'=¡!Ä3À~ÓCVˆÐÑð 	× Ñ  ¨YÔ7Ø×ÑÖgÕhùó Ps
    A;±A;c                óŽ  — |si S | j                  |«      }t        t        |«      dz
  dd«      D ]  }| j                  ||   «      sŒ||d } n | j	                  ddj                  dt        |«      z  «      › d�|«      }|D ��ci c].  }|d   |j                  «       D �ci c]  }|dk7  sŒ	|||   “Œ c}“Œ0 c}}S c c}w c c}}w )	a  Per-task auxiliary usage (``task != ''``: vision, compression, title_generation, ...) summed
        over the session's compression lineage. Aux calls bill to the id the turn STARTED with while
        compression mints child ids mid-turn, so a single-id read misses rows (#112848).r´   r~   Nay  SELECT task,
                       COALESCE(SUM(api_call_count), 0) AS api_calls,
                       COALESCE(SUM(input_tokens), 0) AS input_tokens,
                       COALESCE(SUM(output_tokens), 0) AS output_tokens,
                       COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
                       COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
                       COALESCE(SUM(reasoning_tokens), 0) AS reasoning_tokens,
                       COALESCE(SUM(estimated_cost_usd), 0) AS estimated_cost_usd
                  FROM session_model_usage
                 WHERE session_id IN (ú,r   z/) AND task != ''
                 GROUP BY taskr­   )Ú_session_lineage_root_to_tipÚranger�   Ú_is_explicit_branch_sessionÚ	_read_allr$   Úkeys)r9   r:   ÚchainÚiÚrowsrš   rª   s          r   Úauxiliary_usage_by_taskz)SessionUsageMixin.auxiliary_usage_by_task‰  sá   € ñ ØˆIØ×1Ñ1°*Ó=ˆô ”s˜5“z A‘~ r¨2Ó.ò 	ˆAØ×/Ñ/°°a±Õ9Ø˜a˜b˜	�Ùð	ð �~‰~ð	'ð (+§x¡x°´c¸%³jÑ0@Ó'AÐ&Bð Cð
"ð ó
ˆð X\×\ÐPS��F‘°·±³ÖK¨A¸qÀF»{˜a  Q¡™iÒKÑKÓ\Ð\ùÒKùÓ\s   ÂCÂ 
B<Â+B<Â3CÂ<C)Úmin_message_countÚinclude_archivedc               óÊ   — ddg}|g}|s|j                  d«       | j                  ddj                  |«      › d�|«      }t        |d   xs d«      t	        |d   xs d	«      d
œS )z Tokens and spend across the whole store (one scan), so the sidebar total does not
        shrink with paging. Spend prefers the billed figure over the estimate.zparent_session_id IS NULLzmessage_count >= ?zCOALESCE(archived, 0) = 0zã
            SELECT COALESCE(SUM(COALESCE(input_tokens, 0) + COALESCE(output_tokens, 0)), 0),
                   COALESCE(SUM(COALESCE(actual_cost_usd, estimated_cost_usd, 0)), 0)
              FROM sessions
             WHERE z AND z
            r   r´   r€   )ÚtokensÚcost_usd)rP   Ú	_read_oner$   r—   r°   )r9   rÄ   rÅ   Úwhererž   rš   s         r   Úusage_totalszSessionUsageMixin.usage_totals¦  s�   € ð -Ð.BÐCˆØ.Ð/ˆÙØ�L‰LÐ4Ô5Ø�n‰nð "ð —L‘L Ó'Ð(ð )ð	ð
 óˆô ˜c !™fšk¨Ó*¼¸cÀ!¹fºmÈÓ8LÑMÐMr   )
r:   r"   r8   r"   r7   r"   r,   úOptional[str]r#   rJ   )r:   r"   r#   rJ   rI   )g      @)rl   r°   r#   r˜   )rd   ú List[Tuple[str, Dict[str, Any]]]r#   rJ   )rd   rÍ   r#   rÍ   )g      $@)rŽ   r°   r#   rJ   )r   r   Nr   r   r   NNNNNNNNr   FN)&r:   r"   r
   r—   r   r—   r)   r"   r   r—   r   r—   r   r—   r-   úOptional[float]r    rÎ   r.   rÌ   r/   rÌ   r¨   rÌ   r*   rÌ   r+   rÌ   r,   rÌ   r!   r—   rz   r˜   r©   rÌ   r#   rJ   )"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—   r­   r"   r#   rJ   )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#   rJ   )r:   r"   r#   zDict[str, Dict[str, float]])rÄ   r—   rÅ   r˜   r#   zDict[str, float])Ú__name__Ú
__module__Ú__qualname__Ú__doc__r>   r`   re   r<   rS   rb   rs   r�   rE   r[   r™   r¸   rÃ   rË   r   r   r   r1   r1   R   sß  „ ÙFð _cñ!Øð!Ø,/ð!Ø;>ð!ØN[ð!à	ó!ó.;ó@4ôó8-ó,ió4ô*"-óH&ð
 stØ`dØkoØswØPUØ"ð>!Øð>!Ø-0ð>!ØCFð>!ØQTð>!Ønqð>!àð>!à58ð>!àP_ð>!ð )ð>!ð =Jð>!ð ^kð>!ð 'ð	>!ð @Mð	>!ð fsð	>!ð
 $ð>!ð
 ;>ð>!ð
 LPð>!ð ð>!ð 
ó>!ðB >BÐcgØ(,È$ÐbcØ°QÐPQÐijØ,0ÐSWØ#'ÀDÐ^_ÐkmñrØ #ðrØ/<ðrØUbðrà'ðrà<Iðrà^aðrð ðrð 25ðrð MPðrð fiðrð ,ð	rð DSð	rð
 #ðrð
 7Dðrð
 [^ðrð
 hkðrð 
órð> CGÐhlØ(,ÀÐVWÐpqØ !¸Ð`dØñ	iØðiØ%(ðiØ4AðiØZgðià'ðià<?ðiàRUðiàloðið  ðið 69ðið Q`ðið ð	ið
 
óió0]ð: 89ÐSXö Nr   r1   )r   r˜   r#   r"   )rÒ   Ú
__future__r   rX   r‘   ÚloggingrQ   rg   rV   Útypingr   r   r   r   r   Ú	getLoggerru   r%   r(   r¦   r§   r¯   Ú	frozensetr¤   r1   r   r   r   ú<module>rØ      sˆ   ðñ_õ #ã Û Û Û Û Û ß 3Õ 3ð 
ˆ×	Ñ	˜>Ó	*€àr€óñ: /°UÔ;Ð Ù+°$Ô7Ð ð5Ð ñ0  ð !Gó HÐ ÷aNò aNr   