gproxy_transform/transform/common/
claude_message_controls.rs1use crate::protocol::claude;
2
3pub fn preserve_claude_input_transformations(
4 extra: &mut claude::JsonObject,
5 transformations: Option<Vec<claude::InputTransformation>>,
6) {
7 if let Some(transformations) = transformations {
8 extra.insert(
9 "input_transformations".into(),
10 serde_json::to_value(transformations)
11 .expect("Claude input transformations must serialize"),
12 );
13 }
14}
15
16pub fn apply_claude_message_controls(
17 messages: &mut Vec<claude::MessageParam>,
18 output_config: &mut Option<claude::OutputConfig>,
19) {
20 if let Some(last_user) = messages.iter().rposition(is_user)
21 && let Some(effort) = messages[..last_user]
22 .iter()
23 .rev()
24 .filter(is_system)
25 .find_map(|message| {
26 message
27 .output_config
28 .as_ref()
29 .and_then(|config| config.effort.clone())
30 })
31 {
32 output_config
33 .get_or_insert_with(|| {
34 crate::protocol::wire!(claude::OutputConfig {
35 effort: None,
36 format: None,
37 task_budget: None,
38 extra: Default::default(),
39 })
40 })
41 .effort = Some(effort);
42 }
43
44 let mut later_user = vec![false; messages.len()];
45 let mut seen_user = false;
46 for (index, message) in messages.iter().enumerate().rev() {
47 later_user[index] = seen_user;
48 seen_user |= is_user(message);
49 }
50 let mut index = 0;
51 messages.retain(|message| {
52 let cleared = is_system(&message)
53 && later_user[index]
54 && matches!(
55 message.clear_at,
56 Some(claude::MessageClearAt::Known(
57 claude::MessageClearAtKnown::NextUserMessage
58 ))
59 );
60 let effort_only = is_system(&message)
61 && message.output_config.is_some()
62 && matches!(&message.content, claude::StringOrArray::Array(blocks) if blocks.is_empty());
63 index += 1;
64 !cleared && !effort_only
65 });
66}
67
68fn is_user(message: &claude::MessageParam) -> bool {
69 matches!(
70 message.role,
71 claude::MessageRole::Known(claude::MessageRoleKnown::User)
72 )
73}
74
75fn is_system(message: &&claude::MessageParam) -> bool {
76 matches!(
77 message.role,
78 claude::MessageRole::Known(claude::MessageRoleKnown::System)
79 )
80}
81
82#[cfg(test)]
83mod tests {
84 use serde_json::json;
85
86 use super::*;
87
88 #[test]
89 fn renders_turn_scoped_messages_and_applies_effort_for_current_user() {
90 let mut request: claude::CreateMessageRequestBody = serde_json::from_value(json!({
91 "model": "claude-fable-5-1",
92 "max_tokens": 1024,
93 "output_config": {"effort": "high"},
94 "messages": [
95 {"role": "user", "content": "first"},
96 {"role": "system", "clear_at": "next_user_message", "content": "old"},
97 {"role": "assistant", "content": "done"},
98 {"role": "system", "content": [], "output_config": {"effort": "low"}},
99 {"role": "user", "content": "current"},
100 {"role": "system", "clear_at": "next_user_message", "content": "live"}
101 ]
102 }))
103 .unwrap();
104
105 apply_claude_message_controls(&mut request.messages, &mut request.output_config);
106
107 let wire = serde_json::to_value(request).unwrap();
108 assert_eq!(wire["output_config"]["effort"], "low");
109 assert_eq!(wire["messages"].as_array().unwrap().len(), 4);
110 assert!(
111 wire["messages"].as_array().unwrap().iter().all(|message| {
112 message["content"] != "old" && message["output_config"].is_null()
113 })
114 );
115 assert_eq!(wire["messages"][3]["content"], "live");
116 }
117}