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

This commit is contained in:
zhuchi
2026-09-16 20:03:46 +08:00
parent 9868f5e8b8
commit a35868ddef
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)
}
}

View File

@@ -64,6 +64,17 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
- 已知边界:协议用 `originPath` 声明请求路径,请求路径会替换渠道 Base URL 的路径段,因此渠道继续沿用 `.../compatible-mode/v1` 也能命中原生端点(`.../api/v1/services/...`);只有域名需要正确。协议把 `size` 映射为 1024 级竖版/方形档位,未登记的档位会被省略并交由模型自动推荐(官方要求像素面积 512*512~2048*2048)。
- 尚未处理:AI 模特页仍按“带参考图即走 `/images/edits`”的通用规则分支,未按协议能力判断;换成该插件后两阶段派生能否直接复用需实测确认。
## Agnes 图像协议插件(agnes-image)
- 新增官方协议包 `plugin-packages/agnes-image.yingce-plugin`,解决 Agnes 图片模型无法调用的问题:内置 OpenAI 图片路径会固定发送 `n`,并按渠道能力开关发送 `output_format` 与顶层 `response_format`,而 Agnes 的参数表没有这些字段,`POST /v1/images/generations` 直接返回 HTTP 400 `invalid_request`,上游原话为“output_format 不是文生图队列支持的字段”。`n` 无法通过任何开关摘除,因此必须独立协议。
- 协议形态:同步 `POST /v1/images/generations`(`originPath`),文生图与图生图共用同一入口。`size` 必填,由统一层质量归一为 `1K`/`2K`/`3K`/`4K` 档位,`WxH` 像素尺寸原样透传;画面比例走独立的 `ratio` 字段;参考图放 `extra_body.image` 并优先 Base64 data URL,多图合成传多张,不使用 `/v1/images/edits` multipart;输出格式只能声明在 `extra_body.response_format`(默认 `url`,可用 `providerOptions.agnes-image.response_format` 覆盖为 `b64_json`)。
- 前端能力默认值同步新增 `agnes-image` 分支:尺寸按“比例 + 1k/2k/4k 档位”选择,关闭上游不支持的 `output_format`、顶层 `response_format`、透明背景和蒙版编辑,`maxOutputs` 固定为 1(该端点不接受 `n`,单次请求固定返回一张)。官方另有 `3K` 档位,统一层只提供 1k/2k/4k 三档,故界面不露出。
- 已由单元测试覆盖:逐项断言 `n`、`output_format`、`quality`、`background`、顶层 `response_format` 与顶层 `image` 均不出现在请求体中,参考图进 `extra_body.image` 且按 order 排序并优先 Base64,比例落档位 / 像素原样透传 / `auto` 与空值回落档位,不在官方比例表内的取值在校验阶段拒绝,响应同时读取 `data[0].url` 与 `data[0].b64_json`。`go test ./internal/protocol/`、`go build ./...`、`bun run typecheck` 与相关前端测试均通过。
- 已完成真机验收(Agnes 中国站 Key,`agnes-image-2.1-flash` 与 `agnes-image-2.5-flash` 各一轮,AI 模特页):母版与派生任务全部 `succeeded`,`portrait_role` 分别为 `anchor` 与 `derive` 并拿到 Agnes 的 `task_*` 号;调用日志里母版对应 `/images/t2i/...`、派生对应 `/images/i2i/...`,且两张派生图都被宿主成功下载。派生阶段能产出 i2i 结果,证明参考图确实经 `extra_body.image` 送达——内置 OpenAI 路径只会把参考图送去 Agnes 没有的 `/images/edits`,因此这条链路只可能由本插件完成。
- 待验收:多图合成(单次传多张参考图)、`providerOptions.agnes-image.response_format=b64_json` 覆盖、以及 `size`/`ratio` 的实际取值。任务行只持久化 `mode` 与 `metadata`,调用日志也不记录请求体,因此档位与比例发出去的具体值无法从库中读回,需要抓包或在插件侧临时打印才能确认,目前只能由上游行为反推。
- 渠道配置注意:模型协议改为本插件后必须重启后端,官方插件目录只在启动时装载;已保存过的 `capabilityConfig` 不会被新协议默认值覆盖,需要手动把尺寸参数改为 `aspect_ratio`、最大参考图设回大于 0,否则 AI 模特会退化为不带参考图的单阶段候选图。
- 已知边界:默认输出 URL,由宿主立即下载并持久化;若上游图片地址无法公开回源,改用 `providerOptions.agnes-image.response_format=b64_json`。官方比例表只有 8 种,其他比例(如 `4:5`、`9:21`)在请求前即被拒绝,不会静默回落为 1:1。
## 云端 Agent 行为、能力和偏好分层(待真实环境验收)
- [x] 系统/媒体行为资源由后端发布物加载,运行快照记录策略和能力版本/哈希;用户 → 项目 → 画布偏好经归属校验和独立 revision CAS,合并 revision 固定到新轮次,旧轮次保留原快照。system prompt 只注入偏好清单,正文由 `agent_profile_read` 从固定快照按层读取。工具 schema、节点类型、可写字段和画布读取来自服务端节点能力 registry;收费生成还需实际媒体任务适配器。写入、审批与预算仍以服务端强校验为准。

