local-lemonade 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/openai.md ADDED
@@ -0,0 +1,1192 @@
1
+ # OpenAI-Compatible API
2
+
3
+ This spec defines Lemonade's implementation of the [OpenAI API](https://developers.openai.com/api/docs).
4
+
5
+ | Method | Endpoint | Description | Modality |
6
+ |--------|----------|-------------|----------|
7
+ | `POST` | [`/v1/chat/completions`](#post-v1chatcompletions) | Chat Completions | messages -> completion |
8
+ | `POST` | [`/v1/completions`](#post-v1completions) | Text Completions | prompt -> completion |
9
+ | `POST` | [`/v1/embeddings`](#post-v1embeddings) | Embeddings | text -> vector representations |
10
+ | `POST` | [`/v1/responses`](#post-v1responses) | Responses API | prompt/messages -> event |
11
+ | `POST` | [`/v1/audio/transcriptions`](#post-v1audiotranscriptions) | Audio Transcription | audio file -> text |
12
+ | `POST` | [`/v1/audio/speech`](#post-v1audiospeech) | Text to speech | text -> audio |
13
+ | `WS` | [`/realtime`](#ws-realtime) | Realtime Audio Transcription, OpenAI SDK compatible | streaming audio -> text |
14
+ | `POST` | [`/v1/images/generations`](#post-v1imagesgenerations) | Image Generation | prompt -> image |
15
+ | `POST` | [`/v1/images/edits`](#post-v1imagesedits) | Image Editing | image + prompt -> edited image |
16
+ | `POST` | [`/v1/images/variations`](#post-v1imagesvariations) | Image Variations | image -> varied image |
17
+ | `POST` | [`/v1/images/upscale`](#post-v1imagesupscale) | Image Upscaling | image + ESRGAN model -> upscaled image |
18
+ | `GET` | [`/v1/models`](#get-v1models) | List models available locally | n/a |
19
+ | `GET` | [`/v1/models/{model_id}`](#get-v1modelsmodel_id) | Retrieve a specific model by ID | n/a |
20
+
21
+ ## `POST /v1/chat/completions`
22
+ <sub>![Status](https://img.shields.io/badge/status-partially_available-green)</sub>
23
+
24
+ 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.
25
+
26
+ ### Parameters
27
+
28
+ | Parameter | Required | Description | Status |
29
+ |-----------|----------|-------------|--------|
30
+ | `messages` | Yes | Array of messages in the conversation. Each message should have a `role` ("user" or "assistant") and `content` (the message text). | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
31
+ | `model` | Yes | The model to use for the completion. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
32
+ | `stream` | No | If 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. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
33
+ | `stop` | No | Up 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. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
34
+ | `logprobs` | No | Include log probabilities of the output tokens. If true, returns the log probability of each output token. Defaults to false. | <sub>![Status](https://img.shields.io/badge/not_available-red)</sub> |
35
+ | `temperature` | No | What sampling temperature to use. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
36
+ | `repeat_penalty` | No | Number between 1.0 and 2.0. 1.0 means no penalty. Higher values discourage repetition. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
37
+ | `top_k` | No | Integer that controls the number of top tokens to consider during sampling. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
38
+ | `top_p` | No | Float between 0.0 and 1.0 that controls the cumulative probability of top tokens to consider during nucleus sampling. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
39
+ | `tools` | No | A list of tools the model may call. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
40
+ | `max_tokens` | No | An 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_tokens` | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
41
+ | `max_completion_tokens` | No | An upper bound for the number of tokens that can be generated for a completion. Mutually exclusive with `max_tokens`. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
42
+
43
+ ### Example request
44
+
45
+ === "PowerShell"
46
+
47
+ ```powershell
48
+ Invoke-WebRequest `
49
+ -Uri "http://localhost:13305/v1/chat/completions" `
50
+ -Method POST `
51
+ -Headers @{ "Content-Type" = "application/json" } `
52
+ -Body '{
53
+ "model": "Qwen3-0.6B-GGUF",
54
+ "messages": [
55
+ {
56
+ "role": "user",
57
+ "content": "What is the population of Paris?"
58
+ }
59
+ ],
60
+ "stream": false
61
+ }'
62
+ ```
63
+ === "Bash"
64
+
65
+ ```bash
66
+ curl -X POST http://localhost:13305/v1/chat/completions \
67
+ -H "Content-Type: application/json" \
68
+ -d '{
69
+ "model": "Qwen3-0.6B-GGUF",
70
+ "messages": [
71
+ {"role": "user", "content": "What is the population of Paris?"}
72
+ ],
73
+ "stream": false
74
+ }'
75
+ ```
76
+
77
+ ### Response format
78
+
79
+ === "Non-streaming responses"
80
+
81
+ ```json
82
+ {
83
+ "id": "0",
84
+ "object": "chat.completion",
85
+ "created": 1742927481,
86
+ "model": "Qwen3-0.6B-GGUF",
87
+ "choices": [{
88
+ "index": 0,
89
+ "message": {
90
+ "role": "assistant",
91
+ "content": "Paris has a population of approximately 2.2 million people in the city proper."
92
+ },
93
+ "finish_reason": "stop"
94
+ }]
95
+ }
96
+ ```
97
+ === "Streaming responses"
98
+ For streaming responses, the API returns a stream of server-sent events (however, Open AI recommends using their streaming libraries for parsing streaming responses):
99
+
100
+ ```json
101
+ {
102
+ "id": "0",
103
+ "object": "chat.completion.chunk",
104
+ "created": 1742927481,
105
+ "model": "Qwen3-0.6B-GGUF",
106
+ "choices": [{
107
+ "index": 0,
108
+ "delta": {
109
+ "role": "assistant",
110
+ "content": "Paris"
111
+ }
112
+ }]
113
+ }
114
+ ```
115
+
116
+ ### Image understanding input format (OpenAI-compatible)
117
+
118
+ 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).
119
+
120
+ #### Example request
121
+
122
+ ```bash
123
+ curl -X POST http://localhost:13305/v1/chat/completions \
124
+ -H "Content-Type: application/json" \
125
+ -d '{
126
+ "model": "Qwen2.5-VL-7B-Instruct",
127
+ "messages": [
128
+ {
129
+ "role": "user",
130
+ "content": [
131
+ {"type": "text", "text": "What is in this image?"},
132
+ {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."}}
133
+ ]
134
+ }
135
+ ],
136
+ "stream": false
137
+ }'
138
+ ```
139
+
140
+ #### Example response
141
+
142
+ ```json
143
+ {
144
+ "id": "0",
145
+ "object": "chat.completion",
146
+ "created": 1742927481,
147
+ "model": "Qwen2.5-VL-7B-Instruct",
148
+ "choices": [{
149
+ "index": 0,
150
+ "message": {
151
+ "role": "assistant",
152
+ "content": "The image shows a red apple resting on a wooden table."
153
+ },
154
+ "finish_reason": "stop"
155
+ }]
156
+ }
157
+ ```
158
+
159
+ ### Server-side tools
160
+
161
+ > **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.
162
+
163
+ 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`:
164
+
165
+ - **images** → markdown `![generated image](data:image/png;base64,…)`
166
+ - **speech** → `<audio>data:audio/mpeg;base64,…</audio>`
167
+
168
+ 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.
169
+
170
+ **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](../dev/lemonade-omni.md) for details.
171
+
172
+ #### Example request
173
+
174
+ ```bash
175
+ curl -X POST http://localhost:13305/v1/chat/completions \
176
+ -H "Content-Type: application/json" \
177
+ -d '{
178
+ "model": "LMX-Omni-52B-Halo",
179
+ "messages": [{"role": "user", "content": "Draw a red apple on a table."}],
180
+ "stream": false
181
+ }'
182
+ ```
183
+
184
+ #### Example response
185
+
186
+ The image tool runs during the loop and its output is embedded as a markdown image in the assistant `content`:
187
+
188
+ ```json
189
+ {
190
+ "id": "0",
191
+ "object": "chat.completion",
192
+ "created": 1742927481,
193
+ "model": "LMX-Omni-52B-Halo",
194
+ "choices": [{
195
+ "index": 0,
196
+ "message": {
197
+ "role": "assistant",
198
+ "content": "Here is a red apple on a table.\n\n![generated image](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...)"
199
+ },
200
+ "finish_reason": "stop"
201
+ }]
202
+ }
203
+ ```
204
+
205
+
206
+ ## `POST /v1/completions`
207
+ <sub>![Status](https://img.shields.io/badge/status-fully_available-green)</sub>
208
+
209
+ Text Completions API. You provide a prompt and receive a completion. This API will also load the model if it is not already loaded.
210
+
211
+ ### Parameters
212
+
213
+ | Parameter | Required | Description | Status |
214
+ |-----------|----------|-------------|--------|
215
+ | `prompt` | Yes | The prompt to use for the completion. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
216
+ | `model` | Yes | The model to use for the completion. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
217
+ | `stream` | No | If 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. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
218
+ | `stop` | No | Up 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. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
219
+ | `echo` | No | Echo back the prompt in addition to the completion. Available on non-streaming mode. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
220
+ | `logprobs` | No | Include log probabilities of the output tokens. If true, returns the log probability of each output token. Defaults to false. Only available when `stream=False`. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
221
+ | `temperature` | No | What sampling temperature to use. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
222
+ | `repeat_penalty` | No | Number between 1.0 and 2.0. 1.0 means no penalty. Higher values discourage repetition. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
223
+ | `top_k` | No | Integer that controls the number of top tokens to consider during sampling. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
224
+ | `top_p` | No | Float between 0.0 and 1.0 that controls the cumulative probability of top tokens to consider during nucleus sampling. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
225
+ | `max_tokens` | No | An upper bound for the number of tokens that can be generated for a completion, including input tokens. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
226
+
227
+ ### Example request
228
+
229
+ === "PowerShell"
230
+
231
+ ```powershell
232
+ Invoke-WebRequest -Uri "http://localhost:13305/v1/completions" `
233
+ -Method POST `
234
+ -Headers @{ "Content-Type" = "application/json" } `
235
+ -Body '{
236
+ "model": "Qwen3-0.6B-GGUF",
237
+ "prompt": "What is the population of Paris?",
238
+ "stream": false
239
+ }'
240
+ ```
241
+
242
+ === "Bash"
243
+
244
+ ```bash
245
+ curl -X POST http://localhost:13305/v1/completions \
246
+ -H "Content-Type: application/json" \
247
+ -d '{
248
+ "model": "Qwen3-0.6B-GGUF",
249
+ "prompt": "What is the population of Paris?",
250
+ "stream": false
251
+ }'
252
+ ```
253
+
254
+ ### Response format
255
+
256
+ The following format is used for both streaming and non-streaming responses:
257
+
258
+ ```json
259
+ {
260
+ "id": "0",
261
+ "object": "text_completion",
262
+ "created": 1742927481,
263
+ "model": "Qwen3-0.6B-GGUF",
264
+ "choices": [{
265
+ "index": 0,
266
+ "text": "Paris has a population of approximately 2.2 million people in the city proper.",
267
+ "finish_reason": "stop"
268
+ }],
269
+ }
270
+ ```
271
+
272
+
273
+
274
+ ## `POST /v1/embeddings`
275
+ <sub>![Status](https://img.shields.io/badge/status-fully_available-green)</sub>
276
+
277
+ 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.
278
+
279
+ > **Note:** This endpoint is only available for models using the `llamacpp` or `flm` recipes. ONNX models (OGA recipes) do not support embeddings.
280
+
281
+ ### Parameters
282
+
283
+ | Parameter | Required | Description | Status |
284
+ |-----------|----------|-------------|--------|
285
+ | `input` | Yes | The input text or array of texts to embed. Can be a string or an array of strings. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
286
+ | `model` | Yes | The model to use for generating embeddings. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
287
+ | `encoding_format` | No | The format to return embeddings in. Supported values: `"float"` (default), `"base64"`. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
288
+
289
+ ### Example request
290
+
291
+ === "PowerShell"
292
+
293
+ ```powershell
294
+ Invoke-WebRequest `
295
+ -Uri "http://localhost:13305/v1/embeddings" `
296
+ -Method POST `
297
+ -Headers @{ "Content-Type" = "application/json" } `
298
+ -Body '{
299
+ "model": "nomic-embed-text-v1-GGUF",
300
+ "input": ["Hello, world!", "How are you?"],
301
+ "encoding_format": "float"
302
+ }'
303
+ ```
304
+
305
+ === "Bash"
306
+
307
+ ```bash
308
+ curl -X POST http://localhost:13305/v1/embeddings \
309
+ -H "Content-Type: application/json" \
310
+ -d '{
311
+ "model": "nomic-embed-text-v1-GGUF",
312
+ "input": ["Hello, world!", "How are you?"],
313
+ "encoding_format": "float"
314
+ }'
315
+ ```
316
+
317
+ ### Response format
318
+
319
+ ```json
320
+ {
321
+ "object": "list",
322
+ "data": [
323
+ {
324
+ "object": "embedding",
325
+ "index": 0,
326
+ "embedding": [0.0234, -0.0567, 0.0891, ...]
327
+ },
328
+ {
329
+ "object": "embedding",
330
+ "index": 1,
331
+ "embedding": [0.0456, -0.0678, 0.1234, ...]
332
+ }
333
+ ],
334
+ "model": "nomic-embed-text-v1-GGUF",
335
+ "usage": {
336
+ "prompt_tokens": 12,
337
+ "total_tokens": 12
338
+ }
339
+ }
340
+ ```
341
+
342
+ **Field Descriptions:**
343
+
344
+ - `object` - Type of response object, always `"list"`
345
+ - `data` - Array of embedding objects
346
+ - `object` - Type of embedding object, always `"embedding"`
347
+ - `index` - Index position of the input text in the request
348
+ - `embedding` - Vector representation as an array of floats
349
+ - `model` - Model identifier used to generate the embeddings
350
+ - `usage` - Token usage statistics
351
+ - `prompt_tokens` - Number of tokens in the input
352
+ - `total_tokens` - Total tokens processed
353
+
354
+ ## `POST /v1/responses`
355
+ <sub>![Status](https://img.shields.io/badge/status-partially_available-green)</sub>
356
+
357
+ Responses API. You provide an input and receive a response. This API will also load the model if it is not already loaded.
358
+
359
+ ### Parameters
360
+
361
+ | Parameter | Required | Description | Status |
362
+ |-----------|----------|-------------|--------|
363
+ | `input` | Yes | A list of dictionaries or a string input for the model to respond to. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
364
+ | `model` | Yes | The model to use for the response. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
365
+ | `max_output_tokens` | No | The maximum number of output tokens to generate. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
366
+ | `temperature` | No | What sampling temperature to use. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
367
+ | `repeat_penalty` | No | Number between 1.0 and 2.0. 1.0 means no penalty. Higher values discourage repetition. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
368
+ | `top_k` | No | Integer that controls the number of top tokens to consider during sampling. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
369
+ | `top_p` | No | Float between 0.0 and 1.0 that controls the cumulative probability of top tokens to consider during nucleus sampling. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
370
+ | `stream` | No | If 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. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
371
+
372
+
373
+ ### Streaming Events
374
+
375
+ 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:
376
+
377
+ - `response.created`
378
+ - `response.output_text.delta`
379
+ - `response.completed`
380
+
381
+ For a full list of event types, see the [API reference for streaming](https://platform.openai.com/docs/api-reference/responses-streaming).
382
+
383
+ ### Example request
384
+
385
+ === "PowerShell"
386
+
387
+ ```powershell
388
+ Invoke-WebRequest -Uri "http://localhost:13305/v1/responses" `
389
+ -Method POST `
390
+ -Headers @{ "Content-Type" = "application/json" } `
391
+ -Body '{
392
+ "model": "Llama-3.2-1B-Instruct-Hybrid",
393
+ "input": "What is the population of Paris?",
394
+ "stream": false
395
+ }'
396
+ ```
397
+
398
+ === "Bash"
399
+
400
+ ```bash
401
+ curl -X POST http://localhost:13305/v1/responses \
402
+ -H "Content-Type: application/json" \
403
+ -d '{
404
+ "model": "Llama-3.2-1B-Instruct-Hybrid",
405
+ "input": "What is the population of Paris?",
406
+ "stream": false
407
+ }'
408
+ ```
409
+
410
+
411
+ ### Response format
412
+
413
+ === "Non-streaming responses"
414
+
415
+ ```json
416
+ {
417
+ "id": "0",
418
+ "created_at": 1746225832.0,
419
+ "model": "Llama-3.2-1B-Instruct-Hybrid",
420
+ "object": "response",
421
+ "output": [{
422
+ "id": "0",
423
+ "content": [{
424
+ "annotations": [],
425
+ "text": "Paris has a population of approximately 2.2 million people in the city proper."
426
+ }]
427
+ }]
428
+ }
429
+ ```
430
+
431
+ === "Streaming Responses"
432
+ For streaming responses, the API returns a series of events. Refer to [OpenAI streaming guide](https://platform.openai.com/docs/guides/streaming-responses?api-mode=responses) for details.
433
+
434
+
435
+
436
+ ## `POST /v1/audio/transcriptions`
437
+ <sub>![Status](https://img.shields.io/badge/status-partial-yellow)</sub>
438
+
439
+ 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.
440
+
441
+ > **Note:** This endpoint uses [whisper.cpp](https://github.com/ggerganov/whisper.cpp) as the backend. Whisper models are automatically downloaded when first used.
442
+ >
443
+ > **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.
444
+
445
+ ### Parameters
446
+
447
+ | Parameter | Required | Description | Status |
448
+ |-----------|----------|-------------|--------|
449
+ | `file` | Yes | The audio file to transcribe. Supported formats: wav. | <sub>![Status](https://img.shields.io/badge/partial-yellow)</sub> |
450
+ | `model` | Yes | The Whisper model to use for transcription (e.g., `Whisper-Tiny`, `Whisper-Base`, `Whisper-Small`). | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
451
+ | `language` | No | The 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. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
452
+ | `response_format` | No | The response format. Supported values: `json`, `verbose_json`, `text`, `srt`, `vtt`. `srt` and `vtt` require a backend that reports segment timestamps (whisper.cpp). | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
453
+
454
+ ### Example request
455
+
456
+ === "Windows"
457
+
458
+ ```bash
459
+ curl -X POST http://localhost:13305/v1/audio/transcriptions ^
460
+ -F "file=@C:\path\to\audio.wav" ^
461
+ -F "model=Whisper-Tiny"
462
+ ```
463
+
464
+ === "Linux"
465
+
466
+ ```bash
467
+ curl -X POST http://localhost:13305/v1/audio/transcriptions \
468
+ -F "file=@/path/to/audio.wav" \
469
+ -F "model=Whisper-Tiny"
470
+ ```
471
+
472
+ ### Response format
473
+
474
+ ```json
475
+ {
476
+ "text": "Hello, this is a sample transcription of the audio file."
477
+ }
478
+ ```
479
+
480
+ **Field Descriptions:**
481
+
482
+ - `text` - The transcribed text from the audio file
483
+
484
+
485
+
486
+ ## `WS /realtime`
487
+ <sub>![Status](https://img.shields.io/badge/status-partial-yellow)</sub>
488
+
489
+ 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).
490
+
491
+ > **Limitations:** Only 16kHz mono PCM16 audio format is supported. Uses the same Whisper models as the HTTP transcription endpoint.
492
+
493
+ ### Connection
494
+
495
+ WebSocket upgrades are accepted **directly on the main HTTP port** (default 13305) — the same port as all REST endpoints. Connect with the model name:
496
+
497
+ ```
498
+ ws://localhost:13305/v1/realtime?model=Whisper-Tiny
499
+ ```
500
+
501
+ 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.
502
+
503
+ A dedicated WebSocket port also remains for backward compatibility; it is OS-assigned and reported by the [`/v1/health`](./lemonade.md#get-v1health) endpoint (`websocket_port` field). New clients should prefer the main port:
504
+
505
+ ```
506
+ ws://localhost:<websocket_port>/realtime?model=Whisper-Tiny
507
+ ```
508
+
509
+ Upon connection, the server sends a `session.created` message with a session ID.
510
+
511
+ ### Client → Server Messages
512
+
513
+ | Message Type | Description |
514
+ |--------------|-------------|
515
+ | `session.update` | Configure the session (set model, VAD settings, or disable turn detection) |
516
+ | `input_audio_buffer.append` | Send audio data (base64-encoded PCM16) |
517
+ | `input_audio_buffer.commit` | Force transcription of buffered audio |
518
+ | `input_audio_buffer.clear` | Clear audio buffer without transcribing |
519
+
520
+ ### Server → Client Messages
521
+
522
+ | Message Type | Description |
523
+ |--------------|-------------|
524
+ | `session.created` | Session established, contains session ID |
525
+ | `session.updated` | Session configuration updated |
526
+ | `input_audio_buffer.speech_started` | VAD detected speech start |
527
+ | `input_audio_buffer.speech_stopped` | VAD detected speech end, transcription triggered |
528
+ | `input_audio_buffer.committed` | Audio buffer committed for transcription |
529
+ | `input_audio_buffer.cleared` | Audio buffer cleared |
530
+ | `conversation.item.input_audio_transcription.delta` | Interim/partial transcription (replaceable) |
531
+ | `conversation.item.input_audio_transcription.completed` | Final transcription result |
532
+ | `error` | Error message |
533
+
534
+ ### Example: Configure Session
535
+
536
+ ```json
537
+ {
538
+ "type": "session.update",
539
+ "session": {
540
+ "model": "Whisper-Tiny"
541
+ }
542
+ }
543
+ ```
544
+
545
+ ### Example: Send Audio
546
+
547
+ ```json
548
+ {
549
+ "type": "input_audio_buffer.append",
550
+ "audio": "<base64-encoded PCM16 audio>"
551
+ }
552
+ ```
553
+
554
+ Audio should be:
555
+ - 16kHz sample rate
556
+ - Mono (single channel)
557
+ - 16-bit signed integer (PCM16)
558
+ - Base64 encoded
559
+ - Sent in chunks (~85ms recommended)
560
+
561
+ ### Example: Transcription Result
562
+
563
+ ```json
564
+ {
565
+ "type": "conversation.item.input_audio_transcription.completed",
566
+ "transcript": "Hello, this is a test transcription."
567
+ }
568
+ ```
569
+
570
+ ### VAD Configuration
571
+
572
+ VAD settings can be configured via `session.update`:
573
+
574
+ ```json
575
+ {
576
+ "type": "session.update",
577
+ "session": {
578
+ "model": "Whisper-Tiny",
579
+ "turn_detection": {
580
+ "threshold": 0.01,
581
+ "silence_duration_ms": 800,
582
+ "prefix_padding_ms": 250
583
+ }
584
+ }
585
+ }
586
+ ```
587
+
588
+ | Parameter | Default | Description |
589
+ |-----------|---------|-------------|
590
+ | `threshold` | 0.01 | RMS energy threshold for speech detection |
591
+ | `silence_duration_ms` | 800 | Silence duration to trigger speech end |
592
+ | `prefix_padding_ms` | 250 | Minimum speech duration before triggering |
593
+
594
+ Set `turn_detection` to `null` to disable server-side VAD and use explicit commits instead:
595
+
596
+ ```json
597
+ {
598
+ "type": "session.update",
599
+ "session": {
600
+ "model": "Whisper-Tiny",
601
+ "turn_detection": null
602
+ }
603
+ }
604
+ ```
605
+
606
+ ### Code Examples
607
+
608
+ A complete, runnable example:
609
+
610
+ - **[`realtime_transcription.py`](https://github.com/lemonade-sdk/lemonade/blob/main/examples/realtime_transcription.py)** - Python CLI for microphone streaming
611
+
612
+ ```bash
613
+ # Stream from microphone
614
+ python examples/realtime_transcription.py --model Whisper-Tiny
615
+ ```
616
+
617
+ ### Integration Notes
618
+
619
+ - **Audio Format**: Server expects 16kHz mono PCM16. Higher sample rates must be downsampled client-side.
620
+ - **Chunk Size**: Send audio in ~85-256ms chunks for optimal latency/efficiency.
621
+ - **VAD Behavior**: Server automatically detects speech boundaries and triggers transcription on speech end.
622
+ - **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.
623
+ - **Clear Buffer**: Use `input_audio_buffer.clear` to discard audio without transcribing.
624
+ - **Chunking**: We are still tuning the chunking to balance latency vs. accuracy.
625
+ - **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.
626
+
627
+
628
+ ## `POST /v1/images/generations`
629
+ <sub>![Status](https://img.shields.io/badge/status-fully_available-green)</sub>
630
+
631
+ Image Generation API. You provide a text prompt and receive a generated image.
632
+
633
+ > **Performance:** CPU inference takes ~4-5 minutes per image. GPU (ROCm) is significantly faster.
634
+
635
+ ### Parameters
636
+
637
+ | Parameter | Required | Description | Status |
638
+ |-----------|----------|-------------|--------|
639
+ | `prompt` | Yes | The text description of the image to generate. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
640
+ | `model` | Yes | The diffusion model to use (e.g., `SD-Turbo`, `Krea-2-Turbo`). | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
641
+ | `size` | No | The size of the generated image. Format: `WIDTHxHEIGHT` (e.g., `512x512`, `256x256`). Default: `512x512`. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
642
+ | `n` | No | Number of images to generate. Currently only `1` is supported. | <sub>![Status](https://img.shields.io/badge/partial-yellow)</sub> |
643
+ | `response_format` | No | Format of the response. Only `b64_json` (base64-encoded image) is supported. | <sub>![Status](https://img.shields.io/badge/partial-yellow)</sub> |
644
+ | `steps` | No | Number of inference steps. SD-Turbo works well with 4 steps. Default varies by model. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
645
+ | `cfg_scale` | No | Classifier-free guidance scale. SD-Turbo uses low values (~1.0). Default varies by model. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
646
+ | `seed` | No | Random seed for reproducibility. If not specified, a random seed is used. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
647
+
648
+ ### Example request
649
+
650
+ === "Bash"
651
+
652
+ ```bash
653
+ curl -X POST http://localhost:13305/v1/images/generations \
654
+ -H "Content-Type: application/json" \
655
+ -d '{
656
+ "model": "SD-Turbo",
657
+ "prompt": "A serene mountain landscape at sunset",
658
+ "size": "512x512",
659
+ "steps": 4,
660
+ "response_format": "b64_json"
661
+ }'
662
+ ```
663
+
664
+ ## `POST /v1/images/edits`
665
+ <sub>![Status](https://img.shields.io/badge/status-fully_available-green)</sub>
666
+
667
+ Image Editing API. You provide a source image and a text prompt describing the desired change, and receive an edited image.
668
+
669
+ > **Note:** This endpoint accepts `multipart/form-data` requests (not JSON). Use editing-capable models such as `Flux-2-Klein-4B` or `SD-Turbo`.
670
+ >
671
+ > **Performance:** CPU inference takes several minutes per image. GPU (ROCm) is significantly faster.
672
+
673
+ ### Parameters
674
+
675
+ | Parameter | Required | Description | Status |
676
+ |-----------|----------|-------------|--------|
677
+ | `model` | Yes | The diffusion model to use (e.g., `Flux-2-Klein-4B`, `SD-Turbo`). | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
678
+ | `image` / `image[]` | Yes | The source image file to edit (PNG). Sent as a file in multipart/form-data. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
679
+ | `prompt` | Yes | A text description of the desired edit. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
680
+ | `mask` | No | An optional mask image (PNG). White areas indicate regions to edit; black areas are preserved. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
681
+ | `size` | No | The size of the output image. Format: `WIDTHxHEIGHT` (e.g., `512x512`). Default: `512x512`. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
682
+ | `n` | No | Number of images to generate. Allowed range: `1`–`10`. Default: `1`. Values outside this range are rejected with `400 Bad Request`. | <sub>![Status](https://img.shields.io/badge/partial-yellow)</sub> |
683
+ | `response_format` | No | Format of the response. Only `b64_json` (base64-encoded image) is supported. | <sub>![Status](https://img.shields.io/badge/partial-yellow)</sub> |
684
+ | `steps` | No | Number of inference steps. Default varies by model. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
685
+ | `cfg_scale` | No | Classifier-free guidance scale. Default varies by model. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
686
+ | `seed` | No | Random seed for reproducibility. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
687
+ | `user` | No | OpenAI API compatibility field. Accepted but not forwarded to the backend. | <sub>![Status](https://img.shields.io/badge/not_available-red)</sub> |
688
+ | `background` | No | OpenAI API compatibility field. Accepted but not forwarded to the backend. | <sub>![Status](https://img.shields.io/badge/not_available-red)</sub> |
689
+ | `quality` | No | OpenAI API compatibility field. Accepted but not forwarded to the backend. | <sub>![Status](https://img.shields.io/badge/not_available-red)</sub> |
690
+ | `input_fidelity` | No | OpenAI API compatibility field. Accepted but not forwarded to the backend. | <sub>![Status](https://img.shields.io/badge/not_available-red)</sub> |
691
+ | `output_compression` | No | OpenAI API compatibility field. Accepted; silently ignored by the backend. | <sub>![Status](https://img.shields.io/badge/not_available-red)</sub> |
692
+
693
+ ### Example request
694
+
695
+ === "Bash"
696
+
697
+ ```bash
698
+ curl -X POST http://localhost:13305/v1/images/edits \
699
+ -F "model=Flux-2-Klein-4B" \
700
+ -F "prompt=Add a red barn and mountains in the background, photorealistic" \
701
+ -F "size=512x512" \
702
+ -F "n=1" \
703
+ -F "response_format=b64_json" \
704
+ -F "image=@/path/to/source_image.png"
705
+ ```
706
+
707
+ === "Python (OpenAI client)"
708
+
709
+ ```python
710
+ from openai import OpenAI
711
+ client = OpenAI(base_url="http://localhost:13305/api/v1", api_key="not-needed")
712
+ with open("source_image.png", "rb") as image_file:
713
+ response = client.images.edit(
714
+ model="Flux-2-Klein-4B",
715
+ image=image_file,
716
+ prompt="Add a red barn and mountains in the background, photorealistic",
717
+ size="512x512",
718
+ )
719
+ import base64
720
+ image_data = base64.b64decode(response.data[0].b64_json)
721
+ open("edited_image.png", "wb").write(image_data)
722
+ ```
723
+
724
+ ---
725
+
726
+ ## `POST /v1/images/variations`
727
+ <sub>![Status](https://img.shields.io/badge/status-fully_available-green)</sub>
728
+
729
+ Image Variations API. You provide a source image and receive a variation of it.
730
+
731
+ > **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.
732
+ >
733
+ > **Performance:** CPU inference takes several minutes per image. GPU (ROCm) is significantly faster.
734
+
735
+ ### Parameters
736
+
737
+ | Parameter | Required | Description | Status |
738
+ |-----------|----------|-------------|--------|
739
+ | `model` | Yes | The Stable Diffusion model to use (e.g., `Flux-2-Klein-4B`, `SD-Turbo`). | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
740
+ | `image` | Yes | The source image file (PNG). Sent as a file in multipart/form-data. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
741
+ | `size` | No | The size of the output image. Format: `WIDTHxHEIGHT` (e.g., `512x512`). Default: `512x512`. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
742
+ | `n` | No | Number of variations to generate. Integer between 1 and 10 inclusive. Default: `1`. Values outside this range result in a 400 Bad Request error. | <sub>![Status](https://img.shields.io/badge/partial-yellow)</sub> |
743
+ | `response_format` | No | Format of the response. Only `b64_json` (base64-encoded image) is supported. | <sub>![Status](https://img.shields.io/badge/partial-yellow)</sub> |
744
+ | `user` | No | OpenAI API compatibility field. Accepted but not forwarded to the backend. | <sub>![Status](https://img.shields.io/badge/not_available-red)</sub> |
745
+
746
+ ### Example request
747
+
748
+ === "Bash"
749
+
750
+ ```bash
751
+ curl -X POST http://localhost:13305/v1/images/variations \
752
+ -F "model=Flux-2-Klein-4B" \
753
+ -F "size=512x512" \
754
+ -F "n=1" \
755
+ -F "response_format=b64_json" \
756
+ -F "image=@/path/to/source_image.png"
757
+ ```
758
+
759
+ === "Python (OpenAI client)"
760
+
761
+ ```python
762
+ from openai import OpenAI
763
+ client = OpenAI(base_url="http://localhost:13305/api/v1", api_key="not-needed")
764
+ with open("source_image.png", "rb") as image_file:
765
+ response = client.images.create_variation(
766
+ model="Flux-2-Klein-4B",
767
+ image=image_file,
768
+ size="512x512",
769
+ n=1,
770
+ )
771
+ import base64
772
+ image_data = base64.b64decode(response.data[0].b64_json)
773
+ open("variation.png", "wb").write(image_data)
774
+ ```
775
+
776
+ ---
777
+
778
+ ## `POST /v1/images/upscale`
779
+ <sub>![Status](https://img.shields.io/badge/status-fully_available-green)</sub>
780
+
781
+ Image Upscaling API. You provide a base64-encoded image and a Real-ESRGAN model name, and receive an upscaled image. The upscale factor depends on the selected model, and it is usually reflected in its name.
782
+
783
+ > **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.
784
+
785
+ ### Parameters
786
+
787
+ | Parameter | Required | Description | Status |
788
+ |-----------|----------|-------------|--------|
789
+ | `image` | Yes | Base64-encoded PNG image to upscale. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
790
+ | `model` | Yes | The ESRGAN model to use (e.g., `RealESRGAN-x4plus`, `Remacri-4x-TheNoise`). | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
791
+
792
+ ### Example request
793
+
794
+ A typical workflow is to generate an image first, then upscale it:
795
+
796
+ === "Bash"
797
+
798
+ ```bash
799
+ # Step 1: Generate an image and save the base64 response
800
+ RESPONSE=$(curl -s -X POST http://localhost:13305/v1/images/generations \
801
+ -H "Content-Type: application/json" \
802
+ -d '{
803
+ "model": "SD-Turbo",
804
+ "prompt": "A serene mountain landscape at sunset",
805
+ "size": "512x512",
806
+ "steps": 4,
807
+ "response_format": "b64_json"
808
+ }')
809
+
810
+ # Step 2: Build the upscale JSON payload and pipe it to curl via stdin
811
+ # (base64 images are too large for command-line interpolation)
812
+ echo "$RESPONSE" | python3 -c "
813
+ import sys, json
814
+ b64 = json.load(sys.stdin)['data'][0]['b64_json']
815
+ print(json.dumps({'image': b64, 'model': 'RealESRGAN-x4plus'}))
816
+ " | curl -X POST http://localhost:13305/v1/images/upscale \
817
+ -H "Content-Type: application/json" \
818
+ -d @-
819
+ ```
820
+
821
+ === "PowerShell"
822
+
823
+ ```powershell
824
+ # Step 1: Generate an image
825
+ $genResponse = Invoke-WebRequest `
826
+ -Uri "http://localhost:13305/v1/images/generations" `
827
+ -Method POST `
828
+ -Headers @{ "Content-Type" = "application/json" } `
829
+ -Body '{
830
+ "model": "SD-Turbo",
831
+ "prompt": "A serene mountain landscape at sunset",
832
+ "size": "512x512",
833
+ "steps": 4,
834
+ "response_format": "b64_json"
835
+ }'
836
+
837
+ # Step 2: Extract the base64 image
838
+ $imageB64 = ($genResponse.Content | ConvertFrom-Json).data[0].b64_json
839
+
840
+ # Step 3: Upscale the image with Real-ESRGAN
841
+ $body = @{ image = $imageB64; model = "RealESRGAN-x4plus" } | ConvertTo-Json
842
+ Invoke-WebRequest `
843
+ -Uri "http://localhost:13305/v1/images/upscale" `
844
+ -Method POST `
845
+ -Headers @{ "Content-Type" = "application/json" } `
846
+ -Body $body
847
+ ```
848
+
849
+ === "Python (requests)"
850
+
851
+ ```python
852
+ import requests
853
+ import base64
854
+
855
+ BASE_URL = "http://localhost:13305/api/v1"
856
+
857
+ # Step 1: Generate an image
858
+ gen_response = requests.post(f"{BASE_URL}/images/generations", json={
859
+ "model": "SD-Turbo",
860
+ "prompt": "A serene mountain landscape at sunset",
861
+ "size": "512x512",
862
+ "steps": 4,
863
+ "response_format": "b64_json",
864
+ })
865
+ image_b64 = gen_response.json()["data"][0]["b64_json"]
866
+
867
+ # Step 2: Upscale the image with Real-ESRGAN (512x512 -> 2048x2048)
868
+ upscale_response = requests.post(f"{BASE_URL}/images/upscale", json={
869
+ "image": image_b64,
870
+ "model": "RealESRGAN-x4plus",
871
+ })
872
+
873
+ # Step 3: Save the upscaled image to a file
874
+ upscaled_b64 = upscale_response.json()["data"][0]["b64_json"]
875
+ with open("upscaled.png", "wb") as f:
876
+ f.write(base64.b64decode(upscaled_b64))
877
+ ```
878
+
879
+ ### Response format
880
+
881
+ ```json
882
+ {
883
+ "created": 1742927481,
884
+ "data": [
885
+ {
886
+ "b64_json": "<base64-encoded upscaled PNG>"
887
+ }
888
+ ]
889
+ }
890
+ ```
891
+
892
+ **Field Descriptions:**
893
+
894
+ - `created` - Unix timestamp of when the upscaled image was generated
895
+ - `data` - Array containing the upscaled image
896
+ - `b64_json` - Base64-encoded PNG of the upscaled image
897
+
898
+ ### Error responses
899
+
900
+ | Status Code | Condition | Example |
901
+ |-------------|-----------|---------|
902
+ | 400 | Missing `image` field | `{"error": {"message": "Missing 'image' field (base64 encoded)", "type": "invalid_request_error"}}` |
903
+ | 400 | Missing `model` field | `{"error": {"message": "Missing 'model' field", "type": "invalid_request_error"}}` |
904
+ | 404 | Unknown model name | `{"error": {"message": "Upscale model not found: bad-model", "type": "invalid_request_error"}}` |
905
+ | 500 | Upscale failed | `{"error": {"message": "ESRGAN upscale failed", "type": "server_error"}}` |
906
+
907
+ ---
908
+
909
+ ## `POST /v1/audio/speech`
910
+ <sub>![Status](https://img.shields.io/badge/status-fully_available-green)</sub>
911
+
912
+ Speech Generation API. You provide a text input and receive an audio file. Which engine serves the request depends on the model.
913
+
914
+ > **Note:** Supported models are `kokoro-v1` (fixed voices, [Kokoros](https://github.com/lucasjinreal/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.
915
+ >
916
+ > **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).
917
+
918
+ ### Parameters
919
+
920
+ | Parameter | Required | Description | Status |
921
+ |-----------|----------|-------------|--------|
922
+ | `input` | Yes | The text to speak. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
923
+ | `model` | Yes | The model to use (e.g., `kokoro-v1`). | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
924
+ | `speed` | No | Speaking speed. Default: `1.0`. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
925
+ | `voice` | No | The 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: `shimmer` | <sub>![Status](https://img.shields.io/badge/partial-yellow)</sub> |
926
+ | `voice` (OpenMOSS) | No | For 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`). | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
927
+ | `reference_wav_b64` | No | Lemonade extension (OpenMOSS voice cloning): base64-encoded WAV sample of the voice to clone. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
928
+ | `voice_design_description` | No | Lemonade 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. | <sub>![Status](https://img.shields.io/badge/available-green)</sub> |
929
+ | `response_format` | No | Container 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. | <sub>![Status](https://img.shields.io/badge/partial-yellow)</sub> |
930
+ | `stream_format` | No | If 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 set| <sub>![Status](https://img.shields.io/badge/partial-yellow)</sub> |
931
+
932
+ ### Example request
933
+
934
+ === "Bash"
935
+
936
+ ```bash
937
+ curl -X POST http://localhost:13305/v1/audio/speech \
938
+ -H "Content-Type: application/json" \
939
+ -d '{
940
+ "model": "kokoro-v1",
941
+ "input": "Lemonade can speak!",
942
+ "speed": 1.0,
943
+ "steps": 4,
944
+ "response_format": "mp3"
945
+ }'
946
+ ```
947
+
948
+ ### Response format
949
+
950
+ The generated audio file is returned as-is.
951
+
952
+
953
+ ## `GET /v1/models`
954
+ <sub>![Status](https://img.shields.io/badge/status-fully_available-green)</sub>
955
+
956
+ 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`.
957
+
958
+ By default, only models available locally (downloaded) are shown, matching OpenAI API behavior.
959
+
960
+ 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](../guide/configuration/cloud.md).
961
+
962
+ ### Parameters
963
+
964
+ | Parameter | Required | Description |
965
+ |-----------|----------|-------------|
966
+ | `show_all` | No | If set to `true`, returns all models from the catalog including those not yet downloaded. Defaults to `false`. |
967
+
968
+ ### Example request
969
+
970
+ ```bash
971
+ # Show only downloaded models (OpenAI-compatible)
972
+ curl http://localhost:13305/v1/models
973
+
974
+ # Show all models including not-yet-downloaded (extended usage)
975
+ curl http://localhost:13305/v1/models?show_all=true
976
+ ```
977
+
978
+ ### Response format
979
+
980
+ ```json
981
+ {
982
+ "object": "list",
983
+ "data": [
984
+ {
985
+ "id": "Qwen3-0.6B-GGUF",
986
+ "created": 1744173590,
987
+ "object": "model",
988
+ "owned_by": "lemonade",
989
+ "checkpoint": "unsloth/Qwen3-0.6B-GGUF:Q4_0",
990
+ "recipe": "llamacpp",
991
+ "size": 0.38,
992
+ "max_context_window": 40960,
993
+ "context_length": 8192,
994
+ "downloaded": true,
995
+ "suggested": true,
996
+ "update_available": false,
997
+ "labels": ["reasoning"]
998
+ },
999
+ {
1000
+ "id": "Gemma-3-4b-it-GGUF",
1001
+ "created": 1744173590,
1002
+ "object": "model",
1003
+ "owned_by": "lemonade",
1004
+ "checkpoint": "ggml-org/gemma-3-4b-it-GGUF:Q4_K_M",
1005
+ "recipe": "llamacpp",
1006
+ "size": 3.61,
1007
+ "downloaded": true,
1008
+ "suggested": true,
1009
+ "labels": ["hot", "vision"]
1010
+ },
1011
+ {
1012
+ "id": "SD-Turbo",
1013
+ "created": 1744173590,
1014
+ "object": "model",
1015
+ "owned_by": "lemonade",
1016
+ "checkpoint": "stabilityai/sd-turbo:sd_turbo.safetensors",
1017
+ "recipe": "sd-cpp",
1018
+ "size": 5.2,
1019
+ "downloaded": true,
1020
+ "suggested": true,
1021
+ "labels": ["image"],
1022
+ "image_defaults": {
1023
+ "steps": 4,
1024
+ "cfg_scale": 1.0,
1025
+ "width": 512,
1026
+ "height": 512
1027
+ }
1028
+ }
1029
+ ]
1030
+ }
1031
+ ```
1032
+
1033
+ **Field Descriptions:**
1034
+
1035
+ - `object` - Type of response object, always `"list"`
1036
+ - `data` - Array of model objects with the following fields:
1037
+ - `id` - Model identifier (used for loading and inference requests)
1038
+ - `created` - Unix timestamp of when the model entry was created
1039
+ - `object` - Type of object, always `"model"`
1040
+ - `owned_by` - Owner of the model, always `"lemonade"`
1041
+ - `checkpoint` - Full checkpoint identifier on Hugging Face
1042
+ - `recipe` - Backend/device recipe used to load the model (e.g., `"ryzenai-llm"`, `"llamacpp"`, `"flm"`)
1043
+ - `size` - Model size in GB (omitted for models without size information)
1044
+ - `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.
1045
+ - `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).
1046
+ - `downloaded` - Boolean indicating if the model is downloaded and available locally
1047
+ - `update_available` - Boolean indicating a newer commit exists on HuggingFace for this model. Only set for downloaded HF-backed models. `false` otherwise.
1048
+ - `suggested` - Boolean indicating if the model is recommended for general use
1049
+ - `labels` - Array of tags describing the model's capabilities and characteristics. See [Model Labels](#model-labels) for the full list.
1050
+ - `image_defaults` - (Image models only) Default generation parameters for the model:
1051
+ - `steps` - Number of inference steps (e.g., 4 for turbo models, 20 for standard models)
1052
+ - `cfg_scale` - Classifier-free guidance scale (e.g., 1.0 for turbo models, 7.5 for standard models)
1053
+ - `width` - Default image width in pixels
1054
+ - `height` - Default image height in pixels
1055
+ - `components` - (Omni collections only, `recipe: "collection.omni"`) Ordered array of the component model names that make up the collection
1056
+ - `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`](./lemonade.md#post-v1pull)
1057
+
1058
+
1059
+ ### Model Labels
1060
+
1061
+ Labels describe what a model can do. A model may carry multiple labels.
1062
+
1063
+ **Deployment labels** — determine which backend endpoint the model is routed to.
1064
+ Every model names exactly one deployment mode, and a model is never given two
1065
+ labels that name different modes:
1066
+
1067
+ | Label | Endpoint | Description |
1068
+ |-------|----------|-------------|
1069
+ | `chat` | `/chat/completions`, `/completions`, `/responses` | Text-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. |
1070
+ | `transcription` | `/audio/transcriptions` | Speech-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. |
1071
+ | `embeddings` | `/embeddings` | Produces text embedding vectors. Also accepted as `embedding`. |
1072
+ | `reranking` | `/rerank` | Scores and reranks a list of passages given a query. Also reachable at the aliases `/reranking` and `/reranker`. |
1073
+ | `image` | `/images/generations`, `/images/edits`, `/images/variations` | Text-to-image generation model. |
1074
+ | `tts` | `/audio/speech` | Text-to-speech synthesis model. |
1075
+ | `audio-generation` | `/audio/generations` | Text-to-audio generation model (e.g. music, sound effects). |
1076
+ | `classification` | `/classify` | Text classification model. Also accepted as `classifier`. |
1077
+ | `3d` | `/3d/generations` | Text- or image-to-3D mesh generation model. |
1078
+
1079
+ When a model declares no deployment label at all, it inherits its recipe's
1080
+ default — `chat` for `llamacpp`, `flm`, `ryzenai-llm`, `vllm` and `cloud`,
1081
+ `transcription` for `whispercpp`, `image` for `sd-cpp`, `tts` for `kokoro`, and
1082
+ so on.
1083
+
1084
+ Two label sets describe a model that cannot exist, and are refused rather than
1085
+ repaired:
1086
+
1087
+ - **A mode the recipe's backend does not serve.** `/classify` is served only by
1088
+ `onnxruntime`, so `labels: ["classification"]` on a `llamacpp` model is an
1089
+ error — register it as the chat model it is.
1090
+ - **Two different modes.** `labels: ["chat", "embeddings"]` on a `llamacpp` model
1091
+ is an error even though llama.cpp serves both: the subprocess is launched for
1092
+ one mode, so the second would name an endpoint it was never configured to
1093
+ answer. Register one model per mode. The legacy `embedding` and `reranking`
1094
+ booleans count as mode claims here, exactly as the labels do.
1095
+
1096
+ [`POST /v1/pull`](./lemonade.md#post-v1pull) answers `400` and registers nothing.
1097
+ An entry already stored in `user_models.json` — written before these rules — is
1098
+ skipped at startup with an error naming it, and the file is left untouched so it
1099
+ can be corrected by hand.
1100
+
1101
+ **Input-modality labels** — the model accepts additional input types in `/chat/completions`:
1102
+
1103
+ | Label | Description |
1104
+ |-------|-------------|
1105
+ | `vision` | Accepts image attachments in chat messages. |
1106
+ | `chat-transcription` | Accepts 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`. |
1107
+
1108
+ **Streaming labels** — capability flags for real-time features:
1109
+
1110
+ | Label | Description |
1111
+ |-------|-------------|
1112
+ | `realtime-transcription` | Supports the WebSocket `/realtime` endpoint for live microphone transcription. |
1113
+
1114
+ **Runtime labels** — affect backend launch defaults:
1115
+
1116
+ | Label | Description |
1117
+ |-------|-------------|
1118
+ | `mtp` | Enables 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`. |
1119
+
1120
+ **Image capability labels** — carried alongside `image`; they refine what the model is offered for without changing its deployment mode:
1121
+
1122
+ | Label | Description |
1123
+ |-------|-------------|
1124
+ | `edit` | Tuned for editing an input image (`/images/edits`). Also selects the model for the `edit_image` role in an omni collection. |
1125
+ | `upscaling` | Image upscaling model (e.g. Real-ESRGAN, `/images/upscale`). Used as a component in image pipelines rather than offered on its own. |
1126
+
1127
+ **Characteristic labels** — informational, do not affect routing:
1128
+
1129
+ | Label | Description |
1130
+ |-------|-------------|
1131
+ | `hot` | Featured or popular model, highlighted in the UI. |
1132
+ | `reasoning` | Uses extended chain-of-thought reasoning (e.g. DeepSeek, Qwen3). |
1133
+ | `tool-calling` | Supports function/tool calling in chat completions. |
1134
+ | `coding` | Tuned for code generation and software tasks. |
1135
+ | `experimental` | Not yet validated for production use. |
1136
+
1137
+
1138
+ ## `GET /v1/models/{model_id}`
1139
+ <sub>![Status](https://img.shields.io/badge/status-fully_available-green)</sub>
1140
+
1141
+ Retrieve a specific model by its ID. Returns the same model object format as the list endpoint above.
1142
+
1143
+ ### Parameters
1144
+
1145
+ | Parameter | Required | Description |
1146
+ |-----------|----------|-------------|
1147
+ | `model_id` | Yes | The ID of the model to retrieve. Must match one of the model IDs from the [models list](https://lemonade-server.ai/models.html). |
1148
+
1149
+ ### Example request
1150
+
1151
+ ```bash
1152
+ curl http://localhost:13305/v1/models/Qwen3-0.6B-GGUF
1153
+ ```
1154
+
1155
+ ### Response format
1156
+
1157
+ Returns a single model object with the same fields as described in the [models list endpoint](#get-v1models) 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](../guide/configuration/custom-models.md#share-a-collection-export-import-and-hugging-face).
1158
+
1159
+ ```json
1160
+ {
1161
+ "id": "Qwen3-0.6B-GGUF",
1162
+ "created": 1744173590,
1163
+ "object": "model",
1164
+ "owned_by": "lemonade",
1165
+ "checkpoint": "unsloth/Qwen3-0.6B-GGUF:Q4_0",
1166
+ "recipe": "llamacpp",
1167
+ "size": 0.38,
1168
+ "max_context_window": 40960,
1169
+ "context_length": 8192,
1170
+ "downloaded": true,
1171
+ "suggested": true,
1172
+ "labels": ["reasoning"],
1173
+ "recipe_options": {
1174
+ "ctx_size": 8192,
1175
+ "llamacpp_args": "--no-mmap",
1176
+ "llamacpp_backend": "rocm"
1177
+ }
1178
+ }
1179
+ ```
1180
+
1181
+ ### Error responses
1182
+
1183
+ If the model is not found, the endpoint returns a 404 error:
1184
+
1185
+ ```json
1186
+ {
1187
+ "error": {
1188
+ "message": "Model Qwen3-0.6B-GGUF has not been found",
1189
+ "type": "not_found"
1190
+ }
1191
+ }
1192
+ ```