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
This commit is contained in:
Ethan
2026-06-10 11:37:33 +10:00
committed by GitHub
parent 360611ea15
commit deb6eec68b
4 changed files with 23 additions and 6 deletions
+1
View File
@@ -1588,6 +1588,7 @@ func partsToMessageParts(
continue
}
result = append(result, fantasy.FilePart{
Filename: name,
Data: data,
MediaType: mediaType,
ProviderOptions: opts,
@@ -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)
@@ -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",
},
},
},
},
+5 -1
View File
@@ -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