Phase 0: Update credit costs and CreditService, add credit checks to AI Engine

- Updated CREDIT_COSTS to match Phase 0 spec (flat structure)
- Added get_credit_cost() method to CreditService
- Updated check_credits() to accept operation_type and amount
- Added deduct_credits_for_operation() convenience method
- Updated AI Engine to check credits BEFORE AI call
- Updated AI Engine to deduct credits AFTER successful execution
- Added helper methods for operation type mapping and amount calculation
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-16 18:37:41 +00:00
parent f84be4194f
commit 5b11c4001e
3 changed files with 264 additions and 73 deletions

View File

@@ -1,22 +1,21 @@
"""
Credit Cost Constants
Phase 0: Credit-only system costs per operation
"""
CREDIT_COSTS = {
'clustering': {
'base': 1, # 1 credit per 30 keywords
'per_keyword': 1 / 30,
},
'ideas': {
'base': 1, # 1 credit per idea
},
'content': {
'base': 3, # 3 credits per full blog post
},
'images': {
'base': 1, # 1 credit per image
},
'reparse': {
'base': 1, # 1 credit per reparse
},
'clustering': 10, # Per clustering request
'idea_generation': 15, # Per cluster → ideas request
'content_generation': 1, # Per 100 words
'image_prompt_extraction': 2, # Per content piece
'image_generation': 5, # Per image
'linking': 8, # Per content piece (NEW)
'optimization': 1, # Per 200 words (NEW)
'site_structure_generation': 50, # Per site blueprint (NEW)
'site_page_generation': 20, # Per page (NEW)
# Legacy operation types (for backward compatibility)
'ideas': 15, # Alias for idea_generation
'content': 3, # Legacy: 3 credits per content piece
'images': 5, # Alias for image_generation
'reparse': 1, # Per reparse
}

View File

@@ -13,9 +13,65 @@ class CreditService:
"""Service for managing credits"""
@staticmethod
def check_credits(account, required_credits):
def get_credit_cost(operation_type, amount=None):
"""
Check if account has enough credits.
Get credit cost for operation.
Args:
operation_type: Type of operation (from CREDIT_COSTS)
amount: Optional amount (word count, image count, etc.)
Returns:
int: Number of credits required
Raises:
CreditCalculationError: If operation type is unknown
"""
base_cost = CREDIT_COSTS.get(operation_type, 0)
if base_cost == 0:
raise CreditCalculationError(f"Unknown operation type: {operation_type}")
# Variable cost operations
if operation_type == 'content_generation' and amount:
# Per 100 words
return max(1, int(base_cost * (amount / 100)))
elif operation_type == 'optimization' and amount:
# Per 200 words
return max(1, int(base_cost * (amount / 200)))
elif operation_type == 'image_generation' and amount:
# Per image
return base_cost * amount
elif operation_type == 'idea_generation' and amount:
# Per idea
return base_cost * amount
# Fixed cost operations
return base_cost
@staticmethod
def check_credits(account, operation_type, amount=None):
"""
Check if account has sufficient credits for an operation.
Args:
account: Account instance
operation_type: Type of operation
amount: Optional amount (word count, image count, etc.)
Raises:
InsufficientCreditsError: If account doesn't have enough credits
"""
required = CreditService.get_credit_cost(operation_type, amount)
if account.credits < required:
raise InsufficientCreditsError(
f"Insufficient credits. Required: {required}, Available: {account.credits}"
)
return True
@staticmethod
def check_credits_legacy(account, required_credits):
"""
Legacy method: Check if account has enough credits (for backward compatibility).
Args:
account: Account instance
@@ -51,8 +107,8 @@ class CreditService:
Returns:
int: New credit balance
"""
# Check sufficient credits
CreditService.check_credits(account, amount)
# Check sufficient credits (legacy: amount is already calculated)
CreditService.check_credits_legacy(account, amount)
# Deduct from account.credits
account.credits -= amount
@@ -84,6 +140,61 @@ class CreditService:
return account.credits
@staticmethod
@transaction.atomic
def deduct_credits_for_operation(account, operation_type, amount=None, description=None, metadata=None, cost_usd=None, model_used=None, tokens_input=None, tokens_output=None, related_object_type=None, related_object_id=None):
"""
Deduct credits for an operation (convenience method that calculates cost automatically).
Args:
account: Account instance
operation_type: Type of operation
amount: Optional amount (word count, image count, etc.)
description: Optional description (auto-generated if not provided)
metadata: Optional metadata dict
cost_usd: Optional cost in USD
model_used: Optional AI model used
tokens_input: Optional input tokens
tokens_output: Optional output tokens
related_object_type: Optional related object type
related_object_id: Optional related object ID
Returns:
int: New credit balance
"""
# Calculate credit cost
credits_required = CreditService.get_credit_cost(operation_type, amount)
# Check sufficient credits
CreditService.check_credits(account, operation_type, amount)
# Auto-generate description if not provided
if not description:
if operation_type == 'clustering':
description = f"Clustering operation"
elif operation_type == 'idea_generation':
description = f"Generated {amount or 1} idea(s)"
elif operation_type == 'content_generation':
description = f"Generated content ({amount or 0} words)"
elif operation_type == 'image_generation':
description = f"Generated {amount or 1} image(s)"
else:
description = f"{operation_type} operation"
return CreditService.deduct_credits(
account=account,
amount=credits_required,
operation_type=operation_type,
description=description,
metadata=metadata,
cost_usd=cost_usd,
model_used=model_used,
tokens_input=tokens_input,
tokens_output=tokens_output,
related_object_type=related_object_type,
related_object_id=related_object_id
)
@staticmethod
@transaction.atomic
def add_credits(account, amount, transaction_type, description, metadata=None):
@@ -120,6 +231,7 @@ class CreditService:
def calculate_credits_for_operation(operation_type, **kwargs):
"""
Calculate credits needed for an operation.
Legacy method - use get_credit_cost() instead.
Args:
operation_type: Type of operation
@@ -131,31 +243,22 @@ class CreditService:
Raises:
CreditCalculationError: If calculation fails
"""
if operation_type not in CREDIT_COSTS:
raise CreditCalculationError(f"Unknown operation type: {operation_type}")
cost_config = CREDIT_COSTS[operation_type]
if operation_type == 'clustering':
# 1 credit per 30 keywords
keyword_count = kwargs.get('keyword_count', 0)
credits = max(1, int(keyword_count * cost_config['per_keyword']))
return credits
elif operation_type == 'ideas':
# 1 credit per idea
idea_count = kwargs.get('idea_count', 1)
return cost_config['base'] * idea_count
# Map legacy operation types
if operation_type == 'ideas':
operation_type = 'idea_generation'
elif operation_type == 'content':
# 3 credits per content piece
content_count = kwargs.get('content_count', 1)
return cost_config['base'] * content_count
operation_type = 'content_generation'
elif operation_type == 'images':
# 1 credit per image
image_count = kwargs.get('image_count', 1)
return cost_config['base'] * image_count
elif operation_type == 'reparse':
# 1 credit per reparse
return cost_config['base']
operation_type = 'image_generation'
return cost_config['base']
# Extract amount from kwargs
amount = None
if 'word_count' in kwargs:
amount = kwargs.get('word_count')
elif 'image_count' in kwargs:
amount = kwargs.get('image_count')
elif 'idea_count' in kwargs:
amount = kwargs.get('idea_count')
return CreditService.get_credit_cost(operation_type, amount)