use serde::Deserialize;
use crate::error::{AppError, Result};
use crate::usage::{MinimaxSnapshot, UsageWindow};
const BUCKET_GENERAL: &str = "general";
const BUCKET_VIDEO: &str = "video";
const DEFAULT_INTERVAL: chrono::Duration = chrono::Duration::hours(5);
const DEFAULT_WEEKLY: chrono::Duration = chrono::Duration::days(7);
#[derive(Debug, Clone, Deserialize)]
pub struct BaseResp {
pub status_code: i64,
#[serde(default)]
pub status_msg: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RemainsEnvelope {
#[serde(default)]
pub model_remains: Vec<ModelRemains>,
pub base_resp: BaseResp,
}
impl RemainsEnvelope {
pub fn check_ok(&self) -> Result<()> {
if self.base_resp.status_code == 0 {
return Ok(());
}
Err(AppError::Schema(format!(
"minimax: API reported failure (status_code {})",
self.base_resp.status_code
)))
}
}
pub fn is_auth_failure(status_code: i64) -> bool {
matches!(status_code, 1004 | 2049)
}
#[derive(Debug, Clone, Deserialize)]
pub struct ModelRemains {
pub model_name: String,
pub start_time: i64,
pub end_time: i64,
pub current_interval_remaining_percent: i64,
pub weekly_start_time: i64,
pub weekly_end_time: i64,
pub current_weekly_remaining_percent: i64,
}
fn consumed_pct(remaining: i64) -> i32 {
(100 - remaining.clamp(0, 100)) as i32
}
fn at_millis(ms: i64) -> Option<chrono::DateTime<chrono::Utc>> {
if ms <= 0 {
return None;
}
chrono::DateTime::from_timestamp_millis(ms)
}
fn span(start_ms: i64, end_ms: i64, default: chrono::Duration) -> chrono::Duration {
let delta = end_ms.saturating_sub(start_ms);
if delta > 0 {
chrono::Duration::milliseconds(delta)
} else {
default
}
}
fn interval_window(row: &ModelRemains) -> UsageWindow {
UsageWindow {
utilization_pct: consumed_pct(row.current_interval_remaining_percent),
resets_at: at_millis(row.end_time),
window_duration: span(row.start_time, row.end_time, DEFAULT_INTERVAL),
}
}
fn weekly_window(row: &ModelRemains) -> UsageWindow {
UsageWindow {
utilization_pct: consumed_pct(row.current_weekly_remaining_percent),
resets_at: at_millis(row.weekly_end_time),
window_duration: span(row.weekly_start_time, row.weekly_end_time, DEFAULT_WEEKLY),
}
}
pub fn to_snapshot(env: RemainsEnvelope, plan: &str) -> Result<MinimaxSnapshot> {
let rows = &env.model_remains;
let general = rows
.iter()
.find(|r| r.model_name == BUCKET_GENERAL)
.or_else(|| rows.iter().find(|r| r.model_name != BUCKET_VIDEO))
.ok_or_else(|| {
AppError::Schema("minimax: response carried no usable model bucket".to_string())
})?;
let video = rows.iter().find(|r| r.model_name == BUCKET_VIDEO);
Ok(MinimaxSnapshot {
plan: plan.to_string(),
session: interval_window(general),
weekly: weekly_window(general),
video_session: video.map(interval_window),
video_weekly: video.map(weekly_window),
})
}
#[cfg(test)]
mod tests {
use super::*;
const LIVE: &str = r#"{
"model_remains": [
{
"start_time": 1785164400000,
"end_time": 1785182400000,
"remains_time": 1877492,
"current_interval_total_count": 0,
"current_interval_usage_count": 0,
"model_name": "general",
"current_weekly_total_count": 0,
"current_weekly_usage_count": 0,
"weekly_start_time": 1785110400000,
"weekly_end_time": 1785715200000,
"weekly_remains_time": 534677492,
"current_interval_status": 1,
"current_interval_remaining_percent": 99,
"current_weekly_status": 1,
"current_weekly_remaining_percent": 99
},
{
"start_time": 1785110400000,
"end_time": 1785196800000,
"remains_time": 16277492,
"current_interval_total_count": 3,
"current_interval_usage_count": 0,
"model_name": "video",
"current_weekly_total_count": 21,
"current_weekly_usage_count": 0,
"weekly_start_time": 1785110400000,
"weekly_end_time": 1785715200000,
"weekly_remains_time": 534677492,
"current_interval_status": 1,
"current_interval_remaining_percent": 100,
"current_weekly_status": 1,
"current_weekly_remaining_percent": 100
}
],
"base_resp": { "status_code": 0, "status_msg": "success" }
}"#;
fn parse(raw: &str) -> RemainsEnvelope {
serde_json::from_str(raw).expect("envelope parses")
}
#[test]
fn parses_live_envelope() {
let env = parse(LIVE);
env.check_ok().expect("status_code 0 is success");
assert_eq!(env.model_remains.len(), 2);
assert_eq!(env.model_remains[0].model_name, "general");
}
#[test]
fn inverts_remaining_percent_into_consumed() {
let snap = to_snapshot(parse(LIVE), "Token Plan").unwrap();
assert_eq!(snap.session.utilization_pct, 1);
assert_eq!(snap.weekly.utilization_pct, 1);
assert_eq!(snap.video_session.unwrap().utilization_pct, 0);
}
#[test]
fn derives_window_length_from_the_row_not_a_constant() {
let snap = to_snapshot(parse(LIVE), "Token Plan").unwrap();
assert_eq!(snap.session.window_duration, chrono::Duration::hours(5));
assert_eq!(snap.weekly.window_duration, chrono::Duration::days(7));
assert_eq!(
snap.video_session.unwrap().window_duration,
chrono::Duration::hours(24),
"video rolls daily, not on general's 5h cadence"
);
}
#[test]
fn reset_comes_from_end_time_in_milliseconds() {
let snap = to_snapshot(parse(LIVE), "Token Plan").unwrap();
assert_eq!(
snap.session.resets_at,
chrono::DateTime::from_timestamp_millis(1785182400000)
);
}
#[test]
fn rejects_in_band_auth_failure() {
for raw in [
r#"{"base_resp":{"status_code":1004,"status_msg":"login fail: Please carry the API secret key in the 'Authorization' field of the request header"}}"#,
r#"{"base_resp":{"status_code":2049,"status_msg":"invalid api key"}}"#,
] {
let env = parse(raw);
assert!(env.model_remains.is_empty());
let err = env.check_ok().unwrap_err();
assert!(
matches!(err, AppError::Schema(ref m) if m.contains("minimax")),
"unexpected error: {err:?}"
);
}
}
#[test]
fn in_band_failure_does_not_surface_upstream_message() {
let env =
parse(r#"{"base_resp":{"status_code":9001,"status_msg":"secret request detail"}}"#);
let error = env.check_ok().unwrap_err().to_string();
assert!(error.contains("9001"), "{error}");
assert!(!error.contains("secret request detail"), "{error}");
}
#[test]
fn video_bucket_is_optional() {
let raw = r#"{
"model_remains": [{
"start_time": 1785164400000, "end_time": 1785182400000,
"model_name": "general",
"current_interval_remaining_percent": 40,
"weekly_start_time": 1785110400000, "weekly_end_time": 1785715200000,
"current_weekly_remaining_percent": 55
}],
"base_resp": {"status_code": 0, "status_msg": "success"}
}"#;
let snap = to_snapshot(parse(raw), "Token Plan").unwrap();
assert_eq!(snap.session.utilization_pct, 60);
assert_eq!(snap.weekly.utilization_pct, 45);
assert!(snap.video_session.is_none());
assert!(snap.video_weekly.is_none());
}
#[test]
fn errors_when_no_text_bucket_is_present() {
let raw = r#"{
"model_remains": [{
"start_time": 1, "end_time": 2, "model_name": "video",
"current_interval_remaining_percent": 100,
"weekly_start_time": 1, "weekly_end_time": 2,
"current_weekly_remaining_percent": 100
}],
"base_resp": {"status_code": 0, "status_msg": "success"}
}"#;
assert!(to_snapshot(parse(raw), "Token Plan").is_err());
}
#[test]
fn falls_back_to_a_positive_window_on_degenerate_bounds() {
let raw = r#"{
"model_remains": [{
"start_time": 0, "end_time": 0, "model_name": "general",
"current_interval_remaining_percent": 100,
"weekly_start_time": 0, "weekly_end_time": 0,
"current_weekly_remaining_percent": 100
}],
"base_resp": {"status_code": 0, "status_msg": "success"}
}"#;
let snap = to_snapshot(parse(raw), "Token Plan").unwrap();
assert_eq!(snap.session.window_duration, DEFAULT_INTERVAL);
assert_eq!(snap.weekly.window_duration, DEFAULT_WEEKLY);
assert_eq!(snap.session.resets_at, None, "epoch 0 is unreported");
}
#[test]
fn clamps_out_of_range_percentages() {
assert_eq!(consumed_pct(150), 0);
assert_eq!(consumed_pct(-5), 100);
}
}