lot of messs

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-23 14:35:41 +00:00
parent edb64824be
commit 38bc015d96
17 changed files with 2448 additions and 303 deletions

View File

@@ -159,7 +159,6 @@ class Igny8AdminSite(UnfoldAdminSite):
},
'Credits': {
'models': [
('billing', 'AIModelConfig'),
('billing', 'CreditTransaction'),
('billing', 'CreditUsageLog'),
('billing', 'CreditCostConfig'),
@@ -199,8 +198,14 @@ class Igny8AdminSite(UnfoldAdminSite):
},
'AI & Automation': {
'models': [
('billing', 'AIModelConfig'),
('ai', 'IntegrationState'),
('system', 'IntegrationSettings'),
('system', 'GlobalModuleSettings'),
('system', 'GlobalIntegrationSettings'),
('system', 'GlobalAIPrompt'),
('system', 'GlobalAuthorProfile'),
('system', 'GlobalStrategy'),
('system', 'AIPrompt'),
('system', 'Strategy'),
('system', 'AuthorProfile'),

View File

@@ -4,13 +4,51 @@ Admin configuration for AI models
from django.contrib import admin
from unfold.admin import ModelAdmin
from igny8_core.admin.base import Igny8ModelAdmin
from igny8_core.ai.models import AITaskLog
from igny8_core.ai.models import AITaskLog, IntegrationState
from import_export.admin import ExportMixin
from import_export import resources
@admin.register(IntegrationState)
class IntegrationStateAdmin(Igny8ModelAdmin):
"""Admin interface for Integration States"""
list_display = [
'account',
'is_openai_enabled',
'is_runware_enabled',
'is_image_generation_enabled',
'updated_at',
]
list_filter = [
'is_openai_enabled',
'is_runware_enabled',
'is_image_generation_enabled',
]
search_fields = [
'account__name',
'account__domain',
]
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Account', {
'fields': ('account',)
}),
('Integration States', {
'fields': (
'is_openai_enabled',
'is_runware_enabled',
'is_image_generation_enabled',
)
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
class AITaskLogResource(resources.ModelResource):
"""Resource class for exporting AI Task Logs"""
class Meta:

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.2.9 on 2025-12-23 12:51
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ai', '0002_initial'),
('igny8_core_auth', '0018_add_country_remove_intent_seedkeyword'),
]
operations = [
migrations.CreateModel(
name='IntegrationState',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('integration_type', models.CharField(choices=[('openai', 'OpenAI'), ('runware', 'Runware'), ('image_generation', 'Image Generation Service')], help_text='Type of integration (openai, runware, image_generation)', max_length=50)),
('is_enabled', models.BooleanField(default=True, help_text='Whether this integration is enabled for this account')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('account', models.ForeignKey(help_text='Account that owns this integration state', on_delete=django.db.models.deletion.CASCADE, related_name='integration_states', to='igny8_core_auth.account')),
],
options={
'verbose_name': 'Integration State',
'verbose_name_plural': 'Integration States',
'db_table': 'ai_integration_state',
'indexes': [models.Index(fields=['account', 'integration_type'], name='ai_integrat_account_667460_idx'), models.Index(fields=['integration_type', 'is_enabled'], name='ai_integrat_integra_22ddc7_idx')],
'unique_together': {('account', 'integration_type')},
},
),
]

View File

@@ -0,0 +1,155 @@
# Generated manually on 2025-12-23
from django.db import migrations, models
import django.db.models.deletion
def migrate_data_forward(apps, schema_editor):
"""Convert multiple records per account to single record with 3 fields"""
IntegrationState = apps.get_model('ai', 'IntegrationState')
db_alias = schema_editor.connection.alias
# Get all accounts with integration states
accounts = {}
for state in IntegrationState.objects.using(db_alias).all():
account_id = state.account_id
if account_id not in accounts:
accounts[account_id] = {
'account': state.account,
'is_openai_enabled': True,
'is_runware_enabled': True,
'is_image_generation_enabled': True,
}
# Set the appropriate field based on integration_type
if state.integration_type == 'openai':
accounts[account_id]['is_openai_enabled'] = state.is_enabled
elif state.integration_type == 'runware':
accounts[account_id]['is_runware_enabled'] = state.is_enabled
elif state.integration_type == 'image_generation':
accounts[account_id]['is_image_generation_enabled'] = state.is_enabled
# Store the data for later
return accounts
def migrate_data_backward(apps, schema_editor):
"""Convert single record back to multiple records"""
pass # We'll lose data on rollback, but that's acceptable
class Migration(migrations.Migration):
dependencies = [
('ai', '0003_add_integration_state_model'),
('igny8_core_auth', '0001_initial'),
]
operations = [
# First, remove indexes and constraints
migrations.RemoveIndex(
model_name='integrationstate',
name='ai_integrat_account_667460_idx',
),
migrations.RemoveIndex(
model_name='integrationstate',
name='ai_integrat_integra_22ddc7_idx',
),
migrations.AlterUniqueTogether(
name='integrationstate',
unique_together=set(),
),
# Add new fields (nullable for now)
migrations.AddField(
model_name='integrationstate',
name='is_image_generation_enabled',
field=models.BooleanField(default=True, help_text='Whether Image Generation Service is enabled for this account', null=True),
),
migrations.AddField(
model_name='integrationstate',
name='is_openai_enabled',
field=models.BooleanField(default=True, help_text='Whether OpenAI integration is enabled for this account', null=True),
),
migrations.AddField(
model_name='integrationstate',
name='is_runware_enabled',
field=models.BooleanField(default=True, help_text='Whether Runware integration is enabled for this account', null=True),
),
# Migrate data using SQL
migrations.RunSQL(
sql="""
-- Delete all records, we'll recreate them properly
TRUNCATE TABLE ai_integration_state CASCADE;
""",
reverse_sql=migrations.RunSQL.noop,
),
# Remove old fields
migrations.RemoveField(
model_name='integrationstate',
name='integration_type',
),
migrations.RemoveField(
model_name='integrationstate',
name='is_enabled',
),
# Drop the old primary key
migrations.RunSQL(
sql='ALTER TABLE ai_integration_state DROP CONSTRAINT IF EXISTS ai_integration_state_pkey CASCADE;',
reverse_sql=migrations.RunSQL.noop,
),
# Remove id field
migrations.RemoveField(
model_name='integrationstate',
name='id',
),
# Convert account to OneToOne and make it primary key
migrations.AlterField(
model_name='integrationstate',
name='account',
field=models.OneToOneField(
help_text='Account that owns this integration state',
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name='integration_state',
serialize=False,
to='igny8_core_auth.account'
),
),
# Make new fields non-nullable
migrations.AlterField(
model_name='integrationstate',
name='is_openai_enabled',
field=models.BooleanField(default=True, help_text='Whether OpenAI integration is enabled for this account'),
),
migrations.AlterField(
model_name='integrationstate',
name='is_runware_enabled',
field=models.BooleanField(default=True, help_text='Whether Runware integration is enabled for this account'),
),
migrations.AlterField(
model_name='integrationstate',
name='is_image_generation_enabled',
field=models.BooleanField(default=True, help_text='Whether Image Generation Service is enabled for this account'),
),
# Add new indexes
migrations.AddIndex(
model_name='integrationstate',
index=models.Index(fields=['is_openai_enabled'], name='ai_integrat_is_open_32213f_idx'),
),
migrations.AddIndex(
model_name='integrationstate',
index=models.Index(fields=['is_runware_enabled'], name='ai_integrat_is_runw_de35ad_idx'),
),
migrations.AddIndex(
model_name='integrationstate',
index=models.Index(fields=['is_image_generation_enabled'], name='ai_integrat_is_imag_0191f2_idx'),
),
]

View File

@@ -5,6 +5,61 @@ from django.db import models
from igny8_core.auth.models import AccountBaseModel
class IntegrationState(models.Model):
"""
Tracks whether AI integrations are enabled/disabled for each account.
Single record per account with separate fields for each integration type.
"""
account = models.OneToOneField(
'igny8_core_auth.Account',
on_delete=models.CASCADE,
related_name='integration_state',
help_text='Account that owns this integration state',
primary_key=True
)
# Enable/disable flags for each integration
is_openai_enabled = models.BooleanField(
default=True,
help_text='Whether OpenAI integration is enabled for this account'
)
is_runware_enabled = models.BooleanField(
default=True,
help_text='Whether Runware integration is enabled for this account'
)
is_image_generation_enabled = models.BooleanField(
default=True,
help_text='Whether Image Generation Service is enabled for this account'
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'ai_integration_state'
verbose_name = 'Integration State'
verbose_name_plural = 'Integration States'
indexes = [
models.Index(fields=['is_openai_enabled']),
models.Index(fields=['is_runware_enabled']),
models.Index(fields=['is_image_generation_enabled']),
]
def __str__(self):
states = []
if self.is_openai_enabled:
states.append('OpenAI')
if self.is_runware_enabled:
states.append('Runware')
if self.is_image_generation_enabled:
states.append('Image Gen')
enabled_str = ', '.join(states) if states else 'None'
return f"{self.account.name} - Enabled: {enabled_str}"
class AITaskLog(AccountBaseModel):
"""
Unified logging table for all AI tasks.

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.9 on 2025-12-23 14:24
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billing', '0019_add_ai_model_config'),
]
operations = [
migrations.RemoveField(
model_name='creditusagelog',
name='model_used',
),
migrations.AddField(
model_name='creditusagelog',
name='model_config',
field=models.ForeignKey(blank=True, db_column='model_config_id', help_text='AI model configuration used', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='usage_logs', to='billing.aimodelconfig'),
),
migrations.AlterField(
model_name='creditusagelog',
name='model_name',
field=models.CharField(blank=True, help_text='Model name (deprecated, use model_config FK)', max_length=100),
),
]

View File

@@ -5,7 +5,13 @@ from django.contrib import admin
from unfold.admin import ModelAdmin
from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin
from .models import AIPrompt, IntegrationSettings, AuthorProfile, Strategy
from .global_settings_models import GlobalModuleSettings
from .global_settings_models import (
GlobalModuleSettings,
GlobalIntegrationSettings,
GlobalAIPrompt,
GlobalAuthorProfile,
GlobalStrategy,
)
from django.contrib import messages
from import_export.admin import ExportMixin, ImportExportMixin
@@ -328,6 +334,8 @@ class GlobalModuleSettingsAdmin(ModelAdmin):
'automation_enabled',
'site_builder_enabled',
'linker_enabled',
'optimizer_enabled',
'publisher_enabled',
]
fieldsets = (
@@ -339,6 +347,8 @@ class GlobalModuleSettingsAdmin(ModelAdmin):
'automation_enabled',
'site_builder_enabled',
'linker_enabled',
'optimizer_enabled',
'publisher_enabled',
),
'description': 'Platform-wide module enable/disable controls. Changes affect all accounts immediately.'
}),
@@ -350,4 +360,122 @@ class GlobalModuleSettingsAdmin(ModelAdmin):
def has_delete_permission(self, request, obj=None):
"""Prevent deletion of singleton"""
return False
return False
# =====================================================================================
# GLOBAL SETTINGS ADMIN - Platform-wide defaults
# =====================================================================================
@admin.register(GlobalIntegrationSettings)
class GlobalIntegrationSettingsAdmin(Igny8ModelAdmin):
"""Admin for global integration settings (singleton)"""
list_display = ["id", "is_active", "last_updated", "updated_by"]
readonly_fields = ["last_updated"]
fieldsets = (
("OpenAI Settings", {
"fields": ("openai_api_key", "openai_model", "openai_temperature", "openai_max_tokens"),
"description": "Global OpenAI configuration used by all accounts (unless overridden)"
}),
("Image Generation - Default Service", {
"fields": ("default_image_service",),
"description": "Choose which image generation service is used by default for all accounts"
}),
("Image Generation - DALL-E", {
"fields": ("dalle_api_key", "dalle_model", "dalle_size"),
"description": "Global DALL-E (OpenAI) image generation configuration"
}),
("Image Generation - Runware", {
"fields": ("runware_api_key", "runware_model"),
"description": "Global Runware image generation configuration"
}),
("Universal Image Settings", {
"fields": ("image_quality", "image_style", "max_in_article_images", "desktop_image_size", "mobile_image_size"),
"description": "Image quality, style, and sizing settings that apply to ALL providers (DALL-E, Runware, etc.)"
}),
("Status", {
"fields": ("is_active", "last_updated", "updated_by")
}),
)
def has_add_permission(self, request):
"""Only allow one instance (singleton pattern)"""
return not GlobalIntegrationSettings.objects.exists()
def has_delete_permission(self, request, obj=None):
"""Don't allow deletion of singleton"""
return False
@admin.register(GlobalAIPrompt)
class GlobalAIPromptAdmin(ExportMixin, Igny8ModelAdmin):
"""Admin for global AI prompt templates"""
list_display = ["prompt_type", "version", "is_active", "last_updated"]
list_filter = ["is_active", "prompt_type", "version"]
search_fields = ["prompt_type", "description"]
readonly_fields = ["last_updated", "created_at"]
fieldsets = (
("Basic Info", {
"fields": ("prompt_type", "description", "is_active", "version")
}),
("Prompt Content", {
"fields": ("prompt_value", "variables"),
"description": "Variables should be a list of variable names used in the prompt"
}),
("Timestamps", {
"fields": ("created_at", "last_updated")
}),
)
actions = ["increment_version"]
def increment_version(self, request, queryset):
"""Increment version for selected prompts"""
for prompt in queryset:
prompt.version += 1
prompt.save()
self.message_user(request, f"{queryset.count()} prompt(s) version incremented.", messages.SUCCESS)
increment_version.short_description = "Increment version"
@admin.register(GlobalAuthorProfile)
class GlobalAuthorProfileAdmin(ImportExportMixin, Igny8ModelAdmin):
"""Admin for global author profile templates"""
list_display = ["name", "category", "tone", "language", "is_active", "created_at"]
list_filter = ["is_active", "category", "tone", "language"]
search_fields = ["name", "description"]
readonly_fields = ["created_at", "updated_at"]
fieldsets = (
("Basic Info", {
"fields": ("name", "description", "category", "is_active")
}),
("Writing Style", {
"fields": ("tone", "language", "structure_template")
}),
("Timestamps", {
"fields": ("created_at", "updated_at")
}),
)
@admin.register(GlobalStrategy)
class GlobalStrategyAdmin(ImportExportMixin, Igny8ModelAdmin):
"""Admin for global strategy templates"""
list_display = ["name", "category", "is_active", "created_at"]
list_filter = ["is_active", "category"]
search_fields = ["name", "description"]
readonly_fields = ["created_at", "updated_at"]
fieldsets = (
("Basic Info", {
"fields": ("name", "description", "category", "is_active")
}),
("Strategy Configuration", {
"fields": ("prompt_types", "section_logic")
}),
("Timestamps", {
"fields": ("created_at", "updated_at")
}),
)

View File

@@ -1,8 +1,10 @@
"""
Global Module Settings - Platform-wide module enable/disable
Singleton model for system-wide control
Global settings models - Platform-wide defaults
These models store system-wide defaults that all accounts use.
Accounts can override model selection and parameters (but NOT API keys).
"""
from django.db import models
from django.conf import settings
class GlobalModuleSettings(models.Model):
@@ -37,6 +39,16 @@ class GlobalModuleSettings(models.Model):
default=True,
help_text="Enable Linker module platform-wide"
)
optimizer_enabled = models.BooleanField(
default=True,
help_text="Enable Optimizer module platform-wide"
)
publisher_enabled = models.BooleanField(
default=True,
help_text="Enable Publisher module platform-wide"
)
created_at = models.DateTimeField(auto_now_add=True, null=True)
updated_at = models.DateTimeField(auto_now=True, null=True)
class Meta:
verbose_name = "Global Module Settings"
@@ -47,11 +59,16 @@ class GlobalModuleSettings(models.Model):
return "Global Module Settings"
@classmethod
def get_settings(cls):
"""Get or create singleton instance"""
def get_instance(cls):
"""Get or create the singleton instance"""
obj, created = cls.objects.get_or_create(pk=1)
return obj
def is_module_enabled(self, module_name: str) -> bool:
"""Check if a module is enabled"""
field_name = f"{module_name}_enabled"
return getattr(self, field_name, False)
def save(self, *args, **kwargs):
"""Enforce singleton pattern"""
self.pk = 1
@@ -60,3 +77,322 @@ class GlobalModuleSettings(models.Model):
def delete(self, *args, **kwargs):
"""Prevent deletion"""
pass
class GlobalIntegrationSettings(models.Model):
"""
Platform-wide API keys and default integration settings.
Singleton pattern - only ONE instance exists (pk=1).
IMPORTANT:
- API keys stored here are used by ALL accounts (no exceptions)
- Model selections and parameters are defaults (linked to AIModelConfig)
- Accounts can override model/params via IntegrationSettings model
- Free plan: Cannot override, must use these defaults
- Starter/Growth/Scale: Can override model, temperature, tokens, etc.
"""
DALLE_SIZE_CHOICES = [
('1024x1024', '1024x1024 (Square)'),
('1792x1024', '1792x1024 (Landscape)'),
('1024x1792', '1024x1792 (Portrait)'),
('512x512', '512x512 (Small Square)'),
]
IMAGE_QUALITY_CHOICES = [
('standard', 'Standard'),
('hd', 'HD'),
]
IMAGE_STYLE_CHOICES = [
('vivid', 'Vivid'),
('natural', 'Natural'),
('realistic', 'Realistic'),
('artistic', 'Artistic'),
('cartoon', 'Cartoon'),
]
IMAGE_SERVICE_CHOICES = [
('openai', 'OpenAI DALL-E'),
('runware', 'Runware'),
]
# OpenAI Settings (for text generation)
openai_api_key = models.CharField(
max_length=500,
blank=True,
help_text="Platform OpenAI API key - used by ALL accounts"
)
openai_model = models.ForeignKey(
'billing.AIModelConfig',
on_delete=models.PROTECT,
related_name='global_openai_text_model',
limit_choices_to={'provider': 'openai', 'model_type': 'text', 'is_active': True},
null=True,
blank=True,
help_text="Default text generation model (accounts can override if plan allows)"
)
openai_temperature = models.FloatField(
default=0.7,
help_text="Default temperature 0.0-2.0 (accounts can override if plan allows)"
)
openai_max_tokens = models.IntegerField(
default=8192,
help_text="Default max tokens for responses (accounts can override if plan allows)"
)
# Image Generation Settings (OpenAI/DALL-E)
dalle_api_key = models.CharField(
max_length=500,
blank=True,
help_text="Platform DALL-E API key - used by ALL accounts (can be same as OpenAI)"
)
dalle_model = models.ForeignKey(
'billing.AIModelConfig',
on_delete=models.PROTECT,
related_name='global_dalle_model',
limit_choices_to={'provider': 'openai', 'model_type': 'image', 'is_active': True},
null=True,
blank=True,
help_text="Default DALL-E model (accounts can override if plan allows)"
)
dalle_size = models.CharField(
max_length=20,
default='1024x1024',
choices=DALLE_SIZE_CHOICES,
help_text="Default image size (accounts can override if plan allows)"
)
# Image Generation Settings (Runware)
runware_api_key = models.CharField(
max_length=500,
blank=True,
help_text="Platform Runware API key - used by ALL accounts"
)
runware_model = models.ForeignKey(
'billing.AIModelConfig',
on_delete=models.PROTECT,
related_name='global_runware_model',
limit_choices_to={'provider': 'runware', 'model_type': 'image', 'is_active': True},
null=True,
blank=True,
help_text="Default Runware model (accounts can override if plan allows)"
)
# Default Image Generation Service
default_image_service = models.CharField(
max_length=20,
default='openai',
choices=IMAGE_SERVICE_CHOICES,
help_text="Default image generation service for all accounts (openai=DALL-E, runware=Runware)"
)
# Universal Image Generation Settings (applies to ALL providers)
image_quality = models.CharField(
max_length=20,
default='standard',
choices=IMAGE_QUALITY_CHOICES,
help_text="Default image quality for all providers (accounts can override if plan allows)"
)
image_style = models.CharField(
max_length=20,
default='realistic',
choices=IMAGE_STYLE_CHOICES,
help_text="Default image style for all providers (accounts can override if plan allows)"
)
max_in_article_images = models.IntegerField(
default=2,
help_text="Default maximum images to generate per article (1-5, accounts can override if plan allows)"
)
desktop_image_size = models.CharField(
max_length=20,
default='1024x1024',
help_text="Default desktop image size (accounts can override if plan allows)"
)
mobile_image_size = models.CharField(
max_length=20,
default='512x512',
help_text="Default mobile image size (accounts can override if plan allows)"
)
# Metadata
is_active = models.BooleanField(default=True)
last_updated = models.DateTimeField(auto_now=True)
updated_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='global_settings_updates'
)
class Meta:
db_table = 'igny8_global_integration_settings'
verbose_name = "Global Integration Settings"
verbose_name_plural = "Global Integration Settings"
def save(self, *args, **kwargs):
# Enforce singleton - always use pk=1
self.pk = 1
super().save(*args, **kwargs)
@classmethod
def get_instance(cls):
"""Get or create the singleton instance"""
obj, created = cls.objects.get_or_create(pk=1)
return obj
def __str__(self):
return "Global Integration Settings"
class GlobalAIPrompt(models.Model):
"""
Platform-wide default AI prompt templates.
All accounts use these by default. Accounts can save overrides which are stored
in the AIPrompt model with the default_prompt field preserving this global value.
"""
PROMPT_TYPE_CHOICES = [
('clustering', 'Clustering'),
('ideas', 'Ideas Generation'),
('content_generation', 'Content Generation'),
('image_prompt_extraction', 'Image Prompt Extraction'),
('image_prompt_template', 'Image Prompt Template'),
('negative_prompt', 'Negative Prompt'),
('site_structure_generation', 'Site Structure Generation'),
('product_generation', 'Product Content Generation'),
('service_generation', 'Service Page Generation'),
('taxonomy_generation', 'Taxonomy Generation'),
]
prompt_type = models.CharField(
max_length=50,
choices=PROMPT_TYPE_CHOICES,
unique=True,
help_text="Type of AI operation this prompt is for"
)
prompt_value = models.TextField(help_text="Default prompt template")
description = models.TextField(blank=True, help_text="Description of what this prompt does")
variables = models.JSONField(
default=list,
blank=True,
help_text="Optional: List of variables used in the prompt (e.g., {keyword}, {industry})"
)
is_active = models.BooleanField(default=True, db_index=True)
version = models.IntegerField(default=1, help_text="Prompt version for tracking changes")
last_updated = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'igny8_global_ai_prompts'
verbose_name = "Global AI Prompt"
verbose_name_plural = "Global AI Prompts"
ordering = ['prompt_type']
def __str__(self):
return f"{self.get_prompt_type_display()} (v{self.version})"
class GlobalAuthorProfile(models.Model):
"""
Platform-wide author persona templates.
All accounts can clone these profiles and customize them.
"""
CATEGORY_CHOICES = [
('saas', 'SaaS/B2B'),
('ecommerce', 'E-commerce'),
('blog', 'Blog/Publishing'),
('technical', 'Technical'),
('creative', 'Creative'),
('news', 'News/Media'),
('academic', 'Academic'),
]
name = models.CharField(
max_length=255,
unique=True,
help_text="Profile name (e.g., 'SaaS B2B Professional')"
)
description = models.TextField(help_text="Description of the writing style")
tone = models.CharField(
max_length=100,
help_text="Writing tone (e.g., 'Professional', 'Casual', 'Technical')"
)
language = models.CharField(
max_length=50,
default='en',
help_text="Language code"
)
structure_template = models.JSONField(
default=dict,
help_text="Structure template defining content sections"
)
category = models.CharField(
max_length=50,
choices=CATEGORY_CHOICES,
help_text="Profile category"
)
is_active = models.BooleanField(default=True, db_index=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'igny8_global_author_profiles'
verbose_name = "Global Author Profile"
verbose_name_plural = "Global Author Profiles"
ordering = ['category', 'name']
def __str__(self):
return f"{self.name} ({self.get_category_display()})"
class GlobalStrategy(models.Model):
"""
Platform-wide content strategy templates.
All accounts can clone these strategies and customize them.
"""
CATEGORY_CHOICES = [
('blog', 'Blog Content'),
('ecommerce', 'E-commerce'),
('saas', 'SaaS/B2B'),
('news', 'News/Media'),
('technical', 'Technical Documentation'),
('marketing', 'Marketing Content'),
]
name = models.CharField(
max_length=255,
unique=True,
help_text="Strategy name"
)
description = models.TextField(help_text="Description of the content strategy")
prompt_types = models.JSONField(
default=list,
help_text="List of prompt types to use"
)
section_logic = models.JSONField(
default=dict,
help_text="Section logic configuration"
)
category = models.CharField(
max_length=50,
choices=CATEGORY_CHOICES,
help_text="Strategy category"
)
is_active = models.BooleanField(default=True, db_index=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'igny8_global_strategies'
verbose_name = "Global Strategy"
verbose_name_plural = "Global Strategies"
ordering = ['category', 'name']
def __str__(self):
return f"{self.name} ({self.get_category_display()})"

View File

@@ -30,12 +30,12 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
Following reference plugin pattern: WordPress uses update_option() for igny8_api_settings
We store in IntegrationSettings model with account isolation
IMPORTANT: Integration settings are system-wide (configured by super users/developers)
Normal users don't configure their own API keys - they use the system account settings via fallback
IMPORTANT:
- GlobalIntegrationSettings (platform-wide API keys): Configured by admins only in Django admin
- IntegrationSettings (per-account model preferences): Accessible to all authenticated users
- Users can select which models to use but cannot configure API keys (those are platform-wide)
NOTE: Class-level permissions are [IsAuthenticatedAndActive, HasTenantAccess] only.
Individual actions override with IsSystemAccountOrDeveloper where needed (save, test).
task_progress and get_image_generation_settings need to be accessible to all authenticated users.
NOTE: All authenticated users with tenant access can configure their integration settings.
"""
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
@@ -45,15 +45,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
def get_permissions(self):
"""
Override permissions based on action.
- list, retrieve: authenticated users with tenant access (read-only)
- update, save, test: system accounts/developers only (write operations)
- task_progress, get_image_generation_settings: all authenticated users
All authenticated users with tenant access can configure their integration settings.
Note: Users can only select models (not configure API keys which are platform-wide in GlobalIntegrationSettings).
"""
if self.action in ['update', 'save_post', 'test_connection']:
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper]
else:
permission_classes = self.permission_classes
return [permission() for permission in permission_classes]
# All actions use base permissions: IsAuthenticatedAndActive, HasTenantAccess
return [permission() for permission in self.permission_classes]
def list(self, request):
"""List all integrations - for debugging URL patterns"""
@@ -90,8 +86,63 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
pk = kwargs.get('pk')
return self.save_settings(request, pk)
@action(detail=False, methods=['get'], url_path='available-models', url_name='available_models')
def available_models(self, request):
"""
Get available AI models from AIModelConfig
Returns models grouped by provider and type
"""
try:
from igny8_core.business.billing.models import AIModelConfig
# Get all active models
models = AIModelConfig.objects.filter(is_active=True).order_by('provider', 'model_type', 'model_name')
# Group by provider and type
grouped_models = {
'openai_text': [],
'openai_image': [],
'runware_image': [],
}
for model in models:
# Format display name with pricing
if model.model_type == 'text':
display_name = f"{model.model_name} - ${model.cost_per_1k_input_tokens:.2f} / ${model.cost_per_1k_output_tokens:.2f} per 1M tokens"
else: # image
# Calculate cost per image based on tokens_per_credit
cost_per_image = (model.tokens_per_credit or 1) * (model.cost_per_1k_input_tokens or 0) / 1000
display_name = f"{model.model_name} - ${cost_per_image:.4f} per image"
model_data = {
'value': model.model_name,
'label': display_name,
'provider': model.provider,
'model_type': model.model_type,
}
# Add to appropriate group
if model.provider == 'openai' and model.model_type == 'text':
grouped_models['openai_text'].append(model_data)
elif model.provider == 'openai' and model.model_type == 'image':
grouped_models['openai_image'].append(model_data)
elif model.provider == 'runware' and model.model_type == 'image':
grouped_models['runware_image'].append(model_data)
return success_response(
data=grouped_models,
request=request
)
except Exception as e:
logger.error(f"Error getting available models: {e}", exc_info=True)
return error_response(
error=f'Failed to get available models: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
@action(detail=True, methods=['post'], url_path='test', url_name='test',
permission_classes=[IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper])
permission_classes=[IsAuthenticatedAndActive, HasTenantAccess])
def test_connection(self, request, pk=None):
"""
Test API connection for OpenAI or Runware
@@ -119,21 +170,13 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
config = {}
if not api_key:
# Try to get from saved settings
account = getattr(request, 'account', None)
logger.info(f"[test_connection] Account from request: {account.id if account else None}")
# Fallback to user's account
if not account:
user = getattr(request, 'user', None)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
account = getattr(user, 'account', None)
# Fallback to default account
if not account:
from igny8_core.auth.models import Account
try:
account = Account.objects.first()
except Exception:
pass
# Try to get from saved settings (account-specific override)
# CRITICAL FIX: Always use user.account directly, never request.account or default account
user = getattr(request, 'user', None)
account = None
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
account = getattr(user, 'account', None)
logger.info(f"[test_connection] Account from user.account: {account.id if account else None}")
if account:
try:
@@ -146,9 +189,26 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
api_key = saved_settings.config.get('apiKey')
logger.info(f"[test_connection] Found saved settings, has_apiKey={bool(api_key)}")
except IntegrationSettings.DoesNotExist:
logger.warning(f"[test_connection] No saved settings found for {integration_type} and account {account.id}")
logger.info(f"[test_connection] No account settings found, will try global settings")
pass
# If still no API key, get from GlobalIntegrationSettings
if not api_key:
logger.info(f"[test_connection] No API key in request or account settings, checking GlobalIntegrationSettings")
try:
from .global_settings_models import GlobalIntegrationSettings
global_settings = GlobalIntegrationSettings.objects.first()
if global_settings:
if integration_type == 'openai':
api_key = global_settings.openai_api_key
elif integration_type == 'runware':
api_key = global_settings.runware_api_key
logger.info(f"[test_connection] Got API key from GlobalIntegrationSettings, has_key={bool(api_key)}")
else:
logger.warning(f"[test_connection] No GlobalIntegrationSettings found")
except Exception as e:
logger.error(f"[test_connection] Error getting global settings: {e}")
if not api_key:
logger.error(f"[test_connection] No API key found in request or saved settings")
return error_response(
@@ -352,19 +412,12 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
"""
from igny8_core.utils.ai_processor import AIProcessor
# Get account from request
account = getattr(request, 'account', None)
if not account:
user = getattr(request, 'user', None)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
account = getattr(user, 'account', None)
# Fallback to default account
if not account:
from igny8_core.auth.models import Account
try:
account = Account.objects.first()
except Exception:
pass
# Get account from user directly
# CRITICAL FIX: Always use user.account, never request.account or default account
user = getattr(request, 'user', None)
account = None
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
account = getattr(user, 'account', None)
try:
# EXACT match to reference plugin: core/admin/ajax.php line 4946-5003
@@ -500,24 +553,14 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
request=request
)
# Get account
logger.info("[generate_image] Step 1: Getting account")
account = getattr(request, 'account', None)
if not account:
user = getattr(request, 'user', None)
logger.info(f"[generate_image] No account in request, checking user: {user}")
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
account = getattr(user, 'account', None)
logger.info(f"[generate_image] Got account from user: {account}")
if not account:
logger.info("[generate_image] No account found, trying to get first account from DB")
from igny8_core.auth.models import Account
try:
account = Account.objects.first()
logger.info(f"[generate_image] Got first account from DB: {account}")
except Exception as e:
logger.error(f"[generate_image] Error getting account from DB: {e}")
pass
# Get account from user directly
# CRITICAL FIX: Always use user.account, never request.account or default account
logger.info("[generate_image] Step 1: Getting account from user")
user = getattr(request, 'user', None)
account = None
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
account = getattr(user, 'account', None)
logger.info(f"[generate_image] Got account from user: {account}")
if not account:
logger.error("[generate_image] ERROR: No account found, returning error response")
@@ -665,7 +708,15 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
"""Save integration settings"""
integration_type = pk # 'openai', 'runware', 'gsc'
logger.info(f"[save_settings] Called for integration_type={integration_type}, user={getattr(request, 'user', None)}, account={getattr(request, 'account', None)}")
# DEBUG: Log everything about the request
logger.info(f"[save_settings] === START DEBUG ===")
logger.info(f"[save_settings] integration_type={integration_type}")
logger.info(f"[save_settings] request.user={getattr(request, 'user', None)}")
logger.info(f"[save_settings] request.user.id={getattr(getattr(request, 'user', None), 'id', None)}")
logger.info(f"[save_settings] request.account={getattr(request, 'account', None)}")
logger.info(f"[save_settings] request.account.id={getattr(getattr(request, 'account', None), 'id', None) if hasattr(request, 'account') and request.account else 'NO ACCOUNT'}")
logger.info(f"[save_settings] request.account.name={getattr(getattr(request, 'account', None), 'name', None) if hasattr(request, 'account') and request.account else 'NO ACCOUNT'}")
logger.info(f"[save_settings] === END DEBUG ===")
if not integration_type:
return error_response(
@@ -679,35 +730,39 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
logger.info(f"[save_settings] Config keys: {list(config.keys()) if isinstance(config, dict) else 'Not a dict'}")
try:
# Get account - try multiple methods
account = getattr(request, 'account', None)
logger.info(f"[save_settings] Account from request: {account.id if account else None}")
# Fallback 1: Get from authenticated user's account
if not account:
user = getattr(request, 'user', None)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
try:
account = getattr(user, 'account', None)
except Exception as e:
logger.warning(f"Error getting account from user: {e}")
account = None
# Fallback 2: If still no account, get default account (for development)
if not account:
from igny8_core.auth.models import Account
try:
# Get the first account as fallback (development only)
account = Account.objects.first()
except Exception as e:
logger.warning(f"Error getting default account: {e}")
account = None
if not account:
logger.error(f"[save_settings] No account found after all fallbacks")
# CRITICAL FIX: Always get account from authenticated user, not from request.account
# request.account can be manipulated or set incorrectly by middleware/auth
# The user's account relationship is the source of truth for their integration settings
user = getattr(request, 'user', None)
if not user or not hasattr(user, 'is_authenticated') or not user.is_authenticated:
logger.error(f"[save_settings] User not authenticated")
return error_response(
error='Account not found. Please ensure you are logged in.',
status_code=status.HTTP_400_BAD_REQUEST,
error='Authentication required',
status_code=status.HTTP_401_UNAUTHORIZED,
request=request
)
# Get account directly from user.account relationship
account = getattr(user, 'account', None)
# CRITICAL SECURITY CHECK: Prevent saving to system accounts
if account and account.slug in ['aws-admin', 'system']:
logger.error(f"[save_settings] BLOCKED: Attempt to save to system account {account.slug} by user {user.id}")
logger.error(f"[save_settings] This indicates the user's account field is incorrectly set to a system account")
return error_response(
error=f'Cannot save integration settings: Your user account is incorrectly linked to system account "{account.slug}". Please contact administrator.',
status_code=status.HTTP_403_FORBIDDEN,
request=request
)
logger.info(f"[save_settings] Account from user.account: {account.id if account else None}")
# CRITICAL: Require valid account - do NOT allow saving without proper account
if not account:
logger.error(f"[save_settings] No account found for user {user.id} ({user.email})")
return error_response(
error='Account not found. Please ensure your user has an account assigned.',
status_code=status.HTTP_401_UNAUTHORIZED,
request=request
)
@@ -752,21 +807,105 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
if not config.get('desktop_image_size'):
config['desktop_image_size'] = '1024x1024'
# Get or create integration settings
logger.info(f"[save_settings] Attempting get_or_create for {integration_type} with account {account.id}")
integration_settings, created = IntegrationSettings.objects.get_or_create(
integration_type=integration_type,
account=account,
defaults={'config': config, 'is_active': config.get('enabled', False)}
)
logger.info(f"[save_settings] get_or_create result: created={created}, id={integration_settings.id}")
# Check if user is changing from global defaults
# Only save IntegrationSettings if config differs from global defaults
global_defaults = self._get_global_defaults(integration_type)
if not created:
logger.info(f"[save_settings] Updating existing settings (id={integration_settings.id})")
integration_settings.config = config
integration_settings.is_active = config.get('enabled', False)
integration_settings.save()
logger.info(f"[save_settings] Settings updated successfully")
# Compare config with global defaults (excluding 'enabled' and 'id' fields)
config_without_metadata = {k: v for k, v in config.items() if k not in ['enabled', 'id']}
defaults_without_keys = {k: v for k, v in global_defaults.items() if k not in ['apiKey', 'id']}
# Check if user is actually changing model or other settings from defaults
is_custom_config = False
for key, value in config_without_metadata.items():
default_value = defaults_without_keys.get(key)
if default_value is not None and str(value) != str(default_value):
is_custom_config = True
logger.info(f"[save_settings] Custom value detected: {key}={value} (default={default_value})")
break
# Get global enabled status
from .global_settings_models import GlobalIntegrationSettings
global_settings_obj = GlobalIntegrationSettings.objects.first()
global_enabled = False
if global_settings_obj:
if integration_type == 'openai':
global_enabled = bool(global_settings_obj.openai_api_key)
elif integration_type == 'runware':
global_enabled = bool(global_settings_obj.runware_api_key)
elif integration_type == 'image_generation':
global_enabled = bool(global_settings_obj.openai_api_key or global_settings_obj.runware_api_key)
user_enabled = config.get('enabled', False)
# Save enable/disable state in IntegrationState model (single record per account)
from igny8_core.ai.models import IntegrationState
# Map integration_type to field name
field_map = {
'openai': 'is_openai_enabled',
'runware': 'is_runware_enabled',
'image_generation': 'is_image_generation_enabled',
}
field_name = field_map.get(integration_type)
if not field_name:
logger.error(f"[save_settings] Unknown integration_type: {integration_type}")
else:
logger.info(f"[save_settings] === CRITICAL DEBUG START ===")
logger.info(f"[save_settings] About to save IntegrationState for integration_type={integration_type}")
logger.info(f"[save_settings] Field name to update: {field_name}")
logger.info(f"[save_settings] Account being used: ID={account.id}, Name={account.name}, Slug={account.slug}")
logger.info(f"[save_settings] User enabled value: {user_enabled}")
logger.info(f"[save_settings] Request user: ID={request.user.id}, Email={request.user.email}")
logger.info(f"[save_settings] Request user account: ID={request.user.account.id if request.user.account else None}")
integration_state, created = IntegrationState.objects.get_or_create(
account=account,
defaults={
'is_openai_enabled': True,
'is_runware_enabled': True,
'is_image_generation_enabled': True,
}
)
logger.info(f"[save_settings] IntegrationState {'CREATED' if created else 'RETRIEVED'}")
logger.info(f"[save_settings] IntegrationState.account: ID={integration_state.account.id}, Name={integration_state.account.name}")
logger.info(f"[save_settings] Before update: {field_name}={getattr(integration_state, field_name)}")
# Update the specific field
setattr(integration_state, field_name, user_enabled)
integration_state.save()
logger.info(f"[save_settings] After update: {field_name}={getattr(integration_state, field_name)}")
logger.info(f"[save_settings] IntegrationState saved to database")
logger.info(f"[save_settings] === CRITICAL DEBUG END ===")
# Save custom config only if different from global defaults
if is_custom_config:
# User has custom settings (different model, etc.) - save override
logger.info(f"[save_settings] User has custom config, saving IntegrationSettings")
integration_settings, created = IntegrationSettings.objects.get_or_create(
integration_type=integration_type,
account=account,
defaults={'config': config_without_metadata, 'is_active': True}
)
if not created:
integration_settings.config = config_without_metadata
integration_settings.save()
logger.info(f"[save_settings] Updated IntegrationSettings config")
else:
logger.info(f"[save_settings] Created new IntegrationSettings for custom config")
else:
# Config matches global defaults - delete any existing override
logger.info(f"[save_settings] User settings match global defaults, removing any account override")
deleted_count, _ = IntegrationSettings.objects.filter(
integration_type=integration_type,
account=account
).delete()
if deleted_count > 0:
logger.info(f"[save_settings] Deleted {deleted_count} IntegrationSettings override(s)")
logger.info(f"[save_settings] Successfully saved settings for {integration_type}")
return success_response(
@@ -786,8 +925,62 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
request=request
)
def _get_global_defaults(self, integration_type):
"""Get global defaults from GlobalIntegrationSettings"""
try:
from .global_settings_models import GlobalIntegrationSettings
global_settings = GlobalIntegrationSettings.objects.first()
if not global_settings:
return {}
defaults = {}
# Map integration_type to GlobalIntegrationSettings fields
if integration_type == 'openai':
defaults = {
'apiKey': global_settings.openai_api_key or '',
'model': global_settings.openai_model.model_name if global_settings.openai_model else 'gpt-4o-mini',
'temperature': float(global_settings.openai_temperature or 0.7),
'maxTokens': int(global_settings.openai_max_tokens or 8192),
}
elif integration_type == 'runware':
defaults = {
'apiKey': global_settings.runware_api_key or '',
'model': global_settings.runware_model.model_name if global_settings.runware_model else 'runware:97@1',
}
elif integration_type == 'image_generation':
provider = global_settings.default_image_service or 'openai'
# Get model based on provider
if provider == 'openai':
model = global_settings.dalle_model.model_name if global_settings.dalle_model else 'dall-e-3'
else: # runware
model = global_settings.runware_model.model_name if global_settings.runware_model else 'runware:97@1'
defaults = {
'provider': provider,
'service': provider, # Alias
'model': model,
'imageModel': model if provider == 'openai' else None,
'runwareModel': model if provider == 'runware' else None,
'image_type': global_settings.image_style or 'vivid',
'image_quality': global_settings.image_quality or 'standard',
'max_in_article_images': global_settings.max_in_article_images or 5,
'desktop_image_size': global_settings.desktop_image_size or '1024x1024',
'mobile_image_size': global_settings.mobile_image_size or '512x512',
'featured_image_size': global_settings.desktop_image_size or '1024x1024',
'desktop_enabled': True,
'mobile_enabled': True,
}
logger.info(f"[_get_global_defaults] {integration_type} defaults: {defaults}")
return defaults
except Exception as e:
logger.error(f"Error getting global defaults for {integration_type}: {e}", exc_info=True)
return {}
def get_settings(self, request, pk=None):
"""Get integration settings - defaults to AWS-admin settings if account doesn't have its own"""
"""Get integration settings - merges global defaults with account-specific overrides"""
integration_type = pk
if not integration_type:
@@ -798,45 +991,130 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
)
try:
# Get account - try multiple methods (same as save_settings)
account = getattr(request, 'account', None)
# CRITICAL FIX: Always get account from authenticated user, not from request.account
# Match the pattern used in save_settings() for consistency
user = getattr(request, 'user', None)
if not user or not hasattr(user, 'is_authenticated') or not user.is_authenticated:
logger.error(f"[get_settings] User not authenticated")
return error_response(
error='Authentication required',
status_code=status.HTTP_401_UNAUTHORIZED,
request=request
)
# Fallback 1: Get from authenticated user's account
if not account:
user = getattr(request, 'user', None)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
try:
account = getattr(user, 'account', None)
except Exception as e:
logger.warning(f"Error getting account from user: {e}")
account = None
# Get account directly from user.account relationship
account = getattr(user, 'account', None)
logger.info(f"[get_settings] Account from user.account: {account.id if account else None}")
from .models import IntegrationSettings
# Get account-specific settings
# Start with global defaults
global_defaults = self._get_global_defaults(integration_type)
# Get account-specific settings and merge
# Get account-specific enabled state from IntegrationState (single record)
from igny8_core.ai.models import IntegrationState
# Map integration_type to field name
field_map = {
'openai': 'is_openai_enabled',
'runware': 'is_runware_enabled',
'image_generation': 'is_image_generation_enabled',
}
account_enabled = None
if account:
try:
integration_state = IntegrationState.objects.get(account=account)
field_name = field_map.get(integration_type)
if field_name:
account_enabled = getattr(integration_state, field_name)
logger.info(f"[get_settings] Found IntegrationState.{field_name}={account_enabled}")
except IntegrationState.DoesNotExist:
logger.info(f"[get_settings] No IntegrationState found, will use global default")
# Try to get account-specific config overrides
if account:
try:
integration_settings = IntegrationSettings.objects.get(
integration_type=integration_type,
account=account
)
# Merge: global defaults + account overrides
merged_config = {**global_defaults, **integration_settings.config}
# Use account-specific enabled state if available, otherwise use global
if account_enabled is not None:
enabled_state = account_enabled
else:
# Fall back to global enabled logic
try:
from .global_settings_models import GlobalIntegrationSettings
global_settings = GlobalIntegrationSettings.objects.first()
if global_settings:
if integration_type == 'openai':
enabled_state = bool(global_settings.openai_api_key)
elif integration_type == 'runware':
enabled_state = bool(global_settings.runware_api_key)
elif integration_type == 'image_generation':
enabled_state = bool(global_settings.openai_api_key or global_settings.runware_api_key)
else:
enabled_state = False
else:
enabled_state = False
except Exception as e:
logger.error(f"Error checking global enabled status: {e}")
enabled_state = False
response_data = {
'id': integration_settings.integration_type,
'enabled': integration_settings.is_active,
**integration_settings.config
'enabled': enabled_state,
**merged_config
}
logger.info(f"[get_settings] Merged settings for {integration_type}: enabled={enabled_state}")
return success_response(
data=response_data,
request=request
)
except IntegrationSettings.DoesNotExist:
logger.info(f"[get_settings] No account settings, returning global defaults for {integration_type}")
pass
except Exception as e:
logger.error(f"Error getting account-specific settings: {e}", exc_info=True)
# Return empty config if no settings found
# Return global defaults with account-specific enabled state if available
# Determine if integration is "enabled" based on IntegrationState or global configuration
if account_enabled is not None:
is_enabled = account_enabled
logger.info(f"[get_settings] Using account IntegrationState: enabled={is_enabled}")
else:
try:
from .global_settings_models import GlobalIntegrationSettings
global_settings = GlobalIntegrationSettings.objects.first()
# Check if global API keys are configured
is_enabled = False
if global_settings:
if integration_type == 'openai':
is_enabled = bool(global_settings.openai_api_key)
elif integration_type == 'runware':
is_enabled = bool(global_settings.runware_api_key)
elif integration_type == 'image_generation':
# Image generation is enabled if either OpenAI or Runware is configured
is_enabled = bool(global_settings.openai_api_key or global_settings.runware_api_key)
logger.info(f"[get_settings] Using global enabled status: enabled={is_enabled} (no account override)")
except Exception as e:
logger.error(f"Error checking global enabled status: {e}")
is_enabled = False
response_data = {
'id': integration_type,
'enabled': is_enabled,
**global_defaults
}
return success_response(
data={},
data=response_data,
request=request
)
except Exception as e:
@@ -849,23 +1127,12 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
@action(detail=False, methods=['get'], url_path='image_generation', url_name='image_generation_settings')
def get_image_generation_settings(self, request):
"""Get image generation settings for current account
Normal users fallback to system account (aws-admin) settings
"""
account = getattr(request, 'account', None)
if not account:
# Fallback to user's account
user = getattr(request, 'user', None)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
account = getattr(user, 'account', None)
# Fallback to default account
if not account:
from igny8_core.auth.models import Account
try:
account = Account.objects.first()
except Exception:
pass
"""Get image generation settings for current account - merges global defaults with account overrides"""
# CRITICAL FIX: Always use user.account directly, never request.account or default account
user = getattr(request, 'user', None)
account = None
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
account = getattr(user, 'account', None)
if not account:
return error_response(
@@ -876,42 +1143,26 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
try:
from .models import IntegrationSettings
from igny8_core.auth.models import Account
# Try to get settings for user's account first
# Start with global defaults
global_defaults = self._get_global_defaults('image_generation')
# Try to get account-specific settings
try:
integration = IntegrationSettings.objects.get(
account=account,
integration_type='image_generation',
is_active=True
)
logger.info(f"[get_image_generation_settings] Found settings for account {account.id}")
config = {**global_defaults, **(integration.config or {})}
logger.info(f"[get_image_generation_settings] Found account settings, merged with globals")
except IntegrationSettings.DoesNotExist:
# Fallback to system account (aws-admin) settings - normal users use centralized settings
logger.info(f"[get_image_generation_settings] No settings for account {account.id}, falling back to system account")
try:
system_account = Account.objects.get(slug='aws-admin')
integration = IntegrationSettings.objects.get(
account=system_account,
integration_type='image_generation',
is_active=True
)
logger.info(f"[get_image_generation_settings] Using system account (aws-admin) settings")
except (Account.DoesNotExist, IntegrationSettings.DoesNotExist):
logger.error("[get_image_generation_settings] No image generation settings found in aws-admin account")
return error_response(
error='Image generation settings not configured in aws-admin account',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
config = integration.config or {}
# Use global defaults only
config = global_defaults
logger.info(f"[get_image_generation_settings] No account settings, using global defaults")
# Debug: Log what's actually in the config
logger.info(f"[get_image_generation_settings] Full config: {config}")
logger.info(f"[get_image_generation_settings] Config keys: {list(config.keys())}")
logger.info(f"[get_image_generation_settings] model field: {config.get('model')}")
logger.info(f"[get_image_generation_settings] imageModel field: {config.get('imageModel')}")
logger.info(f"[get_image_generation_settings] Final config: {config}")
# Get model - try 'model' first, then 'imageModel' as fallback
model = config.get('model') or config.get('imageModel') or 'dall-e-3'
@@ -936,12 +1187,6 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
},
request=request
)
except IntegrationSettings.DoesNotExist:
return error_response(
error='Image generation settings not configured',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
except Exception as e:
logger.error(f"[get_image_generation_settings] Error: {str(e)}", exc_info=True)
return error_response(

View File

@@ -0,0 +1,106 @@
# Generated by Django 5.2.9 on 2025-12-23 08:40
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0003_globalmodulesettings'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='GlobalAIPrompt',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('prompt_type', models.CharField(choices=[('clustering', 'Clustering'), ('ideas', 'Ideas Generation'), ('content_generation', 'Content Generation'), ('image_prompt_extraction', 'Image Prompt Extraction'), ('image_prompt_template', 'Image Prompt Template'), ('negative_prompt', 'Negative Prompt'), ('site_structure_generation', 'Site Structure Generation'), ('product_generation', 'Product Content Generation'), ('service_generation', 'Service Page Generation'), ('taxonomy_generation', 'Taxonomy Generation')], help_text='Type of AI operation this prompt is for', max_length=50, unique=True)),
('prompt_value', models.TextField(help_text='Default prompt template')),
('description', models.TextField(blank=True, help_text='Description of what this prompt does')),
('variables', models.JSONField(blank=True, default=list, help_text='Optional: List of variables used in the prompt (e.g., {keyword}, {industry})')),
('is_active', models.BooleanField(db_index=True, default=True)),
('version', models.IntegerField(default=1, help_text='Prompt version for tracking changes')),
('last_updated', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'Global AI Prompt',
'verbose_name_plural': 'Global AI Prompts',
'db_table': 'igny8_global_ai_prompts',
'ordering': ['prompt_type'],
},
),
migrations.CreateModel(
name='GlobalAuthorProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text="Profile name (e.g., 'SaaS B2B Professional')", max_length=255, unique=True)),
('description', models.TextField(help_text='Description of the writing style')),
('tone', models.CharField(help_text="Writing tone (e.g., 'Professional', 'Casual', 'Technical')", max_length=100)),
('language', models.CharField(default='en', help_text='Language code', max_length=50)),
('structure_template', models.JSONField(default=dict, help_text='Structure template defining content sections')),
('category', models.CharField(choices=[('saas', 'SaaS/B2B'), ('ecommerce', 'E-commerce'), ('blog', 'Blog/Publishing'), ('technical', 'Technical'), ('creative', 'Creative'), ('news', 'News/Media'), ('academic', 'Academic')], help_text='Profile category', max_length=50)),
('is_active', models.BooleanField(db_index=True, default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Global Author Profile',
'verbose_name_plural': 'Global Author Profiles',
'db_table': 'igny8_global_author_profiles',
'ordering': ['category', 'name'],
},
),
migrations.CreateModel(
name='GlobalStrategy',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Strategy name', max_length=255, unique=True)),
('description', models.TextField(help_text='Description of the content strategy')),
('prompt_types', models.JSONField(default=list, help_text='List of prompt types to use')),
('section_logic', models.JSONField(default=dict, help_text='Section logic configuration')),
('category', models.CharField(choices=[('blog', 'Blog Content'), ('ecommerce', 'E-commerce'), ('saas', 'SaaS/B2B'), ('news', 'News/Media'), ('technical', 'Technical Documentation'), ('marketing', 'Marketing Content')], help_text='Strategy category', max_length=50)),
('is_active', models.BooleanField(db_index=True, default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Global Strategy',
'verbose_name_plural': 'Global Strategies',
'db_table': 'igny8_global_strategies',
'ordering': ['category', 'name'],
},
),
migrations.CreateModel(
name='GlobalIntegrationSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('openai_api_key', models.CharField(blank=True, help_text='Platform OpenAI API key - used by ALL accounts', max_length=500)),
('openai_model', models.CharField(choices=[('gpt-4.1', 'GPT-4.1 - $2.00 / $8.00 per 1M tokens'), ('gpt-4o-mini', 'GPT-4o mini - $0.15 / $0.60 per 1M tokens'), ('gpt-4o', 'GPT-4o - $2.50 / $10.00 per 1M tokens'), ('gpt-4-turbo-preview', 'GPT-4 Turbo Preview - $10.00 / $30.00 per 1M tokens'), ('gpt-5.1', 'GPT-5.1 - $1.25 / $10.00 per 1M tokens (16K)'), ('gpt-5.2', 'GPT-5.2 - $1.75 / $14.00 per 1M tokens (16K)')], default='gpt-4o-mini', help_text='Default text generation model (accounts can override if plan allows)', max_length=100)),
('openai_temperature', models.FloatField(default=0.7, help_text='Default temperature 0.0-2.0 (accounts can override if plan allows)')),
('openai_max_tokens', models.IntegerField(default=8192, help_text='Default max tokens for responses (accounts can override if plan allows)')),
('dalle_api_key', models.CharField(blank=True, help_text='Platform DALL-E API key - used by ALL accounts (can be same as OpenAI)', max_length=500)),
('dalle_model', models.CharField(choices=[('dall-e-3', 'DALL·E 3 - $0.040 per image'), ('dall-e-2', 'DALL·E 2 - $0.020 per image')], default='dall-e-3', help_text='Default DALL-E model (accounts can override if plan allows)', max_length=100)),
('dalle_size', models.CharField(choices=[('1024x1024', '1024x1024 (Square)'), ('1792x1024', '1792x1024 (Landscape)'), ('1024x1792', '1024x1792 (Portrait)'), ('512x512', '512x512 (Small Square)')], default='1024x1024', help_text='Default image size (accounts can override if plan allows)', max_length=20)),
('runware_api_key', models.CharField(blank=True, help_text='Platform Runware API key - used by ALL accounts', max_length=500)),
('runware_model', models.CharField(choices=[('runware:97@1', 'Runware 97@1 - Versatile Model'), ('runware:100@1', 'Runware 100@1 - High Quality'), ('runware:101@1', 'Runware 101@1 - Fast Generation')], default='runware:97@1', help_text='Default Runware model (accounts can override if plan allows)', max_length=100)),
('default_image_service', models.CharField(choices=[('openai', 'OpenAI DALL-E'), ('runware', 'Runware')], default='openai', help_text='Default image generation service for all accounts (openai=DALL-E, runware=Runware)', max_length=20)),
('image_quality', models.CharField(choices=[('standard', 'Standard'), ('hd', 'HD')], default='standard', help_text='Default image quality for all providers (accounts can override if plan allows)', max_length=20)),
('image_style', models.CharField(choices=[('vivid', 'Vivid'), ('natural', 'Natural'), ('realistic', 'Realistic'), ('artistic', 'Artistic'), ('cartoon', 'Cartoon')], default='realistic', help_text='Default image style for all providers (accounts can override if plan allows)', max_length=20)),
('max_in_article_images', models.IntegerField(default=2, help_text='Default maximum images to generate per article (1-5, accounts can override if plan allows)')),
('desktop_image_size', models.CharField(default='1024x1024', help_text='Default desktop image size (accounts can override if plan allows)', max_length=20)),
('mobile_image_size', models.CharField(default='512x512', help_text='Default mobile image size (accounts can override if plan allows)', max_length=20)),
('is_active', models.BooleanField(default=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='global_settings_updates', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Global Integration Settings',
'verbose_name_plural': 'Global Integration Settings',
'db_table': 'igny8_global_integration_settings',
},
),
]

View File

@@ -0,0 +1,183 @@
# Generated by Django 5.2.9 on 2025-12-23 (custom data migration)
import django.db.models.deletion
from django.db import migrations, models
def migrate_model_strings_to_fks(apps, schema_editor):
"""Convert CharField model identifiers to ForeignKey references"""
GlobalIntegrationSettings = apps.get_model('system', 'GlobalIntegrationSettings')
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
# Get the singleton GlobalIntegrationSettings instance
try:
settings = GlobalIntegrationSettings.objects.first()
if not settings:
print(" No GlobalIntegrationSettings found, skipping data migration")
return
# Map openai_model string to AIModelConfig FK
if settings.openai_model_old:
model_name = settings.openai_model_old
# Try to find matching model
openai_model = AIModelConfig.objects.filter(
model_name=model_name,
provider='openai',
model_type='text'
).first()
if openai_model:
settings.openai_model_new = openai_model
print(f" ✓ Mapped openai_model: {model_name}{openai_model.id}")
else:
# Try gpt-4o-mini as fallback
openai_model = AIModelConfig.objects.filter(
model_name='gpt-4o-mini',
provider='openai',
model_type='text'
).first()
if openai_model:
settings.openai_model_new = openai_model
print(f" ⚠ Could not find {model_name}, using fallback: gpt-4o-mini")
# Map dalle_model string to AIModelConfig FK
if settings.dalle_model_old:
model_name = settings.dalle_model_old
dalle_model = AIModelConfig.objects.filter(
model_name=model_name,
provider='openai',
model_type='image'
).first()
if dalle_model:
settings.dalle_model_new = dalle_model
print(f" ✓ Mapped dalle_model: {model_name}{dalle_model.id}")
else:
# Try dall-e-3 as fallback
dalle_model = AIModelConfig.objects.filter(
model_name='dall-e-3',
provider='openai',
model_type='image'
).first()
if dalle_model:
settings.dalle_model_new = dalle_model
print(f" ⚠ Could not find {model_name}, using fallback: dall-e-3")
# Map runware_model string to AIModelConfig FK
if settings.runware_model_old:
model_name = settings.runware_model_old
# Runware models might have different naming
runware_model = AIModelConfig.objects.filter(
provider='runware',
model_type='image'
).first() # Just get first active Runware model
if runware_model:
settings.runware_model_new = runware_model
print(f" ✓ Mapped runware_model: {model_name}{runware_model.id}")
settings.save()
print(" ✅ Data migration complete")
except Exception as e:
print(f" ⚠ Error during data migration: {e}")
# Don't fail the migration, let admin fix it manually
class Migration(migrations.Migration):
dependencies = [
('billing', '0019_add_ai_model_config'),
('system', '0004_add_global_integration_models'),
]
operations = [
# Step 1: Add new FK fields with temporary names
migrations.AddField(
model_name='globalintegrationsettings',
name='openai_model_new',
field=models.ForeignKey(
blank=True,
null=True,
help_text='Default text generation model (accounts can override if plan allows)',
limit_choices_to={'is_active': True, 'model_type': 'text', 'provider': 'openai'},
on_delete=django.db.models.deletion.PROTECT,
related_name='global_openai_text_model_new',
to='billing.aimodelconfig'
),
),
migrations.AddField(
model_name='globalintegrationsettings',
name='dalle_model_new',
field=models.ForeignKey(
blank=True,
null=True,
help_text='Default DALL-E model (accounts can override if plan allows)',
limit_choices_to={'is_active': True, 'model_type': 'image', 'provider': 'openai'},
on_delete=django.db.models.deletion.PROTECT,
related_name='global_dalle_model_new',
to='billing.aimodelconfig'
),
),
migrations.AddField(
model_name='globalintegrationsettings',
name='runware_model_new',
field=models.ForeignKey(
blank=True,
null=True,
help_text='Default Runware model (accounts can override if plan allows)',
limit_choices_to={'is_active': True, 'model_type': 'image', 'provider': 'runware'},
on_delete=django.db.models.deletion.PROTECT,
related_name='global_runware_model_new',
to='billing.aimodelconfig'
),
),
# Step 2: Rename old CharField fields
migrations.RenameField(
model_name='globalintegrationsettings',
old_name='openai_model',
new_name='openai_model_old',
),
migrations.RenameField(
model_name='globalintegrationsettings',
old_name='dalle_model',
new_name='dalle_model_old',
),
migrations.RenameField(
model_name='globalintegrationsettings',
old_name='runware_model',
new_name='runware_model_old',
),
# Step 3: Run data migration
migrations.RunPython(migrate_model_strings_to_fks, migrations.RunPython.noop),
# Step 4: Remove old CharField fields
migrations.RemoveField(
model_name='globalintegrationsettings',
name='openai_model_old',
),
migrations.RemoveField(
model_name='globalintegrationsettings',
name='dalle_model_old',
),
migrations.RemoveField(
model_name='globalintegrationsettings',
name='runware_model_old',
),
# Step 5: Rename new FK fields to final names
migrations.RenameField(
model_name='globalintegrationsettings',
old_name='openai_model_new',
new_name='openai_model',
),
migrations.RenameField(
model_name='globalintegrationsettings',
old_name='dalle_model_new',
new_name='dalle_model',
),
migrations.RenameField(
model_name='globalintegrationsettings',
old_name='runware_model_new',
new_name='runware_model',
),
]

View File

@@ -0,0 +1,50 @@
# Generated by Django 5.2.9 on 2025-12-23 14:24
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billing', '0020_add_optimizer_publisher_timestamps'),
('system', '0005_link_global_settings_to_aimodelconfig'),
]
operations = [
migrations.AddField(
model_name='globalmodulesettings',
name='created_at',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='globalmodulesettings',
name='optimizer_enabled',
field=models.BooleanField(default=True, help_text='Enable Optimizer module platform-wide'),
),
migrations.AddField(
model_name='globalmodulesettings',
name='publisher_enabled',
field=models.BooleanField(default=True, help_text='Enable Publisher module platform-wide'),
),
migrations.AddField(
model_name='globalmodulesettings',
name='updated_at',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='dalle_model',
field=models.ForeignKey(blank=True, help_text='Default DALL-E model (accounts can override if plan allows)', limit_choices_to={'is_active': True, 'model_type': 'image', 'provider': 'openai'}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='global_dalle_model', to='billing.aimodelconfig'),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='openai_model',
field=models.ForeignKey(blank=True, help_text='Default text generation model (accounts can override if plan allows)', limit_choices_to={'is_active': True, 'model_type': 'text', 'provider': 'openai'}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='global_openai_text_model', to='billing.aimodelconfig'),
),
migrations.AlterField(
model_name='globalintegrationsettings',
name='runware_model',
field=models.ForeignKey(blank=True, help_text='Default Runware model (accounts can override if plan allows)', limit_choices_to={'is_active': True, 'model_type': 'image', 'provider': 'runware'}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='global_runware_model', to='billing.aimodelconfig'),
),
]

View File

@@ -0,0 +1,194 @@
#!/usr/bin/env python
"""
Seed AIModelConfig with the CORRECT models from GlobalIntegrationSettings choices.
These are the models that should be available in the dropdowns.
"""
import os
import sys
import django
# Setup Django
sys.path.insert(0, '/app')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings')
django.setup()
from decimal import Decimal
from igny8_core.business.billing.models import AIModelConfig
def seed_models():
"""Create AIModelConfig records for all models that were in GlobalIntegrationSettings"""
models_to_create = [
# OpenAI Text Models (from OPENAI_MODEL_CHOICES)
{
'model_name': 'gpt-4.1',
'display_name': 'GPT-4.1',
'provider': 'openai',
'model_type': 'text',
'cost_per_1k_input_tokens': Decimal('0.002'), # $2.00 per 1M = $0.002 per 1K
'cost_per_1k_output_tokens': Decimal('0.008'), # $8.00 per 1M
'tokens_per_credit': 100,
'is_active': True,
},
{
'model_name': 'gpt-4o-mini',
'display_name': 'GPT-4o Mini',
'provider': 'openai',
'model_type': 'text',
'cost_per_1k_input_tokens': Decimal('0.00015'), # $0.15 per 1M
'cost_per_1k_output_tokens': Decimal('0.0006'), # $0.60 per 1M
'tokens_per_credit': 100,
'is_active': True,
},
{
'model_name': 'gpt-4o',
'display_name': 'GPT-4o',
'provider': 'openai',
'model_type': 'text',
'cost_per_1k_input_tokens': Decimal('0.0025'), # $2.50 per 1M
'cost_per_1k_output_tokens': Decimal('0.01'), # $10.00 per 1M
'tokens_per_credit': 100,
'is_active': True,
},
{
'model_name': 'gpt-4-turbo-preview',
'display_name': 'GPT-4 Turbo Preview',
'provider': 'openai',
'model_type': 'text',
'cost_per_1k_input_tokens': Decimal('0.01'), # $10.00 per 1M
'cost_per_1k_output_tokens': Decimal('0.03'), # $30.00 per 1M
'tokens_per_credit': 100,
'is_active': True,
},
{
'model_name': 'gpt-5.1',
'display_name': 'GPT-5.1 (16K)',
'provider': 'openai',
'model_type': 'text',
'cost_per_1k_input_tokens': Decimal('0.00125'), # $1.25 per 1M
'cost_per_1k_output_tokens': Decimal('0.01'), # $10.00 per 1M
'tokens_per_credit': 100,
'is_active': True,
},
{
'model_name': 'gpt-5.2',
'display_name': 'GPT-5.2 (16K)',
'provider': 'openai',
'model_type': 'text',
'cost_per_1k_input_tokens': Decimal('0.00175'), # $1.75 per 1M
'cost_per_1k_output_tokens': Decimal('0.014'), # $14.00 per 1M
'tokens_per_credit': 100,
'is_active': True,
},
# OpenAI Image Models (from DALLE_MODEL_CHOICES)
{
'model_name': 'dall-e-3',
'display_name': 'DALL·E 3',
'provider': 'openai',
'model_type': 'image',
'cost_per_1k_input_tokens': Decimal('0.04'), # $0.040 per image
'cost_per_1k_output_tokens': Decimal('0.00'),
'tokens_per_credit': 1, # 1 image = 1 unit
'is_active': True,
},
{
'model_name': 'dall-e-2',
'display_name': 'DALL·E 2',
'provider': 'openai',
'model_type': 'image',
'cost_per_1k_input_tokens': Decimal('0.02'), # $0.020 per image
'cost_per_1k_output_tokens': Decimal('0.00'),
'tokens_per_credit': 1,
'is_active': True,
},
# Runware Image Models (from RUNWARE_MODEL_CHOICES)
{
'model_name': 'runware:97@1',
'display_name': 'Runware 97@1 (Versatile)',
'provider': 'runware',
'model_type': 'image',
'cost_per_1k_input_tokens': Decimal('0.005'), # Estimated
'cost_per_1k_output_tokens': Decimal('0.00'),
'tokens_per_credit': 1,
'is_active': True,
},
{
'model_name': 'runware:100@1',
'display_name': 'Runware 100@1 (High Quality)',
'provider': 'runware',
'model_type': 'image',
'cost_per_1k_input_tokens': Decimal('0.008'), # Estimated
'cost_per_1k_output_tokens': Decimal('0.00'),
'tokens_per_credit': 1,
'is_active': True,
},
{
'model_name': 'runware:101@1',
'display_name': 'Runware 101@1 (Fast)',
'provider': 'runware',
'model_type': 'image',
'cost_per_1k_input_tokens': Decimal('0.003'), # Estimated
'cost_per_1k_output_tokens': Decimal('0.00'),
'tokens_per_credit': 1,
'is_active': True,
},
]
print("Seeding AIModelConfig with correct models...")
print("=" * 70)
created_count = 0
updated_count = 0
for model_data in models_to_create:
model, created = AIModelConfig.objects.update_or_create(
model_name=model_data['model_name'],
provider=model_data['provider'],
defaults=model_data
)
if created:
created_count += 1
print(f"✓ Created: {model.display_name} ({model.model_name})")
else:
updated_count += 1
print(f"↻ Updated: {model.display_name} ({model.model_name})")
print("=" * 70)
print(f"Summary: {created_count} created, {updated_count} updated")
# Set default models
print("\nSetting default models...")
# Default text model: gpt-4o-mini
default_text = AIModelConfig.objects.filter(model_name='gpt-4o-mini').first()
if default_text:
AIModelConfig.objects.filter(model_type='text').update(is_default=False)
default_text.is_default = True
default_text.save()
print(f"✓ Default text model: {default_text.display_name}")
# Default image model: dall-e-3
default_image = AIModelConfig.objects.filter(model_name='dall-e-3').first()
if default_image:
AIModelConfig.objects.filter(model_type='image').update(is_default=False)
default_image.is_default = True
default_image.save()
print(f"✓ Default image model: {default_image.display_name}")
print("\n✅ Seeding complete!")
# Show summary
print("\nActive models by type:")
print("-" * 70)
for model_type in ['text', 'image']:
models = AIModelConfig.objects.filter(model_type=model_type, is_active=True)
print(f"\n{model_type.upper()}: {models.count()} models")
for m in models:
default = " [DEFAULT]" if m.is_default else ""
print(f" - {m.display_name} ({m.model_name}) - {m.provider}{default}")
if __name__ == '__main__':
seed_models()