PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.0.2
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.0.2
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / ai / class-brand-visibility-providers.php

class-brand-visibility-providers.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.0.2, at includes/ai/class-brand-visibility-providers.php

296 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Brand Visibility v2 — per-platform provider resolution.
4 *
5 * Measuring visibility means asking SEVERAL assistants the same question, so
6 * this feature can't be chained to the single site-wide AI provider the rest
7 * of ThinkRank uses. Each platform therefore gets its own optional key
8 * (`bv_key_<platform>`), falling back to the main provider key when the two
9 * happen to be the same vendor. A platform with no usable key is simply not
10 * offered in the wizard.
11 *
12 * Grounding honesty: plain Chat Completions answers reflect the model's
13 * training data, while the consumer apps people actually use often browse the
14 * live web. Perplexity's API is search-grounded; the others are not, and the
15 * UI labels them accordingly rather than implying all four measure the same
16 * thing.
17 *
18 * @package ThinkRank\AI
19 * @since 1.28.0
20 */
21
22 declare(strict_types=1);
23
24 namespace ThinkRank\AI;
25
26 use ThinkRank\Core\Settings;
27
28 if (!defined('ABSPATH')) {
29 exit;
30 }
31
32 /**
33 * Resolves platform slugs to configured AI clients.
34 *
35 * Not final: Brand_Visibility_Runner type-hints this class for its provider
36 * resolution, so a test that needs to drive the runner without reaching a real
37 * provider substitutes a subclass whose client_for() returns a canned client.
38 */
39 class Brand_Visibility_Providers {
40
41 /**
42 * Supported platforms.
43 *
44 * `grounded` drives the UI's honesty label. `main_provider` names the
45 * site-wide provider whose key this platform can borrow.
46 */
47 public const PLATFORMS = [
48 'chatgpt' => [
49 'label' => 'ChatGPT',
50 'vendor' => 'openai',
51 'main_provider' => 'openai',
52 'grounded' => false,
53 'default_model' => 'gpt-5-nano',
54 ],
55 'gemini' => [
56 'label' => 'Gemini',
57 'vendor' => 'gemini',
58 'main_provider' => 'gemini',
59 'grounded' => false,
60 'default_model' => Settings::DEFAULT_GEMINI_MODEL,
61 ],
62 'claude' => [
63 'label' => 'Claude',
64 'vendor' => 'claude',
65 'main_provider' => 'claude',
66 'grounded' => false,
67 'default_model' => Settings::DEFAULT_CLAUDE_MODEL,
68 ],
69 'perplexity' => [
70 'label' => 'Perplexity',
71 'vendor' => 'perplexity',
72 'main_provider' => '', // no site-wide equivalent — needs its own key
73 'grounded' => true,
74 'default_model' => Perplexity_Client::DEFAULT_MODEL,
75 ],
76 ];
77
78 /**
79 * Selectable models per platform.
80 *
81 * Deliberately a short, curated list rather than the vendor's full catalogue:
82 * Brand Visibility fires queries × platforms × samples calls per run, so the
83 * cheap/fast tiers are listed first and are the defaults. The whitelist also
84 * doubles as save-time validation — an unknown id is rejected rather than
85 * stored and failing later inside a queued run.
86 *
87 * @var array<string, array<int, array{value: string, label: string}>>
88 */
89 public const MODEL_CHOICES = [
90 'chatgpt' => [
91 ['value' => 'gpt-5-nano', 'label' => 'GPT-5 nano (fastest, lowest cost)'],
92 ['value' => 'gpt-5-mini', 'label' => 'GPT-5 mini (balanced)'],
93 ['value' => 'gpt-5', 'label' => 'GPT-5 (highest quality)'],
94 ['value' => 'gpt-4o', 'label' => 'GPT-4o (compatibility)'],
95 ],
96 'gemini' => [
97 ['value' => 'gemini-3.5-flash', 'label' => 'Gemini 3.5 Flash (recommended)'],
98 ['value' => 'gemini-3.1-flash-lite', 'label' => 'Gemini 3.1 Flash Lite (fastest)'],
99 ['value' => 'gemini-3.1-pro', 'label' => 'Gemini 3.1 Pro'],
100 ['value' => 'gemini-2.5-pro', 'label' => 'Gemini 2.5 Pro'],
101 ],
102 'claude' => [
103 ['value' => 'claude-haiku-4-5', 'label' => 'Claude Haiku 4.5 (fastest)'],
104 ['value' => 'claude-sonnet-5', 'label' => 'Claude Sonnet 5 (recommended)'],
105 ['value' => 'claude-opus-4-8', 'label' => 'Claude Opus 4.8 (most capable)'],
106 ],
107 'perplexity' => [
108 ['value' => 'sonar', 'label' => 'Sonar (search-grounded)'],
109 ['value' => 'sonar-pro', 'label' => 'Sonar Pro (deeper search)'],
110 ['value' => 'sonar-reasoning', 'label' => 'Sonar Reasoning'],
111 ],
112 ];
113
114 /**
115 * Settings accessor.
116 *
117 * @var Settings
118 */
119 private Settings $settings;
120
121 /**
122 * Constructor.
123 *
124 * @param Settings|null $settings Settings instance.
125 */
126 public function __construct(?Settings $settings = null) {
127 $this->settings = $settings ?? Settings::instance();
128 }
129
130 /**
131 * Option key holding a platform's dedicated API key.
132 *
133 * @param string $platform Platform slug.
134 * @return string
135 */
136 public static function key_option(string $platform): string {
137 return 'bv_key_' . $platform;
138 }
139
140 /**
141 * Option key holding a platform's chosen model.
142 *
143 * @param string $platform Platform slug.
144 * @return string
145 */
146 public static function model_option(string $platform): string {
147 return 'bv_model_' . $platform;
148 }
149
150 /**
151 * Whether a model id is offered for a platform.
152 *
153 * @param string $platform Platform slug.
154 * @param string $model Model id.
155 * @return bool
156 */
157 public static function is_valid_model(string $platform, string $model): bool {
158 foreach (self::MODEL_CHOICES[$platform] ?? [] as $choice) {
159 if ($choice['value'] === $model) {
160 return true;
161 }
162 }
163
164 return false;
165 }
166
167 /**
168 * The model a platform will actually use — its saved choice, else the
169 * platform default. A stored id that is no longer offered falls back to the
170 * default rather than failing mid-run.
171 *
172 * @param string $platform Platform slug.
173 * @return string Empty when the platform is unknown.
174 */
175 public function model_for(string $platform): string {
176 $config = self::PLATFORMS[$platform] ?? null;
177 if (null === $config) {
178 return '';
179 }
180
181 $saved = trim((string) $this->settings->get(self::model_option($platform), ''));
182
183 return self::is_valid_model($platform, $saved) ? $saved : (string) $config['default_model'];
184 }
185
186 /**
187 * Effective model for every platform, for the wizard's model pickers.
188 *
189 * @return array<string, string>
190 */
191 public function models(): array {
192 $out = [];
193
194 foreach (array_keys(self::PLATFORMS) as $slug) {
195 $out[$slug] = $this->model_for($slug);
196 }
197
198 return $out;
199 }
200
201 /**
202 * The API key to use for a platform: its own, else the site-wide key when
203 * the main provider is the same vendor.
204 *
205 * @param string $platform Platform slug.
206 * @return string Empty when the platform is unusable.
207 */
208 public function api_key_for(string $platform): string {
209 $config = self::PLATFORMS[$platform] ?? null;
210 if (null === $config) {
211 return '';
212 }
213
214 $own = trim((string) $this->settings->get(self::key_option($platform), ''));
215 if ('' !== $own) {
216 return $own;
217 }
218
219 // Borrow the site-wide key only when it belongs to the same vendor —
220 // an OpenAI key cannot talk to Anthropic.
221 $main_provider = (string) $this->settings->get('ai_provider', '');
222 if ('' !== $config['main_provider'] && $main_provider === $config['main_provider']) {
223 return trim((string) $this->settings->get('ai_api_key', ''));
224 }
225
226 return '';
227 }
228
229 /**
230 * Platforms with a usable key, for the wizard's platform picker.
231 *
232 * @return array<int, array{slug: string, label: string, grounded: bool, available: bool, uses_shared_key: bool}>
233 */
234 public function available_platforms(): array {
235 $out = [];
236
237 foreach (self::PLATFORMS as $slug => $config) {
238 $own_key = '' !== trim((string) $this->settings->get(self::key_option($slug), ''));
239
240 $out[] = [
241 'slug' => $slug,
242 'label' => $config['label'],
243 'grounded' => (bool) $config['grounded'],
244 'available' => '' !== $this->api_key_for($slug),
245 'uses_shared_key' => !$own_key && '' !== $this->api_key_for($slug),
246 ];
247 }
248
249 return $out;
250 }
251
252 /**
253 * Build a client for a platform.
254 *
255 * @param string $platform Platform slug.
256 * @return object A client exposing generate_completion(string, array): array.
257 * @throws \Exception When the platform is unknown or has no key.
258 */
259 public function client_for(string $platform) {
260 $config = self::PLATFORMS[$platform] ?? null;
261 if (null === $config) {
262 throw new \Exception(sprintf('Unknown Brand Visibility platform: %s', esc_html($platform)));
263 }
264
265 $api_key = $this->api_key_for($platform);
266 if ('' === $api_key) {
267 throw new \Exception(sprintf(
268 /* translators: %s: platform label, e.g. Claude. */
269 esc_html__('No API key configured for %s. Add one under Brand Visibility settings.', 'thinkrank'),
270 esc_html($config['label'])
271 ));
272 }
273
274 $model = $this->model_for($platform);
275
276 switch ($config['vendor']) {
277 case 'openai':
278 return '' !== $model
279 ? new OpenAI_Client($api_key, $model)
280 : new OpenAI_Client($api_key);
281 case 'claude':
282 return '' !== $model
283 ? new Claude_Client($api_key, $model)
284 : new Claude_Client($api_key);
285 case 'gemini':
286 return '' !== $model
287 ? new Gemini_Client($api_key, $model)
288 : new Gemini_Client($api_key);
289 case 'perplexity':
290 return new Perplexity_Client($api_key, $model);
291 }
292
293 throw new \Exception(sprintf('Unsupported vendor for platform: %s', esc_html($platform)));
294 }
295 }
296