# Liquid Inference > Never overpay for a prompt again. Providers compete to answer your requests. You set your requirements, send ordinary OpenAI- or Anthropic-format requests, and pay the lowest offer that meets them. Page: https://inference.ai.exchange/ ## How it works 1. **Providers set their prices.** Providers post a price per token for every model they serve. The winning offer's price is capped before the model starts. 2. **You set the terms.** Choose the models, regions, speed and minimum quality. Only providers that meet your terms compete. 3. **The best offer wins.** The lowest priced model that meets your requirements answers your request. You only pay for what you use. ## Features - Prices that change with supply and demand. - Independent benchmarks of every provider, published. - Itemized billing: what every request cost, line by line. - Public price history for every model. ## Questions - **What do I change in my code?** Most OpenAI and Anthropic code changes in two places: the base URL and the API key. Model names must be names that the exchange lists. See [Quickstart](https://inference.ai.exchange/docs/quickstart.md). - **Can a request cost more than I expect?** No. The exchange fixes a price cap before the first token, and a job never costs more than the cap. You pay for the tokens that the job used. See [Receipts](https://inference.ai.exchange/docs/receipts.md). - **What does the exchange charge?** Providers earn their own price. The exchange adds a fee: the larger of 5% of the provider's charge and a floor of 1 cent per million tokens, lower for a free offer of a model with a reference price. A job that fails before any output pays no fee. See [Fees](https://inference.ai.exchange/docs/fees.md). - **Can I buy a fixed amount of capacity?** Yes. A reservation is a monthly pool of tokens on one model from one provider, at a fixed monthly fee. Ask up to 20 providers for a quote, accept one and name it in a preset. Requests use the pool first, then the market. See [Reservations](https://inference.ai.exchange/docs/reservations.md). - **Does the exchange keep my prompts?** No. It does not store the text of your requests or answers, except a prompt that you save in a workload. See [Data handling](https://inference.ai.exchange/docs/data-handling.md). - **What if a provider fails during a request?** Before any output, the exchange can try the next offer. After output starts it does not switch providers. - **Do I need a card to try it?** No. A new workspace gets a starting credit. ## Start - [Quickstart](https://inference.ai.exchange/docs/quickstart.md): a key, a base URL and your first request. - [Liquid CLI](https://inference.ai.exchange/cli.md): start your coding agent on the exchange from the terminal. - [Liquid MCP](https://inference.ai.exchange/mcp.md): give your agent the exchange as tools. - [Connect an app](https://inference.ai.exchange/docs/connect-overview.md): guides for coding agents, editors and SDKs. - [Sell on the exchange](https://inference.ai.exchange/docs/provider.md): how a provider joins. ## For agents - [llms.txt](https://inference.ai.exchange/llms.txt): the index of every page, as Markdown. - [llms-full.txt](https://inference.ai.exchange/llms-full.txt): every page in one file. - Every docs page is also at `https://inference.ai.exchange/docs/.md`. # Liquid CLI > Start the coding agent you already use, with the exchange as its provider. Pick the ranking and the cap, and the exchange buys each request for you. Page: https://inference.ai.exchange/cli ## Install ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash ``` macOS and Linux, on Intel and Arm. The installer checks the download before it installs. The newest version is in [latest.json](https://inference.ai.exchange/cli/latest.json). ## First request ```bash liquid login liquid claude ``` `liquid claude --model liquid/auto:cheapest --cap 0.50` ranks the offers by price and limits each request to a cap of $0.50. Add `:smartest` or `:fastest` to rank on another axis. The exchange charges for the tokens used and returns the rest of the cap. ## How it works - `liquid ` runs the agent that is on your PATH and sets the base URL, the key and the model for that process only. It writes no file of yours. - `liquid exec -- ` runs any program with the exchange in its environment. - `liquid api ` sends one request to the exchange with the key and the origin added, so a script holds neither. - Each run gives the agent a session key that ends with the run, never the saved key. - `liquid login` signs in with the browser. The first sign-in also makes an API key for this computer and saves it. - `liquid keys list|create|revoke` manages API keys. It needs `liquid login --keys`. - `liquid workspace balance` and `liquid workspace usage --days 30` show what the workspace can spend and what it spent. They need `liquid login`. - `liquid whoami`, `liquid workspace list` and `liquid workspace use [name]` show and switch the workspace that commands act in. `liquid --workspace ` acts in another workspace for one command. - `liquid job` lists your settled jobs. Pick one to read its details. It uses your saved API key. ## More - [CLI guide](https://inference.ai.exchange/docs/cli.md): every command. - [Axis routing](https://inference.ai.exchange/docs/axis-routing.md): rank by smartest, fastest or cheapest. - [API keys](https://inference.ai.exchange/docs/keys.md): scopes, limits and expiry. - [Home](https://inference.ai.exchange/index.md) # Liquid MCP > Give your agent the exchange as tools. It reads prices, previews a purchase and buys an answer, an image or a video. You set the most that it can spend. Page: https://inference.ai.exchange/mcp ## Connect The server uses the Streamable HTTP transport at: ``` https://router.inference.ai.exchange/mcp ``` Add that address to an MCP client. The client opens the browser, and you choose the workspace, whether the agent can buy, and the most that it can spend. The exchange gives the client its own API key. There is no key to paste. ```bash liquid mcp claude claude mcp add --transport http liquid https://router.inference.ai.exchange/mcp claude mcp login liquid ``` The first command uses the [Liquid CLI](https://inference.ai.exchange/cli.md) and also works with `codex`, `cursor` and `opencode`. The last two commands add the server to Claude Code by hand. ## Tools The server has 24 tools. Each one sends an ordinary request through the router with the key of the agent, so the price caps, the key limits and the billing are the same as for the REST API. - **Look at the market:** `list_models`, `get_model`, `compare_models`, `get_book`, `get_markets`, `get_routers`, `get_fills`, `get_volume`, `get_throughput`, `get_lifetime`. Any key. - **Preview a purchase:** `dry_run`, `count_tokens`. They buy nothing. - **Read your account:** `get_balance`, `get_limits`, `list_receipts`, `get_job`. - **Buy:** `send_message`, `generate_image`, `generate_speech`, `create_video`, `transcribe`, `decide`, `embed`. These need a key that can spend. `get_video` reads the status of a video job. A client that supports MCP Apps shows a widget for the market tools, `get_balance`, `get_limits`, `list_receipts`, `get_job`, `dry_run` and the tools that make media. A widget never buys. ## Control The key is named `Liquid MCP: ` and expires in 7 days. You can revoke it at any time on the API keys page. The default spend limit is $10. If you do not allow buying, the key can look up models and the book and read the balance and receipts. A tool that buys also takes `cap_usd`, which is the most that one job can cost. ## For client authors - A request to `/mcp` with no key gets `401` and a `WWW-Authenticate` header that names the resource metadata (RFC 9728) at `/.well-known/oauth-protected-resource/mcp`. - The client registers (RFC 7591) or uses the address of a metadata document as its `client_id`. PKCE with `S256` is required. - The scopes are `mcp:read` and `mcp:spend`. A tool that buys on a read-only key gets `403` with `insufficient_scope`. - The token response has the key as `access_token`. There are no refresh tokens. ## More - [MCP guide](https://inference.ai.exchange/docs/connect-mcp-server.md): every tool, the sign-in and the errors. - [API keys](https://inference.ai.exchange/docs/keys.md): scopes, limits and expiry. - [Home](https://inference.ai.exchange/index.md) # Liquid Inference for Enterprise > Enterprise AI at the lowest price. Set your rules once. Providers compete inside them. Page: https://liquidinference.ai/enterprise/ ## Everyone who signs off - **Engineering:** keep your SDK and change the base URL. A failed offer falls back to the next one. - **Finance:** the price cap is fixed before the first token. Set spending limits per workspace. Every call has a receipt. - **Security and risk:** one guardrail covers every key. Set retention to none. Every change goes to the audit log. - **Procurement and legal:** one agreement for every provider. Provider claims are on file. Reservations have a fixed fee. ## Organizations and access An organization holds one balance. Its workspaces spend from it, each within its own budget. Verify a domain with one DNS record, and colleagues join when they sign up. Give each person a role, then a level for keys, guardrails and presets. See [Organizations](https://inference.ai.exchange/docs/organizations.md) and [Workspace members](https://inference.ai.exchange/docs/members.md). ## Bring your own cloud Connect an AWS Bedrock, Anthropic API, Azure OpenAI or Google Vertex AI account. A job goes to your account first. For each cloud you choose a fallback: **Market** moves a refused job to the next offer, and **Fail** ends it. No other buyer can use your account. Guardrails and receipts apply the same way. ## Rules and receipts - A guardrail is one policy for a whole workspace: which offers may serve, how much to spend, and what text may pass. A request cannot widen it. See [Guardrails](https://inference.ai.exchange/docs/guardrails.md). - You pay for the tokens used, never above the price cap. See [Receipts](https://inference.ai.exchange/docs/receipts.md). ## Reserved capacity A monthly token pool at a fixed fee. Ask up to 20 providers for a quote, accept one, name it in a preset, and pay one fee each month. Requests use the pool first, then the market. See [Reservations](https://inference.ai.exchange/docs/reservations.md). ## How Enterprise works - **Pricing:** provider price plus a fee, the larger of 5% and 1 cent per million tokens. Volume terms on request. See [Fees](https://inference.ai.exchange/docs/fees.md). - **Billing and contract:** one agreement. Invoicing and credit lines. Your cloud rates stay yours. - **Security review:** our data terms, or we review yours. A named engineering contact. ## Talk to us Tell us what you run. We reply with a quote and our security documents: https://architect.co/contact/ ## More - [Data handling](https://inference.ai.exchange/docs/data-handling.md) - [Audit log](https://inference.ai.exchange/docs/audit-log.md) - [Home](https://inference.ai.exchange/index.md) # Documentation Liquid Inference is an exchange for inference. Providers post prices, and you send an ordinary OpenAI or Anthropic request. You pay for the tokens that the job used, and never more than a price cap that the exchange fixes before the first token. ## Buy inference - [Quickstart](https://inference.ai.exchange/docs/quickstart.md): make an API key and send your first request. - [API keys](https://inference.ai.exchange/docs/keys.md): scopes, spending limits and expiry. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): choose models, providers, regions and a price cap. - [Receipts](https://inference.ai.exchange/docs/receipts.md): check what each job cost. ## Connect a tool - [Connect an app](https://inference.ai.exchange/docs/connect-overview.md): base URLs, keys and the guide for your tool. - [Claude Code](https://inference.ai.exchange/docs/connect-claude-code.md), [Codex](https://inference.ai.exchange/docs/connect-codex.md) and [Cursor](https://inference.ai.exchange/docs/connect-cursor.md): coding agents and editors. - [OpenAI SDK](https://inference.ai.exchange/docs/connect-openai-sdk.md): the official client. ## Sell inference - [Become a provider](https://inference.ai.exchange/docs/provider.md): apply, get admitted and make a provider key. - [Deployments](https://inference.ai.exchange/docs/provider-models.md): register the models that you sell. - [Quote over the session](https://inference.ai.exchange/docs/provider-quoting.md): send your prices to the exchange. - [Serve and get paid](https://inference.ai.exchange/docs/provider-dispatch.md): run jobs and settle them. ## Learn by doing The [tutorials](https://inference.ai.exchange/docs/tutorials.md) take one task from a clean start to a working result. ## Browse by section - Guides: [Quickstart](https://inference.ai.exchange/docs/quickstart.md). - Tutorials: [All tutorials](https://inference.ai.exchange/docs/tutorials.md). - Integrations: [Connect an app](https://inference.ai.exchange/docs/connect-overview.md). - Plugins: [Apps](https://inference.ai.exchange/docs/apps.md). - API reference: [API overview](https://inference.ai.exchange/docs/api.md). - Sell: [Become a provider](https://inference.ai.exchange/docs/provider.md). # Quickstart Send an ordinary OpenAI or Anthropic request to the exchange. Providers compete to answer it. You pay for the tokens that the job used, and never more than a price cap that the exchange fixes before the first token. This page is for buyers. To sell inference, start with [Become a provider](https://inference.ai.exchange/docs/provider.md). To make your first request, follow steps 1 and 2. They need a key and one `curl` command. ## 1. Get a key 1. Sign up. Sign-up creates a workspace. A workspace holds your balance, your API keys and your receipts. 2. Open the **API keys** page and choose **Create key**. 3. Copy the key. The page shows it one time only. A new workspace gets a starting credit, so you can send your first requests without a card. Your first top-up of $10 or more adds $2 of free credit. The **Credits** page shows your balance. A key looks like `sk-liquid--`. See [API keys](https://inference.ai.exchange/docs/keys.md) for scopes and spending limits. ## 2. Send a request ```bash curl https://router.inference.ai.exchange/v1/chat/completions \ -H "Authorization: Bearer $LIQUID_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-oss-120b", "messages": [{"role": "user", "content": "Say hello."}] }' ``` The response is a normal chat completion with one more field, `exchange`. It tells you what the job cost. ## 3. Read what you paid Each provider posts standing offers: a price for each model. The exchange picks the best offer for your request, holds the price cap from your balance, and charges the tokens that the job used. It returns the rest of the cap to your balance. | Where | What it shows | | --------------------------------------- | -------------------------------------------------------------- | | `x-liquid-cap-usd` response header | The price cap. The exchange sends it before the output starts. | | `x-liquid-job-id` response header | The job id. | | `exchange.charge.total_usd` in the body | The charge. It is never more than the cap. | | `exchange.provider_slug` in the body | The provider that served the job. | A receipt records each settled job. It does not change after settlement. The **Overview** and **Credits** pages list your receipts. To read a receipt in code, use a key with the `read` scope or higher: ```bash curl https://router.inference.ai.exchange/v1/exchange/jobs/ \ -H "Authorization: Bearer $LIQUID_API_KEY" ``` The exchange writes the receipt just after it answers. A call at once can return `404`. Try again after a moment. To set your own price cap, add `"exchange": {"cap_usd": 0.05}` to the request body. To choose models, regions or a ranking, see [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## 4. Point your tool at the exchange A tool needs a base URL and your key. The two formats use different base URLs, because each client adds its own path. The [Connection guides](https://inference.ai.exchange/docs/connect-overview.md) give the steps for Claude Code, Codex, Cursor, Cline, the official SDKs and many other tools. If your code now sends requests to another product, use its guide: [OpenRouter](https://inference.ai.exchange/docs/move-from-openrouter.md), [LiteLLM](https://inference.ai.exchange/docs/move-from-litellm.md), [the OpenAI API](https://inference.ai.exchange/docs/move-from-openai.md), [the Anthropic API](https://inference.ai.exchange/docs/move-from-anthropic.md) or [Portkey](https://inference.ai.exchange/docs/move-from-portkey.md). To start a coding agent from the terminal, use the [Liquid CLI](https://inference.ai.exchange/docs/cli.md). ## Next steps - [API keys](https://inference.ai.exchange/docs/keys.md): scopes, spending limits and expiry. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): choose the models, the ranking and the limits for each request. - [Market data](https://inference.ai.exchange/docs/market.md): read the book, the list of current offers, before you send a request. - [Modalities](https://inference.ai.exchange/docs/modalities.md): send an image, an audio clip or a document. - [Errors](https://inference.ai.exchange/docs/errors.md): the error format and the error codes. ## Reference ### Endpoints | Method | Path | Description | | ------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `POST` | `/v1/chat/completions` | OpenAI Chat Completions. | | `POST` | `/v1/responses` | OpenAI Responses, without stored state. See [OpenAI SDK](https://inference.ai.exchange/docs/connect-openai-sdk.md). | | `POST` | `/v1/messages` | Anthropic Messages. | | `POST` | `/v1/messages/count_tokens` | Anthropic token count estimate. | | `GET` | `/v1/models` | The models that have offers now. The format is OpenAI, or Anthropic when the request has an `anthropic-version` header. | | `GET` | `/v1/anthropic/models` | The same list, always in the Anthropic format. | | `GET` | `/v1/codex/models` | The models that Codex can use. See [Codex](https://inference.ai.exchange/docs/connect-codex.md). | The exchange also serves images, speech, video and embeddings. See [Modalities](https://inference.ai.exchange/docs/modalities.md) and [Embeddings](https://inference.ai.exchange/docs/embeddings.md). The [API reference](https://inference.ai.exchange/docs/api.md) lists every endpoint. ### Choose a model The `model` field accepts these kinds of name: | Name | Example | What competes | | -------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------- | | A model | `gpt-oss-120b` | Each provider that has an offer for that model. | | A router model | `liquid.auto` | Each model that the router model covers. See [Auto Router](https://inference.ai.exchange/docs/auto.md). | | A basket | `architect.basket` | The models in `exchange.candidates.models.allow`, ranked by `exchange.rank.strategy`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). | | A preset | `fast` | The saved models, strategy and limits that the name refers to. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). | To rank the offers, add `:smartest`, `:fastest` or `:cheapest` to the name, for example `liquid.auto:cheapest`. See [Axis routing](https://inference.ai.exchange/docs/axis-routing.md). To let the exchange choose the model by score and estimated charge, send `liquid.auto:balanced`. See [Auto Router](https://inference.ai.exchange/docs/auto.md). The exchange refuses any other name with `404 model_not_found`. It never uses a different model in its place. The message shows the name that you sent and suggests other names. `GET /v1/models` returns each name that you can use now. The list includes your presets when the request has a key of the workspace that saved them. Each entry lists the request parameters that at least one offer supports. If a parameter is not in the list, the exchange refuses a request that includes it. It does not ignore the parameter. ### Streaming Set `stream: true` to receive `text/event-stream`. The last chunk has the `exchange` block with the settled charge. In the OpenAI format, set `stream_options: {"include_usage": true}` to receive token usage. The [OpenAI SDK](https://inference.ai.exchange/docs/connect-openai-sdk.md) guide shows how to read the cap header from a stream. ### Web access **Beta.** `:web` is a beta feature. Its price, its search services and its behavior can change without notice. A model knows only its training data, up to the cutoff date that its creator publishes. A question about recent events can get an old or wrong answer. To give a model current information, add `:web` to the model name, for example `deepseek-v4.1-flash:web` or `liquid.auto:smartest+web`. The exchange searches the web for the last user message before it ranks the offers. It puts the results in the prompt, before that message. See [Axis routing](https://inference.ai.exchange/docs/axis-routing.md#web-search). The search costs a fixed fee and the cost of the search service. The fee is part of the `fee_usd` of the receipt. The receipt also has a `web_search` object with the service, the number of results and the charge. The search is paid from your cap. A job that fails pays nothing for it. The exchange sends the text of your last message to a search service, TinyFish first and Exa if TinyFish fails. Do not use `:web` for text that must not leave the exchange. Without `:web`, the exchange runs no search. It removes the hosted `web_search` tool of the Responses API from the request and sends the header `x-liquid-notice: web_search_dropped`. It refuses the hosted web search tool of the Messages API. ### OpenAPI document An OpenAPI 3.1 document describes the public API. The exchange serves it without authentication: ``` https://router.inference.ai.exchange/v1/openapi.json ``` You can load it into a client generator, Postman, Insomnia or Bruno. The [API reference](https://inference.ai.exchange/docs/api.md) page shows it. # Liquid CLI Liquid CLI is the `liquid` command. It starts the coding agent that you already use, with the exchange as its provider, and manages your API keys and balance. ## Start in three commands ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid claude ``` 1. The installer checks the checksum and puts `liquid` in `~/.local/bin`. It supports macOS and Linux, on Intel and Arm. 2. `liquid login` opens your browser. After you approve, it makes an API key for this computer and saves it. 3. `liquid claude` starts Claude Code. `liquid codex`, `liquid opencode` and `liquid goose` start the other agents. To limit spending, add `--cap`, the most that one request can cost in USD. The exchange charges the tokens used, never more than the cap. ```bash liquid claude --model liquid/auto:cheapest --cap 0.50 ``` ## Start a coding agent `liquid ` starts the agent that is on your `PATH`. It sets the base URL, the key and the model in the environment of that process only. It writes no file of yours. `liquid tools` lists every agent. Any argument that `liquid` does not use goes to the agent unchanged: ```bash liquid claude --model liquid/auto -p "explain this repository" liquid codex --cap 0.50 exec "say ready" ``` | Flag | Effect | | ------------- | ------------------------------------------------------- | | `-m, --model` | The model for this run. The default is `liquid model`. | | `--cap ` | The most that one request may cost in this run, in USD. | For Claude Code, the first argument can set two models as `main/sub`. The first runs the main agent. The second runs subagents and background calls. One model alone runs everything. With no argument, your saved model and `liquid.auto` do this, or `opus/auto` if you saved no model. ```bash liquid claude opus/auto liquid claude opus # same as opus/auto liquid claude glm/glm # glm-5.3 for everything ``` | Alias | Model | | -------- | ------------------- | | `opus` | `claude-opus-5-5` | | `fable` | `claude-fable-5-1` | | `sonnet` | `claude-sonnet-5-5` | | `haiku` | `claude-haiku-5-5` | | `kimi` | `kimi-k3` | | `glm` | `glm-5.3` | | `argon` | `gemini-4-argon` | | `astra` | `gpt-6-astra` | | `qwen` | `qwen3.8-max` | | `auto` | `liquid.auto` | You can use a model id on either side of the `/`, and `--model` accepts an alias. The `auto` alias is the [Auto Router](https://inference.ai.exchange/docs/auto.md) model. It does not turn on the auto mode of Claude Code. Add `--permission-mode auto` for that. To save your own alias, run `liquid model alias `. You cannot reuse the name of a built-in alias. ```bash liquid model alias fast haiku # save `fast` for claude-haiku-5-5 liquid model alias fast off # remove it ``` OpenCode takes the same `main/sub` argument. The second model runs the `general` and `explore` subagents and small calls. If a folder with that name exists, OpenCode opens it as the project, and the name sets no model. `liquid claude` keeps its settings and history in `~/.config/liquid/claude`, apart from a plain `claude`. See [A separate config directory](https://inference.ai.exchange/docs/connect-claude-code.md#a-separate-config-directory). Each `liquid claude` and `liquid opencode` session also has the [Liquid MCP server](https://inference.ai.exchange/docs/connect-mcp-server.md), so the agent can buy images, speech and video. For a desktop app or an SDK, run `liquid connect cursor` to print the setup. To run any program with the exchange in its environment, use `exec`: ```bash liquid exec -- python app.py ``` To send one request from a shell or a script, use `api`. It adds the key and the origin, so a script never holds either: ```bash liquid api GET /v1/models liquid api POST /v1/chat/completions -d '{"model":"liquid.auto","messages":[{"role":"user","content":"hi"}]}' liquid api POST /v1/audio/transcriptions -F file=@clip.wav -F model=whisper-large-v3 ``` The reply goes to stdout as the exchange sends it, a stream included. `-d @file` reads the body from a file and `-d @-` from stdin. A body is JSON unless `-H 'Content-Type: ...'` says otherwise. `-F name=@file` sends a multipart form, as `curl -F` does. A reply that is not 2xx exits with 1 and prints the status to stderr. The command takes a path and never a URL, because the host is the point: the key goes to the exchange alone. When a tool ends, `liquid` prints the request count, the cost and the largest charge against your cap. It prints this only at a terminal with a saved key. ## Choose a model ```bash liquid model # list the models, then pick the default liquid model liquid/auto:fastest # save a default that ranks offers by speed liquid model liquid/auto:smartest+web # also search the web (beta) liquid model --list # print a table, with no prompt liquid model --json # print the list as JSON ``` At a terminal, `liquid model` shows a searchable list with the name, kind (model, router model or preset) and context window. To rank the offers, add `:premium`, `:balanced`, `:economy`, `:smartest`, `:fastest` or `:cheapest`. See [Axis routing](https://inference.ai.exchange/docs/axis-routing.md) and [Purchasing rules](https://inference.ai.exchange/docs/rules.md). `+web` searches the web for each message. Web search is in beta and adds a small fee to each search. The command offers it only when the exchange has a search service. In the menu, press `w` on the ranking list to turn it on or off. ## Set a cap and other settings ```bash liquid cap 0.50 # the most one request may cost liquid cap off liquid zdr off # allow providers that may keep content liquid zdr on # use only providers that keep no content (the default) liquid config # a menu, or a table without a terminal liquid config rank economy # how router models rank offers liquid config effort high # reasoning effort; `off` removes a setting liquid config ttft 2000 # the slowest first token, in milliseconds liquid config rules region_policy=eu # one purchasing rule; name=off removes it liquid config rules '{"region_policy":["eu"]}' # or all rules, as JSON liquid config context 1m # the context window to tell Claude Code liquid config claude-share instructions,plugins # what liquid claude shares ``` A setting applies to every tool that `liquid` starts. A flag on one run, such as `--model` or `--cap`, takes priority. Only some tools use some settings: - `rank` and `cap` reach every tool. `rank` applies only to a router model name that has no ranking of its own. - `effort` reaches Codex. Claude Code chooses its own effort. - `context` reaches Claude Code. Claude Code does not know a model with a `cc.anthropic/` prefix, so without it, it assumes 200k tokens and compacts the session too early. - `claude-share` reaches Claude Code. It takes `instructions`, `plugins` and `settings`, or `none`. - `ttft` and `rules` reach Claude Code, Codex, OpenCode, Pi, Z Code and curl, as headers. `rules` sends the JSON as the `x-liquid-rules` header and cannot set `exchange.candidates.models.allow` or `exchange.rank.strategy`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Manage keys, balance and jobs ```bash liquid keys list liquid keys create ci --scope spend --spend-limit 20 --period month liquid keys revoke liquid workspace balance liquid workspace usage --days 30 liquid job liquid job ``` - The `keys` commands need a token from `liquid login --keys`. A key that you do not save with `--save` is shown once. See [API keys](https://inference.ai.exchange/docs/keys.md) for scopes and limits. - `liquid workspace balance` shows what the workspace can spend. `usage` shows the last 1 to 90 UTC days, in total and for each model. Both accept `--json`. - `liquid job` lists settled jobs, newest first. At a terminal, type to filter and press Enter for details. `--days` (1 to 90, default 7) widens the list. It uses your saved API key, which needs the `spend` or `read` scope. ## Run `liquid` with no command The command opens a menu to start a tool, pick a model, read the workspace and set the cap. With at least 80 columns and 24 rows, the full-screen menu opens. Press `/` or Ctrl-K to search. Otherwise you get a classic list. Use `--classic` or `--tui` for one run, or `liquid config ui classic|tui|auto` to save the choice. ## Reference ### Sign in `liquid login` saves a token for the command, then makes an API key named after your computer and saves it for the coding agents. The key can spend the balance of the workspace. The token cannot place jobs. Only an owner or an admin can approve a token that makes keys, and that token lasts one day. A member saves a key that an owner or admin made: ```bash liquid login --key sk-liquid-... ``` A computer with a saved key gets a read-only token for 30 days. Add `--keys` to let it manage API keys, and `--write` to let it change guardrails. The command saves settings in `~/.config/liquid/config.toml`, with mode `0600`. `LIQUID_API_KEY` and `LIQUID_BASE_URL` override the file. `liquid logout` removes the saved keys and tokens of every workspace. `liquid logout ` removes those of one workspace. ### A key for each run An agent that holds the saved key can send it to any host that it guesses. So `liquid ` and `liquid exec` do not give the agent the saved key. Each run asks the exchange for a **session key**, made by the saved key, and gives the agent that. A session key has the scope, the preset and the addresses of the saved key. It ends when the run ends, and after 24 hours in any case, so a leak costs at most the rest of one run. The saved key stays in the config file and never enters the agent's process. Session keys show in the key list with the name of the run, such as `liquid opencode 2026-11-06 19:04`. A revoke of the saved key revokes its session keys. A session key cannot make another, so `liquid` inside a run uses the key of the run. The exchange makes no session key for a key with a spend limit, because the limit counts one key, and the run then uses the saved key and says so. Each run also tells the agent where the key works. `LIQUID_BASE_URL` holds the origin of the exchange next to `LIQUID_API_KEY`. Claude Code gets a note at the end of its system prompt, and OpenCode an instruction file at `~/.config/liquid/opencode/AGENTS.md`, that the key works only at that origin and that `liquid api` sends a request from a shell. To give the agent the saved key itself, run `liquid config session-key off`. ### Switch between workspaces Each sign-in is for one workspace. The command keeps the token and key of each one. A workspace shows as `Organization › Workspace`. ```bash liquid whoami # where commands act now liquid workspace list # every saved workspace liquid workspace use acme # switch by handle, name, or a part of either liquid --workspace acme claude # act in another workspace for one command only LIQUID_WORKSPACE=acme liquid job # the same, from the environment ``` To add a workspace, run `liquid login` and pick it on the approval page. Then make a new API key or paste one that you have, because the exchange shows a secret once. In the menu, press `w` to switch. A command with `--workspace` does not change the saved workspace, and a command that writes settings refuses it. ### One key for each workspace An API key spends the balance of one workspace. The command keeps one saved key for each workspace, and the agents use the key of your sign-in workspace. - **You sign in to a different workspace.** The command uses the saved key of that workspace, or makes one. A token that cannot make keys makes none. Run `liquid login` again or use `liquid login --key`. - **An agent starts.** The command asks the exchange which workspace the key spends from. If it differs from your sign-in workspace, or the exchange cannot answer, no agent starts. If the exchange names no workspace, the agent starts with a warning. - **You set `LIQUID_API_KEY`.** The command does the same check when you are signed in. - `liquid claude` does not start if a `.claude/settings.json` or `.claude/settings.local.json` file (current folder, parent folders or home) sets `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_API_KEY` or `ANTHROPIC_BASE_URL` in `env`. Such a value can replace the key and router. ### Install and update Set `LIQUID_INSTALL_DIR` to change the install directory (default `~/.local/bin`). Set `LIQUID_CLI_VERSION`, for example `1.6.0`, to install that version. `liquid update` replaces `liquid` with the newest version after it checks the checksum. Other commands check once a day and print one line if a newer version exists. Set `LIQUID_NO_UPDATE_CHECK=1` to turn off the check. ### Narrow the model list `liquid model apply` sets the filters that Claude Code and Codex use when they read the model list. Paste a link from the models page, or name the filters: ```bash liquid model apply 'https://app.example.com/models?tab=text&provider=acme&min_context=128k' liquid model apply cost_tier=low provider=acme liquid model apply off ``` `kind`, `cost_tier`, `input`, `output`, `contested` and `byok` take a fixed list of values. The command refuses any other value and shows the list. Other keys take names, globs or numbers. `byok=true` keeps only the models of the clouds that you connected with your own keys, and `byok=false` leaves them out. Like `provider`, it needs an API key. The command skips page settings in a link that are not filters, such as the sort order, and tells you which. For one run, use `liquid claude --filter cost_tier=low`. ### Usage events When a command ends, `liquid` sends one anonymous usage event. It holds the command name, the tool id, the `liquid` version, your operating system and processor, the exit code and run time, whether a key is saved, and a random install id. It never holds a prompt, an argument, a file path, a model name, a key or a token. The install id is not linked to your account. The first run sends nothing. To turn the events off: ```bash liquid telemetry off # saved in the config file export LIQUID_TELEMETRY=off # for one shell or one CI job export DO_NOT_TRACK=1 # the common opt-out flag ``` `liquid telemetry status` shows the state and the install id. # API keys An API key lets a program send requests. The exchange charges each job to your workspace balance. To cap what one key can spend, set a spending limit when you issue it. To start, [issue a key](#issue-a-key). ## Issue a key 1. Open **API keys** and choose **Create key**. 2. Enter a name and choose an access level. Choose **Create key** to finish, or choose **Set limits** to continue. **More options** sets an expiry and a preset. 3. For a key that can spend, **Set limits** sets a credit limit and a profile: **Cap the price**, **Keep prompts private** or **Unattended agent**. **Configure guardrails** shows every field. 4. Copy the secret. The console shows it one time only. A key looks like `sk-liquid--`. The key id names the key on the **API keys** page and on every receipt. The exchange cannot show a secret again. If you lose one, revoke the key and issue a new one. A profile or a setting in **Configure guardrails** makes a guardrail for this key and links it to the key. The key follows this guardrail and the [default guardrail](https://inference.ai.exchange/docs/guardrails.md) together. If the console cannot link the guardrail, it shows the secret with a warning. Link the guardrail from the **Guardrails** page. Only workspace owners and admins can issue or revoke a key. ## Choose a scope Each key has one scope. Each scope includes the scopes below it. Give each key the lowest scope that it needs. | Scope | Allows | Typical use | | ------- | ------------------------------------------------------------------------------- | ------------------------ | | `spend` | Place jobs, and do everything below. The default. | Applications and agents | | `read` | Read the receipts, balance and job decisions of this workspace, and the market. | Dashboards and reporting | | `list` | Read market data only: the book, quote history and the live feed. | Monitoring | A request outside the scope of the key returns `403 insufficient_permission`. ## Spending limits A key can have a spending limit: an amount in USD and a period. Set a limit on each key that an unattended process uses. | Period | Behaviour | | ---------------------- | ----------------------------------------------- | | `day`, `week`, `month` | The amount refills at the start of each period. | | none | The amount applies to the lifetime of the key. | When the key reaches the limit, jobs sent with the key return `402 budget_exhausted`, even if the workspace balance is higher. A key with no limit can spend the full balance. You can change the limit after you issue the key. ## Send the key Every endpoint accepts either header: ``` Authorization: Bearer sk-liquid-… x-api-key: sk-liquid-… ``` OpenAI clients send the first. Anthropic clients send the second. ## Revoke a key Open **API keys** and choose **Revoke** on the key. The revocation starts at once, and you cannot reverse it. Jobs awarded before the revocation continue to settlement. New requests with the key return `401 invalid_api_key`. ## Keep keys safe - Issue one key for each integration. Then a revocation stops only one integration. - Do not put a key in a browser, a mobile application or any file that you distribute. - Keys belong to the workspace. Each member of the workspace can see them. ## Reference ### Expiry You can issue a key with an expiry in days, from 1 to 3650. After the expiry, the key returns `401 invalid_api_key`. A key without an expiry is valid until you revoke it. You cannot change the expiry or the scope of an issued key. ### Session keys A key that can spend can make a **session key**: a key for one run of a coding agent, with the same scope, preset and addresses. `POST /v1/exchange/session-keys` with the key as the bearer takes a `name` and a `ttl_seconds` from 1 to 86400, and answers with the new key once. `DELETE /v1/exchange/session-keys/{key_id}` ends it. A session key ends when its parent expires or is revoked, cannot make another session key, and does not count toward the 100 keys of a workspace. A key with a spend limit cannot make one, because the limit counts the spend of one key. The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) makes one for each run of an agent. ### Provider keys A provider key is a different credential. The exchange issues it to a provider, not to a workspace. It cannot spend. See [Provider credentials](https://inference.ai.exchange/docs/provider.md). ## Next steps - [Quickstart](https://inference.ai.exchange/docs/quickstart.md) - [Guardrails](https://inference.ai.exchange/docs/guardrails.md) - [Purchasing rules](https://inference.ai.exchange/docs/rules.md) # Purchasing rules If you send only `model`, the exchange buys the offer with the lowest cost for that model. The cost is the estimated charge plus the value of the predicted wait. See [Price and wait](https://inference.ai.exchange/docs/price-and-wait.md). Add an `exchange` object to the body to change that. It sets which models compete, how the exchange ranks them, and which limits an offer must meet. It is one optional root field, so an unchanged OpenAI or Anthropic SDK can send it. ## How the exchange buys Providers post **offers** on the **book**. An offer is a standing price for one model, with the deadlines that the provider promises. For each request: 1. The exchange drops each offer that fails a limit of your request. 2. It ranks the rest by cost. A [strategy](#strategy) can change the ranking. 3. The best offer wins. Its **price cap** is the most that the job can cost: the worst case, where the answer reaches its output limit and no cache saving counts. 4. The exchange holds the cap on your balance before the first token. 5. You pay the **charge**: the tokens that the provider really used. The rest of the hold returns to your balance. The charge never exceeds the cap. Each settled job has a **receipt**. See [Receipts](https://inference.ai.exchange/docs/receipts.md). If the winner fails before it writes any output, the exchange can try the next offer. After the first output, it never switches provider. ## The controls you use most Each field is optional. - `cap_usd`: an offer with a higher price cap does not compete. - `limits.ttft_ms`: an offer that promises a slower first token does not compete. - `candidates.providers.exclude`: a listed provider does not compete. - `privacy.retention`: `none` limits offers to providers that do not keep your prompt. See [Data handling](https://inference.ai.exchange/docs/data-handling.md). - `rank.strategy`, or a model suffix: `price`, `fastest`, `smartest` or `auto`. See [Strategy](#strategy). ```python response = client.chat.completions.create( model="liquid.auto", messages=[{"role": "user", "content": "Summarise this."}], extra_body={ "exchange": { "cap_usd": 0.05, "limits": {"ttft_ms": 1500}, "candidates": {"providers": {"exclude": ["px_slow"]}}, } }, ) ``` If no offer meets the limits, the exchange returns an error and charges nothing. See [Refusals](#refusals). Every other field is in the [Reference](#reference). ## Send rules The `exchange` object has six groups, in the order that you decide them: - **Candidates**: `exchange.candidates`. Which models, creators and providers compete. - **Rank**: `exchange.rank`. How the exchange ranks the offers that meet all limits. - **Limits**: `exchange.limits`. An offer must meet each limit to compete. - **Privacy**, **cache** and **session**: data rules, caching rules and session rules. `cap_usd` stays at the top of the object, and you can also send it in `limits`. ```bash curl https://router.inference.ai.exchange/v1/chat/completions \ -H "Authorization: Bearer sk-liquid-..." \ -H "Content-Type: application/json" \ -d '{ "model": "architect.basket", "messages": [{"role": "user", "content": "Say hello."}], "exchange": { "candidates": {"models": {"allow": ["gpt-oss-120b", "qwen3-*"]}}, "rank": {"strategy": {"kind": "price", "price_band_bps": 100}}, "cap_usd": 0.02 } }' ``` The Anthropic SDK takes the same `extra_body`. An unknown field in `exchange` returns `400 invalid_request`. So does `allowed_models`. ### The groups Each rule has a place in one of six groups. | Group | Rules | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `candidates` | `models.allow`, `models.exclude`, `creators.allow`, `providers.allow`, `providers.exclude`, `providers.prefer` | | `rank` | `strategy`, `cost_tier`, `fast`, `failure_risk_ranking`, `latency_nanos_per_ms`, `workload_profile`, `allow_single_supplier` | | `limits` | `cap_usd`, `cap_nanos`, `price.input_usd_per_mtok`, `price.output_usd_per_mtok`, `price.cache_read_usd_per_mtok`, `price.cache_write_usd_per_mtok`, `price.unit_usd`, `ttft_ms`, `completion_ms`, `min_tps` | | `privacy` | `retention`, `regions`, `requirements` | | `cache` | `mode`, `expected_hit_rate`, `response`, `response_ttl_seconds` | | `session` | `sticky`, `pin`, `margin` | ```json { "exchange": { "candidates": { "providers": { "exclude": ["px_slow"] } }, "limits": { "cap_nanos": 50000000, "ttft_ms": 1500 } } } ``` - `cap_nanos` is the cap as an integer of nano-USD. `50000000` equals `cap_usd` `0.05`. You can send it at the top of the object or in `limits`. - If you set one rule twice with different values, the exchange returns `400 conflicting_parameters`, and the message names both fields. It never picks one. - `session.sticky` and `session.pin` are two settings: `sticky` applies to ranking by price, and `pin` applies to Auto. - An unknown field in a group returns `400 invalid_request`. ## Models `model` sets which models compete, and whether `exchange.candidates.models.allow` and `exchange.rank.strategy` can change that. | `model` | Models that compete | `exchange.candidates.models.allow` | `exchange.rank.strategy` | | ------------------------------------- | ---------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------- | | A model, such as `gpt-oss-120b` | That model | Refused: `address_takes_no_basket` | `price` or `fastest`. Default `price`. | | A router model, such as `liquid.auto` | The candidates of the router model | Limits the candidates | `price`, `fastest` or `smartest`; `auto` if the router model has a cap. Default: its own ranking. | | `architect.basket` | The models in the basket | Required. Without it: `basket_requires_models` | `price`, `fastest`, `smartest` or `auto`. Default `price`. | | A preset | The saved models of the preset | Replaces the saved models | Replaces the saved strategy. See [Presets](#presets). | A strategy that `model` does not accept returns `400 strategy_conflicts_with_address`. `smartest` for one model returns `400 smartest_needs_several_models`. To rank by speed or score through `liquid.auto`, use a [model-name suffix](#rank-by-one-word). ### The basket `exchange.candidates.models.allow` is a list. Each entry is one of: - A served model name: `gpt-oss-120b`. - A deployment symbol: `acme/gpt-oss-120b`. It admits only that deployment. - A pattern with `*`: `gpt-*`, `*-mini`. It admits each match when the request arrives. `[]` admits each listed model, including a model listed later. If no entry matches a listed model, no offer is admitted. The exchange removes repeated entries, then refuses a blank entry (`400 invalid_basket`) and a router model or `architect.basket` as an entry (`400 basket_names_router`). To choose the ranking, use `exchange.rank.strategy`. ```json { "model": "architect.basket", "messages": [{ "role": "user", "content": "Say hello." }], "exchange": { "candidates": { "models": { "allow": ["gpt-oss-120b", "gpt-oss-20b"] } }, "rank": { "strategy": { "kind": "auto", "preference": "balanced" } }, "cap_usd": 0.02 } } ``` ## Strategy `exchange.rank.strategy` ranks the offers that meet all limits. - `price`: the lowest cost wins. See [Price and wait](https://inference.ai.exchange/docs/price-and-wait.md). - `auto`: the best published score for the estimated charge wins. It needs a price bound: a cap, a cost tier other than `any`, or an output-rate ceiling. See [Auto Router](https://inference.ai.exchange/docs/auto.md). - `smartest`: the highest published score wins, whatever the charge. It is Auto version 4 and needs a price bound. - `fastest`: the highest median output speed that the exchange measured wins. An offer with no measurement ranks last. The estimated charge breaks a tie. See [How the exchange measures performance](https://inference.ai.exchange/docs/performance-measurement.md). ```json { "kind": "price", "price_band_bps": 0 } { "kind": "auto", "preference": "balanced", "version": "2", "price_band_bps": 0 } { "kind": "smartest" } { "kind": "fastest" } ``` Fields: - `kind`: required. `smartest` and `fastest` take no other field. - `price_band_bps`: `price` and `auto` only. Offers within this many basis points of the lowest cost count as one cost. 0 to 10000, default 0. A larger value returns `400 invalid_price_band`. - `preference`: `auto` only. `economy`, `balanced` or `premium`. - `version`: `auto` only. `"1"` to `"8"`, default `"8"`. Version 4 is `smartest`. ### Fewest redos Set `exchange.rank.failure_risk_ranking` to `true` to rank each offer at its estimated charge divided by the chance of a good answer. ```json { "exchange": { "rank": { "failure_risk_ranking": true } } } ``` The chance comes from the rates of the deployment over the last 7 days: failures that the provider caused, answers that came after the deadline, bad tool calls, and bad structured output. A late answer counts by how late it was, and the late rate has half the weight of a failure, because you got and paid for the answer. A deployment with no rate uses the rate of its model. - The rule changes the order of the offers and nothing else. The price cap, the hold and the charge do not change. - The chance of a redo counts at most 50%, so an offer ranks at no more than twice its charge. - It applies to ranking by price and to [Auto](https://inference.ai.exchange/docs/auto.md). The request value wins over the preset. - A request with `response_format` of `json_object` or `json_schema` uses the rule by default. Send `false` to rank by charge alone. ### Preferred providers Set `exchange.candidates.providers.prefer` to try providers in order, then use the rest of the eligible market. See [Preferred providers](https://inference.ai.exchange/docs/provider-preferences.md). ### Rank by one word Add `:smartest`, `:fastest` or `:cheapest` to the model name to choose the ranking. `exchange.rank.strategy` accepts the same words. See [Axis routing](https://inference.ai.exchange/docs/axis-routing.md). ## Presets A preset saves models, a strategy and limits under a name. Create one on the **Presets** page. Then send the name as `model`: ```json { "model": "fast", "messages": [ … ] } ``` A value in the request replaces the saved value: - `exchange.candidates.models.allow` replaces the saved models. - `exchange.rank.strategy` replaces the whole saved strategy. A field that it omits gets its default. - Each limit that the request sends replaces the saved limit. A preset belongs to one workspace. Its name cannot be `architect.basket`. Its models cannot include a router model, `architect.basket` or another preset. A preset with strategy `auto` needs a price bound. The bound can come from the request, the `x-liquid-cap-usd` header, the preset or the key. The editor sets the cost tier `high` if you set no bound. The **Presets** page previews a sample request of 4,000 input and 1,000 output tokens. It buys nothing. A preset buys one job kind: `chat` (default), `image_generation`, `audio_speech` or `video_generation`. Set `kind` when you save. If a save omits it, the kind stays. Auto and token-based price or speed rules apply only to chat, so the exchange refuses to save them on another kind. See [Modalities](https://inference.ai.exchange/docs/modalities.md). ### A preset from a receipt On a receipt, select **Save as preset**. The editor starts from what the request asked for, not from the winning offer. A preset that copies the winner has one model at one provider, so each request fails when that provider pauses. Four stops set how much of the job the preset keeps: - **Exact replay**: only the winning model and provider. - **Same model**: the winning model, any provider. - **Your order**: what the request asked for. This is the default. - **Open**: models and strategy, no limits. The preset keeps a limit that a router model added, such as the default cap of `liquid.auto`. ### A preset linked to a key A key linked to a preset limits each request with that key, for any `model`. The request cannot widen those limits. - **Models.** Only the preset's models can serve. A request that names some of them is limited to those. A request that names none returns `403 key_policy_violation`. An empty list sets no limit. - **Limits.** If both set a limit, the stricter applies: the lower `cap_usd`, `limits.ttft_ms`, `limits.completion_ms`, `limits.price.*` and `rank.cost_tier`; the higher `limits.min_tps`; and `privacy.retention: "none"`, `rank.fast: true` and `rank.allow_single_supplier: false`. Excluded models and providers combine. An offer must pass `exchange.candidates.providers.allow`, `exchange.candidates.creators.allow` and `exchange.privacy.regions` of both. A different `exchange.rank.workload_profile` returns `403 key_policy_violation`. - **Strategy.** The key does not enforce it. A request that names the preset and sends no strategy uses the saved one. ## Headers A client that cannot change the body can send rules as headers. A header that cannot be read returns `400 invalid_header`. No header sets models or ranking. To set them from headers only, use a [preset](#presets). ### Header or body Each purchasing rule has a field in the `exchange` block. Some rules also have a header. When both set the same rule, one of them wins, as the table shows. In general the body wins: a header fills only a rule that the body and the preset leave unset. | Rule | Body field | Header | Which wins | | -------------------------------- | -------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | Price cap | `exchange.cap_usd` | `x-liquid-cap-usd` | The body. | | Time to first token | `exchange.limits.ttft_ms` | `x-liquid-ttft-ms` | The body. | | Workload profile | `exchange.rank.workload_profile` | `x-liquid-profile` | The body. | | Retention | `exchange.privacy.retention` | `x-liquid-retention` | The body. A preset linked to the key can make it stricter, never weaker. | | Expected cache hit rate | `exchange.cache.expected_hit_rate` | `x-liquid-cache-hit-rate` | The body. | | Response cache on or off | `exchange.cache.response` | `x-liquid-cache` | The body. `response: false` turns the cache off even if the header is `true`. | | Response cache lifetime | `exchange.cache.response_ttl_seconds` | `x-liquid-cache-ttl` | The header. A header lifetime outside 1 to 86400 seconds is refused. A body lifetime outside it is moved into the range. | | Clear the stored answer | None | `x-liquid-cache-clear` | Only the header sets it. | | Any rule of the `exchange` block | The field of the rule | `x-liquid-rules` | The body, the preset and the single-rule headers, one rule at a time. A list in the header applies only if the body did not send that list. | | Auto preference | `exchange.rank.strategy.preference` | `x-liquid-policy` | The body. The header fills a preference that no other field set. A price ranking ignores it. | | Candidates, models, ranking | The fields of the groups | None | Only the body or a preset sets them. | | Pin a job to one offer | None | `x-liquid-pin-offer` | Only the header sets it. A key that cannot pin offers gets `403 pin_not_permitted`. | | Cost tag | None | `x-liquid-tag` | Only the header sets it. See [Cost tags](#cost-tags). | | Routing detail in the response | None | `x-liquid-explain` | Only the header sets it. See [Explain the choice](#explain-the-choice). | | Repeat a request safely | `idempotency_key`, on routes that move money | `idempotency-key` | The header and the body must agree, or the route refuses the request. | A preset counts as part of the body for this table. A key's preset and the guardrails of the workspace limit both. See [Presets](#presets). The headers that need more words: - `x-liquid-retention`: `none` or `allowed`. - `x-liquid-cache-hit-rate`: 0 to 1. It is not `x-liquid-cache`, which switches the response cache. - `x-liquid-rules`: a whole `exchange` block as JSON. See [Rules in one header](#rules-in-one-header). - `idempotency-key`: a repeated request gets the first response. No second job runs. ### Rules in one header `x-liquid-rules` holds the `exchange` block as JSON. Use it for rules that have no header of their own: ```text x-liquid-rules: {"privacy":{"regions":["eu"]},"candidates":{"providers":{"exclude":["acme"]}}} ``` - It is the lowest layer. The body, the preset and the single-rule headers win over it, one rule at a time. A list in the header applies only if the body did not send that list and the preset left it empty. - It cannot set `candidates.models.allow` or `rank.strategy`. Use a preset. - A block that is not valid JSON, or has an unknown rule, returns `400 invalid_header`. The header holds ASCII only. ### Cost tags Send `x-liquid-tag` to label what a call is for, such as a team or a project. A report can group the spend by the label. The exchange never sends the tag to a provider. A tag has 1 to 63 characters, starts with a letter or a digit, and holds letters, digits, dots, dashes and underscores. The exchange changes upper case to lower case. A tag that does not fit returns `400 invalid_header`, and the call does not run. ## Response Each response has an `exchange` object. A stream sends it in the last chunk. It holds the `job_id` (also the `x-liquid-job-id` header), the `provider_slug`, the price cap in `charge.cap_usd`, what you paid in `charge.total_usd`, the metered `usage`, and `timing.service`: `met` if the provider kept its deadlines, else `missed`. You still pay for a late job. [Response fields](#response-fields) has every field. ### Explain the choice Send `x-liquid-explain: true` to add a `routing` object to the `exchange` block. This example is shortened. ```json { "exchange": { "job_id": "job_01k9x2", "provider_slug": "acme", "status": "settled", "charge": { "cap_usd": 0.0134, "fee_usd": 0.000038, "total_usd": 0.000804 }, "timing": { "ttft_ms": 120, "completion_ms": 900, "service": "met" }, "routing": { "clearing_rule": "first_price", "offers_considered": 9, "offers_eligible": 2, "single_supplier": false, "eligibility": { "excluded": [ { "provider": "beta", "reason": "service_too_slow", "count": 4 }, { "provider": "gamma", "reason": "region_policy", "count": 3 } ] } } } } ``` ### Read a job later `GET /v1/jobs/{job_id}` returns the same block for one job, with `routing` always included. A key with `read` scope or higher can call it. A job of another workspace returns `404`, the same as a job that does not exist. The exchange writes the receipt just after it answers, so a call at once can return `404`. Try again after a moment. ## Refusals No eligible offer returns `503 no_eligible_offer`. If each offer only lacks a parameter of the request, the error is `400 unsupported_parameter`, and `param` names the parameter. `eligibility.excluded` has one entry per considered offer with its reason. Reason codes are in [Errors](https://inference.ai.exchange/docs/errors.md). ## Reference ### Cost - `cap_usd`: maximum charge for the job. An offer with a higher worst-case charge is not eligible. - `exchange.limits.price.input_usd_per_mtok`, `exchange.limits.price.output_usd_per_mtok`: maximum published input and generation rates, USD per million tokens. - `exchange.limits.price.cache_read_usd_per_mtok`, `exchange.limits.price.cache_write_usd_per_mtok`: the same for cache rates. An offer with no such rate passes. - `rank.cost_tier`: `any`, `low`, `medium`, `high`, `xhigh` or `max`. It overrides the tier of the router model. `any` removes the limit. ### Exchange fee The fee of a job is the larger of 5% of the supplier charge and a floor for each metered token. `charge.fee_usd` shows it. See [Fees](https://inference.ai.exchange/docs/fees.md). - The floor of a kind is at most 1 cent per million tokens (0.1 cent for a cache read). - An offer that charges nothing for a kind pays 5% of the model's list price or mean rate, when one exists, and at most that amount. - A job on a free offer, such as `liquid.free`, pays the floor. Its cap and hold are above zero. - A job priced by image, character or second pays no floor. ### Service - `limits.ttft_ms`: maximum time to first token. The deadline adds 1 second per 10,000 input tokens, for the prompt read. - `limits.completion_ms`: maximum time to completion. - `exchange.limits.min_tps`: minimum generated tokens per second that the offer's deadline must give. - `rank.fast`: a boolean for a stricter service threshold, with no numeric value. ### Eligibility - `exchange.candidates.models.exclude`: names or patterns (`gpt-*`, `*-mini`) that must not serve the job. It wins over the basket. - `exchange.candidates.providers.allow`: handles or patterns that can compete. `[]` and `["*"]` admit all. - `exchange.candidates.providers.prefer`: handles or patterns to try first, in this order. It is not a limit. See [Preferred providers](#preferred-providers). - `exchange.candidates.providers.exclude`: handles that must not compete. - `exchange.candidates.creators.allow`: creators whose models can compete. Case and known aliases match (`meta-llama` = `meta`, `qwen` = `alibaba`). - `exchange.privacy.regions`: regions where the deployment can be. - `exchange.privacy.retention`: `none` or `allowed`. An offer that cannot obey `none` is not eligible. - `exchange.privacy.requirements`: `{ "require": [claim, ...], "checked_only": bool }`. The provider must hold every claim. With `checked_only`, a declared claim does not pass. A lapsed claim never passes. See [Provider claims](https://inference.ai.exchange/docs/provider-claims.md). - `exchange.rank.workload_profile`: a named profile. A saved workload has the profile `.@v`. A profile of another workspace gets `400 invalid_request`. - `exchange.rank.allow_single_supplier`: accept an award when only one independent supplier is eligible. Default `true`. An empty list is a value. `"candidates": {"providers": {"allow": []}}` on a request clears the preset's rule. ### Prompt caching - `exchange.cache.mode`: `auto` (default), `buyer` or `off`. Who places prompt-cache breakpoints. See [Prompt caching](https://inference.ai.exchange/docs/caching.md). - `exchange.cache.expected_hit_rate`: 0 to 1. The share of input tokens that you expect to be billed at the cache read rate. #### Expected cache hit rate By default the exchange ranks offers as if no input were cached. For a job that reads most of its input from a cache, an offer with a cheap input rate and an expensive cache read rate can rank above an offer that costs you less. Set `exchange.cache.expected_hit_rate`, such as `0.99`, and the exchange ranks each offer with a cache read rate on the input that it expects to bill at that rate. - The rule changes the order of the offers and nothing else. The cap, the hold and the charge do not change. - An offer with no cache read rate, or with a cache read that costs no less than uncached input, gets no credit. - `0` turns the credit off. A value outside 0 to 1 returns a `400` that names the field. - The request value wins over the preset. It applies to ranking by price and to [Auto](https://inference.ai.exchange/docs/auto.md). - A rate higher than the share that the provider really reads can make you pay more than the best offer would have cost. If you set no rate, the exchange can use a measured share, when its operator turns the setting on. It is the share of your workspace's input that repeated in the last 7 days, and it needs 50 settled jobs. Your rule wins over it. When a rate applied, the `exchange` block holds `cache_hit_rate`: - `expected`: the share of input that ranking billed at the cache read rate. - `source`: `buyer` or `measured`. - `credit_usd`: what the credit took off `charge.estimated_usd`, the ranking estimate. - `real`: `usage.cached_tokens` divided by `usage.prompt_tokens`, once the job has usage. A `real` rate far below `expected` means that your rate is too high, or that the provider did not read its cache. ### Response fields [Receipts](https://inference.ai.exchange/docs/receipts.md) describes each stored field, including `charge.uncached_usd`, `created_at`, `charge.cache_usd`, `finish_reason` and the media quantities. [Fees](https://inference.ai.exchange/docs/fees.md) explains `charge.fee_usd`. - `job_id`, `provider_slug`, `offer_id`: the job, the serving provider and the winning offer. - `status`: `pending` until usage settles, then `settled` or `credited`. - `charge.cap_usd`: the price cap, locked before the first token. - `charge.total_usd`: supplier charge plus fee, minus credits. `charge.fee_usd` and `charge.credits_usd` are parts of it. - `charge.cap_held_usd`: what the cap took off the metered charge, fee included. Present only when the job used more than its cap. The provider carries it. - `usage.prompt_tokens`, `cached_tokens`, `cache_write_tokens`, `completion_tokens`: metered usage in the provider's tokenizer. `prompt_tokens` includes cache reads and writes. - `usage.reasoning_tokens`: hidden reasoning tokens, billed as output. Present only when the provider reports them. - `timing.ttft_ms`, `timing.completion_ms`: measured time to first token and to completion. TTFT starts at request arrival, so it includes the auction. - `timing.ttft_limit_ms`: your TTFT limit. Absent for image jobs and old receipts. - `timing.ttft_deadline_ms`: `ttft_limit_ms` plus 1 second per 10,000 input tokens. The job is `met` or `missed` against this deadline, not against the provider's promise. - `timing.promised_ttft_ms`: the time that the winning offer promised. A `met` job can be slower. - `timing.service`: `met`, `missed`, `failed`, `pending`, `unknown`, `not_started` or `single_supplier_refused`. A late job is charged like any other. - `addressing`: the requested name, how it resolved, and the model and precision that served it. `addressing.auto` records why Auto chose. See [Auto Router](https://inference.ai.exchange/docs/auto.md). - `answer_checks.structured_output`: `result` and, for `schema_mismatch`, the failed `keyword`. Present only with `json_object` or `json_schema`. See [Structured output](https://inference.ai.exchange/docs/errors.md#structured-output). - `answer_checks.tool_calls`, `tool_call_errors`, `tool_call_faults`, `tool_markup`: the tool calls, the bad ones, the bad ones by kind, and `1` when the text has tool-call markup but the answer has no tool call. Present only with `tools`. See [Tool calls](https://inference.ai.exchange/docs/errors.md#tool-calls). - `buyer.client`: the program that sent the request. See [Client names](#client-names). - `routing`: present only with `x-liquid-explain: true`. See [Explain the choice](#explain-the-choice). ### Client names A receipt shows `buyer.client` only for a client that the exchange lists, such as `claude-code` or `codex`. The exchange reads the user agent and `X-Title` to find it. It never shows the user agent, `Origin`, `Referer` or an unlisted name, because that text can name a project or an internal host. ### Router models A request for `liquid.auto` admits each listed model in its cost tier, and no other router model. Router models have no versions, so `liquid.auto@3` is not one. `GET /v1/exchange/routers` lists each router model with its rules, its ranking (price or Auto) and the models it admits now. `exchange.candidates.models.allow` limits the candidates to the basket. The router model's rules still set each limit that the request omits. See [Auto Router](https://inference.ai.exchange/docs/auto.md). ### Older fields These fields still work. Sending one with the field that replaced it returns `400 invalid_request`. - `exchange.policy`: use `exchange.rank.strategy.preference`. - `exchange.approved_models`: use `exchange.candidates.models.allow` with `model` set to `architect.basket`. - `exchange.price_band_bps`: use `exchange.rank.strategy.price_band_bps`. `exchange.allowed_models` is removed and returns `400 invalid_request`, because its patterns could admit a deployment that a model list cannot name. Use `exchange.candidates.models.allow`. ### OpenRouter `provider` A client that targets OpenRouter sends a `provider` object. The exchange applies each setting that it can apply exactly, and refuses every other setting. It never ignores one. A `provider` that is `null` or `{}` asks for nothing. To move a whole integration, see [Move from OpenRouter](https://inference.ai.exchange/docs/move-from-openrouter.md). - `max_price.prompt`, `max_price.completion`: `exchange.limits.price.input_usd_per_mtok` and `exchange.limits.price.output_usd_per_mtok`. A number or numeric string. - `only`: `exchange.candidates.providers.allow`. - `order`: `exchange.candidates.providers.prefer`. With `allow_fallbacks: false`, it is also `exchange.candidates.providers.allow`, so only those providers compete. - `ignore`: `exchange.candidates.providers.exclude`. A name must be a provider handle, or a pattern with `*`. - `data_collection: "deny"`: `exchange.privacy.retention` becomes `none`. `"allow"` changes nothing. - `zdr: true`: requires the `zero_retention` claim in `exchange.privacy.requirements`. - `sort: "price"`: `exchange.rank.strategy` becomes `price`. Refused when the strategy is another kind. - `require_parameters`, `allow_fallbacks: true`: accepted. The exchange already matches on the features that the request needs. The exchange refuses these with `400 unsupported_parameter`, and `param` is `provider`: `quantizations`, `preferred_min_throughput`, `preferred_max_latency`, `max_price.request`, `max_price.image`, `allow_fallbacks: false` without `order`, any other `sort`, and any unknown setting. One answer lists every refused setting. It also refuses a negative or non-numeric price and an `ignore` name that no provider has. `provider` can only narrow the limits. The exchange applies it after `exchange` and keeps the stricter value of each field. It refuses the request if `only`, `order` with `allow_fallbacks: false` and `exchange.candidates.providers.allow` share no provider, or if `order` and `exchange.candidates.providers.prefer` list different providers. The preset of the key and the guardrails of the workspace limit `provider` as they limit `exchange`. A field in `provider` replaces the value of a preset that you name as `model`. # Bring your own key (BYOK) BYOK sends requests through your own account at a cloud. Connect a key once, then call the same API as for any request. The cloud bills you for the tokens. The exchange charges only its [fee](https://inference.ai.exchange/docs/fees.md). You keep your own agreement and quota. The exchange seals your key when you connect it. The console shows only the last four characters. ## Clouds | Cloud | Name in the API | Key kinds | Region | | ---------------- | --------------- | ------------------------------------------- | ----------------------------------- | | Anthropic | `anthropic` | `anthropic_api_key` | None. The exchange stores `global`. | | Amazon Bedrock | `bedrock` | `aws_access_key`, `aws_api_key` | The AWS region of your models. | | Google Vertex AI | `vertex` | `gcp_service_account` | The Vertex location, `us-east5`. | | Azure OpenAI | `azure_openai` | `azure_api_key`, `azure_client_credentials` | None. The endpoint names the place. | | OpenAI | `openai` | `openai_api_key` | None. The exchange stores `global`. | | Google Gemini | `gemini` | `gemini_api_key` | None. The exchange stores `global`. | | Other makers | See below | `_api_key` | None. The exchange stores `global`. | The other makers and hosts are `xai`, `mistral`, `deepseek`, `moonshot`, `zai`, `qwen`, `together`, `fireworks`, `groq` and `cerebras`. Each takes one API key and uses the OpenAI chat API. `qwen` uses the international endpoint of Alibaba Model Studio. `gemini` means a key from Google AI Studio. Use `vertex` for a Google Cloud project. An operator can turn a cloud off. You then get `403 upstream_disabled` when you connect a key for it. ## Connect a key Choose a cloud in the cloud accounts of your workspace. Only an owner or an admin can connect a key, with a recent sign-in. Only a Clerk session works. An API key or a CLI token cannot connect a key. The console calls `POST /v1/workspaces/{slug}/byok/grants`: ```json { "upstream": "bedrock", "kind": "aws_access_key", "region": "us-east-1", "grant": { "access_key_id": "AKIA…", "secret_access_key": "…" } } ``` | Kind | Fields of `grant` | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `anthropic_api_key` | `api_key` (starts with `sk-ant-`), and optional `workspace_id`. The exchange refuses an Admin key (`sk-ant-admin01-`). | | `_api_key` | `api_key`. Allowed characters are letters, digits, `.`, `-` and `_`. The key must start as the cloud requires, such as `sk-` for `openai`, `AIza` for `gemini` or `xai-` for `xai`. | | `aws_access_key` | `access_key_id`, `secret_access_key`, and `session_token` for temporary keys. | | `aws_api_key` | `api_key`: a Bedrock API key. | | `gcp_service_account` | The key file of the service account, as Google writes it. | | `azure_api_key` | `api_key`, `endpoint`, `api_version` and `deployments`. | | `azure_client_credentials` | `tenant_id`, `client_id`, `client_secret`, `endpoint`, `api_version` and `deployments`. | List 1 to 32 Azure `deployments` yourself. Each has a `name` and a `model`, and can have a `context_window` and `vision`. Use the least rights that work. For Bedrock, allow model invocation and `bedrock:GetFoundationModelAvailability`. ### Bedrock model access The exchange offers only the Bedrock models that your account can call, which it reads with `bedrock:GetFoundationModelAvailability`. Without that action, it offers every Bedrock model, and some can fail at the first call. The grant then shows `missing_permission`. Allow the action and test the key to clear the notice. ### Rotate, revoke and test - A workspace can hold one live key of each kind for each cloud. - Rotate a key with `PUT /v1/workspaces/{slug}/byok/grants/{id}`. - Revoke a key with `DELETE`. This deletes the sealed key and stops service at once. - With several live keys for one cloud, the exchange tries the oldest first. It tries the next key only when the cloud refuses the credentials. The exchange does not test a key at connect. Send `POST …/grants/{id}/test`. The answer is `202`, and the result is in `last_test_ok`. ## Find your models `GET /v1/workspaces/{slug}/byok/models` lists the models that your live keys offer. Each entry has `upstream` (the cloud), `name` (use it as the `model` of a request), `upstream_model` (the id at the cloud, used by the model scope) and `max_input_tokens` (absent when the key states none). A new key shows its models after a few seconds. A model that the cloud adds later appears too, unless the key has a model scope. `GET …/byok/models/unavailable` lists the models that the cloud lists and your account cannot call. Only Bedrock reports them now. Each entry has `upstream`, `name`, `upstream_model` and a `reason`. The `reason` is `not_in_region`, `agreement_missing`, `not_entitled` or `not_authorized`. ## Send a request Name the cloud in the model: `[byok]/`. ```bash curl https://router.inference.ai.exchange/v1/chat/completions \ -H "Authorization: Bearer sk-liquid-..." \ -H "Content-Type: application/json" \ -d '{ "model": "anthropic[byok]/claude-opus-5-5", "messages": [{ "role": "user", "content": "Hello" }] }' ``` The mark uses the names in the table above, with `azure-openai` for Azure. If your key cannot serve the request, your routing policy decides. See [When your account cannot serve](#when-your-account-cannot-serve). With a plain model name, such as `claude-opus-5-5`, your offer competes with the market in the usual ranking. Only your workspace can buy from your account. For the guardrails that apply, see [Your own cloud keys](https://inference.ai.exchange/docs/guardrails.md#your-own-cloud-keys). ### Parameters that some models refuse The exchange refuses these before it calls your cloud, so the cloud does not bill you. | Model | Refused | | ----------------------------------------------- | ---------------------------------------------------------------------- | | Claude Opus 4.7 and later, every Claude 5 model | `temperature`, `top_p`, `top_k` and a thinking budget | | Claude Opus 5.5, Sonnet 5.5, Fable 5.1 | Also a `tool_choice` that forces a tool | | OpenAI GPT-6 models on Bedrock | `temperature`, `top_p`, `stop`, and a `tool_choice` that forces a tool | | gpt-oss | `stop` | The GPT-6 models read images but not documents. A Bedrock model with a long-context price takes only a prompt below that size through your key: 100,000 tokens for Claude Haiku 5.5 and 272,000 for the GPT-6 models. ### Models of an API-key cloud - The models are the chat models that the cloud lists for the key. - These clouds send the whole answer at once, except Anthropic. A streamed request shows output when the model has finished. - OpenAI takes the full set of request features. The other clouds take tools, JSON mode, `top_p` and stop sequences only. - The exchange offers a model only when it knows the maker from the name. A name such as `meta-llama/Llama-3.3-70B-Instruct-Turbo` waits until an operator links it to a model. ## What a request costs The cloud bills you at your own agreement, so the supplier charge is zero. The exchange computes its fee on the charge at the list price of the model, which the maker publishes. The same list price ranks your offer against the market. A cheaper market offer can win. A model with no list price pays the token floor. See [Fees](https://inference.ai.exchange/docs/fees.md). The receipt shows the list price charge in `byok.list_charge_usd`. ## Control a key Change the settings in the console, or with `PATCH /v1/workspaces/{slug}/byok/grants/{id}`. A field that you leave out stays as it is. | Setting | Effect | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `model_scope` | The models that the key may serve, by `upstream_model`. `null` allows all. An empty list is refused. | | `api_key_scope` | The API keys of your workspace that may use the key. `null` allows all. A job with no API key cannot use a scoped key. | | `retention_claim` | What you state about how the cloud keeps your prompts: `default`, `zero` or `keeps`. See [Data handling](https://inference.ai.exchange/docs/data-handling.md). | | `limits` | `max_parallel`, `rpm` and `tpm` (any 60 seconds), `budget_tokens` with `budget_period` (`day` or `month`), `spend_day_nanos` and `spend_month_nanos` (nano-USD at list price, per UTC day or month). `null` clears them. | A request with `exchange.privacy.retention` set to `none` goes to your key only when every key that can serve the model has the claim `zero`. With no claim, only Bedrock counts as zero retention. It does not count for Claude Fable or for OpenAI models other than gpt-oss, because AWS keeps that traffic for up to 30 days. A claim of `keeps` never counts. `GET …/byok/grants/{id}/usage` shows today's tokens and requests, and the use of each limit. ## When your account cannot serve A limit, a refused key or a cloud error can fail a request. Set a routing policy for each cloud with `PUT /v1/workspaces/{slug}/byok/routing/{upstream}` and a body such as `{"on_failure": "fail"}`. Read it with `GET`. For Azure, `{upstream}` is `azure_openai`. | `on_failure` | Effect | | ------------ | --------------------------------------------------------------- | | `market` | The default. The router sends the job to the next market offer. | | `fail` | The job ends with an error. Nothing goes to the market. | A fallback happens only before the cloud sends any output. After the first byte, the job stays on your account. A job that falls back is charged as a market job. `GET /v1/workspaces/{slug}/byok/requests` lists the newest jobs that had a BYOK leg. Each entry has the outcome, whether `byok` or the `market` served it, the tokens, the charge, and `fallback_reason` when the market served a job. ## Errors | Status and code | Meaning | | ----------------------- | ------------------------------------------------------------------------------ | | `400 invalid_grant` | The secret lacks a field or has a value that the cloud cannot use. | | `400 admin_key_refused` | The Anthropic key is an Admin key. Create an API key in the Claude Console. | | `400 invalid_settings` | A setting has a value that is not valid. | | `403 upstream_disabled` | An operator turned this cloud off. | | `404 no_grant` | The key does not exist in your workspace. | | `409 grant_exists` | The workspace has a live key of this kind for this cloud. Rotate or revoke it. | To sell through BYOK, see [Sell with bring your own key](https://inference.ai.exchange/docs/provider-byok.md). # Preferred providers Use `exchange.candidates.providers.prefer` to try providers in a specified order, then use the rest of the eligible market. A preference changes the order of eligible offers. It does not exclude other providers. Use `exchange.candidates.providers.allow` when only the specified providers can serve the request. If you want fallback to other providers, replace that allowlist with a preference. ## Send a request 1. Choose a model from `GET /v1/models`. 2. Get provider handles from the model's page on the exchange. 3. Replace the example model and provider handles with your choices. 4. Send the request with a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md) if you need a key. ```bash curl https://router.inference.ai.exchange/v1/chat/completions \ -H "Authorization: Bearer $LIQUID_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-oss-120b", "messages": [{ "role": "user", "content": "Say hello." }], "max_tokens": 64, "exchange": { "candidates": { "providers": { "prefer": ["acme", "globex"] } }, "cap_usd": 0.05 } }' ``` With the OpenAI Python SDK, put `exchange` in `extra_body`: ```python import os from openai import OpenAI client = OpenAI( base_url="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], ) response = client.chat.completions.create( model="gpt-oss-120b", messages=[{"role": "user", "content": "Say hello."}], max_tokens=64, extra_body={ "exchange": { "candidates": { "providers": { "prefer": ["acme", "globex"] } }, "cap_usd": 0.05, } }, ) print(response.choices[0].message.content) ``` ## How the exchange orders offers For the example above, an eligible offer from `acme` ranks before an eligible offer from `globex`. Other eligible offers follow. If neither preferred provider has an eligible offer, the rest of the market can serve the request. - Each entry is a provider handle or a pattern with `*`, such as `acme-*`. - The first entry that matches a provider sets its preferred position. Put specific handles before broader patterns. - Offers at the same preferred position use the request's usual order, such as price or speed. - A [reservation](https://inference.ai.exchange/docs/reservations.md) leg ranks before a preferred provider. A preferred provider can cost more than another provider. Keep a price cap and any required limits in the request. Every offer must pass the request's limits, the preset of the key, and the guardrails of the workspace. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Fallback before output If an award fails before any output, the exchange can try the next eligible offer. The next offer can come from another preferred provider or from the rest of the market. After output starts, the exchange does not switch providers for that response. An allowlist still limits fallback. For example, this request permits only `acme` and `globex`, with `globex` first: ```json { "exchange": { "candidates": { "providers": { "allow": ["acme", "globex"], "prefer": ["globex"] } } } } ``` To permit fallback across the eligible market, remove the request's `exchange.candidates.providers.allow` limit. The limits of the key and the workspace still apply. ## OpenRouter requests The exchange maps `provider.order` to `exchange.candidates.providers.prefer` on Chat Completions, Messages and Responses requests. This object asks for the same provider order as the first example: ```json { "provider": { "order": ["acme", "globex"], "allow_fallbacks": true } } ``` Use provider handles from the exchange. Do not assume that an OpenRouter provider name is the same handle. With `allow_fallbacks: false`, only providers in `order` can compete. The exchange can still try the next eligible provider in that list before any output. If `provider.only` or `exchange.candidates.providers.allow` is also present, an offer must pass all these lists. Send either `provider.order` or `exchange.candidates.providers.prefer`. If you send both non-empty lists, they must contain the same entries in the same order. Different lists return HTTP `400`. The exchange refuses `allow_fallbacks: false` without a non-empty `order`. See [Move from OpenRouter](https://inference.ai.exchange/docs/move-from-openrouter.md) for the full migration guide. ## Presets and Auto A named [preset](https://inference.ai.exchange/docs/rules.md#presets) can save preferred providers with a price or speed strategy. The request's list replaces the saved list. An empty request list clears the saved preference: ```json { "exchange": { "candidates": { "providers": { "prefer": [] } } } } ``` A guardrail cannot hold provider preferences. A preset linked to an API key does not apply its preferences. To apply saved preferences, send the preset's name as `model`. [Auto](https://inference.ai.exchange/docs/auto.md) and `smartest` rank by score. They refuse a non-empty preference list with `400 preferred_providers_with_auto`. This rule also applies to `provider.order` and preset saves. Use a named model or a basket with a price or speed strategy. ## Check the order without a purchase `POST /v1/exchange/dry-run` shows eligible offers in award order. It buys nothing and holds no funds. Send preferences in `exchange`, because this endpoint does not read `provider`: ```bash curl https://router.inference.ai.exchange/v1/exchange/dry-run \ -H "Authorization: Bearer $LIQUID_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-oss-120b", "exchange": { "candidates": { "providers": { "prefer": ["acme", "globex"] } }, "cap_usd": 0.05 } }' ``` If a provider limit excludes every offer, inspect `error.binding_rule` in the error response. See [Auction errors](https://inference.ai.exchange/docs/errors.md#auction) for the cause and the required change. # Receipts Each job has one receipt. It records what you asked for, which offer won, what the job used, what it cost and how fast it ran. The exchange writes it just after it answers. A receipt is final once its `settlement_status` is `settled`, `credited` or `released`. Use the receipt to check a charge. `supplier_charge_usd` plus `fee_usd`, less `credits_usd`, is `buyer_charge_usd`. [Fees](https://inference.ai.exchange/docs/fees.md) explains the fee. ## Read a receipt - `GET /v1/jobs/{job_id}`: one receipt. The job ID is in the `x-liquid-job-id` header and in `exchange.job_id`. - `GET /v1/exchange/receipts`: your receipts, newest first. It takes `limit`, `cursor`, `model` and `outcome` (comma-separated lists for the last two) and returns `receipts` and `next_cursor`. - The console shows the same receipts with filters and charts. The `list_receipts` MCP tool lists them for an agent. A key with `read` scope or higher can call these routes. A job of another workspace returns `404`. A call at once after the answer can also return `404`. Try again after a moment. ```bash curl "https://router.inference.ai.exchange/v1/exchange/receipts?limit=20&outcome=completed" \ -H "Authorization: Bearer $LIQUID_API_KEY" ``` A stored receipt is one flat JSON object. A receipt for a job with a winner also has the `exchange` block of the live response, and can have `requested_purchase`. An absent field does not apply to the job. It is not zero. Money fields are USD numbers. A rate is USD per million tokens. ## The exchange block The `exchange` block of a live response has a `charge` object and a `usage` object. They name the same money as the fields below. - Each amount has two fields. `supplier_usd` is a JSON number in USD. `supplier_nanos` is the same amount as an integer of nano-USD, so it is exact. `cap`, `estimated`, `supplier`, `fee`, `credits` and `total` each have both fields. - `supplier_*` is what the provider earns, before the exchange fee. `total_*` is what you paid: `supplier` plus `fee`, less `credits`. - `usage` counts tokens as `prompt_tokens`, `cached_tokens`, `cache_write_tokens` and `completion_tokens`. `reasoning_tokens` appears when the provider reports it. - `usage.pricing` lists the use by billable. See Usage below. ```json "charge": { "cap_nanos": 50000000, "supplier_nanos": 1200000, "fee_nanos": 60000, "credits_nanos": 0, "total_nanos": 1260000 }, "usage": { "prompt_tokens": 120, "completion_tokens": 40, "pricing": [{ "billable": "output_text", "unit": "mtok", "quantity": 40, "usd": 0.000019 }] } ``` ## Identity - `job_id`: one logical request has one job. - `award_id`: the award of the job to an offer. Empty when no offer took the job. It is also the idempotency key that the provider receives. - `tenant`, `key_id`: your workspace and the API key. `key_id` is absent for a job from the console. - `client`: the program that sent the job: `name` and `version`. - `cost_tag`: the label from your `x-liquid-tag` header. A provider never sees it. - `applied_preset`, `named_preset`: the preset that the key applied, and the preset that you named as `model`, each with its revision. - `workload`: the workload profile of the job. Absent when the profile names no stored workload. - `requested_model`: the name that you sent as `model`. A job with no award has no `addressing`, so this is its only model name. - `created_at`, `settled_at`: when the exchange received the job, and when it settled. ## Outcome - `outcome`: how the job ended. See below. - `settlement_status`: the state of the money. See below. - `service`: `met`, `missed`, `failed`, `pending`, `unknown`, `not_started` or `single_supplier_refused`. - `finish_reason`: `stop`, `length` (the answer reached the output limit) or `tool_calls`. Absent when no output came. - `shortfall`: `needed_usd` and `balance_usd`. Present only when your balance excluded every offer that could take the job. - `output_limit`: `asked_tokens` and `applied_tokens`. Present when the exchange lowered your output limit so that an offer could take the job. - `reasoning_effort`: `asked` and `sent`. They differ when the offer accepts other levels and the exchange sent the nearest one `sent` is absent when the offer takes no level. `outcome` and what each value charges: | Value | Meaning | Charge | | ----------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------- | | `completed` | The provider finished the job. | The tokens used. | | `no_eligible_offer` | No offer passed the gate. See `eligibility`. | None. The hold is released. | | `failed_before_output` | The job failed before any output. | None. The hold is released. | | `cancelled_after_output` | The job stopped after output began. | The tokens used. | | `provider_error_after_output` | The provider failed after output began. You keep the output. | A full credit. The provider earns nothing. | | `partial_output` | A stream stopped after output began, not by the provider's fault. | The input and relayed tokens, as a lower bound. | | `deadline_missed` | Old receipts only. A late job is now `completed` with `service` `missed`. | A full credit. | `settlement_status`: - `reserved`: the exchange holds the price cap. The job is not settled yet. - `settled`: the job was charged. - `credited`: the job was charged and then credited back. - `pending`: usage is missing or in conflict. You get a credit after the deadline. The exchange does not bill you without a warning. - `released`: the hold went back to your balance. You paid nothing. A job that missed its deadline is charged like any other completed job. `credits_usd` is zero. ## Charge - `cap_usd`: the most the job could cost, locked before the first token. Also the `x-liquid-cap-usd` header. - `estimated_charge_usd`: the cold estimate that ranked the offer, before any cache credit. - `cache_credit`: `hit_rate`, `source` (`buyer` or `measured`) and `credit_usd`. Ranking uses it. The cap, the hold and the charge do not. - `supplier_charge_usd`: what the provider earns, before the exchange fee. - `fee_usd`: the exchange fee, after any referral discount. See [Fees](https://inference.ai.exchange/docs/fees.md). `fee_discount_usd` is the part that a referral discount took off. - `credits_usd`: credit that the exchange gave back for the job. - `buyer_charge_usd`: what you paid. It never exceeds `cap_usd`. - `charge_split`: what the input, cache reads, cache writes and output each cost, fee included, as `tokens` and `usd`. Absent for a job not priced in tokens. When the cap limited the job, the parts add up to `buyer_charge_usd` plus `cap_held_usd`. - `cap_held_usd`: what the cap took off the metered charge, fee included. Present only when the job used more than its cap. The provider carries it. - `uncached_charge_usd`, `cache_charge_usd`: the charge if all input had the uncached rate, and the part that cache reads and writes cost. Present only when some input used a cache. For information only. - `rates`: the rates that priced the job: `uncached_input`, `cache_read`, `cache_write_short`, `cache_write_long` and `generation`. They stay as at award time if the provider changes them later. - `web_search`: the search that you asked for with `:web`. Its `charge_usd` is part of `fee_usd` and `buyer_charge_usd`. ## Usage - `meters`: what the provider reported, in its own tokenizer. See below. - `reasoning_tokens`: hidden reasoning tokens, billed as output. Present only when the provider reports them. - `tokenizer_id`, `rate_schema_version`: the tokenizer that counted the tokens, and the rate table that priced the job. - `usage_provenance`: the upstream usage report, unchanged. On a failed job it holds an `error`. Null for some media jobs. - `answer_checks`: what the exchange checked in the answer. See below. Absent means not checked, and you must not count it as a pass. `meters` fields. A media meter is absent when it is zero. - `uncached_input`: input tokens that no cache served or kept. - `cache_read`: input tokens that a cache served. - `cache_write_short`, `cache_write_long`: input tokens written to the cache, by lifetime. - `generation`: output tokens, reasoning included. `audio_generation` is the audio part. - `images`, `characters`, `video_seconds`, `audio_seconds`: the quantity of an image, speech, video or transcription job. Not tokens. The `exchange.usage` block gives the same numbers in a simpler form. `prompt_tokens` is the sum of all input meters. `exchange.usage.pricing` lists the same use by billable. Each line has `billable`, `unit`, `quantity` and `usd`. The `usd` is the provider charge for that quantity before the exchange fee, at the rate that priced the job. A receipt with no stored rates has no table. The flat counters stay. `answer_checks` changes no charge, except that a chat answer with a bad tool call is rejected before you receive it. - `structured_output.result`: the check against your `response_format`: `passed`, `not_json`, `schema_mismatch`, `cut`, `refused` or `unchecked`. `keyword` names the broken JSON Schema keyword for `schema_mismatch`. See [Structured output](https://inference.ai.exchange/docs/errors.md#structured-output). - `tool_calls`, `tool_call_errors`: the tool calls in the answer, and how many were bad. See [Tool calls](https://inference.ai.exchange/docs/errors.md#tool-calls). - `tool_call_faults`: the bad calls by kind: `invalid_json`, `unknown_name`, `schema_mismatch`. - `tool_markup`: 1 when the text has tool-call markup but the answer has no tool call. ## Timing - `ttft_ms`: time to first token. It starts when the request arrives, so it includes the auction. - `completion_ms`: time from the first byte to the last. - `promised_ttft_ms`: the first-token time that the offer promised. The receipt keeps it. - `ttft_limit_ms`: your limit. Absent for an image job and for old receipts. - `ttft_deadline_ms`: `ttft_limit_ms` plus 1 second for each 10,000 input tokens. The exchange judges the job against this, not against the promise. A job with a `ttft_ms` above `promised_ttft_ms` can still be `met`. ## Provider and offer - `provider_id`: the provider, stable when it changes its slug. `provider_slug`: its slug at the time. - `offer_id`: the offer that served the job. Use it to trace a charge. - `provider_attribution`: what the provider said about its data handling at award time. Each claim has a `basis` (`declared` or `checked`), `checked_at` and `valid_until`. The receipt freezes it. ## Addressing `addressing` says how your request became a served model. It is absent on a job with no award. - `requested`, `target`: the name that you sent, and what it is: a `model`, a `router` or a `preset`. - `match_type`: `exact`, `approved_set`, `router` or `auto`. - `router`, `router_version`: the router model that chose the models, and the Auto mode version. - `served_model`, `served_precision`: the model that answered, and its precision, such as `bf16` or `fp4`. `provider-managed` means the provider does not state it. - `auto`: why Auto chose the model. See [Auto Router](https://inference.ai.exchange/docs/auto.md). `auto_fallback`: why a call to a router model ranked by price and not by Auto. - `synthetic` (a local substitute answered), `unverified` (no operator reviewed the deployment) and `probe` (the exchange sent the job to measure a deployment): do not use such a job as evidence about a model. ## Competition - `clearing_rule`: how the price was set. `first_price` charges the locked rates of the winner, up to its cap. - `offers_considered`, `offers_eligible`: the offers that the gate looked at, and those that passed. - `suppliers_eligible`, `single_supplier`: the independent providers behind the eligible offers, and whether only one was eligible. - `eligibility.excluded`: each offer that the gate refused, with `provider`, `offer_id`, `reason` and, when it helps, `detail`. `exchange.routing.eligibility` gives the same as counts for each provider and reason. `reason` values: - `job_kind_not_served`, `model_not_served`: the offer does not make this kind of output, or this model. - `feature_unsupported`, `media_unsupported`: the deployment cannot do a feature or accept an attachment. `detail` names it. - `context_too_large`, `output_bound_unsupported`: the prompt, your output limit, or a media size is outside what the offer allows. - `recently_failed`: the model failed a job recently, or health samples show it down. - `service_too_slow`, `throughput_too_low`: the offer's promised deadline is too slow for your limit. - `cap_exceeds_buyer_cap`, `cap_exceeds_budget`: the worst-case charge is above your `cap_usd`, or your balance. - `rate_above_ceiling`, `above_cost_tier`: the published rate is above your maximum rate or cost tier. - `region_policy`, `retention_policy`, `provider_requirements`: the provider does not meet your region, data-retention or provider-claim rules. - `model_not_allowed`, `provider_not_allowed`, `vendor_not_allowed`, `provider_blocked`: your model list, provider list, creator list or block list excludes the offer. - `offer_expired`, `pinned_offer_not_resting`: the offer is no longer on the book. - `workload_profile_not_targeted`: the offer is outside the workload profile that you named. - `superseded_by_own_offer`: the same provider had a lower offer that also qualified. - `score_unmapped`, `score_missing`: Auto has no approved score for the deployment, or for the task. ## Exchange block and requested purchase `exchange` is the block of the live response. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#response-fields). `requested_purchase` is what the request asked for before the preset of its key limited it: `target`, `models`, `strategy`, `rules` and `sources`. Absent on old jobs. ## Check a receipt 1. Add `supplier_charge_usd` and `fee_usd`, subtract `credits_usd`, and compare with `buyer_charge_usd`. It must not exceed `cap_usd`. 2. For a job priced in tokens, multiply each `meters` value by the matching rate in `rates`. The sum is `supplier_charge_usd`, unless the cap limited the job. 3. Compare `fee_usd` with [Fees](https://inference.ai.exchange/docs/fees.md). 4. Compare `ttft_ms` with `ttft_deadline_ms`, not with `promised_ttft_ms`. # Fees The exchange adds a fee to the charge of each job. The provider earns its own price. The fee pays for the auction, the hold, the settlement and the receipt. Each [receipt](https://inference.ai.exchange/docs/receipts.md) shows the fee in `fee_usd`. ## What you pay ``` buyer_charge_usd = supplier_charge_usd + fee_usd - credits_usd ``` - `supplier_charge_usd` is what the provider earns, from the offer rates and the metered usage. - `fee_usd` is what the exchange keeps. - `credits_usd` is credit that the exchange gives back. It is usually zero. A job that fails before any output pays no fee. The exchange releases the hold. ## The fee rule For a job priced in tokens, the fee is the larger of two amounts: 1. **A percentage.** 5% of the supplier charge. 2. **A floor.** A least fee for each metered token, by kind of token. | Kind of token | Highest floor, USD per million tokens | | ------------- | ------------------------------------- | | Input | 0.01 | | Cache read | 0.001 | | Cache write | 0.01 | | Output | 0.01 | An offer that charges nothing for a kind, such as a free offer, pays less when the model has a reference price. The reference is the list price of the model creator, or the mean rate of the other providers of the model if there is no list price. $$ \text{floor}_k = \begin{cases} \text{highest}_k & \text{rate}_k > 0 \\ \min\left(\text{highest}_k,\; 0.05 \times \text{reference}_k\right) & \text{rate}_k = 0 \text{ and a reference exists} \\ \text{highest}_k & \text{rate}_k = 0 \text{ and no reference exists} \end{cases} $$ $$ \text{fee} = \max\left(0.05 \times \text{supplier charge},\;\; \sum_k \text{tokens}_k \times \text{floor}_k\right) $$ Here `k` is the kind of token. For example, a free offer of a model with a reference of 0.02 USD per million input tokens pays an input floor of 0.001 USD, not 0.01 USD. The exchange can change these values. ## Which rule applies The percentage wins above about 20 cents per million tokens. The floor wins on cheap models and free offers. | Offer | Fee | What decides | | ------------------------------ | -------------------------------------------- | -------------- | | A model at $5 per Mtok | 5% of the charge | The percentage | | A model at $0.02 per Mtok | 1 cent per Mtok | The floor | | A free offer, or `liquid.free` | 5% of the reference, at most 1 cent per Mtok | The floor | | An image, speech or video job | 5% of the charge | The percentage | On a very cheap model, the fee can be a large share of the charge, although the amount is tiny. A job of 76 input tokens at $0.02 per Mtok paid the provider $0.00000152 and a floor fee of $0.00000076, which is 50%. ## Free offers and `liquid.free` A job on a free offer is not free for the buyer. The supplier charge is zero, so the fee is the floor, at most 1 cent per million tokens. A job of 2,040 tokens on a free offer of a model with no reference pays $0.0000204. `liquid.free` buys only offers that charge nothing. It pays the floor, so its price cap and hold are above zero. Receipts from before 2026-10-22 show a fee of zero on free offers. ## Other cases - **BYOK.** The supplier charge is zero, because the cloud bills you. The fee is the percentage of the charge at the list price of the model. A model with no list price pays the floor. See [Bring your own key (BYOK)](https://inference.ai.exchange/docs/byok.md). - **No token count.** A job that the provider prices by image, character or second pays 5% of the supplier charge and no floor. - **Cache.** A cache read has a lower floor, so a job with many cache reads pays less floor. - **Web search.** A job with `:web` pays the cost of the search service and a fixed exchange fee. Both are in `fee_usd`, and a referral discount does not reduce them. A job that settles with no charge pays nothing for the search. - **Reservations.** A job on a reservation has no charge when it runs. The exchange fee is added to the monthly fee. See [Reservations](https://inference.ai.exchange/docs/reservations.md). ## Cap and hold The price cap of a job includes the fee. Before the first token, the exchange computes the highest fee that the job can pay, with the highest input floor. It holds the cap from your balance and returns the unused part at settlement. Settlement never exceeds the cap. If usage passes the cap, the provider carries the excess. The receipt shows the amount in `cap_held_usd`. ## Discounts A person who joined with a referral link pays 10% less fee for a set period. The provider earns the same. `fee_usd` is the fee after the discount, and `fee_discount_usd` is the amount taken off. See [Referrals](https://inference.ai.exchange/docs/referrals.md). ## Read the fee on a receipt | Field | Description | | --------------------- | ------------------------------------------------------------------------------------------------- | | `supplier_charge_usd` | What the provider earns. | | `fee_usd` | The exchange fee, after any discount. It includes the web search charge. | | `fee_discount_usd` | The amount that a referral discount took off the fee. | | `charge_split` | The cost of input, cache reads, cache writes and output. Each part includes its share of the fee. | | `rates` | The provider rates, before the fee. | To check a fee: 1. Multiply `supplier_charge_usd` by 0.05. 2. Multiply each token count in `meters` by the floor of its kind, and add them. 3. Take the larger result and subtract `fee_discount_usd`. The result is `fee_usd`, unless the job used a web search. The rates in the book are before the fee. [Market data](https://inference.ai.exchange/docs/market.md) shows the buyer rate with the fee added. # Workloads in the API A [workload](https://inference.ai.exchange/docs/workloads.md) is a repeat task that you save in the console. Each version of it has a name, its **profile**. You send the profile with a request, and the exchange labels the job with it. This page tells what the profile does in the API and what it does not do. ## What the profile is A profile has the form `.@v`, such as `acme.invoice-extract@v2`. The workspace part is the slug of your workspace. The workload part is the slug of the workload. `n` is the version. A save that changes the task (the prompt, the model, the answer format or the settings) makes a new version, and so a new profile. A change to the cases makes no version. The workload page and the **Save as workload** dialog show the profile. The API has no route to create or change a workload. Save it in the console. The routes behind the console are not public. ## Send a profile The **Production** tab of a workload exports the code of the newest version in Python, TypeScript and curl. See [Watch the calls in production](https://inference.ai.exchange/docs/workloads.md#watch-the-calls-in-production). To write the call yourself, set `exchange.rank.workload_profile` in the `exchange` block of the request: ```bash curl https://router.inference.ai.exchange/v1/chat/completions \ -H "Authorization: Bearer $LIQUID_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-oss-120b", "messages": [{"role": "user", "content": "Extract the invoice: ACME, due 2026-11-01, 1200 USD"}], "response_format": {"type": "json_object"}, "exchange": {"rank": {"workload_profile": "acme.invoice-extract@v2"}} }' ``` A client that cannot change the body sends the header `x-liquid-profile: acme.invoice-extract@v2`. A header applies only if the body and the preset do not set the rule. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). If a request names no profile, the job takes the default profile of its API key. ## What the profile does not do The exchange does not read the saved prompt, model or answer format from the profile. Your request must send its own `messages`, `model` and `response_format`. To make the same request as the console, copy them from the workload page. A request does not need a profile that exists. The exchange checks only that the part before the first dot is not the slug of another workspace. ## What the profile changes - **Label.** The exchange stores the profile on the job. - **Task match.** When the profile names a workload of your workspace, the exchange compares the call with the version and keeps a mark on the job and the receipt. See [Task match](#task-match). - **Offers that target a profile.** A provider can list `workload_profile_targets` on an offer. - An offer with targets competes only for jobs whose profile is in the list. - An offer with no targets competes for every job. - A provider can use this to hold a price for one buyer. - A dry run names the offers that a profile leaves out with `workload_profile_not_targeted`. The profile does not change how the exchange ranks offers, and the task match changes no award and no charge. To rank by the chance of a good answer, send `exchange.rank.failure_risk_ranking`. See [Fewest redos](https://inference.ai.exchange/docs/rules.md#fewest-redos). ## Task match When the profile names a version of a workload of your workspace, the exchange compares three parts of the call with the version: the system messages, the answer format and the settings that change the answer. The settings are the temperature, the output limit and the reasoning effort. The exchange compares digests, not the text. It does not compare the user messages or the model. The receipt gives the result as `buyer.workload`: ```json "workload": { "profile": "acme.invoice-extract@v2", "task": "drift_format" } ``` | `task` | Meaning | | ---------------- | -------------------------------------------------------------------- | | `matches` | The three parts are those of the version. | | `drift_system` | The system messages differ. | | `drift_format` | The answer format differs. | | `drift_settings` | The temperature, the output limit or the reasoning effort differs. | | `no_version` | The workload exists, but the exchange has no digest of this version. | When more than one part differs, the mark names the first: the system messages, then the format, then the settings. A text answer and no answer format are the same format. A version that you saved less than 10 seconds ago can read `no_version`. A call that drifts is served and charged as any other. The exchange marks it and never refuses it. A profile that names no workload of your workspace gets no mark, and the receipt has no `workload`. ## Run many takes The console runs a workload by sending many requests, one lane at a time. To do the same, send each take with: - The lane that you test. For one provider, send the model and set `exchange.candidates.providers.allow` to the provider. For a preset, a model or Auto, send its name as `model`. Auto also needs a price bound, such as `exchange.cap_usd`. - `exchange.cache.response` set to `false`, so each take buys a new answer. - The same `exchange.rank.workload_profile` on each take. Then read `exchange.answer_checks` on each answer. See [Structured output](https://inference.ai.exchange/docs/structured-output.md). ## Refusals | Status | Code | Meaning | | ------ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `invalid_request` | The profile starts with the slug of another workspace. `param` is `workload_profile`. Use a profile that starts with the slug of your own workspace. | | 403 | `key_policy_violation` | The key has a preset with a different `exchange.rank.workload_profile`. Send the profile of the preset, or use another key. | A [guardrail](https://inference.ai.exchange/docs/guardrails.md) cannot hold an `exchange.rank.workload_profile`. ## Next steps - [Workloads](https://inference.ai.exchange/docs/workloads.md): save, run and read a workload in the console. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): the `exchange` block and the headers. - [Structured output](https://inference.ai.exchange/docs/structured-output.md): the check of each answer. # Price and wait By default, the exchange does not pick the offer with the lowest charge. It picks the offer with the lowest **cost**: the estimated charge plus the **wait cost**. The wait cost is the money value that you give to the time that you wait for the answer. This applies to the `price` strategy, which is the default. The `cheapest` axis uses it too. ## How the cost is made The exchange adds two amounts for each offer that meets your limits: ```text cost = estimated charge + (predicted wait in ms × wait value) ``` - The **estimated charge** is the price that the exchange expects for your request. - The **predicted wait** is the time from the start of the job to the end of the answer. It is based on measurements of the provider. See [How the exchange measures performance](https://inference.ai.exchange/docs/performance-measurement.md). - The **wait value** is the price of one millisecond of wait, in nano-USD (one billionth of a dollar). The default is 200. That is $0.0002 for each second, or $0.72 for each hour. The offer with the lowest cost wins. The exchange then runs the normal steps: the price cap, the hold, and the charge for the tokens that the provider really used. ## Example Two offers answer the same request. The wait value is the default, 200 nano-USD for each millisecond. | Offer | Estimated charge | Predicted wait | Wait cost | Cost | | ----- | ---------------- | -------------- | --------- | ------- | | A | $0.0100 | 4.0 s | $0.0008 | $0.0108 | | B | $0.0105 | 0.5 s | $0.0001 | $0.0106 | Offer B wins, although its charge is higher. You pay the real charge of offer B, which is never more than its price cap. If you set the wait value to zero, offer A wins. ## Change the wait value Set `exchange.rank.latency_nanos_per_ms` in a request or in a [preset](https://inference.ai.exchange/docs/rules.md). Use a whole number that is zero or more. ```json { "exchange": { "rank": { "latency_nanos_per_ms": 0 } } } ``` | Value | Result | | ---------------- | --------------------------------------------------------------------------------- | | Not set | The default of the exchange, 200. | | `0` | The exchange ranks on the estimated charge alone. An explicit `0` beats a preset. | | A larger number | A faster offer wins more often, even when its charge is higher. | | Above 1000000000 | The exchange lowers the value to 1000000000. | The exchange refuses a value that is not a whole number or that is below zero. ## What the wait value changes The wait value changes only the **order** of the offers. These rules do not change: - An offer must still pass every limit, including your price cap. - The price cap, the hold and the settlement work as before. - The exchange never shortens your answer or sets your output limit. - The `price_band_bps` setting applies to the cost, which includes the wait cost. ## When the exchange ranks on price alone The exchange ranks on the estimated charge alone in these cases: - The strategy is `fastest`, `smartest` or `auto`. These strategies have their own rules and ignore the wait value. - The request is not a chat request. - The wait value is zero. - One eligible offer has no complete measurement. The exchange then ranks the whole auction on price, so that an offer with no data does not get an advantage. To see why an offer won, read [Explain the choice](https://inference.ai.exchange/docs/rules.md). ## Next steps - [How the exchange measures performance](https://inference.ai.exchange/docs/performance-measurement.md) - [Purchasing rules](https://inference.ai.exchange/docs/rules.md) - [Axis routing](https://inference.ai.exchange/docs/axis-routing.md) # How the exchange measures performance The exchange measures the speed of each provider itself. It does not use the speed that a provider claims. The measurements feed the predicted wait in [Price and wait](https://inference.ai.exchange/docs/price-and-wait.md) and the speed ranking of `fastest` and [Auto Router](https://inference.ai.exchange/docs/auto.md). ## What the exchange measures The exchange measures three numbers for each provider and each model. | Measure | What it tells you | Jobs that count | Value of one job | | ----------------- | -------------------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------- | | **Base latency** | The fixed time before the first token, in milliseconds. | Jobs with fewer than 1,000 fresh input tokens. | Time from dispatch to the first token. | | **Prefill speed** | How fast the provider reads a long prompt, in tokens a second. | Jobs with 16,000 or more fresh input tokens. | Fresh input tokens ÷ time from dispatch to the first token. | | **Output speed** | How fast the provider writes the answer, in tokens a second. | Jobs whose answer came as a stream. | Output tokens ÷ time from the first token to the end. | **Fresh input tokens** are the input tokens that the provider had to process. They are the uncached tokens plus the tokens written to the cache. Tokens read from the cache are not fresh. A job's answer came as a stream when its output reached the router at four or more separate times, each at least 10 ms after the one before. An answer that arrives in one piece has no output speed, because the time from its first token to its end measures the network, not the model. On a receipt from before the exchange counted these arrivals, the answer must take at least 250 ms instead. Prefill speed counts all the wait before the first token. This includes the base latency, so the number is a little lower than the true reading speed of the provider. ## Where the numbers come from - The exchange takes the numbers from the receipts of settled jobs. Its own router records the times. A provider cannot change them. - A time starts when the router sends the job to the provider. It ends when the first token arrives at the router. It includes the network and the queue of the provider. - A job that failed and went to the next offer counts only for the offer that answered. The time of the failed attempt does not count. - Probe jobs of the exchange count with real jobs. Synthetic test receipts do not count. ## How the numbers are combined 1. **Window.** The exchange uses the last seven complete UTC days. The newest day joins one hour after midnight, so that late receipts can arrive. 2. **Median.** The exchange takes the median of all jobs, not the average. One slow job does not move the number. Jobs are grouped in steps of 1%, so a value is correct to about 1%. 3. **Few jobs.** A provider with few jobs gets a value that is closer to the median of its model. - The weight of the provider's own value is `n ÷ (n + 10)`, where `n` is the number of jobs. - A provider with 10 jobs counts 50% of its own value. A provider with 90 jobs counts 90%. 4. **No jobs.** A provider with no jobs uses the median of its model. If the model has no data, the exchange uses the median of all models. 5. **Refresh.** The router reads the numbers once for each UTC day and keeps them in memory. If a read fails, the router keeps the last numbers. ## How the wait is predicted For each offer, the exchange predicts the wait for your job: ```text predicted wait = base latency + fresh input tokens ÷ prefill speed + expected output tokens ÷ output speed ``` - Fresh input tokens depend on your prompt size and on the cache. The exchange subtracts the tokens that it expects to read from the cache. - The exchange estimates the output length of your job. - If one of the three numbers is missing and the job needs it, the offer has no complete prediction. ## Where the measurements are used | Use | Measure that it reads | Rule | | -------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | [Price and wait](https://inference.ai.exchange/docs/price-and-wait.md) | Base latency, prefill and output speed | Adds the wait cost to the charge. | | `fastest` and [Auto Router](https://inference.ai.exchange/docs/auto.md) | Output speed | Ranks a provider by speed only after 5 jobs of its own. A value that came from other providers never counts. | ## Limits - The numbers describe the last week. They do not show the load of a provider now. - A new provider has no history. The exchange uses the model median until it has jobs. - The exchange cannot tell network time from provider time. Both are in the numbers. - Without a complete measurement, the exchange ranks on price alone for that auction. ## For providers The `measured_p95_ttft_ms` field of your quote is not used in new decisions. To rank higher, answer sooner and write faster. See [Provider quoting](https://inference.ai.exchange/docs/provider-quoting.md). ## Next steps - [Price and wait](https://inference.ai.exchange/docs/price-and-wait.md) - [Axis routing](https://inference.ai.exchange/docs/axis-routing.md) - [Auto Router](https://inference.ai.exchange/docs/auto.md) # Axis routing Add a word to the model name to choose how the exchange ranks the offers. The word is `smartest`, `fastest`, `cheapest`, `premium`, `balanced` or `economy`. ```text deepseek-v4.1-flash:fastest deepseek:cheapest liquid.auto:smartest ``` The name before the colon is the **scope**: the models that compete. The word after it is the **axis**: the ranking. An axis changes only the order. Every offer must still pass your cap, limits and key. | Axis | The offer that wins | | ---------- | ------------------------------------------------------------------------------------ | | `smartest` | The highest published score, whatever the charge. | | `fastest` | The highest median output speed that the exchange measured. The charge breaks a tie. | | `cheapest` | The lowest estimated charge plus the cost of the predicted wait. | | `premium` | [Auto](https://inference.ai.exchange/docs/auto.md): a doubling of the charge must buy 0.5 points. | | `balanced` | Auto: a doubling of the charge must buy 2 points. | | `economy` | Auto: a doubling of the charge must buy 5 points. | ## Send a request ```python from openai import OpenAI client = OpenAI(base_url="https://router.inference.ai.exchange/v1", api_key="...") response = client.chat.completions.create( model="liquid.auto:smartest", messages=[{"role": "user", "content": "Hello"}], extra_body={"exchange": {"cap_usd": 0.5}}, ) ``` `smartest` and the Auto words pay more for a better score, so they need a price bound, such as the `cap_usd` above. With the CLI, write `liquid claude --model liquid/auto:fastest`. ## Choose the scope | Name before the colon | Models that compete | | ------------------------------------------------------------------ | ------------------------------------------------------------------------- | | `architect.basket`, a router model such as `liquid.auto`, a preset | The models that the name has without the axis. The axis sets the ranking. | | A served model name, such as `deepseek-v4.1-flash` | That model, across all of its offers. | | A creator name, such as `deepseek` | Each model of that creator. | `smartest` and the Auto words need more than one model, because one model has one score. ## Web search **Beta.** `:web` can change without notice, including its price and search services. `web` is not an axis. It turns on a web search for the request and combines with any axis. Join the words with `+`, in any order: ```text deepseek-v4.1-flash:web liquid.auto:smartest+web ``` Only an exchange with a search service key has web search. `GET /v1/exchange/features` says whether it does. For a request with `web`, the exchange: - Searches once, for the last user message, before it ranks the offers. It tries TinyFish first, then Exa once. - Puts the results in a system message before the last user message, marked as untrusted text. The earlier messages do not change, so a prompt cache still reads them. - Holds the price of the search with the tokens. - Adds the price of the search to `fee_usd` and `buyer_charge_usd` when the job settles with a charge. A referral discount does not reduce it. - Reuses the results, free, when the same workspace repeats the query within five minutes. A tool loop, such as Claude Code or Codex, pays for one search. | Status | Code | When | | ------ | ------------------------ | -------------------------------------------------------- | | 400 | `web_search_needs_text` | The request has no user message with text to search for. | | 400 | `web_search_exceeds_cap` | The cap is not more than the price of the search. | | 402 | `insufficient_balance` | The balance cannot pay for the search and an answer. | | 503 | `web_search_unavailable` | No search service answered. Nothing is charged. | ## Reference ### Same request, two forms ```json { "model": "deepseek-v4.1-flash:fastest" } ``` ```json { "model": "deepseek-v4.1-flash", "exchange": { "rank": { "strategy": { "kind": "fastest" } } } } ``` | Axis | `exchange.rank.strategy` | | ---------- | ----------------------------------------- | | `smartest` | `{"kind":"smartest"}` | | `fastest` | `{"kind":"fastest"}` | | `cheapest` | `{"kind":"price"}` | | `premium` | `{"kind":"auto","preference":"premium"}` | | `balanced` | `{"kind":"auto","preference":"balanced"}` | | `economy` | `{"kind":"auto","preference":"economy"}` | The word counts as an axis only when the text after the last colon is exactly one of the six words, with a name before it. Otherwise the colon belongs to the model name: `llama3:8b` is one model name. ### How each axis ranks - **`cheapest`** is the default ranking of a model and a basket. A wait value of zero ranks on charge alone. See [Price and wait](https://inference.ai.exchange/docs/price-and-wait.md). - **`fastest`** uses the median output speed over the last 7 days, counted after 5 timed jobs. A difference under 1 token per second is a tie, and the charge decides. An offer with no counted speed ranks last. See [How the exchange measures performance](https://inference.ai.exchange/docs/performance-measurement.md). - **`premium`, `balanced`, `economy`** and **`smartest`** rank by [Auto Router](https://inference.ai.exchange/docs/auto.md). `smartest` is mode version 4 and uses the general score when no offer has a coding score. ### Refusals | Status and code | Cause | | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400 smartest_needs_several_models` | `smartest` with one model. Use a creator, a router model, a basket or a preset. | | `400 strategy_conflicts_with_address` | `premium`, `balanced` or `economy` with one model. | | `400 auto_requires_price_bound` | `smartest` or an Auto word with no cap, no cost tier other than `any` and no output-rate ceiling. A router model with a cap of its own does not need one. | | `400 axis_conflict` | Another ranking in the same request: another `exchange.rank.strategy` kind, another Auto version, the `x-liquid-policy` header, or `exchange.candidates.creators.allow` with a creator name. | | `400 ambiguous_scope` | A name that is both a served model and a creator. Send `architect.basket` with a pattern in `exchange.candidates.models.allow`, such as `deepseek-*`. | See [Errors](https://inference.ai.exchange/docs/errors.md) for every code. # Market data See what providers offer before you send a request. The public endpoints need no key and do not name providers. The live feed names providers and needs an API key. The **book** is the set of standing offers. An **offer** is one provider's price for one model, with the deadlines that the provider promises. A **market** is all the offers for one model. ## Public endpoints | Path | Description | | ----------------------- | --------------------------------------------------------------------------------- | | `/v1/models` | Models and router models with quotes now. With a key, it adds your presets. | | `/v1/exchange/offers` | Each resting offer without the provider name, lowest generation rate first. | | `/v1/exchange/models` | One row for each served model and kind: price range and depth. | | `/v1/exchange/markets` | Depth for each served model: independent suppliers, offer count, liquidity. | | `/v1/exchange/routers` | Each router model: ranking, default cap, rules and the models that it admits now. | | `/v1/exchange/lifetime` | Jobs settled and tokens processed since the exchange opened. | All are `GET`. ```bash curl https://router.inference.ai.exchange/v1/exchange/offers ``` `/v1/exchange/markets` takes `model` (names or patterns) and `query` (a word search). A job pays the rate of the offer that it won. A provider can change its rates at any time. [Offer](#offer) lists the fields. ## Live feed `GET /v1/exchange/stream` upgrades to a WebSocket and requires an API key of scope `list` or higher. Send the key in the first frame, within five seconds after you connect: ```json { "type": "subscribe", "credential": "sk-liquid-..." } ``` The exchange sends a `snapshot` of the book, then one frame for each change. [Frames](#frames) lists them. - Add `"receipts": true` to receive the receipts of your own workspace. - Add `"parts": true` to receive the book in `snapshot_part` frames of at most 50 offers. The last part is a `snapshot` frame. Without `parts`, one `snapshot` frame holds the whole book, which is more than 1 MB. A slow market subscriber misses frames, and the feed does not send them again. The feed disconnects a slow receipts subscriber. Reconnect for a new snapshot. The exchange pings every 30 seconds. It closes a socket that answers no ping before the next one, or that reads no frame for 10 seconds. ## Unverified deployments The exchange lists a new deployment only after it measures it. Until then, the deployment is unverified. The exchange never chooses it for a model name, router model, basket or preset, and no public view shows it. Only the workspace that owns the provider can buy from it, with the deployment symbol, such as `acme/gpt-oss-120b`, as the model. The receipt then has `"addressing": { "unverified": true, … }`. Any other workspace gets `model_not_found`. ## Reference ### Offer ```json { "market": "gpt-oss-120b", "model": "gpt-oss-120b", "creator": "OpenAI", "precision": "fp8", "region": "us", "input_length_band": { "min": 0, "max": 128000 }, "output_limit_band": { "min": 0, "max": 4096 }, "ttft_ms": 1500, "completion_ms": 30000, "pricing": [ { "billable": "input_text", "unit": "mtok", "usd": 0.12 }, { "billable": "output_text", "unit": "mtok", "usd": 0.48 } ], "synthetic": false, "valid_until": "2026-09-21T10:15:02.412Z" } ``` | Field | Description | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `market`, `model`, `creator` | The model, and the company that made it if the provider stated it. | | `input_length_band`, `output_limit_band` | The token limits that the deployment accepts. The exchange refuses a request outside all bands. A `null` output maximum means no output limit. Input and output must then fit in `input_length_band` together. | | `ttft_ms`, `completion_ms` | The provider's deadlines. If it misses one, the receipt shows `missed`. You still pay. | | `pricing` | Every price of the offer, one line for each billable: `billable`, `unit`, `usd` and an optional `variant`. For `mtok`, `usd` is USD per million tokens. Empty on an offer with no price. See [Prompt caching](https://inference.ai.exchange/docs/caching.md) for the cache lines. | | `kind` | `image_generation`, `audio_speech`, `video_generation` or `embedding`. Absent on a chat offer. | | `pricing_unit`, `image_output_tokens_max` | On an image offer priced in tokens: `pricing_unit` is `tokens`, and the cap holds `image_output_tokens_max` tokens for each image. | | `valid_until` | When the quote expires, unless the provider re-marks or withdraws it first. | ### Market ```json { "market": "gpt-oss-120b", "independent_providers": 3, "offer_rows": 5, "affiliation_groups": ["grp_acme"], "liquid": true } ``` Affiliated accounts count as one supplier, and so do resellers of the same upstream. A market is `liquid` with three or more independent suppliers. If a request sends `exchange.rank.allow_single_supplier: false`, the exchange refuses an award when only one supplier is eligible. ### Frames - `snapshot`: each resting offer, with provider identity. Sent once. With `parts`, `snapshot_part` frames come first. - `offer_updated`: a provider placed or re-marked an offer. - `offer_removed`: the offer left the book. An expiry has `reason: "expired"`. - `provider_connected`, `provider_disconnected`: a provider session opened or closed. - `quote_rejected`: a request found no eligible offer. It names the model, not the requester. - `receipt_opened`, `receipt_settled`: a job of your workspace opened or settled. Only with `receipts`. - `refused`: the exchange did not accept the credential. Sent once, before the close. # Reservations A reservation is a monthly pool on one model from one provider, at a fixed monthly fee. The pool of a chat model is tokens. The pool of a video, image or speech model is units: seconds of video, images or characters of speech. You reach it only through a preset. A plain model name still buys on the market. Every reservation requires zero data retention. ## How a reservation works 1. Send a request for quotes to up to 20 providers. State the model and the terms you need. 2. Read the quotes. You can discuss a quote, ask for changes, or reject it. A provider can answer a change request with an updated offer. 3. Accept a quote. The exchange checks that your cash covers 1 monthly fee. You pay nothing yet. 4. The provider links a deployment to your contract. Your contract serves from the first month that starts after the link. To be served sooner, see [Start before the term](#start-before-the-term). 5. On the 1st of each month of the term, you pay the monthly fee from your cash. If your cash is short, you lose that month. 6. Name the reservation in a preset. Requests with that preset go to the provider's deployment first, at no further cost, until you use up the month's pool. ## Terms You set these terms in the request. Each provider answers with what it offers. You see each term that differs from your request. | Term | What you ask for | | ------------------- | -------------------------------------------------------------------------------- | | Token pool | Chat models: input tokens and output tokens for each month | | Unit pool | Video, image and speech models: the units for each month and variant | | Rate limits | Tokens for each minute (units for a unit pool) and requests for each minute | | Output speed | Chat models: the minimum output tokens for each second | | Time to first token | Chat models: the maximum for 95% of requests in a month; the default is no limit | | Start month | The first month of the term | | Term | 1 to 12 months | | Uptime | The minimum uptime, as a percentage | | Regions | Serving regions, such as `us-east` or `eu-west`; empty accepts any | | SOC 2 | Not required, Type 1 or Type 2 | The time to first token runs from the hand-off to the provider to the first usable token, and includes thinking time. The exchange does not refuse a slower request. ## Prices and the monthly fee Each quote gives an input price and an output price, in dollars for each million tokens. Your monthly fee is the input price times the input pool plus the output price times the output pool. You pay it whether or not you use the tokens. ## Reserve a video, image or speech model Pick a video, image or speech model in the request. The form asks for units, not tokens. Embedding, decision and transcription models cannot be reserved. | Model | Unit | Variant you choose | | ------ | ---------------- | ------------------ | | Video | Seconds of video | Resolution | | Image | Images | Quality | | Speech | Characters | None | State a count for each variant that you plan to use, and the most units that you send in one minute. Each quote gives a rate for each variant, in dollars for one unit. Your monthly fee is the sum of each count times its rate. You pay it whether or not you use the units. The provider must charge exactly the rates of the contract, for each variant and no other. Commit each variant that you plan to use. Each job counts its own units against the limit for each minute: the seconds of video, the number of images or the characters of the text. At the limit, the overflow serves the request. A job of another kind than the reservation counts nothing against it. The exchange does not send probe requests to a video, image or speech deployment, because each one would buy real output. The uptime of the deployment comes from the jobs that buyers send. ## Ask for quotes 1. Open **Reservations** in the console and choose **New request**. 2. Pick the model, then the providers that sell reservations of that model. 3. Set the terms and choose **Send request**. Only a workspace owner or admin can send, accept, ask for changes, reject or close. Every member can read. You can accept several quotes on one request. **Close request** stops new quotes and lapses the quotes you did not accept. Accepted reservations stay in force. ## Compare and accept a quote Open the request to see **Reserved so far** (accepted quotes and their use this month) and **Open quotes**, sorted by price at your input and output mix. - Each column is a term on which the quotes differ. A tick means the quote matches your request. A ring means more than you asked for. An orange diamond means less. - **Best price** marks the lowest price among the quotes that match and cover the full pool. - To compare, tick up to 4 quotes and choose **Compare**. - Providers that declined, and quotes that lapsed, are under **Not available**. You can do 4 things with a quote: - **Reply** or **Messages**: write to the provider. Do not send contact details or personal data. The exchange can read messages and can suspend an account that misuses them. - **Ask for changes**: pick your reasons, such as uptime, location, throughput, start date, time to first token or price. An updated offer replaces the quote. - **×** (**Reject quote**): the provider cannot send a new offer on it. You cannot undo this step. - **Accept**: a dialog states the monthly fee, the total for the term and the month of the first fee. Choose **Accept quote** to confirm. You accept the current revision, and its start month must not have begun. If the provider sent an updated offer meanwhile, read it and accept again. To accept, you need withdrawable cash that covers 1 monthly fee with the exchange fee. Credit does not count. You pay nothing when you accept. You cannot cancel a contract after you accept it. ## Put the reservation in a preset Under **Reserved**, choose **Use in a preset** to make a preset with the reservation that ranks by price. Or add this entry to a preset of your own: ```text reservation: ``` A preset can hold 1 reservation. Any other models in the preset are your **overflow**, which you buy on the market when the reservation cannot serve. Overflow is optional. A preset with only the reservation sends nothing to the market. Rank the preset by price or by speed, not by Auto, because your reservation has no score. Send requests with the preset's model name or a bound API key. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). For each request: 1. The exchange tries the reservation's deployment before any market offer, even a cheaper one. 2. The charge of each job, at the contract prices, leaves your month's pool. 3. If the deployment fails before its first token, the overflow serves the job at market prices. After the first token, the exchange does not switch provider. When the pool is used up, the overflow serves the rest of the month. The contract also sets the tokens (or units) and requests a minute that you can send to the reservation. The exchange counts each request in a window of 60 seconds. A request counts its input and the output it asks for (`max_tokens`). At the limit, the overflow serves the request. A request counts even when the deployment fails and the overflow serves it. A dry run counts nothing. If the reservation is the only entry and cannot serve, the exchange refuses the request. See [Errors](https://inference.ai.exchange/docs/errors.md#reservations). ## Each month The contract card shows one of these statuses: | Status | Meaning | | ---------------------- | ------------------------------------------------------------------------------------------------ | | Waiting for start | The term has not started. | | Waiting for deployment | The provider has not linked a deployment, or linked it after the 1st. You are served next month. | | Fee due | You have not paid the fee yet. | | Active | You paid the fee. Your requests go to the reservation. | | Pool used | You used up the month's pool. Your requests go to the overflow. | | Suspended | Your cash did not cover the fee on the 1st. You are not served this month. | | Ended | The term is over. | The fee run is at 00:00 UTC on the 1st, and then once an hour. You pay from withdrawable cash only. If a hold is in flight, the run waits for the next hour. If your cash is still short, you lose the month, and a later top-up does not give it back. You pay no fee for a month that the contract does not serve. Your ledger shows each fee as a `RESERVATION_FEE` entry. The exchange can grant a goodwill credit if the provider fails to serve you. ## Start before the term You can ask the provider to serve the reservation for some days before the first month. The provider must accept, and then the exchange approves or refuses. 1. Under **Reserved**, pick a day under **Start early on** and choose **Ask to start early**. The day is after today, in the month before the term. The form shows the fee for the days to the end of that month, with the exchange fee. 2. Your withdrawable cash must cover that fee. Credit does not count. You can choose **Withdraw** until the provider answers. 3. The provider accepts or declines. It must link a deployment first, and cannot accept on or after the first day. 4. The exchange approves or refuses. On approval, it moves the fee from your cash to a hold. 5. From 00:00 UTC on the first day, the contract is **Active**. The pool for those days has the same share of the monthly pool as the fee has of the monthly fee. When it is used up, the overflow serves until the term starts. 6. On the 1st of the term, the exchange pays the held fee to the provider, then charges the first month as usual. If your cash does not cover the first month, you lose that month, but the provider keeps the payment for the early days. The early fee is the monthly fee times the days to the end of the month, divided by the days in that month, in whole UTC days. For example, at $3,000 a month, an early start on October 24 serves 8 of 31 days for $774.19, plus the exchange fee. The entry under **Reserved** shows the state: - **Early start asked**: the provider has not answered. You can withdraw. - **Waiting for the exchange**: the provider accepted. - **Early start approved**: the fee is held. You are served from the first day. - **Early start withdrawn**, **declined** or **refused**: you can ask again. - **Early start revoked**: you cannot ask again. The exchange can revoke an approval only before its first day, and states its reason. The hold then returns to your cash. The ledger shows `RESERVATION_HOLD`, `RESERVATION_RELEASE` and `RESERVATION_FEE`. For the provider's side, see [Serve a reservation](https://inference.ai.exchange/docs/provider-reservations.md). # Auto Router Send `liquid.auto` as the model and Auto chooses for you. It ranks the models that pass your rules. It compares a published score for each model with the estimated charge, within your cap. The auction then runs as usual, and the receipt records why Auto chose that model. Artificial Analysis publishes the score of each model. It does not measure the deployment that answers your request, so a higher score does not guarantee a better answer. ## Send a request Add a preference to the name: `liquid.auto:premium`, `liquid.auto:balanced` or `liquid.auto:economy`. Set a cap, because Auto pays more for a higher score and needs a price bound. ```bash curl https://router.inference.ai.exchange/v1/chat/completions \ -H "Authorization: Bearer $LIQUID_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "liquid.auto:balanced", "messages": [{"role": "user", "content": "Write a SQL query that finds duplicate emails."}], "exchange": {"cap_usd": 0.05} }' ``` The response says which model answered. The `x-liquid-auto-task` header says which score ranked the request. A bare `liquid.auto` ranks as the exchange sets it, by price or by Auto. See [A router model that ranks by Auto](#a-router-model-that-ranks-by-auto). ### Price bounds One bound is enough: - `exchange.rank.cost_tier` other than `any`: only models whose published rates are within the tier compete. - `exchange.limits.price.output_usd_per_mtok`: only offers at or below this output rate compete. - `cap_usd`, or the `x-liquid-cap-usd` header: offers with a higher worst case for this request do not compete. A cap also refuses an unusually large request. A bound in the preset that the request names, or in the preset linked to your API key, also counts. If no bound applies, `liquid.auto` uses its own `cap_usd`, shown in `GET /v1/exchange/routers`. The receipt shows the cap that applied. A basket that asks for Auto has no cap of its own, so the exchange refuses its request with `auto_requires_price_bound`. ## Choose a preference The preference sets how many score points each doubling of the estimated charge must buy. A model that costs twice as much wins only if its score is at least that many points higher. | Preference | Points for each doubling | | ---------- | ------------------------ | | `premium` | 0.5 | | `balanced` | 2 | | `economy` | 5 | To rank by score alone, send `liquid.auto:smartest`. It ignores the charge, so it needs a price bound. To rank by speed, send `liquid.auto:fastest`. See [Axis routing](https://inference.ai.exchange/docs/axis-routing.md). Without a suffix, set the preference in a strategy: ```json { "exchange": { "rank": { "strategy": { "kind": "auto", "preference": "economy" } } } } ``` A client that can send only headers, such as Claude Code, can use the `x-liquid-policy` header, or a preset whose strategy is Auto. See "Presets" in [Purchasing rules](https://inference.ai.exchange/docs/rules.md). If Auto is off, the exchange refuses to save such a preset with `400 auto_unavailable`. If a router model ranks by Auto and you send no preference, it uses `balanced`. ## Choose the models `liquid.auto` ranks every listed model, and the models of your own cloud accounts for your workspace only. To rank fewer, send `exchange.candidates.models.allow`: a model name, a deployment symbol or a pattern with `*`. See "The basket" in [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ```json { "model": "liquid.auto:balanced", "messages": [{ "role": "user", "content": "Summarise this contract clause." }], "exchange": { "candidates": { "models": { "allow": ["gpt-oss-*"] } }, "cap_usd": 0.05 } } ``` A preference suffix and `"rank": {"strategy": {"kind": "price"}}` in one request conflict, and the exchange refuses the request with `axis_conflict`. To rank your own basket, send `architect.basket` with `exchange.candidates.models.allow` and `"rank": {"strategy": {"kind": "auto"}}`. ## What Auto reads from your prompt Auto ranks by one of two Artificial Analysis scores: the intelligence index for a general request, or the coding index for a coding request. To choose, Auto can ask Jev, a hosted classifier. Jev reads the system prompt and your messages. It does not read assistant turns, tool calls or tool results. Auto calls Jev only if your data rules allow it: - `exchange.privacy.retention` is not `none`. If you do not set it, Auto sends the request to Jev. - No region restriction on the request, or on the preset linked to your key, excludes the processing region of the classifier. If the exchange has not named that region, any region restriction stops Auto from calling Jev. If the classifier or its vendor must not see your requests, set `exchange.privacy.retention` to `none`. Auto then ranks by the general score, and `none` also limits the offers to those that can obey it. If a conversation is too long for Jev, Auto cuts the middle of each long message and keeps its start and end. If Auto does not call Jev, or cannot use the answer, it ranks by the general score. The receipt names the reason. See [Classifier fallbacks](#classifier-fallbacks). ## Sessions and the cache Send the same `prompt_cache_key` on every turn of a conversation. Without a key, the exchange uses a digest of the first block of the prompt. The exchange records which deployment served the previous turn. If the cache that turn wrote has more value than another model, Auto keeps the next turn on that deployment, and the receipt records `kept_for_cache`. Under `exchange.cache.mode: "off"` the exchange drops the key and keeps no session. See [Prompt caching](https://inference.ai.exchange/docs/caching.md). ## Response Two headers arrive before the first token: - `x-liquid-auto-task`: the score that ranked the request, `general` or `coding`. - `x-liquid-auto-fallback`: present after a fallback. It holds a classifier fallback reason, such as `timeout`, or `no_eligible_coding_score`. The receipt records the decision in `exchange.addressing.auto`. See [Receipt fields](#receipt-fields). ## Errors - `503 auto_unavailable`: Auto is off, or the exchange has no score snapshot yet. - `503 auto_scores_stale`: the exchange's copy of the scores is older than its limit. - `503 no_eligible_offer`: no offer passed. `eligibility.excluded` lists why, and `error.auto` holds the decision. - `400 auto_version_unknown`: the version is not 1 to 8. - `400 auto_requires_price_bound`: the request, its preset and the key set no cap, no cost tier other than `any` and no output-rate ceiling. - `400 auto_preference_unsupported`: the version does not accept the preference. - `400 auto_policy_unsupported`: the version does not accept the preference sent as `exchange.policy` or `x-liquid-policy`. - `400 strategy_conflicts_with_address`: a model, or a router model with no cap, was sent with an `auto` strategy. - `400 auto_pin_unsupported`: the request sent `x-liquid-pin-offer`. - `400 auto_reasoning_unsupported`: Auto has no scores for the reasoning effort. Send another effort, a token budget, or none. Two exclusion reasons are specific to Auto: `score_unmapped` (no score for the offer's deployment at the request's reasoning setting) and `score_missing` (no score for the task). See [Errors](https://inference.ai.exchange/docs/errors.md). ## Reference ### A router model that ranks by Auto A router model, such as `liquid.auto`, ranks its offers by price or by Auto when a request names no ranking. `GET /v1/exchange/routers` shows this as `ranking`, and shows the `cap_usd` of a router model that offers Auto. When it ranks by Auto, a request with no strategy gets Auto at version 8 and `balanced`. Your choices always win: - `:premium`, `:balanced` and `:economy` rank by Auto at that preference, even where the router model ranks by price. - A strategy, `exchange.policy` or the `x-liquid-policy` header sets the ranking that you name. - `:smartest` ranks by score alone. `:cheapest`, `:fastest` and `{"kind": "price"}` take the call out of Auto. #### The work of the request If the router model chose the preference, the work of the request can move it one step. Two reads decide the effort: - **Jev.** It rates how much reasoning the latest turn needs: light, standard or heavy. The exchange asks in the same call as the task. It needs a retention policy that is not `none`. An answer with a probability of 0.6 or more decides. A light answer does not lower a request that has tool results. - **The envelope.** Without a confident answer, the exchange reads token counts, tools, tool results and the reasoning setting, and not the messages. Heavy work in the envelope stays heavy whatever Jev says. - `heavy`: a prompt of 30,000 tokens or more, 4 or more tool results, a reasoning effort of `high`, `xhigh` or `max`, or a budget of 8,000 or more. The preference moves one step toward `premium`. - `light`: a prompt of 800 tokens or less, no tools, no reasoning setting and 3 messages or fewer. It moves one step toward `economy`. - `standard`: anything else. It stays. The preference never moves past `premium` or `economy`. It does not move if you chose it or sent a suffix. The receipt records `effort`, `effort_source` (`classifier` or `envelope`) and, if the preference moved, `preference_before_effort`. #### When Auto cannot rank the call A bare call to a router model that ranks by Auto ranks by price, and does not fail, when Auto is off, the exchange has no current scores, the request sends `x-liquid-pin-offer`, or the reasoning setting has no scores. The receipt shows the reason in `exchange.addressing.auto_fallback`, such as `auto_unavailable` or `auto_scores_stale`. If you ask for Auto with a suffix or a strategy, the exchange refuses the request with the code in [Errors](#errors). #### Jobs that are not priced per token A router model that ranks by Auto buys an image, speech or video job at the lowest estimated charge. Scores are measured on text, so Auto does not rank these jobs, and the cost tier and first-token limit of the router model do not apply. A router model that ranks by price is refused on these endpoints. An embedding job is refused for both. See [Modalities](https://inference.ai.exchange/docs/modalities.md). ### Classifier fallbacks The receipt names the reason when Auto ranks by the general score because Jev did not decide: `disabled`, `not_permitted`, `input_too_large`, `unsupported_content`, `overloaded`, `timeout`, `unavailable`, `invalid_response`, `model_mismatch` or `low_confidence`. A Jev answer of `coding` below the confidence threshold is `low_confidence`. In version 2, a coding request ranks by the general score if offers pass your rules and none has a coding score. The receipt records `routing_fallback: "no_eligible_coding_score"`. Version 1 finds no offer and excludes each offer that has no score with `score_missing`. ### Versions A released version never changes, and a pinned version keeps its rules. Choose one in `exchange.rank.strategy.version`, such as `{"kind": "auto", "version": "2"}`. If you choose none, Auto uses version 8. The exchange refuses a version other than 1 to 8 with `auto_version_unknown`. - **1**: score alone. Accepts only `premium`. `balanced` or `economy` returns `auto_preference_unsupported`. - **2**: preferences of 0.5, 2 and 5 points. Default `balanced`. - **3**: available only when the exchange enables it. It has the preferences of version 2 and scores each exact model. An exact model with no score cannot receive an Auto award. A deployment with no score of its own uses the score of its exact model. `exchange.addressing.auto.score_origin` records the source. - **4**: score alone, and the general score when a coding request has no coding score. `:smartest` and `{"kind":"smartest"}` select it. Accepts only `premium`. - **5**: the preferences of version 2, and a [session pin](#session-pin). - **6**: version 5, and a few more points for a faster group. See [Speed](#speed). - **7**: version 6, with three changed rules. See [Price guard](#price-guard) and [Effort in version 7](#effort-in-version-7). - **8**: the rules of version 7, ranked by more task classes. See [Task classes](#task-classes). The exchange lists it only when an operator turns it on. Versions 2 and later default to `balanced`. Versions 1 and 4 default to `premium`. ### Price guard In versions before 7, a free offer made every other model look very dear. A charge below $0.000000001 counted as that amount, so a model at $0.008 was 23 doublings above a free model and lost 11 points at `premium`. Version 7 counts a charge below $0.00001 as $0.00001 and stops the price term at 30 points. A strong model that costs almost nothing now beats a weak free one. These numbers are a first setting that no pilot has measured. ### Effort in version 7 Versions 2 to 6 move the preference one step for heavy or light work. Version 7 keeps the preference and changes how much price counts. For heavy work it also sets a floor on the score. | Effort | `economy` | `balanced` | `premium` | | ---------- | --------------------- | --------------------- | ----------------------- | | `light` | 5 points per doubling | 4 points per doubling | 1 point per doubling | | `standard` | 5 points per doubling | 2 points per doubling | 0.5 points per doubling | | `heavy` | 2, and a floor of 8 | 0.5, and a floor of 4 | 0, and a floor of 0 | A floor removes every model that scores more than that many points below the best model that can win. The best model always stays. The numbers are a first setting that no pilot has measured. They apply only when the router model chose the preference. The envelope can raise the effort to `heavy`, and it cannot lower it to `light`, because the size of a prompt cannot show that a question is easy. Only a Jev answer of 0.6 or more gives `light`. If Jev is off or unsure, the effort is `standard`. The receipt shows `envelope_floor` as the source when this stopped a `light` reading. ### Task classes Version 8 ranks by the score that fits the request. If a class has a score for fewer than half of the models, or no offer that passed your rules has it, Auto ranks by the general score and the receipt says so. - `general`: Jev, or any fallback. Artificial Analysis intelligence index. - `coding`, `math`: Jev. Artificial Analysis coding and math indexes. - `reasoning`: Jev. The mean of GPQA and HLE. - `knowledge`, `instruction`: Jev. MMLU-Pro and IFBench. - `agentic`: a tool definition and at least one tool result. The mean of tau2-bench and Terminal-Bench Hard. - `long_context`: a prompt of 30,000 tokens or more. Artificial Analysis long-context reasoning. A coding request keeps `coding`. A tool loop is `agentic`, and a long prompt with no tool loop is `long_context`. A model needs every benchmark of its class to rank. A Jev label needs a probability of at least 0.5, because it has no held-out measurement yet. Writing, translation and vision have no class, because no published score covers them. ### Speed Version 6 reads the median output speed that the exchange measured for each provider and model over the last 7 days, when at least 5 timed jobs back it. A provider's own claim earns nothing. A group earns 0.5 points for each doubling of its speed over the slowest measured group, and never more than 1 point. A group with no measured speed earns nothing and loses nothing. So speed breaks a near tie and cannot outbid a gap of more than 1 point. These numbers are a first setting that no pilot has measured. ### Session pin A long conversation is cheap only while the same deployment holds its cache. Versions 5 and later keep a session on the deployment that served the previous turn. The session needs a stable key: a `prompt_cache_key`, an `x-session-id` header or the first block of the prompt. The deployment stays first while all of these are true: - It passes your rules, cap and budget, and its offer can win an award. - It sells cache reads at a rate below its input rate. - Jev reads the same task class as on the previous turn. - Its estimated charge is at most 10% above the same charge at the rates of the previous turn. - No other deployment has a value more than the margin above it. The margin is 2 score points, added to the value of the pinned deployment. When one condition is false, the session is released and the normal order decides. The receipt's `pin` field says what happened. Versions 5 and later also price the cache from your workspace. When a deployment has too little history, Auto uses the share of repeated input that providers served from cache for your workspace in the last 7 days. It assumes 50% only when the workspace has no history either. Two rules in the `exchange` block control the pin: - `exchange.session.pin`: `on` (the default of versions 5 to 8) or `off`. `off` ranks every turn again. - `exchange.session.margin`: score points, 0 to 50. A rival must have a value this far above the pinned deployment before the session moves. Versions before 5 ignore both rules. ### Discovery If Auto is available, two endpoints list it with the same descriptor: - `GET /v1/models` has a card for each router model that offers Auto, with `match_type: "router"` and an `auto` object. - `GET /v1/exchange/routers` has an entry with an `auto` object, its `ranking` and its `cap_usd`. `default_policy` and `policies` are the version's default preference and accepted preferences. `tasks` lists the task classes. `effort` is `preference_step` or `weight_and_floor`. `models` lists the models that unpinned Auto can choose now. If Auto is off, neither endpoint lists it. ```json { "name": "liquid.auto", "default_version": "8", "versions": [ { "version": "1", "default_policy": "premium", "policies": ["premium"] }, { "version": "7", "default_policy": "balanced", "policies": ["premium", "balanced", "economy"], "tasks": ["general", "coding"], "effort": "weight_and_floor" } ], "price_bounds": ["cap_usd", "cost_tier", "max_output_usd_per_mtok"], "models": ["gpt-oss-120b"] } ``` ### Receipt fields The `exchange.addressing.auto` object on the receipt records the decision: - `mode_version`, `policy`, `points_per_doubling`: the version, the preference and the points per doubling that ranked the request. - `effort`, `effort_source`, `preference_before_effort`: see [A router model that ranks by Auto](#a-router-model-that-ranks-by-auto). - `task`, `score_field`: the score that ranked the request (`general` or `coding`) and its Artificial Analysis field. - `classification`: `task`, and `outcome`: `{"kind": "classified"}` or `{"kind": "fallback", "reason": …}`. `evidence` holds Jev's answer, when it gave one. - `routing_fallback`: `no_eligible_coding_score`, when a coding request ranked by the general score. - `snapshot_id`, `scores_validated_at`: the score snapshot, and when the exchange last validated its copy. - `score`, `estimated_charge_usd`: the awarded model's score and the estimated charge that ranked it. The actual charge is `charge.total_usd`. - `higher_score`: a higher score that was passed over, its estimated charge, and `reason`: `value`, `competition` or `unavailable`. - `kept_for_cache`: `expected_cached_tokens`, `hit_rate` and `cold_charge_usd`, when the cache kept the award on the previous deployment. The estimated saving is `cold_charge_usd` minus `estimated_charge_usd`. - `pin`: present when the session pin applied. `outcome` is `held` or `released`; `deployment_id` and `margin` say which. `reason` says why the session left: `incumbent_unavailable`, `no_cache_discount`, `task_changed`, `price_rose`, `rival_cheaper` or `rival_above_margin`. A receipt written before these fields existed lacks the fields after `task`. If a request finds no offer, `error.auto` holds the same decision, without `score`, `estimated_charge_usd`, `higher_score` or `kept_for_cache`. # Modalities A modality is a kind of data: text, image, audio, document, video or vector. Every model reads text. Some also read images, audio or documents. The exchange can also buy images, speech, transcripts, video and embeddings. ## How it works Most inputs use the chat endpoints: put a typed content part in a message. Image, speech, video and embedding jobs use their own endpoints. | Modality | You do | Endpoint | Page | | ---------------- | ---------------------------------- | ------------------------------- | ----------------------------------------- | | Image in | Send an image to a chat model | `POST /v1/chat/completions` | [Image input](https://inference.ai.exchange/docs/image-input.md) | | Audio in | Send an audio clip to a chat model | `POST /v1/chat/completions` | [Audio input](https://inference.ai.exchange/docs/audio-input.md) | | Document in | Send a PDF to a chat model | `POST /v1/chat/completions` | [Document input](https://inference.ai.exchange/docs/document-input.md) | | Video in | Send a video to a chat model | `POST /v1/chat/completions` | [Video input](#video-in) | | Image out | Buy an image, or get one in a chat | `POST /v1/images/generations` | [Image generation](https://inference.ai.exchange/docs/image-generation.md) | | Speech out | Buy speech, or get it in a chat | `POST /v1/audio/speech` | [Speech](https://inference.ai.exchange/docs/speech.md) | | Transcript out | Buy a transcript of audio | `POST /v1/audio/transcriptions` | [Speech-to-text](https://inference.ai.exchange/docs/speech-to-text.md) | | Video out | Buy a generated video | `POST /v1/videos` | [Video generation](https://inference.ai.exchange/docs/video-generation.md) | | Vectors out | Buy embeddings for text | `POST /v1/embeddings` | [Embeddings](https://inference.ai.exchange/docs/embeddings.md) | | Reference inputs | Start a job from images or audio | The job endpoints above | [Reference inputs](https://inference.ai.exchange/docs/reference-inputs.md) | An unchanged OpenAI client works for all of these. The Messages API takes images and documents, but no audio or video. ## Find a model that accepts a modality A request with an attachment competes only among the offers that accept that kind of attachment, in the shape that you sent. To see what a model accepts, read `input_modalities` and `output_modalities` in `GET /v1/exchange/models` (no key needed), or open **Models → All models** in the console. An empty list means text only. Each provider sets its own limits, such as the largest image. ```json { "model": "gpt-oss-120b", "input_modalities": [ { "type": "text" }, { "type": "image", "sources": ["url", "base64"], "max_bytes": 20000000 } ], "output_modalities": [{ "type": "text" }] } ``` `GET /v1/exchange/models` gives one entry for each model and kind. `GET /v1/models` gives the cards that an SDK reads, with `kind`, `image_sizes` and `video_seconds`. Both accept these filters: - `kind=image,speech,video`: one of `text`, `image`, `video`, `speech`, `transcription`, `embedding` or `decision`. - `input=image,audio`: models that read every listed input kind. - `output=audio`: models that write every listed output kind. - Also `model`, `family`, `creator`, `feature`, `region`, `min_context`, `min_score`, `max_output_price`, `listed_within_days`, `contested`. `/v1/exchange/models` refuses `provider`, `byok`, `cost_tier` and `max_input_price` with `400`. A bad value gets `400 invalid_model_filter`. ## Read a model card `GET /v1/models` and `GET /v1/models/{id}` return one card for each model. It tells you what to send before you send it. - `modality` is `text`, `image`, `speech`, `video`, `transcription`, `embedding` or `decision`. - `kind` is the job kind. A chat model has none. - `parameters` lists each control that at least one live offer supports. A name that is not a key is not supported, so omit it. Each key has a descriptor with a `type`: | `type` | Fields | Example | | --------- | ------------ | --------------------------------------------- | | `enum` | `values` | `size`: `["1024x1024", "1536x1024"]` | | `range` | `min`, `max` | `seconds`: from 4 to 12, `output_compression` | | `number` | `min`, `max` | `creativity`: from 0 to 1 | | `integer` | none | `seed`, `upscale_factor` | | `boolean` | none | `stream`, `turns`, `diarize` | | `text` | none | `previous_job_id` | - `pricing` lists the cheapest price of each billable: `billable`, `unit` (`mtok`, `image`, `character` or `second`), `usd` and, if the price depends on an option, the `variant`. A router model or a preset has no `pricing`. - `image_sizes` and `video_seconds` list what image and video offers make. Image `resolution` and `aspect_ratio` are not in `parameters`. Read the tier and the shape from `image_sizes`. - `liquid_supported_parameters` lists the feature names, such as `image.seed`. ```json { "id": "acme-video-1", "modality": "video", "kind": "video_generation", "parameters": { "seconds": { "type": "range", "min": 1, "max": 12 }, "resolution": { "type": "enum", "values": ["720p", "1080p"] }, "seed": { "type": "integer" } }, "pricing": [{ "billable": "output_video", "unit": "second", "usd": 0.05 }] } ``` A control works only when an offer declares it. A request that sets a control with no offer for it gets `400 unsupported_parameter`, and `param` names the feature, such as `image.seed`. The exchange holds nothing and sends nothing. ## What you can send | Kind | Chat Completions part | Messages block | Accepted formats | | -------- | --------------------- | -------------- | ---------------------------------------------------- | | Text | `text` | `text` | Any text | | Image | `image_url` | `image` | `image/png`, `image/jpeg`, `image/webp`, `image/gif` | | Audio | `input_audio` | none | `audio/wav`, `audio/flac`, `audio/mpeg` | | Document | `file` | `document` | `application/pdf` | | Video | `video_url` | none | MP4, MOV and WebM, inline | ### URL or base64 | Kind | `https://` URL | Base64 in a `data:` URL | | -------- | -------------- | ----------------------- | | Image | Yes | Yes | | Audio | No | Yes | | Document | No | Yes | | Video | No | Yes | Only an image can be a remote `https://` URL. Send all other parts inline. The exchange refuses a remote document, audio or video part with `400 unsupported_content`. The exchange does not fetch a URL, so it cannot measure a linked image. Send the bytes inline when you can: the cap is then exact, and more offers can serve the request. **Size limit.** The body must fit the request limit, 20 MB by default. Base64 is one third larger than its bytes, so about 15 MiB of attachments fit. A larger body gets `413 request_too_large`. ## What an attachment costs You pay for an attachment as input tokens. The price cap must cover it. The exchange measures each attachment from its header (PNG size, WAV sample rate, PDF page count) and decodes nothing. If it cannot read a header, it refuses the request with `400 unsupported_content`. It never prices a part at zero tokens. Each offer converts a measured attachment to input tokens with its own table. If an offer states none, the exchange uses this one, which is set above the charge of each known upstream: | Measured as | Input tokens | | ----------------------- | ------------------------------- | | An image | 85, plus 170 per 512-pixel tile | | An image given as a URL | 1,200 | | Audio | 50 per second | | Video | 300 per second | | A PDF | 3,000 per page | This table sets the cap and the ranking. The charge is the token count that the provider meters, so an attachment that the upstream read in fewer tokens costs less than the cap. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). Image, speech and video jobs have a price per unit (one image, one character or one second of video), known before the job starts. For these jobs, the cap is exactly the charge. ## Video in - Only a deployment that registered `video` input reads video. Otherwise a `video_url` part gets `400 unsupported_parameter` with `param` `input.video`. - The exchange reads the length of an inline MP4, MOV or WebM file from its header and rounds it up to the second. The cap uses that length. - A file with no total length in its header, such as a live stream or a fragmented MP4 with no `mehd` box, gets `400 unsupported_content`. To make a video, see [Video generation](https://inference.ai.exchange/docs/video-generation.md). ## What the exchange does not do The exchange does not convert between modalities. It does not transcribe audio, render a PDF as images, describe an image, or fetch, host or keep your files. A deployment that cannot read the kind that you sent cannot win. ## When no offer takes your request The exchange refuses the request, and `eligibility.excluded` gives one reason for each offer. - No offer for the model reads that kind of attachment: `400 unsupported_parameter`, with `param` naming the kind. - Otherwise: `503 no_eligible_offer`. - A part that no offer can serve is refused before the auction with `400 unsupported_content`. The error names the part, such as `messages[0].content[1]`. Examples: a remote document, audio or video; bad Base64; an unreadable header; an unknown part type. The exchange never removes a part. | Reason | `detail` | Meaning | | --------------------- | ------------------------------------------------ | ------------------------------------------------------ | | `feature_unsupported` | `input.image`, `input.file`, … | The deployment does not read that kind at all. | | `media_unsupported` | `image.max_bytes`, `image.formats: image/gif`, … | It reads that kind, but not in the shape of this part. | | `job_kind_not_served` | none | The deployment sells a different kind of job. | ## Rules for image, speech and video jobs These jobs have a price for each unit, so they follow different rules from chat jobs. - **Router models.** A router model that ranks by price gets `400 unsupported_model`, because it ranks by token prices and text scores. A router model that ranks by Auto buys at the lowest estimated charge. A preset of another kind gets `400 unsupported_model`. - **Token rules do not apply.** `exchange.limits.price.input_usd_per_mtok`, `exchange.limits.price.output_usd_per_mtok`, `exchange.limits.price.cache_read_usd_per_mtok`, `exchange.limits.price.cache_write_usd_per_mtok`, `cost_tier` other than `any`, and `exchange.limits.min_tps` get `400 unsupported_rule`, also from a preset. `cap_usd` and the other rules apply. - **`exchange.limits.price.unit_usd`** sets the highest supplier price for one image, one character or one second of video. The exchange compares the charge of the job with this value times the count. An image offer priced in tokens is held to the charge of one image at its output token bound. A chat job gets `400 unsupported_rule`. # Image input Send an image to a chat model as a content part. The model answers in text. The part has the shape of the API that you use, so an unchanged client works. ```bash curl https://router.inference.ai.exchange/v1/chat/completions \ -H "Authorization: Bearer sk-liquid-..." \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-oss-120b", "messages": [{ "role": "user", "content": [ { "type": "text", "text": "what is in this picture?" }, { "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBOR…", "detail": "high" } } ] }] }' ``` Put the text part first, then the images. ## Examples **Read a chart.** Send a screenshot of a chart and ask for its numbers. ```json { "role": "user", "content": [ { "type": "text", "text": "List the value of each bar as a table." }, { "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBOR…", "detail": "high" } } ] } ``` Use `detail: "high"` for small text. It costs more tiles. **Compare two photos.** Send more than one image in one message. Each image is a part, and the cap counts all of them. ```json { "role": "user", "content": [ { "type": "text", "text": "What changed between these two photos of the shelf?" }, { "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,/9j/4AAQ…" } }, { "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,/9j/4BBR…" } } ] } ``` Check `max_references` in `input_modalities`. An offer can limit the number of images. ## Send an image | API | Part | Source | | ---------------- | ----------------------------------- | ----------------------------- | | Chat Completions | `image_url` with `url` and `detail` | `https://` URL or `data:` URL | | Responses | `input_image` with `image_url` | `https://` URL or `data:` URL | | Messages | `image` block with a `source` | `base64` or `url` | Accepted formats are `image/png`, `image/jpeg`, `image/webp` and `image/gif`. An offer can accept fewer. `detail` is `auto`, `low` or `high`. The exchange sends it to the provider without change. ## URL or base64 The exchange does not fetch an image that you give as a URL. It prices the image at 1,200 tokens and never sees its media type. So an offer that limited the formats or the sources to `base64` cannot serve it. If you send the bytes inline, the exchange sets the cap from the real image, and more offers can serve the request. Use a URL only when the image is large and the cap is not a concern. ## Cost The exchange reads the width and height from the header. Each offer converts them to input tokens. Without an offer table, the exchange uses 85 tokens plus 170 for each 512-pixel tile. Tiles follow the published rule of OpenAI. Fit the long side to 2048 pixels, then the short side to 768, then count 512-pixel tiles. `detail: "low"` is one tile for all sizes. The charge is the token count that the provider meters. See [What an attachment costs](https://inference.ai.exchange/docs/modalities.md#what-an-attachment-costs). ## Limits An offer can limit the largest image (`max_bytes`), the formats, the sources and the number of images in one request (`max_references`). A request that breaks a limit does not reach that offer. The reason is `media_unsupported`. ## Troubleshooting | Error | Cause | Fix | | --------------------------- | -------------------------------------------------------- | ------------------------------------------------ | | `400 unsupported_parameter` | No offer for the model reads images. | Pick a model with `image` in `input_modalities`. | | `400 unsupported_content` | The bytes are not base64, or the header is not an image. | Check the encoding and the format. | | `413 request_too_large` | The body is over 20 MB. | Send a smaller image. | | `503 no_eligible_offer` | Offers read images, but none takes this image. | Read `eligibility.excluded`. | ## Next steps - [Modalities](https://inference.ai.exchange/docs/modalities.md): the map of all modalities. - [Document input](https://inference.ai.exchange/docs/document-input.md): send a PDF. - [Image generation](https://inference.ai.exchange/docs/image-generation.md): make an image. # Audio input Send an audio clip to a chat model as a content part. The model answers in text. The exchange does not transcribe the clip for a model that cannot hear. Only an offer that declared audio input can win the job. ```json { "role": "user", "content": [ { "type": "text", "text": "what does the speaker ask for?" }, { "type": "input_audio", "input_audio": { "data": "UklGR…", "format": "wav" } } ] } ``` Audio is for Chat Completions only. The Messages API has no audio block. ## Examples **Summarise a voice note.** Send a short recording and ask for the key points. ```json { "role": "user", "content": [ { "type": "text", "text": "Summarise this voice note in three bullet points." }, { "type": "input_audio", "input_audio": { "data": "UklGR…", "format": "wav" } } ] } ``` **Answer a spoken question.** Send the question as audio with no text. The model answers in text. ```json { "role": "user", "content": [{ "type": "input_audio", "input_audio": { "data": "//uQxAAA…", "format": "mp3" } }] } ``` To get the answer as audio, send the answer text to [Speech](https://inference.ai.exchange/docs/speech.md). ## Send audio `data` is bare base64 or a `data:` URL. The exchange refuses a remote URL with `400 unsupported_content`. | Field | Meaning | | -------- | --------------------------------- | | `data` | The bytes of the clip, in base64. | | `format` | `wav`, `flac` or `mp3`. | An offer can accept fewer formats. ## Cost The exchange reads the duration from the container header. It converts the duration to input tokens at 50 tokens for each second, unless the offer states its own table. The charge is the token count that the provider meters. An offer can limit the length with `max_duration_seconds`, and the size with `max_bytes`. ## Troubleshooting | Error | Cause | Fix | | --------------------------- | --------------------------------------- | -------------------------------------------------- | | `400 unsupported_parameter` | No offer for the model hears audio. | Pick a model with `audio` in `input_modalities`. | | `400 unsupported_content` | A URL, or a header that cannot be read. | Send the bytes as base64 in a supported container. | | `413 request_too_large` | The body is over 20 MB. | Send a shorter clip. | To get the words of a clip, buy a transcript. See [Speech-to-text](https://inference.ai.exchange/docs/speech-to-text.md). ## Next steps - [Modalities](https://inference.ai.exchange/docs/modalities.md): the map of all modalities. - [Speech](https://inference.ai.exchange/docs/speech.md): make spoken audio. # Document input Send a PDF to a chat model as a content part. The model reads the document and answers in text. The exchange does not parse the PDF. Only an offer that declared file input can win the job. ```json { "role": "user", "content": [ { "type": "text", "text": "summarise the risks in this report" }, { "type": "file", "file": { "filename": "q3.pdf", "file_data": "data:application/pdf;base64,JVBER…" } } ] } ``` ## Examples **Summarise a report.** Send the PDF with a short instruction. ```json { "role": "user", "content": [ { "type": "text", "text": "Summarise the risks in this report. Cite page numbers." }, { "type": "file", "file": { "filename": "q3.pdf", "file_data": "data:application/pdf;base64,JVBER…" } } ] } ``` **Extract fields from an invoice.** Ask for JSON, so a program can read the answer. ```json { "role": "user", "content": [ { "type": "text", "text": "Return the invoice number, date and total as JSON." }, { "type": "file", "file": { "filename": "invoice-1042.pdf", "file_data": "data:application/pdf;base64,JVBER…" } } ] } ``` A long PDF costs 3,000 tokens for each page in the cap. Send only the pages that you need. ## Send a PDF | API | Part | | ---------------- | --------------------------------------- | | Chat Completions | `file` with `filename` and `file_data` | | Responses | `input_file` with `file_data` | | Messages | `document` block with a `base64` source | `file_data` is a `data:application/pdf;base64,…` URL. The exchange refuses a document URL or a `file_id`, because it cannot count pages without the bytes. The exchange sends `filename` to the provider. Some upstreams show it to the model. ## Cost The exchange counts the pages of the PDF from its structure. It converts each page to 3,000 input tokens, unless the offer states its own table. The charge is the token count that the provider meters. ## Models that cannot read files A request with a PDF reaches only offers that read files. This applies to a named model and to `liquid.auto`. Auto refuses the request with `400 unsupported_parameter` when no offer reads files. The exchange does not extract text from the PDF for a model that cannot read files. To use a text-only model, extract the text yourself and send it as a text part. ## Troubleshooting | Error | Cause | Fix | | --------------------------- | ------------------------------------ | ----------------------------------------------- | | `400 unsupported_parameter` | No offer for the model reads files. | Pick a model with `file` in `input_modalities`. | | `400 unsupported_content` | A URL, a `file_id`, or a broken PDF. | Send the PDF bytes as base64. | | `413 request_too_large` | The body is over 20 MB. | Split the document. | ## Next steps - [Modalities](https://inference.ai.exchange/docs/modalities.md): the map of all modalities. - [Image input](https://inference.ai.exchange/docs/image-input.md): send an image. # Speech-to-text Buy a transcript of an audio file. The endpoint has the shape of the OpenAI transcriptions API, so an unchanged OpenAI client works. It also reads the JSON form that OpenRouter uses. Only a deployment that is registered to transcribe can win the job. A chat deployment never wins one. This job is not the same as [Audio input](https://inference.ai.exchange/docs/audio-input.md). Audio input sends a clip to a chat model that can hear. This job returns the words of the clip. ## Quickstart ```bash curl https://router.inference.ai.exchange/v1/audio/transcriptions \ -H "Authorization: Bearer sk-liquid-..." \ -F model=whisper-1 \ -F file=@meeting.mp3 \ -F response_format=verbose_json ``` ```python from openai import OpenAI client = OpenAI(base_url="https://router.inference.ai.exchange/v1", api_key="sk-liquid-...") with open("meeting.mp3", "rb") as audio: transcript = client.audio.transcriptions.create(model="whisper-1", file=audio) print(transcript.text) ``` ## Find a model A control works only when an offer for the model declares it. Read the model card before you send `diarize` or `keyterms`. - `GET /v1/models?kind=transcription` lists the models as cards, with `kind` `audio_transcription` and `modality` `transcription`. - `parameters` lists `diarize` and `keyterms` when an offer declared them. A name that is not a key is not supported, so omit it. - `pricing` gives the price of one second of audio. See [Modalities](https://inference.ai.exchange/docs/modalities.md#read-a-model-card). If no offer declared a control that you sent, the answer is `400 unsupported_parameter`, and `param` names the feature, such as `transcription.diarize`. ## Controls `diarize` and `keyterms` are offer features. `language`, `timestamp_granularities` and the audio limits are matched against what an offer registered. ### Speakers and key terms `diarize` and `keyterms` are offer features: `transcription.diarize` and `transcription.keyterms`. Only an offer that declared the feature can win a request that sets the field. ```bash curl https://router.inference.ai.exchange/v1/audio/transcriptions \ -H "Authorization: Bearer sk-liquid-..." \ -F model= \ -F file=@meeting.mp3 \ -F response_format=verbose_json \ -F diarize=true \ -F 'keyterms[]=Liquid Inference' \ -F 'keyterms[]=nano-USD' ``` - With `diarize`, each segment has a `speaker`, such as `speaker_0`. Without it, a segment has no `speaker`. `diarize` without `verbose_json` gets `400 invalid_request`. - With `keyterms`, the transcript spells each term as you gave it. A list of more than 100 terms, or a term over 100 characters, gets `400 invalid_request`. ### Audio by link Send an `https://` link in `url` and the exchange fetches the file. ```json { "model": "", "input_references": [{ "type": "audio", "url": "https://example.com/call.wav" }] } ``` - The link must use `https` and name a public host. The exchange refuses an IP address, a host in its own network, a port that is not the default port, a login in the link, and a redirect to any of these. - The file has the same limits as a file that you send: 25 MB and two hours. - The fetch stops after 20 seconds and three redirects. - A link that the exchange cannot read gets `400`, and the message says why. It does not repeat the link. ### Audio limits The exchange reads WAV, FLAC, MP3, Ogg, WebM, M4A and AAC from the bytes of the file. It does not trust the name or the `format` field. - A file can be 25 MB and two hours at most. - A file with no readable length gets `400`, because the exchange cannot fix a cap without it. - An offer can also limit the container, the length (`max_duration_seconds`) and the size (`max_bytes`). A request that breaks a limit does not reach that offer. ## Response `json` returns the text, the usage and the `exchange` block. ```json { "text": "Welcome to the meeting.", "usage": { "type": "duration", "seconds": 4 }, "exchange": { "...": "..." } } ``` `verbose_json` adds `language`, `duration` (the seconds that the exchange measured), `segments` and `words`. A segment or a word is present only when you asked for that granularity. A segment has `id`, `start`, `end` and `text`, and a `speaker` with `diarize`. ```json { "id": 0, "start": 0.0, "end": 3.4, "text": "Welcome to the meeting.", "speaker": "speaker_0" } ``` `text` returns plain text. Its `exchange` block comes back as JSON in the `x-liquid-exchange` response header. ## Billing The price is for each second of audio. The exchange reads the length from the file, so the cap is known before dispatch. It is exactly the most that you can pay. The provider meters the seconds that it worked on. You pay that figure, never more than the cap. ## Request parameters Send `multipart/form-data` with the audio in `file`, or JSON with the audio in `input_references`. | Field | Type | Required | Description | | ------------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | | `model` | string | Yes | The model to buy, or a preset of the transcription kind. | | `file` | file | Yes\* | The audio, in a form body. | | `input_references` | array | Yes\* | One entry of `type` `audio`: a `data:audio/` URL or an `https://` link in `url`, or base64 in `data` and `media_type`. | | `response_format` | string | No | `json` (the default), `text` or `verbose_json`. `srt` and `vtt` are not supported. | | `timestamp_granularities` | array | No | `segment`, `word` or both. Only with `verbose_json`. An offer that cannot return them cannot win. | | `diarize` | boolean | No | Label each segment with its speaker. Needs `verbose_json`. | | `keyterms` | array | No | Up to 100 words or names, each up to 100 characters, that the transcript must spell as given. | | `language` | string | No | An ISO 639-1 code such as `en`. An offer that declared its languages must list it. | | `prompt` | string | No | A hint of vocabulary or style. At most 2,048 characters. | | `temperature` | number | No | From 0 to 1. | | `exchange` | object | No | Purchasing rules, such as `cap_usd`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). | \* Send `file` or `input_references`. Send one audio entry only. `stream` is not supported. The transcript comes whole. ## Errors | Error | Cause | Fix | | --------------------------- | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------- | | `400` | No audio, a file that is too large or too long, or a length that the exchange cannot read. | Send a supported container under 25 MB and two hours. | | `400 unsupported_model` | The model is not a transcription model. | Pick a model that lists audio input and a transcript output. | | `400 unsupported_parameter` | No offer serves your `language`, format or granularity, or none declared a control. | Read `parameters` on the model card, or `eligibility.excluded`. | ## Next steps - [Audio input](https://inference.ai.exchange/docs/audio-input.md): send audio to a chat model. - [Speech](https://inference.ai.exchange/docs/speech.md): turn text into speech. - [Modalities](https://inference.ai.exchange/docs/modalities.md): the map of all modalities. # Image generation Generate and edit images with one request. The endpoint has the shape of the OpenAI images API, so an unchanged OpenAI client works. Only a deployment that is registered to make images can win the job. ## Quickstart ```bash curl https://router.inference.ai.exchange/v1/images/generations \ -H "Authorization: Bearer sk-liquid-..." \ -H "Content-Type: application/json" \ -d '{ "model": "gemini-2.5-flash-image", "prompt": "a cat on a bicycle", "n": 2, "size": "1024x1024" }' ``` ```python from openai import OpenAI client = OpenAI(base_url="https://router.inference.ai.exchange/v1", api_key="sk-liquid-...") result = client.images.generate( model="gemini-2.5-flash-image", prompt="a cat on a bicycle", n=2, size="1024x1024", ) ``` Each image comes back as `b64_json` or `url`. The `exchange` block of the answer holds the receipt. See [Response](#response). ## Examples Operators of the exchange made example images with each model. The page of a model in the console lists them. The app shows two here, each with its model and its prompt. ## Find a model A control works only when an offer for the model declares it. Read what a model supports before you send a control. | Source | Use it for | | ------------------------------------ | ---------------------------------------------------------------------------------------------- | | `GET /v1/models?kind=image` | One card for each model: `parameters`, `image_sizes` and `pricing`. | | `GET /v1/exchange/models?kind=image` | The market view, with no key: `sizes`, `formats`, `response_formats` and `rate_per_image_usd`. | | The model page in the console | The same data, with example images and a Playground. | On the card of an image model: - `kind` is `image_generation` and `modality` is `image`. - `image_sizes` lists the pixel sizes that you can send as `size`. The tier and the shape come from these sizes. See [Size, tier and shape](#size-tier-and-shape). - `parameters` lists each other control that at least one offer supports: `output_format`, `quality`, `seed`, `background`, `output_compression` and `stream`. A name that is not a key is not supported, so omit it. [Modalities](https://inference.ai.exchange/docs/modalities.md#read-a-model-card) describes each descriptor. - `pricing` gives the price of an image. If the price depends on a size or a quality, the line names it. A preset of the image kind buys every listed model of its kind. If a request sets a control that no offer declared, the answer is `400 unsupported_parameter` and `param` names the control or its feature, such as `image.seed`. The exchange holds nothing and sends nothing. ## How it works 1. **Send the request.** The exchange picks the offers that can serve it. 2. **The exchange fixes the price cap.** The price is for one image, so the cap is exactly the charge. 3. **The provider makes the images.** The exchange dispatches the size that it resolved. 4. **You get the answer**, or events if you set `stream`. The charge is for the images that the provider returned. ## Size, tier and shape Send `size` when you know the pixel size. Send `resolution` or `aspect_ratio` when you do not. The exchange then picks a pixel size that an offer declared. It buys, prices and dispatches that size, as if you had sent it as `size`. ```json { "model": "", "prompt": "a cat on a bicycle", "resolution": "2K", "aspect_ratio": "16:9" } ``` | Tier | Sizes that have it | | ------ | ------------------------------------- | | `512` | `512x512` | | `768` | `1024x576` | | `1K` | `1024x1024`, `1536x1024`, `1024x1536` | | `1.5K` | `1920x1080` | | `2K` | `2048x2048`, `3840x2160` | | `4K` | `4096x4096` | - A tier is the pixel count of a square of that side. The tier of a size is the tier that is nearest to its area. - A shape matches a size when the two ratios differ by 1% or less. `aspect_ratio: "auto"` asks for nothing. - Without `resolution`, the exchange picks the size that is nearest to `1K`. - The size that the exchange picks sets the price. A model can charge more for a larger size. - If no declared size has the tier and the shape, the answer is `400 unsupported_parameter`. It lists the sizes that offers declare. A model with no declared size cannot be asked for a tier or a shape. - If you send `size` as well and it has another tier or shape, the answer is `400 conflicting_parameters` and names both fields. ## Controls `seed`, `background`, `output_compression` and `stream` are offer features: `image.seed`, `image.background`, `image.output_compression` and `image.stream`. An offer wins a request that sets one only when it declared the feature. An offer that did not declare it cannot win, because it would ignore the control and still charge for the image. `background: "auto"` and `stream: false` ask for nothing. Any offer can win them. ### Seed `seed` makes a picture repeatable on a best-effort basis. An upstream can change its model. ```json { "model": "", "prompt": "a cat on a bicycle", "seed": 42 } ``` ### Background `background` is `transparent`, `opaque` or `auto`. `transparent` needs `output_format` `png` or `webp`, or no `output_format`. With `jpeg` the answer is `400 conflicting_parameters`. ```json { "model": "", "prompt": "a logo", "output_format": "png", "background": "transparent" } ``` ### Compression `output_compression` is a whole number from 0 to 100. It applies to `jpeg` and `webp`. With `png` the answer is `400 conflicting_parameters`. ```json { "model": "", "prompt": "a cat", "output_format": "webp", "output_compression": 80 } ``` ### Quality An offer can list the quality names that it takes. If the offers of a model list any, the card has a `quality` key in `parameters`, and an offer that omits your name cannot win. An offer that lists none states nothing, so it can win and its provider decides. A provider that does not take the name fails the job before it renders, so send `quality` only when the model lists it. ### Reference images Send images in `input_references` to start from a picture. See [Reference inputs](https://inference.ai.exchange/docs/reference-inputs.md). `POST /v1/images/edits` is the same purchase for the OpenAI SDK. It needs at least one reference image, takes the files in `image`, and refuses a `mask`. ## Stream Send `stream: true` to get the answer as server-sent events. The content type is `text/event-stream`. The request must ask for one image: `stream` with `n` above 1 gets `400 conflicting_parameters`. ```bash curl -N https://router.inference.ai.exchange/v1/images/generations \ -H "Authorization: Bearer sk-liquid-..." \ -H "Content-Type: application/json" \ -d '{"model": "", "prompt": "a cat on a bicycle", "stream": true}' ``` ```text event: image_generation.partial_image data: {"type":"image_generation.partial_image","partial_image_index":0,"b64_json":"iVBOR…","media_type":"image/png"} event: image_generation.completed data: {"type":"image_generation.completed","b64_json":"iVBOR…","media_type":"image/png","exchange":{"...":"..."}} ``` | Event | Fields | When | | -------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------- | | `image_generation.partial_image` | `partial_image_index`, `b64_json`, `media_type` | Once for each draft. A provider sends at most three. | | `image_generation.completed` | `b64_json` or `url`, `media_type`, `revised_prompt` if any, `exchange` | Once, for the final image. | - The exchange sends the events when the job ends, because the charge is known only then. It does not send the first draft early. - A draft is not an image that you pay for. The charge counts the final image. - If the winning offer does not stream, the answer is the normal JSON body. ## Response ```json { "created": 1760000000, "data": [{ "b64_json": "iVBOR…", "media_type": "image/png" }], "exchange": { "...": "..." } } ``` - Each image has `b64_json` or `url`, as the upstream of the provider returned it. `media_type` states the MIME type. `revised_prompt` appears when the upstream rewrote your prompt. - The exchange does not download or host your images. A `url` is the link of the provider, and it can expire. - The `exchange` block is the receipt. It reports `images`, so you can compare the images that you paid for with the images that you got. See [Receipts](https://inference.ai.exchange/docs/receipts.md). - The answer and each event also have `mime_type`, with the same value as `media_type`, for clients that read the older name. ## Billing The price is for one image, so the cap that the exchange fixes is exactly the charge. The price of the size and the quality that the request names sets the charge. An image offer priced in tokens is held to the charge of one image at its output token bound. `exchange.limits.price.unit_usd` sets the highest price for one image. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md) and [Modalities](https://inference.ai.exchange/docs/modalities.md). ## Images in a chat answer Some chat models draw in their answer. Send `modalities` with `image` to `POST /v1/chat/completions`. ```bash curl https://router.inference.ai.exchange/v1/chat/completions \ -H "Authorization: Bearer sk-liquid-..." \ -H "Content-Type: application/json" \ -d '{ "model": "", "modalities": ["image", "text"], "messages": [{ "role": "user", "content": "a cat on a bicycle" }] }' ``` Each image is in `choices[0].message.images`. The `url` of an image is a `data:` URL when the provider returned bytes. It is a link when the provider returned a link. The exchange does not change the form. Only an offer that declared image output (`output.image`) can win the job. If no offer did, the exchange refuses the request with `unsupported_parameter` before the auction. It does not send you a text answer. ### Image config Send `image_config` to ask for a size. It needs `image` in `modalities`. ```json { "model": "", "modalities": ["image", "text"], "image_config": { "resolution": "2K", "aspect_ratio": "16:9" }, "messages": [{ "role": "user", "content": "a cat on a bicycle" }] } ``` | Key | Value | | ---------------------------- | ----------------------------------------------------------- | | `size` | `x`, or `auto`. | | `resolution` or `image_size` | A tier: `512`, `768`, `1K`, `1.5K`, `2K` or `4K`. | | `aspect_ratio` | `:`, or `auto`. | | `quality` | A quality name. It goes to the upstream and is not matched. | - All values are strings. Send `resolution` or `image_size`, not both with different values. - Any other key gets `400 invalid_request`, and the error names the key, such as `image_config.seed`. - An offer wins only when it declared a pixel size that has the `size`, the tier and the shape. An offer that declared `output.image` and no size cannot win a request with one of them. ### Cost and limits in a chat answer - An image costs output tokens. The charge is the output tokens that the provider reports, at the rate of the offer. - If you send no `max_tokens`, the exchange allows 8,192 output tokens. Your price cap still limits the charge. - If you set `max_tokens` below the size of one image, the model can stop before it draws. You pay for the tokens that it used. - Send `stream` as `false`. A request with `stream: true` and `image` in `modalities` gets `400`. - `/v1/messages` and `/v1/responses` do not return images. - If you hang up before the answer arrives, the job does not settle, and you are not charged. ## Request parameters | Field | Type | Required | Description | | -------------------- | ------- | -------- | ----------------------------------------------------------------------------------------- | | `model` | string | Yes | The model to buy, or a preset of the image kind. | | `prompt` | string | Yes | The text that describes the image. At most 32,000 characters. | | `input_references` | array | No | Reference inputs of any kind, in one list. See [Reference inputs](https://inference.ai.exchange/docs/reference-inputs.md). | | `n` | integer | No | How many images, from 1 to 10. The default is 1. | | `size` | string | No | A pixel size such as `1024x1024`, or `auto`. | | `resolution` | string | No | A size tier: `512`, `768`, `1K`, `1.5K`, `2K` or `4K`. | | `aspect_ratio` | string | No | `auto`, or a shape such as `16:9`. | | `output_format` | string | No | `png`, `jpeg` or `webp`. | | `quality` | string | No | A quality name that the model lists in `parameters`. | | `background` | string | No | `transparent`, `opaque` or `auto`. | | `output_compression` | integer | No | The compression of a `jpeg` or `webp` image, from 0 to 100. | | `seed` | integer | No | A number that makes the picture repeatable. | | `stream` | boolean | No | `true` sends the answer as events. | | `response_format` | string | No | `url` or `b64_json`. If you omit it, any offer can answer. | | `exchange` | object | No | Purchasing rules, such as `cap_usd`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). | A request that sends `size`, `output_format` or `quality` goes only to offers that serve that value, so it does not fail after the choice. If you omit `size`, the provider uses its default, and every offer for the model can answer. ## Errors | Error | Cause | Fix | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | `400 unsupported_model` | You named a router model that ranks by price. | Name an image model, or an Auto router model. | | `400 unsupported_rule` | A rule that limits a price or a speed in tokens. | Use `exchange.limits.price.unit_usd` and `cap_usd`. | | `400 unsupported_parameter` | No offer serves your `size`, `resolution`, `aspect_ratio` or `output_format`, or none declared a control. `param` names the field or feature. | Omit it, or read `image_sizes` and `parameters` on the model card. | | `400 conflicting_parameters` | `size` has another tier or shape, `background` `transparent` with `jpeg`, `output_compression` with `png`, or `stream` with `n` above 1. | Send one of the two fields, or change the other. | | `400` | No offer takes all of your references. | Read `eligibility.excluded`. | See [Errors](https://inference.ai.exchange/docs/errors.md) for each code and exclusion reason. ## Next steps - [Reference inputs](https://inference.ai.exchange/docs/reference-inputs.md): start from images, audio or video. - [Video generation](https://inference.ai.exchange/docs/video-generation.md): the same job for video. - [Modalities](https://inference.ai.exchange/docs/modalities.md): the map of all modalities. # Speech Buy spoken audio from text. The endpoint has the shape of the OpenAI speech API, so an unchanged OpenAI client works. The response body is the audio file. ## Quickstart ```bash curl https://router.inference.ai.exchange/v1/audio/speech \ -H "Authorization: Bearer sk-liquid-..." \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini-tts", "input": "a cat on a bicycle", "voice": "alloy", "response_format": "mp3" }' --output speech.mp3 ``` ```python from openai import OpenAI client = OpenAI(base_url="https://router.inference.ai.exchange/v1", api_key="sk-liquid-...") with client.audio.speech.with_streaming_response.create( model="gpt-4o-mini-tts", voice="alloy", input="a cat on a bicycle" ) as response: response.stream_to_file("speech.mp3") ``` ## Examples Operators of the exchange made example voice samples with each model. The page of a model in the console lists them. The app shows two here, each with its model and its prompt. Press play to hear one. ## Find a model A control works only when an offer for the model declares it. Read the model card before you send a control. - `GET /v1/models?kind=speech` lists the models as cards, with `kind` `audio_speech` and `modality` `speech`. - `parameters` lists `instructions` and `turns` when an offer declared them. A name that is not a key is not supported, so omit it. - `pricing` gives the price of one character. - `GET /v1/exchange/models?kind=speech` needs no key. The `audio` entry of `output_modalities` lists the `voices` and `formats` that you can send as `voice` and `response_format`, as the union across the offers. `rate_per_character_usd` is the price in USD for each character. See [Modalities](https://inference.ai.exchange/docs/modalities.md#read-a-model-card) for the card and the other filters. ## Controls A control works only when an offer declares it: `speech.instructions` for a style line and `speech.turns` for more than one speaker. `voice` is matched against the voices that an offer registered. ### Voice `voice` is optional. An offer that declared its voices must list the voice that you name. A request for a voice that no offer lists gets no offer. Leave `voice` out to get the first voice of an offer that declared its voices. An offer that declared no voices cannot win a request that names none. ### Style Send `instructions` to set the style of the clip. Only an offer that declared `speech.instructions` can win. ```json { "model": "", "input": "Welcome back.", "instructions": "warm, slow", "voice": "alloy" } ``` ### Many speakers Send `input` as an array of turns for a clip with more than one speaker. Each turn has its own `voice` and can have its own `instructions`. A turn with no `voice` uses the `voice` of the request. A turn with no `instructions` uses the `instructions` of the request. ```json { "model": "", "input": [ { "text": "Hello, Jane.", "voice": "Kore" }, { "text": "Hi.", "voice": "Puck", "instructions": "tired" } ], "instructions": "cheerful" } ``` - An array holds from 1 to 64 turns. An array with one turn is the same as a string. - An array with more than one turn reaches only an offer that declared `speech.turns`. - A voice sample in `input_references` belongs to one voice. Send it with a string or with one turn. ## Response The body is the audio. - The `exchange` block comes back as JSON in the `x-liquid-exchange` response header, next to `x-liquid-cap-usd` and `x-liquid-job-id`. - It reports `characters`, so you can compare the characters that you paid for with the characters that you sent. - There is no streaming form. The exchange sends each clip complete. ## Billing The price is for each character of the text of all the turns. The exchange counts characters, not bytes, so accented and non-Latin text does not cost more for each character. It does not count voice names or `instructions`. The cap is exactly the charge. ## Request parameters | Field | Type | Required | Description | | ------------------ | --------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `model` | string | Yes | The model to buy, or a preset of the speech kind. | | `input` | string or array | Yes | The text to speak, or an array of turns `{text, voice?, instructions?}` for more than one speaker. At most 4,096 characters in all. | | `input_references` | array | No | A sample of a voice (role `voice_sample`) and its words (a `text` entry with the role `voice_transcript`). See [Reference inputs](https://inference.ai.exchange/docs/reference-inputs.md). | | `voice` | string | No | The voice. See Voice. | | `instructions` | string | No | A style line, such as `cheerful`. At most 1,000 characters. | | `response_format` | string | No | `mp3`, `opus`, `aac`, `flac`, `wav` or `pcm`. The default is `mp3`. | | `speed` | number | No | From 0.25 to 4.0. It changes the length of the audio, not the cost. | | `quality` | string | No | A quality name such as `hd`. A deployment that prices that name apart charges its rate. Otherwise the base rate applies. | | `exchange` | object | No | Purchasing rules, such as `cap_usd`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). | ## Errors | Error | Cause | Fix | | --------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | | `400 unsupported_model` | The model is not a speech model. | Read `/v1/models` for `kind: audio_speech`. | | `400 unsupported_parameter` | No offer serves your `voice` or format, or none declared `speech.instructions` or `speech.turns`. `param` names it. | Read `parameters` on the model card, or `eligibility.excluded`. | ## Speech in a chat answer A chat model that speaks can answer with text and speech in one request. This is a different purchase from `/v1/audio/speech`: it is a chat job priced in tokens. Send `modalities` with `"audio"` and an `audio` object. This is the OpenAI Chat Completions form. ```json { "model": "", "stream": true, "modalities": ["text", "audio"], "audio": { "voice": "alloy", "format": "pcm16" }, "messages": [{ "role": "user", "content": "Say hello" }] } ``` Each stream chunk can hold `choices[0].delta.audio`. The first chunk has an `id`. A chunk has `data` (base64 audio), `transcript` (the text that the audio says), or both. ### Which offers can win - Only an offer that declared `output.audio` can win. If no offer did, the exchange refuses the request and names `output.audio`. - The request needs a `voice` and a `format`. An offer that declared its voices or formats wins only a request for one of them. ### The answer - With `stream: true`, the audio comes as `delta.audio` chunks. - Without it, the answer is one message. `message.audio` holds `id`, `data`, `expires_at` and `transcript`, and `content` is `null`. The router holds at most 32 MiB of audio for one answer. ### Continue the talk Send the earlier assistant turn with `"audio": {"id": "…"}`. The exchange keeps no audio, so the upstream that made the clip resolves the `id`. - This works only on that upstream, and only until it forgets the clip. - The `expires_at` of a buffered answer is the time of the answer, not the upstream's limit. - If the upstream refuses the `id`, send the transcript as the text of the turn. ### Cost - The provider counts the audio tokens apart from the text tokens. Audio tokens are a part of `completion_tokens`. - The exchange charges audio tokens at the audio rate of the offer, and other output tokens at the text rate. - The price cap uses the higher of the two rates for every output token that the request allows. So the cap holds for any mix of text and audio. - The charge uses the mix that the provider reports. The unused part of the cap returns to your balance. ### Failure The exchange can change to the next offer only before the first chunk. After the first chunk, a failure ends the job, and you pay for the output that you received. ## Next steps - [Audio input](https://inference.ai.exchange/docs/audio-input.md): send audio to a chat model. - [Modalities](https://inference.ai.exchange/docs/modalities.md): the map of all modalities. # Video generation Generate video from text, images or video. The endpoints have the shape of the OpenAI videos API, so `client.videos.create_and_poll` and `client.videos.download_content` work. To move code that called Sora, see [Move from the Sora API](https://inference.ai.exchange/docs/move-from-sora.md). A render takes up to several minutes, so a video job is asynchronous. You create the job, then poll it or ask the exchange to call you, then download the file. ## Quickstart ```bash curl https://router.inference.ai.exchange/v1/videos \ -H "Authorization: Bearer sk-liquid-..." \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-1-5-pro", "prompt": "a red paper boat on a stream", "seconds": "5", "size": "1280x720" }' # {"id": "job_...", "object": "video", "status": "in_progress", ...} curl https://router.inference.ai.exchange/v1/videos/job_... \ -H "Authorization: Bearer sk-liquid-..." # repeat until "status" is not "in_progress" curl https://router.inference.ai.exchange/v1/videos/job_.../content \ -H "Authorization: Bearer sk-liquid-..." --output video.mp4 ``` ```python from openai import OpenAI client = OpenAI(base_url="https://router.inference.ai.exchange/v1", api_key="sk-liquid-...") video = client.videos.create_and_poll( model="seedance-1-5-pro", prompt="a red paper boat on a stream", seconds="5", size="1280x720", ) if video.status == "completed": client.videos.download_content(video.id).write_to_file("video.mp4") ``` ## Examples ## How it works 1. **Create the job.** `POST /v1/videos` returns the job with the status `in_progress`. The exchange fixes the price cap and holds it. 2. **Wait.** Poll `GET /v1/videos/{id}`, or send `callback_url` and let the exchange call you. 3. **Read the end state.** The job ends as `completed`, `failed`, `cancelled` or `expired`. 4. **Download.** `GET /v1/videos/{id}/content` returns the file of a `completed` job. You can cancel a job in step 2. Only a `completed` job costs money. ## Find a model A control works only when an offer for the model declares it. Read what a model supports before you send a control. | Source | Use it for | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | | `GET /v1/models?kind=video` | One card for each model: `parameters`, `video_seconds` and `pricing`. | | `GET /v1/exchange/models?kind=video` | The market view, with no key: `sizes`, `seconds`, `formats`, `max_duration_seconds` and `rate_per_video_second_usd`. | | The model page in the console | The same data, with example videos and a Playground. | On the card of a video model: - `kind` is `video_generation` and `modality` is `video`. - `parameters` lists each control that at least one offer supports. `seconds` is a `range` or an `enum`. `size` and `resolution` are an `enum`. `generate_audio` is a `boolean` when the model makes both kinds of video. `seed`, `creativity`, `upscale_factor` and `previous_job_id` are listed when an offer declared them. A name that is not a key is not supported, so omit it. [Modalities](https://inference.ai.exchange/docs/modalities.md#read-a-model-card) describes each descriptor. - `video_seconds` gives the lengths. `pricing` gives the price for each second, with a `variant` when the price depends on the sound or the resolution. - On the market view, if `default_size_only` is true, omit `size`. If a request sets a control that no offer declared, the answer is `400 unsupported_parameter`. `param` names the feature, such as `video.continue`, or the field, such as `resolution`. The exchange holds nothing. It never drops a control, because you would pay for a video made without it. ## Size, shape, resolution and sound Send `size` when you know the pixel size. Otherwise describe the video, and the exchange picks a size that an offer declared. ```json { "model": "", "prompt": "a red paper boat", "seconds": 5, "aspect_ratio": "16:9", "resolution": "720p" } ``` | You send | The exchange does | | ------------------------------- | ------------------------------------------------------------------------------------------ | | `size` | Matches offers that declared that size. | | `aspect_ratio` | Picks the smallest declared size that has the shape. | | `aspect_ratio` and `resolution` | Keeps only the sizes whose short side has the pixels of the label, such as 720 for `720p`. | | `resolution` alone | Picks no size. It limits the offers to those that make that resolution. | | `generate_audio` | Matches offers that make that mode. `true` buys sound, `false` buys a silent video. | | None of these | Lets any offer win. It makes its default. | - If no declared size fits, the answer is `400 unsupported_parameter`. - If `size` has another shape than `aspect_ratio`, or another short side than `resolution`, the answer is `400 conflicting_parameters` and names both fields. - A silent video or a lower resolution can cost less. The cap and the charge use the price of the options that you asked for. ## Controls `seed`, `creativity`, `upscale_factor` and `previous_job_id` are offer features: `video.seed`, `video.creativity`, `video.upscale_factor` and `video.continue`. Only an offer that declared the feature can win a request that sets the field. A negative `seed`, a `creativity` outside 0 to 1 and an `upscale_factor` below 2 get `400 invalid_request` with the name of the field. ### Seed `seed` repeats a render on a best-effort basis. An upstream can change its model. ```json { "model": "", "prompt": "a red paper boat", "seconds": 5, "seed": 7 } ``` ### Upscale `upscale_factor` and `creativity` apply to a model that upscales a video. Send the source as a reference. ```json { "model": "", "prompt": "the same video, sharper", "input_references": [{ "type": "video", "url": "https://example.com/clip.mp4" }], "upscale_factor": 2, "creativity": 0.3 } ``` ### Continue a video `previous_job_id` must name a `completed` video job of your workspace. Each job is charged for its own seconds. The earlier job is not charged again. ```json { "model": "", "prompt": "the boat reaches the sea", "previous_job_id": "job_..." } ``` - An id that is no video job of your workspace, including a job of another workspace, gets `400 invalid_request` with `param` `previous_job_id`. - A job that has not completed gets `400 conflicting_parameters` with `param` `previous_job_id`. - The status response repeats `previous_job_id`. ### Start image and frames A video can start from an image. The exchange picks it in this order: the entry with the role `first_frame`, then the first image with no role in `input_references`. An entry with the role `last_frame` fixes the last frame. See [Reference inputs](https://inference.ai.exchange/docs/reference-inputs.md). An upstream crops a start image that does not have the shape of the video, so the exchange refuses one: - The shape of the video is the shape of `size`, or the default size of each offer without `size`. The two must agree within 1%, so a 1280x720 image fits a 1920x1080 video. - The shape is what a viewer sees: an EXIF turn swaps the width and height of a JPEG. - With `size`, an image of another shape gets `400 invalid_request`. No money is held. - Without `size`, an offer whose default size has another shape cannot win. If none is left, the answer is `400 no_eligible_offer`, and the message names a `size` that fits. - Crop or pad a refused image, or send a `size` of its shape. The exchange reads a linked (`https:`) start image by header only, with `Range` requests. The frame header must start in the first 256 KiB. The link must use `https`, name a public host, use the default port and hold no login, and the host must answer within 3 seconds. If the exchange cannot read the link, it refuses the request and states the reason. ## Job status | `status` | Meaning | Cost | | ------------- | ----------------------------------------------------------------------------------------------------------- | ------- | | `in_progress` | A provider accepted the job and is rendering it. | Held | | `completed` | The file is ready to download. | Charged | | `failed` | The render failed. `error` states why. | None | | `cancelled` | You cancelled the job. | None | | `expired` | The job did not finish within the video job limit of the exchange. The default is 10 minutes from dispatch. | None | - `has_audio` is a field of the answer. It states if the video has sound: the mode that you asked for in `generate_audio`, else the default of the offer that won. It is absent when the exchange does not know. - When the job ends, the status response holds the `exchange` block with `video_seconds`, the seconds that you paid for. ## Cancel a job `POST /v1/videos/{id}/cancel` ends a job that is `in_progress`. ```bash curl -X POST https://router.inference.ai.exchange/v1/videos/job_.../cancel \ -H "Authorization: Bearer sk-liquid-..." ``` - The exchange stops the provider and releases the hold. The job costs nothing. The answer is the job with the status `cancelled`. - The provider stops within a few seconds. If the job finishes first, the answer is `409 video_not_cancellable`, and the job keeps the status `completed` and its charge. - A job that is `completed`, `failed` or `expired` also gets `409 video_not_cancellable`. - A repeat cancel of a cancelled job returns the job. - A key that can only read cannot cancel. ## Callback Add `callback_url` to `POST /v1/videos` to get a call instead of polling. The response then holds a `callback` block: ```json { "id": "job_...", "status": "in_progress", "callback": { "url": "https://example.com/hooks/video", "signing_secret": "whsec_..." } } ``` The `callback` block is only in the response that places the job. Keep the `signing_secret`: the exchange does not state it again. With no `callback` block, poll the job. The address must be an `https` URL with a public host name, the default port, no login and no IP address. Otherwise the exchange refuses the request and places no job. ### Payload When the job ends, the exchange sends a `POST` with a JSON body: ```json { "id": "job_....video.generation.completed", "type": "video.generation.completed", "created_at": 1790000000, "data": { "id": "job_...", "object": "video", "status": "completed", "exchange": {} } } ``` | `type` | Sent when | | ---------------------------- | ----------------------- | | `video.generation.completed` | The job is `completed`. | | `video.generation.failed` | The job is `failed`. | | `video.generation.cancelled` | The job is `cancelled`. | | `video.generation.expired` | The job is `expired`. | `data` is the object that `GET /v1/videos/{id}` returns, with no file. Download the file with `GET /v1/videos/{id}/content`. ### Verify a delivery | Header | Value | | -------------------------- | ------------------------------------------------------------------------------ | | `X-Liquid-Signature` | `t=,v1=`: HMAC-SHA256 of `.` with `signing_secret` | | `X-Liquid-Event` | The same as `type` | | `X-Liquid-Idempotency-Key` | The same for each attempt of one delivery. Drop a repeat | Compute the HMAC over the `t` value, a dot and the raw body. Compare it with `v1` in constant time. Refuse a `t` that is more than five minutes old. ### Answer and retries Answer with a `2xx` status within 10 seconds. The exchange does not follow a redirect. - Another status, or no answer, causes a retry after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 2 hours, 6 hours and 12 hours. Then it stops. An answer of `410` stops it at once. - Delivery is at least once, so your handler must accept a repeat. Polling always works. ## Download `GET /v1/videos/{id}/content` returns the file. The provider that rendered the video serves the download. You can download again until `expires_at`, if the response states it. - A job that is not `completed` gets `404 video_not_ready`. - After `expires_at`, the answer is `410 video_expired`. ## Billing The price is for each second of video that you ask for. The create response returns `x-liquid-cap-usd` and `x-liquid-job-id`. The cap is exactly the charge, and the exchange settles when the render is complete. A `failed`, `cancelled` or `expired` job costs nothing. ## Request parameters The body can be JSON or `multipart/form-data`. | Field | Type | Required | Description | | ------------------ | ---------------- | -------- | ---------------------------------------------------------------------------------------------------------- | | `model` | string | Yes | The model to buy, or a preset of the video kind. | | `prompt` | string | Yes | The text that describes the video. At most 32,000 characters. | | `input_references` | array | No | Reference inputs in one list, with a `role` for the frames. See [Reference inputs](https://inference.ai.exchange/docs/reference-inputs.md). | | `seconds` | number or string | No | The length, from 1 to 30. `duration` is another name for it. | | `size` | string | No | A pixel size such as `1280x720`. | | `aspect_ratio` | string | No | `auto`, or a shape such as `16:9`. | | `resolution` | string | No | A label such as `720p` or `1080p`. | | `generate_audio` | boolean | No | `true` for a video with sound, `false` for a silent video. | | `seed` | integer | No | A whole number, 0 or more, that makes the render repeat. | | `creativity` | number | No | From 0 to 1: how far an upscaling model may depart from the source. | | `upscale_factor` | integer | No | 2 or more: how many times larger than the source the output is. | | `previous_job_id` | string | No | The id of an earlier completed video job of your workspace that this video continues. | | `callback_url` | string | No | An `https` address that the exchange calls when the job ends. See [Callback](#callback). | | `exchange` | object | No | Purchasing rules, such as `cap_usd`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). | Without `seconds`, the exchange uses the shortest length that an offer declares, or 4 if none does. The `seconds` field of the response states the length used. ## Errors | Error | Cause and fix | | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `400 unsupported_model` | Not a video model. Read `/v1/models` for `kind: video_generation`. | | `400 unsupported_parameter` with `param` `size`, `seconds`, `resolution` or `generate_audio` | No offer makes it. The message or `eligibility.excluded` lists what the offers make. | | `400 unsupported_parameter` with `param` `video.seed` and the like | No offer declared the control. Read `parameters` on the model card. | | `400 conflicting_parameters` with `param` `size` | `size` has another shape or short side than `aspect_ratio` or `resolution`. | | `400 invalid_request` with `param` `input_references` | The start image has the wrong shape or is unreadable. Crop, pad or change `size`. | | `400 no_eligible_offer` with `param` `input_references` | The image fits no default size. Send the `size` that the message names. | | `400` with `param` `previous_job_id` | The id is no video job of your workspace, or the job has not completed. | | `409 video_not_cancellable` | The job has ended and cannot be cancelled. | | `404` | The job belongs to another account. Use the key that made the job. | See [Errors](https://inference.ai.exchange/docs/errors.md) for each code and exclusion reason. ## Next steps - [Reference inputs](https://inference.ai.exchange/docs/reference-inputs.md): frames, guidance and voice samples. - [Image generation](https://inference.ai.exchange/docs/image-generation.md): the same job for images. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): set a cap and limits. # Reference inputs An image, speech, transcription or video job can start from reference inputs: images, audio, video and files. Send them in one list, `input_references`, with a `type` and an optional `role` for each entry. Each reference counts as input of the job. Send the list on `POST /v1/images/generations`, `POST /v1/images/edits`, `POST /v1/videos`, `POST /v1/audio/speech` and `POST /v1/audio/transcriptions`. For the names that older clients send, see [What changed in the API](https://inference.ai.exchange/docs/api-changes.md). ```bash curl https://router.inference.ai.exchange/v1/images/generations \ -H "Authorization: Bearer sk-liquid-..." \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-image-1", "prompt": "a logo in the same style", "input_references": [ { "type": "image", "url": "https://example.com/logo.png" }, { "type": "audio", "data": "", "media_type": "audio/wav" } ] }' ``` ## Entries | `type` | Fields | Notes | | ------- | ----------------------------------------- | --------------------------------------------------------------- | | `image` | `url` or `data`, `media_type` | An `https://` URL or a `data:image/` URL. | | `audio` | `url` or `data`, `media_type` | A `data:audio/` URL, or base64 bytes with `media_type`. | | `video` | `url` or `data`, `media_type` | An `https://` URL or a `data:video/` URL. | | `file` | `url` or `data`, `media_type`, `filename` | A `data:` URL, such as a PDF. | | `text` | `text` | The words of a voice sample. Needs the role `voice_transcript`. | - Send `url` or `data`, not both. Bare base64 in `data` needs `media_type`, such as `image/png`. - `media_type` always means a MIME type, such as `image/png`. - Send up to 16 entries in all, frames included. - A link to audio is allowed only on `/v1/audio/transcriptions`. On the other endpoints, send audio as a `data:audio/` URL or as base64. - An endpoint refuses a field that it does not read, with `400` and the name of the field. It never drops it. ## Roles `role` is optional. It is `reference` by default. | `role` | Entry type | Endpoint | Meaning | | ------------------ | ---------- | ----------------------- | --------------------------------------- | | `reference` | any | all of them | Guidance for the job. | | `first_frame` | `image` | `POST /v1/videos` | The first frame of the video. | | `last_frame` | `image` | `POST /v1/videos` | The last frame of the video. | | `voice_sample` | `audio` | `POST /v1/audio/speech` | A sample of the voice to speak in. | | `voice_transcript` | `text` | `POST /v1/audio/speech` | The words that the voice sample speaks. | An endpoint refuses a role that it does not read, with `400`. `POST /v1/audio/transcriptions` takes exactly one `audio` entry, or a `file` in a form. ## Frame images On `POST /v1/videos`, send an image with the role `first_frame` or `last_frame` to fix that frame of the video. ```json "input_references": [ { "type": "image", "url": "https://example.com/closed.png", "role": "first_frame" }, { "type": "image", "url": "https://example.com/open.png", "role": "last_frame" } ] ``` - Send each frame one time at most. Two entries with the same frame and different images get `400`. The same image twice is one frame. - Only an offer that declared the frame can win. If none did, the exchange refuses the request before the auction. `eligibility.excluded` names the frame, for example `image.supported_frame_images: last_frame`. - The start image must have the shape of the video. See Start image and frames in [Video generation](https://inference.ai.exchange/docs/video-generation.md). - Frame images ride the price of the video. They are not charged as reference images. ## Guidance inputs An entry with no role, or with the role `reference`, is guidance. ```json "input_references": [ { "type": "image", "url": "https://example.com/character.png" }, { "type": "video", "url": "https://example.com/motion.mp4" } ] ``` To continue or extend a finished video, do not send it as a reference. Send `previous_job_id`. See [Video generation](https://inference.ai.exchange/docs/video-generation.md#continue-a-video). ## Voice samples On `POST /v1/audio/speech`, send a sample of a voice and, if you have it, the words of the sample. ```json "input_references": [ { "type": "audio", "data": "", "media_type": "audio/wav", "role": "voice_sample" }, { "type": "text", "text": "The words of the sample.", "role": "voice_transcript" } ] ``` A voice sample belongs to one voice. Send it with a string or with one turn, not with many turns. ## Which offers can win Only an offer that declared each input kind can win. The kinds are `input.image`, `input.audio`, `input.video` and `input.file`. The exchange also checks the limits that the offer declared, such as `image.max_references` and `audio.max_bytes`. If no offer takes all of your references: - The exchange refuses the request before the auction with `400 unsupported_parameter`. `param` names the kind, such as `input.image`. - `eligibility.excluded` gives the reason for each offer. - The exchange never sends the request to a chat model. ## Charge - A reference image is charged for each image at the rate of the offer. Without a rate it is free. - A reference video and a reference audio are charged for each second, at most 30 seconds. A job with one goes only to an offer that prices it. - A frame image and the start image are not charged as reference images. See [Modalities](https://inference.ai.exchange/docs/modalities.md) for the cap. ## Edit images and video forms - `POST /v1/images/edits` is the same purchase for the OpenAI SDK. It needs at least one reference, takes files in `image`, and refuses a `mask`. - `POST /v1/videos` takes a start image as a file in a form, as the OpenAI SDK sends it. If no image offer reads your references, the error says so. The Playground then asks a chat model to describe the images, sends the description to the image model, and tells you that it did. The API does not do this. ## In the Playground The Image, Speech, Video and Compare prompt boxes take files. Drag a file onto the box, paste it, or use the paperclip button. ### What a box accepts - An image, speech or video job takes images, WAV, FLAC or MP3 audio, and PDF files as references. - A chat job takes the same files in the message. - A speech run with a transcription model takes one audio clip instead. - The Playground sends up to 8 files, with at most 8 MB for each file and 14 MB in total. - The Playground refuses a file that the exchange cannot read, such as a video, and it says what the job takes. ### Links in the prompt The Playground sends the prompt as text, so a link to an image in the prompt does not become a reference. If the prompt has a link to a PNG, JPEG, WebP or GIF file, the Playground tells you to download the image and attach it as a file. ### When no model reads a file If no model of a lane reads a file, the Playground marks the file and stops the run. Remove the file, or choose a model that reads it. The price estimate for an image counts only the offers that read your files. A speech or video job has no estimate. ### Video start frame In a video run, the first image is the start frame. The Size menu sets the size of the video. If the start frame does not have the shape of the video, the Playground marks it and stops the run. Select **Edit** on the image to crop it to the shape, or to keep the whole image with black bars. The editor gives a JPEG that is no larger than the size of the video. It works in your browser and sends the image nowhere. ### Model page Playground The Playground on the page of an image model has a **Reference images** field. Drop, paste or upload images, up to the limit that the offers of the model declare. The field is absent when no offer reads images. The form also shows each control that an offer declared: the number of images, format, quality, background, compression, seed and streaming. A control that no offer declared is not shown. The JSON view shows the request body. ### Saved files A run keeps its files in this browser. The run header lists them. Run again, Retry and a variation send them again. ## Next steps - [Image generation](https://inference.ai.exchange/docs/image-generation.md) - [Speech](https://inference.ai.exchange/docs/speech.md) - [Video generation](https://inference.ai.exchange/docs/video-generation.md) # Embeddings Buy vectors for text. The endpoint has the shape of the OpenAI embeddings API, so an unchanged OpenAI client works. You pay for the input tokens only, because an embedding job has no output price. ```bash curl https://router.inference.ai.exchange/v1/embeddings \ -H "Authorization: Bearer sk-liquid-..." \ -H "Content-Type: application/json" \ -d '{ "model": "text-embedding-3-small", "input": ["a cat on a bicycle", "a dog in a car"] }' ``` The response has the OpenAI shape. The vectors come back in the order of the texts, and each vector has an `index`. The exchange adds an `exchange` block that shows what the job bought and what it cost. ```json { "object": "list", "model": "text-embedding-3-small", "data": [ { "object": "embedding", "index": 0, "embedding": [0.0123, -0.0456] }, { "object": "embedding", "index": 1, "embedding": [0.0789, 0.0012] } ], "usage": { "prompt_tokens": 9, "total_tokens": 9 }, "exchange": { "...": "..." } } ``` Use a key with the `spend` scope. See [API keys](https://inference.ai.exchange/docs/keys.md). ## Examples **Search your documents.** Embed each document once and store the vectors. To search, embed the query with the same model, then return the documents whose vectors are nearest. ```python docs = ["Reset your password in Settings.", "Invoices are sent on the 1st."] vectors = client.embeddings.create(model="text-embedding-3-small", input=docs) query = client.embeddings.create(model="text-embedding-3-small", input=["how do I change my password"]) ``` **Find duplicates.** Embed a batch of texts in one request. Two texts whose vectors are very close say nearly the same thing. ```python batch = client.embeddings.create(model="text-embedding-3-small", input=tickets) vectors = [item.embedding for item in batch.data] # in the order of `tickets` ``` ## Use the OpenAI SDK ```python from openai import OpenAI client = OpenAI( base_url="https://router.inference.ai.exchange/v1", api_key="sk-liquid-...", ) result = client.embeddings.create( model="text-embedding-3-small", input=["a cat on a bicycle", "a dog in a car"], ) print(len(result.data), len(result.data[0].embedding)) ``` ```typescript import OpenAI from 'openai' const client = new OpenAI({ baseURL: 'https://router.inference.ai.exchange/v1', apiKey: process.env.LIQUID_API_KEY, }) const result = await client.embeddings.create({ model: 'text-embedding-3-small', input: ['a cat on a bicycle', 'a dog in a car'], }) console.log(result.data.length, result.data[0]?.embedding.length) ``` ## Find a model and its price The model list and the prices are public. You need no key to read them. ```bash curl 'https://router.inference.ai.exchange/v1/models?kind=embedding' ``` Each entry has the model `id` and `min_input_usd_per_mtok`, the lowest input price among the offers now on the book, in USD for each million tokens. To keep the models whose cheapest offer is at most USD 0.05, add `max_input_price`: `?kind=embedding&max_input_price=0.05`. To see how many independent providers serve a model, read `providers` in `GET /v1/exchange/models`. See [Market data](https://inference.ai.exchange/docs/market.md). You name the model to buy. The exchange does not choose an embedding model for you. The router models and [Auto Router](https://inference.ai.exchange/docs/auto.md) cannot choose one, because they rank by scores that were measured on text. A preset of the same kind also works. ## What a job costs The exchange auctions each request among the providers that serve the model that you named. The charge is the input rate of the offer that won, not `min_input_usd_per_mtok`. The receipt records the rate. A job runs in three steps: 1. The exchange estimates the tokens of your texts and fixes that figure at the input rate of the offer as the price cap, the highest charge for the job. 2. The exchange returns the cap in `x-liquid-cap-usd` and the job id in `x-liquid-job-id`. 3. The charge is the tokens that the provider reports. It never goes above the cap. The exchange fails the job, and you pay nothing, if the provider sends one of these: - a different number of vectors than texts, - vectors of different lengths, or - output tokens. `exchange.limits.price.input_usd_per_mtok` limits the input price. A rule that limits output price or speed in tokens returns a `400` error. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Keep one model for one index Vectors from different models cannot be compared. A search index must hold vectors from one model, at one vector length. If you change the model, embed the whole corpus again. The provider can change from one request to the next. Offers of one model can differ in precision, so their vectors can differ by a small amount. To keep an index stable: - Use `dimensions` only for a model that supports a shorter vector. - Use the same `dimensions` value for every request that feeds the index. ## Use a framework or a vector database These guides show the exchange as the embedding source: - [LangChain](https://inference.ai.exchange/docs/connect-langchain.md): `OpenAIEmbeddings`. Set `check_embedding_ctx_length=False`. - [LlamaIndex](https://inference.ai.exchange/docs/connect-llamaindex.md): `OpenAILikeEmbedding`. - [pgvector](https://inference.ai.exchange/docs/connect-pgvector.md): store and search vectors in Postgres. - [Qdrant](https://inference.ai.exchange/docs/connect-qdrant.md): store and search vectors in a Qdrant collection. - [Pinecone](https://inference.ai.exchange/docs/connect-pinecone.md): store and search vectors in a Pinecone index. ## Limits - There is no streaming form. The exchange sends the batch complete. - Embeddings do not use [response caching](https://inference.ai.exchange/docs/response-caching.md) or [prompt caching](https://inference.ai.exchange/docs/caching.md). ## Reference ### Request fields | Field | Description | | ----------------- | ---------------------------------------------------------------------------------------------- | | `model` | The deployment to buy from. Required. The exchange does not choose an embedding model for you. | | `input` | One string, or an array of up to 2,048 strings. Required. | | `dimensions` | The length of each vector, from 1 to 4,096. | | `encoding_format` | `float` (the default) or `base64`. | | `input_type` | What the vectors are for: `search_query`, `search_document`, `classification` or `clustering`. | | `exchange` | Your purchasing rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). | Each string can have up to 32,768 characters. All strings together can have up to 1,200,000 characters. The exchange does not support token arrays or multimodal objects. With `base64`, each vector is a string of little-endian 32-bit floats, as the OpenAI SDKs expect. ### Input type A model that embeds a query and a document in different ways reads `input_type`. Only an offer that declared `embedding.input_type` can win a request that has it. Without the field, any offer can win. The model card lists `input_type` in `parameters` when an offer declares it. ```json { "model": "", "input": "how do I cancel a job?", "input_type": "search_query" } ``` A value outside the four gets `400 invalid_request`. If no offer declared `embedding.input_type`, the answer is `400 unsupported_parameter` and `param` is `embedding.input_type`. Multimodal input items are not supported yet. ### Vector length A deployment that shortens its vectors serves a request up to its own length. Any other deployment serves only its own length. A request for a length that the deployment cannot serve goes to a different offer. It does not fail after the choice. ### Job kinds A chat deployment never wins an embedding job. An embedding deployment never wins a chat job. The exclusion reason is `job_kind_not_served`. ## Next steps - [Modalities](https://inference.ai.exchange/docs/modalities.md): images, speech and video. - [Embeddings API reference](https://inference.ai.exchange/docs/api-embeddings.md): the full request and response schema. # Structured output Structured output makes the model answer with JSON. Send an answer format with the request, as in the example below. The exchange sends the request only to offers that support the format. When the job ends, it checks the answer against the format and reports the verdict. ## Send an answer format Send `response_format` on `POST /v1/chat/completions`: ```bash curl https://router.inference.ai.exchange/v1/chat/completions \ -H "Authorization: Bearer $LIQUID_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-oss-120b", "messages": [{"role": "user", "content": "Extract the invoice: ACME, due 2026-11-01, 1200 USD"}], "response_format": { "type": "json_schema", "json_schema": { "name": "invoice", "strict": true, "schema": { "type": "object", "properties": { "vendor": {"type": "string"}, "due": {"type": "string"}, "amount": {"type": "number"} }, "required": ["vendor", "due", "amount"], "additionalProperties": false } } } }' ``` | Endpoint | Field | | --------------------------- | ---------------------- | | `POST /v1/chat/completions` | `response_format` | | `POST /v1/messages` | `output_config.format` | | `POST /v1/responses` | `text.format` | | `type` | The answer must be | | ------------- | ---------------------------------------- | | `json_object` | One JSON value. | | `json_schema` | One JSON value that matches your schema. | The schema name must use 1 to 64 letters, digits, `_` or `-`. Only an offer that declares support for the format can win the job. If no offer does, the call gets `no_eligible_offer`. A dry run names `feature_unsupported` for each offer that it leaves out. ## Read the verdict The exchange checks the text of the answer when the job ends. The verdict is in `exchange.answer_checks.structured_output`, on the last chunk of a stream. The results, such as `passed`, `not_json` and `schema_mismatch`, are in [Errors](https://inference.ai.exchange/docs/errors.md#structured-output). What happens after a failed check depends on streaming: - **No stream.** If the answer is `not_json` or `schema_mismatch`, the exchange drops it and sends the job to the next offer. - **Stream.** A failed check does not change the job. You receive the answer and you pay for it. The exchange records the verdict for the deployment that served the job. A deployment that breaks the format often gets a higher bad-answer rate. The exchange ranks a request with a format by that rate, unless the request sets `exchange.rank.failure_risk_ranking` to `false`. See [Fewest redos](https://inference.ai.exchange/docs/rules.md#fewest-redos). ## Whitespace without end Some models write whitespace until `max_tokens` under a schema, for example after a key with a boolean value. The exchange stops an answer that writes more than 1,024 whitespace characters in a row outside a JSON string. The failure counts against the deployment. - **No stream.** The job goes to the next offer. - **Stream.** The job fails with `provider_failed`. You pay nothing, and you keep the text that you already received. ## Try a format in the playground The playground sends the format, shows the verdict and lists each problem, so you can fix a schema before you use it in code. 1. Open the **Request** tab of a chat. Find **Answer format**. 2. Choose **Text**, **JSON object** or **JSON schema**. 3. For **JSON schema**, give the schema a name. Build the schema as a list of fields, or choose **Edit as JSON** and write it. To start from a sample, choose **Infer from an example** and paste one answer. 4. Turn **Strict** on to require every field. An optional field then accepts `null`. 5. Send the message. A schema opens as fields only when the field list can write back the same schema. Otherwise it stays as JSON, and a note names each part that the list cannot show. The answer shows as a record with a verdict. **Fields** shows each field and **Raw** shows the text. If the answer has a schema, the record also copies that schema, so you can paste it into a new request even after you change the answer format. The playground also checks the answer in the browser against your schema. It lists every field that is wrong, with its path. The exchange verdict decides. The browser list only explains it. If the two differ, a note says so. If a seller sent an answer that is not JSON or does not match the schema, the playground shows the bad-answer rate of that seller. You can send the question again without that seller. The format applies to chat jobs only. If Auto turns a chat into another kind of job, the playground drops the format. A [preset](https://inference.ai.exchange/docs/rules.md) does not hold an answer format. A [workload](https://inference.ai.exchange/docs/workloads.md) does. ## Next steps - [Workloads](https://inference.ai.exchange/docs/workloads.md): run the same prompt many times against each provider. - [Errors](https://inference.ai.exchange/docs/errors.md#structured-output): every result of the check. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): rank by the chance of a good answer. # Prompt caching Prompt caching makes a repeated prompt start cheaper. A multi-turn chat or an agent loop gets the saving with no setup. Many prompts start the same way: a system prompt, tool definitions or a long document. A provider can store that start, called the prefix. A later request with the same prefix reads it from the cache, and the provider charges a fraction of the input rate for those tokens. The exchange makes a cache plan for each job and sends it to the winning provider. To store a whole answer at the exchange and skip the provider, see [Response caching](https://inference.ai.exchange/docs/response-caching.md). ## What a cache costs - A **read** costs less than input, typically 0.1 to 0.5 times the input rate. - A **write** stores a prefix. A five-minute write typically costs 1 to 1.25 times the input rate. A one-hour write costs about 2 times. Many providers write for free. The first request that sends a prefix pays for a write. Each later request that reuses the prefix pays for a read. The lifetime of a write is its time-to-live (TTL). After the TTL ends, the next request writes again. Example: input at USD 3.00 for each million tokens, a read at 0.1 times, a five-minute write at 1.25 times, and a 10,000-token prefix. The rates are an illustration. | Request | Without a cache | With a cache | | --------------- | --------------- | ------------ | | 1 (write) | USD 0.0300 | USD 0.0375 | | 2 (read) | USD 0.0300 | USD 0.0030 | | 3 (read) | USD 0.0300 | USD 0.0030 | | Total for three | USD 0.0900 | USD 0.0435 | The write pays back at the first read. If no later request reads the prefix, the write is a loss, so `auto` does not cache a one-off request. You pay the rates of the winning offer. The order book shows the cache-read and cache-write rates of each offer. A provider that quotes no cache-read rate gets no cache saving in the ranking. ## Check what a job saved Each response has an `exchange` block. Its `charge` object has three cache fields: - `total_usd`: what you paid. - `cache_usd`: the part of the charge, fee included, for cache reads and writes. - `uncached_usd`: what you would have paid with no cache, at the same rates. `cache_usd` and `uncached_usd` are present only when some input used a cache, and no money moves on them. A job that hit its price cap has no `cache_usd`. If `uncached_usd` is smaller than `total_usd`, a write was never read. The price cap includes cache writes up to the last breakpoint. It does not include an assumed saving, and a write cannot charge more than the cap. The `usage` block reports cached input separately from fresh input: | API | Cache read | Cache write | | ---------------- | ------------------------- | ----------------------------- | | Chat Completions | `cached_tokens` | `cache_write_tokens` | | Messages | `cache_read_input_tokens` | `cache_creation_input_tokens` | On Chat Completions both are inside `prompt_tokens_details`, and `prompt_tokens` counts them. On Messages, `input_tokens` excludes them: add the three for the total input. The receipt of the job shows the cache tokens and the charge. If an offer quotes no write rate, the exchange bills written tokens as uncached input. ### If the second request shows no read Check these causes: - The prefix changed before the breakpoint, even by one character. - The TTL ended between the two requests. - The prompt is shorter than the minimum of the upstream. See [What each upstream caches](#what-each-upstream-caches). - Another provider won the second request. See [Which provider serves the next turn](#which-provider-serves-the-next-turn). ## Choose how to cache Set `exchange.cache.mode` in the request body to choose who places the breakpoints. A breakpoint is a point in the prompt. The provider caches everything before it. | Value | Who places the breakpoints | | ---------------- | ----------------------------------------------------------------- | | `auto` (default) | Your `cache_control` markers, or the exchange when you sent none. | | `buyer` | Only your own `cache_control` markers. | | `off` | No one. The exchange sends no breakpoints and no cache key. | ### Let the exchange cache (`auto`) If the request declares tools or continues a conversation, the exchange places breakpoints after the tool definitions, after the system prompt, after the previous request, and after the last message. The next request reads the last one. It places breakpoints only if the estimated prompt is 512 tokens or more, the smallest minimum of the Anthropic models. It does not cache a one-off request. Its breakpoints always use the five-minute TTL. Use your own markers to ask for one hour. ### Place your own breakpoints (`auto` or `buyer`) Add `cache_control` to the last block of the prefix to cache. Under `auto`, a request with any marker keeps your markers and gets none from the exchange. Under `buyer`, the exchange never places its own. | Marker | TTL | Meter | | ------------------------------------ | --------- | ------------------- | | `{"type": "ephemeral"}` | 5 minutes | `cache_write_short` | | `{"type": "ephemeral", "ttl": "1h"}` | 1 hour | `cache_write_long` | Any `ttl` other than `"1h"` is five minutes. Use the short write for requests within five minutes of each other, such as one agent loop. Use the long write when requests are further apart. A long write pays back only if the prefix is read after the short TTL would have ended. **Messages API** (Anthropic format). Mark a block of `system`, a tool, or a block inside a turn: ```python import os from anthropic import Anthropic client = Anthropic( base_url="https://router.inference.ai.exchange", api_key=os.environ["LIQUID_API_KEY"], ) response = client.messages.create( model="liquid.auto", max_tokens=1024, system=[ {"type": "text", "text": "You are terse."}, { "type": "text", "text": long_reference_document, "cache_control": {"type": "ephemeral", "ttl": "1h"}, # long write }, ], messages=[{"role": "user", "content": "What changed in section 4?"}], ) print(response.usage) ``` **Chat Completions.** Mark a content part or a tool. A marker needs the array form of `content`: ```python response = client.chat.completions.create( model="liquid.auto", messages=[ { "role": "system", "content": [ { "type": "text", "text": long_reference_document, "cache_control": {"type": "ephemeral"}, # short write } ], }, {"role": "user", "content": "What changed in section 4?"}, ], ) ``` Rules for markers: - A request can have four breakpoints at most. The exchange drops the shallowest extra ones. - Anthropic refuses a one-hour breakpoint after a five-minute one, so each breakpoint before a one-hour breakpoint also becomes one hour. - A marker on a tool puts the breakpoint after the last tool. - A top-level `cache_control` object puts one breakpoint after the last message, in both APIs. A value that is not an object places nothing: ```python response = client.chat.completions.create( model="liquid.auto", messages=messages, extra_body={"cache_control": {"type": "ephemeral"}}, ) ``` ### Switch off the cache (`off`) `off` drops your own markers and the cache key. It cannot stop an upstream that caches long prompts itself. ```python response = client.chat.completions.create( model="liquid.auto", messages=[{"role": "user", "content": "hello"}], extra_body={"exchange": {"cache": {"mode": "off"}}}, ) ``` ## Keep a conversation on one cache ### Name the conversation Send `session_id` in the request body, or the `x-session-id` header, with the same value on each turn. The body field wins. The value can have 256 characters at most. The exchange picks the cache key in this order: `session_id`, `prompt_cache_key`, then a digest of the first block of the prompt. Some providers, such as OpenAI, route by this key. The exchange salts the key for each buyer, so it never joins the requests of two buyers. With `exchange.cache.mode: "off"`, the exchange ignores `session_id`. ```python response = client.chat.completions.create( model="liquid.auto", messages=messages, extra_body={"session_id": "support-chat-4821"}, ) ``` ### Which provider serves the next turn A cache lives at one provider. The exchange remembers which provider served the last turn of a session, for as long as the longest cache that the turn wrote. Each new turn renews the record. For that provider, the ranking estimate assumes a cache read for part of the prompt, but only if the provider publishes a cache-read rate. - The assumed share is the measured hit rate of the deployment over 30 days of settled jobs, or one half with too little history. - To set the share yourself, use `exchange.cache.expected_hit_rate` in [Purchasing rules](https://inference.ai.exchange/docs/rules.md). When the cache saves anything, the exchange also ranks that provider 5% below its own estimate. So the provider that served the last turn wins unless another offer is cheaper by more than the cache saving plus that margin. The price cap does not include the assumed saving. Under Auto, a session can also stay on the previous deployment. See [Auto Router](https://inference.ai.exchange/docs/auto.md). A new conversation can also find a warm cache. The exchange remembers, for each buyer, which provider served each prefix, for the same time as a session. This works without a `session_id`, and the exchange can lose a record early. ### Keep a session on one deployment Set `exchange.session.sticky: true` in the `exchange` block or in a preset. The deployment that served the previous turn then ranks first while it is eligible: it passes your rules and its price cap fits the cap of the call. Otherwise the exchange ranks as usual. A fallback to the next offer happens only before any output. ```python response = client.chat.completions.create( model="liquid.auto", messages=messages, extra_body={"session_id": "support-chat-4821", "exchange": {"session": {"sticky": True}}}, ) ``` `exchange.session.sticky` is not the Auto session pin of Auto versions 5 and 6. Set `exchange.session.pin: "off"` to turn the pin off, or `exchange.session.margin` to change the score points that a rival must add. See [Session pin](https://inference.ai.exchange/docs/auto.md). ## Privacy between buyers Only you can read your cached prompts. The exchange puts a code that is unique to your account before your system prompt and messages in each cacheable prompt, so a shared upstream account never shares a prefix between buyers. One exception: some upstreams cache the tool definitions before the system prompt, so buyers can share the cache for an identical list of tools. ## Reference ### The meters The exchange meters the input of a job in four parts and the output as a fifth. Each part has its own rate on the offer. | Meter | What it counts | Typical rate | | ------------------- | -------------------------------------------- | -------------------------------- | | `uncached_input` | Input that the cache did not read or write. | The input rate. | | `cache_read` | Input that the provider read from its cache. | 0.1 to 0.5 times the input rate. | | `cache_write_short` | Input stored in the cache for five minutes. | 1 to 1.25 times the input rate. | | `cache_write_long` | Input stored in the cache for one hour. | About 2 times the input rate. | | `generation` | Output tokens. Caching has no effect. | The output rate. | The public offers in [Market data](https://inference.ai.exchange/docs/market.md) carry the rates in the `cache_read`, `cache_write_short` and `cache_write_long` lines of `pricing`. A line is absent when the offer quotes no such price. ### What each upstream caches The exchange sends the same cache plan to each provider, in the form that its upstream reads. The upstream decides what it caches and what it charges. The multipliers below are list prices of the upstream, and an offer can quote other rates. A prompt shorter than the minimum of the upstream is never cached and is billed as uncached input. #### Anthropic - You choose where the cache starts, with `cache_control` markers or exchange breakpoints. - A five-minute write costs 1.25 times the input rate, a one-hour write 2 times, and a read 0.1 times (less on some newer models). - The minimum cacheable prompt depends on the model, from 512 to 4,096 tokens. A prompt below it is processed without a cache and with no error, also with a marker. See the Anthropic documentation for the minimum of each model. - The cache checks at most 20 blocks back from each breakpoint. In a long turn, place a new breakpoint. - A change to the tool definitions invalidates the tools, the system prompt and the messages. A change to the system prompt invalidates the system prompt and the messages. #### OpenAI - OpenAI caches a long prefix by itself. On GPT-5.6 and later, the minimum is 1,024 visible input tokens. - On GPT-5.6 and later, a write costs 1.25 times the input rate and a read 0.1 times. Earlier models have no write charge. - The exchange bills written tokens at the write rate of the offer only when the request has a cache breakpoint. Otherwise it bills them as uncached input. - The exchange sends `prompt_cache_key`, and OpenAI routes requests with the same key to the same cache. Keep the start of the prompt the same and put changing text at the end. #### Other upstreams Groq, Grok, Moonshot AI, DeepSeek, Z.AI, Google Gemini and others cache a repeated prefix by themselves. The exchange sends the same body as for OpenAI, and the upstream can ignore the key. - A read costs less than input. A write is usually free, and the exchange then bills written tokens as uncached input. - DeepSeek reports cached input as `prompt_cache_hit_tokens`. The exchange bills these tokens at the cache-read rate of the offer. - Alibaba caches only at `cache_control` markers, with a five-minute TTL, and charges for the write. - The exchange does not create explicit Gemini caches. Put stable text first. These rules are a guide. The `usage` block shows what the provider reported, and the receipt shows what you paid. ## Next steps - [Response caching](https://inference.ai.exchange/docs/response-caching.md): store a whole answer and get it back at no charge. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): the `exchange.cache.mode` setting and `exchange.session.sticky`. # Response caching Response caching stores the whole answer at the exchange. An identical request later gets that answer back, and no provider runs. A hit costs nothing. It is off by default. Send `x-liquid-cache: true` to use it for one request. It works on Chat Completions, Messages and Responses, with and without streaming. ```bash curl https://router.inference.ai.exchange/v1/chat/completions \ -H "Authorization: Bearer sk-liquid-..." \ -H "x-liquid-cache: true" \ -H "x-liquid-cache-ttl: 3600" \ -H "Content-Type: application/json" \ -d '{"model": "gpt-oss-120b", "messages": [{"role": "user", "content": "Name three rivers."}]}' ``` The first request returns `x-liquid-cache-status: MISS` and runs a job that you pay for. The same request within the hour returns `HIT` and costs nothing. In Python, read the status from the raw response: ```python import os from openai import OpenAI client = OpenAI( base_url="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], ) raw = client.chat.completions.with_raw_response.create( model="gpt-oss-120b", messages=[{"role": "user", "content": "Name three rivers."}], extra_headers={"x-liquid-cache": "true", "x-liquid-cache-ttl": "3600"}, ) print(raw.headers["x-liquid-cache-status"]) # MISS, then HIT completion = raw.parse() ``` ## Before you turn it on - **A shared key shares the cache.** An entry belongs to one API key. If your application uses one key for many end users, an end user can get the stored answer of another end user who sent the same request. Do not turn the cache on for a request that holds private data. - **A hit repeats the answer.** The content is the same each time, so a change of `temperature` does not produce a new answer. To get a new one, send `x-liquid-cache-clear: true`. - **A chat rarely hits.** The whole message history must be the same. A second "Hi" in the same chat has a longer history, so it misses. The same "Hi" in a new chat hits. For a long chat, use [prompt caching](https://inference.ai.exchange/docs/caching.md) instead. | | Prompt caching | Response caching | | -------------------------- | ------------------------------------------- | --------------------------------- | | What it stores | The start of a long prompt, at the provider | The whole answer, at the exchange | | What a repeat request does | Runs on a provider, with cheaper input | Runs on no provider | | Charge | Cached input at a lower rate | Nothing | | Default | On, with `exchange.cache.mode: "auto"` | Off | ## What a hit returns A hit returns the stored content again, with a new `id` and a new `created` time. Every `usage` counter is `0`, and the charge is `0`. The `exchange` block holds a `cache` object in place of the receipt of a job, because no job was bought. A hit has no `x-liquid-job-id` header, and it moves no money. To delete the entry and store a new answer, send `x-liquid-cache-clear`: ```python client.chat.completions.create( model="gpt-oss-120b", messages=[{"role": "user", "content": "Name three rivers."}], extra_headers={"x-liquid-cache": "true", "x-liquid-cache-clear": "true"}, ) ``` The request then runs, and you pay for it. ## What decides a hit A request hits an entry when all of these are the same: - The API key. Two keys never share an entry, also inside one workspace. - The endpoint. - The `stream` setting. A streamed answer and a buffered answer have separate entries. - The request body. The fields can be in any order, and white space does not count. The whole message history counts. The top-level fields `prompt_cache_key` and `session_id` do not count. They only name a conversation for the prompt cache of the provider. Headers do not decide a hit. A purchasing rule that you send as a header, such as `x-liquid-cap-usd`, does not change which entry the request finds. ## Limits - Only a finished answer is stored. A refusal, an error, a failed job and a stream that did not end are never stored. - The exchange can drop an entry before its TTL ends. A request can then miss. - Two identical requests that arrive together both miss, and both are bought. The exchange does not join them. - An answer larger than 4 MiB is returned but not stored. - Images, speech, video and embeddings do not use the cache. ## Reference ### Request headers | Request header | Behaviour | | ---------------------- | ---------------------------------------------------------------------------------------- | | `x-liquid-cache` | `true` turns the cache on for this request. `false` or no header leaves it off. | | `x-liquid-cache-ttl` | Seconds to keep a new entry, from 1 to 86400. The default is 300. | | `x-liquid-cache-clear` | `true` deletes the entry for this request. The request then runs and stores a new entry. | A header that cannot be read returns `400 invalid_header`. This includes a TTL outside 1 to 86400. ### Rules To use the cache for every request of a preset, set `exchange.cache.response: true` in the rules of the preset. The rules `exchange.cache.response` and `exchange.cache.response_ttl_seconds` can also appear in the `exchange` block of a request. They win over the headers. Thus `response_cache: false` in the request turns the cache off, also if you send `x-liquid-cache: true`. A `x-liquid-cache-ttl` header wins over `exchange.cache.response_ttl_seconds`. A rule TTL outside 1 to 86400 is moved into that range. ### Response headers | Response header | Value | | ------------------------------ | --------------------------------------------------------------- | | `x-liquid-cache-status` | `HIT` or `MISS`. Absent when the request did not use the cache. | | `x-liquid-cache-ttl` | The full TTL on a miss. The remaining TTL on a hit. | | `x-liquid-cache-age` | On a hit, the seconds since the entry was stored. | | `x-liquid-cache-source-job-id` | On a hit, the job that produced the stored answer. | ## Next steps - [Prompt caching](https://inference.ai.exchange/docs/caching.md): cache the start of a long prompt. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): `exchange.cache.response` and `exchange.cache.response_ttl_seconds` in a preset. # Workloads A workload finds the cheapest model that does your task right. You give it the task and some cases with the answers that you expect. A run sends each case to up to four lanes, scores each answer field by field, and ranks the lanes by right answers and cost. A run never spends more than its limit. The default limit is $0.10. ## An example A team pulls four fields from each invoice email and needs 85% of the records right. It runs 120 cases on four lanes: | Lane | Records right | $ / 1k right records | | ----------------------------- | ------------- | -------------------- | | `liquid.auto` | 93% | $0.37 | | qwen3-32b | 88% | $0.24 | | deepseek-v4.1-flash on `pera` | 71% | $0.36 | | `acme.cheap-json` | 52% | $0.17 | Auto gets the most records right, but qwen3-32b is also above the 85% floor and costs 35% less. The page names it as the cheapest lane at or above the floor. The numbers are an example. ## When to use a workload Use a workload for a task with one right answer, such as pulling fields from a document, labelling a ticket, or changing free text into a fixed JSON shape. Run it again when you change the prompt or a new model comes to the exchange. ## Make your first workload ### 1. Write the task 1. In the console, open **Workloads** and choose **New workload**. 2. Give the workload a name. 3. Write the system prompt and the user message. The user message must hold `{{ case.input }}`. 4. Choose the model and the [answer format](https://inference.ai.exchange/docs/structured-output.md). 5. Choose **Save as v1**. Or choose **Save as workload** in the playground. The last question becomes case 1. ### 2. Add cases Choose **New case**, or **Import** a CSV or JSONL file. Start with 10 to 30 cases. More cases make each score range smaller. See [Cases](#cases). ### 3. Run the lanes A lane is one way to buy the answer: a provider of the model, another model, a preset or Auto. In the **Runs** tab, choose up to four lanes and the cases to send, then choose **Run**. See [Lanes and takes](#lanes-and-takes). ### 4. Ship and watch On the row of the lane that you want, choose **Ship**. Then choose **Export as code** and copy the code into your application. The **Production** tab shows its calls. See [Watch the calls in production](#watch-the-calls-in-production). ## Read the result A take is one answer of one lane to one case. A take is **judged** when its case has an expected answer and the exchange checked the answer against the format. Only judged takes count. A record is **right** when every field is right. **Lanes** ranks the lanes of the newest run. The lane with the most right records is first. Among equal lanes, the lowest cost for 1,000 right records wins. A lane with no judged take goes last. Each lane shows: - **Records right**, with a 95% confidence range. - **Fields right**, and the share of answers that matched the schema. - The cost of 1,000 takes and of 1,000 right records. - The providers and models that served its takes. The **Empty answer** row is the score of a model that answers nothing. A lane that does not beat it adds nothing. With a [floor](#scoring), a line above the lanes names the cheapest lane at or above it. Other views: **Fields** (share right for each field and lane), **History** (the runs), and **Wrong answers by field**. In the run grid, select a square to see the answer, the expected answer and the receipt. The page shows an answer only for a judged take that is wrong. ## Reference ### The task and its versions The task has a system prompt, a user message, an answer format, a model, the temperature, the output limit and the reasoning effort. If the task has no answer format and some cases have an expected answer, choose **Draft from expected answers**. A save that changes the task makes a new version, such as **Save as v2**. A save that changes only the name does not. Each version has a profile `.@v`. Each job of a run carries it as `exchange.rank.workload_profile`, and an offer can target it. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). These saves fail: | Response | Cause | | ------------------------------ | ---------------------------------------------------------------------------------- | | `409 slug_matches_key_profile` | A key of the workspace uses the workload name as the start of its default profile. | | `400 invalid_request` | The profile starts with the slug of another workspace. | ### Cases Select a case to edit it. The expected answer has one field for each field of the answer format, and a blank field is `null`. A change to the cases saves the next **case set** and makes no version. ### Import cases 1. In the Cases column, choose **Import**. 2. Choose a CSV or JSONL file of 25 MB or less. 3. Choose the input column, and the expected answer column or **None**. 4. For more than 200 rows, you can take a sample of 200 or 500 rows. The same file gives the same sample. 5. Choose **Keep** to add to the cases, or **Replace** to remove them first. Then upload. With an answer format, an expected answer must be JSON or a Python literal, such as `{'name': None}`. Otherwise it is the text of the cell. The page counts each row before the upload: | Count | Meaning | | --------------------------- | ---------------------------------------------------------------- | | No expected answer | A run cannot score the case. | | All null | The expected answer holds only `null`. Finding nothing is right. | | Left out: duplicate | The input is in the workload already, or earlier in the file. | | Left out: breaks the format | The expected answer does not fit the answer format. | | Left out: unreadable | The input is blank or over 32 KiB, or the answer cannot be read. | The page uploads in batches, and each batch saves a case set. If the upload stops, the earlier batches stay in. ### Lanes and takes Add a provider of the model, or choose **+ Preset, model or Auto**. Each take sends: | Lane | The take sends | | -------- | ------------------------------------------------------------------------------------- | | Provider | The model of the workload, and the provider in `exchange.candidates.providers.allow`. | | Model | The name of the model as `model`. | | Preset | The name of the preset as `model`. | | Auto | The Auto name as `model`, with a price cap of $0.05 for each take. | Auto refuses a request with no price bound, so the page adds the cap. See [Auto](https://inference.ai.exchange/docs/auto.md). A run uses the newest version and the newest case set, named in the **Run task** line. You also choose: - The cases to send: 10, 30, 100, 300 or **All**, picked at random. When some cases have an expected answer, only those are sent. - The takes of each case on each lane: 1, 3 or 5. - The run limit. The default is $0.10. Each take buys a new answer, because the response cache is off. **Run** is off when **Setup** has unsaved changes, the workload has no case, or a lane has no dry-run price yet. ### Spend A take starts only if the spend so far, the price caps of the takes in flight and its own price cap fit in the run limit. A take that does not fit waits and costs nothing. If you stop the run or reload the page, the waiting takes do not start. ### How a take is scored When a take ends, the page sends its job id and answer to the exchange. The exchange reads the receipt, scores the answer and keeps the result. It accepts only a job of your workspace that ran this version of this workload. If you close the page before it sends a take, that take is not scored. The line above the results counts these takes. ### Scoring The exchange compares each answer with the expected answer, field by field. A missing field is the same as `null`. Each field has a rule: | Rule | A field is right when | | ----------------- | ------------------------------------------------------------------------------ | | Exact | It has the same value. This is the default. | | Same text | It has the same text, in any case, with spaces and edge punctuation set aside. | | Same set | It is a list with the same members, in any order. | | Same set of texts | It is a list with the same members, in any order, compared as texts. | Select **An empty list and null are the same answer** to count `[]` and `null` as equal. The **Floor** is the share of right records that a lane needs to be worth shipping. A change to the rules or the floor makes no version. To apply it to a run, score the run again. ### Fix an expected answer Choose **Fix expected** and correct the case. The fix saves the next case set. Choose **Score on set** to score the run again. A case whose input changed keeps its old score. ### Ship a lane Choose **Ship** on the row of a lane, or **Ship v2 on this lane** in the line that names the cheapest lane. Ship records the version and the lane, and shows them in the **Ships** column. It makes no version and changes no offer. ### Watch the calls in production The **Production** tab shows the calls that your code sends with a profile of the workload. 1. Choose **Export as code**, which names the newest version, such as **Export v2 as code**. 2. Copy the code in Python, TypeScript or curl, and send calls from your code. The exchange awards and charges each call as any other call. 3. Read the result in the tab. The code holds the prompts, the answer format and the settings of the newest version, a place for the input of each call, and `exchange.rank.workload_profile`. If you shipped a lane, it sends that lane as the `model`, with `exchange.candidates.providers.allow` or a price cap. When the shipped version is older, the tab says so. The exchange compares each call that names a profile of the workload with the version, and marks the receipt. See [Task match](https://inference.ai.exchange/docs/workloads-api.md#task-match). Choose a window of 1, 7 or 30 days. The tab shows: - **Calls**, **Repeat the task** (the share of marked calls that match the version), **Schema pass** and **Spend**. - **By key** and **Served by**. - **Recent calls**: the 20 newest, with version, mark, status, provider, charge and receipt. The **Production 7d** column of the workload list shows the calls of the last 7 days. ### What the exchange stores A workload stores your prompt, the input and expected answer of each case, and the answer of each take. It is the only place where the exchange keeps request text. When you delete a workload, the exchange deletes all its versions, cases and runs. See [Data handling](https://inference.ai.exchange/docs/data-handling.md). ### Limits 50 workloads in a workspace. 5,000 cases in a workload. 32 KiB for one input. 25 MB for an import file. 4 lanes and 2,000 takes in a run. 8 takes run at one time. ### Use a workload from the API Send the profile of a version as `exchange.rank.workload_profile` on a request. See [Workloads in the API](https://inference.ai.exchange/docs/workloads-api.md). To rank offers by the chance of a good answer, see [Fewest redos](https://inference.ai.exchange/docs/rules.md#fewest-redos). The routes under `/v1/workspaces/{slug}/workloads` belong to the console and are not public API. # Request fixes When a request fails in the console, the playground can suggest one change and send the request again. The suggestion works in chat and in Studio. The change fits the reason for the failure, and it uses real numbers from the market. For example, if no offer could serve a request under your price cap, the playground suggests the cap that the cheapest offer needs. You decide what happens. Nothing changes until you accept, unless you set a kind to apply automatically. ## What you see - **The first failure of a kind** opens a dialog. It names the change, the choices and what each one costs. Choose a change, and choose whether it applies to this message or to the whole chat. The playground then sends the request again. - **The same failure again** shows a slim bar above the composer. Dismiss the bar and the playground stays quiet about that kind for the rest of the chat. - **Three failures in a row** show one panel with a way out: **Let Auto Router choose**, or **Reset rules to defaults**. The reset keeps your cap. Suggestions then pause for 10 minutes. Choose **Resume** to turn them on again. A failure that has no fix shows only the error. See [Errors](https://inference.ai.exchange/docs/errors.md). ## What it can suggest | Kind | The suggestion | | ------------------- | ------------------------------------------------------ | | Zero data retention | Allow providers that may keep a copy of the prompt. | | Region | Allow providers outside your regions. | | Spend cap | Raise the most that one request can cost. | | Low balance | Add funds. | | Maximum price | Raise your maximum price. | | Pinned provider | Let any provider answer. | | Provider failed | Send again without the provider that failed. | | Context size | Switch to a model with more room. | | Attachment not read | Switch provider or model, when a file is not read. | | Unsupported option | Switch model, or drop an option that no offer accepts. | ## Choose how each kind behaves 1. Open your avatar menu. 2. Select **Request fixes**. 3. Turn **Suggest a fix after a failed request** on or off. When it is off, you see only the error message. 4. For each kind, choose **Ask**, **Apply automatically** or **Never**. The default is **Ask**. In the dialog, **Don't ask again** sets a kind to **Never**. Zero data retention and Region widen who can see your data. They never apply automatically until you confirm that in the settings. An automatic retry leaves a line in the chat that says what changed. The settings are stored in this browser only. They do not follow you to another browser, and they do not change the rules of your API keys or [guardrails](https://inference.ai.exchange/docs/guardrails.md). A fix changes only the request that the playground sends. ## Next steps - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): the rules that a fix changes. - [Guardrails](https://inference.ai.exchange/docs/guardrails.md): the limits that a fix cannot widen. - [Errors](https://inference.ai.exchange/docs/errors.md) # Regional availability Liquid Inference follows US sanctions and the US rules that protect personal data. Thus some locations cannot use the exchange. These rules apply to your current location and to the people and companies that you act for. ## Buyers You cannot open an account, or use an account, from these locations: - Cuba, Iran, North Korea and Syria. - The Crimea, Sevastopol, Donetsk and Luhansk regions of Ukraine. - China and Russia. The exchange cannot meet the data transfer laws of these countries. ## Providers The exchange does not accept a provider that is located in, or controlled from, these locations: - Cuba, Iran, North Korea and Syria. - The Crimea, Sevastopol, Donetsk and Luhansk regions of Ukraine. - China, including Hong Kong and Macau. - Russia and Venezuela. Do not connect a provider through a VPN, a proxy or Tor. When you register a provider, you specify the country where it's headquartered, its ultimate owner and the ultimate owner's country. See [Become a provider](https://inference.ai.exchange/docs/provider.md). ## Error types The exchange refuses a request from a restricted location with HTTP `451` and the code `jurisdiction_restricted`. In the Anthropic format, the error type is `permission_error`. See [Errors](https://inference.ai.exchange/docs/errors.md). # Data handling The exchange does not store the text of your requests or answers. It stores a prompt and its inputs only when you save them in a workload. This page lists what it stores, where text is held for a short time, and who can see it. ## Keep a request private To tell providers not to keep your prompt, set `exchange.privacy.retention` to `none`. The exchange then offers the request only to providers whose offers promise not to retain it. ```json { "model": "gpt-oss-120b", "messages": [{ "role": "user", "content": "Summarize this contract." }], "exchange": { "privacy": { "retention": "none" } } } ``` A client that cannot send the `exchange` block can send the header `x-liquid-retention: none`. To set the rule for a whole workspace, choose **Zero data retention only** in a [guardrail](https://inference.ai.exchange/docs/guardrails.md). A provider makes the `none` promise itself, and the exchange does not check it. For a promise that the exchange checked, require the `zero_retention` claim as well: ```json { "exchange": { "privacy": { "retention": "none", "requirements": { "require": ["zero_retention", "no_training"], "checked_only": true } } } } ``` See [Provider claims](https://inference.ai.exchange/docs/provider-claims.md) for what "checked" means. ## What the exchange stores For each job, the exchange stores the facts that it needs to charge you and to show you why it chose an offer. It does not store the prompt or the answer. | Record | What it holds | How long | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | Job | The job id, workspace, API key, model name, status, cost tag and times. For the first turn of a chat, hashes of the system messages, the answer format and the settings. | No deletion date. | | Decision | The rules that applied, the offers that it ranked, the reason for each exclusion, the price cap, and hashes of the prompt start. | No deletion date. | | Receipt | Token counts, price cap, charges, timing, the claims of the provider at award time, and the name of the program that sent the job. | No deletion date. | | Content finding | The job id, API key, detector name, action and number of matches. Never the matched text. | See the note below. | | Idempotency key | The `idempotency-key` that you sent, and a hash of the request body. | 30 days. | | Key address | The IP addresses that used an API key. The **API keys** page shows the three most recent. | 30 days. | | Audit log | Who issued or revoked a key, changed a member, or changed a guardrail. See [Audit log](https://inference.ai.exchange/docs/audit-log.md). | No deletion date. | | Workload | The name, model, messages, message template, settings and answer format of each version of a workload that you save. The input and the expected answer of each case of the workload. The answer of each take of a run, which the exchange keeps to score it again. A job stores only the name of the version. | Until you delete the workload, or until a closed workspace is purged. | A project can also hold prompts. See [Project prompts](#project-prompts). A hash of the prompt start does not give back the text. If a job fails at the provider, the receipt keeps the error message of the provider. The exchange does not state a deletion date for content findings. The **Guardrails** page shows only the last 7 days. ## Where text is held for a short time | Place | What it holds | How long | How to avoid it | | --------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Response cache | The whole answer to one request, for the API key that sent it. | The TTL that you set, from 1 to 86400 seconds. The default is 300. | Do not turn the cache on. It is off unless you ask for it. | | Provider | The request that it runs. | Set by the provider. See its [claims](https://inference.ai.exchange/docs/provider-claims.md). | Send `exchange.privacy.retention: "none"`. | | Auto classifier | The system prompt and your messages, for a request that uses Auto. | Set by the classifier vendor. The exchange does not control it. | Send `exchange.privacy.retention: "none"`. Auto then ranks without the classifier. | | Your browser | The chats that you hold on the **Chat** page of the console. | Until you clear the browser storage. The exchange does not store them. | Use the API instead of the **Chat** page. | ### Response cache - The cache is in the memory of the router. It also writes to a shared store, if the exchange runs one, but only for a request with `exchange.privacy.retention: "allowed"`. - A restart empties the memory. - The cache drops the oldest entries when it is full. It does not store an answer larger than 4 MiB. - Send `x-liquid-cache-clear: true` to delete an entry. See [Response caching](https://inference.ai.exchange/docs/response-caching.md). ### Prompt caching Prompt caching can keep the start of a prompt at the upstream of the provider, so that a repeat request costs less. Set `exchange.cache.mode` to `off` to send no cache breakpoints and no cache key. See [Prompt caching](https://inference.ai.exchange/docs/caching.md). ### The responses endpoint The `/v1/responses` endpoint stores nothing. It ignores `store` and refuses `previous_response_id`, `conversation` and `background: true`. ## Who can see what - **The winning provider** sees the request that it runs. The request has no workspace name, API key or email address. A provider that does not win never sees the prompt. - **A provider that served a job** can read the receipt of that job. The receipt does not show your identity, your budget or the competing offers. - **Your workspace** can read its receipts and decisions: members in the console, and API keys with the `read` scope or higher. - **Exchange operators** can read every receipt and decision. These records hold no prompt text. - **The classifier vendor** sees the text of an Auto request, as the table above says. ## Retention policy `exchange.privacy.retention` is a data rule on each request. It has two values. | Value | Effect | | --------- | ------------------------------------------------------------------------------------------ | | `none` | Only offers that promise not to retain the request can compete. | | `allowed` | Any offer can compete, also an offer that promises `none`. Providers can keep the request. | If you set nothing, the exchange treats the request as `none`. The exception is the Auto classifier: it reads your messages unless you set `none` yourself. If no offer meets the rule, the receipt names the reason `retention_policy` for each excluded offer. A guardrail or a preset on your key can make the rule stricter, but not weaker. ## Reference ### Content rules A [guardrail](https://inference.ai.exchange/docs/guardrails.md) can find personal data, secrets and prompt injections in a request. A match stores the detector name and a count. It never stores the matched text. A blocked request is not charged. ### Logs The router writes log lines about each prompt. A line holds the job id, counts of messages and tokens, message roles, and hashes. It does not hold the text. ### Videos A provider keeps the file of a video job. The exchange does not store the file. It fetches the file from the provider when you download it. ## Related pages - [Provider claims](https://inference.ai.exchange/docs/provider-claims.md) - [Purchasing rules](https://inference.ai.exchange/docs/rules.md) - [Guardrails](https://inference.ai.exchange/docs/guardrails.md) - [Response caching](https://inference.ai.exchange/docs/response-caching.md) - [Prompt caching](https://inference.ai.exchange/docs/caching.md) ## Project prompts A project can show the prompts that built it. This is the one case where the exchange keeps prompts of a project, and only if you ask for it. - `liquid record --prompts` shows a notice. You must type `yes`. A script or a coding agent cannot give consent. - The CLI queues the prompts that you type on your computer. It does not collect answers, tool output, files, file names, system prompts or keys. - The CLI also counts, for each prompt, the requests, the tool calls, the seconds of work and the tool name. It never keeps a tool result. A public page says that your CLI reported these counts. - The exchange adds facts from the receipt of each job: the model that served it, the tokens, the charge and the times. The switches of the project apply to a public page. - `liquid record review` shows each prompt. You keep it, drop it, redact it, or mark it public. The CLI flags text that looks like a secret. - `liquid record publish` shows what it will send, and sends it only after you type `yes`. - The exchange stores each prompt encrypted with AES-256-GCM, with a key made for that one row. A copy of the database holds no readable prompt. - A prompt is private by default. You and the operators who review the project can read it. A visitor reads it only if you marked it public and the project is on show. - You can delete one prompt, or delete all of them and withdraw consent, on the project page. The exchange also deletes them with the project. An owner or admin can delete a draft or withdrawn project on its page. Withdraw a project first if it is under review or on show. - The exchange keeps a prompt until you delete it or delete the project. - A prompt can hold a secret. Read each one before you keep it. # Guardrails A guardrail is a policy for a whole workspace. It limits which offers can serve a call, caps spending, limits send rates, and checks the text of a call for prompt injections and sensitive data. The router applies it to every call. A [purchasing rule](https://inference.ai.exchange/docs/rules.md) says what you want to buy. A guardrail says what is never allowed. A request, a preset or a header cannot widen it. ## Set up a guardrail 1. Open **Rules** in the console, then **Guardrails**. 2. Choose the default guardrail, or choose **New guardrail**. 3. Go through the steps, from **Name and keys** to **Review**. 4. In **Prompt injection** and **Sensitive info**, paste a sample under **Try it** to see what a provider would receive. It saves nothing and costs nothing. 5. Save the guardrail. The next call uses the new rules. Only owners and admins can change guardrails. Members can read them. ## Which keys a guardrail covers Each workspace has one **default guardrail**. It covers every API key and every console call. You cannot delete it. - A new guardrail covers **All keys** by default, also keys you make later. - **Select keys** covers only the keys you pick. - A key follows the default guardrail, the all-keys guardrails and its own guardrail together. - A key has at most one guardrail of its own. A workspace has at most 20 guardrails, the default one included. ## What a call sees A guardrail blocks emails. A call with `jane@example.com` in a message gets this response before the router holds money. You pay nothing. ```json { "error": { "message": "a guardrail of this workspace blocks requests that match an email address", "type": "liquid_error", "code": "content_blocked" } } ``` If the detector is set to **Redact**, the call goes through. The provider receives `Reply to [REDACTED_EMAIL]`. ## What a guardrail can hold A guardrail has five kinds of rule: limits that remove offers, a budget, rate limits, a prompt injection check and a sensitive info check. ### Your own cloud keys Some limits decide which other companies can receive a prompt. They do not apply to an offer on a cloud key that your workspace connected (see [Bring your own key (BYOK)](https://inference.ai.exchange/docs/byok.md)), because an admin already chose that cloud. These do not apply: - the allowed and excluded providers - the regions - the retention - the provider claims All other rules apply to your own key. A limit that the call or the preset of its key sets also applies. For example, `"privacy": {"regions": ["eu"]}` excludes a key whose region is `global`. If the call and its preset set no retention, the exchange asks for none, also for your own key. A key that keeps prompts takes only a call that sends `"privacy": {"retention": "allowed"}`. See [Bring your own key (BYOK)](https://inference.ai.exchange/docs/byok.md#control-a-key). ### Budget A budget is an amount in USD and a period: **Daily**, **Weekly** or **Every 30 days**. The period is a window that ends now: the last 24 hours, 7 days or 30 days. The router adds the settled charges in the window and the price caps of jobs in flight. It refuses a call with `402 budget_exhausted` when the call's price cap is more than the budget that remains, so a high cap can fail before the budget is spent. - A key guardrail counts the spend of that key. - The default guardrail counts the spend of the workspace. - To limit one key without a guardrail, use the spending limit in [API keys](https://inference.ai.exchange/docs/keys.md). #### Guardrail of an organization The default guardrail of an organization bounds every workspace that spends from the organization. It does not bind a workspace with its own balance. A workspace adds its own guardrails. It can make limits stricter and cannot loosen them. The router uses the stricter value of each limit. The workspace sees the guardrail of the organization and cannot change it. Only an owner or an admin of the organization can change its guardrail, unless they gave a member the **Guardrail** level of write. See [Workspace members](https://inference.ai.exchange/docs/members.md). ##### Guardrails that cannot merge The exchange refuses to save a workspace guardrail that cannot merge with the guardrail of the organization, such as an allowlist that shares no provider with the allowlist of the organization. The answer is `422` with the code `guardrail_conflicts_with_organization`. A move of a workspace into an organization is refused the same way. If the organization tightens its guardrail later, the save is accepted. The router then refuses each call that cannot merge. An owner or an admin sees each open workspace that no longer merges, in the field `conflicts`. In a workspace under an organization, each step marks the rules that the organization changes: | Mark | Meaning | | ------------------------------- | ------------------------------------------------------------ | | **From organization** | Your rule is blank and the rule of the organization applies. | | **Stricter than organization** | Your rule is tighter. | | **Overridden by organization** | Part of your rule has no effect. | | **Conflicts with organization** | No call could pass both. You cannot save the guardrail. | ##### Budgets of an organization The guardrail of an organization has three budgets: - **Budget of the organization.** It counts the jobs of the organization and of every workspace under it, once, through the payer. - **Budget for each workspace.** Set it in the **Budget for each workspace** card on the Guardrails page of the organization. The router counts each workspace on its own. The lower remaining amount applies. - **Budget for each end user.** It applies to each end user within each workspace. A rate limit of the scope `workspace` applies to each workspace on its own. #### Budget for each end user A guardrail can hold a budget for each end user, with the same periods. Each end user has its own budget, so one end user that spends all of it does not stop the others. See [End users](#end-users). The router takes the lowest of these as the price cap of the call: the balance, the key limit, the budget that remains in each guardrail, and the end-user budget that remains. A call over the end-user budget gets `402`: `insufficient_balance` if settled spend used the budget, or `budget_exhausted` if jobs in flight hold the rest. ### Rate limits A rate limit has two optional numbers. A blank number sets no limit. | Field | Counts | | --------------------- | ------------------------------------------------------------------------------- | | `requests_per_minute` | Calls that the router admits in a sliding window of 60 seconds. | | `tokens_per_minute` | The input tokens plus the output tokens that the call asks for, in that window. | A guardrail sets each limit for three scopes: | Scope | Applies to | | ----------- | ----------------------------------------------------------------------------- | | `key` | Each key of the workspace, one at a time. One busy key does not slow another. | | `end_user` | Each end user that a call names, one at a time. See [End users](#end-users). | | `workspace` | One total for all calls of the workspace. | A call must have room in every limit that applies to it. When two guardrails set the same limit, the lower number wins. - The router counts a call when it admits it, before it holds money. A refused call counts for nothing. - A call with `n` choices counts `n` times. - The token count uses `max_completion_tokens` or `max_tokens`, or else the default output limit. Unused tokens are not given back. A call over a limit gets `429 rate_limited`. Only a refused call carries these headers: ```http HTTP/1.1 429 Too Many Requests Retry-After: 12 X-RateLimit-Limit-Requests: 20 X-RateLimit-Remaining-Requests: 0 X-RateLimit-Scope: end_user ``` - `Retry-After` gives the seconds to wait. - `X-RateLimit-Limit-Requests` and `-Remaining-Requests` give the limit that refused the call and what is left. The `-Tokens` headers do the same for tokens. - `X-RateLimit-Scope` is `key`, `end_user` or `workspace`. A rate limit is not exact. Each router process keeps its counters in memory, so the real limit can be a few times higher. It stops a loop or a leaked key. A budget is exact, because the ledger enforces it. ### End users An **end user** is a person or a system that calls through your application. The exchange has no account for it. You name it in the call, and the router can then limit its speed and its spend. The router reads the end-user id from the first of these that is present: 1. The header `X-Liquid-End-User`. 2. The `user` field of an OpenAI-format body. 3. The `metadata.user_id` field of an Anthropic-format body. For example, add `-H "X-Liquid-End-User: customer-4821"` to a request. - The id has 1 to 512 printable ASCII characters. Otherwise the call gets `400 invalid_end_user`. - An empty `user` field in the body is ignored, because some SDKs send one. An empty header is refused. - A call with no id counts as the end user `anonymous`. - The router stores only a keyed hash of the id, and the same id in two workspaces gives two hashes. The id never goes to a provider. The id is not a credential. Any holder of a key can send any id. An end-user limit protects the workspace from its own customers, and a key limit from the holder of a key. ### Prompt injection The router looks for text that tries to override your instructions, such as "ignore all previous instructions". The action is **Off**, **Flag** (record the match and send the call) or **Block** (refuse the call before a provider sees it). By default the router reads every message, including tool results, because an injection from a web page or a document arrives there. You can choose to read only the last user message. There is no **Redact** action, because replacing text in the middle of a sentence changes its meaning. ### Sensitive info Set one action for each detector: **Off**, **Flag** (record the match and send the text unchanged), **Redact** (replace each match with a placeholder such as `[REDACTED_EMAIL]` before any provider sees it) or **Block** (refuse the call). The detectors are in the [reference](#reference). You can add up to 10 custom patterns, each with its own action. ### What the content rules do not do - The router reads the text of messages and tool-call arguments. It does not read images or the answer. - Detection uses patterns and checksums, not a model. It cannot find a person's name or an address. Do not treat it as complete. - The router checks the text before it holds money or contacts a provider. A blocked call is not charged and never gets a cached answer. - The router records a **finding** for each match: the detector, the action and the number of matches, never the text. The **Guardrails** page shows the findings of the last 7 days. ## When several guardrails apply For a call, the router merges the default guardrail and the key guardrail into the strictest rules that satisfy both. Guardrails are the outermost layer. The rules of a request, the preset of a key and the headers apply inside them. - A ceiling or a time limit takes the lower value. A minimum throughput takes the higher value. - Two allowlists become their intersection. If it is empty, no offer can serve the call. An empty list or a list with `*` does not limit. - Two exclusion lists join. - For each detector, the stronger action wins: `flag`, then `redact`, then `block`. - The injection rule reads at least the messages that either guardrail reads. - Each budget applies on its own. For each rate limit, the lower number wins. Two pattern lists, such as `gpt-*` and `*-mini`, have no exact intersection. The router keeps a pattern only if the other list holds it or names a model that it matches. ## Refusals A refused call gets `guardrail_violation`, `content_blocked`, `budget_exhausted`, `rate_limited`, `invalid_end_user` or `guardrail_unavailable`. See [Errors](https://inference.ai.exchange/docs/errors.md). If the router cannot read the guardrails, it refuses the call, because a call without them could spend what you forbade. ## Reference ### Limits The limits use the same fields as the [Limits](https://inference.ai.exchange/docs/rules.md) of a purchasing rule: - `exchange.candidates.providers.allow` and `exchange.candidates.providers.exclude` - `allowed_models` and `exchange.candidates.models.exclude`. Only a guardrail takes `allowed_models`. A name matches the model or the deployment symbol, and `*` matches any run of characters. - `exchange.candidates.creators.allow` - `exchange.privacy.regions`. The console lists every published region, also a region with no offer now. - `exchange.privacy.retention`, which is **Zero data retention only** in the console - `exchange.privacy.requirements`, the claims that a provider must hold. See [Provider claims](https://inference.ai.exchange/docs/provider-claims.md). - `cap_usd`, the price cap for one call - `exchange.limits.price.input_usd_per_mtok`, `exchange.limits.price.output_usd_per_mtok`, `exchange.limits.price.cache_read_usd_per_mtok` and `exchange.limits.price.cache_write_usd_per_mtok` - `ttft_ms`, `completion_ms` and `exchange.limits.min_tps` A guardrail only removes choices. It cannot hold `exchange.candidates.models.allow`, `exchange.rank.strategy`, `exchange.rank.workload_profile` or `exchange.cache`. The console refuses to save these fields. The rate limits are `limits`, with `key`, `end_user` and `workspace`. A number below 1 is refused. Leave a number out for no limit. The budget for each end user is `end_user_budget`, an amount and a period. ```json { "limits": { "key": { "requests_per_minute": 600, "tokens_per_minute": 400000 }, "end_user": { "requests_per_minute": 20 }, "workspace": { "requests_per_minute": 5000 } } } ``` ### Detectors | Detector | What it finds | | ------------- | ---------------------------------------------------------------- | | `email` | Email addresses. | | `phone` | Phone numbers of 10 to 13 digits. | | `ssn` | US Social Security numbers. | | `credit_card` | Card numbers of 13 to 19 digits that pass the Luhn check. | | `ip_address` | IPv4 addresses. | | `secret` | API keys, access tokens, JSON web tokens and private key blocks. | ### Custom patterns A guardrail can hold up to 10 custom patterns. Each has a name of 1 to 40 letters, digits, `_` or `-`, a regular expression of up to 200 characters, and an action of `flag`, `redact` or `block`. The expression cannot use look-around or back-references. A name cannot repeat a built-in detector. The tester reads at most 10,000 characters. ### Tokens that change guardrails A token from `liquid login --write` can change guardrails for an owner or an admin. A token from `liquid login` can only read them. See [Liquid CLI](https://inference.ai.exchange/docs/cli.md). An API key cannot read or change guardrails. # Workspace members A workspace belongs to the people in it. Each member has a role: owner, admin or member. The role decides who can issue API keys, change guardrails, and invite or remove people. ## Invite a person A person joins a workspace only when they accept an invitation. An owner or an admin invites them by email address. The person does not need an account first. 1. Open **Members** in an organization, or **Settings** in another workspace. Select **Invite people**. 2. Enter the email address that the person signs in with. 3. Choose a role. The default is Member. Only an owner of the workspace can invite an owner. 4. Select **Create invitation**. The exchange emails the link to the address. 5. If the dialog says that the email was not sent, copy the link that it shows and send it to the person. The invitation gives no access until the person accepts it. [Organizations](https://inference.ai.exchange/docs/organizations.md) describes how the person accepts, and how to withdraw or send an invitation again. Two rules apply: - To join a workspace under an organization, a person must be a member of the organization, because each member spends its balance. The exchange refuses the invitation, and its acceptance, while the person is not in the organization (`not_in_organization`). Invite them to the organization first. - A new member can spend the balance of the workspace. So you must have signed in recently to invite a person, change a role or remove a member. If the console asks you to confirm your identity, confirm it and try again. ## What each role can do | Role | In short | | ------ | ------------------------------------------------------------------------------------------------ | | Owner | Everything an admin can do. Also grants the owner role, and changes or removes other owners. | | Admin | Manages API keys, presets, guardrails and members, and reads the audit log. Cannot touch owners. | | Member | Reads the members, guardrails, presets, keys, balance and usage. Cannot change them. | The console shows only the controls that your role allows. The server checks your role again on every change. ### Roles from an organization An owner or an admin of an organization is also an admin in each workspace under it. The list shows them with the badge **Organization owner** or **Organization admin**. - You change their role and remove them on the members page of the organization, not here. - An owner of the organization can change the role of a member to owner here, and change or remove an owner. An admin of the organization cannot. - Only an owner of the workspace invites an owner. - When a person leaves the organization, they lose the access that the organization gave them at once. Direct workspace roles follow the [removal rules for an organization](https://inference.ai.exchange/docs/organizations.md#leave-an-organization). - The keys, invitations and provider changes that they made before stay, as for an admin of the workspace. See [Organizations](https://inference.ai.exchange/docs/organizations.md). ## Change a role, remove a person or leave An owner or an admin can change the role of a member and remove a member. An owner or an admin can also close the workspace. Rules for owners: - Only an owner, or an owner of the organization above, can change a role to owner, or change or remove an owner. - Only an owner of the workspace can invite an owner. - An admin cannot change or remove an owner. - A change on this page always keeps at least one owner. To remove the last owner, make another person an owner first. Any member can leave a workspace. Select **Leave** on your own row in the **Members** card, and confirm. - You lose access to the keys, balance and chats of the workspace. - An owner or an admin can invite you again. - An owner or an admin of the organization keeps access through the organization. ## Set what a member can do A member has one level for each resource. An owner or an admin opens a member's drawer with **Access** on the member's row. - In a workspace, the drawer shows the role and the levels in that workspace. - In an organization, it shows the role, the levels of the organization and the workspaces under it. See [Levels in an organization](#levels-in-an-organization). | Resource | Levels | | ---------- | ---------------------------------------- | | API keys | None, own keys, all keys | | Guardrails | Read, write. A member always reads them. | | Presets | None, read, write | | Workloads | None, read, write | Choose a named role to set all four levels at once: - **Developer** creates keys and writes presets and workloads. - **Viewer** reads everything and changes nothing. - **Member** is the default. Change one level, and the access becomes **Custom**. **Reset to the default** returns the member to the default access. Owners and admins have all levels, so the drawer shows no controls for them. The server checks each change. ### Levels in an organization An organization has no API keys, presets or workloads of its own. Its levels cover what belongs to the organization. Owners and admins have all of them, and a plain member has the default level of each until an owner or an admin changes it. | Resource | Levels | Default | | ---------- | ------------------------------------------------------------------------ | ------- | | Guardrail | Read, write. Write changes the guardrail and budget of the organization. | Read | | Workspaces | None, create. Create puts a workspace of the person under it. | None | | Members | Read, invite. Invite sends an invitation for the member role. | Read | | Settings | Read, edit. Edit changes the name, description and logo. | Read | A named role does not apply, so set each level. The exchange refuses a named role in an organization (`organization_takes_no_role`). Some things have no level and stay with owners and admins: the balance and funds, verified domains, roles and access, and closing the organization. Two levels need care: - A workspace under an organization spends the balance of the organization. A person with the **Create** level can put a workspace that they own under it, so choose who gets this level. The guardrail and its budget for each workspace limit what they spend. - A person with the **Invite** level can invite only the member role. To invite an admin or an owner, you must be an owner or an admin. #### Workspaces in the drawer The drawer of an organization also lists the workspaces under it, with the role of the person in each. - **Remove** ends the person's access to that workspace only. They stay in the organization. - **Add** puts the person in that workspace as a member at once, and the person gets a notice in the bell. Only members of the organization can be added, and there is no invitation to accept. - A person who leaves the organization follows the removal rules above. - To add every member of the organization at once, see [Choose who can access a workspace](https://inference.ai.exchange/docs/organizations.md). - Set the access of a person to a workspace in that workspace. ## Reference ### Refusals | Status | Code | Meaning | | ------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 403 | `permission_denied` | Only owners and admins can manage members and invite people. | | 403 | `owner_required` | Only an owner can invite an owner. An owner, or an owner of the organization above, can change a role to owner, change or remove an owner, and resend or revoke an owner's invitation. | | 409 | `managed_by_organization` | An owner or admin of the organization above cannot change their own role or access in the workspace, or invite themselves. | | 422 | `not_in_parent` | Only members of the organization above a workspace can be added to it. | | 409 | `not_in_organization` | The person is not in the organization above the workspace. Invite them there. | | 409 | `access_needs_member` | Only the access of a plain member can be set. Owners and admins have full access. | | 409 | `organization_takes_no_role` | An organization takes its levels one by one, not a named role. | | 404 | `not_member` | The person is not a member of this workspace. | | 409 | `already_member` | The person is already a member. Change their role instead. | | 409 | `last_owner` | A workspace must keep at least one owner. | | 409 | `system_account` | The workspace of the exchange cannot change members. | Each change goes to the [audit log](https://inference.ai.exchange/docs/audit-log.md). ## Next steps - [API keys](https://inference.ai.exchange/docs/keys.md) - [Organizations](https://inference.ai.exchange/docs/organizations.md) - [Guardrails](https://inference.ai.exchange/docs/guardrails.md) - [Audit log](https://inference.ai.exchange/docs/audit-log.md) # Organizations An organization pays for the workspaces under it. Their jobs are charged to the organization balance. Use one when a company has several teams and wants one balance and one budget. A workspace is on its own or under one parent. The parent is an organization or the default workspace of a person. A parent cannot have a parent. ## Create an organization Open the first breadcrumb menu in the console header and choose **New organization**. Only an owner or an admin of an organization can add funds, withdraw money or list deposits. See [Workspace members](https://inference.ai.exchange/docs/members.md). ## Change the profile and the logo 1. Open **Settings** in the account menu of the organization. 2. In the **Profile** row, select **Edit profile**, change the name or description, and select **Save changes**. To change the logo, select it or drop an image on it. The logo must be a PNG, JPEG or WebP image of 1 MB or less. An owner, an admin, or a member with the **Settings** level of edit can change it. Select **Remove** to go back to the generated picture. The handle never changes. ## Let people join with an email domain People who sign up with an email address at your company domain can join as members. Only an owner or an admin can set this up. 1. In the **Verified domains** row, select **Add domain** and enter the domain, such as `example.com`. 2. Add the TXT record that the page shows at your DNS provider. The name is `_liquid-verify.` followed by the domain. 3. Select **Verify domain**. Try again if DNS has not updated. - A domain has no effect until it is verified, and only one organization can verify it. - A public email domain, such as `gmail.com`, is not allowed. - A person joins only with an address that their identity provider verified. People who already have an account need an invitation. - **Remove** stops new people from joining. The people who joined stay members. The [audit log](https://inference.ai.exchange/docs/audit-log.md) records `ADD_DOMAIN`, `VERIFY_DOMAIN`, `REMOVE_DOMAIN` and `JOIN_BY_DOMAIN`. ## Invite people by email An owner or an admin can invite a person. A member with the **Members** level of invite can invite the member role. 1. Open **Members** and select **Invite people**. 2. Enter the email address and choose a role. Only an owner can invite an owner. 3. Select **Create invitation**. The exchange emails the link. If the dialog says the email was not sent, copy the link and send it yourself. - The link works for 7 days. The page shows it only once. - **New link** and **Send again** email a new link. **Revoke** stops an invitation. The person must sign in or sign up with the invited address. An account with another address cannot accept it. A person with an account can also **Accept** or **Decline** in the bell in the header. They join with the invited role. The [audit log](https://inference.ai.exchange/docs/audit-log.md) records `INVITE_MEMBER`, `RESEND_INVITATION`, `REVOKE_INVITATION`, `ACCEPT_INVITATION` and `DECLINE_INVITATION`. ## Leave an organization Select **Leave organization** on the **Settings** page and confirm. - You lose the access that the organization gave you, including your member roles in its workspaces. - A role that you hold directly in a workspace stays. An organization owner can change or remove it. - The same occurs when an owner or an admin removes you. An organization keeps at least one owner. The last owner must make another person an owner first. ## Move funds between workspaces Move money between workspaces that you own or administer. The money of an organization stays in it, so the exchange refuses a move between an organization and a workspace outside it (`outside_organization`). 1. Open **Credits** for the workspace that holds the money. 2. In **Move funds**, select **Move funds**, choose the receiving workspace and the amount, and confirm. The paid part of the balance moves first. Credit moves only for an amount over it. - A workspace under an organization holds no money, so it is not an end of a move. - You must be an owner or an admin of both workspaces, with a recent sign-in. The [audit log](https://inference.ai.exchange/docs/audit-log.md) records `TRANSFER_FUNDS` in both workspaces. ## Close a workspace Only an owner or an admin can close a workspace. The exchange refuses the close while the workspace holds a balance (`holds_money`) or a job is in flight (`open_hold`). Move the balance to another workspace first. - An organization cannot move its balance out. See [Close an organization that holds money](#close-an-organization-that-holds-money). - An operator can reinstate a closed workspace until it is purged. A workspace under a closed organization comes back only after the organization. ## Create a workspace in an organization Open the workspace menu while you act for an organization, and select **New workspace**. 1. Enter a name. 2. Choose who joins. **Everyone** is the default. Clear it to select members one by one. 3. Select **Create**. Each added person gets a notice in the bell and the default access of a member. An owner or an admin can change it in the [member's drawer](https://inference.ai.exchange/docs/members.md). ## Choose who can access a workspace A workspace under an organization has one of two settings. Owners and admins of the workspace can change it in **Settings**, in the **Who can access** card. - **All organization members**: each member of the organization is a member of the workspace, including people who join the organization later. Choosing this adds all current members at once, with a notice in the bell. - **Only members that an admin adds**: an owner or an admin adds each person. The exchange removes no one when you choose this. Each person keeps access until removed or until they leave the organization. A new workspace uses the first setting. A workspace that you move under an organization uses the second. The [audit log](https://inference.ai.exchange/docs/audit-log.md) records `SET_WORKSPACE_ACCESS_MODE`. ## Put a workspace under an organization Who can move a workspace: - An owner or an admin of the workspace and of each parent that the change touches. - A member with the **Workspaces** level of create in the new parent, for their own workspace. - A move to a default workspace or no parent needs a direct workspace owner. Steps: 1. Open the second breadcrumb menu and select **Settings for** your workspace. 2. In the **Spends from** card, choose the organization or your default workspace. Each member of the workspace must be a member of the organization. Otherwise the exchange refuses the move (`outside_members`) and names each person. Remove them from the workspace or invite them to the organization. The exchange also refuses the move when a [guardrail](https://inference.ai.exchange/docs/guardrails.md) of the workspace cannot merge with the guardrail of the organization. To remove the parent, select **None: this workspace pays for itself**. The exchange refuses this while the workspace holds money or a job is in flight. The move is in the [audit log](https://inference.ai.exchange/docs/audit-log.md) as `MOVE_WORKSPACE`. ## What changes - A job holds its price cap from the balance of the parent. Settlement and refunds go to the parent. The workspace balance stays empty. - A deposit to the workspace credits the parent. - The exchange refuses a job if the parent is suspended. A parent cannot close while a workspace under it is open. - Owners and admins of the parent read the [audit log](https://inference.ai.exchange/docs/audit-log.md) rows of the workspaces under it. - Owners and admins of an organization are admins in every workspace under it. - They show with a badge in the member list and lose the access when they leave the organization. - They have full access to each provider that a workspace registered. - An organization owner can make a member an owner, and change or remove an owner, in each workspace. A default workspace as parent gives no such access. ## Budgets The budget in the default [guardrail](https://inference.ai.exchange/docs/guardrails.md) of the parent counts every job that the parent pays for: its own and those of each workspace under it. Two workspaces cannot pass it together. - The default guardrail of a workspace counts that workspace only. The guardrail of the parent can also set a budget for each workspace. - A workspace that leaves keeps what it spent in the period. ## Close an organization that holds money An operator must close an organization that holds a balance: 1. Open the organization, choose **Close**, and select the workspace that receives the balance. You must be an owner or an admin of both. 2. Send the request. The organization shows **Pending close** and works as before. You can withdraw the request. 3. An operator approves or refuses. An approval moves the whole balance to your workspace and closes the organization. A refusal changes nothing. The request fails with `close_request_open` if one is open, with `has_open_children` while a workspace under the organization is open, and with `open_hold` while a job is in flight. The [audit log](https://inference.ai.exchange/docs/audit-log.md) records `REQUEST_CLOSE` and the decision. # Audit log The audit log records who changed the keys, the members and the guardrails of a workspace. Use it to answer "who did this, and when?". Only owners and admins can read it, because a row can name a colleague. A member gets `403 permission_denied`. To open it, choose **Settings** in the console, then **Audit log**. ## Find a change 1. Open the **Audit log**. The newest row is first. 2. Type in the filter box. It searches the person, the action and the subject. 3. Choose the export button to save every row as a CSV file, not only the rows on screen. The file has the columns `at`, `workspace`, `actor`, `actor_email`, `action`, `subject` and `detail`. The `workspace` column is empty for a row of this workspace. A row records the change. It does not record the secret of a key or the text of a call. ## What it records | Action | Meaning | | ---------------------------------------------------------- | ------------------------------------------------------------------------------- | | `ISSUE_KEY`, `REVOKE_KEY` | Someone issued or revoked an [API key](https://inference.ai.exchange/docs/keys.md). | | `MEMBER_ROLE`, `MEMBER_REMOVE` | Someone changed the role of a [member](https://inference.ai.exchange/docs/members.md), or removed a member. | | `INVITE_MEMBER`, `RESEND_INVITATION`, `REVOKE_INVITATION` | Someone invited a person, sent the invitation again, or withdrew it. | | `ACCEPT_INVITATION`, `DECLINE_INVITATION` | The invited person accepted the invitation, or declined it. | | `GUARDRAIL_CREATE`, `GUARDRAIL_UPDATE`, `GUARDRAIL_REMOVE` | Someone created, changed or deleted a [guardrail](https://inference.ai.exchange/docs/guardrails.md). | | `GUARDRAIL_KEY_ASSIGN`, `GUARDRAIL_KEY_UNASSIGN` | Someone gave a key a guardrail, or removed it. | | `MOVE_WORKSPACE` | Someone moved the workspace under an organization, or out of it. | | `SET_WORKSPACE_ACCESS_MODE` | Someone chose whether all members of the organization can access the workspace. | Each row shows: - **When**: the time of the action. - **Who**: the name of the person, or the email address when there is no name. A change made with a CLI token shows the person who owns the token. An action of the exchange itself shows "The exchange". - **What**: the action, in words, and what it was on: a key, a guardrail, or a member. - **Detail**: the fields of the change. For example, a role change holds the old role and the new role. ## Workspaces under an organization The owners and admins of an organization also read the rows of the workspaces under it. Each of those rows shows the handle of its workspace. The rows follow the current parent: when a workspace moves under the organization, its whole log shows, and when it moves out, its rows go with it. ## Limits of the log - The log starts on the day that the exchange began to record the workspace of each action. An action from before that day is not in it. - The log records changes to keys, members and guardrails only. It does not record the jobs that you send. Receipts record those. - The exchange never deletes or changes a row. ## Next steps - [Workspace members](https://inference.ai.exchange/docs/members.md) - [Guardrails](https://inference.ai.exchange/docs/guardrails.md) - [API keys](https://inference.ai.exchange/docs/keys.md) # Provider claims Each provider can state how it handles your data and which audits it passed. The exchange shows each statement as a badge. You can require a claim, so that only providers that hold it can serve your calls. ## Require a claim Add `exchange.privacy.requirements` to the `exchange` block of a request: ```json { "model": "gpt-oss-120b", "messages": [{ "role": "user", "content": "Summarize this contract." }], "exchange": { "privacy": { "requirements": { "require": ["zero_retention", "soc2_type2"], "checked_only": true } } } } ``` - `require` lists claims. The provider must hold every one. - `checked_only` is `false` by default. When it is `true`, a declared claim does not pass. What happens: - The exchange leaves out each offer from a provider that fails the rule. The receipt names the reason `provider_requirements` for each excluded offer. - A provider with no claim data fails any requirement. - If no provider meets the rule, the call fails with an error. It does not run on a weaker provider. A guardrail can set the same rule for a whole workspace. Open **Guardrails** and choose the step **Data privacy**. A rule on a key or a preset and a rule on a request combine. The union of the claims applies, and `checked_only` applies if either sets it. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md) and [Guardrails](https://inference.ai.exchange/docs/guardrails.md). The **Require** chips on the order book, the models page and the provider directory only filter the list on your screen. They do not change what the exchange buys. ## What a badge shows The shape of a badge shows how much you can rely on the claim. | Badge | Name | Meaning | | ------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------- | | Solid tint and a filled seal | Checked | A reviewer read the document and recorded an end date. The badge shows until that date. | | Dashed outline | Declared | The provider states it. The exchange has not seen evidence. | | Clock | In review | The provider sent a document. A reviewer has not decided. A declared claim stays declared until the decision. | | Line through the text, and a warning | Lapsed | The exchange checked it, and the end date passed. A lapsed claim does not pass any requirement. | | Hatched question mark | Unknown | The provider states nothing. Treat the data as retained and used for training. | The badge shows the standing on the day you look at it. A checked claim with no end date counts as lapsed. ## What a provider can claim | Claim | Meaning | | -------------- | --------------------------------------------------------------------------------------------------------------- | | Zero retention | The provider stores no prompt and no output after the response. | | Kept N days | The provider stores prompts and outputs for N days. | | No training | The provider does not train a model on your prompts. | | May train | The provider can train a model on your prompts. | | Certifications | SOC 2 Type I and II, ISO 27001 and 42001, HIPAA BAA, GDPR DPA, HITRUST, FedRAMP Moderate, PCI DSS and CSA STAR. | Retention and training are two separate facts. A provider can keep data for 7 days and never train on it. A prompt that stays in memory for caching is not retention. The list of claims is closed. SOC 2 Type I and Type II are different claims. In API calls, the claim names are `zero_retention`, `retention_days`, `no_training`, `may_train`, `soc2_type1`, `soc2_type2`, `iso27001`, `iso42001`, `hipaa_baa`, `gdpr_dpa`, `hitrust`, `fedramp_moderate`, `pci_dss` and `csa_star`. ## Claims and the retention policy `exchange.privacy.retention: "none"` and the `zero_retention` claim are two different tools. - `exchange.privacy.retention: "none"` uses the promise in the offer. The provider makes that promise, and the exchange does not check it. - `zero_retention` with `checked_only: true` needs a claim that a reviewer checked and that has not lapsed. See [Data handling](https://inference.ai.exchange/docs/data-handling.md). ## Where the badges appear - **Table row:** three fixed slots show retention as a short word, training as an icon, and the strongest certification with a count of the others. Hover over a slot or move focus to it to see the full wording. - **Provider page:** the section "Data and compliance" lists every claim with its status and dates. - **Quote page:** the data policy lists the claims of the provider. - **Receipt:** the claims are those that held when the exchange awarded the job. A claim that had lapsed is left out. A later change does not rewrite them. ## For providers You can declare claims when you register. You can also add or change them later on the **Registration** page of your provider. 1. Choose how long you keep prompts, whether you train on them, and the certifications you hold. Save the claims. 2. Each claim shows as declared. Buyers who require checked claims skip you until the exchange checks it. 3. Choose **Add a document** for a claim. Send one PDF of 10 MiB or less. For a certification, send the report. 4. The claim shows a clock while the document waits for a decision. 5. The exchange approves or rejects the document. The decision appears on your page. A rejection includes a note that tells you what to fix. A reviewer confirms these points before an approval: - The name on the document matches your registered company. - The document covers the environment that serves jobs. - The document is dated within the last 12 months. A certification report must cover a period that ended within the last 12 months. - A report has no qualified opinion, and the auditor is on the list that the exchange accepts. An approval sets an end date. The claim shows as checked until that date. The exchange publishes the result, never the file. At expiry: - The exchange sends you a warning 14 days before the end date. - On the end date, the check lapses. The claim does not pass any buyer requirement. - To restore the check, send a new document before the end date. The old check stays valid until a reviewer decides. Remove a claim to stop stating it. ## Related pages - [Data handling](https://inference.ai.exchange/docs/data-handling.md) - [Purchasing rules](https://inference.ai.exchange/docs/rules.md) - [Guardrails](https://inference.ai.exchange/docs/guardrails.md) # Notifications The bell in the header shows what needs your attention. Use it to find a low balance, a key near its limit or a payment problem before a request fails. A notice belongs to you, not to a workspace. It stays in the bell when you change workspace. The bell shows the number of unread notices. ## What the bell shows | Notice | When it appears | Who gets it | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | | Welcome | You open your account for the first time. It links to [API keys](https://inference.ai.exchange/docs/keys.md). | The new person | | Balance is low | The balance falls below your [balance alert](#set-a-balance-alert). | Owners and admins of the workspace | | Key near its limit | A key has spent 80% of its spend limit. | Owners and admins of the workspace | | Key limit reached | A key has spent all of its spend limit. The key refuses requests until the window moves or you raise the limit. | Owners and admins of the workspace | | Funds added | A payment is credited to the balance. | Owners and admins of the workspace | | Payment did not go through | A payment expired or failed. No money was taken. | Owners and admins of the workspace | | Payment reversed | The payment rail took back a payment, for example after a chargeback. The workspace is suspended until support clears it. | Owners and admins of the workspace | The bell also lists [invitations](https://inference.ai.exchange/docs/members.md) to an organization and the workspaces that someone added you to. ## Set a balance alert 1. Open **Settings** for the workspace that holds the balance. 2. Under **Notifications**, turn on **Low balance**. The alert starts at $10. 3. Choose the arrow at the right of the row, type an amount in dollars, and choose **Save**. The amount must be more than $0 and at most $1,000,000. The alert is off until you set it. To turn it off, turn off the switch. Only an owner or an admin can set it. A workspace under an organization spends the balance of the organization, so set the alert on the organization. You get one notice each time the balance falls below the amount. When a top-up lifts the balance to the amount or above, the alert arms again and the old notice goes away. If you change the amount, the next check can send a new notice. ## Read and dismiss Choose a notice to mark it read and open the page that it is about. Choose **Mark all read** to clear the count. Choose the close button on a notice to remove it. A removed notice does not come back. The exchange checks balances and key limits every five minutes, so a notice can be up to five minutes late. The exchange keeps a read or removed notice for 90 days. ## Limits - The bell is the only channel. The exchange sends no email for these notices. - The 80% notice and the 100% notice for a key use the same window as the key's spend limit. The window moves with time, so a key that falls below 80% can send the notice again when it climbs. # Referrals Share your referral link. When a person opens a Liquid Inference account with it, you earn a share of the fees that Liquid Inference collects on that person's inference. You earn credit that you can spend on inference. ## Share your link Choose **Referrals** in the sidebar, or open `/referrals`. The page shows these items: - Your link. - The number of people that you referred. - The terms that they joined under. - The total that they earned you, and the amount for each month. It does not show who they are or what each of them spends. The link is a page of the app with your code, for example `/sign-up?ref=abcde12345`. The code works on every page of the app. The app keeps it until the visitor opens an account. ## What you earn You earn two shares of the fees of Liquid Inference. Each share lasts for a set number of months from the day the referred person joined: | Who | What you earn | | ----------------------------------------- | ----------------------------------- | | A person you referred | The direct share of their fees | | A person referred by someone you referred | The second-tier share of their fees | Liquid Inference sets the rates and the period, and can change them for future referrals. The **Referrals** page shows the terms that apply now and the terms of each referral. Both shares for a person become fixed when that person joins. For example, you referred Bob, and Bob referred Carol. Your share of the fees of Carol is the second-tier rate that applied when Carol joined. It lasts for the period of Carol. It stops earlier if your referral of Bob stops first. These rules decide which fees count: - The fees of a person count from the time that person first pays money in. A deposit counts unless it is a promotional credit or a transfer between workspaces. Inference that ran before that time, on the signup credit, earns nothing. - You earn a share of the Liquid Inference fee only. The provider's part of a charge earns nothing. - A job that the exchange credits back to the buyer earns nothing. For example, this occurs when a provider fails after output started. The inference of a person is each job that runs in a workspace that the person created, whoever sent the job. This includes jobs that a provider that you run serves. On these jobs, you earn your share of the fee and your provider earnings. ## The discount for a referred person A person who joins with a referral link pays less of the fee. The default discount is 10% of the fee. Liquid Inference can change the rate. It applies for the same period as the referrer's share. - The discount comes off the fee only. The provider earns the same. - The `exchange.charge` block of the response shows the fee after the discount in `fee_usd`, and the discount in `fee_discount_usd`. The field `fee_discount_usd` is absent when there is no discount. - A referred person pays a lower fee, so your share of that fee is lower too. ## How credits are paid - Liquid Inference pays credits one time each day, for the previous UTC day. It starts to pay at least one hour after the day ends. - It rounds each credit down to the nearest nano-dollar. - Credits go to your personal workspace, which Liquid Inference created when you opened your account. They show as **Referral credit** in your credits. - You can spend credits on inference, as you spend all other balance. You cannot withdraw them, and Liquid Inference does not pay them out as cash. The signup credit works the same way. - The balance shows credits as **credited**, separate from money that you paid in. Inference uses credit first. - The **Referrals** page shows credits as monthly totals. It lists the last 12 months, and a month only when three or more people earned you credits in it. Thus the spending of one person does not show. The estimate of unpaid credit follows the same rule. ## Rules - Liquid Inference records a referral only when the person opens the account. You cannot refer a person who already has an account. Nobody can change who referred a person. - You cannot refer yourself. - A link with a code that does not exist still opens an account, without a referrer. # Move from OpenRouter The exchange serves the OpenAI Chat Completions format, so most OpenRouter code changes in two places: the base URL and the API key. The exchange is an auction. It does not send your request to a fixed provider, so read [What is different about money](#what-is-different-about-money) before you send traffic. ## Change these two things 1. Issue a `spend` key on the **API keys** page. See [API keys](https://inference.ai.exchange/docs/keys.md). 2. Set the base URL to `https://router.inference.ai.exchange/v1`. Send the key as `Authorization: Bearer sk-liquid-…`. ```python from openai import OpenAI client = OpenAI( base_url="https://router.inference.ai.exchange/v1", api_key="sk-liquid-...", ) ``` Then check three more things: - Change each `model` to a name that `GET /v1/models` lists. A name such as `openai/gpt-oss-120b` does not work. - Move each `provider` setting to the `exchange` object. See [Routing settings](#routing-settings). - Run a dry run, then send a small request. See [Check a request without a purchase](#check-a-request-without-a-purchase). ## What differs - **Model names.** The exchange has no `vendor/model` names. To limit the models to one creator, use `exchange.candidates.creators.allow`. - **Fallbacks.** The `models` list is ignored. Send `architect.basket` with `exchange.candidates.models.allow`. - **Suffixes.** Use `:web` (beta) instead of `:online`. `:nitro` and `:floor` return `404 model_not_found`. To rank the offers, use `:fastest` or `:cheapest`. See [Axis routing](https://inference.ai.exchange/docs/axis-routing.md). - **Money.** The exchange fixes a price cap per job. See below. ## What is different about money The exchange fixes a price cap before the first token. The cap is the most the job can cost. The exchange holds that amount from your workspace balance, charges the tokens used, and returns the rest. See [Quickstart](https://inference.ai.exchange/docs/quickstart.md). - Send `exchange.cap_usd` to set your own cap. An offer with a higher worst-case charge does not compete. - The `x-liquid-cap-usd` header and `exchange.charge.cap_usd` give the cap. `exchange.charge.total_usd` gives the charge. - If the cheapest eligible cap is more than the balance, the response is `402 insufficient_balance`. `needed_usd` is the lowest cap that an offer holds for your limit. If the key reached its spending limit, the response is `402 budget_exhausted`. - If an output limit that you set does not fit your cap, the response is `400 no_eligible_offer` with `param` set to `exchange.cap_usd`. The message gives the lowest cap that fits. - The exchange never lowers an output limit that you set. It refuses the request. - If you set no output limit, the exchange chooses one. It lowers the limit to what an offer allows, and halves it when your balance or cap excludes every offer. It stops at 1,024 tokens. [Output limits](https://inference.ai.exchange/docs/errors.md#output-limits) has the exact rule. ## Check a request without a purchase `POST /v1/exchange/dry-run` shows which offers would compete, in award order, and why the others are left out. It places no job and holds no funds. It takes `model` and an `exchange` object, and it does not read `provider`. ```bash curl https://router.inference.ai.exchange/v1/exchange/dry-run \ -H "Authorization: Bearer $LIQUID_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-oss-120b", "exchange": { "candidates": { "providers": { "allow": ["acme"] } }, "cap_usd": 0.05 } }' ``` ## Example An OpenRouter request: ```json { "model": "openai/gpt-oss-120b", "messages": [{ "role": "user", "content": "Say hello." }], "provider": { "only": ["acme"], "ignore": ["slow-*"], "max_price": { "prompt": 1, "completion": 2 }, "data_collection": "deny", "sort": "price" } } ``` The same request for the exchange. The model name is an illustration: ```json { "model": "gpt-oss-120b", "messages": [{ "role": "user", "content": "Say hello." }], "exchange": { "candidates": { "providers": { "allow": ["acme"], "exclude": ["slow-*"] } }, "limits": { "price": { "input_usd_per_mtok": 1, "output_usd_per_mtok": 2 } }, "privacy": { "retention": "none" }, "rank": { "strategy": { "kind": "price" } }, "cap_usd": 0.05 } } ``` ## Reference ### Model names The `model` field accepts a model, a router model, a basket or a preset. [Connect an app](https://inference.ai.exchange/docs/connect-overview.md) has the table. An unknown name returns `404 model_not_found` with suggestions. The exchange never uses a different model. ### Routing settings The exchange reads a `provider` object and applies each setting that it can apply exactly. [Purchasing rules](https://inference.ai.exchange/docs/rules.md#openrouter-provider) has the only table of every setting. In short: - The exchange maps `max_price.prompt`, `max_price.completion`, `only`, `ignore`, `order`, `data_collection: "deny"`, `zdr: true` and `sort: "price"`. - `order` tries its providers first, then the rest of the market. With `allow_fallbacks: false`, only the providers in `order` compete. See [Preferred providers](https://inference.ai.exchange/docs/provider-preferences.md#openrouter-requests). - `require_parameters` and `allow_fallbacks: true` change nothing. - It refuses every other setting. One `400 unsupported_parameter` answer lists all of them. - `provider` can only narrow what `exchange`, the key preset and the workspace guardrails allow. - An `ignore` name must be a provider handle, or a pattern with `*`. A refused setting can have an `exchange` field to use instead: | Refused `provider` setting | Use instead | | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | `preferred_min_throughput` | `exchange.limits.min_tps` (a limit, not a preference) | | `preferred_max_latency` | `exchange.limits.ttft_ms` or `exchange.limits.completion_ms` (limits) | | `quantizations`, `max_price.request`, `max_price.image`, `allow_fallbacks: false` without `order`, any other `sort`, unknown settings | No equal. Remove the setting, or rank with `exchange.rank.strategy`. | ### Other OpenRouter fields | OpenRouter feature | What the exchange does | | -------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `models` (fallback list) | Ignores it. Use `architect.basket` with `exchange.candidates.models.allow`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#the-basket). | | `route`, `transforms`, `plugins`, `usage` (request field) | Ignores them. The response has the OpenAI `usage` object, and the `exchange` block has the cap and the charge. | | Model suffix `:online` | Use `:web` (beta). The exchange searches the web for the last user message and puts the results in the prompt. See [Axis routing](https://inference.ai.exchange/docs/axis-routing.md#web-search) and [Quickstart](https://inference.ai.exchange/docs/quickstart.md#web-access). | | Model suffixes `:nitro`, `:floor` | Part of the model name, so `404 model_not_found`. Use `:fastest` or `:cheapest`. See [Axis routing](https://inference.ai.exchange/docs/axis-routing.md). | | `HTTP-Referer` and `X-Title` headers | Labels for your receipts. They never change a price, a route or an authorization. | | `reasoning` | Reads `reasoning.effort`, `reasoning.max_tokens`, `reasoning.exclude` and `reasoning_effort`. Matches offers that support the setting, or refuses with `unsupported_parameter`. `exclude` hides the reasoning, and the exchange still bills it. | | `response_format`, `tools`, `tool_choice`, `cache_control` | Applied. Only an offer that declares support can win. See [Structured output](https://inference.ai.exchange/docs/errors.md#structured-output) and [Prompt caching](https://inference.ai.exchange/docs/caching.md). | | `top_p`, `top_k`, `min_p`, `repetition_penalty`, `stop`, `seed`, `logprobs`, `top_logprobs`, `logit_bias`, `frequency_penalty`, `presence_penalty` | Passes them to a provider that declares support. If no offer supports one, `unsupported_parameter`. | | `n` greater than 1 | Refuses it. | ### Troubleshooting | Error | Cause | Fix | | ----------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `400 unsupported_parameter`, `param: "provider"` | A `provider` setting cannot map, or a price is negative or not a number. | Read the message. See [Routing settings](#routing-settings). For `provider.ignore`, use a provider handle. See [Market data](https://inference.ai.exchange/docs/market.md). | | `400 unsupported_parameter`, `param` is another field | No offer for the model supports that parameter. | Remove the parameter, or select another model. | | `404 model_not_found` | The `model` is not on the exchange. | Pick a name from `GET /v1/models`. | | `no_eligible_offer` | The limits of the request, key or workspace excluded each offer. | Read `eligibility.excluded`. Raise `cap_usd`, or relax a limit. Do not retry the same request. See [Errors](https://inference.ai.exchange/docs/errors.md). | | `402 insufficient_balance` | The cheapest eligible cap is more than the balance. | Add funds, or send a lower output limit. | # Move from LiteLLM LiteLLM can send a request to any OpenAI Chat Completions endpoint. The exchange needs a new `model`, `api_base` and `api_key`. Settings that LiteLLM runs by itself, such as `fallbacks` and `num_retries`, need more work. For setup, see [LiteLLM](https://inference.ai.exchange/docs/connect-litellm.md). ## Change these three things 1. Issue a `spend` key on the **API keys** page, and export it as `LIQUID_API_KEY`. See [API keys](https://inference.ai.exchange/docs/keys.md). 2. Set `api_base` to `https://router.inference.ai.exchange/v1` (keep `/v1`, add no path) and `api_key` to the key, not a LiteLLM key. In the proxy, set both in `litellm_params` of each `model_list` entry. 3. Set `model` to `openai/` and a name that `GET /v1/models` lists. In the proxy, keep each `model_name`, so your clients do not change. Then check these: - Move the routing settings to the `exchange` object. See [Settings that change](#settings-that-change) and [LiteLLM features](#litellm-features). - Run a dry run. See [Check a request without a purchase](https://inference.ai.exchange/docs/move-from-openrouter.md#check-a-request-without-a-purchase). - Read [What is different about money](https://inference.ai.exchange/docs/move-from-openrouter.md#what-is-different-about-money). The exchange never lowers an output limit that you set. See [Output limits](https://inference.ai.exchange/docs/errors.md#output-limits). ## What differs - **Retries and fallbacks.** LiteLLM runs them before the request leaves it, and the exchange does not read them. Each retry is a new request with its own cap. To compete several models, send `architect.basket` with `exchange.candidates.models.allow`. - **Budgets and keys.** A LiteLLM budget does not reach the exchange. Set a spending limit on the exchange key. - **Traffic weights.** None. The exchange ranks the eligible offers with `exchange.rank.strategy`. ## Example A LiteLLM call with a retry, a fallback and a label, then the same call for the exchange: ```python import litellm response = litellm.completion( model="gpt-4o", messages=[{"role": "user", "content": "Say hello."}], max_tokens=500, num_retries=2, fallbacks=["gpt-4o-mini"], metadata={"team": "support"}, ) ``` ```python import os import litellm response = litellm.completion( model="openai/architect.basket", api_base="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], messages=[{"role": "user", "content": "Say hello."}], max_tokens=500, extra_headers={"X-Title": "LiteLLM", "x-liquid-tag": "support"}, extra_body={ "exchange": { "candidates": {"models": {"allow": ["gpt-oss-120b", "gpt-oss-20b"]}}, "cap_usd": 0.05, } }, ) ``` The basket replaces `fallbacks`, and the cheapest eligible offer wins. `x-liquid-tag` replaces the `metadata` label. One failure returns one error. If you need retries, add them in your code with an `idempotency-key` header. The model names are illustrations. ## Reference ### Settings that change | LiteLLM setting | Exchange setting | | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `model="openai/"` | `openai/` selects the format. The exchange auctions the name after it: a model, router model, `architect.basket` or preset. | | `extra_body` | The `exchange` object. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). | | `extra_headers` | `X-Title` names the client on your receipts. `x-liquid-*` headers send rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#headers). | | `max_budget` and `budget_duration` | A spending limit on the exchange key, per day, week or month. See [API keys](https://inference.ai.exchange/docs/keys.md#spending-limits). | | `tags`, `metadata.tags`, `x-litellm-tags` | `x-liquid-tag` with one label. See [Cost tags](https://inference.ai.exchange/docs/rules.md#cost-tags). | ### LiteLLM features | LiteLLM feature | What the exchange does | | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `num_retries`, `retry_after` | Not read. To get the first response again, send the same `idempotency-key` on each try. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#headers). | | `fallbacks` | Not read. Send `architect.basket` with `exchange.candidates.models.allow`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#the-basket). | | `routing_strategy`, `weight`, `order`, `rpm`, `tpm`, many entries with one `model_name` | Not read. Rank with `exchange.rank.strategy`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#strategy). | | Virtual keys and their `models` list | Not read. To limit the models of an exchange key, link a preset to it. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#a-preset-linked-to-a-key). | | `timeout` | Not read. `exchange.limits.ttft_ms` and `exchange.limits.completion_ms` limit which offers compete. They are not a client timeout. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#service). | | `user`, `metadata` | `user` and `metadata.user_id` name the end user for [guardrail limits](https://inference.ai.exchange/docs/guardrails.md#end-users). The rest of `metadata` is ignored. | | `tools`, `tool_choice`, `response_format`, `stream`, `stream_options`, `max_tokens`, `max_completion_tokens`, `temperature` | Applied. Only an offer that declares the feature can win. See [Other OpenRouter fields](https://inference.ai.exchange/docs/move-from-openrouter.md#other-openrouter-fields). | | `top_p`, `top_k`, `min_p`, `repetition_penalty`, `stop`, `seed`, `logprobs`, `logit_bias`, `frequency_penalty`, `presence_penalty` | Passed to a provider that declares support. If no offer supports one, `unsupported_parameter`. `n` greater than 1 is refused. | ### Troubleshooting For `402 insufficient_balance` and `400 no_eligible_offer`, see [What is different about money](https://inference.ai.exchange/docs/move-from-openrouter.md#what-is-different-about-money). | Error | Cause | Fix | | ----------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | `401 invalid_api_key` | `api_key` holds a LiteLLM key, or the exchange key is revoked or expired. | Set `api_key` to a current exchange key. See [API keys](https://inference.ai.exchange/docs/keys.md). | | `404 model_not_found` | The name after `openai/` is not on the exchange. | Pick a name from `GET /v1/models`. | | `400 unsupported_parameter`, `param` is a parameter you set | No offer for the model supports that parameter. | Remove the parameter, or select another model. See [Errors](https://inference.ai.exchange/docs/errors.md). | | `402 budget_exhausted` | The exchange key reached its spending limit. A LiteLLM budget does not cause this. | Raise the limit of the key. See [API keys](https://inference.ai.exchange/docs/keys.md#spending-limits). | | `503 model_unavailable` | No seller can serve the model now. The response has `Retry-After: 30`. | Try again later, or select another model. | # Move from the OpenAI API The exchange serves Chat Completions and a stateless part of Responses. Most OpenAI code changes in two places: the base URL and the API key. Model names, and features that need OpenAI to keep state, need more work. For setup, see [OpenAI SDK](https://inference.ai.exchange/docs/connect-openai-sdk.md). ## Change these three things 1. Issue a `spend` key on the **API keys** page. See [API keys](https://inference.ai.exchange/docs/keys.md). 2. Set the base URL to `https://router.inference.ai.exchange/v1` (or `OPENAI_BASE_URL`). Send the key as `Authorization: Bearer sk-liquid-…` (or `OPENAI_API_KEY`). 3. Change each `model` to a name that `GET /v1/models` lists. See [Model names](#model-names). Then check these: - Remove each parameter that the exchange refuses. See [OpenAI features](#openai-features). - Run a dry run. See [Check a request without a purchase](https://inference.ai.exchange/docs/move-from-openrouter.md#check-a-request-without-a-purchase). - Read [What is different about money](https://inference.ai.exchange/docs/move-from-openrouter.md#what-is-different-about-money). The exchange never lowers an output limit that you set. See [Output limits](https://inference.ai.exchange/docs/errors.md#output-limits). ## What differs - **Model names.** An unknown name, or a fine-tuned model, returns `404 model_not_found`. - **`n` greater than 1.** Refused. OpenAI bills every choice, and the exchange serves one. - **State in Responses.** `previous_response_id`, `conversation` and `background: true` are refused. Send the full conversation in `input`. - **Hosted tools.** The exchange runs none. It removes `web_search` with the header `x-liquid-notice: web_search_dropped` and refuses the others. To search the web, add `:web` (beta) to the model name. - **Not available.** The Batch, Files, Assistants and Moderations APIs, and image edits. - **Retries.** The SDK retries a `5xx` response, and each try is a new request. To get the first response again, send the same `idempotency-key` header. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#headers). With `with_raw_response`, the SDK returns the headers, including `x-liquid-cap-usd`. ## Example An OpenAI request, then the same request for the exchange. The new request changes the client and the model, removes `n=2` (refused) and `store` and `service_tier` (ignored), and adds a cap and a data rule. `user` stays, because it names the end user for guardrail limits. The model name is an illustration. ```python from openai import OpenAI client = OpenAI() response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "Say hello."}], max_completion_tokens=500, n=2, store=True, service_tier="auto", user="user-123", ) ``` ```python import os from openai import OpenAI client = OpenAI( base_url="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], ) response = client.chat.completions.create( model="gpt-oss-120b", messages=[{"role": "user", "content": "Say hello."}], max_completion_tokens=500, user="user-123", extra_body={"exchange": {"cap_usd": 0.05, "privacy": {"retention": "none"}}}, ) ``` ## Reference ### Model names Send a name that `GET /v1/models` returns. The `model` field accepts a model, a router model, a basket or a preset. See [Model names](https://inference.ai.exchange/docs/move-from-openrouter.md#model-names). The exchange never uses a different model. `max_completion_tokens`, `max_tokens` and `max_output_tokens` (Responses) are all accepted. Reasoning tokens count toward the limit. ### OpenAI features | OpenAI feature | What the exchange does | | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `stream`, `stream_options` | Applied. Set `stream_options: {"include_usage": true}` to get token usage in a stream. | | `tools`, `tool_choice` | Applied. Only an offer that declares tool support can win. | | `parallel_tool_calls`, `service_tier`, `include`, `truncation`, `safety_identifier`, `metadata` | Ignored. For `metadata`, send `x-liquid-tag` to label spend. See [Cost tags](https://inference.ai.exchange/docs/rules.md#cost-tags). An input too long for an offer excludes it. | | `response_format` | Applies `json_schema` and `json_object`. Only an offer that declares support can win. See [Structured output](https://inference.ai.exchange/docs/errors.md#structured-output). | | `reasoning_effort`, `reasoning.effort` (Responses) | Applied to offers that support the level, or `unsupported_parameter`. | | `temperature` | Applied. | | `top_p`, `top_k`, `min_p`, `repetition_penalty`, `stop`, `seed`, `logprobs`, `top_logprobs`, `logit_bias`, `frequency_penalty`, `presence_penalty` | Passed to a provider that declares support. If no offer supports one, `unsupported_parameter`. `/v1/responses` refuses `logprobs` and `top_logprobs`. | | `n` greater than 1 | `400 unsupported_parameter`, `param` is `n`. | | `store` | Ignored. The exchange keeps no response. To limit what a provider keeps, send `exchange.privacy.retention: "none"`. See [Data handling](https://inference.ai.exchange/docs/data-handling.md). | | `user` | Names the end user for the limits of a [guardrail](https://inference.ai.exchange/docs/guardrails.md#end-users). The `X-Liquid-End-User` header wins. | | `prompt_cache_key` | Read into the cache plan of the job. See [Prompt caching](https://inference.ai.exchange/docs/caching.md). | | `previous_response_id`, `conversation`, `background: true` (Responses) | `400 unsupported_parameter`. Send the full conversation in `input`. | | Hosted tool `web_search` (Responses) | Removed, with `x-liquid-notice: web_search_dropped`. No search runs. Add `:web` (beta) to the model name. See [Quickstart](https://inference.ai.exchange/docs/quickstart.md#web-access). | | Other hosted tools: `file_search`, `code_interpreter`, `mcp` | `400 unsupported_parameter`, `param` such as `tools[1]`. Declare your own function. | | Embeddings, image generation, speech and video endpoints | Served. See [Embeddings](https://inference.ai.exchange/docs/embeddings.md), [Modalities](https://inference.ai.exchange/docs/modalities.md) and [Move from the Sora API](https://inference.ai.exchange/docs/move-from-sora.md). | | Batch, Files, Assistants, Moderations, image edits, fine-tuning (`ft:` names) | Not available. Send each request on its own. | ### Troubleshooting For `402 insufficient_balance` and `400 no_eligible_offer` with `param` set to `exchange.cap_usd`, see [What is different about money](https://inference.ai.exchange/docs/move-from-openrouter.md#what-is-different-about-money). | Error | Cause | Fix | | -------------------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `404 model_not_found` | The `model` is an OpenAI name that the exchange does not list. | Pick a name from `GET /v1/models`. | | `400 unsupported_parameter`, `param` is `n` | The request asks for more than one choice. | Remove `n`. | | `400 unsupported_parameter`, `param` is `previous_response_id` | The Responses request asks the exchange to keep state. | Send the full conversation in `input`. | | `400 unsupported_parameter`, `param` is `tools[...]` | The request has a hosted tool that the exchange does not run. | Remove the tool, or declare your own function. | | `400 unsupported_parameter`, `param` is another field | No offer for the model supports that parameter. | Remove the parameter, or select another model. See [Errors](https://inference.ai.exchange/docs/errors.md). | | `402 insufficient_balance` | The cheapest eligible cap is more than the balance. | Add funds, or send a lower output limit. | | `429 rate_limited` | Too many requests from the address, or a guardrail rate limit refused the call. | Wait for `Retry-After`. OpenAI limits do not apply here. See [Guardrails](https://inference.ai.exchange/docs/guardrails.md#rate-limits). | | `503 provider_failed` or `503 model_unavailable` | The awarded provider failed, or no seller can serve the model now. | Set `max_retries` yourself, and send the same `idempotency-key` on a retry. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#headers). | # Move from the Anthropic API The exchange serves `POST /v1/messages` in the Anthropic format. The official clients work with a new base URL and a new key. Server tools and the batch and file endpoints need more work. For setup, see [Anthropic SDK](https://inference.ai.exchange/docs/connect-anthropic-sdk.md) and [Claude Code](https://inference.ai.exchange/docs/connect-claude-code.md). ## Change these three things 1. Issue a `spend` key on the **API keys** page. See [API keys](https://inference.ai.exchange/docs/keys.md). 2. Set the base URL to `https://router.inference.ai.exchange`, with no `/v1`. The client adds `/v1/messages`, so a URL that ends in `/v1` gets a 404. 3. Change each `model` to a name that `GET /v1/anthropic/models` lists. See [Model names](#model-names). Then check these: - Keep `max_tokens` in each request. The exchange requires it. - Remove each server tool. See [Anthropic features](#anthropic-features). - Run a [dry run](https://inference.ai.exchange/docs/move-from-openrouter.md#check-a-request-without-a-purchase). - Read [What is different about money](https://inference.ai.exchange/docs/move-from-openrouter.md#what-is-different-about-money). The exchange never lowers `max_tokens` to fit your balance or cap. ## What differs - **Model names.** The exchange does not forward a Claude name to one provider. An unknown name returns `404 model_not_found`. - **Server tools.** `web_search_*`, `web_fetch`, `code_execution` and other hosted tools return `400 unsupported_parameter`. To search the web, add `:web` (beta) to the model name. - **Documents.** A `document` block must have a `base64` source. A `url` source returns `400 unsupported_content`. - **Not available.** The Message Batches API and the Files API. ## Example An Anthropic request, then the same request for the exchange. The new request changes the client and model, removes the refused `web_search` tool and adds a cap. The model name is an illustration. ```python import anthropic client = anthropic.Anthropic() message = client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, system="Answer in one sentence.", messages=[{"role": "user", "content": "What is new in Rust?"}], tools=[{"type": "web_search_20260209", "name": "web_search"}], metadata={"user_id": "user-123"}, ) ``` ```python import os import anthropic client = anthropic.Anthropic( base_url="https://router.inference.ai.exchange", api_key=os.environ["LIQUID_API_KEY"], ) message = client.messages.create( model="liquid.auto", max_tokens=1024, system="Answer in one sentence.", messages=[{"role": "user", "content": "What is new in Rust?"}], metadata={"user_id": "user-123"}, extra_body={"exchange": {"cap_usd": 0.05}}, ) ``` ## Reference ### Settings that change - Both `x-api-key` and `Authorization: Bearer` work with an exchange key, `sk-liquid-…`. - `anthropic-version` is accepted and not required. On `GET /v1/models`, it selects the Anthropic format. - `anthropic-beta` is accepted and not read. - `max_tokens` is required. The exchange can lower it to the limit of the offers that can take the job. See [Output limits](https://inference.ai.exchange/docs/errors.md#output-limits). ### Model names Send a name that `GET /v1/anthropic/models` returns, a router model, a basket or a preset. See [Connect an app](https://inference.ai.exchange/docs/connect-overview.md). An unknown name returns `404 model_not_found`. The exchange never uses a different model. Send the `exchange` object in the body. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ### Anthropic features | Anthropic feature | What the exchange does | | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `system` (a string or a list of blocks) | Applied. It becomes the first message. | | `stream` | Applied, in the Anthropic event format. The final event has the `exchange` block. | | `stop_sequences`, `temperature` | Applied. An offer must support `stop_sequences` to compete. | | `top_p`, `top_k` | Passed to a provider that declares support. If no offer supports it, `unsupported_parameter`. | | `metadata.user_id` | Names the end user for the limits of a [guardrail](https://inference.ai.exchange/docs/guardrails.md#end-users). | | `service_tier`, `container`, any parameter not in this table | Ignored or accepted and not sent. To label spend, send `x-liquid-tag`. See [Cost tags](https://inference.ai.exchange/docs/rules.md#cost-tags). | | `tools` with an `input_schema`, and `tool_choice` | Applied. Types `auto`, `any`, `tool` and `none` work. `disable_parallel_tool_use` is not read. Only an offer that declares tool support can win. | | Typed client tools `bash`, `text_editor`, `computer`, `memory` | Applied as functions. Your client runs the tool. A tool with no `input_schema` gets an empty object schema, so declare your own. | | Server tool `web_search_*` | `400 unsupported_parameter`, `param` such as `tools[0]`. Add `:web` (beta) to the model name instead. See [Quickstart](https://inference.ai.exchange/docs/quickstart.md#web-access). | | Other typed tools: `web_fetch`, `code_execution`, `tool_search` | `400 unsupported_parameter`. The exchange runs no hosted tool. Run the work in your own code. | | `thinking` with `budget_tokens` | Applied as a reasoning budget. Only an offer that supports a budget can win. `max_tokens` is never lowered to or below the budget. Thinking blocks come back only when it is on. | | `output_config.effort` and `output_config.format` | Applied: a reasoning level, and a `json_schema` that the answer must follow. Other keys are accepted and not sent. | | `cache_control` on a block, and at the top level | Applied. See [Prompt caching](https://inference.ai.exchange/docs/caching.md). | | `image` blocks | Applied to offers that declare image input. The source can be `base64` or a `url`. See [Modalities](https://inference.ai.exchange/docs/modalities.md). | | `document` blocks | Applied to offers that declare file input. The source must be `base64`, because the exchange measures the file before it sets the cap. | | Any other content block in a user turn | `400 unsupported_content`. The message names the block. | | `POST /v1/messages/count_tokens` | An estimate from the exchange, not a provider tokenizer. It counts the text of `system` and `messages`, not tools, images or documents. | | Message Batches API, Files API | Not available. Send each request alone, and send a document as `base64`. | ### Troubleshooting For `402 insufficient_balance` and `400 no_eligible_offer`, see [What is different about money](https://inference.ai.exchange/docs/move-from-openrouter.md#what-is-different-about-money). `error.type` follows the HTTP status. Match on `code`. | Error | Cause | Fix | | ----------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------- | | `404` for `/v1/v1/messages` | The base URL ends in `/v1`. | Remove `/v1`. | | `401 authentication_error` | The key is wrong, revoked or expired. | Send a current key in `x-api-key`. See [API keys](https://inference.ai.exchange/docs/keys.md). | | `404 not_found_error`, `code` is `model_not_found` | The `model` is not on the exchange. | Pick a name from `GET /v1/anthropic/models`. | | `400`, `code` is `unsupported_parameter`, `param` is `tools[...]` | The request has a hosted tool. | Remove the tool. | | `400`, `code` is `unsupported_content` | A block type is unknown, or a `document` has a `url` source. | Send the document as `base64`, or remove the block. | | `stop_reason` is `max_tokens` and the answer has no text | A model that thinks by default used the output limit for thinking. | Send a larger `max_tokens`. | | `503 overloaded_error` | The awarded provider failed, or no seller can serve the model now. | Try again later, or select another model. See [Errors](https://inference.ai.exchange/docs/errors.md#auction). | # Move from Portkey Portkey keeps its routing rules in a config that a request names in the `x-portkey-config` header. The exchange has no config object and reads no `x-portkey-*` header. It takes its rules in an `exchange` object in the request body, or from a preset. The move is more than a new base URL. ## Change these four things 1. Issue a `spend` key on the **API keys** page. See [API keys](https://inference.ai.exchange/docs/keys.md). 2. Use a client that sends the OpenAI format. Set its base URL to `https://router.inference.ai.exchange/v1`, and send the key as `Authorization: Bearer sk-liquid-…`. See [OpenAI SDK](https://inference.ai.exchange/docs/connect-openai-sdk.md). 3. Remove the `x-portkey-*` headers. The `x-liquid-job-id` response header gives the id of each job. 4. Change each `model` to a name that `GET /v1/models` lists. See [Model names](#model-names). Then: 1. Move each config to an `exchange` object or a preset. See [Settings that change](#settings-that-change) and [Portkey features](#portkey-features). 2. Run a dry run. See [Check a request without a purchase](https://inference.ai.exchange/docs/move-from-openrouter.md#check-a-request-without-a-purchase). 3. Read [What is different about money](https://inference.ai.exchange/docs/move-from-openrouter.md#what-is-different-about-money). The exchange never lowers an output limit that you set. See [Output limits](https://inference.ai.exchange/docs/errors.md#output-limits). ## What differs - **Providers.** You hold no provider key. A name such as `@provider-slug/model-name` does not work. To limit the providers, use `exchange.candidates.providers.allow` with provider handles. - **Fallback.** If the awarded provider fails before any output, the exchange tries the next offer. It never does so after output starts. To compete several models, send `architect.basket`. - **Retries, load balancing, guardrails and cache.** See [Portkey features](#portkey-features). ## Example A Portkey config with a fallback, a retry and a cache: ```json { "strategy": { "mode": "fallback", "on_status_codes": [429, 503] }, "targets": [ { "override_params": { "model": "@openai-prod/gpt-4o" } }, { "override_params": { "model": "@anthropic-prod/claude-sonnet-4-6" } } ], "retry": { "attempts": 3 }, "cache": { "mode": "simple", "max_age": 600 } } ``` The same rules for the exchange. The basket replaces the targets: the two models compete, and the lowest estimated charge wins. The cache headers replace `cache`. One failure returns one error. The model names are illustrations. ```bash curl https://router.inference.ai.exchange/v1/chat/completions \ -H "Authorization: Bearer $LIQUID_API_KEY" \ -H "x-liquid-cache: true" \ -H "x-liquid-cache-ttl: 600" \ -H "x-liquid-tag: support" \ -H "Content-Type: application/json" \ -d '{ "model": "architect.basket", "messages": [{"role": "user", "content": "Say hello."}], "exchange": { "candidates": { "models": { "allow": ["gpt-oss-120b", "gpt-oss-20b"] } }, "rank": { "strategy": { "kind": "price" } }, "cap_usd": 0.05 } }' ``` To save these rules, make a preset on the **Presets** page and send its name as `model`. ## Reference ### Settings that change | Portkey setting | Exchange setting | | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `x-portkey-provider`, `provider`, `x-portkey-virtual-key` and `virtual_key` | No equal. Use `exchange.candidates.providers.allow` or `exchange.candidates.providers.exclude` with provider handles. A Portkey slug is not a handle. | | `x-portkey-config` and `config` | An `exchange` object in the body, or a preset that you name as `model`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md) and [Presets](https://inference.ai.exchange/docs/rules.md#presets). | | `x-portkey-metadata` | `x-liquid-tag` with one label. See [Cost tags](https://inference.ai.exchange/docs/rules.md#cost-tags). | ### Model names The exchange does not use the `@provider-slug/model-name` form. Send a name that `GET /v1/models` returns, a router model, a basket or a preset. See [Connect an app](https://inference.ai.exchange/docs/connect-overview.md). ### Portkey features | Portkey feature | What the exchange does | | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `strategy.mode` `fallback`, `targets`, `on_status_codes` | Not read. Send `architect.basket` with `exchange.candidates.models.allow`, and rank with `exchange.rank.strategy`. See [Errors](https://inference.ai.exchange/docs/errors.md#auction). | | `strategy.mode` `loadbalance`, `weight`, `conditional` | Not read or not available. The exchange awards each job to one offer by rank. Choose the `model` in your own code. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#strategy). | | `retry` with `attempts` | Not read. If your code retries, send the same `idempotency-key` on each try, so that no second job runs. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#headers). | | `x-portkey-request-timeout` | Not read. `exchange.limits.ttft_ms` and `exchange.limits.completion_ms` limit which offers compete. A job that misses the deadline is still charged, and its receipt shows `missed`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#service). | | `cache` with `mode` `simple` | Send `x-liquid-cache: true`, or set `exchange.cache.response` in the rules. An identical request from the same key gets the stored answer. The entry lives 1 to 86,400 seconds. See [Response caching](https://inference.ai.exchange/docs/response-caching.md). | | `cache` with `mode` `semantic` | Not available. Only an identical request matches. | | `x-portkey-cache-force-refresh`, `x-portkey-cache-namespace` | Not read. `x-liquid-cache-clear: true` deletes the entry and runs the request again. A key never shares an entry with another key. | | `input_guardrails`, `output_guardrails`, `before_request_hooks` | Not read. The workspace guardrails apply to every call. There is no output check. See [Guardrails](https://inference.ai.exchange/docs/guardrails.md). | | `override_params`, `default_params`, `drop_params` of a target | Not available. Put the `model` and the parameters in the request. | | `tools`, `response_format`, `stream` | Applied. See [Other OpenRouter fields](https://inference.ai.exchange/docs/move-from-openrouter.md#other-openrouter-fields). | ### Troubleshooting For `402 insufficient_balance` and `400 no_eligible_offer` with `param` set to `exchange.cap_usd`, see [What is different about money](https://inference.ai.exchange/docs/move-from-openrouter.md#what-is-different-about-money). | Error | Cause | Fix | | ----------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | `404 model_not_found` | The `model` has a `@provider-slug/` prefix, or is not on the exchange. | Pick a name from `GET /v1/models`. | | `400 invalid_request`, message names a field of `exchange` | The `exchange` object has a Portkey key or an unknown field. | Use only the fields in [Purchasing rules](https://inference.ai.exchange/docs/rules.md). | | `no_eligible_offer`, `binding_rule` is `allowed_providers` | No provider in the allowlist quotes the model. A Portkey slug causes this. | Use provider handles. See [Market data](https://inference.ai.exchange/docs/market.md). | | `400 unsupported_parameter`, `param` is a parameter you set | No offer for the model supports that parameter. | Remove the parameter, or select another model. See [Errors](https://inference.ai.exchange/docs/errors.md). | | `402 insufficient_balance` | The cheapest eligible cap is more than the balance. | Add funds, or send a lower output limit. | # What changed in the API This page is for people who already have a client. It lists what is new in release 3.7.0, which older names still work, and the changes that can alter the result of a request that you already send. ## Two changes in behavior 1. **Image `resolution` and `aspect_ratio` now take effect.** Before, the exchange dropped both fields. Now the exchange picks a pixel size that an offer declared, and it buys, prices and dispatches that size. The size can change the charge. If no declared size has the tier and the shape, the answer is `400 unsupported_parameter`. If `size` disagrees with them, the answer is `400 conflicting_parameters`. A client that sends only `size` does not change. See [Image generation](https://inference.ai.exchange/docs/image-generation.md#tier-and-shape). 2. **A video job past the video job limit ends as `expired`.** Before, it ended as `failed`. The limit is 10 minutes after dispatch by default. An `expired` job costs nothing, and the exchange sends the `video.generation.expired` callback. See [Video generation](https://inference.ai.exchange/docs/video-generation.md#status). A client that reads the status of a video must also handle two new values: `cancelled` and `expired`. ## New in 3.7.0 A control works only when an offer declares it. If no offer did, the answer is `400 unsupported_parameter` and `param` names the feature. See [Errors](https://inference.ai.exchange/docs/errors.md). | Area | New | Page | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | Images | `resolution`, `aspect_ratio`, `seed`, `background`, `output_compression`, `stream` with server-sent events | [Image generation](https://inference.ai.exchange/docs/image-generation.md) | | Chat | `image_config` for images in a chat answer | [Image generation](https://inference.ai.exchange/docs/image-generation.md) | | Video | `aspect_ratio`, `resolution`, `seed`, `creativity`, `upscale_factor`, `previous_job_id`, `cancel`, the states `cancelled` and `expired`, `has_audio` | [Video generation](https://inference.ai.exchange/docs/video-generation.md) | | Speech | `instructions`, `input` as an array of turns, `voice` is optional | [Speech](https://inference.ai.exchange/docs/speech.md) | | Transcription | `diarize`, `keyterms`, audio by `https` link | [Speech-to-text](https://inference.ai.exchange/docs/speech-to-text.md) | | Embeddings | `input_type` | [Embeddings](https://inference.ai.exchange/docs/embeddings.md) | | References | One `input_references` list with a `role` on every media endpoint | [Reference inputs](https://inference.ai.exchange/docs/reference-inputs.md) | | Rules | Grouped `exchange` options, and `cap_nanos` | [Purchasing rules](https://inference.ai.exchange/docs/rules.md) | | Receipt | `*_nanos` amounts, `supplier_*` amounts, new token names, `usage.pricing` | [Receipts](https://inference.ai.exchange/docs/receipts.md) | | Model card | `modality`, `parameters` descriptors, `pricing`, unit names | [Modalities](https://inference.ai.exchange/docs/modalities.md#read-a-model-card) | ## Older names that still work The exchange reads the older name of a request field and maps it to the new name. Each response carries the older name next to the new one, with the same value. Send both with different values and the answer is `400 conflicting_parameters`. New code should use the new names. | Older name | New name | | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | `images`, `image`, `references` on images; `frame_images`, `input_reference` on video; `input_audio` on transcription | `input_references` | | `mime`, `mime_type`, `format` on a reference | `media_type` | | `mime_type` on an image answer and its events | `media_type` | | Flat `exchange` fields, such as `allowed_providers`, `ttft_ms`, `max_output_usd_per_mtok` | The groups `candidates`, `rank`, `limits`, `privacy`, `cache` and `session` | | `tokens_in`, `tokens_cached`, `tokens_cache_write`, `tokens_out` in `exchange.usage` | `prompt_tokens`, `cached_tokens`, `cache_write_tokens`, `completion_tokens` | | `max_tokens`, `min_input_rate`, `min_output_rate` on a model card | `max_output_tokens`, `min_input_usd_per_mtok`, `min_output_usd_per_mtok` | | `rate_per_image`, `rate_per_video_second` and the other `rate_*` fields of an offer | `pricing`, a list of prices | The flat `exchange` names are listed in [Purchasing rules](https://inference.ai.exchange/docs/rules.md). `cap_usd` keeps its name. `cap_nanos` is the same cap as an integer of nano-USD. Send one of the two, or send both with the same value. An older name that the endpoint did not read is still refused. For example, `mask` on `POST /v1/images/edits` gets `400`, as before. ## Check your client 1. Read the status of a video with a `switch` that has a case for `cancelled` and `expired`. 2. If you send `resolution` or `aspect_ratio` for an image, compare the size and the charge with your last results. 3. Move the older names in the table to the new names when you next edit the code. 4. Read the model card for `parameters` before you send a control. See [Modalities](https://inference.ai.exchange/docs/modalities.md#read-a-model-card). # Move from the Sora API OpenAI shut down the Sora 2 models and its Videos API on 24 September 2026. The exchange serves the same three routes: create a video, read its status and download the file. Change the base URL, the API key and the model. The poll loop and the download stay the same. The models are not Sora, so one prompt gives a different clip. Test your prompts first. ## Change these four things 1. Issue a `spend` key on the **API keys** page. See [API keys](https://inference.ai.exchange/docs/keys.md). 2. Set the base URL to `https://router.inference.ai.exchange/v1` (or `OPENAI_BASE_URL`). Send the key as `Authorization: Bearer sk-liquid-…` (or `OPENAI_API_KEY`). 3. Change `model` from `sora-2` or `sora-2-pro` to a video model such as `seedance-1-5-pro`. See [Model names](#model-names). 4. Set a price cap with the `x-liquid-cap-usd` header. See [Set a cap](#set-a-cap). Then remove each [Sora feature](#sora-features) that the exchange lacks, check your [length and size](#length-and-size), and [poll less often](#poll-the-status). ## What differs - **Model names.** `sora-2` and `sora-2-pro` return `404 model_not_found`. The exchange refuses a request without `model`. - **Size.** Many models render only their default size and refuse any `size`. Remove it. - **Webhooks.** Send `callback_url` to get a call when the job ends, or poll the status route. See [Video generation](https://inference.ai.exchange/docs/video-generation.md#callback). - **Not available:** remix, edits, characters, thumbnails, spritesheets, list and delete. Keep the id of each video. - **Retries.** The SDK tries a failed create again, and each try can buy a second video. Send an `idempotency-key` header. A repeated create with the key starts no second job. - **Money.** The price is for each second of video. The exchange fixes the charge before the job starts, and a failed job costs nothing. ## Example ### Python The Sora call is `OpenAI().videos.create_and_poll(model="sora-2", ...)`. The same call for the exchange: ```python import os from openai import OpenAI client = OpenAI( base_url="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], ) video = client.videos.create_and_poll( model="seedance-1-5-pro", prompt="a red paper boat on a stream", seconds="8", size="1280x720", poll_interval_ms=10_000, extra_headers={"x-liquid-cap-usd": "2.00", "idempotency-key": "boat-001"}, ) if video.status == "completed": client.videos.download_content(video.id).write_to_file("boat.mp4") else: print(video.error) ``` The model name is an illustration. The SDK marks the video methods as deprecated, but they still work. ### curl The full request, with `jq` to read the video `id`. It polls each 10 seconds, prints the result and downloads the file: ```bash id=$(curl -s https://router.inference.ai.exchange/v1/videos \ -H "Authorization: Bearer $LIQUID_API_KEY" \ -H "x-liquid-cap-usd: 2.00" \ -H "idempotency-key: boat-001" \ -F model="seedance-1-5-pro" \ -F prompt="a red paper boat on a stream" \ -F seconds="8" \ -F size="1280x720" | jq -r '.id // error(.error.message)') while [ "$(curl -s https://router.inference.ai.exchange/v1/videos/$id \ -H "Authorization: Bearer $LIQUID_API_KEY" | jq -r .status)" = in_progress ]; do sleep 10 done curl -s https://router.inference.ai.exchange/v1/videos/$id \ -H "Authorization: Bearer $LIQUID_API_KEY" | jq '{status, error, exchange}' curl -f https://router.inference.ai.exchange/v1/videos/$id/content \ -H "Authorization: Bearer $LIQUID_API_KEY" --output boat.mp4 ``` ## Reference ### Model names Send the name of a model that renders video. `GET /v1/exchange/models` lists each model, and a video model has `video` in `output_modalities`. The exchange does not use a different model in place of a Sora name. The `video` entry gives the lengths and sizes that the offers render. For example, `seedance-1-5-pro` renders 4 to 12 seconds and accepts a `size`. A model with `default_size_only: true`, such as `veo3.1`, renders only its default size. A chat deployment is excluded with the reason `job_kind_not_served`. ### Set a cap The price is for each second of video. So the cap that the exchange fixes is exactly the charge. Set `x-liquid-cap-usd` to the most that you agree to pay for one video. An offer whose charge is more than the cap cannot win. The OpenAI SDK sends `videos.create` as a form, not as JSON. In a form, the exchange reads the `exchange` block only as one field that holds JSON text. It ignores fields such as `exchange[cap_usd]`, which the SDK makes from `extra_body={"exchange": {...}}`. Use headers, or send the block as text: ```python import json extra_body = {"exchange": json.dumps({"cap_usd": 2.0, "privacy": {"retention": "none"}})} ``` For several rules in one header, use `x-liquid-rules`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#headers). ### Length and size | Sora setting | Exchange setting | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `seconds`: `4`, `8` or `12` | From 1 to 30, as a number or a string. An offer that declares its lengths must list the value. Default: the shortest declared length, or 4. | | `size`, such as `720x1280` | `x`. An offer that declares its sizes must list the value. Without `size`, the provider uses its default size. | | `input_reference`, a file | The image that the video starts from. Only an offer that reads image input can win. The image must have the shape of the video within 1%. See Start image in [Video generation](https://inference.ai.exchange/docs/video-generation.md). | | `input_reference`, `{"image_url"}` | Applied in a JSON body. The URL is `https:` or `data:image/`. `{"file_id"}` is refused, because the exchange has no Files API. | For more reference inputs, such as audio or a second image, send `references`. See [Start from reference inputs](https://inference.ai.exchange/docs/reference-inputs.md). ### Poll the status `POST /v1/videos` returns when a provider accepts the job. The response has `status: "in_progress"` and the `x-liquid-cap-usd` and `x-liquid-job-id` headers. The video `id` is the job id. Send `GET /v1/videos/{id}` until `status` is `completed` or `failed`. A render takes up to several minutes, so poll each 10 to 20 seconds. `create_and_poll` polls each second unless you set `poll_interval_ms`. When the job ends, the response holds the `exchange` block with the charge and `video_seconds`. A failed job holds `error` with `code` and `message`. Send `GET /v1/videos/{id}/content` to download the file. The provider that rendered the video serves the file, and the exchange keeps nothing. You can download it again until `expires_at`, if the response states it. Only your account can read it. ### Sora features | Sora feature | What the exchange does | | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `POST /v1/videos`, JSON or `multipart/form-data` | Applied. `prompt` can have up to 32,000 characters. | | `GET /v1/videos/{id}` | Applied. `status` is `in_progress`, `completed` or `failed`, never `queued`. `progress` is 0 until complete, then 100. | | `GET /v1/videos/{id}/content` | Applied. `404 video_not_ready` before the job is complete, `410 video_expired` after the provider deletes the file. | | `variant=thumbnail` or `spritesheet`, unknown fields | Ignored. The route sends the video. | ### Troubleshooting | Error | Cause | Fix | | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `404 model_not_found` | The `model` is a Sora name, or a name that the exchange does not list. | Pick a video model. See [Model names](#model-names). | | `400`, `param` is `seconds` or `size` | The length is not from 1 to 30, or the size is not `x`. | Send a value in range, such as `seconds="8"` and `size="1280x720"`. | | `400`, `param` is `input_reference` | The start image does not have the shape of the video, or the exchange could not read its link. | Crop or pad the image, or send a `size` with the shape of the image. | | `400 no_eligible_offer` | The cap is less than the charge of each offer. | Raise the cap, or ask for fewer seconds. | | `503 no_eligible_offer` or `400 unsupported_parameter` | No offer for the model renders the length, size or reference input. | Read `eligibility.excluded` in the error. Change the length, remove `size`, or pick another model. | | `402 insufficient_balance` | The charge is more than the balance. `needed_usd` and `balance_usd` give both. | Add funds, or ask for fewer seconds. | | `404 video_not_ready` on the download | The job is not complete. | Poll until `completed`. | | `410 video_expired` | The provider deleted the file. | Download soon after the job is complete. | | `503 provider_unavailable` on the download | The provider that rendered the video is not connected. Only it has the file. | Try again later. | | `409 idempotency_no_result` or `idempotency_conflict` | A retry used the idempotency key of an earlier create. The SDK sends a new form on each try, so the body differs. | Do not create the video again. Poll the job id from the first response. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md#headers). | # All tutorials A tutorial takes one task from a clean start to a working result. Each page lists what you need first and shows how to check that the task worked. For reference pages, use the [guides](https://inference.ai.exchange/docs/quickstart.md). ## Buy - [Run a coding agent on a budget](https://inference.ai.exchange/docs/tutorial-spend-cap.md): start Claude Code on the exchange with a key that cannot spend more than you set. ## Build - [Create and showcase a project](https://inference.ai.exchange/docs/tutorial-showcase-project.md): record your coding sessions, publish a project page and share what you built. - [Make a storyboard with Claude Code](https://inference.ai.exchange/docs/tutorial-storyboard.md): use Liquid MCP and Claude Code to write a script and draw each frame. - [Build semantic search](https://inference.ai.exchange/docs/tutorial-semantic-search.md): embed your notes through the exchange and search them by meaning. - [Transcribe a meeting and summarize it](https://inference.ai.exchange/docs/tutorial-transcribe-summarize.md): turn an audio file into text, then into a short summary with action items. ## Sell - [Sell your first model](https://inference.ai.exchange/docs/tutorial-first-sale.md): register as a provider, quote one model and see your first award. ## Next steps - [Quickstart](https://inference.ai.exchange/docs/quickstart.md): make a key and send a request. - [Connect an app](https://inference.ai.exchange/docs/connect-overview.md): use the exchange from your own tools. # Run a coding agent on a budget Start Claude Code on the exchange with a key that cannot spend more than you set. In about ten minutes, you start Claude Code through `liquid` with a key limited to $5 each day. Then you read the receipt of your first job. ## Before you start - You need a workspace with a balance. See the [Quickstart](https://inference.ai.exchange/docs/quickstart.md). - You must be an owner or an admin of the workspace. Only they can make keys. - Claude Code must be on your `PATH`. The installer supports macOS and Linux. ## Steps 1. Install the Liquid CLI. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash ``` The installer checks the checksum and puts `liquid` in `~/.local/bin`. 2. Sign in with permission to manage keys. ```bash liquid login --keys ``` Your browser opens. Approve the request. The terminal shows that you are signed in. This token lasts one day and cannot place jobs. 3. Make a key with a spending limit and save it for your coding agents. ```bash liquid keys create agent --spend-limit 5 --period day --save ``` The terminal prints `created agent` and the key id. The command does not print a saved key, so you cannot lose it in your scrollback. The limit is $5 each day. When the key reaches it, jobs fail with `402 budget_exhausted`, even if the workspace has more balance. Use `--period week` or `--period month` for a longer period. 4. Start Claude Code with a cap on each request. ```bash liquid claude --model liquid/auto:cheapest --cap 0.50 ``` Claude Code opens. `--cap` is the most that one request can cost in USD. The key limit bounds the total. The cap bounds each request. 5. Give the agent one small task. For example: ```text Explain what this repository does in five lines. ``` 6. Leave Claude Code. When the session ends, `liquid` prints the number of requests, the cost and the largest charge against your cap. ## Check it works List your latest jobs: ```bash liquid job ``` Each row shows the job id, the model, the provider and the amount charged. Type to filter and press Enter to open one job. You can also pass the id: ```bash liquid job ``` Look for three things: - **Provider.** This is the provider that answered. - **Outcome.** A normal job shows `completed`. - **Charged.** The exchange charged the tokens used. It is never more than your cap. The receipt adds the supplier charge and the fee. See [Receipts](https://inference.ai.exchange/docs/receipts.md) for every field. To check the limit itself, open **API keys** in the console. The key `agent` shows its limit and period. ## Add a guardrail (optional) A spending limit stops one key. A guardrail also limits which offers can serve a call, and it can block sensitive text. 1. Open **Rules** in the console, then **Guardrails**. 2. Choose **New guardrail** and select the key `agent` under **Select keys**. 3. Set a budget, then save. The next call follows the new rules. See [Guardrails](https://inference.ai.exchange/docs/guardrails.md). ## If something goes wrong - **`liquid keys` says you need a token.** Run `liquid login --keys` again. A member cannot approve this token. Ask an owner or an admin to make a key, then run `liquid login --key sk-liquid-...`. - **Claude Code does not start.** A `.claude/settings.json` file may set `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN` or `ANTHROPIC_BASE_URL`. Remove that value from `env`. See [Claude Code](https://inference.ai.exchange/docs/connect-claude-code.md). - **A job fails with `402`.** The key reached its limit, or the workspace has no balance. Check the balance with `liquid workspace balance`. ## Next steps - [API keys](https://inference.ai.exchange/docs/keys.md): scopes, expiry and revocation. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): choose price, speed or region for each request. - [Liquid CLI](https://inference.ai.exchange/docs/cli.md): models, cap and workspace settings. # Create and showcase a project Record your coding sessions, publish a project page and share what you built. You make a project, record the prompts you type, and send the project for review. It takes about 20 minutes, plus the time of the review. ## Before you start - Install the Liquid CLI and run `liquid login --projects`. See [Liquid CLI](https://inference.ai.exchange/docs/cli.md). - Use a workspace with a balance. Only an owner or an admin can submit a project. - Have a folder where you build with a coding agent, such as `liquid claude`. ## Steps 1. Open **Projects** in the console and select **New project**. 2. Fill in **Name** and **One line about it**. Add **Link to the live project** if you have one. 3. Select **Create project**. The project starts as **Draft**. Only your workspace can see it. 4. Open the **Record from your terminal** card. In your project folder, run the command that it shows: ```bash liquid project link && liquid record --prompts ``` 5. Read the notice. Type `yes` to give consent. A script or a coding agent cannot give it. 6. Start your tool, for example `liquid claude`. Work as usual. Each call counts toward the project, and each prompt that you type goes to a queue on your computer. 7. Run `liquid record review`. For each prompt, keep it, drop it, redact it or mark it public. A prompt that looks like a secret is held until you keep or redact it. 8. Run `liquid record publish`. The command lists what it sends and asks you to type `yes`. The exchange stores each prompt encrypted. A prompt is private unless you marked it public. 9. In the console, open the project. Under **Story and what visitors see**, write your story. Choose which totals visitors see. 10. Select **Submit for review**. The state changes to **In review**. An operator checks the page, which takes about two days. ## Check it works - `liquid record status` shows that recording is on and how many prompts wait. - The **Prompt history** card lists your uploaded prompts, and says how many are public. - After approval, the state is **On show**. Select **Open the public page** to see what visitors see. ## Change a project that is on show A change to anything that visitors see moves the project from **On show** back to **In review**. Examples are the story, a **Make public** choice and a new public prompt. The page leaves the gallery until an operator approves it again. While a project is in review, you cannot edit it. Select **Withdraw** first. ## If something goes wrong - **Upload refused with `prompt_consent_required`.** Run `liquid record --prompts` and type `yes`. - **The CLI says your token cannot change a project.** Run `liquid login --projects`. - **A prompt does not upload.** The job must belong to your workspace and carry the cost tag of the project. Check `liquid project show`. ## Next steps - [Data handling](https://inference.ai.exchange/docs/data-handling.md): how long the exchange keeps your prompts and how to delete them. - [API keys](https://inference.ai.exchange/docs/keys.md): limit what a key can spend. - [Liquid CLI](https://inference.ai.exchange/docs/cli.md): the other commands of `liquid`. # Make a storyboard with Claude Code Use Liquid MCP and Claude Code to write a script and draw each frame. You will ask Claude Code to write a short script, split it into scenes and draw one image for each scene. It takes about 15 minutes. ## Before you start - Install [Claude Code](https://inference.ai.exchange/docs/connect-claude-code.md) and add funds to your workspace balance. - Expect a small cost. The exchange charges for each image. A video clip costs more, so it is optional. - Set a spend limit. The sign-in page lets you set one. The default is $10. Use $2 for this tutorial. ## Steps 1. Add the Liquid MCP server to Claude Code. ```bash claude mcp add --transport http liquid https://router.inference.ai.exchange/mcp ``` 2. Sign in. Run this command, or run `/mcp` in a session, select **liquid** and choose **Authenticate**. ```bash claude mcp login liquid ``` Your browser opens. Choose the workspace, tick the box that lets the app buy answers, and set the spend limit to $2. Only an owner or admin of the workspace can approve. See [Liquid MCP server](https://inference.ai.exchange/docs/connect-mcp-server.md). 3. Start Claude Code in an empty folder, and ask it to find an image model. ```text Use the liquid MCP tools. List the image models and show the price of each image. Pick the cheapest one. ``` Claude Code calls `list_models` with `kind` set to `image`. You see a short list with prices. 4. Ask for the script and the scenes. ```text Write a 30-second script about a lighthouse keeper who finds a message in a bottle. Split it into 4 scenes. For each scene, write one line of narration and one sentence that describes the picture. ``` Claude Code writes the script and the four scenes. No tool runs yet, so this step is free of exchange charges. 5. Ask Claude Code to preview the cost before it buys. ```text Use dry_run to estimate the cost of 4 images with the model you picked. Tell me the total, and wait for my answer. ``` Claude Code shows the estimated charge and the winning offer. A `dry_run` places no job and holds no funds. 6. Draw the frames. ```text Go ahead. Use generate_image once for each scene, with the same style words in every prompt: "soft watercolor, warm light, wide shot". Set cap_usd to 0.10 for each image. ``` Claude Code calls `generate_image` four times. Each image appears in the session. `cap_usd` is the most that one call can cost. 7. Collect the frames in a file. ```text Write storyboard.md. For each scene, put the scene number, the narration, the image prompt and the job ID from the exchange block. ``` You get `storyboard.md` in the folder. 8. Optional: make a video clip of scene 1. ```text Preview a 5-second video of scene 1 with dry_run. If it costs less than $1, start it with create_video, then check it with get_video until it is done. ``` `create_video` returns a job ID. `get_video` returns a `download_url` when the job completes. The link works for 15 minutes without a key. ## Check it works - Open **Receipts** in the console. Each image shows as one job, with its model and charge. See [Receipts](https://inference.ai.exchange/docs/receipts.md). - Ask Claude Code: `Use get_limits and tell me how much of my spend limit is left.` - Open `storyboard.md`. It must list four scenes. ## If something goes wrong - A tool answers `403` with `insufficient_scope`. The key cannot buy. Sign in again, and tick the box that lets the app buy answers. - Claude Code does not show the `liquid` tools. Run `/mcp`. Choose **Authenticate** if the server shows as not signed in. The key expires after 7 days. - A call fails with `402 budget_exhausted` or no offer wins. Raise the spend limit, or raise `cap_usd`. See [Errors](https://inference.ai.exchange/docs/errors.md). ## Next steps - [Image generation](https://inference.ai.exchange/docs/image-generation.md): the options of each image, such as `size`. - [Reference inputs](https://inference.ai.exchange/docs/reference-inputs.md): keep one character the same in every frame. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): set a price cap and other limits. # Build semantic search Embed your notes through the exchange and search them by meaning. In about 10 minutes, you embed six short notes, store the vectors in memory, and find the three notes that are closest in meaning to a question. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - Python 3.9 or newer. - The key in your shell: ```bash export LIQUID_API_KEY=sk-liquid-... ``` ## 1. Install the OpenAI SDK The exchange has the shape of the OpenAI API, so the official client works without changes. ```bash pip install openai ``` ## 2. Write the search program Save this as `search.py`. The program embeds the notes in one request. It then embeds the query and ranks the notes by cosine similarity. ```python import math import os from openai import OpenAI client = OpenAI( base_url="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], ) MODEL = "text-embedding-3-small" notes = [ "Reset your password in Settings under Security.", "Invoices are sent by email on the first day of each month.", "The office closes at 6 pm on Fridays.", "To cancel a subscription, open Billing and choose Cancel plan.", "Our API returns a 429 error when you send requests too fast.", "New hires get a laptop on their first day.", ] def embed(texts): result = client.embeddings.create(model=MODEL, input=texts) return [item.embedding for item in result.data] def cosine(a, b): dot = sum(x * y for x, y in zip(a, b)) return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b))) vectors = embed(notes) query = "how do I change my password" query_vector = embed([query])[0] ranked = sorted(zip(notes, vectors), key=lambda p: cosine(query_vector, p[1]), reverse=True) for note, vector in ranked[:3]: print(f"{cosine(query_vector, vector):.2f} {note}") ``` ## 3. Run it ```bash python search.py ``` You see three lines, from the closest note to the third closest. The scores differ a little from run to run, because the exchange can buy from a different provider each time. ```text 0.62 Reset your password in Settings under Security. 0.21 To cancel a subscription, open Billing and choose Cancel plan. 0.17 Our API returns a 429 error when you send requests too fast. ``` ## 4. Try your own query Change `query` to `"when do I get a bill"` and run the program again. The invoice note now comes first, although the query has no word in common with it. This is the difference between search by meaning and search by keyword. ## Check it works - The first line is the password note, with a score well above the other two. - The program makes two embedding requests. Each request is one auction, with its own price cap. - Your balance goes down by a very small amount. The **Credits** page lists the jobs. ## If something goes wrong - `401`: the key is wrong, revoked or expired. Export it again. - `KeyError: 'LIQUID_API_KEY'`: the shell does not hold the key. Run the `export` command in the same terminal. - `model_not_found`: the model is not on the book. Run `curl 'https://router.inference.ai.exchange/v1/models?kind=embedding'` and use a name from the list. ## Next steps - [Embeddings](https://inference.ai.exchange/docs/embeddings.md): the request fields, limits and price. - [pgvector](https://inference.ai.exchange/docs/connect-pgvector.md), [Qdrant](https://inference.ai.exchange/docs/connect-qdrant.md) or [Pinecone](https://inference.ai.exchange/docs/connect-pinecone.md): store the vectors in a database, so you embed each note one time only. - Keep one model for one index. If you change the model, embed all the notes again. # Transcribe a meeting and summarize it Turn an audio file into text, then into a short summary with action items. In about 10 minutes, you send a recording to the speech-to-text endpoint. Then you send the transcript to a chat model. The model returns a summary and a list of action items as JSON. At the end, you add up the cost of both calls from the receipts. ## Before you start - You need an API key with the `read` scope or higher, and a balance. See the [Quickstart](https://inference.ai.exchange/docs/quickstart.md). - You need Python 3 and the `openai` package (`pip install openai`). Put your key in `LIQUID_API_KEY`. - You need a recording of a meeting. It can be WAV, FLAC, MP3, Ogg, WebM, M4A or AAC, up to 25 MB and two hours. This tutorial uses `meeting.mp3`. ## Steps 1. Save this script as `notes.py` in the folder that holds `meeting.mp3`. ```python import json, os, time from openai import OpenAI client = OpenAI(base_url="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"]) jobs = [] with open("meeting.mp3", "rb") as audio: raw = client.audio.transcriptions.with_raw_response.create( model="whisper-1", file=audio) jobs.append(raw.headers["x-liquid-job-id"]) transcript = raw.parse().text schema = { "type": "object", "properties": { "summary": {"type": "string"}, "action_items": {"type": "array", "items": {"type": "string"}}, }, "required": ["summary", "action_items"], "additionalProperties": False, } raw = client.chat.completions.with_raw_response.create( model="gpt-oss-120b", messages=[{"role": "user", "content": "Summarize this meeting in three sentences. " "List each action item with its owner.\n\n" + transcript}], response_format={"type": "json_schema", "json_schema": { "name": "meeting_notes", "strict": True, "schema": schema}}, ) jobs.append(raw.headers["x-liquid-job-id"]) notes = json.loads(raw.parse().choices[0].message.content) print(notes["summary"]) for item in notes["action_items"]: print("-", item) time.sleep(3) total = sum(client.get(f"/jobs/{job}", cast_to=object)["buyer_charge_usd"] for job in jobs) print(f"Total cost: ${total:.6f}") ``` 2. Run the script. ```bash python notes.py ``` 3. Wait for the output. The first call returns the transcript. The second call returns the notes. The script does four things. The first call sends the audio to `POST /v1/audio/transcriptions`. The exchange reads the length of the file and fixes a price cap before dispatch. See [Speech-to-text](https://inference.ai.exchange/docs/speech-to-text.md). The second call sends the transcript with a `json_schema` answer format. Only an offer that supports the format can win the job. See [Structured output](https://inference.ai.exchange/docs/structured-output.md). The script then reads the job id from the `x-liquid-job-id` header of each response. Last, it reads the two receipts and adds `buyer_charge_usd` from each. See [Receipts](https://inference.ai.exchange/docs/receipts.md). ## Check it works You see a short summary, then one line for each action item, then the total cost: ```text The team reviewed the launch plan and moved the date to May 12. ... - Dana: update the launch checklist by Friday - Sam: send the new date to the partners Total cost: $0.004213 ``` Your text and cost differ. The cost depends on the length of the audio and the offers that win. To compare, open the receipt of one job in the console. The `buyer_charge_usd` of the two receipts must add up to the total that the script printed. ## If something goes wrong - **`400` on the first call.** The file is over 25 MB or two hours, or the exchange cannot read its length. Cut the recording or convert it to MP3. - **`404` when the script reads a receipt.** The exchange writes the receipt just after it answers. Increase the `time.sleep` value. Also check that your key has the `read` scope or higher. - **`503 no_eligible_offer`.** No offer can take the job now. Read `eligibility.excluded` in the error to see why. See [Errors](https://inference.ai.exchange/docs/errors.md). ## Next steps - [Speech-to-text](https://inference.ai.exchange/docs/speech-to-text.md): add `language` or `verbose_json` for timestamps. - [Structured output](https://inference.ai.exchange/docs/structured-output.md): test a schema in the playground. - [Receipts](https://inference.ai.exchange/docs/receipts.md): read every field of a receipt. # Sell your first model Register as a provider, quote one model and see your first award. You set up a provider, run the reference provider and buy one answer from yourself. Plan about an hour, plus the wait for admission. ## Before you start - A model behind an OpenAI-compatible endpoint that streams token usage. A local server is fine. - A dispatch address that the exchange can reach over `http` or `https`. It cannot be a loopback or private address. - Python 3.12 or later, [uv](https://docs.astral.sh/uv/) and a funded buyer workspace key. See [Keys](https://inference.ai.exchange/docs/keys.md). ## Steps 1. Open **Sell on the exchange** in the console and choose **Request to be a provider**. Fill in the form. Set `declared_endpoint` to your dispatch address. Pick a handle that does not start with `byok-`, because the exchange keeps those for buyers' keys. The status is `PENDING`. The handle is yours from now on. 2. Wait until the status is `ADMITTED`. An operator reviews each registration, and the exchange sends no email. Check the console. 3. Open the provider in the selling console, choose **Issue** and copy the secret. It starts with `sk-liquid-`. The console shows it once. 4. Register your deployment. Change the values to match your model: ```bash curl https://router.inference.ai.exchange/v1/provider/models \ -H "Authorization: Bearer $LIQUID_PROVIDER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"deployment_name":"my-model","precision":"fp8","region":"eu-west", "max_input_tokens":32000,"max_output_tokens":4096}' ``` The reply is `201`. The deployment is `UNLISTED`. Only your own workspace can buy it, by the name `/my-model`. 5. Save the reference provider from Run the reference provider as `provider.py` in a new directory. In the same directory, make `.env.local`: ```bash LIQUID_EXCHANGE_URL=https://router.inference.ai.exchange LIQUID_PROVIDER_SLUG=your-handle LIQUID_PROVIDER_API_KEY=sk-liquid-... LIQUID_DEPLOYMENT_NAME=my-model LIQUID_DISPATCH_BIND=127.0.0.1:9110 LIQUID_DISPATCH_URL=https://provider.example.com/dispatch LIQUID_UPSTREAM_BASE_URL=http://127.0.0.1:9200/v1 LIQUID_UPSTREAM_API_KEY=your-upstream-key LIQUID_RATE_INPUT=0.10 LIQUID_RATE_CACHE_READ=0.02 LIQUID_RATE_OUTPUT=0.20 LIQUID_RETENTION_POLICY=allowed LIQUID_AWARD_DB=awards.sqlite3 ``` The three rates are USD per million tokens. The scheme, host and port of `LIQUID_DISPATCH_URL` must match `declared_endpoint`. Do not commit this file. 6. Start the provider: ```bash set -a source .env.local set +a uv run provider.py ``` The log shows that the login worked, then that the exchange accepted your quote. 7. Send one small request from your buyer workspace: ```bash curl "$LIQUID_EXCHANGE_URL/v1/chat/completions" \ -H "Authorization: Bearer $LIQUID_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"model":"your-handle/my-model","max_tokens":32, "messages":[{"role":"user","content":"Say hello."}], "exchange":{"rank":{"allow_single_supplier":true}, "privacy":{"retention":"allowed"},"cap_usd":0.001}}' ``` You get a short answer. Copy `exchange.job_id` from the response. ## Check it works Read the receipt of the job: ```bash curl "$LIQUID_EXCHANGE_URL/v1/exchange/jobs/$LIQUID_JOB_ID" \ -H "Authorization: Bearer $LIQUID_API_KEY" ``` The receipt shows `settlement_status: settled`. Your agent logs the award for the same job. Then open the **Earnings** page in the selling console. It shows the amount that the exchange owes you. The exchange opens a payout each hour for a balance of $25 or more, and only when you have set a payout destination. See [Serve and get paid](https://inference.ai.exchange/docs/provider-dispatch.md). ## If something goes wrong - **No `ADMITTED` status.** A `PENDING` provider has no keys and no sessions. Wait for an operator. A `REJECTED` status has a reason in the console. - **The login or the quote fails.** Check that the handle and key in `.env.local` are correct. Read the code of the refusal in [Quote over the session](https://inference.ai.exchange/docs/provider-quoting.md). - **The request finds no offer.** Make sure that the provider still runs and that the model name is `/`. Quotes expire, so look at the log for new acknowledgements. ## Next steps - [Deployments](https://inference.ai.exchange/docs/provider-models.md): ask for a listing in the public book. - [Run your agent](https://inference.ai.exchange/docs/provider-operate.md): change prices and upgrade with no gap. - [Serve and get paid](https://inference.ai.exchange/docs/provider-dispatch.md): set your payout destination. # All integrations Use the exchange from the coding agent, SDK or app that you already run. Each guide gives the base URL, the key and the settings for one tool. If your tool is not here, [Connect an app](https://inference.ai.exchange/docs/connect-overview.md) shows how to use any OpenAI-compatible or Anthropic-compatible tool. ## Coding agents and editors - [Claude Code](https://inference.ai.exchange/docs/connect-claude-code.md) - [Codex](https://inference.ai.exchange/docs/connect-codex.md) - [GitHub Copilot CLI](https://inference.ai.exchange/docs/connect-copilot-cli.md) - [Grok Build](https://inference.ai.exchange/docs/connect-grok-build.md) - [Pi](https://inference.ai.exchange/docs/connect-pi.md) - [Factory Droid](https://inference.ai.exchange/docs/connect-factory-droid.md) - [Oh My Pi](https://inference.ai.exchange/docs/connect-omp.md) - [Reason](https://inference.ai.exchange/docs/connect-reason.md) - [Crush](https://inference.ai.exchange/docs/connect-crush.md) - [Goose](https://inference.ai.exchange/docs/connect-goose.md) - [Hermes Agent](https://inference.ai.exchange/docs/connect-hermes-agent.md) - [OpenClaw](https://inference.ai.exchange/docs/connect-openclaw.md) - [OpenCode](https://inference.ai.exchange/docs/connect-opencode.md) - [Paperclip](https://inference.ai.exchange/docs/connect-paperclip.md) - [Cursor](https://inference.ai.exchange/docs/connect-cursor.md) - [DeepSeek Harness](https://inference.ai.exchange/docs/connect-deepseek-harness.md) - [Z Code](https://inference.ai.exchange/docs/connect-zcode.md) - [Cline](https://inference.ai.exchange/docs/connect-cline.md) - [Roo Code](https://inference.ai.exchange/docs/connect-roo-code.md) - [Kilo Code](https://inference.ai.exchange/docs/connect-kilo-code.md) ## Chat apps - [RikkaHub](https://inference.ai.exchange/docs/connect-rikkahub.md) - [Open WebUI](https://inference.ai.exchange/docs/connect-open-webui.md) - [Cherry Studio](https://inference.ai.exchange/docs/connect-cherry-studio.md) ## Automation - [n8n](https://inference.ai.exchange/docs/connect-n8n.md) - [Dify](https://inference.ai.exchange/docs/connect-dify.md) - [LobeHub](https://inference.ai.exchange/docs/connect-lobehub.md) ## Image tools - [ComfyUI](https://inference.ai.exchange/docs/connect-comfyui.md) ## SDKs and frameworks - [OpenAI SDK](https://inference.ai.exchange/docs/connect-openai-sdk.md) - [Anthropic SDK](https://inference.ai.exchange/docs/connect-anthropic-sdk.md) - [Claude Agent SDK](https://inference.ai.exchange/docs/connect-claude-agent-sdk.md) - [OpenAI Agents SDK](https://inference.ai.exchange/docs/connect-openai-agents-sdk.md) - [Vercel AI SDK](https://inference.ai.exchange/docs/connect-vercel-ai-sdk.md) - [LiteLLM](https://inference.ai.exchange/docs/connect-litellm.md) - [LangChain](https://inference.ai.exchange/docs/connect-langchain.md) - [OpenHands](https://inference.ai.exchange/docs/connect-openhands.md) ## Embeddings and search - [LlamaIndex](https://inference.ai.exchange/docs/connect-llamaindex.md) - [pgvector](https://inference.ai.exchange/docs/connect-pgvector.md) - [Qdrant](https://inference.ai.exchange/docs/connect-qdrant.md) - [Pinecone](https://inference.ai.exchange/docs/connect-pinecone.md) - [RAGFlow](https://inference.ai.exchange/docs/connect-ragflow.md) ## MCP - [Liquid MCP server](https://inference.ai.exchange/docs/connect-mcp-server.md) - [MCP servers](https://inference.ai.exchange/docs/connect-mcp-client.md) ## Next steps - [Connect an app](https://inference.ai.exchange/docs/connect-overview.md): base URLs, keys and which guide to use. - [Quickstart](https://inference.ai.exchange/docs/quickstart.md): make a key and send a request. # Connect an app Any tool that uses the OpenAI or Anthropic API can use the exchange. Change two settings: the base URL and the API key. For each request, the exchange picks the best standing offer for the model that you named. It sets a price cap before the first token and charges for the tokens used. Each guide gives the steps for one tool, a check, and fixes for common errors. ## Connect in three steps 1. **Issue a key.** Create a `spend` key in your workspace. The format is `sk-liquid--`. The console shows it one time only. See [API keys](https://inference.ai.exchange/docs/keys.md). 2. **Keep the key out of config files.** Each guide reads it from `LIQUID_API_KEY`. ```bash export LIQUID_API_KEY=sk-liquid-... ``` 3. **Set the base URL and pick a guide.** If your tool is not listed, follow the [OpenAI SDK](https://inference.ai.exchange/docs/connect-openai-sdk.md) or [Anthropic SDK](https://inference.ai.exchange/docs/connect-anthropic-sdk.md) guide for its format. A new workspace has a starting balance, shown on the **Credits** page. If the balance is less than the price cap of a job, the exchange refuses the request. ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) starts the coding agent that you already use with the exchange as its provider. It sets the base URL, key and model for that process only, and writes none of your files. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid claude ``` `liquid tools` lists every tool. `liquid connect ` prints the setup for a tool that the CLI cannot start, such as an app or an SDK. The guides show the same setup by hand. ## Which guide to use The full list is under [All guides](#all-guides). ## Test a request To test a key and a base URL, send one request with `curl`. The exchange accepts the key as a bearer token, or as `x-api-key`. **curl** ```bash 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"}] }' ``` **curl (images)** ```bash curl https://router.inference.ai.exchange/v1/images/generations \ -H "Authorization: Bearer $LIQUID_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "liquid.auto", "prompt": "A red bicycle leaning on a brick wall", "n": 1 }' ``` The `x-liquid-cap-usd` response header is the maximum charge for the job. The actual charge is usually less. ## Name a model Each guide uses `liquid.auto`. The `model` field accepts these names: | Name | Example | Behaviour | | -------------- | ---------------------- | --------------------------------------------------------------------------------------------- | | A model | `gpt-oss-120b` | Every provider that quotes that model competes. | | A router model | `liquid.auto` | Every listed model competes, ranked by price or by Auto. | | Auto | `liquid.auto:balanced` | The exchange chooses by published score and estimated charge. See [Auto Router](https://inference.ai.exchange/docs/auto.md). | | A basket | `architect.basket` | The models in `exchange.candidates.models.allow` compete. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). | | A preset | `fast` | Saved models, strategy and limits. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). | A client that sets only `model` can add a suffix: - `:smartest`, `:fastest` or `:cheapest` picks how the exchange ranks the offers, as in `deepseek:fastest`. - `+web` or `:web` searches the web for each message (beta), as in `deepseek:fastest+web`. A creator name, such as `deepseek`, is valid with a suffix. See [Axis routing](https://inference.ai.exchange/docs/axis-routing.md). The exchange refuses any other name and never uses a different model. `GET /v1/models` lists the names you can use. See [Market data](https://inference.ai.exchange/docs/market.md). ## Read the result Each response has an `exchange` block with `job_id`, `provider_slug`, `charge.cap_usd` (the maximum cost) and `charge.total_usd` (the actual cost). Two headers arrive before the first token, so a streaming client has them from the start: | Header | Meaning | | ------------------ | --------------------------------------- | | `x-liquid-job-id` | The job id. Use it to read the receipt. | | `x-liquid-cap-usd` | The most this job can be charged. | To choose the models, strategy or limits, send an `exchange` block in the body. A client that can send only headers can send the cap and the first-token limit as `x-liquid-*` headers, or name a preset. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Reference ### Base URL and format One origin and one key serve both formats. Both formats accept both auth headers. | Format | Base URL | Auth header | | --------- | ----------------------------------------- | ----------------------------------- | | OpenAI | `https://router.inference.ai.exchange/v1` | `Authorization: Bearer sk-liquid-…` | | Anthropic | `https://router.inference.ai.exchange` | `x-api-key: sk-liquid-…` | An OpenAI client adds a path such as `/chat/completions`. An Anthropic client adds `/v1/messages`, so a base URL that ends in `/v1` gets a 404. ### All guides | Tool | Format | Configured through | Guide | | ------------------ | --------- | --------------------------- | --------------------------------------------------- | | Claude Code | Anthropic | Environment | [Claude Code](https://inference.ai.exchange/docs/connect-claude-code.md) | | Codex | OpenAI | Config file | [Codex](https://inference.ai.exchange/docs/connect-codex.md) | | GitHub Copilot CLI | OpenAI | Environment | [GitHub Copilot CLI](https://inference.ai.exchange/docs/connect-copilot-cli.md) | | DeepSeek Harness | Anthropic | Environment | [DeepSeek Harness](https://inference.ai.exchange/docs/connect-deepseek-harness.md) | | Grok Build | OpenAI | Config file | [Grok Build](https://inference.ai.exchange/docs/connect-grok-build.md) | | Pi | OpenAI | Config file | [Pi](https://inference.ai.exchange/docs/connect-pi.md) | | Factory Droid | OpenAI | Config file | [Factory Droid](https://inference.ai.exchange/docs/connect-factory-droid.md) | | Oh My Pi | OpenAI | Liquid Inference profile | [Oh My Pi](https://inference.ai.exchange/docs/connect-omp.md) | | Reason | OpenAI | In the app | [Reason](https://inference.ai.exchange/docs/connect-reason.md) | | Goose | OpenAI | Custom provider file | [Goose](https://inference.ai.exchange/docs/connect-goose.md) | | Crush | OpenAI | `crushrc` provider | [Crush](https://inference.ai.exchange/docs/connect-crush.md) | | Hermes Agent | OpenAI | Provider plugin | [Hermes Agent](https://inference.ai.exchange/docs/connect-hermes-agent.md) | | OpenClaw | OpenAI | Config file | [OpenClaw](https://inference.ai.exchange/docs/connect-openclaw.md) | | OpenCode | OpenAI | Config file | [OpenCode](https://inference.ai.exchange/docs/connect-opencode.md) | | Paperclip | OpenAI | Adapter environment | [Paperclip](https://inference.ai.exchange/docs/connect-paperclip.md) | | Cursor | OpenAI | In the app | [Cursor](https://inference.ai.exchange/docs/connect-cursor.md) | | Cline | OpenAI | In the app | [Cline](https://inference.ai.exchange/docs/connect-cline.md) | | Z Code | OpenAI | Provider file or in the app | [Z Code](https://inference.ai.exchange/docs/connect-zcode.md) | | RikkaHub | OpenAI | In the app | [RikkaHub](https://inference.ai.exchange/docs/connect-rikkahub.md) | | Roo Code | OpenAI | In the app | [Roo Code](https://inference.ai.exchange/docs/connect-roo-code.md) | | Kilo Code | OpenAI | In the app | [Kilo Code](https://inference.ai.exchange/docs/connect-kilo-code.md) | | Open WebUI | OpenAI | In the app | [Open WebUI](https://inference.ai.exchange/docs/connect-open-webui.md) | | Cherry Studio | OpenAI | In the app | [Cherry Studio](https://inference.ai.exchange/docs/connect-cherry-studio.md) | | n8n | OpenAI | In the app | [n8n](https://inference.ai.exchange/docs/connect-n8n.md) | | Dify | OpenAI | In the app | [Dify](https://inference.ai.exchange/docs/connect-dify.md) | | LobeHub | OpenAI | In the app | [LobeHub](https://inference.ai.exchange/docs/connect-lobehub.md) | | OpenAI SDK | OpenAI | In code | [OpenAI SDK](https://inference.ai.exchange/docs/connect-openai-sdk.md) | | Anthropic SDK | Anthropic | In code | [Anthropic SDK](https://inference.ai.exchange/docs/connect-anthropic-sdk.md) | | Claude Agent SDK | Anthropic | In code | [Claude Agent SDK](https://inference.ai.exchange/docs/connect-claude-agent-sdk.md) | | OpenAI Agents SDK | OpenAI | In code | [OpenAI Agents SDK](https://inference.ai.exchange/docs/connect-openai-agents-sdk.md) | | Vercel AI SDK | OpenAI | In code | [Vercel AI SDK](https://inference.ai.exchange/docs/connect-vercel-ai-sdk.md) | | LiteLLM | OpenAI | In code | [LiteLLM](https://inference.ai.exchange/docs/connect-litellm.md) | | LangChain | OpenAI | In code | [LangChain](https://inference.ai.exchange/docs/connect-langchain.md) | | LlamaIndex | OpenAI | In code | [LlamaIndex](https://inference.ai.exchange/docs/connect-llamaindex.md) | | pgvector | OpenAI | In code | [pgvector](https://inference.ai.exchange/docs/connect-pgvector.md) | | Qdrant | OpenAI | In code | [Qdrant](https://inference.ai.exchange/docs/connect-qdrant.md) | | Pinecone | OpenAI | In code | [Pinecone](https://inference.ai.exchange/docs/connect-pinecone.md) | | RAGFlow | OpenAI | In the app | [RAGFlow](https://inference.ai.exchange/docs/connect-ragflow.md) | | OpenHands SDK | OpenAI | In code | [OpenHands SDK](https://inference.ai.exchange/docs/connect-openhands.md#sdk) | | OpenHands CLI | OpenAI | Config file and environment | [OpenHands CLI](https://inference.ai.exchange/docs/connect-openhands.md#cli) | A **Verified** label with a release number, such as "Verified 2.1.287", means the setup passed a test run with that release in the last 14 days. Hold the pointer over it to see the date. # Claude Code Claude Code is the terminal agent from Anthropic. It uses the Anthropic Messages format and reads its endpoint from environment variables. The quickest way is the Liquid CLI. Use the manual steps only when you must keep the settings in your own files. ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) starts Claude Code with the exchange as its provider. It sets the base URL, the key and the model for that process only. It writes no file of yours. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid claude ``` By default, Opus runs the main agent and `liquid.auto` runs subagents and background calls. | To do this | Run this | | ------------------------------------------- | ------------------------------------------------ | | Pick other models | `liquid claude fable/auto` | | Use one model for all work, ranked by price | `liquid claude --model liquid.auto:cheapest` | | Limit what one request can cost | `liquid claude --cap 0.50` | | Start Claude Code in its auto mode | `liquid claude opus/auto --permission-mode auto` | `liquid claude` reads `--model`, `--cap` and `--filter`. It sends all other arguments to Claude Code. ### A separate config directory `liquid claude` sets `CLAUDE_CONFIG_DIR` to `~/.config/liquid/claude`. That directory holds the settings, history and MCP servers of the Liquid sessions. - A `/model` choice in a Liquid session stays there, so a plain `claude` keeps its own model. - When the session ends, `liquid claude` saves the choice as `liquid model`. The next Liquid session starts with it. At its first start at a terminal, `liquid claude` asks which parts of `~/.claude` to share: | Part | What the Liquid sessions get | | -------------- | ------------------------------------------------------------------------------------------------------------------ | | `instructions` | Links to `CLAUDE.md`, `skills/`, `agents/`, `commands/` and `output-styles/`. | | `plugins` | A link to `plugins/`. Each setup turns plugins on in its own settings. | | `settings` | Your `settings.json` through `--settings`: hooks, permissions and effort. The model of the exchange still applies. | Without a terminal, it shares `instructions`. To change the answer, run one of these commands: ```bash liquid config claude-share instructions,plugins liquid config claude-share none ``` ### The MCP server of the exchange Each `liquid claude` session has the [Liquid MCP server](https://inference.ai.exchange/docs/connect-mcp-server.md) with the key of the session. Claude can buy images, speech and video within the limits of the key. - `liquid mcp claude` adds the server to both directories, with a key that you approve in the browser. - To add another MCP server to the Liquid sessions, run `liquid claude mcp add …`. ## 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, so it never appears in a file: ```bash export LIQUID_API_KEY=sk-liquid-... ``` - Claude Code installed. Install it with the native installer or with npm: ```bash # native installer (macOS, Linux, WSL) curl -fsSL https://claude.ai/install.sh | bash # or with npm npm install -g @anthropic-ai/claude-code ``` ## 1. Point it at the exchange Set these variables in the shell that starts Claude Code. To keep them, add them to `~/.zshrc` or `~/.bashrc`. The second block has the same variables in `.claude/settings.json`, for one project. **Shell** ```bash export ANTHROPIC_AUTH_TOKEN=$LIQUID_API_KEY export ANTHROPIC_BASE_URL=https://router.inference.ai.exchange export ANTHROPIC_DEFAULT_HAIKU_MODEL=cc.anthropic/liquid.auto export ANTHROPIC_DEFAULT_OPUS_MODEL=cc.anthropic/liquid.auto export ANTHROPIC_DEFAULT_SONNET_MODEL=cc.anthropic/liquid.auto export ANTHROPIC_MODEL=cc.anthropic/liquid.auto export CLAUDE_CODE_ATTRIBUTION_HEADER=0 export CLAUDE_CODE_AUTO_MODE_SERVER=0 export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 export CLAUDE_CODE_MAX_OUTPUT_TOKENS=16384 export CLAUDE_CODE_SUBAGENT_MODEL=cc.anthropic/liquid.auto ``` **.claude/settings.json** ```json { "env": { "ANTHROPIC_AUTH_TOKEN": "sk-liquid-...", "ANTHROPIC_BASE_URL": "https://router.inference.ai.exchange", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "cc.anthropic/liquid.auto", "ANTHROPIC_DEFAULT_OPUS_MODEL": "cc.anthropic/liquid.auto", "ANTHROPIC_DEFAULT_SONNET_MODEL": "cc.anthropic/liquid.auto", "ANTHROPIC_MODEL": "cc.anthropic/liquid.auto", "CLAUDE_CODE_ATTRIBUTION_HEADER": "0", "CLAUDE_CODE_AUTO_MODE_SERVER": "0", "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1", "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "16384", "CLAUDE_CODE_SUBAGENT_MODEL": "cc.anthropic/liquid.auto" } } ``` | Variable | Why it is set | | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `ANTHROPIC_BASE_URL` | The exchange origin, with no `/v1`. A URL that ends in `/v1` makes the client request `/v1/v1/messages`. | | `ANTHROPIC_AUTH_TOKEN` | Your key, sent as a bearer token. Use this, not `ANTHROPIC_API_KEY` (see [below](#2-keep-your-existing-login)). | | `ANTHROPIC_MODEL` | The model for the main agent. It needs the `cc.anthropic/` prefix (see [Model discovery](#model-discovery)). | | `ANTHROPIC_DEFAULT_OPUS_MODEL` | The model for the `opus` alias. | | `ANTHROPIC_DEFAULT_SONNET_MODEL` | The model for the `sonnet` alias. Claude Code sends some side calls to it. | | `ANTHROPIC_DEFAULT_HAIKU_MODEL` | The model for background work such as session titles. See [A different model per task](#a-different-model-per-task). | | `CLAUDE_CODE_SUBAGENT_MODEL` | The model for subagents. Without it, a subagent asks for a `claude-*` model that the exchange does not list. | | `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY` | Loads the model list of the exchange into the picker. | | `CLAUDE_CODE_MAX_OUTPUT_TOKENS` | Caps the output tokens that Claude Code requests. See [Output token limit](#output-token-limit). | | `CLAUDE_CODE_AUTO_MODE_SERVER` | Stops a notice about auto mode. See [Auto mode checks](#auto-mode-checks). | | `CLAUDE_CODE_ATTRIBUTION_HEADER` | Leaves the version block out of the prompt. See [The attribution block](#the-attribution-block). | Set `LIQUID_API_KEY` before this block. If a profile sets the key on a later line, the token is empty and each request fails with `401`. The project file holds the key in plain text. Use `.claude/settings.local.json` and keep it out of version control. ## 2. Keep your existing login Do not sign out of Claude Code. The variables apply only to the process that you start with them. A plain `claude` in another shell still uses Anthropic. `ANTHROPIC_API_KEY` has priority over `ANTHROPIC_AUTH_TOKEN`. Clear it in the shell that starts Claude Code: ```bash unset ANTHROPIC_API_KEY ``` To keep the variables out of your normal shell, use a wrapper: ```bash claude-liquid() { ANTHROPIC_BASE_URL=... ANTHROPIC_AUTH_TOKEN="$LIQUID_API_KEY" \ CLAUDE_CONFIG_DIR="$HOME/.config/liquid/claude" \ env -u ANTHROPIC_API_KEY claude "$@" } ``` Add the other variables from step 1 to the same line. Keep `CLAUDE_CONFIG_DIR`, the directory that `liquid claude` uses. Without it, Claude Code saves a `/model` choice of the exchange in `~/.claude`, and a plain `claude` uses it. ## 3. Run it ```bash claude ``` ## 4. Check it works 1. Run `/status`. The API base URL and the model must be the values that you set. 2. Send one message. 3. Read the receipt on the **Overview** or **Credits** page of the console. See [Quickstart](https://inference.ai.exchange/docs/quickstart.md). ## Model discovery Claude Code drops each model id that does not contain "claude" or "anthropic". The `cc.anthropic/` prefix keeps the models of the exchange in the picker. Add the `[1m]` suffix if the input window of the model is one million tokens or more: ```bash export ANTHROPIC_MODEL=cc.anthropic/liquid.auto[1m] ``` Use any model, router model or preset on the book. For names, use `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). ## Output token limit At high effort, Claude Code asks for 32,000 output tokens by default. - If no offer allows that many, the exchange lowers the request to the largest limit of the offers that can take it. See [Errors](https://inference.ai.exchange/docs/errors.md). - The exchange does not halve the limit to fit your balance. If the balance does not cover 32,000 tokens, the request gets `402 insufficient_balance`. Add funds or lower `CLAUDE_CODE_MAX_OUTPUT_TOKENS`. - An answer that reaches the limit stops with `stop_reason: "max_tokens"`. The block in step 1 sets `16384`. A lower value gives a lower cap for each request. ## A different model per task Claude Code has a model variable for each type of work. The block in step 1 sets all of them to one model. Background work and subagents send many requests, so you can send them to `liquid.auto:economy`: ```bash export ANTHROPIC_DEFAULT_HAIKU_MODEL=cc.anthropic/liquid.auto:economy export CLAUDE_CODE_SUBAGENT_MODEL=cc.anthropic/liquid.auto:economy ``` To pick the ranking, add an axis to the name, such as `cc.anthropic/liquid.auto:fastest`. With the CLI, run `liquid claude --model liquid/auto:fastest`. See [Axis routing](https://inference.ai.exchange/docs/axis-routing.md). ## The cap `liquid.auto` has a cap of its own, so Claude Code needs to send nothing. `GET /v1/exchange/routers` shows its `cap_usd`. To set your own cap, send it as a header, because Claude Code cannot send the `exchange` object. Set this variable, then start Claude Code again: ```bash export ANTHROPIC_CUSTOM_HEADERS="x-liquid-cap-usd: 1.00" ``` In a settings file, add `"ANTHROPIC_CUSTOM_HEADERS": "x-liquid-cap-usd: 1.00"` to the `env` object. The header sets a cap on each request, for all models. Auto buys only from offers whose worst-case charge is within the cap. You can link the key to a preset that sets a price bound instead (see [Purchasing rules](https://inference.ai.exchange/docs/rules.md)). ## Purchasing rules Send the cost cap, the first-token limit and the Auto preference as headers in `ANTHROPIC_CUSTOM_HEADERS`. Put a newline between two headers. ```bash export ANTHROPIC_CUSTOM_HEADERS="x-liquid-cap-usd: 0.50 x-liquid-policy: economy" ``` - If the estimated charge is more than the cap, the exchange refuses the request. It does not shrink it. - `x-liquid-policy` applies only to requests that Auto ranks. It needs Claude Code v2.1.227 or later. For any other rule, such as `exchange.privacy.regions`, send the `exchange` block as JSON in one header: ```bash export ANTHROPIC_CUSTOM_HEADERS='x-liquid-rules: {"privacy":{"regions":["eu"]}}' ``` With the CLI, run `liquid config rules '{"region_policy":["eu"]}'`. See [Rules in one header](https://inference.ai.exchange/docs/rules.md#rules-in-one-header). No header sets the models. Save a preset on the **Presets** page and set a model variable to it: ```bash export CLAUDE_CODE_SUBAGENT_MODEL=cc.anthropic/background ``` See [Purchasing rules](https://inference.ai.exchange/docs/rules.md) for each rule. ## Zero retention If nothing sets `exchange.privacy.retention`, Auto can send your prompt to its classifier. To stop this, send the rule as a header. Only providers that agree not to retain a request can then take it. ```bash export ANTHROPIC_CUSTOM_HEADERS="x-liquid-retention: none" ``` To set the rule for every client of the key, use a preset with `exchange.privacy.retention: "none"`. See [Data handling](https://inference.ai.exchange/docs/data-handling.md). ## Auto mode checks Set `CLAUDE_CODE_AUTO_MODE_SERVER=0`. Without it, the first action of each session stops with a notice that the session is not eligible for no-charge classifier requests. Auto mode works in both cases. The `auto` in `opus/auto` is the [Auto Router](https://inference.ai.exchange/docs/auto.md), not this mode. In auto mode, Claude Code checks an action, such as a shell command, before it runs. With Anthropic, the server does that check at no charge. The exchange works differently: - It returns no verdict. It ignores the `safeguards` field, because a provider could otherwise allow any action. - Claude Code checks the action with its own request. The exchange auctions, caps and charges it like any other request. ## The attribution block Set `CLAUDE_CODE_ATTRIBUTION_HEADER=0`. Claude Code adds a version block to the start of the system prompt. Anthropic removes it by position. The exchange does not, so the block goes to the provider and you pay for its tokens. ## Troubleshooting | Symptom | Cause and fix | | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | The model picker shows only built-in Claude models. | `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY` is not `1`, or the shell started before you set it. | | Requests still go to Anthropic. | `ANTHROPIC_API_KEY` has priority. Run `unset ANTHROPIC_API_KEY`, restart, and confirm with `/status`. | | `401` or an authentication error. | Run `echo $ANTHROPIC_AUTH_TOKEN`. If it is empty, a profile sets `LIQUID_API_KEY` too late. Otherwise confirm that the key has the `spend` scope. | | `400 the Auto strategy requires a price bound, …` | `auto_requires_price_bound`. Set the header from [The cap](#the-cap). | | `400 invalid_header`. | A rule in `ANTHROPIC_CUSTOM_HEADERS` cannot be parsed. See [Purchasing rules](#purchasing-rules). | | Answers stop with `stop_reason: "max_tokens"`. | The output limit of the offers is below the request. Choose a model that allows more. | | A plain `claude` reports a problem with the model `cc.anthropic/…`. | A `/model` choice was saved in `~/.claude`. Run `/model` in the plain `claude`. To prevent it, use `liquid claude` or set `CLAUDE_CONFIG_DIR` (see [step 2](#2-keep-your-existing-login)). | | `model_not_found`. | The name is not on the book, or you dropped the `cc.anthropic/` prefix. Check `GET /v1/models`. | | A request is refused for lack of funds. | The balance cannot cover the cap. Add funds on the **Credits** page. | | A notice about auto mode classifier requests. | Set `CLAUDE_CODE_AUTO_MODE_SERVER=0`. See [Auto mode checks](#auto-mode-checks). | | `liquid claude` stops: the saved API key spends from another workspace. | Run `liquid login` to switch to a key for your workspace. See [One key for each workspace](https://inference.ai.exchange/docs/cli.md). | | `liquid claude` stops: a settings file sets `ANTHROPIC_AUTH_TOKEN`. | Remove the variable from the `env` of the file that the error names. | See also [Codex](https://inference.ai.exchange/docs/connect-codex.md) and [Prompt caching](https://inference.ai.exchange/docs/caching.md). # Codex Codex is the terminal agent from OpenAI. It uses the OpenAI Responses API. Add the exchange as a provider in a Codex profile, and start Codex with that profile. The quickest way is the Liquid CLI. Use the manual steps only when you must keep the settings in your own files. ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) starts Codex with the exchange as its provider. It sets the base URL, the key and the model for that process only. It writes no file of yours. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid codex ``` | To do this | Add this | | -------------------------------------- | ------------------------------ | | Rank the offers by price | `--model liquid.auto:cheapest` | | Search the web for each message (beta) | `--model liquid.auto:web` | | Limit what one request can cost | `--cap 0.50` | ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - Codex 0.95 or later. Codex speaks the Responses API only from 0.95. Run `codex --version` after you install: ```bash npm install -g @openai/codex@latest ``` ## 1. Create the profile A Codex profile is the file `~/.codex/.config.toml`. With `--profile `, Codex applies it on top of `~/.codex/config.toml`. - The first block writes the profile. - The second block has the same settings for `~/.codex/config.toml`. The VS Code extension uses that file without `--profile`. **Profile** ```bash cat > ~/.codex/liquid.config.toml <<'EOF' model = "liquid.auto" model_provider = "liquid" web_search = "disabled" suppress_unstable_features_warning = true [features] api_key_model_discovery = true [model_providers.liquid] name = "Liquid Inference" base_url = "https://router.inference.ai.exchange/v1" env_key = "LIQUID_API_KEY" env_key_instructions = "Create a key at https://inference.ai.exchange/keys" model_catalog_url = "https://router.inference.ai.exchange/v1/codex/models" EOF ``` **Make it the default** ```toml # Above the first [section] of ~/.codex/config.toml model = "liquid.auto" model_provider = "liquid" web_search = "disabled" suppress_unstable_features_warning = true # Anywhere after it [features] api_key_model_discovery = true [model_providers.liquid] name = "Liquid Inference" base_url = "https://router.inference.ai.exchange/v1" env_key = "LIQUID_API_KEY" env_key_instructions = "Create a key at https://inference.ai.exchange/keys" model_catalog_url = "https://router.inference.ai.exchange/v1/codex/models" ``` | Setting | Why it is set | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `model` | The model Codex starts with. See [Choosing a model](#choosing-a-model). | | `model_provider` | Selects the `[model_providers.liquid]` table. | | `web_search = "disabled"` | Web search runs on OpenAI servers. Without this line, the exchange removes the tool and sends the header `x-liquid-notice: web_search_dropped`. | | `api_key_model_discovery` | Makes Codex read the model list from `model_catalog_url`. Without it, Codex never fetches the list. | | `suppress_unstable_features_warning` | Hides the start-up warning about `api_key_model_discovery`. | | `base_url` | Ends in `/v1`, because Codex appends `/responses`. | | `name` | The name that Codex shows for the provider. | | `env_key` | The variable that holds the key, so the file does not. Codex sends it as a bearer token to `base_url` and `model_catalog_url`. | | `env_key_instructions` | The text Codex shows when the variable is not set. It tells you where to make a key. | | `model_catalog_url` | The model list in the format of Codex. Each entry gives the context window and the `apply_patch` tool. | Do not add `wire_api`. Codex 0.95 removed `wire_api = "chat"`. In `~/.codex/config.toml`, TOML puts each key after a `[section]` header into that section. So: - Put the top-level lines above the first `[section]` header. - If the file has a `[features]` table, add the line to that table. TOML does not allow the same table twice. ## 2. Export the key Set the key in the shell that starts Codex. To keep it, add the line to `~/.zshrc` or `~/.bashrc`. ```bash export LIQUID_API_KEY=sk-liquid-... ``` ## 3. Run it ```bash export LIQUID_API_KEY=sk-liquid-... codex --profile liquid ``` ## 4. Check it works Send one request without the interactive session: ```bash codex exec --profile liquid "Reply with the word ready" ``` Codex prints the answer. The request shows under **Receipts** on the **Credits** page, with Codex as the client. See [Quickstart](https://inference.ai.exchange/docs/quickstart.md). ## Choosing a model In Codex, `/model` lists these models, in this order: 1. `liquid.auto`, then the other router models. 2. The presets of your workspace. 3. Exact models whose offers accept tools, with the most offers first. Codex sends tools on each turn. If the offers accept a reasoning effort, `/model` also shows low, medium and high. Codex keeps the list for 5 minutes. To choose a model for one session, pass `--model`: ```bash codex --profile liquid --model gpt-oss-120b ``` A model on the book but not in the list still works. Codex then shows ``Model metadata for `` not found``, uses a context window of 272,000 tokens and omits `apply_patch`. The exchange refuses a name that is not on the book. For current names, use `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). ## The cap `liquid.auto` has a cap of its own, so Codex needs to send nothing. To set your own cap, add a header to the `[model_providers.liquid]` table, because Codex cannot send the `exchange` block: ```toml http_headers = { "x-liquid-cap-usd" = "1.00" } ``` This sets a cap of $1 on each request, for all models. See [Auto Router](https://inference.ai.exchange/docs/auto.md). ## Purchasing rules Codex sends the `http_headers` of the provider with each request: ```toml [model_providers.liquid] # ...the settings from step 1... http_headers = { "x-liquid-cap-usd" = "0.50", "x-liquid-ttft-ms" = "3000", "x-liquid-policy" = "economy" } ``` | Header | Sets | | -------------------- | ----------------------------------------------------------------------------------- | | `x-liquid-cap-usd` | The most one request can be charged, in USD. | | `x-liquid-ttft-ms` | The longest wait for the first token, in milliseconds. | | `x-liquid-profile` | The workload profile. | | `x-liquid-policy` | The Auto preference. Ranking by price ignores it. | | `x-liquid-retention` | `none` or `allowed`: whether a provider can keep the prompt. | | `x-liquid-rules` | Any other rule, as JSON. See [Rules in one header](https://inference.ai.exchange/docs/rules.md#rules-in-one-header). | - If the worst-case charge is more than the cap, the exchange refuses the request. It does not shrink the request, except an output limit that it chose (see [Output length](#output-length)). - A header that the exchange cannot parse gets `400 invalid_header`. - No header sets the models. Save them in a preset, and set `model` to the preset name. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Output length Codex sends no output limit. The exchange uses 32,768 tokens. - It lowers the limit to the largest limit of the offers that can take the request. - If your balance or cap is too small, it halves the limit until an offer fits, down to 1,024 tokens. It never changes a limit that the client set. See [Errors](https://inference.ai.exchange/docs/errors.md). An answer that reaches the limit ends with `max_output_tokens`. Codex shows `Incomplete response returned` and sends the request again. The exchange charges again. If this repeats, choose a model that allows longer answers, or raise the cap. ## What isn't supported - **Hosted tools.** The exchange removes `web_search` and runs no search. To search, add `:web` (beta) to the model name. It refuses `file_search`, `code_interpreter` and `image_generation` with `400 unsupported_parameter`. Function tools, MCP tools and `apply_patch` work. - **Stored responses.** The exchange refuses `previous_response_id`, `conversation` and `background: true`, and ignores `store`. Codex sends the full conversation each time, so this does not affect it. ## Control the cost The exchange sets a cap on each request before the first token. To limit total spend, set a spending limit on the Codex key. When the context window fills, Codex asks for a summary, and the exchange charges for it like any request. See [API keys](https://inference.ai.exchange/docs/keys.md). ## Troubleshooting Codex tries a `401`, `402`, `404` or `503` response five times (`Reconnecting... 1/5`). It shows a `400` response at once. | Symptom | Cause and fix | | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | ``Missing environment variable: `LIQUID_API_KEY`.`` | Export the key in this shell and start Codex again. | | `unexpected status 401 Unauthorized` | The key is wrong, revoked or expired. Confirm on the **API keys** page that it is active and has the `spend` scope. | | `unexpected status 404 Not Found: is not provided …` | `model_not_found`. Choose a name from `/model` or `GET /v1/models`. | | `unexpected status 402 Payment Required` | `insufficient_balance`. Add funds on the **Credits** page. | | `"code":"auto_requires_price_bound"` | The model is a router model with no cap. Add the header from [The cap](#the-cap). | | `"code":"unsupported_parameter"` with `"param":"tools[…]"` | Codex sent a hosted tool. Turn off the feature that adds it. See [What isn't supported](#what-isnt-supported). | | ``Model metadata for `` not found``, or `/model` lists OpenAI models. | `api_key_model_discovery = true` is missing from `[features]`, or the model is not in the list. See [Choosing a model](#choosing-a-model). | | `` `wire_api = "chat"` is no longer supported. `` | Delete the `wire_api` line from `[model_providers.liquid]`. | | `codex --version` shows a version before 0.95. | Run `npm install -g @openai/codex@latest`. | | ``--profile `liquid` cannot be used while ~/.codex/config.toml contains legacy …`` | Move your settings from `[profiles.liquid]` or `profile = "liquid"` into `~/.codex/liquid.config.toml`, then delete them. | | Codex uses your usual model and no request reaches the exchange. | The name after `--profile` matches no file, and Codex does not report it. Run `ls ~/.codex/*.config.toml`. | # GitHub Copilot CLI The GitHub Copilot CLI is GitHub's terminal agent. It can send its model requests to a provider that you choose. These steps make the exchange that provider. Copilot reads the settings from environment variables, so they apply only to the shell that you start it from. ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) starts the Copilot CLI with the exchange as its provider. It sets the variables below for that process only. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login --browser --keys liquid keys create laptop --save liquid copilot ``` ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A model that supports tool calls and streaming. Copilot refuses a model without them. ## 1. Install the Copilot CLI ```bash npm install -g @github/copilot ``` ## 2. Point it at the exchange Replace the placeholder with your buyer key. Use the same shell for each command below. ```bash export LIQUID_API_KEY=sk-liquid-... export COPILOT_MODEL=liquid.auto export COPILOT_PROVIDER_API_KEY=$LIQUID_API_KEY export COPILOT_PROVIDER_BASE_URL=https://router.inference.ai.exchange/v1 export COPILOT_PROVIDER_HEADERS="X-Title: GitHub Copilot CLI\nx-liquid-cap-usd: 1.00" export COPILOT_PROVIDER_MAX_OUTPUT_TOKENS=16384 ``` | Setting | Purpose | | ------------------------------------ | ------------------------------------------------------------------------------------- | | `LIQUID_API_KEY` | Your buyer key. | | `COPILOT_PROVIDER_BASE_URL` | The exchange URL, with `/v1`. Copilot sends Chat Completions requests. | | `COPILOT_PROVIDER_API_KEY` | The key that Copilot sends as a bearer token. | | `COPILOT_MODEL` | The model for each request. | | `COPILOT_PROVIDER_HEADERS` | Headers for each request, separated by `\n`. See [Request headers](#request-headers). | | `COPILOT_PROVIDER_MAX_OUTPUT_TOKENS` | The output limit of each request. Without it, Copilot sends no limit. | Copilot needs no GitHub sign-in to use this provider. To stop it from contacting GitHub at all, also set `COPILOT_OFFLINE=true`. ## 3. Check it works ```bash copilot -p "Reply with the word ready" ``` The answer must contain `ready`. Open **Credits → Receipts** to check the charge and the Copilot CLI client label. ## 4. Start a session ```bash copilot ``` ## Request headers `COPILOT_PROVIDER_HEADERS` sends these headers with each model request, and only to the exchange: | Header | Value | Purpose | | ------------------ | -------------------- | ------------------------------------------ | | `X-Title` | `GitHub Copilot CLI` | Identifies the Copilot CLI on the receipt. | | `x-liquid-cap-usd` | `1.00` | Limits the charge for each request. | Keep the `X-Title` header. Without it, the receipt shows the OpenAI SDK as the client. ## Control the cost To set a different cap, change the value of `x-liquid-cap-usd` in `COPILOT_PROVIDER_HEADERS`. With `liquid copilot`, use `--cap`. A task can cause several requests, and the request cap does not limit the total charge. Set a spend limit on the key to limit total spend. See [API keys](https://inference.ai.exchange/docs/keys.md). Use a preset for other purchasing rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Refusals A refused request costs nothing. The Copilot CLI handles each refusal as follows: | Response | What the Copilot CLI does | | ------------ | ---------------------------------------------- | | `400`, `402` | Shows the error at once. | | `429` | Tries 6 times in about a minute, then stops. | | `503` | Tries 6 times in about 20 seconds, then stops. | After you add funds or raise a spend limit, the next request works. ## Sampling parameters Copilot sends `temperature`, `top_p`, `frequency_penalty` and `presence_penalty` with each request. The exchange sends the request only to an offer that accepts `top_p` and both penalties. Most offers accept them. If none of the offers for your model do, the exchange refuses the request and makes no charge. See [Errors](https://inference.ai.exchange/docs/errors.md). ## Troubleshooting | Symptom | Cause and fix | | ------------------------------------ | ----------------------------------------------------------------------------------------- | | `401` | Check that `COPILOT_PROVIDER_API_KEY` holds a valid key that has not expired. | | `402` | Add credit to the workspace or raise the key's spend limit. | | `404` | Keep `/v1` at the end of `COPILOT_PROVIDER_BASE_URL`. | | The error names `no_eligible_offer`. | Read `eligibility.excluded` in the error. It names the rule that each offer did not meet. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the exchange's error codes and causes. ## Next steps - [Market data](https://inference.ai.exchange/docs/market.md): models and live offers. - [API keys](https://inference.ai.exchange/docs/keys.md): key scope and spend limits. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): presets and caps. # Grok Build Grok Build is xAI's terminal coding agent. It can send its model requests to a custom endpoint. These steps make the exchange that endpoint. Grok Build reads its settings from `config.toml` in its home folder. The steps use a separate home folder, so your own `~/.grok` and your xAI sign-in stay as they are. ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) starts Grok Build with the exchange as its endpoint. It writes the settings below and sets `LIQUID_API_KEY` and `GROK_HOME` for that process only. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login --browser --keys liquid keys create laptop --save liquid grok ``` ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A model that supports tool calls and streaming. ## 1. Install Grok Build ```bash npm install -g @xai-official/grok ``` ## 2. Create the folder for the settings ```bash mkdir -p ~/.liquid/grok ``` ## 3. Export the buyer key and the folder Replace the placeholder with your buyer key. Use the same shell for each command below. ```bash export LIQUID_API_KEY=sk-liquid-... export GROK_HOME=$HOME/.liquid/grok ``` | Setting | Purpose | | ---------------- | -------------------------------------------------------------------------- | | `LIQUID_API_KEY` | Your buyer key. The settings name it in `env_key`. | | `GROK_HOME` | The folder that holds `config.toml`. It replaces `~/.grok` for this shell. | ## 4. Write the settings ```bash cat > ~/.liquid/grok/config.toml <<'EOF' [models] default = "liquid" session_summary = "liquid" [model.liquid] model = "liquid.auto" name = "Liquid Inference" base_url = "https://router.inference.ai.exchange/v1" env_key = "LIQUID_API_KEY" api_backend = "chat_completions" context_window = 128000 max_completion_tokens = 16384 extra_headers = { "x-liquid-cap-usd" = "1.00" } EOF ``` | Setting | Purpose | | ----------------------- | ----------------------------------------------------------------------------------------- | | `default` | The model for new sessions. `liquid` is the `[model.liquid]` entry. | | `session_summary` | The model for session titles. Without it, Grok Build names an xAI model. | | `model` | The model that the exchange receives in each request. | | `name` | The name in the model picker. | | `base_url` | The exchange URL, with `/v1`. | | `env_key` | The variable that holds your buyer key. Grok Build sends it as a bearer token. | | `api_backend` | `chat_completions`, which the exchange accepts. | | `context_window` | The input window of the model. Grok Build compacts the conversation before it is full. | | `max_completion_tokens` | The output limit of each request. | | `extra_headers` | Headers that Grok Build sends with each request. See [Request headers](#request-headers). | Grok Build needs no xAI sign-in for these requests. A model that sets its own `env_key` never gets your xAI session token. ## 5. Check it works ```bash grok -p "Reply with the word ready" ``` The answer must contain `ready`. Open **Credits → Receipts** to check the charge and the Grok Build client label. ## 6. Start a session ```bash grok ``` ## Request headers `extra_headers` sends these headers with each model request, and only to the exchange: | Header | Value | Purpose | | ------------------ | ------ | ----------------------------------- | | `x-liquid-cap-usd` | `1.00` | Limits the charge for each request. | Grok Build sends the user agent `grok-shell/`. The exchange reads it and labels the receipt as Grok Build, so the settings need no `X-Title`. ## Control the cost To set a different cap, change the value of `x-liquid-cap-usd`. With `liquid grok`, use `--cap`. A task can cause several requests, and the request cap does not limit the total charge. Set a spend limit on the key to limit total spend. See [API keys](https://inference.ai.exchange/docs/keys.md). Use a preset for other purchasing rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Other requests to xAI Grok Build still contacts xAI for its model catalog, updates and telemetry. These requests do not carry your buyer key. To turn them off, see the `features` and `telemetry` settings in the Grok Build documentation. ## Troubleshooting | Symptom | Cause and fix | | ------------------------------------ | ------------------------------------------------------------------------------------------------ | | Grok Build asks you to sign in. | Check that `GROK_HOME` points at the folder with `config.toml` and that `LIQUID_API_KEY` is set. | | `401` | Check that `LIQUID_API_KEY` holds a valid key that has not expired. | | `402` | Add credit to the workspace or raise the key's spend limit. | | `404` | Keep `/v1` at the end of `base_url`. | | The error names `no_eligible_offer`. | Read `eligibility.excluded` in the error. It names the rule that each offer did not meet. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the exchange's error codes and causes. ## Next steps - [Market data](https://inference.ai.exchange/docs/market.md): models and live offers. - [API keys](https://inference.ai.exchange/docs/keys.md): key scope and spend limits. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): presets and caps. # Pi Pi is a terminal coding agent. It uses the OpenAI Chat Completions format. Pi has no setting for a custom base URL. Each provider and each of its models must be in `~/.pi/agent/models.json`. The block below reads the live model list and adds the exchange as a provider. ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) can start Pi with the exchange as its provider. Pi keeps its settings in a file, so set it up one time with the steps below. `liquid connect pi` prints the same setup with your model filled in. Then start Pi with `liquid pi`. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid connect pi liquid pi ``` ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - Node.js 22.19 or newer, and `jq`. - Pi installed. The package is `@earendil-works/pi-coding-agent`: ```bash # installer curl -fsSL https://pi.dev/install.sh | sh # or with npm npm install -g --ignore-scripts @earendil-works/pi-coding-agent ``` ## 1. Write the provider Pi does not read `/v1/models`. It asks for 16,384 output tokens if a model does not set `maxTokens`. Some offers register a lower limit. This block: - Reads the text models of the book. Pi sends each model to the chat endpoint, so it cannot use an image, speech or video model. - Keeps the other providers in the file. - Writes `contextWindow`, `maxTokens`, `reasoning` and `input` from each model. `apiKey` is the literal `$LIQUID_API_KEY`, which Pi expands at run time. The block sends the key on the models request, so the list includes your presets. ```bash export LIQUID_API_KEY=sk-liquid-... mkdir -p ~/.pi/agent && touch ~/.pi/agent/models.json curl -fsSL "https://router.inference.ai.exchange/v1/models?kind=text" -H "Authorization: Bearer $LIQUID_API_KEY" | jq --slurpfile cur ~/.pi/agent/models.json ' .data as $book | ($cur[0] // {}) | .providers.liquid = { baseUrl: "https://router.inference.ai.exchange/v1", api: "openai-completions", apiKey: "$LIQUID_API_KEY", headers: { "x-liquid-cap-usd": "1.00" }, models: [$book[] | (.liquid_supported_parameters // []) as $p | { id, contextWindow: (.max_input_tokens // 128000), maxTokens: (.max_tokens // 16384), reasoning: ($p | index("reasoning.effort") != null), input: (if ($p | index("input.image")) then ["text", "image"] else ["text"] end) }] }' > ~/.pi/agent/models.json.new && mv ~/.pi/agent/models.json.new ~/.pi/agent/models.json ``` It writes one provider, `liquid`, with a `baseUrl` that ends in `/v1` and `api` set to `openai-completions`. Run the block again when the book changes. It replaces only that provider. ## 2. Run it ```bash pi --model liquid/liquid.auto ``` ## Choosing a model Step 2 starts Pi on `liquid/liquid.auto`. In a session, `/model` changes the model. To set the reasoning effort for one launch, add it to the end of the id: ```bash pi --model liquid/liquid.auto:high ``` `off` sends no effort. Pi sends `xhigh` as `high`. To make a model the default, set `defaultProvider` to `liquid` and `defaultModel` to the model id in `~/.pi/agent/settings.json`. To get the current names, see the **Market** page or use `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). The exchange refuses a name that is not on the book. It never uses another model. ## Control the cost The provider sends `x-liquid-cap-usd` on each request, including `liquid.auto`. To change it, edit the header in `providers.liquid.headers` in `~/.pi/agent/models.json`. The cap applies to each request, not the session. The provider step resets this value each time you run it. The exchange fixes the cap before the first token and returns it in the `x-liquid-cap-usd` response header. To limit the total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md) and [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | The model is not listed. | The book changed after the file was written. Run the block again. | | `401` | `apiKey` in `models.json` must stay `$LIQUID_API_KEY`, including the `$`. A name without the `$` is sent as that literal string. | | `output_bound_unsupported` | `maxTokens` is higher than the offer allows. Run the block again so each model takes its limit from the book. | | `jq` reports a parse error | `models.json` contains comments, which JSON does not allow. Edit the `liquid` provider by hand, or remove the comments and re-run. | ## Next steps - [Codex](https://inference.ai.exchange/docs/connect-codex.md): the same idea for OpenAI's terminal agent. - [API keys](https://inference.ai.exchange/docs/keys.md): scopes and spending limits. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): cost caps and service floors. # Factory Droid Factory Droid is a terminal coding agent. It reads custom models from `~/.factory/settings.json` and does not get the model list itself. The block in step 1 reads the live model list and adds each text model as a custom model. ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) can start Droid with your key. Droid keeps its settings in a file, so set it up one time first. 1. Run `liquid connect factory-droid`. It prints the setup below with your model filled in. 2. Start Droid with `liquid factory-droid`. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid connect factory-droid liquid factory-droid ``` ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A Factory account. Droid asks you to sign in to Factory before it starts, even when you use only custom models. Your Liquid key still pays for each request. - `jq`. - Droid installed: ```bash # installer curl -fsSL https://app.factory.ai/cli | sh # or with npm npm install -g droid ``` ## 1. Write the custom models This block: - Reads the text models of the book. Droid sends each model to the chat endpoint, so it cannot use an image, speech or video model. - Keeps your other settings and your other custom models. It replaces only the models whose name starts with `Liquid: `. - Writes `maxOutputTokens` from each model, because some offers register a lower limit than Droid asks for. The limit is at most 32,768, because the price cap of each request must hold the full limit. - Sets the provider to `generic-chat-completion-api`, which is the OpenAI Chat Completions format. `apiKey` is the literal `${LIQUID_API_KEY}`, which Droid expands at run time. The block sends the key on the models request, so the list includes your presets. ```bash export LIQUID_API_KEY=sk-liquid-... mkdir -p ~/.factory && touch ~/.factory/settings.json curl -fsSL "https://router.inference.ai.exchange/v1/models?kind=text" -H "Authorization: Bearer $LIQUID_API_KEY" | jq --slurpfile cur ~/.factory/settings.json ' .data as $book | ($cur[0] // {}) | .customModels = ( [(.customModels // [])[] | select((.displayName // "") | startswith("Liquid: ") | not)] + [$book[] | { model: .id, displayName: ("Liquid: " + .id), baseUrl: "https://router.inference.ai.exchange/v1", apiKey: "${LIQUID_API_KEY}", provider: "generic-chat-completion-api", maxOutputTokens: ([.max_tokens // 16384, 32768] | min), noImageSupport: ((.liquid_supported_parameters // []) | index("input.image") == null), extraHeaders: { "X-Title": "Factory Droid", "x-liquid-cap-usd": "1.00" } }] )' > ~/.factory/settings.json.new && mv ~/.factory/settings.json.new ~/.factory/settings.json ``` Run the block again when the book changes. ## 2. Run it ```bash droid ``` In a session, run `/model` and choose a model whose name starts with `Liquid: `. To get the current names, see the **Market** page or use `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). The exchange refuses a name that is not on the book. It never uses another model. ## Control the cost Each model sends `x-liquid-cap-usd` in `extraHeaders`. To change it, edit the header in `~/.factory/settings.json`. - The cap applies to each request, not the session. - The block resets this value each time you run it. - The exchange fixes the cap before the first token and returns it in the `x-liquid-cap-usd` response header. To limit the total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md) and [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | Droid shows "Please login with your Factory account". | Droid needs a Factory sign-in before it starts. Choose **Login**, finish the sign-in, then run `/model`. Liquid cannot skip this step. | | The model is not listed. | The book changed after the file was written. Run the block again. | | `401` | The key variable is not set in the shell that starts Droid. Export `LIQUID_API_KEY` and start Droid again. | | `output_bound_unsupported` | `maxOutputTokens` is higher than the offer allows. Run the block again so each model takes its limit from the book. | | `jq` reports a parse error | `settings.json` contains comments, which JSON does not allow. Edit the `customModels` entries by hand, or remove them. | ## Next steps - [Pi](https://inference.ai.exchange/docs/connect-pi.md): the same idea for another terminal agent. - [API keys](https://inference.ai.exchange/docs/keys.md): scopes and spending limits. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): cost caps and service floors. # Oh My Pi Oh My Pi (`omp`) is a terminal agent. It sends OpenAI Chat Completions requests and reads model names and token limits from `GET /v1/models`. This guide uses a separate `liquid` profile, with the provider file `~/.omp/profiles/liquid/agent/models.yml`. Your default profile does not change. ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) starts Oh My Pi with the exchange as its provider. It sets the base URL, the key and the model for that process only. It writes no file of yours. Use it unless you must keep the settings in your own files. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid omp ``` Add `--model liquid.auto:cheapest` to rank the offers by price, or `--cap 0.50` to limit what one request may cost. To set up Oh My Pi 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). - [Bun](https://bun.com/docs/installation) 1.3.14 or newer. - Omp 18.6.1 or newer. ```bash bun install -g @oh-my-pi/pi-coding-agent ``` ## 1. Create the profile directory ```bash mkdir -p ~/.omp/profiles/liquid/agent ``` ## 2. Export the buyer key Replace the placeholder with your buyer key. Use the same shell for the next commands. ```bash export LIQUID_API_KEY=sk-liquid-... ``` | Setting | Why it is set | | ---------------- | -------------------------------------------- | | `LIQUID_API_KEY` | The buyer key that Omp reads from the shell. | ## 3. Write the provider This command replaces the provider file. If the profile has other providers, add the `liquid` block to the existing file instead. ```bash cat > ~/.omp/profiles/liquid/agent/models.yml <<'EOF' providers: liquid: baseUrl: "https://router.inference.ai.exchange/v1" api: openai-completions apiKey: LIQUID_API_KEY auth: apiKey authHeader: true headers: X-Title: Oh My Pi x-liquid-cap-usd: "1.00" discovery: type: openai-models-list EOF ``` | Setting | Why it is set | | ------------------ | ------------------------------------------------------------------ | | `baseUrl` | The exchange URL, with `/v1`. | | `api` | Selects OpenAI Chat Completions. | | `apiKey` | Names the environment variable that holds the buyer key. | | `auth` | Selects API-key authentication. | | `authHeader` | Sends the key as a bearer token. | | `X-Title` | Gives model discovery and chat requests the Oh My Pi client label. | | `x-liquid-cap-usd` | Limits the charge for each request. | | `type` | Selects native model discovery from `GET /v1/models`. | `apiKey` is the name `LIQUID_API_KEY`, without `$`. If the variable is absent, Omp sends the name itself as the key. Discovery uses each model's published output limit, so Omp needs no copied model catalogue. ## 4. Check the model list ```bash omp --profile liquid models liquid --json --no-extensions ``` The list includes the exchange models and your workspace presets. To refresh a cached list after the book changes, run: ```bash omp --profile liquid models refresh liquid --json --no-extensions ``` ## 5. Run it ```bash omp --profile liquid --model liquid/liquid.auto ``` The model selector is `liquid/`. In a session, `/model` changes the model. Start Omp with `--profile liquid` each time. ## Control the cost The provider's `x-liquid-cap-usd` header limits the charge for each request. Change its value to change the cap. A tool task can make several requests, so set a limit on the key to limit total spend. See [API keys](https://inference.ai.exchange/docs/keys.md). For other purchasing rules, add `compat.extraBody.exchange` to the `liquid` provider: ```yaml compat: extraBody: exchange: limits: price: output_usd_per_mtok: 10 ``` Omp sends this `exchange` object in each request body. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | -------------------------- | ------------------------------------------------------------------------------------- | | `omp` cannot find Bun. | Install Bun. Make sure that `bun` is on `PATH`. | | `401` | Export a non-empty `LIQUID_API_KEY`. Keep `apiKey` as the variable name, without `$`. | | The model is absent. | Refresh the list. Make sure that the model has a live offer. | | Omp uses another provider. | Start it with `--profile liquid` and `--model liquid/`. | | `output_bound_unsupported` | Refresh the list to get the current token limits. | ## Next steps - [Pi](https://inference.ai.exchange/docs/connect-pi.md): the separate Pi agent and its provider configuration. - [Market data](https://inference.ai.exchange/docs/market.md): models and live offers. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): price caps and service floors. # Reason Add the exchange to Reason as a Custom provider. Reason then sends its model requests to the exchange. The exchange auctions each request, sets a cap before the first token and charges for the measured use. Reason is a hosted coding agent, built on Pi. The agent runs on the Reason servers, and its commands run on your computer. The model requests come from the Reason servers, so the base URL must be the public URL of the exchange. ## Print the setup with the Liquid CLI `liquid connect reason` prints the values to paste, with your model filled in. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid connect reason ``` ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A Reason plan that permits an API key. Reason permits API keys on its Team plan. ## 1. Install Reason and log in ```bash curl -fsSL https://reasonmachines.com/install.sh | bash reason login ``` ## 2. Connect a Custom provider Do 2a or 2b. Both save the same four values. - The base URL ends in `/v1`. Reason adds `/chat/completions` to it. - The API format is `openai-completions`. This is the Reason default, and the format that this setup is tested with. - List the model IDs. If the list is empty, Reason shows only part of the book. ### 2a. In the Reason settings Open [Reason's model settings](https://reasonmachines.com/customize?settings=1&tab=models) (**Settings → Models**). Then select **Custom → ⋯ → Add another API key**. Enter these values in the order of the dialog: **API key** ``` sk-liquid-... ``` **Base URL** ``` https://router.inference.ai.exchange/v1 ``` **API format** ``` openai-completions ``` **Model IDs** ``` liquid.auto ``` ### 2b. From a terminal This command calls the Reason API with the login of the Reason CLI and the key in `LIQUID_API_KEY`. It replaces the Custom provider of your Reason organization. ```bash creds="$HOME/.config/reason/credentials.json" curl --fail-with-body -sS -X PUT \ "https://api.reasonmachines.com/v3/organizations/$(jq -r .orgId "$creds")/provider-credentials/custom-endpoint" \ -H "Authorization: Bearer $(jq -r .accessToken "$creds")" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg key "$LIQUID_API_KEY" --arg models "liquid.auto" '{ key: $key, config: { CUSTOM_ENDPOINT_BASE_URL: "https://router.inference.ai.exchange/v1", CUSTOM_ENDPOINT_API: "openai-completions", CUSTOM_ENDPOINT_MODELS: $models } }')" ``` If the command fails with `401`, run `reason login` again. ## 3. Run it Reason adds the prefix `byok:custom-endpoint:` to each model name. ```bash reason --model byok:custom-endpoint:liquid.auto ``` To run one task and print the result, add a prompt: `reason "fix the flaky test" --model byok:custom-endpoint:liquid.auto`. ## Notes - Receipts show the client as **Reason**, from the `Reason` user agent. - Reason cannot send the `x-liquid-cap-usd` header. `liquid.auto` applies its own cap. To set a different cap, use a preset that sets one. - Reason stores the key on its servers. Give it a key with a spending limit. See [API keys](https://inference.ai.exchange/docs/keys.md). - This setup is tested with Reason 0.2.260. ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------------------------- | ------------------------------------------------------------------------------ | | Reason refuses the key with `api_keys_require_team_plan`. | Reason connects an API key only on its Team plan. Change the plan in Reason. | | The provider check fails. | The URL is missing `/v1`, or it is not the public URL of the exchange. | | The model does not appear in the picker. | The name is not in the **Model IDs** list. Add it and save the provider again. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | ## Next steps - [Pi](https://inference.ai.exchange/docs/connect-pi.md): the agent that Reason is built on, with the exchange as its provider. - [API keys](https://inference.ai.exchange/docs/keys.md): scopes and spending limits. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): cost caps and service floors. # Crush Crush is the terminal coding agent of Charm. This guide adds the exchange as an OpenAI-compatible provider in a `crushrc` file. Each team member can use the same file with a separate buyer key. ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) starts Crush with the exchange as its provider. It uses its own `crushrc` for that process only, so your own Crush settings are not read or changed. Use it unless you must keep the settings in your own files. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid crush ``` Add `--model liquid.auto:cheapest` to rank the offers by price, or `--cap 0.50` to limit what one request can cost. To set up Crush 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). - Node.js and npm for the install command below. ## 1. Install Crush ```bash npm install -g @charmland/crush ``` ## 2. Create the config directory ```bash mkdir -p ~/.config/crush ``` Crush reads `~/.config/crush/crushrc` on macOS and Linux. If you set `XDG_CONFIG_HOME` or `CRUSH_GLOBAL_CONFIG`, use that directory instead. ## 3. Export the buyer key Replace the placeholder with your buyer key. Use the same shell to start Crush. ```bash export LIQUID_API_KEY=sk-liquid-... ``` | Setting | Purpose | | ---------------- | ------------------------------------------- | | `LIQUID_API_KEY` | The buyer key that the provider file reads. | Keep this key outside the config files. Do not share it. Each team member must use their own key. ## 4. Write the provider file A `crushrc` is a shell script. This file uses the Crush commands `provider add`, `model add` and `model large`. It replaces only `liquid.sh`. ```bash cat > ~/.config/crush/liquid.sh <<'EOF' provider add liquid --name 'Liquid Inference' --type openai-compat \ --base-url 'https://router.inference.ai.exchange/v1' \ --api-key "$LIQUID_API_KEY" \ --extra-header 'X-Title' 'Crush' \ --extra-header 'x-liquid-cap-usd' '1.00' model add 'liquid/liquid.auto' --name 'liquid.auto' --context-window 128000 --default-max-tokens 16384 model large 'liquid/liquid.auto' model small 'liquid/liquid.auto' EOF ``` | Setting | Purpose | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `--name` | The provider and model names in the model picker. | | `--type` | `openai-compat` selects the OpenAI-compatible API for a non-OpenAI provider. | | `--base-url` | The exchange URL, with `/v1`. | | `--api-key` | Reads the buyer key from `$LIQUID_API_KEY`, so the file holds no key. | | `--extra-header` | Sets `X-Title`, which identifies Crush on the receipt, and `x-liquid-cap-usd`, which limits the charge for each request. | | `--context-window` | The number of tokens in the model's context window. | | `--default-max-tokens` | The output limit of a request. | `model large` and `model small` select the model for the main agent and for small tasks. If no live context limit is available, the example uses 128,000 tokens. Change it to match your model. ## 5. Load the file from your crushrc ```bash touch ~/.config/crush/crushrc grep -qxF 'source ~/.config/crush/liquid.sh' ~/.config/crush/crushrc || echo 'source ~/.config/crush/liquid.sh' >> ~/.config/crush/crushrc ``` ## 6. Start Crush and check it works ```bash crush ``` Send a short request. Open **Credits → Receipts** to check the charge and the Crush client label. To use another model, change the model in `liquid.sh`. Use an exact model, a router model or a preset. See [Connect an app](https://inference.ai.exchange/docs/connect-overview.md). ## Control the cost The `x-liquid-cap-usd` header limits each request. Change its value in `liquid.sh` to set a different cap. A task can cause several requests, and the request cap does not limit the total charge. Set a spend limit on the key to limit total spend. See [API keys](https://inference.ai.exchange/docs/keys.md). Use a preset for other purchasing rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | ----------------------- | ------------------------------------------------------------------------------------ | | The provider is absent. | Check that `crushrc` loads `liquid.sh`. Restart Crush. | | `401` | Export `LIQUID_API_KEY` in the shell that starts Crush. Check that the key is valid. | | `403` | Check the key's `spend` scope, address restriction and preset rules. | | `402` | Add credit to the workspace or check the key's spend limit. | | The model is absent. | Check the live offers and the workspace presets. Use an exact model name. | | `cap_exceeds_buyer_cap` | Increase the request cap or select a cheaper model. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the exchange's error codes and causes. ## Next steps - [Market data](https://inference.ai.exchange/docs/market.md): models and live offers. - [API keys](https://inference.ai.exchange/docs/keys.md): key scope and spend limits. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): request caps and presets. # Goose Goose is an agent for the terminal and desktop. This guide adds the exchange as a custom OpenAI-compatible provider. The provider file holds the URL, the model metadata and the headers. Each team member can use the same file with a separate buyer key. ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) starts Goose with the exchange as its provider. It sets the base URL, the key and the model for that process only. It writes no file of yours. Use it unless you must keep the settings in your own files. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid goose ``` Add `--model liquid.auto:cheapest` to rank the offers by price, or `--cap 0.50` to limit what one request can cost. To set up Goose 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). - macOS or Linux for the CLI commands below. - Goose 1.53.0. This setup is tested with that release. Get the CLI or Desktop from the [Goose 1.53.0 release](https://github.com/aaif-goose/goose/releases/tag/v1.53.0). ## 1. Install Goose CLI ```bash curl -fsSL https://github.com/aaif-goose/goose/releases/download/v1.53.0/download_cli.sh | GOOSE_VERSION=1.53.0 CONFIGURE=false bash export PATH="$HOME/.local/bin:$PATH" ``` The installer places the CLI in `~/.local/bin`. The second command adds this directory to the `PATH` of the current shell. ## 2. Create the provider directory ```bash mkdir -p ~/.config/goose/custom_providers ``` Goose uses `~/.config/goose` on macOS and Linux. These commands assume the default directory. - If you set `XDG_CONFIG_HOME`, use its `goose/custom_providers` directory. - If you set `GOOSE_PATH_ROOT`, use its `config/custom_providers` directory. `GOOSE_PATH_ROOT` must be an absolute path. ## 3. Export the buyer key Replace the placeholder with your buyer key. Use the same shell for each CLI command below. ```bash export LIQUID_API_KEY=sk-liquid-... ``` | Setting | Purpose | | ---------------- | ---------------------------------------------- | | `LIQUID_API_KEY` | The buyer key that Goose reads from the shell. | Keep this key outside the provider file. Do not share it. Each team member must use their own key. ## 4. Write the provider file This command replaces only `custom_liquid.json`. Other provider files and the main Goose configuration stay unchanged. ```bash cat > ~/.config/goose/custom_providers/custom_liquid.json <<'EOF' { "name": "custom_liquid", "engine": "openai", "display_name": "Liquid Inference", "api_key_env": "LIQUID_API_KEY", "base_url": "https://router.inference.ai.exchange/v1/chat/completions", "models": [ { "name": "liquid.auto", "context_limit": 128000 } ], "headers": { "X-Title": "Goose", "x-liquid-cap-usd": "1.00" }, "supports_streaming": true, "requires_auth": true, "dynamic_models": true } EOF ``` | Setting | Purpose | | -------------------- | ------------------------------------------------------------ | | `name` | The provider ID for CLI commands. | | `engine` | Selects the OpenAI-compatible engine. | | `display_name` | The provider name in the model picker. | | `base_url` | The full Chat Completions URL, with `/v1/chat/completions`. | | `api_key_env` | Names the variable that holds the buyer key. | | `models` | Supplies the selected model and its context limit. | | `context_limit` | The number of tokens in the selected model's context window. | | `headers` | Sets the request headers below. | | `dynamic_models` | Enables native model discovery from `GET /v1/models`. | | `X-Title` | Identifies Goose on the receipt. | | `x-liquid-cap-usd` | Limits the charge for each request. | | `requires_auth` | Requires a buyer key. | | `supports_streaming` | Enables response streaming. | - Keep `api_key_env` as `LIQUID_API_KEY`, without `$`. Goose reads the secret from the environment or its secret storage. Do not put the key in a header. - The model picker reads the exchange's model list. The file also supplies metadata for the selected model. - If no live context limit is available, the example uses 128,000 tokens. Change it to match your model. For the file format, see [Goose custom providers](https://github.com/aaif-goose/goose/blob/v1.53.0/documentation/docs/getting-started/providers.md#configure-custom-provider). ## 5. Check it works ```bash goose run --provider custom_liquid --model liquid.auto --no-profile --with-builtin developer --max-turns 6 --text "Reply with the word ready" ``` The command selects the provider and model. `--no-profile` omits default extensions. `--with-builtin developer` supplies local file and shell tools. The answer must contain `ready`. Open **Credits → Receipts** to check the charge and the Goose client label. ## 6. Start a session ```bash goose session --provider custom_liquid --model liquid.auto --no-profile --with-builtin developer ``` To use another model, change `--model`. Use an exact model, a router model or a preset. See [Connect an app](https://inference.ai.exchange/docs/connect-overview.md). To set a default provider and model for future sessions, run this command. Select **Configure Providers**, then **Liquid Inference**, then a model. ```bash goose configure ``` ## Use Goose Desktop The CLI and Desktop use the same provider directory on macOS and Linux. Install Desktop from the release above. Write the provider file from step 4 before you open Desktop. Desktop does not inherit the environment of a terminal when you start it from the launcher. Store the buyer key through Desktop instead: 1. Open **Settings → Models → Configure providers**. 2. Select **Liquid Inference**. 3. Enter your buyer key in the **API Key** field. 4. Select **Update Provider**. 5. Open **Settings → Models → Switch models**. 6. Select **Liquid Inference**. 7. Select your model from the list. 8. Select **Select model**. 9. Start a session with a short request. 10. Check its receipt for the Goose client label. Goose stores the key in its keychain. If the keychain is unavailable or disabled, Goose uses `secrets.yaml`. Keep that file private. Do not create a second custom provider through Desktop. Use the shared `custom_liquid.json` file, so each team member has the same headers and URL. See [Goose provider setup](https://github.com/aaif-goose/goose/blob/v1.53.0/documentation/docs/getting-started/providers.md#configure-provider-and-model) for the Desktop controls. ## Control the cost The `x-liquid-cap-usd` header limits each request. Change its value in the provider file to set a different cap. A task can cause several requests, and the request cap does not limit the total charge. Set a spend limit on the key to limit total spend. See [API keys](https://inference.ai.exchange/docs/keys.md). Use a preset for other purchasing rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | ---------------------------------- | --------------------------------------------------------------------------------------------- | | `goose` is absent from `PATH`. | Add `~/.local/bin` to `PATH` in the shell that starts Goose. | | The provider is absent. | Check the provider directory. Check that `custom_liquid.json` is valid JSON. Restart Goose. | | Goose cannot get `LIQUID_API_KEY`. | Export the key in the CLI shell. In Desktop, configure the key through the provider controls. | | `401` | Check that the key is valid and has not expired. | | `403` | Check the key's `spend` scope, address restriction and preset rules. | | `402` | Add credit to the workspace or check the key's spend limit. | | `404` | Keep the full `/v1/chat/completions` URL in `base_url`. | | The model is absent. | Check the live offers and the workspace presets. Use `--model` to select an exact name. | | `output_bound_unsupported` | Select a model with a live offer that accepts the requested output limit. | | `cap_exceeds_buyer_cap` | Increase the request cap or select a cheaper model. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the exchange's error codes and causes. ## Next steps - [Market data](https://inference.ai.exchange/docs/market.md): models and live offers. - [API keys](https://inference.ai.exchange/docs/keys.md): key scope and spend limits. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): request caps and presets. # Hermes Agent Hermes Agent is a terminal agent from Nous Research. The [Liquid Inference plugin](https://github.com/architect-xyz/hermes-liquid-inference) adds the exchange as a model provider. It is also in the [Hermes Agent plugin catalog](https://hermes-agent.nousresearch.com/docs/plugins/liquid-inference-provider). The plugin holds the exchange URL and the request headers. The buyer key stays in the environment. Steps 1 to 5 use the plugin. To set each value yourself, see [Manual configuration](#manual-configuration). ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) can start Hermes Agent with the exchange as its provider. Set Hermes Agent up one time first. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid connect hermes-agent # prints the setup below with your model filled in liquid hermes ``` ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - macOS or Linux, with `git` and `curl`. - On Linux, the `libatomic` library, before step 1. The install command cannot ask for a `sudo` password. The package is `libatomic1` on Debian and Ubuntu, and `libatomic` on Fedora and RHEL. - A model with a context window of 64,000 tokens or more. Hermes Agent refuses a smaller model. - Hermes Agent 0.21.6. This setup is tested with that release. ## 1. Install Hermes Agent ```bash curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/818c13be1dc4fd28987e1e881a9408224afd4535/scripts/install.sh | bash -s -- --commit 818c13be1dc4fd28987e1e881a9408224afd4535 --skip-setup export PATH="$HOME/.local/bin:$PATH" ``` The script comes from the commit of the [Hermes Agent 0.21.6 release](https://github.com/NousResearch/hermes-agent/releases/tag/v0.21.6). It downloads its own Python and Node, and it places `hermes` in `~/.local/bin`. The second command adds that directory to `PATH`. `--skip-setup` omits the setup questions. ## 2. Export the buyer key Use the same shell for each command below. ```bash export LIQUID_API_KEY=sk-liquid-... ``` | Setting | Why it is set | | ----------------- | ----------------------------------------------------- | | `LIQUID_API_KEY` | The buyer key that Hermes Agent reads from the shell. | | `LIQUID_BASE_URL` | The exchange URL, with `/v1`. | `LIQUID_API_KEY` is the buyer key. `LIQUID_BASE_URL` is the exchange URL, with `/v1`. Set it only if the command shows it. To keep a setting between shells, put the line without `export` in `~/.hermes/.env`. Keep that file private. ## 3. Install the plugin and select the model ```bash hermes plugins install architect-xyz/hermes-liquid-inference --ref a7ce8ae3d46253b51c70922c734e3a71ee6bad37 --enable && hermes config set model.provider liquid && hermes config set model.default "liquid.auto" ``` The first command installs one commit of the plugin into `~/.hermes/plugins`. `--ref` fixes the commit, and `--enable` turns the plugin on. The other commands each change one setting in `~/.hermes/config.yaml`: `model.provider` selects the plugin, and `model.default` is the model for new sessions. The plugin sends these headers with each model request, including the requests for a session title, compression and vision: | Header | Value | Purpose | | ------------------ | -------------- | --------------------------------------- | | `X-Title` | `Hermes Agent` | Identifies Hermes Agent on the receipt. | | `x-liquid-cap-usd` | `1.00` | Limits the charge for each request. | Hermes Agent reads the model list from the exchange but not a context window. For a model it does not know, such as `liquid.auto`, it uses 256,000 tokens. To change this, set `model.context_length`. Do not put the key in the configuration file or in a header. ## 4. Check it works ```bash hermes -z "Reply with the word ready" ``` The answer must contain `ready`. Open **Credits → Receipts** to check the charge and the Hermes Agent client label. ## 5. Start a session ```bash hermes ``` To use another model, change `model.default`, or type `/model` in a session. Use an exact model, a router model or a preset. See [Connect an app](https://inference.ai.exchange/docs/connect-overview.md). ## Reasoning effort Task requests use `medium` unless you set a reasoning effort. The plugin sends the level that you select, for example `/reasoning high` in a session or: ```bash hermes config set agent.reasoning_effort high ``` The plugin sends none for `none` or when reasoning is off. The exchange sends a request with a reasoning effort only to an offer that accepts one. Otherwise it refuses with `400 unsupported_parameter` and no charge, and Hermes Agent sends the request again without it. ## The session title request Each session sends one more request for a title, with the same headers and cap. It asks for a JSON schema format. If no offer accepts it, the exchange refuses with `400 unsupported_parameter` and no charge, and Hermes Agent sends it again without the format. To stop the title request: ```bash hermes config set auxiliary.title_generation.enabled false ``` ## Control the cost To set a different cap for each request: ```bash hermes config set model.default_headers.x-liquid-cap-usd 0.25 ``` A header in `model.default_headers` replaces the plugin header of the same name. Do not set `X-Title` there, or the receipt has no client label. One task can send several requests, so the request cap does not limit the total charge. Set a spend limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). For other rules, use a preset. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | ----------------------------------------------------- | ------------------------------------------------------------------------------- | | `hermes` is absent from `PATH`. | Add `~/.local/bin` to `PATH`. | | `Unknown provider 'liquid'` | The plugin is not installed. Run the first command of step 3 again. | | The install command says that the release is too old. | Update Hermes Agent to 0.21.6 or later. | | The context window is below the limit. | Select a model with 64,000 tokens or more. | | A command waits and prints nothing. | Check `LIQUID_BASE_URL`. Hermes Agent waits a long time when it cannot connect. | | `401` | Check that `LIQUID_API_KEY` is set, valid and not expired. | | `403` | Check the key `spend` scope, address restriction and preset rules. | | `402` | Add credit, or check the spend limit of the key. | | `cap_exceeds_buyer_cap` | Increase the request cap or select a cheaper model. | | The model is absent. | Check the live offers and presets. Set `model.default` to an exact name. | See [Errors](https://inference.ai.exchange/docs/errors.md). ## Manual configuration Use the custom OpenAI-compatible endpoint, with no plugin. Do steps 1 and 2, then run these commands in place of step 3. ```bash hermes config set model.provider custom && hermes config set model.base_url "https://router.inference.ai.exchange/v1" && hermes config set model.key_env LIQUID_API_KEY && hermes config set model.default "liquid.auto" && hermes config set model.context_length 128000 && hermes config set model.default_headers.X-Title "Hermes Agent" && hermes config set model.default_headers.x-liquid-cap-usd 1.00 ``` - `model.key_env` names the variable that holds the key. Write `LIQUID_API_KEY` without `$`. - `model.context_length` is the context window of your model. Change it with `model.default`. - Set headers in `model.default_headers`. `extra_headers` on a named provider is not sent with the session title request. ### Reasoning effort with a custom endpoint Hermes Agent sends `reasoning_effort: medium` with each task request and `none` with the title request. If no offer accepts it, the exchange refuses with `400 unsupported_parameter` and no charge, and Hermes Agent sends the request again without it. To prevent the refused requests: ```bash hermes config set model_overrides.custom._default.supports_reasoning false && hermes config set auxiliary.title_generation.enabled false ``` ### Troubleshooting a custom endpoint | Symptom | Cause and fix | | -------------------------------------- | ----------------------------------------------------------- | | A command waits and prints nothing. | Check `model.base_url`. | | The context window is below the limit. | Set `model.context_length` to the real window of the model. | # OpenClaw OpenClaw is a personal agent that runs on your computer and answers in your chat apps. These steps install the Liquid Inference plugin. The plugin adds the exchange to OpenClaw as the model provider `liquid`. - The plugin holds the exchange URL and the request headers. - OpenClaw saves the buyer key in its credential store. ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) can start OpenClaw with the exchange as its provider. OpenClaw keeps its settings in a file, so set it up one time with the steps below. `liquid connect openclaw` prints the same setup with your model filled in. Then start a session with `liquid openclaw`. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid connect openclaw liquid openclaw ``` ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - OpenClaw 2026.10.5 or later, on Node 24.16 or later. OpenClaw refuses to install on an older Node. ## 1. Install OpenClaw ```bash npm install -g openclaw ``` If you already use OpenClaw, keep your install. The steps below add a provider and keep your channels, agents and other providers. ## 2. Export the buyer key Replace the placeholder with your buyer key. Use the same shell for each command below. ```bash export LIQUID_API_KEY=sk-liquid-... ``` | Setting | Purpose | | ---------------- | -------------------------------------------- | | `LIQUID_API_KEY` | The buyer key that step 3 saves in OpenClaw. | After step 3, OpenClaw does not read this variable. A gateway that runs as a service uses the saved key. ## 3. Install the plugin and select the model ```bash openclaw plugins install clawhub:@architect-xyz/openclaw-liquid-inference@0.1.3 --accept-capabilities && printf '%s\n' "$LIQUID_API_KEY" | openclaw models auth paste-api-key --provider liquid --agent main && openclaw config set agents.defaults.model.primary "liquid/liquid.auto" ``` The commands do these things: 1. The first command installs the [Liquid Inference plugin](https://github.com/architect-xyz/openclaw-liquid-inference) from ClawHub. It adds the provider `liquid` and keeps your other providers. `--accept-capabilities` accepts what the plugin declares: a model provider. 2. The second command saves the key in OpenClaw's credential store, under the profile `liquid:manual` of the agent `main`. Other agents use the key of `main`. The key is not in `openclaw.json`. 3. The third command makes the model the default for new sessions. The `&&` at the end of a line stops the list when a command fails. To change the key, run the second command again with the new key. If `agents.defaults.authInheritance` names an agent other than `main`, put that agent's id after `--agent`. The plugin sets these values for the provider: | Setting | Value | Purpose | | --------------- | ---------------------------------------------------------- | -------------------------------------- | | Exchange URL | `https://router.inference.ai.exchange/v1` | Where OpenClaw sends model requests. | | `api` | `openai-completions` | Sends Chat Completions requests. | | `contextWindow` | From the exchange's model list; `128000` for `liquid.auto` | When OpenClaw compacts a long session. | | `maxTokens` | `16384` | The output limit of each request. | The plugin reads the exchange's model list at start, so `openclaw models list --refresh` shows every model on the exchange. OpenClaw sends these headers with each model request: | Header | Value | Purpose | | ------------------ | ---------- | ----------------------------------- | | `X-Title` | `OpenClaw` | Identifies OpenClaw on the receipt. | | `x-liquid-cap-usd` | `1` | Limits the charge for each request. | To send another header, such as a purchasing rule, set `plugins.entries.liquid.config.headers`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## 4. Send a first request ```bash openclaw agent exec --json "Reply with the word ready" ``` The command runs one turn on the local agent and prints JSON. Check two things: - `final` contains `ready`. - **Credits → Receipts** shows the charge and the OpenClaw client label. ## 5. Start a session ```bash openclaw chat ``` `openclaw chat` opens the terminal UI on the local agent. A gateway that is already running uses the new provider without a restart. To use another model, run the last command of step 3 again with that model. Any name on the exchange works as `liquid/`: an exact model, a router model or a preset. See [Connect an app](https://inference.ai.exchange/docs/connect-overview.md). ## Control the cost The `x-liquid-cap-usd` header limits each request. To set a different cap: ```bash openclaw config set plugins.entries.liquid.config.capUsd 0.25 --strict-json ``` The request cap does not limit the total charge. A task can cause several requests, and an agent that answers your chat apps can run many tasks each day. Set a spend limit on the key to limit total spend. See [API keys](https://inference.ai.exchange/docs/keys.md). Use a preset for other purchasing rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Refusals and fallback models OpenClaw sends a refused request again before it shows an error: | Response | What OpenClaw does | | -------- | ----------------------------------------- | | `429` | Tries up to 10 times in about 3 minutes. | | `503` | Tries up to 8 times in about 90 seconds. | | `400` | Shows the error at once. | | `402` | Marks the key as disabled for 10 minutes. | A refused request costs nothing. The exchange sends `402` when the balance or a spend limit does not cover the request. OpenClaw tries the saved key again on the next turn, so a turn after you add funds or raise the limit works. ### Fallback models If `agents.defaults.model.fallbacks` names a model at another provider, OpenClaw sends each refused request to that provider. The turn succeeds, but: - The other provider charges you. - No Liquid Inference receipt shows the request. To keep every request on the exchange, remove the fallbacks, or name only `liquid/` models: ```bash openclaw config set agents.defaults.model.fallbacks '[]' --strict-json ``` ## Troubleshooting | Symptom | Cause and fix | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | The install says that this release requires a newer Node. | Install Node 24.16 or later, then install again. | | `401` | The saved key is wrong or expired. Run the second command of step 3 again with a valid key. | | `403` | Check the key's `spend` scope, address restriction and preset rules. | | `402` | Add credit to the workspace or raise the spend limit, then send again. | | OpenClaw says that `liquid` is disabled after a billing failure. | A `402` refused a request. Add credit or raise the spend limit, then send again. | | A turn succeeded, and no receipt shows it. | A fallback model at another provider answered. See [Refusals and fallback models](#refusals-and-fallback-models). | | `404` | `plugins.entries.liquid.config.baseUrl` is set and does not end in `/v1`. | | OpenClaw says that the plugin needs a newer release. | The plugin needs OpenClaw 2026.10.5 or later. Install OpenClaw again. | | `openclaw config set` says that it requires exclusive offline state ownership. | A newer OpenClaw refuses a config change while a gateway runs. Run `openclaw gateway stop`, then the command, then start the gateway again. | | The error names `no_eligible_offer`. | Read `eligibility.excluded` in the error. It names the rule that each offer did not meet. | | `cap_exceeds_buyer_cap` | Increase the request cap or select a cheaper model. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the exchange's error codes and causes. ## Next steps - [Market data](https://inference.ai.exchange/docs/market.md): models and live offers. - [API keys](https://inference.ai.exchange/docs/keys.md): key scope and spend limits. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): request caps and presets. # OpenCode OpenCode is a terminal agent that accepts custom providers. The Liquid Inference provider uses the OpenAI-compatible adapter and `POST /v1/chat/completions`. ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) starts OpenCode with the exchange as its provider. It sets the base URL, the key and the model for that process only. It writes no file of yours. Use it unless you must keep the settings in your own files. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid opencode ``` Add `--model liquid.auto:cheapest` to rank the offers by price, or `--cap 0.50` to limit what one request may cost. `liquid opencode kimi/auto` runs `kimi-k3` for the main agent and `liquid.auto` for the subagents. See [Start a coding agent](https://inference.ai.exchange/docs/cli.md#start-a-coding-agent). To set up OpenCode yourself, follow the steps below. Each `liquid opencode` session also has the [Liquid MCP server](https://inference.ai.exchange/docs/connect-mcp-server.md) with the key of the session, so the agent can buy images, speech and video. When your OpenCode config already holds a `liquid` server from `liquid mcp opencode`, the session uses that server. ## 1. Install OpenCode Install Node.js and `jq` first. The configuration script requires `jq`. ```bash npm install -g opencode-ai ``` ## 2. Add the provider This script adds `liquid` to `~/.config/opencode/opencode.json`. It keeps your other providers and preferences, and creates the file if it does not exist. If the file contains invalid JSON, the script stops and leaves it unchanged. ```bash ( set -eu opencode_config_dir=~/.config/opencode mkdir -p "$opencode_config_dir" touch "$opencode_config_dir/opencode.json" opencode_config_tmp=$(mktemp "$opencode_config_dir/opencode.json.XXXXXX") trap 'rm -f "$opencode_config_tmp"' EXIT jq --slurpfile current "$opencode_config_dir/opencode.json" ' . as $setup | ($current[0] // {}) | .provider.liquid = $setup.provider.liquid | ."$schema" //= $setup."$schema" ' <<'EOF' > "$opencode_config_tmp" { "$schema": "https://opencode.ai/config.json", "provider": { "liquid": { "npm": "@ai-sdk/openai-compatible", "name": "Liquid Inference", "options": { "baseURL": "https://router.inference.ai.exchange/v1", "apiKey": "{env:LIQUID_API_KEY}", "headers": { "X-Title": "OpenCode" } }, "models": {"liquid.auto":{}} } } } EOF mv "$opencode_config_tmp" "$opencode_config_dir/opencode.json" ) ``` The API key stays an environment reference, `{env:LIQUID_API_KEY}`. The provider sends `X-Title: OpenCode`, so receipts identify the tool. ## 3. Run it ```bash export LIQUID_API_KEY=sk-liquid-... opencode ``` Select the **Liquid Inference** provider and a model with `/models`. To select a model at start, set `model` in `opencode.json`: ```json { "model": "liquid/liquid.auto" } ``` Newer OpenCode releases have no `--model` flag. For one headless request, use: ```bash opencode run 'Say ready.' ``` ## Model list OpenCode does not read `GET /v1/models`. Its model list comes from the configuration file, which holds the selected model, the router models and your workspace's presets. Run the script again after you add a preset. To print the configured names, use: ```bash opencode models liquid ``` The CLI sorts this list by name. The exchange refuses a model name that it does not know. It never substitutes another model. ## Configuration precedence OpenCode merges configuration files, and a later source overrides an earlier one. Project configuration can override this guide's global configuration. `OPENCODE_CONFIG_CONTENT` overrides the project configuration at runtime. If OpenCode uses another URL or model, check the project's `opencode.json` and `.opencode` directory, `OPENCODE_CONFIG` and `OPENCODE_CONFIG_CONTENT`. See [OpenCode configuration](https://opencode.ai/docs/config/). ## Control the cost `liquid.auto` has a cap of its own. To set the most that you accept for one request, add `x-liquid-cap-usd` to the model's `headers` in `provider.liquid.models`. For a cap that applies to other models, add `x-liquid-cap-usd` to `provider.liquid.options.headers`. A model's header overrides the same provider header. Check both locations when you change a cap. Each cap applies to one request. To limit total spend, set a limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md) and [Purchasing rules](https://inference.ai.exchange/docs/rules.md). OpenCode 1.18.34 requests up to 32,000 output tokens when a custom model has no output limit. An offer must support that limit and fit within the price cap. To use a smaller limit, set the model's `limit.output`. ## Troubleshooting | Symptom | Cause and fix | | ----------------------------- | --------------------------------------------------------------------------------------------------------------- | | The model is absent. | Run the configuration script again after you add a preset. | | `401` | Export `LIQUID_API_KEY` in the shell that starts OpenCode. Keep the environment reference in the configuration. | | `jq` reports a parse error. | The script requires valid JSON. OpenCode also accepts JSONC, but this script cannot edit comments. | | The wrong URL or cap is used. | Check project configuration, runtime overrides, and model headers. | | `output_bound_unsupported` | Reduce `limit.output`, or select an offer that supports the requested output limit. | | No offer fits the cap. | Check the model's output limit and prices. Change the limit, cap, or model. | ## Next steps - [Pi](https://inference.ai.exchange/docs/connect-pi.md): another terminal agent with a custom provider. - [Market data](https://inference.ai.exchange/docs/market.md): the current model names and offers. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): request caps and service floors. # Paperclip Paperclip is an open-source control plane that runs AI agents for a company. An agent that uses the OpenCode runtime can use the exchange as its model provider. Paperclip has no Liquid Inference connector in its catalog. Use the OpenCode adapter's provider setting instead. This guide follows the Paperclip documentation for the `opencode_local` adapter. Check each step against your Paperclip release. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A Paperclip agent that uses the OpenCode adapter (`opencode_local`). - A run on the same machine as Paperclip. Paperclip applies the provider setting only to local runs. ## 1. Store the buyer key Store the key as a Paperclip secret. Do not put the key in the provider setting. Each agent can use its own key and its own spend limit. ## 2. Set the adapter environment In the agent's OpenCode configuration, add these values to `env`. Refer to the secret for `LIQUID_API_KEY`. | Variable | Value | | -------------------------------- | --------------------------------------------------------------- | | `LIQUID_API_KEY` | A reference to the secret from step 1. | | `PAPERCLIP_OPENCODE_PROVIDERS` | The provider definition below, as one line of JSON. | | `PAPERCLIP_OPENCODE_SMALL_MODEL` | `liquid/liquid.auto`. OpenCode uses it for its helper requests. | | `OPENCODE_ALLOW_ALL_MODELS` | `true`. This skips the check of OpenCode's own model list. | Use this value for `PAPERCLIP_OPENCODE_PROVIDERS`: ```json { "liquid": { "npm": "@ai-sdk/openai-compatible", "name": "Liquid Inference", "options": { "baseURL": "https://router.inference.ai.exchange/v1", "apiKey": "{env:LIQUID_API_KEY}", "headers": { "X-Title": "Paperclip" } }, "models": { "liquid.auto": {} } } } ``` The `X-Title` header identifies Paperclip on the receipt. Paperclip expands `{env:LIQUID_API_KEY}` when it starts the run. ## 3. Choose the model Set the agent's `model` to `liquid/liquid.auto`. The format is `provider/model`. OpenCode accepts only a model that the `models` map lists. Add one entry for each model that the agent can use, for example `"liquid.auto:cheapest": {}`. See [Market data](https://inference.ai.exchange/docs/market.md) for the model names. ## 4. Check the first run Run the agent with a short task. Open **Credits → Receipts** and check for the charge and the `Paperclip` client label. ## Control the cost Add `x-liquid-cap-usd` to `options.headers` to limit what one request may cost. Paperclip's own budget does not read the exchange's charge. Set a spend limit on the key to limit the total. See [API keys](https://inference.ai.exchange/docs/keys.md) and [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Limits of this setup - Paperclip applies `PAPERCLIP_OPENCODE_PROVIDERS` only when the agent's `dangerouslySkipPermissions` setting is on. This is the default. - The provider does not appear in Paperclip's **Connectors** page. Paperclip shows no connection and no credit information for it. ## Troubleshooting | Symptom | Cause and fix | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | `Configured OpenCode model is unavailable` | Add the model to `models`, or set `OPENCODE_ALLOW_ALL_MODELS` to `true`. | | The run stops before the first answer | Set `PAPERCLIP_OPENCODE_SMALL_MODEL` to a model that the exchange serves. | | The provider setting has no effect | Check that the run is local and that `dangerouslySkipPermissions` is on. Check that the JSON is valid. | | `401` | Check that the secret holds a valid key. | | `402` | Add credit to the workspace or check the key's spend limit. | | `output_bound_unsupported` | Select a model with a live offer that accepts the requested output limit. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the exchange's error codes. ## Next steps - [OpenCode](https://inference.ai.exchange/docs/connect-opencode.md): the same runtime, set up without Paperclip. - [API keys](https://inference.ai.exchange/docs/keys.md): key scope and spend limits. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): request caps and presets. # Cursor Cursor is a code editor with a built-in AI assistant. It uses the OpenAI Chat Completions format and lets you replace the OpenAI base URL. Set the URL to the exchange and add your model by name. These steps configure the Cursor editor. They do not configure Cursor CLI. ## Print the setup with the Liquid CLI Cursor has its own settings screen, so the [Liquid CLI](https://inference.ai.exchange/docs/cli.md) cannot start it. `liquid connect cursor` prints the values to paste, with your model filled in. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid connect cursor ``` To enter the values 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). - Cursor installed, with a project open. ## 1. Override the base URL Open **Settings → Models**. Turn on **Override OpenAI Base URL** and enter: ``` https://router.inference.ai.exchange/v1 ``` The URL ends in `/v1`, because Cursor adds `/chat/completions` to it. ## 2. Paste the key In the **OpenAI API Key** field, paste your key: ``` sk-liquid-... ``` ## 3. Add the models by name Cursor lists only the models that it knows. Add each name to show it in the picker. Then use **Verify** to test the key with the first model. ``` liquid.auto ``` The connect guide in the console lists your chosen model, the router models and your presets. The exchange resolves each name on every request, so the names stay correct when offers change. You can add any other model on the book. To get the current names, see the **Market** page or use `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). Cursor cannot send the `x-liquid-cap-usd` header. `liquid.auto` has a cap of its own, so it works. To change the cap, use a preset that sets one. ## Notes - Some Cursor features, such as Tab completion and some agent modes, can go through the Cursor servers, with or without this setting. The override applies to chat and to the models that you add here. - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). ## Troubleshooting | Symptom | Cause and fix | | ---------------------------------------- | --------------------------------------------------------------------------------------------- | | Verifying the key fails. | The base URL override is off, the URL is missing `/v1`, or the model name is not on the book. | | The model does not appear in the picker. | You have not added it by name under **Settings → Models**. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | ## Next steps - [Cline](https://inference.ai.exchange/docs/connect-cline.md): a VS Code agent that uses the same OpenAI format. - [API keys](https://inference.ai.exchange/docs/keys.md): scopes and spending limits. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): cost caps and service floors. # DeepSeek Harness DeepSeek Harness (`dsh`) is DeepSeek's open-source agent harness. It runs as a web interface or a desktop app. These steps add the exchange to it as a custom provider with an OpenAI-compatible endpoint. A patch file holds the settings, and one environment variable holds the key. They apply only to the shell that you start it from. ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) writes the patch and starts `dsh web` with the exchange as its provider. It sets the key for that process only. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login --browser --keys liquid keys create laptop --save liquid dsh ``` `liquid dsh` reads the model list of the exchange when it starts. It writes each router model, preset and model that accepts tool calls into the patch, so the model list of DeepSeek Harness shows them. - Each entry has the input window of its model and states whether the model reads images. - `--filter` narrows the list. - If the exchange cannot be reached, the patch holds the default model only. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A model that supports tool calls and streaming. ## 1. Install DeepSeek Harness ```bash npm install -g @deepseek-ai/dsh ``` ## 2. Export the buyer key Replace the placeholder with your buyer key. Use the same shell for each command below. ```bash export LIQUID_API_KEY=sk-liquid-... ``` | Setting | Purpose | | ---------------- | -------------------------------------------------------------------- | | `LIQUID_API_KEY` | Your buyer key. The patch names this variable and never holds a key. | ## 3. Write the patch DeepSeek Harness reads extra settings from a patch file. Create the folder, then write the file: ```bash mkdir -p ~/.liquid ``` ```bash cat > ~/.liquid/dsh.patch.yml <<'EOF' - id: llm-pi-ai config: providers: liquid: displayName: Liquid Inference apiKeyEnv: LIQUID_API_KEY api: openai-completions baseURL: "https://router.inference.ai.exchange/v1" headers: X-Title: DeepSeek Harness x-liquid-cap-usd: "1.00" defaultContextWindow: 128000 defaultMaxTokens: 16384 cacheRetention: long compat: supportsLongCacheRetention: true models: - id: "liquid.auto" input: [text, image] - id: agent-default-model config: provider: liquid model: "liquid.auto" - {id: web-search-deepseek, disabled: true} - id: tool-web config: search: false EOF ``` | Setting | Why it is set | | ---------------------------- | ------------------------------------------------------------------------------------------------- | | `displayName` | Shows the provider in the model list. | | `apiKeyEnv` | Names the variable that holds the key. | | `api` | Selects OpenAI Chat Completions. | | `baseURL` | The exchange URL, with `/v1`. | | `X-Title` | Gives each request the DeepSeek Harness client label. | | `x-liquid-cap-usd` | Limits the charge for each request. | | `defaultContextWindow` | The input window of the model. DeepSeek Harness compacts a session by it. | | `defaultMaxTokens` | Limits the output of each request, and so the cap of each request. | | `cacheRetention` | Sends the session as `prompt_cache_key`, so the exchange keeps the turns of one session together. | | `supportsLongCacheRetention` | Allows `cacheRetention: long` for this provider. | | `input` | Lets the model receive images. | | `provider` | Makes the exchange the provider of the default model. | | `model` | Makes the model the default. | | `search` | Turns off the `web_search` tool and keeps `web_fetch`. | The last lines turn off web search. The search backend `web-search-deepseek` calls DeepSeek's own API with `DEEPSEEK_API_KEY`, which the exchange does not accept. The `web_fetch` tool reads a page over plain HTTP and stays on. ## 4. Start the web interface ```bash dsh web --patch ~/.liquid/dsh.patch.yml ``` Open the address that `dsh` prints. Choose a workspace. Send a short request, then open **Credits → Receipts** to check the charge. ## Notes - To use another model, add an entry under `models` and change `model`. Use an exact model, a router model or a preset. To get the current names, see the **Market** page. See [Market data](https://inference.ai.exchange/docs/market.md). - The cap limits each request. It does not limit the total charge of a task. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - The model sends no reasoning effort. For a reasoning model, declare `reasoningEfforts` on its entry, for example `{low: low, high: high}`. Each key is a level of DeepSeek Harness and each value is the effort that the exchange receives. The exchange sends a request with a reasoning effort only to an offer that accepts one. - The patch uses the route of DeepSeek Harness for other endpoints. DeepSeek's own route also sends the session record and the plugin list in each request. This route sends neither. ## Troubleshooting | Symptom | Cause and fix | | ------------------------------------------------ | ---------------------------------------------------------------------------- | | `MISSING_CREDENTIAL` | Export `LIQUID_API_KEY` in the shell that starts `dsh`. | | `401` | The key is empty, wrong or expired. Check `LIQUID_API_KEY`. | | `403` | Check the key's `spend` scope, address restriction and preset rules. | | `402` | Add credit to the workspace or check the key's spend limit. | | `404` | Keep `/v1` at the end of `baseURL`. | | `model_not_found` | The model ID is not on the book. Use an exact name from the **Market** page. | | `reasoning.effort` is not supported by any offer | Remove `reasoningEfforts` from the model entry. | | A refusal that names the output limit | Set `defaultMaxTokens` to 16384 or less. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the exchange's error codes and causes. ## Next steps - [Pi](https://inference.ai.exchange/docs/connect-pi.md): another terminal agent with a provider file. - [Market data](https://inference.ai.exchange/docs/market.md): models and live offers. - [API keys](https://inference.ai.exchange/docs/keys.md): scopes and spending limits. # Z Code Z Code is the desktop coding agent from Z.ai. It keeps its model providers in `~/.zcode/v2/provider_config.json`. Add the exchange to that file as a custom provider with an OpenAI-compatible endpoint. Step 1 writes the provider with a script. To make the same settings in the app, see [Set up in the app](#set-up-in-the-app). ## Print the setup with the Liquid CLI Z Code has its own settings screen, so the [Liquid CLI](https://inference.ai.exchange/docs/cli.md) cannot start it. `liquid connect zcode` prints the values to paste, with your model filled in. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid connect zcode ``` To enter the values 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). - Z Code installed from [zcode.z.ai](https://zcode.z.ai). This setup is tested with Z Code 3.15.1. - `jq`, for the script in step 1. ## 1. Write the provider Quit Z Code. Then replace the placeholder with your buyer key and run this script: ```bash export LIQUID_API_KEY=sk-liquid-... set -e umask 077 zcode_config="$HOME/.zcode/v2/provider_config.json" mkdir -p "$HOME/.zcode/v2" [ -s "$zcode_config" ] || printf '{"schemaVersion":1,"config":{}}' > "$zcode_config" jq --arg key "$LIQUID_API_KEY" --argjson names '["liquid.auto"]' ' .config.providerConfigRules.providerRules = [(.config.providerConfigRules.providerRules // [])[] | select(.providerId != "liquid")] + [{ providerId: "liquid", providerName: "Liquid Inference", enabled: true, config: { group: "standard-personal", access: { type: "api-key", apiKey: $key }, api: { type: "openai-chat-completions", baseUrl: "https://router.inference.ai.exchange/v1", headers: { "x-liquid-cap-usd": "1.00" } }, personalModelIds: $names, modelOrder: $names } }] | .config.modelConfigRules.providerModelRules = [(.config.modelConfigRules.providerModelRules // [])[] | select(.providerId != "liquid")] + [$names[] | { providerId: "liquid", modelId: ., config: ({ optionSpecs: { maxOutputTokens: { max: 8192 }, reasoningLevel: { map: "{}" } } }) }] | .config.modelConfigRules.manualProviderModelRules = [(.config.modelConfigRules.manualProviderModelRules // [])[] | select(.providerId != "liquid")] | if .config.providerOrder then .config.providerOrder = [.config.providerOrder[] | select(. != "liquid")] + ["liquid"] else . end | .config.defaultModelSelection = { providerId: "liquid", modelId: "liquid.auto" } ' "$zcode_config" > "$zcode_config.new" && mv "$zcode_config.new" "$zcode_config" ``` The script adds a provider with the ID `liquid` and keeps your other providers. It replaces a provider with the same ID. It does these things: - It adds each model name in the list as a model of the provider. - It sets the output limit of each model to 8,192 tokens, and it sets the reasoning mapping to `{}`. See [Notes](#notes). - It sets the context window of the model that you picked, when the book states it. - It adds a header that caps each request. - It makes the model that you picked the default model of Z Code. Z Code keeps the key in the file. The script sets `umask 077`, so a new file is readable only by you. ## 2. Start Z Code Start Z Code. The model selector shows the models under **Liquid Inference**. To use other models, run the script again with a different list of names. ## Set up in the app The app has no field for headers. The cap needs one edit of the file. ### Add the provider Open the model settings from the model selector. Click **Create custom provider**, and enter a name: ``` Liquid Inference ``` Do not select a vendor. Set **API format** to **Chat completions**: ``` Chat completions ``` Enter the base URL. It ends in `/v1`, because Z Code adds `/chat/completions` to it: ``` https://router.inference.ai.exchange/v1 ``` Paste your key into **API key**: ``` sk-liquid-... ``` ### Add the models Click **Add model** and type a model ID. Add each ID separately. For example: ``` liquid.auto ``` Z Code does not read the model list of the exchange. Use an exact model, a router model or a preset. To get the current names, see the **Market** page. See [Market data](https://inference.ai.exchange/docs/market.md). Open the **Advanced settings** of each model. Set **Max output tokens** and **Reasoning parameter mapping**: ``` 8192 ``` ``` {} ``` Set **Context window** to the real window of the model. Z Code uses it to decide when to compact a session. Turn on image input only for a model that reads images. Then enable the provider. ### Set a cap on each request Quit Z Code. Open `~/.zcode/v2/provider_config.json` and find the provider in `config.providerConfigRules.providerRules`. Add this to the `api` object of the provider: ``` {"headers":{"x-liquid-cap-usd":"1.00"}} ``` The app keeps these headers when you change the provider in the app later. ## Notes - **Output limit.** For a model that it does not know, Z Code asks for 32,000 output tokens. The exchange sends a request only to an offer that accepts its output limit. Thus the setup sets a smaller limit. - **Reasoning effort.** For a model that it does not know, Z Code also sends a high reasoning effort. The exchange sends such a request only to an offer that accepts one. When no offer accepts one, the exchange refuses the request. The mapping `{}` stops Z Code from sending reasoning parameters. The model then uses its default reasoning. - The cap limits each request. It does not limit the total charge of a task. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - The receipt names Z Code as the client. You do not need to set a header for this. - Use a preset for other purchasing rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `401` | The key is empty, wrong or expired. Paste it again into **API key**, or run the script again. | | `403` | Check the key's `spend` scope, address restriction and preset rules. | | `402` | Add credit to the workspace or check the key's spend limit. | | `404` | Keep `/v1` at the end of the base URL. | | `model_not_found` | The model ID is not on the book. Use an exact name from the **Market** page. | | `reasoning.effort` is not supported by any offer | Set the reasoning parameter mapping of the model to `{}`. | | A refusal that names the output limit | Set **Max output tokens** of the model to 8192 or less. | | The provider sends no cap. | Add the headers to the `api` of the provider in `~/.zcode/v2/provider_config.json`. Z Code does not read providers from `~/.zcode/v2/config.json`. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the exchange's error codes and causes. ## Next steps - [Cline](https://inference.ai.exchange/docs/connect-cline.md): the VS Code agent, with the same kind of provider. - [Market data](https://inference.ai.exchange/docs/market.md): models and live offers. - [API keys](https://inference.ai.exchange/docs/keys.md): scopes and spending limits. # Cline Cline has a CLI and a VS Code extension. Both have an "OpenAI Compatible" provider that uses Chat Completions. Set that provider to the exchange, and Cline sends its requests there. The **CLI verified** label applies to the Cline CLI. It does not apply to the VS Code extension. ## Start with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) can start Cline with the exchange as its provider. Cline keeps its settings in a file, so set it up one time with the steps below. `liquid connect cline` prints the same setup with your model filled in. Then start Cline with `liquid cline`. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid connect cline liquid cline ``` ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - Cline installed from the VS Code marketplace. ## 1. Choose the OpenAI Compatible provider Open the settings panel of Cline and set the API provider to **OpenAI Compatible**. ## 2. Set the base URL and the key Set **Base URL** to: ``` https://router.inference.ai.exchange/v1 ``` The base URL ends in `/v1`, because Cline adds `/chat/completions` to it. Paste your key into **API Key**: ``` sk-liquid-... ``` ## 3. Pick the model Cline reads `GET /v1/models` and shows the names in its **Model ID** list. The order is `liquid.auto`, the other router models, your presets, then the models that support tools. Pick a name, for example: ``` liquid.auto ``` You can also type any other name on the book as a custom model ID. To get the current names, see the **Market** page. See [Market data](https://inference.ai.exchange/docs/market.md). ## 4. Set the editor headers Add these custom headers to the OpenAI Compatible provider: **X-Title** ``` Cline ``` **x-liquid-cap-usd** ``` 1.00 ``` `X-Title` identifies Cline on the receipt. The cap applies to each request. ## CLI procedure Use Node 22 or newer and `jq`. ### 1. Install the CLI ```bash npm install -g cline npm rebuild -g cline ``` ### 2. Configure the CLI Replace the key placeholder before you run this command: ```bash export LIQUID_API_KEY=sk-liquid-... set -e umask 077 cline_settings="$HOME/.cline/data/settings/providers.json" cline_saved=$(mktemp) trap 'rm -f "$cline_saved" "$cline_settings.new"' EXIT if [ -f "$cline_settings" ]; then jq '.' "$cline_settings" > "$cline_saved" else printf '{}' > "$cline_saved" fi cline auth --provider openai-compatible --apikey "$LIQUID_API_KEY" --modelid 'liquid.auto' --baseurl 'https://router.inference.ai.exchange/v1' jq --slurpfile saved "$cline_saved" '$saved[0] * . | .providers["openai-compatible"].settings |= (.maxTokens = 16384 | .headers = ((.headers // {}) | with_entries(select((.key | ascii_downcase) != "x-title" and (.key | ascii_downcase) != "x-liquid-cap-usd")) + {"X-Title": "Cline", "x-liquid-cap-usd": "1.00"}))' "$cline_settings" > "$cline_settings.new" && mv "$cline_settings.new" "$cline_settings" ``` The script uses `cline auth`, then merges the provider settings. - It keeps other providers, preferences and custom headers. - It sets the exchange URL, the key, the model, the cap header and `X-Title: Cline`. - It stores `maxTokens: 16384`. Release 3.0.69 does not send this limit in its API requests. The exchange applies its default output limit when the request has no limit. ### 3. Start the CLI ```bash cline --provider openai-compatible --model 'liquid.auto' ``` ## Notes - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - Cline writes full files, so use a model with a large output limit. If the offers allow less output than Cline asks for, the exchange lowers the request to their limit, and a long answer stops there. See [Errors](https://inference.ai.exchange/docs/errors.md). ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | ---------------------------------------------------------------------------------------- | | `401` or an authentication error. | The key is empty or wrong, or the provider is not set to "OpenAI Compatible". | | `model_not_found`. | The model ID is not on the book. Check it against `GET /v1/models`. | | The model list is empty. | The base URL is wrong. It must end in `/v1`. | | Your presets are not in the list. | The API key is empty or is from a different workspace. | | Answers stop at the output limit. | The offers' output limit is below what Cline asked for. Choose a model that allows more. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | ## Next steps - [Cursor](https://inference.ai.exchange/docs/connect-cursor.md): the editor, with its own model settings. - [Roo Code](https://inference.ai.exchange/docs/connect-roo-code.md): a VS Code agent with a model for each mode. - [API keys](https://inference.ai.exchange/docs/keys.md): scopes and spending limits. # Roo Code Roo Code is a VS Code extension. A VS Code agent with a model for each mode. It has an **OpenAI Compatible** provider. Set that provider to the exchange. The exchange auctions each request, sets a cap before the first token and charges for the measured use. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - VS Code with Roo Code installed. ## 1. Choose the provider Open the provider settings of Roo Code. Choose the **OpenAI Compatible** provider. ## 2. Set the base URL The URL ends in `/v1`, because Roo Code adds `/chat/completions` to it. ``` https://router.inference.ai.exchange/v1 ``` ## 3. Paste the key ``` sk-liquid-... ``` ## 4. Set the model Enter a model name from the book. To get the current names, use `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). ``` liquid.auto ``` ## Notes - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` response header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - Use a preset for other purchasing rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | ------------------------------------------------------------------------------ | | `401` | The key is wrong, revoked or expired. Paste it again. | | `404` | Keep `/v1` at the end of the base URL. | | `model_not_found` | The model is not on the book. Use an exact name from `GET /v1/models`. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the error codes. ## Next steps - [Kilo Code](https://inference.ai.exchange/docs/connect-kilo-code.md): a VS Code agent with the same provider type. - [Cline](https://inference.ai.exchange/docs/connect-cline.md): another VS Code agent that uses the OpenAI format. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): cost caps and service floors. # Kilo Code Kilo Code is a VS Code agent. It has an **OpenAI Compatible** provider. Set that provider to the exchange, and Kilo Code sends its requests there. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - VS Code with Kilo Code installed. ## 1. Choose the provider Open the provider settings of Kilo Code. Choose the **OpenAI Compatible** provider. ## 2. Set the base URL The URL ends in `/v1`, because Kilo Code adds `/chat/completions` to it. ``` https://router.inference.ai.exchange/v1 ``` ## 3. Paste the key ``` sk-liquid-... ``` ## 4. Set the model Enter a model name from the book. To get the current names, use `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). ``` liquid.auto ``` ## Notes - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` response header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - Use a preset for other purchasing rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | ------------------------------------------------------------------------------ | | `401` | The key is wrong, revoked or expired. Paste it again. | | `404` | Keep `/v1` at the end of the base URL. | | `model_not_found` | The model is not on the book. Use an exact name from `GET /v1/models`. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the error codes. ## Next steps - [Roo Code](https://inference.ai.exchange/docs/connect-roo-code.md): a VS Code agent with the same provider type. - [Cline](https://inference.ai.exchange/docs/connect-cline.md): another VS Code agent that uses the OpenAI format. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): cost caps and service floors. # RikkaHub RikkaHub is an open-source chat app for Android. You can add a provider with an OpenAI-compatible endpoint to it. Set that provider to the exchange. The exchange sets a cap before the first token and charges for measured use. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - RikkaHub installed from [rikka-ai.com](https://rikka-ai.com/download) or Google Play. ## 1. Add a provider Open **Settings → Providers**. Tap the **+** button at the top of the page. In the **Add Provider** dialog, keep the provider type **OpenAI**. Enter a name in **Name**: ``` Liquid Inference ``` ## 2. Set the key and the base URL Paste your key into **API Key**: ``` sk-liquid-... ``` Replace the value in **API Base Url** with this URL: ``` https://router.inference.ai.exchange/v1 ``` The base URL ends in `/v1`, because RikkaHub adds `/chat/completions` to it. Keep the default value of **API Path**. Keep **Response API** off. Then tap **Add**. ## 3. Add the models Open the new provider and go to the **Models** tab. RikkaHub reads `GET /v1/models` from the exchange and shows the names in a list. Open the list with the button that shows the number of models. Select each name that you want, for example: ``` liquid.auto ``` To use a name that is not in the list, tap **Add New Model** and type it in **Model ID**. To get the current names, see the **Market** page. See [Market data](https://inference.ai.exchange/docs/market.md). RikkaHub sets the abilities of a model from the model ID. It does not know the names of router models or presets. To use tools or a reasoning level with one of these names, edit the model and turn on **Tool** or **Reasoning** in **Abilities**. ## 4. Set a cap on each request Open the **Advanced Settings** tab of the provider. Under **Custom Headers**, add a header. Set **Header Name** to: ``` x-liquid-cap-usd ``` Set **Header Value** to the cap in USD: ``` 1.00 ``` RikkaHub sends the header with each request to this provider. The cap limits each request. It does not limit the total charge of a conversation. Auto refuses a request that has no price bound. See [Auto Router](https://inference.ai.exchange/docs/auto.md). ## The client on the receipt The receipt names RikkaHub and its version as the client. You do not need to set anything for this. Do not set a value in **Settings → Preferences → Network → User-Agent**. If you set one, the receipt does not name RikkaHub. ## Notes - The exchange sets a cap on each request. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - If a model has the **Reasoning** ability and you select a reasoning level, RikkaHub sends `reasoning_effort`. The exchange sends a request with a reasoning effort only to an offer that accepts one. - Use a preset for other purchasing rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | -------------------------------------------------------------------------------- | | `401` | The key is empty, wrong or expired. Paste it again into **API Key**. | | `403` | Check the key's `spend` scope, address restriction and preset rules. | | `402` | Add credit to the workspace or check the key's spend limit. | | `404` | Keep `/v1` at the end of the base URL, and keep `/chat/completions` as the path. | | The model list is empty. | Check the base URL and the key. RikkaHub reads the list with the same key. | | `model_not_found` | The model ID is not on the book. Use an exact name from the **Market** page. | | `auto_requires_price_bound` | Add the `x-liquid-cap-usd` header in step 4. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the exchange's error codes and causes. ## Next steps - [Z Code](https://inference.ai.exchange/docs/connect-zcode.md): a desktop agent with the same kind of provider. - [Market data](https://inference.ai.exchange/docs/market.md): models and live offers. - [API keys](https://inference.ai.exchange/docs/keys.md): scopes and spending limits. # Open WebUI Open WebUI is a self-hosted chat front end. Add the exchange as an OpenAI API connection. Each model on the book then shows in the model picker. The exchange auctions each request, sets a cap before the first token and charges for the measured use. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - An Open WebUI server that you can configure. ## 1. Add a connection Open **Settings → Connections → OpenAI API**. Add a connection and enter: ``` https://router.inference.ai.exchange/v1 ``` The URL ends in `/v1`. ## 2. Paste the key ``` sk-liquid-... ``` Save the connection. Open WebUI reads `GET /v1/models` from the exchange and lists every model name in the picker. ## Notes - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` response header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - `liquid.auto` has a cap of its own. For other purchasing rules, use a preset. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | ------------------------------------------------------------------------------ | | `401` | The key is wrong, revoked or expired. Paste it again. | | The model picker is empty. | Check the base URL and the key. Open WebUI reads the list with the same key. | | `404` | Keep `/v1` at the end of the base URL. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the error codes. ## Next steps - [Cherry Studio](https://inference.ai.exchange/docs/connect-cherry-studio.md): a desktop chat client with the same provider type. - [Market data](https://inference.ai.exchange/docs/market.md): models and live offers. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): models, strategy and limits, and presets. # Cherry Studio Cherry Studio is a desktop chat client. Add the exchange as an OpenAI-compatible provider, set the API host and paste your key. Cherry Studio then sends its requests to the exchange. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - Cherry Studio installed. ## 1. Add a provider Open **Settings → Model Providers → Add**. Choose the OpenAI-compatible provider type and give the provider a name. ## 2. Set the API host ``` https://router.inference.ai.exchange/v1 ``` The URL ends in `/v1`. ## 3. Paste the key ``` sk-liquid-... ``` ## 4. Add the model Add a model name from the book. To get the current names, use `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). ``` liquid.auto ``` ## Notes - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` response header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - Use a preset for other purchasing rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | ------------------------------------------------------------------------------ | | `401` | The key is wrong, revoked or expired. Paste it again. | | `404` | Keep `/v1` at the end of the API host. | | `model_not_found` | The model is not on the book. Use an exact name from `GET /v1/models`. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the error codes. ## Next steps - [Open WebUI](https://inference.ai.exchange/docs/connect-open-webui.md): a self-hosted chat front end. - [RikkaHub](https://inference.ai.exchange/docs/connect-rikkahub.md): an Android chat app with a custom provider. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): models, strategy and limits, and presets. # n8n n8n is a workflow automation tool. Its workflows can send text requests and buy images and videos from the exchange. Use the HTTP Request node for unattended or retried workflows. Use the OpenAI node for a quick start. | | HTTP Request node | OpenAI node | | ------------------------------- | --------------------------------------- | --------------------------------------------------------- | | Credential | Bearer Auth | OpenAI, with the base URL of the exchange | | Text | `POST /v1/chat/completions` | **Message a Model**, which sends `/v1/responses` | | Images | `POST /v1/images/generations` | **Generate an Image** | | Video | `POST /v1/videos`, then **Wait** | **Generate a Video** | | Job ID and charge | In the output | Text: with **Simplify Output** off. Images and video: no. | | A retry or a new run of a brief | Buys nothing, with an `Idempotency-Key` | Buys again | | Image and video size | Any size that an offer declared | Image: the seller's default. Video: one of four sizes. | ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - n8n Cloud, or n8n on your own server. To install it on your server, use this command: ```bash npm install -g n8n ``` ## Quickstart Paste each workflow on the canvas of a new workflow. Set the credential as [HTTP Request node](#http-request-node) or [OpenAI node](#openai-node) tells you, then run it. ### Text HTTP Request node. Set `prompt` and `model` in **Brief**. **Reply** holds the answer, `job_id` and `charge_usd`. ```json file=liquid-n8n-http-text.json { "name": "Liquid Inference: send a text request", "nodes": [ { "parameters": { "content": "## Send one text request to Liquid Inference\n1. Open **Ask**. Create a **Bearer Auth** credential named `Liquid`, and paste your key into **Bearer Token**.\n2. Set a spend limit on the key in the console.\n3. Change the prompt in **Brief**, then run the workflow.\n\n**Reply** holds the answer, with the job id and the charge.", "height": 260, "width": 520 }, "name": "Read me", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -40, -320 ] }, { "parameters": {}, "name": "Start", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ 0, 0 ] }, { "parameters": { "assignments": { "assignments": [ { "name": "prompt", "value": "Write one sentence about a red bicycle.", "type": "string" }, { "name": "model", "value": "liquid.auto", "type": "string" } ] }, "options": {} }, "name": "Brief", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 220, 0 ] }, { "parameters": { "method": "POST", "url": "https://router.inference.ai.exchange/v1/chat/completions", "authentication": "genericCredentialType", "genericAuthType": "httpBearerAuth", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "x-title", "value": "Liquid n8n workflow" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ model: $json.model, messages: [{ role: 'user', content: $json.prompt }] }) }}", "options": { "response": { "response": { "fullResponse": true, "neverError": true } }, "timeout": 300000 } }, "name": "Ask", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 440, 0 ], "credentials": { "httpBearerAuth": { "name": "Liquid" } } }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "typeValidation": "loose", "version": 2 }, "conditions": [ { "leftValue": "={{ $json.statusCode }}", "operator": { "type": "number", "operation": "equals" }, "rightValue": 200 } ], "combinator": "and" }, "looseTypeValidation": true, "options": {} }, "name": "Answered?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 660, 0 ] }, { "parameters": { "errorMessage": "=The exchange refused the request: {{ $json.body.error.code }}: {{ $json.body.error.message }}" }, "name": "Stop: refused", "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ 880, 140 ] }, { "parameters": { "assignments": { "assignments": [ { "name": "reply", "value": "={{ $json.body.choices[0].message.content }}", "type": "string" }, { "name": "job_id", "value": "={{ $json.body.exchange.job_id }}", "type": "string" }, { "name": "charge_usd", "value": "={{ $json.body.exchange.charge.total_usd }}", "type": "string" } ] }, "options": {} }, "name": "Reply", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 880, -40 ] } ], "connections": { "Start": { "main": [ [ { "node": "Brief", "type": "main", "index": 0 } ] ] }, "Brief": { "main": [ [ { "node": "Ask", "type": "main", "index": 0 } ] ] }, "Ask": { "main": [ [ { "node": "Answered?", "type": "main", "index": 0 } ] ] }, "Answered?": { "main": [ [ { "node": "Reply", "type": "main", "index": 0 } ], [ { "node": "Stop: refused", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1" } } ``` OpenAI node: ```json file=liquid-n8n-openai-text.json { "name": "Liquid Inference: send a text request with the OpenAI node", "nodes": [ { "parameters": { "content": "## Send one text request to Liquid Inference\n1. Open **Message a model**. Select the **OpenAI** credential named `Liquid`, with the base URL of the exchange.\n2. Set a spend limit on the key in the console.\n3. Change the message, then run the workflow.\n\nTo see the job id and the charge, turn off **Simplify Output**.", "height": 260, "width": 520 }, "name": "Read me", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -40, -320 ] }, { "parameters": {}, "name": "Start", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ 0, 0 ] }, { "parameters": { "resource": "text", "operation": "response", "modelId": { "__rl": true, "mode": "id", "value": "liquid.auto" }, "responses": { "values": [ { "content": "Write one sentence about a red bicycle." } ] }, "options": {} }, "name": "Message a model", "type": "@n8n/n8n-nodes-langchain.openAi", "typeVersion": 2.2, "position": [ 220, 0 ], "credentials": { "openAiApi": { "name": "Liquid" } } } ], "connections": { "Start": { "main": [ [ { "node": "Message a model", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1" } } ``` ### Images HTTP Request node. Set `prompt`, `model` and `size` in **Brief**. If `size` is empty, the seller uses its default. **Receipt** holds the image as the binary `data`, with `job_id` and `charge_usd`. ```json file=liquid-n8n-http-image.json { "name": "Liquid Inference: buy an image", "nodes": [ { "parameters": { "content": "## Buy one image on Liquid Inference\n1. Open **Buy image**. Create a **Bearer Auth** credential named `Liquid`, and paste your key into **Bearer Token**.\n2. Set a spend limit on the key in the console.\n3. Change the prompt in **Brief**, then run the workflow.\n\nThe image is the binary `data` of **Receipt**, with the job id and the charge.", "height": 300, "width": 520 }, "name": "Read me", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -40, -360 ] }, { "parameters": { "content": "## A retry buys nothing\n**Key** makes an `Idempotency-Key` from the row id and the brief. A retry or a new run of the same brief gets the first job back, not a second image.\n\nChange `row_id` to buy a new image of the same brief.\n\nA repeat or a refusal stops the workflow with a message. To alert a person, set an error workflow in the workflow settings.", "height": 300, "width": 520 }, "name": "Retries", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 560, -360 ] }, { "parameters": {}, "name": "Start", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ 0, 0 ] }, { "parameters": { "assignments": { "assignments": [ { "name": "row_id", "value": "brief-1", "type": "string" }, { "name": "prompt", "value": "A red bicycle leaning on a brick wall", "type": "string" }, { "name": "model", "value": "liquid.auto", "type": "string" }, { "name": "size", "value": "", "type": "string" } ] }, "options": {} }, "name": "Brief", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 220, 0 ] }, { "parameters": { "action": "hash", "type": "SHA256", "value": "={{ [$json.row_id, $json.prompt, $json.model, $json.size].join('|') }}", "dataPropertyName": "key" }, "name": "Key", "type": "n8n-nodes-base.crypto", "typeVersion": 1, "position": [ 440, 0 ] }, { "parameters": { "method": "POST", "url": "https://router.inference.ai.exchange/v1/images/generations", "authentication": "genericCredentialType", "genericAuthType": "httpBearerAuth", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Idempotency-Key", "value": "={{ $json.key }}" }, { "name": "x-liquid-cap-usd", "value": "0.50" }, { "name": "x-title", "value": "Liquid n8n workflow" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify(Object.assign({ model: $json.model, prompt: $json.prompt, n: 1 }, $json.size ? { size: $json.size } : {})) }}", "options": { "response": { "response": { "fullResponse": true, "neverError": true } }, "timeout": 300000 } }, "name": "Buy image", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 660, 0 ], "credentials": { "httpBearerAuth": { "name": "Liquid" } }, "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 5000 }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "typeValidation": "loose", "version": 2 }, "conditions": [ { "leftValue": "={{ $json.statusCode }}", "operator": { "type": "number", "operation": "equals" }, "rightValue": 200 } ], "combinator": "and" }, "looseTypeValidation": true, "options": {} }, "name": "Bought?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 880, 0 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "typeValidation": "loose", "version": 2 }, "conditions": [ { "leftValue": "={{ $json.body.object }}", "operator": { "type": "string", "operation": "equals" }, "rightValue": "chat.completion.reference" } ], "combinator": "and" }, "looseTypeValidation": true, "options": {} }, "name": "Repeat?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 1100, -120 ] }, { "parameters": { "errorMessage": "=This brief was bought before, as job {{ $json.body.exchange.job_id }}. The exchange does not keep images, so this run has no file. Change row_id in Brief to buy a new image." }, "name": "Stop: already bought", "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ 1320, -240 ] }, { "parameters": { "errorMessage": "=The exchange refused the request: {{ $json.body.error.code }}: {{ $json.body.error.message }}" }, "name": "Stop: refused", "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ 1100, 140 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "typeValidation": "loose", "version": 2 }, "conditions": [ { "leftValue": "={{ $json.body.data[0].b64_json }}", "operator": { "type": "string", "operation": "exists", "singleValue": true } } ], "combinator": "and" }, "looseTypeValidation": true, "options": {} }, "name": "Bytes?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 1320, -40 ] }, { "parameters": { "operation": "toBinary", "sourceProperty": "body.data[0].b64_json", "options": { "mimeType": "={{ $json.body.data[0].mime_type }}" } }, "name": "To file", "type": "n8n-nodes-base.convertToFile", "typeVersion": 1.1, "position": [ 1540, -120 ] }, { "parameters": { "url": "={{ $json.body.data[0].url }}", "options": { "response": { "response": { "responseFormat": "file" } } } }, "name": "Download", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1540, 40 ] }, { "parameters": { "assignments": { "assignments": [ { "name": "job_id", "value": "={{ $('Buy image').item.json.body.exchange.job_id }}", "type": "string" }, { "name": "charge_usd", "value": "={{ $('Buy image').item.json.body.exchange.charge.total_usd }}", "type": "string" } ] }, "includeOtherFields": true, "include": "except", "excludeFields": "body, headers, statusCode, statusMessage", "options": { "stripBinary": false } }, "name": "Receipt", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 1760, -40 ] } ], "connections": { "Start": { "main": [ [ { "node": "Brief", "type": "main", "index": 0 } ] ] }, "Brief": { "main": [ [ { "node": "Key", "type": "main", "index": 0 } ] ] }, "Key": { "main": [ [ { "node": "Buy image", "type": "main", "index": 0 } ] ] }, "Buy image": { "main": [ [ { "node": "Bought?", "type": "main", "index": 0 } ] ] }, "Bought?": { "main": [ [ { "node": "Repeat?", "type": "main", "index": 0 } ], [ { "node": "Stop: refused", "type": "main", "index": 0 } ] ] }, "Repeat?": { "main": [ [ { "node": "Stop: already bought", "type": "main", "index": 0 } ], [ { "node": "Bytes?", "type": "main", "index": 0 } ] ] }, "Bytes?": { "main": [ [ { "node": "To file", "type": "main", "index": 0 } ], [ { "node": "Download", "type": "main", "index": 0 } ] ] }, "To file": { "main": [ [ { "node": "Receipt", "type": "main", "index": 0 } ] ] }, "Download": { "main": [ [ { "node": "Receipt", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1" } } ``` OpenAI node: ```json file=liquid-n8n-openai-image.json { "name": "Liquid Inference: buy an image with the OpenAI node", "nodes": [ { "parameters": { "content": "## Buy one image on Liquid Inference\n1. Open **Generate image**. Select the **OpenAI** credential named `Liquid`, with the base URL of the exchange.\n2. Set a spend limit on the key in the console.\n3. Change the prompt in **Generate image**, then run the workflow.\n\nThe image is the binary `data` of **Generate image**. Each run buys a new image. Do not turn on **Retry On Fail**: a retry can buy a second image.", "height": 300, "width": 520 }, "name": "Read me", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -40, -360 ] }, { "parameters": {}, "name": "Start", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ 0, 0 ] }, { "parameters": { "resource": "image", "operation": "generate", "modelId": { "__rl": true, "mode": "id", "value": "liquid.auto" }, "prompt": "A red bicycle leaning on a brick wall", "options": {} }, "name": "Generate image", "type": "@n8n/n8n-nodes-langchain.openAi", "typeVersion": 2.2, "position": [ 220, 0 ], "credentials": { "openAiApi": { "name": "Liquid" } } } ], "connections": { "Start": { "main": [ [ { "node": "Generate image", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1" } } ``` ### Video HTTP Request node. Set `prompt`, `model`, `seconds` and `size` in **Brief**, and the credential in **Order video**, **Status** and **Download**. See [Wait for a video](#4-wait-for-a-video). ```json file=liquid-n8n-http-video.json { "name": "Liquid Inference: buy a video", "nodes": [ { "parameters": { "content": "## Buy one video on Liquid Inference\n1. Open **Order video**. Create a **Bearer Auth** credential named `Liquid`, and paste your key into **Bearer Token**. Select the same credential in **Status** and **Download**.\n2. Set a spend limit on the key in the console.\n3. Change the prompt in **Brief**, then run the workflow.\n\nThe video is the binary `data` of **Receipt**, with the job id and the charge.", "height": 320, "width": 520 }, "name": "Read me", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -40, -380 ] }, { "parameters": { "content": "## Wait for the render\nA render takes from less than a minute to several minutes. If n8n has a public `https` address, as on n8n Cloud, **Order video** sends `$execution.resumeUrl` as `callback_url`. The exchange calls it when the job ends, and **Wait** resumes at once. Otherwise **Wait** resumes after 20 seconds, and **Status** reads the job again.\n\n**Key** makes an `Idempotency-Key` from the row id and the brief, so a retry buys nothing. Change `row_id` to buy a new video of the same brief.", "height": 320, "width": 520 }, "name": "Waiting", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 560, -380 ] }, { "parameters": {}, "name": "Start", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ 0, 0 ] }, { "parameters": { "assignments": { "assignments": [ { "name": "row_id", "value": "brief-1", "type": "string" }, { "name": "prompt", "value": "A red paper boat on a stream", "type": "string" }, { "name": "model", "value": "seedance-1-5-pro", "type": "string" }, { "name": "seconds", "value": "5", "type": "string" }, { "name": "size", "value": "", "type": "string" } ] }, "options": {} }, "name": "Brief", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 220, 0 ] }, { "parameters": { "action": "hash", "type": "SHA256", "value": "={{ [$json.row_id, $json.prompt, $json.model, $json.seconds, $json.size].join('|') }}", "dataPropertyName": "key" }, "name": "Key", "type": "n8n-nodes-base.crypto", "typeVersion": 1, "position": [ 440, 0 ] }, { "parameters": { "method": "POST", "url": "https://router.inference.ai.exchange/v1/videos", "authentication": "genericCredentialType", "genericAuthType": "httpBearerAuth", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Idempotency-Key", "value": "={{ $json.key }}" }, { "name": "x-liquid-cap-usd", "value": "1.50" }, { "name": "x-title", "value": "Liquid n8n workflow" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify(Object.assign({ model: $json.model, prompt: $json.prompt }, $json.seconds ? { seconds: $json.seconds } : {}, $json.size ? { size: $json.size } : {}, $execution.resumeUrl.startsWith('https://') ? { callback_url: $execution.resumeUrl } : {})) }}", "options": { "response": { "response": { "fullResponse": true, "neverError": true } }, "timeout": 300000 } }, "name": "Order video", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 660, 0 ], "credentials": { "httpBearerAuth": { "name": "Liquid" } }, "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 5000 }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "typeValidation": "loose", "version": 2 }, "conditions": [ { "leftValue": "={{ $json.statusCode }}", "operator": { "type": "number", "operation": "equals" }, "rightValue": 200 } ], "combinator": "and" }, "looseTypeValidation": true, "options": {} }, "name": "Placed?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 880, 0 ] }, { "parameters": { "errorMessage": "=The exchange refused the request: {{ $json.body.error.code }}: {{ $json.body.error.message }}" }, "name": "Stop: refused", "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ 1100, 160 ] }, { "parameters": { "assignments": { "assignments": [ { "name": "video_id", "value": "={{ $json.body.id }}", "type": "string" }, { "name": "wait_seconds", "value": "={{ $json.body.callback ? 600 : 60 }}", "type": "number" } ] }, "options": {} }, "name": "Job", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 1100, -40 ] }, { "parameters": { "url": "=https://router.inference.ai.exchange/v1/videos/{{ $('Job').first().json.video_id }}", "authentication": "genericCredentialType", "genericAuthType": "httpBearerAuth", "options": {} }, "name": "Status", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1320, -40 ], "credentials": { "httpBearerAuth": { "name": "Liquid" } }, "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 5000 }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "typeValidation": "loose", "version": 2 }, "conditions": [ { "leftValue": "={{ $json.status }}", "operator": { "type": "string", "operation": "notEquals" }, "rightValue": "in_progress" } ], "combinator": "and" }, "looseTypeValidation": true, "options": {} }, "name": "Ended?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 1540, -40 ] }, { "parameters": { "resume": "webhook", "httpMethod": "POST", "limitWaitTime": true, "limitType": "afterTimeInterval", "resumeAmount": "={{ $('Job').first().json.wait_seconds }}", "resumeUnit": "seconds", "options": {} }, "name": "Wait", "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ 1540, 180 ], "webhookId": "liquid-video-wait" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "typeValidation": "loose", "version": 2 }, "conditions": [ { "leftValue": "={{ $json.status }}", "operator": { "type": "string", "operation": "equals" }, "rightValue": "completed" } ], "combinator": "and" }, "looseTypeValidation": true, "options": {} }, "name": "Completed?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 1760, -120 ] }, { "parameters": { "errorMessage": "=The render of {{ $json.id }} failed: {{ $json.error.code }}: {{ $json.error.message }}. A failed render costs nothing." }, "name": "Stop: render failed", "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ 1980, 40 ] }, { "parameters": { "url": "=https://router.inference.ai.exchange/v1/videos/{{ $('Job').first().json.video_id }}/content", "authentication": "genericCredentialType", "genericAuthType": "httpBearerAuth", "options": { "response": { "response": { "responseFormat": "file" } }, "timeout": 300000 } }, "name": "Download", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1980, -200 ], "credentials": { "httpBearerAuth": { "name": "Liquid" } } }, { "parameters": { "assignments": { "assignments": [ { "name": "job_id", "value": "={{ $('Status').last().json.id }}", "type": "string" }, { "name": "seconds", "value": "={{ $('Status').last().json.seconds }}", "type": "string" }, { "name": "charge_usd", "value": "={{ $('Status').last().json.exchange.charge.total_usd }}", "type": "string" }, { "name": "repeat", "value": "={{ $('Order video').first().json.body.object === 'chat.completion.reference' }}", "type": "boolean" } ] }, "options": { "stripBinary": false }, "includeOtherFields": true, "include": "selected", "includeFields": "model" }, "name": "Receipt", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 2200, -200 ] } ], "connections": { "Start": { "main": [ [ { "node": "Brief", "type": "main", "index": 0 } ] ] }, "Brief": { "main": [ [ { "node": "Key", "type": "main", "index": 0 } ] ] }, "Key": { "main": [ [ { "node": "Order video", "type": "main", "index": 0 } ] ] }, "Order video": { "main": [ [ { "node": "Placed?", "type": "main", "index": 0 } ] ] }, "Placed?": { "main": [ [ { "node": "Job", "type": "main", "index": 0 } ], [ { "node": "Stop: refused", "type": "main", "index": 0 } ] ] }, "Job": { "main": [ [ { "node": "Status", "type": "main", "index": 0 } ] ] }, "Status": { "main": [ [ { "node": "Ended?", "type": "main", "index": 0 } ] ] }, "Ended?": { "main": [ [ { "node": "Completed?", "type": "main", "index": 0 } ], [ { "node": "Wait", "type": "main", "index": 0 } ] ] }, "Wait": { "main": [ [ { "node": "Status", "type": "main", "index": 0 } ] ] }, "Completed?": { "main": [ [ { "node": "Download", "type": "main", "index": 0 } ], [ { "node": "Stop: render failed", "type": "main", "index": 0 } ] ] }, "Download": { "main": [ [ { "node": "Receipt", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1" } } ``` OpenAI node (see [Generate a video](#4-generate-a-video) for its limits): ```json file=liquid-n8n-openai-video.json { "name": "Liquid Inference: buy a video with the OpenAI node", "nodes": [ { "parameters": { "content": "## Buy one video on Liquid Inference\n1. Open **Generate video**. Select the **OpenAI** credential named `Liquid`, with the base URL of the exchange.\n2. Set a spend limit on the key in the console.\n3. Change the prompt in **Generate video**, then run the workflow.\n\nThe video is the binary `data` of **Generate video**. Each run buys a new video. Do not turn on **Retry On Fail**: a retry can buy a second video.", "height": 300, "width": 520 }, "name": "Read me", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -40, -360 ] }, { "parameters": {}, "name": "Start", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ 0, 0 ] }, { "parameters": { "resource": "video", "operation": "generate", "modelId": { "__rl": true, "mode": "id", "value": "p-video" }, "prompt": "A red paper boat on a stream", "seconds": 5, "size": "1280x720", "options": { "waitTime": 1200 } }, "name": "Generate video", "type": "@n8n/n8n-nodes-langchain.openAi", "typeVersion": 2.2, "position": [ 220, 0 ], "credentials": { "openAiApi": { "name": "Liquid" } } } ], "connections": { "Start": { "main": [ [ { "node": "Generate video", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1" } } ``` ## HTTP Request node ### 1. Create a Bearer Auth credential Set **Authentication** to **Generic Credential Type**, and **Generic Auth Type** to **Bearer Auth**. Create a credential named `Liquid`, and paste the key into **Bearer Token**. Do not type `Bearer`, because n8n adds it. Do not put the key in a header of the node, because n8n writes node headers into each export. ### 2. Set the request Set **Method** to `POST`. Set **URL** to one of these addresses. Text: ``` https://router.inference.ai.exchange/v1/chat/completions ``` Images: ``` https://router.inference.ai.exchange/v1/images/generations ``` Video: ``` https://router.inference.ai.exchange/v1/videos ``` | Field | Value | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | **Send Body** | JSON. Text: `model`, `messages`. Images: `model`, `prompt`, `n` as `1`, optional `size`. Video: `model`, `prompt`, optional `seconds` and `size`. | | **Send Headers** | `x-liquid-cap-usd`: the most one request can cost, such as `0.50` (optional). `Idempotency-Key`: the same value for one request. | | **Options › Response** | Turn on **Include Response Headers and Status** and **Never Error**. | | **Options › Timeout** | `300000`, because an image can take minutes. | | **Settings › Retry On Fail** | On, only with an `Idempotency-Key`. | The image and video quickstarts make the key in their **Key** node, as a SHA-256 of `row_id` and the brief. ### 3. Read the answer | Status and body | Meaning | | ----------------------------------------------------- | -------------------------------------------------------- | | `200`, with `body.choices[0].message.content` | The text answer. | | `200`, with `body.data[0].b64_json` | The image as Base64. Convert it to a file. | | `200`, with `body.data[0].url` | A link to the image. Download it without the credential. | | `200`, with `body.object` `chat.completion.reference` | A repeat of a used key. It has the job and no answer. | | Not `200` | A refusal. `body.error.code` gives the cause. | `body.exchange.job_id` is the job, and `body.exchange.charge.total_usd` is the charge. ### 4. Wait for a video A video request returns when a provider accepts the job. The video quickstart then does these steps: 1. **Order video** sends `POST /v1/videos`. `body.id` is the video and its job. 2. **Status** sends `GET /v1/videos/{id}`. While `status` is `in_progress`, the workflow goes to **Wait** and then to **Status** again. 3. If `status` is `failed`, the workflow stops at **Stop: render failed**. A failed render costs nothing. 4. If `status` is `completed`, **Download** gets `GET /v1/videos/{id}/content`. `body.exchange` of **Status** holds the charge. **Wait** resumes when the exchange calls the workflow, or when its time limit ends: - If the address of n8n starts with `https://`, as on n8n Cloud, **Order video** sends `$execution.resumeUrl` as `callback_url`. The time limit is 10 minutes. - If not, the exchange does not call the workflow. The time limit is one minute. The exchange refuses a `callback_url` that is not a public `https` address, such as `http://localhost:5678`, and no job starts. On your own server, set `WEBHOOK_URL` to the public `https` address of n8n. The callback only resumes the workflow, so you do not need the `signing_secret`. See [Video generation](https://inference.ai.exchange/docs/video-generation.md). ### Retries and repeats The exchange keeps a used `Idempotency-Key` for 30 days. A retry or a new run with the same key buys nothing. The text quickstart sends no key, so each run is a new request. - **Images.** A finished first request stops the quickstart at **Stop: already bought**, because the exchange keeps no images. An unfinished one returns `409` `idempotency_no_result`. Change `row_id` to buy a new image. - **Video.** The exchange returns `chat.completion.reference` with the video as `body.id`, and the quickstart downloads it again. **Receipt** shows `repeat` as `true`. - **Video on n8n Cloud.** The callback address differs for each run, so a repeat gets `409` `idempotency_conflict`. Change `row_id` to buy a new video. ## OpenAI node ### 1. Create an OpenAI credential Click **+** in the left rail, then **Credential**. Select **OpenAI**, not **Azure OpenAI**, and name it `Liquid`. Set **API Key** and **Base URL**. Leave **Organization ID** empty and **Add Custom Header** off. ``` sk-liquid-... ``` ``` https://router.inference.ai.exchange/v1 ``` Keep `/v1` at the end of the base URL. Each OpenAI node that uses the credential then goes through the exchange. Click **Save**. n8n tests it with `GET /v1/models`. ### 2. Send a text request Add an **OpenAI** node, select **Message a Model** and the credential. For **Model**, select **By ID** and enter a name from `GET /v1/models`. Enter a message. ``` liquid.auto ``` To see the job ID and the charge, turn off **Simplify Output**. The output then has the `exchange` block. ### 3. Generate an image Add an **OpenAI** node, select **Generate an Image** and the credential. For **Model**, select **By ID** and enter a name from `GET /v1/models?kind=image`. Enter a prompt. The node holds the image as the binary `data`. - It has no size option for a model by ID, so the seller uses its default. - For a name that does not start with `gpt-image`, it asks for `b64_json`, so only offers that return Base64 can take the job. For a `gpt-image` name, an offer that returns a link makes the node fail. ### 4. Generate a video Add an **OpenAI** node, select **Generate a Video** and the credential. For **Model**, select **By ID** and enter a name from `GET /v1/models?kind=video`. Enter a prompt. The node sends `POST /v1/videos`, reads the status every 10 seconds, and downloads the file as the binary `data`. - It always sends **Size**, and gives only `720x1280`, `1280x720`, `1024x1792` and `1792x1024`. Only offers that declared the size can take the job. Read `sizes` in `GET /v1/exchange/models?kind=video`. - It always sends **Seconds**. Use a length that an offer declared. - It stops after **Options › Wait Timeout**, 300 seconds by default. The job continues and the exchange charges for it, but the node has no file. The quickstart sets 1200 seconds. The OpenAI node gives no job ID or charge for images and video (see the **Receipts** page). It sends no `Idempotency-Key`. Do not turn on **Retry On Fail**, because a retry can buy a second answer, image or video. ## Notes The cap limits each request, not a workflow. To limit total spend, set a spending limit on the key ([API keys](https://inference.ai.exchange/docs/keys.md)). ## Troubleshooting | Symptom | Cause and fix | | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `401`, or `404` when you save the credential | Paste the key again. Keep `/v1` at the end of the base URL. | | `404 model_not_found` | No seller lists the model now. Use an exact name from `GET /v1/models`. | | `402 insufficient_balance` | Add credit to the workspace or check the spend limit of the key. | | `400 unsupported_parameter` | No offer for the model takes the value in `param`, usually `size`. Leave `size` empty. | | `503` with `cap_exceeds_buyer_cap` | Each offer costs more than the cap. Increase `x-liquid-cap-usd`. | | **Stop: refused** with `idempotency_no_result` | The first request with this key has not finished, or the exchange refused it. Change `row_id`, then rerun. | | **Stop: refused** with `callback_url` | The `https` address of n8n is not public. Set `WEBHOOK_URL`. | | **Stop: render failed** | The render failed and costs nothing. Rerun with a new `row_id`, or change `model`. | | **Generate a Video** stops with `Timeout` | The render took longer than **Wait Timeout**. Increase it. The exchange charged for the video. | See [Errors](https://inference.ai.exchange/docs/errors.md). # Dify Dify is a platform for LLM apps and workflows. Its model providers are plugins. Install the OpenAI-API-compatible plugin, add the key and the endpoint, and Dify sends its requests to the exchange. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A Dify workspace where you can install plugins. ## 1. Install the plugin ``` OpenAI-API-compatible ``` ## 2. Add the key and the endpoint Choose **Add Model** and set the type to **LLM**. Dify adds `/chat/completions` to the endpoint URL. ``` sk-liquid-... ``` ``` https://router.inference.ai.exchange/v1 ``` The URL ends in `/v1`. ## 3. Set the model Enter the model by name. To get the current names, use `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). ``` liquid.auto ``` Set the context size and the features of the model. Turn on function calling for an agent that uses tools. ## Notes - Dify sends `X-Title: Dify` with each request. The receipts name Dify as the client. - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` response header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A workflow can make many requests. Set a spending limit on the key for the workflow. - Use a preset for other purchasing rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | ------------------------------------------------------------------------------ | | `401` | The key is wrong, revoked or expired. Paste it again. | | `404` | Keep `/v1` at the end of the endpoint URL. | | `model_not_found` | The model is not on the book. Use an exact name from `GET /v1/models`. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the error codes. ## Next steps - [Dify plugin](https://inference.ai.exchange/docs/plugin-dify.md): the Liquid Inference plugin, which needs only the key. - [n8n](https://inference.ai.exchange/docs/connect-n8n.md): the same setup for workflow automation. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): models, strategy and limits, and presets. - [Errors](https://inference.ai.exchange/docs/errors.md): the error format and what each code means. # LobeHub LobeHub is a workspace for teams of AI agents. It has no Liquid Inference provider yet. Use its OpenAI provider with the exchange as the proxy URL. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A LobeHub workspace where you can change **App Settings**. ## 1. Enable the OpenAI provider Open **App Settings → AI Providers** and choose **OpenAI**. ``` OpenAI ``` ## 2. Add the key and the proxy URL ``` sk-liquid-... ``` ``` https://router.inference.ai.exchange/v1 ``` The URL ends in `/v1`. Select **Check** to test the connection. ## 3. Add the model Add the model by name. To get the current names, use `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). ``` liquid.auto ``` Choose the model in the settings of an agent. ### Self-hosted LobeHub A server that you host can set the same values in its environment: - `OPENAI_PROXY_URL` is the endpoint. - `OPENAI_API_KEY` is the key. - `OPENAI_MODEL_LIST` sets the models that the picker shows. ## Notes - LobeHub sends the OpenAI SDK's user agent. The receipts name the client `openai-sdk`, not LobeHub. - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` response header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - Scheduled agents can make many requests while you are away. Set a spending limit on the key for them. - Use a preset for other purchasing rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | ------------------------------------------------------------------------------ | | `401` | The key is wrong, revoked or expired. Paste it again. | | `404` or an empty answer | Keep `/v1` at the end of the proxy URL. | | `model_not_found` | The model is not on the book. Use an exact name from `GET /v1/models`. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the error codes. ## Next steps - [Open WebUI](https://inference.ai.exchange/docs/connect-open-webui.md): a self-hosted chat front end with the same setup. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): models, strategy and limits, and presets. - [Errors](https://inference.ai.exchange/docs/errors.md): the error format and what each code means. # ComfyUI Install the Liquid nodes in ComfyUI, save your key one time, and buy images or video inside a workflow. Each paid node has a price cap on each run. | Node | What it does | What you pay for | | --------------------------- | ------------------------------------------------------- | -------------------------------- | | **Liquid Image (paid API)** | Sends the prompt to `/v1/images/generations`. | The images returned. | | **Liquid Video (paid API)** | Sends the prompt to `/v1/videos` and waits for the job. | Each second of the video. | | **Liquid Video Job** | Gets the video of a job again. | Nothing. It makes no new charge. | ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - ComfyUI 0.3.45 or newer, with ComfyUI-Manager. The desktop app includes the Manager. ## 1. Install the node Open **Manager**, then **Custom Nodes Manager**. Search for this name and install it: ``` Liquid Inference ``` Restart ComfyUI. With `comfy-cli`, use this command: ```bash comfy node install liquid-inference ``` The source is [architect-xyz/comfyui-liquid-inference](https://github.com/architect-xyz/comfyui-liquid-inference). ## 2. Set the key Open **Settings**, then **Liquid Inference**. Paste the key into **API key** and click **Save**: ``` sk-liquid-... ``` The nodes have no key input, because ComfyUI writes input values into the metadata of each saved image. The server keeps the key in a file that only your system user can read. On a server without a browser, set `LIQUID_API_KEY` before you start ComfyUI. The variable has priority over the saved key. ## 3. Buy images 1. Double-click the canvas and search for **Liquid Image**. 2. Write a prompt, and select a model and a cap. 3. Connect `images` to **Save Image**. The node also gives the job ID and a short receipt as text. - **model** lists the image models on the book. The node refreshes the list each minute. - **size** lists the sizes that at least one offer declared. With `auto`, the node sends no `size` and each seller uses its default. An explicit size goes only to the offers that declared it. ## 4. Buy a video 1. Double-click the canvas and search for **Liquid Video**. 2. Write a prompt, and select a model and a cap. 3. Connect `video` to **Save Video**. **Templates** has examples for both nodes. - **model** lists the video models on the book. - **size** and **seconds** list the values that at least one offer declared. With `auto`, the node sends no field. For `seconds`, the exchange then uses the shortest length that an offer declares. The charge is the price of that length. - **retention** defaults to `none`, which excludes providers that can keep your content. Select `allowed` only if the provider can keep your prompt, start image and video. - `start_image` starts the video from an image. The node sends the first image of the batch as a PNG in `input_reference`. Only a model that reads image input can take it. Use an image with the shape of the size, because a seller can crop a different shape. ### How the wait works The node reads the job status every 5 seconds and waits 30 minutes at most. It then gets the file from `/v1/videos/{id}/content`. It tries a failed status read or download again, because these buy nothing. A stop in ComfyUI stops the wait, not the job. The exchange cannot cancel a video job and charges for it when it is complete. To get the video, paste the job ID into a **Liquid Video Job** node. The seller keeps the file until `expires_at` in the receipt. ## Base URL To use a different exchange, set **Base URL** in **Settings → Liquid Inference**, or set `LIQUID_BASE_URL`: ``` https://router.inference.ai.exchange/v1 ``` ## Limits and notes - The cap limits each run of a node, not a workflow or a queue. Three Liquid nodes with a cap of $0.05 can cost $0.15 for each run. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A paid node never sends a purchase again by itself. If the connection stops, the error gives the `Idempotency-Key`, so you can find the job on the **Receipts** page. - The prompt goes to the seller that wins the auction. It can be a different company for each run. ## Troubleshooting | Symptom | Cause and fix | | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `No Liquid Inference API key` | Save the key in **Settings → Liquid Inference**, or set `LIQUID_API_KEY` and restart ComfyUI. | | `401` | The key is empty, wrong or expired. Save it again. | | `402 insufficient_balance` | Add credit, or check the spend limit of the key. | | `400 unsupported_parameter` | No offer for the model takes the value in `param`, usually `size`. Set **size** to `auto`. | | `503` with `cap_exceeds_buyer_cap` | Each offer costs more than the cap. Increase **cap_usd**, or the spend limit of the key. | | `503 model_unavailable` | The model has sellers, but none can take a job now. Try again in a minute, or select a different model. | | `503 provider_failed` | The seller failed before it made an image. You were not charged. Queue again. A run 30 seconds later can go to the same seller. | | `410 video_expired`; the error says the outcome is unknown | The seller deleted the file; or the connection stopped. Look for the job on the **Receipts** page before you queue again. | | A video job failed | The seller did not make the video. You were not charged. Queue again, or select a different model. | | The node stopped waiting for a video | The job continues and is charged. Paste the job ID into a **Liquid Video Job** node. | See [Errors](https://inference.ai.exchange/docs/errors.md) for all error codes. See also [Modalities](https://inference.ai.exchange/docs/modalities.md) and [Video generation](https://inference.ai.exchange/docs/video-generation.md). # 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. # Anthropic SDK The exchange serves Anthropic Messages, so the official Python and TypeScript clients work without changes. Set the origin of the exchange and your key. Use this SDK for direct Messages API calls. To run Claude Code and its tools from your application, use the [Claude Agent SDK](https://inference.ai.exchange/docs/connect-claude-agent-sdk.md). ## Print the setup with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) prints the setup for the Anthropic 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 anthropic-sdk liquid exec -- python app.py ``` To set up the Anthropic 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 Use the origin **without** `/v1`, because this client adds `/v1/messages` itself. If the URL ends in `/v1`, the client requests `/v1/v1/messages`. **Python** ```python import os from anthropic import Anthropic client = Anthropic( base_url="https://router.inference.ai.exchange", api_key=os.environ["LIQUID_API_KEY"], ) client.messages.create( model="liquid.auto", max_tokens=1024, messages=[{"role": "user", "content": "hello"}], ) ``` **TypeScript** ```typescript import Anthropic from '@anthropic-ai/sdk' const client = new Anthropic({ baseURL: 'https://router.inference.ai.exchange', apiKey: process.env.LIQUID_API_KEY, }) const message = await client.messages.create({ model: 'liquid.auto', max_tokens: 1024, messages: [{ role: 'user', content: 'hello' }], }) ``` The client sends the key in the `x-api-key` header, which the exchange accepts. Use any model, router model or preset on the book. `GET /v1/anthropic/models` lists the names. See [Market data](https://inference.ai.exchange/docs/market.md). ## 2. Read the cap before the first token The `x-liquid-job-id` and `x-liquid-cap-usd` headers arrive before any token. `cap_usd` is the maximum charge for the job. Read the headers from the raw response: `with_raw_response` (Python) or `.asResponse()` (TypeScript). ## 3. Cache a prompt prefix `cache_control` works as it does with Anthropic. Mark the last block of the prefix to cache. The exchange changes your markers into the cache plan of the job, for the winning provider. ```python client.messages.create( model="liquid.auto", max_tokens=1024, system=[ {"type": "text", "text": "You are terse."}, { "type": "text", "text": long_reference_document, "cache_control": {"type": "ephemeral", "ttl": "1h"}, }, ], messages=[{"role": "user", "content": "what changed in section 4?"}], ) ``` | Marked block | Becomes | | ------------------------- | -------------------------------------------- | | A block of `system` | A breakpoint at the end of the system prompt | | A tool definition | A breakpoint after the last tool | | A block inside a turn | A breakpoint at the end of that turn | | Top-level `cache_control` | A breakpoint after the last message | Rules for breakpoints: - Each breakpoint caches at least the prefix that you marked. - The exchange sends four breakpoints at most. If there are more, it drops the shallowest, because a deeper one caches more of the prompt. - `"ttl": "1h"` asks for the one-hour write. All other values are five minutes. The exchange prices the two separately. - The price cap includes cache writes up to the deepest breakpoint, so a write cannot charge more than your cap. The `usage` block reports cached input separately from fresh input: ```json { "input_tokens": 412, "cache_read_input_tokens": 10000, "cache_creation_input_tokens": 0, "output_tokens": 128 } ``` `input_tokens` counts only the input that the cache did not read or write, so the sum of the three does not count cached input two times. If a request has no markers, the exchange places its own breakpoints. It does this only if both conditions are true: - The request declares tools or continues a conversation. - The estimated prompt is 512 tokens or more. To keep only your own markers, or no markers, send `exchange.cache.mode` as `buyer` or `off`: ```python client.messages.create( model="liquid.auto", max_tokens=1024, messages=[{"role": "user", "content": "hello"}], extra_body={"exchange": {"cache": {"mode": "buyer"}}}, ) ``` See [Prompt caching](https://inference.ai.exchange/docs/caching.md) for how the exchange places breakpoints, prices cached input, and keeps your cache separate from other buyers. ## Limits and details ### Output limit The Anthropic format requires `max_tokens`. - If no offer allows that many output tokens, the exchange lowers `max_tokens` to the largest limit of the offers that can take the job. A longer answer then stops with `stop_reason: "max_tokens"`. - The exchange does not lower `max_tokens` to fit your balance or cap. It refuses the request instead. See [Errors](https://inference.ai.exchange/docs/errors.md). ### Hosted tools The exchange does not run hosted tools. - To search the web, add `:web` (beta) to the model name. The exchange then removes `web_search` from the request and runs its own search. - It refuses a request with a tool such as `web_search`, `web_fetch` or `code_execution`. The error is `400 unsupported_parameter`, and `param` is `tools[N]`. - Send a tool with a name and an `input_schema`, which your client runs. The typed client tools `bash`, `text_editor`, `computer` and `memory` are accepted. ### Reasoning blocks The reasoning of a model comes back as `thinking` blocks before the answer. This is also true for models that are not Claude. Encrypted reasoning comes back as `redacted_thinking`. - Send these blocks back unchanged, as the Anthropic API requires in a tool-use loop. - A block from a model that does not sign its reasoning has an empty `signature`. The exchange does not send that block to Claude. ### Cost control - To set a cost cap or service floor for each request, send the `exchange` block in the body, or the same rules as `x-liquid-*` headers. See [Purchasing rules](https://inference.ai.exchange/docs/rules.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 - [Prompt caching](https://inference.ai.exchange/docs/caching.md): breakpoints, retention and reporting. - [Modalities](https://inference.ai.exchange/docs/modalities.md): send images and documents as content blocks. - [Claude Code](https://inference.ai.exchange/docs/connect-claude-code.md): Anthropic's terminal agent, using this format. - [OpenAI SDK](https://inference.ai.exchange/docs/connect-openai-sdk.md): the same idea in the OpenAI format. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): every field of the `exchange` block. # Claude Agent SDK The Claude Agent SDK is Claude Code as a Python or TypeScript library. To send an agent to the exchange, pass the exchange origin, your key and a cost cap in the environment of each `query()` call. Tools, hooks, subagents and sessions work as they do with Anthropic. A `query()` call starts the Claude Code CLI as a subprocess with the environment that you give it. For direct Messages API calls, use the [Anthropic SDK](https://inference.ai.exchange/docs/connect-anthropic-sdk.md). ## Print the setup with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) prints the setup for the Claude Agent 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 claude-agent-sdk liquid exec -- python app.py ``` To set up the Claude Agent 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 the environment of the process that runs the agent. It then never appears in your code: ```bash export LIQUID_API_KEY=sk-liquid-... ``` ## 1. Install the SDK The package includes the Claude Code CLI. Do not install Claude Code separately. ```bash # Python 3.10 or newer pip install claude-agent-sdk # TypeScript, Node 18 or newer npm install @anthropic-ai/claude-agent-sdk ``` ## 2. Point the agent at the exchange Pass these options to every `query()` call. Both versions set the same variables. **Python** ```python import asyncio import os from claude_agent_sdk import ClaudeAgentOptions, query options = ClaudeAgentOptions( env={ "ANTHROPIC_API_KEY": "", "ANTHROPIC_AUTH_TOKEN": os.environ["LIQUID_API_KEY"], "ANTHROPIC_BASE_URL": "https://router.inference.ai.exchange", "ANTHROPIC_CUSTOM_HEADERS": "x-liquid-cap-usd: 0.50", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "cc.anthropic/liquid.auto", "ANTHROPIC_MODEL": "cc.anthropic/liquid.auto", "CLAUDE_CODE_ATTRIBUTION_HEADER": "0", "CLAUDE_CODE_AUTO_MODE_SERVER": "0", "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "16384", "CLAUDE_CODE_SUBAGENT_MODEL": "cc.anthropic/liquid.auto", }, setting_sources=[], allowed_tools=["Read", "Glob"], ) async def main(): async for message in query(prompt="List the files here.", options=options): print(message) asyncio.run(main()) ``` **TypeScript** ```typescript import { query } from '@anthropic-ai/claude-agent-sdk' const options = { env: { ...process.env, ANTHROPIC_API_KEY: '', ANTHROPIC_AUTH_TOKEN: process.env.LIQUID_API_KEY, ANTHROPIC_BASE_URL: 'https://router.inference.ai.exchange', ANTHROPIC_CUSTOM_HEADERS: 'x-liquid-cap-usd: 0.50', ANTHROPIC_DEFAULT_HAIKU_MODEL: 'cc.anthropic/liquid.auto', ANTHROPIC_MODEL: 'cc.anthropic/liquid.auto', CLAUDE_CODE_ATTRIBUTION_HEADER: '0', CLAUDE_CODE_AUTO_MODE_SERVER: '0', CLAUDE_CODE_MAX_OUTPUT_TOKENS: '16384', CLAUDE_CODE_SUBAGENT_MODEL: 'cc.anthropic/liquid.auto', }, settingSources: [], allowedTools: ['Read', 'Glob'], } for await (const message of query({ prompt: 'List the files here.', options })) { console.log(message) } ``` The [Claude Code guide](https://inference.ai.exchange/docs/connect-claude-code.md) explains each variable. These four lines matter for the SDK: | Line | Why it is set | | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ANTHROPIC_AUTH_TOKEN` | Your key, sent as a bearer token. The base URL is the origin with no `/v1`, because the CLI adds `/v1/messages` itself. | | `ANTHROPIC_API_KEY` set to an empty string | The SDK passes its own environment to the CLI. An Anthropic key already in that environment would otherwise be sent to the exchange as `x-api-key`. The exchange uses the bearer token either way, but the empty value keeps your Anthropic key private. | | `ANTHROPIC_CUSTOM_HEADERS` | Caps each request at $0.50. See [Cap each request](#cap-each-request). | | `setting_sources=[]` / `settingSources: []` | By default the CLI loads `~/.claude/settings.json` and the project's `.claude/` settings. An `env` block there replaces the values you pass, and requests can go to Anthropic or to another gateway. The empty list stops the CLI from loading those files. | Differences between the two languages: - In TypeScript, `env` replaces the subprocess environment. Keep `...process.env` first, so the CLI and the commands of the agent keep `PATH`, `HOME` and the rest. - In Python, the SDK adds `env` to the inherited environment. `setting_sources=[]` also stops the CLI from loading `CLAUDE.md` files, skills and hooks from those settings. To load the settings of one project, pass `["project"]`. Then make sure that its `.claude/settings.json` has no `env` block. ## 3. Run it Save the Python version as `agent.py` or the TypeScript version as `agent.mts`. Run it from the shell where `LIQUID_API_KEY` is set: ```bash python agent.py # or npx tsx agent.mts ``` The example allows only the `Read` and `Glob` tools. Change `allowed_tools` / `allowedTools` and the prompt for your own agent. ## 4. Check it works Each request of the agent is a job on the exchange. Open the **Overview** or **Credits** page. The receipt of each job names the client as **Claude Agent SDK**, with the SDK version, and shows its cap. `total_cost_usd` in the result message of the SDK is an estimate from token counts. It is not the charge of the exchange. Read the charge from the receipts. ## Choose a model The exchange lists its models to Claude Code with a `cc.anthropic/` prefix. A model published as `liquid.auto` is `cc.anthropic/liquid.auto` here. Add `[1m]` if the live input window of the model is one million tokens or more. The CLI asks for a different model for each type of work, with one variable for each. The example sets all three to the same model: - `ANTHROPIC_MODEL` is the main agent. - `ANTHROPIC_DEFAULT_HAIKU_MODEL` is background work such as session titles. - `CLAUDE_CODE_SUBAGENT_MODEL` is the model that subagents use. If a variable is not set, its requests ask for a `claude-*` model that the exchange does not list, and they fail. The `model` option of `query()` sets only the model of the main agent. To send background work to `liquid.auto:economy` or to a preset, see [A different model per task](https://inference.ai.exchange/docs/connect-claude-code.md) in the Claude Code guide. ## Cap each request The CLI builds the request body, so the SDK cannot send the `exchange` object. Send the purchasing rules as headers in `ANTHROPIC_CUSTOM_HEADERS`. Separate headers with a newline: ```python "ANTHROPIC_CUSTOM_HEADERS": "x-liquid-cap-usd: 0.50\nx-liquid-policy: economy", ``` - `x-liquid-cap-usd` is the maximum charge for one request, in USD. An agent sends many requests for each `query()`, and each has its own cap. - If the estimated charge of a request is more than the cap, the exchange refuses the request. It does not make the request smaller. - If the exchange cannot parse a header, it returns `400 invalid_header`. - `liquid.auto` has a cap of its own, and the header replaces it. No header can set models. Presets can set models. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md) for each header. ## Custom tools Tools that you define with `@tool` and `create_sdk_mcp_server` (Python) or `tool()` and `createSdkMcpServer` (TypeScript) run in your own process. The model sees them as usual tool definitions, so they need no exchange setting. See the [Agent SDK documentation](https://code.claude.com/docs/en/agent-sdk/overview). ## Control the cost The exchange sets a cap on each request before the first token. To limit the total spend of an agent, give it its own key with a spending limit. When the key reaches the limit, the exchange refuses more requests. See [API keys](https://inference.ai.exchange/docs/keys.md). ## Troubleshooting | Symptom | Cause and fix | | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Requests go to Anthropic or to another gateway. | An `env` block in `~/.claude/settings.json` or `.claude/settings.json` is replacing your values. Pass `setting_sources=[]` / `settingSources: []`. | | `KeyError: 'LIQUID_API_KEY'` in Python, or `401`. | The key is not in the environment of the process that runs the agent. Export it before you start the process. Otherwise confirm the key has the `spend` scope and is not revoked. | | `model_not_found` or the model is rejected. | The name is not on the book, the `cc.anthropic/` prefix is missing, or one of the three model variables is not set. Check the name against `GET /v1/models`. | | `400 invalid_header`. | A rule in `ANTHROPIC_CUSTOM_HEADERS` cannot be parsed. See [Cap each request](#cap-each-request). | | Answers stop with `stop_reason: "max_tokens"`. | The offers' output limit is below what the CLI asked for. Lower `CLAUDE_CODE_MAX_OUTPUT_TOKENS`, or choose a model whose offers allow more. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | ## Next steps - [Claude Code](https://inference.ai.exchange/docs/connect-claude-code.md): what each shared variable does. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): models, strategy and limits, and presets. - [API keys](https://inference.ai.exchange/docs/keys.md): set a spending limit on the key the agent uses. - [Prompt caching](https://inference.ai.exchange/docs/caching.md): how the exchange prices cached input. - [Anthropic SDK](https://inference.ai.exchange/docs/connect-anthropic-sdk.md): call the Messages API directly. # OpenAI Agents SDK The OpenAI Agents SDK runs an agent in your Python or TypeScript application. The SDK sends each model request to the exchange through the Responses API. This SDK controls the agent loop and runs tools. For direct API calls, use the [OpenAI SDK](https://inference.ai.exchange/docs/connect-openai-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 Agents 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-agents-sdk liquid exec -- python app.py ``` To set up the OpenAI Agents SDK yourself, follow the steps below. ## Before you start - Use a workspace with a balance and a key with the `spend` scope. See [API keys](https://inference.ai.exchange/docs/keys.md). - Export the key in the shell that starts your application: ```bash export LIQUID_API_KEY=sk-liquid-... ``` ## 1. Install the SDK ```bash # Python 3.10 or later pip install openai-agents # TypeScript, Node 22 or later npm install @openai/agents openai zod ``` See the [official SDK quickstart](https://developers.openai.com/api/docs/guides/agents/quickstart). ## 2. Create the agent **Python** ```python import asyncio import os from agents import Agent, ModelSettings, OpenAIResponsesModel, RunConfig, Runner from openai import AsyncOpenAI def create_agent() -> Agent: client = AsyncOpenAI( base_url="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], ) return Agent( name="Assistant", model=OpenAIResponsesModel(model="liquid.auto", openai_client=client), model_settings=ModelSettings( max_tokens=1024, store=False, extra_body={"exchange": {"cap_usd": 0.50}}, ), ) async def main() -> None: result = await Runner.run( create_agent(), "Say hello.", run_config=RunConfig(tracing_disabled=True), ) print(result.final_output) if __name__ == "__main__": asyncio.run(main()) ``` **TypeScript** ```typescript import { Agent, OpenAIResponsesModel, Runner } from '@openai/agents' import OpenAI from 'openai' const client = new OpenAI({ baseURL: 'https://router.inference.ai.exchange/v1', apiKey: process.env.LIQUID_API_KEY, }) const agent = new Agent({ name: 'Assistant', model: new OpenAIResponsesModel(client, 'liquid.auto'), modelSettings: { maxTokens: 1024, store: false, providerData: { extra_body: { exchange: { cap_usd: 0.50 } } }, }, }) const runner = new Runner({ tracingDisabled: true }) const result = await runner.run(agent, 'Say hello.') console.log(result.finalOutput) ``` The base URL ends in `/v1`. The explicit model adapter uses `/responses` over HTTP. Choose a model, router model, or preset from `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). The example disables tracing because an exchange key cannot send traces to OpenAI. It sets `store=False` in Python and `store: false` in TypeScript because the exchange keeps no response state. The output limit is 1,024 tokens per request. A model that needs more output requires a larger limit and possibly a larger cap. ## Purchasing rules In Python, put the `exchange` object in `ModelSettings.extra_body`. In TypeScript, put it in `modelSettings.providerData.extra_body`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md) for the fields. The example sets a $0.50 cap for each request. A run can send several requests, and each has its own cap. If no offer fits the cap, the exchange refuses the request. It does not reduce the output limit. To limit the total spend, use a separate key with a spend limit. The exchange refuses more requests after that limit. ## Local function tools Python functions with `@function_tool` and TypeScript functions with `tool()` run in your application. Add them to the agent's `tools` list. The exchange sends their definitions to the provider and returns the model's tool calls. The SDK runs each function and sends the result in the next request. The exchange does not provide OpenAI-hosted tools such as file search or code interpreter. It drops hosted web search definitions and refuses other hosted tools by name. Use local function tools for work that your application can do. ## Continue with the full history Keep response history in your application. The SDK sends the history between tool calls within a run. For a later Python turn, use `result.to_input_list()`: ```python history = result.to_input_list() history.append({"role": "user", "content": "Explain that answer."}) result = await Runner.run( agent, history, run_config=RunConfig(tracing_disabled=True) ) ``` In TypeScript, pass the history from `result.history` to the runner: ```typescript const history = [...result.history, { role: 'user' as const, content: 'Explain that answer.' }] const next = await runner.run(agent, history) ``` Do not use `previous_response_id`, `conversation`, or background mode. The exchange refuses these fields because it keeps no response state. The exchange does not support the Responses WebSocket transport. ## Chat Completions alternative To select Chat Completions explicitly in Python, replace the model adapter: ```python from agents import OpenAIChatCompletionsModel model = OpenAIChatCompletionsModel(model="liquid.auto", openai_client=client) ``` In TypeScript, use the equivalent adapter with these model settings: ```typescript import { OpenAIChatCompletionsModel } from '@openai/agents' const model = new OpenAIChatCompletionsModel(client, 'liquid.auto') const modelSettings = { maxTokens: 1024, store: false, providerData: { exchange: { cap_usd: 0.5 } }, } ``` Use that adapter as the agent's `model`. In TypeScript, also replace its `modelSettings` with the object above. The Chat Completions adapter sends `providerData` directly, without the `extra_body` wrapper of the Responses adapter. Keep the purchasing rules of Python, and disable tracing in both examples. ## Verify the connection Check the receipts on the **Credits** page. Requests show **OpenAI Agents SDK** as the client. Use the receipt for the settled charge. The SDK's token usage does not state the exchange's charge. ## Troubleshooting | Symptom | Cause and action | | ------------------------------------------ | ----------------------------------------------------------------------- | | `401` | Confirm that `LIQUID_API_KEY` is set and the key has the `spend` scope. | | `model_not_found` | Choose a name from `GET /v1/models`. | | The request exceeds the cap | Reduce the output limit or increase the cap. | | `unsupported_parameter` for response state | Send the full history without a continuation id. | | A hosted tool is refused | Replace it with a local function tool. | | The request has insufficient funds | Add funds to the workspace on the **Credits** page. | ## Next steps - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): caps, models, strategy, and service limits. - [API keys](https://inference.ai.exchange/docs/keys.md): total spend limits for each agent. - [OpenAI SDK](https://inference.ai.exchange/docs/connect-openai-sdk.md): call the endpoints directly. # Vercel AI SDK The Vercel AI SDK runs model calls and tool loops in your TypeScript application. Its OpenAI-compatible provider sends each request to the exchange through Chat Completions. ## Print the setup with the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) prints the setup for the Vercel AI 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 vercel-ai liquid exec -- node app.mjs ``` To set up the SDK yourself, follow the steps below. ## Before you start - Use Node 22 or later. The SDK does not accept an older release. - Use a workspace with a balance and a key with the `spend` scope. See [API keys](https://inference.ai.exchange/docs/keys.md). - Export the key in the shell that starts your application: ```bash export LIQUID_API_KEY=sk-liquid-... ``` ## 1. Install the SDK ```bash # Node 22 or later npm install ai @ai-sdk/openai-compatible ``` See the [official provider guide](https://ai-sdk.dev/providers/openai-compatible-providers). ## 2. Add the exchange as a provider Save this file as `liquid.ts`. ```typescript import { createOpenAICompatible } from '@ai-sdk/openai-compatible' const liquid = createOpenAICompatible({ name: 'liquid', baseURL: 'https://router.inference.ai.exchange/v1', apiKey: process.env.LIQUID_API_KEY, headers: { 'x-liquid-cap-usd': '1.00' }, }) export const model = liquid('liquid.auto') ``` The base URL ends in `/v1`, because the provider adds `/chat/completions` to it. Choose a model, router model, or preset from `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). The `x-liquid-cap-usd` header sets a $1.00 cap for each request. ## 3. Send a request ```typescript import { generateText } from 'ai' import { model } from './liquid.ts' const { text } = await generateText({ model, prompt: 'Say hello.' }) console.log(text) ``` ## Purchasing rules Put the `exchange` object in `providerOptions`, below the name of the provider. The provider copies the object into the request body. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md) for the fields. ```typescript const { text } = await generateText({ model, prompt: 'Say hello.', providerOptions: { liquid: { exchange: { cap_usd: 0.05 } } }, }) ``` The key `liquid` is the `name` that `createOpenAICompatible` received. A different key has no effect. A cap in the body replaces the cap header. To change only the header for one call, set `headers` on that call: ```typescript const { text } = await generateText({ model, prompt: 'Say hello.', headers: { 'x-liquid-cap-usd': '0.05' }, }) ``` If no offer fits the cap, the exchange refuses the request. It does not reduce the output limit. To limit the total spend, use a separate key with a spend limit. The exchange refuses more requests after that limit. ## Local tools A tool made with `tool()` runs in your application. The exchange sends its definition to the provider and returns the model's tool calls. The SDK runs each function and sends the result in the next request. ```typescript import { readFileSync } from 'node:fs' import { generateText, isStepCount, jsonSchema, tool } from 'ai' import { model } from './liquid.ts' const { text } = await generateText({ model, prompt: 'Summarize notes.txt.', tools: { read_notes: tool({ description: 'Read notes.txt from the current directory.', inputSchema: jsonSchema({ type: 'object', properties: {} }), execute: async () => readFileSync('notes.txt', 'utf8'), }), }, stopWhen: isStepCount(6), }) ``` Without `stopWhen`, the SDK stops after one request and does not send the tool result. `isStepCount(6)` permits six requests. Each request is a separate job with its own cap. See the [official tool guide](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling) for schemas made with Zod. ## Stream a response `streamText` uses the same model: ```typescript import { streamText } from 'ai' import { model } from './liquid.ts' const result = streamText({ model, prompt: 'Say hello.' }) for await (const part of result.textStream) { process.stdout.write(part) } ``` To get token counts from a stream, add `includeUsage: true` to the options of `createOpenAICompatible`. ## Read the job and its cap The exchange returns the `x-liquid-job-id` and `x-liquid-cap-usd` headers before any token. `x-liquid-cap-usd` is the maximum charge for the job. ```typescript const result = await generateText({ model, prompt: 'Say hello.' }) const headers = result.response.headers console.log(headers?.['x-liquid-job-id'], headers?.['x-liquid-cap-usd']) ``` With `streamText`, use `(await result.response).headers`. ## Verify the connection Check the receipts on the **Credits** page. Requests show **Vercel AI SDK** as the client. Use the receipt for the settled charge. The SDK's token usage does not state the exchange's charge. ## Troubleshooting | Symptom | Cause and action | | ----------------------------------- | ----------------------------------------------------------------------- | | `401` | Confirm that `LIQUID_API_KEY` is set and the key has the `spend` scope. | | `model_not_found` | Choose a name from `GET /v1/models`. | | The request exceeds the cap | Reduce `maxOutputTokens` or increase the cap. | | The purchasing rules have no effect | Use the provider `name` as the key in `providerOptions`. | | The model calls a tool and stops | Add `stopWhen`, so that the SDK sends the tool result. | | The request has insufficient funds | Add funds to the workspace on the **Credits** page. | ## Next steps - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): caps, models, strategy, and service limits. - [API keys](https://inference.ai.exchange/docs/keys.md): total spend limits for each application. - [OpenAI SDK](https://inference.ai.exchange/docs/connect-openai-sdk.md): call the endpoints directly. # LiteLLM LiteLLM is a Python library that gives one interface to many model APIs. Use its OpenAI-compatible route, with the exchange as `api_base`, to send requests to the exchange. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). ## 1. Export the key ```bash export LIQUID_API_KEY=sk-liquid-... ``` ## 2. Install LiteLLM ```bash # Python 3.10 or later pip install litellm ``` ## 3. Send a request Put `openai/` before the model name and set `api_base` to the exchange. The prefix selects the API format. The exchange auctions the model that follows it. The `X-Title` header names the client on your receipts. ```python import os import litellm response = litellm.completion( model="openai/liquid.auto", api_base="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], extra_headers={"X-Title": "LiteLLM"}, messages=[{"role": "user", "content": "hello"}], ) ``` To get the current model names, use `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). ## Notes - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` response header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - To send purchasing rules, pass an `exchange` block in `extra_body`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | ------------------------------------------------------------------------------ | | `401` | The key is wrong, revoked or expired. Export it again. | | `404` | Keep `/v1` at the end of `api_base`. | | `model_not_found` | The model is not on the book. Use an exact name from `GET /v1/models`. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the error codes. ## Next steps - [LangChain](https://inference.ai.exchange/docs/connect-langchain.md): another Python framework with an OpenAI chat model. - [OpenAI SDK](https://inference.ai.exchange/docs/connect-openai-sdk.md): streams and purchasing rules in code. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): models, strategy and limits, and presets. # LangChain LangChain is a framework for applications that use language models. Create its OpenAI chat model with the exchange as the base URL. Use the same base URL and key for embeddings. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). ## 1. Export the key ```bash export LIQUID_API_KEY=sk-liquid-... ``` ## 2. Install the package ```bash # Python 3.10 or later pip install langchain-openai ``` ## 3. Send a request Create the OpenAI chat model with the exchange as its base URL. The `X-Title` header names the client on your receipts. ```python import os from langchain_openai import ChatOpenAI llm = ChatOpenAI( base_url="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], model="liquid.auto", default_headers={"X-Title": "LangChain"}, ) ``` To get the current model names, use `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). ## Embeddings Use `OpenAIEmbeddings` with the same base URL and key to buy vectors. See [Embeddings](https://inference.ai.exchange/docs/embeddings.md). ```python import os from langchain_openai import OpenAIEmbeddings embeddings = OpenAIEmbeddings( model="text-embedding-3-small", base_url="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], check_embedding_ctx_length=False, ) vectors = embeddings.embed_documents(["a cat on a bicycle", "a dog in a car"]) ``` Set `check_embedding_ctx_length=False`. - If it is `True`, the class splits each text into tokens and sends arrays of token numbers. The exchange does not support token arrays and refuses the request. - With `False`, the class sends the text. Pass this `embeddings` object to a vector store such as `QdrantVectorStore` or `PGVector`. See [Qdrant](https://inference.ai.exchange/docs/connect-qdrant.md) and [pgvector](https://inference.ai.exchange/docs/connect-pgvector.md). ## Notes - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` response header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - To send purchasing rules, pass an `exchange` block in `extra_body`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | ------------------------------------------------------------------------------ | | `401` | The key is wrong, revoked or expired. Export it again. | | `404` | Keep `/v1` at the end of the base URL. | | `model_not_found` | The model is not on the book. Use an exact name from `GET /v1/models`. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the error codes. ## Next steps - [LiteLLM](https://inference.ai.exchange/docs/connect-litellm.md): a Python library with one interface to many APIs. - [OpenAI SDK](https://inference.ai.exchange/docs/connect-openai-sdk.md): streams and purchasing rules in code. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): models, strategy and limits, and presets. # OpenHands OpenHands runs an agent with local file and shell tools. Choose the Python SDK for your application or the CLI for a terminal session. Both paths send OpenAI-format requests to the exchange. ## Before you start - A workspace with a balance and a key with the `spend` scope. See [API keys](https://inference.ai.exchange/docs/keys.md). - A model, router model, or preset from `GET /v1/models`. See [Market data](https://inference.ai.exchange/docs/market.md). The selected offer must support text, tool calls, and an output limit of at least 8,192 tokens. - `tmux`, for the terminal tool. On macOS, use `brew install tmux`. On Debian or Ubuntu, use `sudo apt-get install tmux`. ## Use the Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) starts the OpenHands CLI with the exchange as its provider. It sets the base URL, the key and the model for that process only. For the SDK, it prints the setup with your model filled in. ```bash curl -fsSL https://inference.ai.exchange/cli/install.sh | bash liquid login liquid openhands ``` For the SDK, run `liquid connect openhands-sdk` and not the last command. To set up OpenHands yourself, follow the sections below. ## SDK Use the SDK to create an agent in your Python application. See the [official SDK guide](https://docs.openhands.dev/sdk/getting-started). ### SDK setup 1. Install the packages in a Python environment with Python 3.12 or later. ```bash # Python 3.12 or later pip install openhands-sdk openhands-tools ``` 2. Export your buyer key in the shell that starts the application. ```bash export LIQUID_API_KEY=sk-liquid-... ``` | Setting | Purpose | | ---------------- | ------------------------------------------------ | | `LIQUID_API_KEY` | The buyer key that the SDK reads from the shell. | 3. Save this code as `agent.py` in your project directory. ```python import os from pathlib import Path from openhands.sdk import Agent, Conversation, LLM, Tool from openhands.tools.file_editor import FileEditorTool from openhands.tools.terminal import TerminalTool def create_agent() -> Agent: return Agent( llm=LLM( model="openai/liquid.auto", base_url="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], extra_headers={"X-Title": "OpenHands SDK", "x-liquid-cap-usd": "1.00"}, max_output_tokens=16384, native_tool_calling=True, reasoning_effort=None, ), tools=[Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name)], ) def main() -> None: conversation = Conversation(agent=create_agent(), workspace=str(Path.cwd())) try: conversation.send_message("List the files here.") conversation.run() finally: conversation.close() if __name__ == "__main__": main() ``` 4. Run `python agent.py` from the same shell. Notes on the code: - The model name has the `openai/` prefix. LiteLLM uses this prefix to select the OpenAI API format. - The base URL ends in `/v1`. The SDK sends requests to `/chat/completions`. - The terminal and file editor tools run in your current directory. Use a project directory where the agent can change files and run shell commands. ### SDK purchasing rules The `x-liquid-cap-usd` header caps each model request at $1.00. The output limit is 8,192 tokens for each request. A conversation can send several requests. The cap does not limit the total cost of the conversation. To limit total spend, use a separate key with a spend limit. For other purchasing rules, add `litellm_extra_body` to the `LLM` constructor: ```python litellm_extra_body={"exchange": {"max_ttft_ms": 5000}}, ``` Keep the cap in the header. Do not add a second cap in the request body. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md) for the fields and their units. ### SDK verification Check the receipts on the **Credits** page. Requests show **OpenHands SDK** as the client. Use the receipt for the settled charge. The SDK's token usage does not state the exchange's charge. ## CLI Use the CLI for a terminal session. The CLI requires Python 3.12 and installs its own SDK version. Use a separate Python environment from the SDK example above. See the [official CLI guide](https://docs.openhands.dev/openhands/usage/cli). ### CLI setup 1. Create a separate Python environment and install the CLI. ```bash # Python 3.12; use a separate environment for the CLI. python3.12 -m venv .venv-openhands source .venv-openhands/bin/activate pip install openhands ``` 2. Export your buyer key and the model settings in the same shell. ```bash export LIQUID_API_KEY=sk-liquid-... export LLM_API_KEY=$LIQUID_API_KEY export LLM_BASE_URL=https://router.inference.ai.exchange/v1 export LLM_MODEL=openai/liquid.auto ``` | Setting | Purpose | | ---------------- | --------------------------------------------- | | `LIQUID_API_KEY` | Your buyer key. | | `LLM_API_KEY` | Supplies the buyer key to the CLI. | | `LLM_MODEL` | The selected model with the `openai/` prefix. | | `LLM_BASE_URL` | The exchange base URL with `/v1`. | 3. Apply the exchange settings to the default agent file. ```bash python3 - <<'PY' import json from pathlib import Path path = Path.home() / ".openhands" / "agent_settings.json" settings = json.loads(path.read_text()) if path.exists() else {} llm = settings.setdefault("llm", {}) for config in [llm, (settings.get("condenser") or {}).get("llm")]: if config is None: continue config.update({ "model": "openai/liquid.auto", "base_url": "https://router.inference.ai.exchange/v1", "api_key": "env-overrides-key", "max_output_tokens": 16384, "native_tool_calling": True, "reasoning_effort": None, }) headers = config.get("extra_headers") or {} headers.update({ "X-Title": "OpenHands CLI", "x-liquid-cap-usd": "1.00", }) config["extra_headers"] = headers body = config.get("litellm_extra_body") or {} exchange = body.get("exchange") or {} exchange.pop("cap_usd", None) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(settings, indent=2) + "\n") PY ``` The command edits `~/.openhands/agent_settings.json` and preserves unrelated settings. It: - Sets a dummy API key in the file. The environment supplies the buyer key at runtime. - Sets the request headers, native tool calls, and an output limit of 8,192 tokens. - Removes a body cap so the header supplies the price cap. Other purchasing rules stay in the file. 4. Send a test request from a project directory. ```bash openhands --headless --json --override-with-envs --task "Reply with the word ready" ``` 5. Start an interactive session from the same shell. ```bash openhands --override-with-envs ``` Use `--override-with-envs` on every command. Without this flag, the CLI uses the dummy key from the file. - The flag replaces the file's model, API key, and base URL with the environment values. - The CLI keeps the file's headers and output limit. The CLI's local tools can change files and run shell commands in your project directory. ### CLI purchasing rules The `x-liquid-cap-usd` header caps each model request at $1.00. A session can send several requests. Use a separate key with a spend limit to limit total spend. For other purchasing rules, add `litellm_extra_body` inside the file's `llm` object: ```json "litellm_extra_body": {"exchange": {"max_ttft_ms": 5000}} ``` Keep the cap in the header. Do not add a second cap in the request body. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md) for the fields and their units. ### CLI verification Check the receipts on the **Credits** page. Requests show **OpenHands CLI** as the client. ### CLI troubleshooting | Symptom | Action | | ------------------------------------ | ------------------------------------------------------------------- | | `401` | Confirm the buyer key and add `--override-with-envs`. | | `model_not_found` | Choose a name from `GET /v1/models` and keep the `openai/` prefix. | | The request exceeds the cap | Reduce the output limit or increase the cap in `llm.extra_headers`. | | The workspace has insufficient funds | Add funds on the **Credits** page. | # LlamaIndex LlamaIndex is a framework for applications that search your own data. Use its OpenAI-like embedding class with the exchange as the base URL to buy embeddings. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A model name from `GET /v1/models?kind=embedding`. See [Embeddings](https://inference.ai.exchange/docs/embeddings.md). ## 1. Export the key ```bash export LIQUID_API_KEY=sk-liquid-... ``` ## 2. Install the package ```bash # Python 3.9 or later pip install llama-index-embeddings-openai-like ``` ## 3. Embed text Use `OpenAILikeEmbedding`. Do not use `OpenAIEmbedding`, because it accepts only the model names of OpenAI. ```python import os from llama_index.embeddings.openai_like import OpenAILikeEmbedding embed_model = OpenAILikeEmbedding( model_name="text-embedding-3-small", api_base="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], embed_batch_size=64, ) vectors = embed_model.get_text_embedding_batch(["a cat on a bicycle", "a dog in a car"]) print(len(vectors), len(vectors[0])) # To use it in every index and query engine: from llama_index.core import Settings Settings.embed_model = embed_model ``` The last lines set the model for every index and query engine. ## Notes - Keep one model for one index. A change of model needs a new index. See [Embeddings](https://inference.ai.exchange/docs/embeddings.md). - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` response header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A request can have 2,048 texts at most. Keep `embed_batch_size` at or below that number. - The exchange has no streaming form for embeddings. It sends each batch complete. ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | ------------------------------------------------------------------------------ | | `401` | The key is wrong, revoked or expired. Export it again. | | `404` | Keep `/v1` at the end of `api_base`. | | `model_not_found` | The model is not on the book. Use an exact name from `GET /v1/models`. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the error codes. ## Next steps - [Embeddings](https://inference.ai.exchange/docs/embeddings.md): the request, the price and the limits. - [Qdrant](https://inference.ai.exchange/docs/connect-qdrant.md): store the vectors in a collection. - [LangChain](https://inference.ai.exchange/docs/connect-langchain.md): the same task with LangChain. # pgvector pgvector adds a vector column and a nearest-neighbor search to Postgres. The database does not call the exchange. Your code gets the vectors from the exchange and stores them in the table. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A Postgres database where you can run `CREATE EXTENSION vector`. - A model name from `GET /v1/models?kind=embedding`. See [Embeddings](https://inference.ai.exchange/docs/embeddings.md). ## 1. Export the key ```bash export LIQUID_API_KEY=sk-liquid-... export DATABASE_URL=postgresql://user:password@host:5432/dbname ``` ## 2. Install the packages ```bash # Python 3.9 or later pip install openai "psycopg[binary]" ``` ## 3. Create the table and store vectors The column length must equal the vector length of the model. `text-embedding-3-small` returns 1,536 numbers. A model with a different length needs a different column. **SQL** ```sql CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE documents ( id bigserial PRIMARY KEY, body text NOT NULL, embedding vector(1536) NOT NULL ); CREATE INDEX documents_embedding_idx ON documents USING hnsw (embedding vector_cosine_ops); ``` **Python** ```python import os import psycopg from openai import OpenAI MODEL = "text-embedding-3-small" client = OpenAI( base_url="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], ) def embed(texts): result = client.embeddings.create(model=MODEL, input=texts) return [item.embedding for item in result.data] texts = ["a cat on a bicycle", "a dog in a car"] with psycopg.connect(os.environ["DATABASE_URL"]) as conn: for text, vector in zip(texts, embed(texts)): conn.execute( "INSERT INTO documents (body, embedding) VALUES (%s, %s::vector)", (text, str(vector)), ) query = embed(["an animal on two wheels"])[0] rows = conn.execute( "SELECT body, embedding <=> %s::vector AS distance " "FROM documents ORDER BY distance LIMIT 3", (str(query),), ).fetchall() for body, distance in rows: print(f"{distance:.3f} {body}") ``` The operator `<=>` is the cosine distance, so the index uses `vector_cosine_ops`. Use the same model for the stored texts and for the query. ## Notes - Send the texts in batches of up to 2,048. One request is one auction and has one cap. - Keep one model for one table. A change of model needs a new column and a new embed of every row. See [Embeddings](https://inference.ai.exchange/docs/embeddings.md). - An index of type `hnsw` supports vectors of up to 2,000 numbers. For a longer vector, use the `halfvec` type or set `dimensions` to a shorter length on a model that supports it. - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` response header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | ------------------------------------------------------------------------------------ | | `expected 1536 dimensions, not 1024` | The column length differs from the model. Create the column with the model's length. | | `401` | The key is wrong, revoked or expired. Export it again. | | `model_not_found` | The model is not on the book. Use an exact name from `GET /v1/models`. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the error codes. ## Next steps - [Embeddings](https://inference.ai.exchange/docs/embeddings.md): the request, the price and the limits. - [Qdrant](https://inference.ai.exchange/docs/connect-qdrant.md) and [Pinecone](https://inference.ai.exchange/docs/connect-pinecone.md): a database that is only for vectors. # Qdrant Qdrant is a vector database. It does not call the exchange. Your code gets the vectors from the exchange and writes them to a Qdrant collection. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A Qdrant server or a Qdrant Cloud cluster, with its URL and API key. - A model name from `GET /v1/models?kind=embedding`. See [Embeddings](https://inference.ai.exchange/docs/embeddings.md). ## 1. Export the keys ```bash export LIQUID_API_KEY=sk-liquid-... export QDRANT_URL=http://localhost:6333 export QDRANT_API_KEY=... # only for a cluster that needs one ``` ## 2. Install the packages ```bash # Python 3.9 or later pip install openai qdrant-client ``` ## 3. Store and search The size of the collection must equal the vector length of the model. `text-embedding-3-small` returns 1,536 numbers. ```python import os from openai import OpenAI from qdrant_client import QdrantClient, models MODEL = "text-embedding-3-small" openai = OpenAI( base_url="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], ) qdrant = QdrantClient( url=os.environ["QDRANT_URL"], api_key=os.environ.get("QDRANT_API_KEY"), ) def embed(texts): result = openai.embeddings.create(model=MODEL, input=texts) return [item.embedding for item in result.data] qdrant.create_collection( collection_name="documents", vectors_config=models.VectorParams(size=1536, distance=models.Distance.COSINE), ) texts = ["a cat on a bicycle", "a dog in a car"] qdrant.upsert( collection_name="documents", points=[ models.PointStruct(id=i, vector=vector, payload={"body": text}) for i, (text, vector) in enumerate(zip(texts, embed(texts))) ], ) hits = qdrant.query_points( collection_name="documents", query=embed(["an animal on two wheels"])[0], limit=3, ).points for hit in hits: print(f"{hit.score:.3f} {hit.payload['body']}") ``` Use the same model for the stored texts and for the query. ## Notes - Send the texts in batches of up to 2,048. One request is one auction and has one cap. - Keep one model for one collection. A change of model needs a new collection and a new embed of every point. See [Embeddings](https://inference.ai.exchange/docs/embeddings.md). - `create_collection` fails if the collection exists. Create it one time, or check with `collection_exists`. - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` response header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | ------------------------------------------------------------------------------------------ | | `Vector dimension error` | The collection size differs from the model. Create the collection with the model's length. | | `401` from the exchange | The key is wrong, revoked or expired. Export it again. | | `model_not_found` | The model is not on the book. Use an exact name from `GET /v1/models`. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the error codes. ## Next steps - [Embeddings](https://inference.ai.exchange/docs/embeddings.md): the request, the price and the limits. - [LlamaIndex](https://inference.ai.exchange/docs/connect-llamaindex.md): build an index over your documents. - [pgvector](https://inference.ai.exchange/docs/connect-pgvector.md): keep the vectors in Postgres instead. # Pinecone Pinecone is a hosted vector database. It does not call the exchange. Your code gets the vectors from the exchange and writes them to a Pinecone index. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A Pinecone account and API key. - A model name from `GET /v1/models?kind=embedding`. See [Embeddings](https://inference.ai.exchange/docs/embeddings.md). ## 1. Export the keys ```bash export LIQUID_API_KEY=sk-liquid-... export PINECONE_API_KEY=... ``` ## 2. Install the packages ```bash # Python 3.9 or later pip install openai pinecone ``` ## 3. Store and search Create the index with the vector length of the model. `text-embedding-3-small` returns 1,536 numbers. Do not use a Pinecone integrated model: that index embeds the text itself and does not take your vectors. ```python import os from openai import OpenAI from pinecone import Pinecone, ServerlessSpec MODEL = "text-embedding-3-small" openai = OpenAI( base_url="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], ) pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"]) def embed(texts): result = openai.embeddings.create(model=MODEL, input=texts) return [item.embedding for item in result.data] if not pc.has_index("documents"): pc.create_index( name="documents", dimension=1536, metric="cosine", spec=ServerlessSpec(cloud="aws", region="us-east-1"), ) index = pc.Index("documents") texts = ["a cat on a bicycle", "a dog in a car"] index.upsert( vectors=[ {"id": f"doc-{i}", "values": vector, "metadata": {"body": text}} for i, (text, vector) in enumerate(zip(texts, embed(texts))) ] ) result = index.query( vector=embed(["an animal on two wheels"])[0], top_k=3, include_metadata=True, ) for match in result.matches: print(f"{match.score:.3f} {match.metadata['body']}") ``` A new write can take a few seconds to appear in a query. Use the same model for the stored texts and for the query. ## Notes - Send the texts in batches of up to 2,048 to the exchange. Pinecone limits the size of an upsert request, so write the vectors in smaller batches, for example 100 at a time. - Keep one model for one index. A change of model needs a new index and a new embed of every record. See [Embeddings](https://inference.ai.exchange/docs/embeddings.md). - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` response header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | ----------------------------------------------------------------------------------------- | | `Vector dimension 1024 does not match` | The index length differs from the model. Create the index with the model's vector length. | | `401` from the exchange | The key is wrong, revoked or expired. Export it again. | | `model_not_found` | The model is not on the book. Use an exact name from `GET /v1/models`. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the error codes. ## Next steps - [Embeddings](https://inference.ai.exchange/docs/embeddings.md): the request, the price and the limits. - [Qdrant](https://inference.ai.exchange/docs/connect-qdrant.md) and [pgvector](https://inference.ai.exchange/docs/connect-pgvector.md): other places to keep the vectors. # RAGFlow RAGFlow is an open-source engine that answers questions from your own documents. Add the exchange as an OpenAI-API-Compatible provider. RAGFlow then buys its chat answers and its embeddings from the exchange. The exchange auctions each request, sets a cap before the job starts and charges for the measured use. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - A RAGFlow server that you can configure. - A chat model and an embedding model from the book. See [Market data](https://inference.ai.exchange/docs/market.md) and [Embeddings](https://inference.ai.exchange/docs/embeddings.md). ## 1. Add a provider Open **User settings → Model providers**. Add the **OpenAI-API-Compatible** provider and create an instance. Enter: ``` https://router.inference.ai.exchange/v1 ``` The URL ends in `/v1`. ## 2. Paste the key ``` sk-liquid-... ``` ## 3. Add the chat model Add a model to the instance. Set the model type to **chat**. The model name must be an exact name from `GET /v1/models`. ``` liquid.auto ``` Turn on **Tool calling** only for a model that supports tools. To list those models, use `GET /v1/models?feature=tools`. ## 4. Add the embedding model Add a second model to the same instance. Set the model type to **embedding**. Use a name from `GET /v1/models?kind=embedding`. ``` text-embedding-3-small ``` ## 5. Set the defaults In **Set default models**, choose the chat model and the embedding model. RAGFlow needs both. Then use **Verify** on each model. Verify sends a real request, so each check is a small purchase. ## Notes - Keep one embedding model for one dataset. RAGFlow ties the vectors of a dataset to the model that made them. Choose the model before you parse documents. A change of model needs a new dataset. See [Embeddings](https://inference.ai.exchange/docs/embeddings.md). - Name the embedding model. The exchange does not choose one for you, and [Auto Router](https://inference.ai.exchange/docs/auto.md) cannot choose one. - The exchange has no rerank endpoint. Leave the rerank model empty, or use the rerank model of another provider. - Parsing a large set of documents makes many embedding requests. A request can have up to 2,048 texts. - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` response header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - Use a preset for other purchasing rules. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). ## Troubleshooting | Symptom | Cause and fix | | --------------------------------------- | ------------------------------------------------------------------------------ | | `401` | The key is wrong, revoked or expired. Paste it again. | | `404` | Keep `/v1` at the end of the base URL. | | `model_not_found` | The model is not on the book. Use an exact name from `GET /v1/models`. | | A request is refused for lack of funds. | The workspace balance cannot cover the cap. Add funds on the **Credits** page. | | Verify fails for the embedding model. | The name is not an embedding model. Use a name from `?kind=embedding`. | See [Errors](https://inference.ai.exchange/docs/errors.md) for the error codes. ## Next steps - [Embeddings](https://inference.ai.exchange/docs/embeddings.md): the request, the price and the limits. - [LlamaIndex](https://inference.ai.exchange/docs/connect-llamaindex.md): the same task in code. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): models, strategy and limits, and presets. # Liquid MCP server The exchange runs a Model Context Protocol (MCP) server. Add it to an agent, and the agent can look up models and prices, preview a purchase, read your balance and buy answers. To add it, run `liquid mcp claude` or follow [Connect](#connect). Tools that buy charge your balance and need a `spend` key. Use the server while you build. In an application, call the [REST API](https://inference.ai.exchange/docs/connect-openai-sdk.md). ## Connect The server is at `https://router.inference.ai.exchange/mcp`. It uses the Streamable HTTP transport and keeps no session. There are two ways to sign in: - **Sign in with your browser.** You choose the workspace, whether the app can buy answers, and the spend limit. Use this when the client supports OAuth. - **Use a key.** Send an API key as a bearer token, for a client that cannot open a browser. ### Sign in with your browser Only an owner or admin of the workspace can approve a key. The page shows where the browser goes next. Approve only if you started the request from that app. The key that the client gets: - Is named `Liquid MCP: `. Revoke it on the [API keys](https://inference.ai.exchange/docs/keys.md) page. - Expires in 7 days. The client then asks you to sign in again. - Has the spend limit that you set. The default is $10. - Cannot buy answers unless you tick the box on the consent page. It can still look up models and read the balance and receipts. The box is unticked for a client that asks for `mcp:read`, and ticked for `mcp:spend`. You decide either way. #### The Liquid CLI The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) adds the server in one step. Use `claude`, `codex`, `cursor` or `opencode`. A `liquid claude` or `liquid opencode` session has it already. ```bash liquid mcp claude ``` #### Claude Code ```bash claude mcp add --transport http liquid https://router.inference.ai.exchange/mcp claude mcp login liquid ``` You can also run `/mcp` in a session, select **liquid**, and choose **Authenticate**. #### Codex CLI ```bash codex mcp add liquid --url https://router.inference.ai.exchange/mcp codex mcp login liquid ``` Run the login on the machine where Codex runs, because Codex receives the code on `http://127.0.0.1`. #### Cursor Add this to `~/.cursor/mcp.json`, then sign in from the MCP settings. ```json { "mcpServers": { "liquid": { "url": "https://router.inference.ai.exchange/mcp" } } } ``` #### OpenCode Add this to `~/.config/opencode/opencode.json`. OpenCode starts the sign-in when the server answers 401, or when you run `opencode mcp auth liquid`. ```json { "mcp": { "liquid": { "type": "remote", "url": "https://router.inference.ai.exchange/mcp", "enabled": true } } } ``` #### Claude Desktop and Claude on the web The exchange is not in the connector directory, so add a custom connector. 1. Open **Customize → Connectors**, and choose **Add custom connector**. On a Team or Enterprise plan, an owner first adds it in **Organization settings → Connectors**, and each member then chooses **Connect**. 2. Set the name to `Liquid Inference` and the server URL to `https://router.inference.ai.exchange/mcp`. Leave the OAuth client ID and secret empty. 3. Choose **Add**, then **Connect**, and approve the request in your browser. ### Use a key A key with the `spend` scope can buy answers. A key that can only view the market can use the other tools. See [API keys](https://inference.ai.exchange/docs/keys.md). Export the key once: ```bash export LIQUID_API_KEY=sk-liquid-... ``` #### Claude Code ```bash claude mcp add --transport http liquid https://router.inference.ai.exchange/mcp \ --header "Authorization: Bearer $LIQUID_API_KEY" ``` #### Codex CLI Add this to `~/.codex/config.toml`. ```toml [mcp_servers.liquid] url = "https://router.inference.ai.exchange/mcp" bearer_token_env_var = "LIQUID_API_KEY" ``` #### Cursor and OpenCode Use the config from above and add the key: - Cursor: add `"headers": { "Authorization": "Bearer ${env:LIQUID_API_KEY}" }` to the `liquid` entry. - OpenCode: add `"oauth": false`, which makes OpenCode use your key, and `"headers": { "Authorization": "Bearer {env:LIQUID_API_KEY}" }`. ## Tools Each tool calls the REST endpoint of the same name in the [API reference](https://inference.ai.exchange/docs/api.md), with the same key rules, rate limits and billing. ### Tools that read These tools need any key, unless the table says `read`. | Tool | What it does | Key | | ---------------- | ------------------------------------------------------------------------------------ | ---- | | `list_models` | Lists the models that the exchange serves now. | any | | `get_model` | Gets one model card: context window, features, prices. | any | | `compare_models` | Compares two to five models. Takes `models`. | any | | `get_book` | Lists the resting offers. | any | | `get_markets` | Gets the providers and offers for each model. | any | | `get_routers` | Lists router models, such as `liquid.auto`. | any | | `get_fills` | Lists the newest settled jobs of a model. It never names a buyer. | any | | `get_volume` | Gets the tokens and jobs per model, router and preset. | any | | `get_throughput` | Gets the measured speed of each provider and model. | any | | `get_lifetime` | Gets the jobs settled and tokens processed in all. | any | | `count_tokens` | Estimates input tokens. Takes `prompt`, `system`. | any | | `dry_run` | Previews a purchase and buys nothing. See [Preview a purchase](#preview-a-purchase). | any | | `get_video` | Gets the status of a video job. See [Video](#video). | any | | `get_balance` | Gets the workspace balance. | read | | `get_limits` | Gets what the key can spend: grade, balance, spend limit and the part used. | read | | `get_job` | Gets the receipt of one job. | read | | `list_receipts` | Lists the jobs of the workspace, newest first. | read | `list_models` and `get_book` take `kind`, `max_price_usd`, `limit` and `offset` (see [Find a model](#find-a-model)). `get_volume` and `get_throughput` take `days` (1 to 90). `list_receipts` takes `limit`, `cursor`, and `model` and `outcome` as comma-separated lists. ### Tools that buy These tools charge the balance. They need a `spend` key. | Tool | What it buys | Arguments | | ----------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | | `send_message` | One answer. See [Buy an answer](#buy-an-answer). | See that section. | | `generate_image` | One to ten images. | `model`, `prompt`; optionally `n`, `size`, `quality`, `output_format`, references | | `generate_speech` | Spoken audio. | `model`, `input`, `voice`; optionally `response_format` (`mp3`, `opus`, `aac`, `flac`, `wav` or `pcm`), `speed`, references | | `create_video` | A video job. It returns the job ID. | `model`, `prompt`; optionally `seconds` (at most 20), `size`, `callback_url`, references | | `transcribe` | A transcript of an audio file. | `model`, `audio_base64`; optionally `language`, `prompt`, `response_format`, `max_usd_per_second` | | `decide` | Answers to typed questions. | `state`, `questions`; optionally `model`, `orderings` | | `embed` | Vectors for one text or up to 2,048 texts. | `model`, `input`; optionally `dimensions`, `encoding_format` | Every tool that buys also takes `cap_usd` and the [purchasing controls](#purchasing-controls). ### Preview a purchase Call `dry_run` before `send_message`. It shows the estimated charge and the winning offer. It places no job and holds no funds. | Argument | Meaning | | -------------------------- | ------------------------------------------ | | `model` | The model to preview. | | `input_tokens` | Default 4,000. | | `max_output_tokens` | Default 1,000. | | `cap_usd`, `task`, `tools` | Optional. `task` is `general` or `coding`. | `kind` previews a job that is not chat. Send the size of the job with it: | `kind` | Size | | --------------------- | ------------------------------------------------------------------------------------ | | `image_generation` | `n` (default 1), optionally `size`. | | `audio_speech` | `characters`, optionally `voice` (default `alloy`). | | `video_generation` | Optionally `seconds` and `size`. | | `audio_transcription` | `seconds` of audio. | | `embedding` | Optionally `inputs` (default 1) and `dimensions`. `input_tokens` sizes each text. | | `decision` | `questions`, optionally `orderings` (default 4). `input_tokens` sizes the situation. | ### Find a model - `kind` takes one kind or a comma-separated list: `text`, `image`, `video`, `speech`, `transcription`, `embedding` or `decision`. - `supports` (`list_models` only) keeps models that support a parameter, such as `input.image`. - `max_price_usd` keeps the offers whose rate is at most that price. It needs exactly one `kind`. It leaves out offers priced in another unit, and router models. | Kind | Unit of `max_price_usd` | | ----------------------- | ------------------------------ | | `text` | A million output tokens | | `embedding`, `decision` | A million input tokens | | Other kinds | One image, character or second | ### Buy an answer `send_message` charges the balance by the tokens used. `cap_usd` is the most that the job can cost. An offer with a higher worst case does not compete. The reply has the answer, the usage and the `exchange` block with the job ID and charge. `send_message` takes `model` and a `prompt`, `messages` or both. Optional arguments: | Group | Arguments | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | Input | `system`, `image_urls` (`https` or `data:image` URLs), `attachments` | | Sampling | `temperature`, `top_p`, `top_k`, `min_p`, `repetition_penalty`, `frequency_penalty`, `presence_penalty`, `seed`, `logit_bias`, `stop` | | Output | `logprobs`, `top_logprobs`, `reasoning_effort`, `response_format` (`text`, `json_object` or a JSON schema object), `max_tokens` | | Tools | `tools`, `tool_choice` | | Cost and retry | `cap_usd`, `idempotency_key` | - `messages` holds earlier turns as `{role, content}`. The role is `user`, `assistant` or `tool`. The `prompt` follows them. - An `assistant` turn can hold `tool_calls` and no content. A `tool` turn needs a `tool_call_id`. - An `attachment` is a reference (see [Media and references](#media-and-references)), with inline data for audio, video and files. - `max_tokens` defaults to 1024, which keeps the hold small. - An argument that no offer supports gets the refusal of the endpoint, and nothing is charged. When the model calls a tool, the reply has `tool_calls`. The server does not run them. To continue, send the `assistant` turn with its `tool_calls`, then a `tool` turn with the result. To retry a call that timed out without a second purchase, send the same `idempotency_key`. Every tool that buys takes it. The job runs to its end even when the client leaves, so a retry without a key can buy twice. ### Other buying tools #### decide `decide` calls `POST /v1/systemone`. See the [Decisions API](https://inference.ai.exchange/docs/api-systemone.md). - `state` is the situation, as text or data. `questions` holds typed questions by ID. - `model` is a decision model from `list_models`. The default is `liquid.decision`. Liquid Auto cannot answer decisions. - `orderings` sets how many orderings of the options each question gets. The cap holds one read of the prompt for each ordering. #### embed `embed` calls `POST /v1/embeddings` and charges per input token. Vectors are floats, so keep the list short. #### transcribe `transcribe` charges for each second of audio. - Send the file as base64 or as a `data:audio/` URL in `audio_base64`. The limit is 1,000,000 characters, about 750 KB of audio. The tool does not fetch a URL. For a larger file, call `POST /v1/audio/transcriptions`. #### Images and speech `generate_image` and `generate_speech` return the media and the `exchange` block. ### Media and references `create_video`, `generate_image` and `generate_speech` take `references`. `generate_image` and `create_video` also take `reference_image_urls`. | Argument | Meaning | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `image_url` | `create_video` only. An `https` or `data:image` address. The video starts from this frame. | | `reference_image_urls` | Images that the output should keep. | | `references` | The full form: a list of `{type: "image", url}`, `{type: "audio", data, format}`, `{type: "video", url}` and `{type: "file", file_data, filename}`. | | `callback_url` | `create_video` only. An `https` address that the exchange calls when the job ends. | - A call has at most 16 references. The model must accept image input. - A request to `/mcp` is limited to 20 MB. Base64 is a third larger than the file. For a large file, use an `https` address. ### Video `get_video` takes `video_id` and optionally `wait_seconds` (up to 300), `include_file` and `chunk`. A completed job also returns: - `download_url`, a link that works without a key for 15 minutes. - The video file as an embedded resource, up to 4 MB. A client drops a result that is too big, with the link in it. For a larger file, open `download_url`. If there is no link, send `chunk` (0, 1, and so on). Each chunk is 1 MB as base64, and the reply states `chunk_count`. A file can be at most 64 MB this way. Send `include_file` as `false` to get the status and the link only. ### Purchasing controls The tools that buy, and `dry_run`, take the `x-liquid-*` header controls as arguments. A value that cannot be read is a tool error, and nothing is charged. | Argument | Header | Value | | ----------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `retention` | `x-liquid-retention` | `none` keeps only offers whose provider does not retain the content. `allowed` is the default. | | `ttft_ms` | `x-liquid-ttft-ms` | The most time to the first token, in milliseconds. | | `tag` | `x-liquid-tag` | A label for the spend. At most 63 letters, digits, dots, dashes and underscores. | | `rules` | `x-liquid-rules` | An object with other rules of the `exchange` block, such as `exchange.privacy.regions`. It cannot set `exchange.candidates.models.allow` or `exchange.rank.strategy`. | ## Reference ### Resources and prompts A resource read needs the same key as its tool. `liquid://models`, `book`, `markets`, `routers`, `balance` and `lifetime` hold the data of the tools with those names. `liquid://models/{model}`, `liquid://book/{model}`, `liquid://markets/{model}` and `liquid://fills/{model}` hold the data for one model. `liquid://jobs/{job_id}` holds one receipt. `completion/complete` gives model IDs for `model` and `models`, and recent job IDs for `job_id`. Three prompts write a request and buy nothing: `find_cheapest` (`task`, optionally `max_usd`), `audit_spend` (optionally `days`) and `compare_models` (`models`). ### Widgets A client that supports MCP Apps shows a widget under some results. The tool names it in `_meta.ui.resourceUri`. A widget has no button that buys. Each widget is `ui://liquid/`: `models`, `model`, `book`, `receipts`, `job`, `markets`, `fills`, `account` (`get_balance`, `get_limits`), `image`, `speech`, `video` (`create_video`, `get_video`) and `dry-run`. The name follows the tool, such as `book` for `get_book`. The widgets are resources with the media type `text/html;profile=mcp-app`. The server offers the `io.modelcontextprotocol/ui` extension. ### Results and progress A result holds the data as text and as `structuredContent`. A list is the object `{ "items": [...] }`. A failed tool has no `structuredContent`. `get_video` with `wait_seconds` checks the job until it completes, fails or the time ends. For a `notifications/progress` message at each check, the client must send `_meta.progressToken` and accept `text/event-stream`. A refusal for the grade of the key then arrives as a tool error, not a 403. The server does not use elicitation. When a purchase is refused with a `proposed_retry`, the result says to ask the person. ### Tool errors A failed tool returns a result with `isError` set. It holds the HTTP status and the error body of the endpoint. See [Errors](https://inference.ai.exchange/docs/errors.md). ### How the sign-in works This section is for people who build a client. 1. A request to `/mcp` without a key returns `401` with a `WWW-Authenticate` header. It names the resource metadata (RFC 9728) at `/.well-known/oauth-protected-resource/mcp`. 2. That metadata names the authorization server (RFC 8414, `/.well-known/oauth-authorization-server`). 3. The client identifies itself in one of two ways. - **Metadata document.** The `client_id` is the `https` address of a JSON document, and the server metadata sets `client_id_metadata_document_supported` to `true`. - The address must have a path, no query or fragment, no port, and a public host name. - The document must name its own address as `client_id`, list `redirect_uris`, and use `token_endpoint_auth_method` `none`. It must be at most 8 KB with a JSON content type. - The exchange reads it at sign-in. It follows no redirect and refuses a host in its own network. - **Registration.** The client registers at the `registration_endpoint` (RFC 7591). All clients are public: `token_endpoint_auth_method` is `none`. - Each `redirect_uri` must use `https`, `http` on `localhost`, `127.0.0.1` or `[::1]`, or an app scheme. - An app scheme is a reverse-DNS name (it has a `.`) or one of `claude`, `cursor`, `goose`, `vscode`, `vscode-insiders`, `windsurf` and `zed`. 4. The client opens the `authorization_endpoint` with `response_type=code`, `code_challenge_method=S256` and a `state`. PKCE is required. A `resource` (RFC 8707) must be the address of the MCP server or the router origin, or the exchange returns `invalid_target`. 5. The `scope` is `mcp:read`, `mcp:spend`, or both. `mcp:spend` includes reading. Any other scope gets `invalid_scope`. With no scope, the client asks for `mcp:read`. The token response names the scope that the person granted, which can be less. 6. Each redirect to the client, success or error, carries `iss` (RFC 9207). The client must compare it with the issuer that it recorded and refuse a code on a mismatch. 7. The client sends the code to the `token_endpoint` with the `code_verifier`. The response has the key as `access_token`, with `token_type` `Bearer` and `expires_in` in seconds. There are no refresh tokens. The `/mcp` route refuses a browser request with an `Origin` that is not the exchange or a loopback address. A code works one time and ends after 5 minutes. If a client sends a code twice, the exchange revokes the key that the first request got. A wrong `code_verifier` uses up the code. #### Asking for more access A `401` carries `scope="mcp:read"` in `WWW-Authenticate`. If a key cannot do what a tool needs, the server answers `403` with `error="insufficient_scope"` and the needed scope, such as `scope="mcp:spend"`. The JSON body still has the tool error. # MCP servers with the exchange Use any model on the exchange with the tools of an MCP server. An MCP client lists the tools of a server, lets a model choose a tool, and runs it. The exchange accepts OpenAI tool definitions, so the client converts each MCP tool to that format and sends it with the request. To give an agent the tools of the exchange itself, see the [Liquid MCP server](https://inference.ai.exchange/docs/connect-mcp-server.md). ## 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. Install the packages ```bash # Python 3.10 or later, and Node.js for the example server pip install mcp openai ``` The example server needs Node.js. ## 2. Run the tool loop This client starts a local file server, sends its tools with each request, and runs every tool call that the model returns. It stops when the model answers without a tool call. ```python import asyncio import json import os from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from openai import AsyncOpenAI client = AsyncOpenAI( base_url="https://router.inference.ai.exchange/v1", api_key=os.environ["LIQUID_API_KEY"], default_headers={"X-Title": "MCP", "x-liquid-cap-usd": "0.05"}, ) server = StdioServerParameters( command="npx", args=["-y", "@modelcontextprotocol/server-filesystem@1.0.0", os.getcwd()], ) def to_openai_tool(tool): return { "type": "function", "function": { "name": tool.name, "description": tool.description or "", "parameters": tool.input_schema, }, } async def main(question): async with stdio_client(server) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = [to_openai_tool(t) for t in (await session.list_tools()).tools] messages = [{"role": "user", "content": question}] for _ in range(8): response = await client.chat.completions.create( model="liquid.auto", messages=messages, tools=tools ) reply = response.choices[0].message messages.append(reply.model_dump(exclude_none=True)) if not reply.tool_calls: print(reply.content) return for call in reply.tool_calls: result = await session.call_tool( call.function.name, json.loads(call.function.arguments) ) text = "\n".join(p.text for p in result.content if p.type == "text") messages.append({"role": "tool", "tool_call_id": call.id, "content": text}) asyncio.run(main("List the files in this directory.")) ``` ## What it costs Each request is a separate auction, and each has its own charge. The `x-liquid-cap-usd` header limits the charge of one request. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). The loop sends the whole conversation with each request. The exchange charges for every input token. Keep tool results short. ## Change the model Choose another model, router model or preset from `GET /v1/models`. The model must support tools. See [Market data](https://inference.ai.exchange/docs/market.md). ## Notes - A remote MCP server needs `streamable_http_client` from `mcp.client.streamable_http` in place of `stdio_client`. The rest of the loop does not change. - MCP 2.0 renamed `inputSchema` to `input_schema` on a tool. Code for MCP 1.x that reads `inputSchema` fails on 2.x. # DeepSeek Harness plugin The Liquid Inference plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) adds the exchange to the harness. It adds a `liquid` model route and four read-only market tools. The source is [architect-xyz/liquid-dsh-plugin](https://github.com/architect-xyz/liquid-dsh-plugin). To use the exchange without the plugin, see [DeepSeek Harness](https://inference.ai.exchange/docs/connect-deepseek-harness.md). That guide writes a patch file by hand. The [Liquid CLI](https://inference.ai.exchange/docs/cli.md) can also start `dsh` for you. ## Before you start - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). - DeepSeek Harness with a profile, for example `web`. The package is not on npm yet. Install it from GitHub or from a packed tarball. ## 1. Export the buyer key ```bash export LIQUID_API_KEY=sk-liquid-... ``` The plugin reads the key for each request. It reads the credential store of the profile first, and then `LIQUID_API_KEY`. It never writes the key to a config file. A headless profile has no credential store, so it needs the variable. ## 2. Install the plugin Install from GitHub. The package builds itself when you install it: ```bash dsh plugin --profile web add github:architect-xyz/liquid-dsh-plugin ``` The first run fails with `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`, because pnpm blocks the build script. To fix it, add the package to `allowBuilds` in `$DSH_HOME/profiles/web/pnpm-workspace.yaml`, then run the command again: ```yaml allowBuilds: dsh-liquid-inference: true ``` This setting lets the package run its build script on your computer, outside a sandbox. Pin a commit, for example `github:architect-xyz/liquid-dsh-plugin#`, so a later push cannot change what runs. ### Install without a build permission Pack the plugin and install the tarball: ```bash git clone https://github.com/architect-xyz/liquid-dsh-plugin.git cd liquid-dsh-plugin && pnpm install && pnpm pack dsh plugin --profile web add ./dsh-liquid-inference-0.1.0.tgz ``` Restart the profile. Choose **Liquid Auto** (`liquid.auto`) in the model picker. ## What the plugin adds ### The `liquid` model route The plugin needs no adapter code. It patches the stock `llm-pi-ai` adapter with `api: openai-completions`, the exchange base URL with `/v1`, and `apiKeyEnv: LIQUID_API_KEY`. The patch sets these values: | Setting | Value | | --------- | ----------------------------------------------------------------------------------------------------------------- | | Cap | Each request sends `x-liquid-cap-usd`. The default is 1 USD. Set `LIQUID_CAP_USD` before you start to change it. | | Models | `liquid.auto`, the ranking variants `:cheapest`, `:economy`, `:balanced` and `:premium`, and `deepseek-v4-flash`. | | Reasoning | The levels `low`, `medium` and `high` go to the exchange as `reasoning_effort`. | To add a model, add an entry under `models` in the `cordis.patch.yml` of your profile. Use a name from the **Market** page. See [Market data](https://inference.ai.exchange/docs/market.md). The exchange does not check the reasoning level. If the winning provider refuses the level, that request fails. ### Market tools | Tool | Endpoint | Key | | -------------------- | -------------------------- | -------- | | `liquid_get_book` | `GET /v1/exchange/offers` | Not used | | `liquid_get_markets` | `GET /v1/exchange/markets` | Not used | | `liquid_get_routers` | `GET /v1/exchange/routers` | Not used | | `liquid_get_job` | `GET /v1/jobs/{id}` | Used | Chat, image, speech and video purchases are not tools. The model route is the only place where the harness spends money. ## Data handling The harness sends full prompts and tool results to the exchange. The exchange sends them to the provider that wins the auction. See [Data handling](https://inference.ai.exchange/docs/data-handling.md). ## Notes - The patch replaces the whole `config` of the `llm-pi-ai` row. If another bundle also configures pi-ai providers, write their settings again in your `cordis.patch.yml`. - The cap limits each request. To limit the total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). ## Next steps - [DeepSeek Harness](https://inference.ai.exchange/docs/connect-deepseek-harness.md): set up the harness by hand, with no plugin. - [Apps](https://inference.ai.exchange/docs/apps.md): the other plugins and tools. - [Skills](https://inference.ai.exchange/docs/skills.md): teach a coding agent to use the exchange. # Hermes Agent plugin The Liquid Inference plugin adds the exchange to [Hermes Agent](https://github.com/NousResearch/hermes-agent) as a model provider. The source is [architect-xyz/hermes-liquid-inference](https://github.com/architect-xyz/hermes-liquid-inference). The plugin is also in the [Hermes Agent plugin catalog](https://hermes-agent.nousresearch.com/docs/plugins/liquid-inference-provider). To set each value yourself, with no plugin, see [Hermes Agent](https://inference.ai.exchange/docs/connect-hermes-agent.md). ## Before you start - Hermes Agent 0.21.6 or later. - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). ## 1. Install the plugin ```bash hermes plugins install architect-xyz/hermes-liquid-inference --enable ``` The command asks for `LIQUID_API_KEY` and saves it in `~/.hermes/.env`. ## 2. Choose the provider and the model ```bash hermes config set model.provider liquid hermes config set model.default liquid.auto ``` ## 3. Set the cap Each request has a cap of 1 USD. To change the cap, set the header: ```bash hermes config set model.default_headers.x-liquid-cap-usd 0.25 ``` The cap limits each request. To limit the total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). ## Reasoning effort Task requests use `medium` when you set no reasoning effort. To choose another level: ```bash hermes config set agent.reasoning_effort high ``` If you choose `none` or turn reasoning off, the plugin sends no reasoning effort. Auxiliary requests do not use the task default. If no offer accepts the effort, the exchange refuses the request with `400 unsupported_parameter` and makes no charge. Hermes then sends the request again without the effort. ## Next steps - [Hermes Agent](https://inference.ai.exchange/docs/connect-hermes-agent.md): set up Hermes with no plugin, or with the Liquid CLI. - [Axis routing](https://inference.ai.exchange/docs/axis-routing.md): rank offers by smartest, fastest or cheapest. - [Apps](https://inference.ai.exchange/docs/apps.md): the other plugins and tools. # Dify plugin The Liquid Inference plugin adds the exchange to [Dify](https://dify.ai) as a model provider. The source is [architect-xyz/liquid-dify-plugin](https://github.com/architect-xyz/liquid-dify-plugin). The plugin is on the [Dify Marketplace](https://marketplace.dify.ai/plugins/architect-xyz/liquid). ## Before you start - A Dify workspace where you can install plugins. - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). ## 1. Install the plugin In Dify, open **Settings**, then **Model Provider**. Find **Liquid Inference** and install it. ## 2. Add the key Paste the `spend` key in **API Key**. Leave **API Base URL** empty. The plugin uses `https://router.inference.ai.exchange/v1`. ## 3. Choose a model The list has `liquid.auto`, which lets the exchange choose the model for each request. To use another model, choose **Add Model**. Enter the name that `GET /v1/models` returns. Set the context size and the features of the model. See [Market data](https://inference.ai.exchange/docs/market.md). ## Notes - The plugin sends `X-Title: Dify` with each request. The receipts name Dify as the client. - The exchange sets a cap on each request. It is in the `x-liquid-cap-usd` response header. To limit total spend, set a spending limit on the key. See [API keys](https://inference.ai.exchange/docs/keys.md). - The plugin makes no payments. The exchange charges the balance of your workspace. ## Next steps - [Dify](https://inference.ai.exchange/docs/connect-dify.md): set up Dify with no plugin. - [Purchasing rules](https://inference.ai.exchange/docs/rules.md): models, strategy and limits, and presets. - [Apps](https://inference.ai.exchange/docs/apps.md): the other plugins and tools. # ComfyUI node The Liquid Inference package adds paid nodes to [ComfyUI](https://github.com/comfyanonymous/ComfyUI). Install it, set a key, and buy images or video from a workflow. The source is [architect-xyz/comfyui-liquid-inference](https://github.com/architect-xyz/comfyui-liquid-inference). | Node | What it does | | --------------------------- | ----------------------------------------------------------- | | **Liquid Image (paid API)** | Buys images from a text prompt. | | **Liquid Video (paid API)** | Buys a video from a text prompt. | | **Liquid Video Job** | Gets the video of an earlier job again, with no new charge. | A paid node sends the prompt to the exchange. The exchange gives the request to the best offer under your price cap. You pay for the images or the video that you get. Each run of a paid node is a paid request, and the prompt goes to the provider that wins the auction. ## Before you start - ComfyUI 0.3.45 or newer. - A workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). ## 1. Install the node In ComfyUI, open **Manager**, then **Custom Nodes Manager**. Search for "Liquid Inference", install it, and restart ComfyUI. To install by hand, clone the repository into `ComfyUI/custom_nodes/` and restart ComfyUI. The package has no Python dependencies. ## 2. Set the key Open **Settings**, then **Liquid Inference**, paste the key into **API key**, and select **Save**. The server keeps the key in `user/__liquid_inference/api_key`. Or set `LIQUID_API_KEY` before you start ComfyUI. The variable has priority over the stored key. The nodes have no input for the key on purpose. ComfyUI writes each input into the workflow file and into the metadata of each saved image. A key in an input would go to each person who gets your files. ## 3. Buy images 1. Double-click the canvas and search for "Liquid Image". 2. Write a prompt and choose a model. 3. Connect `images` to **Save Image**. 4. Queue the workflow. ## 4. Buy a video 1. Double-click the canvas and search for "Liquid Video". 2. Write a prompt and choose a model. 3. Connect `video` to **Save Video**. 4. Queue the workflow. Options: - To start the video from an image, connect the image to `start_image`. Only the models that read an image input can take it. - Select the content retention rule with `exchange.privacy.retention`. See [ComfyUI](https://inference.ai.exchange/docs/connect-comfyui.md) for the choices and their privacy effects. ### Wait for a video The node waits until the video is complete, for 30 minutes at most. A stop in ComfyUI stops the wait, not the job, and the exchange charges for the video. To get the video later, paste the job ID into a **Liquid Video Job** node. ## Example workflows The `example_workflows` folder has five workflows. Open them from **Templates**. ## Cost The cap applies to each request, not to the workflow. A workflow with three Liquid nodes and a cap of 0.05 can cost up to 0.15 USD for each run. ## Retries A paid node never sends a purchase again by itself, because a second request could buy the images or the video twice. - After a video purchase, the node tries a failed status read or download again, because these requests buy nothing. - If the connection stops before the answer arrives, the node reports that the outcome is unknown and shows an `Idempotency-Key`. Look at your receipts before you run the node again. ## Next steps - [ComfyUI](https://inference.ai.exchange/docs/connect-comfyui.md): the inputs, the outputs and the error table. - [Modalities](https://inference.ai.exchange/docs/modalities.md): what images cost. - [Video generation](https://inference.ai.exchange/docs/video-generation.md): video jobs, lengths and how they are priced. - [Apps](https://inference.ai.exchange/docs/apps.md): the other plugins and tools. # Apps These plugins, nodes and guides are open source. Each one has its own repository, README and page here. Each needs a workspace with a balance and a `spend` key. See [API keys](https://inference.ai.exchange/docs/keys.md). | App | What it adds | Page | Source | | ----------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------- | | DeepSeek Harness plugin | A `liquid` model route and read-only market tools for `dsh`. | [DeepSeek Harness plugin](https://inference.ai.exchange/docs/plugin-deepseek-harness.md) | [liquid-dsh-plugin](https://github.com/architect-xyz/liquid-dsh-plugin) | | Hermes Agent plugin | The exchange as a model provider for Hermes Agent. | [Hermes Agent plugin](https://inference.ai.exchange/docs/plugin-hermes.md) | [hermes-liquid-inference](https://github.com/architect-xyz/hermes-liquid-inference) | | Dify plugin | The exchange as a model provider for Dify. | [Dify plugin](https://inference.ai.exchange/docs/plugin-dify.md) | [liquid-dify-plugin](https://github.com/architect-xyz/liquid-dify-plugin) | | ComfyUI node | The **Liquid Image** and **Liquid Video** nodes. They buy images and videos with a cap on each run. | [ComfyUI node](https://inference.ai.exchange/docs/plugin-comfyui.md) | [comfyui-liquid-inference](https://github.com/architect-xyz/comfyui-liquid-inference) | | Liquid Inference guide | Guides, reference providers and agent skills. | [Liquid Inference guide](https://inference.ai.exchange/docs/app-guide.md) | [liquid-inference-guide](https://github.com/architect-xyz/liquid-inference-guide) | For a tool that has no plugin, see [Connect an app](https://inference.ai.exchange/docs/connect-overview.md). # Liquid Inference guide The [liquid-inference-guide](https://github.com/architect-xyz/liquid-inference-guide) repository holds guides, reference provider code and agent skills for the exchange. It is open source. ## Contents | Folder | For | Holds | | ----------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------ | | `docs/harnesses` | Run a coding agent on the exchange. | Claude Code, Codex, Cursor, Cline, OpenCode, Goose, Pi, OpenHands, Hermes Agent and Copilot CLI. | | `docs/agent-sdks` | Build your own agent. | The Claude Agent SDK, the OpenAI Agents SDK and the Vercel AI SDK. | | `docs/frameworks` | Use a framework. | LangChain, LlamaIndex and LiteLLM. | | `providers` | Sell inference. | Reference providers in Python, TypeScript and Rust. | | `skills` | Teach a coding agent. | Agent skills. See [Skills](https://inference.ai.exchange/docs/skills.md). | Start with `docs/getting-started.md`. It shows how to get a key, set the base URL, send a request and read the receipt. ## Conventions - Read the key from `LIQUID_API_KEY`. Never put it in code or in a committed file. - The base URL for OpenAI-format clients is `https://router.inference.ai.exchange/v1`. The base URL for Anthropic-format clients is `https://router.inference.ai.exchange`. - Examples cap each request at 0.50 USD. - Read the charge from the receipt, not from the cost display of a client. The repository guides come from the product docs. Check the docs of the tool when a step fails, because configuration keys and versions change. ## Next steps - [Skills](https://inference.ai.exchange/docs/skills.md): install the agent skills. - [Become a provider](https://inference.ai.exchange/docs/provider.md): sell on the exchange. - [Apps](https://inference.ai.exchange/docs/apps.md): the other plugins and tools. # Skills An agent skill is a folder with a `SKILL.md` file. It teaches a coding agent one task. The agent loads a skill when the task matches the skill description. The skills for the exchange are in the [liquid-inference-guide](https://github.com/architect-xyz/liquid-inference-guide/tree/main/skills) repository. The list below comes from that repository. When the repository gets a new skill, the new skill appears on this page with no change to the docs. ## Install With `npx skills`, which needs Node: ```bash # every skill, for your user, for every agent it finds npx skills add architect-xyz/liquid-inference-guide --skill '*' --global --yes # one skill, for one agent npx skills add architect-xyz/liquid-inference-guide --skill liquid-quickstart --agent claude-code # list the skills with no install npx skills add architect-xyz/liquid-inference-guide --list ``` Leave out `--global` to install into the current project. Add `--copy` to copy the files and not link them. With the GitHub CLI (`gh skill` is in preview): ```bash # every skill, into the current project gh skill install architect-xyz/liquid-inference-guide --all # one skill gh skill install architect-xyz/liquid-inference-guide liquid-quickstart ``` Without either tool, copy the folders by hand. Copy the whole folder of a skill, because the modality skills have a `scripts/` folder. Those scripts need Node 22 or later. ```bash git clone https://github.com/architect-xyz/liquid-inference-guide mkdir -p ~/.claude/skills cp -R liquid-inference-guide/skills/* ~/.claude/skills/ ``` Use the skills folder of your agent in place of `~/.claude/skills`. Start a new agent session after you install. Every skill reads the key from `LIQUID_API_KEY`. To check an install, ask your agent: "How do I send a first request to Liquid Inference?" ## Available skills The web page reads this list live. For the same list in a file, see the [skills folder on GitHub](https://github.com/architect-xyz/liquid-inference-guide/tree/main/skills). # API overview The exchange has one public API. An OpenAI client, an Anthropic client or plain HTTP can call it. Each endpoint page of this reference comes from the OpenAPI document of the exchange, so it always agrees with the exchange. ## Send a request Send an API key and a `model`. The exchange auctions the request, sets a price cap and charges for the tokens used. The price cap is the most that the job can cost. ```bash 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"}]}' ``` The response has the usual fields of the format, and one more: the `exchange` block. See [What each job returns](#what-each-job-returns). The [Quickstart](https://inference.ai.exchange/docs/quickstart.md) shows how to get a key. ## Endpoints ### Buy inference | Endpoint | Page | Description | | ----------------------------- | --------------------------------- | ------------------------------------------------------------- | | `POST /v1/chat/completions` | [Chat Completions](https://inference.ai.exchange/docs/api-chat.md) | OpenAI Chat Completions. | | `POST /v1/responses` | [Responses](https://inference.ai.exchange/docs/api-responses.md) | OpenAI Responses, without stored state. | | `POST /v1/messages` | [Messages](https://inference.ai.exchange/docs/api-messages.md) | Anthropic Messages, and a token count estimate. | | `POST /v1/systemone` | [Decisions](https://inference.ai.exchange/docs/api-systemone.md) | Typed questions, with a probability for each option. In beta. | | `POST /v1/embeddings` | [Embeddings](https://inference.ai.exchange/docs/api-embeddings.md) | Vectors for text. | | `POST /v1/images/generations` | [Images](https://inference.ai.exchange/docs/api-images.md) | Generated images. | | `POST /v1/audio/speech` | [Speech](https://inference.ai.exchange/docs/api-speech.md) | Spoken audio. | | `POST /v1/videos` | [Video](https://inference.ai.exchange/docs/api-videos.md) | Generated video, with a status and a download. | | `GET /v1/models` | [Models](https://inference.ai.exchange/docs/api-models.md) | The names that you can use as `model` now. | ### Read the market The [Market data](https://inference.ai.exchange/docs/api-market.md) page lists the public book, depth, routers and the live feed. These endpoints need no key. ### Sell inference The [Provider API](https://inference.ai.exchange/docs/api-provider.md) page lists the catalogue calls and the session. Provider credentials are different from API keys. See [Provider credentials](https://inference.ai.exchange/docs/provider.md). If you already have a client, read [What changed in the API](https://inference.ai.exchange/docs/api-changes.md). ## Field order The tables and the examples of this documentation list the fields of a request in one order: 1. `model`. 2. The content: `messages`, `prompt`, `input` or `file`. 3. The references: `input_references`. 4. The shape of the output: for example `n`, `size`, `voice`, `response_format`, `output_format`. 5. The controls: for example `temperature`, `top_p`, `seed`, `stop`, `quality`. 6. The delivery: `stream`, `callback_url`. 7. `exchange`, always last. The order is for the reader. The exchange reads a JSON body in any order. ## Authentication Send an API key in one of two headers. Both headers work on every endpoint. | Header | Used by | | ----------------------------------- | -------------------- | | `Authorization: Bearer sk-liquid-…` | OpenAI clients, curl | | `x-api-key: sk-liquid-…` | Anthropic clients | The scope of the key decides what it can do. A `spend` key can place jobs. A `read` key can read receipts and the balance. See [API keys](https://inference.ai.exchange/docs/keys.md). ## What each job returns Each job response has the shape of the format that you used, with one more field: the `exchange` block. It names the job, the provider handle, the price cap and the charge. Two headers arrive before the first token: | Header | Meaning | | ------------------ | -------------------------------------------------------------- | | `x-liquid-job-id` | The job id. Use it to read the receipt. | | `x-liquid-cap-usd` | The maximum charge for this job, set before the output starts. | To read the block again later, call `GET /v1/jobs/{job_id}`. It returns only the block. `GET /v1/exchange/jobs/{job_id}` returns the whole receipt, with the charge breakdown. See the [Quickstart](https://inference.ai.exchange/docs/quickstart.md). To see how the exchange chose the offer, send the `x-liquid-explain: true` request header. The block then has a `routing` object. To choose the models, the strategy or the limits, send an `exchange` block in the request body, or `x-liquid-*` headers. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). For failures, see [Errors](https://inference.ai.exchange/docs/errors.md). ## Reference ### Base URLs | Format | Base URL | | --------- | ----------------------------------------- | | OpenAI | `https://router.inference.ai.exchange/v1` | | Anthropic | `https://router.inference.ai.exchange` | An Anthropic client adds `/v1/messages` to its base URL. An OpenAI client adds only the endpoint name. ### The OpenAPI document The document is at `/v1/openapi.json` on the exchange. It needs no credential. You can load it into a client generator, Postman, Insomnia or Bruno. It lists only the public API. # Errors An error response has a JSON body. Match on `error.code`, not on the message. The status tells you what kind of fix to make. ## What to do | Status | What to do | | ------ | ------------------------------------------------------------------------ | | 400 | Change the request. No offer can serve it as it is. | | 401 | Send a valid key. See [API keys](https://inference.ai.exchange/docs/keys.md). | | 402 | Add funds, raise the limit, or lower the token limit. | | 403 | Use a key with the right scope, or change the request. | | 404 | Check the name. `GET /v1/models` lists your models. | | 409 | Use a new `idempotency-key` for a new body. | | 413 | Send less data. See [Modalities](https://inference.ai.exchange/docs/modalities.md). | | 429 | Wait for `Retry-After`, then send the request again. | | 503 | Send it again later. Some codes need a change; the tables say which. | | 500 | Send it again. If it repeats, give the job id in the message to support. | When the auction refuses a request, `eligibility.excluded` lists each offer that it left out and why. See [Exclusion reasons](#exclusion-reasons). ## Error format ```json { "error": { "message": "`reasoning.effort` is not supported by any offer for kimi-k3", "type": "liquid_error", "code": "unsupported_parameter", "param": "reasoning.effort", "eligibility": { "excluded": [ … ] } } } ``` | Field | Description | | -------------- | ---------------------------------------------------------------------------------------------- | | `code` | Machine-readable error code. Match on this. | | `message` | Human-readable description. | | `param` | The request field that the error names, if any. | | `eligibility` | Present when the auction refused the request. Lists each excluded offer and the reason. | | `auto` | On `no_eligible_offer` for an Auto request: what Auto ranked by. See [Auto Router](https://inference.ai.exchange/docs/auto.md). | | `binding_rule` | On `no_eligible_offer` when a provider allowlist left no offer. | | `fixes` | On `no_eligible_offer`: what to change. See [Fixes](#fixes). | | `provider` | On `provider_failed`, `request_rejected` and `tools_unsupported`: the awarded provider. | A `502` or `504` with an HTML body comes from the network in front of the exchange, not from the exchange. `POST /v1/messages` returns the Anthropic format, `{"type": "error", "error": {"type": ..., "message": ...}}`. An auction error keeps the `code`, `param`, `eligibility`, `auto`, `binding_rule` and `provider` fields next to `type` and `message`. | HTTP status | Anthropic `error.type` | | ----------- | ----------------------- | | 400 | `invalid_request_error` | | 401 | `authentication_error` | | 403, 451 | `permission_error` | | 404 | `not_found_error` | | 413 | `request_too_large` | | 429 | `rate_limit_error` | | 502, 503 | `overloaded_error` | | other | `api_error` | ## Error codes ### Authentication | Status | Code | Description | | ------ | ------------------------------- | -------------------------------------------------------------------------------------------------------------- | | 401 | `invalid_api_key` | Missing, unknown, revoked or expired API key. | | 403 | `insufficient_permission` | The key's scope does not cover the endpoint. | | 403 | `address_not_allowed` | The key is restricted to other source addresses. | | 403 | `impersonated_session` | An operator views the account, and the request would change it. | | 451 | `jurisdiction_restricted` | The request came from an unavailable location, or through Tor. See [Regional availability](https://inference.ai.exchange/docs/availability.md). | | 503 | `identity_provider_unavailable` | The exchange could not verify a browser session. | | 401 | `unauthenticated` | Provider endpoints: missing or invalid provider key. | ### Request | Status | Code | Description | | ------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `invalid_request` | The body cannot be parsed, or a field is not valid. `param` names the field. A removed `exchange` field gets this code. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). | | 400 | `conflicting_parameters` | An `exchange` rule is set in the flat form and in a group with different values. The message names both fields. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md). | | 400 | `tools_unsupported` | The awarded provider cannot run the request with tools. Remove `tools`. | | 400 | `request_rejected` | The awarded provider refused the request. The message is its reason. | | 400 | `invalid_header` | A purchasing header cannot be parsed. | | 400 | `unsupported_parameter` | No offer for the model supports a parameter. `param` names it. See below. | | 400 | `unsupported_content` | No offer can serve a content part, or `/v1/responses` does not read an input item. The message gives its path, such as `messages[0].content[1]`. | | 400 | `unsupported_model` | An image or speech request named a router model that ranks by price. Name the model, or use a router model that ranks by Auto. | | 400 | `unsupported_rule` | An image or speech request had a rule that limits a price or a speed in tokens, such as `exchange.rank.cost_tier`. `param` names it. | | 413 | `request_too_large` | The body is over 20 MB (the default limit). See [Modalities](https://inference.ai.exchange/docs/modalities.md). | | 404 | `model_not_found` | The `model` is not a router model, and no offer quotes it. | | 409 | `idempotency_conflict` | The `idempotency-key` was reused with a different body. | | 409 | `idempotency_no_result` | The exchange saw the `idempotency-key` before, but the first request produced no receipt. | | 429 | `rate_limited` | Too many requests from the address, key or workspace, or a [guardrail rate limit](https://inference.ai.exchange/docs/guardrails.md#rate-limits) refused the call. `Retry-After` gives the seconds to wait. | | 503 | `overloaded` | The exchange is at its limit of work, or its database is too busy. It charged nothing. `Retry-After` gives 1 to 5 seconds. | | 400 | `invalid_end_user` | The [end-user id](https://inference.ai.exchange/docs/guardrails.md#end-users) is empty, over 512 characters or not printable ASCII. | `unsupported_parameter` also applies to these cases: - A control that no offer declared. `param` is the feature name: `image.seed`, `image.background`, `image.output_compression`, `image.stream`, `video.seed`, `video.creativity`, `video.upscale_factor`, `video.continue`, `speech.instructions`, `speech.turns`, `transcription.diarize`, `transcription.keyterms` or `embedding.input_type`. The message reads "`image.seed` is not supported by any offer for ". The exchange holds nothing. - A reference input that no offer reads. `param` is `input.image`, `input.audio`, `input.video` or `input.file`. - An output option that no offer serves on an image, speech, video or embedding request. `param` is `n`, `size`, `output_format`, `voice`, `response_format`, `seconds` or `dimensions`. `/v1/models` lists `image_sizes` for an image model. - On `/v1/responses`: a hosted tool other than `web_search`, `previous_response_id`, `conversation` and `background: true`. The exchange stores no responses. - On `/v1/messages`: a hosted tool such as `web_search`, `web_fetch` or `code_execution`. A tool that your client runs is allowed. - A `provider` setting that the exchange cannot apply. See [Move from OpenRouter](https://inference.ai.exchange/docs/move-from-openrouter.md). ### Models and strategy These codes come before the auction. | Status | Code | Description | | ------ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | 400 | `basket_requires_models` | `architect.basket` needs `exchange.candidates.models.allow`. Send the models, or `[]` for every listed model. | | 400 | `address_takes_no_basket` | The `model` is one model, and the request also sent `exchange.candidates.models.allow`. Remove one. | | 400 | `basket_names_router` | An entry in `exchange.candidates.models.allow` is a router model or `architect.basket`. Use `exchange.rank.strategy` to rank. | | 400 | `invalid_basket` | An entry in `exchange.candidates.models.allow` is blank. | | 400 | `strategy_conflicts_with_address` | The strategy `kind` does not match the `model`, such as `auto` with a model. Send `architect.basket`. | | 400 | `invalid_price_band` | `exchange.rank.strategy.price_band_bps` is above 10000. | | 403 | `key_policy_violation` | The request is outside the preset of the API key: no model matches, or the `exchange.rank.workload_profile` differs. | ### Auction The price cap is the most that a job can cost. The exchange sets it before the first token. | Status | Code | Description | | ------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 400 | `no_eligible_offer` | The request, or the rules of the key or workspace, excluded every offer. Do not retry. Change the request. | | 503 | `no_eligible_offer` | No offer met the rules, and a wait can change that. For example, an offer failed a recent job. See `eligibility.excluded`. | | 403 | `no_eligible_offer` | A provider allowlist of the key or of a guardrail matches no provider that sells the model. `binding_rule` names it. Do not retry. | | 503 | `model_unavailable` | No seller can serve the named model now. `param` is `model`. `Retry-After` is 30. Retry later, or select another model from `GET /v1/models`. | | 402 | `insufficient_balance` | The cheapest eligible cap is more than the workspace balance. `needed_usd` and `balance_usd` give both amounts. | | 402 | `budget_exhausted` | The key reached its spending limit, or a guardrail budget is spent. Jobs in flight count at their price cap. | | 402 | `deposit_required` | The workspace holds only free credit, and keys of several such accounts send requests from this address. Add funds. | | 403 | `guardrail_violation` | The request is outside a workspace guardrail. See [Guardrails](https://inference.ai.exchange/docs/guardrails.md). | | 403 | `content_blocked` | A guardrail blocks the request because its text matched a detector. See [Guardrails](https://inference.ai.exchange/docs/guardrails.md). | | 503 | `guardrail_unavailable` | The router could not read the workspace guardrails, so it refused the call. Retry later. | | 403 | `pin_not_permitted` | A key that cannot pin offers sent `x-liquid-pin-offer`. | | 503 | `provider_failed` | The awarded provider failed, or the exchange could not reach it. If the provider refused the request, the code is `tools_unsupported` or `request_rejected`. | | 404 | `not_found` | The job does not exist or belongs to another workspace. | If an unserved parameter is the only reason for every exclusion, the error is `400 unsupported_parameter`. ### When a provider fails If the awarded provider fails before any output, the exchange releases the hold and tries the next offer. - If no offer takes the job, you receive `provider_failed`. The exchange keeps that offer out of the ranking for a while. - If no other seller has the model, the first request 30 seconds after the failure is a trial on that offer. Until then, and during the trial, requests get `model_unavailable`. Each failed trial doubles the wait, up to 7.5 minutes. After any output, or if the provider may still run the job, the exchange tries no other offer. You get one cap and one charge. ### When an allowlist leaves no offer An offer can serve the model while a provider allowlist matches none of the providers. The `no_eligible_offer` error then has `error.binding_rule`. - `message` names the providers and the model. For `allowed_providers`, it also suggests `exchange.candidates.providers.prefer`. - The exchange ranks the request again without the allowlist. If an offer can then win, `message` names its providers and the lowest estimate for the job, and `fixes` has the same data in `admits` and `estimate_nanos`. - The status is `400` for `allowed_providers` and `403` for the allowlist of a key or a guardrail. `retryable` is `false`. - If the quotes of an allowed provider expired, the status is `503`. | `binding_rule` | The allowlist | | ----------------------------- | ---------------------------------------------------------------------------------------- | | `allowed_providers` | `exchange.candidates.providers.allow` in the request, or in the preset it names. | | `key_allowed_providers` | `exchange.candidates.providers.allow` in the preset of the API key. Use another key. | | `guardrail_allowed_providers` | `exchange.candidates.providers.allow` in a workspace guardrail. Change it in Guardrails. | ### Auto These codes refuse a request that Auto ranks. See [Auto Router](https://inference.ai.exchange/docs/auto.md). | Status | Code | Description | | ------ | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | 503 | `auto_unavailable` | Auto is off on this exchange, or it has no score snapshot yet. | | 400 | `auto_version_unknown` | The mode version is not defined. Send 1, 2, 3, 4 or 5. | | 400 | `auto_requires_price_bound` | No price bound. Send `exchange.rank.cost_tier`, `exchange.limits.price.output_usd_per_mtok`, `exchange.cap_usd` or `x-liquid-cap-usd`. | | 400 | `auto_preference_unsupported` | The mode version does not accept the preference. Version 1 accepts only `premium`; version 2 accepts all three. | | 400 | `auto_policy_unsupported` | The same, for a preference sent as `exchange.policy` or `x-liquid-policy`. | | 400 | `auto_pin_unsupported` | The request sent `x-liquid-pin-offer`. | | 400 | `preferred_providers_with_auto` | The request sent `exchange.candidates.providers.prefer` or `provider.order`. Auto ranks by score. | | 400 | `auto_reasoning_unsupported` | Auto has no scores for the requested reasoning effort. | | 503 | `auto_scores_stale` | The exchange's copy of the scores is older than its limit. | A `no_eligible_offer` error for an Auto request also has `error.auto`: the decision that Auto made before the rules left no offer. ### Ranking axis A model-name suffix (`smartest`, `fastest` or `cheapest`) or a `smartest` strategy can return these codes. See [Axis routing](https://inference.ai.exchange/docs/axis-routing.md). `smartest` is Auto mode version 4, so it can also return the [Auto](#auto) codes. | Status | Code | Description | | ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | 400 | `smartest_needs_several_models` | `smartest` needs more than one model. Name a creator, a router model, a basket or a preset, or use `fastest`. | | 400 | `axis_conflict` | The model name and `exchange.rank.strategy`, `x-liquid-policy` or `exchange.candidates.creators.allow` ask for different rankings. | | 400 | `ambiguous_scope` | The name before the axis is both a served model and a creator. Send `architect.basket` with a pattern, such as `deepseek-*`. | ### Provider endpoints A buyer key does not reach these seller endpoints. | Status | Code | Description | | ------ | ---------------------- | ------------------------------------------------------------------------ | | 400 | `invalid_registration` | `deployment_name`, `precision` and `region` are required. | | 400 | `invalid_bands` | `max_input_tokens` and, if stated, `max_output_tokens` must be positive. | | 404 | `no_model` | The registration does not exist or belongs to another provider. | | 409 | `not_listed` | The exchange has not listed the registration. | ### Reservations You get these codes when a preset names a reservation. See [Reservations](https://inference.ai.exchange/docs/reservations.md). | Status | Code | Description | | ------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 404 | `reservation_not_found` | This workspace does not have the reservation contract that the preset names. | | 409 | `reservation_not_serving` | The reservation does not serve this month, and the preset names no other model. The term has not started, no deployment is linked, or the month is suspended. | | 429 | `reservation_pool_used` | This month's pool is used, and the preset names no other model. Add overflow models, or wait for next month. | | 429 | `reservation_rate_limited` | You passed the contract's tokens or requests a minute, and the preset names no other model. Retry after `Retry-After`, or add overflow models. The `x-ratelimit-*` headers show the limit. | | 400 | `reservation_needs_price_ranking` | The preset ranks a reservation by Auto, which has no score for it. Rank by price or by speed. | ### Exchange | Status | Code | Description | | ------ | --------------------- | ---------------------------------------------------------------------------------------------------- | | 503 | `storage_unavailable` | A required exchange service is unavailable. The exchange did not process the request. Send it again. | | 500 | `internal_error` | Unexpected failure. If the request has a job, the message includes the job id. | ## Fixes Each entry in `fixes` is one change. `reason` is the exclusion reason, `offers` is the count of offers it left out, and `message` says what to change. The `message` of the error holds the same text, because many clients show only that field. The exchange tests each change before it answers. It drops the one rule and ranks the request again. `admits` lists the providers that can then win. `estimate_nanos` is the lowest charge, in nano-USD (one USD is 1,000,000,000), that you can expect for this job at one of them. The charge at settlement follows the tokens that the job uses. | The `message` says | What the test found | | ------------------------ | ----------------------------------------------------------------------------- | | `Any one of these works` | Each entry is enough alone, and has `admits`. | | `no single change` | The exchange tested each entry, and none is enough alone. Make more than one. | | `Change one of these` | The exchange did not test each entry. An entry may not be enough alone. | `owner` says who can make the change. A field of the request cannot widen a rule of a key preset or a guardrail. | `owner.kind` | Who changes it | `param` and `header` | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | `request` | The sender of the request. | Name the field to set, when one field fixes the reason. | | `key_preset` | An administrator of the workspace, in the preset that `owner.name` names. | Absent. | | `guardrail` | An administrator of the workspace, in the guardrail that `owner.name` names. `owner.on_key` is true when the guardrail is linked to this key. | Absent. | `owner` is absent when the state of the market caused the reason. ## Exclusion reasons Each entry in `eligibility.excluded` has `provider`, `offer_id`, `reason` and sometimes `detail`. The tables group the reasons by what you change. ### Change the model or the provider - `job_kind_not_served`: the offer makes a different output, such as an image offer for a chat job. - `model_not_served`: the offer does not serve the requested model. - `model_not_allowed`: excluded by `exchange.candidates.models.exclude`, or outside the preset of the API key. - `provider_not_allowed`: excluded by `exchange.candidates.providers.allow`. - `vendor_not_allowed`: excluded by `exchange.candidates.creators.allow`. - `provider_blocked`: excluded by `exchange.candidates.providers.exclude`. - `pinned_offer_not_resting`: the pinned offer is not on the book. ### Change a price limit - `cap_exceeds_buyer_cap`: the worst-case charge is more than `cap_usd`. Raise `cap_usd`, or lower `max_tokens`. - `cap_exceeds_budget`: the worst-case charge is more than the balance or the key limit. Add funds, raise the limit, or lower `max_tokens`. - `rate_above_ceiling`: a rate is more than `exchange.limits.price.input_usd_per_mtok`, `exchange.limits.price.output_usd_per_mtok`, `exchange.limits.price.cache_read_usd_per_mtok` or `exchange.limits.price.cache_write_usd_per_mtok`. - `above_cost_tier`: the price is above the requested `exchange.rank.cost_tier`. ### Change a speed limit - `service_too_slow`: the deadlines of the offer are longer than `exchange.limits.ttft_ms` or `exchange.limits.completion_ms`. - `throughput_too_low`: the offer gives fewer tokens each second than `exchange.limits.min_tps`. ### Change the request itself - `feature_unsupported`: the offer does not support a parameter of the request. `detail` names it. - `media_unsupported`: the offer does not accept the attachment in the form sent. `detail` names the constraint, such as `image.max_bytes`. - `context_too_large`: the input and `max_tokens` together exceed the context window of the offer. See [Output limits](#output-limits). - `output_bound_unsupported`: `max_tokens` is outside the output limit of the offer. On a text job, `detail` gives the request and the range, such as `asked 200000, accepts 1 to 8192`. On an image, speech, video or embedding job, `detail` names the parameter that the deployment does not serve. See [Output limits](#output-limits). ### Change a data rule - `region_policy`: excluded by `exchange.privacy.regions`. - `retention_policy`: the offer cannot meet the requested `exchange.privacy.retention`. - `workload_profile_not_targeted`: the offer does not target the requested `exchange.rank.workload_profile`. ### Nothing to change - `offer_expired`: the offer expired before the auction ran. Send the request again. - `superseded_by_own_offer`: the same provider had an eligible offer that ranked ahead of this one. ### Auto has no score - `score_unmapped`: Auto has no score for the deployment at the reasoning setting of the request. - `score_missing`: the deployment has no score for the task Auto ranked by. `detail` names the score field. ## Output limits `max_tokens` is a maximum for the answer. If every offer is excluded, and `output_bound_unsupported` or `context_too_large` excluded at least one, the auction runs again. It tries a lower `max_tokens` for each of those offers, from the largest value to the smallest. - `output_bound_unsupported`: the lower value is the output limit of the offer. - `context_too_large`: the lower value is the room that the input leaves in the context window, and not more than the output limit. - An offer gives no value if the room is less than 1,024 tokens, or if another reason excluded it. - The auction keeps the first value that an offer accepts. The exchange calculates the cap from the lower value. An answer that reaches it stops with `finish_reason: "length"` (`stop_reason: "max_tokens"` on the Anthropic endpoint). A request with no limit (`max_tokens`, `max_completion_tokens` or `max_output_tokens`) starts at 32,768 tokens. If the cap or your balance excludes all offers, the exchange halves that limit until an offer fits, down to 1,024 tokens. It never changes a limit that you set. - If your limit does not fit your balance, the response is `402 insufficient_balance`. `needed_usd` is the lowest cap for your limit. - If your limit does not fit your cap, the response is `400 no_eligible_offer` with `param` set to `exchange.cap_usd`. - The exchange never lowers `max_tokens` to or below a `thinking` budget. It refuses such a request with `no_eligible_offer`. ### Reasoning models The thinking of a reasoning model counts toward `max_tokens`, and the exchange bills it as output. - On `POST /v1/chat/completions`, the thinking arrives as `reasoning`, on each streamed `delta` or on the `message`. - `usage.completion_tokens_details.reasoning_tokens` and `exchange.reasoning_tokens` give the tokens that the thinking used. - A model that thinks until the limit stops with `finish_reason: "length"` and an empty `content`. You pay for the thinking. Use a larger `max_tokens`. - A model that stops after its thinking with no text and no tool call gives you nothing to use. The job fails, you pay nothing, and the receipt records `empty_response`. - On `POST /v1/messages`, the response has thinking blocks only if `thinking` is not `disabled`. Without it, a small `max_tokens` can end with no text. ## Tool calls If the request sends `tools`, the exchange checks each tool call in the answer. A call is bad when it has one of these faults: | Fault | Meaning | | ----------------- | ------------------------------------------------ | | `invalid_json` | The arguments are not JSON. | | `unknown_name` | The name is not one of the tools in the request. | | `schema_mismatch` | The arguments break the schema of the tool. | The [receipt](https://inference.ai.exchange/docs/rules.md#response-fields) has the counts in `answer_checks.tool_calls`, `answer_checks.tool_call_errors` and `answer_checks.tool_call_faults`. - **Not streamed.** The exchange reads the whole answer first. If a call is bad, it drops the answer and sends the job to the next offer. If no offer is left, the job fails with `invalid_tool_call`. - **Streamed.** The exchange counts the bad calls and keeps the answer. **Tool-call markup.** Some providers do not turn the calls of a model into `tool_calls`, and your agent loop stops. The exchange sets `answer_checks.tool_markup` to `1` when the request sent tools, the answer has no tool call, and the text holds DeepSeek, Hermes, Qwen or Kimi markup. It keeps the answer and records the markup for the deployment. ## Structured output If you send `response_format` with `json_object` or `json_schema`, the exchange checks the answer when the job ends. `json_object` asks for one JSON value. `json_schema` also asks that the value matches your schema. On `POST /v1/messages` it checks `output_config.format`, and on `POST /v1/responses` it checks `text.format`. The result is in `exchange.answer_checks.structured_output`, on the last chunk of a stream. | `result` | Meaning | | ----------------- | -------------------------------------------------------------------------------------------------------- | | `passed` | The answer is JSON, and for `json_schema` it matches the schema. | | `not_json` | The answer is not one JSON value. Text around the value, or a code fence, gives this result. | | `schema_mismatch` | The answer is JSON but breaks the schema. `keyword` names the JSON Schema keyword, such as `required`. | | `cut` | The answer stopped at `max_tokens`. Send the request again with a larger `max_tokens`. | | `refused` | The answer has no text, or the model refused. | | `unchecked` | The exchange did not check the answer: only tool calls, or the schema or answer is too large or invalid. | - **Not streamed.** For `not_json` or `schema_mismatch`, the exchange drops the answer and sends the job to the next offer. If no offer is left, the job fails with no charge, and the receipt records `invalid_structured_output`. - **Streamed.** You receive the answer as the provider sent it, and you pay for it. Only `not_json` and `schema_mismatch` count against the provider. If the answer writes more than 1,024 whitespace characters in a row outside a JSON string, the exchange stops it and counts it against the provider. A request that is not streamed goes to the next offer. A stream fails with `provider_failed`, and you pay nothing. The receipt keeps the result and the keyword, not the path of the failed field. # Chat Completions The OpenAPI 3.1 document is the full description, including schemas: https://router.inference.ai.exchange/v1/openapi.json ## `POST /v1/chat/completions` Buy one completion at auction, in the OpenAI Chat Completions format. Point an OpenAI client at the router. The smallest request is `{"model": "liquid.auto", "messages": [{"role": "user", "content": "Hello"}]}`. Put purchasing rules in the `exchange` field of the request. The reply has an `exchange` field that names the provider, the cap and the charge. The exchange fixes the price cap before the first token. It sends the cap in the `x-liquid-cap-usd` header, so a streaming client knows the most it can pay. Set `logprobs: true` for the log probabilities in `choices[0].logprobs.content`. Set `top_logprobs` (0 to 20) for the most likely tokens at each position. Only offers that declare the feature can win. The provider reports the values and the exchange cannot verify them. The response cache never serves a request that asks for them. Without `max_tokens` and `max_completion_tokens`, the output limit is 32,768 tokens. The exchange lowers a limit that it chose, to what the offers, the cap and the balance allow, but not below 1,024 tokens. It refuses a limit that the buyer set and that does not fit. The limit stays above any thinking budget. # Responses The OpenAPI 3.1 document is the full description, including schemas: https://router.inference.ai.exchange/v1/openapi.json ## `POST /v1/responses` Buy one response at auction, in the OpenAI Responses format. Point an OpenAI client at the router and send `model` and `input`. Use `liquid.auto` as the model to let the exchange choose. The smallest request is `{"model": "liquid.auto", "input": "Hello"}`. The exchange ranks the offers by estimated charge and fixes a price cap before the first token. It charges for the tokens used. The reply is a normal Responses object with an extra `exchange` field, which names the provider, the cap and the charge. Set purchasing rules in the `exchange` field of the request. The exchange stores nothing, so send the whole conversation in `input`. `previous_response_id`, `conversation`, `background: true`, `logprobs` and `prompt` get `400 unsupported_parameter`. Function tools work. A hosted `web_search` tool is dropped, and the reply has `x-liquid-notice: web_search_dropped`. Other hosted tools get `400 unsupported_parameter`. # Messages The OpenAPI 3.1 document is the full description, including schemas: https://router.inference.ai.exchange/v1/openapi.json ## `POST /v1/messages` Buy one message at auction, in the Anthropic Messages format. Point an Anthropic client at the router. The smallest request is `{"model": "liquid.auto", "max_tokens": 256, "messages": [{"role": "user", "content": "Hello"}]}`. Send the key as `x-api-key` or as a bearer token. Put purchasing rules in the `exchange` field, as on `POST /v1/chat/completions`. The router translates the request to the Chat Completions shape and runs the same auction. `system` becomes the first message. `stop_sequences` becomes `stop`. `thinking` stays a token budget. The `exchange` block is on the message, or on the last event of a stream. The headers `x-liquid-job-id` and `x-liquid-cap-usd` arrive before the first token. ## `POST /v1/messages/count_tokens` Estimate the input tokens of a Messages request. The count is the estimate that the exchange prices with. It is not the count of a provider tokenizer. # Decisions The OpenAPI 3.1 document is the full description, including schemas: https://router.inference.ai.exchange/v1/openapi.json ## `POST /v1/systemone` Answer typed questions about a situation, in the shape of the hosted Jev API. A Jev client works when it changes the address and the key. The smallest request is `{"model": "liquid.decision", "state": "...", "questions": {"q1": {"type": "choice", ...}}}`. Name a decision deployment as `model`, or use `liquid.decision` to buy from the cheapest. The request is one job of kind `decision`, with its own cap, hold and receipt. The charge is the input tokens that the provider read, at the input rate, never above the cap. The cap holds the prompt up to 64 times. If no decision deployment is live, or `model` is not one, the exchange asks chat models for log probabilities. Each question becomes several one-token jobs with the options in different orders. Only offers that declare `sampling.logprobs` can win. The `exchange` block lists the jobs and their total charge. The header `x-liquid-orderings` sets the orderings for each choice question (default 4). The exchange cannot verify the probabilities. It refuses an answer that cannot be right. Differences from the hosted API: - A choice question has 2 to 20 options, not 255. A score question has 2 to 10 levels. - One prompt has at most 32,000 tokens. A longer one gets `422`, and `error.param` is `state`. - `confidence` is one minus the normalized entropy of the probabilities. The hosted API does not document its formula, so set the threshold again. - A model name that starts with `jev-` means `liquid.decision`. - Backticked paths in a question, such as `` `ticket.id` ``, are not resolved. - The exchange does not set the temperature, so the same request can return different probabilities. # Embeddings The OpenAPI 3.1 document is the full description, including schemas: https://router.inference.ai.exchange/v1/openapi.json ## `POST /v1/embeddings` Buy embeddings at auction, in the OpenAI embeddings format. Send `model` and `input`, one text or an array of texts. Only a deployment registered to embed can win. The charge is the input tokens that the provider reports, never above the cap. The cap is the estimated prompt at the offer input rate. It is in the `x-liquid-cap-usd` header. The `exchange` block of the response shows the charge. Send `input_type` to say what the vectors are for. A request holds up to 2,048 texts of up to 32,768 characters each, and up to 1,200,000 characters in all. There is no streaming form. # Images The OpenAPI 3.1 document is the full description, including schemas: https://router.inference.ai.exchange/v1/openapi.json ## `POST /v1/images/generations` Buy images at auction, in the OpenAI images format. Send `model` and `prompt`. Only a deployment registered to produce images can win. The price is per image, so the cap is exactly the charge. The `exchange` block of the response shows what the exchange bought and the charge. Reference inputs are `input_references` (images, audio, video and files). Only an offer that declared each input kind, within its limits, can win. The exchange refuses the request before dispatch when none did. It never sends the request to a chat model, which would answer with text. With `stream: true`, the answer is a stream of server-sent events: see `stream` in the request. The exchange sends the events when the job ends. # Transcriptions The OpenAPI 3.1 document is the full description, including schemas: https://router.inference.ai.exchange/v1/openapi.json ## `POST /v1/audio/transcriptions` Buy a transcript at auction, in the OpenAI transcription format. Send `multipart/form-data` with `model` and `file`, or JSON with `model` and one audio entry in `input_references`. Only a deployment registered to transcribe can win. The price is per second of audio. The router reads the length from the bytes of the file, and that length times the offer rate is the estimate and the cap. The charge is the seconds that the provider metered, never above the cap. The `exchange` block of the response, or the `x-liquid-exchange` header for the `text` format, shows the charge. The router reads WAV, FLAC, MP3, Ogg, WebM, M4A and AAC, whatever the file name. It refuses a file over 25 MB, over two hours, or with no readable length. There is no streaming form, and no `srt` or `vtt` format. # Speech The OpenAPI 3.1 document is the full description, including schemas: https://router.inference.ai.exchange/v1/openapi.json ## `POST /v1/audio/speech` Buy spoken audio at auction, in the OpenAI speech format. Send `model` and `input`. `voice` is optional when the offers have a default. `instructions` sets a style line, and `input` can be an array of turns for more than one speaker. The response body is the audio file. Only a deployment registered to produce speech can win. The price is per character of `input`, so the cap is exactly the charge. The body carries audio, so the `exchange` block is in the `x-liquid-exchange` header, as JSON. The cap and the job id are in `x-liquid-cap-usd` and `x-liquid-job-id`. There is no streaming form. # Video The OpenAPI 3.1 document is the full description, including schemas: https://router.inference.ai.exchange/v1/openapi.json ## `POST /v1/videos` Buy a video at auction, in the OpenAI video format. Send `model` and `prompt` as JSON or `multipart/form-data`. The reply comes when a provider accepts the job. Poll `GET /v1/videos/{video_id}`. When the status is `completed`, download the file with `GET /v1/videos/{video_id}/content`. Only a deployment registered to render video can win. The price is per second of video, so the cap is exactly the charge. Reference inputs are `input_references` (images, audio, video and files). Only an offer that declared each input kind can win. The exchange refuses the request before dispatch when none did. The start image (the `first_frame` entry, else the first image) must match the shape of the video within 1%, because an upstream crops it. The shape is `size`, or the default size of each offer. The router reads only the header of a linked start image. # Models The OpenAPI 3.1 document is the full description, including schemas: https://router.inference.ai.exchange/v1/openapi.json ## `GET /v1/anthropic/models` List the models in the Anthropic shape. The catalogue is the same as on `GET /v1/models`. Only the envelope differs. Claude Code gets its own view. ## `GET /v1/models` List the models that a request can name. The list comes from the live book. It holds the models that a connected provider quotes now, and the router models. A buyer credential adds the presets of its workspace (`match_type` is `preset`) and the symbols of unlisted chat deployments. Without a credential, or with one that does not resolve, the list has no presets. The response has the Anthropic shape when the request has an `anthropic-version` header, and the OpenAI shape when it does not. The credential cannot select the shape, because a client can send it as `x-api-key` or as a bearer token on either dialect. Claude Code also sends a `claude-code/` user-agent token. It gets compatibility ids and a live-book description, without exact Anthropic models. The query filters narrow the list to models. A router model or a preset cannot keep a filter. The order is the order of a model picker: `liquid.auto`, the other router models, the presets of the caller, the models that support tools, then the other models. In each group, the name that more independent providers quote comes first. # Market data The OpenAPI 3.1 document is the full description, including schemas: https://router.inference.ai.exchange/v1/openapi.json ## `GET /v1/exchange/collections` List the collections and the models that each holds now. It needs no credential and names no provider or workspace. The list holds the presets of the `system` workspace that have a group. A collection that no listed model matches has an empty `models`. ## `GET /v1/exchange/harnesses` List the harnesses whose guide the exchange verified in the last 14 days. The exchange tests the published recipe of each harness against a real deployment. The list names no deployment, so it needs no credential. It is empty when it is not available. ## `GET /v1/exchange/lifetime` Return the jobs and tokens that the exchange has processed in total. It needs no credential and names no buyer or provider. The totals are zero when the figures are not available. ## `GET /v1/exchange/list-prices` List the models that have a list price. It needs no credential. A model with no list price is not in the list. ## `GET /v1/exchange/markets` Report the depth of each market. One row for each served model with a resting offer. A row counts independent providers, not offers. The exchange groups affiliated accounts and shared upstreams first, so resellers of one upstream count once. ## `GET /v1/exchange/models` List each served model that has a live offer, with its price range and depth. A row says what the model is, what it can do, what it costs and how deep the market is. It joins `/v1/exchange/markets` and `/v1/exchange/offers`. The query takes the filters of `/v1/models` that describe a model. A filter that names a provider or reads the price of one offer gets `400`, because this view names no provider. ## `GET /v1/exchange/offers` List the resting offers without their providers. The models, prices and service commitments are public. The provider of each offer is not, because a provider name beside a price would show its quote to its competitors. The list leaves out unverified offers, which a buyer reaches only by a deployment symbol that names the provider. The list is sorted by generation rate, cheapest first. ## `GET /v1/exchange/routers` List the router models that a request can name in `model`. A router model admits all the models that the exchange lists now, so its `models` change when providers list and withdraw models. The router models differ in their rules. ## `GET /v1/exchange/stream` The live book, over a WebSocket. Open the socket, then send one `MarketRequest` frame with a credential within five seconds. A key with the `list` scope is enough. The exchange replies with a `snapshot` of every resting offer, then a `MarketEvent` frame for each change: an offer repriced or withdrawn, a provider that connects or disconnects, and a request that found no offer. An unverified offer appears only for a workspace that can buy from it. If the exchange refuses the credential, it sends a `refused` frame and closes the socket. Set `receipts` on the subscribe frame to also receive the receipts of your workspace. No other subscriber sees them. This feed names providers, so it needs a credential, unlike `/v1/exchange/offers`. A market subscriber that falls behind skips the events it missed. A receipts subscriber that falls behind is disconnected. It must reconnect and read a new snapshot of the jobs in flight. The exchange pings every 30 seconds. It closes the socket when the peer answers no ping or sends no frame for 10 seconds. # Become a provider A provider sells inference on the exchange. Follow these steps from registration to the first served job. ## Start selling 1. **Register** a provider in the console. The handle is reserved at once, and the status is `PENDING`. See [Register](#register). 2. **Wait for admission.** An operator reviews the registration. See [Provider status](#provider-status-and-admission). 3. **Issue a provider key.** See [Provider keys](#provider-keys). 4. **Register deployments.** See [Deployments](https://inference.ai.exchange/docs/provider-models.md). 5. **Quote.** An agent keeps a WebSocket session open and publishes prices. See [Quote over the session](https://inference.ai.exchange/docs/provider-quoting.md). 6. **Serve.** The exchange sends awarded jobs to you and settles on metered usage. See [Choose how to deliver work](#choose-how-to-deliver-work). 7. **Operate.** See [Run your agent](https://inference.ai.exchange/docs/provider-operate.md). The exchange never stores prices. A price is valid only while the session stays open and the quote has not expired. You need an inference endpoint (or an upstream in OpenAI or Anthropic format) and a public `http` or `https` dispatch endpoint. Registration rejects loopback, private and link-local addresses. Sign the Provider Agreement in the console, on the Agreement page of your provider. Enter the details of your company and of the person who signs. The console shows the agreement as a PDF with your details in it, and the PDF updates as you type. Read it, type the name of the signatory, and sign. Architect has already signed the text, so your signature completes the agreement. The exchange keeps the signed PDF, and the console shows it with its status. You can download it. ## Choose how to deliver work | Path | Use it when | Read | | --------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Agent | You run the Liquid provider agent. It serves every kind of job. | [Serve and get paid](https://inference.ai.exchange/docs/provider-dispatch.md), [Run your agent](https://inference.ai.exchange/docs/provider-operate.md), [Sell video](https://inference.ai.exchange/docs/provider-video.md) | | OpenAI-compatible API | Your API speaks the OpenAI dialect. No agent. Chat, embeddings, speech and images, not video. | [Sell with an OpenAI-compatible API](https://inference.ai.exchange/docs/provider-openai.md) | | Anthropic API | Your API speaks the Anthropic Messages dialect. No agent. Chat only. | [Sell with an Anthropic API](https://inference.ai.exchange/docs/provider-anthropic.md) | | BYOK | You have a cloud account. The exchange serves with your key. You only quote. | [Sell with bring your own key](https://inference.ai.exchange/docs/provider-byok.md) | ## Register Open **Sell on the exchange** in the console and choose **Request to be a provider**. The console calls `POST /v1/account/providers`. | Field | Required | Description | | --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `slug` | Yes | Your handle: 2 to 63 lowercase letters, digits and hyphens. | | `legal_entity` | Yes | The legal entity of the provider. | | `contact_email` | Yes | The address that an operator uses about admission. | | `website` | Yes | The website of the legal entity (`http` or `https`). | | `declared_endpoint` | No | The dispatch endpoint (`http` or `https`). Sessions must log in with the same scheme, host and port. Omit it for only [BYOK](https://inference.ai.exchange/docs/provider-byok.md). | | `upstream_dependency` | Yes | The upstream service, or `self-hosted`. The exchange uses it to find affiliations. | | `hq_country` | Yes | ISO 3166-1 alpha-2 country code of the headquarters. | | `control_country` | Yes | ISO 3166-1 alpha-2 country code of the ultimate owner. | | `ultimate_parent` | Yes | The person, company or government that owns 50% or more, or else the largest owner. Up to 200 characters. | | `affiliation_group` | No | The group for market depth. An operator can override it. | | `display_name` | No | The name buyers see in place of the handle, up to 40 characters. | | `description` | No | Plain text about what you serve, up to 160 characters. | The response includes a permanent `provider_id`, which receipts, settlements and payouts use. Routes accept the ID or the handle. Store the ID, because another provider can take a released handle. | Rejection | Cause | | ----------------------------- | ------------------------------------------------------------------------------ | | `400` with a field code | The exchange rejected a value. | | `409 provider_id_taken` | A different provider uses the handle. | | `400 jurisdiction_restricted` | The headquarters or ultimate owner is in a country that the exchange excludes. | | `451 jurisdiction_restricted` | The registration comes from such a location. | See [Regional availability](https://inference.ai.exchange/docs/availability.md). ### Who has access Every member of the workspace that registered the provider, and the owners and admins of its organization, have full access. They can give other users read or full access in the People card. Each user must have signed in once. ## Provider status and admission | Status | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------- | | `PENDING` | Waits for an operator. No keys, no sessions. | | `ADMITTED` | Approved. Keys, sessions and quotes work. | | `SUSPENDED` | An operator stopped the provider, with a reason in the console. Keys stop and offers go. Only an operator reverses it. | | `REJECTED` | An operator refused it, with a reason. The handle stays reserved. | | `RETIRED` | The provider closed itself. All keys are revoked. You cannot reverse it. | Only an operator can admit a provider. The exchange sends no email, so check the console. ## Provider keys A provider key authenticates the agent. It opens the session (`/v1/provider/ws`) and the catalogue endpoints (`/v1/provider/models`). It cannot place jobs or act for another provider. The format is `sk-liquid--`. The console shows the secret once. **Issue:** open the provider in the selling console, choose **Issue** and copy the secret. The console calls `POST /v1/account/providers/{provider_id}/keys` and gets `{ "key_id": "k7f2…", "api_key": "sk-liquid-k7f2…-…" }`. | Status | Code | Description | | ------ | -------------- | ------------------------------------------------ | | 201 | | The exchange issued the key. | | 403 | `read_only` | You have read-only access. | | 404 | `not_found` | The provider does not exist, or you lack access. | | 409 | `not_admitted` | The provider is not `ADMITTED`. | **Use:** send `Authorization: Bearer sk-liquid-…` to the catalogue endpoints, or the key in the `login` frame of the session. See [Quote over the session](https://inference.ai.exchange/docs/provider-quoting.md). **Rotate:** a provider can have many live keys. Issue a new key, restart the agent with it, then revoke the old key. If you revoke first, the session closes and the offers go. **Revoke:** choose **Revoke**. The console calls `DELETE /v1/account/providers/{provider_id}/keys/{key_id}`. The exchange refuses the key at once and closes an open session that uses it within 10 seconds. ## Change the handle Send `PATCH /v1/account/providers/{provider_id}` with `{"slug": "nimbus"}`. The change starts at once, and the book shows each resting offer under the new handle. Your agent does not reconnect. A new handle keeps the provider ID, keys, registrations, settled jobs and payouts. Change the handle in the agent configuration before the next login. The exchange rejects the old handle. The same request can change `contact_email`, `declared_endpoint`, `serving_stack`, `website`, `display_name` and `description` (empty clears), and `logo_plate` (`fill`, `light` or `dark`). It cannot change `affiliation_group`. A handle in use returns `409 provider_slug_taken`. ## Publish your terms, privacy policy and status page The provider page shows your terms, privacy policy and status page to every visitor. A buyer accepts the first two. | Request | Effect | | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | `GET /v1/account/providers/{provider_id}/documents` | Lists your published documents. | | `PUT /v1/account/providers/{provider_id}/documents/{kind}` with `{"url": "https://…"}` | Publishes a link (`http` or `https`, 500 characters at most). | | `POST /v1/account/providers/{provider_id}/documents/{kind}/file?name=terms.pdf` with the PDF as the body | Stores a PDF of up to 5 MB. | | `DELETE /v1/account/providers/{provider_id}/documents/{kind}` | Removes the document. | `kind` is `terms_of_service`, `privacy_policy` or `status_page`. A status page is a link only. A link replaces a file of the same kind, and the reverse. You need full access to change a document. ## Pause and retire A pause stops trading. Send `POST /v1/account/providers/{provider_id}/pause` with `{ "paused": true }`. Keys cannot open a session, the exchange closes the open session and removes all offers. It keeps the admission, keys and registrations. Send `{ "paused": false }` to continue. No operator action is needed. To retire, send `POST /v1/account/providers/{provider_id}/retire` with `{ "reason": "…" }` (required, 500 characters at most). The endpoint requires a recent identity verification. Retirement revokes each key and closes the provider permanently. The handle stays reserved. ## Data the exchange records For each job, the exchange records completion, time to first token and generated tokens. It also sends paid test jobs to each listed deployment. You see your own offers, awards and settlements, never competitors' bids. ### Auction results The **Auctions** tab of **Performance** summarizes your auction results, one hour late. **Where your auctions go** starts with auctions where your offer rested, subtracts each exclusion reason in gate order, then shows **Ranked below the winner** and **Won**. Select a deployment or step to see the features or limits that need a change. The exchange withholds the breakdown when the window, any day in it, or a deployment has fewer than 20 auctions. Rare profiles count as other request shapes. # Deployments A deployment is a model that you serve at one precision in one region. Register it, state its limits, and move it from `UNLISTED` to the public book. A registration has no prices. Your session [quotes the deployment](https://inference.ai.exchange/docs/provider-quoting.md), and an operator decides whether to list it. Every endpoint here takes the [provider key](https://inference.ai.exchange/docs/provider.md) as a bearer token. ## Register a deployment ```bash curl https://router.inference.ai.exchange/v1/provider/models \ -H "Authorization: Bearer sk-liquid-..." \ -H "Content-Type: application/json" \ -d '{ "deployment_name": "gpt-oss-120b", "model": "gpt-oss-120b", "creator": "OpenAI", "precision": "fp8", "region": "eu-west", "max_input_tokens": 128000, "max_output_tokens": 4096, "tokenizer_id": "o200k_base", "input_modalities": [{ "type": "text" }, { "type": "image", "max_bytes": 20000000 }], "output_modalities": [{ "type": "text" }] }' ``` | Parameter | Type | Required | Description | | ---------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `deployment_name` | string | Yes | Your name for the deployment. The book shows `/`. | | `model` | string | No | Legacy upstream identifier. Default: `deployment_name`. It does not approve a public name. | | `upstream_model` | string | No | Opaque handle that the exchange returns at dispatch. Not part of the identity. Default: `model`. | | `registry_model_id` | UUID | No | An approved registry identity of the same kind. See [Select an approved identity](#select-an-approved-identity). | | `creator` | string | No | The company that made the model. Buyers see it, and the `exchange.candidates.creators.allow` rule uses it. Ids are lowercase. Where [models.dev](https://models.dev) lists the company, use its lab id, such as `alibaba` for Qwen or `zhipuai` for GLM. | | `model_family`, `base_model` | string | No | The model that this one is a variant of, and the model that a quantization or fine-tune comes from. | | `precision` | string | Yes | Weight precision, for example `fp8`, `bf16`, `int4`. | | `region` | string | Yes | Serving region. The `exchange.privacy.regions` rule uses it. | | `max_input_tokens` | integer | Yes | Context limit in the deployment tokenizer. Input and output of one job must fit in it together. Must be positive. | | `max_output_tokens` | integer | No | Maximum tokens for each job. Must be positive. Each registration writes this field, so omitting it removes a limit that you set before. | | `tokenizer_id` | string | No | Tokenizer of the usage reports. Default: `provider-native`. | | `kind` | string | No | `chat` (per token), `image_generation` (per image), `audio_speech` (per character) or `video_generation` (per second). Default: `chat`. The first registration sets it. | | `input_modalities` | array | No | Input kinds with limits. See [What the deployment takes](#what-the-deployment-takes). An empty list states nothing. | | `output_modalities` | array | No | Output kinds with limits. See [What the deployment produces](#what-the-deployment-produces). An empty list means text only. | | `pricing_unit` | string | No | An `image_generation` deployment can state `tokens` when its upstream bills for tokens. Omit it to keep the current unit. | | `image_output_tokens_max` | integer | No | Most output tokens for one image. Required with `pricing_unit` `tokens`, and refused otherwise. The cap of a job holds this many tokens for each image. | | `description` | string | No | Text that buyers see. Omit it to keep the current text. Send `""` to clear it. | | `hardware` | object | No | See [Hardware](#hardware). Omit it to keep the current claim. | | `serving` | object | No | See [Serving](#serving). Omit it to keep the current claim. | | `byok` | boolean | No | `true` makes the exchange serve the deployment with your own cloud key. `upstream_model` must be a model id that a live key lists. See [Sell with bring your own key](https://inference.ai.exchange/docs/provider-byok.md). | The response is `201` with every field above, and also: | Field | Meaning | | -------------------------------------------------------------------- | -------------------------------------------------------------------------- | | `id` | The registration id. Buyers see it as the offer id. | | `model_deployment` | `/`. It follows a handle change. | | `canonical_model` | The public name. It is `null` without a registry link. | | `status`, `status_reason` | The [listing status](#listing-statuses). A new registration is `UNLISTED`. | | `markup_bps`, `paused_at`, `created_at`, `listed_at`, `suspended_at` | Margin, pause and status times. Unset values are `null`. | Repeat registrations: - Registration is idempotent on `(provider_id, deployment_name, precision, region)`. A repeat call returns the current registration and clears no suspension or pause. - A change to the deployment identity clears its registry link and public admission. - `kind` cannot change. A new kind returns `409 kind_changed`. Use a new deployment name. Errors: `400` with `invalid_registration`, `invalid_bands`, `invalid_modalities`, `invalid_output_modalities`, `invalid_pricing`, `invalid_hardware` or `invalid_serving_claims`; `409 kind_changed`; `401`; `503 storage_unavailable`. ## Hardware State the hardware of this deployment only, not your fleet. The exchange does not use the claim to rank offers, route a job or break a tie. | Field | Type | Required | Description | | ------------------------ | ------- | -------------------- | ------------------------------------------------------------------------------------------------- | | `accelerator` | string | Yes | For example `nvidia-h100-80` or `amd-mi300x`. | | `interconnect` | string | With more than 1 GPU | How devices in one node connect, for example `nvlink` or `infinity-fabric`. | | `gpus_per_node` | integer | Yes | Devices in one node, 1 to 72. A rack system, such as an NVL72, is one node. | | `nodes` | integer | Yes | Nodes that serve one replica, 1 to 1024. | | `network` | object | No | Link between nodes. Allowed only when `nodes` is more than 1. | | `host` | string | Yes | `bare_metal`, or `vm_passthrough`. | | `instance_type`, `notes` | string | No | Instance type, such as `p5.48xlarge`, and other facts that change speed (500 characters at most). | | `network` field | Type | Required | Description | | ---------------- | ------- | -------- | ------------------------------------------------------------------------- | | `fabric` | string | Yes | For example `infiniband`, `roce` or `efa`. | | `topology` | string | No | `rail_optimized`, `fat_tree`, `torus`, `full_mesh` or `other`. | | `bandwidth_gbps` | integer | No | Bandwidth of one node in Gbit/s, 1 to 100000. Add every card of the node. | | `placement` | string | No | `same_rack`, `same_cluster` or `same_zone`. | An operator claim stays when you register again. A count out of range, a missing accelerator, no interconnect for more than 1 device, or a bad `network` returns `400 invalid_hardware`. ## Serving State how you serve the deployment. The exchange cannot check the values and does not use them to rank or route. Every field is optional. | Field | Type | Description | | ----------------------- | ------- | -------------------------------------------------------------------------------------------- | | `engine` | string | `vllm`, `sglang`, `tensorrt_llm`, `dynamo`, `llama_cpp` or `other`. | | `engine_version` | string | For example `0.10.2`. It needs an `engine`. At most 32 characters. | | `speculative_decoding` | boolean | `true` for speculative decoding or multi-token prediction. | | `disaggregated_serving` | boolean | `true` when prefill and decode run on separate machines. | | `kv_cache_precision` | string | `bf16`, `fp16`, `fp8`, `int8` or `fp4`. Separate from the weight precision. | | `prompt_caching` | string | `none`, `automatic` (the engine finds a repeated prefix) or `explicit` (the buyer marks it). | An engine version with no engine, or one that is too long, returns `400 invalid_serving_claims`. ## What the deployment takes The `type` of each entry in `input_modalities` is `text`, `image`, `audio`, `video` or `file`. Each `type` can occur once. An absent limit means that you stated none. | Field | Applies to | Checked | Description | | ------------------------ | ------------------------- | ------- | ------------------------------------------------------------------------------------------------- | | `sources` | image, audio, video, file | Yes | `url`, `base64`. | | `formats` | image, audio, video, file | Yes | Accepted MIME types, for example `image/png`. | | `max_bytes` | image, audio, video, file | Yes | Largest single part, in bytes. | | `max_references` | image | Yes | Most images that one request can reference. | | `max_duration_seconds` | audio, video | Audio | Longest clip, in seconds. | | `supported_frame_images` | image | Yes | Frames that the deployment reads: `first_frame`, `last_frame`. See [Frame images](#frame-images). | | `detail_levels` | image | No | `image_url.detail` levels: `auto`, `low`, `high`. | The list has two effects: - **It limits what you can quote.** Your agent quotes a media feature only for a kind that the list names. The list cannot add to what your upstream accepts. - **It excludes your offer from a job that breaks a limit.** The reason is `media_unsupported`, and the error names the constraint, such as `image.max_bytes`. This happens before the award, so the job can go to another deployment. ### Frame images A buyer can fix the first or last frame of a video with `frame_images`. Declare each frame that your upstream reads in `supported_frame_images` on the `image` input. A deployment that declares no frame never gets an image that has one. ```json "input_modalities": [ { "type": "text" }, { "type": "image", "max_references": 2, "supported_frame_images": ["first_frame", "last_frame"] } ] ``` ## What the deployment produces The `type` of each entry in `output_modalities` is `text`, `image`, `audio` or `video`, once each. Image, speech and video deployments name `image`, `audio` and `video`, and must also name `text`. | Field | Applies to | Checked | Description | | ---------------------- | ------------------- | ---------- | ------------------------------------------------------------------------------------------------------- | | `formats` | image, audio, video | Yes | Formats it produces. Write `png` or `image/png`. | | `sizes` | image, video | Yes | Pixel sizes, for example `1024x1024`. | | `max_count` | image | Yes | Most images in one response. | | `response_formats` | image | Yes | `url`, `b64_json` or both. If you list none, a request for a form can still reach it. | | `voices` | audio | Yes | Voices it can speak in. | | `seconds` | video | Yes | Lengths it renders, for example `[4, 8]`. | | `max_duration_seconds` | audio, video | Video only | Longest clip. For audio, the exchange only records it, because it cannot measure a clip first. | | `audio` | video | Yes | Sound of the video: `none`, `always` or `optional`. See [Sound](#sound-of-a-video). | | `resolutions` | video | Yes | Resolutions it renders, such as `720p`. See [Sound, resolution and price](#sound-resolution-and-price). | A video request states its length before the job starts, from 1 to 30 seconds. List every length that the deployment renders in `seconds`, for example `[4, 5, 6, …, 30]`. A request for a length that you did not list does not reach you, and registration refuses a length above 30. If you set only `max_duration_seconds`, a request for a length below your real minimum can reach you and fail. A request with no `seconds` gets the shortest length that any offer lists, or 4 when none lists one. The exchange does not accept `seconds: -1`, because the buyer must state the length. ### Sound, resolution and price A buyer can send `generate_audio` (`true` or `false`) and `resolution` (such as `720p`). A request that sends one reaches only a deployment that states the option and can make it. A deployment that states nothing serves only a request that does not send it. - `audio` is `none` (every video is silent), `always` (every video has sound) or `optional` (the buyer chooses, and a request that says nothing gets sound). - `resolutions` lists the labels that the deployment renders, such as `["720p", "1080p"]`. If the price depends on the sound or the resolution, quote several `output_video` lines with a `variant`. [Quote a media model](https://inference.ai.exchange/docs/provider-media-quote.md#3-write-the-price-table) has the rules and an example. The exchange refuses a quote with a variant that names a sound without `audio` as `optional`, or a resolution that `resolutions` does not list. Send the choice to your upstream: the dispatch holds it in `audio` and `resolution` of the `video` object. A request for a format, size, count or voice that you did not list goes to another offer before the award. The charge is known before dispatch: | Kind | `pricing` line | Charge | | --------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Image, each image | `output_image`, no token line | Rate times the images that the buyer asked for. | | Image, `pricing_unit` `tokens` | `input_text` and `output_text`, no `output_image` | The tokens that you meter, up to the cap: the prompt at the input rate plus `image_output_tokens_max` per image at the generation rate. | | Speech | `output_speech` | Rate times the characters in the text. | | Video | `output_video` | Rate times the seconds that the buyer asked for. | | Reference images (image or video) | `input_reference_image` | Rate times the reference images that you metered, at most the number the exchange counted. Without the rate they are free. A token-priced image deployment does not quote it. Start and frame images do not count. | | Reference video | `input_reference_video` | Rate times the seconds of reference video that you metered, at most the seconds sent. | | Reference audio | `input_reference_audio` | Rate times the seconds of reference audio that you metered, at most the seconds sent (30 at most). | `operator_locked` names each field that an operator set. A later registration does not undo it. ## List deployments `GET /v1/provider/models` with your provider key returns each registration in all statuses. The `listed` frame of the session lists what you can quote now. ## From registration to the public book A listing puts a deployment on the public book, where buyers reach it by model name. A registration never lists itself. 1. **Register.** The deployment starts `UNLISTED`. 2. **Quote it.** It rests as an [unverified offer](#unverified-offers). 3. **The exchange measures it** with benchmarks and quality checks. 4. **An operator lists it, or the exchange does.** The exchange sets `listed_at`. See [Automatic listing](#automatic-listing). 5. **After the next quote**, it competes under the model name. ## Listing statuses Two independent fields control availability. A paused `LISTED` deployment keeps its listing, but you do not quote it. | Field | Who sets it | Meaning | | ----------- | -------------------------------------------------------- | --------------------------------------------- | | `status` | An operator, and you when you resume a paused deployment | Whether the deployment is on the public book. | | `paused_at` | You | Set while you do not quote it. | | Status | Quotable | Description | Operator transitions | | ----------- | --------------- | ------------------------------------------------------------------------------------------------ | ----------------------- | | `UNLISTED` | Yes, unverified | Not in the public book. Only your workspace and the exchange buy from it, by `model_deployment`. | `LISTED`, `SUSPENDED` | | `LISTED` | Yes | In the public book. Buyers can ask for it by model name. | `UNLISTED`, `SUSPENDED` | | `SUSPENDED` | No | No buyer can reach it. `status_reason` and `suspended_at` give the reason and time. | `UNLISTED` (reinstate) | Public admission requires an approved registry identity. `status_reason` and `suspended_at` are set only when the status is `SUSPENDED`. `listed_at` is set only when it is `LISTED`. The session rejects a quote for a `SUSPENDED` or paused deployment with `quote_reject`. A new registration does not end a suspension. Repair the cause and contact the operators. ## Automatic listing When automatic listing is on, the exchange checks every deployment every 60 seconds. An operator can still list by hand. It lists an `UNLISTED` deployment when all of these are true: - Your provider is admitted and public. - Your signed provider agreement is on record. Ask the operators to record it. - The deployment has an approved registry identity of its kind, and it names no other identity than its link. - You did not pause it, and no reservation commits it. - Your last decision was not an unlisting. - You list no other deployment of the same identity under the same name. Case and punctuation do not count. ## Unverified offers A quote for an `UNLISTED` deployment rests as an **unverified** offer. Only your workspace and the exchange can buy from it. - Send its `model_deployment`, such as `acme/gpt-oss-120b`, as the model, with an API key of the owning workspace. Another workspace gets `model_not_found`. - The exchange buys from it to measure it. It never chooses it for a model name, router model or preset, and its quotes stay out of the price history. - Use it to test end to end with a real auction and settlement. To change the owner workspace: 1. Open the provider in the console and go to **Access**. 2. Choose **Move to another workspace**. You must be an owner or admin of both workspaces. 3. Restart the agent. The exchange reads the owner when the agent connects. ## Select an approved identity A public deployment must have an approved identity from the model registry, of the same kind. 1. Send `GET /v1/provider/model-registry` with your provider key. Each record has a UUID, canonical name, creator, revision, kind and aliases. 2. Send the UUID as `registry_model_id` when you register. 3. Keep the dispatch handle in `upstream_model`. - `/v1/provider/models` and the session return the public name as `canonical_model`. Read the dispatch handle from `upstream_model`, not `model`. - If an operator retires an identity, the linked deployments are unlisted and their names blocked. Your workspace can still use each exact symbol. Without an approved identity, use a private deployment by its exact symbol. The model request panel in the console accepts evidence for review. ## Pause and resume A pause stops the quotes for one deployment and keeps its listing. ```bash curl -X POST https://router.inference.ai.exchange/v1/provider/models/3f0c.../pause \ -H "Authorization: Bearer sk-liquid-..." ``` This sets `paused_at` and removes the resting offer at once. Use `/resume` to clear the pause. Resume does not grant public admission, and it changes nothing for a `SUSPENDED` deployment. The account API does the same: ``` PATCH /v1/account/providers/{provider_id}/models/{model_id} { "paused": true } ``` Send `{ "paused": false }` to resume. An unknown id returns `404 no_model`. ## Set a margin The console stores a margin for each deployment, in basis points above your upstream cost. ``` PATCH /v1/account/providers/{provider_id}/models/{model_id} { "markup_bps": 1500 } ``` An integer from 0 to 100000 sets the margin (`1500` is 15%). `null` restores the agent default. An absent field changes nothing. A value out of range returns `400 invalid_markup`. The agent reads the margin on its next refresh. # Quote over the session An agent publishes prices over a WebSocket session. A quote is valid only while the session is open and the quote has not expired. 1. [Connect](#connect) and [log in](#log-in). 2. Read the `listed` frame. It names the deployments that you can quote. 3. [Send a `quote`](#send-a-quote) for each one. Read the `quote_ack` or `quote_reject`. 4. [Keep the session alive](#keep-the-session-alive). Quote again before each quote expires. All frames are JSON objects with a `type` field. OpenAPI names them `ProviderMessage` and `ExchangeMessage`. ## Connect ``` wss://router.inference.ai.exchange/v1/provider/ws ``` The exchange closes the socket if `login` is more than five seconds late, or if any frame is 1 MiB or larger. ## Log in The first frame must be `login`. For any other frame, the exchange sends `login_reject` and closes the connection. ```json { "type": "login", "slug": "acme", "api_key": "sk-liquid-...", "dispatch_url": "https://provider.example.com/dispatch", "dispatch_token": "a-long-random-secret", "dispatch_dialect": "liquid", "capabilities": [] } ``` | Field | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `slug` | Your current handle. The exchange refuses a handle that you gave up. | | `api_key` | A provider key. See [Provider credentials](https://inference.ai.exchange/docs/provider.md). | | `dispatch_url` | Where the exchange sends awarded jobs. Omit it with the `byok` dialect. The scheme, host and port must match the registered `declared_endpoint`. The path is free. | | `dispatch_token` | Optional. A secret that you choose. See [Dispatch token](#dispatch-token). | | `dispatch_dialect` | Optional. `liquid` (default), `openai`, `anthropic` or `byok`. See [Dispatch dialect](#dispatch-dialect). | | `capabilities` | Optional. `handover` and `notice_ack`. The exchange ignores unknown values. | ### Dispatch token The exchange sends the token as `Authorization: Bearer ` on each call to `dispatch_url` and to its `/content` route. Refuse a call without it, so that only the exchange can send you work. The exchange keeps the token in memory for this session only. A new login replaces it. The reference agent reads it from `LIQUID_DISPATCH_TOKEN`. ### Dispatch dialect | Value | Effect | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `liquid` | The default. The exchange sends the native delivery. | | `openai` | `dispatch_url` is the base URL of an OpenAI-compatible API. `dispatch_token` is its key. See [OpenAI-compatible](https://inference.ai.exchange/docs/provider-openai.md). | | `anthropic` | The same, for an Anthropic Messages API. See [Anthropic](https://inference.ai.exchange/docs/provider-anthropic.md). | | `byok` | For a provider that sells only [BYOK](https://inference.ai.exchange/docs/provider-byok.md) deployments. Give no `dispatch_url`. | ### Capabilities - `handover`: your agent can move to a new exchange process with no gap. See [Move to a new exchange process](#move-to-a-new-exchange-process). - `notice_ack`: your agent acknowledges each notice. See [Acknowledge notices](#acknowledge-notices). ### Login result After a successful login, the exchange sends `login_ack` and then `listed`: ```json { "type": "login_ack", "provider_id": "7c9e…", "slug": "acme", "server_time": "2026-09-21T10:00:00Z" } { "type": "listed", "models": [ … ] } ``` `login_ack` lists each accepted capability in `capabilities`. A failed login gets `login_reject` with a `reason`, and the connection closes. For example, the key is unknown, the handle does not match the provider, or the `dispatch_url` differs from the declared endpoint. ### Two logins for one provider A new login for the same provider replaces the current session and removes its offers. Run one agent for each provider, unless the agents use `handover`. A login with `handover` keeps the offers of the current session instead: - The older session keeps each offer, and gets its awards, until the newer session quotes it. Then the exchange sends `superseded` and closes the older session. ## Listed deployments The exchange sends `listed` after login. Quote only the deployments in the newest `listed` frame. It has each `LISTED` deployment and each `UNLISTED` deployment (`"unverified": true`). It has no `SUSPENDED` or paused deployment. Only your workspace can buy an unverified offer. `model` is the dispatch handle. `canonical_model` is the public name. See [Register deployments](https://inference.ai.exchange/docs/provider-models.md) for the two names. ```json { "type": "listed", "models": [ { "model_id": "3f0c…", "unverified": false, "deployment_name": "gpt-oss-120b", "model": "gpt-oss-120b", "max_input_tokens": 128000, "max_output_tokens": 4096, "tokenizer_id": "o200k_base", "markup_bps": 1500 } ] } ``` `markup_bps` is the margin from the console. It is absent when the console has no margin. ## Send a quote Send one `quote` frame for each listed deployment. Send it again each time the price changes. ```json { "type": "quote", "target": { "kind": "registered", "model_id": "3f0c…" }, "pricing": [ { "billable": "input_text", "unit": "mtok", "usd": 0.12 }, { "billable": "cache_read", "unit": "mtok", "usd": 0.03 }, { "billable": "output_text", "unit": "mtok", "usd": 0.48 } ], "fixed_fee": 0, "service_class": { "ttft_ms": 1500, "completion_ms": 30000 }, "measured_p95_ttft_ms": 620, "supported_features": ["text", "tools", "input.image", "sampling.top_p"], "router_memberships": ["liquid.auto"], "retention_policy_supported": ["none"], "warm_prefixes": [], "synthetic": false, "revision": 7 } ``` Prices are JSON numbers in USD. | Field | Type | Req. | Description | | ---------------------------- | -------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `target` | object | Yes | `{ "kind": "registered", "model_id": "…" }`. | | `pricing` | array | No | The prices of the quote, one line for each billable. Each line has `billable`, `unit`, `usd` and an optional `variant`. See [The price table](#the-price-table). Omit a billable if you do not charge for it. | | `fixed_fee` | number | No | USD for each job. Default `0`. | | `service_class` | object | Yes | `ttft_ms` and `completion_ms`: the deadlines that you agree to. If you miss one, the buyer still pays and the exchange marks your deployment slow. | | `measured_p95_ttft_ms` | integer | No | Your p95 time to first token. The exchange ranks on its own data. | | `supported_features` | string[] | No | The request parameters that the offer supports. See [Supported features](#supported-features). | | `router_memberships` | string[] | No | The router models that this offer competes under. | | `workload_profile_targets` | string[] | No | The workload profiles that this offer targets. | | `retention_policy_supported` | string[] | Yes | `none`, `allowed`: the policies that you can obey. An offer with `none` also serves buyers who request `allowed`. A buyer who sets no rule needs `none`. The exchange rejects a quote that declares neither. | | `warm_prefixes` | string[] | No | Prefix digests that you agree to serve at the `cache_read` price, also if the cache removes the entry. | | `media_token_cost` | object | No | Input tokens for each attachment: `image_base`, `image_per_tile`, `image_unmeasured`, `audio_per_second`, `pdf_per_page`, `video_per_second`. If absent, the exchange uses its own high table. | | `synthetic` | boolean | No | `true` when a local stand-in serves. A production exchange refuses it. | | `valid_for_ms` | integer | No | How long the quote is valid, in milliseconds. Default: ten minutes. No upper limit. Zero is refused. `quote_ack` gives the accepted value. | | `revision` | integer | Yes | Must increase in a session. The exchange rejects a revision equal to or lower than the current one. A newer session with `handover` takes over an offer at any revision. | ### The price table A quote states its prices as a table in `pricing`. Each line is one price. ```json "pricing": [ {"billable": "input_text", "unit": "mtok", "usd": 0.25}, {"billable": "output_text", "unit": "mtok", "usd": 1.0}, {"billable": "output_video", "unit": "second", "usd": 0.08}, {"billable": "output_video", "unit": "second", "variant": {"resolution": "720p", "audio": false}, "usd": 0.03} ] ``` - `billable` is what the price is for. Token prices: `input_text` (input that no cache served or kept), `cache_read`, `cache_write_short` (five minutes), `cache_write_long` (one hour), `output_text` and `output_audio` (audio output tokens, with `output.audio`). Counted prices: `output_image`, `output_speech`, `output_video`, `input_audio` (audio to transcribe), `input_reference_image`, `input_reference_video` and `input_reference_audio`. - `unit` is the singular unit of the billable: `mtok` (a million tokens), `image`, `character` or `second`. A line with another unit is refused. - `usd` is USD for one `unit`. For `mtok` it is USD per million tokens. A speech price of $15.00 per million characters is `0.000015`. - `variant` holds `resolution`, `audio`, `quality` and `size`. A variant line holds for a request that states every option the variant names. A line with no variant is the price for a request that no variant line holds for. If two variant lines hold, the line that names more options wins. - `output_video` and the three `input_reference_*` billables take `resolution` and `audio`. See [Sound, resolution and price](https://inference.ai.exchange/docs/provider-models.md#sound-resolution-and-price). - `output_image` takes `quality` and `size`. The buyer states them as `quality` and `size` of the image request. For example, `{"quality": "hd"}` is the price of an image that the buyer asks for in `hd`. - `output_speech` takes `quality`. The buyer states it as `quality` of the speech request. Each billable has one line with no variant. The exchange refuses these quotes with `invalid_terms` on the field `pricing`: a line with a variant on a billable that takes none, a line whose `unit` is not the unit of its billable, and two lines with the same billable and the same variant (also when the prices are equal). Do not send a second line with no variant. For token billables, `output_image` and `output_speech` the exchange refuses it. For `output_video` and the `input_reference_*` billables it keeps the later line, so send only one. A quote changes only prices, deadlines and declared capabilities. The registration sets everything else. `media_token_cost` sets the cap and the ranking. Settlement charges the counts that your upstream reports. If your value is too low, you pay the difference. ### Quote result The exchange answers each quote with one `quote_ack` or `quote_reject`. ```json { "type": "quote_ack", "offer_id": "3f0c…", "revision": 7, "activation_seq": 118, "valid_for_ms": 600000, "notes": [ { "code": "defaulted", "field": "valid_for_ms", "detail": "absent: the offer rests for the default lifetime" } ] } { "type": "quote_reject", "model_id": "3f0c…", "reason": "…", "code": "paused" } ``` `valid_for_ms` on the ack is a duration from now. Quote again well before it ends. #### Notes `notes` is absent when the offer rests as you quoted it and agrees with your claims. Each note is one change by the exchange, or one conflict: - `defaulted` on `valid_for_ms`: you omitted the lifetime, so the offer rests for ten minutes. - `defaulted` on `supported_features`: the exchange added `text` to a chat quote. - `ignored`: the exchange does not know the field or value. It has no effect. - `withheld`: the exchange removed a feature for a time, because requests that needed it failed. - `overridden`: an operator set the features of this deployment. They replace yours. - `conflict` on `retention_policy_supported`: the quote disagrees with the retention claim that you state on Data handling. The exchange changes nothing and applies the quote. You state `zero_retention`, but the quote has no `none`: buyers who set no retention rule cannot buy the offer. Or you state `retention_days`, but the quote has `none`. Correct the quote or the claim. #### Rejection codes Read `code` and `field` of `quote_reject`. Do not parse `reason`. `field` is absent when no single field is the cause. | Code | Repair | | ------------------------- | ---------------------------------------------------------------------------------------------------- | | `malformed` | The frame does not parse. `reason` names the field. Send the quote again. | | `no_registration` | Quote a deployment from the `listed` frame. | | `paused` | Resume the deployment in the provider console, then quote again. | | `suspended` | An operator must reinstate the deployment. | | `catalogue_unavailable` | Quote again later. | | `wrong_target` | Quote with `{ "kind": "registered", "model_id" }`. | | `synthetic_on_production` | Quote the real model, or use a development exchange. | | `invalid_terms` | A value cannot rest. Repair the field in `field`. | | `stale_revision` | Send a `revision` higher than the one that rests. | | `superseded` | A newer session quotes this deployment. This session can re-mark only offers that it still holds. | | `contract_price` | A reservation contract is linked to the deployment. Quote its input and output prices, no fixed fee. | | `other` | A code this build does not know. Read `reason`. | The exchange keeps the last refusal of each deployment until a quote rests. `GET /v1/provider/models` returns it as `quote_reject`, with `reason`, `first_rejected_at`, `last_rejected_at` and `count`. The Models page shows it too. ### Supported features `supported_features` uses a fixed list of names. Declare only what your upstream supports. The exchange sends a parameter only to offers that declared it. The registered `input_modalities` also limit the media features ([Register deployments](https://inference.ai.exchange/docs/provider-models.md)). | Feature | Request parameter | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `text` | Plain text. Each request includes it. | | `tools` | `tools` | | `response_format.json_object` | `response_format: {"type": "json_object"}` | | `response_format.json_schema` | `response_format: {"type": "json_schema", …}` | | `input.image` | `image_url` content parts, or Anthropic `image` blocks | | `input.audio` | `input_audio` content parts | | `input.file` | `file` content parts, or Anthropic `document` blocks | | `input.video` | `video_url` content parts. Declare it only if the model reads video | | `sampling.` | `top_p`, `top_k`, `min_p`, `repetition_penalty`, `stop`, `seed`, `logprobs`, `logit_bias`, `frequency_penalty` or `presence_penalty` | | `reasoning.effort` | OpenAI `reasoning_effort` | | `reasoning.budget_tokens` | Anthropic `thinking.budget_tokens` | | `output.audio` | `modalities` with `"audio"`, and `audio`. Needs an `output_audio` line | | `output.image` | `modalities` with `"image"`. Needs an `output_text` line for image tokens | | `image.seed` | `seed` of an image job | | `image.background` | `background` of an image job: `transparent` or `opaque` | | `image.output_compression` | `output_compression` of an image job | | `image.stream` | `stream: true` of an image job: you send draft images before the final image | | `speech.instructions` | `instructions` of a speech job, for the clip or for a turn | | `speech.turns` | `input` of a speech job as more than one turn, each with its own voice | | `transcription.diarize` | `diarize` of a transcription job: each segment names its `speaker` | | `transcription.keyterms` | `keyterms` of a transcription job | | `embedding.input_type` | `input_type` of an embeddings job | | `video.seed` | `seed` of a video job, a whole number of 0 or more | | `video.creativity` | `creativity` of a video job, a number from 0 to 1 | | `video.upscale_factor` | `upscale_factor` of a video job, a whole number of 2 or more | | `video.continue` | `previous_job_id` of a video job: the video continues an earlier job | - **Media controls.** Declare a media control only if your agent sends it to the upstream and the upstream honors it. A job with the control reaches only an offer that declared it. [Quote a media model](https://inference.ai.exchange/docs/provider-media-quote.md) has the rules. - **Image output.** An offer has one `output_text` price. If image output tokens cost more, quote the higher price with `output.image`. Text-only requests then pay it too. - **Reasoning levels.** If your upstream accepts only some levels, list them in `reasoning_efforts`, for example `["low", "high", "max"]`. The exchange sends the nearest listed level, and a tie goes to the higher one. With no list, it sends the buyer's level unchanged. The order is `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. If the upstream refuses a level and its error names the levels it accepts, the exchange remembers them for one hour and tries your offer again at the nearest level. If the error names none, or the upstream takes no level, the job goes to the next offer. If no other offer takes the job, the exchange tries your offer again with no level, and the receipt shows this. - **Log probabilities.** `sampling.logprobs` means you must send them. See [Log probabilities](https://inference.ai.exchange/docs/provider-dispatch.md). ## Withdraw a quote ```json { "type": "withdraw_quote", "target": { "kind": "registered", "model_id": "3f0c…" } } { "type": "mass_withdraw" } ``` `withdraw_quote` removes one offer. `mass_withdraw` removes each offer of the provider, and the exchange confirms with a `risk_notice`. Neither frame cancels a job that the exchange already awarded. A quote has no capacity field. To stop awards when you are full, withdraw the offer or let it expire, and quote it again when you have room. ## Keep the session alive If a session sends no frame for five seconds, the exchange closes it and removes its offers. Send a `heartbeat` when you have nothing else to send. The exchange replies with a `heartbeat`. A heartbeat does not extend a quote. A quote ends after its `valid_for_ms`, even on an open session. A newer revision restores the offer. When the session closes, the exchange removes its offers. Jobs already dispatched continue and settle. ## Reconnect After you connect again, log in and send each quote again. Revisions start again in a new session. After the exchange closes a session, connect again at once. After a failed connection, wait 0.25 seconds, then double the wait up to 5 seconds. After `superseded` with no other session, do not connect again. The close frame has a code: | Code | Cause | Next step | | ------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | | 1012 | The exchange process stops. | Connect again at once. | | 4000 | A login without `handover` replaced this session. | Make sure that only one agent runs, then connect. | | 4001 | A newer session holds all offers of this session. | Do as for `superseded`. | | 4002 | The exchange received no frame for five seconds. | Connect again at once. | | 4003 | The agent did not read its frames and the exchange queue filled. | Read the socket continuously, then connect. | | 4004 to 4006 | An operator revoked the key (4004), the provider is not admitted (4005), or its jurisdiction cannot trade (4006). | Wait, as after a failed connection. | With no close frame, a network fault or crash ended the session. Connect again at once. ## Acknowledge notices The exchange sends a `settlement_notice` to the session that holds the offer. If that session closed, it goes to the current session. If no session is open, the notice is lost, unless you use `notice_ack`. Send `notice_ack` in `capabilities`. When `login_ack` has it, acknowledge each `award_notice` and `settlement_notice`: ```json { "type": "notice_ack", "award_id": "aw_…", "notice": "award" } ``` Set `notice` to `award` or `settlement`. After each login with `notice_ack`, the exchange sends the kept notices again, oldest first. - A notice can arrive twice, or on another session. The award id and kind identify it. - The exchange keeps a notice for 15 minutes, up to 4,096 for each provider, in memory. A crash deletes them. For a job with no notice, read the [jobs feed](https://inference.ai.exchange/docs/provider-dispatch.md#read-the-jobs-feed). - A login without `notice_ack` deletes the kept notices. ## Move to a new exchange process Before the old process of a release stops, the exchange sends `{ "type": "handover" }` to each session that logged in with `handover`. Then do these steps: 1. Keep this session open and quote on it. 2. Open a second session to the same address and log in with `handover`. It reaches the new exchange process. 3. Quote each deployment on the new session. 4. When the new session has a `quote_ack` or `quote_reject` for each quote, send `{ "type": "handover_ready" }` on the old session. 5. Serve jobs from both sessions. The old process closes the old session when it stops. Then use only the new session. Without `handover`, your offers leave the book when the old process stops, until you connect and quote again. A new version of your agent uses the same takeover. See [Upgrade the agent](https://inference.ai.exchange/docs/provider-operate.md#upgrade-the-agent). ### Requirements for the handover capability Send `["handover"]` only when your agent does all of these: 1. Hold two sessions at once and share the award state between them. 2. Send a `heartbeat` on each session. Count `revision` from the start of each session. 3. On `handover`, follow the steps above. Never open a third session. Serve the jobs of the old session until the exchange closes it. 4. On `quote_reject` with `superseded`, stop quoting that deployment on that session, but keep the session alive. If it gets a new `listed` frame, it is current again. Quote each deployment on it. 5. On `superseded`, let the session close. If the agent has no other session, do not log in again. Finish the current dispatches, then exit. Another process holds the offers, and a new login takes them back. 6. On any other close with no other session, [reconnect](#reconnect). On shutdown, send `mass_withdraw` on each session, then finish the current dispatches. If `login_ack` lacks the capability, the session works without `handover`. The reference provider does all of this. [Run your agent](https://inference.ai.exchange/docs/provider-operate.md#test-the-capability) has tests. ## Quote your cache prices A buyer pays for input in up to four parts, each at its own rate (see the [fields](#send-a-quote)). - `input_text`: no price means the offer has no input price. - `cache_read`: without it, settlement refuses a job that reports a cache read and you are not paid. The ranking also assumes no cache saving. If your rate card has no cache-read price, quote the input rate here. - `cache_write_short` and `cache_write_long`: without them, the exchange bills a written token as uncached input. Quote a write rate only if your upstream charges for a write (Anthropic does, and OpenAI from GPT-5.6). Otherwise you carry any extra charge. The cap covers a write only up to a cache breakpoint of the buyer. With no breakpoint, the exchange bills written tokens as uncached input, also if you quoted a write rate. ## Win sessions with your cache In an agentic session, the buyer sends the same conversation on each turn. If you served the previous turn and quote a `cache_read` price, the exchange ranks your offer as if your cache serves part of the next turn. A higher list price can still win. - The exchange measures your hit rate from settled jobs. Until then it assumes one half. - The price cap is the cold worst case. To lower it, list the prefix digests that you guarantee in `warm_prefixes`. The cap then uses the `cache_read` price for them. You carry the risk if your upstream drops the entry. - Keep one deployment for each model. A session stays with one deployment, so a second one does not share its cache. - A session lasts five minutes after your last turn, or one hour with a one-hour breakpoint. # Quote a media model This page shows how to quote an image, speech, transcription, embedding or video model. It tells you which features to declare, how to declare sizes, and how to write the price table. Read [Quote over the session](https://inference.ai.exchange/docs/provider-quoting.md) first for the frames. A buyer's control works only when an offer declares it. If you do not declare a control, no job that sets it reaches you. This is on purpose: an offer that ignores a control and still charges for the job is not a fair sale. ## 1. Declare the controls Each control has a feature. The feature has the name of the request field of the buyer. Put the features that you support in `supported_features` of the quote. | Kind | Field of the buyer | Feature | | ------------- | ---------------------------------------- | -------------------------- | | Image | `seed` | `image.seed` | | Image | `background` (`transparent` or `opaque`) | `image.background` | | Image | `output_compression` | `image.output_compression` | | Image | `stream: true` | `image.stream` | | Video | `seed` | `video.seed` | | Video | `creativity` | `video.creativity` | | Video | `upscale_factor` | `video.upscale_factor` | | Video | `previous_job_id` | `video.continue` | | Speech | `instructions` | `speech.instructions` | | Speech | `input` as more than one turn | `speech.turns` | | Transcription | `diarize` | `transcription.diarize` | | Transcription | `keyterms` | `transcription.keyterms` | | Embedding | `input_type` | `embedding.input_type` | - Declare a control only if your agent sends it to the upstream and the upstream honors it. If you declare a control that you ignore, you are paid for a job that the buyer did not get. - A reference input has its own feature: `input.image`, `input.audio`, `input.video` or `input.file`. Declare the kinds that your upstream reads. - The exchange drops a name that it does not know. A typo in a name means that you win no job with that control. - `resolution`, `aspect_ratio`, `size`, `voice`, `quality` and `generate_audio` are not features. The exchange matches them with the sizes, voices and options that you registered. See [Sizes and resolutions](#2-declare-sizes-and-resolutions). ### In an agent that you write Send the features in the `quote` frame: ```json { "type": "quote", "supported_features": ["text", "image.seed", "image.background"] } ``` With the Rust agent library, an `Upstream` lists its controls in `Upstream::controls(kind, route, handle)`. It returns the features for that kind of job. The agent keeps only the controls that fit the kind. A rate card can narrow the list with `ExecutionRoute::features`. It cannot add to it. `Upstream::reference_inputs(kind)` lists the `input.*` features. The Python reference provider declares only `text`. It cannot win a job that sets a control. Add the names that your upstream honors. ### What the OpenAI adapter declares The first-party OpenAI adapter declares these controls: | Kind | Declared | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Image | `image.background` and `image.output_compression` for a model whose name starts with `gpt-image`. `image.seed` when the route has a seed rule. | | Video | `video.seed` when the route has a seed rule. | | Speech | `speech.instructions` for a model whose name contains `gpt-4o`. | It declares nothing else. In particular, it declares no `image.stream`, `speech.turns`, transcription control, embedding control, `video.creativity`, `video.upscale_factor` or `video.continue`. To sell those, write your own agent. ## 2. Declare sizes and resolutions The registration of the deployment sets what the model makes. See [Register deployments](https://inference.ai.exchange/docs/provider-models.md). The buyer's `resolution` and `aspect_ratio` resolve against it. ### Images List the pixel sizes in `sizes` of the `image` output, as `x`. There is no separate list of tiers or shapes. The exchange works them out from the sizes. - The tier of a size is the tier that is nearest to its area. The tiers are `512`, `768`, `1K`, `1.5K`, `2K` and `4K`. For example, `1024x1024`, `1536x1024` and `1024x1536` are `1K`, `1920x1080` is `1.5K`, and `2048x2048` is `2K`. - A size has a shape when the two ratios differ by 1% or less. - A buyer who sends `resolution` or `aspect_ratio` gets the declared size that has the tier and the shape. If more than one fits, the exchange picks the size that is nearest to the tier, then the one that is nearest to a square. - That size is the size of the dispatch, and it is the size that sets the price. - An offer with no `sizes` cannot win a job that sets `size`, `resolution` or `aspect_ratio`. - If no size fits, the buyer gets `400 unsupported_parameter`, and the error lists the sizes that offers declare. ### Video List the pixel sizes in `sizes` and the labels in `resolutions`, for example `["1280x720"]` and `["720p"]`. - A `resolution` is a label. The exchange compares it with your `resolutions` as a string. An offer that lists none cannot win a job that sets `resolution`. - With `aspect_ratio` and no `size`, the exchange picks the smallest declared size that has the shape. If the buyer also sent a label such as `720p`, the short side of the size must be 720. - With `resolution` and no `aspect_ratio` and no `size`, the exchange picks no size. The label only selects offers. - So to serve both fields, declare the sizes and the labels. An offer with `default_size_only` set declares no size for this. ## 3. Write the price table The quote states prices in `pricing`. A price is USD for one unit. Each line has `billable`, `unit`, `usd` and, if the price depends on an option, a `variant`. | `billable` | `unit` | Counted | | ----------------------- | ----------- | ------------------------------------------ | | `input_text` | `mtok` | Input tokens that no cache served | | `cache_read` | `mtok` | Input tokens that a cache served | | `cache_write_short` | `mtok` | Tokens written to a cache for five minutes | | `cache_write_long` | `mtok` | Tokens written to a cache for one hour | | `output_text` | `mtok` | Output tokens, not the audio tokens | | `output_audio` | `mtok` | Audio output tokens of a chat answer | | `output_image` | `image` | Images | | `output_speech` | `character` | Characters of the text | | `output_video` | `second` | Seconds of video | | `input_audio` | `second` | Seconds of audio to transcribe | | `input_reference_image` | `image` | Reference images | | `input_reference_video` | `second` | Seconds of reference video | | `input_reference_audio` | `second` | Seconds of reference audio | - `unit` must be the unit of the billable. A speech price of $15.00 per million characters is `0.000015` for the unit `character`. - An image, speech, video or transcription offer must have one line with no variant for its own billable: `output_image`, `output_speech`, `output_video` or `input_audio`. It cannot price the unit of another kind. - A reference price is optional. A reference image that you do not price is free. A job with reference video or audio goes only to an offer that prices it. - A `variant` is allowed on `output_video`, the three `input_reference_*` billables, `output_image` and `output_speech`. It has the options `resolution`, `audio`, `quality` and `size`. A video takes `resolution` and `audio`. An image takes `quality` and `size`. Speech takes `quality`. - A variant line holds for a job that states every option the variant names. If more than one line holds, the line that names more options wins. If none holds, the line with no variant sets the price. A video job that does not state `generate_audio` counts as `audio: true`. - A variant that names `audio` needs `audio` as `optional` in the `video` output. A variant that names a `resolution` needs that label in `resolutions`. A variant that names an image `size` needs that size in `sizes`. Otherwise the quote is refused, because no job can reach the price. The cap and the charge of a counted job are known before dispatch. The exchange multiplies the count by the line that holds for the job. So the cap is exactly the charge. ### A video model Registration: ```json { "kind": "video_generation", "output_modalities": [ { "type": "text" }, { "type": "video", "sizes": ["1280x720", "1920x1080"], "resolutions": ["720p", "1080p"], "seconds": [4, 8], "audio": "optional", "formats": ["video/mp4"] } ] } ``` Quote: ```json "pricing": [ { "billable": "output_video", "unit": "second", "usd": 0.10 }, { "billable": "output_video", "unit": "second", "variant": { "audio": false }, "usd": 0.07 }, { "billable": "output_video", "unit": "second", "variant": { "resolution": "720p" }, "usd": 0.06 }, { "billable": "output_video", "unit": "second", "variant": { "resolution": "720p", "audio": false }, "usd": 0.04 }, { "billable": "input_reference_video", "unit": "second", "usd": 0.02 } ] ``` A job for 8 seconds at `720p` with sound pays the third line: 8 × $0.06. A job for `1080p` with no other option pays the first line. A job for `720p` without sound pays the fourth line, because it names more options than the second and the third. ### An image model Registration: ```json { "kind": "image_generation", "output_modalities": [ { "type": "text" }, { "type": "image", "sizes": ["1024x1024", "1536x1024", "2048x2048"], "formats": ["png", "webp"], "max_count": 4 } ] } ``` Quote: ```json "pricing": [ { "billable": "output_image", "unit": "image", "usd": 0.04 }, { "billable": "output_image", "unit": "image", "variant": { "size": "2048x2048" }, "usd": 0.08 } ] ``` A buyer who sends `resolution: "2K"` gets the size `2048x2048` and pays $0.08 for each image. A buyer who sends `1K` gets `1024x1024` and pays $0.04. A buyer who sends `quality` or `size` pays the line whose variant matches, and the line with no variant otherwise. If your upstream bills tokens, set `pricing_unit` to `tokens` and `image_output_tokens_max` in the registration. Quote `input_text` and `output_text` lines, and no `output_image` line. See [Sell images](https://inference.ai.exchange/docs/provider-images.md). ### A speech model Registration lists the `voices` and `formats`. Quote one line, and a variant for a quality that costs more: ```json "pricing": [ { "billable": "output_speech", "unit": "character", "usd": 0.000015 }, { "billable": "output_speech", "unit": "character", "variant": { "quality": "hd" }, "usd": 0.00003 } ] ``` The charge is the characters of the text of all turns, times the price. A job for 500 characters pays $0.0075. With `quality: "hd"` it pays $0.015. A request with no `quality`, or a name that you do not price, pays the line with no variant. ### A transcription model Quote one line: `{ "billable": "input_audio", "unit": "second", "usd": 0.0001 }`. The charge is the seconds of audio. See [Sell transcription](https://inference.ai.exchange/docs/provider-transcription.md). ### Old rate fields An older quote states prices in fields such as `rate_per_image`, `rate_per_video_second` and `pricing_skus`. The exchange still reads them, but they are deprecated. No `rate_*` field is required. - Send `pricing` only. It is the form that the ranking, the hold and the charge read. - If a quote has both forms, they must state the same prices. If they differ, the exchange refuses the quote with `invalid_terms` on the field `pricing`. For example: `pricing states output_text at $2.000000000 and the old rate fields state output_text at $1.000000000`. - Send both forms only to an exchange of an older release. It does not know `pricing`. ## 4. What your agent receives A media dispatch has one object for its kind: `image`, `audio` (speech), `transcription`, `embedding` or `video`. Control fields are present only if the buyer set them and you declared the feature. - **Image:** `count`, `size`, `output_format`, `quality`, `response_format`, `seed`, `background`, `output_compression` and `stream`. If `stream` is true, you can send `image_partial` events. See [Images](https://inference.ai.exchange/docs/provider-dispatch.md#images). - **Speech:** `characters`, `voice`, `instructions`, `turns`, `response_format`, `speed` and `quality`. See [Speech](https://inference.ai.exchange/docs/provider-dispatch.md#speech). - **Transcription:** `seconds`, `format`, `language`, `timestamps`, `prompt`, `temperature`, `diarize` and `keyterms`. Return one `transcript` event. With `diarize`, give each segment a `speaker`, such as `speaker_0`. - **Embeddings:** `inputs`, `dimensions` and `input_type`. Return one vector for each input. - **Video:** `seconds`, `size`, `audio`, `resolution`, `seed`, `creativity`, `upscale_factor` and `previous_job_id`. See [Video](https://inference.ai.exchange/docs/provider-dispatch.md#video). For a video job, the exchange sends no cancel message. When the buyer cancels, or the job runs past the video job limit, it closes the dispatch connection. Watch for the close and stop. Each case costs the buyer nothing. See [Cancel and expiry](https://inference.ai.exchange/docs/provider-video.md#cancel-and-expiry). For `previous_job_id`, the exchange sends only the job id of the earlier video. It does not check that the earlier job ran on your deployment. Map the id to your own handle, and fail the job if you do not know it. See [Continue a video](https://inference.ai.exchange/docs/provider-video.md#continue-a-video). ## 5. What buyers see - The model card lists a control in `parameters` when at least one live offer declared the feature. `seed`, `background`, `output_compression`, `stream`, `creativity`, `upscale_factor`, `previous_job_id`, `instructions`, `turns`, `diarize`, `keyterms` and `input_type` are the keys. - It also lists `size`, `output_format`, `quality`, `resolution`, `seconds` and `generate_audio` from your registration. Image `resolution` and `aspect_ratio` are not keys. Buyers read your sizes in `image_sizes`. - It lists the cheapest line of each billable in `pricing`. If a price depends on an option, the line shows the `variant`. - The receipt of a job has `usage.pricing`. Each line has the billable, the unit, the quantity and the provider charge. See [Receipts](https://inference.ai.exchange/docs/receipts.md). - A buyer who sets a control that no offer declared gets `400 unsupported_parameter`. The error names the feature. ## 6. Checklist for a new model 1. Register the deployment with the right `kind` and the `output_modalities` of the model: sizes, voices, formats, lengths, `resolutions` and `audio`. 2. List only the sizes, voices and lengths that your upstream makes. A buyer can ask for any value you list. 3. Choose the controls that your agent sends and your upstream honors. Declare their features in `supported_features`. 4. Declare the `input.*` features for the reference inputs that you read. 5. Write `pricing`: one line with no variant for the billable of the kind, then the variant lines, then any reference prices. Use the unit of each billable. 6. Make sure that each variant names an option that the registration states. 7. Send the quote. Read the `quote_ack` or the `quote_reject`. For `invalid_terms`, read `field` and `reason`. 8. Quote as an unverified offer first. Buy one job from your own workspace for each control, and compare the result with the request. See [Unverified offers](https://inference.ai.exchange/docs/provider-models.md#unverified-offers). 9. Open the model card. Check `parameters`, `image_sizes` and `pricing`. 10. For video, test a cancel: the agent must stop when the dispatch closes. # Sell with an OpenAI-compatible API If your API already speaks the OpenAI dialect, you can sell with no agent and no delivery endpoint. The exchange calls your API directly. To start, [log in](#log-in) with `dispatch_dialect` set to `openai`. To run an agent instead, see [Serve and get paid](https://inference.ai.exchange/docs/provider-dispatch.md). ## When to use this Use this path when your API streams a chat completion in the OpenAI format. You still keep the session open to quote. [Quote over the session](https://inference.ai.exchange/docs/provider-quoting.md) has the frames. | The exchange sends | Your route | Answer | | ------------------ | ---------------------------------------- | --------------------------------------------- | | Chat | `POST {dispatch_url}/chat/completions` | Server-sent events, with a last `usage` chunk | | Embeddings | `POST {dispatch_url}/embeddings` | One JSON body | | Speech | `POST {dispatch_url}/audio/speech` | The audio file | | Images | `POST {dispatch_url}/images/generations` | One JSON body | These jobs need an agent, and the exchange does not send them to this path. The next offer gets the job. - Video jobs. See [Sell video](https://inference.ai.exchange/docs/provider-video.md). - Decision jobs. - A deployment that needs the queue of the studio engine. ## Set it up 1. Register the provider and its deployments. See [Register deployments](https://inference.ai.exchange/docs/provider-models.md). 2. Log in with `dispatch_dialect` set to `openai`. 3. Quote as usual. ### Log in `dispatch_url` is the base URL of your API. Its scheme, host and port must match the registered `declared_endpoint`. `dispatch_token` is the key of your API. The exchange sends it as `Authorization: Bearer ` and keeps it in memory for this session only. ```json { "type": "login", "slug": "acme", "api_key": "sk-liquid-...", "dispatch_url": "https://api.acme.example/v1", "dispatch_token": "your-upstream-api-key", "dispatch_dialect": "openai", "capabilities": [] } ``` ### Open the session and quote This script logs in, quotes each listed deployment and then sends a `heartbeat` every two seconds. The exchange closes a session that sends no frame for five seconds. See [Keep the session alive](https://inference.ai.exchange/docs/provider-quoting.md#keep-the-session-alive). The script uses the Python `websockets` package. A real provider also quotes again before the quote expires, and when its price changes. ```python import asyncio import json import websockets URL = "wss://router.inference.ai.exchange/v1/provider/ws" LOGIN = { "type": "login", "slug": "acme", "api_key": "sk-liquid-...", "dispatch_url": "https://api.acme.example/v1", "dispatch_token": "your-upstream-api-key", "dispatch_dialect": "openai", "capabilities": [], } def quote(model_id: str, revision: int) -> dict: return { "type": "quote", "target": {"kind": "registered", "model_id": model_id}, "pricing": [ {"billable": "input_text", "unit": "mtok", "usd": 0.12}, {"billable": "output_text", "unit": "mtok", "usd": 0.48} ], "service_class": {"ttft_ms": 1500, "completion_ms": 30000}, "supported_features": ["text"], "retention_policy_supported": ["none"], "revision": revision, } async def beat(socket) -> None: while True: await asyncio.sleep(2) await socket.send(json.dumps({"type": "heartbeat"})) async def main() -> None: async with websockets.connect(URL) as socket: await socket.send(json.dumps(LOGIN)) beating = asyncio.create_task(beat(socket)) try: async for raw in socket: frame = json.loads(raw) print(frame["type"]) if frame["type"] == "listed": for model in frame["models"]: await socket.send(json.dumps(quote(model["model_id"], 1))) elif frame["type"] in ("login_reject", "quote_reject"): raise SystemExit(frame) finally: beating.cancel() asyncio.run(main()) ``` ## What the exchange sends ### Chat The exchange posts a streamed completion to `{dispatch_url}/chat/completions`. The header `Idempotency-Key` is the award id. Your server may ignore it. The exchange never sends an award twice after the request leaves. ```bash curl https://api.acme.example/v1/chat/completions \ -H "Authorization: Bearer your-upstream-api-key" \ -H "Idempotency-Key: aw_8f3c…" \ -H "Content-Type: application/json" \ -d '{ "model": "acme-70b-instruct", "messages": [{ "role": "user", "content": "Name a prime number." }], "max_tokens": 64, "stream": true, "stream_options": { "include_usage": true } }' ``` `model` is the upstream identifier of the deployment. `max_tokens` is the cap of the award. Stream server-sent events. Send a last chunk with `usage`. The exchange settles on `prompt_tokens`, `completion_tokens`, `prompt_tokens_details.cached_tokens` and `completion_tokens_details.reasoning_tokens`. A stream with no usage block does not settle as a success. ```text data: {"choices":[{"index":0,"delta":{"role":"assistant","content":"Seven"}}]} data: {"choices":[{"index":0,"delta":{"content":" is prime."},"finish_reason":"stop"}]} data: {"choices":[],"usage":{"prompt_tokens":14,"completion_tokens":5,"total_tokens":19,"prompt_tokens_details":{"cached_tokens":0},"completion_tokens_details":{"reasoning_tokens":0}}} data: [DONE] ``` ### Embeddings ```bash curl https://api.acme.example/v1/embeddings \ -H "Authorization: Bearer your-upstream-api-key" \ -H "Content-Type: application/json" \ -d '{ "model": "acme-embed-1", "input": ["first text", "second text"], "encoding_format": "float", "dimensions": 256 }' ``` The answer must report `usage.prompt_tokens`, because the job settles on it. Without it the job fails and the buyer pays nothing. ```json { "data": [ { "index": 0, "embedding": [0.012, -0.044, 0.31] }, { "index": 1, "embedding": [0.101, 0.002, -0.27] } ], "usage": { "prompt_tokens": 6, "total_tokens": 6 } } ``` ### Speech ```bash curl https://api.acme.example/v1/audio/speech \ -H "Authorization: Bearer your-upstream-api-key" \ -H "Content-Type: application/json" \ -o answer.mp3 \ -d '{ "model": "acme-tts-1", "input": "The auction is open.", "voice": "alloy", "response_format": "mp3" }' ``` The answer is the audio file. The exchange counts the characters of the text itself, because speech has no token count. It types the file by your `Content-Type`, for example `audio/mpeg`. ### Images ```bash curl https://api.acme.example/v1/images/generations \ -H "Authorization: Bearer your-upstream-api-key" \ -H "Content-Type: application/json" \ -d '{ "model": "acme-image-1", "prompt": "A trading floor made of water", "n": 2, "size": "1024x1024" }' ``` ```json { "data": [{ "b64_json": "iVBORw0KGgo…" }, { "b64_json": "iVBORw0KGgo…" }] } ``` How an image job settles: - A deployment priced for each image settles on the number of images that you return. - A deployment priced in tokens settles on `usage` (`input_tokens` and `output_tokens`, or `prompt_tokens` and `completion_tokens`). It fails without `usage`. The exchange reads the whole answer of an embeddings, speech or images call before the buyer gets any of it, so a bad answer goes to the next offer. A call that sends no byte for two minutes fails. ## Refuse a job Answer an error with a status code of 400 or more before the first chunk, and the exchange offers the job to the next offer. ```http HTTP/1.1 429 Too Many Requests Content-Type: application/json { "error": { "message": "the deployment is at capacity", "type": "rate_limit" } } ``` After the first chunk of a chat answer, the exchange cannot fall back to another offer, because the buyer would get one answer from two providers. ## Limits - A media job that carries a reference input, such as an image to edit, fails on this path. The dialect has no field for it. Use an agent for those jobs. - A render has no queue and no keep-alive. The exchange does not use the studio engine for this path. - Quote as usual. The `quote` frames do not change. # Sell with an Anthropic API If your API already speaks the Anthropic Messages dialect, you can sell with no agent and no delivery endpoint. The exchange calls your API directly. To start, [log in](#log-in) with `dispatch_dialect` set to `anthropic`. - For an OpenAI-compatible API, see [Sell with an OpenAI-compatible API](https://inference.ai.exchange/docs/provider-openai.md). - To run an agent instead, see [Serve and get paid](https://inference.ai.exchange/docs/provider-dispatch.md). ## When to use this Use this path when your API streams a message in the Anthropic format. - You still keep the session open to quote. [Quote over the session](https://inference.ai.exchange/docs/provider-quoting.md) has the frames. - This path serves chat only. A job of another kind needs an agent, and the exchange offers it to the next offer. ## Set it up 1. Register the provider and its deployments. See [Register deployments](https://inference.ai.exchange/docs/provider-models.md). 2. Log in with `dispatch_dialect` set to `anthropic`. 3. Quote as usual. ### Log in `dispatch_url` is the base URL of your API. Its scheme, host and port must match the registered `declared_endpoint`. `dispatch_token` is the key of your API. The exchange sends it as `x-api-key` and keeps it in memory for this session only. ```json { "type": "login", "slug": "acme", "api_key": "sk-liquid-...", "dispatch_url": "https://api.acme.example/v1", "dispatch_token": "your-anthropic-api-key", "dispatch_dialect": "anthropic", "capabilities": [] } ``` ### Open the session and quote This script logs in, quotes each listed deployment and then sends a `heartbeat` every two seconds. The exchange closes a session that sends no frame for five seconds. See [Keep the session alive](https://inference.ai.exchange/docs/provider-quoting.md#keep-the-session-alive). The script uses the Python `websockets` package. A real provider also quotes again before the quote expires, and when its price changes. ```python import asyncio import json import websockets URL = "wss://router.inference.ai.exchange/v1/provider/ws" LOGIN = { "type": "login", "slug": "acme", "api_key": "sk-liquid-...", "dispatch_url": "https://api.acme.example/v1", "dispatch_token": "your-anthropic-api-key", "dispatch_dialect": "anthropic", "capabilities": [], } def quote(model_id: str, revision: int) -> dict: return { "type": "quote", "target": {"kind": "registered", "model_id": model_id}, "pricing": [ {"billable": "input_text", "unit": "mtok", "usd": 0.12}, {"billable": "output_text", "unit": "mtok", "usd": 0.48} ], "service_class": {"ttft_ms": 1500, "completion_ms": 30000}, "supported_features": ["text"], "retention_policy_supported": ["none"], "revision": revision, } async def beat(socket) -> None: while True: await asyncio.sleep(2) await socket.send(json.dumps({"type": "heartbeat"})) async def main() -> None: async with websockets.connect(URL) as socket: await socket.send(json.dumps(LOGIN)) beating = asyncio.create_task(beat(socket)) try: async for raw in socket: frame = json.loads(raw) print(frame["type"]) if frame["type"] == "listed": for model in frame["models"]: await socket.send(json.dumps(quote(model["model_id"], 1))) elif frame["type"] in ("login_reject", "quote_reject"): raise SystemExit(frame) finally: beating.cancel() asyncio.run(main()) ``` ## What the exchange sends The exchange posts a streamed message to `{dispatch_url}/messages`. The header `Idempotency-Key` is the award id. Your server may ignore it. The exchange never sends an award twice after the request leaves. ```bash curl https://api.acme.example/v1/messages \ -H "x-api-key: your-anthropic-api-key" \ -H "anthropic-version: 2023-06-01" \ -H "Idempotency-Key: aw_8f3c…" \ -H "Content-Type: application/json" \ -d '{ "model": "acme-claude-large", "max_tokens": 64, "stream": true, "messages": [{ "role": "user", "content": "Name a prime number." }] }' ``` - `model` is the upstream identifier of the deployment. - `max_tokens` is the cap of the award. - The exchange sends the cache plan of the job as `cache_control` markers, so cache writes are metered. - Thinking blocks, redacted thinking and tool calls pass through. Stream server-sent events in the Messages format, and end with `message_stop`. The exchange settles on the counts in `message_start.message.usage` and `message_delta.usage`. The last value of each count wins. ```text event: message_start data: {"type":"message_start","message":{"id":"msg_1","role":"assistant","usage":{"input_tokens":14,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"output_tokens":1}}} event: content_block_start data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Seven is prime."}} event: content_block_stop data: {"type":"content_block_stop","index":0} event: message_delta data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":5}} event: message_stop data: {"type":"message_stop"} ``` A stream with no usage counts, a stream that ends before `message_stop`, and an `error` event do not settle as a success. ## Refuse a job Answer an error with a status code of 400 or more before the first event, and the exchange offers the job to the next offer. ```http HTTP/1.1 529 Content-Type: application/json { "type": "error", "error": { "type": "overloaded_error", "message": "the deployment is at capacity" } } ``` After the first event of a chat answer, the exchange cannot fall back to another offer, because the buyer would get one answer from two providers. ## Limits - Only chat jobs are sent. A job of another kind goes to the next offer. - A chat job that has an audio or video part goes to the next offer, because the Messages API has no block for it. - Quote as usual. The `quote` frames do not change. # Run your agent This page gives the procedures to start to sell, to change what you sell, and to upgrade your agent without a gap in the book. For a first start, follow [Set up for the first time](#set-up-for-the-first-time). [Quote over the session](https://inference.ai.exchange/docs/provider-quoting.md) gives the frames and their fields. ## Set up for the first time 1. [Apply as a provider](https://inference.ai.exchange/docs/provider.md). Set `declared_endpoint` to the scheme, host and port of your dispatch server. Wait until the status is `ADMITTED`. 2. [Issue a provider key](https://inference.ai.exchange/docs/provider.md). Keep the secret in the secret store of your agent. 3. [Register each deployment](https://inference.ai.exchange/docs/provider-models.md). A new deployment is `UNLISTED`. 4. Configure the agent with these values: | Setting | Value | | ---------------- | ----------------------------------------------------------------------------------------------------------------- | | Exchange address | `wss://router.inference.ai.exchange/v1/provider/ws` | | `slug` | Your current handle. | | `api_key` | The provider key. | | `dispatch_url` | The address of your dispatch server. The scheme, host and port must match `declared_endpoint`. | | `capabilities` | `["handover"]`, if your agent meets the [handover requirements](#enable-the-handover-capability). Otherwise `[]`. | 5. Start the agent. Make sure that these frames arrive on the session: 1. `login_ack`. For `login_reject`, read its `reason` and repair the login. 2. `listed`. It names each deployment that you can quote. An `UNLISTED` deployment has `"unverified": true`. 3. `quote_ack` for each `quote`. For `quote_reject`, do the repair step for its `code`. 6. Keep the deployments reachable and quoted. The exchange measures each deployment, and an operator lists it. See [List a model](https://inference.ai.exchange/docs/provider-models.md). 7. [Set where you are paid](https://inference.ai.exchange/docs/provider-dispatch.md). To see each step work before you write your own agent, run the reference provider. ## Serve each kind of job A deployment has one kind. The agent calls a different route of your upstream for each kind. Register the kind with `kind` in [Register deployments](https://inference.ai.exchange/docs/provider-models.md). | Kind | `kind` | The agent calls | Charge | Read | | ------------- | --------------------- | ------------------------------------- | --------------------- | ------------------------------------------------- | | Chat | `chat` | `/chat/completions` | Tokens | [Serve and get paid](https://inference.ai.exchange/docs/provider-dispatch.md) | | Embeddings | `embedding` | `/embeddings` | Input tokens | [Serve and get paid](https://inference.ai.exchange/docs/provider-dispatch.md) | | Images | `image_generation` | `/images/generations` | Each image, or tokens | [Sell images](https://inference.ai.exchange/docs/provider-images.md) | | Speech | `audio_speech` | `/audio/speech` | Each character | [Sell speech](https://inference.ai.exchange/docs/provider-speech.md) | | Transcription | `audio_transcription` | `/audio/transcriptions` | Each second of audio | [Sell transcription](https://inference.ai.exchange/docs/provider-transcription.md) | | Video | `video_generation` | `/videos`, then a poll and `/content` | Each second of video | [Sell video](https://inference.ai.exchange/docs/provider-video.md) | Images, speech and embeddings can also use the [OpenAI-compatible path](https://inference.ai.exchange/docs/provider-openai.md), with no agent. Video and transcription need an agent. ## Change what you sell You do not have to restart the agent to add a deployment, change a price or pause a deployment. To move the dispatch server or rotate the key, [upgrade the agent](#upgrade-the-agent). | To do this | Do this | | ------------------------ | ---------------------------------------------------- | | Add a deployment | [Register it](#add-a-deployment) and quote it. | | Change a price | [Send a new `quote`](#change-a-price). | | Pause a deployment | [Pause it](#stop-to-sell-a-deployment). | | Move the dispatch server | [Follow the steps below](#move-the-dispatch-server). | | Rotate the key | [Follow the steps below](#rotate-the-key). | ### Add a deployment 1. [Register the deployment](https://inference.ai.exchange/docs/provider-models.md). 2. Wait for a new `listed` frame. The exchange compares the listing of each session about once a minute, and sends `listed` again when it changed. 3. Quote the new deployment. The exchange also sends `listed` again when an operator lists, suspends or reinstates a deployment. Quote only the deployments in the newest `listed` frame. ### Change a price Send a new `quote` for the deployment with a higher `revision`. The new terms replace the offer on the book. ### Stop to sell a deployment [Pause the deployment](https://inference.ai.exchange/docs/provider-models.md). The exchange removes its offer at once and keeps its registration and listing. ### Move the dispatch server If only the path changes, change `dispatch_url` in the agent configuration. Then [upgrade the agent](#upgrade-the-agent). If the scheme, host or port changes, do these steps: 1. Change `declared_endpoint` of the provider. See [Apply as a provider](https://inference.ai.exchange/docs/provider.md). 2. [Upgrade the agent](#upgrade-the-agent) to a configuration with the new `dispatch_url`. 3. Keep the old dispatch server until the old agent stops. The exchange sends jobs for the offers of the old session to the old `dispatch_url`. ### Rotate the key 1. Issue a new key. 2. [Upgrade the agent](#upgrade-the-agent) to a configuration with the new key. 3. Revoke the old key after the old agent stops. Do not revoke the old key first. The exchange closes the session of a revoked key within 10 seconds and removes its offers. ## Upgrade the agent Use this procedure for a new version of your agent and for a change to its configuration. ### With the handover capability The running agent and the new agent must both log in with `handover`. If the running agent did not, the new login replaces its session and removes its offers. The first upgrade to an agent with `handover` therefore has a gap in the book. 1. Start the new agent beside the running agent. Use the same handle. Give it a `dispatch_url` that reaches the new agent. See [Two agents and one endpoint](#two-agents-and-one-endpoint). 2. Wait for `login_ack` on the new session. Make sure that it has `"capabilities": ["handover"]`. 3. Let the new agent quote each deployment in its `listed` frame. Each quote takes over the offer of the old session, at any revision. 4. Wait for `superseded` on the old session. The exchange sends it when the new session holds each offer, and then closes the old session. 5. Let the old agent finish its current dispatches. 6. Stop the old agent. Until step 4, the old session keeps each offer that the new session did not quote. The old agent must serve awards on those offers. If the old session quotes a deployment that the new session took, it gets `quote_reject` with the code `superseded`. If the upgrade fails: - If the new session closes before it takes each offer, the old session is the current session again. The exchange removes the offers that the new session took and sends `listed` on the old session. The old agent must quote each deployment again. - The new session has no time limit before it takes its first offer. After that, if 30 seconds pass with no more offers taken, the exchange closes the old session and removes the offers that it still holds. ### Without the handover capability 1. Stop the old agent. The exchange removes its offers when the session closes. 2. Start the new agent. Its offers rest when it quotes. Your offers are absent from the book between the two steps. Do not run two agents without `handover` for one provider. Each login replaces the session of the other agent and removes its offers. ### Two agents and one endpoint The exchange sends each job to the `dispatch_url` of the session that quoted the offer. During an upgrade, jobs go to both agents. Both `dispatch_url` values must match the scheme, host and port of `declared_endpoint`. The path can differ. If a job must reach the agent that quoted it, do these steps: 1. Give each agent its own path, for example `https://provider.example.com/blue/dispatch` and `https://provider.example.com/green/dispatch`. 2. Configure your proxy to send each path to its agent. If any agent can serve any job, both agents can use one `dispatch_url`. ## When the exchange deploys a release The exchange sends `handover` to each session that logged in with it. Your agent then moves to the new process with no gap in the book. See [Move to a new exchange process](https://inference.ai.exchange/docs/provider-quoting.md#move-to-a-new-exchange-process). Without `handover`, your offers are absent from the book until the agent connects again and quotes. Connect again at once after the exchange closes the session. ## Enable the handover capability [Requirements for the handover capability](https://inference.ai.exchange/docs/provider-quoting.md#requirements-for-the-handover-capability) lists what your agent must do before it sends `["handover"]`. The reference provider implements them. Its source shows one way to share the state between two sessions. ### Test the capability Test a complete takeover: 1. Start your agent. Wait until each deployment has a `quote_ack`. 2. Start a second process of the agent, with its own `dispatch_url`. 3. Make sure that the second session gets `login_ack` with `"capabilities": ["handover"]` and a `quote_ack` for each deployment. 4. Make sure that the first session gets `superseded`, and that the first process does not log in again. Test a failed takeover: 1. Start your agent. Wait until each deployment has a `quote_ack`. 2. Start a second process of the agent, with its own `dispatch_url`. 3. Stop the second process before the first session gets `superseded`. 4. Make sure that the first session gets `listed` and quotes each deployment again. ## Next steps - [Quote over the session](https://inference.ai.exchange/docs/provider-quoting.md) - [Serve the work](https://inference.ai.exchange/docs/provider-dispatch.md) # Sell images This page shows how to sell images. Pick a path, give the agent the route, then register and quote the deployment. [Quote a media model](https://inference.ai.exchange/docs/provider-media-quote.md) has the features, the sizes and the prices. ## Choose a path | Path | Use it when | | ---------------------------------------------------- | ------------------------------------------------------------------------------------------- | | [OpenAI-compatible API](https://inference.ai.exchange/docs/provider-openai.md#images) | Your API has `POST /images/generations` and answers in the OpenAI format. You run no agent. | | [Your own agent](https://inference.ai.exchange/docs/provider-dispatch.md#images) | Your upstream has other routes. Send an `image` event for each image. | The rest of this page is for the reference agent. It reads these routes from your upstream. ## Give the agent this route The agent calls `POST {base}/images/generations` with `Authorization: Bearer `. Set `{base}` in `LIQUID_UPSTREAM_BASE_URL`. ```json { "model": "acme-image-1", "prompt": "A trading floor made of water", "n": 2, "size": "1024x1024", "output_format": "png", "response_format": "b64_json" } ``` - `n` is always present. The other fields are present only when the buyer set them. - When the buyer gave reference images, the agent adds `images`, a list of `{ "image_url": … }`. - When the buyer sent `resolution` or `aspect_ratio`, `size` is the pixel size that the exchange picked. - The agent sends `seed`, `background` and `output_compression` only for a control that you declared. See [Declare the controls](#declare-the-controls). - The agent adds the fixed fields that you pin for the endpoint. Answer with `data`, a list of images. Each image has `b64_json` or `url`. An entry with neither is not an image, and the agent does not count it. ```json { "data": [{ "b64_json": "iVBORw0KGgo…" }, { "b64_json": "iVBORw0KGgo…" }] } ``` The agent charges the images that you return, not the images that the buyer asked for. If `data` is empty, the job fails and the exchange tries the next offer. ## Declare the controls A buyer's `seed`, `background`, `output_compression` and `stream` reach only an offer that declared `image.seed`, `image.background`, `image.output_compression` or `image.stream`. The first-party OpenAI adapter declares `image.background` and `image.output_compression` for a `gpt-image` model, and `image.seed` when the route has a seed rule. It never declares `image.stream`. To send draft images for `image.stream`, write your own agent and send `image_partial` events. See [Images](https://inference.ai.exchange/docs/provider-dispatch.md#images). ## Register and quote Register the deployment with `kind` set to `image_generation`. List the `sizes` and `formats` that it makes, and `max_count`. The tiers and shapes that buyers ask for come from `sizes`. [Register deployments](https://inference.ai.exchange/docs/provider-models.md) has the fields. - To price for each image, quote an `output_image` line in `pricing`. Add a `variant` with `size` or `quality` for a price that depends on them. - If your upstream bills tokens, set `pricing_unit` to `tokens` and `image_output_tokens_max`. Quote `input_text` and `output_text` lines in `pricing`. Your answer must then include `usage`, or the job fails. See [Quote a media model](https://inference.ai.exchange/docs/provider-media-quote.md#an-image-model) for an example. ## Limits - The agent reads the whole answer before it sends the first image. A call that sends no byte for two minutes fails. - A failed call ends the attempt. The exchange charges nothing and tries the next offer. - The reference agent sends no partial image. # Sell speech This page is for a provider that turns text into audio. It tells you which path to use, what your upstream must answer, and how to register and quote the deployment. [Quote a media model](https://inference.ai.exchange/docs/provider-media-quote.md) has the features and the prices. ## Choose a path | Path | Use it when | | ---------------------------------------------------- | ------------------------------------------------------------------------------------- | | [OpenAI-compatible API](https://inference.ai.exchange/docs/provider-openai.md#speech) | Your API has `POST /audio/speech` and answers in the OpenAI format. You run no agent. | | [Your own agent](https://inference.ai.exchange/docs/provider-dispatch.md#speech) | Your upstream has other routes. Send one `audio` event with the clip. | The rest of this page is for the reference agent. It reads these routes from your upstream. ## Give the agent this route The agent calls `POST {base}/audio/speech` with `Authorization: Bearer `. Set `{base}` in `LIQUID_UPSTREAM_BASE_URL`. ```json { "model": "acme-tts-1", "input": "The auction is open.", "voice": "alloy", "response_format": "mp3", "speed": 1.0 } ``` `input` is the last user turn. `response_format` and `speed` are present only when the buyer set them. The agent sends `instructions` only if you declared `speech.instructions`. Answer with the audio file as the body, not JSON. Set `Content-Type` to the type of the file, for example `audio/mpeg`. The buyer gets that type. If you send none, the agent uses the type of the format that was asked for, or `audio/mpeg`. An empty body fails the job. ## Declare the controls - `speech.instructions`: the buyer's style line. The first-party OpenAI adapter declares it for a model whose name contains `gpt-4o`. - `speech.turns`: a clip with more than one voice. No first-party adapter declares it, because they speak one voice for each clip. To sell it, write your own agent. The dispatch holds a list of `turns`. See [Speech](https://inference.ai.exchange/docs/provider-dispatch.md#speech). ## Register and quote Register the deployment with `kind` set to `audio_speech`. List the `voices` and `formats` that it makes. [Register deployments](https://inference.ai.exchange/docs/provider-models.md) has the fields. Quote one `output_speech` line in `pricing`. The charge is that rate multiplied by the characters of the text of all turns. The exchange counts them before dispatch, so the price cap is exactly the charge. Your upstream reports no count, and the agent meters the characters that it received. To price a quality apart, add a line with `variant` `{"quality": "hd"}`. A request that states `quality` pays the line whose variant it matches. A request that states none, or a name that you do not price, pays the line with no variant. See [Quote a media model](https://inference.ai.exchange/docs/provider-media-quote.md#a-speech-model). ## Limits - Speech does not stream. The agent reads the whole file, then sends it as one clip. - A request for a voice or a format that you did not list goes to another offer before the award. - A failed call ends the attempt. The exchange charges nothing and tries the next offer. # Sell transcription This page is for a provider that turns audio into text. It tells you why you need an agent, what your upstream must answer, and how to register the deployment. ## Use an agent A transcription job needs an agent. The [OpenAI-compatible path](https://inference.ai.exchange/docs/provider-openai.md) does not carry it, and the exchange sends the job to the next offer. The agent reads the audio from the job, posts it to your upstream, and reports the seconds that it transcribed. ## Give the agent this route The agent calls `POST {base}/audio/transcriptions` with `Authorization: Bearer `. Set `{base}` in `LIQUID_UPSTREAM_BASE_URL`. The body is `multipart/form-data`: | Field | Value | | --------------------------- | -------------------------------------------------------------------- | | `file` | The audio, named `audio.`. A job has exactly one audio part. | | `model` | The upstream model of the deployment. | | `response_format` | Always `verbose_json`. | | `language`, `prompt` | Present only when the buyer set them. | | `temperature` | Present only when the buyer set it. | | `timestamp_granularities[]` | One field for each granularity that the buyer asked for. | Your upstream must support `verbose_json`. The job is metered on its duration, and only that form holds the duration. Answer with JSON: ```json { "text": "The auction is open.", "language": "en", "duration": 3.4, "segments": [{ "id": 0, "start": 0.0, "end": 3.4, "text": "The auction is open." }], "words": [{ "word": "The", "start": 0.0, "end": 0.2 }] } ``` - `text` is required. - `duration`, in seconds, is required. The agent also accepts `usage.seconds`. An answer with neither fails the job. - `language`, `segments` and `words` are optional. The agent rounds the duration up to whole seconds and meters that number. ## Declare the controls - `transcription.diarize`: the buyer asked for a `speaker` on each segment. The dispatch has `diarize: true`. - `transcription.keyterms`: the buyer gave words that the transcript must spell as given. The dispatch has a list of `keyterms`. No first-party adapter declares them, and the reference agent does not send them. To sell them, write your own agent. See [Quote a media model](https://inference.ai.exchange/docs/provider-media-quote.md#4-what-your-agent-receives). ## Register and quote Register the deployment with `kind` set to `audio_transcription`. In `input_modalities`, name `audio` and state its limits, such as `formats`, `max_bytes` and `max_duration_seconds`. [Register deployments](https://inference.ai.exchange/docs/provider-models.md) has the fields. Quote an `input_audio` line in `pricing`. The charge is that rate multiplied by the seconds of audio. A job has no context window and no token work. ## Limits - A transcription does not stream. The agent sends the transcript as one event. - A failed call or an answer with no duration ends the attempt. The exchange charges nothing and tries the next offer. - To write your own agent, send one `transcript` event and meter `audio_seconds`. See [Transcription](https://inference.ai.exchange/docs/provider-dispatch.md#transcription). # Sell video This page is for a provider that renders video. Use the reference agent if your upstream has the three OpenAI video routes below. Then register the deployment and test it. ## Use an agent A video job needs an agent. The [OpenAI-compatible path](https://inference.ai.exchange/docs/provider-openai.md) does not carry video jobs, and the exchange sends them to the next offer. A render takes minutes, and the buyer downloads the file later. The agent does three jobs that a plain API cannot do: - It starts the render and waits for it to end. - It reports the seconds that you rendered, so that the job settles. - It names the file to the exchange and serves the file when the buyer downloads it. Choose one of two agents: | Choice | Use it when | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | The reference provider agent | Your upstream has the OpenAI video routes that this page describes. You write no code. | | Your own agent | Your upstream has other routes. Follow the [Video section of the dispatch page](https://inference.ai.exchange/docs/provider-dispatch.md#video) and send `video_ready` yourself. | The rest of this page is for the reference agent. ## Give the agent these routes Set the base URL of your upstream in `LIQUID_UPSTREAM_BASE_URL`. The agent adds the paths below to it and sends your key as `Authorization: Bearer `. | The agent calls | Your answer | | -------------------------------- | -------------------------------------------------- | | `POST {base}/videos` | A JSON job with an `id`. | | `GET {base}/videos/{id}` | The job with its `status`. | | `GET {base}/videos/{id}/content` | The file, with a `Content-Type` that is `video/*`. | ### Create a job The agent sends only the fields in this example. ```json { "model": "acme-video-1", "prompt": "A trading floor made of water", "seconds": "8", "size": "1280x720", "input_reference": { "image_url": "https://example.com/start.png" } } ``` | Field | Value | | ----------------- | -------------------------------------------------------------------- | | `model` | The upstream model of the deployment. | | `seconds` | A string, as in the OpenAI API. | | `size` | Present only when the buyer asked for a size. | | `input_reference` | Present only when the buyer gave a start image. See the rules below. | The agent sends at most one start image. It accepts an image with no frame type or with the frame `first_frame`. It fails the job for the frame `last_frame`. Answer `2xx` with a JSON body that has an `id`. A status that is not `2xx`, or a body with no `id`, ends the attempt. ### Report the status The agent asks for the job at a fixed interval. The `status` must be one of these values: | `status` | Meaning | | ------------- | -------------------------------------------------- | | `queued` | The job waits. The agent asks again. | | `in_progress` | The job renders. The agent asks again. | | `completed` | The file is ready. | | `failed` | The job failed. Put the reason in `error.message`. | Any other value fails the job. - A `5xx` answer or a network error on a poll is tried again a few times. Then the job fails. - A `4xx` answer fails the job at once. When the job is `completed`, you can add `expires_at`, in Unix seconds. It is the time when you delete the file. ### Serve the file The agent reads the headers of the file before it tells the exchange that the video exists. Then the buyer is charged. Make sure that `GET {base}/videos/{id}/content` works when the status is `completed`. - Send a `Content-Type` that starts with `video/`. Any other type, or an empty file, fails the job. - Keep the file until `expires_at`. The buyer can download it after the job ends. ### Bill fewer seconds The agent charges the seconds that the buyer asked for. To charge fewer, add this header to the `POST` answer: ```text x-new-api-other-ratios: {"seconds": 6} ``` The agent rounds the value up. It never charges more seconds than the buyer asked for, because the exchange does not pay for them. ## Frames and other references The OpenAI video routes take one start image. The reference agent does not send a last frame, audio or a video reference on this route. The exchange sends your deployment only the frames that you declared in `supported_frame_images`. Declare none that the route cannot read. To read more, write your own agent. A part of the dispatch has `frame_type` when it is a frame image, and you map it to your upstream fields. See [Frame images](https://inference.ai.exchange/docs/provider-models.md#frame-images). ## Declare the controls A buyer's `seed`, `creativity`, `upscale_factor` and `previous_job_id` reach only an offer that declared `video.seed`, `video.creativity`, `video.upscale_factor` or `video.continue`. The first-party OpenAI adapter declares `video.seed` when the route has a seed rule. It declares no other video control. To sell the others, write your own agent. See [Quote a media model](https://inference.ai.exchange/docs/provider-media-quote.md). ## Register the deployment Register the deployment with `kind` set to `video_generation`. Make `deployment_name` and `model` equal, because the agent asks the upstream for that name. The lengths and sizes you list are the only ones that can win. ```bash curl https://router.inference.ai.exchange/v1/provider/models \ -H "Authorization: Bearer $LIQUID_PROVIDER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"deployment_name":"acme-video-1", "model":"acme-video-1", "kind":"video_generation", "precision":"provider-managed","region":"us", "max_input_tokens":2000,"max_output_tokens":1, "output_modalities":[{"type":"video","seconds":[4,8], "sizes":["1280x720"],"formats":["video/mp4"]}]}' ``` [Register deployments](https://inference.ai.exchange/docs/provider-models.md) lists every field. Quote an `output_video` line in `pricing` for the deployment. The charge is that rate multiplied by the seconds that the buyer asked for. The exchange knows the charge before dispatch, so the price cap is exactly the charge. [Quote a media model](https://inference.ai.exchange/docs/provider-media-quote.md#a-video-model) has an example with variants. If your upstream lets the buyer choose the sound or the resolution, state `audio` and `resolutions` in the `video` output and quote `output_video` lines with a `variant` for the options that change the price. The buyer's choice is in `audio` and `resolution` of the `video` object of the dispatch. See [Sound, resolution and price](#sound-resolution-and-price). ## Continue a video A buyer can send `previous_job_id` to continue an earlier video. Declare `video.continue` only if your upstream can do this. - The dispatch holds `video.previous_job_id`. It is the job id that the exchange sent to you in the dispatch of the earlier video. - The exchange checks that the earlier job completed and belongs to the same workspace. It does not check that the earlier job ran on your deployment, and it sends no file or handle. - Keep a map from each job id to your own handle. If you do not know the id, send a `failed` event. The job costs nothing and the exchange tries the next offer. - Each job is charged for its own seconds. ## Cancel and expiry The exchange sends no cancel message. It closes the dispatch connection in these cases: - The buyer calls `POST /v1/videos/{id}/cancel`. The exchange closes the connection within a few seconds. The job costs nothing. - The job runs past the video job limit, 10 minutes by default. The buyer sees `expired`. The job costs nothing, and it counts as a failure of your deployment. Watch for the close. When it comes, stop the work on your upstream if your upstream has a way to cancel. If it has none, you carry the cost of the render. ## Start the agent 1. Set `LIQUID_UPSTREAM_BASE_URL` and the key of your upstream. 2. Set `LIQUID_CONTENT_KEY` to a stable secret. The agent signs the download token with it. Keep the same value after a restart. If the value changes, the buyer cannot download a video that is already paid for. 3. Start the agent and wait for `listed`, then quote the deployment. See [Run your agent](https://inference.ai.exchange/docs/provider-operate.md). 4. Keep the agent running for as long as your files stay on the upstream. The buyer downloads through the agent. ## Limits and failures - The exchange limits the whole job, by default to ten minutes. The agent gives up before that limit and reports its own reason. - If the exchange closes the dispatch, the agent stops to wait. Your upstream keeps rendering, because no cancel route works on every gateway. You carry the cost of that render. See [Cancel and expiry](#cancel-and-expiry). - The exchange can use the next offer only before the first output. After the agent reports a finished video, the job does not move to another provider. - These failures end the attempt: a failed job, a wrong status, a file that is not `video/*`, and a missing `id`. The exchange charges nothing and tries the next offer. ## Test before you quote Run these calls against your upstream. Each one must give the answer that this page describes. ```bash curl -s $BASE/videos -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \ -d '{"model":"acme-video-1","prompt":"test","seconds":"4"}' curl -s $BASE/videos/$ID -H "Authorization: Bearer $KEY" curl -sI $BASE/videos/$ID/content -H "Authorization: Bearer $KEY" ``` Then buy one video from your own deployment with a small price cap, and download it. The buyer's [video guide](https://inference.ai.exchange/docs/video-generation.md) shows the calls. # Serve and get paid When your offer wins, the exchange announces the award on your session and sends the job to your dispatch endpoint over HTTP. You stream the answer back. The exchange pays for the usage that you report. ## Award notice ```json { "type": "award_notice", "award_id": "aw_…", "job_id": "job_…", "offer_id": "3f0c…", "quote_revision": 7, "activation_seq": 12, "served_model": "gpt-oss-120b", "cap_usd": 0.0184, "max_output_tokens": 1024 } ``` - `quote_revision` and `activation_seq` name the winning quote. `activation_seq` comes from its `quote_ack`. - `cap_usd` is the maximum charge. The exchange fixes it before the buyer receives output. - The exchange settles at the winning rates, even if you quote again. Keep the terms of each revision. - The notice and the dispatch request can arrive in any order, and twice. Join them on `award_id`. ## Dispatch request The exchange sends a `POST` with a JSON body to your `dispatch_url`. The `idempotency-key` header holds the award id. ```json { "award_id": "aw_…", "job_id": "job_…", "offer_id": "3f0c…", "served_model": "gpt-oss-120b", "messages": [ { "role": "user", "content": "What is in this picture?", "parts": [ { "type": "text", "text": "What is in this picture?" }, { "type": "image_url", "url": "https://example.com/photo.jpg", "detail": "auto" } ] } ], "max_output_tokens": 1024, "temperature": 0.2, "stream": true } ``` The exchange sends a parameter only to offers that declared it. | Field | Description | | -------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `pricing_unit` | The unit of the winning offer, if unusual for the kind. `tokens` on an image job means a token charge. | | `messages[].content` | The text of the turn. | | `messages[].parts` | Content that is not text. See [Attachments](#attachments). | | `messages[].tool_calls`, `messages[].tool_call_id` | The tool calls of an assistant turn, and the call that a `tool` turn answers. | | `messages[].reasoning` | Reasoning blocks from the buyer. See [Reasoning](#reasoning). | | `max_output_tokens` | The limit for the cap. The exchange charges output above it to you. | | `tools`, `tool_choice` | Tool definitions in the Anthropic `input_schema` form, and the buyer's `tool_choice`. Only with `tools`. | | `response_format` | The buyer's `response_format`. | | `top_p`, `top_k`, `min_p`, `repetition_penalty`, `stop`, `seed`, `logprobs`, `top_logprobs`, `logit_bias`, `frequency_penalty`, `presence_penalty` | Sampling parameters. Each needs the related `sampling.*` feature. | | `reasoning` | `{ "effort": … }` or `{ "budget_tokens": … }`. `effort` is always a string, such as `"80"`. | | `cache` | Cache breakpoints. See [Prompt caching](#prompt-caching). | | `audio_output` | `{ "voice": …, "format": … }`. The buyer asked for speech with the text. Only with `output.audio`. | | `image`, `audio`, `video` | The output to produce, only on a deployment of that kind. See [Images](#images), [Speech](#speech) and [Video](#video). | ### Attachments A turn with more than text has a `parts` array next to its `content`. `content` still holds the text. | Part | Fields | Sent when the offer declared | | ------------- | ----------------------------------- | ---------------------------- | | `text` | `text` | always | | `image_url` | `url`, optional `detail` | `input.image` | | `input_audio` | `data` (base64), `format` | `input.audio` | | `file` | `data`, `mime`, optional `filename` | `input.file` | | `video_url` | `url` | `input.video` | `url` is a link or a `data:` URL, as the buyer sent it. Only images can be links. You get an attachment only when your offer declared the feature and your registration admits it. You get `video_url` only if you declare `input.video`. Today only the OpenAI adapter does so, and only when video input is on or the model card route names video. ### Prompt caching `cache.breakpoints` lists up to four breakpoints in prompt order. Each has an `anchor` and a `ttl` of `5m` or `1h`: ```json { "breakpoints": [ { "anchor": { "at": "tools" }, "ttl": "1h" }, { "anchor": { "at": "message", "index": 5 }, "ttl": "5m" } ] } ``` `tools` is the end of the tool definitions. `message` is the end of `messages[index]`. For an Anthropic upstream, put `cache_control` on the last block before each breakpoint. - `cache.key` groups requests that share a prefix. Send it to an upstream that routes by it, such as `prompt_cache_key` of OpenAI. - `cache.scope` is unique to the buyer. If your upstream account serves more than one buyer, put it before all cached content. Otherwise one buyer can learn what another sent. Anthropic dialect: send it as the first system block, and move each breakpoint after the tools to the end of the system prompt. Automatic caching: send it as the first system message. Do not cache past the deepest breakpoint, because the cap covers writes only up to it. If your upstream charges for writes, quote `cache_write_short` and `cache_write_long` in `pricing`. A job that reports a write with no rate does not settle. ### Reasoning An assistant turn can have `reasoning` blocks: | `type` | Fields | | --------------------- | ----------------------------------------------- | | `reasoning.text` | `text`, optional `signature`, optional `format` | | `reasoning.encrypted` | `data`, optional `format` | `format` names the models that can read the `signature` or `data`, such as `anthropic-claude-v1`. With thinking on, Anthropic refuses the next turn of a tool-use loop unless the signed blocks come first and unchanged. Send back blocks in your upstream format, and drop the others. ## Response Respond with `200` and `Content-Type: application/x-ndjson`. Write one JSON event on each line. Always stream, whatever `stream` says. The exchange buffers the output for buyers who did not ask for streaming. The exchange closes a dispatch that is silent for 120 seconds, and limits a chat job to one hour by default. A streaming buyer pays for the output received before a limit. ``` {"type":"reasoning","text":"The image shows a bicycle. "} {"type":"delta","text":"There is a "} {"type":"delta","text":"red bicycle."} {"type":"done","finish_reason":"stop","usage":{"meters":{"uncached_input":412,"cache_read":0,"cache_write_short":0,"cache_write_long":0,"generation":6},"tokenizer_id":"o200k_base","reasoning_tokens":null,"raw":{}}} ``` | Event | Fields | Description | | --------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `delta` | `text` | Output text. The first `delta` stops the time-to-first-token clock. | | `logprobs` | `content` | Log probabilities for the next `delta`. See [Log probabilities](#log-probabilities). It does not stop the clock. | | `reasoning` | `text` | Model reasoning, if the upstream returns it. It does not stop the clock, but after it the job cannot change provider. | | `audio_delta` | `data` (base64), `transcript`, `id` | A piece of the spoken answer to an `audio_output` request. It has `data`, `transcript` or both. Only the first piece has the `id`. It stops the clock and ends any change of offer. Report `audio_tokens` in `completion_tokens_details`. | | `reasoning_signature` | `signature`, `format` | Ends the reasoning block with the upstream signature. The next `reasoning` starts a new block. | | `reasoning_redacted` | `data`, `format` | An encrypted reasoning block. The buyer cannot read it, but must send it back. | | `tool_call_start` | `index`, `id`, `name` | A tool call starts. `index` orders concurrent calls. | | `tool_call_delta` | `index`, `arguments` | A part of the argument JSON. Join the parts for each `index`. | | `image` | `index`, `b64_json` or `url`, `mime`, optional `revised_prompt` | One complete image. `index` is its position in the requested images. | | `audio` | `b64_audio`, `mime` | The complete clip. | | `video_ready` | `handle`, `token`, `mime`, optional `bytes`, `expires_at` | The finished video. It holds no bytes. | | `done` | `finish_reason`, `usage` | Success. Terminal. | | `failed` | `code`, `message` | Failure. Terminal. After you send output, the exchange does not try another provider. | ### Log probabilities If the dispatch has `logprobs: true`, send a `logprobs` event before each `delta` whose tokens have log probabilities. `content` lists the tokens of that `delta` in order: ```json { "token": "A", "logprob": -0.12, "bytes": [65], "top_logprobs": [{ "token": "B", "logprob": -2.4, "bytes": [66] }] } ``` `top_logprobs` has at most the number of entries that the dispatch asks for, and can be empty. `bytes` is optional. Send no entry for reasoning or tool-call tokens. If your offer declared `sampling.logprobs` and the answer has text, send the first `logprobs` event before the first `delta`. - If you do not, the attempt fails with `logprobs_missing` before the buyer gets output. The exchange tries the next offer and ranks the model out for a short time. - If the events stop after the first `delta`, the job settles and the exchange records the failure. - ### Usage - `meters`: token counts for `uncached_input`, `cache_read`, `cache_write_short`, `cache_write_long` and `generation`. Exactly one meter counts each token. - `tokenizer_id`: must equal the registered `tokenizer_id`. - `reasoning_tokens`: optional. The reasoning tokens in `generation`. - `raw`: the upstream usage object, unchanged. The exchange keeps it as evidence. The exchange settles on `meters`, at the winning rates. ## Images An image dispatch has an `image` object: ```json { "count": 2, "size": "1024x1024", "output_format": "png", "response_format": "b64_json" } ``` The exchange used `count`, `size` and `output_format` to match your offer and set the price. It does not pay for more images or a different size. `size` is the pixel size that the exchange picked when the buyer sent `resolution` or `aspect_ratio`. These fields are present only when the buyer set them and your offer declared the feature: | Field | Feature | What to do | | -------------------- | -------------------------- | ------------------------------------------------------------------------------------ | | `seed` | `image.seed` | Pass it to your upstream. | | `background` | `image.background` | `transparent` or `opaque`. The exchange drops `auto`. | | `output_compression` | `image.output_compression` | A whole number from 0 to 100, for a `jpeg` or `webp` image. | | `stream` | `image.stream` | `true`. The buyer asked for one image. Send drafts as `image_partial` events if any. | `quality` is present when the buyer set it. It sets the price and is not matched. To send a draft, send an `image_partial` event before the final `image`: ``` {"type":"image_partial","index":0,"partial_index":0,"b64_json":"iVBOR…","mime":"image/png"} ``` A draft is valid only if the buyer set `stream`. `index` is below `count`, `partial_index` is 0, 1 or 2, and you send each pair once. Do not send a draft after the final image of the same `index`. A draft with no bytes, or any other break of these rules, fails the job. A draft is not an image: `meters.images` counts the final images only. Send each image complete in one `image` event, with exactly one of `b64_json` and `url`. The first image stops the time-to-first-token clock. Then send `done` with `meters.images` set to the number sent. The charge is that count times the `output_image` price. If you meter more images than you sent, the job fails. When `pricing_unit` is `tokens`, also set `meters.uncached_input` and `meters.generation`. A job with no image fails. ## Speech A speech dispatch has an `audio` object: ```json { "characters": 18, "voice": "alloy", "response_format": "mp3", "speed": 1.0 } ``` The exchange used `voice` and `response_format` to match your offer. `characters` is the length of the text of all turns together, and it is the quantity charged. An empty `voice` means that the buyer named none: speak in the first voice that you declared. - `quality` sets the price and is not matched. - `instructions` is a style line for the clip. It is present only with `speech.instructions`. - `turns` is a list of `{voice?, instructions?}`, one for each turn, and turn n speaks user message n. It is present only for more than one turn, and only with `speech.turns`. Send one `audio` event with the full clip as base64. Then send `done` with `meters.characters` set to the count you received. The charge is that count times the `output_speech` price. The job fails if you meter more characters, send no clip, send invalid base64, or send two clips. ## Transcription A transcription dispatch has a `transcription` object. The audio is one `input_audio` part in the messages. ```json { "seconds": 4, "format": "mp3", "language": "en", "timestamps": ["segment"], "diarize": true, "keyterms": ["Liquid Inference"] } ``` `seconds` is the length that the exchange measured, rounded up. It is the cap. `language`, `timestamps`, `prompt` and `temperature` are present only when the buyer set them. `diarize` and `keyterms` are present only with `transcription.diarize` and `transcription.keyterms`. Send one `transcript` event with `text`, and `language`, `segments` and `words` if you have them. A segment has `id`, `start`, `end` and `text`, and a `speaker` such as `speaker_0` when `diarize` is true. Always return the full form. The exchange builds the format that the buyer asked for. Then send `done` with `meters.audio_seconds` set to the seconds that you transcribed. It must be more than 0. The charge is that count times your `input_audio` price, and not more than the cap. Times must be finite, and `end` must not be before `start`. ## Embeddings An embeddings dispatch has an `embedding` object: `inputs` (the number of texts), `dimensions`, `encoding_format` and `input_type`. `input_type` is present only with `embedding.input_type`, and it is one of `search_query`, `search_document`, `classification` and `clustering`. Send one or more `embeddings` events with the vectors, in order. Send exactly one vector for each input. Meter input tokens only. ## Video If your upstream has the OpenAI video routes, the reference agent does this for you. See [Sell video](https://inference.ai.exchange/docs/provider-video.md). A video dispatch has a `video` object: ```json { "seconds": 8, "size": "1280x720", "audio": true, "resolution": "720p", "seed": 7 } ``` The exchange used `seconds` and `size` to match your offer and set the price. `audio` and `resolution` are the buyer's choice of sound and resolution. `size` is the pixel size that the exchange picked when the buyer sent `aspect_ratio`. The other fields below are present only when the buyer set them and your offer declared the feature. | Field | Feature | What to do | | ----------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `seed` | `video.seed` | A whole number of 0 or more. Pass it to your upstream. | | `creativity` | `video.creativity` | A number from 0 to 1. | | `upscale_factor` | `video.upscale_factor` | A whole number of 2 or more. | | `previous_job_id` | `video.continue` | The job id of an earlier video of the same workspace that completed. See [Continue a video](https://inference.ai.exchange/docs/provider-video.md#continue-a-video). | `reference_images`, `reference_video_seconds` and `reference_audio_seconds` are the most that you can meter for references. The prompt is the last user turn. The buyer's references are content parts of that turn, in the order sent. A frame image comes first, with a `frame_type` of `first_frame` or `last_frame`. See [Frame images](https://inference.ai.exchange/docs/provider-models.md#frame-images). A render can take minutes. While it runs, send a blank line at least every 60 seconds. The exchange ignores blank lines, but closes a dispatch that is silent for 120 seconds. It limits the job to ten minutes by default. If it closes the dispatch, stop the work. The exchange closes the dispatch in the same way when the buyer cancels the job, and it sends no other message. See [Cancel and expiry](https://inference.ai.exchange/docs/provider-video.md#cancel-and-expiry). Do not send the video in the event stream. When your upstream has the file, send one `video_ready` event. Then send `done` with `meters.video_seconds`. The charge is that count times your `output_video` price. ``` {"type":"video_ready","handle":"task_1","token":"5f2c…","mime":"video/mp4","bytes":7195859} {"type":"done","finish_reason":"stop","usage":{"meters":{"video_seconds":8},"tokenizer_id":"provider-native","raw":{}}} ``` - `handle` is your name for the file. - `token` proves that a later request comes from the exchange. Make it from the handle with a key that only you know, such as an HMAC. Keep the key across restarts, or the buyer cannot download a paid video. - `bytes` and `expires_at` are optional. Send `expires_at` if your upstream deletes the file at a known time. The job fails and does not settle if you meter more seconds than asked, send no `video_ready` or two, name an empty file or one that is not `video/*`, or send an event after `done`. When the buyer downloads the video, the exchange sends a `POST` with `{"handle": …, "token": …}` to `content` next to your dispatch path (`/v1/dispatch` becomes `/v1/content`). Check the token, then send the file with its `Content-Type`. Answer any other token with `403`. Keep the file until `expires_at`. ## Idempotency The exchange can send a dispatch twice. Record award ids and refuse a repeat before upstream work starts. The reference agent answers `409 duplicate_award`. ## Failures ### What ends an attempt - A non-`2xx` status, or a connection that does not open, ends your attempt. The exchange tries the next offer. - If no other offer can take the job, the buyer gets `503 provider_failed`. A `4xx` status does not change this, except `tools_unsupported`. - If your endpoint accepts the dispatch and then stops with no event, the job fails with `503 provider_failed`. The exchange does not try another offer, because you may still run the job. ### Report an upstream error Send a `failed` event. Start its `message` with ` returned : `, then the upstream body. A client error is a `4xx` status other than `401`, `402`, `403`, `404` or `429`. The buyer gets `400` with one of these codes: - `tools_unsupported`: the upstream says the model cannot use tools. With no output sent, the exchange tries the next offer. - `request_rejected`: any other client error. The buyer must fix it, so the exchange does not try another offer. - `upstream_account_limited`: your upstream account reached a spend limit or has no funds. See below. Other failures give `503 provider_failed`: an unreachable provider, a provider fault, and the upstream statuses `401`, `402`, `403`, `404` and `429`. A `402` means your upstream account cannot pay. A `404` means your upstream lacks an offered model. With no output sent, the exchange first tries the next offer. ### Account limits The exchange also recognizes the usual words of an account-limit error, such as `spend limit`, at any status. The buyer never sees the `message`, and the receipt keeps only the code. Until you raise the limit or add funds, each attempt that you win for that upstream fails. ### What counts against your deployment A failure counts against your deployment health only when you caused it. A `request_rejected` failure and a fault of the exchange do not count. A late answer counts as slow, not as a failure. An attempt that moved to the next offer counts. ### Bad tool calls and bad JSON The exchange also checks your answers. A bad answer does not change the job or its charge. The rates show in your console. They change the ranking only if the buyer sets `exchange.rank.failure_risk_ranking` ([Purchasing rules](https://inference.ai.exchange/docs/rules.md)). - **Tool calls.** A call is bad when its arguments are not JSON, it names a tool that the request did not offer, or its arguments break the tool schema. - **JSON output.** For `json_object` or `json_schema`, the answer is bad when it is not one JSON value or does not match the schema. A `max_tokens` stop, a refusal and tool calls alone do not count. ### Reference agent behavior When an upstream refuses a job with `upstream_account_limited`, `401`, `402` or `403`, the reference agent marks the account as down for 5 minutes. It quotes each model through the next cheapest upstream that sells it, and withdraws models that no other upstream sells. After the cooldown, the next job tests the account. Each failure doubles the cooldown, up to 1 hour, and a success resets it. The state is in memory. ## Settlement The exchange settles each job on `done`. Your earnings are the reported meters at the winning rates, plus any fixed fee, minus buyer credits. The exchange fee is a separate line that the buyer pays. You then get a `settlement_notice`: ```json { "type": "settlement_notice", "award_id": "aw_…", "job_id": "job_…", "meters": { "uncached_input": 412, "cache_read": 0, "cache_write_short": 0, "cache_write_long": 0, "generation": 6 }, "supplier_charge_usd": 0.0000523, "credit_usd": 0, "status": "settled" } ``` `supplier_charge_usd` is your earnings before the exchange fee. `credit_usd` is the credit that the buyer gets when the provider fails after it sent output. | Condition | Effect | | --------------------------------------------------- | --------------------------------------------------------- | | Output above `max_output_tokens` | The exchange charges the overshoot to you. | | Missed `ttft_ms` or `completion_ms` deadline | The buyer pays. The exchange marks you slow. | | Usage in an unregistered tokenizer | The exchange does not settle it as the same unit. | | Failed job, or failure after output that you caused | Nobody pays and nobody earns. The buyer keeps any output. | | The buyer closes a stream after output | The buyer pays for the output relayed, and you earn it. | The **Work** page of the console shows the settled values. To copy them into your own systems, read the jobs feed. ### Read the jobs feed `GET /v1/provider/jobs` returns your settled jobs, oldest first. It takes the provider key of your agent: ```bash curl "https://router.inference.ai.exchange/v1/provider/jobs?limit=500" \ -H "Authorization: Bearer $LIQUID_PROVIDER_KEY" ``` ```json { "jobs": [ { "job_id": "job_…", "award_id": "aw_…", "served_model": "deepseek-v4.1", "outcome": "completed", "status": "settled", "supplier_charge_usd": 0.0000523, "buyer_charge_usd": 0.0000601, "fee_usd": 0.0000078, "credit_usd": 0, "meters": { "uncached_input": 412, "generation": 6 }, "ttft_ms": 412, "completion_ms": 1290 } ], "next_cursor": "1793628191204000000:job_…", "has_more": false } ``` 1. Read without a cursor. The first read starts 90 days ago. 2. Send `next_cursor` as `cursor` on the next read. 3. While `has_more` is `true`, read again at once. Otherwise wait at least one minute. - A job appears five minutes after it settles and never changes. Its fields match the `settlement_notice`, so join on `award_id`. The feed does not name the buyer. - A read during another read gets `429` with `Retry-After`. Wait that many seconds and read again. ### Time to first token The buyer's `ttft_ms` deadline counts from the start of the job, so it includes earlier failed offers. Your own time is `ttft_ms` less `attempt_started_ms` on the receipt. The exchange marks you slow only when your own attempt passed the deadline. ## Payout destination You set your payout destination through the hosted onboarding of the payout rail. Operators can read and clear a destination, but not set one. These endpoints use the browser session and need manage access. `PUT` and `POST` also need a recent identity check. ### Start onboarding ``` POST /v1/account/providers/{provider_id}/payout/onboarding ``` Returns a URL for Stripe Connect onboarding. You give your identity, business and bank details to Stripe, not to the exchange. The exchange continues the stored account if the rail opened it for this exchange. Otherwise it opens and stores a new account. The account is a company account for the `hq_country` of the provider. That country sets the agreement with Stripe: - The US, Canada, the UK, Switzerland and the EEA (except Iceland): **full** agreement. Transfers arrive on the normal Stripe schedule. - Any other country, including Iceland: **recipient** agreement. The account only receives transfers, and each one is available 24 hours later than usual. - Stripe cannot change the agreement after you accept it. - Outside the US, a full-agreement account also needs the card payments capability, so Stripe asks for card details. The exchange takes no card payments through it. - If Stripe refuses the account, the code is `rail_refused`. ### What the account is It is a Stripe Connect Express account under the exchange platform. It only receives transfers and sends them to your bank. It is separate from your own Stripe accounts, and can share their bank account. The exchange cannot connect a Stripe account that you opened yourself. ### Attach or clear an existing account ``` PUT /v1/account/providers/{provider_id}/payout ``` Attaches an account that the rail connects to this exchange. Send `rail` and `handle` together. A body with neither clears the destination. The exchange asks the rail about the account before it stores the handle: - A handle that is not an account ID, or an account that the rail does not connect to the exchange: `400` with `unknown_payout_account`. Nothing is stored. - The exchange cannot ask the rail: `503`. Nothing is stored. Send the request again. - The account is not finished yet: accepted. `GET` on the same path returns the destination with `payable`: `true` if the rail can pay the account now, `false` if the rail needs more from you or no longer connects the account (`blocked_reason` says why), and `null` with no destination or if the exchange could not ask the rail. ## Payouts Each hour, the exchange opens a payout for each provider whose balance is at least $25. It is one Stripe transfer to the destination. - It pays earnings that settled before today. It rounds down to whole cents and caps each transfer at an operator limit. The remainder stays in the balance. - A provider with no destination, or one that the rail cannot pay, gets no payout. - If Stripe refuses a transfer, the money returns to the balance. The run waits 24 hours before it tries that provider again. The **Earnings** page shows the amount owed and paid. ## Endpoint requirements - The exchange must reach the endpoint at the scheme, host and port of your `declared_endpoint`. You choose the path. - The endpoint must have a public address. The exchange rejects loopback, private and link-local addresses. An exchange can also limit dispatch hosts to an allowlist. - On shutdown, finish the current dispatches first. # Provider API The OpenAPI 3.1 document is the full description, including schemas: https://router.inference.ai.exchange/v1/openapi.json ## `GET /v1/provider/jobs` List the settled jobs of this provider, oldest first. A row has the earnings, the fee, the charge and credit of the buyer, the meters and the measured latency of one job. A job appears five minutes after it settles and does not change after that. Send the `next_cursor` of the last read to get only new jobs. ## `GET /v1/provider/model-registry` ## `GET /v1/provider/models` List every deployment that this provider registered, in every status. Use it to see whether a registration is unlisted, listed or suspended and why, and whether it is paused. The `listed` frame of the session says which deployments can be quoted now. Each row holds the last refused quote of the deployment until a later quote rests, so a client that does not log `quote_reject` frames can still read why. ## `POST /v1/provider/models` Register a deployment and resolve its approved identity. The exchange checks the claimed model name, creator, kind and base model. It leaves an unknown name or a metadata conflict unlinked. An operator must admit a new deployment. Registration keeps the unlists, pauses and suspensions of an operator. A change of metadata voids the earlier admission before the exchange resolves the new claim. ## `POST /v1/provider/models/{id}/pause` Pause a deployment and remove its offer from the book. Use it when you stop serving a deployment. The offer leaves the book at once and cannot win an award. The registration and the decision of the operator stay. Registering again does not resume the deployment. Resume it with `POST /v1/provider/models/{id}/resume`. ## `POST /v1/provider/models/{id}/resume` Resume a deployment that this provider paused. Use it when a model left the rate card and came back. The offer can rest on the book again. A suspended deployment stays unchanged, because an operator barred it. A deployment that was never paused also stays unchanged, so a new model still waits for an operator to list it. ## `GET /v1/provider/ws` Open the session of a bidding agent, over a WebSocket. Send a `login` frame first, with the slug of the provider, its credential and the URL where the exchange sends jobs. Any other first frame closes the socket. The exchange replies with `login_ack` and a `listed` frame. `listed` names each deployment that the provider can quote. The exchange sends it again when an operator changes the list. Then send one `quote` frame for each listed deployment. The exchange replies with `quote_ack` or `quote_reject`. A quote is firm for `valid_for_ms`. It leaves the book when that time ends, when you withdraw it or when the socket closes. Send a new quote when the price changes. `revision` must increase within one session. Jobs arrive separately. The exchange sends a `POST` of a `DispatchRequest` to the `dispatch_url` of the login, with the award id in the `idempotency-key` header. The agent answers with newline-delimited `DispatchEvent` frames. On this socket, an `award_notice` comes before each job and a `settlement_notice` after it. The agent sends `ProviderMessage` frames and the exchange sends `ExchangeMessage` frames. A second login for the same provider replaces the first and removes its offers. This is how a reconnect works. A login with the `handover` capability does not remove them. The first session keeps its offers until the second quotes each one again, at any revision. Then the first gets `superseded` and closes. An award on an offer of the first session goes to that session and its `dispatch_url`. So a new agent process can replace an old one with no gap in the book. A login with the `notice_ack` capability acknowledges each `award_notice` and `settlement_notice` with a `notice_ack` frame. The exchange keeps each notice with no ack for 15 minutes and sends it again at the next login with `notice_ack`. A notice can arrive more than once. When the exchange ends a session, the close frame has a code: - 1012: the exchange process stops. - 4000: a login without `handover` replaced the session. - 4001: superseded. - 4002: no frame before the heartbeat timeout. - 4003: the agent did not read its control messages. - 4004: the key was revoked. - 4005: the provider is not admitted. - 4006: the jurisdiction does not allow trade. After 4004, 4005 or 4006 a login fails, so wait before the next one. A socket that ends with no close frame is a network fault or a crash. # Sell with bring your own key (BYOK) You can sell models that you reach through your own cloud account, such as Claude on Amazon Bedrock. You keep the account and pay the cloud. You run no serving stack. To use your own cloud account for your own requests, see [Bring your own key (BYOK)](https://inference.ai.exchange/docs/byok.md). ## How it works 1. You connect a cloud key to your provider. The exchange seals it. 2. You register a deployment for each model to sell, with `byok` set to `true`. 3. You quote a price for each deployment on an open session. 4. An operator lists the deployments. See [Deployments](https://inference.ai.exchange/docs/provider-models.md). 5. A buyer sends a request. The ranking picks your offer by price. The BYOK engine calls the cloud with your key and returns the answer. 6. The exchange settles on the tokens that the cloud counted. You pay the cloud. The exchange pays you your price, minus its fee. This is a normal offer with a **BYOK** badge. You quote, and the exchange serves. Settlement is as for any provider. See [Serve and get paid](https://inference.ai.exchange/docs/provider-dispatch.md). ## What you need - An admitted provider. See [Become a provider](https://inference.ai.exchange/docs/provider.md). - An account at Anthropic, Amazon Bedrock, Google Vertex AI or Azure OpenAI with access to the models to sell. - Cloud terms that allow you to resell its models. The exchange does not check this. - A program that holds a provider session and sends quotes. See [Quote over the session](https://inference.ai.exchange/docs/provider-quoting.md). The kinds of key, the secret fields and the rules for each cloud are the same as for a buyer's key. See [Clouds](https://inference.ai.exchange/docs/byok.md#clouds) and [Connect a key](https://inference.ai.exchange/docs/byok.md#connect-a-key). A key belongs to one provider. ## Connect a key Open your provider in the console and add a cloud account. Anyone who can manage the provider can do this, with a recent sign-in. Only a Clerk session works. A provider key or CLI token cannot connect a cloud key. The console calls `POST /v1/account/providers/{provider_id}/byok/grants`: ```json { "upstream": "anthropic", "kind": "anthropic_api_key", "grant": { "api_key": "sk-ant-..." } } ``` | Request (under `/v1/account/providers/{provider_id}/byok/`) | Effect | | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET grants` | Lists the keys, revoked ones included. A person with read access can call it. No response holds a secret. | | `POST grants` | Connects a key. The answer is `201`. | | `PUT grants/{id}` with `{"grant": …}` | Replaces the secret. The kind and region stay. | | `DELETE grants/{id}` | Revokes the key and deletes the sealed secret. The answer is `204`. | | `POST grants/{id}/test` | Asks for a test. The answer is `202`. The engine makes one small call and records `last_test_ok`. A Bedrock key without `bedrock:GetFoundationModelAvailability` shows `missing_permission`. | | `PATCH grants/{id}` | Changes `retention_claim` and `model_scope`. | | `GET models` | Lists the models that the live keys offer. | - A grant of another provider gets `404 no_grant`. - You cannot register a handle that starts with `byok-`. The exchange keeps those for buyers' keys. - A seller's key has no `api_key_scope`. The route refuses it with `400 invalid_settings`. ## Choose the models to sell A new key shows its models a few seconds after you connect it. `GET …/byok/models` returns, for each model: | Field | Meaning | | ------------------ | ---------------------------------------------------------------- | | `upstream` | The cloud. | | `name` | The name on the exchange. | | `upstream_model` | The id at the cloud. The engine calls the cloud with this id. | | `max_input_tokens` | The context window for the key. Absent when the key states none. | A model that you do not register is not on the book. To sell only some models, set `model_scope` of the key to their `upstream_model` ids. ## Register a deployment Register each model as in [Deployments](https://inference.ai.exchange/docs/provider-models.md). Set `upstream_model` to the cloud id and `byok` to `true`: ```bash curl https://router.inference.ai.exchange/v1/provider/models \ -H "Authorization: Bearer sk-liquid-..." \ -H "Content-Type: application/json" \ -d '{ "deployment_name": "claude-opus-5-5", "upstream_model": "", "byok": true, "creator": "anthropic", "precision": "bf16", "region": "us-east-1", "max_input_tokens": 200000, "input_modalities": [{ "type": "text" }], "output_modalities": [{ "type": "text" }] }' ``` With `byok` set to `true`, the exchange ignores the dispatch URL of your session for this deployment. - A registration without `byok` keeps the current delivery. Send `"byok": false` to serve with your own agent again. - The exchange refuses `byok` with `409 no_key_for_model` unless a live key of your provider lists `upstream_model`. Connect the key, wait a few seconds, and register again. - Set `max_input_tokens` to the limit that the cloud gives your account. Set `precision` to the value the cloud states, or the nearest true value. Set `region` to the cloud region of the key. The `exchange.privacy.regions` rule uses it. ## Quote your models Your agent holds the session and sends the quotes. The frames are the same as for any provider. Read [Quote over the session](https://inference.ai.exchange/docs/provider-quoting.md) first. What differs here: 1. **Use an open session.** `liquid`, `openai` and `anthropic` sessions can all quote BYOK deployments. Do not log in again, because a second login replaces the first session and removes its offers. With no session, see [A provider that sells only BYOK](#a-provider-that-sells-only-byok). 2. **Read `listed`.** Quote only the `LISTED` and `UNLISTED` deployments in it. 3. **Send one `quote` for each deployment.** Use the target `{ "kind": "registered", "model_id": "…" }` and a higher `revision` each time. 4. **Keep the session open.** Quote again before `valid_for_ms` ends. If you change `byok` on a resting deployment, quote it again. ```json { "type": "quote", "target": { "kind": "registered", "model_id": "3f0c…" }, "pricing": [ { "billable": "input_text", "unit": "mtok", "usd": 3.3 }, { "billable": "output_text", "unit": "mtok", "usd": 16.5 } ], "fixed_fee": 0, "service_class": { "ttft_ms": 3000, "completion_ms": 60000 }, "supported_features": ["text", "tools", "input.image"], "retention_policy_supported": ["allowed"], "synthetic": false, "revision": 1 } ``` Rates are USD per million tokens. | Field | What to set | | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | `input_text`, `output_text` in `pricing` | Your price, above the cloud price by your margin. A job that costs you more than you quote is your loss. | | `cache_read`, `cache_write_*` in `pricing` | Quote a cache rate only when the model and cloud report cached tokens. Test one job. | | `service_class` | Deadlines above what the cloud takes for your region and quota. A missed deadline is still paid, but the exchange marks the deployment slow. | | `supported_features` | Only what the model, cloud and engine support. A feature that the engine cannot send to the cloud fails the job. | | `retention_policy_supported` | Match the `retention_claim` of your keys. See [Retention claim](#retention-claim). | | `synthetic` | `false`. A production exchange refuses `true`. | The registration sets the model, region, context limit and tokenizer. A quote cannot change them. ### Quote only what you can serve The exchange has no capacity field. If a key reaches the cloud quota, the job fails and counts against the health of your deployment. To stop awards, send `withdraw_quote` or `mass_withdraw`, and quote again when you have room. Withdraw a model when: - You revoked the last live key that lists it. - The cloud retired the model or removed your access. - The `model_scope` of your keys no longer lists it. Keep the session open even when it serves no BYOK job. If it closes, the exchange removes your offers. ### A provider that sells only BYOK Leave `declared_endpoint` out when you register. Log in with the `byok` dialect and no `dispatch_url`: ```json { "type": "login", "slug": "nimbus", "api_key": "sk-liquid-...", "dispatch_dialect": "byok" } ``` - The exchange accepts this login only for a provider with a live cloud key. Otherwise it answers `login_reject`. - After `login_ack`, every job of this session goes to the BYOK engine. - A provider with no registered endpoint cannot log in with the `liquid`, `openai` or `anthropic` dialect. `quote_ack` means that the offer rests. For a `quote_reject`, see [Quote over the session](https://inference.ai.exchange/docs/provider-quoting.md#send-a-quote). ## What the engine does with a job - It tries the live keys of your provider oldest first. It goes to the next key only when the cloud refuses the credentials. It skips a key whose model scope does not list the model. - It streams the cloud answer back as the cloud sends it. - It skips a key for a model for ten minutes after the cloud answers 404 for that model. - It reads keys through replication. A revoke stops service at once. If replication fails, the engine exits instead of serving a revoked key. - After the cloud sent output, the job cannot fall back to another offer. Before output, the next offer in the ranking can take it. ## Retention claim Set `retention_claim` of each key to what your cloud agreement says: | Value | Meaning | | --------- | ------------------------------------------------------------------------------------------------------------------- | | `default` | You state nothing. Only Bedrock counts as zero retention, except Claude Fable and OpenAI models other than gpt-oss. | | `zero` | The cloud does not keep prompts. | | `keeps` | The cloud keeps prompts. The offer is never `none` for retention. | An offer declares zero retention only when every key that can serve it has a `zero` claim. The exchange does not check your claim. See [Provider claims](https://inference.ai.exchange/docs/provider-claims.md). ## Sell one model in two ways You can serve the same model with your own agent and with BYOK. Register two deployments with different names. Both share `model`, so both are in the market of the model, each with its own quote, price and health record. | Field | Your agent | BYOK | | ----------------- | --------------------------- | -------------------------------- | | `deployment_name` | `claude-opus-5-5` | `claude-opus-5-5-byok` | | `model` | `claude-opus-5-5` | `claude-opus-5-5` | | `byok` | absent or `false` | `true` | | `upstream_model` | The id that your agent uses | The id that your cloud key lists | The ranking picks the cheaper offer that the buyer's rules allow. To move a deployment from one delivery to the other, send `byok` on its registration and quote again. ## Stop selling To pause one deployment, see [Deployments](https://inference.ai.exchange/docs/provider-models.md). To stop all service from a key, revoke it. To sell fewer models, change `model_scope`. # Serve a reservation A reservation sells a monthly pool from one model at a fixed monthly fee. The pool of a chat model is tokens. The pool of a video, image or speech model is units: seconds of video, images or characters of speech. An operator must turn on reservations for your provider first. Ask the exchange. ## How a reservation sale works A buyer asks you and other chosen providers for quotes. If the buyer accepts yours, the exchange creates a contract. It names you as seller and the buyer's workspace as buyer, and neither side can change it. 1. Answer the buyer's request in the **Reservations** inbox with a quote, or decline. 2. The buyer accepts. The accepted quote becomes a contract. 3. Link one unlisted deployment of the agreed model to the contract. The link is final. 4. Quote the deployment only at the contract prices. 5. On the 1st of each month of the term, the exchange moves the fee from the buyer's cash into your balance. You get it in your next payout. 6. Serve the buyer's requests on the deployment. The buyer can also ask you to serve some days early. See [Start before the term](https://inference.ai.exchange/docs/provider-reservations.md#start-before-the-term). All reservation deployments must follow zero data retention. ## The inbox The provider console has 4 tabs: | Tab | What you find there | | ------------------- | -------------------------------------------------------------------------------------------------------- | | **Needs an answer** | Requests you have not quoted, quotes the buyer asked you to change, and threads with a newer buyer reply | | **Answered** | Requests you quoted | | **Closed** | Requests the buyer closed before you quoted, and quotes the buyer rejected | | **Contracts** | Accepted quotes, in groups | Each row shows the model, buyer, pool size, region, start month, term and one status, such as **New**, **Buyer replied**, **Quote sent**, **Accepted** or **Lapsed**. A row flags uptime above 99.9%, time to first token of 250 ms or less, and SOC 2. **Needs an answer** lists buyer replies first, then change requests, then new requests, oldest first. See [Reservations](https://inference.ai.exchange/docs/reservations.md#terms) for the terms. ## Quote, revise or decline Open a request and set the prices and terms. The buyer sees your prices with the exchange fee added. | Field | What you set | | --------------------- | ---------------------------------------------------------------- | | Input price | Dollars for each million input tokens, before the exchange fee | | Output price | Dollars for each million output tokens, before the exchange fee | | Token pool | Input and output tokens for each month | | Rate limits | Tokens and requests for each minute | | Output speed | Minimum output tokens for each second | | Time to first token | Maximum for 95% of requests in a month, or no limit | | Start month | First month of the term, after the current month | | Term | 1 to 12 months | | Uptime | Minimum uptime, as a percentage | | Regions | Serving regions, such as `us-east` or `eu-west`. Empty means any | | SOC 2 | Not required, Type 1 or Type 2 | | **Note to the buyer** | Optional, 200 characters at most | The monthly fee is the input price times the input pool, plus the output price times the output pool. It must be above zero. For a video, image or speech model, the buyer states a count for each variant. You state a rate for each of them, in nano-USD for one unit (`nanos_per_unit`), before the exchange fee. The monthly fee is the sum of each count times its rate. You cannot add or leave out a variant that has a count, and you cannot price a variant that has none. The terms start as the buyer's terms. The buyer sees each term that differs. If the buyer's start month has begun, the form moves it to the next open month. - **Send quote** sends your quote. - **Send updated offer** replaces your quote with a new revision. The buyer accepts one revision, so a buyer must read the new offer before the acceptance counts. - **Decline** ends your part in the request. After you decline, the buyer accepts or the quote lapses, you cannot quote again. When the buyer asks for changes, you see the reasons. Answer with an updated offer or a reply. A rejected quote is closed. Keep messages about the request. The exchange can read them. ## Link a deployment Open the **Contracts** tab. **Needs action** lists contracts with no deployment, or whose fee the buyer's cash did not cover. Choose a deployment and then **Link deployment**. The deployment must belong to your provider, serve the same model, be unlisted, and serve no other contract. It must also be in a region the contract allows, and your provider must have a checked SOC 2 report of the type the contract asks for or a stronger one (a declared or lapsed report does not count; see [Provider claims](https://inference.ai.exchange/docs/provider-claims.md)). If none qualifies, register one first. See [Deployments](https://inference.ai.exchange/docs/provider-models.md). The link is final. A linked deployment is **committed**: you can never list it, you cannot change its model link, and the exchange leaves it out of automatic admission. Link by 00:00 UTC on the 1st to serve that month. A later link serves from the next month. The exchange refuses the link when no month of the term is left after the current one. The exchange compares region names exactly, so a deployment in `us` or `us-east-1` does not meet a `us-east` term. A contract can name these regions: `us-east`, `us-central`, `us-west`, `ca-central`, `sa-east`, `eu-west`, `eu-central`, `eu-north`, `uk-south`, `me-central`, `af-south`, `ap-south`, `ap-southeast`, `ap-northeast` and `au-southeast`. A refused link gets `409` with one of these codes: | Code | Cause | | ----------------------- | --------------------------------------------------------------------------- | | `deployment_ineligible` | The deployment serves another model, or it is not unlisted | | `deployment_taken` | The deployment serves another contract | | `already_linked` | The contract has a deployment | | `region_outside_terms` | The deployment is in a region the contract does not allow | | `soc2_missing` | Your provider has no checked SOC 2 report of the type the contract asks for | ## Quote the contract prices From the link to the end of the term, quote the deployment only at the contract prices. You get requests and the monthly fee only while you do. In your quote: - State the contract input price as the uncached input rate. - State the contract output price as the generation rate. - State no fixed fee. - Keep the cache read and write rates, and any audio output rate, no higher than the input price and output price respectively. - Quote no rate for images, characters, video seconds or audio seconds, unless the contract is for that unit. A contract for video, image or speech has no token prices. Quote exactly the rate of each variant of the contract, with the same resolution, quality, size and sound options, and no other line. A line for a variant that the buyer did not commit gets `contract_price`. Other prices on a linked deployment get `contract_price`. See [Quote over the session](https://inference.ai.exchange/docs/provider-quoting.md). When you link a deployment that has an offer at other prices, the exchange removes that offer in about 10 seconds. Otherwise, serve as usual. See [Serve and get paid](https://inference.ai.exchange/docs/provider-dispatch.md). ## Each month The contract card shows the month status: | Status | Meaning | | ---------------------- | ----------------------------------------------------------------------------------- | | Waiting for start | The term has not started. | | Waiting for deployment | No deployment is linked, or you linked it after the 1st. You serve from next month. | | Fee due | The buyer has not paid. The exchange charges after you quote the contract prices. | | Active | The buyer paid. You serve the buyer's requests. | | Pool used | The buyer used the pool. Requests go to the market for the rest of the month. | | Suspended | The buyer's cash did not cover the fee on the 1st. You do not serve this month. | | Ended | The term is over. | The fee run is at 00:00 UTC on the 1st, then once an hour. The exchange charges the fee only if you had an offer at the contract prices in the last 5 minutes. Otherwise it tries again in an hour. A suspended month stays suspended, and a later top-up does not resume it. You earn nothing for it, or for a month before the link. ## Start before the term The buyer can ask you to serve for some days before the first month. You accept or decline, and then the exchange approves or refuses. 1. The request shows on the contract in **Contracts**, with the first day and the fee for the days to the end of that month, before the exchange fee. 2. Choose **Accept** or **Decline**. Link the deployment first. You cannot accept from 00:00 UTC on the first day. 3. When you accept, the exchange approves or refuses. If it approves, it holds the fee from the buyer's cash. It cannot approve after the first day begins. 4. From 00:00 UTC on the first day, you serve at the contract prices. The pool for those days is the same share of the monthly pool as the fee is of the monthly fee. 5. On the 1st of the term, you receive the held fee, with the first month's fee if the buyer pays it. The early days are paid even when the buyer cannot pay the first month. The early fee is the monthly fee times the days to the end of the month, divided by the days in that month. Days are whole UTC days. | State | Meaning | | ------------------------------------------ | --------------------------------------------------------------------------------------------------- | | Early start asked | The buyer waits for you and can withdraw until you answer. | | Waiting for the exchange | You accepted. The exchange approves or refuses. | | Early start approved | The fee is held. You serve from the first day and are paid on the 1st. | | Early start withdrawn, declined or refused | The buyer withdrew, you declined, or the exchange refused (with a reason). The buyer can ask again. | | Early start revoked | The exchange revoked the approval, with a reason. The buyer cannot ask again. | ## Uptime and speed While the contract serves a month, the exchange measures the uptime and the output speed of the linked deployment. - Every 5 minutes, the exchange sends a chat deployment a request for 1 token. A video, image or speech deployment gets no probe, so its uptime comes from the buyer's jobs. The exchange does not send this request when a buyer's request got an answer in those 5 minutes. The exchange pays for the request at the contract prices. The request does not use the buyer's pool or rate. - Uptime is a share of time. The exchange divides the time into periods of 5 minutes. In each period, the uptime is the requests that got an answer, divided by all the counted requests from buyers and from the exchange. Each period has the same weight, whatever the number of requests in it. A failure of your deployment counts as down. A time with no offer at the contract prices also counts as down, because the buyer cannot use the deployment. A slow answer counts as up, because the speed term measures it. - Output speed is the median of the output tokens a second, over the requests from buyers and from the exchange. A request with less than 250 ms of output does not count. When a month has fewer than 30 timed requests, the exchange also sends a longer request every 6 hours. - The exchange checks your SOC 2 report during the term. A report that lapses counts against the contract. When the last 24 hours fall below a term, the exchange records the time that the term was below its target. After a month ends, the exchange keeps one result for each term: met, missed or not measured. Uptime needs 100 periods with a counted request for a result, and speed needs 30 timed requests. Uptime is not measured for a month in which the exchange did not count each day, for example the month in which the exchange started to measure. ## Jobs, usage and payout A job inside the pool takes no hold and settles with no charge. Its value at the contract prices counts against the pool. The pool is used when use reaches the monthly fee. If your deployment fails a job before its first token, the buyer's **overflow** models serve it at market prices. The exchange records your failed jobs on the contract. Each paid month becomes one item of your payout, with the same hold window as a settlement. See [Serve and get paid](https://inference.ai.exchange/docs/provider-dispatch.md#payouts).