mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(dashboard): memoize compatible provider groups (#4613)
Integrated into release/v3.8.34 (rebuilt + test added)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user