Potion speaks the OpenAI chat-completions protocol. If you already have an OpenAI client, you change one line and keep everything else — the request body, the response shape, streaming and tool calls are unchanged.
You are reading this signed out, so the base URL below is the generic one and the policy section shows the four shapes rather than yours. Sign in and this page fills in with your own endpoint and bound policy.
https://api.potion.dev/v1
curl https://api.potion.dev/v1/chat/completions \
-H "Authorization: Bearer $POTION_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"potion-auto","messages":[{"role":"user","content":"Write a python function that reverses a string"}]}'import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'https://api.potion.dev/v1',
apiKey: process.env.POTION_API_KEY,
});
const res = await client.chat.completions.create({
model: 'potion-auto', // any label; Potion routes by prompt + policy
messages: [{ role: 'user', content: 'Write a python function that reverses a string' }],
});
console.log(res.choices[0].message.content);from openai import OpenAI
client = OpenAI(
base_url="https://api.potion.dev/v1",
api_key=os.environ["POTION_API_KEY"],
)
res = client.chat.completions.create(
model="potion-auto", # any label; Potion routes by prompt + policy
messages=[{"role": "user", "content": "Write a python function that reverses a string"}],
)
print(res.choices[0].message.content)Every call carries a serving key as a bearer token: Authorization: Bearer $POTION_API_KEY. Keys come in two scopes. A serve key sends traffic and reads its own state; serve+admin is additionally allowed to provision — mint keys, move budgets, rebind policy.
A key is shown once, at creation, and stored only as a SHA-256 hash. Potion cannot show it to you again and will not pretend otherwise — if it is lost, revoke it and mint another. Each key carries its own policy binding, so separate keys are how you run different trade-offs side by side.
You do not bring provider keys. Potion serves every request from its own, across providers — which is also what lets the router reach the whole catalogue rather than the one account you happened to have.
Potion reads each request, works out which kind of work it is, and selects a strategy from the measured frontier under your policy. Whatever you put in model is echoed back and recorded, and is not an input to that decision. potion-auto is the documented convention; sending a specific model id will not pin it.
Every response carries x-frontier-trace, which is the routing decision in full. A router you cannot audit is a router you cannot trust, so this ships on every request rather than behind a debug flag.
cluster=code-gen;strategy=6efe8a56;frontier=v2;policy=min_cost;fallback=0;provenance=live
| cluster | The workload type the prompt was classified into. |
| strategy | First 8 characters of the selected strategy hash — the exact configuration served, resolvable in Frontiers. |
| frontier | Which published frontier version the choice came from. It increments when new evidence republishes. |
| policy | The rule that selected the point: min_cost, max_quality, latency_bound or compound. |
| fallback | 0 means a measured frontier existed and your policy selected a point on it. 1 means it did not, and the request rode the default strategy — the honest signal that Potion has nothing measured for this work yet. |
| provenance | live means the evidence behind the choice came from real provider runs. Anything else means it did not, and should not be treated as a measurement. |
| constrained | Present only as constrained=tools, when the request carried tools and your policy's optimum was a prompt-transforming strategy. Selection narrowed to single-model points, which the tool contract requires. Your policy's bound still held — a quality floor, cost ceiling or latency bound is never breached by narrowing, only its optimum is — so fallback stays 0. |
A policy is the rule Potion optimises under. It is bound per key, and applies to every request that key sends.
min_cost — cheapest option holding a quality floor.max_quality — best measured quality under a cost ceiling.latency_bound — best quality inside a p95 latency budget.compound — a quality floor and a latency bound, cheapest of the survivors.Cost ceilings are expressed per 1,000 requests, not per 1,000 tokens. Latency bounds are p95 in milliseconds.
curl -X POST https://api.potion.dev/v1/policies \
-H "Authorization: Bearer $POTION_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"min_cost","qualityFloor":0.8}'Every prompt is classified into one of these before anything is selected. Each has its own frontier, because the best strategy for extraction is not the best strategy for multi-step reasoning — that difference is the entire reason routing pays.
| code-gen | Writing code to a specification |
| code-review | Finding defects in code and explaining their impact |
| extraction | Pulling structured fields out of unstructured documents |
| summarization | Condensing a document while preserving what matters |
| classification | Assigning a label from a fixed set |
| multi-step-reasoning | Problems needing several dependent steps |
| creative | Open-ended writing where there is no single right answer |
| rewrite-edit | Revising text to a brief without changing its meaning |
| rag-answer | Answering from supplied source passages |
| agentic-tool-use | Planning and sequencing tool calls |
A prompt that matches none of them confidently is served on the default strategy, and the trace says fallback=1 rather than guessing.
Set stream: true and you get standard server-sent events terminated by data: [DONE], the same as any OpenAI-compatible client expects. The routing decision is chosen before the first token, so x-frontier-trace is present on the response headers even while the body is still streaming.
tools is served from a single-model point; if that is not your policy's optimum, the trace says constrained=tools./v1/completions.Errors use the OpenAI envelope — { error: { message, type, param, code } } — so existing client error handling keeps working.
| 400 invalid_request_error | The body did not validate — a missing messages array, a malformed policy. |
| 401 authentication_required | No bearer token was supplied. |
| 401 invalid_api_key | The key is unknown, revoked or expired. |
| 403 | The key is valid but its scope does not cover this call — provisioning with a serve key rather than serve+admin. |
| 413 | The request body exceeds the accepted size. |
| 429 rate_limit_exceeded | Too many requests. Back off and retry. |
| 429 budget_exceeded | Your spend cap would be crossed by this call. Refused BEFORE the provider is called, so it costs nothing. |
| 503 service_unavailable | No upstream could serve the request. |
A budget is a cap on spend with an optional hard stop. The check runs before the upstream call, so a refused request costs nothing — a cap that only notices after the money is gone is not a cap. Set it on Usage or through /api/budgets.
Rate limits are enforced per key. A limited response carries the standard retry hints; treat 429 as backpressure rather than failure.
Everything this dashboard does is an HTTP call you can make yourself with a Bearer token. A serve key covers the serving surface and its own reads; provisioning needs serve+admin.