PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.2.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.2.2
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 / google.php
ai-engine / classes / engines Last commit date
core.php 2 years ago factory.php 2 years ago google.php 2 years ago huggingface.php 2 years ago openai.php 2 years ago openrouter.php 2 years ago
google.php
501 lines
1 <?php
2
3 class Meow_MWAI_Engines_Google extends Meow_MWAI_Engines_Core
4 {
5 // Base (Google)
6 protected $apiKey = null;
7 protected $region = null;
8 protected $projectId = null;
9 protected $endpoint = null;
10
11 // Response
12 protected $inModel = null;
13 protected $inId = null;
14
15 // Streaming
16 private $streamTemporaryBuffer = "";
17 private $streamBuffer = "";
18 private $streamContent = "";
19 private $streamFunctionCall = null;
20 private $streamCallback = null;
21
22 public function __construct( $core, $env )
23 {
24 parent::__construct( $core, $env );
25 $this->set_environment();
26 }
27
28 protected function set_environment() {
29 $env = $this->env;
30 $this->apiKey = $env['apikey'];
31 if ( $this->envType === 'google' ) {
32 // https://{REGION}-aiplatform.googleapis.com/v1/projects/{PROJECT_ID}/locations/{REGION}/publishers/google
33 $this->region = $env['region'];
34 $this->projectId = $env['projectId'];
35
36 // Google Cloud API
37 // $this->endpoint = apply_filters( 'mwai_google_endpoint', "https://{$this->region}-aiplatform.googleapis.com/v1/projects/{$this->projectId}/locations/{$this->region}/publishers/google", $this->env );
38
39 // Generative Language API (less issues with auth)
40 $this->endpoint = apply_filters( 'mwai_google_endpoint', "https://generativelanguage.googleapis.com/v1", $this->env );
41 }
42 else {
43 throw new Exception( 'Unknown environment type: ' . $this->envType );
44 }
45 }
46
47 // Check for a JSON-formatted error in the data, and throw an exception if it's the case.
48 function check_for_error( $data ) {
49 if ( strpos( $data, 'error' ) === false ) {
50 return;
51 }
52 if ( strpos( $data, 'data: ' ) === 0 ) {
53 $jsonPart = substr( $data, strlen( 'data: ' ) );
54 }
55 else {
56 $jsonPart = $data;
57 }
58 $json = json_decode( $jsonPart, true );
59 if ( json_last_error() === JSON_ERROR_NONE ) {
60 if ( isset( $json['error'] ) ) {
61 $error = $json['error'];
62 $code = $error['code'];
63 $message = $error['message'];
64 throw new Exception( "Error $code: $message" );
65 }
66 }
67 }
68
69 private function build_messages( $query ) {
70 $messages = [];
71
72 // First, we need to add the first message (the instructions).
73 if ( !empty( $query->instructions ) ) {
74 $messages[] = [ 'role' => 'model', 'parts' => [ [ 'text' => $query->instructions ] ] ];
75 }
76
77 // Then, if any, we need to add the 'messages', they are already formatted.
78 foreach ( $query->messages as $message ) {
79 // messages contains role and content (as OpenAI does it, but we need to convert it to Google's format)
80 // role's assistant should be model, and user should be user.
81 $newMessage = [ 'role' => $message['role'], 'parts' => [] ];
82 if ( isset( $message['content'] ) ) {
83 $newMessage['parts'][] = [ 'text' => $message['content'] ];
84 }
85 if ( $newMessage['role'] === 'assistant' ) {
86 $newMessage['role'] = 'model';
87 }
88 $messages[] = $newMessage;
89 }
90
91 // If there is a context, we need to add it.
92 if ( !empty( $query->context ) ) {
93 $messages[] = [ 'role' => 'model', 'parts' => [ [ 'text' => $query->context ] ] ];
94 }
95
96 // Finally, we need to add the message, but if there is an image, we need to add it as a model message.
97 $fileUrl = $query->get_file_url();
98 if ( !empty( $fileUrl ) ) {
99 // If the fileUrl actually is data (starts with "data:")
100 $isData = strpos( $fileUrl, 'data:' ) === 0;
101 if ( $isData ) {
102 $messages[] = [
103 'role' => 'user',
104 'parts' => [
105 [
106 "inlineData" => [
107 "mimeType" => "image/jpeg",
108 "data" => $query->file // We need to be careful here to get only the data part
109 ]
110 ],
111 [
112 "text" => $query->get_message()
113 ]
114 ]
115 ];
116 }
117 else {
118 $messages[] = [
119 'role' => 'user',
120 'parts' => [
121 [
122 "fileData" => [
123 "mimeType" => "image/jpeg",
124 "fileUri" => $fileUrl
125 ]
126 ],
127 [
128 "text" => $query->get_message()
129 ]
130 ]
131 ];
132 }
133 // TODO: Gemini doesn't support multiturn chat with Vision...
134 // So we only keep the message that goes with the image.
135 $messages = array_slice( $messages, -1 );
136 }
137 else {
138 $messages[] = [ 'role' => 'user', 'parts' => [ [ 'text' => $query->get_message() ] ] ];
139 }
140
141 // Streamline the messages
142 $messages = $this->streamline_messages( $messages, 'model', 'parts' );
143
144 return $messages;
145 }
146
147 /*
148 This used to be in the core.php, but since it's relative to OpenAI, it's better to have it here.
149 */
150
151 public function stream_handler( $handle, $args, $url ) {
152 curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, false );
153 curl_setopt( $handle, CURLOPT_SSL_VERIFYHOST, false );
154
155 // Maybe we could get some info from headers, as for now, there is only the model.
156 // curl_setopt( $handle, CURLOPT_HEADERFUNCTION, function( $curl, $headerLine ) {
157 // $line = trim( $headerLine );
158 // return strlen( $headerLine );
159 // });
160
161 curl_setopt( $handle, CURLOPT_WRITEFUNCTION, function ( $curl, $data ) {
162 $length = strlen( $data );
163
164 // FOR DEBUG:
165 // preg_match_all( '/"content":"(.*?)"/', $data, $matches );
166 // $contents = $matches[1];
167 // foreach ( $contents as $content ) {
168 // error_log( "Content: $content" );
169 // }
170
171 // Error Management
172 $this->check_for_error( $data );
173
174 // Bufferize the unfinished stream (if it's the case)
175 $this->streamTemporaryBuffer .= $data;
176 $this->streamBuffer .= $data;
177 $lines = explode( "\n", $this->streamTemporaryBuffer );
178 if ( substr( $this->streamTemporaryBuffer, -1 ) !== "\n" ) {
179 $this->streamTemporaryBuffer = array_pop( $lines );
180 }
181 else {
182 $this->streamTemporaryBuffer = "";
183 }
184
185 foreach ( $lines as $line ) {
186 if ( $line === "" ) {
187 continue;
188 }
189 if ( strpos( $line, 'data: ' ) === 0 ) {
190 $line = substr( $line, 6 );
191 $json = json_decode( $line, true );
192
193 if ( json_last_error() === JSON_ERROR_NONE ) {
194 $content = null;
195 // Get the content
196 if ( isset( $json['candidates'][0]['content']['parts'][0]['text'] ) ) {
197 $content = $json['candidates'][0]['content']['parts'][0]['text'];
198 }
199 // else if ( isset( $json['candidates'][0]['delta']['content'] ) ) {
200 // $content = $json['candidates'][0]['delta']['content'];
201 // }
202 if ( $content !== null && $content !== "" ) {
203 $this->streamContent .= $content;
204 call_user_func( $this->streamCallback, $content );
205 }
206 }
207 else {
208 $this->streamTemporaryBuffer .= $line . "\n";
209 }
210 }
211 }
212 return $length;
213 });
214 }
215
216 protected function build_headers( $query ) {
217 if ( $query->apiKey ) {
218 $this->apiKey = $query->apiKey;
219 }
220 if ( empty( $this->apiKey ) ) {
221 throw new Exception( 'No API Key provided. Please visit the Settings.' );
222 }
223 $headers = array(
224 'Content-Type' => 'application/json',
225 );
226 return $headers;
227 }
228
229 protected function build_options( $headers, $json = null, $forms = null, $method = 'POST' ) {
230 $body = null;
231 if ( !empty( $forms ) ) {
232 throw new Exception( 'No support for form-data requests yet.' );
233 // $boundary = wp_generate_password ( 24, false );
234 // $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
235 // $body = $this->build_form_body( $forms, $boundary );
236 }
237 else if ( !empty( $json ) ) {
238 $body = json_encode( $json );
239 }
240 $options = array(
241 'headers' => $headers,
242 'method' => $method,
243 'timeout' => MWAI_TIMEOUT,
244 'body' => $body,
245 'sslverify' => false
246 );
247 return $options;
248 }
249
250 public function run_query( $url, $options, $isStream = false ) {
251 try {
252 $options['stream'] = $isStream;
253 if ( $isStream ) {
254 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
255 }
256 $res = wp_remote_get( $url, $options );
257
258 if ( is_wp_error( $res ) ) {
259 throw new Exception( $res->get_error_message() );
260 }
261
262 if ( $isStream ) {
263 return [ 'stream' => true ];
264 }
265
266 $response = wp_remote_retrieve_body( $res );
267 $headersRes = wp_remote_retrieve_headers( $res );
268 $headers = $headersRes->getAll();
269
270 // Check if Content-Type is 'multipart/form-data' or 'text/plain'
271 // If so, we don't need to decode the response
272 $normalizedHeaders = array_change_key_case( $headers, CASE_LOWER );
273 $resContentType = $normalizedHeaders['content-type'] ?? '';
274 if ( strpos( $resContentType, 'multipart/form-data' ) !== false || strpos( $resContentType, 'text/plain' ) !== false ) {
275 return [ 'stream' => false, 'headers' => $headers, 'data' => $response ];
276 }
277
278 $data = json_decode( $response, true );
279 $this->handle_response_errors( $data );
280 return [ 'headers' => $headers, 'data' => $data ];
281 }
282 catch ( Exception $e ) {
283 error_log( $e->getMessage() );
284 throw $e;
285 }
286 }
287
288 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
289 if ( !is_null( $streamCallback ) ) {
290 $this->streamCallback = $streamCallback;
291 add_action( 'http_api_curl', array( $this, 'stream_handler' ), 10, 3 );
292 }
293
294 $body = array(
295 "generationConfig" => [
296 "candidateCount" => $query->maxResults,
297 "maxOutputTokens" => $query->maxTokens,
298 "temperature" => $query->temperature,
299 "stopSequences" => [],
300 ],
301 );
302
303 // if ( !empty( $query->stop ) ) {
304 // $body['generationConfig']['stop'] = $query->stop;
305 // }
306
307 // if ( !empty( $query->responseFormat ) ) {
308 // if ( $query->responseFormat === 'json' ) {
309 // $body['response_format'] = [ 'type' => 'json_object' ];
310 // }
311 // }
312
313 if ( !empty( $query->functions ) ) {
314 throw new Exception( 'AI Engine doesn\'t support Function Calling with Google models yet.' );
315 //$body['functions'] = $query->functions;
316 //$body['function_call'] = $query->functionCall;
317 }
318
319 if ( $query->mode !== 'chat' ) {
320 throw new Exception( 'Google models only support chat mode.' );
321 }
322
323 $body['contents'] = $this->build_messages( $query );
324 $url = $this->endpoint;
325
326 // Streaming:
327 // $url .= '/models/' . $query->model . ':streamGenerateContent';
328
329 $url .= '/models/' . $query->model . ':generateContent';
330
331 // If streaming is enabled, we need to use the SSE endpoint.
332 if ( !is_null( $streamCallback ) ) {
333 $url .= '?alt=sse';
334 }
335
336 // Add the API key
337 if ( strpos( $url, '?' ) === false ) {
338 $url .= '?key=' . $this->apiKey;
339 }
340 else {
341 $url .= '&key=' . $this->apiKey;
342 }
343
344 $headers = $this->build_headers( $query );
345 $options = $this->build_options( $headers, $body );
346
347 try {
348 $res = $this->run_query( $url, $options, $streamCallback );
349 $reply = new Meow_MWAI_Reply( $query );
350
351 $returned_id = null;
352 $returned_model = $this->inModel;
353 $returned_in_tokens = null;
354 $returned_out_tokens = null;
355 $returned_choices = [];
356
357 if ( !is_null( $streamCallback ) ) {
358 // Streamed data
359 if ( empty( $this->streamContent ) ) {
360 $json = json_decode( $this->streamBuffer, true );
361 if ( isset( $json['error']['message'] ) ) {
362 throw new Exception( $json['error']['message'] );
363 }
364 }
365 $returned_id = $this->inId;
366 $returned_model = $this->inModel ? $this->inModel : $query->model;
367 $returned_choices = [
368 [
369 'message' => [
370 'content' => $this->streamContent,
371 'function_call' => $this->streamFunctionCall
372 ]
373 ]
374 ];
375 }
376 else {
377 // Regular data
378 $data = $res['data'];
379 if ( empty( $data ) ) {
380 throw new Exception( 'No content received (res is null).' );
381 }
382
383 // Not much information from Google's API :(
384 $returned_id = null;
385 $returned_model = $query->model;
386 $returned_in_tokens = 0;
387 $returned_out_tokens = 0;
388
389 // We should return the candidates formatted as OpenAI does it.
390 $returned_choices = [];
391 if ( isset( $data['candidates'] ) ) {
392 $candidates = $data['candidates'];
393 foreach ( $candidates as $candidate ) {
394 $content = $candidate['content'];
395 $text = $content['parts'][0]['text'];
396 $returned_choices[] = [ 'role' => 'assistant', 'text' => $text ];
397 }
398 }
399 }
400
401 // Set the results.
402 $reply->set_choices( $returned_choices );
403 if ( !empty( $returned_id ) ) {
404 $reply->set_id( $returned_id );
405 }
406
407 // Handle tokens.
408 $this->handle_tokens_usage( $reply, $query, $returned_model, $returned_in_tokens, $returned_out_tokens );
409
410 return $reply;
411 }
412 catch ( Exception $e ) {
413 error_log( $e->getMessage() );
414 $message = "From Google: " . $e->getMessage();
415 throw new Exception( $message );
416 }
417 }
418
419 public function handle_tokens_usage( $reply, $query, $returned_model, $returned_in_tokens, $returned_out_tokens ) {
420 $returned_in_tokens = !is_null( $returned_in_tokens ) ? $returned_in_tokens : $reply->get_in_tokens( $query );
421 $returned_out_tokens = !is_null( $returned_out_tokens ) ? $returned_out_tokens : $reply->get_out_tokens();
422 $usage = $this->core->record_tokens_usage( $returned_model, $returned_in_tokens, $returned_out_tokens );
423 $reply->set_usage( $usage );
424 }
425
426 /*
427 This is the rest of the OpenAI API support, not related to the models directly.
428 */
429
430 // Check if there are errors in the response from OpenAI, and throw an exception if so.
431 public function handle_response_errors( $data ) {
432 if ( isset( $data['error'] ) ) {
433 $message = $data['error']['message'];
434 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
435 $message = str_replace( $matches[1], '', $message );
436 }
437 throw new Exception( $message );
438 }
439 }
440
441 public function get_models() {
442 return $this->core->get_option( 'google_models' );
443 }
444
445 public function retrieve_models() {
446 $url = "https://generativelanguage.googleapis.com/v1/models";
447 $url .= "?key=" . $this->apiKey;
448 $response = wp_remote_get( $url );
449 if ( is_wp_error( $response ) ) {
450 throw new Exception( 'AI Engine: ' . $response->get_error_message() );
451 }
452 $body = json_decode( $response['body'], true );
453 $models = array();
454 foreach ( $body['models'] as $model ) {
455 if ( strpos( $model['name'], 'gemini' ) === false ) {
456 continue;
457 }
458 $family = "gemini";
459 $maxCompletionTokens = $model['outputTokenLimit'];
460 $maxContextualTokens = $model['inputTokenLimit'];
461 $priceIn = 0;
462 $priceOut = 0;
463 $tags = [ 'core', 'chat' ];
464 // If the name contains (beta), (alpha) or (preview), add 'preview' tag and remove from name
465 if ( preg_match( '/\((beta|alpha|preview)\)/i', $model['name'], $matches ) ) {
466 $tags[] = 'preview';
467 $model['name'] = preg_replace( '/\((beta|alpha|preview)\)/i', '', $model['name'] );
468 }
469 // If the name includes 'Vision', add 'vision' tag
470 if ( preg_match( '/vision/i', $model['name'], $matches ) ) {
471 $tags[] = 'vision';
472 }
473 $name = preg_replace( '/^models\//', '', $model['name'] );
474 $model = array(
475 'model' => $name,
476 'name' => $name,
477 'family' => $family,
478 'mode' => 'chat',
479 'type' => 'token',
480 'unit' => 1 / 1000,
481 'maxCompletionTokens' => $maxCompletionTokens,
482 'maxContextualTokens' => $maxContextualTokens,
483 'tags' => $tags
484 );
485 if ( $priceIn > 0 && $priceOut > 0 ) {
486 $model['price'] = array(
487 'in' => $priceIn,
488 'out' => $priceOut,
489 );
490 }
491 $models[] = $model;
492 }
493 return $models;
494 }
495
496 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
497 // TODO: Not sure how to get the price from Google's API.
498 return null;
499 }
500 }
501