2424from ..guard import BudgetGuard
2525from ..pricing import resolve_price
2626
27- # Anthropic prompt-cache pricing multipliers, relative to the base input rate:
28- # 5-minute cache writes bill at ~1.25x, cache reads at ~0.1x. See _usage_from.
29- _CACHE_WRITE_WEIGHT = 1.25
30- _CACHE_READ_WEIGHT = 0.1
27+ # Anthropic prompt-cache pricing is now handled natively in the core (guard.py / pricing.py).
28+ # We extract the buckets and pass them through.
3129
3230
3331def _model_from (kwargs : dict [str , Any ], response : Any ) -> str :
@@ -41,38 +39,46 @@ def _model_from(kwargs: dict[str, Any], response: Any) -> str:
4139 return str (model or "" )
4240
4341
44- def _usage_from (response : Any ) -> tuple [int , int ]:
45- """Map an Anthropic message's usage onto (prompt_tokens, completion_tokens) .
42+ def _usage_from (response : Any ) -> tuple [int , int , int , int , int ]:
43+ """Map an Anthropic message's usage onto token buckets .
4644
47- Anthropic reports ``usage.input_tokens`` / ``usage.output_tokens``; the guard
48- settles on the OpenAI-style ``(prompt_tokens, completion_tokens)`` pair, so
49- input maps to prompt and output to completion.
50-
51- Prompt caching: ``input_tokens`` counts only the *fresh* prompt tokens —
52- cached tokens are reported separately as ``cache_creation_input_tokens``
53- (billed ~1.25x base input) and ``cache_read_input_tokens`` (~0.1x). Dropping
54- them would under-meter a cached call, and a budget guard must never
55- under-count. Since the guard prices with a flat per-model input rate, we fold
56- the cache tokens into the prompt bucket at their *effective* weight so the
57- metered cost approximates the real spend (an estimate, not exact cache-aware
58- pricing — safe-direction).
45+ Anthropic reports ``usage.input_tokens`` / ``usage.output_tokens``.
46+ Prompt caching tokens are reported as ``cache_creation_input_tokens``
47+ and ``cache_read_input_tokens``. We return all five buckets so the
48+ core pricing engine can apply exact cost multipliers.
5949 """
6050 usage = getattr (response , "usage" , None )
6151 if usage is None and isinstance (response , dict ):
6252 usage = response .get ("usage" )
6353 if usage is None :
64- return 0 , 0
54+ return 0 , 0 , 0 , 0 , 0
6555 get = usage .get if isinstance (usage , dict ) else lambda k , d = 0 : getattr (usage , k , d )
6656 input_tokens = int (get ("input_tokens" , 0 ) or 0 )
6757 output_tokens = int (get ("output_tokens" , 0 ) or 0 )
68- cache_write = int (get ("cache_creation_input_tokens" , 0 ) or 0 )
58+ cache_write_total = int (get ("cache_creation_input_tokens" , 0 ) or 0 )
6959 cache_read = int (get ("cache_read_input_tokens" , 0 ) or 0 )
70- prompt_tokens = (
71- input_tokens
72- + round (cache_write * _CACHE_WRITE_WEIGHT )
73- + round (cache_read * _CACHE_READ_WEIGHT )
74- )
75- return prompt_tokens , output_tokens
60+
61+ # Resolve TTL buckets from `cache_creation` if present
62+ cache_creation = get ("cache_creation" , None )
63+ cache_write_5m = 0
64+ cache_write_1h = 0
65+ if cache_creation is not None :
66+ get_cc = (
67+ cache_creation .get
68+ if isinstance (cache_creation , dict )
69+ else lambda k , d = 0 : getattr (cache_creation , k , d )
70+ )
71+ cache_write_5m = int (get_cc ("ephemeral_5m_input_tokens" , 0 ) or 0 )
72+ cache_write_1h = int (get_cc ("ephemeral_1h_input_tokens" , 0 ) or 0 )
73+
74+ # Fallback/defense-in-depth: if there are missing or future buckets, allocate
75+ # any leftover cache_creation tokens to the most expensive known bucket (1h) so
76+ # the guard over-counts rather than under-meters spend it can't fully attribute.
77+ leftover = cache_write_total - (cache_write_5m + cache_write_1h )
78+ if leftover > 0 :
79+ cache_write_1h += leftover
80+
81+ return input_tokens , output_tokens , cache_write_5m , cache_write_1h , cache_read
7682
7783
7884def _settle_model (guard : BudgetGuard , kwargs : dict [str , Any ], response : Any ) -> str :
@@ -103,16 +109,36 @@ def _record_response(
103109 if not isinstance (kwargs , dict ):
104110 kwargs = {}
105111 model = _settle_model (guard , kwargs , response )
106- prompt_tokens , completion_tokens = _usage_from (response )
107- if prompt_tokens <= 0 and completion_tokens <= 0 :
112+ (
113+ prompt_tokens ,
114+ completion_tokens ,
115+ cache_creation_5m ,
116+ cache_creation_1h ,
117+ cache_read ,
118+ ) = _usage_from (response )
119+ if (
120+ prompt_tokens <= 0
121+ and completion_tokens <= 0
122+ and cache_creation_5m <= 0
123+ and cache_creation_1h <= 0
124+ and cache_read <= 0
125+ ):
108126 # No tokens spent — free the reservation.
109127 guard .release (reserved )
110128 return
111129 # There IS spend to account for. Route it through settle() even when the
112130 # model id is missing, so the guard's policy applies (fail-closed → warn +
113131 # raise; fail-open → warn + skip) rather than letting a completed call go
114132 # unmetered and skew the next check().
115- guard .settle (model , prompt_tokens , completion_tokens , reserved = reserved )
133+ guard .settle (
134+ model ,
135+ prompt_tokens ,
136+ completion_tokens ,
137+ reserved = reserved ,
138+ cache_creation_input_tokens = cache_creation_5m ,
139+ cache_creation_input_tokens_1h = cache_creation_1h ,
140+ cache_read_input_tokens = cache_read ,
141+ )
116142
117143
118144def _reject_streaming (kwargs : dict [str , Any ]) -> None :
0 commit comments