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.
216 lines
9.5 KiB
JavaScript
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();
|
|
};
|