Skip to main content

gproxy_protocol/adapt/generate/
claude_compat.rs

1//! Upgrade legacy native Messages/count-tokens requests before upstream dispatch.
2//! Work on the original JSON so provider extensions and signed blocks survive;
3//! only fields rejected by API 2026-10-21 are changed.
4
5use crate::{Dialect, HttpBody, Operation, OperationKey, WireRequest, connection::Bytes};
6use serde_json::Value;
7
8pub fn applies_to(operation: OperationKey) -> bool {
9    operation.dialect == Dialect::Claude
10        && matches!(
11            operation.operation,
12            Operation::GenerateContent | Operation::StreamGenerateContent | Operation::CountTokens
13        )
14}
15
16/// Requires a buffered body. Unchanged requests retain their exact bytes.
17/// Fallback credits bind the original payload and must not be rewritten.
18pub fn prepare(operation: OperationKey, request: &mut WireRequest<HttpBody>) {
19    if !applies_to(operation) {
20        return;
21    }
22    let HttpBody::Bytes(bytes) = &request.body else {
23        return;
24    };
25    let Ok(mut body) = serde_json::from_slice::<Value>(bytes) else {
26        return;
27    };
28    let Some(root) = body.as_object_mut() else {
29        return;
30    };
31    if root
32        .get("fallback_credit_token")
33        .is_some_and(|value| !value.is_null())
34    {
35        return;
36    }
37    let mut changed = false;
38    for field in ["temperature", "top_p", "top_k"] {
39        changed |= root.remove(field).is_some();
40    }
41    if let Some(messages) = root.get_mut("messages").and_then(Value::as_array_mut) {
42        let mut output = Vec::with_capacity(messages.len());
43        for mut message in messages.drain(..) {
44            let role = message.get("role").and_then(Value::as_str);
45            let user = role == Some("user");
46            let assistant = role == Some("assistant");
47            if let Some(blocks) = message.get_mut("content").and_then(Value::as_array_mut) {
48                if user {
49                    for block in blocks.iter_mut() {
50                        changed |= user_block(block);
51                    }
52                }
53                if assistant && blocks.iter().any(is_document) {
54                    let blocks = std::mem::take(blocks);
55                    for mut block in blocks {
56                        let mut part = message.clone();
57                        if is_document(&block) {
58                            user_block(&mut block);
59                            part["role"] = Value::String("user".into());
60                        }
61                        part["content"] = Value::Array(vec![block]);
62                        output.push(part);
63                    }
64                    changed = true;
65                    continue;
66                }
67            }
68            output.push(message);
69        }
70        *messages = output;
71    }
72    if changed && let Ok(bytes) = serde_json::to_vec(&body) {
73        request.headers.remove(http::header::CONTENT_LENGTH);
74        request.body = HttpBody::Bytes(Bytes::from(bytes));
75    }
76}
77
78fn is_document(block: &Value) -> bool {
79    block.get("type").and_then(Value::as_str) == Some("document")
80}
81
82fn user_block(block: &mut Value) -> bool {
83    match block.get("type").and_then(Value::as_str) {
84        Some("text") => {
85            if block
86                .get("citations")
87                .and_then(Value::as_array)
88                .is_some_and(|values| !values.is_empty())
89            {
90                block
91                    .as_object_mut()
92                    .expect("text block is an object")
93                    .remove("citations");
94                true
95            } else {
96                false
97            }
98        }
99        Some("search_result" | "tool_result" | "mcp_tool_result") => {
100            let mut changed = false;
101            if let Some(blocks) = block.get_mut("content").and_then(Value::as_array_mut) {
102                for block in blocks {
103                    changed |= user_block(block);
104                }
105            }
106            changed
107        }
108        Some("document") => {
109            let mut changed = false;
110            if block.pointer("/source/type").and_then(Value::as_str) == Some("content")
111                && let Some(blocks) = block
112                    .pointer_mut("/source/content")
113                    .and_then(Value::as_array_mut)
114            {
115                for block in blocks {
116                    changed |= user_block(block);
117                }
118            }
119            changed
120        }
121        Some("web_fetch_tool_result" | "web_fetch_result") => {
122            block.get_mut("content").is_some_and(user_block)
123        }
124        _ => false,
125    }
126}