PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.3.8
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.3.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 7 months ago message-builder.php 8 months ago model-environment.php 7 months ago response-id-manager.php 6 months ago session.php 11 months ago usage-stats.php 6 months ago
usage-stats.php
478 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 // TODO: Remove after April 2026 - Simplify to single argument format
49 public function estimate_tokens( ...$args ) {
50 // Handle multiple argument formats for backward compatibility
51 $text = '';
52 $model = null;
53
54 // If first argument is an array, process messages
55 if ( !empty( $args[0] ) && is_array( $args[0] ) ) {
56 foreach ( $args[0] as $message ) {
57 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : '';
58 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : '';
59 }
60 $model = $args[1] ?? null;
61 }
62 // Otherwise treat first argument as text
63 else {
64 $text = $args[0] ?? '';
65 $model = $args[1] ?? null;
66 }
67
68 // Convert to string if needed
69 if ( !is_string( $text ) ) {
70 // Handle arrays that weren't caught by the first condition
71 if ( is_array( $text ) ) {
72 $text = json_encode( $text );
73 }
74 // Handle objects
75 elseif ( is_object( $text ) ) {
76 $text = method_exists( $text, '__toString' ) ? (string) $text : json_encode( $text );
77 }
78 // Handle other types (int, float, bool, null)
79 else {
80 $text = (string) $text;
81 }
82 }
83
84 // Apply filters for customization
85 $text = apply_filters( 'mwai_estimate_tokens_text', $text, $model );
86 $tokens = apply_filters( 'mwai_estimate_tokens', null, $text, $model );
87 if ( $tokens !== null ) {
88 return $tokens;
89 }
90
91 // Try to use tiktoken for accurate counting (cl100k_base encoder)
92 $encoder = $this->get_tiktoken_encoder();
93 if ( $encoder ) {
94 try {
95 $encoded = $encoder->encode( $text );
96 $token_count = count( $encoded );
97
98 // Comparison logging removed - tiktoken is working correctly
99
100 return $token_count;
101 }
102 catch ( Exception $e ) {
103 error_log( '[AI Engine Tiktoken] Encoding failed, falling back to estimation: ' . $e->getMessage() );
104 }
105 }
106 else {
107 error_log( '[AI Engine Tiktoken] Encoder not available, using fallback' );
108 }
109
110 // Fallback to old estimation method
111 return $this->fallback_estimate_tokens( $text );
112 }
113
114 /**
115 * Fallback token estimation method (the original implementation)
116 */
117 private function fallback_estimate_tokens( $text ) {
118 $multiplier = 4;
119 $hasChineseChars = preg_match( '/[\x{4e00}-\x{9fa5}]/u', $text );
120 $hasJapaneseChars = preg_match( '/[\x{3040}-\x{309f}\x{30a0}-\x{30ff}]/u', $text );
121 $hasKoreanChars = preg_match( '/[\x{ac00}-\x{d7af}]/u', $text );
122 if ( $hasChineseChars || $hasJapaneseChars || $hasKoreanChars ) {
123 $multiplier = 2;
124 }
125 $tokens = (int) ( ( function_exists( 'mb_strlen' ) ? mb_strlen( $text ) : strlen( $text ) ) / $multiplier );
126 return $tokens;
127 }
128
129 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
130 if ( !is_numeric( $in_tokens ) ) {
131 $in_tokens = 0;
132 }
133 if ( !is_numeric( $out_tokens ) ) {
134 $out_tokens = 0;
135 }
136
137 // Normalize returned_price - keep null when price is unavailable
138 $price_for_tracking = 0; // For monthly/daily accumulation
139 if ( !empty( $returned_price ) || $returned_price === 0 ) {
140 $returned_price = is_array( $returned_price ) ?
141 ( isset( $returned_price['price'] ) ? $returned_price['price'] : null ) :
142 ( is_numeric( $returned_price ) ? $returned_price : null );
143 $price_for_tracking = $returned_price ?? 0;
144 } else {
145 $returned_price = null;
146 }
147
148 // Record monthly usage
149 $usage = $this->core->get_option( 'ai_usage' );
150 $month = date( 'Y-m' );
151 if ( !isset( $usage[$month] ) ) {
152 $usage[$month] = [];
153 }
154 if ( !isset( $usage[$month][$model] ) ) {
155 $usage[$month][$model] = [
156 'prompt_tokens' => 0,
157 'completion_tokens' => 0,
158 'total_tokens' => 0,
159 'returned_price' => 0,
160 'queries' => 0
161 ];
162 }
163 // Ensure all token fields exist for existing data
164 if ( !isset( $usage[$month][$model]['prompt_tokens'] ) ) {
165 $usage[$month][$model]['prompt_tokens'] = 0;
166 }
167 if ( !isset( $usage[$month][$model]['completion_tokens'] ) ) {
168 $usage[$month][$model]['completion_tokens'] = 0;
169 }
170 if ( !isset( $usage[$month][$model]['total_tokens'] ) ) {
171 $usage[$month][$model]['total_tokens'] = 0;
172 }
173 if ( !isset( $usage[$month][$model]['returned_price'] ) ) {
174 $usage[$month][$model]['returned_price'] = 0;
175 }
176 if ( !isset( $usage[$month][$model]['queries'] ) ) {
177 $usage[$month][$model]['queries'] = 0;
178 }
179 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
180 $usage[$month][$model]['completion_tokens'] += $out_tokens;
181 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
182 $usage[$month][$model]['queries'] += 1;
183 $usage[$month][$model]['returned_price'] += $price_for_tracking;
184
185 // Clean up old monthly data (keep only last 2 years)
186 $this->cleanup_old_monthly_data( $usage );
187 $this->core->update_option( 'ai_usage', $usage );
188
189 // Record daily usage
190 $daily_usage = $this->core->get_option( 'ai_usage_daily', [] );
191 $day = date( 'Y-m-d' );
192 if ( !isset( $daily_usage[$day] ) ) {
193 $daily_usage[$day] = [];
194 }
195 if ( !isset( $daily_usage[$day][$model] ) ) {
196 $daily_usage[$day][$model] = [
197 'prompt_tokens' => 0,
198 'completion_tokens' => 0,
199 'total_tokens' => 0,
200 'returned_price' => 0,
201 'queries' => 0
202 ];
203 }
204 // Ensure all token fields exist for existing data
205 if ( !isset( $daily_usage[$day][$model]['prompt_tokens'] ) ) {
206 $daily_usage[$day][$model]['prompt_tokens'] = 0;
207 }
208 if ( !isset( $daily_usage[$day][$model]['completion_tokens'] ) ) {
209 $daily_usage[$day][$model]['completion_tokens'] = 0;
210 }
211 if ( !isset( $daily_usage[$day][$model]['total_tokens'] ) ) {
212 $daily_usage[$day][$model]['total_tokens'] = 0;
213 }
214 if ( !isset( $daily_usage[$day][$model]['returned_price'] ) ) {
215 $daily_usage[$day][$model]['returned_price'] = 0;
216 }
217 if ( !isset( $daily_usage[$day][$model]['queries'] ) ) {
218 $daily_usage[$day][$model]['queries'] = 0;
219 }
220 $daily_usage[$day][$model]['prompt_tokens'] += $in_tokens;
221 $daily_usage[$day][$model]['completion_tokens'] += $out_tokens;
222 $daily_usage[$day][$model]['total_tokens'] += $in_tokens + $out_tokens;
223 $daily_usage[$day][$model]['queries'] += 1;
224 $daily_usage[$day][$model]['returned_price'] += $price_for_tracking;
225
226 // Clean up old daily data (keep only last 30 days)
227 $this->cleanup_old_daily_data( $daily_usage );
228 $this->core->update_option( 'ai_usage_daily', $daily_usage );
229
230 // Return the usage data for this specific request
231 return [
232 'prompt_tokens' => $in_tokens,
233 'completion_tokens' => $out_tokens,
234 'total_tokens' => $in_tokens + $out_tokens,
235 'price' => $returned_price,
236 'queries' => 1
237 ];
238 }
239
240 public function record_audio_usage( $model, $seconds ) {
241 // Record monthly usage
242 $usage = $this->core->get_option( 'ai_usage' );
243 $month = date( 'Y-m' );
244 if ( !isset( $usage[$month] ) ) {
245 $usage[$month] = [];
246 }
247 if ( !isset( $usage[$month][$model] ) ) {
248 $usage[$month][$model] = [ 'seconds' => 0, 'queries' => 0 ];
249 }
250 if ( !isset( $usage[$month][$model]['seconds'] ) ) {
251 $usage[$month][$model]['seconds'] = 0;
252 }
253 if ( !isset( $usage[$month][$model]['queries'] ) ) {
254 $usage[$month][$model]['queries'] = 0;
255 }
256 $usage[$month][$model]['seconds'] += $seconds;
257 $usage[$month][$model]['queries'] += 1;
258 $this->cleanup_old_monthly_data( $usage );
259 $this->core->update_option( 'ai_usage', $usage );
260
261 // Record daily usage
262 $daily_usage = $this->core->get_option( 'ai_usage_daily', [] );
263 $day = date( 'Y-m-d' );
264 if ( !isset( $daily_usage[$day] ) ) {
265 $daily_usage[$day] = [];
266 }
267 if ( !isset( $daily_usage[$day][$model] ) ) {
268 $daily_usage[$day][$model] = [ 'seconds' => 0, 'queries' => 0 ];
269 }
270 if ( !isset( $daily_usage[$day][$model]['seconds'] ) ) {
271 $daily_usage[$day][$model]['seconds'] = 0;
272 }
273 if ( !isset( $daily_usage[$day][$model]['queries'] ) ) {
274 $daily_usage[$day][$model]['queries'] = 0;
275 }
276 $daily_usage[$day][$model]['seconds'] += $seconds;
277 $daily_usage[$day][$model]['queries'] += 1;
278 $this->cleanup_old_daily_data( $daily_usage );
279 $this->core->update_option( 'ai_usage_daily', $daily_usage );
280
281 // Return the usage data for this specific request
282 return [
283 'seconds' => $seconds,
284 'queries' => 1
285 ];
286 }
287
288 public function record_images_usage( $model, $resolution, $images ) {
289 // Record monthly usage
290 $usage = $this->core->get_option( 'ai_usage' );
291 $month = date( 'Y-m' );
292 if ( !isset( $usage[$month] ) ) {
293 $usage[$month] = [];
294 }
295 if ( !isset( $usage[$month][$model] ) ) {
296 $usage[$month][$model] = [ 'resolution' => [], 'images' => 0, 'queries' => 0 ];
297 }
298 if ( !isset( $usage[$month][$model]['images'] ) ) {
299 $usage[$month][$model]['images'] = 0;
300 }
301 if ( !isset( $usage[$month][$model]['resolution'] ) ) {
302 $usage[$month][$model]['resolution'] = [];
303 }
304 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
305 $usage[$month][$model]['resolution'][$resolution] = 0;
306 }
307 if ( !isset( $usage[$month][$model]['queries'] ) ) {
308 $usage[$month][$model]['queries'] = 0;
309 }
310 $usage[$month][$model]['images'] += $images;
311 $usage[$month][$model]['resolution'][$resolution] += $images;
312 $usage[$month][$model]['queries'] += 1;
313 $this->cleanup_old_monthly_data( $usage );
314 $this->core->update_option( 'ai_usage', $usage );
315
316 // Record daily usage
317 $daily_usage = $this->core->get_option( 'ai_usage_daily', [] );
318 $day = date( 'Y-m-d' );
319 if ( !isset( $daily_usage[$day] ) ) {
320 $daily_usage[$day] = [];
321 }
322 if ( !isset( $daily_usage[$day][$model] ) ) {
323 $daily_usage[$day][$model] = [ 'resolution' => [], 'images' => 0, 'queries' => 0 ];
324 }
325 if ( !isset( $daily_usage[$day][$model]['images'] ) ) {
326 $daily_usage[$day][$model]['images'] = 0;
327 }
328 if ( !isset( $daily_usage[$day][$model]['resolution'] ) ) {
329 $daily_usage[$day][$model]['resolution'] = [];
330 }
331 if ( !isset( $daily_usage[$day][$model]['resolution'][$resolution] ) ) {
332 $daily_usage[$day][$model]['resolution'][$resolution] = 0;
333 }
334 if ( !isset( $daily_usage[$day][$model]['queries'] ) ) {
335 $daily_usage[$day][$model]['queries'] = 0;
336 }
337 $daily_usage[$day][$model]['images'] += $images;
338 $daily_usage[$day][$model]['resolution'][$resolution] += $images;
339 $daily_usage[$day][$model]['queries'] += 1;
340 $this->cleanup_old_daily_data( $daily_usage );
341 $this->core->update_option( 'ai_usage_daily', $daily_usage );
342
343 // Calculate price based on model and resolution
344 $price = 0;
345 $modelInfo = $this->get_model_info( $model );
346 if ( $modelInfo && isset( $modelInfo['resolutions'] ) ) {
347 foreach ( $modelInfo['resolutions'] as $res ) {
348 if ( $res['name'] === $resolution && isset( $res['price'] ) ) {
349 $price = $res['price'] * $images;
350 break;
351 }
352 }
353 }
354
355 // Return the usage data for this specific request
356 return [
357 'images' => $images,
358 'queries' => 1,
359 'price' => $price,
360 'accuracy' => $price > 0 ? 'price' : 'estimated' // 'price' = calculated from known pricing, not from API
361 ];
362 }
363
364 public function record_videos_usage( $model, $resolution, $seconds ) {
365 // Record monthly usage
366 $usage = $this->core->get_option( 'ai_usage' );
367 $month = date( 'Y-m' );
368 if ( !isset( $usage[$month] ) ) {
369 $usage[$month] = [];
370 }
371 if ( !isset( $usage[$month][$model] ) ) {
372 $usage[$month][$model] = [ 'resolution' => [], 'seconds' => 0, 'queries' => 0 ];
373 }
374 if ( !isset( $usage[$month][$model]['seconds'] ) ) {
375 $usage[$month][$model]['seconds'] = 0;
376 }
377 if ( !isset( $usage[$month][$model]['resolution'] ) ) {
378 $usage[$month][$model]['resolution'] = [];
379 }
380 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
381 $usage[$month][$model]['resolution'][$resolution] = 0;
382 }
383 if ( !isset( $usage[$month][$model]['queries'] ) ) {
384 $usage[$month][$model]['queries'] = 0;
385 }
386 $usage[$month][$model]['seconds'] += $seconds;
387 $usage[$month][$model]['resolution'][$resolution] += $seconds;
388 $usage[$month][$model]['queries'] += 1;
389 $this->cleanup_old_monthly_data( $usage );
390 $this->core->update_option( 'ai_usage', $usage );
391
392 // Record daily usage
393 $daily_usage = $this->core->get_option( 'ai_usage_daily', [] );
394 $day = date( 'Y-m-d' );
395 if ( !isset( $daily_usage[$day] ) ) {
396 $daily_usage[$day] = [];
397 }
398 if ( !isset( $daily_usage[$day][$model] ) ) {
399 $daily_usage[$day][$model] = [ 'resolution' => [], 'seconds' => 0, 'queries' => 0 ];
400 }
401 if ( !isset( $daily_usage[$day][$model]['seconds'] ) ) {
402 $daily_usage[$day][$model]['seconds'] = 0;
403 }
404 if ( !isset( $daily_usage[$day][$model]['resolution'] ) ) {
405 $daily_usage[$day][$model]['resolution'] = [];
406 }
407 if ( !isset( $daily_usage[$day][$model]['resolution'][$resolution] ) ) {
408 $daily_usage[$day][$model]['resolution'][$resolution] = 0;
409 }
410 if ( !isset( $daily_usage[$day][$model]['queries'] ) ) {
411 $daily_usage[$day][$model]['queries'] = 0;
412 }
413 $daily_usage[$day][$model]['seconds'] += $seconds;
414 $daily_usage[$day][$model]['resolution'][$resolution] += $seconds;
415 $daily_usage[$day][$model]['queries'] += 1;
416 $this->cleanup_old_daily_data( $daily_usage );
417 $this->core->update_option( 'ai_usage_daily', $daily_usage );
418
419 // Calculate price based on model, resolution, and seconds
420 $price = 0;
421 $modelInfo = $this->get_model_info( $model );
422 if ( $modelInfo && isset( $modelInfo['resolutions'] ) ) {
423 foreach ( $modelInfo['resolutions'] as $res ) {
424 if ( $res['name'] === $resolution && isset( $res['price'] ) ) {
425 // Price is per second for video models
426 $price = $res['price'] * $seconds;
427 break;
428 }
429 }
430 }
431
432 // Return the usage data for this specific request
433 return [
434 'seconds' => $seconds,
435 'queries' => 1,
436 'price' => $price,
437 'accuracy' => $price > 0 ? 'price' : 'estimated' // 'price' = calculated from known pricing, not from API
438 ];
439 }
440
441 private function get_model_info( $model ) {
442 $engines = $this->core->get_option( 'ai_engines' );
443 if ( !$engines || !is_array( $engines ) ) {
444 return null;
445 }
446
447 foreach ( $engines as $engine ) {
448 if ( isset( $engine['models'] ) && is_array( $engine['models'] ) ) {
449 foreach ( $engine['models'] as $modelInfo ) {
450 if ( isset( $modelInfo['model'] ) && $modelInfo['model'] === $model ) {
451 return $modelInfo;
452 }
453 }
454 }
455 }
456
457 return null;
458 }
459
460 private function cleanup_old_monthly_data( &$usage ) {
461 $two_years_ago = date( 'Y-m', strtotime( '-2 years' ) );
462 foreach ( $usage as $month => $data ) {
463 if ( $month < $two_years_ago ) {
464 unset( $usage[$month] );
465 }
466 }
467 }
468
469 private function cleanup_old_daily_data( &$usage ) {
470 $thirty_days_ago = date( 'Y-m-d', strtotime( '-30 days' ) );
471 foreach ( $usage as $day => $data ) {
472 if ( $day < $thirty_days_ago ) {
473 unset( $usage[$day] );
474 }
475 }
476 }
477 }
478