( source: CustomRegistrySource, fetchImpl: typeof fetch = fetch, signal?: AbortSignal, )
| 180 | * `entry.id`); callers should iterate `Object.values` to apply each entry. |
| 181 | */ |
| 182 | export async function fetchCustomRegistry( |
| 183 | source: CustomRegistrySource, |
| 184 | fetchImpl: typeof fetch = fetch, |
| 185 | signal?: AbortSignal, |
| 186 | ): Promise<Record<string, CustomRegistryProviderEntry>> { |
| 187 | const headers: Record<string, string> = { |
| 188 | Accept: 'application/json', |
| 189 | }; |
| 190 | if (source.apiKey.length > 0) { |
| 191 | headers['Authorization'] = `Bearer ${source.apiKey}`; |
| 192 | } |
| 193 | |
| 194 | const init: RequestInit = { headers }; |
| 195 | if (signal !== undefined) init.signal = signal; |
| 196 | |
| 197 | const response = await fetchImpl(source.url, init); |
| 198 | if (!response.ok) { |
| 199 | const message = await readApiErrorMessage( |
| 200 | response, |
| 201 | `Failed to fetch custom registry at ${source.url} (HTTP ${response.status}).`, |
| 202 | ); |
| 203 | throw new CustomRegistryApiError(message, response.status); |
| 204 | } |
| 205 | |
| 206 | const payload: unknown = await response.json(); |
| 207 | if (!isRecord(payload)) { |
| 208 | throw new Error( |
| 209 | `Unexpected custom registry response at ${source.url}: expected a JSON object keyed by provider id.`, |
| 210 | ); |
| 211 | } |
| 212 | |
| 213 | const out: Record<string, CustomRegistryProviderEntry> = {}; |
| 214 | for (const [key, raw] of Object.entries(payload)) { |
| 215 | const entry = toProviderEntry(raw); |
| 216 | if (entry === undefined) { |
| 217 | // Skip invalid/unknown provider entries instead of aborting the whole |
| 218 | // fetch, mirroring `toModelEntry`'s skip-on-invalid behavior. This keeps |
| 219 | // existing providers working when kokub adds a new provider type that |
| 220 | // this client doesn't yet recognize. |
| 221 | console.warn( |
| 222 | `[custom-registry] Skipping invalid entry "${key}" at ${source.url}: missing required fields or unsupported type (id, name, api, type, models).`, |
| 223 | ); |
| 224 | continue; |
| 225 | } |
| 226 | out[key] = entry; |
| 227 | } |
| 228 | |
| 229 | return out; |
| 230 | } |
| 231 | |
| 232 | /** |
| 233 | * Derives kosong capability strings from the rich (optional) fields on a |
no test coverage detected