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
This commit is contained in:
viettranx committed 2026-03-08 22:32:08 +07:00
1 parent 157d09c021
commit 9d0af657e5
9 files changed
+83 -17

No files matched your search

+12 -2
View File
@@ -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 {
+18 -1
View File
@@ -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)
+23 -1
View File
@@ -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{}{
+3 -3
View File
@@ -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"),
},
}
+7
View File
@@ -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"
}
+13 -4
View File
@@ -29,6 +29,7 @@ export function Combobox({
const [open, setOpen] = React.useState(false);
const [search, setSearch] = React.useState("");
const containerRef = React.useRef<HTMLDivElement>(null);
const dropdownRef = React.useRef<HTMLDivElement>(null);
const [dropdownStyle, setDropdownStyle] = React.useState<React.CSSProperties>({});
// 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 && (
<div
ref={dropdownRef}
style={portalContainer ? dropdownStyle : undefined}
className={cn(
"bg-popover text-popover-foreground max-h-60 min-w-full overflow-y-auto rounded-md border p-1 shadow-md",
!portalContainer && "absolute top-full left-0 z-50 mt-1",
"bg-popover text-popover-foreground max-h-60 overflow-y-auto rounded-md border p-1 shadow-md",
portalContainer ? "overflow-hidden" : "absolute top-full left-0 z-50 mt-1 min-w-full",
)}
>
{filtered.map((o) => (
@@ -425,7 +425,7 @@ export function MediaProviderChainForm({
</DialogDescription>
</DialogHeader>
<div className="space-y-2 max-h-[60vh] overflow-y-auto pr-1">
<div className="space-y-2 max-h-[60vh] overflow-y-auto pr-1 my-4">
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={entries.map((e) => e.id)} strategy={verticalListSortingStrategy}>
{entries.map((entry, index) => (
@@ -101,11 +101,12 @@ export const MEDIA_PARAMS_SCHEMA: Record<string, Record<string, ParamField[]>> =
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" },
],
},
],
@@ -100,7 +100,7 @@ export function SkillDetailDialog({
return (
<Dialog open onOpenChange={() => onClose()}>
<DialogContent className="max-h-[85vh] overflow-hidden flex flex-col sm:max-w-2xl md:max-w-4xl lg:max-w-5xl xl:max-w-6xl 2xl:max-w-7xl">
<DialogContent className="max-h-[85vh] md:min-h-[60vh] overflow-hidden flex flex-col sm:max-w-2xl md:max-w-4xl lg:max-w-5xl xl:max-w-6xl 2xl:max-w-7xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 flex-wrap">
{skill.name}