| @@ -83,12 +83,21 @@ | ||
| 83 | 83 | 'gemini_api_key', |
| 84 | 84 | 'gemini_model', |
| 85 | 85 | 'openrouter_api_key', |
| 86 | 86 | 'openrouter_model', |
| 87 | + 'openai_compatible_base_url', | |
| 88 | + 'openai_compatible_api_key', | |
| 89 | + 'openai_compatible_model', | |
| 90 | + 'openai_compatible_timeout', | |
| 91 | + 'openai_compatible_supports_images', | |
| 92 | + 'openai_compatible_json_mode', | |
| 93 | + 'openai_compatible_price_per_million', | |
| 87 | 94 | 'max_tokens', |
| 88 | 95 | 'temperature', |
| 89 | 96 | 'cache_duration', |
| 90 | 97 | 'max_requests_per_minute', |
| 98 | + 'ai_daily_request_limit', | |
| 99 | + 'ai_paused', | |
| 91 | 100 | 'enable_logging', |
| 92 | 101 | 'debug_mode', |
| 93 | 102 | 'api_timeout', |
| 94 | 103 | 'retry_attempts', |
| @@ -257,12 +266,8 @@ | ||
| 257 | 266 | 'seo_analytics_google_analytics_property_id', |
| 258 | 267 | 'ga_analytics_account_id', |
| 259 | 268 | 'ga_analytics_data_stream_id', |
| 260 | 269 | |
| 261 | - // GA4 Tracking Code Injection (Pro) | |
| 262 | - 'ga4_auto_inject', | |
| 263 | - 'ga4_measurement_id', | |
| 264 | - | |
| 265 | 270 | // Search Console configuration |
| 266 | 271 | 'search_console_property', |
| 267 | 272 | |
| 268 | 273 | // AI features |
| @@ -809,8 +814,10 @@ | ||
| 809 | 814 | switch ($key) { |
| 810 | 815 | case 'openai_api_key': |
| 811 | 816 | case 'claude_api_key': |
| 812 | 817 | case 'openrouter_api_key': |
| 818 | + case 'openai_compatible_api_key': | |
| 819 | + case 'openai_compatible_model': | |
| 813 | 820 | if (!empty($value) && !is_string($value)) { |
| 814 | 821 | $validation['valid'] = false; |
| 815 | 822 | $validation['errors'][] = "{$key} must be a string"; |
| 816 | 823 | } |
| @@ -815,15 +822,29 @@ | ||
| 815 | 822 | $validation['errors'][] = "{$key} must be a string"; |
| 816 | 823 | } |
| 817 | 824 | break; |
| 818 | 825 | |
| 826 | + case 'openai_compatible_base_url': | |
| 827 | + // An unreachable or dangerous URL is refused with a reason | |
| 828 | + // rather than quietly stored (see Endpoint_URL_Validator). | |
| 829 | + if (!empty($value)) { | |
| 830 | + $validated = \ThinkRank\AI\Endpoint_URL_Validator::validate((string) $value); | |
| 831 | + if (is_wp_error($validated)) { | |
| 832 | + $validation['valid'] = false; | |
| 833 | + $validation['errors'][] = $validated->get_error_message(); | |
| 834 | + } | |
| 835 | + } | |
| 836 | + break; | |
| 837 | + | |
| 819 | 838 | case 'max_tokens': |
| 820 | 839 | case 'cache_duration': |
| 821 | 840 | case 'max_requests_per_minute': |
| 841 | + case 'ai_daily_request_limit': | |
| 822 | 842 | case 'seo_score_threshold': |
| 823 | 843 | case 'api_timeout': |
| 824 | 844 | case 'retry_attempts': |
| 825 | 845 | case 'data_retention_days': |
| 846 | + case 'openai_compatible_timeout': | |
| 826 | 847 | if (!is_numeric($value) || $value < 0) { |
| 827 | 848 | $validation['valid'] = false; |
| 828 | 849 | $validation['errors'][] = "{$key} must be a positive number"; |
| 829 | 850 | } |
| @@ -836,11 +857,15 @@ | ||
| 836 | 857 | } |
| 837 | 858 | break; |
| 838 | 859 | |
| 839 | 860 | case 'ai_provider': |
| 840 | - if (!in_array($value, ['openai', 'claude', 'gemini', 'openrouter'], true)) { | |
| 861 | + // '' is legal: it is Settings::AI_PROVIDER_NONE, the state a | |
| 862 | + // fresh install starts in and the one a user returns to by | |
| 863 | + // deselecting their provider (#572). | |
| 864 | + if (!in_array($value, \ThinkRank\Core\Settings::selectable_ai_providers(), true)) { | |
| 841 | 865 | $validation['valid'] = false; |
| 842 | - $validation['errors'][] = "ai_provider must be 'openai', 'claude', 'gemini', or 'openrouter'"; | |
| 866 | + $validation['errors'][] = "ai_provider must be empty (no provider) or one of: " | |
| 867 | + . implode(', ', \ThinkRank\Core\Settings::SUPPORTED_AI_PROVIDERS); | |
| 843 | 868 | } |
| 844 | 869 | break; |
| 845 | 870 | |
| 846 | 871 | case 'dashboard_widgets': |