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

Combine tools in a single call

Lesson 19 of 20 Updated November 2, 2026 Developer
On this page
  1. Combine tools in a single call
  2. Principle of combination
  3. Complete example: analytical assistant
  4. Manage the multi-tool loop
  5. Orchestration strategies
  6. Pattern: multi-skill agent
  7. Key takeaways

Updated November 2, 2026 based on OpenAI documentation: the Code Interpreter tool now requires a container parameter (added to all examples), text input parts use the input_text type, and web search uses the web_search tool (web_search_preview is now a legacy tool without domain filters). Generated files are retrieved using the fichiers_generes and telecharger functions from Lesson 10. Files attached to a message are uploaded with the user_data purpose, which OpenAI recommends for model inputs (File inputs). Sources: Code Interpreter · Web search.

Combine tools in a single call

So far, we have looked at each tool in isolation. In production, interesting questions rarely take that form. Asking “how do our sales compare to the market?” requires fetching an internal figure, an external figure, and then comparing them. Three tools, three sources, one call.

What changes is who decides the order. You do not program the sequence: you declare the available tools and describe the expected result, and the model establishes its own plan. This is convenient, but it is also the point to watch—an implicit plan is a plan you cannot review. The last part of this lesson shows how to regain control when order truly matters.

Principle of combination

All tools are declared in the same tools array, and that is the entire mechanism: there is no composition API, only a list of available capabilities. The model reads your request, infers the necessary steps, and calls what it needs, in the order it deems useful. A tool that is declared but unnecessary for the task simply will not be called—it costs nothing beyond the few tokens in its description.

from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-5.6-terra",
    input="Compare our sales forecasts with market trends, "
          "then create a comparison chart.",
    tools=[
        {"type": "file_search", "vector_store_ids": ["vs_previsions"]},
        {"type": "web_search"},
        {"type": "code_interpreter", "container": {"type": "auto"}}
    ]
)

The model will:

  1. Fetch internal forecasts via File Search
  2. Fetch market trends via Web Search
  3. Generate a comparison chart via Code Interpreter

Complete example: analytical assistant

with open("donnees_ventes.csv", "rb") as f:
    fichier = client.files.create(file=f, purpose="user_data")

tools = [
    # Search internal documents
    {
        "type": "file_search",
        "vector_store_ids": ["vs_rapports", "vs_objectifs"],
        "max_num_results": 10
    },
    # Web search for market context
    {
        "type": "web_search",
        "search_context_size": "medium"
    },
    # Code execution for analysis
    {"type": "code_interpreter", "container": {"type": "auto"}},
    # Function to save the report
    {
        "type": "function",
        "name": "sauvegarder_rapport",
        "description": "Save an analysis report to the document system",
        "parameters": {
            "type": "object",
            "properties": {
                "titre": {"type": "string"},
                "contenu": {"type": "string"},
                "categorie": {
                    "type": "string",
                    "enum": ["analyse", "prevision", "audit", "comparaison"]
                },
                "tags": {
                    "type": "array",
                    "items": {"type": "string"}
                }
            },
            "required": ["titre", "contenu", "categorie", "tags"],
            "additionalProperties": False
        },
        "strict": True
    }
]

response = client.responses.create(
    model="gpt-5.6-terra",
    input=[{
        "role": "user",
        "content": [
            {"type": "input_text", "text": (
                "Based on this sales file: "
                "1. Analyze trends over the last 12 months "
                "2. Compare with our annual goals (search the docs) "
                "3. Search the web for market trends "
                "4. Generate a comparison chart "
                "5. Write a summary report "
                "6. Save the report"
            )},
            {"type": "input_file", "file_id": fichier.id}
        ]
    }],
    tools=tools,
    instructions=(
        "You are a senior business analyst. "
        "Use all tools at your disposal to produce "
        "a complete, sourced analysis."
    )
)

Manage the multi-tool loop

There is an asymmetry here that you must keep in mind, because it determines your code structure. Built-in tools—Web Search, File Search, Code Interpreter—run at OpenAI: the model calls them, gets the result, and continues, all in a single round trip. Your custom functions, however, run on your side: the model can only request their execution, then it yields control and waits.

Hence the loop below. As long as the response contains function calls, you execute them, return the results, and restart. Set a maximum number of iterations: a model that indefinitely recalls the same tool because the result does not satisfy it is a rare but real case, and without a guardrail it will consume your budget until exhaustion.

import json

