Ctrl K
ComunidadGuías…

plugins/alibaba_responses_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package plugins_test

import (
	"bytes"
	"fmt"
	"io"
	"maps"
	"net/http"
	"net/http/httptest"
	"slices"
	"strings"
	"testing"

	"github.com/QuantumNous/new-api/common"
	"github.com/QuantumNous/new-api/constant"
	"github.com/QuantumNous/new-api/model"
	"github.com/QuantumNous/new-api/pkg/billingexpr"
	"github.com/QuantumNous/new-api/pkg/jsplugin"
	builtinplugins "github.com/QuantumNous/new-api/plugins"
	"github.com/QuantumNous/new-api/relay"
	relaychannel "github.com/QuantumNous/new-api/relay/channel"
	taskplugin "github.com/QuantumNous/new-api/relay/channel/task/jsplugin"
	relaycommon "github.com/QuantumNous/new-api/relay/common"
	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestAlibabaResponsesProtocol(t *testing.T) {
	source, err := builtinplugins.Source("alibaba")
	require.NoError(t, err)
	registry := jsplugin.NewRegistry()
	plugin, err := registry.RegisterFactory(source, jsplugin.Options{Key: "alibaba"})
	require.NoError(t, err)

	t.Run("claims every Ali model", func(t *testing.T) {
		for _, model := range plugin.Meta.Models {
			binding, found := registry.Generation().LookupEndpoint("POST", "/v1/responses", model)
			require.True(t, found, model)
			assert.Same(t, plugin, binding.Plugin)
			assert.Equal(t, "openai_responses", binding.Protocol)
		}
	})

	t.Run("selects image and video usage profiles for every declared model", func(t *testing.T) {
		require.Len(t, plugin.Meta.UsageProfiles, 9)
		covered := make(map[string]bool)
		for _, profile := range plugin.Meta.UsageProfiles {
			for _, name := range profile.Models {
				require.False(t, covered[name], name)
				covered[name] = true
			}
		}
		for _, name := range plugin.Meta.Models {
			require.True(t, covered[name], name)
			schema, examples := plugin.Meta.UsageForModel(name)
			assert.Empty(t, examples)
			isImage := strings.Contains(name, "-image") || strings.Contains(name, "-t2i") || strings.Contains(name, "-i2i")
			switch {
			case strings.HasPrefix(name, "qwen-image-3.0"):
				assert.ElementsMatch(t, []string{"image_count", "output_image_type", "input_image_count"}, keysOf(schema), name)
				assert.Equal(t, plugin.Meta.UsageSchema["image_count"], schema["image_count"], name)
				assert.Equal(t, []string{"qima_output_1k", "qima_output_2k"}, schema["output_image_type"].Enum, name)
			case name == "z-image-turbo":
				assert.ElementsMatch(t, []string{"image_count", "prompt_extend"}, keysOf(schema), name)
				assert.Equal(t, "boolean", schema["prompt_extend"].Type, name)
			case isImage:
				assert.Equal(t, map[string]jsplugin.UsageFieldSchema{"image_count": plugin.Meta.UsageSchema["image_count"]}, schema, name)
			case name == "wan2.6-i2v-flash":
				assert.ElementsMatch(t, []string{"seconds", "resolution", "audio"}, keysOf(schema), name)
				assert.Equal(t, plugin.Meta.UsageSchema["seconds"], schema["seconds"], name)
				assert.Equal(t, []string{"720P", "1080P"}, schema["resolution"].Enum, name)
				assert.Equal(t, "boolean", schema["audio"].Type, name)
			default:
				assert.ElementsMatch(t, []string{"seconds", "resolution"}, keysOf(schema), name)
				assert.Equal(t, plugin.Meta.UsageSchema["seconds"], schema["seconds"], name)
				assert.Subset(t, plugin.Meta.UsageSchema["resolution"].Enum, schema["resolution"].Enum, name)
			}
			for key, field := range schema {
				assert.NotEmpty(t, field.Description["en"], name+" "+key)
				assert.NotEmpty(t, field.Description["zh"], name+" "+key)
			}
		}
	})

	t.Run("declares documented usage facts", func(t *testing.T) {
		for _, key := range []string{"seconds", "resolution", "image_count"} {
			schema, exists := plugin.Meta.UsageSchema[key]
			require.True(t, exists, key)
			assert.NotEmpty(t, schema.Description, key)
		}

		value, callErr := plugin.Engine.Call(t.Context(), "extractUsage", map[string]any{
			"model":         "wan2.5-i2v-preview",
			"upstreamModel": "wan2.5-i2v-preview",
			"usagePurpose":  "facts",
			"requestBody": map[string]any{
				"model":    "wan2.5-i2v-preview",
				"duration": 10,
				"size":     "1080p",
				"image":    "https://cdn.example/first.png",
			},
		})
		require.NoError(t, callErr)
		encoded, marshalErr := common.Marshal(value)
		require.NoError(t, marshalErr)
		var facts map[string]any
		require.NoError(t, common.Unmarshal(encoded, &facts))
		assert.Equal(t, map[string]any{"seconds": float64(10), "resolution": "1080P"}, facts)
	})

	t.Run("parses text input and options", func(t *testing.T) {
		value, callErr := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_responses", "decodeRequest"}, map[string]any{
			"model": "wan2.7-t2v", "body": map[string]any{"kind": "json", "value": map[string]any{
				"model":    "wan2.7-t2v",
				"input":    "waves at sunset",
				"size":     "1280*720",
				"duration": 6,
				"metadata": map[string]any{"parameters": map[string]any{"watermark": true}},
			}},
			"stream": false,
		})
		require.NoError(t, callErr)
		encoded, marshalErr := common.Marshal(value)
		require.NoError(t, marshalErr)
		var resolved map[string]any
		require.NoError(t, common.Unmarshal(encoded, &resolved))

		assert.Equal(t, map[string]any{
			"kind":   "submit",
			"model":  "wan2.7-t2v",
			"action": "text_to_video",
			"requestBody": map[string]any{
				"model":    "wan2.7-t2v",
				"prompt":   "waves at sunset",
				"size":     "1280*720",
				"duration": float64(6),
				"metadata": map[string]any{"parameters": map[string]any{"watermark": true}},
			},
		}, resolved)
	})

	t.Run("parses multimodal image input", func(t *testing.T) {
		value, callErr := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_responses", "decodeRequest"}, map[string]any{
			"model": "wan2.7-i2v", "body": map[string]any{"kind": "json", "value": map[string]any{
				"model": "wan2.7-i2v",
				"input": []any{
					map[string]any{
						"role": "user",
						"content": []any{
							map[string]any{"type": "input_text", "text": "animate between frames"},
							map[string]any{"type": "input_image", "image_url": "https://cdn.example/first.png"},
							map[string]any{"type": "input_image", "image_url": map[string]any{"url": "https://cdn.example/last.png"}},
						},
					},
				},
			}},
			"stream": true,
		})
		require.NoError(t, callErr)
		encoded, marshalErr := common.Marshal(value)
		require.NoError(t, marshalErr)
		var resolved map[string]any
		require.NoError(t, common.Unmarshal(encoded, &resolved))

		assert.Equal(t, "wan2.7-i2v", resolved["model"])
		assert.Equal(t, "image_to_video", resolved["action"])
		requestBody, ok := resolved["requestBody"].(map[string]any)
		require.True(t, ok)
		assert.Equal(t, "animate between frames", requestBody["prompt"])
		assert.Equal(t, []any{"https://cdn.example/first.png", "https://cdn.example/last.png"}, requestBody["images"])
	})

	t.Run("accepts image-only i2v input", func(t *testing.T) {
		value, callErr := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_responses", "decodeRequest"}, map[string]any{
			"model": "wan2.7-i2v", "body": map[string]any{"kind": "json", "value": map[string]any{
				"model": "wan2.7-i2v",
				"input": []any{
					map[string]any{"type": "input_image", "image_url": "https://cdn.example/first.png"},
				},
			}},
			"stream": false,
		})
		require.NoError(t, callErr)
		encoded, marshalErr := common.Marshal(value)
		require.NoError(t, marshalErr)
		var resolved map[string]any
		require.NoError(t, common.Unmarshal(encoded, &resolved))

		assert.Equal(t, "image_to_video", resolved["action"])
		requestBody, ok := resolved["requestBody"].(map[string]any)
		require.True(t, ok)
		assert.Equal(t, "", requestBody["prompt"])
		assert.Equal(t, []any{"https://cdn.example/first.png"}, requestBody["images"])
	})

	t.Run("rejects a request without input text", func(t *testing.T) {
		_, callErr := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_responses", "decodeRequest"}, map[string]any{
			"model": "wan2.7-t2v", "body": map[string]any{"kind": "json", "value": map[string]any{"model": "wan2.7-t2v"}},
			"stream": false,
		})
		require.ErrorContains(t, callErr, "input is required")
	})

	protocolContext := map[string]any{
		"requestBody": map[string]any{"model": "wan2.7-t2v"},
		"stream":      true,
		"artifacts": map[string]any{
			"video": map[string]any{
				"key":      "video",
				"type":     "video",
				"mimeType": "video/mp4",
				"url":      "https://gateway.example/v1/tasks/task_public/artifacts/video/content?access=host%2Bcapability%3D",
			},
		},
	}
	successTask := map[string]any{
		"task_id":    "task_public",
		"status":     "SUCCESS",
		"progress":   "100%",
		"created_at": 10,
		"updated_at": 20,
		"data": map[string]any{
			"output": map[string]any{
				"video_url": "https://upstream.example/video.mp4?Expires=1&Signature=must-not-leak",
			},
		},
	}

	t.Run("renders stream semantics accepted by the host", func(t *testing.T) {
		progressValue, callErr := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_responses", "renderEvents"}, protocolContext, map[string]any{
			"task_id":  "task_public",
			"status":   "IN_PROGRESS",
			"progress": "42%",
		})
		require.NoError(t, callErr)
		progressResult, decodeErr := relay.DecodePluginProtocolEventResult(progressValue, relay.DefaultPluginProtocolLimits())
		require.NoError(t, decodeErr)
		require.Len(t, progressResult.Events, 1)
		require.NotNil(t, progressResult.Events[0].Progress)
		assert.Equal(t, float64(42), *progressResult.Events[0].Progress)
		assert.False(t, progressResult.Done)

		value, callErr := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_responses", "renderEvents"}, protocolContext, successTask)
		require.NoError(t, callErr)
		result, decodeErr := relay.DecodePluginProtocolEventResult(value, relay.DefaultPluginProtocolLimits())
		require.NoError(t, decodeErr)
		require.Len(t, result.Events, 1)
		assert.Equal(t, "output", result.Events[0].Type)
		assert.True(t, result.Done)
		var text string
		require.NoError(t, common.Unmarshal(result.Events[0].Data, &text))
		assert.Equal(t, `<video controls src="https://gateway.example/v1/tasks/task_public/artifacts/video/content?access=host%2Bcapability%3D"></video>`, text)
		assert.NotContains(t, text, "upstream.example")

		machine := relay.NewPluginResponsesMachine("task_public", "wan2.7-t2v", 10, relay.DefaultPluginProtocolLimits())
		_, machineErr := machine.CreatedEvent()
		require.NoError(t, machineErr)
		wireEvents, machineErr := machine.ApplyTick(result, "SUCCESS")
		require.NoError(t, machineErr)
		require.NotEmpty(t, wireEvents)
		assert.Equal(t, "response.completed", wireEvents[len(wireEvents)-1].Type)
	})

	t.Run("renders a valid non-stream response", func(t *testing.T) {
		value, callErr := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_responses", "renderFinal"}, protocolContext, successTask)
		require.NoError(t, callErr)
		machine := relay.NewPluginResponsesMachine("task_public", "wan2.7-t2v", 10, relay.DefaultPluginProtocolLimits())
		response, finalErr := machine.FinalResponse(value, "SUCCESS")
		require.NoError(t, finalErr)
		assert.Equal(t, "resp_public", response["id"])
		assert.Equal(t, "completed", response["status"])

		output, ok := response["output"].([]any)
		require.True(t, ok)
		require.Len(t, output, 1)
		item, ok := output[0].(map[string]any)
		require.True(t, ok)
		content, ok := item["content"].([]any)
		require.True(t, ok)
		require.Len(t, content, 1)
		part, ok := content[0].(map[string]any)
		require.True(t, ok)
		text, ok := part["text"].(string)
		require.True(t, ok)
		assert.Equal(t, `<video controls src="https://gateway.example/v1/tasks/task_public/artifacts/video/content?access=host%2Bcapability%3D"></video>`, text)
		assert.NotContains(t, text, "upstream.example")
		metadata, ok := response["metadata"].(map[string]string)
		require.True(t, ok)
		assert.Equal(t, "ali", metadata["vendor"])
	})

	t.Run("does not fall back to the upstream URL when the host artifact is absent", func(t *testing.T) {
		_, callErr := plugin.Engine.CallPath(
			t.Context(),
			"protocols",
			[]string{"openai_responses", "renderFinal"},
			map[string]any{
				"requestBody": map[string]any{"model": "wan2.7-t2v"},
				"stream":      false,
			},
			successTask,
		)
		require.ErrorContains(t, callErr, "video artifact is unavailable")
		assert.NotContains(t, callErr.Error(), "upstream.example")
	})
}

