Skip to content

feat(suno): add suno.com music-generation adapter#1638

Open
ele-yufo wants to merge 4 commits into
jackwener:mainfrom
ele-yufo:feat/suno-adapter
Open

feat(suno): add suno.com music-generation adapter#1638
ele-yufo wants to merge 4 commits into
jackwener:mainfrom
ele-yufo:feat/suno-adapter

Conversation

@ele-yufo
Copy link
Copy Markdown
Contributor

Summary

A new browser-based adapter for Suno, the AI music generation service. Targets the /api/generate/v2-web/ schema introduced in 2026-05 and the cookie-isolation-aware Bearer auth path.

Commands

Command Purpose
opencli suno status login state, plan, credit breakdown, captcha
opencli suno list list recent clips in the user's library
opencli suno generate Simple or Custom mode generation + download
opencli suno download fetch an existing clip by id in MP3 / M4A / WAV / cover

Each generate request returns 2 candidate clips (Suno's native A/B). All formats land alongside a metadata JSON dump for traceability.

Custom-mode controls (matches the web UI)

  • --lyrics + --tags + --negative-tags + --title
  • --weirdness (0..1) → metadata.control_sliders.weirdness_constraint
  • --style-weight (0..1) → metadata.control_sliders.style_weight
  • --instrumental, --model (chirp-fenix / chirp-bluejay / chirp-v4 / chirp-v3-5)

Paid-format guard

WAV downloads charge against the user's account (the Suno UI's Download → WAV flow makes the same billing/clips/{id}/download/ call). Both generate and download skip WAV by default and require --confirm-paid true to actually charge — surfaced in the result row as skipped(needs --confirm-paid):wav rather than silently dropping it.

