PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.1.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.1.2
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 8 months ago message-builder.php 8 months ago model-environment.php 8 months ago response-id-manager.php 11 months ago session.php 11 months ago usage-stats.php 8 months ago
usage-stats.php
477 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 - keep null when price is unavailable
137 $price_for_tracking = 0; // For monthly/daily accumulation
138 if ( !empty( $returned_price ) || $returned_price === 0 ) {
139 $returned_price = is_array( $returned_price ) ?
140 ( isset( $returned_price['price'] ) ? $returned_price['price'] : null ) :
141 ( is_numeric( $returned_price ) ? $returned_price : null );
142 $price_for_tracking = $returned_price ?? 0;
143 } else {
144 $returned_price = null;
145 }
146
147 // Record monthly usage
148 $usage = $this->core->get_option( 'ai_usage' );
149 $month = date( 'Y-m' );
150 if ( !isset( $usage[$month] ) ) {
151 $usage[$month] = [];
152 }
153 if ( !isset( $usage[$month][$model] ) ) {
154 $usage[$month][$model] = [
155 'prompt_tokens' => 0,
156 'completion_tokens' => 0,
157 'total_tokens' => 0,
158 'returned_price' => 0,
159 'queries' => 0
160 ];
161 }
162 // Ensure all token fields exist for existing data
163 if ( !isset( $usage[$month][$model]['prompt_tokens'] ) ) {
164 $usage[$month][$model]['prompt_tokens'] = 0;
165 }
166 if ( !isset( $usage[$month][$model]['completion_tokens'] ) ) {
167 $usage[$month][$model]['completion_tokens'] = 0;
168 }
169 if ( !isset( $usage[$month][$model]['total_tokens'] ) ) {
170 $usage[$month][$model]['total_tokens'] = 0;
171 }
172 if ( !isset( $usage[$month][$model]['returned_price'] ) ) {
173 $usage[$month][$model]['returned_price'] = 0;
174 }
175 if ( !isset( $usage[$month][$model]['queries'] ) ) {
176 $usage[$month][$model]['queries'] = 0;
177 }
178 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
179 $usage[$month][$model]['completion_tokens'] += $out_tokens;
180 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
181 $usage[$month][$model]['queries'] += 1;
182 $usage[$month][$model]['returned_price'] += $price_for_tracking;
183
184 // Clean up old monthly data (keep only last 2 years)
185 $this->cleanup_old_monthly_data( $usage );
186 $this->core->update_option( 'ai_usage', $usage );
187
188 // Record daily usage
189 $daily_usage = $this->core->get_option( 'ai_usage_daily', [] );
190 $day = date( 'Y-m-d' );
191 if ( !isset( $daily_usage[$day] ) ) {
192 $daily_usage[$day] = [];
193 }
194 if ( !isset( $daily_usage[$day][$model] ) ) {
195 $daily_usage[$day][$model] = [
196 'prompt_tokens' => 0,
197 'completion_tokens' => 0,
198 'total_tokens' => 0,
199 'returned_price' => 0,
200 'queries' => 0
201 ];
202 }
203 // Ensure all token fields exist for existing data
204 if ( !isset( $daily_usage[$day][$model]['prompt_tokens'] ) ) {
205 $daily_usage[$day][$model]['prompt_tokens'] = 0;
206 }
207 if ( !isset( $daily_usage[$day][$model]['completion_tokens'] ) ) {
208 $daily_usage[$day][$model]['completion_tokens'] = 0;
209 }
210 if ( !isset( $daily_usage[$day][$model]['total_tokens'] ) ) {
211 $daily_usage[$day][$model]['total_tokens'] = 0;
212 }
213 if ( !isset( $daily_usage[$day][$model]['returned_price'] ) ) {
214 $daily_usage[$day][$model]['returned_price'] = 0;
215 }
216 if ( !isset( $daily_usage[$day][$model]['queries'] ) ) {
217 $daily_usage[$day][$model]['queries'] = 0;
218 }
219 $daily_usage[$day][$model]['prompt_tokens'] += $in_tokens;
220 $daily_usage[$day][$model]['completion_tokens'] += $out_tokens;
221 $daily_usage[$day][$model]['total_tokens'] += $in_tokens + $out_tokens;
222 $daily_usage[$day][$model]['queries'] += 1;
223 $daily_usage[$day][$model]['returned_price'] += $price_for_tracking;
224
225 // Clean up old daily data (keep only last 30 days)
226 $this->cleanup_old_daily_data( $daily_usage );
227 $this->core->update_option( 'ai_usage_daily', $daily_usage );
228
229 // Return the usage data for this specific request
230 return [
231 'prompt_tokens' => $in_tokens,
232 'completion_tokens' => $out_tokens,
233 'total_tokens' => $in_tokens + $out_tokens,
234 'price' => $returned_price,
235 'queries' => 1
236 ];
237 }
238
239 public function record_audio_usage( $model, $seconds ) {
240 // Record monthly usage
241 $usage = $this->core->get_option( 'ai_usage' );
242 $month = date( 'Y-m' );
243 if ( !isset( $usage[$month] ) ) {
244 $usage[$month] = [];
245 }
246 if ( !isset( $usage[$month][$model] ) ) {
247 $usage[$month][$model] = [ 'seconds' => 0, 'queries' => 0 ];
248 }
249 if ( !isset( $usage[$month][$model]['seconds'] ) ) {
250 $usage[$month][$model]['seconds'] = 0;
251 }
252 if ( !isset( $usage[$month][$model]['queries'] ) ) {
253 $usage[$month][$model]['queries'] = 0;
254 }
255 $usage[$month][$model]['seconds'] += $seconds;
256 $usage[$month][$model]['queries'] += 1;
257 $this->cleanup_old_monthly_data( $usage );
258 $this->core->update_option( 'ai_usage', $usage );
259
260 // Record daily usage
261 $daily_usage = $this->core->get_option( 'ai_usage_daily', [] );
262 $day = date( 'Y-m-d' );
263 if ( !isset( $daily_usage[$day] ) ) {
264 $daily_usage[$day] = [];
265 }
266 if ( !isset( $daily_usage[$day][$model] ) ) {
267 $daily_usage[$day][$model] = [ 'seconds' => 0, 'queries' => 0 ];
268 }
269 if ( !isset( $daily_usage[$day][$model]['seconds'] ) ) {
270 $daily_usage[$day][$model]['seconds'] = 0;
271 }
272 if ( !isset( $daily_usage[$day][$model]['queries'] ) ) {
273 $daily_usage[$day][$model]['queries'] = 0;
274 }
275 $daily_usage[$day][$model]['seconds'] += $seconds;
276 $daily_usage[$day][$model]['queries'] += 1;
277 $this->cleanup_old_daily_data( $daily_usage );
278 $this->core->update_option( 'ai_usage_daily', $daily_usage );
279
280 // Return the usage data for this specific request
281 return [
282 'seconds' => $seconds,
283 'queries' => 1
284 ];
285 }
286
287 public function record_images_usage( $model, $resolution, $images ) {
288 // Record monthly usage
289 $usage = $this->core->get_option( 'ai_usage' );
290 $month = date( 'Y-m' );
291 if ( !isset( $usage[$month] ) ) {
292 $usage[$month] = [];
293 }
294 if ( !isset( $usage[$month][$model] ) ) {
295 $usage[$month][$model] = [ 'resolution' => [], 'images' => 0, 'queries' => 0 ];
296 }
297 if ( !isset( $usage[$month][$model]['images'] ) ) {
298 $usage[$month][$model]['images'] = 0;
299 }
300 if ( !isset( $usage[$month][$model]['resolution'] ) ) {
301 $usage[$month][$model]['resolution'] = [];
302 }
303 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
304 $usage[$month][$model]['resolution'][$resolution] = 0;
305 }
306 if ( !isset( $usage[$month][$model]['queries'] ) ) {
307 $usage[$month][$model]['queries'] = 0;
308 }
309 $usage[$month][$model]['images'] += $images;
310 $usage[$month][$model]['resolution'][$resolution] += $images;
311 $usage[$month][$model]['queries'] += 1;
312 $this->cleanup_old_monthly_data( $usage );
313 $this->core->update_option( 'ai_usage', $usage );
314
315 // Record daily usage
316 $daily_usage = $this->core->get_option( 'ai_usage_daily', [] );
317 $day = date( 'Y-m-d' );
318 if ( !isset( $daily_usage[$day] ) ) {
319 $daily_usage[$day] = [];
320 }
321 if ( !isset( $daily_usage[$day][$model] ) ) {
322 $daily_usage[$day][$model] = [ 'resolution' => [], 'images' => 0, 'queries' => 0 ];
323 }
324 if ( !isset( $daily_usage[$day][$model]['images'] ) ) {
325 $daily_usage[$day][$model]['images'] = 0;
326 }
327 if ( !isset( $daily_usage[$day][$model]['resolution'] ) ) {
328 $daily_usage[$day][$model]['resolution'] = [];
329 }
330 if ( !isset( $daily_usage[$day][$model]['resolution'][$resolution] ) ) {
331 $daily_usage[$day][$model]['resolution'][$resolution] = 0;
332 }
333 if ( !isset( $daily_usage[$day][$model]['queries'] ) ) {
334 $daily_usage[$day][$model]['queries'] = 0;
335 }
336 $daily_usage[$day][$model]['images'] += $images;
337 $daily_usage[$day][$model]['resolution'][$resolution] += $images;
338 $daily_usage[$day][$model]['queries'] += 1;
339 $this->cleanup_old_daily_data( $daily_usage );
340 $this->core->update_option( 'ai_usage_daily', $daily_usage );
341
342 // Calculate price based on model and resolution
343 $price = 0;
344 $modelInfo = $this->get_model_info( $model );
345 if ( $modelInfo && isset( $modelInfo['resolutions'] ) ) {
346 foreach ( $modelInfo['resolutions'] as $res ) {
347 if ( $res['name'] === $resolution && isset( $res['price'] ) ) {
348 $price = $res['price'] * $images;
349 break;
350 }
351 }
352 }
353
354 // Return the usage data for this specific request
355 return [
356 'images' => $images,
357 'queries' => 1,
358 'price' => $price,
359 'accuracy' => $price > 0 ? 'price' : 'estimated' // 'price' = calculated from known pricing, not from API
360 ];
361 }
362
363 public function record_videos_usage( $model, $resolution, $seconds ) {
364 // Record monthly usage
365 $usage = $this->core->get_option( 'ai_usage' );
366 $month = date( 'Y-m' );
367 if ( !isset( $usage[$month] ) ) {
368 $usage[$month] = [];
369 }
370 if ( !isset( $usage[$month][$model] ) ) {
371 $usage[$month][$model] = [ 'resolution' => [], 'seconds' => 0, 'queries' => 0 ];
372 }
373 if ( !isset( $usage[$month][$model]['seconds'] ) ) {
374 $usage[$month][$model]['seconds'] = 0;
375 }
376 if ( !isset( $usage[$month][$model]['resolution'] ) ) {
377 $usage[$month][$model]['resolution'] = [];
378 }
379 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
380 $usage[$month][$model]['resolution'][$resolution] = 0;
381 }
382 if ( !isset( $usage[$month][$model]['queries'] ) ) {
383 $usage[$month][$model]['queries'] = 0;
384 }
385 $usage[$month][$model]['seconds'] += $seconds;
386 $usage[$month][$model]['resolution'][$resolution] += $seconds;
387 $usage[$month][$model]['queries'] += 1;
388 $this->cleanup_old_monthly_data( $usage );
389 $this->core->update_option( 'ai_usage', $usage );
390
391 // Record daily usage
392 $daily_usage = $this->core->get_option( 'ai_usage_daily', [] );
393 $day = date( 'Y-m-d' );
394 if ( !isset( $daily_usage[$day] ) ) {
395 $daily_usage[$day] = [];
396 }
397 if ( !isset( $daily_usage[$day][$model] ) ) {
398 $daily_usage[$day][$model] = [ 'resolution' => [], 'seconds' => 0, 'queries' => 0 ];
399 }
400 if ( !isset( $daily_usage[$day][$model]['seconds'] ) ) {
401 $daily_usage[$day][$model]['seconds'] = 0;
402 }
403 if ( !isset( $daily_usage[$day][$model]['resolution'] ) ) {
404 $daily_usage[$day][$model]['resolution'] = [];
405 }
406 if ( !isset( $daily_usage[$day][$model]['resolution'][$resolution] ) ) {
407 $daily_usage[$day][$model]['resolution'][$resolution] = 0;
408 }
409 if ( !isset( $daily_usage[$day][$model]['queries'] ) ) {
410 $daily_usage[$day][$model]['queries'] = 0;
411 }
412 $daily_usage[$day][$model]['seconds'] += $seconds;
413 $daily_usage[$day][$model]['resolution'][$resolution] += $seconds;
414 $daily_usage[$day][$model]['queries'] += 1;
415 $this->cleanup_old_daily_data( $daily_usage );
416 $this->core->update_option( 'ai_usage_daily', $daily_usage );
417
418 // Calculate price based on model, resolution, and seconds
419 $price = 0;
420 $modelInfo = $this->get_model_info( $model );
421 if ( $modelInfo && isset( $modelInfo['resolutions'] ) ) {
422 foreach ( $modelInfo['resolutions'] as $res ) {
423 if ( $res['name'] === $resolution && isset( $res['price'] ) ) {
424 // Price is per second for video models
425 $price = $res['price'] * $seconds;
426 break;
427 }
428 }
429 }
430
431 // Return the usage data for this specific request
432 return [
433 'seconds' => $seconds,
434 'queries' => 1,
435 'price' => $price,
436 'accuracy' => $price > 0 ? 'price' : 'estimated' // 'price' = calculated from known pricing, not from API
437 ];
438 }
439
440 private function get_model_info( $model ) {
441 $engines = $this->core->get_option( 'ai_engines' );
442 if ( !$engines || !is_array( $engines ) ) {
443 return null;
444 }
445
446 foreach ( $engines as $engine ) {
447 if ( isset( $engine['models'] ) && is_array( $engine['models'] ) ) {
448 foreach ( $engine['models'] as $modelInfo ) {
449 if ( isset( $modelInfo['model'] ) && $modelInfo['model'] === $model ) {
450 return $modelInfo;
451 }
452 }
453 }
454 }
455
456 return null;
457 }
458
459 private function cleanup_old_monthly_data( &$usage ) {
460 $two_years_ago = date( 'Y-m', strtotime( '-2 years' ) );
461 foreach ( $usage as $month => $data ) {
462 if ( $month < $two_years_ago ) {
463 unset( $usage[$month] );
464 }
465 }
466 }
467
468 private function cleanup_old_daily_data( &$usage ) {
469 $thirty_days_ago = date( 'Y-m-d', strtotime( '-30 days' ) );
470 foreach ( $usage as $day => $data ) {
471 if ( $day < $thirty_days_ago ) {
472 unset( $usage[$day] );
473 }
474 }
475 }
476 }
477