Files
wevads-platform/scripts/hamid-failover.php
2026-02-26 04:53:11 +01:00

252 lines
8.8 KiB
PHP
Executable File

<?php
/**
* WEVAL MIND Failover System
* Gère la rotation automatique entre providers
*/
class HamidFailover {
private $statusFile = "/tmp/hamid_provider_status.json";
private $providers;
private $config;
// Priorité des providers (gratuit illimité → payant)
private $priority = [
"cerebras", // #1 Gratuit illimité, 500ms
"groq", // #2 Gratuit 100k/jour, 1s
"sambanova", // #3 Gratuit, 2s
"mistral", // #4 Gratuit limité
"cohere", // #5 Gratuit limité
"together", // #6 Gratuit limité
"fireworks", // #7 Gratuit limité
"deepseek", // #9 Payant pas cher
"claude", // #10 Payant premium
"openai", // #11 Payant premium
"ollama" // #12 Local backup
];
// Providers sans clé API
private $noKeyProviders = ["ollama", "ollama-mini", "vllm", "lmstudio", "localai", "n8n"];
public function __construct($providers, $config) {
$this->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']
];
}
}