Decision Models
Decision models answer structured questions about a piece of state. Instead of generating free-form text, they return one answer per named question: a binary judgment, a selected label, or a score on a rubric. The platform serves them through the same API-key authentication and credit billing used by the other model types.
The first model of this type is Jev (jev-1.13.0), served by TypeSafe. You can ask several questions at once over the same state, and the response keeps the type of each question: noul, choice, or score.
Decision models are a separate type from Text Generation. They do not accept chat messages, generation parameters, or tool definitions, and they are not available in the agent, thread, speech, or embedding selectors.
For the current list of decision models and their prices, see the Pricing page and the model selector in the platform.
Interactive Playground
The Decisions Playground lets you write a state, define questions, and inspect every answer with its distribution and confidence.
State
The state is the text or JSON the model reasons about. Paste a support message, a document fragment, or a JSON object. The state can be a string, a JSON object, a JSON array, or null.
Questions
Add one or more named questions. Each question has an editor for its type:
| Type | What you provide | What comes back |
|---|---|---|
noul | An instruction question, plus optional descriptions for the yes and no outcomes | The probability of a yes answer |
choice | An instruction question and a map of labels to descriptions | The selected label and the probability of each label |
score | An instruction question and an ordered rubric of at least two levels | The expected score, the rubric (legend), and the probability of each level |
Question names are yours. Use the same name in the editor and in the response to correlate them.
Execution and Visualization
Run the request to see one card per question:
- The selected answer, highlighted in the distribution.
- The full distribution as bars or a chart, not just the winning option.
- The confidence reported for
choiceandscoreanswers. - Token usage and the request ID.
The Playground does not simulate a conversation. It is a single request over a single state.
TIP
Every question runs in the same request. Asking a safe/not-safe question, a category question, and an urgency question together costs one input measurement, not three separate calls.
Decision Question Types
A request carries a state and a non-empty object of questions keyed by name. Each question declares its type.
Binary questions (noul)
A noul question asks a yes/no judgment and returns the probability of yes.
{
"state": "I was charged twice for the same subscription. Please fix it.",
"questions": {
"billing_related": {
"type": "noul",
"instructions": "Is this request about billing?",
"criteria": {
"true": "A billing or payment issue",
"false": "Anything else"
}
}
}
}Criteria are optional and describe what yes and no mean. They are not translated or rewritten by the platform: send them in the language you want the model to use.
Choice questions (choice)
A choice question selects one label from the criteria map and returns the probability of each label.
{
"state": "I was charged twice for the same subscription. Please fix it.",
"questions": {
"category": {
"type": "choice",
"instructions": "Pick the support category.",
"criteria": {
"billing": "Payment, invoices and charges",
"technical": "Errors, bugs and outages",
"account": "Login, profile and access"
}
}
}
}The criteria keys are the labels. They are your data and are returned unchanged in choice and in probabilities.
Score questions (score)
A score question assigns a score on an ordered rubric. The rubric is an array of at least two descriptions, indexed from zero. The returned score is the expected value and can fall between integer levels.
{
"state": "I need this resolved before tomorrow morning or my account is suspended.",
"questions": {
"urgency": {
"type": "score",
"instructions": "How urgent is this request?",
"criteria": ["Low", "Medium", "High"]
}
}
}The response repeats the rubric as legend, keyed by score as numeric strings. Use the legend to label the distribution in your own interface.
Response
Answers are keyed by the question name you sent. Each answer keeps its type.
{
"model": "jev-1.13.0",
"answers": {
"billing_related": {
"type": "noul",
"noul": 0.98
},
"category": {
"type": "choice",
"choice": "billing",
"confidence": 0.91,
"probabilities": {
"billing": 0.91,
"technical": 0.05,
"account": 0.04
}
},
"urgency": {
"type": "score",
"score": 0.78,
"confidence": 0.58,
"legend": { "0": "Low", "1": "Medium", "2": "High" },
"probabilities": { "0": 0.25, "1": 0.72, "2": 0.03 }
}
},
"usage": {
"input_tokens": 367,
"output_tokens": 62
}
}| Field | Description |
|---|---|
model | The model that answered the request |
answers | One entry per question name sent |
answers[].type | noul, choice or score, matching the question |
answers[].noul | Probability of yes for a noul question, from 0 to 1 |
answers[].choice | Selected label for a choice question |
answers[].score | Expected score for a score question |
answers[].confidence | Reported confidence for choice and score answers |
answers[].probabilities | Probability per label or per score level |
answers[].legend | Rubric descriptions keyed by score, for score answers |
usage.input_tokens | Input tokens measured by the provider |
usage.output_tokens | Output tokens reported by the provider (not billed, see below) |
Confidence and Probabilities
Read the distribution, not only the winning answer.
noulreturns a single probability.noul: 0.5means the two outcomes are equally plausible, not a weak yes.- For
choice,probabilitiessum to 1 across the labels. Aconfidenceof 0.55 with a 0.30 second label is a much weaker decision than a 0.95 with a 0.02 second label. - For
score,scoreis the expected value andprobabilitiesdescribe the level distribution. A high mean with probability spread across two distant levels is not the same as a concentrated answer. - High confidence is not proof of correctness. Calibrate a threshold against your own data before acting automatically on an answer.
The platform never invents a distribution or a confidence value. If a provider cannot report them for a question type, that question type is not offered.
REST API
Decision requests go to the canonical route POST /v1/decisions. The same execution service answers the compatibility aliases used by existing clients.
| Base | Execution route | Model listing |
|---|---|---|
https://api.sippulse.ai/v1 | POST /decisions | GET /models |
https://api.sippulse.ai | POST /v1/systemone | GET /v1/models |
https://api.sippulse.ai/api | POST /api/v1/systemone, POST /api/alpha/decisions | GET /api/v1/models |
All execution routes accept the same body and return the same answer shape. Use the canonical route for new integrations.
Authentication
Send your SipPulse API key in the api-key header, or as Authorization: Bearer <key> when your client expects a bearer token.
Request example
curl -X POST 'https://api.sippulse.ai/v1/decisions' \
-H 'Content-Type: application/json' \
-H 'api-key: $SIPPULSE_API_KEY' \
-d '{
"model": "jev-1.13.0",
"state": "I was charged twice for the same subscription. Please fix it.",
"questions": {
"billing_related": {
"type": "noul",
"instructions": "Is this request about billing?",
"criteria": { "true": "A billing or payment issue", "false": "Anything else" }
},
"urgency": {
"type": "score",
"instructions": "How urgent is this request?",
"criteria": ["Low", "Medium", "High"]
}
}
}'import os
import requests
response = requests.post(
"https://api.sippulse.ai/v1/decisions",
headers={
"Content-Type": "application/json",
"api-key": os.environ["SIPPULSE_API_KEY"],
},
json={
"model": "jev-1.13.0",
"state": "I was charged twice for the same subscription. Please fix it.",
"questions": {
"billing_related": {
"type": "noul",
"instructions": "Is this request about billing?",
"criteria": {"true": "A billing or payment issue", "false": "Anything else"},
},
"urgency": {
"type": "score",
"instructions": "How urgent is this request?",
"criteria": ["Low", "Medium", "High"],
},
},
},
)
response.raise_for_status()
print(response.json()["answers"])const response = await fetch("https://api.sippulse.ai/v1/decisions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"api-key": process.env.SIPPULSE_API_KEY,
},
body: JSON.stringify({
model: "jev-1.13.0",
state: "I was charged twice for the same subscription. Please fix it.",
questions: {
billing_related: {
type: "noul",
instructions: "Is this request about billing?",
criteria: { true: "A billing or payment issue", false: "Anything else" },
},
urgency: {
type: "score",
instructions: "How urgent is this request?",
criteria: ["Low", "Medium", "High"],
},
},
}),
});
if (!response.ok) {
throw new Error(`Decisions API error: ${response.status}`);
}
const { answers, usage } = await response.json();
console.log(answers.billing_related.noul, usage.input_tokens);Model listing
The listing route follows your SDK. The TypeSafe SDK expects a models array; the OpenRouter-style client expects a data array. Both are served by the same model, in the shape each client can unwrap.
curl 'https://api.sippulse.ai/v1/models' \
-H 'api-key: $SIPPULSE_API_KEY'{
"models": [
{
"name": "jev-1.13.0",
"description": "Decision model",
"release_date": "2026-09-20"
}
]
}curl 'https://api.sippulse.ai/api/v1/models' \
-H 'api-key: $SIPPULSE_API_KEY'{
"data": [
{
"id": "jev-1.13.0",
"name": "jev-1.13.0"
}
]
}Compatible SDKs
The decision protocol is compatible with the TypeSafe and OpenRouter client conventions. What changes between them is only how the base URL and credentials are configured.
TypeSafe SDK
Point the client at the SipPulse host and use your SipPulse API key. The SDK sends Authorization: Bearer, which the platform accepts. client.systemOne(...) and client.models.list() work unchanged.
import { TypeSafeClient, noul, score } from "@typesafe-ai/sdk";
const client = new TypeSafeClient({
apiKey: process.env.SIPPULSE_API_KEY,
baseURL: "https://api.sippulse.ai",
});
const { answers, usage } = await client.systemOne({
state: "I was charged twice for the same subscription. Please fix it.",
questions: {
billing_related: noul("Is this request about billing?", {
true: "A billing or payment issue",
false: "Anything else",
}),
urgency: score("How urgent is this request?", ["Low", "Medium", "High"]),
},
});
console.log(answers.billing_related.noul, usage.input_tokens);import os
from typesafe_sdk import Noul, Score, TypeSafeClient
with TypeSafeClient(
api_key=os.environ["SIPPULSE_API_KEY"],
base_url="https://api.sippulse.ai",
) as client:
result = client.system_one(
state="I was charged twice for the same subscription. Please fix it.",
questions={
"billing_related": Noul(instructions="Is this request about billing?"),
"urgency": Score(
instructions="How urgent is this request?",
criteria=["Low", "Medium", "High"],
),
},
)
print(result.nouls["billing_related"].noul, result.usage.input_tokens)OpenRouter client
The OpenRouter client does not accept a serverURL in its constructor for the decisions operation. Its alphaDecisionsCreate call computes the base URL from a per-call serverURL option and otherwise falls back to the OpenRouter default. Pass the SipPulse host per call; there is no gateway alias that changes this.
import { OpenRouter } from "@openrouter/sdk";
const openrouter = new OpenRouter({
apiKey: process.env.SIPPULSE_API_KEY,
});
const result = await openrouter.alpha.decisions.create(
{
model: "jev-1.13.0",
state: "I was charged twice for the same subscription. Please fix it.",
questions: {
billing_related: {
type: "noul",
instructions: "Is this request about billing?",
},
},
},
{ serverURL: "https://api.sippulse.ai" },
);Provider routing options are not supported
The platform answers the decision questions and preserves the response contract, but it does not implement OpenRouter's commercial routing: privacy policies, data retention, provider ordering, fallbacks and upstream selection are not honored. A request that asks for those preferences is rejected instead of silently ignoring them, so an option you see in the OpenRouter schema is not necessarily supported here.
Vercel AI SDK
The AI SDK provider derives the /api/alpha path from a base URL that ends in /api/v1. Configure the provider base URL accordingly; no per-call override is needed.
Usage and Billing
Decision requests are billed by input tokens only, at cost, per the model's price policy. The current Jev rate is US$ 0.042 per million input tokens, and the same rate applies to every plan that offers the model. The output tokens reported by the provider are kept as usage metadata and are not charged.
This is why the response always carries both counters:
usage.input_tokensis what is billed.usage.output_tokensis reported for observability and reconciliation, and its cost is already included in the input-only rate.
Prices are defined in USD. Your organization's currency applies the current exchange rate and applicable taxes when the platform converts the amount, so the value in your local currency is not a fixed multiple of the USD number.
Where the usage appears
In the Dashboard, a decision call produces one usage item of type Decision with the billing rule Input Token, the canonical model identifier, the billed input quantity, and the cost for that call. The request table and request details show the same request ID, project, requester, and total.
The output token count is not a separate billable line. If you reconcile totals, sum the decision items by their input quantity; do not expect a matching output item.
Zero and very small amounts
A decision call can be cheap enough that its billed amount rounds to a very small value. Usage reporting keeps the decimal precision of each call, while credits settle the amount actually debited. Because settlement is per call, a very large number of near-zero calls is not guaranteed to add up exactly to the raw token cost.
Limits
The model declares its decision limits. Limits cover the composed context, not just the state:
| Limit | Description |
|---|---|
| Total context | Maximum tokens for state plus every question, including instructions and criteria |
| Largest question | Maximum tokens for state plus the single largest question |
| Choice options | Maximum number of labels in a choice criteria map |
| Score range | Minimum and maximum number of rubric levels in a score criteria array |
The gateway validates the request shape and the option and rubric cardinality before running inference. An invalid request fails with 400 and is not billed. A request larger than 2 MiB (serialized UTF-8) is rejected before inference with 413; nesting deeper than 64 levels is rejected with 400. There is no separate cap on the number of questions. The two token ceilings are enforced by the model itself, so an oversized request is rejected there instead of being re-counted by the platform.
Rate limits are applied per organization and shared across every alias of the same model. latest and fixed-version names do not have separate counters or prices.
Errors
| Code | Meaning |
|---|---|
400 | Malformed request or invalid parameters, including invalid question shape and unsupported provider preferences |
401 | Invalid or missing SipPulse API key |
402 | Insufficient credits |
404 | Model does not exist or is not available to your organization |
413 | Request payload too large |
429 | Rate limit exceeded; respect the Retry-After header |
500 | Internal server error |
502 / 503 | Upstream provider error or temporarily unavailable |
504 | Timeout |
Decision routes return errors in a shape the OpenRouter SDK can validate: code is the numeric HTTP status, and platform_code is the stable platform error string. Branch on platform_code in code; the numeric code mirrors the response status.
{
"error": {
"statusCode": 404,
"name": "NotFoundError",
"message": "The model 'jev-does-not-exist' does not exist or is not available for your organization.",
"code": 404,
"platform_code": "model_not_found"
}
}An invalid SipPulse key returns 401. A problem with the upstream provider credential does not: it is reported as a provider error (5xx), so a client does not close a valid session over an upstream issue.
const response = await fetch("https://api.sippulse.ai/v1/decisions", {
method: "POST",
headers: { "Content-Type": "application/json", "api-key": key },
body: JSON.stringify(payload),
});
if (!response.ok) {
const { error } = await response.json();
if (error.platform_code === "model_not_found") {
// Handle the stable platform error.
}
throw new Error(`${error.code}: ${error.platform_code ?? error.message}`);
}resp = requests.post(url, headers=headers, json=payload)
if resp.status_code >= 400:
error = resp.json()["error"]
if error.get("platform_code") == "model_not_found":
pass # Handle the stable platform error.
raise RuntimeError(f"{error['code']}: {error.get('platform_code', error['message'])}")Model Identity and Aliases
The canonical model identifier is jev-1.13.0. The platform also accepts a short list of documented aliases, including jev-latest (the TypeSafe SDK default), jev-1.13, and typesafe/jev-1.13. Aliases are explicit; arbitrary prefixes or fuzzy names are not resolved.
A fixed-version name never redirects silently to another model. latest only changes when the platform publishes a new model version, with cache invalidation. The response always reports the version that actually answered, so log answers alongside the model field.
Next Steps
- Text Generation - The LLM model type, for free-form responses
- REST API Integration - Authentication and request conventions
- Request Tracking - Correlate a request ID with its usage
- Dashboard - Understand how usage and costs are shown
