Smart Routing
Smart routing
Not every request needs a frontier model. Celon scores a prompt’s difficulty in a few milliseconds and sends it to the cheapest tier that still holds the quality line.
celon/auto — difficulty-based automatic routing
Request with model: "celon/auto" and a rule-based classifier (no LLM call, <5ms on CPU) computes a difficulty score in the 0–1 range. The score is a weighted sum of the features below.
| Feature | Weight | Description |
|---|---|---|
promptSize | +0.20 | Prompt token count (saturates to 0.5 at 1k tokens) |
codePresence | +0.25 | Code fences (```), code tokens, file extensions |
reasoningSignals | +0.40 | Reasoning keywords — analyze, design, prove, debug, optimize, trade-off … (matched in Korean and English) |
structuredOutput | +0.15 | Whether tools / tool_choice / a JSON response_format is used |
simplicitySignals | −0.25 | Summarize, classify, translate, extract keywords, and very short prompts (lowers the score) |
conversationDepth | +0.10 | Multi-turn conversation depth (accumulated context) |
Score → tier mapping (with the default balanced dial): score ≥ 0.62 → Tier 1, score ≥ 0.30 → Tier 2, and anything below that → Tier 3. The computed score is reported as classifier_score in the response metadata.
Intent dials
The same classifier, but every workload wants a different trade-off. Pick one of four presets with celon.dial (per request) or the key’s routing policy (routing.dial). The applied dial is reported in the dial response metadata and the x-celon-dial header.
| Dial | How it works | Recommended workloads |
|---|---|---|
balanced | The default. Thresholds 0.62 / 0.30, sticky weight 0.5 — the balance point between cost and quality. | General-purpose backends · internal assistants · default integrations |
strict-quality | Lowers the thresholds to 0.45 / 0.18 to make promotion to higher tiers easier, and always applies sticky routing. Conservative about downgrading. | Customer-facing chatbots · legal, medical, and other work where a wrong answer is costly |
max-savings | Raises the thresholds to 0.85 / 0.55 and always forces the cheapest candidate (the only exception being extreme-difficulty queries beyond the anchor). | High-volume batch · offline pipelines · tagging and classification |
low-latency | Reorders candidates to prefer latencyClass fast first, and always prioritizes cache-warm (sticky) providers. Thresholds are the same as the default. | Real-time UIs · autocomplete · voice and streaming frontends |
Purpose routers — intent as a model name
Instead of picking a dial yourself, you can name a purpose router id as the model string. celon/code for coding workloads, celon/quality when quality is non-negotiable (always frontier), celon/nitro when speed is everything (ranked by measured throughput), and celon/floor when cost comes first (ranked by real-time lowest price) — all of them are virtual model ids, so any OpenAI SDK applies them through a single model string.
| Router id | Purpose | Selection | Candidate set |
|---|---|---|---|
celon/code | Dev — Coding-focused — hard code goes to the frontier, lighter code to a chain of strong coding models. | tiered | T2: qwen/qwen3-235b → moonshot/kimi-k2.6 · T3: zhipu/glm-5.2 → deepseek/deepseek-v3.2 → meta-llama/llama-4-scout |
celon/quality | Top quality — Always uses frontier (Tier-1) models only. No difficulty classification — frontier chain + fallback. | frontier | T1: anthropic/claude-fable-5 → openai/gpt-5.2 → anthropic/claude-opus-4.8 |
celon/nitro | Top speed — Picks the fastest model by measured throughput (tokens/sec) and latency. No difficulty tiers. | nitro | Entire text catalog (modality, context, and availability constraints only) |
celon/floor | Lowest price — Picks the cheapest model by live pricing. No difficulty tiers. | floor | Entire text catalog (modality, context, and availability constraints only) |
Savings-target routing
Specify celon.anchor (a quality ceiling) together with celon.savings_target (a target savings rate, in the 0–0.8 range) and the router automatically tunes how aggressively it routes downward so the realized savings converge on the target. For example, 0.4 targets “40% cheaper than always using the anchor alone.”
{
"model": "celon/auto",
"messages": [{ "role": "user", "content": "Summarize this quarter's metrics" }],
"celon": {
"anchor": "anthropic/claude-fable-5",
"savings_target": 0.4
}
}The feedback loop is grounded in the usage ledger — it aggregates the realized savings rate (against the anchor baseline) from the key’s actual billing records, then routes downward more aggressively when it falls short of the target and pulls back more conservatively when it overshoots. Because the basis is the realized value left in the ledger rather than an estimate, cache hits and fallback costs are reflected exactly as they happened.
The four-stage constraint-aware pipeline
A celon/auto routing decision is not a single difficulty score — it runs through a four-stage pipeline that applies the request’s constraints in order. Each stage’s verdict is traced in the pipeline array of the response metadata.
| Stage | Name | Behavior |
|---|---|---|
| 1 | Modality filter | Removes any candidate that doesn’t support a capability the request needs — image input, tool calling, a JSON schema, and so on (modality:pass). |
| 2 | Context / compression | Excludes a candidate when the prompt exceeds its context window, and when context_compression: true, trims the middle (a lost-in-the-middle trim) to fit a cheaper model (context:fit). |
| 3 | Sticky cache routing | Conversations that share a session_id prefer the provider whose prompt cache is warm — the lower the cache re-read price (for example Anthropic and DeepSeek at 0.1×), the greater the weight (sticky:hit, header x-celon-sticky). |
| 4 | Difficulty + anchor fallback | Applies the difficulty score and the dial’s thresholds to the remaining candidates to settle on a tier. When an anchor model is set, even an extreme-difficulty query never climbs above the anchor (difficulty:0.41→tier2). |
The three tiers
The models in each tier are listed in fallback order — if the first model fails, it automatically switches to the next. Prices are per 1M tokens (input / output).
| Tier | Character | Models (fallback order) | Price ($/1M in / out) |
|---|---|---|---|
| Tier 1 | Frontier — hard reasoning and agents, the last line of defense against confident-wrong failures | anthropic/claude-fable-5 → openai/gpt-5.2 | $10 / $50 · $1.75 / $14 |
| Tier 2 | High-performance open weights — everyday work, summarization, RAG answers | meta-llama/llama-4-maverick → qwen/qwen3-235b | $0.15 / $0.60 · $0.09 / $0.60 |
| Tier 3 | Ultra-fast and low-cost — classification, extraction, simple Q&A, high-volume batch | deepseek/deepseek-v3.2 → zhipu/glm-5.2 → meta-llama/llama-4-scout | $0.27 / $0.40 · $0.85 / $2.68 · $0.08 / $0.30 |
Dynamic price tracking
The table above is a snapshot. Actual billing rates sync periodically from the LiteLLM and OpenRouter price feeds (every 6 hours by default), and every change is recorded in the model_pricing history table. Read the current rates with GET /admin/pricing, force an immediate re-sync with POST /admin/pricing/refresh, or pin a specific model’s rate by hand with PUT /admin/pricing/override.
Naming a model explicitly
Catalog ids use the vendor/model form — for example anthropic/claude-fable-5, openai/gpt-5.2, deepseek/deepseek-v3.2. List them all with GET /v1/models. Name a model and the request bypasses the classifier and passes straight through (classifier_score: null); an id that isn’t in the catalog is rejected with 404 model_not_found. The celon.tier hint is ignored for explicit-model requests.
Per-request celon options
The celon object in the request body lets you control routing and caching behavior per request. Every field is optional.
{
"model": "celon/auto",
"messages": [{ "role": "user", "content": "Design the core API" }],
"celon": {
"dial": "strict-quality",
"anchor": "anthropic/claude-fable-5",
"savings_target": 0.3,
"session_id": "conv_8f2a91",
"context_compression": true,
"no_cache": true,
"fallbacks": ["meta-llama/llama-4-maverick", "deepseek/deepseek-v3.2"]
}
}| Field | Type | Description |
|---|---|---|
dial | balanced | strict-quality | max-savings | low-latency | The intent dial for this request. When omitted, the key’s configured dial, then balanced. |
anchor | string | The anchor (ceiling) model id — even an extreme-difficulty query never falls back above this model. Takes precedence over the key’s routing.anchorModel. |
savings_target | number (0–0.8) | Target savings rate against the anchor — uses the realized savings rate from the ledger as feedback to auto-tune routing strength. See Savings-target routing. |
session_id | string | Sticky-routing session id — conversations sharing an id are pinned to the provider whose prompt cache is warm (check the x-celon-sticky header). |
context_compression | boolean | Trims the middle of an over-long prompt (a lost-in-the-middle trim) to fit the context window of a cheaper model. |
tier | 1 | 2 | 3 | celon/auto only — skips the classifier and forces the given tier. |
no_cache | boolean | Disables both cache lookup and storage for this request. |
no_pii_mask | boolean | Passes this one request through without masking even when PII masking is enabled on the key. |
fallbacks | string[] | Explicit-model requests only — a chain of catalog ids appended after the named model. With celon/auto the tier chain already acts as the fallback. |
shadow | boolean | Shadow (validation) mode — the response is served by the anchor (else the frontier baseline) while the router's pick and its estimated cost are only recorded in the celon.shadow response meta (would_model · would_tier · would_cost_usd) and the x-celon-shadow header. Use it to validate the router on real traffic risk-free. Overrides the key's routing.shadow; auto routing only. |
How the fallback chain behaves
- Retry conditions — if an upstream returns
429or5xx, or a network/timeout error occurs, it automatically switches to the next candidate. Any other4xxfails immediately. - Switch order — it first tries the same model’s alternate providers (
altProviders, e.g. DeepInfra → Novita), then moves on to the next candidate model. - Tracing — the attempted path is recorded in the
attemptsarray of the response metadata and thex-celon-attemptsheader. - When all fail — it responds with
502 upstream_unavailable. - Budget downgrade — when a key is over budget and its policy is
downgrade,celon/autorequests are forced to Tier 3. See Governance.
Response metadata
Every response carries the routing outcome in the celon body field and the x-celon-* headers.
{
"id": "chatcmpl-…",
"object": "chat.completion",
"model": "meta-llama/llama-4-maverick",
"choices": [ … ],
"usage": { "prompt_tokens": 21, "completion_tokens": 96, "total_tokens": 117 },
"celon": {
"model": "meta-llama/llama-4-maverick",
"provider": "deepinfra",
"tier": 2,
"classifier_score": 0.41,
"cache_hit": null,
"cost_usd": 0.000122,
"saved_usd": 0.003886,
"latency_ms": 842,
"pii_masked": 0,
"attempts": [],
"dial": "balanced",
"sticky": true,
"pipeline": ["modality:pass", "context:fit", "sticky:hit", "difficulty:0.41→tier2"],
"guardrails": { "mode": "mask", "findings": 0, "blocked": false }
}
}| Header | Value |
|---|---|
x-celon-model | The catalog model id that actually served the request |
x-celon-tier | The routed tier (0–3, when applicable) |
x-celon-cache | exact | semantic | miss |
x-celon-cost-usd | The cost of this request (USD, 6 decimal places) |
x-celon-saved-usd | Savings against the Tier 1 baseline (USD) |
x-celon-attempts | The list of failed attempts (only when a fallback occurred) |
x-celon-dial | The applied intent dial — balanced · strict-quality · max-savings · low-latency |
x-celon-sticky | 1 — only when sticky routing pinned to a cache-warm provider |
x-celon-guardrail-findings | The number of guardrail findings (omitted when 0) — see Tiered guardrails |
With streaming the cost isn’t known up front, so only x-celon-model · x-celon-tier · x-celon-dial · x-celon-sticky · x-celon-attempts go out as headers at the start; the stream’s final data chunk (choices: []) carries the full usage and celon metadata, followed by data: [DONE].