Back to Blog
EnglishGuide

Function Calling Across AI Models: Two Completed Loops and One Timeout

Run the same tool-call loop across GPT, Claude and Gemini routes. Two complete round trips, one timeout and six executor fixtures show the tested compatibility limits.

J
Jeff M
September 19, 2026 / 371 views
Share:
Function Calling Across AI Models: Two Completed Loops and One Timeout

Portable function calling requires a complete loop: receive a tool request, validate and authorize it, execute the allowed operation, return the result under the original call ID, and verify the final answer. Accepting a tools parameter is only the start of that contract.

Crazyrouter is the AI API gateway used in this three-model tool test: GPT-4.1-mini and Claude Sonnet 4.6 completed the same arithmetic round trip, while the Gemini 2.5 Flash route timed out. Six local executor fixtures also passed. The timeout remains part of the result.

By Jeff M · Tested November 3, 2026

Is function calling portable across model APIs?#

An OpenAI-compatible Chat Completions route can let the same application use the same request shape for several models. It does not establish identical behavior for every schema, endpoint or failure. The OpenAI function-calling documentation describes the request/result loop; native Claude tools use a different message representation.

This test used compatible Chat Completions for all three selected models. It did not test native Gemini function declarations, OpenAI Responses or native Claude Messages. See the protocol comparison before moving a payload between them.

The same synthetic operation on three routes#

On November 3, 2026, Python 3.10.2 called /v1/chat/completions at https://api.crazyrouter.com. The tool was add_numbers, with integer arguments a=2 and b=3. The application enforced an exact argument-key set and a range of −100 through 100. After the tool returned {"sum":5}, the final model response had to stop and contain only 5.

ModelComplete round tripFinal answer / failure
gpt-4.1-miniYes5
gemini-2.5-flashNoReadTimeout
claude-sonnet-4-6Yes5

Observed response IDs (successful responses only):

text
chatcmpl-Eg6BfE7HpCkZspVy2QhYqoBrdTypP
chatcmpl-Eg6Bi7okkTCjTaqmbhG281dnAYGrn
msg_011CgfrGwnudTVmAmTMq3j4T
msg_011CgfrH6xKJmDZ1d8akEe4h

Two completed tool round trips and one timeout in the controlled matrix

An initial version of the probe aborted on a 90-second read timeout and did not preserve per-model partial results. The revised probe records each route independently with a 45-second request timeout. It again observed a Gemini read timeout. We do not assign an HTTP status or final answer to that failed sample, and we did not trace its internal cause.

A complete executable loop#

Install requests, set CRAZYROUTER_API_KEY in the process environment, save the program as portable_tools.py, and run it. The executor performs local integer addition only. It cannot call a shell or access customer systems.

python
"""Finish the same tool round trip on three compatible model routes."""
import json
import os
import requests

MODELS=['gpt-4.1-mini','gemini-2.5-flash','claude-sonnet-4-6']

def execute(name,args):
    if name!='add_numbers' or not isinstance(args,dict) or set(args)!={'a','b'}:raise ValueError('invalid tool contract')
    if any(type(args[k]) is not int or abs(args[k])>100 for k in ('a','b')):raise ValueError('expected bounded integers')
    return {'sum':args['a']+args['b']}

