# OpenAI SDK

The exchange serves OpenAI Chat Completions and Responses. The official Python
and TypeScript clients work without changes. Set the base URL and your key.

Use this SDK for direct API calls. For an agent that runs tools, use the
[OpenAI Agents SDK](https://inference.ai.exchange/docs/connect-openai-agents-sdk.md).

## Print the setup with the Liquid CLI

The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) prints the setup for the OpenAI SDK with your model filled in. To run your own program with the exchange in its environment, use `liquid exec`. It sets `OPENAI_BASE_URL`, `ANTHROPIC_BASE_URL` and the keys for that process only.

```bash
curl -fsSL https://inference.ai.exchange/cli/install.sh | bash
liquid login
liquid connect openai-sdk
liquid exec -- python app.py
```

To set up the OpenAI SDK yourself, follow the steps below.

## Before you start

- A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md).
- The key exported in your shell:

  ```bash
  export LIQUID_API_KEY=sk-liquid-...
  ```

## 1. Point the client at the exchange

The base URL ends in `/v1`, because the client adds `/chat/completions` to it.

**Python**

```python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://router.inference.ai.exchange/v1",
    api_key=os.environ["LIQUID_API_KEY"],
)

client.chat.completions.create(
    model="liquid.auto",
    messages=[{"role": "user", "content": "hello"}],
    # The preset applies its saved models, strategy and limits.
)
```

**TypeScript**

```typescript
import OpenAI from 'openai'

const client = new OpenAI({
  baseURL: 'https://router.inference.ai.exchange/v1',
  apiKey: process.env.LIQUID_API_KEY,
})

const completion = await client.chat.completions.create({
  model: 'liquid.auto',
  messages: [{ role: 'user', content: 'hello' }],
  // The preset applies its saved models, strategy and limits.
})
```

**curl**

```bash
# The preset applies its saved models, strategy and limits.
curl https://router.inference.ai.exchange/v1/chat/completions \
  -H "Authorization: Bearer $LIQUID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "liquid.auto",
    "messages": [{"role": "user", "content": "hello"}]
  }'
```

