diff --git a/internal/tools/create_audio.go b/internal/tools/create_audio.go index 2a11187a..a9d80328 100644 --- a/internal/tools/create_audio.go +++ b/internal/tools/create_audio.go @@ -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", diff --git a/internal/tools/create_image.go b/internal/tools/create_image.go index fbf34c45..67a81c64 100644 --- a/internal/tools/create_image.go +++ b/internal/tools/create_image.go @@ -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") diff --git a/internal/tools/create_video.go b/internal/tools/create_video.go index 51f3d2a5..29c12d3e 100644 --- a/internal/tools/create_video.go +++ b/internal/tools/create_video.go @@ -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") diff --git a/internal/tools/media_provider_chain.go b/internal/tools/media_provider_chain.go index c782c344..7b745694 100644 --- a/internal/tools/media_provider_chain.go +++ b/internal/tools/media_provider_chain.go @@ -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++ { diff --git a/internal/tools/read_audio_resolve.go b/internal/tools/read_audio_resolve.go index c8405248..e86c72ce 100644 --- a/internal/tools/read_audio_resolve.go +++ b/internal/tools/read_audio_resolve.go @@ -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). diff --git a/internal/tools/read_document_resolve.go b/internal/tools/read_document_resolve.go index 3ce994b1..222f6a67 100644 --- a/internal/tools/read_document_resolve.go +++ b/internal/tools/read_document_resolve.go @@ -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) diff --git a/internal/tools/read_video_resolve.go b/internal/tools/read_video_resolve.go index bd4f9831..24c45586 100644 --- a/internal/tools/read_video_resolve.go +++ b/internal/tools/read_video_resolve.go @@ -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 {