From 673b4d5c9c2b40a00eea847cc234d892bb0cfd4f Mon Sep 17 00:00:00 2001
From: Diego Rodrigues de Sa e Souza
<8016841+diegosouzapw@users.noreply.github.com>
Date: Sat, 23 May 2026 17:18:48 -0300
Subject: [PATCH] feat(dashboard): free-tier grouping with symbolic link in
/providers (#2632)
Integrated into release/v3.8.3
---
.../providers/components/CategoryDot.tsx | 24 +++
.../providers/components/ProviderCard.tsx | 16 +-
.../(dashboard)/dashboard/providers/page.tsx | 111 +++++++-----
src/i18n/messages/en.json | 2 +
src/i18n/messages/pt-BR.json | 2 +
src/shared/constants/providers.ts | 30 ++-
tests/unit/providers-free-tier-filter.test.ts | 171 ++++++++++++++++++
7 files changed, 302 insertions(+), 54 deletions(-)
create mode 100644 src/app/(dashboard)/dashboard/providers/components/CategoryDot.tsx
create mode 100644 tests/unit/providers-free-tier-filter.test.ts
diff --git a/src/app/(dashboard)/dashboard/providers/components/CategoryDot.tsx b/src/app/(dashboard)/dashboard/providers/components/CategoryDot.tsx
new file mode 100644
index 0000000000..215a524cc6
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/providers/components/CategoryDot.tsx
@@ -0,0 +1,24 @@
+import { cn } from "@/shared/utils";
+
+interface CategoryDotProps {
+ color: string;
+ hasFree?: boolean;
+ label?: string;
+ freeLabel?: string;
+ className?: string;
+}
+
+export function CategoryDot({
+ color,
+ hasFree = false,
+ label,
+ freeLabel,
+ className,
+}: CategoryDotProps) {
+ return (
+
+
+ {hasFree && }
+
+ );
+}
diff --git a/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx b/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx
index 969fa4d049..554d00e8b2 100644
--- a/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx
+++ b/src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx
@@ -13,6 +13,8 @@ import {
isOpenAICompatibleProvider,
} from "@/shared/constants/providers";
+import { CategoryDot } from "./CategoryDot";
+
interface ProviderStats {
total?: number;
connected?: number;
@@ -194,16 +196,12 @@ export default function ProviderCard({
)}
-
- {provider.hasFree === true && authType !== "free" && (
-
- )}
{allDisabled ? (
diff --git a/src/app/(dashboard)/dashboard/providers/page.tsx b/src/app/(dashboard)/dashboard/providers/page.tsx
index 40c9a3727c..ba2bc54fd9 100644
--- a/src/app/(dashboard)/dashboard/providers/page.tsx
+++ b/src/app/(dashboard)/dashboard/providers/page.tsx
@@ -42,9 +42,24 @@ import {
isCodexGlobalFastServiceTierEnabled,
} from "@/lib/providers/codexFastTier";
import AddCompatibleProviderModal from "./components/AddCompatibleProviderModal";
+import { CategoryDot } from "./components/CategoryDot";
import ProviderCard from "./components/ProviderCard";
import ProviderCountBadge from "./components/ProviderCountBadge";
+type DashboardProviderInfo = {
+ id?: string;
+ name: string;
+ color?: string;
+ apiType?: string;
+ deprecated?: boolean;
+ deprecationReason?: string;
+ hasFree?: boolean;
+ freeNote?: string;
+ [key: string]: unknown;
+};
+
+type DashboardProviderEntry = ProviderEntry
;
+
function countConfigured(entries: ProviderEntry[]) {
return {
configured: entries.filter((entry) => Number(entry.stats?.total || 0) > 0).length,
@@ -52,6 +67,19 @@ function countConfigured(entries: ProviderEntry[]) {
};
}
+function dedupeProviderEntries(entries: DashboardProviderEntry[]): DashboardProviderEntry[] {
+ const seen = new Set();
+ return entries.filter((entry) => {
+ if (seen.has(entry.providerId)) return false;
+ seen.add(entry.providerId);
+ return true;
+ });
+}
+
+function providerEntryHasFree(entry: DashboardProviderEntry): boolean {
+ return entry.provider.hasFree === true;
+}
+
type ProviderBatchTestResult = {
connectionId?: string;
connectionName?: string;
@@ -137,7 +165,12 @@ export default function ProvidersPage() {
const [showFreeOnly, setShowFreeOnly] = useState(false);
const [activeCategory, setActiveCategory] = useState(null);
const notify = useNotificationStore();
- const showSection = (category: string) => !activeCategory || activeCategory === category;
+ const hasSearchQuery = searchQuery.trim().length > 0;
+ const showSection = (category: string) => {
+ if (showFreeOnly) return category === "free";
+ if (hasSearchQuery && !activeCategory) return category !== "free";
+ return !activeCategory || activeCategory === category;
+ };
const t = useTranslations("providers");
const tc = useTranslations("common");
const ccCompatibleLabel = t("ccCompatibleLabel");
@@ -556,38 +589,21 @@ export default function ProvidersPage() {
showFreeOnly
);
- const FREE_SECTION_IDS = new Set([
- "kiro",
- "amazon-q",
- "gemini-cli",
- "qoder",
- "pollinations",
- "llm7",
- "opencode",
- "gemini",
- "groq",
- "cerebras",
- "mistral",
- "nvidia",
- "openrouter",
- "cloudflare-ai",
- "together",
- "siliconflow",
- "deepseek",
- "longcat",
- "glhf",
- "morph",
- "bazaarlink",
- "uncloseai",
- "completions",
- "freetheai",
- "enally",
- "puter",
- "blackbox",
+ const staticProviderEntriesAll = dedupeProviderEntries([
+ ...oauthProviderEntriesAll,
+ ...apiKeyProviderEntriesAll,
+ ...webCookieProviderEntriesAll,
+ ...localProviderEntriesAll,
+ ...searchProviderEntriesAll,
+ ...audioProviderEntriesAll,
+ ...cloudAgentProviderEntriesAll,
+ ...upstreamProxyEntriesAll,
+ ] as DashboardProviderEntry[]);
+ const dashboardProviderEntriesAll = dedupeProviderEntries([
+ ...staticProviderEntriesAll,
+ ...compatibleProviderEntriesAll,
]);
- const freeSectionEntriesAll = [...oauthProviderEntriesAll, ...apiKeyProviderEntriesAll].filter(
- (e) => FREE_SECTION_IDS.has(e.providerId)
- );
+ const freeSectionEntriesAll = dashboardProviderEntriesAll.filter(providerEntryHasFree);
const freeSectionEntries = filterConfiguredProviderEntries(
freeSectionEntriesAll,
effectiveShowConfiguredOnly,
@@ -611,12 +627,7 @@ export default function ProvidersPage() {
.filter((e) => e.toggleAuthType === "oauth")
.filter((e) => !IDE_PROVIDER_IDS.has(e.providerId));
const summaryStats = {
- all: {
- configured:
- oauthProviderEntriesAll.filter((e) => Number(e.stats?.total || 0) > 0).length +
- apiKeyProviderEntriesAll.filter((e) => Number(e.stats?.total || 0) > 0).length,
- total: oauthProviderEntriesAll.length + apiKeyProviderEntriesAll.length,
- },
+ all: countConfigured(dashboardProviderEntriesAll),
free: countConfigured(freeSectionEntriesAll),
oauth: countConfigured(oauthOnlyEntriesAll),
apikey: countConfigured(apiKeyProviderEntriesAll),
@@ -728,6 +739,7 @@ export default function ProvidersPage() {
color: "bg-green-500",
label: tc("free"),
stat: summaryStats.free,
+ title: t("freeAggregated"),
},
{
key: "oauth",
@@ -788,6 +800,7 @@ export default function ProvidersPage() {
color: string | null;
label: string;
stat: { configured: number; total: number };
+ title?: string;
}>
).map((cat) => {
const isActive =
@@ -817,9 +830,16 @@ export default function ProvidersPage() {
? "bg-primary text-white border-primary"
: "bg-bg-subtle border-border text-text-muted hover:text-text-primary hover:border-primary/30"
}`}
- title={cat.label}
+ title={cat.title || cat.label}
>
- {cat.color && }
+ {cat.color && (
+
+ )}
{cat.label}
{cat.stat.configured}
@@ -943,10 +963,15 @@ export default function ProvidersPage() {
{t("freeTierProviders")}
-
+
-
{t("freeTierProvidersDesc")}
+
{t("freeAggregated")}