Skip to main content
Chapter 7

API

LocalAI serves one HTTP API. These pages describe the parts of it that need more than an endpoint list.

Task pages that use the API, such as text generation, embeddings and image generation, are in Features. The server also publishes its own interactive API reference at /swagger/index.html.

Subsections of API

OpenAI Functions and Tools

Function calling: one tool-call request shape, each backend's native parser extracts the callsFunction calling: one tool-call request shape, each backend's native parser extracts the calls

LocalAI supports running the OpenAI functions and tools API across multiple backends. The OpenAI request shape is the same regardless of which backend runs your model - LocalAI is responsible for extracting structured tool calls from the model’s output before returning the response.

To learn more about OpenAI functions, see also the OpenAI API blog post.

LocalAI also supports JSON mode out of the box on llama.cpp-compatible models.

💡 Check out LocalAGI for an example on how to use LocalAI functions.

Supported backends

BackendHow tool calls are extracted
llama.cppC++ incremental parser; any ggml/gguf model works out of the box, no configuration needed
vllmvLLM’s native ToolParserManager - select a parser with tool_parser:<name> in the model options. Auto-set by the gallery importer for known families
vllm-omniSame as vLLM
mlxmlx_lm.tool_parsers - auto-detected from the chat template, no configuration needed
mlx-vlmmlx_vlm.tool_parsers (with fallback to mlx-lm parsers) - auto-detected from the chat template, no configuration needed

Reasoning content (<think>...</think> blocks from DeepSeek R1, Qwen3, Gemma 4, etc.) is returned in the OpenAI reasoning_content field on the same backends. When a model both reasons and calls a tool in the same turn, see Interleaved Thinking with Tool Calls for how the reasoning survives the tool-result round trip.

Setup

llama.cpp

No configuration required - the autoparser detects the tool call format for any ggml/gguf model that was trained with tool support.

Migrating older Ministral 3 configurations

The mistralai_ministral-3-14b-reasoning-2512-multimodal gallery entry uses the model’s embedded template and llama.cpp’s native tool parser. Older installations inherit the Mistral 0.3 prompt and JSON parser, which can return tool calls such as pick_tool{...} as text.

For an existing installation, replace the model YAML’s template and function sections with:

template:
  use_tokenizer_template: true
function:
  disable_no_action: true
  automatic_tool_parsing_fallback: true
  grammar:
    disable: true

Set use_jinja:true in the existing options list:

options:
  - use_jinja:true

Remove the inherited stopwords list so llama.cpp controls the model’s end-of-turn markers. Keep your model path, mmproj, sampling settings, and MCP configuration. Reload the model after saving the YAML. Gallery changes do not rewrite installed model configurations.

vLLM / vLLM Omni

The parser must be specified explicitly because vLLM itself doesn’t auto-detect one. Pass it via the model options:

name: qwen3-8b
backend: vllm
parameters:
  model: Qwen/Qwen3-8B
options:
  - tool_parser:hermes
  - reasoning_parser:qwen3
template:
  use_tokenizer_template: true

When you import a vLLM model through the LocalAI gallery, the importer looks up the model family and pre-fills tool_parser: and reasoning_parser: for you - you only need to override them for non-standard model names.

Available tool parsers include hermes, llama3_json, llama4_pythonic, mistral, qwen3_xml, deepseek_v3, granite4, kimi_k2, glm45, and more. Available reasoning parsers include deepseek_r1, qwen3, mistral, gemma4, granite. See the upstream vLLM documentation for the full list.

With backend: vllm, vLLM 0.30 or later, and a configured reasoning parser, streamed responses separate delta.reasoning_content, delta.content, and tool calls as tokens arrive. The final chunk does not repeat the generated text. If parsing fails during a stream, the backend buffers the remaining output and sends any matching, unsent suffixes in the final chunk. Older vLLM versions keep the previous streaming behavior and can include reasoning in delta.content.

MLX / MLX-VLM

MLX backends auto-detect the right tool parser by inspecting the model’s chat template - you don’t need to set anything. Just load an MLX-quantized model that was trained with tool support:

name: qwen2.5-0.5b-mlx
backend: mlx
parameters:
  model: mlx-community/Qwen2.5-0.5B-Instruct-4bit
template:
  use_tokenizer_template: true

The gallery importer will still append tool_parser: and reasoning_parser: entries to the YAML for visibility and consistency with the other backends, but those are informational - the runtime auto-detection in the MLX backend ignores them and uses the parser matched to the chat template.

Supported parser families: hermes/json_tools, mistral, gemma4, glm47, kimi_k2, longcat, minimax_m2, pythonic, qwen3_coder, function_gemma.

Usage example

You can configure a model manually with a YAML config file in the models directory, for example:

name: gpt-3.5-turbo
parameters:
  # Model file name
  model: ggml-openllama.bin
  top_p: 80
  top_k: 0.9
  temperature: 0.1

To use the functions with the OpenAI client in python:

from openai import OpenAI

messages = [{"role": "user", "content": "What is the weather like in Beijing now?"}]
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "Return the temperature of the specified region specified by the user",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "User specified region",
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "temperature unit"
                    },
                },
                "required": ["location"],
            },
        },
    }
]

client = OpenAI(
    # This is the default and can be omitted
    api_key="test",
    base_url="http://localhost:8080/v1/"
)

response =client.chat.completions.create(
    messages=messages,
    tools=tools,
    tool_choice ="auto",
    model="gpt-4",
)
#...

For example, with curl:

curl http://localhost:8080/v1/chat/completions -H "Content-Type: application/json" -d '{
  "model": "gpt-4",
  "messages": [{"role": "user", "content": "What is the weather like in Beijing now?"}],
  "tools": [
        {
            "type": "function",
            "function": {
                "name": "get_current_weather",
                "description": "Return the temperature of the specified region specified by the user",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "User specified region"
                        },
                        "unit": {
                            "type": "string",
                            "enum": ["celsius", "fahrenheit"],
                            "description": "temperature unit"
                        }
                    },
                    "required": ["location"]
                }
            }
        }
    ],
    "tool_choice":"auto"
}'

Return data:

{
    "created": 1724210813,
    "object": "chat.completion",
    "id": "16b57014-477c-4e6b-8d25-aad028a5625e",
    "model": "gpt-4",
    "choices": [
        {
            "index": 0,
            "finish_reason": "tool_calls",
            "message": {
                "role": "assistant",
                "content": "",
                "tool_calls": [
                    {
                        "index": 0,
                        "id": "16b57014-477c-4e6b-8d25-aad028a5625e",
                        "type": "function",
                        "function": {
                            "name": "get_current_weather",
                            "arguments": "{\"location\":\"Beijing\",\"unit\":\"celsius\"}"
                        }
                    }
                ]
            }
        }
    ],
    "usage": {
        "prompt_tokens": 221,
        "completion_tokens": 26,
        "total_tokens": 247
    }
}

Advanced

Use functions without grammars

The functions calls maps automatically to grammars which are currently supported only by llama.cpp, however, it is possible to turn off the use of grammars, and extract tool arguments from the LLM responses, by specifying in the YAML file no_grammar and a regex to map the response from the LLM:

name: model_name
parameters:
  # Model file name
  model: model/name

function:
  # set to true to not use grammars
  no_grammar: true
  # set one or more regexes used to extract the function tool arguments from the LLM response
  response_regex:
  - "(?P<function>\w+)\s*\((?P<arguments>.*)\)"

The response regex have to be a regex with named parameters to allow to scan the function name and the arguments. For instance, consider:

(?P<function>\w+)\s*\((?P<arguments>.*)\)

will catch

function_name({ "foo": "bar"})

Parallel tools calls

This feature is experimental and has to be configured in the YAML of the model by enabling function.parallel_calls:

name: gpt-3.5-turbo
parameters:
  # Model file name
  model: ggml-openllama.bin
  top_p: 80
  top_k: 0.9
  temperature: 0.1

function:
  # set to true to allow the model to call multiple functions in parallel
  parallel_calls: true

Use functions with grammar

It is possible to also specify the full function signature (for debugging, or to use with other clients).

The chat endpoint accepts the grammar_json_functions additional parameter which takes a JSON schema object.

For example, with curl:

curl http://localhost:8080/v1/chat/completions -H "Content-Type: application/json" -d '{
     "model": "gpt-4",
     "messages": [{"role": "user", "content": "How are you?"}],
     "temperature": 0.1,
     "grammar_json_functions": {
        "oneOf": [
            {
                "type": "object",
                "properties": {
                    "function": {"const": "create_event"},
                    "arguments": {
                        "type": "object",
                        "properties": {
                            "title": {"type": "string"},
                            "date": {"type": "string"},
                            "time": {"type": "string"}
                        }
                    }
                }
            },
            {
                "type": "object",
                "properties": {
                    "function": {"const": "search"},
                    "arguments": {
                        "type": "object",
                        "properties": {
                            "query": {"type": "string"}
                        }
                    }
                }
            }
        ]
    }
   }'

Grammars and function tools can be used as well in conjunction with vision APIs:

 curl http://localhost:8080/v1/chat/completions -H "Content-Type: application/json" -d '{
     "model": "llava", "grammar": "root ::= (\"yes\" | \"no\")",
     "messages": [{"role": "user", "content": [{"type":"text", "text": "Is there some grass in the image?"}, {"type": "image_url", "image_url": {"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" }}], "temperature": 0.9}]}'

💡 Examples

A full e2e example with docker-compose is available here.

Realtime API

The realtime voice loop: VAD to STT to LLM to TTS, over WebSocket or WebRTCThe realtime voice loop: VAD to STT to LLM to TTS, over WebSocket or WebRTC

LocalAI supports the OpenAI Realtime API which enables low-latency, multi-modal conversations (voice and text) over WebSocket.

To use the Realtime API, you need to configure a pipeline model that defines the components for Voice Activity Detection (VAD), Transcription (STT), Language Model (LLM), and Text-to-Speech (TTS).

Configuration

Create a model configuration file (e.g., gpt-realtime.yaml) in your models directory. For a complete reference of configuration options, see Model Configuration.

name: gpt-realtime
pipeline:
  vad: silero-vad-ggml
  transcription: whisper-large-turbo
  llm: qwen3-4b
  tts: tts-1

This configuration links the following components:

  • vad: The Voice Activity Detection model (e.g., silero-vad-ggml) to detect when the user is speaking.
  • transcription: The Speech-to-Text model (e.g., whisper-large-turbo) to transcribe user audio.
  • llm: The Large Language Model (e.g., qwen3-4b) to generate responses.
  • tts: The Text-to-Speech model (e.g., tts-1) to synthesize the audio response.

Make sure all referenced models (silero-vad-ggml, whisper-large-turbo, qwen3-4b, tts-1) are also installed or defined in your LocalAI instance.

A pipeline stage can name a failover chain; the stage then switches targets without closing the session.

Transcription backends can include end-of-utterance language tags, such as <en-US> or <de-DE>. LocalAI removes these tags from the user transcript sent to the LLM. The transcription events sent to the client retain the original tags. This prevents a tag from overriding the intended conversation language in the LLM context.

Install the Qwen streaming assistant preset

The gallery entry qwen3-streaming-assistant configures an English voice assistant with Silero VAD, Parakeet TDT 0.6B v3, Qwen3 0.6B, and Qwen3-TTS 0.6B CustomVoice using Ryan’s voice. It streams LLM output text into one persistent TTS session while audio is emitted.

The preset installs only its pipeline configuration. Install all four component models first, keeping their default names so the pipeline can resolve them:

local-ai models install silero-vad-ggml
local-ai models install parakeet-cpp-tdt-0.6b-v3
local-ai models install qwen3-0.6b
local-ai models install qwen3-tts-cpp-customvoice --variant qwen3-tts-cpp-customvoice
local-ai models install qwen3-streaming-assistant

The TTS command pins the Q8 CustomVoice entry. These are separate installs; installing the preset does not install missing components. Use a LocalAI build with incremental Qwen3-TTS input support, then select qwen3-streaming-assistant on the Talk page or as the Realtime API model. WebRTC also requires the opus backend, available from the Backends page.

This preset targets English speech. It uses a named voice and does not require reference audio or ref_text. CPU synthesis can be slower than playback; this preset does not establish GPU or browser latency or throughput.

Install a clause-buffered assistant preset

Two English presets use the same Silero VAD, Parakeet TDT 0.6B v3, and Qwen3 0.6B components. vits-streaming-assistant uses VITS-LJS through Sherpa; piper-streaming-assistant uses Piper’s Amy voice.

Install the shared components once, keeping their default names:

local-ai models install silero-vad-ggml
local-ai models install parakeet-cpp-tdt-0.6b-v3
local-ai models install qwen3-0.6b

Then install either TTS component and its matching preset, or install both:

local-ai models install vits-ljs-sherpa
local-ai models install vits-streaming-assistant
local-ai models install voice-en-us-amy-low
local-ai models install piper-streaming-assistant

These presets install pipeline configurations only. They do not install missing components. Both enable LLM streaming, TTS streaming, and clause_chunking: true. Sherpa streams audio within each clause. Piper emits one audio result per clause. Each clause starts a separate synthesis, which can introduce pauses between clauses. The single-speaker models need no voice override or reference audio.

Select the installed preset on the Talk page or use its name as the Realtime API model. WebSocket and WebRTC share the pipeline; WebRTC also needs the opus backend from the Backends page. Config installation and discovery checks do not establish speech quality, browser latency, or hardware throughput.

Streaming the pipeline

By default each stage runs to completion before the next begins: the whole utterance is transcribed, the full LLM reply is generated, then it is synthesized. Each stage can instead be streamed incrementally, which lowers the time-to-first-audio of a turn:

name: gpt-realtime
pipeline:
  vad: silero-vad-ggml
  transcription: whisper-large-turbo
  llm: qwen3-4b
  tts: tts-1
  streaming:
    llm: true             # stream LLM tokens as transcript deltas
    tts: true             # emit audio deltas per synthesized chunk
    transcription: true   # stream transcript text deltas of the user's speech
    clause_chunking: true # synthesize each clause as soon as it completes
  • streaming.tts: emit a response.output_audio.delta per audio chunk the TTS backend produces (requires a backend that supports streaming synthesis), instead of one delta for the whole utterance. Falls back to a single unary delta otherwise.
  • streaming.transcription: stream conversation.item.input_audio_transcription.delta events as the transcript is produced (requires a transcription backend that supports streaming).
  • streaming.llm: stream the LLM reply token-by-token as response.output_audio_transcript.delta events. When TTS streaming is disabled, the full reply is synthesized once it is complete unless explicit clause chunking is enabled. Native incremental input or explicitly enabled clause buffering can produce audio before the reply finishes, as described below. Reasoning/thinking is always stripped from the spoken transcript. Tool calls are supported while streaming when the LLM uses its tokenizer template (use_tokenizer_template: true): the backend’s autoparser then delivers content and tool calls separately, so the spoken transcript never leaks tool-call tokens. Grammar-based function calling keeps the buffered path.
  • Incremental Qwen3-TTS input: with a qwen3-tts-cpp TTS model, enabling both streaming.llm and streaming.tts forwards cleaned LLM text fragments into one persistent synthesis session. Audio can arrive before the LLM finishes. WebSocket and WebRTC use this path. It takes precedence over clause_chunking; other backends can opt into clause buffering as described below. Pending text is bounded and slow audio playback applies backpressure. Cancellation stops the session. A failed live utterance is not replayed through a fallback model. The initial native implementation supports a single batch slot and reference audio without ref_text; transcript-based ICL cloning is unavailable for live input. Incomplete words are buffered until a stable boundary; CJK, Kana and Hangul characters can commit without whitespace.
  • streaming.clause_chunking: instead of buffering the whole reply before TTS, split it into speakable clauses and synthesize each as soon as it completes, lowering the time-to-first-audio. The splitter is script-aware: it uses Unicode sentence segmentation (so it handles CJK 。!? with no whitespace), CJK clause punctuation (,、;:), and Thai/Lao spaces - it does not rely on whitespace sentence boundaries, so it works for languages such as Chinese, Japanese and Thai where the old per-sentence approach degraded to whole-message buffering. Requires streaming.llm; scripts that genuinely need a dictionary (e.g. Khmer, Burmese) simply stay buffered until a space or end-of-message. Set clause_chunking: true to enable a bounded clause queue. Unset and false retain whole-reply synthesis for backends without native incremental text input. With streaming.tts enabled, Sherpa/VITS can stream audio within each clause; unary engines such as Piper produce one audio result per clause. With streaming.tts disabled, each clause uses unary synthesis. These engines start a separate synthesis for each clause, so they may pause at clause boundaries. Cancellation stops queued work; a streaming failure after audio has been emitted never replays that clause through unary synthesis.

