返回 CodeWhale
cost_status.rs
根目录 / crates / tui / src / cost_status.rs
1 //! Process-wide cost-accrual side-channel (#526).
2 //!
3 //! Background LLM calls outside the main turn-complete path
4 //! (compaction summaries) used
5 //! to drop their token usage on the floor — the dashboard's
6 //! session-cost only saw the parent turn's tokens, so a long
7 //! session that triggered compaction under-reported
8 //! cost by however many tokens those background calls consumed.
9 //!
10 //! Mirrors the [`crate::retry_status`] pattern: background callers
11 //! call [`crate::cost_status::report_effective_route`] after each
12 //! `client.create_message`, the TUI
13 //! render loop calls [`drain`] every frame, and any drained amount
14 //! gets folded into `App::accrue_subagent_cost_estimate`.
15 //!
16 //! Why a side-channel and not a plumbed callback: the leaky callers
17 //! (`compaction.rs`) are
18 //! engine-internal machinery without a direct handle to `App` or
19 //! the engine's event channel. A side-channel keeps the change
20 //! surface tiny — one new `report` line per call site — and any
21 //! future background caller (summarizers, retrieval helpers) gets
22 //! accrued for free without further plumbing.
23 //!
24 //! ## One pool, not a pile of counters (#4318)
25 //!
26 //! Money and the *completeness* of that money are one fact, so they live in one
27 //! mutex-guarded [`PendingBackgroundCost`] that [`drain`] takes atomically.
28 //! Splitting them across free-standing atomics made two things go wrong at once:
29 //! a drain could observe a total without the counters that explain it, and every
30 //! new global was another piece of state a parallel test had to remember to
31 //! reset. There is exactly one *drainable cost pool*. The runtime-owner journal
32 //! below is a separate route/usage copy (never another money counter), and the
33 //! shared test reset clears both stores.
34
35 use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
36 use std::sync::{Arc, Mutex, OnceLock};
37
38 use chrono::{DateTime, Utc};
39
40 use crate::config::ProviderKind;
41 use crate::pricing::{CostEstimate, TurnCostAudit};
42 use crate::route_billing::BillingPresentation;
43 use codewhale_models::Usage;
44
45 /// Everything a drained background accrual needs to be explained.
46 ///
47 /// The money and the coverage/provenance that qualify it are drained together,
48 /// so `/cost` can never show a background subtotal whose completeness came from
49 /// a different observation.
50 #[derive(Debug, Clone, Default, PartialEq)]
51 pub struct PendingBackgroundCost {
52 /// Summed cost of the background turns that were priced.
53 pub estimate: CostEstimate,
54 /// Background turns that produced an authoritative price.
55 pub priced_turns: u32,
56 /// Background turns that were money-metered (or of unknown basis) but
57 /// produced no authoritative price, so their spend is missing.
58 pub unpriced_turns: u32,
59 /// Money-metered turns authoritatively priced in CNY.
60 pub cny_priced_turns: u32,
61 /// Money-metered turns missing authoritative CNY pricing.
62 pub cny_unpriced_turns: u32,
63 /// Stable reason labels for the unpriced turns.
64 pub unpriced_reasons: BTreeSet<&'static str>,
65 pub cny_unpriced_reasons: BTreeSet<&'static str>,
66 /// Token classes used on a background route that carry no published price.
67 pub unpriced_classes: BTreeSet<&'static str>,
68 /// Provenance labels of the pricing rows that were applied or attempted.
69 pub pricing_provenances: BTreeSet<&'static str>,
70 /// Live-pricing downgrade receipts, when a live catalog row could not be
71 /// verified for the endpoint that served the turn.
72 pub live_pricing_defects: BTreeSet<&'static str>,
73 /// Live pricing failed and no bundled row could price the turn. Kept
74 /// separate so `/cost` never claims a bundled fallback was used when the
75 /// result is actually unavailable.
76 pub live_pricing_unusable_defects: BTreeSet<&'static str>,
77 /// One redacted receipt per distinct background route that reported.
78 ///
79 /// See [`EffectiveRouteEnvelope::receipt`] for the exact contents; these carry
80 /// provider identity, endpoint *fingerprint*, billing surface, wire model,
81 /// and currency — never a URL, key, token, or filesystem path.
82 pub route_receipts: BTreeSet<String>,
83 /// Durable, redacted identities of provider responses folded into this
84 /// batch. These travel with the money so a session snapshot can make a
85 /// replay idempotent after reload.
86 pub usage_source_fingerprints: BTreeSet<String>,
87 /// Exact unresolved coverage in this same source ledger, bounded to 64.
88 pub missing_usage_sources: BTreeMap<String, MissingUsageCoverage>,
89 /// A late real receipt supersedes only this exact unresolved source.
90 pub resolved_missing_usage_sources: BTreeSet<String>,
91 pub missing_usage_overflowed: bool,
92 /// Prompt-cache classes the background routes reported, through
93 /// [`crate::pricing::token_usage_for_pricing`] so they never exceed the
94 /// input they partition (#6565). `None` until a child reports cache
95 /// telemetry at all: no report is not a 0% hit rate.
96 pub cache_hit_tokens: Option<u64>,
97 pub cache_miss_tokens: Option<u64>,
98 pub cache_write_tokens: Option<u64>,
99 }
100
101 /// Immutable, non-secret route evidence captured before a provider request.
102 /// It contains enough information to audit the eventual usage without reading
103 /// mutable parent/app config at completion time.
104 #[derive(Debug, Clone, PartialEq, Eq)]
105 pub struct EffectiveRouteEnvelope {
106 pub provider: ProviderKind,
107 pub provider_identity: String,
108 pub model: String,
109 /// Requested OpenRouter upstream, frozen with the client that dispatched.
110 pub openrouter_vendor: Option<String>,
111 pub billing_surface: Option<String>,
112 pub endpoint_fingerprint: Option<String>,
113 /// Frozen provider-live or signed cloud rates captured from the exact catalog scope
114 /// at CodeWhale's pre-permit application-dispatch boundary. Legacy
115 /// receipts omit this and therefore cannot meter a reviewed custom route
116 /// retroactively.
117 pub provider_live_pricing: Option<crate::provider_catalog_live::ProviderLivePricingQuote>,
118 pub billing_mode: RouteBillingMode,
119 pub dispatched_at: DateTime<Utc>,
120 }
121
122 #[derive(serde::Deserialize)]
123 struct EffectiveRouteEnvelopeWire {
124 provider: String,
125 provider_identity: String,
126 model: String,
127 /// Requested OpenRouter upstream, frozen with the client that dispatched.
128 #[serde(default)]
129 openrouter_vendor: Option<String>,
130 billing_surface: Option<String>,
131 endpoint_fingerprint: Option<String>,
132 /// Frozen provider-live or signed cloud rates captured from the exact catalog scope
133 /// at CodeWhale's pre-permit application-dispatch boundary. Legacy
134 /// receipts omit this and therefore cannot meter a reviewed custom route
135 /// retroactively.
136 #[serde(
137 default,
138 deserialize_with = "crate::provider_catalog_live::deserialize_optional_provider_live_pricing"
139 )]
140 provider_live_pricing: Option<crate::provider_catalog_live::ProviderLivePricingQuote>,
141 #[serde(default)]
142 billing_mode: RouteBillingMode,
143 dispatched_at: DateTime<Utc>,
144 }
145
146 impl<'de> serde::Deserialize<'de> for EffectiveRouteEnvelope {
147 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
148 where
149 D: serde::Deserializer<'de>,
150 {
151 let wire = <EffectiveRouteEnvelopeWire as serde::Deserialize>::deserialize(deserializer)?;
152 let provider = codewhale_config::descriptors::kind_from_tui_wire_tag(
153 &wire.provider,
154 &wire.provider_identity,
155 )
156 .ok_or_else(|| serde::de::Error::custom("contradictory usage provider identity"))?;
157 Ok(Self {
158 provider,
159 provider_identity: wire.provider_identity,
160 model: wire.model,
161 openrouter_vendor: wire.openrouter_vendor,
162 billing_surface: wire.billing_surface,
163 endpoint_fingerprint: wire.endpoint_fingerprint,
164 provider_live_pricing: wire.provider_live_pricing,
165 billing_mode: wire.billing_mode,
166 dispatched_at: wire.dispatched_at,
167 })
168 }
169 }
170
171 impl serde::Serialize for EffectiveRouteEnvelope {
172 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
173 where
174 S: serde::Serializer,
175 {
176 use serde::ser::SerializeStruct as _;
177
178 let route = self.sanitized_for_persistence();
179 let mut state = serializer.serialize_struct(
180 "EffectiveRouteEnvelope",
181 8 + usize::from(route.openrouter_vendor.is_some()),
182 )?;
183 let provider = codewhale_config::descriptors::tui_wire_tag_for_route(
184 route.provider,
185 &route.provider_identity,
186 )
187 .ok_or_else(|| serde::ser::Error::custom("contradictory usage provider identity"))?;
188 state.serialize_field("provider", provider)?;
189 state.serialize_field("provider_identity", &route.provider_identity)?;
190 state.serialize_field("model", &route.model)?;
191 if let Some(vendor) = &route.openrouter_vendor {
192 state.serialize_field("openrouter_vendor", vendor)?;
193 }
194 state.serialize_field("billing_surface", &route.billing_surface)?;
195 state.serialize_field("endpoint_fingerprint", &route.endpoint_fingerprint)?;
196 state.serialize_field("provider_live_pricing", &route.provider_live_pricing)?;
197 state.serialize_field("billing_mode", &route.billing_mode)?;
198 state.serialize_field("dispatched_at", &route.dispatched_at)?;
199 state.end()
200 }
201 }
202
203 /// One provider usage payload paired with the immutable route that served it.
204 ///
205 /// Runtime hosts persist these for model calls made below the parent turn
206 /// (sub-agents, review/verify/RLM tools, and compaction). Keeping route and
207 /// usage together makes the record independently auditable and prevents a
208 /// later provider/model selection from changing its price.
209 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
210 pub struct EffectiveRouteUsage {
211 pub route: EffectiveRouteEnvelope,
212 pub usage: Usage,
213 }
214
215 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
216 #[serde(rename_all = "snake_case")]
217 pub enum RouteBillingMode {
218 Metered,
219 Subscription,
220 Local,
221 #[default]
222 Unknown,
223 }
224
225 impl From<BillingPresentation> for RouteBillingMode {
226 fn from(value: BillingPresentation) -> Self {
227 match value {
228 BillingPresentation::Metered => Self::Metered,
229 BillingPresentation::Subscription(_) => Self::Subscription,
230 BillingPresentation::Local => Self::Local,
231 BillingPresentation::Unknown => Self::Unknown,
232 }
233 }
234 }
235
236 impl EffectiveRouteEnvelope {
237 /// Provider-neutral observation. This never opens configuration or credentials.
238 #[must_use]
239 pub fn capture_observed(
240 provider: ProviderKind,
241 provider_identity: impl Into<String>,
242 model: impl Into<String>,
243 base_url: Option<&str>,
244 dispatched_at: DateTime<Utc>,
245 ) -> Self {
246 let key = provider_identity.into();
247 Self::from_facts(None, (provider, &key), model, base_url, dispatched_at)
248 }
249
250 #[cfg(test)]
251 pub fn capture(
252 config: Option<&crate::config::Config>,
253 provider: ProviderKind,
254 provider_identity: impl Into<String>,
255 model: impl Into<String>,
256 base_url: Option<&str>,
257 dispatched_at: DateTime<Utc>,
258 ) -> Self {
259 let key = provider_identity.into();
260 if let Some(config) = config
261 && let Ok(identity) =
262 config.resolve_persisted_provider_identity(Some(provider.as_str()), Some(&key))
263 {
264 Self::from_admitted(Some(config), &identity, model, base_url, dispatched_at)
265 } else {
266 Self::capture_observed(provider, key, model, base_url, dispatched_at)
267 }
268 }
269
270 #[must_use]
271 pub fn from_admitted(
272 config: Option<&crate::config::Config>,
273 identity: &crate::config::ProviderIdentity,
274 model: impl Into<String>,
275 base_url: Option<&str>,
276 dispatched_at: DateTime<Utc>,
277 ) -> Self {
278 let admission = config
279 .filter(|config| config.verify_provider_identity(identity).is_ok())
280 .map(|config| (config, identity));
281 Self::from_facts(
282 admission,
283 (identity.provider, identity.key.as_str()),
284 model,
285 base_url,
286 dispatched_at,
287 )
288 }
289
290 fn from_facts(
291 admission: Option<(&crate::config::Config, &crate::config::ProviderIdentity)>,
292 facts: (ProviderKind, &str),
293 model: impl Into<String>,
294 base_url: Option<&str>,
295 dispatched_at: DateTime<Utc>,
296 ) -> Self {
297 let (provider, provider_identity) = facts;
298 let config = admission.map(|(config, _)| config);
299 let model = model.into();
300 let billing = admission.map_or_else(
301 || crate::route_billing::for_endpoint_without_config(provider, base_url),
302 |(config, identity)| {
303 base_url.map_or_else(
304 || crate::route_billing::for_route(config, identity),
305 |endpoint| {
306 crate::route_billing::for_route_with_endpoint(config, identity, endpoint)
307 },
308 )
309 },
310 );
311 let endpoint_fingerprint = base_url.and_then(endpoint_fingerprint);
312 let provider_live_pricing = base_url.and_then(|base_url| {
313 u64::try_from(dispatched_at.timestamp())
314 .ok()
315 .and_then(|at| {
316 crate::provider_catalog_live::declared_or_catalog_quote(
317 config.and_then(|config| {
318 crate::provider_catalog_live::configured_dispatch_pricing_quote_at(
319 config.custom_models.as_deref().unwrap_or_default(),
320 provider,
321 provider_identity,
322 &model,
323 base_url,
324 at,
325 )
326 }),
327 || {
328 crate::provider_catalog_live::fresh_dispatch_pricing_quote_at(
329 provider,
330 provider_identity,
331 &model,
332 base_url,
333 at,
334 )
335 },
336 )
337 })
338 });
339 Self {
340 provider,
341 provider_identity: sanitize_persisted_route_label(provider_identity),
342 model: sanitize_persisted_route_label(&model),
343 openrouter_vendor: admission
344 .filter(|_| provider == ProviderKind::Openrouter)
345 .and_then(|(config, identity)| config.provider_config_for(identity))
346 .and_then(|entry| entry.vendor.as_deref())
347 .map(str::trim)
348 .filter(|vendor| !vendor.is_empty())
349 .map(sanitize_persisted_route_label),
350 billing_surface: admission
351 .map_or_else(
352 || crate::pricing::billing_surface_for_route(provider, base_url),
353 |(config, identity)| {
354 crate::route_billing::billing_surface_for_dispatch(
355 Some(config),
356 identity,
357 base_url,
358 )
359 },
360 )
361 .map(str::to_string),
362 endpoint_fingerprint,
363 provider_live_pricing,
364 billing_mode: billing.into(),
365 dispatched_at,
366 }
367 }
368
369 #[must_use]
370 pub fn audit(&self, usage: &Usage) -> TurnCostAudit {
371 let reviewed_custom_metered = crate::pricing::reviewed_custom_route_is_metered(
372 self.provider,
373 self.endpoint_fingerprint.as_deref(),
374 );
375 let declared_quote = self.provider_live_pricing.as_ref().filter(|quote| {
376 quote.provenance == codewhale_config::pricing::PricingProvenance::UserOverride
377 && self
378 .endpoint_fingerprint
379 .as_deref()
380 .zip(u64::try_from(self.dispatched_at.timestamp()).ok())
381 .is_some_and(|(fingerprint, at)| {
382 quote
383 .pricing_for_route(
384 self.provider,
385 &self.provider_identity,
386 &self.model,
387 fingerprint,
388 at,
389 )
390 .is_some()
391 })
392 });
393 let declared_estimate = declared_quote.is_some();
394 match self.billing_mode {
395 RouteBillingMode::Subscription | RouteBillingMode::Local => {
396 return TurnCostAudit::unpriced(crate::pricing::UnpricedReason::NotMoneyMetered);
397 }
398 RouteBillingMode::Unknown if !reviewed_custom_metered && !declared_estimate => {
399 return TurnCostAudit::unpriced(
400 crate::pricing::UnpricedReason::UnknownBillingBasis,
401 );
402 }
403 RouteBillingMode::Metered | RouteBillingMode::Unknown => {}
404 }
405 // The OpenRouter model catalog does not identify a pinned upstream's
406 // price. An endpoint match alone must not promote that aggregate rate.
407 // An operator's own declared rate for this exact route is not the
408 // aggregate catalog, so it still prices the pinned turn.
409 if self.provider == ProviderKind::Openrouter
410 && self.openrouter_vendor.is_some()
411 && !declared_quote.is_some_and(|quote| quote.carries_rates())
412 {
413 return TurnCostAudit::unpriced(crate::pricing::UnpricedReason::RoutingDependentPrice);
414 }
415 crate::pricing::audit_turn_cost_for_route_on_endpoint_for_identity_at(
416 self.provider,
417 Some(&self.provider_identity),
418 &self.model,
419 self.billing_surface.as_deref(),
420 self.endpoint_fingerprint.as_deref(),
421 self.provider_live_pricing.as_ref(),
422 usage,
423 self.dispatched_at,
424 )
425 }
426
427 #[must_use]
428 pub fn receipt(&self, audit: &TurnCostAudit) -> String {
429 let route = self.sanitized_for_persistence();
430 let mut receipt = route_receipt(
431 route.provider,
432 Some(&route.provider_identity),
433 &route.model,
434 route.billing_surface.as_deref(),
435 route.endpoint_fingerprint.as_deref(),
436 route.billing_mode,
437 currency_tag(audit),
438 );
439 if let Some(vendor) = route.openrouter_vendor.as_deref() {
440 receipt.push_str(" openrouter_vendor=");
441 receipt.push_str(&safe_receipt_field(vendor));
442 }
443 receipt
444 }
445
446 /// Redact filesystem-like labels before a route crosses a persistence or
447 /// metadata boundary. Ordinary provider model namespaces such as
448 /// `anthropic/claude-*` remain intact; absolute/local path forms do not.
449 #[must_use]
450 pub fn sanitized_for_persistence(&self) -> Self {
451 let mut route = self.clone();
452 route.provider_identity = sanitize_persisted_route_label(&route.provider_identity);
453 route.model = sanitize_persisted_route_label(&route.model);
454 route.openrouter_vendor = route
455 .openrouter_vendor
456 .as_deref()
457 .map(sanitize_persisted_route_label);
458 route.billing_surface = route
459 .billing_surface
460 .as_deref()
461 .map(sanitize_persisted_route_label);
462 route.endpoint_fingerprint =
463 route
464 .endpoint_fingerprint
465 .as_deref()
466 .and_then(|fingerprint| {
467 let fingerprint = fingerprint.trim();
468 (fingerprint.len() == 64
469 && fingerprint.bytes().all(|byte| byte.is_ascii_hexdigit()))
470 .then(|| fingerprint.to_ascii_lowercase())
471 });
472 let quote_is_valid = route
473 .provider_live_pricing
474 .as_ref()
475 .zip(route.endpoint_fingerprint.as_deref())
476 .and_then(|(quote, fingerprint)| {
477 u64::try_from(route.dispatched_at.timestamp())
478 .ok()
479 .and_then(|dispatched_at_unix| {
480 quote.pricing_for_route(
481 route.provider,
482 &route.provider_identity,
483 &route.model,
484 fingerprint,
485 dispatched_at_unix,
486 )
487 })
488 })
489 .is_some();
490 if !quote_is_valid {
491 route.provider_live_pricing = None;
492 }
493 route
494 }
495 }
496
497 fn receipt_with_usage_classes(mut receipt: String, usage: &Usage) -> String {
498 let classes = crate::pricing::token_usage_for_pricing(usage);
499 if classes.cache_write > 0 {
500 receipt.push_str(" cache_write=yes");
501 }
502 if usage.reasoning_tokens.unwrap_or(0) > 0 {
503 receipt.push_str(" reasoning=yes");
504 }
505 receipt
506 }
507
508 /// Canonical redacted route receipt for one exact usage payload.
509 #[must_use]
510 pub fn effective_route_usage_receipt(
511 route: &EffectiveRouteEnvelope,
512 audit: &TurnCostAudit,
513 usage: &Usage,
514 ) -> String {
515 receipt_with_usage_classes(route.receipt(audit), usage)
516 }
517
518 /// Canonical `child_*` token and route metadata for tools that make their own
519 /// LLM calls (`review`, `verify`, and `rlm`). Keeping this next to the immutable
520 /// route envelope prevents the pure model types from depending on app config.
521 #[must_use]
522 pub fn child_usage_metadata_fields(
523 route: &EffectiveRouteEnvelope,
524 usage: &Usage,
525 ) -> serde_json::Map<String, serde_json::Value> {
526 let route = route.sanitized_for_persistence();
527 let mut fields = serde_json::Map::new();
528 fields.insert("child_provider".into(), serde_json::json!(route.provider));
529 fields.insert(
530 "child_provider_identity".into(),
531 serde_json::json!(route.provider_identity),
532 );
533 fields.insert("child_model".into(), serde_json::json!(route.model));
534 fields.insert(
535 "child_openrouter_vendor".into(),
536 serde_json::json!(route.openrouter_vendor),
537 );
538 fields.insert(
539 "child_billing_surface".into(),
540 serde_json::json!(route.billing_surface),
541 );
542 fields.insert(
543 "child_endpoint_fingerprint".into(),
544 serde_json::json!(route.endpoint_fingerprint),
545 );
546 fields.insert(
547 "child_provider_live_pricing".into(),
548 serde_json::json!(route.provider_live_pricing),
549 );
550 fields.insert(
551 "child_billing_mode".into(),
552 serde_json::json!(route.billing_mode),
553 );
554 fields.insert(
555 "child_dispatched_at".into(),
556 serde_json::json!(route.dispatched_at),
557 );
558 fields.insert(
559 "child_input_tokens".into(),
560 serde_json::json!(usage.input_tokens),
561 );
562 fields.insert(
563 "child_output_tokens".into(),
564 serde_json::json!(usage.output_tokens),
565 );
566 fields.insert(
567 "child_prompt_cache_hit_tokens".into(),
568 serde_json::json!(usage.prompt_cache_hit_tokens),
569 );
570 fields.insert(
571 "child_prompt_cache_miss_tokens".into(),
572 serde_json::json!(usage.prompt_cache_miss_tokens),
573 );
574 fields.insert(
575 "child_prompt_cache_write_tokens".into(),
576 serde_json::json!(usage.prompt_cache_write_tokens),
577 );
578 // Informational: reasoning tokens are already included in output tokens.
579 fields.insert(
580 "child_reasoning_tokens".into(),
581 serde_json::json!(usage.reasoning_tokens),
582 );
583 fields.insert(
584 "child_reasoning_replay_tokens".into(),
585 serde_json::json!(usage.reasoning_replay_tokens),
586 );
587 fields.insert(
588 "child_server_tool_use".into(),
589 serde_json::json!(usage.server_tool_use),
590 );
591 fields
592 }
593
594 /// Merge canonical child usage into a tool metadata object.
595 pub fn attach_child_usage_metadata(
596 metadata: &mut serde_json::Value,
597 route: &EffectiveRouteEnvelope,
598 usage: &Usage,
599 ) {
600 if let Some(object) = metadata.as_object_mut() {
601 object.extend(child_usage_metadata_fields(route, usage));
602 }
603 }
604
605 /// Maximum number of distinct routed-usage segments accepted from one tool
606 /// result. RLM reserves against the same bound before dispatch, so a valid
607 /// producer never has to discard a provider receipt after doing the work.
608 pub const MAX_CHILD_USAGE_RECORDS: usize = 64;
609
610 const CHILD_USAGE_RECORDS_KEY: &str = "child_usage_records";
611 const CHILD_DECISION_RECEIPTS_KEY: &str = "child_decision_receipts";
612 const CHILD_USAGE_DROP_RECORDS_KEY: &str = "child_usage_drop_records";
613 const CHILD_USAGE_DROPPED_RECORDS_KEY: &str = "child_usage_dropped_records";
614
615 /// Attach a bounded batch of routed child usage to tool metadata.
616 ///
617 /// The source identity is reduced to a one-way fingerprint before metadata
618 /// can enter a transcript. Routes pass through their persistence sanitizer,
619 /// so neither a raw response id nor an endpoint/credential can hitch a ride.
620 /// New consumers prefer this batch over the legacy single `child_*` fields.
621 /// Attach a bounded batch containing both exact usage receipts and exact
622 /// provider-success/missing-usage route receipts.
623 pub fn attach_child_usage_batch_metadata(
624 metadata: &mut serde_json::Value,
625 batch: &RuntimeUsageBatch,
626 ) {
627 let Some(object) = metadata.as_object_mut() else {
628 return;
629 };
630 object.insert(
631 CHILD_DECISION_RECEIPTS_KEY.into(),
632 serde_json::json!(
633 batch
634 .decisions
635 .iter()
636 .filter(|receipt| receipt.is_bounded())
637 .take(MAX_CHILD_USAGE_RECORDS)
638 .map(RuntimeDecisionReceipt::sanitized)
639 .collect::<Vec<_>>()
640 ),
641 );
642 let retained_records = batch
643 .records
644 .iter()
645 .take(MAX_CHILD_USAGE_RECORDS)
646 .map(|record| {
647 serde_json::json!({
648 "source_id": format!(
649 "routed:{}",
650 usage_source_fingerprint(&record.source_id)
651 ),
652 "route": record.usage.route.sanitized_for_persistence(),
653 "usage": record.usage.usage,
654 })
655 })
656 .collect::<Vec<_>>();
657 let remaining = MAX_CHILD_USAGE_RECORDS.saturating_sub(retained_records.len());
658 let retained_drops = batch
659 .drop_records
660 .iter()
661 .take(remaining)
662 .map(|record| RuntimeUsageDropRecord {
663 reason: record.reason,
664 source_id: format!("routed:{}", usage_source_fingerprint(&record.source_id)),
665 route: record.route.sanitized_for_persistence(),
666 })
667 .collect::<Vec<_>>();
668 object.insert(
669 CHILD_USAGE_RECORDS_KEY.into(),
670 serde_json::json!(retained_records),
671 );
672 object.insert(
673 CHILD_USAGE_DROP_RECORDS_KEY.into(),
674 serde_json::json!(retained_drops),
675 );
676 let usage_overflow = batch.records.len().saturating_sub(MAX_CHILD_USAGE_RECORDS);
677 let decision_overflow = batch
678 .decisions
679 .len()
680 .saturating_sub(MAX_CHILD_USAGE_RECORDS)
681 .saturating_add(
682 batch
683 .decisions
684 .iter()
685 .take(MAX_CHILD_USAGE_RECORDS)
686 .filter(|r| !r.is_bounded())
687 .count(),
688 );
689 let dropped_records = batch
690 .dropped_records
691 .max(u64::try_from(batch.drop_records.len()).unwrap_or(u64::MAX))
692 .saturating_add(u64::try_from(usage_overflow).unwrap_or(u64::MAX))
693 .saturating_add(u64::try_from(decision_overflow).unwrap_or(u64::MAX));
694 if dropped_records > 0 {
695 object.insert(
696 CHILD_USAGE_DROPPED_RECORDS_KEY.into(),
697 serde_json::json!(dropped_records),
698 );
699 } else {
700 object.remove(CHILD_USAGE_DROPPED_RECORDS_KEY);
701 }
702 }
703
704 /// Parse the preferred routed child-usage batch.
705 ///
706 /// `None` means the batch key was absent and callers may use the legacy
707 /// single-record parser. Once the key is present, malformed/overflow entries
708 /// are represented by `dropped_records` instead of falling back and risking a
709 /// partial subtotal being presented as complete.
710 #[must_use]
711 pub fn child_usage_records_from_metadata(
712 metadata: &serde_json::Value,
713 ) -> Option<RuntimeUsageBatch> {
714 let value = metadata.get(CHILD_USAGE_RECORDS_KEY)?;
715 let drop_values = metadata
716 .get(CHILD_USAGE_DROP_RECORDS_KEY)
717 .and_then(serde_json::Value::as_array)
718 .map(Vec::as_slice)
719 .unwrap_or_default();
720 let declared_dropped = metadata
721 .get(CHILD_USAGE_DROPPED_RECORDS_KEY)
722 .and_then(serde_json::Value::as_u64)
723 .unwrap_or(0);
724 let Some(values) = value.as_array() else {
725 return Some(RuntimeUsageBatch {
726 decisions: Vec::new(),
727 records: Vec::new(),
728 drop_records: Vec::new(),
729 dropped_records: declared_dropped.saturating_add(1),
730 });
731 };
732
733 let overflow = values.len().saturating_sub(MAX_CHILD_USAGE_RECORDS);
734 let mut batch = RuntimeUsageBatch {
735 decisions: Vec::new(),
736 records: Vec::with_capacity(values.len().min(MAX_CHILD_USAGE_RECORDS)),
737 drop_records: Vec::with_capacity(drop_values.len().min(MAX_CHILD_USAGE_RECORDS)),
738 dropped_records: declared_dropped
739 .max(u64::try_from(drop_values.len()).unwrap_or(u64::MAX))
740 .saturating_add(u64::try_from(overflow).unwrap_or(u64::MAX)),
741 };
742 for value in values.iter().take(MAX_CHILD_USAGE_RECORDS) {
743 let parsed = (|| {
744 let source_id = value.get("source_id")?.as_str()?;
745 let route =
746 serde_json::from_value::<EffectiveRouteEnvelope>(value.get("route")?.clone())
747 .ok()?
748 .sanitized_for_persistence();
749 let usage = serde_json::from_value::<Usage>(value.get("usage")?.clone()).ok()?;
750 Some(RuntimeUsageRecord {
751 // Treat metadata as an untrusted persistence boundary. A
752 // stable hash preserves idempotence without retaining the
753 // producer's raw identifier.
754 source_id: usage_source_fingerprint(source_id),
755 usage: EffectiveRouteUsage { route, usage },
756 })
757 })();
758 if let Some(record) = parsed {
759 batch.records.push(record);
760 } else {
761 batch.dropped_records = batch.dropped_records.saturating_add(1);
762 }
763 }
764 let remaining = MAX_CHILD_USAGE_RECORDS.saturating_sub(batch.records.len());
765 for value in drop_values.iter().take(remaining) {
766 let parsed = (|| {
767 let source_id = value.get("source_id")?.as_str()?;
768 let route =
769 serde_json::from_value::<EffectiveRouteEnvelope>(value.get("route")?.clone())
770 .ok()?
771 .sanitized_for_persistence();
772 Some(RuntimeUsageDropRecord {
773 reason: value
774 .get("reason")
775 .map(|value| serde_json::from_value(value.clone()))
776 .transpose()
777 .ok()?
778 .unwrap_or_default(),
779 source_id: usage_source_fingerprint(source_id),
780 route,
781 })
782 })();
783 if let Some(record) = parsed {
784 batch.drop_records.push(record);
785 }
786 // Every declared drop slot already contributes to dropped_records,
787 // including malformed entries; do not count the same gap twice.
788 }
789 if let Some(decisions) = metadata.get(CHILD_DECISION_RECEIPTS_KEY) {
790 if let Some(values) = decisions.as_array() {
791 for value in values.iter().take(MAX_CHILD_USAGE_RECORDS) {
792 let bounded = serde_json::to_vec(value).is_ok_and(|bytes| bytes.len() <= 8 * 1024);
793 let parsed = bounded
794 .then(|| serde_json::from_value::<RuntimeDecisionReceipt>(value.clone()).ok())
795 .flatten();
796 if let Some(receipt) = parsed.filter(RuntimeDecisionReceipt::is_bounded) {
797 batch.decisions.push(receipt.sanitized());
798 } else {
799 batch.dropped_records = batch.dropped_records.saturating_add(1);
800 }
801 }
802 batch.dropped_records = batch.dropped_records.saturating_add(
803 u64::try_from(values.len().saturating_sub(MAX_CHILD_USAGE_RECORDS))
804 .unwrap_or(u64::MAX),
805 );
806 } else {
807 batch.dropped_records = batch.dropped_records.saturating_add(1);
808 }
809 }
810 Some(batch)
811 }
812
813 /// Rehydrate the immutable route envelope emitted with child usage. Legacy or
814 /// incomplete metadata becomes an explicitly unknown route and never borrows
815 /// mutable parent-session facts.
816 #[must_use]
817 pub fn child_route_envelope_from_metadata(
818 metadata: &serde_json::Value,
819 ) -> Option<EffectiveRouteEnvelope> {
820 let model = metadata.get("child_model")?.as_str()?.to_string();
821 let provider = metadata
822 .get("child_provider")
823 .cloned()
824 .and_then(|value| serde_json::from_value(value).ok());
825 let provider_identity = metadata
826 .get("child_provider_identity")
827 .and_then(serde_json::Value::as_str)
828 .map(str::to_string);
829 let billing_mode = metadata
830 .get("child_billing_mode")
831 .cloned()
832 .and_then(|value| serde_json::from_value(value).ok());
833 let dispatched_at = metadata
834 .get("child_dispatched_at")
835 .cloned()
836 .and_then(|value| serde_json::from_value(value).ok());
837
838 let complete = provider.is_some()
839 && provider_identity.is_some()
840 && billing_mode.is_some()
841 && dispatched_at.is_some();
842 Some(
843 EffectiveRouteEnvelope {
844 provider: provider.unwrap_or(ProviderKind::Custom),
845 provider_identity: provider_identity.unwrap_or_else(|| "legacy-unreported".to_string()),
846 model,
847 openrouter_vendor: metadata
848 .get("child_openrouter_vendor")
849 .and_then(serde_json::Value::as_str)
850 .map(str::to_string),
851 billing_surface: metadata
852 .get("child_billing_surface")
853 .and_then(serde_json::Value::as_str)
854 .map(str::to_string),
855 endpoint_fingerprint: metadata
856 .get("child_endpoint_fingerprint")
857 .and_then(serde_json::Value::as_str)
858 .map(str::to_string),
859 provider_live_pricing: metadata
860 .get("child_provider_live_pricing")
861 .cloned()
862 .and_then(|value| serde_json::from_value(value).ok()),
863 billing_mode: billing_mode
864 .filter(|_| complete)
865 .unwrap_or(RouteBillingMode::Unknown),
866 dispatched_at: dispatched_at.unwrap_or_else(|| {
867 DateTime::<Utc>::from_timestamp(0, 0).expect("Unix epoch is representable")
868 }),
869 }
870 .sanitized_for_persistence(),
871 )
872 }
873
874 /// Rehydrate the complete child usage payload emitted by
875 /// [`attach_child_usage_metadata`]. The presence of a canonical child token
876 /// field is significant even when every value is zero: a zero-usage provider
877 /// call still needs a route receipt and coverage classification.
878 #[must_use]
879 pub fn child_usage_from_metadata(metadata: &serde_json::Value) -> Option<Usage> {
880 const TOKEN_FIELDS: &[&str] = &[
881 "child_input_tokens",
882 "child_output_tokens",
883 "child_prompt_cache_hit_tokens",
884 "child_prompt_cache_miss_tokens",
885 "child_prompt_cache_write_tokens",
886 "child_reasoning_tokens",
887 "child_reasoning_replay_tokens",
888 ];
889 if !TOKEN_FIELDS
890 .iter()
891 .any(|field| metadata.get(field).is_some())
892 {
893 return None;
894 }
895
896 fn u32_field(metadata: &serde_json::Value, field: &str) -> Option<u32> {
897 metadata
898 .get(field)
899 .and_then(serde_json::Value::as_u64)
900 .map(|value| u32::try_from(value).unwrap_or(u32::MAX))
901 }
902
903 Some(Usage {
904 input_tokens: u32_field(metadata, "child_input_tokens").unwrap_or(0),
905 output_tokens: u32_field(metadata, "child_output_tokens").unwrap_or(0),
906 prompt_cache_hit_tokens: u32_field(metadata, "child_prompt_cache_hit_tokens"),
907 prompt_cache_miss_tokens: u32_field(metadata, "child_prompt_cache_miss_tokens"),
908 prompt_cache_write_tokens: u32_field(metadata, "child_prompt_cache_write_tokens"),
909 reasoning_tokens: u32_field(metadata, "child_reasoning_tokens"),
910 reasoning_replay_tokens: u32_field(metadata, "child_reasoning_replay_tokens"),
911 server_tool_use: metadata
912 .get("child_server_tool_use")
913 .cloned()
914 .and_then(|value| serde_json::from_value(value).ok()),
915 })
916 }
917
918 impl PendingBackgroundCost {
919 /// Whether anything at all was accrued.
920 ///
921 /// Compared against `Default` rather than checking a subset of fields, so a
922 /// field added later cannot be silently left out of the emptiness test.
923 #[must_use]
924 pub fn is_empty(&self) -> bool {
925 *self == Self::default()
926 }
927 }
928
929 #[derive(Default)]
930 struct ScopedPendingBackgroundCost {
931 generation: u64,
932 pending: PendingBackgroundCost,
933 /// All provider responses accepted in this session generation, including
934 /// batches already drained into the live session projection.
935 seen_usage_source_fingerprints: HashSet<String>,
936 missing_usage_sources: BTreeMap<String, MissingUsageCoverage>,
937 missing_usage_overflowed: bool,
938 }
939
940 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
941 pub struct CostScopeToken(u64);
942
943 #[cfg(not(test))]
944 static PENDING: OnceLock<Mutex<ScopedPendingBackgroundCost>> = OnceLock::new();
945
946 #[cfg(test)]
947 static TEST_PENDING: OnceLock<
948 Mutex<std::collections::HashMap<std::thread::ThreadId, ScopedPendingBackgroundCost>>,
949 > = OnceLock::new();
950
951 fn with_pending_state_mut<R>(f: impl FnOnce(&mut ScopedPendingBackgroundCost) -> R) -> R {
952 #[cfg(not(test))]
953 {
954 let mut pending = PENDING
955 .get_or_init(|| Mutex::new(ScopedPendingBackgroundCost::default()))
956 .lock()
957 .unwrap_or_else(|e| e.into_inner());
958 f(&mut pending)
959 }
960 #[cfg(test)]
961 {
962 // Rust tests run concurrently. A test-local collector prevents a UI
963 // drain or successful purge in one test from stealing another test's
964 // accounting. Tokio's default test runtime is current-thread, so async
965 // helpers retain this scope across awaits.
966 let mut by_thread = TEST_PENDING
967 .get_or_init(|| Mutex::new(std::collections::HashMap::new()))
968 .lock()
969 .unwrap_or_else(|e| e.into_inner());
970 f(by_thread.entry(test_cost_scope_id()).or_default())
971 }
972 }
973
974 /// Runtime accounting gets a cloned, owner-scoped copy of compaction usage.
975 /// This journal is deliberately separate from the TUI pending-money pool:
976 /// taking one runtime owner's records cannot steal or reset the foreground
977 /// session's `/cost` state.
978 const MAX_RUNTIME_USAGE_RECORDS_PER_OWNER: usize = 64;
979
980 #[derive(Default)]
981 struct OwnerRuntimeUsageJournal {
982 records: VecDeque<RuntimeUsageRecord>,
983 decisions: VecDeque<RuntimeDecisionReceipt>,
984 drop_records: VecDeque<RuntimeUsageDropRecord>,
985 dropped_records: u64,
986 dropped_source_fingerprints: HashSet<String>,
987 dropped_fingerprint_overflowed: bool,
988 }
989
990 type RuntimeUsageJournal = HashMap<String, OwnerRuntimeUsageJournal>;
991
992 /// Bounded fallback batch returned when no synchronous runtime sink was
993 /// available. `dropped_records` is persisted into the turn so aggregates fail
994 /// closed instead of silently presenting a partial cost as complete.
995 #[derive(Debug, Clone, Default, PartialEq, Eq)]
996 pub struct RuntimeUsageBatch {
997 /// Bounded decision evidence, owned and retired with the same usage ledger.
998 pub decisions: Vec<RuntimeDecisionReceipt>,
999 pub records: Vec<RuntimeUsageRecord>,
1000 /// Exact provider-success calls whose usage payload was absent. The
1001 /// bounded records retain route billing truth; `dropped_records` remains
1002 /// the authoritative total and may exceed this vector after overflow.
1003 pub drop_records: Vec<RuntimeUsageDropRecord>,
1004 pub dropped_records: u64,
1005 }
1006
1007 /// One owner-scoped usage report with the stable provider-call identity used
1008 /// to make durable replay idempotent.
1009 #[derive(Debug, Clone, PartialEq, Eq)]
1010 pub struct RuntimeUsageRecord {
1011 pub source_id: String,
1012 pub usage: EffectiveRouteUsage,
1013 }
1014
1015 /// One provider-success response that omitted usage metadata.
1016 ///
1017 /// The frozen route is required to distinguish money-metered calls from
1018 /// subscription/local calls without consulting mutable completion-time config.
1019 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1020 pub struct RuntimeUsageDropRecord {
1021 #[serde(default, skip_serializing_if = "RuntimeUsageMissingReason::is_success")]
1022 pub reason: RuntimeUsageMissingReason,
1023 pub source_id: String,
1024 pub route: EffectiveRouteEnvelope,
1025 }
1026
1027 /// Why this dispatched request has no usable usage receipt. Legacy bytes
1028 /// mean a successful response omitted usage, never an inferred failure charge.
1029 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1030 #[serde(rename_all = "snake_case")]
1031 pub enum RuntimeUsageMissingReason {
1032 #[default]
1033 SuccessWithoutUsage,
1034 RequestOutcomeUnknown,
1035 }
1036 impl RuntimeUsageMissingReason {
1037 pub fn is_success(&self) -> bool {
1038 *self == Self::SuccessWithoutUsage
1039 }
1040 pub(crate) fn label(self) -> &'static str {
1041 match self {
1042 Self::SuccessWithoutUsage => "provider_success_missing_usage",
1043 Self::RequestOutcomeUnknown => "request_outcome_unknown",
1044 }
1045 }
1046 }
1047 /// Minimal, redacted unresolved-source metadata carried by the existing cost
1048 /// snapshot. It retains no endpoint/model/secret and never infers token usage.
1049 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1050 pub struct MissingUsageCoverage {
1051 pub reason: RuntimeUsageMissingReason,
1052 pub money_metered: bool,
1053 pub route_sha256: String,
1054 }
1055 pub(crate) const MAX_MISSING_USAGE_SOURCES: usize = 64;
1056 pub(crate) fn deserialize_missing_usage_sources<'de, D>(
1057 d: D,
1058 ) -> Result<BTreeMap<String, MissingUsageCoverage>, D::Error>
1059 where
1060 D: serde::Deserializer<'de>,
1061 {
1062 use serde::Deserialize as _;
1063 let values = BTreeMap::<String, MissingUsageCoverage>::deserialize(d)?;
1064 if values.len() > MAX_MISSING_USAGE_SOURCES
1065 || values
1066 .keys()
1067 .any(|key| key.len() != 64 || !key.bytes().all(|b| b.is_ascii_hexdigit()))
1068 || values.values().any(|value| {
1069 !value.route_sha256.is_empty()
1070 && (value.route_sha256.len() != 64
1071 || !value
1072 .route_sha256
1073 .bytes()
1074 .all(|byte| byte.is_ascii_hexdigit()))
1075 })
1076 {
1077 return Err(serde::de::Error::custom(
1078 "unbounded or invalid missing-usage source ledger",
1079 ));
1080 }
1081 Ok(values)
1082 }
1083 impl MissingUsageCoverage {
1084 pub(crate) fn for_route(
1085 route: &EffectiveRouteEnvelope,
1086 reason: RuntimeUsageMissingReason,
1087 ) -> Self {
1088 Self {
1089 reason,
1090 money_metered: !matches!(
1091 route.billing_mode,
1092 RouteBillingMode::Subscription | RouteBillingMode::Local
1093 ),
1094 route_sha256: serde_json::to_string(&route.sanitized_for_persistence())
1095 .map(|value| usage_source_fingerprint(&value))
1096 .unwrap_or_default(),
1097 }
1098 }
1099 pub(crate) fn matches_route(&self, route: &EffectiveRouteEnvelope) -> bool {
1100 !self.route_sha256.is_empty()
1101 && self.route_sha256 == Self::for_route(route, self.reason).route_sha256
1102 }
1103 }
1104 impl RuntimeUsageDropRecord {
1105 pub(crate) fn coverage(&self) -> MissingUsageCoverage {
1106 MissingUsageCoverage::for_route(&self.route, self.reason)
1107 }
1108 }
1109 /// Provider decision evidence attached to the existing origin-turn ledger.
1110 /// It is diagnostic evidence, never an instruction to change the model route.
1111 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1112 pub struct RuntimeDecisionReceipt {
1113 pub source_id: String,
1114 pub route: EffectiveRouteEnvelope,
1115 pub usage: Option<Usage>,
1116 #[serde(default)]
1117 pub usage_complete: bool,
1118 pub shadow: bool,
1119 pub valid_answers: bool,
1120 pub evidence: crate::model_routing::AutoRouteDecisionEvidence,
1121 }
1122
1123 impl RuntimeDecisionReceipt {
1124 pub(crate) fn is_bounded(&self) -> bool {
1125 !self.source_id.trim().is_empty()
1126 && self.source_id.len() <= 128
1127 && self.evidence.choice.len() <= 128
1128 && self.evidence.probabilities_bp.len() <= 64
1129 && self
1130 .evidence
1131 .probabilities_bp
1132 .iter()
1133 .all(|(key, value)| key.len() <= 128 && *value <= 10_000)
1134 && self.evidence.confidence_bp <= 10_000
1135 && self.evidence.min_confidence_bp <= 10_000
1136 && self
1137 .evidence
1138 .thinking
1139 .as_ref()
1140 .is_none_or(|v| v.len() <= 128)
1141 && self
1142 .evidence
1143 .response_model
1144 .as_ref()
1145 .is_none_or(|v| v.len() <= 128)
1146 && self
1147 .evidence
1148 .provider_reported_cost_usd
1149 .as_ref()
1150 .is_none_or(|v| {
1151 v.len() <= 128
1152 && v.parse::<f64>()
1153 .is_ok_and(|cost| cost.is_finite() && cost >= 0.0)
1154 })
1155 && serde_json::to_vec(self).is_ok_and(|bytes| bytes.len() <= 8 * 1024)
1156 }
1157
1158 pub(crate) fn sanitized(&self) -> Self {
1159 let mut receipt = self.clone();
1160 receipt.source_id = usage_source_fingerprint(&receipt.source_id);
1161 receipt.route = receipt.route.sanitized_for_persistence();
1162 receipt.evidence.choice = sanitize_persisted_route_label(&receipt.evidence.choice);
1163 receipt.evidence.thinking = receipt
1164 .evidence
1165 .thinking
1166 .as_deref()
1167 .map(sanitize_persisted_route_label);
1168 receipt.evidence.response_model = receipt
1169 .evidence
1170 .response_model
1171 .as_deref()
1172 .map(sanitize_persisted_route_label);
1173 receipt.evidence.probabilities_bp = receipt
1174 .evidence
1175 .probabilities_bp
1176 .into_iter()
1177 .map(|(key, value)| (sanitize_persisted_route_label(&key), value))
1178 .collect();
1179 receipt
1180 }
1181
1182 pub(crate) fn diagnostic_receipt(&self) -> String {
1183 // Reuse cost_status's persisted diagnostic receipt surface. No prompt,
1184 // response id, URL, auth header or unoffered choice enters this string.
1185 format!(
1186 "decision:{}",
1187 serde_json::to_string(&self.sanitized()).unwrap_or_default()
1188 )
1189 }
1190 }
1191
1192 pub(crate) type RuntimeDecisionSink = Arc<dyn Fn(RuntimeDecisionReceipt) -> bool + Send + Sync>;
1193
1194 pub(crate) type RuntimeUsageSink = Arc<dyn Fn(RuntimeUsageRecord) -> bool + Send + Sync>;
1195 pub(crate) type RuntimeUsageDropSink = Arc<dyn Fn(RuntimeUsageDropRecord) -> bool + Send + Sync>;
1196
1197 struct RuntimeUsageSinkEntry {
1198 sink: RuntimeUsageSink,
1199 dropped_sink: Option<RuntimeUsageDropSink>,
1200 decision_sink: Option<RuntimeDecisionSink>,
1201 leases: usize,
1202 terminal: bool,
1203 }
1204
1205 #[cfg(test)]
1206 pub(crate) fn decision_receipt_fixture(source_id: &str) -> RuntimeDecisionReceipt {
1207 let mut route = EffectiveRouteEnvelope::capture(
1208 None,
1209 crate::config::ProviderKind::Custom,
1210 "typesafe",
1211 "jev-latest",
1212 Some("https://api.typesafe.ai/v1"),
1213 Utc::now(),
1214 );
1215 route.billing_mode = RouteBillingMode::Unknown;
1216 RuntimeDecisionReceipt {
1217 source_id: source_id.to_string(),
1218 route,
1219 usage: Some(Usage {
1220 input_tokens: 9,
1221 output_tokens: 4,
1222 ..Default::default()
1223 }),
1224 usage_complete: true,
1225 shadow: true,
1226 valid_answers: false,
1227 evidence: crate::model_routing::AutoRouteDecisionEvidence {
1228 choice: "invalid".to_string(),
1229 probabilities_bp: Default::default(),
1230 confidence_bp: 0,
1231 min_confidence_bp: 5_000,
1232 cost_saving_kept_fast: false,
1233 thinking: None,
1234 provider_reported_cost_usd: Some("0.000012054".to_string()),
1235 latency_ms: 12,
1236 response_model: Some("jev-latest".to_string()),
1237 },
1238 }
1239 }
1240
1241 /// Keeps an owner sink alive while a detached child can still report usage.
1242 /// The runtime turn may already be terminal; the last child release retires
1243 /// the sink only after its final provider response has been durably appended.
1244 #[derive(Debug)]
1245 pub(crate) struct RuntimeUsageLease {
1246 owner: String,
1247 active: bool,
1248 }
1249
1250 #[cfg(not(test))]
1251 static RUNTIME_USAGE_JOURNAL: OnceLock<Mutex<RuntimeUsageJournal>> = OnceLock::new();
1252
1253 #[cfg(test)]
1254 static TEST_RUNTIME_USAGE_JOURNAL: OnceLock<
1255 Mutex<std::collections::HashMap<std::thread::ThreadId, RuntimeUsageJournal>>,
1256 > = OnceLock::new();
1257
1258 #[cfg(not(test))]
1259 static RUNTIME_USAGE_SINKS: OnceLock<Mutex<HashMap<String, RuntimeUsageSinkEntry>>> =
1260 OnceLock::new();
1261
1262 /// Sinks are keyed by owner id, and owner ids in tests are short fixture
1263 /// strings that repeat across tests. Under the default parallel test harness a
1264 /// process-global map let one test's `register_runtime_usage_sink` replace
1265 /// another's live sink, and let one test's `finish_runtime_usage_owner` retire
1266 /// it — turning exactly-once child accounting into an order-dependent race.
1267 /// Scoping by thread matches the pending-cost pool and the runtime journal,
1268 /// which are already thread-scoped for the same reason.
1269 #[cfg(test)]
1270 #[allow(clippy::type_complexity)]
1271 static TEST_RUNTIME_USAGE_SINKS: OnceLock<
1272 Mutex<HashMap<std::thread::ThreadId, HashMap<String, RuntimeUsageSinkEntry>>>,
1273 > = OnceLock::new();
1274
1275 /// Run `f` against this scope's sink registry.
1276 fn with_runtime_usage_sinks<R>(
1277 f: impl FnOnce(&mut HashMap<String, RuntimeUsageSinkEntry>) -> R,
1278 ) -> R {
1279 #[cfg(not(test))]
1280 {
1281 let mut sinks = RUNTIME_USAGE_SINKS
1282 .get_or_init(|| Mutex::new(HashMap::new()))
1283 .lock()
1284 .unwrap_or_else(|error| error.into_inner());
1285 f(&mut sinks)
1286 }
1287 #[cfg(test)]
1288 {
1289 let mut by_thread = TEST_RUNTIME_USAGE_SINKS
1290 .get_or_init(|| Mutex::new(HashMap::new()))
1291 .lock()
1292 .unwrap_or_else(|error| error.into_inner());
1293 f(by_thread.entry(test_cost_scope_id()).or_default())
1294 }
1295 }
1296
1297 /// Like [`with_runtime_usage_sinks`], but does not create the registry when it
1298 /// has never been initialized. Used on drop paths, where allocating a registry
1299 /// to then find it empty would be pointless.
1300 fn with_existing_runtime_usage_sinks<R>(
1301 f: impl FnOnce(&mut HashMap<String, RuntimeUsageSinkEntry>) -> R,
1302 ) -> Option<R> {
1303 #[cfg(not(test))]
1304 {
1305 let sinks = RUNTIME_USAGE_SINKS.get()?;
1306 let mut sinks = sinks.lock().unwrap_or_else(|error| error.into_inner());
1307 Some(f(&mut sinks))
1308 }
1309 #[cfg(test)]
1310 {
1311 let by_thread = TEST_RUNTIME_USAGE_SINKS.get()?;
1312 let mut by_thread = by_thread.lock().unwrap_or_else(|error| error.into_inner());
1313 let sinks = by_thread.get_mut(&test_cost_scope_id())?;
1314 Some(f(sinks))
1315 }
1316 }
1317
1318 fn with_runtime_usage_journal_mut<R>(f: impl FnOnce(&mut RuntimeUsageJournal) -> R) -> R {
1319 #[cfg(not(test))]
1320 {
1321 let mut journal = RUNTIME_USAGE_JOURNAL
1322 .get_or_init(|| Mutex::new(HashMap::new()))
1323 .lock()
1324 .unwrap_or_else(|error| error.into_inner());
1325 f(&mut journal)
1326 }
1327 #[cfg(test)]
1328 {
1329 let mut by_thread = TEST_RUNTIME_USAGE_JOURNAL
1330 .get_or_init(|| Mutex::new(std::collections::HashMap::new()))
1331 .lock()
1332 .unwrap_or_else(|error| error.into_inner());
1333 f(by_thread.entry(test_cost_scope_id()).or_default())
1334 }
1335 }
1336
1337 fn record_runtime_usage(
1338 owner: &str,
1339 source_id: &str,
1340 route: &EffectiveRouteEnvelope,
1341 usage: &Usage,
1342 ) {
1343 if usage == &Usage::default() {
1344 record_runtime_usage_drop(owner, source_id, route);
1345 return;
1346 }
1347 let owner = owner.trim();
1348 if owner.is_empty() {
1349 return;
1350 }
1351 let record = RuntimeUsageRecord {
1352 source_id: source_id.to_string(),
1353 usage: EffectiveRouteUsage {
1354 route: route.sanitized_for_persistence(),
1355 usage: usage.clone(),
1356 },
1357 };
1358 let sink =
1359 with_runtime_usage_sinks(|sinks| sinks.get(owner).map(|entry| Arc::clone(&entry.sink)));
1360 if sink.is_some_and(|sink| sink(record.clone())) {
1361 return;
1362 }
1363 with_runtime_usage_journal_mut(|journal| {
1364 let owner_journal = journal.entry(owner.to_string()).or_default();
1365 if owner_journal.records.iter().any(|old| {
1366 usage_source_fingerprint(&old.source_id) == usage_source_fingerprint(source_id)
1367 }) {
1368 return;
1369 }
1370 let fingerprint = usage_source_fingerprint(source_id);
1371 if let Some(index) = owner_journal
1372 .drop_records
1373 .iter()
1374 .position(|old| usage_source_fingerprint(&old.source_id) == fingerprint)
1375 {
1376 if owner_journal.drop_records[index].route != record.usage.route {
1377 return;
1378 }
1379 owner_journal.drop_records.remove(index);
1380 owner_journal.dropped_records = owner_journal.dropped_records.saturating_sub(1);
1381 owner_journal
1382 .dropped_source_fingerprints
1383 .remove(&fingerprint);
1384 }
1385 if owner_journal.records.len() == MAX_RUNTIME_USAGE_RECORDS_PER_OWNER {
1386 owner_journal.records.pop_front();
1387 owner_journal.dropped_records = owner_journal.dropped_records.saturating_add(1);
1388 }
1389 owner_journal.records.push_back(record);
1390 });
1391 }
1392
1393 fn record_runtime_usage_drop(owner: &str, source_id: &str, route: &EffectiveRouteEnvelope) {
1394 record_runtime_usage_missing(
1395 owner,
1396 source_id,
1397 route,
1398 RuntimeUsageMissingReason::SuccessWithoutUsage,
1399 );
1400 }
1401 fn record_runtime_usage_missing(
1402 owner: &str,
1403 source_id: &str,
1404 route: &EffectiveRouteEnvelope,
1405 reason: RuntimeUsageMissingReason,
1406 ) {
1407 let owner = owner.trim();
1408 if owner.is_empty() {
1409 return;
1410 }
1411 let fingerprint = usage_source_fingerprint(source_id);
1412 let sink = with_runtime_usage_sinks(|sinks| {
1413 sinks
1414 .get(owner)
1415 .and_then(|entry| entry.dropped_sink.as_ref().map(Arc::clone))
1416 });
1417 let record = RuntimeUsageDropRecord {
1418 reason,
1419 source_id: source_id.to_string(),
1420 route: route.sanitized_for_persistence(),
1421 };
1422 if sink.is_some_and(|sink| sink(record.clone())) {
1423 return;
1424 }
1425 with_runtime_usage_journal_mut(|journal| {
1426 let owner_journal = journal.entry(owner.to_string()).or_default();
1427 if owner_journal
1428 .records
1429 .iter()
1430 .any(|old| usage_source_fingerprint(&old.source_id) == fingerprint)
1431 {
1432 return;
1433 }
1434 if owner_journal
1435 .dropped_source_fingerprints
1436 .contains(&fingerprint)
1437 {
1438 return;
1439 }
1440 if owner_journal.dropped_source_fingerprints.len() < MAX_RUNTIME_USAGE_RECORDS_PER_OWNER {
1441 owner_journal
1442 .dropped_source_fingerprints
1443 .insert(fingerprint);
1444 owner_journal.drop_records.push_back(record);
1445 owner_journal.dropped_records = owner_journal.dropped_records.saturating_add(1);
1446 } else if !owner_journal.dropped_fingerprint_overflowed {
1447 // Preserve a bounded fail-closed overflow marker. Once the exact
1448 // identity ledger is full, further unknown ids share this one
1449 // marker so replays cannot grow the count without bound.
1450 owner_journal.dropped_fingerprint_overflowed = true;
1451 owner_journal.dropped_records = owner_journal.dropped_records.saturating_add(1);
1452 }
1453 });
1454 }
1455
1456 fn record_runtime_usage_drop_count(owner: &str, source_id: &str, count: u64) {
1457 let owner = owner.trim();
1458 if owner.is_empty() || count == 0 {
1459 return;
1460 }
1461 let fingerprint = usage_source_fingerprint(source_id);
1462 with_runtime_usage_journal_mut(|journal| {
1463 let owner_journal = journal.entry(owner.to_string()).or_default();
1464 if owner_journal
1465 .dropped_source_fingerprints
1466 .contains(&fingerprint)
1467 {
1468 return;
1469 }
1470 if owner_journal.dropped_source_fingerprints.len() < MAX_RUNTIME_USAGE_RECORDS_PER_OWNER {
1471 owner_journal
1472 .dropped_source_fingerprints
1473 .insert(fingerprint);
1474 owner_journal.dropped_records = owner_journal.dropped_records.saturating_add(count);
1475 } else if !owner_journal.dropped_fingerprint_overflowed {
1476 owner_journal.dropped_fingerprint_overflowed = true;
1477 owner_journal.dropped_records = owner_journal.dropped_records.saturating_add(1);
1478 }
1479 });
1480 }
1481
1482 /// Install a synchronous durability sink for one active runtime turn.
1483 /// Compaction calls invoke this before they return to the engine, so a process
1484 /// crash cannot erase already-reported usage from an in-memory journal.
1485 #[cfg(test)]
1486 pub(crate) fn register_runtime_usage_sink(owner: &str, sink: RuntimeUsageSink) {
1487 register_runtime_usage_sink_with_drop(owner, sink, None);
1488 }
1489
1490 pub(crate) fn register_runtime_usage_sink_with_drop(
1491 owner: &str,
1492 sink: RuntimeUsageSink,
1493 dropped_sink: Option<RuntimeUsageDropSink>,
1494 ) {
1495 let owner = owner.trim();
1496 if owner.is_empty() {
1497 return;
1498 }
1499 with_runtime_usage_sinks(|sinks| {
1500 sinks.insert(
1501 owner.to_string(),
1502 RuntimeUsageSinkEntry {
1503 sink,
1504 dropped_sink,
1505 decision_sink: None,
1506 leases: 0,
1507 terminal: false,
1508 },
1509 );
1510 });
1511 }
1512
1513 /// Extend the existing owner entry; this is not another sink registry.
1514 pub(crate) fn register_runtime_decision_sink(owner: &str, sink: RuntimeDecisionSink) {
1515 with_runtime_usage_sinks(|sinks| {
1516 if let Some(entry) = sinks.get_mut(owner.trim()) {
1517 entry.decision_sink = Some(sink);
1518 }
1519 });
1520 }
1521
1522 fn record_runtime_decision(owner: &str, record: &RuntimeDecisionReceipt) {
1523 if !record.is_bounded() {
1524 record_runtime_usage_drop(owner, &record.source_id, &record.route);
1525 return;
1526 }
1527 let record = record.sanitized();
1528 let sink = with_runtime_usage_sinks(|sinks| {
1529 sinks
1530 .get(owner)
1531 .and_then(|entry| entry.decision_sink.clone())
1532 });
1533 if sink.is_some_and(|sink| sink(record.clone())) {
1534 return;
1535 }
1536 with_runtime_usage_journal_mut(|journal| {
1537 let entry = journal.entry(owner.to_string()).or_default();
1538 if entry
1539 .decisions
1540 .iter()
1541 .any(|old| old.source_id == record.source_id)
1542 {
1543 return;
1544 }
1545 if entry.decisions.len() == MAX_RUNTIME_USAGE_RECORDS_PER_OWNER {
1546 entry.decisions.pop_front();
1547 entry.dropped_records = entry.dropped_records.saturating_add(1);
1548 }
1549 entry.decisions.push_back(record);
1550 });
1551 }
1552
1553 fn report_interactive_decision(scope: CostScopeToken, receipt: &RuntimeDecisionReceipt) {
1554 if !receipt.is_bounded() {
1555 return;
1556 }
1557 with_pending_state_mut(|state| {
1558 if state.generation == scope.0 {
1559 if state
1560 .pending
1561 .route_receipts
1562 .iter()
1563 .filter(|v| v.starts_with("decision:"))
1564 .count()
1565 < MAX_RUNTIME_USAGE_RECORDS_PER_OWNER
1566 {
1567 state
1568 .pending
1569 .route_receipts
1570 .insert(receipt.diagnostic_receipt());
1571 } else {
1572 state
1573 .pending
1574 .route_receipts
1575 .insert("decision:diagnostic_receipt_bound_reached".to_string());
1576 }
1577 }
1578 });
1579 }
1580
1581 /// Redacted durable identity shared by runtime-turn, worker, and interactive
1582 /// session accounting. Raw response ids never need to be persisted merely to
1583 /// make replay idempotent.
1584 #[must_use]
1585 pub(crate) fn usage_source_fingerprint(source_id: &str) -> String {
1586 let source_id = source_id.trim();
1587 let fingerprint = source_id.strip_prefix("routed:").unwrap_or(source_id);
1588 if fingerprint.len() == 64 && fingerprint.bytes().all(|byte| byte.is_ascii_hexdigit()) {
1589 return fingerprint.to_ascii_lowercase();
1590 }
1591 codewhale_config::catalog::base_url_fingerprint(source_id)
1592 }
1593
1594 /// Install the interactive session's synchronous runtime sink. A detached
1595 /// child may report after the parent mailbox has sealed; its owner lease keeps
1596 /// this sink alive, while the captured scope prevents a later session from
1597 /// inheriting the spend.
1598 #[cfg(test)]
1599 pub(crate) fn register_interactive_runtime_usage_sink(owner: &str, scope: CostScopeToken) {
1600 register_runtime_usage_sink_with_drop(
1601 owner,
1602 Arc::new(move |record| record_interactive_runtime_usage(scope, record)),
1603 Some(Arc::new(move |record| {
1604 record_interactive_runtime_usage_drop(scope, record)
1605 })),
1606 );
1607 }
1608
1609 /// Install an interactive sink whose stale-scope fallback is an origin-session
1610 /// sidecar. `/new` may close the foreground pool while a detached provider call
1611 /// is still running; the sidecar keeps that exact response with the old saved
1612 /// session instead of either dropping it or contaminating the new one.
1613 pub(crate) fn register_persistent_interactive_runtime_usage_sink(
1614 owner: &str,
1615 scope: CostScopeToken,
1616 session_id: &str,
1617 turn_id: &str,
1618 ) {
1619 let Ok(manager) = crate::session_manager::SessionManager::default_location() else {
1620 // With no durable origin gate, leave the owner on the bounded journal
1621 // fallback. An in-memory-only sink could keep accepting a deleted
1622 // session's responses after its directory becomes available again.
1623 return;
1624 };
1625 register_persistent_interactive_runtime_usage_sink_at(
1626 owner,
1627 scope,
1628 session_id,
1629 turn_id,
1630 manager.sessions_dir().to_path_buf(),
1631 );
1632 }
1633
1634 fn register_persistent_interactive_runtime_usage_sink_at(
1635 owner: &str,
1636 scope: CostScopeToken,
1637 session_id: &str,
1638 turn_id: &str,
1639 sessions_dir: std::path::PathBuf,
1640 ) {
1641 let usage_session_id = session_id.to_string();
1642 let usage_turn_id = turn_id.to_string();
1643 let drop_session_id = usage_session_id.clone();
1644 let drop_turn_id = usage_turn_id.clone();
1645 let usage_sessions_dir = sessions_dir.clone();
1646 let decision_sessions_dir = sessions_dir.clone();
1647 let decision_session_id = session_id.to_string();
1648 let decision_turn_id = turn_id.to_string();
1649 register_runtime_usage_sink_with_drop(
1650 owner,
1651 Arc::new(move |record| {
1652 crate::session_manager::SessionManager::new(usage_sessions_dir.clone())
1653 .map(|manager| {
1654 report_effective_route_for_interactive_origin_with_manager(
1655 scope,
1656 &usage_session_id,
1657 &usage_turn_id,
1658 &record.source_id,
1659 &record.usage.route,
1660 &record.usage.usage,
1661 &manager,
1662 )
1663 })
1664 .unwrap_or(false)
1665 }),
1666 Some(Arc::new(move |record| {
1667 crate::session_manager::SessionManager::new(sessions_dir.clone())
1668 .map(|manager| {
1669 report_missing_usage_for_interactive_origin_with_manager(
1670 scope,
1671 &drop_session_id,
1672 &drop_turn_id,
1673 &record.source_id,
1674 &record.route,
1675 record.reason,
1676 &manager,
1677 )
1678 })
1679 .unwrap_or(false)
1680 })),
1681 );
1682 register_runtime_decision_sink(
1683 owner,
1684 Arc::new(move |receipt| {
1685 let Ok(manager) =
1686 crate::session_manager::SessionManager::new(decision_sessions_dir.clone())
1687 else {
1688 return false;
1689 };
1690 let accepted = manager
1691 .persist_late_decision_receipt(&decision_session_id, &decision_turn_id, &receipt)
1692 .unwrap_or(false);
1693 if accepted {
1694 // The append may have been handled by a deletion tombstone. Only
1695 // a live origin may enter the foreground diagnostic projection.
1696 let _ = manager.with_live_session_origin(&decision_session_id, || {
1697 report_interactive_decision(scope, &receipt);
1698 true
1699 });
1700 }
1701 accepted
1702 }),
1703 );
1704 }
1705
1706 #[cfg(test)]
1707 pub(crate) fn register_persistent_interactive_runtime_usage_sink_for_test(
1708 owner: &str,
1709 scope: CostScopeToken,
1710 session_id: &str,
1711 turn_id: &str,
1712 manager: &crate::session_manager::SessionManager,
1713 ) {
1714 register_persistent_interactive_runtime_usage_sink_at(
1715 owner,
1716 scope,
1717 session_id,
1718 turn_id,
1719 manager.sessions_dir().to_path_buf(),
1720 );
1721 }
1722
1723 /// Acquire an owner lease for a root sub-agent runtime. Runtime clones inherit
1724 /// the lease, so top-level detached children can outlive the parent mailbox
1725 /// without losing their accounting path.
1726 pub(crate) fn acquire_runtime_usage_lease(owner: &str) -> Option<RuntimeUsageLease> {
1727 let owner = owner.trim();
1728 if owner.is_empty() {
1729 return None;
1730 }
1731 with_runtime_usage_sinks(|sinks| {
1732 let entry = sinks.get_mut(owner)?;
1733 entry.leases = entry.leases.saturating_add(1);
1734 Some(RuntimeUsageLease {
1735 owner: owner.to_string(),
1736 active: true,
1737 })
1738 })
1739 }
1740
1741 impl RuntimeUsageLease {
1742 #[must_use]
1743 pub(crate) fn owner(&self) -> &str {
1744 &self.owner
1745 }
1746 }
1747
1748 impl Clone for RuntimeUsageLease {
1749 fn clone(&self) -> Self {
1750 if self.active {
1751 let cloned = with_runtime_usage_sinks(|sinks| {
1752 sinks.get_mut(&self.owner).map(|entry| {
1753 entry.leases = entry.leases.saturating_add(1);
1754 })
1755 });
1756 if cloned.is_some() {
1757 return Self {
1758 owner: self.owner.clone(),
1759 active: true,
1760 };
1761 }
1762 }
1763 Self {
1764 owner: self.owner.clone(),
1765 active: false,
1766 }
1767 }
1768 }
1769
1770 impl Drop for RuntimeUsageLease {
1771 fn drop(&mut self) {
1772 if !self.active {
1773 return;
1774 }
1775 with_existing_runtime_usage_sinks(|sinks| {
1776 let should_remove = sinks.get_mut(&self.owner).is_some_and(|entry| {
1777 entry.leases = entry.leases.saturating_sub(1);
1778 entry.terminal && entry.leases == 0
1779 });
1780 if should_remove {
1781 sinks.remove(&self.owner);
1782 }
1783 });
1784 }
1785 }
1786
1787 /// Mark the parent turn terminal. An owner with detached children stays live
1788 /// until their cloned leases drop; owners without children retire now.
1789 pub(crate) fn finish_runtime_usage_owner(owner: &str) {
1790 with_existing_runtime_usage_sinks(|sinks| {
1791 let should_remove = sinks.get_mut(owner).is_some_and(|entry| {
1792 entry.terminal = true;
1793 entry.leases == 0
1794 });
1795 if should_remove {
1796 sinks.remove(owner);
1797 }
1798 });
1799 }
1800
1801 /// Take only the background usage assigned to one runtime turn.
1802 /// Other runtime turns and the TUI pending pool remain untouched.
1803 #[must_use]
1804 pub fn take_runtime_usage(owner: &str) -> RuntimeUsageBatch {
1805 with_runtime_usage_journal_mut(|journal| {
1806 journal
1807 .remove(owner)
1808 .map_or_else(RuntimeUsageBatch::default, |entry| RuntimeUsageBatch {
1809 decisions: entry.decisions.into_iter().collect(),
1810 records: entry.records.into_iter().collect(),
1811 drop_records: entry.drop_records.into_iter().collect(),
1812 dropped_records: entry.dropped_records,
1813 })
1814 })
1815 }
1816
1817 #[cfg(test)]
1818 thread_local! {
1819 static TEST_COST_ORIGIN: std::cell::Cell<Option<std::thread::ThreadId>> = const { std::cell::Cell::new(None) };
1820 }
1821
1822 #[cfg(test)]
1823 pub(crate) fn test_cost_scope_id() -> std::thread::ThreadId {
1824 TEST_COST_ORIGIN
1825 .with(|scope| scope.get())
1826 .unwrap_or_else(|| std::thread::current().id())
1827 }
1828
1829 #[cfg(test)]
1830 pub(crate) struct TestCostScopeBinding(Option<std::thread::ThreadId>);
1831
1832 #[cfg(test)]
1833 pub(crate) fn bind_test_cost_scope(origin: std::thread::ThreadId) -> TestCostScopeBinding {
1834 TestCostScopeBinding(TEST_COST_ORIGIN.with(|scope| scope.replace(Some(origin))))
1835 }
1836
1837 #[cfg(test)]
1838 impl Drop for TestCostScopeBinding {
1839 fn drop(&mut self) {
1840 TEST_COST_ORIGIN.with(|scope| scope.set(self.0));
1841 }
1842 }
1843
1844 /// Capture the current session/run generation before starting a background
1845 /// provider request. The same token must be supplied when its usage returns.
1846 #[must_use]
1847 pub fn scope_token() -> CostScopeToken {
1848 with_pending_state_mut(|state| CostScopeToken(state.generation))
1849 }
1850
1851 /// Atomically close the current cost scope and start a fresh generation.
1852 /// Reports from old in-flight requests are rejected after this returns, so
1853 /// `/new` and session load cannot inherit another session's spend.
1854 #[must_use]
1855 pub fn close_current_scope() -> PendingBackgroundCost {
1856 with_pending_state_mut(|state| {
1857 let pending = std::mem::take(&mut state.pending);
1858 state.generation = state.generation.wrapping_add(1);
1859 state.seen_usage_source_fingerprints.clear();
1860 state.missing_usage_sources.clear();
1861 state.missing_usage_overflowed = false;
1862 pending
1863 })
1864 }
1865
1866 /// Restore the durable response identities belonging to the newly loaded
1867 /// session. Callers close the previous scope before loading, so replacing the
1868 /// set cannot make another session's usage visible here.
1869 #[cfg(test)]
1870 pub(crate) fn restore_usage_source_fingerprints(fingerprints: impl IntoIterator<Item = String>) {
1871 with_pending_state_mut(|state| {
1872 state.seen_usage_source_fingerprints = fingerprints.into_iter().collect();
1873 })
1874 }
1875
1876 pub(crate) fn restore_usage_source_ledger(
1877 fingerprints: impl IntoIterator<Item = String>,
1878 missing: &BTreeMap<String, MissingUsageCoverage>,
1879 overflowed: bool,
1880 ) {
1881 with_pending_state_mut(|state| {
1882 state.missing_usage_sources = missing
1883 .iter()
1884 .take(MAX_MISSING_USAGE_SOURCES)
1885 .map(|(key, value)| (key.clone(), value.clone()))
1886 .collect();
1887 state.missing_usage_overflowed = overflowed || missing.len() > MAX_MISSING_USAGE_SOURCES;
1888 state.seen_usage_source_fingerprints = fingerprints
1889 .into_iter()
1890 .filter(|key| !state.missing_usage_sources.contains_key(key))
1891 .collect();
1892 });
1893 }
1894
1895 /// Mark a deleted origin's response handled in its original live generation.
1896 /// This suppresses legacy mailbox fallback without putting deleted usage or
1897 /// even its fingerprint into the pending pool or a durable session snapshot.
1898 fn acknowledge_retired_usage_source(scope: CostScopeToken, source_id: &str) {
1899 with_pending_state_mut(|state| {
1900 if state.generation == scope.0 {
1901 state
1902 .seen_usage_source_fingerprints
1903 .insert(usage_source_fingerprint(source_id));
1904 }
1905 });
1906 }
1907
1908 /// Whether this session generation already accepted or retired a response.
1909 /// Used by mailbox delivery to avoid pricing a response that the synchronous
1910 /// runtime sink already handled.
1911 #[must_use]
1912 pub(crate) fn usage_source_seen(source_id: &str) -> bool {
1913 let fingerprint = usage_source_fingerprint(source_id);
1914 with_pending_state_mut(|state| {
1915 state.seen_usage_source_fingerprints.contains(&fingerprint)
1916 || state.missing_usage_sources.contains_key(&fingerprint)
1917 })
1918 }
1919
1920 /// The non-secret identity of a background LLM call's route.
1921 ///
1922 /// Background helpers run off a bare client with no app `Config`, so they cannot
1923 /// resolve credential-derived billing. They *can* report what they actually know
1924 /// — which provider, which configured route, which wire model, which endpoint —
1925 /// and this type carries exactly that, so the pricing decision is made from
1926 /// evidence instead of from a provider name.
1927 #[derive(Debug, Clone, Copy)]
1928 #[cfg(test)]
1929 pub struct BackgroundRoute<'a> {
1930 /// Provider kind serving the call.
1931 pub provider: ProviderKind,
1932 /// Configured route identity (the `[providers.<name>]` key), when the
1933 /// caller has one. This is a user-chosen label, not a credential.
1934 pub provider_identity: Option<&'a str>,
1935 /// Wire model id as sent on the request.
1936 pub wire_model: &'a str,
1937 /// Concrete base URL the request went to, when the client exposes one.
1938 ///
1939 /// Only ever used to derive a billing-surface classification and a
1940 /// SHA-256 fingerprint; the URL itself never leaves this struct.
1941 pub base_url: Option<&'a str>,
1942 }
1943
1944 #[cfg(test)]
1945 impl<'a> BackgroundRoute<'a> {
1946 /// A route with no endpoint information.
1947 #[must_use]
1948 pub fn new(provider: ProviderKind, wire_model: &'a str) -> Self {
1949 Self {
1950 provider,
1951 provider_identity: None,
1952 wire_model,
1953 base_url: None,
1954 }
1955 }
1956
1957 #[must_use]
1958 pub fn with_base_url(mut self, base_url: Option<&'a str>) -> Self {
1959 self.base_url = base_url;
1960 self
1961 }
1962
1963 /// Non-secret billing-surface classification for this endpoint.
1964 #[must_use]
1965 pub fn billing_surface(&self) -> Option<&'static str> {
1966 crate::pricing::billing_surface_for_route(self.provider, self.base_url)
1967 }
1968
1969 /// SHA-256 fingerprint of the normalized base URL, or `None` when unknown.
1970 ///
1971 /// This is the same digest the catalog scopes live rows on, so a live
1972 /// pricing row can be proven to price *this* endpoint.
1973 #[must_use]
1974 pub fn endpoint_fingerprint(&self) -> Option<String> {
1975 self.base_url.and_then(endpoint_fingerprint)
1976 }
1977
1978 /// Billing presentation derivable without app config.
1979 #[must_use]
1980 pub fn billing(&self) -> BillingPresentation {
1981 crate::route_billing::for_endpoint_without_config(self.provider, self.base_url)
1982 }
1983
1984 /// A redacted, stable receipt describing this route.
1985 #[must_use]
1986 pub fn receipt(&self, currency: &str) -> String {
1987 route_receipt(
1988 self.provider,
1989 self.provider_identity,
1990 self.wire_model,
1991 self.billing_surface(),
1992 self.endpoint_fingerprint().as_deref(),
1993 self.billing().into(),
1994 currency,
1995 )
1996 }
1997 }
1998
1999 /// Format one redacted route receipt.
2000 ///
2001 /// Contains only: provider kind, configured route label, wire model,
2002 /// billing-surface classification, endpoint fingerprint, billing mode, and the currency the
2003 /// estimate is denominated in. It deliberately contains no URL, no credential,
2004 /// and no filesystem path, so it is safe to persist into a saved session and to
2005 /// log. This is the single formatter, so the foreground turn path and the
2006 /// background pool cannot describe the same route two different ways.
2007 #[must_use]
2008 pub fn route_receipt(
2009 provider: ProviderKind,
2010 provider_identity: Option<&str>,
2011 wire_model: &str,
2012 billing_surface: Option<&str>,
2013 endpoint_fingerprint: Option<&str>,
2014 billing_mode: RouteBillingMode,
2015 currency: &str,
2016 ) -> String {
2017 format!(
2018 "provider={} identity={} model={} surface={} endpoint_fp={} billing_mode={} currency={currency}",
2019 provider.as_str(),
2020 safe_receipt_field(provider_identity.unwrap_or("-")),
2021 safe_receipt_field(wire_model),
2022 safe_receipt_field(billing_surface.unwrap_or("unreported")),
2023 safe_receipt_field(endpoint_fingerprint.unwrap_or("unreported")),
2024 match billing_mode {
2025 RouteBillingMode::Metered => "metered",
2026 RouteBillingMode::Subscription => "subscription",
2027 RouteBillingMode::Local => "local",
2028 RouteBillingMode::Unknown => "unknown",
2029 },
2030 )
2031 }
2032
2033 const MAX_RECEIPT_FIELD_CHARS: usize = 96;
2034
2035 fn safe_receipt_field(raw: &str) -> String {
2036 let sanitized = sanitize_persisted_route_label(raw);
2037 let mut out = String::with_capacity(raw.len().min(MAX_RECEIPT_FIELD_CHARS));
2038 let mut previous_separator = false;
2039 for ch in sanitized.chars() {
2040 if out.chars().count() >= MAX_RECEIPT_FIELD_CHARS {
2041 break;
2042 }
2043 let safe = if ch.is_alphanumeric() || matches!(ch, '.' | '_' | '-' | '/' | ':' | '+') {
2044 ch
2045 } else {
2046 '_'
2047 };
2048 let separator = safe == '_';
2049 if separator && previous_separator {
2050 continue;
2051 }
2052 out.push(safe);
2053 previous_separator = separator;
2054 }
2055 if out.is_empty() { "-".to_string() } else { out }
2056 }
2057
2058 pub(crate) fn sanitize_persisted_route_label(raw: &str) -> String {
2059 const MAX_PERSISTED_ROUTE_LABEL_CHARS: usize = 256;
2060 let value = raw.trim();
2061 let lower = value.to_ascii_lowercase();
2062
2063 if value.is_empty() {
2064 return "-".to_string();
2065 }
2066
2067 // URLs are not route labels. Endpoints have a dedicated, validated hash
2068 // field; persisting a URL here risks leaking userinfo, query credentials,
2069 // or fragments through a custom provider/model name.
2070 if value.contains("://") {
2071 return "redacted-url".to_string();
2072 }
2073
2074 let authorization_value = ["bearer ", "basic ", "digest ", "token ", "apikey "]
2075 .iter()
2076 .any(|scheme| lower.starts_with(scheme))
2077 || lower.contains("authorization:")
2078 || lower.contains("proxy-authorization:");
2079 if authorization_value {
2080 return "redacted-credential".to_string();
2081 }
2082
2083 // Reject credential assignments regardless of common casing or separator:
2084 // FOO_API_KEY=..., access-token:..., password = ....
2085 for (index, ch) in value.char_indices() {
2086 if !matches!(ch, '=' | ':') {
2087 continue;
2088 }
2089 let name = lower[..index]
2090 .trim()
2091 .trim_matches(|ch: char| matches!(ch, '"' | '\'' | '{' | '[' | ','));
2092 let name = name.rsplit([' ', ',', ';']).next().unwrap_or(name);
2093 let normalized = name.replace('-', "_");
2094 if normalized.ends_with("api_key")
2095 || normalized.ends_with("token")
2096 || normalized.ends_with("secret")
2097 || normalized.ends_with("password")
2098 || normalized.ends_with("passwd")
2099 {
2100 return "redacted-credential".to_string();
2101 }
2102 }
2103
2104 // Common credential token prefixes. These are intentionally checked at
2105 // word boundaries so model ids containing an incidental "sk" survive.
2106 let credential_prefix = lower
2107 .split(|ch: char| ch.is_whitespace() || matches!(ch, '=' | ':' | ',' | ';' | '"' | '\''))
2108 .filter(|part| !part.is_empty())
2109 .any(|part| {
2110 [
2111 "sk-",
2112 "sk_",
2113 "rk-",
2114 "pk-",
2115 "ghp_",
2116 "gho_",
2117 "ghu_",
2118 "ghs_",
2119 "github_pat_",
2120 "hf_",
2121 "glpat-",
2122 "xoxb-",
2123 "xoxp-",
2124 "xoxa-",
2125 "akia",
2126 "aiza",
2127 "eyj",
2128 ]
2129 .iter()
2130 .any(|prefix| part.starts_with(prefix))
2131 });
2132 if credential_prefix {
2133 return "redacted-credential".to_string();
2134 }
2135
2136 let windows_absolute = value.as_bytes().get(1) == Some(&b':')
2137 && value
2138 .as_bytes()
2139 .get(2)
2140 .is_some_and(|separator| matches!(separator, b'/' | b'\\'));
2141 let contains_local_root = [
2142 "/users/",
2143 "/volumes/",
2144 "/home/",
2145 "/private/",
2146 "\\users\\",
2147 "file://",
2148 "/.ssh/",
2149 "\\.ssh\\",
2150 ]
2151 .iter()
2152 .any(|needle| lower.contains(needle));
2153 let looks_like_relative_path = value.contains('\\')
2154 || lower.starts_with(".ssh/")
2155 || lower.starts_with(".ssh\\")
2156 || lower.split('/').any(|segment| {
2157 matches!(
2158 segment,
2159 "." | ".."
2160 | ".ssh"
2161 | ".config"
2162 | "secrets"
2163 | "secret"
2164 | "credentials"
2165 | "credential"
2166 | "relative"
2167 | "workspace"
2168 | "tmp"
2169 )
2170 });
2171 if std::path::Path::new(value).is_absolute()
2172 || windows_absolute
2173 || value.starts_with("~/")
2174 || value.starts_with("./")
2175 || value.starts_with("../")
2176 || contains_local_root
2177 || looks_like_relative_path
2178 {
2179 return "redacted-local-path".to_string();
2180 }
2181 let bounded: String = value
2182 .chars()
2183 .filter(|ch| !ch.is_control())
2184 .take(MAX_PERSISTED_ROUTE_LABEL_CHARS)
2185 .collect();
2186 if bounded.is_empty() {
2187 "-".to_string()
2188 } else {
2189 bounded
2190 }
2191 }
2192
2193 /// Validate and canonicalize an endpoint before producing the cryptographic
2194 /// fingerprint persisted in a receipt. Secret-bearing/malformed URLs receive
2195 /// no fingerprint at all; userinfo, query strings, and fragments are never fed
2196 /// to the hash function.
2197 #[must_use]
2198 pub fn endpoint_fingerprint(base_url: &str) -> Option<String> {
2199 let mut parsed = reqwest::Url::parse(base_url.trim()).ok()?;
2200 if !matches!(parsed.scheme(), "http" | "https")
2201 || !parsed.username().is_empty()
2202 || parsed.password().is_some()
2203 || parsed.query().is_some()
2204 || parsed.fragment().is_some()
2205 || parsed.host_str().is_none()
2206 {
2207 return None;
2208 }
2209 parsed.set_query(None);
2210 parsed.set_fragment(None);
2211 let canonical = parsed.as_str().trim_end_matches('/');
2212 Some(codewhale_config::catalog::base_url_fingerprint(canonical))
2213 }
2214
2215 /// Currency tag for a receipt, derived from authoritative currency coverage —
2216 /// not from a positive amount, because a zero-usage priced turn is still a
2217 /// valid zero in its published currency.
2218 #[must_use]
2219 pub fn currency_tag(audit: &TurnCostAudit) -> &'static str {
2220 match (audit.usd_priced, audit.cny_priced) {
2221 (true, true) => "usd+cny",
2222 (true, false) => "usd",
2223 (false, true) => "cny",
2224 (false, false) => "unpriced",
2225 }
2226 }
2227
2228 /// Background callers report their LLM usage here.
2229 ///
2230 /// The route is priced through the same [`crate::pricing::audit_turn_cost_for_route_on_endpoint`]
2231 /// the foreground turn path uses, so a background turn cannot be counted under
2232 /// different rules than a parent turn. Adds no money when the route is exactly
2233 /// non-metered (a local runtime, an OAuth broker, a named plan endpoint), and
2234 /// counts the turn as *missing spend* whenever it is money-metered or of unknown
2235 /// basis but could not be priced — an unknown basis is never waved through as a
2236 /// subscription (#4318).
2237 #[cfg(test)]
2238 pub fn report(scope: CostScopeToken, route: &BackgroundRoute<'_>, usage: &Usage) {
2239 let billing_surface = route.billing_surface();
2240 let fingerprint = route.endpoint_fingerprint();
2241 let audit = crate::pricing::audit_turn_cost_for_route_on_endpoint(
2242 route.provider,
2243 route.wire_model,
2244 billing_surface,
2245 fingerprint.as_deref(),
2246 usage,
2247 chrono::Utc::now(),
2248 route.billing(),
2249 );
2250 record(scope, route.receipt(currency_tag(&audit)), &audit, usage);
2251 }
2252
2253 /// Report background usage to exactly one accounting owner.
2254 ///
2255 /// Runtime-owned calls go only to the durable runtime sink. Calls without a
2256 /// runtime owner belong to the interactive TUI pool. Mixing both paths would
2257 /// count one provider response twice in hosts that expose both projections.
2258 pub fn report_effective_route_for_runtime(
2259 scope: CostScopeToken,
2260 runtime_owner: Option<&str>,
2261 source_id: &str,
2262 route: &EffectiveRouteEnvelope,
2263 usage: &Usage,
2264 ) {
2265 if let Some(owner) = runtime_owner {
2266 record_runtime_usage(owner, source_id, route, usage);
2267 } else {
2268 record_interactive_runtime_usage(
2269 scope,
2270 RuntimeUsageRecord {
2271 source_id: source_id.to_string(),
2272 usage: EffectiveRouteUsage {
2273 route: route.sanitized_for_persistence(),
2274 usage: usage.clone(),
2275 },
2276 },
2277 );
2278 }
2279 }
2280
2281 /// Report an interactive auxiliary response against its immutable origin.
2282 /// A stale foreground scope is not an error: it means `/new` or session load
2283 /// already moved on, so the exact receipt is appended to the old session's
2284 /// durable sidecar instead of being redirected to the active session.
2285 pub(crate) fn report_effective_route_for_interactive_origin(
2286 scope: CostScopeToken,
2287 session_id: &str,
2288 turn_id: &str,
2289 source_id: &str,
2290 route: &EffectiveRouteEnvelope,
2291 usage: &Usage,
2292 ) {
2293 let persisted =
2294 crate::session_manager::SessionManager::default_location().is_ok_and(|manager| {
2295 report_effective_route_for_interactive_origin_with_manager(
2296 scope, session_id, turn_id, source_id, route, usage, &manager,
2297 )
2298 });
2299 if !persisted {
2300 tracing::warn!("late interactive usage could not be persisted for its origin session");
2301 }
2302 }
2303
2304 fn report_effective_route_for_interactive_origin_with_manager(
2305 scope: CostScopeToken,
2306 session_id: &str,
2307 turn_id: &str,
2308 source_id: &str,
2309 route: &EffectiveRouteEnvelope,
2310 usage: &Usage,
2311 manager: &crate::session_manager::SessionManager,
2312 ) -> bool {
2313 let record = RuntimeUsageRecord {
2314 source_id: source_id.to_string(),
2315 usage: EffectiveRouteUsage {
2316 route: route.sanitized_for_persistence(),
2317 usage: usage.clone(),
2318 },
2319 };
2320 match manager.with_live_session_origin(session_id, || {
2321 record_interactive_runtime_usage(scope, record.clone())
2322 }) {
2323 Ok(None) => {
2324 acknowledge_retired_usage_source(scope, source_id);
2325 return true;
2326 }
2327 Ok(Some(true)) => return true,
2328 Ok(Some(false)) => {}
2329 Err(_) => return false,
2330 }
2331 manager
2332 .persist_late_runtime_usage(session_id, turn_id, &record)
2333 .unwrap_or(false)
2334 }
2335
2336 pub(crate) fn report_unreceipted_for_interactive_origin(
2337 scope: CostScopeToken,
2338 session_id: &str,
2339 turn_id: &str,
2340 source_id: &str,
2341 route: &EffectiveRouteEnvelope,
2342 ) {
2343 report_missing_usage_for_interactive_origin(
2344 scope,
2345 session_id,
2346 turn_id,
2347 source_id,
2348 route,
2349 RuntimeUsageMissingReason::SuccessWithoutUsage,
2350 );
2351 }
2352 pub(crate) fn report_missing_usage_for_interactive_origin(
2353 scope: CostScopeToken,
2354 session_id: &str,
2355 turn_id: &str,
2356 source_id: &str,
2357 route: &EffectiveRouteEnvelope,
2358 reason: RuntimeUsageMissingReason,
2359 ) {
2360 let persisted =
2361 crate::session_manager::SessionManager::default_location().is_ok_and(|manager| {
2362 report_missing_usage_for_interactive_origin_with_manager(
2363 scope, session_id, turn_id, source_id, route, reason, &manager,
2364 )
2365 });
2366 if !persisted {
2367 tracing::warn!(
2368 "late interactive missing-usage receipt could not be persisted for its origin session"
2369 );
2370 }
2371 }
2372
2373 #[cfg(test)]
2374 fn report_unreceipted_for_interactive_origin_with_manager(
2375 scope: CostScopeToken,
2376 session_id: &str,
2377 turn_id: &str,
2378 source_id: &str,
2379 route: &EffectiveRouteEnvelope,
2380 manager: &crate::session_manager::SessionManager,
2381 ) -> bool {
2382 report_missing_usage_for_interactive_origin_with_manager(
2383 scope,
2384 session_id,
2385 turn_id,
2386 source_id,
2387 route,
2388 RuntimeUsageMissingReason::SuccessWithoutUsage,
2389 manager,
2390 )
2391 }
2392 fn report_missing_usage_for_interactive_origin_with_manager(
2393 scope: CostScopeToken,
2394 session_id: &str,
2395 turn_id: &str,
2396 source_id: &str,
2397 route: &EffectiveRouteEnvelope,
2398 reason: RuntimeUsageMissingReason,
2399 manager: &crate::session_manager::SessionManager,
2400 ) -> bool {
2401 let record = RuntimeUsageDropRecord {
2402 reason,
2403 source_id: source_id.to_string(),
2404 route: route.sanitized_for_persistence(),
2405 };
2406 match manager.with_live_session_origin(session_id, || {
2407 record_interactive_runtime_usage_drop(scope, record.clone())
2408 }) {
2409 Ok(None) => {
2410 acknowledge_retired_usage_source(scope, source_id);
2411 return true;
2412 }
2413 Ok(Some(true)) => return true,
2414 Ok(Some(false)) => {}
2415 Err(_) => return false,
2416 }
2417 manager
2418 .persist_late_runtime_drop(session_id, turn_id, &record)
2419 .unwrap_or(false)
2420 }
2421
2422 /// Record one provider-success response whose usage payload was absent.
2423 ///
2424 /// Callers must supply the same fixed-length, non-secret source identity they
2425 /// would use for a normal routed usage receipt. Runtime owners persist one
2426 /// bounded dropped-coverage marker; ownerless/interactive calls add one
2427 /// unpriced coverage turn to the captured session scope. Replays are
2428 /// idempotent, and a stale scope cannot contaminate a later session.
2429 pub(crate) fn report_unreceipted_provider_success(
2430 scope: CostScopeToken,
2431 runtime_owner: Option<&str>,
2432 source_id: &str,
2433 route: &EffectiveRouteEnvelope,
2434 ) {
2435 report_missing_runtime_usage(
2436 scope,
2437 runtime_owner,
2438 source_id,
2439 route,
2440 RuntimeUsageMissingReason::SuccessWithoutUsage,
2441 );
2442 }
2443 pub(crate) fn report_missing_runtime_usage(
2444 scope: CostScopeToken,
2445 runtime_owner: Option<&str>,
2446 source_id: &str,
2447 route: &EffectiveRouteEnvelope,
2448 reason: RuntimeUsageMissingReason,
2449 ) {
2450 if let Some(owner) = runtime_owner {
2451 record_runtime_usage_missing(owner, source_id, route, reason);
2452 } else {
2453 record_interactive_runtime_usage_drop(
2454 scope,
2455 RuntimeUsageDropRecord {
2456 reason,
2457 source_id: source_id.to_string(),
2458 route: route.sanitized_for_persistence(),
2459 },
2460 );
2461 }
2462 }
2463
2464 /// Settle one bounded routed-usage batch without repricing or losing exact
2465 /// missing-usage route evidence. Replaying the same batch is idempotent by the
2466 /// stable per-response source ids. Any residual count whose exact record was
2467 /// truncated remains an explicit fail-closed coverage gap.
2468 pub(crate) fn report_runtime_usage_batch(
2469 scope: CostScopeToken,
2470 runtime_owner: Option<&str>,
2471 batch: &RuntimeUsageBatch,
2472 ) {
2473 for receipt in &batch.decisions {
2474 if let Some(owner) = runtime_owner {
2475 record_runtime_decision(owner, receipt);
2476 } else {
2477 report_interactive_decision(scope, receipt);
2478 }
2479 }
2480 for record in &batch.records {
2481 report_effective_route_for_runtime(
2482 scope,
2483 runtime_owner,
2484 &record.source_id,
2485 &record.usage.route,
2486 &record.usage.usage,
2487 );
2488 }
2489 for record in &batch.drop_records {
2490 report_missing_runtime_usage(
2491 scope,
2492 runtime_owner,
2493 &record.source_id,
2494 &record.route,
2495 record.reason,
2496 );
2497 }
2498
2499 let residual = batch
2500 .dropped_records
2501 .saturating_sub(u64::try_from(batch.drop_records.len()).unwrap_or(u64::MAX));
2502 if residual == 0 {
2503 return;
2504 }
2505 let mut identities = batch
2506 .records
2507 .iter()
2508 .map(|record| usage_source_fingerprint(&record.source_id))
2509 .chain(
2510 batch
2511 .drop_records
2512 .iter()
2513 .map(|record| usage_source_fingerprint(&record.source_id)),
2514 )
2515 .take(MAX_RUNTIME_USAGE_RECORDS_PER_OWNER)
2516 .collect::<Vec<_>>();
2517 identities.sort_unstable();
2518 let residual_source = format!(
2519 "runtime-usage-batch-residual:{}",
2520 usage_source_fingerprint(&format!(
2521 "{}:{}:{}:{}",
2522 batch.records.len(),
2523 batch.drop_records.len(),
2524 batch.dropped_records,
2525 identities.join(":")
2526 ))
2527 );
2528 if let Some(owner) = runtime_owner {
2529 record_runtime_usage_drop_count(owner, &residual_source, residual);
2530 } else {
2531 record_interactive_runtime_usage_drop_count(scope, &residual_source, residual);
2532 }
2533 }
2534
2535 #[must_use]
2536 pub(crate) fn background_cost_for_runtime_usage(
2537 record: &RuntimeUsageRecord,
2538 ) -> PendingBackgroundCost {
2539 if record.usage.usage == Usage::default() {
2540 return background_cost_for_runtime_drop(&RuntimeUsageDropRecord {
2541 reason: crate::cost_status::RuntimeUsageMissingReason::default(),
2542 source_id: record.source_id.clone(),
2543 route: record.usage.route.clone(),
2544 });
2545 }
2546 let mut pending = PendingBackgroundCost::default();
2547 let fingerprint = usage_source_fingerprint(&record.source_id);
2548 let audit = record.usage.route.audit(&record.usage.usage);
2549 let receipt = record.usage.route.receipt(&audit);
2550 pending
2551 .usage_source_fingerprints
2552 .insert(fingerprint.clone());
2553 pending.resolved_missing_usage_sources.insert(fingerprint);
2554 fold_audit_into_pending(&mut pending, receipt, &audit, &record.usage.usage);
2555 pending
2556 }
2557
2558 #[must_use]
2559 pub(crate) fn background_cost_for_runtime_drop(
2560 record: &RuntimeUsageDropRecord,
2561 ) -> PendingBackgroundCost {
2562 let mut pending = PendingBackgroundCost::default();
2563 let fingerprint = usage_source_fingerprint(&record.source_id);
2564 pending
2565 .usage_source_fingerprints
2566 .insert(fingerprint.clone());
2567 pending
2568 .missing_usage_sources
2569 .insert(fingerprint, record.coverage());
2570 if record.coverage().money_metered {
2571 pending.unpriced_turns = 1;
2572 pending.cny_unpriced_turns = 1;
2573 pending.unpriced_reasons.insert(record.reason.label());
2574 pending.cny_unpriced_reasons.insert(record.reason.label());
2575 }
2576 pending
2577 }
2578 /// Reconcile exact missing slots in a projection before adding real usage.
2579 /// Overflow and legacy unattributed gaps are never erased by this operation.
2580 pub(crate) fn reconcile_missing_usage_sources(
2581 missing: &mut BTreeMap<String, MissingUsageCoverage>,
2582 resolved: &BTreeSet<String>,
2583 unpriced: &mut u32,
2584 cny_unpriced: &mut u32,
2585 ) {
2586 for fingerprint in resolved {
2587 if missing
2588 .remove(fingerprint)
2589 .is_some_and(|coverage| coverage.money_metered)
2590 {
2591 *unpriced = unpriced.saturating_sub(1);
2592 *cny_unpriced = cny_unpriced.saturating_sub(1);
2593 }
2594 }
2595 }
2596
2597 /// Apply bounded unresolved metadata to an existing cost projection. The
2598 /// returned batch omits exact identities whose missing slots overflowed; a
2599 /// real receipt for such an id can still settle once, but cannot erase the gap.
2600 pub(crate) fn project_missing_usage_ledger(
2601 missing: &mut BTreeMap<String, MissingUsageCoverage>,
2602 overflowed: &mut bool,
2603 unpriced: &mut u32,
2604 cny_unpriced: &mut u32,
2605 pool: &PendingBackgroundCost,
2606 ) -> PendingBackgroundCost {
2607 reconcile_missing_usage_sources(
2608 missing,
2609 &pool.resolved_missing_usage_sources,
2610 unpriced,
2611 cny_unpriced,
2612 );
2613 let mut pool = pool.clone();
2614 for (fingerprint, coverage) in &pool.missing_usage_sources {
2615 if missing.contains_key(fingerprint) {
2616 continue;
2617 }
2618 if missing.len() < MAX_MISSING_USAGE_SOURCES {
2619 missing.insert(fingerprint.clone(), coverage.clone());
2620 } else {
2621 pool.usage_source_fingerprints.remove(fingerprint);
2622 if coverage.money_metered {
2623 pool.unpriced_turns = pool.unpriced_turns.saturating_sub(1);
2624 pool.cny_unpriced_turns = pool.cny_unpriced_turns.saturating_sub(1);
2625 }
2626 if !*overflowed && !pool.missing_usage_overflowed {
2627 pool.missing_usage_overflowed = true;
2628 pool.unpriced_turns = pool.unpriced_turns.saturating_add(1);
2629 pool.cny_unpriced_turns = pool.cny_unpriced_turns.saturating_add(1);
2630 pool.unpriced_reasons
2631 .insert("missing_usage_source_overflow");
2632 pool.cny_unpriced_reasons
2633 .insert("missing_usage_source_overflow");
2634 }
2635 }
2636 }
2637 *overflowed |= pool.missing_usage_overflowed;
2638 pool
2639 }
2640
2641 /// Fold one already-computed audit into the pending pool.
2642 #[cfg(test)]
2643 fn record(scope: CostScopeToken, route_receipt: String, audit: &TurnCostAudit, usage: &Usage) {
2644 with_pending_state_mut(|state| {
2645 if state.generation != scope.0 {
2646 return;
2647 }
2648 fold_audit_into_pending(&mut state.pending, route_receipt, audit, usage);
2649 });
2650 }
2651
2652 fn record_interactive_runtime_usage(scope: CostScopeToken, record: RuntimeUsageRecord) -> bool {
2653 if record.usage.usage == Usage::default() {
2654 return record_interactive_runtime_usage_drop(
2655 scope,
2656 RuntimeUsageDropRecord {
2657 reason: RuntimeUsageMissingReason::SuccessWithoutUsage,
2658 source_id: record.source_id,
2659 route: record.usage.route,
2660 },
2661 );
2662 }
2663 with_pending_state_mut(|state| {
2664 if state.generation != scope.0 {
2665 return false;
2666 }
2667 let fingerprint = usage_source_fingerprint(&record.source_id);
2668 if state
2669 .missing_usage_sources
2670 .get(&fingerprint)
2671 .is_some_and(|coverage| !coverage.matches_route(&record.usage.route))
2672 {
2673 return true;
2674 }
2675 if !state
2676 .seen_usage_source_fingerprints
2677 .insert(fingerprint.clone())
2678 {
2679 return true;
2680 }
2681 if let Some(coverage) = state.missing_usage_sources.remove(&fingerprint) {
2682 if state
2683 .pending
2684 .missing_usage_sources
2685 .remove(&fingerprint)
2686 .is_some()
2687 {
2688 if coverage.money_metered {
2689 state.pending.unpriced_turns = state.pending.unpriced_turns.saturating_sub(1);
2690 state.pending.cny_unpriced_turns =
2691 state.pending.cny_unpriced_turns.saturating_sub(1);
2692 }
2693 } else {
2694 state
2695 .pending
2696 .resolved_missing_usage_sources
2697 .insert(fingerprint.clone());
2698 }
2699 }
2700 let audit = record.usage.route.audit(&record.usage.usage);
2701 let receipt = record.usage.route.receipt(&audit);
2702 state.pending.usage_source_fingerprints.insert(fingerprint);
2703 fold_audit_into_pending(&mut state.pending, receipt, &audit, &record.usage.usage);
2704 true
2705 })
2706 }
2707 fn record_interactive_runtime_usage_drop(
2708 scope: CostScopeToken,
2709 record: RuntimeUsageDropRecord,
2710 ) -> bool {
2711 with_pending_state_mut(|state| {
2712 if state.generation != scope.0 {
2713 return false;
2714 }
2715 let fingerprint = usage_source_fingerprint(&record.source_id);
2716 if state.seen_usage_source_fingerprints.contains(&fingerprint)
2717 || state.missing_usage_sources.contains_key(&fingerprint)
2718 {
2719 return true;
2720 }
2721 if state.missing_usage_sources.len() == MAX_MISSING_USAGE_SOURCES {
2722 if !state.missing_usage_overflowed {
2723 state.missing_usage_overflowed = true;
2724 state.pending.missing_usage_overflowed = true;
2725 state.pending.unpriced_turns = state.pending.unpriced_turns.saturating_add(1);
2726 state.pending.cny_unpriced_turns =
2727 state.pending.cny_unpriced_turns.saturating_add(1);
2728 state
2729 .pending
2730 .unpriced_reasons
2731 .insert("missing_usage_source_overflow");
2732 state
2733 .pending
2734 .cny_unpriced_reasons
2735 .insert("missing_usage_source_overflow");
2736 }
2737 return true;
2738 }
2739 let coverage = record.coverage();
2740 state
2741 .missing_usage_sources
2742 .insert(fingerprint.clone(), coverage.clone());
2743 state
2744 .pending
2745 .missing_usage_sources
2746 .insert(fingerprint.clone(), coverage.clone());
2747 state.pending.usage_source_fingerprints.insert(fingerprint);
2748 if coverage.money_metered {
2749 state.pending.unpriced_turns = state.pending.unpriced_turns.saturating_add(1);
2750 state.pending.cny_unpriced_turns = state.pending.cny_unpriced_turns.saturating_add(1);
2751 state.pending.unpriced_reasons.insert(record.reason.label());
2752 state
2753 .pending
2754 .cny_unpriced_reasons
2755 .insert(record.reason.label());
2756 }
2757 true
2758 })
2759 }
2760
2761 fn record_interactive_runtime_usage_drop_count(
2762 scope: CostScopeToken,
2763 source_id: &str,
2764 count: u64,
2765 ) -> bool {
2766 with_pending_state_mut(|state| {
2767 if state.generation != scope.0 {
2768 return false;
2769 }
2770 let fingerprint = usage_source_fingerprint(source_id);
2771 if !state
2772 .seen_usage_source_fingerprints
2773 .insert(fingerprint.clone())
2774 {
2775 return true;
2776 }
2777 state.pending.usage_source_fingerprints.insert(fingerprint);
2778 let count = u32::try_from(count).unwrap_or(u32::MAX);
2779 state.pending.unpriced_turns = state.pending.unpriced_turns.saturating_add(count);
2780 state.pending.cny_unpriced_turns = state.pending.cny_unpriced_turns.saturating_add(count);
2781 state
2782 .pending
2783 .unpriced_reasons
2784 .insert("routed_usage_receipt_missing");
2785 state
2786 .pending
2787 .cny_unpriced_reasons
2788 .insert("routed_usage_receipt_missing");
2789 true
2790 })
2791 }
2792
2793 fn fold_audit_into_pending(
2794 pending: &mut PendingBackgroundCost,
2795 route_receipt: String,
2796 audit: &TurnCostAudit,
2797 usage: &Usage,
2798 ) {
2799 if let Some(provenance) = audit.provenance.as_ref() {
2800 pending.pricing_provenances.insert(provenance.label());
2801 }
2802 if let Some(defect) = audit.live_pricing_defect.as_ref() {
2803 if audit.estimate.is_some() {
2804 pending.live_pricing_defects.insert(defect.label());
2805 } else {
2806 pending.live_pricing_unusable_defects.insert(defect.label());
2807 }
2808 }
2809 if let Some(cost) = audit.estimate {
2810 pending.estimate = pending.estimate.saturating_add(cost);
2811 }
2812 if usage.prompt_cache_hit_tokens.is_some()
2813 || usage.prompt_cache_miss_tokens.is_some()
2814 || usage.prompt_cache_write_tokens.is_some()
2815 {
2816 let classes = crate::pricing::token_usage_for_pricing(usage);
2817 let add = |slot: &mut Option<u64>, tokens: u64| {
2818 *slot = Some(slot.unwrap_or(0).saturating_add(tokens));
2819 };
2820 add(&mut pending.cache_hit_tokens, classes.cache_read);
2821 add(&mut pending.cache_miss_tokens, classes.input);
2822 add(&mut pending.cache_write_tokens, classes.cache_write);
2823 }
2824
2825 // Only money-metered/unknown-basis turns belong in missing-money coverage
2826 // or its reason list. A subscription/local receipt is still audited below,
2827 // but `not_money_metered` must never be presented as a gap in a subtotal.
2828 if audit.counts_toward_money_coverage() {
2829 if audit.usd_priced {
2830 pending.priced_turns = pending.priced_turns.saturating_add(1);
2831 } else {
2832 pending.unpriced_turns = pending.unpriced_turns.saturating_add(1);
2833 }
2834 if audit.cny_priced {
2835 pending.cny_priced_turns = pending.cny_priced_turns.saturating_add(1);
2836 } else {
2837 pending.cny_unpriced_turns = pending.cny_unpriced_turns.saturating_add(1);
2838 }
2839 for class in &audit.unpriced_classes {
2840 pending.unpriced_classes.insert(class.label());
2841 }
2842 if !audit.usd_priced
2843 && let Some(reason) = audit.unpriced_reason
2844 {
2845 pending.unpriced_reasons.insert(reason.label());
2846 }
2847 if !audit.cny_priced {
2848 pending.cny_unpriced_reasons.insert(
2849 audit
2850 .unpriced_reason
2851 .map_or("currency_not_published", |reason| reason.label()),
2852 );
2853 }
2854 }
2855
2856 // Record which token classes this route actually billed on, so a receipt
2857 // shows whether cache-write/reasoning telemetry was even present.
2858 pending
2859 .route_receipts
2860 .insert(receipt_with_usage_classes(route_receipt, usage));
2861 }
2862
2863 /// Drain the pending pool, returning it and resetting to zero.
2864 ///
2865 /// Money and its completeness leave together, so a caller can never fold a
2866 /// subtotal into a session total without the counters that qualify it.
2867 #[must_use]
2868 pub fn drain() -> PendingBackgroundCost {
2869 with_pending_state_mut(|state| std::mem::take(&mut state.pending))
2870 }
2871
2872 /// Reset the pool to zero without consuming. Test-only helper for
2873 /// suites that share the static and need to start from a known
2874 /// state. Production code should always use [`drain`].
2875 #[cfg(test)]
2876 pub fn reset_for_tests() {
2877 with_pending_state_mut(|state| {
2878 state.pending = PendingBackgroundCost::default();
2879 state.seen_usage_source_fingerprints.clear();
2880 state.missing_usage_sources.clear();
2881 state.missing_usage_overflowed = false;
2882 });
2883 with_runtime_usage_journal_mut(HashMap::clear);
2884 }
2885
2886 #[cfg(test)]
2887 pub(crate) struct TestCostScope;
2888
2889 #[cfg(test)]
2890 impl Drop for TestCostScope {
2891 fn drop(&mut self) {
2892 reset_for_tests();
2893 }
2894 }
2895
2896 #[cfg(test)]
2897 pub(crate) fn test_scope() -> TestCostScope {
2898 reset_for_tests();
2899 TestCostScope
2900 }
2901
2902 #[cfg(test)]
2903 mod tests {
2904 use super::*;
2905
2906 fn configured_fixture_receipt() -> (crate::config::Config, EffectiveRouteEnvelope, Usage) {
2907 let config = toml::from_str(include_str!(
2908 "../../config/tests/fixtures/custom_models.toml"
2909 ))
2910 .unwrap();
2911 let receipt = EffectiveRouteEnvelope::capture(
2912 Some(&config),
2913 ProviderKind::Deepseek,
2914 "deepseek",
2915 "deepseek-v4.1-flash-expires-on-0910",
2916 Some("https://models.example.test/v1"),
2917 Utc::now(),
2918 );
2919 let usage = Usage {
2920 input_tokens: 1_000_000,
2921 output_tokens: 1_000_000,
2922 ..Usage::default()
2923 };
2924 (config, receipt, usage)
2925 }
2926
2927 #[test]
2928 fn configured_model_estimate_is_frozen_and_exactly_bound() {
2929 let (mut config, receipt, usage) = configured_fixture_receipt();
2930 let audit = receipt.audit(&usage);
2931 assert_eq!(
2932 audit.provenance,
2933 Some(codewhale_config::pricing::PricingProvenance::UserOverride)
2934 );
2935 assert!((audit.estimate.unwrap().usd - 2.0).abs() < 1e-12);
2936 let frozen: EffectiveRouteEnvelope =
2937 serde_json::from_str(&serde_json::to_string(&receipt).unwrap()).unwrap();
2938 config.custom_models.as_mut().unwrap()[0]
2939 .cost
2940 .as_mut()
2941 .unwrap()
2942 .input = Some(9.0);
2943 assert!((frozen.audit(&usage).estimate.unwrap().usd - 2.0).abs() < 1e-12);
2944 for (field, value) in [
2945 ("model", "deepseek-v4.1-flash"),
2946 ("identity", "other-provider"),
2947 ("endpoint", "https://other.example.test/v1"),
2948 ] {
2949 let mut wrong = frozen.clone();
2950 match field {
2951 "model" => wrong.model = value.into(),
2952 "identity" => wrong.provider_identity = value.into(),
2953 _ => wrong.endpoint_fingerprint = endpoint_fingerprint(value),
2954 }
2955 assert!(wrong.audit(&usage).estimate.is_none(), "{field}");
2956 }
2957 for billing in [RouteBillingMode::Local, RouteBillingMode::Subscription] {
2958 let mut nonmoney = frozen.clone();
2959 nonmoney.billing_mode = billing;
2960 assert_eq!(
2961 nonmoney.audit(&usage).unpriced_reason,
2962 Some(crate::pricing::UnpricedReason::NotMoneyMetered)
2963 );
2964 }
2965 let mut cached = usage.clone();
2966 cached.prompt_cache_write_tokens = Some(500);
2967 assert!(frozen.audit(&cached).estimate.is_none());
2968 }
2969
2970 #[test]
2971 fn configured_model_missing_prices_stay_unknown_and_vendor_pin_still_wins() {
2972 let (mut config, receipt, usage) = configured_fixture_receipt();
2973 config.custom_models.as_mut().unwrap()[0].cost = None;
2974 let unknown = EffectiveRouteEnvelope::capture(
2975 Some(&config),
2976 receipt.provider,
2977 receipt.provider_identity.clone(),
2978 receipt.model.clone(),
2979 Some("https://models.example.test/v1"),
2980 receipt.dispatched_at,
2981 );
2982 assert!(unknown.provider_live_pricing.is_some());
2983 assert!(unknown.audit(&usage).estimate.is_none());
2984 let mut pinned = receipt;
2985 pinned.provider = ProviderKind::Openrouter;
2986 pinned.openrouter_vendor = Some("exact-upstream".into());
2987 pinned.billing_mode = RouteBillingMode::Metered;
2988 assert_eq!(
2989 pinned.audit(&usage).unpriced_reason,
2990 Some(crate::pricing::UnpricedReason::RoutingDependentPrice)
2991 );
2992 }
2993
2994 #[test]
2995 fn configured_model_client_keeps_its_metadata_snapshot_after_reload() {
2996 let (mut config, _, usage) = configured_fixture_receipt();
2997 config.set_legacy_root(Some("fixture-not-a-provider-credential".into()), None);
2998 let id = "deepseek-v4.1-flash-expires-on-0910";
2999 let route =
3000 crate::route_runtime::resolve_runtime_route(&config, ProviderKind::Deepseek, Some(id))
3001 .unwrap();
3002 let client =
3003 crate::client::CodewhaleClient::from_candidate(&config, &route.candidate).unwrap();
3004 config.custom_models.as_mut().unwrap()[0]
3005 .cost
3006 .as_mut()
3007 .unwrap()
3008 .input = Some(9.0);
3009 let envelope = client.effective_route_envelope(id, Utc::now());
3010 assert!((envelope.audit(&usage).estimate.unwrap().usd - 2.0).abs() < 1e-12);
3011 assert_eq!(
3012 client
3013 .effective_route_envelope("other-model", Utc::now())
3014 .provider_live_pricing,
3015 None
3016 );
3017 }
3018
3019 const DECLARED_OPENROUTER_MODEL: &str = "synthetic/declared-model";
3020
3021 /// An OpenRouter config on the official endpoint with one
3022 /// `[[custom_models]]` row; `extra` adds fields such as `cost`.
3023 fn openrouter_declared_config(extra: &str, vendor: Option<&str>) -> crate::config::Config {
3024 let vendor = vendor.map_or_else(String::new, |vendor| format!("vendor = \"{vendor}\"\n"));
3025 toml::from_str(&format!(
3026 "provider = \"openrouter\"\ntelemetry = false\n\n\
3027 [[custom_models]]\nprovider = \"openrouter\"\n\
3028 base_url = \"{base}\"\nid = \"{DECLARED_OPENROUTER_MODEL}\"\n{extra}\n\n\
3029 [providers.openrouter]\nbase_url = \"{base}\"\n\
3030 api_key = \"fixture-not-a-provider-credential\"\n{vendor}",
3031 base = crate::config::DEFAULT_OPENROUTER_BASE_URL,
3032 ))
3033 .expect("declared OpenRouter config")
3034 }
3035
3036 fn declared_openrouter_client(
3037 config: &crate::config::Config,
3038 ) -> crate::client::CodewhaleClient {
3039 let route = crate::route_runtime::resolve_runtime_route(
3040 config,
3041 ProviderKind::Openrouter,
3042 Some(DECLARED_OPENROUTER_MODEL),
3043 )
3044 .expect("declared OpenRouter route");
3045 crate::client::CodewhaleClient::from_candidate(config, &route.candidate)
3046 .expect("declared OpenRouter client")
3047 }
3048
3049 /// #6690: a `[[custom_models]]` row declared only to add a model (no
3050 /// rates) used to freeze a rate-less quote on the main turn and hide the
3051 /// endpoint's own catalog price. A declared rate still wins, and with no
3052 /// catalog row the rate-less declaration stays frozen so no same-named
3053 /// bundled price can fill it.
3054 #[test]
3055 fn main_turn_rateless_declaration_yields_to_the_endpoint_catalog_price() {
3056 let _env = crate::test_support::lock_test_env();
3057 let home = tempfile::tempdir().expect("isolated catalog home");
3058 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", home.path());
3059 let _reset = ProviderCatalogTestReset;
3060 crate::provider_catalog_live::reset_cache_for_test();
3061 let _live = crate::provider_lake::lock_live_snapshot();
3062 crate::provider_lake::clear_live_snapshot();
3063
3064 let fingerprint = endpoint_fingerprint(crate::config::DEFAULT_OPENROUTER_BASE_URL)
3065 .expect("official endpoint");
3066 let now = u64::try_from(Utc::now().timestamp()).expect("timestamp");
3067 let rateless = declared_openrouter_client(&openrouter_declared_config("", None));
3068 let declared = declared_openrouter_client(&openrouter_declared_config(
3069 "cost = { input = 0.4, output = 1.6 }",
3070 None,
3071 ));
3072 let quote_for = |client: &crate::client::CodewhaleClient| {
3073 crate::client::main_turn_pricing_quote_at(
3074 Some(client),
3075 ProviderKind::Openrouter,
3076 "openrouter",
3077 DECLARED_OPENROUTER_MODEL,
3078 &fingerprint,
3079 now,
3080 )
3081 .expect("a frozen main-turn quote")
3082 };
3083
3084 // No catalog row yet: the rate-less declaration is frozen as-is.
3085 let frozen = quote_for(&rateless);
3086 assert_eq!(
3087 frozen.provenance,
3088 codewhale_config::pricing::PricingProvenance::UserOverride
3089 );
3090 assert!(!frozen.carries_rates());
3091
3092 crate::provider_catalog_live::record_success(priced_provider_delta(
3093 "openrouter",
3094 DECLARED_OPENROUTER_MODEL,
3095 &fingerprint,
3096 now,
3097 ));
3098 let catalog = quote_for(&rateless);
3099 assert_eq!(
3100 catalog.provenance,
3101 codewhale_config::pricing::PricingProvenance::ProviderLive
3102 );
3103 assert_eq!(catalog.input_per_million.as_deref(), Some("1.25"));
3104 assert_eq!(catalog.output_per_million.as_deref(), Some("5"));
3105 assert_eq!(catalog.cache_read_per_million.as_deref(), Some("0.25"));
3106
3107 let explicit = quote_for(&declared);
3108 assert_eq!(
3109 explicit.provenance,
3110 codewhale_config::pricing::PricingProvenance::UserOverride
3111 );
3112 assert_eq!(explicit.input_per_million.as_deref(), Some("0.4"));
3113 assert_eq!(explicit.output_per_million.as_deref(), Some("1.6"));
3114 }
3115
3116 /// #6690 review: a pinned OpenRouter vendor blocks the aggregate catalog
3117 /// price, but an operator's own declared rate for the exact route is not
3118 /// that aggregate and must still price the turn.
3119 #[test]
3120 fn openrouter_vendor_pin_is_priced_by_an_explicit_declared_rate() {
3121 let _env = crate::test_support::lock_test_env();
3122 let home = tempfile::tempdir().expect("isolated catalog home");
3123 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", home.path());
3124 let _reset = ProviderCatalogTestReset;
3125 crate::provider_catalog_live::reset_cache_for_test();
3126 let _live = crate::provider_lake::lock_live_snapshot();
3127 crate::provider_lake::clear_live_snapshot();
3128
3129 let usage = Usage {
3130 input_tokens: 1_000_000,
3131 output_tokens: 1_000_000,
3132 ..Usage::default()
3133 };
3134 let capture = |extra: &str| {
3135 EffectiveRouteEnvelope::capture(
3136 Some(&openrouter_declared_config(extra, Some("cerebras"))),
3137 ProviderKind::Openrouter,
3138 "openrouter",
3139 DECLARED_OPENROUTER_MODEL,
3140 Some(crate::config::DEFAULT_OPENROUTER_BASE_URL),
3141 Utc::now(),
3142 )
3143 };
3144
3145 let declared = capture("cost = { input = 0.4, output = 1.6 }");
3146 assert_eq!(declared.openrouter_vendor.as_deref(), Some("cerebras"));
3147 let audit = declared.audit(&usage);
3148 assert_eq!(audit.unpriced_reason, None, "{audit:?}");
3149 assert_eq!(
3150 audit.provenance,
3151 Some(codewhale_config::pricing::PricingProvenance::UserOverride)
3152 );
3153 let usd = audit.estimate.expect("declared estimate").usd;
3154 assert!((usd - 2.0).abs() < 1e-12, "{usd}");
3155
3156 // A rate-less declaration is no explicit rate: the pin still wins.
3157 let rateless = capture("");
3158 assert_eq!(
3159 rateless.audit(&usage).unpriced_reason,
3160 Some(crate::pricing::UnpricedReason::RoutingDependentPrice)
3161 );
3162 }
3163
3164 struct ProviderCatalogTestReset;
3165
3166 impl Drop for ProviderCatalogTestReset {
3167 fn drop(&mut self) {
3168 crate::provider_catalog_live::reset_cache_for_test();
3169 crate::provider_lake::clear_live_snapshot();
3170 }
3171 }
3172
3173 fn priced_provider_delta(
3174 provider: &str,
3175 model: &str,
3176 fingerprint: &str,
3177 fetched_at: u64,
3178 ) -> codewhale_config::catalog::ProviderCatalogDelta {
3179 priced_provider_delta_with_rates(provider, model, fingerprint, fetched_at, 1.25, 5.0)
3180 }
3181
3182 fn priced_provider_delta_with_rates(
3183 provider: &str,
3184 model: &str,
3185 fingerprint: &str,
3186 fetched_at: u64,
3187 input: f64,
3188 output: f64,
3189 ) -> codewhale_config::catalog::ProviderCatalogDelta {
3190 codewhale_config::catalog::ProviderCatalogDelta {
3191 provider: provider.to_string(),
3192 base_url_fingerprint: fingerprint.to_string(),
3193 fetched_at,
3194 offerings: vec![codewhale_config::catalog::CatalogOffering {
3195 provider: provider.to_string(),
3196 wire_model_id: model.to_string(),
3197 endpoint_key: "chat".to_string(),
3198 cost: Some(codewhale_config::models_dev::ModelsDevCost {
3199 input: Some(input),
3200 output: Some(output),
3201 cache_read: Some(0.25),
3202 cache_write: None,
3203 }),
3204 ..Default::default()
3205 }],
3206 }
3207 }
3208
3209 fn custom_usage_envelope(
3210 identity: &str,
3211 model: &str,
3212 fingerprint: &str,
3213 billing_mode: RouteBillingMode,
3214 dispatched_at: DateTime<Utc>,
3215 ) -> EffectiveRouteEnvelope {
3216 provider_live_usage_envelope(
3217 ProviderKind::Custom,
3218 identity,
3219 model,
3220 fingerprint,
3221 Some(crate::pricing::UNCLASSIFIED_BILLING_SURFACE),
3222 billing_mode,
3223 dispatched_at,
3224 )
3225 }
3226
3227 fn provider_live_usage_envelope(
3228 provider: ProviderKind,
3229 identity: &str,
3230 model: &str,
3231 fingerprint: &str,
3232 billing_surface: Option<&str>,
3233 billing_mode: RouteBillingMode,
3234 dispatched_at: DateTime<Utc>,
3235 ) -> EffectiveRouteEnvelope {
3236 let provider_live_pricing =
3237 u64::try_from(dispatched_at.timestamp())
3238 .ok()
3239 .and_then(|dispatched_at_unix| {
3240 crate::provider_catalog_live::fresh_provider_live_pricing_quote_at(
3241 provider,
3242 identity,
3243 model,
3244 fingerprint,
3245 dispatched_at_unix,
3246 )
3247 });
3248 EffectiveRouteEnvelope {
3249 provider,
3250 provider_identity: identity.to_string(),
3251 model: model.to_string(),
3252 openrouter_vendor: None,
3253 billing_surface: billing_surface.map(str::to_string),
3254 endpoint_fingerprint: Some(fingerprint.to_string()),
3255 provider_live_pricing,
3256 billing_mode,
3257 dispatched_at,
3258 }
3259 }
3260
3261 fn small_usage() -> Usage {
3262 Usage {
3263 input_tokens: 1_000,
3264 output_tokens: 500,
3265 ..Default::default()
3266 }
3267 }
3268
3269 #[test]
3270 fn baseten_usage_prices_only_the_reviewed_identity_on_the_official_endpoint() {
3271 let _env = crate::test_support::lock_test_env();
3272 let _live = crate::provider_lake::lock_live_snapshot();
3273 let home = tempfile::tempdir().expect("test home");
3274 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", home.path());
3275 let _reset = ProviderCatalogTestReset;
3276 crate::provider_catalog_live::reset_cache_for_test();
3277 crate::provider_lake::clear_live_snapshot();
3278
3279 let now = Utc::now();
3280 let fetched_at = u64::try_from(now.timestamp()).expect("nonnegative timestamp");
3281 let model = "synthetic-baseten-priced-model";
3282 let fingerprint = codewhale_config::catalog::base_url_fingerprint(
3283 codewhale_config::catalog::BASETEN_BASE_URL,
3284 );
3285 crate::provider_catalog_live::record_success(priced_provider_delta(
3286 codewhale_config::catalog::BASETEN_PROVIDER_ID,
3287 model,
3288 &fingerprint,
3289 fetched_at,
3290 ));
3291 let usage = Usage {
3292 input_tokens: 1_000_000,
3293 ..Usage::default()
3294 };
3295
3296 let exact = custom_usage_envelope(
3297 codewhale_config::catalog::BASETEN_PROVIDER_ID,
3298 model,
3299 &fingerprint,
3300 RouteBillingMode::Unknown,
3301 now,
3302 )
3303 .audit(&usage);
3304 assert!(exact.is_priced(), "{exact:?}");
3305 assert_eq!(
3306 exact.provenance,
3307 Some(codewhale_config::pricing::PricingProvenance::ProviderLive)
3308 );
3309 assert_eq!(exact.estimate.expect("priced").usd, 1.25);
3310
3311 // A reviewed schema alias remains a distinct custom ownership scope.
3312 // It becomes billable only after that exact identity refreshed its own
3313 // catalog; it cannot borrow the canonical `baseten` partition above.
3314 let alias = "base-ten";
3315 crate::provider_catalog_live::record_success(priced_provider_delta(
3316 alias,
3317 model,
3318 &fingerprint,
3319 fetched_at,
3320 ));
3321 let alias_audit =
3322 custom_usage_envelope(alias, model, &fingerprint, RouteBillingMode::Unknown, now)
3323 .audit(&usage);
3324 assert!(alias_audit.is_priced(), "{alias_audit:?}");
3325 assert_eq!(
3326 alias_audit.provenance,
3327 Some(codewhale_config::pricing::PricingProvenance::ProviderLive)
3328 );
3329 assert_eq!(alias_audit.estimate.expect("priced").usd, 1.25);
3330
3331 let generic = custom_usage_envelope(
3332 "custom-lab",
3333 model,
3334 &fingerprint,
3335 RouteBillingMode::Metered,
3336 now,
3337 )
3338 .audit(&usage);
3339 assert!(!generic.is_priced(), "{generic:?}");
3340 // The endpoint fingerprint establishes Baseten's billing contract no
3341 // matter the table name, so the failure is an unverified price for
3342 // this identity — not an unknown basis (#6289).
3343 assert_eq!(
3344 generic.unpriced_reason,
3345 Some(crate::pricing::UnpricedReason::UnverifiedLivePricing)
3346 );
3347
3348 let wrong_fingerprint =
3349 codewhale_config::catalog::base_url_fingerprint("https://proxy.example/v1");
3350 let wrong_endpoint = custom_usage_envelope(
3351 codewhale_config::catalog::BASETEN_PROVIDER_ID,
3352 model,
3353 &wrong_fingerprint,
3354 RouteBillingMode::Metered,
3355 now,
3356 )
3357 .audit(&usage);
3358 assert!(!wrong_endpoint.is_priced(), "{wrong_endpoint:?}");
3359 assert_eq!(
3360 wrong_endpoint.unpriced_reason,
3361 Some(crate::pricing::UnpricedReason::UnknownBillingBasis)
3362 );
3363 }
3364
3365 #[test]
3366 fn baseten_usage_rejects_unknown_stale_and_failed_live_catalogs() {
3367 let _env = crate::test_support::lock_test_env();
3368 let _live = crate::provider_lake::lock_live_snapshot();
3369 let home = tempfile::tempdir().expect("test home");
3370 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", home.path());
3371 let _reset = ProviderCatalogTestReset;
3372 crate::provider_catalog_live::reset_cache_for_test();
3373 crate::provider_lake::clear_live_snapshot();
3374
3375 let now = Utc::now();
3376 let now_unix = u64::try_from(now.timestamp()).expect("nonnegative timestamp");
3377 let model = "synthetic-baseten-status-model";
3378 let fingerprint = codewhale_config::catalog::base_url_fingerprint(
3379 codewhale_config::catalog::BASETEN_BASE_URL,
3380 );
3381 let unknown_route = custom_usage_envelope(
3382 codewhale_config::catalog::BASETEN_PROVIDER_ID,
3383 model,
3384 &fingerprint,
3385 RouteBillingMode::Unknown,
3386 now,
3387 );
3388 assert!(unknown_route.provider_live_pricing.is_none());
3389 let usage = Usage {
3390 input_tokens: 1_000_000,
3391 ..Usage::default()
3392 };
3393
3394 // A same-model price owned by another custom partition cannot price a
3395 // Baseten receipt whose exact catalog was never refreshed.
3396 crate::provider_catalog_live::record_success(priced_provider_delta(
3397 "other-custom",
3398 model,
3399 &fingerprint,
3400 now_unix,
3401 ));
3402 let unknown = unknown_route.audit(&usage);
3403 assert!(!unknown.is_priced(), "{unknown:?}");
3404 assert_eq!(
3405 unknown.unpriced_reason,
3406 Some(crate::pricing::UnpricedReason::UnverifiedLivePricing)
3407 );
3408
3409 let stale_at = now_unix
3410 .saturating_sub(crate::provider_catalog_live::DEFAULT_PROVIDER_CATALOG_TTL_SECS)
3411 .saturating_sub(1);
3412 crate::provider_catalog_live::record_success(priced_provider_delta(
3413 codewhale_config::catalog::BASETEN_PROVIDER_ID,
3414 model,
3415 &fingerprint,
3416 stale_at,
3417 ));
3418 let stale_route = custom_usage_envelope(
3419 codewhale_config::catalog::BASETEN_PROVIDER_ID,
3420 model,
3421 &fingerprint,
3422 RouteBillingMode::Unknown,
3423 now,
3424 );
3425 assert!(stale_route.provider_live_pricing.is_none());
3426 let stale = stale_route.audit(&usage);
3427 assert!(!stale.is_priced(), "{stale:?}");
3428 assert_eq!(
3429 stale.unpriced_reason,
3430 Some(crate::pricing::UnpricedReason::UnverifiedLivePricing)
3431 );
3432
3433 crate::provider_catalog_live::record_success(priced_provider_delta(
3434 codewhale_config::catalog::BASETEN_PROVIDER_ID,
3435 model,
3436 &fingerprint,
3437 now_unix,
3438 ));
3439 crate::provider_catalog_live::record_failure(
3440 codewhale_config::catalog::BASETEN_PROVIDER_ID,
3441 &fingerprint,
3442 codewhale_config::catalog::CatalogRefreshError::Network,
3443 );
3444 let failed_route = custom_usage_envelope(
3445 codewhale_config::catalog::BASETEN_PROVIDER_ID,
3446 model,
3447 &fingerprint,
3448 RouteBillingMode::Unknown,
3449 now,
3450 );
3451 assert!(failed_route.provider_live_pricing.is_none());
3452 let failed = failed_route.audit(&usage);
3453 assert!(!failed.is_priced(), "{failed:?}");
3454 assert_eq!(
3455 failed.unpriced_reason,
3456 Some(crate::pricing::UnpricedReason::UnverifiedLivePricing)
3457 );
3458 }
3459
3460 #[test]
3461 fn reviewed_provider_live_quotes_survive_same_second_refresh_and_key_state_changes() {
3462 let _env = crate::test_support::lock_test_env();
3463 let _live = crate::provider_lake::lock_live_snapshot();
3464 let home = tempfile::tempdir().expect("test home");
3465 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", home.path());
3466 let _reset = ProviderCatalogTestReset;
3467 crate::provider_catalog_live::reset_cache_for_test();
3468 crate::provider_lake::clear_live_snapshot();
3469
3470 let now = Utc::now();
3471 let fetched_at = u64::try_from(now.timestamp()).expect("nonnegative timestamp");
3472 let cases = [
3473 (
3474 ProviderKind::Openrouter,
3475 ProviderKind::Openrouter.as_str(),
3476 "synthetic-openrouter-frozen-price",
3477 codewhale_config::catalog::base_url_fingerprint(
3478 crate::config::DEFAULT_OPENROUTER_BASE_URL,
3479 ),
3480 crate::pricing::AGGREGATOR_BILLING_SURFACE,
3481 RouteBillingMode::Metered,
3482 ),
3483 (
3484 ProviderKind::Custom,
3485 codewhale_config::catalog::BASETEN_PROVIDER_ID,
3486 "synthetic-baseten-frozen-price",
3487 codewhale_config::catalog::base_url_fingerprint(
3488 codewhale_config::catalog::BASETEN_BASE_URL,
3489 ),
3490 crate::pricing::UNCLASSIFIED_BILLING_SURFACE,
3491 RouteBillingMode::Unknown,
3492 ),
3493 ];
3494 let usage = Usage {
3495 input_tokens: 1_000_000,
3496 ..Usage::default()
3497 };
3498
3499 for (provider, identity, model, fingerprint, surface, mode) in cases {
3500 crate::provider_catalog_live::record_success(priced_provider_delta_with_rates(
3501 identity,
3502 model,
3503 &fingerprint,
3504 fetched_at,
3505 1.25,
3506 5.0,
3507 ));
3508 let first = provider_live_usage_envelope(
3509 provider,
3510 identity,
3511 model,
3512 &fingerprint,
3513 Some(surface),
3514 mode,
3515 now,
3516 );
3517 let first_quote = first
3518 .provider_live_pricing
3519 .as_ref()
3520 .expect("fresh exact scope freezes a quote");
3521
3522 // A second refresh in the same Unix second must still be a distinct
3523 // catalog revision and must not retroactively change `first`.
3524 crate::provider_catalog_live::record_success(priced_provider_delta_with_rates(
3525 identity,
3526 model,
3527 &fingerprint,
3528 fetched_at,
3529 9.5,
3530 19.0,
3531 ));
3532 let second = provider_live_usage_envelope(
3533 provider,
3534 identity,
3535 model,
3536 &fingerprint,
3537 Some(surface),
3538 mode,
3539 now,
3540 );
3541 let second_quote = second
3542 .provider_live_pricing
3543 .as_ref()
3544 .expect("replacement fresh scope freezes a quote");
3545 assert_ne!(
3546 first_quote.catalog_revision, second_quote.catalog_revision,
3547 "same-second price changes need distinct revisions"
3548 );
3549
3550 crate::provider_catalog_live::record_failure(
3551 identity,
3552 &fingerprint,
3553 codewhale_config::catalog::CatalogRefreshError::Unauthorized,
3554 );
3555 if provider == ProviderKind::Custom {
3556 // Baseten's same URL can represent another account after a key
3557 // switch. Starting that refresh clears the mutable old scope.
3558 let _new_key_refresh = crate::provider_catalog_live::begin_refresh_for_identity(
3559 provider,
3560 identity,
3561 codewhale_config::catalog::BASETEN_BASE_URL,
3562 );
3563 }
3564
3565 let first_audit = first.audit(&usage);
3566 let second_audit = second.audit(&usage);
3567 assert_eq!(first_audit.estimate.expect("first quote priced").usd, 1.25);
3568 assert_eq!(second_audit.estimate.expect("second quote priced").usd, 9.5);
3569
3570 let after_mutation = provider_live_usage_envelope(
3571 provider,
3572 identity,
3573 model,
3574 &fingerprint,
3575 Some(surface),
3576 mode,
3577 now,
3578 );
3579 assert!(
3580 after_mutation.provider_live_pricing.is_none(),
3581 "failed or cleared mutable state cannot mint a new quote"
3582 );
3583 }
3584 }
3585
3586 #[test]
3587 fn legacy_no_quote_receipts_cannot_be_retro_priced_by_a_later_refresh() {
3588 let _env = crate::test_support::lock_test_env();
3589 let _live = crate::provider_lake::lock_live_snapshot();
3590 let home = tempfile::tempdir().expect("test home");
3591 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", home.path());
3592 let _reset = ProviderCatalogTestReset;
3593 crate::provider_catalog_live::reset_cache_for_test();
3594 crate::provider_lake::clear_live_snapshot();
3595
3596 let now = Utc::now();
3597 let fetched_at = u64::try_from(now.timestamp()).expect("nonnegative timestamp");
3598 let routes = [
3599 provider_live_usage_envelope(
3600 ProviderKind::Openrouter,
3601 ProviderKind::Openrouter.as_str(),
3602 "synthetic-openrouter-legacy",
3603 &codewhale_config::catalog::base_url_fingerprint(
3604 crate::config::DEFAULT_OPENROUTER_BASE_URL,
3605 ),
3606 Some(crate::pricing::AGGREGATOR_BILLING_SURFACE),
3607 RouteBillingMode::Metered,
3608 now,
3609 ),
3610 custom_usage_envelope(
3611 codewhale_config::catalog::BASETEN_PROVIDER_ID,
3612 "synthetic-baseten-legacy",
3613 &codewhale_config::catalog::base_url_fingerprint(
3614 codewhale_config::catalog::BASETEN_BASE_URL,
3615 ),
3616 RouteBillingMode::Unknown,
3617 now,
3618 ),
3619 ];
3620 assert!(
3621 routes
3622 .iter()
3623 .all(|route| route.provider_live_pricing.is_none())
3624 );
3625
3626 for route in &routes {
3627 crate::provider_catalog_live::record_success(priced_provider_delta(
3628 &route.provider_identity,
3629 &route.model,
3630 route.endpoint_fingerprint.as_deref().expect("fingerprint"),
3631 fetched_at,
3632 ));
3633 let audit = route.audit(&Usage {
3634 input_tokens: 1_000_000,
3635 ..Usage::default()
3636 });
3637 assert_eq!(
3638 audit.unpriced_reason,
3639 Some(if route.provider == ProviderKind::Openrouter {
3640 crate::pricing::UnpricedReason::NoPricingRow
3641 } else {
3642 crate::pricing::UnpricedReason::UnverifiedLivePricing
3643 }),
3644 "a completion-time refresh must not price {route:?}"
3645 );
3646 }
3647 }
3648
3649 #[test]
3650 fn openrouter_offline_bundled_price_is_immutable_after_dispatch() {
3651 let _env = crate::test_support::lock_test_env();
3652 let _live = crate::provider_lake::lock_live_snapshot();
3653 let home = tempfile::tempdir().expect("test home");
3654 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", home.path());
3655 let _reset = ProviderCatalogTestReset;
3656 crate::provider_catalog_live::reset_cache_for_test();
3657 crate::provider_lake::clear_live_snapshot();
3658
3659 let dispatched_at = Utc::now();
3660 let fetched_at = u64::try_from(dispatched_at.timestamp()).expect("timestamp");
3661 let model = "qwen/qwen3.8-flash";
3662 let fingerprint = codewhale_config::catalog::base_url_fingerprint(
3663 crate::config::DEFAULT_OPENROUTER_BASE_URL,
3664 );
3665 let route = provider_live_usage_envelope(
3666 ProviderKind::Openrouter,
3667 ProviderKind::Openrouter.as_str(),
3668 model,
3669 &fingerprint,
3670 Some(crate::pricing::AGGREGATOR_BILLING_SURFACE),
3671 RouteBillingMode::Metered,
3672 dispatched_at,
3673 );
3674 assert!(route.provider_live_pricing.is_none());
3675
3676 let usage = Usage {
3677 input_tokens: 1_000_000,
3678 ..Usage::default()
3679 };
3680 let offline = route.audit(&usage);
3681 assert_eq!(
3682 offline.estimate.expect("bundled OpenRouter price").usd,
3683 0.15
3684 );
3685 assert_eq!(
3686 offline.provenance,
3687 Some(codewhale_config::pricing::PricingProvenance::ModelsDevBundled)
3688 );
3689
3690 // A later mutable refresh cannot change a turn that had no quote at
3691 // the application-dispatch boundary.
3692 crate::provider_catalog_live::record_success(priced_provider_delta_with_rates(
3693 ProviderKind::Openrouter.as_str(),
3694 model,
3695 &fingerprint,
3696 fetched_at,
3697 19.0,
3698 29.0,
3699 ));
3700 let after_refresh = route.audit(&usage);
3701 assert_eq!(after_refresh, offline);
3702
3703 // Admission without provider usage does not create a charge.
3704 let no_usage = route.audit(&Usage::default());
3705 let no_usage_estimate = no_usage.estimate.expect("known zero usage is priced");
3706 assert_eq!(no_usage_estimate.usd, 0.0);
3707 assert_eq!(no_usage_estimate.cny, 0.0);
3708 }
3709
3710 #[test]
3711 fn provider_live_quotes_reject_future_prices_and_every_route_binding_mismatch() {
3712 let _env = crate::test_support::lock_test_env();
3713 let _live = crate::provider_lake::lock_live_snapshot();
3714 let home = tempfile::tempdir().expect("test home");
3715 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", home.path());
3716 let _reset = ProviderCatalogTestReset;
3717 crate::provider_catalog_live::reset_cache_for_test();
3718 crate::provider_lake::clear_live_snapshot();
3719
3720 let dispatched_at = Utc::now();
3721 let dispatch_unix = u64::try_from(dispatched_at.timestamp()).expect("timestamp");
3722 let future_at = dispatched_at + chrono::Duration::seconds(1);
3723 let future_unix = dispatch_unix.saturating_add(1);
3724 let cases = [
3725 (
3726 ProviderKind::Openrouter,
3727 ProviderKind::Openrouter.as_str(),
3728 "synthetic-openrouter-future",
3729 codewhale_config::catalog::base_url_fingerprint(
3730 crate::config::DEFAULT_OPENROUTER_BASE_URL,
3731 ),
3732 crate::pricing::AGGREGATOR_BILLING_SURFACE,
3733 RouteBillingMode::Metered,
3734 ),
3735 (
3736 ProviderKind::Custom,
3737 codewhale_config::catalog::BASETEN_PROVIDER_ID,
3738 "synthetic-baseten-future",
3739 codewhale_config::catalog::base_url_fingerprint(
3740 codewhale_config::catalog::BASETEN_BASE_URL,
3741 ),
3742 crate::pricing::UNCLASSIFIED_BILLING_SURFACE,
3743 RouteBillingMode::Unknown,
3744 ),
3745 ];
3746 let usage = Usage {
3747 input_tokens: 1_000_000,
3748 ..Usage::default()
3749 };
3750
3751 for (provider, identity, model, fingerprint, surface, mode) in cases {
3752 crate::provider_catalog_live::record_success(priced_provider_delta(
3753 identity,
3754 model,
3755 &fingerprint,
3756 future_unix,
3757 ));
3758 let no_future_quote = provider_live_usage_envelope(
3759 provider,
3760 identity,
3761 model,
3762 &fingerprint,
3763 Some(surface),
3764 mode,
3765 dispatched_at,
3766 );
3767 assert!(no_future_quote.provider_live_pricing.is_none());
3768 assert_eq!(
3769 no_future_quote.audit(&usage).unpriced_reason,
3770 Some(if provider == ProviderKind::Openrouter {
3771 crate::pricing::UnpricedReason::NoPricingRow
3772 } else {
3773 crate::pricing::UnpricedReason::UnverifiedLivePricing
3774 })
3775 );
3776
3777 let captured = provider_live_usage_envelope(
3778 provider,
3779 identity,
3780 model,
3781 &fingerprint,
3782 Some(surface),
3783 mode,
3784 future_at,
3785 );
3786 assert!(captured.provider_live_pricing.is_some());
3787
3788 let mut future_relative_to_dispatch = captured.clone();
3789 future_relative_to_dispatch.dispatched_at = dispatched_at;
3790 assert_eq!(
3791 future_relative_to_dispatch.audit(&usage).unpriced_reason,
3792 Some(crate::pricing::UnpricedReason::UnverifiedLivePricing)
3793 );
3794 let persisted = serde_json::to_value(&future_relative_to_dispatch)
3795 .expect("invalid future quote serializes only as absent");
3796 assert!(persisted["provider_live_pricing"].is_null());
3797
3798 let mut wrong_model = captured.clone();
3799 wrong_model.model.push_str("-other");
3800 assert_eq!(
3801 wrong_model.audit(&usage).unpriced_reason,
3802 Some(crate::pricing::UnpricedReason::UnverifiedLivePricing)
3803 );
3804
3805 let mut wrong_identity = captured.clone();
3806 wrong_identity.provider_identity.push_str("-other");
3807 // The endpoint fingerprint still establishes the billing contract,
3808 // so a renamed identity fails quote verification (#6289).
3809 assert_eq!(
3810 wrong_identity.audit(&usage).unpriced_reason,
3811 Some(crate::pricing::UnpricedReason::UnverifiedLivePricing)
3812 );
3813
3814 let mut wrong_endpoint = captured;
3815 wrong_endpoint.endpoint_fingerprint = Some(
3816 codewhale_config::catalog::base_url_fingerprint("https://proxy.example/v1"),
3817 );
3818 assert_eq!(
3819 wrong_endpoint.audit(&usage).unpriced_reason,
3820 Some(if provider == ProviderKind::Custom {
3821 crate::pricing::UnpricedReason::UnknownBillingBasis
3822 } else {
3823 crate::pricing::UnpricedReason::UnverifiedLivePricing
3824 })
3825 );
3826 }
3827 }
3828
3829 #[test]
3830 fn provider_live_quote_serialization_is_secret_free_and_legacy_compatible() {
3831 let _env = crate::test_support::lock_test_env();
3832 let _live = crate::provider_lake::lock_live_snapshot();
3833 let home = tempfile::tempdir().expect("test home");
3834 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", home.path());
3835 let _reset = ProviderCatalogTestReset;
3836 crate::provider_catalog_live::reset_cache_for_test();
3837 crate::provider_lake::clear_live_snapshot();
3838
3839 let now = Utc::now();
3840 let fetched_at = u64::try_from(now.timestamp()).expect("nonnegative timestamp");
3841 let model = "synthetic-baseten-serialized-quote";
3842 let fingerprint = codewhale_config::catalog::base_url_fingerprint(
3843 codewhale_config::catalog::BASETEN_BASE_URL,
3844 );
3845 crate::provider_catalog_live::record_success(priced_provider_delta(
3846 codewhale_config::catalog::BASETEN_PROVIDER_ID,
3847 model,
3848 &fingerprint,
3849 fetched_at,
3850 ));
3851 let route = custom_usage_envelope(
3852 codewhale_config::catalog::BASETEN_PROVIDER_ID,
3853 model,
3854 &fingerprint,
3855 RouteBillingMode::Unknown,
3856 now,
3857 );
3858 assert!(route.provider_live_pricing.is_some());
3859
3860 let serialized = serde_json::to_string(&route).expect("serialize frozen route");
3861 assert!(serialized.contains("provider_live_pricing"));
3862 assert!(serialized.contains("catalog_revision"));
3863 assert!(serialized.contains("input_per_million"));
3864 for secret in [
3865 codewhale_config::catalog::BASETEN_BASE_URL,
3866 "api_key",
3867 "Bearer ",
3868 ] {
3869 // The assertion message must not itself become a logging sink for
3870 // the credential fragment it checks for — name the check, not the
3871 // secret.
3872 assert!(
3873 !serialized.contains(secret),
3874 "frozen route serialization leaked a credential fragment"
3875 );
3876 }
3877
3878 let mut child = serde_json::json!({});
3879 attach_child_usage_metadata(&mut child, &route, &Usage::default());
3880 let child_route = child_route_envelope_from_metadata(&child).expect("child route");
3881 assert_eq!(child_route, route.sanitized_for_persistence());
3882
3883 let mut legacy: serde_json::Value =
3884 serde_json::from_str(&serialized).expect("route JSON value");
3885 legacy
3886 .as_object_mut()
3887 .expect("route object")
3888 .remove("provider_live_pricing");
3889 let legacy: EffectiveRouteEnvelope =
3890 serde_json::from_value(legacy).expect("legacy route remains readable");
3891 assert!(legacy.provider_live_pricing.is_none());
3892 let audit = legacy.audit(&Usage {
3893 input_tokens: 1_000_000,
3894 ..Usage::default()
3895 });
3896 assert_eq!(
3897 audit.unpriced_reason,
3898 Some(crate::pricing::UnpricedReason::UnverifiedLivePricing)
3899 );
3900
3901 let mut wrong_model = route.clone();
3902 wrong_model.model.push_str("-other");
3903 assert_eq!(
3904 wrong_model.audit(&Usage::default()).unpriced_reason,
3905 Some(crate::pricing::UnpricedReason::UnverifiedLivePricing)
3906 );
3907 }
3908
3909 #[test]
3910 fn routed_child_batch_is_preferred_bounded_and_sanitized() {
3911 let route = deepseek_envelope();
3912 let records = vec![
3913 RuntimeUsageRecord {
3914 source_id: "raw-provider-response-id-one".to_string(),
3915 usage: EffectiveRouteUsage {
3916 route: route.clone(),
3917 usage: Usage {
3918 input_tokens: 11,
3919 ..Usage::default()
3920 },
3921 },
3922 },
3923 RuntimeUsageRecord {
3924 source_id: "raw-provider-response-id-two".to_string(),
3925 usage: EffectiveRouteUsage {
3926 route: route.clone(),
3927 usage: Usage {
3928 output_tokens: 7,
3929 ..Usage::default()
3930 },
3931 },
3932 },
3933 ];
3934 let mut metadata = serde_json::json!({});
3935 attach_child_usage_metadata(&mut metadata, &route, &Usage::default());
3936 attach_child_usage_batch_metadata(
3937 &mut metadata,
3938 &RuntimeUsageBatch {
3939 decisions: Vec::new(),
3940 records,
3941 drop_records: Vec::new(),
3942 dropped_records: 0,
3943 },
3944 );
3945
3946 let serialized = serde_json::to_string(&metadata).expect("batch metadata");
3947 assert!(!serialized.contains("raw-provider-response-id"));
3948 let batch = child_usage_records_from_metadata(&metadata).expect("preferred batch");
3949 assert_eq!(batch.records.len(), 2);
3950 assert_eq!(batch.records[0].usage.usage.input_tokens, 11);
3951 assert_eq!(batch.records[1].usage.usage.output_tokens, 7);
3952 assert_eq!(batch.dropped_records, 0);
3953
3954 metadata[CHILD_USAGE_RECORDS_KEY] = serde_json::json!([{"bad": true}]);
3955 let malformed = child_usage_records_from_metadata(&metadata).expect("batch key wins");
3956 assert!(malformed.records.is_empty());
3957 assert_eq!(malformed.dropped_records, 1);
3958 }
3959
3960 #[test]
3961 fn decision_receipts_metadata_round_trip_is_bounded_and_legacy_compatible() {
3962 let receipt = decision_receipt_fixture("raw-child-decision-id");
3963 let mut metadata = serde_json::json!({});
3964 attach_child_usage_batch_metadata(
3965 &mut metadata,
3966 &RuntimeUsageBatch {
3967 decisions: vec![receipt.clone()],
3968 ..Default::default()
3969 },
3970 );
3971 assert!(!metadata.to_string().contains("raw-child-decision-id"));
3972 let decoded = child_usage_records_from_metadata(&metadata).expect("batch");
3973 assert_eq!(decoded.decisions, vec![receipt.sanitized()]);
3974 assert_eq!(decoded.dropped_records, 0);
3975 metadata[CHILD_DECISION_RECEIPTS_KEY][0]["evidence"]["response_model"] =
3976 serde_json::json!("x".repeat(129));
3977 let invalid = child_usage_records_from_metadata(&metadata).expect("batch");
3978 assert!(invalid.decisions.is_empty());
3979 assert_eq!(invalid.dropped_records, 1);
3980 metadata
3981 .as_object_mut()
3982 .expect("metadata")
3983 .remove(CHILD_DECISION_RECEIPTS_KEY);
3984 assert!(
3985 child_usage_records_from_metadata(&metadata)
3986 .expect("old batch")
3987 .decisions
3988 .is_empty()
3989 );
3990 let mut oversized = receipt;
3991 oversized.evidence.response_model = Some("x".repeat(129));
3992 attach_child_usage_batch_metadata(
3993 &mut metadata,
3994 &RuntimeUsageBatch {
3995 decisions: vec![oversized],
3996 ..Default::default()
3997 },
3998 );
3999 let invalid = child_usage_records_from_metadata(&metadata).expect("bounded batch");
4000 assert!(invalid.decisions.is_empty());
4001 assert_eq!(
4002 invalid.dropped_records, 1,
4003 "discarded evidence must leave an explicit coverage gap"
4004 );
4005 }
4006
4007 fn deepseek() -> BackgroundRoute<'static> {
4008 BackgroundRoute::new(ProviderKind::Deepseek, "deepseek-v4-flash")
4009 .with_base_url(Some(crate::config::DEFAULT_DEEPSEEK_BASE_URL))
4010 }
4011
4012 fn deepseek_envelope() -> EffectiveRouteEnvelope {
4013 let config = crate::config::Config::default();
4014 let identity = config
4015 .active_provider_identity()
4016 .expect("captured DeepSeek identity");
4017 EffectiveRouteEnvelope::from_admitted(
4018 Some(&config),
4019 &identity,
4020 "deepseek-v4-flash",
4021 Some(crate::config::DEFAULT_DEEPSEEK_BASE_URL),
4022 Utc::now(),
4023 )
4024 }
4025
4026 #[test]
4027 fn child_cache_classes_reach_the_background_pool_only_when_reported() {
4028 // #6565: sub-agent cache was missing from session totals.
4029 let reported = background_cost_for_runtime_usage(&RuntimeUsageRecord {
4030 source_id: "child-cache-reported".into(),
4031 usage: EffectiveRouteUsage {
4032 route: deepseek_envelope(),
4033 usage: Usage {
4034 input_tokens: 1_000,
4035 output_tokens: 50,
4036 prompt_cache_hit_tokens: Some(700),
4037 prompt_cache_miss_tokens: Some(300),
4038 ..Usage::default()
4039 },
4040 },
4041 });
4042 assert_eq!(reported.cache_hit_tokens, Some(700));
4043 assert_eq!(reported.cache_miss_tokens, Some(300));
4044 assert_eq!(reported.cache_write_tokens, Some(0));
4045
4046 let silent = background_cost_for_runtime_usage(&RuntimeUsageRecord {
4047 source_id: "child-cache-silent".into(),
4048 usage: EffectiveRouteUsage {
4049 route: deepseek_envelope(),
4050 usage: Usage {
4051 input_tokens: 1_000,
4052 output_tokens: 50,
4053 ..Usage::default()
4054 },
4055 },
4056 });
4057 assert_eq!(silent.cache_hit_tokens, None, "no report is not 0%");
4058 assert_eq!(silent.cache_miss_tokens, None);
4059 }
4060
4061 #[test]
4062 fn background_cache_write_only_telemetry_is_recorded() {
4063 for written in [0, 400] {
4064 let pending = background_cost_for_runtime_usage(&RuntimeUsageRecord {
4065 source_id: "child-cache-write-only".into(),
4066 usage: EffectiveRouteUsage {
4067 route: deepseek_envelope(),
4068 usage: Usage {
4069 input_tokens: 1_000,
4070 prompt_cache_write_tokens: Some(written),
4071 ..Usage::default()
4072 },
4073 },
4074 });
4075 assert_eq!(pending.cache_hit_tokens, Some(0));
4076 assert_eq!(pending.cache_miss_tokens, Some(u64::from(1_000 - written)));
4077 assert_eq!(pending.cache_write_tokens, Some(u64::from(written)));
4078 }
4079 }
4080
4081 #[test]
4082 fn default_usage_is_one_missing_receipt_across_canonical_replay_and_owners() {
4083 let _g = test_scope();
4084 let route = deepseek_envelope();
4085 let raw = "compaction:turn:response";
4086 let fingerprint = usage_source_fingerprint(raw);
4087 let encoded = format!("routed:{fingerprint}");
4088 for source in [raw, fingerprint.as_str(), encoded.as_str()] {
4089 report_effective_route_for_runtime(
4090 scope_token(),
4091 None,
4092 source,
4093 &route,
4094 &Usage::default(),
4095 );
4096 }
4097 let missing = drain();
4098 assert_eq!(missing.priced_turns, 0);
4099 assert_eq!(missing.unpriced_turns, 1);
4100 assert_eq!(missing.cny_unpriced_turns, 1);
4101 assert_eq!(missing.estimate, CostEstimate::default());
4102 assert_eq!(
4103 missing.usage_source_fingerprints,
4104 BTreeSet::from([fingerprint.clone()])
4105 );
4106 assert!(
4107 missing
4108 .unpriced_reasons
4109 .contains("provider_success_missing_usage")
4110 );
4111 report_effective_route_for_runtime(
4112 scope_token(),
4113 None,
4114 &encoded,
4115 &route,
4116 &Usage::default(),
4117 );
4118 assert!(
4119 drain().is_empty(),
4120 "replayed metadata must stay consumed after drain"
4121 );
4122
4123 let owner = "runtime-default-usage-owner";
4124 for source in [raw, fingerprint.as_str(), encoded.as_str()] {
4125 report_effective_route_for_runtime(
4126 scope_token(),
4127 Some(owner),
4128 source,
4129 &route,
4130 &Usage::default(),
4131 );
4132 }
4133 let batch = take_runtime_usage(owner);
4134 assert!(batch.records.is_empty());
4135 assert_eq!(batch.drop_records.len(), 1);
4136 assert_eq!(batch.dropped_records, 1);
4137 assert!(drain().is_empty());
4138 let replay = background_cost_for_runtime_usage(&RuntimeUsageRecord {
4139 source_id: encoded,
4140 usage: EffectiveRouteUsage {
4141 route: route.clone(),
4142 usage: Usage::default(),
4143 },
4144 });
4145 assert_eq!(replay.unpriced_turns, missing.unpriced_turns);
4146 assert_eq!(replay.cny_unpriced_turns, missing.cny_unpriced_turns);
4147 assert_eq!(
4148 replay.usage_source_fingerprints,
4149 missing.usage_source_fingerprints
4150 );
4151
4152 for billing_mode in [RouteBillingMode::Subscription, RouteBillingMode::Local] {
4153 let mut nonmetered = route.clone();
4154 nonmetered.billing_mode = billing_mode;
4155 let cost = background_cost_for_runtime_usage(&RuntimeUsageRecord {
4156 source_id: raw.into(),
4157 usage: EffectiveRouteUsage {
4158 route: nonmetered,
4159 usage: Usage::default(),
4160 },
4161 });
4162 assert_eq!(cost.unpriced_turns, 0);
4163 assert_eq!(cost.cny_unpriced_turns, 0);
4164 assert_eq!(cost.usage_source_fingerprints.len(), 1);
4165 }
4166
4167 let tmp = tempfile::tempdir().unwrap();
4168 let manager =
4169 crate::session_manager::SessionManager::new(tmp.path().join("sessions")).unwrap();
4170 let session = crate::session_manager::create_saved_session_with_id_and_mode(
4171 "missing-origin".into(),
4172 &[],
4173 "deepseek-v4-flash",
4174 tmp.path(),
4175 0,
4176 None,
4177 Some("agent"),
4178 );
4179 manager.save_session(&session).unwrap();
4180 let origin_scope = scope_token();
4181 assert!(close_current_scope().is_empty());
4182 assert!(report_effective_route_for_interactive_origin_with_manager(
4183 origin_scope,
4184 "missing-origin",
4185 "turn",
4186 raw,
4187 &route,
4188 &Usage::default(),
4189 &manager,
4190 ));
4191 for _ in 0..2 {
4192 let snapshot = manager.load_session_snapshot("missing-origin").unwrap();
4193 assert_eq!(snapshot.metadata.total_tokens, 0);
4194 assert_eq!(snapshot.metadata.cost.priced_turns, 0);
4195 assert_eq!(snapshot.metadata.cost.unpriced_turns, 1);
4196 assert_eq!(snapshot.metadata.cost.cny_unpriced_turns, 1);
4197 assert_eq!(snapshot.metadata.cost.usage_source_fingerprints.len(), 1);
4198 manager.save_session(&snapshot).unwrap();
4199 }
4200 assert!(drain().is_empty());
4201 }
4202
4203 #[test]
4204 fn openrouter_vendor_pin_does_not_inherit_aggregate_catalog_price() {
4205 let _env = crate::test_support::lock_test_env();
4206 let home = tempfile::tempdir().expect("isolated catalog home");
4207 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", home.path());
4208 let _reset = ProviderCatalogTestReset;
4209 crate::provider_catalog_live::reset_cache_for_test();
4210 let _live = crate::provider_lake::lock_live_snapshot();
4211 crate::provider_lake::clear_live_snapshot();
4212 let mut route = EffectiveRouteEnvelope::capture(
4213 None,
4214 ProviderKind::Openrouter,
4215 "openrouter",
4216 "qwen/qwen3.7-plus",
4217 Some(ProviderKind::Openrouter.provider().default_base_url()),
4218 Utc::now(),
4219 );
4220 let usage = small_usage();
4221 let aggregate = route.audit(&usage);
4222 assert!(
4223 aggregate.is_priced(),
4224 "aggregate fixture must be priced: {aggregate:?}"
4225 );
4226
4227 route.openrouter_vendor = Some("cerebras".to_string());
4228 let audit = route.audit(&usage);
4229 assert_eq!(
4230 audit.unpriced_reason,
4231 Some(crate::pricing::UnpricedReason::RoutingDependentPrice)
4232 );
4233 assert!(audit.estimate.is_none());
4234 assert!(audit.counts_toward_money_coverage());
4235 assert!(route.receipt(&audit).contains("openrouter_vendor=cerebras"));
4236
4237 // Even a valid, frozen aggregate quote has no upstream-vendor dimension.
4238 let fingerprint = route
4239 .endpoint_fingerprint
4240 .clone()
4241 .expect("official endpoint");
4242 let dispatched_at = u64::try_from(route.dispatched_at.timestamp()).expect("timestamp");
4243 crate::provider_catalog_live::record_success(priced_provider_delta(
4244 "openrouter",
4245 &route.model,
4246 &fingerprint,
4247 dispatched_at,
4248 ));
4249 route.provider_live_pricing =
4250 crate::provider_catalog_live::fresh_provider_live_pricing_quote_at(
4251 route.provider,
4252 &route.provider_identity,
4253 &route.model,
4254 &fingerprint,
4255 dispatched_at,
4256 );
4257 assert!(route.provider_live_pricing.is_some());
4258 let saved: EffectiveRouteEnvelope =
4259 serde_json::from_value(serde_json::to_value(&route).unwrap()).unwrap();
4260 let child = child_route_envelope_from_metadata(&serde_json::Value::Object(
4261 child_usage_metadata_fields(&saved, &usage),
4262 ))
4263 .expect("child envelope");
4264 for receipt in [&route, &saved, &child] {
4265 assert_eq!(
4266 receipt.audit(&usage).unpriced_reason,
4267 Some(crate::pricing::UnpricedReason::RoutingDependentPrice)
4268 );
4269 }
4270
4271 for (billing_mode, reason) in [
4272 (
4273 RouteBillingMode::Subscription,
4274 crate::pricing::UnpricedReason::NotMoneyMetered,
4275 ),
4276 (
4277 RouteBillingMode::Local,
4278 crate::pricing::UnpricedReason::NotMoneyMetered,
4279 ),
4280 (
4281 RouteBillingMode::Unknown,
4282 crate::pricing::UnpricedReason::UnknownBillingBasis,
4283 ),
4284 ] {
4285 route.billing_mode = billing_mode;
4286 assert_eq!(route.audit(&usage).unpriced_reason, Some(reason));
4287 }
4288 }
4289
4290 #[test]
4291 fn openrouter_vendor_pin_survives_envelope_and_child_metadata_persistence() {
4292 let mut config = crate::config::Config {
4293 provider: Some("openrouter".to_string()),
4294 ..Default::default()
4295 };
4296 config
4297 .provider_config_for_mut(&config.test_identity_for_kind(ProviderKind::Openrouter))
4298 .unwrap()
4299 .vendor = Some("cerebras".to_string());
4300 let route = EffectiveRouteEnvelope::capture(
4301 Some(&config),
4302 ProviderKind::Openrouter,
4303 "openrouter",
4304 "qwen/qwen3.7-plus",
4305 Some(ProviderKind::Openrouter.provider().default_base_url()),
4306 Utc::now(),
4307 );
4308 config
4309 .provider_config_for_mut(&config.test_identity_for_kind(ProviderKind::Openrouter))
4310 .unwrap()
4311 .vendor = None;
4312 assert_eq!(route.openrouter_vendor.as_deref(), Some("cerebras"));
4313
4314 let mut json = serde_json::to_value(&route).expect("serialize route");
4315 let restored: EffectiveRouteEnvelope =
4316 serde_json::from_value(json.clone()).expect("restore route");
4317 assert_eq!(restored, route);
4318 let metadata =
4319 serde_json::Value::Object(child_usage_metadata_fields(&route, &small_usage()));
4320 assert_eq!(child_route_envelope_from_metadata(&metadata), Some(route));
4321
4322 json.as_object_mut()
4323 .expect("route object")
4324 .remove("openrouter_vendor");
4325 let legacy: EffectiveRouteEnvelope = serde_json::from_value(json).expect("legacy route");
4326 assert_eq!(legacy.openrouter_vendor, None);
4327 }
4328
4329 #[test]
4330 fn child_metadata_round_trip_preserves_zero_and_reasoning_usage() {
4331 let route = deepseek_envelope();
4332 let usage = Usage {
4333 input_tokens: 0,
4334 output_tokens: 9,
4335 reasoning_tokens: Some(7),
4336 reasoning_replay_tokens: Some(3),
4337 ..Usage::default()
4338 };
4339 let mut metadata = serde_json::json!({"tool": "rlm_eval"});
4340 attach_child_usage_metadata(&mut metadata, &route, &usage);
4341
4342 assert_eq!(child_route_envelope_from_metadata(&metadata), Some(route));
4343 assert_eq!(child_usage_from_metadata(&metadata), Some(usage));
4344
4345 let mut zero_metadata = serde_json::json!({});
4346 let zero = Usage::default();
4347 attach_child_usage_metadata(&mut zero_metadata, &deepseek_envelope(), &zero);
4348 assert_eq!(child_usage_from_metadata(&zero_metadata), Some(zero));
4349 }
4350
4351 #[test]
4352 fn runtime_owned_usage_is_isolated_from_tui_pool() {
4353 let _g = test_scope();
4354 let route = deepseek_envelope();
4355 let usage = small_usage();
4356 report_effective_route_for_runtime(
4357 scope_token(),
4358 Some("turn-a"),
4359 "response-a",
4360 &route,
4361 &usage,
4362 );
4363 report_effective_route_for_runtime(
4364 scope_token(),
4365 Some("turn-b"),
4366 "response-b",
4367 &route,
4368 &usage,
4369 );
4370
4371 assert_eq!(take_runtime_usage("turn-a").records.len(), 1);
4372 assert!(take_runtime_usage("turn-a").records.is_empty());
4373 assert_eq!(take_runtime_usage("turn-b").records.len(), 1);
4374 assert!(
4375 drain().is_empty(),
4376 "runtime-owned usage must not enter TUI cost"
4377 );
4378
4379 report_effective_route_for_runtime(scope_token(), None, "response-tui", &route, &usage);
4380 assert_eq!(drain().priced_turns, 1, "ownerless usage belongs to TUI");
4381 }
4382
4383 /// Every piece of shared cost accounting is scoped to the test that owns
4384 /// it, including the durability sink registry.
4385 ///
4386 /// Sinks are keyed by owner id, and owner ids in tests are short fixture
4387 /// strings that repeat. A process-global registry let one test's
4388 /// `register_runtime_usage_sink` overwrite another's live sink, and let one
4389 /// test's `finish_runtime_usage_owner` retire it mid-flight — so a passing
4390 /// exactly-once assertion depended on which tests happened to run
4391 /// concurrently. This pins the isolation directly: a sink registered on
4392 /// another thread must be invisible here, and usage reported here must not
4393 /// reach it.
4394 #[test]
4395 fn runtime_usage_sinks_do_not_leak_across_test_threads() {
4396 let _g = test_scope();
4397 let owner = "shared-owner";
4398 let other_thread_deliveries = Arc::new(std::sync::atomic::AtomicUsize::new(0));
4399
4400 // A concurrent test, standing in for any other test in the binary that
4401 // happens to use the same owner id.
4402 let deliveries = Arc::clone(&other_thread_deliveries);
4403 let (ready_tx, ready_rx) = std::sync::mpsc::channel();
4404 let (done_tx, done_rx) = std::sync::mpsc::channel();
4405 let other = std::thread::spawn(move || {
4406 register_runtime_usage_sink(
4407 owner,
4408 Arc::new(move |_record| {
4409 deliveries.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
4410 true
4411 }),
4412 );
4413 ready_tx.send(()).expect("signal registration");
4414 // Hold the registration open across this thread's assertions.
4415 done_rx.recv().expect("wait for the other test to finish");
4416 // The other thread's own reports still reach its own sink.
4417 report_effective_route_for_runtime(
4418 scope_token(),
4419 Some(owner),
4420 "response-other",
4421 &deepseek_envelope(),
4422 &small_usage(),
4423 );
4424 });
4425 ready_rx.recv().expect("other test registered its sink");
4426
4427 // This thread never registered a sink, so its usage must fall through
4428 // to this thread's journal — not into the other test's sink.
4429 report_effective_route_for_runtime(
4430 scope_token(),
4431 Some(owner),
4432 "response-mine",
4433 &deepseek_envelope(),
4434 &small_usage(),
4435 );
4436 assert_eq!(
4437 other_thread_deliveries.load(std::sync::atomic::Ordering::SeqCst),
4438 0,
4439 "another test's sink received this test's usage"
4440 );
4441 let mine = take_runtime_usage(owner);
4442 assert_eq!(mine.records.len(), 1);
4443 assert_eq!(mine.records[0].source_id, "response-mine");
4444 assert_eq!(mine.dropped_records, 0);
4445
4446 // Retiring the owner here must not retire the other test's sink.
4447 finish_runtime_usage_owner(owner);
4448 done_tx.send(()).expect("release the other test");
4449 other.join().expect("other test thread");
4450 assert_eq!(
4451 other_thread_deliveries.load(std::sync::atomic::Ordering::SeqCst),
4452 1,
4453 "the other test's sink was retired by an unrelated test"
4454 );
4455 }
4456
4457 #[test]
4458 fn runtime_usage_fallback_is_bounded_and_reports_truncation() {
4459 let _g = test_scope();
4460 let route = deepseek_envelope();
4461 for index in 0..(MAX_RUNTIME_USAGE_RECORDS_PER_OWNER + 3) {
4462 report_effective_route_for_runtime(
4463 scope_token(),
4464 Some("turn-bounded"),
4465 &format!("response-{index}"),
4466 &route,
4467 &small_usage(),
4468 );
4469 }
4470
4471 let batch = take_runtime_usage("turn-bounded");
4472 assert_eq!(batch.records.len(), MAX_RUNTIME_USAGE_RECORDS_PER_OWNER);
4473 assert_eq!(batch.dropped_records, 3);
4474 assert!(drain().is_empty(), "runtime fallback must stay out of TUI");
4475 }
4476
4477 #[test]
4478 fn route_labels_redact_local_paths_but_preserve_model_namespaces() {
4479 let route = EffectiveRouteEnvelope {
4480 openrouter_vendor: None,
4481 provider: ProviderKind::Openrouter,
4482 provider_identity: "/Users/alice/.config/provider-secret".to_string(),
4483 model: "/Volumes/private/checkpoints/model.gguf".to_string(),
4484 billing_surface: None,
4485 endpoint_fingerprint: None,
4486 provider_live_pricing: None,
4487 billing_mode: RouteBillingMode::Metered,
4488 dispatched_at: Utc::now(),
4489 };
4490 let sanitized = route.sanitized_for_persistence();
4491 assert_eq!(sanitized.provider_identity, "redacted-local-path");
4492 assert_eq!(sanitized.model, "redacted-local-path");
4493 let receipt = route.receipt(&TurnCostAudit::unpriced(
4494 crate::pricing::UnpricedReason::NoPricingRow,
4495 ));
4496 assert!(!receipt.contains("alice"));
4497 assert!(!receipt.contains("Volumes"));
4498
4499 assert_eq!(
4500 sanitize_persisted_route_label("anthropic/claude-sonnet-5"),
4501 "anthropic/claude-sonnet-5"
4502 );
4503 }
4504
4505 #[test]
4506 fn route_label_sanitizer_rejects_credentials_urls_and_relative_paths() {
4507 for credential in [
4508 "Bearer secret-token",
4509 "Authorization: Basic abc123",
4510 "OPENAI_API_KEY=sk-secret",
4511 "service_token: ghp_secret",
4512 "hf_secret-token",
4513 "glpat-secret-token",
4514 "db-password=hunter2",
4515 "sk-live-secret",
4516 "https://alice:password@example.test/v1?api_key=secret#fragment",
4517 ] {
4518 let sanitized = sanitize_persisted_route_label(credential);
4519 assert!(
4520 sanitized.starts_with("redacted-"),
4521 "credential was not redacted: {credential:?} -> {sanitized:?}"
4522 );
4523 }
4524 for path in [
4525 ".ssh/id_ed25519",
4526 "../secrets/provider.key",
4527 "workspace/.ssh/config",
4528 "relative/path/to/credential",
4529 r"relative\path\credential",
4530 ] {
4531 assert_eq!(
4532 sanitize_persisted_route_label(path),
4533 "redacted-local-path",
4534 "path was not redacted: {path:?}"
4535 );
4536 }
4537 assert_eq!(
4538 sanitize_persisted_route_label("moonshot/kimi-k3"),
4539 "moonshot/kimi-k3"
4540 );
4541 }
4542
4543 #[test]
4544 fn serialized_route_envelopes_records_and_child_receipts_are_secret_free() {
4545 let route = EffectiveRouteEnvelope {
4546 openrouter_vendor: Some("Authorization: Bearer vendor-secret".to_string()),
4547 provider: ProviderKind::Custom,
4548 provider_identity: "Authorization: Bearer provider-secret".to_string(),
4549 model: "MODEL_API_KEY=sk-model-secret".to_string(),
4550 billing_surface: Some(
4551 "https://alice:password@example.test/v1?token=secret#fragment".to_string(),
4552 ),
4553 endpoint_fingerprint: Some("../.ssh/provider_key".to_string()),
4554 provider_live_pricing: None,
4555 billing_mode: RouteBillingMode::Metered,
4556 dispatched_at: Utc::now(),
4557 };
4558 let usage = Usage {
4559 input_tokens: 7,
4560 output_tokens: 3,
4561 ..Usage::default()
4562 };
4563
4564 let envelope_json = serde_json::to_string(&route).expect("serialize envelope");
4565 let record_json = serde_json::to_string(&EffectiveRouteUsage {
4566 route: route.clone(),
4567 usage: usage.clone(),
4568 })
4569 .expect("serialize route usage");
4570 let child_json = serde_json::to_string(&child_usage_metadata_fields(&route, &usage))
4571 .expect("serialize child receipt");
4572 for serialized in [&envelope_json, &record_json, &child_json] {
4573 for secret in [
4574 "vendor-secret",
4575 "provider-secret",
4576 "sk-model-secret",
4577 "alice",
4578 "password",
4579 "token=secret",
4580 ".ssh",
4581 ] {
4582 assert!(
4583 !serialized.contains(secret),
4584 "serialized route leaked {secret:?}: {serialized}"
4585 );
4586 }
4587 }
4588 }
4589
4590 #[test]
4591 fn report_adds_to_pool_and_drain_returns_then_resets() {
4592 let _g = test_scope();
4593 report(scope_token(), &deepseek(), &small_usage());
4594 let first = drain();
4595 assert!(
4596 first.estimate.usd > 0.0,
4597 "expected positive USD cost, got {first:?}"
4598 );
4599 assert!(
4600 first.estimate.cny > 0.0,
4601 "expected positive CNY cost, got {first:?}"
4602 );
4603 assert_eq!(first.priced_turns, 1);
4604 assert_eq!(first.unpriced_turns, 0);
4605 assert_eq!(first.cny_priced_turns, 1);
4606 assert_eq!(first.cny_unpriced_turns, 0);
4607 // The receipt names the route without leaking the endpoint URL.
4608 assert_eq!(first.route_receipts.len(), 1);
4609 let receipt = first.route_receipts.iter().next().expect("receipt");
4610 assert!(receipt.contains("provider=deepseek"), "{receipt}");
4611 assert!(receipt.contains("model=deepseek-v4-flash"), "{receipt}");
4612 assert!(receipt.contains("currency=usd+cny"), "{receipt}");
4613 assert!(!receipt.contains("http"), "{receipt}");
4614
4615 let second = drain();
4616 assert!(second.is_empty(), "drain must zero the pool: {second:?}");
4617 }
4618
4619 #[test]
4620 fn reports_from_a_closed_session_scope_are_discarded() {
4621 let _g = test_scope();
4622 let old_scope = scope_token();
4623 let settled = close_current_scope();
4624 assert!(settled.is_empty());
4625
4626 report(old_scope, &deepseek(), &small_usage());
4627 assert!(drain().is_empty(), "old session usage crossed the boundary");
4628
4629 report(scope_token(), &deepseek(), &small_usage());
4630 assert_eq!(drain().priced_turns, 1);
4631 }
4632
4633 #[test]
4634 fn retired_origin_acknowledges_sources_without_accrual_or_scope_leak() {
4635 let _g = test_scope();
4636 let tmp = tempfile::tempdir().expect("tempdir");
4637 let manager = crate::session_manager::SessionManager::new(tmp.path().join("sessions"))
4638 .expect("manager");
4639 let session = crate::session_manager::create_saved_session_with_id_and_mode(
4640 "retired-origin".to_string(),
4641 &[],
4642 "deepseek-v4-flash",
4643 tmp.path(),
4644 0,
4645 None,
4646 Some("agent"),
4647 );
4648 manager.save_session(&session).expect("save origin");
4649 let origin_scope = scope_token();
4650 manager
4651 .delete_session("retired-origin")
4652 .expect("delete origin");
4653 let route = deepseek_envelope();
4654 for _ in 0..2 {
4655 assert!(report_effective_route_for_interactive_origin_with_manager(
4656 origin_scope,
4657 "retired-origin",
4658 "origin-turn",
4659 "retired-usage",
4660 &route,
4661 &small_usage(),
4662 &manager,
4663 ));
4664 assert!(report_unreceipted_for_interactive_origin_with_manager(
4665 origin_scope,
4666 "retired-origin",
4667 "origin-turn",
4668 "retired-drop",
4669 &route,
4670 &manager,
4671 ));
4672 }
4673 assert!(usage_source_seen("retired-usage"));
4674 assert!(usage_source_seen("retired-drop"));
4675 assert!(
4676 drain().is_empty(),
4677 "retirement must not create any pending projection"
4678 );
4679 assert!(
4680 !manager
4681 .sessions_dir()
4682 .join(".late-usage/retired-origin.json")
4683 .exists()
4684 );
4685
4686 assert!(close_current_scope().is_empty());
4687 assert!(!usage_source_seen("retired-usage"));
4688 assert!(report_effective_route_for_interactive_origin_with_manager(
4689 origin_scope,
4690 "retired-origin",
4691 "origin-turn",
4692 "after-scope-change",
4693 &route,
4694 &small_usage(),
4695 &manager,
4696 ));
4697 assert!(
4698 !usage_source_seen("after-scope-change"),
4699 "old retirement cannot poison a new scope"
4700 );
4701 assert!(drain().is_empty());
4702 report_effective_route_for_runtime(
4703 scope_token(),
4704 None,
4705 "after-scope-change",
4706 &route,
4707 &small_usage(),
4708 );
4709 assert_eq!(
4710 drain().priced_turns,
4711 1,
4712 "the replacement scope still admits its own response"
4713 );
4714 }
4715
4716 #[test]
4717 fn detached_advisor_and_translation_receipts_survive_new_exactly_once() {
4718 let _g = test_scope();
4719 let tmp = tempfile::tempdir().expect("tempdir");
4720 let manager = crate::session_manager::SessionManager::new(tmp.path().join("sessions"))
4721 .expect("session manager");
4722 let old_session_id = "origin-session";
4723 let new_session_id = "replacement-session";
4724 for session_id in [old_session_id, new_session_id] {
4725 let session = crate::session_manager::create_saved_session_with_id_and_mode(
4726 session_id.to_string(),
4727 &[],
4728 "deepseek-v4-flash",
4729 tmp.path(),
4730 0,
4731 None,
4732 Some("agent"),
4733 );
4734 manager.save_session(&session).expect("save session");
4735 }
4736
4737 let origin_scope = scope_token();
4738 let owner = "interactive:origin-session:origin-turn";
4739 register_persistent_interactive_runtime_usage_sink_at(
4740 owner,
4741 origin_scope,
4742 old_session_id,
4743 "origin-turn",
4744 manager.sessions_dir().to_path_buf(),
4745 );
4746 let advisor_lease = acquire_runtime_usage_lease(owner).expect("advisor owner lease");
4747 finish_runtime_usage_owner(owner);
4748
4749 // `/new` closes the old foreground generation while the detached
4750 // advisor and translation requests are still in flight.
4751 assert!(close_current_scope().is_empty());
4752 let route = deepseek_envelope();
4753 let usage = Usage {
4754 input_tokens: 17,
4755 output_tokens: 5,
4756 ..Usage::default()
4757 };
4758 for _ in 0..2 {
4759 report_effective_route_for_runtime(
4760 origin_scope,
4761 Some(owner),
4762 "advisor:origin-turn:response",
4763 &route,
4764 &usage,
4765 );
4766 report_unreceipted_provider_success(
4767 origin_scope,
4768 Some(owner),
4769 "advisor:origin-turn:missing-usage",
4770 &route,
4771 );
4772 assert!(report_effective_route_for_interactive_origin_with_manager(
4773 origin_scope,
4774 old_session_id,
4775 "origin-turn",
4776 "translation:origin-turn:assistant",
4777 &route,
4778 &usage,
4779 &manager,
4780 ));
4781 assert!(report_unreceipted_for_interactive_origin_with_manager(
4782 origin_scope,
4783 old_session_id,
4784 "origin-turn",
4785 "translation:origin-turn:thinking-missing-usage",
4786 &route,
4787 &manager,
4788 ));
4789 }
4790 drop(advisor_lease);
4791
4792 let fallback = take_runtime_usage(owner);
4793 assert!(fallback.records.is_empty());
4794 assert!(fallback.drop_records.is_empty());
4795 assert_eq!(fallback.dropped_records, 0);
4796 assert!(drain().is_empty(), "late receipts polluted the new scope");
4797
4798 let old = manager
4799 .load_session_snapshot(old_session_id)
4800 .expect("load origin session");
4801 assert_eq!(old.metadata.total_tokens, 44);
4802 assert_eq!(old.metadata.cost.priced_turns, 2);
4803 assert_eq!(old.metadata.cost.unpriced_turns, 2);
4804 assert_eq!(old.metadata.cost.cny_unpriced_turns, 2);
4805 assert_eq!(old.metadata.cost.usage_source_fingerprints.len(), 4);
4806
4807 let replay = manager
4808 .load_session_snapshot(old_session_id)
4809 .expect("replay origin session");
4810 assert_eq!(replay.metadata.total_tokens, 44);
4811 assert_eq!(replay.metadata.cost.usage_source_fingerprints.len(), 4);
4812
4813 let replacement = manager
4814 .load_session_snapshot(new_session_id)
4815 .expect("load replacement session");
4816 assert_eq!(replacement.metadata.total_tokens, 0);
4817 assert_eq!(replacement.metadata.cost.priced_turns, 0);
4818 assert_eq!(replacement.metadata.cost.unpriced_turns, 0);
4819 assert!(
4820 replacement
4821 .metadata
4822 .cost
4823 .usage_source_fingerprints
4824 .is_empty()
4825 );
4826 }
4827
4828 #[test]
4829 fn report_counts_unknown_models_as_missing_spend_not_as_free() {
4830 let _g = test_scope();
4831 // NIM-hosted models intentionally have no DeepSeek pricing, but the
4832 // route *is* money-metered — so the turn is missing spend, not absent.
4833 report(
4834 scope_token(),
4835 &BackgroundRoute::new(ProviderKind::NvidiaNim, "deepseek-ai/deepseek-v4-pro"),
4836 &small_usage(),
4837 );
4838 let drained = drain();
4839 assert_eq!(drained.estimate, CostEstimate::default());
4840 assert_eq!(drained.priced_turns, 0);
4841 assert_eq!(drained.unpriced_turns, 1);
4842 assert!(!drained.unpriced_reasons.is_empty());
4843 }
4844
4845 #[test]
4846 fn report_skips_codex_oauth_pricing_without_calling_it_incomplete() {
4847 let _g = test_scope();
4848 report(
4849 scope_token(),
4850 &BackgroundRoute::new(ProviderKind::OpenaiCodex, "gpt-5.5")
4851 .with_base_url(Some("https://chatgpt.com/backend-api/codex")),
4852 &small_usage(),
4853 );
4854 let drained = drain();
4855 assert_eq!(drained.estimate, CostEstimate::default());
4856 // Exactly non-metered: not counted in either coverage bucket.
4857 assert_eq!(drained.priced_turns, 0);
4858 assert_eq!(drained.unpriced_turns, 0);
4859 assert!(drained.unpriced_reasons.is_empty());
4860 assert!(drained.cny_unpriced_reasons.is_empty());
4861 }
4862
4863 #[test]
4864 fn report_skips_stepfun_without_billing_surface() {
4865 let _g = test_scope();
4866 report(
4867 scope_token(),
4868 &BackgroundRoute::new(ProviderKind::Stepfun, "step-3.7-flash"),
4869 &small_usage(),
4870 );
4871 report(
4872 scope_token(),
4873 &BackgroundRoute::new(ProviderKind::Openrouter, "step-3.7-flash"),
4874 &small_usage(),
4875 );
4876 let drained = drain();
4877 assert_eq!(drained.estimate, CostEstimate::default());
4878 // Both are metered-or-unknown routes that could not be priced, so both
4879 // are reported as missing rather than dropped.
4880 assert_eq!(drained.unpriced_turns, 2);
4881 }
4882
4883 /// A local runtime and a plan endpoint must never be guessed into public
4884 /// per-token dollars just because the provider also sells a paid API.
4885 #[test]
4886 fn local_and_plan_endpoints_are_never_treated_as_public_payg() {
4887 let _g = test_scope();
4888 report(
4889 scope_token(),
4890 &BackgroundRoute::new(ProviderKind::Ollama, "llama3.2"),
4891 &small_usage(),
4892 );
4893 report(
4894 scope_token(),
4895 &BackgroundRoute::new(ProviderKind::Zai, "glm-5.2")
4896 .with_base_url(Some("https://api.z.ai/api/coding/paas/v4")),
4897 &small_usage(),
4898 );
4899 report(
4900 scope_token(),
4901 &BackgroundRoute::new(ProviderKind::Moonshot, "kimi-for-coding")
4902 .with_base_url(Some(crate::config::DEFAULT_KIMI_CODE_BASE_URL)),
4903 &small_usage(),
4904 );
4905 let drained = drain();
4906 assert_eq!(drained.estimate, CostEstimate::default());
4907 assert_eq!(drained.priced_turns, 0);
4908 assert_eq!(
4909 drained.unpriced_turns, 0,
4910 "exactly non-metered routes are not missing dollars: {drained:?}"
4911 );
4912 assert!(drained.unpriced_reasons.is_empty());
4913 assert!(drained.cny_unpriced_reasons.is_empty());
4914 assert!(
4915 drained
4916 .route_receipts
4917 .iter()
4918 .any(|receipt| receipt.contains("surface=zai-coding-plan")),
4919 "{drained:?}"
4920 );
4921 assert!(
4922 drained
4923 .route_receipts
4924 .iter()
4925 .any(|receipt| receipt.contains("surface=local-no-bill")),
4926 "{drained:?}"
4927 );
4928 assert!(
4929 drained
4930 .route_receipts
4931 .iter()
4932 .any(|receipt| receipt.contains("surface=moonshot-kimi-code")),
4933 "{drained:?}"
4934 );
4935 }
4936
4937 /// The receipt carries an endpoint *fingerprint*, never the URL.
4938 #[test]
4939 fn route_receipts_fingerprint_the_endpoint_and_keep_secrets_out() {
4940 let _g = test_scope();
4941 let base_url = "https://api.deepseek.com/v1";
4942 report(
4943 scope_token(),
4944 &deepseek().with_base_url(Some(base_url)),
4945 &small_usage(),
4946 );
4947 let drained = drain();
4948 let receipt = drained.route_receipts.iter().next().expect("receipt");
4949 let expected_fp = endpoint_fingerprint(base_url).expect("valid endpoint fingerprint");
4950 assert!(
4951 receipt.contains(&format!("endpoint_fp={expected_fp}")),
4952 "{receipt}"
4953 );
4954 for needle in ["http", "api.deepseek.com", "sk-", "/Users/", "/home/"] {
4955 assert!(!receipt.contains(needle), "{needle} leaked into {receipt}");
4956 }
4957 }
4958
4959 #[test]
4960 fn receipt_fields_are_bounded_and_secret_bearing_urls_are_not_hashed() {
4961 let hostile = format!("model\nAuthorization: bearer {}", "x".repeat(400));
4962 let receipt = route_receipt(
4963 ProviderKind::Deepseek,
4964 Some("identity\r\nforged=yes"),
4965 &hostile,
4966 Some(crate::pricing::FIRST_PARTY_PAYG_BILLING_SURFACE),
4967 None,
4968 RouteBillingMode::Metered,
4969 "usd+cny",
4970 );
4971 assert!(!receipt.contains('\n'), "{receipt}");
4972 assert!(!receipt.contains('\r'), "{receipt}");
4973 assert!(
4974 receipt.len() < 420,
4975 "receipt was not bounded: {}",
4976 receipt.len()
4977 );
4978
4979 for secret_url in [
4980 "https://user:secret@api.example.com/v1",
4981 "https://api.example.com/v1?api_key=secret",
4982 "https://api.example.com/v1#secret",
4983 ] {
4984 assert_eq!(endpoint_fingerprint(secret_url), None, "{secret_url}");
4985 }
4986 assert_eq!(
4987 endpoint_fingerprint("https://API.Example.com/v1/")
4988 .expect("valid endpoint")
4989 .len(),
4990 64
4991 );
4992 }
4993
4994 #[test]
4995 fn report_accumulates_across_multiple_calls() {
4996 let _g = test_scope();
4997 report(scope_token(), &deepseek(), &small_usage());
4998 report(scope_token(), &deepseek(), &small_usage());
4999 let total = drain();
5000 // Two equal reports — total must be 2× a single report.
5001 let single = crate::pricing::calculate_turn_cost_estimate_from_usage(
5002 "deepseek-v4-flash",
5003 &small_usage(),
5004 )
5005 .unwrap();
5006 assert!((total.estimate.usd - 2.0 * single.usd).abs() < 1e-12);
5007 assert!((total.estimate.cny - 2.0 * single.cny).abs() < 1e-12);
5008 assert_eq!(total.priced_turns, 2);
5009 // Identical routes collapse to one receipt rather than growing without
5010 // bound across a long session.
5011 assert_eq!(total.route_receipts.len(), 1);
5012 }
5013
5014 /// A cache-write turn on a route with no published write rate must show up
5015 /// as missing spend naming the class, not as a discounted total.
5016 #[test]
5017 fn unpriced_cache_write_class_is_reported_not_absorbed() {
5018 let _g = test_scope();
5019 let write_heavy = Usage {
5020 input_tokens: 1_000_000,
5021 output_tokens: 100_000,
5022 prompt_cache_hit_tokens: Some(200_000),
5023 prompt_cache_write_tokens: Some(100_000),
5024 ..Default::default()
5025 };
5026 report(
5027 scope_token(),
5028 &BackgroundRoute::new(ProviderKind::Moonshot, "kimi-k2.7-code")
5029 .with_base_url(Some("https://api.moonshot.ai/v1")),
5030 &write_heavy,
5031 );
5032 let drained = drain();
5033 assert_eq!(drained.estimate, CostEstimate::default());
5034 assert_eq!(drained.unpriced_turns, 1);
5035 assert!(drained.unpriced_reasons.contains("missing_class_price"));
5036 assert!(drained.unpriced_classes.contains("cache_write"));
5037 assert!(
5038 drained
5039 .route_receipts
5040 .iter()
5041 .any(|receipt| receipt.contains("cache_write=yes")),
5042 "{drained:?}"
5043 );
5044 }
5045
5046 #[test]
5047 fn missing_usage_reason_preserves_legacy_bytes_and_metadata_identity() {
5048 let route = deepseek_envelope();
5049 let old = serde_json::json!({"source_id":"opaque-response", "route":route});
5050 let legacy: RuntimeUsageDropRecord = serde_json::from_value(old.clone()).unwrap();
5051 assert_eq!(
5052 legacy.reason,
5053 RuntimeUsageMissingReason::SuccessWithoutUsage
5054 );
5055 assert_eq!(serde_json::to_value(&legacy).unwrap(), old);
5056 let unknown = RuntimeUsageDropRecord {
5057 reason: RuntimeUsageMissingReason::RequestOutcomeUnknown,
5058 ..legacy
5059 };
5060 let mut metadata = serde_json::json!({});
5061 attach_child_usage_batch_metadata(
5062 &mut metadata,
5063 &RuntimeUsageBatch {
5064 drop_records: vec![unknown],
5065 dropped_records: 1,
5066 ..Default::default()
5067 },
5068 );
5069 let recovered = child_usage_records_from_metadata(&metadata).unwrap();
5070 assert_eq!(
5071 recovered.drop_records[0].reason,
5072 RuntimeUsageMissingReason::RequestOutcomeUnknown
5073 );
5074 assert_eq!(
5075 recovered.drop_records[0].source_id,
5076 usage_source_fingerprint("opaque-response")
5077 );
5078 assert_eq!(recovered.dropped_records, 1);
5079 assert!(
5080 !serde_json::to_string(&metadata)
5081 .unwrap()
5082 .contains("opaque-response")
5083 );
5084 }
5085
5086 #[test]
5087 fn exact_missing_usage_promotes_once_before_drain_and_rejects_changed_route() {
5088 let _scope = test_scope();
5089 let route = deepseek_envelope();
5090 report_missing_runtime_usage(
5091 scope_token(),
5092 None,
5093 "attempt",
5094 &route,
5095 RuntimeUsageMissingReason::RequestOutcomeUnknown,
5096 );
5097 let mut changed = route.clone();
5098 changed.model = "another-model".into();
5099 report_effective_route_for_runtime(
5100 scope_token(),
5101 None,
5102 "attempt",
5103 &changed,
5104 &small_usage(),
5105 );
5106 report_effective_route_for_runtime(scope_token(), None, "attempt", &route, &small_usage());
5107 report_effective_route_for_runtime(scope_token(), None, "attempt", &route, &small_usage());
5108 report_missing_runtime_usage(
5109 scope_token(),
5110 None,
5111 "attempt",
5112 &route,
5113 RuntimeUsageMissingReason::RequestOutcomeUnknown,
5114 );
5115 let pool = drain();
5116 assert_eq!(pool.priced_turns, 1);
5117 assert_eq!(pool.unpriced_turns, 0);
5118 assert_eq!(pool.cny_unpriced_turns, 0);
5119 assert!(pool.missing_usage_sources.is_empty());
5120 assert!(pool.estimate.usd > 0.0);
5121 assert_eq!(pool.usage_source_fingerprints.len(), 1);
5122 }
5123
5124 #[test]
5125 fn drained_missing_slot_restores_and_late_receipt_resolves_only_its_origin() {
5126 let _scope = test_scope();
5127 let route = deepseek_envelope();
5128 report_missing_runtime_usage(
5129 scope_token(),
5130 None,
5131 "restored-attempt",
5132 &route,
5133 RuntimeUsageMissingReason::RequestOutcomeUnknown,
5134 );
5135 let missing = drain();
5136 let mut slots = missing.missing_usage_sources.clone();
5137 let mut overflow = false;
5138 let mut unpriced = missing.unpriced_turns;
5139 let mut cny_unpriced = missing.cny_unpriced_turns;
5140 assert!(close_current_scope().is_empty());
5141 restore_usage_source_ledger(missing.usage_source_fingerprints.clone(), &slots, overflow);
5142 report_effective_route_for_runtime(
5143 scope_token(),
5144 None,
5145 "restored-attempt",
5146 &route,
5147 &small_usage(),
5148 );
5149 let known = drain();
5150 let projected = project_missing_usage_ledger(
5151 &mut slots,
5152 &mut overflow,
5153 &mut unpriced,
5154 &mut cny_unpriced,
5155 &known,
5156 );
5157 assert!(slots.is_empty());
5158 assert_eq!((unpriced, cny_unpriced), (0, 0));
5159 assert_eq!(projected.priced_turns, 1);
5160 report_effective_route_for_runtime(
5161 scope_token(),
5162 None,
5163 "restored-attempt",
5164 &route,
5165 &small_usage(),
5166 );
5167 assert!(drain().is_empty());
5168 // A legacy snapshot retained only consumed identities. Its unattributed
5169 // coverage remains conservative rather than being erased by a replay.
5170 assert!(close_current_scope().is_empty());
5171 restore_usage_source_fingerprints(missing.usage_source_fingerprints);
5172 report_effective_route_for_runtime(
5173 scope_token(),
5174 None,
5175 "restored-attempt",
5176 &route,
5177 &small_usage(),
5178 );
5179 assert!(drain().is_empty());
5180 }
5181
5182 #[test]
5183 fn missing_usage_overflow_is_bounded_and_cannot_be_erased_by_late_receipts() {
5184 let _scope = test_scope();
5185 let route = deepseek_envelope();
5186 for index in 0..65 {
5187 for _ in 0..2 {
5188 report_missing_runtime_usage(
5189 scope_token(),
5190 None,
5191 &format!("attempt-{index}"),
5192 &route,
5193 RuntimeUsageMissingReason::RequestOutcomeUnknown,
5194 );
5195 }
5196 }
5197 let missing = drain();
5198 assert_eq!(
5199 missing.missing_usage_sources.len(),
5200 MAX_MISSING_USAGE_SOURCES
5201 );
5202 assert_eq!(missing.unpriced_turns, 65);
5203 assert!(missing.missing_usage_overflowed);
5204 let mut slots = missing.missing_usage_sources.clone();
5205 let mut overflow = missing.missing_usage_overflowed;
5206 let mut unpriced = missing.unpriced_turns;
5207 let mut cny_unpriced = missing.cny_unpriced_turns;
5208 for index in 0..65 {
5209 for _ in 0..2 {
5210 report_effective_route_for_runtime(
5211 scope_token(),
5212 None,
5213 &format!("attempt-{index}"),
5214 &route,
5215 &small_usage(),
5216 );
5217 }
5218 }
5219 let known = drain();
5220 let projected = project_missing_usage_ledger(
5221 &mut slots,
5222 &mut overflow,
5223 &mut unpriced,
5224 &mut cny_unpriced,
5225 &known,
5226 );
5227 assert_eq!(projected.priced_turns, 65);
5228 assert_eq!((unpriced, cny_unpriced), (1, 1));
5229 assert!(slots.is_empty());
5230 assert!(overflow);
5231 assert!(projected.estimate.usd > 0.0);
5232 }
5233
5234 #[test]
5235 fn runtime_unknown_batch_keeps_reason_and_promotes_without_new_response_identity() {
5236 let _scope = test_scope();
5237 let route = deepseek_envelope();
5238 let owner = "unknown-batch-owner";
5239 report_missing_runtime_usage(
5240 scope_token(),
5241 Some(owner),
5242 "batch-attempt",
5243 &route,
5244 RuntimeUsageMissingReason::RequestOutcomeUnknown,
5245 );
5246 let batch = take_runtime_usage(owner);
5247 assert_eq!(
5248 batch.drop_records[0].reason,
5249 RuntimeUsageMissingReason::RequestOutcomeUnknown
5250 );
5251 let mut metadata = serde_json::json!({});
5252 attach_child_usage_batch_metadata(&mut metadata, &batch);
5253 let batch = child_usage_records_from_metadata(&metadata).unwrap();
5254 report_runtime_usage_batch(scope_token(), None, &batch);
5255 report_runtime_usage_batch(scope_token(), None, &batch);
5256 let missing = drain();
5257 assert_eq!(missing.unpriced_turns, 1);
5258 assert!(missing.unpriced_reasons.contains("request_outcome_unknown"));
5259 report_effective_route_for_runtime(
5260 scope_token(),
5261 None,
5262 "batch-attempt",
5263 &route,
5264 &small_usage(),
5265 );
5266 let known = drain();
5267 assert_eq!(known.priced_turns, 1);
5268 assert_eq!(
5269 known.resolved_missing_usage_sources,
5270 BTreeSet::from([usage_source_fingerprint("batch-attempt")])
5271 );
5272 }
5273
5274 #[test]
5275 fn late_unknown_receipt_promotes_after_restart_without_charging_replacement_session() {
5276 let _scope = test_scope();
5277 let tmp = tempfile::tempdir().unwrap();
5278 let sessions = tmp.path().join("sessions");
5279 let manager = crate::session_manager::SessionManager::new(sessions.clone()).unwrap();
5280 for id in ["unknown-origin", "replacement"] {
5281 let session = crate::session_manager::create_saved_session_with_id_and_mode(
5282 id.into(),
5283 &[],
5284 "deepseek-v4-flash",
5285 tmp.path(),
5286 0,
5287 None,
5288 Some("agent"),
5289 );
5290 manager.save_session(&session).unwrap();
5291 }
5292 let origin = scope_token();
5293 assert!(close_current_scope().is_empty());
5294 let route = deepseek_envelope();
5295 assert!(report_missing_usage_for_interactive_origin_with_manager(
5296 origin,
5297 "unknown-origin",
5298 "origin-turn",
5299 "late-attempt",
5300 &route,
5301 RuntimeUsageMissingReason::RequestOutcomeUnknown,
5302 &manager
5303 ));
5304 let missing = manager.load_session_snapshot("unknown-origin").unwrap();
5305 assert_eq!(missing.metadata.cost.unpriced_turns, 1);
5306 assert_eq!(missing.metadata.cost.missing_usage_sources.len(), 1);
5307 assert!(
5308 missing
5309 .metadata
5310 .cost
5311 .unpriced_reasons
5312 .contains("request_outcome_unknown")
5313 );
5314 manager.save_session(&missing).unwrap();
5315 drop(manager);
5316 let manager = crate::session_manager::SessionManager::new(sessions).unwrap();
5317 for _ in 0..2 {
5318 assert!(report_effective_route_for_interactive_origin_with_manager(
5319 origin,
5320 "unknown-origin",
5321 "origin-turn",
5322 "late-attempt",
5323 &route,
5324 &small_usage(),
5325 &manager
5326 ));
5327 let known = manager.load_session_snapshot("unknown-origin").unwrap();
5328 assert_eq!(known.metadata.cost.priced_turns, 1);
5329 assert_eq!(known.metadata.cost.unpriced_turns, 0);
5330 assert!(known.metadata.cost.missing_usage_sources.is_empty());
5331 assert_eq!(
5332 known.metadata.total_tokens,
5333 u64::from(small_usage().input_tokens) + u64::from(small_usage().output_tokens)
5334 );
5335 manager.save_session(&known).unwrap();
5336 }
5337 assert!(!report_effective_route_for_interactive_origin_with_manager(
5338 origin,
5339 "unknown-origin",
5340 "different-turn",
5341 "late-attempt",
5342 &route,
5343 &small_usage(),
5344 &manager
5345 ));
5346 let replacement = manager.load_session_snapshot("replacement").unwrap();
5347 assert_eq!(replacement.metadata.total_tokens, 0);
5348 assert_eq!(
5349 (
5350 replacement.metadata.cost.priced_turns,
5351 replacement.metadata.cost.unpriced_turns
5352 ),
5353 (0, 0)
5354 );
5355 assert!(drain().is_empty());
5356 }
5357 }
5358
5358 lines RUST