feat: add selectable routing groups and composite billing

Support per-model provider enablement and compact model editing. Capture request-time billing factors, charge customer costs separately, and preserve historical statistics without backfills.
This commit is contained in:
elky
2026-10-07 14:49:57 +08:00
parent 310098a853
commit 911c7f8875
110 changed files with 6524 additions and 559 deletions
@@ -577,6 +577,45 @@ pub struct UpdateUserApiKeyBasicRecord {
/// unchanged. Keeping this patch in the basic mutation record lets repositories apply the
/// complete user-key update in one atomic write.
pub feature_settings: Option<Option<serde_json::Value>>,
/// Self-service updates merge routing selection separately against the
/// current stored settings. `None` retains administrative replacement semantics.
pub routing_group_selection: Option<UpdateApiKeyRoutingGroupSelection>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateApiKeyRoutingGroupSelection {
/// `None` preserves the latest stored group; `Some(None)` follows the
/// default; `Some(Some(id))` selects the validated public group.
pub group_id: Option<Option<String>>,
}
impl UpdateApiKeyRoutingGroupSelection {
/// Repositories must call this while holding the same write lock as the
/// surrounding API key mutation, so unrelated edits cannot restore a stale
/// group choice or a stale feature-settings object.
pub fn merge_feature_settings(
&self,
current: Option<&serde_json::Value>,
replacement: Option<Option<serde_json::Value>>,
) -> Option<serde_json::Value> {
let group_id = match &self.group_id {
None => current
.and_then(|value| value.get("routing_group_id"))
.and_then(serde_json::Value::as_str)
.map(|id| serde_json::Value::String(id.to_string())),
Some(group_id) => group_id.clone().map(serde_json::Value::String),
};
let mut settings = replacement
.unwrap_or_else(|| current.cloned())
.and_then(|value| value.as_object().cloned())
.unwrap_or_default();
settings.remove("routing_group_id");
settings.remove("routing_group_name");
if let Some(group_id) = group_id {
settings.insert("routing_group_id".to_string(), group_id);
}
(!settings.is_empty()).then_some(serde_json::Value::Object(settings))
}
}
impl std::fmt::Debug for UpdateUserApiKeyBasicRecord {
@@ -346,6 +346,10 @@ pub struct UsageSettlementInput {
pub billing_status: String,
pub total_cost_usd: f64,
pub actual_total_cost_usd: f64,
/// Customer charge after all captured billing factors, independent of upstream cost.
/// Missing values retain the legacy charge based on `actual_total_cost_usd`.
#[serde(default)]
pub billing_cost_usd: Option<f64>,
pub finalized_at_unix_secs: Option<u64>,
}
@@ -366,6 +370,14 @@ impl UsageSettlementInput {
"settlement cost must be finite".to_string(),
));
}
if self
.billing_cost_usd
.is_some_and(|value| !value.is_finite() || value < 0.0)
{
return Err(crate::DataLayerError::InvalidInput(
"settlement billing_cost_usd must be finite and non-negative".to_string(),
));
}
Ok(())
}
}
@@ -511,14 +523,17 @@ pub fn settlement_billing_status_for_usage_status(status: &str) -> &'static str
}
pub fn settlement_billable_cost_usd(input: &UsageSettlementInput) -> f64 {
input.actual_total_cost_usd.max(0.0)
input
.billing_cost_usd
.unwrap_or(input.actual_total_cost_usd)
.max(0.0)
}
#[cfg(test)]
mod tests {
use super::{
validate_wallet_settlement_values, ReconcileUsagePolicyCostInput,
ReserveUsagePolicyCostInput, ReserveUsagePolicyRequestInput,
settlement_billable_cost_usd, validate_wallet_settlement_values,
ReconcileUsagePolicyCostInput, ReserveUsagePolicyCostInput, ReserveUsagePolicyRequestInput,
UsagePolicyCostReservationState, UsagePolicyCostWindow, UsagePolicyRequestWindow,
UsageSettlementInput,
};
@@ -535,11 +550,50 @@ mod tests {
billing_status: "pending".to_string(),
total_cost_usd: 0.1,
actual_total_cost_usd: 0.1,
billing_cost_usd: None,
finalized_at_unix_secs: None,
};
assert!(input.validate().is_err());
}
#[test]
fn explicit_customer_charge_is_validated_independently_of_upstream_cost() {
let mut input: UsageSettlementInput = serde_json::from_value(serde_json::json!({
"request_id": "billing-charge",
"user_id": "user-1",
"api_key_id": null,
"provider_id": "provider-1",
"status": "completed",
"billing_status": "pending",
"total_cost_usd": 2.0,
"actual_total_cost_usd": 0.5,
"finalized_at_unix_secs": null,
}))
.expect("legacy settlement input should deserialize");
assert_eq!(input.billing_cost_usd, None);
assert_eq!(settlement_billable_cost_usd(&input), 0.5);
assert!(input.validate().is_ok());
for charge in [3.0, 0.0] {
input.billing_cost_usd = Some(charge);
assert!(input.validate().is_ok());
assert_eq!(settlement_billable_cost_usd(&input), charge);
assert_eq!(input.actual_total_cost_usd, 0.5);
assert_eq!(
serde_json::from_value::<UsageSettlementInput>(
serde_json::to_value(&input).unwrap()
)
.unwrap(),
input
);
}
for charge in [-0.01, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
input.billing_cost_usd = Some(charge);
assert!(input.validate().is_err());
}
}
#[test]
fn wallet_settlement_values_reject_corruption_and_overflow() {
assert!(validate_wallet_settlement_values(-3.0, 0.0, 12.0, 1.0).is_ok());
@@ -0,0 +1,169 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::DataLayerError;
use super::ROUTING_GROUP_BILLING_MULTIPLIER_METADATA_KEY;
pub const BILLING_MULTIPLIER_SNAPSHOT_METADATA_KEY: &str = "billing_multiplier_snapshot";
/// Immutable customer pricing factors. Provider Key rates belong to upstream cost,
/// not this snapshot. Add future factors (for example `user_group`) at admission.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BillingMultiplierSnapshot {
version: u32,
factors: BTreeMap<String, f64>,
multiplier: f64,
}
impl BillingMultiplierSnapshot {
pub fn from_factors(factors: BTreeMap<String, f64>) -> Result<Self, DataLayerError> {
if factors.len() > 16
|| factors.iter().any(|(name, value)| {
name.is_empty()
|| name.len() > 64
|| !name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
|| !value.is_finite()
|| *value < 0.0
})
{
return Err(invalid_snapshot());
}
let multiplier = if factors.values().any(|value| *value == 0.0) {
0.0
} else {
factors.values().product::<f64>()
};
if !multiplier.is_finite() {
return Err(invalid_snapshot());
}
Ok(Self {
version: 1,
factors,
multiplier,
})
}
pub fn validate(&self) -> Result<(), DataLayerError> {
let expected = Self::from_factors(self.factors.clone())?;
if self.version != 1 || self.multiplier != expected.multiplier {
return Err(invalid_snapshot());
}
Ok(())
}
pub fn multiplier(&self) -> f64 {
self.multiplier
}
pub fn cost(&self, base_cost: f64) -> Result<f64, DataLayerError> {
self.validate()?;
let cost = base_cost * self.multiplier;
if !base_cost.is_finite() || base_cost < 0.0 || !cost.is_finite() {
return Err(DataLayerError::InvalidInput(
"customer billing cost must be finite and non-negative".to_string(),
));
}
// Match wallet storage and usage-policy cost units (eight decimals).
// Scaling a finite large amount must not introduce infinity by itself.
let scaled = cost * 100_000_000.0;
Ok(if scaled.is_finite() {
scaled.round() / 100_000_000.0
} else {
cost
})
}
}
fn invalid_snapshot() -> DataLayerError {
DataLayerError::InvalidInput("invalid billing multiplier snapshot".to_string())
}
/// None preserves legacy charging. A malformed captured snapshot is an error,
/// never an instruction to silently charge a different rate.
pub fn billing_multiplier_snapshot(
metadata: Option<&Value>,
) -> Result<Option<BillingMultiplierSnapshot>, DataLayerError> {
let Some(metadata) = metadata.and_then(Value::as_object) else {
return Ok(None);
};
if let Some(value) = metadata.get(BILLING_MULTIPLIER_SNAPSHOT_METADATA_KEY) {
let snapshot: BillingMultiplierSnapshot =
serde_json::from_value(value.clone()).map_err(|_| invalid_snapshot())?;
snapshot.validate()?;
return Ok(Some(snapshot));
}
if let Some(value) = metadata.get(ROUTING_GROUP_BILLING_MULTIPLIER_METADATA_KEY) {
let multiplier = value.as_f64().ok_or_else(invalid_snapshot)?;
return BillingMultiplierSnapshot::from_factors(BTreeMap::from([(
"routing_group".to_string(),
multiplier,
)]))
.map(Some);
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn composes_customer_factors_without_provider_cost_and_freezes_them() {
let snapshot = BillingMultiplierSnapshot::from_factors(BTreeMap::from([
("routing_group".to_string(), 2.0),
("user_group".to_string(), 0.75),
]))
.unwrap();
assert_eq!(snapshot.multiplier(), 1.5);
assert_eq!(snapshot.cost(10.0).unwrap(), 15.0);
assert_eq!(snapshot.cost(0.123456789).unwrap(), 0.18518518);
let metadata = json!({"billing_multiplier_snapshot": snapshot, "routing_group_billing_multiplier": 99, "rate_multiplier": 0.1});
assert_eq!(
billing_multiplier_snapshot(Some(&metadata)).unwrap(),
Some(snapshot)
);
assert_eq!(billing_multiplier_snapshot(None).unwrap(), None);
}
#[test]
fn rejects_corrupt_overflowing_snapshots_and_accepts_zero_rates() {
for factors in [
BTreeMap::from([("routing_group".into(), -1.0)]),
BTreeMap::from([("routing_group".into(), f64::INFINITY)]),
BTreeMap::from([
("routing_group".into(), f64::MAX),
("user_group".into(), 2.0),
]),
] {
assert!(BillingMultiplierSnapshot::from_factors(factors).is_err());
}
let zero = BillingMultiplierSnapshot::from_factors(BTreeMap::from([
("routing_group".into(), 0.0),
("user_group".into(), 2.0),
]))
.unwrap();
assert_eq!(zero.cost(10.0).unwrap(), 0.0);
for invalid in [
Value::Null,
json!({"version": 2, "factors": {}, "multiplier": 1}),
json!({"version": 1, "factors": {"routing_group": 2}, "multiplier": 1}),
] {
assert!(billing_multiplier_snapshot(Some(
&json!({"billing_multiplier_snapshot": invalid})
))
.is_err());
}
let doubled = BillingMultiplierSnapshot::from_factors(BTreeMap::from([(
"routing_group".into(),
2.0,
)]))
.unwrap();
assert!(doubled.cost(f64::MAX).is_err());
}
}
@@ -8,14 +8,17 @@ use serde_json::{Map, Value};
use crate::repository::candidates::sanitize_request_candidate_skip_reason;
use super::{
normalize_provider_response_model, LIVE_SESSION_METADATA_KEY,
billing_multiplier_snapshot, normalize_provider_response_model,
BILLING_MULTIPLIER_SNAPSHOT_METADATA_KEY, LIVE_SESSION_METADATA_KEY,
PLAN_USAGE_RESERVATION_DEFERRED_METADATA_KEY, PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY,
PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY, PROVIDER_REASONING_EFFORT_METADATA_KEY,
PROVIDER_RESPONSE_MODEL_METADATA_KEY, PROVIDER_SERVICE_TIER_METADATA_KEY,
REALTIME_SESSION_METADATA_KEY, REQUESTED_REASONING_EFFORT_METADATA_KEY,
ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY, ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY,
USAGE_AVAILABLE_METADATA_KEY, USAGE_PRICING_AVAILABLE_METADATA_KEY,
WEBSOCKET_MODE_METADATA_KEY, WEBSOCKET_TRANSPORT_METADATA_KEY,
ROUTING_GROUP_BILLING_MULTIPLIER_METADATA_KEY, ROUTING_GROUP_ID_METADATA_KEY,
ROUTING_GROUP_NAME_METADATA_KEY, USAGE_AVAILABLE_METADATA_KEY,
USAGE_PRICING_AVAILABLE_METADATA_KEY, WEBSOCKET_MODE_METADATA_KEY,
WEBSOCKET_TRANSPORT_METADATA_KEY,
};
const UPSTREAM_IS_STREAM_KEY: &str = "upstream_is_stream";
@@ -43,8 +46,68 @@ pub fn sanitize_usage_request_metadata_ref(value: Option<&Value>) -> Option<Valu
sanitize_usage_request_metadata_object(value?.as_object()?)
}
/// Keep the request's first captured billing snapshot and reservation owner across retries.
pub fn preserve_usage_routing_group_snapshot(
incoming: Option<Value>,
previous: Option<&Value>,
) -> Option<Value> {
let Some(previous) = previous.and_then(Value::as_object) else {
return incoming;
};
let mut snapshot = Map::from_iter(
[
BILLING_MULTIPLIER_SNAPSHOT_METADATA_KEY,
ROUTING_GROUP_BILLING_MULTIPLIER_METADATA_KEY,
ROUTING_GROUP_ID_METADATA_KEY,
ROUTING_GROUP_NAME_METADATA_KEY,
PLAN_USAGE_RESERVATION_TOKEN_KEY,
]
.into_iter()
.filter_map(|key| {
previous
.get(key)
.map(|value| (key.to_string(), value.clone()))
}),
);
if !snapshot.contains_key(BILLING_MULTIPLIER_SNAPSHOT_METADATA_KEY)
&& snapshot.contains_key(ROUTING_GROUP_BILLING_MULTIPLIER_METADATA_KEY)
{
let captured = billing_multiplier_snapshot(Some(&Value::Object(snapshot.clone())))
.ok()
.flatten()
.and_then(|snapshot| serde_json::to_value(snapshot).ok())
.unwrap_or(Value::Null);
snapshot.insert(
BILLING_MULTIPLIER_SNAPSHOT_METADATA_KEY.to_string(),
captured,
);
}
let Some(Value::Object(snapshot)) = sanitize_usage_request_metadata_object(&snapshot) else {
return incoming;
};
let mut metadata = incoming
.and_then(|value| value.as_object().cloned())
.unwrap_or_default();
metadata.extend(snapshot);
Some(Value::Object(metadata))
}
pub fn sanitize_usage_request_metadata_object(source: &Map<String, Value>) -> Option<Value> {
let mut target = Map::new();
if let Some(snapshot) = source.get(BILLING_MULTIPLIER_SNAPSHOT_METADATA_KEY) {
let metadata = serde_json::json!({BILLING_MULTIPLIER_SNAPSHOT_METADATA_KEY: snapshot});
let snapshot = match billing_multiplier_snapshot(Some(&metadata)) {
Ok(Some(snapshot)) => serde_json::to_value(snapshot)
.expect("validated billing multiplier snapshot must serialize"),
// Preserve an invalid marker so malformed financial input cannot silently
// fall back to legacy billing after metadata projection.
_ => Value::Null,
};
target.insert(
BILLING_MULTIPLIER_SNAPSHOT_METADATA_KEY.to_string(),
snapshot,
);
}
if let Some(source) = source
.get("analytics_measurement")
.and_then(|value| value.get("source"))
@@ -81,6 +144,8 @@ pub fn sanitize_usage_request_metadata_object(source: &Map<String, Value>) -> Op
}
insert_token(source, &mut target, "trace_id", 128);
insert_token(source, &mut target, ROUTING_GROUP_ID_METADATA_KEY, 128);
insert_bounded_text(source, &mut target, ROUTING_GROUP_NAME_METADATA_KEY, 256);
insert_ip_address(source, &mut target, "client_ip");
insert_client_family(source, &mut target);
for key in [
@@ -181,6 +246,7 @@ pub fn sanitize_usage_request_metadata_object(source: &Map<String, Value>) -> Op
for key in [
"rate_multiplier",
ROUTING_GROUP_BILLING_MULTIPLIER_METADATA_KEY,
"input_price_per_1m",
"output_price_per_1m",
"cache_creation_price_per_1m",
@@ -189,6 +255,20 @@ pub fn sanitize_usage_request_metadata_object(source: &Map<String, Value>) -> Op
] {
insert_nonnegative_number(source, &mut target, key);
}
// An invalid legacy routing factor must remain a financial tombstone. Dropping it
// would make a subsequent reader silently fall back to the historical provider charge.
if source
.get(ROUTING_GROUP_BILLING_MULTIPLIER_METADATA_KEY)
.is_some_and(|value| {
!value
.as_f64()
.is_some_and(|value| value.is_finite() && value >= 0.0)
})
{
target
.entry(BILLING_MULTIPLIER_SNAPSHOT_METADATA_KEY.to_string())
.or_insert(Value::Null);
}
let billing_snapshot = source
.get("billing_snapshot")
@@ -1156,6 +1236,27 @@ fn insert_token(
target.insert(key.to_string(), Value::String(value.to_string()));
}
fn insert_bounded_text(
source: &Map<String, Value>,
target: &mut Map<String, Value>,
key: &str,
max_len: usize,
) {
let Some(value) = source
.get(key)
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| {
!value.is_empty()
&& value.chars().count() <= max_len
&& !value.chars().any(char::is_control)
})
else {
return;
};
target.insert(key.to_string(), Value::String(value.to_string()));
}
fn insert_dimension_token(source: &Map<String, Value>, target: &mut Map<String, Value>, key: &str) {
let Some(value) = source
.get(key)
@@ -1263,7 +1364,111 @@ fn safe_version_value(value: &Value) -> Option<String> {
mod tests {
use serde_json::json;
use super::{sanitize_usage_request_metadata, sanitize_usage_request_metadata_ref};
use super::{
billing_multiplier_snapshot, preserve_usage_routing_group_snapshot,
sanitize_usage_request_metadata, sanitize_usage_request_metadata_ref,
};
#[test]
fn billing_multiplier_snapshot_projection_preserves_invalid_marker_and_immutable_factors() {
for snapshot in [
serde_json::Value::Null,
json!({"version": 1, "factors": {"routing_group": 2.0}, "multiplier": 1.0}),
json!({"version": 99, "factors": {}, "multiplier": 1.0}),
] {
let projected = sanitize_usage_request_metadata(Some(json!({
"billing_multiplier_snapshot": snapshot,
"routing_group_billing_multiplier": 0.5,
})))
.unwrap();
assert_eq!(
projected.get("billing_multiplier_snapshot"),
Some(&serde_json::Value::Null)
);
assert!(billing_multiplier_snapshot(Some(&projected)).is_err());
let preserved = preserve_usage_routing_group_snapshot(
Some(json!({"billing_multiplier_snapshot": {"version": 1, "factors": {}, "multiplier": 1.0}})),
Some(&projected),
).unwrap();
assert!(billing_multiplier_snapshot(Some(&preserved)).is_err());
}
let legacy =
json!({"routing_group_billing_multiplier": 0.25, "routing_group_name": "历史分组"});
let preserved = preserve_usage_routing_group_snapshot(
Some(json!({"billing_multiplier_snapshot": {"version": 1, "factors": {"routing_group": 99.0}, "multiplier": 99.0}})),
Some(&legacy),
).unwrap();
assert_eq!(
billing_multiplier_snapshot(Some(&preserved))
.unwrap()
.unwrap()
.multiplier(),
0.25
);
assert_eq!(preserved["routing_group_name"], "历史分组");
}
#[test]
fn billing_multiplier_snapshot_projection_rejects_malformed_legacy_factors() {
for factor in [serde_json::Value::Null, json!(-1), json!("2"), json!({})] {
let projected = sanitize_usage_request_metadata(Some(json!({
"routing_group_billing_multiplier": factor,
})))
.expect("invalid financial input must retain a tombstone");
assert_eq!(
projected["billing_multiplier_snapshot"],
serde_json::Value::Null
);
assert!(billing_multiplier_snapshot(Some(&projected)).is_err());
assert_eq!(
sanitize_usage_request_metadata(Some(projected.clone())),
Some(projected)
);
}
let generic = sanitize_usage_request_metadata(Some(json!({
"routing_group_billing_multiplier": -1,
"billing_multiplier_snapshot": {"version": 1, "factors": {"routing_group": 2}, "multiplier": 2},
})))
.unwrap();
assert_eq!(
billing_multiplier_snapshot(Some(&generic))
.unwrap()
.unwrap()
.multiplier(),
2.0
);
}
#[test]
fn billing_multiplier_snapshot_preserves_the_original_reservation_owner() {
let token_a = "550e8400-e29b-41d4-a716-446655440001";
let token_b = "550e8400-e29b-41d4-a716-446655440002";
let incoming = json!({
"plan_usage_reservation_token": token_b,
"billing_multiplier_snapshot": {"version": 1, "factors": {"routing_group": 3}, "multiplier": 3},
});
let previous = json!({
"plan_usage_reservation_token": token_a,
"billing_multiplier_snapshot": {"version": 1, "factors": {"routing_group": 0.5}, "multiplier": 0.5},
});
let preserved =
preserve_usage_routing_group_snapshot(Some(incoming.clone()), Some(&previous)).unwrap();
assert_eq!(preserved["plan_usage_reservation_token"], token_a);
assert_eq!(
billing_multiplier_snapshot(Some(&preserved))
.unwrap()
.unwrap()
.multiplier(),
0.5
);
for empty in [json!({}), json!({"plan_usage_reservation_token": " "})] {
let preserved =
preserve_usage_routing_group_snapshot(Some(incoming.clone()), Some(&empty))
.unwrap();
assert_eq!(preserved["plan_usage_reservation_token"], token_b);
}
}
#[test]
fn account_attribution_preserves_key_flag_without_custom_identity_or_purpose() {
@@ -2,6 +2,7 @@ mod analytics;
#[cfg(test)]
mod analytics_tests;
mod attribution;
mod billing_multiplier;
mod capture_memory;
mod compression;
mod dashboard_summary;
@@ -12,6 +13,7 @@ mod types;
pub use analytics::*;
pub use attribution::*;
pub use billing_multiplier::*;
#[doc(hidden)]
pub use capture_memory::{
mark_usage_capture_memory_omitted, usage_json_heap_estimate, UsageCaptureMemoryBudget,
@@ -60,6 +62,8 @@ pub use types::{
PROVIDER_RESPONSE_MODEL_METADATA_KEY, PROVIDER_SERVICE_TIER_METADATA_KEY,
REALTIME_SESSION_METADATA_KEY, REQUESTED_REASONING_EFFORT_METADATA_KEY,
ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY, ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY,
USAGE_AVAILABLE_METADATA_KEY, USAGE_PRICING_AVAILABLE_METADATA_KEY,
WEBSOCKET_MODE_METADATA_KEY, WEBSOCKET_TRANSPORT_METADATA_KEY,
ROUTING_GROUP_BILLING_MULTIPLIER_METADATA_KEY, ROUTING_GROUP_ID_METADATA_KEY,
ROUTING_GROUP_NAME_METADATA_KEY, USAGE_AVAILABLE_METADATA_KEY,
USAGE_PRICING_AVAILABLE_METADATA_KEY, WEBSOCKET_MODE_METADATA_KEY,
WEBSOCKET_TRANSPORT_METADATA_KEY,
};
@@ -11,6 +11,10 @@ pub const PROVIDER_RESPONSE_MODEL_METADATA_KEY: &str = "provider_response_model"
pub const PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY: &str = "provider_cache_ttl_minutes";
pub const ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY: &str = "routing_candidate_skip_reason";
pub const ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY: &str = "routing_failure_diagnostic";
/// Immutable routing-group multiplier captured when the request is planned.
pub const ROUTING_GROUP_BILLING_MULTIPLIER_METADATA_KEY: &str = "routing_group_billing_multiplier";
pub const ROUTING_GROUP_ID_METADATA_KEY: &str = "routing_group_id";
pub const ROUTING_GROUP_NAME_METADATA_KEY: &str = "routing_group_name";
pub const WEBSOCKET_MODE_METADATA_KEY: &str = "websocket_mode";
pub const WEBSOCKET_TRANSPORT_METADATA_KEY: &str = "websocket_transport";
pub const PLAN_USAGE_RESERVATION_DEFERRED_METADATA_KEY: &str = "plan_usage_reservation_deferred";
@@ -828,6 +832,47 @@ impl StoredRequestUsageAudit {
self.request_metadata_number("rate_multiplier")
}
/// Historical requests without a captured multiplier retain the original 1x rate.
pub fn routing_group_billing_multiplier(&self) -> f64 {
self.request_metadata_number(ROUTING_GROUP_BILLING_MULTIPLIER_METADATA_KEY)
.filter(|value| value.is_finite() && *value >= 0.0)
.unwrap_or(1.0)
}
/// Routing factor projection retained for callers inspecting this individual factor.
pub fn routing_group_billing_cost(&self) -> Option<f64> {
let cost = self.total_cost_usd * self.routing_group_billing_multiplier();
cost.is_finite().then_some(cost)
}
pub fn billing_multiplier(&self) -> f64 {
super::billing_multiplier_snapshot(self.request_metadata.as_ref())
.ok()
.flatten()
.map(|snapshot| snapshot.multiplier())
.unwrap_or(1.0)
}
/// Customer charge is independent of upstream Key cost. Legacy rows keep their
/// original charge; no current configuration is consulted for historical usage.
pub fn billing_cost(&self) -> Option<f64> {
match super::billing_multiplier_snapshot(self.request_metadata.as_ref()).ok()? {
Some(snapshot) => snapshot.cost(self.total_cost_usd).ok(),
None => self
.actual_total_cost_usd
.is_finite()
.then_some(self.actual_total_cost_usd.max(0.0)),
}
}
pub fn routing_group_id(&self) -> Option<&str> {
self.request_metadata_string(ROUTING_GROUP_ID_METADATA_KEY)
}
pub fn routing_group_name(&self) -> Option<&str> {
self.request_metadata_string(ROUTING_GROUP_NAME_METADATA_KEY)
}
pub fn settlement_is_free_tier(&self) -> Option<bool> {
self.request_metadata_bool("is_free_tier")
}
@@ -3407,6 +3452,40 @@ mod tests {
assert!(record.validate().is_err());
}
#[test]
fn routing_group_snapshot_defaults_legacy_multiplier_without_inventing_a_group() {
let mut usage = sample_usage();
usage.total_cost_usd = 4.0;
assert_eq!(usage.routing_group_billing_multiplier(), 1.0);
assert_eq!(usage.routing_group_billing_cost(), Some(4.0));
assert_eq!(usage.routing_group_id(), None);
assert_eq!(usage.routing_group_name(), None);
for (value, multiplier, cost) in [
(json!(0), 0.0, 0.0),
(json!(0.25), 0.25, 1.0),
(json!(2.5), 2.5, 10.0),
(json!(-2), 1.0, 4.0),
(json!("Infinity"), 1.0, 4.0),
(json!(f64::INFINITY), 1.0, 4.0),
(json!(f64::NAN), 1.0, 4.0),
] {
usage.request_metadata = Some(json!({
"routing_group_billing_multiplier": value,
"routing_group_id": "group-recorded",
"routing_group_name": "请求时的分组",
"rate_multiplier": 0.75
}));
assert_eq!(usage.routing_group_billing_multiplier(), multiplier);
assert_eq!(usage.routing_group_billing_cost(), Some(cost));
assert_eq!(usage.routing_group_id(), Some("group-recorded"));
assert_eq!(usage.routing_group_name(), Some("请求时的分组"));
assert_eq!(usage.settlement_rate_multiplier(), Some(0.75));
}
usage.request_metadata = Some(json!({"routing_group_billing_multiplier": 2.0}));
usage.total_cost_usd = f64::MAX;
assert_eq!(usage.routing_group_billing_cost(), None);
}
#[test]
fn settlement_accessors_prefer_typed_metadata() {
let mut usage = sample_usage();
@@ -3286,6 +3286,7 @@ mod tests {
billing_status: "pending".to_string(),
total_cost_usd: 0.1,
actual_total_cost_usd: 0.1,
billing_cost_usd: None,
finalized_at_unix_secs: None,
};
assert!(input.validate().is_err());