use serde_json::Value;
use crate::llmtrim::ir::ProviderKind;
use crate::llmtrim::tokenizer::TokenCounter;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Zone {
Input,
Output,
}
impl Zone {
pub fn as_str(self) -> &'static str {
match self {
Zone::Input => "input",
Zone::Output => "output",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Section {
Static,
Messages,
}
impl Section {
pub fn as_str(self) -> &'static str {
match self {
Section::Static => "static",
Section::Messages => "messages",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Bucket {
System,
Schema,
Text,
Thinking,
ToolUse,
ToolResult,
}
impl Bucket {
pub fn as_str(self) -> &'static str {
match self {
Bucket::System => "system",
Bucket::Schema => "schema",
Bucket::Text => "text",
Bucket::Thinking => "thinking",
Bucket::ToolUse => "tool_use",
Bucket::ToolResult => "tool_result",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Role {
User,
Assistant,
}
impl Role {
pub fn as_str(self) -> &'static str {
match self {
Role::User => "user",
Role::Assistant => "assistant",
}
}
fn parse(s: &str) -> Option<Role> {
match s {
"user" => Some(Role::User),
"assistant" | "model" => Some(Role::Assistant),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlockAttribution {
pub zone: Zone,
pub section: Section,
pub bucket: Bucket,
pub mcp_server: Option<String>,
pub tool_name: Option<String>,
pub role: Option<Role>,
pub msg_index: Option<usize>,
pub tokens: usize,
}
impl BlockAttribution {
pub fn category(&self) -> (&'static str, &'static str) {
match self.zone {
Zone::Output => {
let label = match self.bucket {
Bucket::ToolUse | Bucket::ToolResult => {
if self.mcp_server.is_some() {
"MCP tool use+output"
} else {
"system tool use+output"
}
}
_ => self.bucket.as_str(),
};
("Output", label)
}
Zone::Input => match self.section {
Section::Static => match self.bucket {
Bucket::System => ("Static", "System prompt"),
Bucket::Schema => {
if self.mcp_server.is_some() {
("Static", "MCP tools")
} else {
("Static", "System tools")
}
}
_ => ("Static", self.bucket.as_str()),
},
Section::Messages => match self.bucket {
Bucket::Text => match self.role {
Some(Role::User) => ("Messages", "user text"),
_ => ("Messages", "assistant text"),
},
Bucket::Thinking => ("Messages", "thinking"),
Bucket::ToolUse | Bucket::ToolResult => {
if self.mcp_server.is_some() {
("Messages", "MCP tool use+output")
} else {
("Messages", "system tool use+output")
}
}
_ => ("Messages", self.bucket.as_str()),
},
},
}
}
}
pub fn mcp_server_of(tool: &str) -> Option<&str> {
let rest = tool.strip_prefix("mcp__")?;
let server = rest.split("__").next().unwrap_or(rest);
(!server.is_empty()).then_some(server)
}
pub fn attribute(
body: &Value,
kind: ProviderKind,
counter: &dyn TokenCounter,
) -> Vec<BlockAttribution> {
let mut out = Vec::new();
match kind {
ProviderKind::Anthropic => attribute_anthropic(body, counter, &mut out),
ProviderKind::OpenAi => attribute_openai(body, counter, &mut out),
ProviderKind::Google => attribute_google(body, counter, &mut out),
}
out.retain(|b| b.tokens > 0);
out
}
fn count_value(v: &Value, counter: &dyn TokenCounter) -> usize {
match v {
Value::String(s) => counter.count(s),
Value::Null => 0,
other => counter.count(&other.to_string()),
}
}
fn count_text(v: &Value, counter: &dyn TokenCounter) -> usize {
match v {
Value::String(s) => counter.count(s),
_ => counter.count(&flatten_text(v)),
}
}
fn schema_tool_name(tool: &Value) -> Option<&str> {
tool.get("name").and_then(Value::as_str).or_else(|| {
tool.get("function")
.and_then(|f| f.get("name"))
.and_then(Value::as_str)
})
}
fn attribute_tools(tools: &Value, counter: &dyn TokenCounter, out: &mut Vec<BlockAttribution>) {
let Some(arr) = tools.as_array() else { return };
for tool in arr {
let name = schema_tool_name(tool);
out.push(BlockAttribution {
zone: Zone::Input,
section: Section::Static,
bucket: Bucket::Schema,
mcp_server: name.and_then(mcp_server_of).map(str::to_string),
tool_name: name.map(str::to_string),
role: None,
msg_index: None,
tokens: count_value(tool, counter),
});
}
}
fn attribute_anthropic(body: &Value, counter: &dyn TokenCounter, out: &mut Vec<BlockAttribution>) {
if let Some(system) = body.get("system") {
let tokens = count_value(system, counter);
if tokens > 0 {
out.push(BlockAttribution {
zone: Zone::Input,
section: Section::Static,
bucket: Bucket::System,
mcp_server: None,
tool_name: None,
role: None,
msg_index: None,
tokens,
});
}
}
if let Some(tools) = body.get("tools") {
attribute_tools(tools, counter, out);
}
let Some(messages) = body.get("messages").and_then(Value::as_array) else {
return;
};
for (i, msg) in messages.iter().enumerate() {
let role = msg
.get("role")
.and_then(Value::as_str)
.and_then(Role::parse);
match msg.get("content") {
Some(Value::String(s)) => push_text(out, Section::Messages, role, i, counter.count(s)),
Some(Value::Array(blocks)) => {
for b in blocks {
attribute_anthropic_block(b, role, i, counter, out);
}
}
_ => {}
}
}
}
fn attribute_anthropic_block(
b: &Value,
role: Option<Role>,
msg_index: usize,
counter: &dyn TokenCounter,
out: &mut Vec<BlockAttribution>,
) {
let ty = b.get("type").and_then(Value::as_str).unwrap_or("");
let (bucket, tokens) = match ty {
"text" => (
Bucket::Text,
count_text(b.get("text").unwrap_or(b), counter),
),
"thinking" | "redacted_thinking" => {
let t = b.get("thinking").or_else(|| b.get("data")).unwrap_or(b);
(Bucket::Thinking, count_text(t, counter))
}
"tool_use" | "server_tool_use" => (Bucket::ToolUse, count_value(b, counter)),
"tool_result" | "web_search_tool_result" => {
let c = b.get("content").unwrap_or(b);
(Bucket::ToolResult, count_text(c, counter))
}
_ => (Bucket::Text, count_value(b, counter)),
};
let tool_name = b.get("name").and_then(Value::as_str).map(str::to_string);
out.push(BlockAttribution {
zone: Zone::Input,
section: Section::Messages,
bucket,
mcp_server: tool_name
.as_deref()
.and_then(mcp_server_of)
.map(str::to_string),
tool_name,
role,
msg_index: Some(msg_index),
tokens,
});
}
fn attribute_openai(body: &Value, counter: &dyn TokenCounter, out: &mut Vec<BlockAttribution>) {
if let Some(instr) = body.get("instructions") {
let tokens = count_value(instr, counter);
if tokens > 0 {
out.push(BlockAttribution {
zone: Zone::Input,
section: Section::Static,
bucket: Bucket::System,
mcp_server: None,
tool_name: None,
role: None,
msg_index: None,
tokens,
});
}
}
if let Some(tools) = body.get("tools") {
attribute_tools(tools, counter, out);
}
let items = body
.get("messages")
.or_else(|| body.get("input"))
.and_then(Value::as_array);
let Some(items) = items else { return };
for (i, msg) in items.iter().enumerate() {
attribute_openai_item(msg, i, counter, out);
}
}
fn attribute_openai_item(
msg: &Value,
i: usize,
counter: &dyn TokenCounter,
out: &mut Vec<BlockAttribution>,
) {
let role_str = msg.get("role").and_then(Value::as_str).unwrap_or("");
let item_type = msg.get("type").and_then(Value::as_str).unwrap_or("");
match item_type {
"function_call" | "custom_tool_call" => {
let name = msg.get("name").and_then(Value::as_str).map(str::to_string);
out.push(BlockAttribution {
zone: Zone::Input,
section: Section::Messages,
bucket: Bucket::ToolUse,
mcp_server: name.as_deref().and_then(mcp_server_of).map(str::to_string),
tool_name: name,
role: Some(Role::Assistant),
msg_index: Some(i),
tokens: count_value(msg, counter),
});
return;
}
"function_call_output" | "custom_tool_call_output" => {
let c = msg.get("output").unwrap_or(msg);
out.push(BlockAttribution {
zone: Zone::Input,
section: Section::Messages,
bucket: Bucket::ToolResult,
mcp_server: None,
tool_name: None,
role: Some(Role::User),
msg_index: Some(i),
tokens: count_text(c, counter),
});
return;
}
_ => {}
}
if matches!(role_str, "system" | "developer") {
out.push(BlockAttribution {
zone: Zone::Input,
section: Section::Static,
bucket: Bucket::System,
mcp_server: None,
tool_name: None,
role: None,
msg_index: None,
tokens: count_text(msg.get("content").unwrap_or(msg), counter),
});
return;
}
if role_str == "tool" {
out.push(BlockAttribution {
zone: Zone::Input,
section: Section::Messages,
bucket: Bucket::ToolResult,
mcp_server: None,
tool_name: None,
role: Some(Role::User),
msg_index: Some(i),
tokens: count_text(msg.get("content").unwrap_or(msg), counter),
});
return;
}
let role = Role::parse(role_str);
if let Some(calls) = msg.get("tool_calls").and_then(Value::as_array) {
for call in calls {
let name = call
.get("function")
.and_then(|f| f.get("name"))
.and_then(Value::as_str)
.map(str::to_string);
out.push(BlockAttribution {
zone: Zone::Input,
section: Section::Messages,
bucket: Bucket::ToolUse,
mcp_server: name.as_deref().and_then(mcp_server_of).map(str::to_string),
tool_name: name,
role: Some(Role::Assistant),
msg_index: Some(i),
tokens: count_value(call, counter),
});
}
}
if let Some(content) = msg.get("content") {
let tokens = count_text(content, counter);
if tokens > 0 {
push_text(out, Section::Messages, role, i, tokens);
}
}
}
fn attribute_google(body: &Value, counter: &dyn TokenCounter, out: &mut Vec<BlockAttribution>) {
if let Some(si) = body
.get("systemInstruction")
.or_else(|| body.get("system_instruction"))
{
let tokens = count_value(si, counter);
if tokens > 0 {
out.push(BlockAttribution {
zone: Zone::Input,
section: Section::Static,
bucket: Bucket::System,
mcp_server: None,
tool_name: None,
role: None,
msg_index: None,
tokens,
});
}
}
if let Some(tools) = body.get("tools").and_then(Value::as_array) {
for tool in tools {
if let Some(decls) = tool.get("functionDeclarations").and_then(Value::as_array) {
attribute_tools(&Value::Array(decls.clone()), counter, out);
}
}
}
let Some(contents) = body.get("contents").and_then(Value::as_array) else {
return;
};
for (i, msg) in contents.iter().enumerate() {
let role = msg
.get("role")
.and_then(Value::as_str)
.and_then(Role::parse);
let Some(parts) = msg.get("parts").and_then(Value::as_array) else {
continue;
};
for part in parts {
if let Some(fc) = part.get("functionCall") {
let name = fc.get("name").and_then(Value::as_str).map(str::to_string);
out.push(BlockAttribution {
zone: Zone::Input,
section: Section::Messages,
bucket: Bucket::ToolUse,
mcp_server: name.as_deref().and_then(mcp_server_of).map(str::to_string),
tool_name: name,
role: Some(Role::Assistant),
msg_index: Some(i),
tokens: count_value(fc, counter),
});
} else if let Some(fr) = part.get("functionResponse") {
out.push(BlockAttribution {
zone: Zone::Input,
section: Section::Messages,
bucket: Bucket::ToolResult,
mcp_server: None,
tool_name: None,
role: Some(Role::User),
msg_index: Some(i),
tokens: count_value(fr, counter),
});
} else if let Some(t) = part.get("text") {
let tokens = counter.count(t.as_str().unwrap_or(""));
if tokens > 0 {
push_text(out, Section::Messages, role, i, tokens);
}
}
}
}
}
fn push_text(
out: &mut Vec<BlockAttribution>,
section: Section,
role: Option<Role>,
msg_index: usize,
tokens: usize,
) {
if tokens == 0 {
return;
}
out.push(BlockAttribution {
zone: Zone::Input,
section,
bucket: Bucket::Text,
mcp_server: None,
tool_name: None,
role,
msg_index: Some(msg_index),
tokens,
});
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RequestIdentity {
pub session_id: Option<String>,
pub agent: Option<String>,
pub project: Option<String>,
}
pub fn extract_identity(body: &Value, kind: ProviderKind) -> RequestIdentity {
let system = system_text(body, kind);
RequestIdentity {
session_id: system.as_deref().map(stable_hash),
agent: system
.as_deref()
.and_then(fingerprint_agent)
.map(str::to_string),
project: system.as_deref().and_then(extract_cwd),
}
}
fn system_text(body: &Value, kind: ProviderKind) -> Option<String> {
let field = match kind {
ProviderKind::Anthropic => body.get("system"),
ProviderKind::OpenAi => body.get("instructions"),
ProviderKind::Google => body
.get("systemInstruction")
.or_else(|| body.get("system_instruction")),
};
if let Some(v) = field {
return Some(flatten_text(v));
}
let items = body
.get("messages")
.or_else(|| body.get("input"))
.and_then(Value::as_array)?;
items
.iter()
.find(|m| {
matches!(
m.get("role").and_then(Value::as_str),
Some("system") | Some("developer")
)
})
.map(|m| flatten_text(m.get("content").unwrap_or(m)))
}
fn flatten_text(v: &Value) -> String {
fn walk(v: &Value, acc: &mut String) {
match v {
Value::String(s) => {
acc.push_str(s);
acc.push('\n');
}
Value::Array(a) => a.iter().for_each(|x| walk(x, acc)),
Value::Object(o) => {
if let Some(t) = o.get("text").and_then(Value::as_str) {
acc.push_str(t);
acc.push('\n');
}
}
_ => {}
}
}
let mut acc = String::new();
walk(v, &mut acc);
acc
}
fn stable_hash(s: &str) -> String {
let mut h: u64 = 0xcbf29ce484222325;
for b in s.bytes() {
h ^= u64::from(b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{h:016x}")
}
struct AgentFingerprint {
label: &'static str,
markers: &'static [&'static str],
}
static AGENTS: &[AgentFingerprint] = &[
AgentFingerprint {
label: "claude-code",
markers: &["Claude Code"],
},
AgentFingerprint {
label: "codex",
markers: &["running as a coding agent in the Codex CLI", "Codex CLI"],
},
AgentFingerprint {
label: "cursor",
markers: &["You operate exclusively in Cursor"],
},
AgentFingerprint {
label: "cline",
markers: &["You are Cline,"],
},
AgentFingerprint {
label: "roo",
markers: &["You are Roo,"],
},
AgentFingerprint {
label: "kilo",
markers: &["You are Kilo Code,"],
},
AgentFingerprint {
label: "goose",
markers: &["agent called goose"],
},
AgentFingerprint {
label: "opencode",
markers: &["You are OpenCode,", "You are opencode,"],
},
AgentFingerprint {
label: "crush",
markers: &["You are Crush,"],
},
AgentFingerprint {
label: "qwen",
markers: &["You are Qwen Code,"],
},
AgentFingerprint {
label: "grok",
markers: &["You are Grok CLI"],
},
AgentFingerprint {
label: "kimi",
markers: &["You are Kimi Code CLI,"],
},
AgentFingerprint {
label: "mistral-vibe",
markers: &["operating as and within Mistral Vibe"],
},
AgentFingerprint {
label: "mux",
markers: &["agent called Mux"],
},
AgentFingerprint {
label: "pi",
markers: &["Oh My Pi"],
},
AgentFingerprint {
label: "forge",
markers: &["You are Forge,"],
},
AgentFingerprint {
label: "openclaw",
markers: &["OpenClaw"],
},
AgentFingerprint {
label: "gemini",
markers: &[
"You are Gemini CLI,",
"interactive CLI agent specializing in software engineering tasks",
],
},
];
fn fingerprint_agent(system: &str) -> Option<&'static str> {
AGENTS
.iter()
.find(|a| a.markers.iter().any(|m| system.contains(m)))
.map(|a| a.label)
}
fn extract_cwd(system: &str) -> Option<String> {
for line in system.lines() {
let line = line.trim();
for marker in ["Current working directory:", "cwd:", "Working directory:"] {
if let Some(rest) = line.strip_prefix(marker) {
let p = rest.trim().trim_matches(|c| c == '`' || c == '"');
if !p.is_empty() {
return Some(p.to_string());
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
struct WordCounter;
impl TokenCounter for WordCounter {
fn count(&self, text: &str) -> usize {
text.split_whitespace().count()
}
fn is_exact(&self) -> bool {
false
}
fn label(&self) -> &str {
"words"
}
}
fn cats(blocks: &[BlockAttribution]) -> Vec<(&'static str, &'static str)> {
blocks.iter().map(BlockAttribution::category).collect()
}
#[test]
fn mcp_server_extraction() {
assert_eq!(mcp_server_of("mcp__github__create_issue"), Some("github"));
assert_eq!(mcp_server_of("mcp__filesystem__read"), Some("filesystem"));
assert_eq!(mcp_server_of("Read"), None);
assert_eq!(mcp_server_of("mcp__solo"), Some("solo"));
assert_eq!(mcp_server_of("mcp__"), None);
}
#[test]
fn tool_result_content_array_counts_inner_text_not_json() {
let body = json!({
"messages": [
{"role": "user", "content": [
{"type": "tool_result", "content": [{"type": "text", "text": "one two three"}]}
]}
]
});
let blocks = attribute(&body, ProviderKind::Anthropic, &WordCounter);
let tr = blocks
.iter()
.find(|b| b.bucket == Bucket::ToolResult)
.unwrap();
assert_eq!(tr.tokens, 3);
}
#[test]
fn openai_content_array_counts_text_only() {
let body = json!({
"messages": [
{"role": "user", "content": [{"type": "text", "text": "alpha beta"}]}
]
});
let blocks = attribute(&body, ProviderKind::OpenAi, &WordCounter);
let txt = blocks.iter().find(|b| b.bucket == Bucket::Text).unwrap();
assert_eq!(txt.tokens, 2);
}
#[test]
fn anthropic_full_request() {
let body = json!({
"model": "claude-sonnet-4",
"system": "You are Claude Code. Current working directory: /home/me/proj",
"tools": [
{"name": "Read", "description": "read a file"},
{"name": "mcp__github__create_issue", "description": "open an issue"}
],
"messages": [
{"role": "user", "content": "hello there friend"},
{"role": "assistant", "content": [
{"type": "thinking", "thinking": "let me think about this"},
{"type": "tool_use", "name": "mcp__github__create_issue", "input": {"title": "bug"}}
]},
{"role": "user", "content": [
{"type": "tool_result", "content": "issue created ok"}
]}
]
});
let blocks = attribute(&body, ProviderKind::Anthropic, &WordCounter);
assert_eq!(
cats(&blocks),
vec![
("Static", "System prompt"),
("Static", "System tools"),
("Static", "MCP tools"),
("Messages", "user text"),
("Messages", "thinking"),
("Messages", "MCP tool use+output"),
("Messages", "system tool use+output"),
]
);
let mcp_tool = blocks
.iter()
.find(|b| b.bucket == Bucket::Schema && b.mcp_server.is_some());
assert_eq!(mcp_tool.unwrap().mcp_server.as_deref(), Some("github"));
}
#[test]
fn openai_chat_request() {
let body = json!({
"messages": [
{"role": "system", "content": "system rules"},
{"role": "user", "content": "do a thing"},
{"role": "assistant", "content": null, "tool_calls": [
{"function": {"name": "mcp__db__query"}}
]},
{"role": "tool", "content": "42 rows"}
],
"tools": [{"function": {"name": "mcp__db__query"}}]
});
let blocks = attribute(&body, ProviderKind::OpenAi, &WordCounter);
assert_eq!(
cats(&blocks),
vec![
("Static", "MCP tools"),
("Static", "System prompt"),
("Messages", "user text"),
("Messages", "MCP tool use+output"),
("Messages", "system tool use+output"),
]
);
}
#[test]
fn empty_and_malformed_bodies_yield_no_panic() {
assert!(attribute(&json!({}), ProviderKind::Anthropic, &WordCounter).is_empty());
assert!(
attribute(
&json!({"messages": "not an array"}),
ProviderKind::OpenAi,
&WordCounter
)
.is_empty()
);
assert!(attribute(&json!(7), ProviderKind::Google, &WordCounter).is_empty());
}
#[test]
fn identity_from_claude_code_system() {
let body = json!({
"system": "You are Claude Code, Anthropic's CLI.\nCurrent working directory: /home/me/app",
"messages": [{"role": "user", "content": "hi"}]
});
let id = extract_identity(&body, ProviderKind::Anthropic);
assert_eq!(id.agent.as_deref(), Some("claude-code"));
assert_eq!(id.project.as_deref(), Some("/home/me/app"));
assert!(id.session_id.is_some());
let body2 = json!({
"system": "You are Claude Code, Anthropic's CLI.\nCurrent working directory: /home/me/app",
"messages": [{"role": "user", "content": "second turn"}]
});
assert_eq!(
id.session_id,
extract_identity(&body2, ProviderKind::Anthropic).session_id
);
}
#[test]
fn every_registered_agent_fingerprints() {
for agent in AGENTS {
let marker = agent.markers[0];
let body = json!({ "system": marker, "messages": [] });
let id = extract_identity(&body, ProviderKind::Anthropic);
assert_eq!(
id.agent.as_deref(),
Some(agent.label),
"marker {marker:?} did not fingerprint as {}",
agent.label
);
}
}
#[test]
fn qwen_legacy_phrase_does_not_steal_from_gemini_fork() {
let body = json!({
"system": "You are Qwen Code, an interactive CLI agent specializing in software engineering tasks.",
"messages": []
});
assert_eq!(
extract_identity(&body, ProviderKind::Anthropic)
.agent
.as_deref(),
Some("qwen")
);
}
#[test]
fn unknown_agent_is_none() {
let body = json!({ "system": "You are a helpful assistant.", "messages": [] });
assert_eq!(extract_identity(&body, ProviderKind::Anthropic).agent, None);
}
#[test]
fn google_request() {
let body = json!({
"systemInstruction": {"parts": [{"text": "be concise"}]},
"tools": [{"functionDeclarations": [{"name": "mcp__x__y"}]}],
"contents": [
{"role": "user", "parts": [{"text": "question here"}]},
{"role": "model", "parts": [{"functionCall": {"name": "mcp__x__y"}}]}
]
});
let blocks = attribute(&body, ProviderKind::Google, &WordCounter);
assert_eq!(
cats(&blocks),
vec![
("Static", "System prompt"),
("Static", "MCP tools"),
("Messages", "user text"),
("Messages", "MCP tool use+output"),
]
);
}
}