转换器: 图片内容块跨协议互转保留(base64/url)
- 支持 OpenAI image_url ↔ Anthropic image.source(base64/url) 双向转换 - messages→chat / responses→chat 不再丢弃图片块, 保留图文原始顺序 - chat→messages 的 data URL 拆成 base64 source(media_type+data) - chat→responses 的 image_url 转 input_image - responses↔messages 经 chat 中转, 六方向全覆盖 - 新增多图/顺序回归测试 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -137,9 +137,11 @@ func chatMsgToAnthropic(m chatMsg) any {
|
||||
var url string
|
||||
if iu, ok := b["image_url"].(map[string]any); ok {
|
||||
url, _ = iu["url"].(string)
|
||||
} else if s, ok := b["image_url"].(string); ok {
|
||||
url = s
|
||||
}
|
||||
if url != "" {
|
||||
blocks = append(blocks, map[string]any{"type": "image", "source": map[string]any{"type": "url", "url": url}})
|
||||
blocks = append(blocks, anthropicImageBlock(url))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,12 +231,18 @@ func anthropicMsgToChat(role string, content json.RawMessage) []any {
|
||||
if json.Unmarshal(content, &blocks) == nil && blocks != nil {
|
||||
var out []any
|
||||
var textParts []string
|
||||
var contentBlocks []any // text / image_url 块,保留原始顺序
|
||||
var toolCalls []any
|
||||
for _, b := range blocks {
|
||||
switch b["type"] {
|
||||
case "text":
|
||||
if t, _ := b["text"].(string); t != "" {
|
||||
textParts = append(textParts, t)
|
||||
contentBlocks = append(contentBlocks, map[string]any{"type": "text", "text": t})
|
||||
}
|
||||
case "image":
|
||||
if cb := chatImageBlock(b); cb != nil {
|
||||
contentBlocks = append(contentBlocks, cb)
|
||||
}
|
||||
case "tool_use":
|
||||
id, _ := b["id"].(string)
|
||||
@@ -254,9 +262,19 @@ func anthropicMsgToChat(role string, content json.RawMessage) []any {
|
||||
out = append(out, map[string]any{"role": "tool", "tool_call_id": callID, "content": res})
|
||||
}
|
||||
}
|
||||
if len(textParts) > 0 || len(toolCalls) > 0 {
|
||||
hasImage := false
|
||||
for _, cb := range contentBlocks {
|
||||
if m, _ := cb.(map[string]any); m["type"] == "image_url" {
|
||||
hasImage = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasImage || len(textParts) > 0 || len(toolCalls) > 0 {
|
||||
msg := map[string]any{"role": role}
|
||||
if len(textParts) > 0 {
|
||||
switch {
|
||||
case hasImage:
|
||||
msg["content"] = contentBlocks
|
||||
case len(textParts) > 0:
|
||||
msg["content"] = strings.Join(textParts, "")
|
||||
}
|
||||
if len(toolCalls) > 0 {
|
||||
@@ -416,6 +434,58 @@ func intOrNil(p *int, def int) any {
|
||||
return *p
|
||||
}
|
||||
|
||||
// splitDataURL 解析 data:media_type;base64,data 形式的 URL;非该形式返回 ok=false。
|
||||
func splitDataURL(url string) (media, data string, ok bool) {
|
||||
if !strings.HasPrefix(url, "data:") {
|
||||
return "", "", false
|
||||
}
|
||||
i := strings.Index(url, ";base64,")
|
||||
if i < 0 {
|
||||
return "", "", false
|
||||
}
|
||||
return url[len("data:"):i], url[i+len(";base64,"):], true
|
||||
}
|
||||
|
||||
// chatImageBlock 把 Anthropic image 块转 OpenAI image_url 块。
|
||||
// 仅支持 base64 与 url source;其他类型(如 Files API 的 file_id)不支持,跳过。
|
||||
func chatImageBlock(b map[string]any) any {
|
||||
src, ok := b["source"].(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
switch src["type"] {
|
||||
case "base64":
|
||||
media, _ := src["media_type"].(string)
|
||||
data, _ := src["data"].(string)
|
||||
if data == "" {
|
||||
return nil
|
||||
}
|
||||
if media == "" {
|
||||
media = "image/png"
|
||||
}
|
||||
return map[string]any{"type": "image_url", "image_url": map[string]any{"url": "data:" + media + ";base64," + data}}
|
||||
case "url":
|
||||
url, _ := src["url"].(string)
|
||||
if url == "" {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{"type": "image_url", "image_url": map[string]any{"url": url}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// anthropicImageBlock 把 OpenAI image_url 的 url 转 Anthropic image 块。
|
||||
// data URL → base64 source;http(s) URL → url source。
|
||||
func anthropicImageBlock(url string) any {
|
||||
if media, data, ok := splitDataURL(url); ok {
|
||||
if media == "" {
|
||||
media = "image/png"
|
||||
}
|
||||
return map[string]any{"type": "image", "source": map[string]any{"type": "base64", "media_type": media, "data": data}}
|
||||
}
|
||||
return map[string]any{"type": "image", "source": map[string]any{"type": "url", "url": url}}
|
||||
}
|
||||
|
||||
func rawOrObject(raw json.RawMessage) any {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return map[string]any{}
|
||||
|
||||
Reference in New Issue
Block a user