Ë
    tPêj‡1  ã                   óR  — U d Z ddlmZ ddlmZmZmZ ddlmZmZm	Z	 ddl
mZ ddlmZ ddlmZmZmZ erddlmZ d	Zeed
<   dZeed<   dZeed<   dZeed<   dZeed<   dZeed<   dZeed<    ed¬«      Zeed<    G d„ de	«      Zdddeded ed!ee   d"ed#z  d$ed#z  fd%„Z d&ed$efd'„Z!d(ed$e"eeef   fd)„Z#ddd*e"ed+f   d$e$e"eeef      fd,„Z%ddd(ed-e$e"eeef      d.ed$ef
d/„Z&d(ed$ed#z  fd0„Z'd(ed1ed2ed$e"eeeeeeef   fd3„Z(e fddd*ee   d.ed$d#fd4„Z)dddeded5ed6ed$d#fd7„Z*y#)8aà  
Writer for LiteLLM_BudgetWindowSpend.

The table holds one row per configured budget window whose window_start rolls
forward in place, so budget enforcement can read a maintained running total
instead of aggregating LiteLLM_SpendLogs every time a window counter goes cold
(issue #35766). Raw SQL rather than the Prisma upsert helper because the
conditional roll cannot be expressed through the query builder.

Seeding a row that does not exist yet reads LiteLLM_SpendLogs once, excluding
the requests whose increments are in the same batch so neither source counts
them twice. One gap survives that exclusion: without the Redis transaction
buffer every pod flushes its own increments, so a row seeded by one pod can
include spend logs whose increments are still queued on another pod, and those
increments are added again when that pod flushes. That is bounded by a single
flush interval, happens at most once per window row, and only ever over-counts:
the seed never omits spend, because every increment not yet in the row still
reaches it on its own pod's next flush. A row therefore lags real spend by at
most one flush interval of queued increments, the same lag the SpendLogs
aggregate it replaces (and every other spend column) already has.
é    )ÚSequence)ÚdatetimeÚ	timedeltaÚtimezone)ÚTYPE_CHECKINGÚFinalÚProtocol)Úverbose_proxy_logger)ÚLitellm_EntityType)ÚWindowSpendTransactionÚto_naive_utcÚwindow_spend_group_key)ÚPrismaClientz½SELECT entity_type, entity_id, window_duration FROM "LiteLLM_BudgetWindowSpend" WHERE (entity_type, entity_id, window_duration) IN (SELECT * FROM unnest($1::text[], $2::text[], $3::text[]))Ú_SELECT_EXISTING_ROWS_SQLaq  INSERT INTO "LiteLLM_BudgetWindowSpend" (entity_type, entity_id, window_duration, window_start, spend, created_at, updated_at) VALUES ($1, $2, $3, ($4::timestamptz AT TIME ZONE 'UTC'), $5, ($7::timestamptz AT TIME ZONE 'UTC'), ($7::timestamptz AT TIME ZONE 'UTC')) ON CONFLICT (entity_type, entity_id, window_duration) DO UPDATE SET spend = CASE WHEN "LiteLLM_BudgetWindowSpend".window_start >= EXCLUDED.window_start THEN "LiteLLM_BudgetWindowSpend".spend + $6 ELSE EXCLUDED.spend END, window_start = GREATEST("LiteLLM_BudgetWindowSpend".window_start, EXCLUDED.window_start), updated_at = ($7::timestamptz AT TIME ZONE 'UTC')Ú_UPSERT_WINDOW_SPEND_SQLa  UPDATE "LiteLLM_BudgetWindowSpend" SET window_start = ($4::timestamptz AT TIME ZONE 'UTC'), spend = 0, updated_at = ($5::timestamptz AT TIME ZONE 'UTC') WHERE entity_type = $1 AND entity_id = $2 AND window_duration = $3 AND window_start < ($4::timestamptz AT TIME ZONE 'UTC')Ú_ROLL_WINDOW_SPEND_SQLzìSELECT COALESCE(SUM(spend), 0.0) AS total FROM "LiteLLM_SpendLogs" WHERE api_key = $1 AND "startTime" >= ($2::timestamptz AT TIME ZONE 'UTC') AND NOT (request_id = ANY($3::text[]) AND "startTime" >= ($4::timestamptz AT TIME ZONE 'UTC'))Ú_SEED_FROM_SPEND_LOGS_KEY_SQLzìSELECT COALESCE(SUM(spend), 0.0) AS total FROM "LiteLLM_SpendLogs" WHERE team_id = $1 AND "startTime" >= ($2::timestamptz AT TIME ZONE 'UTC') AND NOT (request_id = ANY($3::text[]) AND "startTime" >= ($4::timestamptz AT TIME ZONE 'UTC'))Ú_SEED_FROM_SPEND_LOGS_TEAM_SQLz�SELECT COALESCE(SUM(spend), 0.0) AS total FROM "LiteLLM_SpendLogs" WHERE api_key = $1 AND "startTime" >= ($2::timestamptz AT TIME ZONE 'UTC')Ú'_SEED_FROM_SPEND_LOGS_KEY_UNBOUNDED_SQLz�SELECT COALESCE(SUM(spend), 0.0) AS total FROM "LiteLLM_SpendLogs" WHERE team_id = $1 AND "startTime" >= ($2::timestamptz AT TIME ZONE 'UTC')Ú(_SEED_FROM_SPEND_LOGS_TEAM_UNBOUNDED_SQLé<   )ÚsecondsÚ_UPSERT_TRANSACTION_TIMEOUTc                   óF   — e Zd ZdZdddedededee   ded	z  d
ed	z  fd„Zy	)ÚWindowSpendLogsAggregatezäSums LiteLLM_SpendLogs for one entity since window_start, ignoring the
    requests whose ids are handed in.

    Injected so the flush can be exercised without a database and so the
    expensive aggregate stays swappable.
    Úprisma_clientr   Úentity_typeÚ	entity_idÚwindow_startÚexclude_request_idsÚexclude_started_atNÚreturnc              ƒ   ó   K  — y ­w)N© )Úselfr   r   r   r   r    r!   s          úp/home/oem/CyberBrain26-unified/.venv/lib/python3.12/site-packages/litellm/proxy/db/budget_window_spend_writer.pyÚ__call__z!WindowSpendLogsAggregate.__call__f   s   è ø€ ð ùs   ‚)	Ú__name__Ú