func newAlibabaPlugin(t *testing.T) *jsplugin.LoadedPlugin {
	t.Helper()
	source, err := builtinplugins.Source("alibaba")
	require.NoError(t, err)
	plugin, err := jsplugin.NewRegistry().RegisterFactory(source, jsplugin.Options{Key: "alibaba"})
	require.NoError(t, err)
	return plugin
}

func alibabaObject(t *testing.T, value any) map[string]any {
	t.Helper()
	encoded, err := common.Marshal(value)
	require.NoError(t, err)
	var result map[string]any
	require.NoError(t, common.Unmarshal(encoded, &result))
	return result
}

// Exercise the production host's usage validation as well as the
// converter, without sending a paid generation request or needing a database.
func submitAlibabaRequest(t *testing.T, plugin *jsplugin.LoadedPlugin, upstream string, request map[string]any) (map[string]any, map[string]any, string) {
	t.Helper()
	info := &relaycommon.RelayInfo{
		ChannelMeta:     &relaycommon.ChannelMeta{ChannelBaseUrl: "https://dashscope.aliyuncs.com", UpstreamModelName: upstream},
		OriginModelName: request["model"].(string),
		TaskRelayInfo:   &relaycommon.TaskRelayInfo{PublicTaskID: "task_public"},
	}
	adaptor := taskplugin.New(plugin)
	adaptor.Init(info)
	c, _ := gin.CreateTestContext(httptest.NewRecorder())
	c.Request = httptest.NewRequest(http.MethodPost, "/v1/videos", nil)
	c.Set("task_request", request)
	require.Nil(t, adaptor.ValidateRequestAndSetAction(c, info))
	reader, err := adaptor.BuildRequestBody(c, info)
	require.NoError(t, err)
	encoded, err := io.ReadAll(reader)
	require.NoError(t, err)
	var body map[string]any
	require.NoError(t, common.Unmarshal(encoded, &body))
	facts, err := adaptor.ExtractUsageFactsValidated(c, info)
	require.NoError(t, err)
	url, err := adaptor.BuildRequestURL(info)
	require.NoError(t, err)
	return body, alibabaObject(t, facts), url
}

func TestAlibabaWanModelCapabilities(t *testing.T) {
	plugin := newAlibabaPlugin(t)
	for _, tc := range []struct {
		model      string
		input      map[string]any
		resolution string
		allowed    []string
		size       string
		ratio      string
		service    string
		seconds    float64
	}{
		{"wan3.0-video", map[string]any{"prompt": "a cat"}, "1080P", []string{"480P", "720P", "1080P"}, "", "adaptive", "video-generation", 5},
		{"wan3.0-video-prime", map[string]any{"prompt": "a cat"}, "1080P", []string{"480P", "720P", "1080P"}, "", "adaptive", "video-generation", 5},
		{"wan2.7-t2v", map[string]any{"prompt": "a cat"}, "1080P", []string{"720P", "1080P"}, "", "16:9", "video-generation", 5},
		{"wan2.7-t2v-2026-04-25", map[string]any{"prompt": "a cat"}, "1080P", []string{"720P", "1080P"}, "", "16:9", "video-generation", 5},
		{"wan2.7-t2v-2026-06-12", map[string]any{"prompt": "a cat"}, "1080P", []string{"720P", "1080P"}, "", "16:9", "video-generation", 5},
		{"wan2.7-i2v", map[string]any{"media": []any{map[string]any{"type": "first_frame", "url": "https://cdn.example/first.png"}}}, "1080P", []string{"720P", "1080P"}, "", "", "video-generation", 5},
		{"wan2.7-i2v-2026-04-25", map[string]any{"media": []any{map[string]any{"type": "first_clip", "url": "https://cdn.example/first.mp4"}}}, "1080P", []string{"720P", "1080P"}, "", "", "video-generation", 5},
		{"wan2.6-t2v", map[string]any{"prompt": "a cat"}, "1080P", []string{"720P", "1080P"}, "1920*1080", "", "video-generation", 5},
		{"wan2.6-t2v-us", map[string]any{"prompt": "a cat"}, "1080P", []string{"720P", "1080P"}, "1920*1080", "", "video-generation", 5},
		{"wan2.6-i2v", map[string]any{"img_url": "https://cdn.example/first.png"}, "1080P", []string{"720P", "1080P"}, "", "", "video-generation", 5},
		{"wan2.6-i2v-flash", map[string]any{"img_url": "https://cdn.example/first.png"}, "1080P", []string{"720P", "1080P"}, "", "", "video-generation", 5},
		{"wan2.6-i2v-us", map[string]any{"img_url": "https://cdn.example/first.png"}, "1080P", []string{"720P", "1080P"}, "", "", "video-generation", 5},
		{"wan2.5-t2v-preview", map[string]any{"prompt": "a cat"}, "1080P", []string{"480P", "720P", "1080P"}, "1920*1080", "", "video-generation", 5},
		{"wan2.5-i2v-preview", map[string]any{"img_url": "https://cdn.example/first.png"}, "1080P", []string{"480P", "720P", "1080P"}, "", "", "video-generation", 5},
		{"wan2.2-t2v-plus", map[string]any{"prompt": "a cat"}, "1080P", []string{"480P", "1080P"}, "1920*1080", "", "video-generation", 5},
		{"wan2.2-i2v-plus", map[string]any{"img_url": "https://cdn.example/first.png"}, "1080P", []string{"480P", "1080P"}, "", "", "video-generation", 5},
		{"wan2.2-i2v-flash", map[string]any{"img_url": "https://cdn.example/first.png"}, "720P", []string{"480P", "720P", "1080P"}, "", "", "video-generation", 5},
		{"wan2.2-kf2v-flash", map[string]any{"first_frame_url": "https://cdn.example/first.png", "last_frame_url": "https://cdn.example/last.png"}, "720P", []string{"480P", "720P", "1080P"}, "", "", "image2video", 5},
		{"wan2.2-s2v", map[string]any{"image_url": "https://cdn.example/first.png", "audio_url": "https://cdn.example/audio.mp3"}, "480P", []string{"480P", "720P"}, "", "", "image2video", 20},
		{"wanx2.1-t2v-plus", map[string]any{"prompt": "a cat"}, "720P", []string{"720P"}, "1280*720", "", "video-generation", 5},
		{"wanx2.1-t2v-turbo", map[string]any{"prompt": "a cat"}, "720P", []string{"480P", "720P"}, "1280*720", "", "video-generation", 5},
		{"wanx2.1-i2v-plus", map[string]any{"img_url": "https://cdn.example/first.png"}, "720P", []string{"720P"}, "", "", "video-generation", 5},
		{"wanx2.1-i2v-turbo", map[string]any{"img_url": "https://cdn.example/first.png"}, "720P", []string{"480P", "720P"}, "", "", "video-generation", 5},
	} {
		t.Run(tc.model, func(t *testing.T) {
			value, err := plugin.Engine.CallPath(t.Context(), "native", []string{"createVideoTask"}, map[string]any{
				"body": map[string]any{"kind": "json", "value": map[string]any{"model": tc.model, "input": tc.input}},
			})
			require.NoError(t, err)
			request := alibabaObject(t, value)["requestBody"].(map[string]any)
			body, facts, url := submitAlibabaRequest(t, plugin, tc.model, request)
			assert.Equal(t, "https://dashscope.aliyuncs.com/api/v1/services/aigc/"+tc.service+"/video-synthesis", url)
			// The pricing table lists exactly the resolutions the model accepts.
			schema, _ := plugin.Meta.UsageForModel(tc.model)
			assert.Equal(t, tc.allowed, schema["resolution"].Enum)
			wantFacts := map[string]any{"seconds": tc.seconds, "resolution": tc.resolution}
			if tc.model == "wan2.6-i2v-flash" {
				wantFacts["audio"] = true // priced separately for silent output; defaults to audio
			}
			assert.Equal(t, wantFacts, facts)
			assert.Equal(t, tc.input, body["input"])
			parameters := body["parameters"].(map[string]any)
			if tc.size != "" {
				assert.Equal(t, tc.size, parameters["size"])
				assert.NotContains(t, parameters, "resolution")
			} else {
				assert.Equal(t, tc.resolution, parameters["resolution"])
				assert.NotContains(t, parameters, "size")
			}
			if tc.ratio != "" {
				assert.Equal(t, tc.ratio, parameters["ratio"])
			} else {
				assert.NotContains(t, parameters, "ratio")
			}
			if tc.model == "wan2.2-s2v" {
				assert.NotContains(t, parameters, "duration")
			} else {
				assert.Equal(t, float64(5), parameters["duration"])
			}
			for _, resolution := range tc.allowed {
				request["resolution"] = resolution
				_, facts, _ := submitAlibabaRequest(t, plugin, tc.model, request)
				assert.Equal(t, resolution, facts["resolution"])
			}
			for _, resolution := range []string{"480P", "720P", "1080P", "4K"} {
				if slices.Contains(tc.allowed, resolution) {
					continue
				}
				request["metadata"].(map[string]any)["parameters"] = map[string]any{"resolution": resolution}
				_, err = plugin.Engine.Call(t.Context(), "buildSubmitRequest", map[string]any{"requestBody": request, "upstreamModel": tc.model})
				require.ErrorContains(t, err, "resolution must be", resolution)
			}
		})
	}
}

func TestAlibabaWanCompatibilityParameters(t *testing.T) {
	plugin := newAlibabaPlugin(t)
	for _, tc := range []struct {
		name       string
		model      string
		request    map[string]any
		parameters map[string]any
	}{
		{"legacy dimensions retain vertical ratio", "wan2.7-t2v", map[string]any{"size": "720*1280"}, map[string]any{"resolution": "720P", "ratio": "9:16"}},
		{"OpenAI dimension separator", "wan3.0-video", map[string]any{"size": "1080x1920"}, map[string]any{"resolution": "1080P", "ratio": "9:16"}},
		{"legacy four thirds", "wan2.7-t2v", map[string]any{"size": "1088*832"}, map[string]any{"resolution": "720P", "ratio": "4:3"}},
		{"modern four thirds", "wan2.7-t2v", map[string]any{"size": "1648*1248"}, map[string]any{"resolution": "1080P", "ratio": "4:3"}},
		{"modern dimensions to legacy protocol", "wan2.5-t2v-preview", map[string]any{"size": "832*1104"}, map[string]any{"size": "832*1088"}},
		{"resolution tier compatibility", "wan2.6-t2v", map[string]any{"size": "720p", "ratio": "1:1"}, map[string]any{"size": "960*960"}},
		{"unified fields override compatibility size", "wan2.7-t2v", map[string]any{"size": "720*1280", "resolution": "1080P", "ratio": "1:1"}, map[string]any{"resolution": "1080P", "ratio": "1:1"}},
		{"native fields override unified fields", "wan2.7-t2v", map[string]any{"size": "720*1280", "resolution": "720P", "ratio": "1:1", "metadata": map[string]any{"parameters": map[string]any{"resolution": "1080P", "ratio": "3:4"}}}, map[string]any{"resolution": "1080P", "ratio": "3:4"}},
		{"native legacy size overrides unified fields", "wan2.6-t2v", map[string]any{"resolution": "1080P", "ratio": "1:1", "metadata": map[string]any{"parameters": map[string]any{"size": "720*1280"}}}, map[string]any{"size": "720*1280"}},
		{"image size only selects tier", "wan2.7-i2v", map[string]any{"image": "https://cdn.example/first.png", "size": "720*1280", "ratio": "1:1"}, map[string]any{"resolution": "720P"}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			request := maps.Clone(tc.request)
			request["model"], request["prompt"] = "my-wan-alias", "a cat"
			body, _, _ := submitAlibabaRequest(t, plugin, tc.model, request)
			assert.Equal(t, tc.model, body["model"])
			parameters := body["parameters"].(map[string]any)
			delete(parameters, "duration")
			delete(parameters, "prompt_extend")
			assert.Equal(t, tc.parameters, parameters)
		})
	}
}

