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