lmjtfy.git / packages / llm / src / lib.rs
lib.rsannotatedlib.rssource382 lines · 15.4 KB · raw

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)]
11use rules::Want;
12use serde::{Deserialize, Serialize};
13use serde_json::{Value, json};
14
15mod models;
16pub use models::{CANDIDATES, FREE_NEURONS_PER_DAY, Model};

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.

168pub fn takes(wants: &[Want], draft: &Draft) -> bool {
169    settled(wants).is_none_or(|tools| tools.contains(&draft.tool()))
170}

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.

202#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
203pub enum Draft {
204    Noul { instructions: String, yes_means: String, no_means: String },
205    Choice { instructions: String, options: Vec<Opt> },
206    Score { instructions: String, levels: Vec<String> },
207}
209#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
210#[serde(deny_unknown_fields)]
211pub struct Opt {
212    pub label: String,
213    pub description: String,
214}
215
216impl Draft {

The tool that was called, which is also the Jev type.

218    pub fn tool(&self) -> &'static str {
219        match self {
220            Draft::Noul { .. } => "jev_noul",
221            Draft::Choice { .. } => "jev_choice",
222            Draft::Score { .. } => "jev_score",
223        }
224    }
226    pub fn instructions(&self) -> &str {
227        match self {
228            Draft::Noul { instructions, .. }
229            | Draft::Choice { instructions, .. }
230            | Draft::Score { instructions, .. } => instructions,
231        }
232    }
233}

One tool call from the reply.

236#[derive(Clone, Debug, PartialEq)]
237pub struct ToolCall {
238    pub name: String,

The arguments exactly as the model wrote them.

240    pub arguments: String,

The question they describe, or why they do not describe one.

242    pub draft: Result<Draft, String>,
243}

What a reply cost, as the API reported it.

246#[derive(Clone, Copy, Debug, Default, PartialEq)]
247pub struct Usage {
248    pub prompt_tokens: u64,
249    pub completion_tokens: u64,

Cloudflare's own count, when the reply carries one.

251    pub neurons: Option<f64>,
252}
254#[derive(Clone, Debug, PartialEq)]
255pub struct Reply {

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.

259    pub dropped: usize,
260    pub usage: Usage,
261}

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;