Router TeamShared

TeamShared AutoRouter

base_urlhttps://router.teamshared.com/v1

Worked example One non-streamed request, end to end. The classification below is run through the same parser, gate and roster that route, so the model named is the one this deployment picks for it.

Request

POST /v1/chat/completions
Authorization: Bearer tsk_…
Content-Type: application/json

{
  "model": "AutoRouter",
  "messages": [
    {"role": "user", "content": "Rewrite this query as a CTE: SELECT …"}
  ]
}
  1. prefilterno rule fired: the turn is not empty, not a probe, not an ack, and the whole body is under 32,000 characters
  2. classify{"difficulty": 3, "task": "sql", "confidence": 0.88, "risk": "low", "needs_tools": false, "output": "short"}
  3. gate0.88 clears the 0.55 confidence floor and risk is low, so the score stands
  4. constrainsql at 3 is claimed by the coder specialist, so deepseek/deepseek-v4.1-flash answers instead of the rung-3 generalist z-ai/glm-5.3-flash; no tools, images or response_format to satisfy, and the input estimate plus the 4,096-token default output budget fits it
  5. completeone upstream call to deepseek/deepseek-v4.1-flash, answered on attempt 1 with finish_reason stop: no fallback, no escalation

Response

HTTP/2 200
X-TeamShared-Difficulty: 3
X-TeamShared-Task: sql
X-TeamShared-Role: coder
X-TeamShared-Assigned-Model: deepseek/deepseek-v4.1-flash
X-TeamShared-Confidence: 0.88
X-TeamShared-Risk: low
X-TeamShared-Output: short
X-TeamShared-Route-Bumped: 0
X-TeamShared-Route-Fallback: 0
X-TeamShared-Upstream-Model: deepseek/deepseek-v4.1-flash
X-TeamShared-Attempts: 1
Server-Timing: validate;dur=…, classify;dur=…, upstream;dur=…

{
  "model": "AutoRouter",
  "choices": [ … ],
  "usage": { … },
  "router": {
    "difficulty": 3,
    "task": "sql",
    "assigned_model": "deepseek/deepseek-v4.1-flash",
    "role": "coder",
    "confidence": 0.88,
    "risk": "low",
    "reason": "classified",
    …
  }
}

Request path

What happens to a request

