diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index f1731cc8..dad578b2 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -82,6 +82,10 @@ class Application extends App implements IBootstrap { public const AUDIO_TO_TEXT_LANGUAGES = [['en', 'English'], ['zh', '中文'], ['de', 'Deutsch'], ['es', 'Español'], ['ru', 'Русский'], ['ko', '한국어'], ['fr', 'Français'], ['ja', '日本語'], ['pt', 'Português'], ['tr', 'Türkçe'], ['pl', 'Polski'], ['ca', 'Català'], ['nl', 'Nederlands'], ['ar', 'العربية'], ['sv', 'Svenska'], ['it', 'Italiano'], ['id', 'Bahasa Indonesia'], ['hi', 'हिन्दी'], ['fi', 'Suomi'], ['vi', 'Tiếng Việt'], ['he', 'עברית'], ['uk', 'Українська'], ['el', 'Ελληνικά'], ['ms', 'Bahasa Melayu'], ['cs', 'Česky'], ['ro', 'Română'], ['da', 'Dansk'], ['hu', 'Magyar'], ['ta', 'தமிழ்'], ['no', 'Norsk (bokmål / riksmål)'], ['th', 'ไทย / Phasa Thai'], ['ur', 'اردو'], ['hr', 'Hrvatski'], ['bg', 'Български'], ['lt', 'Lietuvių'], ['la', 'Latina'], ['mi', 'Māori'], ['ml', 'മലയാളം'], ['cy', 'Cymraeg'], ['sk', 'Slovenčina'], ['te', 'తెలుగు'], ['fa', 'فارسی'], ['lv', 'Latviešu'], ['bn', 'বাংলা'], ['sr', 'Српски'], ['az', 'Azərbaycanca / آذربايجان'], ['sl', 'Slovenščina'], ['kn', 'ಕನ್ನಡ'], ['et', 'Eesti'], ['mk', 'Македонски'], ['br', 'Brezhoneg'], ['eu', 'Euskara'], ['is', 'Íslenska'], ['hy', 'Հայերեն'], ['ne', 'नेपाली'], ['mn', 'Монгол'], ['bs', 'Bosanski'], ['kk', 'Қазақша'], ['sq', 'Shqip'], ['sw', 'Kiswahili'], ['gl', 'Galego'], ['mr', 'मराठी'], ['pa', 'ਪੰਜਾਬੀ / पंजाबी / پنجابي'], ['si', 'සිංහල'], ['km', 'ភាសាខ្មែរ'], ['sn', 'chiShona'], ['yo', 'Yorùbá'], ['so', 'Soomaaliga'], ['af', 'Afrikaans'], ['oc', 'Occitan'], ['ka', 'ქართული'], ['be', 'Беларуская'], ['tg', 'Тоҷикӣ'], ['sd', 'सिनधि'], ['gu', 'ગુજરાતી'], ['am', 'አማርኛ'], ['yi', 'ייִדיש'], ['lo', 'ລາວ / Pha xa lao'], ['uz', 'Ўзбек'], ['fo', 'Føroyskt'], ['ht', 'Krèyol ayisyen'], ['ps', 'پښتو'], ['tk', 'Туркмен / تركمن'], ['nn', 'Norsk (nynorsk)'], ['mt', 'bil-Malti'], ['sa', 'संस्कृतम्'], ['lb', 'Lëtzebuergesch'], ['my', 'Myanmasa'], ['bo', 'བོད་ཡིག / Bod skad'], ['tl', 'Tagalog'], ['mg', 'Malagasy'], ['as', 'অসমীয়া'], ['tt', 'Tatarça'], ['haw', 'ʻŌlelo Hawaiʻi'], ['ln', 'Lingála'], ['ha', 'هَوُسَ'], ['ba', 'Башҡорт'], ['jw', 'ꦧꦱꦗꦮ'], ['su', 'Basa Sunda'], ['yue', '粤语']]; + public const SERVICE_TYPE_IMAGE = 'image'; + public const SERVICE_TYPE_STT = 'stt'; + public const SERVICE_TYPE_TTS = 'tts'; + private IAppConfig $appConfig; public function __construct(array $urlParams = []) { diff --git a/lib/Controller/ConfigController.php b/lib/Controller/ConfigController.php index dae11002..3c40d6a3 100644 --- a/lib/Controller/ConfigController.php +++ b/lib/Controller/ConfigController.php @@ -72,8 +72,11 @@ public function setSensitiveUserConfig(array $values): DataResponse { * @return DataResponse */ public function setAdminConfig(array $values): DataResponse { - if (isset($values['api_key']) || isset($values['basic_password']) || isset($values['basic_user']) || isset($values['url'])) { - return new DataResponse('', Http::STATUS_BAD_REQUEST); + $prefixes = ['', 'image_', 'tts_', 'stt_']; + foreach ($prefixes as $prefix) { + if (isset($values[$prefix . 'api_key']) || isset($values[$prefix . 'basic_password']) || isset($values[$prefix . 'basic_user']) || isset($values[$prefix . 'url'])) { + return new DataResponse('', Http::STATUS_BAD_REQUEST); + } } try { $this->openAiSettingsService->setAdminConfig($values); diff --git a/lib/Controller/OpenAiAPIController.php b/lib/Controller/OpenAiAPIController.php index df7e9114..c27fba4e 100644 --- a/lib/Controller/OpenAiAPIController.php +++ b/lib/Controller/OpenAiAPIController.php @@ -26,12 +26,13 @@ public function __construct( } /** + * @param string|null $serviceType * @return DataResponse */ #[NoAdminRequired] - public function getModels(): DataResponse { + public function getModels(?string $serviceType = null): DataResponse { try { - $response = $this->openAiAPIService->getModels($this->userId, true); + $response = $this->openAiAPIService->getModels($this->userId, true, $serviceType); return new DataResponse($response); } catch (Exception $e) { $code = $e->getCode() === 0 ? Http::STATUS_BAD_REQUEST : intval($e->getCode()); diff --git a/lib/Service/OpenAiAPIService.php b/lib/Service/OpenAiAPIService.php index a7652aec..a9d5a89b 100644 --- a/lib/Service/OpenAiAPIService.php +++ b/lib/Service/OpenAiAPIService.php @@ -38,7 +38,7 @@ */ class OpenAiAPIService { private IClient $client; - private ?array $modelsMemoryCache = null; + private array $modelsMemoryCache = []; public function __construct( private LoggerInterface $logger, @@ -66,21 +66,47 @@ public function createQuotaUsage(string $userId, int $type, int $usage) { } /** + * @param ?string $serviceType * @return bool */ - public function isUsingOpenAi(): bool { - $serviceUrl = $this->openAiSettingsService->getServiceUrl(); + public function isUsingOpenAi(?string $serviceType = null): bool { + $serviceUrl = ''; + if ($serviceType === Application::SERVICE_TYPE_IMAGE) { + $serviceUrl = $this->openAiSettingsService->getImageServiceUrl(); + } elseif ($serviceType === Application::SERVICE_TYPE_STT) { + $serviceUrl = $this->openAiSettingsService->getSttServiceUrl(); + } elseif ($serviceType === Application::SERVICE_TYPE_TTS) { + $serviceUrl = $this->openAiSettingsService->getTtsServiceUrl(); + } + if ($serviceUrl === '') { + $serviceUrl = $this->openAiSettingsService->getServiceUrl(); + } return $serviceUrl === '' || $serviceUrl === Application::OPENAI_API_BASE_URL; } /** + * @param ?string $serviceType + * * @return string */ - public function getServiceName(): string { - if ($this->isUsingOpenAi()) { + public function getServiceName(?string $serviceType = null): string { + if ($this->isUsingOpenAi($serviceType)) { + if ($serviceType === Application::SERVICE_TYPE_IMAGE) { + return $this->l10n->t('OpenAI\'s DALL-E 2'); + } + if ($serviceType === Application::SERVICE_TYPE_TTS) { + $this->l10n->t('OpenAI\'s Text to Speech'); + } return 'OpenAI'; } else { $serviceName = $this->openAiSettingsService->getServiceName(); + if ($serviceType === Application::SERVICE_TYPE_IMAGE && $this->openAiSettingsService->imageOverrideEnabled()) { + $serviceName = $this->openAiSettingsService->getImageServiceName(); + } elseif ($serviceType === Application::SERVICE_TYPE_STT && $this->openAiSettingsService->sttOverrideEnabled()) { + $serviceName = $this->openAiSettingsService->getSttServiceName(); + } elseif ($serviceType === Application::SERVICE_TYPE_TTS && $this->openAiSettingsService->ttsOverrideEnabled()) { + $serviceName = $this->openAiSettingsService->getTtsServiceName(); + } if ($serviceName === '') { return 'LocalAI'; } @@ -111,18 +137,21 @@ private function isModelListValid($models): bool { /** * @param ?string $userId * @param bool $refresh + * @param ?string $serviceType * @return array|string[] * @throws Exception */ - public function getModels(?string $userId, bool $refresh = false): array { + public function getModels(?string $userId, bool $refresh = false, ?string $serviceType = null): array { $cache = $this->cacheFactory->createDistributed(Application::APP_ID); - $userCacheKey = Application::MODELS_CACHE_KEY . '_' . ($userId ?? ''); - $adminCacheKey = Application::MODELS_CACHE_KEY . '-main'; + $userCacheKey = Application::MODELS_CACHE_KEY . '_' . ($userId ?? '') . '_' . ($serviceType ?? 'main'); + $adminCacheKey = Application::MODELS_CACHE_KEY . '-main' . '_' . ($serviceType ?? 'main'); + $dbCacheKey = $serviceType ? 'models' . '_' . $serviceType : 'models'; + $memoryCacheKey = $serviceType ?? 'default'; if (!$refresh) { - if ($this->modelsMemoryCache !== null) { + if (array_key_exists($memoryCacheKey, $this->modelsMemoryCache)) { $this->logger->debug('Getting OpenAI models from the memory cache'); - return $this->modelsMemoryCache; + return $this->modelsMemoryCache[$memoryCacheKey]; } // try to get models from the user cache first @@ -130,7 +159,7 @@ public function getModels(?string $userId, bool $refresh = false): array { $userCachedModels = $cache->get($userCacheKey); if ($userCachedModels) { $this->logger->debug('Getting OpenAI models from user cache for user ' . $userId); - $this->modelsMemoryCache = $userCachedModels; + $this->modelsMemoryCache[$memoryCacheKey] = $userCachedModels; return $userCachedModels; } } @@ -149,13 +178,13 @@ public function getModels(?string $userId, bool $refresh = false): array { // we try to get the models from the admin cache if ($adminCachedModels = $cache->get($adminCacheKey)) { $this->logger->debug('Getting OpenAI models from the main distributed cache'); - $this->modelsMemoryCache = $adminCachedModels; + $this->modelsMemoryCache[$memoryCacheKey] = $adminCachedModels; return $adminCachedModels; } } // if we don't need to refresh to model list and it's not been found in the cache, it is obtained from the DB - $modelsObjectString = $this->appConfig->getValueString(Application::APP_ID, 'models', '{"data":[],"object":"list"}'); + $modelsObjectString = $this->appConfig->getValueString(Application::APP_ID, $dbCacheKey, '{"data":[],"object":"list"}'); $fallbackModels = [ 'data' => [], 'object' => 'list', @@ -167,7 +196,7 @@ public function getModels(?string $userId, bool $refresh = false): array { $newCache = $fallbackModels; } $cache->set($userId !== null ? $userCacheKey : $adminCacheKey, $newCache, Application::MODELS_CACHE_TTL); - $this->modelsMemoryCache = $newCache; + $this->modelsMemoryCache[$memoryCacheKey] = $newCache; return $newCache; } @@ -177,7 +206,7 @@ public function getModels(?string $userId, bool $refresh = false): array { try { $this->logger->debug('Actually getting OpenAI models with a network request'); - $modelsResponse = $this->request($userId, 'models'); + $modelsResponse = $this->request($userId, 'models', serviceType: $serviceType); } catch (Exception $e) { $this->logger->warning('Error retrieving models (exc): ' . $e->getMessage()); throw $e; @@ -197,10 +226,10 @@ public function getModels(?string $userId, bool $refresh = false): array { } $cache->set($userId !== null ? $userCacheKey : $adminCacheKey, $modelsResponse, Application::MODELS_CACHE_TTL); - $this->modelsMemoryCache = $modelsResponse; + $this->modelsMemoryCache[$memoryCacheKey] = $modelsResponse; // we always store the model list after getting it $modelsObjectString = json_encode($modelsResponse); - $this->appConfig->setValueString(Application::APP_ID, 'models', $modelsObjectString); + $this->appConfig->setValueString(Application::APP_ID, $dbCacheKey, $modelsObjectString); return $modelsResponse; } @@ -223,9 +252,9 @@ private function hasOwnOpenAiApiKey(string $userId): bool { * @param string|null $userId * @return array */ - public function getModelEnumValues(?string $userId): array { + public function getModelEnumValues(?string $userId, ?string $serviceType = null): array { try { - $modelResponse = $this->getModels($userId); + $modelResponse = $this->getModels($userId, false, $serviceType); $modelEnumValues = array_map(function (array $model) { return new ShapeEnumValue($model['id'], $model['id']); }, $modelResponse['data'] ?? []); @@ -644,7 +673,8 @@ public function createChatCompletion( foreach ($response['choices'] as $choice) { // get tool calls only if this is the finish reason and it's defined and it's an array - if ($choice['finish_reason'] === 'tool_calls' + if ( + $choice['finish_reason'] === 'tool_calls' && isset($choice['message']['tool_calls']) && is_array($choice['message']['tool_calls']) ) { @@ -779,7 +809,7 @@ public function transcribe( $endpoint = $translate ? 'audio/translations' : 'audio/transcriptions'; $contentType = 'multipart/form-data'; - $response = $this->request($userId, $endpoint, $params, 'POST', $contentType); + $response = $this->request($userId, $endpoint, $params, 'POST', $contentType, serviceType: Application::SERVICE_TYPE_STT); if (!isset($response['text'])) { $this->logger->warning('Audio transcription error: ' . json_encode($response)); @@ -809,7 +839,11 @@ public function transcribe( * @throws Exception */ public function requestImageCreation( - ?string $userId, string $prompt, string $model, int $n = 1, string $size = Application::DEFAULT_DEFAULT_IMAGE_SIZE, + ?string $userId, + string $prompt, + string $model, + int $n = 1, + string $size = Application::DEFAULT_DEFAULT_IMAGE_SIZE, ): array { if ($this->isQuotaExceeded($userId, Application::QUOTA_TYPE_IMAGE)) { throw new Exception($this->l10n->t('Image generation quota exceeded'), Http::STATUS_TOO_MANY_REQUESTS); @@ -822,12 +856,11 @@ public function requestImageCreation( 'model' => $model === Application::DEFAULT_MODEL_ID ? Application::DEFAULT_IMAGE_MODEL_ID : $model, ]; - $apiResponse = $this->request($userId, 'images/generations', $params, 'POST'); + $apiResponse = $this->request($userId, 'images/generations', $params, 'POST', serviceType: Application::SERVICE_TYPE_IMAGE); if (!isset($apiResponse['data']) || !is_array($apiResponse['data'])) { $this->logger->warning('OpenAI image generation error', ['api_response' => $apiResponse]); throw new Exception($this->l10n->t('Unknown image generation error'), Http::STATUS_INTERNAL_SERVER_ERROR); - } else { try { $this->createQuotaUsage($userId ?? '', Application::QUOTA_TYPE_IMAGE, $n); @@ -877,7 +910,11 @@ public function getImageRequestOptions(?string $userId): array { * @throws Exception */ public function requestSpeechCreation( - ?string $userId, string $prompt, string $model, string $voice, float $speed = 1, + ?string $userId, + string $prompt, + string $model, + string $voice, + float $speed = 1, ): array { if ($this->isQuotaExceeded($userId, Application::QUOTA_TYPE_SPEECH)) { throw new Exception($this->l10n->t('Speech generation quota exceeded'), Http::STATUS_TOO_MANY_REQUESTS); @@ -891,7 +928,7 @@ public function requestSpeechCreation( 'speed' => $speed, ]; - $apiResponse = $this->request($userId, 'audio/speech', $params, 'POST'); + $apiResponse = $this->request($userId, 'audio/speech', $params, 'POST', serviceType: Application::SERVICE_TYPE_TTS); try { $charCount = mb_strlen($prompt); @@ -930,7 +967,7 @@ public function updateExpTextProcessingTime(int $runtime): void { * @return int */ public function getExpImgProcessingTime(): int { - return $this->isUsingOpenAi() + return $this->isUsingOpenAi(Application::SERVICE_TYPE_IMAGE) ? intval($this->appConfig->getValueString(Application::APP_ID, 'openai_image_generation_time', strval(Application::DEFAULT_OPENAI_IMAGE_GENERATION_TIME), lazy: true)) : intval($this->appConfig->getValueString(Application::APP_ID, 'localai_image_generation_time', strval(Application::DEFAULT_LOCALAI_IMAGE_GENERATION_TIME), lazy: true)); } @@ -943,7 +980,7 @@ public function updateExpImgProcessingTime(int $runtime): void { $oldTime = floatval($this->getExpImgProcessingTime()); $newTime = (1.0 - Application::EXPECTED_RUNTIME_LOWPASS_FACTOR) * $oldTime + Application::EXPECTED_RUNTIME_LOWPASS_FACTOR * floatval($runtime); - if ($this->isUsingOpenAi()) { + if ($this->isUsingOpenAi(Application::SERVICE_TYPE_IMAGE)) { $this->appConfig->setValueString(Application::APP_ID, 'openai_image_generation_time', strval(intval($newTime)), lazy: true); } else { $this->appConfig->setValueString(Application::APP_ID, 'localai_image_generation_time', strval(intval($newTime)), lazy: true); @@ -958,18 +995,53 @@ public function updateExpImgProcessingTime(int $runtime): void { * @param string $method HTTP query method * @param string|null $contentType * @param bool $logErrors if set to false error logs will be suppressed + * @param string|null $serviceType * @return array decoded request result or error * @throws Exception */ - public function request(?string $userId, string $endPoint, array $params = [], string $method = 'GET', ?string $contentType = null, bool $logErrors = true): array { + public function request(?string $userId, string $endPoint, array $params = [], string $method = 'GET', ?string $contentType = null, bool $logErrors = true, ?string $serviceType = null): array { try { - $serviceUrl = $this->openAiSettingsService->getServiceUrl(); - if ($serviceUrl === '') { - $serviceUrl = Application::OPENAI_API_BASE_URL; + $serviceUrl = ''; + $apiKey = ''; + $basicUser = ''; + $basicPassword = ''; + $useBasicAuth = false; + $timeout = 0; + + if ($serviceType === Application::SERVICE_TYPE_IMAGE && $this->openAiSettingsService->imageOverrideEnabled()) { + $serviceUrl = $this->openAiSettingsService->getImageServiceUrl(); + $apiKey = $this->openAiSettingsService->getAdminImageApiKey(); + $basicUser = $this->openAiSettingsService->getAdminImageBasicUser(); + $basicPassword = $this->openAiSettingsService->getAdminImageBasicPassword(); + $useBasicAuth = $this->openAiSettingsService->getAdminImageUseBasicAuth(); + $timeout = $this->openAiSettingsService->getImageRequestTimeout(); + } elseif ($serviceType === Application::SERVICE_TYPE_STT && $this->openAiSettingsService->sttOverrideEnabled()) { + $serviceUrl = $this->openAiSettingsService->getSttServiceUrl(); + $apiKey = $this->openAiSettingsService->getAdminSttApiKey(); + $basicUser = $this->openAiSettingsService->getAdminSttBasicUser(); + $basicPassword = $this->openAiSettingsService->getAdminSttBasicPassword(); + $useBasicAuth = $this->openAiSettingsService->getAdminSttUseBasicAuth(); + $timeout = $this->openAiSettingsService->getSttRequestTimeout(); + } elseif ($serviceType === Application::SERVICE_TYPE_TTS && $this->openAiSettingsService->ttsOverrideEnabled()) { + $serviceUrl = $this->openAiSettingsService->getTtsServiceUrl(); + $apiKey = $this->openAiSettingsService->getAdminTtsApiKey(); + $basicUser = $this->openAiSettingsService->getAdminTtsBasicUser(); + $basicPassword = $this->openAiSettingsService->getAdminTtsBasicPassword(); + $useBasicAuth = $this->openAiSettingsService->getAdminTtsUseBasicAuth(); + $timeout = $this->openAiSettingsService->getTtsRequestTimeout(); + } else { + // Currently only supporting user api keys for the default service + $serviceUrl = $this->openAiSettingsService->getServiceUrl(); + if ($serviceUrl === '') { + $serviceUrl = Application::OPENAI_API_BASE_URL; + } + $apiKey = $this->openAiSettingsService->getUserApiKey($userId, true); + $basicUser = $this->openAiSettingsService->getUserBasicUser($userId, true); + $basicPassword = $this->openAiSettingsService->getUserBasicPassword($userId, true); + $useBasicAuth = $this->openAiSettingsService->getUseBasicAuth(); + $timeout = $this->openAiSettingsService->getRequestTimeout(); } - $timeout = $this->openAiSettingsService->getRequestTimeout(); - $url = rtrim($serviceUrl, '/') . '/' . $endPoint; $options = [ 'timeout' => $timeout, @@ -978,20 +1050,11 @@ public function request(?string $userId, string $endPoint, array $params = [], s ], ]; - // an API key is mandatory when using OpenAI - $apiKey = $this->openAiSettingsService->getUserApiKey($userId, true); - - // We can also use basic authentication - $basicUser = $this->openAiSettingsService->getUserBasicUser($userId, true); - $basicPassword = $this->openAiSettingsService->getUserBasicPassword($userId, true); - if ($serviceUrl === Application::OPENAI_API_BASE_URL && $apiKey === '') { return ['error' => 'An API key is required for api.openai.com']; } - $useBasicAuth = $this->openAiSettingsService->getUseBasicAuth(); - - if ($this->isUsingOpenAi() || !$useBasicAuth) { + if ($this->isUsingOpenAi($serviceType) || !$useBasicAuth) { if ($apiKey !== '') { $options['headers']['Authorization'] = 'Bearer ' . $apiKey; } @@ -1001,7 +1064,7 @@ public function request(?string $userId, string $endPoint, array $params = [], s } } - if (!$this->isUsingOpenAi()) { + if (!$this->isUsingOpenAi($serviceType)) { $options['nextcloud']['allow_local_address'] = true; } @@ -1077,12 +1140,12 @@ public function request(?string $userId, string $endPoint, array $params = [], s throw new Exception( $this->l10n->t('API request error: ') . ( $e->getResponse()->getStatusCode() === 401 - ? $this->l10n->t('Invalid API Key/Basic Auth: ') - : '' + ? $this->l10n->t('Invalid API Key/Basic Auth: ') + : '' ) . ( isset($parsedResponseBody['error']) && isset($parsedResponseBody['error']['message']) - ? $parsedResponseBody['error']['message'] - : $e->getMessage() + ? $parsedResponseBody['error']['message'] + : $e->getMessage() ), intval($e->getCode()), ); @@ -1095,7 +1158,7 @@ public function request(?string $userId, string $endPoint, array $params = [], s * @return bool whether the T2I provider is available */ public function isT2IAvailable(): bool { - if ($this->isUsingOpenAi()) { + if ($this->openAiSettingsService->imageOverrideEnabled() || $this->isUsingOpenAi()) { return true; } try { @@ -1103,7 +1166,7 @@ public function isT2IAvailable(): bool { 'prompt' => 'a', 'model' => 'invalid-model', ]; - $this->request(null, 'images/generations', $params, 'POST', logErrors: false); + $this->request(null, 'images/generations', $params, 'POST', logErrors: false, serviceType: Application::SERVICE_TYPE_IMAGE); } catch (Exception $e) { return $e->getCode() !== Http::STATUS_NOT_FOUND && $e->getCode() !== Http::STATUS_UNAUTHORIZED; } @@ -1116,7 +1179,7 @@ public function isT2IAvailable(): bool { * @return bool whether the STT provider is available */ public function isSTTAvailable(): bool { - if ($this->isUsingOpenAi()) { + if ($this->openAiSettingsService->sttOverrideEnabled() || $this->isUsingOpenAi()) { return true; } try { @@ -1124,7 +1187,7 @@ public function isSTTAvailable(): bool { 'model' => 'invalid-model', 'file' => 'a', ]; - $this->request(null, 'audio/translations', $params, 'POST', 'multipart/form-data', logErrors: false); + $this->request(null, 'audio/translations', $params, 'POST', 'multipart/form-data', logErrors: false, serviceType: Application::SERVICE_TYPE_STT); } catch (Exception $e) { return $e->getCode() !== Http::STATUS_NOT_FOUND && $e->getCode() !== Http::STATUS_UNAUTHORIZED; } @@ -1137,7 +1200,7 @@ public function isSTTAvailable(): bool { * @return bool whether the TTS provider is available */ public function isTTSAvailable(): bool { - if ($this->isUsingOpenAi()) { + if ($this->openAiSettingsService->ttsOverrideEnabled() || $this->isUsingOpenAi()) { return true; } try { @@ -1148,7 +1211,7 @@ public function isTTSAvailable(): bool { 'response_format' => 'mp3', ]; - $this->request(null, 'audio/speech', $params, 'POST', logErrors: false); + $this->request(null, 'audio/speech', $params, 'POST', logErrors: false, serviceType: Application::SERVICE_TYPE_TTS); } catch (Exception $e) { return $e->getCode() !== Http::STATUS_NOT_FOUND && $e->getCode() !== Http::STATUS_UNAUTHORIZED; } diff --git a/lib/Service/OpenAiSettingsService.php b/lib/Service/OpenAiSettingsService.php index 1d2e1931..19003c5e 100644 --- a/lib/Service/OpenAiSettingsService.php +++ b/lib/Service/OpenAiSettingsService.php @@ -47,7 +47,31 @@ class OpenAiSettingsService { 'chat_endpoint_enabled' => 'boolean', 'basic_user' => 'string', 'basic_password' => 'string', - 'use_basic_auth' => 'boolean' + 'use_basic_auth' => 'boolean', + + 'image_url' => 'string', + 'image_service_name' => 'string', + 'image_api_key' => 'string', + 'image_basic_user' => 'string', + 'image_basic_password' => 'string', + 'image_use_basic_auth' => 'boolean', + 'image_request_timeout' => 'integer', + + 'stt_url' => 'string', + 'stt_service_name' => 'string', + 'stt_api_key' => 'string', + 'stt_basic_user' => 'string', + 'stt_basic_password' => 'string', + 'stt_use_basic_auth' => 'boolean', + 'stt_request_timeout' => 'integer', + + 'tts_url' => 'string', + 'tts_service_name' => 'string', + 'tts_api_key' => 'string', + 'tts_basic_user' => 'string', + 'tts_basic_password' => 'string', + 'tts_use_basic_auth' => 'boolean', + 'tts_request_timeout' => 'integer', ]; private const USER_CONFIG_TYPES = [ @@ -384,6 +408,153 @@ public function getUseBasicAuth(): bool { return $this->appConfig->getValueString(Application::APP_ID, 'use_basic_auth', '0', lazy: true) === '1'; } + /** + * @return string + */ + public function getImageServiceUrl(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'image_url', '', lazy: true); + } + + /** + * @return string + */ + public function getImageServiceName(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'image_service_name', '', lazy: true); + } + + /** + * @return string + */ + public function getAdminImageApiKey(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'image_api_key', '', true); + } + + /** + * @return string + */ + public function getAdminImageBasicUser(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'image_basic_user', '', lazy: true); + } + + /** + * @return string + */ + public function getAdminImageBasicPassword(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'image_basic_password', '', true); + } + + /** + * @return bool + */ + public function getAdminImageUseBasicAuth(): bool { + return $this->appConfig->getValueString(Application::APP_ID, 'image_use_basic_auth', '0', lazy: true) === '1'; + } + + /** + * @return int + */ + public function getImageRequestTimeout(): int { + return intval($this->appConfig->getValueString(Application::APP_ID, 'image_request_timeout', strval(Application::OPENAI_DEFAULT_REQUEST_TIMEOUT), lazy: true)) ?: Application::OPENAI_DEFAULT_REQUEST_TIMEOUT; + } + + /** + * @return string + */ + public function getSttServiceUrl(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'stt_url', '', lazy: true); + } + + /** + * @return string + */ + public function getSttServiceName(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'stt_service_name', '', lazy: true); + } + + /** + * @return string + */ + public function getAdminSttApiKey(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'stt_api_key', '', true); + } + + /** + * @return string + */ + public function getAdminSttBasicUser(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'stt_basic_user', '', lazy: true); + } + + /** + * @return string + */ + public function getAdminSttBasicPassword(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'stt_basic_password', '', true); + } + + /** + * @return bool + */ + public function getAdminSttUseBasicAuth(): bool { + return $this->appConfig->getValueString(Application::APP_ID, 'stt_use_basic_auth', '0', lazy: true) === '1'; + } + + /** + * @return int + */ + public function getSttRequestTimeout(): int { + return intval($this->appConfig->getValueString(Application::APP_ID, 'stt_request_timeout', strval(Application::OPENAI_DEFAULT_REQUEST_TIMEOUT), lazy: true)) ?: Application::OPENAI_DEFAULT_REQUEST_TIMEOUT; + } + + /** + * @return string + */ + public function getTtsServiceUrl(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'tts_url', '', lazy: true); + } + + /** + * @return string + */ + public function getTtsServiceName(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'tts_service_name', '', lazy: true); + } + + /** + * @return string + */ + public function getAdminTtsApiKey(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'tts_api_key', '', true); + } + + /** + * @return string + */ + public function getAdminTtsBasicUser(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'tts_basic_user', '', lazy: true); + } + + /** + * @return string + */ + public function getAdminTtsBasicPassword(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'tts_basic_password', '', true); + } + + /** + * @return bool + */ + public function getAdminTtsUseBasicAuth(): bool { + return $this->appConfig->getValueString(Application::APP_ID, 'tts_use_basic_auth', '0', lazy: true) === '1'; + } + + /** + * @return int + */ + public function getTtsRequestTimeout(): int { + return intval($this->appConfig->getValueString(Application::APP_ID, 'tts_request_timeout', strval(Application::OPENAI_DEFAULT_REQUEST_TIMEOUT), lazy: true)) ?: Application::OPENAI_DEFAULT_REQUEST_TIMEOUT; + } + /** * Get the admin config for the settings page * @return mixed[] @@ -421,7 +592,31 @@ public function getAdminConfig(): array { 'chat_endpoint_enabled' => $this->getChatEndpointEnabled(), 'basic_user' => $this->getAdminBasicUser(), 'basic_password' => $this->getAdminBasicPassword(), - 'use_basic_auth' => $this->getUseBasicAuth() + 'use_basic_auth' => $this->getUseBasicAuth(), + // Get the service details for image, stt and tts + 'image_url' => $this->getImageServiceUrl(), + 'image_service_name' => $this->getImageServiceName(), + 'image_api_key' => $this->getAdminImageApiKey(), + 'image_basic_user' => $this->getAdminImageBasicUser(), + 'image_basic_password' => $this->getAdminImageBasicPassword(), + 'image_use_basic_auth' => $this->getAdminImageUseBasicAuth(), + 'image_request_timeout' => $this->getImageRequestTimeout(), + + 'stt_url' => $this->getSttServiceUrl(), + 'stt_service_name' => $this->getSttServiceName(), + 'stt_api_key' => $this->getAdminSttApiKey(), + 'stt_basic_user' => $this->getAdminSttBasicUser(), + 'stt_basic_password' => $this->getAdminSttBasicPassword(), + 'stt_use_basic_auth' => $this->getAdminSttUseBasicAuth(), + 'stt_request_timeout' => $this->getSttRequestTimeout(), + + 'tts_url' => $this->getTtsServiceUrl(), + 'tts_service_name' => $this->getTtsServiceName(), + 'tts_api_key' => $this->getAdminTtsApiKey(), + 'tts_basic_user' => $this->getAdminTtsBasicUser(), + 'tts_basic_password' => $this->getAdminTtsBasicPassword(), + 'tts_use_basic_auth' => $this->getAdminTtsUseBasicAuth(), + 'tts_request_timeout' => $this->getTtsRequestTimeout(), ]; } @@ -801,6 +996,191 @@ public function setAdminTtsVoices(array $voices): void { $this->invalidateModelsCache(); } + /** + * @param string $url + * @return void + * @throws Exception + */ + public function setImageServiceUrl(string $url): void { + if ($url !== '' && !filter_var($url, FILTER_VALIDATE_URL)) { + throw new Exception('Invalid image service URL'); + } + $this->appConfig->setValueString(Application::APP_ID, 'image_url', $url, lazy: true); + } + + /** + * @param string $name + * @return void + */ + public function setImageServiceName(string $name): void { + $this->appConfig->setValueString(Application::APP_ID, 'image_service_name', $name, lazy: true); + } + + /** + * @param string $apiKey + * @return void + */ + public function setAdminImageApiKey(string $apiKey): void { + $this->appConfig->setValueString(Application::APP_ID, 'image_api_key', $apiKey, true, true); + } + + /** + * @param string $user + * @return void + */ + public function setAdminImageBasicUser(string $user): void { + $this->appConfig->setValueString(Application::APP_ID, 'image_basic_user', $user, lazy: true); + } + + /** + * @param string $password + * @return void + */ + public function setAdminImageBasicPassword(string $password): void { + $this->appConfig->setValueString(Application::APP_ID, 'image_basic_password', $password, true, true); + } + + /** + * @param bool $use + * @return void + */ + public function setAdminImageUseBasicAuth(bool $use): void { + $this->appConfig->setValueString(Application::APP_ID, 'image_use_basic_auth', $use ? '1' : '0', lazy: true); + } + + /** + * @param int $requestTimeout + * @return void + */ + public function setImageRequestTimeout(int $requestTimeout): void { + // Validate input: + $requestTimeout = max(1, $requestTimeout); + $this->appConfig->setValueString(Application::APP_ID, 'image_request_timeout', strval($requestTimeout), lazy: true); + } + /** + * @param string $url + * @return void + * @throws Exception + */ + public function setSttServiceUrl(string $url): void { + if ($url !== '' && !filter_var($url, FILTER_VALIDATE_URL)) { + throw new Exception('Invalid STT service URL'); + } + $this->appConfig->setValueString(Application::APP_ID, 'stt_url', $url, lazy: true); + } + + /** + * @param string $name + * @return void + */ + public function setSttServiceName(string $name): void { + $this->appConfig->setValueString(Application::APP_ID, 'stt_service_name', $name, lazy: true); + } + + /** + * @param string $apiKey + * @return void + */ + public function setAdminSttApiKey(string $apiKey): void { + $this->appConfig->setValueString(Application::APP_ID, 'stt_api_key', $apiKey, true, true); + } + + /** + * @param string $user + * @return void + */ + public function setAdminSttBasicUser(string $user): void { + $this->appConfig->setValueString(Application::APP_ID, 'stt_basic_user', $user, lazy: true); + } + + /** + * @param string $password + * @return void + */ + public function setAdminSttBasicPassword(string $password): void { + $this->appConfig->setValueString(Application::APP_ID, 'stt_basic_password', $password, true, true); + } + + /** + * @param bool $use + * @return void + */ + public function setAdminSttUseBasicAuth(bool $use): void { + $this->appConfig->setValueString(Application::APP_ID, 'stt_use_basic_auth', $use ? '1' : '0', lazy: true); + } + + /** + * @param int $requestTimeout + * @return void + */ + public function setSttRequestTimeout(int $requestTimeout): void { + // Validate input: + $requestTimeout = max(1, $requestTimeout); + $this->appConfig->setValueString(Application::APP_ID, 'stt_request_timeout', strval($requestTimeout), lazy: true); + } + + /** + * @param string $url + * @return void + * @throws Exception + */ + public function setTtsServiceUrl(string $url): void { + if ($url !== '' && !filter_var($url, FILTER_VALIDATE_URL)) { + throw new Exception('Invalid TTS service URL'); + } + $this->appConfig->setValueString(Application::APP_ID, 'tts_url', $url, lazy: true); + } + + /** + * @param string $name + * @return void + */ + public function setTtsServiceName(string $name): void { + $this->appConfig->setValueString(Application::APP_ID, 'tts_service_name', $name, lazy: true); + } + + /** + * @param string $apiKey + * @return void + */ + public function setAdminTtsApiKey(string $apiKey): void { + $this->appConfig->setValueString(Application::APP_ID, 'tts_api_key', $apiKey, true, true); + } + + /** + * @param string $user + * @return void + */ + public function setAdminTtsBasicUser(string $user): void { + $this->appConfig->setValueString(Application::APP_ID, 'tts_basic_user', $user, lazy: true); + } + + /** + * @param string $password + * @return void + */ + public function setAdminTtsBasicPassword(string $password): void { + $this->appConfig->setValueString(Application::APP_ID, 'tts_basic_password', $password, true, true); + } + + /** + * @param bool $use + * @return void + */ + public function setAdminTtsUseBasicAuth(bool $use): void { + $this->appConfig->setValueString(Application::APP_ID, 'tts_use_basic_auth', $use ? '1' : '0', lazy: true); + } + + /** + * @param int $requestTimeout + * @return void + */ + public function setTtsRequestTimeout(int $requestTimeout): void { + // Validate input: + $requestTimeout = max(1, $requestTimeout); + $this->appConfig->setValueString(Application::APP_ID, 'tts_request_timeout', strval($requestTimeout), lazy: true); + } + /** * Set the admin config for the settings page * @param mixed[] $adminConfig @@ -823,10 +1203,7 @@ public function setAdminConfig(array $adminConfig): void { $this->setRequestTimeout($adminConfig['request_timeout']); } if (isset($adminConfig['url'])) { - if (str_ends_with($adminConfig['url'], '/')) { - $adminConfig['url'] = substr($adminConfig['url'], 0, -1) ?: $adminConfig['url']; - } - $this->setServiceUrl($adminConfig['url']); + $this->setServiceUrl(rtrim($adminConfig['url'], ' /')); } if (isset($adminConfig['service_name'])) { $this->setServiceName($adminConfig['service_name']); @@ -909,6 +1286,72 @@ public function setAdminConfig(array $adminConfig): void { if (isset($adminConfig['tts_voices'])) { $this->setAdminTtsVoices($adminConfig['tts_voices']); } + + if (isset($adminConfig['image_url'])) { + $this->setImageServiceUrl(rtrim($adminConfig['image_url'], ' /')); + } + if (isset($adminConfig['image_service_name'])) { + $this->setImageServiceName($adminConfig['image_service_name']); + } + if (isset($adminConfig['image_api_key'])) { + $this->setAdminImageApiKey($adminConfig['image_api_key']); + } + if (isset($adminConfig['image_basic_user'])) { + $this->setAdminImageBasicUser($adminConfig['image_basic_user']); + } + if (isset($adminConfig['image_basic_password'])) { + $this->setAdminImageBasicPassword($adminConfig['image_basic_password']); + } + if (isset($adminConfig['image_use_basic_auth'])) { + $this->setAdminImageUseBasicAuth($adminConfig['image_use_basic_auth']); + } + if (isset($adminConfig['image_request_timeout'])) { + $this->setImageRequestTimeout($adminConfig['image_request_timeout']); + } + + if (isset($adminConfig['stt_url'])) { + $this->setSttServiceUrl(rtrim($adminConfig['stt_url'], ' /')); + } + if (isset($adminConfig['stt_service_name'])) { + $this->setSttServiceName($adminConfig['stt_service_name']); + } + if (isset($adminConfig['stt_api_key'])) { + $this->setAdminSttApiKey($adminConfig['stt_api_key']); + } + if (isset($adminConfig['stt_basic_user'])) { + $this->setAdminSttBasicUser($adminConfig['stt_basic_user']); + } + if (isset($adminConfig['stt_basic_password'])) { + $this->setAdminSttBasicPassword($adminConfig['stt_basic_password']); + } + if (isset($adminConfig['stt_use_basic_auth'])) { + $this->setAdminSttUseBasicAuth($adminConfig['stt_use_basic_auth']); + } + if (isset($adminConfig['stt_request_timeout'])) { + $this->setSttRequestTimeout($adminConfig['stt_request_timeout']); + } + + if (isset($adminConfig['tts_url'])) { + $this->setTtsServiceUrl(rtrim($adminConfig['tts_url'], ' /')); + } + if (isset($adminConfig['tts_service_name'])) { + $this->setTtsServiceName($adminConfig['tts_service_name']); + } + if (isset($adminConfig['tts_api_key'])) { + $this->setAdminTtsApiKey($adminConfig['tts_api_key']); + } + if (isset($adminConfig['tts_basic_user'])) { + $this->setAdminTtsBasicUser($adminConfig['tts_basic_user']); + } + if (isset($adminConfig['tts_basic_password'])) { + $this->setAdminTtsBasicPassword($adminConfig['tts_basic_password']); + } + if (isset($adminConfig['tts_use_basic_auth'])) { + $this->setAdminTtsUseBasicAuth($adminConfig['tts_use_basic_auth']); + } + if (isset($adminConfig['tts_request_timeout'])) { + $this->setTtsRequestTimeout($adminConfig['tts_request_timeout']); + } } /** @@ -1010,4 +1453,25 @@ public function setAnalyzeImageProviderEnabled(bool $enabled): void { public function setChatEndpointEnabled(bool $enabled): void { $this->appConfig->setValueString(Application::APP_ID, 'chat_endpoint_enabled', $enabled ? '1' : '0', lazy: true); } + + /** + * @return bool + */ + public function imageOverrideEnabled(): bool { + return !empty($this->getImageServiceUrl()); + } + + /** + * @return bool + */ + public function sttOverrideEnabled(): bool { + return !empty($this->getSttServiceUrl()); + } + + /** + * @return bool + */ + public function ttsOverrideEnabled(): bool { + return !empty($this->getTtsServiceUrl()); + } } diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index 05c3442e..af3b8b2d 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -31,6 +31,12 @@ public function getForm(): TemplateResponse { $adminConfig = $this->openAiSettingsService->getAdminConfig(); $adminConfig['api_key'] = $adminConfig['api_key'] === '' ? '' : 'dummyApiKey'; $adminConfig['basic_password'] = $adminConfig['basic_password'] === '' ? '' : 'dummyPassword'; + $adminConfig['image_api_key'] = $adminConfig['image_api_key'] === '' ? '' : 'dummyApiKey'; + $adminConfig['image_basic_password'] = $adminConfig['image_basic_password'] === '' ? '' : 'dummyPassword'; + $adminConfig['stt_api_key'] = $adminConfig['stt_api_key'] === '' ? '' : 'dummyApiKey'; + $adminConfig['stt_basic_password'] = $adminConfig['stt_basic_password'] === '' ? '' : 'dummyPassword'; + $adminConfig['tts_api_key'] = $adminConfig['tts_api_key'] === '' ? '' : 'dummyApiKey'; + $adminConfig['tts_basic_password'] = $adminConfig['tts_basic_password'] === '' ? '' : 'dummyPassword'; $isAssistantEnabled = $this->appManager->isEnabledForUser('assistant'); $adminConfig['assistant_enabled'] = $isAssistantEnabled; $adminConfig['quota_start_date'] = $this->openAiSettingsService->getQuotaStart(); diff --git a/lib/TaskProcessing/AudioToTextProvider.php b/lib/TaskProcessing/AudioToTextProvider.php index 2f0d9fc9..c9446345 100644 --- a/lib/TaskProcessing/AudioToTextProvider.php +++ b/lib/TaskProcessing/AudioToTextProvider.php @@ -38,7 +38,7 @@ public function getId(): string { } public function getName(): string { - return $this->openAiAPIService->getServiceName(); + return $this->openAiAPIService->getServiceName(Application::SERVICE_TYPE_STT); } public function getTaskTypeId(): string { diff --git a/lib/TaskProcessing/TextToImageProvider.php b/lib/TaskProcessing/TextToImageProvider.php index 5b0e9129..44632f33 100644 --- a/lib/TaskProcessing/TextToImageProvider.php +++ b/lib/TaskProcessing/TextToImageProvider.php @@ -41,9 +41,7 @@ public function getId(): string { } public function getName(): string { - return $this->openAiAPIService->isUsingOpenAi() - ? $this->l->t('OpenAI\'s DALL-E 2') - : $this->openAiAPIService->getServiceName(); + return $this->openAiAPIService->getServiceName(Application::SERVICE_TYPE_IMAGE); } public function getTaskTypeId(): string { @@ -82,12 +80,12 @@ public function getOptionalInputShape(): array { public function getOptionalInputShapeEnumValues(): array { return [ - 'model' => $this->openAiAPIService->getModelEnumValues($this->userId), + 'model' => $this->openAiAPIService->getModelEnumValues($this->userId, serviceType: Application::SERVICE_TYPE_IMAGE), ]; } public function getOptionalInputShapeDefaults(): array { - $adminModel = $this->openAiAPIService->isUsingOpenAi() + $adminModel = $this->openAiAPIService->isUsingOpenAi(Application::SERVICE_TYPE_IMAGE) ? ($this->appConfig->getValueString(Application::APP_ID, 'default_image_model_id', Application::DEFAULT_MODEL_ID, lazy: true) ?: Application::DEFAULT_MODEL_ID) : $this->appConfig->getValueString(Application::APP_ID, 'default_image_model_id', lazy: true); return [ diff --git a/lib/TaskProcessing/TextToSpeechProvider.php b/lib/TaskProcessing/TextToSpeechProvider.php index b02d2e69..0f854ad5 100644 --- a/lib/TaskProcessing/TextToSpeechProvider.php +++ b/lib/TaskProcessing/TextToSpeechProvider.php @@ -38,9 +38,7 @@ public function getId(): string { } public function getName(): string { - return $this->openAiAPIService->isUsingOpenAi() - ? $this->l->t('OpenAI\'s Text to Speech') - : $this->openAiAPIService->getServiceName(); + return $this->openAiAPIService->getServiceName(Application::SERVICE_TYPE_TTS); } public function getTaskTypeId(): string { @@ -77,7 +75,7 @@ public function getOptionalInputShape(): array { ), 'speed' => new ShapeDescriptor( $this->l->t('Speed'), - $this->openAiAPIService->isUsingOpenAi() + $this->openAiAPIService->isUsingOpenAi(Application::SERVICE_TYPE_TTS) ? $this->l->t('Speech speed modifier (Valid values: 0.25-4)') : $this->l->t('Speech speed modifier'), EShapeType::Number @@ -89,7 +87,7 @@ public function getOptionalInputShapeEnumValues(): array { $voices = json_decode($this->appConfig->getValueString(Application::APP_ID, 'tts_voices', lazy: true)) ?: Application::DEFAULT_SPEECH_VOICES; return [ 'voice' => array_map(function ($v) { return new ShapeEnumValue($v, $v); }, $voices), - 'model' => $this->openAiAPIService->getModelEnumValues($this->userId), + 'model' => $this->openAiAPIService->getModelEnumValues($this->userId, Application::SERVICE_TYPE_TTS), ]; } @@ -143,7 +141,7 @@ public function process(?string $userId, array $input, callable $reportProgress, $speed = 1; if (isset($input['speed']) && is_numeric($input['speed'])) { $speed = $input['speed']; - if ($this->openAiAPIService->isUsingOpenAi()) { + if ($this->openAiAPIService->isUsingOpenAi(Application::SERVICE_TYPE_TTS)) { if ($speed > 4) { $speed = 4; } elseif ($speed < 0.25) { diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index da93ea1f..65d41177 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -232,7 +232,7 @@ v-model="selectedModel.text" class="model-select" :clearable="state.default_completion_model_id !== DEFAULT_MODEL_ITEM.id" - :options="formattedModels" + :options="formattedModels(models)" :input-label="t('integration_openai', 'Default completion model to use')" :no-wrap="true" input-id="openai-model-select" @@ -331,13 +331,23 @@