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