|
12 | 12 | }; |
13 | 13 | }; |
14 | 14 |
|
| 15 | + type EndpointMetadata = { |
| 16 | + display_name: string; |
| 17 | + leftnav_label: string; |
| 18 | + url: string; |
| 19 | + docs_label: string; |
| 20 | + bridges_to_chat_completion?: boolean; |
| 21 | + }; |
| 22 | +
|
15 | 23 | let loading = true; |
16 | 24 | let providers: ProviderEndpoint[] = []; |
| 25 | + let endpointsMetadata: { [key: string]: EndpointMetadata } = {}; |
17 | 26 | let searchQuery = ""; |
18 | 27 | let selectedEndpoint = ""; |
19 | 28 | let allEndpoints: string[] = []; |
|
28 | 37 | const response = await fetch(PROVIDERS_URL); |
29 | 38 | const data = await response.json(); |
30 | 39 | |
| 40 | + // Extract endpoints metadata |
| 41 | + if (data.endpoints) { |
| 42 | + endpointsMetadata = data.endpoints; |
| 43 | + } |
| 44 | + |
31 | 45 | // Transform the data into our format |
32 | 46 | if (data.providers) { |
33 | 47 | providers = Object.entries(data.providers).map(([provider, info]: [string, any]) => ({ |
|
83 | 97 | }, 1000); |
84 | 98 |
|
85 | 99 | function formatEndpointName(endpoint: string): string { |
86 | | - // Convert snake_case to /path format |
| 100 | + // Use leftnav_label from metadata if available, otherwise convert snake_case to /path format |
| 101 | + if (endpointsMetadata[endpoint]?.leftnav_label) { |
| 102 | + return endpointsMetadata[endpoint].leftnav_label; |
| 103 | + } |
87 | 104 | const formatted = endpoint.replace(/_/g, "/"); |
88 | 105 | return `/${formatted}`; |
89 | 106 | } |
90 | 107 |
|
91 | 108 | function formatEndpointTitle(endpoint: string): string { |
92 | | - // Convert snake_case to Title Case |
| 109 | + // Use display_name from metadata if available, otherwise convert snake_case to Title Case |
| 110 | + if (endpointsMetadata[endpoint]?.display_name) { |
| 111 | + return endpointsMetadata[endpoint].display_name; |
| 112 | + } |
93 | 113 | return endpoint |
94 | 114 | .split('_') |
95 | 115 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) |
96 | 116 | .join(' '); |
97 | 117 | } |
98 | 118 |
|
99 | | - // Filter endpoints based on search |
| 119 | + function getEndpointUrl(endpoint: string): string { |
| 120 | + return endpointsMetadata[endpoint]?.url || DOCS_URL; |
| 121 | + } |
| 122 | +
|
| 123 | + // Filter endpoints based on search query - show only endpoints supported by matching providers |
100 | 124 | $: { |
101 | 125 | if (searchQuery.trim() === "") { |
102 | 126 | filteredEndpoints = allEndpoints; |
103 | 127 | } else { |
104 | 128 | const query = searchQuery.toLowerCase(); |
105 | 129 | |
106 | | - // Filter endpoints by name |
107 | | - const matchingEndpoints = allEndpoints.filter((endpoint) => |
108 | | - endpoint.toLowerCase().includes(query) || |
109 | | - formatEndpointName(endpoint).toLowerCase().includes(query) || |
110 | | - formatEndpointTitle(endpoint).toLowerCase().includes(query) |
| 130 | + // Find providers that match the search query |
| 131 | + const matchingProviders = providers.filter(p => |
| 132 | + p.provider.toLowerCase().includes(query) || |
| 133 | + p.display_name.toLowerCase().includes(query) |
111 | 134 | ); |
112 | 135 | |
113 | | - // Also include endpoints if any provider name matches |
114 | | - const endpointsWithMatchingProviders = new Set<string>(); |
115 | | - providers.forEach(p => { |
116 | | - const providerMatches = |
117 | | - p.provider.toLowerCase().includes(query) || |
118 | | - p.display_name.toLowerCase().includes(query); |
119 | | - |
120 | | - if (providerMatches) { |
121 | | - Object.keys(p.endpoints).forEach(endpoint => { |
122 | | - if (p.endpoints[endpoint] === true) { |
123 | | - endpointsWithMatchingProviders.add(endpoint); |
124 | | - } |
125 | | - }); |
126 | | - } |
| 136 | + // Collect all endpoints supported by matching providers |
| 137 | + const supportedEndpoints = new Set<string>(); |
| 138 | + matchingProviders.forEach(p => { |
| 139 | + Object.keys(p.endpoints).forEach(endpoint => { |
| 140 | + if (p.endpoints[endpoint] === true) { |
| 141 | + supportedEndpoints.add(endpoint); |
| 142 | + } |
| 143 | + }); |
127 | 144 | }); |
128 | 145 | |
129 | | - // Combine both sets of endpoints |
130 | | - const combinedEndpoints = new Set([...matchingEndpoints, ...endpointsWithMatchingProviders]); |
131 | | - filteredEndpoints = allEndpoints.filter(e => combinedEndpoints.has(e)); |
| 146 | + // Filter to only show endpoints that are supported by matching providers |
| 147 | + filteredEndpoints = allEndpoints.filter(e => supportedEndpoints.has(e)); |
132 | 148 | } |
133 | 149 | } |
134 | 150 |
|
135 | 151 | // Filter providers that support the selected endpoint AND match search query |
136 | 152 | $: { |
137 | 153 | if (selectedEndpoint) { |
138 | | - let providersList = providers.filter(p => p.endpoints[selectedEndpoint] === true); |
| 154 | + // Check if this endpoint bridges to chat_completions |
| 155 | + const bridgesToChatCompletion = endpointsMetadata[selectedEndpoint]?.bridges_to_chat_completion || false; |
| 156 | + |
| 157 | + let providersList = providers.filter(p => { |
| 158 | + // Include if provider directly supports this endpoint |
| 159 | + const directSupport = p.endpoints[selectedEndpoint] === true; |
| 160 | + |
| 161 | + // If endpoint bridges to chat_completions, also include providers that support chat_completions |
| 162 | + const bridgedSupport = bridgesToChatCompletion && p.endpoints['chat_completions'] === true; |
| 163 | + |
| 164 | + return directSupport || bridgedSupport; |
| 165 | + }); |
139 | 166 | |
140 | | - // Further filter by search query if present |
| 167 | + // Filter by search query - only providers |
141 | 168 | if (searchQuery.trim() !== "") { |
142 | 169 | const query = searchQuery.toLowerCase(); |
143 | 170 | providersList = providersList.filter(p => |
|
155 | 182 |
|
156 | 183 | // Count how many providers support each endpoint |
157 | 184 | function getProviderCount(endpoint: string): number { |
158 | | - return providers.filter(p => p.endpoints[endpoint] === true).length; |
| 185 | + const bridgesToChatCompletion = endpointsMetadata[endpoint]?.bridges_to_chat_completion || false; |
| 186 | + |
| 187 | + return providers.filter(p => { |
| 188 | + const directSupport = p.endpoints[endpoint] === true; |
| 189 | + const bridgedSupport = bridgesToChatCompletion && p.endpoints['chat_completions'] === true; |
| 190 | + return directSupport || bridgedSupport; |
| 191 | + }).length; |
159 | 192 | } |
160 | 193 |
|
161 | 194 | </script> |
|
179 | 212 | bind:value={searchQuery} |
180 | 213 | type="text" |
181 | 214 | autocomplete="off" |
182 | | - placeholder="Search endpoints or providers..." |
| 215 | + placeholder="Search providers..." |
183 | 216 | class="search-input" |
184 | 217 | /> |
185 | 218 | </div> |
|
215 | 248 | <section class="content"> |
216 | 249 | {#if selectedEndpoint} |
217 | 250 | <div class="content-header"> |
218 | | - <h2 class="endpoint-title">{formatEndpointTitle(selectedEndpoint)}</h2> |
| 251 | + <h2 class="endpoint-title"> |
| 252 | + {formatEndpointTitle(selectedEndpoint)} |
| 253 | + <a href={getEndpointUrl(selectedEndpoint)} target="_blank" rel="noopener noreferrer" class="endpoint-docs-link" title="View documentation"> |
| 254 | + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 255 | + <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path> |
| 256 | + <polyline points="15 3 21 3 21 9"></polyline> |
| 257 | + <line x1="10" y1="14" x2="21" y2="3"></line> |
| 258 | + </svg> |
| 259 | + </a> |
| 260 | + </h2> |
219 | 261 | <p class="endpoint-subtitle"> |
220 | 262 | {filteredProviders.length} {filteredProviders.length === 1 ? 'provider' : 'providers'} support {formatEndpointName(selectedEndpoint)} |
221 | 263 | </p> |
|
492 | 534 | font-weight: 700; |
493 | 535 | color: var(--text-color); |
494 | 536 | margin: 0 0 0.5rem 0; |
| 537 | + display: flex; |
| 538 | + align-items: center; |
| 539 | + gap: 0.75rem; |
| 540 | + } |
| 541 | +
|
| 542 | + .endpoint-docs-link { |
| 543 | + display: inline-flex; |
| 544 | + align-items: center; |
| 545 | + justify-content: center; |
| 546 | + color: var(--link-color); |
| 547 | + text-decoration: none; |
| 548 | + transition: all 0.2s ease; |
| 549 | + opacity: 0.7; |
| 550 | + } |
| 551 | +
|
| 552 | + .endpoint-docs-link:hover { |
| 553 | + opacity: 1; |
| 554 | + color: var(--link-hover); |
| 555 | + transform: translateY(-1px); |
| 556 | + } |
| 557 | +
|
| 558 | + .endpoint-docs-link svg { |
| 559 | + width: 24px; |
| 560 | + height: 24px; |
495 | 561 | } |
496 | 562 |
|
497 | 563 | .endpoint-subtitle { |
|
0 commit comments