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:
2026-01-23 18:16:12 +03:00
parent 3aac83474b
commit 5ec5ee48d4
5 changed files with 173 additions and 57 deletions

View File

@@ -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() {
<label class="form-check-label" for="field-${field.name}">${field.label}</label>
${field.help_text ? `<div class="form-text">${field.help_text}</div>` : ''}
`;
} else if (field.type === 'select') {
div.innerHTML = `
<label class="form-label" for="field-${field.name}">
${field.label}
${field.required ? '<span class="text-danger">*</span>' : ''}
</label>
<select class="form-select" id="field-${field.name}"
name="${field.name}"
${field.required ? 'required' : ''}>
${field.choices.map(choice => `
<option value="${choice[0]}" ${data[field.name] === choice[0] ? 'selected' : ''}>
${choice[1]}
</option>
`).join('')}
</select>
${field.help_text ? `<div class="form-text">${field.help_text}</div>` : ''}
`;
let optionsHtml = '';
if (field.dynamic_choices) {
// Динамическая загрузка options
optionsHtml = '<option value="">Загрузка моделей...</option>';
div.innerHTML = `
<label class="form-label" for="field-${field.name}">
${field.label}
${field.required ? '<span class="text-danger">*</span>' : ''}
</label>
<select class="form-select" id="field-${field.name}"
name="${field.name}"
${field.required ? 'required' : ''}>
${optionsHtml}
</select>
${field.help_text ? `<div class="form-text">${field.help_text}</div>` : ''}
`;
container.appendChild(div);
// Асинхронная загрузка
const select = div.querySelector('select');
try {
const response = await fetch(field.choices_url);
const result = await response.json();
if (result.error) {
select.innerHTML = '<option value="">Ошибка загрузки моделей</option>';
console.error(result.error);
} else {
select.innerHTML = result.models.map(m =>
`<option value="${m.id}">${m.name}</option>`
).join('');
if (data[field.name]) {
select.value = data[field.name];
}
}
} catch (error) {
select.innerHTML = '<option value="">Ошибка загрузки моделей</option>';
console.error('Error loading models:', error);
}
} else {
// Статический select (для temperature)
optionsHtml = field.choices.map(choice => `
<option value="${choice[0]}" ${data[field.name] === choice[0] ? 'selected' : ''}>
${choice[1]}
</option>
`).join('');
div.innerHTML = `
<label class="form-label" for="field-${field.name}">
${field.label}
${field.required ? '<span class="text-danger">*</span>' : ''}
</label>
<select class="form-select" id="field-${field.name}"
name="${field.name}"
${field.required ? 'required' : ''}>
${optionsHtml}
</select>
${field.help_text ? `<div class="form-text">${field.help_text}</div>` : ''}
`;
}
} else {
// text, password, url
const inputType = field.type === 'password' ? 'password' : (field.type === 'url' ? 'url' : 'text');
const value = data[field.name] || '';
const placeholder = field.type === 'password' && value === '........' ? 'Введите новое значение для изменения' : '';
let value = data[field.name] || '';
const isMasked = value === '••••••••';
const placeholder = isMasked ? 'Ключ сохранён. Оставьте пустым, чтобы не менять' : '';
// Для password полей показываем звёздочки (8 штук как индикатор сохранённого ключа)
const inputValue = (field.type === 'password' && isMasked) ? '********' : value;
div.innerHTML = `
<label class="form-label" for="field-${field.name}">
${field.label}
${field.required ? '<span class="text-danger">*</span>' : ''}
</label>
<input type="${inputType}" class="form-control" id="field-${field.name}"
name="${field.name}" value="${value !== '........' ? value : ''}"
name="${field.name}" value="${inputValue}"
placeholder="${placeholder}"
${field.required ? 'required' : ''}>
${field.required && !isMasked ? 'required' : ''}>
${field.help_text ? `<div class="form-text">${field.help_text}</div>` : ''}
`;
}
container.appendChild(div);
});
if (field.type !== 'select' || !field.dynamic_choices) {
container.appendChild(div);
}
}
}
// Обработчик клика на интеграцию
@@ -313,9 +370,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Собрать данные формы
for (const [key, value] of formData.entries()) {
// Пропустить пустые password поля (не менять если не введено)
// Пропустить пустые password поля или звёздочки (не менять если не введено новое значение)
const input = document.getElementById(`field-${key}`);
if (input && input.type === 'password' && !value) continue;
if (input && input.type === 'password' && (!value || value === '********')) continue;
data[key] = value;
}