Toolnexus.Translate.openai_messages_to_anthropic
Elixir · package toolnexus · SPEC §11 · elixir/lib/toolnexus/translate.ex
@spec openai_messages_to_anthropic([map()]) :: {[map()], String.t()}def openai_messages_to_anthropic(messages)Converts an OpenAI messages list into Anthropic-native messages plus the extracted system
prompt, returned as {messages, system}. This is the piece a naive text-flattening translator
gets wrong: an assistant turn’s tool_calls become tool_use blocks (with arguments parsed
back from its JSON string into an object), a tool-role result becomes a tool_result block
keyed by tool_call_id, and consecutive tool results merge into a single user turn — because
Anthropic expects one result-bearing turn answering the preceding assistant turn, not one turn per
result. system/developer messages are hoisted out into the returned system string, since
Anthropic takes system separately from the transcript.
Toolnexus.Client.translate/3 calls this internally when style: "anthropic" — it’s exposed
directly for callers who need the conversion without also making a provider call.
When to use it
Section titled “When to use it”- You’re proxying OpenAI-shaped requests to an Anthropic-style upstream and need the
transcript itself, not just
Client.translate/3’s single-call result — e.g. building the request body for a different transport, or inspecting the converted shape before sending it. - You’re storing transcripts in OpenAI shape (the common wire format) but need to hand one to an Anthropic-native client or SDK.
- You’re testing tool-call round-tripping across the two shapes without touching the network.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — a plain user/assistant exchange, system hoisted out
Section titled “1. The smallest useful call — a plain user/assistant exchange, system hoisted out”alias Toolnexus.Translate
messages = [ %{"role" => "system", "content" => "You are terse."}, %{"role" => "user", "content" => "capital of France?"}, %{"role" => "assistant", "content" => "Paris."}]
{converted, system} = Translate.openai_messages_to_anthropic(messages)
true = system == "You are terse."true = converted == [ %{"role" => "user", "content" => "capital of France?"}, %{"role" => "assistant", "content" => [%{"type" => "text", "text" => "Paris."}]}]
IO.puts("ok: system hoisted (#{system}), #{length(converted)} message(s) converted")2. The realistic case — a tool call and its result, correctly re-shaped
Section titled “2. The realistic case — a tool call and its result, correctly re-shaped”The assistant’s tool_calls become a tool_use block with input parsed back into a map; the
matching tool-role result becomes a tool_result block keyed by tool_call_id.
alias Toolnexus.Translate
messages = [ %{"role" => "user", "content" => "weather in Chennai?"}, %{ "role" => "assistant", "content" => nil, "tool_calls" => [ %{"id" => "c1", "type" => "function", "function" => %{"name" => "lookup_weather", "arguments" => ~s({"city":"Chennai"})}} ] }, %{"role" => "tool", "tool_call_id" => "c1", "content" => "28C, humid"}]
{converted, system} = Translate.openai_messages_to_anthropic(messages)
true = system == ""[user1, assistant, user2] = converted
true = user1 == %{"role" => "user", "content" => "weather in Chennai?"}true = assistant == %{ "role" => "assistant", "content" => [%{"type" => "tool_use", "id" => "c1", "name" => "lookup_weather", "input" => %{"city" => "Chennai"}}]}true = user2 == %{"role" => "user", "content" => [%{"type" => "tool_result", "tool_use_id" => "c1", "content" => "28C, humid"}]}
IO.puts("ok: tool_calls -> tool_use, tool result -> tool_result, #{length(converted)} turn(s)")3. The full surface — multiple parallel tool results merge into ONE user turn
Section titled “3. The full surface — multiple parallel tool results merge into ONE user turn”This is the case a flattening translator gets wrong: two consecutive tool-role messages
(parallel tool calls answered in the same turn) merge into a single Anthropic user turn
carrying both tool_result blocks, in order — not two separate user turns.
alias Toolnexus.Translate
messages = [ %{"role" => "developer", "content" => "Be exact."}, %{"role" => "user", "content" => "compare weather in Chennai and Paris"}, %{ "role" => "assistant", "content" => nil, "tool_calls" => [ %{"id" => "c1", "type" => "function", "function" => %{"name" => "weather", "arguments" => ~s({"city":"Chennai"})}}, %{"id" => "c2", "type" => "function", "function" => %{"name" => "weather", "arguments" => ~s({"city":"Paris"})}} ] }, %{"role" => "tool", "tool_call_id" => "c1", "content" => "28C"}, %{"role" => "tool", "tool_call_id" => "c2", "content" => "12C"}, %{"role" => "assistant", "content" => "Chennai is warmer."}]
{converted, system} = Translate.openai_messages_to_anthropic(messages)
# "developer" is hoisted the same as "system"true = system == "Be exact."true = length(converted) == 4
[_user, assistant_calls, merged_results, final] = convertedtrue = length(assistant_calls["content"]) == 2true = merged_results == %{ "role" => "user", "content" => [ %{"type" => "tool_result", "tool_use_id" => "c1", "content" => "28C"}, %{"type" => "tool_result", "tool_use_id" => "c2", "content" => "12C"} ]}true = final == %{"role" => "assistant", "content" => [%{"type" => "text", "text" => "Chennai is warmer."}]}
IO.puts("ok: 2 consecutive tool results merged into #{length(merged_results["content"])}-block user turn (not 2 turns)")Fields
Section titled “Fields”| Type | What it is | |
|---|---|---|
messages (arg) |
[map()] |
OpenAI-shaped messages, string-keyed. |
messages (return) |
[map()] |
Anthropic-native messages — tool_use/tool_result blocks, merged where consecutive. |
system (return) |
String.t() |
Every system/developer message’s text, joined with "\n\n". |
See also
Section titled “See also”Toolnexus.Client.translate— the entry point that calls this internally, then makes the single provider call.