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 +33 -155 4.5.44.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,41 +59,15 @@
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#betterdocs-ai">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 - );
67 -
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
71 -
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 - );
84 - }
68 + $factory = $this->factory();
69 + return $factory->validate( $factory->active_platform(), $api_key );
85 70 }
86 71
87 72 /**
88 73 * Minimum token policy by (feature context, model family). Used as the
@@ -88,9 +73,9 @@
88 73 * Minimum token policy by (feature context, model family). Used as the
89 74 * single source of truth for:
90 75 * - server-side save validation (Core/Settings.php)
91 76 * - field UI props sent to the React notice (Core/Settings.php)
92 - * - runtime payload floor in build_openai_payload()
77 + * - runtime payload floor in the provider layer (AI\Providers\BaseProvider::floor_tokens)
93 78 *
94 79 * Override the whole map (or any cell) via the `betterdocs_ai_min_tokens`
95 80 * filter. Returns 0 when no minimum applies (unknown context or model).
96 81 *
@@ -130,9 +115,9 @@
130 115 *
131 116 * @param string $model OpenAI model identifier.
132 117 * @return bool
133 118 */
134 - private static function is_gpt5_point_release( $model ) {
119 + public static function is_gpt5_point_release( $model ) {
135 120 return (bool) preg_match( '/^gpt-5\.\d/', (string) $model );
136 121 }
137 122
138 123 /**
@@ -150,142 +135,35 @@
150 135 );
151 136 }
152 137
153 138 /**
154 - * Build an OpenAI Chat Completions request body, switching parameter shape
155 - * for model families that reject the legacy max_tokens / custom temperature.
139 + * Make a chat-completion request to the active AI platform.
156 140 *
157 - * GPT-5 family requires max_completion_tokens and rejects any non-default
158 - * temperature, so we omit both. It is also a reasoning model: internal
159 - * reasoning tokens are billed against max_completion_tokens before any
160 - * visible output is produced, so we send a low reasoning_effort by default
161 - * (see default_reasoning_effort()). Without that the model can spend the
162 - * entire budget on reasoning and return empty content with
163 - * finish_reason=length.
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'].
164 144 *
165 - * When `$context` is provided we also raise `$max_tokens` to the per-family
166 - * minimum from get_min_tokens(), so the request never goes out below the
167 - * policy floor regardless of what's stored in settings.
168 - *
169 - * @param string $model OpenAI model identifier (e.g. 'gpt-4o', 'gpt-5-mini').
170 - * @param array $messages Chat messages array.
171 - * @param int $max_tokens Token cap (will be raised to feature minimum if $context is set).
172 - * @param float|null $temperature Optional sampling temperature; ignored for gpt-5*.
173 - * @param string|null $context Feature key for runtime min-token enforcement. Pass null for back-compat.
174 - * @return array Request body ready to JSON-encode.
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.
175 148 */
176 - public static function build_openai_payload( $model, $messages, $max_tokens, $temperature = null, $context = null ) {
177 - if ( null !== $context ) {
178 - $min = self::get_min_tokens( $context, $model );
179 - if ( $min > 0 && (int) $max_tokens < $min ) {
180 - $max_tokens = $min;
181 - }
182 - }
183 -
184 - $payload = array(
185 - 'model' => $model,
186 - 'messages' => $messages,
187 - );
188 -
189 - if ( 0 === strpos( $model, 'gpt-5' ) ) {
190 - $payload['max_completion_tokens'] = $max_tokens;
191 - $payload['reasoning_effort'] = apply_filters( 'betterdocs_openai_gpt5_reasoning_effort', self::default_reasoning_effort( $model ), $model, $max_tokens );
192 - return $payload;
193 - }
194 -
195 - $payload['max_tokens'] = $max_tokens;
196 - if ( null !== $temperature ) {
197 - $payload['temperature'] = $temperature;
198 - }
199 - return $payload;
200 - }
201 -
202 - /**
203 - * Default reasoning_effort for a gpt-5* model.
204 - *
205 - * The original GPT-5 generation (gpt-5, gpt-5-mini, gpt-5-nano) accepts
206 - * 'minimal'. The gpt-5.x point releases (e.g. gpt-5.5) dropped 'minimal'
207 - * from the API and only accept none|low|medium|high|xhigh; sending
208 - * 'minimal' returns a 400 "Unsupported value: 'reasoning_effort'". For
209 - * those we default to 'none' — no reasoning tokens, which is the fastest
210 - * option and leaves the whole token budget for visible output (the closest
211 - * equivalent to the gpt-5 'minimal' behaviour). Override per model via the
212 - * betterdocs_openai_gpt5_reasoning_effort filter.
213 - *
214 - * @param string $model OpenAI model identifier.
215 - * @return string reasoning_effort value.
216 - */
217 - private static function default_reasoning_effort( $model ) {
218 - // Point releases like gpt-5.5 use the new vocabulary; plain gpt-5* keep 'minimal'.
219 - if ( self::is_gpt5_point_release( $model ) ) {
220 - return 'none';
221 - }
222 - return 'minimal';
223 - }
224 -
225 - /**
226 - * Make a request to OpenAI API
227 - *
228 - * @param array $messages Array of messages for the chat completion
229 - * @param array $options Optional parameters (model, max_tokens, temperature, etc.)
230 - * @return string|\WP_Error API response content or error
231 - */
232 149 public function make_openai_request( $messages, $options = array() ) {
233 - $api_key = $this->get_api_key();
234 - $max_tokens = $this->settings->get( 'article_summary_max_token', 1500 );
235 - $model = $this->settings->get( 'article_summary_model', 'gpt-4o-mini' );
236 -
237 - if ( empty( $api_key ) ) {
238 - return new \WP_Error( 'no_api_key', 'OpenAI API key is not configured.' );
239 - }
240 -
241 - // Default options
242 150 $defaults = array(
243 - 'model' => $model,
244 - 'max_tokens' => $max_tokens,
151 + 'max_tokens' => (int) $this->settings->get( 'article_summary_max_token', 1500 ),
245 152 'temperature' => 0.7,
246 - 'timeout' => 50
153 + 'timeout' => 50,
154 + 'context' => 'article_summary',
247 155 );
248 156
249 157 $options = wp_parse_args( $options, $defaults );
250 158
251 - $api_endpoint = 'https://api.openai.com/v1/chat/completions';
159 + $result = $this->factory()->make()->chat( $messages, $options );
252 160
253 - $request_body = self::build_openai_payload(
254 - $options[ 'model' ],
255 - $messages,
256 - $options[ 'max_tokens' ],
257 - $options[ 'temperature' ],
258 - 'article_summary'
259 - );
260 -
261 - $request_options = array(
262 - 'headers' => array(
263 - 'Content-Type' => 'application/json',
264 - 'Authorization' => 'Bearer ' . $api_key
265 - ),
266 - 'body' => json_encode( $request_body ), //phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
267 - 'timeout' => $options[ 'timeout' ]
268 - );
269 -
270 - $response = wp_remote_post( $api_endpoint, $request_options );
271 -
272 - if ( is_wp_error( $response ) ) {
273 - 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;
274 163 }
275 164
276 - $body = wp_remote_retrieve_body( $response );
277 - $data = json_decode( $body, true );
278 -
279 - if ( ! empty( $data[ 'error' ] ) ) {
280 - return new \WP_Error( 'openai_error', $data[ 'error' ][ 'message' ] );
281 - }
282 -
283 - if ( empty( $data[ 'choices' ][ 0 ][ 'message' ][ 'content' ] ) ) {
284 - return new \WP_Error( 'no_content', 'No content received from OpenAI.' );
285 - }
286 -
287 - return $data[ 'choices' ][ 0 ][ 'message' ][ 'content' ];
165 + return $result['content'];
288 166 }
289 167
290 168 /**
291 169 * Analyze article quality using OpenAI