Skip to content

Adapters.toAnthropic

Java · package io.github.muthuishere:toolnexus · SPEC §4 · Adapters.java

public static List<Map<String, Object>> toAnthropic(List<Tool> tools)

Turns a List<Tool> into the tools array an Anthropic Messages request expects. Same tools, same execution — only the schema envelope differs from OpenAI’s.

When you drive the Anthropic Messages API yourself — the official Java SDK, a raw HttpClient call, or Claude on Bedrock / Vertex — and need the tools block for the request body.

Against Adapters.toOpenAI: Anthropic has no function wrapper and names the schema key input_schema, not parameters. Both methods return the same Java type, so picking the wrong one is a 400 from the provider, not a compile error.

tk.toAnthropic() on a Toolkit is this same function applied to that toolkit’s tools — use the static method when you have a bare list.

import io.github.muthuishere.toolnexus.*;
import java.util.List;
import java.util.Map;
public class Example {
public static void main(String[] args) {
Tool weather = NativeTool.of(
"get_weather",
"Current weather for a city",
Map.of("type", "object",
"properties", Map.of("city", Map.of("type", "string")),
"required", List.of("city")),
(Map<String, Object> a) -> "sunny in " + a.get("city")
);
List<Map<String, Object>> schema = Adapters.toAnthropic(List.of(weather));
if (schema.size() != 1) throw new AssertionError("expected 1 entry");
Map<String, Object> entry = schema.get(0);
// Flat — no {"type":"function"} wrapper, unlike OpenAI.
if (entry.containsKey("function")) throw new AssertionError("unexpected function wrapper");
if (!entry.get("name").equals("get_weather")) throw new AssertionError("name");
if (!entry.get("description").equals("Current weather for a city")) throw new AssertionError("description");
if (!entry.containsKey("input_schema")) throw new AssertionError("expected input_schema");
System.out.println("ok: " + entry.get("name"));
}
}

Three keys, no nesting: name, description, input_schema. The inputSchema() map is passed through untouched — only the key is renamed.

2. Feeding it straight into a Messages request body

Section titled “2. Feeding it straight into a Messages request body”

The output is plain Maps and Lists, so any JSON library serializes it without help.

import io.github.muthuishere.toolnexus.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class Example {
static Tool mk(String name, String desc) {
return NativeTool.of(name, desc,
Map.of("type", "object", "properties", Map.of()),
(Map<String, Object> a) -> name);
}
public static void main(String[] args) {
List<Tool> tools = List.of(mk("search", "Search the docs"), mk("ping", "Health check"));
Map<String, Object> body = Map.of(
"model", "claude-sonnet-4-5",
"max_tokens", 1024,
"messages", List.of(Map.of("role", "user", "content", "search for adapters")),
"tools", Adapters.toAnthropic(tools)
);
// The key is read from the environment at call time — never hardcoded.
String apiKey = System.getenv("ANTHROPIC_API_KEY");
@SuppressWarnings("unchecked")
List<Map<String, Object>> entries = (List<Map<String, Object>>) body.get("tools");
if (entries.size() != 2) throw new AssertionError("expected 2 tools");
// Order is preserved — list order in, array order out.
List<String> names = new ArrayList<>();
for (Map<String, Object> e : entries) names.add((String) e.get("name"));
if (!names.equals(List.of("search", "ping"))) throw new AssertionError("order: " + names);
System.out.println("ok: " + String.join(", ", names) + " (key configured: " + (apiKey != null) + ")");
}
}

3. Round-tripping a tool_use block back to the tool

Section titled “3. Round-tripping a tool_use block back to the tool”

Schema out, tool call in. Anthropic sends input as a real JSON object, so it maps straight to execute’s Map<String, Object> — no string-parsing step, unlike OpenAI.

import io.github.muthuishere.toolnexus.*;
import java.util.List;
import java.util.Map;
public class Example {
public static void main(String[] args) {
Tool weather = NativeTool.of(
"get_weather",
"Current weather for a city",
Map.of("type", "object",
"properties", Map.of("city", Map.of("type", "string")),
"required", List.of("city")),
(Map<String, Object> a) -> "sunny in " + a.get("city")
);
List<Tool> tools = List.of(weather);
// A `tool_use` content block from Claude, decoded.
Map<String, Object> toolUse = Map.of(
"type", "tool_use",
"id", "toolu_01ABC",
"name", "get_weather",
"input", Map.of("city", "Chennai")
);
Tool called = tools.stream()
.filter(t -> t.name().equals(toolUse.get("name")))
.findFirst()
.orElseThrow(() -> new AssertionError("the advertised name should resolve back"));
@SuppressWarnings("unchecked")
Map<String, Object> input = (Map<String, Object>) toolUse.get("input");
ToolResult res = called.execute(input, new ToolContext());
if (res.isError() || !res.output().equals("sunny in Chennai")) {
throw new AssertionError(res.output());
}
// The reply block you send back on the next turn — isError maps to is_error.
Map<String, Object> resultBlock = Map.of(
"type", "tool_result",
"tool_use_id", toolUse.get("id"),
"content", res.output(),
"is_error", res.isError()
);
if (!resultBlock.get("content").equals("sunny in Chennai")) throw new AssertionError("content");
// An empty tool list is valid — it just means "no tools this turn".
if (!Adapters.toAnthropic(List.of()).isEmpty()) throw new AssertionError("expected empty");
System.out.println("ok: " + toolUse.get("name") + " -> " + res.output());
}
}
Path From Notes
[].name Tool.name() What the model calls back with in tool_use.name.
[].description Tool.description()
[].input_schema Tool.inputSchema() Renamed — inputSchemainput_schema.

There is no type key and no function wrapper — that is the OpenAI shape.