All streaming flags are off by default. Pipelines with streaming disabled remain unaffected.

Model warm-up (cold start)

Without warm-up the pipeline’s models are loaded into memory only on first use within a session: the VAD on the first audio chunk, transcription at the first end-of-speech, the LLM on the first reply, and TTS on the first spoken output. On a cold session this staggers a load delay across those first few interactions - and a model that fails to load (missing weights, wrong backend, out of memory) only fails part-way through the first turn.

To avoid that, LocalAI warms the pipeline by default: it loads the VAD, transcription, LLM and TTS backends into memory before the session is announced, and the session start blocks until they are all ready. The loads run concurrently, so the wait is the slowest single model, not the sum. This means:

  • The first turn pays no cold-start cost - every backend is already resident.
  • Model-load errors surface at session start. If any stage fails to load, the session is not started and the client receives a model_load_error instead of session.created, so a broken pipeline fails fast and visibly rather than mid-call.

Set disable_warmup: true to restore the lazy “load on first use” behavior - session start no longer waits on loading and load errors surface on the first turn instead. Useful if you want idle sessions to avoid holding model memory they may never use:

name: gpt-realtime
pipeline:
  vad: silero-vad-ggml
  transcription: whisper-large-turbo
  llm: qwen3-4b
  tts: tts-1
  disable_warmup: true   # lazily load each model on first use instead of at session start

Pre-loading a pipeline on demand

Warm-up only fires when a realtime session opens. To load a pipeline into memory ahead of time - e.g. to warm it right after boot, or when running with disable_warmup: true - POST the model name to the admin-only /backend/load endpoint. For a pipeline model it loads every configured sub-model (VAD, transcription, LLM, TTS, sound_detection, voice_recognition) concurrently:

curl -X POST http://localhost:8080/backend/load \
  -H "Content-Type: application/json" \
  -d '{"model": "gpt-realtime"}'

The endpoint is not realtime-specific - it pre-loads any model. See Backend Monitor for the full request/response reference (it is the inverse of /backend/shutdown).

Controlling response interruption

By default, speech detected by either VAD mode interrupts the active response. Set interrupt_response: false to let the response finish while the user speaks:

{
  "type": "session.update",
  "session": {
    "type": "realtime",
    "audio": {
      "input": {
        "turn_detection": {
          "type": "server_vad",
          "silence_duration_ms": 500,
          "interrupt_response": false
        }
      }
    }
  }
}

This option also applies to semantic_vad. Speech events and transcription continue. If a response is active when another turn commits, LocalAI retains that turn in conversation history but skips its automatic response. It does not queue a response for that turn. A later turn can generate a response once the active one finishes. To request a response manually, wait for both response.done and the overlapping turn’s conversation.item.added event, then send response.create. Transcription can finish after the active response. Explicit response.cancel and client response creation keep their existing behavior. When replacing the turn-detection settings, omit interrupt_response or set it to true to enable interruption again.

Turn detection

For client-controlled turns, set turn_detection to null in session.update. After sending audio, send input_audio_buffer.commit, followed by response.create. If transcription is still running, the response waits for the committed user item before starting the LLM. A standalone response.create with no pending client commit starts without this wait. If the commit produces no user item (transcription error, empty transcript or a voice gate rejection), the server does not run the LLM. It sends response.done with status failed and an input_not_committed error, after the error event that describes the cause. If you cancel the response while it waits, you get response.done with status cancelled.

Turn detection decides when the user has finished speaking and the pipeline should respond. Two modes are supported, matching the OpenAI session schema:

  • server_vad (default): silence-based. The VAD model watches the audio and the turn commits after silence_duration_ms (default 500 ms) of silence. Simple and model-agnostic, but a fixed silence window must trade interrupting mid-sentence pauses against sluggish responses.
  • semantic_vad: model-driven. The transcription model itself signals end-of-utterance and the silence window becomes dynamic: short right after the model emits its end-of-utterance token, much longer when it does not - so pausing to think no longer gets cut off, while finished sentences get a fast response.

semantic_vad requires a transcription model that emits an end-of-utterance token over a cache-aware streaming decode - currently parakeet-cpp-realtime_eou_120m-v1 (the model is trained to distinguish “paused, expecting a reply” from “paused mid-thought”). The realtime pipeline feeds it the microphone audio live while the user speaks. With any other transcription backend the session degrades gracefully to silence-only detection using the eagerness timeout below (a warning is logged once). The model also emits a distinct end-of-backchannel token (<EOB>) for short acknowledgments like “uh-huh”: those are transcribed but never treated as the user yielding the turn.

Sessions can opt in via session.update (turn_detection: {"type": "semantic_vad", "eagerness": "medium"}), or the pipeline can set a server-side default so clients need no changes:

name: gpt-realtime
pipeline:
  vad: silero-vad-ggml
  transcription: parakeet-cpp-realtime_eou_120m-v1
  llm: qwen3-4b
  tts: tts-1
  turn_detection:
    type: semantic_vad   # default for sessions on this model (server_vad if unset)
    eagerness: medium    # low | medium | high | auto (auto == medium)
    retranscribe: false  # see below
    # vad_window_sec: 6  # widen the per-tick VAD scan window (see below)

A client session.update still overrides type and eagerness per session.

VAD scan window: each turn-detection tick the VAD rescans only the most recent slice of buffered audio — sized automatically from the commit silence threshold (the server_vad silence window, or the semantic eagerness fallback) plus a warm-up margin, so long turns cost the same per tick as short ones. vad_window_sec widens the window if needed; values below the automatic floor are ignored. Buffered turn audio is retained for at most 90 s — a turn that genuinely never pauses (continuous speech or a noise source the VAD keeps classifying as speech) keeps only its most recent 90 s for the commit-time batch transcription (the semantic live stream is unaffected — it already consumed the audio incrementally).

Eagerness sets the fallback silence window used when no end-of-utterance token was seen (the model missed it, or the user genuinely trails off): low waits 8 s, medium/auto 4 s, high 2 s - the same max-timeout semantics OpenAI documents. After the token is seen, the turn commits on the next VAD tick (~300 ms).

Live captions: while the user speaks, semantic_vad streams conversation.item.input_audio_transcription.delta events under the item id the commit will later reuse, so clients can render the words as they are recognized. The completed event at commit carries the authoritative transcript and replaces the partial text (with retranscribe: true it may differ from the captions); a turn discarded before commit emits conversation.item.input_audio_transcription.failed so clients can retract its captions.

retranscribe (server-side only, semantic_vad only) cross-checks the streaming decode against a batch decode at commit time:

  • false (default): the transcript accumulated from the live stream is used as-is - the model runs once per utterance and the LLM starts immediately at commit.
  • true: the committed audio is re-transcribed offline. If the batch decode also ends with the end-of-utterance token the turn proceeds (using the batch transcript); if it does not, the commit is cancelled and the session keeps listening - treating the streaming token as a false positive. Both transcripts are compared and logged, which makes this mode a useful diagnostic for how well the streaming and batch decodes align, at the cost of one extra decode per turn.

Live speaker and sound events (parakeet-cpp)

When the semantic_vad transcription model is a parakeet-cpp model loaded with a diarization_model and/or sound_model companion (see Audio to Text), the realtime session also streams speaker and sound events while a turn is live, alongside the transcript deltas. Nothing needs to change on the client: unrecognized event types are ignored by standard OpenAI Realtime clients.

The transcription model, with its companions:

name: parakeet-realtime-scene
backend: parakeet-cpp
parameters:
  model: realtime_eou_120m-v1-f16.gguf
options:
  - diarization_model:nemotron-3-diarization-q8_0.gguf
  - sound_model:ced-tiny-q8_0.gguf

The realtime pipeline that uses it:

name: gpt-realtime
pipeline:
  vad: silero-vad-ggml
  transcription: parakeet-realtime-scene
  llm: qwen3-4b
  tts: tts-1
  turn_detection:
    type: semantic_vad

Each closed speaker segment emits a conversation.item.input_audio_transcription.segment event under the turn’s item id, with an empty text (the event exists to carry the speaker boundary, not a transcript - the transcript still comes from the ordinary delta/completed events):

{
  "type": "conversation.item.input_audio_transcription.segment",
  "item_id": "item_abc",
  "content_index": 0,
  "speaker": "0",
  "speaker_name": "Alice",
  "start": 1.92,
  "end": 4.10,
  "text": ""
}

speaker_name is the name of a voice registered through /v1/voice/register, and is present only when the model has a speaker_model: (or, for a parakeet-cpp bundle file, a speaker_component:) and the speaker was identified. A segment that closes before its speaker is identified has none, and later segments of the same speaker do. See Voice Recognition. The segments of the offline path below carry no speaker_name.

Each sound event emits a conversation.item.sound_detection event with one tag and the detection window’s start/end:

{
  "type": "conversation.item.sound_detection",
  "item_id": "item_abc",
  "content_index": 0,
  "detections": [{"label": "Chicken, rooster", "score": 0.91, "index": 99}],
  "start": 24.0,
  "end": 30.0
}

The start/end on both event types are seconds measured from the start of the current turn’s own audio, not the session or the WebSocket connection - the same base the streamed transcript words use.

The companion stream is opened fresh for each speech turn, alongside that turn’s ASR live session, and closed when the turn commits: whatever it had not yet emitted is drained and sent at that point. Because the diarization model runs a brand new session every turn, its speaker indices are scoped to the turn too - "speaker": "0" in one turn and "speaker": "0" in the next are not guaranteed to be the same person, even within the same conversation.

score is the peak score seen for that tag while the sound was live, not an average.

Limitation: under semantic_vad, live transcription (and so this companion stream) only runs during speech turns - it does not see audio between turns. A sound that happens while nobody is speaking is not detected this way. If you need sound events independent of speech turns, use the pipeline’s sound_detection model instead (see Sound Classification), which classifies each VAD-committed utterance on its own. Use one or the other, not both, on the same session - they overlap in purpose and would emit sound detections twice.

Speaker and sound events with an offline model (Parakeet TDT v3)

The live events above need a cache-aware streaming transcription model. An offline model such as Parakeet TDT 0.6B v3 (25 languages) runs under server_vad instead: each VAD-committed turn is transcribed as a whole. The gallery model parakeet-cpp-realtime-scene-tdt bundles it with Nemotron-3-Diarization and CED-Tiny, so one parakeet-cpp backend handles transcription, speakers and sounds. Point both transcription and sound_detection at it and turn on diarization:

name: gpt-realtime-scene
pipeline:
  vad: silero-vad-ggml
  transcription: parakeet-cpp-realtime-scene-tdt
  sound_detection: parakeet-cpp-realtime-scene-tdt
  diarization: true
  llm: qwen3-4b
  tts: tts-1

pipeline.diarization asks the transcription model for speaker labels on each committed turn and emits every labelled segment as a conversation.item.input_audio_transcription.segment event before the turn’s completed event. Unlike the live path, these segments carry their text:

{
  "type": "conversation.item.input_audio_transcription.segment",
  "item_id": "item_abc",
  "content_index": 0,
  "id": "seg_1",
  "speaker": "1",
  "start": 6.85,
  "end": 10.82,
  "text": "Well, I don't wish to see it any more, observed Phoebe, turning away her eyes."
}

sound_detection classifies the same committed audio and emits one conversation.item.sound_detection event per turn (see Sound Classification). As on the live path, times are relative to the turn’s audio and speaker labels are only consistent within a turn. pipeline.diarization is off by default: it needs a transcription model that diarizes (parakeet-cpp with a diarization_model companion), and some other backends fail a diarization request they cannot serve.

Choosing the sound model

Both scene models ship with CED-Tiny, the cheapest to run all the time. parakeet-cpp-realtime-scene-base and parakeet-cpp-realtime-scene-tdt-base are the same pipelines with CED-Base (86M, the largest CED), which tags sounds more confidently. parakeet-cpp-multilingual-diarization-speakers-sounds loads the same TDT, Nemotron-3-Diarization and CED-Tiny trio plus the WeSpeaker encoder, and is meant for offline /v1/audio/diarization calls with include_sounds=true (see Speaker Diarization). Any CED GGUF from mudler/ced-gguf (tiny, mini, small, base) works as sound_model. Measured on CPU (Ryzen 9 9950X3D) over a 37 s clip with two speakers and a rooster, as a fraction of real time:

CED-TinyCED-Base
Live scene stream (diarization low + sound), EOU path0.1030.125
Sound detection per committed turn, TDT path0.0050.031

The EOU model’s own ASR stream adds 0.016. Diarization dominates the live cost, so CED-Base keeps the live path about 7x faster than real time.

Disabling thinking

For reasoning models, you can force the pipeline LLM’s thinking off without editing the LLM model config:

pipeline:
  llm: qwen3-4b
  disable_thinking: true   # maps to enable_thinking=false for the realtime LLM

This is applied only to the realtime session’s copy of the LLM config, so it does not affect other users of the same model. Leave it unset to use the LLM model config’s own reasoning settings.

Conversation compaction (long sessions on CPU)

By default a realtime session feeds only the last max_history_items turns to the LLM; older turns are dropped and forgotten. On CPU, long calls also grow expensive as the prompt fills with verbatim history. Enable compaction to instead fold older turns into a rolling summary, so long calls stay cheap without losing earlier context.

Compaction works with two numbers:

  • max_history_items is the live window - the recent turns kept verbatim in the prompt.
  • compaction.trigger_items is the high-water mark - let the buffer grow to here, then summarize the overflow (everything above max_history_items) into a rolling memory and evict it. It must be greater than max_history_items; if it is not, it is clamped up.

The gap between the two controls how often summarization runs: a summary call fires roughly every (trigger_items - max_history_items) turns (here, about every 6 turns).

pipeline:
  max_history_items: 6        # live window - recent turns kept verbatim
  compaction:
    enabled: true
    trigger_items: 12         # summarize overflow back down to max_history_items
    summary_model: ""         # optional: a small model for the summary (CPU); default = pipeline LLM
    max_summary_tokens: 512
Tip

On CPU, set summary_model to a small, fast model so compaction never competes with the conversation LLM for compute. Left empty, the pipeline’s own LLM produces the summary.

Clients can also manage history directly via the now-supported conversation.item.delete, conversation.item.truncate, and input_audio_buffer.clear realtime events.

Classifier mode (LocalAI extension)

On hardware that can afford prompt processing but not token generation — a Raspberry Pi running a small LLM, for example — a realtime session can replace autoregressive generation with prefill-only classification: you register a fixed list of options, each user turn is scored against them with the Score primitive (a single forward pass, no decode), and the winning option’s canned reply is spoken and/or its canned tool call is emitted. On llama-cpp, scoring runs through the same server slot the LLM uses, so the conversation prefix stays KV-cached across turns, and all options are scored together in one batched decode: the shared prefix (prompt plus the options’ common token prefix) is processed once, then each option’s unique tail rides a forked sequence in a single forward pass. A warm turn costs roughly one pass over the new words plus one small batch over the option tails, independent of the option count.

Enable it in the pipeline config:

