OpenAI-Compatible API

August 26, 2026 · View on GitHub

This spec defines Lemonade's implementation of the OpenAI API.

MethodEndpointDescriptionModality
POST/v1/chat/completionsChat Completionsmessages -> completion
POST/v1/completionsText Completionsprompt -> completion
POST/v1/embeddingsEmbeddingstext -> vector representations
POST/v1/responsesResponses APIprompt/messages -> event
POST/v1/audio/transcriptionsAudio Transcriptionaudio file -> text
POST/v1/audio/speechText to speechtext -> audio
WS/realtimeRealtime Audio Transcription, OpenAI SDK compatiblestreaming audio -> text
POST/v1/images/generationsImage Generationprompt -> image
POST/v1/images/editsImage Editingimage + prompt -> edited image
POST/v1/images/variationsImage Variationsimage -> varied image
POST/v1/images/upscaleImage Upscalingimage + ESRGAN model -> upscaled image
GET/v1/modelsList models available locallyn/a
GET/v1/models/{model_id}Retrieve a specific model by IDn/a

POST /v1/chat/completions

Status

Chat Completions API. You provide a list of messages and receive a completion. This API will also load the model if it is not already loaded.

Parameters

ParameterRequiredDescriptionStatus
messagesYesArray of messages in the conversation. Each message should have a role ("user" or "assistant") and content (the message text).Status
modelYesThe model to use for the completion.Status
streamNoIf true, tokens will be sent as they are generated. If false, the response will be sent as a single message once complete. Defaults to false.Status
stopNoUp to 4 sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence. Can be a string or an array of strings.Status
logprobsNoInclude log probabilities of the output tokens. If true, returns the log probability of each output token. Defaults to false.Status
temperatureNoWhat sampling temperature to use.Status
repeat_penaltyNoNumber between 1.0 and 2.0. 1.0 means no penalty. Higher values discourage repetition.Status
top_kNoInteger that controls the number of top tokens to consider during sampling.Status
top_pNoFloat between 0.0 and 1.0 that controls the cumulative probability of top tokens to consider during nucleus sampling.Status
toolsNoA list of tools the model may call.Status
max_tokensNoAn upper bound for the number of tokens that can be generated for a completion. Mutually exclusive with max_completion_tokens. This value is now deprecated by OpenAI in favor of max_completion_tokensStatus
max_completion_tokensNoAn upper bound for the number of tokens that can be generated for a completion. Mutually exclusive with max_tokens.Status

Example request

=== "PowerShell"

```powershell
Invoke-WebRequest `
  -Uri "http://localhost:13305/v1/chat/completions" `
  -Method POST `
  -Headers @{ "Content-Type" = "application/json" } `
  -Body '{
    "model": "Qwen3-0.6B-GGUF",
    "messages": [
      {
        "role": "user",
        "content": "What is the population of Paris?"
      }
    ],
    "stream": false
  }'
```

=== "Bash"

```bash
curl -X POST http://localhost:13305/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
        "model": "Qwen3-0.6B-GGUF",
        "messages": [
          {"role": "user", "content": "What is the population of Paris?"}
        ],
        "stream": false
      }'
```

Response format

=== "Non-streaming responses"

```json
{
  "id": "0",
  "object": "chat.completion",
  "created": 1742927481,
  "model": "Qwen3-0.6B-GGUF",
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "Paris has a population of approximately 2.2 million people in the city proper."
    },
    "finish_reason": "stop"
  }]
}
```

=== "Streaming responses" For streaming responses, the API returns a stream of server-sent events (however, Open AI recommends using their streaming libraries for parsing streaming responses):

```json
{
  "id": "0",
  "object": "chat.completion.chunk",
  "created": 1742927481,
  "model": "Qwen3-0.6B-GGUF",
  "choices": [{
    "index": 0,
    "delta": {
      "role": "assistant",
      "content": "Paris"
    }
  }]
}
```

Image understanding input format (OpenAI-compatible)

To send images to chat/completions, pass a messages[*].content array that mixes text and image_url items. The image can be provided as a base64 data URL (for example, from FileReader.readAsDataURL(...) in web apps).

Example request

curl -X POST http://localhost:13305/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
        "model": "Qwen2.5-VL-7B-Instruct",
        "messages": [
          {
            "role": "user",
            "content": [
              {"type": "text", "text": "What is in this image?"},
              {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."}}
            ]
          }
        ],
        "stream": false
      }'

Example response

{
  "id": "0",
  "object": "chat.completion",
  "created": 1742927481,
  "model": "Qwen2.5-VL-7B-Instruct",
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "The image shows a red apple resting on a wooden table."
    },
    "finish_reason": "stop"
  }]
}

Server-side tools

Note: Omni collection orchestration is a Lemonade-specific extension to the Chat Completions API; it is not part of the OpenAI specification. Requests that target ordinary (non-collection) models are unaffected and are fully OpenAI-compatible.

When model names an Omni collection model (recipe: "collection.omni", e.g., LMX-Omni-52B-Halo), this endpoint runs an internal tool-calling loop instead of a plain completion. The server injects the reference system prompt and tools, routes to the collection's chat component, executes the omni tools (image generation/editing, text-to-speech) against the matching components, and returns one OpenAI-compatible response. Generated media is embedded in the assistant content:

  • images → markdown ![generated image](data:image/png;base64,…)
  • speech<audio>data:audio/mpeg;base64,…</audio>

