set_user_ai_usage.mjs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. // Set a user's aiFeatureUsage or aiWorkbench (token) usage to a specific value.
  2. //
  3. // Mirrors the period-reset behaviour of:
  4. // app/src/infrastructure/rate-limiters/AiFeatureUsageRateLimiter.mjs
  5. // app/src/infrastructure/rate-limiters/TokenUsageRateLimiter.mjs
  6. // i.e. if the existing period (24h from periodStart) has expired, periodStart
  7. // is reset to NOW before the new usage value is written. Otherwise periodStart
  8. // is preserved.
  9. //
  10. // Usage:
  11. //
  12. // node scripts/set_user_ai_usage.mjs \
  13. // --user-id <userId> \
  14. // --feature <aiFeatureUsage|aiWorkbench> \
  15. // --usage <number> \
  16. // [--commit]
  17. //
  18. // Or, to put the user one use below their tier's quota:
  19. //
  20. // node scripts/set_user_ai_usage.mjs \
  21. // --user-id <userId> \
  22. // --edge <free|basic|standard|unlimited|token> \
  23. // [--commit]
  24. //
  25. // Without --commit the script is a dry run.
  26. import minimist from 'minimist'
  27. import {
  28. db,
  29. ObjectId,
  30. READ_PREFERENCE_PRIMARY,
  31. } from '../app/src/infrastructure/mongodb.mjs'
  32. import { UserFeatureUsage } from '../app/src/models/UserFeatureUsage.mjs'
  33. import { scriptRunner } from './lib/ScriptRunner.mjs'
  34. const VALID_FEATURES = ['aiFeatureUsage', 'aiWorkbench']
  35. const PERIOD_HOURS = 24
  36. // Each value puts the user 1 below the limit for that tier.
  37. const EDGE_PRESETS = {
  38. free: { feature: 'aiFeatureUsage', usage: 4 },
  39. basic: { feature: 'aiFeatureUsage', usage: 4 },
  40. standard: { feature: 'aiFeatureUsage', usage: 9 },
  41. unlimited: { feature: 'aiFeatureUsage', usage: 299 },
  42. token: { feature: 'aiWorkbench', usage: 7_999_999 },
  43. }
  44. const argv = minimist(process.argv.slice(2), {
  45. string: ['user-id', 'feature', 'edge'],
  46. boolean: ['commit', 'help'],
  47. alias: { 'user-id': 'userId', h: 'help' },
  48. })
  49. const HELP_TEXT = `\
  50. Set a user's AI usage (aiFeatureUsage) or token usage (aiWorkbench) to a
  51. specific value, mirroring the 24h period-reset behaviour of the production
  52. rate limiters. Intended for staging QA of the Shared AI Quota system —
  53. paywalls, near-quota warnings, period-reset behaviour, and token-limit
  54. headers — without grinding a real account through the UI.
  55. Usage:
  56. node scripts/set_user_ai_usage.mjs --user-id <id> [--commit] (
  57. --feature <aiFeatureUsage|aiWorkbench> --usage <number>
  58. | --edge <free|basic|standard|unlimited|token>
  59. )
  60. Options:
  61. --user-id <id> Target user's Mongo _id (required).
  62. --feature <name> aiFeatureUsage (use count) or aiWorkbench (tokens).
  63. --usage <number> Non-negative number to set features.<feature>.usage to.
  64. --edge <tier> Shortcut: set the user 1 below their tier's limit.
  65. Implies --feature and --usage; cannot be combined with
  66. either. Tiers:
  67. free aiFeatureUsage = 4
  68. basic aiFeatureUsage = 4
  69. standard aiFeatureUsage = 9
  70. unlimited aiFeatureUsage = 299
  71. token aiWorkbench = 7,999,999
  72. --commit Apply the write. Without this flag the script is a
  73. dry run and only prints the current value.
  74. -h, --help Show this help and exit.
  75. Examples:
  76. # Dry run — show what's currently recorded for a user, no write
  77. node scripts/set_user_ai_usage.mjs \\
  78. --user-id 5f9a2c8e1b3d4f0012abcd34 \\
  79. --feature aiFeatureUsage --usage 4
  80. # Put a Free user one use away from the AI paywall
  81. node scripts/set_user_ai_usage.mjs \\
  82. --user-id 5f9a2c8e1b3d4f0012abcd34 \\
  83. --edge free --commit
  84. # Put a Standard user one use away from the AI paywall
  85. node scripts/set_user_ai_usage.mjs \\
  86. --user-id 5f9a2c8e1b3d4f0012abcd34 \\
  87. --edge standard --commit
  88. # Put a Workbench user one token below the token limit
  89. node scripts/set_user_ai_usage.mjs \\
  90. --user-id 5f9a2c8e1b3d4f0012abcd34 \\
  91. --edge token --commit
  92. # Reset a user's AI usage back to 0
  93. node scripts/set_user_ai_usage.mjs \\
  94. --user-id 5f9a2c8e1b3d4f0012abcd34 \\
  95. --feature aiFeatureUsage --usage 0 --commit
  96. `
  97. if (argv.help) {
  98. console.log(HELP_TEXT)
  99. process.exit(0)
  100. }
  101. const userIdArg = argv['user-id']
  102. const edgeArg = argv.edge
  103. const COMMIT = argv.commit === true
  104. if (!userIdArg) throw new Error('missing --user-id (use --help for usage)')
  105. if (!ObjectId.isValid(userIdArg)) {
  106. throw new Error(`invalid --user-id: ${userIdArg}`)
  107. }
  108. let feature
  109. let usage
  110. if (edgeArg !== undefined) {
  111. if (argv.feature !== undefined || argv.usage !== undefined) {
  112. throw new Error('--edge cannot be combined with --feature or --usage')
  113. }
  114. const preset = EDGE_PRESETS[edgeArg]
  115. if (!preset) {
  116. throw new Error(
  117. `invalid --edge ${JSON.stringify(edgeArg)}; expected one of ${Object.keys(EDGE_PRESETS).join(', ')}`
  118. )
  119. }
  120. feature = preset.feature
  121. usage = preset.usage
  122. } else {
  123. feature = argv.feature
  124. if (!VALID_FEATURES.includes(feature)) {
  125. throw new Error(
  126. `invalid --feature ${JSON.stringify(feature)}; expected one of ${VALID_FEATURES.join(', ')}`
  127. )
  128. }
  129. usage = Number(argv.usage)
  130. if (!Number.isFinite(usage) || usage < 0) {
  131. throw new Error(`invalid --usage: ${argv.usage}`)
  132. }
  133. }
  134. const userId = new ObjectId(userIdArg)
  135. async function main() {
  136. const user = await db.users.findOne(
  137. { _id: userId },
  138. {
  139. projection: { _id: 1, email: 1 },
  140. readPreference: READ_PREFERENCE_PRIMARY,
  141. }
  142. )
  143. if (!user) {
  144. throw new Error(`user not found: ${userIdArg}`)
  145. }
  146. const before = await UserFeatureUsage.findOne({ _id: userId }).lean().exec()
  147. console.log('Before:', {
  148. userId: userIdArg,
  149. email: user.email,
  150. feature,
  151. current: before?.features?.[feature] ?? null,
  152. })
  153. if (!COMMIT) {
  154. console.warn(`Dry run: would set features.${feature}.usage = ${usage}`)
  155. console.warn('Re-run with --commit to apply.')
  156. return
  157. }
  158. const updated = await UserFeatureUsage.findOneAndUpdate(
  159. { _id: userId },
  160. [
  161. // reset periodStart/usage if the previous period (24h) has elapsed
  162. {
  163. $set: {
  164. features: {
  165. [feature]: {
  166. $cond: {
  167. if: {
  168. $lte: [
  169. {
  170. $dateAdd: {
  171. startDate: `$features.${feature}.periodStart`,
  172. unit: 'hour',
  173. amount: PERIOD_HOURS,
  174. },
  175. },
  176. '$$NOW',
  177. ],
  178. },
  179. then: { usage: 0, periodStart: '$$NOW' },
  180. else: `$features.${feature}`,
  181. },
  182. },
  183. },
  184. },
  185. },
  186. // overwrite usage with the requested value, preserving periodStart
  187. {
  188. $set: {
  189. [`features.${feature}.usage`]: usage,
  190. },
  191. },
  192. ],
  193. { new: true, upsert: true }
  194. )
  195. .lean()
  196. .exec()
  197. console.log('After:', {
  198. userId: userIdArg,
  199. feature,
  200. current: updated?.features?.[feature] ?? null,
  201. })
  202. }
  203. try {
  204. await scriptRunner(main, {
  205. userId: userIdArg,
  206. feature,
  207. usage,
  208. edge: edgeArg,
  209. commit: COMMIT,
  210. })
  211. console.error('Done.')
  212. process.exit(0)
  213. } catch (error) {
  214. console.error({ error })
  215. process.exit(1)
  216. }