Adding More Ai Quota Tiers (#32128)

* feat: adding tiers for free and standard

* feat: updating feature calculation to account for more quota tiers

* feat: rename freeTrialQuota to freeQuota

* feat: add hasAiFreeTier and hasUnlimitedAi to editor, block free tier from using workbench

* fix: updating tests

* fix: updating ordering precedence for quota tiers

* feat: bump unlimited ai fair usage to 300 uses

* fix: update workbench quota usage for unlimited plans

* feat: bump features version for ai quota split

* feat: popover should only show for relevant users on workbench, and adding upgrade notification to ineligible users

GitOrigin-RevId: e3ef38797f267677cad51d7273272623027ca330
This commit is contained in:
Jimmy Domagala-Tang
2026-03-25 21:12:23 +09:00
committed by Copybot
parent e4e4193d55
commit c87fd5c42e
14 changed files with 76 additions and 21 deletions
@@ -964,6 +964,12 @@ const _ProjectController = {
showAiFeatures,
onAiFreeTrial:
fullFeatureSet?.aiUsageQuota === Settings.aiFeatures?.freeTrialQuota,
// default to free tier if they dont have a quota
hasAiFreeTier:
fullFeatureSet?.aiUsageQuota === Settings.aiFeatures?.freeQuota ||
!fullFeatureSet?.aiUsageQuota,
hasUnlimitedAi:
fullFeatureSet?.aiUsageQuota === Settings.aiFeatures?.unlimitedQuota,
detachRole,
metadata: { viewport: false },
showUpgradePrompt,
@@ -39,14 +39,12 @@ function mergeFeatures(featuresA, featuresB) {
featuresB.compileTimeout || 0
)
} else if (key === 'aiUsageQuota') {
if (
features.aiUsageQuota === Settings.aiFeatures.unlimitedQuota ||
featuresB.aiUsageQuota === Settings.aiFeatures.unlimitedQuota
) {
features.aiUsageQuota = Settings.aiFeatures.unlimitedQuota
} else {
features.aiUsageQuota = Settings.aiFeatures.freeTrialQuota
}
// later entries have higher precedence
const QUOTA_TIER_LIST = ['free', 'basic', 'standard', 'unlimited']
const quotaA = QUOTA_TIER_LIST.indexOf(features.aiUsageQuota)
const quotaB = QUOTA_TIER_LIST.indexOf(featuresB.aiUsageQuota)
features.aiUsageQuota =
quotaA > quotaB ? features.aiUsageQuota : featuresB.aiUsageQuota
} else {
// Boolean keys, true is better
features[key] = features[key] || featuresB[key]
@@ -30,7 +30,7 @@ class AiFeatureUsageRateLimiter extends FeatureUsageRateLimiter {
if (inQuotaSplitTest) {
const wfQuota = user.writefull?.isPremium
? Settings.writefull.quotaTierGranted
: Settings.aiFeatures.freeTrialQuota
: Settings.aiFeatures.freeQuota
const mergedFeatures = FeaturesHelper.mergeFeatures(user.features, {
aiUsageQuota: wfQuota,
})
@@ -25,7 +25,8 @@ meta(name="ol-debugPdfDetach" data-type="boolean" content=debugPdfDetach)
meta(name="ol-showSymbolPalette" data-type="boolean" content=showSymbolPalette)
meta(name="ol-symbolPaletteAvailable" data-type="boolean" content=symbolPaletteAvailable)
meta(name="ol-showAiFeatures" data-type="boolean" content=showAiFeatures)
meta(name="ol-onAiFreeTrial" data-type="boolean" content=onAiFreeTrial)
meta(name="ol-hasUnlimitedAi" data-type="boolean" content=hasUnlimitedAi)
meta(name="ol-hasAiFreeTier" data-type="boolean" content=hasAiFreeTier)
meta(name="ol-detachRole" data-type="string" content=detachRole)
meta(name="ol-imageNames" data-type="json" content=imageNames)
meta(name="ol-languages" data-type="json" content=languages)
+12 -1
View File
@@ -425,10 +425,21 @@ module.exports = {
},
aiFeatures: {
freeTrialQuota: 'basic',
freeQuota: 'free',
standardQuota: 'standard',
basicQuota: 'basic',
unlimitedQuota: 'unlimited',
},
quotaGrants: {
ai: {
free: 0,
basic: 0,
standard: 0,
unlimited: 0,
},
},
groupPlanModalOptions: {
plan_codes: [],
currencies: [],
@@ -710,6 +710,7 @@
"get_most_subscription_by_checking_overleaf_ai_writefull": "",
"get_real_time_track_changes": "",
"get_unlimited_ai": "",
"get_your_hands_on_the_ultimate_research_writing_ai_assistant": "",
"git": "",
"git_authentication_token": "",
"git_authentication_token_create_modal_info_1": "",
@@ -2139,6 +2140,7 @@
"upgrade_to_add_more_collaborators_and_access_collaboration_features": "",
"upgrade_to_add_more_collaborators_and_more": "",
"upgrade_to_get_feature": "",
"upgrade_to_get_started": "",
"upgrade_to_review": "",
"upgrade_your_subscription": "",
"upload": "",
@@ -7,7 +7,7 @@ import { formatSecondsToHoursAndMinutes } from '@/shared/utils/time'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import getMeta from '@/utils/meta'
const onAiFreeTrial = getMeta('ol-onAiFreeTrial')
const hasUnlimitedAi = getMeta('ol-hasUnlimitedAi')
type aiFeatureLocations = 'errorAssist' | 'workbench'
@@ -33,8 +33,8 @@ function AiPaywallNotification({
return null
}
// todo: quota clean-up: remove once we are transitioned off aiErrorAssistant naming and replace with just !onAiFreeTrial, also remove null FF check
const hasAddOn = !onAiFreeTrial || Boolean(features?.aiErrorAssistant)
// todo: quota clean-up: remove once we are transitioned off aiErrorAssistant naming and replace with just hasUnlimitedAi, also remove null FF check
const hasAddOn = hasUnlimitedAi || Boolean(features?.aiErrorAssistant)
// error assist only needs usage quota
const canUseErrorAssist = hasSuggestionsLeft
@@ -15,7 +15,7 @@ import getMeta from '@/utils/meta'
export const UserFeaturesContext = createContext<User['features']>(undefined)
const onAiFreeTrial = getMeta('ol-onAiFreeTrial')
const hasUnlimitedAi = getMeta('ol-hasUnlimitedAi')
export const UserFeaturesProvider: FC<React.PropsWithChildren> = ({
children,
@@ -35,7 +35,7 @@ export const UserFeaturesProvider: FC<React.PropsWithChildren> = ({
useEffect(() => {
const listener = async ({ isPremium }: { isPremium: boolean }) => {
// todo: quota clean-up: remove once we are transitioned off aiErrorAssistant naming
const hasPremiumQuota = !onAiFreeTrial
const hasPremiumQuota = hasUnlimitedAi
const alreadyPremium =
features?.aiErrorAssistant === isPremium ||
hasPremiumQuota === isPremium
+2 -1
View File
@@ -152,6 +152,7 @@ export interface Meta {
'ol-groupSubscriptionsPendingEnrollment': PendingGroupSubscriptionEnrollment[]
'ol-groupsAndEnterpriseBannerVariant': GroupsAndEnterpriseBannerVariant
'ol-hasAiAssistViaWritefull': boolean
'ol-hasAiFreeTier': boolean
'ol-hasGroupSSOFeature': boolean
'ol-hasIndividualPaidSubscription': boolean
'ol-hasManagedUsersFeature': boolean
@@ -160,6 +161,7 @@ export interface Meta {
'ol-hasSplitTestWriteAccess': boolean
'ol-hasSubscription': boolean
'ol-hasTrackChangesFeature': boolean
'ol-hasUnlimitedAi': boolean
'ol-hasWriteAccess': boolean
'ol-hideLinkingWidgets': boolean // CI only
'ol-historyBlobStats': {
@@ -221,7 +223,6 @@ export interface Meta {
'ol-notificationsInstitution': InstitutionType[]
'ol-oauthProviders': OAuthProviders
'ol-odcData': OnboardingFormData
'ol-onAiFreeTrial': boolean
'ol-otMigrationStage': number
'ol-overallThemes': OverallThemeMeta[]
'ol-ownerIsManaged': boolean
+2
View File
@@ -913,6 +913,7 @@
"get_real_time_track_changes": "Get real-time track changes",
"get_the_best_overleaf_experience": "Get the best Overleaf experience",
"get_unlimited_ai": "Get unlimited use of AI features",
"get_your_hands_on_the_ultimate_research_writing_ai_assistant": "Get your hands on the ultimate research writing AI assistant.",
"git": "Git",
"git_authentication_token": "Git authentication token",
"git_authentication_token_create_modal_info_1": "This is your Git authentication token. You should enter this when prompted for a password.",
@@ -2691,6 +2692,7 @@
"upgrade_to_add_more_collaborators_and_more": "Upgrade to add more collaborators and access features like Overleaf AI, track changes, and full project history.",
"upgrade_to_get_feature": "Upgrade to get __feature__, plus:",
"upgrade_to_get_more_from_overleaf": "Upgrade to get more from Overleaf",
"upgrade_to_get_started": "Upgrade to get started",
"upgrade_to_review": "Upgrade to Review",
"upgrade_your_subscription": "Upgrade your subscription",
"upload": "Upload",
@@ -34,9 +34,19 @@ describe('InstitutionsFeatures', function () {
quotaTierGranted: 'unlimited',
},
aiFeatures: {
freeTrialQuota: 'basic',
freeQuota: 'free',
standardQuota: 'standard',
basicQuota: 'basic',
unlimitedQuota: 'unlimited',
},
quotaGrants: {
ai: {
free: 5,
basic: 5,
standard: 10,
unlimited: 200,
},
},
},
}))
@@ -77,9 +77,19 @@ describe('FeaturesUpdater', function () {
quotaTierGranted: 'unlimited',
},
aiFeatures: {
freeTrialQuota: 'basic',
freeQuota: 'free',
standardQuota: 'standard',
basicQuota: 'basic',
unlimitedQuota: 'unlimited',
},
quotaGrants: {
ai: {
free: 5,
basic: 5,
standard: 10,
unlimited: 200,
},
},
}
ctx.ReferalFeatures = {
@@ -67,9 +67,19 @@ describe('UserGetter', function () {
default: (ctx.settings = {
reconfirmNotificationDays: 14,
aiFeatures: {
freeTrialQuota: 'basic',
freeQuota: 'free',
standardQuota: 'standard',
basicQuota: 'basic',
unlimitedQuota: 'unlimited',
},
quotaGrants: {
ai: {
free: 5,
basic: 5,
standard: 10,
unlimited: 200,
},
},
}),
}))
@@ -61,12 +61,16 @@ describe('AiFeatureUsageRateLimiter', function () {
quotaTierGranted: 'unlimited',
},
aiFeatures: {
freeTrialQuota: 'basic',
freeQuota: 'free',
standardQuota: 'standard',
basicQuota: 'basic',
unlimitedQuota: 'unlimited',
},
quotaGrants: {
ai: {
free: 5,
basic: 5,
standard: 10,
unlimited: 200,
},
},