summaryrefslogtreecommitdiff
path: root/internal/adminui/assets/models.js
diff options
context:
space:
mode:
Diffstat (limited to 'internal/adminui/assets/models.js')
-rw-r--r--internal/adminui/assets/models.js126
1 files changed, 126 insertions, 0 deletions
diff --git a/internal/adminui/assets/models.js b/internal/adminui/assets/models.js
new file mode 100644
index 0000000..4057ac5
--- /dev/null
+++ b/internal/adminui/assets/models.js
@@ -0,0 +1,126 @@
+'use strict';
+
+const state = { models: [], selected: null, registrationEnabled: false };
+const $ = selector => document.querySelector(selector);
+const esc = value => String(value ?? '').replace(/[&<>'"]/g, char => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[char]));
+const integer = value => new Intl.NumberFormat().format(Number(value || 0));
+const protocolName = value => ({chat_completions:'Chat Completions',responses:'Responses',messages:'Anthropic Messages'}[value] || value);
+const price = (micros, currency='usd') => new Intl.NumberFormat(undefined, {style:'currency', currency:String(currency).toUpperCase(), minimumFractionDigits:2, maximumFractionDigits:6}).format(Number(micros || 0) / 1_000_000);
+const date = value => value ? new Intl.DateTimeFormat(undefined, {year:'numeric',month:'short',day:'numeric'}).format(new Date(value)) : 'Not published';
+
+function healthLabel(model) {
+ if (model.health_status === 'unavailable') return ['Unavailable', 'unavailable'];
+ if (model.health_status === 'degraded') return [`${model.available_provider_count}/${model.provider_count} routes`, 'degraded'];
+ return ['Available', 'available'];
+}
+
+function renderStats(models) {
+ const protocols = new Set(models.flatMap(model => model.supported_wire_apis || []));
+ $('#model-count').textContent = integer(models.length);
+ $('#provider-count').textContent = integer(models.reduce((sum, model) => sum + Number(model.provider_count || 0), 0));
+ $('#protocol-count').textContent = integer(protocols.size);
+}
+
+function renderFilters() {
+ const owners = [...new Set(state.models.map(model => model.owned_by).filter(Boolean))].sort();
+ const protocols = [...new Set(state.models.flatMap(model => model.supported_wire_apis || []))].sort();
+ $('#owner').innerHTML = '<option value="">All developers</option>' + owners.map(owner => `<option value="${esc(owner)}">${esc(owner)}</option>`).join('');
+ $('#protocol').innerHTML = '<option value="">All protocols</option>' + protocols.map(item => `<option value="${esc(item)}">${esc(protocolName(item))}</option>`).join('');
+}
+
+function filteredModels() {
+ const query = $('#search').value.trim().toLowerCase();
+ const protocol = $('#protocol').value;
+ const input = $('#input').value;
+ const owner = $('#owner').value;
+ const result = state.models.filter(model => {
+ const haystack = [model.public_id, model.display_name, model.description, model.owned_by, ...(model.capabilities || []), ...(model.aliases || [])].join(' ').toLowerCase();
+ return (!query || haystack.includes(query)) && (!protocol || (model.supported_wire_apis || []).includes(protocol)) && (!input || (model.input_modalities || []).includes(input)) && (!owner || model.owned_by === owner);
+ });
+ const sort = $('#sort').value;
+ result.sort((a,b) => {
+ if (sort === 'name') return String(a.display_name || a.public_id).localeCompare(String(b.display_name || b.public_id));
+ if (sort === 'input_price') return Number(a.input_price_micros_per_million) - Number(b.input_price_micros_per_million);
+ if (sort === 'output_price') return Number(a.output_price_micros_per_million) - Number(b.output_price_micros_per_million);
+ if (sort === 'context') return Number(b.context_window) - Number(a.context_window);
+ return new Date(b.released_at || 0) - new Date(a.released_at || 0) || String(a.public_id).localeCompare(String(b.public_id));
+ });
+ return result;
+}
+
+function renderCatalog() {
+ const models = filteredModels();
+ $('#catalog-empty').classList.toggle('hidden', models.length !== 0);
+ $('#catalog-grid').innerHTML = models.map(model => {
+ const [health, healthClass] = healthLabel(model);
+ return `<article class="model-card">
+ <header><div><small>${esc(model.owned_by || 'Independent')}</small><h2>${esc(model.display_name || model.public_id)}</h2></div><span class="health ${healthClass}">${esc(health)}</span></header>
+ <code>${esc(model.public_id)}</code>
+ <p>${esc(model.description || 'No description published.')}</p>
+ <div class="model-tags">${(model.supported_wire_apis || []).map(item => `<span>${esc(protocolName(item))}</span>`).join('')}${(model.input_modalities || []).map(item => `<span>${esc(item)}</span>`).join('')}</div>
+ <dl><div><dt>Input</dt><dd>${price(model.input_price_micros_per_million, model.price_currency)} / 1M</dd></div><div><dt>Output</dt><dd>${price(model.output_price_micros_per_million, model.price_currency)} / 1M</dd></div><div><dt>Context</dt><dd>${integer(model.context_window)}</dd></div></dl>
+ <button class="button secondary" type="button" data-model="${esc(model.public_id)}">View model</button>
+ </article>`;
+ }).join('');
+}
+
+function renderEstimate() {
+ if (!state.selected) return;
+ const input = Math.max(0, Number($('#estimate-input').value || 0));
+ const output = Math.max(0, Number($('#estimate-output').value || 0));
+ const totalMicros = (Number(state.selected.input_price_micros_per_million || 0) * input + Number(state.selected.output_price_micros_per_million || 0) * output) / 1_000_000;
+ $('#estimate-total').textContent = `${price(totalMicros, state.selected.price_currency)} estimated`;
+}
+
+function openModel(publicID) {
+ const model = state.models.find(item => item.public_id === publicID);
+ if (!model) return;
+ state.selected = model;
+ const [health, healthClass] = healthLabel(model);
+ $('#detail-owner').textContent = model.owned_by || 'Independent';
+ $('#detail-name').textContent = model.display_name || model.public_id;
+ $('#detail-id').textContent = model.public_id;
+ $('#detail-description').textContent = model.description || 'No description published.';
+ $('#detail-tags').innerHTML = [...(model.supported_wire_apis || []).map(protocolName), ...(model.capabilities || []), ...(model.input_modalities || []).map(item => `${item} input`), ...(model.output_modalities || []).map(item => `${item} output`)].map(item => `<span>${esc(item)}</span>`).join('');
+ const rows = [
+ ['Status', `<span class="health ${healthClass}">${esc(health)}</span>`],
+ ['Input price', `${price(model.input_price_micros_per_million, model.price_currency)} / 1M tokens`],
+ ['Output price', `${price(model.output_price_micros_per_million, model.price_currency)} / 1M tokens`],
+ ['Cached input', `${price(model.cache_read_price_micros_per_million, model.price_currency)} / 1M tokens`],
+ ['Context window', `${integer(model.context_window)} tokens`],
+ ['Max output', `${integer(model.max_output_tokens)} tokens`],
+ ['Regions', esc((model.regions || []).join(', ') || 'Global')],
+ ['Released', esc(date(model.released_at))]
+ ];
+ $('#detail-grid').innerHTML = rows.map(([label,value]) => `<div><dt>${esc(label)}</dt><dd>${value}</dd></div>`).join('');
+ const register = $('#detail-register');
+ register.classList.toggle('hidden', !state.registrationEnabled);
+ register.href = `./?auth=register&model=${encodeURIComponent(model.public_id)}`;
+ renderEstimate();
+ $('#model-dialog').showModal();
+}
+
+async function start() {
+ try {
+ const response = await fetch('./api/public/models', {headers:{Accept:'application/json'}});
+ if (!response.ok) throw new Error(`Catalog request failed (${response.status})`);
+ const payload = await response.json();
+ state.models = Array.isArray(payload.data) ? payload.data : [];
+ state.registrationEnabled = Boolean(payload.registration_enabled);
+ $('#create-account').classList.toggle('hidden', !state.registrationEnabled);
+ renderStats(state.models);
+ renderFilters();
+ renderCatalog();
+ } catch (error) {
+ $('#catalog-error').textContent = error.message || 'The model catalog is temporarily unavailable.';
+ $('#catalog-error').classList.remove('hidden');
+ }
+}
+
+$('#catalog-filters').addEventListener('input', renderCatalog);
+$('#catalog-grid').addEventListener('click', event => { const button = event.target.closest('[data-model]'); if (button) openModel(button.dataset.model); });
+$('#close-dialog').addEventListener('click', () => $('#model-dialog').close());
+$('#model-dialog').addEventListener('click', event => { if (event.target === $('#model-dialog')) $('#model-dialog').close(); });
+$('#estimate-input').addEventListener('input', renderEstimate);
+$('#estimate-output').addEventListener('input', renderEstimate);
+start();