func TestAlibabaWanFlashAudioFact(t *testing.T) {
	plugin := newAlibabaPlugin(t)
	for _, tc := range []struct {
		name    string
		request map[string]any
		audio   bool
	}{
		{"audio defaults to true", map[string]any{}, true},
		{"explicit audio", map[string]any{"audio": true}, true},
		{"silent output", map[string]any{"audio": false}, false},
		{"native parameters win", map[string]any{"audio": true, "metadata": map[string]any{"parameters": map[string]any{"audio": false}}}, false},
	} {
		t.Run(tc.name, func(t *testing.T) {
			request := map[string]any{"model": "wan2.6-i2v-flash", "prompt": "a cat", "image": "https://cdn.example/first.png", "resolution": "720P", "duration": float64(5)}
			maps.Copy(request, tc.request)
			body, facts, _ := submitAlibabaRequest(t, plugin, "wan2.6-i2v-flash", request)
			assert.Equal(t, map[string]any{"seconds": float64(5), "resolution": "720P", "audio": tc.audio}, facts)
			parameters := body["parameters"].(map[string]any)
			if _, explicit := tc.request["audio"]; explicit || tc.request["metadata"] != nil {
				assert.Equal(t, tc.audio, parameters["audio"])
			} else {
				assert.NotContains(t, parameters, "audio")
			}
		})
	}
	// Other Wan models keep their two-fact schema even when a client sends audio.
	_, facts, _ := submitAlibabaRequest(t, plugin, "wan2.6-i2v", map[string]any{"model": "wan2.6-i2v", "prompt": "a cat", "image": "https://cdn.example/first.png", "audio": false, "duration": float64(5)})
	assert.Equal(t, map[string]any{"seconds": float64(5), "resolution": "1080P"}, facts)
}

func TestAlibabaWanEntrypointsPreserveOptions(t *testing.T) {
	plugin := newAlibabaPlugin(t)
	input := map[string]any{"prompt": "a cat", "negative_prompt": "blur", "audio_url": "https://cdn.example/audio.mp3"}
	parameters := map[string]any{"resolution": "720P", "ratio": "9:16", "duration": float64(6), "prompt_extend": false, "watermark": false, "seed": float64(0)}
	for _, entry := range []string{"native", "openai_responses", "openai_video", "multipart"} {
		t.Run(entry, func(t *testing.T) {
			ctx := map[string]any{"model": "wan2.7-t2v"}
			var value any
			var err error
			if entry == "native" {
				ctx["body"] = map[string]any{"kind": "json", "value": map[string]any{"model": "wan2.7-t2v", "input": input, "parameters": parameters}}
				value, err = plugin.Engine.CallPath(t.Context(), "native", []string{"createVideoTask"}, ctx)
			} else {
				request := maps.Clone(input)
				maps.Copy(request, parameters)
				request["model"] = "wan2.7-t2v"
				request["resolution"] = "720p"
				if entry == "openai_responses" {
					request["input"] = request["prompt"]
				}
				protocol := entry
				ctx["body"] = map[string]any{"kind": "json", "value": request}
				if entry == "multipart" {
					protocol = "openai_video"
					ctx["body"] = map[string]any{"kind": "multipart", "fields": map[string]any{
						"prompt": []string{"a cat"}, "negative_prompt": []string{"blur"}, "audio_url": []string{"https://cdn.example/audio.mp3"},
						"resolution": []string{"720p"}, "ratio": []string{"9:16"}, "duration": []string{"6"},
						"prompt_extend": []string{"false"}, "watermark": []string{"false"}, "seed": []string{"0"},
					}}
				}
				value, err = plugin.Engine.CallPath(t.Context(), "protocols", []string{protocol, "decodeRequest"}, ctx)
			}
			require.NoError(t, err)
			request := alibabaObject(t, value)["requestBody"].(map[string]any)
			body, facts, _ := submitAlibabaRequest(t, plugin, "wan2.7-t2v", request)
			assert.Equal(t, input, body["input"])
			assert.Equal(t, parameters, body["parameters"])
			assert.Equal(t, map[string]any{"seconds": float64(6), "resolution": "720P"}, facts)
		})
	}
}

func TestAlibabaWanDurationValidation(t *testing.T) {
	plugin := newAlibabaPlugin(t)
	for _, tc := range []struct {
		model   string
		valid   []int
		invalid []any
	}{
		{"wan3.0-video", []int{-1, 2, 30}, []any{0, 1, 31, 2.5}},
		{"wan2.7-t2v", []int{2, 15}, []any{-1, 0, 1, 16, 2.5}},
		{"wan2.6-i2v", []int{2, 15}, []any{1, 16}},
		{"wan2.6-t2v-us", []int{5, 10, 15}, []any{2, 6, 16}},
		{"wan2.5-t2v-preview", []int{5, 10}, []any{6, 15}},
		{"wan2.2-i2v-flash", []int{5}, []any{3, 10}},
		{"wan2.2-kf2v-flash", []int{5}, []any{3, 10}},
		{"wanx2.1-i2v-turbo", []int{3, 4, 5}, []any{2, 6}},
		{"wan2.2-s2v", []int{1, 19}, []any{-1, 0, 20, 3601}},
	} {
		t.Run(tc.model, func(t *testing.T) {
			request := map[string]any{"model": tc.model, "prompt": "a cat", "image": "https://cdn.example/first.png"}
			if tc.model == "wan2.2-s2v" {
				request["audio_url"] = "https://cdn.example/audio.mp3"
			}
			ctx := map[string]any{"requestBody": request, "upstreamModel": tc.model}
			for _, duration := range tc.valid {
				request["duration"] = duration
				_, err := plugin.Engine.Call(t.Context(), "buildSubmitRequest", ctx)
				require.NoError(t, err, "duration=%v", duration)
			}
			request["duration"] = 5
			for _, duration := range append(tc.invalid, "bad", true, 1e30) {
				request["metadata"] = map[string]any{"parameters": map[string]any{"duration": duration}}
				_, err := plugin.Engine.Call(t.Context(), "buildSubmitRequest", ctx)
				require.ErrorContains(t, err, "duration", "duration=%v", duration)
			}
		})
	}
	// Shadowed -1 values must not override explicit metadata duration, nor
	// mutate a request that will be decoded again after channel selection.
	request := map[string]any{"model": "wan3.0-video", "prompt": "a cat", "duration": -1, "metadata": map[string]any{"parameters": map[string]any{"duration": 8}}}
	value, err := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_video", "decodeRequest"}, map[string]any{"model": "wan3.0-video", "body": map[string]any{"kind": "json", "value": request}})
	require.NoError(t, err)
	decoded := alibabaObject(t, value)["requestBody"].(map[string]any)
	assert.NotContains(t, decoded, "auto_duration")
	body, facts, _ := submitAlibabaRequest(t, plugin, "wan3.0-video", decoded)
	assert.Equal(t, float64(8), body["parameters"].(map[string]any)["duration"])
	assert.Equal(t, float64(8), facts["seconds"])

	request["duration"] = 8
	request["metadata"] = map[string]any{"parameters": map[string]any{"duration": -1}}
	value, err = plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_video", "decodeRequest"}, map[string]any{"model": "wan3.0-video", "body": map[string]any{"kind": "json", "value": request}})
	require.NoError(t, err)
	decoded = alibabaObject(t, value)["requestBody"].(map[string]any)
	body, facts, _ = submitAlibabaRequest(t, plugin, "wan3.0-video", decoded)
	assert.Equal(t, float64(-1), body["parameters"].(map[string]any)["duration"])
	assert.Equal(t, float64(30), facts["seconds"])
}

func TestAlibabaWanMediaInputs(t *testing.T) {
	plugin := newAlibabaPlugin(t)
	first := "https://cdn.example/first.png"
	last := "https://cdn.example/last.png"
	audio := "https://cdn.example/audio.mp3"
	clip := "https://cdn.example/clip.mp4"
	for _, tc := range []struct {
		name    string
		model   string
		request map[string]any
		input   map[string]any
		seconds float64
	}{
		{"2.7 frame and driving audio", "wan2.7-i2v", map[string]any{"image": first, "audio_url": audio}, map[string]any{"media": []any{map[string]any{"type": "first_frame", "url": first}, map[string]any{"type": "driving_audio", "url": audio}}}, 5},
		{"2.7 frame pair", "wan2.7-i2v", map[string]any{"images": []string{first, last}}, map[string]any{"media": []any{map[string]any{"type": "first_frame", "url": first}, map[string]any{"type": "last_frame", "url": last}}}, 5},
		{"legacy frame pair", "wan2.2-kf2v-flash", map[string]any{"images": []string{first, last}}, map[string]any{"first_frame_url": first, "last_frame_url": last}, 5},
		{"digital human", "wan2.2-s2v", map[string]any{"image": first, "audio_url": audio}, map[string]any{"image_url": first, "audio_url": audio}, 20},
		{"2.7 continuation counts total duration", "wan2.7-i2v", map[string]any{"duration": 15, "metadata": map[string]any{"input": map[string]any{"media": []any{map[string]any{"type": "first_clip", "url": clip}, map[string]any{"type": "last_frame", "url": last}}}}}, map[string]any{"media": []any{map[string]any{"type": "first_clip", "url": clip}, map[string]any{"type": "last_frame", "url": last}}}, 15},
		{"Wan3 reference video reserves input and output", "wan3.0-video", map[string]any{"duration": 5, "media": []any{map[string]any{"type": "reference_video", "url": clip}, map[string]any{"type": "reference_audio", "url": audio}}}, map[string]any{"media": []any{map[string]any{"type": "reference_video", "url": clip}, map[string]any{"type": "reference_audio", "url": audio}}}, 30},
		{"Wan3 frame pair", "wan3.0-video-prime", map[string]any{"images": []string{first, last}, "audio": false}, map[string]any{"media": []any{map[string]any{"type": "first_frame", "url": first}, map[string]any{"type": "last_frame", "url": last}}}, 5},
	} {
		t.Run(tc.name, func(t *testing.T) {
			request := maps.Clone(tc.request)
			request["model"] = tc.model
			body, facts, _ := submitAlibabaRequest(t, plugin, tc.model, request)
			assert.Equal(t, tc.input, body["input"])
			assert.Equal(t, tc.seconds, facts["seconds"])
			if request["audio"] == false {
				assert.Equal(t, false, body["parameters"].(map[string]any)["audio"])
			}
		})
	}
	for _, tc := range []struct {
		name  string
		model string
		media []any
	}{
		{"missing visual input", "wan2.7-i2v", []any{map[string]any{"type": "driving_audio", "url": audio}}},
		{"duplicate first frame", "wan2.7-i2v", []any{map[string]any{"type": "first_frame", "url": first}, map[string]any{"type": "first_frame", "url": last}}},
		{"continuation with driving audio", "wan2.7-i2v", []any{map[string]any{"type": "first_clip", "url": clip}, map[string]any{"type": "driving_audio", "url": audio}}},
		{"mixed frames and references", "wan3.0-video", []any{map[string]any{"type": "first_frame", "url": first}, map[string]any{"type": "reference_video", "url": clip}}},
		{"unknown media type", "wan3.0-video", []any{map[string]any{"type": "first_clip", "url": clip}}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			_, err := plugin.Engine.Call(t.Context(), "buildSubmitRequest", map[string]any{"requestBody": map[string]any{
				"model": tc.model, "metadata": map[string]any{"input": map[string]any{"media": tc.media}},
			}})
			require.Error(t, err)
		})
	}
}

