diff --git a/backend/internal/pkg/antigravity/request_transformer.go b/backend/internal/pkg/antigravity/request_transformer.go index f9b917fb17..9349e54ee8 100644 --- a/backend/internal/pkg/antigravity/request_transformer.go +++ b/backend/internal/pkg/antigravity/request_transformer.go @@ -532,8 +532,8 @@ func buildParts(content json.RawMessage, toolIDToName map[string]string, allowDu } parts = append(parts, part) - case "image": - if block.Source != nil && block.Source.Type == "base64" { + case "image", "document": + if block.Source != nil && block.Source.Type == "base64" && strings.TrimSpace(block.Source.Data) != "" { parts = append(parts, GeminiPart{ InlineData: &GeminiInlineData{ MimeType: block.Source.MediaType, diff --git a/backend/internal/pkg/antigravity/request_transformer_test.go b/backend/internal/pkg/antigravity/request_transformer_test.go index 99d61867c9..1aff9f5775 100644 --- a/backend/internal/pkg/antigravity/request_transformer_test.go +++ b/backend/internal/pkg/antigravity/request_transformer_test.go @@ -646,6 +646,20 @@ func TestGeminiToolConfig_DropsBuiltinsWhenClientFunctionsPresent(t *testing.T) }) } +func TestBuildParts_DocumentBecomesInlineData(t *testing.T) { + content := `[ + {"type":"text","text":"read this"}, + {"type":"document","source":{"type":"base64","media_type":"application/pdf","data":"JVBERi0="}} + ]` + parts, stripped, err := buildParts(json.RawMessage(content), map[string]string{}, true) + require.NoError(t, err) + require.False(t, stripped) + require.Len(t, parts, 2) + require.NotNil(t, parts[1].InlineData) + require.Equal(t, "application/pdf", parts[1].InlineData.MimeType) + require.Equal(t, "JVBERi0=", parts[1].InlineData.Data) +} + // TestToolConfigAlwaysPresent ensures toolConfig is always emitted, including for // reasoning models without any tools: upstream rejects requests that omit it. func TestToolConfigAlwaysPresent(t *testing.T) { diff --git a/backend/internal/pkg/apicompat/responses_to_anthropic_invalid_blocks_test.go b/backend/internal/pkg/apicompat/responses_to_anthropic_invalid_blocks_test.go index ed410f6851..d3bd29081b 100644 --- a/backend/internal/pkg/apicompat/responses_to_anthropic_invalid_blocks_test.go +++ b/backend/internal/pkg/apicompat/responses_to_anthropic_invalid_blocks_test.go @@ -109,7 +109,23 @@ func TestResponsesToAnthropic_UnknownItemTypeKeepsRecognizableText(t *testing.T) require.NotContains(t, string(messages[0].Content), "drop me") } -// user 消息的分片全部不可识别时,以前会退化成 content:"",Anthropic 拒收空内容消息。 +// data URI 形式的 input_file 要变成 Anthropic document,供后续 Gemini inlineData 使用。 +func TestResponsesToAnthropic_InputFileDataURIBecomesDocument(t *testing.T) { + messages := responsesToAnthropicMessages(t, `[ + {"type":"message","role":"user","content":[ + {"type":"input_text","text":"read this"}, + {"type":"input_file","filename":"token.pdf","file_data":"data:application/pdf;base64,JVBERi0="} + ]} + ]`) + + requireAnthropicMessagesAreSendable(t, messages) + require.Len(t, messages, 1) + require.Contains(t, string(messages[0].Content), `"type":"document"`) + require.Contains(t, string(messages[0].Content), `"media_type":"application/pdf"`) + require.Contains(t, string(messages[0].Content), `"data":"JVBERi0="`) +} + +// 只有 file_id、没有 data URI 的 input_file 仍然无法转换,整条消息丢掉。 func TestResponsesToAnthropic_UserMessageWithOnlyUnknownPartsIsDropped(t *testing.T) { messages := responsesToAnthropicMessages(t, `[ {"type":"message","role":"user","content":[{"type":"input_file","file_id":"file_1"}]} diff --git a/backend/internal/pkg/apicompat/responses_to_anthropic_request.go b/backend/internal/pkg/apicompat/responses_to_anthropic_request.go index 06d9669d29..69928591d3 100644 --- a/backend/internal/pkg/apicompat/responses_to_anthropic_request.go +++ b/backend/internal/pkg/apicompat/responses_to_anthropic_request.go @@ -530,6 +530,14 @@ func convertResponsesUserToAnthropicContent(raw json.RawMessage) (json.RawMessag Source: src, }) } + case "input_file": + src := dataURIToAnthropicFileSource(p.FileData) + if src != nil { + blocks = append(blocks, AnthropicContentBlock{ + Type: "document", + Source: src, + }) + } } } @@ -617,6 +625,12 @@ func dataURIToAnthropicImageSource(dataURI string) *AnthropicImageSource { } } +// dataURIToAnthropicFileSource parses a data URI into a document source. +// file_id-only parts are not convertible here and stay dropped. +func dataURIToAnthropicFileSource(fileData string) *AnthropicImageSource { + return dataURIToAnthropicImageSource(fileData) +} + // mergeConsecutiveMessages merges consecutive messages with the same role // because Anthropic requires alternating user/assistant turns. func mergeConsecutiveMessages(messages []AnthropicMessage) []AnthropicMessage {