Skip to content

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:

TypeWhat you provideWhat comes back
noulAn instruction question, plus optional descriptions for the yes and no outcomesThe probability of a yes answer
choiceAn instruction question and a map of labels to descriptionsThe selected label and the probability of each label
scoreAn instruction question and an ordered rubric of at least two levelsThe 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 choice and score answers.
  • 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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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
  }
}
FieldDescription
modelThe model that answered the request
answersOne entry per question name sent
answers[].typenoul, choice or score, matching the question
answers[].noulProbability of yes for a noul question, from 0 to 1
answers[].choiceSelected label for a choice question
answers[].scoreExpected score for a score question
answers[].confidenceReported confidence for choice and score answers
answers[].probabilitiesProbability per label or per score level
answers[].legendRubric descriptions keyed by score, for score answers
usage.input_tokensInput tokens measured by the provider
usage.output_tokensOutput tokens reported by the provider (not billed, see below)

Confidence and Probabilities ​

Read the distribution, not only the winning answer.

  • noul returns a single probability. noul: 0.5 means the two outcomes are equally plausible, not a weak yes.
  • For choice, probabilities sum to 1 across the labels. A confidence of 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, score is the expected value and probabilities describe 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.

BaseExecution routeModel listing
https://api.sippulse.ai/v1POST /decisionsGET /models
https://api.sippulse.aiPOST /v1/systemoneGET /v1/models
https://api.sippulse.ai/apiPOST /api/v1/systemone, POST /api/alpha/decisionsGET /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 ​

bash
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"]
      }
    }
  }'
python
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"])
javascript
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.

bash
curl 'https://api.sippulse.ai/v1/models' \
  -H 'api-key: $SIPPULSE_API_KEY'
json
{
  "models": [
    {
      "name": "jev-1.13.0",
      "description": "Decision model",
      "release_date": "2026-09-20"
    }
  ]
}
bash
curl 'https://api.sippulse.ai/api/v1/models' \
  -H 'api-key: $SIPPULSE_API_KEY'
json
{
  "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.

javascript
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);
python
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.

javascript
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_tokens is what is billed.
  • usage.output_tokens is 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:

LimitDescription
Total contextMaximum tokens for state plus every question, including instructions and criteria
Largest questionMaximum tokens for state plus the single largest question
Choice optionsMaximum number of labels in a choice criteria map
Score rangeMinimum 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 ​

CodeMeaning
400Malformed request or invalid parameters, including invalid question shape and unsupported provider preferences
401Invalid or missing SipPulse API key
402Insufficient credits
404Model does not exist or is not available to your organization
413Request payload too large
429Rate limit exceeded; respect the Retry-After header
500Internal server error
502 / 503Upstream provider error or temporarily unavailable
504Timeout

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.

json
{
  "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.

javascript
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}`);
}
python
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 ​