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.