# OpenAI compatibility

> The Markdown form of https://openfill.ai/docs/compatibility. The index of every docs page is https://openfill.ai/llms.txt, and every page in one file is https://openfill.ai/llms-full.txt.

OpenFill serves the OpenAI chat and completion shapes, adds a few fields, and refuses a few. This page says which is which, so a port is a base URL change plus a price limit.

## Endpoints

By the call you make in an OpenAI SDK. A served endpoint takes the request shape the SDK already sends, and a price limit by any of the routes below.

| SDK call | Endpoint | Status |
| --- | --- | --- |
| chat.completions.create | POST /v1/chat/completions | Served. Text, and images on a model that reads them. |
| completions.create | POST /v1/completions | Served. Tools are refused here. |
| models.list | GET /v1/models | Served, with pricing weights and length limits per model, and pricing: market, or a fixed-price model's three prices. |
| models.retrieve | GET /v1/models/{model} | Served. An alias answers with x_alias_of naming its model. |
| files.create | POST /v1/files | Served for purpose batch. |
| files.retrieve, files.content | GET /v1/files/{id}, GET /v1/files/{id}/content | Served. |
| files.list, files.delete | GET /v1/files, DELETE /v1/files/{id} | Served. A file a batch is still working from answers 409 file_in_use. |
| batches.* | POST and GET /v1/batches | Served: create, list, retrieve, cancel. |
| embeddings.create | /v1/embeddings | Refused with 404. |
| responses.create | /v1/responses | Served, without server-side state. The Responses API page has the mapping. |
| images, audio, moderations |  | Refused with 404. |
| fine_tuning, assistants |  | Refused with 404. |
| Anthropic Messages | /v1/messages | Served, with count_tokens beside it. The Messages API page has the mapping. |

