Core concepts
Routers
A named policy over your model names, when one source is not enough.
A model name can point at several sources. A router is the policy that decides which one a call gets, kept as its own named object so the decision is reviewable and can change without touching a client.
Order of setup: create the target aliases, allow them on a bundle, then create the router. Creating a router fails if no bundle already allows every target.
Create and inspect
seams routers add balanced --strategy round_robin \
--target acme/fast --target acme/smart
# router balanced
# strategy round_robin
# targets acme/fast, acme/smart
# attached to every bundle that allows both targets
seams routers show balanced
seams routers listimport { Seams } from "@ourseams/sdk"
const seams = new Seams({ apiKey: process.env.SEAMS_API_KEY! })
await seams.routers.add("balanced", {
strategy: "round_robin",
targets: [{ model: "acme/fast" }, { model: "acme/smart" }],
})
await seams.routers.show("balanced")
await seams.routers.list()import os
from seams import Seams, models
seams = Seams(api_key=os.environ["SEAMS_API_KEY"])
seams.routers.add(
"balanced",
strategy="round_robin",
targets=[
models.AddRouterTarget(model="acme/fast"),
models.AddRouterTarget(model="acme/smart"),
],
)
seams.routers.show("balanced")
seams.routers.list()How it attaches to a bundle
You do not run bundles allow with the router slug. Allow the target aliases on the bundle first. When you create (or replace) the router, Seams attaches it to every bundle that already allows all of those targets. A key minted under that bundle may then call the router slug or any allowed alias.
If you later allow the same targets on another bundle, create or replace the router again so it attaches there too.
Strategies
| Strategy | Picks |
|---|---|
cheapest | The lowest priced target your bundle can serve. The default |
round_robin | Walks targets in order, wrapping; cursor is process-local |
random | A uniform random target among those that can serve the call |
All three skip a target that cannot serve the call, no tool support when the call carries tools, no vision when it carries an image, a context window too short for the prompt. A router whose every target fails that check returns no_capable_model rather than picking one that would fail.
Order matters for round_robin. --target is repeatable and the order you pass is the walk order.
What a caller sees
They call the router by its slug, the same way they call any other model name. The response still says that name. Which target answered stays on the request record you see in the console and the request log. Targets can be different providers; the inbound client does not care.
const res = await client.chat.completions.create({
model: "balanced",
messages: [{ role: "user", content: "Say hello" }],
})
// caller still sees model: "balanced"const res = await client.messages.create({
model: "balanced",
max_tokens: 1024,
messages: [{ role: "user", content: "Say hello" }],
})const res = await client.models.generateContent({
model: "balanced",
contents: "Say hello",
})curl https://<app>.gw.ourseams.com/v1/chat/completions \
-H "Authorization: Bearer ak_acme_…" \
-H "Content-Type: application/json" \
-d '{
"model": "balanced",
"messages": [{ "role": "user", "content": "Say hello" }]
}'
# route balanced · round_robin
# route picked openai/gpt-5 (or anthropic/… or google/…)
# 200 caller still sees model: "balanced"Remove
seams routers rm balanced --yesawait seams.routers.rm("balanced")seams.routers.rm("balanced")One target per call
A router picks one target and the call goes there. If that provider returns an error, the error reaches your caller. The router does not walk on to the next target, and it does not send the same call to several providers to compare answers. Handle a failed call the way you handle any other error from a model provider: read the code on the response and decide.
Cross-protocol examples (OpenAI client against an Anthropic target, and the reverse) are on Calling the gateway.