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