From a05aae0c1f2145e493051330c862f7b2fdde4f50 Mon Sep 17 00:00:00 2001 From: James Kiger Date: Wed, 17 Sep 2025 12:49:56 -0400 Subject: [PATCH 01/12] Initial work --- kobo/apps/organizations/constants.py | 11 +++++++++ .../test_organizations_service_usage_api.py | 2 ++ kobo/apps/organizations/types.py | 3 +++ .../tests/api/v2/test_api.py | 10 ++++++++ kobo/apps/stripe/models.py | 4 ++-- kobo/apps/stripe/tests/test_stripe_utils.py | 10 ++++---- kobo/apps/stripe/utils/subscription_limits.py | 10 ++++---- .../migrations/0006_add_total_llm_requests.py | 18 ++++++++++++++ kobo/apps/trackers/models.py | 1 + kobo/apps/trackers/tests/test_utils.py | 2 +- kobo/apps/trackers/utils.py | 5 +++- kpi/tests/test_usage_calculator.py | 24 +++++++++++++++++-- kpi/utils/usage_calculator.py | 23 +++++++++++++++++- 13 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 kobo/apps/trackers/migrations/0006_add_total_llm_requests.py diff --git a/kobo/apps/organizations/constants.py b/kobo/apps/organizations/constants.py index 5c0864d19a..cc56855cfa 100644 --- a/kobo/apps/organizations/constants.py +++ b/kobo/apps/organizations/constants.py @@ -1,11 +1,22 @@ from django.db import models +# Temporary const for code that iterates over usage types +# TODO: Remove when Stripe code has been updated to +# handle LLM usage type +class SupportedUsageType(models.TextChoices): + SUBMISSION = 'submission' + STORAGE_BYTES = 'storage_bytes' + MT_CHARACTERS = 'mt_characters' + ASR_SECONDS = 'asr_seconds' + + class UsageType(models.TextChoices): SUBMISSION = 'submission' STORAGE_BYTES = 'storage_bytes' MT_CHARACTERS = 'mt_characters' ASR_SECONDS = 'asr_seconds' + LLM_REQUESTS = 'llm_requests' INVITE_OWNER_ERROR = ( diff --git a/kobo/apps/organizations/tests/test_organizations_service_usage_api.py b/kobo/apps/organizations/tests/test_organizations_service_usage_api.py index 418e7f7ef0..07303f8db6 100644 --- a/kobo/apps/organizations/tests/test_organizations_service_usage_api.py +++ b/kobo/apps/organizations/tests/test_organizations_service_usage_api.py @@ -83,6 +83,8 @@ def test_check_api_response_without_stripe(self): assert response.data['total_nlp_usage']['asr_seconds_all_time'] == 4728 assert response.data['total_nlp_usage']['mt_characters_current_period'] == 5473 assert response.data['total_nlp_usage']['mt_characters_all_time'] == 6726 + assert response.data['total_nlp_usage']['llm_requests_current_period'] == 20 + assert response.data['total_nlp_usage']['llm_requests_all_time'] == 70 assert response.data['total_storage_bytes'] == self.expected_file_size() # Without stripe, there are no usage limits and diff --git a/kobo/apps/organizations/types.py b/kobo/apps/organizations/types.py index 8dcd6659e7..7d535f6263 100644 --- a/kobo/apps/organizations/types.py +++ b/kobo/apps/organizations/types.py @@ -12,11 +12,13 @@ class UsageLimits(TypedDict): submission_limit: float asr_seconds_limit: float mt_characters_limit: float + llm_requests_limit: float class NLPUsage(TypedDict): asr_seconds: int mt_characters: int + llm_requests: int class UsageBalance(TypedDict): @@ -31,3 +33,4 @@ class UsageBalances(TypedDict): submission: UsageBalance | None asr_seconds: UsageBalance | None mt_characters: UsageBalance | None + llm_requests: UsageBalance | None diff --git a/kobo/apps/project_ownership/tests/api/v2/test_api.py b/kobo/apps/project_ownership/tests/api/v2/test_api.py index aa22acc4b2..c983b8cbb6 100644 --- a/kobo/apps/project_ownership/tests/api/v2/test_api.py +++ b/kobo/apps/project_ownership/tests/api/v2/test_api.py @@ -341,6 +341,12 @@ def setUp(self) -> None: user_id=self.someuser.pk, asset_id=self.asset.pk, ) + update_nlp_counter( + service='mock_nlp_service_llm_requests', + amount=20, + user_id=self.someuser.pk, + asset_id=self.asset.pk, + ) def __add_submissions(self): submissions = [] @@ -390,8 +396,10 @@ def test_account_usage_transferred_to_new_user(self): 'total_nlp_usage': { 'asr_seconds_current_period': 120, 'mt_characters_current_period': 1000, + 'llm_requests_current_period': 20, 'asr_seconds_all_time': 120, 'mt_characters_all_time': 1000, + 'llm_requests_all_time': 20, }, 'total_storage_bytes': 191642, 'total_submission_count': { @@ -404,8 +412,10 @@ def test_account_usage_transferred_to_new_user(self): 'total_nlp_usage': { 'asr_seconds_current_period': 0, 'mt_characters_current_period': 0, + 'llm_requests_current_period': 0, 'asr_seconds_all_time': 0, 'mt_characters_all_time': 0, + 'llm_requests_all_time': 0, }, 'total_storage_bytes': 0, 'total_submission_count': { diff --git a/kobo/apps/stripe/models.py b/kobo/apps/stripe/models.py index 86ef92c6fa..e4d9e17371 100644 --- a/kobo/apps/stripe/models.py +++ b/kobo/apps/stripe/models.py @@ -9,7 +9,7 @@ from djstripe.models import Charge, Price, Subscription from kobo.apps.kobo_auth.shortcuts import User -from kobo.apps.organizations.constants import UsageType +from kobo.apps.organizations.constants import SupportedUsageType, UsageType from kobo.apps.organizations.models import Organization from kobo.apps.stripe.constants import ACTIVE_STRIPE_STATUSES from kobo.apps.stripe.utils.subscription_limits import get_default_add_on_limits @@ -311,6 +311,6 @@ class ExceededLimitCounter(models.Model): on_delete=models.CASCADE, ) days = models.PositiveSmallIntegerField(default=0) - limit_type = models.CharField(choices=UsageType.choices, max_length=20) + limit_type = models.CharField(choices=SupportedUsageType.choices, max_length=20) date_created = models.DateTimeField(auto_now_add=True) date_modified = models.DateTimeField(auto_now=True) diff --git a/kobo/apps/stripe/tests/test_stripe_utils.py b/kobo/apps/stripe/tests/test_stripe_utils.py index 189cf40e6a..4822919bff 100644 --- a/kobo/apps/stripe/tests/test_stripe_utils.py +++ b/kobo/apps/stripe/tests/test_stripe_utils.py @@ -14,7 +14,7 @@ from model_bakery import baker from kobo.apps.kobo_auth.shortcuts import User -from kobo.apps.organizations.constants import UsageType +from kobo.apps.organizations.constants import SupportedUsageType, UsageType from kobo.apps.organizations.models import Organization from kobo.apps.organizations.utils import get_billing_dates from kobo.apps.stripe.models import ExceededLimitCounter @@ -151,7 +151,7 @@ def test__prioritizes_price_metadata(self): self.organization, metadata=product_metadata, price_metadata=price_metadata ) limits = get_paid_subscription_limits([self.organization.id]).first() - for usage_type, _ in UsageType.choices: + for usage_type, _ in SupportedUsageType.choices: assert limits[f'{usage_type}_limit'] == '2' def test_get_subscription_limits_takes_most_recent_active_subscriptions(self): @@ -627,7 +627,7 @@ def test_check_exceeded_limit_adds_counters(self): ): self.add_submissions(count=2, asset=self.asset, username='someuser') self.add_nlp_trackers() - for usage_type, _ in UsageType.choices: + for usage_type, _ in SupportedUsageType.choices: check_exceeded_limit(self.someuser, usage_type) assert ( ExceededLimitCounter.objects.filter( @@ -638,7 +638,7 @@ def test_check_exceeded_limit_adds_counters(self): def test_check_exceeded_limit_updates_counters(self): today = timezone.now() - for usage_type, _ in UsageType.choices: + for usage_type, _ in SupportedUsageType.choices: baker.make( ExceededLimitCounter, user=self.anotheruser, @@ -655,7 +655,7 @@ def test_check_exceeded_limit_updates_counters(self): ): self.add_submissions(count=2, asset=self.asset, username='someuser') self.add_nlp_trackers() - for usage_type, _ in UsageType.choices: + for usage_type, _ in SupportedUsageType.choices: check_exceeded_limit(self.someuser, usage_type) counter = ExceededLimitCounter.objects.get( user_id=self.anotheruser.id, limit_type=usage_type diff --git a/kobo/apps/stripe/utils/subscription_limits.py b/kobo/apps/stripe/utils/subscription_limits.py index c3787b6cb1..5ce2d25963 100644 --- a/kobo/apps/stripe/utils/subscription_limits.py +++ b/kobo/apps/stripe/utils/subscription_limits.py @@ -6,7 +6,7 @@ from django.db.models import F, Max, Q, QuerySet, Window from django.db.models.functions import Coalesce -from kobo.apps.organizations.constants import UsageType +from kobo.apps.organizations.constants import SupportedUsageType, UsageType from kobo.apps.organizations.models import Organization, OrganizationUser from kobo.apps.organizations.types import UsageLimits from kobo.apps.stripe.constants import ACTIVE_STRIPE_STATUSES @@ -114,7 +114,7 @@ def get_organizations_subscription_limits( if row['product_type'] == 'plan': row_limits = { f'{usage_type}_limit': row[f'{usage_type}_limit'] - for usage_type, _ in UsageType.choices + for usage_type, _ in SupportedUsageType.choices } elif row['product_type'] == 'addon': row_limits['addon_storage_limit'] = row[f'{UsageType.STORAGE_BYTES}_limit'] @@ -136,7 +136,7 @@ def get_organizations_subscription_limits( .first() ) or {} default_plan_limits = {} - for usage_type, _ in UsageType.choices: + for usage_type, _ in SupportedUsageType.choices: limit_key = f'{usage_type}_limit' default_limit = default_plan.get(limit_key) if default_limit is None: @@ -147,7 +147,7 @@ def get_organizations_subscription_limits( results = {} for org_id in all_org_ids: all_org_limits = {} - for usage_type, _ in UsageType.choices: + for usage_type, _ in SupportedUsageType.choices: plan_limit = subscription_limits_by_org_id.get(org_id, {}).get( f'{usage_type}_limit' ) @@ -205,7 +205,7 @@ def get_organizations_effective_limits( PlanAddOn = apps.get_model('stripe', 'PlanAddOn') # noqa addon_limits = PlanAddOn.get_organizations_totals(organizations=organizations) for org_id, limits in effective_limits.items(): - for usage_type, _ in UsageType.choices: + for usage_type, _ in SupportedUsageType.choices: addon = addon_limits.get(org_id, {}).get(f'total_{usage_type}_limit', 0) limits[f'{usage_type}_limit'] += addon return effective_limits diff --git a/kobo/apps/trackers/migrations/0006_add_total_llm_requests.py b/kobo/apps/trackers/migrations/0006_add_total_llm_requests.py new file mode 100644 index 0000000000..b79f85c5a6 --- /dev/null +++ b/kobo/apps/trackers/migrations/0006_add_total_llm_requests.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.15 on 2025-09-16 17:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('trackers', '0005_remove_year_and_month'), + ] + + operations = [ + migrations.AddField( + model_name='nlpusagecounter', + name='total_llm_requests', + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/kobo/apps/trackers/models.py b/kobo/apps/trackers/models.py index c1ce7b812e..0a00e458d6 100644 --- a/kobo/apps/trackers/models.py +++ b/kobo/apps/trackers/models.py @@ -16,6 +16,7 @@ class NLPUsageCounter(models.Model): counters = models.JSONField(default=dict) total_asr_seconds = models.PositiveIntegerField(default=0) total_mt_characters = models.PositiveIntegerField(default=0) + total_llm_requests = models.PositiveIntegerField(default=0) class Meta: constraints = [ diff --git a/kobo/apps/trackers/tests/test_utils.py b/kobo/apps/trackers/tests/test_utils.py index 6bb30b9b9e..17c6d5ca50 100644 --- a/kobo/apps/trackers/tests/test_utils.py +++ b/kobo/apps/trackers/tests/test_utils.py @@ -135,7 +135,7 @@ def test_organization_usage_utils(self, usage_type): @pytest.mark.skipif( settings.STRIPE_ENABLED, reason='Tests non-stripe functionality' ) - @data('mt_characters', 'asr_seconds') + @data('mt_characters', 'asr_seconds', 'llm_requests') def test_org_usage_utils_without_stripe(self, usage_type): remaining = get_organization_remaining_usage(self.organization, usage_type) assert remaining == inf diff --git a/kobo/apps/trackers/utils.py b/kobo/apps/trackers/utils.py index 6285149d0b..8c30915e50 100644 --- a/kobo/apps/trackers/utils.py +++ b/kobo/apps/trackers/utils.py @@ -26,7 +26,7 @@ def update_nlp_counter( counter_id: Optional[int] = None, ): """ - Update the NLP ASR and MT tracker for various services + Update the NLP tracker for various services Params: service (str): Service tracker to be updated, provider_service_type for example: @@ -64,6 +64,9 @@ def update_nlp_counter( kwargs['total_mt_characters'] = F('total_mt_characters') + amount if stripe_enabled and asset_id is not None: handle_usage_deduction(organization, UsageType.MT_CHARACTERS, amount) + if service.endswith(UsageType.LLM_REQUESTS): + kwargs['total_llm_requests'] = F('total_llm_requests') + amount + # TODO: run handle_usage_deduction when llm_requests supported by Stripe code NLPUsageCounter.objects.filter(pk=counter_id).update( counters=IncrementValue('counters', keyname=service, increment=amount), diff --git a/kpi/tests/test_usage_calculator.py b/kpi/tests/test_usage_calculator.py index 661e55aba5..c3a066822c 100644 --- a/kpi/tests/test_usage_calculator.py +++ b/kpi/tests/test_usage_calculator.py @@ -84,10 +84,11 @@ def _create_and_set_asset(self, user=None): ) self._deployment = self.asset.deployment - def add_nlp_tracker(self, asset, date, userid, seconds, characters): + def add_nlp_tracker(self, asset, date, userid, seconds, characters, requests): counter = { 'google_asr_seconds': seconds, 'google_mt_characters': characters, + 'some_service_llm_requests': requests, } return NLPUsageCounter.objects.create( user_id=userid, @@ -96,14 +97,17 @@ def add_nlp_tracker(self, asset, date, userid, seconds, characters): counters=counter, total_asr_seconds=seconds, total_mt_characters=characters, + total_llm_requests=requests, ) def add_nlp_trackers( self, seconds_current_month=4586, characters_current_month=5473, + requests_current_month=20, seconds_last_month=142, characters_last_month=1253, + requests_last_month=50, ): """ Add nlp data common across several tests @@ -116,6 +120,7 @@ def add_nlp_trackers( date=today, seconds=seconds_current_month, characters=characters_current_month, + requests=requests_current_month, ) # last month @@ -126,6 +131,7 @@ def add_nlp_trackers( date=last_month, seconds=seconds_last_month, characters=characters_last_month, + requests=requests_last_month, ) def add_submissions( @@ -204,6 +210,10 @@ def test_disable_cache(self): 2 * nlp_usage_A['mt_characters_current_period'] == nlp_usage_B['mt_characters_current_period'] ) + assert ( + 2 * nlp_usage_A['llm_requests_current_period'] + == nlp_usage_B['llm_requests_current_period'] + ) def test_nlp_usage_counters(self): self.add_nlp_trackers() @@ -213,6 +223,8 @@ def test_nlp_usage_counters(self): assert nlp_usage['asr_seconds_all_time'] == 4728 assert nlp_usage['mt_characters_current_period'] == 5473 assert nlp_usage['mt_characters_all_time'] == 6726 + assert nlp_usage['llm_requests_current_period'] == 20 + assert nlp_usage['llm_requests_all_time'] == 70 def test_no_data(self): self.add_nlp_trackers() @@ -224,6 +236,8 @@ def test_no_data(self): assert nlp_usage['asr_seconds_all_time'] == 0 assert nlp_usage['mt_characters_current_period'] == 0 assert nlp_usage['mt_characters_all_time'] == 0 + assert nlp_usage['llm_requests_current_period'] == 0 + assert nlp_usage['llm_requests_all_time'] == 0 assert calculator.get_storage_usage() == 0 assert submission_counters['current_period'] == 0 assert submission_counters['all_time'] == 0 @@ -369,6 +383,7 @@ def test_nlp_counters_current_period_all_orgs(self): date=three_months_ago, seconds=10, characters=20, + requests=10, ) self.add_nlp_tracker( asset=asset_2, @@ -376,6 +391,7 @@ def test_nlp_counters_current_period_all_orgs(self): date=yesterday, seconds=10, characters=20, + requests=10, ) # mock nlp data for someuser from a year ago (out of range) @@ -386,6 +402,7 @@ def test_nlp_counters_current_period_all_orgs(self): date=one_year_ago, seconds=10, characters=20, + requests=10, ) # mock nlp data for another user in range @@ -395,6 +412,7 @@ def test_nlp_counters_current_period_all_orgs(self): date=yesterday, seconds=10, characters=20, + requests=10, ) # mock nlp data for another user from 3 months ago (out of range) self.add_nlp_tracker( @@ -403,6 +421,7 @@ def test_nlp_counters_current_period_all_orgs(self): date=three_months_ago, seconds=10, characters=20, + requests=10, ) with patch( 'kpi.utils.usage_calculator.get_current_billing_period_dates_by_org', @@ -413,6 +432,8 @@ def test_nlp_counters_current_period_all_orgs(self): assert nlp_usage_by_user[self.anotheruser.id]['asr_seconds'] == 10 assert nlp_usage_by_user[self.someuser.id]['mt_characters'] == 40 assert nlp_usage_by_user[self.anotheruser.id]['mt_characters'] == 20 + assert nlp_usage_by_user[self.someuser.id]['llm_requests'] == 20 + assert nlp_usage_by_user[self.anotheruser.id]['llm_requests'] == 10 @pytest.mark.skipif( not settings.STRIPE_ENABLED, reason='Requires stripe functionality' @@ -461,7 +482,6 @@ def test_usage_balances_with_stripe(self): generate_plan_subscription(organization, product_metadata) calculator = ServiceUsageCalculator(self.someuser) - usage_balances = calculator.get_usage_balances() assert usage_balances[UsageType.ASR_SECONDS]['effective_limit'] == limit diff --git a/kpi/utils/usage_calculator.py b/kpi/utils/usage_calculator.py index 1bee9a6d01..1455b38daa 100644 --- a/kpi/utils/usage_calculator.py +++ b/kpi/utils/usage_calculator.py @@ -13,11 +13,11 @@ from kobo.apps.organizations.models import Organization from kobo.apps.organizations.types import NLPUsage, UsageBalance, UsageBalances from kobo.apps.organizations.utils import get_billing_dates +from kobo.apps.stripe.utils.billing_dates import get_current_billing_period_dates_by_org from kobo.apps.stripe.utils.import_management import requires_stripe from kobo.apps.stripe.utils.subscription_limits import ( get_organizations_effective_limits, ) -from kobo.apps.stripe.utils.billing_dates import get_current_billing_period_dates_by_org from kpi.utils.cache import CachedClass, cached_class_property @@ -100,6 +100,10 @@ def get_nlp_usage_in_date_range_by_user_id(date_ranges_by_user) -> dict[int, NLP Sum(f'total_{UsageType.MT_CHARACTERS}'), 0, ), + llm_requests_current_period=Coalesce( + Sum(f'total_{UsageType.LLM_REQUESTS}'), + 0, + ), ) ) results = {} @@ -107,6 +111,7 @@ def get_nlp_usage_in_date_range_by_user_id(date_ranges_by_user) -> dict[int, NLP results[row['user_id']] = { UsageType.ASR_SECONDS: row[f'{UsageType.ASR_SECONDS}_current_period'], UsageType.MT_CHARACTERS: row[f'{UsageType.MT_CHARACTERS}_current_period'], + UsageType.LLM_REQUESTS: row[f'{UsageType.LLM_REQUESTS}_current_period'], } return results @@ -161,6 +166,9 @@ def get_nlp_usage_by_type(self, usage_type: UsageType) -> int: UsageType.MT_CHARACTERS: nlp_usage[ f'{UsageType.MT_CHARACTERS}_current_period' ], + UsageType.LLM_REQUESTS: nlp_usage[ + f'{UsageType.LLM_REQUESTS}_current_period' + ], } return cached_usage[usage_type] @@ -178,6 +186,7 @@ def get_usage_balances(self) -> UsageBalances: limits = get_organizations_effective_limits([self.organization], True, True) org_limits = limits[self.organization.id] + # TODO: Check usage limit for LLM requests when supported by Stripe code return { UsageType.SUBMISSION: calculate_usage_balance( limit=org_limits[f'{UsageType.SUBMISSION}_limit'], @@ -195,6 +204,7 @@ def get_usage_balances(self) -> UsageBalances: limit=org_limits[f'{UsageType.MT_CHARACTERS}_limit'], usage=self.get_nlp_usage_by_type(UsageType.MT_CHARACTERS), ), + UsageType.LLM_REQUESTS: None, } @cached_class_property( @@ -208,6 +218,7 @@ def get_nlp_usage_counters(self): 'date', f'total_{UsageType.ASR_SECONDS}', f'total_{UsageType.MT_CHARACTERS}', + f'total_{UsageType.LLM_REQUESTS}', ) .filter(user_id=self._user_id) .aggregate( @@ -225,10 +236,20 @@ def get_nlp_usage_counters(self): ), 0, ), + llm_requests_current_period=Coalesce( + Sum( + f'total_{UsageType.LLM_REQUESTS}', + filter=self.current_period_filter, + ), + 0, + ), asr_seconds_all_time=Coalesce(Sum(f'total_{UsageType.ASR_SECONDS}'), 0), mt_characters_all_time=Coalesce( Sum(f'total_{UsageType.MT_CHARACTERS}'), 0 ), + llm_requests_all_time=Coalesce( + Sum(f'total_{UsageType.LLM_REQUESTS}'), 0 + ), ) ) From 50db9c9c6cb09461fa34cbdc636f85aa452bf625 Mon Sep 17 00:00:00 2001 From: James Kiger Date: Thu, 18 Sep 2025 08:28:31 -0400 Subject: [PATCH 02/12] Update asset usage endpoint and docs --- .../assetUsageResponseNlpUsageAllTime.ts | 1 + ...assetUsageResponseNlpUsageCurrentPeriod.ts | 1 + ...zationAssetUsageResponseNlpUsageAllTime.ts | 1 + ...AssetUsageResponseNlpUsageCurrentPeriod.ts | 1 + ...rganizationServiceUsageResponseBalances.ts | 2 + ...ServiceUsageResponseBalancesLlmRequests.ts | 14 +++++ ...zationServiceUsageResponseTotalNlpUsage.ts | 1 + .../models/serviceUsageResponseBalances.ts | 2 + ...serviceUsageResponseBalancesLlmRequests.ts | 14 +++++ .../serviceUsageResponseTotalNlpUsage.ts | 1 + kpi/deployment_backends/openrosa_backend.py | 2 + kpi/schema_extensions/v2/generic/schema.py | 3 ++ .../v2/organizations/extensions.py | 1 + .../v2/service_usage/extensions.py | 1 + static/openapi/schema_v2.json | 52 +++++++++++++++++++ static/openapi/schema_v2.yaml | 34 ++++++++++++ 16 files changed, 131 insertions(+) create mode 100644 jsapp/js/api/models/organizationServiceUsageResponseBalancesLlmRequests.ts create mode 100644 jsapp/js/api/models/serviceUsageResponseBalancesLlmRequests.ts diff --git a/jsapp/js/api/models/assetUsageResponseNlpUsageAllTime.ts b/jsapp/js/api/models/assetUsageResponseNlpUsageAllTime.ts index 8a63fbebfa..b664136a00 100644 --- a/jsapp/js/api/models/assetUsageResponseNlpUsageAllTime.ts +++ b/jsapp/js/api/models/assetUsageResponseNlpUsageAllTime.ts @@ -9,4 +9,5 @@ export type AssetUsageResponseNlpUsageAllTime = { total_nlp_asr_seconds?: number total_nlp_mt_characters?: number + total_nlp_llm_requests?: number } diff --git a/jsapp/js/api/models/assetUsageResponseNlpUsageCurrentPeriod.ts b/jsapp/js/api/models/assetUsageResponseNlpUsageCurrentPeriod.ts index 409222c961..6d6c2249a3 100644 --- a/jsapp/js/api/models/assetUsageResponseNlpUsageCurrentPeriod.ts +++ b/jsapp/js/api/models/assetUsageResponseNlpUsageCurrentPeriod.ts @@ -9,4 +9,5 @@ export type AssetUsageResponseNlpUsageCurrentPeriod = { total_nlp_asr_seconds?: number total_nlp_mt_characters?: number + total_nlp_llm_requests?: number } diff --git a/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageAllTime.ts b/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageAllTime.ts index 4663e5add1..98bf0df597 100644 --- a/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageAllTime.ts +++ b/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageAllTime.ts @@ -9,4 +9,5 @@ export type OrganizationAssetUsageResponseNlpUsageAllTime = { total_nlp_asr_seconds?: number total_nlp_mt_characters?: number + total_nlp_llm_requests?: number } diff --git a/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageCurrentPeriod.ts b/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageCurrentPeriod.ts index bf656e49d1..c8f907b471 100644 --- a/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageCurrentPeriod.ts +++ b/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageCurrentPeriod.ts @@ -9,4 +9,5 @@ export type OrganizationAssetUsageResponseNlpUsageCurrentPeriod = { total_nlp_asr_seconds?: number total_nlp_mt_characters?: number + total_nlp_llm_requests?: number } diff --git a/jsapp/js/api/models/organizationServiceUsageResponseBalances.ts b/jsapp/js/api/models/organizationServiceUsageResponseBalances.ts index 06eed8e23d..b38b4d93d2 100644 --- a/jsapp/js/api/models/organizationServiceUsageResponseBalances.ts +++ b/jsapp/js/api/models/organizationServiceUsageResponseBalances.ts @@ -1,4 +1,5 @@ import type { OrganizationServiceUsageResponseBalancesAsrSeconds } from './organizationServiceUsageResponseBalancesAsrSeconds' +import type { OrganizationServiceUsageResponseBalancesLlmRequests } from './organizationServiceUsageResponseBalancesLlmRequests' import type { OrganizationServiceUsageResponseBalancesMtCharacters } from './organizationServiceUsageResponseBalancesMtCharacters' import type { OrganizationServiceUsageResponseBalancesStorageBytes } from './organizationServiceUsageResponseBalancesStorageBytes' /** @@ -15,4 +16,5 @@ export type OrganizationServiceUsageResponseBalances = { storage_bytes?: OrganizationServiceUsageResponseBalancesStorageBytes asr_seconds?: OrganizationServiceUsageResponseBalancesAsrSeconds mt_characters?: OrganizationServiceUsageResponseBalancesMtCharacters + llm_requests?: OrganizationServiceUsageResponseBalancesLlmRequests } diff --git a/jsapp/js/api/models/organizationServiceUsageResponseBalancesLlmRequests.ts b/jsapp/js/api/models/organizationServiceUsageResponseBalancesLlmRequests.ts new file mode 100644 index 0000000000..03d2b6786c --- /dev/null +++ b/jsapp/js/api/models/organizationServiceUsageResponseBalancesLlmRequests.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.10.0 🍺 + * Do not edit manually. + * KoboToolbox API + * Powerful and intuitive data collection tools to make an impact + * OpenAPI spec version: 2.0.0 (api_v2) + */ + +export type OrganizationServiceUsageResponseBalancesLlmRequests = { + effective_limit?: number + balance_value?: number + balance_percent?: number + exceeded?: number +} diff --git a/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts b/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts index 858aec51e3..4887682c06 100644 --- a/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts +++ b/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts @@ -9,6 +9,7 @@ export type OrganizationServiceUsageResponseTotalNlpUsage = { asr_seconds_current_period?: number mt_characters_current_period?: number + llm_requests_current_period?: number asr_seconds_all_time?: number mt_characters_all_time?: number } diff --git a/jsapp/js/api/models/serviceUsageResponseBalances.ts b/jsapp/js/api/models/serviceUsageResponseBalances.ts index c6b671bc08..753d5fba42 100644 --- a/jsapp/js/api/models/serviceUsageResponseBalances.ts +++ b/jsapp/js/api/models/serviceUsageResponseBalances.ts @@ -1,4 +1,5 @@ import type { ServiceUsageResponseBalancesAsrSeconds } from './serviceUsageResponseBalancesAsrSeconds' +import type { ServiceUsageResponseBalancesLlmRequests } from './serviceUsageResponseBalancesLlmRequests' import type { ServiceUsageResponseBalancesMtCharacters } from './serviceUsageResponseBalancesMtCharacters' import type { ServiceUsageResponseBalancesStorageBytes } from './serviceUsageResponseBalancesStorageBytes' /** @@ -15,4 +16,5 @@ export type ServiceUsageResponseBalances = { storage_bytes?: ServiceUsageResponseBalancesStorageBytes asr_seconds?: ServiceUsageResponseBalancesAsrSeconds mt_characters?: ServiceUsageResponseBalancesMtCharacters + llm_requests?: ServiceUsageResponseBalancesLlmRequests } diff --git a/jsapp/js/api/models/serviceUsageResponseBalancesLlmRequests.ts b/jsapp/js/api/models/serviceUsageResponseBalancesLlmRequests.ts new file mode 100644 index 0000000000..b9d6da31dd --- /dev/null +++ b/jsapp/js/api/models/serviceUsageResponseBalancesLlmRequests.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.10.0 🍺 + * Do not edit manually. + * KoboToolbox API + * Powerful and intuitive data collection tools to make an impact + * OpenAPI spec version: 2.0.0 (api_v2) + */ + +export type ServiceUsageResponseBalancesLlmRequests = { + effective_limit?: number + balance_value?: number + balance_percent?: number + exceeded?: number +} diff --git a/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts b/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts index d8679820b9..0f32850974 100644 --- a/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts +++ b/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts @@ -9,6 +9,7 @@ export type ServiceUsageResponseTotalNlpUsage = { asr_seconds_current_period?: number mt_characters_current_period?: number + llm_requests_current_period?: number asr_seconds_all_time?: number mt_characters_all_time?: number } diff --git a/kpi/deployment_backends/openrosa_backend.py b/kpi/deployment_backends/openrosa_backend.py index 93b7aabd41..8f4a4f9ee7 100644 --- a/kpi/deployment_backends/openrosa_backend.py +++ b/kpi/deployment_backends/openrosa_backend.py @@ -870,12 +870,14 @@ def nlp_tracking_data( .aggregate( total_nlp_asr_seconds=Coalesce(Sum('total_asr_seconds'), 0), total_nlp_mt_characters=Coalesce(Sum('total_mt_characters'), 0), + total_nlp_llm_requests=Coalesce(Sum('total_llm_requests'), 0), ) ) except NLPUsageCounter.DoesNotExist: return { 'total_nlp_asr_seconds': 0, 'total_nlp_mt_characters': 0, + 'total_nlp_llm_requests': 0, } else: return nlp_tracking diff --git a/kpi/schema_extensions/v2/generic/schema.py b/kpi/schema_extensions/v2/generic/schema.py index 7025d9f81a..b53feb4311 100644 --- a/kpi/schema_extensions/v2/generic/schema.py +++ b/kpi/schema_extensions/v2/generic/schema.py @@ -35,6 +35,7 @@ properties={ 'total_nlp_asr_seconds': build_basic_type(OpenApiTypes.INT), 'total_nlp_mt_characters': build_basic_type(OpenApiTypes.INT), + 'total_nlp_llm_requests': build_basic_type(OpenApiTypes.INT), } ) @@ -42,8 +43,10 @@ properties={ 'asr_seconds_current_period': build_basic_type(OpenApiTypes.INT), 'mt_characters_current_period': build_basic_type(OpenApiTypes.INT), + 'llm_requests_current_period': build_basic_type(OpenApiTypes.INT), 'asr_seconds_all_time': build_basic_type(OpenApiTypes.INT), 'mt_characters_all_time': build_basic_type(OpenApiTypes.INT), + 'llm_requests_current_period': build_basic_type(OpenApiTypes.INT), } ) diff --git a/kpi/schema_extensions/v2/organizations/extensions.py b/kpi/schema_extensions/v2/organizations/extensions.py index 4233a7bdff..ecc32a3e40 100644 --- a/kpi/schema_extensions/v2/organizations/extensions.py +++ b/kpi/schema_extensions/v2/organizations/extensions.py @@ -40,6 +40,7 @@ def map_serializer_field(self, auto_schema, direction): 'storage_bytes': BALANCE_FIELDS_SCHEMA, 'asr_seconds': BALANCE_FIELDS_SCHEMA, 'mt_characters': BALANCE_FIELDS_SCHEMA, + 'llm_requests': BALANCE_FIELDS_SCHEMA, } ) diff --git a/kpi/schema_extensions/v2/service_usage/extensions.py b/kpi/schema_extensions/v2/service_usage/extensions.py index 77dc345d83..644125eac8 100644 --- a/kpi/schema_extensions/v2/service_usage/extensions.py +++ b/kpi/schema_extensions/v2/service_usage/extensions.py @@ -18,6 +18,7 @@ def map_serializer_field(self, auto_schema, direction): 'storage_bytes': BALANCE_FIELDS_SCHEMA, 'asr_seconds': BALANCE_FIELDS_SCHEMA, 'mt_characters': BALANCE_FIELDS_SCHEMA, + 'llm_requests': BALANCE_FIELDS_SCHEMA, } ) diff --git a/static/openapi/schema_v2.json b/static/openapi/schema_v2.json index 80ed9db69a..e095836bc4 100644 --- a/static/openapi/schema_v2.json +++ b/static/openapi/schema_v2.json @@ -13003,6 +13003,9 @@ }, "total_nlp_mt_characters": { "type": "integer" + }, + "total_nlp_llm_requests": { + "type": "integer" } } }, @@ -13014,6 +13017,9 @@ }, "total_nlp_mt_characters": { "type": "integer" + }, + "total_nlp_llm_requests": { + "type": "integer" } } }, @@ -15731,6 +15737,9 @@ }, "total_nlp_mt_characters": { "type": "integer" + }, + "total_nlp_llm_requests": { + "type": "integer" } } }, @@ -15742,6 +15751,9 @@ }, "total_nlp_mt_characters": { "type": "integer" + }, + "total_nlp_llm_requests": { + "type": "integer" } } }, @@ -15781,6 +15793,9 @@ "mt_characters_current_period": { "type": "integer" }, + "llm_requests_current_period": { + "type": "integer" + }, "asr_seconds_all_time": { "type": "integer" }, @@ -15873,6 +15888,23 @@ "type": "integer" } } + }, + "llm_requests": { + "type": "object", + "properties": { + "effective_limit": { + "type": "integer" + }, + "balance_value": { + "type": "integer" + }, + "balance_percent": { + "type": "integer" + }, + "exceeded": { + "type": "integer" + } + } } } }, @@ -18486,6 +18518,9 @@ "mt_characters_current_period": { "type": "integer" }, + "llm_requests_current_period": { + "type": "integer" + }, "asr_seconds_all_time": { "type": "integer" }, @@ -18578,6 +18613,23 @@ "type": "integer" } } + }, + "llm_requests": { + "type": "object", + "properties": { + "effective_limit": { + "type": "integer" + }, + "balance_value": { + "type": "integer" + }, + "balance_percent": { + "type": "integer" + }, + "exceeded": { + "type": "integer" + } + } } } }, diff --git a/static/openapi/schema_v2.yaml b/static/openapi/schema_v2.yaml index c12570f1a3..44690e7df0 100644 --- a/static/openapi/schema_v2.yaml +++ b/static/openapi/schema_v2.yaml @@ -9404,6 +9404,8 @@ components: type: integer total_nlp_mt_characters: type: integer + total_nlp_llm_requests: + type: integer nlp_usage_all_time: type: object properties: @@ -9411,6 +9413,8 @@ components: type: integer total_nlp_mt_characters: type: integer + total_nlp_llm_requests: + type: integer storage_bytes: type: integer submission_count_current_period: @@ -11346,6 +11350,8 @@ components: type: integer total_nlp_mt_characters: type: integer + total_nlp_llm_requests: + type: integer nlp_usage_all_time: type: object properties: @@ -11353,6 +11359,8 @@ components: type: integer total_nlp_mt_characters: type: integer + total_nlp_llm_requests: + type: integer storage_bytes: type: integer submission_count_current_period: @@ -11380,6 +11388,8 @@ components: type: integer mt_characters_current_period: type: integer + llm_requests_current_period: + type: integer asr_seconds_all_time: type: integer mt_characters_all_time: @@ -11440,6 +11450,17 @@ components: type: integer exceeded: type: integer + llm_requests: + type: object + properties: + effective_limit: + type: integer + balance_value: + type: integer + balance_percent: + type: integer + exceeded: + type: integer current_period_start: type: string format: date-time @@ -13303,6 +13324,8 @@ components: type: integer mt_characters_current_period: type: integer + llm_requests_current_period: + type: integer asr_seconds_all_time: type: integer mt_characters_all_time: @@ -13363,6 +13386,17 @@ components: type: integer exceeded: type: integer + llm_requests: + type: object + properties: + effective_limit: + type: integer + balance_value: + type: integer + balance_percent: + type: integer + exceeded: + type: integer current_period_start: type: string format: date-time From 6febb592d5f69274a3102c03983eacfe31d67cf5 Mon Sep 17 00:00:00 2001 From: James Kiger Date: Thu, 18 Sep 2025 08:56:40 -0400 Subject: [PATCH 03/12] Generate docs after merge --- ...ServiceUsageResponseBalancesLlmRequests.ts | 14 ---------- ...serviceUsageResponseBalancesLlmRequests.ts | 14 ---------- static/openapi/schema_v2.json | 8 +++++- static/openapi/schema_v2.yaml | 26 +++---------------- 4 files changed, 10 insertions(+), 52 deletions(-) delete mode 100644 jsapp/js/api/models/organizationServiceUsageResponseBalancesLlmRequests.ts delete mode 100644 jsapp/js/api/models/serviceUsageResponseBalancesLlmRequests.ts diff --git a/jsapp/js/api/models/organizationServiceUsageResponseBalancesLlmRequests.ts b/jsapp/js/api/models/organizationServiceUsageResponseBalancesLlmRequests.ts deleted file mode 100644 index 03d2b6786c..0000000000 --- a/jsapp/js/api/models/organizationServiceUsageResponseBalancesLlmRequests.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * KoboToolbox API - * Powerful and intuitive data collection tools to make an impact - * OpenAPI spec version: 2.0.0 (api_v2) - */ - -export type OrganizationServiceUsageResponseBalancesLlmRequests = { - effective_limit?: number - balance_value?: number - balance_percent?: number - exceeded?: number -} diff --git a/jsapp/js/api/models/serviceUsageResponseBalancesLlmRequests.ts b/jsapp/js/api/models/serviceUsageResponseBalancesLlmRequests.ts deleted file mode 100644 index b9d6da31dd..0000000000 --- a/jsapp/js/api/models/serviceUsageResponseBalancesLlmRequests.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated by orval v7.10.0 🍺 - * Do not edit manually. - * KoboToolbox API - * Powerful and intuitive data collection tools to make an impact - * OpenAPI spec version: 2.0.0 (api_v2) - */ - -export type ServiceUsageResponseBalancesLlmRequests = { - effective_limit?: number - balance_value?: number - balance_percent?: number - exceeded?: number -} diff --git a/static/openapi/schema_v2.json b/static/openapi/schema_v2.json index a00e5160ed..fff9b43703 100644 --- a/static/openapi/schema_v2.json +++ b/static/openapi/schema_v2.json @@ -15832,6 +15832,9 @@ }, "mt_characters": { "$ref": "#/components/schemas/ServiceUsageBalanceData" + }, + "llm_requests": { + "$ref": "#/components/schemas/ServiceUsageBalanceData" } } }, @@ -18501,6 +18504,9 @@ }, "mt_characters": { "$ref": "#/components/schemas/ServiceUsageBalanceData" + }, + "llm_requests": { + "$ref": "#/components/schemas/ServiceUsageBalanceData" } } }, @@ -19012,4 +19018,4 @@ } } } -} +} \ No newline at end of file diff --git a/static/openapi/schema_v2.yaml b/static/openapi/schema_v2.yaml index ed439e7220..d3f643795c 100644 --- a/static/openapi/schema_v2.yaml +++ b/static/openapi/schema_v2.yaml @@ -11414,6 +11414,8 @@ components: $ref: '#/components/schemas/ServiceUsageBalanceData' mt_characters: $ref: '#/components/schemas/ServiceUsageBalanceData' + llm_requests: + $ref: '#/components/schemas/ServiceUsageBalanceData' current_period_start: type: string format: date-time @@ -13313,31 +13315,9 @@ components: asr_seconds: $ref: '#/components/schemas/ServiceUsageBalanceData' mt_characters: -<<<<<<< HEAD - type: object - properties: - effective_limit: - type: integer - balance_value: - type: integer - balance_percent: - type: integer - exceeded: - type: integer + $ref: '#/components/schemas/ServiceUsageBalanceData' llm_requests: - type: object - properties: - effective_limit: - type: integer - balance_value: - type: integer - balance_percent: - type: integer - exceeded: - type: integer -======= $ref: '#/components/schemas/ServiceUsageBalanceData' ->>>>>>> main current_period_start: type: string format: date-time From cc679982fab03091fbf1f4ffbd7d1563716f3df5 Mon Sep 17 00:00:00 2001 From: James Kiger Date: Thu, 18 Sep 2025 09:11:57 -0400 Subject: [PATCH 04/12] Fix field name --- .editorconfig | 2 +- .../models/organizationServiceUsageResponseTotalNlpUsage.ts | 1 + jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts | 1 + kpi/schema_extensions/v2/generic/schema.py | 2 +- static/openapi/schema_v2.json | 6 ++++++ static/openapi/schema_v2.yaml | 4 ++++ 6 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index 89453b61ac..1a506ea8d3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,7 +5,7 @@ root = true charset = utf-8 end_of_line = lf indent_style = space -insert_final_newline = true +# insert_final_newline = true trim_trailing_whitespace = true # 2 spaces - frontend files mostly diff --git a/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts b/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts index 4887682c06..3d095f9bed 100644 --- a/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts +++ b/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts @@ -12,4 +12,5 @@ export type OrganizationServiceUsageResponseTotalNlpUsage = { llm_requests_current_period?: number asr_seconds_all_time?: number mt_characters_all_time?: number + llm_requests_all_time?: number } diff --git a/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts b/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts index 0f32850974..5e12df19b7 100644 --- a/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts +++ b/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts @@ -12,4 +12,5 @@ export type ServiceUsageResponseTotalNlpUsage = { llm_requests_current_period?: number asr_seconds_all_time?: number mt_characters_all_time?: number + llm_requests_all_time?: number } diff --git a/kpi/schema_extensions/v2/generic/schema.py b/kpi/schema_extensions/v2/generic/schema.py index b53feb4311..b217657b38 100644 --- a/kpi/schema_extensions/v2/generic/schema.py +++ b/kpi/schema_extensions/v2/generic/schema.py @@ -46,7 +46,7 @@ 'llm_requests_current_period': build_basic_type(OpenApiTypes.INT), 'asr_seconds_all_time': build_basic_type(OpenApiTypes.INT), 'mt_characters_all_time': build_basic_type(OpenApiTypes.INT), - 'llm_requests_current_period': build_basic_type(OpenApiTypes.INT), + 'llm_requests_all_time': build_basic_type(OpenApiTypes.INT), } ) diff --git a/static/openapi/schema_v2.json b/static/openapi/schema_v2.json index fff9b43703..390a5dfa9a 100644 --- a/static/openapi/schema_v2.json +++ b/static/openapi/schema_v2.json @@ -15801,6 +15801,9 @@ }, "mt_characters_all_time": { "type": "integer" + }, + "llm_requests_all_time": { + "type": "integer" } } }, @@ -18473,6 +18476,9 @@ }, "mt_characters_all_time": { "type": "integer" + }, + "llm_requests_all_time": { + "type": "integer" } } }, diff --git a/static/openapi/schema_v2.yaml b/static/openapi/schema_v2.yaml index d3f643795c..d8a8f0baad 100644 --- a/static/openapi/schema_v2.yaml +++ b/static/openapi/schema_v2.yaml @@ -11394,6 +11394,8 @@ components: type: integer mt_characters_all_time: type: integer + llm_requests_all_time: + type: integer total_storage_bytes: type: integer total_submission_count: @@ -13296,6 +13298,8 @@ components: type: integer mt_characters_all_time: type: integer + llm_requests_all_time: + type: integer total_storage_bytes: type: integer total_submission_count: From 3e725000ada695a751b911009649ad5eec1693fa Mon Sep 17 00:00:00 2001 From: James Kiger Date: Thu, 18 Sep 2025 09:19:03 -0400 Subject: [PATCH 05/12] remove change accidentally committed. --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 1a506ea8d3..89453b61ac 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,7 +5,7 @@ root = true charset = utf-8 end_of_line = lf indent_style = space -# insert_final_newline = true +insert_final_newline = true trim_trailing_whitespace = true # 2 spaces - frontend files mostly From 1b9b301d6366ce6956827da7bf853b394a497c45 Mon Sep 17 00:00:00 2001 From: James Kiger Date: Thu, 18 Sep 2025 09:51:10 -0400 Subject: [PATCH 06/12] Update asset usage test --- kpi/tests/api/v2/test_api_asset_usage.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/kpi/tests/api/v2/test_api_asset_usage.py b/kpi/tests/api/v2/test_api_asset_usage.py index af746fd322..88ddb7ec00 100644 --- a/kpi/tests/api/v2/test_api_asset_usage.py +++ b/kpi/tests/api/v2/test_api_asset_usage.py @@ -31,6 +31,7 @@ def __add_nlp_trackers(self): counter_1 = { 'google_asr_seconds': 4586, 'google_mt_characters': 5473, + 'some_service_llm_requests': 20, } NLPUsageCounter.objects.create( user_id=self.anotheruser.id, @@ -39,6 +40,7 @@ def __add_nlp_trackers(self): counters=counter_1, total_asr_seconds=counter_1['google_asr_seconds'], total_mt_characters=counter_1['google_mt_characters'], + total_llm_requests=counter_1['some_service_llm_requests'], ) # last month @@ -46,6 +48,7 @@ def __add_nlp_trackers(self): counter_2 = { 'google_asr_seconds': 142, 'google_mt_characters': 1253, + 'some_service_llm_requests': 50, } NLPUsageCounter.objects.create( user_id=self.anotheruser.id, @@ -54,6 +57,7 @@ def __add_nlp_trackers(self): counters=counter_2, total_asr_seconds=counter_2['google_asr_seconds'], total_mt_characters=counter_2['google_mt_characters'], + total_llm_requests=counter_2['some_service_llm_requests'], ) def __add_submissions(self): @@ -177,6 +181,12 @@ def test_check_api_response(self): ] == 4586 ) + assert ( + response.data['results'][0]['nlp_usage_current_period'][ + 'total_nlp_llm_requests' + ] + == 20 + ) assert ( response.data['results'][0]['nlp_usage_current_period'][ 'total_nlp_mt_characters' @@ -191,6 +201,10 @@ def test_check_api_response(self): response.data['results'][0]['nlp_usage_all_time']['total_nlp_mt_characters'] == 6726 ) + assert ( + response.data['results'][0]['nlp_usage_all_time']['total_nlp_llm_requests'] + == 70 + ) assert ( response.data['results'][0]['storage_bytes'] == ( From 9283aa018f4ed13835794c2fee07f6ceed7b9110 Mon Sep 17 00:00:00 2001 From: James Kiger Date: Thu, 25 Sep 2025 12:30:18 -0400 Subject: [PATCH 07/12] Update stripe/limits code to handle usage type --- .../viewsets/test_xform_submission_api.py | 2 +- kobo/apps/organizations/constants.py | 10 -- .../test_organizations_service_usage_api.py | 4 +- kobo/apps/stripe/models.py | 24 ++++- kobo/apps/stripe/tests/test_stripe_utils.py | 100 ++++++++---------- kobo/apps/stripe/tests/utils.py | 1 + kobo/apps/stripe/utils/subscription_limits.py | 19 +++- kpi/tests/test_usage_calculator.py | 6 +- kpi/utils/usage_calculator.py | 6 +- 9 files changed, 94 insertions(+), 78 deletions(-) diff --git a/kobo/apps/openrosa/apps/api/tests/viewsets/test_xform_submission_api.py b/kobo/apps/openrosa/apps/api/tests/viewsets/test_xform_submission_api.py index 3321500594..8e7cc99423 100644 --- a/kobo/apps/openrosa/apps/api/tests/viewsets/test_xform_submission_api.py +++ b/kobo/apps/openrosa/apps/api/tests/viewsets/test_xform_submission_api.py @@ -75,7 +75,7 @@ def test_query_counts(self): # USAGE_LIMIT_ENFORCEMENT variable. But we use caching # so should find a way to keep that out of this count if settings.STRIPE_ENABLED: - expected_queries = FuzzyInt(80, 87) + expected_queries = FuzzyInt(82, 90) with self.assertNumQueries(expected_queries): self.view(request) diff --git a/kobo/apps/organizations/constants.py b/kobo/apps/organizations/constants.py index cc56855cfa..c446676cb1 100644 --- a/kobo/apps/organizations/constants.py +++ b/kobo/apps/organizations/constants.py @@ -1,16 +1,6 @@ from django.db import models -# Temporary const for code that iterates over usage types -# TODO: Remove when Stripe code has been updated to -# handle LLM usage type -class SupportedUsageType(models.TextChoices): - SUBMISSION = 'submission' - STORAGE_BYTES = 'storage_bytes' - MT_CHARACTERS = 'mt_characters' - ASR_SECONDS = 'asr_seconds' - - class UsageType(models.TextChoices): SUBMISSION = 'submission' STORAGE_BYTES = 'storage_bytes' diff --git a/kobo/apps/organizations/tests/test_organizations_service_usage_api.py b/kobo/apps/organizations/tests/test_organizations_service_usage_api.py index 07303f8db6..5b50edfc64 100644 --- a/kobo/apps/organizations/tests/test_organizations_service_usage_api.py +++ b/kobo/apps/organizations/tests/test_organizations_service_usage_api.py @@ -83,8 +83,8 @@ def test_check_api_response_without_stripe(self): assert response.data['total_nlp_usage']['asr_seconds_all_time'] == 4728 assert response.data['total_nlp_usage']['mt_characters_current_period'] == 5473 assert response.data['total_nlp_usage']['mt_characters_all_time'] == 6726 - assert response.data['total_nlp_usage']['llm_requests_current_period'] == 20 - assert response.data['total_nlp_usage']['llm_requests_all_time'] == 70 + assert response.data['total_nlp_usage']['llm_requests_current_period'] == 30 + assert response.data['total_nlp_usage']['llm_requests_all_time'] == 80 assert response.data['total_storage_bytes'] == self.expected_file_size() # Without stripe, there are no usage limits and diff --git a/kobo/apps/stripe/models.py b/kobo/apps/stripe/models.py index e4d9e17371..6091ce28a0 100644 --- a/kobo/apps/stripe/models.py +++ b/kobo/apps/stripe/models.py @@ -9,7 +9,7 @@ from djstripe.models import Charge, Price, Subscription from kobo.apps.kobo_auth.shortcuts import User -from kobo.apps.organizations.constants import SupportedUsageType, UsageType +from kobo.apps.organizations.constants import UsageType from kobo.apps.organizations.models import Organization from kobo.apps.stripe.constants import ACTIVE_STRIPE_STATUSES from kobo.apps.stripe.utils.subscription_limits import get_default_add_on_limits @@ -176,6 +176,26 @@ def get_organizations_totals(organizations: list['Organization'] = None): ), 0, ), + llm_requests_remaining=Coalesce( + Sum( + Cast( + PlanAddOn.get_limits_remaining_field( + UsageType.LLM_REQUESTS + ), + output_field=IntegerField(), + ) + ), + 0, + ), + total_llm_requests_limit=Coalesce( + Sum( + Cast( + PlanAddOn.get_usage_limits_field(UsageType.LLM_REQUESTS), + output_field=IntegerField(), + ) + ), + 0, + ), submission_remaining=Coalesce( Sum( Cast( @@ -311,6 +331,6 @@ class ExceededLimitCounter(models.Model): on_delete=models.CASCADE, ) days = models.PositiveSmallIntegerField(default=0) - limit_type = models.CharField(choices=SupportedUsageType.choices, max_length=20) + limit_type = models.CharField(choices=UsageType.choices, max_length=20) date_created = models.DateTimeField(auto_now_add=True) date_modified = models.DateTimeField(auto_now=True) diff --git a/kobo/apps/stripe/tests/test_stripe_utils.py b/kobo/apps/stripe/tests/test_stripe_utils.py index 4822919bff..fb21fd7e54 100644 --- a/kobo/apps/stripe/tests/test_stripe_utils.py +++ b/kobo/apps/stripe/tests/test_stripe_utils.py @@ -14,7 +14,7 @@ from model_bakery import baker from kobo.apps.kobo_auth.shortcuts import User -from kobo.apps.organizations.constants import SupportedUsageType, UsageType +from kobo.apps.organizations.constants import UsageType from kobo.apps.organizations.models import Organization from kobo.apps.organizations.utils import get_billing_dates from kobo.apps.stripe.models import ExceededLimitCounter @@ -67,75 +67,59 @@ def setUpTestData(cls): def test_get_organization_subscription_limits(self): free_plan = generate_free_plan() + first_paid_plan_limits = { + f'{UsageType.MT_CHARACTERS}_limit': '100', + f'{UsageType.ASR_SECONDS}_limit': '200', + f'{UsageType.LLM_REQUESTS}_limit': '300', + f'{UsageType.SUBMISSION}_limit': '400', + f'{UsageType.STORAGE_BYTES}_limit': '500', + } + # second plan for org2 where we append (not add) 1 to all the limits + second_paid_plan_limits = { + key: f'{value}1' for key, value in first_paid_plan_limits.items() + } product_metadata = { - f'{UsageType.MT_CHARACTERS}_limit': '1234', - f'{UsageType.ASR_SECONDS}_limit': '5678', - f'{UsageType.SUBMISSION}_limit': '91011', - f'{UsageType.STORAGE_BYTES}_limit': '121314', 'product_type': 'plan', 'plan_type': 'enterprise', } - # create a second plan for org2 where we append (not add) 1 to all the limits + first_product_metadata = { + **first_paid_plan_limits, + **product_metadata, + } second_product_metadata = { - key: f'{value}1' if key.endswith('limit') else value - for key, value in product_metadata.items() + **second_paid_plan_limits, + **product_metadata, } - generate_plan_subscription(self.organization, metadata=product_metadata) + + generate_plan_subscription(self.organization, metadata=first_product_metadata) generate_plan_subscription( self.second_organization, metadata=second_product_metadata ) all_limits = get_organizations_subscription_limits() - assert ( - all_limits[self.organization.id][f'{UsageType.MT_CHARACTERS}_limit'] == 1234 - ) - assert ( - all_limits[self.second_organization.id][f'{UsageType.MT_CHARACTERS}_limit'] - == 12341 - ) - assert ( - all_limits[self.organization.id][f'{UsageType.ASR_SECONDS}_limit'] == 5678 - ) - assert ( - all_limits[self.second_organization.id][f'{UsageType.ASR_SECONDS}_limit'] - == 56781 - ) - assert ( - all_limits[self.organization.id][f'{UsageType.SUBMISSION}_limit'] == 91011 - ) - assert ( - all_limits[self.second_organization.id][f'{UsageType.SUBMISSION}_limit'] - == 910111 - ) - assert ( - all_limits[self.organization.id][f'{UsageType.STORAGE_BYTES}_limit'] - == 121314 - ) - assert ( - all_limits[self.second_organization.id][f'{UsageType.STORAGE_BYTES}_limit'] - == 1213141 - ) other_orgs = Organization.objects.exclude( id__in=[self.organization.id, self.second_organization.id] ) - for org in other_orgs: - assert all_limits[org.id][f'{UsageType.MT_CHARACTERS}_limit'] == int( - free_plan.metadata[f'{UsageType.MT_CHARACTERS}_limit'] - ) - assert all_limits[org.id][f'{UsageType.ASR_SECONDS}_limit'] == int( - free_plan.metadata[f'{UsageType.ASR_SECONDS}_limit'] - ) - assert all_limits[org.id][f'{UsageType.SUBMISSION}_limit'] == int( - free_plan.metadata[f'{UsageType.SUBMISSION}_limit'] + for usage_type, _ in UsageType.choices: + assert ( + all_limits[self.organization.id][f'{usage_type}_limit'] + == float(first_paid_plan_limits[f'{usage_type}_limit']) ) - assert all_limits[org.id][f'{UsageType.STORAGE_BYTES}_limit'] == int( - free_plan.metadata[f'{UsageType.STORAGE_BYTES}_limit'] + assert ( + all_limits[self.second_organization.id][f'{usage_type}_limit'] + == float(second_paid_plan_limits[f'{usage_type}_limit']) ) + for org in other_orgs: + assert all_limits[org.id][f'{usage_type}_limit'] == float( + free_plan.metadata[f'{usage_type}_limit'] + ) + def test__prioritizes_price_metadata(self): product_metadata = { f'{UsageType.MT_CHARACTERS}_limit': '1', f'{UsageType.ASR_SECONDS}_limit': '1', + f'{UsageType.LLM_REQUESTS}_limit': '1', f'{UsageType.SUBMISSION}_limit': '1', f'{UsageType.STORAGE_BYTES}_limit': '1', 'product_type': 'plan', @@ -144,6 +128,7 @@ def test__prioritizes_price_metadata(self): price_metadata = { f'{UsageType.MT_CHARACTERS}_limit': '2', f'{UsageType.ASR_SECONDS}_limit': '2', + f'{UsageType.LLM_REQUESTS}_limit': '2', f'{UsageType.SUBMISSION}_limit': '2', f'{UsageType.STORAGE_BYTES}_limit': '2', } @@ -151,7 +136,7 @@ def test__prioritizes_price_metadata(self): self.organization, metadata=product_metadata, price_metadata=price_metadata ) limits = get_paid_subscription_limits([self.organization.id]).first() - for usage_type, _ in SupportedUsageType.choices: + for usage_type, _ in UsageType.choices: assert limits[f'{usage_type}_limit'] == '2' def test_get_subscription_limits_takes_most_recent_active_subscriptions(self): @@ -477,6 +462,7 @@ def test_get_org_effective_limits(self, include_onetime_addons): plan_product_metadata = { f'{UsageType.MT_CHARACTERS}_limit': '1', f'{UsageType.ASR_SECONDS}_limit': '1', + f'{UsageType.LLM_REQUESTS}_limit': '1', f'{UsageType.SUBMISSION}_limit': '1', f'{UsageType.STORAGE_BYTES}_limit': '1', 'product_type': 'plan', @@ -485,6 +471,7 @@ def test_get_org_effective_limits(self, include_onetime_addons): product_metadata = { f'{UsageType.MT_CHARACTERS}_limit': '2', f'{UsageType.ASR_SECONDS}_limit': '2', + f'{UsageType.LLM_REQUESTS}_limit': '2', f'{UsageType.SUBMISSION}_limit': '2', f'{UsageType.STORAGE_BYTES}_limit': '2', 'product_type': 'plan', @@ -497,6 +484,7 @@ def test_get_org_effective_limits(self, include_onetime_addons): one_time_nlp_addon_metadata = { f'{UsageType.MT_CHARACTERS}_limit': '15', f'{UsageType.ASR_SECONDS}_limit': '20', + f'{UsageType.LLM_REQUESTS}_limit': '20', } nlp_addon = _create_one_time_addon_product(one_time_nlp_addon_metadata) customer = baker.make(Customer, subscriber=self.organization) @@ -526,11 +514,15 @@ def test_get_org_effective_limits(self, include_onetime_addons): assert ( results[self.second_organization.id][f'{UsageType.ASR_SECONDS}_limit'] == 2 ) + assert ( + results[self.second_organization.id][f'{UsageType.LLM_REQUESTS}_limit'] == 2 + ) if include_onetime_addons: assert ( results[self.organization.id][f'{UsageType.MT_CHARACTERS}_limit'] == 16 ) assert results[self.organization.id][f'{UsageType.ASR_SECONDS}_limit'] == 21 + assert results[self.organization.id][f'{UsageType.LLM_REQUESTS}_limit'] == 21 assert ( results[self.second_organization.id][f'{UsageType.SUBMISSION}_limit'] == 12 @@ -540,6 +532,7 @@ def test_get_org_effective_limits(self, include_onetime_addons): results[self.organization.id][f'{UsageType.MT_CHARACTERS}_limit'] == 1 ) assert results[self.organization.id][f'{UsageType.ASR_SECONDS}_limit'] == 1 + assert results[self.organization.id][f'{UsageType.LLM_REQUESTS}_limit'] == 1 assert ( results[self.second_organization.id][f'{UsageType.SUBMISSION}_limit'] == 2 @@ -603,6 +596,7 @@ def setUp(self): product_metadata = { f'{UsageType.MT_CHARACTERS}_limit': '1', f'{UsageType.ASR_SECONDS}_limit': '1', + f'{UsageType.LLM_REQUESTS}_limit': '1', f'{UsageType.SUBMISSION}_limit': '1', f'{UsageType.STORAGE_BYTES}_limit': '1', 'product_type': 'plan', @@ -627,7 +621,7 @@ def test_check_exceeded_limit_adds_counters(self): ): self.add_submissions(count=2, asset=self.asset, username='someuser') self.add_nlp_trackers() - for usage_type, _ in SupportedUsageType.choices: + for usage_type, _ in UsageType.choices: check_exceeded_limit(self.someuser, usage_type) assert ( ExceededLimitCounter.objects.filter( @@ -638,7 +632,7 @@ def test_check_exceeded_limit_adds_counters(self): def test_check_exceeded_limit_updates_counters(self): today = timezone.now() - for usage_type, _ in SupportedUsageType.choices: + for usage_type, _ in UsageType.choices: baker.make( ExceededLimitCounter, user=self.anotheruser, @@ -655,7 +649,7 @@ def test_check_exceeded_limit_updates_counters(self): ): self.add_submissions(count=2, asset=self.asset, username='someuser') self.add_nlp_trackers() - for usage_type, _ in SupportedUsageType.choices: + for usage_type, _ in UsageType.choices: check_exceeded_limit(self.someuser, usage_type) counter = ExceededLimitCounter.objects.get( user_id=self.anotheruser.id, limit_type=usage_type diff --git a/kobo/apps/stripe/tests/utils.py b/kobo/apps/stripe/tests/utils.py index c580079048..efe2e33dcf 100644 --- a/kobo/apps/stripe/tests/utils.py +++ b/kobo/apps/stripe/tests/utils.py @@ -23,6 +23,7 @@ def generate_free_plan(): 'product_type': 'plan', f'{UsageType.SUBMISSION}_limit': '5000', f'{UsageType.ASR_SECONDS}_limit': '600', + f'{UsageType.LLM_REQUESTS}_limit': '20', f'{UsageType.MT_CHARACTERS}_limit': '6000', f'{UsageType.STORAGE_BYTES}_limit': '1000', 'default_free_plan': 'true', diff --git a/kobo/apps/stripe/utils/subscription_limits.py b/kobo/apps/stripe/utils/subscription_limits.py index 5ce2d25963..49eca74107 100644 --- a/kobo/apps/stripe/utils/subscription_limits.py +++ b/kobo/apps/stripe/utils/subscription_limits.py @@ -6,7 +6,7 @@ from django.db.models import F, Max, Q, QuerySet, Window from django.db.models.functions import Coalesce -from kobo.apps.organizations.constants import SupportedUsageType, UsageType +from kobo.apps.organizations.constants import UsageType from kobo.apps.organizations.models import Organization, OrganizationUser from kobo.apps.organizations.types import UsageLimits from kobo.apps.stripe.constants import ACTIVE_STRIPE_STATUSES @@ -34,6 +34,7 @@ def get_default_add_on_limits(): f'{UsageType.SUBMISSION}_limit': 0, f'{UsageType.ASR_SECONDS}_limit': 0, f'{UsageType.MT_CHARACTERS}_limit': 0, + f'{UsageType.LLM_REQUESTS}_limit': 0, } @@ -114,7 +115,7 @@ def get_organizations_subscription_limits( if row['product_type'] == 'plan': row_limits = { f'{usage_type}_limit': row[f'{usage_type}_limit'] - for usage_type, _ in SupportedUsageType.choices + for usage_type, _ in UsageType.choices } elif row['product_type'] == 'addon': row_limits['addon_storage_limit'] = row[f'{UsageType.STORAGE_BYTES}_limit'] @@ -124,6 +125,7 @@ def get_organizations_subscription_limits( submission_limit = _get_limit_key(UsageType.SUBMISSION) characters_limit = _get_limit_key(UsageType.MT_CHARACTERS) seconds_limit = _get_limit_key(UsageType.ASR_SECONDS) + requests_limit = _get_limit_key(UsageType.LLM_REQUESTS) # Anyone who does not have a subscription is on the free tier plan by default default_plan = ( Product.objects.filter(metadata__default_free_plan='true') @@ -132,11 +134,12 @@ def get_organizations_subscription_limits( submission_limit=F(f'metadata__{submission_limit}'), mt_characters_limit=F(f'metadata__{characters_limit}'), asr_seconds_limit=F(f'metadata__{seconds_limit}'), + llm_requests_limit=F(f'metadata__{requests_limit}'), ) .first() ) or {} default_plan_limits = {} - for usage_type, _ in SupportedUsageType.choices: + for usage_type, _ in UsageType.choices: limit_key = f'{usage_type}_limit' default_limit = default_plan.get(limit_key) if default_limit is None: @@ -147,7 +150,7 @@ def get_organizations_subscription_limits( results = {} for org_id in all_org_ids: all_org_limits = {} - for usage_type, _ in SupportedUsageType.choices: + for usage_type, _ in UsageType.choices: plan_limit = subscription_limits_by_org_id.get(org_id, {}).get( f'{usage_type}_limit' ) @@ -205,7 +208,7 @@ def get_organizations_effective_limits( PlanAddOn = apps.get_model('stripe', 'PlanAddOn') # noqa addon_limits = PlanAddOn.get_organizations_totals(organizations=organizations) for org_id, limits in effective_limits.items(): - for usage_type, _ in SupportedUsageType.choices: + for usage_type, _ in UsageType.choices: addon = addon_limits.get(org_id, {}).get(f'total_{usage_type}_limit', 0) limits[f'{usage_type}_limit'] += addon return effective_limits @@ -235,6 +238,9 @@ def get_paid_subscription_limits(organization_ids: list[str], **kwargs) -> Query price_seconds_key, product_seconds_key = ( _get_subscription_metadata_fields_for_usage_type(UsageType.ASR_SECONDS) ) + price_requests_key, product_requests_key = ( + _get_subscription_metadata_fields_for_usage_type(UsageType.LLM_REQUESTS) + ) # Get organizations we care about (either those in the 'organizations' param or all) org_filter = Q(customer__subscriber_id__in=[org_id for org_id in organization_ids]) @@ -256,6 +262,9 @@ def get_paid_subscription_limits(organization_ids: list[str], **kwargs) -> Query mt_characters_limit=Coalesce( F(price_characters_key), F(product_characters_key) ), + llm_requests_limit=Coalesce( + F(price_requests_key), F(product_requests_key) + ), sub_start_date=F('start_date'), product_type=F('items__price__product__metadata__product_type'), ) diff --git a/kpi/tests/test_usage_calculator.py b/kpi/tests/test_usage_calculator.py index c3a066822c..e706e5c5b5 100644 --- a/kpi/tests/test_usage_calculator.py +++ b/kpi/tests/test_usage_calculator.py @@ -104,7 +104,7 @@ def add_nlp_trackers( self, seconds_current_month=4586, characters_current_month=5473, - requests_current_month=20, + requests_current_month=30, seconds_last_month=142, characters_last_month=1253, requests_last_month=50, @@ -223,8 +223,8 @@ def test_nlp_usage_counters(self): assert nlp_usage['asr_seconds_all_time'] == 4728 assert nlp_usage['mt_characters_current_period'] == 5473 assert nlp_usage['mt_characters_all_time'] == 6726 - assert nlp_usage['llm_requests_current_period'] == 20 - assert nlp_usage['llm_requests_all_time'] == 70 + assert nlp_usage['llm_requests_current_period'] == 30 + assert nlp_usage['llm_requests_all_time'] == 80 def test_no_data(self): self.add_nlp_trackers() diff --git a/kpi/utils/usage_calculator.py b/kpi/utils/usage_calculator.py index 1455b38daa..c9b340dd0d 100644 --- a/kpi/utils/usage_calculator.py +++ b/kpi/utils/usage_calculator.py @@ -186,7 +186,6 @@ def get_usage_balances(self) -> UsageBalances: limits = get_organizations_effective_limits([self.organization], True, True) org_limits = limits[self.organization.id] - # TODO: Check usage limit for LLM requests when supported by Stripe code return { UsageType.SUBMISSION: calculate_usage_balance( limit=org_limits[f'{UsageType.SUBMISSION}_limit'], @@ -204,7 +203,10 @@ def get_usage_balances(self) -> UsageBalances: limit=org_limits[f'{UsageType.MT_CHARACTERS}_limit'], usage=self.get_nlp_usage_by_type(UsageType.MT_CHARACTERS), ), - UsageType.LLM_REQUESTS: None, + UsageType.LLM_REQUESTS: calculate_usage_balance( + limit=org_limits[f'{UsageType.LLM_REQUESTS}_limit'], + usage=self.get_nlp_usage_by_type(UsageType.LLM_REQUESTS), + ), } @cached_class_property( From fab6c1ff93df103a102b9debde89b0912958c0c2 Mon Sep 17 00:00:00 2001 From: James Kiger Date: Wed, 5 Nov 2025 12:23:41 -0500 Subject: [PATCH 08/12] Post-merge fixes --- .../assetUsageResponseNlpUsageAllTime.ts | 1 + ...assetUsageResponseNlpUsageCurrentPeriod.ts | 1 + ...zationAssetUsageResponseNlpUsageAllTime.ts | 1 + ...AssetUsageResponseNlpUsageCurrentPeriod.ts | 1 + ...zationServiceUsageResponseTotalNlpUsage.ts | 2 ++ .../serviceUsageResponseTotalNlpUsage.ts | 2 ++ kobo/apps/stripe/tests/test_stripe_utils.py | 16 +++++++-------- kobo/apps/stripe/utils/subscription_limits.py | 4 +--- kpi/schema_extensions/v2/generic/schema.py | 8 +++++--- kpi/utils/schema_extensions/url_builder.py | 2 +- static/openapi/schema_v2.json | 16 ++++++++++++++- static/openapi/schema_v2.yaml | 20 +++++++++++++++++++ 12 files changed, 58 insertions(+), 16 deletions(-) diff --git a/jsapp/js/api/models/assetUsageResponseNlpUsageAllTime.ts b/jsapp/js/api/models/assetUsageResponseNlpUsageAllTime.ts index 87e165cbbd..1a74f74f68 100644 --- a/jsapp/js/api/models/assetUsageResponseNlpUsageAllTime.ts +++ b/jsapp/js/api/models/assetUsageResponseNlpUsageAllTime.ts @@ -13,4 +13,5 @@ The endpoints are grouped by area of intended use. Each category contains relate export type AssetUsageResponseNlpUsageAllTime = { total_nlp_asr_seconds: number total_nlp_mt_characters: number + total_nlp_llm_requests: number } diff --git a/jsapp/js/api/models/assetUsageResponseNlpUsageCurrentPeriod.ts b/jsapp/js/api/models/assetUsageResponseNlpUsageCurrentPeriod.ts index e632a67114..65d8d6faca 100644 --- a/jsapp/js/api/models/assetUsageResponseNlpUsageCurrentPeriod.ts +++ b/jsapp/js/api/models/assetUsageResponseNlpUsageCurrentPeriod.ts @@ -13,4 +13,5 @@ The endpoints are grouped by area of intended use. Each category contains relate export type AssetUsageResponseNlpUsageCurrentPeriod = { total_nlp_asr_seconds: number total_nlp_mt_characters: number + total_nlp_llm_requests: number } diff --git a/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageAllTime.ts b/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageAllTime.ts index 1ffa8c6ab3..3f7f872886 100644 --- a/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageAllTime.ts +++ b/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageAllTime.ts @@ -13,4 +13,5 @@ The endpoints are grouped by area of intended use. Each category contains relate export type OrganizationAssetUsageResponseNlpUsageAllTime = { total_nlp_asr_seconds: number total_nlp_mt_characters: number + total_nlp_llm_requests: number } diff --git a/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageCurrentPeriod.ts b/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageCurrentPeriod.ts index 44edc42942..4c6f2f70de 100644 --- a/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageCurrentPeriod.ts +++ b/jsapp/js/api/models/organizationAssetUsageResponseNlpUsageCurrentPeriod.ts @@ -13,4 +13,5 @@ The endpoints are grouped by area of intended use. Each category contains relate export type OrganizationAssetUsageResponseNlpUsageCurrentPeriod = { total_nlp_asr_seconds: number total_nlp_mt_characters: number + total_nlp_llm_requests: number } diff --git a/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts b/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts index 40a39bf58b..f0c794edd9 100644 --- a/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts +++ b/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts @@ -13,6 +13,8 @@ The endpoints are grouped by area of intended use. Each category contains relate export type OrganizationServiceUsageResponseTotalNlpUsage = { asr_seconds_current_period: number mt_characters_current_period: number + llm_requests_current_period: number asr_seconds_all_time: number mt_characters_all_time: number + llm_requests_all_time: number } diff --git a/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts b/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts index 2b00a587f4..4877a616d3 100644 --- a/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts +++ b/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts @@ -13,6 +13,8 @@ The endpoints are grouped by area of intended use. Each category contains relate export type ServiceUsageResponseTotalNlpUsage = { asr_seconds_current_period: number mt_characters_current_period: number + llm_requests_current_period: number asr_seconds_all_time: number mt_characters_all_time: number + llm_requests_all_time: number } diff --git a/kobo/apps/stripe/tests/test_stripe_utils.py b/kobo/apps/stripe/tests/test_stripe_utils.py index 24f4f13b0c..ffd0c0d8fa 100644 --- a/kobo/apps/stripe/tests/test_stripe_utils.py +++ b/kobo/apps/stripe/tests/test_stripe_utils.py @@ -101,14 +101,12 @@ def test_get_organization_subscription_limits(self): id__in=[self.organization.id, self.second_organization.id] ) for usage_type, _ in UsageType.choices: - assert ( - all_limits[self.organization.id][f'{usage_type}_limit'] - == float(first_paid_plan_limits[f'{usage_type}_limit']) - ) - assert ( - all_limits[self.second_organization.id][f'{usage_type}_limit'] - == float(second_paid_plan_limits[f'{usage_type}_limit']) + assert all_limits[self.organization.id][f'{usage_type}_limit'] == float( + first_paid_plan_limits[f'{usage_type}_limit'] ) + assert all_limits[self.second_organization.id][ + f'{usage_type}_limit' + ] == float(second_paid_plan_limits[f'{usage_type}_limit']) for org in other_orgs: assert all_limits[org.id][f'{usage_type}_limit'] == float( @@ -522,7 +520,9 @@ def test_get_org_effective_limits(self, include_onetime_addons): results[self.organization.id][f'{UsageType.MT_CHARACTERS}_limit'] == 16 ) assert results[self.organization.id][f'{UsageType.ASR_SECONDS}_limit'] == 21 - assert results[self.organization.id][f'{UsageType.LLM_REQUESTS}_limit'] == 21 + assert ( + results[self.organization.id][f'{UsageType.LLM_REQUESTS}_limit'] == 21 + ) assert ( results[self.second_organization.id][f'{UsageType.SUBMISSION}_limit'] == 12 diff --git a/kobo/apps/stripe/utils/subscription_limits.py b/kobo/apps/stripe/utils/subscription_limits.py index 49eca74107..b0ac678802 100644 --- a/kobo/apps/stripe/utils/subscription_limits.py +++ b/kobo/apps/stripe/utils/subscription_limits.py @@ -262,9 +262,7 @@ def get_paid_subscription_limits(organization_ids: list[str], **kwargs) -> Query mt_characters_limit=Coalesce( F(price_characters_key), F(product_characters_key) ), - llm_requests_limit=Coalesce( - F(price_requests_key), F(product_requests_key) - ), + llm_requests_limit=Coalesce(F(price_requests_key), F(product_requests_key)), sub_start_date=F('start_date'), product_type=F('items__price__product__metadata__product_type'), ) diff --git a/kpi/schema_extensions/v2/generic/schema.py b/kpi/schema_extensions/v2/generic/schema.py index 7ffe4cf9f3..1d4f5f8080 100644 --- a/kpi/schema_extensions/v2/generic/schema.py +++ b/kpi/schema_extensions/v2/generic/schema.py @@ -44,11 +44,11 @@ 'total_nlp_asr_seconds': build_basic_type(OpenApiTypes.INT), 'total_nlp_mt_characters': build_basic_type(OpenApiTypes.INT), 'total_nlp_llm_requests': build_basic_type(OpenApiTypes.INT), - } - + }, required=[ 'total_nlp_asr_seconds', 'total_nlp_mt_characters', + 'total_nlp_llm_requests', ], ) @@ -60,12 +60,14 @@ 'asr_seconds_all_time': build_basic_type(OpenApiTypes.INT), 'mt_characters_all_time': build_basic_type(OpenApiTypes.INT), 'llm_requests_all_time': build_basic_type(OpenApiTypes.INT), - } + }, required=[ 'asr_seconds_current_period', 'mt_characters_current_period', + 'llm_requests_current_period', 'asr_seconds_all_time', 'mt_characters_all_time', + 'llm_requests_all_time', ], ) diff --git a/kpi/utils/schema_extensions/url_builder.py b/kpi/utils/schema_extensions/url_builder.py index 03a398aa2f..230e484d78 100644 --- a/kpi/utils/schema_extensions/url_builder.py +++ b/kpi/utils/schema_extensions/url_builder.py @@ -19,7 +19,7 @@ def build_url_type(viewname: str, **kwargs) -> dict: """ DEV_DOMAIN_NAMES = [ 'http://kpi', - 'http://kf.kobo.local', + 'http://kf.kobo.local:8090', 'http://kf.kobo.localhost', ] diff --git a/static/openapi/schema_v2.json b/static/openapi/schema_v2.json index 8149c0cc18..2663dbf352 100644 --- a/static/openapi/schema_v2.json +++ b/static/openapi/schema_v2.json @@ -13540,6 +13540,7 @@ }, "required": [ "total_nlp_asr_seconds", + "total_nlp_llm_requests", "total_nlp_mt_characters" ] }, @@ -13558,6 +13559,7 @@ }, "required": [ "total_nlp_asr_seconds", + "total_nlp_llm_requests", "total_nlp_mt_characters" ] }, @@ -16314,10 +16316,14 @@ }, "total_nlp_mt_characters": { "type": "integer" + }, + "total_nlp_llm_requests": { + "type": "integer" } }, "required": [ "total_nlp_asr_seconds", + "total_nlp_llm_requests", "total_nlp_mt_characters" ] }, @@ -16329,10 +16335,14 @@ }, "total_nlp_mt_characters": { "type": "integer" + }, + "total_nlp_llm_requests": { + "type": "integer" } }, "required": [ "total_nlp_asr_seconds", + "total_nlp_llm_requests", "total_nlp_mt_characters" ] }, @@ -16487,6 +16497,8 @@ "required": [ "asr_seconds_all_time", "asr_seconds_current_period", + "llm_requests_all_time", + "llm_requests_current_period", "mt_characters_all_time", "mt_characters_current_period" ] @@ -19563,6 +19575,8 @@ "required": [ "asr_seconds_all_time", "asr_seconds_current_period", + "llm_requests_all_time", + "llm_requests_current_period", "mt_characters_all_time", "mt_characters_current_period" ] @@ -21022,4 +21036,4 @@ "description": "Languages, available permissions, other" } ] -} +} \ No newline at end of file diff --git a/static/openapi/schema_v2.yaml b/static/openapi/schema_v2.yaml index ba560a2242..7f67b44b4e 100644 --- a/static/openapi/schema_v2.yaml +++ b/static/openapi/schema_v2.yaml @@ -9787,8 +9787,11 @@ components: type: integer total_nlp_mt_characters: type: integer + total_nlp_llm_requests: + type: integer required: - total_nlp_asr_seconds + - total_nlp_llm_requests - total_nlp_mt_characters nlp_usage_all_time: type: object @@ -9797,8 +9800,11 @@ components: type: integer total_nlp_mt_characters: type: integer + total_nlp_llm_requests: + type: integer required: - total_nlp_asr_seconds + - total_nlp_llm_requests - total_nlp_mt_characters storage_bytes: type: integer @@ -11749,8 +11755,11 @@ components: type: integer total_nlp_mt_characters: type: integer + total_nlp_llm_requests: + type: integer required: - total_nlp_asr_seconds + - total_nlp_llm_requests - total_nlp_mt_characters nlp_usage_all_time: type: object @@ -11759,8 +11768,11 @@ components: type: integer total_nlp_mt_characters: type: integer + total_nlp_llm_requests: + type: integer required: - total_nlp_asr_seconds + - total_nlp_llm_requests - total_nlp_mt_characters storage_bytes: type: integer @@ -11871,9 +11883,13 @@ components: type: integer mt_characters_all_time: type: integer + llm_requests_all_time: + type: integer required: - asr_seconds_all_time - asr_seconds_current_period + - llm_requests_all_time + - llm_requests_current_period - mt_characters_all_time - mt_characters_current_period total_storage_bytes: @@ -14091,9 +14107,13 @@ components: type: integer mt_characters_all_time: type: integer + llm_requests_all_time: + type: integer required: - asr_seconds_all_time - asr_seconds_current_period + - llm_requests_all_time + - llm_requests_current_period - mt_characters_all_time - mt_characters_current_period total_storage_bytes: From 598a92011f0d3210be0d4d720758ae35c8d5a531 Mon Sep 17 00:00:00 2001 From: James Kiger Date: Wed, 5 Nov 2025 12:31:11 -0500 Subject: [PATCH 09/12] post-merge fixes --- ...anizationServiceUsageResponseTotalNlpUsage.ts | 2 -- .../models/serviceUsageResponseTotalNlpUsage.ts | 2 -- kpi/utils/schema_extensions/url_builder.py | 2 +- static/openapi/schema_v2.json | 16 ++++------------ static/openapi/schema_v2.yaml | 12 ++++-------- 5 files changed, 9 insertions(+), 25 deletions(-) diff --git a/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts b/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts index ba946ea430..e644d145a2 100644 --- a/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts +++ b/jsapp/js/api/models/organizationServiceUsageResponseTotalNlpUsage.ts @@ -14,9 +14,7 @@ export type OrganizationServiceUsageResponseTotalNlpUsage = { asr_seconds_current_period: number llm_requests_current_period: number mt_characters_current_period: number - llm_requests_current_period: number asr_seconds_all_time: number llm_requests_all_time: number mt_characters_all_time: number - llm_requests_all_time: number } diff --git a/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts b/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts index 07146c7bfa..f02d46af2b 100644 --- a/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts +++ b/jsapp/js/api/models/serviceUsageResponseTotalNlpUsage.ts @@ -14,9 +14,7 @@ export type ServiceUsageResponseTotalNlpUsage = { asr_seconds_current_period: number llm_requests_current_period: number mt_characters_current_period: number - llm_requests_current_period: number asr_seconds_all_time: number llm_requests_all_time: number mt_characters_all_time: number - llm_requests_all_time: number } diff --git a/kpi/utils/schema_extensions/url_builder.py b/kpi/utils/schema_extensions/url_builder.py index 230e484d78..03a398aa2f 100644 --- a/kpi/utils/schema_extensions/url_builder.py +++ b/kpi/utils/schema_extensions/url_builder.py @@ -19,7 +19,7 @@ def build_url_type(viewname: str, **kwargs) -> dict: """ DEV_DOMAIN_NAMES = [ 'http://kpi', - 'http://kf.kobo.local:8090', + 'http://kf.kobo.local', 'http://kf.kobo.localhost', ] diff --git a/static/openapi/schema_v2.json b/static/openapi/schema_v2.json index 981a35a799..cb136a3332 100644 --- a/static/openapi/schema_v2.json +++ b/static/openapi/schema_v2.json @@ -16484,9 +16484,6 @@ "mt_characters_current_period": { "type": "integer" }, - "llm_requests_current_period": { - "type": "integer" - }, "asr_seconds_all_time": { "type": "integer" }, @@ -16495,15 +16492,14 @@ }, "mt_characters_all_time": { "type": "integer" - }, - "llm_requests_all_time": { - "type": "integer" } }, "required": [ "asr_seconds_all_time", "asr_seconds_current_period", "llm_requests_all_time", + "llm_requests_all_time", + "llm_requests_current_period", "llm_requests_current_period", "mt_characters_all_time", "mt_characters_current_period" @@ -19572,9 +19568,6 @@ "mt_characters_current_period": { "type": "integer" }, - "llm_requests_current_period": { - "type": "integer" - }, "asr_seconds_all_time": { "type": "integer" }, @@ -19583,15 +19576,14 @@ }, "mt_characters_all_time": { "type": "integer" - }, - "llm_requests_all_time": { - "type": "integer" } }, "required": [ "asr_seconds_all_time", "asr_seconds_current_period", "llm_requests_all_time", + "llm_requests_all_time", + "llm_requests_current_period", "llm_requests_current_period", "mt_characters_all_time", "mt_characters_current_period" diff --git a/static/openapi/schema_v2.yaml b/static/openapi/schema_v2.yaml index 44355ad021..0a0b3dc821 100644 --- a/static/openapi/schema_v2.yaml +++ b/static/openapi/schema_v2.yaml @@ -11892,20 +11892,18 @@ components: type: integer mt_characters_current_period: type: integer - llm_requests_current_period: - type: integer asr_seconds_all_time: type: integer llm_requests_all_time: type: integer mt_characters_all_time: type: integer - llm_requests_all_time: - type: integer required: - asr_seconds_all_time - asr_seconds_current_period - llm_requests_all_time + - llm_requests_all_time + - llm_requests_current_period - llm_requests_current_period - mt_characters_all_time - mt_characters_current_period @@ -14123,20 +14121,18 @@ components: type: integer mt_characters_current_period: type: integer - llm_requests_current_period: - type: integer asr_seconds_all_time: type: integer llm_requests_all_time: type: integer mt_characters_all_time: type: integer - llm_requests_all_time: - type: integer required: - asr_seconds_all_time - asr_seconds_current_period - llm_requests_all_time + - llm_requests_all_time + - llm_requests_current_period - llm_requests_current_period - mt_characters_all_time - mt_characters_current_period From 210f09bd84c0804b105d56e3d27e164fcf5fda63 Mon Sep 17 00:00:00 2001 From: James Kiger Date: Wed, 5 Nov 2025 13:09:52 -0500 Subject: [PATCH 10/12] Remove temporary usage type enum --- kobo/apps/organizations/constants.py | 10 ---------- kobo/apps/stripe/models.py | 4 ++-- kobo/apps/stripe/tests/test_stripe_utils.py | 10 +++++----- kobo/apps/stripe/utils/subscription_limits.py | 10 +++++----- 4 files changed, 12 insertions(+), 22 deletions(-) diff --git a/kobo/apps/organizations/constants.py b/kobo/apps/organizations/constants.py index cc56855cfa..c446676cb1 100644 --- a/kobo/apps/organizations/constants.py +++ b/kobo/apps/organizations/constants.py @@ -1,16 +1,6 @@ from django.db import models -# Temporary const for code that iterates over usage types -# TODO: Remove when Stripe code has been updated to -# handle LLM usage type -class SupportedUsageType(models.TextChoices): - SUBMISSION = 'submission' - STORAGE_BYTES = 'storage_bytes' - MT_CHARACTERS = 'mt_characters' - ASR_SECONDS = 'asr_seconds' - - class UsageType(models.TextChoices): SUBMISSION = 'submission' STORAGE_BYTES = 'storage_bytes' diff --git a/kobo/apps/stripe/models.py b/kobo/apps/stripe/models.py index 0d45fae798..3a50fbba9e 100644 --- a/kobo/apps/stripe/models.py +++ b/kobo/apps/stripe/models.py @@ -9,7 +9,7 @@ from djstripe.models import Charge, Price, Subscription from kobo.apps.kobo_auth.shortcuts import User -from kobo.apps.organizations.constants import SupportedUsageType, UsageType +from kobo.apps.organizations.constants import UsageType from kobo.apps.organizations.models import Organization from kobo.apps.stripe.constants import ACTIVE_STRIPE_STATUSES from kobo.apps.stripe.utils.subscription_limits import get_default_add_on_limits @@ -331,6 +331,6 @@ class ExceededLimitCounter(models.Model): on_delete=models.CASCADE, ) days = models.PositiveSmallIntegerField(default=0) - limit_type = models.CharField(choices=SupportedUsageType.choices, max_length=20) + limit_type = models.CharField(choices=UsageType.choices, max_length=20) date_created = models.DateTimeField(auto_now_add=True) date_modified = models.DateTimeField(auto_now=True) diff --git a/kobo/apps/stripe/tests/test_stripe_utils.py b/kobo/apps/stripe/tests/test_stripe_utils.py index 7d0750556b..ffd0c0d8fa 100644 --- a/kobo/apps/stripe/tests/test_stripe_utils.py +++ b/kobo/apps/stripe/tests/test_stripe_utils.py @@ -14,7 +14,7 @@ from model_bakery import baker from kobo.apps.kobo_auth.shortcuts import User -from kobo.apps.organizations.constants import SupportedUsageType, UsageType +from kobo.apps.organizations.constants import UsageType from kobo.apps.organizations.models import Organization from kobo.apps.organizations.utils import get_billing_dates from kobo.apps.stripe.models import ExceededLimitCounter @@ -134,7 +134,7 @@ def test__prioritizes_price_metadata(self): self.organization, metadata=product_metadata, price_metadata=price_metadata ) limits = get_paid_subscription_limits([self.organization.id]).first() - for usage_type, _ in SupportedUsageType.choices: + for usage_type, _ in UsageType.choices: assert limits[f'{usage_type}_limit'] == '2' def test_get_subscription_limits_takes_most_recent_active_subscriptions(self): @@ -621,7 +621,7 @@ def test_check_exceeded_limit_adds_counters(self): ): self.add_submissions(count=2, asset=self.asset, username='someuser') self.add_nlp_trackers() - for usage_type, _ in SupportedUsageType.choices: + for usage_type, _ in UsageType.choices: check_exceeded_limit(self.someuser, usage_type) assert ( ExceededLimitCounter.objects.filter( @@ -632,7 +632,7 @@ def test_check_exceeded_limit_adds_counters(self): def test_check_exceeded_limit_updates_counters(self): today = timezone.now() - for usage_type, _ in SupportedUsageType.choices: + for usage_type, _ in UsageType.choices: baker.make( ExceededLimitCounter, user=self.anotheruser, @@ -649,7 +649,7 @@ def test_check_exceeded_limit_updates_counters(self): ): self.add_submissions(count=2, asset=self.asset, username='someuser') self.add_nlp_trackers() - for usage_type, _ in SupportedUsageType.choices: + for usage_type, _ in UsageType.choices: check_exceeded_limit(self.someuser, usage_type) counter = ExceededLimitCounter.objects.get( user_id=self.anotheruser.id, limit_type=usage_type diff --git a/kobo/apps/stripe/utils/subscription_limits.py b/kobo/apps/stripe/utils/subscription_limits.py index d6dccebb0d..b0ac678802 100644 --- a/kobo/apps/stripe/utils/subscription_limits.py +++ b/kobo/apps/stripe/utils/subscription_limits.py @@ -6,7 +6,7 @@ from django.db.models import F, Max, Q, QuerySet, Window from django.db.models.functions import Coalesce -from kobo.apps.organizations.constants import SupportedUsageType, UsageType +from kobo.apps.organizations.constants import UsageType from kobo.apps.organizations.models import Organization, OrganizationUser from kobo.apps.organizations.types import UsageLimits from kobo.apps.stripe.constants import ACTIVE_STRIPE_STATUSES @@ -115,7 +115,7 @@ def get_organizations_subscription_limits( if row['product_type'] == 'plan': row_limits = { f'{usage_type}_limit': row[f'{usage_type}_limit'] - for usage_type, _ in SupportedUsageType.choices + for usage_type, _ in UsageType.choices } elif row['product_type'] == 'addon': row_limits['addon_storage_limit'] = row[f'{UsageType.STORAGE_BYTES}_limit'] @@ -139,7 +139,7 @@ def get_organizations_subscription_limits( .first() ) or {} default_plan_limits = {} - for usage_type, _ in SupportedUsageType.choices: + for usage_type, _ in UsageType.choices: limit_key = f'{usage_type}_limit' default_limit = default_plan.get(limit_key) if default_limit is None: @@ -150,7 +150,7 @@ def get_organizations_subscription_limits( results = {} for org_id in all_org_ids: all_org_limits = {} - for usage_type, _ in SupportedUsageType.choices: + for usage_type, _ in UsageType.choices: plan_limit = subscription_limits_by_org_id.get(org_id, {}).get( f'{usage_type}_limit' ) @@ -208,7 +208,7 @@ def get_organizations_effective_limits( PlanAddOn = apps.get_model('stripe', 'PlanAddOn') # noqa addon_limits = PlanAddOn.get_organizations_totals(organizations=organizations) for org_id, limits in effective_limits.items(): - for usage_type, _ in SupportedUsageType.choices: + for usage_type, _ in UsageType.choices: addon = addon_limits.get(org_id, {}).get(f'total_{usage_type}_limit', 0) limits[f'{usage_type}_limit'] += addon return effective_limits From 6d99d31213b44cdb87f4f8c6ee51a29f1733aaa8 Mon Sep 17 00:00:00 2001 From: James Kiger Date: Wed, 5 Nov 2025 14:10:26 -0500 Subject: [PATCH 11/12] Remove duplicate keys --- kpi/schema_extensions/v2/generic/schema.py | 4 ---- static/openapi/schema_v2.json | 4 ---- static/openapi/schema_v2.yaml | 4 ---- 3 files changed, 12 deletions(-) diff --git a/kpi/schema_extensions/v2/generic/schema.py b/kpi/schema_extensions/v2/generic/schema.py index e8f87eba13..55089ac762 100644 --- a/kpi/schema_extensions/v2/generic/schema.py +++ b/kpi/schema_extensions/v2/generic/schema.py @@ -57,21 +57,17 @@ 'asr_seconds_current_period': build_basic_type(OpenApiTypes.INT), 'llm_requests_current_period': build_basic_type(OpenApiTypes.INT), 'mt_characters_current_period': build_basic_type(OpenApiTypes.INT), - 'llm_requests_current_period': build_basic_type(OpenApiTypes.INT), 'asr_seconds_all_time': build_basic_type(OpenApiTypes.INT), 'llm_requests_all_time': build_basic_type(OpenApiTypes.INT), 'mt_characters_all_time': build_basic_type(OpenApiTypes.INT), - 'llm_requests_all_time': build_basic_type(OpenApiTypes.INT), }, required=[ 'asr_seconds_current_period', 'llm_requests_current_period', 'mt_characters_current_period', - 'llm_requests_current_period', 'asr_seconds_all_time', 'llm_requests_all_time', 'mt_characters_all_time', - 'llm_requests_all_time', ], ) diff --git a/static/openapi/schema_v2.json b/static/openapi/schema_v2.json index cb136a3332..03400f2038 100644 --- a/static/openapi/schema_v2.json +++ b/static/openapi/schema_v2.json @@ -16498,8 +16498,6 @@ "asr_seconds_all_time", "asr_seconds_current_period", "llm_requests_all_time", - "llm_requests_all_time", - "llm_requests_current_period", "llm_requests_current_period", "mt_characters_all_time", "mt_characters_current_period" @@ -19582,8 +19580,6 @@ "asr_seconds_all_time", "asr_seconds_current_period", "llm_requests_all_time", - "llm_requests_all_time", - "llm_requests_current_period", "llm_requests_current_period", "mt_characters_all_time", "mt_characters_current_period" diff --git a/static/openapi/schema_v2.yaml b/static/openapi/schema_v2.yaml index 0a0b3dc821..11879ecb52 100644 --- a/static/openapi/schema_v2.yaml +++ b/static/openapi/schema_v2.yaml @@ -11902,8 +11902,6 @@ components: - asr_seconds_all_time - asr_seconds_current_period - llm_requests_all_time - - llm_requests_all_time - - llm_requests_current_period - llm_requests_current_period - mt_characters_all_time - mt_characters_current_period @@ -14131,8 +14129,6 @@ components: - asr_seconds_all_time - asr_seconds_current_period - llm_requests_all_time - - llm_requests_all_time - - llm_requests_current_period - llm_requests_current_period - mt_characters_all_time - mt_characters_current_period From 892cde960192dd97407091ca5ffc9000da33ce7c Mon Sep 17 00:00:00 2001 From: James Kiger Date: Thu, 6 Nov 2025 06:38:48 -0500 Subject: [PATCH 12/12] Small fixes --- .../apps/api/tests/viewsets/test_xform_submission_api.py | 3 ++- kpi/serializers/v2/service_usage.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/kobo/apps/openrosa/apps/api/tests/viewsets/test_xform_submission_api.py b/kobo/apps/openrosa/apps/api/tests/viewsets/test_xform_submission_api.py index 1b88cd6632..2196ad7b1a 100644 --- a/kobo/apps/openrosa/apps/api/tests/viewsets/test_xform_submission_api.py +++ b/kobo/apps/openrosa/apps/api/tests/viewsets/test_xform_submission_api.py @@ -78,7 +78,8 @@ def test_query_counts(self): # USAGE_LIMIT_ENFORCEMENT variable. But we use caching # so should find a way to keep that out of this count if settings.STRIPE_ENABLED: - expected_queries = FuzzyInt(82, 90) + # But because of cache, sometimes goes down to 62 + expected_queries = FuzzyInt(62, 90) with self.assertNumQueries(expected_queries): self.view(request) diff --git a/kpi/serializers/v2/service_usage.py b/kpi/serializers/v2/service_usage.py index a7fce7821a..0d4e8e4e9b 100644 --- a/kpi/serializers/v2/service_usage.py +++ b/kpi/serializers/v2/service_usage.py @@ -66,6 +66,7 @@ def _get_nlp_tracking_data(self, asset, start_date=None): if not asset.has_deployment: return { 'total_nlp_asr_seconds': 0, + 'total_nlp_llm_requests': 0, 'total_nlp_mt_characters': 0, } return OpenRosaDeploymentBackend.nlp_tracking_data(