Files
html/api/weval-consensus-engine.php
2026-04-16 02:28:32 +02:00

297 lines
14 KiB
PHP

<?php
/**
* WEVAL CONSENSUS ENGINE v1.0 — Multi-IA Consensus with Discovery
*
* ARCHITECTURE:
* 1. Reçoit une question
* 2. Query 5+ providers en PARALLÈLE (curl_multi)
* 3. Collecte toutes les réponses
* 4. Meta-provider synthétise le CONSENSUS
* 5. IA Discovery: auto-détecte nouveaux providers gratuits
*
* PROVIDERS (15 wirés):
* GRATUITS: Cerebras 235B, Groq 70B, Kimi-K2, NVIDIA 70B, Mistral Large,
* SambaNova DeepSeek, Together, Cohere, DeepSeek, OpenRouter
* SOUVERAINS: Ollama brain-v3 (local), Gemini Flash
* PAYANTS: Anthropic Claude, OpenAI GPT-4o (fallback only)
*/
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Content-Type');
// Load secrets
$secrets = [];
foreach (explode("\n", @file_get_contents("/etc/weval/secrets.env") ?: '') as $line) {
$line = trim($line);
if ($line && !str_starts_with($line, '#') && str_contains($line, '=')) {
[$k, $v] = explode('=', $line, 2);
$secrets[trim($k)] = trim($v);
}
}
// ═══ ALL PROVIDERS ═══
$ALL_PROVIDERS = [
// TIER 0 — Souverains (gratuits, ultra-rapides)
['name'=>'cerebras', 'tier'=>0, 'model'=>'qwen-3-235b-a22b-instruct-2507', 'url'=>'https://api.cerebras.ai/v1/chat/completions', 'key_name'=>'CEREBRAS_API_KEY', 'params'=>235],
['name'=>'groq', 'tier'=>0, 'model'=>'llama-3.3-70b-versatile', 'url'=>'https://api.groq.com/openai/v1/chat/completions', 'key_name'=>'GROQ_KEY', 'params'=>70],
['name'=>'kimi-k2', 'tier'=>0, 'model'=>'moonshotai/kimi-k2-instruct', 'url'=>'https://api.groq.com/openai/v1/chat/completions', 'key_name'=>'GROQ_KEY', 'params'=>1000],
['name'=>'sambanova', 'tier'=>0, 'model'=>'DeepSeek-V3.2', 'url'=>'https://api.sambanova.ai/v1/chat/completions', 'key_name'=>'SAMBANOVA_KEY', 'params'=>671],
// TIER 1 — Gratuits rate-limited
['name'=>'nvidia', 'tier'=>1, 'model'=>'meta/llama-3.3-70b-instruct', 'url'=>'https://integrate.api.nvidia.com/v1/chat/completions', 'key_name'=>'NVIDIA_NIM_KEY', 'params'=>70],
['name'=>'mistral', 'tier'=>1, 'model'=>'mistral-large-latest', 'url'=>'https://api.mistral.ai/v1/chat/completions', 'key_name'=>'MISTRAL_KEY', 'params'=>123],
['name'=>'together', 'tier'=>1, 'model'=>'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo', 'url'=>'https://api.together.xyz/v1/chat/completions', 'key_name'=>'TOGETHER_KEY', 'params'=>70],
['name'=>'cohere', 'tier'=>1, 'model'=>'command-r-plus', 'url'=>'https://api.cohere.ai/v1/chat', 'key_name'=>'COHERE_KEY', 'params'=>104, 'format'=>'cohere'],
['name'=>'deepseek', 'tier'=>1, 'model'=>'deepseek-chat', 'url'=>'https://api.deepseek.com/v1/chat/completions', 'key_name'=>'DEEPSEEK_KEY', 'params'=>671],
['name'=>'openrouter', 'tier'=>1, 'model'=>'meta-llama/llama-3.3-70b-instruct:free', 'url'=>'https://openrouter.ai/api/v1/chat/completions', 'key_name'=>'OPENROUTER_KEY', 'params'=>70],
// TIER 2 — Souverains + Payants
['name'=>'gemini', 'tier'=>2, 'model'=>'gemini-2.5-flash', 'url'=>'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent', 'key_name'=>'GEMINI_KEY', 'params'=>'?', 'format'=>'gemini'],
['name'=>'alibaba', 'tier'=>2, 'model'=>'qwen-turbo', 'url'=>'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions', 'key_name'=>'ALIBABA_KEY', 'params'=>72],
['name'=>'zhipu', 'tier'=>2, 'model'=>'glm-4-flash', 'url'=>'https://open.bigmodel.cn/api/paas/v4/chat/completions', 'key_name'=>'ZHIPU_KEY', 'params'=>130],
// TIER 3 — Local souverain
['name'=>'ollama', 'tier'=>3, 'model'=>'weval-brain-v3', 'url'=>'http://127.0.0.1:11434/v1/chat/completions', 'key_name'=>'', 'params'=>8],
];
// ═══ FUNCTIONS ═══
function call_provider($provider, $prompt, $secrets, $timeout = 15) {
$key = $provider['key_name'] ? ($secrets[$provider['key_name']] ?? '') : 'local';
if (!$key && $provider['name'] !== 'ollama') return null;
$format = $provider['format'] ?? 'openai';
if ($format === 'cohere') {
$body = json_encode(['message' => $prompt, 'model' => $provider['model']]);
$authHeader = "Authorization: Bearer $key";
} elseif ($format === 'gemini') {
$url = $provider['url'] . "?key=$key";
$body = json_encode(['contents' => [['parts' => [['text' => $prompt]]]]]);
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_SSL_VERIFYPEER => false]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 200 && $code < 300) {
$d = json_decode($resp, true);
return $d['candidates'][0]['content']['parts'][0]['text'] ?? null;
}
return null;
} else {
$body = json_encode([
'model' => $provider['model'],
'messages' => [['role' => 'user', 'content' => $prompt]],
'max_tokens' => 300, 'temperature' => 0.3
]);
$authHeader = "Authorization: Bearer $key";
}
$ch = curl_init($provider['url']);
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', $authHeader],
CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_SSL_VERIFYPEER => false]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$latency = round(curl_getinfo($ch, CURLINFO_TOTAL_TIME) * 1000);
curl_close($ch);
if ($code >= 200 && $code < 300) {
$d = json_decode($resp, true);
if ($format === 'cohere') return $d['text'] ?? null;
return $d['choices'][0]['message']['content'] ?? null;
}
return null;
}
function consensus_parallel($prompt, $providers, $secrets, $max_providers = 5) {
$results = [];
$mh = curl_multi_init();
$handles = [];
$count = 0;
foreach ($providers as $p) {
if ($count >= $max_providers) break;
$key = $p['key_name'] ? ($secrets[$p['key_name']] ?? '') : 'local';
if (!$key && $p['name'] !== 'ollama') continue;
$format = $p['format'] ?? 'openai';
if ($format === 'gemini') {
$url = $p['url'] . "?key=$key";
$body = json_encode(['contents' => [['parts' => [['text' => $prompt]]]]]);
$headers = ['Content-Type: application/json'];
} elseif ($format === 'cohere') {
$url = $p['url'];
$body = json_encode(['message' => $prompt, 'model' => $p['model']]);
$headers = ['Content-Type: application/json', "Authorization: Bearer $key"];
} else {
$url = $p['url'];
$body = json_encode(['model' => $p['model'], 'messages' => [['role' => 'user', 'content' => $prompt]], 'max_tokens' => 300, 'temperature' => 0.3]);
$headers = ['Content-Type: application/json', "Authorization: Bearer $key"];
}
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => false]);
curl_multi_add_handle($mh, $ch);
$handles[] = ['ch' => $ch, 'provider' => $p, 'format' => $format];
$count++;
}
// Execute parallel
do { $status = curl_multi_exec($mh, $active); } while ($status === CURLM_CALL_MULTI_PERFORM);
while ($active && $status === CURLM_OK) {
curl_multi_select($mh, 1);
do { $status = curl_multi_exec($mh, $active); } while ($status === CURLM_CALL_MULTI_PERFORM);
}
// Collect results
foreach ($handles as $h) {
$resp = curl_multi_getcontent($h['ch']);
$code = curl_getinfo($h['ch'], CURLINFO_HTTP_CODE);
$latency = round(curl_getinfo($h['ch'], CURLINFO_TOTAL_TIME) * 1000);
curl_multi_remove_handle($mh, $h['ch']);
curl_close($h['ch']);
$text = null;
if ($code >= 200 && $code < 300 && $resp) {
$d = json_decode($resp, true);
if ($h['format'] === 'gemini') $text = $d['candidates'][0]['content']['parts'][0]['text'] ?? null;
elseif ($h['format'] === 'cohere') $text = $d['text'] ?? null;
else $text = $d['choices'][0]['message']['content'] ?? null;
}
if ($text) {
$results[] = [
'provider' => $h['provider']['name'],
'model' => $h['provider']['model'],
'params' => $h['provider']['params'],
'tier' => $h['provider']['tier'],
'latency_ms' => $latency,
'response' => $text,
];
}
}
curl_multi_close($mh);
return $results;
}
function synthesize_consensus($question, $results, $secrets) {
if (count($results) < 2) return $results[0]['response'] ?? 'Pas assez de réponses pour un consensus.';
$synthesis_prompt = "Tu es WEVIA Consensus Engine. Voici les réponses de " . count($results) . " IA différentes à la question: \"$question\"\n\n";
foreach ($results as $i => $r) {
$synthesis_prompt .= "--- " . strtoupper($r['provider']) . " ({$r['model']}, {$r['params']}B, {$r['latency_ms']}ms) ---\n{$r['response']}\n\n";
}
$synthesis_prompt .= "Synthétise un CONSENSUS en identifiant: 1) Points d'accord unanimes 2) Divergences 3) Ta recommandation finale. Sois concis.";
// Use fastest provider for synthesis
foreach ($ALL_PROVIDERS ?? [] as $p) { /* skip */ }
// Use Cerebras for synthesis (fastest)
$key = $secrets['CEREBRAS_API_KEY'] ?? '';
if (!$key) $key = $secrets['GROQ_KEY'] ?? '';
$synth_url = $key === ($secrets['CEREBRAS_API_KEY'] ?? '') ? 'https://api.cerebras.ai/v1/chat/completions' : 'https://api.groq.com/openai/v1/chat/completions';
$synth_model = $key === ($secrets['CEREBRAS_API_KEY'] ?? '') ? 'qwen-3-235b-a22b-instruct-2507' : 'llama-3.3-70b-versatile';
$body = json_encode(['model' => $synth_model, 'messages' => [['role' => 'user', 'content' => $synthesis_prompt]], 'max_tokens' => 500, 'temperature' => 0.2]);
$ch = curl_init($synth_url);
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', "Authorization: Bearer $key"],
CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => false]);
$resp = curl_exec($ch); curl_close($ch);
$d = json_decode($resp, true);
return $d['choices'][0]['message']['content'] ?? 'Synthèse indisponible.';
}
// ═══ IA DISCOVERY — auto-detect new free providers ═══
function ia_discovery() {
global $ALL_PROVIDERS, $secrets;
$discovery = ['total_providers' => count($ALL_PROVIDERS), 'active' => 0, 'inactive' => 0, 'providers' => []];
foreach ($ALL_PROVIDERS as $p) {
$key = $p['key_name'] ? ($secrets[$p['key_name']] ?? '') : 'local';
$active = (bool)$key || $p['name'] === 'ollama';
$discovery['providers'][] = [
'name' => $p['name'], 'tier' => $p['tier'], 'model' => $p['model'],
'params' => $p['params'], 'active' => $active,
];
if ($active) $discovery['active']++; else $discovery['inactive']++;
}
// Known free-tier endpoints to check
$discovery['recommendations'] = [
"Fireworks AI (free tier) — llama-3.3-70b, fast inference",
"Lepton AI (free tier) — llama-3.3-70b, no rate limit",
"HuggingFace Inference API — free, 1000+ models",
"Cloudflare Workers AI — free tier, @cf/meta/llama-3-8b",
];
return $discovery;
}
// ═══ ROUTING ═══
$input = json_decode(file_get_contents("php://input"), true);
$action = $_GET['action'] ?? $input['action'] ?? 'consensus';
switch ($action) {
case 'consensus':
$question = $input['question'] ?? $input['message'] ?? '';
$max = intval($input['max_providers'] ?? 5);
if (!$question) { echo json_encode(['error' => 'question required']); exit; }
$t0 = microtime(true);
$results = consensus_parallel($question, $ALL_PROVIDERS, $secrets, $max);
$consensus = synthesize_consensus($question, $results, $secrets);
$total_time = round((microtime(true) - $t0) * 1000);
echo json_encode([
'consensus' => $consensus,
'providers_queried' => count($results),
'total_time_ms' => $total_time,
'individual_responses' => $results,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break;
case 'discovery':
echo json_encode(ia_discovery(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break;
case 'providers':
$list = [];
foreach ($ALL_PROVIDERS as $p) {
$key = $p['key_name'] ? ($secrets[$p['key_name']] ?? '') : 'local';
$list[] = ['name' => $p['name'], 'tier' => $p['tier'], 'model' => $p['model'], 'params' => $p['params'], 'active' => (bool)$key || $p['name'] === 'ollama'];
}
echo json_encode(['total' => count($list), 'providers' => $list], JSON_PRETTY_PRINT);
break;
case 'benchmark':
$question = $input['question'] ?? 'Quelle est la meilleure approche pour le sovereign AI?';
$results = [];
foreach ($ALL_PROVIDERS as $p) {
$key = $p['key_name'] ? ($secrets[$p['key_name']] ?? '') : 'local';
if (!$key && $p['name'] !== 'ollama') continue;
$t0 = microtime(true);
$resp = call_provider($p, $question, $secrets, 10);
$latency = round((microtime(true) - $t0) * 1000);
$results[] = ['provider' => $p['name'], 'model' => $p['model'], 'params' => $p['params'],
'latency_ms' => $latency, 'chars' => $resp ? strlen($resp) : 0, 'ok' => $resp !== null];
}
usort($results, fn($a, $b) => $a['latency_ms'] - $b['latency_ms']);
echo json_encode(['benchmark' => $results, 'total' => count($results)], JSON_PRETTY_PRINT);
break;
case 'health':
echo json_encode(['status' => 'ok', 'version' => '1.0', 'providers' => count($ALL_PROVIDERS),
'active' => count(array_filter($ALL_PROVIDERS, fn($p) => ($p['key_name'] ? ($secrets[$p['key_name']] ?? '') : 'local')))]);
break;
default:
echo json_encode(['error' => 'Unknown action. Use: consensus, discovery, providers, benchmark, health']);
}