use crate::api::generated::Client;
use crate::api::generated::types::{
AgentClaimAmount as ClaimAmount, AgentClaimAmount, AgentPaymentClaimRequest, McpToolName,
};
use crate::error::Error;
use rig::completion::Usage;
use std::collections::HashMap;
use std::ops::{Div, Mul};
use tracing::{info, warn};
const MICRO_CORAL_TO_CORAL: f64 = 1_000_000.0;
pub struct ClaimManager {
input_token_cost: ClaimAmount,
output_token_cost: ClaimAmount,
min_budget: ClaimAmount,
base_tool_call_cost: ClaimAmount,
custom_tool_cost: HashMap<String, ClaimAmount>,
base_iteration_cost: ClaimAmount,
base_tool_iteration_cost: ClaimAmount,
exit_on_budget_exhausted: bool,
api_url: String,
remote_session_id: String,
}
impl ClaimManager {
pub fn new() -> Self {
Self {
input_token_cost: ClaimAmount::MicroCoral(0),
output_token_cost: ClaimAmount::MicroCoral(0),
min_budget: ClaimAmount::MicroCoral(0),
base_tool_call_cost: ClaimAmount::MicroCoral(0),
custom_tool_cost: HashMap::new(),
base_iteration_cost: ClaimAmount::MicroCoral(0),
base_tool_iteration_cost: ClaimAmount::MicroCoral(0),
exit_on_budget_exhausted: true,
api_url: std::env::var("CORAL_API_URL").expect("CORAL_API_URL not set"),
remote_session_id: std::env::var("CORAL_SESSION_ID").expect("CORAL_SESSION_ID not set"),
}
}
pub fn input_token_cost(mut self, input_token_cost: ClaimAmount) -> Self {
self.input_token_cost = input_token_cost;
self
}
pub fn mil_input_token_cost(mut self, input_token_cost: ClaimAmount) -> Self {
self.input_token_cost = input_token_cost.div(1_000_000);
self
}
pub fn output_token_cost(mut self, output_token_cost: ClaimAmount) -> Self {
self.output_token_cost = output_token_cost;
self
}
pub fn mil_output_token_cost(mut self, output_token_cost: ClaimAmount) -> Self {
self.output_token_cost = output_token_cost.div(1_000_000);
self
}
pub fn min_budget(mut self, min_budget: ClaimAmount) -> Self {
self.min_budget = min_budget;
self
}
pub fn base_tool_call_cost(mut self, base_tool_call_cost: ClaimAmount) -> Self {
self.base_tool_call_cost = base_tool_call_cost;
self
}
pub fn base_iteration_cost(mut self, base_iteration_cost: ClaimAmount) -> Self {
self.base_iteration_cost = base_iteration_cost;
self
}
pub fn base_tool_iteration_cost(mut self, base_tool_iteration_cost: ClaimAmount) -> Self {
self.base_tool_iteration_cost = base_tool_iteration_cost;
self
}
pub fn exit_on_budget_exhausted(mut self, exit_on_budget_exhausted: bool) -> Self {
self.exit_on_budget_exhausted = exit_on_budget_exhausted;
self
}
pub fn custom_tool_cost(mut self, tool_name: impl Into<String>, cost: ClaimAmount) -> Self {
self.custom_tool_cost.insert(tool_name.into(), cost);
self
}
pub fn coral_custom_tool_cost(mut self, tool_name: McpToolName, cost: ClaimAmount) -> Self {
self.custom_tool_cost.insert(tool_name.to_string(), cost);
self
}
pub(crate) async fn claim_tokens(&self, usage: &Usage) -> Result<(), Error> {
if self.input_token_cost.is_zero() && self.output_token_cost.is_zero() {
info!("not claiming tokens because input_token_cost and output_token_cost are zero");
return Ok(());
}
if usage.input_tokens + usage.output_tokens != usage.total_tokens {
if !self.input_token_cost.is_zero() {
warn!(
"provider only reported total token usage, input_token_cost will be ignored! token cost will be claimed used output_token_cost"
)
}
info!(
"claiming {} for {} tokens",
self.output_token_cost, usage.total_tokens
);
return self
.claim(self.output_token_cost.clone().mul(usage.total_tokens))
.await;
} else if usage.total_tokens == 0 {
warn!("provider reported zero tokens!");
} else {
info!(
"claiming {} for {} input tokens",
self.input_token_cost, usage.input_tokens
);
self.claim(self.input_token_cost.clone().mul(usage.input_tokens))
.await?;
info!(
"claiming {} for {} output tokens",
self.output_token_cost, usage.output_tokens
);
self.claim(self.output_token_cost.clone().mul(usage.output_tokens))
.await?;
}
Ok(())
}
pub(crate) async fn claim_iteration(&self) -> Result<(), Error> {
if !self.base_iteration_cost.is_zero() {
info!(
"claiming {} for one prompt iteration",
self.base_iteration_cost
);
self.claim(self.base_iteration_cost.clone()).await
} else {
info!("not claiming prompt iteration because base_iteration_cost is zero");
Ok(())
}
}
pub(crate) async fn claim_tool_iteration(&self) -> Result<(), Error> {
if !self.base_tool_iteration_cost.is_zero() {
info!(
"claiming {} for one tool iteration",
self.base_tool_iteration_cost
);
self.claim(self.base_tool_iteration_cost.clone()).await
} else {
info!("not claiming tool iteration because base_tool_iteration_cost is zero");
Ok(())
}
}
pub(crate) async fn claim_tool_call(&self, tool_name: impl Into<String>) -> Result<(), Error> {
let name = tool_name.into();
if !self.base_tool_call_cost.is_zero() {
self.claim(self.base_tool_call_cost.clone()).await?;
info!(
"claiming {} as a base cost for tool '{name}'",
self.base_tool_call_cost
);
}
if let Some(cost) = self.custom_tool_cost.get(name.as_str()) {
info!("claiming {cost} as an additional cost for tool '{name}'");
self.claim(cost.clone()).await?;
self.claim(cost.clone()).await?;
}
Ok(())
}
async fn claim(&self, amount: ClaimAmount) -> Result<(), Error> {
if std::env::var("CORAL_SEND_CLAIMS") != Ok("1".to_string()) {
return Ok(());
}
if amount.is_zero() {
return Ok(());
}
let budget = Client::new(self.api_url.as_str())
.submit_rental_claim(
self.remote_session_id.as_str(),
&AgentPaymentClaimRequest { amount },
)
.await
.map_err(Error::ApiError)?
.into_inner();
if self.exit_on_budget_exhausted {
let min_micro = match self.min_budget {
AgentClaimAmount::Coral(coral) => (coral * MICRO_CORAL_TO_CORAL) as i64,
AgentClaimAmount::MicroCoral(micro) => micro,
AgentClaimAmount::Usd(usd) => {
((usd / budget.coral_usd_price) * MICRO_CORAL_TO_CORAL) as i64
}
};
if budget.remaining_budget <= min_micro {
return Err(Error::BudgetExhausted);
}
}
Ok(())
}
}