From f5cc227c094cd111a54855f4d4f03f847c142194 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Thu, 30 Oct 2025 11:21:31 -0500 Subject: [PATCH] Extract router models fetching logic into dedicated service Refactor to improve separation of concerns: - Create src/services/router-models/index.ts to handle provider model fetching - Extract buildProviderFetchList() function for fetch options construction - Extract fetchRouterModels() function for coordinated model fetching - Move 150+ lines of provider-specific logic out of webviewMessageHandler - Add comprehensive tests in router-models-service.spec.ts (11 test cases) Benefits: - Cleaner webviewMessageHandler with less business logic - Reusable service for router model operations - Better testability with isolated unit tests - Clear separation between UI message handling and data fetching Files changed: - New: src/services/router-models/index.ts - New: src/services/router-models/__tests__/router-models-service.spec.ts - Modified: src/core/webview/webviewMessageHandler.ts (simplified) --- src/core/webview/webviewMessageHandler.ts | 316 +++--------------- .../__tests__/router-models-service.spec.ts | 266 +++++++++++++++ src/services/router-models/index.ts | 171 ++++++++++ 3 files changed, 490 insertions(+), 263 deletions(-) create mode 100644 src/services/router-models/__tests__/router-models-service.spec.ts create mode 100644 src/services/router-models/index.ts diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 3d0f497a9e..71b7954b09 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -54,9 +54,9 @@ import { openMention } from "../mentions" import { getWorkspacePath } from "../../utils/path" import { Mode, defaultModeSlug } from "../../shared/modes" import { getModels, flushModels } from "../../api/providers/fetchers/modelCache" -import { GetModelsOptions } from "../../shared/api" import { generateSystemPrompt } from "./generateSystemPrompt" import { getCommand } from "../../utils/commands" +import { fetchRouterModels } from "../../services/router-models" const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"]) @@ -768,152 +768,27 @@ export const webviewMessageHandler = async ( case "requestRouterModels": { // Phase 2: Scope to active provider during chat/task flows const { apiConfiguration } = await provider.getState() - const providerStr = apiConfiguration.apiProvider - const activeProvider: RouterName | undefined = - providerStr && isRouterName(providerStr) ? providerStr : undefined - const routerModels: Partial> = { - openrouter: {}, - "vercel-ai-gateway": {}, - huggingface: {}, - litellm: {}, - deepinfra: {}, - "io-intelligence": {}, - requesty: {}, - unbound: {}, - glama: {}, - ollama: {}, - lmstudio: {}, - roo: {}, - } + const { routerModels, errors } = await fetchRouterModels({ + apiConfiguration, + activeProviderOnly: true, + litellmOverrides: message?.values + ? { + apiKey: message.values.litellmApiKey, + baseUrl: message.values.litellmBaseUrl, + } + : undefined, + }) - const safeGetModels = async (options: GetModelsOptions): Promise => { - try { - return await getModels(options) - } catch (error) { - provider.log( - `Failed to fetch models in webviewMessageHandler requestRouterModels for ${options.provider}: ${error instanceof Error ? error.message : String(error)}`, - ) - throw error - } - } - - // Build full list then filter to active provider - const allFetches: { key: RouterName; options: GetModelsOptions }[] = [ - { key: "openrouter", options: { provider: "openrouter" } }, - { - key: "requesty", - options: { - provider: "requesty", - apiKey: apiConfiguration.requestyApiKey, - baseUrl: apiConfiguration.requestyBaseUrl, - }, - }, - { key: "glama", options: { provider: "glama" } }, - { key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } }, - { key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }, - { - key: "deepinfra", - options: { - provider: "deepinfra", - apiKey: apiConfiguration.deepInfraApiKey, - baseUrl: apiConfiguration.deepInfraBaseUrl, - }, - }, - { - key: "roo", - options: { - provider: "roo", - baseUrl: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy", - apiKey: CloudService.hasInstance() - ? CloudService.instance.authService?.getSessionToken() - : undefined, - }, - }, - ] - - // Include local providers (ollama, lmstudio, huggingface) when they are the active provider - if (activeProvider === "ollama") { - allFetches.push({ - key: "ollama", - options: { - provider: "ollama", - baseUrl: apiConfiguration.ollamaBaseUrl, - apiKey: apiConfiguration.ollamaApiKey, - }, + // Send error notifications for failed providers + errors.forEach((err) => { + provider.log(`Error fetching models for ${err.provider}: ${err.error}`) + provider.postMessageToWebview({ + type: "singleRouterModelFetchResponse", + success: false, + error: err.error, + values: { provider: err.provider }, }) - } - if (activeProvider === "lmstudio") { - allFetches.push({ - key: "lmstudio", - options: { - provider: "lmstudio", - baseUrl: apiConfiguration.lmStudioBaseUrl, - }, - }) - } - if (activeProvider === "huggingface") { - allFetches.push({ - key: "huggingface", - options: { - provider: "huggingface", - }, - }) - } - - // IO Intelligence (optional) - if (apiConfiguration.ioIntelligenceApiKey) { - allFetches.push({ - key: "io-intelligence", - options: { provider: "io-intelligence", apiKey: apiConfiguration.ioIntelligenceApiKey }, - }) - } - - // LiteLLM (optional) - const litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey - const litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl - if (litellmApiKey && litellmBaseUrl) { - allFetches.push({ - key: "litellm", - options: { provider: "litellm", apiKey: litellmApiKey, baseUrl: litellmBaseUrl }, - }) - } - - const modelFetchPromises = activeProvider - ? allFetches.filter(({ key }) => key === activeProvider) - : allFetches - - // If nothing matched (edge case), still post empty structure for stability - if (modelFetchPromises.length === 0) { - await provider.postMessageToWebview({ - type: "routerModels", - routerModels: routerModels as RouterModels, - }) - break - } - - const results = await Promise.allSettled( - modelFetchPromises.map(async ({ key, options }) => { - const models = await safeGetModels(options) - return { key, models } - }), - ) - - results.forEach((result, index) => { - const routerName = modelFetchPromises[index].key - if (result.status === "fulfilled") { - routerModels[routerName] = result.value.models - } else { - const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason) - provider.log(`Error fetching models for ${routerName}: ${errorMessage}`) - routerModels[routerName] = {} - provider.postMessageToWebview({ - type: "singleRouterModelFetchResponse", - success: false, - error: errorMessage, - values: { provider: routerName }, - }) - } }) provider.postMessageToWebview({ type: "routerModels", routerModels: routerModels as RouterModels }) @@ -923,127 +798,42 @@ export const webviewMessageHandler = async ( // Settings and activation: fetch all providers (legacy behavior) const { apiConfiguration } = await provider.getState() - const routerModels: Partial> = { - openrouter: {}, - "vercel-ai-gateway": {}, - huggingface: {}, - litellm: {}, - deepinfra: {}, - "io-intelligence": {}, - requesty: {}, - unbound: {}, - glama: {}, - ollama: {}, - lmstudio: {}, - roo: {}, - } - - const safeGetModels = async (options: GetModelsOptions): Promise => { - try { - return await getModels(options) - } catch (error) { - provider.log( - `Failed to fetch models in webviewMessageHandler requestRouterModelsAll for ${options.provider}: ${error instanceof Error ? error.message : String(error)}`, - ) - throw error - } - } - - const modelFetchPromises: { key: RouterName; options: GetModelsOptions }[] = [ - { key: "openrouter", options: { provider: "openrouter" } }, - { - key: "requesty", - options: { - provider: "requesty", - apiKey: apiConfiguration.requestyApiKey, - baseUrl: apiConfiguration.requestyBaseUrl, - }, - }, - { key: "glama", options: { provider: "glama" } }, - { key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } }, - { key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }, - { - key: "deepinfra", - options: { - provider: "deepinfra", - apiKey: apiConfiguration.deepInfraApiKey, - baseUrl: apiConfiguration.deepInfraBaseUrl, - }, - }, - { - key: "roo", - options: { - provider: "roo", - baseUrl: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy", - apiKey: CloudService.hasInstance() - ? CloudService.instance.authService?.getSessionToken() - : undefined, - }, - }, - ] - - // Add IO Intelligence if API key is provided. - const ioIntelligenceApiKey = apiConfiguration.ioIntelligenceApiKey - if (ioIntelligenceApiKey) { - modelFetchPromises.push({ - key: "io-intelligence", - options: { provider: "io-intelligence", apiKey: ioIntelligenceApiKey }, - }) - } - - // Don't fetch Ollama and LM Studio models by default anymore. - // They have their own specific handlers: requestOllamaModels and requestLmStudioModels. - - const litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey - const litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl - if (litellmApiKey && litellmBaseUrl) { - modelFetchPromises.push({ - key: "litellm", - options: { provider: "litellm", apiKey: litellmApiKey, baseUrl: litellmBaseUrl }, - }) - } - - const results = await Promise.allSettled( - modelFetchPromises.map(async ({ key, options }) => { - const models = await safeGetModels(options) - return { key, models } - }), - ) - - results.forEach((result, index) => { - const routerName = modelFetchPromises[index].key - - if (result.status === "fulfilled") { - routerModels[routerName] = result.value.models - - // Ollama and LM Studio settings pages still need these events. - if (routerName === "ollama" && Object.keys(result.value.models).length > 0) { - provider.postMessageToWebview({ - type: "ollamaModels", - ollamaModels: result.value.models, - }) - } else if (routerName === "lmstudio" && Object.keys(result.value.models).length > 0) { - provider.postMessageToWebview({ - type: "lmStudioModels", - lmStudioModels: result.value.models, - }) - } - } else { - // Handle rejection: Post a specific error message for this provider. - const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason) - provider.log(`Error fetching models for ${routerName}: ${errorMessage}`) - - routerModels[routerName] = {} - - provider.postMessageToWebview({ - type: "singleRouterModelFetchResponse", - success: false, - error: errorMessage, - values: { provider: routerName }, - }) - } + const { routerModels, errors } = await fetchRouterModels({ + apiConfiguration, + activeProviderOnly: false, + litellmOverrides: message?.values + ? { + apiKey: message.values.litellmApiKey, + baseUrl: message.values.litellmBaseUrl, + } + : undefined, }) + // Send error notifications for failed providers + errors.forEach((err) => { + provider.log(`Error fetching models for ${err.provider}: ${err.error}`) + provider.postMessageToWebview({ + type: "singleRouterModelFetchResponse", + success: false, + error: err.error, + values: { provider: err.provider }, + }) + }) + + // Send ollama/lmstudio-specific messages if models were fetched + if (routerModels.ollama && Object.keys(routerModels.ollama).length > 0) { + provider.postMessageToWebview({ + type: "ollamaModels", + ollamaModels: routerModels.ollama, + }) + } + if (routerModels.lmstudio && Object.keys(routerModels.lmstudio).length > 0) { + provider.postMessageToWebview({ + type: "lmStudioModels", + lmStudioModels: routerModels.lmstudio, + }) + } + provider.postMessageToWebview({ type: "routerModels", routerModels: routerModels as RouterModels }) break } diff --git a/src/services/router-models/__tests__/router-models-service.spec.ts b/src/services/router-models/__tests__/router-models-service.spec.ts new file mode 100644 index 0000000000..55c05de9cd --- /dev/null +++ b/src/services/router-models/__tests__/router-models-service.spec.ts @@ -0,0 +1,266 @@ +import { describe, it, expect, beforeEach, vi } from "vitest" +import type { Mock } from "vitest" +import type { ProviderSettings } from "@roo-code/types" +import { fetchRouterModels } from "../index" +import { getModels } from "../../../api/providers/fetchers/modelCache" +import { CloudService } from "@roo-code/cloud" + +// Mock dependencies +vi.mock("../../../api/providers/fetchers/modelCache") +vi.mock("@roo-code/cloud") + +const mockGetModels = getModels as Mock +const mockCloudService = CloudService as any + +describe("RouterModelsService", () => { + const mockModels = { + "test-model": { + maxTokens: 4096, + contextWindow: 8192, + supportsPromptCache: false, + description: "Test model", + }, + } + + const baseApiConfiguration: ProviderSettings = { + apiProvider: "openrouter", + openRouterApiKey: "test-key", + requestyApiKey: "requesty-key", + unboundApiKey: "unbound-key", + ioIntelligenceApiKey: "io-key", + deepInfraApiKey: "deepinfra-key", + litellmApiKey: "litellm-key", + litellmBaseUrl: "http://localhost:4000", + } + + beforeEach(() => { + vi.clearAllMocks() + mockGetModels.mockResolvedValue(mockModels) + mockCloudService.hasInstance = vi.fn().mockReturnValue(false) + }) + + describe("fetchRouterModels", () => { + it("fetches all providers when activeProviderOnly is false", async () => { + const result = await fetchRouterModels({ + apiConfiguration: baseApiConfiguration, + activeProviderOnly: false, + }) + + // Should fetch all standard providers + expect(mockGetModels).toHaveBeenCalledWith({ provider: "openrouter" }) + expect(mockGetModels).toHaveBeenCalledWith( + expect.objectContaining({ provider: "requesty", apiKey: "requesty-key" }), + ) + expect(mockGetModels).toHaveBeenCalledWith({ provider: "glama" }) + expect(mockGetModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" }) + expect(mockGetModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" }) + expect(mockGetModels).toHaveBeenCalledWith( + expect.objectContaining({ provider: "deepinfra", apiKey: "deepinfra-key" }), + ) + expect(mockGetModels).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "roo", + baseUrl: "https://api.roocode.com/proxy", + }), + ) + expect(mockGetModels).toHaveBeenCalledWith({ provider: "io-intelligence", apiKey: "io-key" }) + expect(mockGetModels).toHaveBeenCalledWith({ + provider: "litellm", + apiKey: "litellm-key", + baseUrl: "http://localhost:4000", + }) + + // Should return models for all providers + expect(result.routerModels).toHaveProperty("openrouter") + expect(result.routerModels).toHaveProperty("requesty") + expect(result.routerModels).toHaveProperty("glama") + expect(result.errors).toEqual([]) + }) + + it("fetches only active provider when activeProviderOnly is true", async () => { + const result = await fetchRouterModels({ + apiConfiguration: { ...baseApiConfiguration, apiProvider: "openrouter" }, + activeProviderOnly: true, + }) + + // Should only fetch openrouter + expect(mockGetModels).toHaveBeenCalledTimes(1) + expect(mockGetModels).toHaveBeenCalledWith({ provider: "openrouter" }) + + // Should return models only for openrouter + expect(result.routerModels.openrouter).toEqual(mockModels) + expect(result.errors).toEqual([]) + }) + + it("includes ollama when it is the active provider", async () => { + const config: ProviderSettings = { + ...baseApiConfiguration, + apiProvider: "ollama", + ollamaBaseUrl: "http://localhost:11434", + } + + await fetchRouterModels({ + apiConfiguration: config, + activeProviderOnly: true, + }) + + expect(mockGetModels).toHaveBeenCalledWith({ + provider: "ollama", + baseUrl: "http://localhost:11434", + apiKey: undefined, + }) + }) + + it("includes lmstudio when it is the active provider", async () => { + const config: ProviderSettings = { + ...baseApiConfiguration, + apiProvider: "lmstudio", + lmStudioBaseUrl: "http://localhost:1234", + } + + await fetchRouterModels({ + apiConfiguration: config, + activeProviderOnly: true, + }) + + expect(mockGetModels).toHaveBeenCalledWith({ + provider: "lmstudio", + baseUrl: "http://localhost:1234", + }) + }) + + it("includes huggingface when it is the active provider", async () => { + const config: ProviderSettings = { + ...baseApiConfiguration, + apiProvider: "huggingface", + } + + await fetchRouterModels({ + apiConfiguration: config, + activeProviderOnly: true, + }) + + expect(mockGetModels).toHaveBeenCalledWith({ + provider: "huggingface", + }) + }) + + it("uses litellmOverrides when provided", async () => { + await fetchRouterModels({ + apiConfiguration: { ...baseApiConfiguration, litellmApiKey: undefined, litellmBaseUrl: undefined }, + activeProviderOnly: false, + litellmOverrides: { + apiKey: "override-key", + baseUrl: "http://override:5000", + }, + }) + + expect(mockGetModels).toHaveBeenCalledWith({ + provider: "litellm", + apiKey: "override-key", + baseUrl: "http://override:5000", + }) + }) + + it("handles provider fetch errors gracefully", async () => { + mockGetModels + .mockResolvedValueOnce(mockModels) // openrouter succeeds + .mockRejectedValueOnce(new Error("Requesty API error")) // requesty fails + .mockResolvedValueOnce(mockModels) // glama succeeds + + const result = await fetchRouterModels({ + apiConfiguration: baseApiConfiguration, + activeProviderOnly: false, + }) + + // Should have errors for failed provider + expect(result.errors).toHaveLength(1) + expect(result.errors[0]).toEqual({ + provider: "requesty", + error: "Requesty API error", + }) + + // Should have empty object for failed provider + expect(result.routerModels.requesty).toEqual({}) + + // Should have models for successful providers + expect(result.routerModels.openrouter).toEqual(mockModels) + }) + + it("skips litellm when no api key or base url provided", async () => { + const config: ProviderSettings = { + ...baseApiConfiguration, + litellmApiKey: undefined, + litellmBaseUrl: undefined, + } + + await fetchRouterModels({ + apiConfiguration: config, + activeProviderOnly: false, + }) + + // Should not call getModels for litellm + expect(mockGetModels).not.toHaveBeenCalledWith(expect.objectContaining({ provider: "litellm" })) + }) + + it("skips io-intelligence when no api key provided", async () => { + const config: ProviderSettings = { + ...baseApiConfiguration, + ioIntelligenceApiKey: undefined, + } + + await fetchRouterModels({ + apiConfiguration: config, + activeProviderOnly: false, + }) + + // Should not call getModels for io-intelligence + expect(mockGetModels).not.toHaveBeenCalledWith(expect.objectContaining({ provider: "io-intelligence" })) + }) + + it("uses roo session token when CloudService is available", async () => { + const mockAuthService = { + getSessionToken: vi.fn().mockReturnValue("session-token-123"), + } + + vi.mocked(CloudService.hasInstance).mockReturnValue(true) + Object.defineProperty(CloudService, "instance", { + get: () => ({ authService: mockAuthService }), + configurable: true, + }) + + await fetchRouterModels({ + apiConfiguration: baseApiConfiguration, + activeProviderOnly: false, + }) + + expect(mockGetModels).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "roo", + apiKey: "session-token-123", + }), + ) + }) + + it("initializes all providers with empty objects", async () => { + const result = await fetchRouterModels({ + apiConfiguration: { apiProvider: "openrouter" } as ProviderSettings, + activeProviderOnly: true, + }) + + // All providers should be initialized even if not fetched + expect(result.routerModels).toHaveProperty("openrouter") + expect(result.routerModels).toHaveProperty("requesty") + expect(result.routerModels).toHaveProperty("glama") + expect(result.routerModels).toHaveProperty("unbound") + expect(result.routerModels).toHaveProperty("vercel-ai-gateway") + expect(result.routerModels).toHaveProperty("deepinfra") + expect(result.routerModels).toHaveProperty("roo") + expect(result.routerModels).toHaveProperty("litellm") + expect(result.routerModels).toHaveProperty("ollama") + expect(result.routerModels).toHaveProperty("lmstudio") + expect(result.routerModels).toHaveProperty("huggingface") + expect(result.routerModels).toHaveProperty("io-intelligence") + }) + }) +}) diff --git a/src/services/router-models/index.ts b/src/services/router-models/index.ts new file mode 100644 index 0000000000..52a27ee599 --- /dev/null +++ b/src/services/router-models/index.ts @@ -0,0 +1,171 @@ +import type { ProviderSettings } from "@roo-code/types" +import { CloudService } from "@roo-code/cloud" +import type { RouterName, ModelRecord, GetModelsOptions } from "../../shared/api" +import { getModels } from "../../api/providers/fetchers/modelCache" + +export interface RouterModelsFetchOptions { + apiConfiguration: ProviderSettings + activeProviderOnly?: boolean + litellmOverrides?: { + apiKey?: string + baseUrl?: string + } +} + +export interface RouterModelsFetchResult { + routerModels: Partial> + errors: Array<{ + provider: RouterName + error: string + }> +} + +/** + * Builds the list of provider fetch options based on configuration and mode. + */ +function buildProviderFetchList( + options: RouterModelsFetchOptions, +): Array<{ key: RouterName; options: GetModelsOptions }> { + const { apiConfiguration, activeProviderOnly, litellmOverrides } = options + + const allFetches: Array<{ key: RouterName; options: GetModelsOptions }> = [ + { key: "openrouter", options: { provider: "openrouter" } }, + { + key: "requesty", + options: { + provider: "requesty", + apiKey: apiConfiguration.requestyApiKey, + baseUrl: apiConfiguration.requestyBaseUrl, + }, + }, + { key: "glama", options: { provider: "glama" } }, + { key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } }, + { key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }, + { + key: "deepinfra", + options: { + provider: "deepinfra", + apiKey: apiConfiguration.deepInfraApiKey, + baseUrl: apiConfiguration.deepInfraBaseUrl, + }, + }, + { + key: "roo", + options: { + provider: "roo", + baseUrl: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy", + apiKey: CloudService.hasInstance() ? CloudService.instance.authService?.getSessionToken() : undefined, + }, + }, + ] + + // Include local providers when in active-provider mode and they are selected + if (activeProviderOnly) { + const activeProvider = apiConfiguration.apiProvider + + if (activeProvider === "ollama") { + allFetches.push({ + key: "ollama", + options: { + provider: "ollama", + baseUrl: apiConfiguration.ollamaBaseUrl, + apiKey: apiConfiguration.ollamaApiKey, + }, + }) + } + if (activeProvider === "lmstudio") { + allFetches.push({ + key: "lmstudio", + options: { + provider: "lmstudio", + baseUrl: apiConfiguration.lmStudioBaseUrl, + }, + }) + } + if (activeProvider === "huggingface") { + allFetches.push({ + key: "huggingface", + options: { + provider: "huggingface", + }, + }) + } + } + + // Add IO Intelligence if API key is provided + if (apiConfiguration.ioIntelligenceApiKey) { + allFetches.push({ + key: "io-intelligence", + options: { provider: "io-intelligence", apiKey: apiConfiguration.ioIntelligenceApiKey }, + }) + } + + // Add LiteLLM if configured (with potential overrides from message) + const litellmApiKey = apiConfiguration.litellmApiKey || litellmOverrides?.apiKey + const litellmBaseUrl = apiConfiguration.litellmBaseUrl || litellmOverrides?.baseUrl + if (litellmApiKey && litellmBaseUrl) { + allFetches.push({ + key: "litellm", + options: { provider: "litellm", apiKey: litellmApiKey, baseUrl: litellmBaseUrl }, + }) + } + + return allFetches +} + +/** + * Fetches router models based on the provided options. + * Can fetch all providers or only the active provider. + */ +export async function fetchRouterModels(options: RouterModelsFetchOptions): Promise { + const { apiConfiguration, activeProviderOnly } = options + + // Initialize empty results for all providers + const routerModels: Partial> = { + openrouter: {}, + "vercel-ai-gateway": {}, + huggingface: {}, + litellm: {}, + deepinfra: {}, + "io-intelligence": {}, + requesty: {}, + unbound: {}, + glama: {}, + ollama: {}, + lmstudio: {}, + roo: {}, + } + + const errors: Array<{ provider: RouterName; error: string }> = [] + + // Build fetch list + const fetchList = buildProviderFetchList(options) + + // Filter to active provider if requested + const activeProvider = apiConfiguration.apiProvider as RouterName | undefined + const modelFetchPromises = + activeProviderOnly && activeProvider ? fetchList.filter(({ key }) => key === activeProvider) : fetchList + + // Execute fetches + const results = await Promise.allSettled( + modelFetchPromises.map(async ({ key, options }) => { + const models = await getModels(options) + return { key, models } + }), + ) + + // Process results + results.forEach((result, index) => { + const routerName = modelFetchPromises[index].key + + if (result.status === "fulfilled") { + routerModels[routerName] = result.value.models + } else { + const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason) + routerModels[routerName] = {} + errors.push({ provider: routerName, error: errorMessage }) + } + }) + + return { routerModels, errors } +}