The LLM that sits between a visitor and Jev: what it is told, the tools it is given, and what it wrote back.
Jev cannot write text. It judges a question whose answer space somebody
has already written down. The LLM's whole job is to write that question:
one of three tool calls, one per Jev type. Nothing here does I/O; the
Worker sends [request] through the Workers AI binding and the eval sends
it over REST, and both hand the reply to [parse].
9#![forbid(unsafe_code)]
The most tool calls one input may become. More are dropped, not run.
19pub const MAX_CALLS: usize = 4;
The most options a Choice may list: enough for a real field, few enough to read as bars on a phone. Jev itself takes up to 255.
22pub const MAX_OPTIONS: usize = 8;
A Score's levels, as Jev takes them.
24pub const SCORE_LEVELS: std::ops::RangeInclusive<usize> = 2..=10;
The reply's token ceiling, and so the worst case a call can cost. Qwen3 reasons before it calls the tool: replies measured 2026-10-05 ran to 1320 completion tokens, so the earlier 1200 cut about half of the how-spicy calls off with no tool call. Only the tokens used are billed.
29pub const MAX_TOKENS: u32 = 2400;
31const SYSTEM: &str = "\ 32You sit between a person and Jev. Jev is a model that cannot write text. It only judges, in three ways: 33- jev_noul: a yes-or-no question, answered with the probability of yes. 34- jev_choice: one of several options that you list, answered with a probability for each. 35- jev_score: a position on a scale whose levels you write, lowest first. 36 37Turn the person's input into the tool call that answers it. 38- Always call a tool. Never answer the question yourself, and write no other text. 39- A yes-or-no question becomes jev_noul. So does \"how likely is X\": ask whether X, and the probability is the answer. 40- \"Which\", \"who\", \"what is the best\" and other questions with a best answer become jev_choice. \ 41You choose 2 to 8 real, specific options and give each a one-line description. 42- \"How good\", \"how much\", \"how many\", \"how spicy\", \"rate this\" become jev_score with 3 to 7 levels, lowest first. Its levels cover every possible answer, lowest first: for a count or an amount the first is none or zero when that is possible and the last is open-ended (\"more than 5\"). Name each level by a range or a word, so that \"0\", \"1 to 2\", \"3 to 5\", \"more than 5\" is right and \"1\", \"2\", \"3\" is not. 43- Use one call. Use more only when the input plainly asks several separate things, and never more than 4. 44- Write `instructions` as one clear question. Jev sees the person's input beside it, and nothing else.";
The three tools, in the OpenAI function format Workers AI takes.
47fn tools() -> Value { 48 let text = |description: &str| json!({ "type": "string", "description": description }); 49 let tool = |name: &str, description: &str, properties: Value, required: &[&str]| { 50 json!({ 51 "type": "function", 52 "function": { 53 "name": name, 54 "description": description, 55 "parameters": { "type": "object", "properties": properties, "required": required }, 56 }, 57 }) 58 }; 59 json!([ 60 tool( 61 "jev_noul", 62 "Ask Jev a yes-or-no question. Jev answers with the probability that the answer is yes.", 63 json!({ 64 "instructions": text("The yes-or-no question."), 65 "yes_means": text("What a yes means, in one sentence."), 66 "no_means": text("What a no means, in one sentence."), 67 }), 68 &["instructions", "yes_means", "no_means"], 69 ), 70 tool( 71 "jev_choice", 72 "Ask Jev to pick one of several options. Jev answers with a probability for every option.", 73 json!({ 74 "instructions": text("The question the options answer."), 75 "options": { 76 "type": "array", 77 "description": "2 to 8 options. Labels are short and all different.", 78 "items": { 79 "type": "object", 80 "properties": { 81 "label": text("The option's short name."), 82 "description": text("One line on what this option is."), 83 }, 84 "required": ["label", "description"], 85 }, 86 }, 87 }), 88 &["instructions", "options"], 89 ), 90 tool( 91 "jev_score", 92 "Ask Jev to place the input on a scale. Jev answers with a score and a probability for every level.", 93 json!({ 94 "instructions": text("What is being scored."), 95 "levels": { 96 "type": "array", 97 "description": "2 to 10 levels, lowest first, covering every possible answer (a count starts at none or zero when that is possible). Each is one line naming a range or a word, not a bare number.", 98 "items": { "type": "string" }, 99 }, 100 }), 101 &["instructions", "levels"], 102 ), 103 ]) 104}
The tools the LLM may call for wants: every one when the input is
several questions (what each one is, is the LLM's to work out), and
otherwise only the kinds the rules settled on. None is every tool.
109fn settled(wants: &[Want]) -> Option<Vec<&'static str>> { 110 if wants.is_empty() || wants.contains(&Want::Split) { 111 return None; 112 } 113 Some( 114 wants 115 .iter() 116 .map(|want| match want { 117 Want::Options => "jev_choice", 118 Want::Scale => "jev_score", 119 Want::Split => unreachable!("ruled out above"), 120 }) 121 .collect(), 122 ) 123}
The LLM's instructions when the rules have settled what the input is.
It is told about, and given, only the tools for that: Jev has already
answered the other readings itself, and a second answer to one of them
from here would only disagree with the first (2026-10-02: "what are the
chances that…" got a yes-or-no from Jev and another, with a different
probability, from a jev_noul the LLM wrote when a scale was wanted).
131fn settled_system(tools: &[&str]) -> String { 132 let has = |tool: &str| tools.contains(&tool); 133 let mut text = String::from( 134 "You sit between a person and Jev. Jev is a model that cannot write text. It only judges. \ 135 For this input, like this:\n", 136 ); 137 if has("jev_choice") { 138 text.push_str("- jev_choice: one of several options that you list, answered with a probability for each.\n"); 139 } 140 if has("jev_score") { 141 text.push_str("- jev_score: a position on a scale whose levels you write, lowest first.\n"); 142 } 143 text.push_str(match (has("jev_choice"), has("jev_score")) { 144 (true, true) => { 145 "\nThe person's input has already been read two ways: as a pick among possibilities, and as a \ 146 how-much question. Write exactly two tool calls, one jev_choice and one jev_score.\n" 147 } 148 (true, false) => { 149 "\nThe person's input has already been read as a pick among possibilities. Write exactly one \ 150 jev_choice call.\n" 151 } 152 _ => "\nThe person's input has already been read as a how-much question. Write exactly one jev_score call.\n", 153 }); 154 text.push_str("- Always call a tool. Never answer the question yourself, and write no other text.\n"); 155 if has("jev_choice") { 156 text.push_str("- For jev_choice, choose 2 to 8 real, specific options and give each a one-line description.\n"); 157 } 158 if has("jev_score") { 159 text.push_str( 160 "- For jev_score, write 3 to 7 levels, lowest first, that fit what is asked: its own units, ranges \ 161 or named grades where it has them, and levels of likelihood where it asks how likely. Its levels cover every possible answer, lowest first: for a count or an amount the first is none or zero when that is possible and the last is open-ended (\"more than 5\"). Name each level by a range or a word, so that \"0\", \"1 to 2\", \"3 to 5\", \"more than 5\" is right and \"1\", \"2\", \"3\" is not.\n", 162 ); 163 } 164 text.push_str("- Write `instructions` as one clear question. Jev sees the person's input beside it, and nothing else."); 165 text 166}
Whether a question the LLM wrote is of a kind it was asked for. One that is not, is not sent: Jev has answered that reading already, or the rules did not take it.
The request body for input, as JSON text. The same bytes go to the
binding and to the page's tool call panel. wants is what the rules say
the LLM is to write ([rules::Network::wants]): sorted, no repeats.
178pub fn request(input: &str, wants: &[Want]) -> String { 179 let (system, tools) = match settled(wants) { 180 Some(names) => { 181 let given: Vec<Value> = tools() 182 .as_array() 183 .into_iter() 184 .flatten() 185 .filter(|tool| names.iter().any(|name| tool["function"]["name"] == *name)) 186 .cloned() 187 .collect(); 188 (settled_system(&names), Value::Array(given)) 189 } 190 None => (SYSTEM.to_owned(), tools()), 191 }; 192 json!({ 193 "messages": [ 194 { "role": "system", "content": system }, 195 { "role": "user", "content": input }, 196 ], 197 "tools": tools, 198 "max_tokens": MAX_TOKENS, 199 "temperature": 0, 200 }) 201 .to_string() 202}
A question for Jev as the LLM wrote it, checked for shape.
The tool that was called, which is also the Jev type.
One tool call from the reply.
The arguments exactly as the model wrote them.
243 pub arguments: String,
The question they describe, or why they do not describe one.
What a reply cost, as the API reported it.
At most [MAX_CALLS], in the order the model made them.
260 pub calls: Vec<ToolCall>,
How many calls the model made past the cap. They were dropped.
Reads a chat-completion reply: the binding's own, or REST's, which wraps
it in result. An Err is a reply with no usable shape at all; a reply
whose calls are malformed is Ok, with the reason on each call.
269pub fn parse(body: &str) -> Result<Reply, String> { 270 let root: Value = serde_json::from_str(body).map_err(|e| format!("the reply is not JSON: {e}"))?; 271 let root = root.get("result").unwrap_or(&root); 272 let message = root 273 .pointer("/choices/0/message") 274 .ok_or_else(|| "the reply has no choices[0].message".to_owned())?; 275 let made: &[Value] = message.get("tool_calls").and_then(Value::as_array).map_or(&[], Vec::as_slice); 276 let calls = made.iter().take(MAX_CALLS).map(tool_call).collect(); 277 let usage = root.get("usage"); 278 let count = |name: &str| usage.and_then(|u| u.get(name)).and_then(Value::as_u64).unwrap_or(0); 279 Ok(Reply { 280 calls, 281 dropped: made.len().saturating_sub(MAX_CALLS), 282 usage: Usage { 283 prompt_tokens: count("prompt_tokens"), 284 completion_tokens: count("completion_tokens"), 285 neurons: usage.and_then(|u| u.get("neurons")).and_then(Value::as_f64), 286 }, 287 }) 288}
290fn tool_call(call: &Value) -> ToolCall { 291 let name = call.pointer("/function/name").and_then(Value::as_str).unwrap_or_default().to_owned(); 292 let arguments = match call.pointer("/function/arguments") { 293 Some(Value::String(text)) => text.clone(), 294 Some(other) => other.to_string(), 295 None => String::new(), 296 }; 297 let draft = draft(&name, &arguments); 298 ToolCall { name, arguments, draft } 299}
The arguments as an object. The format says arguments is JSON text of an
object. One model (granite-4.0-h-micro, measured 2026-10-02) encodes that
text a second time, so a string that decodes to a string is decoded once
more. Nothing else is repaired.
305fn object(arguments: &str) -> Result<Value, String> { 306 let mut value: Value = 307 serde_json::from_str(arguments).map_err(|e| format!("the arguments are not JSON: {e}"))?; 308 if let Value::String(inner) = &value { 309 value = serde_json::from_str(inner).map_err(|e| format!("the arguments are not JSON: {e}"))?; 310 } 311 if value.is_object() { Ok(value) } else { Err("the arguments are not an object".to_owned()) } 312}
314fn draft(name: &str, arguments: &str) -> Result<Draft, String> { 315 #[derive(Deserialize)] 316 #[serde(deny_unknown_fields)] 317 struct NoulArgs { 318 instructions: String, 319 yes_means: String, 320 no_means: String, 321 } 322 #[derive(Deserialize)] 323 #[serde(deny_unknown_fields)] 324 struct ChoiceArgs { 325 instructions: String, 326 options: Vec<Opt>, 327 } 328 #[derive(Deserialize)] 329 #[serde(deny_unknown_fields)] 330 struct ScoreArgs { 331 instructions: String, 332 levels: Vec<String>, 333 } 334 fn read<T: for<'de> Deserialize<'de>>(value: Value) -> Result<T, String> { 335 serde_json::from_value(value).map_err(|e| e.to_string()) 336 } 337 let filled = |what: &str, text: &str| { 338 if text.trim().is_empty() { Err(format!("{what} is empty")) } else { Ok(()) } 339 }; 340 341 let value = object(arguments)?; 342 match name { 343 "jev_noul" => { 344 let NoulArgs { instructions, yes_means, no_means } = read(value)?; 345 filled("instructions", &instructions)?; 346 filled("yes_means", &yes_means)?; 347 filled("no_means", &no_means)?; 348 Ok(Draft::Noul { instructions, yes_means, no_means }) 349 } 350 "jev_choice" => { 351 let ChoiceArgs { instructions, options } = read(value)?; 352 filled("instructions", &instructions)?; 353 if !(2..=MAX_OPTIONS).contains(&options.len()) { 354 return Err(format!("a choice takes 2 to {MAX_OPTIONS} options, not {}", options.len())); 355 } 356 for (i, option) in options.iter().enumerate() { 357 filled("an option's label", &option.label)?; 358 if options[..i].iter().any(|earlier| earlier.label == option.label) { 359 return Err(format!("the option {:?} appears twice", option.label)); 360 } 361 } 362 Ok(Draft::Choice { instructions, options }) 363 } 364 "jev_score" => { 365 let ScoreArgs { instructions, levels } = read(value)?; 366 filled("instructions", &instructions)?; 367 if !SCORE_LEVELS.contains(&levels.len()) { 368 return Err(format!( 369 "a score takes {} to {} levels, not {}", 370 SCORE_LEVELS.start(), 371 SCORE_LEVELS.end(), 372 levels.len() 373 )); 374 } 375 for level in &levels { 376 filled("a level", level)?; 377 } 378 Ok(Draft::Score { instructions, levels }) 379 } 380 other => Err(format!("there is no tool called {other:?}")), 381 } 382} 383 384#[cfg(test)] 385mod tests;