Skip to content

feat: send tool result media to providers like user files - #31383

Draft
ethanndickson wants to merge 9 commits into
mainfrom
codagt-1430-tool-result-filename
Draft

ethanndickson wants to merge 9 commits into
mainfrom
codagt-1430-tool-result-filename

Conversation

@ethanndickson

@ethanndickson ethanndickson commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Part of CODAGT-1430. This is the first of four stacked PRs.

Today a PDF in an MCP tool result reaches no provider. On Anthropic we strip it in chatd. On OpenAI Chat Completions fantasy swaps it for a "cannot be displayed" note, and on Responses it sends a placeholder that points at a user message it never sends.

coder/fantasy#73 and coder/fantasy#74 make fantasy convert tool result media with the converter it already uses for user files: an Anthropic document inside tool_result, an input_file inside function_call_output on Responses, and a user file part straight after the tool message on Chat Completions. This PR points charm.land/fantasy at coder/fantasy#74. Chat Completions file parts now refuse text files, as Responses already did, so AcceptsFilePartMediaType follows.

The converters take a filename, so an MCP embedded resource blob is now named after the last element of its URI path, and we store the name with the tool result so later turns send the same one. OpenAI picks the file type from the extension, so we append the right one when it's missing or wrong. We drop names with control characters, as Postgres jsonb rejects NUL, and we redact the name like the rest of the tool response, since a URI like https://host/sse/<token> would otherwise put the token in the chat history.

There's also a test proving aibridge's Chat Completions re-encode keeps a user file part, as that's where these PDFs end up.

The go.mod replace points at the coder/fantasy#74 head for now. Once both fantasy PRs merge I'll repin to the merged commit before this leaves draft. Anthropic still strips tool result PDFs until the next PR in the stack.

coder/fantasy#73 and coder/fantasy#74 convert tool result media through
each provider's file part converter and add a filename. Repin to
coder_2_33 once they merge.

coder/fantasy#74 also stops sending text files as Chat Completions file
parts, so Chat Completions routes no longer accept them and chatprompt
inlines user text files as text, as it does on Responses.
… storage

An MCP tool that returns an embedded resource blob now names the media
after the last element of the resource URI path. chatloop copies the
name into the media tool result, and chatprompt stores it in the
persisted media result and restores it for the next prompt, so
providers can label the file the same way they label a user attachment.
…transport as files

Send a PDF tool result with a filename through the anthropic, OpenAI
Chat Completions, OpenAI Responses and openai-compat clients and check
the request body: an Anthropic document inside tool_result, a user file
part right after the tool message on Chat Completions, and an
input_file inside function_call_output on Responses, each with the
filename.
… user file part

chatd sends PDFs on Chat Completions as user file parts, both for
attachments and for tool results. aibridge decodes the body into
openai-go types and encodes it again, so check that file_data and
filename survive.
…the path import

resourceFilename imports the path package, and revive rejects the
newServerRedactor loop variable named path.
…haracters

A percent-encoded NUL in a resource URI decodes into the filename, and
Postgres jsonb rejects \u0000, so the tool result could not be saved.
Such a name is never a useful label, so send the media without one.
…atch the media type

OpenAI detects the file type of an input file from its filename extension. A PDF from a URI such as https://api/x/files/12345 had no extension, and report.bin had a wrong one. The stored name is replayed on every turn, so a rejected name would break every later turn.

Keep the name when its extension maps to the media type. Otherwise append the canonical extension, which is always .pdf for application/pdf. Return no name when the media type has no known extension, so the provider default applies.
The filename comes from the resource URI of an untrusted inline MCP server. A URI such as https://host/sse/<token> made the token the filename, which reached the model and the stored chat history. Redact the filename like the other tool response fields.
@linear-code

linear-code Bot commented Oct 28, 2026

Copy link
Copy Markdown

CODAGT-1430

lint/go/new-code runs errcheck with check-blank, which rejects the
discarded mime.ParseMediaType and mime.ExtensionsByType errors.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant