diff --git a/myproject/integrations/migrations/0010_alter_openrouterintegration_model_name.py b/myproject/integrations/migrations/0010_alter_openrouterintegration_model_name.py new file mode 100644 index 0000000..4cb932b --- /dev/null +++ b/myproject/integrations/migrations/0010_alter_openrouterintegration_model_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.10 on 2026-01-23 15:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('integrations', '0009_alter_glmintegration_model_name_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='openrouterintegration', + name='model_name', + field=models.CharField(blank=True, default='', help_text='Название используемой модели OpenRouter (загружается автоматически)', max_length=200, verbose_name='Название модели'), + ), + ] diff --git a/myproject/integrations/models/ai_services/openrouter.py b/myproject/integrations/models/ai_services/openrouter.py index 6d91960..f375c80 100644 --- a/myproject/integrations/models/ai_services/openrouter.py +++ b/myproject/integrations/models/ai_services/openrouter.py @@ -10,14 +10,6 @@ def validate_temperature(value): raise ValidationError('Температура должна быть в диапазоне 0.0-2.0') -# Список доступных моделей OpenRouter (бесплатные) -OPENROUTER_MODEL_CHOICES = [ - ('xiaomi/mimo-v2-flash:free', 'Xiaomi MIMO v2 Flash (Бесплатная)'), - ('mistralai/devstral-2512:free', 'Mistral Devstral 2512 (Бесплатная)'), - ('z-ai/glm-4.5-air:free', 'Z.AI GLM-4.5 Air (Бесплатная)'), - ('qwen/qwen3-coder:free', 'Qwen 3 Coder (Бесплатная)'), -] - # Предустановленные значения температуры OPENROUTER_TEMPERATURE_CHOICES = [ (0.1, '0.1 - Очень консервативно'), @@ -59,11 +51,11 @@ class OpenRouterIntegration(AIIntegration): ) model_name = models.CharField( - max_length=100, - default="xiaomi/mimo-v2-flash:free", - choices=OPENROUTER_MODEL_CHOICES, + max_length=200, + default="", + blank=True, verbose_name="Название модели", - help_text="Название используемой модели OpenRouter" + help_text="Название используемой модели OpenRouter (загружается автоматически)" ) temperature = models.FloatField( diff --git a/myproject/integrations/urls.py b/myproject/integrations/urls.py index dab9ef1..5e3394d 100644 --- a/myproject/integrations/urls.py +++ b/myproject/integrations/urls.py @@ -6,6 +6,7 @@ from .views import ( get_integration_form_data, test_integration_connection, RecommerceBatchSyncView, + get_openrouter_models, ) app_name = 'integrations' @@ -22,4 +23,7 @@ urlpatterns = [ # Синхронизация path("recommerce/sync/", RecommerceBatchSyncView.as_view(), name="recommerce_sync"), + + # OpenRouter модели + path("openrouter/models/", get_openrouter_models, name="openrouter_models"), ] diff --git a/myproject/integrations/views.py b/myproject/integrations/views.py index 5f32330..eb71a3f 100644 --- a/myproject/integrations/views.py +++ b/myproject/integrations/views.py @@ -1,7 +1,10 @@ import json +import logging from django.views.generic import TemplateView from django.http import JsonResponse -from django.views.decorators.http import require_POST +from django.views.decorators.http import require_POST, require_GET + +logger = logging.getLogger(__name__) from user_roles.mixins import OwnerRequiredMixin from .models import RecommerceIntegration, WooCommerceIntegration, GLMIntegration, OpenRouterIntegration @@ -181,6 +184,44 @@ def get_integration_service(integration_id: str, instance): return None +@require_GET +def get_openrouter_models(request): + """ + GET /settings/integrations/openrouter/models/ + Возвращает список моделей OpenRouter (бесплатные сверху) + """ + import requests + + try: + response = requests.get('https://openrouter.ai/api/v1/models', timeout=10) + response.raise_for_status() + data = response.json() + + models = data.get('data', []) + + # Разделить на бесплатные и платные + free_models = [] + paid_models = [] + + for model in models: + model_id = model.get('id', '') + model_name = model.get('name', model_id) + + if ':free' in model_id: + free_models.append({'id': model_id, 'name': f"{model_name} (Бесплатная)"}) + else: + paid_models.append({'id': model_id, 'name': model_name}) + + # Бесплатные сверху + all_models = free_models + paid_models + + return JsonResponse({'models': all_models}) + + except Exception as e: + logger.error(f"Error fetching OpenRouter models: {e}") + return JsonResponse({'error': str(e)}, status=500) + + class RecommerceBatchSyncView(TemplateView): """ API View для запуска массовой синхронизации с Recommerce. @@ -363,29 +404,33 @@ def get_form_fields_meta(model): 'label': getattr(field, 'verbose_name', field_name), 'help_text': getattr(field, 'help_text', ''), 'required': not getattr(field, 'blank', True), - 'type': 'text', # default + 'type': 'password' if field_name == 'api_key' else 'text', } - - # Определить тип поля - if 'BooleanField' in field.__class__.__name__: - field_info['type'] = 'checkbox' - elif 'URLField' in field.__class__.__name__: - field_info['type'] = 'url' - elif 'secret' in field_name.lower() or 'token' in field_name.lower() or 'key' in field_name.lower(): - field_info['type'] = 'password' - fields.append(field_info) - elif field_name in ['model_name', 'temperature']: + + elif field_name == 'temperature': field = model._meta.get_field(field_name) field_info = { 'name': field_name, 'label': getattr(field, 'verbose_name', field_name), 'help_text': getattr(field, 'help_text', ''), 'required': not getattr(field, 'blank', True), - 'type': 'select', # dropdown + 'type': 'select', 'choices': getattr(field, 'choices', []) } + fields.append(field_info) + elif field_name == 'model_name': + field = model._meta.get_field(field_name) + field_info = { + 'name': field_name, + 'label': getattr(field, 'verbose_name', field_name), + 'help_text': getattr(field, 'help_text', ''), + 'required': not getattr(field, 'blank', True), + 'type': 'select', + 'dynamic_choices': True, + 'choices_url': '/settings/integrations/openrouter/models/' + } fields.append(field_info) # Для WooCommerce показываем только базовые поля для подключения elif model.__name__ == 'WooCommerceIntegration': diff --git a/myproject/system_settings/templates/system_settings/integrations.html b/myproject/system_settings/templates/system_settings/integrations.html index 0c6b3e5..d0966c6 100644 --- a/myproject/system_settings/templates/system_settings/integrations.html +++ b/myproject/system_settings/templates/system_settings/integrations.html @@ -153,8 +153,8 @@ document.addEventListener('DOMContentLoaded', function() { } statusBadge.style.display = 'inline'; - // Построить форму - buildForm(data.fields, data.data || {}); + // Построить форму (теперь асинхронно) + await buildForm(data.fields, data.data || {}); // Показать/скрыть кнопку тестирования const testBtn = document.getElementById('test-connection-btn'); @@ -173,14 +173,14 @@ document.addEventListener('DOMContentLoaded', function() { } // Построение формы из метаданных полей - function buildForm(fields, data) { + async function buildForm(fields, data) { const container = document.getElementById('settings-fields'); container.innerHTML = ''; - - fields.forEach(field => { + + for (const field of fields) { const div = document.createElement('div'); div.className = 'mb-3'; - + if (field.type === 'checkbox') { div.className = 'form-check mb-3'; div.innerHTML = ` @@ -189,43 +189,100 @@ document.addEventListener('DOMContentLoaded', function() { ${field.help_text ? `