claude_codex/providers/
translate_shared.rs1use serde_json::Value;
2
3use crate::anthropic::schema::MessagesRequest;
4
5pub const REASONING_OPEN: &str = "<previous_reasoning>";
12pub const REASONING_CLOSE: &str = "</previous_reasoning>";
13
14pub 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}