Files
GopherGate/static/js/pages/models.js
hobokenchicken e1bc3b35eb
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled
feat(dashboard): add provider, modality, and capability filters to Model Registry
This commit enhances the Model Registry UI by adding dropdown filters for Provider, Modality (Text/Image/Audio), and Capabilities (Tool Calling/Reasoning) alongside the existing text search. The filtering logic has been refactored to be non-destructive and apply instantly on the client side.
2026-03-07 01:28:39 +00:00

216 lines
9.5 KiB
JavaScript

// Models Page Module
class ModelsPage {
constructor() {
this.models = [];
this.init();
}
async init() {
await this.loadModels();
this.setupEventListeners();
}
async loadModels() {
try {
const data = await window.api.get('/models');
this.models = data;
this.renderModelsTable();
} catch (error) {
console.error('Error loading models:', error);
window.authManager.showToast('Failed to load models', 'error');
}
}
renderModelsTable() {
const tableBody = document.querySelector('#models-table tbody');
if (!tableBody) return;
if (this.models.length === 0) {
tableBody.innerHTML = '<tr><td colspan="7" class="text-center">No models found in registry</td></tr>';
return;
}
const searchInput = document.getElementById('model-search');
const providerFilter = document.getElementById('model-provider-filter');
const modalityFilter = document.getElementById('model-modality-filter');
const capabilityFilter = document.getElementById('model-capability-filter');
const q = searchInput ? searchInput.value.toLowerCase() : '';
const providerVal = providerFilter ? providerFilter.value : '';
const modalityVal = modalityFilter ? modalityFilter.value : '';
const capabilityVal = capabilityFilter ? capabilityFilter.value : '';
// Apply filters non-destructively
let filteredModels = this.models.filter(m => {
// Text search
if (q && !(m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q) || m.provider.toLowerCase().includes(q))) {
return false;
}
// Provider filter
if (providerVal) {
if (providerVal === 'other') {
const known = ['openai', 'anthropic', 'google', 'deepseek', 'xai', 'meta', 'cohere', 'mistral'];
if (known.includes(m.provider.toLowerCase())) return false;
} else if (m.provider.toLowerCase() !== providerVal) {
return false;
}
}
// Modality filter
if (modalityVal) {
const mods = m.modalities && m.modalities.input ? m.modalities.input.map(x => x.toLowerCase()) : [];
if (!mods.includes(modalityVal)) return false;
}
// Capability filter
if (capabilityVal === 'tool_call' && !m.tool_call) return false;
if (capabilityVal === 'reasoning' && !m.reasoning) return false;
return true;
});
if (filteredModels.length === 0) {
tableBody.innerHTML = '<tr><td colspan="7" class="text-center">No models match the selected filters</td></tr>';
return;
}
// Sort by provider then name
filteredModels.sort((a, b) => {
if (a.provider !== b.provider) return a.provider.localeCompare(b.provider);
return a.name.localeCompare(b.name);
});
tableBody.innerHTML = filteredModels.map(model => {
const statusClass = model.enabled ? 'success' : 'secondary';
const statusIcon = model.enabled ? 'check-circle' : 'ban';
return `
<tr>
<td><code class="code-sm">${model.id}</code></td>
<td><strong>${model.name}</strong></td>
<td><span class="badge-client">${model.provider.toUpperCase()}</span></td>
<td>${window.api.formatCurrency(model.prompt_cost)} / ${window.api.formatCurrency(model.completion_cost)}</td>
<td>${model.context_limit ? (model.context_limit / 1000) + 'k' : 'Unknown'}</td>
<td>
<span class="status-badge ${statusClass}">
<i class="fas fa-${statusIcon}"></i>
${model.enabled ? 'Active' : 'Disabled'}
</span>
</td>
<td>
${window._userRole === 'admin' ? `
<div class="action-buttons">
<button class="btn-action" title="Edit Access/Pricing" onclick="window.modelsPage.configureModel('${model.id}')">
<i class="fas fa-cog"></i>
</button>
</div>
` : ''}
</td>
</tr>
`;
}).join('');
}
configureModel(id) {
const model = this.models.find(m => m.id === id);
if (!model) return;
const modal = document.createElement('div');
modal.className = 'modal active';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Manage Model: ${model.name}</h3>
<button class="modal-close" onclick="this.closest('.modal').remove()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<div class="form-control">
<label class="checkbox-label" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
<input type="checkbox" id="model-enabled" ${model.enabled ? 'checked' : ''} style="width: auto;">
<span>Enable this model for proxying</span>
</label>
</div>
<div class="grid-2">
<div class="form-control">
<label for="model-prompt-cost">Input Cost (per 1M tokens)</label>
<input type="number" id="model-prompt-cost" value="${model.prompt_cost}" step="0.01">
</div>
<div class="form-control">
<label for="model-completion-cost">Output Cost (per 1M tokens)</label>
<input type="number" id="model-completion-cost" value="${model.completion_cost}" step="0.01">
</div>
</div>
<div class="form-control">
<label for="model-cache-read-cost">Cache Read Cost (per 1M tokens)</label>
<input type="number" id="model-cache-read-cost" value="${model.cache_read_cost || 0}" step="0.01">
</div>
<div class="form-control">
<label for="model-cache-write-cost">Cache Write Cost (per 1M tokens)</label>
<input type="number" id="model-cache-write-cost" value="${model.cache_write_cost || 0}" step="0.01">
</div>
<div class="form-control">
<label for="model-mapping">Internal Mapping (Optional)</label>
<input type="text" id="model-mapping" value="${model.mapping || ''}" placeholder="e.g. gpt-4o-2024-05-13">
<small>Route this model ID to a different specific provider ID</small>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="this.closest('.modal').remove()">Cancel</button>
<button class="btn btn-primary" id="save-model-config">Save Changes</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.querySelector('#save-model-config').onclick = async () => {
const enabled = modal.querySelector('#model-enabled').checked;
const promptCost = parseFloat(modal.querySelector('#model-prompt-cost').value);
const completionCost = parseFloat(modal.querySelector('#model-completion-cost').value);
const cacheReadCost = parseFloat(modal.querySelector('#model-cache-read-cost').value);
const cacheWriteCost = parseFloat(modal.querySelector('#model-cache-write-cost').value);
const mapping = modal.querySelector('#model-mapping').value;
try {
await window.api.put(`/models/${id}`, {
enabled,
prompt_cost: promptCost,
completion_cost: completionCost,
cache_read_cost: isNaN(cacheReadCost) ? null : cacheReadCost,
cache_write_cost: isNaN(cacheWriteCost) ? null : cacheWriteCost,
mapping: mapping || null
});
window.authManager.showToast(`Model ${model.id} updated`, 'success');
modal.remove();
this.loadModels();
} catch (error) {
window.authManager.showToast(error.message, 'error');
}
};
}
setupEventListeners() {
const attachFilter = (id) => {
const el = document.getElementById(id);
if (el) {
el.addEventListener('input', () => this.renderModelsTable());
el.addEventListener('change', () => this.renderModelsTable());
}
};
attachFilter('model-search');
attachFilter('model-provider-filter');
attachFilter('model-modality-filter');
attachFilter('model-capability-filter');
}
}
window.initModels = async () => {
window.modelsPage = new ModelsPage();
};