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