Decisions

A decisions model answers typed questions about a text with probabilities. It writes no text: you get numbers your code can act on.

How it differs from chat

A chat model writes an answer. A decisions model reads your text, answers the questions you typed about it, and returns probabilities. There is no prose to parse: your code reads a number and acts on it.

  • What comes backChat: Text, written token by tokenDecisions: A number per question: a probability, a pick or a place on a scale
  • Billed onChat: Input and output tokensDecisions: Input tokens only, priced by the size of the input
  • Request sizeChat: The model's context lengthDecisions: The model's context length; a longer input can cost more per token
  • StreamingChat: Yes, with stream: trueDecisions: No: one JSON answer
  • Tools and samplingChat: Tools, temperature and the restDecisions: None: only model, state and questions are read, other fields are ignored
  • Repeat callsChat: Can differ: replies are sampledDecisions: On Clef, the same request returns the same numbers: there is no sampling
  • SpeedChat: Grows with the length of the replyDecisions: Fast: no text to generate, one round trip
  • EndpointChat: /v1/chat/completionsDecisions: /v1/systemone

Use it when the answer is a choice your code makes:

  • Triage. Which queue a message belongs in, and how urgent it is.
  • Routing. Which team, tool or model should take a request.
  • Moderation flags. Whether a text contains something you look for, as a probability you set your own threshold on.
  • Scoring. How well a text meets a rubric, on a scale you define.
  • Comparing two texts. Put both in state as A and B, and ask a choice question between a and b.

Use a chat model for anything that needs written text: answers, summaries, translations, code. Clef is free for inputs up to about 1,500 tokens until November 4, 2026, 00:00 UTC; larger inputs, and every other decisions model, are billed on input tokens only.

The call

Send the text as state and your questions as questions to POST https://api.klinn.dev/v1/systemone, with a model key as the bearer token. The examples use Clef, Cloudflare's open decision model (Apache-2.0), id clef. Every model with kind: "decisions" in the catalog answers the same call: Clef Flash, Perplexity's Decider and others.

curl https://api.klinn.dev/v1/systemone \
  -H "Authorization: Bearer $KLINN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "clef",
    "state": "I was charged twice for my order and nobody answers my emails. I need this fixed today.",
    "questions": {
      "frustrated": { "type": "noul", "instructions": "Is the customer frustrated?" },
      "topic": {
        "type": "choice",
        "instructions": "What is the message about?",
        "criteria": { "refund": "Money back", "bug": "Something broken" }
      },
      "urgency": { "type": "score", "criteria": ["Can wait", "This week", "Today"] }
    }
  }'

The same call from Python and from JavaScript (Node 18 or later). Keep the key on your server, never in a browser.

import os, requests

r = requests.post(
    "https://api.klinn.dev/v1/systemone",
    headers={"Authorization": f"Bearer {os.environ['KLINN_API_KEY']}"},
    json={
        "model": "clef",
        "state": "I was charged twice for my order and nobody answers my emails.",
        "questions": {
            "frustrated": {"type": "noul", "instructions": "Is the customer frustrated?"},
            "topic": {"type": "choice", "criteria": {"refund": "Money back", "bug": "Something broken"}},
        },
    },
    timeout=30,
)
r.raise_for_status()
answers = r.json()["answers"]
if answers["frustrated"]["noul"] > 0.8 and answers["topic"]["choice"] == "refund":
    print("Send to the refunds team, flagged urgent")
const res = await fetch("https://api.klinn.dev/v1/systemone", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.KLINN_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    model: "clef",
    state: "I was charged twice for my order and nobody answers my emails.",
    questions: {
      frustrated: { type: "noul", instructions: "Is the customer frustrated?" },
      topic: { type: "choice", criteria: { refund: "Money back", bug: "Something broken" } },
    },
  }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const { answers } = await res.json();
if (answers.frustrated.noul > 0.8 && answers.topic.choice === "refund") {
  console.log("Send to the refunds team, flagged urgent");
}

If you sell Klinn on your own domain, your customers call the same path there, POST https://api.yourbrand.com/v1/systemone, with their own keys, at your price. A model you turned off answers 404 for them.

In the catalog, a decisions model has kind: "decisions" and endpoint: "/v1/systemone", a path on the API host. It does not answer /v1/chat/completions, and a chat model does not answer here: either way is 404 model_not_found.

Decisions need a model key. x402 does not pay them: a call with no key gets 401 invalid_api_key.

Jev-API compatible

Clef takes the same request body as the Jev API and returns the same answers, so code written for it works here: point it at https://api.klinn.dev/v1/systemone, use a Klinn key and the model clef.

  • The same body: model, state, and questions with type noul, choice or score, optional instructions, and criteria: an object of options for a choice, an ordered list of labels for a score.
  • The same answers, keyed by question name, passed through as the model returns them.
  • Only model, state and questions are read. Any other top-level field is dropped, with no error. The state is text, or JSON: an object or an array.
  • The envelope around the answers is a superset: model, answers and usage as usual, plus id, object: "decision" and usage.cost. Errors use the OpenAI shape, and answers are not streamed.

Questions

Each question has a name, its key in questions, and a type. instructions is optional: the question in plain words. Its answer comes back under the same name.

  • noulYes or no. No criteria. Returns noul, the probability that the answer is yes.
  • choiceOne option from a set. criteria maps each option to a short description. Returns the option picked, a confidence, and a probability for every option.
  • scoreA place on an ordered scale. criteria lists the labels in order, the first at position 0. Returns a score, a confidence, a probability for every position, and a legend.

