Skip to main content

hanzo_client/apis/
usage_api.rs

1/*
2 * Hanzo Cloud API
3 *
4 * The Hanzo Cloud API as a customer calls it: every operation under /v1/ except the operator's admin product, relay routes, legacy spellings and capabilities still reached by flag. Tagged by product: the first path segment after /v1/.
5 *
6 * The version of the OpenAPI document: v1
7 * 
8 * Generated by: https://openapi-generator.tech
9 */
10
11
12use reqwest;
13use serde::{Deserialize, Serialize, de::Error as _};
14use crate::{apis::ResponseContent, models};
15use super::{Error, configuration, ContentType};
16
17
18/// struct for typed errors of method [`get_usage_analytics`]
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(untagged)]
21pub enum GetUsageAnalyticsError {
22    UnknownValue(serde_json::Value),
23}
24
25/// struct for typed errors of method [`get_usage_analytics_access`]
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(untagged)]
28pub enum GetUsageAnalyticsAccessError {
29    UnknownValue(serde_json::Value),
30}
31
32/// struct for typed errors of method [`get_usage_samples`]
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(untagged)]
35pub enum GetUsageSamplesError {
36    UnknownValue(serde_json::Value),
37}
38
39/// struct for typed errors of method [`get_usage_summary`]
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(untagged)]
42pub enum GetUsageSummaryError {
43    UnknownValue(serde_json::Value),
44}
45
46/// struct for typed errors of method [`post_usage`]
47#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(untagged)]
49pub enum PostUsageError {
50    UnknownValue(serde_json::Value),
51}
52
53
54/// Is the entitlement-GATED per-provider breakdown of the caller org's LLM usage — the paid lens over the same warehouse ledger GET /v1/usage/summary reads its totals from. Basic own-org usage stays ungated at /v1/usage/summary.  A plan that does not grant the analytics datastore is refused with 402, and an unresolvable plan fails closed to the free floor, which does not grant it. The window is clamped forward to the plan's retention entitlement, so a tenant can never read older than its plan allows even with a custom start. The response is marked no-store.  INTERIM (mirrors apps/world's limits echo): no org→plan resolver exists in cloud yet — the subscription lookup is owned by the billing plane and the gateway principal carries no plan claim — so the caller passes the plan and the gate resolves THAT plan's access.
55pub async fn get_usage_analytics(configuration: &configuration::Configuration, end: Option<&str>, plan: Option<&str>, range: Option<&str>, start: Option<&str>) -> Result<models::UsageAnalyticsView, Error<GetUsageAnalyticsError>> {
56    // add a prefix to parameters to efficiently prevent name collisions
57    let p_end = end;
58    let p_plan = plan;
59    let p_range = range;
60    let p_start = start;
61
62    let uri_str = format!("{}/v1/usage/analytics", configuration.base_path);
63    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
64
65    if let Some(ref param_value) = p_end {
66        req_builder = req_builder.query(&[("end", &param_value.to_string())]);
67    }
68    if let Some(ref param_value) = p_plan {
69        req_builder = req_builder.query(&[("plan", &param_value.to_string())]);
70    }
71    if let Some(ref param_value) = p_range {
72        req_builder = req_builder.query(&[("range", &param_value.to_string())]);
73    }
74    if let Some(ref param_value) = p_start {
75        req_builder = req_builder.query(&[("start", &param_value.to_string())]);
76    }
77    if let Some(ref user_agent) = configuration.user_agent {
78        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
79    }
80    if let Some(ref token) = configuration.bearer_access_token {
81        req_builder = req_builder.bearer_auth(token.to_owned());
82    };
83
84    let req = req_builder.build()?;
85    let resp = configuration.client.execute(req).await?;
86
87    let status = resp.status();
88    let content_type = resp
89        .headers()
90        .get("content-type")
91        .and_then(|v| v.to_str().ok())
92        .unwrap_or("application/octet-stream");
93    let content_type = super::ContentType::from(content_type);
94
95    if !status.is_client_error() && !status.is_server_error() {
96        let content = resp.text().await?;
97        match content_type {
98            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
99            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::UsageAnalyticsView`"))),
100            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::UsageAnalyticsView`")))),
101        }
102    } else {
103        let content = resp.text().await?;
104        let entity: Option<GetUsageAnalyticsError> = serde_json::from_str(&content).ok();
105        Err(Error::ResponseError(ResponseContent { status, content, entity }))
106    }
107}
108
109/// Echoes a plan's resolved analytics entitlement so a dashboard can configure itself against the LIVE catalog instead of hardcoding tier numbers. An empty plan resolves the free floor, and a catalog resolution failure serves that same floor rather than erroring — so this always answers 200. It is a read-only contract echo and carries no tenant data.
110pub async fn get_usage_analytics_access(configuration: &configuration::Configuration, plan: Option<&str>) -> Result<models::UsageAnalyticsAccess, Error<GetUsageAnalyticsAccessError>> {
111    // add a prefix to parameters to efficiently prevent name collisions
112    let p_plan = plan;
113
114    let uri_str = format!("{}/v1/usage/analytics/access", configuration.base_path);
115    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
116
117    if let Some(ref param_value) = p_plan {
118        req_builder = req_builder.query(&[("plan", &param_value.to_string())]);
119    }
120    if let Some(ref user_agent) = configuration.user_agent {
121        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
122    }
123    if let Some(ref token) = configuration.bearer_access_token {
124        req_builder = req_builder.bearer_auth(token.to_owned());
125    };
126
127    let req = req_builder.build()?;
128    let resp = configuration.client.execute(req).await?;
129
130    let status = resp.status();
131    let content_type = resp
132        .headers()
133        .get("content-type")
134        .and_then(|v| v.to_str().ok())
135        .unwrap_or("application/octet-stream");
136    let content_type = super::ContentType::from(content_type);
137
138    if !status.is_client_error() && !status.is_server_error() {
139        let content = resp.text().await?;
140        match content_type {
141            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
142            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::UsageAnalyticsAccess`"))),
143            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::UsageAnalyticsAccess`")))),
144        }
145    } else {
146        let content = resp.text().await?;
147        let entity: Option<GetUsageAnalyticsAccessError> = serde_json::from_str(&content).ok();
148        Err(Error::ResponseError(ResponseContent { status, content, entity }))
149    }
150}
151
152/// Is the PER-PROVIDER view: one connected account's own consumption of its own plan — \"my plan is 47% through its 6h window, resets at 14:20\".  `current` is the newest instance of each lane (the headline); `windows` is the history behind it. Both come from ONE deduped read, so they can never disagree. The rows are the caller's OWN linked accounts, scoped to the validated principal and its subject — never another user's, and never another org's.
153pub async fn get_usage_samples(configuration: &configuration::Configuration, account: Option<&str>, provider: Option<&str>, range: Option<&str>, window: Option<&str>) -> Result<models::DashResp, Error<GetUsageSamplesError>> {
154    // add a prefix to parameters to efficiently prevent name collisions
155    let p_account = account;
156    let p_provider = provider;
157    let p_range = range;
158    let p_window = window;
159
160    let uri_str = format!("{}/v1/usage/samples", configuration.base_path);
161    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
162
163    if let Some(ref param_value) = p_account {
164        req_builder = req_builder.query(&[("account", &param_value.to_string())]);
165    }
166    if let Some(ref param_value) = p_provider {
167        req_builder = req_builder.query(&[("provider", &param_value.to_string())]);
168    }
169    if let Some(ref param_value) = p_range {
170        req_builder = req_builder.query(&[("range", &param_value.to_string())]);
171    }
172    if let Some(ref param_value) = p_window {
173        req_builder = req_builder.query(&[("window", &param_value.to_string())]);
174    }
175    if let Some(ref user_agent) = configuration.user_agent {
176        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
177    }
178    if let Some(ref token) = configuration.bearer_access_token {
179        req_builder = req_builder.bearer_auth(token.to_owned());
180    };
181
182    let req = req_builder.build()?;
183    let resp = configuration.client.execute(req).await?;
184
185    let status = resp.status();
186    let content_type = resp
187        .headers()
188        .get("content-type")
189        .and_then(|v| v.to_str().ok())
190        .unwrap_or("application/octet-stream");
191    let content_type = super::ContentType::from(content_type);
192
193    if !status.is_client_error() && !status.is_server_error() {
194        let content = resp.text().await?;
195        match content_type {
196            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
197            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::DashResp`"))),
198            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::DashResp`")))),
199        }
200    } else {
201        let content = resp.text().await?;
202        let entity: Option<GetUsageSamplesError> = serde_json::from_str(&content).ok();
203        Err(Error::ResponseError(ResponseContent { status, content, entity }))
204    }
205}
206
207/// Answers GET /v1/usage/summary: the caller's own usage footprint over one window — the categorized spend roll-up from the commerce ledger, the org's LLM usage totals from the warehouse, and the caller's OWN linked provider accounts beside the org's Hanzo-routed usage.  Every source degrades INDEPENDENTLY to honest zeros and says so in `sources` and in its own `available` flag, so a partial deploy reports \"no data\" rather than fabricating spend. The account rows and the Hanzo rows are concatenated and never summed: a plan's percent is not money.  The response is org-scoped from the validated principal and marked no-store — a signed-out caller is refused.
208pub async fn get_usage_summary(configuration: &configuration::Configuration, range: Option<&str>, start: Option<&str>, end: Option<&str>) -> Result<models::UsageSummary, Error<GetUsageSummaryError>> {
209    // add a prefix to parameters to efficiently prevent name collisions
210    let p_range = range;
211    let p_start = start;
212    let p_end = end;
213
214    let uri_str = format!("{}/v1/usage/summary", configuration.base_path);
215    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
216
217    if let Some(ref param_value) = p_range {
218        req_builder = req_builder.query(&[("range", &param_value.to_string())]);
219    }
220    if let Some(ref param_value) = p_start {
221        req_builder = req_builder.query(&[("start", &param_value.to_string())]);
222    }
223    if let Some(ref param_value) = p_end {
224        req_builder = req_builder.query(&[("end", &param_value.to_string())]);
225    }
226    if let Some(ref user_agent) = configuration.user_agent {
227        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
228    }
229    if let Some(ref token) = configuration.bearer_access_token {
230        req_builder = req_builder.bearer_auth(token.to_owned());
231    };
232
233    let req = req_builder.build()?;
234    let resp = configuration.client.execute(req).await?;
235
236    let status = resp.status();
237    let content_type = resp
238        .headers()
239        .get("content-type")
240        .and_then(|v| v.to_str().ok())
241        .unwrap_or("application/octet-stream");
242    let content_type = super::ContentType::from(content_type);
243
244    if !status.is_client_error() && !status.is_server_error() {
245        let content = resp.text().await?;
246        match content_type {
247            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
248            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::UsageSummary`"))),
249            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::UsageSummary`")))),
250        }
251    } else {
252        let content = resp.text().await?;
253        let entity: Option<GetUsageSummaryError> = serde_json::from_str(&content).ok();
254        Err(Error::ResponseError(ResponseContent { status, content, entity }))
255    }
256}
257
258/// Ingests a batch of account-usage samples — what a developer's OWN AI accounts have consumed of their OWN plans, metered from each provider's own login — and appends them to the warehouse series. Answers 202.  Send either a `samples` array or one sample's fields at the top level. Every sample needs a provider, a machine and a known window class; an unknown window or kind is refused rather than silently rewritten, because a dash filled with a class nobody reported is worse than an error. There is no timestamp field: the server owns the observation clock, and a sample says which window it measured with windowStart or resetsAt.  It is FAIL-SOFT on storage: a warehouse outage costs a poll of history (stored:false), never a failed request. It records usage ONLY — the link registry is refreshed separately via POST /v1/link, so there is one and only one way to update an account row.
259pub async fn post_usage(configuration: &configuration::Configuration, report_req: models::ReportReq) -> Result<models::ReportResp, Error<PostUsageError>> {
260    // add a prefix to parameters to efficiently prevent name collisions
261    let p_report_req = report_req;
262
263    let uri_str = format!("{}/v1/usage", configuration.base_path);
264    let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
265
266    if let Some(ref user_agent) = configuration.user_agent {
267        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
268    }
269    if let Some(ref token) = configuration.bearer_access_token {
270        req_builder = req_builder.bearer_auth(token.to_owned());
271    };
272    req_builder = req_builder.json(&p_report_req);
273
274    let req = req_builder.build()?;
275    let resp = configuration.client.execute(req).await?;
276
277    let status = resp.status();
278    let content_type = resp
279        .headers()
280        .get("content-type")
281        .and_then(|v| v.to_str().ok())
282        .unwrap_or("application/octet-stream");
283    let content_type = super::ContentType::from(content_type);
284
285    if !status.is_client_error() && !status.is_server_error() {
286        let content = resp.text().await?;
287        match content_type {
288            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
289            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::ReportResp`"))),
290            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::ReportResp`")))),
291        }
292    } else {
293        let content = resp.text().await?;
294        let entity: Option<PostUsageError> = serde_json::from_str(&content).ok();
295        Err(Error::ResponseError(ResponseContent { status, content, entity }))
296    }
297}
298