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