The one-line change
Change the base URL and use a NanoGPT key from the API page.Check these four things first
- 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.
- Your
providersettings. The object works, but selecting a provider adds a markup, normally 5%, and provider IDs can differ. See Routing. - How you read cost.
usage.costcan be missing, and it isn’t always in USD. See Cost and usage. - 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: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
Theprovider object uses the same field names. Three things differ:
- Selecting a provider costs more. With
providerrouting 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. Leaveproviderout 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 inprovider.onlyreturns400. Inprovider.orderit’s ignored, unlessallow_fallbacksisfalse. Thenorderis the full list and is enforced likeonly, so saved preferences can’t add to it: if none of its IDs is usable, or none is also inonly, the request returns400instead 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 get400no_allowed_provider_for_model. - Fewer errors move to the next model.
modelstries 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 yourproviderlimits also ends the request, while OpenRouter moves on. See Model fallbacks.
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
Sendusage: {"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.
usage.currencymatters.usage.costis inUSDor, if the request was paid from a Nano balance,XNO. On OpenRouter it’s always in credits, which track USD.usage.costcan still be missing. If billing takes longer than the wait, or the response came through an internal fallback, there may be nousage.cost.x_nanogpt_pricingthen holds an estimate withisEstimate: true. Don’t treat an estimate as the charge.0is a real value. A request covered by a subscription reports0.cost_detailsandis_byokaren’t returned on Chat Completions.- Embeddings report tokens but no cost. Use the Usage API.
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:thinkingsuffix. See Extended Thinking. - Invalid values aren’t always refused. An unknown
reasoning.effortreturns200. On a model that always thinks,reasoning.enabled: falsehides 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.
Errors
Errors use the same outer shape, buterror.code is a string instead of a number:
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.
transformshas no effect. A prompt that’s too long returns400context_length_exceeded, so trim it yourself or add a longer-context model tomodels. - Web search.
pluginshas no effect, soplugins: [{"id": "web"}]runs no search and raises no error. Use:online.web_search_optionsdoes turn on search here, and search is billed. - Stop sequences. If a
stopstring matches the very start of the answer, NanoGPT can return502empty_response, while OpenRouter returns an empty success. - Structured output. Support depends on the model. A model without it returns
400model_response_format_unsupported. - Response
model. Suffixes such as:floorstay in the response’smodel. OpenRouter returns the base ID. native_finish_reason. Not returned. Usefinish_reason.- Validation. Unknown keys in
providerand invalid values in some fields are accepted without an error. A200doesn’t prove a setting took effect. - App attribution.
HTTP-RefererandX-Titledon’t create an app page or a ranking entry.