func TestAlibabaWanCompletionFactsAndArtifacts(t *testing.T) {
	plugin := newAlibabaPlugin(t)
	for _, tc := range []struct {
		name  string
		model string
		usage map[string]any
		facts map[string]any
	}{
		{"digital human fractional duration", "wan2.2-s2v", map[string]any{"duration": 18.13, "SR": 480}, map[string]any{"seconds": 18.13, "resolution": "480P"}},
		{"Wan3 input plus output", "wan3.0-video", map[string]any{"input_video_duration": 4.25, "output_video_duration": 6.5, "duration": 6.5, "SR": 720}, map[string]any{"seconds": 10.75, "resolution": "720P"}},
		{"Wan3 without reference video", "wan3.0-video-prime", map[string]any{"input_video_duration": 0, "output_video_duration": 8, "SR": 1080}, map[string]any{"seconds": float64(8), "resolution": "1080P"}},
		{"Wan3 missing input duration preserves reserved seconds", "wan3.0-video", map[string]any{"output_video_duration": 6, "SR": 720}, map[string]any{"resolution": "720P"}},
		{"2.7 continuation uses total duration", "wan2.7-i2v", map[string]any{"duration": 15, "output_video_duration": 12, "SR": 1080}, map[string]any{"seconds": float64(15), "resolution": "1080P"}},
		{"flash silent output settles as silent", "wan2.6-i2v-flash", map[string]any{"duration": 5, "SR": 720, "audio": false}, map[string]any{"seconds": float64(5), "resolution": "720P", "audio": false}},
		{"flash without audio flag keeps the estimate", "wan2.6-i2v-flash", map[string]any{"duration": 5, "SR": 1080}, map[string]any{"seconds": float64(5), "resolution": "1080P"}},
		{"audio flag is ignored for models priced without it", "wan2.6-i2v", map[string]any{"duration": 5, "SR": 1080, "audio": false}, map[string]any{"seconds": float64(5), "resolution": "1080P"}},
		{"missing usage keeps reservation", "wan2.7-t2v", map[string]any{}, nil},
		{"oversized duration rejected by host", "wan2.2-s2v", map[string]any{"duration": 1e30, "SR": 480}, nil},
		{"negative input cannot reduce Wan3 charge", "wan3.0-video", map[string]any{"input_video_duration": -5, "output_video_duration": 10}, nil},
		{"negative output cannot reduce Wan3 charge", "wan3.0-video", map[string]any{"input_video_duration": 10, "output_video_duration": -5}, nil},
		{"non-numeric duration rejected by host", "wan2.2-s2v", map[string]any{"duration": "invalid", "SR": 480}, nil},
		{"unknown resolution rejected by host", "wan2.7-t2v", map[string]any{"duration": 5, "SR": 4320}, nil},
	} {
		t.Run(tc.name, func(t *testing.T) {
			adaptor := taskplugin.New(plugin)
			task := &model.Task{Properties: model.Properties{OriginModelName: "my-alias", UpstreamModelName: tc.model}}
			encoded, err := common.Marshal(map[string]any{
				"output": map[string]any{"task_status": "SUCCEEDED", "results": map[string]any{"video_url": "https://cdn.example/result.mp4"}},
				"usage":  tc.usage,
			})
			require.NoError(t, err)
			result, err := adaptor.ParseTaskResult(task, &http.Response{StatusCode: http.StatusOK, Header: make(http.Header)}, encoded)
			require.NoError(t, err)
			assert.Equal(t, "SUCCESS", result.Status)
			assert.Equal(t, "https://cdn.example/result.mp4", result.Url)
			if tc.facts == nil {
				assert.Nil(t, result.UsageFacts)
			} else {
				assert.Equal(t, tc.facts, alibabaObject(t, result.UsageFacts))
			}
		})
	}

	adaptor := taskplugin.New(plugin)
	task := &model.Task{TaskID: "task_public", Status: model.TaskStatusSuccess, Properties: model.Properties{OriginModelName: "wan2.2-s2v"}}
	task.SetData(map[string]any{"output": map[string]any{"task_status": "SUCCEEDED", "results": map[string]any{"video_url": "https://cdn.example/result.mp4"}}})
	artifacts, err := adaptor.ListArtifacts(task)
	require.NoError(t, err)
	require.Len(t, artifacts, 1)
	assert.Equal(t, "video", artifacts[0].Key)
	value, err := plugin.Engine.Call(t.Context(), "buildContentRequest", map[string]any{"artifactKey": "video", "data": map[string]any{"output": map[string]any{"results": map[string]any{"video_url": "https://cdn.example/result.mp4"}}}, "clientRequest": map[string]any{"method": "GET"}})
	require.NoError(t, err)
	assert.Equal(t, "https://cdn.example/result.mp4", alibabaObject(t, value)["url"])
}

func TestAlibabaImageSubmission(t *testing.T) {
	source, err := builtinplugins.Source("alibaba")
	require.NoError(t, err)
	registry := jsplugin.NewRegistry()
	plugin, err := registry.RegisterFactory(source, jsplugin.Options{Key: "alibaba"})
	require.NoError(t, err)
	const base = "/api/v1/services/aigc/"
	for _, tc := range []struct {
		name       string
		model      string
		input      map[string]any
		parameters map[string]any
		path       string
		count      float64
	}{
		{"legacy text", "wan2.2-t2i-flash", map[string]any{"prompt": "a cat"}, map[string]any{"n": 2}, "text2image/image-synthesis", 2},
		{"legacy default count", "wanx2.1-t2i-plus", map[string]any{"prompt": "a cat"}, nil, "text2image/image-synthesis", 4},
		{"2.6 text", "wan2.6-t2i", map[string]any{"messages": []any{map[string]any{"role": "user", "content": []any{map[string]any{"text": "a cat"}}}}}, map[string]any{"n": 2, "seed": 0, "watermark": false, "size": "960*1696"}, "image-generation/generation", 2},
		{"2.6 synchronous", "wan2.6-t2i", map[string]any{"messages": []any{map[string]any{"role": "user", "content": []any{map[string]any{"text": "a cat"}}}}}, nil, "multimodal-generation/generation", 1},
		{"2.7 group default count", "wan2.7-image", map[string]any{"messages": []any{map[string]any{"role": "user", "content": []any{map[string]any{"text": "seasons"}}}}}, map[string]any{"enable_sequential": true}, "image-generation/generation", 12},
		{"2.6 interleaved default count", "wan2.6-image", map[string]any{"messages": []any{map[string]any{"role": "user", "content": []any{map[string]any{"text": "recipe"}}}}}, map[string]any{"enable_interleave": true}, "image-generation/generation", 5},
		{"2.6 edit", "wan2.6-image", map[string]any{"messages": []any{map[string]any{"role": "user", "content": []any{map[string]any{"image": "https://cdn.example/input.png"}, map[string]any{"text": "watercolor"}}}}}, map[string]any{"size": "2K", "n": 1}, "multimodal-generation/generation", 1},
		{"2.7 4K", "wan2.7-image-pro", map[string]any{"messages": []any{map[string]any{"role": "user", "content": []any{map[string]any{"text": "a cat"}}}}}, map[string]any{"size": "4K"}, "multimodal-generation/generation", 1},
	} {
		t.Run(tc.name, func(t *testing.T) {
			path := "/ali" + base + tc.path
			binding, found := registry.Generation().LookupDeclaredRoute(http.MethodPost, path)
			require.True(t, found)
			assert.Contains(t, binding.Route.Models, tc.model)
			value, err := plugin.Engine.CallPath(t.Context(), "native", []string{binding.Route.Decode}, map[string]any{
				"path": path, "body": map[string]any{"kind": "json", "value": map[string]any{"model": tc.model, "input": tc.input, "parameters": tc.parameters}},
			})
			require.NoError(t, err)
			request := alibabaObject(t, value)["requestBody"].(map[string]any)
			body, facts, url := submitAlibabaRequest(t, plugin, tc.model, request)
			assert.Equal(t, "https://dashscope.aliyuncs.com"+base+tc.path, url)
			assert.Equal(t, alibabaObject(t, tc.input), body["input"])
			assert.Equal(t, map[string]any{"image_count": tc.count}, facts)
			parameters := body["parameters"].(map[string]any)
			for key, expected := range alibabaObject(t, tc.parameters) {
				assert.Equal(t, expected, parameters[key], key)
			}
			value, err = plugin.Engine.Call(t.Context(), "buildSubmitRequest", map[string]any{"upstreamModel": tc.model, "requestBody": request})
			require.NoError(t, err)
			headers := alibabaObject(t, value)["headers"].(map[string]any)
			if tc.path == "multimodal-generation/generation" {
				assert.NotContains(t, headers, "X-DashScope-Async")
			} else {
				assert.Equal(t, "enable", headers["X-DashScope-Async"])
			}
		})
	}

	t.Run("Responses images use task artifacts and do not claim video creation", func(t *testing.T) {
		for _, name := range []string{"wan2.6-t2i", "wan2.7-image", "wanx2.1-t2i-plus"} {
			_, found := registry.Generation().LookupEndpoint(http.MethodPost, "/v1/responses", name)
			require.True(t, found)
			_, found = registry.Generation().LookupEndpoint(http.MethodPost, "/v1/videos", name)
			assert.False(t, found)
		}
		value, err := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_responses", "decodeRequest"}, map[string]any{
			"model": "my-image-alias", "upstreamModel": "wan2.7-image", "body": map[string]any{"kind": "json", "value": map[string]any{
				"input": []any{map[string]any{"type": "input_text", "text": "watercolor"}, map[string]any{"type": "input_image", "image_url": "https://cdn.example/input.png"}}, "n": 2,
			}},
		})
		require.NoError(t, err)
		resolved := alibabaObject(t, value)
		assert.Equal(t, "image_to_image", resolved["action"])
		body, facts, url := submitAlibabaRequest(t, plugin, "wan2.7-image", resolved["requestBody"].(map[string]any))
		assert.Equal(t, "wan2.7-image", body["model"])
		assert.Equal(t, map[string]any{"image_count": float64(2)}, facts)
		assert.Equal(t, "https://dashscope.aliyuncs.com"+base+"image-generation/generation", url)
	})
}

func TestAlibabaImageValidation(t *testing.T) {
	plugin := newAlibabaPlugin(t)
	for _, tc := range []struct {
		name    string
		model   string
		request map[string]any
		message string
	}{
		{"zero count", "wan2.6-t2i", map[string]any{"n": 0}, "n must be"},
		{"fractional count", "wan2.6-t2i", map[string]any{"n": 1.5}, "n must be"},
		{"string count", "wan2.6-t2i", map[string]any{"n": "2"}, "n must be"},
		{"oversized nested count", "wan2.6-t2i", map[string]any{"metadata": map[string]any{"parameters": map[string]any{"n": 1e30}}}, "n must be"},
		{"negative nested count", "wan2.6-t2i", map[string]any{"metadata": map[string]any{"parameters": map[string]any{"n": -1}}}, "n must be"},
		{"group limit", "wan2.7-image", map[string]any{"enable_sequential": true, "n": 13}, "n must be"},
		{"interleaved limit", "wan2.6-image", map[string]any{"enable_interleave": true, "max_images": 6}, "max_images must be"},
		{"nested interleaved limit", "wan2.6-image", map[string]any{"metadata": map[string]any{"parameters": map[string]any{"enable_interleave": true, "max_images": 1e30}}}, "max_images must be"},
		{"interleaved n", "wan2.6-image", map[string]any{"enable_interleave": true, "n": 2}, "n must be"},
		{"string mode flag", "wan2.7-image", map[string]any{"enable_sequential": "true"}, "must be a boolean"},
		{"upstream SSE", "wan2.6-image", map[string]any{"metadata": map[string]any{"parameters": map[string]any{"stream": true}}}, "upstream streaming requires"},
		{"legacy synchronous", "wan2.2-t2i-flash", map[string]any{"metadata": map[string]any{"upstream_mode": "sync"}}, "only supports asynchronous"},
		{"edit without reference", "wan2.6-image", nil, "requires a reference image"},
		{"t2i with reference", "wan2.6-t2i", map[string]any{"image": "https://cdn.example/input.png"}, "only supports text-to-image input"},
		{"invalid 4K", "wan2.7-image", map[string]any{"size": "4K"}, "4K is only supported"},
		{"oversized pixel dimensions", "wan2.7-image", map[string]any{"size": "999999999999*999999999999"}, "pixel and aspect-ratio limits"},
		{"4K group dimensions", "wan2.7-image-pro", map[string]any{"size": "4096*4096", "enable_sequential": true}, "pixel and aspect-ratio limits"},
		{"model override", "wan2.7-image", map[string]any{"metadata": map[string]any{"model": "wan2.7-image-pro"}}, "can't change model"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			request := map[string]any{"model": tc.model, "prompt": "a cat"}
			maps.Copy(request, tc.request)
			_, err := plugin.Engine.Call(t.Context(), "buildSubmitRequest", map[string]any{"requestBody": request, "upstreamModel": tc.model})
			require.ErrorContains(t, err, tc.message)
			// The production validator must reject the same input before HTTP/billing.
			info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: tc.model}, TaskRelayInfo: &relaycommon.TaskRelayInfo{}}
			adaptor := taskplugin.New(plugin)
			adaptor.Init(info)
			c, _ := gin.CreateTestContext(httptest.NewRecorder())
			c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
			c.Set("task_request", request)
			taskErr := adaptor.ValidateRequestAndSetAction(c, info)
			require.NotNil(t, taskErr)
			assert.Equal(t, http.StatusBadRequest, taskErr.StatusCode)
		})
	}
}

