PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.1.0
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.1.0
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 / services / usage-stats.php
ai-engine / classes / services Last commit date
image.php 1 year ago message-builder.php 10 months ago model-environment.php 11 months ago response-id-manager.php 1 year ago session.php 11 months ago usage-stats.php 9 months ago
usage-stats.php
341 lines
1 <?php
2
3 class Meow_MWAI_Services_UsageStats {
4 private $core;
5 private $tiktoken_encoders = [];
6 private $encoder_provider = null;
7
8 public function __construct( $core ) {
9 $this->core = $core;
10 }
11
12 /**
13 * Get the cl100k_base tiktoken encoder
14 * Note: We always use cl100k_base regardless of model since it's the standard for all modern OpenAI models
15 * @return object|null The tiktoken encoder or null if not available
16 */
17 private function get_tiktoken_encoder() {
18 // Return cached encoder if available
19 if ( isset( $this->tiktoken_encoders['cl100k_base'] ) ) {
20 return $this->tiktoken_encoders['cl100k_base'];
21 }
22
23 try {
24 // Check if class exists
25 if ( !class_exists( 'Yethee\Tiktoken\EncoderProvider' ) ) {
26 error_log( '[AI Engine Tiktoken] EncoderProvider class not found. Check if composer autoload is working.' );
27 return null;
28 }
29
30 // Initialize encoder provider if needed
31 if ( $this->encoder_provider === null ) {
32 $this->encoder_provider = new \Yethee\Tiktoken\EncoderProvider();
33 }
34
35 // Get the cl100k_base encoder (standard for modern OpenAI models)
36 $encoder = $this->encoder_provider->get( 'cl100k_base' );
37 $this->tiktoken_encoders['cl100k_base'] = $encoder;
38
39 // Removed success log to reduce noise
40 return $encoder;
41 }
42 catch ( \Exception $e ) {
43 error_log( '[AI Engine Tiktoken] Failed to initialize encoder: ' . $e->getMessage() );
44 return null;
45 }
46 }
47
48 public function estimate_tokens( ...$args ) {
49 // Handle multiple argument formats for backward compatibility
50 $text = '';
51 $model = null;
52
53 // If first argument is an array, process messages
54 if ( !empty( $args[0] ) && is_array( $args[0] ) ) {
55 foreach ( $args[0] as $message ) {
56 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : '';
57 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : '';
58 }
59 $model = $args[1] ?? null;
60 }
61 // Otherwise treat first argument as text
62 else {
63 $text = $args[0] ?? '';
64 $model = $args[1] ?? null;
65 }
66
67 // Convert to string if needed
68 if ( !is_string( $text ) ) {
69 // Handle arrays that weren't caught by the first condition
70 if ( is_array( $text ) ) {
71 $text = json_encode( $text );
72 }
73 // Handle objects
74 elseif ( is_object( $text ) ) {
75 $text = method_exists( $text, '__toString' ) ? (string) $text : json_encode( $text );
76 }
77 // Handle other types (int, float, bool, null)
78 else {
79 $text = (string) $text;
80 }
81 }
82
83 // Apply filters for customization
84 $text = apply_filters( 'mwai_estimate_tokens_text', $text, $model );
85 $tokens = apply_filters( 'mwai_estimate_tokens', null, $text, $model );
86 if ( $tokens !== null ) {
87 return $tokens;
88 }
89
90 // Try to use tiktoken for accurate counting (cl100k_base encoder)
91 $encoder = $this->get_tiktoken_encoder();
92 if ( $encoder ) {
93 try {
94 $encoded = $encoder->encode( $text );
95 $token_count = count( $encoded );
96
97 // Comparison logging removed - tiktoken is working correctly
98
99 return $token_count;
100 }
101 catch ( Exception $e ) {
102 error_log( '[AI Engine Tiktoken] Encoding failed, falling back to estimation: ' . $e->getMessage() );
103 }
104 }
105 else {
106 error_log( '[AI Engine Tiktoken] Encoder not available, using fallback' );
107 }
108
109 // Fallback to old estimation method
110 return $this->fallback_estimate_tokens( $text );
111 }
112
113 /**
114 * Fallback token estimation method (the original implementation)
115 */
116 private function fallback_estimate_tokens( $text ) {
117 $multiplier = 4;
118 $hasChineseChars = preg_match( '/[\x{4e00}-\x{9fa5}]/u', $text );
119 $hasJapaneseChars = preg_match( '/[\x{3040}-\x{309f}\x{30a0}-\x{30ff}]/u', $text );
120 $hasKoreanChars = preg_match( '/[\x{ac00}-\x{d7af}]/u', $text );
121 if ( $hasChineseChars || $hasJapaneseChars || $hasKoreanChars ) {
122 $multiplier = 2;
123 }
124 $tokens = (int) ( ( function_exists( 'mb_strlen' ) ? mb_strlen( $text ) : strlen( $text ) ) / $multiplier );
125 return $tokens;
126 }
127
128 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
129 if ( !is_numeric( $in_tokens ) ) {
130 $in_tokens = 0;
131 }
132 if ( !is_numeric( $out_tokens ) ) {
133 $out_tokens = 0;
134 }
135
136 // Normalize returned_price once at the beginning
137 if ( !empty( $returned_price ) ) {
138 $returned_price = is_array( $returned_price ) ?
139 ( isset( $returned_price['price'] ) ? $returned_price['price'] : 0 ) :
140 ( is_numeric( $returned_price ) ? $returned_price : 0 );
141 } else {
142 $returned_price = 0;
143 }
144
145 // Record monthly usage
146 $usage = $this->core->get_option( 'ai_usage' );
147 $month = date( 'Y-m' );
148 if ( !isset( $usage[$month] ) ) {
149 $usage[$month] = [];
150 }
151 if ( !isset( $usage[$month][$model] ) ) {
152 $usage[$month][$model] = [
153 'prompt_tokens' => 0,
154 'completion_tokens' => 0,
155 'total_tokens' => 0,
156 'returned_price' => 0,
157 'queries' => 0
158 ];
159 }
160 // Ensure queries field exists for existing data
161 if ( !isset( $usage[$month][$model]['queries'] ) ) {
162 $usage[$month][$model]['queries'] = 0;
163 }
164 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
165 $usage[$month][$model]['completion_tokens'] += $out_tokens;
166 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
167 $usage[$month][$model]['queries'] += 1;
168 $usage[$month][$model]['returned_price'] += $returned_price;
169
170 // Clean up old monthly data (keep only last 2 years)
171 $this->cleanup_old_monthly_data( $usage );
172 $this->core->update_option( 'ai_usage', $usage );
173
174 // Record daily usage
175 $daily_usage = $this->core->get_option( 'ai_usage_daily', [] );
176 $day = date( 'Y-m-d' );
177 if ( !isset( $daily_usage[$day] ) ) {
178 $daily_usage[$day] = [];
179 }
180 if ( !isset( $daily_usage[$day][$model] ) ) {
181 $daily_usage[$day][$model] = [
182 'prompt_tokens' => 0,
183 'completion_tokens' => 0,
184 'total_tokens' => 0,
185 'returned_price' => 0,
186 'queries' => 0
187 ];
188 }
189 // Ensure queries field exists for existing data
190 if ( !isset( $daily_usage[$day][$model]['queries'] ) ) {
191 $daily_usage[$day][$model]['queries'] = 0;
192 }
193 $daily_usage[$day][$model]['prompt_tokens'] += $in_tokens;
194 $daily_usage[$day][$model]['completion_tokens'] += $out_tokens;
195 $daily_usage[$day][$model]['total_tokens'] += $in_tokens + $out_tokens;
196 $daily_usage[$day][$model]['queries'] += 1;
197 $daily_usage[$day][$model]['returned_price'] += $returned_price;
198
199 // Clean up old daily data (keep only last 30 days)
200 $this->cleanup_old_daily_data( $daily_usage );
201 $this->core->update_option( 'ai_usage_daily', $daily_usage );
202
203 // Return the usage data for this specific request
204 return [
205 'prompt_tokens' => $in_tokens,
206 'completion_tokens' => $out_tokens,
207 'total_tokens' => $in_tokens + $out_tokens,
208 'price' => $returned_price,
209 'queries' => 1
210 ];
211 }
212
213 public function record_audio_usage( $model, $seconds ) {
214 // Record monthly usage
215 $usage = $this->core->get_option( 'ai_usage' );
216 $month = date( 'Y-m' );
217 if ( !isset( $usage[$month] ) ) {
218 $usage[$month] = [];
219 }
220 if ( !isset( $usage[$month][$model] ) ) {
221 $usage[$month][$model] = [ 'seconds' => 0, 'queries' => 0 ];
222 }
223 if ( !isset( $usage[$month][$model]['seconds'] ) ) {
224 $usage[$month][$model]['seconds'] = 0;
225 }
226 if ( !isset( $usage[$month][$model]['queries'] ) ) {
227 $usage[$month][$model]['queries'] = 0;
228 }
229 $usage[$month][$model]['seconds'] += $seconds;
230 $usage[$month][$model]['queries'] += 1;
231 $this->cleanup_old_monthly_data( $usage );
232 $this->core->update_option( 'ai_usage', $usage );
233
234 // Record daily usage
235 $daily_usage = $this->core->get_option( 'ai_usage_daily', [] );
236 $day = date( 'Y-m-d' );
237 if ( !isset( $daily_usage[$day] ) ) {
238 $daily_usage[$day] = [];
239 }
240 if ( !isset( $daily_usage[$day][$model] ) ) {
241 $daily_usage[$day][$model] = [ 'seconds' => 0, 'queries' => 0 ];
242 }
243 if ( !isset( $daily_usage[$day][$model]['seconds'] ) ) {
244 $daily_usage[$day][$model]['seconds'] = 0;
245 }
246 if ( !isset( $daily_usage[$day][$model]['queries'] ) ) {
247 $daily_usage[$day][$model]['queries'] = 0;
248 }
249 $daily_usage[$day][$model]['seconds'] += $seconds;
250 $daily_usage[$day][$model]['queries'] += 1;
251 $this->cleanup_old_daily_data( $daily_usage );
252 $this->core->update_option( 'ai_usage_daily', $daily_usage );
253
254 // Return the usage data for this specific request
255 return [
256 'seconds' => $seconds,
257 'queries' => 1
258 ];
259 }
260
261 public function record_images_usage( $model, $resolution, $images ) {
262 // Record monthly usage
263 $usage = $this->core->get_option( 'ai_usage' );
264 $month = date( 'Y-m' );
265 if ( !isset( $usage[$month] ) ) {
266 $usage[$month] = [];
267 }
268 if ( !isset( $usage[$month][$model] ) ) {
269 $usage[$month][$model] = [ 'resolution' => [], 'images' => 0, 'queries' => 0 ];
270 }
271 if ( !isset( $usage[$month][$model]['images'] ) ) {
272 $usage[$month][$model]['images'] = 0;
273 }
274 if ( !isset( $usage[$month][$model]['resolution'] ) ) {
275 $usage[$month][$model]['resolution'] = [];
276 }
277 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
278 $usage[$month][$model]['resolution'][$resolution] = 0;
279 }
280 if ( !isset( $usage[$month][$model]['queries'] ) ) {
281 $usage[$month][$model]['queries'] = 0;
282 }
283 $usage[$month][$model]['images'] += $images;
284 $usage[$month][$model]['resolution'][$resolution] += $images;
285 $usage[$month][$model]['queries'] += 1;
286 $this->cleanup_old_monthly_data( $usage );
287 $this->core->update_option( 'ai_usage', $usage );
288
289 // Record daily usage
290 $daily_usage = $this->core->get_option( 'ai_usage_daily', [] );
291 $day = date( 'Y-m-d' );
292 if ( !isset( $daily_usage[$day] ) ) {
293 $daily_usage[$day] = [];
294 }
295 if ( !isset( $daily_usage[$day][$model] ) ) {
296 $daily_usage[$day][$model] = [ 'resolution' => [], 'images' => 0, 'queries' => 0 ];
297 }
298 if ( !isset( $daily_usage[$day][$model]['images'] ) ) {
299 $daily_usage[$day][$model]['images'] = 0;
300 }
301 if ( !isset( $daily_usage[$day][$model]['resolution'] ) ) {
302 $daily_usage[$day][$model]['resolution'] = [];
303 }
304 if ( !isset( $daily_usage[$day][$model]['resolution'][$resolution] ) ) {
305 $daily_usage[$day][$model]['resolution'][$resolution] = 0;
306 }
307 if ( !isset( $daily_usage[$day][$model]['queries'] ) ) {
308 $daily_usage[$day][$model]['queries'] = 0;
309 }
310 $daily_usage[$day][$model]['images'] += $images;
311 $daily_usage[$day][$model]['resolution'][$resolution] += $images;
312 $daily_usage[$day][$model]['queries'] += 1;
313 $this->cleanup_old_daily_data( $daily_usage );
314 $this->core->update_option( 'ai_usage_daily', $daily_usage );
315
316 // Return the usage data for this specific request
317 return [
318 'images' => $images,
319 'queries' => 1
320 ];
321 }
322
323 private function cleanup_old_monthly_data( &$usage ) {
324 $two_years_ago = date( 'Y-m', strtotime( '-2 years' ) );
325 foreach ( $usage as $month => $data ) {
326 if ( $month < $two_years_ago ) {
327 unset( $usage[$month] );
328 }
329 }
330 }
331
332 private function cleanup_old_daily_data( &$usage ) {
333 $thirty_days_ago = date( 'Y-m-d', strtotime( '-30 days' ) );
334 foreach ( $usage as $day => $data ) {
335 if ( $day < $thirty_days_ago ) {
336 unset( $usage[$day] );
337 }
338 }
339 }
340 }
341