n4nAI

Migrating function calling from OpenAI to Claude

Step-by-step function calling migration OpenAI to Claude tutorial: adapt OpenAI tool definitions to Anthropic schema, call Claude, and parse tool_use in Python.

n4n Team2 min read528 words

Audio narration

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

Function calling migration OpenAI to Claude requires more than swapping the model name. The two providers use different request shapes, tool schema formats, and response structures. This tutorial walks through a concrete Python migration, from an OpenAI baseline to a working Claude implementation.

Prerequisites

  • Python 3.10 or newer
  • openai and anthropic Python SDKs
  • API keys for both providers (set as OPENAI_API_KEY and ANTHROPIC_API_KEY)
pip install openai anthropic

You should understand the basic OpenAI tool-calling loop. We’ll migrate a simple weather lookup tool.

OpenAI baseline

Here is a minimal OpenAI function calling loop. It defines a get_weather tool, asks the model to use it, executes the function, and returns the result.

from openai import OpenAI
import json

client = OpenAI()

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current temperature for a location",
            "parameters": {
                "type": "object",
                "properties": {"location": {"type": "string"}},
                "required": ["location"],
            },
        },
    }
]

messages = [{"role": "user", "content": "What's the weather in San Francisco?"}]
resp = client.chat.completions.create(model="gpt-4o", tools=tools, messages=messages)
msg = resp.choices[0].message

if msg.tool_calls:
    call = msg.tool_calls[0]
    args = json.loads(call.function.arguments)
    # Fake execution
    result = f"{args['location']} is 72F"
    messages.append({"role": "assistant", "content": None, "tool_calls": [call]})
    messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
    final = client.chat.completions.create(model="gpt-4o", tools=tools, messages=messages)
    print(final.choices[0].message.content)

Expected output:

San Francisco is 72F.

Key differences in Claude’s tool use

Before rewriting, note the structural gaps:

  • Tool schema: OpenAI wraps everything in function. Claude uses a flat object with input_schema.
  • System prompt: OpenAI accepts system inside messages. Claude takes a top-level system parameter.
  • Response: OpenAI returns tool_calls on the message. Claude returns content blocks of type tool_use.
  • Round-trip: OpenAI uses role: "tool". Claude expects a user message containing tool_result blocks.

Step 1: Convert tool definitions

Map the OpenAI tool to Anthropic shape.

openai_tool = {
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get current temperature for a location",
        "parameters": {
            "type": "object",
            "properties": {"location": {"type": "string"}},
            "required": ["location"],
        },
    },
}

anthropic_tool = {
    "name": "get_weather",
    "description": "Get current temperature for a location",
    "input_schema": {
        "type": "object",
        "properties": {"location": {"type": "string"}},
        "required": ["location"],
    },
}

No type: "function" wrapper. The JSON schema lives under input_schema.

Nested and array parameters

Both providers use JSON Schema, but Claude is stricter about type on every property. If your OpenAI schema omits type for some fields, Claude rejects it. Always specify type.

{
  "input_schema": {
    "type": "object",
    "properties": {
      "coordinates": {
        "type": "array",
        "items": {"type": "number"}
      }
    }
  }
}

OpenAI often tolerates implicit typing; Claude does not.

Step 2: Call Claude with tools

Instantiate the Anthropic client and call messages.create. You must set max_tokens (Claude has no default). Pass system separately.

from anthropic import Anthropic

client = Anthropic()
SYSTEM = "You are a helpful assistant that can check weather."

initial = client.messages.create(
    model="claude-3-5-sonnet-20240620",
    max_tokens=1024,
    system=SYSTEM,
    tools=[anthropic_tool],
    messages=[{"role": "user", "content": "What's the weather in San Francisco?"}],
)
print(initial.content)

Expected output (abbreviated):

[{'type': 'tool_use', 'id': 'toolu_01A', 'name': 'get_weather', 'input': {'location': 'San Francisco'}}]

Step 3: Handle the tool_use response

