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>