Add M2a plan: thirteen tasks, tests, fake server and recordings
The tasks build the inference path: emsha-backed SHA-256, inferproxy, config, a hand-written HTTP and SSE client, request building, delta assembly, the chat state machine, the thinking cap, the slot gate with retry, the startup self-test and on-device verification. Everything the tasks copy in was checked against a private reference implementation: the gate passes after each task in order, the timing tests pass repeatedly under CPU load, and the reference passes the self-test and all four device checks on straylight. Expected results for the recorded streams were derived by a separate script. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
//! Tests for the request body. Do not edit.
|
||||
//!
|
||||
//! Bodies are compared as JSON values, so key order is free. What is not free: which keys
|
||||
//! appear, and that the same input always gives the same bytes.
|
||||
|
||||
mod support;
|
||||
|
||||
use loopd::llama::request::build_body;
|
||||
use loopd::llama::{ChatMessage, ChatRequest, ToolSchema};
|
||||
use serde_json::{Value, json};
|
||||
use std::path::Path;
|
||||
|
||||
fn body(req: &ChatRequest) -> Value {
|
||||
let cfg = support::test_config(Path::new("/tmp/unused.sock"));
|
||||
serde_json::from_str(&build_body(&cfg, req).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn user(text: &str) -> ChatMessage {
|
||||
ChatMessage::User {
|
||||
content: text.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_file_tool() -> ToolSchema {
|
||||
ToolSchema {
|
||||
name: "read_file".to_string(),
|
||||
description: "Read a text file.".to_string(),
|
||||
parameters: json!({"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_plain_request_carries_every_setting() {
|
||||
let req = ChatRequest {
|
||||
slot: 1,
|
||||
messages: vec![user("hi")],
|
||||
tools: vec![],
|
||||
thinking: true,
|
||||
};
|
||||
let want = json!({
|
||||
"model": "test-model",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"id_slot": 1,
|
||||
"cache_prompt": true,
|
||||
"stream": true,
|
||||
"return_progress": true,
|
||||
"timings_per_token": true,
|
||||
"reasoning_control": true,
|
||||
"max_tokens": 8192,
|
||||
"temperature": 0.6,
|
||||
"top_p": 0.95,
|
||||
"top_k": 20,
|
||||
"chat_template_kwargs": {"enable_thinking": true},
|
||||
});
|
||||
assert_eq!(body(&req), want, "no `tools` key when there are no tools");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thinking_off_and_tools() {
|
||||
let req = ChatRequest {
|
||||
slot: 0,
|
||||
messages: vec![user("hi")],
|
||||
tools: vec![read_file_tool()],
|
||||
thinking: false,
|
||||
};
|
||||
let got = body(&req);
|
||||
assert_eq!(
|
||||
got["chat_template_kwargs"],
|
||||
json!({"enable_thinking": false})
|
||||
);
|
||||
let want_tools = json!([{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Read a text file.",
|
||||
"parameters": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]},
|
||||
},
|
||||
}]);
|
||||
assert_eq!(got["tools"], want_tools);
|
||||
}
|
||||
|
||||
/// One message of every kind, and both shapes of assistant message. An example of each is
|
||||
/// here on purpose: they are rendered differently.
|
||||
#[test]
|
||||
fn every_kind_of_message() {
|
||||
let call = proto::ToolCall {
|
||||
id: "call_1".to_string(),
|
||||
name: "read_file".to_string(),
|
||||
arguments: r#"{"path":"/etc/hostname"}"#.to_string(),
|
||||
};
|
||||
let messages = vec![
|
||||
ChatMessage::System {
|
||||
content: "be careful".to_string(),
|
||||
},
|
||||
user("read it"),
|
||||
ChatMessage::Assistant {
|
||||
content: None,
|
||||
reasoning_content: Some("I should read the file.".to_string()),
|
||||
tool_calls: vec![call],
|
||||
},
|
||||
ChatMessage::Tool {
|
||||
tool_call_id: "call_1".to_string(),
|
||||
content: "straylight\n".to_string(),
|
||||
},
|
||||
ChatMessage::Assistant {
|
||||
content: Some("It says straylight.".to_string()),
|
||||
reasoning_content: None,
|
||||
tool_calls: vec![],
|
||||
},
|
||||
];
|
||||
let req = ChatRequest {
|
||||
slot: 0,
|
||||
messages,
|
||||
tools: vec![read_file_tool()],
|
||||
thinking: true,
|
||||
};
|
||||
let want = json!([
|
||||
{"role": "system", "content": "be careful"},
|
||||
{"role": "user", "content": "read it"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"reasoning_content": "I should read the file.",
|
||||
"tool_calls": [{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": "read_file", "arguments": "{\"path\":\"/etc/hostname\"}"},
|
||||
}],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call_1", "content": "straylight\n"},
|
||||
{"role": "assistant", "content": "It says straylight."},
|
||||
]);
|
||||
assert_eq!(body(&req)["messages"], want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn values_come_from_the_config() {
|
||||
let mut cfg = support::test_config(Path::new("/tmp/unused.sock"));
|
||||
cfg.infer.model = "another-model".to_string();
|
||||
cfg.limits.max_tokens = 99;
|
||||
cfg.sampling.temperature = 0.25;
|
||||
cfg.sampling.top_p = 0.5;
|
||||
cfg.sampling.top_k = 7;
|
||||
let req = ChatRequest {
|
||||
slot: 3,
|
||||
messages: vec![user("hi")],
|
||||
tools: vec![],
|
||||
thinking: false,
|
||||
};
|
||||
let got: Value = serde_json::from_str(&build_body(&cfg, &req).unwrap()).unwrap();
|
||||
assert_eq!(got["model"], "another-model");
|
||||
assert_eq!(got["max_tokens"], 99);
|
||||
assert_eq!(got["temperature"], 0.25);
|
||||
assert_eq!(got["top_p"], 0.5);
|
||||
assert_eq!(got["top_k"], 7);
|
||||
assert_eq!(got["id_slot"], 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_same_input_gives_the_same_bytes() {
|
||||
let cfg = support::test_config(Path::new("/tmp/unused.sock"));
|
||||
let req = ChatRequest {
|
||||
slot: 0,
|
||||
messages: vec![user(
|
||||
"text with \"quotes\", a \\ backslash, a\nnewline and caf\u{e9} \u{1f4e6}",
|
||||
)],
|
||||
tools: vec![read_file_tool()],
|
||||
thinking: true,
|
||||
};
|
||||
let first = build_body(&cfg, &req).unwrap();
|
||||
for _ in 0..20 {
|
||||
assert_eq!(build_body(&cfg, &req).unwrap(), first);
|
||||
}
|
||||
let back: Value = serde_json::from_str(&first).unwrap();
|
||||
assert_eq!(
|
||||
back["messages"][0]["content"],
|
||||
"text with \"quotes\", a \\ backslash, a\nnewline and caf\u{e9} \u{1f4e6}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user