An OpenAI-compatible POST /v1/chat/completions. Send AutoRouter as the model and the router decides a difficulty from 1 to 5 and a task kind, looks the pair up in a roster of ten models — five generalists, one per rung of the ladder, and five specialists that claim particular tasks at particular rungs — and forwards your original messages to that one model. The id you asked for is the id you get back. Five stages, always in this order: the first three settle on a number and a task, the last two turn them into a model and an answer.

  1. Prefilter

    deterministic · latest user turn only · no model call

    The latest user turn is lowercased, stripped of punctuation and emoji, and whitespace-collapsed, then compared against two short word tables. Earlier turns are ignored on purpose: a thanks after a hard thread must not pay for a scoring hop or a frontier model. Size is measured over the whole request — system prompt, tool schemas, tool-call arguments, tool results, and 340 characters for every image — because the model has to read all of it whatever the user turn says.

    Latest user turnRouteWhat the response says
    Empty, with no system or developer message carrying a taskShort-circuit: a one-word ok completion, no model calledprefilter: "empty"
    X-TeamShared-Attempts: 0
    X-TeamShared-Upstream-Model: prefilter
    ping, healthz, status, ready and six moreShort-circuitprefilter: "health"
    Tool results with no user text and no instructions anywhereShort-circuitprefilter: "tool_ping"
    Whole request over 160,000 charactersRung 4, scoring skipped: a terse follow-up to a huge thread would otherwise score as easyprefilter: "size-floor"
    reason: "large context"
    thanks, ok, lgtm, got it, np and the rest of the acknowledgement tableRung 1, scoring skippedprefilter: "ack"
    difficulty: 1
    Whole request over 32,000 charactersScored as usual, then floored: a score below 3 becomes 3bumped: true
    reason: "large context"
    Anything elseScored—
  2. Classify

    one scoring hop · 2 s budget · cached 3,600 s

    The scored text is the latest user turn — its last 8,000 characters — plus up to 1,500 characters of earlier user turns, most recent first, so a follow-up like now do all of them still reads as one request. Images become the token [image] in the scored text only; what is forwarded upstream is untouched. With no user turn at all, an agent whose task lives in its system prompt is scored on that prompt instead. The classifier is asked for six things and never for a model id:

    {"difficulty": 3, "task": "code", "confidence": 0.8, "risk": "low", "needs_tools": false, "output": "medium"}

    difficulty is scored by the work involved, on the rubric printed with the ladder below. task is one of eleven kinds: chat, lookup, rewrite, translate, code, sql, math, security, research, creative, other. risk is the cost of a too-cheap route — high for security, irreversible, legal or medical work, or when the classifier is guessing. Which model scores is deployment configuration: a general model answering in JSON, a typed decision endpoint that returns a probability distribution over the five levels, or an in-process heuristic that costs nothing and makes no network call.

    A hop that overruns its 2-second budget, fails, or returns something unparseable does not guess. The request snaps to rung 3 with fallback: true and a reason of classifier unreachable or snapped to tier default. Successful scores are cached for 3,600 seconds against a whitespace- and case-insensitive hash of the scored text, 2,048 entries deep, so a retried or regenerated turn routes for free. A snapped fallback is never cached: it is not a decision worth repeating.

  3. Gate

    no network · at most one rung

    An unsure score buys headroom rather than a coin flip. Each of the signals below moves the assignment up one rung, and one rung in total however many of them fire, capped at 5. A rung the prefilter assigned, or a snap to the middle because scoring failed outright, is left where it is. A bump is looked up in the roster again at the new rung, so a specialist that claims the task only at the lower rung gives way to whichever model claims it at the higher one.

    SignalEffectWhat the response says
    confidence below 0.55Up one rungbumped: true
    X-TeamShared-Route-Bumped: 1
    risk of highUp one rungas above
    Where the classifier returns a distribution: more than 0.15 of the probability mass sitting on scores cheaper than the winnerUp one rungas above, plus the distribution itself under classifier_detail
  4. Constrain

    no network · capability walk · size floor · sticky loops

    What the request needs is read from the body, not guessed: tools when a tools list is present and tool_choice is not "none", or when the history already carries tool traffic; images when any content part is one; structured outputs for response_format of type json_schema and JSON mode for json_object; and a context estimate of one token per four characters plus the max_tokens you sent, or 4,096 when you sent none. A rung fits when it has every capability and estimated input plus output is at most 0.9 of its context window.

    Before the walk, the number and the task have already been resolved to one model, in a fixed order: a per-task routing table entry, when one is configured, wins and says so with table_hit: true; otherwise the roster specialist that declares that task at that rung; otherwise the rung's generalist. The response names the row that won with role. The walk starts at that model and moves up the ladder, then down — never to a cheaper model first. A specialist sits beside its rung rather than on it, so one that cannot take the request hands it to its own rung's generalist, which then walks as usual: the math specialist has no image input, so a math question with a chart in it is answered by rung 3's generalist with capability_bump: "images". Where the walk lands is reported as capability_bump, naming what the assigned model lacked: tools, images, structured outputs, json mode or context. When nothing on the ladder fits, the answer is 400 no_capable_model rather than a quietly truncated prompt.

    A request that continues a tool loop — its last message is a tool result, or an assistant turn carrying tool_calls — reuses the model that answered the loop's first turn instead of re-scoring every step. The loop is keyed on the caller, the chat id and every message up to the last user turn, and the pin lives 1,800 seconds. If the loop's context has grown past what that model can take, the pin is dropped with reason: "sticky broken: context"; otherwise the response carries sticky: true and X-TeamShared-Sticky: 1.

  5. Complete

    one upstream call · connect 5 s · read 120 s · HTTP/2

    Your original messages go to the chosen model with your bearer swapped for the router's own provider key; your token never leaves this host. Before the first byte, a connect failure or a 408, 429, 500, 502, 503 or 504 is retried 1 more time with backoff capped at 2 seconds — an upstream Retry-After is honoured up to the same cap — and then the next model is tried: a specialist falls back to its rung's generalist, a generalist to the adjacent rung — one up, or one down from rung 5. After the first byte nothing is replayed. X-TeamShared-Attempts counts every try, and X-TeamShared-Assigned-Model names the model that actually answered.

    A non-streamed answer is then checked before it is returned. Five things make it unusable:

    KindTest
    emptyNo text and no tool calls, with finish_reason of stop
    truncatedfinish_reason of length below the max_tokens you asked for
    bad_tool_callA call to a function you did not declare, or arguments that are not JSON
    bad_jsonA json_object or json_schema answer that does not parse, or misses a required key of the schema
    refusalA refusal field, or a short answer opening I'm sorry, I can't help, As an AI

    An unusable answer buys one more completion one rung up for the same task — the next rung's specialist when one claims the task there, else its generalist — or one down when it was already at the top. If the second passes it is returned with X-TeamShared-Escalated naming the kind that failed; if it does not, the first answer comes back as it was. A stream is already flushing by the time any of this could be known, so it is returned as it stands — its routing rides on the response headers, and stream_options.include_usage is added so it is metered like everything else.

One scoring hop, then one completion — a second only when the first will not answer or comes back unusable. This is a completions gateway, not an agent loop: nothing here runs tools, keeps memory, or calls a model twice on your behalf beyond that.

The roster

Five rungs, ten models

