From 08ac5608291452be017b85ddfb82b24919be9042 Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Sat, 14 Feb 2026 22:15:00 +0000 Subject: [PATCH 1/9] Fix: Remove orphaned models when disabling providers When a provider is deselected on the Providers tab, models that would have zero enabled providers are now automatically removed from the selection. A confirmation dialog is shown to the user listing which models will be affected before proceeding. Changes: - Add computeModelsOnlyFromProvider() helper to identify orphaned models - Update toggleProviderEnabled() to remove orphaned models - Add DisableProviderConfirmDialog component - Update OrganizationProvidersAndModelsPage to show confirmation - Add comprehensive unit tests --- .husky/_/post-checkout | 3 + .husky/_/post-commit | 3 + .husky/_/post-merge | 3 + .husky/_/pre-push | 3 + .kilocode/session-id | 1 + .../DisableProviderConfirmDialog.tsx | 63 ++++++++++++++ .../OrganizationProvidersAndModelsPage.tsx | 79 ++++++++++++++++- .../allowLists.domain.test.ts | 84 +++++++++++++++++++ .../providers-and-models/allowLists.domain.ts | 69 +++++++++++++++ ...eProvidersAndModelsAllowListsState.test.ts | 7 ++ .../useProvidersAndModelsAllowListsState.ts | 5 +- 11 files changed, 316 insertions(+), 4 deletions(-) create mode 100755 .husky/_/post-checkout create mode 100755 .husky/_/post-commit create mode 100755 .husky/_/post-merge create mode 100755 .husky/_/pre-push create mode 100644 .kilocode/session-id create mode 100644 src/components/organizations/providers-and-models/DisableProviderConfirmDialog.tsx diff --git a/.husky/_/post-checkout b/.husky/_/post-checkout new file mode 100755 index 000000000..5abf8ed93 --- /dev/null +++ b/.husky/_/post-checkout @@ -0,0 +1,3 @@ +#!/bin/sh +command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-checkout' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; } +git lfs post-checkout "$@" diff --git a/.husky/_/post-commit b/.husky/_/post-commit new file mode 100755 index 000000000..b8b76c2c4 --- /dev/null +++ b/.husky/_/post-commit @@ -0,0 +1,3 @@ +#!/bin/sh +command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-commit' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; } +git lfs post-commit "$@" diff --git a/.husky/_/post-merge b/.husky/_/post-merge new file mode 100755 index 000000000..726f90989 --- /dev/null +++ b/.husky/_/post-merge @@ -0,0 +1,3 @@ +#!/bin/sh +command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-merge' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; } +git lfs post-merge "$@" diff --git a/.husky/_/pre-push b/.husky/_/pre-push new file mode 100755 index 000000000..5f26dc455 --- /dev/null +++ b/.husky/_/pre-push @@ -0,0 +1,3 @@ +#!/bin/sh +command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; } +git lfs pre-push "$@" diff --git a/.kilocode/session-id b/.kilocode/session-id new file mode 100644 index 000000000..6c7f6822a --- /dev/null +++ b/.kilocode/session-id @@ -0,0 +1 @@ +agent_1771105235131_gd6k7n \ No newline at end of file diff --git a/src/components/organizations/providers-and-models/DisableProviderConfirmDialog.tsx b/src/components/organizations/providers-and-models/DisableProviderConfirmDialog.tsx new file mode 100644 index 000000000..50e811023 --- /dev/null +++ b/src/components/organizations/providers-and-models/DisableProviderConfirmDialog.tsx @@ -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 ( + !isOpen && onCancel()}> + + + Disable {providerDisplayName}? + + Disabling this provider will also deselect {modelsToRemove.length}{' '} + {modelsToRemove.length === 1 ? 'model' : 'models'} that would have no remaining enabled + providers. + + + +
+
+ Models to be deselected +
+
+ {modelsToRemove.map(model => ( +
+
{model.modelName}
+
{model.modelId}
+
+ ))} +
+
+ + + + + +
+
+ ); +} diff --git a/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx b/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx index 06112cbc4..6281eaed0 100644 --- a/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx +++ b/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx @@ -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, @@ -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, @@ -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; @@ -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(); for (let i = 0; i < preferredModels.length; i++) { @@ -161,9 +169,58 @@ 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, + }); + + if (orphanedModelIds.length === 0) { + // No orphaned models, 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?.name ?? providerSlug; + + const modelsToRemove = orphanedModelIds + .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, + canEdit, + openRouterModels, + openRouterProviders, + selectors.allProviderSlugsWithEndpoints, + selectors.modelProvidersIndex, + state, + ] ); const handleToggleModelAllowed = useCallback( @@ -542,6 +599,22 @@ export function OrganizationProvidersAndModelsPage({ organizationId, role }: Pro onClose={() => actions.setInfoProviderSlug(null)} /> + { + if (pendingProviderDisable) { + actions.toggleProvider({ + providerSlug: pendingProviderDisable.providerSlug, + nextEnabled: false, + }); + setPendingProviderDisable(null); + } + }} + onCancel={() => setPendingProviderDisable(null)} + /> + { expect(next).toEqual([]); }); + + test('computeModelsOnlyFromProvider returns models that would have zero enabled providers', () => { + const modelProvidersIndex = buildModelProvidersIndex([ + { + slug: 'cerebras', + models: [ + { slug: 'cerebras/llama-70b', endpoint: {} }, + { slug: 'shared/model-1', endpoint: {} }, + ], + }, + { + slug: 'openai', + models: [{ slug: 'shared/model-1', endpoint: {} }], + }, + ]); + + const orphaned = computeModelsOnlyFromProvider({ + providerSlug: 'cerebras', + draftModelAllowList: ['cerebras/llama-70b', 'shared/model-1'], + draftProviderAllowList: ['cerebras', 'openai'], + allProviderSlugsWithEndpoints: ['cerebras', 'openai'], + modelProvidersIndex, + }); + + // cerebras/llama-70b is only from cerebras, so it's orphaned + // shared/model-1 is also from openai, so it's NOT orphaned + expect(orphaned).toEqual(['cerebras/llama-70b']); + }); + + test('computeModelsOnlyFromProvider returns empty when no models would be orphaned', () => { + const modelProvidersIndex = buildModelProvidersIndex([ + { + slug: 'cerebras', + models: [{ slug: 'shared/model-1', endpoint: {} }], + }, + { + slug: 'openai', + models: [{ slug: 'shared/model-1', endpoint: {} }], + }, + ]); + + const orphaned = computeModelsOnlyFromProvider({ + providerSlug: 'cerebras', + draftModelAllowList: ['shared/model-1'], + draftProviderAllowList: ['cerebras', 'openai'], + allProviderSlugsWithEndpoints: ['cerebras', 'openai'], + modelProvidersIndex, + }); + + expect(orphaned).toEqual([]); + }); + + test('toggleProviderEnabled(disable) removes orphaned models when modelProvidersIndex is provided', () => { + const modelProvidersIndex = buildModelProvidersIndex([ + { + slug: 'cerebras', + models: [ + { slug: 'cerebras/llama-70b', endpoint: {} }, + { slug: 'shared/model-1', endpoint: {} }, + ], + }, + { + slug: 'openai', + models: [{ slug: 'openai/gpt-4.1', endpoint: {} }, { slug: 'shared/model-1', endpoint: {} }], + }, + ]); + + const { nextModelAllowList, nextProviderAllowList } = toggleProviderEnabled({ + providerSlug: 'cerebras', + nextEnabled: false, + draftProviderAllowList: ['cerebras', 'openai'], + draftModelAllowList: ['cerebras/llama-70b', 'openai/gpt-4.1', 'shared/model-1'], + allProviderSlugsWithEndpoints: ['cerebras', 'openai'], + hadAllProvidersInitially: false, + modelProvidersIndex, + }); + + // cerebras/llama-70b should be removed (only from cerebras) + // shared/model-1 should remain (also from openai) + // openai/gpt-4.1 should remain (from openai) + expect(nextModelAllowList.sort()).toEqual(['openai/gpt-4.1', 'shared/model-1']); + expect(nextProviderAllowList).toEqual(['openai']); + }); }); diff --git a/src/components/organizations/providers-and-models/allowLists.domain.ts b/src/components/organizations/providers-and-models/allowLists.domain.ts index c6445c956..4737ae52f 100644 --- a/src/components/organizations/providers-and-models/allowLists.domain.ts +++ b/src/components/organizations/providers-and-models/allowLists.domain.ts @@ -120,6 +120,54 @@ export function computeAllowedModelIds( return allowed; } +/** + * Compute which models from the current allow list would have zero enabled providers + * if the given provider were disabled. + */ +export function computeModelsOnlyFromProvider(params: { + providerSlug: string; + draftModelAllowList: ReadonlyArray; + draftProviderAllowList: ReadonlyArray; + allProviderSlugsWithEndpoints: ReadonlyArray; + modelProvidersIndex: Map>; +}): string[] { + const { + providerSlug, + draftModelAllowList, + draftProviderAllowList, + allProviderSlugsWithEndpoints, + modelProvidersIndex, + } = params; + + // Compute which providers would remain enabled after disabling this one + const enabledAfterDisable = computeEnabledProviderSlugs( + draftProviderAllowList.length === 0 + ? allProviderSlugsWithEndpoints.filter(slug => slug !== providerSlug) + : draftProviderAllowList.filter(slug => slug !== providerSlug), + allProviderSlugsWithEndpoints + ); + + const orphanedModels: string[] = []; + + // Check each model in the allow list + for (const entry of draftModelAllowList) { + // Skip wildcards + if (entry.endsWith('/*')) continue; + + const modelId = normalizeModelId(entry); + const providersForModel = modelProvidersIndex.get(modelId); + if (!providersForModel) continue; + + // Check if this model has any enabled providers remaining + const hasEnabledProvider = [...providersForModel].some(p => enabledAfterDisable.has(p)); + if (!hasEnabledProvider) { + orphanedModels.push(modelId); + } + } + + return orphanedModels; +} + export function toggleProviderEnabled(params: { providerSlug: string; nextEnabled: boolean; @@ -127,6 +175,7 @@ export function toggleProviderEnabled(params: { draftModelAllowList: ReadonlyArray; allProviderSlugsWithEndpoints: ReadonlyArray; hadAllProvidersInitially: boolean; + modelProvidersIndex?: Map>; }): { nextProviderAllowList: string[]; nextModelAllowList: string[] } { const { providerSlug, @@ -135,12 +184,32 @@ export function toggleProviderEnabled(params: { draftModelAllowList, allProviderSlugsWithEndpoints, hadAllProvidersInitially, + modelProvidersIndex, } = params; let nextModelAllowList = [...draftModelAllowList]; if (!nextEnabled) { if (nextModelAllowList.length !== 0) { + // Remove provider wildcard nextModelAllowList = nextModelAllowList.filter(entry => entry !== `${providerSlug}/*`); + + // Remove models that would have zero enabled providers + if (modelProvidersIndex) { + const orphanedModels = computeModelsOnlyFromProvider({ + providerSlug, + draftModelAllowList: nextModelAllowList, + draftProviderAllowList, + allProviderSlugsWithEndpoints, + modelProvidersIndex, + }); + if (orphanedModels.length > 0) { + const orphanedSet = new Set(orphanedModels); + nextModelAllowList = nextModelAllowList.filter(entry => { + if (entry.endsWith('/*')) return true; + return !orphanedSet.has(normalizeModelId(entry)); + }); + } + } } } nextModelAllowList = canonicalizeModelAllowList(nextModelAllowList); diff --git a/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.test.ts b/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.test.ts index c74a87224..2b395f595 100644 --- a/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.test.ts +++ b/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.test.ts @@ -4,6 +4,7 @@ import { providersAndModelsAllowListsReducer, type ProvidersAndModelsAllowListsState, } from '@/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState'; +import { buildModelProvidersIndex } from '@/components/organizations/providers-and-models/allowLists.domain'; describe('providersAndModelsAllowListsReducer', () => { test('init -> toggle -> reset returns to initial', () => { @@ -50,11 +51,17 @@ describe('providersAndModelsAllowListsReducer', () => { throw new Error('expected ready state'); } + const modelProvidersIndex = buildModelProvidersIndex([ + { slug: 'openai', models: [] }, + { slug: 'anthropic', models: [] }, + ]); + state = providersAndModelsAllowListsReducer(state, { type: 'TOGGLE_PROVIDER', providerSlug: 'openai', nextEnabled: false, allProviderSlugsWithEndpoints: ['openai', 'anthropic'], + modelProvidersIndex, }); state = providersAndModelsAllowListsReducer(state, { type: 'MARK_SAVED' }); diff --git a/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.ts b/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.ts index 8922c271b..1654ef402 100644 --- a/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.ts +++ b/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.ts @@ -61,6 +61,7 @@ export type ProvidersAndModelsAllowListsAction = providerSlug: string; nextEnabled: boolean; allProviderSlugsWithEndpoints: ReadonlyArray; + modelProvidersIndex: Map>; } | { type: 'TOGGLE_MODEL'; @@ -168,6 +169,7 @@ export function providersAndModelsAllowListsReducer( draftModelAllowList: state.draftModelAllowList, allProviderSlugsWithEndpoints: action.allProviderSlugsWithEndpoints, hadAllProvidersInitially: state.initialProviderAllowList.length === 0, + modelProvidersIndex: action.modelProvidersIndex, }); return { ...state, @@ -360,9 +362,10 @@ export function useProvidersAndModelsAllowListsState(params: { providerSlug: input.providerSlug, nextEnabled: input.nextEnabled, allProviderSlugsWithEndpoints, + modelProvidersIndex, }); }, - [allProviderSlugsWithEndpoints] + [allProviderSlugsWithEndpoints, modelProvidersIndex] ); const toggleModel = useCallback( From 720b28d298fe44c88f0e5ee48489debfdb6f4ef2 Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Mon, 16 Feb 2026 12:48:41 +0000 Subject: [PATCH 2/9] feat(providers-models): filter allowed models by enabled providers Add logic to prevent enabling models when all their providers are disabled. When attempting to enable a model without any enabled providers, the model details dialog is opened instead. --- .../OrganizationProvidersAndModelsPage.tsx | 22 ++++-- .../allowLists.domain.test.ts | 68 ++++++++++++++++++- .../providers-and-models/allowLists.domain.ts | 25 ++++++- .../useProvidersAndModelsAllowListsState.ts | 9 ++- 4 files changed, 114 insertions(+), 10 deletions(-) diff --git a/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx b/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx index 6281eaed0..59bddc0c7 100644 --- a/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx +++ b/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx @@ -199,9 +199,7 @@ export function OrganizationProvidersAndModelsPage({ organizationId, role }: Pro const modelsToRemove = orphanedModelIds .map(modelId => { const model = openRouterModels.find(m => normalizeModelId(m.slug) === modelId); - return model - ? { modelId, modelName: model.name } - : { modelId, modelName: modelId }; + return model ? { modelId, modelName: model.name } : { modelId, modelName: modelId }; }) .sort((a, b) => a.modelName.localeCompare(b.modelName)); @@ -226,9 +224,25 @@ export function OrganizationProvidersAndModelsPage({ organizationId, role }: Pro 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) { + 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( diff --git a/src/components/organizations/providers-and-models/allowLists.domain.test.ts b/src/components/organizations/providers-and-models/allowLists.domain.test.ts index dd09ff35c..ea9adce80 100644 --- a/src/components/organizations/providers-and-models/allowLists.domain.test.ts +++ b/src/components/organizations/providers-and-models/allowLists.domain.test.ts @@ -145,7 +145,10 @@ describe('allowLists.domain', () => { }, { slug: 'openai', - models: [{ slug: 'openai/gpt-4.1', endpoint: {} }, { slug: 'shared/model-1', endpoint: {} }], + models: [ + { slug: 'openai/gpt-4.1', endpoint: {} }, + { slug: 'shared/model-1', endpoint: {} }, + ], }, ]); @@ -165,4 +168,67 @@ describe('allowLists.domain', () => { expect(nextModelAllowList.sort()).toEqual(['openai/gpt-4.1', 'shared/model-1']); expect(nextProviderAllowList).toEqual(['openai']); }); + + test('computeAllowedModelIds filters out models when their providers are disabled', () => { + const openRouterModels = [ + { slug: 'cerebras/llama-70b' }, + { slug: 'openai/gpt-4.1' }, + { slug: 'shared/model-1' }, + ]; + const openRouterProviders = [ + { + slug: 'cerebras', + models: [ + { slug: 'cerebras/llama-70b', endpoint: {} }, + { slug: 'shared/model-1', endpoint: {} }, + ], + }, + { + slug: 'openai', + models: [ + { slug: 'openai/gpt-4.1', endpoint: {} }, + { slug: 'shared/model-1', endpoint: {} }, + ], + }, + ]; + + // All models are in the allow list + const modelAllowList = ['cerebras/llama-70b', 'openai/gpt-4.1', 'shared/model-1']; + + // But only openai provider is enabled + const enabledProviderSlugs = new Set(['openai']); + + const allowed = computeAllowedModelIds( + modelAllowList, + openRouterModels, + openRouterProviders, + enabledProviderSlugs + ); + + // cerebras/llama-70b should be filtered out (only from cerebras, which is disabled) + // openai/gpt-4.1 should be included (from openai, which is enabled) + // shared/model-1 should be included (available from openai, which is enabled) + expect([...allowed].sort()).toEqual(['openai/gpt-4.1', 'shared/model-1']); + }); + + test('computeAllowedModelIds includes all models when enabledProviderSlugs is not provided', () => { + const openRouterModels = [{ slug: 'cerebras/llama-70b' }, { slug: 'openai/gpt-4.1' }]; + const openRouterProviders = [ + { + slug: 'cerebras', + models: [{ slug: 'cerebras/llama-70b', endpoint: {} }], + }, + { + slug: 'openai', + models: [{ slug: 'openai/gpt-4.1', endpoint: {} }], + }, + ]; + + const modelAllowList = ['cerebras/llama-70b', 'openai/gpt-4.1']; + + // Not passing enabledProviderSlugs - should include all models in allow list + const allowed = computeAllowedModelIds(modelAllowList, openRouterModels, openRouterProviders); + + expect([...allowed].sort()).toEqual(['cerebras/llama-70b', 'openai/gpt-4.1']); + }); }); diff --git a/src/components/organizations/providers-and-models/allowLists.domain.ts b/src/components/organizations/providers-and-models/allowLists.domain.ts index 4737ae52f..b155280f6 100644 --- a/src/components/organizations/providers-and-models/allowLists.domain.ts +++ b/src/components/organizations/providers-and-models/allowLists.domain.ts @@ -93,7 +93,8 @@ export function computeEnabledProviderSlugs( export function computeAllowedModelIds( draftModelAllowList: ReadonlyArray, openRouterModels: ReadonlyArray, - openRouterProviders: OpenRouterProviderModelsSnapshot + openRouterProviders: OpenRouterProviderModelsSnapshot, + enabledProviderSlugs?: ReadonlySet ): Set { const allowed = new Set(); @@ -105,6 +106,8 @@ export function computeAllowedModelIds( } const allowListArray = [...draftModelAllowList]; + const modelProvidersIndex = buildModelProvidersIndex(openRouterProviders); + for (const model of openRouterModels) { const normalizedModelId = normalizeModelId(model.slug); const isAllowed = isModelAllowedProviderAwareClient( @@ -112,9 +115,25 @@ export function computeAllowedModelIds( allowListArray, openRouterProviders ); - if (isAllowed) { - allowed.add(normalizedModelId); + if (!isAllowed) { + continue; } + + // If enabledProviderSlugs is provided, also check that at least one provider + // offering this model is enabled + if (enabledProviderSlugs) { + const providersForModel = modelProvidersIndex.get(normalizedModelId); + if (providersForModel) { + const hasEnabledProvider = [...providersForModel].some(providerSlug => + enabledProviderSlugs.has(providerSlug) + ); + if (!hasEnabledProvider) { + continue; + } + } + } + + allowed.add(normalizedModelId); } return allowed; diff --git a/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.ts b/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.ts index 1654ef402..4332e6567 100644 --- a/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.ts +++ b/src/components/organizations/providers-and-models/useProvidersAndModelsAllowListsState.ts @@ -321,8 +321,13 @@ export function useProvidersAndModelsAllowListsState(params: { const allowedModelIds = useMemo(() => { if (!draftModelAllowList) return new Set(); - return computeAllowedModelIds(draftModelAllowList, openRouterModels, openRouterProviders); - }, [draftModelAllowList, openRouterModels, openRouterProviders]); + return computeAllowedModelIds( + draftModelAllowList, + openRouterModels, + openRouterProviders, + enabledProviderSlugs + ); + }, [draftModelAllowList, openRouterModels, openRouterProviders, enabledProviderSlugs]); const hasUnsavedChanges = useMemo(() => { if ( From c51ed9f437d58b34b0bc7523854df4be8d181824 Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Mon, 16 Feb 2026 12:57:39 +0000 Subject: [PATCH 3/9] removing husky and session files --- .husky/_/post-checkout | 3 --- .husky/_/post-commit | 3 --- .husky/_/post-merge | 3 --- .husky/_/pre-push | 3 --- .kilocode/session-id | 1 - 5 files changed, 13 deletions(-) delete mode 100755 .husky/_/post-checkout delete mode 100755 .husky/_/post-commit delete mode 100755 .husky/_/post-merge delete mode 100755 .husky/_/pre-push delete mode 100644 .kilocode/session-id diff --git a/.husky/_/post-checkout b/.husky/_/post-checkout deleted file mode 100755 index 5abf8ed93..000000000 --- a/.husky/_/post-checkout +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-checkout' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; } -git lfs post-checkout "$@" diff --git a/.husky/_/post-commit b/.husky/_/post-commit deleted file mode 100755 index b8b76c2c4..000000000 --- a/.husky/_/post-commit +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-commit' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; } -git lfs post-commit "$@" diff --git a/.husky/_/post-merge b/.husky/_/post-merge deleted file mode 100755 index 726f90989..000000000 --- a/.husky/_/post-merge +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-merge' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; } -git lfs post-merge "$@" diff --git a/.husky/_/pre-push b/.husky/_/pre-push deleted file mode 100755 index 5f26dc455..000000000 --- a/.husky/_/pre-push +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; } -git lfs pre-push "$@" diff --git a/.kilocode/session-id b/.kilocode/session-id deleted file mode 100644 index 6c7f6822a..000000000 --- a/.kilocode/session-id +++ /dev/null @@ -1 +0,0 @@ -agent_1771105235131_gd6k7n \ No newline at end of file From a6867b629b782c5a458162b8af2954c84e83a1bc Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Mon, 16 Feb 2026 12:59:19 +0000 Subject: [PATCH 4/9] perf improvement --- .../providers-and-models/allowLists.domain.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/components/organizations/providers-and-models/allowLists.domain.ts b/src/components/organizations/providers-and-models/allowLists.domain.ts index b155280f6..3211dae93 100644 --- a/src/components/organizations/providers-and-models/allowLists.domain.ts +++ b/src/components/organizations/providers-and-models/allowLists.domain.ts @@ -124,9 +124,13 @@ export function computeAllowedModelIds( if (enabledProviderSlugs) { const providersForModel = modelProvidersIndex.get(normalizedModelId); if (providersForModel) { - const hasEnabledProvider = [...providersForModel].some(providerSlug => - enabledProviderSlugs.has(providerSlug) - ); + let hasEnabledProvider = false; + for (const providerSlug of providersForModel) { + if (enabledProviderSlugs.has(providerSlug)) { + hasEnabledProvider = true; + break; + } + } if (!hasEnabledProvider) { continue; } @@ -178,7 +182,13 @@ export function computeModelsOnlyFromProvider(params: { if (!providersForModel) continue; // Check if this model has any enabled providers remaining - const hasEnabledProvider = [...providersForModel].some(p => enabledAfterDisable.has(p)); + let hasEnabledProvider = false; + for (const p of providersForModel) { + if (enabledAfterDisable.has(p)) { + hasEnabledProvider = true; + break; + } + } if (!hasEnabledProvider) { orphanedModels.push(modelId); } From 5e04e98a3c6c5968c39dfb6103a4044e8c133db4 Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Mon, 16 Feb 2026 13:56:01 +0000 Subject: [PATCH 5/9] filter orphaned models by provider --- .../OrganizationProvidersAndModelsPage.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx b/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx index 59bddc0c7..33f5bff9f 100644 --- a/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx +++ b/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx @@ -186,8 +186,14 @@ export function OrganizationProvidersAndModelsPage({ organizationId, role }: Pro modelProvidersIndex: selectors.modelProvidersIndex, }); - if (orphanedModelIds.length === 0) { - // No orphaned models, proceed directly + // 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; } @@ -196,7 +202,7 @@ export function OrganizationProvidersAndModelsPage({ organizationId, role }: Pro const provider = openRouterProviders.find(p => p.slug === providerSlug); const providerDisplayName = provider?.name ?? providerSlug; - const modelsToRemove = orphanedModelIds + const modelsToRemove = currentlyEnabledOrphanedModelIds .map(modelId => { const model = openRouterModels.find(m => normalizeModelId(m.slug) === modelId); return model ? { modelId, modelName: model.name } : { modelId, modelName: modelId }; From 61c7fc061c88c3b116031cc4f5461c8990383e57 Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Mon, 16 Feb 2026 14:16:02 +0000 Subject: [PATCH 6/9] code review changes --- .../OrganizationProvidersAndModelsPage.tsx | 1 + .../providers-and-models/allowLists.domain.ts | 24 ++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx b/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx index 33f5bff9f..a4d0be19e 100644 --- a/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx +++ b/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx @@ -218,6 +218,7 @@ export function OrganizationProvidersAndModelsPage({ organizationId, role }: Pro }, [ actions, + allowedModelIds, canEdit, openRouterModels, openRouterProviders, diff --git a/src/components/organizations/providers-and-models/allowLists.domain.ts b/src/components/organizations/providers-and-models/allowLists.domain.ts index 3211dae93..60c53a9f4 100644 --- a/src/components/organizations/providers-and-models/allowLists.domain.ts +++ b/src/components/organizations/providers-and-models/allowLists.domain.ts @@ -97,16 +97,34 @@ export function computeAllowedModelIds( enabledProviderSlugs?: ReadonlySet ): Set { const allowed = new Set(); + const modelProvidersIndex = enabledProviderSlugs + ? buildModelProvidersIndex(openRouterProviders) + : null; if (draftModelAllowList.length === 0) { for (const model of openRouterModels) { - allowed.add(normalizeModelId(model.slug)); + const normalizedModelId = normalizeModelId(model.slug); + + // If enabledProviderSlugs is provided, filter models without enabled providers + if (enabledProviderSlugs && modelProvidersIndex) { + const providersForModel = modelProvidersIndex.get(normalizedModelId); + if (!providersForModel) continue; // Exclude models with unknown providers + let hasEnabledProvider = false; + for (const providerSlug of providersForModel) { + if (enabledProviderSlugs.has(providerSlug)) { + hasEnabledProvider = true; + break; + } + } + if (!hasEnabledProvider) continue; + } + + allowed.add(normalizedModelId); } return allowed; } const allowListArray = [...draftModelAllowList]; - const modelProvidersIndex = buildModelProvidersIndex(openRouterProviders); for (const model of openRouterModels) { const normalizedModelId = normalizeModelId(model.slug); @@ -121,7 +139,7 @@ export function computeAllowedModelIds( // If enabledProviderSlugs is provided, also check that at least one provider // offering this model is enabled - if (enabledProviderSlugs) { + if (enabledProviderSlugs && modelProvidersIndex) { const providersForModel = modelProvidersIndex.get(normalizedModelId); if (providersForModel) { let hasEnabledProvider = false; From d0a21b7898ab1e0b9aa5f1e8329c7392a34270af Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Mon, 16 Feb 2026 14:27:16 +0000 Subject: [PATCH 7/9] Exclude models with unknown providers --- .../providers-and-models/allowLists.domain.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/components/organizations/providers-and-models/allowLists.domain.ts b/src/components/organizations/providers-and-models/allowLists.domain.ts index 60c53a9f4..ba6f1ec6a 100644 --- a/src/components/organizations/providers-and-models/allowLists.domain.ts +++ b/src/components/organizations/providers-and-models/allowLists.domain.ts @@ -141,18 +141,17 @@ export function computeAllowedModelIds( // offering this model is enabled if (enabledProviderSlugs && modelProvidersIndex) { const providersForModel = modelProvidersIndex.get(normalizedModelId); - if (providersForModel) { - let hasEnabledProvider = false; - for (const providerSlug of providersForModel) { - if (enabledProviderSlugs.has(providerSlug)) { - hasEnabledProvider = true; - break; - } - } - if (!hasEnabledProvider) { - continue; + if (!providersForModel) continue; // Exclude models with unknown providers + let hasEnabledProvider = false; + for (const providerSlug of providersForModel) { + if (enabledProviderSlugs.has(providerSlug)) { + hasEnabledProvider = true; + break; } } + if (!hasEnabledProvider) { + continue; + } } allowed.add(normalizedModelId); From 21922e90737ee17feb99e1576b23f6d58f6e012a Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Mon, 16 Feb 2026 21:17:30 +0000 Subject: [PATCH 8/9] checking for wildcards too --- .../allowLists.domain.test.ts | 29 +++++++++++++++++++ .../providers-and-models/allowLists.domain.ts | 19 +++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/components/organizations/providers-and-models/allowLists.domain.test.ts b/src/components/organizations/providers-and-models/allowLists.domain.test.ts index ea9adce80..dc279fee1 100644 --- a/src/components/organizations/providers-and-models/allowLists.domain.test.ts +++ b/src/components/organizations/providers-and-models/allowLists.domain.test.ts @@ -231,4 +231,33 @@ describe('allowLists.domain', () => { expect([...allowed].sort()).toEqual(['cerebras/llama-70b', 'openai/gpt-4.1']); }); + + test('computeModelsOnlyFromProvider expands wildcards to detect orphaned models', () => { + const modelProvidersIndex = buildModelProvidersIndex([ + { + slug: 'cerebras', + models: [ + { slug: 'cerebras/llama-70b', endpoint: {} }, + { slug: 'shared/model-1', endpoint: {} }, + ], + }, + { + slug: 'openai', + models: [{ slug: 'shared/model-1', endpoint: {} }], + }, + ]); + + const orphaned = computeModelsOnlyFromProvider({ + providerSlug: 'cerebras', + draftModelAllowList: ['cerebras/*'], + draftProviderAllowList: ['cerebras', 'openai'], + allProviderSlugsWithEndpoints: ['cerebras', 'openai'], + modelProvidersIndex, + }); + + // cerebras/* expands to cerebras/llama-70b and shared/model-1 + // cerebras/llama-70b is only from cerebras, so it's orphaned + // shared/model-1 is also from openai, so it's NOT orphaned + expect(orphaned).toEqual(['cerebras/llama-70b']); + }); }); diff --git a/src/components/organizations/providers-and-models/allowLists.domain.ts b/src/components/organizations/providers-and-models/allowLists.domain.ts index ba6f1ec6a..b05c9c121 100644 --- a/src/components/organizations/providers-and-models/allowLists.domain.ts +++ b/src/components/organizations/providers-and-models/allowLists.domain.ts @@ -189,12 +189,23 @@ export function computeModelsOnlyFromProvider(params: { const orphanedModels: string[] = []; - // Check each model in the allow list + // Collect all model IDs to check, expanding wildcards to concrete models + const modelIdsToCheck = new Set(); for (const entry of draftModelAllowList) { - // Skip wildcards - if (entry.endsWith('/*')) continue; + if (entry.endsWith('/*')) { + const providerPrefix = entry.slice(0, -2); + for (const [modelId, providers] of modelProvidersIndex) { + if (providers.has(providerPrefix)) { + modelIdsToCheck.add(modelId); + } + } + } else { + modelIdsToCheck.add(normalizeModelId(entry)); + } + } - const modelId = normalizeModelId(entry); + // Check each model for orphan status + for (const modelId of modelIdsToCheck) { const providersForModel = modelProvidersIndex.get(modelId); if (!providersForModel) continue; From 5ce05e6f1704805ca84940a097a332e6753b5ae2 Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Mon, 16 Feb 2026 21:19:54 +0000 Subject: [PATCH 9/9] using displayname --- .../providers-and-models/OrganizationProvidersAndModelsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx b/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx index a4d0be19e..957843e5f 100644 --- a/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx +++ b/src/components/organizations/providers-and-models/OrganizationProvidersAndModelsPage.tsx @@ -200,7 +200,7 @@ export function OrganizationProvidersAndModelsPage({ organizationId, role }: Pro // Find provider display name and model details const provider = openRouterProviders.find(p => p.slug === providerSlug); - const providerDisplayName = provider?.name ?? providerSlug; + const providerDisplayName = provider?.displayName ?? providerSlug; const modelsToRemove = currentlyEnabledOrphanedModelIds .map(modelId => {