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 }