What “OpenAI-Compatible” Actually Means for an Endpoint
10 min read · updated August 11, 2026
“OpenAI-compatible” is not a certification and there is no conformance suite behind it. It is a claim that you can point an OpenAI client library at a different base_url and it will work. That claim is testable, and it is worth knowing exactly what it has to be true of.
What the claim is actually about
The reason the phrase exists at all is that OpenAI’s client libraries take a base URL as configuration. Set it to your own server, keep the same code, and the SDK will issue the same HTTP requests to your host. Everything the phrase means follows from that: an OpenAI-compatible endpoint is one that satisfies the expectations those libraries have when they parse a response.
Note what this makes the reference implementation. It is not a specification document — the compatibility target is the wire format that the SDKs accept, which is documented but not versioned as a spec that third parties can conform to. In practice the surface that matters is the smaller one that widely used clients depend on, and that is what a server should be tested against.
It is also worth being clear that the phrase now attaches to two different OpenAI APIs. The overwhelming majority of servers claiming compatibility implement /v1/chat/completions, the older Chat Completions shape. Support for the newer Responses API, with its typed streaming events and its input rather than messagesfield, is a separate and much rarer claim. If a server says “OpenAI-compatible” without qualification, assume Chat Completions.
The surface a client depends on
Concretely, a server has to get these right before any of the interesting behaviour matters.
- Path and method.
POSTto a path ending/chat/completions, where the configured base URL supplies the prefix. Clients append the path to the base URL as given, so a server whose base URL already ends in/v1and a client configured with the same suffix produce/v1/v1/chat/completionsand a 404. This is the single most common first failure. - Auth. An
Authorization: Bearer <key>header. A server that requires no key still has to tolerate the header being sent, because the SDK will send one; several clients also refuse to start without a non-empty key, which is why placeholder values are conventional. - Request fields. At minimum
modelandmessages, where each message is an object withroleandcontent, and roles include at leastsystem,userandassistant. Unknown fields in the body should be ignored rather than rejected — clients send fields their users set, and a server that 400s on an unrecognised key is unusable behind a shared abstraction. - Response envelope. A JSON object with
id,objectset to the literalchat.completion,created,model, and achoicesarray whose entries haveindex,message(withroleandcontent) andfinish_reason. Typed SDKs validate this envelope; a missingobjectfield or acreatedsent as a string rather than a number fails at deserialisation, before any of your model output is reached. - A usage object.
usagewithprompt_tokens,completion_tokensandtotal_tokens. Some servers omit it. Nothing crashes, but every cost and quota feature downstream goes blank — see the usage object mapping page for why an absent count is worse than a wrong one. - A model listing.
GET /v1/modelsreturning an object withobject: "list"and adataarray. Tools that populate a model dropdown call this, and a server without it appears to have no models even though inference works.
The streaming contract
Streaming is where most compatibility claims are thinnest, because it has more implicit requirements than the request shape does. With stream: true the server must respond with Content-Type: text/event-stream and emit server-sent events whose data: payload is a chunk object.
data: {"id":"chatcmpl-1","object":"chat.completion.chunk","created":1750000000,
"model":"local-model","choices":[{"index":0,
"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-1","object":"chat.completion.chunk","created":1750000000,
"model":"local-model","choices":[{"index":0,
"delta":{"content":"Hel"},"finish_reason":null}]}
data: {"id":"chatcmpl-1","object":"chat.completion.chunk","created":1750000000,
"model":"local-model","choices":[{"index":0,
"delta":{},"finish_reason":"stop"}]}
data: [DONE]The requirements hidden in that are: object is chat.completion.chunk, not chat.completion; the content is under delta, not message; finish_reason is present and null on every chunk until the last one, where it is set; the id is stable across all chunks of one response; and the stream ends with the literal line data: [DONE], which is the one payload that is deliberately not JSON. A server that closes the connection instead leaves clients unable to distinguish completion from a dropped connection.
Two further details separate a working stream from a good one. Chunks must be flushed as they are produced rather than buffered, or the stream is a slow non-streaming response with extra steps — and any reverse proxy in front of the server must be configured not to buffer them either, which is a configuration failure that looks exactly like an implementation failure. And if the server supports stream_options: { include_usage: true }, it must emit the extra usage-bearing chunk with an empty choices array before the sentinel. If it does not support the option, ignoring it is acceptable; silently sending nothing while accepting the flag is what leaves callers with no token counts and no error. The chunk shape itself is covered in the streaming chunk format page.
Three levels of the same claim
When you read the phrase on a project’s README, it is usually one of three quite different assertions, and distinguishing them tells you what you will spend your week on.
- Transport-compatible. The path, the auth header and the response envelope are right, so a
curlworks and a simple client works. This is the level most self-hosted inference servers reach. Streaming may or may not be correct. - SDK-compatible. The official client libraries work unmodified, including streaming, which means the envelope validates under typed deserialisation and the sentinel is present. This is the level the phrase implies and a meaningful fraction of servers do not reach.
- Semantically compatible. The parameters do what they do on OpenAI:
temperaturehas the same range,stopsequences are honoured and excluded from the output,response_formatconstrains generation rather than being advisory,tool_choiceis enforced. Almost nothing reaches this level completely, and the gaps are the subject of where OpenAI-compatible endpoints break.
What the claim never covers
Even a server at the third level is not a drop-in for a model, and conflating the two is where teams lose time. Compatibility is about the interface; it says nothing about tokenisation, so token counts and therefore costs and context limits differ. It says nothing about which models are available or what the model strings are, so the identifier in your request has to change. It says nothing about rate-limit headers, request ids, organisation headers or the shape of the error bodies, which are covered in the error shape mapping page.
And it says nothing about behaviour. Two servers can be perfectly compatible at the wire level and return answers different enough to fail your evaluations, because compatibility is a property of the envelope and quality is a property of the model inside it. Treat the two as separate migrations with separate tests, and run the interface test first: there is no point comparing outputs from an endpoint that is silently ignoring your stop parameter.