> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nano-gpt.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from OpenRouter

> Change the base URL and key, then check the few things that work differently.

NanoGPT takes the same Chat Completions requests as OpenRouter, so basic chat, streaming and tool calls usually move with one line. This page lists what stays the same and what doesn't, so you can decide before you switch.

We ran the requests behind this page against both services on November 6, 2026, and rechecked OpenRouter's published details on November 7. OpenRouter can change at any time, so its [API reference](https://openrouter.ai/docs/api-reference/overview) is the source for its side.

## The one-line change

Change the base URL and use a NanoGPT key from the [API page](https://nano-gpt.com/api).

```python Python theme={null}
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.nano-gpt.com/api/v1",  # was https://openrouter.ai/api/v1
    api_key=os.environ["NANOGPT_API_KEY"],       # was OPENROUTER_API_KEY
)

reply = client.chat.completions.create(
    model="openai/gpt-6.1-sol",
    messages=[{"role": "user", "content": "Hello!"}],
)
print(reply.choices[0].message.content)
```

```typescript TypeScript theme={null}
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.nano-gpt.com/api/v1", // was https://openrouter.ai/api/v1
  apiKey: process.env.NANOGPT_API_KEY,        // was OPENROUTER_API_KEY
});

const reply = await client.chat.completions.create({
  model: "openai/gpt-6.1-sol",
  messages: [{ role: "user", content: "Hello!" }],
});
console.log(reply.choices[0].message.content);
```

```bash curl theme={null}
curl https://api.nano-gpt.com/api/v1/chat/completions \
  -H "Authorization: Bearer $NANOGPT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "openai/gpt-6.1-sol", "messages": [{"role": "user", "content": "Hello!"}]}'
```

## Check these four things first

1. **Your model IDs.** All 10 models in OpenRouter's weekly top ten on November 7 had the same ID here. About half of its full catalog matched exactly, and some others still resolve. See [Model IDs](#model-ids).
2. **Your `provider` settings.** The object works, but selecting a provider adds a markup, normally 5%, and provider IDs can differ. See [Routing](#routing).
3. **How you read cost.** `usage.cost` can be missing, and it isn't always in USD. See [Cost and usage](#cost-and-usage).
4. **Your prices.** NanoGPT has no percentage fee on card top-ups, though tax can apply at checkout, and many popular models have the same list price on both services. But prices differ by model, in both directions, and some models cost more here even after OpenRouter's fee. Compare the models you use on [API pricing](https://nano-gpt.com/api-pricing) before you move traffic.

## Mapping table

| OpenRouter | NanoGPT | What to do |
| - | - | - |
| `https://openrouter.ai/api/v1` | `https://api.nano-gpt.com/api/v1` | Change it |
| `Authorization: Bearer` | Same. `x-api-key` also works | Nothing |
| `model` | Same format | [Check your IDs](#model-ids) |
| `models` fallback list | Works, with up to 4 IDs | [Fewer errors move on](#routing) |
| `route: "fallback"` | Accepted, no effect | Remove it |
| `provider.order`, `only`, `ignore`, `allow_fallbacks`, `sort`, `max_price`, `quantizations`, `zdr` | Work on models that support provider selection | [Check IDs and the markup](#routing) |
| `provider.preferred_min_throughput`, `preferred_max_latency` | Work alongside another routing control. Alone they do nothing | Add `provider.sort` |
| `provider.require_parameters` | Applies only on routes that can check parameters | Don't rely on it as a guarantee |
| `provider.data_collection: "deny"` | Refused with `400` | Use `provider.zdr: true`, a stricter limit that can rule out some models or cost more |
| `transforms: ["middle-out"]` | Accepted, no effect | [Trim long prompts yourself](#known-differences) |
| `plugins` with `web` | Accepted, no effect | Use the `:online` suffix |
| `plugins` with `file-parser` | Not needed for ordinary PDFs, which are read for you. You can't choose a parser engine | Remove it |
| `reasoning` | `effort`, `max_tokens`, `exclude` and `enabled` work | [Check the default](#reasoning) |
| `usage: {"include": true}` | Works | [Read the cost notes](#cost-and-usage) |
| `HTTP-Referer`, `X-Title` | Accepted and ignored | Nothing, or remove them |
| `@preset/name` as the model | Works once you [create that preset here](https://docs.nano-gpt.com/api-reference/miscellaneous/model-presets.md) | Recreate your presets |
| `"preset": "@preset/name"`, `model@preset/name` | Refused with `400` | Send `"model": "presets/name"`. The preset holds its own model, so set it there |
| `preset` with a bare name | Ignored, as on OpenRouter | Remove it |
| `GET /generation?id=` | Not available | [Use the request billing lookup](#cost-and-usage) |
| `GET /models` | Different fields and price units | [Use `?detailed=true`](#models-endpoint) |
| `GET /models/{id}/endpoints` | `GET /api/models/{id}/providers`, with the ID URL-encoded | Update the call |
| `GET /credits` | `POST /api/check-balance` | Update the call and how you read the response |
| `GET /key` | `GET /api/v1/api-keys/current` | Update the call and how you read the response |

## Model IDs

Check an ID with the public catalog:

```bash theme={null}
curl -s https://api.nano-gpt.com/api/v1/models | jq -r '.data[].id' | grep -Fx 'anthropic/claude-opus-5.5'
```

Some IDs that aren't listed still work. For example, `google/gemini-2.5-flash` works, while the catalog lists `gemini-2.5-flash`. If you get `model_not_supported`, look up the model on the [models page](https://nano-gpt.com/models/text).

| OpenRouter | NanoGPT |
| - | - |
| `:nitro` | Sorts providers by throughput, like `:throughput`. On OpenRouter it also [allows priority-tier endpoints](https://openrouter.ai/docs/guides/routing/provider-selection); here it doesn't |
| `:floor` | Sorts providers by price, like `:price`. On OpenRouter it also allows flex-tier endpoints; here it doesn't |
| `:online` | Works. Search is billed separately |
| `:free` | Not available. Pick a model from the catalog |
| `:exacto`, `:extended` | Not available. Remove the suffix and pick the model again |
| `openrouter/auto` | Use `auto-model`, which picks models its own way, so test it first |
| `openrouter/free` | Not available |
| `~vendor/model-latest` | Not available. Use a specific ID |
| Dated `canonical_slug` IDs | Not available. Use `id` |

These stay errors on purpose. Rerouting one could bill you for a model you didn't pick. For most of them, the error message says what to send instead.

`:nitro`, `:floor` and the other routing suffixes work only on models that support provider selection, and they're billed like selecting a provider. See [Model Suffixes](https://docs.nano-gpt.com/api-reference/miscellaneous/model-suffixes.md).

## Routing

The `provider` object uses the same field names. Three things differ:

* **Selecting a provider costs more.** With `provider` routing controls or a routing suffix, billing uses that provider's price plus a markup, normally 5%, and the request is treated as pay-as-you-go. Leave `provider` out to pay the model's default price. See [Billing and Markup](https://docs.nano-gpt.com/api-reference/miscellaneous/provider-selection.md#billing-and-markup).
* **Provider IDs can differ.** Find a model's IDs with `GET /api/models/{id}/providers`. An unknown ID in `provider.only` returns `400`. In `provider.order` it's ignored, unless `allow_fallbacks` is `false`. Then `order` is the full list and is enforced like `only`, so saved preferences can't add to it: if none of its IDs is usable, or none is also in `only`, the request returns `400` instead of going to a provider you didn't list. That also applies to models that don't support provider selection: if their fixed route isn't in the list, you get `400` `no_allowed_provider_for_model`.
* **Fewer errors move to the next model.** `models` tries the next model after a provider failure or a prompt that's too long for that model. Authentication, balance and invalid-request errors end the request. A first model that doesn't meet your `provider` limits also ends the request, while OpenRouter moves on. See [Model fallbacks](https://docs.nano-gpt.com/api-reference/miscellaneous/provider-selection.md#model-fallbacks).

If **Enable paid models on API** is off for a subscription, `provider` is dropped so the request stays on the plan, and `only` and `ignore` aren't applied. `provider.zdr` still applies. `data_collection: "deny"` and `order` with `allow_fallbacks: false` return `400`, because the plan can't honor them.

NanoGPT doesn't return the provider that served a request: there's no `provider` field in responses or chunks, and no `metadata.provider_name` in errors. The [Usage page](https://nano-gpt.com/usage) shows a provider when public attribution is available.

## Cost and usage

**Send `usage: {"include": true}` if you read `usage.cost`.** On a non-streaming request, NanoGPT then waits briefly for billing to finish, so the field is usually present. `"include_usage": true` does the same. Without either, `usage.cost` is present only if billing finished before the response was ready.

```json theme={null}
"usage": {
  "prompt_tokens": 10,
  "completion_tokens": 2,
  "total_tokens": 12,
  "cost": 0.0000013,
  "currency": "USD"
}
```

Read the result like this:

* **`usage.currency` matters.** `usage.cost` is in `USD` or, if the request was paid from a Nano balance, `XNO`. On OpenRouter it's always in credits, which track USD.
* **`usage.cost` can still be missing.** If billing takes longer than the wait, or the response came through an internal fallback, there may be no `usage.cost`. `x_nanogpt_pricing` then holds an estimate with `isEstimate: true`. Don't treat an estimate as the charge.
* **`0` is a real value.** A request covered by a subscription reports `0`.
* **`cost_details` and `is_byok` aren't returned on Chat Completions.**
* **Embeddings report tokens but no cost.** Use the [Usage API](https://docs.nano-gpt.com/api-reference/endpoint/usage.md).

**For streams,** send `stream_options: {"include_usage": true}` or `usage: {"include": true}`. OpenRouter always sends usage. NanoGPT sends it only when asked, or when prompt caching is on. The usage object can share the last chunk with `finish_reason`, so don't wait for a chunk with empty `choices`.

If you use `@openrouter/ai-sdk-provider`, set `compatibility: "strict"`. In its default mode, token counts come back empty.

**To look up a charge later,** replace `GET /generation` with [`GET /api/v1/usage/requests/{request_id}`](https://docs.nano-gpt.com/api-reference/endpoint/request-billing):

| | `GET /generation` | `GET /api/v1/usage/requests/{request_id}` |
| - | - | - |
| ID to send | Response `id` | `X-Request-ID` response header |
| After a `models` fallback | Response `id` | `x_nanogpt_pricing.requestId` |
| Available for | See OpenRouter's docs | 24 hours, with the same API key |
| Cost | Number in USD | Decimal string in `currency` |
| Provider, latency, native tokens | Returned | Not returned |

The response `id` doesn't work here, and neither does `X-Request-ID` after a fallback: it keeps the first attempt's ID, and `x_nanogpt_pricing.requestId` has the ID of the attempt that answered. Don't send your own `X-Request-ID` on chat completions, or the lookup can't find the charge by it.

## Reasoning

`reasoning.effort`, `max_tokens`, `exclude` and `enabled` work, and so does `include_reasoning`. Reasoning comes back in `message.reasoning`. `reasoning_details` depends on the model, so don't require it.

Two differences:

* **Thinking can be off by default.** Some models think by default on OpenRouter but not here. To turn it on, send `reasoning: {"enabled": true}` or add the `:thinking` suffix. See [Extended Thinking](https://docs.nano-gpt.com/api-reference/miscellaneous/extended-thinking.md).
* **Invalid values aren't always refused.** An unknown `reasoning.effort` returns `200`. On a model that always thinks, `reasoning.enabled: false` hides the reasoning, but the tokens are still counted.

## Models endpoint

`GET /api/v1/models` returns only `id`, `object`, `created` and `owned_by`. Add `?detailed=true` for everything else.

| Field | OpenRouter | NanoGPT with `?detailed=true` |
| - | - | - |
| `pricing.prompt`, `pricing.completion` | USD per token, as a string | USD per 1M tokens, as a number |
| `context_length` | Yes | Yes |
| `architecture` | Yes | Yes, without `tokenizer` or `instruct_type` |
| `supported_parameters` | Yes | No. Use `capabilities` and `reasoning_efforts` |
| `top_provider`, `per_request_limits` | Yes | No. Use `max_output_tokens` and `providers` |
| `canonical_slug` | Yes | No |
| Query filters such as `supported_parameters` | Yes | Ignored |

> [!WARNING]
>
> If your code computes cost from `pricing`, divide NanoGPT's values by 1,000,000 instead of using them as a per-token price. Otherwise your estimates will be a million times too high.

## Errors

Errors use the same outer shape, but `error.code` is a string instead of a number:

```json theme={null}
{
  "error": {
    "message": "Model example/model is not supported on /v1/chat/completions.",
    "type": "invalid_request_error",
    "param": "model",
    "code": "model_not_supported"
  }
}
```

Branch on the HTTP status, or on the string code. `metadata` isn't returned. Some statuses differ for the same cause: an unavailable provider is `400` here and `404` on OpenRouter, and a zero data retention conflict is `403` here. See [Error Handling](https://docs.nano-gpt.com/api-reference/miscellaneous/error-handling.md).

## Known differences

* **Long prompts.** `transforms` has no effect. A prompt that's too long returns `400` `context_length_exceeded`, so trim it yourself or add a longer-context model to `models`.
* **Web search.** `plugins` has no effect, so `plugins: [{"id": "web"}]` runs no search and raises no error. Use `:online`. `web_search_options` does turn on search here, and search is billed.
* **Stop sequences.** If a `stop` string matches the very start of the answer, NanoGPT can return `502` `empty_response`, while OpenRouter returns an empty success.
* **Structured output.** Support depends on the model. A model without it returns `400` `model_response_format_unsupported`.
* **Response `model`.** Suffixes such as `:floor` stay in the response's `model`. OpenRouter returns the base ID.
* **`native_finish_reason`.** Not returned. Use `finish_reason`.
* **Validation.** Unknown keys in `provider` and invalid values in some fields are accepted without an error. A `200` doesn't prove a setting took effect.
* **App attribution.** `HTTP-Referer` and `X-Title` don't create an app page or a ranking entry.

Something missing or wrong here? [Tell us](https://nano-gpt.com/support) what you ran into.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.