PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.3.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.3.4
3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / replicate.php
ai-engine / classes / engines Last commit date
anthropic.php 5 months ago chatml.php 5 months ago core.php 7 months ago factory.php 8 months ago google.php 5 months ago mistral.php 5 months ago open-router.php 5 months ago openai.php 5 months ago perplexity.php 6 months ago replicate.php 5 months ago
replicate.php
897 lines
1 <?php
2
3 class Meow_MWAI_Engines_Replicate extends Meow_MWAI_Engines_Core {
4 // Base (Replicate)
5 protected $apiKey = null;
6 protected $organizationId = null;
7
8 // Response
9 protected $inModel = null;
10 protected $inId = null;
11
12 // Streaming
13 protected $streamFunctionCall = null;
14 protected $streamToolCalls = [];
15 protected $streamLastMessage = null;
16 protected $streamImageIds = [];
17 protected $streamInTokens = null;
18 protected $streamOutTokens = null;
19
20 public function __construct( $core, $env ) {
21 parent::__construct( $core, $env );
22 $this->set_environment();
23 }
24
25 public function reset_stream() {
26 $this->streamContent = null;
27 $this->streamBuffer = null;
28 $this->streamFunctionCall = null;
29 $this->streamToolCalls = [];
30 $this->streamLastMessage = null;
31 $this->streamInTokens = null;
32 $this->streamOutTokens = null;
33 $this->inModel = null;
34 $this->inId = null;
35 }
36
37 protected function set_environment() {
38 $env = $this->env;
39 $this->apiKey = $env['apikey'];
40 }
41
42 protected function build_messages( $query ) {
43 $messages = [];
44
45 // First, we need to add the first message (the instructions).
46 if ( !empty( $query->instructions ) ) {
47 $messages[] = [ 'role' => 'system', 'content' => $query->instructions ];
48 }
49
50 // Then, if any, we need to add the 'messages', they are already formatted.
51 foreach ( $query->messages as $message ) {
52 $messages[] = $message;
53 }
54
55 // If there is a context, we need to add it with proper framing.
56 if ( !empty( $query->context ) ) {
57 $framedContext = $this->core->frame_context( $query->context );
58 $messages[] = [ 'role' => 'system', 'content' => $framedContext ];
59 }
60
61 // Finally, we need to add the message, but if there is an image, we need to add it as a system message.
62 $attachments = method_exists( $query, 'getAttachments' ) ? $query->getAttachments() : [];
63 if ( !empty( $attachments ) ) {
64 // Get first image attachment
65 $imageFile = null;
66 foreach ( $attachments as $file ) {
67 $mimeType = $file->get_mimeType() ?? '';
68 if ( strpos( $mimeType, 'image/' ) === 0 ) {
69 $imageFile = $file;
70 break;
71 }
72 }
73
74 if ( $imageFile ) {
75 $finalUrl = $query->image_remote_upload
76 ? $imageFile->get_url()
77 : $imageFile->get_inline_base64_url();
78 $messages[] = [
79 'role' => 'user',
80 'content' => [
81 [
82 'type' => 'text',
83 'text' => $query->get_message()
84 ],
85 [
86 'type' => 'image_url',
87 'image_url' => [
88 'url' => $finalUrl
89 ]
90 ]
91 ]
92 ];
93 }
94 else {
95 $messages[] = [ 'role' => 'user', 'content' => $query->get_message() ];
96 }
97 }
98 else {
99 $messages[] = [ 'role' => 'user', 'content' => $query->get_message() ];
100 }
101
102 return $messages;
103 }
104
105 protected function build_body( $query, $streamCallback = null, $extra = null ) {
106 if ( $query instanceof Meow_MWAI_Query_Text ) {
107 $body = [
108 'model' => $query->model,
109 'stream' => !is_null( $streamCallback ),
110 ];
111
112 if ( !empty( $query->maxTokens ) ) {
113 $body['max_tokens'] = $query->maxTokens;
114 }
115
116 if ( !empty( $query->temperature ) ) {
117 $body['temperature'] = $query->temperature;
118 }
119
120 if ( !empty( $query->maxResults ) ) {
121 $body['n'] = $query->maxResults;
122 }
123
124 if ( !empty( $query->stop ) ) {
125 $body['stop'] = $query->stop;
126 }
127
128 if ( !empty( $query->responseFormat ) ) {
129 if ( $query->responseFormat === 'json' ) {
130 $body['response_format'] = [ 'type' => 'json_object' ];
131 }
132 }
133
134 // Usage Data (only for Replicate)
135 // https://cookbook.openai.com/examples/how_to_stream_completions#4-how-to-get-token-usage-data-for-streamed-chat-completion-response
136 if ( !empty( $streamCallback ) && $this->envType === 'openai' ) {
137 $body['stream_options'] = [
138 'include_usage' => true,
139 ];
140 }
141
142 if ( !empty( $query->functions ) ) {
143 $model = $this->retrieve_model_info( $query->model );
144 if ( !empty( $model['tags'] ) && !in_array( 'functions', $model['tags'] ) ) {
145 Meow_MWAI_Logging::warn( 'The model ' . $query->model . ' doesn\'t support Function Calling.' );
146 }
147 else if ( strpos( $query->model, 'ft:' ) === 0 ) {
148 Meow_MWAI_Logging::warn( 'Replicate doesn\'t support Function Calling with fine-tuned models yet.' );
149 }
150 else {
151 $body['tools'] = [];
152 // Dynamic function: they will interactively enhance the completion (tools).
153 foreach ( $query->functions as $function ) {
154 $body['tools'][] = [
155 'type' => 'function',
156 'function' => $function->serializeForReplicate()
157 ];
158 }
159 // Static functions: they will be executed at the end of the completion.
160 //$body['function_call'] = $query->functionCall;
161 }
162 }
163
164 $body['messages'] = $this->build_messages( $query );
165
166 // Add the feedback if it's a feedback query.
167 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
168 if ( !empty( $query->blocks ) ) {
169 foreach ( $query->blocks as $feedback_block ) {
170 $body['messages'][] = $feedback_block['rawMessage'];
171 foreach ( $feedback_block['feedbacks'] as $feedback ) {
172 $body['messages'][] = [
173 'tool_call_id' => $feedback['request']['toolId'],
174 'role' => 'tool',
175 'name' => $feedback['request']['name'],
176 'content' => $feedback['reply']['value']
177 ];
178 }
179 }
180 }
181 return $body;
182 }
183
184 return $body;
185 }
186 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
187 $body = [
188 'prompt' => $query->message,
189 'model' => $query->model,
190 'response_format' => 'text',
191 'file' => basename( $query->url ),
192 'data' => $extra
193 ];
194 return $body;
195 }
196 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
197 $body = [ 'input' => $query->message, 'model' => $query->model ];
198 if ( $this->envType === 'azure' ) {
199 $body = [ 'input' => $query->message ];
200 }
201 // Dimensions are only supported by v3 models
202 if ( !empty( $query->dimensions ) && strpos( $query->model, 'ada-002' ) === false ) {
203 $body['dimensions'] = $query->dimensions;
204 }
205 return $body;
206 }
207 else if ( $query instanceof Meow_MWAI_Query_Image ) {
208 $model = $query->model;
209 $modelInfo = $this->retrieve_model_info( $model );
210 $body = [ 'input' => [] ];
211
212 if ( isset( $modelInfo['version'] ) ) {
213 $body['version'] = $modelInfo['version'];
214 }
215
216 // From Replicate:
217 // Files should be passed as HTTP URLs or data URLs.
218
219 if ( $query->feature === 'text-to-image' ) {
220
221 // This works with Flux
222 // The model name starts with black-forest-labs/
223 if ( strpos( $model, 'black-forest-labs/' ) === 0 ) {
224 $body['input']['steps'] = 25;
225 $body['input']['prompt'] = $query->message;
226 $body['input']['safety_tolerance'] = 5;
227 if ( !empty( $query->resolution ) ) {
228 $body['input']['aspect_ratio'] = $query->resolution;
229 $body['input']['output_format'] = 'jpg';
230 $body['input']['output_quality'] = 85;
231 }
232 }
233 else if ( strpos( $model, 'stability-ai/' ) === 0 ) {
234 $body['input']['prompt'] = $query->message;
235 $body['input']['num_inference_steps'] = 25;
236 if ( !empty( $query->resolution ) ) {
237 // $query->resolution is actually a string like 1024x1024
238 $parts = explode( 'x', $query->resolution );
239 $width = intval( $parts[0] );
240 $height = intval( $parts[1] );
241 $body['input']['width'] = $width;
242 $body['input']['height'] = $height;
243 }
244 }
245 else {
246 throw new Exception( 'The model ' . $model . ' is not supported for text-to-image.' );
247 }
248
249 // seed
250 // steps: Number of diffusion steps
251 // guidance: Controls the balance between adherence to the text prompt and image quality/diversity. Higher values make the output more closely match the prompt but may reduce overall image quality. Lower values allow for more creative freedom but might produce results less relevant to the prompt.
252 // interval: Interval is a setting that increases the variance in possible outputs letting the model be a tad more dynamic in what outputs it may produce in terms of composition, color, detail, and prompt interpretation. Setting this value low will ensure strong prompt following with more consistent outputs, setting it higher will produce more dynamic or varied outputs.
253 // aspect_ratio: Aspect ratio for the generated image
254 // safety_tolerance: Safety tolerance, 1 is most strict and 5 is most permissive
255 }
256 return $body;
257 }
258 }
259
260 protected function build_url( $query, $endpoint = null ) {
261 $url = '';
262 $env = $this->env;
263 // This endpoint is basically Replicate or Azure, but in the case this class
264 // is overriden, we can pass the endpoint directly (for OpenRouter or HuggingFace, for example).
265 if ( empty( $endpoint ) ) {
266 $endpoint = apply_filters( 'mwai_replicate_endpoint', 'https://api.replicate.com/v1', $this->env );
267 }
268 // Add the base API to the URL
269 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
270 throw new Exception( 'Not implemented yet.' );
271 return $url;
272 }
273 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
274 throw new Exception( 'Not implemented yet.' );
275 return $url;
276 }
277 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
278 throw new Exception( 'Not implemented yet.' );
279 return $url;
280 }
281 else if ( $query instanceof Meow_MWAI_Query_Image ) {
282 //$url .= trailingslashit( $endpoint ) . 'models/' . $query->model . '/predictions';
283 $url .= trailingslashit( $endpoint ) . 'predictions';
284 return $url;
285 }
286 throw new Exception( 'The query is not supported by build_url().' );
287 }
288
289 protected function build_headers( $query ) {
290 if ( $query->apiKey ) {
291 $this->apiKey = $query->apiKey;
292 }
293 if ( empty( $this->apiKey ) ) {
294 throw new Exception( 'No API Key provided. Please visit the Settings. (Replicate Engine)' );
295 }
296 $headers = [
297 'Content-Type' => 'application/json',
298 'Authorization' => 'Bearer ' . $this->apiKey,
299 ];
300 if ( $this->organizationId ) {
301 $headers['Replicate-Organization'] = $this->organizationId;
302 }
303 if ( $this->envType === 'azure' ) {
304 $headers = [ 'Content-Type' => 'application/json', 'api-key' => $this->apiKey ];
305 }
306 return $headers;
307 }
308
309 protected function build_options( $headers, $json = null, $forms = null, $method = 'POST' ) {
310 $body = null;
311 if ( !empty( $forms ) ) {
312 $boundary = wp_generate_password( 24, false );
313 $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
314 $body = $this->build_form_body( $forms, $boundary );
315 }
316 else if ( !empty( $json ) ) {
317 $body = $this->safe_json_encode( $json, 'request body' );
318 }
319 $options = [
320 'headers' => $headers,
321 'method' => $method,
322 'timeout' => MWAI_TIMEOUT,
323 'body' => $body,
324 'sslverify' => MWAI_SSL_VERIFY
325 ];
326 return $options;
327 }
328
329 public function run_query( $url, $options, $isStream = false ) {
330 try {
331 $options['stream'] = $isStream;
332 if ( $isStream ) {
333 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
334 }
335 $res = wp_remote_get( $url, $options );
336
337 if ( is_wp_error( $res ) ) {
338 throw new Exception( $res->get_error_message() );
339 }
340
341 $responseCode = wp_remote_retrieve_response_code( $res );
342 if ( $responseCode === 404 ) {
343 throw new Exception( 'The model\'s API URL was not found: ' . $url );
344 }
345 if ( $responseCode === 400 ) {
346 $message = wp_remote_retrieve_body( $res );
347 if ( empty( $message ) ) {
348 $message = wp_remote_retrieve_response_message( $res );
349 }
350 if ( empty( $message ) ) {
351 $message = 'Bad Request';
352 }
353 throw new Exception( $message );
354 }
355
356 if ( $isStream ) {
357 return [ 'stream' => true ];
358 }
359
360 $response = wp_remote_retrieve_body( $res );
361 $headersRes = wp_remote_retrieve_headers( $res );
362 $headers = $headersRes->getAll();
363
364 // Check if Content-Type is 'multipart/form-data' or 'text/plain'
365 // If so, we don't need to decode the response
366 $normalizedHeaders = array_change_key_case( $headers, CASE_LOWER );
367 $resContentType = $normalizedHeaders['content-type'] ?? '';
368 if ( strpos( $resContentType, 'multipart/form-data' ) !== false || strpos( $resContentType, 'text/plain' ) !== false ) {
369 return [ 'stream' => false, 'headers' => $headers, 'data' => $response ];
370 }
371
372 $data = json_decode( $response, true );
373 $this->handle_response_errors( $data );
374 return [ 'headers' => $headers, 'data' => $data ];
375 }
376 catch ( Exception $e ) {
377 Meow_MWAI_Logging::error( 'Replicate: ' . $e->getMessage() );
378 throw $e;
379 }
380 finally {
381 if ( $isStream && file_exists( $options['filename'] ) ) {
382 unlink( $options['filename'] );
383 }
384 }
385 }
386
387 public function try_decode_error( $data ) {
388 $json = json_decode( $data, true );
389 if ( isset( $json['error']['message'] ) ) {
390 return $json['error']['message'];
391 }
392 return null;
393 }
394
395 public function run_completion_query( $query, $streamCallback = null ): Meow_MWAI_Reply {
396 $isStreaming = !is_null( $streamCallback );
397
398 if ( $isStreaming ) {
399 $this->streamCallback = $streamCallback;
400 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
401 }
402
403 $this->reset_stream();
404 $body = $this->build_body( $query, $streamCallback );
405 $url = $this->build_url( $query );
406 $headers = $this->build_headers( $query );
407 $options = $this->build_options( $headers, $body );
408
409 try {
410 $res = $this->run_query( $url, $options, $streamCallback );
411 $reply = new Meow_MWAI_Reply( $query );
412
413 $returned_id = null;
414 $returned_model = $this->inModel;
415 $returned_in_tokens = null;
416 $returned_out_tokens = null;
417 $returned_price = null;
418 $returned_choices = [];
419
420 // Streaming Mode
421 if ( $isStreaming ) {
422 if ( empty( $this->streamContent ) ) {
423 $error = $this->try_decode_error( $this->streamBuffer );
424 if ( !is_null( $error ) ) {
425 throw new Exception( $error );
426 }
427 }
428 $returned_id = $this->inId;
429 $returned_model = $this->inModel ? $this->inModel : $query->model;
430 $message = [ 'role' => 'assistant', 'content' => $this->streamContent ];
431 if ( !empty( $this->streamFunctionCall ) ) {
432 $message['function_call'] = $this->streamFunctionCall;
433 }
434 if ( !empty( $this->streamToolCalls ) ) {
435 $message['tool_calls'] = $this->streamToolCalls;
436 }
437 if ( !is_null( $this->streamInTokens ) ) {
438 $returned_in_tokens = $this->streamInTokens;
439 }
440 if ( !is_null( $this->streamOutTokens ) ) {
441 $returned_out_tokens = $this->streamOutTokens;
442 }
443 $returned_choices = [ [ 'message' => $message ] ];
444 }
445 // Standard Mode
446 else {
447 $data = $res['data'];
448 if ( empty( $data ) ) {
449 throw new Exception( 'No content received (res is null).' );
450 }
451 if ( !$data['model'] ) {
452 Meow_MWAI_Logging::error( 'Replicate: Invalid response (no model information).' );
453 Meow_MWAI_Logging::error( print_r( $data, 1 ) );
454 throw new Exception( 'Invalid response (no model information).' );
455 }
456 $returned_id = $data['id'];
457 $returned_model = $data['model'];
458 $returned_in_tokens = isset( $data['usage']['prompt_tokens'] ) ?
459 $data['usage']['prompt_tokens'] : null;
460 $returned_out_tokens = isset( $data['usage']['completion_tokens'] ) ?
461 $data['usage']['completion_tokens'] : null;
462 $returned_price = isset( $data['usage']['total_cost'] ) ?
463 $data['usage']['total_cost'] : null;
464 $returned_choices = $data['choices'];
465 }
466
467 // Set the results.
468 $reply->set_choices( $returned_choices );
469 if ( !empty( $returned_id ) ) {
470 $reply->set_id( $returned_id );
471 }
472 if ( !empty( $returned_id ) ) {
473 $reply->set_id( $returned_id );
474 }
475
476 return $reply;
477 }
478 catch ( Exception $e ) {
479 Meow_MWAI_Logging::error( 'Replicate: ' . $e->getMessage() );
480 $message = 'From Replicate: ' . $e->getMessage();
481 throw new Exception( $message );
482 }
483 finally {
484 if ( !is_null( $streamCallback ) ) {
485 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
486 }
487 }
488 }
489
490 // TODO: We should find a way to add text-to-image somewhere in this query
491 public function run_image_query( $query, $streamCallback = null ) {
492 $body = $this->build_body( $query );
493 $url = $this->build_url( $query );
494 $headers = $this->build_headers( $query );
495 $options = $this->build_options( $headers, $body );
496
497 try {
498 $res = $this->run_query( $url, $options );
499 $data = $res['data'];
500 if ( $data['status'] === 422 ) {
501 if ( isset( $data['title'] ) && isset( $data['detail'] ) ) {
502 throw new Exception( $data['title'] . ': ' . $data['detail'] );
503 }
504 throw new Exception( 'The image generation failed.' );
505 }
506 $getUrl = $data['urls']['get'];
507 $status = $data['status'];
508 while ( $status === 'processing' || $status === 'starting' ) {
509 sleep( 1 );
510 $data = $this->execute( 'GET', $getUrl, null, null, true, null, null, true );
511 $status = $data['status'];
512 }
513 if ( $status !== 'succeeded' ) {
514 // if $data has title and detail, we can use them to throw a more detailed error
515 if ( isset( $data['title'] ) && isset( $data['detail'] ) ) {
516 throw new Exception( $data['title'] . ': ' . $data['detail'] );
517 }
518 throw new Exception( 'The image generation failed.' );
519 }
520 $choices = [];
521 $output = isset( $data['output'] ) ? $data['output'] : [];
522 // Flux Schnell returns an array of urls in 'output'
523 if ( is_array( $output ) ) {
524 foreach ( $output as $item ) {
525 $choices[] = [ 'url' => $item ];
526 }
527 }
528 // Flux Schnell returns 'url' in 'output'
529 else if ( is_string( $output ) ) {
530 $choices[] = [ 'url' => $output ];
531 }
532 if ( empty( $choices ) ) {
533 throw new Exception( 'No output URL received.' );
534 }
535 $reply = new Meow_MWAI_Reply( $query );
536 $model = $query->model;
537 $resolution = null;
538 if ( isset( $data['metrics']['width'] ) && isset( $data['metrics']['height'] ) ) {
539 $resolution = $data['metrics']['width'] . 'x' . $data['metrics']['height'];
540 }
541 else {
542 $raw_resolution = Meow_MWAI_Core::get_image_resolution( $choices[0]['url'] );
543 if ( !empty( $raw_resolution ) ) {
544 $resolution = $raw_resolution['width'] . 'x' . $raw_resolution['height'];
545 }
546
547 }
548 if ( !empty( $resolution ) ) {
549 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
550 $reply->set_usage( $usage );
551 }
552
553 $reply->set_choices( $choices );
554 $reply->set_type( 'images' );
555
556 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
557 foreach ( $reply->results as &$result ) {
558 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
559 'query_envId' => $query->envId,
560 'query_session' => $query->session,
561 'query_model' => $query->model,
562 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
563 $fileUrl = $this->core->files->get_url( $fileId );
564 $result = $fileUrl;
565 }
566 }
567 $reply->result = $reply->results[0];
568 return $reply;
569 }
570 catch ( Exception $e ) {
571 Meow_MWAI_Logging::error( 'Replicate: ' . $e->getMessage() );
572 throw new Exception( 'From Replicate: ' . $e->getMessage() );
573 }
574 }
575
576 /*
577 This is the rest of the Replicate API support, not related to the models directly.
578 */
579
580 // Check if there are errors in the response from Replicate, and throw an exception if so.
581 protected function handle_response_errors( $data ) {
582 if ( isset( $data['error'] ) && !empty( $data['error'] ) ) {
583 $message = $data['error'];
584 throw new Exception( $message );
585 }
586 }
587
588 /**
589 * Build the body of a form request.
590 * If the field name is 'file', then the field value is the filename of the file to upload.
591 * The file contents are taken from the 'data' field.
592 *
593 * @param array $fields
594 * @param string $boundary
595 * @return string
596 */
597 public function build_form_body( $fields, $boundary ) {
598 $body = '';
599 foreach ( $fields as $name => $value ) {
600 if ( $name == 'data' ) {
601 continue;
602 }
603 $body .= "--$boundary\r\n";
604 $body .= "Content-Disposition: form-data; name=\"$name\"";
605 if ( $name == 'file' ) {
606 $body .= "; filename=\"{$value}\"\r\n";
607 $body .= "Content-Type: application/json\r\n\r\n";
608 $body .= $fields['data'] . "\r\n";
609 }
610 else {
611 $body .= "\r\n\r\n$value\r\n";
612 }
613 }
614 $body .= "--$boundary--\r\n";
615 return $body;
616 }
617
618 /**
619 * Run a request to the Replicate API.
620 * Fore more information about the $formFields, refer to the build_form_body method.
621 *
622 * @param string $method POST, PUT, GET, DELETE...
623 * @param string $url The API endpoint
624 * @param array $query The query parameters (json)
625 * @param array $formFields The form fields (multipart/form-data)
626 * @param bool $json Whether to return the response as json or not
627 * @return array
628 */
629 public function execute(
630 $method,
631 $url,
632 $query = null,
633 $formFields = null,
634 $json = true,
635 $extraHeaders = null,
636 $streamCallback = null,
637 $overrideUrl = false
638 ) {
639 $headers = "Content-Type: application/json\r\n" . 'Authorization: Bearer ' . $this->apiKey . "\r\n";
640 $body = $query ? $this->safe_json_encode( $query, 'query body' ) : null;
641 if ( !empty( $formFields ) ) {
642 $boundary = wp_generate_password( 24, false );
643 $headers = [
644 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
645 'Authorization' => 'Bearer ' . $this->apiKey
646 ];
647 $body = $this->build_form_body( $formFields, $boundary );
648 }
649
650 // Maybe we should have headers always as an array... not sure why we have it as a string.
651 if ( !empty( $extraHeaders ) ) {
652 foreach ( $extraHeaders as $key => $value ) {
653 if ( is_array( $headers ) ) {
654 $headers[$key] = $value;
655 }
656 else {
657 $headers .= "$key: $value\r\n";
658 }
659 }
660 }
661
662 // If it's a GET, body should be null, and we should append the query to the URL.
663 if ( $method === 'GET' ) {
664 if ( !empty( $query ) ) {
665 $url .= '?' . http_build_query( $query );
666 }
667 $body = null;
668 }
669
670 $url = $overrideUrl ? $url : ( 'https://api.replicate.com/v1' . $url );
671 $options = [
672 'headers' => $headers,
673 'method' => $method,
674 'timeout' => MWAI_TIMEOUT,
675 'body' => $body,
676 'sslverify' => MWAI_SSL_VERIFY
677 ];
678
679 try {
680 if ( !is_null( $streamCallback ) ) {
681 $options['stream'] = true;
682 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
683 // The stream handler calls the streamCallback every time there is content
684 // TODO: For assistants, we should probably have a different stream handler to
685 // handle the assistant's specific reply and perform the necessary actions.
686 $this->streamCallback = $streamCallback;
687 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
688 }
689 $res = wp_remote_request( $url, $options );
690 if ( is_wp_error( $res ) ) {
691 throw new Exception( $res->get_error_message() );
692 }
693 $res = wp_remote_retrieve_body( $res );
694 $data = $json ? json_decode( $res, true ) : $res;
695 $this->handle_response_errors( $data );
696 return $data;
697 }
698 catch ( Exception $e ) {
699 Meow_MWAI_Logging::error( 'Replicate: ' . $e->getMessage() );
700 throw new Exception( $e->getMessage() );
701 //throw new Exception( 'From Replicate: ' . $e->getMessage() );
702 }
703 finally {
704 if ( !is_null( $streamCallback ) ) {
705 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
706 }
707 if ( !empty( $options['stream'] ) && file_exists( $options['filename'] ) ) {
708 unlink( $options['filename'] );
709 }
710 }
711 }
712
713 public function get_models() {
714 return $this->core->get_engine_models( 'replicate' );
715 }
716
717 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
718 return null;
719 }
720
721 public function generate_resolutions( $widths, $heights ) {
722 $resolutions = [];
723 $acceptable_ratios = [
724 '1:1' => [ 'name' => 'Square', 'ratio' => 1 ],
725 '16:9' => [ 'name' => 'Widescreen', 'ratio' => 16 / 9 ],
726 '2:3' => [ 'name' => 'Portrait', 'ratio' => 2 / 3 ],
727 '3:2' => [ 'name' => 'Landscape', 'ratio' => 3 / 2 ],
728 '4:5' => [ 'name' => 'Portrait', 'ratio' => 4 / 5 ],
729 '5:4' => [ 'name' => 'Landscape', 'ratio' => 5 / 4 ],
730 '9:16' => [ 'name' => 'Story', 'ratio' => 9 / 16 ]
731 ];
732
733 foreach ( $widths as $width ) {
734 foreach ( $heights as $height ) {
735 if ( $height <= 1024 && $width <= 1536 && $height >= 64 && $width >= 64 ) {
736 $ratio = $width / $height;
737 $ratio_name = null;
738
739 foreach ( $acceptable_ratios as $key => $ratio_info ) {
740 if ( abs( $ratio - $ratio_info['ratio'] ) < 0.01 ) {
741 $ratio_name = $key;
742 $ratio_label = $ratio_info['name'];
743 break;
744 }
745 }
746
747 if ( $ratio_name ) {
748 $label = "{$ratio_label} ({$ratio_name}): {$width}x{$height}";
749 $resolutions[] = [
750 'name' => "{$width}x{$height}",
751 'label' => $label
752 ];
753 }
754 }
755 }
756 }
757
758 // Sort resolutions by total pixel count
759 usort( $resolutions, function ( $a, $b ) {
760 $aPixels = explode( 'x', $a['name'] );
761 $bPixels = explode( 'x', $b['name'] );
762 return ( $aPixels[0] * $aPixels[1] ) - ( $bPixels[0] * $bPixels[1] );
763 } );
764
765 return $resolutions;
766 }
767
768 public function retrieve_models() {
769 return $this->retrieve_recommended_models();
770 }
771
772 public function retrieve_recommended_models() {
773 $collections = [ 'flux', 'text-to-image' ];
774 $allowed_owners = [ 'black-forest-labs', 'stability-ai' ];
775 $rawModels = $this->_retrieve_models( $collections, $allowed_owners );
776 $models = $this->_process_raw_models( $rawModels );
777 return $models;
778 }
779
780 public function retrieve_all_models() {
781 $allowed_owners = [ 'black-forest-labs', 'stability-ai' ];
782 $rawModels = $this->_retrieve_models( null, $allowed_owners );
783 $models = $this->_process_raw_models( $rawModels );
784 return $models;
785 }
786
787 // Private method to retrieve models, optionally filtered by collections
788 private function _retrieve_models( $collections = null, $allowed_owners = [] ) {
789 $rawModels = [];
790 if ( $collections ) {
791 foreach ( $collections as $collection ) {
792 $next = '/collections/' . $collection;
793 $cursor = null;
794 while ( $next ) {
795 $query_args = $cursor ? [ 'cursor' => $cursor ] : [];
796 $response = $this->execute( 'GET', $next, $query_args );
797 if ( !is_array( $response ) || empty( $response['models'] ) ) {
798 break;
799 }
800 $filtered_results = array_filter( $response['models'], function ( $model ) use ( $allowed_owners ) {
801 $isAllowedOwner = isset( $model['owner'] ) && in_array( $model['owner'], $allowed_owners );
802 $isPublic = isset( $model['visibility'] ) && $model['visibility'] === 'public';
803 return $isAllowedOwner && $isPublic;
804 } );
805 $rawModels = array_merge( $rawModels, $filtered_results );
806 if ( empty( $response['next'] ) ) {
807 break;
808 }
809 $parsed_url = wp_parse_url( $response['next'] );
810 parse_str( $parsed_url['query'] ?? '', $query_params );
811 $cursor = $query_params['cursor'] ?? '';
812 $next = '/collections/' . $collection;
813 }
814 }
815 }
816 else {
817 $next = '/models';
818 $cursor = null;
819 while ( $next ) {
820 $query_args = $cursor ? [ 'cursor' => $cursor ] : [];
821 $response = $this->execute( 'GET', $next, $query_args );
822 if ( !is_array( $response ) || empty( $response['results'] ) ) {
823 break;
824 }
825 $filtered_results = array_filter( $response['results'], function ( $model ) use ( $allowed_owners ) {
826 $isAllowedOwner = isset( $model['owner'] ) && in_array( $model['owner'], $allowed_owners );
827 $isPublic = isset( $model['visibility'] ) && $model['visibility'] === 'public';
828 return $isAllowedOwner && $isPublic;
829 } );
830 $rawModels = array_merge( $rawModels, $filtered_results );
831 if ( empty( $response['next'] ) ) {
832 break;
833 }
834 $parsed_url = wp_parse_url( $response['next'] );
835 parse_str( $parsed_url['query'] ?? '', $query_params );
836 $cursor = $query_params['cursor'] ?? '';
837 $next = '/models';
838 }
839 }
840 return $rawModels;
841 }
842
843 // Private method to process raw models
844 private function _process_raw_models( $rawModels ) {
845 $models = [];
846 foreach ( $rawModels as $rawModel ) {
847 $name = trim( $rawModel['name'] );
848 $family = trim( $rawModel['owner'] );
849 $tags = [ 'image', 'text-to-image' ];
850 $model = $family . '/' . $name;
851 $version = isset( $rawModel['latest_version']['id'] ) ? $rawModel['latest_version']['id'] : null;
852
853 if ( $family === 'stability-ai' ) {
854 $tags[] = 'image-to-image';
855 $tags[] = 'inpainting';
856 }
857
858 $resolutions = [];
859
860 // Black Forest Labs
861 if ( $family === 'black-forest-labs' ) {
862 // These work at least for Flux Pro
863 $resolutions[] = [ 'name' => '1:1', 'label' => 'Square (1:1)' ];
864 $resolutions[] = [ 'name' => '16:9', 'label' => 'Widescreen (16:9)' ];
865 $resolutions[] = [ 'name' => '2:3', 'label' => 'Portrait (2:3)' ];
866 $resolutions[] = [ 'name' => '3:2', 'label' => 'Landscape (3:2)' ];
867 $resolutions[] = [ 'name' => '4:5', 'label' => 'Portrait (4:5)' ];
868 $resolutions[] = [ 'name' => '5:4', 'label' => 'Landscape (5:4)' ];
869 $resolutions[] = [ 'name' => '9:16', 'label' => 'Story (9:16)' ];
870 }
871
872 // Stability AI
873 if ( $family === 'stability-ai' ) {
874 $heights = [ 64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 1152, 1216, 1344, 1536 ];
875 $widths = [ 64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024 ];
876 $resolutions = $this->generate_resolutions( $widths, $heights );
877 }
878
879 $models[] = [
880 'model' => $model,
881 'name' => $name,
882 'family' => $family,
883 'version' => $version,
884 'features' => [ 'text-to-image' ],
885 'price' => null,
886 'type' => 'image',
887 'resolutions' => $resolutions,
888 'unit' => 1 / 1000,
889 'maxCompletionTokens' => null,
890 'maxContextualTokens' => null,
891 'tags' => $tags
892 ];
893 }
894 return $models;
895 }
896 }
897