Cost
Keep model spend predictable as usage grows
Send every task through one OpenAI-compatible endpoint, route it to the cheapest model that can do the job, and log the token usage each response reports.
Copy the prompt
Replace the bracketed fields, then paste it into Claude Code, Codex, or any AI with terminal access. It installs the Felo skill, asks before spending credits, and runs the workflow.
Review our model spend and propose a routing plan for these tasks: [list tasks or features]. Constraints: monthly budget [amount], latency target [target], quality bar [bar]. Read the live model catalog before recommending anything. Produce: 1. A task -> model recommendation table. 2. Which tasks can move down a tier, and the quality risk. 3. A usage-logging plan per task and feature. 4. What to measure before changing production. Rules: - Read current model IDs and prices from the live catalog; never quote a rate from memory. - Do not change production configuration or send real traffic. - Ask before spending credits on test calls. - Keep a rollback plan and a per-feature spending cap. - Write the plan to ./routing-plan.md and summarize it in chat.
The copy also includes the install steps and safety rules.
What you get
Example output from a run. Not a live generation.
| Task | Current | Proposed | Risk |
|---|---|---|---|
| Classify ticket | strong model | fast model | low |
| Draft reply | strong model | strong model | none |
| Summarize changelog | strong model | fast model | medium |
Before changing anything: log usage per task for one week and compare quality on 50 sampled outputs.
The exact calls
For developers: the same workflow as copy-paste code, in the order it runs.
Show the exact API calls
Send every task through one endpoint
Chat Completions is OpenAI-compatible, so one
fetchcovers every model you route to. The only thing that changes between a cheap and a strong call is themodelvalue. Readchoices[0].message.contentfor the text and keepusage— the logging step needs it.routing.ts const BASE_URL = "https://openapi.felo.ai/api/v1"; const headers = { Authorization: `Bearer ${process.env.FELO_API_KEY}`, "Content-Type": "application/json", }; type Completion = { choices: { message: { content: string } }[]; usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number }; }; async function chat(model: string, prompt: string): Promise<Completion> { const res = await fetch(`${BASE_URL}/chat/completions`, { method: "POST", headers, body: JSON.stringify({ model, messages: [{ role: "user", content: prompt }] }), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json() as Promise<Completion>; } const summary = await chat( "claude-haiku-5-5", "Summarize this changelog in three bullets: …", ); console.log(summary.choices[0].message.content);Example output
{ "choices": [{ "message": { "content": "• …" } }], "usage": { "prompt_tokens": 812, "completion_tokens": 96, "total_tokens": 908 } }Route each task to a fast or strong model
Write the routing table once, keyed by what the task needs rather than by feature name. Classification, extraction, and short summaries belong on the fast tier; drafting and review belong on a stronger one. The request value is the public model ID shown on the model page, so the catalog slugs
anthropic/claude-haiku-5.5andanthropic/claude-sonnet-5.5are requested asclaude-haiku-5-5andclaude-sonnet-5-5.routing.ts type TaskKind = "classify" | "extract" | "summarize" | "draft" | "review"; const MODEL_ROUTES: Record<TaskKind, string> = { classify: "claude-haiku-5-5", extract: "claude-haiku-5-5", summarize: "claude-haiku-5-5", draft: "claude-sonnet-5-5", review: "claude-sonnet-5-5", }; export async function route(kind: TaskKind, prompt: string) { const model = MODEL_ROUTES[kind]; const result = await chat(model, prompt); return { model, text: result.choices[0].message.content, usage: result.usage, }; }Example output
{ "model": "claude-haiku-5-5", "text": "• …", "usage": { "prompt_tokens": 812, "completion_tokens": 96, "total_tokens": 908 } }Read the usage off every response and log it
Each completion reports
prompt_tokens,completion_tokens, andtotal_tokens. Write one structured line per call and aggregate bymodeland task kind: together they show which tier the spend is going to, and you can slice the same records by feature before you set a budget.routing.ts type UsageRecord = { model: string; kind: TaskKind; promptTokens: number; completionTokens: number; totalTokens: number; }; const usageLog: UsageRecord[] = []; function logUsage(kind: TaskKind, model: string, usage: Completion["usage"]) { const entry: UsageRecord = { model, kind, promptTokens: usage.prompt_tokens, completionTokens: usage.completion_tokens, totalTokens: usage.total_tokens, }; usageLog.push(entry); console.log(JSON.stringify(entry)); // ship this line to your metrics store return entry; } export function tokensByModel() { return usageLog.reduce<Record<string, number>>((totals, entry) => { totals[entry.model] = (totals[entry.model] ?? 0) + entry.totalTokens; return totals; }, {}); } const result = await route("summarize", "Summarize this changelog in three bullets: …"); logUsage("summarize", result.model, result.usage); console.log(tokensByModel());Example output
{"model":"claude-haiku-5-5","kind":"summarize","promptTokens":812,"completionTokens":96,"totalTokens":908} { "claude-haiku-5-5": 18234, "claude-sonnet-5-5": 4201 }Use one key for every supplier
There is no per-provider key to manage: the same
FELO_API_KEYcovers every model and every supplier. OmitX-Felo-Channeland the platform picks a lower-priced available supplier for that model; pin a channel only when a workload needs a specific one. Pinning is a selection, not a retry policy — add your own fallback if you need failover.routing.ts async function chatOnChannel(model: string, prompt: string, channel?: string) { const res = await fetch(`${BASE_URL}/chat/completions`, { method: "POST", headers: { ...headers, ...(channel ? { "X-Felo-Channel": channel } : {}), }, body: JSON.stringify({ model, messages: [{ role: "user", content: prompt }] }), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json() as Promise<Completion>; } // Leave `channel` undefined for the default supplier. export function routePinned(kind: TaskKind, prompt: string, channel?: string) { return chatOnChannel(MODEL_ROUTES[kind], prompt, channel); }Read rates from the live catalog, not from code
A rate table in your repository goes stale the day a supplier changes a price. List the current chat model IDs from the catalog endpoint, then price the tokens you logged against the live rates on
/modelsand/pricing. Discovery does not invoke a model, so keep the call out of the request path and cache the result.routing.ts type Catalog = { data: { id: string; owned_by: string; model_kind: string }[]; }; export async function chatModelIds() { const res = await fetch( `${BASE_URL}/models?exclude_video_and_decisions=true`, { headers }, ); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); const catalog = (await res.json()) as Catalog; return catalog.data .filter((model) => model.model_kind === "chat") .map((model) => model.id); } // Price `tokensByModel()` against https://openapi.felo.ai/pricing and // https://openapi.felo.ai/models. Keep the links, not the rates, in your code.Example output
["claude-haiku-5-5", "claude-sonnet-5-5"]
What's included
- A task-to-model recommendation table based on the live catalog
- A per-task usage logging plan
- A downgrade candidate list with quality risk
- A measurement and rollback plan
What it doesn't do
- It does not change production config or route real traffic
- It does not guarantee a lower bill; it proposes what to measure
- Prices and model IDs change; always read the live catalog
- It does not evaluate quality for you; that requires your own samples
- It does not replace a billing or observability system
Questions
Will this cut our bill?
It proposes where a cheaper tier may hold quality. The saving is only real after you compare sampled outputs and log usage.
Does it call models automatically?
Only if you approve test calls. The default plan reads the catalog and writes a recommendation, without production traffic.
How do I know which model is cheaper?
Read the live model catalog and pricing page. The workflow never hardcodes rates because they change.
Can I pin a provider?
Yes, when latency matters more than price. Pinning is a selection, not a retry policy; add your own fallback.
What should I log?
One record per call: task, feature, model, prompt tokens, completion tokens, total tokens, and latency. Aggregate by feature before setting budgets.
How much does the review cost?
Catalog reads do not spend a model call. Test calls are usage-based; the prompt asks for approval before any are made.
Related workflows
Answer support questions from your own documentation
Create a LiveDoc, load your help center and internal runbook, retrieve the passages that answer the ticket, and let a model answer only from that evidence.
Open workflowResearchTurn one question into a sourced research brief
Ask the web, pull the pages behind the answer as Markdown, add what X is saying, then keep the evidence in a LiveDoc you can query again later.
Open workflowOfficeTurn a research page into a slide deck
Extract the page, file it as LiveDoc context, then let the PPT Task API build the deck while you poll for the finished link.
Open workflowMake it yours
The same recipe, adapted to the way you actually ship.
Move the seam only after you measure it
Run the same prompt on both tiers, compare the outputs, and downgrade a task kind only where quality holds. The usage log is the ruler: it shows what each downgrade saves, per feature and per call, instead of leaving you to guess.
Cap spend per feature
Add a feature name to each usage record and aggregate by it instead of by model alone. When a feature approaches its daily budget, stop escalating its work to the strong tier and queue it for the next window rather than letting the router spend without a ceiling.
Pin a supplier when latency beats price
Send `X-Felo-Channel` with the public channel code from the model page when one supplier is visibly faster for your workload. Compare supplier rows on `/models` first, and implement retries yourself — a pinned channel does not fall back to another provider.
Keep going
The reference pages behind this recipe, and where to go next.