func TestAlibabaImageResults(t *testing.T) {
	plugin := newAlibabaPlugin(t)
	const first = "https://cdn.example/first.png"
	const second = "https://cdn.example/second.png"
	for _, tc := range []struct {
		name   string
		model  string
		output map[string]any
	}{
		{"legacy partial success", "wan2.2-t2i-flash", map[string]any{"results": []any{map[string]any{"url": first}, map[string]any{"code": "DataInspectionFailed"}, map[string]any{"url": second}}}},
		{"modern interleaved output", "wan2.6-image", map[string]any{"choices": []any{map[string]any{"message": map[string]any{"content": []any{map[string]any{"text": "Step 1"}, map[string]any{"image": first}, map[string]any{"text": "Step 2"}, map[string]any{"image": second}}}}}}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			output := maps.Clone(tc.output)
			output["task_status"] = "SUCCEEDED"
			body := map[string]any{"output": output, "usage": map[string]any{"image_count": 2}}
			encoded, err := common.Marshal(body)
			require.NoError(t, err)
			task := &model.Task{TaskID: "task_public", Status: model.TaskStatusSuccess, Properties: model.Properties{OriginModelName: "alias", UpstreamModelName: tc.model}}
			task.SetData(body)
			adaptor := taskplugin.New(plugin)
			adaptor.Init(&relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{}})
			result, err := adaptor.ParseTaskResult(task, &http.Response{StatusCode: http.StatusOK}, encoded)
			require.NoError(t, err)
			assert.Equal(t, "SUCCESS", result.Status)
			assert.Equal(t, first, result.Url)
			require.Equal(t, map[string]any{"image_count": float64(2)}, result.UsageFacts)
			// A reservation for four images can settle to the two actual successes.
			cost, _, err := billingexpr.RunExprWithRequest(`tier("image", u("image_count") * 0.1)`, billingexpr.TokenParams{}, billingexpr.RequestInput{Usage: result.UsageFacts})
			require.NoError(t, err)
			assert.InDelta(t, 0.2, cost, 1e-9)
			artifacts, err := adaptor.ListArtifacts(task)
			require.NoError(t, err)
			require.Len(t, artifacts, 2)
			assert.Equal(t, "image-2", artifacts[1].Key)
			assert.Equal(t, "image", artifacts[1].Type)
			content, err := adaptor.BuildContentRequest(t.Context(), task, "image-2", relaychannel.TaskArtifactClientRequest{Method: http.MethodGet})
			require.NoError(t, err)
			assert.Equal(t, second, content.URL)
			assert.True(t, content.Credentialless)
			assert.Empty(t, content.Headers)
			_, err = adaptor.BuildContentRequest(t.Context(), task, "image-3", relaychannel.TaskArtifactClientRequest{Method: http.MethodGet})
			require.Error(t, err)
			value, err := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_responses", "renderFinal"}, map[string]any{
				"artifacts": map[string]any{"image-1": map[string]any{"url": "/v1/tasks/task_public/artifacts/image-1/content"}, "image-2": map[string]any{"url": "/v1/tasks/task_public/artifacts/image-2/content"}},
			}, map[string]any{"status": "SUCCESS", "data": body})
			require.NoError(t, err)
			final := alibabaObject(t, value)
			message := final["output"].([]any)[0].(map[string]any)["content"].([]any)[0].(map[string]any)["text"].(string)
			assert.Contains(t, message, "![Image 1](</v1/tasks/task_public/artifacts/image-1/content>)")
			assert.Contains(t, message, "![Image 2](</v1/tasks/task_public/artifacts/image-2/content>)")
			assert.NotContains(t, message, "cdn.example")
			if tc.model == "wan2.6-image" {
				assert.Contains(t, message, "Step 1\n\n![Image 1]")
			}
		})
	}

	t.Run("synchronous completion uses the host ID and retains result JSON", func(t *testing.T) {
		info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: "wan2.6-t2i"}, TaskRelayInfo: &relaycommon.TaskRelayInfo{PublicTaskID: "task_public"}}
		adaptor := taskplugin.New(plugin)
		adaptor.Init(info)
		c, _ := gin.CreateTestContext(httptest.NewRecorder())
		c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
		c.Set("task_request", map[string]any{"model": "wan2.6-t2i", "prompt": "a cat", "metadata": map[string]any{"upstream_mode": "sync"}})
		body := map[string]any{"request_id": "request_upstream", "output": map[string]any{"finished": true, "choices": []any{map[string]any{"message": map[string]any{"content": []any{map[string]any{"image": first}}}}}}, "usage": map[string]any{"image_count": 1}}
		encoded, err := common.Marshal(body)
		require.NoError(t, err)
		parsed, taskErr := adaptor.ParseResponse(c, &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(encoded))}, info)
		require.Nil(t, taskErr)
		require.NotNil(t, parsed.Immediate)
		assert.Equal(t, "task_public", parsed.UpstreamTaskID)
		assert.Equal(t, "SUCCESS", parsed.Immediate.Status)
		assert.Equal(t, "100%", parsed.Immediate.Progress)
		assert.Equal(t, first, parsed.Immediate.Url)
		assert.JSONEq(t, string(encoded), string(parsed.TaskData))
		assert.Equal(t, map[string]any{"image_count": float64(1)}, parsed.Immediate.UsageFacts)
		value, err := plugin.Engine.CallPath(t.Context(), "native", []string{"imageCreated"}, map[string]any{}, map[string]any{"data": body})
		require.NoError(t, err)
		assert.Equal(t, alibabaObject(t, body), alibabaObject(t, value))
	})

	t.Run("asynchronous submission remains pending", func(t *testing.T) {
		value, err := plugin.Engine.Call(t.Context(), "parseSubmitResponse", map[string]any{"model": "wan2.6-t2i", "requestBody": map[string]any{"model": "wan2.6-t2i", "prompt": "a cat"}}, map[string]any{"body": map[string]any{"output": map[string]any{"task_id": "upstream_task", "task_status": "PENDING"}}})
		require.NoError(t, err)
		result := alibabaObject(t, value)
		assert.Equal(t, "upstream_task", result["taskId"])
		assert.NotContains(t, result, "immediate")
	})

	t.Run("failed and malformed synchronous results do not complete a task", func(t *testing.T) {
		for _, body := range []map[string]any{
			{"code": "DataInspectionFailed", "message": "rejected"},
			{"output": map[string]any{"finished": true, "choices": []any{}}},
			{"output": map[string]any{"finished": false, "choices": []any{map[string]any{"message": map[string]any{"content": []any{map[string]any{"image": first}}}}}}},
		} {
			_, err := plugin.Engine.Call(t.Context(), "parseSubmitResponse", map[string]any{"upstreamModel": "wan2.6-t2i", "publicTaskId": "task_public", "requestBody": map[string]any{"model": "wan2.6-t2i", "prompt": "a cat", "metadata": map[string]any{"upstream_mode": "sync"}}}, map[string]any{"body": body})
			require.Error(t, err)
		}
	})

	t.Run("invalid completion counts retain the reservation", func(t *testing.T) {
		adaptor := taskplugin.New(plugin)
		task := &model.Task{Properties: model.Properties{UpstreamModelName: "wan2.6-t2i"}}
		for _, count := range []any{-1, 0, 1.5, 1e30, "2"} {
			encoded, err := common.Marshal(map[string]any{"output": map[string]any{"task_status": "SUCCEEDED", "choices": []any{map[string]any{"message": map[string]any{"content": []any{map[string]any{"image": first}}}}}}, "usage": map[string]any{"image_count": count}})
			require.NoError(t, err)
			result, err := adaptor.ParseTaskResult(task, &http.Response{StatusCode: http.StatusOK}, encoded)
			require.NoError(t, err)
			assert.Equal(t, "SUCCESS", result.Status)
			assert.Nil(t, result.UsageFacts, "invalid count %v must not replace the reservation", count)
		}
	})

	t.Run("interleaved text without images has zero image usage", func(t *testing.T) {
		body := map[string]any{"output": map[string]any{"task_status": "SUCCEEDED", "choices": []any{map[string]any{"message": map[string]any{"content": []any{map[string]any{"text": "Text-only answer"}}}}}}}
		encoded, err := common.Marshal(body)
		require.NoError(t, err)
		adaptor := taskplugin.New(plugin)
		result, err := adaptor.ParseTaskResult(&model.Task{Properties: model.Properties{UpstreamModelName: "wan2.6-image"}}, &http.Response{StatusCode: http.StatusOK}, encoded)
		require.NoError(t, err)
		assert.Equal(t, "SUCCESS", result.Status)
		assert.Equal(t, map[string]any{"image_count": float64(0)}, result.UsageFacts)
		value, err := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_responses", "renderEvents"}, map[string]any{}, map[string]any{"status": "SUCCESS", "data": body}, nil)
		require.NoError(t, err)
		events := alibabaObject(t, value)
		assert.Equal(t, true, events["done"])
		assert.Equal(t, []any{map[string]any{"type": "output", "data": "Text-only answer"}}, events["events"])
	})
}

func TestAlibabaSynchronousMultiImageAndStream(t *testing.T) {
	plugin := newAlibabaPlugin(t)
	for _, streaming := range []bool{false, true} {
		t.Run(fmt.Sprintf("SSE=%t", streaming), func(t *testing.T) {
			name := "wan2.7-image"
			parameters := map[string]any{"n": 4}
			if streaming {
				name = "wan2.6-image"
				parameters = map[string]any{"enable_interleave": true, "max_images": 3}
			}
			info := &relaycommon.RelayInfo{OriginModelName: name, ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: name, ChannelBaseUrl: "https://dashscope.aliyuncs.com"}, TaskRelayInfo: &relaycommon.TaskRelayInfo{PublicTaskID: "task_sync"}}
			adaptor := taskplugin.New(plugin)
			adaptor.Init(info)
			c, _ := gin.CreateTestContext(httptest.NewRecorder())
			c.Request = httptest.NewRequest("POST", "/v1/responses", nil)
			c.Set("task_request", map[string]any{"model": name, "prompt": "recipe", "metadata": map[string]any{"upstream_mode": "sync", "parameters": parameters}})
			require.Nil(t, adaptor.ValidateRequestAndSetAction(c, info))
			facts, err := adaptor.ExtractUsageFactsValidated(c, info)
			require.NoError(t, err)
			wantEstimate := float64(4)
			if streaming {
				wantEstimate = 3
			}
			assert.Equal(t, wantEstimate, facts["image_count"])
			var content []any
			if streaming {
				content = append(content, map[string]any{"type": "text", "text": "第一步"})
			}
			content = append(content, map[string]any{"type": "image", "image": "https://cdn.example/1.png"})
			if streaming {
				content = append(content, map[string]any{"type": "text", "text": "第二步"})
			}
			content = append(content, map[string]any{"type": "image", "image": "https://cdn.example/2.png"})
			body := map[string]any{"request_id": "vendor-request", "output": map[string]any{"finished": true, "choices": []any{map[string]any{"finish_reason": "stop", "message": map[string]any{"role": "assistant", "content": content}}}}, "usage": map[string]any{"image_count": 2}}
			encoded, err := common.Marshal(body)
			require.NoError(t, err)
			responseType := "application/json"
			if streaming {
				var stream strings.Builder
				parts := []map[string]any{{"type": "text", "text": "第"}, {"type": "text", "text": "一步"}, {"type": "image", "image": "https://cdn.example/1.png"}, {"type": "text", "text": "第二步"}, {"type": "image", "image": "https://cdn.example/2.png"}}
				count := 0
				for index, part := range parts {
					if part["image"] != nil {
						count++
					}
					finish := "null"
					if index == len(parts)-1 {
						finish = "stop"
					}
					chunk, err := common.Marshal(map[string]any{"request_id": "vendor-request", "output": map[string]any{"finished": true, "choices": []any{map[string]any{"finish_reason": finish, "message": map[string]any{"role": "assistant", "content": []any{part}}}}}, "usage": map[string]any{"image_count": count}})
					require.NoError(t, err)
					stream.WriteString("data: ")
					stream.Write(chunk)
					stream.WriteString("\n\n")
				}
				encoded = []byte(stream.String())
				responseType = "text/event-stream"
				request := httptest.NewRequest("POST", "https://dashscope.aliyuncs.com", nil)
				require.NoError(t, adaptor.BuildRequestHeader(c, request, info))
				assert.Equal(t, "enable", request.Header.Get("X-DashScope-Sse"))
			}
			parsed, taskErr := adaptor.ParseResponse(c, &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": {responseType}}, Body: io.NopCloser(bytes.NewReader(encoded))}, info)
			require.Nil(t, taskErr)
			require.NotNil(t, parsed.Immediate)
			assert.Equal(t, "SUCCESS", parsed.Immediate.Status)
			assert.Equal(t, map[string]any{"image_count": float64(2)}, parsed.Immediate.UsageFacts)
			want, err := common.Marshal(body)
			require.NoError(t, err)
			assert.JSONEq(t, string(want), string(parsed.TaskData))
			assert.False(t, c.Writer.Written())
		})
	}
}

