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.
26pub const MAX_TOKENS: u32 = 1200;
28const SYSTEM: &str = "\ 29You sit between a person and Jev. Jev is a model that cannot write text. It only judges, in three ways: 30- jev_noul: a yes-or-no question, answered with the probability of yes. 31- jev_choice: one of several options that you list, answered with a probability for each. 32- jev_score: a position on a scale whose levels you write, lowest first. 33 34Turn the person's input into the tool call that answers it. 35- Always call a tool. Never answer the question yourself, and write no other text. 36- A yes-or-no question becomes jev_noul. So does \"how likely is X\": ask whether X, and the probability is the answer. 37- \"Which\", \"who\", \"what is the best\" and other questions with a best answer become jev_choice. \ 38You choose 2 to 8 real, specific options and give each a one-line description. 39- \"How good\", \"how much\", \"how spicy\", \"rate this\" become jev_score with 3 to 7 levels, lowest first. 40- Use one call. Use more only when the input plainly asks several separate things, and never more than 4. 41- 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.
44fn tools() -> Value { 45 let text = |description: &str| json!({ "type": "string", "description": description }); 46 let tool = |name: &str, description: &str, properties: Value, required: &[&str]| { 47 json!({ 48 "type": "function", 49 "function": { 50 "name": name, 51 "description": description, 52 "parameters": { "type": "object", "properties": properties, "required": required }, 53 }, 54 }) 55 }; 56 json!([ 57 tool( 58 "jev_noul", 59 "Ask Jev a yes-or-no question. Jev answers with the probability that the answer is yes.", 60 json!({ 61 "instructions": text("The yes-or-no question."), 62 "yes_means": text("What a yes means, in one sentence."), 63 "no_means": text("What a no means, in one sentence."), 64 }), 65 &["instructions", "yes_means", "no_means"], 66 ), 67 tool( 68 "jev_choice", 69 "Ask Jev to pick one of several options. Jev answers with a probability for every option.", 70 json!({ 71 "instructions": text("The question the options answer."), 72 "options": { 73 "type": "array", 74 "description": "2 to 8 options. Labels are short and all different.", 75 "items": { 76 "type": "object", 77 "properties": { 78 "label": text("The option's short name."), 79 "description": text("One line on what this option is."), 80 }, 81 "required": ["label", "description"], 82 }, 83 }, 84 }), 85 &["instructions", "options"], 86 ), 87 tool( 88 "jev_score", 89 "Ask Jev to place the input on a scale. Jev answers with a score and a probability for every level.", 90 json!({ 91 "instructions": text("What is being scored."), 92 "levels": { 93 "type": "array", 94 "description": "2 to 10 levels, lowest first. Each is one line saying what that level means.", 95 "items": { "type": "string" }, 96 }, 97 }), 98 &["instructions", "levels"], 99 ), 100 ]) 101}
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.
106fn settled(wants: &[Want]) -> Option<Vec<&'static str>> { 107 if wants.is_empty() || wants.contains(&Want::Split) { 108 return None; 109 } 110 Some( 111 wants 112 .iter() 113 .map(|want| match want { 114 Want::Options => "jev_choice", 115 Want::Scale => "jev_score", 116 Want::Split => unreachable!("ruled out above"), 117 }) 118 .collect(), 119 ) 120}
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).
128fn settled_system(tools: &[&str]) -> String { 129 let has = |tool: &str| tools.contains(&tool); 130 let mut text = String::from( 131 "You sit between a person and Jev. Jev is a model that cannot write text. It only judges. \ 132 For this input, like this:\n", 133 ); 134 if has("jev_choice") { 135 text.push_str("- jev_choice: one of several options that you list, answered with a probability for each.\n"); 136 } 137 if has("jev_score") { 138 text.push_str("- jev_score: a position on a scale whose levels you write, lowest first.\n"); 139 } 140 text.push_str(match (has("jev_choice"), has("jev_score")) { 141 (true, true) => { 142 "\nThe person's input has already been read two ways: as a pick among possibilities, and as a \ 143 how-much question. Write exactly two tool calls, one jev_choice and one jev_score.\n" 144 } 145 (true, false) => { 146 "\nThe person's input has already been read as a pick among possibilities. Write exactly one \ 147 jev_choice call.\n" 148 } 149 _ => "\nThe person's input has already been read as a how-much question. Write exactly one jev_score call.\n", 150 }); 151 text.push_str("- Always call a tool. Never answer the question yourself, and write no other text.\n"); 152 if has("jev_choice") { 153 text.push_str("- For jev_choice, choose 2 to 8 real, specific options and give each a one-line description.\n"); 154 } 155 if has("jev_score") { 156 text.push_str( 157 "- For jev_score, write 3 to 7 levels, lowest first, that fit what is asked: its own units, ranges \ 158 or named grades where it has them, and levels of likelihood where it asks how likely.\n", 159 ); 160 } 161 text.push_str("- Write `instructions` as one clear question. Jev sees the person's input beside it, and nothing else."); 162 text 163}
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.
175pub fn request(input: &str, wants: &[Want]) -> String { 176 let (system, tools) = match settled(wants) { 177 Some(names) => { 178 let given: Vec<Value> = tools() 179 .as_array() 180 .into_iter() 181 .flatten() 182 .filter(|tool| names.iter().any(|name| tool["function"]["name"] == *name)) 183 .cloned() 184 .collect(); 185 (settled_system(&names), Value::Array(given)) 186 } 187 None => (SYSTEM.to_owned(), tools()), 188 }; 189 json!({ 190 "messages": [ 191 { "role": "system", "content": system }, 192 { "role": "user", "content": input }, 193 ], 194 "tools": tools, 195 "max_tokens": MAX_TOKENS, 196 "temperature": 0, 197 }) 198 .to_string() 199}
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.
240 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.
257 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.
266pub fn parse(body: &str) -> Result<Reply, String> { 267 let root: Value = serde_json::from_str(body).map_err(|e| format!("the reply is not JSON: {e}"))?; 268 let root = root.get("result").unwrap_or(&root); 269 let message = root 270 .pointer("/choices/0/message") 271 .ok_or_else(|| "the reply has no choices[0].message".to_owned())?; 272 let made: &[Value] = message.get("tool_calls").and_then(Value::as_array).map_or(&[], Vec::as_slice); 273 let calls = made.iter().take(MAX_CALLS).map(tool_call).collect(); 274 let usage = root.get("usage"); 275 let count = |name: &str| usage.and_then(|u| u.get(name)).and_then(Value::as_u64).unwrap_or(0); 276 Ok(Reply { 277 calls, 278 dropped: made.len().saturating_sub(MAX_CALLS), 279 usage: Usage { 280 prompt_tokens: count("prompt_tokens"), 281 completion_tokens: count("completion_tokens"), 282 neurons: usage.and_then(|u| u.get("neurons")).and_then(Value::as_f64), 283 }, 284 }) 285}
287fn tool_call(call: &Value) -> ToolCall { 288 let name = call.pointer("/function/name").and_then(Value::as_str).unwrap_or_default().to_owned(); 289 let arguments = match call.pointer("/function/arguments") { 290 Some(Value::String(text)) => text.clone(), 291 Some(other) => other.to_string(), 292 None => String::new(), 293 }; 294 let draft = draft(&name, &arguments); 295 ToolCall { name, arguments, draft } 296}
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.
302fn object(arguments: &str) -> Result<Value, String> { 303 let mut value: Value = 304 serde_json::from_str(arguments).map_err(|e| format!("the arguments are not JSON: {e}"))?; 305 if let Value::String(inner) = &value { 306 value = serde_json::from_str(inner).map_err(|e| format!("the arguments are not JSON: {e}"))?; 307 } 308 if value.is_object() { Ok(value) } else { Err("the arguments are not an object".to_owned()) } 309}
311fn draft(name: &str, arguments: &str) -> Result<Draft, String> { 312 #[derive(Deserialize)] 313 #[serde(deny_unknown_fields)] 314 struct NoulArgs { 315 instructions: String, 316 yes_means: String, 317 no_means: String, 318 } 319 #[derive(Deserialize)] 320 #[serde(deny_unknown_fields)] 321 struct ChoiceArgs { 322 instructions: String, 323 options: Vec<Opt>, 324 } 325 #[derive(Deserialize)] 326 #[serde(deny_unknown_fields)] 327 struct ScoreArgs { 328 instructions: String, 329 levels: Vec<String>, 330 } 331 fn read<T: for<'de> Deserialize<'de>>(value: Value) -> Result<T, String> { 332 serde_json::from_value(value).map_err(|e| e.to_string()) 333 } 334 let filled = |what: &str, text: &str| { 335 if text.trim().is_empty() { Err(format!("{what} is empty")) } else { Ok(()) } 336 }; 337 338 let value = object(arguments)?; 339 match name { 340 "jev_noul" => { 341 let NoulArgs { instructions, yes_means, no_means } = read(value)?; 342 filled("instructions", &instructions)?; 343 filled("yes_means", &yes_means)?; 344 filled("no_means", &no_means)?; 345 Ok(Draft::Noul { instructions, yes_means, no_means }) 346 } 347 "jev_choice" => { 348 let ChoiceArgs { instructions, options } = read(value)?; 349 filled("instructions", &instructions)?; 350 if !(2..=MAX_OPTIONS).contains(&options.len()) { 351 return Err(format!("a choice takes 2 to {MAX_OPTIONS} options, not {}", options.len())); 352 } 353 for (i, option) in options.iter().enumerate() { 354 filled("an option's label", &option.label)?; 355 if options[..i].iter().any(|earlier| earlier.label == option.label) { 356 return Err(format!("the option {:?} appears twice", option.label)); 357 } 358 } 359 Ok(Draft::Choice { instructions, options }) 360 } 361 "jev_score" => { 362 let ScoreArgs { instructions, levels } = read(value)?; 363 filled("instructions", &instructions)?; 364 if !SCORE_LEVELS.contains(&levels.len()) { 365 return Err(format!( 366 "a score takes {} to {} levels, not {}", 367 SCORE_LEVELS.start(), 368 SCORE_LEVELS.end(), 369 levels.len() 370 )); 371 } 372 for level in &levels { 373 filled("a level", level)?; 374 } 375 Ok(Draft::Score { instructions, levels }) 376 } 377 other => Err(format!("there is no tool called {other:?}")), 378 } 379} 380 381#[cfg(test)] 382mod tests;