PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.6.1
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.6.1
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 1 year ago core.php 1 year ago factory.php 1 year ago google.php 1 year ago huggingface.php 1 year ago openai.php 1 year ago openrouter.php 1 year ago replicate.php 1 year ago
replicate.php
830 lines
1 <?php
2
3 class Meow_MWAI_Engines_Replicate extends Meow_MWAI_Engines_Core
4 {
5 // Base (Replicate)
6 protected $apiKey = null;
7 protected $organizationId = null;
8
9 // Response
10 protected $inModel = null;
11 protected $inId = null;
12
13 // Streaming
14 protected $streamFunctionCall = null;
15 protected $streamToolCalls = [];
16 protected $streamLastMessage = null;
17 protected $streamImageIds = [];
18 protected $streamInTokens = null;
19 protected $streamOutTokens = null;
20
21 public function __construct( $core, $env )
22 {
23 parent::__construct( $core, $env );
24 $this->set_environment();
25 }
26
27 public function reset_stream() {
28 $this->streamContent = null;
29 $this->streamBuffer = null;
30 $this->streamFunctionCall = null;
31 $this->streamToolCalls = [];
32 $this->streamLastMessage = null;
33 $this->streamInTokens = null;
34 $this->streamOutTokens = null;
35 $this->inModel = null;
36 $this->inId = null;
37 }
38
39 protected function set_environment() {
40 $env = $this->env;
41 $this->apiKey = $env['apikey'];
42 }
43
44 protected function build_messages( $query ) {
45 $messages = [];
46
47 // First, we need to add the first message (the instructions).
48 if ( !empty( $query->instructions ) ) {
49 $messages[] = [ 'role' => 'system', 'content' => $query->instructions ];
50 }
51
52 // Then, if any, we need to add the 'messages', they are already formatted.
53 foreach ( $query->messages as $message ) {
54 $messages[] = $message;
55 }
56
57 // If there is a context, we need to add it.
58 if ( !empty( $query->context ) ) {
59 $messages[] = [ 'role' => 'system', 'content' => $query->context ];
60 }
61
62 // Finally, we need to add the message, but if there is an image, we need to add it as a system message.
63 if ( $query->attachedFile ) {
64 $finalUrl = null;
65 if ( $query->image_remote_upload ) {
66 $finalUrl = $query->attachedFile->get_url();
67 }
68 else {
69 $finalUrl = $query->attachedFile->get_inline_base64_url();
70 }
71 $messages[] = [
72 'role' => 'user',
73 'content' => [
74 [
75 "type" => "text",
76 "text" => $query->get_message()
77 ],
78 [
79 "type" => "image_url",
80 "image_url" => [
81 "url" => $finalUrl
82 ]
83 ]
84 ]
85 ];
86 }
87 else {
88 $messages[] = [ 'role' => 'user', 'content' => $query->get_message() ];
89 }
90
91 return $messages;
92 }
93
94 protected function build_body( $query, $streamCallback = null, $extra = null ) {
95 if ( $query instanceof Meow_MWAI_Query_Text ) {
96 $body = array(
97 "model" => $query->model,
98 "stream" => !is_null( $streamCallback ),
99 );
100
101 if ( !empty( $query->maxTokens ) ) {
102 $body['max_tokens'] = $query->maxTokens;
103 }
104
105 if ( !empty( $query->temperature ) ) {
106 $body['temperature'] = $query->temperature;
107 }
108
109 if ( !empty( $query->maxResults ) ) {
110 $body['n'] = $query->maxResults;
111 }
112
113 if ( !empty( $query->stop ) ) {
114 $body['stop'] = $query->stop;
115 }
116
117 if ( !empty( $query->responseFormat ) ) {
118 if ( $query->responseFormat === 'json' ) {
119 $body['response_format'] = [ 'type' => 'json_object' ];
120 }
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 = array(
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 = array( 'input' => $query->message, 'model' => $query->model );
188 if ( $this->envType === 'azure' ) {
189 $body = array( "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 = array( "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 }
220 }
221 else if ( strpos( $model, 'stability-ai/' ) === 0 ) {
222 $body['input']['prompt'] = $query->message;
223 $body['input']['num_inference_steps'] = 25;
224 if ( !empty( $query->resolution ) ) {
225 // $query->resolution is actually a string like 1024x1024
226 $parts = explode( 'x', $query->resolution );
227 $width = intval( $parts[0] );
228 $height = intval( $parts[1] );
229 $body['input']['width'] = $width;
230 $body['input']['height'] = $height;
231 }
232 }
233 else {
234 throw new Exception( 'The model ' . $model . ' is not supported for text-to-image.' );
235 }
236
237 // seed
238 // steps: Number of diffusion steps
239 // 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.
240 // 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.
241 // aspect_ratio: Aspect ratio for the generated image
242 // safety_tolerance: Safety tolerance, 1 is most strict and 5 is most permissive
243 }
244 return $body;
245 }
246 }
247
248 protected function build_url( $query, $endpoint = null ) {
249 $url = "";
250 $env = $this->env;
251 // This endpoint is basically Replicate or Azure, but in the case this class
252 // is overriden, we can pass the endpoint directly (for OpenRouter or HuggingFace, for example).
253 if ( empty( $endpoint ) ) {
254 $endpoint = apply_filters( 'mwai_replicate_endpoint', 'https://api.replicate.com/v1', $this->env );
255 }
256 // Add the base API to the URL
257 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
258 throw new Exception( 'Not implemented yet.' );
259 return $url;
260 }
261 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
262 throw new Exception( 'Not implemented yet.' );
263 return $url;
264 }
265 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
266 throw new Exception( 'Not implemented yet.' );
267 return $url;
268 }
269 else if ( $query instanceof Meow_MWAI_Query_Image ) {
270 //$url .= trailingslashit( $endpoint ) . 'models/' . $query->model . '/predictions';
271 $url .= trailingslashit( $endpoint ) . 'predictions';
272 return $url;
273 }
274 throw new Exception( 'The query is not supported by build_url().' );
275 }
276
277 protected function build_headers( $query ) {
278 if ( $query->apiKey ) {
279 $this->apiKey = $query->apiKey;
280 }
281 if ( empty( $this->apiKey ) ) {
282 throw new Exception( 'No API Key provided. Please visit the Settings.' );
283 }
284 $headers = array(
285 'Content-Type' => 'application/json',
286 'Authorization' => 'Bearer ' . $this->apiKey,
287 );
288 if ( $this->organizationId ) {
289 $headers['Replicate-Organization'] = $this->organizationId;
290 }
291 if ( $this->envType === 'azure' ) {
292 $headers = array( 'Content-Type' => 'application/json', 'api-key' => $this->apiKey );
293 }
294 return $headers;
295 }
296
297 protected function build_options( $headers, $json = null, $forms = null, $method = 'POST' ) {
298 $body = null;
299 if ( !empty( $forms ) ) {
300 $boundary = wp_generate_password ( 24, false );
301 $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
302 $body = $this->build_form_body( $forms, $boundary );
303 }
304 else if ( !empty( $json ) ) {
305 $body = json_encode( $json );
306 }
307 $options = array(
308 'headers' => $headers,
309 'method' => $method,
310 'timeout' => MWAI_TIMEOUT,
311 'body' => $body,
312 'sslverify' => false
313 );
314 return $options;
315 }
316
317 public function run_query( $url, $options, $isStream = false ) {
318 try {
319 $options['stream'] = $isStream;
320 if ( $isStream ) {
321 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
322 }
323 $res = wp_remote_get( $url, $options );
324
325 if ( is_wp_error( $res ) ) {
326 throw new Exception( $res->get_error_message() );
327 }
328
329 $responseCode = wp_remote_retrieve_response_code( $res );
330 if ( $responseCode === 404 ) {
331 throw new Exception( 'The model\'s API URL was not found: ' . $url );
332 }
333 if ( $responseCode === 400 ) {
334 $message = wp_remote_retrieve_body( $res );
335 if ( empty( $message ) ) {
336 $message = wp_remote_retrieve_response_message( $res );
337 }
338 if ( empty( $message ) ) {
339 $message = 'Bad Request';
340 }
341 throw new Exception( $message );
342 }
343
344 if ( $isStream ) {
345 return [ 'stream' => true ];
346 }
347
348 $response = wp_remote_retrieve_body( $res );
349 $headersRes = wp_remote_retrieve_headers( $res );
350 $headers = $headersRes->getAll();
351
352 // Check if Content-Type is 'multipart/form-data' or 'text/plain'
353 // If so, we don't need to decode the response
354 $normalizedHeaders = array_change_key_case( $headers, CASE_LOWER );
355 $resContentType = $normalizedHeaders['content-type'] ?? '';
356 if ( strpos( $resContentType, 'multipart/form-data' ) !== false || strpos( $resContentType, 'text/plain' ) !== false ) {
357 return [ 'stream' => false, 'headers' => $headers, 'data' => $response ];
358 }
359
360 $data = json_decode( $response, true );
361 $this->handle_response_errors( $data );
362 return [ 'headers' => $headers, 'data' => $data ];
363 }
364 catch ( Exception $e ) {
365 Meow_MWAI_Logging::error( 'Replicate: ' . $e->getMessage() );
366 throw $e;
367 }
368 finally {
369 if ( $isStream && file_exists( $options['filename'] ) ) {
370 unlink( $options['filename'] );
371 }
372 }
373 }
374
375 public function try_decode_error( $data ) {
376 $json = json_decode( $data, true );
377 if ( isset( $json['error']['message'] ) ) {
378 return $json['error']['message'];
379 }
380 return null;
381 }
382
383 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
384 $isStreaming = !is_null( $streamCallback );
385
386 if ( $isStreaming ) {
387 $this->streamCallback = $streamCallback;
388 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
389 }
390
391 $this->reset_stream();
392 $body = $this->build_body( $query, $streamCallback );
393 $url = $this->build_url( $query );
394 $headers = $this->build_headers( $query );
395 $options = $this->build_options( $headers, $body );
396
397 try {
398 $res = $this->run_query( $url, $options, $streamCallback );
399 $reply = new Meow_MWAI_Reply( $query );
400
401 $returned_id = null;
402 $returned_model = $this->inModel;
403 $returned_in_tokens = null;
404 $returned_out_tokens = null;
405 $returned_price = null;
406 $returned_choices = [];
407
408 // Streaming Mode
409 if ( $isStreaming ) {
410 if ( empty( $this->streamContent ) ) {
411 $error = $this->try_decode_error( $this->streamBuffer );
412 if ( !is_null( $error ) ) {
413 throw new Exception( $error );
414 }
415 }
416 $returned_id = $this->inId;
417 $returned_model = $this->inModel ? $this->inModel : $query->model;
418 $message = [ 'role' => 'assistant', 'content' => $this->streamContent ];
419 if ( !empty( $this->streamFunctionCall ) ) {
420 $message['function_call'] = $this->streamFunctionCall;
421 }
422 if ( !empty( $this->streamToolCalls ) ) {
423 $message['tool_calls'] = $this->streamToolCalls;
424 }
425 if ( !is_null( $this->streamInTokens ) ) {
426 $returned_in_tokens = $this->streamInTokens;
427 }
428 if ( !is_null( $this->streamOutTokens ) ) {
429 $returned_out_tokens = $this->streamOutTokens;
430 }
431 $returned_choices = [ [ 'message' => $message ] ];
432 }
433 // Standard Mode
434 else {
435 $data = $res['data'];
436 if ( empty( $data ) ) {
437 throw new Exception( 'No content received (res is null).' );
438 }
439 if ( !$data['model'] ) {
440 Meow_MWAI_Logging::error( 'Replicate: Invalid response (no model information).' );
441 Meow_MWAI_Logging::error( print_r( $data, 1 ) );
442 throw new Exception( 'Invalid response (no model information).' );
443 }
444 $returned_id = $data['id'];
445 $returned_model = $data['model'];
446 $returned_in_tokens = isset( $data['usage']['prompt_tokens'] ) ?
447 $data['usage']['prompt_tokens'] : null;
448 $returned_out_tokens = isset( $data['usage']['completion_tokens'] ) ?
449 $data['usage']['completion_tokens'] : null;
450 $returned_price = isset( $data['usage']['total_cost'] ) ?
451 $data['usage']['total_cost'] : null;
452 $returned_choices = $data['choices'];
453 }
454
455 // Set the results.
456 $reply->set_choices( $returned_choices );
457 if ( !empty( $returned_id ) ) {
458 $reply->set_id( $returned_id );
459 }
460 if ( !empty( $returned_id ) ) {
461 $reply->set_id( $returned_id );
462 }
463
464 return $reply;
465 }
466 catch ( Exception $e ) {
467 Meow_MWAI_Logging::error( 'Replicate: ' . $e->getMessage() );
468 $message = "From Replicate: " . $e->getMessage();
469 throw new Exception( $message );
470 }
471 finally {
472 if ( !is_null( $streamCallback ) ) {
473 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
474 }
475 }
476 }
477
478 // TODO: We should find a way to add text-to-image somewhere in this query
479 public function run_image_query( $query ) {
480 $body = $this->build_body( $query );
481 $url = $this->build_url( $query );
482 $headers = $this->build_headers( $query );
483 $options = $this->build_options( $headers, $body );
484
485 try {
486 $res = $this->run_query( $url, $options );
487 $data = $res['data'];
488 if ( $data['status'] === 422 ) {
489 if ( isset( $data['title'] ) && isset( $data['detail'] ) ) {
490 throw new Exception( $data['title'] . ': ' . $data['detail'] );
491 }
492 throw new Exception( 'The image generation failed.' );
493 }
494 $getUrl = $data['urls']['get'];
495 $status = $data['status'];
496 while ( $status === 'processing' || $status === 'starting' ) {
497 sleep( 1 );
498 $data = $this->execute( 'GET', $getUrl, null, null, true, null, null, true );
499 $status = $data['status'];
500 }
501 if ( $status !== 'succeeded' ) {
502 // if $data has title and detail, we can use them to throw a more detailed error
503 if ( isset( $data['title'] ) && isset( $data['detail'] ) ) {
504 throw new Exception( $data['title'] . ': ' . $data['detail'] );
505 }
506 throw new Exception( 'The image generation failed.' );
507 }
508 $choices = [];
509 $output = isset( $data['output'] ) ? $data['output'] : [];
510 // Flux Schnell returns an array of urls in 'output'
511 if ( is_array( $output ) ) {
512 foreach ( $output as $item ) {
513 $choices[] = [ 'url' => $item ];
514 }
515 }
516 // Flux Schnell returns 'url' in 'output'
517 else if ( is_string( $output ) ) {
518 $choices[] = [ 'url' => $output ];
519 }
520 if ( empty( $choices ) ) {
521 throw new Exception( 'No output URL received.' );
522 }
523 $reply = new Meow_MWAI_Reply( $query );
524 $model = $query->model;
525 $resolution = null;
526 if ( isset( $data['metrics']['width'] ) && isset( $data['metrics']['height'] ) ) {
527 $resolution = $data['metrics']['width'] . 'x' . $data['metrics']['height'];
528 }
529 else {
530 $raw_resolution = Meow_MWAI_Core::get_image_resolution( $choices[0]['url'] );
531 if ( !empty( $raw_resolution ) ) {
532 $resolution = $raw_resolution['width'] . 'x' . $raw_resolution['height'];
533 }
534
535 }
536 if ( !empty( $resolution ) ) {
537 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
538 $reply->set_usage( $usage );
539 }
540
541 $reply->set_choices( $choices );
542 $reply->set_type( 'images' );
543
544 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
545 foreach ( $reply->results as &$result ) {
546 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
547 'query_envId' => $query->envId,
548 'query_session' => $query->session,
549 'query_model' => $query->model,
550 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
551 $fileUrl = $this->core->files->get_url( $fileId );
552 $result = $fileUrl;
553 }
554 }
555 $reply->result = $reply->results[0];
556 return $reply;
557 }
558 catch ( Exception $e ) {
559 Meow_MWAI_Logging::error( 'Replicate: ' . $e->getMessage() );
560 throw new Exception( "From Replicate: " . $e->getMessage() );
561 }
562 }
563
564 /*
565 This is the rest of the Replicate API support, not related to the models directly.
566 */
567
568 // Check if there are errors in the response from Replicate, and throw an exception if so.
569 protected function handle_response_errors( $data ) {
570 if ( isset( $data['error'] ) && !empty( $data['error'] ) ) {
571 $message = $data['error'];
572 throw new Exception( $message );
573 }
574 }
575
576 /**
577 * Build the body of a form request.
578 * If the field name is 'file', then the field value is the filename of the file to upload.
579 * The file contents are taken from the 'data' field.
580 *
581 * @param array $fields
582 * @param string $boundary
583 * @return string
584 */
585 public function build_form_body( $fields, $boundary )
586 {
587 $body = '';
588 foreach ( $fields as $name => $value ) {
589 if ( $name == 'data' ) {
590 continue;
591 }
592 $body .= "--$boundary\r\n";
593 $body .= "Content-Disposition: form-data; name=\"$name\"";
594 if ( $name == 'file' ) {
595 $body .= "; filename=\"{$value}\"\r\n";
596 $body .= "Content-Type: application/json\r\n\r\n";
597 $body .= $fields['data'] . "\r\n";
598 }
599 else {
600 $body .= "\r\n\r\n$value\r\n";
601 }
602 }
603 $body .= "--$boundary--\r\n";
604 return $body;
605 }
606
607 /**
608 * Run a request to the Replicate API.
609 * Fore more information about the $formFields, refer to the build_form_body method.
610 *
611 * @param string $method POST, PUT, GET, DELETE...
612 * @param string $url The API endpoint
613 * @param array $query The query parameters (json)
614 * @param array $formFields The form fields (multipart/form-data)
615 * @param bool $json Whether to return the response as json or not
616 * @return array
617 */
618 public function execute( $method, $url, $query = null, $formFields = null,
619 $json = true, $extraHeaders = null, $streamCallback = null, $overrideUrl = false ) {
620 $headers = "Content-Type: application/json\r\n" . "Authorization: Bearer " . $this->apiKey . "\r\n";
621 $body = $query ? json_encode( $query ) : null;
622 if ( !empty( $formFields ) ) {
623 $boundary = wp_generate_password( 24, false );
624 $headers = [
625 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
626 'Authorization' => 'Bearer ' . $this->apiKey
627 ];
628 $body = $this->build_form_body( $formFields, $boundary );
629 }
630
631 // Maybe we should have headers always as an array... not sure why we have it as a string.
632 if ( !empty( $extraHeaders ) ) {
633 foreach ( $extraHeaders as $key => $value ) {
634 if ( is_array( $headers ) ) {
635 $headers[$key] = $value;
636 }
637 else {
638 $headers .= "$key: $value\r\n";
639 }
640 }
641 }
642
643 // If it's a GET, body should be null, and we should append the query to the URL.
644 if ( $method === 'GET' ) {
645 if ( !empty( $query ) ) {
646 $url .= '?' . http_build_query( $query );
647 }
648 $body = null;
649 }
650
651 $url = $overrideUrl ? $url : ( 'https://api.replicate.com/v1' . $url );
652 $options = [
653 "headers" => $headers,
654 "method" => $method,
655 "timeout" => MWAI_TIMEOUT,
656 "body" => $body,
657 "sslverify" => false
658 ];
659
660 try {
661 if ( !is_null( $streamCallback ) ) {
662 $options['stream'] = true;
663 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
664 // The stream handler calls the streamCallback every time there is content
665 // TODO: For assistants, we should probably have a different stream handler to
666 // handle the assistant's specific reply and perform the necessary actions.
667 $this->streamCallback = $streamCallback;
668 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
669 }
670 $res = wp_remote_request( $url, $options );
671 if ( is_wp_error( $res ) ) {
672 throw new Exception( $res->get_error_message() );
673 }
674 $res = wp_remote_retrieve_body( $res );
675 $data = $json ? json_decode( $res, true ) : $res;
676 $this->handle_response_errors( $data );
677 return $data;
678 }
679 catch ( Exception $e ) {
680 Meow_MWAI_Logging::error( 'Replicate: ' . $e->getMessage() );
681 throw new Exception( $e->getMessage() );
682 //throw new Exception( 'From Replicate: ' . $e->getMessage() );
683 }
684 finally {
685 if ( !is_null( $streamCallback ) ) {
686 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
687 }
688 if ( !empty( $options['stream'] ) && file_exists( $options['filename'] ) ) {
689 unlink( $options['filename'] );
690 }
691 }
692 }
693
694 public function get_models() {
695 return $this->core->get_engine_models( 'replicate' );
696 }
697
698 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
699 {
700 return null;
701 }
702
703 function generate_resolutions( $widths, $heights ) {
704 $resolutions = [];
705 $acceptable_ratios = [
706 '1:1' => [ 'name' => 'Square', 'ratio' => 1 ],
707 '16:9' => [ 'name' => 'Widescreen', 'ratio' => 16/9 ],
708 '2:3' => [ 'name' => 'Portrait', 'ratio' => 2/3 ],
709 '3:2' => [ 'name' => 'Landscape', 'ratio' => 3/2 ],
710 '4:5' => [ 'name' => 'Portrait', 'ratio' => 4/5 ],
711 '5:4' => [ 'name' => 'Landscape', 'ratio' => 5/4 ],
712 '9:16' => [ 'name' => 'Story', 'ratio' => 9/16 ]
713 ];
714
715 foreach ( $widths as $width ) {
716 foreach ( $heights as $height ) {
717 if ( $height <= 1024 && $width <= 1536 && $height >= 64 && $width >= 64 ) {
718 $ratio = $width / $height;
719 $ratio_name = null;
720
721 foreach ( $acceptable_ratios as $key => $ratio_info ) {
722 if ( abs( $ratio - $ratio_info['ratio'] ) < 0.01 ) {
723 $ratio_name = $key;
724 $ratio_label = $ratio_info['name'];
725 break;
726 }
727 }
728
729 if ( $ratio_name ) {
730 $label = "{$ratio_label} ({$ratio_name}): {$width}x{$height}";
731 $resolutions[] = [
732 'name' => "{$width}x{$height}",
733 'label' => $label
734 ];
735 }
736 }
737 }
738 }
739
740 // Sort resolutions by total pixel count
741 usort( $resolutions, function( $a, $b ) {
742 $aPixels = explode( 'x', $a['name'] );
743 $bPixels = explode( 'x', $b['name'] );
744 return ( $aPixels[0] * $aPixels[1] ) - ( $bPixels[0] * $bPixels[1] );
745 } );
746
747 return $resolutions;
748 }
749
750 public function retrieve_models() {
751 $rawModels = [];
752 $next = '/models';
753 $cursor = null;
754 $allowed_owners = [ 'black-forest-labs', 'stability-ai' ];
755 while ( $next ) {
756 $query_args = $cursor ? [ 'cursor' => $cursor ] : [];
757 $response = $this->execute( 'GET', $next, $query_args );
758 if ( !is_array( $response ) || empty( $response['results'] ) ) {
759 break;
760 }
761 $filtered_results = array_filter( $response['results'], function( $model ) use ( $allowed_owners ) {
762 $isAllowedOwner = isset( $model['owner'] ) && in_array( $model['owner'], $allowed_owners );
763 $isPublic = isset( $model['visibility'] ) && $model['visibility'] === 'public';
764 return $isAllowedOwner && $isPublic;
765 } );
766 $rawModels = array_merge( $rawModels, $filtered_results );
767 if ( empty( $response['next'] ) ) {
768 break;
769 }
770 $parsed_url = wp_parse_url( $response['next'] );
771 parse_str( $parsed_url['query'] ?? '', $query_params );
772 $cursor = $query_params['cursor'] ?? '';
773 $next = '/models';
774 }
775 $models = array();
776 foreach ( $rawModels as $rawModel ) {
777
778 $name = trim( $rawModel['name'] );
779 $family = trim( $rawModel['owner'] );
780 $tags = ['image', 'text-to-image'];
781 $model = $family . '/' . $name;
782 $version = isset( $rawModel['latest_version']['id'] ) ? $rawModel['latest_version']['id'] : null;
783
784 // outpainting, super-resolution,
785 if ( $family === 'stability-ai' ) {
786 $tags[] = 'image-to-image';
787 $tags[] = 'inpainting';
788 }
789
790 $resolutions = [];
791
792 // Black Forest Labs
793 if ( $family === 'black-forest-labs' ) {
794
795 // Those works at least for Flux Pro
796 $resolutions[] = [ 'name' => '1:1', 'label' => 'Square (1:1)' ];
797 $resolutions[] = [ 'name' => '16:9', 'label' => 'Widescreen (16:9)' ];
798 $resolutions[] = [ 'name' => '2:3', 'label' => 'Portrait (2:3)' ];
799 $resolutions[] = [ 'name' => '3:2', 'label' => 'Landscape (3:2)' ];
800 $resolutions[] = [ 'name' => '4:5', 'label' => 'Portrait (4:5)' ];
801 $resolutions[] = [ 'name' => '5:4', 'label' => 'Landscape (5:4)' ];
802 $resolutions[] = [ 'name' => '9:16', 'label' => 'Story (9:16)' ];
803 }
804
805 // Stability AI
806 if ( $family === 'stability-ai' ) {
807 $heights = [64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 1152, 1216, 1344, 1536];
808 $widths = [64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024];
809 $resolutions = $this->generate_resolutions( $widths, $heights );
810 }
811
812 $models[] = array(
813 'model' => $model,
814 'name' => $name,
815 'family' => $family,
816 'version' => $version,
817 'features' => ['text-to-image'],
818 'price' => null,
819 'type' => 'image',
820 'resolutions' => $resolutions,
821 'unit' => 1 / 1000,
822 'maxCompletionTokens' => null,
823 'maxContextualTokens' => null,
824 'tags' => $tags
825 );
826 }
827 return $models;
828 }
829 }
830