Restore standard MiniMax resolutions and client mapping

This commit is contained in:
wangbo 2026-06-08 09:29:45 +08:00
parent ca5e71c8e8
commit 97f29ed156
8 changed files with 587 additions and 4 deletions

View File

@ -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)

View File

@ -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")
}

View File

@ -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")

View File

@ -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{
{

View File

@ -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":

View File

@ -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) {

View 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';

View 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';