Skip to content

ACE-Step OpenRouter API Documentation

OpenAI Chat Completions-compatible API for AI music generation

Base URL: http://{host}:{port} (default http://127.0.0.1:8002)


Table of Contents


Authentication

If the server is configured with an API key (via the OPENROUTER_API_KEY environment variable or --api-key CLI flag), all requests must include the following header:

Authorization: Bearer <your-api-key>

No authentication is required when no API key is configured.


Endpoints

1. Generate Music

POST /v1/chat/completions

Generates music from chat messages and returns audio data along with LM-generated metadata.

Request Parameters

FieldTypeRequiredDefaultDescription
modelstringNoautoModel ID (obtain from /v1/models)
messagesarrayYes-Chat message list. See Input Modes
streambooleanNofalseEnable streaming response. See Streaming Responses
audio_configobjectNonullAudio generation configuration. See below
temperaturefloatNo0.85LM sampling temperature
top_pfloatNo0.9LM nucleus sampling parameter
seedint | stringNonullRandom seed. When batch_size > 1, use comma-separated values, e.g. "42,123,456"
lyricsstringNo""Lyrics passed directly (takes priority over lyrics parsed from messages). When set, messages text becomes the prompt
sample_modebooleanNofalseEnable LLM sample mode. Messages text becomes sample_query for LLM to auto-generate prompt/lyrics
thinkingbooleanNofalseEnable LLM thinking mode for deeper reasoning
use_formatbooleanNofalseWhen user provides prompt/lyrics, enhance them via LLM formatting
use_cot_captionbooleanNotrueRewrite/enhance the music description via Chain-of-Thought
use_cot_languagebooleanNotrueAuto-detect vocal language via Chain-of-Thought
guidance_scalefloatNo7.0Classifier-free guidance scale
batch_sizeintNo1Number of audio samples to generate
task_typestringNo"text2music"Task type. See Audio Input
repainting_startfloatNo0.0Repaint region start position (seconds)
repainting_endfloatNonullRepaint region end position (seconds)
audio_cover_strengthfloatNo1.0Cover strength (0.0~1.0)

audio_config Object

FieldTypeDefaultDescription
durationfloatnullAudio duration in seconds. If omitted, determined automatically by the LM
bpmintegernullBeats per minute. If omitted, determined automatically by the LM
vocal_languagestring"en"Vocal language code (e.g. "zh", "en", "ja")
instrumentalbooleannullWhether to generate instrumental-only (no vocals). If omitted, auto-determined from lyrics
formatstring"mp3"Output audio format
key_scalestringnullMusical key (e.g. "C major")
time_signaturestringnullTime signature (e.g. "4/4")

Messages text meaning depends on the mode:

  • If lyrics is set → messages text = caption
  • If sample_mode: true is set → messages text = sample_query (let LLM generate everything)
  • Neither set → auto-detect: tags → tag mode, lyrics-like → lyrics mode, otherwise used as caption directly for generation

messages Format

Supports both plain text and multimodal (text + audio) formats:

Plain text:

json
{
  "messages": [
    {"role": "user", "content": "Your input content"}
  ]
}

Multimodal (with audio input):

json
{
  "messages": [
    {
      "role": "user",
      "content": [
        {"type": "text", "text": "Cover this song"},
        {
          "type": "input_audio",
          "input_audio": {
            "data": "<base64 audio data>",
            "format": "mp3"
          }
        }
      ]
    }
  ]
}

Non-Streaming Response (stream: false)

json
{
  "id": "chatcmpl-a1b2c3d4e5f6g7h8",
  "object": "chat.completion",
  "created": 1706688000,
  "model": "acemusic/acestep-v15-turbo",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "## Metadata\n**Caption:** Upbeat pop song...\n**BPM:** 120\n**Duration:** 30s\n**Key:** C major\n\n## Lyrics\n[Verse 1]\nHello world...",
        "audio": [
          {
            "type": "audio_url",
            "audio_url": {
              "url": "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAA..."
            }
          }
        ]
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 10,
    "completion_tokens": 100,
    "total_tokens": 110
  }
}

Response Fields:

