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
266fn stop_to_finish(stop: Option<&str>) -> &'static str {
267    match stop {
268        Some("max_tokens") => "length",
269        Some("tool_use") => "tool_calls",
270        _ => "stop",
271    }
272}
273
274pub fn anthropic_response_to_openai_chat(resp: &Value, model: &str) -> Value {
275    let mut texts = Vec::new();
276    let mut calls = Vec::new();
277    for b in resp["content"].as_array().into_iter().flatten() {
278        match b["type"].as_str() {
279            Some("text") => texts.push(b["text"].as_str().unwrap_or("").to_string()),
280            Some("tool_use") => calls.push(json!({
281                "id": b["id"],
282                "type": "function",
283                "function": {"name": b["name"], "arguments": b["input"].to_string()},
284            })),
285            _ => {}
286        }
287    }
288    let content = if texts.is_empty() {
289        Value::Null
290    } else {
291        Value::String(texts.join(""))
292    };
293    let mut msg = json!({"role": "assistant", "content": content});
294    if !calls.is_empty() {
295        msg["tool_calls"] = Value::Array(calls);
296    }
297    let u = &resp["usage"];
298    let pt = u["input_tokens"].as_i64().unwrap_or(0);
299    let ct = u["output_tokens"].as_i64().unwrap_or(0);
300    json!({
301        "id": format!("chatcmpl-{}", resp["id"].as_str().unwrap_or("")),
302        "object": "chat.completion",
303        "created": 0,
304        "model": model,
305        "choices": [{
306            "index": 0,
307            "message": msg,
308            "finish_reason": stop_to_finish(resp["stop_reason"].as_str()),
309        }],
310        "usage": {
311            "prompt_tokens": pt,
312            "completion_tokens": ct,
313            "total_tokens": pt + ct,
314            "prompt_tokens_details": {
315                "cached_tokens": u["cache_read_input_tokens"].as_i64().unwrap_or(0),
316            },
317        },
318    })
319}
320
321pub fn anthropic_response_to_openai_responses(resp: &Value, model: &str) -> Value {
322    let id = resp["id"].as_str().unwrap_or("");
323    let mut output = Vec::new();
324    for b in resp["content"].as_array().into_iter().flatten() {
325        match b["type"].as_str() {
326            Some("text") => output.push(json!({
327                "type": "message",
328                "id": format!("msg_{id}"),
329                "role": "assistant",
330                "status": "completed",
331                "content": [{"type": "output_text", "text": b["text"], "annotations": []}],
332            })),
333            Some("tool_use") => output.push(json!({
334                "type": "function_call",
335                "id": b["id"],
336                "call_id": b["id"],
337                "name": b["name"],
338                "arguments": b["input"].to_string(),
339                "status": "completed",
340            })),
341            _ => {}
342        }
343    }
344    let status = if resp["stop_reason"] == "max_tokens" {
345        "incomplete"
346    } else {
347        "completed"
348    };
349    let u = &resp["usage"];
350    let it = u["input_tokens"].as_i64().unwrap_or(0);
351    let ot = u["output_tokens"].as_i64().unwrap_or(0);
352    json!({
353        "id": format!("resp_{id}"),
354        "object": "response",
355        "status": status,
356        "model": model,
357        "output": output,
358        "usage": {
359            "input_tokens": it,
360            "output_tokens": ot,
361            "total_tokens": it + ot,
362            "input_tokens_details": {
363                "cached_tokens": u["cache_read_input_tokens"].as_i64().unwrap_or(0),
364            },
365            "output_tokens_details": {"reasoning_tokens": 0},
366        },
367    })
368}
369
370pub fn responses_final_to_anthropic(resp: &Value, model: &str) -> Value {
371    let mut content = Vec::new();
372    let mut has_call = false;
373    for it in resp["output"].as_array().into_iter().flatten() {
374        match it["type"].as_str() {
375            Some("message") => {
376                for p in it["content"].as_array().into_iter().flatten() {
377                    if p["type"] == "output_text" {
378                        content.push(json!({"type": "text", "text": p["text"]}));
379                    }
380                }
381            }
382            Some("function_call") => {
383                has_call = true;
384                content.push(json!({
385                    "type": "tool_use",
386                    "id": it["call_id"],
387                    "name": it["name"],
388                    "input": parse_args(it["arguments"].as_str().unwrap_or("{}")),
389                }));
390            }
391            _ => {}
392        }
393    }
394    let stop = if resp["status"] == "incomplete" {
395        "max_tokens"
396    } else if has_call {
397        "tool_use"
398    } else {
399        "end_turn"
400    };
401    let u = &resp["usage"];
402    json!({
403        "id": format!("msg_{}", resp["id"].as_str().unwrap_or("")),
404        "type": "message",
405        "role": "assistant",
406        "model": model,
407        "content": content,
408        "stop_reason": stop,
409        "stop_sequence": null,
410        "usage": {
411            "input_tokens": u["input_tokens"].as_i64().unwrap_or(0),
412            "output_tokens": u["output_tokens"].as_i64().unwrap_or(0),
413            "cache_read_input_tokens": u["input_tokens_details"]["cached_tokens"].as_i64().unwrap_or(0),
414        },
415    })
416}
417
418pub fn responses_final_to_openai_chat(resp: &Value, model: &str) -> Value {
419    let mut texts = Vec::new();
420    let mut calls = Vec::new();
421    for it in resp["output"].as_array().into_iter().flatten() {
422        match it["type"].as_str() {
423            Some("message") => {
424                for p in it["content"].as_array().into_iter().flatten() {
425                    if p["type"] == "output_text" {
426                        texts.push(p["text"].as_str().unwrap_or("").to_string());
427                    }
428                }
429            }
430            Some("function_call") => calls.push(json!({
431                "id": it["call_id"],
432                "type": "function",
433                "function": {"name": it["name"], "arguments": it["arguments"]},
434            })),
435            _ => {}
436        }
437    }
438    let content = if texts.is_empty() {
439        Value::Null
440    } else {
441        Value::String(texts.join(""))
442    };
443    let mut msg = json!({"role": "assistant", "content": content});
444    let finish = if resp["status"] == "incomplete" {
445        "length"
446    } else if calls.is_empty() {
447        "stop"
448    } else {
449        "tool_calls"
450    };
451    if !calls.is_empty() {
452        msg["tool_calls"] = Value::Array(calls);
453    }
454    let u = &resp["usage"];
455    let pt = u["input_tokens"].as_i64().unwrap_or(0);
456    let ct = u["output_tokens"].as_i64().unwrap_or(0);
457    json!({
458        "id": format!("chatcmpl-{}", resp["id"].as_str().unwrap_or("")),
459        "object": "chat.completion",
460        "created": 0,
461        "model": model,
462        "choices": [{"index": 0, "message": msg, "finish_reason": finish}],
463        "usage": {
464            "prompt_tokens": pt,
465            "completion_tokens": ct,
466            "total_tokens": pt + ct,
467            "prompt_tokens_details": {
468                "cached_tokens": u["input_tokens_details"]["cached_tokens"].as_i64().unwrap_or(0),
469            },
470        },
471    })
472}
473
474fn sse_datas(sse: &str) -> impl Iterator<Item = Value> + '_ {
475    sse.lines().filter_map(|l| {
476        let d = l.strip_prefix("data:")?.trim();
477        if d.is_empty() || d == "[DONE]" {
478            return None;
479        }
480        serde_json::from_str(d).ok()
481    })
482}
483
484pub fn parse_anthropic_sse_to_message(sse: &str) -> Option<Value> {
485    let mut msg: Option<Value> = None;
486    let mut blocks: Vec<Value> = Vec::new();
487    let mut partials: Vec<String> = Vec::new();
488    for v in sse_datas(sse) {
489        match v["type"].as_str() {
490            Some("message_start") => {
491                if v["message"].is_object() {
492                    msg = Some(v["message"].clone());
493                }
494            }
495            Some("content_block_start") => {
496                let i = v["index"].as_u64().unwrap_or(blocks.len() as u64) as usize;
497                while blocks.len() <= i {
498                    blocks.push(Value::Null);
499                    partials.push(String::new());
500                }
501                blocks[i] = v["content_block"].clone();
502                partials[i] = String::new();
503            }
504            Some("content_block_delta") => {
505                let i = v["index"].as_u64().unwrap_or(0) as usize;
506                if i >= blocks.len() {
507                    continue;
508                }
509                let d = &v["delta"];
510                match d["type"].as_str() {
511                    Some("text_delta") => {
512                        let t = format!(
513                            "{}{}",
514                            blocks[i]["text"].as_str().unwrap_or(""),
515                            d["text"].as_str().unwrap_or("")
516                        );
517                        blocks[i]["text"] = json!(t);
518                    }
519                    Some("input_json_delta") => {
520                        partials[i].push_str(d["partial_json"].as_str().unwrap_or(""));
521                    }
522                    _ => {}
523                }
524            }
525            Some("content_block_stop") => {
526                let i = v["index"].as_u64().unwrap_or(0) as usize;
527                if i < blocks.len() && blocks[i]["type"] == "tool_use" && !partials[i].is_empty() {
528                    blocks[i]["input"] = parse_args(&partials[i]);
529                }
530            }
531            Some("message_delta") => {
532                let Some(m) = msg.as_mut() else { continue };
533                for k in ["stop_reason", "stop_sequence"] {
534                    if !v["delta"][k].is_null() {
535                        m[k] = v["delta"][k].clone();
536                    }
537                }
538                if let Some(uo) = v["usage"].as_object() {
539                    if !m["usage"].is_object() {
540                        m["usage"] = json!({});
541                    }
542                    for (k, val) in uo {
543                        m["usage"][k.as_str()] = val.clone();
544                    }
545                }
546            }
547            _ => {}
548        }
549    }
550    let mut m = msg?;
551    m["content"] = Value::Array(blocks.into_iter().filter(|b| !b.is_null()).collect());
552    Some(m)
553}
554
555pub fn parse_responses_sse_final(sse: &str) -> Option<Value> {
556    let mut last = None;
557    let mut items: Vec<Value> = Vec::new();
558    for v in sse_datas(sse) {
559        match v["type"].as_str() {
560            Some("response.completed" | "response.incomplete" | "response.failed") => {
561                last = Some(v["response"].clone());
562            }
563            Some("response.output_item.done") => {
564                if v["item"].is_object() {
565                    items.push(v["item"].clone());
566                }
567            }
568            _ => {}
569        }
570    }
571    let mut resp = last?;
572    if resp["output"].as_array().map(|a| a.is_empty()).unwrap_or(true) && !items.is_empty() {
573        resp["output"] = Value::Array(items);
574    }
575    Some(resp)
576}
577
578pub fn synth_openai_chat_sse(chat_resp: &Value) -> String {
579    let chunk = |delta: Value, finish: Value, usage: Option<&Value>| {
580        let mut c = json!({
581            "id": chat_resp["id"],
582            "object": "chat.completion.chunk",
583            "created": 0,
584            "model": chat_resp["model"],
585            "choices": [{"index": 0, "delta": delta, "finish_reason": finish}],
586        });
587        if let Some(u) = usage {
588            c["usage"] = u.clone();
589        }
590        format!("data: {c}\n\n")
591    };
592    let msg = &chat_resp["choices"][0]["message"];
593    let mut out = chunk(json!({"role": "assistant"}), Value::Null, None);
594    if let Some(t) = msg["content"].as_str() {
595        out.push_str(&chunk(json!({"content": t}), Value::Null, None));
596    }
597    if let Some(tcs) = msg["tool_calls"].as_array() {
598        let tcs: Vec<Value> = tcs
599            .iter()
600            .enumerate()
601            .map(|(i, tc)| {
602                let mut tc = tc.clone();
603                tc["index"] = json!(i);
604                tc
605            })
606            .collect();
607        out.push_str(&chunk(json!({"tool_calls": tcs}), Value::Null, None));
608    }
609    let usage = chat_resp["usage"].is_object().then_some(&chat_resp["usage"]);
610    out.push_str(&chunk(
611        json!({}),
612        chat_resp["choices"][0]["finish_reason"].clone(),
613        usage,
614    ));
615    out.push_str("data: [DONE]\n\n");
616    out
617}
618
619fn sse_event(name: &str, data: Value) -> String {
620    format!("event: {name}\ndata: {data}\n\n")
621}
622
623pub fn synth_openai_responses_sse(responses_resp: &Value) -> String {
624    let mut created = responses_resp.clone();
625    created["status"] = json!("in_progress");
626    let mut out = sse_event(
627        "response.created",
628        json!({"type": "response.created", "response": created}),
629    );
630    for (i, it) in responses_resp["output"]
631        .as_array()
632        .into_iter()
633        .flatten()
634        .enumerate()
635    {
636        out.push_str(&sse_event(
637            "response.output_item.added",
638            json!({"type": "response.output_item.added", "output_index": i, "item": it}),
639        ));
640        if it["type"] == "message" {
641            let text = txt(&it["content"]);
642            out.push_str(&sse_event(
643                "response.output_text.delta",
644                json!({
645                    "type": "response.output_text.delta",
646                    "item_id": it["id"],
647                    "output_index": 0,
648                    "content_index": 0,
649                    "delta": text,
650                }),
651            ));
652            out.push_str(&sse_event(
653                "response.output_text.done",
654                json!({
655                    "type": "response.output_text.done",
656                    "item_id": it["id"],
657                    "output_index": 0,
658                    "content_index": 0,
659                    "text": text,
660                }),
661            ));
662        }
663    }
664    out.push_str(&sse_event(
665        "response.completed",
666        json!({"type": "response.completed", "response": responses_resp}),
667    ));
668    out
669}
670
671pub fn synth_anthropic_sse(anthropic_resp: &Value) -> String {
672    let mut start = anthropic_resp.clone();
673    start["content"] = json!([]);
674    start["stop_reason"] = Value::Null;
675    start["stop_sequence"] = Value::Null;
676    start["usage"] = json!({
677        "input_tokens": anthropic_resp["usage"]["input_tokens"].as_i64().unwrap_or(0),
678        "output_tokens": 0,
679    });
680    let mut out = sse_event(
681        "message_start",
682        json!({"type": "message_start", "message": start}),
683    );
684    for (i, b) in anthropic_resp["content"]
685        .as_array()
686        .into_iter()
687        .flatten()
688        .enumerate()
689    {
690        match b["type"].as_str() {
691            Some("text") => {
692                out.push_str(&sse_event(
693                    "content_block_start",
694                    json!({
695                        "type": "content_block_start",
696                        "index": i,
697                        "content_block": {"type": "text", "text": ""},
698                    }),
699                ));
700                out.push_str(&sse_event(
701                    "content_block_delta",
702                    json!({
703                        "type": "content_block_delta",
704                        "index": i,
705                        "delta": {"type": "text_delta", "text": b["text"]},
706                    }),
707                ));
708            }
709            Some("tool_use") => {
710                out.push_str(&sse_event(
711                    "content_block_start",
712                    json!({
713                        "type": "content_block_start",
714                        "index": i,
715                        "content_block": {"type": "tool_use", "id": b["id"], "name": b["name"], "input": {}},
716                    }),
717                ));
718                out.push_str(&sse_event(
719                    "content_block_delta",
720                    json!({
721                        "type": "content_block_delta",
722                        "index": i,
723                        "delta": {"type": "input_json_delta", "partial_json": b["input"].to_string()},
724                    }),
725                ));
726            }
727            _ => continue,
728        }
729        out.push_str(&sse_event(
730            "content_block_stop",
731            json!({"type": "content_block_stop", "index": i}),
732        ));
733    }
734    out.push_str(&sse_event(
735        "message_delta",
736        json!({
737            "type": "message_delta",
738            "delta": {
739                "stop_reason": anthropic_resp["stop_reason"],
740                "stop_sequence": anthropic_resp["stop_sequence"],
741            },
742            "usage": {
743                "output_tokens": anthropic_resp["usage"]["output_tokens"].as_i64().unwrap_or(0),
744            },
745        }),
746    ));
747    out.push_str(&sse_event("message_stop", json!({"type": "message_stop"})));
748    out
749}
750
751fn tool_result_snip(text: &str) -> String {
752    let head: String = text.chars().take(200).collect();
753    format!("[tool result] {head}")
754}
755
756pub fn last_user_text(format_str: &str, req: &Value) -> Option<String> {
757    match format_str {
758        "anthropic" => {
759            for m in req["messages"].as_array().into_iter().flatten().rev() {
760                if m["role"] != "user" {
761                    continue;
762                }
763                match &m["content"] {
764                    Value::String(s) if !s.is_empty() => return Some(s.clone()),
765                    Value::Array(blocks) => {
766                        let text = blocks
767                            .iter()
768                            .filter(|b| b["type"] == "text")
769                            .filter_map(|b| b["text"].as_str())
770                            .collect::<Vec<_>>()
771                            .join("\n");
772                        if !text.is_empty() {
773                            return Some(text);
774                        }
775                        if let Some(tr) = blocks.iter().find(|b| b["type"] == "tool_result") {
776                            return Some(tool_result_snip(&txt(&tr["content"])));
777                        }
778                    }
779                    _ => {}
780                }
781            }
782            None
783        }
784        "openai-chat" => {
785            for m in req["messages"].as_array().into_iter().flatten().rev() {
786                match m["role"].as_str() {
787                    Some("user") => {
788                        let t = txt(&m["content"]);
789                        if !t.is_empty() {
790                            return Some(t);
791                        }
792                    }
793                    Some("tool") => return Some(tool_result_snip(&txt(&m["content"]))),
794                    _ => {}
795                }
796            }
797            None
798        }
799        "openai-responses" => {
800            if let Some(s) = req["input"].as_str() {
801                return (!s.is_empty()).then(|| s.to_string());
802            }
803            for it in req["input"].as_array().into_iter().flatten().rev() {
804                match it["type"].as_str().unwrap_or("message") {
805                    "message" if it["role"] == "user" => {
806                        let t = match &it["content"] {
807                            Value::String(s) => s.clone(),
808                            Value::Array(parts) => parts
809                                .iter()
810                                .filter(|p| p["type"] == "input_text")
811                                .filter_map(|p| p["text"].as_str())
812                                .collect::<Vec<_>>()
813                                .join("\n"),
814                            _ => String::new(),
815                        };
816                        if !t.is_empty() {
817                            return Some(t);
818                        }
819                    }
820                    "function_call_output" => {
821                        return Some(tool_result_snip(&txt(&it["output"])))
822                    }
823                    _ => {}
824                }
825            }
826            None
827        }
828        "gemini" => {
829            for c in req["contents"].as_array().into_iter().flatten().rev() {
830                if c["role"].as_str().unwrap_or("user") != "user" {
831                    continue;
832                }
833                let text = gemini_parts_text(&c["parts"]);
834                if !text.is_empty() {
835                    return Some(text);
836                }
837                if let Some(fr) = c["parts"]
838                    .as_array()
839                    .into_iter()
840                    .flatten()
841                    .find(|p| p["functionResponse"].is_object())
842                {
843                    return Some(tool_result_snip(
844                        &fr["functionResponse"]["response"].to_string(),
845                    ));
846                }
847            }
848            None
849        }
850        _ => None,
851    }
852}
853
854fn anthropic_message_text(msg: &Value) -> Option<String> {
855    let parts: Vec<&str> = msg["content"]
856        .as_array()
857        .into_iter()
858        .flatten()
859        .filter(|b| b["type"] == "text")
860        .filter_map(|b| b["text"].as_str())
861        .collect();
862    (!parts.is_empty()).then(|| parts.join(""))
863}
864
865fn responses_output_text(resp: &Value) -> Option<String> {
866    let mut out = String::new();
867    for it in resp["output"].as_array().into_iter().flatten() {
868        if it["type"] != "message" {
869            continue;
870        }
871        for p in it["content"].as_array().into_iter().flatten() {
872            if p["type"] == "output_text" {
873                out.push_str(p["text"].as_str().unwrap_or(""));
874            }
875        }
876    }
877    (!out.is_empty()).then_some(out)
878}
879
880fn openai_chat_sse_text(sse: &str) -> Option<String> {
881    let mut out = String::new();
882    for v in sse_datas(sse) {
883        if let Some(c) = v["choices"][0]["delta"]["content"].as_str() {
884            out.push_str(c);
885        }
886    }
887    (!out.is_empty()).then_some(out)
888}
889
890fn tool_call_json(name: &Value, args: &Value) -> Value {
891    let arguments = match args {
892        Value::String(s) => s.clone(),
893        Value::Null => String::new(),
894        other => other.to_string(),
895    };
896    json!({"name": name, "arguments": arguments})
897}
898
899pub fn assistant_tool_calls(upstream_format: &str, resp_text: &str) -> Vec<Value> {
900    let trimmed = resp_text.trim_start();
901    let is_sse = trimmed.starts_with("event:") || trimmed.starts_with("data:");
902    match upstream_format {
903        "anthropic" => {
904            let msg = if is_sse {
905                parse_anthropic_sse_to_message(resp_text)
906            } else {
907                serde_json::from_str(resp_text).ok()
908            };
909            msg.map(|m| {
910                m["content"]
911                    .as_array()
912                    .into_iter()
913                    .flatten()
914                    .filter(|b| b["type"] == "tool_use")
915                    .map(|b| tool_call_json(&b["name"], &b["input"]))
916                    .collect()
917            })
918            .unwrap_or_default()
919        }
920        "openai-chat" => {
921            if is_sse {
922                let mut calls: Vec<(String, String)> = Vec::new();
923                for v in sse_datas(resp_text) {
924                    for tc in v["choices"][0]["delta"]["tool_calls"]
925                        .as_array()
926                        .into_iter()
927                        .flatten()
928                    {
929                        let idx = tc["index"].as_u64().unwrap_or(0) as usize;
930                        while calls.len() <= idx {
931                            calls.push((String::new(), String::new()));
932                        }
933                        if let Some(n) = tc["function"]["name"].as_str() {
934                            calls[idx].0.push_str(n);
935                        }
936                        if let Some(a) = tc["function"]["arguments"].as_str() {
937                            calls[idx].1.push_str(a);
938                        }
939                    }
940                }
941                calls
942                    .into_iter()
943                    .filter(|(n, _)| !n.is_empty())
944                    .map(|(n, a)| json!({"name": n, "arguments": a}))
945                    .collect()
946            } else {
947                serde_json::from_str::<Value>(resp_text)
948                    .ok()
949                    .map(|v| {
950                        v["choices"][0]["message"]["tool_calls"]
951                            .as_array()
952                            .into_iter()
953                            .flatten()
954                            .map(|tc| tool_call_json(&tc["function"]["name"], &tc["function"]["arguments"]))
955                            .collect()
956                    })
957                    .unwrap_or_default()
958            }
959        }
960        "openai-responses" => {
961            let resp = if is_sse {
962                parse_responses_sse_final(resp_text)
963            } else {
964                serde_json::from_str(resp_text).ok()
965            };
966            resp.map(|r| {
967                r["output"]
968                    .as_array()
969                    .into_iter()
970                    .flatten()
971                    .filter(|it| it["type"] == "function_call")
972                    .map(|it| tool_call_json(&it["name"], &it["arguments"]))
973                    .collect()
974            })
975            .unwrap_or_default()
976        }
977        _ => Vec::new(),
978    }
979}
980
981pub fn assistant_reply_text(upstream_format: &str, resp_text: &str) -> Option<String> {
982    let trimmed = resp_text.trim_start();
983    let is_sse = trimmed.starts_with("event:") || trimmed.starts_with("data:");
984    match upstream_format {
985        "anthropic" => {
986            let msg = if is_sse {
987                parse_anthropic_sse_to_message(resp_text)?
988            } else {
989                serde_json::from_str(resp_text).ok()?
990            };
991            anthropic_message_text(&msg)
992        }
993        "openai-chat" => {
994            if is_sse {
995                openai_chat_sse_text(resp_text)
996            } else {
997                let v: Value = serde_json::from_str(resp_text).ok()?;
998                v["choices"][0]["message"]["content"]
999                    .as_str()
1000                    .map(String::from)
1001            }
1002        }
1003        "openai-responses" => {
1004            let resp = if is_sse {
1005                parse_responses_sse_final(resp_text)?
1006            } else {
1007                serde_json::from_str(resp_text).ok()?
1008            };
1009            responses_output_text(&resp)
1010        }
1011        "gemini" => {
1012            if is_sse {
1013                let mut out = String::new();
1014                for v in sse_datas(resp_text) {
1015                    out.push_str(&gemini_parts_text(&v["candidates"][0]["content"]["parts"]));
1016                }
1017                (!out.is_empty()).then_some(out)
1018            } else {
1019                let v: Value = serde_json::from_str(resp_text).ok()?;
1020                let text = gemini_parts_text(&v["candidates"][0]["content"]["parts"]);
1021                (!text.is_empty()).then_some(text)
1022            }
1023        }
1024        _ => None,
1025    }
1026}
1027
1028pub(crate) fn gemini_parts_text(parts: &Value) -> String {
1029    parts
1030        .as_array()
1031        .into_iter()
1032        .flatten()
1033        .filter_map(|p| p["text"].as_str())
1034        .collect::<Vec<_>>()
1035        .join("\n")
1036}
1037
1038pub fn gemini_to_anthropic(req: &Value) -> Value {
1039    let mut msgs = Vec::new();
1040    let mut call_ids: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1041    let mut call_counter = 0usize;
1042    for content in req["contents"].as_array().into_iter().flatten() {
1043        let role = content["role"].as_str().unwrap_or("user");
1044        let mut blocks = Vec::new();
1045        for part in content["parts"].as_array().into_iter().flatten() {
1046            if let Some(t) = part["text"].as_str() {
1047                if !t.is_empty() {
1048                    blocks.push(json!({"type": "text", "text": t}));
1049                }
1050            } else if part["functionCall"].is_object() {
1051                call_counter += 1;
1052                let name = part["functionCall"]["name"].as_str().unwrap_or("");
1053                let id = format!("toolu_gemini_{call_counter}");
1054                call_ids.insert(name.to_string(), id.clone());
1055                blocks.push(json!({
1056                    "type": "tool_use",
1057                    "id": id,
1058                    "name": name,
1059                    "input": if part["functionCall"]["args"].is_object() {
1060                        part["functionCall"]["args"].clone()
1061                    } else {
1062                        json!({})
1063                    },
1064                }));
1065            } else if part["functionResponse"].is_object() {
1066                let name = part["functionResponse"]["name"].as_str().unwrap_or("");
1067                let id = call_ids
1068                    .get(name)
1069                    .cloned()
1070                    .unwrap_or_else(|| format!("toolu_gemini_{name}"));
1071                let payload = &part["functionResponse"]["response"];
1072                let text = match payload {
1073                    Value::String(s) => s.clone(),
1074                    v if v.is_null() => String::new(),
1075                    v => v.to_string(),
1076                };
1077                blocks.push(json!({
1078                    "type": "tool_result",
1079                    "tool_use_id": id,
1080                    "content": [{"type": "text", "text": text}],
1081                }));
1082            }
1083        }
1084        if blocks.is_empty() {
1085            continue;
1086        }
1087        let a_role = if role == "model" { "assistant" } else { "user" };
1088        msgs.push(json!({"role": a_role, "content": blocks}));
1089    }
1090    let mut o = Map::new();
1091    put(&mut o, "model", &req["model"]);
1092    let system = gemini_parts_text(&req["systemInstruction"]["parts"]);
1093    if !system.trim().is_empty() {
1094        o.insert("system".to_string(), Value::String(system));
1095    }
1096    o.insert("messages".to_string(), Value::Array(msgs));
1097    let tools: Vec<Value> = req["tools"]
1098        .as_array()
1099        .into_iter()
1100        .flatten()
1101        .flat_map(|t| t["functionDeclarations"].as_array().cloned().unwrap_or_default())
1102        .map(|fd| {
1103            let mut tool = Map::new();
1104            put(&mut tool, "name", &fd["name"]);
1105            put(&mut tool, "description", &fd["description"]);
1106            if fd["parameters"].is_object() {
1107                tool.insert("input_schema".to_string(), fd["parameters"].clone());
1108            } else {
1109                tool.insert("input_schema".to_string(), json!({"type": "object"}));
1110            }
1111            Value::Object(tool)
1112        })
1113        .collect();
1114    if !tools.is_empty() {
1115        o.insert("tools".to_string(), Value::Array(tools));
1116    }
1117    match req["toolConfig"]["functionCallingConfig"]["mode"].as_str() {
1118        Some("ANY") => {
1119            o.insert("tool_choice".to_string(), json!({"type": "any"}));
1120        }
1121        Some("AUTO") => {
1122            o.insert("tool_choice".to_string(), json!({"type": "auto"}));
1123        }
1124        _ => {}
1125    }
1126    let g = &req["generationConfig"];
1127    let max = g["maxOutputTokens"].as_i64().unwrap_or(8192);
1128    o.insert("max_tokens".to_string(), json!(max));
1129    put(&mut o, "temperature", &g["temperature"]);
1130    put(&mut o, "top_p", &g["topP"]);
1131    if let Some(stops) = g["stopSequences"].as_array() {
1132        if !stops.is_empty() {
1133            o.insert("stop_sequences".to_string(), Value::Array(stops.clone()));
1134        }
1135    }
1136    Value::Object(o)
1137}
1138
1139fn stop_to_gemini_finish(stop: Option<&str>) -> &'static str {
1140    match stop {
1141        Some("max_tokens") => "MAX_TOKENS",
1142        _ => "STOP",
1143    }
1144}
1145
1146pub fn anthropic_response_to_gemini(resp: &Value, model: &str) -> Value {
1147    let mut parts = Vec::new();
1148    for b in resp["content"].as_array().into_iter().flatten() {
1149        match b["type"].as_str() {
1150            Some("text") => {
1151                parts.push(json!({"text": b["text"].as_str().unwrap_or("")}));
1152            }
1153            Some("tool_use") => {
1154                parts.push(json!({
1155                    "functionCall": {"name": b["name"], "args": b["input"].clone()},
1156                }));
1157            }
1158            _ => {}
1159        }
1160    }
1161    if parts.is_empty() {
1162        parts.push(json!({"text": ""}));
1163    }
1164    let u = &resp["usage"];
1165    let pt = u["input_tokens"].as_i64().unwrap_or(0)
1166        + u["cache_read_input_tokens"].as_i64().unwrap_or(0);
1167    let ct = u["output_tokens"].as_i64().unwrap_or(0);
1168    json!({
1169        "candidates": [{
1170            "content": {"role": "model", "parts": parts},
1171            "finishReason": stop_to_gemini_finish(resp["stop_reason"].as_str()),
1172            "index": 0,
1173        }],
1174        "usageMetadata": {
1175            "promptTokenCount": pt,
1176            "candidatesTokenCount": ct,
1177            "totalTokenCount": pt + ct,
1178            "cachedContentTokenCount": u["cache_read_input_tokens"].as_i64().unwrap_or(0),
1179        },
1180        "modelVersion": model,
1181    })
1182}
1183
1184pub fn anthropic_to_gemini_request(req: &Value) -> Value {
1185    let mut contents = Vec::new();
1186    let mut tool_names: std::collections::HashMap<String, String> =
1187        std::collections::HashMap::new();
1188    for m in req["messages"].as_array().into_iter().flatten() {
1189        let role = if m["role"] == "assistant" { "model" } else { "user" };
1190        let mut parts = Vec::new();
1191        match &m["content"] {
1192            Value::String(s) => {
1193                if !s.is_empty() {
1194                    parts.push(json!({"text": s}));
1195                }
1196            }
1197            Value::Array(blocks) => {
1198                for b in blocks {
1199                    match b["type"].as_str() {
1200                        Some("text") => {
1201                            parts.push(json!({"text": b["text"].as_str().unwrap_or("")}));
1202                        }
1203                        Some("tool_use") => {
1204                            let id = b["id"].as_str().unwrap_or("").to_string();
1205                            let name = b["name"].as_str().unwrap_or("").to_string();
1206                            tool_names.insert(id, name.clone());
1207                            parts.push(json!({
1208                                "functionCall": {"name": name, "args": b["input"].clone()},
1209                            }));
1210                        }
1211                        Some("tool_result") => {
1212                            let id = b["tool_use_id"].as_str().unwrap_or("");
1213                            let name = tool_names
1214                                .get(id)
1215                                .cloned()
1216                                .unwrap_or_else(|| id.to_string());
1217                            parts.push(json!({
1218                                "functionResponse": {
1219                                    "name": name,
1220                                    "response": {"result": txt(&b["content"])},
1221                                },
1222                            }));
1223                        }
1224                        _ => {}
1225                    }
1226                }
1227            }
1228            _ => {}
1229        }
1230        if !parts.is_empty() {
1231            contents.push(json!({"role": role, "parts": parts}));
1232        }
1233    }
1234    let mut o = Map::new();
1235    o.insert("contents".to_string(), Value::Array(contents));
1236    let system = txt(&req["system"]);
1237    if !system.is_empty() {
1238        o.insert(
1239            "systemInstruction".to_string(),
1240            json!({"parts": [{"text": system}]}),
1241        );
1242    }
1243    let decls: Vec<Value> = req["tools"]
1244        .as_array()
1245        .into_iter()
1246        .flatten()
1247        .map(|t| {
1248            json!({
1249                "name": t["name"],
1250                "description": t["description"],
1251                "parameters": if t["input_schema"].is_object() {
1252                    t["input_schema"].clone()
1253                } else {
1254                    json!({"type": "object"})
1255                },
1256            })
1257        })
1258        .collect();
1259    if !decls.is_empty() {
1260        o.insert(
1261            "tools".to_string(),
1262            json!([{"functionDeclarations": decls}]),
1263        );
1264    }
1265    let mut g = Map::new();
1266    put(&mut g, "temperature", &req["temperature"]);
1267    put(&mut g, "topP", &req["top_p"]);
1268    put(&mut g, "maxOutputTokens", &req["max_tokens"]);
1269    if let Some(stops) = req["stop_sequences"].as_array() {
1270        if !stops.is_empty() {
1271            g.insert("stopSequences".to_string(), Value::Array(stops.clone()));
1272        }
1273    }
1274    if !g.is_empty() {
1275        o.insert("generationConfig".to_string(), Value::Object(g));
1276    }
1277    Value::Object(o)
1278}
1279
1280pub fn gemini_response_to_anthropic(resp: &Value, model: &str) -> Value {
1281    let mut content = Vec::new();
1282    let mut saw_tool = false;
1283    let mut call_counter = 0usize;
1284    for part in resp["candidates"][0]["content"]["parts"]
1285        .as_array()
1286        .into_iter()
1287        .flatten()
1288    {
1289        if let Some(t) = part["text"].as_str() {
1290            if !t.is_empty() {
1291                content.push(json!({"type": "text", "text": t}));
1292            }
1293        } else if part["functionCall"].is_object() {
1294            saw_tool = true;
1295            call_counter += 1;
1296            content.push(json!({
1297                "type": "tool_use",
1298                "id": format!("toolu_gemini_{call_counter}"),
1299                "name": part["functionCall"]["name"],
1300                "input": if part["functionCall"]["args"].is_object() {
1301                    part["functionCall"]["args"].clone()
1302                } else {
1303                    json!({})
1304                },
1305            }));
1306        }
1307    }
1308    let finish = resp["candidates"][0]["finishReason"].as_str();
1309    let stop_reason = if saw_tool {
1310        "tool_use"
1311    } else if finish == Some("MAX_TOKENS") {
1312        "max_tokens"
1313    } else {
1314        "end_turn"
1315    };
1316    let u = &resp["usageMetadata"];
1317    json!({
1318        "id": format!("msg_gemini_{}", resp["responseId"].as_str().unwrap_or("0")),
1319        "type": "message",
1320        "role": "assistant",
1321        "model": model,
1322        "content": content,
1323        "stop_reason": stop_reason,
1324        "usage": {
1325            "input_tokens": u["promptTokenCount"].as_i64().unwrap_or(0),
1326            "output_tokens": u["candidatesTokenCount"].as_i64().unwrap_or(0)
1327                + u["thoughtsTokenCount"].as_i64().unwrap_or(0),
1328            "cache_read_input_tokens": u["cachedContentTokenCount"].as_i64().unwrap_or(0),
1329        },
1330    })
1331}
1332
1333pub fn parse_gemini_upstream_final(text: &str) -> Option<Value> {
1334    let unwrap = |v: Value| -> Value {
1335        if v["response"].is_object() {
1336            v["response"].clone()
1337        } else {
1338            v
1339        }
1340    };
1341    let trimmed = text.trim_start();
1342    if !(trimmed.starts_with("data:") || trimmed.starts_with("event:")) {
1343        return serde_json::from_str::<Value>(text).ok().map(unwrap);
1344    }
1345    let mut texts = String::new();
1346    let mut calls: Vec<Value> = Vec::new();
1347    let mut finish = Value::Null;
1348    let mut usage = Value::Null;
1349    let mut model_version = Value::Null;
1350    let mut saw_any = false;
1351    for frame in sse_datas(text) {
1352        let v = unwrap(frame);
1353        if !v["candidates"].is_array() && !v["usageMetadata"].is_object() {
1354            continue;
1355        }
1356        saw_any = true;
1357        for part in v["candidates"][0]["content"]["parts"]
1358            .as_array()
1359            .into_iter()
1360            .flatten()
1361        {
1362            if let Some(t) = part["text"].as_str() {
1363                texts.push_str(t);
1364            } else if part["functionCall"].is_object() {
1365                calls.push(part.clone());
1366            }
1367        }
1368        if v["candidates"][0]["finishReason"].is_string() {
1369            finish = v["candidates"][0]["finishReason"].clone();
1370        }
1371        if v["usageMetadata"].is_object() {
1372            usage = v["usageMetadata"].clone();
1373        }
1374        if v["modelVersion"].is_string() {
1375            model_version = v["modelVersion"].clone();
1376        }
1377    }
1378    if !saw_any {
1379        return None;
1380    }
1381    let mut parts = Vec::new();
1382    if !texts.is_empty() {
1383        parts.push(json!({"text": texts}));
1384    }
1385    parts.extend(calls);
1386    Some(json!({
1387        "candidates": [{
1388            "content": {"role": "model", "parts": parts},
1389            "finishReason": if finish.is_null() { json!("STOP") } else { finish },
1390            "index": 0,
1391        }],
1392        "usageMetadata": usage,
1393        "modelVersion": model_version,
1394    }))
1395}
1396
1397pub fn synth_gemini_sse(resp: &Value) -> String {
1398    let text = gemini_parts_text(&resp["candidates"][0]["content"]["parts"]);
1399    let mut frames = Vec::new();
1400    if !text.is_empty() {
1401        let content_frame = json!({
1402            "candidates": [{
1403                "content": {"role": "model", "parts": [{"text": text}]},
1404                "index": 0,
1405            }],
1406            "modelVersion": resp["modelVersion"],
1407        });
1408        frames.push(format!("data: {content_frame}\n\n"));
1409    }
1410    let mut fin = resp.clone();
1411    if !text.is_empty() {
1412        if let Some(parts) = fin["candidates"][0]["content"]["parts"].as_array_mut() {
1413            parts.retain(|p| p["text"].as_str().is_none());
1414            if parts.is_empty() {
1415                parts.push(json!({"text": ""}));
1416            }
1417        }
1418    }
1419    frames.push(format!("data: {fin}\n\n"));
1420    frames.concat()
1421}
1422
1423pub fn normalize_codex_request(req: &mut Value) {
1424    let Some(o) = req.as_object_mut() else { return };
1425    o.insert("store".to_string(), json!(false));
1426    o.insert("stream".to_string(), json!(true));
1427    if !o.contains_key("tool_choice") {
1428        o.insert("tool_choice".to_string(), json!("auto"));
1429    }
1430    if !o.contains_key("parallel_tool_calls") {
1431        o.insert("parallel_tool_calls".to_string(), json!(true));
1432    }
1433    o.insert("include".to_string(), json!(["reasoning.encrypted_content"]));
1434    for k in [
1435        "context_management",
1436        "max_completion_tokens",
1437        "max_output_tokens",
1438        "max_tokens",
1439        "prompt_cache_retention",
1440        "safety_identifier",
1441        "temperature",
1442        "top_p",
1443        "truncation",
1444        "user",
1445    ] {
1446        o.remove(k);
1447    }
1448}
1449
1450#[cfg(test)]
1451mod tests {
1452    use super::*;
1453
1454    fn gemini_req() -> Value {
1455        json!({
1456            "model": "gpt-5.5",
1457            "systemInstruction": {"parts": [{"text": "be terse"}]},
1458            "contents": [
1459                {"role": "user", "parts": [{"text": "what is the weather in SF?"}]},
1460                {"role": "model", "parts": [
1461                    {"text": "checking"},
1462                    {"functionCall": {"name": "get_weather", "args": {"city": "SF"}}}
1463                ]},
1464                {"role": "user", "parts": [
1465                    {"functionResponse": {"name": "get_weather", "response": {"temp": 18}}}
1466                ]}
1467            ],
1468            "tools": [{"functionDeclarations": [{
1469                "name": "get_weather",
1470                "description": "look up weather",
1471                "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}
1472            }]}],
1473            "toolConfig": {"functionCallingConfig": {"mode": "AUTO"}},
1474            "generationConfig": {
1475                "temperature": 0.5,
1476                "topP": 0.9,
1477                "maxOutputTokens": 1024,
1478                "stopSequences": ["END"]
1479            }
1480        })
1481    }
1482
1483    #[test]
1484    fn gemini_to_anthropic_full() {
1485        let a = gemini_to_anthropic(&gemini_req());
1486        assert_eq!(a["model"], "gpt-5.5");
1487        assert_eq!(a["system"], "be terse");
1488        assert_eq!(a["max_tokens"], 1024);
1489        assert_eq!(a["temperature"], 0.5);
1490        assert_eq!(a["top_p"], 0.9);
1491        assert_eq!(a["stop_sequences"][0], "END");
1492        assert_eq!(a["tool_choice"]["type"], "auto");
1493        assert_eq!(a["tools"][0]["name"], "get_weather");
1494        assert_eq!(a["tools"][0]["input_schema"]["type"], "object");
1495        let msgs = a["messages"].as_array().unwrap();
1496        assert_eq!(msgs.len(), 3);
1497        assert_eq!(msgs[0]["role"], "user");
1498        assert_eq!(msgs[0]["content"][0]["text"], "what is the weather in SF?");
1499        assert_eq!(msgs[1]["role"], "assistant");
1500        assert_eq!(msgs[1]["content"][1]["type"], "tool_use");
1501        assert_eq!(msgs[1]["content"][1]["name"], "get_weather");
1502        assert_eq!(msgs[1]["content"][1]["input"]["city"], "SF");
1503        let call_id = msgs[1]["content"][1]["id"].as_str().unwrap();
1504        assert_eq!(msgs[2]["content"][0]["type"], "tool_result");
1505        assert_eq!(msgs[2]["content"][0]["tool_use_id"], call_id);
1506        assert!(msgs[2]["content"][0]["content"][0]["text"]
1507            .as_str()
1508            .unwrap()
1509            .contains("18"));
1510    }
1511
1512    #[test]
1513    fn gemini_to_anthropic_defaults() {
1514        let a = gemini_to_anthropic(&json!({
1515            "contents": [{"parts": [{"text": "hi"}]}]
1516        }));
1517        assert_eq!(a["max_tokens"], 8192);
1518        assert_eq!(a["messages"][0]["role"], "user");
1519        assert!(a.get("system").is_none());
1520        assert!(a.get("tools").is_none());
1521    }
1522
1523    #[test]
1524    fn anthropic_resp_to_gemini_text_and_tools() {
1525        let resp = json!({
1526            "id": "msg_1",
1527            "content": [
1528                {"type": "text", "text": "PONG"},
1529                {"type": "tool_use", "id": "t1", "name": "get_weather", "input": {"city": "SF"}}
1530            ],
1531            "stop_reason": "end_turn",
1532            "usage": {"input_tokens": 10, "output_tokens": 3, "cache_read_input_tokens": 4}
1533        });
1534        let g = anthropic_response_to_gemini(&resp, "gpt-5.5");
1535        assert_eq!(g["candidates"][0]["content"]["role"], "model");
1536        assert_eq!(g["candidates"][0]["content"]["parts"][0]["text"], "PONG");
1537        assert_eq!(
1538            g["candidates"][0]["content"]["parts"][1]["functionCall"]["name"],
1539            "get_weather"
1540        );
1541        assert_eq!(g["candidates"][0]["finishReason"], "STOP");
1542        assert_eq!(g["usageMetadata"]["promptTokenCount"], 14);
1543        assert_eq!(g["usageMetadata"]["candidatesTokenCount"], 3);
1544        assert_eq!(g["usageMetadata"]["cachedContentTokenCount"], 4);
1545        assert_eq!(g["modelVersion"], "gpt-5.5");
1546
1547        let max = json!({"content": [], "stop_reason": "max_tokens", "usage": {}});
1548        let g2 = anthropic_response_to_gemini(&max, "m");
1549        assert_eq!(g2["candidates"][0]["finishReason"], "MAX_TOKENS");
1550    }
1551
1552    #[test]
1553    fn gemini_sse_synth_round_trip() {
1554        let resp = anthropic_response_to_gemini(
1555            &json!({
1556                "content": [{"type": "text", "text": "PONG"}],
1557                "stop_reason": "end_turn",
1558                "usage": {"input_tokens": 5, "output_tokens": 1}
1559            }),
1560            "gpt-5.5",
1561        );
1562        let sse = synth_gemini_sse(&resp);
1563        assert!(sse.starts_with("data: "));
1564        assert!(!sse.contains("[DONE]"));
1565        let frames: Vec<Value> = sse_datas(&sse).collect();
1566        assert_eq!(frames.len(), 2);
1567        assert_eq!(
1568            frames[0]["candidates"][0]["content"]["parts"][0]["text"],
1569            "PONG"
1570        );
1571        assert_eq!(frames[1]["candidates"][0]["finishReason"], "STOP");
1572        assert_eq!(frames[1]["usageMetadata"]["promptTokenCount"], 5);
1573        assert_eq!(
1574            assistant_reply_text("gemini", &sse).as_deref(),
1575            Some("PONG")
1576        );
1577    }
1578
1579    #[test]
1580    fn anthropic_to_gemini_request_round_trip() {
1581        let a = json!({
1582            "model": "gemini-2.5-flash",
1583            "system": "be terse",
1584            "max_tokens": 256,
1585            "temperature": 0.3,
1586            "messages": [
1587                {"role": "user", "content": "weather?"},
1588                {"role": "assistant", "content": [
1589                    {"type": "tool_use", "id": "tu1", "name": "get_weather", "input": {"city": "SF"}}
1590                ]},
1591                {"role": "user", "content": [
1592                    {"type": "tool_result", "tool_use_id": "tu1", "content": [{"type": "text", "text": "18C"}]}
1593                ]}
1594            ],
1595            "tools": [{"name": "get_weather", "description": "w", "input_schema": {"type": "object"}}]
1596        });
1597        let g = anthropic_to_gemini_request(&a);
1598        assert_eq!(g["systemInstruction"]["parts"][0]["text"], "be terse");
1599        assert_eq!(g["generationConfig"]["maxOutputTokens"], 256);
1600        assert_eq!(g["generationConfig"]["temperature"], 0.3);
1601        assert_eq!(g["tools"][0]["functionDeclarations"][0]["name"], "get_weather");
1602        let c = g["contents"].as_array().unwrap();
1603        assert_eq!(c[0]["role"], "user");
1604        assert_eq!(c[0]["parts"][0]["text"], "weather?");
1605        assert_eq!(c[1]["role"], "model");
1606        assert_eq!(c[1]["parts"][0]["functionCall"]["name"], "get_weather");
1607        assert_eq!(c[2]["parts"][0]["functionResponse"]["name"], "get_weather");
1608        assert_eq!(c[2]["parts"][0]["functionResponse"]["response"]["result"], "18C");
1609    }
1610
1611    #[test]
1612    fn gemini_response_to_anthropic_basic() {
1613        let g = json!({
1614            "candidates": [{
1615                "content": {"role": "model", "parts": [{"text": "PONG"}]},
1616                "finishReason": "STOP"
1617            }],
1618            "usageMetadata": {"promptTokenCount": 10, "candidatesTokenCount": 2, "thoughtsTokenCount": 3}
1619        });
1620        let a = gemini_response_to_anthropic(&g, "gemini-2.5-flash");
1621        assert_eq!(a["role"], "assistant");
1622        assert_eq!(a["content"][0]["text"], "PONG");
1623        assert_eq!(a["stop_reason"], "end_turn");
1624        assert_eq!(a["usage"]["input_tokens"], 10);
1625        assert_eq!(a["usage"]["output_tokens"], 5);
1626    }
1627
1628    #[test]
1629    fn gemini_upstream_final_unwraps_envelope_and_sse() {
1630        // non-stream, wrapped in code-assist "response" envelope
1631        let wrapped = json!({
1632            "response": {
1633                "candidates": [{"content": {"role": "model", "parts": [{"text": "hi"}]}, "finishReason": "STOP"}],
1634                "usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 1}
1635            }
1636        });
1637        let final_v = parse_gemini_upstream_final(&wrapped.to_string()).unwrap();
1638        assert_eq!(final_v["candidates"][0]["content"]["parts"][0]["text"], "hi");
1639        assert_eq!(final_v["usageMetadata"]["promptTokenCount"], 1);
1640
1641        // streaming SSE with response-wrapped frames
1642        let sse = "data: {\"response\":{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"PO\"}]}}]}}\n\n\
1643                   data: {\"response\":{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"NG\"}]},\"finishReason\":\"STOP\"}],\"usageMetadata\":{\"promptTokenCount\":5,\"candidatesTokenCount\":1}}}\n\n";
1644        let final_sse = parse_gemini_upstream_final(sse).unwrap();
1645        assert_eq!(final_sse["candidates"][0]["content"]["parts"][0]["text"], "PONG");
1646        assert_eq!(final_sse["candidates"][0]["finishReason"], "STOP");
1647        assert_eq!(final_sse["usageMetadata"]["candidatesTokenCount"], 1);
1648    }
1649
1650    #[test]
1651    fn gemini_last_user_and_reply_text() {
1652        assert_eq!(
1653            last_user_text("gemini", &gemini_req()).as_deref(),
1654            Some("[tool result] {\"temp\":18}")
1655        );
1656        let plain = json!({
1657            "candidates": [{"content": {"role": "model", "parts": [{"text": "hello"}]}}]
1658        });
1659        assert_eq!(
1660            assistant_reply_text("gemini", &plain.to_string()).as_deref(),
1661            Some("hello")
1662        );
1663    }
1664
1665    use super::*;
1666
1667    #[test]
1668    fn chat_to_anthropic_basic() {
1669        let req = json!({
1670            "model": "claude-sonnet-4-5",
1671            "messages": [
1672                {"role": "system", "content": "be brief"},
1673                {"role": "system", "content": [{"type": "text", "text": "and kind"}]},
1674                {"role": "user", "content": [
1675                    {"type": "text", "text": "hi"},
1676                    {"type": "image_url", "image_url": {"url": "http://x"}},
1677                ]},
1678            ],
1679            "max_completion_tokens": 512,
1680            "temperature": 0.5,
1681            "stop": "END",
1682            "stream": true,
1683        });
1684        let out = openai_chat_to_anthropic(&req);
1685        assert_eq!(out["system"], "be brief\n\nand kind");
1686        assert_eq!(out["messages"][0]["role"], "user");
1687        assert_eq!(out["messages"][0]["content"], "hi");
1688        assert_eq!(out["max_tokens"], 512);
1689        assert_eq!(out["temperature"], 0.5);
1690        assert_eq!(out["stop_sequences"], json!(["END"]));
1691        assert_eq!(out["stream"], true);
1692        assert!(out.get("tools").is_none());
1693    }
1694
1695    #[test]
1696    fn chat_to_anthropic_tools_round_trip() {
1697        let req = json!({
1698            "model": "gpt-5.1",
1699            "messages": [
1700                {"role": "user", "content": "weather?"},
1701                {"role": "assistant", "content": null, "tool_calls": [
1702                    {"id": "call_1", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\":\"SF\"}"}},
1703                ]},
1704                {"role": "tool", "tool_call_id": "call_1", "content": "sunny"},
1705            ],
1706            "tools": [
1707                {"type": "function", "function": {"name": "get_weather", "description": "d", "parameters": {"type": "object"}}},
1708            ],
1709            "tool_choice": {"type": "function", "function": {"name": "get_weather"}},
1710        });
1711        let out = openai_chat_to_anthropic(&req);
1712        let asst = &out["messages"][1];
1713        assert_eq!(asst["content"][0]["type"], "tool_use");
1714        assert_eq!(asst["content"][0]["id"], "call_1");
1715        assert_eq!(asst["content"][0]["input"], json!({"city": "SF"}));
1716        let result = &out["messages"][2];
1717        assert_eq!(result["role"], "user");
1718        assert_eq!(result["content"][0]["type"], "tool_result");
1719        assert_eq!(result["content"][0]["tool_use_id"], "call_1");
1720        assert_eq!(result["content"][0]["content"][0]["text"], "sunny");
1721        assert_eq!(out["tools"][0]["name"], "get_weather");
1722        assert_eq!(out["tools"][0]["input_schema"], json!({"type": "object"}));
1723        assert_eq!(out["tool_choice"], json!({"type": "tool", "name": "get_weather"}));
1724        assert_eq!(out["max_tokens"], 8192);
1725    }
1726
1727    #[test]
1728    fn chat_tool_choice_auto_and_none() {
1729        let auto = openai_chat_to_anthropic(&json!({"messages": [], "tool_choice": "auto"}));
1730        assert_eq!(auto["tool_choice"], json!({"type": "auto"}));
1731        let none = openai_chat_to_anthropic(&json!({"messages": [], "tool_choice": "none"}));
1732        assert!(none.get("tool_choice").is_none());
1733    }
1734
1735    #[test]
1736    fn responses_to_anthropic() {
1737        let req = json!({
1738            "model": "claude-opus-4-8",
1739            "instructions": "sys",
1740            "input": [
1741                {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "hi"}]},
1742                {"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "checking"}]},
1743                {"type": "function_call", "call_id": "c1", "name": "f", "arguments": "{\"a\":1}"},
1744                {"type": "function_call_output", "call_id": "c1", "output": "42"},
1745            ],
1746            "tools": [{"type": "function", "name": "f", "description": "d", "parameters": {"type": "object"}}],
1747            "max_output_tokens": 100,
1748            "stream": true,
1749        });
1750        let out = openai_responses_to_anthropic(&req);
1751        assert_eq!(out["system"], "sys");
1752        assert_eq!(out["messages"][0], json!({"role": "user", "content": "hi"}));
1753        assert_eq!(out["messages"][1]["role"], "assistant");
1754        assert_eq!(out["messages"][2]["content"][0]["type"], "tool_use");
1755        assert_eq!(out["messages"][2]["content"][0]["id"], "c1");
1756        assert_eq!(out["messages"][2]["content"][0]["input"], json!({"a": 1}));
1757        assert_eq!(out["messages"][3]["content"][0]["type"], "tool_result");
1758        assert_eq!(out["messages"][3]["content"][0]["content"][0]["text"], "42");
1759        assert_eq!(out["tools"][0]["input_schema"], json!({"type": "object"}));
1760        assert_eq!(out["max_tokens"], 100);
1761        assert_eq!(out["stream"], true);
1762    }
1763
1764    #[test]
1765    fn responses_to_anthropic_string_input() {
1766        let out = openai_responses_to_anthropic(&json!({"model": "m", "input": "hello"}));
1767        assert_eq!(out["messages"][0], json!({"role": "user", "content": "hello"}));
1768        assert_eq!(out["max_tokens"], 8192);
1769        assert!(out.get("system").is_none());
1770    }
1771
1772    #[test]
1773    fn anthropic_to_responses() {
1774        let req = json!({
1775            "model": "gpt-5.5",
1776            "system": [{"type": "text", "text": "sys"}],
1777            "messages": [
1778                {"role": "user", "content": "hi"},
1779                {"role": "assistant", "content": [
1780                    {"type": "text", "text": "using tool"},
1781                    {"type": "tool_use", "id": "t1", "name": "f", "input": {"a": 1}},
1782                ]},
1783                {"role": "user", "content": [
1784                    {"type": "tool_result", "tool_use_id": "t1", "content": [{"type": "text", "text": "ok"}]},
1785                ]},
1786            ],
1787            "tools": [{"name": "f", "description": "d", "input_schema": {"type": "object"}}],
1788            "max_tokens": 256,
1789            "stream": true,
1790        });
1791        let out = anthropic_to_openai_responses(&req);
1792        assert_eq!(out["instructions"], "sys");
1793        assert_eq!(out["input"][0]["content"][0]["type"], "input_text");
1794        assert_eq!(out["input"][1]["content"][0]["type"], "output_text");
1795        assert_eq!(out["input"][2]["type"], "function_call");
1796        assert_eq!(out["input"][2]["call_id"], "t1");
1797        assert_eq!(out["input"][2]["arguments"], "{\"a\":1}");
1798        assert_eq!(out["input"][3]["type"], "function_call_output");
1799        assert_eq!(out["input"][3]["output"], "ok");
1800        assert_eq!(out["tools"][0]["type"], "function");
1801        assert_eq!(out["tools"][0]["parameters"], json!({"type": "object"}));
1802        assert_eq!(out["tools"][0]["strict"], false);
1803        assert_eq!(out["max_output_tokens"], 256);
1804        assert_eq!(out["stream"], true);
1805    }
1806
1807    fn anthropic_resp() -> Value {
1808        json!({
1809            "id": "msg_01",
1810            "type": "message",
1811            "role": "assistant",
1812            "content": [
1813                {"type": "text", "text": "hi "},
1814                {"type": "text", "text": "there"},
1815                {"type": "tool_use", "id": "t1", "name": "f", "input": {"a": 1}},
1816            ],
1817            "stop_reason": "tool_use",
1818            "usage": {"input_tokens": 10, "output_tokens": 5, "cache_read_input_tokens": 3},
1819        })
1820    }
1821
1822    #[test]
1823    fn anthropic_resp_to_chat() {
1824        let out = anthropic_response_to_openai_chat(&anthropic_resp(), "m");
1825        assert_eq!(out["id"], "chatcmpl-msg_01");
1826        assert_eq!(out["object"], "chat.completion");
1827        assert_eq!(out["model"], "m");
1828        let msg = &out["choices"][0]["message"];
1829        assert_eq!(msg["content"], "hi there");
1830        assert_eq!(msg["tool_calls"][0]["id"], "t1");
1831        assert_eq!(msg["tool_calls"][0]["function"]["arguments"], "{\"a\":1}");
1832        assert_eq!(out["choices"][0]["finish_reason"], "tool_calls");
1833        assert_eq!(out["usage"]["prompt_tokens"], 10);
1834        assert_eq!(out["usage"]["completion_tokens"], 5);
1835        assert_eq!(out["usage"]["total_tokens"], 15);
1836        assert_eq!(out["usage"]["prompt_tokens_details"]["cached_tokens"], 3);
1837    }
1838
1839    #[test]
1840    fn anthropic_resp_to_responses() {
1841        let out = anthropic_response_to_openai_responses(&anthropic_resp(), "m");
1842        assert_eq!(out["id"], "resp_msg_01");
1843        assert_eq!(out["status"], "completed");
1844        assert_eq!(out["output"][0]["type"], "message");
1845        assert_eq!(out["output"][0]["content"][0]["type"], "output_text");
1846        assert_eq!(out["output"][0]["content"][0]["text"], "hi ");
1847        assert_eq!(out["output"][2]["type"], "function_call");
1848        assert_eq!(out["output"][2]["call_id"], "t1");
1849        assert_eq!(out["output"][2]["arguments"], "{\"a\":1}");
1850        assert_eq!(out["usage"]["input_tokens"], 10);
1851        assert_eq!(out["usage"]["total_tokens"], 15);
1852        assert_eq!(out["usage"]["input_tokens_details"]["cached_tokens"], 3);
1853        let mut capped = anthropic_resp();
1854        capped["stop_reason"] = json!("max_tokens");
1855        assert_eq!(anthropic_response_to_openai_responses(&capped, "m")["status"], "incomplete");
1856    }
1857
1858    fn responses_resp() -> Value {
1859        json!({
1860            "id": "r1",
1861            "object": "response",
1862            "status": "completed",
1863            "output": [
1864                {"type": "reasoning", "id": "rs1", "summary": []},
1865                {"type": "message", "id": "m1", "role": "assistant", "status": "completed",
1866                 "content": [{"type": "output_text", "text": "hello", "annotations": []}]},
1867                {"type": "function_call", "id": "fc1", "call_id": "c1", "name": "f", "arguments": "{\"a\":1}"},
1868            ],
1869            "usage": {"input_tokens": 7, "output_tokens": 2, "input_tokens_details": {"cached_tokens": 4}},
1870        })
1871    }
1872
1873    #[test]
1874    fn responses_to_anthropic_resp() {
1875        let out = responses_final_to_anthropic(&responses_resp(), "m");
1876        assert_eq!(out["id"], "msg_r1");
1877        assert_eq!(out["type"], "message");
1878        assert_eq!(out["content"][0], json!({"type": "text", "text": "hello"}));
1879        assert_eq!(out["content"][1]["type"], "tool_use");
1880        assert_eq!(out["content"][1]["id"], "c1");
1881        assert_eq!(out["content"][1]["input"], json!({"a": 1}));
1882        assert_eq!(out["stop_reason"], "tool_use");
1883        assert_eq!(out["usage"]["input_tokens"], 7);
1884        assert_eq!(out["usage"]["cache_read_input_tokens"], 4);
1885        let mut inc = responses_resp();
1886        inc["status"] = json!("incomplete");
1887        assert_eq!(responses_final_to_anthropic(&inc, "m")["stop_reason"], "max_tokens");
1888        let mut plain = responses_resp();
1889        plain["output"].as_array_mut().unwrap().pop();
1890        assert_eq!(responses_final_to_anthropic(&plain, "m")["stop_reason"], "end_turn");
1891    }
1892
1893    #[test]
1894    fn responses_to_chat_resp() {
1895        let out = responses_final_to_openai_chat(&responses_resp(), "m");
1896        assert_eq!(out["id"], "chatcmpl-r1");
1897        let msg = &out["choices"][0]["message"];
1898        assert_eq!(msg["content"], "hello");
1899        assert_eq!(msg["tool_calls"][0]["id"], "c1");
1900        assert_eq!(msg["tool_calls"][0]["function"]["arguments"], "{\"a\":1}");
1901        assert_eq!(out["choices"][0]["finish_reason"], "tool_calls");
1902        assert_eq!(out["usage"]["prompt_tokens"], 7);
1903        assert_eq!(out["usage"]["total_tokens"], 9);
1904        assert_eq!(out["usage"]["prompt_tokens_details"]["cached_tokens"], 4);
1905    }
1906
1907    #[test]
1908    fn anthropic_sse_reassembly() {
1909        let sse = concat!(
1910            "event: message_start\n",
1911            "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",
1912            "event: content_block_start\n",
1913            "data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n",
1914            "event: content_block_delta\n",
1915            "data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"hel\"}}\n\n",
1916            "event: content_block_delta\n",
1917            "data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"lo\"}}\n\n",
1918            "event: content_block_stop\n",
1919            "data: {\"type\":\"content_block_stop\",\"index\":0}\n\n",
1920            "event: content_block_start\n",
1921            "data: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"t1\",\"name\":\"f\",\"input\":{}}}\n\n",
1922            "event: content_block_delta\n",
1923            "data: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"a\\\":\"}}\n\n",
1924            "event: content_block_delta\n",
1925            "data: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"1}\"}}\n\n",
1926            "event: content_block_stop\n",
1927            "data: {\"type\":\"content_block_stop\",\"index\":1}\n\n",
1928            "event: message_delta\n",
1929            "data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null},\"usage\":{\"output_tokens\":25}}\n\n",
1930            "event: message_stop\n",
1931            "data: {\"type\":\"message_stop\"}\n\n",
1932        );
1933        let m = parse_anthropic_sse_to_message(sse).unwrap();
1934        assert_eq!(m["content"][0]["text"], "hello");
1935        assert_eq!(m["content"][1]["type"], "tool_use");
1936        assert_eq!(m["content"][1]["input"], json!({"a": 1}));
1937        assert_eq!(m["stop_reason"], "tool_use");
1938        assert_eq!(m["usage"]["input_tokens"], 10);
1939        assert_eq!(m["usage"]["output_tokens"], 25);
1940        assert!(parse_anthropic_sse_to_message("data: {\"type\":\"ping\"}\n\n").is_none());
1941    }
1942
1943    #[test]
1944    fn responses_sse_final() {
1945        let sse = concat!(
1946            "event: response.created\n",
1947            "data: {\"type\":\"response.created\",\"response\":{\"id\":\"r1\",\"status\":\"in_progress\"}}\n\n",
1948            "event: response.output_text.delta\n",
1949            "data: {\"type\":\"response.output_text.delta\",\"delta\":\"hi\"}\n\n",
1950            "event: response.completed\n",
1951            "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"r1\",\"status\":\"completed\",\"output\":[]}}\n\n",
1952        );
1953        let r = parse_responses_sse_final(sse).unwrap();
1954        assert_eq!(r["id"], "r1");
1955        assert_eq!(r["status"], "completed");
1956        assert!(parse_responses_sse_final("data: {\"type\":\"response.created\"}\n\n").is_none());
1957    }
1958
1959    #[test]
1960    fn chat_sse_synth() {
1961        let chat = anthropic_response_to_openai_chat(&anthropic_resp(), "m");
1962        let sse = synth_openai_chat_sse(&chat);
1963        let chunks: Vec<Value> = sse
1964            .lines()
1965            .filter_map(|l| l.strip_prefix("data: "))
1966            .filter(|d| *d != "[DONE]")
1967            .map(|d| serde_json::from_str(d).unwrap())
1968            .collect();
1969        assert_eq!(chunks[0]["choices"][0]["delta"]["role"], "assistant");
1970        assert_eq!(chunks[0]["object"], "chat.completion.chunk");
1971        assert_eq!(chunks[1]["choices"][0]["delta"]["content"], "hi there");
1972        assert_eq!(chunks[2]["choices"][0]["delta"]["tool_calls"][0]["index"], 0);
1973        assert_eq!(chunks[2]["choices"][0]["delta"]["tool_calls"][0]["id"], "t1");
1974        let last = chunks.last().unwrap();
1975        assert_eq!(last["choices"][0]["finish_reason"], "tool_calls");
1976        assert_eq!(last["usage"]["total_tokens"], 15);
1977        assert!(sse.ends_with("data: [DONE]\n\n"));
1978    }
1979
1980    #[test]
1981    fn responses_sse_synth() {
1982        let sse = synth_openai_responses_sse(&responses_resp());
1983        assert!(sse.starts_with("event: response.created\n"));
1984        assert!(sse.contains("event: response.output_item.added\n"));
1985        assert!(sse.contains("event: response.output_text.delta\n"));
1986        assert!(sse.contains("event: response.output_text.done\n"));
1987        assert!(sse.contains("event: response.completed\n"));
1988        let fin = parse_responses_sse_final(&sse).unwrap();
1989        assert_eq!(fin, responses_resp());
1990    }
1991
1992    #[test]
1993    fn anthropic_sse_synth() {
1994        let sse = synth_anthropic_sse(&anthropic_resp());
1995        assert!(sse.starts_with("event: message_start\n"));
1996        assert!(sse.contains("event: content_block_start\n"));
1997        assert!(sse.contains("event: message_stop\n"));
1998        let m = parse_anthropic_sse_to_message(&sse).unwrap();
1999        assert_eq!(m["content"][0]["text"], "hi ");
2000        assert_eq!(m["content"][2]["input"], json!({"a": 1}));
2001        assert_eq!(m["stop_reason"], "tool_use");
2002        assert_eq!(m["usage"]["input_tokens"], 10);
2003        assert_eq!(m["usage"]["output_tokens"], 5);
2004    }
2005
2006    #[test]
2007    fn codex_normalize() {
2008        let mut req = json!({
2009            "model": "gpt-5.1-codex",
2010            "input": [],
2011            "temperature": 0.7,
2012            "top_p": 0.9,
2013            "max_output_tokens": 100,
2014            "max_tokens": 100,
2015            "max_completion_tokens": 100,
2016            "truncation": "auto",
2017            "user": "u",
2018            "safety_identifier": "s",
2019            "prompt_cache_retention": "24h",
2020            "context_management": {},
2021            "reasoning": {"effort": "high"},
2022            "text": {"verbosity": "low"},
2023            "prompt_cache_key": "k",
2024            "service_tier": "flex",
2025            "tool_choice": "none",
2026        });
2027        normalize_codex_request(&mut req);
2028        assert_eq!(req["store"], false);
2029        assert_eq!(req["stream"], true);
2030        assert_eq!(req["tool_choice"], "none");
2031        assert_eq!(req["parallel_tool_calls"], true);
2032        assert_eq!(req["include"], json!(["reasoning.encrypted_content"]));
2033        assert_eq!(req["reasoning"]["effort"], "high");
2034        assert_eq!(req["text"]["verbosity"], "low");
2035        assert_eq!(req["prompt_cache_key"], "k");
2036        assert_eq!(req["service_tier"], "flex");
2037        for k in [
2038            "temperature",
2039            "top_p",
2040            "max_output_tokens",
2041            "max_tokens",
2042            "max_completion_tokens",
2043            "truncation",
2044            "user",
2045            "safety_identifier",
2046            "prompt_cache_retention",
2047            "context_management",
2048        ] {
2049            assert!(req.get(k).is_none(), "{k} should be removed");
2050        }
2051    }
2052
2053    #[test]
2054    fn codex_normalize_defaults() {
2055        let mut req = json!({"model": "m", "input": []});
2056        normalize_codex_request(&mut req);
2057        assert_eq!(req["tool_choice"], "auto");
2058        assert_eq!(req["parallel_tool_calls"], true);
2059    }
2060
2061    #[test]
2062    fn last_user_text_anthropic() {
2063        let req = json!({"messages": [
2064            {"role": "user", "content": "first"},
2065            {"role": "assistant", "content": "reply"},
2066            {"role": "user", "content": [
2067                {"type": "text", "text": "part1"},
2068                {"type": "text", "text": "part2"},
2069            ]},
2070        ]});
2071        assert_eq!(last_user_text("anthropic", &req), Some("part1\npart2".into()));
2072        let long = "x".repeat(500);
2073        let tool = json!({"messages": [
2074            {"role": "user", "content": "q"},
2075            {"role": "assistant", "content": [{"type": "tool_use", "id": "t1", "name": "f", "input": {}}]},
2076            {"role": "user", "content": [
2077                {"type": "tool_result", "tool_use_id": "t1",
2078                 "content": [{"type": "text", "text": long}]},
2079            ]},
2080        ]});
2081        let got = last_user_text("anthropic", &tool).unwrap();
2082        assert!(got.starts_with("[tool result] xxx"));
2083        assert_eq!(got.chars().count(), "[tool result] ".chars().count() + 200);
2084        assert_eq!(last_user_text("anthropic", &json!({"messages": []})), None);
2085    }
2086
2087    #[test]
2088    fn last_user_text_openai_chat() {
2089        let req = json!({"messages": [
2090            {"role": "system", "content": "s"},
2091            {"role": "user", "content": "hello"},
2092            {"role": "assistant", "content": "hi"},
2093            {"role": "user", "content": [{"type": "text", "text": "again"}]},
2094        ]});
2095        assert_eq!(last_user_text("openai-chat", &req), Some("again".into()));
2096        let tool = json!({"messages": [
2097            {"role": "user", "content": "q"},
2098            {"role": "assistant", "content": null},
2099            {"role": "tool", "tool_call_id": "c1", "content": "result body"},
2100        ]});
2101        assert_eq!(
2102            last_user_text("openai-chat", &tool),
2103            Some("[tool result] result body".into())
2104        );
2105        assert_eq!(last_user_text("openai-chat", &json!({})), None);
2106    }
2107
2108    #[test]
2109    fn last_user_text_openai_responses() {
2110        let req = json!({"input": [
2111            {"type": "message", "role": "user",
2112             "content": [{"type": "input_text", "text": "one"}]},
2113            {"type": "message", "role": "assistant",
2114             "content": [{"type": "output_text", "text": "r"}]},
2115            {"type": "message", "role": "user",
2116             "content": [{"type": "input_text", "text": "two"}]},
2117        ]});
2118        assert_eq!(last_user_text("openai-responses", &req), Some("two".into()));
2119        let tool = json!({"input": [
2120            {"type": "message", "role": "user",
2121             "content": [{"type": "input_text", "text": "q"}]},
2122            {"type": "function_call", "call_id": "c1", "name": "f", "arguments": "{}"},
2123            {"type": "function_call_output", "call_id": "c1", "output": "tool says hi"},
2124        ]});
2125        assert_eq!(
2126            last_user_text("openai-responses", &tool),
2127            Some("[tool result] tool says hi".into())
2128        );
2129        assert_eq!(
2130            last_user_text("openai-responses", &json!({"input": "raw"})),
2131            Some("raw".into())
2132        );
2133        assert_eq!(last_user_text("mystery", &json!({})), None);
2134    }
2135
2136    #[test]
2137    fn assistant_reply_anthropic_plain_and_sse() {
2138        let plain = json!({
2139            "id": "msg_01", "type": "message", "role": "assistant",
2140            "content": [
2141                {"type": "thinking", "thinking": "hmm"},
2142                {"type": "text", "text": "hello "},
2143                {"type": "text", "text": "world"},
2144            ],
2145            "stop_reason": "end_turn",
2146        });
2147        assert_eq!(
2148            assistant_reply_text("anthropic", &plain.to_string()),
2149            Some("hello world".into())
2150        );
2151        let sse = synth_anthropic_sse(&anthropic_resp());
2152        assert_eq!(
2153            assistant_reply_text("anthropic", &sse),
2154            Some("hi there".into())
2155        );
2156        assert_eq!(assistant_reply_text("anthropic", "not json"), None);
2157    }
2158
2159    #[test]
2160    fn assistant_reply_openai_chat_plain_and_sse() {
2161        let plain = json!({"choices": [{"message": {"role": "assistant", "content": "chat reply"}}]});
2162        assert_eq!(
2163            assistant_reply_text("openai-chat", &plain.to_string()),
2164            Some("chat reply".into())
2165        );
2166        let sse = concat!(
2167            "data: {\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"}}]}\n\n",
2168            "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"str\"}}]}\n\n",
2169            "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"eamed\"}}]}\n\n",
2170            "data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n",
2171            "data: [DONE]\n\n",
2172        );
2173        assert_eq!(
2174            assistant_reply_text("openai-chat", sse),
2175            Some("streamed".into())
2176        );
2177        assert_eq!(assistant_reply_text("openai-chat", "data: {}\n\n"), None);
2178    }
2179
2180    #[test]
2181    fn assistant_reply_openai_responses_plain_and_sse() {
2182        assert_eq!(
2183            assistant_reply_text("openai-responses", &responses_resp().to_string()),
2184            Some("hello".into())
2185        );
2186        let sse = synth_openai_responses_sse(&responses_resp());
2187        assert_eq!(
2188            assistant_reply_text("openai-responses", &sse),
2189            Some("hello".into())
2190        );
2191        assert_eq!(
2192            assistant_reply_text("openai-responses", "data: {\"type\":\"ping\"}\n\n"),
2193            None
2194        );
2195    }
2196}