An OpenAI-compatible endpoint. Change one base URL, handle one new
finish_reason, and you are integrated. Model access is included -- you do
not bring a provider account.
One gateway, two dialects. Keep the client library you already use. Copy the block for your client, change nothing else.
from anthropic import Anthropic
client = Anthropic(
base_url="https://api.bees.riif.com",
api_key=os.environ["BEES_API_KEY"],
)
r = client.messages.create(
model="claude-sonnet-4",
max_tokens=1024,
system="...",
messages=[...],
tools=[...], # passed through, both directions
)
from openai import OpenAI
client = OpenAI(
base_url="https://api.bees.riif.com/v1",
api_key=os.environ["BEES_API_KEY"],
)
r = client.chat.completions.create(model="bees", messages=[...])
/v1, and it does not matter.
Each SDK appends its own path, so the correct base differs. We accept both forms either
way, so if you copy the wrong line it still works. Authentication is the same story:
send x-api-key or Authorization: Bearer, whichever your client
already sends.
model field is not a request, and the response tells you the
truth. We route to the models we operate, so passing
claude-sonnet-4 does not select it. The model we return names
the one that actually answered, never an echo of what you asked for.
Two steps, in this order. The first costs nothing, so if it fails you know the problem is the key or the URL rather than anything downstream.
curl https://api.bees.riif.com/v1/usage \
-H "Authorization: Bearer $BEES_API_KEY"
{
"tenant": "acme",
"requests": 0,
"balance_usd": 250.00,
"burn_per_day_usd": 0,
"days_remaining": null
}
A 401 here means the key is wrong or revoked. Nothing else can be at fault
yet, because no model has been asked to do anything.
Anthropic:
curl https://api.bees.riif.com/v1/messages \
-H "x-api-key: $BEES_API_KEY" \
-H "content-type: application/json" \
-d '{"model":"claude-sonnet-4","max_tokens":32,
"messages":[{"role":"user","content":"Reply with exactly: hello from bees"}]}'
{
"type": "message",
"role": "assistant",
"model": "<the model that answered>",
"content": [{ "type": "text", "text": "hello from bees" }],
"stop_reason": "end_turn",
"usage": { "input_tokens": 21, "output_tokens": 4 }
}
OpenAI:
curl https://api.bees.riif.com/v1/chat/completions \
-H "Authorization: Bearer $BEES_API_KEY" \
-H "content-type: application/json" \
-d '{"messages":[{"role":"user","content":"Reply with exactly: hello from bees"}]}'
model in that response. It names what actually ran,
not what you asked for. That is how you can always tell what is answering your traffic,
without taking our word for it.
Rerun step 1 afterwards: requests will have moved and
balance_usd will have gone down by a fraction of a cent. That confirms
metering and billing agree with what you just did.
Send a label identifying which of your customers a request belongs to:
X-Bees-Customer: acme-legal
Use anything you like. We never read it or try to interpret it. It only has to be different for different customers.
Two things follow from sending it. Their work is kept apart from every other customer's, so an answer produced for one is never handed to another. And your usage comes back broken down per customer, so you can see what each one costs you.
The older header name X-Bees-Subtenant still works.
One extra body field, and one question decides it: how hard is this request?
Each policy is a different amount of work we do before answering. Pick by how much the answer is relied upon.
| policy | Can escalate | Use it when |
|---|---|---|
fast | no | the task is easy and does not need corroborating |
verified | yes, only when the work calls for it | the output is acted upon |
passthrough | not applicable | you already know this one is hard |
Default is verified. fast does the least,
verified does more and reaches further only when the request warrants
it, and passthrough goes straight to the strongest option. Right on a
known-hard request, wrong by accident.
{
"messages": [ ... ],
"policy": "verified" // fast | verified | passthrough
}
Standard OpenAI response, plus an additive bees block. Existing
integrations do not break, because they ignore what they do not read.
{
"choices": [ ... ],
"usage": { "prompt_tokens": 6000, "completion_tokens": 500 },
"bees": {
"outcome": "answered", // answered | escalated | needs_review
"balance_usd": 45.11,
<<<<<<< HEAD
"policy": "verified",
"escalated": false
=======
"policy": "verified"
>>>>>>> board
}
}
Token counts are the provider's reported figures, never our estimate. If they ever fail to reconcile against your invoice, that is a bug on our side. Contracted accounts receive a fuller per-call breakdown, so the charge can be recomputed from published provider rates without taking our word for it.
When the work does not come out solid, you do not get a completion.
{
"choices": [{
"index": 0,
"finish_reason": "needs_review",
"message": { "role": "assistant", "content": null }
}],
"bees": {
<<<<<<< HEAD
"policy": "verified",
"refused": true,
"positions": [ // what a reviewer needs to decide
{ "summary": "..." },
{ "summary": "..." }
]
=======
"outcome": "needs_review",
"policy": "verified"
>>>>>>> board
}
}
The finish reason is deliberately a value your code has never seen, and content is null — a best guess handed over with a warning gets used. Open the request by its id and a reviewer has what they need to decide in seconds.
Caps are enforced before dispatch, from a deliberately pessimistic estimate. A cap checked after the money is spent is a report, not a cap.
HTTP 429
{
"error": {
"type": "spend_cap_exceeded",
"limit_usd": 25.0,
"would_spend_usd": 0.31,
"subject": "provider_spend",
"window": "day"
}
}
Two ceilings: one on inference spend, one on our fees.
GET /v1/usage?days=30
Authorization: Bearer <key>
Requests, refusals and spend — all computed from the same rows that drive billing, so the dashboard cannot drift from your invoice.
| Status | Meaning |
|---|---|
| 401 | Unknown key |
| 429 | A spend cap would be breached. Nothing was dispatched. |
| 502 | Your provider failed. Not billed. |
Streaming. Send stream=false. A streaming request is
refused with a clear message rather than hanging.
Checking on tool calls. Requests carrying tools are
served straight through. The same tool called with different arguments is not something
we can judge honestly yet, so we do not pretend to.
No streaming yet. No shared cross-customer cache tier. No action gating — we check answers, not tool calls. Say if any of these blocks you and it moves up the list.