FieldDescription
choices[0].message.contentText information generated by the LM, including Metadata (Caption/BPM/Duration/Key/Time Signature/Language) and Lyrics. Returns "Music generated successfully." if LM was not involved
choices[0].message.audioAudio data array. Each item contains type ("audio_url") and audio_url.url (Base64 Data URL in format data:audio/mpeg;base64,...)
choices[0].finish_reason"stop" indicates normal completion

Decoding Audio:

The audio_url.url value is a Data URL: data:audio/mpeg;base64,<base64_data>

Extract the base64 portion after the comma and decode it to get the MP3 file:

python
import base64

url = response["choices"][0]["message"]["audio"][0]["audio_url"]["url"]
# Strip the "data:audio/mpeg;base64," prefix
b64_data = url.split(",", 1)[1]
audio_bytes = base64.b64decode(b64_data)

with open("output.mp3", "wb") as f:
    f.write(audio_bytes)
javascript
const url = response.choices[0].message.audio[0].audio_url.url;
const b64Data = url.split(",")[1];
const audioBytes = atob(b64Data);
// Or use the Data URL directly in an <audio> element
const audio = new Audio(url);
audio.play();

2. List Models

GET /v1/models

Returns available model information.

Response

json
{
  "data": [
    {
      "id": "acemusic/acestep-v15-turbo",
      "name": "ACE-Step",
      "created": 1706688000,
      "description": "High-performance text-to-music generation model. Supports multiple styles, lyrics input, and various audio durations.",
      "input_modalities": ["text", "audio"],
      "output_modalities": ["audio", "text"],
      "context_length": 4096,
      "pricing": {"prompt": "0", "completion": "0", "request": "0"},
      "supported_sampling_parameters": ["temperature", "top_p"]
    }
  ]
}

3. Health Check

GET /health

Response

json
{
  "status": "ok",
  "service": "ACE-Step OpenRouter API",
  "version": "1.0"
}

Input Modes

The system automatically selects the input mode based on the content of the last user message. You can also explicitly specify via the lyrics or sample_mode fields.

Use <prompt> and <lyrics> tags to explicitly specify the music description and lyrics:

json
{
  "messages": [
    {
      "role": "user",
      "content": "<prompt>A gentle acoustic ballad in C major, female vocal</prompt>\n<lyrics>[Verse 1]\nSunlight through the window\nA brand new day begins\n\n[Chorus]\nWe are the dreamers\nWe are the light</lyrics>"
    }
  ],
  "audio_config": {
    "duration": 30,
    "vocal_language": "en"
  }
}
  • <prompt>...</prompt> — Music style/scene description (caption)
  • <lyrics>...</lyrics> — Lyrics content
  • Either tag can be used alone
  • When use_format: true, the LLM automatically enhances both prompt and lyrics

Mode 2: Natural Language Mode (Sample Mode)

Describe the desired music in natural language. The system uses LLM to generate the prompt and lyrics automatically:

json
{
  "messages": [
    {"role": "user", "content": "Generate an upbeat pop song about summer and travel"}
  ],
  "sample_mode": true,
  "audio_config": {
    "vocal_language": "en"
  }
}

Trigger condition: sample_mode: true, or message content contains no tags and does not resemble lyrics.

Mode 3: Lyrics-Only Mode

Pass in lyrics with structural markers directly. The system identifies them automatically:

json
{
  "messages": [
    {
      "role": "user",
      "content": "[Verse 1]\nWalking down the street\nFeeling the beat\n\n[Chorus]\nDance with me tonight\nUnder the moonlight"
    }
  ],
  "audio_config": {"duration": 30}
}

Trigger condition: Message content contains [Verse], [Chorus], or similar markers, or has a multi-line short-text structure.

Mode 4: Lyrics + Prompt Separation

Use the lyrics field to pass lyrics directly, and messages text automatically becomes the prompt:

json
{
  "messages": [
    {"role": "user", "content": "Energetic EDM with heavy bass drops"}
  ],
  "lyrics": "[Verse 1]\nFeel the rhythm in your soul\nLet the music take control\n\n[Drop]\n(instrumental break)",
  "audio_config": {
    "bpm": 128,
    "duration": 60
  }
}

Instrumental Mode

Set audio_config.instrumental: true:

