providers = $providers; $this->config = $config; } public function getStatus() { if (!file_exists($this->statusFile)) return []; $status = json_decode(file_get_contents($this->statusFile), true) ?: []; // Nettoyer les vieux statuts (> 5 min) foreach ($status as $p => $s) { if (time() - ($s["time"] ?? 0) > 300) { unset($status[$p]); } } return $status; } public function markDown($provider, $error) { $status = $this->getStatus(); $status[$provider] = ["down" => true, "error" => $error, "time" => time()]; file_put_contents($this->statusFile, json_encode($status)); } public function markUp($provider) { $status = $this->getStatus(); unset($status[$provider]); file_put_contents($this->statusFile, json_encode($status)); } public function isAvailable($provider) { // Vérifier si down $status = $this->getStatus(); if (isset($status[$provider]["down"])) return false; // Vérifier si clé API configurée if (!in_array($provider, $this->noKeyProviders)) { $keyField = $provider . "_api_key"; if ($provider == "claude") $keyField = "claude_api_key"; if (empty($this->config[$keyField])) return false; } return true; } public function getNextProvider($currentProvider) { $idx = array_search($currentProvider, $this->priority); if ($idx === false) $idx = -1; for ($i = $idx + 1; $i < count($this->priority); $i++) { if ($this->isAvailable($this->priority[$i])) { return $this->priority[$i]; } } // Fallback: retourner ollama (toujours dispo localement) return "ollama"; } public function getBestProvider($requested = null) { // Si provider demandé est dispo, l'utiliser if ($requested && $this->isAvailable($requested)) { return $requested; } // Sinon, trouver le meilleur disponible foreach ($this->priority as $p) { if ($this->isAvailable($p)) { return $p; } } return "ollama"; // Dernier recours } public function callWithFailover($message, $history, $systemPrompt, $requestedProvider, $maxAttempts = 3) { $attempt = 0; $provider = $this->getBestProvider($requestedProvider); $originalProvider = $requestedProvider; $failoverUsed = ($provider !== $requestedProvider); $logs = []; while ($attempt < $maxAttempts) { $attempt++; $start = microtime(true); try { $result = $this->callProvider($provider, $message, $history, $systemPrompt); $duration = round((microtime(true) - $start) * 1000); if ($result['success']) { $this->markUp($provider); return [ 'success' => true, 'response' => $result['response'], 'provider' => $provider, 'model' => $result['model'], 'duration' => $duration, 'failover_used' => $failoverUsed, 'original_provider' => $originalProvider, 'attempts' => $logs ]; } else { $this->markDown($provider, $result['error']); $logs[] = ['provider' => $provider, 'error' => $result['error'], 'time' => $duration]; } } catch (Exception $e) { $duration = round((microtime(true) - $start) * 1000); $this->markDown($provider, $e->getMessage()); $logs[] = ['provider' => $provider, 'error' => $e->getMessage(), 'time' => $duration]; } // Passer au provider suivant $provider = $this->getNextProvider($provider); $failoverUsed = true; } return [ 'success' => false, 'error' => 'Tous les providers ont échoué', 'attempts' => $logs ]; } private function callProvider($provider, $message, $history, $systemPrompt) { $cfg = $this->providers[$provider] ?? null; if (!$cfg) return ['success' => false, 'error' => 'Provider non configuré']; $ch = curl_init(); $timeout = ($provider === 'ollama' || $provider === 'ollama-mini') ? 120 : 30; // Construire les messages $messages = [['role' => 'system', 'content' => $systemPrompt]]; if (!empty($history)) { foreach ($history as $h) { $messages[] = ['role' => $h['role'], 'content' => $h['content']]; } } $messages[] = ['role' => 'user', 'content' => $message]; // Config spécifique Claude if ($provider === 'claude') { $claudeMessages = array_filter($messages, fn($m) => $m['role'] !== 'system'); $claudeMessages = array_values($claudeMessages); $payload = [ 'model' => $cfg['model'], 'max_tokens' => 4096, 'system' => $systemPrompt, 'messages' => $claudeMessages ]; curl_setopt_array($ch, [ CURLOPT_URL => $cfg['url'], CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'x-api-key: ' . $cfg['key'], 'anthropic-version: 2023-06-01' ], CURLOPT_POSTFIELDS => json_encode($payload) ]); } else { // OpenAI-compatible $payload = [ 'model' => $cfg['model'], 'messages' => $messages, 'max_tokens' => $cfg['max_tokens'] ?? 4096, 'temperature' => 0.7 ]; $headers = ['Content-Type: application/json']; if (!empty($cfg['key'])) { $headers[] = 'Authorization: Bearer ' . $cfg['key']; } curl_setopt_array($ch, [ CURLOPT_URL => $cfg['url'], CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => json_encode($payload) ]); } $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); if ($curlError) { return ['success' => false, 'error' => 'timeout']; } if ($httpCode >= 400) { $errMap = [429 => 'rate_limit', 402 => 'no_credits', 401 => 'invalid_key', 403 => 'forbidden']; return ['success' => false, 'error' => $errMap[$httpCode] ?? "http_$httpCode"]; } $data = json_decode($response, true); // Extraire réponse if ($provider === 'claude') { $text = $data['content'][0]['text'] ?? ''; } else { $text = $data['choices'][0]['message']['content'] ?? ''; } if (empty($text)) { return ['success' => false, 'error' => 'empty_response']; } return [ 'success' => true, 'response' => $text, 'model' => $cfg['model'] ]; } }