django admin Groups reorg, Frontend udpates for site settings, #Migration runs

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-05 01:21:52 +00:00
parent 6e30d2d4e8
commit dc7a459ebb
39 changed files with 3142 additions and 1589 deletions

View File

@@ -552,19 +552,18 @@ class AccountPaymentMethodAdmin(AccountAdminMixin, Igny8ModelAdmin):
@admin.register(CreditCostConfig)
class CreditCostConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
"""
Admin for Credit Cost Configuration.
Per final-model-schemas.md - Fixed credits per operation type.
"""
list_display = [
'operation_type',
'display_name',
'tokens_per_credit_display',
'price_per_credit_usd',
'min_credits',
'is_active',
'cost_change_indicator',
'updated_at',
'updated_by'
'base_credits_display',
'is_active_icon',
]
list_filter = ['is_active', 'updated_at']
list_filter = ['is_active']
search_fields = ['operation_type', 'display_name', 'description']
actions = ['bulk_activate', 'bulk_deactivate']
@@ -572,60 +571,30 @@ class CreditCostConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
('Operation', {
'fields': ('operation_type', 'display_name', 'description')
}),
('Token-to-Credit Configuration', {
'fields': ('tokens_per_credit', 'min_credits', 'price_per_credit_usd', 'is_active'),
'description': 'Configure how tokens are converted to credits for this operation'
}),
('Audit Trail', {
'fields': ('previous_tokens_per_credit', 'updated_by', 'created_at', 'updated_at'),
'classes': ('collapse',)
('Credits', {
'fields': ('base_credits', 'is_active'),
'description': 'Fixed credits charged per operation'
}),
)
readonly_fields = ['created_at', 'updated_at', 'previous_tokens_per_credit']
def tokens_per_credit_display(self, obj):
"""Show token ratio with color coding"""
if obj.tokens_per_credit <= 50:
color = 'red' # Expensive (low tokens per credit)
elif obj.tokens_per_credit <= 100:
color = 'orange'
else:
color = 'green' # Cheap (high tokens per credit)
def base_credits_display(self, obj):
"""Show base credits with formatting"""
return format_html(
'<span style="color: {}; font-weight: bold;">{} tokens/credit</span>',
color,
obj.tokens_per_credit
'<span style="font-weight: bold;">{} credits</span>',
obj.base_credits
)
tokens_per_credit_display.short_description = 'Token Ratio'
base_credits_display.short_description = 'Credits'
def cost_change_indicator(self, obj):
"""Show if token ratio changed recently"""
if obj.previous_tokens_per_credit is not None:
if obj.tokens_per_credit < obj.previous_tokens_per_credit:
icon = '📈' # More expensive (fewer tokens per credit)
color = 'red'
elif obj.tokens_per_credit > obj.previous_tokens_per_credit:
icon = '📉' # Cheaper (more tokens per credit)
color = 'green'
else:
icon = '➡️' # Same
color = 'gray'
def is_active_icon(self, obj):
"""Active status icon"""
if obj.is_active:
return format_html(
'{} <span style="color: {};">({}{})</span>',
icon,
color,
obj.previous_tokens_per_credit,
obj.tokens_per_credit
'<span style="color: green; font-size: 18px;" title="Active">●</span>'
)
return ''
cost_change_indicator.short_description = 'Recent Change'
def save_model(self, request, obj, form, change):
"""Track who made the change"""
obj.updated_by = request.user
super().save_model(request, obj, form, change)
return format_html(
'<span style="color: red; font-size: 18px;" title="Inactive">●</span>'
)
is_active_icon.short_description = 'Active'
@admin.action(description='Activate selected configurations')
def bulk_activate(self, request, queryset):
@@ -763,67 +732,60 @@ class BillingConfigurationAdmin(Igny8ModelAdmin):
@admin.register(AIModelConfig)
class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
"""
Admin for AI Model Configuration - Database-driven model pricing
Replaces hardcoded MODEL_RATES and IMAGE_MODEL_RATES
Admin for AI Model Configuration - Single Source of Truth for Models.
Per final-model-schemas.md
"""
list_display = [
'model_name',
'display_name_short',
'model_type_badge',
'provider_badge',
'pricing_display',
'credit_display',
'quality_tier',
'is_active_icon',
'is_default_icon',
'sort_order',
'updated_at',
]
list_filter = [
'model_type',
'provider',
'quality_tier',
'is_active',
'is_default',
'supports_json_mode',
'supports_vision',
'supports_function_calling',
]
search_fields = ['model_name', 'display_name', 'description']
search_fields = ['model_name', 'display_name']
ordering = ['model_type', 'sort_order', 'model_name']
ordering = ['model_type', 'model_name']
readonly_fields = ['created_at', 'updated_at', 'updated_by']
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Basic Information', {
'fields': ('model_name', 'display_name', 'model_type', 'provider', 'description'),
'description': 'Core model identification and classification'
'fields': ('model_name', 'model_type', 'provider', 'display_name'),
'description': 'Core model identification'
}),
('Text Model Pricing', {
'fields': ('input_cost_per_1m', 'output_cost_per_1m', 'context_window', 'max_output_tokens'),
'description': 'Pricing and limits for TEXT models only (leave blank for image models)',
'fields': ('cost_per_1k_input', 'cost_per_1k_output', 'tokens_per_credit', 'max_tokens', 'context_window'),
'description': 'For TEXT models only',
'classes': ('collapse',)
}),
('Image Model Pricing', {
'fields': ('cost_per_image', 'valid_sizes'),
'description': 'Pricing and configuration for IMAGE models only (leave blank for text models)',
'fields': ('credits_per_image', 'quality_tier'),
'description': 'For IMAGE models only',
'classes': ('collapse',)
}),
('Capabilities', {
'fields': ('supports_json_mode', 'supports_vision', 'supports_function_calling'),
'description': 'Model features and capabilities'
}),
('Status & Display', {
'fields': ('is_active', 'is_default', 'sort_order'),
'description': 'Control model availability and ordering in dropdowns'
}),
('Lifecycle', {
'fields': ('release_date', 'deprecation_date'),
'description': 'Model release and deprecation dates',
'fields': ('capabilities',),
'description': 'JSON: vision, function_calling, json_mode, etc.',
'classes': ('collapse',)
}),
('Audit Trail', {
'fields': ('created_at', 'updated_at', 'updated_by'),
('Status', {
'fields': ('is_active', 'is_default'),
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
@@ -831,8 +793,8 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
# Custom display methods
def display_name_short(self, obj):
"""Truncated display name for list view"""
if len(obj.display_name) > 50:
return obj.display_name[:47] + '...'
if len(obj.display_name) > 40:
return obj.display_name[:37] + '...'
return obj.display_name
display_name_short.short_description = 'Display Name'
@@ -841,7 +803,6 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
colors = {
'text': '#3498db', # Blue
'image': '#e74c3c', # Red
'embedding': '#2ecc71', # Green
}
color = colors.get(obj.model_type, '#95a5a6')
return format_html(
@@ -855,10 +816,10 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
def provider_badge(self, obj):
"""Colored badge for provider"""
colors = {
'openai': '#10a37f', # OpenAI green
'anthropic': '#d97757', # Anthropic orange
'runware': '#6366f1', # Purple
'google': '#4285f4', # Google blue
'openai': '#10a37f',
'anthropic': '#d97757',
'runware': '#6366f1',
'google': '#4285f4',
}
color = colors.get(obj.provider, '#95a5a6')
return format_html(
@@ -869,23 +830,20 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
)
provider_badge.short_description = 'Provider'
def pricing_display(self, obj):
"""Format pricing based on model type"""
if obj.model_type == 'text':
def credit_display(self, obj):
"""Format credit info based on model type"""
if obj.model_type == 'text' and obj.tokens_per_credit:
return format_html(
'<span style="color: #2c3e50; font-family: monospace;">'
'${} / ${} per 1M</span>',
obj.input_cost_per_1m,
obj.output_cost_per_1m
'<span style="font-family: monospace;">{} tokens/credit</span>',
obj.tokens_per_credit
)
elif obj.model_type == 'image':
elif obj.model_type == 'image' and obj.credits_per_image:
return format_html(
'<span style="color: #2c3e50; font-family: monospace;">'
'${} per image</span>',
obj.cost_per_image
'<span style="font-family: monospace;">{} credits/image</span>',
obj.credits_per_image
)
return '-'
pricing_display.short_description = 'Pricing'
credit_display.short_description = 'Credits'
def is_active_icon(self, obj):
"""Active status icon"""
@@ -915,41 +873,27 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
def bulk_activate(self, request, queryset):
"""Enable selected models"""
count = queryset.update(is_active=True)
self.message_user(
request,
f'{count} model(s) activated successfully.',
messages.SUCCESS
)
self.message_user(request, f'{count} model(s) activated.', messages.SUCCESS)
bulk_activate.short_description = 'Activate selected models'
def bulk_deactivate(self, request, queryset):
"""Disable selected models"""
count = queryset.update(is_active=False)
self.message_user(
request,
f'{count} model(s) deactivated successfully.',
messages.WARNING
)
self.message_user(request, f'{count} model(s) deactivated.', messages.WARNING)
bulk_deactivate.short_description = 'Deactivate selected models'
def set_as_default(self, request, queryset):
"""Set one model as default for its type"""
if queryset.count() != 1:
self.message_user(
request,
'Please select exactly one model to set as default.',
messages.ERROR
)
self.message_user(request, 'Select exactly one model.', messages.ERROR)
return
model = queryset.first()
# Unset other defaults for same type
AIModelConfig.objects.filter(
model_type=model.model_type,
is_default=True
).exclude(pk=model.pk).update(is_default=False)
# Set this as default
model.is_default = True
model.save()
@@ -958,9 +902,4 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
f'{model.model_name} is now the default {model.get_model_type_display()} model.',
messages.SUCCESS
)
set_as_default.short_description = 'Set as default model (for its type)'
def save_model(self, request, obj, form, change):
"""Track who made the change"""
obj.updated_by = request.user
super().save_model(request, obj, form, change)
set_as_default.short_description = 'Set as default model'

View File

@@ -0,0 +1,43 @@
# Generated by Django 5.2.9 on 2026-01-04 06:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billing', '0024_update_image_models_v2'),
]
operations = [
migrations.AddField(
model_name='aimodelconfig',
name='credits_per_image',
field=models.IntegerField(blank=True, help_text='Fixed credits per image generated. For image models only. (e.g., 1, 5, 15)', null=True),
),
migrations.AddField(
model_name='aimodelconfig',
name='quality_tier',
field=models.CharField(blank=True, choices=[('basic', 'Basic'), ('quality', 'Quality'), ('premium', 'Premium')], help_text='Quality tier for frontend UI display (Basic/Quality/Premium). For image models.', max_length=20, null=True),
),
migrations.AddField(
model_name='aimodelconfig',
name='tokens_per_credit',
field=models.IntegerField(blank=True, help_text='Number of tokens that equal 1 credit. For text models only. (e.g., 1000, 10000)', null=True),
),
migrations.AddField(
model_name='historicalaimodelconfig',
name='credits_per_image',
field=models.IntegerField(blank=True, help_text='Fixed credits per image generated. For image models only. (e.g., 1, 5, 15)', null=True),
),
migrations.AddField(
model_name='historicalaimodelconfig',
name='quality_tier',
field=models.CharField(blank=True, choices=[('basic', 'Basic'), ('quality', 'Quality'), ('premium', 'Premium')], help_text='Quality tier for frontend UI display (Basic/Quality/Premium). For image models.', max_length=20, null=True),
),
migrations.AddField(
model_name='historicalaimodelconfig',
name='tokens_per_credit',
field=models.IntegerField(blank=True, help_text='Number of tokens that equal 1 credit. For text models only. (e.g., 1000, 10000)', null=True),
),
]

View File

@@ -0,0 +1,63 @@
# Generated manually for data migration
from django.db import migrations
def populate_aimodel_credit_fields(apps, schema_editor):
"""
Populate credit calculation fields in AIModelConfig.
- Text models: tokens_per_credit (how many tokens = 1 credit)
- Image models: credits_per_image (fixed credits per image) + quality_tier
"""
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
# Text models: tokens_per_credit
text_model_credits = {
'gpt-4o-mini': 10000, # Cheap model: 10k tokens = 1 credit
'gpt-4o': 1000, # Premium model: 1k tokens = 1 credit
'gpt-5.1': 1000, # Default model: 1k tokens = 1 credit
'gpt-5.2': 1000, # Future model
'gpt-4.1': 1000, # Legacy
'gpt-4-turbo-preview': 500, # Expensive
}
for model_name, tokens_per_credit in text_model_credits.items():
AIModelConfig.objects.filter(
model_name=model_name,
model_type='text'
).update(tokens_per_credit=tokens_per_credit)
# Image models: credits_per_image + quality_tier
image_model_credits = {
'runware:97@1': {'credits_per_image': 1, 'quality_tier': 'basic'}, # Basic - cheap
'dall-e-3': {'credits_per_image': 5, 'quality_tier': 'quality'}, # Quality - mid
'google:4@2': {'credits_per_image': 15, 'quality_tier': 'premium'}, # Premium - expensive
'dall-e-2': {'credits_per_image': 2, 'quality_tier': 'basic'}, # Legacy
}
for model_name, credits_data in image_model_credits.items():
AIModelConfig.objects.filter(
model_name=model_name,
model_type='image'
).update(**credits_data)
def reverse_migration(apps, schema_editor):
"""Clear credit fields"""
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
AIModelConfig.objects.all().update(
tokens_per_credit=None,
credits_per_image=None,
quality_tier=None
)
class Migration(migrations.Migration):
dependencies = [
('billing', '0025_add_aimodel_credit_fields'),
]
operations = [
migrations.RunPython(populate_aimodel_credit_fields, reverse_migration),
]

View File

@@ -0,0 +1,356 @@
# Generated by Django 5.2.9 on 2026-01-04 10:40
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billing', '0026_populate_aimodel_credits'),
]
operations = [
migrations.AlterModelOptions(
name='aimodelconfig',
options={'ordering': ['model_type', 'model_name'], 'verbose_name': 'AI Model Configuration', 'verbose_name_plural': 'AI Model Configurations'},
),
migrations.RemoveField(
model_name='aimodelconfig',
name='cost_per_image',
),
migrations.RemoveField(
model_name='aimodelconfig',
name='deprecation_date',
),
migrations.RemoveField(
model_name='aimodelconfig',
name='description',
),
migrations.RemoveField(
model_name='aimodelconfig',
name='input_cost_per_1m',
),
migrations.RemoveField(
model_name='aimodelconfig',
name='max_output_tokens',
),
migrations.RemoveField(
model_name='aimodelconfig',
name='output_cost_per_1m',
),
migrations.RemoveField(
model_name='aimodelconfig',
name='release_date',
),
migrations.RemoveField(
model_name='aimodelconfig',
name='sort_order',
),
migrations.RemoveField(
model_name='aimodelconfig',
name='supports_function_calling',
),
migrations.RemoveField(
model_name='aimodelconfig',
name='supports_json_mode',
),
migrations.RemoveField(
model_name='aimodelconfig',
name='supports_vision',
),
migrations.RemoveField(
model_name='aimodelconfig',
name='updated_by',
),
migrations.RemoveField(
model_name='aimodelconfig',
name='valid_sizes',
),
migrations.RemoveField(
model_name='creditcostconfig',
name='created_at',
),
migrations.RemoveField(
model_name='creditcostconfig',
name='id',
),
migrations.RemoveField(
model_name='creditcostconfig',
name='min_credits',
),
migrations.RemoveField(
model_name='creditcostconfig',
name='previous_tokens_per_credit',
),
migrations.RemoveField(
model_name='creditcostconfig',
name='price_per_credit_usd',
),
migrations.RemoveField(
model_name='creditcostconfig',
name='tokens_per_credit',
),
migrations.RemoveField(
model_name='creditcostconfig',
name='updated_at',
),
migrations.RemoveField(
model_name='creditcostconfig',
name='updated_by',
),
migrations.RemoveField(
model_name='historicalaimodelconfig',
name='cost_per_image',
),
migrations.RemoveField(
model_name='historicalaimodelconfig',
name='deprecation_date',
),
migrations.RemoveField(
model_name='historicalaimodelconfig',
name='description',
),
migrations.RemoveField(
model_name='historicalaimodelconfig',
name='input_cost_per_1m',
),
migrations.RemoveField(
model_name='historicalaimodelconfig',
name='max_output_tokens',
),
migrations.RemoveField(
model_name='historicalaimodelconfig',
name='output_cost_per_1m',
),
migrations.RemoveField(
model_name='historicalaimodelconfig',
name='release_date',
),
migrations.RemoveField(
model_name='historicalaimodelconfig',
name='sort_order',
),
migrations.RemoveField(
model_name='historicalaimodelconfig',
name='supports_function_calling',
),
migrations.RemoveField(
model_name='historicalaimodelconfig',
name='supports_json_mode',
),
migrations.RemoveField(
model_name='historicalaimodelconfig',
name='supports_vision',
),
migrations.RemoveField(
model_name='historicalaimodelconfig',
name='updated_by',
),
migrations.RemoveField(
model_name='historicalaimodelconfig',
name='valid_sizes',
),
migrations.RemoveField(
model_name='historicalcreditcostconfig',
name='created_at',
),
migrations.RemoveField(
model_name='historicalcreditcostconfig',
name='id',
),
migrations.RemoveField(
model_name='historicalcreditcostconfig',
name='min_credits',
),
migrations.RemoveField(
model_name='historicalcreditcostconfig',
name='previous_tokens_per_credit',
),
migrations.RemoveField(
model_name='historicalcreditcostconfig',
name='price_per_credit_usd',
),
migrations.RemoveField(
model_name='historicalcreditcostconfig',
name='tokens_per_credit',
),
migrations.RemoveField(
model_name='historicalcreditcostconfig',
name='updated_at',
),
migrations.RemoveField(
model_name='historicalcreditcostconfig',
name='updated_by',
),
migrations.AddField(
model_name='aimodelconfig',
name='capabilities',
field=models.JSONField(blank=True, default=dict, help_text='Capabilities: vision, function_calling, json_mode, etc.'),
),
migrations.AddField(
model_name='aimodelconfig',
name='cost_per_1k_input',
field=models.DecimalField(blank=True, decimal_places=6, help_text='Provider cost per 1K input tokens (USD) - text models', max_digits=10, null=True),
),
migrations.AddField(
model_name='aimodelconfig',
name='cost_per_1k_output',
field=models.DecimalField(blank=True, decimal_places=6, help_text='Provider cost per 1K output tokens (USD) - text models', max_digits=10, null=True),
),
migrations.AddField(
model_name='aimodelconfig',
name='max_tokens',
field=models.IntegerField(blank=True, help_text='Model token limit', null=True),
),
migrations.AddField(
model_name='creditcostconfig',
name='base_credits',
field=models.IntegerField(default=1, help_text='Fixed credits per operation', validators=[django.core.validators.MinValueValidator(0)]),
),
migrations.AddField(
model_name='historicalaimodelconfig',
name='capabilities',
field=models.JSONField(blank=True, default=dict, help_text='Capabilities: vision, function_calling, json_mode, etc.'),
),
migrations.AddField(
model_name='historicalaimodelconfig',
name='cost_per_1k_input',
field=models.DecimalField(blank=True, decimal_places=6, help_text='Provider cost per 1K input tokens (USD) - text models', max_digits=10, null=True),
),
migrations.AddField(
model_name='historicalaimodelconfig',
name='cost_per_1k_output',
field=models.DecimalField(blank=True, decimal_places=6, help_text='Provider cost per 1K output tokens (USD) - text models', max_digits=10, null=True),
),
migrations.AddField(
model_name='historicalaimodelconfig',
name='max_tokens',
field=models.IntegerField(blank=True, help_text='Model token limit', null=True),
),
migrations.AddField(
model_name='historicalcreditcostconfig',
name='base_credits',
field=models.IntegerField(default=1, help_text='Fixed credits per operation', validators=[django.core.validators.MinValueValidator(0)]),
),
migrations.AlterField(
model_name='aimodelconfig',
name='context_window',
field=models.IntegerField(blank=True, help_text='Model context size', null=True),
),
migrations.AlterField(
model_name='aimodelconfig',
name='credits_per_image',
field=models.IntegerField(blank=True, help_text='Image: credits per image (e.g., 1, 5, 15)', null=True),
),
migrations.AlterField(
model_name='aimodelconfig',
name='display_name',
field=models.CharField(help_text='Human-readable name', max_length=200),
),
migrations.AlterField(
model_name='aimodelconfig',
name='is_active',
field=models.BooleanField(db_index=True, default=True, help_text='Enable/disable'),
),
migrations.AlterField(
model_name='aimodelconfig',
name='is_default',
field=models.BooleanField(db_index=True, default=False, help_text='One default per type'),
),
migrations.AlterField(
model_name='aimodelconfig',
name='model_name',
field=models.CharField(db_index=True, help_text="Model identifier (e.g., 'gpt-5.1', 'dall-e-3', 'runware:97@1')", max_length=100, unique=True),
),
migrations.AlterField(
model_name='aimodelconfig',
name='model_type',
field=models.CharField(choices=[('text', 'Text Generation'), ('image', 'Image Generation')], db_index=True, help_text='text / image', max_length=20),
),
migrations.AlterField(
model_name='aimodelconfig',
name='provider',
field=models.CharField(choices=[('openai', 'OpenAI'), ('anthropic', 'Anthropic'), ('runware', 'Runware'), ('google', 'Google')], db_index=True, help_text='Links to IntegrationProvider', max_length=50),
),
migrations.AlterField(
model_name='aimodelconfig',
name='quality_tier',
field=models.CharField(blank=True, choices=[('basic', 'Basic'), ('quality', 'Quality'), ('premium', 'Premium')], help_text='basic / quality / premium - for image models', max_length=20, null=True),
),
migrations.AlterField(
model_name='aimodelconfig',
name='tokens_per_credit',
field=models.IntegerField(blank=True, help_text='Text: tokens per 1 credit (e.g., 1000, 10000)', null=True),
),
migrations.AlterField(
model_name='creditcostconfig',
name='description',
field=models.TextField(blank=True, help_text='Admin notes about this operation'),
),
migrations.AlterField(
model_name='creditcostconfig',
name='operation_type',
field=models.CharField(help_text="Unique operation ID (e.g., 'article_generation', 'image_generation')", max_length=50, primary_key=True, serialize=False, unique=True),
),
migrations.AlterField(
model_name='historicalaimodelconfig',
name='context_window',
field=models.IntegerField(blank=True, help_text='Model context size', null=True),
),
migrations.AlterField(
model_name='historicalaimodelconfig',
name='credits_per_image',
field=models.IntegerField(blank=True, help_text='Image: credits per image (e.g., 1, 5, 15)', null=True),
),
migrations.AlterField(
model_name='historicalaimodelconfig',
name='display_name',
field=models.CharField(help_text='Human-readable name', max_length=200),
),
migrations.AlterField(
model_name='historicalaimodelconfig',
name='is_active',
field=models.BooleanField(db_index=True, default=True, help_text='Enable/disable'),
),
migrations.AlterField(
model_name='historicalaimodelconfig',
name='is_default',
field=models.BooleanField(db_index=True, default=False, help_text='One default per type'),
),
migrations.AlterField(
model_name='historicalaimodelconfig',
name='model_name',
field=models.CharField(db_index=True, help_text="Model identifier (e.g., 'gpt-5.1', 'dall-e-3', 'runware:97@1')", max_length=100),
),
migrations.AlterField(
model_name='historicalaimodelconfig',
name='model_type',
field=models.CharField(choices=[('text', 'Text Generation'), ('image', 'Image Generation')], db_index=True, help_text='text / image', max_length=20),
),
migrations.AlterField(
model_name='historicalaimodelconfig',
name='provider',
field=models.CharField(choices=[('openai', 'OpenAI'), ('anthropic', 'Anthropic'), ('runware', 'Runware'), ('google', 'Google')], db_index=True, help_text='Links to IntegrationProvider', max_length=50),
),
migrations.AlterField(
model_name='historicalaimodelconfig',
name='quality_tier',
field=models.CharField(blank=True, choices=[('basic', 'Basic'), ('quality', 'Quality'), ('premium', 'Premium')], help_text='basic / quality / premium - for image models', max_length=20, null=True),
),
migrations.AlterField(
model_name='historicalaimodelconfig',
name='tokens_per_credit',
field=models.IntegerField(blank=True, help_text='Text: tokens per 1 credit (e.g., 1000, 10000)', null=True),
),
migrations.AlterField(
model_name='historicalcreditcostconfig',
name='description',
field=models.TextField(blank=True, help_text='Admin notes about this operation'),
),
migrations.AlterField(
model_name='historicalcreditcostconfig',
name='operation_type',
field=models.CharField(db_index=True, help_text="Unique operation ID (e.g., 'article_generation', 'image_generation')", max_length=50),
),
]

View File

@@ -255,6 +255,23 @@ class AIModelConfigSerializer(serializers.Serializer):
)
valid_sizes = serializers.ListField(read_only=True, allow_null=True)
# Credit calculation fields (NEW)
credits_per_image = serializers.IntegerField(
read_only=True,
allow_null=True,
help_text="Credits charged per image generation"
)
tokens_per_credit = serializers.IntegerField(
read_only=True,
allow_null=True,
help_text="Tokens per credit for text models"
)
quality_tier = serializers.CharField(
read_only=True,
allow_null=True,
help_text="Quality tier: basic, quality, or premium"
)
# Capabilities
supports_json_mode = serializers.BooleanField(read_only=True)
supports_vision = serializers.BooleanField(read_only=True)

View File

@@ -789,7 +789,7 @@ class AIModelConfigViewSet(viewsets.ReadOnlyModelViewSet):
is_default_bool = is_default.lower() in ['true', '1', 'yes']
queryset = queryset.filter(is_default=is_default_bool)
return queryset.order_by('model_type', 'sort_order', 'model_name')
return queryset.order_by('model_type', 'model_name')
def get_serializer_class(self):
"""Return serializer class"""