From deb6eec68b9bf76ce7c0763d75dc7ae23720da03 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:37:33 +1000 Subject: [PATCH] fix: forward attached filenames to Anthropic chat models (#26051) Previously, fantasy's Anthropic provider adapter accepted PDF and text FileParts but dropped the filename on the floor, so Claude (direct or via Bedrock) saw the document bytes without any handle and could not answer questions like "what's in foo.pdf". Other providers (OpenAI, Gemini, OpenRouter, Vercel) already forwarded filenames. Bumps `coder/fantasy` past [coder/fantasy#38](https://github.com/coder/fantasy/pull/38), which sanitizes `FilePart.Filename` and sets it as the Anthropic `DocumentBlockParam.Title` for both `application/pdf` and `text/*` attachments, and emits a `CallWarning` for unsupported `FilePart` media types instead of silently dropping them. On this side, plumbs the resolved filename through `partsToMessageParts` so the `FilePart` literal carries it into the provider. The existing `TestModelFromConfig_AnthropicPDFFilePartReachesProvider` regression test is extended to assert the outbound Anthropic request includes the sanitized title (`quarterly_report.v1.pdf` becomes `quarterly report v1 pdf`). Closes CODAGT-545 --- coderd/x/chatd/chatprompt/chatprompt.go | 1 + coderd/x/chatd/chatprompt/chatprompt_test.go | 1 + .../x/chatd/chatprovider/chatprovider_test.go | 21 ++++++++++++++----- go.mod | 6 +++++- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/coderd/x/chatd/chatprompt/chatprompt.go b/coderd/x/chatd/chatprompt/chatprompt.go index 126edf8dba..ac86b11e96 100644 --- a/coderd/x/chatd/chatprompt/chatprompt.go +++ b/coderd/x/chatd/chatprompt/chatprompt.go @@ -1588,6 +1588,7 @@ func partsToMessageParts( continue } result = append(result, fantasy.FilePart{ + Filename: name, Data: data, MediaType: mediaType, ProviderOptions: opts, diff --git a/coderd/x/chatd/chatprompt/chatprompt_test.go b/coderd/x/chatd/chatprompt/chatprompt_test.go index 8f66da7cec..895fe02aea 100644 --- a/coderd/x/chatd/chatprompt/chatprompt_test.go +++ b/coderd/x/chatd/chatprompt/chatprompt_test.go @@ -502,6 +502,7 @@ func TestConvertMessagesWithFiles_MixedResolvedAndMissingFilePartsInSingleMessag filePart, ok := fantasy.AsMessagePart[fantasy.FilePart](prompt[0].Content[0]) require.True(t, ok, "expected first part to stay a FilePart") + require.Equal(t, "resolved.png", filePart.Filename) require.Equal(t, resolvedData, filePart.Data) require.Equal(t, "image/png", filePart.MediaType) diff --git a/coderd/x/chatd/chatprovider/chatprovider_test.go b/coderd/x/chatd/chatprovider/chatprovider_test.go index 80911d89cd..261c6e9528 100644 --- a/coderd/x/chatd/chatprovider/chatprovider_test.go +++ b/coderd/x/chatd/chatprovider/chatprovider_test.go @@ -1401,10 +1401,11 @@ func TestModelFromConfig_ExtraHeaders(t *testing.T) { // path that lets a user-uploaded PDF actually reach Claude/Bedrock: a // fantasy.FilePart with MediaType "application/pdf" must be serialized as an // Anthropic "document" content block with a base64 source carrying the PDF -// bytes. Older fantasy versions silently dropped PDF FileParts in the -// Anthropic provider, so the user message ended up empty and the model never -// saw the document. See coder/fantasy#37 (cherry-pick of upstream -// charmbracelet/fantasy#197). The Generate call would fail outright on the +// bytes and a sanitized filename as the document title. Older fantasy versions +// silently dropped PDF FileParts in the Anthropic provider, so the user +// message ended up empty and the model never saw the document. The underlying +// PDF block support came from coder/fantasy#37, a cherry-pick of upstream +// charmbracelet/fantasy#197. The Generate call would fail outright on the // regressed code path because the dropped FilePart leaves the request with // zero messages. func TestModelFromConfig_AnthropicPDFFilePartReachesProvider(t *testing.T) { @@ -1423,6 +1424,7 @@ func TestModelFromConfig_AnthropicPDFFilePartReachesProvider(t *testing.T) { var blocks []struct { Type string `json:"type"` + Title string `json:"title"` Source struct { Type string `json:"type"` MediaType string `json:"media_type"` @@ -1439,6 +1441,11 @@ func TestModelFromConfig_AnthropicPDFFilePartReachesProvider(t *testing.T) { } assert.Equal(t, "base64", block.Source.Type, "PDF document block must use a base64 source") assert.Equal(t, wantData, block.Source.Data, "PDF bytes must round-trip base64 unchanged") + assert.Equal(t, + "quarterly report v1 pdf", + block.Title, + "PDF filename must reach Anthropic as a sanitized document title", + ) if block.Source.MediaType != "" { assert.Equal(t, "application/pdf", block.Source.MediaType) } @@ -1462,7 +1469,11 @@ func TestModelFromConfig_AnthropicPDFFilePartReachesProvider(t *testing.T) { { Role: fantasy.MessageRoleUser, Content: []fantasy.MessagePart{ - fantasy.FilePart{Data: pdfData, MediaType: "application/pdf"}, + fantasy.FilePart{ + Filename: "quarterly_report.v1.pdf", + Data: pdfData, + MediaType: "application/pdf", + }, }, }, }, diff --git a/go.mod b/go.mod index 95f96d276c..82bfbab673 100644 --- a/go.mod +++ b/go.mod @@ -94,7 +94,11 @@ replace github.com/spf13/afero => github.com/aslilac/afero v0.0.0-20250403163713 // emit a Base64 PDF document block for application/pdf FileParts on the // Anthropic provider so user-uploaded PDFs actually reach Claude/Bedrock // instead of being silently dropped. -// 11) coder/fantasy#39, support Anthropic thinking_display natively. +// 11) coder/fantasy#38, forward PDF and text filenames as a sanitized +// Anthropic document title so Claude can refer to attachments by +// name, and warn on unsupported FilePart media types instead of +// silently dropping them. +// 12) coder/fantasy#39, support Anthropic thinking_display natively. // See: https://github.com/coder/fantasy/commits/a2a3f2171ec8 replace charm.land/fantasy => github.com/coder/fantasy v0.0.0-20260604204802-a2a3f2171ec8