cCarebun Reading‹ All reading
Carebun Interactive/Decision models Decision labCheat sheet Study view

Part I · Novice

Chapter 2

Your first call

One tenant message, three questions, one answer. We read the request and the response of a real call field by field, run it with curl, and measure where its 625 tokens went.

12 min read · interactive

On this page
  1. The request has three parts
  2. A question has a type, instructions and criteria
  3. Sending it with curl
  4. The response has one answer per question
  5. What the call cost
  6. What the response does not contain
  7. Try it yourself: change one sentence, watch one stamp
  8. Common questions
  9. Carry this
  10. Check yourself

In this chapter, we will learn to read a request and a response field by field. We will take the one call from Chapter 1, print all of it, send it with one command, check three of its numbers by hand, work out its cost and measure where the tokens went. At the end we will list what the answer does not contain, which matters as much.

The request has three parts

A request is one JSON object with three fields: model, state and questions. JSON is a plain text format for names and values: an object is a set of named values inside curly brackets. Experiment 0001 printed the request before sending it. This is that print, complete.

REQUEST
{
  "model": "typesafe/jev-1.13",
  "state": "Tenant message, flat 4B: Water is dripping through my bathroom ceiling light. It started an hour ago and it is getting faster. I am at work until 6, you can let yourself in.",
  "questions": {
    "trade": {
      "type": "choice",
      "instructions": "Which trade should handle this request?",
      "criteria": {
        "plumbing": "Leaks, drains, taps, toilets, water pressure.",
        "electrical": "Power, lights, sockets, fuse box.",
        "heating": "Radiators, boiler, hot water, air conditioning.",
        "appliance": "Oven, fridge, washing machine, dishwasher.",
        "pests": "Insects, mice, rats, birds.",
        "locks": "Doors, locks, keys, windows, intercom."
      }
    },
    "severity": {
      "type": "score",
      "instructions": "How severe is the problem described?",
      "criteria": [
        "Cosmetic. Nothing stops working.",
        "Inconvenient. Something works badly but the flat is usable.",
        "An essential service is lost: no water, no heat, no power, no working toilet or no secure door.",
        "Danger to people or to the building: fire, gas, flooding, electric shock."
      ]
    },
    "may_enter": {
      "type": "noul",
      "instructions": "The tenant has given permission to enter the flat while they are out.",
      "criteria": {
        "true": "The message clearly says staff may enter without the tenant present.",
        "false": "The message does not give that permission, or asks staff to wait."
      }
    }
  }
}
FieldWhat it isIn our call
modelwhich clerk should answertypesafe/jev-1.13, the name Jev has on OpenRouter
statethe paper on the desk: the content to judgeone tenant message, as a string
questionsthe slips: a map from a name you choose to a questionthree questions named trade, severity, may_enter

The documentation says the state may be a string, an object or an array, and that it must be text. Jev does not accept images or sound. Chapter 7 is about shaping the state. Here a single string is enough.

A question has a type, instructions and criteria

A question is a small object that names the stamp (type), asks the question (instructions) and describes the possible answers (criteria). The shape of criteria depends on the type.

typeStampcriteria isIn our request
choicetray stampan object: each option name with a descriptionsix trades
scoreruleran ordered list of level descriptions; the first is level 0four levels, 0 to 3
noulyes/no dialoptional: what true means and what false meansboth given

Look at the Noul. Its instruction is a statement, not a question. The documentation allows both forms. The dial then says how likely the statement is to be true.

One detail is easy to miss. The names trade, severity and may_enter are for our code only. TypeSafe's API reference, the page that lists every field a program may send, says of such a key: "The key is not sent to the underlying model and is not used in inference." If so, the model reads the same request when the question is named q1. Everything the clerk needs must be written in the instructions and the criteria.

Sending it with curl

A call is an HTTP POST: we send the JSON to an address and get JSON back. The tool curl does this from a terminal. Save the request above (without the word REQUEST) as request.json. Put your key, the secret string that identifies your OpenRouter account, in an environment variable, which is a named value that the terminal remembers, so that the key never appears in a file.

export OPENROUTER_API_KEY="paste-your-own-key-here"

curl https://openrouter.ai/api/alpha/decisions \
  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json

The address and the two header lines (the -H lines, which carry the key and say that the body is JSON) follow OpenRouter's own tutorial. Our experiment sent this same body to this same address from a short Python program, not from curl.

The response has one answer per question

A response is a JSON object with the model that answered, an answers map with the same names we chose, and a usage block with the bill. This is what the same program printed next.

