Files
2026-06-30 10:38:52 +08:00

214 lines
5.4 KiB
Go

package modeladapter
import (
"encoding/base64"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
)
const (
contentPartTypeText = "text"
contentPartTypeImage = "image"
)
// ContentPart 表示一条消息中的结构化内容块。
type ContentPart struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
Image *ImageContent `json:"image,omitempty"`
}
// ImageContent 表示消息中携带的一张图片。
type ImageContent struct {
MIMEType string `json:"mime_type,omitempty"`
Path string `json:"path,omitempty"`
Data []byte `json:"data,omitempty"`
}
func hasImageContentParts(parts []ContentPart) bool {
for _, part := range parts {
if normalizeContentPartType(part.Type) == contentPartTypeImage {
return true
}
}
return false
}
func collapseTextContentParts(parts []ContentPart) string {
if len(parts) == 0 {
return ""
}
texts := make([]string, 0, len(parts))
for _, part := range parts {
if normalizeContentPartType(part.Type) != contentPartTypeText {
continue
}
if strings.TrimSpace(part.Text) == "" {
continue
}
texts = append(texts, part.Text)
}
return strings.Join(texts, "")
}
func normalizeContentPartType(value string) string {
trimmed := strings.TrimSpace(strings.ToLower(value))
switch trimmed {
case "", contentPartTypeText:
return contentPartTypeText
case contentPartTypeImage:
return contentPartTypeImage
default:
return trimmed
}
}
func openAIContentValue(message Message) (any, error) {
if !hasImageContentParts(message.ContentParts) {
content := message.Content
if strings.TrimSpace(content) == "" && len(message.ContentParts) > 0 {
content = collapseTextContentParts(message.ContentParts)
}
return content, nil
}
parts := make([]map[string]any, 0, len(message.ContentParts)+1)
if len(message.ContentParts) == 0 && strings.TrimSpace(message.Content) != "" {
parts = append(parts, map[string]any{
"type": contentPartTypeText,
"text": message.Content,
})
}
for _, part := range message.ContentParts {
switch normalizeContentPartType(part.Type) {
case contentPartTypeText:
if part.Text == "" {
continue
}
parts = append(parts, map[string]any{
"type": contentPartTypeText,
"text": part.Text,
})
case contentPartTypeImage:
dataURL, err := imageContentDataURL(part.Image)
if err != nil {
return nil, err
}
parts = append(parts, map[string]any{
"type": "image_url",
"image_url": map[string]any{
"url": dataURL,
},
})
default:
return nil, fmt.Errorf("unsupported openai content part type: %s", strings.TrimSpace(part.Type))
}
}
if len(parts) == 0 {
return message.Content, nil
}
return parts, nil
}
func anthropicContentBlocks(message Message) ([]map[string]any, error) {
if len(message.ContentParts) == 0 {
if strings.TrimSpace(message.Content) == "" {
return nil, nil
}
return []map[string]any{{
"type": contentPartTypeText,
"text": message.Content,
}}, nil
}
blocks := make([]map[string]any, 0, len(message.ContentParts))
for _, part := range message.ContentParts {
switch normalizeContentPartType(part.Type) {
case contentPartTypeText:
if part.Text == "" {
continue
}
blocks = append(blocks, map[string]any{
"type": contentPartTypeText,
"text": part.Text,
})
case contentPartTypeImage:
payload, mediaType, err := resolveImageContent(part.Image)
if err != nil {
return nil, err
}
blocks = append(blocks, map[string]any{
"type": contentPartTypeImage,
"source": map[string]any{
"type": "base64",
"media_type": mediaType,
"data": base64.StdEncoding.EncodeToString(payload),
},
})
default:
return nil, fmt.Errorf("unsupported anthropic content part type: %s", strings.TrimSpace(part.Type))
}
}
if len(blocks) == 0 && strings.TrimSpace(message.Content) != "" {
blocks = append(blocks, map[string]any{
"type": contentPartTypeText,
"text": message.Content,
})
}
return blocks, nil
}
func imageContentDataURL(image *ImageContent) (string, error) {
payload, mediaType, err := resolveImageContent(image)
if err != nil {
return "", err
}
return "data:" + mediaType + ";base64," + base64.StdEncoding.EncodeToString(payload), nil
}
func resolveImageContent(image *ImageContent) ([]byte, string, error) {
if image == nil {
return nil, "", fmt.Errorf("image content is required")
}
if len(image.Data) > 0 {
return image.Data, normalizeImageMIMEType(image.MIMEType, image.Path, image.Data), nil
}
path := strings.TrimSpace(image.Path)
if path == "" {
return nil, "", fmt.Errorf("image content is missing data and path")
}
payload, err := os.ReadFile(path)
if err != nil {
return nil, "", fmt.Errorf("read image content failed: %w", err)
}
return payload, normalizeImageMIMEType(image.MIMEType, path, payload), nil
}
func normalizeImageMIMEType(mimeType string, path string, payload []byte) string {
trimmed := strings.TrimSpace(strings.ToLower(mimeType))
if trimmed != "" {
return trimmed
}
if len(payload) > 0 {
detected := strings.TrimSpace(strings.ToLower(http.DetectContentType(payload)))
if strings.HasPrefix(detected, "image/") {
return detected
}
}
switch strings.ToLower(filepath.Ext(strings.TrimSpace(path))) {
case ".jpg", ".jpeg":
return "image/jpeg"
case ".png":
return "image/png"
case ".gif":
return "image/gif"
case ".webp":
return "image/webp"
default:
return "image/png"
}
}