Summary
suno stems <clip_id> exits with code 1 and "error decoding response body" on v0.5.7
for every clip, including clips where suno info <clip_id> confirms "has_stem": true.
The root cause is a Rust type mismatch in src/api/stems.rs: the stems() function
attempts to deserialize the Suno API response as the generic Clip struct, but the
stems endpoint returns a different schema — and Clip.model_name is a required
(non-Option) String that stem responses almost certainly don't include, causing
serde_json to fail immediately with a missing-field error that reqwest wraps as
"error decoding response body".
Environment
| Field |
Value |
| suno-cli version |
0.5.7 (confirmed current latest via suno update) |
| OS |
macOS 15.5 (Darwin arm64 / Apple Silicon M-series) |
| Install method |
Homebrew (brew tap paperfoot/tap && brew install suno) |
| Auth status |
Valid — JWT refreshed via suno auth --refresh, suno credits returns correct balance and plan |
| Plan |
Pro (includes get_stems feature in usage_plan_features) |
Steps to Reproduce
# 1. Generate a clip (any method), wait for completion, note the clip ID
# 2. Confirm stems exist for the clip
suno info <clip_id> --json | jq .data.metadata.has_stem
# → true
# 3. Request stems
suno stems <clip_id> --json
# → exit 1
Also fails with:
suno stems <clip_id> (without --json)
suno stems <clip_id> --wait --json (--wait makes no difference)
After auth refresh (auth is not the cause):
suno auth --refresh
# → "JWT refreshed successfully / Authenticated! Plan: Pro Plan"
suno stems <clip_id> --json
# → still fails with same error
Actual Output
{
"version": "1",
"status": "error",
"error": {
"code": "http_error",
"message": "error decoding response body",
"suggestion": "Check your network connection and retry"
}
}
Exit code: 1
Expected Output
Something shaped like this (two stem objects — vocal + instrumental):
{
"version": "1",
"status": "success",
"data": [
{
"id": "<vocal-stem-clip-id>",
"title": "My Song (Vocals)",
"audio_url": "https://cdn1.suno.ai/<vocal-stem-clip-id>.wav",
"status": "complete"
},
{
"id": "<instrumental-stem-clip-id>",
"title": "My Song (Instrumental)",
"audio_url": "https://cdn1.suno.ai/<instrumental-stem-clip-id>.wav",
"status": "complete"
}
]
}
Root Cause (code-level)
src/api/stems.rs — the failing function
pub async fn stems(&self, clip_id: &str) -> Result<Clip, CliError> {
self.with_auth_retry(|| async {
let resp = self
.post(&format!("/api/edit/stems/{clip_id}"))
.send()
.await?;
let resp = self.check_response(resp).await?;
Ok(resp.json().await?) // ← attempts to deserialize as Clip — FAILS
})
.await
}
src/api/types.rs — the Clip struct that deserialization targets
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Clip {
pub id: String,
pub title: String,
pub status: String,
pub model_name: String, // ← required, non-Option
pub audio_url: Option<String>,
pub video_url: Option<String>,
pub image_url: Option<String>,
pub created_at: String, // ← required, non-Option
#[serde(default)]
pub play_count: u64,
#[serde(default)]
pub upvote_count: u64,
#[serde(default)]
pub metadata: ClipMetadata,
}
Why deserialization fails
The Suno stems endpoint (POST /api/edit/stems/{clip_id}) returns stem clip objects,
not standard generated clips. Based on documentation from third-party Suno API wrappers
(gcui-art/suno-api), stem objects have a different shape:
- They include a
stem_from_id field (the source clip's ID) not present in Clip
- They likely omit
model_name, which is String (required) in Clip — this
would cause serde_json to return missing field 'model_name' immediately
- The response is likely an array of two clip objects (vocal + instrumental),
not a single object — so resp.json::<Clip>() would fail on the type mismatch
even if all fields were present
Additionally, main.rs dispatches stems as:
Commands::Stems(args) => {
let clip = client().await?.stems(&args.clip_id).await?;
match fmt {
OutputFormat::Json => output::json::success(&clip),
OutputFormat::Table => output::table::clips(&[clip]),
}
}
This treats the result as a single Clip, which doesn't match a two-stem array response.
The comment in stems.rs is worth noting
// Uses /api/edit/stems/{song_id} per gcui-art/paean-ai evidence.
The endpoint was inferred from a third-party source. It's possible the endpoint path,
request format, or response schema has changed since that evidence was gathered.
Misleading dry-run fixture
The project's test fixture at tests/fixtures/stems.json reads:
{
"data": {
"clip_id": "clip_dryrun_a",
"instrumental_url": "https://example.invalid/clip_dryrun_a-instrumental.wav",
"vocals_url": "https://example.invalid/clip_dryrun_a-vocals.wav"
}
}
This fixture is consumed by the dry-run shim and downstream tooling, not by the
actual CLI deserialization path. Its format doesn't reflect what the live Suno API returns.
The fixture working correctly in dry-run tests masks the live API schema mismatch.
Also: --wait flag not implemented
StemsArgs declares --wait as a flag but stems() in src/api/stems.rs is a single
POST with no polling loop — the flag is accepted but silently ignored. Stem generation is
likely async on Suno's side; without polling, the response may also return an in-progress
status rather than final stem data, which could be a second contributing factor.
Suggested Fix
-
Determine the actual response schema from the live Suno API (most important step — the CLI crashes before the body is visible to the caller).
-
Add a dedicated response type (adjust field names to match actual schema):
#[derive(Debug, Deserialize)]
pub struct StemClip {
pub id: String,
pub title: String,
pub status: String,
pub audio_url: Option<String>,
pub stem_from_id: Option<String>,
// model_name intentionally omitted — not returned by the stems endpoint
}
- Update
stems() (likely Vec<StemClip> if the endpoint returns an array):
pub async fn stems(&self, clip_id: &str) -> Result<Vec<StemClip>, CliError> {
// ...
Ok(resp.json::<Vec<StemClip>>().await?)
}
-
Implement --wait polling if stem generation is async.
-
Update main.rs to handle the array and distinguish vocal from instrumental by title or a stem-type field.
Capturing the raw response for debugging
// In stems.rs, temporarily replace resp.json() with:
let body = resp.text().await?;
eprintln!("DEBUG stems body: {}", body);
I'm happy to test a patched binary — I have the affected clips saved and can re-run immediately.
Impact
suno stems is completely non-functional for all clips in v0.5.7
- Pro plan subscribers pay for
get_stems but cannot use it programmatically
- Automated quality checks that depend on stem separation always return
MANUAL,
requiring manual stem download from the web UI as a workaround
- The
--wait flag being silently ignored is a secondary UX issue
Tested clip IDs (all confirmed "has_stem": true via suno info):
b8759abd-6a12-4538-8c45-d0dc78f26753
40c54f63-a66b-456d-a1fa-c042cc710820
2d848894-5329-4856-9f62-61b8ad28efda
Summary
suno stems <clip_id>exits with code 1 and"error decoding response body"on v0.5.7for every clip, including clips where
suno info <clip_id>confirms"has_stem": true.The root cause is a Rust type mismatch in
src/api/stems.rs: thestems()functionattempts to deserialize the Suno API response as the generic
Clipstruct, but thestems endpoint returns a different schema — and
Clip.model_nameis a required(non-
Option)Stringthat stem responses almost certainly don't include, causingserde_json to fail immediately with a missing-field error that reqwest wraps as
"error decoding response body".Environment
suno update)brew tap paperfoot/tap && brew install suno)suno auth --refresh,suno creditsreturns correct balance and planget_stemsfeature inusage_plan_features)Steps to Reproduce
Also fails with:
suno stems <clip_id>(without --json)suno stems <clip_id> --wait --json(--wait makes no difference)After auth refresh (auth is not the cause):
Actual Output
{ "version": "1", "status": "error", "error": { "code": "http_error", "message": "error decoding response body", "suggestion": "Check your network connection and retry" } }Exit code: 1
Expected Output
Something shaped like this (two stem objects — vocal + instrumental):
{ "version": "1", "status": "success", "data": [ { "id": "<vocal-stem-clip-id>", "title": "My Song (Vocals)", "audio_url": "https://cdn1.suno.ai/<vocal-stem-clip-id>.wav", "status": "complete" }, { "id": "<instrumental-stem-clip-id>", "title": "My Song (Instrumental)", "audio_url": "https://cdn1.suno.ai/<instrumental-stem-clip-id>.wav", "status": "complete" } ] }Root Cause (code-level)
src/api/stems.rs— the failing functionsrc/api/types.rs— theClipstruct that deserialization targetsWhy deserialization fails
The Suno stems endpoint (
POST /api/edit/stems/{clip_id}) returns stem clip objects,not standard generated clips. Based on documentation from third-party Suno API wrappers
(gcui-art/suno-api), stem objects have a different shape:
stem_from_idfield (the source clip's ID) not present inClipmodel_name, which isString(required) inClip— thiswould cause serde_json to return
missing field 'model_name'immediatelynot a single object — so
resp.json::<Clip>()would fail on the type mismatcheven if all fields were present
Additionally,
main.rsdispatches stems as:This treats the result as a single
Clip, which doesn't match a two-stem array response.The comment in
stems.rsis worth notingThe endpoint was inferred from a third-party source. It's possible the endpoint path,
request format, or response schema has changed since that evidence was gathered.
Misleading dry-run fixture
The project's test fixture at
tests/fixtures/stems.jsonreads:{ "data": { "clip_id": "clip_dryrun_a", "instrumental_url": "https://example.invalid/clip_dryrun_a-instrumental.wav", "vocals_url": "https://example.invalid/clip_dryrun_a-vocals.wav" } }This fixture is consumed by the dry-run shim and downstream tooling, not by the
actual CLI deserialization path. Its format doesn't reflect what the live Suno API returns.
The fixture working correctly in dry-run tests masks the live API schema mismatch.
Also:
--waitflag not implementedStemsArgsdeclares--waitas a flag butstems()insrc/api/stems.rsis a singlePOST with no polling loop — the flag is accepted but silently ignored. Stem generation is
likely async on Suno's side; without polling, the response may also return an in-progress
status rather than final stem data, which could be a second contributing factor.
Suggested Fix
Determine the actual response schema from the live Suno API (most important step — the CLI crashes before the body is visible to the caller).
Add a dedicated response type (adjust field names to match actual schema):
stems()(likelyVec<StemClip>if the endpoint returns an array):Implement
--waitpolling if stem generation is async.Update
main.rsto handle the array and distinguish vocal from instrumental by title or a stem-type field.Capturing the raw response for debugging
I'm happy to test a patched binary — I have the affected clips saved and can re-run immediately.
Impact
suno stemsis completely non-functional for all clips in v0.5.7get_stemsbut cannot use it programmaticallyMANUAL,requiring manual stem download from the web UI as a workaround
--waitflag being silently ignored is a secondary UX issueTested clip IDs (all confirmed
"has_stem": trueviasuno info):b8759abd-6a12-4538-8c45-d0dc78f2675340c54f63-a66b-456d-a1fa-c042cc7108202d848894-5329-4856-9f62-61b8ad28efda