// Interleaved Wan output is a stream for a native client that asks for one
// with X-DashScope-Sse. DashScope's error event reaches the client and ends
// the result: before any image the task fails and costs nothing, after one
// the reservation stands. A gateway released before relayed streams answers
// the stream request with one JSON body, which settles instead of failing
// after that gateway has billed it.
func TestAlibabaInterleavedStreamOutcomes(t *testing.T) {
	plugin := newAlibabaPlugin(t)
	route := plugin.Meta.Routes[slices.IndexFunc(plugin.Meta.Routes, func(route jsplugin.Route) bool {
		return route.Path == "/api/v1/services/aigc/multimodal-generation/generation"
	})]
	submit := func(t *testing.T, channelType int, contentType, body string) (*taskplugin.TaskAdaptor, *relaycommon.RelayInfo, *relaychannel.TaskSubmitResponse, string) {
		info := &relaycommon.RelayInfo{OriginModelName: "wan2.6-image", ChannelMeta: &relaycommon.ChannelMeta{ChannelType: channelType, UpstreamModelName: "wan2.6-image", ChannelBaseUrl: "https://upstream.example", ApiKey: "key"},
			TaskRelayInfo: &relaycommon.TaskRelayInfo{PublicTaskID: "task_public"}}
		adaptor := taskplugin.New(plugin)
		adaptor.Init(info)
		recorder := httptest.NewRecorder()
		c, _ := gin.CreateTestContext(recorder)
		c.Request = httptest.NewRequest(http.MethodPost, route.Path, nil)
		c.Request.Header.Set("X-DashScope-Sse", "enable")
		c.Set(jsplugin.ContextKeyPinnedRoute, jsplugin.PinnedRoute{Plugin: plugin, Route: route})
		c.Set("task_request", map[string]any{"model": "wan2.6-image", "metadata": map[string]any{"upstream_mode": "sync",
			"input":      map[string]any{"messages": []any{map[string]any{"role": "user", "content": []any{map[string]any{"text": "a recipe"}}}}},
			"parameters": map[string]any{"enable_interleave": true, "stream": true, "max_images": 3}}})
		require.Nil(t, adaptor.ValidateRequestAndSetAction(c, info))
		request := httptest.NewRequest(http.MethodPost, "https://upstream.example", nil)
		require.NoError(t, adaptor.BuildRequestHeader(c, request, info))
		require.Equal(t, "enable", request.Header.Get("X-DashScope-Sse"), "DashScope and current gateways stream interleaved output")
		parsed, taskErr := adaptor.ParseResponse(c, &http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": {contentType}}, Body: io.NopCloser(strings.NewReader(body))}, info)
		require.Nil(t, taskErr)
		require.NotNil(t, parsed.Immediate)
		return adaptor, info, parsed, recorder.Body.String()
	}
	const image = "id:1\nevent:result\n:HTTP_STATUS/200\ndata:{\"output\":{\"choices\":[{\"message\":{\"role\":\"assistant\",\"content\":[{\"image\":\"https://cdn.example/1.png\"}]},\"finish_reason\":\"null\"}],\"finished\":false},\"usage\":{\"image_count\":1},\"request_id\":\"r\"}\n\n"
	const failed = "id:2\nevent:error\n:HTTP_STATUS/400\ndata:{\"code\":\"DataInspectionFailed\",\"message\":\"Output data may contain inappropriate content.\",\"request_id\":\"r\"}\n\n"

	t.Run("an error event before any image fails the task", func(t *testing.T) {
		_, _, parsed, relayed := submit(t, 0, "text/event-stream", failed)
		assert.Equal(t, failed, relayed)
		assert.Equal(t, "FAILURE", parsed.Immediate.Status)
		assert.Equal(t, "DataInspectionFailed: Output data may contain inappropriate content.", parsed.Immediate.Reason)
	})

	t.Run("an error event after an image keeps the reservation", func(t *testing.T) {
		adaptor, info, parsed, relayed := submit(t, 0, "text/event-stream", image+failed)
		assert.Equal(t, image+failed, relayed)
		assert.Equal(t, "SUCCESS", parsed.Immediate.Status)
		assert.Equal(t, "task_public", parsed.UpstreamTaskID)
		assert.Nil(t, parsed.Immediate.UsageFacts)
		assert.Nil(t, adaptor.AdjustBillingOnSubmit(info, parsed.TaskData))
	})

	t.Run("an earlier gateway answers with JSON", func(t *testing.T) {
		body := `{"request_id":"r","output":{"finished":true,"choices":[{"finish_reason":"stop","message":{"role":"assistant","content":[{"text":"step"},{"image":"https://cdn.example/1.png"},{"image":"https://cdn.example/2.png"}]}}]},"usage":{"image_count":2}}`
		_, _, parsed, relayed := submit(t, constant.ChannelTypeNewAPI, "application/json", body)
		assert.Empty(t, relayed, "the presenter answers the client")
		assert.Equal(t, "SUCCESS", parsed.Immediate.Status)
		assert.Equal(t, map[string]any{"image_count": float64(2)}, parsed.Immediate.UsageFacts)
		assert.JSONEq(t, body, string(parsed.TaskData))
	})
}

// DashScope's own bodies go to DashScope as sent, for it to check. The
// gateway asks the asynchronous endpoints for a task itself, sends a video
// body to the endpoint its client called, and relays interleaved output as a
// stream only to a client that asks for SSE; host protocols keep the model's
// endpoint and always take interleaved output as a stream.
func TestAlibabaNativeRequestsGoAsSent(t *testing.T) {
	plugin := newAlibabaPlugin(t)
	call := func(export string, members []string, args ...any) (map[string]any, error) {
		value, err := plugin.Engine.CallPath(t.Context(), export, members, args...)
		if err != nil {
			return nil, err
		}
		return alibabaObject(t, value), nil
	}
	messages := map[string]any{"messages": []any{map[string]any{"role": "user", "content": []any{map[string]any{"text": "a recipe"}}}}}
	for _, tc := range []struct {
		name          string
		path          string
		decoder       string
		headers       map[string]any
		body          map[string]any
		upstream      string
		wantURL       string
		wantHeaders   map[string]any
		wantType      any
		wantStream    any
		wantFacts     map[string]any
		wantVideoBody bool
	}{
		{
			name: "a video task without X-DashScope-Async", path: "/api/v1/services/aigc/video-generation/video-synthesis", decoder: "createVideoTask",
			body:    map[string]any{"model": "wan3.0-video", "input": map[string]any{"prompt": "a cat"}},
			wantURL: "https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis", wantHeaders: map[string]any{"X-DashScope-Async": "enable"},
			wantFacts: map[string]any{"seconds": float64(5), "resolution": "1080P"}, wantVideoBody: true,
		},
		{
			name: "a video model at another endpoint", path: "/ali/api/v1/services/aigc/image2video/video-synthesis", decoder: "createVideoTask",
			body:    map[string]any{"model": "wan3.0-video", "input": map[string]any{"prompt": "a cat"}},
			wantURL: "https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis", wantHeaders: map[string]any{"X-DashScope-Async": "enable"},
			wantFacts: map[string]any{"seconds": float64(5), "resolution": "1080P"}, wantVideoBody: true,
		},
		{
			name: "an alias goes as the channel's model", path: "/api/v1/services/aigc/video-generation/video-synthesis", decoder: "createVideoTask",
			body: map[string]any{"model": "my-wan", "input": map[string]any{"prompt": "a cat"}}, upstream: "wan3.0-video",
			wantURL: "https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis", wantHeaders: map[string]any{"X-DashScope-Async": "enable"},
			wantFacts: map[string]any{"seconds": float64(5), "resolution": "1080P"}, wantVideoBody: true,
		},
		{
			name: "interleaved output without X-DashScope-Sse", path: "/api/v1/services/aigc/multimodal-generation/generation", decoder: "createImageTask",
			body:    map[string]any{"model": "wan2.6-image", "input": messages, "parameters": map[string]any{"enable_interleave": true, "max_images": 3}},
			wantURL: "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", wantHeaders: map[string]any{},
			wantType: "json", wantFacts: map[string]any{"image_count": float64(3)},
		},
		{
			name: "a stream in the body without X-DashScope-Sse", path: "/api/v1/services/aigc/multimodal-generation/generation", decoder: "createImageTask",
			body:    map[string]any{"model": "wan2.6-image", "input": messages, "parameters": map[string]any{"enable_interleave": true, "stream": true, "max_images": 3}},
			wantURL: "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", wantHeaders: map[string]any{},
			wantType: "sse", wantStream: true, wantFacts: map[string]any{"image_count": float64(3)},
		},
		{
			name: "interleaved output with X-DashScope-Sse", path: "/api/v1/services/aigc/multimodal-generation/generation", decoder: "createImageTask",
			headers: map[string]any{"X-Dashscope-Sse": "enable"},
			body:    map[string]any{"model": "wan2.6-image", "input": messages, "parameters": map[string]any{"enable_interleave": true, "max_images": 3}},
			wantURL: "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", wantHeaders: map[string]any{"X-DashScope-Sse": "enable"},
			wantType: "sse", wantStream: true, wantFacts: map[string]any{"image_count": float64(3)},
		},
		{
			name: "a top-level stream field", path: "/api/v1/services/aigc/multimodal-generation/generation", decoder: "createImageTask",
			body:    map[string]any{"model": "wan2.6-t2i", "stream": true, "input": map[string]any{"messages": []any{map[string]any{"role": "user", "content": []any{map[string]any{"text": "a cat"}}}}}},
			wantURL: "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", wantHeaders: map[string]any{},
			wantType: "json", wantFacts: map[string]any{"image_count": float64(1)},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			headers := tc.headers
			if headers == nil {
				headers = map[string]any{}
			}
			intent, err := call("native", []string{tc.decoder}, map[string]any{"path": tc.path, "requestHeaders": headers, "body": map[string]any{"kind": "json", "value": tc.body}})
			require.NoError(t, err)
			upstream := tc.body["model"]
			if tc.upstream != "" {
				upstream = tc.upstream
			}
			driver := map[string]any{"path": tc.path, "requestHeaders": headers, "requestBody": intent["requestBody"], "model": tc.body["model"], "upstreamModel": upstream,
				"baseUrl": "https://dashscope.aliyuncs.com", "apiKey": "key", "upstream": map[string]any{"kind": "vendor"}, "publicTaskId": "task_public"}
			descriptor, err := call("buildSubmitRequest", nil, driver)
			require.NoError(t, err)
			assert.Equal(t, tc.wantURL, descriptor["url"])
			sent := map[string]any{}
			for key, value := range descriptor["headers"].(map[string]any) {
				if strings.HasPrefix(key, "X-DashScope") {
					sent[key] = value
				}
			}
			assert.Equal(t, tc.wantHeaders, sent)
			assert.Equal(t, tc.wantType, descriptor["responseType"])
			body := descriptor["body"].(map[string]any)
			if tc.wantVideoBody {
				assert.Equal(t, upstream, body["model"])
				assert.Equal(t, tc.body["input"], body["input"])
			} else {
				assert.Equal(t, tc.wantStream, body["parameters"].(map[string]any)["stream"])
			}
			facts, err := call("extractUsage", nil, driver)
			require.NoError(t, err)
			assert.Equal(t, tc.wantFacts, facts)
		})
	}

	t.Run("a video request needs its model", func(t *testing.T) {
		_, err := call("native", []string{"createVideoTask"}, map[string]any{"path": "/api/v1/services/aigc/video-generation/video-synthesis",
			"body": map[string]any{"kind": "json", "value": map[string]any{"input": map[string]any{"prompt": "a cat"}}}})
		require.ErrorContains(t, err, "model is required")
	})

	// A /v1/videos client cannot pass its request off as a native one.
	t.Run("a host protocol keeps the model's endpoint", func(t *testing.T) {
		intent, err := call("protocols", []string{"openai_video", "decodeRequest"}, map[string]any{"model": "wan2.2-kf2v-flash", "path": "/v1/videos",
			"body": map[string]any{"kind": "json", "value": map[string]any{"model": "wan2.2-kf2v-flash", "prompt": "a cat",
				"first_frame_url": "https://cdn.example/first.png", "last_frame_url": "https://cdn.example/last.png", "native": map[string]any{"service": "video-generation"}}}})
		require.NoError(t, err)
		descriptor, err := call("buildSubmitRequest", nil, map[string]any{"path": "/v1/videos", "requestHeaders": map[string]any{}, "requestBody": intent["requestBody"],
			"model": "wan2.2-kf2v-flash", "upstreamModel": "wan2.2-kf2v-flash", "baseUrl": "https://dashscope.aliyuncs.com", "apiKey": "key", "upstream": map[string]any{"kind": "vendor"}})
		require.NoError(t, err)
		assert.Equal(t, "https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis", descriptor["url"])
	})
}

// The OpenAI Images protocol claims every declared image model on both image
// endpoints; chat and video models keep the built-in relay.
func TestAlibabaOpenAIImageProtocolBindings(t *testing.T) {
	source, err := builtinplugins.Source("alibaba")
	require.NoError(t, err)
	registry := jsplugin.NewRegistry()
	plugin, err := registry.RegisterFactory(source, jsplugin.Options{Key: "alibaba"})
	require.NoError(t, err)
	for _, name := range []string{"qwen-image-3.0-pro", "qwen-image-plus", "qwen-image-edit-plus", "z-image-turbo", "wan2.6-t2i", "wan2.2-t2i-flash", "wan2.5-i2i-preview", "wanx2.1-imageedit", "qwen-image-edit-max-2026-01-16"} {
		for _, path := range []string{"/v1/images/generations", "/v1/images/edits"} {
			binding, found := registry.Generation().LookupEndpoint(http.MethodPost, path, name)
			require.True(t, found, name+" "+path)
			assert.Same(t, plugin, binding.Plugin)
			assert.Equal(t, "openai_image", binding.Protocol)
		}
		_, found := registry.Generation().LookupEndpoint(http.MethodPost, "/v1/videos", name)
		assert.False(t, found, name)
	}
	for _, name := range []string{"wan2.6-t2v", "qwen-turbo"} {
		_, found := registry.Generation().LookupEndpoint(http.MethodPost, "/v1/images/generations", name)
		assert.False(t, found, name)
	}
}

