cost-control.service.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. /**
  2. * Cost control and quota management service
  3. * Used to manage API call costs for Vision Pipeline
  4. */
  5. import { Injectable, Logger } from '@nestjs/common';
  6. import { ConfigService } from '@nestjs/config';
  7. import { InjectRepository } from '@nestjs/typeorm';
  8. import { Repository } from 'typeorm';
  9. import { User } from '../user/user.entity';
  10. export interface UserQuota {
  11. userId: string;
  12. monthlyCost: number; // Current month used cost
  13. maxCost: number; // Monthly max cost
  14. remaining: number; // Remaining cost
  15. lastReset: Date; // Last reset time
  16. }
  17. export interface CostEstimate {
  18. estimatedCost: number; // Estimated cost
  19. estimatedTime: number; // Estimated time(seconds)
  20. pageBreakdown: { // Per-page breakdown
  21. pageIndex: number;
  22. cost: number;
  23. }[];
  24. }
  25. @Injectable()
  26. export class CostControlService {
  27. private readonly logger = new Logger(CostControlService.name);
  28. private readonly COST_PER_PAGE = 0.01; // Cost per page(USD)
  29. private readonly DEFAULT_MONTHLY_LIMIT = 100; // Default monthly limit(USD)
  30. constructor(
  31. private configService: ConfigService,
  32. @InjectRepository(User)
  33. private userRepository: Repository<User>,
  34. ) { }
  35. /**
  36. * Estimate processing cost
  37. */
  38. estimateCost(pageCount: number, quality: 'low' | 'medium' | 'high' = 'medium'): CostEstimate {
  39. // Adjust cost coefficient based on quality
  40. const qualityMultiplier = {
  41. low: 0.5,
  42. medium: 1.0,
  43. high: 1.5,
  44. };
  45. const baseCost = pageCount * this.COST_PER_PAGE * qualityMultiplier[quality];
  46. const estimatedTime = pageCount * 3; // // Approximately 3 seconds
  47. const pageBreakdown = Array.from({ length: pageCount }, (_, i) => ({
  48. pageIndex: i + 1,
  49. cost: this.COST_PER_PAGE * qualityMultiplier[quality],
  50. }));
  51. return {
  52. estimatedCost: baseCost,
  53. estimatedTime,
  54. pageBreakdown,
  55. };
  56. }
  57. /**
  58. * Check user quota
  59. */
  60. async checkQuota(userId: string, estimatedCost: number): Promise<{
  61. allowed: boolean;
  62. quota: UserQuota;
  63. reason?: string;
  64. }> {
  65. const quota = await this.getUserQuota(userId);
  66. // Check monthly reset
  67. this.checkAndResetMonthlyQuota(quota);
  68. if (quota.remaining < estimatedCost) {
  69. this.logger.warn(
  70. `User ${userId} quota insufficient: remaining $${quota.remaining.toFixed(2)}, required $${estimatedCost.toFixed(2)}`,
  71. );
  72. return {
  73. allowed: false,
  74. quota,
  75. reason: `Insufficient quota: remaining $${quota.remaining.toFixed(2)}, required $${estimatedCost.toFixed(2)}`,
  76. };
  77. }
  78. return {
  79. allowed: true,
  80. quota,
  81. };
  82. }
  83. /**
  84. * Deduct from quota
  85. */
  86. async deductQuota(userId: string, actualCost: number): Promise<void> {
  87. const quota = await this.getUserQuota(userId);
  88. quota.monthlyCost += actualCost;
  89. quota.remaining = quota.maxCost - quota.monthlyCost;
  90. await this.userRepository.update(userId, {
  91. monthlyCost: quota.monthlyCost,
  92. });
  93. this.logger.log(
  94. `Deducted $${actualCost.toFixed(2)} from user ${userId} quota. Remaining: $${quota.remaining.toFixed(2)}`,
  95. );
  96. }
  97. /**
  98. * Get user quota
  99. */
  100. async getUserQuota(userId: string): Promise<UserQuota> {
  101. const user = await this.userRepository.findOne({ where: { id: userId } });
  102. if (!user) {
  103. throw new Error(`User ${userId} does not exist`);
  104. }
  105. // Use default if user has no quota info
  106. const monthlyCost = user.monthlyCost || 0;
  107. const maxCost = user.maxCost || this.DEFAULT_MONTHLY_LIMIT;
  108. const lastReset = user.lastQuotaReset || new Date();
  109. return {
  110. userId,
  111. monthlyCost,
  112. maxCost,
  113. remaining: maxCost - monthlyCost,
  114. lastReset,
  115. };
  116. }
  117. /**
  118. * Check and reset monthly quota
  119. */
  120. private checkAndResetMonthlyQuota(quota: UserQuota): void {
  121. const now = new Date();
  122. const lastReset = quota.lastReset;
  123. // Check if crossed month
  124. if (
  125. now.getMonth() !== lastReset.getMonth() ||
  126. now.getFullYear() !== lastReset.getFullYear()
  127. ) {
  128. this.logger.log(`Reset monthly quota for user ${quota.userId}`);
  129. // Reset quota
  130. quota.monthlyCost = 0;
  131. quota.remaining = quota.maxCost;
  132. quota.lastReset = now;
  133. // Update database
  134. this.userRepository.update(quota.userId, {
  135. monthlyCost: 0,
  136. lastQuotaReset: now,
  137. });
  138. }
  139. }
  140. /**
  141. * Set user quota limit
  142. */
  143. async setQuotaLimit(userId: string, maxCost: number): Promise<void> {
  144. await this.userRepository.update(userId, { maxCost });
  145. this.logger.log(`Set quota limit to $${maxCost} for user ${userId}`);
  146. }
  147. /**
  148. * Get cost report
  149. */
  150. async getCostReport(userId: string, days: number = 30): Promise<{
  151. totalCost: number;
  152. dailyAverage: number;
  153. pageStats: {
  154. totalPages: number;
  155. avgCostPerPage: number;
  156. };
  157. quotaUsage: number; // Percentage
  158. }> {
  159. const quota = await this.getUserQuota(userId);
  160. const usagePercent = (quota.monthlyCost / quota.maxCost) * 100;
  161. // Query history records here(if implemented)
  162. // Return current quota info temporarily
  163. return {
  164. totalCost: quota.monthlyCost,
  165. dailyAverage: quota.monthlyCost / Math.max(days, 1),
  166. pageStats: {
  167. totalPages: Math.floor(quota.monthlyCost / this.COST_PER_PAGE),
  168. avgCostPerPage: this.COST_PER_PAGE,
  169. },
  170. quotaUsage: usagePercent,
  171. };
  172. }
  173. /**
  174. * Check cost warning threshold
  175. */
  176. async checkWarningThreshold(userId: string): Promise<{
  177. shouldWarn: boolean;
  178. message: string;
  179. }> {
  180. const quota = await this.getUserQuota(userId);
  181. const usagePercent = (quota.monthlyCost / quota.maxCost) * 100;
  182. if (usagePercent >= 90) {
  183. return {
  184. shouldWarn: true,
  185. message: `⚠️ Quota usage reached ${usagePercent.toFixed(1)}%. Remaining: $${quota.remaining.toFixed(2)}`,
  186. };
  187. }
  188. if (usagePercent >= 75) {
  189. return {
  190. shouldWarn: true,
  191. message: `💡 Quota usage at ${usagePercent.toFixed(1)}%. Please monitor your costs carefully`,
  192. };
  193. }
  194. return {
  195. shouldWarn: false,
  196. message: '',
  197. };
  198. }
  199. /**
  200. * Format cost display
  201. */
  202. formatCost(cost: number): string {
  203. return `$${cost.toFixed(2)}`;
  204. }
  205. /**
  206. * Format time display
  207. */
  208. formatTime(seconds: number): string {
  209. if (seconds < 60) {
  210. return `${seconds.toFixed(0)}s`;
  211. }
  212. const minutes = Math.floor(seconds / 60);
  213. const remainingSeconds = seconds % 60;
  214. return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
  215. }
  216. }