Both non-streaming and stream: true are supported. In streaming mode the media arrives as a content delta on a chat.completion.chunk frame the moment its tool finishes.

Merge semantics. A client-provided system prompt is prepended by the built-in omni system prompt. Client-provided tools are merged with the built-in omni tools. The server resolves omni tool calls internally; calls to client-provided tools are returned in a finish_reason: "tool_calls" response for the client to execute and resume. Targeting a collection name invokes the server-side loop; targeting a component LLM name bypasses it and returns a plain completion. See Lemonade Omni Models for details.

Example request

curl -X POST http://localhost:13305/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
        "model": "LMX-Omni-52B-Halo",
        "messages": [{"role": "user", "content": "Draw a red apple on a table."}],
        "stream": false
      }'

Example response

The image tool runs during the loop and its output is embedded as a markdown image in the assistant content:

{
  "id": "0",
  "object": "chat.completion",
  "created": 1742927481,
  "model": "LMX-Omni-52B-Halo",
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "Here is a red apple on a table.\n\n![generated image](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...)"
    },
    "finish_reason": "stop"
  }]
}

POST /v1/completions

Status

Text Completions API. You provide a prompt and receive a completion. This API will also load the model if it is not already loaded.

Parameters

ParameterRequiredDescriptionStatus
promptYesThe prompt to use for the completion.Status
modelYesThe model to use for the completion.Status
streamNoIf true, tokens will be sent as they are generated. If false, the response will be sent as a single message once complete. Defaults to false.Status
stopNoUp to 4 sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence. Can be a string or an array of strings.Status
echoNoEcho back the prompt in addition to the completion. Available on non-streaming mode.Status
logprobsNoInclude log probabilities of the output tokens. If true, returns the log probability of each output token. Defaults to false. Only available when stream=False.Status
temperatureNoWhat sampling temperature to use.Status
repeat_penaltyNoNumber between 1.0 and 2.0. 1.0 means no penalty. Higher values discourage repetition.Status
top_kNoInteger that controls the number of top tokens to consider during sampling.Status
top_pNoFloat between 0.0 and 1.0 that controls the cumulative probability of top tokens to consider during nucleus sampling.Status
max_tokensNoAn upper bound for the number of tokens that can be generated for a completion, including input tokens.Status

Example request

=== "PowerShell"

```powershell
Invoke-WebRequest -Uri "http://localhost:13305/v1/completions" `
  -Method POST `
  -Headers @{ "Content-Type" = "application/json" } `
  -Body '{
    "model": "Qwen3-0.6B-GGUF",
    "prompt": "What is the population of Paris?",
    "stream": false
  }'
```

=== "Bash"

```bash
curl -X POST http://localhost:13305/v1/completions \
  -H "Content-Type: application/json" \
  -d '{
        "model": "Qwen3-0.6B-GGUF",
        "prompt": "What is the population of Paris?",
        "stream": false
      }'
```

Response format

The following format is used for both streaming and non-streaming responses:

{
  "id": "0",
  "object": "text_completion",
  "created": 1742927481,
  "model": "Qwen3-0.6B-GGUF",
  "choices": [{
    "index": 0,
    "text": "Paris has a population of approximately 2.2 million people in the city proper.",
    "finish_reason": "stop"
  }],
}

POST /v1/embeddings

Status

Embeddings API. You provide input text and receive vector representations (embeddings) that can be used for semantic search, clustering, and similarity comparisons. This API will also load the model if it is not already loaded.

Note: This endpoint is only available for models using the llamacpp or flm recipes. ONNX models (OGA recipes) do not support embeddings.

Parameters

ParameterRequiredDescriptionStatus
inputYesThe input text or array of texts to embed. Can be a string or an array of strings.Status
modelYesThe model to use for generating embeddings.Status
encoding_formatNoThe format to return embeddings in. Supported values: "float" (default), "base64".Status

Example request

=== "PowerShell"

```powershell
Invoke-WebRequest `
  -Uri "http://localhost:13305/v1/embeddings" `
  -Method POST `
  -Headers @{ "Content-Type" = "application/json" } `
  -Body '{
    "model": "nomic-embed-text-v1-GGUF",
    "input": ["Hello, world!", "How are you?"],
    "encoding_format": "float"
  }'
```

=== "Bash"

```bash
curl -X POST http://localhost:13305/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{
        "model": "nomic-embed-text-v1-GGUF",
        "input": ["Hello, world!", "How are you?"],
        "encoding_format": "float"
      }'