func decodeAlibabaImage(t *testing.T, plugin *jsplugin.LoadedPlugin, operation string, body map[string]any) (map[string]any, error) {
	t.Helper()
	path := "/v1/images/generations"
	if operation == "edit" {
		path = "/v1/images/edits"
	}
	value, err := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_image", "decodeRequest"}, map[string]any{
		"protocol": "openai_image", "operation": operation, "model": body["model"], "path": path, "method": http.MethodPost,
		"body": map[string]any{"kind": "json", "value": body},
	})
	if err != nil {
		return nil, err
	}
	return alibabaObject(t, value), nil
}

// OpenAI image requests map onto the DashScope services the models support,
// reserve the requested count, and reject counts the vendor would refuse
// before any quota is reserved.
func TestAlibabaOpenAIImageDecodeAndSubmission(t *testing.T) {
	plugin := newAlibabaPlugin(t)
	const base = "https://dashscope.aliyuncs.com/api/v1/services/aigc/"
	for _, tc := range []struct {
		name, model, operation string
		body                   map[string]any
		wantService, wantAsync string
		wantAction             string
		wantN                  float64
		wantFacts              map[string]any
		wantRatios             map[string]any
	}{
		{"qwen-image-3.0-pro two images", "qwen-image-3.0-pro", "generate", map[string]any{"prompt": "a white siamese cat", "n": 2, "size": "1024x1024"},
			"multimodal-generation/generation", "", "text_to_image", 2,
			map[string]any{"image_count": float64(2), "output_image_type": "qima_output_1k", "input_image_count": float64(0)}, map[string]any{"image_count": float64(2)}},
		{"qwen-image-3.0-pro without size reserves the 2K tier", "qwen-image-3.0", "generate", map[string]any{"prompt": "poster"},
			"multimodal-generation/generation", "", "text_to_image", 1,
			map[string]any{"image_count": float64(1), "output_image_type": "qima_output_2k", "input_image_count": float64(0)}, map[string]any{"image_count": float64(1)}},
		{"fixed count model", "qwen-image-plus", "generate", map[string]any{"prompt": "a cat", "size": "1328x1328"},
			"multimodal-generation/generation", "", "text_to_image", 1, map[string]any{"image_count": float64(1)}, map[string]any{"image_count": float64(1)}},
		{"z-image with prompt rewriting", "z-image-turbo", "generate", map[string]any{"prompt": "a cat", "prompt_extend": true},
			"multimodal-generation/generation", "", "text_to_image", 1,
			map[string]any{"image_count": float64(1), "prompt_extend": true}, map[string]any{"image_count": float64(1), "prompt_extend_ratio": float64(2)}},
		{"legacy text-to-image is asynchronous", "wan2.2-t2i-flash", "generate", map[string]any{"prompt": "a cat", "n": 2, "size": "1024x1024"},
			"text2image/image-synthesis", "enable", "text_to_image", 2, map[string]any{"image_count": float64(2)}, map[string]any{"image_count": float64(2)}},
		{"wan2.6 text-to-image is asynchronous by default", "wan2.6-t2i", "generate", map[string]any{"prompt": "a cat", "n": 2},
			"image-generation/generation", "enable", "text_to_image", 2, map[string]any{"image_count": float64(2)}, map[string]any{"image_count": float64(2)}},
		{"qwen edit with an image URL", "qwen-image-edit-plus", "edit", map[string]any{"prompt": "watercolor", "image": "https://cdn.example/input.png"},
			"multimodal-generation/generation", "", "image_to_image", 1, map[string]any{"image_count": float64(1)}, map[string]any{"image_count": float64(1)}},
		{"qwen-image-3.0 edit counts input images", "qwen-image-3.0-pro", "edit", map[string]any{"prompt": "watercolor", "image": []any{"https://cdn.example/1.png", "https://cdn.example/2.png"}, "size": "2048x2048"},
			"multimodal-generation/generation", "", "image_to_image", 1,
			map[string]any{"image_count": float64(1), "output_image_type": "qima_output_2k", "input_image_count": float64(2)}, map[string]any{"image_count": float64(1)}},
		{"wan2.5 legacy edit is asynchronous", "wan2.5-i2i-preview", "edit", map[string]any{"prompt": "watercolor", "image": "https://cdn.example/input.png", "n": 2},
			"image2image/image-synthesis", "enable", "image_to_image", 2, map[string]any{"image_count": float64(2)}, map[string]any{"image_count": float64(2)}},
		{"wanx2.1 function edit is asynchronous", "wanx2.1-imageedit", "edit", map[string]any{"prompt": "make the hair red", "image": "https://cdn.example/input.png", "n": 2},
			"image2image/image-synthesis", "enable", "image_to_image", 2, map[string]any{"image_count": float64(2)}, map[string]any{"image_count": float64(2)}},
		{"provider parameters pass through", "qwen-image-2.0-pro", "generate", map[string]any{"prompt": "a cat", "n": 3, "parameters": map[string]any{"negative_prompt": "blurry", "watermark": false}},
			"multimodal-generation/generation", "", "text_to_image", 3, map[string]any{"image_count": float64(3)}, map[string]any{"image_count": float64(3)}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			body := map[string]any{"model": tc.model}
			maps.Copy(body, tc.body)
			resolved, err := decodeAlibabaImage(t, plugin, tc.operation, body)
			require.NoError(t, err)
			assert.Equal(t, "submit", resolved["kind"])
			assert.Equal(t, tc.model, resolved["model"])
			assert.Equal(t, tc.wantAction, resolved["action"])
			request := resolved["requestBody"].(map[string]any)
			upstreamBody, facts, url := submitAlibabaRequest(t, plugin, tc.model, request)
			assert.Equal(t, base+tc.wantService, url)
			assert.Equal(t, tc.model, upstreamBody["model"])
			parameters := upstreamBody["parameters"].(map[string]any)
			assert.Equal(t, tc.wantN, parameters["n"], "the reserved count is sent explicitly")
			if size, ok := tc.body["size"].(string); ok {
				assert.Equal(t, strings.Replace(size, "x", "*", 1), parameters["size"])
			}
			if extra, ok := tc.body["parameters"].(map[string]any); ok {
				for key, expected := range alibabaObject(t, extra) {
					assert.Equal(t, expected, parameters[key], key)
				}
			}
			assert.Equal(t, tc.wantFacts, facts)
			value, err := plugin.Engine.Call(t.Context(), "extractUsage", map[string]any{"upstreamModel": tc.model, "model": tc.model, "usagePurpose": "billing_ratios", "requestBody": request})
			require.NoError(t, err)
			assert.Equal(t, tc.wantRatios, alibabaObject(t, value))
			value, err = plugin.Engine.Call(t.Context(), "buildSubmitRequest", map[string]any{"upstreamModel": tc.model, "requestBody": request, "baseUrl": "https://dashscope.aliyuncs.com", "apiKey": "k"})
			require.NoError(t, err)
			headers := alibabaObject(t, value)["headers"].(map[string]any)
			if tc.wantAsync == "" {
				assert.NotContains(t, headers, "X-DashScope-Async")
			} else {
				assert.Equal(t, tc.wantAsync, headers["X-DashScope-Async"])
			}
		})
	}

	t.Run("multipart edits address every uploaded file", func(t *testing.T) {
		value, err := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_image", "decodeRequest"}, map[string]any{
			"protocol": "openai_image", "operation": "edit", "model": "qwen-image-edit-plus", "path": "/v1/images/edits", "method": http.MethodPost,
			"body": map[string]any{"kind": "multipart",
				"fields": map[string][]string{"model": {"qwen-image-edit-plus"}, "prompt": {"watercolor"}, "n": {"2"}, "response_format": {"b64_json"}, "watermark": {"false"}},
				"files": []any{
					map[string]any{"ref": "request_file:image[]", "field": "image[]", "filename": "a.png", "mimeType": "image/png", "size": 10},
					map[string]any{"ref": "request_file:image[]#1", "field": "image[]", "filename": "b.jpg", "mimeType": "image/jpeg", "size": 10},
					map[string]any{"ref": "request_file:mask", "field": "mask", "filename": "m.png", "mimeType": "image/png", "size": 10},
				},
			},
		})
		require.NoError(t, err)
		resolved := alibabaObject(t, value)
		assert.Equal(t, "image_to_image", resolved["action"])
		request := resolved["requestBody"].(map[string]any)
		assert.Equal(t, float64(2), request["n"])
		assert.Equal(t, false, request["watermark"])
		images := request["images"].([]any)
		require.Len(t, images, 2, "mask uploads are not reference images")
		assert.Equal(t, map[string]any{"__fileRef": "request_file:image[]", "encoding": "dataUrl", "mimeType": "image/png", "maxBytes": float64(10485760)}, images[0])
		assert.Equal(t, "request_file:image[]#1", images[1].(map[string]any)["__fileRef"])
		driver := map[string]any{"upstreamModel": "qwen-image-edit-plus", "model": "qwen-image-edit-plus", "requestBody": request, "baseUrl": "https://dashscope.aliyuncs.com", "apiKey": "k", "usagePurpose": "facts"}
		value, err = plugin.Engine.Call(t.Context(), "buildSubmitRequest", driver)
		require.NoError(t, err)
		body := alibabaObject(t, value)["body"].(map[string]any)
		content := body["input"].(map[string]any)["messages"].([]any)[0].(map[string]any)["content"].([]any)
		require.Len(t, content, 3)
		assert.Equal(t, "request_file:image[]#1", content[1].(map[string]any)["image"].(map[string]any)["__fileRef"], "placeholders reach the upstream body for the host to inline")
		assert.Equal(t, "watercolor", content[2].(map[string]any)["text"])
		value, err = plugin.Engine.Call(t.Context(), "extractUsage", driver)
		require.NoError(t, err)
		assert.Equal(t, map[string]any{"image_count": float64(2)}, alibabaObject(t, value))
	})

	// wanx2.1-imageedit selects its operation with input.function. OpenAI edit
	// fields map onto the documented body: the image becomes base_image_url, a
	// mask becomes mask_image_url and selects local repainting, and the native
	// route forwards the DashScope body unchanged.
	t.Run("wanx2.1-imageedit maps OpenAI edit fields onto the function API", func(t *testing.T) {
		const model = "wanx2.1-imageedit"
		const image, mask = "https://cdn.example/base.png", "https://cdn.example/mask.png"
		for _, tc := range []struct {
			name           string
			body           map[string]any
			wantInput      map[string]any
			wantParameters map[string]any
		}{
			{"defaults to instruction editing", map[string]any{"prompt": "make the hair red", "image": image},
				map[string]any{"prompt": "make the hair red", "function": "description_edit", "base_image_url": image}, map[string]any{"n": float64(1)}},
			{"a mask selects local repainting", map[string]any{"prompt": "a ceramic rabbit", "image": image, "mask": mask, "n": 2},
				map[string]any{"prompt": "a ceramic rabbit", "function": "description_edit_with_mask", "base_image_url": image, "mask_image_url": mask}, map[string]any{"n": float64(2)}},
			{"explicit function with its parameters", map[string]any{"prompt": "a green fairy", "image": image, "function": "expand", "top_scale": 1.5, "watermark": true},
				map[string]any{"prompt": "a green fairy", "function": "expand", "base_image_url": image}, map[string]any{"n": float64(1), "top_scale": 1.5, "watermark": true}},
		} {
			body := map[string]any{"model": model}
			maps.Copy(body, tc.body)
			resolved, err := decodeAlibabaImage(t, plugin, "edit", body)
			require.NoError(t, err, tc.name)
			upstreamBody, facts, url := submitAlibabaRequest(t, plugin, model, resolved["requestBody"].(map[string]any))
			assert.Equal(t, base+"image2image/image-synthesis", url, tc.name)
			assert.Equal(t, tc.wantInput, upstreamBody["input"], tc.name)
			assert.Equal(t, tc.wantParameters, upstreamBody["parameters"], tc.name)
			assert.Equal(t, map[string]any{"image_count": tc.wantParameters["n"]}, facts, tc.name)
		}

		native, err := plugin.Engine.CallPath(t.Context(), "native", []string{"createImageTask"}, map[string]any{
			"path": "/ali/api/v1/services/aigc/image2image/image-synthesis",
			"body": map[string]any{"kind": "json", "value": map[string]any{"model": model, "input": map[string]any{"function": "super_resolution", "prompt": "sharpen", "base_image_url": image}, "parameters": map[string]any{"upscale_factor": 2, "n": 1}}},
		})
		require.NoError(t, err)
		upstreamBody, facts, url := submitAlibabaRequest(t, plugin, model, alibabaObject(t, native)["requestBody"].(map[string]any))
		assert.Equal(t, base+"image2image/image-synthesis", url)
		assert.Equal(t, map[string]any{"function": "super_resolution", "prompt": "sharpen", "base_image_url": image}, upstreamBody["input"])
		assert.Equal(t, map[string]any{"upscale_factor": float64(2), "n": float64(1)}, upstreamBody["parameters"])
		assert.Equal(t, map[string]any{"image_count": float64(1)}, facts)

		value, err := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_image", "decodeRequest"}, map[string]any{
			"protocol": "openai_image", "operation": "edit", "model": model, "path": "/v1/images/edits", "method": http.MethodPost,
			"body": map[string]any{"kind": "multipart",
				"fields": map[string][]string{"model": {model}, "prompt": {"a ceramic rabbit"}},
				"files": []any{
					map[string]any{"ref": "request_file:image", "field": "image", "filename": "a.png", "mimeType": "image/png", "size": 10},
					map[string]any{"ref": "request_file:mask", "field": "mask", "filename": "m.png", "mimeType": "image/png", "size": 10},
				},
			},
		})
		require.NoError(t, err)
		value, err = plugin.Engine.Call(t.Context(), "buildSubmitRequest", map[string]any{"upstreamModel": model, "requestBody": alibabaObject(t, value)["requestBody"], "baseUrl": "https://dashscope.aliyuncs.com", "apiKey": "k"})
		require.NoError(t, err)
		input := alibabaObject(t, value)["body"].(map[string]any)["input"].(map[string]any)
		assert.Equal(t, "description_edit_with_mask", input["function"], "a mask upload selects local repainting")
		assert.Equal(t, "request_file:image", input["base_image_url"].(map[string]any)["__fileRef"])
		assert.Equal(t, "request_file:mask", input["mask_image_url"].(map[string]any)["__fileRef"], "the mask placeholder reaches the upstream body for the host to inline")
	})

	for _, tc := range []struct {
		name, model, operation string
		body                   map[string]any
		decodeErr, submitErr   string
	}{
		{"wanx2.1-imageedit takes exactly one image", "wanx2.1-imageedit", "edit", map[string]any{"prompt": "x", "image": []any{"https://cdn.example/1.png", "https://cdn.example/2.png"}}, "", "exactly one input image"},
		{"wanx2.1-imageedit rejects unknown functions", "wanx2.1-imageedit", "edit", map[string]any{"prompt": "x", "image": "https://cdn.example/1.png", "function": "paint"}, "", "function must be one of"},
		{"wanx2.1-imageedit local repainting needs a mask", "wanx2.1-imageedit", "edit", map[string]any{"prompt": "x", "image": "https://cdn.example/1.png", "function": "description_edit_with_mask"}, "", "requires a mask image"},
		{"wanx2.1-imageedit has no size parameter", "wanx2.1-imageedit", "edit", map[string]any{"prompt": "x", "image": "https://cdn.example/1.png", "size": "1024x1024"}, "", "does not accept size"},
		{"wanx2.1-imageedit bounds n", "wanx2.1-imageedit", "edit", map[string]any{"prompt": "x", "image": "https://cdn.example/1.png", "n": 5}, "", "n must be an integer between 1 and 4"},
		{"fixed count model rejects n=2 before reservation", "qwen-image-plus", "generate", map[string]any{"prompt": "a cat", "n": 2}, "", "n must be 1 for this model"},
		{"z-image rejects n=2", "z-image-turbo", "generate", map[string]any{"prompt": "a cat", "n": 2}, "", "n must be 1 for this model"},
		{"qwen-image-3.0 bounds n", "qwen-image-3.0-pro", "generate", map[string]any{"prompt": "a cat", "n": 7}, "", "n must be an integer between 1 and 6"},
		{"legacy bounds n", "wan2.2-t2i-flash", "generate", map[string]any{"prompt": "a cat", "n": 5}, "", "n must be an integer between 1 and 4"},
		{"edit requires an image", "qwen-image-edit-plus", "edit", map[string]any{"prompt": "watercolor"}, "image is required", ""},
		{"edit-only model requires an image on generations", "qwen-image-edit", "generate", map[string]any{"prompt": "watercolor"}, "", "requires at least one input image"},
		{"text-to-image model rejects images", "qwen-image-plus", "edit", map[string]any{"prompt": "a cat", "image": "https://cdn.example/1.png"}, "", "only supports text-to-image input"},
		{"streaming is rejected", "qwen-image-plus", "generate", map[string]any{"prompt": "a cat", "stream": true}, "stream is not supported", ""},
		{"zero count is rejected", "qwen-image-3.0-pro", "generate", map[string]any{"prompt": "a cat", "n": 0}, "n must be a positive integer", ""},
		{"response_format is validated", "qwen-image-plus", "generate", map[string]any{"prompt": "a cat", "response_format": "png"}, "response_format must be url or b64_json", ""},
		{"prompt is required", "qwen-image-plus", "generate", map[string]any{}, "prompt is required", ""},
		{"size bounds follow the model", "z-image-turbo", "generate", map[string]any{"prompt": "a cat", "size": "256x256"}, "", "pixel and aspect-ratio limits"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			body := map[string]any{"model": tc.model}
			maps.Copy(body, tc.body)
			resolved, err := decodeAlibabaImage(t, plugin, tc.operation, body)
			if tc.decodeErr != "" {
				require.ErrorContains(t, err, tc.decodeErr)
				return
			}
			require.NoError(t, err)
			request := resolved["requestBody"].(map[string]any)
			_, err = plugin.Engine.Call(t.Context(), "buildSubmitRequest", map[string]any{"requestBody": request, "upstreamModel": tc.model})
			require.ErrorContains(t, err, tc.submitErr)
			info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: tc.model}, TaskRelayInfo: &relaycommon.TaskRelayInfo{}}
			adaptor := taskplugin.New(plugin)
			adaptor.Init(info)
			c, _ := gin.CreateTestContext(httptest.NewRecorder())
			c.Request = httptest.NewRequest(http.MethodPost, "/v1/images/generations", nil)
			c.Set("task_request", request)
			taskErr := adaptor.ValidateRequestAndSetAction(c, info)
			require.NotNil(t, taskErr, "the production validator must reject the request before billing")
			assert.Equal(t, http.StatusBadRequest, taskErr.StatusCode)
		})
	}
}

