From e813d6455c784320c82ad279c53a6b2de34ca9df Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 7 Jan 2026 15:24:49 +0530 Subject: [PATCH] feat(providers.svelte): use a grid where y-axis is endpoint, and x-axis is providers follow zapier style of discoverability for integrations --- src/Providers.svelte | 889 +++++++++++++++++-------------------------- 1 file changed, 343 insertions(+), 546 deletions(-) diff --git a/src/Providers.svelte b/src/Providers.svelte index b78337f..a6024f3 100644 --- a/src/Providers.svelte +++ b/src/Providers.svelte @@ -14,14 +14,11 @@ let loading = true; let providers: ProviderEndpoint[] = []; - let filteredProviders: ProviderEndpoint[] = []; - let filteredEndpoints: string[] = []; let searchQuery = ""; - let viewMode: "provider" | "endpoint" = "provider"; - let selectedFilter = ""; - let endpointColumns: string[] = []; - let viewModeOpen = false; - let filterOpen = false; + let selectedEndpoint = ""; + let allEndpoints: string[] = []; + let filteredEndpoints: string[] = []; + let filteredProviders: ProviderEndpoint[] = []; const PROVIDERS_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/provider_endpoints_support.json"; const DOCS_URL = "https://docs.litellm.ai/docs/providers"; @@ -31,7 +28,7 @@ const response = await fetch(PROVIDERS_URL); const data = await response.json(); - // Transform the data into our format - data.providers is the actual providers object + // Transform the data into our format if (data.providers) { providers = Object.entries(data.providers).map(([provider, info]: [string, any]) => ({ provider, @@ -41,29 +38,32 @@ })); } - filteredProviders = providers; + // Extract all unique endpoints + const endpointsSet = new Set(); + providers.forEach(p => { + Object.keys(p.endpoints).forEach(e => endpointsSet.add(e)); + }); + + // Sort endpoints with priority + const priorityOrder = ['chat_completions', 'responses', 'messages']; + const sorted = Array.from(endpointsSet).sort(); + allEndpoints = [ + ...priorityOrder.filter(e => sorted.includes(e)), + ...sorted.filter(e => !priorityOrder.includes(e)) + ]; + + filteredEndpoints = allEndpoints; + + // Select first endpoint by default + if (allEndpoints.length > 0) { + selectedEndpoint = allEndpoints[0]; + } + loading = false; } catch (error) { console.error("Failed to load providers:", error); loading = false; } - - // Close dropdowns when clicking outside - const handleClickOutside = (event: Event) => { - const target = event.target as HTMLElement; - const dropdown = target.closest('.custom-dropdown'); - if (!dropdown) { - viewModeOpen = false; - filterOpen = false; - } - }; - - // Use mousedown instead of click for better responsiveness - document.addEventListener('mousedown', handleClickOutside); - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; }); // Debounce function @@ -76,79 +76,86 @@ }; // Debounced search tracking - const trackSearchDebounced = debounce((query: string, mode: string, resultsCount: number) => { + const trackSearchDebounced = debounce((query: string, resultsCount: number) => { if (query) { - trackSearch(query, mode, resultsCount); + trackSearch(query, "endpoint", resultsCount); } }, 1000); - // Reset selected filter when view mode changes - $: if (viewMode) { - selectedFilter = ""; + function formatEndpointName(endpoint: string): string { + // Convert snake_case to /path format + const formatted = endpoint.replace(/_/g, "/"); + return `/${formatted}`; } - $: { - // Apply filters - ensure we start with full data when switching views - let tempProviders = providers; - let tempEndpoints = endpointColumns.length > 0 ? [...endpointColumns] : []; + function formatEndpointTitle(endpoint: string): string { + // Convert snake_case to Title Case + return endpoint + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } - // Apply search query - if (searchQuery.trim() !== "") { + // Filter endpoints based on search + $: { + if (searchQuery.trim() === "") { + filteredEndpoints = allEndpoints; + } else { const query = searchQuery.toLowerCase(); - if (viewMode === "provider") { - tempProviders = tempProviders.filter((p) => + + // Filter endpoints by name + const matchingEndpoints = allEndpoints.filter((endpoint) => + endpoint.toLowerCase().includes(query) || + formatEndpointName(endpoint).toLowerCase().includes(query) || + formatEndpointTitle(endpoint).toLowerCase().includes(query) + ); + + // Also include endpoints if any provider name matches + const endpointsWithMatchingProviders = new Set(); + providers.forEach(p => { + const providerMatches = p.provider.toLowerCase().includes(query) || - p.display_name.toLowerCase().includes(query) - ); - } else { - tempEndpoints = tempEndpoints.filter((endpoint) => - endpoint.toLowerCase().includes(query) || - formatEndpointName(endpoint).toLowerCase().includes(query) - ); - } + p.display_name.toLowerCase().includes(query); + + if (providerMatches) { + Object.keys(p.endpoints).forEach(endpoint => { + if (p.endpoints[endpoint] === true) { + endpointsWithMatchingProviders.add(endpoint); + } + }); + } + }); + + // Combine both sets of endpoints + const combinedEndpoints = new Set([...matchingEndpoints, ...endpointsWithMatchingProviders]); + filteredEndpoints = allEndpoints.filter(e => combinedEndpoints.has(e)); } + } - // Apply dropdown filter - if (selectedFilter) { - if (viewMode === "provider") { - tempProviders = tempProviders.filter((p) => p.provider === selectedFilter); - } else { - tempEndpoints = tempEndpoints.filter((e) => e === selectedFilter); + // Filter providers that support the selected endpoint AND match search query + $: { + if (selectedEndpoint) { + let providersList = providers.filter(p => p.endpoints[selectedEndpoint] === true); + + // Further filter by search query if present + if (searchQuery.trim() !== "") { + const query = searchQuery.toLowerCase(); + providersList = providersList.filter(p => + p.provider.toLowerCase().includes(query) || + p.display_name.toLowerCase().includes(query) + ); } + + filteredProviders = providersList; + trackSearchDebounced(searchQuery, filteredProviders.length); + } else { + filteredProviders = []; } - - filteredProviders = tempProviders; - filteredEndpoints = tempEndpoints; - - // Track search event (debounced) - const resultsCount = viewMode === "provider" ? tempProviders.length : tempEndpoints.length; - trackSearchDebounced(searchQuery, viewMode, resultsCount); } - function formatEndpointName(endpoint: string): string { - // Convert snake_case to /path format - const formatted = endpoint.replace(/_/g, "/"); - return `/${formatted}`; - } - - $: { - // Recalculate endpoint columns whenever providers change - const allEndpoints = new Set(); - - if (providers.length > 0) { - providers.forEach(p => { - Object.keys(p.endpoints).forEach(e => allEndpoints.add(e)); - }); - } - - // Custom sort order: chat_completions, responses, messages first, then alphabetical - const priorityOrder = ['chat_completions', 'responses', 'messages']; - const sorted = Array.from(allEndpoints).sort(); - - endpointColumns = [ - ...priorityOrder.filter(e => sorted.includes(e)), - ...sorted.filter(e => !priorityOrder.includes(e)) - ]; + // Count how many providers support each endpoint + function getProviderCount(endpoint: string): number { + return providers.filter(p => p.endpoints[endpoint] === true).length; } @@ -156,93 +163,25 @@
-

