@@ -193,6 +193,10 @@ def __init__(
193193 )
194194 # Per-call ledger; deque(maxlen=None) is unbounded, otherwise a ring buffer.
195195 self ._spend_log : deque [SpendEvent ] = deque (maxlen = max_log_events )
196+ # Active streams' (accrued_usd, reserved_usd), keyed by registry token —
197+ # see _stream_register(). Lets parallel streams count each other's
198+ # in-flight accrual against the ceiling before anything settles.
199+ self ._stream_costs : dict [object , tuple [float , float ]] = {}
196200 self ._lock = threading .Lock ()
197201
198202 # ── enforcement ───────────────────────────────────────────────────────────
@@ -214,6 +218,36 @@ def check(self, estimated_next_cost: float | None = None) -> None:
214218 if self ._would_cross (estimated_next_cost ):
215219 self ._block ()
216220
221+ def estimate_call (
222+ self ,
223+ model : str ,
224+ prompt_tokens : int ,
225+ max_completion_tokens : int = 0 ,
226+ * ,
227+ price : ManualPrice | None = None ,
228+ ) -> float | None :
229+ """Price the ACTUAL incoming request, for a request-sized reserve()/check().
230+
231+ :meth:`check` and :meth:`reserve` default to predicting the next call
232+ from the LAST call's cost — which is blind on the first call and wrong
233+ for a call much larger than the previous one. Feed this the request you
234+ are about to send (its real prompt size and output cap) and pass the
235+ result straight through::
236+
237+ est = guard.estimate_call("gpt-4o", prompt_tokens, max_completion_tokens=1024)
238+ handle = guard.reserve(est) # blocks NOW if this call alone would cross
239+
240+ The estimate is worst-case on output (the model may stop well short of
241+ ``max_completion_tokens``); the hold is corrected to actual cost at
242+ :meth:`settle`. Returns ``None`` when the model is unpriceable — and
243+ ``reserve(None)`` / ``check(None)`` fall back to the last-cost
244+ prediction, so the wiring degrades gracefully instead of failing.
245+ """
246+ priced = self ._resolve (model , price )
247+ if priced is None :
248+ return None
249+ return price_tokens (priced , prompt_tokens , max_completion_tokens )
250+
217251 def reserve (self , estimated_cost : float | None = None ) -> float :
218252 """Atomically check the ceiling AND hold the estimated cost in-flight.
219253
@@ -454,6 +488,41 @@ def _validate_estimate(self, estimated: float | None) -> None:
454488 if estimated is not None and not math .isfinite (estimated ):
455489 raise ValueError (f"estimated cost must be a finite number, got { estimated !r} " )
456490
491+ def _stream_register (self , reserved : float ) -> object :
492+ """Register an active stream (see :class:`~floe_guard.stream.StreamGuard`)
493+ and return its registry key. Active streams' accrued-but-unsettled costs
494+ count against the ceiling for each OTHER stream, so parallel unreserved
495+ streams share the budget instead of each spending the full ceiling."""
496+ key = object ()
497+ with self ._lock :
498+ self ._stream_costs [key ] = (0.0 , max (0.0 , reserved ))
499+ return key
500+
501+ def _stream_unregister (self , key : object ) -> None :
502+ """Drop a stream's registry entry once its cost is settled (settle()
503+ moves the accrual into ``spent_usd``, so keeping it would double-count)."""
504+ with self ._lock :
505+ self ._stream_costs .pop (key , None )
506+
507+ def _stream_would_cross (self , key : object , cumulative_call_cost : float ) -> bool :
508+ """Atomically record stream ``key``'s cumulative cost so far and answer:
509+ would it cross the ceiling? Counted against the limit: settled spend,
510+ other calls' reservations (this stream's own hold is excluded — its real
511+ accrued cost replaces the estimate), and each OTHER active stream's
512+ accrual beyond its own reservation (the reservation part is already
513+ inside ``_reserved``). Used by :class:`~floe_guard.stream.StreamGuard`.
514+ """
515+ with self ._lock :
516+ own_reserved = self ._stream_costs .get (key , (0.0 , 0.0 ))[1 ]
517+ self ._stream_costs [key ] = (cumulative_call_cost , own_reserved )
518+ other_overage = sum (
519+ max (0.0 , accrued - held )
520+ for k , (accrued , held ) in self ._stream_costs .items ()
521+ if k is not key
522+ )
523+ others = self .spent_usd + max (0.0 , self ._reserved - own_reserved ) + other_overage
524+ return others + cumulative_call_cost > self .limit_usd + _EPS
525+
457526 def _would_cross (self , estimated_next_cost : float | None ) -> bool :
458527 with self ._lock :
459528 estimate = (
0 commit comments