From 41e322c53d7b4b796eb377d0df9c29ecd10ba431 Mon Sep 17 00:00:00 2001 From: Chia Date: Thu, 6 Aug 2026 09:29:41 +1200 Subject: feat: complete commercial control plane, billing, auth, and model catalog - add PostgreSQL control-plane persistence with Redis-degraded hot reload - implement prepaid balance, usage ledger, Stripe top-up and reconciliation - add registration, email verification, password reset, invitations and RBAC - support TOTP, Passkey MFA, device sessions, quotas and rate limits - add tenant billing profiles, audit logs and operational readiness checks - build authenticated admin console, Quickstart, Playground and usage analytics - add public model catalog with pricing, filtering and cost estimation - support OpenAI Responses providers and provider health failover - validate real upstream usage reporting and balance settlement --- internal/adminui/assets/models.js | 126 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 internal/adminui/assets/models.js (limited to 'internal/adminui/assets/models.js') 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 => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[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 = '' + owners.map(owner => ``).join(''); + $('#protocol').innerHTML = '' + protocols.map(item => ``).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 `
+
${esc(model.owned_by || 'Independent')}

${esc(model.display_name || model.public_id)}

${esc(health)}
+ ${esc(model.public_id)} +

${esc(model.description || 'No description published.')}

+
${(model.supported_wire_apis || []).map(item => `${esc(protocolName(item))}`).join('')}${(model.input_modalities || []).map(item => `${esc(item)}`).join('')}
+
Input
${price(model.input_price_micros_per_million, model.price_currency)} / 1M
Output
${price(model.output_price_micros_per_million, model.price_currency)} / 1M
Context
${integer(model.context_window)}
+ +
`; + }).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 => `${esc(item)}`).join(''); + const rows = [ + ['Status', `${esc(health)}`], + ['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]) => `
${esc(label)}
${value}
`).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(); -- cgit v1.2.3