Skip to content

Commit 26fedab

Browse files
authored
Merge pull request #277 from nextcloud/fix/272/unreachable-service
Store the model list in the DB
2 parents 0e1f142 + ead25f3 commit 26fedab

File tree

5 files changed

+125
-47
lines changed

5 files changed

+125
-47
lines changed

appinfo/info.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ Negative:
101101
102102
Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud.com/blog/nextcloud-ethical-ai-rating/).
103103
]]> </description>
104-
<version>3.8.0</version>
104+
<version>3.9.0</version>
105105
<licence>agpl</licence>
106106
<author>Julien Veyssier</author>
107107
<namespace>OpenAi</namespace>
@@ -121,6 +121,7 @@ Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud
121121
</dependencies>
122122
<background-jobs>
123123
<job>OCA\OpenAi\Cron\CleanupQuotaDb</job>
124+
<job>OCA\OpenAi\Cron\RefreshModels</job>
124125
</background-jobs>
125126
<settings>
126127
<admin>OCA\OpenAi\Settings\Admin</admin>

lib/Controller/OpenAiAPIController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function __construct(
3131
#[NoAdminRequired]
3232
public function getModels(): DataResponse {
3333
try {
34-
$response = $this->openAiAPIService->getModels($this->userId);
34+
$response = $this->openAiAPIService->getModels($this->userId, true);
3535
return new DataResponse($response);
3636
} catch (Exception $e) {
3737
$code = $e->getCode() === 0 ? Http::STATUS_BAD_REQUEST : intval($e->getCode());

lib/Cron/RefreshModels.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\OpenAi\Cron;
11+
12+
use OCA\OpenAi\Service\OpenAiAPIService;
13+
use OCP\AppFramework\Utility\ITimeFactory;
14+
use OCP\BackgroundJob\TimedJob;
15+
use Psr\Log\LoggerInterface;
16+
17+
class RefreshModels extends TimedJob {
18+
public function __construct(
19+
ITimeFactory $time,
20+
private OpenAiAPIService $openAIAPIService,
21+
private LoggerInterface $logger,
22+
) {
23+
parent::__construct($time);
24+
$this->setInterval(60 * 60 * 24); // Daily
25+
}
26+
27+
protected function run($argument) {
28+
$this->logger->debug('Run daily model refresh job');
29+
$this->openAIAPIService->getModels(null, true);
30+
}
31+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\OpenAi\Migration;
11+
12+
use Closure;
13+
use OCA\OpenAi\Service\OpenAiAPIService;
14+
use OCP\Migration\IOutput;
15+
use OCP\Migration\SimpleMigrationStep;
16+
17+
class Version030900Date20251006152735 extends SimpleMigrationStep {
18+
19+
public function __construct(
20+
private OpenAIAPIService $openAIAPIService,
21+
) {
22+
}
23+
24+
/**
25+
* @param IOutput $output
26+
* @param Closure $schemaClosure
27+
* @param array $options
28+
*/
29+
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
30+
// we refresh the model list to make sure they are stored in oc_appconfig
31+
// so they are available immediately after the app upgrade to populate the task types enum values
32+
$this->openAIAPIService->getModels(null, true);
33+
}
34+
}

lib/Service/OpenAiAPIService.php

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
class OpenAiAPIService {
4040
private IClient $client;
4141
private ?array $modelsMemoryCache = null;
42-
private ?bool $areCredsValid = null;
4342

4443
public function __construct(
4544
private LoggerInterface $logger,
@@ -111,68 +110,80 @@ private function isModelListValid($models): bool {
111110

112111
/**
113112
* @param ?string $userId
113+
* @param bool $refresh
114114
* @return array|string[]
115115
* @throws Exception
116116
*/
117-
public function getModels(?string $userId): array {
118-
// caching against 'getModelEnumValues' calls from all the providers
119-
if ($this->areCredsValid === false) {
120-
$this->logger->info('Cannot get OpenAI models without an API key');
121-
return [];
122-
} elseif ($this->areCredsValid === null) {
123-
if ($this->isUsingOpenAi() && $this->openAiSettingsService->getUserApiKey($userId, true) === '') {
124-
$this->areCredsValid = false;
125-
$this->logger->info('Cannot get OpenAI models without an API key');
126-
return [];
127-
}
128-
$this->areCredsValid = true;
129-
}
130-
131-
if ($this->modelsMemoryCache !== null) {
132-
$this->logger->debug('Getting OpenAI models from the memory cache');
133-
return $this->modelsMemoryCache;
134-
}
135-
117+
public function getModels(?string $userId, bool $refresh = false): array {
118+
$cache = $this->cacheFactory->createDistributed(Application::APP_ID);
136119
$userCacheKey = Application::MODELS_CACHE_KEY . '_' . ($userId ?? '');
137120
$adminCacheKey = Application::MODELS_CACHE_KEY . '-main';
138-
$cache = $this->cacheFactory->createDistributed(Application::APP_ID);
139121

140-
// try to get models from the user cache first
141-
if ($userId !== null) {
142-
$userCachedModels = $cache->get($userCacheKey);
143-
if ($userCachedModels) {
144-
$this->logger->debug('Getting OpenAI models from user cache for user ' . $userId);
145-
return $userCachedModels;
122+
if (!$refresh) {
123+
if ($this->modelsMemoryCache !== null) {
124+
$this->logger->debug('Getting OpenAI models from the memory cache');
125+
return $this->modelsMemoryCache;
146126
}
147-
}
148127

149-
// if the user has an API key or uses basic auth, skip the admin cache
150-
if (!(
151-
$this->openAiSettingsService->getUserApiKey($userId, false) !== ''
152-
|| (
153-
$this->openAiSettingsService->getUseBasicAuth()
154-
&& $this->openAiSettingsService->getUserBasicUser($userId) !== ''
155-
&& $this->openAiSettingsService->getUserBasicPassword($userId) !== ''
156-
)
157-
)) {
158-
// if no user cache or userId is null, try to get from the admin cache
159-
if ($adminCachedModels = $cache->get($adminCacheKey)) {
160-
$this->logger->debug('Getting OpenAI models from the main distributed cache');
161-
return $adminCachedModels;
128+
// try to get models from the user cache first
129+
if ($userId !== null) {
130+
$userCachedModels = $cache->get($userCacheKey);
131+
if ($userCachedModels) {
132+
$this->logger->debug('Getting OpenAI models from user cache for user ' . $userId);
133+
$this->modelsMemoryCache = $userCachedModels;
134+
return $userCachedModels;
135+
}
162136
}
137+
138+
// if the user has an API key or uses basic auth, skip the admin cache
139+
if ($userId === null || (
140+
$this->openAiSettingsService->getUserApiKey($userId, false) === ''
141+
&& (
142+
!$this->openAiSettingsService->getUseBasicAuth()
143+
|| $this->openAiSettingsService->getUserBasicUser($userId) === ''
144+
|| $this->openAiSettingsService->getUserBasicPassword($userId) === ''
145+
)
146+
)) {
147+
// here we know there is either no user cache or userId is null
148+
// so if there is no user-defined service credentials
149+
// we try to get the models from the admin cache
150+
if ($adminCachedModels = $cache->get($adminCacheKey)) {
151+
$this->logger->debug('Getting OpenAI models from the main distributed cache');
152+
$this->modelsMemoryCache = $adminCachedModels;
153+
return $adminCachedModels;
154+
}
155+
}
156+
157+
// 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
158+
$modelsObjectString = $this->appConfig->getValueString(Application::APP_ID, 'models', '{"data":[],"object":"list"}');
159+
$fallbackModels = [
160+
'data' => [],
161+
'object' => 'list',
162+
];
163+
try {
164+
$newCache = json_decode($modelsObjectString, true) ?? $fallbackModels;
165+
} catch (Throwable $e) {
166+
$this->logger->warning('Could not decode the model JSON string', ['model_string', $modelsObjectString, 'exception' => $e]);
167+
$newCache = $fallbackModels;
168+
}
169+
$cache->set($userId !== null ? $userCacheKey : $adminCacheKey, $newCache, Application::MODELS_CACHE_TTL);
170+
$this->modelsMemoryCache = $newCache;
171+
return $newCache;
163172
}
164173

174+
// we know we are refreshing so we clear the caches and make the network request
175+
$cache->remove($adminCacheKey);
176+
$cache->remove($userCacheKey);
177+
165178
try {
166179
$this->logger->debug('Actually getting OpenAI models with a network request');
167180
$modelsResponse = $this->request($userId, 'models');
168181
} catch (Exception $e) {
169182
$this->logger->warning('Error retrieving models (exc): ' . $e->getMessage());
170-
$this->areCredsValid = false;
171183
throw $e;
172184
}
173185
if (isset($modelsResponse['error'])) {
174186
$this->logger->warning('Error retrieving models: ' . json_encode($modelsResponse));
175-
$this->areCredsValid = false;
176187
throw new Exception($modelsResponse['error'], Http::STATUS_INTERNAL_SERVER_ERROR);
177188
}
178189
if (!isset($modelsResponse['data'])) {
@@ -182,13 +193,14 @@ public function getModels(?string $userId): array {
182193

183194
if (!$this->isModelListValid($modelsResponse['data'])) {
184195
$this->logger->warning('Invalid models response: ' . json_encode($modelsResponse));
185-
$this->areCredsValid = false;
186196
throw new Exception($this->l10n->t('Invalid models response received'), Http::STATUS_INTERNAL_SERVER_ERROR);
187197
}
188198

189199
$cache->set($userId !== null ? $userCacheKey : $adminCacheKey, $modelsResponse, Application::MODELS_CACHE_TTL);
190200
$this->modelsMemoryCache = $modelsResponse;
191-
$this->areCredsValid = true;
201+
// we always store the model list after getting it
202+
$modelsObjectString = json_encode($modelsResponse);
203+
$this->appConfig->setValueString(Application::APP_ID, 'models', $modelsObjectString);
192204
return $modelsResponse;
193205
}
194206

0 commit comments

Comments
 (0)