summaryrefslogtreecommitdiff
path: root/internal/adminui/assets/models.js
blob: 4057ac520cf078c633c3f1475ae5c2300349cff0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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();