fix(images): 兼容 OpenAI 多图编辑 multipart

- 将 OpenAI images/edits 请求构造成 multipart/form-data
- 在运行时安全转存 URL 图片为 data URL 后提交二进制文件
- 覆盖多图字段、真实上游模型名和内部元数据过滤测试
This commit is contained in:
2026-07-24 08:59:42 +08:00
parent 1da712854f
commit db640c1211
4 changed files with 331 additions and 2 deletions
+64
View File
@@ -344,6 +344,70 @@ func TestOpenAIClientChatContract(t *testing.T) {
}
}
func TestOpenAIClientImageEditUsesMultipartWithMultipleImages(t *testing.T) {
var contentType string
var receivedFields map[string][]string
var receivedImageCount int
var receivedImageFields []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
contentType = r.Header.Get("Content-Type")
if err := r.ParseMultipartForm(1 << 20); err != nil {
t.Fatalf("parse multipart image edit: %v", err)
}
receivedFields = r.MultipartForm.Value
for field, files := range r.MultipartForm.File {
receivedImageFields = append(receivedImageFields, field)
receivedImageCount += len(files)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": []any{map[string]any{"b64_json": "aW1hZ2U="}},
})
}))
defer server.Close()
_, err := (OpenAIClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
Kind: "images.edits",
ModelType: "image_edit",
Model: "gpt-image-2",
Body: map[string]any{
"model": "gpt-image-2",
"prompt": "combine the references",
"size": "1024x1536",
"quality": "medium",
"images": []any{
"data:image/png;base64," + base64.StdEncoding.EncodeToString([]byte("image one")),
"data:image/jpeg;base64," + base64.StdEncoding.EncodeToString([]byte("image two")),
},
"_metadata": map[string]any{"private": true},
},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
Provider: "openai",
ProviderModelName: "gpt-image-2-vip",
ModelType: "image_edit",
Credentials: map[string]any{"apiKey": "openai-key"},
},
})
if err != nil {
t.Fatalf("run OpenAI image edit: %v", err)
}
if !strings.HasPrefix(contentType, "multipart/form-data; boundary=") {
t.Fatalf("image edit should use multipart/form-data, got %q", contentType)
}
if receivedImageCount != 2 || len(receivedImageFields) != 1 || receivedImageFields[0] != "image" {
t.Fatalf("unexpected multipart image files: fields=%v count=%d", receivedImageFields, receivedImageCount)
}
if receivedFields["model"][0] != "gpt-image-2-vip" ||
receivedFields["prompt"][0] != "combine the references" ||
receivedFields["size"][0] != "1024x1536" ||
receivedFields["quality"][0] != "medium" {
t.Fatalf("unexpected multipart fields: %+v", receivedFields)
}
if _, ok := receivedFields["_metadata"]; ok {
t.Fatalf("internal metadata must not be forwarded: %+v", receivedFields)
}
}
func TestOpenAIClientChatReasoningParamsByProvider(t *testing.T) {
cases := []struct {
name string