Skip to content

Commit 6ad46d3

Browse files
committed
feat(llm): add centralized state management for services
Introduce LLMServiceManager singleton to manage LLM and Image Generation service configurations using reactive StateFlow. This allows UI components to update configurations dynamically without recreating service instances.
1 parent 0b16a02 commit 6ad46d3

4 files changed

Lines changed: 161 additions & 23 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package cc.unitmesh.llm
2+
3+
import cc.unitmesh.config.ConfigManager
4+
import kotlinx.coroutines.flow.MutableStateFlow
5+
import kotlinx.coroutines.flow.StateFlow
6+
import kotlinx.coroutines.flow.asStateFlow
7+
8+
/**
9+
* Singleton manager for LLM and Image Generation service configurations.
10+
*
11+
* This manager provides reactive StateFlow-based configuration that allows
12+
* UI components to update service configurations dynamically without recreating
13+
* service instances.
14+
*
15+
* Usage:
16+
* - UI components call updateModelConfig() when user selects a different model
17+
* - UI components call updateImageGenerationModel() when user selects a different image model
18+
* - Services read from currentModelConfig and currentImageGenerationModel StateFlows
19+
*/
20+
object LLMServiceManager {
21+
22+
/**
23+
* Default image generation model
24+
*/
25+
const val DEFAULT_IMAGE_MODEL = "cogview-3-flash"
26+
27+
/**
28+
* Available image generation models for GLM CogView
29+
*/
30+
val AVAILABLE_IMAGE_MODELS = listOf("cogview-3-flash", "cogview-4")
31+
32+
// Current LLM model configuration
33+
private val _currentModelConfig = MutableStateFlow<ModelConfig?>(null)
34+
val currentModelConfig: StateFlow<ModelConfig?> = _currentModelConfig.asStateFlow()
35+
36+
// Current image generation model (GLM CogView)
37+
private val _currentImageGenerationModel = MutableStateFlow(DEFAULT_IMAGE_MODEL)
38+
val currentImageGenerationModel: StateFlow<String> = _currentImageGenerationModel.asStateFlow()
39+
40+
/**
41+
* Update the current LLM model configuration.
42+
* Called by ModelSelector when user selects a different model.
43+
*
44+
* @param config The new model configuration
45+
*/
46+
fun updateModelConfig(config: ModelConfig) {
47+
_currentModelConfig.value = config
48+
}
49+
50+
/**
51+
* Update the current image generation model.
52+
* Called by ImageGenerationModelSelector when user selects a different model.
53+
*
54+
* @param model The model ID (e.g., "cogview-3-flash", "cogview-4")
55+
*/
56+
fun updateImageGenerationModel(model: String) {
57+
if (model in AVAILABLE_IMAGE_MODELS) {
58+
_currentImageGenerationModel.value = model
59+
}
60+
}
61+
62+
/**
63+
* Get the current image generation model synchronously.
64+
* Used by ImageGenerationService when building requests.
65+
*/
66+
fun getImageGenerationModel(): String = _currentImageGenerationModel.value
67+
68+
/**
69+
* Get the current model config synchronously.
70+
* Used by services that need the current config.
71+
*/
72+
fun getModelConfig(): ModelConfig? = _currentModelConfig.value
73+
74+
/**
75+
* Check if the current provider is GLM (supports image generation).
76+
*/
77+
fun isGlmProvider(): Boolean {
78+
return _currentModelConfig.value?.provider == LLMProviderType.GLM
79+
}
80+
81+
/**
82+
* Initialize from ConfigManager.
83+
* Should be called at app startup to load the active configuration.
84+
*/
85+
suspend fun initializeFromConfig() {
86+
try {
87+
val wrapper = ConfigManager.load()
88+
val activeConfig = wrapper.getActiveModelConfig()
89+
if (activeConfig != null) {
90+
_currentModelConfig.value = activeConfig
91+
}
92+
} catch (e: Exception) {
93+
// Config not available, leave as null
94+
}
95+
}
96+
97+
/**
98+
* Reset to defaults (for testing or cleanup).
99+
*/
100+
fun reset() {
101+
_currentModelConfig.value = null
102+
_currentImageGenerationModel.value = DEFAULT_IMAGE_MODEL
103+
}
104+
}
105+

