Repository: danielmiessler/fabric
File: internal/plugins/ai/anthropic/anthropic.go (Go)

Code before:
			}
		case chat.ChatMessagePartTypeImageURL:
			if part.ImageURL == nil || strings.TrimSpace(part.ImageURL.URL) == "" {
				continue
			}
			if block, ok := contentBlockFromAttachmentURL(part.ImageURL.URL); ok {
				blocks = append(blocks, block)
			}
		}
	}
	return blocks
}

func prependSystemContentToBlocks(systemContent string, blocks []anthropic.ContentBlockParamUnion) []anthropic.ContentBlockParamUnion {
	if len(blocks) == 0 {
		return []anthropic.ContentBlockParamUnion{anthropic.NewTextBlock(systemContent)}
	}
	if blocks[0].OfText != nil {
		blocks[0].OfText.Text = systemContent + "\n\n" + blocks[0].OfText.Text
		return blocks
	}
	return append([]anthropic.ContentBlockParamUnion{anthropic.NewTextBlock(systemContent)}, blocks...)
}

func contentBlockFromAttachmentURL(url string) (anthropic.ContentBlockParamUnion, bool) {
	if strings.HasPrefix(url, "data:") {
		mimeType, data, ok := parseDataURL(url)
		if !ok {
			return anthropic.ContentBlockParamUnion{}, false
		}
		if strings.EqualFold(mimeType, "application/pdf") {
			return anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{Data: data}), true
		}
		if normalized := normalizeImageMimeType(mimeType); normalized != "" {
			return anthropic.NewImageBlockBase64(normalized, data), true
		}
		return anthropic.ContentBlockParamUnion{}, false
	}
	if isPDFURL(url) {
		return anthropic.NewDocumentBlock(anthropic.URLPDFSourceParam{URL: url}), true
	}
	return anthropic.NewImageBlock(anthropic.URLImageSourceParam{URL: url}), true
}

func parseDataURL(value string) (mimeType string, data string, ok bool) {
	if !strings.HasPrefix(value, "data:") {
		return "", "", false
	}
	withoutPrefix := strings.TrimPrefix(value, "data:")
	parts := strings.SplitN(withoutPrefix, ",", 2)
	if len(parts) != 2 {

Review comment (performance):
While technically correct about mutation, this is intentional design for this use case. The function is only called with freshly created blocks in `contentBlocksFromMultiContent`, and the mutation is a performance optimization to avoid unnecessary allocations. The caller never reuses the blocks slice after this call.

Creating a copy would add complexity and memory allocation without practical benefit here. Keeping the simpler, more efficient approach.

Code after:
		switch part.Type {
		case chat.ChatMessagePartTypeText:
			if strings.TrimSpace(part.Text) != "" {
				blocks = append(blocks, anthropic.NewTextBlock(part.Text))
			}
		case chat.ChatMessagePartTypeImageURL:
			if part.ImageURL == nil || strings.TrimSpace(part.ImageURL.URL) == "" {
				continue
			}
			if block, ok := contentBlockFromAttachmentURL(part.ImageURL.URL); ok {
				blocks = append(blocks, block)
			}
		}
	}
	return blocks
}

// prependSystemContentToBlocks prepends system content to content blocks. If the first
// block is text, it merges the system content with it; otherwise, it prepends a new text block.
func prependSystemContentToBlocks(systemContent string, blocks []anthropic.ContentBlockParamUnion) []anthropic.ContentBlockParamUnion {
	if len(blocks) == 0 {
		return []anthropic.ContentBlockParamUnion{anthropic.NewTextBlock(systemContent)}
	}
	if blocks[0].OfText != nil {
		blocks[0].OfText.Text = systemContent + "\n\n" + blocks[0].OfText.Text
		return blocks
	}
	return append([]anthropic.ContentBlockParamUnion{anthropic.NewTextBlock(systemContent)}, blocks...)
}

// contentBlockFromAttachmentURL converts an attachment URL into an Anthropic content block.
// For data URLs, it parses the MIME type and base64 data to create image or PDF blocks.
// For remote URLs, it creates URL-based image blocks, or PDF document blocks if the URL ends in .pdf.
// Returns the content block and true on success, or an empty block and false if unsupported.
func contentBlockFromAttachmentURL(url string) (anthropic.ContentBlockParamUnion, bool) {
	if strings.HasPrefix(url, "data:") {
		mimeType, data, ok := parseDataURL(url)
		if !ok {
			debuglog.Debug(debuglog.Basic, "contentBlockFromAttachmentURL: failed to parse data URL")
			return anthropic.ContentBlockParamUnion{}, false
		}
		if strings.EqualFold(mimeType, "application/pdf") {
			return anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{Data: data}), true
		}
		if normalized := normalizeImageMimeType(mimeType); normalized != "" {
			return anthropic.NewImageBlockBase64(normalized, data), true
		}
		debuglog.Debug(debuglog.Basic, "contentBlockFromAttachmentURL: unsupported MIME type %s", mimeType)
		return anthropic.ContentBlockParamUnion{}, false
	}
	if isPDFURL(url) {