How it works

What happens between your code and the model.

Every request to /v1/chat/completions goes through the same six steps. This page describes them as the gateway actually runs them, including the parts other gateways leave out: what we retry, what we don't, and exactly how a request is charged.

Request lifecycle

A request is checked and paid for before it reaches a provider, and settled once the response is complete. Nothing is sent upstream until your key, its rules and your balance have all passed.

  1. 01

    Authenticate

    Key hash lookup, account active, email verified

  2. 02

    Apply key rules

    Model allowlist, monthly spend cap

  3. 03

    Reserve cost

    Worst case held from your balance

  4. 04

    Dispatch

    Forwarded upstream; retried on transient errors

  5. 05

    Stream back

    Tokens relayed as they arrive

  6. 06

    Settle & log

    Actual cost charged, reservation released

The six stages of a chat completion request
  1. 1

    Authenticate

    The key is hashed (SHA-256) and looked up; the raw key is never stored. Inactive keys, deactivated accounts and accounts with an unverified email are refused.
  2. 2

    Apply the key's rules

    If the key has a model allowlist, the model id must be on it. If it has a monthly spend cap, this month's spend plus the request's estimated cost must fit under it.
  3. 3

    Reserve the worst-case cost

    The most the request could cost is held from your balance, so concurrent requests can't overdraw it. See cost reservation.
  4. 4

    Dispatch upstream

    Only the fields the API accepts are forwarded. Your request headers are not passed on.
  5. 5

    Relay the response

    Non-streaming responses are returned as one JSON body; streams are relayed chunk by chunk.
  6. 6

    Settle and log

    The real cost replaces the reservation, and a log row is written with the model, token counts, cost, latency and status. Prompt and completion text is not logged.

Endpoints and authentication

API endpoints
EndpointAuthPurpose
POST /v1/chat/completionsAPI keyChat completions, streaming or not
GET /v1/modelsNoneThe live catalog: ids, prices, context windows
GET /v1/models/{id}NoneOne model by id

Send your key as Authorization: Bearer sk-ng-… (what every OpenAI SDK does) or as an X-API-Key header. Chat completions is the only generation endpoint: there are no embeddings, image or audio endpoints. Vision works through image parts in chat messages.

Model ids

A model id is provider/model, for example anthropic/claude-sonnet-5.5 or openai/gpt-6-sol. The match is exact: no case folding and no guessing. An unknown or retired id returns 404.

Kinds of model id
KindExampleBehaviour
Pinnedopenai/gpt-6-solAlways that model. Use it when output must stay stable.
Latest alias~anthropic/claude-sonnet-latestFollows the provider's newest release in that family. Logged and billed under the alias id at its listed price.
Dated snapshotmistralai/mistral-large-2512A fixed release, for reproducible evaluations.

The catalog syncs daily. Free (:free) variants and models without a fixed per-token price are never added, and a model whose upstream price becomes variable is taken offline rather than billed at a guess.

Cost reservation

Before dispatch, the gateway estimates the most the request could cost and holds that amount. The hold is an atomic database update that only succeeds if your available balance covers it, so two requests can't both spend the same dollar.

How the reservation is estimated
PartEstimate
InputSize of your messages and tool definitions, at a conservative 3 bytes per token
OutputYour max_tokens, capped at the model's maximum output; without max_tokens, the model's maximum
PriceThe model's listed input and output price per 1M tokens

When your balance is low

If you didn't set max_tokens and the worst case is more than your balance, the output budget is shrunk to what you can afford (never below 256 tokens) and sent upstream as max_tokens. If even that doesn't fit, the request is refused with 402. Setting max_tokens yourself keeps reservations small and predictable.

Upstream dispatch and retries

Requests are forwarded to OpenRouter, which serves the model from one of the providers hosting it. Several providers often host the same open-weight model; choosing between them is OpenRouter's job, and it moves traffic away from a host that is failing. The model never changes: a request for one model is only ever served by that model.

What is retried
SituationNon-streamingStreaming
502, 503 or 529 from upstreamRetried up to 2 times (after 1s, then 2s)Not retried
Connection error or timeoutRetried up to 2 timesNot retried
500, 504, 429 or any 4xxReturned to youReturned to you
All attempts failError returned; you pay $0Error event in the stream; you pay $0

Retries are safe because nothing is billed until a response completes. Streams aren't retried because tokens may already have reached you.

Falling back to another model

NeuronGate does not switch to a different model when one fails: a silent swap would change output quality, price and behaviour without telling you. If you want a fallback chain, keep it in your code where you can see and test it. Because every model uses the same API and the same key, it's a short loop:

from openai import OpenAI, APIStatusError, APIConnectionError

client = OpenAI(base_url="https://neurongate.net/v1", api_key="sk-ng-...")

# Your order of preference. NeuronGate never swaps models for you.
MODELS = ["~anthropic/claude-sonnet-latest", "~openai/gpt-latest", "deepseek/deepseek-v4-pro"]

