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
betterdocs / includes / AI / Providers / OpenAICompatibleProvider.php

OpenAICompatibleProvider.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.2, at includes/AI/Providers/OpenAICompatibleProvider.php

180 lines 6.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\AI\Providers;
4
5 /**
6 * Base for platforms that speak the OpenAI Chat Completions wire format
7 * (OpenAI itself, DeepSeek, OpenRouter, …). Subclasses only declare their id,
8 * label, and base URL; everything else — payload, auth, parsing, key
9 * validation — is shared here.
10 *
11 * @since 4.4.0
12 */
13 abstract class OpenAICompatibleProvider extends BaseProvider {
14
15 /**
16 * API base URL without trailing slash, e.g. 'https://api.openai.com/v1'.
17 *
18 * @return string
19 */
20 abstract protected function base_url();
21
22 /**
23 * Chat completions endpoint.
24 *
25 * @return string
26 */
27 protected function chat_url() {
28 return $this->base_url() . '/chat/completions';
29 }
30
31 /**
32 * Endpoint used to validate a key (a cheap authenticated GET).
33 *
34 * @return string
35 */
36 protected function models_url() {
37 return $this->base_url() . '/models';
38 }
39
40 /**
41 * Authorization + any platform-specific headers.
42 *
43 * @param string $api_key
44 * @return array
45 */
46 protected function request_headers( $api_key ) {
47 return array_merge(
48 array(
49 'Content-Type' => 'application/json',
50 'Authorization' => 'Bearer ' . $api_key,
51 ),
52 $this->extra_headers()
53 );
54 }
55
56 /**
57 * Optional extra headers (OpenRouter uses these for attribution).
58 *
59 * @return array
60 */
61 protected function extra_headers() {
62 return array();
63 }
64
65 /**
66 * Build the request body. Overridden by OpenAIProvider for the GPT-5 family.
67 *
68 * @param string $model
69 * @param array $messages
70 * @param int $max_tokens
71 * @param float|null $temperature
72 * @return array
73 */
74 protected function build_payload( $model, $messages, $max_tokens, $temperature = null ) {
75 $payload = array(
76 'model' => $model,
77 'messages' => $messages,
78 'max_tokens' => (int) $max_tokens,
79 );
80 if ( null !== $temperature ) {
81 $payload['temperature'] = (float) $temperature;
82 }
83 return $payload;
84 }
85
86 /**
87 * {@inheritDoc}
88 */
89 public function chat( $messages, $options = array() ) {
90 if ( empty( $this->api_key ) ) {
91 return new \WP_Error( 'no_api_key', sprintf(
92 /* translators: %s: provider label */
93 __( '%s API key is not configured.', 'betterdocs' ),
94 $this->label()
95 ) );
96 }
97
98 $model = $this->resolve_model( $options );
99 $context = isset( $options['context'] ) ? $options['context'] : null;
100 $max_tokens = $this->floor_tokens(
101 isset( $options['max_tokens'] ) ? $options['max_tokens'] : 2500,
102 $model,
103 $context
104 );
105 $temperature = isset( $options['temperature'] ) ? $options['temperature'] : null;
106 $timeout = isset( $options['timeout'] ) ? $options['timeout'] : 50;
107
108 $payload = $this->build_payload( $model, $messages, $max_tokens, $temperature );
109
110 $status = null;
111 $data = $this->post_json( $this->chat_url(), $this->request_headers( $this->api_key ), $payload, $timeout, $status );
112 if ( is_wp_error( $data ) ) {
113 return $data;
114 }
115
116 if ( ! empty( $data['error'] ) ) {
117 $message = isset( $data['error']['message'] ) ? $data['error']['message'] : __( 'Unknown API error.', 'betterdocs' );
118 // Classify by HTTP status so a rate limit reads as one. Every platform on
119 // this wire format (OpenAI, DeepSeek, OpenRouter) answers a quota/rate
120 // rejection with 429 and a retired model with 404, but their raw text
121 // differs per vendor — without this the REST layer relayed that text
122 // verbatim as a bare `ai_upstream` 502 and the user never learned it was
123 // a rate limit they could simply wait out.
124 return new \WP_Error( 'provider_error', $this->classify_http_error( $status, $message, $model ) );
125 }
126
127 if ( ! isset( $data['choices'][0]['message']['content'] ) || '' === $data['choices'][0]['message']['content'] ) {
128 return new \WP_Error( 'no_content', sprintf(
129 /* translators: %s: provider label */
130 __( 'No content received from %s.', 'betterdocs' ),
131 $this->label()
132 ) );
133 }
134
135 $usage = $this->normalize_usage(
136 isset( $data['usage'] ) && is_array( $data['usage'] ) ? $data['usage'] : array(),
137 array( 'prompt' => 'prompt_tokens', 'completion' => 'completion_tokens', 'total' => 'total_tokens' )
138 );
139
140 return $this->success(
141 $data['choices'][0]['message']['content'],
142 isset( $data['model'] ) ? $data['model'] : $model,
143 $usage,
144 isset( $data['choices'][0]['finish_reason'] ) ? $data['choices'][0]['finish_reason'] : null
145 );
146 }
147
148 /**
149 * {@inheritDoc}
150 */
151 public function validate_key( $api_key = '' ) {
152 $api_key = $api_key !== '' ? $api_key : $this->api_key;
153
154 if ( empty( $api_key ) ) {
155 return array(
156 'valid' => false,
157 'message' => __( 'Please insert your API key to use AI features.', 'betterdocs' ),
158 );
159 }
160
161 $response = wp_remote_get( $this->models_url(), array(
162 'headers' => $this->request_headers( $api_key ),
163 'timeout' => 15,
164 ) );
165
166 if ( is_wp_error( $response ) ) {
167 return array( 'valid' => false, 'message' => $response->get_error_message() );
168 }
169
170 $code = (int) wp_remote_retrieve_response_code( $response );
171 if ( 200 === $code ) {
172 return array( 'valid' => true, 'message' => __( 'Valid API Key', 'betterdocs' ) );
173 }
174
175 $body = json_decode( wp_remote_retrieve_body( $response ), true );
176 $message = isset( $body['error']['message'] ) ? $body['error']['message'] : __( 'Invalid API Key', 'betterdocs' );
177 return array( 'valid' => false, 'message' => $message );
178 }
179 }
180