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.
Print the setup with the Liquid CLI
The Liquid CLI 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.
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
spendkey. See API keys. -
The key exported in your shell:
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
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
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
# 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.
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
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
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
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
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.
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:
- Join the
textof the parts with the sameindex. - Keep their
signature,dataandformat. - 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.
# 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
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
response = client.responses.create(
model="liquid.auto",
instructions="Answer in one sentence.",
input="What is an order book?",
)
print(response.output_text)
// 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.effortand theexchangeblock. Reasoning comes back as areasoningitem with readablereasoning_text, before the message. It has noencrypted_content, so sending it back has no effect. - Ignored:
store. The exchange stores no response. - Refused with
400 unsupported_parameter:previous_response_id,conversationandbackground: true. Send the full conversation ininputwith each request. - Hosted tools: The exchange removes
web_searchfrom the request and sends the headerx-liquid-notice: web_search_dropped. No search runs, unless the model name ends in:web(beta). It refuses all other hosted tools, such asfile_searchorcode_interpreter.paramnames the tool, for exampletools[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.
Notes
client.embeddings.createworks. See Embeddings.- To limit total spend across many requests, set a spending limit on the key. See API keys.
Next steps
- Anthropic SDK: the same idea in the Anthropic format.
- Purchasing rules: every field of the
exchangeblock. - Errors: the error format and what each code means.