use runtime_ports::{ModelMessage, ModelToolDefinition};
use serde::Serialize;
use sha2::{Digest, Sha256};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ContextMeasurement {
pub(crate) message_tokens: u64,
pub(crate) tool_schema_tokens: u64,
pub(crate) protocol_overhead_tokens: u64,
pub(crate) reserved_output_tokens: u64,
pub(crate) total_tokens: u64,
}
pub(crate) fn measure_context(
messages: &[ModelMessage],
tools: &[ModelToolDefinition],
protocol_overhead_tokens: u64,
reserved_output_tokens: u32,
) -> ContextMeasurement {
let message_tokens = estimated_serialized_tokens(messages);
let tool_schema_tokens = estimated_serialized_tokens(tools);
let reserved_output_tokens = u64::from(reserved_output_tokens);
ContextMeasurement {
message_tokens,
tool_schema_tokens,
protocol_overhead_tokens,
reserved_output_tokens,
total_tokens: message_tokens
.saturating_add(tool_schema_tokens)
.saturating_add(protocol_overhead_tokens)
.saturating_add(reserved_output_tokens),
}
}
pub(crate) fn model_request_digest(
messages: &[ModelMessage],
tools: &[ModelToolDefinition],
max_output_tokens: Option<u32>,
) -> String {
let mut digest = Sha256::new();
digest.update(serde_json::to_vec(messages).unwrap_or_default());
digest.update([0]);
digest.update(serde_json::to_vec(tools).unwrap_or_default());
digest.update([0]);
digest.update(max_output_tokens.unwrap_or_default().to_be_bytes());
format!("sha256:{:x}", digest.finalize())
}
fn estimated_serialized_tokens<T: Serialize + ?Sized>(value: &T) -> u64 {
let bytes = serde_json::to_vec(value)
.map(|encoded| encoded.len() as u64)
.unwrap_or_default();
bytes.saturating_add(3) / 4
}
#[cfg(test)]
mod tests {
use runtime_ports::{ModelMessage, ModelRole, ModelToolDefinition};
use serde_json::json;
use super::*;
#[test]
fn complete_measurement_counts_tools_protocol_and_reserved_output() {
let messages = vec![ModelMessage::text(ModelRole::User, "task")];
let tools = vec![ModelToolDefinition {
name: "run".into(),
description: "execute a command".into(),
input_schema: json!({"type": "object"}),
}];
let without_tools = measure_context(&messages, &[], 0, 0);
let complete = measure_context(&messages, &tools, 17, 31);
assert!(complete.tool_schema_tokens > 0);
assert_eq!(complete.protocol_overhead_tokens, 17);
assert_eq!(complete.reserved_output_tokens, 31);
assert!(complete.total_tokens > without_tools.total_tokens + 47);
}
#[test]
fn request_digest_changes_with_tool_projection() {
let messages = vec![ModelMessage::text(ModelRole::User, "task")];
let one = ModelToolDefinition {
name: "one".into(),
description: String::new(),
input_schema: json!({}),
};
let two = ModelToolDefinition {
name: "two".into(),
..one.clone()
};
assert_ne!(
model_request_digest(&messages, &[one], Some(8)),
model_request_digest(&messages, &[two], Some(8))
);
}
}