Merge pull request #17177 from BerriAI/litellm_ui_model_perf_2

[Infra] UI - Migrate Provider Fields to React Query
This commit is contained in:
yuneng-jiang 2025-11-26 20:07:57 -08:00 committed by GitHub
commit e093429740
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 359 additions and 258 deletions

View file

@ -0,0 +1,14 @@
import { getProviderCreateMetadata, ProviderCreateInfo } from "@/components/networking";
import { useQuery } from "@tanstack/react-query";
import { createQueryKeys } from "../common/queryKeysFactory";
const providerFieldsKeys = createQueryKeys("providerFields");
export const useProviderFields = () => {
return useQuery<ProviderCreateInfo[]>({
queryKey: providerFieldsKeys.list({}),
queryFn: async () => await getProviderCreateMetadata(),
staleTime: 24 * 60 * 60 * 1000, // 24 hours - data rarely changes
gcTime: 24 * 60 * 60 * 1000, // 24 hours - keep in cache for 24 hours
});
};

View file

@ -1,13 +1,13 @@
import { render, renderHook, waitFor } from "@testing-library/react";
import { describe, it, vi, expect } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, renderHook, screen } from "@testing-library/react";
import { Form } from "antd";
import AddModelTab from "./add_model_tab";
import { Providers } from "../provider_info_helpers";
import type { UploadProps } from "antd/es/upload";
import { describe, expect, it, vi } from "vitest";
import type { Team } from "../key_team_helpers/key_list";
import type { CredentialItem } from "../networking";
import type { UploadProps } from "antd/es/upload";
import { Providers } from "../provider_info_helpers";
import AddModelTab from "./add_model_tab";
// Mock the networking module
vi.mock("../networking", async () => {
const actual = await vi.importActual("../networking");
return {
@ -19,6 +19,12 @@ vi.mock("../networking", async () => {
modelAvailableCall: vi.fn().mockResolvedValue({
data: [{ id: "model-group-1" }, { id: "model-group-2" }],
}),
modelHubCall: vi.fn().mockResolvedValue({
data: [
{ model_group: "gpt-4", mode: "chat" },
{ model_group: "gpt-3.5-turbo", mode: "chat" },
],
}),
getProviderCreateMetadata: vi.fn().mockResolvedValue([
{
provider: "OpenAI",
@ -31,91 +37,198 @@ vi.mock("../networking", async () => {
};
});
describe("Add Model Tab", () => {
it(
"should render",
async () => {
// Create a form instance using renderHook
const { result } = renderHook(() => Form.useForm());
const [form] = result.current;
// Mock functions
const handleOk = vi.fn();
const setSelectedProvider = vi.fn();
const setProviderModelsFn = vi.fn();
const getPlaceholder = vi.fn((provider: Providers) => `Enter ${provider} model name`);
const setShowAdvancedSettings = vi.fn();
// Mock data
const selectedProvider = Providers.OpenAI;
const providerModels = ["gpt-4", "gpt-3.5-turbo"];
const showAdvancedSettings = false;
const teams: Team[] = [
{
team_id: "team-1",
team_alias: "Test Team",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "monthly",
tpm_limit: null,
rpm_limit: null,
organization_id: "org-1",
created_at: "2024-01-01T00:00:00Z",
keys: [],
members_with_roles: [],
},
];
const credentials: CredentialItem[] = [
{
credential_name: "test-credential",
credential_values: {},
credential_info: {
custom_llm_provider: "openai",
description: "Test credential",
},
},
];
const uploadProps: UploadProps = {
beforeUpload: () => false,
showUploadList: false,
};
const accessToken = "test-access-token";
const userRole = "Admin";
const premiumUser = true;
const { container, findByText } = render(
<AddModelTab
form={form}
handleOk={handleOk}
selectedProvider={selectedProvider}
setSelectedProvider={setSelectedProvider}
providerModels={providerModels}
setProviderModelsFn={setProviderModelsFn}
getPlaceholder={getPlaceholder}
uploadProps={uploadProps}
showAdvancedSettings={showAdvancedSettings}
setShowAdvancedSettings={setShowAdvancedSettings}
teams={teams}
credentials={credentials}
accessToken={accessToken}
userRole={userRole}
premiumUser={premiumUser}
/>,
);
// Wait for the tabs to render which indicates the component loaded
await waitFor(
() => {
const tabs = container.querySelectorAll('[role="tab"]');
expect(tabs.length).toBeGreaterThan(0);
},
{ timeout: 10000 },
);
const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
15000,
);
});
const createTestProps = () => {
const { result } = renderHook(() => Form.useForm());
const [form] = result.current;
const handleOk = vi.fn();
const setSelectedProvider = vi.fn();
const setProviderModelsFn = vi.fn();
const getPlaceholder = vi.fn((provider: Providers) => `Enter ${provider} model name`);
const setShowAdvancedSettings = vi.fn();
const selectedProvider = Providers.OpenAI;
const providerModels = ["gpt-4", "gpt-3.5-turbo"];
const showAdvancedSettings = false;
const teams: Team[] = [
{
team_id: "team-1",
team_alias: "Test Team",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "monthly",
tpm_limit: null,
rpm_limit: null,
organization_id: "org-1",
created_at: "2024-01-01T00:00:00Z",
keys: [],
members_with_roles: [],
},
];
const credentials: CredentialItem[] = [
{
credential_name: "test-credential",
credential_values: {},
credential_info: {
custom_llm_provider: "openai",
description: "Test credential",
},
},
];
const uploadProps: UploadProps = {
beforeUpload: () => false,
showUploadList: false,
};
return {
form,
handleOk,
setSelectedProvider,
setProviderModelsFn,
getPlaceholder,
setShowAdvancedSettings,
selectedProvider,
providerModels,
showAdvancedSettings,
teams,
credentials,
uploadProps,
accessToken: "test-access-token",
userRole: "Admin",
premiumUser: true,
};
};
describe("Add Model Tab", () => {
it("should render", async () => {
const props = createTestProps();
const queryClient = createQueryClient();
render(
<QueryClientProvider client={queryClient}>
<AddModelTab
form={props.form}
handleOk={props.handleOk}
selectedProvider={props.selectedProvider}
setSelectedProvider={props.setSelectedProvider}
providerModels={props.providerModels}
setProviderModelsFn={props.setProviderModelsFn}
getPlaceholder={props.getPlaceholder}
uploadProps={props.uploadProps}
showAdvancedSettings={props.showAdvancedSettings}
setShowAdvancedSettings={props.setShowAdvancedSettings}
teams={props.teams}
credentials={props.credentials}
accessToken={props.accessToken}
userRole={props.userRole}
premiumUser={props.premiumUser}
/>
</QueryClientProvider>,
);
expect(await screen.findByRole("tab", { name: "Add Model" })).toBeInTheDocument();
});
it("should display both Add Model and Add Auto Router tabs", async () => {
const props = createTestProps();
const queryClient = createQueryClient();
render(
<QueryClientProvider client={queryClient}>
<AddModelTab
form={props.form}
handleOk={props.handleOk}
selectedProvider={props.selectedProvider}
setSelectedProvider={props.setSelectedProvider}
providerModels={props.providerModels}
setProviderModelsFn={props.setProviderModelsFn}
getPlaceholder={props.getPlaceholder}
uploadProps={props.uploadProps}
showAdvancedSettings={props.showAdvancedSettings}
setShowAdvancedSettings={props.setShowAdvancedSettings}
teams={props.teams}
credentials={props.credentials}
accessToken={props.accessToken}
userRole={props.userRole}
premiumUser={props.premiumUser}
/>
</QueryClientProvider>,
);
expect(await screen.findByRole("tab", { name: "Add Model" })).toBeInTheDocument();
expect(await screen.findByRole("tab", { name: "Add Auto Router" })).toBeInTheDocument();
});
it("should display provider selection field", async () => {
const props = createTestProps();
const queryClient = createQueryClient();
render(
<QueryClientProvider client={queryClient}>
<AddModelTab
form={props.form}
handleOk={props.handleOk}
selectedProvider={props.selectedProvider}
setSelectedProvider={props.setSelectedProvider}
providerModels={props.providerModels}
setProviderModelsFn={props.setProviderModelsFn}
getPlaceholder={props.getPlaceholder}
uploadProps={props.uploadProps}
showAdvancedSettings={props.showAdvancedSettings}
setShowAdvancedSettings={props.setShowAdvancedSettings}
teams={props.teams}
credentials={props.credentials}
accessToken={props.accessToken}
userRole={props.userRole}
premiumUser={props.premiumUser}
/>
</QueryClientProvider>,
);
expect(await screen.findByText("Provider")).toBeInTheDocument();
});
it("should display Test Connect and Add Model buttons", async () => {
const props = createTestProps();
const queryClient = createQueryClient();
render(
<QueryClientProvider client={queryClient}>
<AddModelTab
form={props.form}
handleOk={props.handleOk}
selectedProvider={props.selectedProvider}
setSelectedProvider={props.setSelectedProvider}
providerModels={props.providerModels}
setProviderModelsFn={props.setProviderModelsFn}
getPlaceholder={props.getPlaceholder}
uploadProps={props.uploadProps}
showAdvancedSettings={props.showAdvancedSettings}
setShowAdvancedSettings={props.setShowAdvancedSettings}
teams={props.teams}
credentials={props.credentials}
accessToken={props.accessToken}
userRole={props.userRole}
premiumUser={props.premiumUser}
/>
</QueryClientProvider>,
);
const testConnectButtons = await screen.findAllByRole("button", { name: "Test Connect" });
expect(testConnectButtons.length).toBeGreaterThan(0);
expect(await screen.findByRole("button", { name: "Add Model" })).toBeInTheDocument();
}, 10000); // 10 seconds timeout for complex logic
});

View file

@ -1,31 +1,29 @@
import React, { useEffect, useMemo, useState } from "react";
import { Card, Form, Button, Tooltip, Typography, Select as AntdSelect, Modal } from "antd";
import { useProviderFields } from "@/app/(dashboard)/hooks/providers/useProviderFields";
import { all_admin_roles } from "@/utils/roles";
import { Switch, Tab, TabGroup, TabList, TabPanel, TabPanels, Text } from "@tremor/react";
import type { FormInstance } from "antd";
import { Select as AntdSelect, Button, Card, Col, Form, Modal, Row, Tooltip, Typography } from "antd";
import type { UploadProps } from "antd/es/upload";
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
import LiteLLMModelNameField from "./litellm_model_name";
import ConditionalPublicModelName from "./conditional_public_model_name";
import ProviderSpecificFields from "./provider_specific_fields";
import AdvancedSettings from "./advanced_settings";
import { Providers, providerLogoMap } from "../provider_info_helpers";
import React, { useEffect, useMemo, useState } from "react";
import TeamDropdown from "../common_components/team_dropdown";
import type { Team } from "../key_team_helpers/key_list";
import {
type CredentialItem,
type ProviderCreateInfo,
getGuardrailsList,
getProviderCreateMetadata,
modelAvailableCall,
tagListCall,
} from "../networking";
import ConnectionErrorDisplay from "./model_connection_test";
import { TEST_MODES } from "./add_model_modes";
import { Row, Col } from "antd";
import { Text, Switch } from "@tremor/react";
import TeamDropdown from "../common_components/team_dropdown";
import { all_admin_roles } from "@/utils/roles";
import AddAutoRouterTab from "./add_auto_router_tab";
import { handleAddAutoRouterSubmit } from "./handle_add_auto_router_submit";
import { Providers, providerLogoMap } from "../provider_info_helpers";
import { Tag } from "../tag_management/types";
import AddAutoRouterTab from "./add_auto_router_tab";
import { TEST_MODES } from "./add_model_modes";
import AdvancedSettings from "./advanced_settings";
import ConditionalPublicModelName from "./conditional_public_model_name";
import { handleAddAutoRouterSubmit } from "./handle_add_auto_router_submit";
import LiteLLMModelNameField from "./litellm_model_name";
import ConnectionErrorDisplay from "./model_connection_test";
import ProviderSpecificFields from "./provider_specific_fields";
interface AddModelTabProps {
form: FormInstance; // For the Add Model tab
@ -76,9 +74,11 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
const [connectionTestId, setConnectionTestId] = useState<string>("");
// Provider metadata for driving the provider select from backend config
const [providerMetadata, setProviderMetadata] = useState<ProviderCreateInfo[] | null>(null);
const [isProviderMetadataLoading, setIsProviderMetadataLoading] = useState<boolean>(false);
const [providerMetadataError, setProviderMetadataError] = useState<string | null>(null);
const {
data: providerMetadata,
isLoading: isProviderMetadataLoading,
error: providerMetadataError,
} = useProviderFields();
useEffect(() => {
const fetchGuardrails = async () => {
@ -107,37 +107,6 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
fetchTags();
}, [accessToken]);
useEffect(() => {
let isMounted = true;
const fetchProviderMetadata = async () => {
setIsProviderMetadataLoading(true);
setProviderMetadataError(null);
try {
const metadata = await getProviderCreateMetadata();
if (!isMounted) {
return;
}
setProviderMetadata(metadata);
} catch (error) {
console.error("Failed to fetch provider metadata:", error);
if (isMounted) {
setProviderMetadataError("Failed to load providers");
}
} finally {
if (isMounted) {
setIsProviderMetadataLoading(false);
}
}
};
fetchProviderMetadata();
return () => {
isMounted = false;
};
}, []);
// Test connection when button is clicked
const handleTestConnection = async () => {
setIsTestingConnection(true);
@ -168,6 +137,12 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
return [...providerMetadata].sort((a, b) => a.provider_display_name.localeCompare(b.provider_display_name));
}, [providerMetadata]);
const providerMetadataErrorText = providerMetadataError
? providerMetadataError instanceof Error
? providerMetadataError.message
: "Failed to load providers"
: null;
const isAdmin = all_admin_roles.includes(userRole);
const handleAutoRouterOk = () => {
@ -232,9 +207,9 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
});
}}
>
{providerMetadataError && sortedProviderMetadata.length === 0 && (
{providerMetadataErrorText && sortedProviderMetadata.length === 0 && (
<AntdSelect.Option key="__error" value="">
{providerMetadataError}
{providerMetadataErrorText}
</AntdSelect.Option>
)}
{sortedProviderMetadata.map((providerInfo) => {

View file

@ -1,6 +1,7 @@
import { render, waitFor } from "@testing-library/react";
import { describe, it, expect, beforeAll, vi } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import { Form } from "antd";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { Providers } from "../provider_info_helpers";
import ProviderSpecificFields from "./provider_specific_fields";
@ -97,7 +98,6 @@ vi.mock("../networking", async () => {
};
});
// Mock window.matchMedia for Ant Design components
beforeAll(() => {
Object.defineProperty(window, "matchMedia", {
writable: true,
@ -105,8 +105,8 @@ beforeAll(() => {
matches: false,
media: query,
onchange: null,
addListener: () => {}, // deprecated
removeListener: () => {}, // deprecated
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
@ -114,80 +114,105 @@ beforeAll(() => {
});
});
const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
});
describe("ProviderSpecificFields", () => {
it("should render the provider specific fields for OpenAI", async () => {
const { getByLabelText, getByPlaceholderText } = render(
<Form>
<ProviderSpecificFields selectedProvider={Providers.OpenAI} />
</Form>,
it("should render", async () => {
const queryClient = createQueryClient();
render(
<QueryClientProvider client={queryClient}>
<Form>
<ProviderSpecificFields selectedProvider={Providers.OpenAI} />
</Form>
</QueryClientProvider>,
);
await waitFor(() => {
// Check for the API Base text input
const apiBaseInput = getByPlaceholderText("https://api.openai.com/v1");
expect(screen.getByLabelText("OpenAI API Key")).toBeInTheDocument();
});
});
it("should render the provider specific fields for OpenAI", async () => {
const queryClient = createQueryClient();
render(
<QueryClientProvider client={queryClient}>
<Form>
<ProviderSpecificFields selectedProvider={Providers.OpenAI} />
</Form>
</QueryClientProvider>,
);
await waitFor(() => {
const apiKeyLabel = screen.getByLabelText("OpenAI API Key");
expect(apiKeyLabel).toBeInTheDocument();
const apiBaseInput = screen.getByPlaceholderText("https://api.openai.com/v1");
expect(apiBaseInput).toBeInTheDocument();
expect(apiBaseInput).toHaveAttribute("type", "text");
// Check for Organization field
const orgInput = getByPlaceholderText("[OPTIONAL] my-unique-org");
const orgInput = screen.getByPlaceholderText("[OPTIONAL] my-unique-org");
expect(orgInput).toBeInTheDocument();
// Check for API Key field
const apiKeyLabel = getByLabelText("OpenAI API Key");
expect(apiKeyLabel).toBeInTheDocument();
});
});
it("should render the provider specific fields for vLLM", async () => {
const { getByLabelText, getByPlaceholderText } = render(
<Form>
<ProviderSpecificFields selectedProvider={"Hosted_Vllm" as Providers} />
</Form>,
const queryClient = createQueryClient();
render(
<QueryClientProvider client={queryClient}>
<Form>
<ProviderSpecificFields selectedProvider={"Hosted_Vllm" as Providers} />
</Form>
</QueryClientProvider>,
);
await waitFor(() => {
const apiBaseInput = getByPlaceholderText("https://...");
const apiKeyLabel = screen.getByLabelText("vLLM API Key");
expect(apiKeyLabel).toBeInTheDocument();
const apiBaseInput = screen.getByPlaceholderText("https://...");
expect(apiBaseInput).toBeInTheDocument();
expect(apiBaseInput).toHaveAttribute("type", "text");
// Check for API Key field
const apiKeyLabel = getByLabelText("vLLM API Key");
expect(apiKeyLabel).toBeInTheDocument();
});
});
it("should render the provider specific fields for Azure", async () => {
const { getByLabelText, getByPlaceholderText } = render(
<Form>
<ProviderSpecificFields selectedProvider={Providers.Azure} />
</Form>,
const queryClient = createQueryClient();
render(
<QueryClientProvider client={queryClient}>
<Form>
<ProviderSpecificFields selectedProvider={Providers.Azure} />
</Form>
</QueryClientProvider>,
);
await waitFor(() => {
// Check for API Base field
const apiBaseInput = getByPlaceholderText("https://...");
expect(apiBaseInput).toBeInTheDocument();
expect(apiBaseInput).toHaveAttribute("type", "text");
// Check for API Version field
const apiVersionInput = getByPlaceholderText("2023-07-01-preview");
expect(apiVersionInput).toBeInTheDocument();
// Check for Base Model field
const baseModelInput = getByPlaceholderText("azure/gpt-3.5-turbo");
expect(baseModelInput).toBeInTheDocument();
// Check for API Key field
const apiKeyInput = getByLabelText("Azure API Key");
const apiKeyInput = screen.getByLabelText("Azure API Key");
expect(apiKeyInput).toBeInTheDocument();
expect(apiKeyInput).toHaveAttribute("type", "password");
expect(apiKeyInput).toHaveAttribute("placeholder", "Enter your Azure API Key");
// Check for Azure AD Token field
const azureAdTokenInput = getByLabelText("Azure AD Token");
const azureAdTokenInput = screen.getByLabelText("Azure AD Token");
expect(azureAdTokenInput).toBeInTheDocument();
expect(azureAdTokenInput).toHaveAttribute("type", "password");
expect(azureAdTokenInput).toHaveAttribute("placeholder", "Enter your Azure AD Token");
const apiBaseInput = screen.getByPlaceholderText("https://...");
expect(apiBaseInput).toBeInTheDocument();
expect(apiBaseInput).toHaveAttribute("type", "text");
const apiVersionInput = screen.getByPlaceholderText("2023-07-01-preview");
expect(apiVersionInput).toBeInTheDocument();
const baseModelInput = screen.getByPlaceholderText("azure/gpt-3.5-turbo");
expect(baseModelInput).toBeInTheDocument();
});
});
});

View file

@ -1,15 +1,10 @@
import React from "react";
import { Form, Select } from "antd";
import { TextInput, Text } from "@tremor/react";
import { Row, Col, Typography, Button as Button2, Upload, UploadProps } from "antd";
import { useProviderFields } from "@/app/(dashboard)/hooks/providers/useProviderFields";
import { UploadOutlined } from "@ant-design/icons";
import { Text, TextInput } from "@tremor/react";
import { Button as Button2, Col, Form, Row, Select, Typography, Upload, UploadProps } from "antd";
import React from "react";
import { CredentialItem, ProviderCredentialFieldMetadata } from "../networking";
import { provider_map, Providers } from "../provider_info_helpers";
import {
CredentialItem,
ProviderCreateInfo,
ProviderCredentialFieldMetadata,
getProviderCreateMetadata,
} from "../networking";
const { Link } = Typography;
interface ProviderSpecificFieldsProps {
@ -99,65 +94,42 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({ selecte
const selectedProviderEnum = Providers[selectedProvider as keyof typeof Providers] as Providers;
const form = Form.useFormInstance(); // Get form instance from context
const [providerMetadata, setProviderMetadata] = React.useState<ProviderCreateInfo[] | null>(null);
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [loadError, setLoadError] = React.useState<string | null>(null);
const { data: providerMetadata, isLoading, error: loadError } = useProviderFields();
// Memoize the expensive cache computation
const cacheEntries = React.useMemo(() => {
if (!providerMetadata) {
return null;
}
// Compute cache entries keyed by provider display name and identifiers
const entries: Record<string, ProviderCredentialField[]> = {};
providerMetadata.forEach((providerInfo) => {
const displayName = providerInfo.provider_display_name;
const mappedFields = providerInfo.credential_fields.map(mapFieldMetadataToUiField);
// Primary key: human-readable display name
entries[displayName] = mappedFields;
// Also cache by backend identifiers so lookups by provider slug work
if (providerInfo.provider) {
entries[providerInfo.provider] = mappedFields;
}
if (providerInfo.litellm_provider) {
entries[providerInfo.litellm_provider] = mappedFields;
}
});
return entries;
}, [providerMetadata]);
// Sync memoized cache entries to module-level cache
React.useEffect(() => {
const hasCachedFields = Object.keys(providerFieldsByDisplayName).length > 0;
if (hasCachedFields) {
// We already have fields cached globally; no need to refetch.
// This is important so we can reuse credential field definitions
// across mounts and in non-React helpers.
if (!cacheEntries) {
return;
}
let isMounted = true;
const fetchProviderFields = async () => {
setIsLoading(true);
setLoadError(null);
try {
const metadata = await getProviderCreateMetadata();
if (!isMounted) {
return;
}
setProviderMetadata(metadata);
// Populate cache keyed by provider display name and identifiers
metadata.forEach((providerInfo) => {
const displayName = providerInfo.provider_display_name;
const mappedFields = providerInfo.credential_fields.map(mapFieldMetadataToUiField);
// Primary key: human-readable display name
providerFieldsByDisplayName[displayName] = mappedFields;
// Also cache by backend identifiers so lookups by provider slug work
if (providerInfo.provider) {
providerFieldsByDisplayName[providerInfo.provider] = mappedFields;
}
if (providerInfo.litellm_provider) {
providerFieldsByDisplayName[providerInfo.litellm_provider] = mappedFields;
}
});
} catch (error) {
console.error("Failed to load provider credential fields:", error);
if (isMounted) {
setLoadError("Failed to load provider credential fields");
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
fetchProviderFields();
return () => {
isMounted = false;
};
}, []);
Object.assign(providerFieldsByDisplayName, cacheEntries);
}, [cacheEntries]);
const allFields = React.useMemo(() => {
// First try to resolve from the in-memory cache. We support both the
@ -234,7 +206,9 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({ selecte
{loadError && allFields.length === 0 && (
<Row>
<Col span={24}>
<Text className="mb-2 text-red-500">{loadError}</Text>
<Text className="mb-2 text-red-500">
{loadError instanceof Error ? loadError.message : "Failed to load provider credential fields"}
</Text>
</Col>
</Row>
)}