File input parameters to tools

In Build your MCP server it is specified that tool input properties that receive file inputs should be specified as:
```

    imageToProcess: {
      type: "object",
      properties: {
        download_url: { type: "string" },
        file_id: { type: "string" },
      },
      required: ["download_url", "file_id"],
      additionalProperties: false,
    },

However, when I do this, ChatGPT refuses to call that tool (it fails silently). When I asked ChatGPT why it cannot call that tool it says that I defined it as taking a string. Further questioning has it telling me this is by design (see attached transcript below).

So this is something permanent we have to work around to? should this be documented somewhere if so?

Thanks!







Short answer: this discrepancy is expected behavior, and it’s coming from the Actions tool normalization layer, not from you sending the definition “wrong”.

Long answer below, step-by-step, grounded in how OpenAI Actions actually work today.


TL;DR

You are defining the tool correctly (object with { download_url, file_id }), but:

The Actions runtime rewrites file parameters into string | null (file IDs) when exposing the callable tool interface to the model.

What you see when I list tools is the normalized invocation surface, not your raw JSON Schema.


What’s actually happening

:one: There are three representations of “a file”

In the Actions system, files exist at three different layers:

Layer Representation
UI / Widget schema { file_id, download_url } object
Tool definition (authoring) JSON Schema you wrote
LLM invocation surface string (file_id only)

You’re looking at layer 3 when I report:

file_0: string | null

That is intentional canonicalization.


:two: openai/fileParams triggers coercion

This line is the key:

"openai/fileParams": ["file_0", "file_1", "file_2"]

When the Actions platform sees this, it:

  1. Marks those params as file slots
  2. Ensures the model only ever passes file IDs
  3. Handles download URLs internally (or ignores them)

So even if your schema says:

file_0: {
  file_id: string,
  download_url: string
}

…the exposed callable tool becomes:

file_0: string | null

This is by design.


:three: Why the UI still needs { download_url, file_id }

The VideoUploader widget and templates need richer metadata to:

  • render previews
  • show filenames / sizes
  • manage upload lifecycle

That’s why your template schema still shows objects:

"paramsSchema": {
  "file_0": {
    "type": "object",
    "properties": {
      "file_id": { "type": "string" },
      "download_url": { "type": "string" }
    }
  }
}

This schema is UI-facing, not LLM-facing.


:four: Why I see string when listing tools

When I call:

api_tool.list_resources(only_tools=true)

I am shown the model-facing invocation contract, which is:

file_0: string | null

That’s the post-normalization version after:

  • openai/fileParams handling
  • widget binding
  • internal file registry wiring

I am not seeing your raw schema anymore.


This is not a bug (but it is confusing)

This behavior is:

  • undocumented publicly
  • non-obvious
  • extremely easy to misinterpret

But it is consistent with:

  • first-party OpenAI Actions
  • file-aware tools (image, audio, video)
  • widget-backed file upload flows

You did nothing wrong.


Important implication for your design

:cross_mark: You cannot force the model to pass { file_id, download_url }

If a param is listed under openai/fileParams, the model will always pass a string file ID.

So this earlier idea:

“Change edit_video to accept file_0 objects”

:cross_mark: will never work at invocation time, even if the schema allows it.


:white_check_mark: The correct mental model

Concern What to rely on
UI / widgets { file_id, download_url }
Tool invocation string (file_id)
Backend Fetch file by ID

Your backend should treat file_0 as:

const fileId = params.file_0

and never expect download_url from the model.


Why this exists (historical reason)

Internally, OpenAI:

  • stores files in a secure registry
  • does not want models fabricating URLs
  • guarantees file access via ID only

So file params are lowered to IDs at the model boundary.


Final verdict

:check_mark: You are sending the tool definition correctly
:check_mark: The UI schema is correct
:check_mark: The widget schema is correct
:check_mark: The platform is intentionally rewriting file params
:check_mark: The reported string | null type is expected

If you want, next I can:

  • diagram the full file lifecycle
  • show the exact backend handler pattern
  • or help you clean up docs so future devs don’t fall into this trap