Reading this as an agent? Fetch index.md for plain Markdown. A running server serves the same reference at GET /api.

NeoRouter API

NeoRouter is a self-hosted inference router. One endpoint speaks the OpenAI Chat Completions, OpenAI Responses and Anthropic Messages APIs, plus OpenAI-style embeddings, images, speech, transcription, moderation, video, web search and weather. It routes each request to one of the providers its owner has connected and falls back to another model when one fails.

Quick start

  1. Get a router key (nrt_…): create one in the web app at https://neorouter.example.com/app/ (Keys), or let your app ask for one with OAuth (see "Getting a key from an app").
  2. Point any OpenAI or Anthropic SDK at https://neorouter.example.com/v1 with that key.
  3. Use "model": "auto" unless you need a specific model.
curl https://neorouter.example.com/v1/chat/completions \
  -H "Authorization: Bearer $NEOROUTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "auto", "messages": [{"role": "user", "content": "Hello"}]}'
import os
from openai import OpenAI
client = OpenAI(base_url="https://neorouter.example.com/v1", api_key=os.environ["NEOROUTER_KEY"])
reply = client.chat.completions.create(model="auto", messages=[{"role": "user", "content": "Hello"}])
import os
from anthropic import Anthropic
client = Anthropic(base_url="https://neorouter.example.com", api_key=os.environ["NEOROUTER_KEY"])  # the SDK appends /v1
reply = client.messages.create(model="auto", max_tokens=1024, messages=[{"role": "user", "content": "Hello"}])

Authentication

Every /v1 request needs a router key. Send it the way your SDK does; all of these work:

A missing or wrong key gives 401 with code INVALID_API_KEY.

A key carries its own policy, set by its owner: which kinds of model it may use (free, prepaid plans, paid), a USD budget (total, monthly or daily), a requests-per-minute limit, a token limit for free and plan models, allowed model patterns, a default routing style and an expiry date. Requests outside that policy are refused or routed around; you cannot widen it from the request.

Choosing a model

The model field accepts:

Value Meaning
auto Let NeoRouter pick, using the key's default routing style. Also auto:default.
auto:<style> Pick with a named style: balanced, fast, smart, or a custom style's name. Older names still work: reliable → balanced, cheap → balanced, best → smart.
auto-free, auto-plan, auto-paid Like auto, but only free models, only models a subscription already covers, or only paid models. Combine with a built-in style: auto-free:fast. Custom styles already choose their kinds of model, so they have no such variants.
provider-slug/model-id That exact model first; the same model on the owner's other providers is the fallback.
model-id Every connected provider that serves this model.

GET https://neorouter.example.com/v1/models lists exactly what this key may use, including every auto… variant. Do not hardcode model ids from elsewhere: the catalog depends on which providers the owner connected.

Within auto, the router scores models on intelligence, measured speed, measured reliability and cost, skips models that are rate limited, out of quota, too small for the request or missing a needed feature (tools, vision), and learns from every outcome.

Response headers

Header Meaning
X-Request-Id Id of this request. Send your own X-Request-Id to set it.
X-NeoRouter-Model The model that answered (provider-slug/model-id).
X-NeoRouter-Trail Every model tried, in order, comma-separated. More than one means a fallback happened.
X-NeoRouter-Task Chat only: the kind of task the router saw and how hard it judged it, e.g. coding; difficulty=0.62 (0 easy to 1 hard). Easy requests can go to smaller models that suffice.
X-NeoRouter-Budget-Pace For keys with a daily or monthly budget: ahead, on_track, tight or over.
X-NeoRouter-Redacted How many values the privacy shield hid from the provider (restored in the answer).
X-NeoRouter-Compression Prompt compression level and estimated tokens saved, when enabled.
X-NeoRouter-Web-Searches Number of web searches run for a chat request.
X-NeoRouter-Embedding-Pin The embedding model an auto embedding key is pinned to.
X-NeoRouter-Audio-Format The audio format actually returned by /audio/speech.
Retry-After Seconds to wait, on 429 KEY_RATE_LIMITED.

Optional request header: X-NeoRouter-Compression: off | lossless | balanced | aggressive lowers the owner's prompt compression level for this request (it can never raise it).

