http://localhost:18080/v1
All requests require an Authorization: Bearer <api-key> header matching a key in config.yaml.
POST /v1/chat/completions
Standard OpenAI chat completions. Supports streaming ("stream": true).
| Parameter | Type | Required | Description |
|---|---|---|---|
model |
string | Yes | Model name (e.g., gemini-2.5-pro, ollama:llama3.2) |
messages |
array | Yes | Array of message objects |
stream |
boolean | No | Enable SSE streaming |
tools |
array | No | Tool definitions. [{"type":"web_search"}] is supported natively by ail-compound — see Web Search below. |
ail-compound supports a built-in web_search tool that performs live web lookups and folds the results into the response. Enable it via the tools field:
curl http://localhost:18080/v1/chat/completions \
-H "Authorization: Bearer $AIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ail-compound",
"messages": [{"role": "user", "content": "Who won the 2026 Super Bowl?"}],
"max_tokens": 2000,
"tools": [{"type": "web_search"}]
}'Important: web search inflates the prompt context with search results (often 6k–13k prompt tokens per query). Set max_tokens >= 2000 to leave headroom for reasoning + final answer, otherwise the response will be truncated or empty. The response returns the final answer in choices[0].message.content — there are no separate tool_calls for the client to handle; the model calls search internally.
Recommended path (2026-04-22 on): Agentic callers should discover provider-native tools by calling
GET /v1/modelsand splicing each.data[].native_tools[]entry into their owntools[]at chat-completion time./v1/modelsis the source of truth.tytus capabilities(the Tytus CLI wrapper) prints the same surface as a human tree for quick inspection. The autoinject path below remains as a fallback for clients that cannot or do not consume/v1/models— naive SDKs, legacy scripts, and agents where adding a discovery step isn't feasible.
Operators running switchailocal in front of agents that don't themselves send tools: [{"type": "web_search"}] (e.g. a LangChain OpenAI client, naive SDKs) can have the handler append the entry on the way through. As of the 2026-04-22 native-tool-discovery sprint, autoinject is discovery-driven by default: when the target model has native_tools declared in config.yaml (under openai-compatibility.*.models[].native_tools), those entries drive the injection surface — no env-var allowlist needed. AIL_AUTOINJECT_MODELS is retained as a legacy env-var fallback for operators who haven't populated native_tools yet.
| env var | values | effect |
|---|---|---|
AIL_AUTOINJECT_WEBSEARCH |
"true" or anything else |
master flag — "true" activates autoinject; any other value (incl. empty) = OFF |
AIL_AUTOINJECT_MODELS |
"ail-compound,minimax/ail-compound" (example) |
Legacy. Comma-separated model-name allowlist; whitespace trimmed; only these request-model names receive injection on the fallback path. Ignored when the model has native_tools declared in config — discovery wins and the allowlist becomes irrelevant. |
AIL_AUTOINJECT_FORCE_THRESHOLD |
integer (default 5, 0 disables) |
when the caller's tools already has ≥ N entries with type:"function", the injected web_search entry is stamped force_search:true — beats MiniMax's function-tool preference heuristic for agentic clients. Below the threshold the bare autonomous form is used. Applies on both the discovery and the legacy allowlist paths. |
AIL_DEBUG_DUMP |
"true" or empty |
logs the raw request body at handler entry to /app/logs/main.log — useful for auditing what agents send |
Semantics (set-union + dedupe):
- Caller
toolsare preserved. A{"type":"web_search"}entry is appended only when no existing entry intoolshastype == "web_search". - Caller-parameterised
web_search(e.g.{"type":"web_search","force_search":true,"max_keyword":5}) wins untouched — autoinject never overwrites it with a bare entry. This is the path a caller that consumes/v1/models.native_toolsand splices it themselves will hit: autoinject becomes a silent no-op. - When injection fires,
max_tokensis bumped to the 2000 floor documented above if the caller sent a lower value or none at all. Pre-existingmax_tokens >= 2000is preserved. - Injection does not fire when: the master flag is off, the caller sent
X-Ail-Autoinject: off, the caller already included aweb_searchtool, OR (on the fallback path) the model is not inAIL_AUTOINJECT_MODELS.
Per-request opt-out header:
X-Ail-Autoinject: off
suppresses injection for that single request (useful when a caller wants the raw no-tools path for comparison or cost reasons).
Response header for build verification:
Every /v1/chat/completions response carries:
X-Ail-Build: <commit-sha>-<hostname>
Operators running multiple switchailocal instances behind a load-balancer use this to verify each instance is on the expected build after a rolling restart — for example by issuing ~N×2 requests through the LB and asserting every instance-ID appears with the expected sha.
POST /v1/completions
Legacy completions endpoint.
GET /v1/models
List all available models. Supports modality filtering.
| Query Param | Values | Description |
|---|---|---|
modality |
text, image, audio, embedding, vision |
Filter models by capability |
Response includes a capabilities array per model (e.g., ["text"], ["image"], ["audio"]).
Each model entry may carry a native_tools array describing the provider-native tools that upstream model supports out of the box (currently: MiniMax M2.7's autonomous web_search). Agentic runtimes like OpenClaw and Hermes are expected to query /v1/models at session init, parse .data[*].native_tools, and splice each entry into their own caller tools[] at chat-completion time. MiniMax then picks web_search as a first-class declared tool for recent-events queries — no server-side autoinject hack required.
The shape mirrors the OpenAI tools[] wire format so callers splice {"type": "web_search"} (plus any params they want to override) directly into their chat-completion request without translation. Models with no provider-native tools omit the key entirely rather than emitting null / [] — use key presence as your discovery signal. Operators declare the surface in openai-compatibility.*.models[].native_tools in config.yaml; see config.example.yaml for the full shape.
POST /v1/embeddings
Generate vector embeddings for input text.
| Parameter | Type | Required | Description |
|---|---|---|---|
model |
string | Yes | Embedding model (e.g., switchai-embed) |
input |
string/array | Yes | Text to embed |
POST /v1/images/generations
Generate images from text prompts. Supports application/json and multipart/form-data.
| Parameter | Type | Required | Description |
|---|---|---|---|
model |
string | Yes | Image model (e.g., dall-e-3) |
prompt |
string | Yes | Image description |
size |
string | No | Image size (e.g., 1024x1024) |
n |
integer | No | Number of images |
Note
Provider Quirks: For the minimax provider, switchAILocal automatically rewrites the upstream path from /v1/images/generations to their native /v1/image_generation to maintain OpenAI compatibility.
For existing SwitchAI clients, /v1/images/generations preserves the configured upstream response shape. For strict OpenAI Images API clients that require data[0].url or data[0].b64_json, use the additive compatibility namespace:
POST /openai/v1/images/generations
This route leaves already-standard OpenAI image responses unchanged and normalizes gateway responses such as {"data":{"image_urls":["https://..."]}} to {"data":[{"url":"https://..."}]}.
POST /v1/images/edits
Edit existing images. Supports application/json (image URLs) and multipart/form-data (binary upload).
| Parameter | Type | Required | Description |
|---|---|---|---|
model |
string | Yes | Image model |
prompt |
string | Yes | Edit instructions |
image |
file/url | Yes | Source image |
Strict OpenAI-compatible image edits are also available at:
POST /openai/v1/images/edits
| mask | file/url | No | Edit mask |
POST /v1/audio/speech
Convert text to speech audio.
| Parameter | Type | Required | Description |
|---|---|---|---|
model |
string | Yes | TTS model or alias, usually ail-speech |
input / text |
string | Yes | Text to speak |
voice / voice_setting.voice_id |
string | Yes | MiniMax voice ID, e.g. English_expressive_narrator |
response_format / audio_setting.format |
string | No | mp3 (default), flac, wav for MiniMax sync T2A |
Response is binary audio with appropriate Content-Type.
MiniMax speech-2.8-hd is the default speech model in intelligence.matrix. The gateway runs an adapter (internal/runtime/executor/minimax_tts.go) that translates OpenAI-shape or MiniMax-native requests into MiniMax's native /v1/t2a_v2 API and resolves the returned hex/url audio to raw bytes before handing them back.
curl http://localhost:18080/v1/audio/speech \
-H "Authorization: Bearer $AIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ail-speech",
"input": "Hello from switchAILocal",
"voice": "English_expressive_narrator",
"response_format": "mp3"
}' \
--output hello.mp3MiniMax-native T2A controls are also accepted through the same local endpoint:
curl http://localhost:18080/v1/audio/speech \
-H "Authorization: Bearer $AIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ail-speech",
"text": "Omg(sighs), the real danger is not that computers start thinking like people.",
"stream": false,
"voice_setting": {
"voice_id": "English_expressive_narrator",
"speed": 1,
"vol": 1,
"pitch": 0
},
"audio_setting": {
"sample_rate": 32000,
"bitrate": 128000,
"format": "mp3",
"channel": 1
},
"pronunciation_dict": {
"tone": ["Omg/Oh my god"]
},
"language_boost": "auto",
"voice_modify": {
"pitch": 0,
"intensity": 0,
"timbre": 0,
"sound_effects": "spacious_echo"
},
"output_format": "hex"
}' \
--output speech.mp3Voice IDs are MiniMax-native — OpenAI voice names (alloy, echo, fable, onyx, nova, shimmer) are not aliased. Sample MiniMax voices:
| Voice ID | Description |
|---|---|
English_expressive_narrator |
English narrator, expressive |
male-qn-qingse |
Male, calm |
female-shaonv |
Female, young |
audiobook_male_2 |
Male, narrator-style |
presenter_male |
Male, anchor-style |
clever_boy |
Male, youthful |
Supported formats: mp3 (default), flac, wav for non-streaming MiniMax t2a_v2. Bitrate and sample rate can be overridden via OpenAI-style top-level bitrate, audio_sample_rate, channel, or MiniMax-native audio_setting.
Streaming: MiniMax t2a_v2 supports streaming, but SwitchAI Local's /v1/audio/speech adapter currently exposes synchronous raw bytes only. Send "stream": false.
Plan limits: MiniMax Plus token plan allows ~9000 characters/day and imposes a very tight RPM (1–5 requests/min). When the RPM is exceeded, MiniMax returns internal code 1002 which the adapter maps to HTTP 429 (rate_limit) — the failover taxonomy then classifies this as ClassRateLimit and advances to the next provider if a fallback chain is configured. Internal code 2061 ("plan not support") maps to HTTP 402 (out_of_credits).
Known error codes (internal MiniMax → HTTP):
| MiniMax code | HTTP | Meaning | Failover class |
|---|---|---|---|
| 0 | 200 | success | — |
| 1002 | 429 | RPM rate limit | ClassRateLimit (advances) |
| 1004 / 1008 | 401 | auth / balance | ClassAuth (advances) |
| 2013 | 400 | invalid params | ClassPermanent (aborts) |
| 2061 | 402 | plan doesn't support this model | ClassOutOfCredits (advances) |
POST /v1/music/generations
Generate music from lyrics using MiniMax music-2.6 (text-to-music) or music-cover (reference-audio style transfer). Synthesis runs synchronously by default and typically takes 30–90 seconds for a 1–2 minute song; the sync response hex-decodes the upstream audio and returns base64-encoded MP3 bytes with lifted metadata. Pass stream: true to get raw MP3 bytes streamed progressively (see Streaming mode below).
| Parameter | Type | Required | Description |
|---|---|---|---|
model |
string | No | ail-music (default via intelligence.matrix.music_generation) or ail-music-cover |
stream |
bool | No | true streams raw audio/mpeg bytes as they arrive (~20s TTFB vs ~60s sync); default false returns JSON with base64 audio |
lyrics |
string | Yes for music-2.6 |
Lyrics with optional structure tags: [Intro], [Verse], [Chorus], [Bridge], [Outro], [Inst] |
prompt |
string | Yes for music-cover (10–300 chars) |
Style description for the cover |
audio_url OR audio_base64 |
string | Yes for music-cover |
Reference audio: 6s–6min, ≤50MB, mp3/wav/flac. Mutually exclusive. |
Response shape:
{
"model": "music-2.6",
"trace_id": "063294b934ecf975cf75a296dc3cc3f4",
"data": {
"audio": "<base64-encoded MP3 bytes>",
"format": "mp3",
"size_bytes": 3150729,
"duration_ms": 98298,
"sample_rate": 44100,
"channels": 2,
"bitrate": 256000
},
"extra_info": { /* raw upstream metadata */ }
}Text-to-music example:
curl http://localhost:18080/v1/music/generations \
-H "Authorization: Bearer $AIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ail-music",
"lyrics": "[Verse]\nCode flows through the night\n[Chorus]\nDebugging makes it right"
}' | jq -r .data.audio | base64 -d > song.mp3Style-cover example (reference audio):
curl http://localhost:18080/v1/music/generations \
-H "Authorization: Bearer $AIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ail-music-cover",
"prompt": "upbeat jazz cover with saxophone solo",
"audio_url": "https://example.com/reference.mp3"
}' | jq -r .data.audio | base64 -d > cover.mp3Plan limits: MiniMax Plus plan = 100 songs/day each for music-2.6 and music-cover. Max song length 6 minutes. Errors follow the same mapping as TTS — 2013 (invalid_params) aborts, 1002 (rate_limit) and 2061 (plan_not_support) are advance-eligible.
Streaming mode (stream: true):
Instead of JSON with a base64 audio blob, the server returns Content-Type: audio/mpeg with raw MP3 bytes streamed as they arrive from upstream. Measured wins vs sync mode (verified 2026-04-18 against live MiniMax):
- TTFB ≈ 20s vs 30–90s sync — client can start playback before MiniMax finishes generating
- ~50% less wire bandwidth — upstream's SSE stream includes a terminal frame that duplicates the full song as hex; the adapter discards it and only forwards progressive chunks
- Natively playable — the first chunk carries the ID3v2 header, so concatenated bytes form a valid MP3 that any audio element can decode while downloading
curl -N http://localhost:18080/v1/music/generations \
-H "Authorization: Bearer $AIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ail-music",
"stream": true,
"lyrics": "[Verse]\nCode flows through the night\n[Chorus]\nDebugging makes it right"
}' > song.mp3<!-- Browser: play while downloading -->
<audio src="/v1/music/generations" controls></audio>
<!-- send the request via fetch() with POST body then feed response.body to MediaSource -->Pre-first-byte errors (HTTP 5xx, 429, 2061 plan-not-support, upstream stall within 60s) return a JSON error with the appropriate status code. Errors that arrive AFTER bytes have been flushed close the stream cleanly — the client keeps whatever MP3 frames it received, which are still playable.
POST /v1/music/lyrics
Generate structured song lyrics. Typical latency 2–5 seconds. Pure JSON in / JSON out — no audio.
| Parameter | Type | Required | Description |
|---|---|---|---|
mode |
string | No | write_full_song (default) or edit (modify/continue existing lyrics) |
prompt |
string | No | Song theme / style description (max 2000 chars). Empty = random. |
lyrics |
string | Only in edit mode |
Existing lyrics to extend (max 3500 chars) |
title |
string | No | Desired song title |
Response shape:
{
"song_title": "Midnight Code Delight",
"style_tags": "pop, happy, coding, late night, electronic",
"lyrics": "[Intro]\n\n[Verse]\nScreen glow bright...",
"base_resp": { "status_code": 0, "status_msg": "success" }
}Example:
curl http://localhost:18080/v1/music/lyrics \
-H "Authorization: Bearer $AIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "write_full_song",
"prompt": "a happy pop song about sunshine"
}'Common workflow: call /v1/music/lyrics first to generate structured lyrics, then feed the result into /v1/music/generations with model: "ail-music".
Plan limits: 100 lyrics/day on the Plus plan.
POST /v1/audio/transcriptions
Transcribe audio to text. Requires multipart/form-data.
| Parameter | Type | Required | Description |
|---|---|---|---|
model |
string | Yes | Transcription model (e.g., whisper-1) |
file |
file | Yes | Audio file (mp3, mp4, mpeg, mpga, m4a, wav, webm) |
language |
string | No | ISO-639-1 language code |
response_format |
string | No | json, text, srt, verbose_json, vtt |
POST /v1/audio/translations
Translate audio to English text. Requires multipart/form-data.
| Parameter | Type | Required | Description |
|---|---|---|---|
model |
string | Yes | Translation model (e.g., whisper-1) |
file |
file | Yes | Audio file in any language |
response_format |
string | No | json, text, srt, verbose_json, vtt |
GET /v1/providers
List all configured AI providers.
| Query Param | Values | Description |
|---|---|---|
filter |
active, inactive, all |
Filter by provider status |
POST /v1/models/refresh
Trigger re-discovery of available models from all providers.
| Query Param | Description |
|---|---|
provider |
Refresh specific provider only |
GET /metrics
Prometheus-compatible metrics endpoint.
Exposes standard Go process metrics along with switchAILocal-specific telemetry:
switchailocal_requests_total: Total requests by model, provider, status, and routing type.switchailocal_request_duration_milliseconds: Request latency histogram.switchailocal_llm_tokens_total: Input and output token consumption.switchailocal_routing_quality_score: Routing Quality Score (RQS) tracking.switchailocal_fallbacks_total: Failover/fallback tracking by model.
Note: This endpoint is served on the root level (/metrics), not under /v1/.
GET /v0/management/observability/dashboard
Returns real-time proxy health and Go runtime statistics (heap, GC, goroutines). Requires your management-secret-key.
📖 See the Performance & Production Guide for details on tuning rate limits, load shedding, and observability.
All advanced parameters are passed through transparently to the upstream provider. switchAILocal doesn't modify or filter these fields.
{
"model": "xiaomi:mimo-v2-flash",
"messages": [{"role": "user", "content": "Latest AI news?"}],
"tools": [{"type": "web_search", "max_keyword": 3, "force_search": true, "limit": 3}],
"tool_choice": "auto"
}{
"model": "gemini-2.5-pro",
"messages": [{"role": "user", "content": "Weather in Berlin?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location"]
}
}
}],
"tool_choice": "auto"
}{
"model": "claudecli:claude-sonnet-4",
"messages": [{"role": "user", "content": "Prove sqrt(2) is irrational"}],
"thinking": {"type": "enabled", "budget_tokens": 10000}
}{
"model": "geminicli:gemini-2.5-pro",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "What do you see?"},
{"type": "image_url", "image_url": {"url": "https://example.com/photo.jpg"}}
]
}]
}Pass local files and folders to CLI providers:
{
"model": "geminicli:gemini-2.5-pro",
"messages": [{"role": "user", "content": "Review this code"}],
"extra_body": {
"cli": {
"files": ["/path/to/main.go"],
"directories": ["/path/to/internal/"]
}
}
}Set "stream": true to receive Server-Sent Events:
{
"model": "gemini-2.5-pro",
"messages": [{"role": "user", "content": "Tell me a story"}],
"stream": true
}All endpoints require a valid API key in the Authorization header:
Authorization: Bearer sk-your-key-here
API keys are configured in config.yaml under access-management.api-keys.
{ "id": "ail-compound", "object": "model", "owned_by": "minimax", "capabilities": ["text", "vision", "audio"], "native_tools": [ { "type": "web_search", "description": "MiniMax native web search. Autonomous by default; set force_search:true to force when the question is clearly recent-events.", "params": { "force_search": { "type": "boolean", "default": false }, "max_keyword": { "type": "integer", "default": 3 }, "limit": { "type": "integer", "default": 3 } } } ] }