The answer

One JSON body. Decisions are not streamed.

{
  "id": "dec-…",
  "object": "decision",
  "model": "clef",
  "answers": {
    "frustrated": { "type": "noul", "noul": 0.9 },
    "topic": {
      "type": "choice",
      "choice": "refund",
      "confidence": 0.69,
      "probabilities": { "refund": 0.92, "bug": 0.08 }
    },
    "urgency": {
      "type": "score",
      "score": 1.8436,
      "confidence": 0.66,
      "probabilities": { "0": 0.0314, "1": 0.0936, "2": 0.875 },
      "legend": { "0": "Can wait", "1": "This week", "2": "Today" }
    }
  },
  "usage": { "input_tokens": 299, "output_tokens": 0, "cost": 0 }
}
  • noul is the probability that the answer is yes, from 0 to 1.
  • choice is the option picked. probabilities gives every option's probability, and confidence, from 0 to 1, says how sure the model is of its pick: higher is surer.
  • score is the expected position on the scale: each position times its probability, added up (0 × 0.0314 + 1 × 0.0936 + 2 × 0.875 = 1.8436). It sits between This week (1) and Today (2), close to Today. probabilities is keyed by position, and legend names each position.

usage.input_tokens, the input tokens the model counted, is what the call is billed on. output_tokens is always 0.

Decision gateways

Save the model, a context line and the questions once as a gateway of kind decision, and each call sends only the text: your own classifier, named per request or made a key's default. Make one under Gateway → New gateway → Decisions in the console: pick a decision model (Decider v1.1 is preselected, with our price per million input tokens), add the questions, try it on sample texts in its Playground, and attach it to a key.

{
  "name": "otc-rfq",
  "kind": "decision",
  "default_model": "pplx-decider-v1.1-27b",
  "instructions": "Chat messages from an OTC crypto desk; the last message is the one to classify.",
  "questions": {
    "intent":  { "type": "choice", "options": ["rfq", "indication_of_interest", "chit_chat", "trade_confirm", "cancel"] },
    "side":    { "type": "choice", "options": ["buy", "sell", "two_way", "unknown"] },
    "urgency": { "type": "choice", "options": ["now", "today", "no_rush"] },
    "is_real": { "type": "score", "instructions": "Is this a real request for a quote?" }
  },
  "cache": { "ttl_seconds": 3600 }, "fallbacks": ["clef"], "retry": 2, "rate_limit": { "rpm": 600 }
}

Calling it is three lines. The gateway fills in the model, the context and the questions, and answers the usual decision envelope.

curl https://api.klinn.dev/v1/systemone -H "Authorization: Bearer $KLINN_API_KEY" \
  -H "x-klinn-gateway: otc-rfq" -H "Content-Type: application/json" \
  -d '{ "state": "Looking for a two-way price on 50 BTC, need it now" }'
  • A question is choice with 2 to 64 options, score (a number from 0 to 1; optionally 2 to 10 options as levels, lowest first) or noul (yes or no). Names are 1 to 40 characters of a-z, 0-9 and _, starting with a letter; 1 to 128 questions; an instructions line of up to 500 characters each, and up to 2,000 for the gateway's context.
  • The body may add questions for one call, and name a model the gateway allows. Everything else comes from the config.
  • The routing rules apply as on chat: a fallback across decision models, retries, the response cache (the same text gets the kept answers, free), a rate limit, a time limit and the models allowed. Guardrail rules do not: a decision gateway is the check. Answers carry x-klinn-gateway, x-klinn-target and x-klinn-cache.
  • Billed as any decisions call: input tokens at the model's price, output free, the gateway free. The console shows, per question, how the answers split over the last 24 hours or 7 days, and Logs filters on the gateway and the decisions endpoint.

Price and limits

A decisions call is billed on input tokens only, as the model counts them in usage.input_tokens, at the price for the size of the input. In the catalog, price.promptUsdPerM is the price for the smallest inputs and price.longContext lists the larger sizes: from aboveTokens input tokens, the whole call is billed at that size's price. Each answer carries the price it was billed at in x-price-per-million: input=…, in USD per million input tokens, and usage.cost is what the call was charged, in USD.

Clef is free for inputs up to about 1,500 tokens until November 4, 2026, 00:00 UTC, with no free-tier caps: only your key's usual rate limit applies, and it needs no balance. A larger input is billed at its size's price, so it needs a balance (402 insufficient_balance without one). The catalog has the current prices; free and freeUntil describe the smallest size only.

A reseller's customers call it with their own keys, at the reseller's price. Their header and usage.cost show that price.

  • Text and questions: up to the model's contextLength in the catalog, counted at about four characters a token. Over it is 400 context_length_exceeded, and the message names the limit.
  • The request body: up to 256 KB. More is 413.
  • Text only: no images.
  • A model key is required: x402 does not pay decisions.

Errors

Errors use the OpenAI shape, as on chat. 400 when state or questions is missing, 402 insufficient_balance for a priced call on a key with no balance, 403 model_not_allowed when the key is limited to other models, 429 with a Retry-After when the key is over its request rate or the model is busy, and 502 model_unavailable when the model did not answer: retry shortly. Every code is on Errors.