PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.2.0
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.2.0
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 8 months ago chatml.php 8 months ago core.php 8 months ago factory.php 8 months ago google.php 8 months ago mistral.php 9 months ago open-router.php 11 months ago openai.php 8 months ago perplexity.php 10 months ago replicate.php 8 months ago
replicate.php
887 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.
56 if ( !empty( $query->context ) ) {
57 $messages[] = [ 'role' => 'system', 'content' => $query->context ];
58 }
59
60 // Finally, we need to add the message, but if there is an image, we need to add it as a system message.
61 $attachments = method_exists( $query, 'getAttachments' ) ? $query->getAttachments() : [];
62 if ( !empty( $attachments ) ) {
63 // Get first attachment
64 $file = $attachments[0];
65 $finalUrl = null;
66 if ( $query->image_remote_upload ) {
67 $finalUrl = $file->get_url();
68 }
69 else {
70 $finalUrl = $file->get_inline_base64_url();
71 }
72 $messages[] = [
73 'role' => 'user',
74 'content' => [
75 [
76 'type' => 'text',
77 'text' => $query->get_message()
78 ],
79 [
80 'type' => 'image_url',
81 'image_url' => [
82 'url' => $finalUrl
83 ]
84 ]
85 ]
86 ];
87 }
88 else {
89 $messages[] = [ 'role' => 'user', 'content' => $query->get_message() ];
90 }
91
92 return $messages;
93 }
94
95 protected function build_body( $query, $streamCallback = null, $extra = null ) {
96 if ( $query instanceof Meow_MWAI_Query_Text ) {
97 $body = [
98 'model' => $query->model,
99 'stream' => !is_null( $streamCallback ),
100 ];
101
102 if ( !empty( $query->maxTokens ) ) {
103 $body['max_tokens'] = $query->maxTokens;
104 }
105
106 if ( !empty( $query->temperature ) ) {
107 $body['temperature'] = $query->temperature;
108 }
109
110 if ( !empty( $query->maxResults ) ) {
111 $body['n'] = $query->maxResults;
112 }
113
114 if ( !empty( $query->stop ) ) {
115 $body['stop'] = $query->stop;
116 }
117
118 if ( !empty( $query->responseFormat ) ) {
119 if ( $query->responseFormat === 'json' ) {
120 $body['response_format'] = [ 'type' => 'json_object' ];
121 }
122 }
123
124 // Usage Data (only for Replicate)
125 // https://cookbook.openai.com/examples/how_to_stream_completions#4-how-to-get-token-usage-data-for-streamed-chat-completion-response
126 if ( !empty( $streamCallback ) && $this->envType === 'openai' ) {
127 $body['stream_options'] = [
128 'include_usage' => true,
129 ];
130 }
131
132 if ( !empty( $query->functions ) ) {
133 $model = $this->retrieve_model_info( $query->model );
134 if ( !empty( $model['tags'] ) && !in_array( 'functions', $model['tags'] ) ) {
135 Meow_MWAI_Logging::warn( 'The model ' . $query->model . ' doesn\'t support Function Calling.' );
136 }
137 else if ( strpos( $query->model, 'ft:' ) === 0 ) {
138 Meow_MWAI_Logging::warn( 'Replicate doesn\'t support Function Calling with fine-tuned models yet.' );
139 }
140 else {
141 $body['tools'] = [];
142 // Dynamic function: they will interactively enhance the completion (tools).
143 foreach ( $query->functions as $function ) {
144 $body['tools'][] = [
145 'type' => 'function',
146 'function' => $function->serializeForReplicate()
147 ];
148 }
149 // Static functions: they will be executed at the end of the completion.
150 //$body['function_call'] = $query->functionCall;
151 }
152 }
153
154 $body['messages'] = $this->build_messages( $query );
155
156 // Add the feedback if it's a feedback query.
157 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
158 if ( !empty( $query->blocks ) ) {
159 foreach ( $query->blocks as $feedback_block ) {
160 $body['messages'][] = $feedback_block['rawMessage'];
161 foreach ( $feedback_block['feedbacks'] as $feedback ) {
162 $body['messages'][] = [
163 'tool_call_id' => $feedback['request']['toolId'],
164 'role' => 'tool',
165 'name' => $feedback['request']['name'],
166 'content' => $feedback['reply']['value']
167 ];
168 }
169 }
170 }
171 return $body;
172 }
173
174 return $body;
175 }
176 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
177 $body = [
178 'prompt' => $query->message,
179 'model' => $query->model,
180 'response_format' => 'text',
181 'file' => basename( $query->url ),
182 'data' => $extra
183 ];
184 return $body;
185 }
186 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
187 $body = [ 'input' => $query->message, 'model' => $query->model ];
188 if ( $this->envType === 'azure' ) {
189 $body = [ 'input' => $query->message ];
190 }
191 // Dimensions are only supported by v3 models
192 if ( !empty( $query->dimensions ) && strpos( $query->model, 'ada-002' ) === false ) {
193 $body['dimensions'] = $query->dimensions;
194 }
195 return $body;
196 }
197 else if ( $query instanceof Meow_MWAI_Query_Image ) {
198 $model = $query->model;
199 $modelInfo = $this->retrieve_model_info( $model );
200 $body = [ 'input' => [] ];
201
202 if ( isset( $modelInfo['version'] ) ) {
203 $body['version'] = $modelInfo['version'];
204 }
205
206 // From Replicate:
207 // Files should be passed as HTTP URLs or data URLs.
208
209 if ( $query->feature === 'text-to-image' ) {
210
211 // This works with Flux
212 // The model name starts with black-forest-labs/
213 if ( strpos( $model, 'black-forest-labs/' ) === 0 ) {
214 $body['input']['steps'] = 25;
215 $body['input']['prompt'] = $query->message;
216 $body['input']['safety_tolerance'] = 5;
217 if ( !empty( $query->resolution ) ) {
218 $body['input']['aspect_ratio'] = $query->resolution;
219 $body['input']['output_format'] = 'jpg';
220 $body['input']['output_quality'] = 85;
221 }
222 }
223 else if ( strpos( $model, 'stability-ai/' ) === 0 ) {
224 $body['input']['prompt'] = $query->message;
225 $body['input']['num_inference_steps'] = 25;
226 if ( !empty( $query->resolution ) ) {
227 // $query->resolution is actually a string like 1024x1024
228 $parts = explode( 'x', $query->resolution );
229 $width = intval( $parts[0] );
230 $height = intval( $parts[1] );
231 $body['input']['width'] = $width;
232 $body['input']['height'] = $height;
233 }
234 }
235 else {
236 throw new Exception( 'The model ' . $model . ' is not supported for text-to-image.' );
237 }
238
239 // seed
240 // steps: Number of diffusion steps
241 // 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.
242 // 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.
243 // aspect_ratio: Aspect ratio for the generated image
244 // safety_tolerance: Safety tolerance, 1 is most strict and 5 is most permissive
245 }
246 return $body;
247 }
248 }
249
250 protected function build_url( $query, $endpoint = null ) {
251 $url = '';
252 $env = $this->env;
253 // This endpoint is basically Replicate or Azure, but in the case this class
254 // is overriden, we can pass the endpoint directly (for OpenRouter or HuggingFace, for example).
255 if ( empty( $endpoint ) ) {
256 $endpoint = apply_filters( 'mwai_replicate_endpoint', 'https://api.replicate.com/v1', $this->env );
257 }
258 // Add the base API to the URL
259 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
260 throw new Exception( 'Not implemented yet.' );
261 return $url;
262 }
263 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
264 throw new Exception( 'Not implemented yet.' );
265 return $url;
266 }
267 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
268 throw new Exception( 'Not implemented yet.' );
269 return $url;
270 }
271 else if ( $query instanceof Meow_MWAI_Query_Image ) {
272 //$url .= trailingslashit( $endpoint ) . 'models/' . $query->model . '/predictions';
273 $url .= trailingslashit( $endpoint ) . 'predictions';
274 return $url;
275 }
276 throw new Exception( 'The query is not supported by build_url().' );
277 }
278
279 protected function build_headers( $query ) {
280 if ( $query->apiKey ) {
281 $this->apiKey = $query->apiKey;
282 }
283 if ( empty( $this->apiKey ) ) {
284 throw new Exception( 'No API Key provided. Please visit the Settings.' );
285 }
286 $headers = [
287 'Content-Type' => 'application/json',
288 'Authorization' => 'Bearer ' . $this->apiKey,
289 ];
290 if ( $this->organizationId ) {
291 $headers['Replicate-Organization'] = $this->organizationId;
292 }
293 if ( $this->envType === 'azure' ) {
294 $headers = [ 'Content-Type' => 'application/json', 'api-key' => $this->apiKey ];
295 }
296 return $headers;
297 }
298
299 protected function build_options( $headers, $json = null, $forms = null, $method = 'POST' ) {
300 $body = null;
301 if ( !empty( $forms ) ) {
302 $boundary = wp_generate_password( 24, false );
303 $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
304 $body = $this->build_form_body( $forms, $boundary );
305 }
306 else if ( !empty( $json ) ) {
307 $body = $this->safe_json_encode( $json, 'request body' );
308 }
309 $options = [
310 'headers' => $headers,
311 'method' => $method,
312 'timeout' => MWAI_TIMEOUT,
313 'body' => $body,
314 'sslverify' => false
315 ];
316 return $options;
317 }
318
319 public function run_query( $url, $options, $isStream = false ) {
320 try {
321 $options['stream'] = $isStream;
322 if ( $isStream ) {
323 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
324 }
325 $res = wp_remote_get( $url, $options );
326
327 if ( is_wp_error( $res ) ) {
328 throw new Exception( $res->get_error_message() );
329 }
330
331 $responseCode = wp_remote_retrieve_response_code( $res );
332 if ( $responseCode === 404 ) {
333 throw new Exception( 'The model\'s API URL was not found: ' . $url );
334 }
335 if ( $responseCode === 400 ) {
336 $message = wp_remote_retrieve_body( $res );
337 if ( empty( $message ) ) {
338 $message = wp_remote_retrieve_response_message( $res );
339 }
340 if ( empty( $message ) ) {
341 $message = 'Bad Request';
342 }
343 throw new Exception( $message );
344 }
345
346 if ( $isStream ) {
347 return [ 'stream' => true ];
348 }
349
350 $response = wp_remote_retrieve_body( $res );
351 $headersRes = wp_remote_retrieve_headers( $res );
352 $headers = $headersRes->getAll();
353
354 // Check if Content-Type is 'multipart/form-data' or 'text/plain'
355 // If so, we don't need to decode the response
356 $normalizedHeaders = array_change_key_case( $headers, CASE_LOWER );
357 $resContentType = $normalizedHeaders['content-type'] ?? '';
358 if ( strpos( $resContentType, 'multipart/form-data' ) !== false || strpos( $resContentType, 'text/plain' ) !== false ) {
359 return [ 'stream' => false, 'headers' => $headers, 'data' => $response ];
360 }
361
362 $data = json_decode( $response, true );
363 $this->handle_response_errors( $data );
364 return [ 'headers' => $headers, 'data' => $data ];
365 }
366 catch ( Exception $e ) {
367 Meow_MWAI_Logging::error( 'Replicate: ' . $e->getMessage() );
368 throw $e;
369 }
370 finally {
371 if ( $isStream && file_exists( $options['filename'] ) ) {
372 unlink( $options['filename'] );
373 }
374 }
375 }
376
377 public function try_decode_error( $data ) {
378 $json = json_decode( $data, true );
379 if ( isset( $json['error']['message'] ) ) {
380 return $json['error']['message'];
381 }
382 return null;
383 }
384
385 public function run_completion_query( $query, $streamCallback = null ): Meow_MWAI_Reply {
386 $isStreaming = !is_null( $streamCallback );
387
388 if ( $isStreaming ) {
389 $this->streamCallback = $streamCallback;
390 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
391 }
392
393 $this->reset_stream();
394 $body = $this->build_body( $query, $streamCallback );
395 $url = $this->build_url( $query );
396 $headers = $this->build_headers( $query );
397 $options = $this->build_options( $headers, $body );
398
399 try {
400 $res = $this->run_query( $url, $options, $streamCallback );
401 $reply = new Meow_MWAI_Reply( $query );
402
403 $returned_id = null;
404 $returned_model = $this->inModel;
405 $returned_in_tokens = null;
406 $returned_out_tokens = null;
407 $returned_price = null;
408 $returned_choices = [];
409
410 // Streaming Mode
411 if ( $isStreaming ) {
412 if ( empty( $this->streamContent ) ) {
413 $error = $this->try_decode_error( $this->streamBuffer );
414 if ( !is_null( $error ) ) {
415 throw new Exception( $error );
416 }
417 }
418 $returned_id = $this->inId;
419 $returned_model = $this->inModel ? $this->inModel : $query->model;
420 $message = [ 'role' => 'assistant', 'content' => $this->streamContent ];
421 if ( !empty( $this->streamFunctionCall ) ) {
422 $message['function_call'] = $this->streamFunctionCall;
423 }
424 if ( !empty( $this->streamToolCalls ) ) {
425 $message['tool_calls'] = $this->streamToolCalls;
426 }
427 if ( !is_null( $this->streamInTokens ) ) {
428 $returned_in_tokens = $this->streamInTokens;
429 }
430 if ( !is_null( $this->streamOutTokens ) ) {
431 $returned_out_tokens = $this->streamOutTokens;
432 }
433 $returned_choices = [ [ 'message' => $message ] ];
434 }
435 // Standard Mode
436 else {
437 $data = $res['data'];
438 if ( empty( $data ) ) {
439 throw new Exception( 'No content received (res is null).' );
440 }
441 if ( !$data['model'] ) {
442 Meow_MWAI_Logging::error( 'Replicate: Invalid response (no model information).' );
443 Meow_MWAI_Logging::error( print_r( $data, 1 ) );
444 throw new Exception( 'Invalid response (no model information).' );
445 }
446 $returned_id = $data['id'];
447 $returned_model = $data['model'];
448 $returned_in_tokens = isset( $data['usage']['prompt_tokens'] ) ?
449 $data['usage']['prompt_tokens'] : null;
450 $returned_out_tokens = isset( $data['usage']['completion_tokens'] ) ?
451 $data['usage']['completion_tokens'] : null;
452 $returned_price = isset( $data['usage']['total_cost'] ) ?
453 $data['usage']['total_cost'] : null;
454 $returned_choices = $data['choices'];
455 }
456
457 // Set the results.
458 $reply->set_choices( $returned_choices );
459 if ( !empty( $returned_id ) ) {
460 $reply->set_id( $returned_id );
461 }
462 if ( !empty( $returned_id ) ) {
463 $reply->set_id( $returned_id );
464 }
465
466 return $reply;
467 }
468 catch ( Exception $e ) {
469 Meow_MWAI_Logging::error( 'Replicate: ' . $e->getMessage() );
470 $message = 'From Replicate: ' . $e->getMessage();
471 throw new Exception( $message );
472 }
473 finally {
474 if ( !is_null( $streamCallback ) ) {
475 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
476 }
477 }
478 }
479
480 // TODO: We should find a way to add text-to-image somewhere in this query
481 public function run_image_query( $query, $streamCallback = null ) {
482 $body = $this->build_body( $query );
483 $url = $this->build_url( $query );
484 $headers = $this->build_headers( $query );
485 $options = $this->build_options( $headers, $body );
486
487 try {
488 $res = $this->run_query( $url, $options );
489 $data = $res['data'];
490 if ( $data['status'] === 422 ) {
491 if ( isset( $data['title'] ) && isset( $data['detail'] ) ) {
492 throw new Exception( $data['title'] . ': ' . $data['detail'] );
493 }
494 throw new Exception( 'The image generation failed.' );
495 }
496 $getUrl = $data['urls']['get'];
497 $status = $data['status'];
498 while ( $status === 'processing' || $status === 'starting' ) {
499 sleep( 1 );
500 $data = $this->execute( 'GET', $getUrl, null, null, true, null, null, true );
501 $status = $data['status'];
502 }
503 if ( $status !== 'succeeded' ) {
504 // if $data has title and detail, we can use them to throw a more detailed error
505 if ( isset( $data['title'] ) && isset( $data['detail'] ) ) {
506 throw new Exception( $data['title'] . ': ' . $data['detail'] );
507 }
508 throw new Exception( 'The image generation failed.' );
509 }
510 $choices = [];
511 $output = isset( $data['output'] ) ? $data['output'] : [];
512 // Flux Schnell returns an array of urls in 'output'
513 if ( is_array( $output ) ) {
514 foreach ( $output as $item ) {
515 $choices[] = [ 'url' => $item ];
516 }
517 }
518 // Flux Schnell returns 'url' in 'output'
519 else if ( is_string( $output ) ) {
520 $choices[] = [ 'url' => $output ];
521 }
522 if ( empty( $choices ) ) {
523 throw new Exception( 'No output URL received.' );
524 }
525 $reply = new Meow_MWAI_Reply( $query );
526 $model = $query->model;
527 $resolution = null;
528 if ( isset( $data['metrics']['width'] ) && isset( $data['metrics']['height'] ) ) {
529 $resolution = $data['metrics']['width'] . 'x' . $data['metrics']['height'];
530 }
531 else {
532 $raw_resolution = Meow_MWAI_Core::get_image_resolution( $choices[0]['url'] );
533 if ( !empty( $raw_resolution ) ) {
534 $resolution = $raw_resolution['width'] . 'x' . $raw_resolution['height'];
535 }
536
537 }
538 if ( !empty( $resolution ) ) {
539 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
540 $reply->set_usage( $usage );
541 }
542
543 $reply->set_choices( $choices );
544 $reply->set_type( 'images' );
545
546 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
547 foreach ( $reply->results as &$result ) {
548 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
549 'query_envId' => $query->envId,
550 'query_session' => $query->session,
551 'query_model' => $query->model,
552 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
553 $fileUrl = $this->core->files->get_url( $fileId );
554 $result = $fileUrl;
555 }
556 }
557 $reply->result = $reply->results[0];
558 return $reply;
559 }
560 catch ( Exception $e ) {
561 Meow_MWAI_Logging::error( 'Replicate: ' . $e->getMessage() );
562 throw new Exception( 'From Replicate: ' . $e->getMessage() );
563 }
564 }
565
566 /*
567 This is the rest of the Replicate API support, not related to the models directly.
568 */
569
570 // Check if there are errors in the response from Replicate, and throw an exception if so.
571 protected function handle_response_errors( $data ) {
572 if ( isset( $data['error'] ) && !empty( $data['error'] ) ) {
573 $message = $data['error'];
574 throw new Exception( $message );
575 }
576 }
577
578 /**
579 * Build the body of a form request.
580 * If the field name is 'file', then the field value is the filename of the file to upload.
581 * The file contents are taken from the 'data' field.
582 *
583 * @param array $fields
584 * @param string $boundary
585 * @return string
586 */
587 public function build_form_body( $fields, $boundary ) {
588 $body = '';
589 foreach ( $fields as $name => $value ) {
590 if ( $name == 'data' ) {
591 continue;
592 }
593 $body .= "--$boundary\r\n";
594 $body .= "Content-Disposition: form-data; name=\"$name\"";
595 if ( $name == 'file' ) {
596 $body .= "; filename=\"{$value}\"\r\n";
597 $body .= "Content-Type: application/json\r\n\r\n";
598 $body .= $fields['data'] . "\r\n";
599 }
600 else {
601 $body .= "\r\n\r\n$value\r\n";
602 }
603 }
604 $body .= "--$boundary--\r\n";
605 return $body;
606 }
607
608 /**
609 * Run a request to the Replicate API.
610 * Fore more information about the $formFields, refer to the build_form_body method.
611 *
612 * @param string $method POST, PUT, GET, DELETE...
613 * @param string $url The API endpoint
614 * @param array $query The query parameters (json)
615 * @param array $formFields The form fields (multipart/form-data)
616 * @param bool $json Whether to return the response as json or not
617 * @return array
618 */
619 public function execute(
620 $method,
621 $url,
622 $query = null,
623 $formFields = null,
624 $json = true,
625 $extraHeaders = null,
626 $streamCallback = null,
627 $overrideUrl = false
628 ) {
629 $headers = "Content-Type: application/json\r\n" . 'Authorization: Bearer ' . $this->apiKey . "\r\n";
630 $body = $query ? $this->safe_json_encode( $query, 'query body' ) : null;
631 if ( !empty( $formFields ) ) {
632 $boundary = wp_generate_password( 24, false );
633 $headers = [
634 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
635 'Authorization' => 'Bearer ' . $this->apiKey
636 ];
637 $body = $this->build_form_body( $formFields, $boundary );
638 }
639
640 // Maybe we should have headers always as an array... not sure why we have it as a string.
641 if ( !empty( $extraHeaders ) ) {
642 foreach ( $extraHeaders as $key => $value ) {
643 if ( is_array( $headers ) ) {
644 $headers[$key] = $value;
645 }
646 else {
647 $headers .= "$key: $value\r\n";
648 }
649 }
650 }
651
652 // If it's a GET, body should be null, and we should append the query to the URL.
653 if ( $method === 'GET' ) {
654 if ( !empty( $query ) ) {
655 $url .= '?' . http_build_query( $query );
656 }
657 $body = null;
658 }
659
660 $url = $overrideUrl ? $url : ( 'https://api.replicate.com/v1' . $url );
661 $options = [
662 'headers' => $headers,
663 'method' => $method,
664 'timeout' => MWAI_TIMEOUT,
665 'body' => $body,
666 'sslverify' => false
667 ];
668
669 try {
670 if ( !is_null( $streamCallback ) ) {
671 $options['stream'] = true;
672 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
673 // The stream handler calls the streamCallback every time there is content
674 // TODO: For assistants, we should probably have a different stream handler to
675 // handle the assistant's specific reply and perform the necessary actions.
676 $this->streamCallback = $streamCallback;
677 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
678 }
679 $res = wp_remote_request( $url, $options );
680 if ( is_wp_error( $res ) ) {
681 throw new Exception( $res->get_error_message() );
682 }
683 $res = wp_remote_retrieve_body( $res );
684 $data = $json ? json_decode( $res, true ) : $res;
685 $this->handle_response_errors( $data );
686 return $data;
687 }
688 catch ( Exception $e ) {
689 Meow_MWAI_Logging::error( 'Replicate: ' . $e->getMessage() );
690 throw new Exception( $e->getMessage() );
691 //throw new Exception( 'From Replicate: ' . $e->getMessage() );
692 }
693 finally {
694 if ( !is_null( $streamCallback ) ) {
695 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
696 }
697 if ( !empty( $options['stream'] ) && file_exists( $options['filename'] ) ) {
698 unlink( $options['filename'] );
699 }
700 }
701 }
702
703 public function get_models() {
704 return $this->core->get_engine_models( 'replicate' );
705 }
706
707 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
708 return null;
709 }
710
711 public function generate_resolutions( $widths, $heights ) {
712 $resolutions = [];
713 $acceptable_ratios = [
714 '1:1' => [ 'name' => 'Square', 'ratio' => 1 ],
715 '16:9' => [ 'name' => 'Widescreen', 'ratio' => 16 / 9 ],
716 '2:3' => [ 'name' => 'Portrait', 'ratio' => 2 / 3 ],
717 '3:2' => [ 'name' => 'Landscape', 'ratio' => 3 / 2 ],
718 '4:5' => [ 'name' => 'Portrait', 'ratio' => 4 / 5 ],
719 '5:4' => [ 'name' => 'Landscape', 'ratio' => 5 / 4 ],
720 '9:16' => [ 'name' => 'Story', 'ratio' => 9 / 16 ]
721 ];
722
723 foreach ( $widths as $width ) {
724 foreach ( $heights as $height ) {
725 if ( $height <= 1024 && $width <= 1536 && $height >= 64 && $width >= 64 ) {
726 $ratio = $width / $height;
727 $ratio_name = null;
728
729 foreach ( $acceptable_ratios as $key => $ratio_info ) {
730 if ( abs( $ratio - $ratio_info['ratio'] ) < 0.01 ) {
731 $ratio_name = $key;
732 $ratio_label = $ratio_info['name'];
733 break;
734 }
735 }
736
737 if ( $ratio_name ) {
738 $label = "{$ratio_label} ({$ratio_name}): {$width}x{$height}";
739 $resolutions[] = [
740 'name' => "{$width}x{$height}",
741 'label' => $label
742 ];
743 }
744 }
745 }
746 }
747
748 // Sort resolutions by total pixel count
749 usort( $resolutions, function ( $a, $b ) {
750 $aPixels = explode( 'x', $a['name'] );
751 $bPixels = explode( 'x', $b['name'] );
752 return ( $aPixels[0] * $aPixels[1] ) - ( $bPixels[0] * $bPixels[1] );
753 } );
754
755 return $resolutions;
756 }
757
758 public function retrieve_models() {
759 return $this->retrieve_recommended_models();
760 }
761
762 public function retrieve_recommended_models() {
763 $collections = [ 'flux', 'text-to-image' ];
764 $allowed_owners = [ 'black-forest-labs', 'stability-ai' ];
765 $rawModels = $this->_retrieve_models( $collections, $allowed_owners );
766 $models = $this->_process_raw_models( $rawModels );
767 return $models;
768 }
769
770 public function retrieve_all_models() {
771 $allowed_owners = [ 'black-forest-labs', 'stability-ai' ];
772 $rawModels = $this->_retrieve_models( null, $allowed_owners );
773 $models = $this->_process_raw_models( $rawModels );
774 return $models;
775 }
776
777 // Private method to retrieve models, optionally filtered by collections
778 private function _retrieve_models( $collections = null, $allowed_owners = [] ) {
779 $rawModels = [];
780 if ( $collections ) {
781 foreach ( $collections as $collection ) {
782 $next = '/collections/' . $collection;
783 $cursor = null;
784 while ( $next ) {
785 $query_args = $cursor ? [ 'cursor' => $cursor ] : [];
786 $response = $this->execute( 'GET', $next, $query_args );
787 if ( !is_array( $response ) || empty( $response['models'] ) ) {
788 break;
789 }
790 $filtered_results = array_filter( $response['models'], function ( $model ) use ( $allowed_owners ) {
791 $isAllowedOwner = isset( $model['owner'] ) && in_array( $model['owner'], $allowed_owners );
792 $isPublic = isset( $model['visibility'] ) && $model['visibility'] === 'public';
793 return $isAllowedOwner && $isPublic;
794 } );
795 $rawModels = array_merge( $rawModels, $filtered_results );
796 if ( empty( $response['next'] ) ) {
797 break;
798 }
799 $parsed_url = wp_parse_url( $response['next'] );
800 parse_str( $parsed_url['query'] ?? '', $query_params );
801 $cursor = $query_params['cursor'] ?? '';
802 $next = '/collections/' . $collection;
803 }
804 }
805 }
806 else {
807 $next = '/models';
808 $cursor = null;
809 while ( $next ) {
810 $query_args = $cursor ? [ 'cursor' => $cursor ] : [];
811 $response = $this->execute( 'GET', $next, $query_args );
812 if ( !is_array( $response ) || empty( $response['results'] ) ) {
813 break;
814 }
815 $filtered_results = array_filter( $response['results'], function ( $model ) use ( $allowed_owners ) {
816 $isAllowedOwner = isset( $model['owner'] ) && in_array( $model['owner'], $allowed_owners );
817 $isPublic = isset( $model['visibility'] ) && $model['visibility'] === 'public';
818 return $isAllowedOwner && $isPublic;
819 } );
820 $rawModels = array_merge( $rawModels, $filtered_results );
821 if ( empty( $response['next'] ) ) {
822 break;
823 }
824 $parsed_url = wp_parse_url( $response['next'] );
825 parse_str( $parsed_url['query'] ?? '', $query_params );
826 $cursor = $query_params['cursor'] ?? '';
827 $next = '/models';
828 }
829 }
830 return $rawModels;
831 }
832
833 // Private method to process raw models
834 private function _process_raw_models( $rawModels ) {
835 $models = [];
836 foreach ( $rawModels as $rawModel ) {
837 $name = trim( $rawModel['name'] );
838 $family = trim( $rawModel['owner'] );
839 $tags = [ 'image', 'text-to-image' ];
840 $model = $family . '/' . $name;
841 $version = isset( $rawModel['latest_version']['id'] ) ? $rawModel['latest_version']['id'] : null;
842
843 if ( $family === 'stability-ai' ) {
844 $tags[] = 'image-to-image';
845 $tags[] = 'inpainting';
846 }
847
848 $resolutions = [];
849
850 // Black Forest Labs
851 if ( $family === 'black-forest-labs' ) {
852 // These work at least for Flux Pro
853 $resolutions[] = [ 'name' => '1:1', 'label' => 'Square (1:1)' ];
854 $resolutions[] = [ 'name' => '16:9', 'label' => 'Widescreen (16:9)' ];
855 $resolutions[] = [ 'name' => '2:3', 'label' => 'Portrait (2:3)' ];
856 $resolutions[] = [ 'name' => '3:2', 'label' => 'Landscape (3:2)' ];
857 $resolutions[] = [ 'name' => '4:5', 'label' => 'Portrait (4:5)' ];
858 $resolutions[] = [ 'name' => '5:4', 'label' => 'Landscape (5:4)' ];
859 $resolutions[] = [ 'name' => '9:16', 'label' => 'Story (9:16)' ];
860 }
861
862 // Stability AI
863 if ( $family === 'stability-ai' ) {
864 $heights = [ 64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 1152, 1216, 1344, 1536 ];
865 $widths = [ 64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024 ];
866 $resolutions = $this->generate_resolutions( $widths, $heights );
867 }
868
869 $models[] = [
870 'model' => $model,
871 'name' => $name,
872 'family' => $family,
873 'version' => $version,
874 'features' => [ 'text-to-image' ],
875 'price' => null,
876 'type' => 'image',
877 'resolutions' => $resolutions,
878 'unit' => 1 / 1000,
879 'maxCompletionTokens' => null,
880 'maxContextualTokens' => null,
881 'tags' => $tags
882 ];
883 }
884 return $models;
885 }
886 }
887