Optional request header for chat: X-NeoRouter-Task: <kind> names the task when you know it better than the prompt shows. Kinds: chat, coding, math, reasoning, writing, translation, summarization, extraction, terminal, browser, agent, vision, long_context, knowledge. Unknown kinds are ignored.

Chat

POST /v1/chat/completions

OpenAI Chat Completions. Supports stream, tools / tool_choice, images in content parts, response_format, max_tokens, temperature and the other usual fields. Response and stream chunks have the OpenAI shape.

POST /v1/messages

Anthropic Messages, including streaming, tools and images. Errors use the Anthropic error shape. POST /v1/messages/count_tokens returns an estimate: {"input_tokens": 123}.

POST /v1/responses

OpenAI Responses API, including streaming and tools.

Behaviour shared by all three

Models

GET /v1/models

Lists the models this key may use, auto variants first. Add ?type=<kind> to list another kind (embedding, image, speech, transcription, moderation, video, search, weather, live); the default is chat. With an anthropic-version header the list uses the Anthropic shape.

Each model has the OpenAI fields plus:

The list also carries neorouter.not_allowed_for_key: how many models of this kind exist but are outside the key's free/plan/paid choice. Use it to tell "nothing connected" from "not allowed for this key".

GET /v1/models/{model}

One chat model, or 404 MODEL_NOT_FOUND.

Embeddings

POST /v1/embeddings

{"model": "auto", "input": ["first text", "second text"], "dimensions": 768, "encoding_format": "float", "input_type": "document"}

Images

POST /v1/images/generations

{"model": "auto", "prompt": "a lighthouse at dawn", "n": 1, "size": "1024x1024", "response_format": "b64_json"}

n is 1–4. response_format is b64_json (default) or url. URLs point at https://neorouter.example.com/v1/media/{id}, need no key and expire.

Speech

POST /v1/audio/speech

{"model": "auto", "input": "Hello there", "voice": "alloy", "response_format": "mp3", "speed": 1.0}

Returns audio bytes. input is at most 4096 characters. response_format: mp3, opus, aac, flac, wav, pcm.

Transcription

POST /v1/audio/transcriptions and POST /v1/audio/translations

multipart/form-data with a file field (at most 25 MB) and optional model, language, prompt, temperature, response_format (json, text, srt, vtt, verbose_json). Translations return English text.

curl https://neorouter.example.com/v1/audio/transcriptions -H "Authorization: Bearer $NEOROUTER_KEY" \
  -F file=@meeting.mp3 -F model=auto -F response_format=srt

Moderation (System One)

POST /v1/moderations

{"model": "auto", "input": "text to check", "check": "safety"}

Returns OpenAI moderation results. check (NeoRouter extension) is safety (default), prompt_injection, or topic together with a policy that says which topics are allowed. input may be up to 32 texts.

POST /v1/systemone

Typed decisions over the decision models of the connected providers.

{"model": "auto", "state": {"…": "…"}, "questions": {"…": "…"}}

Returns {"model": "…", "answers": …, "usage": …}.

Video

Video generation is asynchronous.

Web search

POST /v1/search

{"model": "auto", "query": "latest node.js release", "max_results": 5, "include_domains": [], "exclude_domains": [], "recency": "week"}

max_results is 1–20; recency is day, week, month or year. Returns {"object": "search", "model", "query", "results": [{"title", "url", "snippet", "content", "published"}]}.

Weather

POST /v1/weather

{"model": "auto", "location": "Berlin", "current": true, "hourly": false, "daily": true, "days": 3, "units": "metric"}

location is a place name or {"latitude": 52.52, "longitude": 13.41}. days is 1–16 (services cap it at their own forecast length); units is metric (°C, km/h, mm) or imperial (°F, mph, inch). At least one of current, hourly and daily must be true. Services: Open-Meteo (no key), OpenWeatherMap, WeatherAPI.com, Tomorrow.io and Visual Crossing.

Returns the same shape whichever service answered; a part not asked for is null:

{"object": "weather", "model": "open-meteo/open-meteo-forecast", "units": "metric",
 "location": {"name", "latitude", "longitude", "timezone"},
 "current": {"time", "temperature", "feels_like", "humidity", "wind_speed", "wind_direction", "precipitation", "condition"},
 "hourly": [{"time", "temperature", "precipitation_probability", "precipitation", "wind_speed", "condition"}],
 "daily": [{"date", "temperature_max", "temperature_min", "precipitation", "precipitation_probability", "condition", "sunrise", "sunset"}]}

