From fd84c08dabfa1a1b9437919011dbbefeb0f38019 Mon Sep 17 00:00:00 2001 From: Matteo Date: Fri, 10 Jul 2026 18:10:40 +0200 Subject: [PATCH 1/2] Fix billing schema drift breaking credits/auth/models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suno's /api/billing/info/ endpoint no longer returns a nested `plan: { name, plan_key }` object or a `period` string — both were required fields on the Rust BillingInfo struct, so every command touching billing (credits, auth, config check, models) failed to deserialize the response and errored out. The live response now exposes `subscription_type` (either `false` for the free tier, or the active plan's `plan_key` string) plus a `plans` array describing the full catalog (id, key, name, pricing, features). Verified against a live authenticated call to the current API. Replace the required plan/period/renews_on fields with subscription_type + plans, and add BillingInfo::plan_name() to resolve a human-readable plan name from that catalog. Update the two call sites that read info.plan.name and the credits table renderer accordingly. --- src/api/types.rs | 56 +++++++++++++++++++++++++++++++++++++++++---- src/main.rs | 6 +++-- src/output/table.rs | 6 +---- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/api/types.rs b/src/api/types.rs index 327151a..6c5b23e 100644 --- a/src/api/types.rs +++ b/src/api/types.rs @@ -9,22 +9,68 @@ pub struct BillingInfo { pub monthly_usage: u64, pub monthly_limit: u64, pub is_active: bool, - pub plan: Plan, + #[serde(default)] + pub is_past_due: bool, + /// Suno no longer nests the active plan under a `plan` key. Instead + /// `subscription_type` is `false` on the free tier or the plan's + /// `plan_key` string (e.g. `"pro"`) when subscribed, and the full catalog + /// of plans (with pricing/features) is listed separately in `plans`. + #[serde(default)] + pub subscription_type: SubscriptionType, pub models: Vec, - pub period: String, - pub renews_on: Option, + #[serde(default)] + pub plans: Vec, + #[serde(default)] + pub accessible_features: Vec, #[serde(default)] pub remaster_model_types: Vec, } #[derive(Debug, Deserialize, Serialize)] -pub struct Plan { - pub name: String, +#[serde(untagged)] +pub enum SubscriptionType { + /// No active paid subscription (free tier). + None(bool), + /// Active plan, identified by its `plan_key` (e.g. "pro"). + Plan(String), +} + +impl Default for SubscriptionType { + fn default() -> Self { + SubscriptionType::None(false) + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct PlanOption { pub plan_key: String, + pub name: String, #[serde(default)] pub usage_plan_features: Vec, } +impl BillingInfo { + /// Human-readable name of the account's current plan, resolved from + /// `subscription_type` against the `plans` catalog (falls back to the + /// raw plan key, or "Free" when there's no active subscription). + pub fn plan_name(&self) -> String { + match &self.subscription_type { + SubscriptionType::Plan(key) => self + .plans + .iter() + .find(|p| &p.plan_key == key) + .map(|p| p.name.clone()) + .unwrap_or_else(|| key.clone()), + SubscriptionType::None(_) => self + .plans + .iter() + .find(|p| p.plan_key == "free") + .map(|p| p.name.clone()) + .unwrap_or_else(|| "Free".to_string()), + } + } +} + #[derive(Debug, Deserialize, Serialize)] pub struct Feature { pub name: String, diff --git a/src/main.rs b/src/main.rs index 947e981..cc628e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -221,7 +221,8 @@ async fn run() -> Result<(), CliError> { } eprintln!( "Authenticated! Plan: {}, Credits: {}", - info.plan.name, info.total_credits_left + info.plan_name(), + info.total_credits_left ); } @@ -609,7 +610,8 @@ async fn run() -> Result<(), CliError> { let info = client.billing_info().await?; eprintln!( "Auth: OK — {}, {} credits", - info.plan.name, info.total_credits_left + info.plan_name(), + info.total_credits_left ); } Err(CliError::AuthExpired) => { diff --git a/src/output/table.rs b/src/output/table.rs index 6759597..a403d93 100644 --- a/src/output/table.rs +++ b/src/output/table.rs @@ -37,17 +37,13 @@ pub fn billing(info: &BillingInfo) { .apply_modifier(UTF8_ROUND_CORNERS) .set_header(vec!["Field", "Value"]); - table.add_row(vec!["Plan", &info.plan.name]); + table.add_row(vec!["Plan", &info.plan_name()]); table.add_row(vec!["Credits Left", &info.total_credits_left.to_string()]); table.add_row(vec![ "Monthly Usage", &format!("{} / {}", info.monthly_usage, info.monthly_limit), ]); table.add_row(vec!["Active", &info.is_active.to_string()]); - table.add_row(vec!["Period", &info.period]); - if let Some(ref renew) = info.renews_on { - table.add_row(vec!["Renews On", renew]); - } println!("{table}"); } From cffbd01df9ee6c0b2eb1deec132f6a0e918c95b8 Mon Sep 17 00:00:00 2001 From: Matteo Date: Fri, 10 Jul 2026 18:11:07 +0200 Subject: [PATCH 2/2] Add --title to describe, add v4.5-all model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `describe` (inspiration mode) had no --title, forcing a mismatched custom-mode `generate` call just to name the clip. Add it directly to the GenerateRequest. Add the free-tier model v4.5-all (chirp-auk-turbo) to ModelVersion — missing from the CLI's enum even though it's the only model reporting can_use: true for free-tier accounts. --- src/cli.rs | 8 ++++++++ src/main.rs | 1 + 2 files changed, 9 insertions(+) diff --git a/src/cli.rs b/src/cli.rs index c3b20f8..f4c1cb5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -178,6 +178,10 @@ pub struct DescribeArgs { #[arg(short, long)] pub prompt: String, + /// Song title + #[arg(short, long)] + pub title: Option, + /// Style tags (optional, guides the generation) #[arg(long)] pub tags: Option, @@ -494,6 +498,8 @@ pub enum ModelVersion { V45Plus, #[value(name = "v4.5")] V45, + #[value(name = "v4.5-all")] + V45All, #[value(name = "v4")] V4, #[value(name = "v3.5")] @@ -511,6 +517,7 @@ impl ModelVersion { Self::V5 => "chirp-crow", Self::V45Plus => "chirp-bluejay", Self::V45 => "chirp-auk", + Self::V45All => "chirp-auk-turbo", Self::V4 => "chirp-v4", Self::V35 => "chirp-v3-5", Self::V3 => "chirp-v3-0", @@ -524,6 +531,7 @@ impl ModelVersion { Self::V5 => "v5", Self::V45Plus => "v4.5+", Self::V45 => "v4.5", + Self::V45All => "v4.5-all", Self::V4 => "v4", Self::V35 => "v3.5", Self::V3 => "v3", diff --git a/src/main.rs b/src/main.rs index cc628e2..c84dccd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -345,6 +345,7 @@ async fn run() -> Result<(), CliError> { // text is sent in the same `prompt` field as custom mode. let mut req = GenerateRequest::new(args.model.to_api_key(), "inspiration"); req.prompt = args.prompt; + req.title = args.title; req.tags = tags; req.make_instrumental = args.instrumental; req.persona_id = args.persona.clone();