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