Live voice (WebSocket)

Connect to the same path you would use at the provider, with a router key instead of the provider's. Messages pass through unchanged; only the model name is rewritten. The model is chosen when the session opens and stays fixed.

Protocol URL Model comes from
OpenAI Realtime wss://neorouter.example.com/v1/realtime?model=auto the model query parameter
OpenAI Live wss://neorouter.example.com/v1/live/sessions session.model in the first session.start event
Gemini Live wss://neorouter.example.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent setup.model in the first message

Browsers that cannot set headers can pass the key as ?key=nrt_… or as the openai-insecure-api-key.nrt_… subprotocol. GET /v1/models?type=live shows which protocols each model speaks.

Errors

OpenAI-style endpoints return:

{"error": {"message": "Readable explanation.", "type": "invalid_request_error", "code": "NO_MODEL_AVAILABLE", "details": {}}}

/v1/messages returns {"type": "error", "error": {"type", "message"}, "request_id"}. A failure after a stream started arrives as an error event in the stream.

Status Code What to do
400 INVALID_REQUEST, INVALID_JSON Fix the request body.
400 CONTEXT_TOO_LARGE The input is larger than every model this key may use. Shorten it.
400 UNKNOWN_PROFILE The auto:<style> name does not exist. Check GET /v1/models.
400 EMBEDDING_PIN_MISMATCH Use the pinned dimensions or reset the pin.
401 INVALID_API_KEY Send a valid nrt_… key.
402 BUDGET_EXHAUSTED The key's budget is used up. Wait for the period to reset or ask the owner.
403 MODEL_NOT_ALLOWED The named model is outside the key's policy. Use auto or a listed model.
404 MODEL_NOT_FOUND The model is not in the catalog. Use GET /v1/models.
413 BODY_TOO_LARGE, FILE_TOO_LARGE Send less.
429 KEY_RATE_LIMITED Wait Retry-After seconds.
429 TOKEN_LIMIT_REACHED The key's token limit for free and plan models is used up for this period.
502 ALL_ATTEMPTS_FAILED Every candidate failed. details.trail lists the attempts. Retry later.
503 NO_MODEL_AVAILABLE No model can take the request right now (rate limits, quotas, missing features). When a model frees up within a few seconds NeoRouter waits for it instead (up to 45 s); this error means none would. The message says why; details.rejected counts the reasons, and Retry-After (also details.retryAfterSeconds) says when one frees up, if known.
503 EMBEDDING_PIN_UNAVAILABLE No provider serves the pinned embedding model right now.

Retry 429, 502 and 503 with backoff. Do not retry 400–404 unchanged.

Getting a key from an app (OAuth)

Apps can ask the owner for a router key instead of having it pasted in. Discovery: GET https://neorouter.example.com/.well-known/oauth-authorization-server. Scopes: chat, embedding, image, speech, transcription, moderation, video, search, weather, live (space-separated). The owner decides on the approval screen which kinds of model, budget and limits the key gets.

Authorization code with PKCE (S256) for apps with a browser:

  1. Send the user to https://neorouter.example.com/oauth/authorize?response_type=code&client_id=…&redirect_uri=…&code_challenge=…&code_challenge_method=S256&scope=chat&state=….
  2. Exchange the returned code: POST https://neorouter.example.com/oauth/token with grant_type=authorization_code, code, client_id, redirect_uri, code_verifier.

Device flow for CLIs and agents without a browser:

  1. POST https://neorouter.example.com/oauth/device/code with client_id (and optional client_name, scope). You get device_code, user_code, verification_uri_complete, interval, expires_in.
  2. Show the user verification_uri_complete (or user_code).
  3. Poll POST https://neorouter.example.com/oauth/token with grant_type=urn:ietf:params:oauth:grant-type:device_code, device_code, client_id every interval seconds. authorization_pending means keep waiting; slow_down means add 5 seconds; access_denied / expired_token mean stop.

Both return {"access_token": "nrt_…", "token_type": "Bearer", "scope", "base_url": "https://neorouter.example.com/v1", "key_id"}. Use access_token as the router key against base_url.

Other endpoints