Direct label scoring.
SGLang checks the A–Z answer labels and scores every option at the answer position. There is no generated letter to parse or top-k truncation.
2BA.AI / Developer docs
One base URL, one API key, model amber. Choose an endpoint below.
Get started
One API key from your dashboard authenticates every endpoint. Everything below is current and verifiable.
https://api.2ba.ai/v1 — OpenAI compatiblehttps://api.2ba.ai — Claude Code appends /v1/messagesamber — text and vision inputSetup
The installer pairs your browser, creates your API key and configures the tools it finds. Per-tool steps live in the client guides; the essentials:
curl -fsSL https://2ba.ai/install.sh | sh does the whole pass.https://api.2ba.ai/v1, model amber.amber.ANTHROPIC_BASE_URL=https://api.2ba.ai and ANTHROPIC_AUTH_TOKEN set to your key.2ba provider with npm @ai-sdk/openai in ~/.config/opencode/opencode.json.apiBase: https://api.2ba.ai/v1.aider --model openai/amber with OPENAI_API_BASE=https://api.2ba.ai/v1.Chat API
Point any OpenAI client at the base URL with your 2BA key. amber takes text and images, streams tokens, and speaks the same wire format your tools already expect.
$ curl https://api.2ba.ai/v1/chat/completions \
-H "Authorization: Bearer your_2ba_api_key" \
-H "Content-Type: application/json" \
-d '{ "model": "amber",
"messages": [{ "role": "user", "content": "Hello!" }] }'
POST /v1/chat/completions — OpenAI compatible. Anthropic clients use https://api.2ba.ai; Claude Code appends /v1/messages.amber — text and image input via standard content parts."stream": true for server-sent events in the OpenAI format.OpenAI format
POST/v1/decisions
Classify text, check a condition, or score against ordered levels. Send shared input and typed questions; receive answers in the same order.
Model amber · Text only · Zero output tokens
$ curl -X POST https://api.2ba.ai/v1/decisions \
-H "Authorization: Bearer your_2ba_api_key" \
-H "Content-Type: application/json" \
-d '{
"model": "amber",
"input": "Customer was charged twice for the same invoice.",
"questions": [{
"type": "choice", "name": "queue",
"instructions": "Which queue should handle this request?",
"choices": [
{"value": "billing"},
{"value": "technical"},
{"value": "sales"}
]
}]
}'
from openai import OpenAI
client = OpenAI(base_url="https://api.2ba.ai/v1", api_key="your_2ba_api_key")
d = client.decisions.create(
model="amber",
input="Customer was charged twice for the same invoice.",
questions=[{
"type": "choice", "name": "queue",
"instructions": "Which queue should handle this request?",
"choices": [{"value": "billing"}, {"value": "technical"}, {"value": "sales"}],
}],
)
answer = d.answers[0]
if answer.type == "choice":
print(answer.choice, answer.confidence)
else:
print("Needs review:", answer.name)
Illustrative values. Each answer corresponds to a question in the request.
{
"model": "amber",
"answers": [{
"type": "choice", "name": "queue", "choice": "billing",
"confidence": 0.85,
"probabilities": [
{"value": "billing", "probability": 0.9},
{"value": "technical", "probability": 0.05},
{"value": "sales", "probability": 0.05}
]
}],
"usage": {
"input_tokens": 300, "output_tokens": 0, "total_tokens": 300,
"input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
"output_tokens_details": {"reasoning_tokens": 0}
}
}Ask 1–400 questions per request. Each needs instructions; an optional name identifies its answer.
| Type | Provide | Receive |
|---|---|---|
predicate | A condition to check. | probability that it is true, from 0 to 1. |
choice | 2–26 choices with unique string or boolean values and optional descriptions. | Your choice, confidence, and probabilities: [{value, probability}]. |
score | 2–10 ordered levels, each with a label and optional description. | The weighted level index as score, confidence, and probabilities: [{value, label, probability}]. |
Handle refusals. Any question can return type: "refusal" with its name. Check the type before reading answer fields. Other questions can still receive answers.
inputanswersnull when unnamed). Choices preserve string and boolean values: true and "true" stay distinct. Native label mass below 0.5 yields a refusal. Confidence is not calibrated correctness; validate thresholds on your own data.usageinput_tokens, output_tokens, total_tokens, input_tokens_details and output_tokens_details. Output and reasoning tokens are zero. Cache token details are zero because native scoring does not expose them.safety_identifier is accepted but discarded; it does not enable OpenAI safety policy. See the OpenAI Decisions guide for the wire format.decision_effort, vote split and the legacy response fields apply only to /v1/choices.Using the existing state/options format? Open the 2BA choices reference. POST /v1/choices keeps that contract.
POST /v1/choices Existing 2BA formatExisting 2BA format
POST /v1/choices turns the served model into a typed classifier: your state and 2–26 options go in, one chosen option and per-option probabilities come out — no answer sentence is written. The gateway batches the rotations through native SGLang decision scoring with thinking disabled. Each rotation scores its answer labels directly; no completion tokens are generated.
SGLang checks the A–Z answer labels and scores every option at the answer position. There is no generated letter to parse or top-k truncation.
Each decision runs 1–5 cyclic rotations of the option list (default 3) in one upstream batch. Aggregating them reduces sensitivity to option order; more rotations still use more prefill compute.
label_mass is the probability mass on the offered answer labels. Below 0.5 the choice is null and abstained is true — route those to review instead of guessing.
Choices / Request
Authentication is the same API key as every other 2BA endpoint. The state can be in any language; it is passed through verbatim. This existing 2BA state/options format remains on POST /v1/choices. For the OpenAI Decisions format, use POST /v1/decisions.
$ curl -X POST https://api.2ba.ai/v1/choices \
-H "Authorization: Bearer your_2ba_api_key" \
-H "Content-Type: application/json" \
-d '{
"state": "Customer was charged twice for the same invoice.",
"question": "Which queue should handle this request?",
"decision_effort": "high",
"options": [
{"id": "billing", "description": "Invoices, payments, refunds"},
{"id": "technical", "description": "Bugs, outages, system errors"},
{"id": "sales", "description": "Pricing and account questions"}
]
}'
statequestionoptionsids. description is optional and falls back to the id.decision_effortlow = 1 rotation, medium = 2, high = 3. Omit it for the default; explicit null and other invalid values return 400. Thinking stays disabled. These are compute budgets, not guaranteed quality tiers.rotationsinstructionsChoices / Response
The response carries the aggregated distribution, the abstain signal, and a per-rotation tally that shows when option orders disagree.
{
"model": "amber",
"choice": "billing",
"probabilities": {"billing": 0.86, "technical": 0.11, "sales": 0.03},
"confidence": 0.86,
"label_mass": 0.97,
"vote_split": {"billing": 3},
"abstained": false,
"rotations": 3,
"usage": {"prompt_tokens": 651, "completion_tokens": 0}
}
probabilitieslabel_masschoice is null and abstained is true.vote_splitusageChoices / Examples
Any HTTP client works; the endpoint is plain JSON in, plain JSON out.
# pip install requests
import requests
API_KEY = "your_2ba_api_key" # from your 2BA dashboard
d = requests.post("https://api.2ba.ai/v1/choices",
headers={"Authorization": "Bearer " + API_KEY},
json={
"state": "Customer was charged twice for the same invoice.",
"question": "Which queue should handle this request?",
"options": [
{"id": "billing", "description": "Invoices, payments, refunds"},
{"id": "technical", "description": "Bugs, outages, system errors"},
{"id": "sales", "description": "Pricing and account questions"}],
}).json()
print(d["choice"], d["vote_split"], d["abstained"])
// Standard library only.
package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
func main() {
body := `{"state": "Customer was charged twice for the same invoice.",
"question": "Which queue should handle this request?",
"options": [
{"id": "billing", "description": "Invoices, payments, refunds"},
{"id": "technical", "description": "Bugs, outages, system errors"},
{"id": "sales", "description": "Pricing and account questions"}]}`
req, err := http.NewRequest("POST", "https://api.2ba.ai/v1/choices", strings.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer your_2ba_api_key")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var d struct {
Choice string `json:"choice"`
VoteSplit map[string]int `json:"vote_split"`
Abstained bool `json:"abstained"`
}
if err := json.NewDecoder(res.Body).Decode(&d); err != nil {
panic(err)
}
fmt.Println(d.Choice, d.VoteSplit, d.Abstained)
}
// Node 18+, Deno and Bun: fetch is built in.
const res = await fetch("https://api.2ba.ai/v1/choices", {
method: "POST",
headers: {
Authorization: "Bearer your_2ba_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
state: "Customer was charged twice for the same invoice.",
question: "Which queue should handle this request?",
options: [
{ id: "billing", description: "Invoices, payments, refunds" },
{ id: "technical", description: "Bugs, outages, system errors" },
{ id: "sales", description: "Pricing and account questions" },
],
}),
});
const decision = await res.json();
console.log(decision.choice, decision.vote_split, decision.abstained);
Historical evaluation
Historical evaluation of the earlier chat-based implementation on a fixed 36-item dataset: one run per configuration, no retries, no prompt tuning per item. Native scoring has not yet been re-evaluated.
01 / Product categorization
Items assigned the correct category on the same fixed dataset. Higher is better.
86%correct
81%correct
The earlier path measured ~150 ms and ~4 output tokens per decision. Native scoring now generates zero output tokens. These historical accuracy and latency figures do not establish the native path’s performance.
Internal evaluation, single run per configuration on a 36-item dataset. Not a general-purpose benchmark; validate on your own data before routing production traffic. Coding benchmarks live on the quality page.
Reference
Label probabilities can be high on correct and incorrect answers alike. Neither confidence nor vote_split is calibrated uncertainty; validate review thresholds on your own data.
Native scoring reads the exact answer-label probabilities without logit_bias, text generation or output parsing. Scores can still shift with cache state and batch composition.
The native choice set is capped at 26 (A–Z); /v1/choices accepts one option set per request. For larger sets, compose hierarchically: choose a branch first, then a leaf.
The questions or rotations form one native batch. An upstream failure or incomplete scoring response returns 502; no partial decision is returned.
Get started
Pair your browser, create an API key and send your first decision.