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-integrator.php +149 -13 3.2.22 → 3.2.23 View file →
@@ -4044,9 +4044,9 @@
4044 4044 private function mxchat_openai_token_param_for($model) {
4045 4045 if (class_exists('MxChat_Model_Catalog') && method_exists('MxChat_Model_Catalog', 'openai_token_param')) {
4046 4046 return MxChat_Model_Catalog::openai_token_param($model);
4047 4047 }
4048 - return strpos((string) $model, 'gpt-5') === 0 ? 'max_completion_tokens' : 'max_tokens';
4048 + return (strpos((string) $model, 'gpt-5') === 0 || strpos((string) $model, 'gpt-6') === 0) ? 'max_completion_tokens' : 'max_tokens'; // gpt-6 too (plan 45c22d)
4049 4049 }
4050 4050
4051 4051 /**
4052 4052 * plan-mxchat-20260715-7124f4: whether a NON-default temperature may be sent to
@@ -4060,9 +4060,9 @@
4060 4060 private function mxchat_openai_supports_temperature_for($model) {
4061 4061 if (class_exists('MxChat_Model_Catalog') && method_exists('MxChat_Model_Catalog', 'supports_temperature')) {
4062 4062 return MxChat_Model_Catalog::supports_temperature($model);
4063 4063 }
4064 - return strpos((string) $model, 'gpt-5') !== 0;
4064 + return strpos((string) $model, 'gpt-5') !== 0 && strpos((string) $model, 'gpt-6') !== 0; // gpt-6 too (plan 45c22d)
4065 4065 }
4066 4066
4067 4067 /**
4068 4068 * plan-mxchat-20260714-dcb71c: per-surface reasoning_effort, sourced from the
@@ -4082,9 +4082,9 @@
4082 4082 return $this->mxchat_reasoning_effort_fallback($model, $context);
4083 4083 }
4084 4084
4085 4085 private function mxchat_reasoning_effort_fallback($model, $context) {
4086 - if (strpos($model, 'gpt-5') !== 0) {
4086 + if (strpos($model, 'gpt-5') !== 0 && strpos($model, 'gpt-6') !== 0) { // gpt-6 too (plan 45c22d)
4087 4087 return null;
4088 4088 }
4089 4089 if ($context === 'websearch') {
4090 4090 $no_reasoning_web = array('gpt-5.2', 'gpt-5.3-chat-latest', 'gpt-5.4-mini', 'gpt-5.4-nano');
@@ -4091,8 +4091,9 @@
4091 4091 if (in_array($model, $no_reasoning_web, true)) return null;
4092 4092 if ($model === 'gpt-5.1-2025-11-13') return 'low';
4093 4093 if ($model === 'gpt-5.5') return 'low';
4094 4094 if ($model === 'gpt-5.4') return 'low';
4095 + if (in_array($model, array('gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna'), true)) return 'low'; // plan 45c22d
4095 4096 if (in_array($model, array('gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna'), true)) return 'low';
4096 4097 return null;
4097 4098 }
4098 4099 // 'chat'
@@ -4103,8 +4104,9 @@
4103 4104 if (in_array($model, $no_reasoning_models, true)) return null;
4104 4105 if ($model === 'gpt-5.1-2025-11-13') return 'low';
4105 4106 if ($model === 'gpt-5.5') return 'none';
4106 4107 if ($model === 'gpt-5.4') return 'none';
4108 + if (in_array($model, array('gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna'), true)) return 'low'; // plan 45c22d
4107 4109 if (in_array($model, array('gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna'), true)) return 'low';
4108 4110 return 'minimal';
4109 4111 }
4110 4112
@@ -8534,17 +8536,24 @@
8534 8536 // ----- (b) keyword leg -----
8535 8537 $keyword_hits = array();
8536 8538 $keyword_tokens = array();
8537 8539 $keyword_leg = 'none';
8540 + $keyword_code_match = array('exact' => 0, 'prefix' => 0);
8538 8541 if ($hybrid_enabled) {
8539 8542 $keyword_tokens = MxChat_Pinecone_Documents::extract_code_tokens($user_query);
8540 8543 if (!empty($keyword_tokens)) {
8541 8544 $leg_k = min(20, $top_k);
8545 + // 3e83e4: the filter also matches the normalised code set stored on
8546 + // each document, so "R165321320" reaches the "R1653" records; the
8547 + // codes field comes back with the hit so exact matches rank first.
8542 8548 $kw_filter = MxChat_Pinecone_Documents::merge_filters($user_filter, MxChat_Pinecone_Documents::keyword_filter($keyword_tokens));
8543 - $kw_hits = MxChat_Pinecone_Documents::search_dense($user_embedding, $leg_k, $kw_filter, $docs_cfg);
8549 + $kw_fields = array_merge(MxChat_Pinecone_Documents::RECORD_FIELDS, array(MxChat_Pinecone_Documents::CODES_FIELD));
8550 + $kw_hits = MxChat_Pinecone_Documents::search_dense($user_embedding, $leg_k, $kw_filter, $docs_cfg, $kw_fields);
8544 8551 if (!is_wp_error($kw_hits) && !empty($kw_hits)) {
8545 - $keyword_hits = $kw_hits;
8552 + $ranked = MxChat_Pinecone_Documents::rank_keyword_hits($kw_hits, $keyword_tokens);
8553 + $keyword_hits = $ranked['hits'];
8546 8554 $keyword_leg = 'filter';
8555 + $keyword_code_match = array('exact' => $ranked['exact'], 'prefix' => $ranked['prefix']);
8547 8556 } else {
8548 8557 $bm25 = MxChat_Pinecone_Documents::search_text($user_query, $leg_k, $user_filter, $docs_cfg);
8549 8558 if (!is_wp_error($bm25) && !empty($bm25)) {
8550 8559 // BM25 scores are not cosines: anchor on the dense score when
@@ -8563,8 +8572,9 @@
8563 8572 }
8564 8573 }
8565 8574 $this->last_similarity_analysis['keyword_tokens'] = $keyword_tokens;
8566 8575 $this->last_similarity_analysis['keyword_leg'] = $keyword_leg;
8576 + $this->last_similarity_analysis['keyword_codes'] = $keyword_code_match;
8567 8577
8568 8578 // ----- access + threshold -----
8569 8579 $matches_by_id = array();
8570 8580 foreach ($dense as $hit) {
@@ -8621,8 +8631,44 @@
8621 8631 $ordered[] = $cand;
8622 8632 }
8623 8633 } else {
8624 8634 $fused = $this->mxchat_rrf_fuse(array_slice($vector_candidates, 0, 20), $keyword_rows, 60);
8635 + // 3e83e4: a prefix-only code hit (the visitor typed "R165321320", the
8636 + // record only says "R1653") ranks BELOW every exact code hit. Fusion
8637 + // alone can tie them when the dense leg disagrees by one place, so
8638 + // prefix rows are capped just under the lowest exact row; exact rows
8639 + // and vector-only rows keep the scores fusion gave them.
8640 + $exact_ids = array();
8641 + $prefix_ids = array();
8642 + foreach ($keyword_hits as $kh) {
8643 + if (($kh['code_match'] ?? 'exact') === 'prefix') {
8644 + $prefix_ids[$kh['id']] = true;
8645 + } else {
8646 + $exact_ids[$kh['id']] = true;
8647 + }
8648 + }
8649 + if (!empty($exact_ids) && !empty($prefix_ids)) {
8650 + $floor = null;
8651 + foreach ($fused as $fid => $f) {
8652 + if (isset($exact_ids[$fid])) {
8653 + $floor = ($floor === null) ? $f['rrf'] : min($floor, $f['rrf']);
8654 + }
8655 + }
8656 + if ($floor !== null) {
8657 + $capped = 0;
8658 + foreach ($fused as $fid => $f) {
8659 + if (isset($prefix_ids[$fid]) && $f['rrf'] >= $floor) {
8660 + $capped++;
8661 + $fused[$fid]['rrf'] = $floor - $capped * 1e-7;
8662 + }
8663 + }
8664 + if ($capped > 0) {
8665 + uasort($fused, function ($a, $b) {
8666 + return $b['rrf'] <=> $a['rrf'];
8667 + });
8668 + }
8669 + }
8670 + }
8625 8671 $rank = 0;
8626 8672 foreach ($fused as $f) {
8627 8673 $rank++;
8628 8674 $fused_rank_map[$f['id']] = $rank;
@@ -10395,17 +10441,20 @@
10395 10441 }
10396 10442
10397 10443 /* ---------------- OpenAI-compatible loop (OpenAI/xAI/DeepSeek/OpenRouter/Custom) -------------- */
10398 10444 private function mxchat_fc_loop_openai($prov, $system, $relevant_content, $conversation_history, $tools, $orig_message, $user_id, $session_id) {
10445 + $depth = MxChat_Tool_Registry::max_depth();
10446 + $budget = MxChat_Tool_Registry::max_tool_calls_per_turn();
10447 + $tool_schema = MxChat_Tool_Registry::to_openai_tools($tools);
10399 10448 $messages = array();
10400 - $messages[] = array('role' => 'system', 'content' => $system . ' ' . $relevant_content);
10449 + // plan 150b72: the model is told its per-turn tool budget up front (only
10450 + // when tools are actually offered). A local string — the owner's
10451 + // instructions from get_system_instructions() are never mutated.
10452 + $messages[] = array('role' => 'system', 'content' => $system . ' ' . $relevant_content . $this->mxchat_fc_budget_notice($budget, $tool_schema));
10401 10453 foreach ($this->mxchat_fc_normalize_history($conversation_history) as $m) {
10402 10454 $messages[] = $m;
10403 10455 }
10404 10456
10405 - $depth = MxChat_Tool_Registry::max_depth();
10406 - $budget = MxChat_Tool_Registry::max_tool_calls_per_turn();
10407 - $tool_schema = MxChat_Tool_Registry::to_openai_tools($tools);
10408 10457 $used_tool = false;
10409 10458 $calls_made = 0;
10410 10459 $tool_texts = array();
10411 10460 $out_tokens = 0;
@@ -10432,8 +10481,18 @@
10432 10481 }
10433 10482 if ($offer_tools) {
10434 10483 $body['tools'] = $tool_schema;
10435 10484 $body['tool_choice'] = 'auto';
10485 + if (($budget - $calls_made) === 1) {
10486 + // plan 150b72: one call left for this turn — ask for it as ONE
10487 + // call. Without this the model fans out, one call runs, the
10488 + // rest get the budget-exhausted result and it answers from a
10489 + // fraction of the data. Only on the last-call hop: a wide,
10490 + // legitimate fan-out earlier in the turn stays parallel. An
10491 + // owner's http_request_args filter runs after this body is
10492 + // built, so a value set there still wins.
10493 + $body['parallel_tool_calls'] = false;
10494 + }
10436 10495 }
10437 10496 $r = $this->mxchat_fc_post($prov['url'], $body, $prov['headers'], $prov['tag']);
10438 10497 if ($r['code'] !== 200 || !is_array($r['data'])) {
10439 10498 return $this->mxchat_fc_degrade('openai', $this->mxchat_fc_failure_detail($r), $used_tool, $calls_made, $tool_texts);
@@ -10485,8 +10544,15 @@
10485 10544 'tool_call_id' => $tool_call_id,
10486 10545 'content' => $exec['content'],
10487 10546 );
10488 10547 }
10548 + if ($calls_made < $budget) {
10549 + // plan 150b72: say what is left after this hop's calls — as its own
10550 + // system line, never inside a tool's result (a result the model
10551 + // parses as data stays pure). A spent budget is already covered by
10552 + // mxchat_fc_budget_exhausted_text() on the refused calls.
10553 + $messages[] = array('role' => 'system', 'content' => $this->mxchat_fc_remaining_text($budget - $calls_made));
10554 + }
10489 10555 }
10490 10556 return $this->mxchat_fc_degrade('openai', 'hop depth exhausted', $used_tool, $calls_made, $tool_texts);
10491 10557 }
10492 10558
@@ -10497,8 +10563,12 @@
10497 10563
10498 10564 $depth = MxChat_Tool_Registry::max_depth();
10499 10565 $budget = MxChat_Tool_Registry::max_tool_calls_per_turn();
10500 10566 $tool_schema = MxChat_Tool_Registry::to_anthropic_tools($tools);
10567 + // plan 150b72: budget sentence INSIDE the single cached system block, so the
10568 + // cache_control breakpoint mxchat_anthropic_system_blocks() sets still
10569 + // covers tools + system as one prefix (the sentence is constant per turn).
10570 + $system .= $this->mxchat_fc_budget_notice($budget, $tool_schema);
10501 10571 $omit_temp = $this->mxchat_claude_omits_temperature($prov['model']);
10502 10572 $used_tool = false;
10503 10573 $calls_made = 0;
10504 10574 $tool_texts = array();
@@ -10515,8 +10585,12 @@
10515 10585 if ($omit_temp) unset($body['temperature']);
10516 10586 if ($offer_tools) {
10517 10587 $body['tools'] = $tool_schema;
10518 10588 $body['tool_choice'] = array('type' => 'auto');
10589 + if (($budget - $calls_made) === 1) {
10590 + // plan 150b72: see the OpenAI loop — one call left, ask for one call.
10591 + $body['tool_choice']['disable_parallel_tool_use'] = true;
10592 + }
10519 10593 }
10520 10594 $r = $this->mxchat_fc_post($prov['url'], $body, $prov['headers'], $prov['tag']);
10521 10595 if ($r['code'] !== 200 || !is_array($r['data'])) {
10522 10596 return $this->mxchat_fc_degrade('anthropic', $this->mxchat_fc_failure_detail($r), $used_tool, $calls_made, $tool_texts);
@@ -10569,8 +10643,13 @@
10569 10643 'tool_use_id' => $tool_use_id,
10570 10644 'content' => $exec['content'],
10571 10645 );
10572 10646 }
10647 + if ($calls_made < $budget) {
10648 + // plan 150b72: remaining-count line as a text block AFTER the
10649 + // tool_result blocks (Anthropic requires the results first).
10650 + $results[] = array('type' => 'text', 'text' => $this->mxchat_fc_remaining_text($budget - $calls_made));
10651 + }
10573 10652 $messages[] = array('role' => 'user', 'content' => $results);
10574 10653 }
10575 10654 return $this->mxchat_fc_degrade('anthropic', 'hop depth exhausted', $used_tool, $calls_made, $tool_texts);
10576 10655 }
@@ -10576,10 +10655,18 @@
10576 10655 }
10577 10656
10578 10657 /* ---------------- Google Gemini loop ---------------- */
10579 10658 private function mxchat_fc_loop_gemini($prov, $system, $relevant_content, $conversation_history, $tools, $orig_message, $user_id, $session_id) {
10659 + $depth = MxChat_Tool_Registry::max_depth();
10660 + $budget = MxChat_Tool_Registry::max_tool_calls_per_turn();
10661 + $tool_schema = MxChat_Tool_Registry::to_gemini_tools($tools);
10580 10662 $contents = array();
10581 - $contents[] = array('role' => 'user', 'parts' => array(array('text' => '[System Instructions] ' . $system . ' ' . $relevant_content)));
10663 + // plan 150b72: the same budget sentence as the other two loops, in the
10664 + // same place Gemini gets its system text. Gemini has no documented
10665 + // per-request parallel-disable flag (functionCallingConfig carries mode
10666 + // and allowedFunctionNames only — checked 2026-09-27), so the prompt
10667 + // side is all it gets; nothing undocumented is sent.
10668 + $contents[] = array('role' => 'user', 'parts' => array(array('text' => '[System Instructions] ' . $system . ' ' . $relevant_content . $this->mxchat_fc_budget_notice($budget, $tool_schema))));
10582 10669 $contents[] = array('role' => 'model', 'parts' => array(array('text' => 'I understand and will follow these instructions.')));
10583 10670 foreach ($this->mxchat_fc_normalize_history($conversation_history) as $m) {
10584 10671 $contents[] = array('role' => ($m['role'] === 'assistant' ? 'model' : 'user'),
10585 10672 'parts' => array(array('text' => $m['content'])));
@@ -10584,11 +10671,8 @@
10584 10671 $contents[] = array('role' => ($m['role'] === 'assistant' ? 'model' : 'user'),
10585 10672 'parts' => array(array('text' => $m['content'])));
10586 10673 }
10587 10674
10588 - $depth = MxChat_Tool_Registry::max_depth();
10589 - $budget = MxChat_Tool_Registry::max_tool_calls_per_turn();
10590 - $tool_schema = MxChat_Tool_Registry::to_gemini_tools($tools);
10591 10675 // Function calling (tools + functionDeclarations + toolConfig) is a v1beta feature on the
10592 10676 // Generative Language REST API. The v1 endpoint silently ignores the tools array, so a
10593 10677 // non-preview model (e.g. gemini-2.5-pro, gemini-3.5-flash, gemini-3.1-flash-lite) would
10594 10678 // just answer in text and never emit a tool call. Always use v1beta for the FC loop —
@@ -10679,8 +10763,13 @@
10679 10763 // guidance). Older models omit the id — then we send none, exactly as before.
10680 10764 if (isset($fcall['id']) && $fcall['id'] !== '') { $fr['id'] = $fcall['id']; }
10681 10765 $resp_parts[] = array('functionResponse' => $fr);
10682 10766 }
10767 + if ($calls_made < $budget) {
10768 + // plan 150b72: remaining-count line as a text part after the
10769 + // functionResponse parts of the same turn.
10770 + $resp_parts[] = array('text' => $this->mxchat_fc_remaining_text($budget - $calls_made));
10771 + }
10683 10772 $contents[] = array('role' => 'user', 'parts' => $resp_parts);
10684 10773 }
10685 10774 return $this->mxchat_fc_degrade('gemini', 'hop depth exhausted', $used_tool, $calls_made, $tool_texts);
10686 10775 }
@@ -10697,8 +10786,55 @@
10697 10786 * tool_call_id, Gemini a functionResponse per functionCall.
10698 10787 */
10699 10788 private function mxchat_fc_budget_exhausted_text() {
10700 10789 return 'Tool-call budget for this turn is exhausted; answer with what you already have.';
10790 +}
10791 +
10792 +/**
10793 + * plan 150b72 — the budget sentence the model reads BEFORE it plans its
10794 + * calls. Before this the model learned the cap only from the refused calls'
10795 + * exhausted text, i.e. after it had already overrun it (3c23ab made
10796 + * exhaustion safe, not visible). Returned WITH a leading space so callers
10797 + * concatenate it onto their system text; '' when no tools are offered or the
10798 + * owner suppresses it. Two sentences at most: it precedes every owner's own
10799 + * instructions and must not compete with them. Model-facing, never shown.
10800 + *
10801 + * @param int $budget Resolved per-turn cap (MxChat_Tool_Registry::max_tool_calls_per_turn()).
10802 + * @param array $tool_schema Provider-shaped tool list; empty = no tools offered = no sentence.
10803 + * @return string
10804 + */
10805 +private function mxchat_fc_budget_notice($budget, $tool_schema) {
10806 + $budget = (int) $budget;
10807 + if ($budget < 1 || empty($tool_schema)) {
10808 + return '';
10809 + }
10810 + $notice = sprintf(
10811 + 'You may make at most %d tool call%s while answering this message. Prefer one call with complete arguments over several narrow ones, and answer from what you have once the calls are used up.',
10812 + $budget,
10813 + $budget === 1 ? '' : 's'
10814 + );
10815 + /**
10816 + * Filter the tool-budget sentence appended to the system content when
10817 + * tools are offered. Return '' to send nothing (an owner who writes their
10818 + * own tool policy).
10819 + *
10820 + * @param string $notice The sentence(s).
10821 + * @param int $budget The resolved per-turn tool-call budget.
10822 + */
10823 + $notice = apply_filters('mxchat_function_calling_budget_notice', $notice, $budget);
10824 + $notice = is_string($notice) ? trim($notice) : '';
10825 + return $notice === '' ? '' : ' ' . $notice;
10826 +}
10827 +
10828 +/**
10829 + * plan 150b72 — one line appended to a hop's tool-result batch (its own
10830 + * message / block / part, never inside a tool's result) saying how many
10831 + * calls remain for the turn. Only while the budget is not yet spent — a
10832 + * spent budget is covered by mxchat_fc_budget_exhausted_text().
10833 + */
10834 +private function mxchat_fc_remaining_text($remaining) {
10835 + $remaining = max(0, (int) $remaining);
10836 + return sprintf('%d tool call%s for this message.', $remaining, $remaining === 1 ? ' remains' : 's remain');
10701 10837 }
10702 10838
10703 10839 /**
10704 10840 * plan 3c23ab — the loop cannot finish normally (provider rejected a hop, or