422 lines
18 KiB
PHP
Executable File
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)
|
|
];
|
|
}
|
|
?>
|