think role. It sits
between your agent and your reasoning model. On every turn it decides whether it
has answered this turn before: if it has, it replies from its cache; if not, it
calls your model and answers as normal. You keep your model, your provider,
and your bill for the turns that reach it, and gain speed on the turns that
repeat.
This is the one SLNG optimization you opt into. Listening and speaking are
already optimised behind their bindings, as
Execution Layer explains. Thinking is not,
until you make this change.
On this page:
- Start here - three edits to one binding
- Every key a router binding takes - the full shape
- Reading the router’s log line - how to see a hit
- How it decides - what it treats as a repeat
- One stable
agent_id, one scope per prompt - naming your cache - World parts - the 13 places the router serves you from
- Your model, and who serves it - the
upstreamblock - Params ride the request body - what is forwarded
- Advanced - prompt text, effort, and personal prompts
- Troubleshooting - when a repeat never caches
Start here
Three edits, all to one binding:1
Declare the key
Add
SLNG_API_KEY to secrets:. Your own model’s key stays where it is.2
Point think at slng
Change the
think binding to provider: slng, keeping the model you already
use.3
Name the cache and the model behind it
Add an
agent_id, which names your project’s cache, and an upstream, which
says where your model actually lives. One agent_id for the package; the
compiler gives each agent and task its own scope under it.agent.yaml
Every key a router binding takes
slng
required
What points the
think binding at the router instead of straight at a vendor.model id
required
The model you already use. The router calls it through
upstream.string
required
Names your project’s cache. One value for the package, and compiling fails if
two think profiles disagree about it. The compiler gives each agent and task
its own scope under it.
block
required
Where the router actually calls your model. There is no default, because your
provider’s credentials travel with the request.
string
Literal text appended to every system prompt this binding sends. See
prompt_suffix.world part
required
Which of the 13 world parts serves this binding. The same key
and the same values a
listen or speak model takes. The compiler turns it
into the base URL, so it never reaches your upstream.block
world_part and slng_pure_proxy are consumed here and everything else rides
the request body. See
Params ride the request body.Reading the router’s log line
Every think request writes one line, at info level, in every run, deployed or local:source=cache means no model ran. Repeat an exchange and watch it change: that
is the check, and it needs no dashboard and no code of yours. layer appears
only on a hit, model only on a live answer, request_id is what to quote to
support.
The line is built from three response headers, which the generated project
reads through a hook on its own HTTP client because neither framework surfaces
them.
Read
x-slng-response-source rather than guessing a hit from how fast the
reply came back. Fast is not proof of a hit. A repeat served by the model is
expected, not a fault: the router decides which turns are worth serving from
cache. A miss just costs a little, an extra hop in front of your own model.
Streamed responses report no usage on either path, so token savings cannot be
read off the stream either.
How it decides
The router remembers pairs: what your agent said last, and what the caller said next. When that same pair comes round again, it can answer from the cache. A first turn never caches. There is no preceding pair yet, so the first turn of every call takes the full path to your model. A conversation with no repeat in it never shows a hit. Not every repeat caches, and that is by design. The router decides which turns are worth treating as repeatable. Two pairs identical in everything a client controls can still behave differently: one served from cache, one sent to the model every time. A repeat served by the model is expected, not a fault.One stable agent_id, one scope per prompt
agent_id names your project’s cache, and everything learned under one scope
is invisible to another. You write one value and own its version suffix:
change a prompt in a way that should make old answers wrong, and bump it,
salon-concierge-v1 to salon-concierge-v2, so the router starts fresh. Fix
a typo and leave it alone, since throwing the cache away buys nothing. The
compiler never derives this value from your prompts or hashes them into it,
because it has no way to know whether a reworded sentence changes what a good
answer looks like. A package sends one agent_id, and compiling fails if two
think profiles disagree about it.
What the compiler does add is the name of whoever is speaking. Each agent and
task sends the agent_id, a colon, then its own name:
agent_id. Bump the id, or start sending one scope
per prompt, and that arrangement has to be redone. The generated runbook prints
the exact list of scopes your package sends.
World parts
params.world_part picks which router serves you. It is the same key, and the
same 13 world parts, that a speech model takes. Learn a world part once and you
write the same word for listening, thinking and speaking.
One word, three roles.
eu-west on a think binding and eu-west on a
listen binding are the same place, so a package can keep a whole call in one
world part. See the speech gateways
for the same table on the speech side.Your model, and who serves it
Theupstream block says where the router actually calls your model. It is
required, and there is no default, because your provider’s credentials travel with
the request and a package has to name whose they are.
The
openai and openai-compat rows have been exercised against the live
router. azure, vertex and bedrock come from the router team’s published
field list and have not been run here, so treat your first call on one of
those as the test.
On openai the compiler supplies the URL and the OPENAI_API_KEY name, so
the block is one line, upstream: {provider: openai}. Any other upstream
writes its own, naming its own secret:
agent.yaml
upstream block only. Everything under params: is
forwarded without checking, which is what makes it useful.
The configuration travels inline, in the request body of every think
request, so your upstream credentials are sent to SLNG. That is a trust
decision, and it is the price of not registering anything anywhere in
advance. What the tooling guarantees is narrower: a credential is always
named, never written. A *_env field holds an environment variable name,
the generated agent reads it with os.environ[...] at run time, and no
package, generated file, or compile report ever contains a value. Every name
you write has to appear in secrets: and joins the generated startup check,
so a missing value stops the agent at boot rather than on the first turn of a
live call.
auth_header is a header name, not a secret, for an openai-compat host
that wants the key somewhere other than Authorization: Bearer; the value
still comes from key_env. A Vertex credentials_env may hold the key JSON
itself, that JSON base64 encoded, or a path to the key file, and the agent
works out which at startup.
Upstream fields
string
required
Accepts
openai, openai-compat, azure, vertex, or bedrock. No provider is
inferred. Fields belonging to another provider are refused.string
The upstream endpoint. Required for
openai-compat and azure; defaults to
https://api.openai.com/v1 for openai. On Azure, use the resource root, not a
deployment URL.string
An UPPER_SNAKE environment variable name. Required for
openai-compat and azure;
defaults to OPENAI_API_KEY for openai.string
Header name for
openai-compat authentication. Omit to use the default bearer
Authorization header; the value still comes from key_env.string
Required for
azure: the deployment name. No name is inferred.string
Required for
azure: its API version string. No version is inferred.string
Required for
vertex: an UPPER_SNAKE environment name holding service-account JSON,
base64 JSON, or a path to its file. No credentials are inferred.string
Required for
vertex: its location name. No location is inferred.string
Vertex project id. Omit to use the project from the service-account key.
string
Required for
bedrock: an UPPER_SNAKE name holding the AWS access key id. No name is
inferred.string
Required for
bedrock: an UPPER_SNAKE name holding the AWS secret access key. No name
is inferred.string
Required for
bedrock: an AWS region name. No region is inferred.string
Required for
bedrock: its model id, which may differ from the model profile’s label.
No id is inferred.string
An UPPER_SNAKE name holding the temporary AWS session token for
bedrock. Omit when the
credentials need no session token.Params ride the request body
Everything underparams: on a router think binding is passthrough. The compiler
reads two names and forwards the rest:
world part
Consumed, and becomes the router’s base URL for that world part. One of the 13
values in the table above, the same set a speech binding takes.
switch
Consumed, and rides the body as the router’s shadow-trial switch.
forwarded
Forwarded, in the request body, on both targets.
provider: {only: ["groq"]} forwards straight
through to OpenRouter.
Trying it on a package you already have
No shipped example binds to the router today.examples/salon-concierge
reaches Google Vertex directly, which makes it a starting point rather than a
finished demonstration. It has several prompt sites, so pointing its think
binding at the router gives you one scope per site to look at.
To see the router’s own contribution on your own agent, compile it once with
the think profile bound directly to the upstream vendor and once bound to the
router, then run the same conversation through both.
Advanced
Three things a first router binding does not need.prompt_suffix, for what no parameter reaches
Some models take instructions that only work as prompt text. prompt_suffix is
literal text the compiler appends to every system prompt that binding sends:
each agent’s, each task’s on that binding’s profile, and the summarizer’s where one
is emitted.
agent.yaml
/no_think directive in the prompt
works across hosts, from the system prompt, mid-prompt, and on a tool turn.
It goes on the model, not on an agent, since it is a fact about the model; an
agent that needs different wording has its own prompt file. It is literal,
never a placeholder. The router substitutes placeholders from a snapshot of
the names your prompts reference, so one arriving from a suffix would be sent
with no value and come back 422 mid-call, which the compiler refuses. It
moves no cache, since scopes come from names and never from prompt content,
so bumping agent_id is still the only way to retire one deliberately. And
the compiler attaches no meaning to it: /no_think is just a string here,
and the next model’s directive works the same way.
You can read what it did in the emitted *_PROMPT constants, and unmute compile names it on its own line.
reasoning_effort, and when it is the problem
If your upstream serves an OpenAI reasoning model and your agent has tools, set
it. The GPT-5 family rejects function tools on chat completions without it, and
every tool turn comes back as:
"none" works. This is the same trap a direct OpenAI think binding has, and
the reasoning model page explains it there too. The compiler
warns when your package has tools and the param is missing, but only for the
openai and azure upstreams, the ones serving the models the advice is
about.
On an openai-compat upstream, ignore all of that, and do not set it
defensively. The advice is for OpenAI’s own model families. Elsewhere the param
ranges from useless to fatal depending on the host: one host answers a request
carrying it with a 400, and another accepts it with a 200 and ignores it. Same
model, same param, two different failures, and neither is a reason to set it.
Personal prompts stay cacheable
A prompt that names the caller is different on every call, which would mean nothing ever repeats. So a router-bound prompt keeps its placeholders, for example{{caller_name}} in an instructions file with a matching
caller_name variable. The router receives the prompt with the placeholder
intact, plus a map of this call’s values, and substitutes them itself. Every
call sends the same prompt, so turns can still repeat. This applies to the
system prompt only; greetings, tool arguments, injected values, and webhook
paths keep rendering locally.
Only variables referenced by the current prompt are sent. Declaring a shape
or saving a task result does not add all call state to a router request.
Dotted references such as {{appointment.date}} select that field alone.
On LiveKit, values are read again for every request. On Pipecat, they refresh
when the call saves state. Direct-provider agent prompts refresh when their
tasks save values, and task prompts render on entry.
An unset referenced value is sent as none recorded yet., never None.
Shapes and lists use compact JSON. A rendered value over 4000 characters is
shortened with a warning. Prefetch has its own smaller input limit; see
Variables.
Which values belong in a placeholder
A placeholder is for a value your agent says: the caller’s name, an appointment detail read back aloud. Supplied that way, the stored copy holds the placeholder instead of the value, so the turn caches like any other and the next caller hears their own name in it. A value that changes what the answer is, like the reply language, does not belong in one, however much it varies per call. Two callers would share one cached answer written in a single language. Write it into the prompt text instead. Variants that must never share answers need their ownagent_id, which is the only thing that partitions the
cache. The compiler does not judge a value by its name, so this is not checked
for you.
The value also has to match the answer character for character to be
recognised and stored. A phone number the model regroups, or a date it
rewrites, leaves the real thing in the stored copy. The personal-data scan then
refuses to share it, and the turn stops caching. There is no error, just a hit
rate of zero. Write the expected shape into the variable’s own description,
and make whatever produces the value follow it. This decides caching only for a
value the agent says out loud. A value passed only to tools can take whatever
shape the rest of your package uses. examples/salon-concierge returns E.164
everywhere and never reads a number back to the caller.
What the router will not cache, whatever you do
Three rules decide this, and none of them is about your placeholders:
A fourth rule, and unlike these three a placeholder beats it: the router
refuses to store an answer holding a number in any script, since a number
in a reply is usually caller-specific. A number the agent says from a
placeholder is not in the stored copy at all, so that answer still caches; a
number the agent renders or reformats itself falls under the rule. So an
agent that quotes times and prices from its own working sees fewer hits than
one that asks questions, and a placeholder is what turns per-call numbers
back into hits.
Troubleshooting
A repeat never comes back from the cache
x-slng-response-source reads llm on a turn you expected to hit.
Fix: check four things. The same agent_id on both calls, a couple of
seconds since the turn it repeats, the second turn or later, and the exact pair
of previous reply and current message. If all four hold, stop looking: some
turns just never cache.
Compiling refuses a scope
The value leaves as an HTTP header, and the bound is 128 characters, counted on the finished scope rather than on theagent_id alone. It refuses rather than
truncating, because two truncated scopes could land on the same string.
Fix: shorten the name the message gives you. It names which agent or task
produced the long value.
The compiler refuses a world_part_override
That key is gone. The router took four names of its own, and now it takes the
same world parts as speech.
Fix: rename the key to params.world_part and write a world part. india
becomes in and indonesia becomes id; eu and us each become a specific
world part, such as eu-west or us-east.
The compiler refuses a world_part
The value is not one of the 13 world parts. The refusal lists
them all.
Fix: write one of the 13. A speech binding on the same package takes the
same words, so copying the value from there works.
Next
Regional infrastructure
How region works across all three SLNG roles, thinking included.
Reasoning model
The
think role and the vendors each target can construct directly.