name: drone-pi
pipeline:
  vad: silero-vad-sherpa
  transcription: parakeet-cpp-realtime_eou_120m-v1
  llm: lfm2.5-1.2b-instruct       # scores AND (if asked) generates
  tts: vits-piper-en_US-amy-sherpa
  classifier:
    enabled: true
    threshold: 0.85               # softmax floor the winner must clear (see note below)
    fallback:
      mode: reply                 # none | reply | generate
      reply: "Say again?"
    options:
      - id: up
        description: the user asks the drone to move or fly up/higher
        reply: Going up.
        tool:
          name: move
          arguments: {direction: up}
      - id: greeting
        description: the user greets the assistant
        reply: Hello, ready to fly.

Or per session / per response from the client (the field is additive — OpenAI clients simply never send it):

{"type": "session.update", "session": {"type": "realtime", "localai_classifier": {
  "enabled": true, "threshold": 0.85,
  "options": [{"id": "up", "description": "...", "reply": "Going up.",
               "tool": {"name": "move", "arguments": {"direction": "up"}}}],
  "fallback": {"mode": "reply", "reply": "Say again?"}
}}}

Like tools, the option list is replaced wholesale on each update. A response.create may carry its own localai_classifier to override the session for one response — {"enabled": false} runs normal generation once.

How a classified response behaves:

  • The winner’s reply is emitted through the ordinary response events (spoken via TTS, or response.output_text.* in text-only mode), and its tool (if any) is emitted as a standard function_call item with exactly the configured arguments — the client executes it and reports back with conversation.item.create as usual. In classifier mode you typically should not send a follow-up response.create after the tool output: the canned reply already acknowledged the command, and the follow-up would classify a tool-output turn.
  • Every classified response also emits a localai.classifier.result event carrying the full softmax distribution, the chosen option id (empty when the fallback applied), the threshold, and the scoring latency — useful for visualizing confidence in a client UI.
  • A committed turn whose transcript is empty (the VAD fired on noise and the ASR heard no words) is never scored — an empty prompt produces a confidently arbitrary winner. The fallback applies directly: the result event carries an empty scores list, and generate mode falls through to generation.
  • Wake-word gating: set address: {names: ["drone"], mode: ignore} and the assistant only acts on turns that mention one of the names (“Drone go up”, not just “go up”). The check is a deterministic case-insensitive whole-word match on the latest transcript — deliberately not model-based: scoring cannot detect a missing name (a 1.2B scorer rates “go up” as addressed with p≈1.0 even with a dedicated addressing stage), while a literal match is exact and free. Unaddressed turns skip scoring entirely (ambient conversation costs nothing) and emit a result event with empty scores and fallback: "not_addressed"; mode: ignore completes the response silently, mode: reply speaks reply.
  • When no option clears threshold, fallback.mode decides: none completes the response with no output, reply speaks the canned fallback reply, and generate falls through to normal autoregressive generation (slow on weak hardware, but always available since the same model config serves both paths). Set the threshold high: with the default raw normalization a confident in-list pick lands near 1.0, while an out-of-list request spreads its probability across the options — measured on a 1.2B scorer, in-list utterances scored ≥0.97 and out-of-list ones peaked around 0.8, so a floor of ~0.85 separates them. A low threshold (say 0.35) practically never falls back. Also keep each option’s description narrowly scoped: a catch-all clause like “…or asks for help” turns that option into a magnet for every request the model cannot map, defeating the fallback.
  • Agentic follow-up turns (after a server-side assistant tool executes) always use generation — the option list describes user intents, not tool outputs.

Knobs that matter for latency and accuracy: keep option descriptions short (they all go into the scoring system prompt) and the option count small. By default only the latest user message is scored — earlier turns echo option names (the canned replies especially) and empirically make small scoring models re-choose the previous option regardless of the new command. history_items: N opts the trailing N conversation messages back in (role-labeled); only do that with a scorer large enough to weigh the context. normalization: mean divides each option’s joint log-prob by its token count — useful when option ids have very different lengths. The scoring model needs a Go-side chat template (template.chat / template.chat_message); without one the scoring prompt falls back to a generic ChatML envelope, which may be off-distribution for the model. Use classifier.model to score on a different config than the pipeline LLM (rarely needed).

Argument slots (hybrid classify-then-complete)

A canned tool call can leave holes for the model to fill: declare slots on the option’s tool and reference them as "{{name}}" in the arguments template. When the option wins, LocalAI runs a short grammar-constrained completion that continues the exact scoring prompt (so the llama.cpp prompt cache is already warm) with the chosen route JSON re-opened at the first slot — only the value tokens are free; everything else is pinned by the grammar. Number slots substitute unquoted, enum/string slots inside their quotes.

tool:
  name: move_drone
  arguments:
    direction: forward
    distance: "{{distance}}"
    units: "{{units}}"
  slots:
    - name: distance
      type: number            # number | enum | string
    - name: units
      type: enum
      values: [m, meters, ft, feet]
      default: m              # used if inference fails outright
      hint: assume m when the user gives no units

The option’s spoken reply can reference the same placeholders — reply: "Going forward {{distance}} {{units}}." — and the filled values are spliced in as plain text before the reply is emitted, so what the assistant says confirms what it inferred. Reply placeholders are optional (one that names no slot stays literal).

Slot declarations (and hints) are appended to the option’s description in the shared system prompt, so they also inform scoring and cost no extra per-turn tokens. The localai.classifier.result event carries the final arguments and a fill_latency_ms. On an inference failure the slots’ defaults apply (and template the reply); if any slot lacks a default the response fails (or falls through to generation with fallback.mode: generate). Slot filling requires completion in the scoring model’s known_usecases alongside score.

Registering an option list (pipeline seed or session.update) prewarms the scoring prompt in the background: one throwaway score prefills the option-list prompt and plants a state checkpoint exactly at the per-turn probe boundary. Every scoring call also declares that boundary (the probe-invariant prompt prefix) to the backend, which checkpoints there on each prefill — on hybrid/recurrent models (which cannot rewind their state arbitrarily, only restore checkpoints) this is what keeps every turn at probe-size cost instead of a full option-list re-prefill. A client that swaps option lists at runtime (e.g. voice-switched command modes) pays nothing on the first turn after a swap: the rewarm hides behind the acknowledgement reply, and it runs on every registration deliberately — if the list’s slot was evicted, the rewarm is exactly the re-prefill the next turn would otherwise pay in the foreground. Swapping between several lists? Give the scoring model a slot per list and make the prefix routing selective: options: [parallel:3, sps:0.5] (the default slot-similarity threshold of 0.1 funnels different lists onto one slot — they share enough prompt structure to clear it).

The concrete scoring model must declare score in known_usecases. A single llama.cpp model can serve ordinary inference and classification concurrently by declaring multiple use cases, for example known_usecases: [chat, completion, score]; LocalAI reserves the scoring slots only when score is present. Scoring also requires the unified KV cache, which is enabled by default, so a score-enabled model cannot set kv_unified:false.

Speech-to-speech models (experimental)

A speech-to-speech (S2S) model takes audio in and answers with audio, text or both, in one model. Instead of chaining VAD, transcription, an LLM and TTS, the session sends audio to the model and gets the reply back from it. The llama-cpp backend serves these models.

Warning

Speech-to-speech over the Realtime API is experimental. It needs a llama.cpp build with LFM3-Audio and realtime support, which the LocalAI llama.cpp pin must include. It has not been measured on real hardware, and its behaviour may change.

Configuration

The gallery has LiquidAI LFM3-Audio-2B in two quantizations (Q4_K_M or Q8_0 weights, both with the Q8_0 audio projector) and four shapes each. Every entry is independent: installing lfm3-audio-2b always gets the Q4_K_M build, never Q8_0.

Entry (Q4_K_M)Entry (Q8_0)ShapeVAD
lfm3-audio-2blfm3-audio-2b-q8One speech-to-speech modelnone, the client commits turns
lfm3-audio-2b-vadlfm3-audio-2b-q8-vadOne speech-to-speech modelsilero-vad-ggml
lfm3-audio-2b-pipelinelfm3-audio-2b-q8-pipelineChained pipeline on the same modelnone, the client commits turns
lfm3-audio-2b-pipeline-vadlfm3-audio-2b-q8-pipeline-vadChained pipeline on the same modelsilero-vad-ggml

The gallery cannot install one model as a dependency of another. The -vad entries name silero-vad-ggml in pipeline.vad, so install it first with local-ai models install silero-vad-ggml. Without it, the session fails to start with a model-not-found error.

Install one with local-ai models install lfm3-audio-2b. Its model configuration looks like this:

name: lfm3-audio-2b
backend: llama-cpp
context_size: 8192
mmproj: mmproj-LFM3-Audio-2B-Q8_0.gguf
parameters:
  model: LFM3-Audio-2B-Q4_K_M.gguf
options:
  - parallel:2
known_usecases: [realtime_audio, chat, transcript, tts]
pipeline:
  any_to_any: true
template:
  use_tokenizer_template: true
  • pipeline.any_to_any: true runs the model as one speech-to-speech model. Without it, a realtime_audio model keeps the chained path (VAD, transcription, LLM, TTS).
  • parallel:2 gives the model two slots. One is held by the session. The other serves transcription, TTS or chat calls at the same time.

Chained pipeline on LFM3

The -pipeline entries do not use the speech-to-speech stream. They run the classic realtime pipeline (VAD, transcription, LLM, TTS), with the transcription, chat and speech stages all served by the one llama.cpp model: speech recognition through the transcription path, the reply through the chat path (with its templates, tools and streaming), and speech synthesis through the TTS path with the named voice female (or male). Choose it when you want the regular chat path or the text of each stage; choose the speech-to-speech entries for the lowest latency. The model configuration names itself in each slot and is not a realtime_audio model:

name: lfm3-audio-2b-pipeline-vad
backend: llama-cpp
mmproj: mmproj-LFM3-Audio-2B-Q8_0.gguf
parameters:
  model: LFM3-Audio-2B-Q4_K_M.gguf
known_usecases: [chat, transcript, tts]
tts:
  voice: female
pipeline:
  vad: silero-vad-ggml
  transcription: lfm3-audio-2b-pipeline-vad
  llm: lfm3-audio-2b-pipeline-vad
  tts: lfm3-audio-2b-pipeline-vad

Keep the installed model name, because the slots refer to it. The -pipeline entries have no vad line. A chained pipeline without a VAD starts with turn detection off, refuses turn_detection of type server_vad in session.update, and expects input_audio_buffer.commit followed by response.create.

Migrating from the liquid-audio backend

The Python liquid-audio backend is removed, together with its importer and its gallery entries. It served LFM2-Audio models. Use the llama-cpp backend with lfm3-audio-2b or lfm3-audio-2b-q8 instead, and set pipeline.any_to_any: true as shown above. A model with backend: llama-cpp and realtime_audio but without pipeline.any_to_any is refused with invalid_pipeline, because llama.cpp has no VAD, transcription or TTS of its own.

LFM2.5-Audio is not covered: llama.cpp does not run it, so there is no replacement for it. Remove the old model configurations that name backend: liquid-audio.

Voice activity detection

Speech-to-speech models do not have a VAD of their own. Turn detection depends on the pipeline:

  • With a VAD model (pipeline.vad, for example silero-vad-ggml, installed separately), the server detects the start and end of each turn. Use turn_detection with server_vad as in the rest of this page. Audio is forwarded from 300 ms before speech starts, so silence before the user speaks is not added to the model context.
  • Without a VAD model (the plain lfm3-audio-2b entries), server VAD is off. The client ends each turn with input_audio_buffer.commit and then sends response.create. Set turn_detection to null in session.update to make this explicit.

Tools

Define tools in session.update as usual. When the model calls one, the session sends response.output_item.added with a function_call item, then response.function_call_arguments.delta and response.function_call_arguments.done, and then response.done. To return the result, send a conversation.item.create with type: function_call_output, the same call_id and the output string, and then response.create. The model answers with the result in the next reply. A reply that is only a tool call has no audio.

How a call is read depends on the model:

  • The backend reads the text of the reply with the parser and grammar of the model’s chat template, as the llama.cpp server does for /v1/realtime. The call is cut out of the transcript. The audio is sent as the model produces it, also after a call started. tool_choice (auto, none or required) works in session.update and in response.create, and the response value wins. A named function cannot be forced: session.update and response.create refuse it with an error. The backend knows only the tools of the session, so a tools list on response.create does not change what it reads. With tool_choice set to none, a call the model writes anyway stays text.
  • If the backend found no call and the request had tools, LocalAI parses the raw text of the reply with the function settings of the model, the way the chained pipeline does. Set function.xml_format_preset (the LFM3-Audio gallery entries set it to lfm3) or the other function options for a format the backend parser does not know. In this case the call text can already be in the streamed transcript, and the transcript is replaced only if function.capture_llm_results captures the content.

Transcripts

  • Reply transcript. response.output_audio_transcript.delta and response.output_audio_transcript.done carry the text of the spoken reply. In a text-only response, response.output_text.delta is used instead.
  • User transcript. The stream answers from the audio, so an any-to-any model does not transcribe the input unless pipeline.transcription is set. When it names another model (a sherpa-onnx ASR, for example), conversation.item.input_audio_transcription.completed is sent for the user item, in parallel with the reply. When it names the model itself, the transcript is made on a second slot before the reply starts, so it arrives before the first output of that reply (for a manual commit and for a VAD turn), and it delays the reply by its own run time. Both share one llama.cpp context, and while they run together llama.cpp rebuilds its buffers on every step, which slows the reply to a few tokens per second; this is why the default is no transcript. The transcript of the reply always comes from the stream. If transcription fails, conversation.item.input_audio_transcription.failed is sent and the reply continues.

Barge-in

Interruption works as described in Controlling response interruption. When the user starts speaking during a reply, the server stops the reply, sends response.done with status: cancelled, and handles the new speech as the next turn. The part of the reply that was already decoded stays in the model context, as in llama.cpp.

Limits

  • Audio format. The model listens to 16 kHz mono audio. LocalAI resamples the session input to 16 kHz. Output audio is 24 kHz mono PCM.
  • Voice. The voice setting is not applied to speech-to-speech output. llama.cpp’s realtime slot has no voice selection. The voice setting still applies to the text-to-speech endpoint.
  • Context. A session holds one slot for its whole life, and all its audio stays in the context. A long session reaches context_size. The session then sends an error and closes. There is no compaction for speech-to-speech sessions.
  • Session updates after the first turn. Once the first audio or message has been added, instructions, tools and voice cannot change. A session.update that tries to change them gets an error. output_modalities and max_output_tokens can still change.
  • Not supported. conversation.item.truncate and conversation.item.delete return an error, because the context cannot be cut by audio time. input_audio_buffer.clear drops audio that has not been committed. Without a VAD model, the server keeps appended audio in its buffer and sends it to the model only at input_audio_buffer.commit (or at a response.create that finds uncommitted audio), so a clear discards it for good. With server VAD, audio is streamed to the model during a turn, and what was already sent stays in the context.
  • Pipeline features. The pipeline features that need a separate LLM call are rejected with invalid_pipeline: classifier mode, conversation compaction with a summary, streaming.llm, and voice recognition gating.
  • Distributed mode. The conversation state lives in one backend slot, so a session stays on the instance that opened it until it ends. See distributed mode for how instances are chosen.

Transports

The Realtime API supports two transports: WebSocket and WebRTC.

WebSocket

Connect to the WebSocket endpoint:

ws://localhost:8080/v1/realtime?model=gpt-realtime

Audio is sent and received as raw PCM in the WebSocket messages, following the OpenAI Realtime API protocol.

In the chained pipeline, committed user audio reaches the transcription backend as 16 kHz mono WAV, whichever way the turn was committed: server-side VAD and client input_audio_buffer.commit both resample from the session input sample rate (for example 24 kHz) first.

WebRTC

The WebRTC transport enables browser-based voice conversations with lower latency. OpenAI-compatible clients can send a raw SDP offer and select the model with the query parameter:

POST http://localhost:8080/v1/realtime/calls?model=gpt-realtime
Content-Type: application/sdp

<SDP offer body>

The response has the application/sdp content type and contains the bare SDP answer.

