From 9d0af657e562a831bec4d2898ae2994ffe2353f1 Mon Sep 17 00:00:00 2001 From: viettranx Date: Sun, 8 Mar 2026 22:32:08 +0700 Subject: [PATCH] fix(tools): correct media provider params and UI fixes - MiniMax audio: fix invalid params (sample_rate/bitrate as int, auto instrumental when no lyrics, pass duration_seconds) - MiniMax/DashScope image: map aspect_ratio to provider size format - Gemini video: read person_generation from chain params instead of hardcode - GetParamInt: add string-to-int coercion for UI select values - UI: fix combobox portal selection bug, dropdown overflow clipping, provider chain form spacing, skill dialog min-height - Update bitrate options to numeric bps values in params schema --- internal/tools/create_audio_minimax.go | 14 +++++++++-- internal/tools/create_image_dashscope.go | 19 ++++++++++++++- internal/tools/create_image_minimax.go | 24 ++++++++++++++++++- internal/tools/create_video.go | 6 ++--- internal/tools/media_provider_chain.go | 7 ++++++ ui/web/src/components/ui/combobox.tsx | 17 +++++++++---- .../media-provider-chain-form.tsx | 2 +- .../media-provider-params-schema.ts | 9 +++---- .../src/pages/skills/skill-detail-dialog.tsx | 2 +- 9 files changed, 83 insertions(+), 17 deletions(-) diff --git a/internal/tools/create_audio_minimax.go b/internal/tools/create_audio_minimax.go index cee76fbf..51337932 100644 --- a/internal/tools/create_audio_minimax.go +++ b/internal/tools/create_audio_minimax.go @@ -19,8 +19,15 @@ func callMinimaxMusicGen(ctx context.Context, apiKey, apiBase, model, prompt str lyrics := GetParamString(params, "lyrics", "") instrumental := GetParamBool(params, "instrumental", false) lyricsOptimizer := GetParamBool(params, "lyrics_optimizer", false) - sampleRate := GetParamString(params, "sample_rate", "44100") - bitrate := GetParamString(params, "bitrate", "320k") + sampleRate := GetParamInt(params, "sample_rate", 44100) + bitrate := GetParamInt(params, "bitrate", 256000) + + // lyrics is required when is_instrumental is false + if !instrumental && lyrics == "" { + instrumental = true + } + + duration := GetParamInt(params, "duration", 0) body := map[string]interface{}{ "model": model, @@ -37,6 +44,9 @@ func callMinimaxMusicGen(ctx context.Context, apiKey, apiBase, model, prompt str if lyrics != "" { body["lyrics"] = lyrics } + if duration > 0 { + body["duration_seconds"] = duration + } jsonBody, err := json.Marshal(body) if err != nil { diff --git a/internal/tools/create_image_dashscope.go b/internal/tools/create_image_dashscope.go index 0d7bee38..9d8434cb 100644 --- a/internal/tools/create_image_dashscope.go +++ b/internal/tools/create_image_dashscope.go @@ -59,8 +59,25 @@ func dashScopeTaskEndpoint(apiBase, taskID string) string { // callDashScopeImageGen calls the DashScope (Alibaba/Bailian) multimodal image generation API. // The API is async: an initial POST returns a task_id, which is then polled until done. // On completion, output.results[].url contains the image URL to download. +// aspectRatioToDashScopeSize converts aspect_ratio to DashScope size format. +// Falls back to explicit "size" param if set, otherwise uses aspect_ratio mapping. +func aspectRatioToDashScopeSize(params map[string]any) string { + if s := GetParamString(params, "size", ""); s != "" { + return s + } + ar := GetParamString(params, "aspect_ratio", "1:1") + switch ar { + case "16:9": + return "1280*720" + case "9:16": + return "720*1280" + default: + return "1024*1024" + } +} + func callDashScopeImageGen(ctx context.Context, apiKey, apiBase, model, prompt string, params map[string]any) ([]byte, *providers.Usage, error) { - size := GetParamString(params, "size", "1024*1024") + size := aspectRatioToDashScopeSize(params) promptExtend := GetParamBool(params, "prompt_extend", true) endpoint := dashScopeImageEndpoint(apiBase) diff --git a/internal/tools/create_image_minimax.go b/internal/tools/create_image_minimax.go index fa859367..a2da73bd 100644 --- a/internal/tools/create_image_minimax.go +++ b/internal/tools/create_image_minimax.go @@ -16,8 +16,30 @@ import ( // callMinimaxImageGen calls the MiniMax image generation API. // Endpoint: POST {apiBase}/image_generation // Response: base64 image data in data.image_list[0].base64_image +// aspectRatioToMinimaxSize converts aspect_ratio (e.g. "16:9") to MiniMax size format. +// Falls back to explicit "size" param if set, otherwise uses aspect_ratio mapping. +func aspectRatioToMinimaxSize(params map[string]any) string { + // Explicit size param takes priority (from UI provider settings) + if s := GetParamString(params, "size", ""); s != "" { + return s + } + ar := GetParamString(params, "aspect_ratio", "1:1") + switch ar { + case "16:9": + return "1280*720" + case "9:16": + return "720*1280" + case "4:3": + return "1024*768" + case "3:4": + return "768*1024" + default: + return "1024*1024" + } +} + func callMinimaxImageGen(ctx context.Context, apiKey, apiBase, model, prompt string, params map[string]any) ([]byte, *providers.Usage, error) { - size := GetParamString(params, "size", "1024*1024") + size := aspectRatioToMinimaxSize(params) promptOptimizer := GetParamBool(params, "prompt_optimizer", true) body := map[string]interface{}{ diff --git a/internal/tools/create_video.go b/internal/tools/create_video.go index 493ced99..32066e88 100644 --- a/internal/tools/create_video.go +++ b/internal/tools/create_video.go @@ -149,7 +149,7 @@ func (t *CreateVideoTool) callProvider(ctx context.Context, cp credentialProvide switch ProviderTypeFromName(providerName) { case "gemini": - return t.callGeminiVideoGen(ctx, cp.APIKey(), cp.APIBase(), model, prompt, duration, aspectRatio) + return t.callGeminiVideoGen(ctx, cp.APIKey(), cp.APIBase(), model, prompt, duration, aspectRatio, params) case "minimax": return callMinimaxVideoGen(ctx, cp.APIKey(), cp.APIBase(), model, params) default: @@ -159,7 +159,7 @@ func (t *CreateVideoTool) callProvider(ctx context.Context, cp credentialProvide // callGeminiVideoGen uses the Gemini predictLongRunning API for Veo video generation. // Flow: POST predictLongRunning → poll operation → download video from URI. -func (t *CreateVideoTool) callGeminiVideoGen(ctx context.Context, apiKey, apiBase, model, prompt string, duration int, aspectRatio string) ([]byte, *providers.Usage, error) { +func (t *CreateVideoTool) callGeminiVideoGen(ctx context.Context, apiKey, apiBase, model, prompt string, duration int, aspectRatio string, params map[string]any) ([]byte, *providers.Usage, error) { nativeBase := strings.TrimRight(apiBase, "/") nativeBase = strings.TrimSuffix(nativeBase, "/openai") @@ -173,7 +173,7 @@ func (t *CreateVideoTool) callGeminiVideoGen(ctx context.Context, apiKey, apiBas "parameters": map[string]interface{}{ "aspectRatio": aspectRatio, "durationSeconds": duration, - "personGeneration": "allow_all", + "personGeneration": GetParamString(params, "person_generation", "allow_all"), }, } diff --git a/internal/tools/media_provider_chain.go b/internal/tools/media_provider_chain.go index e3de72fe..1725a22b 100644 --- a/internal/tools/media_provider_chain.go +++ b/internal/tools/media_provider_chain.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "log/slog" + "strconv" "strings" "time" @@ -297,6 +298,10 @@ func GetParamInt(params map[string]any, key string, fallback int) int { return int(v) case int: return v + case string: + if n, err := strconv.Atoi(v); err == nil { + return n + } } return fallback } @@ -319,6 +324,8 @@ func ProviderTypeFromName(name string) string { return "anthropic" case name == "suno" || strings.HasPrefix(name, "suno"): return "suno" + case name == "yescale": + return "openai" default: return "openai_compat" } diff --git a/ui/web/src/components/ui/combobox.tsx b/ui/web/src/components/ui/combobox.tsx index d3558015..50806805 100644 --- a/ui/web/src/components/ui/combobox.tsx +++ b/ui/web/src/components/ui/combobox.tsx @@ -29,6 +29,7 @@ export function Combobox({ const [open, setOpen] = React.useState(false); const [search, setSearch] = React.useState(""); const containerRef = React.useRef(null); + const dropdownRef = React.useRef(null); const [dropdownStyle, setDropdownStyle] = React.useState({}); // Sync search text when value changes externally — show label if available @@ -41,7 +42,11 @@ export function Combobox({ React.useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { - if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + const target = e.target as Node; + if ( + containerRef.current && !containerRef.current.contains(target) && + (!dropdownRef.current || !dropdownRef.current.contains(target)) + ) { setOpen(false); } }; @@ -54,11 +59,14 @@ export function Combobox({ if (!open || !portalContainer?.current || !containerRef.current) return; const inputRect = containerRef.current.getBoundingClientRect(); const portalRect = portalContainer.current.getBoundingClientRect(); + const left = inputRect.left - portalRect.left; + const maxWidth = portalRect.width - left; setDropdownStyle({ position: "absolute", top: inputRect.bottom - portalRect.top + 4, - left: inputRect.left - portalRect.left, + left, width: inputRect.width, + maxWidth, zIndex: 50, }); }, [open, search, portalContainer]); @@ -89,10 +97,11 @@ export function Combobox({ const dropdownContent = open && filtered.length > 0 && (
{filtered.map((o) => ( diff --git a/ui/web/src/pages/builtin-tools/media-provider-chain-form.tsx b/ui/web/src/pages/builtin-tools/media-provider-chain-form.tsx index 33caeaad..aa1a7fe9 100644 --- a/ui/web/src/pages/builtin-tools/media-provider-chain-form.tsx +++ b/ui/web/src/pages/builtin-tools/media-provider-chain-form.tsx @@ -425,7 +425,7 @@ export function MediaProviderChainForm({ -
+
e.id)} strategy={verticalListSortingStrategy}> {entries.map((entry, index) => ( diff --git a/ui/web/src/pages/builtin-tools/media-provider-params-schema.ts b/ui/web/src/pages/builtin-tools/media-provider-params-schema.ts index 8618307d..7a6cebb3 100644 --- a/ui/web/src/pages/builtin-tools/media-provider-params-schema.ts +++ b/ui/web/src/pages/builtin-tools/media-provider-params-schema.ts @@ -101,11 +101,12 @@ export const MEDIA_PARAMS_SCHEMA: Record> = key: "bitrate", label: "Bitrate", type: "select", - default: "320k", + default: "256000", options: [ - { value: "128k", label: "128k" }, - { value: "256k", label: "256k" }, - { value: "320k", label: "320k" }, + { value: "32000", label: "32kbps" }, + { value: "64000", label: "64kbps" }, + { value: "128000", label: "128kbps" }, + { value: "256000", label: "256kbps" }, ], }, ], diff --git a/ui/web/src/pages/skills/skill-detail-dialog.tsx b/ui/web/src/pages/skills/skill-detail-dialog.tsx index 5246a881..ed0559a0 100644 --- a/ui/web/src/pages/skills/skill-detail-dialog.tsx +++ b/ui/web/src/pages/skills/skill-detail-dialog.tsx @@ -100,7 +100,7 @@ export function SkillDetailDialog({ return ( onClose()}> - + {skill.name}