fix: allow OAuth providers in media tool chain (read_audio, read_image, etc.) (#150)

ExecuteWithChain previously required all providers to implement
credentialProvider (APIKey/APIBase). OAuth-based providers like
CodexProvider (ChatGPT OAuth) don't expose static credentials,
causing all media tools to fail with "does not expose API credentials".

Make credentialProvider optional (nil when unsupported). Each
callProvider gracefully falls back to the provider's Chat() API
when credentials are unavailable. Generation tools (create_image,
create_video, create_audio) return a clear error since they require
direct API access with no Chat fallback.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
This commit is contained in:
Luan VuandLuvu182 authored and GitHub committed 2026-03-11 16:40:35 +07:00
1 parent 2fdb791802
commit fa5f51e72e
7 files changed
+36 -26

No files matched your search

+3
View File
@@ -173,6 +173,9 @@ func (t *CreateAudioTool) Execute(ctx context.Context, args map[string]any) *Res
// callProvider dispatches to the correct music generation implementation based on provider type.
func (t *CreateAudioTool) callProvider(ctx context.Context, cp credentialProvider, providerName, model string, params map[string]any) ([]byte, *providers.Usage, error) {
if cp == nil {
return nil, nil, fmt.Errorf("provider %q does not expose API credentials required for audio generation", providerName)
}
prompt := GetParamString(params, "prompt", "")
slog.Info("create_audio: calling music generation API",
+3
View File
@@ -129,6 +129,9 @@ func (t *CreateImageTool) Execute(ctx context.Context, args map[string]any) *Res
// callProvider dispatches to the correct image generation implementation based on provider type.
func (t *CreateImageTool) callProvider(ctx context.Context, cp credentialProvider, providerName, model string, params map[string]any) ([]byte, *providers.Usage, error) {
if cp == nil {
return nil, nil, fmt.Errorf("provider %q does not expose API credentials required for image generation", providerName)
}
prompt := GetParamString(params, "prompt", "")
aspectRatio := GetParamString(params, "aspect_ratio", "1:1")
+3
View File
@@ -140,6 +140,9 @@ func (t *CreateVideoTool) Execute(ctx context.Context, args map[string]any) *Res
// callProvider dispatches to the correct video generation implementation based on provider type.
func (t *CreateVideoTool) callProvider(ctx context.Context, cp credentialProvider, providerName, model string, params map[string]any) ([]byte, *providers.Usage, error) {
if cp == nil {
return nil, nil, fmt.Errorf("provider %q does not expose API credentials required for video generation", providerName)
}
prompt := GetParamString(params, "prompt", "")
duration := GetParamInt(params, "duration", 8)
aspectRatio := GetParamString(params, "aspect_ratio", "16:9")
+4 -7
View File
@@ -169,13 +169,10 @@ func ExecuteWithChain(
continue
}
cp, ok := p.(credentialProvider)
if !ok {
slog.Warn("media_chain: provider does not expose credentials, skipping",
"provider", entry.Provider)
lastErr = fmt.Errorf("provider %q does not expose API credentials", entry.Provider)
continue
}
// credentialProvider is optional — providers that don't expose static
// credentials (e.g. OAuth-based CodexProvider) pass nil and each
// callProvider falls back to using the provider's Chat() API.
cp, _ := p.(credentialProvider)
// Retry loop for this provider
for attempt := 1; attempt <= entry.MaxRetries; attempt++ {
+19 -15
View File
@@ -60,24 +60,28 @@ func (t *ReadAudioTool) callProvider(ctx context.Context, cp credentialProvider,
data, _ := params["data"].([]byte)
mime := GetParamString(params, "mime", "audio/mpeg")
// Gemini: use File API (inlineData doesn't work for audio).
if strings.HasPrefix(providerName, "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 {
return nil, nil, fmt.Errorf("gemini file API: %w", err)
// 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 {
// Gemini: use File API (inlineData doesn't work for audio).
if strings.HasPrefix(providerName, "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 {
return nil, nil, fmt.Errorf("gemini file API: %w", err)
}
return []byte(resp.Content), resp.Usage, nil
}
return []byte(resp.Content), resp.Usage, nil
}
// OpenAI: use input_audio content part (supports wav, mp3).
if strings.HasPrefix(providerName, "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 {
return nil, nil, fmt.Errorf("openai audio call: %w", err)
// OpenAI: use input_audio content part (supports wav, mp3).
if strings.HasPrefix(providerName, "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 {
return nil, nil, fmt.Errorf("openai audio call: %w", err)
}
return []byte(resp.Content), resp.Usage, nil
}
return []byte(resp.Content), resp.Usage, nil
}
// Other providers: try standard Chat API with base64 audio as image_url (best effort).
+2 -2
View File
@@ -61,8 +61,8 @@ func (t *ReadDocumentTool) callProvider(ctx context.Context, cp credentialProvid
data, _ := params["data"].([]byte)
mime := GetParamString(params, "mime", "application/octet-stream")
// Gemini: use native API (OpenAI-compat endpoint doesn't support non-image MIME types).
if strings.HasPrefix(providerName, "gemini") {
// Gemini: use native API (requires credentials; OpenAI-compat endpoint doesn't support non-image MIME types).
if cp != nil && strings.HasPrefix(providerName, "gemini") {
slog.Info("read_document: using gemini native API",
"provider", providerName, "model", model,
"doc_size", len(data), "mime", mime)
+2 -2
View File
@@ -59,8 +59,8 @@ func (t *ReadVideoTool) callProvider(ctx context.Context, cp credentialProvider,
data, _ := params["data"].([]byte)
mime := GetParamString(params, "mime", "video/mp4")
// Gemini: use File API.
if strings.HasPrefix(providerName, "gemini") {
// Gemini: use File API (requires credentials).
if cp != nil && strings.HasPrefix(providerName, "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 {