Restore standard MiniMax resolutions and client mapping
This commit is contained in:
parent
ca5e71c8e8
commit
97f29ed156
@ -5,6 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@ -157,9 +158,147 @@ func minimaxSpec() providerTaskSpec {
|
||||
StatusPaths: []string{"status"},
|
||||
SuccessStatuses: []string{"success"},
|
||||
FailureStatuses: []string{"failed", "expired"},
|
||||
DefaultSubmitBody: func(request Request, body map[string]any) map[string]any {
|
||||
return minimaxVideoPayload(request, body)
|
||||
},
|
||||
ResolveSuccess: minimaxResolveVideoFile,
|
||||
}
|
||||
}
|
||||
|
||||
func minimaxVideoPayload(request Request, body map[string]any) map[string]any {
|
||||
model := upstreamModelName(request.Candidate)
|
||||
body["model"] = model
|
||||
if strings.TrimSpace(stringFromAny(body["prompt"])) == "" {
|
||||
body["prompt"] = mediaPromptText(body)
|
||||
}
|
||||
firstFrame, lastFrame := minimaxFrameImages(body)
|
||||
if firstFrame != "" && strings.TrimSpace(stringFromAny(body["first_frame_image"])) == "" {
|
||||
body["first_frame_image"] = firstFrame
|
||||
}
|
||||
if lastFrame != "" && strings.TrimSpace(stringFromAny(body["last_frame_image"])) == "" {
|
||||
body["last_frame_image"] = lastFrame
|
||||
}
|
||||
if resolution := minimaxVideoResolution(model, stringFromAny(body["resolution"])); resolution != "" {
|
||||
body["resolution"] = resolution
|
||||
}
|
||||
for _, key := range []string{
|
||||
"content", "input", "_paramWarnings",
|
||||
"first_frame", "firstFrame", "last_frame", "lastFrame",
|
||||
"image", "images", "image_url", "imageUrl", "image_urls", "imageUrls",
|
||||
"reference_image", "referenceImage", "duration_seconds", "durationSeconds",
|
||||
} {
|
||||
delete(body, key)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func minimaxFrameImages(body map[string]any) (string, string) {
|
||||
firstFrame := firstNonEmptyStringValue(body, "first_frame_image", "firstFrameImage", "first_frame", "firstFrame")
|
||||
lastFrame := firstNonEmptyStringValue(body, "last_frame_image", "lastFrameImage", "last_frame", "lastFrame")
|
||||
imageURLs := firstNonEmptyStringListFromAny(body["image"], body["images"], body["image_url"], body["imageUrl"], body["image_urls"], body["imageUrls"])
|
||||
if firstFrame == "" && len(imageURLs) > 0 {
|
||||
firstFrame = imageURLs[0]
|
||||
}
|
||||
for _, item := range contentItems(body["content"]) {
|
||||
if strings.TrimSpace(stringFromAny(item["type"])) != "image_url" {
|
||||
continue
|
||||
}
|
||||
url := minimaxNestedURL(item, "image_url")
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
switch strings.TrimSpace(stringFromAny(item["role"])) {
|
||||
case "first_frame":
|
||||
if firstFrame == "" {
|
||||
firstFrame = url
|
||||
}
|
||||
case "last_frame":
|
||||
if lastFrame == "" {
|
||||
lastFrame = url
|
||||
}
|
||||
default:
|
||||
if firstFrame == "" {
|
||||
firstFrame = url
|
||||
}
|
||||
}
|
||||
}
|
||||
return firstFrame, lastFrame
|
||||
}
|
||||
|
||||
func minimaxNestedURL(item map[string]any, key string) string {
|
||||
if url := strings.TrimSpace(stringFromAny(item[key])); url != "" {
|
||||
return url
|
||||
}
|
||||
nested := mapFromAny(item[key])
|
||||
return strings.TrimSpace(stringFromAny(nested["url"]))
|
||||
}
|
||||
|
||||
func minimaxVideoResolution(model string, resolution string) string {
|
||||
resolution = strings.TrimSpace(resolution)
|
||||
if resolution == "" {
|
||||
return ""
|
||||
}
|
||||
normalized := strings.ToLower(strings.ReplaceAll(resolution, " ", ""))
|
||||
isHailuo23 := strings.Contains(strings.ToLower(model), "hailuo-2.3")
|
||||
if isHailuo23 {
|
||||
switch normalized {
|
||||
case "720", "720p", "768", "768p":
|
||||
return "768P"
|
||||
case "1080", "1080p":
|
||||
return "1080P"
|
||||
}
|
||||
}
|
||||
switch normalized {
|
||||
case "512", "512p":
|
||||
return "512P"
|
||||
case "720", "720p":
|
||||
return "720P"
|
||||
case "768", "768p":
|
||||
return "768P"
|
||||
case "1080", "1080p":
|
||||
return "1080P"
|
||||
default:
|
||||
if strings.HasSuffix(normalized, "p") {
|
||||
return strings.TrimSuffix(normalized, "p") + "P"
|
||||
}
|
||||
return resolution
|
||||
}
|
||||
}
|
||||
|
||||
func minimaxResolveVideoFile(ctx context.Context, client *http.Client, request Request, result map[string]any) (map[string]any, string, error) {
|
||||
if len(providerTaskData(request, result)) > 0 {
|
||||
return result, "", nil
|
||||
}
|
||||
fileID := strings.TrimSpace(stringFromPathValue(valueAtPath(result, "file_id")))
|
||||
if fileID == "" {
|
||||
return result, "", nil
|
||||
}
|
||||
fetched, requestID, err := providerGetJSON(ctx, client, providerURL(request.Candidate.BaseURL, "/files/retrieve?file_id="+url.QueryEscape(fileID)), request.Candidate.Credentials, "bearer")
|
||||
if err != nil {
|
||||
return nil, requestID, err
|
||||
}
|
||||
if isProviderTaskFailure(minimaxSpec(), fetched) {
|
||||
return nil, requestID, providerTaskFailure(minimaxSpec(), fetched, firstNonEmptyString(requestID, requestIDFromResult(fetched)), time.Now())
|
||||
}
|
||||
downloadURL := firstNonEmptyString(
|
||||
valueAtPath(fetched, "file.download_url"),
|
||||
valueAtPath(fetched, "file.downloadUrl"),
|
||||
valueAtPath(fetched, "download_url"),
|
||||
valueAtPath(fetched, "downloadUrl"),
|
||||
valueAtPath(fetched, "url"),
|
||||
)
|
||||
if downloadURL == "" {
|
||||
return nil, requestID, &ClientError{Code: "invalid_response", Message: "minimax video download url is missing", RequestID: requestID, Retryable: false}
|
||||
}
|
||||
out := cloneMapAny(result)
|
||||
out["video_url"] = downloadURL
|
||||
if file, ok := fetched["file"].(map[string]any); ok {
|
||||
out["file"] = cloneMapAny(file)
|
||||
}
|
||||
out["file_retrieve"] = cloneMapAny(fetched)
|
||||
return out, firstNonEmptyString(requestID, requestIDFromResult(fetched)), nil
|
||||
}
|
||||
|
||||
func (c MinimaxClient) runSpeech(ctx context.Context, request Request) (Response, error) {
|
||||
startedAt := time.Now()
|
||||
payload := minimaxSpeechPayload(request)
|
||||
|
||||
@ -21,6 +21,7 @@ type providerTaskSpec struct {
|
||||
FailureStatuses []string
|
||||
ProcessStatuses []string
|
||||
DefaultSubmitBody func(Request, map[string]any) map[string]any
|
||||
ResolveSuccess func(context.Context, *http.Client, Request, map[string]any) (map[string]any, string, error)
|
||||
}
|
||||
|
||||
type providerTaskClient struct {
|
||||
@ -54,6 +55,15 @@ func (c providerTaskClient) Run(ctx context.Context, request Request) (Response,
|
||||
return Response{}, providerTaskFailure(c.Spec, result, requestID, startedAt)
|
||||
}
|
||||
if isProviderTaskSuccess(c.Spec, result) && hasProviderTaskResult(result) {
|
||||
resolved, resolvedRequestID, err := c.resolveSuccess(ctx, request, result)
|
||||
if err != nil {
|
||||
return Response{}, annotateResponseError(err, firstNonEmptyString(resolvedRequestID, requestID), startedAt, time.Now())
|
||||
}
|
||||
result = resolved
|
||||
requestID = firstNonEmptyString(resolvedRequestID, requestID)
|
||||
if isProviderTaskFailure(c.Spec, result) {
|
||||
return Response{}, providerTaskFailure(c.Spec, result, requestID, startedAt)
|
||||
}
|
||||
return Response{
|
||||
Result: normalizeProviderTaskResult(request, c.Spec, result, ""),
|
||||
RequestID: requestID,
|
||||
@ -96,6 +106,15 @@ func (c providerTaskClient) Run(ctx context.Context, request Request) (Response,
|
||||
lastResult = result
|
||||
requestID = firstNonEmptyString(pollRequestID, requestID, requestIDFromResult(result), upstreamTaskID)
|
||||
if isProviderTaskSuccess(c.Spec, result) {
|
||||
resolved, resolvedRequestID, err := c.resolveSuccess(ctx, request, result)
|
||||
if err != nil {
|
||||
return Response{}, annotateResponseError(err, firstNonEmptyString(resolvedRequestID, requestID, upstreamTaskID), pollStarted, time.Now())
|
||||
}
|
||||
result = resolved
|
||||
requestID = firstNonEmptyString(resolvedRequestID, requestID)
|
||||
if isProviderTaskFailure(c.Spec, result) {
|
||||
return Response{}, providerTaskFailure(c.Spec, result, requestID, startedAt)
|
||||
}
|
||||
finishedAt := time.Now()
|
||||
return Response{
|
||||
Result: normalizeProviderTaskResult(request, c.Spec, result, upstreamTaskID),
|
||||
@ -119,6 +138,13 @@ func (c providerTaskClient) Run(ctx context.Context, request Request) (Response,
|
||||
}
|
||||
}
|
||||
|
||||
func (c providerTaskClient) resolveSuccess(ctx context.Context, request Request, result map[string]any) (map[string]any, string, error) {
|
||||
if c.Spec.ResolveSuccess == nil {
|
||||
return result, "", nil
|
||||
}
|
||||
return c.Spec.ResolveSuccess(ctx, httpClient(request.HTTPClient, c.HTTPClient), request, result)
|
||||
}
|
||||
|
||||
func providerTaskKindSupported(kind string) bool {
|
||||
switch kind {
|
||||
case "images.generations", "images.edits", "videos.generations", "song.generations", "music.generations", "speech.generations":
|
||||
@ -282,9 +308,16 @@ func isProviderTaskSuccess(spec providerTaskSpec, result map[string]any) bool {
|
||||
}
|
||||
|
||||
func isProviderTaskFailure(spec providerTaskSpec, result map[string]any) bool {
|
||||
if statusCode := providerTaskBusinessStatusCode(result); statusCode != "" && statusCode != "0" {
|
||||
return true
|
||||
}
|
||||
return containsStatus(append([]string{"failed", "failure", "error", "cancelled", "canceled", "fail", "expired", "task not found"}, spec.FailureStatuses...), providerTaskStatus(spec, result))
|
||||
}
|
||||
|
||||
func providerTaskBusinessStatusCode(result map[string]any) string {
|
||||
return strings.TrimSpace(stringFromPathValue(valueAtPath(result, "base_resp.status_code")))
|
||||
}
|
||||
|
||||
func containsStatus(values []string, status string) bool {
|
||||
status = strings.ToLower(strings.TrimSpace(status))
|
||||
for _, value := range values {
|
||||
@ -397,9 +430,9 @@ func appendURLValues(out *[]any, value any) {
|
||||
}
|
||||
|
||||
func providerTaskFailure(spec providerTaskSpec, result map[string]any, requestID string, startedAt time.Time) error {
|
||||
message := firstNonEmptyString(valueAtPath(result, "message"), valueAtPath(result, "error.message"), valueAtPath(result, "error"), valueAtPath(result, "Response.ErrorMessage"), valueAtPath(result, "comment"), spec.Name+" task failed")
|
||||
message := firstNonEmptyString(valueAtPath(result, "message"), valueAtPath(result, "error.message"), valueAtPath(result, "error"), valueAtPath(result, "base_resp.status_msg"), valueAtPath(result, "Response.ErrorMessage"), valueAtPath(result, "comment"), spec.Name+" task failed")
|
||||
return &ClientError{
|
||||
Code: firstNonEmptyString(valueAtPath(result, "code"), valueAtPath(result, "error_code"), valueAtPath(result, "Response.ErrorCode"), "provider_failed"),
|
||||
Code: firstNonEmptyPathString(valueAtPath(result, "code"), valueAtPath(result, "error_code"), valueAtPath(result, "base_resp.status_code"), valueAtPath(result, "Response.ErrorCode"), "provider_failed"),
|
||||
Message: message,
|
||||
RequestID: requestID,
|
||||
ResponseStartedAt: startedAt,
|
||||
@ -409,6 +442,15 @@ func providerTaskFailure(spec providerTaskSpec, result map[string]any, requestID
|
||||
}
|
||||
}
|
||||
|
||||
func firstNonEmptyPathString(values ...any) string {
|
||||
for _, value := range values {
|
||||
if text := stringFromPathValue(value); text != "" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func providerPollInterval(request Request) time.Duration {
|
||||
return durationFromConfig(request.Candidate.PlatformConfig, 2*time.Second, "pollIntervalMs", "poll_interval_ms")
|
||||
}
|
||||
|
||||
@ -228,6 +228,75 @@ func TestProviderTaskClientsSubmitAndPoll(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinimaxClientNormalizesHailuo23PayloadAndRetrievesFile(t *testing.T) {
|
||||
var submitted map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "Bearer test-key" {
|
||||
t.Fatalf("unexpected auth header: %q", r.Header.Get("Authorization"))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("x-request-id", "req-minimax")
|
||||
switch {
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/video_generation":
|
||||
if err := json.NewDecoder(r.Body).Decode(&submitted); err != nil {
|
||||
t.Fatalf("decode minimax submit request: %v", err)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"task_id":"mm-task","base_resp":{"status_code":0,"status_msg":"success"}}`))
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/query/video_generation" && r.URL.Query().Get("task_id") == "mm-task":
|
||||
_, _ = w.Write([]byte(`{"task_id":"mm-task","status":"Success","file_id":"file-1","base_resp":{"status_code":0,"status_msg":"success"}}`))
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/files/retrieve" && r.URL.Query().Get("file_id") == "file-1":
|
||||
_, _ = w.Write([]byte(`{"file":{"download_url":"https://cdn.example/minimax-file.mp4"},"base_resp":{"status_code":0,"status_msg":"success"}}`))
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
response, err := (MinimaxClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
|
||||
Kind: "videos.generations",
|
||||
ModelType: "image_to_video",
|
||||
Model: "海螺2.3",
|
||||
Body: map[string]any{
|
||||
"resolution": "720p",
|
||||
"duration": 6,
|
||||
"content": []any{
|
||||
map[string]any{"type": "text", "text": "camera moves in"},
|
||||
map[string]any{"type": "image_url", "role": "first_frame", "image_url": map[string]any{"url": "https://example.com/first.png"}},
|
||||
},
|
||||
},
|
||||
Candidate: store.RuntimeModelCandidate{
|
||||
Provider: "minimax",
|
||||
SpecType: "minimax",
|
||||
BaseURL: server.URL,
|
||||
Credentials: map[string]any{"apiKey": "test-key"},
|
||||
PlatformConfig: map[string]any{"pollIntervalMs": 1, "pollTimeoutMs": 1000},
|
||||
ModelName: "海螺2.3",
|
||||
ProviderModelName: "MiniMax-Hailuo-2.3",
|
||||
ModelType: "image_to_video",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("run minimax client: %v", err)
|
||||
}
|
||||
if submitted["model"] != "MiniMax-Hailuo-2.3" || submitted["prompt"] != "camera moves in" || submitted["first_frame_image"] != "https://example.com/first.png" {
|
||||
t.Fatalf("unexpected minimax submit payload: %+v", submitted)
|
||||
}
|
||||
if submitted["resolution"] != "768P" {
|
||||
t.Fatalf("hailuo 2.3 720p should be submitted as 768P, got %+v", submitted)
|
||||
}
|
||||
if _, ok := submitted["content"]; ok {
|
||||
t.Fatalf("minimax native request should not include generic content: %+v", submitted)
|
||||
}
|
||||
data, _ := response.Result["data"].([]any)
|
||||
if len(data) != 1 {
|
||||
t.Fatalf("unexpected minimax response data: %+v", response.Result)
|
||||
}
|
||||
first, _ := data[0].(map[string]any)
|
||||
if first["url"] != "https://cdn.example/minimax-file.mp4" {
|
||||
t.Fatalf("unexpected minimax video url: %+v", response.Result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSunoClientSubmitsAndPollsAudioGeneration(t *testing.T) {
|
||||
var submitted map[string]any
|
||||
var submittedRemoteTaskID string
|
||||
@ -303,6 +372,39 @@ func TestSunoClientSubmitsAndPollsAudioGeneration(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProviderTaskClientFailureAndRetryableErrors(t *testing.T) {
|
||||
t.Run("submit business failure", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("x-request-id", "req-minimax-invalid")
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/video_generation" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"base_resp":{"status_code":1008,"status_msg":"invalid resolution"}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
_, err := (MinimaxClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
|
||||
Kind: "videos.generations",
|
||||
ModelType: "video_generate",
|
||||
Model: "海螺2.3",
|
||||
Body: map[string]any{"model": "海螺2.3", "prompt": "hello"},
|
||||
Candidate: store.RuntimeModelCandidate{
|
||||
Provider: "minimax",
|
||||
SpecType: "minimax",
|
||||
BaseURL: server.URL,
|
||||
Credentials: map[string]any{"apiKey": "test-key"},
|
||||
PlatformConfig: map[string]any{"pollIntervalMs": 1, "pollTimeoutMs": 1000},
|
||||
ModelName: "海螺2.3",
|
||||
ProviderModelName: "MiniMax-Hailuo-2.3",
|
||||
ModelType: "video_generate",
|
||||
},
|
||||
})
|
||||
var clientErr *ClientError
|
||||
if !errors.As(err, &clientErr) || clientErr.Code != "1008" || clientErr.Message != "invalid resolution" || clientErr.RequestID != "req-minimax-invalid" {
|
||||
t.Fatalf("expected minimax business failure, got %#v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("poll failure", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@ -28,6 +28,38 @@ func TestFilterRuntimeCandidatesByRequestResolutionKeepsSupportedCandidate(t *te
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterRuntimeCandidatesUsesStandardVideoResolutionValues(t *testing.T) {
|
||||
candidates := []store.RuntimeModelCandidate{
|
||||
{
|
||||
PlatformID: "minimax",
|
||||
PlatformKey: "minimax",
|
||||
PlatformName: "MiniMax",
|
||||
PlatformModelID: "hailuo23",
|
||||
ModelName: "MiniMax-Hailuo-2.3",
|
||||
ModelAlias: "海螺2.3",
|
||||
ModelType: "video_generate",
|
||||
Capabilities: map[string]any{
|
||||
"video_generate": map[string]any{
|
||||
"output_resolutions": []any{"720p", "1080p"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
filtered, summary, err := filterRuntimeCandidatesByRequest("videos.generations", "海螺2.3", "video_generate", map[string]any{
|
||||
"resolution": "720p",
|
||||
}, candidates)
|
||||
if err != nil {
|
||||
t.Fatalf("standard 720p request should match MiniMax catalog capability before client-side upstream mapping: %v", err)
|
||||
}
|
||||
if len(filtered) != 1 || filtered[0].PlatformModelID != "hailuo23" {
|
||||
t.Fatalf("expected MiniMax candidate to remain, got %+v", filtered)
|
||||
}
|
||||
if summary["supportedCandidateCount"] != 1 || summary["filteredCandidateCount"] != 0 {
|
||||
t.Fatalf("unexpected filter summary: %+v", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterRuntimeCandidatesByScopedVideoResolution(t *testing.T) {
|
||||
candidates := []store.RuntimeModelCandidate{
|
||||
{
|
||||
|
||||
@ -323,6 +323,19 @@ func weightValueAliases(key string, name string) []string {
|
||||
return nil
|
||||
}
|
||||
switch key {
|
||||
case "resolutionWeights":
|
||||
aliases := []string{name}
|
||||
lower := strings.ToLower(name)
|
||||
if lower != name {
|
||||
aliases = append(aliases, lower)
|
||||
}
|
||||
if strings.HasSuffix(lower, "p") {
|
||||
aliases = append(aliases, strings.TrimSuffix(lower, "p")+"P")
|
||||
}
|
||||
if lower == "768p" {
|
||||
aliases = append(aliases, "720p")
|
||||
}
|
||||
return aliases
|
||||
case "audioWeights":
|
||||
return []string{name, "audio-" + name}
|
||||
case "referenceVideoWeights":
|
||||
|
||||
@ -46,7 +46,7 @@ func TestVideoBillingEstimateUsesFiveSecondUnitsAndDynamicWeights(t *testing.T)
|
||||
"video": map[string]any{
|
||||
"basePrice": 100,
|
||||
"dynamicWeight": map[string]any{
|
||||
"resolutionWeights": map[string]any{"1080p": 1.5},
|
||||
"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},
|
||||
@ -59,7 +59,7 @@ func TestVideoBillingEstimateUsesFiveSecondUnitsAndDynamicWeights(t *testing.T)
|
||||
items := service.billings(context.Background(), nil, "videos.generations", map[string]any{
|
||||
"audio": true,
|
||||
"duration": 12,
|
||||
"resolution": "1080p",
|
||||
"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"}},
|
||||
@ -82,6 +82,15 @@ func TestVideoBillingEstimateUsesFiveSecondUnitsAndDynamicWeights(t *testing.T)
|
||||
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 TestMusicBillingUsesSongResourceAndOutputCount(t *testing.T) {
|
||||
|
||||
123
apps/api/migrations/0048_minimax_hailuo23_capabilities.sql
Normal file
123
apps/api/migrations/0048_minimax_hailuo23_capabilities.sql
Normal file
@ -0,0 +1,123 @@
|
||||
WITH minimax_hailuo_defs(provider_model_name, capabilities) AS (
|
||||
VALUES
|
||||
(
|
||||
'MiniMax-Hailuo-2.3',
|
||||
'{
|
||||
"video_generate": {
|
||||
"output_resolutions": ["720p", "1080p"],
|
||||
"duration_range": {"720p": [6, 10], "1080p": [6, 6]},
|
||||
"duration_options": {"720p": [6, 10], "1080p": [6]},
|
||||
"aspect_ratio_allowed": []
|
||||
},
|
||||
"image_to_video": {
|
||||
"output_resolutions": ["720p", "1080p"],
|
||||
"duration_range": {"720p": [6, 10], "1080p": [6, 6]},
|
||||
"duration_options": {"720p": [6, 10], "1080p": [6]},
|
||||
"input_first_frame": true,
|
||||
"input_first_last_frame": false,
|
||||
"input_last_frame": false,
|
||||
"input_reference_generate_single": false,
|
||||
"input_reference_generate_multiple": false,
|
||||
"aspect_ratio_allowed": [],
|
||||
"support_video_effect_template": false
|
||||
},
|
||||
"originalTypes": ["video_generate", "image_to_video"]
|
||||
}'::jsonb
|
||||
),
|
||||
(
|
||||
'MiniMax-Hailuo-2.3-Fast',
|
||||
'{
|
||||
"image_to_video": {
|
||||
"output_resolutions": ["720p", "1080p"],
|
||||
"duration_range": {"720p": [6, 10], "1080p": [6, 6]},
|
||||
"duration_options": {"720p": [6, 10], "1080p": [6]},
|
||||
"input_first_frame": true,
|
||||
"input_first_last_frame": false,
|
||||
"input_last_frame": false,
|
||||
"input_reference_generate_single": false,
|
||||
"input_reference_generate_multiple": false,
|
||||
"aspect_ratio_allowed": [],
|
||||
"support_video_effect_template": false
|
||||
},
|
||||
"originalTypes": ["image_to_video"]
|
||||
}'::jsonb
|
||||
)
|
||||
)
|
||||
UPDATE base_model_catalog model
|
||||
SET capabilities = defs.capabilities,
|
||||
metadata = CASE
|
||||
WHEN jsonb_typeof(model.metadata->'rawModel') = 'object' THEN jsonb_set(model.metadata, '{rawModel,capabilities}', defs.capabilities, true)
|
||||
ELSE model.metadata
|
||||
END,
|
||||
default_snapshot = CASE
|
||||
WHEN model.default_snapshot IS NULL THEN NULL
|
||||
ELSE jsonb_set(
|
||||
CASE
|
||||
WHEN jsonb_typeof(model.default_snapshot->'metadata'->'rawModel') = 'object' THEN jsonb_set(model.default_snapshot, '{metadata,rawModel,capabilities}', defs.capabilities, true)
|
||||
ELSE model.default_snapshot
|
||||
END,
|
||||
'{capabilities}',
|
||||
defs.capabilities,
|
||||
true
|
||||
)
|
||||
END,
|
||||
updated_at = now()
|
||||
FROM minimax_hailuo_defs defs
|
||||
WHERE model.provider_key = 'minimax'
|
||||
AND model.provider_model_name = defs.provider_model_name
|
||||
AND COALESCE(model.metadata->>'source', '') = 'server-main.integration-platform';
|
||||
|
||||
WITH minimax_hailuo_defs(provider_model_name, capabilities) AS (
|
||||
VALUES
|
||||
(
|
||||
'MiniMax-Hailuo-2.3',
|
||||
'{
|
||||
"video_generate": {
|
||||
"output_resolutions": ["720p", "1080p"],
|
||||
"duration_range": {"720p": [6, 10], "1080p": [6, 6]},
|
||||
"duration_options": {"720p": [6, 10], "1080p": [6]},
|
||||
"aspect_ratio_allowed": []
|
||||
},
|
||||
"image_to_video": {
|
||||
"output_resolutions": ["720p", "1080p"],
|
||||
"duration_range": {"720p": [6, 10], "1080p": [6, 6]},
|
||||
"duration_options": {"720p": [6, 10], "1080p": [6]},
|
||||
"input_first_frame": true,
|
||||
"input_first_last_frame": false,
|
||||
"input_last_frame": false,
|
||||
"input_reference_generate_single": false,
|
||||
"input_reference_generate_multiple": false,
|
||||
"aspect_ratio_allowed": [],
|
||||
"support_video_effect_template": false
|
||||
},
|
||||
"originalTypes": ["video_generate", "image_to_video"]
|
||||
}'::jsonb
|
||||
),
|
||||
(
|
||||
'MiniMax-Hailuo-2.3-Fast',
|
||||
'{
|
||||
"image_to_video": {
|
||||
"output_resolutions": ["720p", "1080p"],
|
||||
"duration_range": {"720p": [6, 10], "1080p": [6, 6]},
|
||||
"duration_options": {"720p": [6, 10], "1080p": [6]},
|
||||
"input_first_frame": true,
|
||||
"input_first_last_frame": false,
|
||||
"input_last_frame": false,
|
||||
"input_reference_generate_single": false,
|
||||
"input_reference_generate_multiple": false,
|
||||
"aspect_ratio_allowed": [],
|
||||
"support_video_effect_template": false
|
||||
},
|
||||
"originalTypes": ["image_to_video"]
|
||||
}'::jsonb
|
||||
)
|
||||
)
|
||||
UPDATE platform_models model
|
||||
SET capabilities = defs.capabilities,
|
||||
updated_at = now()
|
||||
FROM minimax_hailuo_defs defs, integration_platforms platform, base_model_catalog base_model
|
||||
WHERE platform.provider = 'minimax'
|
||||
AND platform.id = model.platform_id
|
||||
AND base_model.id = model.base_model_id
|
||||
AND COALESCE(model.provider_model_name, model.model_name) = defs.provider_model_name
|
||||
AND COALESCE(base_model.metadata->>'source', '') = 'server-main.integration-platform';
|
||||
@ -0,0 +1,123 @@
|
||||
WITH minimax_hailuo_defs(provider_model_name, capabilities) AS (
|
||||
VALUES
|
||||
(
|
||||
'MiniMax-Hailuo-2.3',
|
||||
'{
|
||||
"video_generate": {
|
||||
"output_resolutions": ["720p", "1080p"],
|
||||
"duration_range": {"720p": [6, 10], "1080p": [6, 6]},
|
||||
"duration_options": {"720p": [6, 10], "1080p": [6]},
|
||||
"aspect_ratio_allowed": []
|
||||
},
|
||||
"image_to_video": {
|
||||
"output_resolutions": ["720p", "1080p"],
|
||||
"duration_range": {"720p": [6, 10], "1080p": [6, 6]},
|
||||
"duration_options": {"720p": [6, 10], "1080p": [6]},
|
||||
"input_first_frame": true,
|
||||
"input_first_last_frame": false,
|
||||
"input_last_frame": false,
|
||||
"input_reference_generate_single": false,
|
||||
"input_reference_generate_multiple": false,
|
||||
"aspect_ratio_allowed": [],
|
||||
"support_video_effect_template": false
|
||||
},
|
||||
"originalTypes": ["video_generate", "image_to_video"]
|
||||
}'::jsonb
|
||||
),
|
||||
(
|
||||
'MiniMax-Hailuo-2.3-Fast',
|
||||
'{
|
||||
"image_to_video": {
|
||||
"output_resolutions": ["720p", "1080p"],
|
||||
"duration_range": {"720p": [6, 10], "1080p": [6, 6]},
|
||||
"duration_options": {"720p": [6, 10], "1080p": [6]},
|
||||
"input_first_frame": true,
|
||||
"input_first_last_frame": false,
|
||||
"input_last_frame": false,
|
||||
"input_reference_generate_single": false,
|
||||
"input_reference_generate_multiple": false,
|
||||
"aspect_ratio_allowed": [],
|
||||
"support_video_effect_template": false
|
||||
},
|
||||
"originalTypes": ["image_to_video"]
|
||||
}'::jsonb
|
||||
)
|
||||
)
|
||||
UPDATE base_model_catalog model
|
||||
SET capabilities = defs.capabilities,
|
||||
metadata = CASE
|
||||
WHEN jsonb_typeof(model.metadata->'rawModel') = 'object' THEN jsonb_set(model.metadata, '{rawModel,capabilities}', defs.capabilities, true)
|
||||
ELSE model.metadata
|
||||
END,
|
||||
default_snapshot = CASE
|
||||
WHEN model.default_snapshot IS NULL THEN NULL
|
||||
ELSE jsonb_set(
|
||||
CASE
|
||||
WHEN jsonb_typeof(model.default_snapshot->'metadata'->'rawModel') = 'object' THEN jsonb_set(model.default_snapshot, '{metadata,rawModel,capabilities}', defs.capabilities, true)
|
||||
ELSE model.default_snapshot
|
||||
END,
|
||||
'{capabilities}',
|
||||
defs.capabilities,
|
||||
true
|
||||
)
|
||||
END,
|
||||
updated_at = now()
|
||||
FROM minimax_hailuo_defs defs
|
||||
WHERE model.provider_key = 'minimax'
|
||||
AND model.provider_model_name = defs.provider_model_name
|
||||
AND COALESCE(model.metadata->>'source', '') = 'server-main.integration-platform';
|
||||
|
||||
WITH minimax_hailuo_defs(provider_model_name, capabilities) AS (
|
||||
VALUES
|
||||
(
|
||||
'MiniMax-Hailuo-2.3',
|
||||
'{
|
||||
"video_generate": {
|
||||
"output_resolutions": ["720p", "1080p"],
|
||||
"duration_range": {"720p": [6, 10], "1080p": [6, 6]},
|
||||
"duration_options": {"720p": [6, 10], "1080p": [6]},
|
||||
"aspect_ratio_allowed": []
|
||||
},
|
||||
"image_to_video": {
|
||||
"output_resolutions": ["720p", "1080p"],
|
||||
"duration_range": {"720p": [6, 10], "1080p": [6, 6]},
|
||||
"duration_options": {"720p": [6, 10], "1080p": [6]},
|
||||
"input_first_frame": true,
|
||||
"input_first_last_frame": false,
|
||||
"input_last_frame": false,
|
||||
"input_reference_generate_single": false,
|
||||
"input_reference_generate_multiple": false,
|
||||
"aspect_ratio_allowed": [],
|
||||
"support_video_effect_template": false
|
||||
},
|
||||
"originalTypes": ["video_generate", "image_to_video"]
|
||||
}'::jsonb
|
||||
),
|
||||
(
|
||||
'MiniMax-Hailuo-2.3-Fast',
|
||||
'{
|
||||
"image_to_video": {
|
||||
"output_resolutions": ["720p", "1080p"],
|
||||
"duration_range": {"720p": [6, 10], "1080p": [6, 6]},
|
||||
"duration_options": {"720p": [6, 10], "1080p": [6]},
|
||||
"input_first_frame": true,
|
||||
"input_first_last_frame": false,
|
||||
"input_last_frame": false,
|
||||
"input_reference_generate_single": false,
|
||||
"input_reference_generate_multiple": false,
|
||||
"aspect_ratio_allowed": [],
|
||||
"support_video_effect_template": false
|
||||
},
|
||||
"originalTypes": ["image_to_video"]
|
||||
}'::jsonb
|
||||
)
|
||||
)
|
||||
UPDATE platform_models model
|
||||
SET capabilities = defs.capabilities,
|
||||
updated_at = now()
|
||||
FROM minimax_hailuo_defs defs, integration_platforms platform, base_model_catalog base_model
|
||||
WHERE platform.provider = 'minimax'
|
||||
AND platform.id = model.platform_id
|
||||
AND base_model.id = model.base_model_id
|
||||
AND COALESCE(model.provider_model_name, model.model_name) = defs.provider_model_name
|
||||
AND COALESCE(base_model.metadata->>'source', '') = 'server-main.integration-platform';
|
||||
Loading…
Reference in New Issue
Block a user