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 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 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}