File size: 8,975 Bytes
d7eaa23 65eba13 d7eaa23 | 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 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>TokenLab Model Explorer</title>
<style>
:root {
color-scheme: light;
--ink: #172033;
--muted: #667085;
--line: #d9dee9;
--panel: #ffffff;
--wash: #f5f7fb;
--accent: #3155d9;
--accent-wash: #eef2ff;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--wash);
color: var(--ink);
font: 15px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
main { width: min(1180px, calc(100% - 32px)); margin: 0 auto; padding: 34px 0 48px; }
header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 24px; }
h1 { margin: 0; font-size: clamp(26px, 4vw, 42px); line-height: 1.08; letter-spacing: 0; }
.lede { max-width: 680px; margin: 12px 0 0; color: var(--muted); }
.links { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
a { color: var(--accent); }
.links a, button { border: 1px solid var(--line); border-radius: 7px; background: var(--panel); color: var(--ink); padding: 9px 12px; text-decoration: none; font: inherit; cursor: pointer; }
.links a:hover, button:hover { border-color: var(--accent); color: var(--accent); }
.toolbar { display: grid; grid-template-columns: minmax(220px, 1fr) 180px auto; gap: 10px; margin-bottom: 16px; }
input, select { width: 100%; border: 1px solid var(--line); border-radius: 7px; background: var(--panel); color: var(--ink); padding: 10px 12px; font: inherit; }
.status { color: var(--muted); min-height: 24px; margin: 8px 0 14px; }
.table-wrap { overflow: auto; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); }
table { width: 100%; border-collapse: collapse; min-width: 720px; }
th, td { padding: 12px 14px; text-align: left; border-bottom: 1px solid var(--line); vertical-align: top; }
th { color: var(--muted); font-size: 12px; font-weight: 650; text-transform: uppercase; letter-spacing: .04em; background: #fbfcfe; }
tr:last-child td { border-bottom: 0; }
.model { font-weight: 650; overflow-wrap: anywhere; }
.muted { color: var(--muted); }
.price { white-space: nowrap; }
.example { margin-top: 18px; padding: 16px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); }
.example-head { display: flex; justify-content: space-between; gap: 12px; align-items: center; }
h2 { margin: 0; font-size: 17px; }
pre { overflow: auto; margin: 12px 0 0; padding: 14px; border-radius: 6px; background: #111827; color: #e5e7eb; font-size: 12px; }
.note { margin-top: 18px; color: var(--muted); font-size: 13px; }
@media (max-width: 720px) {
main { width: min(100% - 22px, 1180px); padding-top: 24px; }
header { display: block; }
.links { justify-content: flex-start; margin-top: 16px; }
.toolbar { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<main>
<header>
<div>
<h1>TokenLab Model Explorer</h1>
<p class="lede">Compare public model metadata and pricing, then copy an OpenAI-compatible or native endpoint example. This explorer is read-only and never asks for an API key.</p>
</div>
<nav class="links" aria-label="TokenLab links">
<a href="https://tokenlab.sh" target="_blank" rel="noreferrer">Website</a>
<a href="https://docs.tokenlab.sh" target="_blank" rel="noreferrer">Docs</a>
<a href="https://tokenlab-model-explorer.vercel.app/widget" target="_blank" rel="noreferrer">Hosted MCP</a>
</nav>
</header>
<section class="toolbar" aria-label="Model filters">
<input id="query" type="search" placeholder="Search models" aria-label="Search models" />
<select id="category" aria-label="Filter by category"><option value="">All categories</option></select>
<button id="refresh" type="button">Refresh public data</button>
</section>
<div id="status" class="status" role="status">Loading public model data...</div>
<div class="table-wrap">
<table>
<thead><tr><th>Model</th><th>Category</th><th>Provider</th><th>Input / 1M</th><th>Output / 1M</th></tr></thead>
<tbody id="models"><tr><td colspan="5" class="muted">Loading...</td></tr></tbody>
</table>
</div>
<section class="example">
<div class="example-head"><h2>Endpoint example</h2><button id="copy" type="button">Copy cURL</button></div>
<pre id="curl">Select a model to generate an example.</pre>
</section>
<p class="note">Pricing and model availability can change. Verify current details in the <a href="https://docs.tokenlab.sh" target="_blank" rel="noreferrer">TokenLab documentation</a> before production use.</p>
</main>
<script>
const API = "https://api.tokenlab.sh";
const state = { models: [], selected: "" };
const $ = (id) => document.getElementById(id);
function flattenModels(index) {
return Object.entries(index.categories || {}).flatMap(([category, value]) =>
(value.models || []).map((id) => ({ id, category }))
);
}
function endpointExample(model) {
return `curl https://api.tokenlab.sh/v1/chat/completions \\
-H "Authorization: Bearer $TOKENLAB_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{"model":"${model}","messages":[{"role":"user","content":"Hello from TokenLab"}]}'`;
}
function render() {
const query = $("query").value.trim().toLowerCase();
const category = $("category").value;
const visible = state.models.filter((model) =>
(!category || model.category === category) && (!query || model.id.toLowerCase().includes(query))
).slice(0, 200);
$("status").textContent = `${visible.length} of ${state.models.length} public models shown.`;
$("models").innerHTML = visible.length ? visible.map((model) => `
<tr data-model="${encodeURIComponent(model.id)}">
<td><button class="model" type="button">${model.id}</button></td>
<td>${model.category}</td><td class="muted">${model.provider || "Not listed"}</td>
<td class="price">${model.input_per_1m == null ? "Not listed" : "$" + model.input_per_1m}</td>
<td class="price">${model.output_per_1m == null ? "Not listed" : "$" + model.output_per_1m}</td>
</tr>`).join("") : '<tr><td colspan="5" class="muted">No matching public models.</td></tr>';
document.querySelectorAll("[data-model]").forEach((row) => row.addEventListener("click", () => {
state.selected = decodeURIComponent(row.dataset.model);
$("curl").textContent = endpointExample(state.selected);
}));
}
async function load() {
$("status").textContent = "Loading public model data...";
try {
const [modelsResponse, pricingResponse] = await Promise.all([
fetch(`${API}/models.json`), fetch(`${API}/pricing.json`)
]);
if (!modelsResponse.ok || !pricingResponse.ok) throw new Error("Public discovery endpoint unavailable");
const [modelsIndex, pricingIndex] = await Promise.all([modelsResponse.json(), pricingResponse.json()]);
const prices = new Map((pricingIndex.data || []).map((entry) => [entry.model, entry]));
state.models = flattenModels(modelsIndex).map((model) => {
const price = prices.get(model.id) || {};
return { ...model, provider: price.provider, input_per_1m: price.platform?.input_per_1m, output_per_1m: price.platform?.output_per_1m };
});
const categories = [...new Set(state.models.map((model) => model.category))].sort();
$("category").insertAdjacentHTML("beforeend", categories.map((item) => `<option value="${item}">${item}</option>`).join(""));
render();
} catch (error) {
$("status").textContent = `Could not load public data: ${error.message}`;
$("models").innerHTML = '<tr><td colspan="5" class="muted">Try refreshing, or use the TokenLab documentation link below.</td></tr>';
}
}
$("query").addEventListener("input", render);
$("category").addEventListener("change", render);
$("refresh").addEventListener("click", load);
$("copy").addEventListener("click", async () => {
if (!state.selected) return;
await navigator.clipboard.writeText($("curl").textContent);
$("copy").textContent = "Copied";
setTimeout(() => { $("copy").textContent = "Copy cURL"; }, 1200);
});
load();
</script>
</body>
</html>
|