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
@@ -160,6 +160,9 @@ func (s *Service) hydrateProviderRequestAssetString(ctx context.Context, value s
return value, nil
}
style, ok := requestAssetCapabilityHydrationForMedia("image", candidate, raw, "")
if !ok && openAIImageEditRequiresMultipartBytes(candidate) {
style, ok = requestAssetHydrateDataURL, true
}
if !ok {
return value, nil
}
@@ -320,6 +323,9 @@ func requestAssetHydrationForField(path []string, asset store.RequestAsset, cand
if style, ok := requestAssetCapabilityHydrationForMedia("image", candidate, asset.URL, asset.StorageProvider); ok {
return style
}
if openAIImageEditRequiresMultipartBytes(candidate) {
return requestAssetHydrateDataURL
}
}
if mediaURLFieldNeedsHydration(path) {
if style := configuredRequestAssetMediaURLHydration(candidate, requestAssetMediaURLKind(path)); style != "" {
@@ -332,6 +338,11 @@ func requestAssetHydrationForField(path []string, asset store.RequestAsset, cand
return requestAssetHydrateURL
}
func openAIImageEditRequiresMultipartBytes(candidate store.RuntimeModelCandidate) bool {
return normalizeProviderKey(candidate.Provider) == "openai" &&
strings.TrimSpace(candidate.ModelType) == "image_edit"
}
func requestAssetMediaKindForHydration(path []string, asset store.RequestAsset) string {
if mediaURLFieldNeedsHydration(path) {
return requestAssetMediaURLKind(path)
@@ -205,6 +205,35 @@ func TestHydrateProviderRequestAssetsUsesImageCapabilityBase64ForTopLevelImageAs
}
}
func TestHydrateProviderRequestAssetsConvertsOpenAIEditImagesForMultipart(t *testing.T) {
payload := []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', 1, 2, 3, 4}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
_, _ = w.Write(payload)
}))
defer server.Close()
service := &Service{}
body := map[string]any{
"images": []any{
server.URL + "/first.png",
server.URL + "/second.png",
},
}
hydrated, err := service.hydrateProviderRequestAssets(context.Background(), body, store.RuntimeModelCandidate{
Provider: "openai",
ModelType: "image_edit",
})
if err != nil {
t.Fatalf("hydrate OpenAI edit images: %v", err)
}
images := hydrated["images"].([]any)
want := "data:image/png;base64," + base64.StdEncoding.EncodeToString(payload)
if len(images) != 2 || stringFromAny(images[0]) != want || stringFromAny(images[1]) != want {
t.Fatalf("OpenAI edit images should be hydrated for multipart: %+v", images)
}
}
func TestHydrateProviderRequestAssetsImageCapabilityOverridesProviderDataURLDefault(t *testing.T) {
service := &Service{}
body := map[string]any{