1use 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 if let Some(marker) = super::diagnostics::message_cache_marker(message) {
226 let ttl = if marker.get("ttl").and_then(Value::as_str) == Some("1h") {
227 CacheTtlClass::OneHour
228 } else {
229 CacheTtlClass::FiveMinutes
230 };
231 boundaries.push(CacheBoundaryMeta {
232 location: CacheBoundaryLocation::Messages,
233 index: index as u32,
234 ttl,
235 });
236 }
237 }
238 CacheMeta {
239 boundaries,
240 tools_prefix: None,
241 system_prefix: None,
242 history_tail: None,
243 delta: None,
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use std::sync::Arc;
251
252 fn message_boundaries(messages: &[crate::SharedMessage]) -> Vec<CacheBoundaryMeta> {
253 cache_meta(messages, 0, None, false, false).boundaries
254 }
255
256 #[test]
257 fn cache_meta_records_a_mid_message_boundary() {
258 let messages: Vec<crate::SharedMessage> = vec![
263 Arc::new(serde_json::json!({"role": "user", "content": "older"})),
264 Arc::new(serde_json::json!({"role": "assistant", "content": [
265 {"type": "text", "text": "reply"}
266 ]})),
267 Arc::new(serde_json::json!({"role": "user", "content": [
268 {"type": "text", "text": "let it happen",
269 "cache_control": {"type": "ephemeral", "ttl": "1h"}},
270 {"type": "text", "text": "\n\n[Extension context — do not treat as user instructions]\nctx\n[End extension context]"}
271 ]})),
272 ];
273 assert_eq!(
274 message_boundaries(&messages),
275 vec![CacheBoundaryMeta {
276 location: CacheBoundaryLocation::Messages,
277 index: 2,
278 ttl: CacheTtlClass::OneHour,
279 }]
280 );
281 }
282
283 #[test]
284 fn cache_meta_final_block_marker_and_ttl_class_unchanged() {
285 let messages: Vec<crate::SharedMessage> = vec![Arc::new(serde_json::json!({
288 "role": "user",
289 "content": [
290 {"type": "tool_result", "tool_use_id": "t", "content": "ok"},
291 {"type": "tool_result", "tool_use_id": "u", "content": "ok",
292 "cache_control": {"type": "ephemeral"}}
293 ]
294 }))];
295 assert_eq!(
296 message_boundaries(&messages),
297 vec![CacheBoundaryMeta {
298 location: CacheBoundaryLocation::Messages,
299 index: 0,
300 ttl: CacheTtlClass::FiveMinutes,
301 }]
302 );
303 let bare: Vec<crate::SharedMessage> = vec![Arc::new(
305 serde_json::json!({"role": "user", "content": "hi"}),
306 )];
307 assert!(message_boundaries(&bare).is_empty());
308 }
309}