diff --git a/backend/igny8_core/ai/engine.py b/backend/igny8_core/ai/engine.py index c1a179fd..157df9e0 100644 --- a/backend/igny8_core/ai/engine.py +++ b/backend/igny8_core/ai/engine.py @@ -376,18 +376,34 @@ class AIEngine: # Map function name to operation type operation_type = self._get_operation_type(function_name) - # Calculate actual amount based on results + # Extract token usage from AI response (standardize key names) + tokens_input = raw_response.get('input_tokens', 0) + tokens_output = raw_response.get('output_tokens', 0) + total_tokens = tokens_input + tokens_output + + # Get model_config for token-based billing + model_config = None + if tokens_input > 0 or tokens_output > 0: + # Get model from response or use account default + model_config = CreditService.get_model_for_operation( + account=self.account, + operation_type=operation_type, + task_model_override=None # TODO: Support task-level model override + ) + + # Calculate actual amount based on results (for non-token operations) actual_amount = self._get_actual_amount(function_name, save_result, parsed, data) - # Deduct credits using the new convenience method + # Deduct credits using token-based calculation if tokens available CreditService.deduct_credits_for_operation( account=self.account, operation_type=operation_type, - amount=actual_amount, + amount=actual_amount, # Fallback for non-token operations + tokens_input=tokens_input, + tokens_output=tokens_output, + model_config=model_config, cost_usd=raw_response.get('cost'), model_used=raw_response.get('model', ''), - tokens_input=raw_response.get('tokens_input', 0), - tokens_output=raw_response.get('tokens_output', 0), related_object_type=self._get_related_object_type(function_name), related_object_id=save_result.get('id') or save_result.get('cluster_id') or save_result.get('task_id'), metadata={ @@ -399,7 +415,7 @@ class AIEngine: } ) - logger.info(f"[AIEngine] Credits deducted: {operation_type}, amount: {actual_amount}") + logger.info(f"[AIEngine] Credits deducted: {operation_type}, tokens: {total_tokens} ({tokens_input} in, {tokens_output} out), model: {model_config.model_name if model_config else 'legacy'}") except InsufficientCreditsError as e: # This shouldn't happen since we checked before, but log it logger.error(f"[AIEngine] Insufficient credits during deduction: {e}") diff --git a/backend/igny8_core/modules/system/global_settings_models.py b/backend/igny8_core/modules/system/global_settings_models.py new file mode 100644 index 00000000..2b2761f1 --- /dev/null +++ b/backend/igny8_core/modules/system/global_settings_models.py @@ -0,0 +1,62 @@ +""" +Global Module Settings - Platform-wide module enable/disable +Singleton model for system-wide control +""" +from django.db import models + + +class GlobalModuleSettings(models.Model): + """ + Global module enable/disable settings (platform-wide). + + Singleton model - only one record exists (pk=1). + Controls which modules are available across the entire platform. + No per-account overrides allowed - this is admin-only control. + """ + planner_enabled = models.BooleanField( + default=True, + help_text="Enable Planner module platform-wide" + ) + writer_enabled = models.BooleanField( + default=True, + help_text="Enable Writer module platform-wide" + ) + thinker_enabled = models.BooleanField( + default=True, + help_text="Enable Thinker module platform-wide" + ) + automation_enabled = models.BooleanField( + default=True, + help_text="Enable Automation module platform-wide" + ) + site_builder_enabled = models.BooleanField( + default=True, + help_text="Enable Site Builder module platform-wide" + ) + linker_enabled = models.BooleanField( + default=True, + help_text="Enable Linker module platform-wide" + ) + + class Meta: + verbose_name = "Global Module Settings" + verbose_name_plural = "Global Module Settings" + db_table = "igny8_global_module_settings" + + def __str__(self): + return "Global Module Settings" + + @classmethod + def get_settings(cls): + """Get or create singleton instance""" + obj, created = cls.objects.get_or_create(pk=1) + return obj + + def save(self, *args, **kwargs): + """Enforce singleton pattern""" + self.pk = 1 + super().save(*args, **kwargs) + + def delete(self, *args, **kwargs): + """Prevent deletion""" + pass diff --git a/backend/igny8_core/modules/system/migrations/0003_globalmodulesettings.py b/backend/igny8_core/modules/system/migrations/0003_globalmodulesettings.py new file mode 100644 index 00000000..f7a67cf2 --- /dev/null +++ b/backend/igny8_core/modules/system/migrations/0003_globalmodulesettings.py @@ -0,0 +1,30 @@ +# Generated by Django on 2025-12-23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('system', '0002_add_model_fk_to_integrations'), + ] + + operations = [ + migrations.CreateModel( + name='GlobalModuleSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('planner_enabled', models.BooleanField(default=True, help_text='Enable Planner module platform-wide')), + ('writer_enabled', models.BooleanField(default=True, help_text='Enable Writer module platform-wide')), + ('thinker_enabled', models.BooleanField(default=True, help_text='Enable Thinker module platform-wide')), + ('automation_enabled', models.BooleanField(default=True, help_text='Enable Automation module platform-wide')), + ('site_builder_enabled', models.BooleanField(default=True, help_text='Enable Site Builder module platform-wide')), + ('linker_enabled', models.BooleanField(default=True, help_text='Enable Linker module platform-wide')), + ], + options={ + 'verbose_name': 'Global Module Settings', + 'verbose_name_plural': 'Global Module Settings', + 'db_table': 'igny8_global_module_settings', + }, + ), + ]