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

422 lines
18 KiB
PHP
Executable File

<?php
require_once("/opt/wevads/config/credentials.php");
/**
* IA Provider Discovery API
* - Auto-test all provider keys
* - Detect obsolete models
* - Suggest replacements
* - Monitor quotas & rate limits
* - Auto-failover configuration
*/
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
$dbHost = 'localhost';
$dbPort = '5432';
$dbName = 'adx_system';
$dbUser = 'admin';
$dbPass = WEVADS_DB_PASS;
try {
$pdo = new PDO("pgsql:host=$dbHost;port=$dbPort;dbname=$dbName", $dbUser, $dbPass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
echo json_encode(['status' => 'error', 'message' => 'DB connection failed: ' . $e->getMessage()]);
exit;
}
$action = $_GET['action'] ?? $_POST['action'] ?? 'status';
// ═══ MODEL CATALOG — Current best models per provider ═══
$MODEL_CATALOG = [
'cerebras' => [
'current' => 'llama-3.3-70b',
'alternatives' => ['llama-3.1-70b', 'llama-3.1-8b'],
'api_format' => 'openai',
'base_url' => 'https://api.cerebras.ai/v1/chat/completions'
],
'groq' => [
'current' => 'llama-3.3-70b-versatile',
'alternatives' => ['llama-3.1-70b-versatile', 'mixtral-8x7b-32768', 'gemma2-9b-it'],
'api_format' => 'openai',
'base_url' => 'https://api.groq.com/openai/v1/chat/completions'
],
'deepseek' => [
'current' => 'deepseek-chat',
'alternatives' => ['deepseek-reasoner'],
'api_format' => 'openai',
'base_url' => 'https://api.deepseek.com/v1/chat/completions'
],
'gemini' => [
'current' => 'gemini-2.0-flash',
'alternatives' => ['gemini-1.5-flash', 'gemini-1.5-pro'],
'api_format' => 'gemini',
'base_url' => 'https://generativelanguage.googleapis.com/v1beta/models'
],
'claude' => [
'current' => 'claude-3-5-sonnet-20241022',
'alternatives' => ['claude-3-haiku-20240307'],
'api_format' => 'anthropic',
'base_url' => 'https://api.anthropic.com/v1/messages'
],
'sambanova' => [
'current' => 'Meta-Llama-3.3-70B-Instruct',
'alternatives' => ['Meta-Llama-3.1-405B-Instruct'],
'api_format' => 'openai',
'base_url' => 'https://api.sambanova.ai/v1/chat/completions',
'notes' => 'Check if service still active (HTTP 410 = discontinued)'
],
'hyperbolic' => [
'current' => 'meta-llama/Llama-3.3-70B-Instruct',
'alternatives' => ['meta-llama/Llama-3.2-70B-Instruct'],
'api_format' => 'openai',
'base_url' => 'https://api.hyperbolic.xyz/v1/chat/completions'
],
'mistral' => [
'current' => 'mistral-large-latest',
'alternatives' => ['mistral-medium-latest', 'open-mistral-7b'],
'api_format' => 'openai',
'base_url' => 'https://api.mistral.ai/v1/chat/completions'
],
'cohere' => [
'current' => 'command-r-plus',
'alternatives' => ['command-r', 'command-light'],
'api_format' => 'cohere',
'base_url' => 'https://api.cohere.ai/v1/chat'
],
'ollama' => [
'current' => 'llama3.1:8b',
'alternatives' => ['mistral:7b-instruct', 'phi', 'tinyllama'],
'api_format' => 'ollama',
'base_url' => 'http://127.0.0.1:11434/api/chat'
]
];
// ═══ HTTP Error Interpretation ═══
$ERROR_MEANINGS = [
200 => ['status' => 'ok', 'msg' => 'Working'],
400 => ['status' => 'error', 'msg' => 'Bad request — model may be obsolete or wrong format'],
401 => ['status' => 'error', 'msg' => 'Invalid API key — needs renewal'],
402 => ['status' => 'error', 'msg' => 'Payment required — credits exhausted'],
403 => ['status' => 'error', 'msg' => 'Forbidden — account suspended or wrong permissions'],
404 => ['status' => 'error', 'msg' => 'Not found — endpoint or model does not exist'],
410 => ['status' => 'dead', 'msg' => 'Gone — service discontinued'],
429 => ['status' => 'warning', 'msg' => 'Rate limited — too many requests, but key works'],
500 => ['status' => 'error', 'msg' => 'Server error — provider side issue'],
503 => ['status' => 'warning', 'msg' => 'Service unavailable — temporary, retry later']
];
// ═══ ACTIONS ═══
switch ($action) {
// ─── Full discovery scan ───
case 'scan':
case 'discover':
$results = [];
// Get all providers from DB
$stmt = $pdo->query("SELECT * FROM admin.hamid_providers ORDER BY priority");
$providers = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($providers as $p) {
$name = strtolower($p['provider_name']);
$key = $p['api_key'] ?? '';
$model = $p['model'] ?? '';
$catalog = $MODEL_CATALOG[$name] ?? null;
$result = [
'provider' => $p['provider_name'],
'model_current' => $model,
'model_recommended' => $catalog ? $catalog['current'] : null,
'has_key' => !empty($key),
'key_preview' => !empty($key) ? substr($key, 0, 8) . '...' : 'EMPTY',
'is_active' => $p['is_active'] ?? false,
'priority' => $p['priority'] ?? 99,
'test_result' => null,
'issues' => [],
'actions' => []
];
// Check model obsolescence
if ($catalog && $model !== $catalog['current']) {
$result['issues'][] = "Model '$model' may be outdated. Current: {$catalog['current']}";
$result['actions'][] = [
'type' => 'update_model',
'from' => $model,
'to' => $catalog['current'],
'alternatives' => $catalog['alternatives']
];
}
// Test the API if key exists
if (!empty($key) && $name !== 'ollama') {
$testResult = testProvider($name, $key, $model, $catalog);
$result['test_result'] = $testResult;
if ($testResult['http_code'] !== 200) {
$meaning = $ERROR_MEANINGS[$testResult['http_code']] ?? ['status' => 'error', 'msg' => 'Unknown error'];
$result['issues'][] = $meaning['msg'];
// Suggest actions based on error
if ($testResult['http_code'] == 401) {
$result['actions'][] = ['type' => 'renew_key', 'reason' => 'Key invalid or expired'];
} elseif ($testResult['http_code'] == 402) {
$result['actions'][] = ['type' => 'add_credits', 'reason' => 'Credits exhausted'];
} elseif ($testResult['http_code'] == 400 && $catalog) {
// Try alternative models
$result['actions'][] = ['type' => 'try_alt_model', 'alternatives' => $catalog['alternatives']];
} elseif ($testResult['http_code'] == 410) {
$result['actions'][] = ['type' => 'deactivate', 'reason' => 'Service discontinued'];
}
// Auto-try alternative model if 400
if ($testResult['http_code'] == 400 && $catalog && !empty($catalog['alternatives'])) {
foreach ($catalog['alternatives'] as $alt) {
$altTest = testProvider($name, $key, $alt, $catalog);
if ($altTest['http_code'] == 200) {
$result['actions'][] = [
'type' => 'auto_fix_model',
'new_model' => $alt,
'message' => "Model '$alt' works! Auto-update available."
];
break;
}
}
}
}
} elseif ($name === 'ollama') {
// Test Ollama locally
$ch = curl_init('http://127.0.0.1:11434/api/chat');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['model' => $model ?: 'llama3.1:8b', 'messages' => [['role' => 'user', 'content' => 'Say OK']], 'stream' => false]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$time = round(curl_getinfo($ch, CURLINFO_TOTAL_TIME) * 1000);
curl_close($ch);
$result['test_result'] = ['http_code' => $code, 'latency_ms' => $time, 'response_preview' => substr($body, 0, 100)];
// List available Ollama models
$modelsResp = @file_get_contents('http://127.0.0.1:11434/api/tags');
if ($modelsResp) {
$models = json_decode($modelsResp, true);
$result['ollama_models'] = array_map(function($m) { return $m['name']; }, $models['models'] ?? []);
}
} else {
$result['issues'][] = 'No API key configured';
$result['actions'][] = ['type' => 'add_key', 'reason' => 'Key missing'];
}
// Overall health
$httpCode = $result['test_result']['http_code'] ?? 0;
if ($httpCode === 200) {
$result['health'] = 'healthy';
} elseif ($httpCode === 429) {
$result['health'] = 'rate_limited';
} elseif (in_array($httpCode, [401, 402, 403])) {
$result['health'] = 'auth_error';
} elseif ($httpCode === 410) {
$result['health'] = 'dead';
} elseif (empty($key)) {
$result['health'] = 'no_key';
} else {
$result['health'] = 'error';
}
$results[] = $result;
}
// Summary
$healthy = count(array_filter($results, fn($r) => $r['health'] === 'healthy'));
$total = count($results);
$issues = array_sum(array_map(fn($r) => count($r['issues']), $results));
$actions = array_sum(array_map(fn($r) => count($r['actions']), $results));
// Log discovery to DB
try {
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.ia_discovery_log (
id SERIAL PRIMARY KEY,
scan_time TIMESTAMP DEFAULT NOW(),
healthy INT,
total INT,
issues INT,
actions INT,
results JSONB
)");
$stmt = $pdo->prepare("INSERT INTO admin.ia_discovery_log (healthy, total, issues, actions, results) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$healthy, $total, $issues, $actions, json_encode($results)]);
} catch (Exception $e) {}
echo json_encode([
'status' => 'ok',
'scan_time' => date('Y-m-d H:i:s'),
'summary' => [
'healthy' => $healthy,
'total' => $total,
'issues' => $issues,
'pending_actions' => $actions,
'health_pct' => $total > 0 ? round(($healthy / $total) * 100) : 0
],
'providers' => $results,
'model_catalog' => $MODEL_CATALOG
], JSON_PRETTY_PRINT);
break;
// ─── Auto-fix: update model for a provider ───
case 'fix_model':
$provider = $_POST['provider'] ?? '';
$newModel = $_POST['model'] ?? '';
if (empty($provider) || empty($newModel)) {
echo json_encode(['status' => 'error', 'message' => 'provider and model required']);
break;
}
$stmt = $pdo->prepare("UPDATE admin.hamid_providers SET model = ? WHERE LOWER(provider_name) = LOWER(?)");
$stmt->execute([$newModel, $provider]);
// Also update ai_providers if exists
$pdo->prepare("UPDATE admin.ai_providers SET model = ? WHERE LOWER(name) = LOWER(?)")->execute([$newModel, $provider]);
echo json_encode(['status' => 'ok', 'message' => "Model updated to '$newModel' for $provider"]);
break;
// ─── Auto-fix: deactivate dead provider ───
case 'deactivate':
$provider = $_POST['provider'] ?? '';
if (empty($provider)) {
echo json_encode(['status' => 'error', 'message' => 'provider required']);
break;
}
$stmt = $pdo->prepare("UPDATE admin.hamid_providers SET is_active = false, notes = COALESCE(notes,'') || ' [DEACTIVATED by IA Discover ' || NOW()::text || ']' WHERE LOWER(provider_name) = LOWER(?)");
$stmt->execute([$provider]);
echo json_encode(['status' => 'ok', 'message' => "$provider deactivated"]);
break;
// ─── Update API key ───
case 'update_key':
$provider = $_POST['provider'] ?? '';
$newKey = $_POST['api_key'] ?? '';
if (empty($provider) || empty($newKey)) {
echo json_encode(['status' => 'error', 'message' => 'provider and api_key required']);
break;
}
$stmt = $pdo->prepare("UPDATE admin.hamid_providers SET api_key = ? WHERE LOWER(provider_name) = LOWER(?)");
$stmt->execute([$newKey, $provider]);
$pdo->prepare("UPDATE admin.ai_providers SET api_key = ? WHERE LOWER(name) = LOWER(?)")->execute([$newKey, $provider]);
echo json_encode(['status' => 'ok', 'message' => "Key updated for $provider"]);
break;
// ─── Reorder priorities based on health + speed ───
case 'optimize_priorities':
$stmt = $pdo->query("SELECT id, provider_name, is_active FROM admin.hamid_providers ORDER BY priority");
$providers = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Simple: active first, inactive last
$priority = 1;
foreach ($providers as $p) {
if ($p['is_active']) {
$pdo->prepare("UPDATE admin.hamid_providers SET priority = ? WHERE id = ?")->execute([$priority++, $p['id']]);
}
}
foreach ($providers as $p) {
if (!$p['is_active']) {
$pdo->prepare("UPDATE admin.hamid_providers SET priority = ? WHERE id = ?")->execute([$priority++, $p['id']]);
}
}
echo json_encode(['status' => 'ok', 'message' => 'Priorities optimized']);
break;
// ─── Status (lightweight) ───
case 'status':
default:
$stmt = $pdo->query("SELECT provider_name, model, is_active, priority,
CASE WHEN api_key IS NOT NULL AND api_key != '' THEN true ELSE false END as has_key
FROM admin.hamid_providers ORDER BY priority");
$providers = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get last scan
$lastScan = null;
try {
$row = $pdo->query("SELECT scan_time, healthy, total, issues FROM admin.ia_discovery_log ORDER BY id DESC LIMIT 1")->fetch(PDO::FETCH_ASSOC);
if ($row) $lastScan = $row;
} catch (Exception $e) {}
echo json_encode([
'status' => 'ok',
'providers' => $providers,
'last_scan' => $lastScan,
'model_catalog_version' => '2026-02-09'
], JSON_PRETTY_PRINT);
break;
}
// ═══ HELPER: Test a provider API ═══
function testProvider($name, $key, $model, $catalog) {
$format = $catalog['api_format'] ?? 'openai';
$url = $catalog['base_url'] ?? '';
$startTime = microtime(true);
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
if ($format === 'gemini') {
curl_setopt($ch, CURLOPT_URL, "$url/$model:generateContent?key=$key");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'contents' => [['parts' => [['text' => 'Say OK']]]]
]));
} elseif ($format === 'anthropic') {
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
"x-api-key: $key",
'anthropic-version: 2023-06-01'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'model' => $model,
'max_tokens' => 10,
'messages' => [['role' => 'user', 'content' => 'Say OK']]
]));
} else {
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
"Authorization: Bearer $key"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'model' => $model,
'messages' => [['role' => 'user', 'content' => 'Say OK']],
'max_tokens' => 10
]));
}
$body = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$latency = round((microtime(true) - $startTime) * 1000);
curl_close($ch);
return [
'http_code' => $httpCode,
'latency_ms' => $latency,
'response_preview' => substr($body, 0, 200)
];
}
?>