Revert "[Web] Enable Quota System for AI Features (#31544)" (#31767)

This reverts commit 17763447965aae5777053b783d2601517bfe6b12.

GitOrigin-RevId: f6589bdbf0ac7e71313739e3e3f4fb5bedd48c22
This commit is contained in:
Jimmy Domagala-Tang
2026-02-23 11:41:51 -05:00
committed by Copybot
parent 92463fb3e2
commit 892047fcf6
18 changed files with 14 additions and 112 deletions
@@ -15,14 +15,7 @@ async function _getInstitutionsAddons(userId) {
const hasAssistBundle = affiliates.some(
affiliate => affiliate?.institution?.writefullCommonsAccount === true
)
// todo: seperate quota value depending on source of entitlement if needed
// todo: quota clean-up: remove aiErrorAssistant once migration finishes
const bundleFeatures = {
aiUsageQuota: Settings.writefull.quotaTierGranted,
aiErrorAssistant: true,
}
return hasAssistBundle ? bundleFeatures : {}
return hasAssistBundle ? { aiErrorAssistant: true } : {}
}
async function getInstitutionsFeatures(userId) {
@@ -952,8 +952,6 @@ const _ProjectController = {
symbolPaletteAvailable: Features.hasFeature('symbol-palette'),
userRestrictions: Array.from(req.userRestrictions || []),
showAiFeatures: aiFeaturesAllowed && !aiFeaturesDisabled,
onAiFreeTrial:
user.features?.aiUsageQuota === Settings.aiFeatures?.freeTrialQuota,
detachRole,
metadata: { viewport: false },
showUpgradePrompt,
@@ -510,10 +510,8 @@ async function projectListPage(req, res, next) {
logger.error({ err: error }, 'Failed to get individual subscription')
}
const aiBlocked =
Features.hasFeature('saas') && !(await _canUseAIAssist(user))
const hasAiAssist =
Features.hasFeature('saas') && (await _userHasAIAssist(user))
const aiBlocked = !(await _canUseAIAssist(user))
const hasAiAssist = await _userHasAIAssist(user)
await SplitTestHandler.promises.getAssignment(
req,
@@ -901,13 +899,11 @@ function _hasActiveFilter(filters) {
)
}
// todo: quota clean-up: rename function and vars
async function _userHasAIAssist(user) {
// Check if the user has a non free trial version of our AI features
if (user.features?.aiUsageQuota === Settings.aiFeatures.unlimitedQuota) {
// Check if the user has AI Assist enabled via Overleaf
if (user.features?.aiErrorAssistant) {
return true
}
// Check if the user has AI Assist enabled via Writefull
const { isPremium: hasAiAssistViaWritefull } =
await UserGetter.promises.getWritefullData(user._id)
@@ -922,7 +918,6 @@ async function _userHasAIAssist(user) {
// It does NOT determine if the user has AI Assist enabled
async function _canUseAIAssist(user) {
// Check if the assistant has been manually disabled by the user
// post https://github.com/overleaf/internal/pull/31273 we can rely on user.aiFeatures being populated
if (user.aiFeatures?.enabled === false) {
return false
}
@@ -306,20 +306,6 @@ async function getOneTimeAssignment(splitTestName) {
}
}
/**
* Checks if a feature flag is enabled for a specific user
*
* Retrieves the feature flag assignment for a user and determines if the assigned variant is 'enabled'
*
* @param {string} userId - The ID of the user to check the feature flag for
* @param {string} splitTestName - The unique name of the feature flag
* @returns {Promise<boolean>} True if the user's assigned variant is 'enabled', false otherwise
*/
async function featureFlagEnabledForUser(userId, splitTestName) {
const { variant } = await getAssignmentForUser(userId, splitTestName)
return variant === 'enabled'
}
/**
* Returns an array of valid variant names for the given split test, including default
*
@@ -816,7 +802,6 @@ export default {
getPercentile,
getAssignment: callbackify(getAssignment),
getAssignmentForUser: callbackify(getAssignmentForUser),
featureFlagEnabledForUser: callbackify(featureFlagEnabledForUser),
getOneTimeAssignment: callbackify(getOneTimeAssignment),
getActiveAssignmentsForUser: callbackify(getActiveAssignmentsForUser),
hasUserBeenAssignedToVariant: callbackify(hasUserBeenAssignedToVariant),
@@ -825,7 +810,6 @@ export default {
promises: {
getAssignment,
getAssignmentForUser,
featureFlagEnabledForUser,
getOneTimeAssignment,
getActiveAssignmentsForUser,
hasUserBeenAssignedToVariant,
@@ -38,15 +38,6 @@ function mergeFeatures(featuresA, featuresB) {
features.compileTimeout || 0,
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
}
} else {
// Boolean keys, true is better
features[key] = features[key] || featuresB[key]
@@ -100,10 +100,7 @@ async function refreshFeatures(userId, reason) {
},
json: {
userOverleafId: userId,
// todo: quota clean-up: collab with writefull to rename this, and check if still needed
hasAiAssist:
newFeatures.aiErrorAssistant ||
newFeatures.aiUsageQuota === Settings.aiFeatures.unlimitedQuota,
hasAiAssist: newFeatures.aiErrorAssistant,
},
method: 'POST',
}
@@ -195,9 +192,6 @@ async function _getIndividualFeatures(userId) {
featureSets.push(_subscriptionToFeatures(subscription))
}
// todo: quota clean-up - remove
// if they are in the quota split test, we no longer look at the add-on, since every plan will now have the same quota
// standalone plan will receive correct state since their plan will provide the correct quota
featureSets.push(_aiAddOnFeatures(subscription))
return _.reduce(featureSets, FeaturesHelper.mergeFeatures, {})
}
@@ -243,14 +237,9 @@ function _subscriptionToFeatures(subscription) {
}
}
// todo: quota clean-up: remove post split test
function _aiAddOnFeatures(subscription) {
if (subscription?.addOns?.some(addOn => addOn.addOnCode === AI_ADD_ON_CODE)) {
return {
// allow both naming systems to work
aiErrorAssistant: true,
aiUsageQuota: Settings.aiFeatures.unlimitedQuota,
}
return { aiErrorAssistant: true }
} else {
return {}
}
@@ -87,6 +87,7 @@ export default class FeatureUsageRateLimiter {
upsert: true,
}
).exec()
const featureUsage = featureUsages.features?.[this.featureName] ?? {}
setRateLimitHeaders(res, featureUsage, allowance)
this._checkRateLimit(featureUsage, allowance)
@@ -184,6 +185,7 @@ function setRateLimitHeaders(res, featureUsage, allowance) {
const usage = featureUsage.usage ?? 0
const refreshEpoch = periodStart.getTime() + PERIOD_IN_MILLISECONDS
const secondsTillReset = Math.ceil((refreshEpoch - Date.now()) / 1000)
if (!res.headersSent) {
res.set('RateLimit-Limit', String(allowance))
res.set('RateLimit-Remaining', String(Math.max(0, allowance - usage)))
-3
View File
@@ -143,7 +143,6 @@ export const UserSchema = new Schema(
type: Boolean,
default: false,
},
aiUsageQuota: { type: String, default: 'basic' },
},
featuresOverrides: [
{
@@ -156,9 +155,7 @@ export const UserSchema = new Schema(
expiresAt: { type: Date },
note: { type: String },
features: {
// todo: quota clean-up: remove aiErrorAssistant
aiErrorAssistant: { type: Boolean },
aiUsageQuota: { type: String },
collaborators: { type: Number },
versioning: { type: Boolean },
dropbox: { type: Boolean },
@@ -25,7 +25,6 @@ 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-detachRole" data-type="string" content=detachRole)
meta(name="ol-imageNames" data-type="json" content=imageNames)
meta(name="ol-languages" data-type="json" content=languages)
-5
View File
@@ -419,11 +419,6 @@ module.exports = {
personal: defaultFeatures,
},
aiFeatures: {
freeTrialQuota: 'basic',
unlimitedQuota: 'unlimited',
},
groupPlanModalOptions: {
plan_codes: [],
currencies: [],
@@ -11,12 +11,9 @@ import { useUserContext } from './user-context'
import { useReceiveUser } from '../hooks/user-channel/use-receive-user'
import { getJSON } from '@/infrastructure/fetch-json'
import { useEditorContext } from './editor-context'
import getMeta from '@/utils/meta'
export const UserFeaturesContext = createContext<User['features']>(undefined)
const onAiFreeTrial = getMeta('ol-onAiFreeTrial')
export const UserFeaturesProvider: FC<React.PropsWithChildren> = ({
children,
}) => {
@@ -34,12 +31,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 alreadyPremium =
features?.aiErrorAssistant === isPremium ||
hasPremiumQuota === isPremium
if (alreadyPremium) {
if (features?.aiErrorAssistant === isPremium) {
// the user is premium on writefull and has the AI assist, no need to refresh the features
return
}
-1
View File
@@ -213,7 +213,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
@@ -115,7 +115,6 @@ module.exports = {
compileGroup: 'standard',
trackChanges: false,
symbolPalette: false,
aiUsageQuota: 'basic',
aiErrorAssistant: false,
},
personal: {
@@ -133,7 +132,6 @@ module.exports = {
compileGroup: 'standard',
trackChanges: false,
symbolPalette: false,
aiUsageQuota: 'basic',
aiErrorAssistant: false,
},
collaborator: {
@@ -151,7 +149,6 @@ module.exports = {
compileGroup: 'priority',
trackChanges: true,
symbolPalette: true,
aiUsageQuota: 'basic',
aiErrorAssistant: false,
},
professional: {
@@ -169,7 +166,6 @@ module.exports = {
compileGroup: 'priority',
trackChanges: true,
symbolPalette: true,
aiUsageQuota: 'basic',
aiErrorAssistant: false,
},
}),
@@ -30,13 +30,6 @@ describe('InstitutionsFeatures', function () {
default: {
institutionPlanCode: ctx.institutionPlanCode,
overleaf: {},
writefull: {
quotaTierGranted: 'unlimited',
},
aiFeatures: {
freeTrialQuota: 'basic',
unlimitedQuota: 'unlimited',
},
},
}))
@@ -59,18 +52,10 @@ describe('InstitutionsFeatures', function () {
}
ctx.testFeatures = { features: { institution: 'all' } }
ctx.testFeaturesWithAiAddon = {
features: {
institution: 'all',
aiUsageQuota: 'unlimited',
aiErrorAssistant: true,
},
features: { institution: 'all', aiErrorAssistant: true },
}
ctx.testFeaturesWithNoAddon = {
features: {
institution: 'all',
aiUsageQuota: 'basic',
aiErrorAssistant: false,
},
features: { institution: 'all', aiErrorAssistant: false },
}
})
@@ -40,7 +40,6 @@ describe('ProjectListController', function () {
theme: 'textmate',
mode: 'none',
},
aiFeatures: { enabled: false },
}
ctx.users = {
'user-1': {
@@ -74,11 +74,6 @@ describe('FeaturesUpdater', function () {
},
writefull: {
overleafApiUrl: 'https://www.writefull.com',
quotaTierGranted: 'unlimited',
},
aiFeatures: {
freeTrialQuota: 'basic',
unlimitedQuota: 'unlimited',
},
}
@@ -328,7 +323,6 @@ describe('FeaturesUpdater', function () {
default: 'features',
individual: 'features',
aiErrorAssistant: true,
aiUsageQuota: 'unlimited',
})
})
})
@@ -347,7 +341,6 @@ describe('FeaturesUpdater', function () {
expect(features).to.deep.equal({
default: 'features',
aiErrorAssistant: true,
aiUsageQuota: 'unlimited',
})
})
})
@@ -66,10 +66,6 @@ describe('UserGetter', function () {
vi.doMock('@overleaf/settings', () => ({
default: (ctx.settings = {
reconfirmNotificationDays: 14,
aiFeatures: {
freeTrialQuota: 'basic',
unlimitedQuota: 'unlimited',
},
}),
}))
@@ -1316,8 +1312,8 @@ describe('UserGetter', function () {
it('should take into account features overrides from modules', async function (ctx) {
// this case occurs when the user has bought the ai bundle on WF, which should include our error assistant
const bundleFeatures = { aiUsageQuota: 'unlimited' }
ctx.fakeUser.features = { aiUsageQuota: 'basic' }
const bundleFeatures = { aiErrorAssistant: true }
ctx.fakeUser.features = { aiErrorAssistant: false }
ctx.Modules.promises.hooks.fire = sinon.stub().resolves([bundleFeatures])
const features = await ctx.UserGetter.promises.getUserFeatures(
ctx.fakeUser._id
-1
View File
@@ -10,7 +10,6 @@ export type UserId = Brand<string, 'UserId'>
export type Features = {
aiErrorAssistant?: boolean
aiUsageQuota?: string
collaborators?: number
compileGroup?: 'standard' | 'priority'
compileTimeout?: number