Skip to main content
Lesson 9 of 16 · Fine-Tuning OpenAI Models

Fine-tuning for function calling

Lesson 9 of 16 Updated November 2, 2026 Advanced
On this page
  1. Why fine-tune for function calling
  2. Data format for function calling
  3. Common use cases
  4. Data preparation script
  5. Launching the fine-tuning job
  6. Best practices
  7. Key takeaways

Update November 2, 2026 — Fine-tuning availability. OpenAI’s self-service fine-tuning is being phased out: since May 7, 2026, organizations that have never used it can no longer create jobs; since July 2, you must have used a fine-tuned model within the last 60 days; and starting January 6, 2027, no customer will be able to launch new jobs. Already trained models remain usable until their base model is retired. This course remains useful for understanding and leveraging an existing model; for new projects, prioritize prompt engineering, RAG, and evaluations.

Why fine-tune for function calling

Function calling lets the model decide when and how to invoke external functions. In this context, fine-tuning isn’t about tweaking writing style: it hardens a decision-making mechanism. A model trained on your own calls first learns to pick the right function from several available options. If your app exposes both slot lookup and appointment creation, the phrase “I’d like to see what’s free on Thursday” must never trigger a booking.

It then learns to extract the right parameters from the user’s request—that is, to convert natural phrasing into values that conform to the schema: “tomorrow at 2 PM” becomes a normalized date and time. The same mechanism teaches less flashy but equally critical reflexes. Knowing when NOT to call a function avoids the pitfall of a model triggering a tool on a simple hello. And chaining multiple function calls logically—listing availability, then booking the chosen slot—requires the model to have seen complete sequences during training, not isolated calls.

Data format for function calling

Training examples include the function definitions (tools) and the corresponding calls. The assistant’s message no longer carries text but a tool_calls array; the result returned by your system appears in a tool role message, linked to the call by tool_call_id; the assistant then speaks again to formulate the response intended for the user. It’s this entire loop, not just the call, that the model must learn to reproduce.

{
  "messages": [
    {
      "role": "system",
      "content": "You are an assistant that helps manage appointments."
    },
    {
      "role": "user",
      "content": "Book me a slot tomorrow at 2 PM with Dr. Martin."
    },
    {
      "role": "assistant",
      "tool_calls": [
        {
          "id": "call_001",
          "type": "function",
          "function": {
            "name": "create_appointment",
            "arguments": "{\"date\": \"2026-04-03\", \"time\": \"14:00\", \"practitioner\": \"Dr. Martin\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_001",
      "content": "{\"status\": \"confirmed\", \"id\": \"appt_42\"}"
    },
    {
      "role": "assistant",
      "content": "Your appointment with Dr. Martin is confirmed for tomorrow at 2 PM. Reference: APPT-42."
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "create_appointment",
        "description": "Creates a new medical appointment",
        "parameters": {
          "type": "object",
          "properties": {
            "date": {
              "type": "string",
              "description": "Date in YYYY-MM-DD format"
            },
            "time": {
              "type": "string",
              "description": "Time in HH:MM format"
            },
            "practitioner": {
              "type": "string",
              "description": "Name of the practitioner"
            }
          },
          "required": ["date", "time", "practitioner"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "list_slots",
        "description": "Lists available slots for a practitioner",
        "parameters": {
          "type": "object",
          "properties": {
            "practitioner": {"type": "string"},
            "date": {"type": "string"}
          },
          "required": ["practitioner"]
        }
      }
    }
  ]
}

Common use cases

Selecting the right function

Train the model to distinguish which function to call based on context. In the example below, the request concerns checking availability, not making a reservation: the example reinforces that an open-ended question calls list_slots, and nothing else.

{
  "messages": [
    {"role": "user", "content": "What slots are free on Thursday with Dr. Martin?"},
    {
      "role": "assistant",
      "tool_calls": [
        {
          "id": "call_002",
          "type": "function",
          "function": {
            "name": "list_slots",
            "arguments": "{\"practitioner\": \"Dr. Martin\", \"date\": \"2026-04-04\"}"
          }
        }
      ]
    }
  ],
  "tools": ["... same tools as above ..."]
}

NOT calling a function

Reserve part of your dataset for exchanges where the model must not use any function. A model trained exclusively on calls gets into the habit of producing one at every turn, even for a greeting or a general question—behavior that annoys users and costs your backend. The counter-example below takes just a few lines and eliminates an entire class of errors.

{
  "messages": [
    {"role": "user", "content": "Hello, how are you?"},
    {
      "role": "assistant",
      "content": "Hello! I'm doing well, thanks. How can I help you with your appointments today?"
    }
  ],
  "tools": ["... same tools ..."]
}

Extracting complex parameters

Real requests are rarely structured. “Next Monday morning” and “the cardiologist” must become an absolute date, a time period, and a specialty. This is precisely where the base model stumbles most often, so it’s where fine-tuning delivers the most visible gain: multiply this type of example rather than cases that are already clean.

{
  "messages": [
    {"role": "user", "content": "Cancel my appointment next Monday morning with the cardiologist"},
    {
      "role": "assistant",
      "tool_calls": [
        {
          "id": "call_003",
          "type": "function",
          "function": {
            "name": "cancel_appointment",
            "arguments": "{\"date\": \"2026-04-06\", \"period\": \"morning\", \"specialty\": \"cardiology\"}"
          }
        }
      ]
    }
  ],
  "tools": ["..."]
}

Data preparation script

Building the file remains mechanical: an assembly function, a tool definition shared by all examples, a list of cases, then line-by-line writing in JSONL format. Note the use of json.dumps for the arguments field, which must be a string, not an object.

import json

def create_function_calling_example(
    messages: list[dict],
    tools: list[dict]
) -> dict:
    """Creates a training example for function calling."""
    return {
        "messages": messages,
        "tools": tools
    }

# Tool definitions
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_product",
            "description": "Searches for a product in the catalog",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Search terms"},
                    "category": {"type": "string", "enum": ["electronics", "clothing", "home"]},
                    "max_price": {"type": "number", "description": "Maximum price in euros"}
                },
                "required": ["query"]
            }
        }
    }
]