__module__Ú__qualname__Ú__doc__Ústrr   r   Úfloatr'   r$   ó    r&   r   r   ^   s\   „ ñðà%ðð ðð ð	ð
 ðð & c™]ðð % t™Oðð 
�‰ôr.   r   r   r   r   r   r   r    r!   Nr"   c           	   ƒ   óÈ  K  — |t         j                  j                  k(  rt        t        }}n+|t         j
                  j                  k(  rt        t        }}ny|�|s%| j                  j                  |||«      ƒ d{  –—† n8| j                  j                  |||t        |«      t        |«      «      ƒ d{  –—† }|syt        |d   j                  d«      xs d«      S 7 Œa7 Œ*­w)aÑ  LiteLLM_SpendLogs spend for one entity since window_start, minus the
    requests already accounted for by the increments being flushed.

    The spend log writer drains its own queue on a ~2s poll whenever anything
    is queued, while window increments flush on the much slower batch tick, so
    by the time a window row is seeded its batch's log rows are normally
    already in the table. Counting them in the seed and again in the increment
    is what made a fresh row land at twice the true spend.

    The exclusion is bounded to rows that started at or after the batch's
    earliest request. request_id can be chosen by the client
    (x-litellm-call-id), so an unbounded exclusion would let a replayed old id
    erase a historical row from the seed while its increment still lands.
    Without a known start the batch's ids are not excluded at all: that can
    only over-count once, which enforcement tolerates, whereas under-counting
    is a budget bypass.
    Nç        r   Útotal)r   ÚKEYÚvaluer   r   ÚTEAMr   r   ÚdbÚ	query_rawÚtupleÚ_exclusion_lower_boundr-   Úget)	r   r   r   r   r    r!   Úbounded_sqlÚunbounded_sqlÚrowss	            r&   Úspend_logs_total_excludingr=   q   sÞ   è ø€ ð2 Ô(×,Ñ,×2Ñ2Ò2Ü%BÔDk�]‰Ø	Ô*×/Ñ/×5Ñ5Ò	5Ü%CÔEm�]‰àð Ð%Ñ-@ð ×Ñ×(Ñ(¨¸	À<ÓP×PÐPà ×#Ñ#×-Ñ-ØØØÜÐ%Ó&Ü"Ð#5Ó6ó
÷ 
ð 	ñ ØÜ��a‘—‘˜WÓ%Ò,¨Ó-Ð-ð 	Qøð
ús$   ‚A:C"Á<CÁ=8C"Â5C Â6)C"Ã C"Ú
started_atc                 ó8   — t        | «      j                  d¬«      S )z”LiteLLM_SpendLogs.startTime is TIMESTAMP(3); floor to the second so a
    millisecond rounding of the batch's own earliest row cannot slip under it.r   )Úmicrosecond)r   Úreplace)r>   s    r&   r8   r8       s   € ô ˜