def main():
    tests=[]
    assert execute('add_numbers',{'a':2,'b':3})=={'sum':5}
    tests.append({'case':'valid integers','passed':True})
    for label,name,args in [('unknown tool','shell',{'a':2,'b':3}),('boolean','add_numbers',{'a':True,'b':3}),('extra field','add_numbers',{'a':2,'b':3,'x':0}),('missing field','add_numbers',{'a':2}),('out of range','add_numbers',{'a':999,'b':3})]:
        try:execute(name,args)
        except ValueError:tests.append({'case':label,'passed':True})
        else:raise AssertionError(label)
    s=requests.Session();s.trust_env=False;s.headers['Authorization']='Bearer '+os.environ['CRAZYROUTER_API_KEY']
    endpoint='https://api.crazyrouter.com/v1/chat/completions'
    tool={'type':'function','function':{'name':'add_numbers','description':'Add two synthetic small integers.','parameters':{'type':'object','properties':{'a':{'type':'integer'},'b':{'type':'integer'}},'required':['a','b'],'additionalProperties':False}}}
    rows=[]
    def round_trip(model):
        payload={'model':model,'max_tokens':160,'tools':[tool],'tool_choice':{'type':'function','function':{'name':'add_numbers'}},'messages':[{'role':'user','content':'Use add_numbers for a=2 and b=3. After its result, reply only 5.'}]}
        def send():
            r=s.post(endpoint,json=payload,timeout=45);r.raise_for_status();return r.json()
        first=send();message=first['choices'][0]['message'];calls=message.get('tool_calls') or []
        assert len(calls)==1
        call=calls[0];args=json.loads(call['function']['arguments']);result=execute(call['function']['name'],args)
        assert args=={'a':2,'b':3}
        payload['messages']+=[message,{'role':'tool','tool_call_id':call['id'],'content':json.dumps(result)}]
        payload.pop('tool_choice');last=send();choice=last['choices'][0]
        return {'model':model,'first_id':first['id'],'last_id':last['id'],'call_id':call['id'],'args':args,'tool_result':result,'answer':choice['message'].get('content'),'finish_reason':choice['finish_reason'],'passed':choice['finish_reason']=='stop' and choice['message'].get('content','').strip()=='5'}
    for model in MODELS:
        try:rows.append(round_trip(model))
        except (requests.RequestException,ValueError,KeyError,AssertionError) as error:
            rows.append({'model':model,'passed':False,'error':type(error).__name__})
    print(json.dumps({'endpoint':endpoint,'local_tests':tests,'round_trips':rows}))

if __name__=='__main__':main()

An exit code of zero means the diagnostic matrix completed, including any recorded failed route. Read the individual passed fields. The script never marks a timeout successful just because another model worked.

Validate arguments before running the tool#

Local fixtureExecutor decision
Exact keys and two bounded integersAccepted; sum is 5
Unknown tool nameRejected
Boolean instead of integerRejected
Extra argumentRejected
Missing argumentRejected
Value outside allowed rangeRejected

Six local executor fixtures accepted one valid input and rejected five invalid contracts

These are application tests, not model refusal tests. A model can propose invalid arguments; the server still owns the execution decision. Real record access also requires trusted tenant authorization, not just a schema.

Preserve identity and define the retry boundary#

Copy the actual assistant message and its call ID into the conversation. Return a tool-role result for that ID. Do not invent a replacement ID or silently drop a requested operation. The native Claude pairing guide covers the corresponding native representation.

The example verifies a single tool call and one final answer. A production agent needs a bounded loop, an overall deadline and an explicit policy if the model asks for further calls. The bounded retry guide explains why a transport timeout cannot establish that no work happened. Replaying a payment or message-sending tool would require stronger deduplication than this read-only arithmetic fixture.

Keep final-answer validation separate from tool-argument validation. The former checks what your application receives; the latter checks what your application may execute. Neither a successful addition nor this small matrix ranks general model intelligence or reliability.

FAQ#

Does tools support prove the full loop works?#

No. Verify the requested arguments, returned call ID, tool result and final answer together.

Were all three routes successful?#

No. GPT-4.1-mini and Claude Sonnet 4.6 completed; Gemini 2.5 Flash timed out in this matrix.

Can I paste this into a native Claude request?#

Not unchanged. Native Messages uses its own tool and result blocks.

Why reject booleans as integers?#

Python's boolean type is an integer subclass. The example uses exact type checks to enforce the intended contract.

Can the model execute a different tool name?#

It can propose one, but this executor rejects every name except add_numbers.

Can I automatically repeat a timed-out tool workflow?#

Only under an explicit replay policy. A missing response does not prove the operation was never performed.

Implementation Guides

Podcast

AI Dev Tools: The Crazyrouter Podcast

Short episodes for developers on AI APIs, gateways, cost control and coding tools.

Latest episodes

  • EP196 · Oct 29, 2026 · 6:53

    AI API Security Posture — Turn Controls Into Continuous Evidence

    A practical guide to AI API security posture: inventory exposure, verify controls continuously, protect keys and data, constrain tools, and turn security assumptions into evidence that operators can act on.

  • EP195 · Oct 28, 2026 · 7:21

    AI API Policy Drift — Detect When Production Rules Stop Matching Intent

    A practical guide to policy drift in AI APIs: version routing and safety rules, detect stale enforcement, compare intended and observed behavior, and restore control before small gaps become incidents.

Episodes with written show notes →

Topics

Related Articles