fix(media): 规范比例与 OpenAI 图像尺寸参数
统一过滤非 x:x 比例,避免默认值参与候选匹配或透传上游。 支持 xK 分辨率归一化,并按 Gemini、Volces 和 OpenAI 的参数规范生成上游请求;OpenAI 显式 WxH 始终保留用户原始尺寸。 验证:在独立临时 worktree 中执行 apps/api 全量 go test ./... -count=1 通过,真实 OpenAI 请求已到达上游但受本地无效凭据阻断。
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
func TestGeminiGenerationConfigDropsDefaultRatioTokensAndNormalizesKSize(t *testing.T) {
|
||||
config := geminiGenerationConfig(map[string]any{
|
||||
"aspect_ratio": "auto",
|
||||
"size": "2k",
|
||||
"generationConfig": map[string]any{
|
||||
"imageConfig": map[string]any{
|
||||
"aspectRatio": "adaptive",
|
||||
"imageSize": "2k",
|
||||
},
|
||||
},
|
||||
}, true)
|
||||
|
||||
imageConfig := mapFromAny(config["imageConfig"])
|
||||
if _, ok := imageConfig["aspectRatio"]; ok {
|
||||
t.Fatalf("non-ratio tokens must not be sent to Gemini: %+v", config)
|
||||
}
|
||||
if imageConfig["imageSize"] != "2K" {
|
||||
t.Fatalf("Gemini imageSize should use canonical K format: %+v", config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeOfficialOpenAIImageSizeUsesModelSpecificWireFormat(t *testing.T) {
|
||||
legacy := map[string]any{
|
||||
"model": "gpt-image-1",
|
||||
"size": "2K",
|
||||
"width": 2048,
|
||||
"height": 1152,
|
||||
"aspect_ratio": "16:9",
|
||||
}
|
||||
normalizeOfficialOpenAIImageSize(legacy, map[string]any{
|
||||
"size": "2K",
|
||||
"aspect_ratio": "16:9",
|
||||
})
|
||||
if legacy["size"] != "1536x1024" {
|
||||
t.Fatalf("legacy GPT Image should use an official orientation size: %+v", legacy)
|
||||
}
|
||||
|
||||
flexible := map[string]any{
|
||||
"model": "gpt-image-2",
|
||||
"size": "2K",
|
||||
"resolution": "2k",
|
||||
"aspect_ratio": "16:9",
|
||||
}
|
||||
normalizeOfficialOpenAIImageSize(flexible, map[string]any{
|
||||
"size": "2K",
|
||||
"aspect_ratio": "16:9",
|
||||
})
|
||||
if flexible["size"] != "2048x1152" {
|
||||
t.Fatalf("GPT Image 2 should receive flexible pixel dimensions: %+v", flexible)
|
||||
}
|
||||
|
||||
rounded := map[string]any{
|
||||
"model": "gpt-image-2",
|
||||
"size": "2K",
|
||||
"resolution": "2K",
|
||||
"aspect_ratio": "3:2",
|
||||
}
|
||||
normalizeOfficialOpenAIImageSize(rounded, map[string]any{
|
||||
"size": "2K",
|
||||
"aspect_ratio": "3:2",
|
||||
})
|
||||
if rounded["size"] != "2048x1360" {
|
||||
t.Fatalf("GPT Image 2 dimensions should satisfy the official 16-pixel multiple: %+v", rounded)
|
||||
}
|
||||
|
||||
explicit := map[string]any{
|
||||
"model": "gpt-image-2",
|
||||
"size": "1232x768",
|
||||
"width": 1232,
|
||||
"height": 768,
|
||||
}
|
||||
normalizeOfficialOpenAIImageSize(explicit, map[string]any{
|
||||
"size": "1234x777",
|
||||
})
|
||||
if explicit["size"] != "1234x777" {
|
||||
t.Fatalf("an explicit WxH size must be sent unchanged to OpenAI: %+v", explicit)
|
||||
}
|
||||
|
||||
explicitLegacy := map[string]any{
|
||||
"model": "gpt-image-1",
|
||||
"size": "2048x1152",
|
||||
}
|
||||
normalizeOfficialOpenAIImageSize(explicitLegacy, map[string]any{
|
||||
"size": "2048x1152",
|
||||
})
|
||||
if explicitLegacy["size"] != "2048x1152" {
|
||||
t.Fatalf("explicit WxH must also win for legacy OpenAI image models: %+v", explicitLegacy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIClientUsesOriginalWxHAndConvertsKResolution(t *testing.T) {
|
||||
received := make([]map[string]any, 0, 3)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode OpenAI image request: %v", err)
|
||||
}
|
||||
received = append(received, body)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"data": []any{map[string]any{"b64_json": "aW1hZ2U="}},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
candidate := store.RuntimeModelCandidate{
|
||||
BaseURL: server.URL,
|
||||
Provider: "openai",
|
||||
ProviderModelName: "gpt-image-2",
|
||||
ModelType: "image_generate",
|
||||
Credentials: map[string]any{"apiKey": "openai-key"},
|
||||
}
|
||||
requests := []Request{
|
||||
{
|
||||
Kind: "images.generations",
|
||||
ModelType: "image_generate",
|
||||
Body: map[string]any{
|
||||
"prompt": "explicit dimensions",
|
||||
"size": "1232x768",
|
||||
"width": 1232,
|
||||
"height": 768,
|
||||
},
|
||||
OriginalBody: map[string]any{
|
||||
"size": "1234x777",
|
||||
},
|
||||
Candidate: candidate,
|
||||
},
|
||||
{
|
||||
Kind: "images.generations",
|
||||
ModelType: "image_generate",
|
||||
Body: map[string]any{
|
||||
"prompt": "resolution conversion",
|
||||
"size": "2048x1365",
|
||||
"resolution": "2K",
|
||||
"aspect_ratio": "3:2",
|
||||
"width": 2048,
|
||||
"height": 1365,
|
||||
},
|
||||
OriginalBody: map[string]any{
|
||||
"size": "2K",
|
||||
"aspect_ratio": "3:2",
|
||||
},
|
||||
Candidate: candidate,
|
||||
},
|
||||
{
|
||||
Kind: "images.generations",
|
||||
ModelType: "image_generate",
|
||||
Body: map[string]any{
|
||||
"prompt": "resolution field conversion",
|
||||
"size": "1024x1024",
|
||||
"resolution": "1K",
|
||||
"aspect_ratio": "1:1",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
},
|
||||
OriginalBody: map[string]any{
|
||||
"resolution": "1k",
|
||||
"aspect_ratio": "1:1",
|
||||
},
|
||||
Candidate: candidate,
|
||||
},
|
||||
}
|
||||
for _, request := range requests {
|
||||
if _, err := (OpenAIClient{HTTPClient: server.Client()}).Run(context.Background(), request); err != nil {
|
||||
t.Fatalf("run OpenAI image request: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(received) != 3 {
|
||||
t.Fatalf("unexpected request count: %d", len(received))
|
||||
}
|
||||
if received[0]["size"] != "1234x777" {
|
||||
t.Fatalf("explicit WxH should win over preprocessed dimensions: %+v", received[0])
|
||||
}
|
||||
if received[1]["size"] != "2048x1360" {
|
||||
t.Fatalf("2K with 3:2 should use normalized OpenAI dimensions: %+v", received[1])
|
||||
}
|
||||
if received[2]["size"] != "1024x1024" {
|
||||
t.Fatalf("resolution=1K should be converted to OpenAI dimensions: %+v", received[2])
|
||||
}
|
||||
for _, body := range received {
|
||||
for _, key := range []string{"resolution", "aspect_ratio", "width", "height"} {
|
||||
if _, ok := body[key]; ok {
|
||||
t.Fatalf("generic geometry field %q must not reach OpenAI: %+v", key, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolcesImageBodySelectsConfiguredSizeFormat(t *testing.T) {
|
||||
body := volcesImageBody(Request{
|
||||
Kind: "images.generations",
|
||||
ModelType: "image_generate",
|
||||
Body: map[string]any{
|
||||
"model": "Seedream-5.0-Pro",
|
||||
"resolution": "2k",
|
||||
"size": "2048x1152",
|
||||
"width": 2048,
|
||||
"height": 1152,
|
||||
"aspect_ratio": "16:9",
|
||||
"platformId": "gateway-internal-platform",
|
||||
},
|
||||
Candidate: store.RuntimeModelCandidate{
|
||||
ProviderModelName: "doubao-seedream-5-0-pro-260628",
|
||||
ModelType: "image_generate",
|
||||
Capabilities: map[string]any{
|
||||
"image_generate": map[string]any{
|
||||
"size_param_format": "resolution",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if body["size"] != "2K" {
|
||||
t.Fatalf("resolution-only Volces model should receive canonical K size: %+v", body)
|
||||
}
|
||||
for _, key := range []string{"resolution", "width", "height", "aspect_ratio", "platformId"} {
|
||||
if _, ok := body[key]; ok {
|
||||
t.Fatalf("generic geometry field %q must not reach Volces: %+v", key, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user