From 315aba49c29b1521afcea3dbfce960ae68d8f33d Mon Sep 17 00:00:00 2001 From: GGHansome Date: Fri, 10 Jul 2026 03:17:02 +0800 Subject: [PATCH] fix: claude model can not access image --- internal/backend/agent/model/anthropic.go | 80 ++++++++++++++++++++++- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/internal/backend/agent/model/anthropic.go b/internal/backend/agent/model/anthropic.go index 7906539..ab822c9 100644 --- a/internal/backend/agent/model/anthropic.go +++ b/internal/backend/agent/model/anthropic.go @@ -156,6 +156,18 @@ func anthropicEndpointURL(baseURL string) string { return base + "/v1/messages" } +// shouldRelocateAnthropicImages 判断是否需要把图片块搬运到末条 user 消息。 +// +// 官方 Anthropic 端点(api.anthropic.com)可正确处理任意位置的图片,保持原样; +// 其余第三方中转站默认启用搬运,规避「非末尾图片被丢弃」的转换问题。 +func shouldRelocateAnthropicImages(baseURL string) bool { + base := strings.ToLower(strings.TrimSpace(baseURL)) + if base == "" { + return false + } + return !strings.Contains(base, "api.anthropic.com") +} + // ApplyAnthropicCompatibleAuthHeaders 同时兼容 Anthropic 原生 x-api-key 和 Bearer token 代理。 func ApplyAnthropicCompatibleAuthHeaders(httpReq *http.Request, apiKey string) { if httpReq == nil { @@ -215,8 +227,9 @@ func (adapter *AnthropicAdapter) Stream(ctx context.Context, req StreamRequest, body := cloneRequestBodyOverride(req.RequestBodyOverride) if len(body) == 0 { thinkingConfig := buildAnthropicThinkingConfig(req) + relocateImages := shouldRelocateAnthropicImages(baseURL) stableMessageCount := anthropicStableProviderMessageCount(req.Messages, req.StableMessageCount, thinkingConfig != nil) - systemParts, messages, err := normalizeAnthropicProviderMessages(req.Messages, thinkingConfig != nil) + systemParts, messages, err := normalizeAnthropicProviderMessages(req.Messages, thinkingConfig != nil, relocateImages) if err != nil { return err } @@ -1066,7 +1079,7 @@ func anthropicStableProviderMessageCount(input []Message, stableReplayMessageCou if len(stableReplayMessages) == 0 { return 0 } - _, messages, err := normalizeAnthropicProviderMessages(stableReplayMessages, thinkingEnabled) + _, messages, err := normalizeAnthropicProviderMessages(stableReplayMessages, thinkingEnabled, false) if err != nil { return 0 } @@ -1122,7 +1135,7 @@ func isAnthropicCacheableBlock(block map[string]any) bool { } } -func normalizeAnthropicProviderMessages(input []Message, thinkingEnabled bool) ([]string, []anthropicMessage, error) { +func normalizeAnthropicProviderMessages(input []Message, thinkingEnabled bool, relocateImages bool) ([]string, []anthropicMessage, error) { systemParts := make([]string, 0, len(input)) messages := make([]anthropicMessage, 0, len(input)) pendingToolResults := make([]map[string]any, 0, 2) @@ -1208,9 +1221,70 @@ func normalizeAnthropicProviderMessages(input []Message, thinkingEnabled bool) ( } } flushToolResults() + if relocateImages { + messages = relocateAnthropicImagesToLastUserMessage(messages) + } return systemParts, messages, nil } +// relocateAnthropicImagesToLastUserMessage 把所有 user 消息里的 image 块搬运到最后一条 user 消息的末尾。 +// +// 背景:部分第三方中转站(如 Bedrock 代理)在 Anthropic→上游 的消息转换中, +// 会丢弃「后面还跟着大量文本/消息」的非末尾图片块。将图片统一移动到末条 user 消息 +// 可规避该问题,同时保留图片信息本身。 +// +// 数据流演变: +// +// [user_info] [query + IMG] [reminder] [reminder] [current_request] +// → [user_info] [query] [reminder] [reminder] [current_request + IMG] +// +// 搬运后若某条 user 消息 content 变空,则丢弃该消息,避免 Anthropic 拒绝空内容消息。 +func relocateAnthropicImagesToLastUserMessage(messages []anthropicMessage) []anthropicMessage { + lastUserIndex := -1 + for index := len(messages) - 1; index >= 0; index-- { + if strings.TrimSpace(messages[index].Role) == "user" { + lastUserIndex = index + break + } + } + if lastUserIndex < 0 { + return messages + } + + relocated := make([]map[string]any, 0, 2) + for index := 0; index < len(messages); index++ { + if index == lastUserIndex || strings.TrimSpace(messages[index].Role) != "user" { + continue + } + kept := make([]map[string]any, 0, len(messages[index].Content)) + for _, block := range messages[index].Content { + if isAnthropicImageBlock(block) { + relocated = append(relocated, block) + continue + } + kept = append(kept, block) + } + messages[index].Content = kept + } + if len(relocated) == 0 { + return messages + } + messages[lastUserIndex].Content = append(messages[lastUserIndex].Content, relocated...) + + compacted := make([]anthropicMessage, 0, len(messages)) + for index, message := range messages { + if index != lastUserIndex && strings.TrimSpace(message.Role) == "user" && len(message.Content) == 0 { + continue + } + compacted = append(compacted, message) + } + return compacted +} + +func isAnthropicImageBlock(block map[string]any) bool { + return strings.TrimSpace(anthropicStringField(block, "type")) == "image" +} + func anthropicProviderContentBlocks(message Message, thinkingEnabled bool) ([]map[string]any, error) { blocks, err := anthropicContentBlocks(message) if err != nil {