```

Response format

{
  "object": "list",
  "data": [
    {
      "object": "embedding",
      "index": 0,
      "embedding": [0.0234, -0.0567, 0.0891, ...]
    },
    {
      "object": "embedding",
      "index": 1,
      "embedding": [0.0456, -0.0678, 0.1234, ...]
    }
  ],
  "model": "nomic-embed-text-v1-GGUF",
  "usage": {
    "prompt_tokens": 12,
    "total_tokens": 12
  }
}

Field Descriptions:

  • object - Type of response object, always "list"
  • data - Array of embedding objects
    • object - Type of embedding object, always "embedding"
    • index - Index position of the input text in the request
    • embedding - Vector representation as an array of floats
  • model - Model identifier used to generate the embeddings
  • usage - Token usage statistics
    • prompt_tokens - Number of tokens in the input
    • total_tokens - Total tokens processed

POST /v1/responses

Status

Responses API. You provide an input and receive a response. This API will also load the model if it is not already loaded.

Parameters

ParameterRequiredDescriptionStatus
inputYesA list of dictionaries or a string input for the model to respond to.Status
modelYesThe model to use for the response.Status
max_output_tokensNoThe maximum number of output tokens to generate.Status
temperatureNoWhat sampling temperature to use.Status
repeat_penaltyNoNumber between 1.0 and 2.0. 1.0 means no penalty. Higher values discourage repetition.Status
top_kNoInteger that controls the number of top tokens to consider during sampling.Status
top_pNoFloat between 0.0 and 1.0 that controls the cumulative probability of top tokens to consider during nucleus sampling.Status
streamNoIf true, tokens will be sent as they are generated. If false, the response will be sent as a single message once complete. Defaults to false.Status

Streaming Events

The Responses API uses semantic events for streaming. Each event is typed with a predefined schema, so you can listen for events you care about. Our initial implementation only offers support to:

  • response.created
  • response.output_text.delta
  • response.completed

For a full list of event types, see the API reference for streaming.

Example request

=== "PowerShell"

```powershell
Invoke-WebRequest -Uri "http://localhost:13305/v1/responses" `
  -Method POST `
  -Headers @{ "Content-Type" = "application/json" } `
  -Body '{
    "model": "Llama-3.2-1B-Instruct-Hybrid",
    "input": "What is the population of Paris?",
    "stream": false
  }'
```

=== "Bash"

```bash
curl -X POST http://localhost:13305/v1/responses \
  -H "Content-Type: application/json" \
  -d '{
        "model": "Llama-3.2-1B-Instruct-Hybrid",
        "input": "What is the population of Paris?",
        "stream": false
      }'
```

Response format

=== "Non-streaming responses"

```json
{
  "id": "0",
  "created_at": 1746225832.0,
  "model": "Llama-3.2-1B-Instruct-Hybrid",
  "object": "response",
  "output": [{
    "id": "0",
    "content": [{
      "annotations": [],
      "text": "Paris has a population of approximately 2.2 million people in the city proper."
    }]
  }]
}
```

=== "Streaming Responses" For streaming responses, the API returns a series of events. Refer to OpenAI streaming guide for details.

POST /v1/audio/transcriptions

Status

Audio Transcription API. You provide an audio file and receive a text transcription. This API will also load the model if it is not already loaded.

Note: This endpoint uses whisper.cpp as the backend. Whisper models are automatically downloaded when first used.

Limitations: Only wav audio input is currently supported. The response_format field supports json, verbose_json, text, srt, and vtt. On the FastFlowLM (FLM) backend, srt and vtt are rejected with a 400 because FLM returns no segment timestamps, and verbose_json returns the compact shape without a segments field.

Parameters

ParameterRequiredDescriptionStatus
fileYesThe audio file to transcribe. Supported formats: wav.Status
modelYesThe Whisper model to use for transcription (e.g., Whisper-Tiny, Whisper-Base, Whisper-Small).Status
languageNoThe language of the audio (ISO 639-1 code, e.g., en, es, fr). Defaults to auto, which tells whisper.cpp to detect the source language instead of using whisper-server's English default.Status
response_formatNoThe response format. Supported values: json, verbose_json, text, srt, vtt. srt and vtt require a backend that reports segment timestamps (whisper.cpp).Status

Example request

=== "Windows"

```bash
curl -X POST http://localhost:13305/v1/audio/transcriptions ^
  -F "file=@C:\path\to\audio.wav" ^
  -F "model=Whisper-Tiny"
```

=== "Linux"

```bash
curl -X POST http://localhost:13305/v1/audio/transcriptions \
  -F "file=@/path/to/audio.wav" \
  -F "model=Whisper-Tiny"
```

Response format

{
  "text": "Hello, this is a sample transcription of the audio file."
}

Field Descriptions:

  • text - The transcribed text from the audio file

WS /realtime

Status

Realtime Audio Transcription API via WebSocket (OpenAI SDK compatible). Stream audio from a microphone and receive transcriptions in real-time with Voice Activity Detection (VAD).

Limitations: Only 16kHz mono PCM16 audio format is supported. Uses the same Whisper models as the HTTP transcription endpoint.

Connection

WebSocket upgrades are accepted directly on the main HTTP port (default 13305) — the same port as all REST endpoints. Connect with the model name:

ws://localhost:13305/v1/realtime?model=Whisper-Tiny

Accepted paths are /realtime and /logs/stream, bare or under any of the standard prefixes (/v1, /v0, /api/v1, /api/v0), so OpenAI Realtime SDK clients (/v1/realtime) connect as-is. When LEMONADE_API_KEY is set, pass ?api_key=KEY as a query parameter.

A dedicated WebSocket port also remains for backward compatibility; it is OS-assigned and reported by the /v1/health endpoint (websocket_port field). New clients should prefer the main port:

