New Model & tokens/credits updates

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-23 06:26:15 +00:00
parent 1d4825ad77
commit d768ed71d4
9 changed files with 945 additions and 35 deletions

View File

@@ -3,7 +3,9 @@ Credit Service for managing credit transactions and deductions
"""
from django.db import transaction
from django.utils import timezone
from igny8_core.business.billing.models import CreditTransaction, CreditUsageLog
from decimal import Decimal
import math
from igny8_core.business.billing.models import CreditTransaction, CreditUsageLog, AIModelConfig
from igny8_core.business.billing.constants import CREDIT_COSTS
from igny8_core.business.billing.exceptions import InsufficientCreditsError, CreditCalculationError
from igny8_core.auth.models import Account
@@ -117,7 +119,10 @@ class CreditService:
@staticmethod
@transaction.atomic
def deduct_credits(account, amount, operation_type, description, metadata=None, cost_usd=None, model_used=None, tokens_input=None, tokens_output=None, related_object_type=None, related_object_id=None):
def deduct_credits(account, amount, operation_type, description, metadata=None,
cost_usd_input=None, cost_usd_output=None, cost_usd_total=None,
model_config=None, tokens_input=None, tokens_output=None,
related_object_type=None, related_object_id=None):
"""
Deduct credits and log transaction.
@@ -127,8 +132,10 @@ class CreditService:
operation_type: Type of operation (from CreditUsageLog.OPERATION_TYPE_CHOICES)
description: Description of the transaction
metadata: Optional metadata dict
cost_usd: Optional cost in USD
model_used: Optional AI model used
cost_usd_input: Optional input cost in USD
cost_usd_output: Optional output cost in USD
cost_usd_total: Optional total cost in USD
model_config: Optional AIModelConfig instance
tokens_input: Optional input tokens
tokens_output: Optional output tokens
related_object_type: Optional related object type
@@ -154,25 +161,45 @@ class CreditService:
metadata=metadata or {}
)
# Create CreditUsageLog
CreditUsageLog.objects.create(
account=account,
operation_type=operation_type,
credits_used=amount,
cost_usd=cost_usd,
model_used=model_used or '',
tokens_input=tokens_input,
tokens_output=tokens_output,
related_object_type=related_object_type or '',
related_object_id=related_object_id,
metadata=metadata or {}
)
# Create CreditUsageLog with new model_config FK
log_data = {
'account': account,
'operation_type': operation_type,
'credits_used': amount,
'tokens_input': tokens_input,
'tokens_output': tokens_output,
'related_object_type': related_object_type or '',
'related_object_id': related_object_id,
'metadata': metadata or {},
}
# Add model tracking (new FK)
if model_config:
log_data['model_config'] = model_config
log_data['model_name'] = model_config.model_name
# Add cost tracking (new fields)
if cost_usd_input is not None:
log_data['cost_usd_input'] = cost_usd_input
if cost_usd_output is not None:
log_data['cost_usd_output'] = cost_usd_output
if cost_usd_total is not None:
log_data['cost_usd_total'] = cost_usd_total
# Legacy cost_usd field (backward compatibility)
if cost_usd_total is not None:
log_data['cost_usd'] = cost_usd_total
CreditUsageLog.objects.create(**log_data)
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):
def deduct_credits_for_operation(account, operation_type, amount=None, description=None,
metadata=None, cost_usd_input=None, cost_usd_output=None,
cost_usd_total=None, model_config=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).
@@ -182,8 +209,10 @@ class CreditService:
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
cost_usd_input: Optional input cost in USD
cost_usd_output: Optional output cost in USD
cost_usd_total: Optional total cost in USD
model_config: Optional AIModelConfig instance
tokens_input: Optional input tokens
tokens_output: Optional output tokens
related_object_type: Optional related object type
@@ -192,24 +221,33 @@ class CreditService:
Returns:
int: New credit balance
"""
# Calculate credit cost
credits_required = CreditService.get_credit_cost(operation_type, amount)
# Calculate credit cost - use token-based if tokens provided
if tokens_input is not None and tokens_output is not None and model_config:
credits_required = CreditService.calculate_credits_from_tokens(
operation_type, tokens_input, tokens_output, model_config
)
else:
credits_required = CreditService.get_credit_cost(operation_type, amount)
# Check sufficient credits
CreditService.check_credits(account, operation_type, amount)
CreditService.check_credits_legacy(account, credits_required)
# Auto-generate description if not provided
if not description:
model_name = model_config.display_name if model_config else "AI"
if operation_type == 'clustering':
description = f"Clustering operation"
description = f"Clustering operation ({model_name})"
elif operation_type == 'idea_generation':
description = f"Generated {amount or 1} idea(s)"
description = f"Generated {amount or 1} idea(s) ({model_name})"
elif operation_type == 'content_generation':
description = f"Generated content ({amount or 0} words)"
if tokens_input and tokens_output:
description = f"Generated content ({tokens_input + tokens_output} tokens, {model_name})"
else:
description = f"Generated content ({amount or 0} words, {model_name})"
elif operation_type == 'image_generation':
description = f"Generated {amount or 1} image(s)"
description = f"Generated {amount or 1} image(s) ({model_name})"
else:
description = f"{operation_type} operation"
description = f"{operation_type} operation ({model_name})"
return CreditService.deduct_credits(
account=account,
@@ -217,8 +255,10 @@ class CreditService:
operation_type=operation_type,
description=description,
metadata=metadata,
cost_usd=cost_usd,
model_used=model_used,
cost_usd_input=cost_usd_input,
cost_usd_output=cost_usd_output,
cost_usd_total=cost_usd_total,
model_config=model_config,
tokens_input=tokens_input,
tokens_output=tokens_output,
related_object_type=related_object_type,
@@ -292,3 +332,152 @@ class CreditService:
return CreditService.get_credit_cost(operation_type, amount)
@staticmethod
def calculate_credits_from_tokens(operation_type, tokens_input, tokens_output, model_config):
"""
Calculate credits based on actual token usage and AI model configuration.
This is the new token-aware calculation method.
Args:
operation_type: Type of operation (e.g., 'content_generation')
tokens_input: Number of input tokens used
tokens_output: Number of output tokens used
model_config: AIModelConfig instance
Returns:
int: Number of credits to deduct
Raises:
CreditCalculationError: If calculation fails
"""
import logging
logger = logging.getLogger(__name__)
try:
from igny8_core.business.billing.models import CreditCostConfig
# Get operation config
config = CreditCostConfig.objects.filter(
operation_type=operation_type,
is_active=True
).first()
if not config:
raise CreditCalculationError(f"No active config found for operation: {operation_type}")
# Check if operation uses token-based billing
if config.unit in ['per_100_tokens', 'per_1000_tokens']:
total_tokens = tokens_input + tokens_output
# Get model's tokens-per-credit ratio
tokens_per_credit = model_config.tokens_per_credit
if tokens_per_credit <= 0:
raise CreditCalculationError(f"Invalid tokens_per_credit: {tokens_per_credit}")
# Calculate credits (float)
credits_float = Decimal(total_tokens) / Decimal(tokens_per_credit)
# Apply rounding (always round up to avoid undercharging)
credits = math.ceil(credits_float)
# Apply minimum cost from config (if set)
credits = max(credits, config.credits_cost)
logger.info(
f"Token-based calculation: {total_tokens} tokens / {tokens_per_credit} = {credits} credits "
f"(model: {model_config.model_name}, operation: {operation_type})"
)
return credits
else:
# Fall back to legacy calculation for non-token operations
logger.warning(
f"Operation {operation_type} uses unit {config.unit}, falling back to legacy calculation"
)
return config.credits_cost
except Exception as e:
logger.error(f"Failed to calculate credits from tokens: {e}")
raise CreditCalculationError(f"Credit calculation failed: {e}")
@staticmethod
def get_model_for_operation(account, operation_type, task_model_override=None):
"""
Determine which AI model to use for an operation.
Priority: Task Override > Account Default > Operation Default > System Default
Args:
account: Account instance
operation_type: Type of operation
task_model_override: Optional AIModelConfig instance from task
Returns:
AIModelConfig: The model to use
"""
import logging
logger = logging.getLogger(__name__)
# 1. Task-level override (highest priority)
if task_model_override:
logger.info(f"Using task-level model override: {task_model_override.model_name}")
return task_model_override
# 2. Account default model (from IntegrationSettings)
try:
from igny8_core.modules.system.models import IntegrationSettings
from igny8_core.business.billing.models import CreditCostConfig
integration = IntegrationSettings.objects.filter(account=account).first()
if integration:
# Determine if this is text or image operation
config = CreditCostConfig.objects.filter(
operation_type=operation_type,
is_active=True
).first()
if config and config.default_model:
model_type = config.default_model.model_type
if model_type == 'text' and integration.default_text_model:
logger.info(f"Using account default text model: {integration.default_text_model.model_name}")
return integration.default_text_model
elif model_type == 'image' and integration.default_image_model:
logger.info(f"Using account default image model: {integration.default_image_model.model_name}")
return integration.default_image_model
except Exception as e:
logger.warning(f"Failed to get account default model: {e}")
# 3. Operation default model
try:
from igny8_core.business.billing.models import CreditCostConfig
config = CreditCostConfig.objects.filter(
operation_type=operation_type,
is_active=True
).first()
if config and config.default_model:
logger.info(f"Using operation default model: {config.default_model.model_name}")
return config.default_model
except Exception as e:
logger.warning(f"Failed to get operation default model: {e}")
# 4. System-wide default (fallback)
try:
default_model = AIModelConfig.objects.filter(
is_default=True,
is_active=True
).first()
if default_model:
logger.info(f"Using system default model: {default_model.model_name}")
return default_model
except Exception as e:
logger.warning(f"Failed to get system default model: {e}")
# 5. Hard-coded fallback
logger.warning("All model selection failed, using hard-coded fallback: gpt-4o-mini")
return AIModelConfig.objects.filter(model_name='gpt-4o-mini').first()