PluginProbe
BotWriter – AI Writer & SEO Content Generator / 3.4.8
BotWriter – AI Writer & SEO Content Generator v3.4.8
3.4.10 3.4.9 3.4.8 3.4.7 3.4.6 3.4.4 3.4.2 3.4.1 3.4.0 3.3.9 3.3.8 3.3.7 3.3.6 3.3.5 3.3.4 3.3.3 3.3.2 3.3.1 3.3.0 3.2.8 trunk 1.3.0 1.3.1 1.3.2 1.3.3 All 51 releases
botwriter / includes / models.php

models.php in BotWriter – AI Writer & SEO Content Generator 3.4.8, at includes/models.php

361 lines 11.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * BotWriter Models Manager
4 *
5 * Handles loading, storing and updating AI provider models from JSON.
6 * Models are stored in WordPress options and can be updated dynamically.
7 *
8 * @package BotWriter
9 */
10
11 if (!defined('ABSPATH')) {
12 exit;
13 }
14
15 /**
16 * Option name for storing models data
17 */
18 define('BOTWRITER_MODELS_OPTION', 'botwriter_models_data');
19
20 /**
21 * Deprecated model IDs that must not appear as selectable options.
22 *
23 * @param string $provider Provider key.
24 * @param string $model_id Model ID.
25 * @return bool
26 */
27 function botwriter_is_deprecated_text_model_choice($provider, $model_id) {
28 $provider = sanitize_key((string) $provider);
29 $model_id = strtolower(trim((string) $model_id));
30
31 if ($model_id === '') {
32 return false;
33 }
34
35 $blocked = [
36 'google' => [
37 'gemini-2.5-flash',
38 'gemini-2.5-flash-lite',
39 'models/gemini-2.5-flash',
40 'models/gemini-2.5-flash-lite',
41 ],
42 'openrouter' => [
43 'google/gemini-2.5-flash',
44 ],
45 ];
46
47 if (!isset($blocked[$provider])) {
48 return false;
49 }
50
51 return in_array($model_id, $blocked[$provider], true);
52 }
53
54 /**
55 * Filter deprecated model IDs from a model map.
56 *
57 * @param string $provider Provider key.
58 * @param array $models Map model_id => model_name.
59 * @return array
60 */
61 function botwriter_filter_deprecated_text_models($provider, $models) {
62 if (!is_array($models)) {
63 return [];
64 }
65
66 $filtered = [];
67 foreach ($models as $model_id => $model_name) {
68 if (botwriter_is_deprecated_text_model_choice($provider, $model_id)) {
69 continue;
70 }
71 $filtered[$model_id] = $model_name;
72 }
73
74 return $filtered;
75 }
76
77 /**
78 * Sanitize stored models data to enforce current curated defaults.
79 *
80 * @param array $models_data Models catalog data.
81 * @return array [sanitized_data, changed]
82 */
83 function botwriter_sanitize_models_data($models_data) {
84 if (!is_array($models_data) || empty($models_data['providers']) || !is_array($models_data['providers'])) {
85 return [$models_data, false];
86 }
87
88 $changed = false;
89
90 if (isset($models_data['providers']['google']) && is_array($models_data['providers']['google'])) {
91 $current_google_default = (string) ($models_data['providers']['google']['default'] ?? '');
92 if ($current_google_default === '' || botwriter_is_deprecated_text_model_choice('google', $current_google_default)) {
93 $models_data['providers']['google']['default'] = 'gemini-3.5-flash';
94 $changed = true;
95 }
96 }
97
98 foreach ($models_data['providers'] as $provider => $provider_data) {
99 if (!is_array($provider_data)) {
100 continue;
101 }
102
103 if (!empty($provider_data['groups']) && is_array($provider_data['groups'])) {
104 foreach ($provider_data['groups'] as $group_name => $group_models) {
105 if (!is_array($group_models)) {
106 continue;
107 }
108
109 $filtered_group_models = botwriter_filter_deprecated_text_models($provider, $group_models);
110 if ($filtered_group_models !== $group_models) {
111 $changed = true;
112 }
113
114 if (empty($filtered_group_models)) {
115 unset($models_data['providers'][$provider]['groups'][$group_name]);
116 } else {
117 $models_data['providers'][$provider]['groups'][$group_name] = $filtered_group_models;
118 }
119 }
120 }
121
122 if (!empty($provider_data['all_models']) && is_array($provider_data['all_models'])) {
123 $filtered_all_models = [];
124 foreach ($provider_data['all_models'] as $model_row) {
125 $model_id = is_array($model_row) ? (string) ($model_row['id'] ?? '') : '';
126 if ($model_id !== '' && botwriter_is_deprecated_text_model_choice($provider, $model_id)) {
127 $changed = true;
128 continue;
129 }
130 $filtered_all_models[] = $model_row;
131 }
132
133 if ($filtered_all_models !== $provider_data['all_models']) {
134 $models_data['providers'][$provider]['all_models'] = $filtered_all_models;
135 $changed = true;
136 }
137 }
138 }
139
140 if ($changed) {
141 $models_data['updated'] = function_exists('current_time')
142 ? current_time('Y-m-d')
143 : gmdate('Y-m-d');
144 }
145
146 return [$models_data, $changed];
147 }
148
149 /**
150 * Get the models data, initializing from JSON if needed
151 *
152 * @return array The models data structure
153 */
154 function botwriter_get_models_data() {
155 $models_data = get_option(BOTWRITER_MODELS_OPTION);
156
157 // If no data exists, load from default JSON file
158 if (empty($models_data)) {
159 $models_data = botwriter_load_default_models();
160 if ($models_data) {
161 update_option(BOTWRITER_MODELS_OPTION, $models_data);
162 }
163 }
164
165 if (!empty($models_data)) {
166 list($sanitized_models_data, $changed) = botwriter_sanitize_models_data($models_data);
167 if ($changed) {
168 $models_data = $sanitized_models_data;
169 update_option(BOTWRITER_MODELS_OPTION, $models_data);
170 }
171 }
172
173 return $models_data;
174 }
175
176 /**
177 * Load models from the default JSON file
178 *
179 * @return array|false The models data or false on failure
180 */
181 function botwriter_load_default_models() {
182 $json_file = plugin_dir_path(dirname(__FILE__)) . 'models-default.json';
183
184 if (!file_exists($json_file)) {
185 return false;
186 }
187
188 $json_content = file_get_contents($json_file);
189 if ($json_content === false) {
190 return false;
191 }
192
193 $models_data = json_decode($json_content, true);
194 if (json_last_error() !== JSON_ERROR_NONE) {
195 return false;
196 }
197
198 return $models_data;
199 }
200
201 /**
202 * Get models for a specific provider
203 *
204 * @param string $provider The provider name (openai, anthropic, google, etc.)
205 * @return array The models grouped structure for select options
206 */
207 function botwriter_get_provider_models($provider) {
208 $models_data = botwriter_get_models_data();
209 $provider = sanitize_key((string) $provider);
210
211 if (empty($models_data['providers'][$provider])) {
212 return [];
213 }
214
215 $provider_data = $models_data['providers'][$provider];
216 $result = [];
217
218 // Add grouped models (our curated list)
219 if (!empty($provider_data['groups'])) {
220 foreach ($provider_data['groups'] as $group_name => $models) {
221 $filtered_models = botwriter_filter_deprecated_text_models($provider, $models);
222 if (!empty($filtered_models)) {
223 $result[$group_name] = $filtered_models;
224 }
225 }
226 }
227
228 // Add all_models if present (from API discovery)
229 if (!empty($provider_data['all_models'])) {
230 $result['*** ALL MODELS ***'] = [];
231 foreach ($provider_data['all_models'] as $model) {
232 if (!is_array($model)) {
233 continue;
234 }
235 $model_id = isset($model['id']) ? (string) $model['id'] : '';
236 if ($model_id === '') {
237 continue;
238 }
239 if (botwriter_is_deprecated_text_model_choice($provider, $model_id)) {
240 continue;
241 }
242 $model_name = isset($model['name']) && $model['name'] !== $model_id
243 ? $model['name'] . ' (' . $model_id . ')'
244 : $model_id;
245 $result['*** ALL MODELS ***'][$model_id] = $model_name;
246 }
247
248 if (empty($result['*** ALL MODELS ***'])) {
249 unset($result['*** ALL MODELS ***']);
250 }
251 }
252
253 return $result;
254 }
255
256 /**
257 * Get the default model for a provider
258 *
259 * @param string $provider The provider name
260 * @return string The default model ID
261 */
262 function botwriter_get_provider_default_model($provider) {
263 $models_data = botwriter_get_models_data();
264
265 if (empty($models_data['providers'][$provider]['default'])) {
266 return '';
267 }
268
269 return $models_data['providers'][$provider]['default'];
270 }
271
272 /**
273 * Update the all_models list for a provider
274 *
275 * @param string $provider The provider name
276 * @param array $models Array of models with 'id' and 'name' keys
277 * @return bool True on success
278 */
279 function botwriter_update_provider_all_models($provider, $models) {
280 $models_data = botwriter_get_models_data();
281 $provider = sanitize_key((string) $provider);
282
283 if (empty($models_data['providers'][$provider])) {
284 return false;
285 }
286
287 // Update the all_models list, filtering deprecated selectable aliases.
288 $filtered_models = [];
289 foreach ((array) $models as $model_row) {
290 $model_id = is_array($model_row) ? (string) ($model_row['id'] ?? '') : '';
291 if ($model_id !== '' && botwriter_is_deprecated_text_model_choice($provider, $model_id)) {
292 continue;
293 }
294 $filtered_models[] = $model_row;
295 }
296
297 $models_data['providers'][$provider]['all_models'] = $filtered_models;
298 $models_data['updated'] = current_time('Y-m-d');
299
300 return update_option(BOTWRITER_MODELS_OPTION, $models_data);
301 }
302
303 /**
304 * Reset models to default from JSON file
305 *
306 * @return bool True on success
307 */
308 function botwriter_reset_models_to_default() {
309 delete_option(BOTWRITER_MODELS_OPTION);
310
311 $text_ok = botwriter_get_models_data() !== false;
312 $image_ok = true;
313
314 // Keep the reset button behavior consistent with its wording:
315 // reset all model lists (text + image).
316 if (function_exists('botwriter_reset_image_models_to_default')) {
317 $image_ok = botwriter_reset_image_models_to_default();
318 } else {
319 delete_option('botwriter_image_models_data');
320 }
321
322 return $text_ok && $image_ok;
323 }
324
325 /**
326 * Render a model select dropdown for a provider
327 *
328 * @param string $provider The provider name
329 * @param string $field_name The form field name
330 * @param string $current_value The currently selected value
331 * @param array $extra_attrs Extra HTML attributes
332 */
333 function botwriter_render_model_select($provider, $field_name, $current_value, $extra_attrs = []) {
334 $models = botwriter_get_provider_models($provider);
335 $default = botwriter_get_provider_default_model($provider);
336
337 if (empty($current_value)) {
338 $current_value = $default;
339 }
340
341 $attrs_str = '';
342 foreach ($extra_attrs as $key => $value) {
343 $attrs_str .= ' ' . esc_attr($key) . '="' . esc_attr($value) . '"';
344 }
345
346 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $attrs_str is assembled only from esc_attr()-escaped keys and values.
347 echo '<select name="' . esc_attr($field_name) . '" class="form-select model-select" data-provider="' . esc_attr($provider) . '"' . $attrs_str . ' style="max-width: 400px;">';
348
349 foreach ($models as $group_name => $group_models) {
350 echo '<optgroup label="' . esc_attr($group_name) . '">';
351 foreach ($group_models as $model_id => $model_name) {
352 echo '<option value="' . esc_attr($model_id) . '"';
353 selected($current_value, $model_id);
354 echo '>' . esc_html($model_name) . '</option>';
355 }
356 echo '</optgroup>';
357 }
358
359 echo '</select>';
360 }
361