mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-18 12:07:02 +08:00
v0.3.8
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user