Mixed-model sessions
Let each turn of a conversation run on the least expensive model that is good enough for it. Turn it on per key and see the savings first, with no code changes. Acting on them takes a small change to your app.
Why opt in
Most conversations mix easy turns and hard ones. A summary, a translation or a field extraction rarely needs the model you picked for planning and code. Mixed-model sessions classify each turn and pick the cheapest model in your pool that scores above your quality floor for that kind of task.
Start in Shadow mode. It needs no code changes, never changes which model answers, and costs you nothing: the classifier runs after the response on our account. The Savings page then shows what each turn would have cost on the suggested model.
Collecting those savings takes development work. In Suggest mode nothing switches on its own: your app has to read each suggestion and send it back to accept it. Changing the mode in the console doesn't do that for you. See From Shadow to Suggest for what to change.
Shadow mode is free and changes nothing about your responses. Turning it on for a key is the quickest way to find out what mixed-model sessions would save you.
How it works
For each turn from an opted-in key:
- A small classifier (TypeSafe Jev) reads the system prompt, tool names, the latest user message, and as many earlier turns as fit in 2,000 tokens. It returns one task class and a confidence.
- Below your minimum confidence, or for
unknown, nothing is suggested. - Otherwise, the candidates are the models in your pool that are certified, allowed by Directives, support what the request needs (tools, structured output, web search), and score at or above the quality floor for that task.
- The cheapest candidate is suggested, or your manual pick for that task. Nothing is suggested if it would not save more than the classifier costs.
Task classes: extraction, summarization, rewriting, translation, general_qa, coding, complex_reasoning, and unknown (small talk, acknowledgements, or a mix).
Turn it on
- Open Dispatch → Mixed-model sessions for the project.
- Under Keys, set the key to Shadow and save. Each key has its own mode (Off, Shadow or Suggest); keys that are Off ignore everything on this page.
- Send traffic as usual, ideally with a session id (next section).
- Review Savings. To collect the savings, change your app to send suggestions back, deploy it, and only then set the same key to Suggest. See From Shadow to Suggest.
Use Edit pool to choose which models a turn may move to, and Model per task to pin a model for one task class. The Open test panel button runs a turn through the classifier without changing your settings.
Group turns into sessions
A session id ties the turns of one conversation together. Pass the same id on every turn, either as ib.session in the body or as an ib-session header. Use whichever your client makes easier: a header needs no body changes, so it works with SDKs that reject unknown fields. Ids are 1 to 128 characters of letters, digits, _, ., : and -. An invalid header is ignored; an invalid ib.session returns 400.
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.inferencebureau.com/v1",
apiKey: process.env.IB_API_KEY,
});
// One id per conversation. A header works with any client;
// the ib.session body field does the same thing.
const res = await client.chat.completions.create(
{ model: "anthropic/claude-sonnet-5", messages },
{ headers: { "ib-session": conversationId } },
);With a session id:
- Logs → Sessions shows each conversation, which model served each turn, and why it switched.
- In Suggest mode, a turn sent without a token declines the session's open suggestion, so stale suggestions don't pile up.
- The response echoes it back in the
ib-sessionheader.
Sessions are optional. Without one, every turn is still classified, and turns just aren't grouped.
Shadow mode
Every turn is served by the model you requested. After the response, the classifier runs on our account and records what it would have suggested. Nothing is added to the response and nothing is charged. Results appear under Shadow results on the project's Savings page: what Suggest would have done on each turn, and the estimated saving.
Shadow works on chat completions and Messages, streamed or not. Turns in the middle of a tool cycle are skipped. Content is never sent to the classifier when your Directives disallow Jev. When they require zero data retention, it goes only to Jev's ZDR provider.
From Shadow to Suggest
Shadow tells you what you would save. Suggest is how you actually save it, and it needs a code change in the place where your app sends turns. Switching the mode without that change saves nothing: every turn keeps running on the requested model, and with inline confirmation you start paying to classify each one.
- Choose a confirmation style. Inline returns a suggestion on each response for the next turn and adds no latency. Pre-flight asks
POST /v1/suggestbefore the turn, so the switch applies to that turn and a user can approve it first. - Send a session id on every turn, if you don't already. It's optional, but it groups turns in Sessions and stops stale suggestions from piling up.
- Read the suggestion. Inline: the
ib-suggestionresponse header, which is the only place it appears on streams. Pre-flight:suggestionin the/v1/suggestresponse. Most SDKs hide response headers by default. The OpenAI SDKs need.withResponse()orwith_raw_response. - Keep it with the conversation, and send it back as
ib.suggestionon the next turn (inline) or this turn (pre-flight). Keep requesting your usual model; we do the swap. Typed SDKs may needextra_bodyor a type override to send theibobject. - Check what changes for you. Accepted turns are answered by a different model. On Messages, earlier thinking blocks are dropped while Suggest is on (see What never changes).
- Deploy first, then switch the key. While the key is in Shadow no tokens are issued, so the new code just sends nothing extra. Once you set the key to Suggest, it starts accepting suggestions. The same key does both; you never need a second one.
- Verify. Responses to turns that sent a token carry
ib-suggestion-status: accepted, and switched turns appear in Dispatch, Sessions and Savings. If every status isignored_…, the reason is in the value (see Reference).
Not ready to change the client? Stay in Shadow. It keeps measuring at no cost.
Suggest mode
In Suggest mode the classifier result comes back to your app as a suggestion: a short-lived token naming a cheaper model. Nothing switches on its own. Your app accepts a suggestion by sending the token back with the turn, and Inference Bureau then serves that turn on the suggested model. The request still names your usual model; you never send the suggested model yourself.
Choose how suggestions reach you under Confirmation:
| Inline | Pre-flight | |
|---|---|---|
| Where the suggestion arrives | On the response to this turn | From POST /v1/suggest, before the turn |
| When it applies | The next turn | This turn |
| Added latency | None: classified alongside the turn | One classifier round trip, usually under 0.5 s |
| Token lifetime | 15 minutes | 2 minutes |
| Good for | Agents and chat apps that loop over turns | Apps that want the switch on the turn itself, or a user to confirm first |
Suggest mode only saves money if your app sends tokens back. A client that ignores ib-suggestion keeps getting the requested model on every turn, and in inline mode still pays for classifying each turn. If you can't change the client yet, use Shadow.
Inline confirmation
The turn is served by the requested model while the classifier runs alongside it. The response carries a suggestion for the next turn: in the ib-suggestion header, and on non-streamed responses also in an ib object in the body. Send that token as ib.suggestion on the next turn to accept it.
let pending: string | null = null;
async function turn(messages: OpenAI.ChatCompletionMessageParam[]) {
const { data, response } = await client.chat.completions
.create({
// Always the model you would normally use. An accepted
// suggestion swaps it on our side.
model: "anthropic/claude-sonnet-5",
messages,
// @ts-expect-error Inference Bureau extension, removed before dispatch
ib: { session: conversationId, ...(pending ? { suggestion: pending } : {}) },
})
.withResponse();
// Headers carry the suggestion on streamed and non-streamed responses.
pending = response.headers.get("ib-suggestion");
return data;
}On a non-streamed response the body includes the full suggestion:
"ib": {
"suggestion": {
"suggestion": "sug_8x2k…",
"model": "openai/gpt-5-mini",
"taskClass": "summarization",
"confidence": 0.91,
"reason": "cheapest_passing",
"estimate": { "baselineUsd": "0.004120", "suggestedUsd": "0.000610", "classifierUsd": "0.000040" },
"expiresIn": 900
}
}Streams send bytes through untouched, so on a stream the suggestion is in headers only. Read them from the response before consuming the body.
Pre-flight confirmation
Before sending a turn, post the same body to POST /v1/suggest. If it returns a suggestion, send the turn with that token to run it on the suggested model; send it without the token to decline. This is the place to ask a user to confirm, since you know the model before the turn runs. Chat completions, Messages and Responses bodies all work, and a suggestion only names a model the turn's endpoint serves.
const turnBody = { model: "anthropic/claude-sonnet-5", messages };
const s = await fetch("https://api.inferencebureau.com/v1/suggest", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.IB_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ ...turnBody, ib: { session: conversationId } }),
}).then((r) => r.json());
// s.suggestion is null when the requested model is already the best choice.
const res = await client.chat.completions.create({
...turnBody,
// @ts-expect-error Inference Bureau extension
ib: { session: conversationId, ...(s.suggestion ? { suggestion: s.suggestion } : {}) },
});Suggestion tokens
A token is accepted when it is from the same project, still open, unexpired, and issued for the model the turn requests. Each token works once. A token that can't be used never fails the request: the turn runs on the requested model and the ib-suggestion-status header says why. To decline explicitly, send the token with "decision": "decline"; that also records whether a person or a rule made the call (confirmed_by), which shows up in Sessions.
Tool cycles
While a tool cycle is open (the last message is a tool result, or the assistant's last message called tools) the model is pinned: no classifier call, no charge, and any token sent is left unused for later. The response carries ib-pinned: tool_cycle. Switching resumes on the next user turn, so a tool loop always finishes on the model that started it.
Task hints
If your app already knows what a turn is, for example a dedicated summarize button, send ib.task with the task class. The classifier is skipped and the hinted turn costs nothing extra. Hints only choose the task class: the pool, floor and Directives still decide the model.
Pointing an alias at a suggestion
When your app sends a team alias such as org/fast, suggestions are made for the model it names, and an accepted one changes the model for that turn only. If the same cheaper model keeps winning, you can make it the alias's model and skip the suggestion step:
- The project's Savings page lists an alias when, in the last 7 days, at least 5 accepted turns sent as it ran on the same suggested model and saved money overall, net of the classifier. Say
org/fastnamesanthropic/claude-sonnet-5and its turns keep acceptinganthropic/claude-haiku-4.5. - Owners and admins see Point org/fast at anthropic/claude-haiku-4.5, then confirm. Nothing changes an alias on its own.
- The change is an ordinary Directives edit at the level that defines the alias (team, project, environment or key): checked against the version the page showed, so a change made meanwhile is refused rather than overwritten, recorded in the audit log, and applied to new requests. A preset keeps its defaults and fallbacks (minus the new model).
- Aliases being retired (
deprecatedAt) and aliases whose model changed since those turns aren't offered.
What it costs
| Mode | Classifier charge |
|---|---|
| Off, or key not opted in | None |
| Shadow | None. We pay for it. |
| Suggest, inline | Each classified turn, at Jev's input-token price plus the platform fee, added to that turn's receipt. Charged whether or not you accept. |
| Suggest, pre-flight | Each /v1/suggest call, as its own request with its own receipt. |
| Tool cycles and task hints | None |
The classifier sees at most 2,000 tokens, so a charge is a fraction of a cent. Suggestions already account for it: a cheaper model is only suggested when it saves more than the classifier costs. See Pricing for current prices.
What never changes
- Directives still decide which models and providers may serve a turn.
- A suggested model must support everything the request asks for: tools, structured output, and web search tools.
- The classifier only returns a task label. Text inside a prompt can't add a model to the pool, lower the floor, or pick a model.
- One suggestion per turn, at most. The requested model is always the fallback.
- On Messages, thinking blocks from earlier turns are dropped before dispatch while Suggest mode is on, because a model won't accept another provider's signed thinking. The open tool loop keeps them.
Reference
The ib object, accepted on chat completions, Messages, Responses and /v1/suggest, and removed before the request reaches a provider:
| Field | Type | Meaning |
|---|---|---|
session | string | Conversation id. Same as the ib-session header; the body wins if both are sent. |
suggestion | string | A sug_… token to accept (or decline, with decision). |
decision | accept | decline | What to do with suggestion. Defaults to accept. |
confirmed_by | end_user | app_rule | Who decided, recorded on the session. |
task | string | A task class. Skips the classifier for this turn. |
Response headers:
| Header | When |
|---|---|
ib-suggestion | A suggestion was made. The token to send back. |
ib-suggested-model | The model that token would serve. |
ib-task-class | The turn was classified. |
ib-confidence | The classifier's confidence, two decimals. |
ib-suggestion-reason | No suggestion was made. Why (below). |
ib-suggestion-status | You sent a token. What happened to it (below). |
ib-pinned | tool_cycle while a tool cycle is open. |
ib-session | Echoes the session id. |
ib-suggestion-status values: accepted, declined, ignored_expired, ignored_model_mismatch (the token was issued for a different requested model), ignored_already_accepted / ignored_already_declined, ignored_unknown, ignored_tool_cycle, ignored_key_not_opted_in (the key is Off), ignored_shadow_mode (the key is in Shadow).
The most common reasons a suggestion is or isn't made, in reason and ib-suggestion-reason:
| Reason | Meaning |
|---|---|
cheapest_passing | Suggested: the cheapest pool model above the floor. |
manual_override | Suggested: your pick for this task under Model per task. |
requested_is_best | The requested model is already the cheapest passing choice. |
no_passing_model | No pool model is certified, allowed, capable and above the floor for this task. |
uneconomic | A cheaper model passes, but the saving wouldn't cover the classifier. |
low_confidence | Classifier confidence was below your minimum. |
unknown_task | Classified as unknown. |
context_too_large | The system prompt and latest turn alone exceed 2,000 tokens. |
privacy_requires_zdr | Directives require zero data retention, and no certified Jev provider offers it. Nothing was sent. |
privacy_requires_no_training | Directives require providers that do not train on prompts, and no certified Jev provider is listed as one. Nothing was sent. |
classifier_denied_by_directives | Directives don't allow Jev. Nothing was sent. |
classifier_timeout | The classifier didn't answer in time (1.5 s inline, 3 s pre-flight). |
tool_cycle_open | Pre-flight only: the turn is inside a tool cycle. |