From 33b9839cd218b46db8b2c2d462ff5f208af9cfaf Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Fri, 28 Aug 2026 14:11:00 +0530 Subject: [PATCH 1/2] Forward a data: URIContent to Anthropic as an inline base64 block buildMessageParam's URIContent case only handled image and PDF as URL sources (anthropic.URLImageSourceParam / URLPDFSourceParam), which require an external http(s) reference. A URIContent carrying a data: URI was sent with the whole data: string as the url, which Anthropic rejects (400). Decode the data: URI and emit a base64 image/PDF block instead, reusing the same primitives as the DataContent branch (NewImageBlockBase64 / Base64PDFSourceParam). This mirrors the Gemini provider (data: -> InlineData) and the OpenAI chat provider (data: -> inline), which already special-case data: URIs on URIContent. Non-data http(s) URLs keep the URL source. --- provider/anthropicprovider/agent.go | 18 +++++++++ provider/anthropicprovider/agent_test.go | 51 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/provider/anthropicprovider/agent.go b/provider/anthropicprovider/agent.go index a3873254..fe82bc5f 100644 --- a/provider/anthropicprovider/agent.go +++ b/provider/anthropicprovider/agent.go @@ -5,6 +5,7 @@ package anthropicprovider import ( "cmp" "context" + "encoding/base64" "encoding/json" "fmt" "iter" @@ -691,6 +692,23 @@ func buildMessageParam(msg *message.Message) (anthropic.MessageParam, error) { } case *message.URIContent: switch { + case strings.HasPrefix(strings.ToLower(c.URI), "data:"): + // A data: URI carries the bytes inline. Anthropic's URL image/PDF + // sources require an external http(s) reference, so a data: URI sent + // as a url source is rejected; decode it and send a base64 block + // instead, mirroring the DataContent branch and the Gemini/OpenAI + // data: handling. + data, mediaType, err := message.DecodeDataURI(c.URI) + if err != nil { + break + } + encoded := base64.StdEncoding.EncodeToString(data) + switch { + case strings.HasPrefix(mediaType, "image/"): + content = append(content, anthropic.NewImageBlockBase64(mediaType, encoded)) + case isPDFMediaType(mediaType): + content = append(content, anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{Data: encoded})) + } case c.TopLevelMediaType() == "image": content = append(content, anthropic.NewImageBlock(anthropic.URLImageSourceParam{URL: c.URI})) case isPDFMediaType(c.MediaType): diff --git a/provider/anthropicprovider/agent_test.go b/provider/anthropicprovider/agent_test.go index 0ef3c0c1..3363033c 100644 --- a/provider/anthropicprovider/agent_test.go +++ b/provider/anthropicprovider/agent_test.go @@ -1509,3 +1509,54 @@ func TestStreamingClosesResponseBody(t *testing.T) { t.Fatal("streaming response body was not closed after early consumer exit") } } + +// A URIContent carrying a data: URI must be decoded and forwarded to Anthropic +// as an inline base64 block, not passed through as a URL source (which the API +// rejects). Mirrors the DataContent branch and the Gemini/OpenAI data: handling. +func TestBuildMessageParam_DataURIImageForwardedAsBase64(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: "data:image/png;base64,aGVsbG8=", MediaType: "image/png"}, + }}, + } + if _, err := a.Run(t.Context(), msgs).Collect(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + body := <-bodyCh + var req map[string]any + if err := json.Unmarshal(body, &req); err != nil { + t.Fatalf("unmarshal request body: %v", err) + } + messages, _ := req["messages"].([]any) + var base64Image bool + for _, m := range messages { + msg, _ := m.(map[string]any) + blocks, _ := msg["content"].([]any) + for _, b := range blocks { + block, _ := b.(map[string]any) + source, _ := block["source"].(map[string]any) + if block["type"] == "image" && source["type"] == "base64" && source["media_type"] == "image/png" { + base64Image = true + } + } + } + if !base64Image { + t.Fatalf("expected data: URIContent image forwarded as a base64 image source, got: %s", body) + } +} From b218324d499f2bb3ef20d8b8d55ba5d68fb451b9 Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Sun, 6 Sep 2026 08:54:24 +0530 Subject: [PATCH 2/2] Address review: error on data URI decode failure + assert payload - Return an explicit error (matching geminiprovider) instead of swallowing a DecodeDataURI failure with break, which would drop the content and send a request with missing blocks. - Let an explicit URIContent.MediaType override the media type parsed from the data: URI, and match media types case-insensitively (mirrors gemini). - Strengthen the test to assert the forwarded base64 payload, not just the source type/media_type. --- provider/anthropicprovider/agent.go | 9 +++++++-- provider/anthropicprovider/agent_test.go | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/provider/anthropicprovider/agent.go b/provider/anthropicprovider/agent.go index fe82bc5f..f9aef53d 100644 --- a/provider/anthropicprovider/agent.go +++ b/provider/anthropicprovider/agent.go @@ -700,11 +700,16 @@ func buildMessageParam(msg *message.Message) (anthropic.MessageParam, error) { // data: handling. data, mediaType, err := message.DecodeDataURI(c.URI) if err != nil { - break + return anthropic.MessageParam{}, fmt.Errorf("anthropicprovider: failed to decode data URI content: %w", err) + } + // An explicit URIContent.MediaType overrides the one parsed from the + // data: URI, matching the gemini provider. + if c.MediaType != "" { + mediaType = c.MediaType } encoded := base64.StdEncoding.EncodeToString(data) switch { - case strings.HasPrefix(mediaType, "image/"): + case strings.HasPrefix(strings.ToLower(mediaType), "image/"): content = append(content, anthropic.NewImageBlockBase64(mediaType, encoded)) case isPDFMediaType(mediaType): content = append(content, anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{Data: encoded})) diff --git a/provider/anthropicprovider/agent_test.go b/provider/anthropicprovider/agent_test.go index 3363033c..c4e7e56c 100644 --- a/provider/anthropicprovider/agent_test.go +++ b/provider/anthropicprovider/agent_test.go @@ -1551,7 +1551,7 @@ func TestBuildMessageParam_DataURIImageForwardedAsBase64(t *testing.T) { for _, b := range blocks { block, _ := b.(map[string]any) source, _ := block["source"].(map[string]any) - if block["type"] == "image" && source["type"] == "base64" && source["media_type"] == "image/png" { + if block["type"] == "image" && source["type"] == "base64" && source["media_type"] == "image/png" && source["data"] == "aGVsbG8=" { base64Image = true } }