From b9e9e6e34af3e6984f85e1ccee149d0aa5be5d29 Mon Sep 17 00:00:00 2001 From: viettranx Date: Tue, 10 Mar 2026 22:54:52 +0700 Subject: [PATCH] refactor(media): migrate builtin tool settings from legacy flat to chain format - Update seed defaults to use chain format {"providers":[...]} - Add startup auto-migration for existing legacy flat settings in DB - Remove legacy flat format parsing from parseChainSettings() --- cmd/gateway.go | 1 + cmd/gateway_builtin_tools.go | 88 +++++++++++++++++++++++--- internal/tools/media_provider_chain.go | 52 +++++---------- 3 files changed, 96 insertions(+), 45 deletions(-) diff --git a/cmd/gateway.go b/cmd/gateway.go index 248a688d..a6e554d9 100644 --- a/cmd/gateway.go +++ b/cmd/gateway.go @@ -658,6 +658,7 @@ func runGateway() { // Seed + apply builtin tool disables if pgStores.BuiltinTools != nil { seedBuiltinTools(context.Background(), pgStores.BuiltinTools) + migrateBuiltinToolSettings(context.Background(), pgStores.BuiltinTools) applyBuiltinToolDisables(context.Background(), pgStores.BuiltinTools, toolsReg) } diff --git a/cmd/gateway_builtin_tools.go b/cmd/gateway_builtin_tools.go index 80bc6ccf..b21528e2 100644 --- a/cmd/gateway_builtin_tools.go +++ b/cmd/gateway_builtin_tools.go @@ -42,33 +42,33 @@ func builtinToolSeedData() []store.BuiltinToolDef { Requires: []string{"knowledge_graph"}, }, - // media + // media — seed uses chain format: {"providers":[...]} {Name: "read_image", DisplayName: "Read Image", Description: "Analyze images using a vision-capable LLM provider", Category: "media", Enabled: true, - Settings: json.RawMessage(`{"provider":"openrouter","model":"google/gemini-2.5-flash-image"}`), + Settings: json.RawMessage(`{"providers":[{"provider":"openrouter","model":"google/gemini-2.5-flash-image","enabled":true,"timeout":120,"max_retries":2}]}`), Requires: []string{"vision_provider"}, }, {Name: "read_document", DisplayName: "Read Document", Description: "Analyze documents (PDF, Word, Excel, PowerPoint, CSV, etc.) using a document-capable LLM provider", Category: "media", Enabled: true, - Settings: json.RawMessage(`{"provider":"gemini","model":"gemini-2.5-flash"}`), + Settings: json.RawMessage(`{"providers":[{"provider":"gemini","model":"gemini-2.5-flash","enabled":true,"timeout":120,"max_retries":2}]}`), Requires: []string{"document_provider"}, }, {Name: "create_image", DisplayName: "Create Image", Description: "Generate images from text prompts using an image generation provider", Category: "media", Enabled: true, - Settings: json.RawMessage(`{"provider":"openrouter","model":"google/gemini-2.5-flash-image"}`), + Settings: json.RawMessage(`{"providers":[{"provider":"openrouter","model":"google/gemini-2.5-flash-image","enabled":true,"timeout":120,"max_retries":2}]}`), Requires: []string{"image_gen_provider"}, }, {Name: "read_audio", DisplayName: "Read Audio", Description: "Analyze audio files (speech, music, sounds) using an audio-capable LLM provider", Category: "media", Enabled: true, - Settings: json.RawMessage(`{"provider":"gemini","model":"gemini-2.5-flash"}`), + Settings: json.RawMessage(`{"providers":[{"provider":"gemini","model":"gemini-2.5-flash","enabled":true,"timeout":120,"max_retries":2}]}`), Requires: []string{"audio_provider"}, }, {Name: "read_video", DisplayName: "Read Video", Description: "Analyze video files using a video-capable LLM provider", Category: "media", Enabled: true, - Settings: json.RawMessage(`{"provider":"gemini","model":"gemini-2.5-flash"}`), + Settings: json.RawMessage(`{"providers":[{"provider":"gemini","model":"gemini-2.5-flash","enabled":true,"timeout":120,"max_retries":2}]}`), Requires: []string{"video_provider"}, }, {Name: "create_video", DisplayName: "Create Video", Description: "Generate videos from text descriptions using AI", Category: "media", Enabled: true, - Settings: json.RawMessage(`{"provider":"gemini","model":"veo-3.0-generate-preview"}`), + Settings: json.RawMessage(`{"providers":[{"provider":"gemini","model":"veo-3.0-generate-preview","enabled":true,"timeout":120,"max_retries":2}]}`), Requires: []string{"video_gen_provider"}, }, {Name: "create_audio", DisplayName: "Create Audio", Description: "Generate music or sound effects from text descriptions using AI", Category: "media", Enabled: true, - Settings: json.RawMessage(`{"provider":"minimax","model":"music-2.5+"}`), + Settings: json.RawMessage(`{"providers":[{"provider":"minimax","model":"music-2.5+","enabled":true,"timeout":120,"max_retries":2}]}`), Requires: []string{"audio_gen_provider"}, }, {Name: "tts", DisplayName: "Text to Speech", Description: "Convert text to natural-sounding speech audio", Category: "media", Enabled: true, @@ -137,6 +137,78 @@ func seedBuiltinTools(ctx context.Context, bts store.BuiltinToolStore) { slog.Info("builtin tools seeded", "count", len(seeds)) } +// mediaToolNames lists media tools whose settings should use chain format. +var mediaToolNames = map[string]bool{ + "read_image": true, "read_document": true, "create_image": true, + "read_audio": true, "read_video": true, "create_video": true, "create_audio": true, +} + +// migrateBuiltinToolSettings converts legacy flat settings {"provider":"X","model":"Y"} +// to chain format {"providers":[...]} in the database. Runs once at startup. +func migrateBuiltinToolSettings(ctx context.Context, bts store.BuiltinToolStore) { + all, err := bts.List(ctx) + if err != nil { + slog.Warn("builtin_tools: failed to list for migration", "error", err) + return + } + + var migrated int + for _, t := range all { + if !mediaToolNames[t.Name] { + continue + } + if len(t.Settings) == 0 || string(t.Settings) == "{}" { + continue + } + + // Detect legacy flat format: has "provider" key but no "providers" key + var raw map[string]json.RawMessage + if err := json.Unmarshal(t.Settings, &raw); err != nil { + continue + } + if _, hasProviders := raw["providers"]; hasProviders { + continue // already chain format + } + if _, hasProvider := raw["provider"]; !hasProvider { + continue // neither format, skip + } + + // Parse legacy flat fields + var flat struct { + Provider string `json:"provider"` + Model string `json:"model"` + } + if err := json.Unmarshal(t.Settings, &flat); err != nil || flat.Provider == "" { + continue + } + + // Convert to chain format + chain := map[string]any{ + "providers": []map[string]any{{ + "provider": flat.Provider, + "model": flat.Model, + "enabled": true, + "timeout": 120, + "max_retries": 2, + }}, + } + newSettings, err := json.Marshal(chain) + if err != nil { + continue + } + + if err := bts.Update(ctx, t.Name, map[string]any{"settings": json.RawMessage(newSettings)}); err != nil { + slog.Warn("builtin_tools: failed to migrate settings", "tool", t.Name, "error", err) + continue + } + migrated++ + } + + if migrated > 0 { + slog.Info("builtin_tools: migrated legacy settings to chain format", "count", migrated) + } +} + // applyBuiltinToolDisables unregisters disabled builtin tools from the registry. // Called at startup and on cache invalidation. func applyBuiltinToolDisables(ctx context.Context, bts store.BuiltinToolStore, toolsReg *tools.Registry) { diff --git a/internal/tools/media_provider_chain.go b/internal/tools/media_provider_chain.go index 1725a22b..c782c344 100644 --- a/internal/tools/media_provider_chain.go +++ b/internal/tools/media_provider_chain.go @@ -25,12 +25,10 @@ type MediaProviderEntry struct { } // mediaProviderChain is the settings JSON structure for media tools. -// Supports both new (providers array) and legacy (flat provider/model) formats. +// Only supports chain format: {"providers":[...]}. +// Legacy flat format is auto-migrated at startup (see cmd/gateway_builtin_tools.go). type mediaProviderChain struct { Providers []MediaProviderEntry `json:"providers,omitempty"` - // Legacy fields (backward compat) - Provider string `json:"provider,omitempty"` - Model string `json:"model,omitempty"` } // applyDefaults fills in zero-value fields with sensible defaults. @@ -97,41 +95,21 @@ func parseChainSettings(raw []byte, defaultModels map[string]string) []MediaProv return nil } - // New format: providers array - if len(chain.Providers) > 0 { - var result []MediaProviderEntry - for _, e := range chain.Providers { - if !e.Enabled { - continue - } - if e.Provider == "" { - continue - } - if e.Model == "" { - e.Model = defaultModels[e.Provider] - } - e.applyDefaults() - result = append(result, e) + var result []MediaProviderEntry + for _, e := range chain.Providers { + if !e.Enabled { + continue } - return result + if e.Provider == "" { + continue + } + if e.Model == "" { + e.Model = defaultModels[e.Provider] + } + e.applyDefaults() + result = append(result, e) } - - // Legacy format: flat provider/model - if chain.Provider != "" { - model := chain.Model - if model == "" { - model = defaultModels[chain.Provider] - } - entry := MediaProviderEntry{ - Provider: chain.Provider, - Model: model, - Enabled: true, - } - entry.applyDefaults() - return []MediaProviderEntry{entry} - } - - return nil + return result } // buildDefaultChain creates a chain from the hardcoded priority list,