| @@ -2242,8 +2242,22 @@ | ||
| 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 | 2262 | $model = $options['content_model'] ?? $options['model'] ?? 'gpt-5.6-sol'; |
| 2249 | 2263 | |
| @@ -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'); |
| @@ -2351,8 +2366,38 @@ | ||
| 2351 | 2366 | |
| 2352 | 2367 | $status_code = wp_remote_retrieve_response_code($response); |
| 2353 | 2368 | $decoded = json_decode(wp_remote_retrieve_body($response), true); |
| 2354 | 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 | + | |
| 2355 | 2400 | if ($status_code !== 200) { |
| 2356 | 2401 | $error_msg = $decoded['error']['message'] ?? __('API request failed with status ', 'mxchat') . $status_code; |
| 2357 | 2402 | return new WP_Error('api_error', $error_msg); |
| 2358 | 2403 | } |
| @@ -2368,11 +2413,10 @@ | ||
| 2368 | 2413 | * Call Claude (Anthropic) API |
| 2369 | 2414 | */ |
| 2370 | 2415 | private function call_claude($model, $api_key, $system_prompt, $messages, $max_tokens) { |
| 2371 | 2416 | // Anthropic retired claude-opus-4-20250514 / claude-sonnet-4-20250514 on 2026-06-15. |
| 2372 | - // Read-time rescue: remap a saved dead ID to the current equivalent before the API call. | |
| 2373 | - if ($model === 'claude-opus-4-20250514') { $model = 'claude-opus-4-8'; } | |
| 2374 | - elseif ($model === 'claude-sonnet-4-20250514') { $model = 'claude-sonnet-4-6'; } | |
| 2417 | + // Read-time rescue: a saved retired id is remapped to the catalog's replacement before the API call. | |
| 2418 | + $model = MxChat_Model_Catalog::resolve_retired($model); // plan 8c4eb4: the catalog's one map | |
| 2375 | 2419 | if (empty($api_key)) { |
| 2376 | 2420 | return new WP_Error('no_api_key', __('Claude API key not configured.', 'mxchat')); |
| 2377 | 2421 | } |
| 2378 | 2422 | |
| @@ -2392,9 +2436,14 @@ | ||
| 2392 | 2436 | 'model' => $model, |
| 2393 | 2437 | 'max_tokens' => $max_tokens, |
| 2394 | 2438 | 'temperature' => 0.7, |
| 2395 | 2439 | 'messages' => $formatted, |
| 2396 | - 'system' => $system_prompt, | |
| 2440 | + // Prompt-cache breakpoint (plan 1ff43b): generation pipelines reuse | |
| 2441 | + // the same system prompt across consecutive requests within the | |
| 2442 | + // 5-minute cache window. Silently no-ops below the model minimum. | |
| 2443 | + 'system' => trim((string) $system_prompt) === '' ? $system_prompt : array( | |
| 2444 | + array('type' => 'text', 'text' => $system_prompt, 'cache_control' => array('type' => 'ephemeral')), | |
| 2445 | + ), | |
| 2397 | 2446 | ); |
| 2398 | 2447 | |
| 2399 | 2448 | // Anthropic removed temperature on Opus 4.7+ flagships (400 if sent) — |
| 2400 | 2449 | // sourced from the core model catalog (plan-dcb71c); frozen list fallback. |
| @@ -2506,12 +2555,29 @@ | ||
| 2506 | 2555 | $error_msg = $decoded['error']['message'] ?? __('Gemini API error ', 'mxchat') . $status_code; |
| 2507 | 2556 | return new WP_Error('gemini_error', $error_msg); |
| 2508 | 2557 | } |
| 2509 | 2558 | |
| 2510 | - if (isset($decoded['candidates'][0]['content']['parts'][0]['text'])) { | |
| 2511 | - return trim($decoded['candidates'][0]['content']['parts'][0]['text']); | |
| 2559 | + // plan 781afd: join every text part (skip thought parts), not only parts[0]. | |
| 2560 | + $text = ''; | |
| 2561 | + if (isset($decoded['candidates'][0]['content']['parts']) && is_array($decoded['candidates'][0]['content']['parts'])) { | |
| 2562 | + foreach ($decoded['candidates'][0]['content']['parts'] as $part) { | |
| 2563 | + if (is_array($part) && isset($part['text']) && is_string($part['text']) && empty($part['thought'])) { | |
| 2564 | + $text .= $part['text']; | |
| 2565 | + } | |
| 2566 | + } | |
| 2512 | 2567 | } |
| 2568 | + $text = trim($text); | |
| 2569 | + if ($text !== '') { | |
| 2570 | + return $text; | |
| 2571 | + } | |
| 2513 | 2572 | |
| 2573 | + $no_text_reason = isset($decoded['candidates'][0]['finishReason']) && is_string($decoded['candidates'][0]['finishReason']) | |
| 2574 | + ? $decoded['candidates'][0]['finishReason'] | |
| 2575 | + : (isset($decoded['promptFeedback']['blockReason']) && is_string($decoded['promptFeedback']['blockReason']) ? $decoded['promptFeedback']['blockReason'] : ''); | |
| 2576 | + if ($no_text_reason !== '') { | |
| 2577 | + /* translators: %s: Gemini's finish or block reason, e.g. SAFETY */ | |
| 2578 | + return new WP_Error('gemini_no_text', sprintf(__('Gemini returned no text (%s).', 'mxchat'), $no_text_reason)); | |
| 2579 | + } | |
| 2514 | 2580 | return new WP_Error('unexpected_response', __('Unexpected Gemini response format.', 'mxchat')); |
| 2515 | 2581 | } |
| 2516 | 2582 | |
| 2517 | 2583 | // ─── Model Detection Helpers ──────────────────────────────────────── |