@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.
- package/README.md +22 -1
- package/dist/{chunk-UTPRIJFE.js → chunk-XQCHSU6L.js} +122 -9
- package/dist/chunk-XQCHSU6L.js.map +1 -0
- package/dist/cli.js +853 -88
- package/dist/cli.js.map +1 -1
- package/dist/ui/public/app.js +188 -5
- package/dist/ui/public/index.html +10 -2
- package/dist/ui/public/provider-model-browser.js +29 -0
- package/dist/ui/public/style.css +203 -0
- package/dist/{ui-command-A2WDJOSZ.js → ui-command-W2W6P6VJ.js} +40 -6
- package/dist/ui-command-W2W6P6VJ.js.map +1 -0
- package/package.json +3 -1
- package/dist/chunk-UTPRIJFE.js.map +0 -1
- package/dist/ui-command-A2WDJOSZ.js.map +0 -1
package/dist/ui/public/app.js
CHANGED
|
@@ -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-
|
|
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
|
-
<
|
|
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
|
-
|
|
485
|
+
configToggle.setAttribute('aria-expanded', String(isOpen));
|
|
473
486
|
body.setAttribute('aria-hidden', String(!isOpen));
|
|
474
487
|
}
|
|
475
488
|
|
|
476
|
-
header.
|
|
477
|
-
|
|
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 & 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
|
-
<
|
|
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
|
-
</
|
|
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
|
+
}
|
package/dist/ui/public/style.css
CHANGED
|
@@ -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;
|