> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hopscotchlabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# OpenAI compatibility

> What OpenAI-compatible means here: the endpoints that exist today, the differences that are additive, and what a call to anything else returns.

export const FeatureStatus = ({missing, feature, instead, detail, children}) => <Info>
    <strong>Not available yet: {missing ?? feature}.</strong>
    {instead ?? detail ? ` ${instead ?? detail}` : null} This page documents what
    the product does today. When that changes, this page changes with it.
    {children}
  </Info>;

export const NotDocumented = ({subject, reason}) => <Info>
    <strong>{subject} is not documented here.</strong> {reason} We would rather
    say nothing than describe something you cannot use.
  </Info>;

"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://api.hopscotchlabs.ai` and give it a
Hopscotch API key. Nothing else about your code changes.

```text Base URL theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
https://api.hopscotchlabs.ai
```

## The endpoints that exist today

| Endpoint               | Method | Status                               |
| ---------------------- | ------ | ------------------------------------ |
| `/v1/chat/completions` | `POST` | Serves. Streaming and non-streaming. |
| `/v1/responses`        | `POST` | Serves. Streaming and non-streaming. |
| `/v1/models`           | `GET`  | Serves.                              |
| `/v1/models/{id}`      | `GET`  | Serves.                              |

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`:

```bash theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
curl https://api.hopscotchlabs.ai/v1/images/generations \
  -H "Authorization: Bearer ub_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"model": "openai/gpt-4o-mini", "prompt": "hello"}'
```

```json 404 theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
{
  "error": {
    "message": "POST /v1/images/generations is not supported in this release.",
    "type": "invalid_request_error",
    "param": null,
    "code": "endpoint_not_supported",
    "request_id": "00000000-0000-4000-8000-000000000000"
  }
}
```

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 `hopscotch/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](/api-reference/parameters)
covers both in full.

Two consequences worth stating:

* Nothing Hopscotch-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](/concepts/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 Hopscotch 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`](/concepts/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-hopscotch-*` 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-hopscotch-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:

| Header                            | What it says                                                       |
| --------------------------------- | ------------------------------------------------------------------ |
| `x-hopscotch-provider`            | Which provider family answered.                                    |
| `x-hopscotch-retry-attempt-count` | How many attempts it took, so a slow success has a visible reason. |
| `x-hopscotch-cache-status`        | Whether the reply came from cache rather than from the provider.   |
| `x-hopscotch-trace-id`            | A second internal id, distinct from your request id.               |

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](/concepts/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:

```text theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
:x-hopscotch-usage {"v":1,...}
```

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](/concepts/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](/concepts/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 Hopscotch `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.
