Skip to main content
“OpenAI-compatible” here means three specific things, and it is worth being exact about each of them rather than leaving you to find the edges by calling.
  1. The request and response bodies are OpenAI’s, and travel unchanged but for two rewrites named below.
  2. Errors arrive in the OpenAI error envelope, so your existing error handling parses them.
  3. What we add is additive: extra response headers and one SSE comment line, both of which a compliant client ignores.
Point your existing OpenAI client at https://ai.uniblock.dev and give it a Uniblock API key. Nothing else about your code changes.
Base URL

The endpoints that exist today

That table is the whole documented surface. A path that is not on it is one this site says nothing about, which is not the same as a path that does not exist.

What a call to a path we do not serve returns

A path under /v1 that this release does not serve is refused at one boundary, so it fails the same way every time rather than in a new way per path. It is a 404 in the OpenAI envelope, with type invalid_request_error and code endpoint_not_supported:
404
A method we do not serve on a path we do is a different answer, because the boundary tells the two apart: that is a 405 with the code method_not_allowed and an Allow header naming the methods that are served. A GET on /v1/chat/completions is the everyday example.

Request parameters

The chat completion body is forwarded as you sent it. There is no request schema of ours in front of it and no allowlist of parameters, so a parameter your client already sends works without waiting for us to add it. A body the provider rejects comes back as the provider’s own OpenAI-shaped error. Two fields are read on the way through, both to work out what the request could cost before it is sent: model, and max_tokens or max_completion_tokens. Reading one does not change it. Two rewrites exist, and they are the whole list. A request naming a routing profile or unirouter/auto has its model replaced with the model being tried on that attempt, because that is what a route means. And a request that names no output ceiling, sent to a provider whose own API refuses a request without one, has max_tokens filled in with the model’s published maximum output, which is the figure credit was already being held against. Every other provider is sent your body without the field. Request parameters covers both in full. Two consequences worth stating:
  • Nothing Uniblock-specific belongs in a request. There is no parameter of ours to send, and no header can change how a request is served.
  • max_tokens is worth sending even though it is optional. It is what sizes the amount we hold against your balance before the call, so a request without it is held at a larger estimate and reaches your account spend rate cap sooner. See rate limits and spend controls.

Model ids

Models are named provider/model, for example openai/gpt-4o-mini. The id is split once on the first slash: the part before it names the provider that will serve the request, and everything after it is that provider’s own id for the model, slashes included and passed on unchanged. The same model carried by two providers is two ids, and you choose which one you send. A bare OpenAI-style name with no provider is not a Uniblock model id. It is refused with 400 and the code model_id_not_pinned, naming both accepted forms, rather than being routed to whichever provider we happen to carry it under. Ask GET /v1/models for the ids you can call.

Request headers

Send Authorization and Content-Type. Nothing else you send is forwarded: the request we make on your behalf is built from your request body and our own provider credential, with a fresh header set, so headers beyond those two are neither honoured nor passed to the provider. x-uniblock-* is our reserved namespace on both directions of the wire. Sending one has no effect for the same reason: it does not travel.

Response differences

All three differences are additions to an otherwise unchanged OpenAI response.

The request id

Every response, success or error, carries x-uniblock-request-id, and the same value on x-request-id, which is the header the OpenAI SDKs already read. On an error, the id also appears as request_id inside the error object. That fifth field inside error is the one shape difference from OpenAI’s envelope, and it is inside error rather than beside it precisely so that an SDK reading the fields it knows ignores it safely.

Diagnostic headers

A short allowlist of response headers describes how the request was served. These are the ones that ship today: Nothing else the provider sets travels onward. That is deliberate rather than incidental, and it has one consequence you will notice: provider rate limit headers are not forwarded. If you currently read the remaining-requests and reset headers that a provider sets, those are absent here, because they describe a pool we hold rather than a limit that applies to you. Your own limits arrive as 429 responses with codes you can branch on, documented in rate limits and spend controls.

The usage comment line on a stream

A streamed reply carries one extra line immediately before data: [DONE], starting with a colon:
The SSE specification requires a parser to ignore a line starting with a colon, so the OpenAI SDKs skip it and your own parser should too unless you want the counts. Streaming documents its fields. A non-streamed reply carries no equivalent header today. The counts are read on our side and become your usage record, which is where to read them.

Errors

Status codes and the closed set of type values are OpenAI’s. Branch on type and code, never on the message text: the fields are the contract and the sentences are prose that may be reworded. Every shipped code and its status is in errors. One difference in kind is worth naming. Some of our refusals are about money rather than about your request: not enough credit, or an account spend rate cap. Those reuse OpenAI’s own insufficient_quota and rate_limit_error types, so the branch your client already has keeps working, and carry a Uniblock code that says which of them happened.

What we do not claim

  • We have not published a proof run of an unmodified official SDK against the production hostname. The instructions on this site set only base_url and api_key, which is the whole intended change, and we will say so as a tested result rather than an expectation once that run exists.
  • We make no statement here about behaviour under concurrency, latency added by the hop, or availability. No figure for any of those has been measured in a way we are willing to publish.