
    `gj                       U 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mZ ddl	m
Z
mZmZ ddlmZ  ej                  e      Zdaded	<    ed
dh      Z ej,                  d      Z e       Zd;dZd<dZe G d d             ZdZdZdZded<   dZ e G d d             Z!d=d>dZ"dd	 	 	 	 	 	 	 d?dZ#	 d=	 	 	 	 	 d@dZ$ e%dddd d!d"d#d$d
d%&
       e%d'd(d'd(d!d"d
d%)       e%d*d+d*d+d!d"d
d%)       e%d,d-d,d-d!d"d
d%)       e%d#d$dd.d!d"d#d$d
d%&
       e%dd.dd.dd.dd/0       e%dd.d1d2d!d"dd.d
dd/3      d4Z&d5ed6<   dAd7Z'dAd8Z(dBd9Z)dCd:Z*y)DuO  Credits tracking for Nous inference API responses.

Parses x-nous-credits-* (and optional x-nous-tool-pool-*) headers from
inference responses into a validated CreditsState dataclass.  Provides
depletion detection (paid_access), subscription-cap used_fraction, and
warn-once schema-version gating.  This is the hardened parser used by all
live consumers (run_agent, tui_gateway) — not a dev-only shim.

Header schema (x-nous-credits-* family):
    x-nous-credits-version                    contract/schema version
    x-nous-credits-remaining-micros           total remaining balance (micros)
    x-nous-credits-remaining-usd              same, formatted USD string
    x-nous-credits-subscription-micros        subscription balance (SIGNED; may be negative/debt)
    x-nous-credits-subscription-usd           same, formatted USD string
    x-nous-credits-subscription-limit-micros  subscription cap (PAIRED/optional)
    x-nous-credits-subscription-limit-usd     same, formatted USD string (PAIRED/optional)
    x-nous-credits-rollover-micros            rolled-over balance (micros)
    x-nous-credits-purchased-micros           purchased balance (micros)
    x-nous-credits-purchased-usd              same, formatted USD string
    x-nous-credits-denominator-kind           "subscription_cap" | "none"
    x-nous-credits-paid-access                "true" | "false" (STRING!)
    x-nous-credits-disabled-reason            reason string (header omitted when null)
    x-nous-credits-as-of-ms                   server-side timestamp (ms epoch)

Tool-pool headers use a SEPARATE prefix:
    x-nous-tool-pool-micros                   tool-pool balance (micros)
    x-nous-tool-pool-gated-off                "true" | "false" (STRING!)

Money is handled as micros ints only; *_usd values are preserved verbatim as
the raw strings the server sent (never re-parsed to float).
    )annotationsN)	dataclass)AnyMappingOptional)is_truthy_valueFbool_version_warning_emittedsubscription_capnonez^-?\d+\.\d{2}$c                r    | t         S 	 t        t        |             S # t        t        f$ r	 t         cY S w xY w)u;  Parse a header value to an exact int (money-safe).

    The contract guarantees every ``*_micros`` field is an integer string —
    we parse with ``int()`` directly, NOT ``int(float(...))``, to avoid float-
    precision loss above 2**53 that would silently corrupt large money values.

    Returns the parsed int, or ``_SENTINEL`` if the value is not a valid integer
    string (including float-shaped strings like "1.5").  The sentinel lets callers
    detect the failure and return None from the overall parse (fail-hard-on-bad-
    input, not silently coerce).
    )	_SENTINELintstr	TypeError
ValueErrorvalues    H/root/.hermes/venv/lib/python3.12/site-packages/agent/credits_tracker.py	_safe_intr   >   s<     }3u:z" s    66c                D    | yt        t        j                  |             S )zCReturn True iff value is a non-None string matching ^-?\d+\.\d{2}$.F)r	   _USD_REmatchr   s    r   _validate_usdr   S   s    }e$%%    c                  V   e Zd ZU dZdZded<   dZded<   dZded<   dZded	<   dZ	ded
<   dZ
ded<   dZded<   dZded<   dZded<   dZded<   dZded<   dZded<   dZded<   dZded<   dZded<   dZded<   dZded<   dZded <   ed%d!       Zed&d"       Zed%d#       Zed'd$       Zy)(CreditsStatezAFull credits state parsed from x-nous-credits-* response headers.r   r   versionremaining_micros r   remaining_usdsubscription_microssubscription_usdNOptional[int]subscription_limit_microsOptional[str]subscription_limit_usdrollover_microspurchased_microspurchased_usdtool_pool_microsFr	   tool_pool_gated_offr   denominator_kindTpaid_accessdisabled_reasonas_of_ms        floatcaptured_atfrom_headerc                     | j                   dkD  S Nr   )r3   selfs    r   has_datazCreditsState.has_datat   s    !##r   c                r    | j                   st        d      S t        j                         | j                  z
  S )Ninf)r9   r2   timer3   r7   s    r   age_secondszCreditsState.age_secondsx   s*    }}<yy{T----r   c                    | j                    S )u  True when the account has lost paid access.

        Keyed off ``paid_access == False`` ONLY — never ``remaining_micros == 0``,
        which would give a false positive whenever the balance is zero but access
        is still live (e.g. subscription renewal pending).
        )r.   r7   s    r   depletedzCreditsState.depleted~   s     ####r   c                    t        | j                  t              sy| j                  dk  ry| j                  | j                  z
  }t	        dt        d|| j                  z              S )u  Fraction of the subscription cap consumed, in [0.0, 1.0].

        Computable only when ``subscription_limit_micros`` is a truthy (non-zero,
        non-None) int.  Guarded on the LIMIT FIELD, not ``denominator_kind`` —
        the limit field is the real denominator; ``denominator_kind`` is metadata.
        Returns None when there is no computable denominator (no limit, or limit==0).
        Nr   r1         ?)
isinstancer%   r   r"   maxmin)r8   useds     r   used_fractionzCreditsState.used_fraction   s]     $88#>))Q.--0H0HH3C(F(F!FGHHr   returnr	   )rH   r2   )rH   zOptional[float])__name__
__module____qualname____doc__r   __annotations__r   r!   r"   r#   r%   r'   r(   r)   r*   r+   r,   r-   r.   r/   r0   r3   r4   propertyr9   r=   r?   rF    r   r   r   r   ]   s   KGScM3  c/3}3,0M0OScM3c %%"c"K%)O])HcKK$ $ . .
 $ $ I Ir   r   stickyi@  ))g      ?info2   )g      ?warnK   )g?rS   Z   z"tuple[tuple[float, str, int], ...]CREDITS_USAGE_BANDSzcredits.usagec                  b    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Zded<   dZ	ded<   y)AgentNoticeu  A structured, driver-agnostic out-of-band notice.

    The agent fires these via ``AIAgent.notice_callback`` (and clears them via
    ``notice_clear_callback``); each driver renders it its own way — the TUI as a
    status-bar override, the CLI as a console line, etc. v1 credits notices are all
    ``kind="sticky"``; ``kind``/``ttl_ms`` are kept fully expressive so a future
    config/slash-command can switch them to TTL without touching the policy (a
    single default seam — see L4).
    r   textrQ   levelrP   kindNr$   ttl_msr&   keyid)
rI   rJ   rK   rL   rM   rZ   r[   r\   r]   r^   rO   r   r   rX   rX      s@     IE3D# FM CBr   rX   c                   | sy| j                  d      ry|sy	 ddlm}m} |j	                  d      }|j                  d      r|dd	 j	                  d      }|j                  |      }|sy || |      S # t        $ r Y yw xY w)
u  Return True when *model* is a Nous free-tier model, using ONLY local data.

    Two signals, both zero-network:

    1. The ``:free`` suffix — the canonical Nous free SKU marker (e.g.
       ``nvidia/nemotron-3-ultra:free``). Free by construction on the API side
       (spend is forced to 0 for ``:free`` ids).
    2. A peek into the in-process pricing cache in ``hermes_cli.models``
       (populated when the model picker fetched ``/v1/models`` pricing for
       *base_url*). PEEK ONLY — a cache miss never triggers a fetch. This is
       CLI/TUI-session best-effort: gateway sessions never run the picker's
       pricing fetch, so suppression there rests entirely on the ``:free``
       suffix (which all Nous free SKUs carry).

    Fail-open to False (the depleted notice still shows) on any error: wrongly
    showing the warning is recoverable noise; wrongly hiding it on a paid model
    would mask a real billing block.
    Fz:freeTr   )_is_model_free_pricing_cache/z/v1N)endswithhermes_cli.modelsr`   ra   rstripget	Exception)modelbase_urlr`   ra   r]   pricings         r   is_free_tier_modelrl      s    & ~~gD
 ooc"<<cr(//#&C $$S)eW-- s   AA5 ,A5 5	B B)model_is_freec          
        g }g }| j                   }t        d   d   }|
||k  rd|d<   |d   }d}|t        D ]  }	||	d   k\  s|	} | j                  dkD  rd}| j                  dk(  xr |duxr |dk\  xr | j                  dkD  }
| j                   }|j                  d      }|r
|d   r|d	   nd}||k7  rt        |v r*|j                  t               |j                  t               |e| j                  xs d
}|d   }|j                  t        |dk(  rdnd d| d| d|t        t        t                     |j                  t               ||d<   |
rGd|vrC|j                  t        d| j                   ddt        dd             |j                  d       n(d|v r$|
s"|j                  d       |j                  d       |xr | }|r<d|vr8|j                  t        ddt        dd             |j                  d       ||fS d|v rJ|sH|j                  d       |j                  d       |s$|j                  t        dddt        dd             ||fS )u  Reconcile credits notices against the latch. Mutates ``latch`` IN PLACE.

    latch = {"active": set[str], "seen_below_90": bool, "usage_band": Optional[int]}.

    ``model_is_free``: True when the session's active model is a Nous free-tier
    model (see :func:`is_free_tier_model`). Suppresses the ``credits.depleted``
    notice — a depleted account on a free model can keep inferencing, so the
    error banner is noise (and confuses free-tier users who never had credits).
    Suppression does NOT emit the "restored" success notice; that fires only on
    a genuine ``paid_access`` flip back to True.

    Returns ``(to_show: list[AgentNotice], to_clear: list[str])``.
    Caller emits to_clear FIRST, then to_show.

    Pure function — no I/O, no agent/run_agent imports.
    r   NTseen_below_90activer   rA   
usage_band   ?   rS   u   ⚠u   •z	 Credits u   % used · $z cap)rY   rZ   r[   r]   r^   zcredits.grant_spentu   • Grant spent · $z top-up leftrQ   zcredits.depletedu0   ✕ Credit access paused · run /topup to top uperroru   ✓ Credit access restoredsuccessttlzcredits.restored)rY   rZ   r[   r\   r]   r^   )rF   rV   r)   r-   r.   rg   CREDITS_USAGE_KEYappenddiscardr'   rX   CREDITS_NOTICE_KINDaddr*   CREDITS_RESTORED_TTL_MS)statelatchrm   to_showto_clearuf_lowest_bandrp   current_bandband
grant_conddepleted_cond
shown_bandtarget_band_cap_usd_levelshow_depleteds                    r   evaluate_credits_noticesr      s   , "$GH			B 'q)!,L	~"|+!%o8_F 6:L	~' 	$DT!W}#	$ !"44 	'dN	'#I	' ""Q&	  )))M <(J&2u_7M,q/TXKj &OO-.NN,-" 33:sH!!_FNN%+v%5E5A;-Wbckbllpq ,)( JJ())l +69+E,?,?+@M()(	
 	

()	&	(-.,- "7-&7M+69G(&%	
 	

%&& X% 
v	%m*+)* NN5#2*)	 Xr   c                   	 t        d | D              sy| j                         D ci c]  \  }}|j                         | c}}j                  d      }|yt	        |      }|t
        u ry|dk7  r$|dkD  rt        sdat        j                  d|       yd2fd}d2fd} |d	      }|t
        u ry |d
      }	|	t
        u ry |d      }
|
t
        u ry |d      }|t
        u ryj                  d      }|d}nt	        |      }|t
        u s|dk  ry|} |d      }|t
        u ryj                  dd      }t        |      syj                  dd      }t        |      syj                  dd      }t        |      syj                  d      }j                  d      }d}d}|,|*t	        |      }|t
        u ry|dk  ryt        |      sy|}|}j                  dd      }|t        vrydv r,d   j                         j                         }|dvry|dk(  }nd}dv r,d   j                         j                         }|dvry|dk(  }nd}j                  d      }t        d3i d|d|d |d!|	d"|d#|d$|d%|
d&|d'|d(|d)|d*|d+|d,|d-|d.t        j                         d/dS c c}}w # t        $ r t        j                  d0d1       Y yw xY w)4a-  Parse x-nous-credits-* (and x-nous-tool-pool-*) headers into a CreditsState.

    Returns None (miss) on ANY of:
    - No ``x-nous-credits-version`` header present.
    - Version != 1 (> 1 also emits a one-time logger.warning).
    - Any ``*_micros`` field is non-integer, or negative for a non-subscription field.
    - Any ``*_usd`` field doesn't match ``^-?\d+\.\d{2}$``.
    - ``denominator_kind`` is not in {"subscription_cap", "none"}.
    - ``paid_access`` / ``tool_pool_gated_off`` is not exactly "true"/"false".
    - ``as_of_ms`` is not a valid integer.
    - Any unexpected exception.

    Fail-open on the subscription_limit pair: a half-pair (only -micros or only
    -usd present) is treated as both-absent; the overall parse STILL SUCCEEDS
    but with subscription_limit_micros/usd both None.
    c              3  B   K   | ]  }|j                         d k(    yw)x-nous-credits-versionN)lower).0ks     r   	<genexpr>z(parse_credits_headers.<locals>.<genexpr>  s     JQ1779 88Js   Nr   rt   TuA   credits header version %d unsupported, ignoring — update Hermesc                r    j                  |       }t        |      }|t        u rt        S |dk  rt        S |S r6   rg   r   r   r]   rawvallowereds      r   _req_nonnegz*parse_credits_headers.<locals>._req_nonneg  s:    ++c"CC.Ci  Qw  Jr   c                \    j                  |       }t        |      }|t        u rt        S |S )Nr   r   s      r   _req_intz'parse_credits_headers.<locals>._req_int  s-    ++c"CC.Ci  Jr   zx-nous-credits-remaining-microsz"x-nous-credits-subscription-microszx-nous-credits-rollover-microszx-nous-credits-purchased-microszx-nous-tool-pool-microsr   zx-nous-credits-as-of-mszx-nous-credits-remaining-usdr    zx-nous-credits-subscription-usdzx-nous-credits-purchased-usdz(x-nous-credits-subscription-limit-microsz%x-nous-credits-subscription-limit-usdzx-nous-credits-denominator-kindr   zx-nous-credits-paid-access)truefalser   zx-nous-tool-pool-gated-offFzx-nous-credits-disabled-reasonr   r   r!   r"   r#   r%   r'   r(   r)   r*   r+   r,   r-   r.   r/   r0   r3   r4   u9   credits ▸ parse_credits_headers raised (fail-open miss)exc_info)r]   r   rH   r   rO   )anyitemsr   rg   r   r   r
   loggerwarningr   _VALID_DENOMINATOR_KINDSstripr   r<   rh   debug)headersproviderr   vversion_rawversion_valr   r   r   r"   r(   r)   _tp_rawr+   _tp_valr0   r!   r#   r*   sub_limit_micros_rawsub_limit_usd_rawr%   r'   lmr-   pa_rawr.   tpgo_rawr,   r/   r   s                                 @r   parse_credits_headersr     s   ,g J'JJ -4MMO<DAq1779a<< kk":;,)#!Q'?+/(W 		 ''HIy(&'KL)+%&FGi'&'HIy( ++78? (G)#w{&89y   $BBG]+";;'H"M-.$BBG]+
  '{{+UV#KK(OP37!04+0A0M/0BYAv !23(*%%6" #;;'H&Q#;; (729:@@BHHJF.. F*KK'72;<BBDJJLH00"*f"4"' "++&FG 

-
 (
 !4	

 .
 '@
 $:
 ,
 .
 (
 .
 !4
 .
 $
 ,
  !
" 		#
$ %
 	
G =p   	P[_`s   K  K  J:K  K  0(K  K  6K  K  K  ).K  K  +K  	K  'K  =K  K  	K  K  4)K  1K  A)K  :K    K#"K#iz30.34iz18.00i -1z20.00i K z12.34T)
r   r!   r"   r#   r%   r'   r)   r*   r-   r.   i z10.00)r   r!   r"   r#   r%   r'   r-   r.   i@KL z5.00i z2.000.00out_of_credits)r   r!   r"   r#   r)   r*   r.   r/   iz-5.00)r   r!   r"   r#   r%   r'   r)   r*   r-   r.   r/   )healthy	sub_50pct	sub_75pct	sub_90pctgrant_exhaustedr?   debtzdict[str, dict]_DEV_FIXTURESc                 :   t        t        j                  j                  d            syt        j                  j                  dd      j	                         } | sy| }t        j
                  j                  | v sd| v r6	 t        | dd      5 }|j                         j	                         }ddd       t        j                  |j                               }|syd	d
