【Suno SDK 系列 01】Crazyrouter Suno SDK 怎么接入?从 0 提交一个 suno/generate 任务,附 2026-07-06 实测
适合读者:后端开发、AI 工具开发者、正在接入 AI 音乐生成能力的 SaaS 团队。 文章重点:不是只复制文档示例,而是按真实接口跑一遍submit -> task_id -> poll -> content url -> audio_url,并记录本次遇到的请求体差异。
先说结论
我在2026-07-06 +08:00按 Crazyrouter 的 Suno SDK 文档做了一次最小实测。
文档地址:
https://docs.crazyrouter.com/sdks/suno-sdk本次测的是最基础的歌曲生成模型:
Base URL: https://api.crazyrouter.com Endpoint: POST /v1/video/generations Model: suno/generate Version: V5_5实测结果如下:
| 步骤 | 请求 | 结果 |
|---|---|---|
文档input写法提交 | POST /v1/video/generations | 返回prompt is required |
顶层prompt+metadata写法提交 | POST /v1/video/generations | 返回task_id,状态queued |
| 查询任务 | GET /v1/video/generations/{task_id} | 返回succeeded,旧格式显示mp4 |
| 下载公开内容 URL | GET /v1/videos/{task_id}/content | 原先返回 404/502,代理未拿到媒体文件 |
| 下载真实媒体 URL | resultJson.data[0].audio_url | HTTP 200,audio/mp3,412326 bytes |
所以这篇文章会给出一个更谨慎的结论:
suno/generate 的任务提交和轮询链路可以跑通; 但生产判断不能停在 succeeded,必须继续验证 content url 或真实 audio_url 是否真的返回媒体文件。这点很重要。音频/视频/音乐生成都是异步任务,HTTP 200、queued、succeeded分别代表不同阶段,不能混在一起。
1. Suno SDK 这套接口到底是什么
Crazyrouter 文档里的 Suno SDK 指的是一组 Suno 统一任务模型族,不是 OpenAI 官方 SDK 里的client.audio.*。
当前文档列出的模型包括:
suno/generate suno/extend suno/upload-extend suno/cover suno/timestamped-lyrics suno/vocal-separation统一任务入口是:
POST /v1/video/generations GET /v1/video/generations/{task_id} GET /v1/videos/{task_id}/content也就是说,它的调用方式更像“提交一个异步任务”,而不是“调用一个同步音频生成函数”。
推荐你按下面这个思路理解:
提交任务:告诉服务端我要生成什么音乐 拿 task_id:服务端确认任务已经进入队列 轮询任务:等待上游真正生成完成 下载内容:拿到最终 mp3/audio 内容并校验2. 准备 API Key 和环境变量
不要把 API Key 写死在代码里。建议先放到环境变量。
Linux / macOS:
exportCRAZYROUTER_API_KEY="你的 Crazyrouter API Key"PowerShell:
$env:CRAZYROUTER_API_KEY="你的 Crazyrouter API Key"本文使用的 Base URL:
https://api.crazyrouter.com文档也提到,如果业务里已经统一使用主站域名,也可以用:
https://crazyrouter.com我本次两个域名都测了,后面会说差异。
3. 第一次按文档 input 写法提交:失败
文档里的通用示例大致是这种结构:
{"model":"suno/generate","channel":"auto","input":{"model_version":"V5_5","customMode":true,"instrumental":false,"prompt":"[Verse]\nMorning lights across the wire\nTiny sparks climb higher","style":"bright synth pop, short, clean vocals","title":"CR Suno SDK Demo"}}我用中文歌词做了一个最小请求:
{"model":"suno/generate","channel":"auto","input":{"model_version":"V5_5","customMode":true,"instrumental":false,"prompt":"[Verse]\n清晨的键盘亮起光\n请求穿过云端的墙\n[Chorus]\n用一次实测把链路唱响\n从任务到结果都清清爽爽","style":"bright synth pop, short, clean vocals, Mandarin pop","title":"Crazyrouter Suno SDK Smoke Test 20260706"}}提交命令:
curl-sS-XPOST https://api.crazyrouter.com/v1/video/generations\-H"Authorization: Bearer$CRAZYROUTER_API_KEY"\-H"Content-Type: application/json"\--data-binary @suno_sdk_generate_payload_20260706.json返回:
{"code":"invalid_request","message":"prompt is required","data":null}我又把域名换成:
https://crazyrouter.com/v1/video/generations返回仍然是:
{"code":"invalid_request","message":"prompt is required","data":null}这说明它不是单一域名导致的问题,而是当前生产接口对顶层prompt有校验。
4. 第二次用顶层 prompt + metadata:提交成功
本地历史生产测试脚本里使用过一种兼容写法:
{"model":"suno/generate","prompt":"这里放主提示词","metadata":{"model_version":"V5_5","customMode":true,"instrumental":false,"prompt":"这里也放完整歌词或描述","style":"bright synth pop, short, clean vocals, Mandarin pop","title":"Crazyrouter Suno SDK Smoke Test 20260706"}}完整请求体:
{"model":"suno/generate","prompt":"[Verse]\n清晨的键盘亮起光\n请求穿过云端的墙\n[Chorus]\n用一次实测把链路唱响\n从任务到结果都清清爽爽","metadata":{"model_version":"V5_5","customMode":true,"instrumental":false,"prompt":"[Verse]\n清晨的键盘亮起光\n请求穿过云端的墙\n[Chorus]\n用一次实测把链路唱响\n从任务到结果都清清爽爽","style":"bright synth pop, short, clean vocals, Mandarin pop","title":"Crazyrouter Suno SDK Smoke Test 20260706"}}提交:
curl-sS-XPOST https://api.crazyrouter.com/v1/video/generations\-H"Authorization: Bearer$CRAZYROUTER_API_KEY"\-H"Content-Type: application/json"\--data-binary @suno_sdk_generate_payload_metadata_20260706.json返回:
{"id":"task_ac90b23619334516","task_id":"task_ac90b23619334516","object":"video","model":"suno/generate","status":"queued","progress":0,"created_at":1783267658}这里要注意,queued只代表任务提交成功,还没有生成完成。
5. 轮询任务状态
拿到task_id后,开始查:
curl-sShttps://api.crazyrouter.com/v1/video/generations/task_ac90b23619334516\-H"Authorization: Bearer$CRAZYROUTER_API_KEY"本次轮询结果:
{"code":"success","message":"","data":{"error":null,"format":"mp4","metadata":null,"status":"succeeded","task_id":"task_ac90b23619334516","url":"https://crazyrouter.com/v1/videos/task_ac90b23619334516/content"}}到这里,任务层面的结果是成功的:
status: succeeded format: mp4 url: https://crazyrouter.com/v1/videos/task_ac90b23619334516/content但是注意,url出现不等于内容已经成功下载。下一步还要校验媒体文件。
6. JavaScript / TypeScript 最小示例
下面这段代码把提交和轮询合在一起。这里我保留顶层prompt + metadata写法,因为这是本次实测能通过生产接口校验的结构。
constapiKey=process.env.CRAZYROUTER_API_KEY;constbaseURL=process.env.CRAZYROUTER_BASE_URL??"https://api.crazyrouter.com";if(!apiKey){thrownewError("Missing CRAZYROUTER_API_KEY");}asyncfunctionrequest(path:string,init:RequestInit={}){constresponse=awaitfetch(`${baseURL}${path}`,{...init,headers:{Authorization:`Bearer${apiKey}`,Accept:"application/json",...(init.body?{"Content-Type":"application/json"}:{}),...init.headers,},});constbody=awaitresponse.json().catch(()=>({}));if(!response.ok){thrownewError(`HTTP${response.status}:${JSON.stringify(body)}`);}returnbody;}asyncfunctionsubmitSunoGenerate(){constprompt="[Verse]\n清晨的键盘亮起光\n请求穿过云端的墙\n[Chorus]\n用一次实测把链路唱响\n从任务到结果都清清爽爽";consttask=awaitrequest("/v1/video/generations",{method:"POST",body:JSON.stringify({model:"suno/generate",prompt,metadata:{model_version:"V5_5",customMode:true,instrumental:false,prompt,style:"bright synth pop, short, clean vocals, Mandarin pop",title:"Crazyrouter Suno SDK Demo",},}),});consttaskId=task.data?.task_id??task.task_id??task.id;if(!taskId){thrownewError(`Missing task_id:${JSON.stringify(task)}`);}returntaskId;}asyncfunctionwaitForTask(taskId:string,timeoutMs=10*60*1000){constdeadline=Date.now()+timeoutMs;while(Date.now()<deadline){constresult=awaitrequest(`/v1/video/generations/${encodeURIComponent(taskId)}`);conststatus=String(result.data?.status??result.status??"").toLowerCase();console.log("status:",status);if(["succeeded","success","completed","done"].includes(status)){returnresult;}if(["failed","failure","error","cancelled","canceled"].includes(status)){thrownewError(`Task failed:${JSON.stringify(result)}`);}awaitnewPromise((resolve)=>setTimeout(resolve,8000));}thrownewError(`Task timeout:${taskId}`);}consttaskId=awaitsubmitSunoGenerate();console.log("taskId:",taskId);constresult=awaitwaitForTask(taskId);console.log("final:",result.data);7. Python 最小示例
Python 版本如下:
importosimporttimeimportrequests BASE_URL=os.getenv("CRAZYROUTER_BASE_URL","https://api.crazyrouter.com")API_KEY=os.getenv("CRAZYROUTER_API_KEY")ifnotAPI_KEY:raiseRuntimeError("Missing CRAZYROUTER_API_KEY")headers={"Authorization":f"Bearer{API_KEY}","Content-Type":"application/json","Accept":"application/json",}prompt="[Verse]\n清晨的键盘亮起光\n请求穿过云端的墙\n[Chorus]\n用一次实测把链路唱响\n从任务到结果都清清爽爽"submit=requests.post(f"{BASE_URL}/v1/video/generations",headers=headers,json={"model":"suno/generate","prompt":prompt,"metadata":{"model_version":"V5_5","customMode":True,"instrumental":False,"prompt":prompt,"style":"bright synth pop, short, clean vocals, Mandarin pop","title":"Crazyrouter Suno SDK Demo",},},timeout=120,)submit.raise_for_status()task=submit.json()task_id=task.get("data",{}).get("task_id")ortask.get("task_id")ortask.get("id")ifnottask_id:raiseRuntimeError(f"Missing task_id:{task}")print("task_id:",task_id)whileTrue:response=requests.get(f"{BASE_URL}/v1/video/generations/{task_id}",headers={"Authorization":f"Bearer{API_KEY}","Accept":"application/json"},timeout=120,)response.raise_for_status()data=response.json()status=str(data.get("data",{}).get("status","")).lower()print("status:",status)ifstatusin{"succeeded","success","completed","done"}:print("content url:",data.get("data",{}).get("url"))breakifstatusin{"failed","failure","error","cancelled","canceled"}:raiseRuntimeError(data)time.sleep(8)8. 这次实测暴露的两个坑
坑一:文档示例和当前生产校验可能不完全一致
文档里写的是:
{"model":"suno/generate","channel":"auto","input":{"prompt":"..."}}但我在2026-07-06 +08:00用两个域名实测,均返回:
{"code":"invalid_request","message":"prompt is required","data":null}兼容写法是:
{"model":"suno/generate","prompt":"...","metadata":{"prompt":"...","model_version":"V5_5"}}如果你上线前只照抄文档,很可能会卡在第一步。
坑二:succeeded 不代表公开 content URL 一定可下载
本次查询接口返回:
status=succeeded format=mp4 url=https://crazyrouter.com/v1/videos/task_ac90b23619334516/content但公开内容 URL 校验时:
HEAD crazyrouter.com content URL: 404 GET api.crazyrouter.com content URL + Authorization: 502继续追上游原始结果后,真实媒体地址是:
resultJson.data[0].audio_url直链下载成功:
HTTP 200 Content-Type: audio/mp3 Downloaded: 412326 bytes所以严谨的生产判断应该是:
submit 成功 task_id 存在 poll 到 succeeded content url 或 audio_url 返回 HTTP 200 Content-Type 是 audio/* 文件大小明显大于错误页 可播放或可被 ffprobe 识别9. 最小接入检查清单
第一次接入时,建议按这个清单排查:
CRAZYROUTER_API_KEY是否存在。- Base URL 是否是
https://api.crazyrouter.com或https://crazyrouter.com。 - endpoint 是否是
/v1/video/generations,不是/v1/chat/completions。 model是否明确传了suno/generate。- 是否有顶层
prompt。 model_version是否使用V5_5、V5、V4_5PLUS等文档列出的枚举值。- 是否拿到了
task_id。 - 是否轮询到终态。
- 是否验证了内容 URL 或
resultJson.data[0].audio_url。 - 是否记录了失败响应体和 request id。
10. FAQ
Q1:Suno SDK 可以用 OpenAI SDK 直接调吗?
不建议按client.audio.*或client.responses.*来理解。文档明确说这套suno/*当前属于 Crazyrouter 原生任务接口,应该走/v1/video/generations。
Q2:为什么是 video/generations,不是 audio/generations?
这是 Crazyrouter 当前统一任务接口命名。旧查询结果里返回format=mp4,但本次真实audio_url下载到的是audio/mp3。接口路径叫 video 不影响它作为音乐生成任务使用,最终格式要看实际媒体 URL 和Content-Type。
Q3:我应该用input还是metadata?
文档展示的是input。但我在2026-07-06的生产实测中,纯input写法返回prompt is required,顶层prompt + metadata写法可以提交成功。上线前建议你用自己的 API Key 对当前生产接口复测。
Q4:拿到succeeded后是不是就可以给用户展示下载按钮?
不建议。还要验证内容 URL 能否下载到真实媒体文件。本次实测是任务succeeded后,公开/content代理未拿到媒体文件;继续解析resultJson.data[0].audio_url后,真实音频文件可以下载。
最后总结
如果你只是想先跑通 Suno SDK,最小路径是:
POST /v1/video/generations model=suno/generate 顶层 prompt + metadata 拿 task_id 轮询 /v1/video/generations/{task_id} 继续验证 /v1/videos/{task_id}/content 或 resultJson.data[0].audio_url这篇的重点不是证明“所有能力都稳定”,而是把接入的第一步跑清楚:提交任务和查询任务可以跑通,但真正上线还必须做内容 URL 或audio_url校验。
下一篇我会专门讲异步任务轮询、内容下载、404/502 的排查方式,以及如何从resultJson.data[0].audio_url找到真正可下载的 Suno 结果。