feat(provider): 模型协议 - 新增 Agnes 图像声明式协议插件

This commit is contained in:
zhuchi
2026-09-16 20:03:46 +08:00
parent da0531f6eb
commit ff1a5afac5
7 changed files with 1073 additions and 0 deletions

View 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)
}
}