Ó#×+Ñ+¸Ð+Ó:Ð:r.   Útransactionc                 ó   — | d   | d   | d   fS )Nr   r   Úwindow_durationr$   )rB   s    r&   Ú_primary_keyrE   ¦   s%   € à�MÑ"Ø�KÑ ØÐ%Ñ&ðð r.   Útransactions.c           	   ƒ   óî   K  — | j                   j                  t        t        d„ |D «       «      t        d„ |D «       «      t        d„ |D «       «      «      ƒ d {  –—† }t	        d„ |xs dD «       «      S 7 Œ­w)Nc              3   ó&   K  — | ]	  }|d    –— Œ y­w)r   Nr$   ©Ú.0rB   s     r&   ú	<genexpr>z)_existing_primary_keys.<locals>.<genexpr>´   s   è ø€ ÒI¨[ˆk˜-Õ(ÑIùó   ‚c              3   ó&   K  — | ]	  }|d    –— Œ y­w)r   Nr$   rI   s     r&   rK   z)_existing_primary_keys.<locals>.<genexpr>µ   s   è ø€ ÒG¨;ˆk˜+Õ&ÑGùrL   c              3   ó&   K  — | ]	  }|d    –— Œ y­w)rD   Nr$   rI   s     r&   rK   z)_existing_primary_keys.<locals>.<genexpr>¶   s   è ø€ ÒM°ˆkÐ+Õ,ÑMùrL   c              3   ó8   K  — | ]  }|d    |d   |d   f–— Œ y­w)r   r   rD   Nr$   )rJ   Úrows     r&   rK   z)_existing_primary_keys.<locals>.<genexpr>¸   s(   è ø€ ÒjÐX[�c˜-Ñ(¨#¨kÑ*:¸CÐ@QÑ<RÔSÑjùs   ‚r$   )r5   r6   r   r7   Ú	frozenset)r   rF   r<   s      r&   Ú_existing_primary_keysrR   ®   so   è ø€ ð &×(Ñ(×2Ñ2Ü!ÜÑI¸LÔIÓIÜÑG¸,ÔGÓGÜÑMÀÔMÓMó	÷ €Dô ÑjÐ_cÒ_iÐgiÔjÓjÐjðús   ‚AA5ÁA3ÁA5Úexisting_primary_keysÚspend_logs_aggregatec           
   ƒ   ó   K  — t        |«      |v ry || |d   |d   t        j                  |d   «      j                  t        j
                  ¬«      |d   t        |«      ¬«      ƒ d{  –—† }t        |xs d«      S 7 Œ­w)	aO  Spend already recorded for a window that has no row yet.

    This is the LiteLLM_SpendLogs aggregate the window counter reseed runs on
    every cold counter today, but here it runs once per window lifetime and off
    the request path, and it excludes this batch's own requests so they are
    counted by their increments alone.
    r0   r   r   r   ©ÚtzinfoÚrequest_ids)r   r   r   r   r    r!   N)rE   r   ÚfromisoformatrA   r   ÚutcÚ_transaction_started_atr-   )r   rB   rS   rT   Úbases        r&   Ú_seed_base_for_missing_rowr]   »   s‰   è ø€ ô �KÓ Ð$9Ñ9ØÙ,Ø#Ø Ñ.Ø˜kÑ*Ü×+Ñ+¨K¸Ñ,GÓH×PÑPÔX`×XdÑXdÐPÓeØ'¨Ñ6Ü2°;Ó?ô÷ €Dô �’˜ÓÐðús   ‚A&A>Á(A<Á)A>c                 ó�   — | j                  d«      }|€y t        j                  |«      j                  t        j
                  ¬«      S )Nr>   rV   )r9   r   rY   rA   r   rZ   )rB   r>   s     r&   r[   r[   Õ   s<   € Ø#Ÿ™¨Ó5€JØÐØÜ×!Ñ! *Ó-×5Ñ5¼X¿\¹\Ð5ÓJÐJr.   Ú	seed_baseÚnowc                 ót   — t        | d   «      }| d   | d   | d   t        j                  | d   «      ||z   ||fS )a  $5 is what a brand new row starts at (pre-existing spend plus this
    increment); $6 is the increment alone, which is all an already-current row
    may add. They are equal for every row that already existed, so a row is
    never seeded twice when two pods flush the same new window.Úspendr   r   rD   r   )r-   r   rY   )rB   r_   r`   Ú	increments       r&   Ú_upsert_paramsrd   Ü   sW   € ô ˜[¨Ñ1Ó2€Ià�MÑ"Ø�KÑ ØÐ%Ñ&Ü×Ñ˜{¨>Ñ:Ó;Ø�IÑØØðð r.   c              ƒ   ó   K  — |syt        t        |t        ¬«      «      }t        | |¬«      ƒ d{  –—† }t        |D �cg c]  }t	        | |||¬«      ƒ d{  –—† ‘Œ c}«      }t        t        j                  t        j                  «      «      }t        j                  dt        |«      t        |«      «       | j                  j                  t        ¬«      4 ƒd{  –—† }|j!                  «       4 ƒd{  –—† }	t#        ||«      D ](  \  }}
 |	j$                  t&        gt)        ||
|¬«      ¢­Ž  Œ* ddd«      ƒd{  –—†  ddd«      ƒd{  –—†  y7 �Œ"7 Œÿc c}w 7 Œ7 Œh7 Œ## 1 ƒd{  –—†7  sw Y   Œ3xY w7 Œ*# 1 ƒd{  –—†7  sw Y   yxY w­w)aø  Apply aggregated window increments to LiteLLM_BudgetWindowSpend.

    An increment at or behind the row's window_start adds into the row (this is
    how in-flight requests that raced a reset carry into the new window); an
    increment ahead of it rolls the window and starts from that increment.

    Statements are ordered by primary key so concurrent pods take row locks in
    the same order, with window_start breaking ties so an older window is
    applied before the roll that supersedes it.
    N)Úkey)r   rF   )r   rB   rS   rT   zPSpend tracking - committing %d budget window spend upserts over %d existing rows)Útimeout)rB   r_   r`   )r7   Úsortedr   rR   r]   r   r   r`   r   rZ   r
   ÚdebugÚlenr5   Útxr   Úbatch_ÚzipÚexecute_rawr   rd   )r   rF   rT   ÚorderedrS   rB   Ú
seed_basesr`   Údb_transactionÚbatcherr_   s              r&   Úcommit_window_spend_updatesrs   ñ   s‘  è ø€ ñ Øäœ6 ,Ô4JÔKÓL€GÜ)?Ø#Øô*÷ $Ðô ð  'ö	
ð ô -Ø+Ø'Ø&;Ø%9ô	÷ ñ ò	
ó
€Jô œhŸl™l¬8¯<©<Ó8Ó9€CÜ×ÑØZÜˆG‹ÜÐ!Ó"ôð 	×Ñ×ÑÔ$?ÐÓ@÷ð ØDRØ×ÑÓ÷ð à#*ä&)¨'°:Ó&>ò 	Ñ"ˆK˜ØˆG×ÑÜ(ðä¨KÀ9ÐRUÔVôð	÷	÷ ÷ ÷ ð ð-$ùðúò	
ð"øð øð ø÷ ÷ ñ úð ø÷ ÷ ñ üs½   ‚.F°E±F¾EÁEÁEÁB FÃEÃFÃ E9Ã5EÃ6E9Ã98E"Ä1E9Ä<E Ä=E9ÅFÅE7ÅFÅEÅFÅE9Å E9Å"E4	Å(E+Å)E4	Å0E9Å7FÅ9FÅ?FÆ FÆFrD   Únew_window_startc              ƒ   óÎ   K  — | j                   j                  t        |||t        |«      t        t	        j
                  t        j                  «      «      «      ƒ d{  –—†  y7 Œ­w)a  Move a row onto the window that just started and zero its spend.

    Conditional on the stored window_start still being behind the new one so a
    pod that already rolled the row (or increments that arrived under the new
    window) are not clobbered.
    N)r5   rn   r   r   r   r`   r   rZ   )r   r   r   rD   rt   s        r&   Úroll_window_spend_rowrv   %  sP   è ø€ ð ×
Ñ
×
&Ñ
&ÜØØØÜÐ%Ó&Ü”X—\‘\¤(§,¡,Ó/Ó0ó÷ ò ús   ‚AA%ÁA#ÁA%)+r+   Úcollections.abcr   r   r   r   Útypingr   r   r	   Úlitellm._loggingr
   Úlitellm.proxy._typesr   Ú?litellm.proxy.db.db_transaction_queue.window_spend_update_queuer   r   r   Úlitellm.proxy.utilsr   r   Ú__annotations__r   r   r   r   r   r   r   r   r,   r-   r=   r8   r7   rE   rQ   rR   r]   r[   rd   rs   rv   r$   r.   r&   ú<module>r~      sŸ  ðòõ, %ß 2Ñ 2ß 1Ñ 1å 1Ý 3÷ñ ñ Ý0ðDð ˜5ó ð8ð ˜%ó ð >ð ˜ó ðgð ˜uó ðgð  ó ðSð (¨ó ðSð )¨%ó ñ
 &/°rÔ%:Ð ˜UÓ :ô˜xô ð&,.Ø!ð,.àð,.ð ð,.ð ð	,.ð
 " #™ð,.ð ! 4™ð,.ð ˆT�\ó,.ð^; xð ;°Hó ;ðÐ4ð ¸¸sÀCÈ¸}Ñ9Mó ð
kØ!ð
kàÐ.°Ð3Ñ4ð
kð ˆu�S˜#˜s�]Ñ#Ñ$ó
kðØ!ðà'ðð % U¨3°°S¨=Ñ%9Ñ:ðð 3ð	ð
 óð4KÐ)?ð KÀHÈtÁOó KðØ'ðàðð 
ðð ˆ3��S˜( E¨5°(Ð:Ñ;ó	ð0 6Pñ1Ø!ð1àÐ1Ñ2ð1ð 3ð1ð 
ó	1ðhØ!ðàðð ðð ð	ð
 ðð 
ôr.   