How LLMs Keep Built-in and Function Tools From Colliding

In the previous post, we established that built-in tools like code_interpreter and web_search outperform custom function tools because they’re in-distribution – the model was trained on their exact invocation patterns during post-training. Custom function tools, by contrast, are out-of-distribution: the model encounters them for the first time at inference and must rely on in-context learning to figure out what they do.

This raises a practical question that rarely gets asked: what happens if you name a custom function tool code_interpreter?

{
  "type": "function",
  "name": "code_interpreter",
  "description": "Executes Python code in a sandbox",
  "parameters": {
    "type": "object",
    "properties": {
      "code": { "type": "string" }
    }
  }
}

Nothing breaks. There’s no collision with the built-in code_interpreter. And the reason why reveals a clean piece of architecture that runs from the token level all the way up to the API surface.

Namespaces at the Token Level

Recall from the special tokens post that OpenAI’s Harmony format encodes tool calls as structured token sequences. A function tool call looks like this:

<|start|>assistant<|channel|>commentary to=functions.get_weather
<|constrain|>json<|message|>{"city":"Tokyo"}<|call|>

The addressing is to=functions.get_weather. That functions. prefix is a namespace. Every user-defined function tool lives inside it. When the tool responds, the same namespace appears in the return path:

<|start|>functions.get_weather to=assistant<|channel|>commentary
<|message|>{"temp":22}<|end|>

Built-in tools don’t live in the functions namespace. They exist at the root level – the model addresses them directly, without a prefix. So if you define a custom function tool named code_interpreter, the token stream addresses it as functions.code_interpreter. The built-in code interpreter is just code_interpreter. Different namespaces, no ambiguity.

This also reinforces why built-in tools are in-distribution in a way that no function tool can replicate, regardless of naming. Even if you give your function the same name and an identical description, the model still sees functions.code_interpreter in the token stream – a sequence it was never specifically trained on. The real code_interpreter, addressed at the root level, triggers the exact token patterns the model was fine-tuned to produce.

Namespaces at the API Level

The same separation is mirrored in the API’s type system. OpenAI’s Responses API uses a discriminated union – the type field on each tool determines how it’s defined, how it’s invoked, and what output it produces:

ToolDefinitionOutput Item Type
User-defined function{ "type": "function", "name": "code_interpreter" }function_call
Built-in code interpreter{ "type": "code_interpreter" }code_interpreter_call
Built-in web search{ "type": "web_search" }web_search_call
Built-in file search{ "type": "file_search" }file_search_call

A function tool named code_interpreter produces a function_call output with "name": "code_interpreter". The built-in code interpreter produces a code_interpreter_call output. The API routes on type first and only uses name for disambiguation within the function type.

This explains a design choice that might otherwise seem odd: why does the Responses API wrap all user-defined tools under type: "function" instead of promoting each one to its own type like type: "get_weather"? Because the type field is the namespace boundary. Built-in tools each get their own type because the model and the API both handle them differently – they execute server-side, use in-distribution invocation patterns, and return specialized output types. Function tools share the single function type because they all follow the same execution model: the model generates arguments, the client executes, the result comes back.

Two Boundaries, One Design

The token-level namespace (functions. prefix in Harmony) and the API-level namespace (type discriminator in the Responses API) are two expressions of the same architectural principle: built-in tools and function tools are fundamentally different things that need to coexist without interference.

At the token level, the namespace ensures the model activates different learned pathways for code_interpreter (root, in-distribution) versus functions.code_interpreter (namespaced, out-of-distribution). At the API level, the type discriminator ensures the orchestration layer routes built-in tool calls to server-side execution and function tool calls back to the client – even when the names are identical.

It’s a small detail, but it’s load-bearing. Without namespace separation, every built-in tool name would be a reserved word that developers couldn’t use for their own functions. With it, the two worlds are cleanly isolated, and you can name your function tools anything you want without worrying about stepping on the provider’s built-in capabilities.

There’s more to say about how this plays out across generations of OpenAI’s API surface – from the raw token access of the legacy Completions API, through the function-only world of Chat Completions, to the server-side execution model of the Responses API. That’s the next post in this series.

Comments

Came here from LinkedIn or X? Join the conversation below — all discussion lives here.