Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 79 additions & 3 deletions src/agentsight/src/analyzer/audit/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,16 @@ impl AuditAnalyzer {
http_record: &HttpRecord,
token_record: Option<&TokenRecord>,
) -> Option<AuditRecord> {
// Only create llm_call for SSE responses
if !http_record.is_sse {
// Create llm_call audit records for SSE responses AND non-streaming
// LLM API calls (identified by path). Without this, non-streaming
// completions (stream:false) are invisible in audit --type llm.
let is_llm_path = http_record.path.contains("/chat/completions")
|| http_record.path.contains("/v1/messages")
|| http_record.path.contains("/v1/completions")
|| http_record
.path
.contains("/api/v1/copilot/generate_copilot");
if !http_record.is_sse && !is_llm_path {
return None;
}

Expand Down Expand Up @@ -88,7 +96,7 @@ impl AuditAnalyzer {
output_tokens,
cache_creation_tokens,
cache_read_tokens,
is_sse: true,
is_sse: http_record.is_sse,
},
session_id: None,
})
Expand Down Expand Up @@ -181,6 +189,7 @@ fn detect_model_from_request(pair: &HttpPair) -> Option<String> {
mod tests {
use super::*;
use crate::aggregator::AggregatedProcess;
use crate::analyzer::token::TokenRecord;

#[test]
fn test_extract_process_action_propagates_session_id() {
Expand Down Expand Up @@ -208,4 +217,71 @@ mod tests {

assert_eq!(record.session_id, None);
}

fn make_http_record(path: &str, is_sse: bool, response_body: Option<&str>) -> HttpRecord {
HttpRecord {
timestamp_ns: 0,
pid: 1,
comm: "test".into(),
method: "POST".into(),
path: path.into(),
status_code: 200,
request_headers: "{}".into(),
request_body: Some(r#"{"model":"test-model"}"#.into()),
response_headers: "{}".into(),
response_body: response_body.map(|s| s.to_string()),
duration_ns: 1000,
is_sse,
sse_event_count: 0,
}
}

#[test]
fn test_nonsse_llm_path_produces_audit() {
let analyzer = AuditAnalyzer::new();
let record = make_http_record("/v1/chat/completions", false, None);
let token = TokenRecord::new(1, "test".into(), "openai".into(), 100, 20);
let result = analyzer.analyze_http(&record, Some(&token));
assert!(
result.is_some(),
"non-SSE LLM path must produce audit record"
);
let audit = result.unwrap();
if let AuditExtra::LlmCall {
is_sse,
input_tokens,
output_tokens,
..
} = &audit.extra
{
assert!(!is_sse, "non-SSE call must have is_sse=false");
assert_eq!(*input_tokens, 100);
assert_eq!(*output_tokens, 20);
} else {
panic!("expected LlmCall extra");
}
}

#[test]
fn test_nonsse_nonllm_path_no_audit() {
let analyzer = AuditAnalyzer::new();
let record = make_http_record("/api/health", false, None);
let result = analyzer.analyze_http(&record, None);
assert!(
result.is_none(),
"non-LLM path must NOT produce audit record"
);
}

#[test]
fn test_sse_path_produces_audit_with_sse_true() {
let analyzer = AuditAnalyzer::new();
let record = make_http_record("/v1/chat/completions", true, None);
let token = TokenRecord::new(1, "test".into(), "openai".into(), 50, 10);
let result = analyzer.analyze_http(&record, Some(&token));
assert!(result.is_some());
if let AuditExtra::LlmCall { is_sse, .. } = &result.unwrap().extra {
assert!(*is_sse, "SSE call must have is_sse=true");
}
}
}
2 changes: 1 addition & 1 deletion src/agentsight/src/bin/cli/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ fn gather_sessions(path: &Path, start_ns: i64, end_ns: i64) -> SessionStats {
return SessionStats::default();
}
};
let sessions = match store.list_sessions(start_ns, end_ns) {
let sessions = match store.list_sessions(start_ns, end_ns, true) {
Ok(s) => s,
Err(e) => {
eprintln!("warning: cannot query sessions: {e}");
Expand Down
22 changes: 9 additions & 13 deletions src/agentsight/src/server/token_savings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,22 +171,18 @@ fn generate_optimization_reason(operation: &str, before_tokens: i64, after_token
};
match operation {
"compress-response" => format!(
"MCP 服务器返回的响应内容经过压缩处理,移除冗余字段和重复信息,节省 {}%({} tokens)",
pct, saved
"MCP 服务器返回的响应内容经过压缩处理,移除冗余字段和重复信息,节省 {pct}%({saved} tokens)"
),
"rewrite-command" => format!(
"工具调用的输出内容经过精简重写,保留关键语义同时降低 token 开销,节省 {}%({} tokens)",
pct, saved
"工具调用的输出内容经过精简重写,保留关键语义同时降低 token 开销,节省 {pct}%({saved} tokens)"
),
"compress-schema" => format!(
"工具的 JSON Schema 定义经过压缩,移除描述性文本和可选字段,节省 {}%({} tokens)",
pct, saved
"工具的 JSON Schema 定义经过压缩,移除描述性文本和可选字段,节省 {pct}%({saved} tokens)"
),
"compress-toon" => format!(
"使用 TOON 结构化编码替代原始 JSON,大幅缩减 token 占用,节省 {}%({} tokens)",
pct, saved
"使用 TOON 结构化编码替代原始 JSON,大幅缩减 token 占用,节省 {pct}%({saved} tokens)"
),
_ => format!("内容经过优化处理,节省 {}%({} tokens)", pct, saved),
_ => format!("内容经过优化处理,节省 {pct}%({saved} tokens)"),
}
}

Expand Down Expand Up @@ -1139,11 +1135,11 @@ mod tests {
#[test]
fn test_diff_large_input_fallback() {
let big_before = (0..1200)
.map(|i| format!("line{}", i))
.map(|i| format!("line{i}"))
.collect::<Vec<_>>()
.join("\n");
let big_after = (0..1000)
.map(|i| format!("new{}", i))
.map(|i| format!("new{i}"))
.collect::<Vec<_>>()
.join("\n");
let result = compute_diff_lines(Some(&big_before), Some(&big_after));
Expand All @@ -1163,11 +1159,11 @@ mod tests {
fn test_diff_max_lines_cap() {
// Create input that would generate many diff lines
let before = (0..150)
.map(|i| format!("old_{}", i))
.map(|i| format!("old_{i}"))
.collect::<Vec<_>>()
.join("\n");
let after = (0..150)
.map(|i| format!("new_{}", i))
.map(|i| format!("new_{i}"))
.collect::<Vec<_>>()
.join("\n");
let result = compute_diff_lines(Some(&before), Some(&after));
Expand Down
Loading