Skip to content
docs/chatCore API

Chat

Chat completions in OpenAI's shape, with automatic failover behind every model.

POST /v1/chat/completions takes OpenAI's request and returns OpenAI's response. Point an OpenAI client at this base URL, change the model to a slug from GET /v1/models, and the rest of the code is unchanged. The gateway picks a route that can serve that model, falls back to the next when one fails, and says in the response headers which one answered.

A first request

cURL
curl https://api.routehook.ai/v1/chat/completions \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-4o-mini",
    "messages": [
      { "role": "user", "content": "Hello" }
    ]
  }'

POST/v1/chat/completionsAvailable

Chat completions in OpenAI's shape, streamed or whole, with automatic failover.

AUTHENTICATION
Bearer token, Authorization header
REQUIRED SCOPE
api-key

Request parameters

PARAMETERTYPEREQUIREDDESCRIPTION
modelstringrequiredModel slug from GET /v1/models. Openai/gpt-4o-mini, not gpt-4o-mini.
messagesarrayrequiredAt least one turn. Roles are system, developer, user, assistant and tool.
streambooleanoptionalEmit server-sent events instead of one body. Default false.
stream_optionsobjectoptionalOnly include_usage is read. It adds a final frame carrying usage and cost.
max_tokensintegeroptionalCeiling on generated tokens. Also caps what is reserved before the call runs.
max_completion_tokensintegeroptionalOpenAI's newer spelling of the same ceiling.
temperaturenumberoptional0 to 2. Forwarded unchanged.
top_pnumberoptional0 to 1. Forwarded unchanged.
top_kintegeroptional0 or greater. Models that do not offer it ignore it.
frequency_penaltynumberoptional-2 to 2.
presence_penaltynumberoptional-2 to 2.
repetition_penaltynumberoptional0 to 2.
seedintegeroptionalBest-effort determinism, where the model supports it.
stopstring | string[]optionalUp to eight sequences that end generation when produced.
nintegeroptional1 to 8 completions. Multiplies the reservation and the bill.
logprobsbooleanoptionalReturn log probabilities for the tokens that came back.
top_logprobsintegeroptional0 to 20. Needs logprobs.
toolsarrayoptionalTool definitions, in OpenAI's function schema.
tool_choicestring | objectoptionalauto, none, required, or a named function.
parallel_tool_callsbooleanoptionalAllow more than one tool call in a single turn.
response_formatobjectoptionaljson_object or json_schema, where the model supports it.
reasoning_effortstringoptionalForwarded to reasoning models that read it.
userstringoptionalUp to 256 characters. An opaque end-user id, forwarded upstream.
modelsstring[]optionalUp to eight fallback model slugs, tried in order after model.
providerobjectoptionalProvider preferences: only, ignore, order, sort, allow_fallbacks, require_parameters, max_price. They narrow or reorder the chain, never extend it.
routefallbackoptionalOpenRouter's spelling of what models already does. Accepted for compatibility; the models array is the field to reach for.
logit_biasobjectoptionalToken id to bias, forwarded to models that read it.
min_pnumberoptional0 to 1. Forwarded unchanged.
top_anumberoptional0 to 1. Forwarded unchanged.
predictionobjectoptionalPredicted output, for models that support speculative decoding.
transformsstring[]optionalParsed but refused: a non-empty array answers 400 invalid_request rather than silently leaving your prompt unchanged.
pluginsobject[]optionalSame. A non-empty array answers 400 rather than dropping the plugin quietly.

Messages

Each turn carries a role and content. content is a string, or an array of parts for multimodal input: a text part, an image_url part taking a URL or a data URI, and an input_audio part. Part types newer than this gateway are forwarded as they arrive rather than refused. A tool turn carries the tool_call_id it answers; an assistant turn that asked for a tool carries tool_calls and may have content: null.

Model fallback

