Merge branch 'main' of https://git.igny8.com/salman/igny8
This commit is contained in:
@@ -8,16 +8,17 @@ from unfold.admin import ModelAdmin
|
||||
from simple_history.admin import SimpleHistoryAdmin
|
||||
from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin
|
||||
from igny8_core.business.billing.models import (
|
||||
AIModelConfig,
|
||||
CreditCostConfig,
|
||||
BillingConfiguration,
|
||||
Invoice,
|
||||
Payment,
|
||||
CreditPackage,
|
||||
PaymentMethodConfig,
|
||||
PlanLimitUsage,
|
||||
AIModelConfig,
|
||||
)
|
||||
from .models import CreditTransaction, CreditUsageLog, AccountPaymentMethod
|
||||
from import_export.admin import ExportMixin
|
||||
from import_export.admin import ExportMixin, ImportExportMixin
|
||||
from import_export import resources
|
||||
from rangefilter.filters import DateRangeFilter
|
||||
|
||||
@@ -50,43 +51,21 @@ class CreditTransactionAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
get_account_display.short_description = 'Account'
|
||||
|
||||
|
||||
@admin.register(AIModelConfig)
|
||||
class AIModelConfigAdmin(Igny8ModelAdmin):
|
||||
list_display = ['display_name', 'model_name', 'provider', 'model_type', 'tokens_per_credit', 'cost_per_1k_input_tokens', 'cost_per_1k_output_tokens', 'is_active', 'is_default']
|
||||
list_filter = ['provider', 'model_type', 'is_active', 'is_default']
|
||||
search_fields = ['model_name', 'display_name', 'description']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
fieldsets = (
|
||||
('Model Information', {
|
||||
'fields': ('model_name', 'display_name', 'description', 'provider', 'model_type')
|
||||
}),
|
||||
('Pricing', {
|
||||
'fields': ('cost_per_1k_input_tokens', 'cost_per_1k_output_tokens', 'tokens_per_credit')
|
||||
}),
|
||||
('Status', {
|
||||
'fields': ('is_active', 'is_default')
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
# If setting as default, unset other defaults of same type
|
||||
if obj.is_default:
|
||||
AIModelConfig.objects.filter(
|
||||
model_type=obj.model_type,
|
||||
is_default=True
|
||||
).exclude(pk=obj.pk).update(is_default=False)
|
||||
super().save_model(request, obj, form, change)
|
||||
class CreditUsageLogResource(resources.ModelResource):
|
||||
"""Resource class for exporting Credit Usage Logs"""
|
||||
class Meta:
|
||||
model = CreditUsageLog
|
||||
fields = ('id', 'account__name', 'operation_type', 'credits_used', 'cost_usd',
|
||||
'model_used', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(CreditUsageLog)
|
||||
class CreditUsageLogAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
list_display = ['id', 'account', 'operation_type', 'credits_used', 'cost_usd', 'model_config', 'created_at']
|
||||
list_filter = ['operation_type', 'created_at', 'account', 'model_config']
|
||||
search_fields = ['account__name', 'model_name']
|
||||
class CreditUsageLogAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = CreditUsageLogResource
|
||||
list_display = ['id', 'account', 'operation_type', 'credits_used', 'cost_usd', 'model_used', 'created_at']
|
||||
list_filter = ['operation_type', 'created_at', 'account', 'model_used']
|
||||
search_fields = ['account__name', 'model_used']
|
||||
readonly_fields = ['created_at']
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
@@ -100,8 +79,18 @@ class CreditUsageLogAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
get_account_display.short_description = 'Account'
|
||||
|
||||
|
||||
class InvoiceResource(resources.ModelResource):
|
||||
"""Resource class for exporting Invoices"""
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = ('id', 'invoice_number', 'account__name', 'status', 'total', 'currency',
|
||||
'invoice_date', 'due_date', 'created_at', 'updated_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(Invoice)
|
||||
class InvoiceAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
class InvoiceAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = InvoiceResource
|
||||
list_display = [
|
||||
'invoice_number',
|
||||
'account',
|
||||
@@ -114,6 +103,56 @@ class InvoiceAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
list_filter = ['status', 'currency', 'invoice_date', 'account']
|
||||
search_fields = ['invoice_number', 'account__name']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = [
|
||||
'bulk_set_status_draft',
|
||||
'bulk_set_status_sent',
|
||||
'bulk_set_status_paid',
|
||||
'bulk_set_status_overdue',
|
||||
'bulk_set_status_cancelled',
|
||||
'bulk_send_reminders',
|
||||
]
|
||||
|
||||
def bulk_set_status_draft(self, request, queryset):
|
||||
"""Set selected invoices to draft status"""
|
||||
updated = queryset.update(status='draft')
|
||||
self.message_user(request, f'{updated} invoice(s) set to draft.', messages.SUCCESS)
|
||||
bulk_set_status_draft.short_description = 'Set status to Draft'
|
||||
|
||||
def bulk_set_status_sent(self, request, queryset):
|
||||
"""Set selected invoices to sent status"""
|
||||
updated = queryset.update(status='sent')
|
||||
self.message_user(request, f'{updated} invoice(s) set to sent.', messages.SUCCESS)
|
||||
bulk_set_status_sent.short_description = 'Set status to Sent'
|
||||
|
||||
def bulk_set_status_paid(self, request, queryset):
|
||||
"""Set selected invoices to paid status"""
|
||||
updated = queryset.update(status='paid')
|
||||
self.message_user(request, f'{updated} invoice(s) set to paid.', messages.SUCCESS)
|
||||
bulk_set_status_paid.short_description = 'Set status to Paid'
|
||||
|
||||
def bulk_set_status_overdue(self, request, queryset):
|
||||
"""Set selected invoices to overdue status"""
|
||||
updated = queryset.update(status='overdue')
|
||||
self.message_user(request, f'{updated} invoice(s) set to overdue.', messages.SUCCESS)
|
||||
bulk_set_status_overdue.short_description = 'Set status to Overdue'
|
||||
|
||||
def bulk_set_status_cancelled(self, request, queryset):
|
||||
"""Set selected invoices to cancelled status"""
|
||||
updated = queryset.update(status='cancelled')
|
||||
self.message_user(request, f'{updated} invoice(s) set to cancelled.', messages.SUCCESS)
|
||||
bulk_set_status_cancelled.short_description = 'Set status to Cancelled'
|
||||
|
||||
def bulk_send_reminders(self, request, queryset):
|
||||
"""Send reminder emails for selected invoices"""
|
||||
# TODO: Implement email sending logic when email service is configured
|
||||
unpaid = queryset.filter(status__in=['sent', 'overdue'])
|
||||
count = unpaid.count()
|
||||
self.message_user(
|
||||
request,
|
||||
f'{count} invoice reminder(s) queued for sending. (Email integration required)',
|
||||
messages.INFO
|
||||
)
|
||||
bulk_send_reminders.short_description = 'Send payment reminders'
|
||||
|
||||
|
||||
class PaymentResource(resources.ModelResource):
|
||||
@@ -160,7 +199,7 @@ class PaymentAdmin(ExportMixin, AccountAdminMixin, SimpleHistoryAdmin, Igny8Mode
|
||||
'manual_notes'
|
||||
]
|
||||
readonly_fields = ['created_at', 'updated_at', 'approved_at', 'processed_at', 'failed_at', 'refunded_at']
|
||||
actions = ['approve_payments', 'reject_payments']
|
||||
actions = ['approve_payments', 'reject_payments', 'bulk_refund']
|
||||
|
||||
fieldsets = (
|
||||
('Payment Info', {
|
||||
@@ -406,14 +445,71 @@ class PaymentAdmin(ExportMixin, AccountAdminMixin, SimpleHistoryAdmin, Igny8Mode
|
||||
self.message_user(request, f'Rejected {count} payment(s)')
|
||||
|
||||
reject_payments.short_description = 'Reject selected manual payments'
|
||||
|
||||
def bulk_refund(self, request, queryset):
|
||||
"""Refund selected payments"""
|
||||
from django.utils import timezone
|
||||
|
||||
# Only refund succeeded payments
|
||||
succeeded_payments = queryset.filter(status='succeeded')
|
||||
count = 0
|
||||
|
||||
for payment in succeeded_payments:
|
||||
# Mark as refunded
|
||||
payment.status = 'refunded'
|
||||
payment.refunded_at = timezone.now()
|
||||
payment.admin_notes = f'{payment.admin_notes or ""}\nBulk refunded by {request.user.email} on {timezone.now()}'
|
||||
payment.save()
|
||||
|
||||
# TODO: Process actual refund through payment gateway (Stripe/PayPal)
|
||||
# For now, just marking as refunded in database
|
||||
|
||||
count += 1
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
f'{count} payment(s) marked as refunded. Note: Actual gateway refunds need to be processed separately.',
|
||||
messages.WARNING
|
||||
)
|
||||
bulk_refund.short_description = 'Refund selected payments'
|
||||
|
||||
|
||||
class CreditPackageResource(resources.ModelResource):
|
||||
"""Resource class for importing/exporting Credit Packages"""
|
||||
class Meta:
|
||||
model = CreditPackage
|
||||
fields = ('id', 'name', 'slug', 'credits', 'price', 'discount_percentage',
|
||||
'is_active', 'is_featured', 'sort_order', 'created_at')
|
||||
export_order = fields
|
||||
import_id_fields = ('id',)
|
||||
skip_unchanged = True
|
||||
|
||||
|
||||
@admin.register(CreditPackage)
|
||||
class CreditPackageAdmin(Igny8ModelAdmin):
|
||||
class CreditPackageAdmin(ImportExportMixin, Igny8ModelAdmin):
|
||||
resource_class = CreditPackageResource
|
||||
list_display = ['name', 'slug', 'credits', 'price', 'discount_percentage', 'is_active', 'is_featured', 'sort_order']
|
||||
list_filter = ['is_active', 'is_featured']
|
||||
search_fields = ['name', 'slug']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = [
|
||||
'bulk_activate',
|
||||
'bulk_deactivate',
|
||||
]
|
||||
actions = [
|
||||
'bulk_activate',
|
||||
'bulk_deactivate',
|
||||
]
|
||||
|
||||
def bulk_activate(self, request, queryset):
|
||||
updated = queryset.update(is_active=True)
|
||||
self.message_user(request, f'{updated} credit package(s) activated.', messages.SUCCESS)
|
||||
bulk_activate.short_description = 'Activate selected packages'
|
||||
|
||||
def bulk_deactivate(self, request, queryset):
|
||||
updated = queryset.update(is_active=False)
|
||||
self.message_user(request, f'{updated} credit package(s) deactivated.', messages.SUCCESS)
|
||||
bulk_deactivate.short_description = 'Deactivate selected packages'
|
||||
|
||||
|
||||
@admin.register(PaymentMethodConfig)
|
||||
@@ -459,55 +555,57 @@ class CreditCostConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
list_display = [
|
||||
'operation_type',
|
||||
'display_name',
|
||||
'credits_cost_display',
|
||||
'unit',
|
||||
'tokens_per_credit_display',
|
||||
'price_per_credit_usd',
|
||||
'min_credits',
|
||||
'is_active',
|
||||
'cost_change_indicator',
|
||||
'updated_at',
|
||||
'updated_by'
|
||||
]
|
||||
|
||||
list_filter = ['is_active', 'unit', 'updated_at']
|
||||
list_filter = ['is_active', 'updated_at']
|
||||
search_fields = ['operation_type', 'display_name', 'description']
|
||||
|
||||
fieldsets = (
|
||||
('Operation', {
|
||||
'fields': ('operation_type', 'display_name', 'description')
|
||||
}),
|
||||
('Cost Configuration', {
|
||||
'fields': ('credits_cost', 'unit', 'is_active')
|
||||
('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_cost', 'updated_by', 'created_at', 'updated_at'),
|
||||
'fields': ('previous_tokens_per_credit', 'updated_by', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at', 'previous_cost']
|
||||
readonly_fields = ['created_at', 'updated_at', 'previous_tokens_per_credit']
|
||||
|
||||
def credits_cost_display(self, obj):
|
||||
"""Show cost with color coding"""
|
||||
if obj.credits_cost >= 20:
|
||||
color = 'red'
|
||||
elif obj.credits_cost >= 10:
|
||||
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'
|
||||
color = 'green' # Cheap (high tokens per credit)
|
||||
return format_html(
|
||||
'<span style="color: {}; font-weight: bold;">{} credits</span>',
|
||||
'<span style="color: {}; font-weight: bold;">{} tokens/credit</span>',
|
||||
color,
|
||||
obj.credits_cost
|
||||
obj.tokens_per_credit
|
||||
)
|
||||
credits_cost_display.short_description = 'Cost'
|
||||
tokens_per_credit_display.short_description = 'Token Ratio'
|
||||
|
||||
def cost_change_indicator(self, obj):
|
||||
"""Show if cost changed recently"""
|
||||
if obj.previous_cost is not None:
|
||||
if obj.credits_cost > obj.previous_cost:
|
||||
icon = '📈' # Increased
|
||||
"""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.credits_cost < obj.previous_cost:
|
||||
icon = '📉' # Decreased
|
||||
elif obj.tokens_per_credit > obj.previous_tokens_per_credit:
|
||||
icon = '📉' # Cheaper (more tokens per credit)
|
||||
color = 'green'
|
||||
else:
|
||||
icon = '➡️' # Same
|
||||
@@ -517,8 +615,8 @@ class CreditCostConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
'{} <span style="color: {};">({} → {})</span>',
|
||||
icon,
|
||||
color,
|
||||
obj.previous_cost,
|
||||
obj.credits_cost
|
||||
obj.previous_tokens_per_credit,
|
||||
obj.tokens_per_credit
|
||||
)
|
||||
return '—'
|
||||
cost_change_indicator.short_description = 'Recent Change'
|
||||
@@ -529,8 +627,18 @@ class CreditCostConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
class PlanLimitUsageResource(resources.ModelResource):
|
||||
"""Resource class for exporting Plan Limit Usage"""
|
||||
class Meta:
|
||||
model = PlanLimitUsage
|
||||
fields = ('id', 'account__name', 'limit_type', 'amount_used',
|
||||
'period_start', 'period_end', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(PlanLimitUsage)
|
||||
class PlanLimitUsageAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
class PlanLimitUsageAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = PlanLimitUsageResource
|
||||
"""Admin for tracking plan limit usage across billing periods"""
|
||||
list_display = [
|
||||
'account',
|
||||
@@ -548,6 +656,10 @@ class PlanLimitUsageAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
search_fields = ['account__name']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
date_hierarchy = 'period_start'
|
||||
actions = [
|
||||
'bulk_reset_usage',
|
||||
'bulk_delete_old_records',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Usage Info', {
|
||||
@@ -570,4 +682,272 @@ class PlanLimitUsageAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
"""Display billing period range"""
|
||||
return f"{obj.period_start} to {obj.period_end}"
|
||||
period_display.short_description = 'Billing Period'
|
||||
|
||||
def bulk_reset_usage(self, request, queryset):
|
||||
"""Reset usage counters to zero"""
|
||||
updated = queryset.update(amount_used=0)
|
||||
self.message_user(request, f'{updated} usage counter(s) reset to zero.', messages.SUCCESS)
|
||||
bulk_reset_usage.short_description = 'Reset usage counters'
|
||||
|
||||
def bulk_delete_old_records(self, request, queryset):
|
||||
"""Delete usage records older than 1 year"""
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
cutoff_date = timezone.now() - timedelta(days=365)
|
||||
old_records = queryset.filter(period_end__lt=cutoff_date)
|
||||
count = old_records.count()
|
||||
old_records.delete()
|
||||
self.message_user(request, f'{count} old usage record(s) deleted (older than 1 year).', messages.SUCCESS)
|
||||
bulk_delete_old_records.short_description = 'Delete old records (>1 year)'
|
||||
|
||||
|
||||
@admin.register(BillingConfiguration)
|
||||
class BillingConfigurationAdmin(Igny8ModelAdmin):
|
||||
"""Admin for global billing configuration (Singleton)"""
|
||||
list_display = [
|
||||
'id',
|
||||
'default_tokens_per_credit',
|
||||
'default_credit_price_usd',
|
||||
'credit_rounding_mode',
|
||||
'enable_token_based_reporting',
|
||||
'updated_at',
|
||||
'updated_by'
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Global Token-to-Credit Settings', {
|
||||
'fields': ('default_tokens_per_credit', 'default_credit_price_usd', 'credit_rounding_mode'),
|
||||
'description': 'These settings apply when no operation-specific config exists'
|
||||
}),
|
||||
('Reporting Settings', {
|
||||
'fields': ('enable_token_based_reporting',),
|
||||
'description': 'Control token-based reporting features'
|
||||
}),
|
||||
('Audit Trail', {
|
||||
'fields': ('updated_by', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['updated_at']
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Only allow one instance (singleton)"""
|
||||
from igny8_core.business.billing.models import BillingConfiguration
|
||||
return not BillingConfiguration.objects.exists()
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Prevent deletion of the singleton"""
|
||||
return False
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@admin.register(AIModelConfig)
|
||||
class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
"""
|
||||
Admin for AI Model Configuration - Database-driven model pricing
|
||||
Replaces hardcoded MODEL_RATES and IMAGE_MODEL_RATES
|
||||
"""
|
||||
list_display = [
|
||||
'model_name',
|
||||
'display_name_short',
|
||||
'model_type_badge',
|
||||
'provider_badge',
|
||||
'pricing_display',
|
||||
'is_active_icon',
|
||||
'is_default_icon',
|
||||
'sort_order',
|
||||
'updated_at',
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
'model_type',
|
||||
'provider',
|
||||
'is_active',
|
||||
'is_default',
|
||||
'supports_json_mode',
|
||||
'supports_vision',
|
||||
'supports_function_calling',
|
||||
]
|
||||
|
||||
search_fields = ['model_name', 'display_name', 'description']
|
||||
|
||||
ordering = ['model_type', 'sort_order', 'model_name']
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at', 'updated_by']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Information', {
|
||||
'fields': ('model_name', 'display_name', 'model_type', 'provider', 'description'),
|
||||
'description': 'Core model identification and classification'
|
||||
}),
|
||||
('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)',
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Image Model Pricing', {
|
||||
'fields': ('cost_per_image', 'valid_sizes'),
|
||||
'description': 'Pricing and configuration for IMAGE models only (leave blank for text models)',
|
||||
'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',
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Audit Trail', {
|
||||
'fields': ('created_at', 'updated_at', 'updated_by'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
# 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] + '...'
|
||||
return obj.display_name
|
||||
display_name_short.short_description = 'Display Name'
|
||||
|
||||
def model_type_badge(self, obj):
|
||||
"""Colored badge for model type"""
|
||||
colors = {
|
||||
'text': '#3498db', # Blue
|
||||
'image': '#e74c3c', # Red
|
||||
'embedding': '#2ecc71', # Green
|
||||
}
|
||||
color = colors.get(obj.model_type, '#95a5a6')
|
||||
return format_html(
|
||||
'<span style="background-color: {}; color: white; padding: 3px 10px; '
|
||||
'border-radius: 3px; font-weight: bold;">{}</span>',
|
||||
color,
|
||||
obj.get_model_type_display()
|
||||
)
|
||||
model_type_badge.short_description = 'Type'
|
||||
|
||||
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
|
||||
}
|
||||
color = colors.get(obj.provider, '#95a5a6')
|
||||
return format_html(
|
||||
'<span style="background-color: {}; color: white; padding: 3px 10px; '
|
||||
'border-radius: 3px; font-weight: bold;">{}</span>',
|
||||
color,
|
||||
obj.get_provider_display()
|
||||
)
|
||||
provider_badge.short_description = 'Provider'
|
||||
|
||||
def pricing_display(self, obj):
|
||||
"""Format pricing based on model type"""
|
||||
if obj.model_type == 'text':
|
||||
return format_html(
|
||||
'<span style="color: #2c3e50; font-family: monospace;">'
|
||||
'${} / ${} per 1M</span>',
|
||||
obj.input_cost_per_1m,
|
||||
obj.output_cost_per_1m
|
||||
)
|
||||
elif obj.model_type == 'image':
|
||||
return format_html(
|
||||
'<span style="color: #2c3e50; font-family: monospace;">'
|
||||
'${} per image</span>',
|
||||
obj.cost_per_image
|
||||
)
|
||||
return '-'
|
||||
pricing_display.short_description = 'Pricing'
|
||||
|
||||
def is_active_icon(self, obj):
|
||||
"""Active status icon"""
|
||||
if obj.is_active:
|
||||
return format_html(
|
||||
'<span style="color: green; font-size: 18px;" title="Active">●</span>'
|
||||
)
|
||||
return format_html(
|
||||
'<span style="color: red; font-size: 18px;" title="Inactive">●</span>'
|
||||
)
|
||||
is_active_icon.short_description = 'Active'
|
||||
|
||||
def is_default_icon(self, obj):
|
||||
"""Default status icon"""
|
||||
if obj.is_default:
|
||||
return format_html(
|
||||
'<span style="color: gold; font-size: 18px;" title="Default">★</span>'
|
||||
)
|
||||
return format_html(
|
||||
'<span style="color: #ddd; font-size: 18px;" title="Not Default">☆</span>'
|
||||
)
|
||||
is_default_icon.short_description = 'Default'
|
||||
|
||||
# Admin actions
|
||||
actions = ['bulk_activate', 'bulk_deactivate', 'set_as_default']
|
||||
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
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()
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-24 01:20
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import simple_history.models
|
||||
from decimal import Decimal
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def seed_ai_models(apps, schema_editor):
|
||||
"""Seed AIModelConfig with data from constants.py"""
|
||||
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
|
||||
|
||||
# Text Models (from MODEL_RATES in constants.py)
|
||||
text_models = [
|
||||
{
|
||||
'model_name': 'gpt-4o-mini',
|
||||
'display_name': 'GPT-4o mini - Fast & Affordable',
|
||||
'model_type': 'text',
|
||||
'provider': 'openai',
|
||||
'input_cost_per_1m': Decimal('0.1500'),
|
||||
'output_cost_per_1m': Decimal('0.6000'),
|
||||
'context_window': 128000,
|
||||
'max_output_tokens': 16000,
|
||||
'supports_json_mode': True,
|
||||
'supports_vision': False,
|
||||
'supports_function_calling': True,
|
||||
'is_active': True,
|
||||
'is_default': True, # Default text model
|
||||
'sort_order': 1,
|
||||
'description': 'Fast and cost-effective model for most tasks. Best balance of speed and quality.',
|
||||
},
|
||||
{
|
||||
'model_name': 'gpt-4.1',
|
||||
'display_name': 'GPT-4.1 - Legacy Model',
|
||||
'model_type': 'text',
|
||||
'provider': 'openai',
|
||||
'input_cost_per_1m': Decimal('2.0000'),
|
||||
'output_cost_per_1m': Decimal('8.0000'),
|
||||
'context_window': 8192,
|
||||
'max_output_tokens': 4096,
|
||||
'supports_json_mode': False,
|
||||
'supports_vision': False,
|
||||
'supports_function_calling': False,
|
||||
'is_active': True,
|
||||
'is_default': False,
|
||||
'sort_order': 10,
|
||||
'description': 'Legacy GPT-4 model. Higher cost but reliable.',
|
||||
},
|
||||
{
|
||||
'model_name': 'gpt-4o',
|
||||
'display_name': 'GPT-4o - High Quality with Vision',
|
||||
'model_type': 'text',
|
||||
'provider': 'openai',
|
||||
'input_cost_per_1m': Decimal('2.5000'),
|
||||
'output_cost_per_1m': Decimal('10.0000'),
|
||||
'context_window': 128000,
|
||||
'max_output_tokens': 4096,
|
||||
'supports_json_mode': True,
|
||||
'supports_vision': True,
|
||||
'supports_function_calling': True,
|
||||
'is_active': True,
|
||||
'is_default': False,
|
||||
'sort_order': 5,
|
||||
'description': 'Most capable GPT-4 variant with vision capabilities. Best for complex tasks.',
|
||||
},
|
||||
{
|
||||
'model_name': 'gpt-5.1',
|
||||
'display_name': 'GPT-5.1 - Advanced (16K context)',
|
||||
'model_type': 'text',
|
||||
'provider': 'openai',
|
||||
'input_cost_per_1m': Decimal('1.2500'),
|
||||
'output_cost_per_1m': Decimal('10.0000'),
|
||||
'context_window': 16000,
|
||||
'max_output_tokens': 16000,
|
||||
'supports_json_mode': True,
|
||||
'supports_vision': False,
|
||||
'supports_function_calling': True,
|
||||
'is_active': True,
|
||||
'is_default': False,
|
||||
'sort_order': 20,
|
||||
'description': 'Advanced GPT-5 model with 16K context window.',
|
||||
},
|
||||
{
|
||||
'model_name': 'gpt-5.2',
|
||||
'display_name': 'GPT-5.2 - Most Advanced (16K context)',
|
||||
'model_type': 'text',
|
||||
'provider': 'openai',
|
||||
'input_cost_per_1m': Decimal('1.7500'),
|
||||
'output_cost_per_1m': Decimal('14.0000'),
|
||||
'context_window': 16000,
|
||||
'max_output_tokens': 16000,
|
||||
'supports_json_mode': True,
|
||||
'supports_vision': False,
|
||||
'supports_function_calling': True,
|
||||
'is_active': True,
|
||||
'is_default': False,
|
||||
'sort_order': 30,
|
||||
'description': 'Most advanced GPT-5 variant. Highest quality output.',
|
||||
},
|
||||
]
|
||||
|
||||
# Image Models (from IMAGE_MODEL_RATES in constants.py)
|
||||
image_models = [
|
||||
{
|
||||
'model_name': 'dall-e-3',
|
||||
'display_name': 'DALL-E 3 - High Quality Images',
|
||||
'model_type': 'image',
|
||||
'provider': 'openai',
|
||||
'cost_per_image': Decimal('0.0400'),
|
||||
'valid_sizes': ['1024x1024', '1024x1792', '1792x1024'],
|
||||
'supports_json_mode': False,
|
||||
'supports_vision': False,
|
||||
'supports_function_calling': False,
|
||||
'is_active': True,
|
||||
'is_default': True, # Default image model
|
||||
'sort_order': 1,
|
||||
'description': 'Latest DALL-E model with best quality and prompt adherence.',
|
||||
},
|
||||
{
|
||||
'model_name': 'dall-e-2',
|
||||
'display_name': 'DALL-E 2 - Standard Quality',
|
||||
'model_type': 'image',
|
||||
'provider': 'openai',
|
||||
'cost_per_image': Decimal('0.0200'),
|
||||
'valid_sizes': ['256x256', '512x512', '1024x1024'],
|
||||
'supports_json_mode': False,
|
||||
'supports_vision': False,
|
||||
'supports_function_calling': False,
|
||||
'is_active': True,
|
||||
'is_default': False,
|
||||
'sort_order': 10,
|
||||
'description': 'Cost-effective image generation with good quality.',
|
||||
},
|
||||
{
|
||||
'model_name': 'gpt-image-1',
|
||||
'display_name': 'GPT Image 1 (Not compatible with OpenAI)',
|
||||
'model_type': 'image',
|
||||
'provider': 'openai',
|
||||
'cost_per_image': Decimal('0.0420'),
|
||||
'valid_sizes': ['1024x1024'],
|
||||
'supports_json_mode': False,
|
||||
'supports_vision': False,
|
||||
'supports_function_calling': False,
|
||||
'is_active': False, # Not valid for OpenAI endpoint
|
||||
'is_default': False,
|
||||
'sort_order': 20,
|
||||
'description': 'Not compatible with OpenAI /v1/images/generations endpoint.',
|
||||
},
|
||||
{
|
||||
'model_name': 'gpt-image-1-mini',
|
||||
'display_name': 'GPT Image 1 Mini (Not compatible with OpenAI)',
|
||||
'model_type': 'image',
|
||||
'provider': 'openai',
|
||||
'cost_per_image': Decimal('0.0110'),
|
||||
'valid_sizes': ['1024x1024'],
|
||||
'supports_json_mode': False,
|
||||
'supports_vision': False,
|
||||
'supports_function_calling': False,
|
||||
'is_active': False, # Not valid for OpenAI endpoint
|
||||
'is_default': False,
|
||||
'sort_order': 30,
|
||||
'description': 'Not compatible with OpenAI /v1/images/generations endpoint.',
|
||||
},
|
||||
]
|
||||
|
||||
# Create all models
|
||||
for model_data in text_models + image_models:
|
||||
AIModelConfig.objects.create(**model_data)
|
||||
|
||||
|
||||
def reverse_seed(apps, schema_editor):
|
||||
"""Remove seeded data"""
|
||||
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
|
||||
AIModelConfig.objects.all().delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0019_populate_token_based_config'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='HistoricalAIModelConfig',
|
||||
fields=[
|
||||
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
||||
('model_name', models.CharField(db_index=True, help_text="Model identifier used in API calls (e.g., 'gpt-4o-mini', 'dall-e-3')", max_length=100)),
|
||||
('display_name', models.CharField(help_text="Human-readable name shown in UI (e.g., 'GPT-4o mini - Fast & Affordable')", max_length=200)),
|
||||
('model_type', models.CharField(choices=[('text', 'Text Generation'), ('image', 'Image Generation'), ('embedding', 'Embedding')], db_index=True, help_text='Type of model - determines which pricing fields are used', max_length=20)),
|
||||
('provider', models.CharField(choices=[('openai', 'OpenAI'), ('anthropic', 'Anthropic'), ('runware', 'Runware'), ('google', 'Google')], db_index=True, help_text='AI provider (OpenAI, Anthropic, etc.)', max_length=50)),
|
||||
('input_cost_per_1m', models.DecimalField(blank=True, decimal_places=4, help_text='Cost per 1 million input tokens (USD). For text models only.', max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.0001'))])),
|
||||
('output_cost_per_1m', models.DecimalField(blank=True, decimal_places=4, help_text='Cost per 1 million output tokens (USD). For text models only.', max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.0001'))])),
|
||||
('context_window', models.IntegerField(blank=True, help_text='Maximum input tokens (context length). For text models only.', null=True, validators=[django.core.validators.MinValueValidator(1)])),
|
||||
('max_output_tokens', models.IntegerField(blank=True, help_text='Maximum output tokens per request. For text models only.', null=True, validators=[django.core.validators.MinValueValidator(1)])),
|
||||
('cost_per_image', models.DecimalField(blank=True, decimal_places=4, help_text='Fixed cost per image generation (USD). For image models only.', max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.0001'))])),
|
||||
('valid_sizes', models.JSONField(blank=True, help_text='Array of valid image sizes (e.g., ["1024x1024", "1024x1792"]). For image models only.', null=True)),
|
||||
('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')),
|
||||
('is_active', models.BooleanField(db_index=True, default=True, help_text='Enable/disable model without deleting')),
|
||||
('is_default', models.BooleanField(db_index=True, default=False, help_text='Mark as default model for its type (only one per type)')),
|
||||
('sort_order', models.IntegerField(default=0, help_text='Control order in dropdown lists (lower numbers first)')),
|
||||
('description', models.TextField(blank=True, help_text='Admin notes about model usage, strengths, limitations')),
|
||||
('release_date', models.DateField(blank=True, help_text='When model was released/added', null=True)),
|
||||
('deprecation_date', models.DateField(blank=True, help_text='When model will be removed', null=True)),
|
||||
('created_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('updated_at', models.DateTimeField(blank=True, editable=False)),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField(db_index=True)),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('updated_by', models.ForeignKey(blank=True, db_constraint=False, help_text='Admin who last updated', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical AI Model Configuration',
|
||||
'verbose_name_plural': 'historical AI Model Configurations',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': ('history_date', 'history_id'),
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AIModelConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('model_name', models.CharField(db_index=True, help_text="Model identifier used in API calls (e.g., 'gpt-4o-mini', 'dall-e-3')", max_length=100, unique=True)),
|
||||
('display_name', models.CharField(help_text="Human-readable name shown in UI (e.g., 'GPT-4o mini - Fast & Affordable')", max_length=200)),
|
||||
('model_type', models.CharField(choices=[('text', 'Text Generation'), ('image', 'Image Generation'), ('embedding', 'Embedding')], db_index=True, help_text='Type of model - determines which pricing fields are used', max_length=20)),
|
||||
('provider', models.CharField(choices=[('openai', 'OpenAI'), ('anthropic', 'Anthropic'), ('runware', 'Runware'), ('google', 'Google')], db_index=True, help_text='AI provider (OpenAI, Anthropic, etc.)', max_length=50)),
|
||||
('input_cost_per_1m', models.DecimalField(blank=True, decimal_places=4, help_text='Cost per 1 million input tokens (USD). For text models only.', max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.0001'))])),
|
||||
('output_cost_per_1m', models.DecimalField(blank=True, decimal_places=4, help_text='Cost per 1 million output tokens (USD). For text models only.', max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.0001'))])),
|
||||
('context_window', models.IntegerField(blank=True, help_text='Maximum input tokens (context length). For text models only.', null=True, validators=[django.core.validators.MinValueValidator(1)])),
|
||||
('max_output_tokens', models.IntegerField(blank=True, help_text='Maximum output tokens per request. For text models only.', null=True, validators=[django.core.validators.MinValueValidator(1)])),
|
||||
('cost_per_image', models.DecimalField(blank=True, decimal_places=4, help_text='Fixed cost per image generation (USD). For image models only.', max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.0001'))])),
|
||||
('valid_sizes', models.JSONField(blank=True, help_text='Array of valid image sizes (e.g., ["1024x1024", "1024x1792"]). For image models only.', null=True)),
|
||||
('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')),
|
||||
('is_active', models.BooleanField(db_index=True, default=True, help_text='Enable/disable model without deleting')),
|
||||
('is_default', models.BooleanField(db_index=True, default=False, help_text='Mark as default model for its type (only one per type)')),
|
||||
('sort_order', models.IntegerField(default=0, help_text='Control order in dropdown lists (lower numbers first)')),
|
||||
('description', models.TextField(blank=True, help_text='Admin notes about model usage, strengths, limitations')),
|
||||
('release_date', models.DateField(blank=True, help_text='When model was released/added', null=True)),
|
||||
('deprecation_date', models.DateField(blank=True, help_text='When model will be removed', null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('updated_by', models.ForeignKey(blank=True, help_text='Admin who last updated', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ai_model_updates', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'AI Model Configuration',
|
||||
'verbose_name_plural': 'AI Model Configurations',
|
||||
'db_table': 'igny8_ai_model_config',
|
||||
'ordering': ['model_type', 'sort_order', 'model_name'],
|
||||
'indexes': [models.Index(fields=['model_type', 'is_active'], name='igny8_ai_mo_model_t_1eef71_idx'), models.Index(fields=['provider', 'is_active'], name='igny8_ai_mo_provide_fbda6c_idx'), models.Index(fields=['is_default', 'model_type'], name='igny8_ai_mo_is_defa_95bfb9_idx')],
|
||||
},
|
||||
),
|
||||
# Seed initial model data
|
||||
migrations.RunPython(seed_ai_models, reverse_seed),
|
||||
]
|
||||
@@ -142,3 +142,59 @@ class UsageLimitsSerializer(serializers.Serializer):
|
||||
"""Serializer for usage limits response"""
|
||||
limits: LimitCardSerializer = LimitCardSerializer(many=True)
|
||||
|
||||
|
||||
class AIModelConfigSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for AI Model Configuration (Read-Only API)
|
||||
Provides model information for frontend dropdowns and displays
|
||||
"""
|
||||
model_name = serializers.CharField(read_only=True)
|
||||
display_name = serializers.CharField(read_only=True)
|
||||
model_type = serializers.CharField(read_only=True)
|
||||
provider = serializers.CharField(read_only=True)
|
||||
|
||||
# Text model fields
|
||||
input_cost_per_1m = serializers.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=4,
|
||||
read_only=True,
|
||||
allow_null=True
|
||||
)
|
||||
output_cost_per_1m = serializers.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=4,
|
||||
read_only=True,
|
||||
allow_null=True
|
||||
)
|
||||
context_window = serializers.IntegerField(read_only=True, allow_null=True)
|
||||
max_output_tokens = serializers.IntegerField(read_only=True, allow_null=True)
|
||||
|
||||
# Image model fields
|
||||
cost_per_image = serializers.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=4,
|
||||
read_only=True,
|
||||
allow_null=True
|
||||
)
|
||||
valid_sizes = serializers.ListField(read_only=True, allow_null=True)
|
||||
|
||||
# Capabilities
|
||||
supports_json_mode = serializers.BooleanField(read_only=True)
|
||||
supports_vision = serializers.BooleanField(read_only=True)
|
||||
supports_function_calling = serializers.BooleanField(read_only=True)
|
||||
|
||||
# Status
|
||||
is_default = serializers.BooleanField(read_only=True)
|
||||
sort_order = serializers.IntegerField(read_only=True)
|
||||
|
||||
# Computed field
|
||||
pricing_display = serializers.SerializerMethodField()
|
||||
|
||||
def get_pricing_display(self, obj):
|
||||
"""Generate pricing display string based on model type"""
|
||||
if obj.model_type == 'text':
|
||||
return f"${obj.input_cost_per_1m}/{obj.output_cost_per_1m} per 1M"
|
||||
elif obj.model_type == 'image':
|
||||
return f"${obj.cost_per_image} per image"
|
||||
return ""
|
||||
|
||||
|
||||
@@ -751,3 +751,75 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
||||
return Response({'error': 'Method not found'}, status=404)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(tags=['AI Models'], summary='List available AI models'),
|
||||
retrieve=extend_schema(tags=['AI Models'], summary='Get AI model details'),
|
||||
)
|
||||
class AIModelConfigViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
ViewSet for AI Model Configuration (Read-Only)
|
||||
Provides model information for frontend dropdowns and displays
|
||||
"""
|
||||
permission_classes = [IsAuthenticatedAndActive]
|
||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
||||
throttle_scope = 'billing'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
pagination_class = None # No pagination for model lists
|
||||
lookup_field = 'model_name'
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get AIModelConfig queryset with filters"""
|
||||
from igny8_core.business.billing.models import AIModelConfig
|
||||
|
||||
queryset = AIModelConfig.objects.filter(is_active=True)
|
||||
|
||||
# Filter by model type
|
||||
model_type = self.request.query_params.get('type', None)
|
||||
if model_type:
|
||||
queryset = queryset.filter(model_type=model_type)
|
||||
|
||||
# Filter by provider
|
||||
provider = self.request.query_params.get('provider', None)
|
||||
if provider:
|
||||
queryset = queryset.filter(provider=provider)
|
||||
|
||||
# Filter by default
|
||||
is_default = self.request.query_params.get('default', None)
|
||||
if is_default is not None:
|
||||
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')
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Return serializer class"""
|
||||
from .serializers import AIModelConfigSerializer
|
||||
return AIModelConfigSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""List all available models with filters"""
|
||||
queryset = self.get_queryset()
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
|
||||
return success_response(
|
||||
data=serializer.data,
|
||||
message='AI models retrieved successfully'
|
||||
)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""Get details for a specific model"""
|
||||
try:
|
||||
instance = self.get_queryset().get(model_name=kwargs.get('model_name'))
|
||||
serializer = self.get_serializer(instance)
|
||||
|
||||
return success_response(
|
||||
data=serializer.data,
|
||||
message='AI model details retrieved successfully'
|
||||
)
|
||||
except Exception as e:
|
||||
return error_response(
|
||||
message='Model not found',
|
||||
errors={'model_name': [str(e)]},
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user