ws://localhost:<websocket_port>/realtime?model=Whisper-Tiny

Upon connection, the server sends a session.created message with a session ID.

Client → Server Messages

Message TypeDescription
session.updateConfigure the session (set model, VAD settings, or disable turn detection)
input_audio_buffer.appendSend audio data (base64-encoded PCM16)
input_audio_buffer.commitForce transcription of buffered audio
input_audio_buffer.clearClear audio buffer without transcribing

Server → Client Messages

Message TypeDescription
session.createdSession established, contains session ID
session.updatedSession configuration updated
input_audio_buffer.speech_startedVAD detected speech start
input_audio_buffer.speech_stoppedVAD detected speech end, transcription triggered
input_audio_buffer.committedAudio buffer committed for transcription
input_audio_buffer.clearedAudio buffer cleared
conversation.item.input_audio_transcription.deltaInterim/partial transcription (replaceable)
conversation.item.input_audio_transcription.completedFinal transcription result
errorError message

Example: Configure Session

{
  "type": "session.update",
  "session": {
    "model": "Whisper-Tiny"
  }
}

Example: Send Audio

{
  "type": "input_audio_buffer.append",
  "audio": "<base64-encoded PCM16 audio>"
}

Audio should be:

  • 16kHz sample rate
  • Mono (single channel)
  • 16-bit signed integer (PCM16)
  • Base64 encoded
  • Sent in chunks (~85ms recommended)

Example: Transcription Result

{
  "type": "conversation.item.input_audio_transcription.completed",
  "transcript": "Hello, this is a test transcription."
}

VAD Configuration

VAD settings can be configured via session.update:

{
  "type": "session.update",
  "session": {
    "model": "Whisper-Tiny",
    "turn_detection": {
      "threshold": 0.01,
      "silence_duration_ms": 800,
      "prefix_padding_ms": 250
    }
  }
}
ParameterDefaultDescription
threshold0.01RMS energy threshold for speech detection
silence_duration_ms800Silence duration to trigger speech end
prefix_padding_ms250Minimum speech duration before triggering

Set turn_detection to null to disable server-side VAD and use explicit commits instead:

{
  "type": "session.update",
  "session": {
    "model": "Whisper-Tiny",
    "turn_detection": null
  }
}

Code Examples

A complete, runnable example:

# Stream from microphone
python examples/realtime_transcription.py --model Whisper-Tiny

Integration Notes

  • Audio Format: Server expects 16kHz mono PCM16. Higher sample rates must be downsampled client-side.
  • Chunk Size: Send audio in ~85-256ms chunks for optimal latency/efficiency.
  • VAD Behavior: Server automatically detects speech boundaries and triggers transcription on speech end.
  • Manual Commit: Set turn_detection to null, then use input_audio_buffer.commit to force transcription. In this mode the server buffers audio but does not emit VAD or interim transcription events.
  • Clear Buffer: Use input_audio_buffer.clear to discard audio without transcribing.
  • Chunking: We are still tuning the chunking to balance latency vs. accuracy.
  • Migrating off the dedicated port: Clients that discover websocket_port via /v1/health and connect there can switch to ws://HOST:13305/v1/realtime?model=... — the protocol (events, audio format, auth) is identical on both ports, so it is a URL change only. This also simplifies remote setups (one port to expose) and works through reverse proxies that pass Upgrade: websocket. Keep the websocket_port fallback only if you must support servers older than this release.

POST /v1/images/generations

Status

Image Generation API. You provide a text prompt and receive a generated image. This API uses stable-diffusion.cpp as the backend.

Note: Image generation uses Stable Diffusion models. Available models include SD-Turbo (fast, ~4 steps), SDXL-Turbo, SD-1.5, and SDXL-Base-1.0.

Performance: CPU inference takes ~4-5 minutes per image. GPU (Vulkan) is faster but may have compatibility issues with some hardware.

Parameters

ParameterRequiredDescriptionStatus
promptYesThe text description of the image to generate.Status
modelYesThe Stable Diffusion model to use (e.g., SD-Turbo, SDXL-Turbo).Status
sizeNoThe size of the generated image. Format: WIDTHxHEIGHT (e.g., 512x512, 256x256). Default: 512x512.Status
nNoNumber of images to generate. Currently only 1 is supported.Status
response_formatNoFormat of the response. Only b64_json (base64-encoded image) is supported.Status
stepsNoNumber of inference steps. SD-Turbo works well with 4 steps. Default varies by model.Status
cfg_scaleNoClassifier-free guidance scale. SD-Turbo uses low values (~1.0). Default varies by model.Status
seedNoRandom seed for reproducibility. If not specified, a random seed is used.Status

Example request

=== "Bash"

```bash
curl -X POST http://localhost:13305/v1/images/generations \
  -H "Content-Type: application/json" \
  -d '{
        "model": "SD-Turbo",
        "prompt": "A serene mountain landscape at sunset",
        "size": "512x512",
        "steps": 4,
        "response_format": "b64_json"
      }'
```

POST /v1/images/edits

Status

Image Editing API. You provide a source image and a text prompt describing the desired change, and receive an edited image. This API uses stable-diffusion.cpp as the backend.

