Inbound translation — transcripts across providers
Rust · crate toolnexus · SPEC §11
Convert an OpenAI-shaped transcript into Anthropic shape, including the tool-result merging a flattening translator gets wrong.
What it does
Section titled “What it does”On Style::Anthropic, translate converts the OpenAI transcript before its one call:
system/developermessages are hoisted intosystem(joined with a blank line); a requestsystemoverride wins.- An assistant turn’s
tool_callsbecometool_useblocks (id,name, parsedinput). tool-role results becometool_resultblocks keyed bytool_call_id, and consecutive results merge into ONE user turn.- A content-parts array keeps its non-text parts (
image_url,input_audio,file, §1B parts) as provider blocks; a part Anthropic cannot carry becomes a named text placeholder. - OpenAI
toolsbecome{name, description, input_schema};tool_choicerequired→any,none→none, a named function →{type: "tool", name}.
The reply’s stop_reason maps back to an OpenAI finish_reason; any tool call wins as tool_calls.
What to use instead
Section titled “What to use instead”Call Client::translate on a client built with
style: Style::Anthropic. It converts the OpenAI transcript as described above, makes its one
provider call, and maps the reply back to OpenAI shape. This example shows the merge a flattening
translator gets wrong: two consecutive tool results land in ONE user turn.
use std::sync::{Arc, Mutex};use tokio::io::{AsyncReadExt, AsyncWriteExt};use serde_json::{Value, json};use toolnexus::client::{ClientOptions, Style, TranslateRequest, create_client};
/// A one-shot loopback provider: answers each request with `reply`, keeps the last body.async fn stub(reply: &'static str) -> (String, Arc<Mutex<String>>) { let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let url = format!("http://{}", l.local_addr().unwrap()); let seen = Arc::new(Mutex::new(String::new())); let keep = seen.clone(); tokio::spawn(async move { while let Ok((mut s, _)) = l.accept().await { let mut buf = Vec::new(); let mut chunk = [0u8; 8192]; loop { let n = s.read(&mut chunk).await.unwrap_or(0); if n == 0 { break; } buf.extend_from_slice(&chunk[..n]); let text = String::from_utf8_lossy(&buf).to_string(); if let Some(i) = text.find("\r\n\r\n") { let len = text[..i].lines() .find_map(|h| h.to_ascii_lowercase().strip_prefix("content-length:").map(|v| v.trim().parse().unwrap_or(0))) .unwrap_or(0usize); if buf.len() >= i + 4 + len { *keep.lock().unwrap() = String::from_utf8_lossy(&buf[i + 4..i + 4 + len]).into(); break; } } } let resp = format!("HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{reply}", reply.len()); let _ = s.write_all(resp.as_bytes()).await; } }); (url, seen)}
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let (url, body) = stub(r#"{"content":[{"type":"text","text":"both done"}],"stop_reason":"end_turn","usage":{"input_tokens":9,"output_tokens":2}}"#).await; let client = create_client(ClientOptions { base_url: url, style: Style::Anthropic, model: "claude-x".into(), api_key: "test-key".into(), ..Default::default() });
let res = client .translate(TranslateRequest { messages: vec![ json!({"role": "user", "content": "check a and b"}), json!({"role": "assistant", "content": null, "tool_calls": [ {"id": "a1", "type": "function", "function": {"name": "check", "arguments": "{\"x\":\"a\"}"}}, {"id": "b1", "type": "function", "function": {"name": "check", "arguments": "{\"x\":\"b\"}"}} ]}), json!({"role": "tool", "tool_call_id": "a1", "content": "a ok"}), json!({"role": "tool", "tool_call_id": "b1", "content": "b ok"}), ], ..Default::default() }) .await?; assert_eq!(res.text, "both done");
let sent: Value = serde_json::from_str(&body.lock().unwrap())?; let msgs = sent["messages"].as_array().unwrap(); assert_eq!(msgs.len(), 3, "user, assistant(tool_use x2), ONE user(tool_result x2): {msgs:?}"); let last = &msgs[2]; assert_eq!(last["role"], "user"); let ids: Vec<&str> = last["content"].as_array().unwrap().iter() .filter(|b| b["type"] == "tool_result") .map(|b| b["tool_use_id"].as_str().unwrap()) .collect(); assert_eq!(ids, ["a1", "b1"]);
println!("ok: 2 tool results merged into one user turn: {ids:?}"); Ok(())}See also
Section titled “See also”translate— one turn, no loop.