Back to blog

API & SDKGuide

Function calling in Python with an OpenAI-compatible API

Send tools, read tool_calls and return the result with role tool: a two-step function calling loop in Python with MiniMax-M2.7.

By CheapTokens TeamPublished Updated 6 min read

Diagram of a two-step tool call loop: send tools, read tool_calls, return the result as a tool message
On this page
  1. How function calling works
  2. Set up the client
  3. Declare a function and send the first request
  4. What you should see
  5. Several calls and longer loops
  6. Why MiniMax-M2.7 suits tool loops
  7. If it does not work
  8. Next steps

Function calling (tool calling) lets a model ask your code to run a function. You send a tools list with the request, the model answers with tool_calls instead of text, you run the function yourself, and you send its result back in a message with role: tool. The model then writes the final answer. This guide shows the whole loop in Python against the OpenAI-compatible endpoint https://api.cheaptokens.dev/v1/chat/completions with MiniMax-M2.7 (id MiniMaxAI/MiniMax-M2.7, 180K context, up to 8,192 output tokens per answer).

Tool calling is listed as supported for every model in the catalog (CheapTokens docs, checked on 22 October 2026). You need an API key; if you have not made a request yet, start with your first API request.

How function calling works#

The model never runs anything. It only produces a structured request: "call get_weather with {\"city\": \"Paris\"}". Your program does the real work and reports back. That makes a loop of two requests:

  1. Request 1. You send the user message plus the tools list. The model either answers in text or returns one or more tool_calls.
  2. Your code. You parse each call, run the matching function and collect the result.
  3. Request 2. You send the whole conversation again: the original messages, the assistant message with tool_calls, and one role: tool message per call. The model uses the results to write the final answer.

Every request resends the whole history, so tool loops use more input tokens than a single question. If this is new to you, read what a token is.

Set up the client#

Install the OpenAI SDK (pip install openai) and put your key in an environment variable instead of the source code:

bash
export CHEAPTOKENS_API_KEY="your-key-here"

The base URL is https://api.cheaptokens.dev/v1. Take the model id from the CheapTokens documentation: different providers name the same model differently. This guide uses MiniMaxAI/MiniMax-M2.7.

Declare a function and send the first request#

A function is described in the tools array. Each entry has type: function and a function object with a name, a description and parameters written as JSON Schema. The description is what the model reads to decide when to call the function, so write it as a short, concrete sentence.

Below is a complete script. The weather function is a stub with fake data, so the example needs no extra services:

python
import json
import os

from openai import OpenAI

client = OpenAI(
    base_url='https://api.cheaptokens.dev/v1',
    api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'MiniMaxAI/MiniMax-M2.7'


def get_weather(city: str) -> dict:
    # Stub with fake data: replace with a real lookup
    fake = {'Paris': 14, 'Tokyo': 19}
    return {'city': city, 'temp_c': fake.get(city, 10)}


FUNCTIONS = {'get_weather': get_weather}

tools = [
    {
        'type': 'function',
        'function': {
            'name': 'get_weather',
            'description': 'Get the current temperature in Celsius for a city.',
            'parameters': {
                'type': 'object',
                'properties': {
                    'city': {'type': 'string', 'description': 'City name, for example Paris'},
                },
                'required': ['city'],
            },
        },
    }
]

messages = [{'role': 'user', 'content': 'What is the weather in Paris right now?'}]

# Step 1: the model decides whether to call a function
first = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
message = first.choices[0].message

if not message.tool_calls:
    print(message.content)
    raise SystemExit

# Step 2: run every requested call and send the results back
messages.append(message)
for call in message.tool_calls:
    args = json.loads(call.function.arguments)
    result = FUNCTIONS[call.function.name](**args)
    messages.append({
        'role': 'tool',
        'tool_call_id': call.id,
        'content': json.dumps(result),
    })

final = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
print(final.choices[0].message.content)

Three details decide whether the loop works:

  • call.function.arguments is a JSON string, not a dictionary. Parse it with json.loads.
  • Each role: tool message must carry the tool_call_id of the call it answers, which is call.id.
  • The content of a tool message is a string. Use json.dumps for structured results.

What you should see#

If you print the fields after the first request, they look like this (the call id differs on every run):

text
finish_reason: tool_calls
call.id: call_abc123
call.function.name: get_weather
call.function.arguments: {"city": "Paris"}

The script then prints the final answer, a plain sentence along the lines of "It is currently 14 °C in Paris." The wording changes from run to run; the number comes from your function.

Several calls and longer loops#

A model can return more than one call in a single response, for example when you ask about the weather in two cities. That is why the example loops over message.tool_calls and answers each call by its own id. OpenAI's function calling guide describes the same behaviour: with the default tool_choice of auto the model may call zero, one or several functions (OpenAI docs, checked on 22 October 2026; the page has no last-updated date and describes the newer Responses API shape, while this guide uses the Chat Completions shape of the /v1/chat/completions endpoint).

Agents repeat the two steps until the model stops asking for tools. Wrap the requests in a loop and always set a step limit:

python
for _ in range(5):  # safety limit against endless loops
    response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
    message = response.choices[0].message
    if not message.tool_calls:
        print(message.content)
        break
    messages.append(message)
    for call in message.tool_calls:
        args = json.loads(call.function.arguments)
        result = FUNCTIONS[call.function.name](**args)
        messages.append({'role': 'tool', 'tool_call_id': call.id, 'content': json.dumps(result)})

If you want the answer to appear token by token, combine this with streaming in Python.

Why MiniMax-M2.7 suits tool loops#

A tool loop sends many requests with a growing history. For that pattern CheapTokens offers MiniMax-M2.7: 180K context, up to 8,192 output tokens per answer, one price at any time of day with no peak hours, and one balance for all models. Input and output counted together, it is cheaper than OpenRouter's reference price (CheapTokens price comparison, checked 31 October 2026).

Current prices per 1M tokens: input $0.05, output $0.15. As an example workload (not a measurement), the two-request round trip above with about 2,700 input and 350 output tokens in total costs $0.00019. The full price list is on the pricing page.

To try another model, change only the id; the list of ids is on the models page.

If it does not work#

  • message.tool_calls is None. The model answered in text. Make the function description more specific, and make sure the question actually needs the function.
  • json.JSONDecodeError on arguments. The model produced invalid JSON. Wrap json.loads in try/except and send the error text back as the tool result, so the model can retry with corrected arguments.
  • The second request fails or the model ignores the result. Check that every call has exactly one role: tool message with a matching tool_call_id, and that the assistant message with tool_calls is in the history before them.
  • KeyError in FUNCTIONS[...]. The model named a function you did not register. Return a short error string as the tool result instead of crashing.
  • The answer is cut off. Check finish_reason: the value length means the output token limit was reached. See why an AI answer stops mid-sentence.
  • 401 or authentication errors. Check that the key is in CHEAPTOKENS_API_KEY and that the base URL ends with /v1.

Next steps#

Replace the stub with a real function: a database query, a search call or a calculator. Add more entries to tools and to FUNCTIONS, and keep every function result short, because it goes back into the history on each following request. To get a key and make the first call, open the CheapTokens docs.