Note: This endpoint accepts multipart/form-data requests (not JSON). Use editing-capable models such as Flux-2-Klein-4B or SD-Turbo.

Performance: CPU inference takes several minutes per image. GPU (ROCm) is significantly faster.

Parameters

ParameterRequiredDescriptionStatus
modelYesThe Stable Diffusion model to use (e.g., Flux-2-Klein-4B, SD-Turbo).Status
image / image[]YesThe source image file to edit (PNG). Sent as a file in multipart/form-data.Status
promptYesA text description of the desired edit.Status
maskNoAn optional mask image (PNG). White areas indicate regions to edit; black areas are preserved.Status
sizeNoThe size of the output image. Format: WIDTHxHEIGHT (e.g., 512x512). Default: 512x512.Status
nNoNumber of images to generate. Allowed range: 110. Default: 1. Values outside this range are rejected with 400 Bad Request.Status
response_formatNoFormat of the response. Only b64_json (base64-encoded image) is supported.Status
stepsNoNumber of inference steps. Default varies by model.Status
cfg_scaleNoClassifier-free guidance scale. Default varies by model.Status
seedNoRandom seed for reproducibility.Status
userNoOpenAI API compatibility field. Accepted but not forwarded to the backend.Status
backgroundNoOpenAI API compatibility field. Accepted but not forwarded to the backend.Status
qualityNoOpenAI API compatibility field. Accepted but not forwarded to the backend.Status
input_fidelityNoOpenAI API compatibility field. Accepted but not forwarded to the backend.Status
output_compressionNoOpenAI API compatibility field. Accepted; silently ignored by the backend.Status

Example request

=== "Bash"

```bash
curl -X POST http://localhost:13305/v1/images/edits \
  -F "model=Flux-2-Klein-4B" \
  -F "prompt=Add a red barn and mountains in the background, photorealistic" \
  -F "size=512x512" \
  -F "n=1" \
  -F "response_format=b64_json" \
  -F "image=@/path/to/source_image.png"
```

=== "Python (OpenAI client)"

```python
from openai import OpenAI
client = OpenAI(base_url="http://localhost:13305/api/v1", api_key="not-needed")
with open("source_image.png", "rb") as image_file:
    response = client.images.edit(
        model="Flux-2-Klein-4B",
        image=image_file,
        prompt="Add a red barn and mountains in the background, photorealistic",
        size="512x512",
    )
import base64
image_data = base64.b64decode(response.data[0].b64_json)
open("edited_image.png", "wb").write(image_data)
```

POST /v1/images/variations

Status

Image Variations API. You provide a source image and receive a variation of it. This API uses stable-diffusion.cpp as the backend.

Note: This endpoint accepts multipart/form-data requests (not JSON). Unlike /images/edits, a prompt parameter is not supported and will be ignored — the model generates a variation based solely on the input image.

Performance: CPU inference takes several minutes per image. GPU (ROCm) is significantly faster.

Parameters

ParameterRequiredDescriptionStatus
modelYesThe Stable Diffusion model to use (e.g., Flux-2-Klein-4B, SD-Turbo).Status
imageYesThe source image file (PNG). Sent as a file in multipart/form-data.Status
sizeNoThe size of the output image. Format: WIDTHxHEIGHT (e.g., 512x512). Default: 512x512.Status
nNoNumber of variations to generate. Integer between 1 and 10 inclusive. Default: 1. Values outside this range result in a 400 Bad Request error.Status
response_formatNoFormat of the response. Only b64_json (base64-encoded image) is supported.Status
userNoOpenAI API compatibility field. Accepted but not forwarded to the backend.Status

Example request

=== "Bash"

```bash
curl -X POST http://localhost:13305/v1/images/variations \
  -F "model=Flux-2-Klein-4B" \
  -F "size=512x512" \
  -F "n=1" \
  -F "response_format=b64_json" \
  -F "image=@/path/to/source_image.png"
```

=== "Python (OpenAI client)"

```python
from openai import OpenAI
client = OpenAI(base_url="http://localhost:13305/api/v1", api_key="not-needed")
with open("source_image.png", "rb") as image_file:
    response = client.images.create_variation(
        model="Flux-2-Klein-4B",
        image=image_file,
        size="512x512",
        n=1,
    )
import base64
image_data = base64.b64decode(response.data[0].b64_json)
open("variation.png", "wb").write(image_data)
```

POST /v1/images/upscale

Status

Image Upscaling API. You provide a base64-encoded image and a Real-ESRGAN model name, and receive a 4x upscaled image. This API uses the sd-cli binary from stable-diffusion.cpp to perform super-resolution.

Note: Available upscale models are RealESRGAN-x4plus (general-purpose, 64 MB) and RealESRGAN-x4plus-anime (optimized for anime-style art, 17 MB). Both produce a 4x resolution increase (e.g., 256x256 → 1024x1024).

Note: Unlike /images/edits and /images/variations, this endpoint accepts a JSON body (not multipart/form-data). The image must be provided as a base64-encoded string.

Parameters

ParameterRequiredDescriptionStatus
imageYesBase64-encoded PNG image to upscale.Status
modelYesThe ESRGAN model to use (e.g., RealESRGAN-x4plus, RealESRGAN-x4plus-anime).Status

