Skip to main content

made_core/value_objects/budget/
budget_limits.rs

1use super::{
2    BudgetQuantities, BudgetTokenCount, CostMicros, CurrencyCode, ExecutionDuration, ToolCallCount,
3};
4use crate::DomainError;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct BudgetLimits {
9    duration: Option<ExecutionDuration>,
10    tokens: Option<BudgetTokenCount>,
11    cost: Option<CostMicros>,
12    tool_calls: Option<ToolCallCount>,
13    currency: Option<CurrencyCode>,
14}
15impl BudgetLimits {
16    /// Preserve presence at public boundaries: a supplied ceiling must be positive.
17    /// Only absence means that the dimension has no ceiling.
18    pub fn from_optional(
19        duration: Option<ExecutionDuration>,
20        tokens: Option<BudgetTokenCount>,
21        cost: Option<CostMicros>,
22        tool_calls: Option<ToolCallCount>,
23        currency: Option<CurrencyCode>,
24    ) -> Result<Self, DomainError> {
25        for (field, amount) in [
26            (
27                "budget_limits.duration_micros",
28                duration.map(ExecutionDuration::as_micros),
29            ),
30            ("budget_limits.tokens", tokens.map(BudgetTokenCount::value)),
31            ("budget_limits.cost_micros", cost.map(CostMicros::value)),
32            (
33                "budget_limits.tool_calls",
34                tool_calls.map(ToolCallCount::value),
35            ),
36        ] {
37            if amount == Some(0) {
38                return Err(DomainError::MustBeNonZero { field });
39            }
40        }
41        Self::new(
42            BudgetQuantities::new(
43                duration.unwrap_or_else(|| ExecutionDuration::from_micros(0)),
44                tokens.unwrap_or_else(|| BudgetTokenCount::new(0)),
45                cost.unwrap_or_else(|| CostMicros::new(0)),
46                tool_calls.unwrap_or_else(|| ToolCallCount::new(0)),
47            ),
48            currency,
49        )
50    }
51
52    pub fn new(
53        maximum: BudgetQuantities,
54        currency: Option<CurrencyCode>,
55    ) -> Result<Self, DomainError> {
56        if maximum.is_zero() {
57            return Err(DomainError::EmptyCollection {
58                field: "budget_limits",
59            });
60        }
61        if (maximum.cost().value() > 0) != currency.is_some() {
62            return Err(DomainError::InvariantViolated {
63                reason: "a cost budget and its currency must be declared together",
64            });
65        }
66        Ok(Self {
67            duration: (maximum.duration().as_micros() > 0).then_some(maximum.duration()),
68            tokens: (maximum.tokens().value() > 0).then_some(maximum.tokens()),
69            cost: (maximum.cost().value() > 0).then_some(maximum.cost()),
70            tool_calls: (maximum.tool_calls().value() > 0).then_some(maximum.tool_calls()),
71            currency,
72        })
73    }
74    #[must_use]
75    pub const fn duration(&self) -> Option<ExecutionDuration> {
76        self.duration
77    }
78    #[must_use]
79    pub const fn tokens(&self) -> Option<BudgetTokenCount> {
80        self.tokens
81    }
82    #[must_use]
83    pub const fn cost(&self) -> Option<CostMicros> {
84        self.cost
85    }
86    #[must_use]
87    pub const fn tool_calls(&self) -> Option<ToolCallCount> {
88        self.tool_calls
89    }
90    #[must_use]
91    pub const fn currency(&self) -> Option<&CurrencyCode> {
92        self.currency.as_ref()
93    }
94}