1use serde_json::{json, Map, Value};
2
3fn put(o: &mut Map<String, Value>, k: &str, v: &Value) {
4 if !v.is_null() {
5 o.insert(k.to_string(), v.clone());
6 }
7}
8
9pub(crate) fn txt(c: &Value) -> String {
10 match c {
11 Value::String(s) => s.clone(),
12 Value::Array(parts) => parts
13 .iter()
14 .filter_map(|p| p["text"].as_str())
15 .collect::<Vec<_>>()
16 .join("\n"),
17 _ => String::new(),
18 }
19}
20
21fn parse_args(s: &str) -> Value {
22 serde_json::from_str(s).unwrap_or_else(|_| json!({}))
23}
24
25pub fn openai_chat_to_anthropic(req: &Value) -> Value {
26 let mut sys = Vec::new();
27 let mut msgs = Vec::new();
28 for m in req["messages"].as_array().into_iter().flatten() {
29 match m["role"].as_str().unwrap_or("") {
30 "system" => sys.push(txt(&m["content"])),
31 "user" => msgs.push(json!({"role": "user", "content": txt(&m["content"])})),
32 "assistant" => {
33 let mut blocks = Vec::new();
34 let t = txt(&m["content"]);
35 if !t.is_empty() {
36 blocks.push(json!({"type": "text", "text": t}));
37 }
38 for tc in m["tool_calls"].as_array().into_iter().flatten() {
39 blocks.push(json!({
40 "type": "tool_use",
41 "id": tc["id"],
42 "name": tc["function"]["name"],
43 "input": parse_args(tc["function"]["arguments"].as_str().unwrap_or("{}")),
44 }));
45 }
46 msgs.push(json!({"role": "assistant", "content": blocks}));
47 }
48 "tool" => msgs.push(json!({
49 "role": "user",
50 "content": [{
51 "type": "tool_result",
52 "tool_use_id": m["tool_call_id"],
53 "content": [{"type": "text", "text": txt(&m["content"])}],
54 }],
55 })),
56 _ => {}
57 }
58 }
59 let mut o = Map::new();
60 put(&mut o, "model", &req["model"]);
61 if !sys.is_empty() {
62 o.insert("system".to_string(), Value::String(sys.join("\n\n")));
63 }
64 o.insert("messages".to_string(), Value::Array(msgs));
65 if let Some(ts) = req["tools"].as_array() {
66 let tools: Vec<Value> = ts
67 .iter()
68 .filter(|t| t["function"].is_object())
69 .map(|t| {
70 let f = &t["function"];
71 let mut tool = Map::new();
72 put(&mut tool, "name", &f["name"]);
73 put(&mut tool, "description", &f["description"]);
74 put(&mut tool, "input_schema", &f["parameters"]);
75 Value::Object(tool)
76 })
77 .collect();
78 if !tools.is_empty() {
79 o.insert("tools".to_string(), Value::Array(tools));
80 }
81 }
82 match &req["tool_choice"] {
83 Value::String(s) if s == "auto" => {
84 o.insert("tool_choice".to_string(), json!({"type": "auto"}));
85 }
86 v if v["type"] == "function" => {
87 o.insert(
88 "tool_choice".to_string(),
89 json!({"type": "tool", "name": v["function"]["name"]}),
90 );
91 }
92 _ => {}
93 }
94 let max = req["max_tokens"]
95 .as_i64()
96 .or_else(|| req["max_completion_tokens"].as_i64())
97 .unwrap_or(8192);
98 o.insert("max_tokens".to_string(), json!(max));
99 put(&mut o, "temperature", &req["temperature"]);
100 put(&mut o, "top_p", &req["top_p"]);
101 match &req["stop"] {
102 Value::String(s) => {
103 o.insert("stop_sequences".to_string(), json!([s]));
104 }
105 Value::Array(a) => {
106 o.insert("stop_sequences".to_string(), Value::Array(a.clone()));
107 }
108 _ => {}
109 }
110 put(&mut o, "stream", &req["stream"]);
111 Value::Object(o)
112}
113
114pub fn openai_responses_to_anthropic(req: &Value) -> Value {
115 let mut msgs = Vec::new();
116 match &req["input"] {
117 Value::String(s) => msgs.push(json!({"role": "user", "content": s})),
118 Value::Array(items) => {
119 for it in items {
120 match it["type"].as_str().unwrap_or("message") {
121 "message" => {
122 let role = if it["role"] == "assistant" { "assistant" } else { "user" };
123 msgs.push(json!({"role": role, "content": txt(&it["content"])}));
124 }
125 "function_call" => msgs.push(json!({
126 "role": "assistant",
127 "content": [{
128 "type": "tool_use",
129 "id": it["call_id"],
130 "name": it["name"],
131 "input": parse_args(it["arguments"].as_str().unwrap_or("{}")),
132 }],
133 })),
134 "function_call_output" => msgs.push(json!({
135 "role": "user",
136 "content": [{
137 "type": "tool_result",
138 "tool_use_id": it["call_id"],
139 "content": [{"type": "text", "text": txt(&it["output"])}],
140 }],
141 })),
142 _ => {}
143 }
144 }
145 }
146 _ => {}
147 }
148 let mut o = Map::new();
149 put(&mut o, "model", &req["model"]);
150 put(&mut o, "system", &req["instructions"]);
151 o.insert("messages".to_string(), Value::Array(msgs));
152 if let Some(ts) = req["tools"].as_array() {
153 let tools: Vec<Value> = ts
154 .iter()
155 .filter(|t| t["type"] == "function")
156 .map(|t| {
157 let mut tool = Map::new();
158 put(&mut tool, "name", &t["name"]);
159 put(&mut tool, "description", &t["description"]);
160 put(&mut tool, "input_schema", &t["parameters"]);
161 Value::Object(tool)
162 })
163 .collect();
164 if !tools.is_empty() {
165 o.insert("tools".to_string(), Value::Array(tools));
166 }
167 }
168 o.insert(
169 "max_tokens".to_string(),
170 json!(req["max_output_tokens"].as_i64().unwrap_or(8192)),
171 );
172 put(&mut o, "temperature", &req["temperature"]);
173 put(&mut o, "top_p", &req["top_p"]);
174 put(&mut o, "stream", &req["stream"]);
175 Value::Object(o)
176}
177
178pub fn anthropic_to_openai_responses(req: &Value) -> Value {
179 let mut input = Vec::new();
180 let mut sys_extra: Vec<String> = Vec::new();
181 for m in req["messages"].as_array().into_iter().flatten() {
182 let role = m["role"].as_str().unwrap_or("user");
183 if role == "system" || role == "developer" {
184 let text = txt(&m["content"]);
185 if !text.is_empty() {
186 sys_extra.push(text);
187 }
188 continue;
189 }
190 let part = if role == "assistant" { "output_text" } else { "input_text" };
191 match &m["content"] {
192 Value::String(s) => input.push(json!({
193 "type": "message",
194 "role": role,
195 "content": [{"type": part, "text": s}],
196 })),
197 Value::Array(blocks) => {
198 let mut parts = Vec::new();
199 let mut items = Vec::new();
200 for b in blocks {
201 match b["type"].as_str() {
202 Some("text") => parts.push(json!({"type": part, "text": b["text"]})),
203 Some("tool_use") => items.push(json!({
204 "type": "function_call",
205 "call_id": b["id"],
206 "name": b["name"],
207 "arguments": b["input"].to_string(),
208 })),
209 Some("tool_result") => items.push(json!({
210 "type": "function_call_output",
211 "call_id": b["tool_use_id"],
212 "output": txt(&b["content"]),
213 })),
214 _ => {}
215 }
216 }
217 if !parts.is_empty() {
218 input.push(json!({"type": "message", "role": role, "content": parts}));
219 }
220 input.extend(items);
221 }
222 _ => {}
223 }
224 }
225 let mut o = Map::new();
226 put(&mut o, "model", &req["model"]);
227 let mut instructions = match &req["system"] {
228 Value::String(s) => s.clone(),
229 Value::Array(_) => txt(&req["system"]),
230 _ => String::new(),
231 };
232 for extra in sys_extra {
233 if !instructions.is_empty() {
234 instructions.push_str("\n\n");
235 }
236 instructions.push_str(&extra);
237 }
238 if !instructions.is_empty() {
239 o.insert("instructions".to_string(), Value::String(instructions));
240 }
241 o.insert("input".to_string(), Value::Array(input));
242 if let Some(ts) = req["tools"].as_array() {
243 let tools: Vec<Value> = ts
244 .iter()
245 .map(|t| {
246 let mut tool = Map::new();
247 tool.insert("type".to_string(), json!("function"));
248 put(&mut tool, "name", &t["name"]);
249 put(&mut tool, "description", &t["description"]);
250 put(&mut tool, "parameters", &t["input_schema"]);
251 tool.insert("strict".to_string(), json!(false));
252 Value::Object(tool)
253 })
254 .collect();
255 if !tools.is_empty() {
256 o.insert("tools".to_string(), Value::Array(tools));
257 }
258 }
259 if let Some(mt) = req["max_tokens"].as_i64() {
260 o.insert("max_output_tokens".to_string(), json!(mt));
261 }
262 put(&mut o, "stream", &req["stream"]);
263 Value::Object(o)
264}
265
266pub fn anthropic_to_openai_chat(req: &Value) -> Value {
269 let mut msgs = Vec::new();
270 let system = match &req["system"] {
271 Value::String(s) => s.clone(),
272 Value::Array(_) => txt(&req["system"]),
273 _ => String::new(),
274 };
275 if !system.is_empty() {
276 msgs.push(json!({"role": "system", "content": system}));
277 }
278 for m in req["messages"].as_array().into_iter().flatten() {
279 let role = m["role"].as_str().unwrap_or("user");
280 if role == "system" || role == "developer" {
281 let text = txt(&m["content"]);
282 if !text.is_empty() {
283 msgs.push(json!({"role": "system", "content": text}));
284 }
285 continue;
286 }
287 match &m["content"] {
288 Value::String(s) => msgs.push(json!({"role": role, "content": s})),
289 Value::Array(blocks) => {
290 if role == "assistant" {
291 let mut texts = Vec::new();
292 let mut calls = Vec::new();
293 for b in blocks {
294 match b["type"].as_str() {
295 Some("text") => {
296 texts.push(b["text"].as_str().unwrap_or("").to_string());
297 }
298 Some("tool_use") => calls.push(json!({
299 "id": b["id"],
300 "type": "function",
301 "function": {
302 "name": b["name"],
303 "arguments": b["input"].to_string(),
304 },
305 })),
306 _ => {}
307 }
308 }
309 let content = if texts.is_empty() {
310 Value::Null
311 } else {
312 Value::String(texts.join(""))
313 };
314 let mut msg = json!({"role": "assistant", "content": content});
315 if !calls.is_empty() {
316 msg["tool_calls"] = Value::Array(calls);
317 }
318 msgs.push(msg);
319 } else {
320 let mut text_parts = Vec::new();
322 for b in blocks {
323 match b["type"].as_str() {
324 Some("text") => {
325 if let Some(t) = b["text"].as_str() {
326 if !t.is_empty() {
327 text_parts.push(t.to_string());
328 }
329 }
330 }
331 Some("tool_result") => {
332 msgs.push(json!({
333 "role": "tool",
334 "tool_call_id": b["tool_use_id"],
335 "content": txt(&b["content"]),
336 }));
337 }
338 _ => {}
339 }
340 }
341 if !text_parts.is_empty() {
342 msgs.push(json!({
343 "role": "user",
344 "content": text_parts.join("\n"),
345 }));
346 }
347 }
348 }
349 _ => {}
350 }
351 }
352 let mut o = Map::new();
353 put(&mut o, "model", &req["model"]);
354 o.insert("messages".to_string(), Value::Array(msgs));
355 if let Some(ts) = req["tools"].as_array() {
356 let tools: Vec<Value> = ts
357 .iter()
358 .map(|t| {
359 let mut f = Map::new();
360 put(&mut f, "name", &t["name"]);
361 put(&mut f, "description", &t["description"]);
362 f.insert(
363 "parameters".to_string(),
364 if t["input_schema"].is_object() {
365 t["input_schema"].clone()
366 } else {
367 json!({"type": "object"})
368 },
369 );
370 json!({"type": "function", "function": Value::Object(f)})
371 })
372 .collect();
373 if !tools.is_empty() {
374 o.insert("tools".to_string(), Value::Array(tools));
375 }
376 }
377 match &req["tool_choice"] {
378 Value::Object(tc) => match tc.get("type").and_then(|v| v.as_str()) {
379 Some("auto") => {
380 o.insert("tool_choice".to_string(), json!("auto"));
381 }
382 Some("any") => {
383 o.insert("tool_choice".to_string(), json!("required"));
384 }
385 Some("none") => {
386 o.insert("tool_choice".to_string(), json!("none"));
387 }
388 Some("tool") => {
389 o.insert(
390 "tool_choice".to_string(),
391 json!({
392 "type": "function",
393 "function": {"name": tc.get("name").cloned().unwrap_or(json!(""))},
394 }),
395 );
396 }
397 _ => {}
398 },
399 _ => {}
400 }
401 if let Some(mt) = req["max_tokens"].as_i64() {
402 o.insert("max_tokens".to_string(), json!(mt));
403 }
404 put(&mut o, "temperature", &req["temperature"]);
405 put(&mut o, "top_p", &req["top_p"]);
406 match &req["stop_sequences"] {
407 Value::Array(a) if !a.is_empty() => {
408 o.insert("stop".to_string(), Value::Array(a.clone()));
409 }
410 _ => {}
411 }
412 put(&mut o, "stream", &req["stream"]);
413 Value::Object(o)
415}
416
417fn stop_to_finish(stop: Option<&str>) -> &'static str {
418 match stop {
419 Some("max_tokens") => "length",
420 Some("tool_use") => "tool_calls",
421 _ => "stop",
422 }
423}
424
425fn finish_to_stop(finish: Option<&str>) -> &'static str {
426 match finish {
427 Some("length") => "max_tokens",
428 Some("tool_calls") => "tool_use",
429 _ => "end_turn",
430 }
431}
432
433pub fn anthropic_response_to_openai_chat(resp: &Value, model: &str) -> Value {
434 let mut texts = Vec::new();
435 let mut calls = Vec::new();
436 for b in resp["content"].as_array().into_iter().flatten() {
437 match b["type"].as_str() {
438 Some("text") => texts.push(b["text"].as_str().unwrap_or("").to_string()),
439 Some("tool_use") => calls.push(json!({
440 "id": b["id"],
441 "type": "function",
442 "function": {"name": b["name"], "arguments": b["input"].to_string()},
443 })),
444 _ => {}
445 }
446 }
447 let content = if texts.is_empty() {
448 Value::Null
449 } else {
450 Value::String(texts.join(""))
451 };
452 let mut msg = json!({"role": "assistant", "content": content});
453 if !calls.is_empty() {
454 msg["tool_calls"] = Value::Array(calls);
455 }
456 let u = &resp["usage"];
457 let pt = u["input_tokens"].as_i64().unwrap_or(0);
458 let ct = u["output_tokens"].as_i64().unwrap_or(0);
459 json!({
460 "id": format!("chatcmpl-{}", resp["id"].as_str().unwrap_or("")),
461 "object": "chat.completion",
462 "created": 0,
463 "model": model,
464 "choices": [{
465 "index": 0,
466 "message": msg,
467 "finish_reason": stop_to_finish(resp["stop_reason"].as_str()),
468 }],
469 "usage": {
470 "prompt_tokens": pt,
471 "completion_tokens": ct,
472 "total_tokens": pt + ct,
473 "prompt_tokens_details": {
474 "cached_tokens": u["cache_read_input_tokens"].as_i64().unwrap_or(0),
475 },
476 },
477 })
478}
479
480pub fn openai_chat_response_to_anthropic(resp: &Value, model: &str) -> Value {
482 let msg = &resp["choices"][0]["message"];
483 let mut content = Vec::new();
484 if let Some(t) = msg["content"].as_str() {
485 if !t.is_empty() {
486 content.push(json!({"type": "text", "text": t}));
487 }
488 }
489 for tc in msg["tool_calls"].as_array().into_iter().flatten() {
490 content.push(json!({
491 "type": "tool_use",
492 "id": tc["id"],
493 "name": tc["function"]["name"],
494 "input": parse_args(tc["function"]["arguments"].as_str().unwrap_or("{}")),
495 }));
496 }
497 let u = &resp["usage"];
498 let pt = u["prompt_tokens"].as_i64().unwrap_or(0);
499 let ct = u["completion_tokens"].as_i64().unwrap_or(0);
500 let cached = u["prompt_tokens_details"]["cached_tokens"]
501 .as_i64()
502 .unwrap_or(0);
503 let id = resp["id"].as_str().unwrap_or("");
504 let msg_id = id
505 .strip_prefix("chatcmpl-")
506 .unwrap_or(id);
507 json!({
508 "id": if msg_id.is_empty() { "msg_chat".to_string() } else { format!("msg_{msg_id}") },
509 "type": "message",
510 "role": "assistant",
511 "model": model,
512 "content": content,
513 "stop_reason": finish_to_stop(resp["choices"][0]["finish_reason"].as_str()),
514 "stop_sequence": null,
515 "usage": {
516 "input_tokens": pt,
517 "output_tokens": ct,
518 "cache_read_input_tokens": cached,
519 },
520 })
521}
522
523pub fn anthropic_response_to_openai_responses(resp: &Value, model: &str) -> Value {
524 let id = resp["id"].as_str().unwrap_or("");
525 let mut output = Vec::new();
526 for b in resp["content"].as_array().into_iter().flatten() {
527 match b["type"].as_str() {
528 Some("text") => output.push(json!({
529 "type": "message",
530 "id": format!("msg_{id}"),
531 "role": "assistant",
532 "status": "completed",
533 "content": [{"type": "output_text", "text": b["text"], "annotations": []}],
534 })),
535 Some("tool_use") => output.push(json!({
536 "type": "function_call",
537 "id": b["id"],
538 "call_id": b["id"],
539 "name": b["name"],
540 "arguments": b["input"].to_string(),
541 "status": "completed",
542 })),
543 _ => {}
544 }
545 }
546 let status = if resp["stop_reason"] == "max_tokens" {
547 "incomplete"
548 } else {
549 "completed"
550 };
551 let u = &resp["usage"];
552 let it = u["input_tokens"].as_i64().unwrap_or(0);
553 let ot = u["output_tokens"].as_i64().unwrap_or(0);
554 json!({
555 "id": format!("resp_{id}"),
556 "object": "response",
557 "status": status,
558 "model": model,
559 "output": output,
560 "usage": {
561 "input_tokens": it,
562 "output_tokens": ot,
563 "total_tokens": it + ot,
564 "input_tokens_details": {
565 "cached_tokens": u["cache_read_input_tokens"].as_i64().unwrap_or(0),
566 },
567 "output_tokens_details": {"reasoning_tokens": 0},
568 },
569 })
570}
571
572pub fn responses_final_to_anthropic(resp: &Value, model: &str) -> Value {
573 let mut content = Vec::new();
574 let mut has_call = false;
575 for it in resp["output"].as_array().into_iter().flatten() {
576 match it["type"].as_str() {
577 Some("message") => {
578 for p in it["content"].as_array().into_iter().flatten() {
579 if p["type"] == "output_text" {
580 content.push(json!({"type": "text", "text": p["text"]}));
581 }
582 }
583 }
584 Some("function_call") => {
585 has_call = true;
586 content.push(json!({
587 "type": "tool_use",
588 "id": it["call_id"],
589 "name": it["name"],
590 "input": parse_args(it["arguments"].as_str().unwrap_or("{}")),
591 }));
592 }
593 _ => {}
594 }
595 }
596 let stop = if resp["status"] == "incomplete" {
597 "max_tokens"
598 } else if has_call {
599 "tool_use"
600 } else {
601 "end_turn"
602 };
603 let u = &resp["usage"];
604 json!({
605 "id": format!("msg_{}", resp["id"].as_str().unwrap_or("")),
606 "type": "message",
607 "role": "assistant",
608 "model": model,
609 "content": content,
610 "stop_reason": stop,
611 "stop_sequence": null,
612 "usage": {
613 "input_tokens": u["input_tokens"].as_i64().unwrap_or(0),
614 "output_tokens": u["output_tokens"].as_i64().unwrap_or(0),
615 "cache_read_input_tokens": u["input_tokens_details"]["cached_tokens"].as_i64().unwrap_or(0),
616 },
617 })
618}
619
620pub fn responses_final_to_openai_chat(resp: &Value, model: &str) -> Value {
621 let mut texts = Vec::new();
622 let mut calls = Vec::new();
623 for it in resp["output"].as_array().into_iter().flatten() {
624 match it["type"].as_str() {
625 Some("message") => {
626 for p in it["content"].as_array().into_iter().flatten() {
627 if p["type"] == "output_text" {
628 texts.push(p["text"].as_str().unwrap_or("").to_string());
629 }
630 }
631 }
632 Some("function_call") => calls.push(json!({
633 "id": it["call_id"],
634 "type": "function",
635 "function": {"name": it["name"], "arguments": it["arguments"]},
636 })),
637 _ => {}
638 }
639 }
640 let content = if texts.is_empty() {
641 Value::Null
642 } else {
643 Value::String(texts.join(""))
644 };
645 let mut msg = json!({"role": "assistant", "content": content});
646 let finish = if resp["status"] == "incomplete" {
647 "length"
648 } else if calls.is_empty() {
649 "stop"
650 } else {
651 "tool_calls"
652 };
653 if !calls.is_empty() {
654 msg["tool_calls"] = Value::Array(calls);
655 }
656 let u = &resp["usage"];
657 let pt = u["input_tokens"].as_i64().unwrap_or(0);
658 let ct = u["output_tokens"].as_i64().unwrap_or(0);
659 json!({
660 "id": format!("chatcmpl-{}", resp["id"].as_str().unwrap_or("")),
661 "object": "chat.completion",
662 "created": 0,
663 "model": model,
664 "choices": [{"index": 0, "message": msg, "finish_reason": finish}],
665 "usage": {
666 "prompt_tokens": pt,
667 "completion_tokens": ct,
668 "total_tokens": pt + ct,
669 "prompt_tokens_details": {
670 "cached_tokens": u["input_tokens_details"]["cached_tokens"].as_i64().unwrap_or(0),
671 },
672 },
673 })
674}
675
676fn sse_datas(sse: &str) -> impl Iterator<Item = Value> + '_ {
677 sse.lines().filter_map(|l| {
678 let d = l.strip_prefix("data:")?.trim();
679 if d.is_empty() || d == "[DONE]" {
680 return None;
681 }
682 serde_json::from_str(d).ok()
683 })
684}
685
686pub fn parse_anthropic_sse_to_message(sse: &str) -> Option<Value> {
687 let mut msg: Option<Value> = None;
688 let mut blocks: Vec<Value> = Vec::new();
689 let mut partials: Vec<String> = Vec::new();
690 for v in sse_datas(sse) {
691 match v["type"].as_str() {
692 Some("message_start") => {
693 if v["message"].is_object() {
694 msg = Some(v["message"].clone());
695 }
696 }
697 Some("content_block_start") => {
698 let i = v["index"].as_u64().unwrap_or(blocks.len() as u64) as usize;
699 while blocks.len() <= i {
700 blocks.push(Value::Null);
701 partials.push(String::new());
702 }
703 blocks[i] = v["content_block"].clone();
704 partials[i] = String::new();
705 }
706 Some("content_block_delta") => {
707 let i = v["index"].as_u64().unwrap_or(0) as usize;
708 if i >= blocks.len() {
709 continue;
710 }
711 let d = &v["delta"];
712 match d["type"].as_str() {
713 Some("text_delta") => {
714 let t = format!(
715 "{}{}",
716 blocks[i]["text"].as_str().unwrap_or(""),
717 d["text"].as_str().unwrap_or("")
718 );
719 blocks[i]["text"] = json!(t);
720 }
721 Some("input_json_delta") => {
722 partials[i].push_str(d["partial_json"].as_str().unwrap_or(""));
723 }
724 _ => {}
725 }
726 }
727 Some("content_block_stop") => {
728 let i = v["index"].as_u64().unwrap_or(0) as usize;
729 if i < blocks.len() && blocks[i]["type"] == "tool_use" && !partials[i].is_empty() {
730 blocks[i]["input"] = parse_args(&partials[i]);
731 }
732 }
733 Some("message_delta") => {
734 let Some(m) = msg.as_mut() else { continue };
735 for k in ["stop_reason", "stop_sequence"] {
736 if !v["delta"][k].is_null() {
737 m[k] = v["delta"][k].clone();
738 }
739 }
740 if let Some(uo) = v["usage"].as_object() {
741 if !m["usage"].is_object() {
742 m["usage"] = json!({});
743 }
744 for (k, val) in uo {
745 m["usage"][k.as_str()] = val.clone();
746 }
747 }
748 }
749 _ => {}
750 }
751 }
752 let mut m = msg?;
753 m["content"] = Value::Array(blocks.into_iter().filter(|b| !b.is_null()).collect());
754 Some(m)
755}
756
757pub fn parse_responses_sse_final(sse: &str) -> Option<Value> {
758 let mut last = None;
759 let mut items: Vec<Value> = Vec::new();
760 for v in sse_datas(sse) {
761 match v["type"].as_str() {
762 Some("response.completed" | "response.incomplete" | "response.failed") => {
763 last = Some(v["response"].clone());
764 }
765 Some("response.output_item.done") => {
766 if v["item"].is_object() {
767 items.push(v["item"].clone());
768 }
769 }
770 _ => {}
771 }
772 }
773 let mut resp = last?;
774 if resp["output"].as_array().map(|a| a.is_empty()).unwrap_or(true) && !items.is_empty() {
775 resp["output"] = Value::Array(items);
776 }
777 Some(resp)
778}
779
780pub fn parse_openai_chat_sse_final(sse: &str) -> Option<Value> {
782 let mut id = String::new();
783 let mut model = String::new();
784 let mut content = String::new();
785 let mut tool_calls: Vec<(String, String, String)> = Vec::new(); let mut finish_reason = Value::Null;
787 let mut usage = Value::Null;
788 let mut saw_chunk = false;
789 for v in sse_datas(sse) {
790 if v["object"] == "chat.completion" && v["choices"][0]["message"].is_object() {
791 return Some(v);
793 }
794 if !v["choices"].is_array() && v.get("usage").is_none() {
795 continue;
796 }
797 saw_chunk = true;
798 if let Some(s) = v["id"].as_str() {
799 if !s.is_empty() {
800 id = s.to_string();
801 }
802 }
803 if let Some(s) = v["model"].as_str() {
804 if !s.is_empty() {
805 model = s.to_string();
806 }
807 }
808 if let Some(u) = v.get("usage").filter(|u| u.is_object()) {
809 usage = u.clone();
810 }
811 let choice = &v["choices"][0];
812 if !choice["finish_reason"].is_null() {
813 finish_reason = choice["finish_reason"].clone();
814 }
815 let delta = &choice["delta"];
816 if let Some(c) = delta["content"].as_str() {
817 content.push_str(c);
818 }
819 for tc in delta["tool_calls"].as_array().into_iter().flatten() {
820 let idx = tc["index"].as_u64().unwrap_or(0) as usize;
821 while tool_calls.len() <= idx {
822 tool_calls.push((String::new(), String::new(), String::new()));
823 }
824 if let Some(tc_id) = tc["id"].as_str() {
825 tool_calls[idx].0.push_str(tc_id);
826 }
827 if let Some(n) = tc["function"]["name"].as_str() {
828 tool_calls[idx].1.push_str(n);
829 }
830 if let Some(a) = tc["function"]["arguments"].as_str() {
831 tool_calls[idx].2.push_str(a);
832 }
833 }
834 if let Some(m) = choice.get("message") {
836 if let Some(c) = m["content"].as_str() {
837 if content.is_empty() {
838 content.push_str(c);
839 }
840 }
841 if tool_calls.is_empty() {
842 for tc in m["tool_calls"].as_array().into_iter().flatten() {
843 tool_calls.push((
844 tc["id"].as_str().unwrap_or("").to_string(),
845 tc["function"]["name"].as_str().unwrap_or("").to_string(),
846 tc["function"]["arguments"].as_str().unwrap_or("").to_string(),
847 ));
848 }
849 }
850 }
851 }
852 if !saw_chunk {
853 return None;
854 }
855 let content_val = if content.is_empty() {
856 Value::Null
857 } else {
858 Value::String(content)
859 };
860 let mut msg = json!({"role": "assistant", "content": content_val});
861 if !tool_calls.is_empty() {
862 let tcs: Vec<Value> = tool_calls
863 .into_iter()
864 .filter(|(i, n, _)| !i.is_empty() || !n.is_empty())
865 .map(|(i, n, a)| {
866 json!({
867 "id": i,
868 "type": "function",
869 "function": {"name": n, "arguments": a},
870 })
871 })
872 .collect();
873 if !tcs.is_empty() {
874 msg["tool_calls"] = Value::Array(tcs);
875 }
876 }
877 let mut out = json!({
878 "id": id,
879 "object": "chat.completion",
880 "created": 0,
881 "model": model,
882 "choices": [{
883 "index": 0,
884 "message": msg,
885 "finish_reason": finish_reason,
886 }],
887 });
888 if usage.is_object() {
889 out["usage"] = usage;
890 }
891 Some(out)
892}
893
894pub fn synth_openai_chat_sse(chat_resp: &Value) -> String {
895 let chunk = |delta: Value, finish: Value, usage: Option<&Value>| {
896 let mut c = json!({
897 "id": chat_resp["id"],
898 "object": "chat.completion.chunk",
899 "created": 0,
900 "model": chat_resp["model"],
901 "choices": [{"index": 0, "delta": delta, "finish_reason": finish}],
902 });
903 if let Some(u) = usage {
904 c["usage"] = u.clone();
905 }
906 format!("data: {c}\n\n")
907 };
908 let msg = &chat_resp["choices"][0]["message"];
909 let mut out = chunk(json!({"role": "assistant"}), Value::Null, None);
910 if let Some(t) = msg["content"].as_str() {
911 out.push_str(&chunk(json!({"content": t}), Value::Null, None));
912 }
913 if let Some(tcs) = msg["tool_calls"].as_array() {
914 let tcs: Vec<Value> = tcs
915 .iter()
916 .enumerate()
917 .map(|(i, tc)| {
918 let mut tc = tc.clone();
919 tc["index"] = json!(i);
920 tc
921 })
922 .collect();
923 out.push_str(&chunk(json!({"tool_calls": tcs}), Value::Null, None));
924 }
925 let usage = chat_resp["usage"].is_object().then_some(&chat_resp["usage"]);
926 out.push_str(&chunk(
927 json!({}),
928 chat_resp["choices"][0]["finish_reason"].clone(),
929 usage,
930 ));
931 out.push_str("data: [DONE]\n\n");
932 out
933}
934
935fn sse_event(name: &str, data: Value) -> String {
936 format!("event: {name}\ndata: {data}\n\n")
937}
938
939pub fn synth_openai_responses_sse(responses_resp: &Value) -> String {
940 let mut created = responses_resp.clone();
941 created["status"] = json!("in_progress");
942 let mut out = sse_event(
943 "response.created",
944 json!({"type": "response.created", "response": created}),
945 );
946 for (i, it) in responses_resp["output"]
947 .as_array()
948 .into_iter()
949 .flatten()
950 .enumerate()
951 {
952 out.push_str(&sse_event(
953 "response.output_item.added",
954 json!({"type": "response.output_item.added", "output_index": i, "item": it}),
955 ));
956 if it["type"] == "message" {
957 let text = txt(&it["content"]);
958 out.push_str(&sse_event(
959 "response.output_text.delta",
960 json!({
961 "type": "response.output_text.delta",
962 "item_id": it["id"],
963 "output_index": 0,
964 "content_index": 0,
965 "delta": text,
966 }),
967 ));
968 out.push_str(&sse_event(
969 "response.output_text.done",
970 json!({
971 "type": "response.output_text.done",
972 "item_id": it["id"],
973 "output_index": 0,
974 "content_index": 0,
975 "text": text,
976 }),
977 ));
978 }
979 }
980 out.push_str(&sse_event(
981 "response.completed",
982 json!({"type": "response.completed", "response": responses_resp}),
983 ));
984 out
985}
986
987pub fn synth_anthropic_sse(anthropic_resp: &Value) -> String {
988 let mut start = anthropic_resp.clone();
989 start["content"] = json!([]);
990 start["stop_reason"] = Value::Null;
991 start["stop_sequence"] = Value::Null;
992 start["usage"] = json!({
993 "input_tokens": anthropic_resp["usage"]["input_tokens"].as_i64().unwrap_or(0),
994 "output_tokens": 0,
995 });
996 let mut out = sse_event(
997 "message_start",
998 json!({"type": "message_start", "message": start}),
999 );
1000 for (i, b) in anthropic_resp["content"]
1001 .as_array()
1002 .into_iter()
1003 .flatten()
1004 .enumerate()
1005 {
1006 match b["type"].as_str() {
1007 Some("text") => {
1008 out.push_str(&sse_event(
1009 "content_block_start",
1010 json!({
1011 "type": "content_block_start",
1012 "index": i,
1013 "content_block": {"type": "text", "text": ""},
1014 }),
1015 ));
1016 out.push_str(&sse_event(
1017 "content_block_delta",
1018 json!({
1019 "type": "content_block_delta",
1020 "index": i,
1021 "delta": {"type": "text_delta", "text": b["text"]},
1022 }),
1023 ));
1024 }
1025 Some("tool_use") => {
1026 out.push_str(&sse_event(
1027 "content_block_start",
1028 json!({
1029 "type": "content_block_start",
1030 "index": i,
1031 "content_block": {"type": "tool_use", "id": b["id"], "name": b["name"], "input": {}},
1032 }),
1033 ));
1034 out.push_str(&sse_event(
1035 "content_block_delta",
1036 json!({
1037 "type": "content_block_delta",
1038 "index": i,
1039 "delta": {"type": "input_json_delta", "partial_json": b["input"].to_string()},
1040 }),
1041 ));
1042 }
1043 _ => continue,
1044 }
1045 out.push_str(&sse_event(
1046 "content_block_stop",
1047 json!({"type": "content_block_stop", "index": i}),
1048 ));
1049 }
1050 out.push_str(&sse_event(
1051 "message_delta",
1052 json!({
1053 "type": "message_delta",
1054 "delta": {
1055 "stop_reason": anthropic_resp["stop_reason"],
1056 "stop_sequence": anthropic_resp["stop_sequence"],
1057 },
1058 "usage": {
1059 "output_tokens": anthropic_resp["usage"]["output_tokens"].as_i64().unwrap_or(0),
1060 },
1061 }),
1062 ));
1063 out.push_str(&sse_event("message_stop", json!({"type": "message_stop"})));
1064 out
1065}
1066
1067fn tool_result_snip(text: &str) -> String {
1068 let head: String = text.chars().take(200).collect();
1069 format!("[tool result] {head}")
1070}
1071
1072pub fn last_user_text(format_str: &str, req: &Value) -> Option<String> {
1073 match format_str {
1074 "anthropic" => {
1075 for m in req["messages"].as_array().into_iter().flatten().rev() {
1076 if m["role"] != "user" {
1077 continue;
1078 }
1079 match &m["content"] {
1080 Value::String(s) if !s.is_empty() => return Some(s.clone()),
1081 Value::Array(blocks) => {
1082 let text = blocks
1083 .iter()
1084 .filter(|b| b["type"] == "text")
1085 .filter_map(|b| b["text"].as_str())
1086 .collect::<Vec<_>>()
1087 .join("\n");
1088 if !text.is_empty() {
1089 return Some(text);
1090 }
1091 if let Some(tr) = blocks.iter().find(|b| b["type"] == "tool_result") {
1092 return Some(tool_result_snip(&txt(&tr["content"])));
1093 }
1094 }
1095 _ => {}
1096 }
1097 }
1098 None
1099 }
1100 "openai-chat" => {
1101 for m in req["messages"].as_array().into_iter().flatten().rev() {
1102 match m["role"].as_str() {
1103 Some("user") => {
1104 let t = txt(&m["content"]);
1105 if !t.is_empty() {
1106 return Some(t);
1107 }
1108 }
1109 Some("tool") => return Some(tool_result_snip(&txt(&m["content"]))),
1110 _ => {}
1111 }
1112 }
1113 None
1114 }
1115 "openai-responses" => {
1116 if let Some(s) = req["input"].as_str() {
1117 return (!s.is_empty()).then(|| s.to_string());
1118 }
1119 for it in req["input"].as_array().into_iter().flatten().rev() {
1120 match it["type"].as_str().unwrap_or("message") {
1121 "message" if it["role"] == "user" => {
1122 let t = match &it["content"] {
1123 Value::String(s) => s.clone(),
1124 Value::Array(parts) => parts
1125 .iter()
1126 .filter(|p| p["type"] == "input_text")
1127 .filter_map(|p| p["text"].as_str())
1128 .collect::<Vec<_>>()
1129 .join("\n"),
1130 _ => String::new(),
1131 };
1132 if !t.is_empty() {
1133 return Some(t);
1134 }
1135 }
1136 "function_call_output" => {
1137 return Some(tool_result_snip(&txt(&it["output"])))
1138 }
1139 _ => {}
1140 }
1141 }
1142 None
1143 }
1144 "gemini" => {
1145 for c in req["contents"].as_array().into_iter().flatten().rev() {
1146 if c["role"].as_str().unwrap_or("user") != "user" {
1147 continue;
1148 }
1149 let text = gemini_parts_text(&c["parts"]);
1150 if !text.is_empty() {
1151 return Some(text);
1152 }
1153 if let Some(fr) = c["parts"]
1154 .as_array()
1155 .into_iter()
1156 .flatten()
1157 .find(|p| p["functionResponse"].is_object())
1158 {
1159 return Some(tool_result_snip(
1160 &fr["functionResponse"]["response"].to_string(),
1161 ));
1162 }
1163 }
1164 None
1165 }
1166 _ => None,
1167 }
1168}
1169
1170fn anthropic_message_text(msg: &Value) -> Option<String> {
1171 let parts: Vec<&str> = msg["content"]
1172 .as_array()
1173 .into_iter()
1174 .flatten()
1175 .filter(|b| b["type"] == "text")
1176 .filter_map(|b| b["text"].as_str())
1177 .collect();
1178 (!parts.is_empty()).then(|| parts.join(""))
1179}
1180
1181fn responses_output_text(resp: &Value) -> Option<String> {
1182 let mut out = String::new();
1183 for it in resp["output"].as_array().into_iter().flatten() {
1184 if it["type"] != "message" {
1185 continue;
1186 }
1187 for p in it["content"].as_array().into_iter().flatten() {
1188 if p["type"] == "output_text" {
1189 out.push_str(p["text"].as_str().unwrap_or(""));
1190 }
1191 }
1192 }
1193 (!out.is_empty()).then_some(out)
1194}
1195
1196fn openai_chat_sse_text(sse: &str) -> Option<String> {
1197 let mut out = String::new();
1198 for v in sse_datas(sse) {
1199 if let Some(c) = v["choices"][0]["delta"]["content"].as_str() {
1200 out.push_str(c);
1201 }
1202 }
1203 (!out.is_empty()).then_some(out)
1204}
1205
1206fn tool_call_json(name: &Value, args: &Value) -> Value {
1207 let arguments = match args {
1208 Value::String(s) => s.clone(),
1209 Value::Null => String::new(),
1210 other => other.to_string(),
1211 };
1212 json!({"name": name, "arguments": arguments})
1213}
1214
1215pub fn assistant_tool_calls(upstream_format: &str, resp_text: &str) -> Vec<Value> {
1216 let trimmed = resp_text.trim_start();
1217 let is_sse = trimmed.starts_with("event:") || trimmed.starts_with("data:");
1218 match upstream_format {
1219 "anthropic" => {
1220 let msg = if is_sse {
1221 parse_anthropic_sse_to_message(resp_text)
1222 } else {
1223 serde_json::from_str(resp_text).ok()
1224 };
1225 msg.map(|m| {
1226 m["content"]
1227 .as_array()
1228 .into_iter()
1229 .flatten()
1230 .filter(|b| b["type"] == "tool_use")
1231 .map(|b| tool_call_json(&b["name"], &b["input"]))
1232 .collect()
1233 })
1234 .unwrap_or_default()
1235 }
1236 "openai-chat" => {
1237 if is_sse {
1238 let mut calls: Vec<(String, String)> = Vec::new();
1239 for v in sse_datas(resp_text) {
1240 for tc in v["choices"][0]["delta"]["tool_calls"]
1241 .as_array()
1242 .into_iter()
1243 .flatten()
1244 {
1245 let idx = tc["index"].as_u64().unwrap_or(0) as usize;
1246 while calls.len() <= idx {
1247 calls.push((String::new(), String::new()));
1248 }
1249 if let Some(n) = tc["function"]["name"].as_str() {
1250 calls[idx].0.push_str(n);
1251 }
1252 if let Some(a) = tc["function"]["arguments"].as_str() {
1253 calls[idx].1.push_str(a);
1254 }
1255 }
1256 }
1257 calls
1258 .into_iter()
1259 .filter(|(n, _)| !n.is_empty())
1260 .map(|(n, a)| json!({"name": n, "arguments": a}))
1261 .collect()
1262 } else {
1263 serde_json::from_str::<Value>(resp_text)
1264 .ok()
1265 .map(|v| {
1266 v["choices"][0]["message"]["tool_calls"]
1267 .as_array()
1268 .into_iter()
1269 .flatten()
1270 .map(|tc| tool_call_json(&tc["function"]["name"], &tc["function"]["arguments"]))
1271 .collect()
1272 })
1273 .unwrap_or_default()
1274 }
1275 }
1276 "openai-responses" => {
1277 let resp = if is_sse {
1278 parse_responses_sse_final(resp_text)
1279 } else {
1280 serde_json::from_str(resp_text).ok()
1281 };
1282 resp.map(|r| {
1283 r["output"]
1284 .as_array()
1285 .into_iter()
1286 .flatten()
1287 .filter(|it| it["type"] == "function_call")
1288 .map(|it| tool_call_json(&it["name"], &it["arguments"]))
1289 .collect()
1290 })
1291 .unwrap_or_default()
1292 }
1293 _ => Vec::new(),
1294 }
1295}
1296
1297pub fn assistant_reply_text(upstream_format: &str, resp_text: &str) -> Option<String> {
1298 let trimmed = resp_text.trim_start();
1299 let is_sse = trimmed.starts_with("event:") || trimmed.starts_with("data:");
1300 match upstream_format {
1301 "anthropic" => {
1302 let msg = if is_sse {
1303 parse_anthropic_sse_to_message(resp_text)?
1304 } else {
1305 serde_json::from_str(resp_text).ok()?
1306 };
1307 anthropic_message_text(&msg)
1308 }
1309 "openai-chat" => {
1310 if is_sse {
1311 openai_chat_sse_text(resp_text)
1312 } else {
1313 let v: Value = serde_json::from_str(resp_text).ok()?;
1314 v["choices"][0]["message"]["content"]
1315 .as_str()
1316 .map(String::from)
1317 }
1318 }
1319 "openai-responses" => {
1320 let resp = if is_sse {
1321 parse_responses_sse_final(resp_text)?
1322 } else {
1323 serde_json::from_str(resp_text).ok()?
1324 };
1325 responses_output_text(&resp)
1326 }
1327 "gemini" => {
1328 if is_sse {
1329 let mut out = String::new();
1330 for v in sse_datas(resp_text) {
1331 out.push_str(&gemini_parts_text(&v["candidates"][0]["content"]["parts"]));
1332 }
1333 (!out.is_empty()).then_some(out)
1334 } else {
1335 let v: Value = serde_json::from_str(resp_text).ok()?;
1336 let text = gemini_parts_text(&v["candidates"][0]["content"]["parts"]);
1337 (!text.is_empty()).then_some(text)
1338 }
1339 }
1340 _ => None,
1341 }
1342}
1343
1344pub(crate) fn gemini_parts_text(parts: &Value) -> String {
1345 parts
1346 .as_array()
1347 .into_iter()
1348 .flatten()
1349 .filter_map(|p| p["text"].as_str())
1350 .collect::<Vec<_>>()
1351 .join("\n")
1352}
1353
1354pub fn gemini_to_anthropic(req: &Value) -> Value {
1355 let mut msgs = Vec::new();
1356 let mut call_ids: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1357 let mut call_counter = 0usize;
1358 for content in req["contents"].as_array().into_iter().flatten() {
1359 let role = content["role"].as_str().unwrap_or("user");
1360 let mut blocks = Vec::new();
1361 for part in content["parts"].as_array().into_iter().flatten() {
1362 if let Some(t) = part["text"].as_str() {
1363 if !t.is_empty() {
1364 blocks.push(json!({"type": "text", "text": t}));
1365 }
1366 } else if part["functionCall"].is_object() {
1367 call_counter += 1;
1368 let name = part["functionCall"]["name"].as_str().unwrap_or("");
1369 let id = format!("toolu_gemini_{call_counter}");
1370 call_ids.insert(name.to_string(), id.clone());
1371 blocks.push(json!({
1372 "type": "tool_use",
1373 "id": id,
1374 "name": name,
1375 "input": if part["functionCall"]["args"].is_object() {
1376 part["functionCall"]["args"].clone()
1377 } else {
1378 json!({})
1379 },
1380 }));
1381 } else if part["functionResponse"].is_object() {
1382 let name = part["functionResponse"]["name"].as_str().unwrap_or("");
1383 let id = call_ids
1384 .get(name)
1385 .cloned()
1386 .unwrap_or_else(|| format!("toolu_gemini_{name}"));
1387 let payload = &part["functionResponse"]["response"];
1388 let text = match payload {
1389 Value::String(s) => s.clone(),
1390 v if v.is_null() => String::new(),
1391 v => v.to_string(),
1392 };
1393 blocks.push(json!({
1394 "type": "tool_result",
1395 "tool_use_id": id,
1396 "content": [{"type": "text", "text": text}],
1397 }));
1398 }
1399 }
1400 if blocks.is_empty() {
1401 continue;
1402 }
1403 let a_role = if role == "model" { "assistant" } else { "user" };
1404 msgs.push(json!({"role": a_role, "content": blocks}));
1405 }
1406 let mut o = Map::new();
1407 put(&mut o, "model", &req["model"]);
1408 let system = gemini_parts_text(&req["systemInstruction"]["parts"]);
1409 if !system.trim().is_empty() {
1410 o.insert("system".to_string(), Value::String(system));
1411 }
1412 o.insert("messages".to_string(), Value::Array(msgs));
1413 let tools: Vec<Value> = req["tools"]
1414 .as_array()
1415 .into_iter()
1416 .flatten()
1417 .flat_map(|t| t["functionDeclarations"].as_array().cloned().unwrap_or_default())
1418 .map(|fd| {
1419 let mut tool = Map::new();
1420 put(&mut tool, "name", &fd["name"]);
1421 put(&mut tool, "description", &fd["description"]);
1422 if fd["parameters"].is_object() {
1423 tool.insert("input_schema".to_string(), fd["parameters"].clone());
1424 } else {
1425 tool.insert("input_schema".to_string(), json!({"type": "object"}));
1426 }
1427 Value::Object(tool)
1428 })
1429 .collect();
1430 if !tools.is_empty() {
1431 o.insert("tools".to_string(), Value::Array(tools));
1432 }
1433 match req["toolConfig"]["functionCallingConfig"]["mode"].as_str() {
1434 Some("ANY") => {
1435 o.insert("tool_choice".to_string(), json!({"type": "any"}));
1436 }
1437 Some("AUTO") => {
1438 o.insert("tool_choice".to_string(), json!({"type": "auto"}));
1439 }
1440 _ => {}
1441 }
1442 let g = &req["generationConfig"];
1443 let max = g["maxOutputTokens"].as_i64().unwrap_or(8192);
1444 o.insert("max_tokens".to_string(), json!(max));
1445 put(&mut o, "temperature", &g["temperature"]);
1446 put(&mut o, "top_p", &g["topP"]);
1447 if let Some(stops) = g["stopSequences"].as_array() {
1448 if !stops.is_empty() {
1449 o.insert("stop_sequences".to_string(), Value::Array(stops.clone()));
1450 }
1451 }
1452 Value::Object(o)
1453}
1454
1455fn stop_to_gemini_finish(stop: Option<&str>) -> &'static str {
1456 match stop {
1457 Some("max_tokens") => "MAX_TOKENS",
1458 _ => "STOP",
1459 }
1460}
1461
1462pub fn anthropic_response_to_gemini(resp: &Value, model: &str) -> Value {
1463 let mut parts = Vec::new();
1464 for b in resp["content"].as_array().into_iter().flatten() {
1465 match b["type"].as_str() {
1466 Some("text") => {
1467 parts.push(json!({"text": b["text"].as_str().unwrap_or("")}));
1468 }
1469 Some("tool_use") => {
1470 parts.push(json!({
1471 "functionCall": {"name": b["name"], "args": b["input"].clone()},
1472 }));
1473 }
1474 _ => {}
1475 }
1476 }
1477 if parts.is_empty() {
1478 parts.push(json!({"text": ""}));
1479 }
1480 let u = &resp["usage"];
1481 let pt = u["input_tokens"].as_i64().unwrap_or(0)
1482 + u["cache_read_input_tokens"].as_i64().unwrap_or(0);
1483 let ct = u["output_tokens"].as_i64().unwrap_or(0);
1484 json!({
1485 "candidates": [{
1486 "content": {"role": "model", "parts": parts},
1487 "finishReason": stop_to_gemini_finish(resp["stop_reason"].as_str()),
1488 "index": 0,
1489 }],
1490 "usageMetadata": {
1491 "promptTokenCount": pt,
1492 "candidatesTokenCount": ct,
1493 "totalTokenCount": pt + ct,
1494 "cachedContentTokenCount": u["cache_read_input_tokens"].as_i64().unwrap_or(0),
1495 },
1496 "modelVersion": model,
1497 })
1498}
1499
1500pub fn anthropic_to_gemini_request(req: &Value) -> Value {
1501 let mut contents = Vec::new();
1502 let mut tool_names: std::collections::HashMap<String, String> =
1503 std::collections::HashMap::new();
1504 for m in req["messages"].as_array().into_iter().flatten() {
1505 let role = if m["role"] == "assistant" { "model" } else { "user" };
1506 let mut parts = Vec::new();
1507 match &m["content"] {
1508 Value::String(s) => {
1509 if !s.is_empty() {
1510 parts.push(json!({"text": s}));
1511 }
1512 }
1513 Value::Array(blocks) => {
1514 for b in blocks {
1515 match b["type"].as_str() {
1516 Some("text") => {
1517 parts.push(json!({"text": b["text"].as_str().unwrap_or("")}));
1518 }
1519 Some("tool_use") => {
1520 let id = b["id"].as_str().unwrap_or("").to_string();
1521 let name = b["name"].as_str().unwrap_or("").to_string();
1522 tool_names.insert(id, name.clone());
1523 parts.push(json!({
1524 "functionCall": {"name": name, "args": b["input"].clone()},
1525 }));
1526 }
1527 Some("tool_result") => {
1528 let id = b["tool_use_id"].as_str().unwrap_or("");
1529 let name = tool_names
1530 .get(id)
1531 .cloned()
1532 .unwrap_or_else(|| id.to_string());
1533 parts.push(json!({
1534 "functionResponse": {
1535 "name": name,
1536 "response": {"result": txt(&b["content"])},
1537 },
1538 }));
1539 }
1540 _ => {}
1541 }
1542 }
1543 }
1544 _ => {}
1545 }
1546 if !parts.is_empty() {
1547 contents.push(json!({"role": role, "parts": parts}));
1548 }
1549 }
1550 let mut o = Map::new();
1551 o.insert("contents".to_string(), Value::Array(contents));
1552 let system = txt(&req["system"]);
1553 if !system.is_empty() {
1554 o.insert(
1555 "systemInstruction".to_string(),
1556 json!({"parts": [{"text": system}]}),
1557 );
1558 }
1559 let decls: Vec<Value> = req["tools"]
1560 .as_array()
1561 .into_iter()
1562 .flatten()
1563 .map(|t| {
1564 json!({
1565 "name": t["name"],
1566 "description": t["description"],
1567 "parameters": if t["input_schema"].is_object() {
1568 t["input_schema"].clone()
1569 } else {
1570 json!({"type": "object"})
1571 },
1572 })
1573 })
1574 .collect();
1575 if !decls.is_empty() {
1576 o.insert(
1577 "tools".to_string(),
1578 json!([{"functionDeclarations": decls}]),
1579 );
1580 }
1581 let mut g = Map::new();
1582 put(&mut g, "temperature", &req["temperature"]);
1583 put(&mut g, "topP", &req["top_p"]);
1584 put(&mut g, "maxOutputTokens", &req["max_tokens"]);
1585 if let Some(stops) = req["stop_sequences"].as_array() {
1586 if !stops.is_empty() {
1587 g.insert("stopSequences".to_string(), Value::Array(stops.clone()));
1588 }
1589 }
1590 if !g.is_empty() {
1591 o.insert("generationConfig".to_string(), Value::Object(g));
1592 }
1593 Value::Object(o)
1594}
1595
1596pub fn gemini_response_to_anthropic(resp: &Value, model: &str) -> Value {
1597 let mut content = Vec::new();
1598 let mut saw_tool = false;
1599 let mut call_counter = 0usize;
1600 for part in resp["candidates"][0]["content"]["parts"]
1601 .as_array()
1602 .into_iter()
1603 .flatten()
1604 {
1605 if let Some(t) = part["text"].as_str() {
1606 if !t.is_empty() {
1607 content.push(json!({"type": "text", "text": t}));
1608 }
1609 } else if part["functionCall"].is_object() {
1610 saw_tool = true;
1611 call_counter += 1;
1612 content.push(json!({
1613 "type": "tool_use",
1614 "id": format!("toolu_gemini_{call_counter}"),
1615 "name": part["functionCall"]["name"],
1616 "input": if part["functionCall"]["args"].is_object() {
1617 part["functionCall"]["args"].clone()
1618 } else {
1619 json!({})
1620 },
1621 }));
1622 }
1623 }
1624 let finish = resp["candidates"][0]["finishReason"].as_str();
1625 let stop_reason = if saw_tool {
1626 "tool_use"
1627 } else if finish == Some("MAX_TOKENS") {
1628 "max_tokens"
1629 } else {
1630 "end_turn"
1631 };
1632 let u = &resp["usageMetadata"];
1633 json!({
1634 "id": format!("msg_gemini_{}", resp["responseId"].as_str().unwrap_or("0")),
1635 "type": "message",
1636 "role": "assistant",
1637 "model": model,
1638 "content": content,
1639 "stop_reason": stop_reason,
1640 "usage": {
1641 "input_tokens": u["promptTokenCount"].as_i64().unwrap_or(0),
1642 "output_tokens": u["candidatesTokenCount"].as_i64().unwrap_or(0)
1643 + u["thoughtsTokenCount"].as_i64().unwrap_or(0),
1644 "cache_read_input_tokens": u["cachedContentTokenCount"].as_i64().unwrap_or(0),
1645 },
1646 })
1647}
1648
1649pub fn parse_gemini_upstream_final(text: &str) -> Option<Value> {
1650 let unwrap = |v: Value| -> Value {
1651 if v["response"].is_object() {
1652 v["response"].clone()
1653 } else {
1654 v
1655 }
1656 };
1657 let trimmed = text.trim_start();
1658 if !(trimmed.starts_with("data:") || trimmed.starts_with("event:")) {
1659 return serde_json::from_str::<Value>(text).ok().map(unwrap);
1660 }
1661 let mut texts = String::new();
1662 let mut calls: Vec<Value> = Vec::new();
1663 let mut finish = Value::Null;
1664 let mut usage = Value::Null;
1665 let mut model_version = Value::Null;
1666 let mut saw_any = false;
1667 for frame in sse_datas(text) {
1668 let v = unwrap(frame);
1669 if !v["candidates"].is_array() && !v["usageMetadata"].is_object() {
1670 continue;
1671 }
1672 saw_any = true;
1673 for part in v["candidates"][0]["content"]["parts"]
1674 .as_array()
1675 .into_iter()
1676 .flatten()
1677 {
1678 if let Some(t) = part["text"].as_str() {
1679 texts.push_str(t);
1680 } else if part["functionCall"].is_object() {
1681 calls.push(part.clone());
1682 }
1683 }
1684 if v["candidates"][0]["finishReason"].is_string() {
1685 finish = v["candidates"][0]["finishReason"].clone();
1686 }
1687 if v["usageMetadata"].is_object() {
1688 usage = v["usageMetadata"].clone();
1689 }
1690 if v["modelVersion"].is_string() {
1691 model_version = v["modelVersion"].clone();
1692 }
1693 }
1694 if !saw_any {
1695 return None;
1696 }
1697 let mut parts = Vec::new();
1698 if !texts.is_empty() {
1699 parts.push(json!({"text": texts}));
1700 }
1701 parts.extend(calls);
1702 Some(json!({
1703 "candidates": [{
1704 "content": {"role": "model", "parts": parts},
1705 "finishReason": if finish.is_null() { json!("STOP") } else { finish },
1706 "index": 0,
1707 }],
1708 "usageMetadata": usage,
1709 "modelVersion": model_version,
1710 }))
1711}
1712
1713pub fn synth_gemini_sse(resp: &Value) -> String {
1714 let text = gemini_parts_text(&resp["candidates"][0]["content"]["parts"]);
1715 let mut frames = Vec::new();
1716 if !text.is_empty() {
1717 let content_frame = json!({
1718 "candidates": [{
1719 "content": {"role": "model", "parts": [{"text": text}]},
1720 "index": 0,
1721 }],
1722 "modelVersion": resp["modelVersion"],
1723 });
1724 frames.push(format!("data: {content_frame}\n\n"));
1725 }
1726 let mut fin = resp.clone();
1727 if !text.is_empty() {
1728 if let Some(parts) = fin["candidates"][0]["content"]["parts"].as_array_mut() {
1729 parts.retain(|p| p["text"].as_str().is_none());
1730 if parts.is_empty() {
1731 parts.push(json!({"text": ""}));
1732 }
1733 }
1734 }
1735 frames.push(format!("data: {fin}\n\n"));
1736 frames.concat()
1737}
1738
1739pub fn normalize_codex_request(req: &mut Value) {
1740 let Some(o) = req.as_object_mut() else { return };
1741 o.insert("store".to_string(), json!(false));
1742 o.insert("stream".to_string(), json!(true));
1743 if !o.contains_key("tool_choice") {
1744 o.insert("tool_choice".to_string(), json!("auto"));
1745 }
1746 if !o.contains_key("parallel_tool_calls") {
1747 o.insert("parallel_tool_calls".to_string(), json!(true));
1748 }
1749 o.insert("include".to_string(), json!(["reasoning.encrypted_content"]));
1750 for k in [
1751 "context_management",
1752 "max_completion_tokens",
1753 "max_output_tokens",
1754 "max_tokens",
1755 "prompt_cache_retention",
1756 "safety_identifier",
1757 "temperature",
1758 "top_p",
1759 "truncation",
1760 "user",
1761 ] {
1762 o.remove(k);
1763 }
1764}
1765
1766#[cfg(test)]
1767mod tests {
1768 use super::*;
1769
1770 fn gemini_req() -> Value {
1771 json!({
1772 "model": "gpt-5.5",
1773 "systemInstruction": {"parts": [{"text": "be terse"}]},
1774 "contents": [
1775 {"role": "user", "parts": [{"text": "what is the weather in SF?"}]},
1776 {"role": "model", "parts": [
1777 {"text": "checking"},
1778 {"functionCall": {"name": "get_weather", "args": {"city": "SF"}}}
1779 ]},
1780 {"role": "user", "parts": [
1781 {"functionResponse": {"name": "get_weather", "response": {"temp": 18}}}
1782 ]}
1783 ],
1784 "tools": [{"functionDeclarations": [{
1785 "name": "get_weather",
1786 "description": "look up weather",
1787 "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}
1788 }]}],
1789 "toolConfig": {"functionCallingConfig": {"mode": "AUTO"}},
1790 "generationConfig": {
1791 "temperature": 0.5,
1792 "topP": 0.9,
1793 "maxOutputTokens": 1024,
1794 "stopSequences": ["END"]
1795 }
1796 })
1797 }
1798
1799 #[test]
1800 fn gemini_to_anthropic_full() {
1801 let a = gemini_to_anthropic(&gemini_req());
1802 assert_eq!(a["model"], "gpt-5.5");
1803 assert_eq!(a["system"], "be terse");
1804 assert_eq!(a["max_tokens"], 1024);
1805 assert_eq!(a["temperature"], 0.5);
1806 assert_eq!(a["top_p"], 0.9);
1807 assert_eq!(a["stop_sequences"][0], "END");
1808 assert_eq!(a["tool_choice"]["type"], "auto");
1809 assert_eq!(a["tools"][0]["name"], "get_weather");
1810 assert_eq!(a["tools"][0]["input_schema"]["type"], "object");
1811 let msgs = a["messages"].as_array().unwrap();
1812 assert_eq!(msgs.len(), 3);
1813 assert_eq!(msgs[0]["role"], "user");
1814 assert_eq!(msgs[0]["content"][0]["text"], "what is the weather in SF?");
1815 assert_eq!(msgs[1]["role"], "assistant");
1816 assert_eq!(msgs[1]["content"][1]["type"], "tool_use");
1817 assert_eq!(msgs[1]["content"][1]["name"], "get_weather");
1818 assert_eq!(msgs[1]["content"][1]["input"]["city"], "SF");
1819 let call_id = msgs[1]["content"][1]["id"].as_str().unwrap();
1820 assert_eq!(msgs[2]["content"][0]["type"], "tool_result");
1821 assert_eq!(msgs[2]["content"][0]["tool_use_id"], call_id);
1822 assert!(msgs[2]["content"][0]["content"][0]["text"]
1823 .as_str()
1824 .unwrap()
1825 .contains("18"));
1826 }
1827
1828 #[test]
1829 fn gemini_to_anthropic_defaults() {
1830 let a = gemini_to_anthropic(&json!({
1831 "contents": [{"parts": [{"text": "hi"}]}]
1832 }));
1833 assert_eq!(a["max_tokens"], 8192);
1834 assert_eq!(a["messages"][0]["role"], "user");
1835 assert!(a.get("system").is_none());
1836 assert!(a.get("tools").is_none());
1837 }
1838
1839 #[test]
1840 fn anthropic_resp_to_gemini_text_and_tools() {
1841 let resp = json!({
1842 "id": "msg_1",
1843 "content": [
1844 {"type": "text", "text": "PONG"},
1845 {"type": "tool_use", "id": "t1", "name": "get_weather", "input": {"city": "SF"}}
1846 ],
1847 "stop_reason": "end_turn",
1848 "usage": {"input_tokens": 10, "output_tokens": 3, "cache_read_input_tokens": 4}
1849 });
1850 let g = anthropic_response_to_gemini(&resp, "gpt-5.5");
1851 assert_eq!(g["candidates"][0]["content"]["role"], "model");
1852 assert_eq!(g["candidates"][0]["content"]["parts"][0]["text"], "PONG");
1853 assert_eq!(
1854 g["candidates"][0]["content"]["parts"][1]["functionCall"]["name"],
1855 "get_weather"
1856 );
1857 assert_eq!(g["candidates"][0]["finishReason"], "STOP");
1858 assert_eq!(g["usageMetadata"]["promptTokenCount"], 14);
1859 assert_eq!(g["usageMetadata"]["candidatesTokenCount"], 3);
1860 assert_eq!(g["usageMetadata"]["cachedContentTokenCount"], 4);
1861 assert_eq!(g["modelVersion"], "gpt-5.5");
1862
1863 let max = json!({"content": [], "stop_reason": "max_tokens", "usage": {}});
1864 let g2 = anthropic_response_to_gemini(&max, "m");
1865 assert_eq!(g2["candidates"][0]["finishReason"], "MAX_TOKENS");
1866 }
1867
1868 #[test]
1869 fn gemini_sse_synth_round_trip() {
1870 let resp = anthropic_response_to_gemini(
1871 &json!({
1872 "content": [{"type": "text", "text": "PONG"}],
1873 "stop_reason": "end_turn",
1874 "usage": {"input_tokens": 5, "output_tokens": 1}
1875 }),
1876 "gpt-5.5",
1877 );
1878 let sse = synth_gemini_sse(&resp);
1879 assert!(sse.starts_with("data: "));
1880 assert!(!sse.contains("[DONE]"));
1881 let frames: Vec<Value> = sse_datas(&sse).collect();
1882 assert_eq!(frames.len(), 2);
1883 assert_eq!(
1884 frames[0]["candidates"][0]["content"]["parts"][0]["text"],
1885 "PONG"
1886 );
1887 assert_eq!(frames[1]["candidates"][0]["finishReason"], "STOP");
1888 assert_eq!(frames[1]["usageMetadata"]["promptTokenCount"], 5);
1889 assert_eq!(
1890 assistant_reply_text("gemini", &sse).as_deref(),
1891 Some("PONG")
1892 );
1893 }
1894
1895 #[test]
1896 fn anthropic_to_gemini_request_round_trip() {
1897 let a = json!({
1898 "model": "gemini-2.5-flash",
1899 "system": "be terse",
1900 "max_tokens": 256,
1901 "temperature": 0.3,
1902 "messages": [
1903 {"role": "user", "content": "weather?"},
1904 {"role": "assistant", "content": [
1905 {"type": "tool_use", "id": "tu1", "name": "get_weather", "input": {"city": "SF"}}
1906 ]},
1907 {"role": "user", "content": [
1908 {"type": "tool_result", "tool_use_id": "tu1", "content": [{"type": "text", "text": "18C"}]}
1909 ]}
1910 ],
1911 "tools": [{"name": "get_weather", "description": "w", "input_schema": {"type": "object"}}]
1912 });
1913 let g = anthropic_to_gemini_request(&a);
1914 assert_eq!(g["systemInstruction"]["parts"][0]["text"], "be terse");
1915 assert_eq!(g["generationConfig"]["maxOutputTokens"], 256);
1916 assert_eq!(g["generationConfig"]["temperature"], 0.3);
1917 assert_eq!(g["tools"][0]["functionDeclarations"][0]["name"], "get_weather");
1918 let c = g["contents"].as_array().unwrap();
1919 assert_eq!(c[0]["role"], "user");
1920 assert_eq!(c[0]["parts"][0]["text"], "weather?");
1921 assert_eq!(c[1]["role"], "model");
1922 assert_eq!(c[1]["parts"][0]["functionCall"]["name"], "get_weather");
1923 assert_eq!(c[2]["parts"][0]["functionResponse"]["name"], "get_weather");
1924 assert_eq!(c[2]["parts"][0]["functionResponse"]["response"]["result"], "18C");
1925 }
1926
1927 #[test]
1928 fn gemini_response_to_anthropic_basic() {
1929 let g = json!({
1930 "candidates": [{
1931 "content": {"role": "model", "parts": [{"text": "PONG"}]},
1932 "finishReason": "STOP"
1933 }],
1934 "usageMetadata": {"promptTokenCount": 10, "candidatesTokenCount": 2, "thoughtsTokenCount": 3}
1935 });
1936 let a = gemini_response_to_anthropic(&g, "gemini-2.5-flash");
1937 assert_eq!(a["role"], "assistant");
1938 assert_eq!(a["content"][0]["text"], "PONG");
1939 assert_eq!(a["stop_reason"], "end_turn");
1940 assert_eq!(a["usage"]["input_tokens"], 10);
1941 assert_eq!(a["usage"]["output_tokens"], 5);
1942 }
1943
1944 #[test]
1945 fn gemini_upstream_final_unwraps_envelope_and_sse() {
1946 let wrapped = json!({
1948 "response": {
1949 "candidates": [{"content": {"role": "model", "parts": [{"text": "hi"}]}, "finishReason": "STOP"}],
1950 "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 1}
1951 }
1952 });
1953 let final_v = parse_gemini_upstream_final(&wrapped.to_string()).unwrap();
1954 assert_eq!(final_v["candidates"][0]["content"]["parts"][0]["text"], "hi");
1955 assert_eq!(final_v["usageMetadata"]["promptTokenCount"], 1);
1956
1957 let sse = "data: {\"response\":{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"PO\"}]}}]}}\n\n\
1959 data: {\"response\":{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"NG\"}]},\"finishReason\":\"STOP\"}],\"usageMetadata\":{\"promptTokenCount\":5,\"candidatesTokenCount\":1}}}\n\n";
1960 let final_sse = parse_gemini_upstream_final(sse).unwrap();
1961 assert_eq!(final_sse["candidates"][0]["content"]["parts"][0]["text"], "PONG");
1962 assert_eq!(final_sse["candidates"][0]["finishReason"], "STOP");
1963 assert_eq!(final_sse["usageMetadata"]["candidatesTokenCount"], 1);
1964 }
1965
1966 #[test]
1967 fn gemini_last_user_and_reply_text() {
1968 assert_eq!(
1969 last_user_text("gemini", &gemini_req()).as_deref(),
1970 Some("[tool result] {\"temp\":18}")
1971 );
1972 let plain = json!({
1973 "candidates": [{"content": {"role": "model", "parts": [{"text": "hello"}]}}]
1974 });
1975 assert_eq!(
1976 assistant_reply_text("gemini", &plain.to_string()).as_deref(),
1977 Some("hello")
1978 );
1979 }
1980
1981 use super::*;
1982
1983 #[test]
1984 fn chat_to_anthropic_basic() {
1985 let req = json!({
1986 "model": "claude-sonnet-4-5",
1987 "messages": [
1988 {"role": "system", "content": "be brief"},
1989 {"role": "system", "content": [{"type": "text", "text": "and kind"}]},
1990 {"role": "user", "content": [
1991 {"type": "text", "text": "hi"},
1992 {"type": "image_url", "image_url": {"url": "http://x"}},
1993 ]},
1994 ],
1995 "max_completion_tokens": 512,
1996 "temperature": 0.5,
1997 "stop": "END",
1998 "stream": true,
1999 });
2000 let out = openai_chat_to_anthropic(&req);
2001 assert_eq!(out["system"], "be brief\n\nand kind");
2002 assert_eq!(out["messages"][0]["role"], "user");
2003 assert_eq!(out["messages"][0]["content"], "hi");
2004 assert_eq!(out["max_tokens"], 512);
2005 assert_eq!(out["temperature"], 0.5);
2006 assert_eq!(out["stop_sequences"], json!(["END"]));
2007 assert_eq!(out["stream"], true);
2008 assert!(out.get("tools").is_none());
2009 }
2010
2011 #[test]
2012 fn chat_to_anthropic_tools_round_trip() {
2013 let req = json!({
2014 "model": "gpt-5.1",
2015 "messages": [
2016 {"role": "user", "content": "weather?"},
2017 {"role": "assistant", "content": null, "tool_calls": [
2018 {"id": "call_1", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\":\"SF\"}"}},
2019 ]},
2020 {"role": "tool", "tool_call_id": "call_1", "content": "sunny"},
2021 ],
2022 "tools": [
2023 {"type": "function", "function": {"name": "get_weather", "description": "d", "parameters": {"type": "object"}}},
2024 ],
2025 "tool_choice": {"type": "function", "function": {"name": "get_weather"}},
2026 });
2027 let out = openai_chat_to_anthropic(&req);
2028 let asst = &out["messages"][1];
2029 assert_eq!(asst["content"][0]["type"], "tool_use");
2030 assert_eq!(asst["content"][0]["id"], "call_1");
2031 assert_eq!(asst["content"][0]["input"], json!({"city": "SF"}));
2032 let result = &out["messages"][2];
2033 assert_eq!(result["role"], "user");
2034 assert_eq!(result["content"][0]["type"], "tool_result");
2035 assert_eq!(result["content"][0]["tool_use_id"], "call_1");
2036 assert_eq!(result["content"][0]["content"][0]["text"], "sunny");
2037 assert_eq!(out["tools"][0]["name"], "get_weather");
2038 assert_eq!(out["tools"][0]["input_schema"], json!({"type": "object"}));
2039 assert_eq!(out["tool_choice"], json!({"type": "tool", "name": "get_weather"}));
2040 assert_eq!(out["max_tokens"], 8192);
2041 }
2042
2043 #[test]
2044 fn chat_tool_choice_auto_and_none() {
2045 let auto = openai_chat_to_anthropic(&json!({"messages": [], "tool_choice": "auto"}));
2046 assert_eq!(auto["tool_choice"], json!({"type": "auto"}));
2047 let none = openai_chat_to_anthropic(&json!({"messages": [], "tool_choice": "none"}));
2048 assert!(none.get("tool_choice").is_none());
2049 }
2050
2051 #[test]
2052 fn responses_to_anthropic() {
2053 let req = json!({
2054 "model": "claude-opus-4-8",
2055 "instructions": "sys",
2056 "input": [
2057 {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "hi"}]},
2058 {"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "checking"}]},
2059 {"type": "function_call", "call_id": "c1", "name": "f", "arguments": "{\"a\":1}"},
2060 {"type": "function_call_output", "call_id": "c1", "output": "42"},
2061 ],
2062 "tools": [{"type": "function", "name": "f", "description": "d", "parameters": {"type": "object"}}],
2063 "max_output_tokens": 100,
2064 "stream": true,
2065 });
2066 let out = openai_responses_to_anthropic(&req);
2067 assert_eq!(out["system"], "sys");
2068 assert_eq!(out["messages"][0], json!({"role": "user", "content": "hi"}));
2069 assert_eq!(out["messages"][1]["role"], "assistant");
2070 assert_eq!(out["messages"][2]["content"][0]["type"], "tool_use");
2071 assert_eq!(out["messages"][2]["content"][0]["id"], "c1");
2072 assert_eq!(out["messages"][2]["content"][0]["input"], json!({"a": 1}));
2073 assert_eq!(out["messages"][3]["content"][0]["type"], "tool_result");
2074 assert_eq!(out["messages"][3]["content"][0]["content"][0]["text"], "42");
2075 assert_eq!(out["tools"][0]["input_schema"], json!({"type": "object"}));
2076 assert_eq!(out["max_tokens"], 100);
2077 assert_eq!(out["stream"], true);
2078 }
2079
2080 #[test]
2081 fn responses_to_anthropic_string_input() {
2082 let out = openai_responses_to_anthropic(&json!({"model": "m", "input": "hello"}));
2083 assert_eq!(out["messages"][0], json!({"role": "user", "content": "hello"}));
2084 assert_eq!(out["max_tokens"], 8192);
2085 assert!(out.get("system").is_none());
2086 }
2087
2088 #[test]
2089 fn anthropic_to_responses() {
2090 let req = json!({
2091 "model": "gpt-5.5",
2092 "system": [{"type": "text", "text": "sys"}],
2093 "messages": [
2094 {"role": "user", "content": "hi"},
2095 {"role": "assistant", "content": [
2096 {"type": "text", "text": "using tool"},
2097 {"type": "tool_use", "id": "t1", "name": "f", "input": {"a": 1}},
2098 ]},
2099 {"role": "user", "content": [
2100 {"type": "tool_result", "tool_use_id": "t1", "content": [{"type": "text", "text": "ok"}]},
2101 ]},
2102 ],
2103 "tools": [{"name": "f", "description": "d", "input_schema": {"type": "object"}}],
2104 "max_tokens": 256,
2105 "stream": true,
2106 });
2107 let out = anthropic_to_openai_responses(&req);
2108 assert_eq!(out["instructions"], "sys");
2109 assert_eq!(out["input"][0]["content"][0]["type"], "input_text");
2110 assert_eq!(out["input"][1]["content"][0]["type"], "output_text");
2111 assert_eq!(out["input"][2]["type"], "function_call");
2112 assert_eq!(out["input"][2]["call_id"], "t1");
2113 assert_eq!(out["input"][2]["arguments"], "{\"a\":1}");
2114 assert_eq!(out["input"][3]["type"], "function_call_output");
2115 assert_eq!(out["input"][3]["output"], "ok");
2116 assert_eq!(out["tools"][0]["type"], "function");
2117 assert_eq!(out["tools"][0]["parameters"], json!({"type": "object"}));
2118 assert_eq!(out["tools"][0]["strict"], false);
2119 assert_eq!(out["max_output_tokens"], 256);
2120 assert_eq!(out["stream"], true);
2121 }
2122
2123 #[test]
2124 fn anthropic_to_chat_basic() {
2125 let req = json!({
2126 "model": "grok-4",
2127 "system": "be brief",
2128 "messages": [
2129 {"role": "user", "content": "hi"},
2130 ],
2131 "max_tokens": 512,
2132 "temperature": 0.5,
2133 "top_p": 0.9,
2134 "stop_sequences": ["END"],
2135 "stream": true,
2136 "thinking": {"type": "enabled", "budget_tokens": 4096},
2137 });
2138 let out = anthropic_to_openai_chat(&req);
2139 assert_eq!(out["model"], "grok-4");
2140 assert_eq!(out["messages"][0], json!({"role": "system", "content": "be brief"}));
2141 assert_eq!(out["messages"][1], json!({"role": "user", "content": "hi"}));
2142 assert_eq!(out["max_tokens"], 512);
2143 assert_eq!(out["temperature"], 0.5);
2144 assert_eq!(out["top_p"], 0.9);
2145 assert_eq!(out["stop"], json!(["END"]));
2146 assert_eq!(out["stream"], true);
2147 assert!(out.get("thinking").is_none());
2148 assert!(out.get("tools").is_none());
2149 }
2150
2151 #[test]
2152 fn anthropic_to_chat_tools_round_trip() {
2153 let req = json!({
2154 "model": "grok-4",
2155 "system": [{"type": "text", "text": "sys"}],
2156 "messages": [
2157 {"role": "user", "content": "weather?"},
2158 {"role": "assistant", "content": [
2159 {"type": "text", "text": "checking"},
2160 {"type": "tool_use", "id": "call_1", "name": "get_weather", "input": {"city": "SF"}},
2161 ]},
2162 {"role": "user", "content": [
2163 {"type": "tool_result", "tool_use_id": "call_1",
2164 "content": [{"type": "text", "text": "sunny"}]},
2165 ]},
2166 ],
2167 "tools": [{
2168 "name": "get_weather",
2169 "description": "d",
2170 "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}},
2171 }],
2172 "tool_choice": {"type": "tool", "name": "get_weather"},
2173 "max_tokens": 256,
2174 });
2175 let out = anthropic_to_openai_chat(&req);
2176 assert_eq!(out["messages"][0]["role"], "system");
2177 assert_eq!(out["messages"][0]["content"], "sys");
2178 assert_eq!(out["messages"][1]["content"], "weather?");
2179 let asst = &out["messages"][2];
2180 assert_eq!(asst["role"], "assistant");
2181 assert_eq!(asst["content"], "checking");
2182 assert_eq!(asst["tool_calls"][0]["id"], "call_1");
2183 assert_eq!(asst["tool_calls"][0]["function"]["name"], "get_weather");
2184 assert_eq!(asst["tool_calls"][0]["function"]["arguments"], "{\"city\":\"SF\"}");
2185 let tool = &out["messages"][3];
2186 assert_eq!(tool["role"], "tool");
2187 assert_eq!(tool["tool_call_id"], "call_1");
2188 assert_eq!(tool["content"], "sunny");
2189 assert_eq!(out["tools"][0]["type"], "function");
2190 assert_eq!(out["tools"][0]["function"]["name"], "get_weather");
2191 assert_eq!(
2192 out["tools"][0]["function"]["parameters"],
2193 json!({"type": "object", "properties": {"city": {"type": "string"}}})
2194 );
2195 assert_eq!(
2196 out["tool_choice"],
2197 json!({"type": "function", "function": {"name": "get_weather"}})
2198 );
2199 assert_eq!(out["max_tokens"], 256);
2200
2201 let back = openai_chat_to_anthropic(&out);
2203 assert_eq!(back["messages"][1]["content"][1]["type"], "tool_use");
2204 assert_eq!(back["messages"][1]["content"][1]["id"], "call_1");
2205 assert_eq!(back["messages"][1]["content"][1]["input"], json!({"city": "SF"}));
2206 assert_eq!(back["messages"][2]["content"][0]["type"], "tool_result");
2207 assert_eq!(back["messages"][2]["content"][0]["tool_use_id"], "call_1");
2208 }
2209
2210 #[test]
2211 fn anthropic_to_chat_tool_choice_variants() {
2212 let auto = anthropic_to_openai_chat(&json!({
2213 "messages": [], "tool_choice": {"type": "auto"}
2214 }));
2215 assert_eq!(auto["tool_choice"], "auto");
2216 let any = anthropic_to_openai_chat(&json!({
2217 "messages": [], "tool_choice": {"type": "any"}
2218 }));
2219 assert_eq!(any["tool_choice"], "required");
2220 let none = anthropic_to_openai_chat(&json!({
2221 "messages": [], "tool_choice": {"type": "none"}
2222 }));
2223 assert_eq!(none["tool_choice"], "none");
2224 }
2225
2226 fn anthropic_resp() -> Value {
2227 json!({
2228 "id": "msg_01",
2229 "type": "message",
2230 "role": "assistant",
2231 "content": [
2232 {"type": "text", "text": "hi "},
2233 {"type": "text", "text": "there"},
2234 {"type": "tool_use", "id": "t1", "name": "f", "input": {"a": 1}},
2235 ],
2236 "stop_reason": "tool_use",
2237 "usage": {"input_tokens": 10, "output_tokens": 5, "cache_read_input_tokens": 3},
2238 })
2239 }
2240
2241 #[test]
2242 fn anthropic_resp_to_chat() {
2243 let out = anthropic_response_to_openai_chat(&anthropic_resp(), "m");
2244 assert_eq!(out["id"], "chatcmpl-msg_01");
2245 assert_eq!(out["object"], "chat.completion");
2246 assert_eq!(out["model"], "m");
2247 let msg = &out["choices"][0]["message"];
2248 assert_eq!(msg["content"], "hi there");
2249 assert_eq!(msg["tool_calls"][0]["id"], "t1");
2250 assert_eq!(msg["tool_calls"][0]["function"]["arguments"], "{\"a\":1}");
2251 assert_eq!(out["choices"][0]["finish_reason"], "tool_calls");
2252 assert_eq!(out["usage"]["prompt_tokens"], 10);
2253 assert_eq!(out["usage"]["completion_tokens"], 5);
2254 assert_eq!(out["usage"]["total_tokens"], 15);
2255 assert_eq!(out["usage"]["prompt_tokens_details"]["cached_tokens"], 3);
2256 }
2257
2258 #[test]
2259 fn chat_resp_to_anthropic() {
2260 let chat = json!({
2261 "id": "chatcmpl-abc",
2262 "object": "chat.completion",
2263 "model": "grok-4",
2264 "choices": [{
2265 "index": 0,
2266 "message": {
2267 "role": "assistant",
2268 "content": "hi there",
2269 "tool_calls": [{
2270 "id": "t1",
2271 "type": "function",
2272 "function": {"name": "f", "arguments": "{\"a\":1}"}
2273 }],
2274 },
2275 "finish_reason": "tool_calls",
2276 }],
2277 "usage": {
2278 "prompt_tokens": 10,
2279 "completion_tokens": 5,
2280 "total_tokens": 15,
2281 "prompt_tokens_details": {"cached_tokens": 3},
2282 },
2283 });
2284 let out = openai_chat_response_to_anthropic(&chat, "grok-4");
2285 assert_eq!(out["id"], "msg_abc");
2286 assert_eq!(out["type"], "message");
2287 assert_eq!(out["role"], "assistant");
2288 assert_eq!(out["model"], "grok-4");
2289 assert_eq!(out["content"][0], json!({"type": "text", "text": "hi there"}));
2290 assert_eq!(out["content"][1]["type"], "tool_use");
2291 assert_eq!(out["content"][1]["id"], "t1");
2292 assert_eq!(out["content"][1]["name"], "f");
2293 assert_eq!(out["content"][1]["input"], json!({"a": 1}));
2294 assert_eq!(out["stop_reason"], "tool_use");
2295 assert_eq!(out["usage"]["input_tokens"], 10);
2296 assert_eq!(out["usage"]["output_tokens"], 5);
2297 assert_eq!(out["usage"]["cache_read_input_tokens"], 3);
2298
2299 let stop = json!({
2300 "id": "chatcmpl-x",
2301 "choices": [{"message": {"role": "assistant", "content": "done"}, "finish_reason": "stop"}],
2302 "usage": {"prompt_tokens": 1, "completion_tokens": 1},
2303 });
2304 assert_eq!(
2305 openai_chat_response_to_anthropic(&stop, "m")["stop_reason"],
2306 "end_turn"
2307 );
2308 let len = json!({
2309 "id": "chatcmpl-y",
2310 "choices": [{"message": {"role": "assistant", "content": "cut"}, "finish_reason": "length"}],
2311 "usage": {},
2312 });
2313 assert_eq!(
2314 openai_chat_response_to_anthropic(&len, "m")["stop_reason"],
2315 "max_tokens"
2316 );
2317
2318 let round = openai_chat_response_to_anthropic(
2320 &anthropic_response_to_openai_chat(&anthropic_resp(), "m"),
2321 "m",
2322 );
2323 assert_eq!(round["content"][0]["text"], "hi there");
2324 assert_eq!(round["content"][1]["input"], json!({"a": 1}));
2325 assert_eq!(round["stop_reason"], "tool_use");
2326 }
2327
2328 #[test]
2329 fn anthropic_resp_to_responses() {
2330 let out = anthropic_response_to_openai_responses(&anthropic_resp(), "m");
2331 assert_eq!(out["id"], "resp_msg_01");
2332 assert_eq!(out["status"], "completed");
2333 assert_eq!(out["output"][0]["type"], "message");
2334 assert_eq!(out["output"][0]["content"][0]["type"], "output_text");
2335 assert_eq!(out["output"][0]["content"][0]["text"], "hi ");
2336 assert_eq!(out["output"][2]["type"], "function_call");
2337 assert_eq!(out["output"][2]["call_id"], "t1");
2338 assert_eq!(out["output"][2]["arguments"], "{\"a\":1}");
2339 assert_eq!(out["usage"]["input_tokens"], 10);
2340 assert_eq!(out["usage"]["total_tokens"], 15);
2341 assert_eq!(out["usage"]["input_tokens_details"]["cached_tokens"], 3);
2342 let mut capped = anthropic_resp();
2343 capped["stop_reason"] = json!("max_tokens");
2344 assert_eq!(anthropic_response_to_openai_responses(&capped, "m")["status"], "incomplete");
2345 }
2346
2347 fn responses_resp() -> Value {
2348 json!({
2349 "id": "r1",
2350 "object": "response",
2351 "status": "completed",
2352 "output": [
2353 {"type": "reasoning", "id": "rs1", "summary": []},
2354 {"type": "message", "id": "m1", "role": "assistant", "status": "completed",
2355 "content": [{"type": "output_text", "text": "hello", "annotations": []}]},
2356 {"type": "function_call", "id": "fc1", "call_id": "c1", "name": "f", "arguments": "{\"a\":1}"},
2357 ],
2358 "usage": {"input_tokens": 7, "output_tokens": 2, "input_tokens_details": {"cached_tokens": 4}},
2359 })
2360 }
2361
2362 #[test]
2363 fn responses_to_anthropic_resp() {
2364 let out = responses_final_to_anthropic(&responses_resp(), "m");
2365 assert_eq!(out["id"], "msg_r1");
2366 assert_eq!(out["type"], "message");
2367 assert_eq!(out["content"][0], json!({"type": "text", "text": "hello"}));
2368 assert_eq!(out["content"][1]["type"], "tool_use");
2369 assert_eq!(out["content"][1]["id"], "c1");
2370 assert_eq!(out["content"][1]["input"], json!({"a": 1}));
2371 assert_eq!(out["stop_reason"], "tool_use");
2372 assert_eq!(out["usage"]["input_tokens"], 7);
2373 assert_eq!(out["usage"]["cache_read_input_tokens"], 4);
2374 let mut inc = responses_resp();
2375 inc["status"] = json!("incomplete");
2376 assert_eq!(responses_final_to_anthropic(&inc, "m")["stop_reason"], "max_tokens");
2377 let mut plain = responses_resp();
2378 plain["output"].as_array_mut().unwrap().pop();
2379 assert_eq!(responses_final_to_anthropic(&plain, "m")["stop_reason"], "end_turn");
2380 }
2381
2382 #[test]
2383 fn responses_to_chat_resp() {
2384 let out = responses_final_to_openai_chat(&responses_resp(), "m");
2385 assert_eq!(out["id"], "chatcmpl-r1");
2386 let msg = &out["choices"][0]["message"];
2387 assert_eq!(msg["content"], "hello");
2388 assert_eq!(msg["tool_calls"][0]["id"], "c1");
2389 assert_eq!(msg["tool_calls"][0]["function"]["arguments"], "{\"a\":1}");
2390 assert_eq!(out["choices"][0]["finish_reason"], "tool_calls");
2391 assert_eq!(out["usage"]["prompt_tokens"], 7);
2392 assert_eq!(out["usage"]["total_tokens"], 9);
2393 assert_eq!(out["usage"]["prompt_tokens_details"]["cached_tokens"], 4);
2394 }
2395
2396 #[test]
2397 fn anthropic_sse_reassembly() {
2398 let sse = concat!(
2399 "event: message_start\n",
2400 "data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_01\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"usage\":{\"input_tokens\":10,\"output_tokens\":1}}}\n\n",
2401 "event: content_block_start\n",
2402 "data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n",
2403 "event: content_block_delta\n",
2404 "data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"hel\"}}\n\n",
2405 "event: content_block_delta\n",
2406 "data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"lo\"}}\n\n",
2407 "event: content_block_stop\n",
2408 "data: {\"type\":\"content_block_stop\",\"index\":0}\n\n",
2409 "event: content_block_start\n",
2410 "data: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"t1\",\"name\":\"f\",\"input\":{}}}\n\n",
2411 "event: content_block_delta\n",
2412 "data: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"a\\\":\"}}\n\n",
2413 "event: content_block_delta\n",
2414 "data: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"1}\"}}\n\n",
2415 "event: content_block_stop\n",
2416 "data: {\"type\":\"content_block_stop\",\"index\":1}\n\n",
2417 "event: message_delta\n",
2418 "data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null},\"usage\":{\"output_tokens\":25}}\n\n",
2419 "event: message_stop\n",
2420 "data: {\"type\":\"message_stop\"}\n\n",
2421 );
2422 let m = parse_anthropic_sse_to_message(sse).unwrap();
2423 assert_eq!(m["content"][0]["text"], "hello");
2424 assert_eq!(m["content"][1]["type"], "tool_use");
2425 assert_eq!(m["content"][1]["input"], json!({"a": 1}));
2426 assert_eq!(m["stop_reason"], "tool_use");
2427 assert_eq!(m["usage"]["input_tokens"], 10);
2428 assert_eq!(m["usage"]["output_tokens"], 25);
2429 assert!(parse_anthropic_sse_to_message("data: {\"type\":\"ping\"}\n\n").is_none());
2430 }
2431
2432 #[test]
2433 fn responses_sse_final() {
2434 let sse = concat!(
2435 "event: response.created\n",
2436 "data: {\"type\":\"response.created\",\"response\":{\"id\":\"r1\",\"status\":\"in_progress\"}}\n\n",
2437 "event: response.output_text.delta\n",
2438 "data: {\"type\":\"response.output_text.delta\",\"delta\":\"hi\"}\n\n",
2439 "event: response.completed\n",
2440 "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"r1\",\"status\":\"completed\",\"output\":[]}}\n\n",
2441 );
2442 let r = parse_responses_sse_final(sse).unwrap();
2443 assert_eq!(r["id"], "r1");
2444 assert_eq!(r["status"], "completed");
2445 assert!(parse_responses_sse_final("data: {\"type\":\"response.created\"}\n\n").is_none());
2446 }
2447
2448 #[test]
2449 fn chat_sse_synth() {
2450 let chat = anthropic_response_to_openai_chat(&anthropic_resp(), "m");
2451 let sse = synth_openai_chat_sse(&chat);
2452 let chunks: Vec<Value> = sse
2453 .lines()
2454 .filter_map(|l| l.strip_prefix("data: "))
2455 .filter(|d| *d != "[DONE]")
2456 .map(|d| serde_json::from_str(d).unwrap())
2457 .collect();
2458 assert_eq!(chunks[0]["choices"][0]["delta"]["role"], "assistant");
2459 assert_eq!(chunks[0]["object"], "chat.completion.chunk");
2460 assert_eq!(chunks[1]["choices"][0]["delta"]["content"], "hi there");
2461 assert_eq!(chunks[2]["choices"][0]["delta"]["tool_calls"][0]["index"], 0);
2462 assert_eq!(chunks[2]["choices"][0]["delta"]["tool_calls"][0]["id"], "t1");
2463 let last = chunks.last().unwrap();
2464 assert_eq!(last["choices"][0]["finish_reason"], "tool_calls");
2465 assert_eq!(last["usage"]["total_tokens"], 15);
2466 assert!(sse.ends_with("data: [DONE]\n\n"));
2467 }
2468
2469 #[test]
2470 fn chat_sse_parse_and_anthropic_resynth() {
2471 let chat = json!({
2473 "id": "chatcmpl-xyz",
2474 "object": "chat.completion",
2475 "model": "grok-4",
2476 "choices": [{
2477 "index": 0,
2478 "message": {
2479 "role": "assistant",
2480 "content": "hello",
2481 "tool_calls": [{
2482 "id": "t1",
2483 "type": "function",
2484 "function": {"name": "f", "arguments": "{\"a\":1}"}
2485 }],
2486 },
2487 "finish_reason": "tool_calls",
2488 }],
2489 "usage": {
2490 "prompt_tokens": 8,
2491 "completion_tokens": 4,
2492 "total_tokens": 12,
2493 "prompt_tokens_details": {"cached_tokens": 2},
2494 },
2495 });
2496 let sse = synth_openai_chat_sse(&chat);
2497 let parsed = parse_openai_chat_sse_final(&sse).unwrap();
2498 assert_eq!(parsed["id"], "chatcmpl-xyz");
2499 assert_eq!(parsed["choices"][0]["message"]["content"], "hello");
2500 assert_eq!(
2501 parsed["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"],
2502 "{\"a\":1}"
2503 );
2504 assert_eq!(parsed["choices"][0]["finish_reason"], "tool_calls");
2505 assert_eq!(parsed["usage"]["prompt_tokens"], 8);
2506
2507 let anth = openai_chat_response_to_anthropic(&parsed, "grok-4");
2508 assert_eq!(anth["content"][0]["text"], "hello");
2509 assert_eq!(anth["content"][1]["input"], json!({"a": 1}));
2510 assert_eq!(anth["stop_reason"], "tool_use");
2511 assert_eq!(anth["usage"]["input_tokens"], 8);
2512 assert_eq!(anth["usage"]["cache_read_input_tokens"], 2);
2513
2514 let anth_sse = synth_anthropic_sse(&anth);
2515 assert!(anth_sse.starts_with("event: message_start\n"));
2516 assert!(anth_sse.contains("event: content_block_delta\n"));
2517 assert!(anth_sse.contains("event: message_stop\n"));
2518 let reassembled = parse_anthropic_sse_to_message(&anth_sse).unwrap();
2519 assert_eq!(reassembled["content"][0]["text"], "hello");
2520 assert_eq!(reassembled["content"][1]["input"], json!({"a": 1}));
2521 assert_eq!(reassembled["stop_reason"], "tool_use");
2522 assert_eq!(reassembled["usage"]["output_tokens"], 4);
2523
2524 let text_sse = concat!(
2526 "data: {\"id\":\"chatcmpl-t\",\"object\":\"chat.completion.chunk\",\"model\":\"m\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"},\"finish_reason\":null}]}\n\n",
2527 "data: {\"id\":\"chatcmpl-t\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"PO\"},\"finish_reason\":null}]}\n\n",
2528 "data: {\"id\":\"chatcmpl-t\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"NG\"},\"finish_reason\":null}]}\n\n",
2529 "data: {\"id\":\"chatcmpl-t\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":3,\"completion_tokens\":1,\"total_tokens\":4}}\n\n",
2530 "data: [DONE]\n\n",
2531 );
2532 let t = parse_openai_chat_sse_final(text_sse).unwrap();
2533 assert_eq!(t["choices"][0]["message"]["content"], "PONG");
2534 assert_eq!(t["choices"][0]["finish_reason"], "stop");
2535 assert_eq!(t["usage"]["prompt_tokens"], 3);
2536 let a = openai_chat_response_to_anthropic(&t, "m");
2537 assert_eq!(a["content"][0]["text"], "PONG");
2538 assert_eq!(a["stop_reason"], "end_turn");
2539 assert!(parse_openai_chat_sse_final("data: {\"type\":\"ping\"}\n\n").is_none());
2540 }
2541
2542 #[test]
2543 fn responses_sse_synth() {
2544 let sse = synth_openai_responses_sse(&responses_resp());
2545 assert!(sse.starts_with("event: response.created\n"));
2546 assert!(sse.contains("event: response.output_item.added\n"));
2547 assert!(sse.contains("event: response.output_text.delta\n"));
2548 assert!(sse.contains("event: response.output_text.done\n"));
2549 assert!(sse.contains("event: response.completed\n"));
2550 let fin = parse_responses_sse_final(&sse).unwrap();
2551 assert_eq!(fin, responses_resp());
2552 }
2553
2554 #[test]
2555 fn anthropic_sse_synth() {
2556 let sse = synth_anthropic_sse(&anthropic_resp());
2557 assert!(sse.starts_with("event: message_start\n"));
2558 assert!(sse.contains("event: content_block_start\n"));
2559 assert!(sse.contains("event: message_stop\n"));
2560 let m = parse_anthropic_sse_to_message(&sse).unwrap();
2561 assert_eq!(m["content"][0]["text"], "hi ");
2562 assert_eq!(m["content"][2]["input"], json!({"a": 1}));
2563 assert_eq!(m["stop_reason"], "tool_use");
2564 assert_eq!(m["usage"]["input_tokens"], 10);
2565 assert_eq!(m["usage"]["output_tokens"], 5);
2566 }
2567
2568 #[test]
2569 fn codex_normalize() {
2570 let mut req = json!({
2571 "model": "gpt-5.1-codex",
2572 "input": [],
2573 "temperature": 0.7,
2574 "top_p": 0.9,
2575 "max_output_tokens": 100,
2576 "max_tokens": 100,
2577 "max_completion_tokens": 100,
2578 "truncation": "auto",
2579 "user": "u",
2580 "safety_identifier": "s",
2581 "prompt_cache_retention": "24h",
2582 "context_management": {},
2583 "reasoning": {"effort": "high"},
2584 "text": {"verbosity": "low"},
2585 "prompt_cache_key": "k",
2586 "service_tier": "flex",
2587 "tool_choice": "none",
2588 });
2589 normalize_codex_request(&mut req);
2590 assert_eq!(req["store"], false);
2591 assert_eq!(req["stream"], true);
2592 assert_eq!(req["tool_choice"], "none");
2593 assert_eq!(req["parallel_tool_calls"], true);
2594 assert_eq!(req["include"], json!(["reasoning.encrypted_content"]));
2595 assert_eq!(req["reasoning"]["effort"], "high");
2596 assert_eq!(req["text"]["verbosity"], "low");
2597 assert_eq!(req["prompt_cache_key"], "k");
2598 assert_eq!(req["service_tier"], "flex");
2599 for k in [
2600 "temperature",
2601 "top_p",
2602 "max_output_tokens",
2603 "max_tokens",
2604 "max_completion_tokens",
2605 "truncation",
2606 "user",
2607 "safety_identifier",
2608 "prompt_cache_retention",
2609 "context_management",
2610 ] {
2611 assert!(req.get(k).is_none(), "{k} should be removed");
2612 }
2613 }
2614
2615 #[test]
2616 fn codex_normalize_defaults() {
2617 let mut req = json!({"model": "m", "input": []});
2618 normalize_codex_request(&mut req);
2619 assert_eq!(req["tool_choice"], "auto");
2620 assert_eq!(req["parallel_tool_calls"], true);
2621 }
2622
2623 #[test]
2624 fn last_user_text_anthropic() {
2625 let req = json!({"messages": [
2626 {"role": "user", "content": "first"},
2627 {"role": "assistant", "content": "reply"},
2628 {"role": "user", "content": [
2629 {"type": "text", "text": "part1"},
2630 {"type": "text", "text": "part2"},
2631 ]},
2632 ]});
2633 assert_eq!(last_user_text("anthropic", &req), Some("part1\npart2".into()));
2634 let long = "x".repeat(500);
2635 let tool = json!({"messages": [
2636 {"role": "user", "content": "q"},
2637 {"role": "assistant", "content": [{"type": "tool_use", "id": "t1", "name": "f", "input": {}}]},
2638 {"role": "user", "content": [
2639 {"type": "tool_result", "tool_use_id": "t1",
2640 "content": [{"type": "text", "text": long}]},
2641 ]},
2642 ]});
2643 let got = last_user_text("anthropic", &tool).unwrap();
2644 assert!(got.starts_with("[tool result] xxx"));
2645 assert_eq!(got.chars().count(), "[tool result] ".chars().count() + 200);
2646 assert_eq!(last_user_text("anthropic", &json!({"messages": []})), None);
2647 }
2648
2649 #[test]
2650 fn last_user_text_openai_chat() {
2651 let req = json!({"messages": [
2652 {"role": "system", "content": "s"},
2653 {"role": "user", "content": "hello"},
2654 {"role": "assistant", "content": "hi"},
2655 {"role": "user", "content": [{"type": "text", "text": "again"}]},
2656 ]});
2657 assert_eq!(last_user_text("openai-chat", &req), Some("again".into()));
2658 let tool = json!({"messages": [
2659 {"role": "user", "content": "q"},
2660 {"role": "assistant", "content": null},
2661 {"role": "tool", "tool_call_id": "c1", "content": "result body"},
2662 ]});
2663 assert_eq!(
2664 last_user_text("openai-chat", &tool),
2665 Some("[tool result] result body".into())
2666 );
2667 assert_eq!(last_user_text("openai-chat", &json!({})), None);
2668 }
2669
2670 #[test]
2671 fn last_user_text_openai_responses() {
2672 let req = json!({"input": [
2673 {"type": "message", "role": "user",
2674 "content": [{"type": "input_text", "text": "one"}]},
2675 {"type": "message", "role": "assistant",
2676 "content": [{"type": "output_text", "text": "r"}]},
2677 {"type": "message", "role": "user",
2678 "content": [{"type": "input_text", "text": "two"}]},
2679 ]});
2680 assert_eq!(last_user_text("openai-responses", &req), Some("two".into()));
2681 let tool = json!({"input": [
2682 {"type": "message", "role": "user",
2683 "content": [{"type": "input_text", "text": "q"}]},
2684 {"type": "function_call", "call_id": "c1", "name": "f", "arguments": "{}"},
2685 {"type": "function_call_output", "call_id": "c1", "output": "tool says hi"},
2686 ]});
2687 assert_eq!(
2688 last_user_text("openai-responses", &tool),
2689 Some("[tool result] tool says hi".into())
2690 );
2691 assert_eq!(
2692 last_user_text("openai-responses", &json!({"input": "raw"})),
2693 Some("raw".into())
2694 );
2695 assert_eq!(last_user_text("mystery", &json!({})), None);
2696 }
2697
2698 #[test]
2699 fn assistant_reply_anthropic_plain_and_sse() {
2700 let plain = json!({
2701 "id": "msg_01", "type": "message", "role": "assistant",
2702 "content": [
2703 {"type": "thinking", "thinking": "hmm"},
2704 {"type": "text", "text": "hello "},
2705 {"type": "text", "text": "world"},
2706 ],
2707 "stop_reason": "end_turn",
2708 });
2709 assert_eq!(
2710 assistant_reply_text("anthropic", &plain.to_string()),
2711 Some("hello world".into())
2712 );
2713 let sse = synth_anthropic_sse(&anthropic_resp());
2714 assert_eq!(
2715 assistant_reply_text("anthropic", &sse),
2716 Some("hi there".into())
2717 );
2718 assert_eq!(assistant_reply_text("anthropic", "not json"), None);
2719 }
2720
2721 #[test]
2722 fn assistant_reply_openai_chat_plain_and_sse() {
2723 let plain = json!({"choices": [{"message": {"role": "assistant", "content": "chat reply"}}]});
2724 assert_eq!(
2725 assistant_reply_text("openai-chat", &plain.to_string()),
2726 Some("chat reply".into())
2727 );
2728 let sse = concat!(
2729 "data: {\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"}}]}\n\n",
2730 "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"str\"}}]}\n\n",
2731 "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"eamed\"}}]}\n\n",
2732 "data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n",
2733 "data: [DONE]\n\n",
2734 );
2735 assert_eq!(
2736 assistant_reply_text("openai-chat", sse),
2737 Some("streamed".into())
2738 );
2739 assert_eq!(assistant_reply_text("openai-chat", "data: {}\n\n"), None);
2740 }
2741
2742 #[test]
2743 fn assistant_reply_openai_responses_plain_and_sse() {
2744 assert_eq!(
2745 assistant_reply_text("openai-responses", &responses_resp().to_string()),
2746 Some("hello".into())
2747 );
2748 let sse = synth_openai_responses_sse(&responses_resp());
2749 assert_eq!(
2750 assistant_reply_text("openai-responses", &sse),
2751 Some("hello".into())
2752 );
2753 assert_eq!(
2754 assistant_reply_text("openai-responses", "data: {\"type\":\"ping\"}\n\n"),
2755 None
2756 );
2757 }
2758}