models is a list of slugs to try after the first, and it is the only fallback lever you hold: the whole chain behind model is exhausted before the second slug is considered at all. Use it where a second model is an acceptable answer to the same question (a cheaper sibling, or another vendor's equivalent), and price it accordingly. The second model has its own rate, so a request that falls through costs what the model that answered costs, not what you budgeted for the first.

Falling back to another model

JSONRequest body
{
  "model": "openai/gpt-4o-mini",
  "models": ["anthropic/claude-sonnet-4"],
  "messages": [{ "role": "user", "content": "Summarise this in one line." }]
}

What the response headers say

A fallback is otherwise invisible: you get an ordinary 200 with no sign that the first attempt failed. Every response carries headers describing what your own call did. How many attempts, whether it fell back, what it cost, how long the upstream leg took. None of them names the host that answered.

HEADERMEANING
X-Routehook-Request-IdThe request id. Quote it in support, and pass it to GET /v1/generation.
X-Routehook-AttemptsHow many attempts the request took.
X-Routehook-Fallbacktrue when the first choice did not answer and another did.
X-Routehook-CostUSD charged. Absent on a stream. The cost is not known when the headers go out.
X-Routehook-Upstream-Latency-MsHow long the upstream leg took, excluding our own overhead.

What it cost

The body carries the charge too. usage.cost follows OpenRouter as a JSON number; usage.cost_decimal is Routehook's exact decimal-string extension for accounting code. usage.prompt_tokens_details.cached_tokens and usage.completion_tokens_details.reasoning_tokens are broken out where the upstream reports them, because both are billed differently from ordinary tokens.

The response

JSON200 OK
{
  "id": "chatcmpl_5f81c0",
  "object": "chat.completion",
  "created": 1786312455,
  "model": "openai/gpt-4o-mini",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "Hello. How can I help?" },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 9,
    "completion_tokens": 8,
    "total_tokens": 17,
    "cost": "0.0000042"
  }
}

Tool calling

Tools are OpenAI's shape and pass through unchanged. The model answers with finish_reason: "tool_calls" and one or more tool_calls on the assistant message; you run them, append one tool message per call carrying the matching tool_call_id, and send the whole conversation back. Each hop is a separate billed request. A three-hop tool conversation is three completions, not one, and every hop re-sends the growing message list as prompt tokens.

A tool round trip

JAVASCRIPTtools.js
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.routehook.ai/v1",
  apiKey: process.env.ROUTEHOOK_API_KEY,
});

const tools = [
  {
    type: "function",
    function: {
      name: "get_tide",
      description: "Tide height in metres for a port, right now.",
      parameters: {
        type: "object",
        properties: { port: { type: "string" } },
        required: ["port"],
      },
    },
  },
];

const messages = [{ role: "user", content: "What is the tide at Felixstowe?" }];

const first = await client.chat.completions.create({
  model: "openai/gpt-4o-mini",
  messages,
  tools,
  // Skip any endpoint that has not declared tool support, rather than
  // sending the tools and getting prose back from one that ignores them.
});

const call = first.choices[0].message.tool_calls?.[0];
messages.push(first.choices[0].message);

if (call) {
  const { port } = JSON.parse(call.function.arguments);

  messages.push({
    role: "tool",
    tool_call_id: call.id,
    content: JSON.stringify({ port, metres: 2.4 }),
  });

  const second = await client.chat.completions.create({
    model: "openai/gpt-4o-mini",
    messages,
    tools,
  });

  console.log(second.choices[0].message.content);
}

n multiplies the bill

n asks for that many completions of one prompt, between 1 and 8, and is forwarded verbatim. The prompt is charged once; every completion is charged in full, so n: 4 costs about four times the output side of n: 1. The reservation taken before the call is multiplied to match, which means a large n against a thin balance can be refused with insufficient_credits before anything runs. Not every model honours it: where one does not you get a single choice back, and pay for one.

Failures worth handling

409 model_unavailable means nothing could serve that model. A wrong slug, or a model with no live route right now. 402 insufficient_credits is refused before any upstream call is made, so nothing was spent. 503 upstream_unavailable means every endpoint in the chain failed; it is safe to retry, and sending an Idempotency-Key stops a retry reserving the money a second time.