Each difficulty maps to a generalist that answers well at that level for as little as possible. Five specialists sit beside the ladder, each claiming particular task kinds at particular rungs; a specialist's rung is the same 1 to 5 scale, not a separate one. Because input and output are priced separately, a model is not always cheaper than the one above it on both. Every id is pasteable: send it as the model to pin that model.

  1. 1trivial: greetings, arithmetic, one-fact lookup
  2. 2easy rewrite, translation, or short summary
  3. 3standard coding, SQL, or professional analysis
  4. 4hard systems, proofs, security review, architecture
  5. 5expert research, novel protocols, formal methods
Difficulty Generalist Input / 1M Output / 1M
1mistralai/mistral-nemo$0.03$0.03
2deepseek/deepseek-v4-flash$0.09$0.18
3z-ai/glm-5.3-flash$0.15$0.50
4tencent/hy4-preview$0.83$2.50
5deepseek/deepseek-v4.1-flash-1017$0.01$0.50
Difficulty Specialist Role Tasks Basis Input / 1M Output / 1M
1–2google/gemini-2.5-flash-litelinguisttranslate, rewrite, lookupdeclared$0.10$0.40
3deepseek/deepseek-v4.1-flashcodercode, sqldeclared$0.05$0.57
3–4openai/gpt-oss-120bmathematicianmathdeclared$0.15$0.60
4minimax/minimax-m3writercreative, researchdeclared$0.30$1.20
4–5openai/gpt-5.6-lunaarchitectcode 4⁠–⁠5, security 4⁠–⁠5, research 5declared$0.20$1.20

This is gating, not voting: once the score and the task are known exactly one model is asked. A per-task routing table entry wins when one is configured; otherwise a specialist that declares that task at that rung; otherwise the rung's generalist. A specialist at rung 3 answers the tasks it declares when they score 3, and nothing that scores 2 or 4. The classifier never sees this table — it describes the request, and the router picks the model — so editing the roster never requires re-tuning the classifier. The Basis column is the honest part: declared means the role and its strength are a prior written into the roster by hand, not a measurement. scripts/build_labels.py --roster runs a labelled prompt set through all ten models and judges each answer, and scripts/build_quality.py turns the passes into per-task, per-difficulty pass rates that outvote the declaration as they accumulate. The response names the row that answered: router.role and X-TeamShared-Role carry the specialist's role, and are empty and absent when a generalist answered.

The rubric on the left is the one the classifier is scored against, verbatim. Both tables are generated from the ladder and the roster this deployment routes on, not written by hand, and the prices are list, per million tokens, from the model catalog the router ships and refreshes. What you are billed is the cost the upstream reports for your own call, which is the number the ledger records. Neither table is an allowlist: any model the router carries still routes by explicit id whether or not it has a row.

Response

Every answer says how it was routed

A JSON completion carries a router object alongside the usual choices and usage. A stream cannot carry a body field, so the decision rides on response headers too — every field that fits in one has a header, and a few are body-only.

Body fieldHeaderMeaning
difficultyX-TeamShared-Difficulty1 to 5, after the gate and the size floor: the rung the roster was looked up at.
taskX-TeamShared-TaskOne of the eleven task kinds.
assigned_modelX-TeamShared-Assigned-ModelThe upstream id that answered. After an availability fallback or an escalation it is the model that actually did, not the one first assigned.
confidenceX-TeamShared-ConfidenceThe classifier's 0 to 1 confidence in its score. Absent when the prefilter or a sticky loop assigned the rung.
riskX-TeamShared-Risklow, med or high: what a too-cheap route would cost.
outputX-TeamShared-OutputExpected answer length: short, medium, long or very_long.
needs_tools—Whether the classifier read the request as wanting tools, browsing or execution.
prefilterX-TeamShared-PrefilterThe deterministic rule that fired: empty, health, tool_ping, ack, size-floor or learned. An empty string, and no header, when none did.
bumpedX-TeamShared-Route-Bumped1 when the gate or the size floor raised the rung.
fallbackX-TeamShared-Route-Fallback1 when scoring failed and the request snapped to rung 3.
stickyX-TeamShared-Sticky1 when a tool loop reused the model that answered its first turn.
capability_bumpX-TeamShared-Capability-BumpWhat the assigned rung lacked when the walk moved the request, comma-separated.
table_hitX-TeamShared-Table-Hit1 when a per-task routing table chose the model, ahead of the roster.
roleX-TeamShared-RoleThe roster role of the model that answered — coder, mathematician and the rest of the Role column above. Empty, and no header, when a generalist answered, including after a specialist fell back to its rung's generalist.
classifier—Who decided: the classifier's own name, prefilter, or sticky.
reason—The classifier's stated reason, or the rule that fired: ack, health probe, large context, sticky loop, classifier unreachable.
suggested_model—Kept for older classifier output that named a model. Empty today.
—X-TeamShared-EscalatedPresent when a non-streamed answer failed the answer check and the retry passed: the kind that failed.
—X-TeamShared-AttemptsUpstream tries, counting retries and fallbacks. 0 for a short-circuit.
—X-TeamShared-Upstream-ModelThe upstream's own id for the model that answered; prefilter for a short-circuit.
—Server-Timingvalidate, classify and upstream (time to first byte), in milliseconds.
—X-Request-Id, X-Trace-Id, traceparentPer-request ids. An inbound traceparent is kept, so every step of an agent loop shares one trace.

