Docs

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.

Endpoints

One gateway, two dialects. Keep the client library you already use. Copy the block for your client, change nothing else.

If your code uses Anthropic

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
)

If your code uses OpenAI

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=[...])
The two base URLs differ by a /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.
The 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.

Verify your setup

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.

1. Check the key. No model runs, nothing is charged.

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.

2. Make one real request.

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": "Qwen/Qwen3.5-9B",
  "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"}]}'
Look at 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.

If you serve several customers

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.

Separation is built in, not a setting you can forget to switch on. It also holds between you and everyone else using Bees. An answer produced under one set of instructions is never served to a request carrying different ones, even inside the same account.

The older header name X-Bees-Subtenant still works.

Policy

One extra body field, and one question decides it: how hard is this request?

Your hive is a panel of small models. P is the premium model for that hive: strictly stronger than every bee in it. The three policies are three answers to how much of that machinery a request needs.

policyWhat runsEscalatesUse it when
fastone beeno the task is easy and does not need corroborating
verifiedthe whole hiveto P, only on measured disagreement the output is acted upon
passthroughP alone, hive bypassednot applicable you already know the hive will not help

Default is verified. It is also a cost ladder: fast is the cheapest thing we can do, verified costs more but only reaches for P when the bees actually disagree, and passthrough skips the cheap work to go straight to the expensive model. Right on a known-hard request, wrong by accident.

{
  "messages": [ ... ],
  "policy": "verified",      // fast | verified | passthrough
  "threshold": 0.70          // optional; you may raise it, not lower a clamped one
}

Response

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": {
    "burned_usd": 0.004890,        // taken from your balance for this request
    "balance_usd": 45.11,
    "policy": "verified",
    "consensus_strength": 0.87,    // null on `fast`: one model, so no agreement to report
    "threshold": 0.70,
    "escalated": false,
    "hive": [                      // every call that ran on this request
      {
        "model": "Qwen/Qwen3.5-9B",
        "role": "panel",           // panel | escalation | direct
        "prompt_tokens": 6000,
        "completion_tokens": 500
      }
      // ... one entry per model that ran
    ],
    "vs_frontier": {
      "model": "claude-opus-5",
      "would_have_cost_usd": 0.042500,
      "you_saved_usd": 0.037610,
      "you_saved_pct": 88.5
    }
  }
}

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.

The refusal

When agreement falls below the effective threshold, you do not get a completion.

{
  "choices": [{
    "index": 0,
    "finish_reason": "low_consensus",
    "message": { "role": "assistant", "content": null }
  }],
  "bees": {
    "consensus_strength": 0.41,
    "threshold": 0.70,
    "cluster_count": 3,
    "clusters": [
      { "size": 2, "summary": "..." },
      { "size": 2, "summary": "..." },
      { "size": 1, "summary": "..." }
    ],
    "escalated": true
  }
}

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. You get the competing positions so a reviewer can decide in seconds.

Spend caps

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.

Usage

GET /v1/usage?days=30
Authorization: Bearer <key>

Requests, refusals, spend and net savings — all computed from the same rows that drive billing, so the dashboard cannot drift from your invoice. Savings are quoted net of our fee.

Errors

StatusMeaning
401Unknown key
429A spend cap would be breached. Nothing was dispatched.
502Your provider failed. Not billed.

Not in this version

Streaming. Send stream=false. A streaming request is refused with a clear message rather than hanging.

Consensus over tool calls. Requests carrying tools are served straight through. Two models choosing the same tool with different arguments is neither agreement nor disagreement, so we do not pretend to measure it.

No streaming yet. No shared cross-customer cache tier. No action gating — we score responses, not tool calls. Say if any of these blocks you and it moves up the list.