统一过滤非 x:x 比例,避免默认值参与候选匹配或透传上游。 支持 xK 分辨率归一化,并按 Gemini、Volces 和 OpenAI 的参数规范生成上游请求;OpenAI 显式 WxH 始终保留用户原始尺寸。 验证:在独立临时 worktree 中执行 apps/api 全量 go test ./... -count=1 通过,真实 OpenAI 请求已到达上游但受本地无效凭据阻断。
233 lines
6.4 KiB
Go
233 lines
6.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|