This week we measured a model at 1/270th the price of the best — at 99% of its quality.

Docs

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.

Quickstart

  1. 1. Create a serving key on API keys.
  2. 2. Point your client at the base URL below.
  3. 3. Send a request. Watch it appear on Connect with the routing decision it got.
Base URL
https://api.potion.dev/v1
curl
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"}]}'
Node.js (openai SDK)
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);
Python (openai SDK)
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)

Authentication

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.

The model field is a label, not a choice

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.

The decision header

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.

x-frontier-trace
cluster=code-gen;strategy=6efe8a56;frontier=v2;policy=min_cost;fallback=0;provenance=live
clusterThe workload type the prompt was classified into.
strategyFirst 8 characters of the selected strategy hash — the exact configuration served, resolvable in Frontiers.
frontierWhich published frontier version the choice came from. It increments when new evidence republishes.
policyThe rule that selected the point: min_cost, max_quality, latency_bound or compound.
fallback0 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.
provenancelive 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.
constrainedPresent 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.

Policies

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.

Rebind this key's policy
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}'

Workload types

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-genWriting code to a specification
code-reviewFinding defects in code and explaining their impact
extractionPulling structured fields out of unstructured documents
summarizationCondensing a document while preserving what matters
classificationAssigning a label from a fixed set
multi-step-reasoningProblems needing several dependent steps
creativeOpen-ended writing where there is no single right answer
rewrite-editRevising text to a brief without changing its meaning
rag-answerAnswering from supplied source passages
agentic-tool-usePlanning 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.

Streaming and compatibility

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 and function calling pass through to the selected model unchanged. Because tool semantics cannot survive a strategy that rewrites or fans out the prompt, a request carrying tools is served from a single-model point; if that is not your policy's optimum, the trace says constrained=tools.
  • Token usage and cost come back on the response, taken from the provider’s own reported figures where it reports them rather than from a modelled estimate.
  • Legacy completions are shimmed at /v1/completions.

Errors

Errors use the OpenAI envelope — { error: { message, type, param, code } } — so existing client error handling keeps working.

400 invalid_request_errorThe body did not validate — a missing messages array, a malformed policy.
401 authentication_requiredNo bearer token was supplied.
401 invalid_api_keyThe key is unknown, revoked or expired.
403The key is valid but its scope does not cover this call — provisioning with a serve key rather than serve+admin.
413The request body exceeds the accepted size.
429 rate_limit_exceededToo many requests. Back off and retry.
429 budget_exceededYour spend cap would be crossed by this call. Refused BEFORE the provider is called, so it costs nothing.
503 service_unavailableNo upstream could serve the request.

Limits and budgets

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.

API reference

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.

  • POST/v1/chat/completionsServe a request. OpenAI-compatible; streaming supported.
  • POST/v1/completionsLegacy completions shim.
  • POST/v1/embeddingsPlatform embedder.
  • GET/v1/modelsThe catalogue, with potion.measured marking what is actually routable.
  • GET / POST/v1/policiesRead or rebind the calling key's own policy.
  • POST/api/planDescribe what you're building → workload type + measured options. member+
  • GET/api/connectionBase URL, bound policy, keys, per-cluster routing readiness.
  • GET/api/routing-activityRecent requests with the routing decision each one got.
  • GET / POST/api/api-keysList or mint keys. Minting requires serve+admin.
  • GET / PUT/api/budgetsSpending cap and hard stop. admin
  • GET/api/usageRequests, tokens and spend.

Things worth knowing

  • A catalogue is not a frontier. Potion knows about more models than it will route to. Only points it has measured for your kind of work are ever selected automatically — an unmeasured model is reachable, never auto-chosen.
  • Quality numbers carry intervals. Every measured quality ships with the confidence interval its evidence supports. Where two strategies overlap inside that interval, Potion reports them as tied rather than inventing a ranking — and your policy decides on cost or latency instead.
  • Latency numbers start provisional. Before you have traffic, p95 comes from evaluation runs (the model call only). Potion switches to serving-grade latency, measured end to end on your own requests, once there is enough of it.
  • Your first requests route on platform evidence. Measurements of the workload type, not of you. As your traffic accumulates, the numbers become yours.