json
{
  "messages": [
    {"role": "user", "content": "<prompt>Epic orchestral cinematic score, dramatic and powerful</prompt>"}
  ],
  "audio_config": {
    "instrumental": true,
    "duration": 30
  }
}

Audio Input

Audio files can be passed via multimodal messages (base64 encoded) for cover, repaint, and other tasks.

task_type Types

task_typeDescriptionAudio Input Required
text2musicText to music (default)Optional (as reference)
coverCover/style transferRequires src_audio
repaintPartial repaintRequires src_audio
legoAudio splicingRequires src_audio
extractAudio extractionRequires src_audio
completeAudio continuationRequires src_audio

Audio Routing Rules

Multiple input_audio blocks are routed to different parameters in order (similar to multi-image upload):

task_typeaudio[0]audio[1]
text2musicreference_audio (style reference)-
cover/repaint/lego/extract/completesrc_audio (audio to edit)reference_audio (optional style reference)

Audio Input Examples

Cover Task:

json
{
  "messages": [
    {
      "role": "user",
      "content": [
        {"type": "text", "text": "<prompt>Jazz style cover with saxophone</prompt>"},
        {
          "type": "input_audio",
          "input_audio": {"data": "<base64 source audio>", "format": "mp3"}
        }
      ]
    }
  ],
  "task_type": "cover",
  "audio_cover_strength": 0.8,
  "audio_config": {"duration": 30}
}

Repaint Task:

json
{
  "messages": [
    {
      "role": "user",
      "content": [
        {"type": "text", "text": "<prompt>Replace with guitar solo</prompt>"},
        {
          "type": "input_audio",
          "input_audio": {"data": "<base64 source audio>", "format": "mp3"}
        }
      ]
    }
  ],
  "task_type": "repaint",
  "repainting_start": 10.0,
  "repainting_end": 20.0,
  "audio_config": {"duration": 30}
}

Streaming Responses

Set "stream": true to enable SSE (Server-Sent Events) streaming.

Event Format

Each event starts with data: , followed by JSON, ending with a double newline \n\n:

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1706688000,"model":"acemusic/acestep-v15-turbo","choices":[{"index":0,"delta":{...},"finish_reason":null}]}

Streaming Event Sequence

PhaseDelta ContentDescription
1. Initialization{"role":"assistant","content":""}Establishes the connection
2. LM Content{"content":"\n\n## Metadata\n..."}Metadata and lyrics pushed after LM generation (if LM was used)
3. Heartbeat{"content":"."}Sent every 2 seconds during audio generation to keep the connection alive
4. Audio Data{"audio":[{"type":"audio_url","audio_url":{"url":"data:..."}}]}Audio base64 data
5. Finishfinish_reason: "stop"Generation complete
6. Terminationdata: [DONE]End-of-stream marker

Streaming Response Example

data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1706688000,"model":"acemusic/acestep-v15-turbo","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1706688000,"model":"acemusic/acestep-v15-turbo","choices":[{"index":0,"delta":{"content":"\n\n## Metadata\n**Caption:** Upbeat pop\n**BPM:** 120"},"finish_reason":null}]}

data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1706688000,"model":"acemusic/acestep-v15-turbo","choices":[{"index":0,"delta":{"content":"."},"finish_reason":null}]}

data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1706688000,"model":"acemusic/acestep-v15-turbo","choices":[{"index":0,"delta":{"audio":[{"type":"audio_url","audio_url":{"url":"data:audio/mpeg;base64,..."}}]},"finish_reason":null}]}

data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1706688000,"model":"acemusic/acestep-v15-turbo","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]

Client-Side Streaming Handling

python
import json
import httpx

with httpx.stream("POST", "http://127.0.0.1:8002/v1/chat/completions", json={
    "messages": [{"role": "user", "content": "Generate a cheerful guitar piece"}],
    "sample_mode": True,
    "stream": True,
    "audio_config": {"instrumental": True}
}) as response:
    content_parts = []
    audio_url = None

    for line in response.iter_lines():
        if not line or not line.startswith("data: "):
            continue
        if line == "data: [DONE]":
            break

        chunk = json.loads(line[6:])
        delta = chunk["choices"][0]["delta"]

        if "content" in delta and delta["content"]:
            content_parts.append(delta["content"])

        if "audio" in delta and delta["audio"]:
            audio_url = delta["audio"][0]["audio_url"]["url"]

        if chunk["choices"][0].get("finish_reason") == "stop":
            print("Generation complete!")

    print("Content:", "".join(content_parts))
    if audio_url:
        import base64
        b64_data = audio_url.split(",", 1)[1]
        with open("output.mp3", "wb") as f:
            f.write(base64.b64decode(b64_data))
