Feature: Add configurable LLM endpoint for Claude-compatible alternatives

This commit is contained in:
WLTBAgent
2026-02-16 16:35:42 +00:00
parent 8b5c8826e2
commit 3c2d356eb0
5 changed files with 45 additions and 12 deletions

View File

@@ -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');

View File

@@ -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

View File

@@ -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,
]); ]);

View File

@@ -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));

View File

@@ -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)')); ?>