Files
easyai-ai-gateway/apps/api/internal/runner/pricing_test.go
T
easyai 3056cf8fca
ci / verify (pull_request) Successful in 15m34s
feat(gateway): 补齐桌面端高级媒体直连接口
新增图片矢量化、视频超分、每日用量、计价与任务隔离能力,并通过环境变量解析平台凭据。

已通过 Go 全量门禁、迁移检查、镜像构建以及 Vectorizer 五格式和 Topaz 3 秒视频真实 DEV 验收。
2026-07-22 14:02:53 +08:00

354 lines
12 KiB
Go

package runner
import (
"context"
"testing"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func TestImageBillingEstimateUsesCountResolutionAndQuality(t *testing.T) {
service := &Service{}
candidate := store.RuntimeModelCandidate{
ModelName: "image-model",
BaseBillingConfig: map[string]any{
"image": map[string]any{
"basePrice": 10,
"dynamicWeight": map[string]any{
"resolutionFactors": map[string]any{"2K": 1.5},
"qualityFactors": map[string]any{"high": 1.5},
},
},
},
}
items := service.billings(context.Background(), nil, "images.generations", map[string]any{
"count": 2,
"quality": "high",
"resolution": "2K",
}, candidate, clients.Response{}, true)
line := firstBillingLine(t, items)
if got, want := floatFromAny(line["amount"]), 45.0; got != want {
t.Fatalf("image estimated amount = %v, want %v", got, want)
}
if got, want := line["quantity"], 2; got != want {
t.Fatalf("image quantity = %v, want %v", got, want)
}
}
func TestVideoBillingEstimateProratesFiveSecondUnitsAndDynamicWeights(t *testing.T) {
service := &Service{}
candidate := store.RuntimeModelCandidate{
ModelName: "video-model",
BaseBillingConfig: map[string]any{
"video": map[string]any{
"basePrice": 100,
"dynamicWeight": map[string]any{
"resolutionWeights": map[string]any{"720p": 1.25, "1080p": 1.5},
"audioWeights": map[string]any{"true": 2},
"referenceVideoWeights": map[string]any{"true": 1.5},
"voiceSpecifiedWeights": map[string]any{"true": 1.2},
"unusedCompatibilityField": map[string]any{"true": 99},
},
},
},
}
items := service.billings(context.Background(), nil, "videos.generations", map[string]any{
"audio": true,
"duration": 12,
"resolution": "1080P",
"voice_id": "voice-a",
"content": []any{
map[string]any{"type": "video_url", "video_url": map[string]any{"url": "https://example.com/reference.mp4"}},
},
}, candidate, clients.Response{}, true)
line := firstBillingLine(t, items)
if got, want := floatFromAny(line["amount"]), 1296.0; got != want {
t.Fatalf("video estimated amount = %v, want %v", got, want)
}
if got, want := floatFromAny(line["durationUnitCount"]), 2.4; got != want {
t.Fatalf("video duration units = %v, want %v", got, want)
}
if got, want := floatFromAny(line["quantity"]), 2.4; got != want {
t.Fatalf("video quantity = %v, want %v", got, want)
}
if got, want := line["durationSource"], "preprocessed_request"; got != want {
t.Fatalf("video duration source = %v, want %v", got, want)
}
if got, want := line["audioSource"], "preprocessed_request"; got != want {
t.Fatalf("video audio source = %v, want %v", got, want)
}
items = service.billings(context.Background(), nil, "videos.generations", map[string]any{
"duration": 5,
"resolution": "768P",
}, candidate, clients.Response{}, true)
line = firstBillingLine(t, items)
if got, want := floatFromAny(line["amount"]), 125.0; got != want {
t.Fatalf("768P should use 720p billing weight, got %v, want %v", got, want)
}
}
func TestAdvancedMediaBillingUsesConversionAndTransitionMatrix(t *testing.T) {
service := &Service{}
vectorCandidate := store.RuntimeModelCandidate{ModelName: "easy-image-vectorizer-1", BaseBillingConfig: map[string]any{
"image_vectorize": map[string]any{"basePrice": 200},
}}
vectorLine := firstBillingLine(t, service.billings(context.Background(), nil, "images.vectorize", map[string]any{"count": 1}, vectorCandidate, clients.Response{}, true))
if vectorLine["resourceType"] != "image_vectorize" || vectorLine["unit"] != "conversion" || floatFromAny(vectorLine["amount"]) != 200 {
t.Fatalf("unexpected vectorizer billing: %+v", vectorLine)
}
topazCandidate := store.RuntimeModelCandidate{ModelName: "easy-proteus-standard-4", BaseBillingConfig: map[string]any{
"video_enhance": map[string]any{
"basePrice": 100,
"dynamicWeight": map[string]any{
"resolutionTransitions": map[string]any{"720p->1080p": 1.8},
"frameRateTransitions": map[string]any{"24->60": 2.5},
"markup": 1,
},
},
}}
topazLine := firstBillingLine(t, service.billings(context.Background(), nil, "videos.upscales", map[string]any{
"count": 1, "duration": 5, "source_resolution": "720p", "target_resolution": "1080p",
"source_frame_rate": 24, "target_frame_rate": 60, "slow_motion_rate": 2,
"model": "easy-proteus-standard-4",
}, topazCandidate, clients.Response{}, true))
if got, want := floatFromAny(topazLine["amount"]), 900.0; got != want {
t.Fatalf("video enhance amount=%v, want=%v, line=%+v", got, want, topazLine)
}
if topazLine["resolutionTransitionKey"] != "720p->1080p" || topazLine["frameRateTransitionKey"] != "24->60" {
t.Fatalf("missing transition evidence: %+v", topazLine)
}
}
func TestVideoEnhancePricingFallbackUsesPixelsAndBaseline(t *testing.T) {
amount, details := videoEnhanceAmount(map[string]any{"video_enhance": map[string]any{"dynamicWeight": map[string]any{}}}, videoEnhancePricingInputs{
DurationSeconds: 5, DurationUnits: 1, SourceResolution: "720p", TargetResolution: "2160p",
SourceFrameRate: 30, TargetFrameRate: 60, SlowMotionRate: 1,
}, 1, 100)
if amount != 2250 {
t.Fatalf("fallback amount=%v, want=2250 details=%+v", amount, details)
}
}
func TestVideoEnhancePricingPrefersProbedDimensions(t *testing.T) {
inputs := resolveVideoEnhancePricingInputs(map[string]any{
"duration": 3,
"source_resolution": "180p",
"source_width": 320,
"source_height": 180,
"target_resolution": "720p",
"output_width": 1280,
"output_height": 720,
}, clients.Response{})
if inputs.SourceResolution != "480p" || inputs.TargetResolution != "720p" {
t.Fatalf("dimension buckets = %s->%s, want 480p->720p", inputs.SourceResolution, inputs.TargetResolution)
}
}
func TestMusicBillingUsesSongResourceAndOutputCount(t *testing.T) {
service := &Service{}
candidate := store.RuntimeModelCandidate{
ModelName: "suno-model",
BaseBillingConfig: map[string]any{
"musicBase": 6,
"music": map[string]any{"basePrice": 9},
},
}
items := service.billings(context.Background(), nil, "song.generations", map[string]any{
"prompt": "city lights",
"count": 3,
}, candidate, clients.Response{}, true)
line := firstBillingLine(t, items)
if got, want := line["resourceType"], "music"; got != want {
t.Fatalf("music resource type = %v, want %v", got, want)
}
if got, want := line["unit"], "song"; got != want {
t.Fatalf("music billing unit = %v, want %v", got, want)
}
if got, want := line["quantity"], 3; got != want {
t.Fatalf("music quantity = %v, want %v", got, want)
}
if got, want := floatFromAny(line["amount"]), 18.0; got != want {
t.Fatalf("music amount = %v, want %v", got, want)
}
}
func TestSpeechBillingUsesAudioCharacters(t *testing.T) {
service := &Service{}
candidate := store.RuntimeModelCandidate{
ModelName: "speech-model",
BaseBillingConfig: map[string]any{
"audioBase": 0.5,
"audio": map[string]any{"basePrice": 0.8},
},
}
items := service.billings(context.Background(), nil, "speech.generations", map[string]any{
"text": "你好abc",
"voice_id": "female-shaonv",
}, candidate, clients.Response{}, true)
line := firstBillingLine(t, items)
if got, want := line["resourceType"], "audio"; got != want {
t.Fatalf("speech resource type = %v, want %v", got, want)
}
if got, want := line["unit"], "character"; got != want {
t.Fatalf("speech billing unit = %v, want %v", got, want)
}
if got, want := line["quantity"], 5; got != want {
t.Fatalf("speech character quantity = %v, want %v", got, want)
}
if got, want := floatFromAny(line["amount"]), 2.5; got != want {
t.Fatalf("speech amount = %v, want %v", got, want)
}
}
func TestVideoBillingPrefersGeneratedDuration(t *testing.T) {
service := &Service{}
candidate := store.RuntimeModelCandidate{
ModelName: "video-model",
BaseBillingConfig: map[string]any{
"video": map[string]any{"basePrice": 100},
},
}
items := service.billings(context.Background(), nil, "videos.generations", map[string]any{
"duration": 12,
"resolution": "720p",
}, candidate, clients.Response{
Result: map[string]any{
"data": []any{map[string]any{"type": "video", "duration": 6.6}},
},
}, false)
line := firstBillingLine(t, items)
if got, want := floatFromAny(line["durationSeconds"]), 6.6; got != want {
t.Fatalf("video generated duration = %v, want %v", got, want)
}
if got, want := floatFromAny(line["durationUnitCount"]), 1.32; got != want {
t.Fatalf("video generated duration units = %v, want %v", got, want)
}
if got, want := floatFromAny(line["amount"]), 132.0; got != want {
t.Fatalf("video generated duration amount = %v, want %v", got, want)
}
if got, want := line["durationSource"], "generated_video"; got != want {
t.Fatalf("video duration source = %v, want %v", got, want)
}
}
func TestVideoBillingPrefersGeneratedAudio(t *testing.T) {
service := &Service{}
candidate := store.RuntimeModelCandidate{
ModelName: "video-model",
BaseBillingConfig: map[string]any{
"video": map[string]any{
"basePrice": 100,
"audioWeights": map[string]any{"true": 2, "false": 0.5},
"voiceSpecifiedWeights": map[string]any{"true": 4},
},
},
}
items := service.billings(context.Background(), nil, "videos.generations", map[string]any{
"audio": false,
"duration": 5,
}, candidate, clients.Response{
Result: map[string]any{
"data": []any{map[string]any{"type": "video", "has_audio": true}},
},
}, false)
line := firstBillingLine(t, items)
if got, want := floatFromAny(line["amount"]), 200.0; got != want {
t.Fatalf("video generated audio amount = %v, want %v", got, want)
}
if got, want := line["audio"], true; got != want {
t.Fatalf("video generated audio = %v, want %v", got, want)
}
if got, want := line["audioSource"], "generated_video"; got != want {
t.Fatalf("video audio source = %v, want %v", got, want)
}
items = service.billings(context.Background(), nil, "videos.generations", map[string]any{
"audio": true,
"duration": 5,
"voice_id": "voice-a",
}, candidate, clients.Response{
Result: map[string]any{
"data": []any{map[string]any{"type": "video", "hasAudio": false}},
},
}, false)
line = firstBillingLine(t, items)
if got, want := floatFromAny(line["amount"]), 50.0; got != want {
t.Fatalf("video generated no-audio amount = %v, want %v", got, want)
}
if got, want := line["audio"], false; got != want {
t.Fatalf("video generated no-audio = %v, want %v", got, want)
}
if got, want := line["audioSource"], "generated_video"; got != want {
t.Fatalf("video no-audio source = %v, want %v", got, want)
}
}
func TestVideoBillingEstimateSupportsServerMainStyleDynamicKeys(t *testing.T) {
service := &Service{}
candidate := store.RuntimeModelCandidate{
ModelName: "legacy-video-model",
BaseBillingConfig: map[string]any{
"videoBase": 100,
"video": map[string]any{
"dynamicWeight": map[string]any{
"720p": 1.25,
"audio-true": 2,
"reference-video-true": 1.5,
},
},
},
}
items := service.billings(context.Background(), nil, "videos.generations", map[string]any{
"audio": "true",
"duration": 5,
"resolution": "720p",
"video_list": []any{map[string]any{"url": "https://example.com/reference.mp4"}},
}, candidate, clients.Response{}, true)
line := firstBillingLine(t, items)
if got, want := floatFromAny(line["amount"]), 375.0; got != want {
t.Fatalf("legacy video estimated amount = %v, want %v", got, want)
}
}
func TestVideoDurationEstimateSumsMultiShotDurations(t *testing.T) {
duration := requestDurationSeconds(map[string]any{
"multi_prompt": []any{
map[string]any{"prompt": "shot 1", "duration": 3},
map[string]any{"prompt": "shot 2", "duration": 7},
},
})
if duration != 10 {
t.Fatalf("multi-shot duration = %v, want 10", duration)
}
}
func firstBillingLine(t *testing.T, items []any) map[string]any {
t.Helper()
if len(items) != 1 {
t.Fatalf("items length = %d, want 1: %+v", len(items), items)
}
line, ok := items[0].(map[string]any)
if !ok {
t.Fatalf("item type = %T, want map[string]any", items[0])
}
return line
}