javascript
const response = await fetch("http://127.0.0.1:8002/v1/chat/completions", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    messages: [{ role: "user", content: "Generate a cheerful guitar piece" }],
    sample_mode: true,
    stream: true,
    audio_config: { instrumental: true }
  })
});

const reader = response.body.getReader();
const decoder = new TextDecoder();
let audioUrl = null;
let content = "";

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  const text = decoder.decode(value);
  for (const line of text.split("\n")) {
    if (!line.startsWith("data: ") || line === "data: [DONE]") continue;

    const chunk = JSON.parse(line.slice(6));
    const delta = chunk.choices[0].delta;

    if (delta.content) content += delta.content;
    if (delta.audio) audioUrl = delta.audio[0].audio_url.url;
  }
}

// audioUrl can be used directly as <audio src="...">

Examples

Example 1: Natural Language Generation (Simplest Usage)

bash
curl -X POST http://127.0.0.1:8002/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {"role": "user", "content": "A soft folk song about hometown and memories"}
    ],
    "sample_mode": true,
    "audio_config": {"vocal_language": "en"}
  }'

Example 2: Tagged Mode with Specific Parameters

bash
curl -X POST http://127.0.0.1:8002/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {
        "role": "user",
        "content": "<prompt>Energetic EDM track with heavy bass drops and synth leads</prompt><lyrics>[Verse 1]\nFeel the rhythm in your soul\nLet the music take control\n\n[Drop]\n(instrumental break)</lyrics>"
      }
    ],
    "audio_config": {
      "bpm": 128,
      "duration": 60,
      "vocal_language": "en"
    }
  }'

Example 3: Instrumental with LM Enhancement Disabled

bash
curl -X POST http://127.0.0.1:8002/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {
        "role": "user",
        "content": "<prompt>Peaceful piano solo, slow tempo, jazz harmony</prompt>"
      }
    ],
    "use_cot_caption": false,
    "audio_config": {
      "instrumental": true,
      "duration": 45
    }
  }'

Example 4: Streaming Request

bash
curl -X POST http://127.0.0.1:8002/v1/chat/completions \
  -H "Content-Type: application/json" \
  -N \
  -d '{
    "messages": [
      {"role": "user", "content": "Generate a happy birthday song"}
    ],
    "sample_mode": true,
    "stream": true
  }'

Example 5: Multi-Seed Batch Generation

bash
curl -X POST http://127.0.0.1:8002/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {"role": "user", "content": "<prompt>Lo-fi hip hop beat</prompt>"}
    ],
    "batch_size": 3,
    "seed": "42,123,456",
    "audio_config": {
      "instrumental": true,
      "duration": 30
    }
  }'

Error Codes

HTTP StatusDescription
400Invalid request format or missing valid input
401Missing or invalid API key
429Service busy, queue full
500Internal error during music generation
503Model not yet initialized
504Generation timeout

Error response format:

json
{
  "detail": "Error description message"
}

Server Configuration (Environment Variables)

The following environment variables can be used to configure the server (for operations reference):

VariableDefaultDescription
OPENROUTER_API_KEYNoneAPI authentication key
OPENROUTER_HOST127.0.0.1Listen address
OPENROUTER_PORT8002Listen port
ACESTEP_CONFIG_PATHacestep-v15-turboDiT model configuration path
ACESTEP_DEVICEautoInference device
ACESTEP_LM_MODEL_PATHacestep-5Hz-lm-0.6BLLM model path
ACESTEP_LM_BACKENDvllmLLM inference backend
ACESTEP_QUEUE_MAXSIZE200Task queue max capacity
ACESTEP_GENERATION_TIMEOUT600Non-streaming request timeout (seconds)

Released under the MIT License.