Skip to main content

gproxy_transform/transform/common/
claude_message_controls.rs

1use 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}