agent_engine/runtime/trace/
anthropic.rs1use super::emit::{wire_meta_from_sent_bytes, RequestStructure};
9use super::key::{keyed_digest, DigestDomain, TraceDigestKey};
10use super::types::{
11 BlockKind, BlockMeta, CacheBoundaryLocation, CacheBoundaryMeta, CacheMeta, CacheTtlClass,
12 MessageMeta, MessageRole, RequestAnatomy, RetryClass, StopReason, SystemSegmentKind,
13 SystemSegmentMeta, ToolMeta, TraceId, WireName,
14};
15use serde_json::Value;
16
17pub fn retry_class_for_status(status: u16) -> RetryClass {
19 match status {
20 429 => RetryClass::RateLimited,
21 529 => RetryClass::Overloaded,
22 500 | 502 | 503 => RetryClass::ServerError,
23 401 | 403 => RetryClass::Auth,
24 408 => RetryClass::Timeout,
25 _ => RetryClass::Other,
26 }
27}
28
29pub fn retry_class_for_stream_error(error_type: Option<&str>) -> RetryClass {
32 match error_type {
33 Some("overloaded_error") => RetryClass::Overloaded,
34 Some("rate_limit_error") => RetryClass::RateLimited,
35 Some("api_error") => RetryClass::ServerError,
36 Some(_) => RetryClass::Other,
37 None => RetryClass::Network,
38 }
39}
40
41pub fn stop_reason_from_wire(raw: &str) -> StopReason {
44 match raw {
45 "end_turn" => StopReason::EndTurn,
46 "max_tokens" => StopReason::MaxTokens,
47 "stop_sequence" => StopReason::StopSequence,
48 "tool_use" => StopReason::ToolUse,
49 "pause_turn" => StopReason::PauseTurn,
50 "refusal" => StopReason::Refusal,
51 _ => StopReason::Other,
52 }
53}
54
55#[allow(clippy::too_many_arguments)]
68pub fn anthropic_request_structure(
69 key: Option<&TraceDigestKey>,
70 sent_bytes: &[u8],
71 messages: &[crate::SharedMessage],
72 system_prompt: Option<&str>,
73 tools_schema: &[Value],
74 prefix_marker_ttl: Option<CacheTtlClass>,
75 has_tool_marker: bool,
76 has_system_marker: bool,
77 translation: Vec<super::types::TranslationLoss>,
78 snapshots: Option<&super::diagnostics::CacheSnapshotStore>,
79) -> RequestStructure {
80 let message_meta: Vec<MessageMeta> = messages.iter().map(|m| message_meta(m)).collect();
81 let block_count: u32 = message_meta.iter().map(|m| m.blocks.len() as u32).sum();
82
83 let system_segment_count = u32::from(system_prompt.is_some_and(|s| !s.is_empty()));
84 let anatomy = RequestAnatomy {
85 system_segment_count,
86 message_count: messages.len() as u32,
87 block_count,
88 tool_count: tools_schema.len() as u32,
89 };
90
91 let system_segments = match (key, system_prompt) {
92 (Some(key), Some(s)) if !s.is_empty() => vec![SystemSegmentMeta {
93 kind: SystemSegmentKind::Primary,
94 byte_len: s.len() as u64,
95 digest: keyed_digest(key, DigestDomain::SystemSegment, s.as_bytes()),
96 }],
97 _ => Vec::new(),
98 };
99
100 let tools = key
101 .map(|key| {
102 tools_schema
103 .iter()
104 .filter_map(|t| tool_meta(key, t))
105 .collect()
106 })
107 .unwrap_or_default();
108
109 let mut cache = cache_meta(
110 messages,
111 tools_schema.len(),
112 prefix_marker_ttl,
113 has_tool_marker,
114 has_system_marker,
115 );
116 if let Some(snapshots) = snapshots {
120 let diag = snapshots.compare_and_update(key, tools_schema, system_prompt, messages);
121 cache.tools_prefix = diag.tools_prefix;
122 cache.system_prefix = diag.system_prefix;
123 cache.history_tail = diag.history_tail;
124 cache.delta = diag.delta;
125 }
126
127 RequestStructure {
128 anatomy,
129 wire: key.map(|key| wire_meta_from_sent_bytes(key, sent_bytes)),
130 system_segments,
131 messages: message_meta,
132 tools,
133 cache,
134 translation,
135 }
136}
137
138fn message_meta(message: &Value) -> MessageMeta {
139 let role = match message["role"].as_str() {
140 Some("assistant") => MessageRole::Assistant,
141 Some("system") => MessageRole::System,
142 Some("tool") => MessageRole::Tool,
143 _ => MessageRole::User,
144 };
145 let blocks = match &message["content"] {
146 Value::String(s) => vec![BlockMeta {
147 kind: BlockKind::Text,
148 byte_len: s.len() as u64,
149 }],
150 Value::Array(items) => items.iter().map(block_meta).collect(),
151 other => vec![BlockMeta {
152 kind: BlockKind::Unknown,
153 byte_len: serialized_len(other),
154 }],
155 };
156 MessageMeta { role, blocks }
157}
158
159fn block_meta(block: &Value) -> BlockMeta {
160 let kind = match block["type"].as_str() {
161 Some("text") => BlockKind::Text,
162 Some("thinking") | Some("redacted_thinking") => BlockKind::Thinking,
163 Some("tool_use") => BlockKind::ToolUse,
164 Some("tool_result") => BlockKind::ToolResult,
165 Some("image") | Some("document") => BlockKind::Media,
166 _ => BlockKind::Unknown,
167 };
168 BlockMeta {
169 kind,
170 byte_len: serialized_len(block),
171 }
172}
173
174fn serialized_len(value: &Value) -> u64 {
175 serde_json::to_vec(value)
176 .map(|v| v.len() as u64)
177 .unwrap_or(0)
178}
179
180fn tool_meta(key: &TraceDigestKey, tool: &Value) -> Option<ToolMeta> {
181 let name = tool["name"].as_str()?;
182 let wire_name = WireName::new(name).ok()?;
186 let stable_id = TraceId::new(name).ok()?;
187 let schema_bytes = serde_json::to_vec(&tool["input_schema"]).ok()?;
188 Some(ToolMeta {
189 stable_id,
190 wire_name,
191 schema_byte_len: schema_bytes.len() as u64,
192 schema_digest: keyed_digest(key, DigestDomain::ToolSchema, &schema_bytes),
193 })
194}
195
196fn cache_meta(
201 messages: &[crate::SharedMessage],
202 tool_count: usize,
203 prefix_marker_ttl: Option<CacheTtlClass>,
204 has_tool_marker: bool,
205 has_system_marker: bool,
206) -> CacheMeta {
207 let mut boundaries = Vec::new();
208 if let (Some(ttl), true) = (prefix_marker_ttl, has_tool_marker) {
209 boundaries.push(CacheBoundaryMeta {
210 location: CacheBoundaryLocation::Tools,
211 index: tool_count.saturating_sub(1) as u32,
212 ttl,
213 });
214 }
215 if let (Some(ttl), true) = (prefix_marker_ttl, has_system_marker) {
216 boundaries.push(CacheBoundaryMeta {
217 location: CacheBoundaryLocation::System,
218 index: 0,
219 ttl,
220 });
221 }
222 for (index, message) in messages.iter().enumerate() {
223 let marker = message["content"]
224 .as_array()
225 .and_then(|blocks| blocks.last())
226 .and_then(|b| b.get("cache_control"));
227 if let Some(marker) = marker {
228 let ttl = if marker.get("ttl").and_then(Value::as_str) == Some("1h") {
229 CacheTtlClass::OneHour
230 } else {
231 CacheTtlClass::FiveMinutes
232 };
233 boundaries.push(CacheBoundaryMeta {
234 location: CacheBoundaryLocation::Messages,
235 index: index as u32,
236 ttl,
237 });
238 }
239 }
240 CacheMeta {
241 boundaries,
242 tools_prefix: None,
243 system_prefix: None,
244 history_tail: None,
245 delta: None,
246 }
247}