From 2b67a42361ad9c9bf1d3a44b775197b44a5d58d6 Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Thu, 23 Jul 2026 11:12:19 +0530 Subject: [PATCH 1/2] Forward Anthropic image-URL, PDF, and hosted-file inputs buildMessageParam only handled base64 image DataContent and dropped every other multimodal content type on the floor: URIContent (including image URLs), application/pdf DataContent, and HostedFileContent all fell through the switch and were silently discarded before reaching the API. Extend the switch to match the OpenAI chat provider's multimodal mapping: base64 PDF DataContent -> NewDocumentBlock(Base64PDFSourceParam), image URIContent -> NewImageBlock(URLImageSourceParam), and PDF URIContent -> NewDocumentBlock(URLPDFSourceParam). The stable Messages API in anthropic-sdk-go v1.58.1 has no file-id image/document source (only the Beta API does), so HostedFileContent is documented as not-yet-forwardable rather than silently ignored. --- provider/anthropicprovider/agent.go | 19 +++++- provider/anthropicprovider/agent_test.go | 76 ++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/provider/anthropicprovider/agent.go b/provider/anthropicprovider/agent.go index 82822f09..3bd767c1 100644 --- a/provider/anthropicprovider/agent.go +++ b/provider/anthropicprovider/agent.go @@ -481,13 +481,30 @@ func buildMessageParam(msg *message.Message) (anthropic.MessageParam, error) { } content = append(content, anthropic.NewToolResultBlock(c.CallID, resStr, c.Error != nil)) case *message.DataContent: - if c.TopLevelMediaType() == "image" { + switch { + case c.TopLevelMediaType() == "image": mediaType := c.MediaType if mediaType == "" { mediaType = "image/jpeg" } content = append(content, anthropic.NewImageBlockBase64(mediaType, c.Data)) + case c.MediaType == "application/pdf": + content = append(content, anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{ + Data: c.Data, + })) } + case *message.URIContent: + switch { + case c.TopLevelMediaType() == "image": + content = append(content, anthropic.NewImageBlock(anthropic.URLImageSourceParam{URL: c.URI})) + case c.MediaType == "application/pdf": + content = append(content, anthropic.NewDocumentBlock(anthropic.URLPDFSourceParam{URL: c.URI})) + } + case *message.HostedFileContent: + // The stable Anthropic Messages API used here (anthropic.MessageNewParams) + // has no file-id image/document source in anthropic-sdk-go v1.58.1; only + // the Beta API exposes BetaFileImageSourceParam/BetaFileDocumentSourceParam. + // A hosted file reference therefore cannot be forwarded yet. } } diff --git a/provider/anthropicprovider/agent_test.go b/provider/anthropicprovider/agent_test.go index 7ffecea2..64e35da7 100644 --- a/provider/anthropicprovider/agent_test.go +++ b/provider/anthropicprovider/agent_test.go @@ -575,3 +575,79 @@ func TestToolUseEmptyArgumentsSerializeAsObject(t *testing.T) { t.Fatal("tool_use block for toolu_1 not found in request") } } + +// A URIContent image URL and a DataContent application/pdf must be forwarded to +// Anthropic as an image block with a URL source and a document block. Before the +// fix these inputs fell through the content switch and were silently dropped, +// diverging from the OpenAI chat provider which maps all three multimodal inputs. +func TestBuildMessageParam_ImageURLAndPDFAreForwarded(t *testing.T) { + bodyCh := make(chan []byte, 1) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + t.Errorf("read request body: %v", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + bodyCh <- body + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, minimalMessageResponse("ok")) + })) + defer server.Close() + + a := newTestClient(t, server) + + msgs := []*message.Message{ + {Role: message.RoleUser, Contents: message.Contents{ + &message.URIContent{URI: "https://example.com/cat.png", MediaType: "image/png"}, + &message.DataContent{Data: "JVBERi0xLjQK", MediaType: "application/pdf"}, + }}, + } + if _, err := a.Run(t.Context(), msgs).Collect(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var req map[string]any + if err := json.Unmarshal(<-bodyCh, &req); err != nil { + t.Fatalf("unmarshal request body: %v", err) + } + messages, ok := req["messages"].([]any) + if !ok { + t.Fatalf("request messages = %#v, want a JSON array", req["messages"]) + } + + var imageURL, documentBase64 bool + for _, m := range messages { + msg, ok := m.(map[string]any) + if !ok { + continue + } + blocks, ok := msg["content"].([]any) + if !ok { + continue + } + for _, b := range blocks { + block, ok := b.(map[string]any) + if !ok { + continue + } + source, _ := block["source"].(map[string]any) + switch block["type"] { + case "image": + if source["type"] == "url" && source["url"] == "https://example.com/cat.png" { + imageURL = true + } + case "document": + if source["type"] == "base64" && source["media_type"] == "application/pdf" && source["data"] == "JVBERi0xLjQK" { + documentBase64 = true + } + } + } + } + if !imageURL { + t.Error("image block with a URL source not found in request") + } + if !documentBase64 { + t.Error("document block with a base64 application/pdf source not found in request") + } +} From 7f3d1ac18093c61c853bacb92031f42162e8a765 Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Fri, 24 Jul 2026 10:27:06 +0530 Subject: [PATCH 2/2] anthropic: robustly detect PDF media types and error on hosted file references --- provider/anthropicprovider/agent.go | 20 +++++- provider/anthropicprovider/agent_test.go | 90 ++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/provider/anthropicprovider/agent.go b/provider/anthropicprovider/agent.go index 3bd767c1..8185d415 100644 --- a/provider/anthropicprovider/agent.go +++ b/provider/anthropicprovider/agent.go @@ -9,6 +9,7 @@ import ( "fmt" "iter" "maps" + "mime" "reflect" "slices" "strings" @@ -439,6 +440,17 @@ func (a *client) buildMessageParams(messages []*message.Message, opts []agent.Op return params, nil } +// isPDFMediaType reports whether mediaType denotes a PDF document. Media types +// may carry parameters (e.g. "application/pdf; charset=binary") and arbitrary +// casing, so the base type is parsed and compared case-insensitively. +func isPDFMediaType(mediaType string) bool { + base, _, err := mime.ParseMediaType(mediaType) + if err != nil { + base = strings.ToLower(strings.TrimSpace(mediaType)) + } + return base == "application/pdf" +} + func buildMessageParam(msg *message.Message) (anthropic.MessageParam, error) { var content []anthropic.ContentBlockParamUnion @@ -488,7 +500,7 @@ func buildMessageParam(msg *message.Message) (anthropic.MessageParam, error) { mediaType = "image/jpeg" } content = append(content, anthropic.NewImageBlockBase64(mediaType, c.Data)) - case c.MediaType == "application/pdf": + case isPDFMediaType(c.MediaType): content = append(content, anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{ Data: c.Data, })) @@ -497,14 +509,16 @@ func buildMessageParam(msg *message.Message) (anthropic.MessageParam, error) { switch { case c.TopLevelMediaType() == "image": content = append(content, anthropic.NewImageBlock(anthropic.URLImageSourceParam{URL: c.URI})) - case c.MediaType == "application/pdf": + case isPDFMediaType(c.MediaType): content = append(content, anthropic.NewDocumentBlock(anthropic.URLPDFSourceParam{URL: c.URI})) } case *message.HostedFileContent: // The stable Anthropic Messages API used here (anthropic.MessageNewParams) // has no file-id image/document source in anthropic-sdk-go v1.58.1; only // the Beta API exposes BetaFileImageSourceParam/BetaFileDocumentSourceParam. - // A hosted file reference therefore cannot be forwarded yet. + // A hosted file reference therefore cannot be forwarded yet, so surface an + // explicit error rather than silently dropping it. + return anthropic.MessageParam{}, fmt.Errorf("anthropic: hosted file references (file id %q) are not supported by the Messages API; use DataContent or URIContent instead", c.FileID) } } diff --git a/provider/anthropicprovider/agent_test.go b/provider/anthropicprovider/agent_test.go index 64e35da7..5a4f3c05 100644 --- a/provider/anthropicprovider/agent_test.go +++ b/provider/anthropicprovider/agent_test.go @@ -651,3 +651,93 @@ func TestBuildMessageParam_ImageURLAndPDFAreForwarded(t *testing.T) { t.Error("document block with a base64 application/pdf source not found in request") } } + +// A PDF media type that carries parameters or non-canonical casing (e.g. +// "application/PDF; charset=binary") must still be recognized and forwarded as a +// document block, not dropped. +func TestBuildMessageParam_PDFMediaTypeWithParametersIsForwarded(t *testing.T) { + bodyCh := make(chan []byte, 1) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + t.Errorf("read request body: %v", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + bodyCh <- body + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, minimalMessageResponse("ok")) + })) + defer server.Close() + + a := newTestClient(t, server) + + msgs := []*message.Message{ + {Role: message.RoleUser, Contents: message.Contents{ + &message.DataContent{Data: "JVBERi0xLjQK", MediaType: "application/PDF; charset=binary"}, + }}, + } + if _, err := a.Run(t.Context(), msgs).Collect(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var req map[string]any + if err := json.Unmarshal(<-bodyCh, &req); err != nil { + t.Fatalf("unmarshal request body: %v", err) + } + messages, ok := req["messages"].([]any) + if !ok { + t.Fatalf("request messages = %#v, want a JSON array", req["messages"]) + } + + var documentBase64 bool + for _, m := range messages { + msg, ok := m.(map[string]any) + if !ok { + continue + } + blocks, ok := msg["content"].([]any) + if !ok { + continue + } + for _, b := range blocks { + block, ok := b.(map[string]any) + if !ok { + continue + } + source, _ := block["source"].(map[string]any) + if block["type"] == "document" && source["type"] == "base64" && source["data"] == "JVBERi0xLjQK" { + documentBase64 = true + } + } + } + if !documentBase64 { + t.Error("document block for a PDF media type with parameters not found in request") + } +} + +// A HostedFileContent cannot be represented by the stable Messages API, so the +// request must fail with an explicit error rather than silently dropping it. +func TestBuildMessageParam_HostedFileContentReturnsError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Error("request should not be sent when a hosted file reference is present") + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, minimalMessageResponse("ok")) + })) + defer server.Close() + + a := newTestClient(t, server) + + msgs := []*message.Message{ + {Role: message.RoleUser, Contents: message.Contents{ + &message.HostedFileContent{FileID: "file_123"}, + }}, + } + _, err := a.Run(t.Context(), msgs).Collect() + if err == nil { + t.Fatal("expected an error for a hosted file reference, got nil") + } + if !strings.Contains(err.Error(), "file_123") { + t.Errorf("error = %v, want it to mention the offending file id", err) + } +}