feat: filter api/models by active providers + disabled indicator on provider cards (Closes #81) (#85)

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-02-19 22:22:16 -03:00
committed by GitHub
parent c34973f40d
commit 937a8f3714
2 changed files with 65 additions and 22 deletions

View File

@@ -134,14 +134,18 @@ export default function ProvidersPage() {
const error = errorConns.length;
const total = providerConnections.length;
// Check if all connections are manually disabled
const allDisabled = total > 0 && providerConnections.every((c) => c.isActive === false);
// Get latest error info
const latestError = errorConns.sort(
(a: any, b: any) => (new Date(b.lastErrorAt || 0) as any) - (new Date(a.lastErrorAt || 0) as any)
(a: any, b: any) =>
(new Date(b.lastErrorAt || 0) as any) - (new Date(a.lastErrorAt || 0) as any)
)[0];
const errorCode = latestError ? getConnectionErrorTag(latestError) : null;
const errorTime = latestError?.lastErrorAt ? getRelativeTime(latestError.lastErrorAt) : null;
return { connected, error, total, errorCode, errorTime };
return { connected, error, total, errorCode, errorTime, allDisabled };
};
const handleBatchTest = async (mode, providerId = null) => {
@@ -422,7 +426,7 @@ export default function ProvidersPage() {
}
function ProviderCard({ providerId, provider, stats, authType }) {
const { connected, error, errorCode, errorTime } = stats;
const { connected, error, errorCode, errorTime, allDisabled } = stats;
const [imgError, setImgError] = useState(false);
const dotColors = {
@@ -437,7 +441,7 @@ function ProviderCard({ providerId, provider, stats, authType }) {
<Link href={`/dashboard/providers/${providerId}`} className="group">
<Card
padding="xs"
className="h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer"
className={`h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer ${allDisabled ? "opacity-50" : ""}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
@@ -470,8 +474,19 @@ function ProviderCard({ providerId, provider, stats, authType }) {
/>
</h3>
<div className="flex items-center gap-2 text-xs flex-wrap">
{getStatusDisplay(connected, error, errorCode)}
{errorTime && <span className="text-text-muted"> {errorTime}</span>}
{allDisabled ? (
<Badge variant="default" size="sm">
<span className="flex items-center gap-1">
<span className="material-symbols-outlined text-[12px]">pause_circle</span>
Disabled
</span>
</Badge>
) : (
<>
{getStatusDisplay(connected, error, errorCode)}
{errorTime && <span className="text-text-muted"> {errorTime}</span>}
</>
)}
</div>
</div>
</div>
@@ -503,7 +518,7 @@ ProviderCard.propTypes = {
// API Key providers - use image with textIcon fallback (same as OAuth providers)
function ApiKeyProviderCard({ providerId, provider, stats, authType }) {
const { connected, error, errorCode, errorTime } = stats;
const { connected, error, errorCode, errorTime, allDisabled } = stats;
const isCompatible = providerId.startsWith(OPENAI_COMPATIBLE_PREFIX);
const isAnthropicCompatible = providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX);
const [imgError, setImgError] = useState(false);
@@ -531,7 +546,7 @@ function ApiKeyProviderCard({ providerId, provider, stats, authType }) {
<Link href={`/dashboard/providers/${providerId}`} className="group">
<Card
padding="xs"
className="h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer"
className={`h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer ${allDisabled ? "opacity-50" : ""}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
@@ -564,18 +579,29 @@ function ApiKeyProviderCard({ providerId, provider, stats, authType }) {
/>
</h3>
<div className="flex items-center gap-2 text-xs flex-wrap">
{getStatusDisplay(connected, error, errorCode)}
{isCompatible && (
{allDisabled ? (
<Badge variant="default" size="sm">
{provider.apiType === "responses" ? "Responses" : "Chat"}
<span className="flex items-center gap-1">
<span className="material-symbols-outlined text-[12px]">pause_circle</span>
Disabled
</span>
</Badge>
) : (
<>
{getStatusDisplay(connected, error, errorCode)}
{isCompatible && (
<Badge variant="default" size="sm">
{provider.apiType === "responses" ? "Responses" : "Chat"}
</Badge>
)}
{isAnthropicCompatible && (
<Badge variant="default" size="sm">
Messages
</Badge>
)}
{errorTime && <span className="text-text-muted"> {errorTime}</span>}
</>
)}
{isAnthropicCompatible && (
<Badge variant="default" size="sm">
Messages
</Badge>
)}
{errorTime && <span className="text-text-muted"> {errorTime}</span>}
</div>
</div>
</div>

View File

@@ -1,20 +1,37 @@
import { NextResponse } from "next/server";
import { getModelAliases, setModelAlias } from "@/models";
import { getModelAliases, setModelAlias, getProviderConnections } from "@/models";
import { AI_MODELS } from "@/shared/constants/config";
// GET /api/models - Get models with aliases
export async function GET() {
// GET /api/models - Get models with aliases (only from active providers by default)
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const showAll = searchParams.get("all") === "true";
const modelAliases = await getModelAliases();
const models = AI_MODELS.map((m) => {
// Get active provider connections to filter available models
let activeProviders: Set<string> | null = null;
if (!showAll) {
try {
const connections = await getProviderConnections();
const active = connections.filter((c: any) => c.isActive !== false);
activeProviders = new Set(active.map((c: any) => c.provider));
} catch {
// If DB unavailable, show all models
}
}
const models = AI_MODELS.map((m: any) => {
const fullModel = `${m.provider}/${m.model}`;
const available = !activeProviders || activeProviders.has(m.provider);
return {
...m,
fullModel,
alias: modelAliases[fullModel] || m.model,
available,
};
});
}).filter((m: any) => showAll || m.available);
return NextResponse.json({ models });
} catch (error) {