Files
ttstd-infinite-canvas/backend/internal/protocol/official_catalog_test.go

757 lines
32 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package protocol
import (
"context"
"encoding/json"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
const (
manifestContractStart = "<!-- YINGCE_MANIFEST_CONTRACT_START -->"
manifestContractEnd = "<!-- YINGCE_MANIFEST_CONTRACT_END -->"
manifestDocumentationPlaceholder = "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>"
)
func TestOfficialProtocolPackagesAreSelfContainedDeclarativePlugins(t *testing.T) {
paths, err := filepath.Glob(filepath.Join("..", "..", "..", "plugin-packages", "*.yingce-plugin"))
if err != nil {
t.Fatal(err)
}
if len(paths) < 70 {
t.Fatalf("official protocol packages = %d, want at least 70", len(paths))
}
providerOwners := map[string]string{}
for _, path := range paths {
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
pkg, err := ParsePluginPackage(data)
if err != nil {
t.Fatalf("parse %s: %v", filepath.Base(path), err)
}
if len(pkg.Manifest.Contributes.PaymentProviders) > 0 {
// Payment packages use the yingce.payment/v1 executable ABI rather than
// the declarative AI provider contract covered by this catalog test.
continue
}
if pkg.Manifest.APIVersion != "yingce.plugin/v2" {
t.Fatalf("%s apiVersion = %q", filepath.Base(path), pkg.Manifest.APIVersion)
}
if strings.HasPrefix(strings.TrimSpace(pkg.Manifest.Runtime.Backend), "host:") {
t.Fatalf("%s depends on host runtime", filepath.Base(path))
}
if len(pkg.Files["README.md"]) == 0 || len(pkg.Files["docs/interface.md"]) == 0 {
t.Fatalf("%s must contain README.md and docs/interface.md", filepath.Base(path))
}
interfaceDocs := string(pkg.Files["docs/interface.md"])
expectedDocumentation := strings.TrimSpace(string(pkg.Files["README.md"])) + "\n\n---\n\n" + strings.TrimSpace(interfaceDocs)
if strings.TrimSpace(pkg.Manifest.Metadata.Documentation) != expectedDocumentation {
t.Fatalf("%s manifest.documentation does not contain the packaged README and interface document", filepath.Base(path))
}
assertManifestContractMatchesPackage(t, filepath.Base(path), pkg.ManifestRaw, interfaceDocs)
if !strings.Contains(interfaceDocs, "响应") {
t.Fatalf("%s interface documentation must describe response mapping", filepath.Base(path))
}
for _, field := range pkg.Manifest.Configuration.Fields {
if !strings.Contains(interfaceDocs, field.Name) {
t.Fatalf("%s interface documentation is missing configuration field %q", filepath.Base(path), field.Name)
}
}
adapters, err := LoadInstalledProviders(pkg.ManifestRaw, nil)
if err != nil {
t.Fatalf("load %s: %v", filepath.Base(path), err)
}
if len(adapters) != len(pkg.Manifest.Contributes.Providers) {
t.Fatalf("%s adapters = %d, providers = %d", filepath.Base(path), len(adapters), len(pkg.Manifest.Contributes.Providers))
}
for _, provider := range pkg.Manifest.Contributes.Providers {
if owner, duplicate := providerOwners[provider.ID]; duplicate {
t.Fatalf("provider id %q is shared by %s and %s", provider.ID, owner, filepath.Base(path))
}
providerOwners[provider.ID] = filepath.Base(path)
for _, parameter := range provider.Parameters {
if strings.TrimSpace(parameter.Name) == "" || strings.TrimSpace(parameter.Type) == "" || strings.TrimSpace(parameter.Mapping) == "" || strings.TrimSpace(parameter.Description) == "" {
t.Fatalf("%s provider %s has incomplete parameter documentation: %#v", filepath.Base(path), provider.ID, parameter)
}
if !strings.Contains(interfaceDocs, parameter.Name) {
t.Fatalf("%s interface documentation is missing provider parameter %q", filepath.Base(path), parameter.Name)
}
}
}
}
}
func TestOfficialAtlasCloudChatProfile(t *testing.T) {
data, err := os.ReadFile(filepath.Join("..", "..", "..", "plugin-packages", "atlascloud-chat.yingce-plugin"))
if err != nil {
t.Fatal(err)
}
pkg, err := ParsePluginPackage(data)
if err != nil {
t.Fatal(err)
}
provider := pkg.Manifest.Contributes.Providers[0]
if provider.ID != "atlascloud-chat" || provider.BaseURL != "https://api.atlascloud.ai" || provider.Auth.Type != "bearer" {
t.Fatalf("Atlas Cloud provider metadata = %#v", provider)
}
adapter := officialPackageAdapter(t, "atlascloud-chat.yingce-plugin", "atlascloud-chat")
spec, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Model: "openai/gpt-5.6-luna",
Messages: []Message{{Role: "user", Content: "hello"}},
}})
if err != nil {
t.Fatal(err)
}
body := manifestTestBody(t, spec)
if spec.Method != "POST" || spec.Path != "/v1/chat/completions" || body["model"] != "openai/gpt-5.6-luna" {
t.Fatalf("Atlas Cloud request = %#v, body = %#v", spec, body)
}
result, err := adapter.ParseCreate(context.Background(), []byte(`{"choices":[{"message":{"content":"atlas ok"}}],"usage":{"total_tokens":3}}`))
if err != nil {
t.Fatal(err)
}
if result.Status != StatusSucceeded || result.Result == nil || result.Result.Text != "atlas ok" {
t.Fatalf("Atlas Cloud response = %#v", result)
}
}
func assertManifestContractMatchesPackage(t *testing.T, packageName string, manifestRaw []byte, interfaceDocs string) {
t.Helper()
start := strings.Index(interfaceDocs, manifestContractStart)
end := strings.Index(interfaceDocs, manifestContractEnd)
if start < 0 || end < start {
t.Fatalf("%s interface documentation has no complete manifest contract", packageName)
}
contractSection := interfaceDocs[start:end]
jsonStart := strings.Index(contractSection, "```json\n")
if jsonStart < 0 {
t.Fatalf("%s manifest contract has no JSON block", packageName)
}
jsonStart += len("```json\n")
jsonEnd := strings.Index(contractSection[jsonStart:], "\n```")
if jsonEnd < 0 {
t.Fatalf("%s manifest contract JSON block is not terminated", packageName)
}
var actual map[string]any
if err := json.Unmarshal(manifestRaw, &actual); err != nil {
t.Fatalf("decode %s manifest: %v", packageName, err)
}
actual["documentation"] = manifestDocumentationPlaceholder
var documented map[string]any
if err := json.Unmarshal([]byte(contractSection[jsonStart:jsonStart+jsonEnd]), &documented); err != nil {
t.Fatalf("decode %s documented manifest contract: %v", packageName, err)
}
if !reflect.DeepEqual(documented, actual) {
t.Fatalf("%s documented manifest contract does not exactly match manifest.json", packageName)
}
}
func TestOfficialAgentProfilesMapToolRequestsAndResponses(t *testing.T) {
requests := map[string]any{
"chatCompletion": map[string]any{"messages": []any{map[string]any{"role": "user", "content": "inspect"}}, "tools": []any{map[string]any{"type": "function"}}, "tool_choice": "required", "marker": "chat"},
"responses": map[string]any{"input": []any{map[string]any{"role": "user", "content": "inspect"}}, "tools": []any{map[string]any{"type": "function"}}, "tool_choice": "required", "marker": "responses"},
"claude": map[string]any{"messages": []any{map[string]any{"role": "user", "content": "inspect"}}, "tools": []any{map[string]any{"name": "canvas_get_state"}}, "tool_choice": map[string]any{"type": "any"}, "max_tokens": 512, "marker": "claude"},
"gemini": map[string]any{"contents": []any{map[string]any{"role": "user", "parts": []any{map[string]any{"text": "inspect"}}}}, "tools": []any{map[string]any{"functionDeclarations": []any{}}}, "toolConfig": map[string]any{"functionCallingConfig": map[string]any{"mode": "ANY"}}, "marker": "gemini"},
}
tests := []struct {
name, packageName, providerID, marker, wantPath, response, wantID, wantSignature string
}{
{
name: "openai-chat", packageName: "openai-chat-completions.yingce-plugin", providerID: "chat-completion", marker: "chat", wantPath: "/chat/completions", wantID: "call-chat",
response: `{"choices":[{"message":{"content":"chat answer","tool_calls":[{"id":"call-chat","function":{"name":"canvas_get_state","arguments":"{\"scope\":\"all\"}"}}]}}]}`,
},
{
name: "openai-responses", packageName: "openai-responses.yingce-plugin", providerID: "openai-response", marker: "responses", wantPath: "/responses", wantID: "call-responses",
response: `{"output_text":"responses answer","output":[{"type":"message"},{"type":"function_call","call_id":"call-responses","name":"canvas_get_state","arguments":"{\"scope\":\"all\"}"}]}`,
},
{
name: "anthropic", packageName: "anthropic-messages.yingce-plugin", providerID: "claude-api", marker: "claude", wantPath: "/v1/messages", wantID: "call-claude",
response: `{"content":[{"type":"text","text":"claude answer"},{"type":"tool_use","id":"call-claude","name":"canvas_get_state","input":{"scope":"all"}}]}`,
},
{
name: "gemini", packageName: "google-gemini-generate-content.yingce-plugin", providerID: "gemini-generate-content", marker: "gemini", wantPath: "/v1beta/models/gemini-test:generateContent", wantSignature: "signature-1",
response: `{"candidates":[{"content":{"parts":[{"text":"gemini answer"},{"functionCall":{"name":"canvas_get_state","args":{"scope":"all"}},"thoughtSignature":"signature-1"}]}}]}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
adapter := officialPackageAdapter(t, test.packageName, test.providerID)
capability, ok := adapter.(AgentCapability)
if !ok || !capability.AgentAvailable() {
t.Fatalf("%s does not expose its declared agent operation", test.providerID)
}
agent, ok := adapter.(AgentAdapter)
if !ok {
t.Fatalf("%s does not implement AgentAdapter", test.providerID)
}
spec, err := agent.BuildAgent(context.Background(), AgentRequestContext{Model: "gemini-test", Request: requests})
if err != nil {
t.Fatal(err)
}
if spec.Path != test.wantPath {
t.Fatalf("request path = %q, want %q", spec.Path, test.wantPath)
}
body := manifestTestBody(t, spec)
if body["marker"] != test.marker {
t.Fatalf("request selected wrong protocol body: %#v", body)
}
if test.providerID != "gemini-generate-content" && body["model"] != "gemini-test" {
t.Fatalf("request model = %#v", body["model"])
}
result, err := agent.ParseAgent(context.Background(), []byte(test.response))
if err != nil {
t.Fatal(err)
}
if len(result.ToolCalls) != 1 || result.ToolCalls[0].Name != "canvas_get_state" || result.ToolCalls[0].Arguments != `{"scope":"all"}` {
t.Fatalf("agent result = %#v", result)
}
if test.wantID != "" && result.ToolCalls[0].ID != test.wantID {
t.Fatalf("tool call id = %q, want %q", result.ToolCalls[0].ID, test.wantID)
}
if test.wantID == "" && !strings.HasPrefix(result.ToolCalls[0].ID, "call_") {
t.Fatalf("provider omitted tool call id and no stable id was synthesized: %#v", result.ToolCalls[0])
}
if result.ToolCalls[0].ThoughtSignature != test.wantSignature {
t.Fatalf("thought signature = %q, want %q", result.ToolCalls[0].ThoughtSignature, test.wantSignature)
}
})
}
}
func TestOfficialVideoProfilesPreserveExplicitMediaRoles(t *testing.T) {
tests := []struct {
packageName string
providerID string
model string
resolution string
assert func(t *testing.T, spec RequestSpec)
}{
{
packageName: "minimax-hailuo-video-v2.yingce-plugin", providerID: "minimax-video", model: "MiniMax-H3", resolution: "1080P",
assert: func(t *testing.T, spec RequestSpec) {
body := manifestTestBody(t, spec)
content, _ := body["content"].([]any)
if len(content) != 3 {
t.Fatalf("MiniMax content = %#v", content)
}
first, _ := content[1].(map[string]any)
last, _ := content[2].(map[string]any)
if first["role"] != "first_frame" || last["role"] != "last_frame" || body["ratio"] != "adaptive" || body["resolution"] != "1080P" {
t.Fatalf("MiniMax role payload = %#v", body)
}
},
},
{
packageName: "newapi-media-task-v1.yingce-plugin", providerID: "newapi-channel-1", model: "minimax-h3", resolution: "720P",
assert: func(t *testing.T, spec RequestSpec) {
body := manifestTestBody(t, spec)
input, _ := body["input"].(map[string]any)
media, _ := input["media"].([]any)
first, _ := media[0].(map[string]any)
last, _ := media[1].(map[string]any)
if first["type"] != "first_frame" || last["type"] != "last_frame" {
t.Fatalf("NewAPI media = %#v", media)
}
},
},
{
packageName: "agnes-video-25.yingce-plugin", providerID: "agnes-video", model: "agnes-video-2.5", resolution: "720P",
assert: func(t *testing.T, spec RequestSpec) {
body := manifestTestBody(t, spec)
if body["mode"] != "keyframe" || body["first_frame"] != "https://cdn.example/first.png" || body["last_frame"] != "https://cdn.example/last.png" {
t.Fatalf("Agnes keyframe payload = %#v", body)
}
},
},
{
packageName: "dashscope-wan-video.yingce-plugin", providerID: "dashscope-wan-video", model: "wan2.2-kf2v-flash", resolution: "720P",
assert: func(t *testing.T, spec RequestSpec) {
body := manifestTestBody(t, spec)
input, _ := body["input"].(map[string]any)
if input["first_frame_url"] != "https://cdn.example/first.png" || input["last_frame_url"] != "https://cdn.example/last.png" {
t.Fatalf("Wan keyframe input = %#v", input)
}
if _, duplicated := input["img_url"]; duplicated {
t.Fatalf("Wan keyframe input must not also send img_url: %#v", input)
}
},
},
}
for _, test := range tests {
t.Run(test.providerID, func(t *testing.T) {
adapter := officialPackageAdapter(t, test.packageName, test.providerID)
spec, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Model: test.model, Prompt: "镜头推进", Duration: 6, AspectRatio: "16:9", Resolution: test.resolution,
Images: []MediaReference{
{URL: "https://cdn.example/last.png", Role: "last_frame", Order: 2},
{URL: "https://cdn.example/first.png", Role: "first_frame", Order: 1},
},
}})
if err != nil {
t.Fatal(err)
}
test.assert(t, spec)
})
}
}
func TestOfficialWanProfilesUseMutuallyExclusiveReferenceFields(t *testing.T) {
video := officialPackageAdapter(t, "dashscope-wan-video.yingce-plugin", "dashscope-wan-video")
videoSpec, err := video.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Model: "wan2.6-r2v", Prompt: "保持角色一致", Operation: "reference_to_video",
Images: []MediaReference{
{URL: "https://cdn.example/second.png", Role: "subject_reference", Order: 2},
{URL: "https://cdn.example/first.png", Role: "reference_image", Order: 1},
},
}})
if err != nil {
t.Fatal(err)
}
videoBody := manifestTestBody(t, videoSpec)
videoInput, _ := videoBody["input"].(map[string]any)
if _, duplicated := videoInput["img_url"]; duplicated {
t.Fatalf("Wan reference mode must not also send img_url: %#v", videoInput)
}
references, _ := videoInput["reference_images"].([]any)
if len(references) != 2 || references[0] != "https://cdn.example/first.png" || references[1] != "https://cdn.example/second.png" {
t.Fatalf("Wan reference_images = %#v", references)
}
image := officialPackageAdapter(t, "dashscope-wanx-image.yingce-plugin", "dashscope-wanx-image")
for _, test := range []struct {
name string
images []MediaReference
single bool
}{
{name: "single", images: []MediaReference{{URL: "https://cdn.example/one.png", Role: "reference_image", Order: 1}}, single: true},
{name: "multiple", images: []MediaReference{{URL: "https://cdn.example/two.png", Role: "reference_image", Order: 2}, {URL: "https://cdn.example/one.png", Role: "reference_image", Order: 1}}},
} {
t.Run("wanx-"+test.name, func(t *testing.T) {
spec, err := image.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{Model: "wanx2.1-t2i-turbo", Prompt: "角色设定", Images: test.images}})
if err != nil {
t.Fatal(err)
}
body := manifestTestBody(t, spec)
input, _ := body["input"].(map[string]any)
_, hasSingle := input["ref_img"]
_, hasMultiple := input["ref_images"]
if hasSingle != test.single || hasMultiple == test.single {
t.Fatalf("Wanx reference fields are not mutually exclusive: %#v", input)
}
})
}
}
func TestOfficialRollDekWanVideoUsesVideosLifecycle(t *testing.T) {
adapter := officialPackageAdapter(t, "rolldek-wan-video.yingce-plugin", "rolldek-wan-video")
request := GenerationRequest{
Model: "wan3.0-video-prime-1080p", Prompt: "保持角色一致", Duration: 11, AspectRatio: "16:9", Resolution: "1080p",
Images: []MediaReference{{URL: "https://cdn.example/character.png", Role: "reference_image"}},
Videos: []MediaReference{{URL: "https://cdn.example/motion.mp4", Role: "reference_video", Metadata: map[string]any{"durationMs": 5250}}},
Audios: []MediaReference{{URL: "https://cdn.example/voice.mp3", Role: "reference_audio"}},
}
create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: request})
if err != nil {
t.Fatal(err)
}
if create.Method != "POST" || create.Path != "/v1/videos" || create.ContentType != "application/json" {
t.Fatalf("RollDek create = %#v", create)
}
body := manifestTestBody(t, create)
if body["model"] != request.Model || body["seconds"] != "11" || body["size"] != "1080P" || body["aspect_ratio"] != "16:9" {
t.Fatalf("RollDek create body = %#v", body)
}
images, _ := body["reference_images"].([]any)
image, _ := images[0].(map[string]any)
videos, _ := body["reference_videos"].([]any)
video, _ := videos[0].(map[string]any)
audios, _ := body["reference_audios"].([]any)
audio, _ := audios[0].(map[string]any)
if image["url"] != request.Images[0].URL || image["role"] != "reference_image" || video["url"] != request.Videos[0].URL || video["duration"] != 5.25 || audio["url"] != request.Audios[0].URL {
t.Fatalf("RollDek references = images:%#v videos:%#v audios:%#v", images, videos, audios)
}
created, err := adapter.ParseCreate(context.Background(), []byte(`{"id":"task-roll-1","task_id":"task-roll-1","status":"queued"}`))
if err != nil {
t.Fatal(err)
}
if created.TaskID != "task-roll-1" || created.Status != StatusPending {
t.Fatalf("RollDek create result = %#v", created)
}
poll, err := adapter.BuildPoll(context.Background(), PollContext{Request: request, TaskID: created.TaskID})
if err != nil {
t.Fatal(err)
}
if poll.Method != "GET" || poll.Path != "/v1/videos/task-roll-1" {
t.Fatalf("RollDek poll = %#v", poll)
}
state, err := adapter.ParsePoll(context.Background(), PollContext{Request: request, TaskID: created.TaskID}, []byte(`{"id":"task-roll-1","status":"completed","metadata":{"url":"https://cdn.example/result.mp4"}}`))
if err != nil {
t.Fatal(err)
}
if state.Status != StatusSucceeded || state.Result == nil || len(state.Result.Videos) != 1 || state.Result.Videos[0].URL != "https://cdn.example/result.mp4" {
t.Fatalf("RollDek poll result = %#v", state)
}
resultAdapter, ok := adapter.(ResultAdapter)
if !ok {
t.Fatal("RollDek adapter does not expose result download")
}
result, err := resultAdapter.BuildResult(context.Background(), PollContext{Request: request, TaskID: created.TaskID})
if err != nil {
t.Fatal(err)
}
if result.Method != "GET" || result.Path != "/v1/videos/task-roll-1/content" {
t.Fatalf("RollDek result = %#v", result)
}
}
func TestNewAPIVideoGenerationsParsesNestedTaskIDs(t *testing.T) {
adapter := officialPackageAdapter(t, "newapi-video-generations-v1.yingce-plugin", "newapi-channel-2")
tests := []struct {
name string
payload string
wantID string
}{
{name: "snake case", payload: `{"data":{"task_id":"task-snake","status":"queued"}}`, wantID: "task-snake"},
{name: "camel case", payload: `{"data":{"taskId":"task-camel","status":"queued"}}`, wantID: "task-camel"},
{name: "nested upstream id wins over wrapper id", payload: `{"id":"49137","data":{"task_id":"task-upstream","status":"queued"}}`, wantID: "task-upstream"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
created, err := adapter.ParseCreate(context.Background(), []byte(test.payload))
if err != nil {
t.Fatal(err)
}
if created.TaskID != test.wantID || created.Status != StatusPending {
t.Fatalf("created = %#v, want task ID %q and pending status", created, test.wantID)
}
})
}
}
func TestNewAPIVideoGenerationsParsesNestedVideoResults(t *testing.T) {
adapter := officialPackageAdapter(t, "newapi-video-generations-v1.yingce-plugin", "newapi-channel-2")
tests := []struct {
name string
payload string
}{
{name: "channel result URL", payload: `{"code":"success","data":{"task_id":"task-upstream","status":"SUCCESS","result_url":"https://cdn.example/channel-result.mp4"}}`},
{name: "provider nested video URL", payload: `{"code":"success","data":{"task_id":"task-upstream","status":"SUCCESS","data":{"status":"completed","video_url":"https://cdn.example/provider-result.mp4"}}}`},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
state, err := adapter.ParsePoll(context.Background(), PollContext{TaskID: "task-upstream"}, []byte(test.payload))
if err != nil {
t.Fatal(err)
}
if state.Status != StatusSucceeded || state.Result == nil || len(state.Result.Videos) != 1 {
t.Fatalf("state = %#v, want one completed video", state)
}
if state.Result.Videos[0].URL == "" {
t.Fatalf("video = %#v, want a result URL", state.Result.Videos[0])
}
})
}
}
func TestOfficialOpenAIVideosDeclaresAuthenticatedResultDownload(t *testing.T) {
adapter := officialPackageAdapter(t, "openai-videos.yingce-plugin", "newapi")
capability, ok := adapter.(ResultCapability)
if !ok || !capability.ResultAvailable() {
t.Fatal("OpenAI Videos result operation is unavailable")
}
resultAdapter, ok := adapter.(ResultAdapter)
if !ok {
t.Fatal("OpenAI Videos does not implement ResultAdapter")
}
spec, err := resultAdapter.BuildResult(context.Background(), PollContext{Model: "sora-2", TaskID: "video-1"})
if err != nil {
t.Fatal(err)
}
if spec.Method != "GET" || spec.Path != "/v1/videos/video-1/content" || spec.Headers["Accept"] != "video/mp4" || spec.Auth.Type != "bearer" {
t.Fatalf("result request = %#v", spec)
}
}
// 系统指令必须真正到达上游:只映射 messages 的协议要收到 system 消息,
// 有独立 system 字段的协议要用该字段且不能在消息数组里重复发送。
func TestOfficialTextProtocolsDeliverInstructions(t *testing.T) {
tests := []struct {
name, packageName, providerID, messagesField, instructionField string
}{
{name: "openai-chat", packageName: "openai-chat-completions.yingce-plugin", providerID: "chat-completion", messagesField: "messages"},
{name: "deepseek", packageName: "deepseek-chat.yingce-plugin", providerID: "deepseek-chat", messagesField: "messages"},
{name: "atlascloud", packageName: "atlascloud-chat.yingce-plugin", providerID: "atlascloud-chat", messagesField: "messages"},
{name: "openai-responses", packageName: "openai-responses.yingce-plugin", providerID: "openai-response", messagesField: "input", instructionField: "instructions"},
{name: "anthropic", packageName: "anthropic-messages.yingce-plugin", providerID: "claude-api", messagesField: "messages", instructionField: "system"},
{name: "gemini", packageName: "google-gemini-generate-content.yingce-plugin", providerID: "gemini-generate-content", messagesField: "contents", instructionField: "systemInstruction"},
}
const instructions = "只输出一个 JSON 对象"
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
adapter := officialPackageAdapter(t, test.packageName, test.providerID)
spec, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Capability: CapabilityText, Model: "model-test", Prompt: "一句话故事", Instructions: instructions,
}})
if err != nil {
t.Fatal(err)
}
body := manifestTestBody(t, spec)
encodedMessages, err := json.Marshal(body[test.messagesField])
if err != nil {
t.Fatal(err)
}
carriesInstructions := strings.Contains(string(encodedMessages), instructions)
if test.instructionField == "" {
if !carriesInstructions {
t.Fatalf("%s dropped the system instruction: %s", test.providerID, encodedMessages)
}
return
}
if carriesInstructions {
t.Fatalf("%s duplicated the system instruction into %s: %s", test.providerID, test.messagesField, encodedMessages)
}
encodedInstruction, err := json.Marshal(body[test.instructionField])
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(encodedInstruction), instructions) {
t.Fatalf("%s did not map the system instruction to %s: %s", test.providerID, test.instructionField, encodedInstruction)
}
})
}
}
func officialPackageAdapter(t *testing.T, packageName, providerID string) Adapter {
t.Helper()
data, err := os.ReadFile(filepath.Join("..", "..", "..", "plugin-packages", packageName))
if err != nil {
t.Fatal(err)
}
pkg, err := ParsePluginPackage(data)
if err != nil {
t.Fatal(err)
}
adapters, err := LoadInstalledProviders(pkg.ManifestRaw, nil)
if err != nil {
t.Fatal(err)
}
for _, adapter := range adapters {
if adapter.Metadata().ID == providerID {
return adapter
}
}
t.Fatalf("provider %s is missing from %s", providerID, packageName)
return nil
}
func manifestTestBody(t *testing.T, spec RequestSpec) map[string]any {
t.Helper()
data, err := json.Marshal(spec.Body)
if err != nil {
t.Fatal(err)
}
var body map[string]any
if err := json.Unmarshal(data, &body); err != nil {
t.Fatal(err)
}
return body
}
func TestOfficialArkSeedreamMapsAspectRatioToPixelSize(t *testing.T) {
adapter := officialPackageAdapter(t, "volcengine-ark-seedream.yingce-plugin", "volcengine-ark-image")
tests := []struct {
name, aspectRatio, wantSize string
}{
{name: "ratio maps to 2K pixels", aspectRatio: "1:1", wantSize: "2048x2048"},
{name: "landscape ratio", aspectRatio: "16:9", wantSize: "2560x1440"},
{name: "auto maps to 2k tier", aspectRatio: "auto", wantSize: "2k"},
{name: "pixel size passes through", aspectRatio: "1920x1080", wantSize: "1920x1080"},
{name: "tier passes through", aspectRatio: "4k", wantSize: "4k"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{Model: "doubao-seedream-5-0-260128", Prompt: "circle", AspectRatio: tt.aspectRatio}})
if err != nil {
t.Fatal(err)
}
if create.Path != "/api/v3/images/generations" {
t.Fatalf("Seedream create = %#v", create)
}
if body := manifestTestBody(t, create); body["size"] != tt.wantSize {
t.Fatalf("Seedream size = %#v, want %q", body["size"], tt.wantSize)
}
})
}
create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{Model: "doubao-seedream-5-0-260128", Prompt: "circle"}})
if err != nil {
t.Fatal(err)
}
if _, ok := manifestTestBody(t, create)["size"]; ok {
t.Fatalf("Seedream must omit size when no ratio is requested")
}
}
func TestOfficialGeminiImageMapsQualityToImageSize(t *testing.T) {
adapter := officialPackageAdapter(t, "google-gemini-image.yingce-plugin", "gemini-image")
tests := []struct {
name, quality, wantSize string
wantOmitted bool
}{
{name: "4k becomes 4K", quality: "4k", wantSize: "4K"},
{name: "high becomes 4K", quality: "high", wantSize: "4K"},
{name: "2k becomes 2K", quality: "2k", wantSize: "2K"},
{name: "1k becomes 1K", quality: "1k", wantSize: "1K"},
{name: "video 720 omitted", quality: "720", wantOmitted: true},
{name: "auto omitted", quality: "auto", wantOmitted: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Model: "gemini-3-pro-image-preview", Prompt: "landscape", AspectRatio: "16:9", Quality: tt.quality,
}})
if err != nil {
t.Fatal(err)
}
if create.Path != "/v1beta/models/gemini-3-pro-image-preview:generateContent" {
t.Fatalf("create path = %q", create.Path)
}
body := manifestTestBody(t, create)
generationConfig, _ := body["generationConfig"].(map[string]any)
if _, ok := generationConfig["candidateCount"]; ok {
t.Fatalf("candidateCount must not be mapped from imageCount, got %#v", generationConfig["candidateCount"])
}
imageConfig, _ := generationConfig["imageConfig"].(map[string]any)
if imageConfig["aspectRatio"] != "16:9" {
t.Fatalf("aspectRatio = %#v", imageConfig["aspectRatio"])
}
if tt.wantOmitted {
if imageConfig["imageSize"] != nil {
t.Fatalf("imageSize should be omitted, got %#v", imageConfig["imageSize"])
}
return
}
if imageConfig["imageSize"] != tt.wantSize {
t.Fatalf("imageSize = %#v, want %q", imageConfig["imageSize"], tt.wantSize)
}
})
}
}
func TestOfficialGeminiImagePrefersQualityOverVideoResolution(t *testing.T) {
adapter := officialPackageAdapter(t, "google-gemini-image.yingce-plugin", "gemini-image")
tests := []struct {
name, quality, resolution, wantSize string
wantOmitted bool
}{
{name: "canvas 4k keeps imageSize when vquality is 720", quality: "4k", resolution: "720", wantSize: "4K"},
{name: "empty quality still maps resolution 4k", quality: "", resolution: "4k", wantSize: "4K"},
{name: "video 720 alone is omitted", quality: "", resolution: "720", wantOmitted: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Model: "gemini-3-pro-image-preview", Prompt: "landscape", AspectRatio: "16:9", Quality: tt.quality, Resolution: tt.resolution,
}})
if err != nil {
t.Fatal(err)
}
body := manifestTestBody(t, create)
generationConfig, _ := body["generationConfig"].(map[string]any)
imageConfig, _ := generationConfig["imageConfig"].(map[string]any)
if tt.wantOmitted {
if imageConfig["imageSize"] != nil {
t.Fatalf("imageSize should be omitted, got %#v", imageConfig["imageSize"])
}
return
}
if imageConfig["imageSize"] != tt.wantSize {
t.Fatalf("imageSize = %#v, want %q", imageConfig["imageSize"], tt.wantSize)
}
})
}
}
func TestOfficialGrokImageMapsAspectAndResolution(t *testing.T) {
adapter := officialPackageAdapter(t, "xai-grok-images.yingce-plugin", "grok-image")
create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Model: "grok-imagine-image", Prompt: "a cat", AspectRatio: "1280x720", Quality: "high",
}})
if err != nil {
t.Fatal(err)
}
if create.Path != "/v1/images/generations" {
t.Fatalf("path = %q", create.Path)
}
body := manifestTestBody(t, create)
if body["aspect_ratio"] != "16:9" || body["resolution"] != "2k" {
t.Fatalf("body = %#v", body)
}
if _, ok := body["size"]; ok {
t.Fatalf("size must be omitted: %#v", body)
}
}
func TestOfficialJimengImageSplitsPixelSize(t *testing.T) {
adapter := officialPackageAdapter(t, "volcengine-jimeng-image.yingce-plugin", "volcengine-jimeng-image")
create, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Model: "jimeng_t2i_v40", Prompt: "still", AspectRatio: "1024x768",
Images: []MediaReference{{DataURL: "data:image/png;base64,aGVsbG8="}},
}})
if err != nil {
t.Fatal(err)
}
body := manifestTestBody(t, create)
if body["width"] != float64(1024) || body["height"] != float64(768) {
t.Fatalf("dimensions = %#v %#v", body["width"], body["height"])
}
binary, ok := body["binary_data_base64"].([]any)
if !ok || len(binary) != 1 || binary[0] != "aGVsbG8=" {
t.Fatalf("binary_data_base64 = %#v", body["binary_data_base64"])
}
}
func TestOfficialOpenAIAudioUsesBinaryPayload(t *testing.T) {
adapter := officialPackageAdapter(t, "openai-audio.yingce-plugin", "openai-audio")
result, err := adapter.ParseCreate(context.Background(), []byte("ID3fake-mp3"))
if err != nil {
t.Fatal(err)
}
if result.Status != StatusSucceeded || result.Result == nil || len(result.Result.Audios) != 1 {
t.Fatalf("result = %#v", result)
}
}
func TestOfficialOpenAIAudioSpeedDefaultsInvalidAndZeroValues(t *testing.T) {
adapter := officialPackageAdapter(t, "openai-audio.yingce-plugin", "openai-audio")
for _, test := range []struct {
name string
value any
want float64
}{
{name: "invalid", value: "not-a-number", want: 1},
{name: "zero", value: "0", want: 1},
{name: "valid", value: "1.25", want: 1.25},
} {
t.Run(test.name, func(t *testing.T) {
spec, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Model: "gpt-4o-mini-tts", Prompt: "hello", Extra: map[string]any{"audioSpeed": test.value},
}})
if err != nil {
t.Fatal(err)
}
body := manifestTestBody(t, spec)
if got := body["speed"]; !reflect.DeepEqual(got, test.want) {
t.Fatalf("speed = %#v, want %v", got, test.want)
}
})
}
}