Skip to main content
Lesson 3 of 20 · API Tools: Search, Files, and Code Interpreter

Parallel and Sequential Calls

Lesson 3 of 20 Developer
On this page
  1. Parallel and Sequential Calls
  2. Parallel Calls
  3. Disable Parallel Calls
  4. Sequential Calls with a Loop
  5. Worked Example: Processing Pipeline
  6. Hybrid Pattern: Parallel Then Sequential
  7. Key Takeaways

Parallel and Sequential Calls

The model can issue multiple function calls in a single response, doing so automatically whenever it identifies independent needs. This isn’t just cosmetic optimization: three two-second calls executed together take two seconds total, but chained sequentially they take six.

The real focus of this lesson isn’t how to parallelize—the model handles that—but how to recognize cases where you shouldn’t. Two calls are independent when execution order doesn’t affect the result. As soon as one operation reads what another writes, or a parameter must come from a previous call, parallelism becomes a hard-to-reproduce bug.

Parallel Calls

Nothing needs enabling: this is the default behavior. A query about three cities produces three function_call items in the same response; you execute them. Note that the model reasons about the meaning of functions based on your descriptions—descriptions drive the decision, as in almost everything in this course.

from openai import OpenAI
import json
import asyncio

client = OpenAI()

tools = [
    {
        "type": "function",
        "name": "get_meteo",
        "description": "Get current weather for a city",
        "parameters": {
            "type": "object",
            "properties": {
                "ville": {"type": "string"}
            },
            "required": ["ville"],
            "additionalProperties": False
        },
        "strict": True
    },
    {
        "type": "function",
        "name": "get_taux_change",
        "description": "Get exchange rate between two currencies",
        "parameters": {
            "type": "object",
            "properties": {
                "source": {"type": "string"},
                "cible": {"type": "string"}
            },
            "required": ["source", "cible"],
            "additionalProperties": False
        },
        "strict": True
    }
]

response = client.responses.create(
    model="gpt-5.6-terra",
    input="What's the weather in Paris and Berlin, "
          "and what is the EUR/USD exchange rate?",
    tools=tools
)

# The model emits 3 calls in parallel
appels = [item for item in response.output if item.type == "function_call"]
print(f"Nombre d'appels : {len(appels)}")
# => Nombre d'appels : 3

Execute Parallel Calls on the Client Side

Receiving three calls at once is useless if your code executes them one after another: you lose all the gain on the client side. asyncio.gather is the minimum; add two precautions for production—a concurrency limit, so you don’t open fifty simultaneous connections to a service that supports only ten, and per-call error handling, so an isolated failure doesn’t take down the other results.

async def executer_fonction(name: str, arguments: str) -> dict:
    args = json.loads(arguments)
    if name == "get_meteo":
        return await api_meteo(args["ville"])
    elif name == "get_taux_change":
        return await api_change(args["source"], args["cible"])

async def traiter_appels_paralleles(appels):
    taches = [
        executer_fonction(appel.name, appel.arguments)
        for appel in appels
    ]
    resultats = await asyncio.gather(*taches)

    return [
        {
            "type": "function_call_output",
            "call_id": appel.call_id,
            "output": json.dumps(resultat)
        }
        for appel, resultat in zip(appels, resultats)
    ]

# Execute
resultats = asyncio.run(traiter_appels_paralleles(appels))

# Send back to the model
response_finale = client.responses.create(
    model="gpt-5.6-terra",
    input=response.output + resultats,
    tools=tools
)
print(response_finale.output_text)

Disable Parallel Calls

parallel_tool_calls=False forces the model to emit only one call per response. Use this setting deliberately: it trades latency for safety. Reserve it for functions that write, modify, or trigger something external.

response = client.responses.create(
    model="gpt-5.6-terra",
    input="Process these three orders",
    tools=tools,
    parallel_tool_calls=False  # One call per response
)

Use cases for disabling parallelism:

  • Operations that must execute in a specific order
  • Functions with side effects that interfere with each other
  • When one call’s result conditions the next

Sequential Calls with a Loop

Sequential execution isn’t controlled by a parameter: it emerges from the loop. You execute the call, send back the result, and the model decides the next step based on what it just learned. This is precisely what distinguishes a dynamic chain from a rigid plan—if step 2 reveals that step 3 is unnecessary, the model won’t call it.

def boucle_function_calling(prompt: str, tools: list, max_tours: int = 10):
    """Complete loop for sequential function calling."""
    messages_input = prompt
    tour = 0

    while tour < max_tours:
        response = client.responses.create(
            model="gpt-5.6-terra",
            input=messages_input,
            tools=tools,
            parallel_tool_calls=False
        )

        # Check for function calls
        appels = [i for i in response.output if i.type == "function_call"]

        if not appels:
            # Model is done, responding with text
            return response.output_text

        # Execute each call
        resultats = []
        for appel in appels:
            resultat = executer_fonction_sync(appel.name, appel.arguments)
            resultats.append({
                "type": "function_call_output",
                "call_id": appel.call_id,
                "output": json.dumps(resultat)
            })

        # Prepare next turn
        messages_input = response.output + resultats
        tour += 1

    return "Turn limit reached"

Worked Example: Processing Pipeline

A typical case where sequential execution is mandatory, because each step consumes what the previous one produced:

tools_pipeline = [
    {
        "type": "function",
        "name": "valider_commande",
        "description": "Verify order validity (stock, price, customer)",
        "parameters": {
            "type": "object",
            "properties": {
                "commande_id": {"type": "string"}
            },
            "required": ["commande_id"],
            "additionalProperties": False
        },
        "strict": True
    },
    {
        "type": "function",
        "name": "calculer_livraison",
        "description": "Calculate shipping fees and delays. "
                       "Requires a validated order.",
        "parameters": {
            "type": "object",
            "properties": {
                "commande_id": {"type": "string"},
                "mode": {"type": "string", "enum": ["standard", "express"]}
            },
            "required": ["commande_id", "mode"],
            "additionalProperties": False
        },
        "strict": True
    },
    {
        "type": "function",
        "name": "confirmer_commande",
        "description": "Confirm the order and trigger payment. "
                       "Requires prior validation and shipping calculation.",
        "parameters": {
            "type": "object",
            "properties": {
                "commande_id": {"type": "string"},
                "livraison_id": {"type": "string"}
            },
            "required": ["commande_id", "livraison_id"],
            "additionalProperties": False
        },
        "strict": True
    }
]

# The model will naturally call in this order:
# 1. valider_commande
# 2. calculer_livraison (using step 1 result)
# 3. confirmer_commande (using results from steps 1 and 2)
resultat = boucle_function_calling(
    "Process order CMD-42567 with express shipping",
    tools_pipeline
)

Hybrid Pattern: Parallel Then Sequential

In practice, most real-world workflows follow this mixed pattern: a collection phase where everything can run simultaneously, followed by a processing phase that respects dependency dependencies. You don’t need to code anything special for this—it’s the natural consequence of the loop, provided your descriptions make dependencies clear.

# Turn 1: Parallel data collection
# The model calls get_client AND get_stock in parallel

# Turn 2: Sequential decision based on results
# The model calls valider_commande with collected info

# Turn 3: Final action
# The model calls confirmer_commande

This pattern feels natural: the model automatically parallelizes when calls are independent, then chains sequentially when a dependency appears.

Key Takeaways

  • The model automatically parallelizes independent calls
  • Use asyncio.gather to execute parallel calls on the client side
  • Disable with parallel_tool_calls=False when order matters
  • Implement a loop for multi-step workflows
  • Function descriptions guide the model on dependencies between steps