RESPONSE
{
  "model": "typesafe/jev-1.13-20260917",
  "answers": {
    "trade": {
      "type": "choice",
      "choice": "plumbing",
      "probabilities": {
        "plumbing": 0.99,
        "pests": 0,
        "appliance": 0,
        "heating": 0,
        "electrical": 0.01,
        "locks": 0
      },
      "confidence": 0.99
    },
    "severity": {
      "type": "score",
      "score": 2.99,
      "legend": {
        "0": "Cosmetic. Nothing stops working.",
        "1": "Inconvenient. Something works badly but the flat is usable.",
        "2": "An essential service is lost: no water, no heat, no power, no working toilet or no secure door.",
        "3": "Danger to people or to the building: fire, gas, flooding, electric shock."
      },
      "probabilities": {
        "0": 0,
        "1": 0,
        "2": 0.01,
        "3": 0.99
      },
      "confidence": 0.99
    },
    "may_enter": {
      "type": "noul",
      "noul": 0.98
    }
  },
  "usage": {
    "input_tokens": 625,
    "output_tokens": 99,
    "cost": 2.625e-05
  }
}

latency: 311 ms   cached: True

We read it from the top.

  • model is longer than the name we sent. OpenRouter's tutorial says this field "names the dated snapshot that served your request". A snapshot is one fixed version of the model.
  • trade is the Choice. choice is the winning option. probabilities gives every option a number, and the numbers sum to 1. confidence is one number from 0 to 1 computed from them.
  • severity is the Score. probabilities gives each level a number. score is the position on the scale. legend repeats our level descriptions, so that the number can be read without the request.
  • may_enter is the Noul. It has one number, noul: the probability of yes. There is no confidence field. The documentation's reason is that with only two outcomes, the single number already describes everything.
  • usage is the bill. cost is in dollars, written in scientific notation: 2.625e-05 means 0.00002625.

The last printed line is ours, not the model's. Our program stores every reply on disk, and cached: True says this print replayed the stored reply. The 311 ms is the waiting time of the original call.

Three of these numbers can be checked by hand. The formulas are published by TypeSafe, and the next three chapters explain them. For now, watch them agree.

trade probabilities   0.99 + 0 + 0 + 0 + 0.01 + 0                =  1.00

trade confidence      (top probability - 1/6) / (1 - 1/6)
                      (0.99 - 0.1667) / 0.8333                   =  0.988    returned: 0.99

severity score        each level number times its probability
                      0 x 0 + 1 x 0 + 2 x 0.01 + 3 x 0.99        =  2.99     returned: 2.99

Now try it on other messages. Experiment 0002 sent the same three questions with 215 different tenant messages, and the box below holds 20 of them. Its numbers come from the recorded answers of that experiment, the file in which the program saved every response. It opens on message 10, an oven that pops and smells of burning. Before you press the button, predict three things: which trade, which level from 0 to 3, and whether the dial is near 0 or near 1.

What the call cost

The cost of a call is the number of input tokens times the price per token. A token is a small piece of text, a word or part of a word. Input tokens are what the model reads. TypeSafe publishes one price, $0.042 per million input tokens, and says output is free.

input tokens              625
price                     $0.042 per 1,000,000 input tokens
cost                      625 x 0.042 / 1,000,000      =  $0.00002625
the response says         "cost": 2.625e-05            =  $0.00002625
1,000 calls like this     0.00002625 x 1,000           =  $0.026

The two lines agree to the last digit, so the 99 output tokens were not charged. What that number counts is not explained anywhere in the documentation. We leave it alone.

Where do 625 tokens come from? The state has only 33 words and 173 characters, our short heading included. TypeSafe does not document how tokens are counted, so experiment 0027 measured it. It sent the call seven times, with pieces removed or added, and read input_tokens each time. A "bare Noul" is one yes/no question with no criteria.

state              questions                         input tokens
one word ('x')     one bare Noul, no criteria             272
the message        one bare Noul, no criteria             313
one word ('x')     trade (Choice, 6 options)              419
the message        trade                                  460
the message        trade + severity                       562
the message        trade + severity + may_enter           625
the message twice  trade + severity + may_enter           667
the smallest call we made                          272   (one-word state, one bare Noul)
the message itself                 313 - 272   =    41   (173 characters / 41  =  4.2 per token)
trade Choice instead of bare Noul  419 - 272   =   147
adding the severity Score          562 - 460   =   102
adding the may_enter Noul          625 - 562   =    63
total                 272 + 41 + 147 + 102 + 63  =  625

the message's share                41 / 625    =   6.6%
a second copy of the message       667 - 625   =    42

Against a one-word state, the message added 41 of the 625 tokens. The smallest call already reads 272, and what those 272 contain is not documented. The documentation's own example of one Noul on a short state reports 296 input tokens, a similar size. The three questions make up the rest.

Two lessons follow. Take the token count from usage, not from a word count. And on a call like this one a longer message is cheap: a whole second copy of it added 42 tokens. The questions and the fixed part are the bill.

What the response does not contain