// Count derivation for synchronous image results: usage.image_count first,
// then the Qwen-Image-3.0 usage.output_image_count, then image payloads
// flattened across every choice; payload-less parts never count and an
// inconsistent count retains the reservation. The response renderer emits one
// OpenAI data entry per image, so the client sees as many images as are billed.
func TestAlibabaOpenAIImageCountDerivationAndRender(t *testing.T) {
	plugin := newAlibabaPlugin(t)
	const first, second = "https://cdn.example/first.png", "https://cdn.example/second.png"
	twoImagesOneChoice := []any{map[string]any{"finish_reason": "stop", "message": map[string]any{"role": "assistant", "content": []any{map[string]any{"image": first}, map[string]any{"image": second}}}}}
	for _, tc := range []struct {
		name, model string
		output      map[string]any
		usage       map[string]any
		wantFacts   map[string]any
		wantData    int
		wantErr     bool
	}{
		{"qwen-image-3.0 reports output_image_count", "qwen-image-3.0-pro", map[string]any{"choices": twoImagesOneChoice},
			map[string]any{"output_width": 1024, "output_height": 1024, "input_image_count": 0, "input_image_type": "qima_input_1k", "output_image_count": 2, "output_image_type": "qima_output_1k"},
			map[string]any{"image_count": float64(2), "output_image_type": "qima_output_1k", "input_image_count": float64(0)}, 2, false},
		{"one choice with two images and no usage counts payloads", "qwen-image-2.0-pro", map[string]any{"choices": twoImagesOneChoice}, nil,
			map[string]any{"image_count": float64(2)}, 2, false},
		{"usage.image_count is preferred", "wan2.6-t2i", map[string]any{"choices": twoImagesOneChoice}, map[string]any{"image_count": 2},
			map[string]any{"image_count": float64(2)}, 2, false},
		{"payload-less parts are not counted", "qwen-image-plus", map[string]any{"choices": []any{map[string]any{"message": map[string]any{"content": []any{map[string]any{"text": "revised"}, map[string]any{"image": first}, map[string]any{"image": ""}}}}}}, nil,
			map[string]any{"image_count": float64(1)}, 1, false},
		{"z-image keeps its count", "z-image-turbo", map[string]any{"choices": []any{map[string]any{"message": map[string]any{"content": []any{map[string]any{"image": first}}}}}}, map[string]any{"image_count": 1, "width": 1024, "height": 1536},
			map[string]any{"image_count": float64(1)}, 1, false},
		{"inconsistent zero count retains the reservation", "qwen-image-3.0-pro", map[string]any{"choices": twoImagesOneChoice}, map[string]any{"output_image_count": 0},
			nil, 2, true},
		{"fractional count retains the reservation", "wan2.6-t2i", map[string]any{"choices": twoImagesOneChoice}, map[string]any{"image_count": 1.5},
			nil, 2, true},
	} {
		t.Run(tc.name, func(t *testing.T) {
			body := map[string]any{"request_id": "r", "output": tc.output}
			if tc.usage != nil {
				body["usage"] = tc.usage
			}
			request := map[string]any{"model": tc.model, "prompt": "a cat", "n": 2}
			if tc.model == "qwen-image-plus" || tc.model == "z-image-turbo" {
				request["n"] = 1
			}
			if strings.HasPrefix(tc.model, "wan") {
				// Wan models default to the asynchronous service; the synchronous response shape needs the sync mode.
				request["metadata"] = map[string]any{"upstream_mode": "sync"}
			}
			info := &relaycommon.RelayInfo{OriginModelName: tc.model, ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: tc.model, ChannelBaseUrl: "https://dashscope.aliyuncs.com"}, TaskRelayInfo: &relaycommon.TaskRelayInfo{PublicTaskID: "task_public"}}
			adaptor := taskplugin.New(plugin)
			adaptor.Init(info)
			c, _ := gin.CreateTestContext(httptest.NewRecorder())
			c.Request = httptest.NewRequest(http.MethodPost, "/v1/images/generations", nil)
			c.Set("task_request", request)
			require.Nil(t, adaptor.ValidateRequestAndSetAction(c, info))
			encoded, err := common.Marshal(body)
			require.NoError(t, err)
			parsed, taskErr := adaptor.ParseResponse(c, &http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": {"application/json"}}, Body: io.NopCloser(bytes.NewReader(encoded))}, info)
			require.Nil(t, taskErr)
			require.NotNil(t, parsed.Immediate)
			assert.Equal(t, "SUCCESS", parsed.Immediate.Status)
			assert.Equal(t, "task_public", parsed.UpstreamTaskID, "synchronous results keep the host task id")
			if tc.wantErr {
				assert.Nil(t, parsed.Immediate.UsageFacts, "an unrecognized count must not replace the reservation")
			} else {
				assert.Equal(t, tc.wantFacts, parsed.Immediate.UsageFacts)
				ratios := adaptor.AdjustBillingOnSubmit(info, parsed.TaskData)
				assert.Equal(t, tc.wantFacts["image_count"], ratios["image_count"], "legacy per-call pricing settles to the same count")
			}
			value, err := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_image", "render"}, map[string]any{"model": tc.model}, map[string]any{"task_id": "task_public", "status": "SUCCESS", "created_at": 1700000000, "data": body})
			require.NoError(t, err)
			rendered := alibabaObject(t, value)
			assert.Equal(t, float64(1700000000), rendered["created"])
			data := rendered["data"].([]any)
			require.Len(t, data, tc.wantData)
			assert.Equal(t, first, data[0].(map[string]any)["url"])
			assert.NotContains(t, data[0], "b64_json")
		})
	}

	t.Run("legacy results and Base64 payloads render as OpenAI entries", func(t *testing.T) {
		body := map[string]any{"output": map[string]any{"task_status": "SUCCEEDED", "results": []any{map[string]any{"url": first}, map[string]any{"code": "DataInspectionFailed"}, map[string]any{"url": "data:image/png;base64,QUFB"}}}, "usage": map[string]any{"image_count": 2}}
		value, err := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_image", "render"}, map[string]any{}, map[string]any{"status": "SUCCESS", "created_at": 1, "data": body})
		require.NoError(t, err)
		rendered := alibabaObject(t, value)
		data := rendered["data"].([]any)
		require.Len(t, data, 2)
		assert.Equal(t, map[string]any{"url": first}, data[0])
		assert.Equal(t, map[string]any{"b64_json": "QUFB"}, data[1])
		// metadata keeps the upstream body so clients still see DashScope usage.
		assert.Equal(t, float64(2), rendered["metadata"].(map[string]any)["usage"].(map[string]any)["image_count"])
	})
}