mpp-core/src/commonMain/kotlin/cc/unitmesh/llm/image/ImageGenerationService.kt

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cc.unitmesh.llm.image
33
import cc.unitmesh.agent.tool.impl.http.HttpClientFactory
44
import cc.unitmesh.config.ConfigManager
55
import cc.unitmesh.llm.LLMProviderType
6+
import cc.unitmesh.llm.LLMServiceManager
67
import cc.unitmesh.llm.ModelConfig
78
import cc.unitmesh.llm.ModelRegistry
89
import io.ktor.client.*
@@ -27,15 +28,16 @@ import kotlinx.serialization.json.*
2728
* Supported models: cogview-3-flash, cogview-4
2829
* It is designed to work across all platforms (JVM, JS, WASM) using Ktor HTTP client.
2930
*
31+
* The image model is read dynamically from LLMServiceManager, allowing UI components
32+
* to change the model without recreating the service instance.
33+
*
3034
* API Reference: https://docs.bigmodel.cn/api-reference/模型-api/图像生成
3135
*
3236
* @param config ModelConfig containing GLM API key and optional base URL
33-
* @param imageModel The image generation model to use (default: cogview-3-flash)
3437
*/
3538
class ImageGenerationService(
3639
private val config: ModelConfig,
37-
private val client: HttpClient = HttpClientFactory.create(),
38-
private var imageModel: String = DEFAULT_IMAGE_MODEL
40+
private val client: HttpClient = HttpClientFactory.create()
3941
) {
4042
companion object {
4143
const val DEFAULT_IMAGE_MODEL = "cogview-3-flash"
@@ -47,9 +49,14 @@ class ImageGenerationService(
4749

4850
/**
4951
* Create an ImageGenerationService from a ModelConfig.
52+
* Note: imageModel parameter is deprecated, use LLMServiceManager.updateImageGenerationModel() instead.
5053
*/
5154
fun create(config: ModelConfig, imageModel: String = DEFAULT_IMAGE_MODEL): ImageGenerationService {
52-
return ImageGenerationService(config, imageModel = imageModel)
55+
// Set the initial model in LLMServiceManager if provided
56+
if (imageModel != DEFAULT_IMAGE_MODEL) {
57+
LLMServiceManager.updateImageGenerationModel(imageModel)
58+
}
59+
return ImageGenerationService(config)
5360
}
5461

5562
suspend fun default(): ImageGenerationService? {
@@ -59,15 +66,20 @@ class ImageGenerationService(
5966

6067
/**
6168
* Create an ImageGenerationService with just an API key.
69+
* Note: imageModel parameter is deprecated, use LLMServiceManager.updateImageGenerationModel() instead.
6270
*/
6371
fun create(apiKey: String, imageModel: String = DEFAULT_IMAGE_MODEL): ImageGenerationService {
72+
// Set the initial model in LLMServiceManager if provided
73+
if (imageModel != DEFAULT_IMAGE_MODEL) {
74+
LLMServiceManager.updateImageGenerationModel(imageModel)
75+
}
6476
val config = ModelConfig(
6577
provider = LLMProviderType.GLM,
6678
modelName = imageModel,
6779
apiKey = apiKey,
6880
baseUrl = ModelRegistry.getDefaultBaseUrl(LLMProviderType.GLM)
6981
)
70-
return ImageGenerationService(config, imageModel = imageModel)
82+
return ImageGenerationService(config)
7183
}
7284
}
7385
private val json = Json {
@@ -147,27 +159,29 @@ class ImageGenerationService(
147159

148160
/**
149161
* Set the image generation model.
162+
* This now delegates to LLMServiceManager for global state management.
150163
* @param model The model ID (e.g., "cogview-3-flash", "cogview-4")
151164
*/
152165
fun setImageModel(model: String) {
153-
if (model in AVAILABLE_MODELS) {
154-
imageModel = model
155-
}
166+
LLMServiceManager.updateImageGenerationModel(model)
156167
}
157168

158169
/**
159170
* Get the current image generation model.
171+
* This now reads from LLMServiceManager for consistent global state.
160172
*/
161-
fun getImageModel(): String = imageModel
173+
fun getImageModel(): String = LLMServiceManager.getImageGenerationModel()
162174

163175
private fun buildImageRequest(prompt: String, size: String): String {
176+
// Read the current model from LLMServiceManager for dynamic updates
177+
val currentModel = LLMServiceManager.getImageGenerationModel()
164178
val requestJson = buildJsonObject {
165-
put("model", imageModel)
179+
put("model", currentModel)
166180
put("prompt", prompt)
167181
put("size", size)
168182
}
169183

170-
println("Image model: $imageModel")
184+
println("Image model: $currentModel")
171185
return json.encodeToString(JsonObject.serializer(), requestJson)
172186
}
173187

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/ModelSelector.kt

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import cc.unitmesh.devins.ui.compose.icons.AutoDevComposeIcons
1111
import cc.unitmesh.devins.ui.provider.CopilotModelRefresher
1212
import cc.unitmesh.config.ConfigManager
1313
import cc.unitmesh.devins.ui.i18n.Strings
14+
import cc.unitmesh.llm.LLMServiceManager
1415
import cc.unitmesh.llm.ModelConfig
1516
import cc.unitmesh.llm.NamedModelConfig
1617
import kotlinx.coroutines.launch
@@ -34,7 +35,7 @@ fun ModelSelector(onConfigChange: (ModelConfig) -> Unit = {}) {
3435
var currentConfigName by remember { mutableStateOf<String?>(null) }
3536
var isRefreshingCopilot by remember { mutableStateOf(false) }
3637
val scope = rememberCoroutineScope()
37-
38+
3839
// Check if GitHub Copilot refresh is available (JVM with local token)
3940
val isCopilotAvailable = remember { CopilotModelRefresher.isAvailable() }
4041

@@ -45,8 +46,11 @@ fun ModelSelector(onConfigChange: (ModelConfig) -> Unit = {}) {
4546
availableConfigs = wrapper.getAllConfigs()
4647
currentConfigName = wrapper.getActiveName()
4748

48-
// Notify parent of initial config
49-
wrapper.getActiveModelConfig()?.let { onConfigChange(it) }
49+
// Notify parent of initial config and update LLMServiceManager
50+
wrapper.getActiveModelConfig()?.let { config ->
51+
LLMServiceManager.updateModelConfig(config)
52+
onConfigChange(config)
53+
}
5054
} catch (e: Exception) {
5155
println("Failed to load configs: ${e.message}")
5256
}
@@ -102,7 +106,10 @@ fun ModelSelector(onConfigChange: (ModelConfig) -> Unit = {}) {
102106
try {
103107
ConfigManager.setActive(config.name)
104108
currentConfigName = config.name
105-
onConfigChange(config.toModelConfig())
109+
val modelConfig = config.toModelConfig()
110+
// Update LLMServiceManager for global state
111+
LLMServiceManager.updateModelConfig(modelConfig)
112+
onConfigChange(modelConfig)
106113
expanded = false
107114
} catch (e: Exception) {
108115
println("Failed to set active config: ${e.message}")
@@ -157,11 +164,11 @@ fun ModelSelector(onConfigChange: (ModelConfig) -> Unit = {}) {
157164
)
158165
}
159166
)
160-
167+
161168
// Refresh GitHub Copilot button (only shown on JVM with local token)
162169
if (isCopilotAvailable) {
163170
DropdownMenuItem(
164-
text = {
171+
text = {
165172
Text(
166173
if (isRefreshingCopilot) "Refreshing Copilot..." else "Refresh GitHub Copilot"
167174
)
@@ -176,13 +183,13 @@ fun ModelSelector(onConfigChange: (ModelConfig) -> Unit = {}) {
176183
// Save each config to config.yaml
177184
val existingNames = availableConfigs.map { it.name }.toMutableSet()
178185
var savedCount = 0
179-
186+
180187
for (config in copilotConfigs) {
181188
// Skip if already exists
182189
if (config.name in existingNames) {
183190
continue
184191
}
185-
192+
186193
try {
187194
ConfigManager.saveConfig(config, setActive = false)
188195
existingNames.add(config.name)
@@ -191,11 +198,11 @@ fun ModelSelector(onConfigChange: (ModelConfig) -> Unit = {}) {
191198
println("Failed to save config ${config.name}: ${e.message}")
192199
}
193200
}
194-
201+
195202
// Reload configs
196203
val wrapper = ConfigManager.load()
197204
availableConfigs = wrapper.getAllConfigs()
198-
205+
199206
println("Added $savedCount GitHub Copilot configurations")
200207
}
201208
} catch (e: Exception) {
@@ -240,7 +247,7 @@ fun ModelSelector(onConfigChange: (ModelConfig) -> Unit = {}) {
240247
ModelConfigDialog(
241248
currentConfig = if (isNewConfig) ModelConfig() else (currentConfig?.toModelConfig() ?: ModelConfig()),
242249
currentConfigName = if (isNewConfig) null else currentConfigName,
243-
onDismiss = {
250+
onDismiss = {
244251
showConfigDialog = false
245252
isNewConfig = false
246253
},
@@ -272,7 +279,8 @@ fun ModelSelector(onConfigChange: (ModelConfig) -> Unit = {}) {
272279
availableConfigs = wrapper.getAllConfigs()
273280
currentConfigName = wrapper.getActiveName()
274281

275-
// Notify parent
282+
// Update LLMServiceManager and notify parent
283+
LLMServiceManager.updateModelConfig(newModelConfig)
276284
onConfigChange(newModelConfig)
277285
showConfigDialog = false
278286

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/multimodal/ImageGenerationModelSelector.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import androidx.compose.ui.Alignment
88
import androidx.compose.ui.Modifier
99
import androidx.compose.ui.unit.dp
1010
import cc.unitmesh.devins.ui.compose.icons.AutoDevComposeIcons
11+
import cc.unitmesh.llm.LLMServiceManager
1112

1213
/**
1314
* Available image generation models for GLM CogView.
@@ -28,6 +29,9 @@ enum class ImageGenerationModel(val modelId: String, val displayName: String) {
2829
* Shows current model and allows switching between available image generation models.
2930
* Only visible when GLM provider is selected.
3031
*
32+
* This component automatically updates LLMServiceManager when the model changes,
33+
* ensuring that ImageGenerationService uses the correct model.
34+
*
3135
* @param currentModel The currently selected image generation model
3236
* @param onModelChange Callback when user selects a different model
3337
* @param modifier Modifier for the component
@@ -40,6 +44,11 @@ fun ImageGenerationModelSelector(
4044
) {
4145
var expanded by remember { mutableStateOf(false) }
4246

47+
// Initialize LLMServiceManager with current model on first composition
48+
LaunchedEffect(Unit) {
49+
LLMServiceManager.updateImageGenerationModel(currentModel.modelId)
50+
}
51+
4352
Box(modifier = modifier) {
4453
Row(
4554
modifier = Modifier
@@ -87,6 +96,8 @@ fun ImageGenerationModelSelector(
8796
}
8897
},
8998
onClick = {
99+
// Update LLMServiceManager for global state
100+
LLMServiceManager.updateImageGenerationModel(model.modelId)
90101
onModelChange(model)
91102
expanded = false
92103
},

0 commit comments

Comments
 (0)