d|}t        di |dt        j                         dS # 1 sw Y   XxY w# t        $ r Y yw xY w)ub  Return a fixture CreditsState for HERMES_DEV_CREDITS_FIXTURE, or None.

    The env value is a state name, OR a path to a file whose contents are a state
    name (re-read each call → flip states live without a restart). Unknown name /
    "clear" / "none" / unset → None (normal behaviour). Throwaway test scaffolding.

    Hard prod-leak guard: a fixture applies ONLY when the dev flag HERMES_DEV_CREDITS
    is also on, so a stray HERMES_DEV_CREDITS_FIXTURE (leaked into a shell profile, a
    container env, a launch plist, …) can never surface fabricated balances/notices
    on a real account.
    HERMES_DEV_CREDITSNHERMES_DEV_CREDITS_FIXTUREr    rb   rzutf-8)encodingrt   r   )r   r*   T)r4   r3   rO   )r   osenvironrg   r   pathsepopenreadOSErrorr   r   r   r<   )r   namefhspecmergeds        r   dev_fixture_credits_stater     s     2::>>*>?@
**..5r
:
@
@
BCD	ww{{cSCZ	c31 )Rwwy() TZZ\*D V<t<FL&Ld		LL) ) 		s*   >D D+D DD 	DDc                   	 t        | dd      }t        | dd      }d }d }t        |dd      }t        |t        t        f      xr |dkD  }t        | dd      }t	         |t        |d	d             |t        |d	d             |t        |d
d             |t        |d
d            |r ||      nd|r ||      nd |t        |dd             |t        |dd             |t        |dd            |rdndt        |t
              r|nddt        j                               S # t        $ r t        j                  dd       Y yw xY w)u4  Map a NousPortalAccountInfo into a header-shaped CreditsState for the seed.

    Float account dollars → micros (plus a DISPLAY *_usd string — allowed, since
    we're formatting account floats, NOT parsing a server-provided *_usd). Returns
    None if the account can't yield a usable state (fail-open).paid_service_access_infoNsubscriptionc                `    t        | t        t        f      rt        t        | dz              S dS )Ni@B r   )rB   r   r2   rounddollarss    r   
_to_microsz/_credits_state_from_account.<locals>._to_micros  s+    6@3PU,6W3uWy012^]^^r   c                :    t        | t        t        f      r| dS dS )Nz.2fr    )rB   r   r2   r   s    r   _to_usdz,_credits_state_from_account.<locals>._to_usd  s      (2'C<'Hgc]PbPr   monthly_creditsr   paid_service_accesstotal_usable_creditssubscription_credits_remainingpurchased_credits_remainingrollover_creditsr   r   TF)r   r!   r"   r#   r%   r'   r)   r*   r(   r-   r.   r4   r3   u/   credits ▸ seed account→state mapping failedr   )
getattrrB   r   r2   r   r	   r<   rh   r   r   )rQ   _acc_subr   r   _monthly_has_cap_paids           r   _credits_state_from_accountr     sW    t7>t^T2	_	Q
 4!2D9he5F(Q,3T:'6Ld(ST!'$0F"MN *749Y[_+` a$WT3SUY%Z[>Fj&:D8@78#4d'6SUY(Z[!'$0Mt"TU&wt5G'NO3;/!+E4!8d		
 	
  FQUVs   D!D$ $ EEc                    || _         t        | dd      |j                  | _        t        | dd      }t	        |t
              r|j                  d|d<   t        | dd      }t        |      r |        yy)u  Install a seed CreditsState on the agent and fire the notice policy once.

    Sets _credits_state, latches session-start remaining, and primes the crossing
    gate (the cold-start snapshot IS the first observation, so a session that opens
    already in a band warns immediately — the live header path keeps true crossing
    semantics), then emits. Safe to call from a worker thread: emit already runs
    off-thread in the TUI build path._credits_session_start_microsN_credits_latchTro   _emit_credits_notices)_credits_stater   r   r   rB   dictrF   callable)agentr~   _latchemits       r   _hydrate_seed_stater     sx     !Eu5t<D.3.D.D+U,d3F&$E$7$7$C"&5148D~ r   c                D    	 t         dd      dk7  ryt         dd      yd}	 t               }|t         |       yddl}d fd	}|j                  |d