Auth notes (worth a reviewer's attention)

The studio API rejects the OpenCLI bridge's credentials: 'include' cross-origin fetch (third-party cookie isolation in the eval context). The adapter explicitly pulls the live Clerk JWT via await window.Clerk.session.getToken() and sends it as Authorization: Bearer, alongside the browser-token (anti-replay base64-encoded timestamp) and device-id (from the suno_device_id cookie) headers that the studio API also requires. This matches the wire format observed in a real /api/generate/v2-web/ request capture.

Validation

  • `npx tsc --noEmit` — clean
  • `opencli validate suno` — PASS, 0 errors, 0 warnings, 4 commands
  • `npx vitest run --project adapter clis/suno/` — 47 tests pass across 4 files
  • `npm test` — 3811 passing across 380 files
  • Live (Pro account, 10 credits/song): two-clip generation completes end-to-end; MP3 (796 KB, 64 kbps 48 kHz), M4A (603 KB opus), and WAV (6.7 MB PCM 16-bit 48 kHz) all download intact; cover JPEG + metadata JSON also written.

Known gaps (intentional / deferrable)

  • Stems extraction (12-track separation) — schema known from the capture (`task: 'gen_stem'`, `stem_type_id: 91`, `stem_task: 'twelve'`, `continue_clip_id: `), but not yet wired. Happy to follow up in a separate PR if reviewers want it included; stems are paid and the design space (one-shot vs poll for 12 separate child clips) is worth its own review pass.
  • Captcha solving — `/api/c/check` is checked pre-flight and the command fails fast with a clear instruction if Suno requires a challenge. Solving captchas headlessly is out of scope.

Files

```
clis/suno/
├── utils.js (498) — session, Bearer auth, generation, polling, download helpers
├── generate.js (208) — main generate command (Simple + Custom + sliders)
├── download.js (120) — standalone clip download by id
├── status.js (54) — health check
├── list.js (65) — library pagination
├── utils.test.js (148) — pure-function tests (parseFormats, sliders, sanitizers)
├── generate.test.js (206) — generate flow + argument validation + paid-format guard
├── download.test.js (98) — download by id + clip-id parsing + paid-format guard
└── commands.test.js (125) — status + list end-to-end (mocked utils)
```

Total: +1522 lines, 47 tests.

ele-yufo added 4 commits May 17, 2026 21:26
A new browser-based adapter for Suno (https://suno.com), the AI music
generation service. Targets the /api/generate/v2-web/ schema introduced
in 2026-05 and the cookie-isolation-aware Bearer auth path.

## Commands

- `opencli suno status` — login state, plan, credit breakdown, captcha
- `opencli suno list` — list recent clips in the user's library
- `opencli suno generate` — Simple or Custom mode generation + download
- `opencli suno download` — fetch an existing clip by id in MP3/M4A/WAV/cover

Each generate request returns 2 candidate clips (Suno's native A/B). All
formats land alongside a metadata JSON dump.

## Custom mode controls

Matches the web UI exactly:
- `--lyrics` + `--tags` + `--negative-tags` + `--title`
- `--weirdness` (0..1) → `metadata.control_sliders.weirdness_constraint`
- `--style-weight` (0..1) → `metadata.control_sliders.style_weight`
- `--instrumental`, `--model` (chirp-fenix / chirp-bluejay / v4 / v3-5)

## Paid-format guard

WAV downloads charge against the user's account (the Suno UI's
"Download → WAV" flow makes the same `billing/clips/{id}/download/`
call). Both `generate` and `download` skip WAV by default and require
`--confirm-paid true` to charge — surfaced in the result row as
`skipped(needs --confirm-paid):wav` rather than silently dropping it.

## Auth notes

Studio API rejects the OpenCLI bridge's `credentials: 'include'`
cross-origin fetch (third-party cookie isolation in the eval context).
The adapter explicitly pulls the live Clerk JWT via
`await window.Clerk.session.getToken()` and sends it as
`Authorization: Bearer`, alongside the `browser-token` (anti-replay
timestamp) and `device-id` (from the `suno_device_id` cookie) headers
that the studio API also requires. This matches the wire format observed
in a real `/api/generate/v2-web/` request capture.

## Validation

- `npx tsc --noEmit` — clean
- `opencli validate suno` — PASS, 0 errors, 0 warnings, 4 commands
- `npx vitest run --project adapter clis/suno/` — 47 tests pass (4 files)
- `npm test` — 3811 passing across 380 files
- Live (Pro account, 2 credits/song): two-clip generation completes,
  MP3 (796 KB, 64 kbps 48 kHz), M4A (603 KB opus), and WAV (6.7 MB PCM
  16-bit 48 kHz) all download intact; cover JPEG + metadata JSON also
  written.

## Known gaps

- **Stems extraction (12-track separation)** — schema known from the
  capture (`task: 'gen_stem'`, `stem_type_id: 91`, `stem_task: 'twelve'`,
  `continue_clip_id: <source>`), but not yet wired. Happy to follow up
  in a separate PR if reviewers want it included; stems are paid and
  the design space (one-shot vs poll for 12 separate child clips) is
  worth its own review pass.
- **Captcha solving** — `/api/c/check` is checked pre-flight and the
  command fails fast with a clear instruction if Suno requires a
  challenge. Solving captchas headlessly is out of scope.
CI on the previous commit failed on two checks:

- `build (ubuntu-latest)` → cli-manifest.json was out of sync with the
  source; I had run `git checkout -- cli-manifest.json` before pushing,
  which dropped the manifest entries for the four new suno commands.
  This regenerates the manifest via `npm run build`.

- `doc-coverage --strict` → suno was the only adapter without a
  matching `docs/adapters/<mode>/suno.md`. Add a browser-mode page
  covering all four commands, options, behaviour notes, and the
  Clerk-Bearer + browser-token / device-id auth quirk.

Doc coverage now reports `146/146 adapters documented`. No source
changes — adapter behaviour is unchanged.
The convention-audit CI gate scans object literals returned anywhere
in an adapter — including inner page.evaluate IIFE returns — and
treats any literal whose keys overlap row-shape names (e.g. `status`)
as a row candidate. My inner `return { status: res.status, body }`
shape made the audit flag `body` as a silently dropped column.

Refactor the feed lookup to return the same `{ ok, error/clips }`
envelope shape already used in list.js: it both passes the audit and
matches the rest of the adapter's error-handling discipline.

- `npx vitest run --project adapter clis/suno/` — 47 tests still pass
- `node scripts/check-silent-column-drop.mjs` — no new violations
`Plan: session.planKey || 'unknown'` tripped the typed-error-lint
`silent-sentinel` rule, which forbids `|| '<literal>'` fallbacks in
adapter rows. `ensureSunoSession` already populates planKey from the
same billing/info response as planId (and throws on missing planId),
so the fallback was dead defensive code anyway.

While here, harden the captcha catch block: a probe failure now
defaults to `required: true` (conservative), keeping the displayed
status row truthful instead of claiming "Not required" for an
unverified state.

- `node scripts/check-typed-error-lint.mjs` — 173/173 baseline, no new
- `node scripts/check-silent-column-drop.mjs` — 102/102 baseline, no new
- `npx vitest run --project adapter clis/suno/` — 47 tests pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant