feat(anthropic): add document support for pdf and text file content blocks (#197)

Nic-vdwalt created

Change summary

providers/anthropic/anthropic.go      | 15 ++++++++-
providers/anthropic/anthropic_test.go | 46 ++++++++++++++++++++++++++++
2 files changed, 58 insertions(+), 3 deletions(-)

Detailed changes

providers/anthropic/anthropic.go 🔗

@@ -831,7 +831,6 @@ func toPrompt(prompt fantasy.Prompt, sendReasoningData bool) ([]anthropic.TextBl
 							if !ok {
 								continue
 							}
-							// TODO: handle other file types
 							switch {
 							case strings.HasPrefix(file.MediaType, "image/"):
 								base64Encoded := base64.StdEncoding.EncodeToString(file.Data)
@@ -840,10 +839,22 @@ func toPrompt(prompt fantasy.Prompt, sendReasoningData bool) ([]anthropic.TextBl
 									imageBlock.OfImage.CacheControl = anthropic.NewCacheControlEphemeralParam()
 								}
 								anthropicContent = append(anthropicContent, imageBlock)
+							case file.MediaType == "application/pdf":
+								base64Encoded := base64.StdEncoding.EncodeToString(file.Data)
+								docBlock := anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{
+									Data: base64Encoded,
+								})
+								if cacheControl != nil {
+									docBlock.OfDocument.CacheControl = anthropic.NewCacheControlEphemeralParam()
+								}
+								anthropicContent = append(anthropicContent, docBlock)
 							case strings.HasPrefix(file.MediaType, "text/"):
 								documentBlock := anthropic.NewDocumentBlock(anthropic.PlainTextSourceParam{
 									Data: string(file.Data),
 								})
+								if cacheControl != nil {
+									documentBlock.OfDocument.CacheControl = anthropic.NewCacheControlEphemeralParam()
+								}
 								anthropicContent = append(anthropicContent, documentBlock)
 							}
 						}
@@ -1050,7 +1061,7 @@ func toPrompt(prompt fantasy.Prompt, sendReasoningData bool) ([]anthropic.TextBl
 
 func hasVisibleUserContent(content []anthropic.ContentBlockParamUnion) bool {
 	for _, block := range content {
-		if block.OfText != nil || block.OfImage != nil || block.OfToolResult != nil {
+		if block.OfText != nil || block.OfImage != nil || block.OfDocument != nil || block.OfToolResult != nil {
 			return true
 		}
 	}

providers/anthropic/anthropic_test.go 🔗

@@ -262,6 +262,50 @@ func TestToPrompt_DropsEmptyMessages(t *testing.T) {
 		require.Empty(t, warnings)
 	})
 
+	t.Run("should keep user messages with PDF content", func(t *testing.T) {
+		t.Parallel()
+
+		prompt := fantasy.Prompt{
+			{
+				Role: fantasy.MessageRoleUser,
+				Content: []fantasy.MessagePart{
+					fantasy.FilePart{
+						Data:      []byte("fake pdf data"),
+						MediaType: "application/pdf",
+					},
+				},
+			},
+		}
+
+		systemBlocks, messages, warnings := toPrompt(prompt, true)
+
+		require.Empty(t, systemBlocks)
+		require.Len(t, messages, 1)
+		require.Empty(t, warnings)
+	})
+
+	t.Run("should keep user messages with text document content", func(t *testing.T) {
+		t.Parallel()
+
+		prompt := fantasy.Prompt{
+			{
+				Role: fantasy.MessageRoleUser,
+				Content: []fantasy.MessagePart{
+					fantasy.FilePart{
+						Data:      []byte("# Hello World\nSome markdown content"),
+						MediaType: "text/markdown",
+					},
+				},
+			},
+		}
+
+		systemBlocks, messages, warnings := toPrompt(prompt, true)
+
+		require.Empty(t, systemBlocks)
+		require.Len(t, messages, 1)
+		require.Empty(t, warnings)
+	})
+
 	t.Run("should drop user messages without visible content", func(t *testing.T) {
 		t.Parallel()
 
@@ -271,7 +315,7 @@ func TestToPrompt_DropsEmptyMessages(t *testing.T) {
 				Content: []fantasy.MessagePart{
 					fantasy.FilePart{
 						Data:      []byte("not supported"),
-						MediaType: "application/pdf",
+						MediaType: "application/zip",
 					},
 				},
 			},