pool-anything 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
1
  import { LOGO_V, DOCS_URL, ICONS } from "./branding.js";
2
+ import { HOME_HEAD_EXTRA, SEO_TITLE, subPageHead } from "./seo.js";
3
+ import { ADAPTERS } from "../playground/adapters.js";
2
4
  import { shellCss, shellNav, shellJs, sharedHeader, sharedLayoutCss } from "./shell.js";
3
5
  import { tokensCss } from "./tokens.js";
4
6
  import { docsComponentsCss, ddSelect } from "./components.js";
@@ -124,7 +126,8 @@ export const page = `<!doctype html>
124
126
  <head>
125
127
  <meta charset="utf-8" />
126
128
  <meta name="viewport" content="width=device-width, initial-scale=1" />
127
- <title>pool-anything</title>
129
+ <title>${SEO_TITLE}</title>
130
+ ${HOME_HEAD_EXTRA}
128
131
  <link rel="icon" type="image/png" href="/logo.png" />
129
132
  <style>
130
133
  ${tokensCss}
@@ -134,9 +137,13 @@ export const page = `<!doctype html>
134
137
  * { box-sizing:border-box; }
135
138
  body { margin:0; font-family:var(--font-sans); background:var(--bg); color:var(--ink); }
136
139
  header { height:56px; background:var(--card); border-bottom:1px solid var(--line); display:flex; align-items:center; padding:0 16px; gap:8px; position:sticky; top:0; }
137
- header .spacer { margin-left:auto; display:flex; gap:4px; }
140
+ header .spacer { margin-left:auto; display:flex; gap:4px; align-items:center; }
138
141
  header button, header a { font-size:14px; padding:6px 12px; border-radius:8px; border:0; background:inherit; color:var(--ink); text-decoration:none; cursor:pointer; }
139
142
  header button:hover, header a:hover { background:var(--paper-2); }
143
+ .ver-pill { font-size:12px; font-weight:600; color:var(--ink-soft); background:var(--paper-2); border:1px solid var(--line); border-radius:999px; padding:4px 10px; white-space:nowrap; }
144
+ .ver-update { font-size:12px !important; font-weight:600; border:1px solid var(--ink) !important; background:var(--ink) !important; color:var(--card) !important; border-radius:999px !important; padding:4px 12px !important; cursor:pointer; white-space:nowrap; }
145
+ .ver-update[hidden] { display:none; }
146
+ .ver-update:disabled { opacity:.6; cursor:default; }
140
147
  .shell { display:grid; grid-template-columns:var(--sbw,260px) 1fr; min-height:100vh; transition:grid-template-columns 250ms cubic-bezier(0.77,0,0.175,1); }
141
148
  .shell.collapsed { --sbw:57px; }
142
149
  .sidebar { background:var(--card); border-right:1px solid var(--line); display:flex; flex-direction:column; min-height:100vh; height:100vh; position:sticky; top:0; overflow:hidden; white-space:nowrap; }
@@ -230,6 +237,12 @@ export const page = `<!doctype html>
230
237
  .hero-row, .search-card { max-width:400px; }
231
238
  .hero-row h1, .hero-row { gap:2px; }
232
239
  h1 { font-family:var(--font-serif); font-size:clamp(20px, 4vw, 26px); font-weight:500; }
240
+ .hero-sub { width:100%; max-width:640px; margin:8px auto 0; text-align:center; font-size:14px; line-height:1.55; color:var(--ink-soft); }
241
+ .seo-foot { width:100%; max-width:640px; margin:28px auto 0; background:var(--card); border:1px solid var(--line); border-radius:12px; padding:18px 20px; font-size:13.5px; line-height:1.6; color:var(--body-copy); }
242
+ .seo-foot h2 { font-size:14px; margin:14px 0 6px; color:var(--ink); }
243
+ .seo-foot h2:first-child { margin-top:0; }
244
+ .seo-foot ul { margin:6px 0; padding-left:18px; display:flex; flex-direction:column; gap:4px; }
245
+ .seo-foot a { color:var(--ink); }
233
246
  .sec-label { font-size:11px; font-weight:600; letter-spacing:.08em; text-transform:uppercase; color:var(--ink-soft); margin:0 0 8px; }
234
247
  .conn-grid { width:100%; display:flex; flex-direction:column; gap:8px; }
235
248
  .prov-lg { display:flex; align-items:center; gap:10px; width:100%; background:var(--card); border:1px solid var(--line); border-radius:12px; padding:10px 12px; cursor:pointer; font-size:13px; font-weight:600; text-align:left; animation:fadeSlide .28s cubic-bezier(.2,.7,.3,1) both; }
@@ -349,6 +362,7 @@ ${docsComponentsCss}
349
362
  <a class="mi" href="/keys"><span class="ic">${ICONS.keys}</span><span class="lbl">API key manager</span></a>
350
363
  <a class="mi" href="/playground"><span class="ic">${ICONS.playground}</span><span class="lbl">Playground</span></a>
351
364
  <a class="mi" href="/analytics"><span class="ic">${ICONS.analytics}</span><span class="lbl">Analytics</span></a>
365
+ <a class="mi" href="/history"><span class="ic">${ICONS.history}</span><span class="lbl">History</span></a>
352
366
  <a class="mi" href="${DOCS_URL}" target="_blank" rel="noopener" title="Opens external docs in a new tab"><span class="ic">${ICONS.docs}</span><span class="lbl">Docs</span><span class="ext">${ICONS.external}</span></a>
353
367
  </nav>
354
368
  <div class="sb-footer"><button class="collapse-btn" id="collapseBtn" type="button" data-sidebar="trigger" aria-expanded="true" aria-label="Collapse sidebar"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M21.25 6.72v10.56a2.97 2.97 0 0 1-2.97 2.97H5.72a2.97 2.97 0 0 1-2.97-2.97V6.72a2.97 2.97 0 0 1 2.97-2.97h12.56a2.97 2.97 0 0 1 2.97 2.97"></path><path d="M6.25 7.25v9.5"></path></svg></button></div>
@@ -371,12 +385,15 @@ ${docsComponentsCss}
371
385
  <header>
372
386
  <div></div>
373
387
  <div class="spacer">
388
+ <span class="ver-pill" id="verPill" title="pool-anything version">v…</span>
389
+ <button class="ver-update" id="verUpdate" type="button" hidden>Update</button>
374
390
  <a href="${DOCS_URL}" target="_blank" rel="noopener">Docs</a>
375
391
  </div>
376
392
  </header>
377
393
  <main>
378
394
  <div class="wrap">
379
395
  <div class="hero-row"><img class="hero-logo" src="/logo.png" alt="pool-anything logo" width="40" height="40" /><h1>What do you want to pool</h1></div>
396
+ <p class="hero-sub">Pool free-tier API keys and rotate through them — one endpoint, round-robin key rotation, usage &amp; quota tracking for Groq, OpenRouter, Gemini and 30+ providers.</p>
380
397
  <div class="search-card">
381
398
  <div class="search-box">
382
399
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"></path></svg>
@@ -425,6 +442,18 @@ ${docsComponentsCss}
425
442
  </div>
426
443
  </div>
427
444
  </div>
445
+ <section class="seo-foot" aria-label="About pool-anything">
446
+ <h2>Pool free-tier API keys into one endpoint</h2>
447
+ <p>Most AI and API providers hand you a small free tier — a rate limit, a daily quota, a handful of trial tokens. <strong>pool-anything</strong> gathers N keys for the same provider into a pool, then serves them through a single drop-in HTTP proxy that round-robins across keys and tracks usage per key in SQLite.</p>
448
+ <h2>Why pool keys?</h2>
449
+ <ul>
450
+ <li>Round-robin rotation evens out rate limits across all keys.</li>
451
+ <li>Per-key usage, quota windows, cooldown and failover keep pools healthy.</li>
452
+ <li>36 preconfigured providers: Groq, OpenRouter, Gemini, OpenAI, Anthropic, ElevenLabs, Tavily, Resend and more — plus any custom provider.</li>
453
+ <li>Self-hosted TypeScript + SQLite core with zero-dependency server, web UI, analytics dashboard and playground.</li>
454
+ </ul>
455
+ <p>Start in <a href="/pools">Pools</a>, gather keys in the <a href="/keys">API key manager</a>, try a pooled key in the <a href="/playground">Playground</a>, or read the <a href="https://pool-anything.pages.dev/docs">docs</a>.</p>
456
+ </section>
428
457
  </div>
429
458
  </main>
430
459
  </div>
@@ -698,6 +727,40 @@ ${docsComponentsCss}
698
727
  setTimeout(() => document.getElementById('browseAll').scrollIntoView({ block: 'start' }), 100);
699
728
  }
700
729
  });
730
+ (function () {
731
+ var pill = document.getElementById('verPill');
732
+ var btn = document.getElementById('verUpdate');
733
+ if (!pill || !btn) return;
734
+ function txt(el, s) { if (el) el.textContent = s; }
735
+ fetch('/api/version').then(function (r) { return r.json(); }).then(function (v) {
736
+ if (!v || !v.current) return;
737
+ txt(pill, 'v' + v.current);
738
+ pill.title = v.latest ? ('current v' + v.current + ', latest v' + v.latest) : ('pool-anything v' + v.current);
739
+ if (v.updateAvailable) {
740
+ btn.hidden = false;
741
+ txt(btn, 'Update to v' + v.latest);
742
+ }
743
+ }).catch(function () {});
744
+ btn.addEventListener('click', function () {
745
+ if (btn.disabled) return;
746
+ btn.disabled = true;
747
+ txt(btn, 'Updating…');
748
+ fetch('/api/update', { method: 'POST' }).then(function (r) { return r.json(); }).then(function (j) {
749
+ if (j && j.ok) {
750
+ txt(pill, 'v' + (j.current || j.latest || ''));
751
+ txt(btn, 'Updated — restart server');
752
+ pill.title = (j.text || 'updated');
753
+ } else {
754
+ txt(btn, 'Update failed — retry');
755
+ btn.disabled = false;
756
+ pill.title = ((j && (j.text || j.error)) || 'update failed');
757
+ }
758
+ }).catch(function () {
759
+ txt(btn, 'Update failed — retry');
760
+ btn.disabled = false;
761
+ });
762
+ });
763
+ })();
701
764
  loadProviders();
702
765
  loadAnalytics();
703
766
  </script>
@@ -708,6 +771,7 @@ export function keysPage() {
708
771
  <html lang="en"><head><meta charset="utf-8"/>
709
772
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
710
773
  <title>API key manager — pool-anything</title>
774
+ ${subPageHead("/keys/", "Add, view, edit and rotate pooled API keys per provider with masked storage and per-key usage.")}
711
775
  <link rel="icon" type="image/png" href="/logo.png" />
712
776
  <style>
713
777
  *{box-sizing:border-box}body{margin:0;font-family:var(--font-sans);background:var(--paper-2);color:var(--ink)}
@@ -769,7 +833,7 @@ async function refresh(){
769
833
  const li=document.createElement('li');
770
834
  li.innerHTML='<span></span><small></small>';
771
835
  li.querySelector('span').textContent=k.label+' · '+k.masked;
772
- li.querySelector('small').textContent='used '+((us.perKey||[]).find(x=>x.id===k.id)?.used||0);
836
+ li.querySelector('small').textContent='used '+((us.perKey||[]).find(x=>x.id===k.id)?.used||0)+(k.cooling?' · cooling':'');
773
837
  const v=document.createElement('button');v.textContent='View';v.className='ghost sm';
774
838
  v.onclick=async()=>{
775
839
  if(v.dataset.open){
@@ -843,6 +907,7 @@ export function analyticsPage() {
843
907
  <html lang="en"><head><meta charset="utf-8"/>
844
908
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
845
909
  <title>Analytics — pool-anything</title>
910
+ ${subPageHead("/analytics/", "Analytics dashboard: total requests, tokens tracked, pools, keys pooled and 14-day usage charts.")}
846
911
  <link rel="icon" type="image/png" href="/logo.png" />
847
912
  <style>
848
913
  *{box-sizing:border-box}body{margin:0;font-family:var(--font-sans);background:var(--paper-2);color:var(--ink)}
@@ -990,6 +1055,7 @@ export function poolsPage() {
990
1055
  <html lang="en"><head><meta charset="utf-8"/>
991
1056
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
992
1057
  <title>Pools — pool-anything</title>
1058
+ ${subPageHead("/pools/", "Pools holding API keys with per-pool usage, key counts and quota status.")}
993
1059
  <link rel="icon" type="image/png" href="/logo.png" />
994
1060
  <style>
995
1061
  *{box-sizing:border-box}body{margin:0;font-family:var(--font-sans);background:var(--paper-2);color:var(--ink)}
@@ -1029,14 +1095,14 @@ async function init(){
1029
1095
  card.style.animationDelay=Math.min(i*40,320)+'ms';
1030
1096
  card.innerHTML='<div class="chead"><img alt=""/><b></b><span class="pill"></span></div><ul></ul><div class="row" style="margin-top:10px"><a class="manage" href="/keys">Manage keys →</a></div>';
1031
1097
  logoSrc(card.querySelector('img'),prov.logoFile);
1032
- card.querySelector('b').textContent=prov.name+' · '+p.name;
1098
+ card.querySelector('b').textContent=prov.name+' · '+p.name+' · #'+p.id;
1033
1099
  card.querySelector('.pill').textContent=ks.length+' key(s) · used '+(us.used||0);
1034
1100
  const ul=card.querySelector('ul');
1035
1101
  ks.forEach(k=>{
1036
1102
  const li=document.createElement('li');
1037
1103
  li.innerHTML='<span></span><small></small>';
1038
1104
  li.querySelector('span').textContent=k.label+' · '+k.masked;
1039
- li.querySelector('small').textContent='used '+((us.perKey||[]).find(x=>x.id===k.id)?.used||0);
1105
+ li.querySelector('small').textContent='used '+((us.perKey||[]).find(x=>x.id===k.id)?.used||0)+(k.cooling?' · cooling':'');
1040
1106
  ul.appendChild(li);
1041
1107
  });
1042
1108
  box.appendChild(card);
@@ -1046,11 +1112,145 @@ async function init(){
1046
1112
  init();
1047
1113
  </script></div></div>${shellJs}</body></html>`;
