provider = $provider; $this->selected_provider = $selected; $this->actual_provider = $actual; $this->fallback_used = $fallback; $this->warnings = $warnings; } } class WP_Agentic_Writer_Provider_Manager { /** * Transient cache TTL for connection test results (5 minutes). * * @var int */ const CONNECTION_TEST_CACHE_TTL = 300; /** * Transient cache key prefix for connection tests. * * @var string */ const CONNECTION_TEST_CACHE_PREFIX = "wpaw_conn_test_"; /** * Get provider instance for specific task type * * @param string $type Task type (chat, clarity, planning, writing, refinement, image). * @return WPAW_Provider_Selection_Result Provider selection result with metadata. */ public static function get_provider_for_task($type) { $settings = get_option("wp_agentic_writer_settings", []); $task_providers = $settings["task_providers"] ?? []; $allow_openrouter_fallback = !empty( $settings["allow_openrouter_fallback"] ); // Determine which provider to use for this task $requested_provider = $task_providers[$type] ?? "openrouter"; if (defined("WP_DEBUG") && WP_DEBUG) { error_log( "WPAW Provider Manager: task={$type}, provider_name={$requested_provider}, task_providers=" . json_encode($task_providers), ); } $warnings = []; $fallback_used = false; $actual_provider = $requested_provider; // Get provider instance with fallback logic $provider = self::get_provider_instance($requested_provider, $type); $can_fallback_to_openrouter = "openrouter" === $requested_provider || $allow_openrouter_fallback; // If provider not configured or unavailable. if (!$provider || !$provider->is_configured()) { if (defined("WP_DEBUG") && WP_DEBUG) { error_log( "Provider '{$requested_provider}' not available for task '{$type}'", ); } // Never silently spend OpenRouter credits when user selected another provider. if (!$can_fallback_to_openrouter) { $warnings[] = "Provider '{$requested_provider}' unavailable. No automatic fallback was applied."; return new WPAW_Provider_Selection_Result( $provider, $requested_provider, $requested_provider, false, $warnings, ); } $warnings[] = "Provider '{$requested_provider}' unavailable, fell back to OpenRouter"; $provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance(); $actual_provider = "openrouter"; $fallback_used = true; } // For custom endpoint, verify it's actually reachable before using it. // Use transient cache to avoid testing connection on every request (5 min TTL). if ( "local_backend" === $requested_provider && !$fallback_used && method_exists($provider, "test_connection") ) { $cache_key = self::CONNECTION_TEST_CACHE_PREFIX . md5($requested_provider . $type); $cached_result = get_transient($cache_key); if (false !== $cached_result) { // Use cached result if (is_wp_error($cached_result)) { $test_result = $cached_result; } else { // Cached success - skip test entirely if (defined("WP_DEBUG") && WP_DEBUG) { error_log( "WPAW: Using cached connection test result for '{$requested_provider}'", ); } $test_result = null; // null means "test passed" } } else { // No cache - run the actual test $test_result = $provider->test_connection(); // Cache the result for CONNECTION_TEST_CACHE_TTL seconds set_transient( $cache_key, $test_result, self::CONNECTION_TEST_CACHE_TTL, ); } if (isset($test_result) && is_wp_error($test_result)) { if (defined("WP_DEBUG") && WP_DEBUG) { error_log( "Custom endpoint not reachable for task '{$type}'. Error: " . $test_result->get_error_message(), ); } if ($can_fallback_to_openrouter) { $warnings[] = "Custom endpoint not reachable, fell back to OpenRouter."; $provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance(); $actual_provider = "openrouter"; $fallback_used = true; } else { $warnings[] = "Custom endpoint not reachable. No automatic fallback was applied."; } } } return new WPAW_Provider_Selection_Result( $provider, $requested_provider, $actual_provider, $fallback_used, $warnings, ); } /** * Get provider instance by name * * @param string $provider_name Provider identifier. * @param string $task_type Task type for validation. * @return WP_Agentic_Writer_AI_Provider_Interface|null Provider instance or null. */ private static function get_provider_instance($provider_name, $task_type) { switch ($provider_name) { case "local_backend": if (!class_exists("WP_Agentic_Writer_Local_Backend_Provider")) { require_once plugin_dir_path(__FILE__) . "class-local-backend-provider.php"; } $provider = new WP_Agentic_Writer_Local_Backend_Provider(); break; case "codex": if (!class_exists("WP_Agentic_Writer_Codex_Provider")) { require_once plugin_dir_path(__FILE__) . "class-codex-provider.php"; } $provider = new WP_Agentic_Writer_Codex_Provider(); break; case "openrouter": default: $provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance(); break; } // Validate provider supports this task type if ($provider && !$provider->supports_task_type($task_type)) { error_log( "Provider '{$provider_name}' does not support task type '{$task_type}'", ); return null; } return $provider; } /** * Get all available providers with their status * * @return array Array of provider info with name, status, supported tasks. */ public static function get_available_providers() { $providers = []; // OpenRouter (always available) $openrouter = WP_Agentic_Writer_OpenRouter_Provider::get_instance(); $providers["openrouter"] = [ "name" => "OpenRouter", "configured" => $openrouter->is_configured(), "supports" => [ "chat", "clarity", "planning", "writing", "refinement", "image", ], "icon" => "☁️", ]; // Custom Endpoint if (class_exists("WP_Agentic_Writer_Local_Backend_Provider")) { $local = new WP_Agentic_Writer_Local_Backend_Provider(); $providers["local_backend"] = [ "name" => "Custom Endpoint", "configured" => $local->is_configured(), "supports" => [ "chat", "clarity", "planning", "writing", "refinement", ], "icon" => "🏠", ]; } // Codex if (class_exists("WP_Agentic_Writer_Codex_Provider")) { $codex = new WP_Agentic_Writer_Codex_Provider(); $providers["codex"] = [ "name" => "Codex (OpenAI)", "configured" => $codex->is_configured(), "supports" => [ "chat", "clarity", "planning", "writing", "refinement", ], "icon" => "🔗", ]; } return $providers; } /** * Test all configured providers * * @return array Results of connection tests. */ public static function test_all_providers() { $results = []; $providers = self::get_available_providers(); foreach ($providers as $key => $info) { if (!$info["configured"]) { $results[$key] = [ "success" => false, "message" => "Not configured", ]; continue; } $provider = self::get_provider_instance($key, "chat"); if ($provider) { $test_result = $provider->test_connection(); $results[$key] = is_wp_error($test_result) ? [ "success" => false, "message" => $test_result->get_error_message(), ] : $test_result; } } return $results; } /** * Clear the connection test transient cache. * Call this when local backend settings are updated. * * @return int Number of cache entries deleted. */ public static function clear_connection_test_cache() { global $wpdb; $prefix = self::CONNECTION_TEST_CACHE_PREFIX; $count = 0; // Delete all transients with our prefix $transients = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like("_transient_") . $prefix . "%", ), ); foreach ($transients as $transient) { // Strip '_transient_' prefix if present $key = $transient; if (0 === strpos($key, "_transient_")) { $key = substr($key, 11); } if (delete_transient($key)) { $count++; } } if (defined("WP_DEBUG") && WP_DEBUG) { error_log("WPAW: Cleared {$count} connection test cache entries"); } return $count; } }