feat(provider): 模型协议 - 新增 Agnes 图像声明式协议插件
This commit is contained in:
162
backend/internal/protocol/agnes_image_test.go
Normal file
162
backend/internal/protocol/agnes_image_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Agnes 图像端点是同步的,文生图与图生图共用 POST /v1/images/generations。
|
||||
// 官方参数表只承认 model、prompt、size(必填)、ratio、image、return_base64 与
|
||||
// extra_body:顶层 response_format 是官方明确列出的错误写法,output_format 会被
|
||||
// 直接拒绝,n 也不在参数表内。因此这里逐字段固定线协议,防止退回 OpenAI 兼容字段。
|
||||
func TestAgnesImageSendsOnlyDocumentedFields(t *testing.T) {
|
||||
adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image")
|
||||
if adapter.Metadata().RequiresPublicMediaURLs {
|
||||
t.Fatal("Agnes 图像支持 Data URI 参考图,不应强制要求公共媒体 URL")
|
||||
}
|
||||
create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
|
||||
Model: "agnes-image-2.1-flash", Prompt: "海边写真的亚洲女模特", AspectRatio: "3:4", Quality: "2k", ImageCount: 4,
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if create.Method != "POST" || create.Path != "/v1/images/generations" {
|
||||
t.Fatalf("create = %#v", create)
|
||||
}
|
||||
// 原生端点在域名根下,originPath 让请求路径替换 Base URL 的路径段,
|
||||
// 渠道沿用 .../v1 也能命中正确地址。
|
||||
if !create.OriginPath {
|
||||
t.Fatal("create 必须声明 originPath")
|
||||
}
|
||||
|
||||
body := manifestTestBody(t, create)
|
||||
if body["model"] != "agnes-image-2.1-flash" || body["prompt"] != "海边写真的亚洲女模特" {
|
||||
t.Fatalf("body = %#v", body)
|
||||
}
|
||||
if body["size"] != "2K" {
|
||||
t.Fatalf("size = %#v, 期望按质量档位发送 2K", body["size"])
|
||||
}
|
||||
if body["ratio"] != "3:4" {
|
||||
t.Fatalf("ratio = %#v, 期望独立的 ratio 字段", body["ratio"])
|
||||
}
|
||||
// 官方参数表之外的字段会被上游按“不支持该字段”拒绝,逐个钉死。
|
||||
for _, forbidden := range []string{"n", "output_format", "quality", "background", "response_format", "image", "mask", "style", "user"} {
|
||||
if _, present := body[forbidden]; present {
|
||||
t.Fatalf("body = %#v, 官方参数表没有 %s 字段,不能发送", body, forbidden)
|
||||
}
|
||||
}
|
||||
|
||||
extra, ok := body["extra_body"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("extra_body = %#v", body["extra_body"])
|
||||
}
|
||||
if extra["response_format"] != "url" {
|
||||
t.Fatalf("extra_body.response_format = %#v, 输出格式只能声明在 extra_body 内", extra["response_format"])
|
||||
}
|
||||
if _, present := extra["image"]; present {
|
||||
t.Fatalf("extra_body = %#v, 文生图不应发送 image", extra)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgnesImagePutsReferencesInExtraBody(t *testing.T) {
|
||||
adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image")
|
||||
create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
|
||||
Model: "agnes-image-2.1-flash", Prompt: "换装并保留原始构图", AspectRatio: "3:4", Quality: "1k",
|
||||
Images: []MediaReference{
|
||||
{URL: "https://cdn.example/anchor.png", Role: "edit_source", Order: 0},
|
||||
{DataURL: "data:image/png;base64,QUJD", Role: "edit_source", Order: 1},
|
||||
},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
body := manifestTestBody(t, create)
|
||||
if _, present := body["image"]; present {
|
||||
t.Fatalf("body = %#v, 参考图必须放在 extra_body.image,而不是顶层 image", body)
|
||||
}
|
||||
extra, _ := body["extra_body"].(map[string]any)
|
||||
images, ok := extra["image"].([]any)
|
||||
if !ok || len(images) != 2 {
|
||||
t.Fatalf("extra_body.image = %#v, 期望两张参考图", extra["image"])
|
||||
}
|
||||
if images[0] != "https://cdn.example/anchor.png" {
|
||||
t.Fatalf("第一张 = %#v, order 决定顺序", images[0])
|
||||
}
|
||||
// 有 Data URL 时优先 Base64:Agnes 明确支持 Data URI,本地开发无法提供公共图片地址。
|
||||
if images[1] != "data:image/png;base64,QUJD" {
|
||||
t.Fatalf("第二张 = %#v, 有 dataUrl 时必须优先 Base64", images[1])
|
||||
}
|
||||
if body["size"] != "1K" {
|
||||
t.Fatalf("size = %#v, 图生图同样必须携带必填的 size", body["size"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgnesImageSizeFollowsPixelOrTier(t *testing.T) {
|
||||
adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image")
|
||||
tests := []struct {
|
||||
name string
|
||||
aspectRatio string
|
||||
quality string
|
||||
wantSize string
|
||||
wantRatio string
|
||||
}{
|
||||
{name: "比例落到分辨率档位", aspectRatio: "16:9", quality: "4k", wantSize: "4K", wantRatio: "16:9"},
|
||||
{name: "像素尺寸原样透传", aspectRatio: "1024x768", quality: "2k", wantSize: "1024x768"},
|
||||
{name: "auto 也必须落到档位", aspectRatio: "auto", quality: "1k", wantSize: "1K"},
|
||||
{name: "空比例回落到默认档位", aspectRatio: "", quality: "", wantSize: "1K"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
|
||||
Model: "agnes-image-2.1-flash", Prompt: "circle", AspectRatio: test.aspectRatio, Quality: test.quality,
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := manifestTestBody(t, create)
|
||||
if body["size"] != test.wantSize {
|
||||
t.Fatalf("size = %#v, 期望 %q", body["size"], test.wantSize)
|
||||
}
|
||||
gotRatio, present := body["ratio"]
|
||||
if test.wantRatio == "" {
|
||||
if present {
|
||||
t.Fatalf("ratio = %#v, 非比例输入不得发送 ratio", gotRatio)
|
||||
}
|
||||
return
|
||||
}
|
||||
if gotRatio != test.wantRatio {
|
||||
t.Fatalf("ratio = %#v, 期望 %q", gotRatio, test.wantRatio)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgnesImageRejectsUnsupportedRatio(t *testing.T) {
|
||||
adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image")
|
||||
// 9:21 / 4:5 等比例不在官方比例表内,静默回落到 1:1 会让用户拿到与选择不符的画幅。
|
||||
if _, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
|
||||
Model: "agnes-image-2.1-flash", Prompt: "x", AspectRatio: "9:21",
|
||||
}}); err == nil {
|
||||
t.Fatal("9:21 不在官方比例表内,必须在校验阶段拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgnesImageReadsUrlAndBase64Output(t *testing.T) {
|
||||
adapter := officialPackageAdapter(t, "agnes-image.yingce-plugin", "agnes-image")
|
||||
result, err := adapter.ParseCreate(context.Background(), []byte(`{"created":1780000000,"data":[{"url":"https://storage.googleapis.com/agnes-aigc/x.png","b64_json":null}]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Result == nil || len(result.Result.Images) != 1 || result.Result.Images[0].URL != "https://storage.googleapis.com/agnes-aigc/x.png" {
|
||||
t.Fatalf("images = %#v", result.Result)
|
||||
}
|
||||
|
||||
inline, err := adapter.ParseCreate(context.Background(), []byte(`{"data":[{"url":null,"b64_json":"aW1hZ2U="}]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if inline.Result == nil || len(inline.Result.Images) != 1 || inline.Result.Images[0].DataURL != "data:image/png;base64,aW1hZ2U=" {
|
||||
t.Fatalf("images = %#v", inline.Result)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user