feat: multi-provider AI with per-company override and Claude support
- Global settings: custom AI panel with OpenAI/Gemini/Claude each with API key, model selector, and live test-connection button (no save needed) - Per-company IA tab: same three providers + fallback to global when empty - AiBot: reads per-company config_json first, falls back to env() global - Added callClaude() for Anthropic Messages API - More OpenAI models: gpt-4.1, gpt-4.1-mini - test-ai endpoint: accepts provider/api_key/model from POST for live test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
b82fcf6aaa
commit
2b859e3dc0
+40
-22
@@ -540,6 +540,10 @@ $routes = [
|
||||
if ($geminiKey !== '') $config['gemini_api_key'] = $geminiKey;
|
||||
$geminiModel = trim($_POST['gemini_model'] ?? '');
|
||||
if ($geminiModel !== '') $config['gemini_model'] = $geminiModel;
|
||||
$claudeKey = trim($_POST['claude_api_key'] ?? '');
|
||||
if ($claudeKey !== '') $config['claude_api_key'] = $claudeKey;
|
||||
$claudeModel = trim($_POST['claude_model'] ?? '');
|
||||
if ($claudeModel !== '') $config['claude_model'] = $claudeModel;
|
||||
|
||||
// per_type is managed exclusively via /admin/bot-config/save-per-type — copy as-is from DB
|
||||
if (isset($existingConfig['per_type'])) {
|
||||
@@ -563,6 +567,7 @@ $routes = [
|
||||
// Preserve any DB keys the form doesn't explicitly manage (welcome_menu, per_type, etc.)
|
||||
$formManagedKeys = ['commands', 'menus', 'flows', 'ai_prompt', 'ai_model', 'ai_temperature',
|
||||
'ai_provider', 'openai_api_key', 'gemini_api_key', 'gemini_model',
|
||||
'claude_api_key', 'claude_model',
|
||||
'ai_for_media', 'greeting', 'fallback', 'approval_webhook',
|
||||
'ignore_prefixes', 'welcome_menu', 'per_type'];
|
||||
foreach ($existingConfig as $k => $v) {
|
||||
@@ -622,7 +627,8 @@ $routes = [
|
||||
$allowed = [
|
||||
'whatsapp_access_token', 'whatsapp_app_secret', 'whatsapp_verify_token',
|
||||
'whatsapp_business_account_id', 'whatsapp_default_phone_number_id',
|
||||
'ai_provider', 'openai_api_key', 'openai_model', 'gemini_api_key', 'gemini_model', 'ai_max_tokens', 'ai_default_prompt',
|
||||
'ai_provider', 'openai_api_key', 'openai_model', 'gemini_api_key', 'gemini_model',
|
||||
'claude_api_key', 'claude_model', 'ai_max_tokens', 'ai_default_prompt',
|
||||
'verify_hmac_signature',
|
||||
];
|
||||
// Checkbox: si no está presente, desactivar
|
||||
@@ -652,32 +658,44 @@ $routes = [
|
||||
|
||||
['POST', '/admin/settings/test-ai', fn() => (function () {
|
||||
SessionAuth::require();
|
||||
$provider = Settings::get('ai_provider', 'mock');
|
||||
// Accept provider/api_key/model from POST (live test), or fall back to saved settings
|
||||
$provider = trim($_POST['provider'] ?? '') ?: Settings::get('ai_provider', 'mock');
|
||||
$apiKey = trim($_POST['api_key'] ?? '');
|
||||
$model = trim($_POST['model'] ?? '');
|
||||
|
||||
if ($provider === 'gemini') {
|
||||
$apiKey = Settings::get('gemini_api_key', '');
|
||||
$model = Settings::get('gemini_model', 'gemini-2.0-flash');
|
||||
if ($apiKey === '') { jsonResponse(200, ['ok' => false, 'provider' => 'gemini', 'error' => 'API Key no configurada. Guarda primero.']); return; }
|
||||
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}";
|
||||
$payload = json_encode(['contents' => [['role' => 'user', 'parts' => [['text' => 'Responde solo: OK']]]]]);
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15]);
|
||||
if ($apiKey === '') $apiKey = Settings::get('gemini_api_key', '');
|
||||
if ($model === '') $model = Settings::get('gemini_model', 'gemini-2.5-flash');
|
||||
if ($apiKey === '') { jsonResponse(200, ['ok' => false, 'provider' => 'gemini', 'error' => 'API Key vacía']); return; }
|
||||
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}";
|
||||
$body = json_encode(['contents' => [['role' => 'user', 'parts' => [['text' => 'Responde solo: OK']]]]]);
|
||||
$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 => 15]);
|
||||
$resp = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch);
|
||||
if ($code !== 200) { $msg = $err ?: (json_decode($resp, true)['error']['message'] ?? "HTTP {$code}"); jsonResponse(200, ['ok' => false, 'provider' => 'gemini', 'error' => $msg]); return; }
|
||||
$text = json_decode($resp, true)['candidates'][0]['content']['parts'][0]['text'] ?? '(sin respuesta)';
|
||||
jsonResponse(200, ['ok' => true, 'provider' => 'gemini', 'response' => trim($text)]);
|
||||
if ($code !== 200) { jsonResponse(200, ['ok' => false, 'provider' => 'gemini', 'error' => $err ?: (json_decode($resp, true)['error']['message'] ?? "HTTP {$code}")]); return; }
|
||||
jsonResponse(200, ['ok' => true, 'provider' => 'gemini', 'response' => trim(json_decode($resp, true)['candidates'][0]['content']['parts'][0]['text'] ?? '(sin respuesta)')]);
|
||||
} elseif ($provider === 'openai') {
|
||||
$apiKey = Settings::get('openai_api_key', '');
|
||||
$model = Settings::get('openai_model', 'gpt-4o-mini');
|
||||
if ($apiKey === '') { jsonResponse(200, ['ok' => false, 'provider' => 'openai', 'error' => 'API Key no configurada. Guarda primero.']); return; }
|
||||
$payload = json_encode(['model' => $model, 'messages' => [['role' => 'user', 'content' => 'Responde solo: OK']], 'max_tokens' => 5]);
|
||||
$ch = curl_init('https://api.openai.com/v1/chat/completions');
|
||||
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $apiKey], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15]);
|
||||
if ($apiKey === '') $apiKey = Settings::get('openai_api_key', '');
|
||||
if ($model === '') $model = Settings::get('openai_model', 'gpt-4o-mini');
|
||||
if ($apiKey === '') { jsonResponse(200, ['ok' => false, 'provider' => 'openai', 'error' => 'API Key vacía']); return; }
|
||||
$body = json_encode(['model' => $model, 'messages' => [['role' => 'user', 'content' => 'Responde solo: OK']], 'max_tokens' => 5]);
|
||||
$ch = curl_init('https://api.openai.com/v1/chat/completions');
|
||||
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $apiKey], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15]);
|
||||
$resp = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch);
|
||||
if ($code !== 200) { $msg = $err ?: (json_decode($resp, true)['error']['message'] ?? "HTTP {$code}"); jsonResponse(200, ['ok' => false, 'provider' => 'openai', 'error' => $msg]); return; }
|
||||
$text = json_decode($resp, true)['choices'][0]['message']['content'] ?? '(sin respuesta)';
|
||||
jsonResponse(200, ['ok' => true, 'provider' => 'openai', 'response' => trim($text)]);
|
||||
if ($code !== 200) { jsonResponse(200, ['ok' => false, 'provider' => 'openai', 'error' => $err ?: (json_decode($resp, true)['error']['message'] ?? "HTTP {$code}")]); return; }
|
||||
jsonResponse(200, ['ok' => true, 'provider' => 'openai', 'response' => trim(json_decode($resp, true)['choices'][0]['message']['content'] ?? '(sin respuesta)')]);
|
||||
} elseif ($provider === 'claude') {
|
||||
if ($apiKey === '') $apiKey = Settings::get('claude_api_key', '');
|
||||
if ($model === '') $model = Settings::get('claude_model', 'claude-haiku-4-5');
|
||||
if ($apiKey === '') { jsonResponse(200, ['ok' => false, 'provider' => 'claude', 'error' => 'API Key vacía']); return; }
|
||||
$body = json_encode(['model' => $model, 'max_tokens' => 10, 'messages' => [['role' => 'user', 'content' => 'Responde solo: OK']]]);
|
||||
$ch = curl_init('https://api.anthropic.com/v1/messages');
|
||||
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'x-api-key: ' . $apiKey, 'anthropic-version: 2023-06-01'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15]);
|
||||
$resp = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch);
|
||||
if ($code !== 200) { jsonResponse(200, ['ok' => false, 'provider' => 'claude', 'error' => $err ?: (json_decode($resp, true)['error']['message'] ?? "HTTP {$code}")]); return; }
|
||||
jsonResponse(200, ['ok' => true, 'provider' => 'claude', 'response' => trim(json_decode($resp, true)['content'][0]['text'] ?? '(sin respuesta)')]);
|
||||
} else {
|
||||
jsonResponse(200, ['ok' => true, 'provider' => 'mock', 'response' => 'Mock activo — no hay proveedor real configurado.']);
|
||||
jsonResponse(200, ['ok' => true, 'provider' => 'mock', 'response' => 'Mock activo.']);
|
||||
}
|
||||
})()],
|
||||
|
||||
|
||||
Reference in New Issue
Block a user