From 6094fb1e87d01fdc377dc1907707e720539eca95 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 6 Oct 2025 15:50:15 +0200 Subject: [PATCH 1/2] store the model list in the DB, only refresh it when the settings page is accessed and on upgrade to 3.9.0 and once a day in a bg job Signed-off-by: Julien Veyssier --- appinfo/info.xml | 3 +- lib/Controller/OpenAiAPIController.php | 2 +- lib/Cron/RefreshModels.php | 31 ++++++ .../Version030900Date20251006152735.php | 34 +++++++ lib/Service/OpenAiAPIService.php | 97 ++++++++++--------- 5 files changed, 121 insertions(+), 46 deletions(-) create mode 100644 lib/Cron/RefreshModels.php create mode 100644 lib/Migration/Version030900Date20251006152735.php 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..3ecbcf74 --- /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..96be9fc4 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,77 @@ 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 []; + public function getModels(?string $userId, bool $refresh = false): array { + $cache = $this->cacheFactory->createDistributed(Application::APP_ID); + if (!$refresh) { + if ($this->modelsMemoryCache !== null) { + $this->logger->debug('Getting OpenAI models from the memory cache'); + return $this->modelsMemoryCache; } - $this->areCredsValid = true; - } - if ($this->modelsMemoryCache !== null) { - $this->logger->debug('Getting OpenAI models from the memory cache'); - return $this->modelsMemoryCache; - } + $userCacheKey = Application::MODELS_CACHE_KEY . '_' . ($userId ?? ''); + $adminCacheKey = Application::MODELS_CACHE_KEY . '-main'; - $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; + } + } - // 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 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; + } } - } - // 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; + // 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) { + $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 + $adminCacheKey = Application::MODELS_CACHE_KEY . '-main'; + $cache->remove($adminCacheKey); + $userCacheKey = Application::MODELS_CACHE_KEY . '_' . ($userId ?? ''); + $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 +190,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; } From ead25f31920aec0714cebfeedf75d7877a1a993f Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Wed, 8 Oct 2025 09:57:57 +0200 Subject: [PATCH 2/2] address review comments, make conditions more explicit, add warning log Signed-off-by: Julien Veyssier --- .../Version030900Date20251006152735.php | 2 +- lib/Service/OpenAiAPIService.php | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/Migration/Version030900Date20251006152735.php b/lib/Migration/Version030900Date20251006152735.php index 3ecbcf74..a721fc98 100644 --- a/lib/Migration/Version030900Date20251006152735.php +++ b/lib/Migration/Version030900Date20251006152735.php @@ -3,7 +3,7 @@ declare(strict_types=1); /** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ diff --git a/lib/Service/OpenAiAPIService.php b/lib/Service/OpenAiAPIService.php index 96be9fc4..09b658df 100644 --- a/lib/Service/OpenAiAPIService.php +++ b/lib/Service/OpenAiAPIService.php @@ -116,36 +116,40 @@ private function isModelListValid($models): bool { */ 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'; + if (!$refresh) { if ($this->modelsMemoryCache !== null) { $this->logger->debug('Getting OpenAI models from the memory cache'); return $this->modelsMemoryCache; } - $userCacheKey = Application::MODELS_CACHE_KEY . '_' . ($userId ?? ''); - $adminCacheKey = Application::MODELS_CACHE_KEY . '-main'; - // 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 (!( - $this->openAiSettingsService->getUserApiKey($userId, false) !== '' - || ( - $this->openAiSettingsService->getUseBasicAuth() - && $this->openAiSettingsService->getUserBasicUser($userId) !== '' - && $this->openAiSettingsService->getUserBasicPassword($userId) !== '' + if ($userId === null || ( + $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 + // 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; } } @@ -159,6 +163,7 @@ public function getModels(?string $userId, bool $refresh = false): array { 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); @@ -167,9 +172,7 @@ public function getModels(?string $userId, bool $refresh = false): array { } // we know we are refreshing so we clear the caches and make the network request - $adminCacheKey = Application::MODELS_CACHE_KEY . '-main'; $cache->remove($adminCacheKey); - $userCacheKey = Application::MODELS_CACHE_KEY . '_' . ($userId ?? ''); $cache->remove($userCacheKey); try {