django admin Groups reorg, Frontend udpates for site settings, #Migration runs
This commit is contained in:
@@ -114,65 +114,48 @@ class CreditUsageLog(AccountBaseModel):
|
||||
|
||||
class CreditCostConfig(models.Model):
|
||||
"""
|
||||
Token-based credit pricing configuration.
|
||||
ALL operations use token-to-credit conversion.
|
||||
Fixed credit costs per operation type.
|
||||
|
||||
Per final-model-schemas.md:
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| operation_type | CharField(50) PK | Yes | Unique operation ID |
|
||||
| display_name | CharField(100) | Yes | Human-readable |
|
||||
| base_credits | IntegerField | Yes | Fixed credits per operation |
|
||||
| is_active | BooleanField | Yes | Enable/disable |
|
||||
| description | TextField | No | Admin notes |
|
||||
"""
|
||||
# Operation identification
|
||||
# Operation identification (Primary Key)
|
||||
operation_type = models.CharField(
|
||||
max_length=50,
|
||||
unique=True,
|
||||
choices=CreditUsageLog.OPERATION_TYPE_CHOICES,
|
||||
help_text="AI operation type"
|
||||
primary_key=True,
|
||||
help_text="Unique operation ID (e.g., 'article_generation', 'image_generation')"
|
||||
)
|
||||
|
||||
# Token-to-credit ratio (tokens per 1 credit)
|
||||
tokens_per_credit = models.IntegerField(
|
||||
default=100,
|
||||
validators=[MinValueValidator(1)],
|
||||
help_text="Number of tokens that equal 1 credit (e.g., 100 tokens = 1 credit)"
|
||||
# Human-readable name
|
||||
display_name = models.CharField(
|
||||
max_length=100,
|
||||
help_text="Human-readable name"
|
||||
)
|
||||
|
||||
# Minimum credits (for very small token usage)
|
||||
min_credits = models.IntegerField(
|
||||
# Fixed credits per operation
|
||||
base_credits = models.IntegerField(
|
||||
default=1,
|
||||
validators=[MinValueValidator(0)],
|
||||
help_text="Minimum credits to charge regardless of token usage"
|
||||
help_text="Fixed credits per operation"
|
||||
)
|
||||
|
||||
# Price per credit (for revenue reporting)
|
||||
price_per_credit_usd = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=4,
|
||||
default=Decimal('0.01'),
|
||||
validators=[MinValueValidator(Decimal('0.0001'))],
|
||||
help_text="USD price per credit (for revenue reporting)"
|
||||
)
|
||||
|
||||
# Metadata
|
||||
display_name = models.CharField(max_length=100, help_text="Human-readable name")
|
||||
description = models.TextField(blank=True, help_text="What this operation does")
|
||||
|
||||
# Status
|
||||
is_active = models.BooleanField(default=True, help_text="Enable/disable this operation")
|
||||
|
||||
|
||||
# Audit fields
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
updated_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='credit_cost_updates',
|
||||
help_text="Admin who last updated"
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Enable/disable this operation"
|
||||
)
|
||||
|
||||
# Change tracking
|
||||
previous_tokens_per_credit = models.IntegerField(
|
||||
null=True,
|
||||
# Admin notes
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
help_text="Tokens per credit before last update (for audit trail)"
|
||||
help_text="Admin notes about this operation"
|
||||
)
|
||||
|
||||
# History tracking
|
||||
@@ -186,18 +169,7 @@ class CreditCostConfig(models.Model):
|
||||
ordering = ['operation_type']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.display_name} - {self.tokens_per_credit} tokens/credit"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Track token ratio changes
|
||||
if self.pk:
|
||||
try:
|
||||
old = CreditCostConfig.objects.get(pk=self.pk)
|
||||
if old.tokens_per_credit != self.tokens_per_credit:
|
||||
self.previous_tokens_per_credit = old.tokens_per_credit
|
||||
except CreditCostConfig.DoesNotExist:
|
||||
pass
|
||||
super().save(*args, **kwargs)
|
||||
return f"{self.display_name} - {self.base_credits} credits"
|
||||
|
||||
|
||||
class BillingConfiguration(models.Model):
|
||||
@@ -696,18 +668,34 @@ class AccountPaymentMethod(AccountBaseModel):
|
||||
|
||||
class AIModelConfig(models.Model):
|
||||
"""
|
||||
AI Model Configuration - Database-driven model pricing and capabilities.
|
||||
Replaces hardcoded MODEL_RATES and IMAGE_MODEL_RATES from constants.py
|
||||
All AI models (text + image) with pricing and credit configuration.
|
||||
Single Source of Truth for Models.
|
||||
|
||||
Two pricing models:
|
||||
- Text models: Cost per 1M tokens (input/output), credits calculated AFTER AI call
|
||||
- Image models: Cost per image, credits calculated BEFORE AI call
|
||||
Per final-model-schemas.md:
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| id | AutoField PK | Auto | |
|
||||
| model_name | CharField(100) | Yes | gpt-5.1, dall-e-3, runware:97@1 |
|
||||
| model_type | CharField(20) | Yes | text / image |
|
||||
| provider | CharField(50) | Yes | Links to IntegrationProvider |
|
||||
| display_name | CharField(200) | Yes | Human-readable |
|
||||
| is_default | BooleanField | Yes | One default per type |
|
||||
| is_active | BooleanField | Yes | Enable/disable |
|
||||
| cost_per_1k_input | DecimalField | No | Provider cost (USD) - text models |
|
||||
| cost_per_1k_output | DecimalField | No | Provider cost (USD) - text models |
|
||||
| tokens_per_credit | IntegerField | No | Text: tokens per 1 credit (e.g., 1000) |
|
||||
| credits_per_image | IntegerField | No | Image: credits per image (e.g., 1, 5, 15) |
|
||||
| quality_tier | CharField(20) | No | basic / quality / premium |
|
||||
| max_tokens | IntegerField | No | Model token limit |
|
||||
| context_window | IntegerField | No | Model context size |
|
||||
| capabilities | JSONField | No | vision, function_calling, etc. |
|
||||
| created_at | DateTime | Auto | |
|
||||
| updated_at | DateTime | Auto | |
|
||||
"""
|
||||
|
||||
MODEL_TYPE_CHOICES = [
|
||||
('text', 'Text Generation'),
|
||||
('image', 'Image Generation'),
|
||||
('embedding', 'Embedding'),
|
||||
]
|
||||
|
||||
PROVIDER_CHOICES = [
|
||||
@@ -717,145 +705,112 @@ class AIModelConfig(models.Model):
|
||||
('google', 'Google'),
|
||||
]
|
||||
|
||||
QUALITY_TIER_CHOICES = [
|
||||
('basic', 'Basic'),
|
||||
('quality', 'Quality'),
|
||||
('premium', 'Premium'),
|
||||
]
|
||||
|
||||
# Basic Information
|
||||
model_name = models.CharField(
|
||||
max_length=100,
|
||||
unique=True,
|
||||
db_index=True,
|
||||
help_text="Model identifier used in API calls (e.g., 'gpt-4o-mini', 'dall-e-3')"
|
||||
)
|
||||
|
||||
display_name = models.CharField(
|
||||
max_length=200,
|
||||
help_text="Human-readable name shown in UI (e.g., 'GPT-4o mini - Fast & Affordable')"
|
||||
help_text="Model identifier (e.g., 'gpt-5.1', 'dall-e-3', 'runware:97@1')"
|
||||
)
|
||||
|
||||
model_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=MODEL_TYPE_CHOICES,
|
||||
db_index=True,
|
||||
help_text="Type of model - determines which pricing fields are used"
|
||||
help_text="text / image"
|
||||
)
|
||||
|
||||
provider = models.CharField(
|
||||
max_length=50,
|
||||
choices=PROVIDER_CHOICES,
|
||||
db_index=True,
|
||||
help_text="AI provider (OpenAI, Anthropic, etc.)"
|
||||
help_text="Links to IntegrationProvider"
|
||||
)
|
||||
|
||||
# Text Model Pricing (Only for model_type='text')
|
||||
input_cost_per_1m = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=4,
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(Decimal('0.0001'))],
|
||||
help_text="Cost per 1 million input tokens (USD). For text models only."
|
||||
)
|
||||
|
||||
output_cost_per_1m = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=4,
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(Decimal('0.0001'))],
|
||||
help_text="Cost per 1 million output tokens (USD). For text models only."
|
||||
)
|
||||
|
||||
context_window = models.IntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(1)],
|
||||
help_text="Maximum input tokens (context length). For text models only."
|
||||
)
|
||||
|
||||
max_output_tokens = models.IntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(1)],
|
||||
help_text="Maximum output tokens per request. For text models only."
|
||||
)
|
||||
|
||||
# Image Model Pricing (Only for model_type='image')
|
||||
cost_per_image = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=4,
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(Decimal('0.0001'))],
|
||||
help_text="Fixed cost per image generation (USD). For image models only."
|
||||
)
|
||||
|
||||
valid_sizes = models.JSONField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text='Array of valid image sizes (e.g., ["1024x1024", "1024x1792"]). For image models only.'
|
||||
)
|
||||
|
||||
# Capabilities
|
||||
supports_json_mode = models.BooleanField(
|
||||
default=False,
|
||||
help_text="True for models with JSON response format support"
|
||||
)
|
||||
|
||||
supports_vision = models.BooleanField(
|
||||
default=False,
|
||||
help_text="True for models that can analyze images"
|
||||
)
|
||||
|
||||
supports_function_calling = models.BooleanField(
|
||||
default=False,
|
||||
help_text="True for models with function calling capability"
|
||||
)
|
||||
|
||||
# Status & Configuration
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
db_index=True,
|
||||
help_text="Enable/disable model without deleting"
|
||||
display_name = models.CharField(
|
||||
max_length=200,
|
||||
help_text="Human-readable name"
|
||||
)
|
||||
|
||||
is_default = models.BooleanField(
|
||||
default=False,
|
||||
db_index=True,
|
||||
help_text="Mark as default model for its type (only one per type)"
|
||||
help_text="One default per type"
|
||||
)
|
||||
|
||||
sort_order = models.IntegerField(
|
||||
default=0,
|
||||
help_text="Control order in dropdown lists (lower numbers first)"
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
db_index=True,
|
||||
help_text="Enable/disable"
|
||||
)
|
||||
|
||||
# Metadata
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
help_text="Admin notes about model usage, strengths, limitations"
|
||||
)
|
||||
|
||||
release_date = models.DateField(
|
||||
# Text Model Pricing (cost per 1K tokens)
|
||||
cost_per_1k_input = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=6,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="When model was released/added"
|
||||
help_text="Provider cost per 1K input tokens (USD) - text models"
|
||||
)
|
||||
|
||||
deprecation_date = models.DateField(
|
||||
cost_per_1k_output = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=6,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="When model will be removed"
|
||||
help_text="Provider cost per 1K output tokens (USD) - text models"
|
||||
)
|
||||
|
||||
# Audit Fields
|
||||
# Credit Configuration
|
||||
tokens_per_credit = models.IntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Text: tokens per 1 credit (e.g., 1000, 10000)"
|
||||
)
|
||||
|
||||
credits_per_image = models.IntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Image: credits per image (e.g., 1, 5, 15)"
|
||||
)
|
||||
|
||||
quality_tier = models.CharField(
|
||||
max_length=20,
|
||||
choices=QUALITY_TIER_CHOICES,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="basic / quality / premium - for image models"
|
||||
)
|
||||
|
||||
# Model Limits
|
||||
max_tokens = models.IntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Model token limit"
|
||||
)
|
||||
|
||||
context_window = models.IntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Model context size"
|
||||
)
|
||||
|
||||
# Capabilities
|
||||
capabilities = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
help_text="Capabilities: vision, function_calling, json_mode, etc."
|
||||
)
|
||||
|
||||
# Timestamps
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
updated_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='ai_model_updates',
|
||||
help_text="Admin who last updated"
|
||||
)
|
||||
|
||||
# History tracking
|
||||
history = HistoricalRecords()
|
||||
@@ -865,7 +820,7 @@ class AIModelConfig(models.Model):
|
||||
db_table = 'igny8_ai_model_config'
|
||||
verbose_name = 'AI Model Configuration'
|
||||
verbose_name_plural = 'AI Model Configurations'
|
||||
ordering = ['model_type', 'sort_order', 'model_name']
|
||||
ordering = ['model_type', 'model_name']
|
||||
indexes = [
|
||||
models.Index(fields=['model_type', 'is_active']),
|
||||
models.Index(fields=['provider', 'is_active']),
|
||||
@@ -878,52 +833,26 @@ class AIModelConfig(models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
"""Ensure only one is_default per model_type"""
|
||||
if self.is_default:
|
||||
# Unset other defaults for same model_type
|
||||
AIModelConfig.objects.filter(
|
||||
model_type=self.model_type,
|
||||
is_default=True
|
||||
).exclude(pk=self.pk).update(is_default=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_cost_for_tokens(self, input_tokens, output_tokens):
|
||||
"""Calculate cost for text models based on token usage"""
|
||||
if self.model_type != 'text':
|
||||
raise ValueError("get_cost_for_tokens only applies to text models")
|
||||
|
||||
if not self.input_cost_per_1m or not self.output_cost_per_1m:
|
||||
raise ValueError(f"Model {self.model_name} missing cost_per_1m values")
|
||||
|
||||
cost = (
|
||||
(Decimal(input_tokens) * self.input_cost_per_1m) +
|
||||
(Decimal(output_tokens) * self.output_cost_per_1m)
|
||||
) / Decimal('1000000')
|
||||
|
||||
return cost
|
||||
@classmethod
|
||||
def get_default_text_model(cls):
|
||||
"""Get the default text generation model"""
|
||||
return cls.objects.filter(model_type='text', is_default=True, is_active=True).first()
|
||||
|
||||
def get_cost_for_images(self, num_images):
|
||||
"""Calculate cost for image models"""
|
||||
if self.model_type != 'image':
|
||||
raise ValueError("get_cost_for_images only applies to image models")
|
||||
|
||||
if not self.cost_per_image:
|
||||
raise ValueError(f"Model {self.model_name} missing cost_per_image")
|
||||
|
||||
return self.cost_per_image * Decimal(num_images)
|
||||
@classmethod
|
||||
def get_default_image_model(cls):
|
||||
"""Get the default image generation model"""
|
||||
return cls.objects.filter(model_type='image', is_default=True, is_active=True).first()
|
||||
|
||||
def validate_size(self, size):
|
||||
"""Check if size is valid for this image model"""
|
||||
if self.model_type != 'image':
|
||||
raise ValueError("validate_size only applies to image models")
|
||||
|
||||
if not self.valid_sizes:
|
||||
return True # No size restrictions
|
||||
|
||||
return size in self.valid_sizes
|
||||
|
||||
def get_display_with_pricing(self):
|
||||
"""For dropdowns: show model with pricing"""
|
||||
if self.model_type == 'text':
|
||||
return f"{self.display_name} - ${self.input_cost_per_1m}/${self.output_cost_per_1m} per 1M"
|
||||
elif self.model_type == 'image':
|
||||
return f"{self.display_name} - ${self.cost_per_image} per image"
|
||||
return self.display_name
|
||||
@classmethod
|
||||
def get_image_models_by_tier(cls):
|
||||
"""Get all active image models grouped by quality tier"""
|
||||
return cls.objects.filter(
|
||||
model_type='image',
|
||||
is_active=True
|
||||
).order_by('quality_tier', 'model_name')
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
Credit Service for managing credit transactions and deductions
|
||||
"""
|
||||
import math
|
||||
import logging
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from igny8_core.business.billing.models import CreditTransaction, CreditUsageLog
|
||||
@@ -8,10 +10,124 @@ 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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CreditService:
|
||||
"""Service for managing credits - Token-based only"""
|
||||
|
||||
@staticmethod
|
||||
def calculate_credits_for_image(model_name: str, num_images: int = 1) -> int:
|
||||
"""
|
||||
Calculate credits for image generation based on AIModelConfig.credits_per_image.
|
||||
|
||||
Args:
|
||||
model_name: The AI model name (e.g., 'dall-e-3', 'flux-1-1-pro')
|
||||
num_images: Number of images to generate
|
||||
|
||||
Returns:
|
||||
int: Credits required
|
||||
|
||||
Raises:
|
||||
CreditCalculationError: If model not found or has no credits_per_image
|
||||
"""
|
||||
from igny8_core.business.billing.models import AIModelConfig
|
||||
|
||||
try:
|
||||
model = AIModelConfig.objects.filter(
|
||||
model_name=model_name,
|
||||
is_active=True
|
||||
).first()
|
||||
|
||||
if not model:
|
||||
raise CreditCalculationError(f"Model {model_name} not found or inactive")
|
||||
|
||||
if model.credits_per_image is None:
|
||||
raise CreditCalculationError(
|
||||
f"Model {model_name} has no credits_per_image configured"
|
||||
)
|
||||
|
||||
credits = model.credits_per_image * num_images
|
||||
|
||||
logger.info(
|
||||
f"Calculated credits for {model_name}: "
|
||||
f"{num_images} images × {model.credits_per_image} = {credits} credits"
|
||||
)
|
||||
|
||||
return credits
|
||||
|
||||
except AIModelConfig.DoesNotExist:
|
||||
raise CreditCalculationError(f"Model {model_name} not found")
|
||||
|
||||
@staticmethod
|
||||
def calculate_credits_from_tokens_by_model(model_name: str, total_tokens: int) -> int:
|
||||
"""
|
||||
Calculate credits from token usage based on AIModelConfig.tokens_per_credit.
|
||||
|
||||
This is the model-specific version that uses the model's configured rate.
|
||||
For operation-based calculation, use calculate_credits_from_tokens().
|
||||
|
||||
Args:
|
||||
model_name: The AI model name (e.g., 'gpt-4o', 'claude-3-5-sonnet')
|
||||
total_tokens: Total tokens used (input + output)
|
||||
|
||||
Returns:
|
||||
int: Credits required (minimum 1)
|
||||
|
||||
Raises:
|
||||
CreditCalculationError: If model not found
|
||||
"""
|
||||
from igny8_core.business.billing.models import AIModelConfig, BillingConfiguration
|
||||
|
||||
try:
|
||||
model = AIModelConfig.objects.filter(
|
||||
model_name=model_name,
|
||||
is_active=True
|
||||
).first()
|
||||
|
||||
if model and model.tokens_per_credit:
|
||||
tokens_per_credit = model.tokens_per_credit
|
||||
else:
|
||||
# Fallback to global default
|
||||
billing_config = BillingConfiguration.get_config()
|
||||
tokens_per_credit = billing_config.default_tokens_per_credit
|
||||
logger.info(
|
||||
f"Model {model_name} has no tokens_per_credit, "
|
||||
f"using default: {tokens_per_credit}"
|
||||
)
|
||||
|
||||
if tokens_per_credit <= 0:
|
||||
raise CreditCalculationError(
|
||||
f"Invalid tokens_per_credit for {model_name}: {tokens_per_credit}"
|
||||
)
|
||||
|
||||
# Get rounding mode
|
||||
billing_config = BillingConfiguration.get_config()
|
||||
rounding_mode = billing_config.credit_rounding_mode
|
||||
|
||||
credits_float = total_tokens / tokens_per_credit
|
||||
|
||||
if rounding_mode == 'up':
|
||||
credits = math.ceil(credits_float)
|
||||
elif rounding_mode == 'down':
|
||||
credits = math.floor(credits_float)
|
||||
else: # nearest
|
||||
credits = round(credits_float)
|
||||
|
||||
# Minimum 1 credit
|
||||
credits = max(credits, 1)
|
||||
|
||||
logger.info(
|
||||
f"Calculated credits for {model_name}: "
|
||||
f"{total_tokens} tokens ÷ {tokens_per_credit} = {credits} credits"
|
||||
)
|
||||
|
||||
return credits
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating credits for {model_name}: {e}")
|
||||
raise CreditCalculationError(f"Error calculating credits: {e}")
|
||||
|
||||
@staticmethod
|
||||
def calculate_credits_from_tokens(operation_type, tokens_input, tokens_output):
|
||||
"""
|
||||
@@ -323,4 +439,56 @@ class CreditService:
|
||||
)
|
||||
|
||||
return account.credits
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def deduct_credits_for_image(
|
||||
account,
|
||||
model_name: str,
|
||||
num_images: int = 1,
|
||||
description: str = None,
|
||||
metadata: dict = None,
|
||||
cost_usd: float = None,
|
||||
related_object_type: str = None,
|
||||
related_object_id: int = None
|
||||
):
|
||||
"""
|
||||
Deduct credits for image generation based on model's credits_per_image.
|
||||
|
||||
Args:
|
||||
account: Account instance
|
||||
model_name: AI model used (e.g., 'dall-e-3', 'flux-1-1-pro')
|
||||
num_images: Number of images generated
|
||||
description: Optional description
|
||||
metadata: Optional metadata dict
|
||||
cost_usd: Optional cost in USD
|
||||
related_object_type: Optional related object type
|
||||
related_object_id: Optional related object ID
|
||||
|
||||
Returns:
|
||||
int: New credit balance
|
||||
"""
|
||||
credits_required = CreditService.calculate_credits_for_image(model_name, num_images)
|
||||
|
||||
if account.credits < credits_required:
|
||||
raise InsufficientCreditsError(
|
||||
f"Insufficient credits. Required: {credits_required}, Available: {account.credits}"
|
||||
)
|
||||
|
||||
if not description:
|
||||
description = f"Image generation: {num_images} images with {model_name} = {credits_required} credits"
|
||||
|
||||
return CreditService.deduct_credits(
|
||||
account=account,
|
||||
amount=credits_required,
|
||||
operation_type='image_generation',
|
||||
description=description,
|
||||
metadata=metadata,
|
||||
cost_usd=cost_usd,
|
||||
model_used=model_name,
|
||||
tokens_input=None,
|
||||
tokens_output=None,
|
||||
related_object_type=related_object_type,
|
||||
related_object_id=related_object_id
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user