The unified OpenAI interface is also supported. Send multipart/form-data with an sdp field that contains the offer and a JSON session field. LocalAI reads the model from the session object:

curl http://localhost:8080/v1/realtime/calls \
  -F "sdp=<offer.sdp;type=application/sdp" \
  -F 'session={"type":"realtime","model":"gpt-realtime"};type=application/json'

LocalAI also accepts its original JSON request format for compatibility. A JSON request contains top-level sdp and model fields and receives a JSON response with sdp and session_id fields.

Opus backend requirement

WebRTC uses the Opus audio codec for encoding and decoding audio on RTP tracks. The opus backend must be installed for WebRTC to work. Install it from the Backends page in the web UI, or from the backend gallery with the API:

curl -X POST http://localhost:8080/backends/apply \
  -H "Content-Type: application/json" \
  -d '{"id": "opus"}'

For a local binary installation, you can instead use the CLI:

local-ai backends install opus

Or set the EXTERNAL_GRPC_BACKENDS environment variable if running a local build:

EXTERNAL_GRPC_BACKENDS=opus:/path/to/backend/go/opus/opus

The opus backend is loaded automatically when a WebRTC session starts. It does not require any model configuration file - just the backend binary.

WebRTC behind Docker host networking or NAT

By default pion gathers a host ICE candidate for every local interface. Under Docker host networking that includes bridge addresses (docker0/veth, 172.x) that a remote browser cannot route to: the call typically connects on a good candidate and then drops a few seconds later when ICE consent checks fail on the unreachable ones. Two settings let you advertise only the reachable address:

# Advertise these IPs as the host ICE candidates (e.g. the host's LAN IP)
LOCALAI_WEBRTC_NAT_1TO1_IPS=192.168.1.10

# ...or restrict ICE gathering to specific interfaces
LOCALAI_WEBRTC_ICE_INTERFACES=eth0
Tip

For a browser on another LAN machine talking to LocalAI in a host-networked container, set LOCALAI_WEBRTC_NAT_1TO1_IPS to the host’s LAN IP. This is the most reliable fix for WebRTC connections that establish and then drop.

LocalAI never offers deprecated IPv6 addresses as candidates. After an IPv6 prefix change (many routers rotate the prefix, often daily) the old addresses stay on the interface as deprecated until their lifetime runs out; ICE checks over them time out and the call never connects. On Linux the kernel’s address flags (/proc/net/if_inet6) are read on every gathering, so this needs no setting.

Fixed WebRTC UDP port

By default, each WebRTC peer connection uses an ephemeral UDP port. To route all realtime WebRTC ICE traffic through one shared port, start LocalAI with --web-rtc-udp-port 3478 or set LOCALAI_WEBRTC_UDP_PORT=3478.

When running in a container, publish the same port with the UDP protocol:

docker run -p 8080:8080 -p 3478:3478/udp \
  -e LOCALAI_WEBRTC_UDP_PORT=3478 localai/localai:latest

Allow the selected UDP port through the host and network firewalls. If LocalAI cannot bind it, WebRTC signaling requests return an HTTP 500 error describing the bind failure.

Protocol

The API follows the OpenAI Realtime API protocol for handling sessions, audio buffers, and conversation items.

Response modalities (text-only sessions)

By default a realtime session responds with audio plus a transcript. To make the model respond with text only, set the output modalities on either the session or an individual response:

{"type": "session.update", "session": {"output_modalities": ["text"]}}
{"type": "response.create", "response": {"output_modalities": ["text"]}}

output_modalities is the GA field name. For compatibility, LocalAI also accepts the legacy Realtime beta field name modalities as an alias (a lot of community sample code still sends modalities: ["text"]):

{"type": "session.update", "session": {"modalities": ["text"]}}

The GA output_modalities wins when both are present. A response-level value overrides the session-level one, and when neither is set the session falls back to ["audio"].

Out-of-band responses and metadata

A response can be created outside the default conversation by setting conversation to none. The reply is not added to the conversation history, which is what makes it usable for a side channel — answering a chat message or a webhook while a spoken conversation is in progress.

Because such a response arrives on the same socket as everything else, attach metadata to correlate it. LocalAI echoes the map back verbatim on both response.created and response.done:

{"type": "response.create", "response": {
  "conversation": "none",
  "output_modalities": ["text"],
  "metadata": {"client_run": "abc123"},
  "input": [{"type": "message", "role": "user",
             "content": [{"type": "input_text", "text": "Is the oven still on?"}]}]
}}
{"type": "response.done", "response": {
  "id": "resp_...", "status": "completed",
  "metadata": {"client_run": "abc123"}
}}

Keys are strings up to 64 characters, values up to 512. A response created without metadata omits the field rather than sending an empty object.

Gating a realtime pipeline with voice recognition

A pipeline realtime model can require speaker verification before it responds. Add a voice_recognition block under pipeline. When present, each committed utterance is verified against authorized speakers; unauthorized utterances are dropped before the LLM runs (no LLM call, no tool execution, no TTS). The session stays open.

The same block also drives two optional, independent behaviors: an authorization gate (enforce) and speaker surfacing/personalization (identity). Set enforce: false to keep recognizing the speaker without ever rejecting a turn.

name: my-realtime
pipeline:
  vad: silero-vad
  transcription: whisper
  llm: qwen
  tts: kokoro
  voice_recognition:
    model: speaker-recognition   # the speaker-recognition backend model
    mode: identify               # "identify" (registry) or "verify" (references)
    threshold: 0.25              # cosine distance; <= passes
    enforce: true                # authorization gate (default true)
    when: every                  # "every" (default) or "first"
    on_reject: drop_event        # "drop_event" (default) or "drop_silent"
    anti_spoofing: false         # optional liveness check (verify mode)

    # identify mode: authorized registry identities (multiple persons)
    allow:
      names: ["alice", "bob"]    # match registered speaker names
      labels: ["family"]         # OR any identity carrying this label
      # empty allow = any registered speaker within threshold passes

    # verify mode: reference speakers (multiple persons)
    references:
      - name: alice
        audio: /models/voices/alice.wav
      - name: bob
        audio: /models/voices/bob.wav

One bundle for every stage

A parakeet-cpp bundle can fill the speaker stage as well as the others, so one model serves vad, transcription, sound_detection and voice_recognition. The bundle config must list speaker_recognition in known_usecases (the gallery entries do), and the libparakeet.so must export parakeet_capi_speaker_embed_pcm:

name: my-realtime
pipeline:
  vad: parakeet-cpp-bundle-small
  transcription: parakeet-cpp-bundle-small
  sound_detection: parakeet-cpp-bundle-small
  llm: qwen
  tts: kokoro
  voice_recognition:
    model: parakeet-cpp-bundle-small
    mode: identify
    threshold: 0.5               # WeSpeaker distance; see Voice Recognition

The bundle’s encoder has the same weights as voice-detect-wespeaker-resnet34, so voices registered with that model are matched. Embedding an utterance uses the same engine lock as transcription, so it adds to the turn latency. Voices from a 192-dimension model (ECAPA-TDNN, for example) cannot be matched by a bundle; see Voice Recognition.

Identifying speakers without gating

To recognize who is speaking and surface it to the client and the LLM without ever rejecting a turn, set enforce: false and add an identity block. The identity block works with or without the gate; when it is set, the speaker is resolved on every turn even if when: first.

name: my-realtime
pipeline:
  vad: silero-vad
  transcription: whisper
  llm: qwen
  tts: kokoro
  voice_recognition:
    model: speaker-recognition
    mode: identify
    threshold: 0.25
    # Authorization gate. Defaults to enforcing (rejects unauthorized speakers).
    # Set enforce:false to identify the speaker WITHOUT rejecting anyone.
    enforce: false
    when: every
    # Surface the recognized speaker to the client and the LLM. Works with or
    # without enforce; when set, identity is resolved on every turn even if
    # when:first.
    identity:
      announce: true            # emit the conversation.item.speaker event
      announce_unknown: false   # also emit it when there is no confident match
      personalize: true         # tell the LLM who is speaking
      inject_name: true         # set the per-message OpenAI name field
      inject_system_note: true  # append a "current speaker" line to the system message
      note_unknown: false       # append a "speaker is unknown" note when unidentified
FieldMeaning
modelSpeaker-recognition backend model name.
modeidentify matches against speakers registered via /v1/voice/register; verify matches against the references audios.
thresholdMaximum cosine distance that still counts as a match (default ~0.25).
enforceAuthorization gate. true (or omitted) rejects unauthorized speakers (the gating behavior above). false resolves and surfaces the speaker without ever dropping a turn.
whenevery verifies each utterance; first verifies once then trusts the session. When an identity block is set, the speaker is still resolved on every turn even with first.
on_rejectdrop_event drops and emits a speaker_not_authorized error event; drop_silent drops quietly.
anti_spoofingVerify mode only: runs the backend liveness check (slower).
allow.names / allow.labelsidentify mode: which registry identities are authorized. Empty = any registered speaker.
referencesverify mode: authorized reference speakers; the utterance passes if it matches any.
identity.announceEmit the conversation.item.speaker event to the client (see below).
identity.announce_unknownAlso emit that event when there is no confident match. By default the event is emitted only on a match.
identity.personalizeInform the LLM who is speaking.
identity.inject_nameSet the per-message OpenAI name field on each user turn.
identity.inject_system_noteAppend a The current speaker is <Name>. line to the system message.
identity.note_unknownWhen unidentified, append The current speaker is unknown. (lets the model ask who it is talking to).

identify mode requires the voice registry (speakers registered through /v1/voice/register). verify mode needs no registry: reference audios are embedded once at model load.

The conversation.item.speaker event

When identity.announce is enabled, the server emits a conversation.item.speaker event after the user conversation item, naming the recognized speaker. In a transcription-only session there is no conversation item, so the server emits the event before the conversation.item.input_audio_transcription.completed event of the turn, with the same item_id:

{
  "type": "conversation.item.speaker",
  "item_id": "item_abc",
  "speaker": { "name": "Jeremy", "id": "spk_1", "labels": { "role": "owner" }, "confidence": 92.0, "distance": 0.1, "matched": true }
}

confidence is a 0-100 score, distance is the cosine distance, and matched is true when a confident match was found. labels carries any labels attached to the registered speaker (identify mode); it is omitted when the speaker has none. The name and id fields are omitted when empty. By default the event is emitted only on a match; set identity.announce_unknown: true to also emit it (with matched: false) when no speaker is identified.

This event is a LocalAI extension to the OpenAI Realtime API and is server-emitted only. Standard OpenAI Realtime clients ignore event types they do not recognize, so enabling it is non-breaking.

Examples

  • Realtime voice assistant demo (Go): a minimal Go client for the Realtime (WebSocket) API with a full talk-back voice loop and an example tool call. Ships a docker compose setup that brings up a realtime-capable LocalAI for you.
  • Realtime voice assistant example (Python): thin-client architecture (Silero VAD on the client, heavy lifting on LocalAI), suited to running the client on a Raspberry Pi.

Talk in the web UI

The Talk page of the web UI is a client for this API over WebRTC. Pick a pipeline model with the chip at the top, then press Start session. The microphone streams while the session runs. The server detects when you stop talking and answers on its own, and you can speak over a reply to interrupt it. The page has no push-to-talk or hands-free switch. A speech-to-speech model with no VAD model set has no turn detection: the page shows Done speaking, which ends the turn and asks for the reply.

The heading under the orb says what is happening: connecting, listening, thinking (also while a tool runs), speaking, or interrupted after you cut a reply off. The orb follows the real microphone level while it listens and the reply’s audio while it speaks. These states have their own screen:

  • Microphone blocked. The browser did not give the page the microphone. Allow it in the address bar and press Try again.
  • Connection lost. The WebRTC link failed during a session. The transcript stays and Reconnect starts a new session.
  • Something went wrong. The server reported an error or the call could not be set up. The reason is shown, with a link to the traces.
  • Talk needs a pipeline model. No pipeline model exists yet. The page links to the model editor with the pipeline template and to the gallery.

The transcript shows what you said, the replies and, in Manage mode, the tool calls and results. Copy puts it on the clipboard. It is not saved.

The sliders button opens the session settings: the instructions, the voice, the transcription language, client-side MCP tools, Manage mode (admin, fixed once a session is open) and the parts of the selected pipeline. The gauge button, shown during a session, adds audio diagnostics (waveform, spectrum and WebRTC statistics).

API Discovery & Instructions

Note
The install script, the macOS app and the latest container image give version 4.12.1. The add-ons, Office, Sandbox, Pages and Team in these docs need a newer build.

LocalAI exposes a set of discovery endpoints that let external agents, coding assistants, and automation tools programmatically learn what the instance can do and how to control it - without reading documentation ahead of time.

Quick start

# 1. Discover what's available
curl http://localhost:8080/.well-known/localai.json

# 2. Browse instruction areas
curl http://localhost:8080/api/instructions

# 3. Get an API guide for a specific instruction
curl http://localhost:8080/api/instructions/config-management

Authentication and discovery

You can read these discovery surfaces without authentication. This remains true when LocalAI uses database authentication or legacy API keys:

  • GET /.well-known/localai.json
  • GET /api/instructions
  • GET /api/instructions/{name}
  • Swagger GET requests at /swagger and under /swagger/

Discovery does not grant access to the endpoints it advertises. Those endpoints require credentials unless the authentication guide lists them as a public discovery or bootstrap route. For example, GET /version requires credentials even though the well-known response includes the instance version.

Well-Known Discovery Endpoint

GET /.well-known/localai.json

Returns the instance version, all available endpoint URLs (flat and categorized), and runtime capabilities.

Example response (abbreviated):

{
  "version": "vX.Y.Z",
  "endpoints": {
    "chat_completions": "/v1/chat/completions",
    "models": "/v1/models",
    "models_capabilities": "/v1/models/capabilities",
    "config_metadata": "/api/models/config-metadata",
    "instructions": "/api/instructions",
    "swagger": "/swagger/index.html"
  },
  "endpoint_groups": {
    "openai_compatible": { "chat_completions": "/v1/chat/completions", "..." : "..." },
    "config_management": { "config_metadata": "/api/models/config-metadata", "..." : "..." },
    "model_management": { "..." : "..." },
    "monitoring": { "..." : "..." }
  },
  "capabilities": {
    "config_metadata": true,
    "config_patch": true,
    "vram_estimate": true,
    "model_storage": true,
    "backend_install": true,
    "mcp": true,
    "chats": true,
    "agent_jobs": true,
    "collections": true,
    "p2p": false
  }
}

The capabilities object reflects the current runtime configuration - for example, mcp is only true if MCP is enabled, chats and agent_jobs are false with --disable-chats, collections is false with --disable-collections, and backend_install is false when the server runs with --disable-backend-install (backend install and upgrade endpoints then answer 403).

Instructions API

Instructions are curated groups of related API endpoints. Each instruction maps to one or more Swagger tags and provides a focused, LLM-readable guide.

List all instructions

GET /api/instructions

curl http://localhost:8080/api/instructions

Returns a compact list of instruction areas:

{
  "instructions": [
    {
      "name": "chat-inference",
      "description": "OpenAI-compatible chat completions, text completions, and embeddings",
      "tags": ["inference", "embeddings"],
      "url": "/api/instructions/chat-inference"
    },
    {
      "name": "config-management",
      "description": "Discover, read, and modify model configuration fields with VRAM estimation",
      "tags": ["config"],
      "url": "/api/instructions/config-management"
    }
  ],
  "hint": "Fetch GET {url} for a markdown API guide. Add ?format=json for a raw OpenAPI fragment."
}

Available instructions:

