PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.23
MxChat – AI Chatbot & Content Generation for WordPress v3.2.23
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-content-generator.php +91 -17 3.2.14 → 3.2.23 View file →
@@ -2242,11 +2242,25 @@
2242 2242
2243 2243 /**
2244 2244 * Call the configured content model
2245 2245 */
2246 + /**
2247 + * Provider slugs (MxChat_Model_Catalog keys) that call_content_model() can
2248 + * dispatch to. MUST mirror the if/elseif chain below — the Content Model
2249 + * picker derives its provider set from this list (plan ccb751), so a
2250 + * provider added to the dispatch without updating this is never offered,
2251 + * and one added here without dispatch support would break generation.
2252 + * OpenRouter and the custom OpenAI-compatible provider are absent on
2253 + * purpose: their pseudo-model ids would fall through to the OpenAI default
2254 + * branch with the wrong key and endpoint.
2255 + */
2256 + public static function supported_content_providers() {
2257 + return array('openai', 'claude', 'gemini', 'xai', 'deepseek');
2258 + }
2259 +
2246 2260 private function call_content_model($system_prompt, $messages, $max_tokens = 4096) {
2247 2261 $options = get_option('mxchat_options', array());
2248 - $model = $options['content_model'] ?? $options['model'] ?? 'gpt-5.1-chat-latest';
2262 + $model = $options['content_model'] ?? $options['model'] ?? 'gpt-5.6-sol';
2249 2263
2250 2264 // Determine provider from model name
2251 2265 if ($this->is_claude_model($model)) {
2252 2266 return $this->call_claude($model, $options['claude_api_key'] ?? '', $system_prompt, $messages, $max_tokens);
@@ -2270,13 +2284,14 @@
2270 2284 * used only when the core model catalog is unavailable. Byte-identical to the
2271 2285 * old inline if/elseif at the call site.
2272 2286 */
2273 2287 private function mxchat_cg_reasoning_effort_fallback($model) {
2274 - if (strpos($model, 'gpt-5') !== 0) return null;
2288 + if (strpos($model, 'gpt-5') !== 0 && strpos($model, 'gpt-6') !== 0) return null; // gpt-6 too (plan 45c22d)
2275 2289 if ($model === 'gpt-5.2' || $model === 'gpt-5.1-chat-latest') return null;
2276 2290 if ($model === 'gpt-5.1-2025-11-13') return 'low';
2277 2291 if ($model === 'gpt-5.5') return 'low';
2278 2292 if ($model === 'gpt-5.4') return 'low';
2293 + if (in_array($model, array('gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna'), true)) return 'low'; // plan 45c22d
2279 2294 if (in_array($model, array('gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna'), true)) return 'low';
2280 2295 return 'minimal';
2281 2296 }
2282 2297
@@ -2297,9 +2312,9 @@
2297 2312 // GPT-5.x models require max_completion_tokens and only support temperature=1.
2298 2313 // Token key, temperature gate, and reasoning_effort are sourced from the
2299 2314 // core model catalog (plan-dcb71c); frozen fallbacks preserve exact
2300 2315 // pre-dcb71c behavior if the catalog class is unavailable.
2301 - $is_gpt5 = strpos($model, 'gpt-5') === 0;
2316 + $is_gpt5 = strpos($model, 'gpt-5') === 0 || strpos($model, 'gpt-6') === 0; // gpt-6 too (plan 45c22d)
2302 2317 $catalog = class_exists('MxChat_Model_Catalog');
2303 2318 $token_key = ($catalog && method_exists('MxChat_Model_Catalog', 'openai_token_param'))
2304 2319 ? MxChat_Model_Catalog::openai_token_param($model)
2305 2320 : ($is_gpt5 ? 'max_completion_tokens' : 'max_tokens');
@@ -2324,8 +2339,16 @@
2324 2339 if ($effort !== null) {
2325 2340 $body['reasoning_effort'] = $effort;
2326 2341 }
2327 2342
2343 + // DeepSeek V4 defaults to thinking mode ON (temperature ignored and
2344 + // reasoning eats the token budget); generation wants the legacy
2345 + // deepseek-chat semantics = non-thinking. Endpoint-gated because this
2346 + // helper is shared with OpenAI and xAI.
2347 + if (strpos($endpoint, 'api.deepseek.com') !== false) {
2348 + $body['thinking'] = array('type' => 'disabled');
2349 + }
2350 +
2328 2351 // Scale timeout with token count — large generation calls need more time
2329 2352 $timeout = ($max_tokens > 8000) ? 300 : 120;
2330 2353
2331 2354 $response = wp_remote_post($endpoint, array(
@@ -2343,8 +2366,38 @@
2343 2366
2344 2367 $status_code = wp_remote_retrieve_response_code($response);
2345 2368 $decoded = json_decode(wp_remote_retrieve_body($response), true);
2346 2369
2370 + // plan-25b972 self-heal: supported reasoning_effort VALUES are per-model;
2371 + // a 400 rejecting the value we sent (stale catalog entry / provider
2372 + // drift) is deterministic — strip the param and retry ONCE.
2373 + if ($status_code === 400
2374 + && isset($body['reasoning_effort'])
2375 + && isset($decoded['error']['message'])
2376 + && is_string($decoded['error']['message'])
2377 + && preg_match('/Unsupported value:.*reasoning_effort/i', $decoded['error']['message'])) {
2378 + if (defined('WP_DEBUG') && WP_DEBUG) {
2379 + error_log(sprintf(
2380 + '[MxChat] content-generator: model %s rejected reasoning_effort \'%s\' — retrying once without the param (plan-25b972).',
2381 + $model, $body['reasoning_effort']
2382 + ));
2383 + }
2384 + unset($body['reasoning_effort']);
2385 + $response = wp_remote_post($endpoint, array(
2386 + 'headers' => array(
2387 + 'Authorization' => 'Bearer ' . $api_key,
2388 + 'Content-Type' => 'application/json',
2389 + ),
2390 + 'body' => wp_json_encode($body),
2391 + 'timeout' => $timeout,
2392 + ));
2393 + if (is_wp_error($response)) {
2394 + return $response;
2395 + }
2396 + $status_code = wp_remote_retrieve_response_code($response);
2397 + $decoded = json_decode(wp_remote_retrieve_body($response), true);
2398 + }
2399 +
2347 2400 if ($status_code !== 200) {
2348 2401 $error_msg = $decoded['error']['message'] ?? __('API request failed with status ', 'mxchat') . $status_code;
2349 2402 return new WP_Error('api_error', $error_msg);
2350 2403 }
@@ -2384,9 +2437,14 @@
2384 2437 'model' => $model,
2385 2438 'max_tokens' => $max_tokens,
2386 2439 'temperature' => 0.7,
2387 2440 'messages' => $formatted,
2388 - 'system' => $system_prompt,
2441 + // Prompt-cache breakpoint (plan 1ff43b): generation pipelines reuse
2442 + // the same system prompt across consecutive requests within the
2443 + // 5-minute cache window. Silently no-ops below the model minimum.
2444 + 'system' => trim((string) $system_prompt) === '' ? $system_prompt : array(
2445 + array('type' => 'text', 'text' => $system_prompt, 'cache_control' => array('type' => 'ephemeral')),
2446 + ),
2389 2447 );
2390 2448
2391 2449 // Anthropic removed temperature on Opus 4.7+ flagships (400 if sent) —
2392 2450 // sourced from the core model catalog (plan-dcb71c); frozen list fallback.
@@ -2391,9 +2449,9 @@
2391 2449 // Anthropic removed temperature on Opus 4.7+ flagships (400 if sent) —
2392 2450 // sourced from the core model catalog (plan-dcb71c); frozen list fallback.
2393 2451 $omit_temp = (class_exists('MxChat_Model_Catalog') && method_exists('MxChat_Model_Catalog', 'supports_temperature'))
2394 2452 ? !MxChat_Model_Catalog::supports_temperature($model)
2395 - : in_array($model, array('claude-opus-4-7', 'claude-opus-4-8', 'claude-fable-5', 'claude-sonnet-5'), true);
2453 + : in_array($model, array('claude-opus-5', 'claude-opus-4-7', 'claude-opus-4-8', 'claude-fable-5', 'claude-sonnet-5'), true);
2396 2454 if ($omit_temp) {
2397 2455 unset($body['temperature']);
2398 2456 }
2399 2457
@@ -3302,23 +3360,39 @@
3302 3360 array('key' => '_mxchat_seo_score', 'compare' => 'NOT EXISTS'),
3303 3361 );
3304 3362 }
3305 3363
3306 - // When sorting by a meta field, ensure meta_key is set for ordering.
3307 - // For 'all' filter, include posts without the meta key via OR clause.
3364 + // When sorting by a meta field, posts WITHOUT the key must stay in the
3365 + // list — GSC meta is no longer zero-filled for no-data posts (plan
3366 + // 282c80), and score meta never existed for unanalyzed posts. A single
3367 + // OR EXISTS/NOT-EXISTS meta_query cannot order this reliably (under an
3368 + // OR relation WP_Meta_Query leaves the orderby clause's join alias
3369 + // unconstrained by meta_key, so ORDER BY reads arbitrary meta rows).
3370 + // Instead: two id-only queries — posts WITH the key sorted by its
3371 + // value, then posts WITHOUT it appended (newest first). Missing-data
3372 + // rows deliberately sort last in BOTH directions.
3308 3373 if ($sort_meta_key) {
3309 - $args['meta_key'] = $sort_meta_key;
3310 - if ($filter === 'all') {
3311 - $args['meta_query'] = array(
3312 - 'relation' => 'OR',
3313 - array('key' => $sort_meta_key, 'compare' => 'EXISTS'),
3314 - array('key' => $sort_meta_key, 'compare' => 'NOT EXISTS'),
3315 - );
3374 + $with_args = $args;
3375 + $with_args['meta_key'] = $sort_meta_key;
3376 + $with_args['orderby'] = 'meta_value_num';
3377 + $q_with = new \WP_Query($with_args);
3378 +
3379 + $without_args = $args;
3380 + $missing_clause = array('key' => $sort_meta_key, 'compare' => 'NOT EXISTS');
3381 + if (!empty($without_args['meta_query'])) {
3382 + $without_args['meta_query'] = array('relation' => 'AND', $without_args['meta_query'], $missing_clause);
3383 + } else {
3384 + $without_args['meta_query'] = array($missing_clause);
3316 3385 }
3386 + $without_args['orderby'] = 'date';
3387 + $without_args['order'] = 'DESC';
3388 + $q_without = new \WP_Query($without_args);
3389 +
3390 + $all_ids = array_merge($q_with->posts, $q_without->posts);
3391 + } else {
3392 + $query = new \WP_Query($args);
3393 + $all_ids = $query->posts;
3317 3394 }
3318 -
3319 - $query = new \WP_Query($args);
3320 - $all_ids = $query->posts;
3321 3395 $total = count($all_ids);
3322 3396 $pages = max(1, ceil($total / $per_page));
3323 3397 $page = min($page, $pages);
3324 3398 $offset = ($page - 1) * $per_page;