PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.0
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 3.5.2 All 199 releases
betterdocs / includes / AI / Providers / BaseProvider.php

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

208 lines 7.3 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 use WPDeveloper\BetterDocs\AI\ModelRegistry;
6 use WPDeveloper\BetterDocs\AI\Contracts\AIProvider;
7 use WPDeveloper\BetterDocs\Utils\AIHelper;
8
9 /**
10 * Shared plumbing for concrete providers: construction, model lookup, the
11 * min-token floor, HTTP transport, and usage normalization. Concrete providers
12 * implement id()/label()/chat()/validate_key() and reuse the helpers here.
13 *
14 * @since 4.4.0
15 */
16 abstract class BaseProvider implements AIProvider {
17
18 /**
19 * @var string Configured API key.
20 */
21 protected $api_key;
22
23 /**
24 * @var string Configured model id.
25 */
26 protected $model;
27
28 /**
29 * @param string $api_key Configured key for this platform.
30 * @param string $model Configured model id; falls back to the platform default.
31 */
32 public function __construct( $api_key = '', $model = '' ) {
33 $this->api_key = (string) $api_key;
34 $this->model = $model !== '' ? (string) $model : $this->default_model();
35 }
36
37 /**
38 * {@inheritDoc}
39 */
40 public function models() {
41 return ModelRegistry::models( $this->id() );
42 }
43
44 /**
45 * {@inheritDoc}
46 */
47 public function default_model() {
48 return ModelRegistry::default_model( $this->id() );
49 }
50
51 /**
52 * Resolve the model to use for a request (option override wins).
53 *
54 * @param array $options
55 * @return string
56 */
57 protected function resolve_model( $options ) {
58 return ! empty( $options['model'] ) ? (string) $options['model'] : $this->model;
59 }
60
61 /**
62 * Apply the per-feature min-token floor when a context is supplied.
63 *
64 * Reuses the existing policy in AIHelper so the floor stays consistent with
65 * the React notice and server-side save validation.
66 *
67 * @param int $max_tokens
68 * @param string $model
69 * @param string|null $context
70 * @return int
71 */
72 protected function floor_tokens( $max_tokens, $model, $context = null ) {
73 $max_tokens = (int) $max_tokens;
74 if ( null === $context ) {
75 return $max_tokens;
76 }
77 $min = AIHelper::get_min_tokens( $context, $model );
78 return ( $min > 0 && $max_tokens < $min ) ? $min : $max_tokens;
79 }
80
81 /**
82 * POST JSON and decode the response into an array (or WP_Error).
83 *
84 * @param string $url
85 * @param array $headers
86 * @param array $body
87 * @param int $timeout
88 * @param int|null $status_code Out-param: set to the HTTP response code (0 on
89 * transport failure) so callers can classify errors
90 * by status without re-reading the response.
91 * @return array|\WP_Error Decoded body array, or WP_Error on transport failure.
92 */
93 protected function post_json( $url, $headers, $body, $timeout = 50, &$status_code = null ) {
94 $headers = wp_parse_args( $headers, array( 'Content-Type' => 'application/json' ) );
95
96 $response = wp_remote_post( $url, array(
97 'headers' => $headers,
98 'body' => wp_json_encode( $body ),
99 'timeout' => (int) $timeout,
100 ) );
101
102 if ( is_wp_error( $response ) ) {
103 $status_code = 0;
104 return new \WP_Error( 'api_error', sprintf(
105 /* translators: 1: provider label, 2: error message */
106 __( 'Failed to connect to %1$s: %2$s', 'betterdocs' ),
107 $this->label(),
108 $response->get_error_message()
109 ) );
110 }
111
112 $status_code = (int) wp_remote_retrieve_response_code( $response );
113
114 $data = json_decode( wp_remote_retrieve_body( $response ), true );
115 if ( ! is_array( $data ) ) {
116 return new \WP_Error( 'no_content', sprintf(
117 /* translators: %s: provider label */
118 __( 'Empty or invalid response from %s.', 'betterdocs' ),
119 $this->label()
120 ) );
121 }
122
123 return $data;
124 }
125
126 /**
127 * Turn an HTTP status + the provider's raw error text into a clear, actionable
128 * message. Chiefly distinguishes a retired/unknown model (404) from a genuine
129 * quota / rate-limit rejection (429) — without this, a dead model id and a real
130 * quota error both surface the provider's raw text and look identical (a retired
131 * model reads like "quota exceeded"). Falls back to the raw message otherwise.
132 *
133 * @param int $status HTTP status code.
134 * @param string $raw_message Provider-supplied error message.
135 * @param string $model Model id in play, for the "unavailable" message.
136 * @return string
137 */
138 protected function classify_http_error( $status, $raw_message, $model = '' ) {
139 $status = (int) $status;
140 $raw = strtolower( (string) $raw_message );
141
142 if ( 404 === $status || false !== strpos( $raw, 'not found' ) || false !== strpos( $raw, 'is not supported' ) ) {
143 return sprintf(
144 /* translators: 1: provider label, 2: model id */
145 __( 'The %1$s model "%2$s" is unavailable — it may have been retired. Choose a different model.', 'betterdocs' ),
146 $this->label(),
147 $model
148 );
149 }
150
151 if ( 429 === $status || false !== strpos( $raw, 'resource_exhausted' ) || false !== strpos( $raw, 'quota' ) || false !== strpos( $raw, 'rate limit' ) ) {
152 return sprintf(
153 /* translators: %s: provider label */
154 __( 'Your %s request hit a quota or rate limit. Check your plan and limits, then try again.', 'betterdocs' ),
155 $this->label()
156 );
157 }
158
159 return (string) $raw_message;
160 }
161
162 /**
163 * Normalize a usage block into prompt/completion/total token counts.
164 *
165 * @param array $usage Provider-specific usage payload.
166 * @param array $map Keys map: array( 'prompt'=>..., 'completion'=>..., 'total'=>... ).
167 * @return array
168 */
169 protected function normalize_usage( $usage, $map ) {
170 $get = function ( $key ) use ( $usage ) {
171 return ( $key && isset( $usage[ $key ] ) ) ? (int) $usage[ $key ] : null;
172 };
173
174 $prompt = $get( isset( $map['prompt'] ) ? $map['prompt'] : null );
175 $completion = $get( isset( $map['completion'] ) ? $map['completion'] : null );
176 $total = $get( isset( $map['total'] ) ? $map['total'] : null );
177
178 if ( null === $total && ( null !== $prompt || null !== $completion ) ) {
179 $total = (int) $prompt + (int) $completion;
180 }
181
182 return array(
183 'prompt_tokens' => $prompt,
184 'completion_tokens' => $completion,
185 'total_tokens' => $total,
186 );
187 }
188
189 /**
190 * Build the normalized success envelope returned by chat().
191 *
192 * @param string $content
193 * @param string $model
194 * @param array $usage
195 * @param string|null $finish_reason
196 * @return array
197 */
198 protected function success( $content, $model, $usage, $finish_reason = null ) {
199 return array(
200 'success' => true,
201 'content' => (string) $content,
202 'model' => (string) $model,
203 'usage' => $usage,
204 'finish_reason' => $finish_reason,
205 );
206 }
207 }
208