Major refactoring cleanup: - Add new controller architecture (class-controller-*.php) - Add new settings-v2 UI (views/settings-v2/) - Add new CSS architecture (agentic-sidebar.css, tokens) - Add esbuild build pipeline (scripts/build.js, package.json) - Add composer dependencies (vendor/) - Add frontend src directory (assets/js/src/index.jsx) - Add documentation files - Remove old/obsolete files (class-settings.php, old CSS) This commits all pending changes from previous refactoring efforts.
363 lines
12 KiB
PHP
363 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* AI Provider Manager
|
|
*
|
|
* Routes AI requests to appropriate provider based on task type and configuration
|
|
*
|
|
* @package WP_Agentic_Writer
|
|
*/
|
|
|
|
if (!defined("ABSPATH")) {
|
|
exit();
|
|
}
|
|
|
|
/**
|
|
* Result object containing provider instance plus selection metadata.
|
|
* Used to satisfy the DoD Provider Transparency contract.
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
class WPAW_Provider_Selection_Result
|
|
{
|
|
public $provider; // Provider instance
|
|
public $selected_provider; // Original requested provider name
|
|
public $actual_provider; // Actually used provider name (may differ if fallback)
|
|
public $fallback_used; // True if fallback occurred
|
|
public $warnings; // Array of warning messages
|
|
|
|
public function __construct(
|
|
$provider,
|
|
$selected,
|
|
$actual,
|
|
$fallback,
|
|
$warnings = [],
|
|
) {
|
|
$this->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;
|
|
}
|
|
}
|