def boucle_multi_outils(prompt, tools, fichiers=None, max_tours=15):
    """Function calling loop with multi-tool support."""
    content = [{"type": "input_text", "text": prompt}]
    if fichiers:
        for fid in fichiers:
            content.append({"type": "input_file", "file_id": fid})

    messages = [{"role": "user", "content": content}]

    for tour in range(max_tours):
        response = client.responses.create(
            model="gpt-5.6-terra",
            input=messages,
            tools=tools,
            instructions="Use the necessary tools to answer completely."
        )

        # Built-in tools (web_search, file_search, code_interpreter)
        # are executed automatically on the server side.
        # We only handle custom function_call items.
        appels_custom = [
            item for item in response.output
            if item.type == "function_call"
        ]

        if not appels_custom:
            # No custom call: the model is finished
            return {
                "reponse": response.output_text,
                "tours": tour + 1,
                "tokens": response.usage.total_tokens
            }

        # Execute custom functions
        resultats = []
        for appel in appels_custom:
            resultat = dispatcher_fonctions(appel.name, appel.arguments)
            resultats.append({
                "type": "function_call_output",
                "call_id": appel.call_id,
                "output": json.dumps(resultat)
            })

        # Prepare the next turn
        messages = response.output + resultats

    return {"reponse": "Turn limit reached", "tours": max_tours}

Orchestration strategies

Guide the tool order

Automatic orchestration works fine as long as the order does not matter. It stops working when one step must inform the next: searching your documents before the web, for example, because internal vocabulary conditions the relevance of the external query. The system prompt is the lever—not as a technical constraint, but as a methodological instruction that the model follows all the better when it is justified.

# Strategy 1: search first, act later
instructions_recherche_dabord = (
    "Before any action, first search for the necessary information "
    "in internal documents and on the web. "
    "Do not propose actions until you have gathered all data."
)

# Strategy 2: data analysis then enrichment
instructions_analyse_dabord = (
    "Start by analyzing the data with Code Interpreter. "
    "Then enrich your conclusions with web searches. "
    "Finally, compare with internal documents."
)

# Strategy 3: cross-verification
instructions_verification = (
    "For each important piece of information, verify it in at least two sources: "
    "internal documents AND web search. Report any contradictions."
)

Limit tools by step

The opposite approach is to remove the choice. Rather than a single call with the full toolkit, you split the workflow into several calls, each declaring only the tool for its specific step. You lose automatic orchestration but gain three things that matter in production: guaranteed order, predictable cost, and isolatable errors. For a critical flow, this is almost always the right trade-off.

# Step 1: data collection (no code)
response_collecte = client.responses.create(
    model="gpt-5.6-terra",
    input="Gather information on Project Alpha",
    tools=[
        {"type": "file_search", "vector_store_ids": ["vs_projets"]},
        {"type": "web_search"}
    ]
)

# Step 2: analysis with collected data
response_analyse = client.responses.create(
    model="gpt-5.6-terra",
    input=f"Analyze this information and generate charts:\n"
          f"{response_collecte.output_text}",
    tools=[{"type": "code_interpreter", "container": {"type": "auto"}}]
)

Pattern: multi-skill agent

This last example combines both approaches: a router selects the tool configuration based on the nature of the request, then lets the model orchestrate within that restricted scope. It is the compromise that holds up in usage—enough freedom to absorb the variety of requests, enough structure to keep costs and behavior predictable.

class AgentMultiCompetences:
    def __init__(self, vector_stores: list[str], fonctions: list[dict]):
        self.client = OpenAI()
        self.tools = [
            {"type": "file_search", "vector_store_ids": vector_stores},
            {"type": "web_search", "search_context_size": "medium"},
            {"type": "code_interpreter", "container": {"type": "auto"}},
        ] + fonctions

    def traiter(self, requete: str, fichiers: list[str] = None) -> dict:
        return boucle_multi_outils(requete, self.tools, fichiers)

# Instantiate the agent
agent = AgentMultiCompetences(
    vector_stores=["vs_docs", "vs_contrats"],
    fonctions=[
        {
            "type": "function",
            "name": "envoyer_email",
            "description": "Send a notification email",
            "parameters": {
                "type": "object",
                "properties": {
                    "destinataire": {"type": "string"},
                    "sujet": {"type": "string"},
                    "corps": {"type": "string"}
                },
                "required": ["destinataire", "sujet", "corps"],
                "additionalProperties": False
            },
            "strict": True
        },
        {
            "type": "function",
            "name": "creer_tache",
            "description": "Create a task in the project manager",
            "parameters": {
                "type": "object",
                "properties": {
                    "titre": {"type": "string"},
                    "assignee": {"type": "string"},
                    "priorite": {"type": "string", "enum": ["basse", "moyenne", "haute"]},
                    "echeance": {"type": "string"}
                },
                "required": ["titre", "assignee", "priorite"],
                "additionalProperties": False
            },
            "strict": True
        }
    ]
)

# The agent can now:
# - Search documents
# - Search the web
# - Analyze data
# - Send emails
# - Create tasks
resultat = agent.traiter(
    "Analyze Q1 sales, compare with goals, "
    "and create a task for the sales director if we are behind."
)

Key takeaways

  • All tools coexist in the same tools array
  • Built-in tools run server-side, custom functions run client-side
  • The model automatically orchestrates the order of calls
  • Guide the orchestration strategy via the system prompt
  • Implement a loop to handle custom function calls