Skip to content

Commit a863686

Browse files
authored
fix(model): hide disabled provider models (#1574)
Prevent stale enabled models from remaining visible after a\nprovider is disabled or removed.\n\nKeep new-thread, status bar, and MCP sampling selection logic\naligned with the active provider set.
1 parent cd5077a commit a863686

5 files changed

Lines changed: 160 additions & 46 deletions

File tree

src/renderer/src/components/chat/ChatStatusBar.vue

Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,9 @@ const providerNameMap = computed(() => {
12021202
})
12031203
return map
12041204
})
1205+
const activeEnabledModelGroups = computed(
1206+
() => modelStore.activeEnabledModels ?? modelStore.enabledModels
1207+
)
12051208
const isModelOptionsReady = computed(() => isAcpAgent.value || modelStore.initialized)
12061209
const hasModelOptionsError = computed(
12071210
() => !isAcpAgent.value && !modelStore.initialized && Boolean(modelStore.initializationError)
@@ -1221,41 +1224,23 @@ const modelGroups = computed<GroupedModelList[]>(() => {
12211224
return []
12221225
}
12231226
1224-
const groupsById = new Map(
1225-
modelStore.enabledModels
1226-
.filter((group) => group.providerId !== 'acp')
1227-
.map((group) => [group.providerId, getChatSelectableModels(group.models)] as const)
1228-
.filter(([, models]) => models.length > 0)
1229-
)
1230-
1231-
const result: GroupedModelList[] = []
1232-
1233-
providerStore.sortedProviders
1227+
return providerStore.sortedProviders
12341228
.filter((provider) => provider.enable && provider.id !== 'acp')
1235-
.forEach((provider) => {
1236-
const models = groupsById.get(provider.id)
1237-
if (!models || models.length === 0) {
1238-
return
1229+
.map((provider) => {
1230+
const enabledGroup = activeEnabledModelGroups.value.find(
1231+
(group) => group.providerId === provider.id
1232+
)
1233+
const models = enabledGroup ? getChatSelectableModels(enabledGroup.models) : []
1234+
if (models.length === 0) {
1235+
return null
12391236
}
1240-
result.push({
1237+
return {
12411238
providerId: provider.id,
12421239
providerName: provider.name,
12431240
models
1244-
})
1245-
groupsById.delete(provider.id)
1246-
})
1247-
1248-
Array.from(groupsById.entries())
1249-
.sort(([left], [right]) => left.localeCompare(right))
1250-
.forEach(([providerId, models]) => {
1251-
result.push({
1252-
providerId,
1253-
providerName: providerNameMap.value.get(providerId) ?? providerId,
1254-
models
1255-
})
1241+
}
12561242
})
1257-
1258-
return result
1243+
.filter((group): group is GroupedModelList => group !== null)
12591244
})
12601245
12611246
const filteredModelGroups = computed<GroupedModelList[]>(() => {
@@ -1609,7 +1594,7 @@ const getAcpOptionDisplayValue = (option: AcpConfigOption): string => {
16091594
}
16101595
16111596
const findEnabledModelMeta = (providerId: string, modelId: string): RENDERER_MODEL_META | null => {
1612-
const group = modelStore.enabledModels.find((item) => item.providerId === providerId)
1597+
const group = activeEnabledModelGroups.value.find((item) => item.providerId === providerId)
16131598
return (
16141599
group?.models.find((model) => model.id === modelId && isChatSelectableModelType(model.type)) ??
16151600
null
@@ -1762,14 +1747,14 @@ const findEnabledModel = (providerId: string, modelId: string): ModelSelection |
17621747
}
17631748
17641749
const pickFirstEnabledModel = (): ModelSelection | null => {
1765-
for (const group of modelStore.enabledModels) {
1750+
for (const group of activeEnabledModelGroups.value) {
17661751
if (group.providerId === 'acp') continue
17671752
const firstModel = group.models.find((model) => isChatSelectableModelType(model.type))
17681753
if (firstModel) {
17691754
return { providerId: group.providerId, modelId: firstModel.id }
17701755
}
17711756
}
1772-
for (const group of modelStore.enabledModels) {
1757+
for (const group of activeEnabledModelGroups.value) {
17731758
const firstModel = group.models.find((model) => isChatSelectableModelType(model.type))
17741759
if (firstModel) {
17751760
return { providerId: group.providerId, modelId: firstModel.id }
@@ -2584,7 +2569,7 @@ watch(
25842569
isAcpAgent,
25852570
() => agentStore.selectedAgentId,
25862571
() => modelStore.initialized,
2587-
() => modelStore.enabledModels
2572+
() => activeEnabledModelGroups.value
25882573
],
25892574
() => {
25902575
if (hasActiveSession.value) return

src/renderer/src/pages/NewThreadPage.vue

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ const draftStore = useDraftStore()
132132
const configClient = createConfigClient()
133133
const sessionClient = createSessionClient()
134134
const { t } = useI18n()
135+
const activeEnabledModelGroups = computed(
136+
() => modelStore.activeEnabledModels ?? modelStore.enabledModels
137+
)
135138
136139
const message = ref('')
137140
const attachedFiles = ref<MessageFile[]>([])
@@ -205,7 +208,7 @@ const getEnabledModel = (
205208
modelId?: string
206209
): { providerId: string; modelId: string } | null => {
207210
if (!providerId || !modelId) return null
208-
const matched = modelStore.enabledModels.some(
211+
const matched = activeEnabledModelGroups.value.some(
209212
(group) =>
210213
group.providerId === providerId &&
211214
group.models.some((model) => model.id === modelId && isChatSelectableModel(model))
@@ -261,7 +264,7 @@ async function resolveModel(): Promise<{ providerId: string; modelId: string } |
261264
}
262265
263266
// 3. First available enabled model
264-
for (const group of modelStore.enabledModels) {
267+
for (const group of activeEnabledModelGroups.value) {
265268
const firstChatSelectableModel = group.models.find(isChatSelectableModel)
266269
if (firstChatSelectableModel) {
267270
return { providerId: group.providerId, modelId: firstChatSelectableModel.id }
@@ -289,7 +292,7 @@ const resolveStartModelSelection = (
289292
return null
290293
}
291294
292-
for (const group of modelStore.enabledModels) {
295+
for (const group of activeEnabledModelGroups.value) {
293296
const matched = group.models.find(
294297
(model) => model.id.toLowerCase() === normalizedModelId && isChatSelectableModel(model)
295298
)
@@ -298,7 +301,7 @@ const resolveStartModelSelection = (
298301
}
299302
}
300303
301-
for (const group of modelStore.enabledModels) {
304+
for (const group of activeEnabledModelGroups.value) {
302305
const matched = group.models.find(
303306
(model) => model.id.toLowerCase().includes(normalizedModelId) && isChatSelectableModel(model)
304307
)

src/renderer/src/stores/mcpSampling.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ export const useMcpSamplingStore = defineStore('mcpSampling', () => {
118118

119119
const requiresVision = computed(() => request.value?.requiresVision ?? false)
120120
const selectedModelSupportsVision = computed(() => selectedModel.value?.vision ?? false)
121+
const activeEnabledModelGroups = computed(
122+
() => modelStore.activeEnabledModels ?? modelStore.enabledModels
123+
)
121124
const selectedProviderLabel = computed(() => {
122125
if (!selectedProviderId.value) {
123126
return null
@@ -177,7 +180,7 @@ export const useMcpSamplingStore = defineStore('mcpSampling', () => {
177180
: null
178181

179182
const selection = resolveSamplingDefaultModel({
180-
enabledModels: modelStore.enabledModels,
183+
enabledModels: activeEnabledModelGroups.value,
181184
providerOrder,
182185
requiresVision: requiresVision.value,
183186
activeSelection,
@@ -194,7 +197,7 @@ export const useMcpSamplingStore = defineStore('mcpSampling', () => {
194197
}
195198

196199
const requiresVisionValue = requiresVision.value
197-
return modelStore.enabledModels.some((entry) =>
200+
return activeEnabledModelGroups.value.some((entry) =>
198201
entry.models.some((model) => !requiresVisionValue || model.vision)
199202
)
200203
})
@@ -250,7 +253,7 @@ export const useMcpSamplingStore = defineStore('mcpSampling', () => {
250253
return false
251254
}
252255

253-
const providerEntry = modelStore.enabledModels.find(
256+
const providerEntry = activeEnabledModelGroups.value.find(
254257
(entry) => entry.providerId === sessionInfo.providerId
255258
)
256259

src/renderer/src/stores/modelStore.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { computed, type ComputedRef, readonly, ref } from 'vue'
1+
import { computed, type ComputedRef, readonly, ref, watch } from 'vue'
22
import { defineStore } from 'pinia'
33
import { useQueryCache, type DataState, type EntryKey, type UseQueryEntry } from '@pinia/colada'
44
import { useThrottleFn } from '@vueuse/core'
@@ -55,6 +55,15 @@ export const useModelStore = defineStore('model', () => {
5555
const pendingRefreshStarts = new Set<string>()
5656
const pendingModelStatusEchoes = new Map<string, boolean>()
5757
const providerModelsReadyAt = new Map<string, number>()
58+
const activeProviderIds = computed(
59+
() =>
60+
new Set(
61+
providerStore.providers.filter((provider) => provider.enable).map((provider) => provider.id)
62+
)
63+
)
64+
const activeEnabledModels = computed(() =>
65+
enabledModels.value.filter((group) => activeProviderIds.value.has(group.providerId))
66+
)
5867

5968
const MODEL_TOGGLE_PERF_LOG_PREFIX = '[ModelTogglePerf]'
6069
const getPerfNow = () => (typeof performance !== 'undefined' ? performance.now() : Date.now())
@@ -109,6 +118,43 @@ export const useModelStore = defineStore('model', () => {
109118
).filter((providerId): providerId is string => Boolean(providerId))
110119
}
111120

121+
const removeProviderGroups = (
122+
groups: { providerId: string; models: RENDERER_MODEL_META[] }[],
123+
providerId: string
124+
) => {
125+
return groups.filter((group) => group.providerId !== providerId)
126+
}
127+
128+
const purgeRemovedProviderState = (providerId: string) => {
129+
allProviderModels.value = removeProviderGroups(allProviderModels.value, providerId)
130+
customModels.value = removeProviderGroups(customModels.value, providerId)
131+
enabledModels.value = removeProviderGroups(enabledModels.value, providerId)
132+
providerModelQueries.delete(providerId)
133+
customModelQueries.delete(providerId)
134+
enabledModelQueries.delete(providerId)
135+
pendingRefreshStarts.delete(providerId)
136+
rerunRequested.delete(providerId)
137+
clearProviderModelsReady(providerId)
138+
139+
for (const statusKey of Array.from(pendingModelStatusEchoes.keys())) {
140+
if (statusKey.startsWith(`${providerId}:`)) {
141+
pendingModelStatusEchoes.delete(statusKey)
142+
}
143+
}
144+
}
145+
146+
watch(
147+
() => providerStore.providers.map((provider) => provider.id),
148+
(providerIds) => {
149+
const providerIdSet = new Set(providerIds)
150+
for (const materializedProviderId of getMaterializedProviderIds()) {
151+
if (!providerIdSet.has(materializedProviderId)) {
152+
purgeRemovedProviderState(materializedProviderId)
153+
}
154+
}
155+
}
156+
)
157+
112158
const refreshMaterializedProviders = async () => {
113159
const providerIds = getMaterializedProviderIds()
114160
for (const providerId of providerIds) {
@@ -775,7 +821,7 @@ export const useModelStore = defineStore('model', () => {
775821

776822
const searchModels = (query: string) => {
777823
const normalized = query.toLowerCase()
778-
return enabledModels.value
824+
return activeEnabledModels.value
779825
.map((group) => ({
780826
providerId: group.providerId,
781827
models: group.models.filter(
@@ -1233,6 +1279,7 @@ export const useModelStore = defineStore('model', () => {
12331279

12341280
return {
12351281
enabledModels,
1282+
activeEnabledModels,
12361283
allProviderModels,
12371284
customModels,
12381285
initialized: readonly(initialized),

test/renderer/stores/modelStore.test.ts

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, vi } from 'vitest'
2-
import { ref } from 'vue'
2+
import { reactive, ref } from 'vue'
33
import { ModelType } from '../../../src/shared/model'
44

55
const createQueryCache = () => {
@@ -51,11 +51,11 @@ const setupStore = async (overrides?: {
5151
onModelConfigChanged: vi.fn(() => vi.fn()),
5252
...overrides?.modelClient
5353
}
54-
const providerStore = {
54+
const providerStore = reactive({
5555
providers: [],
5656
ensureInitialized: vi.fn(async () => undefined),
5757
...overrides?.providerStore
58-
}
58+
})
5959

6060
vi.doMock('pinia', async () => {
6161
const actual = await vi.importActual<typeof import('pinia')>('pinia')
@@ -95,7 +95,8 @@ const setupStore = async (overrides?: {
9595
return {
9696
store,
9797
agentModelStore,
98-
modelClient
98+
modelClient,
99+
providerStore
99100
}
100101
}
101102

@@ -196,6 +197,81 @@ describe('modelStore.refreshProviderModels', () => {
196197
expect(modelClient.getProviderModels).toHaveBeenCalledWith('openai')
197198
})
198199

200+
it('exposes only enabled provider groups through activeEnabledModels', async () => {
201+
const { store } = await setupStore({
202+
providerStore: {
203+
providers: [
204+
{ id: 'openai', enable: true },
205+
{ id: 'deepseek', enable: false }
206+
]
207+
}
208+
})
209+
210+
store.enabledModels.value = [
211+
{
212+
providerId: 'openai',
213+
models: [{ id: 'gpt-5', name: 'GPT-5', providerId: 'openai' } as any]
214+
},
215+
{
216+
providerId: 'deepseek',
217+
models: [{ id: 'deepseek-chat', name: 'DeepSeek Chat', providerId: 'deepseek' } as any]
218+
}
219+
]
220+
221+
expect(store.activeEnabledModels.value).toEqual([
222+
{
223+
providerId: 'openai',
224+
models: [expect.objectContaining({ id: 'gpt-5' })]
225+
}
226+
])
227+
})
228+
229+
it('purges deleted providers from local model state', async () => {
230+
const { store, providerStore } = await setupStore({
231+
providerStore: {
232+
providers: [
233+
{ id: 'openai', enable: true },
234+
{ id: 'deepseek', enable: true }
235+
]
236+
}
237+
})
238+
239+
store.enabledModels.value = [
240+
{
241+
providerId: 'openai',
242+
models: [{ id: 'gpt-5', name: 'GPT-5', providerId: 'openai' } as any]
243+
},
244+
{
245+
providerId: 'deepseek',
246+
models: [{ id: 'deepseek-chat', name: 'DeepSeek Chat', providerId: 'deepseek' } as any]
247+
}
248+
]
249+
store.allProviderModels.value = [...store.enabledModels.value]
250+
store.customModels.value = [
251+
{
252+
providerId: 'deepseek',
253+
models: [{ id: 'deepseek-custom', name: 'DeepSeek Custom', providerId: 'deepseek' } as any]
254+
}
255+
]
256+
257+
providerStore.providers = [{ id: 'openai', enable: true }]
258+
await flushMicrotasks()
259+
260+
expect(store.enabledModels.value).toEqual([
261+
{
262+
providerId: 'openai',
263+
models: [expect.objectContaining({ id: 'gpt-5' })]
264+
}
265+
])
266+
expect(store.allProviderModels.value).toEqual([
267+
{
268+
providerId: 'openai',
269+
models: [expect.objectContaining({ id: 'gpt-5' })]
270+
}
271+
])
272+
expect(store.customModels.value).toEqual([])
273+
})
274+
199275
it('merges same-tick concurrent refreshes into a single provider fetch', async () => {
200276
const deferredModels = createDeferred<any[]>()
201277
const model = {

0 commit comments

Comments
 (0)