Translate.openAIMessagesToAnthropic
Java · package io.github.muthuishere:toolnexus · SPEC §11 · Translate.java
public record Converted(List<Object> messages, String system)
public static Converted openAIMessagesToAnthropic(List<Object> messages)The pure conversion function LlmClient.translate uses
internally on the Anthropic path — exposed directly for callers who want the shape, not a
provider call. Converts an OpenAI messages array into Anthropic-native messages plus the
extracted system prompt: 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, consecutive tool results merge into one user turn
(providers expect a single result-bearing turn answering the preceding assistant turn, not one
turn per result), and system/developer messages are hoisted out since Anthropic takes system
separately. This is exactly “the part a text-flattening translator gets wrong” — collapsing tool
structure to prose loses the tool_call_id correlation a provider needs to replay the transcript.
When to use it
Section titled “When to use it”You’re building something that needs Anthropic-shaped messages from an OpenAI-shaped transcript
but isn’t making a provider call through translate — inspecting what the wire payload would look
like, feeding a different Anthropic-speaking client, or testing your own OpenAI-side message
construction against what a conforming translator should produce.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — tool_calls become tool_use, arguments re-parsed to an object
Section titled “1. The smallest useful call — tool_calls become tool_use, arguments re-parsed to an object”import io.github.muthuishere.toolnexus.Translate;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;
public class Example { public static void main(String[] args) { Map<String, Object> assistant = new LinkedHashMap<>(); assistant.put("role", "assistant"); assistant.put("tool_calls", List.of(Map.of( "id", "call_abc", "type", "function", "function", Map.of("name", "get_weather", "arguments", "{\"city\":\"Chennai\"}"))));
Translate.Converted converted = Translate.openAIMessagesToAnthropic(List.of( Map.of("role", "user", "content", "weather in Chennai?"), assistant));
List<Object> out = converted.messages(); if (out.size() != 2) throw new AssertionError(out); Map<String, Object> assistantOut = (Map<String, Object>) out.get(1); List<Object> blocks = (List<Object>) assistantOut.get("content"); Map<String, Object> toolUse = (Map<String, Object>) blocks.get(0);
if (!"tool_use".equals(toolUse.get("type"))) throw new AssertionError(toolUse); // arguments is re-parsed from its JSON STRING into an OBJECT for the tool_use block. Map<String, Object> input = (Map<String, Object>) toolUse.get("input"); if (!"Chennai".equals(input.get("city"))) throw new AssertionError(input);
System.out.println("ok: " + toolUse.get("name") + " input=" + input); }}2. The realistic case — three consecutive tool results merge into ONE user turn
Section titled “2. The realistic case — three consecutive tool results merge into ONE user turn”import io.github.muthuishere.toolnexus.Translate;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;
public class Example { public static void main(String[] args) { Map<String, Object> assistant = new LinkedHashMap<>(); assistant.put("role", "assistant"); assistant.put("tool_calls", List.of( Map.of("id", "a", "function", Map.of("name", "f", "arguments", "{}")), Map.of("id", "b", "function", Map.of("name", "f", "arguments", "{}")), Map.of("id", "c", "function", Map.of("name", "f", "arguments", "{}"))));
Translate.Converted converted = Translate.openAIMessagesToAnthropic(List.of( Map.of("role", "user", "content", "do three things"), assistant, Map.of("role", "tool", "tool_call_id", "a", "content", "ra"), Map.of("role", "tool", "tool_call_id", "b", "content", "rb"), Map.of("role", "tool", "tool_call_id", "c", "content", "rc")));
int resultTurns = 0; int resultsInTurn = 0; for (Object m : converted.messages()) { Map<String, Object> mm = (Map<String, Object>) m; if (!(mm.get("content") instanceof List<?> blocks)) continue; int n = 0; for (Object b : blocks) { if (b instanceof Map<?, ?> bm && "tool_result".equals(bm.get("type"))) n++; } if (n > 0) { resultTurns++; resultsInTurn = n; } }
if (resultTurns != 1) throw new AssertionError("results spread over " + resultTurns + " turns"); if (resultsInTurn != 3) throw new AssertionError("merged turn carries " + resultsInTurn + " results");
System.out.println("ok: " + resultTurns + " turn carrying " + resultsInTurn + " tool_result blocks"); }}3. The full surface — system/developer hoisted out, content parts flattened, object-form arguments accepted
Section titled “3. The full surface — system/developer hoisted out, content parts flattened, object-form arguments accepted”import io.github.muthuishere.toolnexus.Translate;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;
public class Example { public static void main(String[] args) { // (a) system + developer messages are hoisted out of the message list. Translate.Converted withSystem = Translate.openAIMessagesToAnthropic(List.of( Map.of("role", "system", "content", "Be terse."), Map.of("role", "developer", "content", "Prefer metric units."), Map.of("role", "user", "content", "hi"))); if (!withSystem.system().equals("Be terse.\n\nPrefer metric units.")) { throw new AssertionError(withSystem.system()); } boolean anySystemLeftInMessages = withSystem.messages().stream() .anyMatch(m -> "system".equals(((Map<?, ?>) m).get("role")) || "developer".equals(((Map<?, ?>) m).get("role"))); if (anySystemLeftInMessages) throw new AssertionError("system/developer must be hoisted out");
// (b) a content-parts array is flattened to plain text. Translate.Converted parts = Translate.openAIMessagesToAnthropic(List.of( Map.of("role", "user", "content", List.of( Map.of("type", "text", "text", "part one "), Map.of("type", "text", "text", "part two"))))); Map<String, Object> flattenedUserTurn = (Map<String, Object>) parts.messages().get(0); if (!"part one part two".equals(flattenedUserTurn.get("content"))) { throw new AssertionError("content parts were not flattened: " + flattenedUserTurn.get("content")); }
// (c) some callers send `arguments` as an OBJECT rather than a JSON string — accepted too. Map<String, Object> assistant = new LinkedHashMap<>(); assistant.put("role", "assistant"); assistant.put("tool_calls", List.of(Map.of("id", "z", "function", Map.of("name", "f", "arguments", Map.of("city", "Madurai"))))); Translate.Converted objectArgs = Translate.openAIMessagesToAnthropic(List.of( Map.of("role", "user", "content", "go"), assistant, Map.of("role", "tool", "tool_call_id", "z", "content", "done"))); Map<String, Object> assistantOut = (Map<String, Object>) objectArgs.messages().get(1); List<Object> blocks = (List<Object>) assistantOut.get("content"); Map<String, Object> toolUse = (Map<String, Object>) blocks.get(0); Map<String, Object> input = (Map<String, Object>) toolUse.get("input"); if (!"Madurai".equals(input.get("city"))) throw new AssertionError("object-form arguments lost: " + input);
System.out.println("ok: system=[" + withSystem.system() + "] object-args city=" + input.get("city")); }}Fields
Section titled “Fields”| Member | Type | What it is |
|---|---|---|
openAIMessagesToAnthropic(messages) |
Converted |
Converts one OpenAI messages array. |
Converted.messages |
List<Object> |
Anthropic-native messages: tool_use/tool_result blocks, merged consecutive results. |
Converted.system |
String |
The hoisted system prompt, \n\n-joined from any system/developer messages found. |
See also
Section titled “See also”LlmClient.translate— Declare a toolkit to a provider and translate one request/response without executing anything or keeping state; uses this conversion internally on the Anthropic path.