def complete(messages):
    last_error = None
    for model in MODELS:
        try:
            return client.chat.completions.create(model=model, messages=messages)
        except APIConnectionError as e:
            last_error = e
        except APIStatusError as e:
            # 402 (balance/cap) and 4xx won't succeed on another model: stop.
            if e.status_code < 500 and e.status_code != 429:
                raise
            last_error = e
    raise last_error

Don't fall back on 402

A 402 means your balance or the key's monthly cap can't cover the request. Another model won't fix that, and retrying in a loop just repeats the refusal.

Streaming

With stream: true the response is server-sent events in the OpenAI chunk format. The gateway always asks the upstream for a final usage chunk, so token counts arrive even if you didn't set stream_options.include_usage. Every data: event is a standard chunk, so the OpenAI SDKs' usual chunk.choices[0] loop works as is:

Raw event stream
: neurongate request_id=6957bbea-… model=~anthropic/claude-sonnet-latest adapter=openrouter

data: {"id":"gen-…","choices":[{"index":0,"delta":{"content":"Hel"}}]}

data: {"id":"gen-…","choices":[{"index":0,"delta":{"content":"lo"}}]}

data: {"id":"gen-…","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":12,"completion_tokens":2,"total_tokens":14}}

data: [DONE]
  • Gateway details (the request id, model and route) arrive as an SSE comment line starting with :, and keep-alives during long silences as : ping. SSE clients ignore comment lines. The request id is also in the X-Request-ID response header.
  • An upstream failure after the stream has started arrives as an {"error": {...}} event followed by [DONE], because the HTTP 200 has already been sent.
  • If you disconnect mid-stream, the tokens generated so far are still billed. The provider charges for them either way.

Billing and logging

A request's charge is computed when it completes:

How a request is charged
computed = input_tokens  × input_price  / 1,000,000
         + output_tokens × output_price / 1,000,000

charged  = max(computed, cost reported by the upstream)

The upstream's reported cost matters for models with prompt-length price tiers, where long prompts cost more than the listed base price. Arithmetic is done in exact decimals, not floating point. Failed requests cost nothing and their reservation is released.

Each request is logged with the model, input and output tokens, cost, latency, status and request id. That is what powers the logs and activity pages and the CSV export. The prompt and completion are not part of the log.

Idempotent retries

A network failure can leave you unsure whether a request ran. For non-streaming requests, send an Idempotency-Key header: a retry with the same key and the same body returns the original response instead of running (and billing) the request again.

cURL
curl https://neurongate.net/v1/chat/completions \
  -H "Authorization: Bearer sk-ng-..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-4821-summary" \
  -d '{"model":"deepseek/deepseek-v4-flash","messages":[{"role":"user","content":"Summarise order 4821"}]}'
  • Reusing a key with a different body returns 409.
  • The stored response is kept for 24 hours so retries can be answered, then deleted. Requests without the header store nothing.

Limits and timeouts

Limits
LimitValue
Chat completions per API key60 requests per minute
Request body10 MB
Completions per request (n)1
Upstream connect timeout10 seconds
Wait between bytes, non-streaming120 seconds
Wait between bytes, streaming300 seconds

Timeouts apply to silence, not total length: a long generation that keeps producing tokens isn't cut off. Need a higher request rate? Spread work across several keys, or get in touch.

Errors

Gateway errors use one JSON shape. detail is usually a string; for key-rule errors it is an object with a machine-readable type. Errors from the provider itself are passed through with their original status.

Error body
{
  "error": {
    "status_code": 402,
    "detail": "Insufficient balance",
    "request_id": "6b0d…"
  }
}
Status codes
StatusMeaningWhat to do
400Model not in this key's allowlist (model_not_allowed)Use an allowed model or edit the key
401Missing, unknown or revoked keyCheck the key; create a new one if revoked
402Insufficient balance, or the key's monthly cap is reachedTop up, or raise the cap
403Account email not verifiedVerify your email
404Unknown or retired model idPick an id from /v1/models
409Idempotency-Key reused with a different bodyUse a new key for a new request
413Body over 10 MBShrink images or context
422Invalid request (unknown field, bad value)The fields list in the response says which
429Rate limit or account spend limit reachedBack off and retry, or wait for the limit to reset
502Upstream failed after retriesRetry later or fall back in your code

FAQ

Does NeuronGate pick a cheaper or faster model for me?

No. The model you name is the model that runs. Routing decides which host serves that model, never which model answers.

Do retries cost extra?

No. A request is billed once, when a response completes. Failed attempts and failed requests cost nothing.

Why was my request refused when my balance looked big enough?

The gateway reserves the worst case before dispatch: your input plus the maximum output the request allows. Set max_tokens to what you actually need and the reservation shrinks to match.

Do you cache prompts or responses?

No response caching. The only stored response is the 24-hour Idempotency-Key cache, and only when you send that header.

How much latency does the gateway add?

The checks before dispatch are a handful of database and cache lookups. Most of the time to first token is the model itself. Each request's total latency is shown in your logs.