From 86287e5dcf04ea1a25d5c79ff0dc50016f9a60d4 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 4 Jun 2026 07:10:37 +0000 Subject: [PATCH] fix: forward attached filenames to Anthropic chat models 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, 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 TestModelFromConfig_AnthropicPDFFilePartReachesProvider regression test now asserts 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 | 8 +++++-- go.sum | 4 ++-- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/coderd/x/chatd/chatprompt/chatprompt.go b/coderd/x/chatd/chatprompt/chatprompt.go index 126edf8dba8ab..ac86b11e96b17 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 8f66da7cecfc8..895fe02aeaa87 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 1d9b43c4ff4f1..891a01f880c3a 100644 --- a/coderd/x/chatd/chatprovider/chatprovider_test.go +++ b/coderd/x/chatd/chatprovider/chatprovider_test.go @@ -1308,10 +1308,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) { @@ -1330,6 +1331,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"` @@ -1346,6 +1348,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) } @@ -1369,7 +1376,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 241ab475c02d8..bfd4d2b0f748c 100644 --- a/go.mod +++ b/go.mod @@ -94,8 +94,12 @@ 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. -// See: https://github.com/coder/fantasy/commits/7d46e640327a -replace charm.land/fantasy => github.com/coder/fantasy v0.0.0-20260602023814-7d46e640327a +// 11) coder/fantasy#38 @b2b2fc6d524c, 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. +// See: https://github.com/coder/fantasy/commits/b2b2fc6d524c +replace charm.land/fantasy => github.com/coder/fantasy v0.0.0-20260604065934-b2b2fc6d524c // coder/coder uses a fork of charmbracelet's fork of the Anthropic Go SDK // with performance improvements and Bedrock header cleanup. diff --git a/go.sum b/go.sum index 61bfd608f8bed..58ffd01501505 100644 --- a/go.sum +++ b/go.sum @@ -324,8 +324,8 @@ github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41 h1:SBN/DA63+ZHwu github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41/go.mod h1:I9ULxr64UaOSUv7hcb3nX4kowodJCVS7vt7VVJk/kW4= github.com/coder/clistat v1.2.1 h1:P9/10njXMyj5cWzIU5wkRsSy5LVQH49+tcGMsAgWX0w= github.com/coder/clistat v1.2.1/go.mod h1:m7SC0uj88eEERgvF8Kn6+w6XF21BeSr+15f7GoLAw0A= -github.com/coder/fantasy v0.0.0-20260602023814-7d46e640327a h1:ffQixHAwjJLHgFfe4rtrAsFNRGhEyWnBSpInnLIxDPo= -github.com/coder/fantasy v0.0.0-20260602023814-7d46e640327a/go.mod h1:wZ0e3lEPqrM0XiIdAUQLvMKCLYhc3gi96MRX2wjbX44= +github.com/coder/fantasy v0.0.0-20260604065934-b2b2fc6d524c h1:POkVNoDnbNv3CfeFInwai0yMkjGkdqjgqIcG/SlnuLg= +github.com/coder/fantasy v0.0.0-20260604065934-b2b2fc6d524c/go.mod h1:wZ0e3lEPqrM0XiIdAUQLvMKCLYhc3gi96MRX2wjbX44= github.com/coder/flog v1.1.0 h1:kbAes1ai8fIS5OeV+QAnKBQE22ty1jRF/mcAwHpLBa4= github.com/coder/flog v1.1.0/go.mod h1:UQlQvrkJBvnRGo69Le8E24Tcl5SJleAAR7gYEHzAmdQ= github.com/coder/go-httpstat v0.0.0-20230801153223-321c88088322 h1:m0lPZjlQ7vdVpRBPKfYIFlmgevoTkBxB10wv6l2gOaU=