Binary file not shown.

View File

@@ -0,0 +1,5 @@
# Agnes Image
该目录是独立官方协议插件源码。后端从生成的 `agnes-image.yingce-plugin` 包加载,不依赖系统内置 `host:` 适配器。
完整接口见 [docs/interface.md](docs/interface.md)。

View File

@@ -0,0 +1,462 @@
# Agnes Image 接口字段
## 协议身份
- 插件 ID:`agnes-image`。
- Provider ID:`agnes-image`。
- 能力:`image`。
- 默认 Base URL:`https://api.agnes-ai.cn`。
- 鉴权驱动:`bearer`。
- 创建:`POST /v1/images/generations`。
- 生命周期:同步响应。
## 配置字段
| 字段 | 类型 | 必填 | 含义 |
| --- | --- | --- | --- |
| `apiKey` | secret | 是 | API Key |
## 统一字段映射
| 统一字段 | 类型 | 必填 | 上游映射 | 说明 |
| --- | --- | --- | --- | --- |
| `model` | string | 是 | `model` | 图片模型 ID。 |
| `prompt` | string | 是 | `prompt` | 图片提示词。 |
| `images` | media[] | 否 | `provider image/reference fields` | 参考图或编辑源图,role 由业务层确定。 |
| `imageCount` | integer | 否 | `n/sample_count` | 输出数量。 |
| `aspectRatio` | string | 否 | `size/aspect_ratio` | 比例或尺寸,语义按协议说明。 |
| `resolution` | string | 否 | `resolution/imageSize` | 分辨率档位。 |
| `quality` | string | 否 | `quality` | 质量档位。 |
| `providerOptions` | object | 否 | `provider-specific fields` | 插件命名空间内的厂商扩展字段。 |
## 上游请求模板逐字段清单
下表由插件请求模板生成,覆盖 body、query、headers 和 multipart 文件声明中的每个字段。
| 上游位置 | 值或转换表达式 |
| --- | --- |
| `create.method` | `"POST"` |
| `create.path` | `"/v1/images/generations"` |
| `create.contentType` | `"application/json"` |
| `create.body.model` | `{"$ref":"request.model"}` |
| `create.body.prompt` | `{"$ref":"request.prompt"}` |
| `create.body.size` | `{"$omitEmpty":{"$if":{"condition":{"$eq":[{"$len":{"$split":[{"$trim":{"$ref":"request.aspectRatio"}},"x"]}},2]},"then":{"$trim":{"$ref":"request.aspectRatio"}},"else":{"$switch":{"cases":[{"when":{"$in":[{"$lower":{"$trim":{"$ref":"request.quality"}}},["1k","low","standard"]]},"then":"1K"},{"when":{"$in":[{"$lower":{"$trim":{"$ref":"request.quality"}}},["2k","medium","hd","high"]]},"then":"2K"},{"when":{"$in":[{"$lower":{"$trim":{"$ref":"request.quality"}}},["3k"]]},"then":"3K"},{"when":{"$in":[{"$lower":{"$trim":{"$ref":"request.quality"}}},["4k"]]},"then":"4K"}],"default":"1K"}}}}}` |
| `create.body.ratio` | `{"$omitEmpty":{"$if":{"condition":{"$in":[{"$trim":{"$ref":"request.aspectRatio"}},["1:1","3:4","4:3","16:9","9:16","2:3","3:2","21:9"]]},"then":{"$trim":{"$ref":"request.aspectRatio"}},"else":null}}}` |
| `create.body.extra_body` | `{"$omitEmpty":{"image":{"$omitEmpty":{"$map":{"from":{"$sortByOrder":{"$ref":"request.images"}},"as":"media","in":{"$coalesce":[{"$ref":"media.dataUrl"},{"$ref":"media.url"}]}}}},"response_format":{"$coalesce":[{"$ref":"request.providerOptions.agnes-image.response_format"},"url"]}}}` |
## Provider 扩展键
- `providerOptions.agnes-image.response_format`
动态模型或工作流允许使用文档声明的完整 `parameters/input/extra_body` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。
## 响应映射逐字段清单
| 映射位置 | 上游路径或转换表达式 |
| --- | --- |
| `response.status` | `"succeeded"` |
| `response.images` | `{"$map":{"from":{"$ref":"response.data"},"as":"item","in":{"url":{"$omitEmpty":{"$ref":"item.url"}},"dataUrl":{"$if":{"condition":{"$ref":"item.b64_json"},"then":{"$concat":["data:image/png;base64,",{"$ref":"item.b64_json"}]},"else":null}}}}}` |
| `response.errorPaths[0]` | `"error.code"` |
| `response.errorPaths[1]` | `"code"` |
| `response.messagePaths[0]` | `"error.message"` |
| `response.messagePaths[1]` | `"message"` |
| `response.messagePaths[2]` | `"msg"` |
## 响应与错误
插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。
## 兼容边界
Agnes 官方图像端点,同步返回。文生图与图生图共用 /v1/images/generations:不传 image 为文生图,传 image 为图生图或多图合成。size 必填,取 1K/2K/3K/4K 档位或 WxH 精确尺寸;画面比例走独立的 ratio 字段。参考图放在 extra_body.image,支持公共 HTTPS URL 或 Data URI Base64。顶层 response_format 是官方明确列出的错误写法,输出格式只能声明在 extra_body.response_format。该端点不接受 n,单次请求固定返回一张图片,需要多张时由上层拆分为多个任务。
<!-- YINGCE_MANIFEST_CONTRACT_START -->
## Manifest 完整接口定义
以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。
```json
{
"apiVersion": "yingce.plugin/v2",
"id": "agnes-image",
"name": "Agnes Image",
"version": "2.0.0",
"author": "Agnes AI / 影策",
"description": "Agnes Image 独立请求协议插件。",
"documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>",
"permissions": [
"generation.run",
"media.read"
],
"configuration": {
"fields": [
{
"name": "apiKey",
"type": "secret",
"label": "API Key",
"required": true
}
]
},
"contributes": {
"providers": [
{
"id": "agnes-image",
"label": "Agnes Image",
"capabilities": [
"image"
],
"scopes": [
"admin.system-channel",
"user.custom-channel",
"canvas",
"creation",
"agent"
],
"baseUrl": "https://api.agnes-ai.cn",
"requiresPublicMediaUrls": false,
"auth": {
"type": "bearer",
"field": "apiKey"
},
"parameters": [
{
"name": "model",
"type": "string",
"required": true,
"mapping": "model",
"description": "图片模型 ID。"
},
{
"name": "prompt",
"type": "string",
"required": true,
"mapping": "prompt",
"description": "图片提示词。"
},
{
"name": "images",
"type": "media[]",
"required": false,
"mapping": "provider image/reference fields",
"description": "参考图或编辑源图,role 由业务层确定。"
},
{
"name": "imageCount",
"type": "integer",
"required": false,
"mapping": "n/sample_count",
"description": "输出数量。"
},
{
"name": "aspectRatio",
"type": "string",
"required": false,
"mapping": "size/aspect_ratio",
"description": "比例或尺寸,语义按协议说明。"
},
{
"name": "resolution",
"type": "string",
"required": false,
"mapping": "resolution/imageSize",
"description": "分辨率档位。"
},
{
"name": "quality",
"type": "string",
"required": false,
"mapping": "quality",
"description": "质量档位。"
},
{
"name": "providerOptions",
"type": "object",
"required": false,
"mapping": "provider-specific fields",
"description": "插件命名空间内的厂商扩展字段。"
}
],
"validations": [
{
"assert": {
"$or": [
{
"$in": [
{
"$trim": {
"$ref": "request.aspectRatio"
}
},
[
"",
"auto",
"1:1",
"3:4",
"4:3",
"16:9",
"9:16",
"2:3",
"3:2",
"21:9"
]
]
},
{
"$eq": [
{
"$len": {
"$split": [
{
"$trim": {
"$ref": "request.aspectRatio"
}
},
"x"
]
}
},
2
]
}
]
},
"message": "Agnes 图像只支持 1:1、3:4、4:3、16:9、9:16、2:3、3:2、21:9 比例或 WxH 像素尺寸"
}
],
"create": {
"method": "POST",
"path": "/v1/images/generations",
"contentType": "application/json",
"body": {
"model": {
"$ref": "request.model"
},
"prompt": {
"$ref": "request.prompt"
},
"size": {
"$omitEmpty": {
"$if": {
"condition": {
"$eq": [
{
"$len": {
"$split": [
{
"$trim": {
"$ref": "request.aspectRatio"
}
},
"x"
]
}
},
2
]
},
"then": {
"$trim": {
"$ref": "request.aspectRatio"
}
},
"else": {
"$switch": {
"cases": [
{
"when": {
"$in": [
{
"$lower": {
"$trim": {
"$ref": "request.quality"
}
}
},
[
"1k",
"low",
"standard"
]
]
},
"then": "1K"
},
{
"when": {
"$in": [
{
"$lower": {
"$trim": {
"$ref": "request.quality"
}
}
},
[
"2k",
"medium",
"hd",
"high"
]
]
},
"then": "2K"
},
{
"when": {
"$in": [
{
"$lower": {
"$trim": {
"$ref": "request.quality"
}
}
},
[
"3k"
]
]
},
"then": "3K"
},
{
"when": {
"$in": [
{
"$lower": {
"$trim": {
"$ref": "request.quality"
}
}
},
[
"4k"
]
]
},
"then": "4K"
}
],
"default": "1K"
}
}
}
}
},
"ratio": {
"$omitEmpty": {
"$if": {
"condition": {
"$in": [
{
"$trim": {
"$ref": "request.aspectRatio"
}
},
[
"1:1",
"3:4",
"4:3",
"16:9",
"9:16",
"2:3",
"3:2",
"21:9"
]
]
},
"then": {
"$trim": {
"$ref": "request.aspectRatio"
}
},
"else": null
}
}
},
"extra_body": {
"$omitEmpty": {
"image": {
"$omitEmpty": {
"$map": {
"from": {
"$sortByOrder": {
"$ref": "request.images"
}
},
"as": "media",
"in": {
"$coalesce": [
{
"$ref": "media.dataUrl"
},
{
"$ref": "media.url"
}
]
}
}
}
},
"response_format": {
"$coalesce": [
{
"$ref": "request.providerOptions.agnes-image.response_format"
},
"url"
]
}
}
}
},
"originPath": true
},
"response": {
"status": "succeeded",
"images": {
"$map": {
"from": {
"$ref": "response.data"
},
"as": "item",
"in": {
"url": {
"$omitEmpty": {
"$ref": "item.url"
}
},
"dataUrl": {
"$if": {
"condition": {
"$ref": "item.b64_json"
},
"then": {
"$concat": [
"data:image/png;base64,",
{
"$ref": "item.b64_json"
}
]
},
"else": null
}
}
}
}
},
"errorPaths": [
"error.code",
"code"
],
"messagePaths": [
"error.message",
"message",
"msg"
]
}
}
]
}
}
```
<!-- YINGCE_MANIFEST_CONTRACT_END -->

