feat: correx-managed model lifecycle slice 1 — config + manager factory + boot/shutdown
[[models]] config (ModelConfig/ModelsSettings) parsed by ConfigLoader; InfrastructureModule.createModelManager + modelConfigToDescriptor; Main.kt managed boot path spawns the default llama-server when [[models]] is present (static [[providers]] path preserved when absent) and kills it on shutdown. Plan: docs/plans/2026-05-31-model-lifecycle-management.md (slice 1 of 5).
This commit is contained in:
@@ -35,7 +35,9 @@ object ConfigLoader {
|
||||
var currentSection = ""
|
||||
val sections = mutableMapOf<String, MutableMap<String, Any>>()
|
||||
val providers = mutableListOf<MutableMap<String, Any>>()
|
||||
val models = mutableListOf<MutableMap<String, Any>>()
|
||||
var currentProvider: MutableMap<String, Any>? = null
|
||||
var currentModel: MutableMap<String, Any>? = null
|
||||
|
||||
for ((lineNum, line) in lines.withIndex()) {
|
||||
val trimmed = line.trim()
|
||||
@@ -46,18 +48,28 @@ object ConfigLoader {
|
||||
}
|
||||
trimmed == "[[providers]]" -> {
|
||||
// Start a new provider entry
|
||||
if (currentProvider != null) {
|
||||
providers.add(currentProvider)
|
||||
}
|
||||
if (currentProvider != null) providers.add(currentProvider)
|
||||
if (currentModel != null) { models.add(currentModel); currentModel = null }
|
||||
currentProvider = mutableMapOf()
|
||||
currentSection = ""
|
||||
}
|
||||
trimmed == "[[models]]" -> {
|
||||
// Start a new model entry
|
||||
if (currentModel != null) models.add(currentModel)
|
||||
if (currentProvider != null) { providers.add(currentProvider); currentProvider = null }
|
||||
currentModel = mutableMapOf()
|
||||
currentSection = ""
|
||||
}
|
||||
trimmed.startsWith("[") && !trimmed.startsWith("[[") && trimmed.endsWith("]") -> {
|
||||
// Parse section headers like [server] or [tools.shell]
|
||||
if (currentProvider != null) {
|
||||
providers.add(currentProvider)
|
||||
currentProvider = null
|
||||
}
|
||||
if (currentModel != null) {
|
||||
models.add(currentModel)
|
||||
currentModel = null
|
||||
}
|
||||
currentSection = trimmed.substring(1, trimmed.length - 1).trim()
|
||||
sections.putIfAbsent(currentSection, mutableMapOf())
|
||||
}
|
||||
@@ -69,22 +81,21 @@ object ConfigLoader {
|
||||
val valueStr = trimmed.substring(eqIndex + 1).trim()
|
||||
val parsedValue = parseValue(valueStr, lineNum + 1)
|
||||
|
||||
if (currentProvider != null) {
|
||||
currentProvider[key] = parsedValue
|
||||
} else if (currentSection.isNotEmpty()) {
|
||||
sections[currentSection]?.put(key, parsedValue)
|
||||
when {
|
||||
currentProvider != null -> currentProvider[key] = parsedValue
|
||||
currentModel != null -> currentModel[key] = parsedValue
|
||||
currentSection.isNotEmpty() -> sections[currentSection]?.put(key, parsedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last provider if file ends with [[providers]] block
|
||||
if (currentProvider != null) {
|
||||
providers.add(currentProvider)
|
||||
}
|
||||
// Don't forget the last array-of-table entry if file ends with one
|
||||
if (currentProvider != null) providers.add(currentProvider)
|
||||
if (currentModel != null) models.add(currentModel)
|
||||
|
||||
return buildConfig(sections, providers)
|
||||
return buildConfig(sections, providers, models)
|
||||
}
|
||||
|
||||
private fun parseValue(valueStr: String, lineNum: Int): Any {
|
||||
@@ -200,6 +211,7 @@ object ConfigLoader {
|
||||
private fun buildConfig(
|
||||
sections: Map<String, Map<String, Any>>,
|
||||
providersList: List<Map<String, Any>> = emptyList(),
|
||||
modelsList: List<Map<String, Any>> = emptyList(),
|
||||
): CorrexConfig {
|
||||
val serverSection = sections["server"] ?: emptyMap()
|
||||
val tuiSection = sections["tui"] ?: emptyMap()
|
||||
@@ -211,6 +223,7 @@ object ConfigLoader {
|
||||
val toolsFileEditSection = sections["tools.file_edit"] ?: emptyMap()
|
||||
val routerEmbedderSection = sections["router.embedder"] ?: emptyMap()
|
||||
val routerL3Section = sections["router.l3"] ?: emptyMap()
|
||||
val modelsSection = sections["models"] ?: emptyMap()
|
||||
|
||||
val server = ServerConfig(
|
||||
host = asString(serverSection["host"], "localhost"),
|
||||
@@ -347,6 +360,32 @@ object ConfigLoader {
|
||||
l3 = l3,
|
||||
)
|
||||
|
||||
val models = modelsList.mapNotNull { modelMap ->
|
||||
val id = asString(modelMap["id"]) ?: return@mapNotNull null
|
||||
val modelPath = asString(modelMap["model_path"]) ?: return@mapNotNull null
|
||||
val contextSize = asInt(modelMap["context_size"], 8192)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val params = (modelMap["params"] as? Map<String, Any>)
|
||||
?.mapValues { it.value.toString() }
|
||||
?: emptyMap()
|
||||
val capabilities = parseCapabilities(modelMap["capabilities"])
|
||||
|
||||
ModelConfig(
|
||||
id = id,
|
||||
modelPath = modelPath,
|
||||
contextSize = contextSize,
|
||||
params = params,
|
||||
capabilities = capabilities,
|
||||
)
|
||||
}
|
||||
|
||||
val modelsSettings = ModelsSettings(
|
||||
defaultModel = asStringOrNull(modelsSection["default_model"]),
|
||||
llamaServerBin = asString(modelsSection["llama_server_bin"], "llama-server"),
|
||||
host = asString(modelsSection["host"], "127.0.0.1"),
|
||||
port = asInt(modelsSection["port"], 10000),
|
||||
)
|
||||
|
||||
return CorrexConfig(
|
||||
server = server,
|
||||
tui = tui,
|
||||
@@ -354,6 +393,8 @@ object ConfigLoader {
|
||||
tools = tools,
|
||||
providers = providers,
|
||||
router = router,
|
||||
models = models,
|
||||
modelsSettings = modelsSettings,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ data class CorrexConfig(
|
||||
val tools: ToolsConfig = ToolsConfig(),
|
||||
val providers: List<ProviderConfig> = emptyList(),
|
||||
val router: RouterConfig = RouterConfig(),
|
||||
val models: List<ModelConfig> = emptyList(),
|
||||
val modelsSettings: ModelsSettings = ModelsSettings(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -90,3 +92,20 @@ data class L3Config(
|
||||
val dim: Int = 1536,
|
||||
val bitWidth: Int = 4,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ModelConfig(
|
||||
val id: String,
|
||||
val modelPath: String,
|
||||
val contextSize: Int = 8192,
|
||||
val params: Map<String, String> = emptyMap(),
|
||||
val capabilities: Map<String, Double> = emptyMap(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ModelsSettings(
|
||||
val defaultModel: String? = null,
|
||||
val llamaServerBin: String = "llama-server",
|
||||
val host: String = "127.0.0.1",
|
||||
val port: Int = 10000,
|
||||
)
|
||||
|
||||
@@ -270,4 +270,98 @@ class ConfigLoaderTest {
|
||||
assertEquals(1536, result.router.l3.dim)
|
||||
assertEquals(4, result.router.l3.bitWidth)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseToml parses models array-of-tables`() {
|
||||
val toml = """
|
||||
[models]
|
||||
default_model = "mistral-7b"
|
||||
llama_server_bin = "/usr/local/bin/llama-server"
|
||||
host = "127.0.0.1"
|
||||
port = 10001
|
||||
|
||||
[[models]]
|
||||
id = "mistral-7b"
|
||||
model_path = "/models/mistral-7b.gguf"
|
||||
context_size = 4096
|
||||
capabilities = { General = 1.0, Coding = 0.7, Reasoning = 0.6 }
|
||||
|
||||
[[models]]
|
||||
id = "codellama-7b"
|
||||
model_path = "/models/codellama-7b.gguf"
|
||||
context_size = 8192
|
||||
""".trimIndent()
|
||||
|
||||
val loader = ConfigLoader::class.java
|
||||
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||
parseTomlMethod.isAccessible = true
|
||||
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||
|
||||
assertEquals(2, result.models.size)
|
||||
|
||||
assertEquals("mistral-7b", result.models[0].id)
|
||||
assertEquals("/models/mistral-7b.gguf", result.models[0].modelPath)
|
||||
assertEquals(4096, result.models[0].contextSize)
|
||||
assertEquals(3, result.models[0].capabilities.size)
|
||||
assertEquals(1.0, result.models[0].capabilities["General"])
|
||||
assertEquals(0.7, result.models[0].capabilities["Coding"])
|
||||
assertEquals(0.6, result.models[0].capabilities["Reasoning"])
|
||||
|
||||
assertEquals("codellama-7b", result.models[1].id)
|
||||
assertEquals("/models/codellama-7b.gguf", result.models[1].modelPath)
|
||||
assertEquals(8192, result.models[1].contextSize)
|
||||
assertEquals(0, result.models[1].capabilities.size)
|
||||
|
||||
assertEquals("mistral-7b", result.modelsSettings.defaultModel)
|
||||
assertEquals("/usr/local/bin/llama-server", result.modelsSettings.llamaServerBin)
|
||||
assertEquals("127.0.0.1", result.modelsSettings.host)
|
||||
assertEquals(10001, result.modelsSettings.port)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseToml returns empty models list and default modelsSettings when sections absent`() {
|
||||
val toml = """
|
||||
[server]
|
||||
host = "localhost"
|
||||
""".trimIndent()
|
||||
|
||||
val loader = ConfigLoader::class.java
|
||||
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||
parseTomlMethod.isAccessible = true
|
||||
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||
|
||||
assertEquals(0, result.models.size)
|
||||
assertEquals(null, result.modelsSettings.defaultModel)
|
||||
assertEquals("llama-server", result.modelsSettings.llamaServerBin)
|
||||
assertEquals("127.0.0.1", result.modelsSettings.host)
|
||||
assertEquals(10000, result.modelsSettings.port)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseToml parses models alongside providers`() {
|
||||
val toml = """
|
||||
[[providers]]
|
||||
id = "remote-provider"
|
||||
type = "llamacpp"
|
||||
model_id = "remote-model"
|
||||
url = "http://192.168.1.10:10000"
|
||||
|
||||
[[models]]
|
||||
id = "local-model"
|
||||
model_path = "/models/local.gguf"
|
||||
""".trimIndent()
|
||||
|
||||
val loader = ConfigLoader::class.java
|
||||
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||
parseTomlMethod.isAccessible = true
|
||||
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||
|
||||
assertEquals(1, result.providers.size)
|
||||
assertEquals("remote-provider", result.providers[0].id)
|
||||
|
||||
assertEquals(1, result.models.size)
|
||||
assertEquals("local-model", result.models[0].id)
|
||||
assertEquals("/models/local.gguf", result.models[0].modelPath)
|
||||
assertEquals(8192, result.models[0].contextSize)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user