Supported Endpoints & Providers (Docs)

+

AI Provider Integrations (Docs)

Browse all AI providers and their supported endpoints through LiteLLM AI Gateway

- +
-
-
- - - - - -
- - -
- - {#if viewModeOpen} - - {/if} -
- - -
- - {#if filterOpen} - - {/if} -
+
+ + + + +
@@ -251,120 +190,78 @@ Loading providers...
{:else} -
- - {#if viewMode === "provider"} - - - - - {#each endpointColumns as endpoint} - - {/each} - - - - {#each filteredProviders as { provider, display_name, url, endpoints } (provider)} - - - {#each endpointColumns as endpoint} - - {/each} - - {/each} - - {:else} - - - - - {#each filteredProviders as { provider, display_name } (provider)} - - {/each} - - - - {#each filteredEndpoints as endpoint} - - - {#each filteredProviders as { endpoints } } - - {/each} - +

{display_name.replace(/\s*\(.*?\)\s*$/, '')}

+ +

+ {display_name.replace(/\s*\(.*?\)\s*$/, '')} supports {formatEndpointName(selectedEndpoint)} endpoint through LiteLLM AI Gateway. +

+
+ {provider} +
+ {/each} - + + {:else} +
+

Select an endpoint to view available providers

+
{/if} -
Provider{formatEndpointName(endpoint)}
-
-
- {#if getProviderLogo(provider)} - {provider} { - e.currentTarget.style.display = 'none'; - e.currentTarget.nextElementSibling.style.display = 'flex'; - }} - /> - - {:else} -
- {getProviderInitial(provider)} -
- {/if} -
- -
-
- {#if endpoints[endpoint] === true} -
+
+ + + + +
+ {#if selectedEndpoint} +
+

{formatEndpointTitle(selectedEndpoint)}

+

+ {filteredProviders.length} {filteredProviders.length === 1 ? 'provider' : 'providers'} support {formatEndpointName(selectedEndpoint)} +

+
+ +
Endpoint -
-
- {#if getProviderLogo(provider)} - {provider} { - e.currentTarget.style.display = 'none'; - e.currentTarget.nextElementSibling.style.display = 'flex'; - }} - /> - - {:else} -
- {getProviderInitial(provider)} -
- {/if} -
-
- {display_name.replace(/\s*\(.*?\)\s*$/, '')} -
-
({provider})
-
- {formatEndpointName(endpoint)} - - {#if endpoints[endpoint] === true} -
- {:else} -
- {/if} -
+
{/if}
@@ -383,7 +280,7 @@ /* Hero Section */ .hero { - padding: 4rem 2rem 3rem; + padding: 3rem 2rem 2rem; max-width: 1400px; margin: 0 auto; background-color: var(--bg-color); @@ -391,14 +288,14 @@ } .hero-content { - text-align: left; + text-align: center; } .hero-title { - font-size: 3rem; + font-size: 2.5rem; font-weight: 700; color: var(--text-color); - margin-bottom: 1rem; + margin-bottom: 0.75rem; letter-spacing: -0.02em; line-height: 1.1; } @@ -406,7 +303,7 @@ .docs-link { color: var(--link-color); text-decoration: none; - font-size: 2rem; + font-size: 1.75rem; } .docs-link:hover { @@ -415,116 +312,18 @@ } .hero-subtitle { - font-size: 1.25rem; + font-size: 1.125rem; color: var(--muted-color); line-height: 1.6; } /* Search Section */ .search-section { - max-width: 1400px; - margin: 4rem auto 2rem; + max-width: 900px; + margin: 2rem auto; padding: 0 2rem; } - .filters-row { - display: grid; - grid-template-columns: 1fr auto auto; - gap: 1rem; - width: 100%; - } - - .custom-dropdown { - position: relative; - min-width: 220px; - } - - .dropdown-trigger { - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; - padding: 0.875rem 1rem; - font-size: 1rem; - border: 1px solid var(--border-color-strong); - border-radius: 8px; - background-color: var(--bg-color); - cursor: pointer; - transition: all 0.2s ease; - font-family: inherit; - color: var(--text-color); - height: 48px; - box-sizing: border-box; - } - - .dropdown-trigger:hover { - border-color: var(--muted-color); - } - - .dropdown-trigger:focus { - outline: none; - border-color: #6366f1; - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); - } - - .dropdown-trigger span { - flex: 1; - text-align: left; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-weight: 500; - } - - .dropdown-trigger svg { - flex-shrink: 0; - color: var(--muted-color); - } - - .dropdown-menu { - position: absolute; - top: calc(100% + 0.5rem); - left: 0; - right: 0; - background-color: var(--card-bg); - border: 1px solid var(--border-color-strong); - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - z-index: 9999; - overflow: hidden; - } - - .dropdown-menu.scrollable { - max-height: 300px; - overflow-y: auto; - } - - .dropdown-option { - width: 100%; - padding: 0.75rem 1rem; - text-align: left; - background: none; - border: none; - cursor: pointer; - font-size: 0.9375rem; - color: var(--text-color); - transition: background-color 0.15s ease; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .dropdown-option:hover { - background-color: var(--hover-bg); - } - - .dropdown-option.selected { - background-color: var(--bg-tertiary); - color: var(--link-color); - font-weight: 500; - } - .search-input-wrapper { position: relative; width: 100%; @@ -575,201 +374,174 @@ color: var(--muted-color); } - /* Table */ - .table-container { - margin: 2rem auto 4rem; + /* Main Container with Sidebar */ + .container { + display: flex; max-width: 1400px; + margin: 2rem auto; padding: 0 2rem; - overflow-x: auto; + gap: 2rem; + align-items: flex-start; } - table { - width: 100%; - border-collapse: collapse; + /* Left Sidebar */ + .sidebar { + width: 340px; + flex-shrink: 0; background: var(--card-bg); - border-radius: 12px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); border: 1px solid var(--border-color); - } - - thead { + border-radius: 12px; + overflow: hidden; position: sticky; - top: 0; - z-index: 5; - background-color: var(--card-bg); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); + top: 2rem; + max-height: calc(100vh - 4rem); + display: flex; + flex-direction: column; } - thead::before { - content: ''; - position: absolute; - top: -1px; - left: 0; - right: 0; - height: 1px; - background-color: var(--border-color); + .sidebar-header { + padding: 1.25rem 1.5rem; + border-bottom: 1px solid var(--border-color); + background: var(--card-bg); } - th { - padding: 1rem 1.5rem; - text-align: left; + .sidebar-header h2 { + margin: 0; + font-size: 0.875rem; font-weight: 600; - font-size: 0.75rem; + text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted-color); - background-color: var(--card-bg); - border-bottom: 1px solid var(--border-color); } - th:first-child { - text-transform: uppercase; - } - - th:not(:first-child) { - font-family: monospace; - font-size: 0.8125rem; - font-weight: 400; - text-align: center; - } - - tbody { - background-color: var(--card-bg); + .endpoints-list { + overflow-y: auto; + flex: 1; } - tbody tr { + .endpoint-item { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.875rem 1.5rem; + background: none; + border: none; border-bottom: 1px solid var(--border-color); - transition: background-color 0.15s ease; - background-color: var(--card-bg); - } - - tbody tr:hover { - background-color: var(--hover-bg); + cursor: pointer; + transition: all 0.15s ease; + text-align: left; + color: var(--text-color); } - tbody tr:last-child { + .endpoint-item:last-child { border-bottom: none; } - td { - padding: 0.875rem 1.5rem; - vertical-align: middle; - font-size: 0.9375rem; - color: var(--text-color); - background-color: var(--card-bg); + .endpoint-item:hover { + background-color: var(--hover-bg); } - .provider-cell { + .endpoint-item.active { + background-color: var(--bg-tertiary); + color: var(--litellm-primary); font-weight: 500; - min-width: 250px; - color: var(--text-color); - } - - .provider-info { - display: flex; - align-items: center; - gap: 0.75rem; + border-left: 3px solid var(--litellm-primary); + padding-left: calc(1.5rem - 3px); } - .provider-avatar { - width: 32px; - height: 32px; - border-radius: 50%; - background-color: var(--bg-color); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; + .endpoint-label { + font-family: monospace; + font-size: 0.875rem; + flex: 1; overflow: hidden; - position: relative; - padding: 4px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + text-overflow: ellipsis; + white-space: nowrap; } - .provider-logo-img { - width: 100%; - height: 100%; - object-fit: contain; + .provider-count { + font-size: 0.75rem; + background: var(--bg-color); + color: var(--muted-color); + padding: 0.25rem 0.5rem; + border-radius: 12px; + font-weight: 500; + margin-left: 0.5rem; + min-width: 32px; + text-align: center; + flex-shrink: 0; } - .provider-initial { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - background-color: var(--text-color); - color: var(--bg-color); - font-weight: 600; - font-size: 0.75rem; - border-radius: 50%; - margin: -4px; + .endpoint-item.active .provider-count { + background: rgba(99, 102, 241, 0.1); + color: var(--litellm-primary); } - .provider-details { - display: flex; - flex-direction: column; - gap: 0.125rem; + /* Main Content Area */ + .content { + flex: 1; + min-width: 0; } - .provider-link { - font-weight: 500; - color: var(--contrast); - text-decoration: none; - transition: color 0.2s ease; + .content-header { + margin-bottom: 2rem; } - .provider-link:hover { - color: #2563eb; + .endpoint-title { + font-size: 2rem; + font-weight: 700; + color: var(--text-color); + margin: 0 0 0.5rem 0; } - .provider-key { - font-size: 0.8125rem; + .endpoint-subtitle { + font-size: 1rem; color: var(--muted-color); - font-family: monospace; + margin: 0; } - .endpoint-cell { + .empty-state { text-align: center; - vertical-align: middle; - padding: 0.875rem 0.5rem; + padding: 4rem 2rem; + color: var(--muted-color); } - .status-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 50%; - font-size: 16px; - font-weight: bold; + /* Provider Cards Grid */ + .providers-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; } - .status-icon.supported { - background-color: #22c55e; - color: white; + .provider-card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 1.5rem; + text-decoration: none; + color: var(--text-color); + transition: all 0.2s ease; + display: flex; + flex-direction: column; + gap: 1rem; } - .status-icon.unsupported { - background-color: transparent; - color: var(--border-color-strong); - border: 2px solid var(--border-color-strong); - font-size: 20px; + .provider-card:hover { + border-color: var(--litellm-primary); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15); + transform: translateY(-2px); } - /* Endpoint View Styles */ - .provider-header { + .provider-card-header { display: flex; - flex-direction: column; align-items: center; - gap: 0.375rem; - min-width: 120px; + gap: 1rem; } - .provider-avatar-small { - width: 24px; - height: 24px; - border-radius: 50%; + .provider-avatar { + width: 48px; + height: 48px; + border-radius: 12px; background-color: var(--bg-color); display: flex; align-items: center; @@ -777,17 +549,17 @@ flex-shrink: 0; overflow: hidden; position: relative; - padding: 3px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + padding: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } - .provider-logo-img-small { + .provider-logo-img { width: 100%; height: 100%; object-fit: contain; } - .provider-initial-small { + .provider-initial { width: 100%; height: 100%; display: flex; @@ -796,36 +568,58 @@ background-color: var(--text-color); color: var(--bg-color); font-weight: 600; - font-size: 0.625rem; - border-radius: 50%; - margin: -3px; + font-size: 1rem; + border-radius: 8px; + margin: -8px; } - .provider-name-small { - font-size: 0.75rem; - font-weight: 500; - color: var(--contrast); - text-align: center; - line-height: 1.2; + .provider-name { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-color); + margin: 0; + line-height: 1.3; } - .provider-key-small { - font-size: 0.625rem; + .provider-description { + font-size: 0.9375rem; color: var(--muted-color); - font-family: monospace; + line-height: 1.5; + margin: 0; + flex: 1; } - .endpoint-name-cell { - font-weight: 500; - min-width: 200px; + .provider-meta { + display: flex; + align-items: center; + gap: 0.5rem; + padding-top: 0.75rem; + border-top: 1px solid var(--border-color); } - .endpoint-name { + .provider-key { + font-size: 0.8125rem; + color: var(--muted-color); font-family: monospace; - font-size: 0.875rem; } /* Responsive Design */ + @media (max-width: 1024px) { + .container { + flex-direction: column; + } + + .sidebar { + width: 100%; + position: static; + max-height: 400px; + } + + .providers-grid { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + } + } + @media (max-width: 768px) { .hero-title { font-size: 2rem; @@ -835,17 +629,20 @@ font-size: 1rem; } - .view-section { - flex-direction: column; - align-items: stretch; + .docs-link { + font-size: 1.5rem; } - .search-input-wrapper { - max-width: 100%; + .container { + padding: 0 1rem; } - .table-container { - padding: 0 1rem; + .providers-grid { + grid-template-columns: 1fr; + } + + .endpoint-title { + font-size: 1.5rem; } }