Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions provider/anthropicprovider/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package anthropicprovider
import (
"cmp"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"iter"
Expand Down Expand Up @@ -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}))
}
Comment on lines +695 to +711
case c.TopLevelMediaType() == "image":
content = append(content, anthropic.NewImageBlock(anthropic.URLImageSourceParam{URL: c.URI}))
case isPDFMediaType(c.MediaType):
Expand Down
51 changes: 51 additions & 0 deletions provider/anthropicprovider/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading