169 lines
8.2 KiB
JavaScript
169 lines
8.2 KiB
JavaScript
// Model Groups Management Page
|
|
|
|
class ModelGroupsPage {
|
|
constructor() {
|
|
this.container = document.getElementById('page-content');
|
|
}
|
|
|
|
async render() {
|
|
this.container.innerHTML = `
|
|
<div class="page-header">
|
|
<h3>Model Groups</h3>
|
|
<p class="text-muted">Define auto-routing groups that pick the best model for each request.</p>
|
|
<button class="btn btn-primary" onclick="modelGroupsPage.showCreateForm()">
|
|
<i class="fas fa-plus"></i> Add Group
|
|
</button>
|
|
</div>
|
|
<div id="model-groups-list" class="table-container"></div>
|
|
<div id="model-group-form" class="form-container" style="display:none;"></div>
|
|
`;
|
|
await this.loadGroups();
|
|
}
|
|
|
|
async loadGroups() {
|
|
try {
|
|
const groups = await api.get('/api/model-groups');
|
|
const list = document.getElementById('model-groups-list');
|
|
if (!groups || groups.length === 0) {
|
|
list.innerHTML = '<div class="empty-state">No model groups defined. Create one to enable auto-routing.</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<table class="data-table"><thead><tr>';
|
|
html += '<th>Group ID</th><th>Strategy</th><th>Targets</th><th>Actions</th>';
|
|
html += '</tr></thead><tbody>';
|
|
|
|
groups.forEach(g => {
|
|
html += '<tr>';
|
|
html += '<td><code>' + this.esc(g.id) + '</code></td>';
|
|
html += '<td><span class="badge">' + this.esc(g.strategy) + '</span></td>';
|
|
html += '<td><code>' + this.esc(g.targets) + '</code></td>';
|
|
html += '<td>';
|
|
html += '<button class="btn btn-sm" onclick="modelGroupsPage.showEditForm(\'' + this.esc(g.id) + '\')">Edit</button> ';
|
|
html += '<button class="btn btn-sm btn-danger" onclick="modelGroupsPage.deleteGroup(\'' + this.esc(g.id) + '\')">Delete</button>';
|
|
html += '</td></tr>';
|
|
});
|
|
|
|
html += '</tbody></table>';
|
|
list.innerHTML = html;
|
|
} catch (err) {
|
|
document.getElementById('model-groups-list').innerHTML =
|
|
'<div class="error-message">Failed to load model groups: ' + this.esc(err.message) + '</div>';
|
|
}
|
|
}
|
|
|
|
showCreateForm() {
|
|
this.renderForm(null);
|
|
}
|
|
|
|
async showEditForm(id) {
|
|
try {
|
|
const groups = await api.get('/api/model-groups');
|
|
const group = groups.find(g => g.id === id);
|
|
if (group) this.renderForm(group);
|
|
} catch (err) {
|
|
alert('Failed to load group: ' + err.message);
|
|
}
|
|
}
|
|
|
|
renderForm(group) {
|
|
const isEdit = !!group;
|
|
const form = document.getElementById('model-group-form');
|
|
form.style.display = 'block';
|
|
form.innerHTML = `
|
|
<h4>${isEdit ? 'Edit' : 'Create'} Model Group</h4>
|
|
<form onsubmit="modelGroupsPage.saveGroup(event, ${isEdit})">
|
|
<div class="form-control">
|
|
<label>Group ID</label>
|
|
<input type="text" id="mg-id" value="${this.esc(group ? group.id : '')}" ${isEdit ? 'readonly' : 'required'}
|
|
placeholder="e.g. deepseek-auto">
|
|
<small>Clients use this as the model name.</small>
|
|
</div>
|
|
<div class="form-control">
|
|
<label>Strategy</label>
|
|
<select id="mg-strategy">
|
|
<option value="heuristic" ${group && group.strategy === 'heuristic' ? 'selected' : ''}>Heuristic (rules-based)</option>
|
|
<option value="classifier" ${group && group.strategy === 'classifier' ? 'selected' : ''}>Classifier (LLM judge)</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-control">
|
|
<label>Targets (JSON array)</label>
|
|
<input type="text" id="mg-targets" value='${this.esc(group ? group.targets : '["cheap-model","smart-model"]')}' required>
|
|
<small>First target = cheapest/fastest. Last target = smartest/most expensive.</small>
|
|
</div>
|
|
<div class="form-control" id="mg-selector-row" style="${group && group.strategy === 'classifier' ? '' : 'display:none'}">
|
|
<label>Selector Model</label>
|
|
<input type="text" id="mg-selector-model" value="${this.esc(group && group.selector_model ? group.selector_model : 'gpt-4o-mini')}"
|
|
placeholder="Model used to judge task complexity">
|
|
</div>
|
|
<div class="form-control" id="mg-threshold-row" style="${group && group.strategy === 'classifier' ? '' : 'display:none'}">
|
|
<label>Complexity Threshold</label>
|
|
<input type="number" id="mg-threshold" value="${group && group.complexity_threshold ? group.complexity_threshold : ''}" min="1"
|
|
placeholder="Tasks rated >= this go to the smart model">
|
|
</div>
|
|
<div class="form-control" id="mg-rules-row" style="${group && group.strategy === 'heuristic' ? '' : 'display:none'}">
|
|
<label>Heuristic Rules (JSON array)</label>
|
|
<textarea id="mg-rules" rows="4" placeholder='[{"pattern":"step by step","target":1}]'>${group && group.heuristic_rules ? group.heuristic_rules : ''}</textarea>
|
|
<small>Pattern to match in user messages. target = index into targets array.</small>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary">Save</button>
|
|
<button type="button" class="btn" onclick="document.getElementById('model-group-form').style.display='none'">Cancel</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
document.getElementById('mg-strategy').onchange = function() {
|
|
var isClassifier = this.value === 'classifier';
|
|
document.getElementById('mg-selector-row').style.display = isClassifier ? '' : 'none';
|
|
document.getElementById('mg-threshold-row').style.display = isClassifier ? '' : 'none';
|
|
document.getElementById('mg-rules-row').style.display = isClassifier ? 'none' : '';
|
|
};
|
|
}
|
|
|
|
async saveGroup(event, isEdit) {
|
|
event.preventDefault();
|
|
var id = document.getElementById('mg-id').value.trim();
|
|
var strategy = document.getElementById('mg-strategy').value;
|
|
var targets = document.getElementById('mg-targets').value;
|
|
var selectorModel = document.getElementById('mg-selector-model').value.trim() || null;
|
|
var thresholdVal = document.getElementById('mg-threshold').value;
|
|
var rules = document.getElementById('mg-rules').value.trim() || null;
|
|
|
|
try { JSON.parse(targets); } catch (e) { alert('Targets must be valid JSON array'); return; }
|
|
if (rules) { try { JSON.parse(rules); } catch (e) { alert('Heuristic rules must be valid JSON'); return; } }
|
|
|
|
var body = { id: id, strategy: strategy, targets: targets, selector_model: selectorModel, heuristic_rules: rules };
|
|
if (thresholdVal) body.complexity_threshold = parseInt(thresholdVal);
|
|
|
|
try {
|
|
if (isEdit) {
|
|
await api.put('/api/model-groups/' + encodeURIComponent(id), body);
|
|
} else {
|
|
await api.post('/api/model-groups', body);
|
|
}
|
|
document.getElementById('model-group-form').style.display = 'none';
|
|
await this.loadGroups();
|
|
} catch (err) {
|
|
alert('Failed to save: ' + err.message);
|
|
}
|
|
}
|
|
|
|
async deleteGroup(id) {
|
|
if (!confirm('Delete model group "' + id + '"? This cannot be undone.')) return;
|
|
try {
|
|
await api.delete('/api/model-groups/' + encodeURIComponent(id));
|
|
await this.loadGroups();
|
|
} catch (err) {
|
|
alert('Failed to delete: ' + err.message);
|
|
}
|
|
}
|
|
|
|
esc(str) {
|
|
if (!str) return '';
|
|
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
}
|
|
|
|
var modelGroupsPage = new ModelGroupsPage();
|