PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 All 200 releases
← All changes | includes/Utils/AIHelper.php +94 -77 4.4.14.9.2 View file →
@@ -2,8 +2,9 @@
2 2
3 3 namespace WPDeveloper\BetterDocs\Utils;
4 4
5 5 use WPDeveloper\BetterDocs\Core\Settings;
6 +use WPDeveloper\BetterDocs\AI\ProviderFactory;
6 7
7 8 class AIHelper {
8 9
9 10 /**
@@ -17,14 +18,24 @@
17 18 $this->settings = $settings;
18 19 }
19 20
20 21 /**
21 - * Get OpenAI API key from settings
22 + * Build a provider factory bound to the current settings.
22 23 *
24 + * @return ProviderFactory
25 + */
26 + private function factory() {
27 + return new ProviderFactory( $this->settings );
28 + }
29 +
30 + /**
31 + * Get the API key for the active AI platform.
32 + *
23 33 * @return string
24 34 */
25 35 public function get_api_key() {
26 - return $this->settings->get( 'ai_autowrite_api_key', '' );
36 + $factory = $this->factory();
37 + return $factory->api_key_for( $factory->active_platform() );
27 38 }
28 39
29 40 /**
30 41 * Check if OpenAI API key is configured
@@ -48,105 +59,111 @@
48 59 }
49 60
50 61 if ( empty( $api_key ) ) {
51 62 return array(
52 - 'valid' => false,
53 - 'message' => 'Please Insert your <a href="/admin.php?page=betterdocs-settings">OpenAI API Key</a> to use AI features.'
63 + 'valid' => false,
64 + 'message' => 'Please Insert your <a href="/admin.php?page=betterdocs-settings#betterdocs-ai">API Key</a> to use AI features.'
54 65 );
55 66 }
56 67
57 - $ch = curl_init( 'https://api.openai.com/v1/models' ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_init
58 - curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
59 - curl_setopt( //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
60 - $ch,
61 - CURLOPT_HTTPHEADER,
62 - array(
63 - 'Content-Type: application/json',
64 - 'Authorization: Bearer ' . $api_key
65 - )
66 - );
68 + $factory = $this->factory();
69 + return $factory->validate( $factory->active_platform(), $api_key );
70 + }
67 71
68 - $response = curl_exec( $ch ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_exec
69 - $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_getinfo
70 - curl_close( $ch ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_close
72 + /**
73 + * Minimum token policy by (feature context, model family). Used as the
74 + * single source of truth for:
75 + * - server-side save validation (Core/Settings.php)
76 + * - field UI props sent to the React notice (Core/Settings.php)
77 + * - runtime payload floor in the provider layer (AI\Providers\BaseProvider::floor_tokens)
78 + *
79 + * Override the whole map (or any cell) via the `betterdocs_ai_min_tokens`
80 + * filter. Returns 0 when no minimum applies (unknown context or model).
81 + *
82 + * @param string $context Feature key, e.g. 'write_with_ai' or 'article_summary'.
83 + * @param string $model OpenAI model identifier.
84 + * @return int Minimum recommended max_tokens for that pair.
85 + */
86 + public static function get_min_tokens( $context, $model ) {
87 + $family = self::token_family( $model );
88 + $map = apply_filters( 'betterdocs_ai_min_tokens', array(
89 + 'write_with_ai' => array( 'gpt-4' => 2500, 'gpt-5' => 4500, 'gpt-5.5' => 10000 ),
90 + 'article_summary' => array( 'gpt-4' => 1500, 'gpt-5' => 2500, 'gpt-5.5' => 10000 ),
91 + ) );
92 + return isset( $map[ $context ][ $family ] ) ? (int) $map[ $context ][ $family ] : 0;
93 + }
71 94
72 - if ( 200 == $httpCode ) {
73 - return array(
74 - 'valid' => true,
75 - 'message' => 'Valid API Key'
76 - );
77 - } else {
78 - $responseData = json_decode( $response, true );
79 - $messageData = $responseData[ 'error' ] ?? '';
80 - return array(
81 - 'valid' => false,
82 - 'message' => $messageData[ 'message' ] ?? 'Invalid API Key'
83 - );
95 + /**
96 + * Map a model id to its token-floor family key.
97 + *
98 + * gpt-5.x point releases (gpt-5.5, gpt-5.1, ...) generate much larger, slower
99 + * responses and need a heavier floor than the base gpt-5 family, so they get
100 + * their own 'gpt-5.5' key. Plain gpt-5* stays 'gpt-5'; everything else 'gpt-4'.
101 + *
102 + * @param string $model OpenAI model identifier.
103 + * @return string Family key used in the min-token map.
104 + */
105 + private static function token_family( $model ) {
106 + if ( self::is_gpt5_point_release( $model ) ) {
107 + return 'gpt-5.5';
84 108 }
109 + return ( 0 === strpos( (string) $model, 'gpt-5' ) ) ? 'gpt-5' : 'gpt-4';
85 110 }
86 111
87 112 /**
88 - * Make a request to OpenAI API
113 + * Whether a model is a gpt-5.x point release (gpt-5.5, gpt-5.1, ...), which
114 + * use the newer reasoning_effort vocabulary and need a heavier token floor.
89 115 *
90 - * @param array $messages Array of messages for the chat completion
91 - * @param array $options Optional parameters (model, max_tokens, temperature, etc.)
92 - * @return string|\WP_Error API response content or error
116 + * @param string $model OpenAI model identifier.
117 + * @return bool
93 118 */
94 - public function make_openai_request( $messages, $options = array() ) {
95 - $api_key = $this->get_api_key();
96 - $max_tokens = $this->settings->get( 'article_summary_max_token', 1500 );
97 - $model = $this->settings->get( 'article_summary_model', 'gpt-4o-mini' );
119 + public static function is_gpt5_point_release( $model ) {
120 + return (bool) preg_match( '/^gpt-5\.\d/', (string) $model );
121 + }
98 122
99 - if ( empty( $api_key ) ) {
100 - return new \WP_Error( 'no_api_key', 'OpenAI API key is not configured.' );
101 - }
123 + /**
124 + * Return the threshold map for one context, in {family => min} shape, so
125 + * Settings.php can serialize it onto a field for the React notice to read.
126 + *
127 + * @param string $context Feature key.
128 + * @return array<string,int>
129 + */
130 + public static function get_min_tokens_map( $context ) {
131 + return array(
132 + 'gpt-4' => self::get_min_tokens( $context, 'gpt-4o' ),
133 + 'gpt-5' => self::get_min_tokens( $context, 'gpt-5' ),
134 + 'gpt-5.5' => self::get_min_tokens( $context, 'gpt-5.5' ),
135 + );
136 + }
102 137
103 - // Default options
138 + /**
139 + * Make a chat-completion request to the active AI platform.
140 + *
141 + * Provider-agnostic: the platform, model, key, payload shape and parsing are
142 + * resolved by ProviderFactory. The model is the global `ai_model`; callers
143 + * may still override per request via $options['model'].
144 + *
145 + * @param array $messages Array of messages for the chat completion.
146 + * @param array $options Optional parameters (model, max_tokens, temperature, timeout).
147 + * @return string|\WP_Error API response content or error.
148 + */
149 + public function make_openai_request( $messages, $options = array() ) {
104 150 $defaults = array(
105 - 'model' => $model,
106 - 'max_tokens' => $max_tokens,
151 + 'max_tokens' => (int) $this->settings->get( 'article_summary_max_token', 1500 ),
107 152 'temperature' => 0.7,
108 - 'timeout' => 50
153 + 'timeout' => 50,
154 + 'context' => 'article_summary',
109 155 );
110 156
111 157 $options = wp_parse_args( $options, $defaults );
112 158
113 - $api_endpoint = 'https://api.openai.com/v1/chat/completions';
159 + $result = $this->factory()->make()->chat( $messages, $options );
114 160
115 - $request_body = array(
116 - 'model' => $options[ 'model' ],
117 - 'messages' => $messages,
118 - 'max_tokens' => $options[ 'max_tokens' ],
119 - 'temperature' => $options[ 'temperature' ]
120 - );
121 -
122 - $request_options = array(
123 - 'headers' => array(
124 - 'Content-Type' => 'application/json',
125 - 'Authorization' => 'Bearer ' . $api_key
126 - ),
127 - 'body' => json_encode( $request_body ), //phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
128 - 'timeout' => $options[ 'timeout' ]
129 - );
130 -
131 - $response = wp_remote_post( $api_endpoint, $request_options );
132 -
133 - if ( is_wp_error( $response ) ) {
134 - return new \WP_Error( 'api_error', 'Failed to connect to OpenAI API: ' . $response->get_error_message() );
161 + if ( is_wp_error( $result ) ) {
162 + return $result;
135 163 }
136 164
137 - $body = wp_remote_retrieve_body( $response );
138 - $data = json_decode( $body, true );
139 -
140 - if ( ! empty( $data[ 'error' ] ) ) {
141 - return new \WP_Error( 'openai_error', $data[ 'error' ][ 'message' ] );
142 - }
143 -
144 - if ( empty( $data[ 'choices' ][ 0 ][ 'message' ][ 'content' ] ) ) {
145 - return new \WP_Error( 'no_content', 'No content received from OpenAI.' );
146 - }
147 -
148 - return $data[ 'choices' ][ 0 ][ 'message' ][ 'content' ];
165 + return $result['content'];
149 166 }
150 167
151 168 /**
152 169 * Analyze article quality using OpenAI