Claude returns a list of content blocks. Iterate and filter for type == "tool_use".

tool_use = next(b for b in initial.content if b.type == "tool_use")
args = tool_use.input
result = f"{args['location']} is 72F"

The id field is required for the round-trip.

Step 4: Return tool results

Claude expects the assistant’s tool_use block echoed, then a user message with tool_result.

follow_up = client.messages.create(
    model="claude-3-5-sonnet-20240620",
    max_tokens=1024,
    system=SYSTEM,
    tools=[anthropic_tool],
    messages=[
        {"role": "user", "content": "What's the weather in San Francisco?"},
        {"role": "assistant", "content": [tool_use]},
        {"role": "user", "content": [
            {
                "type": "tool_result",
                "tool_use_id": tool_use.id,
                "content": result,
            }
        ]},
    ],
)
print(follow_up.content[0].text)

Expected output:

San Francisco is 72F.

Step 5: Complete migrated loop

Here is the full Claude migration in one script.

from anthropic import Anthropic

client = Anthropic()
SYSTEM = "You are a helpful assistant that can check weather."
tool = {
    "name": "get_weather",
    "description": "Get current temperature for a location",
    "input_schema": {
        "type": "object",
        "properties": {"location": {"type": "string"}},
        "required": ["location"],
    },
}

messages = [{"role": "user", "content": "What's the weather in San Francisco?"}]
resp = client.messages.create(
    model="claude-3-5-sonnet-20240620",
    max_tokens=1024,
    system=SYSTEM,
    tools=[tool],
    messages=messages,
)

tool_use = next((b for b in resp.content if b.type == "tool_use"), None)
if tool_use:
    result = f"{tool_use.input['location']} is 72F"
    messages.append({"role": "assistant", "content": [tool_use]})
    messages.append({"role": "user", "content": [
        {"type": "tool_result", "tool_use_id": tool_use.id, "content": result}
    ]})
    final = client.messages.create(
        model="claude-3-5-sonnet-20240620",
        max_tokens=1024,
        system=SYSTEM,
        tools=[tool],
        messages=messages,
    )
    print(final.content[0].text)

Run it. You get the same answer as the OpenAI version, but the wire format is entirely different.

Parallel tool calls

Both models can request multiple tools in one turn. OpenAI returns a list of tool_calls; Claude returns multiple tool_use blocks in content. Handle them uniformly by iterating.

tool_uses = [b for b in resp.content if b.type == "tool_use"]
results = []
for tu in tool_uses:
    results.append({"tool_use_id": tu.id, "content": f"{tu.input['location']} is 72F"})

messages.append({"role": "assistant", "content": tool_uses})
messages.append({"role": "user", "content": [
    {"type": "tool_result", "tool_use_id": r["tool_use_id"], "content": r["content"]}
    for r in results
]})

This mirrors OpenAI’s list of tool_calls and keeps your orchestration logic provider-agnostic.

Alternative: skip the rewrite

If you want to avoid maintaining two code paths, an OpenAI-compatible gateway like n4n.ai fronts 240+ models behind one endpoint with automatic fallback. Your existing OpenAI function calling code runs against Claude without schema translation. That trades per-provider control for operational simplicity.

Gotchas

  • max_tokens is mandatory on Claude. Forget it and the SDK raises.
  • Tool name limits: Claude restricts names to ^[a-zA-Z0-9_-]{1,64}$. OpenAI is looser.
  • Streaming: OpenAI streams tool_calls deltas; Claude streams content blocks with tool_use partial JSON. Parse differently.
  • Error handling: Claude returns error blocks inside content if a tool result is malformed. Check block types before assuming text.
  • System separation: Putting system text in the first user message degrades Claude’s instruction adherence. Use the system parameter.

Function calling migration OpenAI to Claude is mechanical once you internalize the schema and message shape differences. Write a thin adapter for tools and responses, and your app logic stays stable.

Tagsfunction-callingopenaiclaudemigration

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 migrating between llm providers posts →