n4nAI

Consistent tool schemas across OpenAI, Anthropic, and Llama 4

Practical guide to building consistent tool schemas OpenAI Anthropic Llama 4: canonical JSON Schema, provider adapters, response normalization, and pitfalls.

n4n Team3 min read751 words

Audio narration

Coming soon — every post will get a voice note here.

Shipping agentic features means your code must handle consistent tool schemas OpenAI Anthropic Llama 4 without rewriting your function definitions for every provider. The three ecosystems share JSON Schema as a base, but each imposes different wrapping, validation rules, and response shapes that break naive integrations.

1. Define a single canonical schema

Start by writing your tool definitions once in a provider-agnostic shape. Treat JSON Schema as the lingua franca, but store metadata (name, description) alongside.

{
  "name": "get_weather",
  "description": "Fetch current weather for a location",
  "parameters": {
    "type": "object",
    "properties": {
      "location": { "type": "string" },
      "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
    },
    "required": ["location"]
  }
}

Keep this canonical object in your codebase or a registry. Every adapter reads from it. Avoid provider-specific fields like cache_control at this stage; inject those later.

2. Map to OpenAI’s tool format

OpenAI wraps functions in a tools array with type: "function". The inner function object matches your canonical name, description, and parameters directly.

def to_openai(tool):
    return {
        "type": "function",
        "function": {
            "name": tool["name"],
            "description": tool["description"],
            "parameters": tool["parameters"],
        },
    }

If you enable strict mode (recommended for reliable parsing), OpenAI requires additionalProperties: false at the top level of parameters and all properties listed in required. Transform defensively:

def to_openai_strict(tool):
    params = dict(tool["parameters"])
    params["additionalProperties"] = False
    params["required"] = list(params.get("properties", {}).keys())
    return {"type": "function", "function": {**tool, "parameters": params}}

Tradeoff: strict mode improves parser determinism but forces you to declare every field as required, which may not match optional UX.

3. Map to Anthropic’s tool format

Anthropic drops the function wrapper and renames parameters to input_schema. The shape is otherwise JSON Schema.

def to_anthropic(tool):
    return {
        "name": tool["name"],
        "description": tool["description"],
        "input_schema": tool["parameters"],
    }

Anthropic ignores title and $schema keywords; strip them to avoid 400s. If you want prompt caching on large tool sets, attach cache_control at the tool level:

tool_def = to_anthropic(canonical)
tool_def["cache_control"] = {"type": "ephemeral"}

Pitfall: Anthropic’s input_schema does not support default values reliably—the model may skip them. Validate post-hoc.

4. Handle Llama 4 tool calling

Llama 4’s native weights emit tool calls differently depending on serving stack. Meta’s hosted API mimics OpenAI’s tools structure, but self-hosted vLLM or TGI often stream a sentinel token followed by raw JSON.

A minimal native parser for a Llama 4 response containing a JSON blob:

import json, re

def parse_llama4(raw_text):
    m = re.search(r'\{"name":\s*"(\w+)",\s*"arguments":\s*(\{.*\})\}', raw_text)
    if not m:
        return None
    return {"name": m.group(1), "arguments": json.loads(m.group(2))}

If you serve Llama 4 behind an OpenAI-compatible server, treat it like step 2. The consistent tool schemas OpenAI Anthropic Llama 4 problem shrinks when the server normalizes output.

Tradeoff: native parsing is brittle against partial streams. Prefer a gateway or server that converts to tool_calls.

5. Normalize responses into one shape

Write a thin normalizer per provider so the rest of your app sees identical structures.

def normalize_openai(resp):
    out = []
    for tc in resp.choices[0].message.tool_calls:
        out.append({
            "id": tc.id,
            "name": tc.function.name,
            "arguments": json.loads(tc.function.arguments),
        })
    return out

def normalize_anthropic(resp):
    out = []
    for block in resp.content:
        if block.type == "tool_use":
            out.append({"id": block.id, "name": block.name, "arguments": block.input})
    return out

For Llama 4 native, wrap parse_llama4 output with a synthetic id.

This step is where consistent tool schemas OpenAI Anthropic Llama 4 pay off: downstream executors never branch on provider.

6. Stream and reassemble incrementally

Streaming tool calls means arguments arrive in chunks. OpenAI sends tool_calls deltas with function.arguments as string fragments. Anthropic streams input_json_delta inside content blocks. Llama 4 streams raw text.

OpenAI accumulation example:

buffer = {}
for chunk in stream:
    delta = chunk.choices[0].delta
    if delta.tool_calls:
        for tc in delta.tool_calls:
            idx = tc.index
            buffer.setdefault(idx, {"name": "", "arguments": ""})
            if tc.function and tc.function.name:
                buffer[idx]["name"] += tc.function.name
            if tc.function and tc.function.arguments:
                buffer[idx]["arguments"] += tc.function.arguments

calls = [{"name": b["name"], "arguments": json.loads(b["arguments"])} for b in buffer.values()]

Pitfall: do not parse arguments until the stream closes. Intermediate strings are invalid JSON.

Anthropic’s input_json_delta provides partial JSON you must concatenate similarly, then json.loads on completion.

7. Validate before execution

Run the parsed arguments through jsonschema using your canonical parameters. This catches model drift across all three providers.

from jsonschema import validate

def validate_args(tool, args):
    validate(instance=args, schema=tool["parameters"])

If validation fails, either retry with a correction prompt or fall back to a simpler model. The consistent tool schemas OpenAI Anthropic Llama 4 approach makes this retry logic uniform.

8. Route through a unified endpoint

Maintaining three HTTP clients is overhead. An OpenAI-compatible gateway that addresses 240+ models, such as n4n.ai, lets you send the same tools payload to OpenAI, Anthropic, and Llama 4 variants while the gateway applies provider-specific wrapping and honors cache-control hints. You keep one adapter (the OpenAI shape) and get automatic fallback when a provider is degraded.

Tradeoff: you lose fine-grained control over Anthropic-specific fields unless the gateway forwards them. Check that your gateway passes cache_control and client routing directives.

9. Common pitfalls and tradeoffs

  • Enum coercion: OpenAI strict mode rejects unknown enums; Llama 4 may lowercase. Normalize enums before validation.
  • Null vs missing: Anthropic sometimes omits optional keys; OpenAI includes null. Use .get() defensively.
  • Nested arrays: Deeply nested JSON Schema can confuse Llama 4’s tokenizer. Flatten if possible.
  • Streaming timeouts: Tool call streams without a final delimiter hang parsers. Set explicit read timeouts.
  • Schema drift: Providers silently tighten JSON Schema subsets. Pin versions and test in CI.

10. Test matrix

Build a CI job that sends the same canonical tool to each provider and asserts normalized output executes. Use recorded fixtures for Anthropic and Llama 4 to avoid cost. The investment in consistent tool schemas OpenAI Anthropic Llama 4 upfront reduces per-model special-casing to near zero.

Run a small suite:

pytest tests/test_tool_schema_openai.py tests/test_tool_schema_anthropic.py tests/test_tool_schema_llama4.py

Keep the canonical schema under version control. When a new Llama 4 checkpoint drops, only the parser in step 4 changes.

That’s the ordered path: define once, adapt per provider, normalize, stream safely, validate, and route smart. Your agent code stays clean while the models behind it swap freely.

Tagstool-callingschemasinteroperability

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All streaming, tool calling & structured outputs support posts →