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