Chat Completions

POST /v1/chat/completions — parameters, response shape and streaming.

POSThttps://iranrouter.com/v1/chat/completions

The main surface. OpenAI-compatible, so the request and response shapes are what SDKs already expect.

Parameters

NameTypeNotes
modelstringRequired. Model ID or alias; or @preset/slug; or iranrouter/auto:cheap | balanced | best (below).
modelsarrayFallbacks. If model cannot answer and nothing has been sent yet, the next is tried; each model is its own request with its own bill. The chain is at most three models in total, model included; the rest are ignored. X-IranRouter-Model-Used and the response's model field name the one that answered; X-IranRouter-Models-Tried lists the models that stepped aside with their error code (slug:code), then the one this response comes from — when every model failed, the last one tried, whose error this is. On the usage page the whole chain is ONE request: the skipped models open under it and do not count as failures.
messagesarrayRequired. Messages with role system / user / assistant / tool.
max_tokensintOutput ceiling. Directly drives the amount held from your wallet — set it realistically.
max_completion_tokensintThe newer clients' spelling of max_tokens; both are honoured.
streamboolSSE streaming response.
temperaturenumberSampling randomness.
top_pnumberNucleus sampling.
toolsarrayTool definitions. Their schemas count as input tokens.
tool_choicestring|objectForce or free the tool choice.
response_formatobjectStructured / JSON output.
stopstring|arrayStop sequences.
seedintFor reproducibility, where the model supports it.
Each model accepts its own subset. The exact supported-parameter list is on that model's page; unknown parameters are ignored rather than rejected.

Auto router (iranrouter/auto)

Three fixed IDs: iranrouter/auto:cheap, iranrouter/auto:balanced and iranrouter/auto:best (iranrouter/auto without a tier is balanced). Each is a chain of up to three models we maintain and revise over time; your request runs like a models chain: the first model, then the next if it could not answer or your key, plan or a guardrail does not allow it. You always pay the price of the model that answered, named in X-IranRouter-Model-Used, in the response's model field and on the usage page; a tier has no price of its own. The tiers are listed by GET /v1/models, without a price. A tier that currently has no models is 404 model_not_found. If you put your own model in front of a tier (your model, the tier in models), the cap of three leaves room for two of the tier's, and a refusal of your own model is still a 403. In a tool loop, the model that answered the first turn takes every later turn. To choose the models yourself, use models or a preset.

Idempotency-Key

On a non-streaming request, an Idempotency-Key header (up to 200 characters) answers a retry from memory and never bills twice. A replayed response carries x-idempotent-replay: true. While the original is still running you get 409 idempotency_in_flight; if the original response was too large to store, 409 idempotency_completed_unreplayable. Ignored on streams.

Conversation and app (session_id, X-Title)

A session_id in the body, or an x-session-id header (up to 256 characters), ties the turns of one conversation together: they stay on one route so the prompt cache stays warm and the repeated prefix costs less, and the usage log finds them by that id. Without it, prompt_cache_key, then user (or metadata.user_id), then a fingerprint of the conversation's opening is used; the X-IranRouter-Session response header says which. session_id and prompt_cache_key are stored as sent so you can find your own session — do not put personal data in them; user and the fingerprint are stored only as a keyed hash. The value of none of these fields goes upstream; the upstream sees only an opaque key that is never shared between customers.

The X-Title and HTTP-Referer headers name your app, so usage can be read per app. Only the origin of the referer is kept.

Route preference (provider)

A provider object in the body orders or restricts the upstream routes. It depends on the plan: without it the field is silently ignored and the response carries an explanatory X-IranRouter-Provider-Prefs header; with it, an invalid value is a 400 with code invalid_provider_prefs. The field is never forwarded upstream.

Response

JSON
{
  "id": "chatcmpl-...",
  "object": "chat.completion",
  "created": 1767000000,
  "model": "openai/gpt-4o-mini",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "..." },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 24,
    "completion_tokens": 108,
    "total_tokens": 132,
    "prompt_tokens_details": { "cached_tokens": 0 },
    "completion_tokens_details": { "reasoning_tokens": 0 },
    "cost_rial": 1840
  }
}

The usage.cost_rial field is this request's final charge in Rial. The class counts come both under OpenAI's names — prompt_tokens_details.cached_tokens (plus cache_write_tokens, audio_tokens, image_tokens where present) and completion_tokens_details.reasoning_tokens (plus audio_tokens, image_tokens) — and under our flat names: cached_input_tokens, cache_write_tokens, reasoning_tokens and so on. On a stream the same object arrives on the last chunk.

The non-streaming path returns tool_calls, logprobs, refusal and n>1 in full.

Full example

Shell
curl https://iranrouter.com/v1/chat/completions \
  -H "Authorization: Bearer ir-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-4o-mini",
    "messages": [
      {"role": "system", "content": "You answer in Persian."},
      {"role": "user", "content": "پایتخت ایران کجاست؟"}
    ],
    "max_tokens": 200,
    "temperature": 0.4
  }'

Plugins and transforms

Two optional fields, under the names OpenRouter's clients already send. Both are run by us; an unknown name is a 400, so a plugin that does nothing is never mistaken for one that is broken.

FieldWhat it does
transforms: ["middle-out"]If the conversation does not fit the model's context window, messages are dropped from the middle until it does: the system prompt and the last turn always stay; the first user message stays too, cut in its own middle if it is the thing that does not fit. A tool call is never separated from its result, and what is sent always opens with a user turn. The X-IranRouter-Transform header says how many went. Without it, a request whose text cannot fit the window even on the most generous count gets 400 context_length_exceeded before anything is reserved; in a models chain the next model is tried.
plugins: [{"id": "response-healing"}]Non-streaming answers only (and the turns of a tool loop, which always are), and only under a response_format of json_object or json_schema. A ```json fence, prose around the JSON and a trailing comma are removed. An answer cut off by max_tokens is rolled back to its last COMPLETE element and closed: a half-written value is never kept or guessed, and finish_reason still says length. It is not re-validated against your schema. What cannot be repaired honestly is returned as it came. Free.
plugins: [{"id": "file-parser"}]Each PDF in the conversation (up to 4; by file_id or as a data URI) is turned into markdown by OCR before the model sees it. The OCR model is the operator's choice and is exempt from your key's and plan's model lists. The request counts ONCE against your RPM, not once per PDF; each PDF still has its per-page price and its own usage row. Certain refusals (a model you may not use, the RPM limit) are checked before any OCR and cost nothing; but if the chat turn is refused after the OCR — the document's text is larger than the model's window, or your wallet or budget cannot cover the chat itself — the OCR has been charged. The free plan has a daily page allowance (Tehran day): past it, 429 with code file_parser_daily_pages and a Retry-After until midnight. Your organization's content guardrail applies to the document's text too. With pdf.engine set to native the file reaches the model untouched.

For streaming → · For tools →