# Training examples
examples = [
    create_function_calling_example(
        messages=[
            {"role": "user", "content": "I'm looking for headphones under 100 euros"},
            {
                "role": "assistant",
                "tool_calls": [{
                    "id": "call_1",
                    "type": "function",
                    "function": {
                        "name": "search_product",
                        "arguments": json.dumps({
                            "query": "headphones",
                            "category": "electronics",
                            "max_price": 100
                        })
                    }
                }]
            }
        ],
        tools=tools
    )
]

# Save
with open("function_calling_train.jsonl", "w", encoding="utf-8") as f:
    for ex in examples:
        f.write(json.dumps(ex, ensure_ascii=False) + "\n")

Launching the fine-tuning job

The job is created exactly as for conversational fine-tuning: the enriched format is recognized from the file content, with no additional parameters.

from openai import OpenAI

client = OpenAI()

file = client.files.create(
    file=open("function_calling_train.jsonl", "rb"),
    purpose="fine-tune"
)

job = client.fine_tuning.jobs.create(
    training_file=file.id,
    model="gpt-5.6-luna",
    suffix="function-calling-v1"
)

Best practices

The result depends first on the diversity of your dataset. Each function must appear multiple times, user phrasings must vary for the same intent, and you benefit from including ambiguous cases accompanied by the resolution you deem correct, without forgetting normal conversations that call no function.

Next comes parameter realism. The arguments must match exactly the schema defined in tools, with plausible values rather than placeholders like “xxx”—a model trained on fake values will produce them. Systematically test that every arguments field is valid JSON: a missing brace or quote is enough to make the example unusable.

Finally, there’s the question of proportions. For a dataset of 100 examples covering 3 functions, the distribution below gives a healthy balance between functional coverage and restraint.

Example typeQuantity
Per function25-30
No function call10-15
Chained calls (multi-step)5-10

Key takeaways

  • The format includes tools in addition to messages
  • Function calls use tool_calls in the assistant message
  • Include examples where the model must NOT call a function
  • Vary phrasings for the same intent
  • Verify that all arguments are valid JSON conforming to the schema