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
11 changes: 6 additions & 5 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@

module.exports = {
globals: {
appVersion: true
appVersion: true,
},
parserOptions: {
requireConfigFile: false
requireConfigFile: false,
},
extends: [
'@nextcloud'
'@nextcloud',
],
rules: {
'jsdoc/require-jsdoc': 'off',
'jsdoc/tag-lines': 'off',
'vue/first-attribute-linebreak': 'off',
'import/extensions': 'off'
}
'import/extensions': 'off',
'vue/no-v-model-argument': 'off',
},
}
1 change: 1 addition & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class Application extends App implements IBootstrap {
public const MIN_CHUNK_SIZE = 500;
public const DEFAULT_MAX_NUM_OF_TOKENS = 1000;
public const DEFAULT_QUOTA_PERIOD = 30;
public const DEFAULT_QUOTA_CONFIG = ['length' => self::DEFAULT_QUOTA_PERIOD, 'unit' => 'day', 'day' => 1];

public const DEFAULT_OPENAI_TEXT_GENERATION_TIME = 10; // seconds
public const DEFAULT_LOCALAI_TEXT_GENERATION_TIME = 60; // seconds
Expand Down
18 changes: 8 additions & 10 deletions lib/Cron/CleanupQuotaDb.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,33 @@

use OCA\OpenAi\AppInfo\Application;
use OCA\OpenAi\Db\QuotaUsageMapper;
use OCA\OpenAi\Service\OpenAiSettingsService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\IAppConfig;
use Psr\Log\LoggerInterface;

class CleanupQuotaDb extends TimedJob {
public function __construct(
ITimeFactory $time,
private QuotaUsageMapper $quotaUsageMapper,
private LoggerInterface $logger,
private IAppConfig $appConfig,
private OpenAiSettingsService $openAiSettingsService,
) {
parent::__construct($time);
$this->setInterval(60 * 60 * 24); // Daily
}

protected function run($argument) {
$this->logger->debug('Run cleanup job for OpenAI quota db');
$quota = $this->openAiSettingsService->getQuotaPeriod();
$days = $quota['length'];
if ($quota['unit'] == 'month') {
$days *= 30;
}
$this->quotaUsageMapper->cleanupQuotaUsages(
// The mimimum period is limited to DEFAULT_QUOTA_PERIOD to not lose
// the stored quota usage data below this limit.
max(
intval($this->appConfig->getValueString(
Application::APP_ID,
'quota_period',
strval(Application::DEFAULT_QUOTA_PERIOD)
)),
Application::DEFAULT_QUOTA_PERIOD
)
max($days, Application::DEFAULT_QUOTA_PERIOD)
);

}
Expand Down
14 changes: 4 additions & 10 deletions lib/Db/QuotaUsageMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,16 @@ public function getQuotaUsageOfUser(int $id, string $userId): QuotaUsage {

/**
* @param int $type Type of the quota
* @param int $timePeriod Time period in days
* @param int $periodStart Start time of quota
* @return int
* @throws DoesNotExistException
* @throws Exception
* @throws MultipleObjectsReturnedException
* @throws \RuntimeException
*/
public function getQuotaUnitsInTimePeriod(int $type, int $timePeriod): int {
public function getQuotaUnitsInTimePeriod(int $type, int $periodStart): int {
$qb = $this->db->getQueryBuilder();

// Get a timestamp of the beginning of the time period
$periodStart = (new DateTime())->sub(new DateInterval('P' . $timePeriod . 'D'))->getTimestamp();

// Get the sum of the units used in the time period
$qb->select($qb->createFunction('SUM(units)'))
->from($this->getTableName())
Expand All @@ -103,19 +100,16 @@ public function getQuotaUnitsInTimePeriod(int $type, int $timePeriod): int {
/**
* @param string $userId
* @param int $type Type of the quota
* @param int $timePeriod Time period in days
* @param int $periodStart Start time of quota
* @return int
* @throws DoesNotExistException
* @throws Exception
* @throws MultipleObjectsReturnedException
* @throws \RuntimeException
*/
public function getQuotaUnitsOfUserInTimePeriod(string $userId, int $type, int $timePeriod): int {
public function getQuotaUnitsOfUserInTimePeriod(string $userId, int $type, int $periodStart): int {
$qb = $this->db->getQueryBuilder();

// Get a timestamp of the beginning of the time period
$periodStart = (new DateTime())->sub(new DateInterval('P' . $timePeriod . 'D'))->getTimestamp();

// Get the sum of the units used in the time period
$qb->select($qb->createFunction('SUM(units)'))
->from($this->getTableName())
Expand Down
16 changes: 10 additions & 6 deletions lib/Service/OpenAiAPIService.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,10 @@ public function isQuotaExceeded(?string $userId, int $type): bool {
return false;
}

$quotaPeriod = $this->openAiSettingsService->getQuotaPeriod();
$quotaStart = $this->openAiSettingsService->getQuotaStart();

try {
$quotaUsage = $this->quotaUsageMapper->getQuotaUnitsOfUserInTimePeriod($userId, $type, $quotaPeriod);
$quotaUsage = $this->quotaUsageMapper->getQuotaUnitsOfUserInTimePeriod($userId, $type, $quotaStart);
} catch (DoesNotExistException|MultipleObjectsReturnedException|DBException|RuntimeException $e) {
$this->logger->warning('Could not retrieve quota usage for user: ' . $userId . ' and quota type: ' . $type . '. Error: ' . $e->getMessage());
throw new Exception('Could not retrieve quota usage.', Http::STATUS_INTERNAL_SERVER_ERROR);
Expand Down Expand Up @@ -322,12 +322,14 @@ public function getUserQuotaInfo(string $userId): array {
$quotas = $this->hasOwnOpenAiApiKey($userId) ? Application::DEFAULT_QUOTAS : $this->openAiSettingsService->getQuotas();
// Get quota period
$quotaPeriod = $this->openAiSettingsService->getQuotaPeriod();
$quotaStart = $this->openAiSettingsService->getQuotaStart();
$quotaEnd = $this->openAiSettingsService->getQuotaEnd();
// Get quota usage for each quota type:
$quotaInfo = [];
foreach (Application::DEFAULT_QUOTAS as $quotaType => $_) {
$quotaInfo[$quotaType]['type'] = $this->translatedQuotaType($quotaType);
try {
$quotaInfo[$quotaType]['used'] = $this->quotaUsageMapper->getQuotaUnitsOfUserInTimePeriod($userId, $quotaType, $quotaPeriod);
$quotaInfo[$quotaType]['used'] = $this->quotaUsageMapper->getQuotaUnitsOfUserInTimePeriod($userId, $quotaType, $quotaStart);
} catch (DoesNotExistException|MultipleObjectsReturnedException|DBException|RuntimeException $e) {
$this->logger->warning('Could not retrieve quota usage for user: ' . $userId . ' and quota type: ' . $quotaType . '. Error: ' . $e->getMessage(), ['app' => Application::APP_ID]);
throw new Exception($this->l10n->t('Unknown error while retrieving quota usage.'), Http::STATUS_INTERNAL_SERVER_ERROR);
Expand All @@ -339,6 +341,8 @@ public function getUserQuotaInfo(string $userId): array {
return [
'quota_usage' => $quotaInfo,
'period' => $quotaPeriod,
'start' => $quotaStart,
'end' => $quotaEnd,
];
}

Expand All @@ -347,14 +351,14 @@ public function getUserQuotaInfo(string $userId): array {
* @throws Exception
*/
public function getAdminQuotaInfo(): array {
// Get quota period
$quotaPeriod = $this->openAiSettingsService->getQuotaPeriod();
// Get quota start time
$startTime = $this->openAiSettingsService->getQuotaStart();
// Get quota usage of all users for each quota type:
$quotaInfo = [];
foreach (Application::DEFAULT_QUOTAS as $quotaType => $_) {
$quotaInfo[$quotaType]['type'] = $this->translatedQuotaType($quotaType);
try {
$quotaInfo[$quotaType]['used'] = $this->quotaUsageMapper->getQuotaUnitsInTimePeriod($quotaType, $quotaPeriod);
$quotaInfo[$quotaType]['used'] = $this->quotaUsageMapper->getQuotaUnitsInTimePeriod($quotaType, $startTime);
} catch (DoesNotExistException|MultipleObjectsReturnedException|DBException|RuntimeException $e) {
$this->logger->warning('Could not retrieve quota usage for quota type: ' . $quotaType . '. Error: ' . $e->getMessage(), ['app' => Application::APP_ID]);
// We can pass detailed error info to the UI here since the user is an admin in any case:
Expand Down
113 changes: 103 additions & 10 deletions lib/Service/OpenAiSettingsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

namespace OCA\OpenAi\Service;

use DateInterval;
use DateTime;
use Exception;
use OCA\OpenAi\AppInfo\Application;
use OCP\IAppConfig;
Expand All @@ -33,7 +35,7 @@ class OpenAiSettingsService {
'max_tokens' => 'integer',
'use_max_completion_tokens_param' => 'boolean',
'llm_extra_params' => 'string',
'quota_period' => 'integer',
'quota_period' => 'array',
'quotas' => 'array',
'translation_provider_enabled' => 'boolean',
'llm_provider_enabled' => 'boolean',
Expand Down Expand Up @@ -62,6 +64,67 @@ public function __construct(
) {
}

/**
* Gets the timestamp of the beginning of the quota period
*
* @return int
* @throws Exception
*/
public function getQuotaStart(): int {
$quotaPeriod = $this->getQuotaPeriod();
$now = new DateTime();

if ($quotaPeriod['unit'] === 'day') {
// Get a timestamp of the beginning of the time period
$periodStart = $now->sub(new DateInterval('P' . $quotaPeriod['length'] . 'D'));
} else {
$periodStart = new DateTime(date('Y-m-' . $quotaPeriod['day']));
// Ensure that this isn't in the future
if ($periodStart > $now) {
$periodStart = $periodStart->sub(new DateInterval('P1M'));
}
if ($quotaPeriod['length'] > 1) {
// Calculate number of months since 2000-01 to ensure the start month is consistent
$startDate = new DateTime('2000-01-' . $quotaPeriod['day']);
$months = $startDate->diff($periodStart)->m + $startDate->diff($periodStart)->y * 12;
$remainder = $months % $quotaPeriod['length'];
$periodStart = $periodStart->sub(new DateInterval('P' . $remainder . 'M'));
}
}
return $periodStart->getTimestamp();
}

/**
* Gets the timestamp of the end of the quota period
* if the period is floating, then this will be the current time
*
* @return int
* @throws Exception
*/
public function getQuotaEnd(): int {
$quotaPeriod = $this->getQuotaPeriod();
$now = new DateTime();

if ($quotaPeriod['unit'] === 'day') {
// Get a timestamp of the beginning of the time period
$periodEnd = $now;
} else {
$periodEnd = new DateTime(date('Y-m-' . $quotaPeriod['day']));
// Ensure that this isn't in the past
if ($periodEnd < $now) {
$periodEnd = $periodEnd->add(new DateInterval('P1M'));
}
if ($quotaPeriod['length'] > 1) {
// Calculate number of months since 2000-01 to ensure the start month is consistent
$startDate = new DateTime('2000-01-' . $quotaPeriod['day']);
$months = $startDate->diff($periodEnd)->m + $startDate->diff($periodEnd)->y * 12;
$remainder = $months % $quotaPeriod['length'];
$periodEnd = $periodEnd->add(new DateInterval('P' . $quotaPeriod['length'] - $remainder . 'M'));
}
}
return $periodEnd->getTimestamp();
}

public function invalidateModelsCache(): void {
$cache = $this->cacheFactory->createDistributed(Application::APP_ID);
$cache->clear(Application::MODELS_CACHE_KEY);
Expand Down Expand Up @@ -197,10 +260,23 @@ public function getLlmExtraParams(): string {
}

/**
* @return int
* @return array
*/
public function getQuotaPeriod(): int {
return intval($this->appConfig->getValueString(Application::APP_ID, 'quota_period', strval(Application::DEFAULT_QUOTA_PERIOD))) ?: Application::DEFAULT_QUOTA_PERIOD;
public function getQuotaPeriod(): array {
$value = json_decode(
$this->appConfig->getValueString(Application::APP_ID, 'quota_period', json_encode(Application::DEFAULT_QUOTA_CONFIG)),
true
) ?: Application::DEFAULT_QUOTA_CONFIG;
// Migrate from old quota period to new one
if (is_int($value)) {
$value = ['length' => $value];
}
foreach (Application::DEFAULT_QUOTA_CONFIG as $key => $defaultValue) {
if (!isset($value[$key])) {
$value[$key] = $defaultValue;
}
}
return $value;
}

/**
Expand Down Expand Up @@ -593,14 +669,31 @@ public function setLlmExtraParams(string $llmExtraParams): void {
}

/**
* Setter for quotaPeriod; minimum is 1 day
* @param int $quotaPeriod
* Setter for quotaPeriod; minimum is 1 day.
* Days are floating, and months are set dates
* @param array $quotaPeriod
* @return void
* @throws Exception
*/
public function setQuotaPeriod(int $quotaPeriod): void {
// Validate input:
$quotaPeriod = max(1, $quotaPeriod);
$this->appConfig->setValueString(Application::APP_ID, 'quota_period', strval($quotaPeriod));
public function setQuotaPeriod(array $quotaPeriod): void {
if (!isset($quotaPeriod['length']) || !is_int($quotaPeriod['length'])) {
throw new Exception('Invalid quota period length');
}
$quotaPeriod['length'] = max(1, $quotaPeriod['length']);
if (!isset($quotaPeriod['unit']) || !is_string($quotaPeriod['unit'])) {
throw new Exception('Invalid quota period unit');
}
// Checks month period
if ($quotaPeriod['unit'] === 'month') {
if (!isset($quotaPeriod['day']) || !is_int($quotaPeriod['day'])) {
throw new Exception('Invalid quota period day');
}
$quotaPeriod['day'] = max(1, $quotaPeriod['day']);
$quotaPeriod['day'] = min($quotaPeriod['day'], 28);
} elseif ($quotaPeriod['unit'] !== 'day') {
throw new Exception('Invalid quota period unit');
}
$this->appConfig->setValueString(Application::APP_ID, 'quota_period', json_encode($quotaPeriod));
}

/**
Expand Down
20 changes: 6 additions & 14 deletions src/components/AdminSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -500,19 +500,9 @@
</h2>
<div class="line">
<!--Time period in days for the token usage-->
<NcInputField
id="openai-api-quota-period"
v-model="state.quota_period"
class="input"
type="number"
:label="t('integration_openai', 'Quota enforcement time period (days)')"
:show-trailing-button="!!state.quota_period"
@update:model-value="onInput()"
@trailing-button-click="state.quota_period = '' ; onInput()">
<template #trailing-button-icon>
<CloseIcon :size="20" />
</template>
</NcInputField>
<QuotaPeriodPicker
v-model:value="state.quota_period"
@update:value="onInput()" />
</div>
<h2>
{{ t('integration_openai', 'Usage quotas per time period') }}
Expand Down Expand Up @@ -619,13 +609,15 @@ import { loadState } from '@nextcloud/initial-state'
import { confirmPassword } from '@nextcloud/password-confirmation'
import { generateUrl } from '@nextcloud/router'
import debounce from 'debounce'
import QuotaPeriodPicker from './QuotaPeriodPicker.vue'

const DEFAULT_MODEL_ITEM = { id: 'Default' }

export default {
name: 'AdminSettings',

components: {
QuotaPeriodPicker,
OpenAiIcon,
KeyOutlineIcon,
CloseIcon,
Expand Down Expand Up @@ -871,7 +863,7 @@ export default {
max_tokens: parseInt(this.state.max_tokens),
llm_extra_params: this.state.llm_extra_params,
default_image_size: this.state.default_image_size,
quota_period: parseInt(this.state.quota_period),
quota_period: this.state.quota_period,
quotas: this.state.quotas,
tts_voices: this.state.tts_voices,
default_tts_voice: this.state.default_tts_voice,
Expand Down
Loading
Loading