use serde::{Deserialize, Serialize};
use crate::hysteresis::HysteresisGate;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EconomicMode {
#[default]
Sovereign,
Conserving,
Hustle,
Hibernate,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ModelTier {
Flagship,
#[default]
Standard,
Budget,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EconomicState {
pub identity_address: Option<String>,
pub balance_micro_credits: i64,
pub lifetime_revenue: i64,
pub lifetime_costs: i64,
pub monthly_burn_estimate: i64,
pub mode: EconomicMode,
pub cost_last_5min: i64,
pub last_cost_event_ms: u64,
pub mode_gate: HysteresisGate,
}
impl Default for EconomicState {
fn default() -> Self {
Self {
identity_address: None,
balance_micro_credits: 10_000_000, lifetime_revenue: 0,
lifetime_costs: 0,
monthly_burn_estimate: 0,
mode: EconomicMode::Sovereign,
cost_last_5min: 0,
last_cost_event_ms: 0,
mode_gate: HysteresisGate::new(0.7, 0.3, 30_000),
}
}
}
impl EconomicState {
pub fn balance_to_burn_ratio(&self) -> f64 {
if self.monthly_burn_estimate <= 0 {
return f64::INFINITY;
}
self.balance_micro_credits as f64 / self.monthly_burn_estimate as f64
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelCostRates {
pub input_per_token: i64,
pub output_per_token: i64,
}
impl Default for ModelCostRates {
fn default() -> Self {
Self {
input_per_token: 3, output_per_token: 15, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CostReason {
ModelInference {
model: String,
prompt_tokens: u32,
completion_tokens: u32,
},
ToolExecution { tool_name: String },
Storage { bytes: u64 },
Adjustment { description: String },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn economic_mode_serde_roundtrip() {
for mode in [
EconomicMode::Sovereign,
EconomicMode::Conserving,
EconomicMode::Hustle,
EconomicMode::Hibernate,
] {
let json = serde_json::to_string(&mode).unwrap();
let back: EconomicMode = serde_json::from_str(&json).unwrap();
assert_eq!(mode, back);
}
}
#[test]
fn model_tier_serde_roundtrip() {
for tier in [ModelTier::Flagship, ModelTier::Standard, ModelTier::Budget] {
let json = serde_json::to_string(&tier).unwrap();
let back: ModelTier = serde_json::from_str(&json).unwrap();
assert_eq!(tier, back);
}
}
#[test]
fn economic_state_default() {
let state = EconomicState::default();
assert_eq!(state.balance_micro_credits, 10_000_000);
assert_eq!(state.mode, EconomicMode::Sovereign);
assert_eq!(state.lifetime_costs, 0);
}
#[test]
fn balance_to_burn_ratio_zero_burn() {
let state = EconomicState::default();
assert!(state.balance_to_burn_ratio().is_infinite());
}
#[test]
fn balance_to_burn_ratio_normal() {
let state = EconomicState {
balance_micro_credits: 2_000_000,
monthly_burn_estimate: 1_000_000,
..Default::default()
};
assert!((state.balance_to_burn_ratio() - 2.0).abs() < f64::EPSILON);
}
#[test]
fn cost_reason_serde_roundtrip() {
let reason = CostReason::ModelInference {
model: "claude-sonnet".into(),
prompt_tokens: 100,
completion_tokens: 50,
};
let json = serde_json::to_string(&reason).unwrap();
let back: CostReason = serde_json::from_str(&json).unwrap();
assert!(matches!(back, CostReason::ModelInference { .. }));
}
}