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    },
40}
41
42#[derive(Debug)]
43pub struct ImageSource {
44    pub media_type: String,
45    pub data: String,
46    pub source_type: String,
47}
48
49pub fn flatten_system_text(system_val: Option<&Value>) -> Option<String> {
50    let system = system_val?;
51    let texts: Vec<String> = match system {
52        Value::String(s) => vec![s.clone()],
53        Value::Array(arr) => arr
54            .iter()
55            .filter_map(|b| {
56                let text = b.get("text").and_then(|v| v.as_str())?;
57                if text.starts_with("x-anthropic-billing-header:") {
58                    None
59                } else {
60                    Some(text.to_string())
61                }
62            })
63            .collect(),
64        _ => return None,
65    };
66    if texts.is_empty() {
67        None
68    } else {
69        Some(texts.join("\n\n"))
70    }
71}
72
73pub fn read_effort(req: &MessagesRequest) -> Result<Option<&str>, anyhow::Error> {
74    let output_config = match req.extra.get("output_config") {
75        Some(Value::Object(m)) => m,
76        _ => return Ok(None),
77    };
78    match output_config.get("effort") {
79        Some(Value::String(s)) => {
80            let valid = ["low", "medium", "high", "xhigh", "max"];
81            if valid.contains(&s.as_str()) {
82                Ok(Some(s.as_str()))
83            } else {
84                anyhow::bail!("Invalid output_config.effort: {s}")
85            }
86        }
87        _ => Ok(None),
88    }
89}
90
91pub fn normalize_content(content: &Value, missing_tool_input: Value) -> Vec<ContentBlock> {
92    match content {
93        Value::String(s) => {
94            vec![ContentBlock::Text { text: s.clone() }]
95        }
96        Value::Array(arr) => {
97            let mut blocks = Vec::new();
98            for item in arr {
99                if let Some(block) = parse_content_block(item, missing_tool_input.clone()) {
100                    blocks.push(block);
101                }
102            }
103            blocks
104        }
105        _ => Vec::new(),
106    }
107}
108
109pub fn image_source_to_url(source: &ImageSource) -> String {
110    if source.source_type == "url" {
111        source.data.clone()
112    } else {
113        format!("data:{};base64,{}", source.media_type, source.data)
114    }
115}
116
117pub fn image_block_to_url(block: &Value) -> String {
118    let source_type = block
119        .get("source")
120        .and_then(|s| s.get("type"))
121        .and_then(|v| v.as_str())
122        .unwrap_or("base64");
123    if source_type == "url" {
124        block
125            .get("source")
126            .and_then(|s| s.get("url"))
127            .and_then(|v| v.as_str())
128            .unwrap_or("")
129            .to_string()
130    } else {
131        let media_type = block
132            .get("source")
133            .and_then(|s| s.get("media_type"))
134            .and_then(|v| v.as_str())
135            .unwrap_or("image/png");
136        let data = block
137            .get("source")
138            .and_then(|s| s.get("data"))
139            .and_then(|v| v.as_str())
140            .unwrap_or("");
141        format!("data:{media_type};base64,{data}")
142    }
143}
144
145fn parse_content_block(value: &Value, missing_tool_input: Value) -> Option<ContentBlock> {
146    let kind = value.get("type").and_then(|v| v.as_str())?;
147    match kind {
148        "text" => {
149            let text = value
150                .get("text")
151                .and_then(|v| v.as_str())
152                .unwrap_or("")
153                .to_string();
154            Some(ContentBlock::Text { text })
155        }
156        "image" => {
157            let source = value.get("source")?;
158            let media_type = source
159                .get("media_type")
160                .and_then(|v| v.as_str())
161                .unwrap_or("image/png")
162                .to_string();
163            let source_type = source
164                .get("type")
165                .and_then(|v| v.as_str())
166                .unwrap_or("base64")
167                .to_string();
168            let data = if source_type == "url" {
169                source.get("url").and_then(|v| v.as_str()).unwrap_or("")
170            } else {
171                source.get("data").and_then(|v| v.as_str()).unwrap_or("")
172            }
173            .to_string();
174            Some(ContentBlock::Image {
175                source: ImageSource {
176                    media_type,
177                    data,
178                    source_type,
179                },
180            })
181        }
182        "tool_use" => {
183            let id = value
184                .get("id")
185                .and_then(|v| v.as_str())
186                .unwrap_or("")
187                .to_string();
188            let name = value
189                .get("name")
190                .and_then(|v| v.as_str())
191                .unwrap_or("")
192                .to_string();
193            let input = value.get("input").cloned().unwrap_or(missing_tool_input);
194            Some(ContentBlock::ToolUse { id, name, input })
195        }
196        "tool_result" => {
197            let tool_use_id = value
198                .get("tool_use_id")
199                .and_then(|v| v.as_str())
200                .unwrap_or("")
201                .to_string();
202            let content = value
203                .get("content")
204                .cloned()
205                .unwrap_or(Value::String(String::new()));
206            let is_error = value.get("is_error").and_then(|v| v.as_bool());
207            Some(ContentBlock::ToolResult {
208                tool_use_id,
209                content,
210                is_error,
211            })
212        }
213        "thinking" => {
214            let thinking = value
215                .get("thinking")
216                .and_then(|v| v.as_str())
217                .unwrap_or("")
218                .to_string();
219            Some(ContentBlock::Thinking { thinking })
220        }
221        _ => None,
222    }
223}