From d6f9792f397693d1c18aee74dc71839ac45b72ce Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 28 Oct 2026 18:13:14 +0100 Subject: [PATCH] connector-spend: one function decides the charge; a bound result is billed its true cost whatever else is malformed (s4.bound-malformed-overrun-undercharged, s4.binding-identity-unpinned) Ruling spend-bills-on-identity-not-validity: ProviderCall::charge is the only charge decision; complete no longer routes billing through validate_against. Tests: malformed bound overruns reach the ceiling; each identity field alone sends a result to the reservation. Co-Authored-By: Claude Opus 5.5 --- crates/model-connector/src/service.rs | 126 ++++++++++++++++++++------ 1 file changed, 100 insertions(+), 26 deletions(-) diff --git a/crates/model-connector/src/service.rs b/crates/model-connector/src/service.rs index 2837c84..f65dc2b 100644 --- a/crates/model-connector/src/service.rs +++ b/crates/model-connector/src/service.rs @@ -393,6 +393,34 @@ impl ProviderCall { }) } + /// What a completed `result` is billed: the one owner of the charge + /// decision. A transported result bound to `invocation` by request id and + /// caller (on the result and on its receipt) costs what its own outcome's + /// usage derives, with the overrun past the reservation recorded + /// truthfully, whether or not it is otherwise malformed. Any other result + /// (refused, or bound to another request or caller) costs the whole + /// reservation. Validity decides what the caller is told, never this. + fn charge(&self, invocation: &TransportInvocation, result: &TransportResult) -> Charge { + match &result.disposition { + TransportDisposition::Transported { receipt, .. } + if result.request_id == invocation.request_id() + && result.caller_runtime_id == invocation.caller_runtime_id + && receipt.request_id == invocation.request_id() + && receipt.caller_runtime_id == invocation.caller_runtime_id => + { + let cost_micro_usd = self.cost_of(&receipt.outcome); + Charge { + cost_micro_usd, + overrun_micro_usd: self.overrun_of(cost_micro_usd), + } + } + _ => Charge { + cost_micro_usd: self.reserved_micro_usd, + overrun_micro_usd: 0, + }, + } + } + /// How far `cost_micro_usd` exceeds what was reserved. The reservation is /// the worst case the kit can compute, so a nonzero overrun means the /// backend broke its contract (usage above its own request's bounds). @@ -839,28 +867,7 @@ impl TransportService { // is recorded and the slot released, so the caller gets an answer and // a retry replays it. let admissible = self.admits_result(&result, &claim); - // The service prices what the backend reported, but only for a result - // bound to this invocation: a transported result charges the cost its - // own outcome's usage derives, and its overrun past the reservation, - // whatever its receipt says (a receipt that misstates either is - // replaced by the stand-in but still charged the true cost). Any - // other result, including one bound to another request, charges the - // whole reservation. - let charge = match &result.disposition { - TransportDisposition::Transported { receipt, .. } - if result.validate_against(invocation).is_ok() => - { - let cost_micro_usd = claim.call.cost_of(&receipt.outcome); - Charge { - cost_micro_usd, - overrun_micro_usd: claim.call.overrun_of(cost_micro_usd), - } - } - _ => Charge { - cost_micro_usd: reserved_micro_usd, - overrun_micro_usd: 0, - }, - }; + let charge = claim.call.charge(invocation, &result); let charged_micro_usd = charge.cost_micro_usd; let mut response = if admissible { encrypt_result(&result, &security).inspect_err(|_| { @@ -3471,6 +3478,76 @@ mod tests { drop(two); } + #[test] + fn a_bound_malformed_overrun_is_charged_its_true_cost_and_reaches_the_ceiling() { + let reserved = reservation(); + for case in 0..3 { + let store = TestStore::new(); + let mut service = open_limited(&store, 2 * reserved, 3_600_000); + let claim = claim_at(&mut service, "request-1", 1_000); + let mut result = overrun_result(claim.call(), "done"); + let TransportDisposition::Transported { events, receipt } = &mut result.disposition + else { + unreachable!(); + }; + match case { + 0 => events[0].sequence = 1, + 1 => { + let TransportOutcome::Completed { usage, .. } = &receipt.outcome else { + unreachable!(); + }; + receipt.outcome = TransportOutcome::Failed { + kind: FailureKind::Provider, + message: String::new(), + usage: Some(usage.clone()), + }; + } + _ => receipt.provider = String::new(), + } + let invocation = invocation_at("request-1", 1_000, MAX_COST); + assert!(result.validate_against(&invocation).is_err(), "{case}"); + let response = service.complete(*claim, result, FRAME).unwrap(); + // The caller is told the result was refused; the bill is its true cost. + assert_eq!( + receipt_of(&response, &invocation).provider, + STAND_IN_PROVIDER, + "{case}" + ); + assert_eq!(charged_of(&service, "request-1"), OVERRUN_COST, "{case}"); + assert_eq!(service.window_spend(CALLER, 0), OVERRUN_COST, "{case}"); + assert_eq!( + refusal_at(&mut service, "request-2", 1_000, MAX_COST), + Refusal::SpendCeiling, + "{case}" + ); + } + } + + #[test] + fn each_binding_check_alone_sends_a_result_to_the_reservation() { + let reserved = reservation(); + // Every other field is valid and the usage overruns: a guard that + // ignored the one changed identity field would charge 30,000,000. + for case in 0..4 { + let store = TestStore::new(); + let mut service = open_limited(&store, 2 * reserved, 3_600_000); + let claim = claim_at(&mut service, "request-1", 1_000); + let mut result = overrun_result(claim.call(), "done"); + let TransportDisposition::Transported { receipt, .. } = &mut result.disposition else { + unreachable!(); + }; + match case { + 0 => result.request_id = "request-2".to_string(), + 1 => result.caller_runtime_id = "another-caller".to_string(), + 2 => receipt.request_id = "request-2".to_string(), + _ => receipt.caller_runtime_id = "another-caller".to_string(), + } + service.complete(*claim, result, FRAME).unwrap(); + assert_eq!(charged_of(&service, "request-1"), reserved, "{case}"); + assert_eq!(service.window_spend(CALLER, 0), reserved, "{case}"); + } + } + #[test] fn a_result_that_is_not_transported_is_charged_the_reservation() { let store = TestStore::new(); @@ -3518,10 +3595,7 @@ mod tests { let mut service = open_limited(&store, ceiling, window); assert_eq!(service.admission_window_start_unix_ms(CALLER), None); assert_eq!(run_window(&mut service, "fill", 50_000), 3); - assert_eq!( - service.admission_window_start_unix_ms(CALLER), - Some(50_000) - ); + assert_eq!(service.admission_window_start_unix_ms(CALLER), Some(50_000)); // Behind the full window: the cause is named, no slot is taken and // nothing is reserved. assert_eq!(