From 797edeedf48d3ca40311d1e6d68eec47f258d8e7 Mon Sep 17 00:00:00 2001 From: wangbo Date: Tue, 30 Jun 2026 21:20:09 +0800 Subject: [PATCH] feat(gateway): sync Seedance 2.0 capabilities --- apps/api/internal/clients/clients_test.go | 32 +- apps/api/internal/clients/volces.go | 10 + .../0057_volces_seedance20_mini.sql | 191 ++++++++++++ ...0058_volces_seedance20_capability_sync.sql | 294 ++++++++++++++++++ 4 files changed, 526 insertions(+), 1 deletion(-) create mode 100644 apps/api/migrations/0057_volces_seedance20_mini.sql create mode 100644 apps/api/migrations/0058_volces_seedance20_capability_sync.sql diff --git a/apps/api/internal/clients/clients_test.go b/apps/api/internal/clients/clients_test.go index 85cf910..4096745 100644 --- a/apps/api/internal/clients/clients_test.go +++ b/apps/api/internal/clients/clients_test.go @@ -1660,6 +1660,7 @@ func TestVolcesVideoBodyAllowsOnlyTaskPayloadFields(t *testing.T) { "cameraFixed": false, "watermark": true, "seed": -1, + "tools": []any{map[string]any{"type": "web_search"}}, "task_id": "local-task-id", "runMode": "simulation", "fps": 24, @@ -1707,7 +1708,7 @@ func TestVolcesVideoBodyAllowsOnlyTaskPayloadFields(t *testing.T) { allowedTopLevel := map[string]bool{ "model": true, "content": true, "callback_url": true, "return_last_frame": true, "execution_expires_after": true, "generate_audio": true, "draft": true, "resolution": true, "ratio": true, "duration": true, - "seed": true, "camera_fixed": true, "watermark": true, + "seed": true, "camera_fixed": true, "watermark": true, "tools": true, } for key := range body { if !allowedTopLevel[key] { @@ -1728,6 +1729,10 @@ func TestVolcesVideoBodyAllowsOnlyTaskPayloadFields(t *testing.T) { body["watermark"] != true { t.Fatalf("unexpected direct video fields: %+v", body) } + tools, ok := body["tools"].([]any) + if !ok || len(tools) != 1 { + t.Fatalf("expected web search tools to pass through, got %+v", body["tools"]) + } content, ok := body["content"].([]map[string]any) if !ok || len(content) != 5 { @@ -1782,6 +1787,31 @@ func TestVolcesVideoBodyPrefersFramesOverDuration(t *testing.T) { } } +func TestVolcesVideoBodyBuildsWebSearchToolsFromFlag(t *testing.T) { + body := volcesVideoBody(Request{ + Kind: "videos.generations", + ModelType: "video_generate", + Model: "豆包Seedance-2.0", + Body: map[string]any{ + "model": "豆包Seedance-2.0", + "prompt": "查找最新城市夜景素材再生成视频", + "enable_web_search": true, + }, + Candidate: store.RuntimeModelCandidate{ + ProviderModelName: "doubao-seedance-2-0-260128", + }, + }) + + tools, ok := body["tools"].([]any) + if !ok || len(tools) != 1 { + t.Fatalf("expected generated web search tools, got %+v", body["tools"]) + } + tool, ok := tools[0].(map[string]any) + if !ok || tool["type"] != "web_search" { + t.Fatalf("unexpected generated web search tool: %+v", tools[0]) + } +} + func TestVolcesClientVideoResumePollsExistingTaskID(t *testing.T) { var submitCalled bool var pollPath string diff --git a/apps/api/internal/clients/volces.go b/apps/api/internal/clients/volces.go index a739f2d..0de43af 100644 --- a/apps/api/internal/clients/volces.go +++ b/apps/api/internal/clients/volces.go @@ -336,6 +336,16 @@ func addVolcesVideoTaskParams(out map[string]any, body map[string]any) { copyVolcesBoolParam(out, "return_last_frame", body, "return_last_frame", "returnLastFrame") copyVolcesIntParam(out, "execution_expires_after", body, "execution_expires_after", "executionExpiresAfter") copyVolcesBoolParam(out, "generate_audio", body, "generate_audio", "generateAudio", "audio") + if tools, ok := body["tools"]; ok { + out["tools"] = tools + } else { + for _, key := range []string{"enable_web_search", "enableWebSearch"} { + if value, ok := volcesBoolFromAny(body[key]); ok && value { + out["tools"] = []any{map[string]any{"type": "web_search"}} + break + } + } + } copyVolcesBoolParam(out, "draft", body, "draft") copyVolcesStringParam(out, "resolution", body, "resolution", "size") copyVolcesStringParam(out, "ratio", body, "ratio", "aspect_ratio", "aspectRatio") diff --git a/apps/api/migrations/0057_volces_seedance20_mini.sql b/apps/api/migrations/0057_volces_seedance20_mini.sql new file mode 100644 index 0000000..bc7aa08 --- /dev/null +++ b/apps/api/migrations/0057_volces_seedance20_mini.sql @@ -0,0 +1,191 @@ +WITH seedance_mini_model AS ( + SELECT + 'volces:doubao-seedance-2-0-mini-260615' AS canonical_model_key, + 'doubao-seedance-2-0-mini-260615' AS provider_model_name, + '豆包Seedance-2.0-mini' AS display_name, + '["video_generate","image_to_video","omni_video"]'::jsonb AS model_type, + '{ + "video_generate": { + "output_resolutions": ["720p", "480p"], + "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], + "duration_range": [4, 15], + "input_audio": true, + "output_audio": true, + "supportWebSearch": true, + "output_support_return_last_frame": true, + "output_video_continuation": true, + "max_images": 9, + "max_videos": 3 + }, + "image_to_video": { + "output_resolutions": ["720p", "480p"], + "input_first_frame": true, + "input_first_last_frame": true, + "input_last_frame": false, + "input_reference_generate_single": true, + "input_reference_generate_multiple": true, + "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], + "duration_range": [4, 15], + "input_audio": true, + "support_video_effect_template": false, + "output_audio": true, + "supportWebSearch": true, + "output_support_return_last_frame": true, + "output_video_continuation": true, + "max_images": 9, + "max_videos": 3, + "max_images_for_last_frame": 2 + }, + "omni_video": { + "supported_modes": ["element_reference", "text_to_video", "image_reference", "first_last_frame", "video_reference", "video_edit", "multi_shot"], + "output_resolutions": ["720p", "480p"], + "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], + "duration_options": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "output_audio": true, + "input_audio": true, + "supportWebSearch": true, + "output_support_return_last_frame": true, + "output_video_continuation": true, + "max_videos": 3, + "max_audios": 3, + "max_images": 9, + "max_elements": 9, + "max_images_and_elements": 9, + "max_images_for_last_frame": 2, + "support_instruction_edit": true, + "supports_portrait_asset_reference": true + }, + "originalTypes": ["video_generate", "image_to_video", "omni_video"] + }'::jsonb AS capabilities, + '{"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"currency":"resource"}'::jsonb AS base_billing_config, + '{"rules":[{"metric":"rpm","limit":600,"windowSeconds":60},{"metric":"concurrent","limit":10,"leaseTtlSeconds":120}]}'::jsonb AS default_rate_limit_policy, + '{ + "source": "server-main.integration-platform", + "sourceProviderCode": "volces", + "sourceProviderName": "火山引擎(豆包)", + "sourceSpecType": "volces", + "originalTypes": ["video_generate", "image_to_video", "omni_video"], + "alias": "豆包Seedance-2.0-mini", + "description": "Seedance 2.0 轻量低成本视频生成模型,支持 480p/720p", + "iconPath": "https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg", + "billingType": "external-api", + "billingMode": "", + "referenceModel": "", + "modelWeight": null, + "selectable": true, + "rawModel": { + "name": "doubao-seedance-2-0-mini-260615", + "types": ["video_generate", "image_to_video", "omni_video"], + "alias": "豆包Seedance-2.0-mini", + "description": "Seedance 2.0 轻量低成本视频生成模型,支持 480p/720p", + "icon_path": "https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg", + "model_limits": { + "max_request_per_minute": 600, + "max_concurrent_requests": 10 + } + } + }'::jsonb AS metadata +) +INSERT INTO base_model_catalog ( + provider_id, + provider_key, + canonical_model_key, + provider_model_name, + model_type, + display_name, + capabilities, + base_billing_config, + default_rate_limit_policy, + metadata, + catalog_type, + default_snapshot, + status +) +SELECT + (SELECT id FROM model_catalog_providers WHERE provider_key = 'volces' OR provider_code = 'volces' LIMIT 1), + 'volces', + model.canonical_model_key, + model.provider_model_name, + model.model_type, + model.display_name, + model.capabilities, + model.base_billing_config, + model.default_rate_limit_policy, + jsonb_set(model.metadata, '{rawModel,capabilities}', model.capabilities, true), + 'system', + jsonb_build_object( + 'providerKey', 'volces', + 'canonicalModelKey', model.canonical_model_key, + 'providerModelName', model.provider_model_name, + 'modelType', model.model_type, + 'modelAlias', model.display_name, + 'displayName', model.display_name, + 'capabilities', model.capabilities, + 'baseBillingConfig', model.base_billing_config, + 'defaultRateLimitPolicy', model.default_rate_limit_policy, + 'metadata', jsonb_set(model.metadata, '{rawModel,capabilities}', model.capabilities, true), + 'status', 'active' + ), + 'active' +FROM seedance_mini_model model +ON CONFLICT (canonical_model_key) DO UPDATE +SET provider_id = EXCLUDED.provider_id, + provider_key = EXCLUDED.provider_key, + provider_model_name = CASE WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.provider_model_name ELSE base_model_catalog.provider_model_name END, + model_type = CASE WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.model_type ELSE base_model_catalog.model_type END, + display_name = CASE WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.display_name ELSE base_model_catalog.display_name END, + capabilities = CASE WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.capabilities ELSE base_model_catalog.capabilities END, + base_billing_config = CASE WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.base_billing_config ELSE base_model_catalog.base_billing_config END, + default_rate_limit_policy = CASE WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.default_rate_limit_policy ELSE base_model_catalog.default_rate_limit_policy END, + metadata = CASE WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.metadata ELSE base_model_catalog.metadata END, + catalog_type = CASE WHEN base_model_catalog.customized_at IS NULL THEN 'system' ELSE base_model_catalog.catalog_type END, + default_snapshot = CASE WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.default_snapshot ELSE base_model_catalog.default_snapshot END, + status = CASE WHEN base_model_catalog.customized_at IS NULL THEN 'active' ELSE base_model_catalog.status END, + updated_at = now(); + +INSERT INTO platform_models ( + platform_id, + base_model_id, + model_name, + provider_model_name, + model_alias, + model_type, + display_name, + capabilities, + pricing_mode, + billing_config, + retry_policy, + rate_limit_policy, + enabled +) +SELECT + platform.id, + base_model.id, + base_model.provider_model_name, + base_model.provider_model_name, + base_model.display_name, + base_model.model_type, + base_model.display_name, + base_model.capabilities, + 'inherit_discount', + base_model.base_billing_config, + '{"enabled":true,"maxAttempts":1}'::jsonb, + base_model.default_rate_limit_policy, + true +FROM integration_platforms platform +JOIN base_model_catalog base_model ON base_model.canonical_model_key = 'volces:doubao-seedance-2-0-mini-260615' +WHERE platform.provider = 'volces' + AND platform.deleted_at IS NULL +ON CONFLICT (platform_id, model_name) DO UPDATE +SET base_model_id = EXCLUDED.base_model_id, + provider_model_name = EXCLUDED.provider_model_name, + model_alias = EXCLUDED.model_alias, + display_name = EXCLUDED.display_name, + model_type = EXCLUDED.model_type, + capabilities = EXCLUDED.capabilities, + pricing_mode = EXCLUDED.pricing_mode, + billing_config = EXCLUDED.billing_config, + retry_policy = EXCLUDED.retry_policy, + rate_limit_policy = EXCLUDED.rate_limit_policy, + enabled = EXCLUDED.enabled, + updated_at = now(); diff --git a/apps/api/migrations/0058_volces_seedance20_capability_sync.sql b/apps/api/migrations/0058_volces_seedance20_capability_sync.sql new file mode 100644 index 0000000..cdf284d --- /dev/null +++ b/apps/api/migrations/0058_volces_seedance20_capability_sync.sql @@ -0,0 +1,294 @@ +WITH seedance20_models AS ( + SELECT * + FROM ( + VALUES + ( + 'volces:doubao-seedance-2-0-260128', + 'doubao-seedance-2-0-260128', + '["video_generate","image_to_video","omni_video"]'::jsonb, + '{ + "video_generate": { + "output_resolutions": ["720p", "480p", "1080p", "2160p"], + "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], + "duration_range": [4, 15], + "input_audio": true, + "output_audio": true, + "supportWebSearch": true, + "output_support_return_last_frame": true, + "output_video_continuation": true, + "max_images": 9, + "max_videos": 3 + }, + "image_to_video": { + "output_resolutions": ["720p", "480p", "1080p", "2160p"], + "input_first_frame": true, + "input_first_last_frame": true, + "input_last_frame": false, + "input_reference_generate_single": true, + "input_reference_generate_multiple": true, + "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], + "duration_range": [4, 15], + "input_audio": true, + "support_video_effect_template": false, + "output_audio": true, + "supportWebSearch": true, + "output_support_return_last_frame": true, + "output_video_continuation": true, + "max_images": 9, + "max_videos": 3, + "max_images_for_last_frame": 2 + }, + "omni_video": { + "supported_modes": ["element_reference", "text_to_video", "image_reference", "first_last_frame", "video_reference", "video_edit", "multi_shot"], + "output_resolutions": ["720p", "480p", "1080p", "2160p"], + "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], + "duration_options": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "output_audio": true, + "input_audio": true, + "supportWebSearch": true, + "output_support_return_last_frame": true, + "output_video_continuation": true, + "max_videos": 3, + "max_audios": 3, + "max_images": 9, + "max_elements": 9, + "max_images_and_elements": 9, + "max_images_for_last_frame": 2, + "support_instruction_edit": true, + "supports_portrait_asset_reference": true + }, + "originalTypes": ["video_generate", "image_to_video", "omni_video"] + }'::jsonb + ), + ( + 'volces:doubao-seedance-2-0-fast-260128', + 'doubao-seedance-2-0-fast-260128', + '["video_generate","image_to_video","omni_video"]'::jsonb, + '{ + "video_generate": { + "output_resolutions": ["720p", "480p"], + "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], + "duration_range": [4, 15], + "input_audio": true, + "output_audio": true, + "supportWebSearch": true, + "output_support_return_last_frame": true, + "output_video_continuation": true, + "max_images": 9, + "max_videos": 3 + }, + "image_to_video": { + "output_resolutions": ["720p", "480p"], + "input_first_frame": true, + "input_first_last_frame": true, + "input_last_frame": false, + "input_reference_generate_single": true, + "input_reference_generate_multiple": true, + "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], + "duration_range": [4, 15], + "input_audio": true, + "support_video_effect_template": false, + "output_audio": true, + "supportWebSearch": true, + "output_support_return_last_frame": true, + "output_video_continuation": true, + "max_images": 9, + "max_videos": 3, + "max_images_for_last_frame": 2 + }, + "omni_video": { + "supported_modes": ["element_reference", "text_to_video", "image_reference", "first_last_frame", "video_reference", "video_edit", "multi_shot"], + "output_resolutions": ["720p", "480p"], + "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], + "duration_options": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "output_audio": true, + "input_audio": true, + "supportWebSearch": true, + "output_support_return_last_frame": true, + "output_video_continuation": true, + "max_videos": 3, + "max_audios": 3, + "max_images": 9, + "max_elements": 9, + "max_images_and_elements": 9, + "max_images_for_last_frame": 2, + "support_instruction_edit": true, + "supports_portrait_asset_reference": true + }, + "originalTypes": ["video_generate", "image_to_video", "omni_video"] + }'::jsonb + ), + ( + 'volces:doubao-seedance-2-0-mini-260615', + 'doubao-seedance-2-0-mini-260615', + '["video_generate","image_to_video","omni_video"]'::jsonb, + '{ + "video_generate": { + "output_resolutions": ["720p", "480p"], + "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], + "duration_range": [4, 15], + "input_audio": true, + "output_audio": true, + "supportWebSearch": true, + "output_support_return_last_frame": true, + "output_video_continuation": true, + "max_images": 9, + "max_videos": 3 + }, + "image_to_video": { + "output_resolutions": ["720p", "480p"], + "input_first_frame": true, + "input_first_last_frame": true, + "input_last_frame": false, + "input_reference_generate_single": true, + "input_reference_generate_multiple": true, + "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], + "duration_range": [4, 15], + "input_audio": true, + "support_video_effect_template": false, + "output_audio": true, + "supportWebSearch": true, + "output_support_return_last_frame": true, + "output_video_continuation": true, + "max_images": 9, + "max_videos": 3, + "max_images_for_last_frame": 2 + }, + "omni_video": { + "supported_modes": ["element_reference", "text_to_video", "image_reference", "first_last_frame", "video_reference", "video_edit", "multi_shot"], + "output_resolutions": ["720p", "480p"], + "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], + "duration_options": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "output_audio": true, + "input_audio": true, + "supportWebSearch": true, + "output_support_return_last_frame": true, + "output_video_continuation": true, + "max_videos": 3, + "max_audios": 3, + "max_images": 9, + "max_elements": 9, + "max_images_and_elements": 9, + "max_images_for_last_frame": 2, + "support_instruction_edit": true, + "supports_portrait_asset_reference": true + }, + "originalTypes": ["video_generate", "image_to_video", "omni_video"] + }'::jsonb + ) + ) AS item(canonical_model_key, provider_model_name, model_type, capabilities) +), +seedance20_payload AS ( + SELECT + *, + '{"max_request_per_minute":600,"max_concurrent_requests":10}'::jsonb AS model_limits, + '{"rules":[{"metric":"rpm","limit":600,"windowSeconds":60},{"metric":"concurrent","limit":10,"leaseTtlSeconds":120}]}'::jsonb AS rate_limit_policy + FROM seedance20_models +) +UPDATE base_model_catalog base_model +SET model_type = seedance_model.model_type, + capabilities = seedance_model.capabilities, + default_rate_limit_policy = seedance_model.rate_limit_policy, + metadata = jsonb_set( + COALESCE(base_model.metadata, '{}'::jsonb), + '{rawModel}', + COALESCE(base_model.metadata->'rawModel', '{}'::jsonb) + || jsonb_build_object( + 'capabilities', seedance_model.capabilities, + 'model_limits', seedance_model.model_limits + ), + true + ), + default_snapshot = CASE + WHEN COALESCE(base_model.default_snapshot, '{}'::jsonb) = '{}'::jsonb THEN base_model.default_snapshot + ELSE jsonb_set( + jsonb_set( + jsonb_set( + jsonb_set( + base_model.default_snapshot, + '{modelType}', + seedance_model.model_type, + true + ), + '{capabilities}', + seedance_model.capabilities, + true + ), + '{defaultRateLimitPolicy}', + seedance_model.rate_limit_policy, + true + ), + '{metadata}', + COALESCE(base_model.default_snapshot->'metadata', '{}'::jsonb) + || jsonb_build_object( + 'rawModel', + COALESCE(base_model.default_snapshot->'metadata'->'rawModel', '{}'::jsonb) + || jsonb_build_object( + 'capabilities', seedance_model.capabilities, + 'model_limits', seedance_model.model_limits + ) + ), + true + ) + END, + updated_at = now() +FROM seedance20_payload seedance_model +WHERE base_model.provider_key = 'volces' + AND ( + base_model.canonical_model_key = seedance_model.canonical_model_key + OR base_model.provider_model_name = seedance_model.provider_model_name + ); + +WITH seedance20_models AS ( + SELECT * + FROM ( + VALUES + ( + 'doubao-seedance-2-0-260128', + '["video_generate","image_to_video","omni_video"]'::jsonb, + '{ + "video_generate": {"output_resolutions": ["720p", "480p", "1080p", "2160p"], "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], "duration_range": [4, 15], "input_audio": true, "output_audio": true, "supportWebSearch": true, "output_support_return_last_frame": true, "output_video_continuation": true, "max_images": 9, "max_videos": 3}, + "image_to_video": {"output_resolutions": ["720p", "480p", "1080p", "2160p"], "input_first_frame": true, "input_first_last_frame": true, "input_last_frame": false, "input_reference_generate_single": true, "input_reference_generate_multiple": true, "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], "duration_range": [4, 15], "input_audio": true, "support_video_effect_template": false, "output_audio": true, "supportWebSearch": true, "output_support_return_last_frame": true, "output_video_continuation": true, "max_images": 9, "max_videos": 3, "max_images_for_last_frame": 2}, + "omni_video": {"supported_modes": ["element_reference", "text_to_video", "image_reference", "first_last_frame", "video_reference", "video_edit", "multi_shot"], "output_resolutions": ["720p", "480p", "1080p", "2160p"], "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], "duration_options": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "output_audio": true, "input_audio": true, "supportWebSearch": true, "output_support_return_last_frame": true, "output_video_continuation": true, "max_videos": 3, "max_audios": 3, "max_images": 9, "max_elements": 9, "max_images_and_elements": 9, "max_images_for_last_frame": 2, "support_instruction_edit": true, "supports_portrait_asset_reference": true}, + "originalTypes": ["video_generate", "image_to_video", "omni_video"] + }'::jsonb + ), + ( + 'doubao-seedance-2-0-fast-260128', + '["video_generate","image_to_video","omni_video"]'::jsonb, + '{ + "video_generate": {"output_resolutions": ["720p", "480p"], "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], "duration_range": [4, 15], "input_audio": true, "output_audio": true, "supportWebSearch": true, "output_support_return_last_frame": true, "output_video_continuation": true, "max_images": 9, "max_videos": 3}, + "image_to_video": {"output_resolutions": ["720p", "480p"], "input_first_frame": true, "input_first_last_frame": true, "input_last_frame": false, "input_reference_generate_single": true, "input_reference_generate_multiple": true, "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], "duration_range": [4, 15], "input_audio": true, "support_video_effect_template": false, "output_audio": true, "supportWebSearch": true, "output_support_return_last_frame": true, "output_video_continuation": true, "max_images": 9, "max_videos": 3, "max_images_for_last_frame": 2}, + "omni_video": {"supported_modes": ["element_reference", "text_to_video", "image_reference", "first_last_frame", "video_reference", "video_edit", "multi_shot"], "output_resolutions": ["720p", "480p"], "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], "duration_options": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "output_audio": true, "input_audio": true, "supportWebSearch": true, "output_support_return_last_frame": true, "output_video_continuation": true, "max_videos": 3, "max_audios": 3, "max_images": 9, "max_elements": 9, "max_images_and_elements": 9, "max_images_for_last_frame": 2, "support_instruction_edit": true, "supports_portrait_asset_reference": true}, + "originalTypes": ["video_generate", "image_to_video", "omni_video"] + }'::jsonb + ), + ( + 'doubao-seedance-2-0-mini-260615', + '["video_generate","image_to_video","omni_video"]'::jsonb, + '{ + "video_generate": {"output_resolutions": ["720p", "480p"], "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], "duration_range": [4, 15], "input_audio": true, "output_audio": true, "supportWebSearch": true, "output_support_return_last_frame": true, "output_video_continuation": true, "max_images": 9, "max_videos": 3}, + "image_to_video": {"output_resolutions": ["720p", "480p"], "input_first_frame": true, "input_first_last_frame": true, "input_last_frame": false, "input_reference_generate_single": true, "input_reference_generate_multiple": true, "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], "duration_range": [4, 15], "input_audio": true, "support_video_effect_template": false, "output_audio": true, "supportWebSearch": true, "output_support_return_last_frame": true, "output_video_continuation": true, "max_images": 9, "max_videos": 3, "max_images_for_last_frame": 2}, + "omni_video": {"supported_modes": ["element_reference", "text_to_video", "image_reference", "first_last_frame", "video_reference", "video_edit", "multi_shot"], "output_resolutions": ["720p", "480p"], "aspect_ratio_allowed": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], "duration_options": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "output_audio": true, "input_audio": true, "supportWebSearch": true, "output_support_return_last_frame": true, "output_video_continuation": true, "max_videos": 3, "max_audios": 3, "max_images": 9, "max_elements": 9, "max_images_and_elements": 9, "max_images_for_last_frame": 2, "support_instruction_edit": true, "supports_portrait_asset_reference": true}, + "originalTypes": ["video_generate", "image_to_video", "omni_video"] + }'::jsonb + ) + ) AS item(provider_model_name, model_type, capabilities) +), +seedance20_payload AS ( + SELECT + *, + '{"rules":[{"metric":"rpm","limit":600,"windowSeconds":60},{"metric":"concurrent","limit":10,"leaseTtlSeconds":120}]}'::jsonb AS rate_limit_policy + FROM seedance20_models +) +UPDATE platform_models model +SET model_type = seedance_model.model_type, + capabilities = seedance_model.capabilities, + rate_limit_policy = seedance_model.rate_limit_policy, + updated_at = now() +FROM integration_platforms platform +JOIN seedance20_payload seedance_model ON TRUE +WHERE model.platform_id = platform.id + AND platform.provider = 'volces' + AND platform.deleted_at IS NULL + AND COALESCE(NULLIF(model.provider_model_name, ''), model.model_name) = seedance_model.provider_model_name;