fix(dashboard): memoize compatible provider groups (#4613)

Integrated into release/v3.8.34 (rebuilt + test added)
This commit is contained in:
KooshaPari
2026-06-22 13:33:54 -07:00
committed by GitHub
parent 312d2f5887
commit ecfb054b66
3 changed files with 125 additions and 34 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useMemo } from "react";
import { Card, CardSkeleton, Badge, Button, CollapsibleSection } from "@/shared/components";
import {
AGGREGATOR_PROVIDER_IDS,
@@ -9,7 +9,6 @@ import {
IDE_PROVIDER_IDS,
IMAGE_ONLY_PROVIDER_IDS,
VIDEO_PROVIDER_IDS,
isClaudeCodeCompatibleProvider,
} from "@/shared/constants/providers";
import { useRouter, useSearchParams } from "next/navigation";
import { getErrorCode, getRelativeTime } from "@/shared/utils";
@@ -19,6 +18,7 @@ import { useNotificationStore } from "@/store/notificationStore";
import { useTranslations } from "next-intl";
import {
buildStaticProviderEntries,
buildCompatibleProviderGroups,
filterConfiguredProviderEntries,
shouldFilterProviderEntriesForDisplayMode,
shouldShowFirstProviderHint,
@@ -480,37 +480,18 @@ export default function ProvidersPage() {
}
};
const compatibleProviders = providerNodes
.filter((node) => node.type === "openai-compatible")
.map((node) => ({
id: node.id,
name: node.name || t("openaiCompatibleName"),
color: "#10A37F",
textIcon: "OC",
apiType: node.apiType,
}));
const anthropicCompatibleProviders = providerNodes
.filter(
(node) => node.type === "anthropic-compatible" && !isClaudeCodeCompatibleProvider(node.id)
)
.map((node) => ({
id: node.id,
name: node.name || t("anthropicCompatibleName"),
color: "#D97757",
textIcon: "AC",
}));
const ccCompatibleProviders = providerNodes
.filter(
(node) => node.type === "anthropic-compatible" && isClaudeCodeCompatibleProvider(node.id)
)
.map((node) => ({
id: node.id,
name: node.name || ccCompatibleLabel,
color: "#B45309",
textIcon: "CC",
}));
const compatibleProviderGroups = useMemo(
() =>
buildCompatibleProviderGroups(providerNodes, {
openaiCompatibleName: t("openaiCompatibleName"),
anthropicCompatibleName: t("anthropicCompatibleName"),
claudeCodeCompatibleName: ccCompatibleLabel,
}),
[ccCompatibleLabel, providerNodes, t]
);
const compatibleProviders = compatibleProviderGroups.openai;
const anthropicCompatibleProviders = compatibleProviderGroups.anthropic;
const ccCompatibleProviders = compatibleProviderGroups.claudeCode;
const effectiveProviderDisplayMode =
providerDisplayMode === "configured" && connections.length === 0 ? "all" : providerDisplayMode;
@@ -530,7 +511,7 @@ export default function ProvidersPage() {
activeServiceKind
);
const blockedProviderSet = new Set(blockedProviders);
const blockedProviderSet = useMemo(() => new Set(blockedProviders), [blockedProviders]);
const rawNoAuthEntriesAll = buildStaticProviderEntries("no-auth", getProviderStats);
const noAuthEntriesAll = rawNoAuthEntriesAll.filter(({ providerId, provider }) => {
const alias = typeof provider.alias === "string" ? provider.alias : null;

View File

@@ -7,6 +7,7 @@ import {
type ResolvedProviderCatalogEntry,
type StaticProviderCatalogCategory,
} from "@/lib/providers/catalog";
import { isClaudeCodeCompatibleProvider } from "@/shared/constants/providers";
import { getModelsByProviderId } from "@/shared/constants/models";
import { providerHasServiceKind } from "@/lib/providers/serviceKindIndex";
import { compareTr, matchesSearch } from "@/shared/utils/turkishText";
@@ -25,6 +26,20 @@ export interface ProviderEntry<TProvider = Record<string, unknown>> {
toggleAuthType: "oauth" | "free" | "apikey" | "no-auth";
}
export type CompatibleProviderInfo = {
id: string;
name: string;
color: string;
textIcon: string;
apiType?: string;
};
export type CompatibleProviderGroups = {
openai: CompatibleProviderInfo[];
anthropic: CompatibleProviderInfo[];
claudeCode: CompatibleProviderInfo[];
};
export function shouldApplyConfiguredOnlyFilter(
showConfiguredOnly: boolean,
connectionCount: number
@@ -110,6 +125,53 @@ export function buildStaticProviderEntries(
);
}
export function buildCompatibleProviderGroups(
providerNodes: Array<{ id: string; name?: string; type?: string; apiType?: string }>,
labels: {
openaiCompatibleName: string;
anthropicCompatibleName: string;
claudeCodeCompatibleName: string;
}
): CompatibleProviderGroups {
const openai: CompatibleProviderInfo[] = [];
const anthropic: CompatibleProviderInfo[] = [];
const claudeCode: CompatibleProviderInfo[] = [];
for (const node of providerNodes) {
if (node.type === "openai-compatible") {
openai.push({
id: node.id,
name: node.name || labels.openaiCompatibleName,
color: "#10A37F",
textIcon: "OC",
apiType: node.apiType,
});
continue;
}
if (node.type !== "anthropic-compatible") continue;
if (isClaudeCodeCompatibleProvider(node.id)) {
claudeCode.push({
id: node.id,
name: node.name || labels.claudeCodeCompatibleName,
color: "#B45309",
textIcon: "CC",
});
continue;
}
anthropic.push({
id: node.id,
name: node.name || labels.anthropicCompatibleName,
color: "#D97757",
textIcon: "AC",
});
}
return { openai, anthropic, claudeCode };
}
export function filterConfiguredProviderEntries<TProvider>(
entries: ProviderEntry<TProvider>[],
showConfiguredOnly: boolean,

View File

@@ -1001,3 +1001,51 @@ test("model search filter is case-insensitive and partial-match", () => {
);
assert.equal(byPartial.length, 1, "partial model id 'minimax' should match 'minimax-m3'");
});
// #4613: buildCompatibleProviderGroups partitions provider nodes into the
// openai-compatible / anthropic-compatible / claude-code-compatible buckets the
// providers page renders. The memoization in page.tsx wraps this pure helper, so
// guarding the partition logic here is the regression that matters (Rule #18).
test("buildCompatibleProviderGroups partitions nodes by type + claude-code prefix", () => {
const labels = {
openaiCompatibleName: "OpenAI-compatible",
anthropicCompatibleName: "Anthropic-compatible",
claudeCodeCompatibleName: "Claude Code-compatible",
};
const groups = providerPageUtils.buildCompatibleProviderGroups(
[
{ id: "my-oai", name: "My OAI", type: "openai-compatible", apiType: "responses" },
{ id: "my-anthropic", name: "My Claude", type: "anthropic-compatible" },
{ id: "anthropic-compatible-cc-acme", name: "Acme CC", type: "anthropic-compatible" },
{ id: "ignored-node", name: "Ignored", type: "gemini-cli" },
// name omitted → falls back to the provided label
{ id: "anon-oai", type: "openai-compatible" },
],
labels
);
assert.deepEqual(
groups.openai.map((p) => p.id),
["my-oai", "anon-oai"],
"openai-compatible nodes land in the openai bucket"
);
assert.equal(groups.openai[0].apiType, "responses", "apiType is preserved");
assert.equal(
groups.openai[1].name,
labels.openaiCompatibleName,
"missing name falls back to the openai-compatible label"
);
assert.deepEqual(
groups.anthropic.map((p) => p.id),
["my-anthropic"],
"plain anthropic-compatible nodes land in the anthropic bucket"
);
assert.deepEqual(
groups.claudeCode.map((p) => p.id),
["anthropic-compatible-cc-acme"],
"anthropic-compatible nodes with the cc- prefix land in the claudeCode bucket"
);
});