SDK Diff: Anthropic and OpenAI-compatible¶
The same tool loop has different wrappers. Identify the fields, then validate every arg supplied by the model.
The main differences¶
| Part | Anthropic SDK | OpenAI-compatible SDK |
|---|---|---|
| Tool schema | {name, description, input_schema} |
{"type":"function","function":{name, description, parameters}} |
| Tool call | tool_use content block |
message.tool_calls |
| Args | call.input is a dict |
call.function.arguments is a JSON string |
| Tool result ID | tool_use_id |
tool_call_id |
| Common completion signal | stop_reason == "end_turn" |
finish_reason == "stop" |
Providers and versions can add other values. Check the current official documentation and keep the raw response for debugging.
Safety guards needed on both paths¶
A schema is a hint, not a firewall. Apply an allowlist—the short list of tools the app permits—and validate args before executing a tool.
import json
MAX_STEPS = 5
ALLOWED_TOOLS = {"get_weather"}
def validate_args(name, args):
if name not in ALLOWED_TOOLS:
raise ValueError(f"tool not allowed: {name}")
if not isinstance(args, dict):
raise ValueError("args must be an object")
city = args.get("city")
if not isinstance(city, str) or not city.strip():
raise ValueError("city must be a non-empty string")
return {"city": city.strip()}
def call_tool(name, args):
clean_args = validate_args(name, args)
return TOOL_IMPL[name](**clean_args)
def expected_error(exc):
return json.dumps({"ok": False, "error": str(exc)})
Convert only expected input errors into structured results. Log and surface unexpected exceptions; never swallow them silently.
Anthropic: bounded loop¶
messages = [{"role": "user", "content": "Is it raining in Taipei?"}]
for step in range(MAX_STEPS):
resp = client.messages.create(
model=MODEL, max_tokens=1024, tools=TOOLS, messages=messages
)
messages.append({"role": "assistant", "content": resp.content})
calls = [block for block in resp.content if block.type == "tool_use"]
if resp.stop_reason == "end_turn" and not calls:
break
tool_results = []
for call in calls:
try:
result = call_tool(call.name, call.input)
content = json.dumps({"ok": True, "result": result})
except ValueError as exc:
content = expected_error(exc)
tool_results.append({
"type": "tool_result",
"tool_use_id": call.id,
"content": content,
})
if not tool_results:
raise RuntimeError(f"unexpected stop_reason: {resp.stop_reason}")
messages.append({"role": "user", "content": tool_results})
else:
raise RuntimeError("tool loop reached MAX_STEPS")
OpenAI-compatible: bounded loop¶
messages = [{"role": "user", "content": "Is it raining in Taipei?"}]
for step in range(MAX_STEPS):
resp = client.chat.completions.create(
model=MODEL, tools=TOOLS, messages=messages
)
msg = resp.choices[0].message
messages.append(msg.model_dump(exclude_none=True))
if not msg.tool_calls:
if resp.choices[0].finish_reason == "stop":
break
raise RuntimeError(
f"unexpected finish_reason: {resp.choices[0].finish_reason}"
)
for call in msg.tool_calls:
try:
args = json.loads(call.function.arguments)
result = call_tool(call.function.name, args)
content = json.dumps({"ok": True, "result": result})
except (json.JSONDecodeError, ValueError) as exc:
content = expected_error(exc)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": content,
})
else:
raise RuntimeError("tool loop reached MAX_STEPS")
Four easy mistakes¶
parametersandinput_schemaare different wrappers.- OpenAI-compatible arguments need
json.loads; both paths still need validation. - Every result must carry the matching
tool_call_idortool_use_id. - The loop must preserve complete assistant history and enforce
MAX_STEPS.
Runnable comparisons: Stage 3 multi-tool selection through schema design.