Skip to content

Getting started

Terminal window
pip install curva-ai
export OPENROUTER_API_KEY=sk-or-v1-... # any OpenRouter key; free models work
import curva
d = 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 None

There 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:

Terminal window
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-auth

Then, 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.

The Python SDK and the curva server come in one package:

Terminal window
pip install curva-ai
curva --version # curva 0.1.0

Platform 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:

Terminal window
export OPENROUTER_API_KEY=sk-or-v1-...

To use a different OpenAI-compatible endpoint, set CURVA_PROVIDER_URL (see Self-hosting).

curva.local() starts a private server for this Python process on a free localhost port, and stops it when Python exits:

import curva
from 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.9999
print(d["frustration"].score) # 0.65
print(d["refund"].noul) # 0.999
print(d.mode, d.latency_ms, d.cost_usd, d.cached)
  • state is any JSON value: a string, an object or an array. It is treated as data, never as instructions.
  • questions maps 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. Pass model= to change the default model and db= to use another file.

Use it as a context manager to stop the server early:

with curva.local() as client:
...

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.

Run one server for a team or a service, and point clients at it:

Terminal window
curva serve --addr 127.0.0.1:7777 --db curva.db
from curva import Curva
client = Curva("http://your-server:7777") # or set CURVA_BASE_URL

The 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.

Any language can call the server directly:

Terminal window
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.

© 2026 Tarkova Private Limited.