Skip to content

Commit 43b093e

Browse files
gsabranCopilot
andauthored
Keep the list of active models in sync with the settings (#193)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent fd51007 commit 43b093e

File tree

6 files changed

+236
-47
lines changed

6 files changed

+236
-47
lines changed
Binary file not shown.

app/modules/serviceInterfaces/LLMServiceInterface/Sources/LLMService.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,7 @@ public protocol LLMService: Sendable {
9393
newSettings: Settings.AIProviderSettings)
9494
async throws -> [AIProviderModel]
9595

96-
/// A read-only subject that publishes the currently active models.
97-
///
98-
/// This provides reactive updates whenever the set of active models changes.
96+
/// The models that have a provider configured and are enabled (ie the models that the user is able to use given their current configuration).
9997
var activeModels: ReadonlyCurrentValueSubject<[AIModel], Never> { get }
10098

10199
/// Returns the low tier model from configured providers with the cheapest input cost.

app/modules/services/LLMService/Sources/LLMModelManager.swift

Lines changed: 81 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ final class AIModelsManager: AIModelsManagerProtocol {
7373
modelsByProviderId = .init(llmModelByProvider.values.flatMap(\.self).reduce(into: [:]) { acc, model in
7474
acc[model.id] = model
7575
})
76-
providerModelsByModelId = .init(llmModelByProvider.values.flatMap(\.self).reduce(into: [:]) { acc, model in
76+
let providerModelsByModelIdDict = llmModelByProvider.values.flatMap(\.self).reduce(into: [:]) { acc, model in
7777
acc[model.modelInfo.id, default: []].append(model)
78-
})
78+
}
79+
providerModelsByModelId = .init(providerModelsByModelIdDict)
7980
let modelByModelId = PublishedDictionary<AIModelID, AIModel>(llmModelByProvider.values.flatMap(\.self)
8081
.reduce(into: [:]) { acc, model in
8182
acc[model.modelInfo.id] = model.modelInfo
@@ -86,6 +87,15 @@ final class AIModelsManager: AIModelsManagerProtocol {
8687
settings: settingsService.value(for: \.llmProviderSettings),
8788
models: llmModelByProvider))
8889

90+
// Initialize activeModels with the current state
91+
let configuredProviders = Set(settingsService.value(for: \.llmProviderSettings).keys)
92+
let initialActiveModels = Self.filterActiveModels(
93+
models: modelByModelId.sortedValues,
94+
enabledModels: settingsService.value(for: \.enabledModels),
95+
providerModelsByModelId: providerModelsByModelIdDict,
96+
configuredProviders: configuredProviders)
97+
_activeModels = .init(initialActiveModels)
98+
8999
observerChangesToSettings()
90100
}
91101

@@ -98,14 +108,7 @@ final class AIModelsManager: AIModelsManagerProtocol {
98108
}
99109

100110
var activeModels: ReadonlyCurrentValueSubject<[AIModel], Never> {
101-
ReadonlyCurrentValueSubject<[AIModel], Never>(
102-
filterActiveModels(models.currentValue),
103-
publisher: models.map { @Sendable [weak self] models in
104-
guard let self else { return [] }
105-
return filterActiveModels(models)
106-
}
107-
.removeDuplicates()
108-
.eraseToAnyPublisher())
111+
_activeModels.readonly()
109112
}
110113

111114
func provider(for model: AIModel) -> ReadonlyCurrentValueSubject<AIProvider?, Never> {
@@ -146,6 +149,7 @@ final class AIModelsManager: AIModelsManagerProtocol {
146149
}
147150

148151
private let _availableModels: CurrentValueSubject<[AIModel], Never>
152+
private let _activeModels: CurrentValueSubject<[AIModel], Never>
149153

150154
private let localServer: LocalServer
151155
private let settingsService: SettingsService
@@ -229,6 +233,43 @@ final class AIModelsManager: AIModelsManagerProtocol {
229233
}
230234
}
231235

236+
/// Return the list of models that are active.
237+
/// - Parameters:
238+
/// - models: The list of models to choose from, typically all known models
239+
/// - enabledModels: The models that have been enabled by the user.
240+
/// - providerModelsByModelId: The list of known providers for each model
241+
/// - configuredProviders: The providers that have been configured by the user.
242+
private static func filterActiveModels(
243+
models: [AIModel],
244+
enabledModels: [AIModelID],
245+
providerModelsByModelId: [AIModelID: [AIProviderModel]],
246+
configuredProviders: Set<AIProvider>)
247+
-> [AIModel]
248+
{
249+
let enabledModels = Set(enabledModels)
250+
return models.filter { model in
251+
// Get the list of providers that support the model
252+
guard let providerModels = providerModelsByModelId[model.id], !providerModels.isEmpty else {
253+
return false
254+
}
255+
256+
// Check if at least one of the providers for this model is configured
257+
let hasConfiguredProvider = providerModels.contains { configuredProviders.contains($0.provider) }
258+
guard hasConfiguredProvider else {
259+
return false
260+
}
261+
262+
// Check if it should be active
263+
let isEnabled = enabledModels.contains(model.id)
264+
// Check if the model is from an external agent
265+
let isExternalAgent = providerModels.contains { providerModel in
266+
providerModel.provider.isExternalAgent
267+
}
268+
269+
return isEnabled || isExternalAgent
270+
}
271+
}
272+
232273
private func fetchAndSaveModelsAvailable(
233274
for provider: AIProvider,
234275
settings: AIProviderSettings)
@@ -305,14 +346,32 @@ final class AIModelsManager: AIModelsManagerProtocol {
305346
guard let self else { return }
306347
await updateModels(from: previous, to: llmProviderSettings)
307348
_availableModels.send(Self.availableModels(settings: llmProviderSettings, models: llmModelByProvider.wrappedValue))
349+
updateActiveModels()
308350
}
309351
}.store(in: &cancellables)
352+
310353
settingsService.liveValue(for: \.enabledModels).sink { @Sendable [weak self] _ in
311354
guard let self else { return }
312-
mutableModels.send(mutableModels.value) // This will trigger a new filtering of active models
355+
updateActiveModels()
356+
}.store(in: &cancellables)
357+
358+
// Update activeModels when the models list changes
359+
mutableModels.sink { @Sendable [weak self] _ in
360+
guard let self else { return }
361+
updateActiveModels()
313362
}.store(in: &cancellables)
314363
}
315364

