Felo API PlatformFelo API Platform

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.

Build time20 minutesLevelIntermediateUpdated2026-10-29
Who it's forPlatform teamsAPI product ownersEngineering leads

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.

Example routing plan (excerpt)
TaskCurrentProposedRisk
Classify ticketstrong modelfast modellow
Draft replystrong modelstrong modelnone
Summarize changelogstrong modelfast modelmedium

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
  1. Send every task through one endpoint

    Chat Completions is OpenAI-compatible, so one fetch covers every model you route to. The only thing that changes between a cheap and a strong call is the model value. Read choices[0].message.content for the text and keep usage — 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 }
    }
  2. 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.5 and anthropic/claude-sonnet-5.5 are requested as claude-haiku-5-5 and claude-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 }
    }
  3. Read the usage off every response and log it

    Each completion reports prompt_tokens, completion_tokens, and total_tokens. Write one structured line per call and aggregate by model and 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 }
  4. Use one key for every supplier

    There is no per-provider key to manage: the same FELO_API_KEY covers every model and every supplier. Omit X-Felo-Channel and 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);
    }
  5. 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 /models and /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

Make 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.

The reference pages behind this recipe, and where to go next.