Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Negative:

Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud.com/blog/nextcloud-ethical-ai-rating/).
]]> </description>
<version>3.8.0</version>
<version>3.9.0</version>
<licence>agpl</licence>
<author>Julien Veyssier</author>
<namespace>OpenAi</namespace>
Expand All @@ -121,6 +121,7 @@ Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud
</dependencies>
<background-jobs>
<job>OCA\OpenAi\Cron\CleanupQuotaDb</job>
<job>OCA\OpenAi\Cron\RefreshModels</job>
</background-jobs>
<settings>
<admin>OCA\OpenAi\Settings\Admin</admin>
Expand Down
2 changes: 1 addition & 1 deletion lib/Controller/OpenAiAPIController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
31 changes: 31 additions & 0 deletions lib/Cron/RefreshModels.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\OpenAi\Cron;

use OCA\OpenAi\Service\OpenAiAPIService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use Psr\Log\LoggerInterface;

class RefreshModels extends TimedJob {
public function __construct(
ITimeFactory $time,
private OpenAiAPIService $openAIAPIService,
private LoggerInterface $logger,
) {
parent::__construct($time);
$this->setInterval(60 * 60 * 24); // Daily
}

protected function run($argument) {
$this->logger->debug('Run daily model refresh job');
$this->openAIAPIService->getModels(null, true);
}
}
34 changes: 34 additions & 0 deletions lib/Migration/Version030900Date20251006152735.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\OpenAi\Migration;

use Closure;
use OCA\OpenAi\Service\OpenAiAPIService;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version030900Date20251006152735 extends SimpleMigrationStep {

public function __construct(
private OpenAIAPIService $openAIAPIService,
) {
}

/**
* @param IOutput $output
* @param Closure $schemaClosure
* @param array $options
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
// we refresh the model list to make sure they are stored in oc_appconfig
// so they are available immediately after the app upgrade to populate the task types enum values
$this->openAIAPIService->getModels(null, true);
}
}
102 changes: 57 additions & 45 deletions lib/Service/OpenAiAPIService.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
class OpenAiAPIService {
private IClient $client;
private ?array $modelsMemoryCache = null;
private ?bool $areCredsValid = null;

public function __construct(
private LoggerInterface $logger,
Expand Down Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The memory cache is never populated when the value is retrieved from the distributed cache. Might be nice to do that

}
}

// 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'])) {
Expand All @@ -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;
}

Expand Down
Loading