| <!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> |
|
|