Integrations
Backstop has **two native surfaces** and **one wire protocol**. That is the whole of it, and it is deliberately small:
- In-process —
Backstop.wrap(client)injects the transport into an OpenAI or Anthropic Python SDK client. No proxy, no sidecar, no network hop. - The gateway —
backstop serveruns as an OpenAI-compatible reverse proxy in front of any upstream.
Everything else in this document is a consequence of the second one: because the enforcement engine works on the request and the response rather than on a provider’s identity, any server that speaks the OpenAI-compatible protocol is a valid target.
The four kinds of server
| Server | Speaks | Backstop adds |
|---|---|---|
| OpenAI, Anthropic | their own endpoints | budgets, breaker, concurrency, and a price from the bundled catalogue |
| vLLM, SGLang, llama.cpp, Ollama, LM Studio, TGI | an OpenAI-compatible endpoint | budgets, breaker, concurrency; price it yourself |
| DeepInfRA, Together AI, Groq, Fireworks, OpenRouter, Replicate, Baseten, Cohere, Mistral, NVIDIA | an OpenAI-compatible endpoint | budgets, breaker, concurrency; price it yourself |
| Amazon Bedrock, Azure OpenAI, Google Vertex AI | an OpenAI-compatible gateway | budgets, breaker, concurrency; price it yourself |
There is no per-provider plugin, and there is no need for one. The engine counts requests, tokens and concurrent calls. It does not need to know who served the model.
Pointing at an OpenAI-compatible server
In-process
Give the OpenAI SDK a base_url and wrap it. The wrapper keeps the SDK’s own
base_url — it does not reset it to the provider’s default — so the wrapped
client talks to the server you pointed it at.
from decimal import Decimal
from openai import OpenAI
from backstop import Backstop, BackstopConfig
client = Backstop.wrap(
OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed"),
budget=50_000, # a token ceiling
budget_usd=Decimal("50.00"), # and a dollar one
config=BackstopConfig(initial_concurrency=4),
)
client.chat.completions.create(
model="Qwen/Qwen3-8B",
messages=[{"role": "user", "content": "hello"}],
)
That is the whole integration. Nothing else changes: the client still has the same methods, the same arguments and the same return value.
The Anthropic SDK is wrapped the same way, against Anthropic’s own endpoint.
As a gateway
When the caller is not a Python process — another service, a Go program, a shell script — run Backstop in front of the server instead:
backstop serve --target https://api.deepinfra.com/v1/openai --port 8787
The gateway speaks the OpenAI-compatible API, so the caller points at
http://localhost:8787/v1 instead.
Two things about the gateway worth knowing before you choose it:
- It buffers. The upstream response is read in full and then returned. A token-by-token stream is not streamed through the gateway. If you need incremental streaming, wrap the client in-process instead.
- A denial is a 502. An enforcement decision — a budget ceiling reached, a
circuit open — stops the request and the gateway reports
502 upstream request failed. The detail stays in the log; the response body deliberately does not echo an internal exception, because a proxy that does hands out hostnames and file paths. The in-process path raises the provider’s own error type instead, which is catchable.
The price list is the one difference
The limits do not change with the provider. The price list does.
The bundled catalogue prices OpenAI and Anthropic models only. A request to
any other host is recorded as provider="unknown" and an unpriced request,
because the engine refuses to invent a provider it does not recognise — an
invented provider produces a charge-back row that looks authoritative and is
wrong.
That matters the moment a dollar ceiling is in play:
config = BackstopConfig(budget_usd=Decimal("50.00"))
With budget_usd set and the model unpriced, the request is refused:
a dollar ceiling is only enforceable for a model the catalogue prices, and a
ceiling with a hole in it is not a ceiling.
There are two ways forward, and both are explicit.
File a price
Point price_catalog_path at a JSON file. It is layered over the bundled
catalogue, so it only needs the models you actually serve:
{
"effective_from": "2026-10-01",
"entries": [
{
"model": "Qwen/Qwen3-8B",
"provider": "unknown",
"input_per_mtok_usd": "0.30",
"output_per_mtok_usd": "0.60"
}
]
}
config = BackstopConfig(
budget_usd=Decimal("50.00"),
price_catalog_path="/etc/backstop/prices.json",
)
Money is a decimal string above all else, so 0.30 means exactly 0.30. Every
entry filed this way is recorded as source="user", confidence="negotiated"
— a file a deployment wrote is a rate it agreed to, whatever it was copied
from.
Say that you mean it
If you would rather let the hole stand than guess, say so:
config = BackstopConfig(
budget_usd=Decimal("50.00"),
budget_usd_unpriced_policy="allow",
)
The request is then allowed through and recorded as unpriced — the same visible hole the ledger records for every unpriced request, rather than a plausible wrong number. The token and concurrency limits still apply. Only the dollar ceiling stops being enforceable.
Frameworks
LangChain and LlamaIndex have adapters. They do not change how enforcement works — the transport enforces regardless — but they give a framework’s own callback surface a way to carry a tenant id, so a framework run can be labelled without editing every call site.
See framework adapters.
What this does not do
- It is not a router. Backstop does not choose a provider, fail over between them, or pick the cheapest model. It limits the spend of whichever server you pointed it at.
- It does not make an unpriced model priced. The default is to refuse a dollar budget it cannot measure. Filing a price is a decision you make.
- It is not a per-provider integration list. There is no DeepInfRA adapter and no vLLM adapter, because the wire protocol is the integration.