Example request

A typical workflow is to generate an image first, then upscale it:

=== "Bash"

```bash
# Step 1: Generate an image and save the base64 response
RESPONSE=$(curl -s -X POST http://localhost:13305/v1/images/generations \
  -H "Content-Type: application/json" \
  -d '{
        "model": "SD-Turbo",
        "prompt": "A serene mountain landscape at sunset",
        "size": "512x512",
        "steps": 4,
        "response_format": "b64_json"
      }')

# Step 2: Build the upscale JSON payload and pipe it to curl via stdin
# (base64 images are too large for command-line interpolation)
echo "$RESPONSE" | python3 -c "
import sys, json
b64 = json.load(sys.stdin)['data'][0]['b64_json']
print(json.dumps({'image': b64, 'model': 'RealESRGAN-x4plus'}))
" | curl -X POST http://localhost:13305/v1/images/upscale \
  -H "Content-Type: application/json" \
  -d @-
```

=== "PowerShell"

```powershell
# Step 1: Generate an image
$genResponse = Invoke-WebRequest `
  -Uri "http://localhost:13305/v1/images/generations" `
  -Method POST `
  -Headers @{ "Content-Type" = "application/json" } `
  -Body '{
    "model": "SD-Turbo",
    "prompt": "A serene mountain landscape at sunset",
    "size": "512x512",
    "steps": 4,
    "response_format": "b64_json"
  }'

# Step 2: Extract the base64 image
$imageB64 = ($genResponse.Content | ConvertFrom-Json).data[0].b64_json

# Step 3: Upscale the image with Real-ESRGAN
$body = @{ image = $imageB64; model = "RealESRGAN-x4plus" } | ConvertTo-Json
Invoke-WebRequest `
  -Uri "http://localhost:13305/v1/images/upscale" `
  -Method POST `
  -Headers @{ "Content-Type" = "application/json" } `
  -Body $body
```

=== "Python (requests)"

```python
import requests
import base64

BASE_URL = "http://localhost:13305/api/v1"

# Step 1: Generate an image
gen_response = requests.post(f"{BASE_URL}/images/generations", json={
    "model": "SD-Turbo",
    "prompt": "A serene mountain landscape at sunset",
    "size": "512x512",
    "steps": 4,
    "response_format": "b64_json",
})
image_b64 = gen_response.json()["data"][0]["b64_json"]

# Step 2: Upscale the image with Real-ESRGAN (512x512 -> 2048x2048)
upscale_response = requests.post(f"{BASE_URL}/images/upscale", json={
    "image": image_b64,
    "model": "RealESRGAN-x4plus",
})

# Step 3: Save the upscaled image to a file
upscaled_b64 = upscale_response.json()["data"][0]["b64_json"]
with open("upscaled.png", "wb") as f:
    f.write(base64.b64decode(upscaled_b64))
```

Response format

{
  "created": 1742927481,
  "data": [
    {
      "b64_json": "<base64-encoded upscaled PNG>"
    }
  ]
}

Field Descriptions:

  • created - Unix timestamp of when the upscaled image was generated
  • data - Array containing the upscaled image
    • b64_json - Base64-encoded PNG of the upscaled image

Error responses

Status CodeConditionExample
400Missing image field{"error": {"message": "Missing 'image' field (base64 encoded)", "type": "invalid_request_error"}}
400Missing model field{"error": {"message": "Missing 'model' field", "type": "invalid_request_error"}}
404Unknown model name{"error": {"message": "Upscale model not found: bad-model", "type": "invalid_request_error"}}
500Upscale failed{"error": {"message": "ESRGAN upscale failed", "type": "server_error"}}

POST /v1/audio/speech

Status

Speech Generation API. You provide a text input and receive an audio file. Which engine serves the request depends on the model.

Note: Supported models are kokoro-v1 (fixed voices, Kokoros backend) and the OpenMOSS family — OpenMOSS-TTS and MOSS-TTS-Local support cloning and integrated voice design. MOSS-VoiceGen remains available as a legacy compatibility model while existing GUI/settings paths migrate to the integrated design flow.

Limitations: Which response_format values are accepted depends on the model's backend: kokoro-v1 encodes mp3, wav, opus, and pcm; OpenMOSS v0.3 encodes buffered wav or pcm. Native streaming is narrower for both backends and uses pcm only, so an explicit non-PCM response_format on a streaming request is rejected rather than mislabeled or silently transcoded. OpenMOSS raw PCM is returned as audio/pcm with X-MOSS-Sample-Rate and X-MOSS-Channels, because its native format is model-dependent (24 kHz mono for OpenMOSS-TTS and 48 kHz stereo for MOSS-TTS-Local).

Parameters