InstructionDescription
motionLive human pose capture with Protobuf over WebSocket
agent-jobsAgent tasks and jobs: saved prompt templates run in the background, with webhooks
collectionsCollections: the knowledge base of documents the chats can search
chatsChats the server keeps: steering, sub-agents and live events
addonsOwner-routed addon lifecycle and response relays
library-sourcesLibrary sources of add-ons, backends and models
addon-navigationThe built-in tiles and the per-user pins of the Add-ons drawer
fine-tuning-quantizationExport status of the fine-tuning and quantization add-ons
chat-inferenceOpenAI-compatible chat completions, text completions, and embeddings
moderationOpenAI-compatible text moderation using a local completion model
audioText-to-speech, voice activity detection, transcription, speaker diarization, sound classification, and sound generation
voice-libraryCreate, preview, list, and delete reusable voice-cloning reference profiles
imagesImage generation and inpainting
model-managementBrowse the gallery, install, delete, and manage models and backends
config-managementDiscover, read, and modify model configuration fields with VRAM estimation
monitoringSystem metrics, backend status, API and backend traces, backend process logs, and system information
mcpModel Context Protocol — tool-augmented chat with MCP servers
videoVideo generation from text prompts with optional image or audio conditioning
3d3D mesh generation, remeshing, and animation (binary glTF / GLB)
forecastTime-series forecasting: quantile trajectories for numeric series, with optional covariates
face-recognitionFace verification (1:1), identification (1:N), embedding, and demographic analysis
voice-recognitionSpeaker verification (1:1), embedding, and demographic analysis from voice
decisionsTyped local decisions through OpenAI-compatible /v1/decisions and SystemOne APIs
brandingWhitelabel the instance: configure name, tagline, logo, and favicon
usage-and-billingPer-user token usage and request counts, with optional cost tracking
pii-filteringInspect the NER-based PII filter applied to chat requests
middleware-adminInspect and configure the routing-module middleware (PII filter and routing)
failoverModel failover chains: target health, pinning and switch events
intelligent-routingPer-model router: configuration that classifies requests and rewrites the served model

Get an instruction guide

GET /api/instructions/:name

By default, returns a markdown guide suitable for LLMs and humans:

curl http://localhost:8080/api/instructions/config-management

Add ?format=json to get a raw OpenAPI fragment (filtered Swagger spec with only the relevant paths and definitions):

curl http://localhost:8080/api/instructions/config-management?format=json

Model Capabilities

GET /v1/models/capabilities

An additive, LocalAI-specific superset of /v1/models. It returns the same set of models but enriches each entry with the capabilities the model supports and the input/output modalities it accepts and produces. Use it to decide, before sending a request, whether a given model can take an image, audio, or video attachment directly - or whether the input needs converting/transcribing first.

The reported context_size uses a positive model-level value first. If the model does not set context_size, it uses Settings → Performance → Default Context Size when positive. Otherwise, it uses the backend fallback of 4096 tokens. For llama.cpp with separate KV caches, the reported value accounts for the number of parallel slots.

An entry has a description when the model configuration sets one. With authentication on, a user whose account has a model allow-list sees only the models on it.

Because it is purely additive, clients that only understand /v1/models keep working unchanged; they simply never call this route.

curl http://localhost:8080/v1/models/capabilities
{
  "object": "list",
  "data": [
    {
      "id": "qwen2.5-omni",
      "object": "model",
      "capabilities": ["chat", "vision", "tools"],
      "description": "A model that reads images and audio.",
      "input_modalities": ["text", "image", "audio"],
      "output_modalities": ["text"]
    },
    {
      "id": "parakeet",
      "object": "model",
      "capabilities": ["transcript"],
      "input_modalities": ["audio"],
      "output_modalities": ["text"]
    }
  ]
}
  • capabilities - canonical usecase strings (e.g. chat, vision, transcript, tts, embeddings, image, video) plus the modifiers tools and thinking.
  • input_modalities / output_modalities - the subsets of {text, image, audio, video} the model accepts and produces. LocalAI combines usecase-based inference, backend settings such as vLLM limit_mm_per_prompt, and explicit model-level known_input_modalities / known_output_modalities. The explicit fields cover distinctions a usecase cannot express, such as a video model that also accepts speech.

The same query parameters as /v1/models are honored (filter, excludeConfigured), and the same per-user model allowlist is applied when authentication is enabled.

Configuration Management APIs

These endpoints let agents discover model configuration fields, read current settings, modify them, and estimate VRAM usage.

Config metadata

GET /api/models/config-metadata

Returns structured metadata for all model configuration fields, organized by section. Each field includes its YAML path, Go type, UI type, label, description, default value, validation constraints, and available options.

# All fields
curl http://localhost:8080/api/models/config-metadata

# Filter by section
curl http://localhost:8080/api/models/config-metadata?section=parameters

Autocomplete values

GET /api/models/config-metadata/autocomplete/:provider

Returns runtime values for dynamic fields. Providers include backends, models, models:chat, models:tts, models:transcript, models:vad.

# List available backends
curl http://localhost:8080/api/models/config-metadata/autocomplete/backends

# List chat-capable models
curl http://localhost:8080/api/models/config-metadata/autocomplete/models:chat

Read model config

GET /api/models/config-json/:name

Returns the full model configuration as JSON:

curl http://localhost:8080/api/models/config-json/my-model

Update model config

PATCH /api/models/config-json/:name

Deep-merges a JSON patch into the existing model configuration. Only include the fields you want to change:

curl -X PATCH http://localhost:8080/api/models/config-json/my-model \
  -H "Content-Type: application/json" \
  -d '{"context_size": 16384, "gpu_layers": 40}'

The endpoint validates the merged config and writes it to disk as YAML.

Warning

Config management endpoints require admin authentication when authentication is configured. The well-known endpoint, instructions API, and Swagger GET routes remain available without authentication.

Model storage report

GET /api/models/storage (admin)

Reports the on-disk footprint of the installed model configurations: per-model sizes with the portion shared with other models split out, every referenced file with the models referencing it, and references whose files are missing from disk.

curl http://localhost:8080/api/models/storage

VRAM estimation

POST /api/models/vram-estimate

Estimates VRAM usage for an installed model based on its weight files, context size, and GPU layer offloading:

curl -X POST http://localhost:8080/api/models/vram-estimate \
  -H "Content-Type: application/json" \
  -d '{"model": "my-model", "context_size": 8192}'
{
  "sizeBytes": 4368438272,
  "sizeDisplay": "4.4 GB",
  "vramBytes": 6123456789,
  "vramDisplay": "6.1 GB",
  "context_note": "Estimate used default context_size=8192. The model's trained maximum context is 131072; VRAM usage will be higher at larger context sizes.",
  "model_max_context": 131072
}

Optional parameters: gpu_layers (number of layers to offload, 0 = all), kv_quant_bits (KV cache quantization, 0 = fp16).

Integration guide

A recommended workflow for agent/tool builders:

  1. Discover: Fetch /.well-known/localai.json to learn available endpoints and capabilities.
  2. Browse instructions: Fetch /api/instructions for an overview of instruction areas.
  3. Deep dive: Fetch /api/instructions/{name} for a markdown API guide on a specific area.
  4. Authenticate: Obtain credentials before you call an advertised protected endpoint.
  5. Explore config: Use admin credentials with /api/models/config-metadata to understand configuration fields.
  6. Interact: Use credentials with inference endpoints and configuration APIs.

Swagger UI

The full interactive API documentation is available without authentication at /swagger/index.html. Requests sent to protected endpoints from Swagger still require credentials.

Decisions API

The Decisions API is a fast, typed decision layer. You send a piece of text (the state) and a set of named questions. A decision model answers each question with a value and a confidence, in one pass. The model does not generate text, so there is nothing to parse and no free-form output to validate.

LocalAI serves it on the /v1/systemone routes. The request and response shapes follow the kev project, and the field names and question types are the same ones Ollama serves on its /v1/systemone endpoint (Ollama 0.35 and later). The wire contract is called SystemOne; the capability a model declares is called decisions. See Compatibility with Ollama for what differs.

LocalAI also serves the OpenAI-compatible /v1/decisions contract with input and an ordered questions array. Both APIs share SystemOne execution, model selection, validation, admission limits, and backend support. They differ in request and response shape, not in the underlying model.

OpenAI-compatible decisions

Use an installed local decision model, such as julia-1-llama-cpp:

curl http://localhost:8080/v1/decisions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "julia-1-llama-cpp",
    "input": "My order arrived broken and I want my money back. This is the second time.",
    "questions": [
      {
        "type": "predicate",
        "name": "refund_requested",
        "instructions": "The customer explicitly asks for a refund"
      },
      {
        "type": "choice",
        "name": "team",
        "instructions": "Which team should handle this ticket?",
        "choices": [
          {"value": "billing", "description": "Payments, invoices, and refunds"},
          {"value": "shipping", "description": "Delivery and damaged goods"},
          {"value": "product", "description": "Questions about how the product works"}
        ]
      },
      {
        "type": "score",
        "name": "urgency",
        "instructions": "How urgent is this ticket?",
        "levels": [
          {"label": "not urgent"},
          {"label": "somewhat urgent"},
          {"label": "urgent"},
          {"label": "critical"}
        ]
      }
    ]
  }'

The response contains model, an ordered answers array, and usage. Answers preserve question order and optional names. Named questions must have unique names. Each question requires instructions.

Question typeInputAnswer
predicateA statement in instructionsprobability from 0 to 1; maps to SystemOne noul
choicechoices with unique string or boolean value fields and optional descriptionsTyped choice, confidence, and a probabilities array
scoreOrdered levels with required label fields and optional descriptionsscore, confidence, and a probabilities array with zero-based numeric values and labels

Choice values retain their JSON types: true and "true" are different choices. Numbers, objects, arrays, and null are not supported choice values. Scores are probability-weighted sums of zero-based level indices, not necessarily integers. Backend refusals appear as answers with type: "refusal".

usage contains input_tokens, output_tokens, their sum as total_tokens, input_tokens_details, and output_tokens_details. Counts reflect available backend accounting, including explicit zero output tokens. The NER path reports zero token counts. Detail objects contain only backend-supplied counts and are empty objects when unavailable. LocalAI does not invent cached, reasoning, text, or image token breakdowns.

Input and compatibility limits

input accepts a text string or an array of user messages. Each message has role: "user", optional type: "message", and content. Content accepts a string or an array of input_text and input_image parts:

{
  "input": [{
    "role": "user",
    "content": [
      {"type": "input_text", "text": "Does this package look damaged?"},
      {"type": "input_image", "image_url": "data:image/png;base64,...", "detail": "auto"}
    ]
  }]
}

This fragment requires a complete PNG or JPEG data URL and an image-capable model, such as OpenJev with its projector. Image-only input is supported. Only omitted or default ("auto") image detail is supported; other values return an error. LocalAI never fetches external image URLs or file paths.

The adapter joins text parts with newlines and collects images in encounter order. It flattens message boundaries and text/image interleaving into SystemOne text and an image list. It does not preserve a conversation layout for inference.

Requests support at most 64 questions. Text-only bodies have a 64 KiB limit; image-bearing bodies have a 16 MiB limit. The shared image limits are 8 images, 12 MiB aggregate encoded data-URL bytes, 8 MiB aggregate decoded bytes, 4096 pixels per dimension, and 16 million aggregate pixels. See Bounded image input for validation and admission behavior. Text-only models and the NER path reject images instead of dropping them. Backend-specific question limits still apply, including llama.cpp’s 2–10 score levels.

safety_identifier is accepted as compatibility metadata. It does not establish an authentication identity or enable a safety service. Normal LocalAI authentication and the decisions permission still apply.

Probabilities and confidence come from the local model and its pipeline. OpenAI-compatible fields do not guarantee OpenAI calibration, model quality, or safety behavior. Evaluate the installed model for your task.

Endpoints

EndpointMethodDescription
/v1/decisionsPOSTAnswer ordered predicate, choice, and score questions using the OpenAI-compatible contract
/v1/systemonePOSTAnswer all questions in one pass
/v1/systemone/permutePOSTRe-run one choice question under n_perm option orders
/v1/systemone/separatePOSTAnswer each question in its own pass

Which route a model can serve depends on its kind:

Model kind/v1/systemone/permute and /separate
Decision model (decisions), such as Laya or GLiNER2.5-DecideYesNo, returns 400
Zero-shot NER model (token_classify), such as GLiNER2.5Yes, through the NER pathYes

Question types

TypeAnswerFields in the answer
choiceOne option out of a named setchoice, probabilities, confidence
noulYes, no or unknown for a statementnoul (0 to 1), entities
scoreOne level on a scalescore, legend, probabilities, confidence

Example

curl http://localhost:8080/v1/systemone -H "Content-Type: application/json" -d '{
  "model": "laya-vllm-cpp",
  "state": "My order arrived broken and I want my money back. This is the second time.",
  "questions": {
    "team": {
      "type": "choice",
      "instructions": "Which team should handle this ticket?",
      "criteria": {
        "billing": "Payments, invoices and refunds",
        "shipping": "Delivery and damaged goods",
        "product": "Questions about how the product works"
      }
    },
    "refund_requested": {
      "type": "noul",
      "instructions": "The customer explicitly asks for a refund"
    },
    "urgency": {
      "type": "score",
      "instructions": "How urgent is this ticket?",
      "criteria": ["not urgent", "somewhat urgent", "urgent", "critical"]
    }
  }
}'

Answers from a decision model carry a confidence value, and the response reports token usage and latency_ms. The NER path does not report token usage.

Choosing a model

A model can serve the Decisions API only if it is a decision model. Declare the usecase in the model config:

name: laya
backend: vllm-cpp
known_usecases:
  - decisions
parameters:
  model: convaiinnovations/laya

decisions is never guessed, and a model that declares it is not listed as a chat, completion or embeddings model. A model that declares usecases without decisions or token_classify gets a 400 from these endpoints that names the missing usecase. A model that declares token_classify and not decisions is served by the zero-shot NER path. A vllm-cpp config that declares no usecases is treated as a decision model, so setups that predate the flag keep working, but a config that declares only chat (as an older laya gallery entry did) now gets the 400 and needs known_usecases: [decisions].

Install one from the gallery and filter on the decisions tag:

Gallery entryModelNotes
laya-vllm-cppLayaModernBERT-large, non-autoregressive, about 800 MB
gliner25-decide-vllm-cppGLiNER2.5-DecideDeBERTa-v3-large with a classification head, about 2 GB
tev1-4b-vllm-cppTev1 4BAutoregressive Qwen3.5-4B fine-tune that answers with an option letter, about 9.3 GB
tev1-0.8b-vllm-cppTev1 0.8BAutoregressive Qwen3.5-0.8B fine-tune that answers with an option letter, about 1.8 GB
kev-0.8b-vllm-cppkev 0.8BQwen3.5-0.8B-Base with a merged LoRA and a PointerHead readout, converted for vllm.cpp only, about 1.53 GB
nimble-9b-vllm-cppBespoke Nimble 9BQwen3.5-9B with the Nimble LoRA merged, reads the answer-letter logits, converted for vllm.cpp only, about 19.3 GB
clm-v0.1-8b-vllm-cppCLM v0.1 8BBi-encoder: Qwen3-8B backbone with state and action heads, answers by cosine similarity, converted for vllm.cpp only, about 16.5 GB

The engine, vllm.cpp, also supports the xor decision model. That checkpoint needs a conversion step, so it is not a gallery entry yet. The kev, Nimble and CLM entries install checkpoints that were already converted with the vllm.cpp convert-kev.py, convert-nimble.py and convert-clm.py scripts.

Nimble refuses a question with more than 26 choices (the upstream release allows 255). On CPU it needs about 20 GB of free RAM. For CLM, put the question in instructions: the state head reads the state followed by the instructions. On CPU it needs about 19 GB of free RAM.

Tev1 is an autoregressive decision model. The engine answers each question by scoring the option letters, so its confidence is the entropy measure Ollama uses. A Tev1 choice or score question accepts at most 24 options (Ollama allows 26), because the model is trained on the letters A to X, and every option needs a nonempty description. The published checkpoints name another architecture in config.json, so the Tev1 gallery entries set engine_args.hf_overrides to load them as Tev1Model (see Overriding config.json keys). The same model also answers /v1/chat/completions requests.

