252 lines
8.8 KiB
PHP
Executable File
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']
|
|
];
|
|
}
|
|
}
|
|
|