# Chat and completions

> The Markdown form of https://openfill.ai/docs/api/chat. 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.

Two endpoints, one handler. A chat request is the OpenAI shape with a price limit; a completion request is the legacy shape, remapped to a single user message on the way to the model.

## Create a chat completion

**POST /v1/chat/completions**

```bash
curl https://api.openfill.ai/v1/chat/completions \
  -H "Authorization: Bearer $OPENFILL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "market-preview", "messages": [{"role": "user", "content": "hi"}], "price_limit": 0.10}'
```

### Request body

The OpenAI fields first, then the OpenFill extensions. Optional scalar fields accept null as absent. A field absent from this table is forwarded to the model untouched, except the identifiers listed below.

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| model | string | `the default model` | A served model id. GET /v1/models lists them. |
| messages | array | required | At least one message with a nonempty role. Content can be a string, an array of parts, null or absent. Tool calls, tool results and name pass through. |
| stream | boolean | `false` | Server-sent events. The response is 200 once the order is queued. |
| stream_options | object | none | {include_usage: true} adds a final chunk carrying usage and x_market. |
| max_tokens, max_completion_tokens | integer | `the model's max output` | Positive integers up to 10000000. max_tokens takes precedence. The selected cap is clamped down to the model's max output length. |
| temperature | number | none | 0 to 2. |
| top_p | number | none | 0 to 1. |
| stop | string or array | none | Up to four strings of up to 256 characters. |
| n, best_of | integer | `1` | Must be 1. Anything else is refused with 400. |
| tools, tool_choice | as OpenAI | none | Forwarded to the model. Calls are assembled from the stream and returned whole. |
| response_format, seed | as OpenAI | none | Forwarded to the model untouched. |
| user, safety_identifier, metadata, prompt_cache_key | as OpenAI | none | 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 | as OpenAI | none | Forwarded. The response carries choices[0].logprobs. |
| 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). The range is 0 to 1000000000. A bid of 0 can run when capacity reaches it and is charged nothing. On a market model, none anywhere is 400 missing_price_limit. Ignored on a fixed-price model, whose prices are under pricing on GET /v1/models. |
| price_lock | boolean | `account setting` | Only for a request whose price has to be known before sending. Bid the lower of the published quote and price_limit. Starts within 10 seconds or expires with 429 capacity_unavailable. 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 order may wait, 10 seconds to 30 days. Alias expires_after. The schema accepts 1 to 2592000; values outside that range or fractions are 400. Within it, the wait is clamped to the configured range. max_wait takes precedence over expires_after. A lock uses its own window after validation. |
| detached | boolean | `false` | Answer 202 with an order id and run in the background. Refused with 400 alongside stream. |
| store | boolean | `account setting` | false keeps nothing durable for this request. |
| retention_days | integer | `account setting` | A nonnegative integer. Clamped down to the account setting; 0 keeps no result or request log. store: false takes precedence. |

### Response

The OpenAI `chat.completion` object with exactly one choice, and `usage.x_market` added.

```json
{
  "id": "chatcmpl-...", "object": "chat.completion", "created": 1756700000, "model": "market-preview",
  "choices": [{
    "index": 0,
    "message": {"role": "assistant", "content": "...", "reasoning_content": "..."},
    "finish_reason": "stop",
    "logprobs": null
  }],
  "usage": {
    "prompt_tokens": 12, "completion_tokens": 40, "total_tokens": 52,
    "prompt_tokens_details": {"cached_tokens": 8},
    "x_market": {"order_id": "<order_id>", "pricing": "market", "level_at_execution": 0.0143, "bid": 0.1, "charged_usd": 0.000005272}
  }
}
```

| Field | Meaning |
| --- | --- |
| message.content | The answer. null for a turn that is only tool calls. |
| message.reasoning_content | A reasoning model's chain of thought, when the model sends one. |
| message.tool_calls | Present when the model called tools. |
| logprobs | {content: [...]} when you asked for logprobs, else null. |
| usage.x_market.order_id | The order id used by every /v1/orders route, also in x-openfill-order-id. Older stored results can omit this field. |
| usage.x_market.level_at_execution | The price the order was charged at: the level at dispatch, or the bid where that was lower. On a fixed-price model, its prices at the reference mix, in the price limit's unit. |
| usage.x_market.bid | What the order 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 | The debit, in USD to nine decimal places. |
| usage.x_market.pricing | market, or fixed on a fixed-price model. Older stored results can omit this field, 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. |

### Response with detached: true

```json
202 {
  "id": "<order_id>", "object": "order", "status": "queued", "expires_at": "...",
  "result_url": "/v1/orders/<order_id>/result",
  "result_stored": true, "result_retention_days": 7
}
```

`warning` is added when nothing will be stored. The [detached orders page](https://openfill.ai/docs/detached) has the collection semantics.

### Response with stream: true

`200` and a `text/event-stream` body: queue comments while the order waits, the model's chunks, a usage chunk when asked for, and `data: [DONE]`. Every frame is on the [streaming page](https://openfill.ai/docs/streaming).

### Statuses this endpoint answers

| Status | When |
| --- | --- |
| 200 | The completion, or a stream. |
| 202 | detached: true, once the order is queued. |
| 400 | Malformed body, n or best_of above 1, missing_price_limit, invalid_output_cap, model_deprecated, model_preview on a preview of the market, a bad X-Price-Limit or X-Expires-After, or a request the model refused. |
| 401, 403 | The key, or the account behind it. |
| 402 | insufficient_balance at the current level, or at a fixed-price model's prices. |
| 404 | model_not_found. |
| 408 | order_expired: the wait on a market model passed max_wait. |
| 409 | order_canceled while it waited, or idempotency_error on a mismatched replay. |
| 410 | result_unavailable on a replay of a finished order with nothing stored. |
| 413 | The body is over the size limit. |
| 429 | rate_limit_error on a ceiling, capacity_unavailable on a missed lock window or when a request to a fixed-price model did not start before max_wait, result_pending on a replay still being stored. |
| 500, 502, 503 | Our side, the backend mid-stream, or the backend unavailable. A settlement error may leave the charge uncertain; check the order and usage before repeating the work. |

## Create a completion

**POST /v1/completions**

```bash
curl https://api.openfill.ai/v1/completions \
  -H "Authorization: Bearer $OPENFILL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "market-preview", "prompt": "Say hi", "price_limit": 0.10}'
```

The same handler and the same extensions, with the legacy shape's differences:

| Field | On /v1/completions |
| --- | --- |
| prompt | A string, or an array of one string. It becomes a single user message. Absent, it is empty. |
| tools, tool_choice, functions, function_call | Refused with 400: the response has nowhere to put a call. |
| logprobs, top_logprobs, echo, suffix | Removed before the model sees them. The response has logprobs: null. |
| max_completion_tokens | Read as the output cap when max_tokens is absent; a non-integer or a value under 1 is 400 invalid_output_cap on that path. |

```json
{
  "id": "cmpl-...", "object": "text_completion", "created": 1756700000, "model": "market-preview",
  "choices": [{"index": 0, "text": "...", "finish_reason": "stop", "logprobs": null}],
  "usage": {"prompt_tokens": 4, "completion_tokens": 2, "total_tokens": 6,
            "x_market": {"order_id": "<order_id>", "pricing": "market", "level_at_execution": 0.0143, "bid": 0.1, "charged_usd": 4.97e-7}}
}
```

> Streaming on this endpoint sends `text_completion` chunks with `choices[0].text`, and the same queue comments and usage chunk as chat.

Previous: [API reference](https://openfill.ai/docs/api). Next: [Responses API](https://openfill.ai/docs/api/responses).