365+
private func updateActiveModels() {
366+
let configuredProviders = Set(settingsService.value(for: \.llmProviderSettings).keys)
367+
let activeModels = Self.filterActiveModels(
368+
models: modelByModelId.sortedValues,
369+
enabledModels: settingsService.value(for: \.enabledModels),
370+
providerModelsByModelId: providerModelsByModelId.wrappedValue,
371+
configuredProviders: configuredProviders)
372+
_activeModels.send(activeModels)
373+
}
374+
316375
private func updateModels(
317376
from previous: [AIProvider: AIProviderSettings]?,
318377
to current: [AIProvider: AIProviderSettings]?)
@@ -326,11 +385,13 @@ final class AIModelsManager: AIModelsManagerProtocol {
326385
{
327386
// Remove providers that are no longer present
328387
let removedProviders = (previous ?? [:]).keys.filter { current?[$0] == nil }
329-
let modelInfos = inLock { state in
388+
let (modelInfos, modelsToPersist) = inLock { state in
330389
for provider in removedProviders {
331390
Self.remove(provider: provider, from: &state)
332391
}
333-
return state.modelByModelId.sortedValues
392+
return (
393+
state.modelByModelId.sortedValues,
394+
state.llmModelByProvider.wrappedValue.reduce(into: [:]) { $0[$1.key] = $1.value })
334395
}
335396
mutableModels.send(modelInfos)
336397

@@ -350,12 +411,13 @@ final class AIModelsManager: AIModelsManagerProtocol {
350411
await group.waitForAll()
351412
}
352413

353-
do {
354-
try Self.persist(
355-
models: llmModelByProvider.wrappedValue.reduce(into: [:], { $0[$1.key] = $1.value }),
356-
fileManager: fileManager)
357-
} catch {
358-
defaultLogger.error("Failed to persist models", error)
414+
// Only persist if we removed providers (fetched models handle their own persistence)
415+
if !removedProviders.isEmpty {
416+
do {
417+
try Self.persist(models: modelsToPersist, fileManager: fileManager)
418+
} catch {
419+
defaultLogger.error("Failed to persist models", error)
420+
}
359421
}
360422
}
361423
// Ensure that those updates are serial, since they rely on the change between two states.
@@ -364,15 +426,6 @@ final class AIModelsManager: AIModelsManagerProtocol {
364426
}.value
365427
}
366428

367-
private func filterActiveModels(_ models: [AIModel]) -> [AIModel] {
368-
models.filter { model in
369-
settingsService.value(for: \.enabledModels).contains(model.id) ||
370-
// The model that represent an external agent should always be considered active.
371-
// To disable it, the user can disable the provider instead.
372-
providerModelsByModelId[model.id]?.first?.provider.isExternalAgent == true
373-
}
374-
}
375-
376429
}
377430

378431
extension PublishedDictionary<AIModelID, AIModel> {

0 commit comments

Comments
 (0)