Getting started
In three lines
Section titled “In three lines”pip install curva-aiexport OPENROUTER_API_KEY=sk-or-v1-... # any OpenRouter key; free models workimport curvad = curva.decide("I was charged twice, please refund me", {"team": ["billing", "technical"], "refund": "Asks for a refund?", "total": float})print(d.team, d.refund, d.total) # billing True NoneThere is nothing to set up. The first curva.decide starts a private server for this Python
process, reuses it for every later call and stops it when Python exits. With CURVA_BASE_URL
set, it talks to that server instead.
Shorthand questions. Each value in the questions dict can be:
| Shorthand | Question |
|---|---|
["billing", "technical"] or {"billing": "payments", "technical": "bugs"} |
Choice |
"Asks for a refund?" (a string ending in ?) |
Noul (yes/no) |
bool |
Noul, asked with the key: "is_urgent": bool asks “Is urgent” |
float, int, str |
Number, Integer, Text, asked with the key: "total_due": float asks “Total due” |
Choice(...), Score(...), … or {"type": ...} |
used as is |
Anything else raises a ValueError with an example.
Plain answers. d.team is the chosen option, d.refund is True when P(yes) ≥ 0.5,
d.total is the number or None, and d.to_dict() returns all of them as a dict. d["team"]
is still the full answer, with confidence and probabilities. The decision’s own fields (id,
model, project, mode, …) come first, so for a question with such a name use d["id"].
Feedback. curva.feedback(d.id, "team", "billing") and curva.calibration("team") use the
same server. See Teach it your data.
Other providers. Without OPENROUTER_API_KEY, the first key set among OPENAI_API_KEY,
ANTHROPIC_API_KEY, GEMINI_API_KEY (or GOOGLE_API_KEY), GROQ_API_KEY, MISTRAL_API_KEY,
DEEPSEEK_API_KEY, TOGETHER_API_KEY, FIREWORKS_API_KEY and XAI_API_KEY picks a small, fast
model that the provider lists for your key, such as @openai/gpt-4.1-mini or @anthropic/claude-haiku-4-5. Providers
rename models often: check the current names, and set CURVA_MODEL to choose, e.g.
CURVA_MODEL=@groq/openai/gpt-oss-20b or CURVA_MODEL=@ollama/qwen3:4b for a local model
(see Model providers). With no key at all, curva.decide raises an error
naming the variables to set.
TypeScript. The TypeScript SDK doesn’t start a server. Run one with curva serve (from
pip install curva-ai) or with Docker:
docker run --rm -p 127.0.0.1:7777:7777 -e OPENROUTER_API_KEY ghcr.io/itsmohitrohilla/curva \ serve --addr 0.0.0.0:7777 --no-authThen, with the same shorthand (Number, String and Boolean stand for the types):
import { decide } from "curva-ai"; // CURVA_BASE_URL, default http://localhost:7777
const d = await decide("I was charged twice, please refund me", { team: ["billing", "technical"], refund: "Asks for a refund?", total: Number });d.values; // { team: "billing", refund: true, total: null }d.answers.team.confidence;The rest of this page is the full API, for when you want more control.
Install
Section titled “Install”The Python SDK and the curva server come in one package:
pip install curva-aicurva --version # curva 0.1.0Platform wheels for Linux (x86_64, aarch64), macOS (Apple Silicon, Intel) and Windows include the server binary, so you don’t need Rust. The SDK itself uses only the Python standard library (Python 3.9 or newer).
Curva calls models through OpenRouter by default. Any OpenRouter key works, and free models are fine for trying it out:
export OPENROUTER_API_KEY=sk-or-v1-...To use a different OpenAI-compatible endpoint, set CURVA_PROVIDER_URL (see
Self-hosting).
Your first decision
Section titled “Your first decision”curva.local() starts a private server for this Python process on a free localhost port, and
stops it when Python exits:
import curvafrom curva import Choice, Score, Noul
client = curva.local()d = client.decide( state={"ticket": "I was charged twice for order A-104. Please refund the duplicate!"}, questions={ "team": Choice("Which team should handle this?", {"billing": "payments, refunds", "technical": "bugs", "sales": "pricing"}), "frustration": Score("How frustrated is the customer?", ["calm", "annoyed", "angry"]), "refund": Noul("The customer explicitly asks for a refund"), },)
print(d["team"].choice, d["team"].confidence) # billing 0.9999print(d["frustration"].score) # 0.65print(d["refund"].noul) # 0.999print(d.mode, d.latency_ms, d.cost_usd, d.cached)stateis any JSON value: a string, an object or an array. It is treated as data, never as instructions.questionsmaps your own keys to typed questions. All of them are answered in one request.curva.local()keeps its database at~/.curva/curva.db, so calibration learned in one run is there in the next. Passmodel=to change the default model anddb=to use another file.
Use it as a context manager to stop the server early:
with curva.local() as client: ...Teach it your data
Section titled “Teach it your data”When you learn the true answer, send it back. This is what makes the probabilities trustworthy:
client.feedback(d.id, "team", "billing")From 30 labels for a question, Curva calibrates its answers whenever that makes them more accurate
(calibrated=True); a model that is already well calibrated is left as it is. See
Calibrate with feedback.
Use a shared server
Section titled “Use a shared server”Run one server for a team or a service, and point clients at it:
curva serve --addr 127.0.0.1:7777 --db curva.dbfrom curva import Curva
client = Curva("http://your-server:7777") # or set CURVA_BASE_URLThe client reads CURVA_BASE_URL (default http://localhost:7777) and CURVA_API_KEY when
they aren’t passed. Once the server has API keys, every request needs one; see
Self-hosting.
Plain HTTP
Section titled “Plain HTTP”Any language can call the server directly:
curl -s localhost:7777/v1/decide -H 'content-type: application/json' -d '{ "state": {"ticket": "The app crashes on launch"}, "questions": {"team": {"type": "choice", "instructions": "Which team?", "options": {"billing": "", "technical": ""}}}}'The full contract is in the HTTP API reference.

