mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-19 08:15:12 +00:00
fix: resolve media provider type from DB instead of guessing from name (#154)
Media tools (create_image, create_video, create_audio, read_audio, read_video, read_document) routed API calls based on provider name pattern matching (e.g. strings.HasPrefix(name, "gemini")). This breaks when users give custom names to DB providers — a Gemini provider named "chatgpt-sap-het" would be misrouted to the OpenAI-compat endpoint, causing 404 errors. Fix: carry the DB provider_type through OpenAIProvider, resolve it via typedProvider interface in ExecuteWithChain, and inject as _provider_type param for callProvider routing. Name-based heuristic kept as fallback for config-file providers that don't have a DB type. Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
This commit is contained in:
1 parent
d6f83afda5
commit
405a753239
9 files changed
+64
-12
No files matched your search
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user