feat: implement web UI for provider and model configuration
- Added 'provider_configs' and 'model_configs' tables to database. - Refactored ProviderManager to support thread-safe dynamic updates and database overrides. - Implemented 'Models' tab in dashboard to manage model visibility, mapping, and pricing. - Added provider configuration modal to 'Providers' tab. - Integrated database overrides into chat completion logic (enabled state, mapping, and cost).
This commit is contained in:
165
static/js/pages/models.js
Normal file
165
static/js/pages/models.js
Normal file
@@ -0,0 +1,165 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Sort by provider then name
|
||||
this.models.sort((a, b) => {
|
||||
if (a.provider !== b.provider) return a.provider.localeCompare(b.provider);
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
tableBody.innerHTML = this.models.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>
|
||||
<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-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 mapping = modal.querySelector('#model-mapping').value;
|
||||
|
||||
try {
|
||||
await window.api.put(`/models/${id}`, {
|
||||
enabled,
|
||||
prompt_cost: promptCost,
|
||||
completion_cost: completionCost,
|
||||
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 searchInput = document.getElementById('model-search');
|
||||
if (searchInput) {
|
||||
searchInput.oninput = (e) => this.filterModels(e.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
filterModels(query) {
|
||||
if (!query) {
|
||||
this.renderModelsTable();
|
||||
return;
|
||||
}
|
||||
|
||||
const q = query.toLowerCase();
|
||||
const originalModels = this.models;
|
||||
this.models = this.models.filter(m =>
|
||||
m.id.toLowerCase().includes(q) ||
|
||||
m.name.toLowerCase().includes(q) ||
|
||||
m.provider.toLowerCase().includes(q)
|
||||
);
|
||||
this.renderModelsTable();
|
||||
this.models = originalModels;
|
||||
}
|
||||
}
|
||||
|
||||
window.initModels = async () => {
|
||||
window.modelsPage = new ModelsPage();
|
||||
};
|
||||
Reference in New Issue
Block a user