PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.24
MxChat – AI Chatbot & Content Generation for WordPress v3.2.24
3.2.25 3.2.24 3.2.23 3.2.22 3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 All 156 releases
← All changes | includes/class-mxchat-model-catalog.php +363 -33 3.2.18 → 3.2.24 View file →
@@ -55,11 +55,15 @@
55 55 'label' => __('Google Gemini', 'mxchat'),
56 56 'key_option' => 'gemini_api_key',
57 57 'requires_key_to_load_models' => false,
58 58 'models' => array(
59 - 'gemini-3.5-flash' => array('label' => 'Gemini 3.5 Flash', 'description' => __('Stable — newest Flash generation, recommended default', 'mxchat')),
59 + 'gemini-3.8-flash' => array('label' => 'Gemini 3.8 Flash', 'description' => __('Stable — newest Flash generation, recommended default', 'mxchat')),
60 + 'gemini-3.7-flash' => array('label' => 'Gemini 3.7 Flash', 'description' => __('Stable — efficiency-first Flash', 'mxchat')),
61 + 'gemini-3.6-flash' => array('label' => 'Gemini 3.6 Flash', 'description' => __('Stable — previous Flash generation', 'mxchat')),
62 + 'gemini-3.5-flash' => array('label' => 'Gemini 3.5 Flash', 'description' => __('Stable — older Flash, priced above 3.8 Flash on every token', 'mxchat')),
60 63 'gemini-3.1-pro-preview' => array('label' => 'Gemini 3.1 Pro', 'description' => __('Preview — most intelligent, multimodal & agentic', 'mxchat')),
61 64 'gemini-3-flash-preview' => array('label' => 'Gemini 3 Flash', 'description' => __('Preview — balanced speed and scale', 'mxchat')),
65 + 'gemini-3.5-flash-lite' => array('label' => 'Gemini 3.5 Flash-Lite', 'description' => __('Stable — cheapest Gemini, no thinking', 'mxchat')),
62 66 'gemini-3.1-flash-lite' => array('label' => 'Gemini 3.1 Flash-Lite', 'description' => __('Stable — cost-efficient, high throughput', 'mxchat')),
63 67 'gemini-3.1-flash-lite-preview' => array('label' => 'Gemini 3.1 Flash-Lite (Preview)', 'description' => __('Preview — latest cost-efficient model', 'mxchat')),
64 68 'gemini-2.5-pro' => array('label' => 'Gemini 2.5 Pro', 'description' => __('Stable — advanced thinking, code, math & long context', 'mxchat')),
65 69 'gemini-2.5-flash' => array('label' => 'Gemini 2.5 Flash', 'description' => __('Stable — strong price-performance with thinking', 'mxchat')),
@@ -79,9 +83,18 @@
79 83 // 'deprecated_on' (Y-m-d) + 'replacement' are optional per-model
80 84 // fields: pickers label a future date as "retires <date>" and hide
81 85 // the entry once the date passes (a saved value keeps rendering via
82 86 // settings_dropdown_groups($current_model)).
83 - 'gpt-5.6-sol' => array('label' => 'GPT-5.6 Sol', 'description' => __('Recommended — newest OpenAI flagship for reasoning, coding and chat', 'mxchat')),
87 + // GPT-6 (announced 2026-09-22; plan 45c22d). Probed live on
88 + // 2026-09-27: all three need max_completion_tokens, reject any
89 + // temperature but 1 and reject reasoning_effort 'minimal';
90 + // Astra also rejects 'none'. Every "modern OpenAI" gate goes
91 + // through is_modern_openai() so these ids are not sent legacy
92 + // parameters (which 400 on the first message).
93 + 'gpt-6-astra' => array('label' => 'GPT-6 Astra', 'description' => __('Recommended — newest OpenAI flagship; strongest reasoning, coding and agentic work', 'mxchat')),
94 + 'gpt-6-sol' => array('label' => 'GPT-6 Sol', 'description' => __('Strong reasoning on demanding tasks at mid-tier cost', 'mxchat')),
95 + 'gpt-6-luna' => array('label' => 'GPT-6 Luna', 'description' => __('Fastest and cheapest GPT-6 for high-volume, repeatable work', 'mxchat')),
96 + 'gpt-5.6-sol' => array('label' => 'GPT-5.6 Sol', 'description' => __('Previous OpenAI flagship for reasoning, coding and chat', 'mxchat')),
84 97 'gpt-5.6-terra' => array('label' => 'GPT-5.6 Terra', 'description' => __('Balanced GPT-5.6 — strong capability at lower cost', 'mxchat')),
85 98 'gpt-5.6-luna' => array('label' => 'GPT-5.6 Luna', 'description' => __('Fastest and cheapest GPT-5.6 for lightweight tasks', 'mxchat')),
86 99 'gpt-5.5' => array('label' => 'GPT-5.5', 'description' => __('Previous flagship — powerful reasoning and coding model', 'mxchat')),
87 100 'gpt-5.4' => array('label' => 'GPT-5.4', 'description' => __('Flagship reasoning and coding model', 'mxchat')),
@@ -107,9 +120,8 @@
107 120 'claude-sonnet-5' => array('label' => 'Claude Sonnet 5', 'description' => __('Newest Sonnet - excellent balance of speed and capability', 'mxchat')),
108 121 'claude-sonnet-4-6' => array('label' => 'Claude Sonnet 4.6', 'description' => __('Previous Sonnet - excellent balance of speed and capability', 'mxchat')),
109 122 'claude-opus-4-5' => array('label' => 'Claude Opus 4.5', 'description' => __('Highly capable for complex tasks', 'mxchat')),
110 123 'claude-sonnet-4-5-20250929' => array('label' => 'Claude Sonnet 4.5', 'description' => __('Best for complex agents and coding', 'mxchat')),
111 - 'claude-opus-4-1-20250805' => array('label' => 'Claude Opus 4.1', 'description' => __('Exceptional for specialized complex tasks', 'mxchat')),
112 124 'claude-haiku-4-5-20251001' => array('label' => 'Claude Haiku 4.5', 'description' => __('Fastest and most intelligent Haiku', 'mxchat')),
113 125 ),
114 126 ),
115 127 'xai' => array(
@@ -115,16 +127,35 @@
115 127 'xai' => array(
116 128 'label' => __('xAI Grok', 'mxchat'),
117 129 'key_option' => 'xai_api_key',
118 130 'requires_key_to_load_models' => false,
131 + // Refreshed 2026-08-14 (plan-mxchat-20260814-e1aa4a) against TWO
132 + // primary sources that agree: a live pull of xAI's /v1/models with
133 + // a real key, and docs.x.ai/docs/models. Context windows below are
134 + // that listing's own context_length; image-input support for every
135 + // current-generation id was confirmed by a 32x32 probe returning a
136 + // correct answer (all HTTP 200), not inferred from docs.
137 + //
138 + // The legacy block below is INTENTIONALLY still here. xAI has
139 + // published no retirement dates for these ids and they still answer
140 + // HTTP 200 (grok-4-0709 re-probed 2026-08-14), so 'deprecated_on'
141 + // would be an invented date and deleting them would strand every
142 + // install saved on one. Current generation simply sorts above them.
119 143 'models' => array(
120 - 'grok-4-1-fast-reasoning' => array('label' => 'Grok 4.1 Fast (Reasoning)', 'description' => __('2M context window and reasoning', 'mxchat')),
121 - 'grok-4-1-fast-non-reasoning' => array('label' => 'Grok 4.1 Fast (Non-Reasoning)', 'description' => __('2M context window and faster responses', 'mxchat')),
122 - 'grok-4-0709' => array('label' => 'Grok 4', 'description' => __('Latest flagship model - unparalleled performance in natural language, math and reasoning', 'mxchat')),
123 - 'grok-3-beta' => array('label' => 'Grok-3', 'description' => __('Powerful model with 131K context', 'mxchat')),
124 - 'grok-3-fast-beta' => array('label' => 'Grok-3 Fast', 'description' => __('High performance with faster responses', 'mxchat')),
125 - 'grok-3-mini-beta' => array('label' => 'Grok-3 Mini', 'description' => __('Affordable model with good performance', 'mxchat')),
126 - 'grok-3-mini-fast-beta' => array('label' => 'Grok-3 Mini Fast', 'description' => __('Quick and cost-effective', 'mxchat')),
144 + 'grok-4.6' => array('label' => 'Grok 4.6', 'description' => __('Newest xAI flagship — 500K context, accepts image input', 'mxchat')),
145 + 'grok-4.5' => array('label' => 'Grok 4.5', 'description' => __('Previous flagship — 500K context, accepts image input', 'mxchat')),
146 + 'grok-4.3' => array('label' => 'Grok 4.3', 'description' => __('Largest context in the Grok lineup — 1M tokens, accepts image input', 'mxchat')),
147 + 'grok-4.20-0309-reasoning' => array('label' => 'Grok 4.20 (Reasoning)', 'description' => __('1M context with reasoning — dated release, pinned behaviour', 'mxchat')),
148 + 'grok-4.20-0309-non-reasoning'=> array('label' => 'Grok 4.20 (Non-Reasoning)', 'description' => __('1M context without reasoning — faster replies at the same price', 'mxchat')),
149 + // --- Previous generations. Still served by xAI, no announced
150 + // retirement date, kept so saved selections keep working. ---
151 + 'grok-4-1-fast-reasoning' => array('label' => 'Grok 4.1 Fast (Reasoning)', 'description' => __('Previous generation — no longer listed by xAI, still served', 'mxchat')),
152 + 'grok-4-1-fast-non-reasoning' => array('label' => 'Grok 4.1 Fast (Non-Reasoning)', 'description' => __('Previous generation — no longer listed by xAI, still served', 'mxchat')),
153 + 'grok-4-0709' => array('label' => 'Grok 4', 'description' => __('Previous generation — no longer listed by xAI, still served', 'mxchat')),
154 + 'grok-3-beta' => array('label' => 'Grok-3', 'description' => __('Older generation — no longer listed by xAI, still served', 'mxchat')),
155 + 'grok-3-fast-beta' => array('label' => 'Grok-3 Fast', 'description' => __('Older generation — no longer listed by xAI, still served', 'mxchat')),
156 + 'grok-3-mini-beta' => array('label' => 'Grok-3 Mini', 'description' => __('Older generation — no longer listed by xAI, still served', 'mxchat')),
157 + 'grok-3-mini-fast-beta' => array('label' => 'Grok-3 Mini Fast', 'description' => __('Older generation — no longer listed by xAI, still served', 'mxchat')),
127 158 ),
128 159 ),
129 160 'deepseek' => array(
130 161 'label' => __('DeepSeek', 'mxchat'),
@@ -184,8 +215,64 @@
184 215 );
185 216 }
186 217
187 218 /**
219 + * Canonical retired-model map: every chat model id a provider has retired
220 + * (or hard-deprecated) that stored settings may still carry, mapped to its
221 + * current replacement (plan-mxchat-20260822-202df5).
222 + *
223 + * THE ONLY PLACE THIS MAPPING LIVES. mxchat_migrate_deprecated_models()
224 + * consumes it for the main chat + content models, and add-ons that store
225 + * their own model ids (mxchat-multi-bot's per-bot overrides) consume it for
226 + * theirs — a second hardcoded list in any consumer is how a stranded-model
227 + * bug recurs, so route every new retirement through this map.
228 + *
229 + * Claude ids map BY TIER so a rescued site lands on the current generation,
230 + * not an older intermediate (plan dc91bd). Never map onto a model that is
231 + * itself on a deprecation list (the e46b8f rule).
232 + *
233 + * Entry shape: retired_id => array(
234 + * 'to' => replacement model id (must be current, never itself retired),
235 + * 'label' => replacement display name for user-facing notices,
236 + * 'reason' => notice clause explaining the retirement; deliberately NOT
237 + * translated — migration messages are stored in an option and
238 + * rendered later, matching the existing migration notices.
239 + * )
240 + */
241 + public static function retired_model_map() {
242 + return array(
243 + // Anthropic Claude — Opus tier.
244 + 'claude-3-opus-20240229' => array('to' => 'claude-opus-5', 'label' => 'Claude Opus 5', 'reason' => 'because Anthropic retired the older Claude model'), // Retired Jan 5, 2026
245 + 'claude-opus-4-20250514' => array('to' => 'claude-opus-5', 'label' => 'Claude Opus 5', 'reason' => 'because Anthropic retired the older Claude model'), // Deprecated
246 + 'claude-opus-4-1-20250805' => array('to' => 'claude-opus-5', 'label' => 'Claude Opus 5', 'reason' => 'because Anthropic retired the older Claude model'), // Retired Aug 5, 2026 (first-party API)
247 + // Anthropic Claude — Sonnet tier.
248 + 'claude-3-5-sonnet-20240620' => array('to' => 'claude-sonnet-5', 'label' => 'Claude Sonnet 5', 'reason' => 'because Anthropic retired the older Claude model'), // Retired Oct 28, 2025
249 + 'claude-3-5-sonnet-20241022' => array('to' => 'claude-sonnet-5', 'label' => 'Claude Sonnet 5', 'reason' => 'because Anthropic retired the older Claude model'), // Retired Oct 28, 2025
250 + 'claude-3-7-sonnet-20250219' => array('to' => 'claude-sonnet-5', 'label' => 'Claude Sonnet 5', 'reason' => 'because Anthropic retired the older Claude model'), // Retired Feb 19, 2026
251 + 'claude-3-sonnet-20240229' => array('to' => 'claude-sonnet-5', 'label' => 'Claude Sonnet 5', 'reason' => 'because Anthropic retired the older Claude model'), // Legacy
252 + 'claude-sonnet-4-20250514' => array('to' => 'claude-sonnet-5', 'label' => 'Claude Sonnet 5', 'reason' => 'because Anthropic retired the older Claude model'), // Deprecated
253 + // Anthropic Claude — Haiku tier.
254 + 'claude-3-haiku-20240307' => array('to' => 'claude-haiku-4-5-20251001', 'label' => 'Claude Haiku 4.5', 'reason' => 'because Anthropic retired the older Claude model'), // Legacy
255 + 'claude-3-5-haiku-20241022' => array('to' => 'claude-haiku-4-5-20251001', 'label' => 'Claude Haiku 4.5', 'reason' => 'because Anthropic retired the older Claude model'), // Deprecated
256 + // OpenAI — GPT-4 series + GPT-3.5 Turbo (deprecated 2026-02-17).
257 + 'gpt-4o' => array('to' => 'gpt-5.6-sol', 'label' => 'GPT-5.6 Sol', 'reason' => 'due to OpenAI deprecating older models'),
258 + 'gpt-4.1-2025-04-14' => array('to' => 'gpt-5.6-sol', 'label' => 'GPT-5.6 Sol', 'reason' => 'due to OpenAI deprecating older models'),
259 + 'gpt-4-turbo' => array('to' => 'gpt-5.6-sol', 'label' => 'GPT-5.6 Sol', 'reason' => 'due to OpenAI deprecating older models'),
260 + 'gpt-4' => array('to' => 'gpt-5.6-sol', 'label' => 'GPT-5.6 Sol', 'reason' => 'due to OpenAI deprecating older models'),
261 + 'gpt-3.5-turbo' => array('to' => 'gpt-5.6-sol', 'label' => 'GPT-5.6 Sol', 'reason' => 'due to OpenAI deprecating older models'),
262 + // OpenAI — the gpt-5.x-chat-latest aliases retired 2026-08-10 (e46b8f).
263 + 'gpt-5.1-chat-latest' => array('to' => 'gpt-5.6-sol', 'label' => 'GPT-5.6 Sol', 'reason' => 'because OpenAI retires the GPT-5.x Chat Latest models on August 10, 2026'),
264 + 'gpt-5.3-chat-latest' => array('to' => 'gpt-5.6-sol', 'label' => 'GPT-5.6 Sol', 'reason' => 'because OpenAI retires the GPT-5.x Chat Latest models on August 10, 2026'),
265 + // OpenAI — mini tier.
266 + 'gpt-4o-mini' => array('to' => 'gpt-5-mini', 'label' => 'GPT-5 Mini', 'reason' => 'due to OpenAI deprecating GPT-4 series models'),
267 + 'gpt-4.1-mini' => array('to' => 'gpt-5-mini', 'label' => 'GPT-5 Mini', 'reason' => 'due to OpenAI deprecating GPT-4 series models'),
268 + // DeepSeek — removed at the vendor 2026-07-24 (hard cutoff, every request 400s).
269 + 'deepseek-chat' => array('to' => 'deepseek-v4-flash', 'label' => 'DeepSeek V4 Flash', 'reason' => 'because DeepSeek retired its older API models on July 24, 2026'),
270 + 'deepseek-reasoner' => array('to' => 'deepseek-v4-flash', 'label' => 'DeepSeek V4 Flash', 'reason' => 'because DeepSeek retired its older API models on July 24, 2026'),
271 + );
272 + }
273 +
274 + /**
188 275 * Returns the display label for the given chat-or-embedding provider slug.
189 276 * Chat providers checked first; embedding-only providers fall through.
190 277 */
191 278 public static function provider_label($slug) {
@@ -209,8 +296,142 @@
209 296 return '';
210 297 }
211 298
212 299 /**
300 + * The provider slug that owns a chat model id, or '' when the catalog
301 + * doesn't list it (plan b65e8d).
302 + *
303 + * NOTE for callers doing retirement work: a model the provider has already
304 + * retired is frequently ABSENT from this catalog while still saved on an
305 + * install — that is precisely the state the liveness check exists to catch.
306 + * Treat '' as "catalog doesn't know", not "not a real model", and fall back
307 + * to a family-prefix guess if you need one.
308 + */
309 + public static function provider_for_chat_model($model_id) {
310 + $model_id = (string) $model_id;
311 + foreach (self::chat_models() as $provider_slug => $provider) {
312 + if (isset($provider['models'][$model_id])) {
313 + return $provider_slug;
314 + }
315 + }
316 + return '';
317 + }
318 +
319 + /**
320 + * URL + auth headers for a provider's public model-listing endpoint
321 + * (plan b65e8d — the daily liveness check).
322 + *
323 + * Only providers that actually publish a listing are mapped. OpenRouter is
324 + * deliberately excluded: its 'openrouter' catalog entry is a meta-selector
325 + * that resolves to a per-account sub-model at request time, so "is the
326 + * configured id listed" is not a meaningful question. DeepSeek and the
327 + * custom OpenAI-compatible provider are excluded too — a self-hosted or
328 + * proxied endpoint's /models is not authoritative about retirement.
329 + *
330 + * @param string $provider Provider slug.
331 + * @param string $api_key The install's key for that provider.
332 + * @return array|null array('url' => string, 'headers' => array) or null.
333 + */
334 + public static function models_listing_request($provider, $api_key) {
335 + $api_key = (string) $api_key;
336 + if ($api_key === '') {
337 + return null;
338 + }
339 +
340 + switch ($provider) {
341 + case 'openai':
342 + return array(
343 + 'url' => 'https://api.openai.com/v1/models',
344 + 'headers' => array('Authorization' => 'Bearer ' . $api_key),
345 + );
346 + case 'claude':
347 + return array(
348 + 'url' => 'https://api.anthropic.com/v1/models?limit=1000',
349 + 'headers' => array(
350 + 'x-api-key' => $api_key,
351 + 'anthropic-version' => '2023-06-01',
352 + ),
353 + );
354 + case 'gemini':
355 + return array(
356 + 'url' => 'https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000',
357 + 'headers' => array('x-goog-api-key' => $api_key),
358 + );
359 + case 'xai':
360 + return array(
361 + 'url' => 'https://api.x.ai/v1/models',
362 + 'headers' => array('Authorization' => 'Bearer ' . $api_key),
363 + );
364 + }
365 +
366 + return null;
367 + }
368 +
369 + /**
370 + * A minimal 1-token chat request used ONLY to confirm whether a model id
371 + * still exists (plan b65e8d). Never used for content — the response is
372 + * thrown away; only the HTTP status and error code are read.
373 + *
374 + * WHY THIS EXISTS: a provider's model LISTING is not authoritative about
375 + * retirement, in either direction (all probed live 2026-08-13):
376 + * - OpenAI KEEPS retired ids listed — gpt-5.3-chat-latest is in
377 + * /v1/models and 404s "model_deprecated" on use.
378 + * - Anthropic DROPS working aliases — claude-opus-4-5 is absent from
379 + * /v1/models and answers HTTP 200 perfectly.
380 + * - xAI's listing is partial — grok-4-0709 is absent and answers 200.
381 + * So "absent from the listing" is a suspicion, not a verdict. This probe is
382 + * the verdict, and it is the only thing allowed to raise a warning.
383 + *
384 + * @return array|null array('url','headers','body') or null when unsupported.
385 + */
386 + public static function inference_probe_request($provider, $api_key, $model) {
387 + $api_key = (string) $api_key;
388 + $model = (string) $model;
389 + if ($api_key === '' || $model === '') {
390 + return null;
391 + }
392 +
393 + $msg = array(array('role' => 'user', 'content' => 'hi'));
394 +
395 + switch ($provider) {
396 + case 'openai':
397 + return array(
398 + 'url' => 'https://api.openai.com/v1/chat/completions',
399 + 'headers' => array('Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json'),
400 + 'body' => array('model' => $model, 'messages' => $msg, 'max_completion_tokens' => 1),
401 + );
402 + case 'xai':
403 + return array(
404 + 'url' => 'https://api.x.ai/v1/chat/completions',
405 + 'headers' => array('Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json'),
406 + 'body' => array('model' => $model, 'messages' => $msg, 'max_tokens' => 1),
407 + );
408 + case 'claude':
409 + return array(
410 + 'url' => 'https://api.anthropic.com/v1/messages',
411 + 'headers' => array(
412 + 'x-api-key' => $api_key,
413 + 'anthropic-version' => '2023-06-01',
414 + 'Content-Type' => 'application/json',
415 + ),
416 + 'body' => array('model' => $model, 'max_tokens' => 1, 'messages' => $msg),
417 + );
418 + case 'gemini':
419 + return array(
420 + 'url' => 'https://generativelanguage.googleapis.com/v1beta/models/'
421 + . rawurlencode($model) . ':generateContent',
422 + 'headers' => array('x-goog-api-key' => $api_key, 'Content-Type' => 'application/json'),
423 + 'body' => array(
424 + 'contents' => array(array('parts' => array(array('text' => 'hi')))),
425 + 'generationConfig' => array('maxOutputTokens' => 1),
426 + ),
427 + );
428 + }
429 +
430 + return null;
431 + }
432 +
433 + /**
213 434 * Flat allowlist of every chat model id across all providers. Used by the
214 435 * sanitize() flow in class-mxchat-admin.php and the AJAX autosave validator
215 436 * in admin/class-ajax-handler.php so a new model id only needs editing in
216 437 * the catalog above.
@@ -271,13 +492,22 @@
271 492 * Retired models (past 'deprecated_on') are hidden — EXCEPT the install's
272 493 * currently saved model, which is re-injected with a "retired" marker so
273 494 * the settings screen never renders a blank select (plan e46b8f).
274 495 *
275 - * @param string $current_model Optionally, the install's saved model id.
496 + * @param string $current_model Optionally, the install's saved model id.
497 + * @param array|null $provider_slugs Optional allowlist of provider slugs
498 + * (catalog keys, e.g. 'openai', 'claude').
499 + * null = every provider. Lets a consumer
500 + * that can only dispatch to some providers
501 + * (the content generator — plan ccb751)
502 + * derive its picker from the same catalog.
276 503 */
277 - public static function settings_dropdown_groups($current_model = '') {
504 + public static function settings_dropdown_groups($current_model = '', $provider_slugs = null) {
278 505 $out = array();
279 506 foreach (self::chat_models() as $provider_slug => $provider) {
507 + if (is_array($provider_slugs) && !in_array($provider_slug, $provider_slugs, true)) {
508 + continue;
509 + }
280 510 $out[$provider['label']] = array();
281 511 foreach ($provider['models'] as $id => $entry) {
282 512 $label = self::picker_label($entry);
283 513 if ($label === null && $id !== $current_model) {
@@ -432,12 +662,15 @@
432 662 * stated replacement for the gpt-5.x-chat-latest aliases retired
433 663 * 2026-08-10 (which this map previously pinned; plan e46b8f).
434 664 * - claude: the current Haiku — image analysis wants fast + cheap; Haiku
435 665 * is vision-capable and the catalog's newest Haiku generation.
436 - * - xai: the catalog's dated id for what the bare 'grok-4' alias resolves
437 - * to today — documented vision-capable. The grok-4-1-fast pair is newer
438 - * but its image-input support is unconfirmed; promote it HERE once
439 - * confirmed and every asking add-on follows.
666 + * - xai: grok-4.6, the current flagship. Promoted 2026-08-14 (plan
667 + * e1aa4a) from grok-4-0709, which was two generations behind and no
668 + * longer appears in xAI's /v1/models listing. Image input is CONFIRMED,
669 + * not assumed: a 32x32 fixture probe on 2026-08-14 returned the correct
670 + * colour on grok-4.6 / 4.5 / 4.3 and both 4.20 variants (all HTTP 200).
671 + * grok-4.3 is the cheaper vision option (roughly half the image-token
672 + * price, 1M context) if cost ever outweighs capability here.
440 673 * - gemini: current stable Flash — vision-capable, no per-image surcharge.
441 674 *
442 675 * NOT covered: image-GENERATION models (Gemini Imagen / Flash Image etc.).
443 676 * Those are a different capability the catalog does not model; add-ons that
@@ -450,10 +683,10 @@
450 683 public static function default_vision_model($provider) {
451 684 $map = array(
452 685 'openai' => 'gpt-5.6-sol',
453 686 'claude' => 'claude-haiku-4-5-20251001',
454 - 'xai' => 'grok-4-0709',
455 - 'gemini' => 'gemini-3.5-flash',
687 + 'xai' => 'grok-4.6',
688 + 'gemini' => 'gemini-3.8-flash',
456 689 );
457 690 $model = isset($map[$provider]) ? $map[$provider] : '';
458 691 return (string) apply_filters('mxchat_default_vision_model', $model, $provider);
459 692 }
@@ -471,17 +704,22 @@
471 704 *
472 705 * The mapping is DELIBERATELY per-context because the surfaces genuinely
473 706 * differ today (they are NOT a single clamped ladder):
474 707 * - 'chat' — /v1/chat/completions main chat. gpt-5.5 and gpt-5.4 use
475 - * 'none'; the *-chat-latest / mini / nano / gpt-5.2 ids
476 - * send NO reasoning_effort.
708 + * 'none'; the *-chat-latest / 5.4-mini / 5.4-nano /
709 + * gpt-5.2 ids send NO reasoning_effort; the original
710 + * gpt-5 / gpt-5-mini / gpt-5-nano send 'minimal' (their
711 + * generation requires it — 25b972 probe). Unknown future
712 + * gpt-5* ids send NOTHING (allowlist fall-through).
477 713 * - 'content' — content generation (content-generator, AC ai-client,
478 714 * AC content-calendar). gpt-5.5 / gpt-5.4 use 'low';
479 - * gpt-5.3-chat-latest / gpt-5.4-mini / gpt-5.4-nano use
480 - * 'minimal'; only gpt-5.2 and gpt-5.1-chat-latest send
481 - * nothing. This is an ALLOWLIST (5621dd safe-default):
482 - * a future gpt-5* id not listed gets NO param rather than
483 - * a guessed value that might 400.
715 + * gpt-5.4-mini / gpt-5.4-nano use 'none' (they never
716 + * supported 'minimal' — 25b972); gpt-5.3-chat-latest and
717 + * the original gpt-5 / -mini / -nano use 'minimal'; only
718 + * gpt-5.2 and gpt-5.1-chat-latest send nothing. This is
719 + * an ALLOWLIST (5621dd safe-default): a future gpt-5* id
720 + * not listed gets NO param rather than a guessed value
721 + * that might 400.
484 722 * - 'websearch' — /v1/responses web-search. Only the gpt-5.6 trio, 5.5,
485 723 * 5.4, and 5.1-2025-11-13 send 'low'; every other gpt-5 id
486 724 * (including gpt-5 / mini / nano / chat-latest) sends
487 725 * nothing.
@@ -493,10 +731,29 @@
493 731 */
494 732 public static function reasoning_effort_for($model, $context = 'chat') {
495 733 $model = (string) $model;
496 734
497 - // Only the gpt-5 family carries reasoning_effort. Everything else omits.
498 - if (strpos($model, 'gpt-5') !== 0) {
735 + // plan a9774b: every Gemini 3.x output price is "including thinking
736 + // tokens" and the model thinks at its default level unless told
737 + // otherwise — measured on this site's own support prompt, 71% of
738 + // billed output on 3.8 Flash was hidden thinking, with indistinguishable
739 + // replies at 'low' (0 hidden tokens). So the chat surface sends 'low':
740 + // as reasoning_effort on the OpenAI-compatible endpoint, and as
741 + // generationConfig.thinkingConfig.thinkingLevel on the native one (see
742 + // gemini_thinking_config()). Flash-Lite does not think and returns 400
743 + // on any reasoning parameter — it must send nothing.
744 + if (strpos($model, 'gemini-3') === 0) {
745 + if (strpos($model, 'flash-lite') !== false) {
746 + return null;
747 + }
748 + return ($context === 'chat') ? 'low' : null;
749 + }
750 +
751 + // Only the modern OpenAI family (gpt-5*, gpt-6*) carries
752 + // reasoning_effort. Everything else omits. plan 45c22d widened this
753 + // from a literal gpt-5 prefix: a gpt-6 id used to fall through all
754 + // three maps below and be sent legacy parameters.
755 + if (!self::is_modern_openai($model)) {
499 756 return null;
500 757 }
501 758
502 759 switch ($context) {
@@ -503,8 +760,11 @@
503 760 case 'websearch':
504 761 // integrator web-search (Responses API). Explicit allowlist, no
505 762 // "else" — anything not listed omits reasoning entirely.
506 763 $map = array(
764 + 'gpt-6-astra' => 'low', // plan 45c22d, probed 2026-09-27 on /v1/responses
765 + 'gpt-6-sol' => 'low',
766 + 'gpt-6-luna' => 'low',
507 767 'gpt-5.6-sol' => 'low',
508 768 'gpt-5.6-terra' => 'low',
509 769 'gpt-5.6-luna' => 'low',
510 770 'gpt-5.5' => 'low',
@@ -515,9 +775,22 @@
515 775
516 776 case 'content':
517 777 // content-generator + AC class-ai-client + AC content-calendar.
518 778 // ALLOWLIST (5621dd): a gpt-5* id absent here sends nothing.
779 + //
780 + // plan-25b972 (probed live 2026-08-13): supported reasoning_effort
781 + // VALUES are per-model. gpt-5.4-mini/nano never supported
782 + // 'minimal' (their floor is 'none' — the old 'minimal' entries
783 + // 400'd on every call); the original gpt-5/-mini/-nano still
784 + // REQUIRE 'minimal' and reject 'none'. Do not "harmonize" these
785 + // — the two generations genuinely differ.
519 786 $map = array(
787 + // plan 45c22d (probed 2026-09-27): gpt-6-astra accepts only
788 + // low|medium|high; gpt-6-sol/-luna accept none|low|medium|high.
789 + // 'low' for all three, as for their 5.6 siblings.
790 + 'gpt-6-astra' => 'low',
791 + 'gpt-6-sol' => 'low',
792 + 'gpt-6-luna' => 'low',
520 793 'gpt-5.6-sol' => 'low',
521 794 'gpt-5.6-terra' => 'low',
522 795 'gpt-5.6-luna' => 'low',
523 796 'gpt-5.5' => 'low',
@@ -522,10 +795,10 @@
522 795 'gpt-5.6-luna' => 'low',
523 796 'gpt-5.5' => 'low',
524 797 'gpt-5.4' => 'low',
525 798 'gpt-5.1-2025-11-13' => 'low',
526 - 'gpt-5.4-mini' => 'minimal',
527 - 'gpt-5.4-nano' => 'minimal',
799 + 'gpt-5.4-mini' => 'none',
800 + 'gpt-5.4-nano' => 'none',
528 801 'gpt-5.3-chat-latest' => 'minimal',
529 802 'gpt-5' => 'minimal',
530 803 'gpt-5-mini' => 'minimal',
531 804 'gpt-5-nano' => 'minimal',
@@ -551,12 +824,26 @@
551 824 }
552 825 if ($model === 'gpt-5.4') {
553 826 return 'none';
554 827 }
828 + // plan 45c22d (probed 2026-09-27): gpt-6-astra accepts only
829 + // low|medium|high; -sol/-luna accept none|low|medium|high.
830 + // 'low' for all three, as for their 5.6 siblings.
831 + if (in_array($model, array('gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna'), true)) {
832 + return 'low';
833 + }
555 834 if (in_array($model, array('gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna'), true)) {
556 835 return 'low';
557 836 }
558 - return 'minimal';
837 + // plan-25b972: the original gpt-5 generation REQUIRES 'minimal'
838 + // (probed 2026-08-13: 'none' is rejected for these three).
839 + if (in_array($model, array('gpt-5', 'gpt-5-mini', 'gpt-5-nano'), true)) {
840 + return 'minimal';
841 + }
842 + // plan-25b972: allowlist fall-through (5621dd philosophy). An
843 + // unknown future gpt-5* id sends NO param — a guessed value is
844 + // exactly how the 5.4-mini/nano 400s shipped.
845 + return null;
559 846 }
560 847 }
561 848
562 849 /**
@@ -575,13 +862,35 @@
575 862 *
576 863 * @param string $model Chat model id.
577 864 * @return bool true if a custom temperature may be sent.
578 865 */
866 + /**
867 + * generationConfig.thinkingConfig for the native Gemini endpoint, or null
868 + * to send none (plan a9774b). Derived from reasoning_effort_for() so the
869 + * two Gemini paths — native generateContent and the OpenAI-compatible
870 + * endpoint — always agree on the thinking level. Overridable via
871 + * mxchat_gemini_thinking_level (return '' to send none).
872 + *
873 + * @param string $model Chat model id.
874 + * @return array|null e.g. array('thinkingLevel' => 'low').
875 + */
876 + public static function gemini_thinking_config($model) {
877 + $level = self::reasoning_effort_for($model, 'chat');
878 + if (strpos((string) $model, 'gemini-') !== 0) {
879 + return null;
880 + }
881 + $level = apply_filters('mxchat_gemini_thinking_level', $level, $model);
882 + if (!is_string($level) || $level === '' || !in_array($level, array('minimal', 'low', 'medium', 'high'), true)) {
883 + return null;
884 + }
885 + return array('thinkingLevel' => $level);
886 + }
887 +
579 888 public static function supports_temperature($model) {
580 889 $model = (string) $model;
581 890
582 - // gpt-5 family accepts only the default temperature (1).
583 - if (strpos($model, 'gpt-5') === 0) {
891 + // gpt-5 / gpt-6 families accept only the default temperature (1).
892 + if (self::is_modern_openai($model)) {
584 893 return false;
585 894 }
586 895
587 896 // Anthropic flagships that reject the temperature param outright.
@@ -610,7 +919,28 @@
610 919 * @param string $model Chat model id.
611 920 * @return string 'max_completion_tokens' | 'max_tokens'
612 921 */
613 922 public static function openai_token_param($model) {
614 - return strpos((string) $model, 'gpt-5') === 0 ? 'max_completion_tokens' : 'max_tokens';
923 + return self::is_modern_openai($model) ? 'max_completion_tokens' : 'max_tokens';
924 + }
925 +
926 + /**
927 + * plan 45c22d: ONE predicate for "a modern OpenAI chat model" — the
928 + * generations that take max_completion_tokens, only the default
929 + * temperature, and reasoning_effort. Every gate that used to test the
930 + * literal prefix gpt-5 routes through here, so the next generation is a
931 + * one-line change instead of nine edits. Before this, gpt-6-* ids fell
932 + * through all nine and were sent legacy parameters: a 400 on the first
933 + * message while looking fine in the picker.
934 + *
935 + * The frozen partial-upgrade fallbacks in the integrator, the content
936 + * generator and the editor assistant carry the same two-prefix test
937 + * inline (they exist for the window where this class is not loaded).
938 + *
939 + * @param string $model Chat model id.
940 + * @return bool
941 + */
942 + public static function is_modern_openai($model) {
943 + $model = (string) $model;
944 + return strpos($model, 'gpt-5') === 0 || strpos($model, 'gpt-6') === 0;
615 945 }
616 946 }