- Calls: A first call · Models and keys · Parameters · Streaming · Async · Tools · The Responses API · The Messages API · Embeddings
- What comes back: What a call returns · Errors · Cost and model details (adding models) · Streams put back together · Callbacks · Counting tokens · Tests
- Gateways: Gateways · A gateway you set · The local gateway · Routes · Running the same gateway for a team · Settings
import theagentrouter
resp = theagentrouter.completion(
model="anthropic/claude-sonnet-5-5",
messages=[{"role": "user", "content": "Summarise yesterday's refunds"}],
)
print(resp.choices[0].message.content)completion takes the same arguments as litellm.completion and returns the same response shape.
The SDK has four kinds of call, each as in LiteLLM:
| Call | For | Section |
|---|---|---|
completion, acompletion |
Chat with any model: OpenAI Chat Completions, and Anthropic Messages for Claude | Streaming, Tools |
responses, aresponses |
OpenAI's Responses API, for OpenAI models | The Responses API |
anthropic.messages.create, acreate |
Anthropic's Messages API, for Claude | The Messages API |
embedding, aembedding |
Vectors | Embeddings |
A model name is provider/model, such as openai/gpt-5.4-mini. A name with no provider works where it works in LiteLLM: OpenAI and Anthropic names (gpt-5.4-mini, claude-haiku-5-5). Other names need the provider, as in LiteLLM (mistral/mistral-large-latest, groq/llama-3.1-8b-instant). A bare gemini-… name is refused with BadRequestError: LiteLLM sends it to Google Cloud's Vertex AI, which the local gateway does not serve yet, so write gemini/gemini-2.5-flash for the Gemini API. Every gateway gets the name as you wrote it, with or without the provider, and a gateway you set can have names of its own, such as claude-sonnet-5.
Every call goes through a gateway: the local gateway (Agent Router on this computer), or one you set. The local gateway reads each provider's key from the environment when it starts:
| Model name starts with | Key |
|---|---|
openai/ |
OPENAI_API_KEY |
anthropic/ |
ANTHROPIC_API_KEY |
gemini/ |
GEMINI_API_KEY |
groq/ |
GROQ_API_KEY |
mistral/ |
MISTRAL_API_KEY |
deepseek/ |
DEEPSEEK_API_KEY |
together/ |
TOGETHER_API_KEY |
bedrock/ |
AWS_PROFILE, or AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY; and AWS_REGION |
azure/ |
AZURE_API_KEY and AZURE_API_BASE (your resource's endpoint); AZURE_API_VERSION if you need a version other than 2024-10-21 |
For Azure OpenAI, the name after azure/ is your deployment's name: azure/gpt-5-mini calls the deployment gpt-5-mini. As with Bedrock, the bundled model list has none of these names, so each new one restarts a running gateway with a rule for it. To avoid that, add them with register_model first (see Adding models).
api_key= on a call is used in two cases: with api_base=, as the key for that endpoint, and with a gateway you set, as that gateway's key. With the local gateway it has no effect and warns once, because the local gateway reads provider keys from the environment when it starts.
To send one call to an endpoint you name, in the provider's own format, use api_base= (LiteLLM's argument), for example a local test server. The call goes there in place of the gateway, as in LiteLLM, so routes, fallbacks and limits apply only if that address is an Agent Router gateway that has them; the SDK warns once for each address.
For OpenAI Chat Completions, these go to the provider as they are:
temperature, top_p, n, stop, max_tokens, max_completion_tokens, presence_penalty, frequency_penalty, logit_bias, logprobs, top_logprobs, seed, response_format, tool_choice, parallel_tool_calls, reasoning_effort, user, modalities, prediction, audio, service_tier.
For Claude, which goes to the gateway in Anthropic's Messages format (so prompt caching and thinking keep working), the SDK translates temperature, top_p, top_k, stop, max_tokens, max_completion_tokens, tool_choice, parallel_tool_calls and user. parallel_tool_calls=False allows at most one tool call. As in LiteLLM, user is left out when it is an email address or a phone number, which Anthropic refuses, tools also takes Anthropic's own shape and its hosted tools, and n=1 is fine. A JSON schema in response_format maps to the Messages output format, changed as in LiteLLM: every object closes with additionalProperties: false, oneOf becomes anyOf, and what Anthropic refuses, such as maximum, moves into the description. response_format also takes a pydantic class, for every provider. Claude has no JSON mode without a schema, so a plain json_object is not applied: the call goes ahead without it, and the SDK warns once.
To turn on thinking, use reasoning_effort="low" (or another level), or LiteLLM's thinking={"type": "enabled", "budget_tokens": 1024}. What reaches Claude depends on the model, by LiteLLM's rules:
| You pass | Before Opus 4.6 and Sonnet 4.6 | Opus 4.6 and Sonnet 4.6 | Opus 4.7 and newer |
|---|---|---|---|
reasoning_effort |
A token budget: 1024 for minimal and low, 2048 for medium, 4096 for high, 8192 for xhigh, 16384 for max |
Adaptive thinking at that level. minimal is low, and xhigh raises BadRequestError |
Adaptive thinking at that level. minimal is low |
thinking with budget_tokens |
As written | As written | Adaptive thinking, at the level that fits the budget |
thinking={"type": "adaptive"} |
A budget of 2048, below max_tokens. With max_tokens of 1024 or less, no thinking |
As written | As written |
thinking={"type": "disabled"} |
As written | As written | As written, but left out on Sonnet 5.5, Opus 5.5, Fable 5 and Fable 5.1, which always think |
In adaptive thinking, the model decides how much to think. thinking wins over reasoning_effort, which then only sets the level on a model that has levels. thinking=True is the medium setting. For a Claude name the SDK does not know, the version in the name decides which column applies, and Anthropic answers for the rest.
With reasoning_effort and a max_tokens at or below the token budget, LiteLLM sends the call and Anthropic refuses it. The SDK uses a smaller budget instead, and raises BadRequestError if max_tokens is 1024 or less.
Without max_tokens, Claude gets the model's own limit from the model list, or 4096 for a name not on the list. With a token budget, it gets the budget plus 4096. If you pass both max_tokens and max_completion_tokens, max_completion_tokens wins.
Some options raise UnsupportedParamsError; with drop_params=True (or theagentrouter.drop_params = True, which holds whatever a call says) they are dropped instead. additional_drop_params=["seed"] drops the options you name:
- OpenAI-only options such as
seed, on every Claude model. temperatureother than 1,top_pandtop_k, on Opus 4.7 and every newer Claude model (Opus 4.8, Sonnet 5, Sonnet 5.5, Haiku 5.5, Opus 5, Opus 5.5, Fable 5, Fable 5.1).- A forced tool call (
tool_choice="required"or a named tool), on Sonnet 5.5, Opus 5.5 and Fable 5.1. Withdrop_params=Truethe call usestool_choice="auto", and the SDK warns once.
A gateway you set decides for itself: without drop_params, the last two go to it, because its model names are its own.
Claude on Bedrock follows the same rules, by the name after bedrock/; an inference profile's ARN counts as a model before adaptive thinking unless it names a newer Claude. Signed thinking goes back to Bedrock with a tool result, as it does to Anthropic. On Bedrock models, tool_choice="none" raises UnsupportedParamsError, as do seed, n above 1, presence_penalty, frequency_penalty, logit_bias, logprobs and top_logprobs; with drop_params=True they are dropped. Cohere's models take all of these except logit_bias, logprobs and top_logprobs, and a gateway you set gets them all except tool_choice="none". The gateway has no field for four things, so they do not reach Bedrock, and the SDK warns once for each: an effort level (Claude uses its default), top_k, parallel_tool_calls=False and response_format.
Use extra_body for other native Anthropic options. Prompt caching takes LiteLLM's cache_control markers: on a content part, on a whole message, or on a tool; cache reads show in usage.prompt_tokens_details.cached_tokens. With thinking on, the reply carries reasoning_content and thinking_blocks (streamed: delta.reasoning_content); send the assistant message back as it is with the tool result, since Anthropic needs its signed thinking blocks. Model capability lookups describe the bundled model catalog; they do not promise that this SDK translates every native option.
These control the call itself:
| Parameter | What it does |
|---|---|
api_key |
The key for api_base, or for a gateway you set. Ignored, with a warning, for the local gateway |
api_base |
Send this call to this address instead of the gateway, in the provider's own format. Warns once per address |
timeout |
Seconds before giving up; the default is 600, as in LiteLLM |
extra_headers |
More HTTP headers on the request |
extra_body |
More fields in the request body, for provider-specific options |
mock_response |
Answer on this computer without calling anything, for tests (Tests) |
num_retries (or max_retries) sets bounded request retries for connection failures, timeouts, 408, 429 and 5xx. It defaults to zero; authentication and budget errors are not retried. Streams, completion and native Messages alike, retry only before yielding their first chunk. A route's Retry is separate and runs in the gateway, so enabling both can multiply attempts. Other unknown completion arguments warn and are not sent; see Coming from LiteLLM.
for chunk in theagentrouter.completion(model="openai/gpt-5.4-mini", messages=messages, stream=True):
print(chunk.choices[0].delta.content or "", end="")Every chunk has a choice, as in LiteLLM. The last chunk, the one with the finish reason, carries usage with the token counts; with stream_options={"include_usage": True} the usage comes instead on one more chunk with an empty choice, as LiteLLM sends it. Provider error events raise an SDK exception, including errors that arrive after partial output.
resp = await theagentrouter.acompletion(model="openai/gpt-5.4-mini", messages=messages)
stream = await theagentrouter.acompletion(model="openai/gpt-5.4-mini", messages=messages, stream=True)
async for chunk in stream:
print(chunk.choices[0].delta.content or "", end="")Tools use the OpenAI format for every provider, Anthropic included:
import json
tools = [{
"type": "function",
"function": {
"name": "search_refunds",
"description": "Find refunds for a day",
"parameters": {"type": "object", "properties": {"day": {"type": "string"}}, "required": ["day"]},
},
}]
messages = [{"role": "user", "content": "How much was refunded yesterday?"}]
resp = theagentrouter.completion(model="anthropic/claude-sonnet-5-5", messages=messages, tools=tools)
call = resp.choices[0].message.tool_calls[0]
args = json.loads(call.function.arguments)
messages.append(resp.choices[0].message.to_dict())
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps({"total_usd": 42})})
final = theagentrouter.completion(model="anthropic/claude-sonnet-5-5", messages=messages, tools=tools)
print(final.choices[0].message.content)resp = theagentrouter.responses(input="Summarise yesterday's refunds", model="openai/gpt-6-luna", max_output_tokens=200)
resp.output_text # the text of the output messages
resp.output[-1].content[0].text # the reply as OpenAI sends it, by attribute or key; reasoning may come first
resp.usage.input_tokens
resp._hidden_params["response_cost"]LiteLLM's responses and aresponses, through the gateway's /v1/responses. Agent Router serves the Responses API for OpenAI models, so other providers are refused with BadRequestError; use completion for them. instructions, tools, tool_choice, max_output_tokens, temperature, reasoning, previous_response_id and the other Responses arguments are passed on. Streaming is not supported yet.
resp = theagentrouter.anthropic.messages.create(
model="anthropic/claude-sonnet-5-5",
max_tokens=1024,
system=[{"type": "text", "text": policy, "cache_control": {"type": "ephemeral"}}],
thinking={"type": "adaptive"},
messages=[{"role": "user", "content": "Can I get a refund after 40 days?"}],
)
resp["content"] # thinking and text blocks, as Anthropic sends them
resp["usage"]["cache_read_input_tokens"]
resp._hidden_params["response_cost"]
for event in theagentrouter.anthropic.messages.create(..., stream=True):
print(event) # b'event: content_block_delta\ndata: {...}\n\n'LiteLLM's anthropic.messages.create and acreate (also anthropic_messages, the async one), through the gateway's /anthropic/v1/messages. The request and the reply are Anthropic's own format, so every Messages feature works: prompt caching, thinking, tools, citations and anthropic-beta headers (in extra_headers). As in LiteLLM, what a model refuses is changed first. thinking and reasoning_effort follow the table in Parameters, and output_config.effort becomes a token budget before Opus 4.6 and Sonnet 4.6. Opus 4.5 keeps the effort level and gets no adaptive thinking. Sampling options raise UnsupportedParamsError from Opus 4.7 on, unless drop_params=True. Before 4.6, a temperature other than 1 is dropped while thinking is on. metadata keeps only user_id. speed, compaction, context_management, output_format, output_config.format and safeguards get the anthropic-beta header they need, beside any of yours. The reply is a dict, as in LiteLLM, that also reads by attribute (resp.content[0].text). With stream=True you get the server-sent events as bytes, as in LiteLLM.
Agent Router serves the Messages API for Claude, so other providers are refused with BadRequestError; use completion for them. Routes are refused too, because the local gateway answers a route in the OpenAI format; use completion with a route. completion also sends Claude in the Messages format, and translates it to and from OpenAI's shape; use anthropic.messages.create when your code already speaks Anthropic's format.
resp = theagentrouter.embedding(model="openai/text-embedding-3-small", input=["refunds", "returns"])
resp.data[0]["embedding"] # the first vector; resp.data[0].embedding works too
resp.usage.prompt_tokens
resp._hidden_params["response_cost"] # US dollars, from the model listaembedding is the async version. dimensions, encoding_format and user are passed to the provider. Embeddings go where a completion for the same model goes: through the local gateway or one you set. OpenAI, Gemini, Mistral, Together and Bedrock's Titan models have embeddings; Anthropic has no embeddings API.
What completion returns reads like LiteLLM's, both as attributes and as a dict:
resp.choices[0].message.content
resp["choices"][0]["message"]["content"]
resp.usage.total_tokens
resp.to_dict()model_dump(mode="json", exclude_none=True) works too: it leaves out optional fields that are None, and keeps null values inside payload dictionaries and lists. Serialization differences when migrating.
resp._hidden_params says where the call went and what it cost, as LiteLLM's does:
| Key | Example |
|---|---|
route |
local gateway, gateway (one you set) or api_base (an endpoint named on the call) |
sent_to |
http://127.0.0.1:52413/anthropic/v1/messages: the address the SDK sent the request to, here the local gateway |
llm_provider |
anthropic |
model_sent |
anthropic/claude-sonnet-5-5: the model name in the request, as you wrote it. The gateway passes the provider its own name |
response_cost |
0.000123, in US dollars, from the model list. completion_cost(resp) gives the same. None when the model has no known price, such as a route name on a gateway you set, as in LiteLLM; completion_cost(resp) then warns and returns 0 |
served_by, route_name |
For a route: the model that answered, and the route's name |
Errors keep LiteLLM's names and carry status_code, model and llm_provider:
from theagentrouter import exceptions
try:
theagentrouter.completion(model="openai/gpt-5.4-mini", messages=messages)
except exceptions.ContextWindowExceededError:
...
except exceptions.RateLimitError:
...| Error | When |
|---|---|
UnsupportedParamsError |
An option cannot be translated to the selected request format, or the model refuses it |
BadRequestError |
The request is wrong, for example a model name the SDK cannot place with a provider (acme-model), or a bare gemini-… name |
ContextWindowExceededError |
The messages are too long for the model |
ContentPolicyViolationError |
The provider refused the content |
AuthenticationError |
The key is missing or wrong |
PermissionDeniedError |
The key cannot use this model |
NotFoundError |
The model does not exist |
RateLimitError |
Too many requests |
BudgetExceededError |
A gateway's budget is used up. It is a kind of RateLimitError |
Timeout |
No answer within timeout |
APIConnectionError |
The provider or gateway could not be reached, or the local gateway could not start (the message says why) |
InternalServerError, BadGatewayError, ServiceUnavailableError |
The provider or gateway failed |
theagentrouter.completion_cost(resp) # US dollars for this response
theagentrouter.get_model_info("openai/gpt-5.4-mini") # context window, prices, features
theagentrouter.supports_function_calling("anthropic/claude-sonnet-5-5")
theagentrouter.model_cost["openai/gpt-5.4-mini"] # the raw entry, as in litellm.model_costThese read the model list that ships with each release (theagentrouter/agent-router-models): OpenAI, Anthropic, Gemini and Groq models, and DeepInfra and xAI models for prices only. It has no Bedrock, Mistral, DeepSeek or Together models. max_tokens and max_output_tokens are the output limit where the list has one, and None where it has not. For OpenAI models without an explicit input limit, max_input_tokens is the context window less the output limit, since OpenAI's window includes the output. The SDK never downloads the list while it runs. get_model_info raises NotFoundError for a model that is not on the list.
The list also marks models deprecated. A model it still offers has a deprecation_date (the date it retires, LiteLLM's name for it), and the first call to it in a program warns with the list's reason, such as anthropic/claude-sonnet-4-5 is deprecated and retires on 2026-11-30: replaced by claude-sonnet-5-5. A model it no longer offers is not served by the local gateway: a call raises NotFoundError with the reason, such as deepinfra/Qwen/Qwen3-Max is no longer offered: Retired on DeepInfra on 2026-10-10. Replacement: Qwen/Qwen3.8-Max. A gateway you set decides for itself.
register_model adds models or changes their details, in LiteLLM's format, as litellm.register_model does:
theagentrouter.register_model({
"ft:gpt-5.4-mini:acme": {"litellm_provider": "openai", "mode": "chat", "input_cost_per_token": 3e-07, "output_cost_per_token": 1.2e-06},
})
# Or LiteLLM's whole list, about 4,000 models from every provider:
theagentrouter.register_model("https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json")
# Or a newer copy of the list this SDK ships, one file per provider:
theagentrouter.register_model("https://raw.githubusercontent.com/theagentrouter/agent-router-models/main/openai.json")A registered model gets prices and details, a rule in the local gateway when it next starts, and a rule in local_gateway.export_config(). Registrations last for the program that makes them.
theagentrouter.stream_chunk_builder(chunks) turns the chunks of a streamed call back into one response, as LiteLLM's does: each choice on its own, with text and reasoning joined, tool calls joined by their index, thinking blocks kept, the last finish reason, and the last usage.
Code that sets LiteLLM's callback lists keeps working through theagentrouter.compat.litellm:
from theagentrouter.compat import litellm
def log(kwargs, response, start_time, end_time):
print(kwargs["model"], kwargs["response_cost"], response.usage.total_tokens)
litellm.success_callback = [log] # also failure_callback, and callbacks for logger objectsA function is called as fn(kwargs, response, start_time, end_time); a logger object gets log_success_event and log_failure_event, or their async_ versions under acompletion. They run in your program after each completion, after a stream ends (with the response put back together), and on errors. A callback that fails warns and does not stop the call. LiteLLM's built-in integrations, named by string ("langfuse"), are not here: the gateway's OpenTelemetry traces and metrics record calls.
theagentrouter.token_counter(model="anthropic/claude-sonnet-5-5", messages=messages, tools=tools)
theagentrouter.token_counter(model="openai/gpt-5.4-mini", text="Summarise yesterday's refunds")LiteLLM's arguments. For OpenAI and Anthropic models, with or without the provider in the name, the gateway counts: Agent Router asks the provider (/responses/input_tokens, /messages/count_tokens), so the number is what the provider will charge for. Every other provider, a name the SDK cannot place with a provider, and a count the gateway cannot give are estimated on this computer the way LiteLLM does: its rules for messages and tools, with tiktoken. Install it with pip install theagentrouter[tokens]; without it the estimate is about four characters a token, and the SDK warns once.
The gateway's count is a request each time. Code that counts before every turn can set AGENTROUTER_TOKEN_COUNT=estimate to always estimate, with no request, as LiteLLM does. litellm.disable_token_counter = True makes it return 0, as in LiteLLM.
mock_response works as in LiteLLM: the call is answered on your computer, so no gateway starts and no provider is called.
resp = theagentrouter.completion(model="openai/gpt-5.4-mini", messages=messages, mock_response="Offline reply")
resp.choices[0].message.content # "Offline reply"; usage is 10 prompt and 20 completion tokens
theagentrouter.completion(model="openai/gpt-5.4-mini", messages=messages, mock_response="litellm.RateLimitError") # raises RateLimitErrorIt also takes an exception to raise, a dict or ModelResponse as the whole response, stream=True (the text comes three characters at a time, with usage on the last chunk) and acompletion. mock_tool_calls adds tool calls, mock_timeout=True with a timeout raises Timeout, and mock_delay waits first. Callbacks run as for any call. To test the request the SDK sends, use a test server with api_base instead.
The SDK never calls a provider itself. A call goes to the first of these that applies:
api_baseon the call. That endpoint, for this call only, in the provider's own format (LiteLLM's argument).- A gateway you set. With
AGENTROUTER_BASE_URL, every call goes there. - The local gateway. Agent Router on this computer, started by the first call (
AGENTROUTER_AUTOSTART=0turns that off). If the local gateway is not installed, as on Windows, the call fails with a message that says to setAGENTROUTER_BASE_URL.
A gateway you set and the local gateway get the same requests, so code that works with one works with the other:
| Call | Path on the gateway | Format |
|---|---|---|
completion to Claude (anthropic/… or claude-…) |
/anthropic/v1/messages |
Anthropic Messages, so prompt caching and thinking work |
completion to any other model |
/v1/chat/completions |
OpenAI Chat Completions |
embedding |
/v1/embeddings |
OpenAI |
responses |
/v1/responses |
OpenAI Responses |
anthropic.messages.create |
/anthropic/v1/messages |
Anthropic Messages, as you wrote it |
token_counter |
/anthropic/v1/messages/count_tokens for Claude, /v1/responses/input_tokens for OpenAI |
The model name is sent as you wrote it: anthropic/claude-sonnet-5-5 or claude-sonnet-5-5. The gateway passes the provider its own name.
A remote gateway: an Agent Router your team deploys, or your company's Agent Router Enterprise.
export AGENTROUTER_BASE_URL=https://gateway.acme.example/v1
export AGENTROUTER_API_KEY=... # the gateway's key, not a provider'sSet the gateway's address with or without the /v1 that OpenAI clients add; the SDK adds the paths above. Every call goes there and the local gateway never starts. Provider keys stay on the gateway, so this computer needs none. The key goes as Authorization: Bearer on OpenAI-format calls and as x-api-key on Claude calls, as Anthropic's own clients send it. Routes defined in code set up the local gateway; that gateway uses its own routes of the same name: see Routes and a gateway you set.
It comes with pip install theagentrouter on macOS 13 or newer on Apple chips and on Linux with glibc 2.36 or newer (not with theagentrouter-core, the SDK alone). There is nothing else to run. The first call starts Agent Router's aigw on a free port chosen at start, answering only programs on this computer, and says so once:
theagentrouter: started the local gateway on http://127.0.0.1:52413 (providers from your environment: anthropic, bedrock, openai).
The port changes from start to start, so another Agent Router gateway on 1975 (your own aigw run, for example) never blocks it. Every program using the SDK finds the running gateway by itself; to reach it from another tool, read the port from that line or from port in ~/.agentrouter/local-gateway/state.json. Two Agent Router gateways still cannot run on one computer: aigw keeps two internal ports fixed (1061 and 18000), so with another Agent Router gateway running the SDK says so and asks you to stop it or set AGENTROUTER_BASE_URL.
- The SDK sets the gateway up from the provider keys in your environment. The gateway holds the keys: each is in a private file it reads (mode 0600), never in its config file.
- It starts with a rule for every model in the SDK's bundled model list for those providers. A model name without a rule restarts a running gateway with one, as LiteLLM sends any name to the provider. Register custom models with
register_modelbefore the gateway starts to avoid the restart. AGENTROUTER_AUTOSTART=0stops the SDK from starting it.- A gateway set with
AGENTROUTER_BASE_URLalways wins, and then the SDK starts nothing. - Its files, including its log
aigw.log, are in~/.agentrouter/local-gateway/. - The very first start downloads Envoy (about 100 MB, into
~/.local/share/aigw), so it needs the internet and can take a minute. - Adding a model or provider needs the keys of every provider already running; without them the SDK raises and leaves the gateway running.
- It keeps running after your program ends. To stop it:
python -c "from theagentrouter import local_gateway; local_gateway.stop()". The next call starts it again. - To load a new SDK version or a rotated provider key, stop it when other programs have finished; the next call starts it again.
The local gateway is for development. Its known limits, such as a new route, model or provider restarting it and interrupting other programs, are listed in How the local gateway works.
| Provider | Set in the environment |
|---|---|
| AWS Bedrock | AWS_PROFILE, or AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY; and AWS_REGION |
| Azure OpenAI | AZURE_API_KEY and AZURE_API_BASE; optionally AZURE_API_VERSION |
| OpenAI, Anthropic, Gemini, Groq, Mistral, DeepSeek, Together | The key from Models and keys |
A route is a model name of your own with the models behind it. The local gateway serves routes.
import theagentrouter as ar
ar.route(
"chat",
primary="openai/gpt-5.4",
fallbacks=["anthropic/claude-sonnet-5-5"],
retry=ar.Retry(attempts=2, on=(429, 500, 502, 503, 504)),
)
resp = ar.completion(model="chat", messages=messages)
resp._hidden_params["served_by"] # the model that answered
resp._hidden_params["route_name"] # "chat"The local gateway does the retries and the fallbacks, as Agent Router Enterprise does: the SDK sends one request, named after the route.
- Retry: the gateway tries each tier
attemptsmore times on the listed status codes and on connection failures, withbackoffseconds before the first retry, doubling up to 10 seconds, and an optional per-trytimeout. - Fallback: when a tier still fails with one of those codes, or cannot be reached, the gateway moves to the next tier. A stream falls back only before its reply starts.
- Sharing:
primary=["openai/gpt-5.4", "openai/gpt-5.4-mini"]puts two models in one tier, which share the traffic equally. A fallback can be a list too. LiteLLM'sRoutershares a group byweight,rpmortpm, as LiteLLM does: see Coming from LiteLLM. served_byis read from the model name in the reply, so it is missing when a provider answers with a name that does not start with the model's own.- Routes are kept in
~/.agentrouter/routes.jsonand shared by every program on the computer, like the local gateway. Defining a route again replaces it;ar.list_routes()andar.remove_route(name)list and remove them. - A route can take a model's name, such as
openai/gpt-5.4orgpt-5.4, as a group in the LiteLLM proxy's model list can. Every program's calls to that model then go through the route. - Define routes without local provider keys or a companion package when exporting configuration. The machine that starts the gateway needs credentials for every provider in the saved routes.
- A route sends one request, and any of its models may get it. In a route of OpenAI or Azure with Anthropic, Gemini, Groq or Bedrock models,
max_tokensgoes asmax_completion_tokens, which they all take. Any other route sends what you wrote. If an OpenAI model refusesmax_tokens, the SDK makes the call once more withmax_completion_tokens. The gateway removestemperatureandtop_pbefore a request reaches a newer Claude model on Anthropic or Bedrock (see Parameters), and an effort level before it reaches Claude on Bedrock. thinkingandtop_khave no field in the OpenAI format, so they are not sent in a route, and the SDK warns once.extra_bodyreaches every model in the route.- A route reaches Anthropic through its OpenAI-compatible endpoint, which has its own rules: a JSON schema in
response_formatneeds"strict": true, and prompt caching does not apply. A forced tool call is still refused on Sonnet 5.5, Opus 5.5 and Fable 5.1, even withdrop_params. - Limit:
limit=ar.Limit(60, per="minute")lets at most 60 requests a minute through the route; the gateway answers the rest with 429 (RateLimitError).peris"second","minute","hour"or"day". The count covers every program on the computer that uses the route. It counts requests, not tokens (see Known limits).
LiteLLM's Router works on top of routes: see Coming from LiteLLM.
local_gateway.export_config() gives the config the SDK writes for the local gateway, to run on a server. Developers then set AGENTROUTER_BASE_URL to that server, and the same code works there.
from pathlib import Path
from theagentrouter import local_gateway
Path("config.yaml").write_text(local_gateway.export_config(port=1975))aigw is the program inside the local gateway package, and it is not on your PATH. On a server, pip install theagentrouter-local-gateway and find it as below, or download it from Agent Router's releases.
export AGENTROUTER_API_KEY=... # the server client key; set it on clients too
AIGW=$(python -c "from theagentrouter_local_gateway import binary_path; print(binary_path())")
ENVOY_VERSION=1.39.4 "$AIGW" run config.yaml # provider keys come from the server environmentWhat the file holds:
- Providers: those whose keys are set on your computer, or the ones you name:
export_config(providers=["openai", "anthropic"]). - Models: a rule for every model in the SDK's bundled model list for those providers, with and without the provider in the name, plus any you added with
register_model. The server serves only these. The bundled list has no Bedrock, Mistral, DeepSeek or Together models, so for those providers name the models (export_config(models=["mistral/mistral-large-latest"])) or register LiteLLM's list first (see Adding models). Export again when the models change. - Routes: the routes defined on your computer, with their fallbacks, retries and limits, under the names code calls them by: a LiteLLM
Routergroup's route is named after the group,chat, notrouter-1a2b3c4d:chat. Two routes that would get the same name raiseBadRequestError; remove one withar.remove_route(). A client cannot see which models are behind a route's name, somax_tokensgoes as written. If an OpenAI model refuses it, the SDK makes the call once more withmax_completion_tokens. Passmax_completion_tokensyourself to save that extra request. A route name shaped like a Claude model, such asclaude-chat-5, goes to the gateway as Claude, so use another name for a route. - No key values: only references such as
${OPENAI_API_KEY}, whichaigwfills in from the server's environment.
Exported configurations require a client key by default. Set AGENTROUTER_API_KEY on the server and on each client. The gateway accepts Authorization: Bearer <key> or x-api-key: <key>, refuses a missing or wrong key with 403, and removes the client key before forwarding. A server started without AGENTROUTER_API_KEY refuses every request. The SDK raises that refusal as PermissionDeniedError, and its message names AGENTROUTER_API_KEY on both sides. export_config(authentication=False) explicitly exports an open endpoint. Terminate TLS in front of the server when callers use an untrusted network.
Routes defined in code set up the local gateway only. With AGENTROUTER_BASE_URL set, the same code runs unchanged:
route()and LiteLLM'sRouterset up nothing on this computer and write nothing toroutes.json.- A call to a route sends its name to that gateway, and the SDK warns once. That gateway's own route of that name, with its fallbacks, retries and limit, applies. A
Routersends the group name,chat. - A gateway made with
export_config()has this computer's routes under those names, so code tested against the local gateway calls the same routes there.
| Setting | What it does |
|---|---|
AGENTROUTER_BASE_URL |
Send every call through this gateway instead of the local one |
AGENTROUTER_API_KEY |
The gateway's key |
AGENTROUTER_HOME |
Folder for the SDK's files (default ~/.agentrouter) |
AGENTROUTER_AUTOSTART |
0 stops the SDK from starting the local gateway |
AGENTROUTER_TOKEN_COUNT |
estimate makes token_counter estimate on this computer instead of asking the gateway |
AGENTROUTER_BASE_URL, AGENTROUTER_API_KEY and AGENTROUTER_TOKEN_COUNT can also be kept in AGENTROUTER_HOME/config.json, as base_url, api_key and token_count. Environment variables win over the file. When AGENTROUTER_BASE_URL names a different gateway from the file, the file's key is not used, so it never goes to a gateway it was not made for.
To check that the keys a model needs are set, use LiteLLM's validate_environment(model). For the local gateway it checks what the gateway needs for that provider (for Bedrock: a profile, or a key and secret, and a region); with a gateway set, it checks AGENTROUTER_API_KEY.