ParameterRequiredDescriptionStatus
inputYesThe text to speak.Status
modelYesThe model to use (e.g., kokoro-v1).Status
speedNoSpeaking speed. Default: 1.0.Status
voiceNoThe voice to use. All OpenAI-defined voices can be used (alloy, ash, ...), as well as those defined by the kokoro model (af_sky, am_echo, ...). Default: shimmerStatus
voice (OpenMOSS)NoFor OpenMOSS models the field is a free-text voice/style instruction instead of a fixed voice name (e.g. a calm, deep male narrator voice).Status
reference_wav_b64NoLemonade extension (OpenMOSS voice cloning): base64-encoded WAV sample of the voice to clone.Status
voice_design_descriptionNoLemonade extension (OpenMOSS voice design): a description of the voice to invent, e.g. a warm low female voice with a British accent. Lemonade renders a short sample in that voice and uses it as the reference, so the effect is the same as supplying reference_wav_b64 yourself. Ignored when reference_wav_b64 is also present. Design is opt-in through this field only — voice never triggers it.Status
response_formatNoContainer for the returned audio. Which values are accepted depends on the model's backend (see Limitations above). Default: mp3 when buffered and pcm when streaming, falling back to the backend's first supported format when it cannot encode that default.Status
stream_formatNoIf set, the response is streamed. Only audio is supported. This selects the transport only — the container still comes from response_format, and an explicit one is honored on both transports. Default: not setStatus

Example request

=== "Bash"

```bash
curl -X POST http://localhost:13305/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{
        "model": "kokoro-v1",
        "input": "Lemonade can speak!",
        "speed": 1.0,
        "steps": 4,
        "response_format": "mp3"
      }'
```

Response format

The generated audio file is returned as-is.

GET /v1/models

Status

Returns a list of models available on the server in an OpenAI-compatible format. Each model object includes extended fields like checkpoint, recipe, size, downloaded, labels, context_length, and, when known, max_context_window.

By default, only models available locally (downloaded) are shown, matching OpenAI API behavior.

When lemond is configured with cloud providers, cloud-routed models appear here alongside local ones with recipe: "cloud" and a cloud_provider field. They are dot-namespaced by provider (e.g. fireworks.kimi-k2p5) and accept the standard chat-completions / completions requests below — see Cloud Offload.

Parameters

ParameterRequiredDescription
show_allNoIf set to true, returns all models from the catalog including those not yet downloaded. Defaults to false.

Example request

# Show only downloaded models (OpenAI-compatible)
curl http://localhost:13305/v1/models

# Show all models including not-yet-downloaded (extended usage)
curl http://localhost:13305/v1/models?show_all=true

Response format

{
  "object": "list",
  "data": [
    {
      "id": "Qwen3-0.6B-GGUF",
      "created": 1744173590,
      "object": "model",
      "owned_by": "lemonade",
      "checkpoint": "unsloth/Qwen3-0.6B-GGUF:Q4_0",
      "recipe": "llamacpp",
      "size": 0.38,
      "max_context_window": 40960,
      "context_length": 8192,
      "downloaded": true,
      "suggested": true,
      "update_available": false,
      "labels": ["reasoning"]
    },
    {
      "id": "Gemma-3-4b-it-GGUF",
      "created": 1744173590,
      "object": "model",
      "owned_by": "lemonade",
      "checkpoint": "ggml-org/gemma-3-4b-it-GGUF:Q4_K_M",
      "recipe": "llamacpp",
      "size": 3.61,
      "downloaded": true,
      "suggested": true,
      "labels": ["hot", "vision"]
    },
    {
      "id": "SD-Turbo",
      "created": 1744173590,
      "object": "model",
      "owned_by": "lemonade",
      "checkpoint": "stabilityai/sd-turbo:sd_turbo.safetensors",
      "recipe": "sd-cpp",
      "size": 5.2,
      "downloaded": true,
      "suggested": true,
      "labels": ["image"],
      "image_defaults": {
        "steps": 4,
        "cfg_scale": 1.0,
        "width": 512,
        "height": 512
      }
    }
  ]
}

Field Descriptions:

  • object - Type of response object, always "list"
  • data - Array of model objects with the following fields:
    • id - Model identifier (used for loading and inference requests)
    • created - Unix timestamp of when the model entry was created
    • object - Type of object, always "model"
    • owned_by - Owner of the model, always "lemonade"
    • checkpoint - Full checkpoint identifier on Hugging Face
    • recipe - Backend/device recipe used to load the model (e.g., "ryzenai-llm", "llamacpp", "flm")
    • size - Model size in GB (omitted for models without size information)
    • max_context_window - Optional integer indicating the maximum model-supported text context discovered from local static metadata. Currently populated for downloaded GGUF/llama.cpp models and installed FLM text-context models.
    • context_length - Number of tokens the model can handle in one request. Uses the loaded value when the model is running and the configured ctx_size otherwise (omitted when neither is known).
    • downloaded - Boolean indicating if the model is downloaded and available locally
    • update_available - Boolean indicating a newer commit exists on HuggingFace for this model. Only set for downloaded HF-backed models. false otherwise.
    • suggested - Boolean indicating if the model is recommended for general use
    • labels - Array of tags describing the model's capabilities and characteristics. See Model Labels for the full list.
    • image_defaults - (Image models only) Default generation parameters for the model:
      • steps - Number of inference steps (e.g., 4 for turbo models, 20 for standard models)
      • cfg_scale - Classifier-free guidance scale (e.g., 1.0 for turbo models, 7.5 for standard models)
      • width - Default image width in pixels
      • height - Default image height in pixels
    • components - (Omni collections only, recipe: "collection.omni") Ordered array of the component model names that make up the collection
    • models - (Omni collections only) Ordered array embedding each component's full model object (same shape as the entries in this list), parallel to components. This makes a collection's /v1/models/{model_id} response self-contained — exporting it produces a file that can be imported elsewhere via /v1/pull

