diff --git a/appinfo/routes.php b/appinfo/routes.php index 7c5e6e14..ce8e4355 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -11,6 +11,7 @@ ['name' => 'config#setSensitiveUserConfig', 'url' => '/config/sensitive', 'verb' => 'PUT'], ['name' => 'config#setAdminConfig', 'url' => '/admin-config', 'verb' => 'PUT'], ['name' => 'config#setSensitiveAdminConfig', 'url' => '/admin-config/sensitive', 'verb' => 'PUT'], + ['name' => 'config#autoDetectFeatures', 'url' => '/admin-config/auto-detect-features', 'verb' => 'POST'], ['name' => 'openAiAPI#getModels', 'url' => '/models', 'verb' => 'GET'], ['name' => 'openAiAPI#getUserQuotaInfo', 'url' => '/quota-info', 'verb' => 'GET'], diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 7ddd8eec..d894e987 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -100,6 +100,9 @@ public function register(IRegistrationContext $context): void { $context->registerTaskProcessingProvider(AudioToTextProvider::class); } + $serviceUrl = $this->appConfig->getValueString(Application::APP_ID, 'url'); + $isUsingOpenAI = $serviceUrl === '' || $serviceUrl === Application::OPENAI_API_BASE_URL; + if ($this->appConfig->getValueString(Application::APP_ID, 'llm_provider_enabled', '1') === '1') { $context->registerTaskProcessingProvider(TextToTextProvider::class); $context->registerTaskProcessingProvider(TextToTextChatProvider::class); @@ -119,10 +122,12 @@ public function register(IRegistrationContext $context): void { if (class_exists('OCP\\TaskProcessing\\TaskTypes\\TextToTextProofread')) { $context->registerTaskProcessingProvider(\OCA\OpenAi\TaskProcessing\ProofreadProvider::class); } - if (!class_exists('OCP\\TaskProcessing\\TaskTypes\\AnalyzeImages')) { - $context->registerTaskProcessingTaskType(\OCA\OpenAi\TaskProcessing\AnalyzeImagesTaskType::class); + if ($isUsingOpenAI || $this->appConfig->getValueString(Application::APP_ID, 'analyze_image_provider_enabled') === '1') { + if (!class_exists('OCP\\TaskProcessing\\TaskTypes\\AnalyzeImages')) { + $context->registerTaskProcessingTaskType(\OCA\OpenAi\TaskProcessing\AnalyzeImagesTaskType::class); + } + $context->registerTaskProcessingProvider(\OCA\OpenAi\TaskProcessing\AnalyzeImagesProvider::class); } - $context->registerTaskProcessingProvider(\OCA\OpenAi\TaskProcessing\AnalyzeImagesProvider::class); } if (!class_exists('OCP\\TaskProcessing\\TaskTypes\\TextToSpeech')) { $context->registerTaskProcessingTaskType(\OCA\OpenAi\TaskProcessing\TextToSpeechTaskType::class); @@ -133,8 +138,6 @@ public function register(IRegistrationContext $context): void { } // only register audio chat stuff if we're using OpenAI or stt+llm+tts are enabled - $serviceUrl = $this->appConfig->getValueString(Application::APP_ID, 'url'); - $isUsingOpenAI = $serviceUrl === '' || $serviceUrl === Application::OPENAI_API_BASE_URL; if ( $isUsingOpenAI || ( diff --git a/lib/Controller/ConfigController.php b/lib/Controller/ConfigController.php index c467705a..dae11002 100644 --- a/lib/Controller/ConfigController.php +++ b/lib/Controller/ConfigController.php @@ -8,6 +8,7 @@ namespace OCA\OpenAi\Controller; use Exception; +use OCA\OpenAi\Service\OpenAiAPIService; use OCA\OpenAi\Service\OpenAiSettingsService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; @@ -22,6 +23,7 @@ public function __construct( string $appName, IRequest $request, private OpenAiSettingsService $openAiSettingsService, + private OpenAiAPIService $openAiAPIService, private ?string $userId, ) { parent::__construct($appName, $request); @@ -98,4 +100,17 @@ public function setSensitiveAdminConfig(array $values): DataResponse { return new DataResponse(''); } + + /** + * Set admin config values + * @return DataResponse + */ + public function autoDetectFeatures(): DataResponse { + try { + $config = $this->openAiAPIService->autoDetectFeatures(); + return new DataResponse($config); + } catch (Exception $e) { + return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); + } + } } diff --git a/lib/Service/OpenAiAPIService.php b/lib/Service/OpenAiAPIService.php index e124eef5..0f25548b 100644 --- a/lib/Service/OpenAiAPIService.php +++ b/lib/Service/OpenAiAPIService.php @@ -891,10 +891,11 @@ public function updateExpImgProcessingTime(int $runtime): void { * @param array $params Query parameters (key/val pairs) * @param string $method HTTP query method * @param string|null $contentType + * @param bool $logErrors if set to false error logs will be suppressed * @return array decoded request result or error * @throws Exception */ - public function request(?string $userId, string $endPoint, array $params = [], string $method = 'GET', ?string $contentType = null): array { + public function request(?string $userId, string $endPoint, array $params = [], string $method = 'GET', ?string $contentType = null, bool $logErrors = true): array { try { $serviceUrl = $this->openAiSettingsService->getServiceUrl(); if ($serviceUrl === '') { @@ -1000,10 +1001,12 @@ public function request(?string $userId, string $endPoint, array $params = [], s } catch (ClientException|ServerException $e) { $responseBody = $e->getResponse()->getBody(); $parsedResponseBody = json_decode($responseBody, true); - if ($e->getResponse()->getStatusCode() === 404) { - $this->logger->debug('API request error : ' . $e->getMessage(), ['response_body' => $responseBody, 'exception' => $e]); - } else { - $this->logger->warning('API request error : ' . $e->getMessage(), ['response_body' => $responseBody, 'exception' => $e]); + if ($logErrors) { + if ($e->getResponse()->getStatusCode() === 404) { + $this->logger->debug('API request error : ' . $e->getMessage(), ['response_body' => $responseBody, 'exception' => $e]); + } else { + $this->logger->warning('API request error : ' . $e->getMessage(), ['response_body' => $responseBody, 'exception' => $e]); + } } throw new Exception( $this->l10n->t('API request error: ') . ( @@ -1019,4 +1022,86 @@ public function request(?string $userId, string $endPoint, array $params = [], s ); } } + + /** + * Check if the T2I provider is available + * + * @return bool whether the T2I provider is available + */ + public function isT2IAvailable(): bool { + if ($this->isUsingOpenAi()) { + return true; + } + try { + $params = [ + 'prompt' => 'a', + 'model' => 'invalid-model', + ]; + $this->request(null, 'images/generations', $params, 'POST', logErrors: false); + } catch (Exception $e) { + return $e->getCode() !== Http::STATUS_NOT_FOUND && $e->getCode() !== Http::STATUS_UNAUTHORIZED; + } + return true; + } + + /** + * Check if the STT provider is available + * + * @return bool whether the STT provider is available + */ + public function isSTTAvailable(): bool { + if ($this->isUsingOpenAi()) { + return true; + } + try { + $params = [ + 'model' => 'invalid-model', + 'file' => 'a', + ]; + $this->request(null, 'audio/translations', $params, 'POST', 'multipart/form-data', logErrors: false); + } catch (Exception $e) { + return $e->getCode() !== Http::STATUS_NOT_FOUND && $e->getCode() !== Http::STATUS_UNAUTHORIZED; + } + return true; + } + + /** + * Check if the TTS provider is available + * + * @return bool whether the TTS provider is available + */ + public function isTTSAvailable(): bool { + if ($this->isUsingOpenAi()) { + return true; + } + try { + $params = [ + 'input' => 'a', + 'voice' => 'invalid-voice', + 'model' => 'invalid-model', + 'response_format' => 'mp3', + ]; + + $this->request(null, 'audio/speech', $params, 'POST', logErrors: false); + } catch (Exception $e) { + return $e->getCode() !== Http::STATUS_NOT_FOUND && $e->getCode() !== Http::STATUS_UNAUTHORIZED; + } + return true; + } + + /** + * Updates the admin config with the availability of the providers + * + * @return array the updated config + * @throws Exception + */ + public function autoDetectFeatures(): array { + $config = []; + $config['t2i_provider_enabled'] = $this->isT2IAvailable(); + $config['stt_provider_enabled'] = $this->isSTTAvailable(); + $config['tts_provider_enabled'] = $this->isTTSAvailable(); + $this->openAiSettingsService->setAdminConfig($config); + $config['analyze_image_provider_enabled'] = $this->openAiSettingsService->getAnalyzeImageProviderEnabled(); + return $config; + } } diff --git a/lib/Service/OpenAiSettingsService.php b/lib/Service/OpenAiSettingsService.php index 833023c8..a81c11d6 100644 --- a/lib/Service/OpenAiSettingsService.php +++ b/lib/Service/OpenAiSettingsService.php @@ -40,6 +40,7 @@ class OpenAiSettingsService { 't2i_provider_enabled' => 'boolean', 'stt_provider_enabled' => 'boolean', 'tts_provider_enabled' => 'boolean', + 'analyze_image_provider_enabled' => 'boolean', 'chat_endpoint_enabled' => 'boolean', 'basic_user' => 'string', 'basic_password' => 'string', @@ -321,6 +322,7 @@ public function getAdminConfig(): array { 't2i_provider_enabled' => $this->getT2iProviderEnabled(), 'stt_provider_enabled' => $this->getSttProviderEnabled(), 'tts_provider_enabled' => $this->getTtsProviderEnabled(), + 'analyze_image_provider_enabled' => $this->getAnalyzeImageProviderEnabled(), 'chat_endpoint_enabled' => $this->getChatEndpointEnabled(), 'basic_user' => $this->getAdminBasicUser(), 'basic_password' => $this->getAdminBasicPassword(), @@ -400,6 +402,19 @@ public function getTtsProviderEnabled(): bool { return $this->appConfig->getValueString(Application::APP_ID, 'tts_provider_enabled', '1') === '1'; } + /** + * @return bool + */ + public function getAnalyzeImageProviderEnabled(): bool { + $config = $this->appConfig->getValueString(Application::APP_ID, 'analyze_image_provider_enabled'); + if ($config === '') { + $serviceUrl = $this->getServiceUrl(); + $isUsingOpenAI = $serviceUrl === '' || $serviceUrl === Application::OPENAI_API_BASE_URL; + return $isUsingOpenAI; + } + return $config === '1'; + } + //////////////////////////////////////////// //////////// Setters for settings ////////// @@ -731,6 +746,9 @@ public function setAdminConfig(array $adminConfig): void { if (isset($adminConfig['tts_provider_enabled'])) { $this->setTtsProviderEnabled($adminConfig['tts_provider_enabled']); } + if (isset($adminConfig['analyze_image_provider_enabled'])) { + $this->setAnalyzeImageProviderEnabled($adminConfig['analyze_image_provider_enabled']); + } if (isset($adminConfig['default_tts_voice'])) { $this->setAdminDefaultTtsVoice($adminConfig['default_tts_voice']); } @@ -833,6 +851,14 @@ public function setTtsProviderEnabled(bool $enabled): void { $this->appConfig->setValueString(Application::APP_ID, 'tts_provider_enabled', $enabled ? '1' : '0'); } + /** + * @param bool $enabled + * @return void + */ + public function setAnalyzeImageProviderEnabled(bool $enabled): void { + $this->appConfig->setValueString(Application::APP_ID, 'analyze_image_provider_enabled', $enabled ? '1' : '0'); + } + /** * @param bool $enabled */ diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 70553a11..e630f615 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -586,6 +586,11 @@ @update:model-value="onCheckboxChanged($event, 'tts_provider_enabled', false)"> {{ t('integration_openai', 'Text-to-speech provider') }} + + {{ t('integration_openai', 'Analyze image provider') }} + @@ -700,6 +705,23 @@ export default { label: model.id + (model.owned_by ? ' (' + model.owned_by + ')' : ''), } }, + autoDetectFeatures() { + return axios.post(generateUrl('/apps/integration_openai/admin-config/auto-detect-features')).then((response) => { + const data = response.data ?? {} + console.debug(data) + this.state = { + ...this.state, + ...data, + } + }).catch((error) => { + showError( + t('integration_openai', 'Failed to auto update config') + + ': ' + this.reduceStars(error.response?.data?.error), + { timeout: 10000 }, + ) + console.error(error) + }) + }, getModels(shouldSave = true) { this.models = null @@ -837,6 +859,7 @@ export default { if (getModels) { this.getModels() } + this.autoDetectFeatures() }, 2000), onInput: debounce(async function() { // sanitize quotas