diff --git a/cmd/gateway_providers.go b/cmd/gateway_providers.go index 0ce69f9f..4507fad0 100644 --- a/cmd/gateway_providers.go +++ b/cmd/gateway_providers.go @@ -318,6 +318,7 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi registry.Register(providers.NewOpenAIProvider(p.Name, p.APIKey, base, "")) default: prov := providers.NewOpenAIProvider(p.Name, p.APIKey, p.APIBase, "") + prov.WithProviderType(p.ProviderType) if p.ProviderType == store.ProviderMiniMax { prov.WithChatPath("/text/chatcompletion_v2") } diff --git a/internal/providers/openai.go b/internal/providers/openai.go index f1b42ce5..f9ad5f8f 100644 --- a/internal/providers/openai.go +++ b/internal/providers/openai.go @@ -20,6 +20,7 @@ type OpenAIProvider struct { apiBase string chatPath string // defaults to "/chat/completions" defaultModel string + providerType string // DB provider_type (e.g. "gemini_native", "openai", "minimax_native") client *http.Client retryConfig RetryConfig } @@ -52,6 +53,13 @@ func (p *OpenAIProvider) DefaultModel() string { return p.defaultModel } func (p *OpenAIProvider) SupportsThinking() bool { return true } func (p *OpenAIProvider) APIKey() string { return p.apiKey } func (p *OpenAIProvider) APIBase() string { return p.apiBase } +func (p *OpenAIProvider) ProviderType() string { return p.providerType } + +// WithProviderType sets the DB provider_type for correct API endpoint routing in media tools. +func (p *OpenAIProvider) WithProviderType(pt string) *OpenAIProvider { + p.providerType = pt + return p +} // resolveModel returns the model ID to use for a request. // For OpenRouter, model IDs require a provider prefix (e.g. "anthropic/claude-sonnet-4-5-20250929"). diff --git a/internal/tools/create_audio.go b/internal/tools/create_audio.go index a9d80328..61494e47 100644 --- a/internal/tools/create_audio.go +++ b/internal/tools/create_audio.go @@ -181,7 +181,7 @@ func (t *CreateAudioTool) callProvider(ctx context.Context, cp credentialProvide slog.Info("create_audio: calling music generation API", "provider", providerName, "model", model) - switch ProviderTypeFromName(providerName) { + switch GetParamString(params, "_provider_type", providerTypeFromName(providerName)) { case "minimax": return callMinimaxMusicGen(ctx, cp.APIKey(), cp.APIBase(), model, prompt, params) case "suno": diff --git a/internal/tools/create_image.go b/internal/tools/create_image.go index 6d8d1c90..4b79dfab 100644 --- a/internal/tools/create_image.go +++ b/internal/tools/create_image.go @@ -131,7 +131,7 @@ func (t *CreateImageTool) callProvider(ctx context.Context, cp credentialProvide slog.Info("create_image: calling image generation API", "provider", providerName, "model", model, "aspect_ratio", aspectRatio) - switch ProviderTypeFromName(providerName) { + switch GetParamString(params, "_provider_type", providerTypeFromName(providerName)) { case "gemini": return t.callGeminiNativeImageGen(ctx, cp.APIKey(), cp.APIBase(), model, prompt, params) case "openrouter": diff --git a/internal/tools/create_video.go b/internal/tools/create_video.go index 29c12d3e..7a057b56 100644 --- a/internal/tools/create_video.go +++ b/internal/tools/create_video.go @@ -150,7 +150,7 @@ func (t *CreateVideoTool) callProvider(ctx context.Context, cp credentialProvide slog.Info("create_video: calling video generation API", "provider", providerName, "model", model, "duration", duration, "aspect_ratio", aspectRatio) - switch ProviderTypeFromName(providerName) { + switch GetParamString(params, "_provider_type", providerTypeFromName(providerName)) { case "gemini": return t.callGeminiVideoGen(ctx, cp.APIKey(), cp.APIBase(), model, prompt, duration, aspectRatio, params) case "minimax": diff --git a/internal/tools/media_provider_chain.go b/internal/tools/media_provider_chain.go index 7b745694..dfeda71c 100644 --- a/internal/tools/media_provider_chain.go +++ b/internal/tools/media_provider_chain.go @@ -174,6 +174,14 @@ func ExecuteWithChain( // callProvider falls back to using the provider's Chat() API. cp, _ := p.(credentialProvider) + // Inject resolved provider type into params so callProvider can route correctly. + // This uses the actual DB provider_type instead of guessing from the name. + resolvedType := ResolveProviderType(p) + if entry.Params == nil { + entry.Params = make(map[string]any) + } + entry.Params["_provider_type"] = resolvedType + // Retry loop for this provider for attempt := 1; attempt <= entry.MaxRetries; attempt++ { timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(entry.Timeout)*time.Second) @@ -281,9 +289,42 @@ func GetParamInt(params map[string]any, key string, fallback int) int { return fallback } -// ProviderTypeFromName returns the provider_type based on known provider naming patterns. -// Used to determine which API endpoint to call. -func ProviderTypeFromName(name string) string { +// typedProvider is optionally implemented by providers that carry their DB provider_type. +type typedProvider interface { + ProviderType() string +} + +// dbTypeToMediaType maps DB provider_type values to the media routing type +// used by callProvider switch statements. +var dbTypeToMediaType = map[string]string{ + "gemini_native": "gemini", + "openai": "openai", + "openrouter": "openrouter", + "minimax_native": "minimax", + "dashscope": "dashscope", + "anthropic_native": "anthropic", + "suno": "suno", +} + +// ResolveProviderType returns the media routing type for a provider. +// It first checks the provider's DB type (via typedProvider interface), +// then falls back to name-based heuristics for config-registered providers. +func ResolveProviderType(p providers.Provider) string { + // Prefer the actual DB provider_type when available + if tp, ok := p.(typedProvider); ok { + if pt := tp.ProviderType(); pt != "" { + if mt, found := dbTypeToMediaType[pt]; found { + return mt + } + } + } + // Fallback: infer from provider name (for config-registered providers) + return providerTypeFromName(p.Name()) +} + +// providerTypeFromName infers provider type from naming patterns. +// Used as fallback when the provider doesn't carry its DB type. +func providerTypeFromName(name string) string { switch { case name == "gemini" || strings.HasPrefix(name, "gemini"): return "gemini" diff --git a/internal/tools/read_audio_resolve.go b/internal/tools/read_audio_resolve.go index 15489c50..47ad7a24 100644 --- a/internal/tools/read_audio_resolve.go +++ b/internal/tools/read_audio_resolve.go @@ -62,12 +62,13 @@ func (t *ReadAudioTool) callProvider(ctx context.Context, cp credentialProvider, // Provider-specific paths require API credentials; skip when cp is nil // (e.g. OAuth-based providers that don't expose static keys). - if cp == nil && (strings.HasPrefix(providerName, "gemini") || strings.HasPrefix(providerName, "openai")) { + ptype := GetParamString(params, "_provider_type", providerTypeFromName(providerName)) + if cp == nil && (ptype == "gemini" || ptype == "openai") { slog.Info("read_audio: no API credentials, falling back to Chat API", "provider", providerName) } if cp != nil { // Gemini: use File API (inlineData doesn't work for audio). - if strings.HasPrefix(providerName, "gemini") { + if ptype == "gemini" { slog.Info("read_audio: using gemini file API", "provider", providerName, "model", model, "size", len(data), "mime", mime) resp, err := geminiFileAPICall(ctx, cp.APIKey(), model, prompt, data, mime, 120*time.Second) if err != nil { @@ -77,7 +78,7 @@ func (t *ReadAudioTool) callProvider(ctx context.Context, cp credentialProvider, } // OpenAI: use input_audio content part (supports wav, mp3). - if strings.HasPrefix(providerName, "openai") { + if ptype == "openai" { slog.Info("read_audio: using openai input_audio API", "provider", providerName, "model", model, "size", len(data), "mime", mime) resp, err := openaiAudioCall(ctx, cp.APIKey(), cp.APIBase(), model, prompt, data, mime) if err != nil { diff --git a/internal/tools/read_document_resolve.go b/internal/tools/read_document_resolve.go index 222f6a67..67269a09 100644 --- a/internal/tools/read_document_resolve.go +++ b/internal/tools/read_document_resolve.go @@ -62,7 +62,8 @@ func (t *ReadDocumentTool) callProvider(ctx context.Context, cp credentialProvid mime := GetParamString(params, "mime", "application/octet-stream") // Gemini: use native API (requires credentials; OpenAI-compat endpoint doesn't support non-image MIME types). - if cp != nil && strings.HasPrefix(providerName, "gemini") { + ptype := GetParamString(params, "_provider_type", providerTypeFromName(providerName)) + if cp != nil && ptype == "gemini" { slog.Info("read_document: using gemini native API", "provider", providerName, "model", model, "doc_size", len(data), "mime", mime) diff --git a/internal/tools/read_video_resolve.go b/internal/tools/read_video_resolve.go index 24c45586..85bb3682 100644 --- a/internal/tools/read_video_resolve.go +++ b/internal/tools/read_video_resolve.go @@ -6,7 +6,6 @@ import ( "fmt" "log/slog" "path/filepath" - "strings" "time" "github.com/nextlevelbuilder/goclaw/internal/providers" @@ -60,7 +59,8 @@ func (t *ReadVideoTool) callProvider(ctx context.Context, cp credentialProvider, mime := GetParamString(params, "mime", "video/mp4") // Gemini: use File API (requires credentials). - if cp != nil && strings.HasPrefix(providerName, "gemini") { + ptype := GetParamString(params, "_provider_type", providerTypeFromName(providerName)) + if cp != nil && ptype == "gemini" { slog.Info("read_video: using gemini file API", "provider", providerName, "model", model, "size", len(data), "mime", mime) resp, err := geminiFileAPICall(ctx, cp.APIKey(), model, prompt, data, mime, 180*time.Second) if err != nil {