Model Labels

Labels describe what a model can do. A model may carry multiple labels.

Deployment labels — determine which backend endpoint the model is routed to. Every model names exactly one deployment mode, and a model is never given two labels that name different modes:

LabelEndpointDescription
chat/chat/completions, /completions, /responsesText-generating LLM. This label is what makes a model an LLM — it is not inferred from reasoning/vision/tool-calling/chat-transcription, which are characteristics rather than deployment modes.
transcription/audio/transcriptionsSpeech-to-text transcription model (e.g. Whisper). An omni LLM that accepts audio in a chat turn is not one of these — it carries chat and the chat-transcription capability below.
embeddings/embeddingsProduces text embedding vectors. Also accepted as embedding.
reranking/rerankScores and reranks a list of passages given a query. Also reachable at the aliases /reranking and /reranker.
image/images/generations, /images/edits, /images/variationsText-to-image generation model.
tts/audio/speechText-to-speech synthesis model.
audio-generation/audio/generationsText-to-audio generation model (e.g. music, sound effects).
classification/classifyText classification model. Also accepted as classifier.
3d/3d/generationsText- or image-to-3D mesh generation model.

When a model declares no deployment label at all, it inherits its recipe's default — chat for llamacpp, flm, ryzenai-llm, vllm and cloud, transcription for whispercpp, image for sd-cpp, tts for kokoro, and so on.

Two label sets describe a model that cannot exist, and are refused rather than repaired:

  • A mode the recipe's backend does not serve. /classify is served only by onnxruntime, so labels: ["classification"] on a llamacpp model is an error — register it as the chat model it is.
  • Two different modes. labels: ["chat", "embeddings"] on a llamacpp model is an error even though llama.cpp serves both: the subprocess is launched for one mode, so the second would name an endpoint it was never configured to answer. Register one model per mode. The legacy embedding and reranking booleans count as mode claims here, exactly as the labels do.

POST /v1/pull answers 400 and registers nothing. An entry already stored in user_models.json — written before these rules — is skipped at startup with an error naming it, and the file is left untouched so it can be corrected by hand.

Input-modality labels — the model accepts additional input types in /chat/completions:

LabelDescription
visionAccepts image attachments in chat messages.
chat-transcriptionAccepts audio attachments in chat messages and transcribes them as part of its answer (e.g. Qwen2.5-Omni). Like vision, this is something a chat model can do, not a deployment mode of its own — a model carrying it also carries chat. It is distinct from transcription, which deploys a dedicated ASR model on /audio/transcriptions.

Streaming labels — capability flags for real-time features:

LabelDescription
realtime-transcriptionSupports the WebSocket /realtime endpoint for live microphone transcription.

Runtime labels — affect backend launch defaults:

LabelDescription
mtpEnables llama.cpp MTP draft decoding defaults (--spec-type draft-mtp --spec-draft-n-max 3 --spec-draft-p-min 0.75); users can override these with llamacpp_args.

Image capability labels — carried alongside image; they refine what the model is offered for without changing its deployment mode:

LabelDescription
editTuned for editing an input image (/images/edits). Also selects the model for the edit_image role in an omni collection.
upscalingImage upscaling model (e.g. Real-ESRGAN, /images/upscale). Used as a component in image pipelines rather than offered on its own.

Characteristic labels — informational, do not affect routing:

LabelDescription
hotFeatured or popular model, highlighted in the UI.
reasoningUses extended chain-of-thought reasoning (e.g. DeepSeek, Qwen3).
tool-callingSupports function/tool calling in chat completions.
codingTuned for code generation and software tasks.
experimentalNot yet validated for production use.

GET /v1/models/{model_id}

Status

Retrieve a specific model by its ID. Returns the same model object format as the list endpoint above.

Parameters

ParameterRequiredDescription
model_idYesThe ID of the model to retrieve. Must match one of the model IDs from the models list.

Example request

curl http://localhost:13305/v1/models/Qwen3-0.6B-GGUF

Response format

Returns a single model object with the same fields as described in the models list endpoint above. For Omni collections (recipe: "collection.omni"), the object additionally carries components (ordered component names) and models (each component's full model object) — see the collection file documentation.

{
  "id": "Qwen3-0.6B-GGUF",
  "created": 1744173590,
  "object": "model",
  "owned_by": "lemonade",
  "checkpoint": "unsloth/Qwen3-0.6B-GGUF:Q4_0",
  "recipe": "llamacpp",
  "size": 0.38,
  "max_context_window": 40960,
  "context_length": 8192,
  "downloaded": true,
  "suggested": true,
  "labels": ["reasoning"],
  "recipe_options": {
    "ctx_size": 8192,
    "llamacpp_args": "--no-mmap",
    "llamacpp_backend": "rocm"
  }
}

Error responses

If the model is not found, the endpoint returns a 404 error:

{
  "error": {
    "message": "Model Qwen3-0.6B-GGUF has not been found",
    "type": "not_found"
  }
}