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.
On this page
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."
}
}
}
}
| Field | What it is | In our call |
|---|---|---|
model | which clerk should answer | typesafe/jev-1.13, the name Jev has on OpenRouter |
state | the paper on the desk: the content to judge | one tenant message, as a string |
questions | the slips: a map from a name you choose to a question | three 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.
type | Stamp | criteria is | In our request |
|---|---|---|---|
choice | tray stamp | an object: each option name with a description | six trades |
score | ruler | an ordered list of level descriptions; the first is level 0 | four levels, 0 to 3 |
noul | yes/no dial | optional: what true means and what false means | both 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.
modelis 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.tradeis the Choice.choiceis the winning option.probabilitiesgives every option a number, and the numbers sum to 1.confidenceis one number from 0 to 1 computed from them.severityis the Score.probabilitiesgives each level a number.scoreis the position on the scale.legendrepeats our level descriptions, so that the number can be read without the request.may_enteris 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.usageis the bill.costis 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:
choiceorscorewithprobabilitiesandconfidence, or a singlenoul. Plususage. - 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.