1use serde_json::Value;
17
18use crate::ai::types::{ChatMessage, ChatRequest, ChatRole, ContentPart, ToolChoice, ToolSpec};
19
20pub fn parse_anthropic_request(body: &Value) -> Result<ChatRequest, String> {
32 let obj = body
33 .as_object()
34 .ok_or("request body must be a JSON object")?;
35
36 let model = obj
37 .get("model")
38 .and_then(|v| v.as_str())
39 .ok_or("missing 'model' field")?
40 .to_string();
41
42 let mut messages = Vec::new();
44 if let Some(system) = obj.get("system") {
45 let system_text = match system {
46 Value::String(s) => s.clone(),
47 Value::Array(parts) => parts
48 .iter()
49 .filter_map(|p| p.get("text").and_then(|t| t.as_str()))
50 .collect::<Vec<_>>()
51 .join("\n"),
52 _ => String::new(),
53 };
54 if !system_text.is_empty() {
55 messages.push(ChatMessage::text(ChatRole::System, system_text));
56 }
57 }
58
59 let raw_messages = obj
61 .get("messages")
62 .and_then(|v| v.as_array())
63 .ok_or("missing 'messages' field")?;
64 for msg in raw_messages {
65 let role_str = msg
66 .get("role")
67 .and_then(|v| v.as_str())
68 .ok_or("message missing 'role'")?;
69 let role = match role_str {
70 "user" => ChatRole::User,
71 "assistant" => ChatRole::Assistant,
72 other => return Err(format!("unknown role '{other}'")),
73 };
74 let content = parse_anthropic_content(msg.get("content"))?;
75 let tool_calls = parse_anthropic_tool_calls(msg.get("content"));
76 messages.push(ChatMessage {
77 role,
78 content,
79 name: None,
80 tool_calls,
81 tool_call_id: None,
82 });
83 }
84
85 if messages.is_empty() {
86 return Err("messages must be non-empty".to_string());
87 }
88
89 let tools = obj
91 .get("tools")
92 .and_then(|v| v.as_array())
93 .map(|tools| {
94 tools
95 .iter()
96 .filter_map(|t| {
97 let name = t.get("name")?.as_str()?.to_string();
98 let description = t
99 .get("description")
100 .and_then(|d| d.as_str())
101 .map(String::from);
102 let parameters = t.get("input_schema").cloned();
103 Some(ToolSpec {
104 name,
105 description,
106 parameters,
107 })
108 })
109 .collect::<Vec<_>>()
110 })
111 .unwrap_or_default();
112
113 let tool_choice = obj.get("tool_choice").and_then(parse_anthropic_tool_choice);
115
116 let temperature = obj.get("temperature").and_then(|v| v.as_f64());
118 let top_p = obj.get("top_p").and_then(|v| v.as_f64());
119 let max_tokens = obj.get("max_tokens").and_then(|v| v.as_u64());
120 let stop = obj
121 .get("stop_sequences")
122 .and_then(|v| v.as_array())
123 .map(|a| {
124 a.iter()
125 .filter_map(|v| v.as_str().map(String::from))
126 .collect()
127 });
128 let stream = obj.get("stream").and_then(|v| v.as_bool()).unwrap_or(false);
129
130 let known = [
132 "model",
133 "messages",
134 "system",
135 "max_tokens",
136 "temperature",
137 "top_p",
138 "stop_sequences",
139 "stream",
140 "tools",
141 "tool_choice",
142 ];
143 let other = obj
144 .iter()
145 .filter(|(k, _)| !known.contains(&k.as_str()))
146 .map(|(k, v)| (k.clone(), v.clone()))
147 .collect();
148
149 Ok(ChatRequest {
150 model,
151 messages,
152 tools,
153 tool_choice,
154 temperature,
155 top_p,
156 max_tokens,
157 stop,
158 stream,
159 stream_options_include_usage: false,
160 response_format: None,
161 prompt: None,
162 prompt_variables: Default::default(),
163 other,
164 })
165}
166
167fn parse_anthropic_content(content: Option<&Value>) -> Result<Vec<ContentPart>, String> {
169 match content {
170 None | Some(Value::Null) => Ok(Vec::new()),
171 Some(Value::String(s)) => Ok(vec![ContentPart::Text { text: s.clone() }]),
172 Some(Value::Array(parts)) => {
173 let mut result = Vec::new();
174 for part in parts {
175 let part_type = part.get("type").and_then(|t| t.as_str()).unwrap_or("text");
176 match part_type {
177 "text" => {
178 let text = part.get("text").and_then(|t| t.as_str()).unwrap_or("");
179 result.push(ContentPart::Text {
180 text: text.to_string(),
181 });
182 }
183 "image" => {
184 if let Some(source) = part.get("source") {
185 let media_type = source
186 .get("media_type")
187 .and_then(|m| m.as_str())
188 .map(String::from);
189 let data_b64 = source
190 .get("data")
191 .and_then(|d| d.as_str())
192 .map(String::from);
193 let url = source.get("url").and_then(|u| u.as_str()).map(String::from);
194 result.push(ContentPart::Image {
195 url,
196 media_type,
197 data_b64,
198 });
199 }
200 }
201 "tool_use" => {
202 }
204 "tool_result" => {
205 if let Some(content) = part.get("content") {
207 let text = match content {
208 Value::String(s) => s.clone(),
209 Value::Array(parts) => parts
210 .iter()
211 .filter_map(|p| p.get("text").and_then(|t| t.as_str()))
212 .collect::<Vec<_>>()
213 .join(""),
214 _ => String::new(),
215 };
216 result.push(ContentPart::Text { text });
217 }
218 }
219 _ => {}
220 }
221 }
222 Ok(result)
223 }
224 _ => Err("content must be a string or array".to_string()),
225 }
226}
227
228fn parse_anthropic_tool_calls(content: Option<&Value>) -> Vec<crate::ai::types::ToolCall> {
230 let mut calls = Vec::new();
231 if let Some(Value::Array(parts)) = content {
232 for part in parts {
233 if part.get("type").and_then(|t| t.as_str()) == Some("tool_use") {
234 if let (Some(id), Some(name), Some(input)) = (
235 part.get("id").and_then(|i| i.as_str()),
236 part.get("name").and_then(|n| n.as_str()),
237 part.get("input"),
238 ) {
239 let arguments = serde_json::to_string(input).unwrap_or_default();
240 calls.push(crate::ai::types::ToolCall {
241 id: id.to_string(),
242 name: name.to_string(),
243 arguments,
244 });
245 }
246 }
247 }
248 }
249 calls
250}
251
252fn parse_anthropic_tool_choice(v: &Value) -> Option<ToolChoice> {
254 let tc_type = v.get("type").and_then(|t| t.as_str())?;
255 match tc_type {
256 "auto" => Some(ToolChoice::Auto),
257 "any" => Some(ToolChoice::Any),
258 "tool" => v
259 .get("name")
260 .and_then(|n| n.as_str())
261 .map(|name| ToolChoice::Tool(name.to_string())),
262 _ => None,
263 }
264}
265
266pub fn parse_gemini_request(body: &Value) -> Result<ChatRequest, String> {
274 let obj = body
275 .as_object()
276 .ok_or("request body must be a JSON object")?;
277
278 let mut messages = Vec::new();
280 if let Some(sys) = obj.get("systemInstruction") {
281 let system_text = sys
282 .get("parts")
283 .and_then(|p| p.as_array())
284 .map(|parts| {
285 parts
286 .iter()
287 .filter_map(|p| p.get("text").and_then(|t| t.as_str()))
288 .collect::<Vec<_>>()
289 .join("\n")
290 })
291 .unwrap_or_default();
292 if !system_text.is_empty() {
293 messages.push(ChatMessage::text(ChatRole::System, system_text));
294 }
295 }
296
297 let contents = obj
299 .get("contents")
300 .and_then(|v| v.as_array())
301 .ok_or("missing 'contents' field")?;
302 for msg in contents {
303 let role_str = msg.get("role").and_then(|v| v.as_str()).unwrap_or("user");
304 let role = match role_str {
305 "user" => ChatRole::User,
306 "model" => ChatRole::Assistant,
307 "function" => ChatRole::Tool,
308 other => return Err(format!("unknown role '{other}'")),
309 };
310 let content = parse_gemini_parts(msg.get("parts"));
311 let tool_calls = parse_gemini_function_calls(msg.get("parts"));
312 messages.push(ChatMessage {
313 role,
314 content,
315 name: None,
316 tool_calls,
317 tool_call_id: None,
318 });
319 }
320
321 if messages.is_empty() {
322 return Err("contents must be non-empty".to_string());
323 }
324
325 let tools = obj
327 .get("tools")
328 .and_then(|v| v.as_array())
329 .and_then(|a| a.first())
330 .and_then(|t| t.get("functionDeclarations"))
331 .and_then(|f| f.as_array())
332 .map(|decls| {
333 decls
334 .iter()
335 .filter_map(|d| {
336 let name = d.get("name")?.as_str()?.to_string();
337 let description = d
338 .get("description")
339 .and_then(|d| d.as_str())
340 .map(String::from);
341 let parameters = d.get("parameters").cloned();
342 Some(ToolSpec {
343 name,
344 description,
345 parameters,
346 })
347 })
348 .collect::<Vec<_>>()
349 })
350 .unwrap_or_default();
351
352 let tool_choice = obj
354 .get("toolConfig")
355 .and_then(|tc| tc.get("mode"))
356 .and_then(|m| m.as_str())
357 .and_then(|mode| match mode {
358 "AUTO" => Some(ToolChoice::Auto),
359 "NONE" => Some(ToolChoice::None),
360 "ANY" => Some(ToolChoice::Any),
361 _ => None,
362 });
363
364 let gen_cfg = obj.get("generationConfig");
366 let temperature = gen_cfg
367 .and_then(|g| g.get("temperature"))
368 .and_then(|v| v.as_f64());
369 let top_p = gen_cfg.and_then(|g| g.get("topP")).and_then(|v| v.as_f64());
370 let max_tokens = gen_cfg
371 .and_then(|g| g.get("maxOutputTokens"))
372 .and_then(|v| v.as_u64());
373 let stop = gen_cfg
374 .and_then(|g| g.get("stopSequences"))
375 .and_then(|v| v.as_array())
376 .map(|a| {
377 a.iter()
378 .filter_map(|v| v.as_str().map(String::from))
379 .collect()
380 });
381
382 let known = [
384 "contents",
385 "systemInstruction",
386 "tools",
387 "toolConfig",
388 "generationConfig",
389 ];
390 let other = obj
391 .iter()
392 .filter(|(k, _)| !known.contains(&k.as_str()))
393 .map(|(k, v)| (k.clone(), v.clone()))
394 .collect();
395
396 Ok(ChatRequest {
397 model: String::new(), messages,
399 tools,
400 tool_choice,
401 temperature,
402 top_p,
403 max_tokens,
404 stop,
405 stream: false,
406 stream_options_include_usage: false,
407 response_format: None,
408 prompt: None,
409 prompt_variables: Default::default(),
410 other,
411 })
412}
413
414fn parse_gemini_parts(parts: Option<&Value>) -> Vec<ContentPart> {
416 let mut result = Vec::new();
417 if let Some(Value::Array(parts)) = parts {
418 for part in parts {
419 if let Some(text) = part.get("text").and_then(|t| t.as_str()) {
420 result.push(ContentPart::Text {
421 text: text.to_string(),
422 });
423 } else if let Some(inline) = part.get("inlineData").or_else(|| part.get("inline_data"))
424 {
425 let media_type = inline
426 .get("mimeType")
427 .or_else(|| inline.get("mime_type"))
428 .and_then(|m| m.as_str())
429 .map(String::from);
430 let data_b64 = inline
431 .get("data")
432 .and_then(|d| d.as_str())
433 .map(String::from);
434 result.push(ContentPart::Image {
435 url: None,
436 media_type,
437 data_b64,
438 });
439 } else if let Some(file) = part.get("fileData").or_else(|| part.get("file_data")) {
440 let url = file
441 .get("fileUri")
442 .or_else(|| file.get("file_uri"))
443 .and_then(|u| u.as_str())
444 .map(String::from);
445 let media_type = file
446 .get("mimeType")
447 .or_else(|| file.get("mime_type"))
448 .and_then(|m| m.as_str())
449 .map(String::from);
450 result.push(ContentPart::Image {
451 url,
452 media_type,
453 data_b64: None,
454 });
455 }
456 }
457 }
458 result
459}
460
461fn parse_gemini_function_calls(parts: Option<&Value>) -> Vec<crate::ai::types::ToolCall> {
463 let mut calls = Vec::new();
464 if let Some(Value::Array(parts)) = parts {
465 for (i, part) in parts.iter().enumerate() {
466 if let Some(fc) = part.get("functionCall") {
467 let name = fc.get("name").and_then(|n| n.as_str()).unwrap_or("unknown");
468 let args = fc.get("args").cloned().unwrap_or(Value::Null);
469 let arguments = serde_json::to_string(&args).unwrap_or_default();
470 calls.push(crate::ai::types::ToolCall {
471 id: format!("call-{i}-{name}"),
472 name: name.to_string(),
473 arguments,
474 });
475 }
476 }
477 }
478 calls
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484 use serde_json::json;
485
486 #[test]
487 fn parse_anthropic_simple_request() {
488 let body = json!({
489 "model": "claude-3-5-sonnet-20241022",
490 "max_tokens": 1024,
491 "messages": [
492 {"role": "user", "content": "Hello, world!"}
493 ]
494 });
495 let req = parse_anthropic_request(&body).unwrap();
496 assert_eq!(req.model, "claude-3-5-sonnet-20241022");
497 assert_eq!(req.messages.len(), 1);
498 assert_eq!(req.max_tokens, Some(1024));
499 }
500
501 #[test]
502 fn parse_anthropic_with_system() {
503 let body = json!({
504 "model": "claude-3-5-sonnet",
505 "max_tokens": 1024,
506 "system": "You are a helpful assistant.",
507 "messages": [
508 {"role": "user", "content": "Hi"}
509 ]
510 });
511 let req = parse_anthropic_request(&body).unwrap();
512 assert_eq!(req.messages.len(), 2);
513 assert_eq!(req.messages[0].role, ChatRole::System);
514 }
515
516 #[test]
517 fn parse_anthropic_with_tools() {
518 let body = json!({
519 "model": "claude-3-5-sonnet",
520 "max_tokens": 1024,
521 "messages": [{"role": "user", "content": "What's the weather?"}],
522 "tools": [{
523 "name": "get_weather",
524 "description": "Get weather",
525 "input_schema": {"type": "object", "properties": {}}
526 }],
527 "tool_choice": {"type": "any"}
528 });
529 let req = parse_anthropic_request(&body).unwrap();
530 assert_eq!(req.tools.len(), 1);
531 assert_eq!(req.tools[0].name, "get_weather");
532 assert_eq!(req.tool_choice, Some(ToolChoice::Any));
533 }
534
535 #[test]
536 fn parse_gemini_simple_request() {
537 let body = json!({
538 "contents": [
539 {"role": "user", "parts": [{"text": "Hello, world!"}]}
540 ]
541 });
542 let req = parse_gemini_request(&body).unwrap();
543 assert_eq!(req.messages.len(), 1);
544 assert_eq!(req.messages[0].role, ChatRole::User);
545 }
546
547 #[test]
548 fn parse_gemini_with_system_instruction() {
549 let body = json!({
550 "systemInstruction": {"parts": [{"text": "You are helpful."}]},
551 "contents": [
552 {"role": "user", "parts": [{"text": "Hi"}]}
553 ]
554 });
555 let req = parse_gemini_request(&body).unwrap();
556 assert_eq!(req.messages.len(), 2);
557 assert_eq!(req.messages[0].role, ChatRole::System);
558 }
559
560 #[test]
561 fn parse_gemini_with_tools() {
562 let body = json!({
563 "contents": [{"role": "user", "parts": [{"text": "Weather?"}]}],
564 "tools": [{
565 "functionDeclarations": [{
566 "name": "get_weather",
567 "description": "Get weather",
568 "parameters": {"type": "object"}
569 }]
570 }],
571 "toolConfig": {"mode": "ANY"},
572 "generationConfig": {"temperature": 0.7, "maxOutputTokens": 1024}
573 });
574 let req = parse_gemini_request(&body).unwrap();
575 assert_eq!(req.tools.len(), 1);
576 assert_eq!(req.tools[0].name, "get_weather");
577 assert_eq!(req.tool_choice, Some(ToolChoice::Any));
578 assert_eq!(req.temperature, Some(0.7));
579 assert_eq!(req.max_tokens, Some(1024));
580 }
581}