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=