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