转换器: 图片内容块跨协议互转保留(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:
Sakurasan
2026-08-16 09:04:09 +08:00
co-authored by Claude
parent 7f28c8b83b
commit 4b507d8064
3 changed files with 382 additions and 9 deletions
@@ -101,20 +101,82 @@ func responsesInputToChat(raw json.RawMessage) []any {
out = append(out, map[string]any{"role": role, "content": content})
} else if blocks, ok := item["content"].([]any); ok {
var text []string
var contentBlocks []any
for _, b := range blocks {
if bm, ok := b.(map[string]any); ok {
bm, ok := b.(map[string]any)
if !ok {
continue
}
switch bm["type"] {
case "input_text", "text":
if t, _ := bm["text"].(string); t != "" {
text = append(text, t)
contentBlocks = append(contentBlocks, map[string]any{"type": "text", "text": t})
}
case "input_image":
var url string
if s, ok := bm["image_url"].(string); ok {
url = s
} else if m, ok := bm["image_url"].(map[string]any); ok {
url, _ = m["url"].(string)
}
if url != "" {
contentBlocks = append(contentBlocks, map[string]any{"type": "image_url", "image_url": map[string]any{"url": url}})
}
}
}
out = append(out, map[string]any{"role": role, "content": strings.Join(text, "")})
hasImage := false
for _, cb := range contentBlocks {
if m, _ := cb.(map[string]any); m["type"] == "image_url" {
hasImage = true
break
}
}
if hasImage {
out = append(out, map[string]any{"role": role, "content": contentBlocks})
} else {
out = append(out, map[string]any{"role": role, "content": strings.Join(text, "")})
}
}
}
}
return out
}
// chatContentToResponsesBlocks 把 Chat 用户消息 content 转 Responses input 块数组(input_text / input_image)。
func chatContentToResponsesBlocks(content json.RawMessage) []any {
// 纯字符串 → 单个 input_text
var s string
if json.Unmarshal(content, &s) == nil && s != "" {
return []any{map[string]any{"type": "input_text", "text": s}}
}
// 数组 → 按块转换(text / image_url)
var arr []map[string]any
if json.Unmarshal(content, &arr) == nil && arr != nil {
var out []any
for _, b := range arr {
switch b["type"] {
case "text", "input_text":
if t, _ := b["text"].(string); t != "" {
out = append(out, map[string]any{"type": "input_text", "text": t})
}
case "image_url":
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 != "" {
out = append(out, map[string]any{"type": "input_image", "image_url": url})
}
}
}
return out
}
return nil
}
// ---------------------------------------------------------------------------
// 请求:Chat → Responses
@@ -170,10 +232,8 @@ func chatToResponsesReq(body []byte) ([]byte, error) {
}})
}
default:
if s := str(m.Content); s != "" {
input = append(input, map[string]any{"type": "message", "role": "user", "content": []any{
map[string]any{"type": "input_text", "text": s},
}})
if blocks := chatContentToResponsesBlocks(m.Content); len(blocks) > 0 {
input = append(input, map[string]any{"type": "message", "role": "user", "content": blocks})
}
}
}