Skip to main content
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 is the source for its side.

The one-line change

Change the base URL and use a NanoGPT key from the API page.

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.
  2. Your provider settings. The object works, but selecting a provider adds a markup, normally 5%, and provider IDs can differ. See Routing.
  3. How you read cost. usage.cost can be missing, and it isn’t always in USD. See 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 before you move traffic.

Mapping table

Model IDs

Check an ID with the public catalog:
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. 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.

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.
  • 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.
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 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.
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.
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}: 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.
  • 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.
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:
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.

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 what you ran into.