date('Y-m-d'), 'cost' => self::$dailyCost, 'updated_at' => date('Y-m-d H:i:s') ]; @file_put_contents(self::$dailyCostFile, json_encode($costData), LOCK_EX); } } private static function trackCost($usage) { if (!defined('ENABLE_COST_TRACKING') || !ENABLE_COST_TRACKING || !$usage) { return; } $promptTokens = $usage['prompt_tokens'] ?? 0; $completionTokens = $usage['completion_tokens'] ?? 0; $totalTokens = $usage['total_tokens'] ?? ($promptTokens + $completionTokens); // Calculate cost based on model $model = GPT_MODEL; $costPer1kTokens = GPT35_COST_PER_1K_TOKENS; // Default to GPT-3.5 pricing if (strpos($model, 'gpt-4') !== false) { $costPer1kTokens = GPT4_COST_PER_1K_TOKENS; } $requestCost = ($totalTokens / 1000) * $costPer1kTokens; self::$dailyCost += $requestCost; self::costLog("Request cost: $" . number_format($requestCost, 4) . " (Tokens: $totalTokens, Model: $model, Daily total: $" . number_format(self::$dailyCost, 2) . ")"); self::saveDailyCost(); // Check if we've exceeded daily cost limit if (defined('MAX_DAILY_COST') && self::$dailyCost > MAX_DAILY_COST) { throw new Exception("Daily cost limit exceeded ($" . number_format(MAX_DAILY_COST, 2) . "). Processing stopped to prevent overspending."); } // Warning threshold if (defined('COST_WARNING_THRESHOLD') && self::$dailyCost > COST_WARNING_THRESHOLD) { self::debugLog("WARNING: Daily cost ($" . number_format(self::$dailyCost, 2) . ") approaching limit ($" . number_format(MAX_DAILY_COST, 2) . ")"); } } private static function loadCache() { if (!defined('ENABLE_COMBINATION_CACHE') || !ENABLE_COMBINATION_CACHE) { return; } if (file_exists(self::$cacheFile)) { $cacheData = @json_decode(file_get_contents(self::$cacheFile), true); if ($cacheData && is_array($cacheData)) { self::$combinationCache = $cacheData; // Clean expired cache entries $expiry = time() - (defined('CACHE_EXPIRY_HOURS') ? CACHE_EXPIRY_HOURS * 3600 : 24 * 3600); $cleaned = 0; foreach (self::$combinationCache as $key => $entry) { if (!isset($entry['timestamp']) || $entry['timestamp'] < $expiry) { unset(self::$combinationCache[$key]); $cleaned++; } } if ($cleaned > 0) { self::debugLog("Cleaned $cleaned expired cache entries"); self::saveCache(); } // Limit cache size $maxEntries = defined('MAX_CACHE_ENTRIES') ? MAX_CACHE_ENTRIES : 10000; if (count(self::$combinationCache) > $maxEntries) { $sortedCache = self::$combinationCache; uasort($sortedCache, function($a, $b) { return ($b['timestamp'] ?? 0) - ($a['timestamp'] ?? 0); }); self::$combinationCache = array_slice($sortedCache, 0, $maxEntries, true); self::saveCache(); self::debugLog("Cache size limited to $maxEntries entries"); } } } } private static function saveCache() { if (!defined('ENABLE_COMBINATION_CACHE') || !ENABLE_COMBINATION_CACHE) { return; } @file_put_contents(self::$cacheFile, json_encode(self::$combinationCache), LOCK_EX); } private static function getCacheKey($attr1, $choice1, $attr2, $choice2) { // Create normalized cache key (order independent) $combinations = [ strtolower(trim($attr1)) . '=' . strtolower(trim($choice1)), strtolower(trim($attr2)) . '=' . strtolower(trim($choice2)) ]; sort($combinations); return md5(implode('|', $combinations)); } private static function enforceRateLimit($isOptimAIze = false) { $requestsPerMinute = $isOptimAIze && defined('OPTIMAIZE_REQUESTS_PER_MINUTE') ? OPTIMAIZE_REQUESTS_PER_MINUTE : GPT_REQUESTS_PER_MINUTE; $currentTime = time(); // Reset window if a minute has passed if ($currentTime - self::$windowStartTime >= 60) { self::$windowStartTime = $currentTime; self::$requestCount = 0; self::debugLog("Rate limit window reset. Requests per minute limit: $requestsPerMinute"); } // Check if we've hit the rate limit if (self::$requestCount >= $requestsPerMinute) { $waitTime = 60 - ($currentTime - self::$windowStartTime) + 5; // Add 5 second buffer self::debugLog("Rate limit reached ($requestsPerMinute/$requestsPerMinute). Waiting $waitTime seconds..."); sleep($waitTime); // Reset after waiting self::$windowStartTime = time(); self::$requestCount = 0; } // Ensure minimum time between requests $minInterval = 60 / $requestsPerMinute; $timeSinceLastRequest = $currentTime - self::$lastRequestTime; if ($timeSinceLastRequest < $minInterval) { $waitTime = ceil($minInterval - $timeSinceLastRequest); self::debugLog("Enforcing minimum interval: waiting $waitTime seconds"); sleep($waitTime); } self::$lastRequestTime = time(); self::$requestCount++; self::debugLog("Request #{self::$requestCount} in current window"); } public static function getRateLimitStatus($isOptimAIze = false) { $requestsPerMinute = $isOptimAIze && defined('OPTIMAIZE_REQUESTS_PER_MINUTE') ? OPTIMAIZE_REQUESTS_PER_MINUTE : GPT_REQUESTS_PER_MINUTE; $currentTime = time(); // Reset window if a minute has passed if ($currentTime - self::$windowStartTime >= 60) { self::$windowStartTime = $currentTime; self::$requestCount = 0; } $canMakeRequest = self::$requestCount < $requestsPerMinute; $cooldownRemaining = $canMakeRequest ? 0 : (60 - ($currentTime - self::$windowStartTime)); return [ 'can_make_request' => $canMakeRequest, 'requests_made' => self::$requestCount, 'requests_limit' => $requestsPerMinute, 'cooldown_remaining' => $cooldownRemaining, 'window_start' => self::$windowStartTime, 'rate_limit_hits' => self::$rateLimitHits, 'daily_cost' => self::$dailyCost, 'cost_limit' => defined('MAX_DAILY_COST') ? MAX_DAILY_COST : 0 ]; } public static function resetRateLimit() { self::$requestCount = 0; self::$windowStartTime = time(); self::$lastRequestTime = 0; self::$rateLimitHits = 0; self::debugLog("Rate limit counters reset"); } public static function makeRequest($messages, $model = null, $temperature = null, $isOptimAIze = false) { self::init(); // Check daily cost limit before making request if (defined('ENABLE_COST_TRACKING') && ENABLE_COST_TRACKING && defined('MAX_DAILY_COST') && self::$dailyCost >= MAX_DAILY_COST) { return [ 'success' => false, 'error' => 'Daily cost limit reached ($' . number_format(MAX_DAILY_COST, 2) . '). Processing stopped.', 'response' => null, 'cost_limited' => true ]; } $maxRetries = GPT_MAX_RETRIES; $retryDelay = GPT_RETRY_DELAY; for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { try { // Enforce rate limiting before each attempt self::enforceRateLimit($isOptimAIze); // Use default values if not provided $model = $model ?? GPT_MODEL; $temperature = $temperature ?? GPT_TEMPERATURE; $data = [ 'model' => $model, 'messages' => $messages, 'max_tokens' => GPT_MAX_TOKENS, 'temperature' => $temperature, 'top_p' => 1, 'frequency_penalty' => 0, 'presence_penalty' => 0 ]; self::debugLog("Making OpenAI API request (attempt $attempt/$maxRetries, model: $model)"); $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => GPT_API_ENDPOINT, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($data), CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . OPENAI_API_KEY ], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, CURLOPT_CONNECTTIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_HEADER => false ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); self::debugLog("API response: HTTP $httpCode"); if ($curlError) { throw new Exception("CURL Error: " . $curlError); } if ($response === false) { throw new Exception("Failed to get response from OpenAI API"); } $decodedResponse = json_decode($response, true); if ($httpCode === 429) { self::$rateLimitHits++; $errorMessage = isset($decodedResponse['error']['message']) ? $decodedResponse['error']['message'] : 'Rate limit exceeded'; self::debugLog("Rate limit hit #" . self::$rateLimitHits . ": $errorMessage"); // Check if it's a quota/billing issue if (stripos($errorMessage, 'quota') !== false || stripos($errorMessage, 'billing') !== false) { return [ 'success' => false, 'error' => 'OpenAI quota/billing limit exceeded. Please check your account billing.', 'response' => null, 'quota_exceeded' => true ]; } if ($attempt < $maxRetries) { $waitTime = $retryDelay * $attempt; // Exponential backoff self::debugLog("Waiting $waitTime seconds before retry..."); sleep($waitTime); continue; } else { throw new Exception("OpenAI rate limit exceeded after $maxRetries attempts. Please wait and try again."); } } if ($httpCode !== 200) { $errorMessage = isset($decodedResponse['error']['message']) ? $decodedResponse['error']['message'] : 'Unknown API error'; // Check for quota/billing errors if (stripos($errorMessage, 'quota') !== false || stripos($errorMessage, 'billing') !== false) { return [ 'success' => false, 'error' => 'OpenAI quota/billing limit exceeded. Please check your account billing.', 'response' => null, 'quota_exceeded' => true ]; } throw new Exception("OpenAI API Error (HTTP $httpCode): " . $errorMessage); } if (!isset($decodedResponse['choices'][0]['message']['content'])) { throw new Exception("Invalid response format from OpenAI API"); } $content = trim($decodedResponse['choices'][0]['message']['content']); $usage = $decodedResponse['usage'] ?? null; // Track cost if ($usage) { self::trackCost($usage); } self::debugLog("API request successful (Daily cost: $" . number_format(self::$dailyCost, 2) . ")"); return [ 'success' => true, 'response' => $content, 'usage' => $usage, 'attempt' => $attempt, 'daily_cost' => self::$dailyCost, 'model_used' => $model ]; } catch (Exception $e) { self::debugLog("Attempt $attempt failed: " . $e->getMessage()); if ($attempt >= $maxRetries || strpos($e->getMessage(), 'CURL Error') !== false) { break; // Don't retry on CURL errors or after max retries } // Wait before retry (but not on last attempt) if ($attempt < $maxRetries) { sleep($retryDelay); } } } // If we get here, all attempts failed $finalError = isset($e) ? $e->getMessage() : 'Unknown error after all retry attempts'; self::debugLog("All retry attempts failed: $finalError"); return [ 'success' => false, 'error' => $finalError, 'response' => null, 'attempts_made' => $maxRetries, 'daily_cost' => self::$dailyCost ]; } /** * Cost-effective combination analysis with caching */ public static function analyzeCombination($attr1, $choice1, $attr2, $choice2) { self::init(); // Check cache first (this saves money!) $cacheKey = self::getCacheKey($attr1, $choice1, $attr2, $choice2); if (defined('ENABLE_COMBINATION_CACHE') && ENABLE_COMBINATION_CACHE && isset(self::$combinationCache[$cacheKey])) { $cached = self::$combinationCache[$cacheKey]; $expiry = time() - (defined('CACHE_EXPIRY_HOURS') ? CACHE_EXPIRY_HOURS * 3600 : 24 * 3600); if (($cached['timestamp'] ?? 0) > $expiry) { self::debugLog("Cache hit for: $attr1=$choice1 + $attr2=$choice2 (Cost saved!)"); return [ 'is_impossible' => $cached['is_impossible'] ?? false, 'reasoning' => $cached['reasoning'] ?? 'Cached result', 'cached' => true, 'cost_saved' => true ]; } else { unset(self::$combinationCache[$cacheKey]); } } // Prepare cost-effective GPT request $messages = [ [ 'role' => 'system', 'content' => 'Analyze demographic combinations for logical impossibilities. Be concise. Answer: IMPOSSIBLE or POSSIBLE with brief reason.' ], [ 'role' => 'user', 'content' => "Analyze: $attr1='$choice1' + $attr2='$choice2'\n" . "Is this logically impossible?\n" . "Format: RESULT: [IMPOSSIBLE/POSSIBLE]\nREASON: [brief explanation]" ] ]; // Make request with cost tracking $response = self::makeRequest($messages, GPT_MODEL, 0.1, true); // Very low temperature for consistency if (!$response['success']) { if (isset($response['quota_exceeded']) && $response['quota_exceeded']) { throw new Exception("OpenAI billing quota exceeded. Please add credits to your account."); } if (isset($response['cost_limited']) && $response['cost_limited']) { throw new Exception("Daily cost limit reached. Processing paused to prevent overspending."); } throw new Exception("GPT analysis failed: " . $response['error']); } // Parse response $content = $response['response']; $isImpossible = false; $reasoning = "Unable to determine from response"; if (preg_match('/RESULT:\s*(IMPOSSIBLE|POSSIBLE)/i', $content, $matches)) { $isImpossible = strtoupper($matches[1]) === 'IMPOSSIBLE'; } elseif (preg_match('/(IMPOSSIBLE|POSSIBLE)/i', $content, $matches)) { $isImpossible = strtoupper($matches[1]) === 'IMPOSSIBLE'; } if (preg_match('/REASON:\s*(.+?)(?:\n|$)/i', $content, $matches)) { $reasoning = trim($matches[1]); } elseif (preg_match('/REASONING:\s*(.+?)(?:\n|$)/i', $content, $matches)) { $reasoning = trim($matches[1]); } $result = [ 'is_impossible' => $isImpossible, 'reasoning' => $reasoning, 'cached' => false, 'daily_cost' => $response['daily_cost'] ?? 0, 'model_used' => $response['model_used'] ?? GPT_MODEL ]; // Cache the result to save future costs if (defined('ENABLE_COMBINATION_CACHE') && ENABLE_COMBINATION_CACHE) { self::$combinationCache[$cacheKey] = [ 'is_impossible' => $isImpossible, 'reasoning' => $reasoning, 'timestamp' => time(), 'combination' => "$attr1=$choice1 + $attr2=$choice2", 'model_used' => $response['model_used'] ?? GPT_MODEL ]; self::saveCache(); self::debugLog("Cached: $attr1=$choice1 + $attr2=$choice2 => " . ($isImpossible ? 'IMPOSSIBLE' : 'POSSIBLE') . " (Future cost savings!)"); } return $result; } /** * Get comprehensive statistics including cost tracking */ /** * Get comprehensive statistics including cost tracking */ public static function getCacheStats() { self::init(); $total = count(self::$combinationCache); $impossible = 0; $possible = 0; foreach (self::$combinationCache as $entry) { if (isset($entry['is_impossible']) && $entry['is_impossible']) { $impossible++; } else { $possible++; } } // Calculate cost savings from cache using correct constants $avgInputTokens = defined('AVERAGE_INPUT_TOKENS_PER_REQUEST') ? AVERAGE_INPUT_TOKENS_PER_REQUEST : 150; $avgOutputTokens = defined('AVERAGE_OUTPUT_TOKENS_PER_REQUEST') ? AVERAGE_OUTPUT_TOKENS_PER_REQUEST : 50; // Use gpt-4o-mini pricing if available, otherwise fallback if (defined('GPT4OMINI_COST_PER_1K_INPUT_TOKENS') && defined('GPT4OMINI_COST_PER_1K_OUTPUT_TOKENS')) { $inputCost = ($avgInputTokens / 1000) * GPT4OMINI_COST_PER_1K_INPUT_TOKENS; $outputCost = ($avgOutputTokens / 1000) * GPT4OMINI_COST_PER_1K_OUTPUT_TOKENS; $avgCostPerRequest = $inputCost + $outputCost; } elseif (defined('GPT35_COST_PER_1K_TOKENS')) { $avgCostPerRequest = (($avgInputTokens + $avgOutputTokens) / 1000) * GPT35_COST_PER_1K_TOKENS; } else { // Fallback to gpt-4o-mini pricing $avgCostPerRequest = (($avgInputTokens / 1000) * 0.000150) + (($avgOutputTokens / 1000) * 0.000600); } $costSavedByCache = $total * $avgCostPerRequest; return [ 'total_cached' => $total, 'impossible_cached' => $impossible, 'possible_cached' => $possible, 'cache_file' => self::$cacheFile, 'cache_size_kb' => file_exists(self::$cacheFile) ? round(filesize(self::$cacheFile) / 1024, 2) : 0, 'rate_limit_hits' => self::$rateLimitHits, 'daily_cost' => self::$dailyCost, 'cost_saved_by_cache' => $costSavedByCache, 'cost_limit' => defined('MAX_DAILY_COST') ? MAX_DAILY_COST : 0, 'model_in_use' => GPT_MODEL, 'cost_per_request' => $avgCostPerRequest ]; } /** * Reset daily cost (for testing or new day) */ public static function resetDailyCost() { self::$dailyCost = 0; self::saveDailyCost(); self::costLog("Daily cost counter reset"); } /** * Clear the combination cache */ public static function clearCache() { self::$combinationCache = []; if (self::$cacheFile && file_exists(self::$cacheFile)) { unlink(self::$cacheFile); } self::debugLog("Combination cache cleared"); } }