Request limits

A request is refused with 400 (or 413 for the body size) when:

  • a text-only body is larger than 64 KiB (image-bearing bodies have the bounded budget below),
  • state is missing or blank,
  • there are no questions, or more than 64,
  • a question id is blank,
  • a choice question has fewer than 2 options or a blank option key,
  • a score question has fewer than 2 levels,
  • a noul question has criteria with keys other than "false" and "true".

A noul question may carry criteria with a description for each outcome, for example {"false": "No refund is requested", "true": "The customer requests a refund"}. Some models cap the number of options for a choice or score question. Models that answer with a letter accept at most 26, and Tev1 accepts at most 24. The engine refuses more options than the model supports and the error names the limit.

Compatibility with Ollama

The field names, question types and answer fields are the same as Ollama’s /v1/systemone, so a client written for one works against the other for the common case. These behaviors differ:

OllamaLocalAI
confidence1 - H(p) / ln(N), an entropy measureComputed by the model’s pipeline. For kev and Laya it is a normalized margin, so the same probabilities give a different value
Errors{"error": "message"}{"error": {"message": "...", "type": "invalid_request"}}
keep_aliveSets how long the model stays loadedAccepted and ignored. Model lifetime follows the LocalAI idle and watchdog settings
state given as an objectSerialized as JSON textRendered as labeled lines, the way kev does it
noul answer on the NER path{type, noul}Also carries entities
Token usageFull prompt lengths across all questionsWhatever the backend reports; the NER path reports 0

Access control

When authentication is on, all four routes need the decisions feature. It is on by default for every user, like the other API features, and an administrator can turn it off per user.

Native llama.cpp decisions

The stock llama-cpp backend supports text-only decision GGUFs carrying upstream SystemOne metadata. Declare known_usecases: [decisions]; ordinary score need not be enabled. Requests use the existing internal Score RPC, not a backend HTTP server. Choice, score, and noul questions may be combined in one request. Structured state and questions are forwarded without NER rendering. llama.cpp score questions accept 2–10 levels; this backend-specific limit does not constrain vllm-cpp. Older forks without native decision support return 501. Missing decision metadata also returns 501, while backend invalid requests return 400.

A llama-cpp backend image built before native decisions cannot answer these requests, even when the gallery reports it as up to date. LocalAI detects this from the failed request: it returns 501 with a message that the backend is older than the server, and GET /backends/upgrades lists the backend with a reason field. Upgrade the backend, or install a current build such as the -development variant. Adding score to known_usecases does not help. The flag clears when the backend is reinstalled; if the reinstalled image is still old, the next decision request flags it again.

Bounded image input

The public routes and internal decision validator share an image contract. Supply PNG or JPEG base64 data URLs in images, OpenAI image_url message content, or Anthropic image content with a base64 source and media_type. Remote URLs and file paths are never fetched. MIME must match the decoded image header; malformed base64, unsupported formats and invalid headers return 400.

Limits per request are 8 images, 12 MiB aggregate encoded data-URL bytes, 8 MiB aggregate decoded bytes, 4096 pixels per dimension, and 16 million aggregate pixels. Exceeding these limits returns 413. Headers are checked before full pixel decoding, which rejects truncated or corrupt images; native decoders must independently protect direct RPC inputs.

Image-bearing request bodies may use up to 16 MiB. Text-only requests retain the 64 KiB raw-wire limit, including whitespace; JSON escaping during internal serialization does not impose a second HTTP limit. Absent, null, or empty images do not enable the larger budget. Native decision responses retain a separate 64 KiB limit, independent of the request budget. These limits do not raise any global HTTP limit.

A shared 8-request admission ceiling covers public decision handlers and internal decision runners before body buffering, image decoding or serialization. Saturation fails promptly (HTTP 503); cancellation before admission does not take a slot. A slot remains held through inference/response handling, and an internal cancelled call retains its slot until its underlying worker actually ends. This bounds concurrent decision-owned allocation and retained request bodies, not total process memory: caller-owned inputs, upstream middleware buffers and model/backend memory are outside this budget. Full decoding is sequential per admitted request, with each pixel buffer limited by the checked dimensions and aggregate pixel budget (up to 16 million pixels; decoded byte storage depends on pixel format). Garbage collection timing is not an RSS guarantee. The separate 8-operation backend ceiling still bounds abandoned native operations. Validation helpers do not acquire nested slots.

For image-only input, provide explicit structured state such as "state": {} alongside images, or a message containing image content. Missing, null or blank string state remains invalid. Arbitrary domain JSON is preserved, not interpreted as image content outside message content parts. Images are never replaced by invented text.

Admission is not a promise of model image capability: the NER path and text-only native decision models reject images with 501 rather than silently dropping them. OpenJev requires its vision projector (see below). Failed requests are not billed. Router image probes preserve the canonical request; full public API image validation across installed models is separate from native RPC validation.

Native responses report backend input/output usage, including zero generated tokens. LocalAI records supplied usage once; explicit zero counts are distinct from missing usage. Missing counts are not estimated, and invalid negative counts are rejected rather than billed.

Julia-1 CPU example

Install the separate stock llama.cpp entry (existing vllm-cpp entries are unchanged):

local-ai models install julia-1-llama-cpp

Julia-1 is a 144.3M-parameter multilingual text decision model. The gallery pins ggml-org/Julia-1-GGUF revision 16fee17949206fbf58da9347daea44d792a81211, file Julia-1-Q8_0.gguf (168,166,496 bytes, about 160.4 MiB), SHA-256 1ea6a7e87156eeeda88cb7a36a61265b37ba7b993897b7289b99aea5b5e47069. The source model and GGUF publisher declare Apache-2.0. Source provenance: SupersonicLabs/Julia-1 revision a85b127321d580d65176c89ced8273f305745d85, based on jhu-clsp/mmBERT-small. This is a real model, not the upstream tiny test fixture; assess its accuracy for your own tasks.

curl http://localhost:8080/v1/systemone \
  -H 'Content-Type: application/json' \
  -d '{"model":"julia-1-llama-cpp","state":"I was charged twice and need a refund.","questions":{"route":{"type":"choice","instructions":"Which team should handle this?","criteria":{"billing":"payments and refunds","shipping":"delivery problems","technical":"software issues"}},"refund":{"type":"noul","instructions":"Does the customer request a refund?"}}}'

The pinned artifact was installed through the gallery installer, checksum-verified, and tested on CPU with the native Score RPC using choice, score, and noul in one request. That smoke returned 97 input tokens and zero output tokens; token counts vary with the request. This does not establish broad model accuracy or image support.

Native family defaults

Each family has a separately named default; these do not replace vllm-cpp entries. All entries except OpenJev are text-only and omit projectors. OpenJev includes its Q8 projector. Download size is not a RAM estimate.

Gallery entryQuantizationArtifact bytesLicenseValidation status
julia-1-llama-cppQ8_0168,166,496Apache-2.0Gallery install and CPU request verified
laya-llama-cppQ8_0449,397,600Apache-2.0Gallery install and CPU choice/score/noul verified
kev-4b-llama-cppQ4_K_M3,033,489,824Apache-2.0Gallery install and CPU choice/score/noul verified
lev-llama-cppQ4_K_M3,011,777,440Apache-2.0Gallery install and CPU choice/score/noul verified
openjev-llama-cppQ4_K_M + Q8 projector19,603,119,520CC-BY-NC-4.0Gallery install and CPU choice/score/noul verified
nimble-9b-v3-llama-cppQ4_K_M6,324,185,632CC-BY-NC-4.0Gallery install and CPU choice/score/noul verified

OpenJev and Nimble are noncommercial models. OpenJev’s upstream multimodal capability does not imply LocalAI decision-image support. Nimble requires the native Nimble integration included in this source tree’s llama.cpp pin bed0a856606ee4a24a164066f73d2379447033f5; older installed backends must be updated before serving it. This source prerequisite is integrated, but the OpenJev and Nimble installation/runtime checks remain pending as listed above. The published entries pin revisions and SHA-256 checksums, but metadata verification alone is not a runtime test. No model-quality guarantee follows from these smoke tests. Laya, Kev-4B, and lev were also retested against the newer native backend with 1- and 11-level score requests correctly rejected.

OpenJev and Nimble validation used the native backend at llama.cpp revision bed0a856606ee4a24a164066f73d2379447033f5, CPU-only with two threads, a 2048-token context, and batch size 512. Each artifact was installed through the gallery, SHA-256 verified, and checked for its decision metadata and SystemOne template. Each request included choice, score, and noul questions together, including the full question set required by Nimble. Response-shape, probability-normalization, and noul-bound assertions passed; 1- and 11-level score requests were rejected. The test requests reported 224 input tokens for OpenJev and 933 for Nimble, with explicit zero output tokens for both. No projector was installed or tested.

These are bounded text contract smoke tests, not accuracy benchmarks or performance guarantees. Floating-point probabilities can vary with hardware and build settings; tests do not require exact answer probabilities or token counts. Neither image support nor interruption during active evaluation is established by these tests. CC-BY-NC-4.0’s noncommercial restriction still applies.

Multimodal router probes

The decisions router classifier preserves ordered OpenAI message content and Anthropic base64 image sources as structured state, including image-only turns. It uses the internal decision runner, not a loopback HTTP request. The shared image limits above are validated before model loading; no URL is fetched by the classifier. Original message content is not rewritten when selecting a candidate or the configured fallback.

Score, rerank and KNN classifiers are text-only: image input produces an explicit classifier error and follows the existing configured fallback policy, rather than classifying an image-stripped prompt. Without a fallback, routing fails. Parent cancellation remains terminal and does not select a fallback. Image probes bypass text embedding caches and are not trimmed to text-only turns. Native context overflow is reported by the backend rather than silently dropping images. These transport guarantees do not establish installed projector capability or real-model image accuracy; those require separate native and end-to-end validation.

OpenAI chat routing classifies the original structured message before preparing media for the selected model. Remote image URLs are not downloaded as decision inputs. After selection (including a configured fallback), the served model’s normal media preparation runs without replacing the original content blocks. Invalid classifier configuration fails closed even with a configured fallback. Runtime classification and input errors follow the configured fallback policy. Cancellation never selects a fallback.

Router probe extraction checks the shared 16 MiB state budget before copying text or serializing messages, including JSON escaping expansion. This applies to typed and untyped internal requests as well as parsed API requests; it does not add a limit to non-router inference. Direct internal probes containing custom structs, JSON/text marshalers, or excessively nested values fail extraction rather than executing their serialization. Supported internal values are the chat schema message/content/tool types and plain JSON values (including json.RawMessage, conservatively budgeted for escaping). Direct prompt-only probes also check serialized escaping before allocation. The separate 64 KiB text-only Decisions request limit is unchanged. Anthropic conversion preserves typed content blocks through both native selection and fallback, including ordered text and images.

OpenJev image decisions

The openjev-llama-cpp gallery entry installs OpenJev Q4_K_M and its pinned Q8 vision projector (mmproj-OpenJev-Q8_0.gguf). Both artifacts come from ggml-org/OpenJev-GGUF revision 10840f375658dea7afc5ff4711127bca8218b560. The weights occupy 18,973,872,288 bytes and the projector 629,247,232 bytes: 19,603,119,520 bytes total (about 19.61 GB decimal), excluding runtime memory, KV cache and backend files. The model is CC-BY-NC-4.0, noncommercial only; LocalAI’s software license does not override the model license.

For a manually installed model, set mmproj: mmproj-OpenJev-Q8_0.gguf alongside parameters.model, not inside parameters. LocalAI resolves that filename relative to the model directory and forwards it to llama.cpp’s projector loader. The gallery uses an 8192-token context to leave room for image tokens and question framing. This is not a guarantee that all eight maximum-sized images fit; context overflow remains an error. Size the context for the actual workload.

Native image decisions require both a decision format that accepts images and a loaded projector that supports vision input. A missing projector, an audio-only projector, or a text-only decision model does not silently fall back to a text decision. Other decision gallery entries remain text-only.

Direct native RPC callers receive the same image count, encoded/decoded byte, dimension and aggregate pixel bounds as public callers. Validation precedes llama.cpp’s permissive media parsing and full pixel decode; PNG decompression is independently bounded before stb decodes pixels. This validation applies only to native decision tasks, not ordinary chat or legacy scoring.

Native decision image validation rejects PNG streams with invalid checksums and incomplete JPEG scans, including truncated scans with an appended end marker. Source builds with native decision support require zlib and libjpeg development packages (zlib1g-dev libjpeg-dev on Ubuntu; zlib jpeg-turbo on Homebrew). Packaged backends include the required runtime libraries.

Stores

Stores are an experimental feature to help with querying data using similarity search. It is a low level API that consists of only get, set, delete and find.

Tip

Face recognition uses this store. The 1:N face identification flow (/v1/face/register, /v1/face/identify, /v1/face/forget) is built on top of the generic store - see Face Recognition for the face-oriented API.

For example if you have an embedding of some text and want to find text with similar embeddings. You can create embeddings for chunks of all your text then compare them against the embedding of the text you are searching on.

An embedding here meaning a vector of numbers that represent some information about the text. The embeddings are created from an A.I. model such as BERT or a more traditional method such as word frequency.

Previously you would have to integrate with an external vector database or library directly. With the stores feature you can now do it through the LocalAI API.

Note however that doing a similarity search on embeddings is just one way to do retrieval. A higher level API can take this into account, so this may not be the best place to start.

API overview

There is an internal gRPC API and an external facing HTTP JSON API. We’ll just discuss the external HTTP API, however the HTTP API mirrors the gRPC API. Consult pkg/store/client for internal usage.

Everything is in columnar format meaning that instead of getting an array of objects with a key and a value each. You instead get two separate arrays of keys and values.

Keys are arrays of floating point numbers with a maximum width of 32bits. Values are strings (in gRPC they are bytes).

The key vectors must all be the same length and it’s best for search performance if they are normalized. When addings keys it will be detected if they are not normalized and what length they are.

All endpoints accept a store field which specifies which store to operate on. Stores are created on the fly. By default the in-memory local-store backend is used and no configuration is required, but you can select a different store backend per request (see Backends below).

Backends