d      j                          y# t        $ r d}Y Iw xY w# t        $ r t        j                  dd       Y yw xY w)u}  Hydrate agent._credits_state from /api/oauth/account (or a dev fixture) and
    fire the notice policy, so depletion / usage-band warnings show at session OPEN.

    Shared by (a) the TUI/desktop agent build (fires at "ready", before any message)
    and (b) the first-turn conversation setup (fallback for plain CLI / when the
    build path didn't seed). Idempotent: a second call is a no-op once a seed or a
    real header has already populated _credits_state.

    Returns True if it seeded this call, False otherwise (not nous / already seeded /
    fail-open error). Never raises — credits must never block session startup.
    r   r    nousFr   NTr   c                     	 ddl m}   | d      }t        dd       y t        |      }|t	        |       y y # t
        $ r t        j                  dd       Y y w xY w)Nr   )get_nous_portal_account_infoT)force_freshr   u2   credits ▸ session-start seed (background) failedr   )hermes_cli.nous_accountr   r   r   r   rh   r   r   )r   rQ   r~   r   s      r   _bg_seedz/seed_credits_at_session_start.<locals>._bg_seed  sk    	bP3E5"2D9E3D9$'u5 % bQ\`abs   ; ;  AAzcredits-seed)targetr   daemonu1   credits ▸ session-start seed failed (fail-open)r   rH   None)	r   r   rh   r   	threadingThreadstartr   r   )r   fixturer   r   s   `   r   seed_credits_at_session_startr    s    )5*b)V35*D1=	/1G   w/
 	
	b 	~dKQQS5  	G	6   	HSWXsB   A< A< A< 
A+ A< ,A< +A96A< 8A99A< < BB)r   r   rH   r   )r   r&   rH   r	   )r    )ri   r   rj   r   rH   r	   )r~   r   r   r   rm   r	   rH   z#tuple[list[AgentNotice], list[str]])r   zMapping[str, str]r   r   rH   Optional[CreditsState])rH   r  r   rG   )+rL   
__future__r   loggingr   rer<   dataclassesr   typingr   r   r   utilsr   	getLoggerrI   r   r
   rM   	frozensetr   compiler   objectr   r   r   r   r{   r}   rV   rx   rX   rl   r   r   r   r   r   r   r   r  rO   r   r   <module>r     sK  @ #  	 	  ! ) ) !			8	$ "' $ & %&8&%AB  "**&
' H	*& 8I 8I 8I@   ; 7 
 $    ,'b  	KKK 	K
 )Kf }}} }\ #7&",W#7+ #7&",W+	 "&%",W+	 "&%",W+	 #7",W#7+ &&+;	 &&",W&+(O/" /d!MH&R&5r   