diff --git a/appinfo/info.xml b/appinfo/info.xml index 75449e62..6f388944 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -101,7 +101,7 @@ Negative: Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud.com/blog/nextcloud-ethical-ai-rating/). ]]> - 3.8.0 + 3.9.0 agpl Julien Veyssier OpenAi @@ -121,6 +121,7 @@ Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud OCA\OpenAi\Cron\CleanupQuotaDb + OCA\OpenAi\Cron\RefreshModels OCA\OpenAi\Settings\Admin diff --git a/lib/Controller/OpenAiAPIController.php b/lib/Controller/OpenAiAPIController.php index 3860942e..df7e9114 100644 --- a/lib/Controller/OpenAiAPIController.php +++ b/lib/Controller/OpenAiAPIController.php @@ -31,7 +31,7 @@ public function __construct( #[NoAdminRequired] public function getModels(): DataResponse { try { - $response = $this->openAiAPIService->getModels($this->userId); + $response = $this->openAiAPIService->getModels($this->userId, true); return new DataResponse($response); } catch (Exception $e) { $code = $e->getCode() === 0 ? Http::STATUS_BAD_REQUEST : intval($e->getCode()); diff --git a/lib/Cron/RefreshModels.php b/lib/Cron/RefreshModels.php new file mode 100644 index 00000000..0de60679 --- /dev/null +++ b/lib/Cron/RefreshModels.php @@ -0,0 +1,31 @@ +setInterval(60 * 60 * 24); // Daily + } + + protected function run($argument) { + $this->logger->debug('Run daily model refresh job'); + $this->openAIAPIService->getModels(null, true); + } +} diff --git a/lib/Migration/Version030900Date20251006152735.php b/lib/Migration/Version030900Date20251006152735.php new file mode 100644 index 00000000..a721fc98 --- /dev/null +++ b/lib/Migration/Version030900Date20251006152735.php @@ -0,0 +1,34 @@ +openAIAPIService->getModels(null, true); + } +} diff --git a/lib/Service/OpenAiAPIService.php b/lib/Service/OpenAiAPIService.php index c08cf038..09b658df 100644 --- a/lib/Service/OpenAiAPIService.php +++ b/lib/Service/OpenAiAPIService.php @@ -39,7 +39,6 @@ class OpenAiAPIService { private IClient $client; private ?array $modelsMemoryCache = null; - private ?bool $areCredsValid = null; public function __construct( private LoggerInterface $logger, @@ -111,68 +110,80 @@ private function isModelListValid($models): bool { /** * @param ?string $userId + * @param bool $refresh * @return array|string[] * @throws Exception */ - public function getModels(?string $userId): array { - // caching against 'getModelEnumValues' calls from all the providers - if ($this->areCredsValid === false) { - $this->logger->info('Cannot get OpenAI models without an API key'); - return []; - } elseif ($this->areCredsValid === null) { - if ($this->isUsingOpenAi() && $this->openAiSettingsService->getUserApiKey($userId, true) === '') { - $this->areCredsValid = false; - $this->logger->info('Cannot get OpenAI models without an API key'); - return []; - } - $this->areCredsValid = true; - } - - if ($this->modelsMemoryCache !== null) { - $this->logger->debug('Getting OpenAI models from the memory cache'); - return $this->modelsMemoryCache; - } - + public function getModels(?string $userId, bool $refresh = false): array { + $cache = $this->cacheFactory->createDistributed(Application::APP_ID); $userCacheKey = Application::MODELS_CACHE_KEY . '_' . ($userId ?? ''); $adminCacheKey = Application::MODELS_CACHE_KEY . '-main'; - $cache = $this->cacheFactory->createDistributed(Application::APP_ID); - // try to get models from the user cache first - if ($userId !== null) { - $userCachedModels = $cache->get($userCacheKey); - if ($userCachedModels) { - $this->logger->debug('Getting OpenAI models from user cache for user ' . $userId); - return $userCachedModels; + if (!$refresh) { + if ($this->modelsMemoryCache !== null) { + $this->logger->debug('Getting OpenAI models from the memory cache'); + return $this->modelsMemoryCache; } - } - // if the user has an API key or uses basic auth, skip the admin cache - if (!( - $this->openAiSettingsService->getUserApiKey($userId, false) !== '' - || ( - $this->openAiSettingsService->getUseBasicAuth() - && $this->openAiSettingsService->getUserBasicUser($userId) !== '' - && $this->openAiSettingsService->getUserBasicPassword($userId) !== '' - ) - )) { - // if no user cache or userId is null, try to get from the admin cache - if ($adminCachedModels = $cache->get($adminCacheKey)) { - $this->logger->debug('Getting OpenAI models from the main distributed cache'); - return $adminCachedModels; + // try to get models from the user cache first + if ($userId !== null) { + $userCachedModels = $cache->get($userCacheKey); + if ($userCachedModels) { + $this->logger->debug('Getting OpenAI models from user cache for user ' . $userId); + $this->modelsMemoryCache = $userCachedModels; + return $userCachedModels; + } } + + // if the user has an API key or uses basic auth, skip the admin cache + if ($userId === null || ( + $this->openAiSettingsService->getUserApiKey($userId, false) === '' + && ( + !$this->openAiSettingsService->getUseBasicAuth() + || $this->openAiSettingsService->getUserBasicUser($userId) === '' + || $this->openAiSettingsService->getUserBasicPassword($userId) === '' + ) + )) { + // here we know there is either no user cache or userId is null + // so if there is no user-defined service credentials + // 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; + 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"}'); + $fallbackModels = [ + 'data' => [], + 'object' => 'list', + ]; + try { + $newCache = json_decode($modelsObjectString, true) ?? $fallbackModels; + } catch (Throwable $e) { + $this->logger->warning('Could not decode the model JSON string', ['model_string', $modelsObjectString, 'exception' => $e]); + $newCache = $fallbackModels; + } + $cache->set($userId !== null ? $userCacheKey : $adminCacheKey, $newCache, Application::MODELS_CACHE_TTL); + $this->modelsMemoryCache = $newCache; + return $newCache; } + // we know we are refreshing so we clear the caches and make the network request + $cache->remove($adminCacheKey); + $cache->remove($userCacheKey); + try { $this->logger->debug('Actually getting OpenAI models with a network request'); $modelsResponse = $this->request($userId, 'models'); } catch (Exception $e) { $this->logger->warning('Error retrieving models (exc): ' . $e->getMessage()); - $this->areCredsValid = false; throw $e; } if (isset($modelsResponse['error'])) { $this->logger->warning('Error retrieving models: ' . json_encode($modelsResponse)); - $this->areCredsValid = false; throw new Exception($modelsResponse['error'], Http::STATUS_INTERNAL_SERVER_ERROR); } if (!isset($modelsResponse['data'])) { @@ -182,13 +193,14 @@ public function getModels(?string $userId): array { if (!$this->isModelListValid($modelsResponse['data'])) { $this->logger->warning('Invalid models response: ' . json_encode($modelsResponse)); - $this->areCredsValid = false; throw new Exception($this->l10n->t('Invalid models response received'), Http::STATUS_INTERNAL_SERVER_ERROR); } $cache->set($userId !== null ? $userCacheKey : $adminCacheKey, $modelsResponse, Application::MODELS_CACHE_TTL); $this->modelsMemoryCache = $modelsResponse; - $this->areCredsValid = true; + // we always store the model list after getting it + $modelsObjectString = json_encode($modelsResponse); + $this->appConfig->setValueString(Application::APP_ID, 'models', $modelsObjectString); return $modelsResponse; }