Each /stores/* request accepts an optional backend field selecting the store implementation. Three backends ship with LocalAI:

Backendbackend valuePersistenceNotes
Local (default)local-store (alias embedded-store)In-memory, lost on restartExact cosine similarity, zero configuration.
Valkey Searchvalkey-store (alias valkey)Durable (Valkey RDB/AOF)Backed by a Valkey Search (FT.*) server; survives restarts and supports opt-in HNSW.
Qdrantqdrant-store (alias qdrant)Durable (Qdrant storage)Backed by a Qdrant server or cluster, or Qdrant Cloud; HNSW-indexed; on a single node a find right after a set sees the new vectors.

Internal features that use stores (face and voice recognition, the router’s KNN classifier and embedding cache) don’t send a backend field. To move the router’s stores (router-cache-<router> and router-corpus-<router>) to a durable backend, create a model config named after the store with the backend: and options: shown below. Keep the default cosine distance_metric for them: they compare similarities against cosine thresholds. Keep face and voice recognition on local-store for now: their registries remember enrolled IDs only in memory, so after a restart entries in a durable store could no longer be forgotten. With tracing enabled, the router’s vector-store calls appear in /api/backend-traces under the backend each store resolved to.

A backend field that names a different backend than the store’s own (its model config’s backend:, or local-store when there is none) is served by a separate backend instance, so it never lands on the store’s usual backend. It gets the model config’s options: only if the config has no backend:; otherwise those options belong to the configured backend, and it gets its own defaults.

Valkey store backend

The valkey-store backend persists vectors in a Valkey Search server, so the data survives a LocalAI restart — unlike the in-memory default. It requires a reachable server that ships the Valkey Search module (for example the valkey/valkey-bundle image).

Select it by passing "backend": "valkey-store" (or the "valkey" alias) on any /stores/* request:

curl -X POST http://localhost:8080/stores/set \
     -H "Content-Type: application/json" \
     -d '{"backend": "valkey-store", "store": "my-vectors", "keys": [[0.1, 0.2], [0.3, 0.4]], "values": ["foo", "bar"]}'

The connection and index are configured through a model config named after the store (the store field on the request, which is the store’s model ID). Create a YAML in your models directory whose name matches the store, set backend: valkey-store, and put the connection / index settings in the options: list as key:value strings. Because each store resolves its own config, different stores can point at different Valkey servers or use different index settings within one LocalAI process:

name: my-vectors
backend: valkey-store
options:
  - addr:valkey.internal:6379
  - username_env:MY_VALKEY_USER
  - password_env:MY_VALKEY_PASSWORD
  - index_algo:HNSW
  - distance_metric:COSINE

The username_env / password_env options name an environment variable that holds the actual credential rather than putting the secret directly in the YAML. This mirrors cloud-proxy’s api_key_env pattern and lets distinct store configs each reference their own credentials. The direct username / password options still work for backward compatibility, and take precedence when both are set.

When no config exists for a store, the backend connects to localhost:6379 with the defaults below (so the zero-config experience still works).

OptionDefaultDescription
addrlocalhost:6379Valkey server address (host:port).
username(empty)Optional ACL username (plaintext in config).
password(empty)Optional password / ACL secret (plaintext in config).
username_env(empty)Name of an env var that holds the username. Preferred over username for secrets — keeps credentials out of the model YAML.
password_env(empty)Name of an env var that holds the password. Preferred over password for secrets.
tlsfalseEnable TLS (required by many managed deployments).
tls_ca_cert(empty)Path to a PEM CA bundle used to verify the server certificate (self-signed / private CA).
tls_skip_verifyfalseSkip TLS certificate verification. Insecure — for testing only.
client_namelocalai-valkey-storeConnection name reported by CLIENT LIST. Always set.
db0Logical Valkey DB index (SELECT n). Namespace prefixing already isolates keyspaces on a shared DB.
index_algoFLATFLAT (exact, default) or HNSW (approximate ANN for large corpora).
hnsw_m16HNSW graph degree (only when index_algo:HNSW).
hnsw_ef_construction200HNSW build-time candidate list (HNSW only).
hnsw_ef_runtime10HNSW query-time candidate list (HNSW only).
distance_metricCOSINECOSINE (default), L2 or IP.
request_timeout_ms5000Per-command timeout in milliseconds.

For COSINE the returned similarities follow the same convention as the local store (1.0 = identical, -1.0 = opposite); internally Valkey returns a cosine distance which the backend converts with similarity = 1 - distance. For L2 and IP the raw Valkey score is returned in similarities (for L2, smaller means closer — the opposite ordering of COSINE); nearest-first ordering of the results is preserved in all cases.

Note

Valkey Search updates its vector index asynchronously after a write. A find issued immediately after set may not yet see the new vectors — poll find briefly (or retry) until the expected results appear. get and delete are synchronous and unaffected.

Note

This backend targets a standalone Valkey Search server (one server per namespace/model). Valkey Cluster is not a supported target yet — index coordination across shards is out of scope for this backend.

Warning

tls defaults to false (plaintext). Set tls:true whenever the Valkey server is not on localhost or a password/username is configured, otherwise the credentials and the stored vectors travel the network unencrypted. The TLS ServerName (SNI) is derived from the host portion of addr, so certificate verification works for both hostname and IP-addressed endpoints. For a self-signed / private CA, point tls_ca_cert at the PEM bundle; tls_skip_verify:true disables verification entirely and should only be used for local testing.

Qdrant store backend

The qdrant-store backend keeps vectors in a Qdrant collection, so the data survives a LocalAI restart. It works with a single Qdrant node, a Qdrant cluster and Qdrant Cloud. It talks to Qdrant’s gRPC port (6334 by default), not the 6333 REST port.

Select it by passing "backend": "qdrant-store" (or the "qdrant" alias) on any /stores/* request:

curl -X POST http://localhost:8080/stores/set \
     -H "Content-Type: application/json" \
     -d '{"backend": "qdrant-store", "store": "my-vectors", "keys": [[0.1, 0.2], [0.3, 0.4]], "values": ["foo", "bar"]}'

As with Valkey, the connection and collection are configured through a model config named after the store, with the settings in options: as key:value strings:

name: my-vectors
backend: qdrant-store
options:
  - addr:xyz-example.eu-central.aws.cloud.qdrant.io:6334
  - tls:true
  - api_key_env:QDRANT_API_KEY
  - distance_metric:COSINE

When no config exists for a store, the backend connects to localhost:6334 with the defaults below.

Each store gets its own collection, named localai_<store>-<hash> (for example localai_my-vectors-1a2b3c4d) unless you set collection. It is created on the first set with the dimension of the first key, and Qdrant rejects keys and queries of any other dimension, even after every key is deleted. To change the dimension (for example after switching embedding models), delete the collection in Qdrant or set a new collection. The backend never deletes collections itself, since with several writers that could lose another writer’s data.

Keys are matched exactly, and a repeated key keeps the last value. A set is a single Qdrant request, so a rejected set writes nothing. NaN and Inf components are rejected.

OptionDefaultDescription
addrlocalhost:6334Qdrant gRPC address (host:port; the port defaults to 6334). Not a URL — use tls:true for HTTPS endpoints.
api_key(empty)Qdrant API key (plaintext in config).
api_key_env(empty)Name of an env var that holds the API key. Preferred over api_key — keeps the secret out of the model YAML.
tlsfalseEnable TLS (required by Qdrant Cloud).
tls_ca_cert(empty)Path to a PEM CA bundle used to verify the server certificate (self-signed / private CA). Turns on tls.
tls_skip_verifyfalseSkip TLS certificate verification. Insecure — for testing only. Turns on tls.
collection(derived)Use this exact collection name instead of the derived one.
distance_metricCOSINECOSINE, EUCLID, DOT or MANHATTAN (case-insensitive).
request_timeout_ms5000Per-request timeout in milliseconds.

For COSINE the returned similarities follow the local store’s convention (1.0 = identical, -1.0 = opposite). For DOT they are dot products (larger is closer). For EUCLID and MANHATTAN they are distances, so smaller is closer, which is the opposite ordering to COSINE. In every case results come back nearest-first. If a collection already exists with a different distance than distance_metric, loading the store (or, if the collection appeared later, the first find) fails rather than returning scores with the wrong meaning.

Warning

Only COSINE returns similarity-like scores. With DOT, EUCLID or MANHATTAN the backend returns Qdrant’s raw dot products or distances, which are not bounded to [-1, 1] and, for EUCLID and MANHATTAN, run in the opposite direction. Router stores, face recognition and voice recognition compare scores against cosine thresholds, so keep them on COSINE. The backend logs a warning at load time when a store is configured with another metric.

Note

The backend creates collections with Qdrant’s default settings. For custom settings (sharding and replication on a cluster, HNSW parameters, on-disk vectors, quantization), create the collection in Qdrant yourself and point collection at it: an existing collection is used as long as it has a single unnamed vector with the configured distance_metric.

Note

Small collections are searched exactly. Once Qdrant builds the HNSW index (when a segment passes its indexing threshold, 10 MB of vectors per segment by default, so typically tens of thousands of embeddings), find is approximate, with recall set by the collection’s HNSW settings.

Warning

tls defaults to false (plaintext). Set tls:true whenever Qdrant is not on localhost or an API key is configured, otherwise the key and the stored vectors travel the network unencrypted.

Set

To set some keys you can do

curl -X POST http://localhost:8080/stores/set \
     -H "Content-Type: application/json" \
     -d '{"keys": [[0.1, 0.2], [0.3, 0.4]], "values": ["foo", "bar"]}'

Setting the same keys again will update their values.

On success 200 OK is returned with no body.

Get

To get some keys you can do

curl -X POST http://localhost:8080/stores/get \
     -H "Content-Type: application/json" \
     -d '{"keys": [[0.1, 0.2]]}'

Both the keys and values are returned, e.g: {"keys":[[0.1,0.2]],"values":["foo"]}

The order of the keys is not preserved! If a key does not exist then nothing is returned.

Delete

To delete keys and values you can do

curl -X POST http://localhost:8080/stores/delete \
     -H "Content-Type: application/json" \
     -d '{"keys": [[0.1, 0.2]]}'

If a key doesn’t exist then it is ignored.

On success 200 OK is returned with no body.

Find

To do a similarity search you can do

curl -X POST http://localhost:8080/stores/find 
     -H "Content-Type: application/json" \
     -d '{"topk": 2, "key": [0.2, 0.1]}'

topk limits the number of results returned. The result value is the same as get, except that it also includes an array of similarities. Where 1.0 is the maximum similarity. They are returned in the order of most similar to least.

API Error Reference

This page documents the error responses returned by the LocalAI API. LocalAI supports multiple API formats (OpenAI, Anthropic, Open Responses), each with its own error structure.

Note

For backend and runtime failure messages (a model that will not load, could not load model, grpc service not ready, SIGILL, VRAM out of memory, the model-load cooldown 503), rather than API-envelope errors, see /docs/reference/runtime-errors/.

Error Response Formats

OpenAI-Compatible Format

Most endpoints return errors using the OpenAI-compatible format:

{
  "error": {
    "code": 400,
    "message": "A human-readable description of the error",
    "type": "invalid_request_error",
    "param": null
  }
}
FieldTypeDescription
codeinteger|stringHTTP status code or error code string
messagestringHuman-readable error description
typestringError category (e.g., invalid_request_error)
paramstring|nullThe parameter that caused the error, if applicable

This format is used by: /v1/chat/completions, /v1/completions, /v1/embeddings, /v1/images/generations, /v1/audio/transcriptions, /models, and other OpenAI-compatible endpoints.

Anthropic Format

The /v1/messages endpoint returns errors in Anthropic’s format:

{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "A human-readable description of the error"
  }
}
FieldTypeDescription
typestringAlways "error" for error responses
error.typestringinvalid_request_error or api_error
error.messagestringHuman-readable error description

Open Responses Format

The /v1/responses endpoint returns errors with this structure:

{
  "error": {
    "type": "invalid_request",
    "message": "A human-readable description of the error",
    "code": "",
    "param": "parameter_name"
  }
}
FieldTypeDescription
typestringOne of: invalid_request, not_found, server_error, model_error, invalid_request_error
messagestringHuman-readable error description
codestringOptional error code
paramstringThe parameter that caused the error, if applicable

HTTP Status Codes

CodeMeaningWhen It Occurs
400Bad RequestInvalid input, missing required fields, malformed JSON
401UnauthorizedMissing or invalid API key
404Not FoundModel or resource does not exist
409ConflictResource already exists (e.g., duplicate token)
422Unprocessable EntityValidation failed (e.g., invalid parameter range)
429Too Many RequestsAll backends are saturated (per-model max_concurrent or process-wide --max-concurrent-backend-requests ceiling reached). Includes a Retry-After header and type: "rate_limit_error" so OpenAI-compatible clients and harnesses back off automatically
500Internal Server ErrorBackend inference failure, unexpected server errors
503Service UnavailableNo healthy node available to serve the model (cluster is full, eviction cannot free a slot, or a node_selector excludes all candidates). Also used during model-load cooldown and while a model is still cold-loading. Retryable

Global Error Handling

Authentication Errors (401)

When authentication is enabled - either via API keys (LOCALAI_API_KEY) or the user auth system (LOCALAI_AUTH=true) - API requests must include valid credentials. Credentials can be provided through:

  • Authorization: Bearer <key> header (API key, user API key, or session ID)
  • x-api-key: <key> header
  • xi-api-key: <key> header
  • session cookie (user auth sessions)
  • token cookie (legacy API keys)

Authorization Errors (403)

When user authentication is enabled, admin-only endpoints (model management, system settings, traces, agents, etc.) return 403 if accessed by a non-admin user. See Authentication & Authorization for the full list of admin-only endpoints.

Example request without a key:

curl http://localhost:8080/v1/models \
  -H "Content-Type: application/json"

Error response:

{
  "error": {
    "code": 401,
    "message": "An authentication key is required",
    "type": "invalid_request_error"
  }
}

The response also includes the header WWW-Authenticate: Bearer.

Request Parsing Errors (400)

All endpoints return a 400 error if the request body cannot be parsed:

{
  "error": {
    "code": 400,
    "message": "failed parsing request body: <details>",
    "type": ""
  }
}

Not Found (404)

Requests to undefined routes return:

{
  "error": {
    "code": 404,
    "message": "Resource not found"
  }
}

Opaque Errors Mode

When LOCALAI_OPAQUE_ERRORS=true is set, all error responses return an empty body with only the HTTP status code. This is a security hardening option that prevents information leaks.

Per-Endpoint Error Scenarios

Chat Completions - POST /v1/chat/completions

StatusCauseExample Message
400Invalid or malformed request bodyBad Request
400Model not found in configurationBad Request
500Backend inference failureInternal Server Error
# Missing model field
curl http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "hello"}]}'

See also: Text Generation

Completions - POST /v1/completions

StatusCauseExample Message
400Invalid or malformed request bodyBad Request
500Backend inference failureInternal Server Error

Embeddings - POST /v1/embeddings

StatusCauseExample Message
400Invalid or malformed request bodyBad Request
400Model not found in configurationBad Request
400No model in the body and no embeddings model configuredBad Request
500Backend inference failureInternal Server Error

See also: Embeddings

Image Generation - POST /v1/images/generations

StatusCauseExample Message
400Invalid or malformed request bodyBad Request
400Model not found in configurationBad Request
500Backend inference failureInternal Server Error

See also: Image Generation

Image Editing (Inpainting) - POST /v1/images/edits

StatusCauseExample Message
400Invalid or malformed request bodyBad Request
400Missing image filemissing image file
400Missing mask filemissing mask file
500Storage preparation failurefailed to prepare storage

Audio Transcription - POST /v1/audio/transcriptions

StatusCauseExample Message
400Missing file field in form dataBad Request
400Model not found in configurationBad Request
500Backend inference failureInternal Server Error

See also: Audio to Text

Text to Speech - POST /v1/audio/speech, POST /tts

StatusCauseExample Message
400Invalid or malformed request bodyBad Request
400Model not found in configurationBad Request
500Backend inference failureInternal Server Error

See also: Text to Audio

ElevenLabs TTS - POST /v1/text-to-speech/:voice-id

StatusCauseExample Message
400Invalid or malformed request bodyBad Request
400Model not found in configurationBad Request
500Backend inference failureInternal Server Error

ElevenLabs Sound Generation - POST /v1/sound-generation

StatusCauseExample Message
400Invalid or malformed request bodyBad Request
400Model not found in configurationBad Request
500Backend inference failureInternal Server Error

Reranking - POST /v1/rerank, POST /jina/v1/rerank

StatusCauseExample Message
400Invalid or malformed request bodyBad Request
422top_n less than 1top_n - should be greater than or equal to 1
500Backend inference failureInternal Server Error

See also: Reranker

Anthropic Messages - POST /v1/messages

StatusCauseError TypeExample Message
400Missing model fieldinvalid_request_errormodel is required
400Model not in configurationinvalid_request_errormodel configuration not found
400Missing or invalid max_tokensinvalid_request_errormax_tokens is required and must be greater than 0
500Backend inference failureapi_errormodel inference failed: <details>
500Prediction failureapi_errorprediction failed: <details>
# Missing model field
curl http://localhost:8080/v1/messages \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "hello"}], "max_tokens": 100}'
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "model is required"
  }
}

Open Responses - POST /v1/responses

StatusCauseError TypeExample Message
400Missing model fieldinvalid_requestmodel is required
400Model not in configurationinvalid_requestmodel configuration not found
400Failed to parse inputinvalid_requestfailed to parse input: <details>
400background=true without store=trueinvalid_request_errorbackground=true requires store=true
404Previous response not foundnot_foundprevious response not found: <id>
500Backend inference failuremodel_errormodel inference failed: <details>
500Prediction failuremodel_errorprediction failed: <details>
500Tool execution failuremodel_errorfailed to execute tools: <details>
500MCP configuration errorserver_errorfailed to get MCP config: <details>
500No MCP servers availableserver_errorno working MCP servers found
# Missing model field
curl http://localhost:8080/v1/responses \
  -H "Content-Type: application/json" \
  -d '{"input": "hello"}'
{
  "error": {
    "type": "invalid_request",
    "message": "model is required",
    "code": "",
    "param": ""
  }
}

Open Responses - GET /v1/responses/:id

StatusCauseError TypeExample Message
400Missing response IDinvalid_request_errorresponse ID is required
404Response not foundnot_foundresponse not found: <id>

Open Responses Events - GET /v1/responses/:id/events

StatusCauseError TypeExample Message
400Missing response IDinvalid_request_errorresponse ID is required
400Response was not created with streaminvalid_request_errorcannot stream a response that was not created with stream=true
400Invalid starting_after valueinvalid_request_errorstarting_after must be an integer
404Response not foundnot_foundresponse not found: <id>
500Failed to retrieve eventsserver_errorfailed to get events: <details>

Object Detection - POST /v1/detection

StatusCauseExample Message
400Invalid or malformed request bodyBad Request
400Model not found in configurationBad Request
500Backend inference failureInternal Server Error

See also: Object Detection

Video Generation - POST /v1/video/generations

StatusCauseExample Message
400Invalid or malformed request bodyBad Request
400Model not found in configurationBad Request
500Backend inference failureInternal Server Error

Voice Activity Detection - POST /v1/audio/vad

StatusCauseExample Message
400Invalid or malformed request bodyBad Request
400Model not found in configurationBad Request
500Backend inference failureInternal Server Error

Tokenize - POST /v1/tokenize

StatusCauseExample Message
400Invalid or malformed request bodyBad Request
400Model not found in configurationBad Request

Slot cache - POST /v1/slots/save, POST /v1/slots/restore

StatusCauseExample Message
400Invalid id on restore, an id sent on save, negative slot, or model not foundinvalid id: expected 32 lowercase hex characters
404The id is unknown, or was issued for another modelno saved state with this id for this model
400slot_save_path is not set for the modelslot save and restore is disabled for this model: set the slot_save_path option
403Caller is not an admin (authentication on)Forbidden
412Backend has save and restore turned off, or the saved file no longer matches its recordslot save/restore is disabled
507The saved state is larger than the 8 GiB limit and was deletedthe saved state is larger than the 8589934592 byte limit and was deleted
501Backend does not support the callslot save and restore is not supported by this model's backend

Models - GET /v1/models, GET /models

StatusCauseExample Message
500Failed to list modelsInternal Server Error

See also: Model Gallery

Handling Errors in Client Code

Python (OpenAI SDK)

from openai import OpenAI, APIError

client = OpenAI(base_url="http://localhost:8080/v1", api_key="your-key")

try:
    response = client.chat.completions.create(
        model="my-model",
        messages=[{"role": "user", "content": "hello"}],
    )
except APIError as e:
    print(f"Status: {e.status_code}, Message: {e.message}")

curl

# Check HTTP status code
response=$(curl -s -w "\n%{http_code}" http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model": "nonexistent", "messages": [{"role": "user", "content": "hi"}]}')

http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | head -1)

if [ "$http_code" -ne 200 ]; then
  echo "Error $http_code: $body"
fi
Environment VariableDescription
LOCALAI_API_KEYComma-separated list of valid API keys
LOCALAI_OPAQUE_ERRORSSet to true to hide error details (returns empty body with status code only)
LOCALAI_SUBTLE_KEY_COMPARISONUse constant-time key comparison for timing-attack resistance

Integrations

Community Integrations

The lists below cover software and community projects that integrate with LocalAI.

Feel free to open up a Pull request (by clicking at the “Edit page” below) to get your project added!

Build & Deploy

Distribution Packages

Community-maintained packages of LocalAI. Package versions and availability may lag upstream releases.

  • Homebrew: a community package, not maintained by the LocalAI project. Check the formula before you use it.
  • ALT Sisyphus - localai package
  • Gentoo overlay - sci-ml/local-ai and per-backend packages, overlay local-ai in Gentoo’s repository index

Web UIs

Agentic Libraries & Assistants

  • cogito - Agentic library for Go
  • LocalAGI - Local smart assistant with autonomous agents
  • Nika - Intent-as-code AI workflow engine in a single Rust binary, with LocalAI as a built-in local provider

Coding Agents

Point Codex CLI, Claude Code, opencode, Hermes Agent, nib, pi, Aider, Continue, Cline or Goose at your own LocalAI server. See Connect coding agents for the exact settings of each one.

MCP Servers

  • MCPs - Model Context Protocol servers

OS Assistants

  • Keygeist - AI-powered keyboard operator for Linux

Voice

  • VoxInput - Use voice to control your desktop

IDE & Editor Plugins

Framework Integrations

Terminal Tools

Chat Bots

Home Automation

Automation & DevOps

  • Reflexia - Auto-documentation
  • GitHelper - GitHub bot for issues with code and documentation context
  • kairos - Immutable Linux OS

Other Integrations

Configuration Guides

This section provides step-by-step instructions for configuring specific software to work with LocalAI.

OpenCode

OpenCode is an AI-powered code editor that can be configured to use LocalAI as its backend provider.

Prerequisites

  • LocalAI must be running and accessible (either locally or on a network)
  • You need to know your LocalAI server’s IP address/hostname and port (default is 8080)

Configuration Steps

  1. Edit the OpenCode configuration file

    Open the OpenCode configuration file located at ~/.config/opencode/opencode.json in your editor.

  2. Add LocalAI provider configuration

    Add the following configuration to your opencode.json file, replacing the values with your own:

    {
      "$schema": "https://opencode.ai/config.json",
      "provider": {
        "LocalAI": {
          "npm": "@ai-sdk/openai-compatible",
          "name": "LocalAI (local)",
          "options": {
            "baseURL": "http://127.0.0.1:8080/v1"
          },
          "models": {
            "Qwen3-Coder-30B-A3B-Instruct-i1-GGUF": {
              "name": "Qwen3-Coder-30B-A3B-Instruct-i1-GGUF",
              "limit": {
                "context": 38000,
                "output": 65536
              }
            },
            "qwen_qwen3-30b-a3b-instruct-2507": {
              "name": "qwen_qwen3-30b-a3b-instruct-2507",
              "limit": {
                "context": 38000,
                "output": 65536
              }
            }
          }
        }
      }
    }
  3. Customize the configuration

    • baseURL: Replace http://127.0.0.1:8080/v1 with your LocalAI server’s address and port.
    • name: Change “LocalAI (local)” to a descriptive name for your setup.
    • models: Replace the model names with the actual model names available in your LocalAI instance. You can find available models by checking your LocalAI models directory or using the LocalAI API.
    • limit: Adjust the context and output token limits based on your model’s capabilities and available resources.
  4. Verify your models

    Ensure that the model names in the configuration match exactly with the model names configured in your LocalAI instance. You can verify available models by checking your LocalAI configuration or using the /v1/models endpoint.

  5. Restart OpenCode

    After saving the configuration file, restart OpenCode for the changes to take effect.

Claude Code

Claude Code is Anthropic’s official CLI tool for coding with Claude. LocalAI implements the Anthropic Messages API (/v1/messages), so Claude Code can be pointed directly at a LocalAI instance.

Prerequisites

  • LocalAI must be running and accessible (either locally or on a network)
  • You need to know your LocalAI server’s IP address/hostname and port (default is 8080)
  • An API key configured in your LocalAI instance

Running Claude Code with LocalAI

Set the ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY environment variables to point Claude Code at your LocalAI server:

ANTHROPIC_BASE_URL=http://127.0.0.1:8080 \
ANTHROPIC_API_KEY=your-localai-api-key \
claude --model your-model-name

For example, if you have a Gemma model loaded:

ANTHROPIC_BASE_URL=http://127.0.0.1:8080 \
ANTHROPIC_API_KEY=your-localai-api-key \
claude --model gemma-4-12B-it-GGUF

You can also run a single prompt non-interactively:

ANTHROPIC_BASE_URL=http://127.0.0.1:8080 \
ANTHROPIC_API_KEY=your-localai-api-key \
claude -p "list the files in /tmp" --model your-model-name

Configuration

To avoid setting environment variables every time, you can add them to your shell profile (e.g., ~/.bashrc or ~/.zshrc):

export ANTHROPIC_BASE_URL=http://127.0.0.1:8080
export ANTHROPIC_API_KEY=your-localai-api-key

Verify available models

Check which models are available in your LocalAI instance:

curl http://127.0.0.1:8080/v1/models

Use one of the listed model IDs as the --model argument.

Notes

  • Models with tool calling support (e.g., Gemma 4, Qwen 3) work best, as Claude Code relies heavily on tool use for file operations and code editing.
  • Larger models generally produce better results for complex coding tasks.
  • The Anthropic Messages API endpoint supports both streaming and non-streaming modes.

Charm Crush

You can ask Charm Crush to generate your config by giving it this documentation’s URL and your LocalAI instance URL. The configuration will look something like the following and goes in ~/.config/crush/crush.json:

{
  "$schema": "https://charm.land/crush.json",
  "providers": {
    "localai": {
      "name": "LocalAI",
      "base_url": "http://localai.lan:8081/v1",
      "type": "openai-compat",
      "models": [
        {
          "id": "qwen3-coder-480b-a35b-instruct",
          "name": "Qwen 3 Coder 480b",
          "context_window": 256000
        },
        {
          "id": "qwen3-30b-a3b",
          "name": "Qwen 3 30b a3b",
          "context_window": 32000
        }
      ]
    }
  }
}

A list of models can be fetched with https://<server_address>/v1/models by crush itself and appropriate models added to the provider list. Crush does not appear to be optimized for smaller models.

GitHub Actions

You can use LocalAI in GitHub Actions workflows to perform AI-powered tasks like code review, diff summarization, or automated analysis. The LocalAI GitHub Action makes it easy to spin up a LocalAI instance in your CI/CD pipeline.

Prerequisites

  • A GitHub repository with Actions enabled
  • A model name from models.localai.io or a Hugging Face model reference

Example Workflow

This example workflow demonstrates how to use LocalAI to summarize pull request diffs and send notifications:

  1. Create a workflow file

    Create a new file in your repository at .github/workflows/localai.yml:

name: Use LocalAI in GHA
on:
  pull_request:
     types:
       - closed

jobs:
  notify-discord:
    if: ${{ (github.event.pull_request.merged == true) && (contains(github.event.pull_request.labels.*.name, 'area/ai-model')) }}
    env:
        MODEL_NAME: qwen_qwen3-4b-instruct-2507
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0 # needed to checkout all branches for this Action to work
    # Starts the LocalAI container
    - id: foo
      uses: mudler/localai-github-action@v1.1
      with:
        model: 'qwen_qwen3-4b-instruct-2507' # Any from models.localai.io, or from huggingface.com with: "huggingface://<repository>/file"
    # Check the PR diff using the current branch and the base branch of the PR
    - uses: GrantBirki/git-diff-action@v2.7.0
      id: git-diff-action
      with:
            json_diff_file_output: diff.json
            raw_diff_file_output: diff.txt
            file_output_only: "true"
    # Ask to explain the diff to LocalAI
    - name: Summarize
      env:
        DIFF: ${{ steps.git-diff-action.outputs.raw-diff-path }}
      id: summarize
      run: |
            input="$(cat $DIFF)"

            # Define the LocalAI API endpoint
            API_URL="http://localhost:8080/chat/completions"

            # Create a JSON payload using jq to handle special characters
            json_payload=$(jq -n --arg input "$input" '{
            model: "'$MODEL_NAME'",
            messages: [
                {
                role: "system",
                content: "Write a message summarizing the change diffs"
                },
                {
                role: "user",
                content: $input
                }
            ]
            }')

            # Send the request to LocalAI
            response=$(curl -s -X POST $API_URL \
            -H "Content-Type: application/json" \
            -d "$json_payload")

            # Extract the summary from the response
            summary="$(echo $response | jq -r '.choices[0].message.content')"

            # Print the summary
            echo "Summary:"
            echo "$summary"
            echo "payload sent"
            echo "$json_payload"
            {
                echo 'message<<EOF'
                echo "$summary"
                echo EOF
              } >> "$GITHUB_OUTPUT"
    # Send the summary somewhere (e.g. Discord)
    - name: Discord notification
      env:
        DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }}
        DISCORD_USERNAME: "discord-bot"
        DISCORD_AVATAR: ""
      uses: Ilshidur/action-discord@master
      with:
        args: ${{ steps.summarize.outputs.message }}

Configuration Options

  • Model selection: Replace qwen_qwen3-4b-instruct-2507 with any model from models.localai.io. You can also use Hugging Face models by using the full huggingface model url`.
  • Trigger conditions: Customize the if condition to control when the workflow runs. The example only runs when a PR is merged and has a specific label.
  • API endpoint: The LocalAI container runs on http://localhost:8080 by default. The action exposes the service on the standard port.
  • Custom prompts: Modify the system message in the JSON payload to change what LocalAI is asked to do with the diff.

Use Cases

  • Code review automation: Automatically review code changes and provide feedback
  • Diff summarization: Generate human-readable summaries of code changes
  • Documentation generation: Create documentation from code changes
  • Security scanning: Analyze code for potential security issues
  • Test generation: Generate test cases based on code changes

Additional Resources

Realtime Voice Assistant

LocalAI supports realtime voice interactions , enabling voice assistant applications with real-time speech-to-speech communication. A complete example implementation is available in the LocalAI-examples repository. For a minimal native client, see the Go realtime voice assistant demo: a tiny Go client for the Realtime (WebSocket) API with a full talk-back loop and an example tool call, plus a docker compose setup that brings up a realtime-capable LocalAI for you.

Overview

The realtime voice assistant example demonstrates how to build a voice assistant that:

  • Captures audio input from the user in real-time
  • Transcribes speech to text using LocalAI’s transcription capabilities
  • Processes the text with a language model
  • Generates audio responses using text-to-speech
  • Streams audio back to the user in real-time

Prerequisites

  • A transcription model (e.g., Whisper) configured in LocalAI
  • A text-to-speech model configured in LocalAI
  • A language model for generating responses

Getting Started

  1. Clone the example repository

    git clone https://github.com/mudler/LocalAI-examples.git
    cd LocalAI-examples/realtime
  2. Start LocalAI with Docker Compose

    docker compose up -d

    The first time you start docker compose, it will take a while to download the available models. You can follow the model downloads in real-time:

    docker logs -f realtime-localai-1
  3. Install host dependencies

    Install the required host dependencies (sudo is required):

    sudo bash setup.sh
  4. Run the voice assistant

    Start the voice assistant application:

    bash run.sh

Configuration Notes

  • CPU vs GPU: The example is optimized for CPU usage. However, you can run LocalAI with a GPU for better performance and to use bigger/better models.
  • Python client: The Python part downloads PyTorch for CPU, but this is fine as computation is offloaded to LocalAI. The Python client only runs Silero VAD (Voice Activity Detection), which is fast, and handles audio recording.
  • Thin client architecture: The Python client is designed to run on thin clients such as Raspberry PIs, while LocalAI handles the heavier computational workload on a more powerful machine.

Key Features

  • Real-time processing: Low-latency audio streaming for natural conversations
  • Voice Activity Detection (VAD): Automatic detection of when the user is speaking
  • Turn-taking: Handles conversation flow with proper turn detection
  • OpenAI-compatible API: Uses LocalAI’s OpenAI-compatible realtime API endpoints

Use Cases

  • Voice assistants: Build custom voice assistants for home automation or productivity
  • Accessibility tools: Create voice interfaces for accessibility applications
  • Interactive applications: Add voice interaction to games, educational software, or entertainment apps
  • Customer service: Implement voice-based customer support systems

Additional Resources