Every routed request also writes one structured decision line on the router's side. Your prompt is never written to it verbatim: it is hashed after lowercasing and collapsing whitespace, and the request is otherwise described by counts, lengths and the decision itself. The one field to know about is reason, which is copied from the classifier's own output — a classifier that quotes the prompt back puts that quote in the line.

Try it

Run the prefilter on a prompt

The first stage is deterministic, so it runs here in your browser. The word tables and the size floors it uses are rendered into this page from the module and the settings that route, not copied into the script. The classifier is not simulated: past this stage a real request costs one scoring hop.

System prompt, tool schemas, tool results. The size floors count all of it.

Scripts are off, so nothing runs here. The rules are the first table under the request path.

Coverage

Namespaced ids, one bearer

The first path segment of a model id selects the upstream adapter; the rest is forwarded as that provider's own id. Provider keys stay on the router, so your client holds one token however many providers it reaches. A prefix whose upstream is not configured is left out of GET /v1/models and answers 400 upstream_not_configured.

SendWhat happens
AutoRouterScored and labelled by task, then one model of the roster above: the specialist that declares that task at that score, else the rung's generalist. The id echoed back is AutoRouter; the model that answered is in the headers.
deepseek/deepseek-v4-flashRouted to OpenRouter (the openrouter/ prefix is optional). Any OpenRouter id routes, listed or not; the list is a catalog, not an allowlist.
openrouter/nvidia/nemotron-3-super-120b-a12b:freeA free-tier id. OpenRouter caps :free models at 20 requests a minute and 1,000 a day across the account.
xai/grok-4The xAI adapter, forwarded as grok-4.
zai/glm-5.3The Z.ai adapter, forwarded as glm-5.3.
fireworks/accounts/fireworks/models/deepseek-v3p1The Fireworks adapter, forwarded with its full account path.

OpenRouter's own model fallbacks pass through: send models as a list of router ids — openrouter/google/gemma-4-31b-it:free then openrouter/deepseek/deepseek-v4-flash, say — and OpenRouter tries them in order when the first rate-limits or is down. The router strips the prefix, drops ids on other prefixes, and drops the list entirely for non-OpenRouter upstreams.

Quickstart

Change the base URL, keep the code

Any OpenAI client works, streaming included. Mint a key in the console, point the SDK at https://router.teamshared.com/v1, and send AutoRouter to let the router pick — or an explicit id to pin one. The routing decision is a header away in every SDK that exposes the raw response.

curl -i https://router.teamshared.com/v1/chat/completions \
  -H "Authorization: Bearer $ROUTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "AutoRouter",
    "messages": [{"role": "user", "content": "Rewrite this query as a CTE: SELECT ..."}]
  }'
import os
from openai import OpenAI

client = OpenAI(base_url="https://router.teamshared.com/v1", api_key=os.environ["ROUTER_KEY"])

raw = client.chat.completions.with_raw_response.create(
    model="AutoRouter",
    messages=[{"role": "user", "content": "Rewrite this query as a CTE: SELECT ..."}],
)
print(raw.headers["x-teamshared-assigned-model"])  # the model that answered
completion = raw.parse()
print(completion.choices[0].message.content)
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://router.teamshared.com/v1", apiKey: process.env.ROUTER_KEY });

const { data, response } = await client.chat.completions
  .create({
    model: "AutoRouter",
    messages: [{ role: "user", content: "Rewrite this query as a CTE: SELECT ..." }],
  })
  .withResponse();
console.log(response.headers.get("x-teamshared-assigned-model")); // the model that answered
console.log(data.choices[0].message.content);

Auth

The bearer is your TeamShared identity: an org API key minted in the console, or the session or Connect token TeamShared already issued you. It is validated against TeamShared before any upstream call, and the check fails closed — a 401 and no upstream request when it cannot be confirmed. Spend is attributed to the org and user that token resolves to, which is why one shared key for everyone is the wrong shape.

Credits

Prepaid. Each completion draws down by the cost the upstream reports for that call, and the console shows the balance plus a per-completion ledger you can export. With credit gating on, a zero balance answers 402 insufficient_credits before any upstream call rather than after one. Top-ups go through hosted Stripe Checkout; a card never touches this host.