diff --git a/appinfo/routes.php b/appinfo/routes.php index beed6668..0a8f9a2e 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -20,5 +20,6 @@ ['name' => 'quotaRule#addRule', 'url' => '/quota/rule', 'verb' => 'POST'], ['name' => 'quotaRule#updateRule', 'url' => '/quota/rule', 'verb' => 'PUT'], ['name' => 'quotaRule#deleteRule', 'url' => '/quota/rule', 'verb' => 'DELETE'], + ['name' => 'quotaRule#getQuotaUsage', 'url' => '/quota/download-usage', 'verb' => 'GET'], ], ]; diff --git a/lib/Controller/QuotaRuleController.php b/lib/Controller/QuotaRuleController.php index d830c924..004eb163 100644 --- a/lib/Controller/QuotaRuleController.php +++ b/lib/Controller/QuotaRuleController.php @@ -11,7 +11,10 @@ use OCA\OpenAi\Service\QuotaRuleService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\TextPlainResponse; use OCP\IRequest; class QuotaRuleController extends Controller { @@ -79,4 +82,35 @@ public function deleteRule(int $id): DataResponse { return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } } + /** + * Gets the quota usage between two dates + * @param int $startDate + * @param int $endDate + * @param int $type + * @return Http\StreamResponse|TextPlainResponse + */ + #[NoCSRFRequired] + public function getQuotaUsage(int $startDate, int $endDate, int $type): Response { + try { + $result = $this->quotaRuleService->getQuotaUsage($startDate, $endDate, $type); + $csv = fopen('php://memory', 'w'); + try { + foreach ($result as $row) { + fputcsv($csv, $row); + } + rewind($csv); + + $response = new Http\StreamResponse($csv, Http::STATUS_OK); + $response->setHeaders([ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="quota_usage.csv"' + ]); + return $response; + } catch (Exception $e) { + return new TextPlainResponse('Failed to get quota usage:' . $e->getMessage(), Http::STATUS_INTERNAL_SERVER_ERROR); + } + } catch (Exception $e) { + return new TextPlainResponse('Failed to get quota usage:' . $e->getMessage(), Http::STATUS_NOT_FOUND); + } + } } diff --git a/lib/Cron/CleanupQuotaDb.php b/lib/Cron/CleanupQuotaDb.php index b6a122c4..30f29c39 100644 --- a/lib/Cron/CleanupQuotaDb.php +++ b/lib/Cron/CleanupQuotaDb.php @@ -30,14 +30,15 @@ public function __construct( protected function run($argument) { $this->logger->debug('Run cleanup job for OpenAI quota db'); $quota = $this->openAiSettingsService->getQuotaPeriod(); - $days = $quota['length']; + $quotaDays = $quota['length']; if ($quota['unit'] == 'month') { - $days *= 30; + $quotaDays *= 30; } + $days = $this->openAiSettingsService->getUsageStorageTime(); $this->quotaUsageMapper->cleanupQuotaUsages( // The mimimum period is limited to DEFAULT_QUOTA_PERIOD to not lose // the stored quota usage data below this limit. - max($days, Application::DEFAULT_QUOTA_PERIOD) + max($quotaDays, $days, Application::DEFAULT_QUOTA_PERIOD) ); } diff --git a/lib/Db/QuotaUsageMapper.php b/lib/Db/QuotaUsageMapper.php index 926815c6..7d00aacf 100644 --- a/lib/Db/QuotaUsageMapper.php +++ b/lib/Db/QuotaUsageMapper.php @@ -82,7 +82,7 @@ public function getQuotaUnitsInTimePeriod(int $type, int $periodStart): int { $qb = $this->db->getQueryBuilder(); // Get the sum of the units used in the time period - $qb->select($qb->createFunction('SUM(units)')) + $qb->select($qb->func()->sum('units')) ->from($this->getTableName()) ->where( $qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT)) @@ -113,7 +113,7 @@ public function getQuotaUnitsOfUserInTimePeriod(string $userId, int $type, int $ $qb = $this->db->getQueryBuilder(); // Get the sum of the units used in the time period - $qb->select($qb->createFunction('SUM(units)')) + $qb->select($qb->func()->sum('units')) ->from($this->getTableName()) ->where( $qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT)) @@ -170,7 +170,7 @@ public function getQuotaUsagesOfUser(string $userId, int $type): array { public function getQuotaUnitsOfUser(string $userId, int $type): int { $qb = $this->db->getQueryBuilder(); - $qb->select($qb->createFunction('SUM(units)')) + $qb->select($qb->func()->sum('units')) ->from($this->getTableName()) ->where( $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) @@ -274,4 +274,51 @@ public function cleanupQuotaUsages(int $timePeriod): void { ); $qb->executeStatement(); } + + /** + * Gets quota usage of all users + * @param int $startTime + * @param int $endTime + * @return array + * @throws Exception + * @throws RuntimeException + */ + public function getUsersQuotaUsage(int $startTime, int $endTime, $type): array { + $qb = $this->db->getQueryBuilder(); + + $qb->select('user_id') + ->selectAlias($qb->func()->sum('units'), 'usage') + ->from($this->getTableName()) + ->where($qb->expr()->gte('timestamp', $qb->createNamedParameter($startTime, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->lte('timestamp', $qb->createNamedParameter($endTime, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT))) + ->groupBy('user_id') + ->orderBy('usage', 'DESC'); + + return $qb->executeQuery()->fetchAll(); + } + /** + * Gets quota usage of all pools + * @param int $startTime + * @param int $endTime + * @param int $type + * @return array + * @throws Exception + * @throws RuntimeException + */ + public function getPoolsQuotaUsage(int $startTime, int $endTime, int $type): array { + $qb = $this->db->getQueryBuilder(); + + $qb->select('pool') + ->selectAlias($qb->func()->sum('units'), 'usage') + ->from($this->getTableName()) + ->where($qb->expr()->neq('pool', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->gte('timestamp', $qb->createNamedParameter($startTime, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->lte('timestamp', $qb->createNamedParameter($endTime, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT))) + ->groupBy('type', 'pool') + ->orderBy('usage', 'DESC'); + + return $qb->executeQuery()->fetchAll(); + } } diff --git a/lib/Service/OpenAiSettingsService.php b/lib/Service/OpenAiSettingsService.php index ad2f14a7..1006789b 100644 --- a/lib/Service/OpenAiSettingsService.php +++ b/lib/Service/OpenAiSettingsService.php @@ -37,6 +37,7 @@ class OpenAiSettingsService { 'llm_extra_params' => 'string', 'quota_period' => 'array', 'quotas' => 'array', + 'usage_storage_time' => 'integer', 'translation_provider_enabled' => 'boolean', 'llm_provider_enabled' => 'boolean', 't2i_provider_enabled' => 'boolean', @@ -119,7 +120,9 @@ public function getQuotaEnd(): int { $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')); + if ($remainder != 0) { + $periodEnd = $periodEnd->add(new DateInterval('P' . $quotaPeriod['length'] - $remainder . 'M')); + } } } return $periodEnd->getTimestamp(); @@ -308,6 +311,10 @@ public function getQuotas(): array { return $quotas; } + public function getUsageStorageTime() : int { + return $this->appConfig->getValueInt(Application::APP_ID, 'usage_storage_time', Application::DEFAULT_QUOTA_PERIOD, lazy: true); + } + /** * @return boolean */ @@ -395,6 +402,7 @@ public function getAdminConfig(): array { // Updated to get quota period 'quotas' => $this->getQuotas(), // Get quotas from the config value and return it + 'usage_storage_time' => $this->getUsageStorageTime(), 'translation_provider_enabled' => $this->getTranslationProviderEnabled(), 'llm_provider_enabled' => $this->getLlmProviderEnabled(), 't2i_provider_enabled' => $this->getT2iProviderEnabled(), @@ -522,6 +530,15 @@ public function setQuotas(array $quotas): void { $cache->clear(Application::QUOTA_RULES_CACHE_PREFIX); } + /** + * @param int $usageStorageTime + * @return void + */ + public function setUsageStorageTime(int $usageStorageTime): void { + $usageStorageTime = max(1, $usageStorageTime); + $this->appConfig->setValueInt(Application::APP_ID, 'usage_storage_time', $usageStorageTime, lazy: true); + } + /** * @param string $apiKey * @return void @@ -831,6 +848,9 @@ public function setAdminConfig(array $adminConfig): void { if (isset($adminConfig['quotas'])) { $this->setQuotas($adminConfig['quotas']); } + if (isset($adminConfig['usage_storage_time'])) { + $this->setUsageStorageTime(intval($adminConfig['usage_storage_time'])); + } if (isset($adminConfig['use_max_completion_tokens_param'])) { $this->setUseMaxCompletionParam($adminConfig['use_max_completion_tokens_param']); } diff --git a/lib/Service/QuotaRuleService.php b/lib/Service/QuotaRuleService.php index 67f6f163..530170f1 100644 --- a/lib/Service/QuotaRuleService.php +++ b/lib/Service/QuotaRuleService.php @@ -11,11 +11,13 @@ use OCA\OpenAi\AppInfo\Application; use OCA\OpenAi\Db\EntityType; use OCA\OpenAi\Db\QuotaRuleMapper; +use OCA\OpenAi\Db\QuotaUsageMapper; use OCA\OpenAi\Db\QuotaUserMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\ICacheFactory; use OCP\IGroupManager; +use OCP\IL10N; use OCP\IUserManager; use Psr\Log\LoggerInterface; @@ -27,6 +29,8 @@ public function __construct( private IGroupManager $groupManager, private ICacheFactory $cacheFactory, private IUserManager $userManager, + private QuotaUsageMapper $quotaUsageMapper, + private IL10N $l10n, private LoggerInterface $logger, ) { } @@ -178,4 +182,29 @@ private function validateEntities(array $entities) { } } } + public function getQuotaUsage(int $startDate, int $endDate, int $type): array { + $data = [[$this->l10n->t('Name'), $this->l10n->t('Usage')]]; + $users = $this->quotaUsageMapper->getUsersQuotaUsage($startDate, $endDate, $type); + $pools = $this->quotaUsageMapper->getPoolsQuotaUsage($startDate, $endDate, $type); + $usersIdx = 0; + $poolsIdx = 0; + while ($usersIdx < count($users) && $poolsIdx < count($pools)) { + if ($users[$usersIdx]['usage'] > $pools[$poolsIdx]['usage']) { + $data[] = [$users[$usersIdx]['user_id'], $users[$usersIdx]['usage']]; + $usersIdx++; + } else { + $data[] = [$this->l10n->t('Quota pool for rule %d', $pools[$poolsIdx]['pool']), $pools[$poolsIdx]['usage']]; + $poolsIdx++; + } + } + while ($usersIdx < count($users)) { + $data[] = [$users[$usersIdx]['user_id'], $users[$usersIdx]['usage']]; + $usersIdx++; + } + while ($poolsIdx < count($pools)) { + $data[] = [$this->l10n->t('Quota pool for rule %d', $pools[$poolsIdx]['pool']), $pools[$poolsIdx]['usage']]; + $poolsIdx++; + } + return $data; + } } diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index 0655bb50..05c3442e 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -33,6 +33,8 @@ public function getForm(): TemplateResponse { $adminConfig['basic_password'] = $adminConfig['basic_password'] === '' ? '' : 'dummyPassword'; $isAssistantEnabled = $this->appManager->isEnabledForUser('assistant'); $adminConfig['assistant_enabled'] = $isAssistantEnabled; + $adminConfig['quota_start_date'] = $this->openAiSettingsService->getQuotaStart(); + $adminConfig['quota_end_date'] = $this->openAiSettingsService->getQuotaEnd(); $this->initialStateService->provideInitialState('admin-config', $adminConfig); $rules = $this->quotaRuleService->getRules(); $this->initialStateService->provideInitialState('rules', $rules); diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index ae5e455a..26a3be39 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -546,6 +546,30 @@ +