Skip to main content

claude_codex/providers/
translate_shared.rs

1use serde_json::Value;
2
3use crate::anthropic::schema::MessagesRequest;
4
5/// Tags wrapping a prior-turn `thinking` block when it is rehydrated as plain text for a
6/// backend that cannot take it in native form (Anthropic rejects a signature-less
7/// `thinking` block; the codex Responses translation has no `thinking` container). Fixed
8/// strings so the rewrite stays byte-stable across turns and keeps the prompt-cache
9/// prefix intact. Shared by the anthropic passthrough and the codex request builder so
10/// reasoning is marked identically in both switch directions.
11pub const REASONING_OPEN: &str = "<previous_reasoning>";
12pub const REASONING_CLOSE: &str = "</previous_reasoning>";
13
14/// Wrap reasoning text in the shared `<previous_reasoning>` tags.
15pub fn wrap_reasoning(reasoning: &str) -> String {
16    format!("{REASONING_OPEN}\n{reasoning}\n{REASONING_CLOSE}")
17}
18
19#[derive(Debug)]
20pub enum ContentBlock {
21    Text {
22        text: String,
23    },
24    Image {
25        source: ImageSource,
26    },
27    ToolUse {
28        id: String,
29        name: String,
30        input: Value,
31    },
32    ToolResult {
33        tool_use_id: String,
34        content: Value,
35        is_error: Option<bool>,
36    },
37    Thinking {
38        thinking: String,
39        signature: Option<String>,
40    },
41}
42
43#[derive(Debug)]
44pub struct ImageSource {
45    pub media_type: String,
46    pub data: String,
47    pub source_type: String,
48}
49
50pub fn flatten_system_text(system_val: Option<&Value>) -> Option<String> {
51    let system = system_val?;
52    let texts: Vec<String> = match system {
53        Value::String(s) => vec![s.clone()],
54        Value::Array(arr) => arr
55            .iter()
56            .filter_map(|b| {
57                let text = b.get("text").and_then(|v| v.as_str())?;
58                if text.starts_with("x-anthropic-billing-header:") {
59                    None
60                } else {
61                    Some(text.to_string())
62                }
63            })
64            .collect(),
65        _ => return None,
66    };
67    if texts.is_empty() {
68        None
69    } else {
70        Some(texts.join("\n\n"))
71    }
72}
73
74pub fn parallel_tool_calls(req: &MessagesRequest) -> Option<bool> {
75    req.extra
76        .get("tool_choice")
77        .and_then(Value::as_object)
78        .and_then(|choice| choice.get("disable_parallel_tool_use"))
79        .and_then(Value::as_bool)
80        .map(|disabled| !disabled)
81}
82
83pub fn read_effort(req: &MessagesRequest) -> Result<Option<&str>, anyhow::Error> {
84    let output_config = match req.extra.get("output_config") {
85        Some(Value::Object(m)) => m,
86        _ => return Ok(None),
87    };
88    match output_config.get("effort") {
89        Some(Value::String(s)) => {
90            let valid = ["low", "medium", "high", "xhigh", "max"];
91            if valid.contains(&s.as_str()) {
92                Ok(Some(s.as_str()))
93            } else {
94                anyhow::bail!("Invalid output_config.effort: {s}")
95            }
96        }
97        _ => Ok(None),
98    }
99}
100
101pub fn normalize_content(content: &Value, missing_tool_input: Value) -> Vec<ContentBlock> {
102    match content {
103        Value::String(s) => {
104            vec![ContentBlock::Text { text: s.clone() }]
105        }
106        Value::Array(arr) => {
107            let mut blocks = Vec::new();
108            for item in arr {
109                if let Some(block) = parse_content_block(item, missing_tool_input.clone()) {
110                    blocks.push(block);
111                }
112            }
113            blocks
114        }
115        _ => Vec::new(),
116    }
117}
118
119pub fn image_source_to_url(source: &ImageSource) -> String {
120    if source.source_type == "url" {
121        source.data.clone()
122    } else {
123        format!("data:{};base64,{}", source.media_type, source.data)
124    }
125}
126
127pub fn image_block_to_url(block: &Value) -> String {
128    let source_type = block
129        .get("source")
130        .and_then(|s| s.get("type"))
131        .and_then(|v| v.as_str())
132        .unwrap_or("base64");
133    if source_type == "url" {
134        block
135            .get("source")
136            .and_then(|s| s.get("url"))
137            .and_then(|v| v.as_str())
138            .unwrap_or("")
139            .to_string()
140    } else {
141        let media_type = block
142            .get("source")
143            .and_then(|s| s.get("media_type"))
144            .and_then(|v| v.as_str())
145            .unwrap_or("image/png");
146        let data = block
147            .get("source")
148            .and_then(|s| s.get("data"))
149            .and_then(|v| v.as_str())
150            .unwrap_or("");
151        format!("data:{media_type};base64,{data}")
152    }
153}
154
155fn parse_content_block(value: &Value, missing_tool_input: Value) -> Option<ContentBlock> {
156    let kind = value.get("type").and_then(|v| v.as_str())?;
157    match kind {
158        "text" => {
159            let text = value
160                .get("text")
161                .and_then(|v| v.as_str())
162                .unwrap_or("")
163                .to_string();
164            Some(ContentBlock::Text { text })
165        }
166        "image" => {
167            let source = value.get("source")?;
168            let media_type = source
169                .get("media_type")
170                .and_then(|v| v.as_str())
171                .unwrap_or("image/png")
172                .to_string();
173            let source_type = source
174                .get("type")
175                .and_then(|v| v.as_str())
176                .unwrap_or("base64")
177                .to_string();
178            let data = if source_type == "url" {
179                source.get("url").and_then(|v| v.as_str()).unwrap_or("")
180            } else {
181                source.get("data").and_then(|v| v.as_str()).unwrap_or("")
182            }
183            .to_string();
184            Some(ContentBlock::Image {
185                source: ImageSource {
186                    media_type,
187                    data,
188                    source_type,
189                },
190            })
191        }
192        "tool_use" => {
193            let id = value
194                .get("id")
195                .and_then(|v| v.as_str())
196                .unwrap_or("")
197                .to_string();
198            let name = value
199                .get("name")
200                .and_then(|v| v.as_str())
201                .unwrap_or("")
202                .to_string();
203            let input = value.get("input").cloned().unwrap_or(missing_tool_input);
204            Some(ContentBlock::ToolUse { id, name, input })
205        }
206        "tool_result" => {
207            let tool_use_id = value
208                .get("tool_use_id")
209                .and_then(|v| v.as_str())
210                .unwrap_or("")
211                .to_string();
212            let content = value
213                .get("content")
214                .cloned()
215                .unwrap_or(Value::String(String::new()));
216            let is_error = value.get("is_error").and_then(|v| v.as_bool());
217            Some(ContentBlock::ToolResult {
218                tool_use_id,
219                content,
220                is_error,
221            })
222        }
223        "thinking" => {
224            let thinking = value
225                .get("thinking")
226                .and_then(|v| v.as_str())
227                .unwrap_or("")
228                .to_string();
229            let signature = value
230                .get("signature")
231                .and_then(|v| v.as_str())
232                .map(str::to_string);
233            Some(ContentBlock::Thinking {
234                thinking,
235                signature,
236            })
237        }
238        _ => None,
239    }
240}