How to Write a Tool Calling API? Four-Layer Cross-Model Differences and the Loop Skeleton

2026-08-26 70 0

The tool calling API loop skeleton has only four parts: declare tools → send request → execute tool_calls and return results using tool_call_id → check if there are more tool_calls to decide whether to continue. The differences concentrate on four layers: function declarations, trigger control, return structure, and recursive termination.

Difference LayerCommon Failure Symptoms
Function declarationsMissing fields, Schema rejected or parse errors after model switch
Trigger controlModel doesn't return tool_calls, gives text directly
Return structuretool_call_id mismatched or missing in parallel calls
Recursive terminationInfinite loop or mid-loop interruption in multi-step loops

The specifications in this article are based on the official documentation status available as of August 2026.

Clarifying Concepts First: Are Function Calling and Tool Calling the Same Thing?

Many developers get confused by terms like “function calling,” “tool calling,” and “MCP tool.” Simply put, OpenAI’s early functions field and the current tools / tool_calls are the same thing in different generations of writing; while tool in the MCP spec is the same JSON Schema expressed in another protocol (JSON-RPC).

So you can directly map name, description, inputSchema from MCP to the OpenAI format tools definition. The mental model is: declaration formats are mappable, calling protocols differ. Don’t debug them mixed together, or you’ll waste a lot of time.

Difference 1: Function Declarations—How to Write JSON Schema That Works Across Multiple Models

In the tool calling API, each function consists of three parts: name, description, and parameter JSON Schema. This is similar to MCP’s name / description / inputSchema structure.

Engineering recommendations:

  • Explicitly write required: Missing it may cause missing parameters.
  • Avoid deep nesting and overly long enums: Some models have difficulty parsing them.
  • In description, describe when to call: Not just “what it does,” but also “under what conditions.”

Note that the specific supported range depends on each vendor’s official docs; there is no unified fixed number.

Difference 2: Trigger Control—When to Use auto, required, none, and Specific Functions

tool_choice has four possible values, each with different semantics (see OpenAI Function calling official documentation):

ValueSemanticsTypical Use Case
"auto"Model decides whether to callExploratory conversation
"required"Force at least one function callNeed structured output
"none"Disallow calls, pure textFallback or simple Q&A
{type:"function", function:{name:"xxx"}}Force a specific functionRoute to a particular feature

So when “model doesn’t return tool_calls,” the troubleshooting order is: first check if tool_choice is none or auto and the model decided it wasn’t needed; then check if description provides clear trigger conditions; only then suspect model capability.

Difference 3: Return Structure—How to Unpack Parallel tool_calls and Pair ids with tool Messages

The model returns a tool_calls list, each with a unique id. After executing the business logic, you must return for each id a message with role tool and the corresponding tool_call_id. Missing, mismatching, or combining return messages is the most common source of 400 errors in multi-step loops.

The parallel_tool_calls switch determines whether parallel calls are allowed: parallel saves round trips, serial is easier to troubleshoot. Process roughly like this:

for tool_call in assistant_msg.tool_calls:
    # 执行工具,得到 result
    messages.append({'role': 'tool', 'tool_call_id': tool_call.id, 'content': result})

Note: You must first messages.append(assistant_msg), then return the tool message. See the next section for the complete loop.

Difference 4: Multi-step Recursion—Termination Conditions, Maximum Steps, and Fallback on Errors

How to write a multi-step tool calling loop without infinite loops? The following skeleton is self-contained:

import json
import openai

def get_weather(city):
    return f"{city} 天气晴,25°C"

client = openai.OpenAI()
MODEL = "..."
messages = [{"role": "user", "content": "查询北京和上海天气,并总结"}]
max_steps = 5

TOOLS = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "获取指定城市的天气",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string"}
            },
            "required": ["city"]
        }
    }
}]

for step in range(max_steps):
    resp = client.chat.completions.create(
        model=MODEL,
        messages=messages,
        tools=TOOLS,
        tool_choice="auto"
    )
    assistant_msg = resp.choices[0].message
    if not assistant_msg.tool_calls:
        break
    messages.append(assistant_msg)
    for tool_call in assistant_msg.tool_calls:
        try:
            args = json.loads(tool_call.function.arguments)
            result = get_weather(**args)
        except Exception as e:
            result = f"错误: {e}"
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": result
        })
