@jacobbd/relay-ai 0.4.5 → 0.4.7

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,3 +1,9 @@
1
+ import {
2
+ formatModelPrice,
3
+ getProviderModelPage,
4
+ PROVIDER_MODEL_PAGE_SIZE,
5
+ } from './provider-model-browser.js';
6
+
1
7
  // ─── State ───────────────────────────────────────────────────────────────────
2
8
 
3
9
  const AGY_MAX = 6;
@@ -13,6 +19,9 @@ const state = {
13
19
  modelsLoaded: false,
14
20
  modelsError: null,
15
21
  providerFilter: '',
22
+ activeProviderId: null,
23
+ providerModelFilter: '',
24
+ providerModelPage: 1,
16
25
  modelFilter: '',
17
26
  modelFreeOnly: false,
18
27
  agyFilter: '',
@@ -25,6 +34,7 @@ const state = {
25
34
  appModelFilters: {},
26
35
  appModelOpen: null,
27
36
  appSelections: {},
37
+ appHttpProxy: {},
28
38
  server: {
29
39
  status: null,
30
40
  error: null,
@@ -293,6 +303,7 @@ async function loadModels() {
293
303
  freeStatus: m.freeStatus,
294
304
  freeLabel: m.freeLabel,
295
305
  cost: m.cost,
306
+ claudeTransparentCompatible: Boolean(m.claudeTransparentCompatible),
296
307
  });
297
308
  if (typeof p.modelCount === 'number') p._rawCount = p.modelCount;
298
309
  }
@@ -436,7 +447,7 @@ function buildProviderCard(provider) {
436
447
  header.className = 'provider-card-header';
437
448
  header.setAttribute('role', 'button');
438
449
  header.setAttribute('tabindex', '0');
439
- header.setAttribute('aria-expanded', 'false');
450
+ header.setAttribute('aria-label', `View models from ${displayName}`);
440
451
 
441
452
  const logoHtml = logo?.type === 'svg'
442
453
  ? logo.content
@@ -454,7 +465,9 @@ function buildProviderCard(provider) {
454
465
  <span class="status-chip ${provider.hasKey ? 'has-key' : provider.freeAccess ? 'free-access' : 'no-key'}">
455
466
  ${provider.hasKey ? 'Key stored' : provider.freeAccess ? 'Free models' : 'Not configured'}
456
467
  </span>
457
- <svg class="provider-chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
468
+ <button class="provider-config-toggle" type="button" aria-label="Manage ${escapeHtml(displayName)} provider" aria-expanded="false" title="Manage provider">
469
+ <svg class="provider-chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
470
+ </button>
458
471
  </div>
459
472
  `;
460
473
 
@@ -469,18 +482,151 @@ function buildProviderCard(provider) {
469
482
 
470
483
  function toggle() {
471
484
  const isOpen = card.classList.toggle('open');
472
- header.setAttribute('aria-expanded', String(isOpen));
485
+ configToggle.setAttribute('aria-expanded', String(isOpen));
473
486
  body.setAttribute('aria-hidden', String(!isOpen));
474
487
  }
475
488
 
476
- header.addEventListener('click', toggle);
477
- header.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } });
489
+ const configToggle = header.querySelector('.provider-config-toggle');
490
+ configToggle.addEventListener('click', event => { event.stopPropagation(); toggle(); });
491
+ header.addEventListener('click', () => openProviderModelBrowser(provider.id));
492
+ header.addEventListener('keydown', e => {
493
+ if (e.target === header && (e.key === 'Enter' || e.key === ' ')) {
494
+ e.preventDefault();
495
+ openProviderModelBrowser(provider.id);
496
+ }
497
+ });
478
498
 
479
499
  card.appendChild(header);
480
500
  card.appendChild(body);
481
501
  return card;
482
502
  }
483
503
 
504
+ function providerIdFromHash() {
505
+ const prefix = '#provider/';
506
+ if (!window.location.hash.startsWith(prefix)) return null;
507
+ try {
508
+ return decodeURIComponent(window.location.hash.slice(prefix.length));
509
+ } catch {
510
+ return null;
511
+ }
512
+ }
513
+
514
+ function openProviderModelBrowser(providerId) {
515
+ if (state.activeProviderId !== providerId) {
516
+ state.providerModelFilter = '';
517
+ state.providerModelPage = 1;
518
+ }
519
+ window.location.hash = `provider/${encodeURIComponent(providerId)}`;
520
+ }
521
+
522
+ function syncProviderModelBrowserFromHash() {
523
+ const providerId = providerIdFromHash();
524
+ const content = document.getElementById('content');
525
+ state.activeProviderId = providerId;
526
+ content.classList.toggle('provider-browser-open', Boolean(providerId));
527
+ if (!providerId) return;
528
+ document.querySelectorAll('.nav-item').forEach(item => {
529
+ item.classList.toggle('active', item.dataset.section === 'providers');
530
+ });
531
+ content.scrollTop = 0;
532
+ renderProviderModelBrowser();
533
+ }
534
+
535
+ function renderProviderModelBrowser() {
536
+ const container = document.getElementById('provider-model-browser');
537
+ if (!state.activeProviderId) return;
538
+
539
+ const provider = state.providers.find(item => item.id === state.activeProviderId);
540
+ if (!provider) {
541
+ container.innerHTML = state.modelsLoaded
542
+ ? '<div class="provider-browser-empty">Provider not found. <a href="#providers">Return to providers</a></div>'
543
+ : '<div class="provider-browser-empty">Loading models…</div>';
544
+ return;
545
+ }
546
+
547
+ const result = getProviderModelPage(provider.models ?? [], state.providerModelFilter, state.providerModelPage);
548
+ state.providerModelPage = result.page;
549
+ const first = result.total === 0 ? 0 : (result.page - 1) * PROVIDER_MODEL_PAGE_SIZE + 1;
550
+ const last = Math.min(result.page * PROVIDER_MODEL_PAGE_SIZE, result.total);
551
+ const [c1, c2] = providerPalette(provider.id);
552
+ const displayName = getProviderName(provider.id);
553
+
554
+ container.innerHTML = `
555
+ <a class="provider-browser-back" href="#providers">← Providers &amp; Keys</a>
556
+ <div class="provider-browser-hero">
557
+ <div class="provider-icon" style="background:linear-gradient(135deg,${c1},${c2})">${providerInitial(displayName)}</div>
558
+ <div>
559
+ <div class="section-eyebrow">Provider catalog</div>
560
+ <h1 class="section-heading">${escapeHtml(displayName)} <span class="heading-accent">models</span></h1>
561
+ <p class="section-sub">${provider.models?.length ?? 0} models available · prices shown per 1M tokens</p>
562
+ </div>
563
+ </div>
564
+ <div class="provider-browser-tools">
565
+ <div class="search-field">
566
+ <svg class="search-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
567
+ <input class="search-input" id="provider-model-search" type="search" value="${escapeHtml(state.providerModelFilter)}" placeholder="Search ${escapeHtml(displayName)} models…" aria-label="Search ${escapeHtml(displayName)} models">
568
+ </div>
569
+ <button class="btn btn-ghost" id="provider-model-refresh" type="button">Refresh</button>
570
+ </div>
571
+ <div class="provider-model-table-wrap">
572
+ <table class="provider-model-table">
573
+ <thead><tr><th>Model</th><th>Context</th><th>Price in / out</th></tr></thead>
574
+ <tbody>
575
+ ${result.items.map(model => `
576
+ <tr>
577
+ <td><strong>${escapeHtml(model.name || model.id)}</strong><code>${escapeHtml(model.id)}</code></td>
578
+ <td>${fmtCtx(model.contextWindow) || '—'}</td>
579
+ <td>${formatModelPrice(model.cost)}</td>
580
+ </tr>
581
+ `).join('') || '<tr><td colspan="3" class="provider-model-empty">No models match your search.</td></tr>'}
582
+ </tbody>
583
+ </table>
584
+ </div>
585
+ <div class="provider-model-pagination">
586
+ <span>${first}–${last} of ${result.total}</span>
587
+ <div>
588
+ <button class="btn btn-ghost" id="provider-model-prev" type="button" ${result.page <= 1 ? 'disabled' : ''}>Previous</button>
589
+ <span>Page ${result.page} of ${result.totalPages}</span>
590
+ <button class="btn btn-primary" id="provider-model-next" type="button" ${result.page >= result.totalPages ? 'disabled' : ''}>Next</button>
591
+ </div>
592
+ </div>
593
+ `;
594
+
595
+ document.getElementById('provider-model-search').addEventListener('input', event => {
596
+ state.providerModelFilter = event.target.value;
597
+ state.providerModelPage = 1;
598
+ renderProviderModelBrowser();
599
+ const search = document.getElementById('provider-model-search');
600
+ search?.focus();
601
+ search?.setSelectionRange(search.value.length, search.value.length);
602
+ });
603
+ document.getElementById('provider-model-prev').addEventListener('click', () => {
604
+ state.providerModelPage -= 1;
605
+ renderProviderModelBrowser();
606
+ });
607
+ document.getElementById('provider-model-next').addEventListener('click', () => {
608
+ state.providerModelPage += 1;
609
+ renderProviderModelBrowser();
610
+ });
611
+ document.getElementById('provider-model-refresh').addEventListener('click', async event => {
612
+ const button = event.currentTarget;
613
+ button.disabled = true;
614
+ button.textContent = 'Refreshing…';
615
+ const refreshed = await refreshProvider(provider.id);
616
+ if (!refreshed.ok) {
617
+ showToast(refreshed.error ?? 'Refresh failed');
618
+ button.disabled = false;
619
+ button.textContent = 'Refresh';
620
+ return;
621
+ }
622
+ await initModels();
623
+ renderProviders();
624
+ renderProviderStats();
625
+ renderProviderModelBrowser();
626
+ showToast(`${refreshed.count} models refreshed`);
627
+ });
628
+ }
629
+
484
630
  function buildTemplateCard(template) {
485
631
  const [c1, c2] = providerPalette(template.id);
486
632
  const initial = providerInitial(template.name);
@@ -1404,6 +1550,7 @@ function initNav() {
1404
1550
  setActive('providers');
1405
1551
 
1406
1552
  content.addEventListener('scroll', () => {
1553
+ if (content.classList.contains('provider-browser-open')) return;
1407
1554
  const contentTop = content.getBoundingClientRect().top;
1408
1555
  const threshold = content.clientHeight * 0.4;
1409
1556
  let activeId = 'providers';
@@ -1508,8 +1655,12 @@ async function init() {
1508
1655
  renderAgyList();
1509
1656
  if (state.modelFilter) buildModelResults(state.modelFilter, 'general');
1510
1657
  if (state.agyFilter) buildModelResults(state.agyFilter, 'agy');
1658
+ syncProviderModelBrowserFromHash();
1511
1659
  });
1512
1660
 
1661
+ window.addEventListener('hashchange', syncProviderModelBrowserFromHash);
1662
+ syncProviderModelBrowserFromHash();
1663
+
1513
1664
  document.getElementById('provider-search').addEventListener('input', e => {
1514
1665
  state.providerFilter = e.target.value;
1515
1666
  renderProviders();
@@ -1649,6 +1800,16 @@ function appModelInputValue(appId) {
1649
1800
  return getAppSelection(appId).label;
1650
1801
  }
1651
1802
 
1803
+ function claudeHttpProxyAvailable(appId) {
1804
+ if (appId !== 'claude') return false;
1805
+ const selection = getAppSelection(appId);
1806
+ if (selection.mode !== 'model') return true;
1807
+ const model = state.allModels.find(
1808
+ candidate => candidate.providerId === selection.providerId && candidate.id === selection.modelId,
1809
+ );
1810
+ return Boolean(model?.claudeTransparentCompatible);
1811
+ }
1812
+
1652
1813
  function matchedAppModels(appId) {
1653
1814
  const localFilter = state.appModelFilters[appId] ?? '';
1654
1815
  const q = (localFilter || state.appModelFilter).trim().toLowerCase();
@@ -1749,6 +1910,7 @@ function renderApps() {
1749
1910
  const modelInputValue = appModelInputValue(app.id);
1750
1911
  const modelResults = state.appModelOpen === app.id ? buildAppModelResults(app.id) : '';
1751
1912
  const launchFolder = state.appLaunchFolders[app.id] ?? '';
1913
+ const httpProxyAvailable = claudeHttpProxyAvailable(app.id);
1752
1914
  const recentFolders = state.recentLaunchFolders
1753
1915
  .map(folder => `<button class="launch-folder-chip" type="button" onclick="selectLaunchFolder('${app.id}', '${encodeURIComponent(folder)}')">${escapeHtml(folder)}</button>`)
1754
1916
  .join('');
@@ -1782,6 +1944,16 @@ function renderApps() {
1782
1944
  ` : ''}
1783
1945
  </div>
1784
1946
  </div>
1947
+ ${app.id === 'claude' ? `
1948
+ <label class="claude-proxy-option${httpProxyAvailable ? '' : ' is-disabled'}">
1949
+ <input type="checkbox" ${state.appHttpProxy[app.id] && httpProxyAvailable ? 'checked' : ''} ${httpProxyAvailable ? '' : 'disabled'} onchange="setClaudeHttpProxy('${app.id}', this.checked)">
1950
+ <span class="claude-proxy-label">
1951
+ Keep my Anthropic login and add Relay models
1952
+ <span class="claude-proxy-tooltip" tabindex="0" role="img" aria-label="Launches Claude Code through a temporary local connection. Your normal Anthropic login and models continue to work, while compatible Relay AI favorites become available for model switching. The connection closes automatically when Claude Code exits." data-tooltip="Launches Claude Code through a temporary local connection. Your normal Anthropic login and models continue to work, while compatible Relay AI favorites become available for model switching. The connection closes automatically when Claude Code exits.">?</span>
1953
+ ${httpProxyAvailable ? '' : '<span class="claude-proxy-unavailable">This selected model cannot be combined with your Anthropic login.</span>'}
1954
+ </span>
1955
+ </label>
1956
+ ` : ''}
1785
1957
  ${app.type !== 'app' ? `
1786
1958
  <div class="launch-folder-control">
1787
1959
  <label style="font-size: 12px; font-weight: 500; color: var(--color-muted);">Launch folder 📁</label>
@@ -1864,6 +2036,9 @@ async function launchApp(appId) {
1864
2036
  body.providerId = selection.providerId;
1865
2037
  body.modelId = selection.modelId;
1866
2038
  }
2039
+ if (appId === 'claude' && state.appHttpProxy[appId] && claudeHttpProxyAvailable(appId)) {
2040
+ body.httpProxy = true;
2041
+ }
1867
2042
 
1868
2043
  const folder = (state.appLaunchFolders[appId] ?? '').trim();
1869
2044
  if (folder) {
@@ -1968,6 +2143,9 @@ function selectLaunchModel(appId, encodedProviderId, encodedModelId) {
1968
2143
  const favorite = isGeneralFavorite(providerId, modelId);
1969
2144
  const label = `${favorite ? '★ ' : ''}${model?.name || modelId}`;
1970
2145
  state.appSelections[appId] = { mode: 'model', providerId, modelId, label };
2146
+ if (appId === 'claude' && !model?.claudeTransparentCompatible) {
2147
+ state.appHttpProxy[appId] = false;
2148
+ }
1971
2149
  state.appModelOpen = null;
1972
2150
  state.appModelFilters[appId] = '';
1973
2151
  renderApps();
@@ -1977,6 +2155,10 @@ function setLaunchFolder(appId, value) {
1977
2155
  state.appLaunchFolders[appId] = value;
1978
2156
  }
1979
2157
 
2158
+ function setClaudeHttpProxy(appId, checked) {
2159
+ state.appHttpProxy[appId] = claudeHttpProxyAvailable(appId) && Boolean(checked);
2160
+ }
2161
+
1980
2162
  function selectLaunchFolder(appId, encodedFolder) {
1981
2163
  state.appLaunchFolders[appId] = decodeURIComponent(encodedFolder);
1982
2164
  renderApps();
@@ -2033,6 +2215,7 @@ window.selectLaunchDefault = selectLaunchDefault;
2033
2215
  window.selectLaunchFavorites = selectLaunchFavorites;
2034
2216
  window.selectLaunchModel = selectLaunchModel;
2035
2217
  window.setLaunchFolder = setLaunchFolder;
2218
+ window.setClaudeHttpProxy = setClaudeHttpProxy;
2036
2219
  window.selectLaunchFolder = selectLaunchFolder;
2037
2220
  window.browseLaunchFolder = browseLaunchFolder;
2038
2221
  window.saveAppPath = saveAppPath;
@@ -15,7 +15,13 @@
15
15
 
16
16
  <!-- Sidebar -->
17
17
  <aside class="sidebar">
18
- <div class="sidebar-brand">
18
+ <a
19
+ class="sidebar-brand"
20
+ href="https://github.com/jacob-bd/relay-ai"
21
+ target="_blank"
22
+ rel="noopener noreferrer"
23
+ aria-label="Open the relay-ai GitHub repository"
24
+ >
19
25
  <div class="brand-logo">
20
26
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="100%" height="100%">
21
27
  <defs>
@@ -42,7 +48,7 @@
42
48
  <div class="brand-name">relay<span class="brand-accent">-ai</span></div>
43
49
  <div class="brand-byline">by jacob-bd</div>
44
50
  </div>
45
- </div>
51
+ </a>
46
52
 
47
53
  <div class="sidebar-version-area" id="sidebar-version-area">
48
54
  <div class="sidebar-version">v{{VERSION}}</div>
@@ -236,6 +242,8 @@
236
242
  <div id="server-panel"></div>
237
243
  </section>
238
244
 
245
+ <section id="provider-model-browser" class="section provider-model-browser" aria-live="polite"></section>
246
+
239
247
  </main>
240
248
  </div>
241
249
 
@@ -0,0 +1,29 @@
1
+ export const PROVIDER_MODEL_PAGE_SIZE = 25;
2
+
3
+ export function filterProviderModels(models, query) {
4
+ const needle = query.trim().toLowerCase();
5
+ if (!needle) return models;
6
+ return models.filter(model =>
7
+ model.id.toLowerCase().includes(needle)
8
+ || (model.name ?? '').toLowerCase().includes(needle),
9
+ );
10
+ }
11
+
12
+ export function getProviderModelPage(models, query, requestedPage) {
13
+ const filtered = filterProviderModels(models, query);
14
+ const totalPages = Math.max(1, Math.ceil(filtered.length / PROVIDER_MODEL_PAGE_SIZE));
15
+ const page = Math.min(Math.max(1, requestedPage), totalPages);
16
+ const start = (page - 1) * PROVIDER_MODEL_PAGE_SIZE;
17
+ return {
18
+ items: filtered.slice(start, start + PROVIDER_MODEL_PAGE_SIZE),
19
+ page,
20
+ total: filtered.length,
21
+ totalPages,
22
+ };
23
+ }
24
+
25
+ export function formatModelPrice(cost) {
26
+ if (!cost || !Number.isFinite(cost.input) || !Number.isFinite(cost.output)) return '—';
27
+ const format = value => `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 4 })}`;
28
+ return `${format(cost.input)} / ${format(cost.output)}`;
29
+ }
@@ -125,8 +125,14 @@ input { font-family: inherit; }
125
125
  align-items: center;
126
126
  gap: 12px;
127
127
  padding: 28px 24px 20px;
128
+ color: inherit;
129
+ text-decoration: none;
130
+ transition: background var(--dur-sm) var(--ease);
128
131
  }
129
132
 
133
+ .sidebar-brand:hover { background: var(--surface-hover); }
134
+ .sidebar-brand:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
135
+
130
136
  .brand-logo {
131
137
  width: 46px;
132
138
  height: 46px;
@@ -768,6 +774,95 @@ input { font-family: inherit; }
768
774
  gap: 4px;
769
775
  }
770
776
 
777
+ .claude-proxy-option {
778
+ display: flex;
779
+ align-items: flex-start;
780
+ gap: 9px;
781
+ padding: 10px 12px;
782
+ border: 1px solid var(--border);
783
+ border-radius: var(--radius);
784
+ background: var(--surface);
785
+ color: var(--text-2);
786
+ cursor: pointer;
787
+ }
788
+
789
+ .claude-proxy-option input {
790
+ width: 15px;
791
+ height: 15px;
792
+ margin-top: 2px;
793
+ accent-color: var(--accent);
794
+ flex: 0 0 auto;
795
+ }
796
+
797
+ .claude-proxy-option.is-disabled {
798
+ cursor: not-allowed;
799
+ opacity: 0.62;
800
+ }
801
+
802
+ .claude-proxy-label {
803
+ display: inline-flex;
804
+ align-items: center;
805
+ gap: 7px;
806
+ font-size: 12px;
807
+ font-weight: 600;
808
+ line-height: 1.45;
809
+ flex-wrap: wrap;
810
+ }
811
+
812
+ .claude-proxy-unavailable {
813
+ flex-basis: 100%;
814
+ color: var(--text-3);
815
+ font-size: 11px;
816
+ font-weight: 500;
817
+ }
818
+
819
+ .claude-proxy-tooltip {
820
+ position: relative;
821
+ display: inline-flex;
822
+ align-items: center;
823
+ justify-content: center;
824
+ width: 17px;
825
+ height: 17px;
826
+ border: 1px solid var(--border-bright);
827
+ border-radius: 999px;
828
+ color: var(--text-3);
829
+ font-size: 11px;
830
+ font-weight: 700;
831
+ cursor: help;
832
+ outline: none;
833
+ }
834
+
835
+ .claude-proxy-tooltip::after {
836
+ content: attr(data-tooltip);
837
+ position: absolute;
838
+ z-index: 90;
839
+ right: -4px;
840
+ bottom: calc(100% + 9px);
841
+ width: min(310px, 72vw);
842
+ padding: 9px 11px;
843
+ border: 1px solid var(--border-bright);
844
+ border-radius: var(--radius-sm);
845
+ background: var(--surface-raised);
846
+ box-shadow: 0 12px 28px oklch(0% 0 0 / 0.26);
847
+ color: var(--text-1);
848
+ font-size: 12px;
849
+ font-weight: 500;
850
+ line-height: 1.45;
851
+ text-align: left;
852
+ transform: translateY(4px);
853
+ opacity: 0;
854
+ visibility: hidden;
855
+ pointer-events: none;
856
+ transition: opacity var(--dur-sm) var(--ease), transform var(--dur-sm) var(--ease);
857
+ }
858
+
859
+ .claude-proxy-tooltip:hover::after,
860
+ .claude-proxy-tooltip:focus-visible::after {
861
+ opacity: 1;
862
+ visibility: visible;
863
+ transform: translateY(0);
864
+ }
865
+
771
866
  .launch-model-input-wrap {
772
867
  position: relative;
773
868
  }
@@ -928,6 +1023,21 @@ input { font-family: inherit; }
928
1023
  border-bottom-right-radius: calc(var(--radius) - 1px);
929
1024
  }
930
1025
 
1026
+ .provider-config-toggle {
1027
+ display: grid;
1028
+ place-items: center;
1029
+ width: 34px;
1030
+ height: 34px;
1031
+ border-radius: var(--radius-sm);
1032
+ color: var(--text-3);
1033
+ }
1034
+
1035
+ .provider-config-toggle:hover,
1036
+ .provider-config-toggle:focus-visible {
1037
+ background: var(--accent-muted);
1038
+ color: var(--accent);
1039
+ }
1040
+
931
1041
  .provider-card-header:hover { background: var(--surface-hover); }
932
1042
  .provider-card.open .provider-card-header {
933
1043
  background: var(--surface-2);
@@ -1039,6 +1149,99 @@ input { font-family: inherit; }
1039
1149
  color: var(--accent);
1040
1150
  }
1041
1151
 
1152
+ /* Provider model browser */
1153
+ .provider-model-browser { display: none; max-width: 960px; }
1154
+ .content.provider-browser-open > .section { display: none; }
1155
+ .content.provider-browser-open > .provider-model-browser { display: block; margin: 0; border: 0; }
1156
+
1157
+ .provider-browser-back {
1158
+ display: inline-flex;
1159
+ margin-bottom: 26px;
1160
+ color: var(--text-2);
1161
+ font-size: 14px;
1162
+ font-weight: 600;
1163
+ }
1164
+
1165
+ .provider-browser-back:hover { color: var(--accent); }
1166
+
1167
+ .provider-browser-hero {
1168
+ display: flex;
1169
+ align-items: center;
1170
+ gap: 16px;
1171
+ margin-bottom: 28px;
1172
+ }
1173
+
1174
+ .provider-browser-hero .section-eyebrow { margin-bottom: 3px; }
1175
+ .provider-browser-hero .section-heading { margin-bottom: 4px; }
1176
+
1177
+ .provider-browser-tools {
1178
+ display: grid;
1179
+ grid-template-columns: minmax(0, 1fr) auto;
1180
+ gap: 10px;
1181
+ margin-bottom: 14px;
1182
+ }
1183
+
1184
+ .provider-model-table-wrap {
1185
+ overflow-x: auto;
1186
+ border: 1px solid var(--border);
1187
+ border-radius: var(--radius);
1188
+ background: var(--surface);
1189
+ }
1190
+
1191
+ .provider-model-table {
1192
+ width: 100%;
1193
+ border-collapse: collapse;
1194
+ font-size: 14px;
1195
+ }
1196
+
1197
+ .provider-model-table th {
1198
+ padding: 11px 16px;
1199
+ text-align: left;
1200
+ color: var(--text-3);
1201
+ background: var(--surface-2);
1202
+ border-bottom: 1px solid var(--border);
1203
+ font-size: 12px;
1204
+ font-weight: 700;
1205
+ letter-spacing: 0.05em;
1206
+ text-transform: uppercase;
1207
+ }
1208
+
1209
+ .provider-model-table th:nth-child(2) { width: 120px; }
1210
+ .provider-model-table th:nth-child(3) { width: 170px; }
1211
+
1212
+ .provider-model-table td {
1213
+ padding: 12px 16px;
1214
+ color: var(--text-2);
1215
+ border-bottom: 1px solid var(--border);
1216
+ vertical-align: middle;
1217
+ }
1218
+
1219
+ .provider-model-table tr:last-child td { border-bottom: 0; }
1220
+ .provider-model-table tbody tr:hover { background: var(--surface-hover); }
1221
+ .provider-model-table strong { display: block; color: var(--text-1); font-size: 14px; }
1222
+ .provider-model-table code { display: block; margin-top: 3px; color: var(--text-3); font-size: 12px; }
1223
+ .provider-model-table .provider-model-empty { padding: 34px 16px; text-align: center; color: var(--text-3); }
1224
+
1225
+ .provider-model-pagination {
1226
+ display: flex;
1227
+ align-items: center;
1228
+ justify-content: space-between;
1229
+ gap: 16px;
1230
+ padding-top: 14px;
1231
+ color: var(--text-3);
1232
+ font-size: 13px;
1233
+ }
1234
+
1235
+ .provider-model-pagination > div { display: flex; align-items: center; gap: 10px; }
1236
+ .provider-browser-empty { padding-top: 52px; color: var(--text-2); }
1237
+
1238
+ @media (max-width: 760px) {
1239
+ .provider-browser-tools { grid-template-columns: 1fr; }
1240
+ .provider-model-pagination { align-items: flex-start; flex-direction: column; }
1241
+ .provider-model-table th:nth-child(2) { width: 90px; }
1242
+ .provider-model-table th:nth-child(3) { width: 140px; }
1243
+ }
1244
+
1042
1245
  /* Provider body expand */
1043
1246
  .provider-body {
1044
1247
  display: grid;