feat(integrations): add dynamic OpenRouter model loading
- Remove hardcoded OPENROUTER_MODEL_CHOICES from openrouter.py
- Add API endpoint /integrations/openrouter/models/ to fetch models dynamically
- Models loaded from OpenRouter API with free models (':free') at top
- Update OpenRouterIntegration model_name field (remove choices, blank=True)
- Add async buildForm() with dynamic_choices support
- Show asterisks (********) for saved API keys with helpful placeholder
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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='Название модели'),
|
||||
),
|
||||
]
|
||||
@@ -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(
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user