else:
    # 超过最大步数,降级要求文本结论
    messages.append({
        "role": "user",
        "content": "请直接给出文本结论,不要再调用工具。"
    })
    resp = client.chat.completions.create(model=MODEL, messages=messages)

When tool execution fails, return the error message as the content of the tool message instead of raising an interruption; after exceeding the limit, fall back to returning a text conclusion. This keeps the loop stable and non-blocking.

Write Once, Run Everywhere: Consolidating the Four Differences into a Thin Adapter

The pain of migrating the tool calling API across models can be solved with a thin adapter: business code only depends on a unified tool registry and result structure. OpenAI’s tools/tool_choice structure and MCP’s tools/list, tools/call JSON-RPC interfaces are two de facto standards; the engineering focus should shift from protocol selection to hardening Schema and termination logic. The adapter handles four things:

  • Schema downgrade: Flatten deep nesting, fill in required fields
  • Normalize tool_choice semantics: Unify to auto/required/none/specific function
  • Normalize tool_calls: Convert to a list structure uniformly
  • Streaming chunk accumulation: Merge tool call chunks in streaming mode

It’s best to add observable points in the adapter layer, recording model, step number, tool name, and duration for each step to facilitate troubleshooting. For further reading on cross-model migration, see OpenAI API migration.

Cross-model tool calling four-layer difference matrix

How to Verify the Adapter Layer Is Truly Portable: One Schema, One Loop, Only Change the model to Run Comparisons

The minimal reproducible verification method: Use a fixed tool schema, the same loop code, and only change the model parameter on an OpenAI-compatible endpoint, referring to How to change OpenAI base_url. Record whether the tool triggers, parameter completeness, parallel unpacking shape, and step count.

For example, point base_url to NexAIX’s OpenAI-compatible endpoint (https://api.nexaix.net/v1),同一套代码只改 model can horizontally run across different models. Pay special attention to the model field in the response, which corresponds to the actual executing model—this determines whether you can attribute “not triggering tools” to a specific model rather than fields being swallowed by middleware. Current available models and specs are subject to NexAIX Models page.

Tool Calling Regression Checklist to Run Before Model Switching

Check ItemFailure Location Guidance
Single function triggerSchema and description
Parallel multi-functiontool_call_id return
required forced triggerWhether model supports required
Nested parameter parsingSchema downgrade
Tool error fallbacktry/except and return content
Step limit downgrademax_steps and downgrade logic
Streaming chunk consumptionChunk accumulation
tool_call_id pairingMissing in return loop

Frequently Asked Questions

What to do if tool_calls format differs after changing models?

First, check if the response conforms to OpenAI Chat Completions spec. If field names differ, normalize at the adapter layer. All models follow official documentation.

What is the difference between tool_choice required and auto?

required forces the model to call at least one function, suitable for structured output; auto lets the model decide, suitable for open dialogue. If the model isn’t triggered under auto, first check description.

How to return parallel tool call results?

Add a separate tool_call_id message for each role: "tool", don’t combine them. Then append the assistant message and make the next request.

Why doesn’t the model return tool_calls?

Check in order: whether tool_choice is set to none or auto and the model thinks it’s unnecessary; whether description clearly describes trigger conditions; only then suspect model capability. Sometimes middleware swallows fields.

Are function calling and tool calling the same?

Basically yes. Today’s tool calling API is the current version of early function calling. The tool in MCP is another protocol expression of the same declaration, and the structures are mappable.

Last updated on 2026-08-26 21:08:11

Related Posts

How to Integrate Agent APIs: Four Verification Points from Framework Configur...
How to Write a Tool Calling API? Four-Layer Cross-Model Differences and the L...
How Long Does LLM API Caching Last? Choosing Between 5 Minutes and 1 Hour

Comments(0)

No comments yet

Leave a Comment