PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / trunk
AI Engine – The Chatbot, AI Framework & MCP for WordPress vtrunk
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 day ago message-builder.php 3 weeks ago model-environment.php 7 months ago response-id-manager.php 1 day ago session.php 11 months ago usage-stats.php 1 month ago
usage-stats.php
478 lines
1 <?php
2
3 /**
4 * Usage tracking lives in two WP options:
5 * - ai_usage: monthly totals, keyed by 'YYYY-MM' then by model id
6 * - ai_usage_daily: daily totals, keyed by 'YYYY-MM-DD' then by model id
7 *
8 * Each per-model entry uses one of three shapes, depending on the billing
9 * unit returned by the provider:
10 *
11 * token-billed (default, most providers including OpenAI/Anthropic/Gemini
12 * text and image-gen): { prompt_tokens, completion_tokens, total_tokens,
13 * returned_price, queries }
14 *
15 * per-image (legacy Imagen, Replicate per-image models, OAI-compatible
16 * custom servers without `usage`): { resolution: {…}, images, queries }
17 *
18 * per-second (Whisper, Sora, Realtime audio fallback): { seconds, queries }
19 * per-second + resolution (Sora video): { resolution: {…}, seconds, queries }
20 *
21 * Token-billed is the canonical shape and what we encourage going forward.
22 * The user-facing label across the admin UI is "Tokens" (see Migration plan
23 * in this repo). The `unit`/`units` terminology is legacy and retained only
24 * for back-compat (DB column on wp_mwai_logs, the Reply::get_units() public
25 * helper, and dual-emit fields in the stats REST output).
26 */
27 class Meow_MWAI_Services_UsageStats {
28 private $core;
29 private $tiktoken_encoders = [];
30 private $encoder_provider = null;
31
32 public function __construct( $core ) {
33 $this->core = $core;
34 }
35
36 /**
37 * Get the cl100k_base tiktoken encoder
38 * Note: We always use cl100k_base regardless of model since it's the standard for all modern OpenAI models
39 * @return object|null The tiktoken encoder or null if not available
40 */
41 private function get_tiktoken_encoder() {
42 // Return cached encoder if available
43 if ( isset( $this->tiktoken_encoders['cl100k_base'] ) ) {
44 return $this->tiktoken_encoders['cl100k_base'];
45 }
46
47 try {
48 // Check if class exists
49 if ( !class_exists( 'Yethee\Tiktoken\EncoderProvider' ) ) {
50 error_log( '[AI Engine Tiktoken] EncoderProvider class not found. Check if composer autoload is working.' );
51 return null;
52 }
53
54 // Initialize encoder provider if needed
55 if ( $this->encoder_provider === null ) {
56 $this->encoder_provider = new \Yethee\Tiktoken\EncoderProvider();
57 // The library defaults its vocab cache to {sys_temp}/tiktoken — a fixed shared
58 // path. On multi-tenant hosts where each site runs as a different uid, the first
59 // site to create the directory owns it and other sites get EACCES on write. Scope
60 // the cache by ABSPATH so every install gets its own writable directory.
61 $cacheDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'tiktoken-' . substr( md5( ABSPATH ), 0, 12 );
62 $this->encoder_provider->setVocabCache( $cacheDir );
63 }
64
65 // Get the cl100k_base encoder (standard for modern OpenAI models)
66 $encoder = $this->encoder_provider->get( 'cl100k_base' );
67 $this->tiktoken_encoders['cl100k_base'] = $encoder;
68
69 // Removed success log to reduce noise
70 return $encoder;
71 }
72 catch ( \Exception $e ) {
73 error_log( '[AI Engine Tiktoken] Failed to initialize encoder: ' . $e->getMessage() );
74 return null;
75 }
76 }
77
78 public function estimate_tokens( $text = '', $model = null ) {
79 if ( !is_string( $text ) ) {
80 $text = is_array( $text ) || is_object( $text ) ? json_encode( $text ) : (string) $text;
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 }
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