Fri, 24 Jul 2026 16:13:44 +0800

This commit is contained in:
lyc
2026-07-24 16:13:44 +08:00
commit 7381b59874
29 changed files with 3832 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
use serde_json::Value;
/// Canonical Byte Representation of the `messages` field.
///
/// This produces a deterministic byte sequence from the messages JSON,
/// independent of the original key ordering or whitespace in the request.
/// Semantically identical messages arrays always produce identical bytes.
pub fn canonical_messages_bytes(messages: &Value) -> Vec<u8> {
let canonical = canonicalize_value(messages);
// Compact (no extraneous whitespace), deterministic key order
serde_json::to_vec(&canonical).expect("canonical JSON serialization is infallible")
}
/// Canonical cache identity for every request field that can affect prompt
/// rendering, followed by `messages` so conversation growth extends the tail.
pub fn canonical_prompt_bytes(body: &Value) -> Vec<u8> {
let Some(object) = body.as_object() else {
return canonical_messages_bytes(&Value::Null);
};
let messages = object.get("messages").unwrap_or(&Value::Null);
let mut prompt_options = serde_json::Map::new();
for (key, value) in object {
if key != "messages" && !is_generation_only_field(key) {
prompt_options.insert(key.clone(), canonicalize_value(value));
}
}
let mut bytes = serde_json::to_vec(&Value::Object(prompt_options))
.expect("canonical JSON serialization is infallible");
bytes.push(0);
bytes.extend(canonical_messages_bytes(messages));
bytes
}
/// Fields known not to alter tokenized prompt construction. Unknown extension
/// fields are deliberately included so backend-specific template kwargs and
/// future prompt controls cannot silently alias an unrelated cache namespace.
fn is_generation_only_field(key: &str) -> bool {
matches!(
key,
"frequency_penalty"
| "logit_bias"
| "logprobs"
| "max_completion_tokens"
| "max_tokens"
| "metadata"
| "n"
| "parallel_tool_calls"
| "presence_penalty"
| "seed"
| "service_tier"
| "stop"
| "store"
| "stream"
| "stream_options"
| "temperature"
| "top_logprobs"
| "top_p"
| "user"
)
}
/// Recursively sort object keys for deterministic serialization.
fn canonicalize_value(value: &Value) -> Value {
match value {
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
let mut out = serde_json::Map::with_capacity(map.len());
for k in keys {
out.insert(k.clone(), canonicalize_value(&map[k]));
}
Value::Object(out)
}
Value::Array(arr) => Value::Array(arr.iter().map(canonicalize_value).collect()),
other => other.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_canonical_stable_key_order() {
let a: Value =
serde_json::from_str(r#"{"messages":[{"role":"user","content":"hi"}]}"#).unwrap();
let b: Value =
serde_json::from_str(r#"{"messages":[{"content":"hi","role":"user"}]}"#).unwrap();
let messages_a = &a["messages"];
let messages_b = &b["messages"];
assert_eq!(
canonical_messages_bytes(messages_a),
canonical_messages_bytes(messages_b)
);
}
#[test]
fn test_canonical_whitespace_agnostic() {
let a: Value =
serde_json::from_str(r#"{"messages":[{"role":"user","content":"hello world"}]}"#)
.unwrap();
let b: Value = serde_json::from_str(
r#"{
"messages": [
{
"role": "user",
"content": "hello world"
}
]
}"#,
)
.unwrap();
assert_eq!(
canonical_messages_bytes(&a["messages"]),
canonical_messages_bytes(&b["messages"])
);
}
#[test]
fn test_different_content_different_bytes() {
let a: Value =
serde_json::from_str(r#"{"messages":[{"role":"user","content":"hi"}]}"#).unwrap();
let b: Value =
serde_json::from_str(r#"{"messages":[{"role":"user","content":"hello"}]}"#).unwrap();
assert_ne!(
canonical_messages_bytes(&a["messages"]),
canonical_messages_bytes(&b["messages"])
);
}
#[test]
fn test_prompt_identity_includes_template_inputs() {
let a = serde_json::json!({
"model": "model-a",
"messages": [{"role": "user", "content": "hi"}],
"chat_template_kwargs": {"enable_thinking": true}
});
let b = serde_json::json!({
"model": "model-a",
"messages": [{"role": "user", "content": "hi"}],
"chat_template_kwargs": {"enable_thinking": false}
});
assert_ne!(canonical_prompt_bytes(&a), canonical_prompt_bytes(&b));
}
#[test]
fn test_prompt_identity_ignores_generation_controls() {
let a = serde_json::json!({
"model": "model-a",
"messages": [{"role": "user", "content": "hi"}],
"temperature": 0.1
});
let b = serde_json::json!({
"model": "model-a",
"messages": [{"role": "user", "content": "hi"}],
"temperature": 0.9
});
assert_eq!(canonical_prompt_bytes(&a), canonical_prompt_bytes(&b));
}
}