Before you start
Three things decide whether your first request works:- The right key. Create the key inside a project from Improve my agent,
on its Project Vault > SLNG API key page. A models-only key from Call
TTS/STT models API returns
403 project_mode_forbidden. - The project’s region. The key works only in the region the project was
created in. Use that region’s code in the base URL,
https://{region}.context-router.slng.ai/v1, and in your speech base URL. - An agent ID and a session ID on every request. See Required fields.
Endpoint and authentication
The router exposes two OpenAI-compatible surfaces. Both share the same auth, routing, caching, and features.The
/responses endpoint is a translation layer over the same router:
identical auth, routing, caching, and features. Everything in this guide
applies to both surfaces unless a section says otherwise.SLNG_API_KEY, passed as a Bearer token. The
OpenAI SDK’s api_key parameter sets this header for you, so you never set
internal auth headers yourself.
Making a request
A request is a standard OpenAI Chat Completions call.Streaming
Streaming works exactly as in OpenAI, for every model the router serves (reasoning and non-reasoning alike).Streaming request
-
Reasoning blocks and control tokens are removed for you. If your model emits
a visible reasoning block (
<think>,<thought>,<thinking>,<reasoning>) or a stray control token (</s>,<|im_end|>and similar), the router strips it before the chunk reaches you, so your TTS never reads it aloud. Settingslng_pure_proxysuspends this. -
On a stream that completes, all answer content arrives before the
finish_reason: "stop"chunk, so stopping there is safe. -
The
usageblock rides the final chunk. That is whyinclude_usagematters. -
A stream that fails says so. If the model fails once the stream is open,
even before its first token, the router cannot change the
200it already sent, so it ends the stream with one error chunk and thendata: [DONE]:That stream carries noError chunkfinish_reasonand nousageblock.codeis the status you would have received if the request had failed before streaming began:504for a timeout, the provider’s own status if it rejected the request (for example429), and502otherwise. A failure inside the router itself ends the stream the same way, with"type": "server_error","message": "Internal error"and"code": 500, so check for both types. The OpenAI SDKs raise anAPIErrorwhen they read this chunk (in Python, a plainopenai.APIErrorwith the status ine.code), so your client sees an error instead of a short answer that looks complete. Treat the text you already received as partial: a voice agent should not present it as a finished reply. On the/responsesendpoint the same failure ends the stream with aresponse.failedevent instead ofresponse.completed.
Available parameters
slng/auto vs a named model
slng/auto
Recommended. The router answers from your configuration, splitting traffic by
weight. With two or more tiers, it picks a tier for each request, and it fails over when a model fails.
A named model
The exact name of one model in the
slng_config you send. It pins the request to that
model and skips per-turn routing, so there is no failover to another model, and
its traffic keeps its own cache entries.Required fields
Your org identity is managed by SLNG. The agent ID and session ID are yours to send, and both are required on every request.- Agent ID names the logical agent a call belongs to, and scopes its cache. Use
a stable value per agent. When you change that agent’s prompt in a meaningful way,
give it a fresh ID, for example
clinic-scheduler-v2, so answers produced under the old prompt are not reused. - Session ID identifies a single call. Use a value unique to each call, like a UUID, and keep it the same across all turns of that call.
X-Slng-Agent-Id, X-Slng-Session-Id) or as a
top-level body field (slng_agent_id, slng_session_id). For the value format and
the errors a missing or malformed ID returns, see
Agent and session headers.
Reading the response
Every request returns a set ofx-slng-* headers that tell you how the answer
was produced.
On an error, only
x-slng-request-id comes back, because no answer was produced.
Two outcomes are possible on a successful request:
Normal LLM response
x-slng-response-source: llm. The request was routed to the LLM, and usage
carries the tokens you were billed for.Cache hit
x-slng-response-source: cache. No LLM ran. usage carries the original
answer’s tokens, so it is what the cache saved rather than what this call cost.Response caching
The Context Router is a PII-aware service that can serve a repeated turn straight from a cache instead of calling the LLM again. Caching is on by default, and a turn that qualifies is served from cache automatically. Cache layers are checked fastest-first; the first hit wins.Pre-warmed answers are set up together with you, per agent.
What SLNG does not store
So one caller’s answer is never replayed to another, some responses are not stored. The caller still gets their answer; only the caching is skipped.- Any reply containing a number, in any script. A number is usually specific to one caller or record, so identifiers, prices and dates are never cached.
- Personal information, such as names, addresses and emails.
- Any reply carrying a placeholder the router did not fill in itself.
Per-call values (template_variables)
Voice agents need per-call values inside the system prompt: the caller’s name,
their city, an appointment time. Instead of building those strings yourself
before every request, write a placeholder once and let SLNG fill it in.
Do this rather than building the string yourself. The stored answer then holds the
placeholder instead of the caller’s data, which is what makes a personalized
answer cacheable and lets your callers share one entry. The model sees the same
final prompt either way.
Syntax. Use double braces around the variable name:
{{caller_name}},
{{city}}. Names are letters, digits, and underscore. Single braces
({name}), Jinja, and ${var} are not placeholders. Only {{name}} is.
Substitution applies to your prompt messages, the system and developer roles.
Your user and assistant message content is never modified, so a literal
{{...}} a caller happens to type is left exactly as-is.You are calling Rajesh in Mumbai.
What belongs in template_variables
- Do carry personalization values that get spoken or echoed in the conversation: the customer’s name, the agent’s name, the company name, an appointment detail rendered as text.
- Don’t carry values that steer or change the content of the answer itself: the response language, a plan or tier that changes which policy is described, a region that changes which rules apply.
- The language case deserves spelling out. A language variable is fine when it is
just text to be said, for example the agent confirming “So your preferred
language is
{{language}}, correct?” during the call. It is not fine when it controls the language the model answers in: two callers who chose different languages would share one cached answer, and one would hear the wrong language.
Limits. Up to 64 variables per request, names up to 64 characters, values
up to 4000 characters. Exceeding these returns a
422.Runtime variables. To leave a placeholder deliberately unfilled, because your
own orchestrator fills it mid-call, set
template_vars_strict: false on your
configuration. The placeholder then passes through untouched, and an answer that
echoes one is never cached.Which configuration answers
Every request is answered from one configuration: its models, in up to three tiers, and its settings, such as caching. Which one depends on how the request reaches the router:- A key created in a project uses that project’s configuration: the model chosen in the project’s Think section.
- An agent you deploy on SLNG uses its own configuration, chosen in its own Think section.
slng_config in the request applies on top
of the configuration that applies, for that request only. See
Inline, per request.
BYOK Context Router (slng_config)
There are two ways to give the router your models:
Inline, per request
Send
slng_config in the request body. It can name any endpoint and key,
including ones you never saved in the dashboard.In the Think section
Choose a model in your project’s Think section. LLM keys you save on the dashboard’s BYOK page are offered there.
Inline, per request
Whether yourslng_config carries tiers decides which models answer:
- With
tiers, the models in it answer, on the endpoints and keys it names. - Without
tiers, the models of the configuration that applies answer. For a key created in a project, those are the project’s.
slng_config
sends replaces the configuration’s value for that request only, and every field it
leaves out keeps the configuration’s value. So sending your own models with a
project’s key keeps the project’s other settings.
Your tiers and your default_tier travel together. If you send tiers, the
configuration’s own default_tier no longer applies: send yours with them, or it
is unset.
slng_config carries your own endpoints and provider keys, so treat it like a
credential: send it only over HTTPS and keep it out of logs.Saved in the dashboard
Every LLM key you save on the BYOK page is offered in your projects’ Think sections, wherever it serves the project’s language and region. Choose it there to use it. A key marked Disabled cannot be chosen or used.Smart routing
Tiers rank your models: tier 1 is the cheapest, tier 3 the most capable. With"model": "slng/auto" (the default when you omit model) and two or more
filled tiers, the router picks a tier for each request: the cheapest one it
expects to answer that turn well. There is no separate field: send two or more
tiers in slng_config.
- Turns that may need a tool call, turns that look demanding, and turns the router cannot judge (an image, say) go to your most capable tier.
default_tieranswers an opening line your agent has the model write, before the caller has said anything, whether or not tools are sent with it (most frameworks send an agent’s tools with every request). Only a request that forces a tool call goes to your most capable tier. A fixed greeting your code speaks never reaches the router. Unset, your cheapest tier answers that line. If the tier you set is empty, the nearest filled tier above it answers, or your most capable tier when there is none above.- One tier means no per-turn routing. That tier answers every turn, exactly as before.
- Naming a model in
modelpins it and skips per-turn routing. - With
slng_pure_proxy, there is no per-turn pick: yourdefault_tieranswers every turn, or your most capable tier when it is unset (see Shadow mode).
x-slng-model response header names the model that
answered, after any failover. A cache hit carries no x-slng-model.
Tiers are for per-turn routing, not a backup list. If you have been sending two
tiers to mean “preferred model first, backup second”, put both in one tier and
give the backup priority: 1. One tier means no per-turn routing, and the
backup still answers when the preferred model fails:
slng_config
Example: two tiers
This request sends two Gemini models through Google’s OpenAI-compatible endpoint. A short, formulaic turn goes togemini-2.5-flash-lite. Every other turn,
including one that may need a tool call, goes to gemini-2.5-flash.
Two tiers
Failover
If the chosen model fails (a5xx, a timeout, a 429 or a dropped connection),
the router tries another model when your configuration has one, in this order:
- Another model in the same tier.
- Otherwise, the next more capable tier.
- Otherwise, when the chosen tier is already your most capable, the closest cheaper tier.
- How many models. Without
priorityentries, a request tries at most two models. If the chosen tier has entries atpriority1,2, …, the router also works through them, in that order, before it moves on to other tiers, trying at most four models. - Brief outages. On a
502,503or504, or a dropped connection, the router retries the same model once before it steps down to a cheaper tier or gives up. A pinned model gets only that retry. - Other errors. Any other
4xxfrom your provider comes back to you straight away. If every model tried fails, the request fails with the last error. - Streams. Failover happens only before a stream starts. A failure after that ends the stream with an error chunk (see Streaming).
Configuration reference
A configuration has these fields:
These are the fields most requests need. Any other top-level setting of the
configuration can be sent in
slng_config the same way, and replaces that setting
for the request. See Inline, per request.
Each entry in a tier has:
The smallest useful configuration is one tier with one OpenAI-compatible
endpoint:
Supported providers
Theendpoint object defaults to provider: "openai-compat", which covers any
service that speaks the OpenAI Chat Completions API (OpenAI itself, Azure-hosted
alternatives, Groq, self-hosted vLLM, and so on). Four more providers are
supported; each needs its own fields on the endpoint object.
On
azure, url is the resource root, not the full deployment URL. On
openai-compat, add auth_header when the provider wants its key in a custom
header. Fields belonging to another provider are rejected rather than ignored.
For example, an Azure OpenAI entry looks like:
Limits and errors
- The
slng_configobject must stay under 256 KB when serialized; larger returns a400. - A configuration that fails validation returns a
400starting withinvalid slng_config:and a description of the problem. Your credentials never appear in these messages. - Sending
slng_configas anything other than a JSON object (a string, a list) returns a422with codeinvalid_slng_config.
Shadow mode
Two optional flags let you route production traffic through SLNG and measure what it would save, before you let it change anything. Both default tofalse and need no
setup.
Send both together and you have a shadow trial. You receive exactly what your own
model produces, while the router measures what it could have served from cache and
what that would have saved. Going live is removing the two flags: same URL, same
key, same request body.
A trial measures; it does not speed anything up. A turn the router would have
answered from cache goes to your model instead, so trial latency reflects your own
model. Send your prompt in template form with
template_variables for the
measurement to be meaningful.