Final Router

Integrations

Everything a builder needs, on one page

The facts an integrating product asks about - SDK setup, structured output, the caching contract, usage accounting, errors and attribution - written down so nobody has to ask.

One line, seven providersYOUR CLIENTbaseURL: api.openai.comfinalrouter.com/api/v1everything else unchangedOpenAIAnthropicGoogleMetaMistralDeepSeekMoonshotxAI"auto" picks
The integration is one changed line: point your existing OpenAI-compatible client at the gateway and every provider behind it becomes a model string.

Two-line setup

Final Router speaks OpenAI’s Chat Completions format, so the SDK you already use is the SDK for this. Vercel AI SDK:

TypeScript
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";

const finalrouter = createOpenAICompatible({
  name: "finalrouter",
  baseURL: "https://finalrouter.com/api/v1",
  apiKey: process.env.FINAL_ROUTER_KEY, // fr_live_...
});

// Then use it anywhere the AI SDK takes a model:
const model = finalrouter("auto"); // or any id from GET /v1/models

Official OpenAI SDK:

TypeScript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://finalrouter.com/api/v1",
  apiKey: process.env.FINAL_ROUTER_KEY,
});
Python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://finalrouter.com/api/v1",
    api_key=os.environ["FINAL_ROUTER_KEY"],
)

Run your coding tool on Final Router

Cursor, Cline, Roo - anything that speaks OpenAI’s API can use Final Router as its model provider, which puts fallback, spend caps and usage tracking under your assistant’s own inference. Menus move between versions; the constants are the base URL and your key.

  1. 1Settings → Models → API Keys: paste your fr_live_… key in the OpenAI key field
  2. 2Enable "Override OpenAI Base URL" and set it to https://finalrouter.com/api/v1
  3. 3Add the model ids you want as custom models - "auto" works, so does any id from GET /v1/models

The other direction - giving your assistant live Final Router data over MCP while it codes - has its own page.

Structured output, on every model

response_format: json_schema works across every routed model: the gateway translates the schema into a forced tool call - which all our providers honour, through routing and fallbacks - and hands the arguments back as ordinary JSON content. Both of the AI SDK’s generateObject paths work here: its default JSON mode (json_object with the schema in the prompt), and native json_schema if you enable structured outputs on the model - the second is the stronger guarantee, since the schema is enforced through the tool contract rather than asked for politely. Streamed, the JSON arrives as one content delta.

TypeScript
// OpenAI's json_schema format works on EVERY model we route -
// the gateway translates it into a forced tool call and returns
// plain JSON content, so generateObject's default path just works.
const completion = await client.chat.completions.create({
  model: "auto",
  response_format: {
    type: "json_schema",
    json_schema: {
      name: "finding",
      schema: {
        type: "object",
        properties: { site: { type: "string" }, issues: { type: "number" } },
        required: ["site", "issues"],
      },
    },
  },
  messages: [{ role: "user", content: "..." }],
});
const data = JSON.parse(completion.choices[0].message.content);
// One rule: json_schema cannot be combined with tools in the same
// request — the translation needs the request's tool slot.

The caching contract

Do not send cache_control - the gateway manages provider caching for you. Anthropic cache breakpoints are placed automatically when a stable prefix recurs (a cache write costs 1.25×, so we only cache what repeats); OpenAI, Gemini, Kimi and Grok implicit caching flow through untouched. Cache hits are billed to you at each provider’s discounted rate and reported in usage.prompt_tokens_details.cached_tokens. On top of that, an identical deterministic request (temperature 0, or cache: "exact") is answered from Final Router’s own response cache at $0.00.

Usage accounting

Per-message budgets need real numbers on streamed calls, so the final SSE frame always carries them - no include_usage opt-in required. Cached reads are not inside prompt_tokens; they were billed at the discount, and the details field is the receipt.

JSON
// The final frame of every stream (and every JSON response) carries
// the real numbers — include_usage is honoured by always being on:
"usage": {
  "prompt_tokens": 812,
  "completion_tokens": 64,
  "total_tokens": 876,
  "prompt_tokens_details": { "cached_tokens": 512 }  // when a cache hit happened
},
"final_router": {
  "provider": "anthropic",
  "cost_cents": 1               // what this answer actually cost you
}

Build your picker from the catalogue

GET /v1/models answers for your key - our providers plus your own BYOK keys, minus models you switched off - with capabilities that are live-tested rather than assumed. A picker built from it self-updates when we add providers.

JSON
// GET /v1/models - build your model picker from this, not a hardcoded list.
{
  "id": "openai/gpt-5-mini",
  "final_router": {
    "context_window": 400000,
    "input_price_per_mtok": 0.25,
    "output_price_per_mtok": 2,
    "modality": "text+image->text",
    "capabilities": {
      "vision": true,          // live-tested per model, never assumed
      "tools": true,
      "streaming": true,
      "json_object": true,
      "json_schema": true,
      "unsupported_params": ["top_p", "stop", "presence_penalty/frequency_penalty"]
    }
  }
}

Errors, strict and lenient

Errors are OpenAI-shaped: {"error": {"message", "type", "code", "param"}}. By default the schema is strict - a field we cannot honour is refused with a named 400 that says the fix, never silently dropped. If a framework you do not control sends fields like n or logit_bias, send the header X-Compat-Mode: lenient and those recognised-but-unsupported fields are dropped instead - with their names echoed in final_router.dropped_params so nothing ever disappears without a trace. Typos are refused in both modes. The full field and error tables live in the API reference.

Rate limits

Trial keys: 20 requests a minute. Funded keys: 60 a minute plus 6 for every dollar of balance, capped at 600. Every 429 carries RateLimit-* and Retry-After headers so clients can back off mechanically. The free tier answers 25 requests a day on the two starter models - enough to smoke-test an integration before funding it.

Model aliases - never redeploy for a model bump

<provider>/best, <provider>/fastest and <provider>/cheapest resolve at request time - by quality score, median latency and combined price respectively - so anthropic/best quietly becomes the next Claude the day it ships, with no code change on your side. The resolution is stated in final_router.reason on every response, because an alias is a standing order, not a silent substitution. Aliases respect your account’s model switches; auto remains the cross-provider version of the same idea.

MCP server

Building with an AI coding assistant? Give it live Final Router data while it writes your integration:

Claude Code
claude mcp add --transport http finalrouter https://finalrouter.com/api/mcp

Four tools, all public data: list_models (the catalogue with capabilities and aliases), get_model, estimate_cost and provider_status (live probe health). No API key needed - it serves nothing the site doesn’t already say in public. Per-tool setup for Cursor, Claude Desktop, Cline, Roo and Gemini CLI lives on the MCP page.

Attribution headers

Two optional headers label traffic as yours: X-Title names which of your applications made the call, and X-Session-Id names the run or conversation it belongs to - it also scopes per-session spend and request caps, which is how an agent product puts a hard ceiling under one runaway job. The user and tags body fields do the same for your end customers and your own labels, and every screen in the dashboard can group cost by them.