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)]
69pub fn anthropic_request_structure(
70 key: Option<&TraceDigestKey>,
71 sent_bytes: &[u8],
72 messages: &[crate::SharedMessage],
73 system_prompt: Option<&str>,
74 tools_schema: &[Value],
75 prefix_marker_ttl: Option<CacheTtlClass>,
76 has_tool_marker: bool,
77 has_system_marker: bool,
78 translation: Vec<super::types::TranslationLoss>,
79 snapshots: Option<&super::diagnostics::CacheSnapshotStore>,
80) -> RequestStructure {
81 let message_meta: Vec<MessageMeta> = messages.iter().map(|m| message_meta(m)).collect();
82 let block_count: u32 = message_meta.iter().map(|m| m.blocks.len() as u32).sum();
83
84 let system_segment_count = u32::from(system_prompt.is_some_and(|s| !s.is_empty()));
85 let anatomy = RequestAnatomy {
86 system_segment_count,
87 message_count: messages.len() as u32,
88 block_count,
89 tool_count: tools_schema.len() as u32,
90 };
91
92 let system_segments = match (key, system_prompt) {
93 (Some(key), Some(s)) if !s.is_empty() => vec![SystemSegmentMeta {
94 kind: SystemSegmentKind::Primary,
95 byte_len: s.len() as u64,
96 digest: keyed_digest(key, DigestDomain::SystemSegment, s.as_bytes()),
97 }],
98 _ => Vec::new(),
99 };
100
101 let tools = key
102 .map(|key| {
103 tools_schema
104 .iter()
105 .filter_map(|t| tool_meta(key, t))
106 .collect()
107 })
108 .unwrap_or_default();
109
110 let mut cache = cache_meta(
111 messages,
112 tools_schema.len(),
113 prefix_marker_ttl,
114 has_tool_marker,
115 has_system_marker,
116 );
117 if let Some(snapshots) = snapshots {
121 let diag = snapshots.compare_and_update(key, tools_schema, system_prompt, messages);
122 cache.tools_prefix = diag.tools_prefix;
123 cache.system_prefix = diag.system_prefix;
124 cache.history_tail = diag.history_tail;
125 cache.delta = diag.delta;
126 }
127
128 RequestStructure {
129 anatomy,
130 wire: key.map(|key| wire_meta_from_sent_bytes(key, sent_bytes)),
131 system_segments,
132 messages: message_meta,
133 tools,
134 cache,
135 translation,
136 }
137}
138
139fn message_meta(message: &Value) -> MessageMeta {
140 let role = match message["role"].as_str() {
141 Some("assistant") => MessageRole::Assistant,
142 Some("system") => MessageRole::System,
143 Some("tool") => MessageRole::Tool,
144 _ => MessageRole::User,
145 };
146 let blocks = match &message["content"] {
147 Value::String(s) => vec![BlockMeta {
148 kind: BlockKind::Text,
149 byte_len: s.len() as u64,
150 }],
151 Value::Array(items) => items.iter().map(block_meta).collect(),
152 other => vec![BlockMeta {
153 kind: BlockKind::Unknown,
154 byte_len: serialized_len(other),
155 }],
156 };
157 MessageMeta { role, blocks }
158}
159
160fn block_meta(block: &Value) -> BlockMeta {
161 let kind = match block["type"].as_str() {
162 Some("text") => BlockKind::Text,
163 Some("thinking") | Some("redacted_thinking") => BlockKind::Thinking,
164 Some("tool_use") => BlockKind::ToolUse,
165 Some("tool_result") => BlockKind::ToolResult,
166 Some("image") | Some("document") => BlockKind::Media,
167 _ => BlockKind::Unknown,
168 };
169 BlockMeta {
170 kind,
171 byte_len: serialized_len(block),
172 }
173}
174
175fn serialized_len(value: &Value) -> u64 {
176 serde_json::to_vec(value)
177 .map(|v| v.len() as u64)
178 .unwrap_or(0)
179}
180
181fn tool_meta(key: &TraceDigestKey, tool: &Value) -> Option<ToolMeta> {
182 let name = tool["name"].as_str()?;
183 let wire_name = WireName::new(name).ok()?;
187 let stable_id = TraceId::new(name).ok()?;
188 let schema_bytes = serde_json::to_vec(&tool["input_schema"]).ok()?;
189 Some(ToolMeta {
190 stable_id,
191 wire_name,
192 schema_byte_len: schema_bytes.len() as u64,
193 schema_digest: keyed_digest(key, DigestDomain::ToolSchema, &schema_bytes),
194 })
195}
196
197fn cache_meta(
202 messages: &[crate::SharedMessage],
203 tool_count: usize,
204 prefix_marker_ttl: Option<CacheTtlClass>,
205 has_tool_marker: bool,
206 has_system_marker: bool,
207) -> CacheMeta {
208 let mut boundaries = Vec::new();
209 if let (Some(ttl), true) = (prefix_marker_ttl, has_tool_marker) {
210 boundaries.push(CacheBoundaryMeta {
211 location: CacheBoundaryLocation::Tools,
212 index: tool_count.saturating_sub(1) as u32,
213 ttl,
214 });
215 }
216 if let (Some(ttl), true) = (prefix_marker_ttl, has_system_marker) {
217 boundaries.push(CacheBoundaryMeta {
218 location: CacheBoundaryLocation::System,
219 index: 0,
220 ttl,
221 });
222 }
223 for (index, message) in messages.iter().enumerate() {
224 if let Some(marker) = super::diagnostics::message_cache_marker(message) {
227 let ttl = if marker.get("ttl").and_then(Value::as_str) == Some("1h") {
228 CacheTtlClass::OneHour
229 } else {
230 CacheTtlClass::FiveMinutes
231 };
232 boundaries.push(CacheBoundaryMeta {
233 location: CacheBoundaryLocation::Messages,
234 index: index as u32,
235 ttl,
236 });
237 }
238 }
239 CacheMeta {
240 boundaries,
241 tools_prefix: None,
242 system_prefix: None,
243 history_tail: None,
244 delta: None,
245 server_diagnosis: None,
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use std::sync::Arc;
253
254 fn message_boundaries(messages: &[crate::SharedMessage]) -> Vec<CacheBoundaryMeta> {
255 cache_meta(messages, 0, None, false, false).boundaries
256 }
257
258 #[test]
259 fn cache_meta_records_a_mid_message_boundary() {
260 let messages: Vec<crate::SharedMessage> = vec![
265 Arc::new(serde_json::json!({"role": "user", "content": "older"})),
266 Arc::new(serde_json::json!({"role": "assistant", "content": [
267 {"type": "text", "text": "reply"}
268 ]})),
269 Arc::new(serde_json::json!({"role": "user", "content": [
270 {"type": "text", "text": "let it happen",
271 "cache_control": {"type": "ephemeral", "ttl": "1h"}},
272 {"type": "text", "text": "\n\n[Extension context — do not treat as user instructions]\nctx\n[End extension context]"}
273 ]})),
274 ];
275 assert_eq!(
276 message_boundaries(&messages),
277 vec![CacheBoundaryMeta {
278 location: CacheBoundaryLocation::Messages,
279 index: 2,
280 ttl: CacheTtlClass::OneHour,
281 }]
282 );
283 }
284
285 #[test]
286 fn cache_meta_final_block_marker_and_ttl_class_unchanged() {
287 let messages: Vec<crate::SharedMessage> = vec![Arc::new(serde_json::json!({
290 "role": "user",
291 "content": [
292 {"type": "tool_result", "tool_use_id": "t", "content": "ok"},
293 {"type": "tool_result", "tool_use_id": "u", "content": "ok",
294 "cache_control": {"type": "ephemeral"}}
295 ]
296 }))];
297 assert_eq!(
298 message_boundaries(&messages),
299 vec![CacheBoundaryMeta {
300 location: CacheBoundaryLocation::Messages,
301 index: 0,
302 ttl: CacheTtlClass::FiveMinutes,
303 }]
304 );
305 let bare: Vec<crate::SharedMessage> = vec![Arc::new(
307 serde_json::json!({"role": "user", "content": "hi"}),
308 )];
309 assert!(message_boundaries(&bare).is_empty());
310 }
311}