Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';

type Props = {
open: boolean;
providerDisplayName: string;
modelsToRemove: Array<{ modelId: string; modelName: string }>;
onConfirm: () => void;
onCancel: () => void;
};

export function DisableProviderConfirmDialog({
open,
providerDisplayName,
modelsToRemove,
onConfirm,
onCancel,
}: Props) {
return (
<Dialog open={open} onOpenChange={isOpen => !isOpen && onCancel()}>
<DialogContent>
<DialogHeader>
<DialogTitle>Disable {providerDisplayName}?</DialogTitle>
<DialogDescription>
Disabling this provider will also deselect {modelsToRemove.length}{' '}
{modelsToRemove.length === 1 ? 'model' : 'models'} that would have no remaining enabled
providers.
</DialogDescription>
</DialogHeader>

<div className="max-h-60 overflow-y-auto rounded-lg border">
<div className="bg-muted/50 border-b px-4 py-2 text-sm font-medium">
Models to be deselected
</div>
<div className="divide-y">
{modelsToRemove.map(model => (
<div key={model.modelId} className="px-4 py-2">
<div className="text-sm font-medium">{model.modelName}</div>
<div className="text-muted-foreground text-xs">{model.modelId}</div>
</div>
))}
</div>
</div>

<DialogFooter>
<Button type="button" variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button type="button" onClick={onConfirm}>
Disable Provider
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { OrganizationRole } from '@/lib/organizations/organization-types';
import {
useOrganizationWithMembers,
Expand All @@ -19,6 +19,7 @@ import { ModelsTab } from '@/components/organizations/providers-and-models/Model
import { ProvidersTab } from '@/components/organizations/providers-and-models/ProvidersTab';
import { ModelDetailsDialog } from '@/components/organizations/providers-and-models/ModelDetailsDialog';
import { ProviderDetailsDialog } from '@/components/organizations/providers-and-models/ProviderDetailsDialog';
import { DisableProviderConfirmDialog } from '@/components/organizations/providers-and-models/DisableProviderConfirmDialog';
import type {
ModelRow,
ProviderModelRow,
Expand All @@ -30,6 +31,7 @@ import {
type ProviderPolicyFilter,
} from '@/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState';
import { preferredModels } from '@/lib/models';
import { computeModelsOnlyFromProvider } from '@/components/organizations/providers-and-models/allowLists.domain';

type Props = {
organizationId: string;
Expand Down Expand Up @@ -109,6 +111,12 @@ export function OrganizationProvidersAndModelsPage({ organizationId, role }: Pro
openRouterProviders,
});

const [pendingProviderDisable, setPendingProviderDisable] = useState<{
providerSlug: string;
providerDisplayName: string;
modelsToRemove: Array<{ modelId: string; modelName: string }>;
} | null>(null);

const preferredIndexByModelId = useMemo(() => {
const index = new Map<string, number>();
for (let i = 0; i < preferredModels.length; i++) {
Expand Down Expand Up @@ -161,17 +169,87 @@ export function OrganizationProvidersAndModelsPage({ organizationId, role }: Pro
const handleToggleProviderEnabled = useCallback(
(providerSlug: string, nextEnabled: boolean) => {
if (!canEdit) return;
actions.toggleProvider({ providerSlug, nextEnabled });
if (state.status !== 'ready') return;

// If enabling, just toggle directly
if (nextEnabled) {
actions.toggleProvider({ providerSlug, nextEnabled });
return;
}

// If disabling, check if any models would be orphaned
const orphanedModelIds = computeModelsOnlyFromProvider({
providerSlug,
draftModelAllowList: state.draftModelAllowList,
draftProviderAllowList: state.draftProviderAllowList,
allProviderSlugsWithEndpoints: selectors.allProviderSlugsWithEndpoints,
modelProvidersIndex: selectors.modelProvidersIndex,
});

// Filter to only include models that are currently enabled (have at least one enabled provider)
// This prevents showing models that are already disabled in the confirmation dialog
const currentlyEnabledOrphanedModelIds = orphanedModelIds.filter(modelId =>
allowedModelIds.has(modelId)
);

if (currentlyEnabledOrphanedModelIds.length === 0) {
// No currently enabled models would be orphaned, proceed directly
actions.toggleProvider({ providerSlug, nextEnabled });
return;
}

// Find provider display name and model details
const provider = openRouterProviders.find(p => p.slug === providerSlug);
const providerDisplayName = provider?.displayName ?? providerSlug;

const modelsToRemove = currentlyEnabledOrphanedModelIds
.map(modelId => {
const model = openRouterModels.find(m => normalizeModelId(m.slug) === modelId);
return model ? { modelId, modelName: model.name } : { modelId, modelName: modelId };
})
.sort((a, b) => a.modelName.localeCompare(b.modelName));

// Show confirmation dialog
setPendingProviderDisable({
providerSlug,
providerDisplayName,
modelsToRemove,
});
},
[actions, canEdit]
[
actions,
allowedModelIds,
canEdit,
openRouterModels,
openRouterProviders,
selectors.allProviderSlugsWithEndpoints,
selectors.modelProvidersIndex,
state,
]
);

const handleToggleModelAllowed = useCallback(
(modelId: string, nextAllowed: boolean) => {
if (!canEdit) return;

// If trying to enable a model, check if it has any enabled providers
if (nextAllowed) {
const providersForModel = selectors.modelProvidersIndex.get(modelId);
if (providersForModel) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Enabling a model with no providers may silently fail

providersForModel is only checked when truthy. If selectors.modelProvidersIndex.get(modelId) returns undefined (model missing from the index), enabling proceeds via actions.toggleModel, but later computeAllowedModelIds(..., enabledProviderSlugs) will currently drop models with unknown providers. Consider treating a missing index entry as "no enabled providers" (open details / block enable) to avoid a confusing toggle that doesn"t stick.

const hasEnabledProvider = [...providersForModel].some(providerSlug =>
enabledProviderSlugs.has(providerSlug)
);
if (!hasEnabledProvider) {
// No enabled providers - open the model details dialog instead
actions.setInfoModelId(modelId);
return;
}
}
}

actions.toggleModel({ modelId, nextAllowed });
},
[actions, canEdit]
[actions, canEdit, enabledProviderSlugs, selectors.modelProvidersIndex]
);

const handleToggleAllowFutureModelsForProvider = useCallback(
Expand Down Expand Up @@ -542,6 +620,22 @@ export function OrganizationProvidersAndModelsPage({ organizationId, role }: Pro
onClose={() => actions.setInfoProviderSlug(null)}
/>

<DisableProviderConfirmDialog
open={pendingProviderDisable !== null}
providerDisplayName={pendingProviderDisable?.providerDisplayName ?? ''}
modelsToRemove={pendingProviderDisable?.modelsToRemove ?? []}
onConfirm={() => {
if (pendingProviderDisable) {
actions.toggleProvider({
providerSlug: pendingProviderDisable.providerSlug,
nextEnabled: false,
});
setPendingProviderDisable(null);
}
}}
onCancel={() => setPendingProviderDisable(null)}
/>

<ModelSelectionStatusBar
isVisible={hasUnsavedChanges}
selectedProvidersCount={enabledProviderSlugs.size}
Expand Down
Loading