Feature: Add configurable LLM endpoint for Claude-compatible alternatives
This commit is contained in:
@@ -26,6 +26,7 @@
|
|||||||
google_client_id: document.getElementById('google_client_id').value,
|
google_client_id: document.getElementById('google_client_id').value,
|
||||||
google_client_secret: document.getElementById('google_client_secret').value,
|
google_client_secret: document.getElementById('google_client_secret').value,
|
||||||
google_refresh_token: document.getElementById('google_refresh_token').value,
|
google_refresh_token: document.getElementById('google_refresh_token').value,
|
||||||
|
llm_api_endpoint: document.getElementById('llm_api_endpoint').value,
|
||||||
anthropic_api_key: document.getElementById('anthropic_api_key').value
|
anthropic_api_key: document.getElementById('anthropic_api_key').value
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,6 +95,7 @@
|
|||||||
document.getElementById('google_client_id').value = data.data.google_client_id || '';
|
document.getElementById('google_client_id').value = data.data.google_client_id || '';
|
||||||
document.getElementById('google_client_secret').value = '';
|
document.getElementById('google_client_secret').value = '';
|
||||||
document.getElementById('google_refresh_token').value = '';
|
document.getElementById('google_refresh_token').value = '';
|
||||||
|
document.getElementById('llm_api_endpoint').value = data.data.llm_api_endpoint || '';
|
||||||
document.getElementById('anthropic_api_key').value = '';
|
document.getElementById('anthropic_api_key').value = '';
|
||||||
updateStatus(!!data.data.is_configured);
|
updateStatus(!!data.data.is_configured);
|
||||||
showNotification('Success', 'Configuration loaded');
|
showNotification('Success', 'Configuration loaded');
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class AdminController {
|
|||||||
public function load(): JSONResponse {
|
public function load(): JSONResponse {
|
||||||
$clientId = $this->config->getAppValue('google_client_id', 'analyticshub', '');
|
$clientId = $this->config->getAppValue('google_client_id', 'analyticshub', '');
|
||||||
$apiKey = $this->config->getAppValue('anthropic_api_key', 'analyticshub', '');
|
$apiKey = $this->config->getAppValue('anthropic_api_key', 'analyticshub', '');
|
||||||
|
$llmEndpoint = $this->config->getAppValue('llm_api_endpoint', 'analyticshub', '');
|
||||||
$refreshToken = $this->config->getAppValue('google_refresh_token', 'analyticshub', '');
|
$refreshToken = $this->config->getAppValue('google_refresh_token', 'analyticshub', '');
|
||||||
|
|
||||||
// Check if configured
|
// Check if configured
|
||||||
@@ -54,6 +55,7 @@ class AdminController {
|
|||||||
'data' => [
|
'data' => [
|
||||||
'google_client_id' => $clientId,
|
'google_client_id' => $clientId,
|
||||||
'google_refresh_token' => $maskedRefreshToken,
|
'google_refresh_token' => $maskedRefreshToken,
|
||||||
|
'llm_api_endpoint' => $llmEndpoint,
|
||||||
'anthropic_api_key' => $maskedApiKey,
|
'anthropic_api_key' => $maskedApiKey,
|
||||||
'is_configured' => $isConfigured,
|
'is_configured' => $isConfigured,
|
||||||
],
|
],
|
||||||
@@ -77,6 +79,7 @@ class AdminController {
|
|||||||
$clientSecret = $data['google_client_secret'] ?? '';
|
$clientSecret = $data['google_client_secret'] ?? '';
|
||||||
$refreshToken = $data['google_refresh_token'] ?? '';
|
$refreshToken = $data['google_refresh_token'] ?? '';
|
||||||
$apiKey = $data['anthropic_api_key'] ?? '';
|
$apiKey = $data['anthropic_api_key'] ?? '';
|
||||||
|
$llmEndpoint = $data['llm_api_endpoint'] ?? '';
|
||||||
|
|
||||||
if (empty($clientId) || empty($clientSecret) || empty($apiKey)) {
|
if (empty($clientId) || empty($clientSecret) || empty($apiKey)) {
|
||||||
return new JSONResponse([
|
return new JSONResponse([
|
||||||
@@ -93,6 +96,10 @@ class AdminController {
|
|||||||
$this->config->setAppValue('google_refresh_token', 'analyticshub', $refreshToken);
|
$this->config->setAppValue('google_refresh_token', 'analyticshub', $refreshToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!empty($llmEndpoint)) {
|
||||||
|
$this->config->setAppValue('llm_api_endpoint', 'analyticshub', $llmEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
$this->config->setAppValue('anthropic_api_key', 'analyticshub', $apiKey);
|
$this->config->setAppValue('anthropic_api_key', 'analyticshub', $apiKey);
|
||||||
|
|
||||||
// Check if fully configured
|
// Check if fully configured
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class PageController extends \OCP\AppFramework\Controller {
|
|||||||
// Get configuration values (masked for secrets)
|
// Get configuration values (masked for secrets)
|
||||||
$clientId = $this->config->getAppValue('google_client_id', 'analyticshub', '');
|
$clientId = $this->config->getAppValue('google_client_id', 'analyticshub', '');
|
||||||
$apiKey = $this->config->getAppValue('anthropic_api_key', 'analyticshub', '');
|
$apiKey = $this->config->getAppValue('anthropic_api_key', 'analyticshub', '');
|
||||||
|
$llmEndpoint = $this->config->getAppValue('llm_api_endpoint', 'analyticshub', '');
|
||||||
|
|
||||||
// Mask API key for display
|
// Mask API key for display
|
||||||
$maskedApiKey = '';
|
$maskedApiKey = '';
|
||||||
@@ -76,6 +77,7 @@ class PageController extends \OCP\AppFramework\Controller {
|
|||||||
'is_ga_configured' => $isGAConfigured,
|
'is_ga_configured' => $isGAConfigured,
|
||||||
'is_llm_configured' => $isLLMConfigured,
|
'is_llm_configured' => $isLLMConfigured,
|
||||||
'google_client_id' => $clientId,
|
'google_client_id' => $clientId,
|
||||||
|
'llm_api_endpoint' => $llmEndpoint,
|
||||||
'anthropic_api_key_masked' => $maskedApiKey,
|
'anthropic_api_key_masked' => $maskedApiKey,
|
||||||
'request' => $this->request,
|
'request' => $this->request,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ class LLMService {
|
|||||||
private IConfig $config;
|
private IConfig $config;
|
||||||
private ?ILogger $logger;
|
private ?ILogger $logger;
|
||||||
|
|
||||||
// Anthropic API endpoint
|
// Anthropic API endpoint (default)
|
||||||
private const ANTHROPIC_API = 'https://api.anthropic.com/v1/messages';
|
private const DEFAULT_API_ENDPOINT = 'https://api.anthropic.com/v1/messages';
|
||||||
private const MAX_RETRIES = 3;
|
private const MAX_RETRIES = 3;
|
||||||
private const RATE_LIMIT_DELAY = 12; // Seconds to wait between retries
|
private const RATE_LIMIT_DELAY = 12; // Seconds to wait between retries
|
||||||
private const TIMEOUT_SECONDS = 30;
|
private const TIMEOUT_SECONDS = 30;
|
||||||
@@ -44,17 +44,21 @@ class LLMService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$apiKey = $this->config->getAppValue('anthropic_api_key', 'analyticshub');
|
$apiKey = $this->config->getAppValue('anthropic_api_key', 'analyticshub');
|
||||||
|
$apiEndpoint = $this->config->getAppValue('llm_api_endpoint', 'analyticshub', '');
|
||||||
|
|
||||||
if (empty($apiKey)) {
|
if (empty($apiKey)) {
|
||||||
throw new \Exception('Anthropic API key not configured');
|
throw new \Exception('API key not configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use configured endpoint or default to Anthropic
|
||||||
|
$endpoint = !empty($apiEndpoint) ? $apiEndpoint : self::DEFAULT_API_ENDPOINT;
|
||||||
|
|
||||||
// Build prompt
|
// Build prompt
|
||||||
$systemPrompt = $this->buildSystemPrompt($client);
|
$systemPrompt = $this->buildSystemPrompt($client);
|
||||||
$userPrompt = $this->buildUserPrompt($processedData);
|
$userPrompt = $this->buildUserPrompt($processedData);
|
||||||
|
|
||||||
// Call with retry
|
// Call with retry
|
||||||
$response = $this->callWithRetry($systemPrompt, $userPrompt, $apiKey);
|
$response = $this->callWithRetry($systemPrompt, $userPrompt, $apiKey, $endpoint);
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
|
|
||||||
@@ -120,12 +124,12 @@ PROMPT;
|
|||||||
/**
|
/**
|
||||||
* Call Claude API with retry logic
|
* Call Claude API with retry logic
|
||||||
*/
|
*/
|
||||||
private function callWithRetry(string $systemPrompt, string $userPrompt, string $apiKey): string {
|
private function callWithRetry(string $systemPrompt, string $userPrompt, string $apiKey, string $endpoint): string {
|
||||||
for ($attempt = 0; $attempt < self::MAX_RETRIES; $attempt++) {
|
for ($attempt = 0; $attempt < self::MAX_RETRIES; $attempt++) {
|
||||||
$this->logger->info("LLM API call attempt {$attempt}/" . self::MAX_RETRIES);
|
$this->logger->info("LLM API call attempt {$attempt}/" . self::MAX_RETRIES);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = $this->makeLLMRequest($systemPrompt, $userPrompt, $apiKey);
|
$response = $this->makeLLMRequest($systemPrompt, $userPrompt, $apiKey, $endpoint);
|
||||||
|
|
||||||
// Validate response
|
// Validate response
|
||||||
$this->validateResponse($response);
|
$this->validateResponse($response);
|
||||||
@@ -180,9 +184,9 @@ PROMPT;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make HTTP request to Anthropic API
|
* Make HTTP request to LLM API
|
||||||
*/
|
*/
|
||||||
private function makeLLMRequest(string $systemPrompt, string $userPrompt, string $apiKey): string {
|
private function makeLLMRequest(string $systemPrompt, string $userPrompt, string $apiKey, string $endpoint): string {
|
||||||
$ch = curl_init();
|
$ch = curl_init();
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
@@ -197,7 +201,7 @@ PROMPT;
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
curl_setopt($ch, CURLOPT_URL, self::ANTHROPIC_API);
|
curl_setopt($ch, CURLOPT_URL, $endpoint);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||||
|
|||||||
@@ -76,9 +76,27 @@ style('display:none');
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Anthropic Claude API Configuration -->
|
<!-- LLM Configuration -->
|
||||||
<div class="analytics-hub-settings__section">
|
<div class="analytics-hub-settings__section">
|
||||||
<h3><?php p($l->t('Anthropic Claude API')); ?></h3>
|
<h3><?php p($l->t('LLM Configuration (Claude-compatible)')); ?></h3>
|
||||||
|
|
||||||
|
<div class="analytics-hub-settings__field">
|
||||||
|
<label for="llm_api_endpoint">
|
||||||
|
<?php p($l->t('API Endpoint')); ?>
|
||||||
|
<span class="analytics-hub-settings__optional"><?php p($l->t('(Optional - defaults to Anthropic)')); ?></span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="llm_api_endpoint"
|
||||||
|
name="llm_api_endpoint"
|
||||||
|
value="<?php p($_['llm_api_endpoint']); ?>"
|
||||||
|
placeholder="https://api.anthropic.com/v1/messages"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<p class="analytics-hub-settings__hint">
|
||||||
|
<?php p($l->t('Enter a Claude-compatible API endpoint. Leave blank to use Anthropic official API.')); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="analytics-hub-settings__field">
|
<div class="analytics-hub-settings__field">
|
||||||
<label for="anthropic_api_key">
|
<label for="anthropic_api_key">
|
||||||
@@ -95,7 +113,7 @@ style('display:none');
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p class="analytics-hub-settings__hint">
|
<p class="analytics-hub-settings__hint">
|
||||||
<?php p($l->t('Enter your Anthropic API key for AI-powered report generation.')); ?>
|
<?php p($l->t('Enter your API key for AI-powered report generation.')); ?>
|
||||||
</p>
|
</p>
|
||||||
<p class="analytics-hub-settings__hint">
|
<p class="analytics-hub-settings__hint">
|
||||||
<?php p($l->t('Model: claude-sonnet-4-5-20250929 (cost-effective)')); ?>
|
<?php p($l->t('Model: claude-sonnet-4-5-20250929 (cost-effective)')); ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user