File diff suppressed because one or more lines are too long

View File

@@ -686,6 +686,57 @@ add({
})
});
// Agnes 图像是同步端点,文生图与图生图共用 POST /v1/images/generations:
// 不传 image 为文生图,传 image 为图生图或多图合成。官方参数表只承认
// model、prompt、size(必填,档位 1K/2K/3K/4K 或 WxH 精确尺寸)、ratio、image、
// return_base64 和 extra_body,因此这里不发送 n、output_format、quality、
// background 等 OpenAI 兼容字段。顶层 response_format 是官方明确列出的错误写法,
// 输出格式只能声明在 extra_body 内。参考图放 extra_body.image,支持公共 HTTPS URL
// 和 Data URI Base64,所以不强制公共媒体 URL,本地开发也能直接联调。
const agnesImageRatios = ["1:1", "3:4", "4:3", "16:9", "9:16", "2:3", "3:2", "21:9"];
const agnesImageAspect = trim(ref("request.aspectRatio"));
const agnesImageIsRatio = { $in: [agnesImageAspect, agnesImageRatios] };
const agnesImageIsPixel = eq(len(split(agnesImageAspect, "x")), 2);
// size 是官方必填项:像素尺寸原样透传,其余情况(比例、auto、空值)一律落到分辨率档位,不能省略。
const agnesImageSize = conditional(agnesImageIsPixel, agnesImageAspect, {
$switch: {
cases: [
{ when: { $in: [lower(trim(ref("request.quality"))), ["1k", "low", "standard"]] }, then: "1K" },
{ when: { $in: [lower(trim(ref("request.quality"))), ["2k", "medium", "hd", "high"]] }, then: "2K" },
{ when: { $in: [lower(trim(ref("request.quality"))), ["3k"]] }, then: "3K" },
{ when: { $in: [lower(trim(ref("request.quality"))), ["4k"]] }, then: "4K" }
],
default: "1K"
}
});
add({
id: "agnes-image", providerId: "agnes-image", name: "Agnes Image", vendor: "Agnes AI", capability: "image",
baseUrl: "https://api.agnes-ai.cn", auth: bearer, params: imageParams, requiresPublicMediaUrls: false,
notes: "Agnes 官方图像端点,同步返回。文生图与图生图共用 /v1/images/generations:不传 image 为文生图,传 image 为图生图或多图合成。size 必填,取 1K/2K/3K/4K 档位或 WxH 精确尺寸;画面比例走独立的 ratio 字段。参考图放在 extra_body.image,支持公共 HTTPS URL 或 Data URI Base64。顶层 response_format 是官方明确列出的错误写法,输出格式只能声明在 extra_body.response_format。该端点不接受 n,单次请求固定返回一张图片,需要多张时由上层拆分为多个任务。",
validations: [
{ assert: { $or: [{ $in: [agnesImageAspect, ["", "auto", ...agnesImageRatios]] }, eq(len(split(agnesImageAspect, "x")), 2)] }, message: "Agnes 图像只支持 1:1、3:4、4:3、16:9、9:16、2:3、3:2、21:9 比例或 WxH 像素尺寸" }
],
create: jsonCreate("/v1/images/generations", {
model: ref("request.model"),
prompt: ref("request.prompt"),
size: omit(agnesImageSize),
ratio: omit(conditional(agnesImageIsRatio, agnesImageAspect)),
extra_body: omit({
image: omit(map(sorted(ref("request.images")), "media", coalesce(ref("media.dataUrl"), ref("media.url")))),
response_format: coalesce(ref("request.providerOptions.agnes-image.response_format"), "url")
})
}, { originPath: true }),
response: {
status: "succeeded",
images: map(ref("response.data"), "item", {
url: omit(ref("item.url")),
dataUrl: conditional(ref("item.b64_json"), { $concat: ["data:image/png;base64,", ref("item.b64_json")] })
}),
errorPaths: ["error.code", "code"], messagePaths: ["error.message", "message", "msg"]
}
});
add({
id: "agnes-video-v20", providerId: "agnes-video-v20", name: "Agnes Video V2.0", vendor: "Agnes AI", capability: "video",
baseUrl: "https://apihub.agnes-ai.com/v1", auth: bearer, params: videoParams, requiresPublicMediaUrls: true,