Each answer holds a typed value and its probabilities, and no text of the model's own. It is worth saying what is missing, because each absence is a design decision you will lean on later.

  • No explanation. Nothing says why the clerk chose plumbing. The documentation states that System One models "do not write replies, produce code, or generate explanations of their reasoning".
  • No free text. In the answers above, every value is a name or a description we supplied, a type name or a number. There is no sentence to parse.
  • No answer outside the list. TypeSafe says the output is restricted to the options supplied, so the clerk cannot reply "roofing" when roofing is not an option. It can still pick the wrong option. The shape is guaranteed. The truth is not.
  • No settings. The request has no temperature, no seed and no system prompt, the randomness controls and the standing instruction that chat models offer. TypeSafe's API reference documents only the three fields we used.
  • No promise of identical repeats. TypeSafe does not claim that the same request always returns the same numbers, and OpenRouter's tutorial warns that "your probabilities will differ slightly from run to run". Chapter 15 measures how much they move.

If Rosa needs a written reason for a tenant, that is a letter. OpenRouter's guide suggests exactly this split: let Jev make the decision, then use a chat model to explain it.

Try it yourself: change one sentence, watch one stamp

You need an OpenRouter account and the two commands above. Run the call once and compare your response with ours, line by line.

Then edit the state in request.json. Delete its last sentence, "I am at work until 6, you can let yourself in." Before you run it again, write down what you expect for noul, and whether trade and severity should move at all. Next, put the sentence back and change "you can let yourself in" to "please wait until I am home".

Experiment 0028 made both edits. It also rewrote the problem twice: a bathroom tap that has dripped a little for weeks, and a ceiling light that flickers and goes out. In its table, p is the top trade probability and conf is the confidence of the severity.

edit                                trade           p  severity  conf  may_enter
the original                        plumbing     0.99      2.99  0.99       0.98
permission sentence removed         plumbing     0.99      2.99  0.99       0.03
permission reversed                 plumbing     0.98      2.98  0.99       0.02
a slow drip, not through the light  plumbing     1.00      0.93  0.92       0.98
the light, not the water            electrical   1.00      1.47  0.53       0.98

Removing the permission moved may_enter from 0.98 to 0.03 and no other number in the table. Reversing it gave 0.02, and the other two answers moved by 0.01. The slow drip stayed with plumbing, and the ruler fell from 2.99 to 0.93. The flickering light changed the tray to electrical. Its severity of 1.47 lies between level 1 and level 2, and the confidence of 0.53 says so. Five messages are a demonstration, not a measurement.

Common questions

Why are the probabilities in a different order from my options?

We do not know why, and it does not matter. In our call the request listed plumbing, electrical, heating, appliance, pests, locks. The response listed plumbing, pests, appliance, heating, electrical, locks. Always read a probability by its name, never by its position.

Why do the numbers have only two decimals?

We take them to be rounded. Experiment 0040 counted the 3,010 numbers in the 215 recorded answers of experiment 0002: 2 have more than two decimals. Experiment 0014 recomputed 215 Choice confidences and 215 scores from those probabilities and found gaps of up to 0.006 and 0.02, about what such rounding would cause. That is also why 0.988 and 0.99 differed above.

Can I ask more than three questions in one call?

Yes. The documentation says the questions of one call are answered independently and in parallel, and that one answer is never context for another. Chapter 9 measures what extra questions cost.

Does OpenRouter return anything that TypeSafe does not?

OpenRouter's guide says it adds an id, a provider and the cost inside usage. Our program printed only model, answers and usage, which is why you see cost above but no id.

Carry this

  • Request: model, state, questions. A question: type, instructions, criteria.
  • The names of your questions are not sent to the model. Write the whole question in the instructions.
  • Response: choice or score with probabilities and confidence, or a single noul. Plus usage.
  • Our call: 625 input tokens, 311 ms, 625 x 0.042 / 1,000,000 = $0.00002625. The message added 41 of the 625 tokens. The smallest call we made read 272.
  • No explanation, no free text, no answer outside your list. Right shape, not guaranteed truth.

Check yourself

1. A longer tenant message makes the call read 800 input tokens. What does the call cost, and what do 10,000 such calls cost?

Answer
one call        800 x 0.042 / 1,000,000   =  $0.0000336
10,000 calls    0.0000336 x 10,000        =  $0.336

2. You rename the question may_enter to permission_to_enter_given, shorten its instructions to "Is it true?" and drop the criteria, hoping the name is enough. What goes wrong?

Answer

The name is not sent to the model, so the clerk would see only "Is it true?" and could not know what "it" is. The name is only the label under which your code finds the answer. Keep the full statement in instructions.

3. A Choice with six options returns a top probability of 0.90. Compute its confidence with the formula used above.

Answer
even split        1 / 6                      =  0.1667
confidence        (0.90 - 0.1667) / 0.8333   =  0.7333 / 0.8333  =  0.88

Chapter 3 explains why the formula subtracts the even split.