Use any model, router model or preset on the book. `GET /v1/models` lists the
names. See [Market data](https://inference.ai.exchange/docs/market.md).

## 2. Stream and read the cap before the first token

The `x-liquid-job-id` and `x-liquid-cap-usd` headers arrive before any token.
`x-liquid-cap-usd` is the maximum charge for the job. The final chunk has the settled
charge in its `exchange` block.

Read the headers with `with_raw_response` (Python) or `withResponse()` (TypeScript).

```python
# Python
raw = client.chat.completions.with_raw_response.create(
    model="liquid.auto",
    messages=[{"role": "user", "content": "hello"}],
    stream=True,
    stream_options={"include_usage": True},
)
print(raw.headers["x-liquid-job-id"], raw.headers["x-liquid-cap-usd"])
for chunk in raw.parse():
    print(chunk.choices[0].delta.content or "", end="")
```

```typescript
// TypeScript
const { data: stream, response } = await client.chat.completions
  .create({
    model: 'liquid.auto',
    messages: [{ role: 'user', content: 'hello' }],
    stream: true,
    stream_options: { include_usage: true },
  })
  .withResponse()

console.log(response.headers.get('x-liquid-job-id'), response.headers.get('x-liquid-cap-usd'))
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta.content ?? '')
}
```

### Read the reasoning

A reasoning model sends its reasoning in the `delta` of each chunk as
`reasoning`. It comes before the answer text or between parts of it.

- The final chunk has `usage.completion_tokens_details.reasoning_tokens`, the output tokens that the reasoning used.
- If the provider does not return its reasoning, the model sends none.
- The SDK types do not include the field. Read it as an extra attribute:

```python
# Python
for chunk in raw.parse():
    delta = chunk.choices[0].delta if chunk.choices else None
    if delta and getattr(delta, "reasoning", None):
        print(delta.reasoning, end="")
```

```typescript
// TypeScript
for await (const chunk of stream) {
  const delta = chunk.choices[0]?.delta as { reasoning?: string } | undefined
  if (delta?.reasoning) process.stdout.write(delta.reasoning)
}
```

A response that is not streamed has the reasoning in `message.reasoning`. The
exchange bills reasoning as output, and it counts toward `max_tokens`. See
[Output limits](https://inference.ai.exchange/docs/errors.md).

#### Ask for reasoning

Put a `reasoning` object in the request body. Use `extra_body` in Python and an
extra field in TypeScript:

| Field        | Meaning                                                                                                                       |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `effort`     | A level name such as `low`, `medium` or `high`, or a whole number for a model that takes one. The same as `reasoning_effort`. |
| `max_tokens` | A reasoning budget in tokens, for models that take one.                                                                       |
| `exclude`    | `true` leaves the reasoning out of the response. It is still generated, counted and billed.                                   |

`effort` and `max_tokens` are requirements. Only offers that accept them
compete for the request.

#### Keep reasoning blocks in a tool loop

Each chunk with reasoning also has `reasoning_details`. These are parts of the
reasoning blocks, with a number in `index`. A model that signs its reasoning
adds a `signature`. It can also return a block as encrypted `data`.

In a tool-use loop:

1. Join the `text` of the parts with the same `index`.
2. Keep their `signature`, `data` and `format`.
3. Put the blocks back on the assistant turn that called the tool, as `reasoning_details`.

If you do not send them, a model that signs its reasoning refuses the next turn.

## 3. Send purchasing rules in the body

An `exchange` block sets the purchasing rules for one request: which models
compete, how the exchange ranks offers, and limits such as a cost cap and a
first-token deadline. The SDK types do not include it. Use `extra_body` in
Python and an extra field in TypeScript. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md).

```python
# Python
client.chat.completions.create(
    model="liquid.auto",
    messages=[{"role": "user", "content": "hello"}],
    extra_body={"exchange": {"cap_usd": 0.05, "limits": {"ttft_ms": 1500}}},
)
```

```typescript
// TypeScript
await client.chat.completions.create({
  model: 'liquid.auto',
  messages: [{ role: 'user', content: 'hello' }],
  // @ts-expect-error -- the SDK types do not include the exchange block
  exchange: { cap_usd: 0.05, limits: { ttft_ms: 1500 } },
})
```

## 4. Use the Responses API

`client.responses.create` works with the same base URL. The exchange handles
the request in the same way as a Chat Completions request.

```python
# Python
response = client.responses.create(
    model="liquid.auto",
    instructions="Answer in one sentence.",
    input="What is an order book?",
)
print(response.output_text)
```

```typescript
// TypeScript
const response = await client.responses.create({
  model: 'liquid.auto',
  instructions: 'Answer in one sentence.',
  input: 'What is an order book?',
})
console.log(response.output_text)
```

The exchange serves a stateless subset of the API:

- **Supported:** text and image input, `instructions`, streaming, function
  tools, namespaced tools, custom (freeform) tools, `tool_choice`,
  `text.format`, `reasoning.effort` and the `exchange` block. Reasoning comes
  back as a `reasoning` item with readable `reasoning_text`, before the
  message. It has no `encrypted_content`, so sending it back has no effect.
- **Ignored:** `store`. The exchange stores no response.
- **Refused with `400 unsupported_parameter`:** `previous_response_id`,
  `conversation` and `background: true`. Send the full conversation in `input`
  with each request.
- **Hosted tools:** The exchange removes `web_search` from the request and sends
  the header `x-liquid-notice: web_search_dropped`. No search runs, unless the
  model name ends in `:web` (beta). It refuses all other hosted tools, such as
  `file_search` or `code_interpreter`. `param` names the tool, for example
  `tools[1]`.

Without `max_output_tokens`, the output limit is 32,768 tokens. The exchange
lowers it to the limit that the offers allow. See [Output limits](https://inference.ai.exchange/docs/errors.md).

## Notes

- `client.embeddings.create` works. See [Embeddings](https://inference.ai.exchange/docs/embeddings.md).
- To limit total spend across many requests, set a spending limit on the key.
  See [API keys](https://inference.ai.exchange/docs/keys.md).

## Next steps

- [Anthropic SDK](https://inference.ai.exchange/docs/connect-anthropic-sdk.md): the same idea in the Anthropic format.
- [Purchasing rules](https://inference.ai.exchange/docs/rules.md): every field of the `exchange` block.
- [Errors](https://inference.ai.exchange/docs/errors.md): the error format and what each code means.