OpenFill adds endpoints no OpenAI SDK has a method for: orders, your account defaults, usage, and the public market data. They are plain HTTP, listed on the [API reference](https://openfill.ai/docs/api).

## Request fields

Each field the OpenAI shape carries is treated one of four ways: checked and forwarded, forwarded untouched, removed, or refused. Anything absent from this table is forwarded to the model untouched, except functions and function_call, which /v1/completions refuses with 400 like tools, and the identifiers listed below, which stay on our side.

| Field | On | What happens |
| --- | --- | --- |
| model | both | Checked against GET /v1/models. Absent, the default model is used. A retired model answers 400 model_deprecated, and a preview of the market 400 model_preview until it opens. |
| messages | chat | Required, at least one. Each message is forwarded whole, so tool results and names pass through. |
| prompt | completions | A string, or an array of one string. A longer array is refused with 400. |
| stream | both | Honoured. See streaming. |
| max_tokens, max_completion_tokens | both | Checked, then clamped to the model's max output length. A missing cap is that length. |
| temperature, top_p, stop | both | Range checked, then forwarded. stop takes up to four strings. |
| n, best_of | both | Must be 1, null or absent. Anything else is refused with 400 before the order exists. |
| tools, tool_choice | chat | Forwarded. On /v1/completions they are refused with 400, since that shape has nowhere to put a call. |
| response_format, seed | both | Forwarded to the model untouched. |
| user, safety_identifier, metadata, prompt_cache_key | both | Accepted and kept on our side. These fields are removed before execution; content inside messages and tools is sent to the model. |
| logprobs, top_logprobs | chat | Forwarded on chat. Removed on /v1/completions, whose response has nowhere to put them. |
| stream_options.include_usage | both | Honoured. A live stream sends its final usage chunk when this is true. An idempotent streaming replay includes usage in its single result chunk. |
| echo, suffix | completions | Removed before the request reaches the model. |

### Content

A user message may carry images as `image_url` parts on a model whose datasheet on `GET /v1/models` lists vision. Each is a base64 data URL, or an http or https URL to a PNG, JPEG, GIF or WebP image of up to 6 MB, with at most 16 URLs and 6 MB of fetched images to a request. A URL is fetched when the request is submitted, or on a batch line when the line becomes an order, from our network and with no credentials, cookies or headers of yours, following up to 3 redirects within 15 seconds for each image; the image then travels with the request exactly as its bytes would have, and a retry under the same `Idempotency-Key` replays the request's order without fetching again. A URL that cannot be fetched is refused with `400 image_url_unfetchable`, naming the part and the reason. A model listed as text only refuses an image with `400`, and so does a model server that cannot read one. A part of any other type, such as `input_audio`, `video_url` or `file`, is refused with `400` by every model whose datasheet lists its capabilities. Nothing is charged, since nothing ran.

A body over 8 MB is refused with `413`, and any string carrying a null character is refused with `400`.

## The OpenFill fields

Six fields on the request body, none of them in the OpenAI shape. With the OpenAI SDKs they travel in `extra_body`; the table below this one shows the route in each SDK.

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| price_limit | number | `key or account` | The most you will pay, in USD per 1M input tokens at the reference mix (90% cached, 2% output). A request to a market model with no price limit stated anywhere is refused with 400 missing_price_limit. Ignored on a fixed-price model, whose prices are under pricing on GET /v1/models. |
| price_lock | boolean | `account setting` | true bids the lower of the published quote and your price limit, so the most you can be charged is known before you send; the charge is still the level when that is lower. The request starts within 10 seconds or expires with 429. Ignored on a fixed-price model, whose price is known before the request is sent; that request waits up to its order timeout. |
| max_wait | integer | `account setting` | Seconds an unlocked request may wait in the queue, 10 seconds to 30 days. Alias expires_after. Past it the request expires with 408 and no charge, or with 429 capacity_unavailable on a fixed-price model. A locked request on a market model ignores it; its wait is bounded by the execute window. |
| detached | boolean | `false` | Answer 202 with an order id at once and run the request in the background. Cannot be combined with stream. |
| store | boolean | `account setting` | false keeps nothing durable for this request: no result, no request log. It can only shorten what your account keeps. |
| retention_days | integer | `account setting` | Keep this request's data for fewer days than your account setting. Clamped down to the setting; it never extends it. |

### Headers

| Header | Meaning |
| --- | --- |
| Authorization | Bearer and your key. Required on every endpoint that is yours. |
| X-Price-Limit | The same parameter as price_limit, for a client that cannot add a body field. The body wins if both are present. Ignored on a fixed-price model. |
| X-Expires-After | The same parameter as max_wait. The body wins if both are present, and a header that is not a whole number of seconds is refused with 400 even then. |
| Idempotency-Key | Up to 255 characters. A retry with the same key returns the original's outcome while it is running or stored, instead of running again. An expired, failed or cancelled original releases the key, and a retry is then a new request. |

### Carrying a price limit from each SDK

The OpenAI SDKs type their request bodies, so a field they do not declare needs the SDK's own route for one. The header works from every client that can set one.

| Client | Route | Example |
| --- | --- | --- |
| Python | extra_body | extra_body={"price_limit": 0.10} |
| TypeScript | header, or a cast | { headers: { "X-Price-Limit": "0.10" } } |
| Go | body option | option.WithJSONSet("price_limit", 0.10) |
| Java | body property | .putAdditionalBodyProperty("price_limit", JsonValue.from(0.10)) |
| .NET | header, or the options patch | options.Patch.Set("$.price_limit"u8, 0.10) |
| Ruby | request option | request_options: {extra_body: {price_limit: 0.10}} |
| Rust (async-openai) | bring your own type | client.chat().create_byot(json!({..., "price_limit": 0.10})) |
| OpenAI Responses methods | extra body, on /v1/responses | client.responses.create(..., extra_body={"price_limit": 0.10}) |
| Anthropic SDKs | extra body, on /v1/messages | client.messages.create(..., extra_body={"price_limit": 0.10}) |
| curl | the body | "price_limit": 0.10 |

In TypeScript the body route is a `// @ts-expect-error` line above `price_limit: 0.10`, which the SDK forwards at runtime. The .NET patch is marked experimental by that SDK, so the header is the settled route there. A tool with no field and no header, such as a coding agent's settings screen, works once a max price is set on the key or the account, on [Bidding](https://openfill.ai/bidding).

## What comes back

The OpenAI response shapes, with one object added and every choice list of length one.

| Field | Meaning |
| --- | --- |
| usage.x_market.order_id | The order this completion ran as. The completion's own id is the model server's; every /v1/orders route takes this one, and the same value is on the response as the x-openfill-order-id header. |
| usage.x_market.level_at_execution | The price the request was charged at: the level at dispatch, or your bid where that was lower, in the price limit's unit. On a fixed-price model, its prices at the reference mix, in the same unit. |
| usage.x_market.bid | The price the request rested at: your price limit, or the lower of the quote and your price limit when locked. On a fixed-price model, the same figure as level_at_execution. |
| usage.x_market.charged_usd | What the request cost, in USD to nine decimal places. 0 at a level of 0. |
| usage.x_market.pricing | market, or fixed on a fixed-price model. Absent on a result stored before the field existed, which reads as market. |
| usage.x_market.usd_per_million | On a fixed-price model only: the three prices applied, per 1M tokens of cache-hit input, cache-miss input and output. |
| usage.prompt_tokens_details.cached_tokens | The prompt tokens served from cache, which are priced at the cache-hit weight, or the cache-hit price on a fixed-price model. |
| choices[0].message.reasoning_content | A reasoning model's chain of thought, passed through as the model sends it. In a stream it arrives in delta.reasoning_content. |
| choices[0].message.tool_calls | Assembled from the model's stream, so a call split across chunks arrives whole. |
| choices[0].logprobs | Present when you asked for logprobs on chat. Always null on /v1/completions. |

### Reasoning models

Reasoning tokens are output tokens. They are charged at the output weight and they count toward `max_tokens` ahead of the visible answer, so a small cap can spend the whole budget on reasoning and return an empty `content` with `finish_reason` of `length`. The tokens generated are charged either way. Leave `max_tokens` unset, which allows the model's full output length, or set it with room for the reasoning.

## A port in two lines

Both official SDKs read `OPENAI_BASE_URL` and `OPENAI_API_KEY`, as do most tools built on them. With those set, code that names no base URL reaches OpenFill unchanged, and the price limit comes from the key or the account.

```bash
export OPENAI_BASE_URL="https://api.openfill.ai/v1"
export OPENAI_API_KEY="$OPENFILL_API_KEY"
```

> Some newer SDK helpers default to the Responses API. OpenFill serves it at `/v1/responses` without server-side state, and the chat completions method lands on `/v1/chat/completions`; the two are priced and charged alike.

Previous: [Overview](https://openfill.ai/docs). Next: [Models](https://openfill.ai/docs/models).