1048
1114
  }
1115
+ export function historyPage() {
1116
+ return `<!doctype html>
1117
+ <html lang="en"><head><meta charset="utf-8"/>
1118
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
1119
+ <title>History — pool-anything</title>
1120
+ ${subPageHead("/history/", "History of proxied API calls per pool and key: status, tokens, errors and retained responses.")}
1121
+ <link rel="icon" type="image/png" href="/logo.png" />
1122
+ <style>
1123
+ *{box-sizing:border-box}body{margin:0;font-family:var(--font-sans);background:var(--paper-2);color:var(--ink)}
1124
+ ${shellCss}
1125
+ ${sharedLayoutCss}
1126
+ .card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px}
1127
+ .row{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;align-items:center}
1128
+ select{border:1px solid var(--line);border-radius:8px;min-height:34px;padding:0 10px;font-size:14px}
1129
+ button{border:1px solid var(--ink);background:var(--ink);color:var(--card);border-radius:8px;min-height:34px;padding:0 12px;font-size:14px;cursor:pointer}
1130
+ label.auto{display:flex;gap:6px;align-items:center;font-size:13px;color:var(--ink-soft)}
1131
+ table.hist{width:100%;border-collapse:collapse;margin-top:12px;font-size:13px}
1132
+ table.hist th{text-align:left;font-size:12px;color:var(--ink-soft);font-weight:600;padding:8px 10px;border-bottom:1px solid var(--line);white-space:nowrap}
1133
+ table.hist td{padding:8px 10px;border-bottom:1px solid var(--line-soft);vertical-align:top}
1134
+ table.hist td.mono{font-family:var(--font-mono);font-size:12px}
1135
+ .st{display:inline-block;min-width:44px;text-align:center;font-family:var(--font-mono);font-size:12px;font-weight:600;border-radius:6px;padding:2px 8px}
1136
+ .st2{background:#e6f4ea;color:#1F7A4D}.st4{background:#fef3e2;color:#B26A00}.st5{background:#fdecea;color:#B3261E}.st0{background:var(--paper-2);color:var(--ink-soft)}
1137
+ details.body{max-width:420px}details.body summary{cursor:pointer;color:var(--ink-soft);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:420px}
1138
+ details.body pre{white-space:pre-wrap;word-break:break-word;font-size:12px;background:var(--paper-2);border-radius:8px;padding:8px;max-height:240px;overflow:auto}
1139
+ .empty{color:var(--ink-soft);font-size:14px;text-align:center;padding:24px 0}
1140
+ .err{color:var(--danger-ink);font-size:13px;min-height:18px}
1141
+ ${docsComponentsCss}
1142
+ </style></head><body><div class="shell" id="shell">${shellNav("/history")}<div class="content">${sharedHeader}<main>
1143
+ <div class="wrap">
1144
+ <div class="page-head"><a href="/"><img src="/logo.png" alt="pool-anything" width="28" height="28"/></a><h1>History</h1></div>
1145
+ <a class="back" href="/">← pool search</a>
1146
+ <div class="err" id="err"></div>
1147
+ <div class="card"><div class="row"><select id="hpool" aria-label="Pool filter"></select><select id="hlimit" aria-label="Row limit"><option value="25">25 rows</option><option value="50" selected>50 rows</option><option value="100">100 rows</option><option value="200">200 rows</option></select><button id="hrefresh">Refresh</button><label class="auto"><input type="checkbox" id="hauto"/> auto-refresh 5s</label></div>
1148
+ <table class="hist"><thead><tr><th>Time (UTC)</th><th>Pool</th><th>Key</th><th>Request</th><th>Status</th><th>Tokens</th><th>Error</th><th>Response</th></tr></thead><tbody id="hrows"></tbody></table>
1149
+ <div class="empty" id="hempty" hidden>No proxy calls recorded yet — send traffic through <span style="font-family:var(--font-mono)">/api/pools/:id/proxy</span>.</div>
1150
+ </div>
1151
+ </div>
1152
+ </main><script>
1153
+ ${clientCoreJs}
1154
+ var poolsById={};
1155
+ function stClass(s){if(s===null||s===undefined)return 'st0';if(s>=200&&s<300)return 'st2';if(s>=400&&s<500)return 'st4';if(s>=500)return 'st5';return 'st0';}
1156
+ function stLabel(s){return (s===null||s===undefined)?'—':String(s);}
1157
+ function mediaFor(c){
1158
+ if(c.encoding==='base64'&&c.body&&!c.truncated){
1159
+ var ct=String(c.content_type||'');
1160
+ var src='data:'+(ct||'application/octet-stream')+';base64,'+c.body;
1161
+ if(/audio/i.test(ct))return {kind:'audio',src:src};
1162
+ if(/video/i.test(ct))return {kind:'video',src:src};
1163
+ if(/image/i.test(ct))return {kind:'image',src:src};
1164
+ return null;
1165
+ }
1166
+ if(c.encoding!=='base64'&&c.body&&!c.truncated){
1167
+ try{
1168
+ var d=JSON.parse(c.body);
1169
+ var first=d&&d.data&&d.data[0];
1170
+ var u=(first&&first.url)||d.url||'';
1171
+ if(typeof u==='string'&&/^https?:\\/\\//.test(u))return {kind:'url',src:u};
1172
+ }catch(e){}
1173
+ }
1174
+ return null;
1175
+ }
1176
+ async function loadPools(){
1177
+ const pools=await j(await fetch('/api/pools'));
1178
+ poolsById={};
1179
+ const sel=document.getElementById('hpool');sel.innerHTML='';
1180
+ const all=document.createElement('option');all.value='';all.textContent='All pools';sel.appendChild(all);
1181
+ pools.forEach(function(p){poolsById[p.id]=p;var o=document.createElement('option');o.value=String(p.id);o.textContent='#'+p.id+' '+p.name+' ('+p.provider+')';sel.appendChild(o);});
1182
+ }
1183
+ async function refresh(){
1184
+ err('');
1185
+ const pid=document.getElementById('hpool').value;
1186
+ const limit=document.getElementById('hlimit').value;
1187
+ var rows=[];
1188
+ if(pid){
1189
+ rows=await j(await fetch('/api/pools/'+pid+'/calls?limit='+encodeURIComponent(limit)));
1190
+ if(rows&&rows.error){err(rows.error);return;}
1191
+ }else{
1192
+ const pools=await j(await fetch('/api/pools'));
1193
+ const lists=await Promise.all(pools.map(async function(p){return j(await fetch('/api/pools/'+p.id+'/calls?limit='+encodeURIComponent(limit)));}));
1194
+ lists.forEach(function(l){if(Array.isArray(l))rows=rows.concat(l);});
1195
+ rows.sort(function(a,b){return b.id-a.id;});
1196
+ rows=rows.slice(0,Number(limit));
1197
+ }
1198
+ const tb=document.getElementById('hrows');tb.innerHTML='';
1199
+ document.getElementById('hempty').hidden=rows.length>0;
1200
+ rows.forEach(function(c){
1201
+ var p=poolsById[c.pool_id]||{name:'#'+c.pool_id,provider:''};
1202
+ var tr=document.createElement('tr');
1203
+ var preview=c.body?(c.body.length>90?c.body.slice(0,90)+'…':c.body):'—';
1204
+ tr.innerHTML='<td class="mono"></td><td></td><td class="mono"></td><td class="mono"></td><td><span></span></td><td class="mono"></td><td class="mono"></td><td></td>';
1205
+ var tds=tr.querySelectorAll('td');
1206
+ tds[0].textContent=c.created_at||'';
1207
+ tds[1].textContent=p.name+(p.provider?' ('+p.provider+')':'');
1208
+ tds[2].textContent='#'+c.key_id;
1209
+ tds[3].textContent=c.method+' '+c.path;
1210
+ var badge=tds[4].querySelector('span');badge.className='st '+stClass(c.status);badge.textContent=stLabel(c.status);
1211
+ tds[5].textContent=c.tokens||'';
1212
+ tds[6].textContent=c.error||'';
1213
+ var cell=tds[7];
1214
+ var media=mediaFor(c);
1215
+ if(media&&(media.kind==='audio'||media.kind==='video'||media.kind==='image')){
1216
+ var mel;
1217
+ if(media.kind==='audio'){mel=document.createElement('audio');mel.controls=true;}
1218
+ else if(media.kind==='video'){mel=document.createElement('video');mel.controls=true;mel.style.maxWidth='320px';}
1219
+ else{mel=document.createElement('img');mel.alt='generated image';mel.style.maxWidth='240px';mel.style.borderRadius='8px';}
1220
+ mel.src=media.src;mel.preload='metadata';cell.appendChild(mel);
1221
+ }else if(media&&media.kind==='url'){
1222
+ var link=document.createElement('a');link.href=media.src;link.target='_blank';link.rel='noopener';
1223
+ if(/\.(png|jpe?g|webp|gif)(\\?|#|$)/i.test(media.src)){var th=document.createElement('img');th.src=media.src;th.alt='generated image';th.style.maxWidth='240px';th.style.borderRadius='8px';link.appendChild(th);}
1224
+ else link.textContent=media.src.slice(0,90);
1225
+ cell.appendChild(link);
1226
+ }else if(c.body){
1227
+ var d=document.createElement('details');d.className='body';
1228
+ var s=document.createElement('summary');s.textContent=preview;d.appendChild(s);
1229
+ var pre=document.createElement('pre');var cap=c.encoding==='base64'?'~2MB media cap':'4000 chars';pre.textContent=c.body+((c.truncated)?'\\n…(truncated at '+cap+', tail not retained)':'');d.appendChild(pre);
1230
+ cell.appendChild(d);
1231
+ }else cell.textContent='—';
1232
+ tb.appendChild(tr);
1233
+ });
1234
+ }
1235
+ var autoTimer=null;
1236
+ function setAuto(on){
1237
+ if(autoTimer){clearInterval(autoTimer);autoTimer=null;}
1238
+ if(on)autoTimer=setInterval(refresh,5000);
1239
+ }
1240
+ document.getElementById('hrefresh').onclick=refresh;
1241
+ document.getElementById('hpool').onchange=refresh;
1242
+ document.getElementById('hlimit').onchange=refresh;
1243
+ document.getElementById('hauto').onchange=function(e){setAuto(e.target.checked);};
1244
+ async function init(){await loadPools();refresh();}
1245
+ init();
1246
+ </script></div></div>${shellJs}</body></html>`;
1247
+ }
1049
1248
  export function playgroundPage() {
1050
1249
  return `<!doctype html>
1051
1250
  <html lang="en"><head><meta charset="utf-8"/>
1052
1251
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
1053
1252
  <title>Playground — pool-anything</title>
1253
+ ${subPageHead("/playground/", "Chat, image, audio and video playground to try a pooled API key across providers.")}
1054
1254
  <link rel="icon" type="image/png" href="/logo.png" />
1055
1255
  <style>
1056
1256
  *{box-sizing:border-box}body{margin:0;font-family:var(--font-sans);background:var(--paper-2);color:var(--ink)}
@@ -1063,6 +1263,11 @@ ${sharedLayoutCss}
1063
1263
  .seg button.on{background:var(--card);box-shadow:var(--shadow-sm);color:var(--ink);font-weight:600}
1064
1264
  .ctl{border:1px solid var(--line);border-radius:8px;min-height:34px;padding:0 10px;font-size:13px;background:var(--card);color:var(--ink);font-family:inherit}
1065
1265
  select.ctl{max-width:220px}input.ctl.model{min-width:220px}
1266
+ .dd-combo{position:relative;display:inline-flex;align-items:stretch;min-width:0}
1267
+ .dd-combo input.ctl.model{border-radius:8px 0 0 8px;min-width:0}
1268
+ .model-chev{border:1px solid var(--line);border-left:0;background:var(--card);color:var(--ink-faint);border-radius:0 8px 8px 0;min-width:34px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-family:inherit}
1269
+ .model-chev:hover{background:var(--paper-2);color:var(--ink)}
1270
+ .dd-combo .dd-menu{left:auto;right:0;min-width:220px;max-width:340px}
1066
1271
  .btn{border:1px solid var(--line);background:var(--card);border-radius:8px;min-height:34px;padding:0 12px;font-size:13px;cursor:pointer;color:var(--ink);font-family:inherit;display:inline-flex;align-items:center;gap:6px}
1067
1272
  .btn:hover{background:var(--paper-2)}
1068
1273
  .pg-grid{display:grid;grid-template-columns:minmax(0,1fr) 400px;gap:16px;align-items:start;min-height:560px}
@@ -1120,6 +1325,20 @@ details.params summary{cursor:pointer;color:var(--ink-soft);font-size:12px}
1120
1325
  .modes button.off{opacity:.4;cursor:not-allowed}
1121
1326
  .media-out{margin-top:12px;display:flex;flex-direction:column;gap:10px}
1122
1327
  .media-out audio,.media-out video{width:100%;border-radius:10px;background:var(--code-dark)}
1328
+ .pa{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:10px 12px;box-shadow:var(--shadow-sm)}
1329
+ .pa-row{display:flex;align-items:center;gap:10px}
1330
+ .pa-play{width:40px;height:40px;flex-shrink:0;border:0;border-radius:50%;background:var(--green-bg);color:var(--green-ink);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-family:inherit}
1331
+ .pa-play:hover{filter:brightness(.96)}
1332
+ .pa-play svg{width:16px;height:16px;display:block}
1333
+ .pa-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13.5px;font-weight:600;color:var(--ink)}
1334
+ .pa-x{flex-shrink:0;border:0;background:none;color:var(--ink-faint);cursor:pointer;font-size:18px;line-height:1;border-radius:6px;padding:2px 6px;font-family:inherit}
1335
+ .pa-x:hover{color:var(--ink);background:var(--paper-2)}
1336
+ .pa-wave{display:flex;align-items:flex-end;gap:1px;height:56px;margin-top:10px;cursor:pointer;border-radius:6px}
1337
+ .pa-wave:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
1338
+ .pa-bar{flex:1;min-width:1px;border-radius:2px;background:linear-gradient(to top,var(--line),var(--paper-2));min-height:4px;opacity:.85;transition:height .25s ease,background .2s ease,opacity .2s ease}
1339
+ .pa-bar.on{background:linear-gradient(to top,var(--green-ink),var(--green-chip));opacity:1}
1340
+ .pa-times{display:flex;justify-content:space-between;margin-top:6px;font-size:11px;color:var(--ink-soft);font-variant-numeric:tabular-nums}
1341
+ .pa audio{display:none}
1123
1342
  .media-out .dl{font-size:13px;color:var(--ink)}
1124
1343
  ${docsComponentsCss}
1125
1344
  </style></head><body><div class="shell" id="shell">${shellNav("/playground")}<div class="content">${sharedHeader}<main>
@@ -1130,7 +1349,7 @@ ${docsComponentsCss}
1130
1349
  <div class="seg"><button id="tabChat" class="on" type="button">Chat</button><button id="tabStudio" type="button">Studio</button></div>
1131
1350
  <div class="spacer"></div>
1132
1351
  ${ddSelect("pool", "Pool")}
1133
- <input class="ctl model" id="model" value="" placeholder="model id" aria-label="Model" spellcheck="false" autocomplete="off"/>
1352
+ <span class="dd-combo" id="dd-model"><input class="ctl model" id="model" value="" placeholder="model id" aria-label="Model" spellcheck="false" autocomplete="off"/><button type="button" class="model-chev" id="modelChev" aria-haspopup="listbox" aria-expanded="false" aria-label="Choose model"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button><div class="dd-menu" id="modelMenu" role="listbox" hidden></div></span>
1134
1353
  <button class="btn" id="copyTop" type="button" title="Copy code">⧉</button>
1135
1354
  <button class="btn" id="hideCode" type="button">&lt;/&gt; Hide code</button>
1136
1355
  </div>
@@ -1144,6 +1363,20 @@ ${ddSelect("pool", "Pool")}
1144
1363
  <label>Temperature<input class="ctl" id="temp" value="1" inputmode="decimal"/></label>
1145
1364
  <label>Max tokens<input class="ctl" id="maxtokens" value="2048" inputmode="numeric"/></label>
1146
1365
  <label>Top P<input class="ctl" id="topp" value="1" inputmode="decimal"/></label>
1366
+ </div>
1367
+ <div class="params-grid" id="probeVars" hidden style="margin-top:8px">
1368
+ <label id="probeOrgWrap">Org<input class="ctl" id="probeOrg" value="" placeholder="turso org slug" spellcheck="false" autocomplete="off"/></label>
1369
+ <label id="probeSidWrap">Account SID<input class="ctl" id="probeSid" value="" placeholder="twilio AC… SID" spellcheck="false" autocomplete="off"/></label>
1370
+ <label id="probeDomainWrap">Domain<input class="ctl" id="probeDomain" value="" placeholder="mailgun domain" spellcheck="false" autocomplete="off"/></label>
1371
+ <label id="probeDepthWrap">Depth<select class="ctl" id="probeDepth"><option value="basic">basic (1 credit)</option><option value="advanced">advanced (2 credits)</option><option value="fast">fast (1 credit)</option><option value="ultra-fast">ultra-fast (1 credit)</option></select></label>
1372
+ <label id="probeTopicWrap">Topic<select class="ctl" id="probeTopic"><option value="general">general</option><option value="news">news</option><option value="finance">finance</option></select></label>
1373
+ <label id="probeTimeWrap">Time range<select class="ctl" id="probeTime"><option value="">any time</option><option value="day">day</option><option value="week">week</option><option value="month">month</option><option value="year">year</option></select></label>
1374
+ <label id="probeMaxWrap">Max results<input class="ctl" id="probeMax" value="5" inputmode="numeric"/></label>
1375
+ <label id="probeAnswerWrap">Answer<select class="ctl" id="probeAnswer"><option value="basic">answer: basic</option><option value="advanced">answer: advanced</option><option value="false">answer: off</option></select></label>
1376
+ <label id="probeChunksWrap">Chunks/source<select class="ctl" id="probeChunks"><option value="3">3</option><option value="1">1</option><option value="2">2</option></select></label>
1377
+ <label id="probeAutoWrap">Auto params<select class="ctl" id="probeAuto"><option value="false">off</option><option value="true">on (2 credits)</option></select></label>
1378
+ <label id="probeInclWrap">Include domains<input class="ctl" id="probeIncl" value="" placeholder="comma-separated" spellcheck="false" autocomplete="off"/></label>
1379
+ <label id="probeExclWrap">Exclude domains<input class="ctl" id="probeExcl" value="" placeholder="comma-separated" spellcheck="false" autocomplete="off"/></label>
1147
1380
  </div></details>
1148
1381
  <div class="chat-foot">
1149
1382
  <button class="iconbtn" id="addMsg" type="button" title="Add message">+</button>
@@ -1179,7 +1412,8 @@ ${ddSelect("codeLang", "Code language", '<option value="python">Python</option><
1179
1412
  <div id="studioImage">
1180
1413
  <div class="fld" style="margin-top:12px"><label>PROMPT</label><textarea class="ghost" id="imgPrompt" rows="2" placeholder="Describe the image you want to generate..."></textarea></div>
1181
1414
  <div class="params-grid" style="margin-top:12px">
1182
- <label>Model<input class="ctl" id="imgModel" value="" placeholder="image model id" spellcheck="false" autocomplete="off"/></label>
1415
+ <label>Model<input class="ctl" id="imgModel" value="" placeholder="image model id" spellcheck="false" autocomplete="off" list="imgModelList"/></label>
1416
+ <datalist id="imgModelList"></datalist>
1183
1417
  <label>Size${ddSelect("imgSize", "Size", "<option>1024x1024</option><option>1792x1024</option><option>1024x1792</option><option>512x512</option>", "dd-block")}</label>
1184
1418
  </div>
1185
1419
  <div class="chat-foot" style="justify-content:center">
@@ -1190,8 +1424,10 @@ ${ddSelect("codeLang", "Code language", '<option value="python">Python</option><
1190
1424
  <div id="studioAudio" hidden>
1191
1425
  <div class="fld" style="margin-top:12px"><label>TEXT TO SPEAK</label><textarea class="ghost" id="audText" rows="2" placeholder="Text for TTS / audio generation..."></textarea></div>
1192
1426
  <div class="params-grid" style="margin-top:12px">
1193
- <label>Model<input class="ctl" id="audModel" value="" placeholder="audio model id" spellcheck="false" autocomplete="off"/></label>
1194
- <label>Voice<input class="ctl" id="audVoice" value="alloy" spellcheck="false" autocomplete="off"/></label>
1427
+ <label>Model<input class="ctl" id="audModel" value="" placeholder="audio model id" spellcheck="false" autocomplete="off" list="audModelList"/></label>
1428
+ <datalist id="audModelList"></datalist>
1429
+ <label>Voice<input class="ctl" id="audVoice" value="alloy" spellcheck="false" autocomplete="off" list="audVoiceList"/></label>
1430
+ <datalist id="audVoiceList"></datalist>
1195
1431
  <label>Format${ddSelect("audFormat", "Format", "<option>mp3</option><option>wav</option><option>opus</option>", "dd-block")}</label>
1196
1432
  </div>
1197
1433
  <div class="chat-foot" style="justify-content:center">
@@ -1202,7 +1438,8 @@ ${ddSelect("codeLang", "Code language", '<option value="python">Python</option><
1202
1438
  <div id="studioVideo" hidden>
1203
1439
  <div class="fld" style="margin-top:12px"><label>PROMPT</label><textarea class="ghost" id="vidPrompt" rows="2" placeholder="Describe the video you want to generate..."></textarea></div>
1204
1440
  <div class="params-grid" style="margin-top:12px">
1205
- <label>Model<input class="ctl" id="vidModel" value="" placeholder="video model id" spellcheck="false" autocomplete="off"/></label>
1441
+ <label>Model<input class="ctl" id="vidModel" value="" placeholder="video model id" spellcheck="false" autocomplete="off" list="vidModelList"/></label>
1442
+ <datalist id="vidModelList"></datalist>
1206
1443
  <label>Duration${ddSelect("vidDur", "Duration", "<option>4s</option><option>8s</option><option>12s</option>", "dd-block")}</label>
1207
1444
  <label>Resolution${ddSelect("vidRes", "Resolution", "<option>720p</option><option>1080p</option><option>480p</option>", "dd-block")}</label>
1208
1445
  </div>
@@ -1212,15 +1449,35 @@ ${ddSelect("codeLang", "Code language", '<option value="python">Python</option><
1212
1449
  <div class="media-out" id="videoOut"></div>
1213
1450
  </div>
1214
1451
  </div>
1452
+ <details class="params" style="margin-top:12px"><summary>Debug log</summary><div class="chat-foot" style="margin-top:8px">
1453
+ <button class="btn" id="copyDebug" type="button">⧉ Copy</button>
1454
+ <button class="btn" id="clearDebug" type="button">Clear</button>
1455
+ </div><pre id="debugLog" style="max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:12px">No requests yet.</pre></details>
1215
1456
  </div>
1216
1457
  </main><script>
1217
1458
  ${clientCoreJs}
1218
1459
  ${clientDropdownJs}
1219
- const MODELS={groq:'openai/gpt-oss-120b',openai:'gpt-4o-mini',openrouter:'meta-llama/llama-3.3-70b-instruct:free',mistral:'mistral-small-latest',fireworks:'accounts/fireworks/models/llama-v3p3-70b-instruct',together:'meta-llama/Llama-3.3-70B-Instruct-Turbo',gemini:'gemini-2.0-flash',anthropic:'claude-sonnet-4-20250514',perplexity:'sonar',cohere:'command-r',replicate:'ibm-granite/granite-3.3-8b-instruct',huggingface:'meta-llama/Llama-3.3-70B-Instruct',deepgram:'nova-3',elevenlabs:'eleven_multilingual_v2',cartesia:'sonic-2',tavily:'tavily-search',anyscale:'meta-llama/Llama-3.3-70B-Instruct'};
1220
- const IMG_MODELS={openai:'gpt-image-1',together:'FLUX.1-schnell',fireworks:'accounts/fireworks/models/flux-1-schnell',replicate:'black-forest-labs/flux-schnell',huggingface:'black-forest-labs/FLUX.1-schnell',openrouter:'black-forest-labs/flux-1-schnell:free',gemini:'imagen-3.0-generate-002',mistral:'mistral-medium'};
1221
- const AUD_MODELS={openai:'tts-1',elevenlabs:'eleven_multilingual_v2',cartesia:'sonic-2',deepgram:'aura-2-thalia-en',gemini:'gemini-2.5-flash-preview-tts'};
1222
- const VID_MODELS={gemini:'veo-3.0-generate-001',replicate:'stability-ai/stable-video-diffusion'};
1223
- const CAPS={openai:{vision:1,audioIn:1,videoIn:0,imageOut:1,audioOut:1,videoOut:0},anthropic:{vision:1,audioIn:0,videoIn:0,imageOut:0,audioOut:0,videoOut:0},gemini:{vision:1,audioIn:1,videoIn:1,imageOut:1,audioOut:1,videoOut:1},groq:{vision:1,audioIn:1,videoIn:0,imageOut:0,audioOut:0,videoOut:0},openrouter:{vision:1,audioIn:0,videoIn:0,imageOut:1,audioOut:0,videoOut:0},mistral:{vision:1,audioIn:0,videoIn:0,imageOut:0,audioOut:0,videoOut:0},fireworks:{vision:1,audioIn:0,videoIn:0,imageOut:1,audioOut:0,videoOut:0},together:{vision:1,audioIn:0,videoIn:0,imageOut:1,audioOut:0,videoOut:0},huggingface:{vision:1,audioIn:0,videoIn:0,imageOut:1,audioOut:0,videoOut:0},replicate:{vision:1,audioIn:0,videoIn:0,imageOut:1,audioOut:0,videoOut:1},perplexity:{vision:1,audioIn:0,videoIn:0,imageOut:0,audioOut:0,videoOut:0},cohere:{vision:1,audioIn:0,videoIn:0,imageOut:0,audioOut:0,videoOut:0},elevenlabs:{vision:0,audioIn:0,videoIn:0,imageOut:0,audioOut:1,videoOut:0},deepgram:{vision:0,audioIn:1,videoIn:0,imageOut:0,audioOut:1,videoOut:0},cartesia:{vision:0,audioIn:0,videoIn:0,imageOut:0,audioOut:1,videoOut:0}};
1460
+ const ADAPTERS=${JSON.stringify(ADAPTERS)};
1461
+ // Derived defaults/caps from the adapter table (single source of truth —
1462
+ // edit src/playground/adapters.ts, not these maps).
1463
+ const MODELS={};const IMG_MODELS={};const AUD_MODELS={};const VID_MODELS={};const VOICE_DEFAULTS={};const CAPS={};
1464
+ for(var __pid in ADAPTERS){var __a=ADAPTERS[__pid];
1465
+ if(__a.defaults.chat)MODELS[__pid]=__a.defaults.chat;
1466
+ if(__a.defaults.image)IMG_MODELS[__pid]=__a.defaults.image;
1467
+ if(__a.defaults.audio)AUD_MODELS[__pid]=__a.defaults.audio;
1468
+ VOICE_DEFAULTS[__pid]=__a.voiceDefault||'alloy';
1469
+ CAPS[__pid]={vision:__a.vision?1:0,audioIn:__a.audioIn?1:0,videoIn:__a.videoIn?1:0,imageOut:__a.image?1:0,audioOut:__a.audio?1:0,videoOut:__a.video?1:0};
1470
+ }
1471
+ function curAdapter(){var p=curProv();return (p&&ADAPTERS[p])||null;}
1472
+ function getPath(o,path){try{var parts=String(path).split('.');var cur=o;for(var i=0;i<parts.length;i++){if(cur==null)return undefined;var k=parts[i];if(k==='$')continue;cur=/^[0-9]+$/.test(k)?cur[Number(k)]:cur[k];}return cur;}catch(e){return undefined;}}
1473
+ function fillTpl(t,vars){if(typeof t==='string'){if(Object.prototype.hasOwnProperty.call(vars,t))return vars[t];var out=t;for(var k in vars){var v=vars[k];if(v===null||v===undefined||typeof v==='object')continue;out=out.split(k).join(v);}return out;}if(Array.isArray(t))return t.map(function(x){return fillTpl(x,vars);});if(t&&typeof t==='object'){var o={};for(var k in t){var fv=fillTpl(t[k],vars);if(fv!==null&&fv!==undefined)o[k]=fv;}return o;}return t;}
1474
+ function csvList(s){return String(s||'').split(',').map(function(x){return x.trim();}).filter(Boolean);}
1475
+ function probeVars(text){return {'<<QUERY>>':text,'<<TEXT>>':text,'<<MODEL>>':document.getElementById('model').value.trim(),'<<ORG>>':document.getElementById('probeOrg').value.trim(),'<<SID>>':document.getElementById('probeSid').value.trim(),'<<DOMAIN>>':document.getElementById('probeDomain').value.trim(),'<<DEPTH>>':document.getElementById('probeDepth').value,'<<TOPIC>>':document.getElementById('probeTopic').value,'<<TIME_RANGE>>':document.getElementById('probeTime').value||null,'<<MAX_RESULTS>>':Math.max(1,Math.min(20,parseInt(document.getElementById('probeMax').value,10)||5)),'<<ANSWER>>':document.getElementById('probeAnswer').value==='false'?false:document.getElementById('probeAnswer').value,'<<CHUNKS>>':Math.max(1,Math.min(3,parseInt(document.getElementById('probeChunks').value,10)||3)),'<<AUTO>>':document.getElementById('probeAuto').value==='true','<<INCL_DOMAINS>>':(function(){var l=csvList(document.getElementById('probeIncl').value);return l.length?l:null;})(),'<<EXCL_DOMAINS>>':(function(){var l=csvList(document.getElementById('probeExcl').value);return l.length?l:null;})(),'<<VOICE>>':document.getElementById('audVoice').value.trim(),'<<SIZE>>':(document.getElementById('imgSize')||{value:''}).value};}
1476
+ function extractText(d,paths){var list=(paths||[]).concat(['choices.0.message.content','content.0.text','candidates.0.content.parts.0.text']);for(var i=0;i<list.length;i++){if(list[i]==='$')continue;var v=getPath(d,list[i]);if(typeof v==='string'&&v)return v;if(typeof v==='number')return String(v);if(v&&typeof v==='object'){var s=JSON.stringify(v);if(s&&s!=='{}'&&s!=='[]')return s;}}return null;}
1477
+ function anthropicBody(p,msgs){var sys=[];var rest=[];msgs.forEach(function(m){if(m.role==='system'&&typeof m.content==='string')sys.push(m.content);else rest.push(m);});var b={model:p.model,max_tokens:p.maxtokens,messages:rest.length?rest:[{role:'user',content:'hi'}]};if(sys.length)b.system=sys.join('\\n\\n');return b;}
1478
+ function geminiBody(p,msgs){var sys=[];var contents=[];msgs.forEach(function(m){var parts=Array.isArray(m.content)?m.content:[{text:typeof m.content==='string'?m.content:JSON.stringify(m.content)}];if(m.role==='system')parts.forEach(function(x){if(x.text)sys.push(x.text);});else if(m.role==='assistant')contents.push({role:'model',parts:parts});else contents.push({role:'user',parts:parts});});if(!contents.length)contents.push({role:'user',parts:[{text:'hi'}]});var b={contents:contents};if(sys.length)b.system_instruction={parts:[{text:sys.join('\\n\\n')}]};return b;}
1479
+ function chatBodyFor(prov,p,msgs){var a=curAdapter();if(a&&a.chatStyle==='anthropic')return anthropicBody(p,msgs);if(a&&a.chatStyle==='gemini')return geminiBody(p,msgs);return {model:p.model,messages:msgs,temperature:p.temp,max_tokens:p.maxtokens,top_p:p.topp};}
1480
+ function chatPathFor(prov,model,fallback){var a=curAdapter();var t=(a&&a.chatPath)||fallback||'/chat/completions';return t.split('<<MODEL>>').join(model||'');}
1224
1481
  const NO_VISION_RE=/tts|whisper|transcri|embed|image|flux|diffusion|imagen|dall|sonic|nova|stt|audit/i;
1225
1482
  function capsFor(prov,model){
1226
1483
  const b=CAPS[prov]||{vision:0,audioIn:0,videoIn:0,imageOut:0,audioOut:0,videoOut:0};
@@ -1244,6 +1501,17 @@ const IMG_MIMES=['image/png','image/jpeg','image/webp'];
1244
1501
  const AUD_MIMES=['audio/mpeg','audio/wav','audio/webm','audio/ogg','audio/mp4'];
1245
1502
  const VID_MIMES=['video/mp4','video/webm'];
1246
1503
  const MAX_MEDIA_BYTES=8000000;
1504
+ // Debug log: last 20 playground proxy calls (request + upstream response).
1505
+ // Rendered into #debugLog; never includes API keys (auth is server-side).
1506
+ let pgLog=[];
1507
+ function pgLogAdd(e){
1508
+ e.t=new Date().toISOString().slice(11,19);
1509
+ pgLog.unshift(e);if(pgLog.length>20)pgLog.length=20;
1510
+ var el=document.getElementById('debugLog');
1511
+ if(el)el.textContent=pgLog.map(function(x){
1512
+ return '['+x.t+'] '+x.tab+' '+x.method+' '+x.path+' prov='+x.prov+' model='+(x.model||'-')+' → up='+x.upstream+' key='+(x.key||'-')+' '+x.ms+'ms'+(x.err?' ERR='+x.err:'')+'\\n req: '+x.req+'\\n res: '+x.res;
1513
+ }).join('\\n')||'No requests yet.';
1514
+ }
1247
1515
  let pendingMedia=[];
1248
1516
  function fmtKB(n){return Math.round(n/1024)+'KB';}
1249
1517
  function renderMediaPrev(){
@@ -1316,6 +1584,12 @@ function chatContentFor(prov,text,media){
1316
1584
  if(nonImg.length)return null;
1317
1585
  return anthropicBlocks(text,media);
1318
1586
  }
1587
+ var a=curAdapter();
1588
+ if(a&&a.chatStyle==='gemini'){
1589
+ const parts=[{text:text}];
1590
+ media.forEach(function(x){if(x.kind==='image')parts.push({inline_data:{mime_type:x.mime,data:x.b64}});});
1591
+ return parts;
1592
+ }
1319
1593
  return openAiParts(text,media);
1320
1594
  }
1321
1595
  let lastDefault='';
@@ -1418,6 +1692,9 @@ function updateCode(){
1418
1692
  const url=location.origin+'/api/pools/'+pid+'/proxy';
1419
1693
  const lang=document.getElementById('codeLang').value;
1420
1694
  const prov=curProv(),model=p.model;
1695
+ const pa=curAdapter();
1696
+ if(pa&&!pa.chatPath&&pa.probe){updateProbeCode();return;}
1697
+ const effPath=p.path||chatPathFor(prov,p.model,'/chat/completions');
1421
1698
  const c=capsFor(prov,model);
1422
1699
  const bad=pendingMedia.filter(function(a){return (a.kind==='image'&&!c.vision)||(a.kind==='audio'&&!c.audioIn)||(a.kind==='video'&&!c.videoIn);});
1423
1700
  if(pendingMedia.length&&bad.length){
@@ -1426,10 +1703,11 @@ function updateCode(){
1426
1703
  document.getElementById('code').innerHTML='<span class="c">'+esc(rawSnippet)+'</span>';
1427
1704
  return;
1428
1705
  }
1429
- const body={model:p.model,messages:msgs,temperature:p.temp,max_tokens:p.maxtokens,top_p:p.topp};
1706
+ const body=chatBodyFor(prov,p,msgs);
1707
+ const style=(pa&&pa.chatStyle)||'openai';
1430
1708
  const shortMsgs=JSON.stringify(msgs,function(k,v){return k==='data'&&typeof v==='string'&&v.length>64?v.slice(0,64)+'…<base64 '+v.length+' chars>':k==='url'&&typeof v==='string'&&v.indexOf('base64,')>=0?v.slice(0,80)+'…<base64>':v;});
1431
1709
  if(lang==='curl'){
1432
- rawSnippet='curl -X POST '+url+' \\\\\\n -H "Content-Type: application/json" \\\\\\n -d '+pyStr(JSON.stringify({path:p.path,method:'POST',body}))+';\\n';
1710
+ rawSnippet='curl -X POST '+url+' \\\\\\n -H "Content-Type: application/json" \\\\\\n -d '+pyStr(JSON.stringify({path:effPath,method:'POST',body}))+';\\n';
1433
1711
  document.getElementById('code').innerHTML='<span class="c"># runs through your pooled keys — auth injected server-side</span>\\n'+esc(rawSnippet);
1434
1712
  return;
1435
1713
  }
@@ -1438,8 +1716,9 @@ function updateCode(){
1438
1716
  lines.push('resp = requests.<span class="fn">post</span>(');
1439
1717
  lines.push(' '+pyStr(url)+',');
1440
1718
  lines.push(' json={');
1441
- lines.push(' '+pyStr('path')+': '+pyStr(p.path)+',');
1719
+ lines.push(' '+pyStr('path')+': '+pyStr(effPath)+',');
1442
1720
  lines.push(' '+pyStr('method')+': '+pyStr('POST')+',');
1721
+ if(style==='openai'){
1443
1722
  lines.push(' '+pyStr('body')+': {');
1444
1723
  lines.push(' '+pyStr('model')+': '+pyStr(p.model)+',');
1445
1724
  lines.push(' '+pyStr('messages')+': '+esc(shortMsgs)+',');
@@ -1447,6 +1726,9 @@ function updateCode(){
1447
1726
  lines.push(' '+pyStr('max_tokens')+': <span class="n">'+p.maxtokens+'</span>,');
1448
1727
  lines.push(' '+pyStr('top_p')+': <span class="n">'+p.topp+'</span>,');
1449
1728
  lines.push(' },');
1729
+ }else{
1730
+ lines.push(' '+pyStr('body')+': '+esc(JSON.stringify(body))+',');
1731
+ }
1450
1732
  lines.push(' },');
1451
1733
  lines.push(' timeout=<span class="n">120</span>,');
1452
1734
  lines.push(')');
@@ -1454,7 +1736,7 @@ function updateCode(){
1454
1736
  lines.push('<span class="fn">print</span>(data[<span class="s">'+pyStr('status')+'</span>])');
1455
1737
  lines.push('<span class="fn">print</span>(data[<span class="s">'+pyStr('body')+'</span>])');
1456
1738
  document.getElementById('code').innerHTML=lines.join('\\n');
1457
- rawSnippet='import requests\\n\\nresp = requests.post(\\n '+JSON.stringify(url)+',\\n json='+JSON.stringify({path:p.path,method:'POST',body},null,4)+',\\n timeout=120,\\n)\\ndata = resp.json()\\nprint(data["status"])\\nprint(data["body"])\\n';
1739
+ rawSnippet='import requests\\n\\nresp = requests.post(\\n '+JSON.stringify(url)+',\\n json='+JSON.stringify({path:effPath,method:'POST',body},null,4)+',\\n timeout=120,\\n)\\ndata = resp.json()\\nprint(data["status"])\\nprint(data["body"])\\n';
1458
1740
  }
1459
1741
  function studioMode(){if(document.getElementById('modeAudio').classList.contains('on'))return 'audio';if(document.getElementById('modeVideo').classList.contains('on'))return 'video';return 'image';}
1460
1742
  function setStudioMode(m){
@@ -1465,10 +1747,11 @@ function setStudioMode(m){
1465
1747
  document.getElementById('studioAudio').hidden=m!=='audio';
1466
1748
  document.getElementById('studioVideo').hidden=m!=='video';
1467
1749
  updateStudioModes();updateCode();
1750
+ pgSyncHash();
1468
1751
  }
1469
1752
  function updateStudioModes(){
1470
1753
  const prov=curProv();
1471
- const im=document.getElementById('imgModel').value.trim()||AUD_MODELS[prov]||'';
1754
+ const im=document.getElementById('imgModel').value.trim()||IMG_MODELS[prov]||'';
1472
1755
  const c=capsFor(prov,im);
1473
1756
  const mi=document.getElementById('modeImage'),ma=document.getElementById('modeAudio'),mv=document.getElementById('modeVideo');
1474
1757
  const note=document.getElementById('studioNote');
@@ -1494,9 +1777,22 @@ function studioParams(){
1494
1777
  };
1495
1778
  }
1496
1779
  function studioPayload(s){
1497
- if(s.mode==='audio')return {path:'/audio/speech',body:{model:s.model,input:s.prompt,voice:s.voice,response_format:s.format}};
1498
- if(s.mode==='video')return {path:'/videos/generations',body:{model:s.model,prompt:s.prompt,duration:s.duration,resolution:s.resolution,n:1}};
1499
- return {path:'/images/generations',body:{model:s.model,prompt:s.prompt,size:s.size,n:1}};
1780
+ var a=curAdapter();
1781
+ var spec=s.mode==='audio'?(a&&a.audio):s.mode==='video'?(a&&a.video):(a&&a.image);
1782
+ if(!spec){
1783
+ if(s.mode==='audio')return {path:'/audio/speech',body:{model:s.model,input:s.prompt,voice:s.voice,response_format:s.format}};
1784
+ if(s.mode==='video')return {path:'/videos/generations',body:{model:s.model,prompt:s.prompt,duration:s.duration,resolution:s.resolution,n:1}};
1785
+ return {path:'/images/generations',body:{model:s.model,prompt:s.prompt,size:s.size,n:1}};
1786
+ }
1787
+ var vars={'<<MODEL>>':s.model,'<<TEXT>>':s.prompt,'<<VOICE>>':s.voice||VOICE_DEFAULTS[curProv()]||'alloy','<<SIZE>>':s.size||'1024x1024'};
1788
+ var usePath=spec.path;
1789
+ if(spec.altPath&&spec.altMatch){try{if(new RegExp(spec.altMatch,'i').test(s.model))usePath=spec.altPath;}catch(e){}}
1790
+ var out={path:fillTpl(usePath,vars),method:'POST'};
1791
+ if(spec.query)out.query=fillTpl(spec.query,vars);
1792
+ var b=fillTpl(spec.body,vars);
1793
+ if(s.mode==='audio'&&s.format&&b&&typeof b==='object'&&curProv()==='openai')b.response_format=s.format;
1794
+ if(a&&a.formOnly)out.form=b;else out.body=b;
1795
+ return out;
1500
1796
  }
1501
1797
  function updateStudioCode(){
1502
1798
  const s=studioParams();
@@ -1504,9 +1800,13 @@ function updateStudioCode(){
1504
1800
  const url=location.origin+'/api/pools/'+pid+'/proxy';
1505
1801
  const lang=document.getElementById('codeLang').value;
1506
1802
  const pl=studioPayload(s);
1507
- const body=pl.body;
1803
+ const shown={path:pl.path,method:'POST'};
1804
+ if(pl.query)shown.query=pl.query;
1805
+ if(pl.form)shown.form=pl.form;else shown.body=pl.body;
1806
+ const bodyKey=pl.form?'form':'body';
1807
+ const body=pl.form||pl.body;
1508
1808
  if(lang==='curl'){
1509
- rawSnippet='curl -X POST '+url+' \\\\\\n -H "Content-Type: application/json" \\\\\\n -d '+pyStr(JSON.stringify({path:pl.path,method:'POST',body}))+';\\n';
1809
+ rawSnippet='curl -X POST '+url+' \\\\\\n -H "Content-Type: application/json" \\\\\\n -d '+pyStr(JSON.stringify(shown))+';\\n';
1510
1810
  document.getElementById('code').innerHTML='<span class="c"># runs through your pooled keys — auth injected server-side</span>\\n'+esc(rawSnippet);
1511
1811
  return;
1512
1812
  }
@@ -1515,9 +1815,9 @@ function updateStudioCode(){
1515
1815
  lines.push('resp = requests.<span class="fn">post</span>(');
1516
1816
  lines.push(' '+pyStr(url)+',');
1517
1817
  lines.push(' json={');
1518
- lines.push(' '+pyStr('path')+': '+pyStr(pl.path)+',');
1818
+ lines.push(' '+pyStr('path')+': '+pyStr(shown.path)+',');
1519
1819
  lines.push(' '+pyStr('method')+': '+pyStr('POST')+',');
1520
- lines.push(' '+pyStr('body')+': '+esc(JSON.stringify(body,null,4))+',');
1820
+ lines.push(' '+pyStr(bodyKey)+': '+esc(JSON.stringify(body,null,4))+',');
1521
1821
  lines.push(' },');
1522
1822
  lines.push(' timeout=<span class="n">120</span>,');
1523
1823
  lines.push(')');
@@ -1525,16 +1825,61 @@ function updateStudioCode(){
1525
1825
  lines.push('<span class="fn">print</span>(data[<span class="s">'+pyStr('status')+'</span>])');
1526
1826
  lines.push('<span class="fn">print</span>(data[<span class="s">'+pyStr('body')+'</span>])');
1527
1827
  document.getElementById('code').innerHTML=lines.join('\\n');
1528
- rawSnippet='import requests\\n\\nresp = requests.post(\\n '+JSON.stringify(url)+',\\n json='+JSON.stringify({path:pl.path,method:'POST',body},null,4)+',\\n timeout=120,\\n)\\ndata = resp.json()\\nprint(data["status"])\\nprint(data["body"])\\n';
1828
+ rawSnippet='import requests\\n\\nresp = requests.post(\\n '+JSON.stringify(url)+',\\n json='+JSON.stringify(shown,null,4)+',\\n timeout=120,\\n)\\ndata = resp.json()\\nprint(data["status"])\\nprint(data["body"])\\n';
1529
1829
  }
1530
1830
  function mediaResultBox(id){return document.getElementById(id);}
1831
+ const PA_PLAY='<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>';
1832
+ const PA_PAUSE='<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true"><rect x="7" y="5" width="3.4" height="14" rx="1" fill="currentColor"/><rect x="13.6" y="5" width="3.4" height="14" rx="1" fill="currentColor"/></svg>';
1833
+ var __paCtx=null;
1834
+ function paContext(){try{if(!__paCtx)__paCtx=new (window.AudioContext||window.webkitAudioContext)();if(__paCtx.state==='suspended')__paCtx.resume();return __paCtx;}catch(e){return null;}}
1835
+ function fmtTime(s){if(!isFinite(s)||s<0)s=0;var m=Math.floor(s/60);var ss=Math.floor(s%60);return m+':'+(ss<10?'0':'')+ss;}
1836
+ function audioPlayer(src,title){
1837
+ var wrap=document.createElement('div');wrap.className='pa';
1838
+ var row=document.createElement('div');row.className='pa-row';
1839
+ var pb=document.createElement('button');pb.type='button';pb.className='pa-play';pb.setAttribute('aria-label','Play audio');pb.innerHTML=PA_PLAY;
1840
+ var tt=document.createElement('div');tt.className='pa-title';tt.textContent=title||'audio';
1841
+ var x=document.createElement('button');x.type='button';x.className='pa-x';x.setAttribute('aria-label','Remove player');x.textContent='×';
1842
+ row.appendChild(pb);row.appendChild(tt);row.appendChild(x);wrap.appendChild(row);
1843
+ var wave=document.createElement('div');wave.className='pa-wave';wave.setAttribute('role','slider');wave.setAttribute('aria-label','Seek in audio');wave.tabIndex=0;
1844
+ wrap.appendChild(wave);
1845
+ var times=document.createElement('div');times.className='pa-times';
1846
+ var cur=document.createElement('span');cur.textContent='0:00';
1847
+ var dur=document.createElement('span');dur.textContent='0:00';
1848
+ times.appendChild(cur);times.appendChild(dur);wrap.appendChild(times);
1849
+ var au=document.createElement('audio');au.src=src;au.preload='metadata';wrap.appendChild(au);
1850
+ var bars=[];
1851
+ function paint(f){for(var i=0;i<bars.length;i++)bars[i].classList.toggle('on',bars[i]._f<=f);}
1852
+ function buildBars(vals){wave.innerHTML='';bars=[];for(var i=0;i<vals.length;i++){var b=document.createElement('div');b.className='pa-bar';b._f=(i+1)/vals.length;b.style.height=Math.max(8,Math.round(vals[i]*100))+'%';bars.push(b);wave.appendChild(b);}}
1853
+ var init=[];for(var k=0;k<150;k++)init.push(0.12);
1854
+ buildBars(init);
1855
+ function decode(){
1856
+ var ctx=paContext();if(!ctx)return;
1857
+ fetch(src).then(function(r){return r.arrayBuffer();}).then(function(buf){return ctx.decodeAudioData(buf);}).then(function(ab){
1858
+ var ch=ab.getChannelData(0);var n=150;var step=Math.max(1,Math.floor(ch.length/n));
1859
+ var vals=[];var mx=0.0001;
1860
+ for(var i=0;i<n;i++){var m=0;for(var j=i*step;j<Math.min(ch.length,(i+1)*step);j+=7){var v=Math.abs(ch[j]);if(v>m)m=v;}vals.push(m);if(m>mx)mx=m;}
1861
+ buildBars(vals.map(function(v){return v/mx;}));
1862
+ }).catch(function(){});
1863
+ }
1864
+ pb.onclick=function(){if(au.paused){try{var pr=au.play();if(pr&&pr.catch)pr.catch(function(){});}catch(e){}}else au.pause();};
1865
+ var raf=0;var lastSec=-1;
1866
+ function tick(){if(au.paused)return;var s=Math.floor(au.currentTime);if(s!==lastSec){lastSec=s;cur.textContent=fmtTime(au.currentTime);}paint(au.duration?au.currentTime/au.duration:0);raf=requestAnimationFrame(tick);}
1867
+ au.onplay=function(){pb.innerHTML=PA_PAUSE;pb.setAttribute('aria-label','Pause audio');cancelAnimationFrame(raf);lastSec=-1;tick();};
1868
+ au.onpause=function(){pb.innerHTML=PA_PLAY;pb.setAttribute('aria-label','Play audio');cancelAnimationFrame(raf);};
1869
+ au.onloadedmetadata=function(){dur.textContent=fmtTime(au.duration);};
1870
+ au.ontimeupdate=function(){cur.textContent=fmtTime(au.currentTime);};
1871
+ au.onended=function(){cancelAnimationFrame(raf);paint(0);cur.textContent=fmtTime(0);};
1872
+ x.onclick=function(){try{au.pause();}catch(e){}wrap.remove();};
1873
+ wave.onclick=function(e){var r=wave.getBoundingClientRect();var f=(e.clientX-r.left)/r.width;if(au.duration)au.currentTime=Math.max(0,Math.min(1,f))*au.duration;};
1874
+ wave.onkeydown=function(e){if(!au.duration)return;if(e.key==='ArrowRight'){au.currentTime=Math.min(au.duration,au.currentTime+5);e.preventDefault();}else if(e.key==='ArrowLeft'){au.currentTime=Math.max(0,au.currentTime-5);e.preventDefault();}else if(e.key==='Home'){au.currentTime=0;e.preventDefault();}else if(e.key==='End'){au.currentTime=au.duration;e.preventDefault();}};
1875
+ decode();
1876
+ return wrap;
1877
+ }
1531
1878
  function appendMediaOut(boxId,kind,src,mime,caption,downloadName){
1532
1879
  const box=mediaResultBox(boxId);
1533
1880
  const wrap=document.createElement('div');
1534
- const el=document.createElement(kind==='audio'?'audio':'video');
1535
- el.controls=true;el.src=src;
1536
- if(kind==='video'){el.preload='metadata';}
1537
- wrap.appendChild(el);
1881
+ if(kind==='audio')wrap.appendChild(audioPlayer(src,caption||downloadName||'audio'));
1882
+ else{const el=document.createElement('video');el.controls=true;el.src=src;el.preload='metadata';wrap.appendChild(el);}
1538
1883
  const cap=document.createElement('div');cap.style.cssText='font-size:12px;color:var(--ink-soft)';cap.textContent=caption;
1539
1884
  wrap.appendChild(cap);
1540
1885
  const a=document.createElement('a');a.className='dl';a.href=src;a.download=downloadName;a.textContent='Download '+kind;
@@ -1561,6 +1906,8 @@ function detectMediaInBody(r){
1561
1906
  }
1562
1907
  if(first.url)return {kind:studioMode(),src:first.url,mime:'',json:true,caption:first.revised_prompt||''};
1563
1908
  }
1909
+ if(Array.isArray(d.output)&&d.output.length&&typeof d.output[0]==='string'&&d.output[0].indexOf('http')===0)
1910
+ return {kind:'image',src:d.output[0],mime:'',json:true};
1564
1911
  if(d.audio||d.audio_base64||d.data_base64){
1565
1912
  const b=d.audio||d.audio_base64||d.data_base64;
1566
1913
  return {kind:'audio',src:'data:audio/mpeg;base64,'+b,mime:'audio/mpeg',json:true};
@@ -1568,6 +1915,32 @@ function detectMediaInBody(r){
1568
1915
  }catch{}
1569
1916
  return null;
1570
1917
  }
1918
+ async function pollReplicate(r){
1919
+ if(curProv()!=='replicate')return null;
1920
+ let id=null;
1921
+ try{const d=JSON.parse(r.body||'{}');if(d.status==='succeeded')return r;id=d.id||null;}catch(e){return null;}
1922
+ if(!id)return null;
1923
+ const pid=Number(document.getElementById('pool').value);
1924
+ for(let i=0;i<45;i++){
1925
+ await new Promise(function(res){setTimeout(res,2000);});
1926
+ const p=await j(await fetch('/api/pools/'+pid+'/proxy',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({path:'/predictions/'+id,method:'GET'})}));
1927
+ if(p.error)continue;
1928
+ try{const pd=JSON.parse(p.body||'{}');if(pd.status==='succeeded'||pd.status==='failed'||pd.status==='canceled')return p;}catch(e){return p;}
1929
+ }
1930
+ err('Replicate prediction timed out; check its dashboard for the result.');
1931
+ return null;
1932
+ }
1933
+ function triedNote(r){
1934
+ var t=r.tried;
1935
+ if(!t||!t.length)return '';
1936
+ var bits=t.map(function(x){
1937
+ var what=x.status?('HTTP '+x.status):(x.detail||'skipped');
1938
+ return '#'+x.key_id+' ('+(x.label||'?')+'): '+what;
1939
+ });
1940
+ var s=' — tried '+bits.join(', ');
1941
+ if(r.error==='all keys failed or cooling down')s+='. Keys recover on their own (30s backoff, doubling); re-saving a key in the key manager clears its cooldown immediately.';
1942
+ return s;
1943
+ }
1571
1944
  async function sendStudio(){
1572
1945
  err('');
1573
1946
  const s=studioParams();
@@ -1575,6 +1948,7 @@ async function sendStudio(){
1575
1948
  if(s.mode==='image'&&!s.prompt.trim()){err('Describe the image first.');return;}
1576
1949
  if(s.mode!=='image'&&!s.prompt.trim()){err(s.mode==='audio'?'Type text to synthesize first.':'Describe the video first.');return;}
1577
1950
  if(!s.model){err('Enter a model id.');return;}
1951
+ if(/[{}]/.test(s.model)){err('Model "'+s.model+'" looks like a template — replace {...} with a real id (e.g. aura-2-thalia-en for Deepgram).');return;}
1578
1952
  const prov=curProv();
1579
1953
  const need=s.mode==='image'?'imageOut':s.mode==='audio'?'audioOut':'videoOut';
1580
1954
  if(!capsFor(prov,s.model)[need]){err(gateNote(need,prov,s.model));return;}
@@ -1583,10 +1957,17 @@ async function sendStudio(){
1583
1957
  const label=btn.innerHTML;
1584
1958
  btn.disabled=true;btn.textContent=s.mode==='image'?'Generating…':s.mode==='audio'?'Synthesizing…':'Rendering…';
1585
1959
  try{
1586
- const r=await j(await fetch('/api/pools/'+pid+'/proxy',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({path:pl.path,method:'POST',body:pl.body})}));
1587
- if(r.error){err(r.error);return;}
1588
- const hit=detectMediaInBody(r);
1589
- if(!hit){err('No media in upstream response (status '+r.status+').');return;}
1960
+ const preq={path:pl.path,method:'POST'};
1961
+ if(pl.query)preq.query=pl.query;
1962
+ if(pl.form)preq.form=pl.form;else preq.body=pl.body;
1963
+ const t0=Date.now();
1964
+ const r=await j(await fetch('/api/pools/'+pid+'/proxy',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(preq)}));
1965
+ pgLogAdd({tab:'studio-'+s.mode,method:'POST',path:preq.path,prov:prov,model:s.model,upstream:r.error?'ERR':r.status,key:r.key_id?('#'+r.key_id):'',ms:Date.now()-t0,err:r.error||'',req:JSON.stringify(preq).slice(0,1500),res:String(r.body||'').slice(0,800)});
1966
+ if(r.error){err(r.error+(r.body?' — '+String(r.body).slice(0,300):'')+triedNote(r));return;}
1967
+ const rep=await pollReplicate(r);
1968
+ if(rep&&rep!==r)pgLogAdd({tab:'studio-poll',method:'GET',path:'replicate prediction',prov:prov,model:s.model,upstream:rep.error?'ERR':rep.status,key:'',ms:0,err:rep.error||'',req:'',res:String(rep.body||'').slice(0,800)});
1969
+ const hit=detectMediaInBody(rep||r);
1970
+ if(!hit){var said=String((rep||r).body||'');err('No media in upstream response (status '+r.status+', '+pl.path+'). Upstream said: '+(said?said.slice(0,300):'(empty — often a wrong path or model id for this provider)'));return;}
1590
1971
  if(s.mode==='image'){
1591
1972
  let src=hit.src;
1592
1973
  if(hit.kind!=='image'&&!hit.json){err('Unexpected media kind in response.');return;}
@@ -1595,12 +1976,82 @@ async function sendStudio(){
1595
1976
  const img=document.createElement('img');img.src=src;img.alt=s.prompt;img.loading='lazy';
1596
1977
  const cap=document.createElement('figcaption');cap.textContent=s.model+' · '+(hit.caption||s.prompt);
1597
1978
  fig.appendChild(img);fig.appendChild(cap);
1979
+ histPush('imgModel',s.model);histFill('imgModel','imgModelList');
1598
1980
  gal.prepend(fig);
1599
1981
  return;
1600
1982
  }
1983
+ if(s.mode==='audio'){histPush('audModel',s.model);histPush('audVoice',s.voice);histFill('audModel','audModelList');histFill('audVoice','audVoiceList');}
1984
+ else{histPush('vidModel',s.model);histFill('vidModel','vidModelList');}
1601
1985
  appendMediaOut(s.mode==='audio'?'audioOut':'videoOut',s.mode,hit.src,hit.mime,s.model+' · '+s.prompt.slice(0,60),s.mode+'-output');
1602
1986
  }finally{btn.disabled=false;btn.innerHTML=label;updateCode();}
1603
1987
  }
1988
+ function probeQueryText(){
1989
+ var out=[];
1990
+ document.getElementById('turns').childNodes.forEach(function(n){
1991
+ if(n.className&&n.className.indexOf('uturn')>=0){
1992
+ var t=n.dataset.locked?(n.dataset.content||''):n.querySelector('textarea').value;
1993
+ if(t.trim())out.push(t.trim());
1994
+ }
1995
+ });
1996
+ return out.join('\\n');
1997
+ }
1998
+ function probeRequest(){
1999
+ var a=curAdapter();var pr=a.probe;var q=probeQueryText()||'<<QUERY>>';
2000
+ var vars=probeVars(q);
2001
+ var path=fillTpl(pr.path,vars);
2002
+ var req={path:path,method:pr.method||'POST'};
2003
+ if(pr.query)req.query=fillTpl(pr.query,vars);
2004
+ if(pr.body!==undefined&&pr.body!==null){
2005
+ var b=fillTpl(pr.body,vars);
2006
+ if(a.formOnly)req.form=b;else req.body=b;
2007
+ }
2008
+ var s=JSON.stringify(req);
2009
+ var missing=['<<ORG>>','<<SID>>','<<DOMAIN>>'].filter(function(k){return s.indexOf(k)>=0;});
2010
+ return {req:req,missing:missing};
2011
+ }
2012
+ async function sendProbe(){
2013
+ err('');
2014
+ var btn=document.getElementById('submit');
2015
+ var a=curAdapter();
2016
+ if(!probeQueryText()){err(a&&a.kind==='search'?'Type the search query first.':'Type the request text first.');return;}
2017
+ var pr=probeRequest();
2018
+ if(pr.missing.length){err('Fill '+pr.missing.join(', ')+' under Parameters first.');return;}
2019
+ var pid=Number(document.getElementById('pool').value);
2020
+ btn.disabled=true;lockComposers();
2021
+ try{
2022
+ var t0=Date.now();
2023
+ var r=await j(await fetch('/api/pools/'+pid+'/proxy',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(pr.req)}));
2024
+ pgLogAdd({tab:'probe',method:pr.req.method||'POST',path:pr.req.path,prov:curProv(),model:document.getElementById('model').value.trim(),upstream:r.error?'ERR':r.status,key:r.key_id?('#'+r.key_id):'',ms:Date.now()-t0,err:r.error||'',req:JSON.stringify(pr.req).slice(0,1500),res:String(r.body||'').slice(0,800)});
2025
+ if(r.error){addAssistantTurn(r.error,true);addUserTurn('').focus();return;}
2026
+ var text=r.body||'';
2027
+ try{
2028
+ var d=JSON.parse(text);
2029
+ var hit=extractText(d,a.textPaths);
2030
+ text=hit!==null?hit:text;
2031
+ if(typeof text!=='string')text=JSON.stringify(text,null,2);
2032
+ }catch(e){}
2033
+ addAssistantTurn(text,r.status<200||r.status>=300);
2034
+ addUserTurn('').focus();
2035
+ }finally{btn.disabled=false;updateCode();}
2036
+ }
2037
+ function updateProbeCode(){
2038
+ var pid=document.getElementById('pool').value||'1';
2039
+ var url=location.origin+'/api/pools/'+pid+'/proxy';
2040
+ var lang=document.getElementById('codeLang').value;
2041
+ var pr=probeRequest();
2042
+ if(pr.missing.length){
2043
+ rawSnippet='# Fill '+pr.missing.join(', ')+' under Parameters first.\\n';
2044
+ document.getElementById('code').innerHTML='<span class="c">'+esc(rawSnippet)+'</span>';
2045
+ return;
2046
+ }
2047
+ if(lang==='curl'){
2048
+ rawSnippet='curl -X POST '+url+' -H "Content-Type: application/json" -d '+pyStr(JSON.stringify(pr.req))+';\\n';
2049
+ document.getElementById('code').innerHTML='<span class="c"># runs through your pooled keys — auth injected server-side</span>\\n'+esc(rawSnippet);
2050
+ return;
2051
+ }
2052
+ rawSnippet='import requests\\n\\nresp = requests.post(\\n '+JSON.stringify(url)+',\\n json='+JSON.stringify(pr.req,null,4)+',\\n timeout=120,\\n)\\ndata = resp.json()\\nprint(data["status"])\\nprint(data["body"])\\n';
2053
+ document.getElementById('code').innerHTML='<span class="c"># probe via pooled keys — auth injected server-side</span>\\n'+esc(rawSnippet);
2054
+ }
1604
2055
  async function send(){
1605
2056
  err('');
1606
2057
  const btn=document.getElementById('submit');
@@ -1608,6 +2059,8 @@ async function send(){
1608
2059
  if(!msgs.some(m=>m.role==='user')){err('Type a user message first.');return;}
1609
2060
  if(!p.model){err('Enter a model id.');return;}
1610
2061
  const prov=curProv();
2062
+ const pa0=curAdapter();
2063
+ if(pa0&&!pa0.chatPath&&pa0.probe){return sendProbe();}
1611
2064
  const c=capsFor(prov,p.model);
1612
2065
  const bad=pendingMedia.filter(function(a){return (a.kind==='image'&&!c.vision)||(a.kind==='audio'&&!c.audioIn)||(a.kind==='video'&&!c.videoIn);});
1613
2066
  if(bad.length){err(gateNote(bad[0].kind==='image'?'vision':bad[0].kind==='audio'?'audioIn':'videoIn',prov,p.model));return;}
@@ -1615,19 +2068,131 @@ async function send(){
1615
2068
  btn.disabled=true;
1616
2069
  lockComposers();
1617
2070
  try{
1618
- const r=await j(await fetch('/api/pools/'+pid+'/proxy',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({path:p.path,method:'POST',body:{model:p.model,messages:msgs,temperature:p.temp,max_tokens:p.maxtokens,top_p:p.topp}})}));
2071
+ const t0=Date.now();
2072
+ const preq={path:chatPathFor(prov,p.model,p.path),method:'POST',body:chatBodyFor(prov,p,msgs)};
2073
+ const r=await j(await fetch('/api/pools/'+pid+'/proxy',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(preq)}));
2074
+ pgLogAdd({tab:'chat',method:'POST',path:preq.path,prov:prov,model:p.model,upstream:r.error?'ERR':r.status,key:r.key_id?('#'+r.key_id):'',ms:Date.now()-t0,err:r.error||'',req:JSON.stringify(preq).slice(0,1500),res:String(r.body||'').slice(0,800)});
1619
2075
  pendingMedia=[];renderMediaPrev();updateMediaUI();
1620
2076
  if(r.error){addAssistantTurn(r.error,true);addUserTurn('').focus();return;}
1621
2077
  let text=r.body||'';
1622
2078
  try{
1623
2079
  const d=JSON.parse(text);
1624
- text=(d.choices&&d.choices[0]&&(d.choices[0].message||{}).content)||(d.content&&d.content[0]&&d.content[0].text)||text;
2080
+ const hit=extractText(d,(pa0&&pa0.textPaths)||[]);
2081
+ text=hit!==null?hit:text;
1625
2082
  if(typeof text!=='string')text=JSON.stringify(text,null,2);
1626
2083
  }catch{}
2084
+ if(r.status>=200&&r.status<300){histPush('model',p.model);modelMenuSync();}
1627
2085
  addAssistantTurn(text,r.status<200||r.status>=300);
1628
2086
  addUserTurn('').focus();
1629
2087
  }finally{btn.disabled=false;updateCode();}
1630
2088
  }
2089
+ // Pasted-model history: every id the user types is remembered per
2090
+ // provider+field (localStorage, max 10 each) and offered back as
2091
+ // datalist suggestions, ahead of live-listed models.
2092
+ function histKey(field){return curProv()+'::'+field;}
2093
+ function histGet(field){
2094
+ try{var h=JSON.parse(localStorage.getItem('pa-model-history')||'{}');var l=h[histKey(field)];return Array.isArray(l)?l.filter(function(x){return typeof x==='string'&&x;}):[];}catch(e){return [];}
2095
+ }
2096
+ function histPush(field,val){
2097
+ val=String(val||'').trim();if(!val)return;
2098
+ try{
2099
+ var h=JSON.parse(localStorage.getItem('pa-model-history')||'{}');
2100
+ var k=histKey(field);var l=h[k]||[];
2101
+ l=[val].concat(l.filter(function(x){return x!==val;}));
2102
+ h[k]=l.slice(0,10);
2103
+ localStorage.setItem('pa-model-history',JSON.stringify(h));
2104
+ }catch(e){}
2105
+ }
2106
+ function histFill(field,listId,live){
2107
+ var dl=document.getElementById(listId);if(!dl)return;dl.innerHTML='';
2108
+ var seen={};
2109
+ histGet(field).concat(live||[]).slice(0,200).forEach(function(id){
2110
+ if(!id||seen[id])return;seen[id]=1;
2111
+ var o=document.createElement('option');o.value=id;dl.appendChild(o);
2112
+ });
2113
+ }
2114
+ var histTimers={};
2115
+ function histSoon(field){
2116
+ if(histTimers[field])clearTimeout(histTimers[field]);
2117
+ histTimers[field]=setTimeout(function(){
2118
+ var el=document.getElementById(field);if(!el||!el.value.trim())return;
2119
+ histPush(field,el.value);
2120
+ if(field==='model')modelMenuSync();
2121
+ else histFill(field,field+'List',undefined);
2122
+ },800);
2123
+ }
2124
+ function modelMenuSync(){
2125
+ var m=document.getElementById('modelMenu');if(!m)return;m.innerHTML='';
2126
+ var pid=String(document.getElementById('pool').value);
2127
+ var live=(window._pgModels&&window._pgModels[pid])||[];
2128
+ var a=curAdapter();var def=a&&a.defaults&&a.defaults.chat;
2129
+ var seen={};var cur=document.getElementById('model').value;
2130
+ var seeds=histGet('model').concat(live);
2131
+ if(def)seeds=seeds.concat([def]);
2132
+ if(a&&a.defaults){if(a.defaults.image)seeds=seeds.concat([a.defaults.image]);if(a.defaults.audio)seeds=seeds.concat([a.defaults.audio]);}
2133
+ seeds.slice(0,200).forEach(function(id){
2134
+ if(!id||seen[id])return;seen[id]=1;
2135
+ var it=document.createElement('button');it.type='button';it.className='dd-item';it.setAttribute('role','option');
2136
+ var t=document.createElement('span');t.className='dd-txt';t.textContent=id;it.appendChild(t);
2137
+ if(id===cur)it.setAttribute('aria-selected','true');
2138
+ it.onclick=(function(v){return function(){
2139
+ var box=document.getElementById('model');box.value=v;
2140
+ box.dispatchEvent(new Event('input',{bubbles:true}));box.dispatchEvent(new Event('change',{bubbles:true}));
2141
+ modelMenuClose();box.focus();
2142
+ };})(id);
2143
+ m.appendChild(it);
2144
+ });
2145
+ if(!m.childNodes.length){var d=document.createElement('div');d.className='dd-item';d.style.cursor='default';d.textContent='No saved models yet — paste an id into the field.';m.appendChild(d);}
2146
+ }
2147
+ function modelMenuClose(){var w=document.getElementById('dd-model');if(w)w.classList.remove('open');var m=document.getElementById('modelMenu');if(m)m.hidden=true;var c=document.getElementById('modelChev');if(c)c.setAttribute('aria-expanded','false');}
2148
+ function modelMenuToggle(){var w=document.getElementById('dd-model');var m=document.getElementById('modelMenu');var c=document.getElementById('modelChev');if(!w||!m)return;modelMenuSync();if(w.classList.contains('open'))modelMenuClose();else{w.classList.add('open');m.hidden=false;if(c)c.setAttribute('aria-expanded','true');}}
2149
+ async function refreshModels(){
2150
+ const list=document.getElementById('modelList');
2151
+ const box=document.getElementById('model');
2152
+ if(list)list.innerHTML='';
2153
+ const a=curAdapter();
2154
+ if(!a||!a.modelsPath)return;
2155
+ try{
2156
+ const pid=document.getElementById('pool').value;
2157
+ const r=await j(await fetch('/api/pools/'+pid+'/proxy',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({path:a.modelsPath,method:'GET'})}));
2158
+ if(r.error||!r.body)return;
2159
+ const d=JSON.parse(r.body);
2160
+ let ids=[];
2161
+ if(a.modelsStyle==='gemini'&&Array.isArray(d.models))
2162
+ ids=d.models.map(function(m){return String(m.name||'').replace(/^models\\//,'');}).filter(Boolean);
2163
+ else if(Array.isArray(d.data))
2164
+ ids=d.data.map(function(m){return m.id||m.name||'';}).filter(Boolean);
2165
+ window._pgModels=window._pgModels||{};
2166
+ window._pgModels[String(pid)]=ids;
2167
+ modelMenuSync();
2168
+ if(box&&!box.value&&ids.length){
2169
+ const pref=(a.defaults&&a.defaults.chat)||'';
2170
+ box.value=ids.indexOf(pref)>=0?pref:ids[0];
2171
+ }
2172
+ }catch(e){}
2173
+ }
2174
+ function applyPoolDefaults(){
2175
+ const prov=curProv();const a=curAdapter();
2176
+ const pathBox=document.getElementById('path');
2177
+ const modelBox=document.getElementById('model');
2178
+ if(a&&a.chatPath)pathBox.value=chatPathFor(prov,modelBox.value||((a.defaults||{}).chat||''),'/chat/completions');
2179
+ else if(a&&a.probe)pathBox.value=fillTpl(a.probe.path,probeVars(''));
2180
+ const pv=document.getElementById('probeVars');
2181
+ const pt=a&&a.probe?JSON.stringify(a.probe):'';
2182
+ if(pv)pv.hidden=!(a&&a.probe&&/<<(ORG|SID|DOMAIN|DEPTH|TOPIC|TIME_RANGE|MAX_RESULTS|ANSWER|CHUNKS|AUTO|INCL_DOMAINS|EXCL_DOMAINS)>>/.test(pt));
2183
+ var showPv=function(id,ph){var w=document.getElementById(id);if(w)w.hidden=pt.indexOf(ph)<0;};
2184
+ showPv('probeOrgWrap','<<ORG>>');showPv('probeSidWrap','<<SID>>');showPv('probeDomainWrap','<<DOMAIN>>');showPv('probeDepthWrap','<<DEPTH>>');showPv('probeTopicWrap','<<TOPIC>>');showPv('probeTimeWrap','<<TIME_RANGE>>');showPv('probeMaxWrap','<<MAX_RESULTS>>');showPv('probeAnswerWrap','<<ANSWER>>');showPv('probeChunksWrap','<<CHUNKS>>');showPv('probeAutoWrap','<<AUTO>>');showPv('probeInclWrap','<<INCL_DOMAINS>>');showPv('probeExclWrap','<<EXCL_DOMAINS>>');
2185
+ const mw=document.getElementById('dd-model');
2186
+ if(mw)mw.hidden=!!(a&&!a.chatPath);
2187
+ const av=document.getElementById('audVoice');
2188
+ const vd=VOICE_DEFAULTS[prov]||'alloy';
2189
+ if(av&&(!av.value||Object.keys(VOICE_DEFAULTS).map(function(k){return VOICE_DEFAULTS[k];}).indexOf(av.value)>=0))av.value=vd;
2190
+ histFill('imgModel','imgModelList');
2191
+ histFill('audModel','audModelList');
2192
+ histFill('vidModel','vidModelList');
2193
+ histFill('audVoice','audVoiceList');
2194
+ refreshModels();
2195
+ }
1631
2196
  async function init(){
1632
2197
  initDropdowns();
1633
2198
  const provs=await j(await fetch('/api/providers'));
@@ -1638,11 +2203,10 @@ async function init(){
1638
2203
  const pmap=window._pgPmap;
1639
2204
  for(const p of pools){
1640
2205
  const ks=await j(await fetch('/api/pools/'+p.id+'/keys'));
1641
- const prov=provs.find(x=>x.id===p.provider);
1642
2206
  pmap[p.id]=p.provider;
1643
2207
  const o=document.createElement('option');
1644
2208
  o.value=p.id;
1645
- o.textContent=(prov?prov.name:p.provider)+' · '+p.name+' ('+ks.length+' keys)';
2209
+ o.textContent=p.name;
1646
2210
  o.disabled=ks.length===0;
1647
2211
  sel.appendChild(o);
1648
2212
  }
@@ -1675,12 +2239,16 @@ async function init(){
1675
2239
  lastImgDefault=document.getElementById('imgModel').value;
1676
2240
  lastAudDefault=document.getElementById('audModel').value;
1677
2241
  lastVidDefault=document.getElementById('vidModel').value;
1678
- sel.onchange=()=>{applyDefault();applyImgDefault();applyAudDefault();applyVidDefault();lastDefault=document.getElementById('model').value;lastImgDefault=document.getElementById('imgModel').value;lastAudDefault=document.getElementById('audModel').value;lastVidDefault=document.getElementById('vidModel').value;updateMediaUI();updateStudioModes();updateCode();};
2242
+ sel.onchange=()=>{applyDefault();applyImgDefault();applyAudDefault();applyVidDefault();lastDefault=document.getElementById('model').value;lastImgDefault=document.getElementById('imgModel').value;lastAudDefault=document.getElementById('audModel').value;lastVidDefault=document.getElementById('vidModel').value;applyPoolDefaults();updateMediaUI();updateStudioModes();updateCode();};
2243
+ applyPoolDefaults();
1679
2244
  addUserTurn('');
1680
- ['system','model','path','temp','maxtokens','topp'].forEach(id=>document.getElementById(id).addEventListener('input',()=>{if(id==='model')lastDefault='⌁';if(id==='model'){updateMediaUI();updateStudioModes();}updateCode();}));
1681
- ['imgPrompt','imgModel','imgSize'].forEach(id=>document.getElementById(id).addEventListener('input',()=>{if(id==='imgModel')lastImgDefault='⌁';updateStudioModes();updateCode();}));
1682
- ['audText','audModel','audVoice','audFormat'].forEach(id=>document.getElementById(id).addEventListener('input',()=>{if(id==='audModel')lastAudDefault='⌁';updateStudioModes();updateCode();}));
1683
- ['vidPrompt','vidModel','vidDur','vidRes'].forEach(id=>document.getElementById(id).addEventListener('input',()=>{if(id==='vidModel')lastVidDefault='⌁';updateStudioModes();updateCode();}));
2245
+ ['system','model','path','temp','maxtokens','topp'].forEach(id=>document.getElementById(id).addEventListener('input',()=>{if(id==='model'){lastDefault='⌁';histSoon('model');updateMediaUI();updateStudioModes();}updateCode();}));
2246
+ ['probeOrg','probeSid','probeDomain','probeDepth','probeTopic','probeTime','probeMax','probeAnswer','probeChunks','probeAuto','probeIncl','probeExcl'].forEach(id=>document.getElementById(id).addEventListener('input',updateCode));
2247
+ ['probeOrg','probeSid','probeDomain','probeDepth','probeTopic','probeTime','probeMax','probeAnswer','probeChunks','probeAuto','probeIncl','probeExcl'].forEach(id=>document.getElementById(id).addEventListener('change',updateCode));
2248
+ ['imgPrompt','imgModel','imgSize'].forEach(id=>document.getElementById(id).addEventListener('input',()=>{if(id==='imgModel'){lastImgDefault='⌁';histSoon('imgModel');}updateStudioModes();updateCode();}));
2249
+ ['audText','audModel','audVoice','audFormat'].forEach(id=>document.getElementById(id).addEventListener('input',()=>{if(id==='audModel')lastAudDefault='⌁';if(id==='audModel')histSoon('audModel');if(id==='audVoice')histSoon('audVoice');updateStudioModes();updateCode();}));
2250
+ ['vidPrompt','vidModel','vidDur','vidRes'].forEach(id=>document.getElementById(id).addEventListener('input',()=>{if(id==='vidModel'){lastVidDefault='⌁';histSoon('vidModel');}updateStudioModes();updateCode();}));
2251
+ [['model','model'],['imgModel','imgModel'],['audModel','audModel'],['vidModel','vidModel'],['audVoice','audVoice']].forEach(function(pair){document.getElementById(pair[0]).addEventListener('change',function(){histPush(pair[1],this.value);if(pair[1]==='model')modelMenuSync();else histFill(pair[1],pair[1]+'List',window._pgModels&&window._pgModels[String(document.getElementById('pool').value)]);});});
1684
2252
  document.getElementById('generate').onclick=()=>sendStudio();
1685
2253
  document.getElementById('genAudio').onclick=()=>sendStudio();
1686
2254
  document.getElementById('genVideo').onclick=()=>sendStudio();
@@ -1700,12 +2268,28 @@ async function init(){
1700
2268
  document.getElementById('addMsg').onclick=()=>{addUserTurn('').focus();};
1701
2269
  const copy=()=>{navigator.clipboard.writeText(rawSnippet).catch(()=>{});};
1702
2270
  document.getElementById('copyCode').onclick=copy;
2271
+ document.getElementById('copyDebug').onclick=()=>{navigator.clipboard.writeText(JSON.stringify(pgLog,null,2)).catch(()=>{});};
2272
+ document.getElementById('clearDebug').onclick=()=>{pgLog=[];document.getElementById('debugLog').textContent='No requests yet.';};
1703
2273
  document.getElementById('copyTop').onclick=copy;
1704
2274
  document.getElementById('hideCode').onclick=e=>{const g=document.getElementById('grid');g.classList.toggle('nocode');e.target.textContent=g.classList.contains('nocode')?'</> Show code':'</> Hide code';};
1705
2275
  document.getElementById('tabChat').onclick=()=>setTab(true);
1706
2276
  document.getElementById('tabStudio').onclick=()=>setTab(false);
2277
+ document.getElementById('modelChev').onclick=function(e){e.stopPropagation();modelMenuToggle();};
2278
+ document.getElementById('modelMenu').addEventListener('keydown',function(e){if(e.key==='Escape'){modelMenuClose();document.getElementById('model').focus();}});
2279
+ document.addEventListener('pointerdown',function(e){var w=document.getElementById('dd-model');if(w&&w.classList.contains('open')&&!w.contains(e.target))modelMenuClose();});
1707
2280
  document.addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){e.preventDefault();if(document.getElementById('tabStudio').classList.contains('on'))sendStudio();else send();}});
2281
+ window.addEventListener('hashchange',()=>{pgFromHash();});
1708
2282
  updateMediaUI();updateStudioModes();updateCode();
2283
+ pgFromHash();
2284
+ }
2285
+ function pgAnchor(){return isStudio()?('studio-'+studioMode()):'chat';}
2286
+ function pgSyncHash(){try{history.replaceState(null,'','#'+pgAnchor());}catch(e){}}
2287
+ function pgFromHash(){
2288
+ var h=String(location.hash||'').replace('#','');
2289
+ if(h==='chat'){setTab(true);return true;}
2290
+ if(h==='studio'){setStudioMode('image');setTab(false);return true;}
2291
+ if(h.indexOf('studio-')===0){var m=h.slice(7);if(m==='image'||m==='audio'||m==='video'){setStudioMode(m);setTab(false);return true;}}
2292
+ return false;
1709
2293
  }
1710
2294
  function setTab(chat){
1711
2295
  document.getElementById('tabChat').classList.toggle('on',chat);
@@ -1713,6 +2297,7 @@ function setTab(chat){
1713
2297
  document.getElementById('grid').hidden=!chat;
1714
2298
  document.getElementById('studioPane').hidden=chat;
1715
2299
  updateCode();
2300
+ pgSyncHash();
1716
2301
  }
1717
2302
  init();
1718
2303
  </script></div></div>${shellJs}</body></html>`;
@@ -1722,6 +2307,7 @@ export function providerPage() {
1722
2307
  <html lang="en"><head><meta charset="utf-8"/>
1723
2308
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
1724
2309
  <title>Provider — pool-anything</title>
2310
+ ${subPageHead("/provider/", "Gather API keys for one provider, see per-key usage and copy a pooled proxy snippet.")}
1725
2311
  <link rel="icon" type="image/png" href="/logo.png" />
1726
2312
  <style>
1727
2313
  *{box-sizing:border-box}body{margin:0;font-family:var(--font-sans);background:var(--paper-2);color:var(--ink)}
@@ -1805,7 +2391,7 @@ async function refresh(){
1805
2391
  const row=document.createElement('div');
1806
2392
  row.className='krow';row.style.animationDelay=Math.min(i*40,300)+'ms';
1807
2393
  const m=document.createElement('span');m.className='meta';m.textContent=k.label+' · '+k.masked;row.appendChild(m);
1808
- const u=document.createElement('span');u.className='use';u.textContent=(usageMap[k.id]||0)+' tokens';row.appendChild(u);
2394
+ const u=document.createElement('span');u.className='use';u.textContent=(usageMap[k.id]||0)+' tokens'+(k.cooling?' · cooling':'');row.appendChild(u);
1809
2395
  const v=document.createElement('button');v.textContent='View';v.type='button';
1810
2396
  v.onclick=async()=>{
1811
2397
  if(v.dataset.open){delete v.dataset.open;v.textContent='View';m.textContent=k.label+' · '+k.masked;return;}