PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.3.4
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.3.4
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 All 66 releases
better-payment / includes / AI / Providers / AbstractProvider.php

AbstractProvider.php in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.3.4, at includes/AI/Providers/AbstractProvider.php

247 lines 8.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Better_Payment\Lite\AI\Providers;
4
5 use Better_Payment\Lite\AI\Contracts\AIProviderInterface;
6
7 if ( ! defined( 'ABSPATH' ) ) {
8 exit;
9 }
10
11 /**
12 * Shared plumbing for HTTP-based AI providers.
13 *
14 * Follows the plugin's existing outbound-request idiom (see
15 * Blocks/BlockActions.php): wp_remote_post + is_wp_error + wp_remote_retrieve_body
16 * + json_decode. Concrete providers implement only the request/response mapping
17 * for their API.
18 *
19 * @see AIProviderInterface
20 */
21 abstract class AbstractProvider implements AIProviderInterface {
22
23 /**
24 * Provider configuration.
25 *
26 * @var array {
27 * @type string $api_key
28 * @type string $model
29 * @type string $image_model
30 * @type float $temperature
31 * @type int $max_tokens
32 * }
33 */
34 protected array $config;
35
36 /**
37 * @param array $config
38 */
39 public function __construct( array $config = [] ) {
40 $this->config = wp_parse_args( $config, [
41 'api_key' => '',
42 'model' => '',
43 'image_model' => '',
44 'temperature' => 0.7,
45 'max_tokens' => 4096,
46 ] );
47 }
48
49 public function is_configured(): bool {
50 return '' !== trim( (string) $this->config['api_key'] );
51 }
52
53 public function supports_images(): bool {
54 return false;
55 }
56
57 public function generate_image( string $prompt, array $options = [] ): array {
58 return [ 'error' => __( 'This provider does not support image generation.', 'better-payment' ) ];
59 }
60
61 /**
62 * Resolve the model id for a chat request (option override → config → first known).
63 */
64 protected function resolve_model( array $options ): string {
65 if ( ! empty( $options['model'] ) ) {
66 return (string) $options['model'];
67 }
68 if ( ! empty( $this->config['model'] ) ) {
69 return (string) $this->config['model'];
70 }
71 $models = $this->get_models();
72 return $models[0] ?? '';
73 }
74
75 protected function temperature( array $options ): float {
76 if ( isset( $options['temperature'] ) && is_numeric( $options['temperature'] ) ) {
77 return (float) $options['temperature'];
78 }
79 return (float) $this->config['temperature'];
80 }
81
82 protected function max_tokens( array $options ): int {
83 if ( isset( $options['max_tokens'] ) && is_numeric( $options['max_tokens'] ) ) {
84 return (int) $options['max_tokens'];
85 }
86 return (int) $this->config['max_tokens'];
87 }
88
89 /**
90 * Perform a JSON POST and return a normalised result.
91 *
92 * @param string $url
93 * @param array $headers
94 * @param array $body Encoded with wp_json_encode.
95 * @param int $timeout
96 * @return array {
97 * @type bool $ok
98 * @type int $status
99 * @type array|mixed $data Decoded JSON body (assoc array) on success.
100 * @type string|null $error
101 * }
102 */
103 protected function post_json( string $url, array $headers, array $body, int $timeout = 60 ): array {
104 $headers = wp_parse_args( $headers, [ 'Content-Type' => 'application/json' ] );
105
106 $response = wp_remote_post( $url, [
107 'headers' => $headers,
108 'body' => wp_json_encode( $body ),
109 'timeout' => $timeout,
110 ] );
111
112 if ( is_wp_error( $response ) ) {
113 return [ 'ok' => false, 'status' => 0, 'data' => null, 'error' => $response->get_error_message() ];
114 }
115
116 $status = (int) wp_remote_retrieve_response_code( $response );
117 $raw = wp_remote_retrieve_body( $response );
118 $data = json_decode( $raw, true );
119
120 if ( $status < 200 || $status >= 300 ) {
121 return [
122 'ok' => false,
123 'status' => $status,
124 'data' => $data,
125 'error' => self::extract_error_message( $data, $status ),
126 ];
127 }
128
129 return [ 'ok' => true, 'status' => $status, 'data' => $data, 'error' => null ];
130 }
131
132 /**
133 * Best-effort extraction of an API error message from a decoded body.
134 *
135 * @param mixed $data
136 */
137 protected static function extract_error_message( $data, int $status ): string {
138 if ( is_array( $data ) ) {
139 if ( isset( $data['error']['message'] ) ) {
140 return (string) $data['error']['message'];
141 }
142 if ( isset( $data['error'] ) && is_string( $data['error'] ) ) {
143 return $data['error'];
144 }
145 if ( isset( $data['message'] ) && is_string( $data['message'] ) ) {
146 return $data['message'];
147 }
148 }
149 /* translators: %d: HTTP status code. */
150 return sprintf( __( 'AI provider request failed (HTTP %d).', 'better-payment' ), $status );
151 }
152
153 /**
154 * Convert a provider-agnostic tool param map (OperationRegistry shape) into a
155 * JSON-Schema object, shared by all function/tool-calling providers.
156 *
157 * @param array $params [ key => [ 'type' => ..., 'required' => bool, 'summary' => ... ] ]
158 * @return array{type: string, properties: array, required: array}
159 */
160 protected static function params_to_json_schema( array $params ): array {
161 $properties = [];
162 $required = [];
163
164 foreach ( $params as $key => $spec ) {
165 $type = $spec['type'] ?? 'string';
166 $prop = [ 'description' => $spec['summary'] ?? '' ];
167
168 switch ( $type ) {
169 case 'array':
170 $prop['type'] = 'array';
171 $prop['items'] = [ 'type' => 'object' ];
172 break;
173 case 'object':
174 $prop['type'] = 'object';
175 break;
176 case 'integer':
177 $prop['type'] = 'integer';
178 break;
179 case 'number':
180 $prop['type'] = 'number';
181 break;
182 case 'mixed':
183 // Leave type open — accept any JSON value.
184 break;
185 default:
186 $prop['type'] = 'string';
187 }
188
189 $properties[ $key ] = $prop;
190 if ( ! empty( $spec['required'] ) ) {
191 $required[] = $key;
192 }
193 }
194
195 return [
196 'type' => 'object',
197 'properties' => empty( $properties ) ? (object) [] : $properties,
198 'required' => $required,
199 ];
200 }
201
202 /**
203 * Decode a tool-call arguments payload that may arrive as a JSON string or array.
204 *
205 * @param mixed $arguments
206 * @return array
207 */
208 protected static function decode_arguments( $arguments ): array {
209 if ( is_array( $arguments ) ) {
210 return $arguments;
211 }
212 if ( is_string( $arguments ) && '' !== $arguments ) {
213 $decoded = json_decode( $arguments, true );
214 return is_array( $decoded ) ? $decoded : [];
215 }
216 return [];
217 }
218
219 /**
220 * Split normalised messages into a system prompt + non-system turns.
221 * Providers that carry the system prompt separately use this.
222 *
223 * @param array $messages
224 * @param string $system_option Explicit system option (takes precedence).
225 * @return array{0: string, 1: array} [ system, turns ]
226 */
227 protected static function split_system( array $messages, string $system_option = '' ): array {
228 $system = $system_option;
229 $turns = [];
230 foreach ( $messages as $message ) {
231 if ( ! is_array( $message ) ) {
232 continue;
233 }
234 $role = $message['role'] ?? 'user';
235 if ( 'system' === $role ) {
236 $system = trim( $system . "\n" . (string) ( $message['content'] ?? '' ) );
237 continue;
238 }
239 $turns[] = [
240 'role' => 'assistant' === $role ? 'assistant' : 'user',
241 'content' => (string) ( $message['content'] ?? '' ),
242 ];
243 }
244 return [ trim( $system ), $turns ];
245 }
246 }
247