use serde::{Deserialize, Serialize};
use crate::client::Client;
use crate::error::Result;
use super::usage_report::{ContextWindow, ServiceTier};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum CostType {
Tokens,
WebSearch,
CodeExecution,
SessionUsage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum TokenType {
#[serde(rename = "uncached_input_tokens")]
UncachedInput,
#[serde(rename = "output_tokens")]
Output,
#[serde(rename = "cache_read_input_tokens")]
CacheRead,
#[serde(rename = "cache_creation.ephemeral_1h_input_tokens")]
CacheCreate1h,
#[serde(rename = "cache_creation.ephemeral_5m_input_tokens")]
CacheCreate5m,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum CostGroupBy {
WorkspaceId,
Description,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CostBucketWidth {
#[serde(rename = "1d")]
Day,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CostRow {
pub amount: String,
pub currency: String,
#[serde(default)]
pub cost_type: Option<CostType>,
#[serde(default)]
pub token_type: Option<TokenType>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub workspace_id: Option<String>,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub context_window: Option<ContextWindow>,
#[serde(default)]
pub service_tier: Option<ServiceTier>,
#[serde(default)]
pub inference_geo: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CostBucket {
pub starting_at: String,
pub ending_at: String,
pub results: Vec<CostRow>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CostReport {
pub data: Vec<CostBucket>,
#[serde(default)]
pub has_more: bool,
#[serde(default)]
pub next_page: Option<String>,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct CostReportParams {
pub starting_at: String,
pub ending_at: Option<String>,
pub bucket_width: Option<CostBucketWidth>,
pub group_by: Vec<CostGroupBy>,
pub limit: Option<u32>,
pub page: Option<String>,
}
impl CostReportParams {
#[must_use]
pub fn starting_at(starting_at: impl Into<String>) -> Self {
Self {
starting_at: starting_at.into(),
..Self::default()
}
}
fn to_query(&self) -> Vec<(&'static str, String)> {
let mut q = Vec::new();
q.push(("starting_at", self.starting_at.clone()));
if let Some(e) = &self.ending_at {
q.push(("ending_at", e.clone()));
}
if let Some(_b) = self.bucket_width {
q.push(("bucket_width", "1d".into()));
}
for g in &self.group_by {
let s = match g {
CostGroupBy::WorkspaceId => "workspace_id",
CostGroupBy::Description => "description",
};
q.push(("group_by[]", s.into()));
}
if let Some(l) = self.limit {
q.push(("limit", l.to_string()));
}
if let Some(p) = &self.page {
q.push(("page", p.clone()));
}
q
}
}
pub struct Cost<'a> {
client: &'a Client,
}
impl<'a> Cost<'a> {
pub(crate) fn new(client: &'a Client) -> Self {
Self { client }
}
pub async fn report(&self, params: CostReportParams) -> Result<CostReport> {
let query = params.to_query();
self.client
.execute_with_retry(
|| {
let mut req = self
.client
.request_builder(reqwest::Method::GET, "/v1/organizations/cost_report");
for (k, v) in &query {
req = req.query(&[(k, v)]);
}
req
},
&[],
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn client_for(mock: &MockServer) -> Client {
Client::builder()
.api_key("sk-ant-admin-test")
.base_url(mock.uri())
.build()
.unwrap()
}
#[tokio::test]
async fn cost_report_groups_by_description_and_decodes_token_type() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/organizations/cost_report"))
.and(wiremock::matchers::query_param(
"starting_at",
"2026-05-01T00:00:00Z",
))
.and(wiremock::matchers::query_param("group_by[]", "description"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [{
"starting_at": "2026-05-01T00:00:00Z",
"ending_at": "2026-05-02T00:00:00Z",
"results": [{
"amount": "123.45",
"currency": "USD",
"cost_type": "tokens",
"token_type": "uncached_input_tokens",
"description": "Sonnet 4.6 input",
"model": "claude-sonnet-4-6"
}]
}],
"has_more": false,
"next_page": null
})))
.mount(&mock)
.await;
let client = client_for(&mock);
let r = client
.admin()
.cost()
.report(CostReportParams {
starting_at: "2026-05-01T00:00:00Z".into(),
group_by: vec![CostGroupBy::Description],
..Default::default()
})
.await
.unwrap();
assert_eq!(r.data.len(), 1);
let row = &r.data[0].results[0];
assert_eq!(row.amount, "123.45");
assert_eq!(row.cost_type, Some(CostType::Tokens));
assert_eq!(row.token_type, Some(TokenType::UncachedInput));
}
}