use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::enums::Unit;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Amount {
pub unit: Unit,
pub amount: i64,
}
impl Amount {
pub fn usd_microcents(amount: i64) -> Self {
Self {
unit: Unit::UsdMicrocents,
amount,
}
}
pub fn tokens(amount: i64) -> Self {
Self {
unit: Unit::Tokens,
amount,
}
}
pub fn credits(amount: i64) -> Self {
Self {
unit: Unit::Credits,
amount,
}
}
pub fn risk_points(amount: i64) -> Self {
Self {
unit: Unit::RiskPoints,
amount,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedAmount {
pub unit: Unit,
pub amount: i64,
}
impl SignedAmount {
pub fn usd_microcents(amount: i64) -> Self {
Self {
unit: Unit::UsdMicrocents,
amount,
}
}
pub fn tokens(amount: i64) -> Self {
Self {
unit: Unit::Tokens,
amount,
}
}
pub fn credits(amount: i64) -> Self {
Self {
unit: Unit::Credits,
amount,
}
}
pub fn risk_points(amount: i64) -> Self {
Self {
unit: Unit::RiskPoints,
amount,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Subject {
#[serde(skip_serializing_if = "Option::is_none")]
pub tenant: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub app: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workflow: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub toolset: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dimensions: Option<HashMap<String, String>>,
}
impl Subject {
pub fn has_field(&self) -> bool {
self.tenant.is_some()
|| self.workspace.is_some()
|| self.app.is_some()
|| self.workflow.is_some()
|| self.agent.is_some()
|| self.toolset.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Action {
pub kind: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
}
impl Action {
pub fn new(kind: impl Into<String>, name: impl Into<String>) -> Self {
Self {
kind: kind.into(),
name: name.into(),
tags: None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Caps {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_steps_remaining: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_allowlist: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_denylist: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cooldown_ms: Option<i64>,
}
impl Caps {
pub fn is_tool_allowed(&self, tool: &str) -> bool {
if let Some(ref allowlist) = self.tool_allowlist {
if !allowlist.is_empty() {
return allowlist.iter().any(|t| t == tool);
}
}
if let Some(ref denylist) = self.tool_denylist {
if !denylist.is_empty() {
return !denylist.iter().any(|t| t == tool);
}
}
true
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct CyclesMetrics {
#[serde(skip_serializing_if = "Option::is_none")]
pub tokens_input: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tokens_output: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub latency_ms: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Balance {
pub scope: String,
pub scope_path: String,
pub remaining: SignedAmount,
#[serde(skip_serializing_if = "Option::is_none")]
pub reserved: Option<Amount>,
#[serde(skip_serializing_if = "Option::is_none")]
pub spent: Option<Amount>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allocated: Option<Amount>,
#[serde(skip_serializing_if = "Option::is_none")]
pub debt: Option<Amount>,
#[serde(skip_serializing_if = "Option::is_none")]
pub overdraft_limit: Option<Amount>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_over_limit: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn amount_constructors() {
let a = Amount::usd_microcents(5000);
assert_eq!(a.unit, Unit::UsdMicrocents);
assert_eq!(a.amount, 5000);
let b = Amount::tokens(100);
assert_eq!(b.unit, Unit::Tokens);
assert_eq!(b.amount, 100);
let c = Amount::credits(50);
assert_eq!(c.unit, Unit::Credits);
assert_eq!(c.amount, 50);
let d = Amount::risk_points(75);
assert_eq!(d.unit, Unit::RiskPoints);
assert_eq!(d.amount, 75);
}
#[test]
fn signed_amount_constructors() {
let a = SignedAmount::usd_microcents(-500);
assert_eq!(a.unit, Unit::UsdMicrocents);
assert_eq!(a.amount, -500);
let b = SignedAmount::tokens(200);
assert_eq!(b.unit, Unit::Tokens);
assert_eq!(b.amount, 200);
let c = SignedAmount::credits(-10);
assert_eq!(c.unit, Unit::Credits);
assert_eq!(c.amount, -10);
let d = SignedAmount::risk_points(30);
assert_eq!(d.unit, Unit::RiskPoints);
assert_eq!(d.amount, 30);
}
#[test]
fn subject_has_field() {
let empty = Subject::default();
assert!(!empty.has_field());
let with_tenant = Subject {
tenant: Some("acme".to_string()),
..Default::default()
};
assert!(with_tenant.has_field());
}
#[test]
fn caps_tool_allowed() {
let caps = Caps {
tool_allowlist: Some(vec!["web_search".to_string()]),
..Default::default()
};
assert!(caps.is_tool_allowed("web_search"));
assert!(!caps.is_tool_allowed("code_exec"));
let caps_deny = Caps {
tool_denylist: Some(vec!["dangerous".to_string()]),
..Default::default()
};
assert!(caps_deny.is_tool_allowed("web_search"));
assert!(!caps_deny.is_tool_allowed("dangerous"));
let caps_empty = Caps::default();
assert!(caps_empty.is_tool_allowed("anything"));
}
#[test]
fn amount_serde_roundtrip() {
let a = Amount::usd_microcents(5000);
let json = serde_json::to_string(&a).unwrap();
assert!(json.contains("\"USD_MICROCENTS\""));
assert!(json.contains("5000"));
let b: Amount = serde_json::from_str(&json).unwrap();
assert_eq!(a, b);
}
#[test]
fn subject_serde_skips_none() {
let s = Subject {
tenant: Some("acme".to_string()),
..Default::default()
};
let json = serde_json::to_string(&s).unwrap();
assert!(json.contains("\"tenant\""));
assert!(!json.contains("\"workspace\""));
}
#[test]
fn action_new() {
let a = Action::new("llm.completion", "gpt-4o");
assert_eq!(a.kind, "llm.completion");
assert_eq!(a.name, "gpt-4o");
assert!(a.tags.is_none());
}
}