Skip to main content

alex_core/
translate.rs

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    // Pi speaks Anthropic Messages to the Alexandria provider. Adaptive-thinking
271    // models carry Pi's selected level in output_config.effort; preserve it when
272    // translating to the OpenAI Responses API used by Codex.
273    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
280/// Anthropic Messages request → OpenAI chat completions (pivot → chat).
281/// Drops `thinking` / budget fields (no chat equivalent).
282pub 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                    // user (or other): text stays as user; tool_result → role:tool
335                    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    // thinking / budget_tokens intentionally dropped — no chat equivalent
428    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
494/// OpenAI chat completion response → Anthropic Messages shape (chat → pivot).
495pub 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
799/// Reassemble OpenAI chat.completion.chunk SSE into a final chat.completion object.
800pub 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(); // id, name, args
805    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            // non-chunk full object in a data: line
811            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        // some providers put the full message on the last chunk instead of delta
854        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        // Codex consumes completed output items, including function calls, from
1001        // this event. `response.completed` alone does not dispatch its tool loop.
1002        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        // non-stream, wrapped in code-assist "response" envelope
1999        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        // streaming SSE with response-wrapped frames
2013        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        // round-trip through openai_chat_to_anthropic preserves tool shape
2269        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        // inverse of anthropic_response_to_openai_chat
2386        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        // chat SSE deltas → final chat → anthropic → anthropic SSE
2554        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        // text-only streaming deltas
2607        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}