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:
@@ -8,6 +8,8 @@ import com.correx.core.artifacts.repository.ArtifactRepository
|
||||
import com.correx.core.artifactstore.ArtifactStore
|
||||
import com.correx.core.config.EmbedderConfig
|
||||
import com.correx.core.config.L3Config
|
||||
import com.correx.core.config.ModelConfig
|
||||
import com.correx.core.config.ModelsSettings
|
||||
import com.correx.core.events.EventDispatcher
|
||||
import com.correx.core.events.stores.EventStore
|
||||
import com.correx.core.inference.CapabilityScore
|
||||
@@ -35,8 +37,11 @@ import com.correx.infrastructure.artifactscas.index.SqliteArtifactIndex
|
||||
import com.correx.infrastructure.inference.DefaultProviderRegistry
|
||||
import com.correx.infrastructure.inference.commons.ModelDescriptor
|
||||
import com.correx.infrastructure.inference.commons.ResidencyMode
|
||||
import com.correx.infrastructure.inference.llama.cpp.DefaultModelManager
|
||||
import com.correx.infrastructure.inference.llama.cpp.LlamaCppEmbedder
|
||||
import com.correx.infrastructure.inference.llama.cpp.LlamaCppInferenceProvider
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import com.correx.infrastructure.router.turbovec.TurboVecL3MemoryStore
|
||||
import com.correx.infrastructure.router.turbovec.TurboVecSidecarConfig
|
||||
import com.correx.infrastructure.persistence.SqliteEventStore
|
||||
@@ -110,6 +115,49 @@ object InfrastructureModule {
|
||||
baseUrl = baseUrl,
|
||||
)
|
||||
|
||||
fun createModelManager(
|
||||
settings: ModelsSettings,
|
||||
eventStore: EventStore,
|
||||
eventDispatcher: EventDispatcher,
|
||||
httpClient: HttpClient = HttpClient(CIO),
|
||||
healthTimeoutMs: Long = 30_000L,
|
||||
): DefaultModelManager = DefaultModelManager(
|
||||
llamaServerBin = settings.llamaServerBin,
|
||||
host = settings.host,
|
||||
port = settings.port,
|
||||
healthTimeoutMs = healthTimeoutMs,
|
||||
eventStore = eventStore,
|
||||
httpClient = httpClient,
|
||||
eventDispatcher = eventDispatcher,
|
||||
)
|
||||
|
||||
fun modelConfigToDescriptor(config: ModelConfig): ModelDescriptor = ModelDescriptor(
|
||||
modelId = config.id,
|
||||
modelPath = config.modelPath,
|
||||
residencyMode = ResidencyMode.PERSISTENT,
|
||||
contextSize = config.contextSize,
|
||||
capabilities = parseCapabilitiesFromConfig(config.capabilities),
|
||||
)
|
||||
|
||||
private fun parseCapabilitiesFromConfig(capsMap: Map<String, Double>): Set<CapabilityScore> {
|
||||
val result = mutableSetOf<CapabilityScore>()
|
||||
capsMap.forEach { (capName, score) ->
|
||||
val capability = when (capName.lowercase()) {
|
||||
"general" -> ModelCapability.General
|
||||
"coding" -> ModelCapability.Coding
|
||||
"reasoning" -> ModelCapability.Reasoning
|
||||
"summarization" -> ModelCapability.Summarization
|
||||
"toolcalling" -> ModelCapability.ToolCalling
|
||||
else -> {
|
||||
log.warn("Unknown capability name '{}' in [[models]] config", capName)
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
result.add(CapabilityScore(capability, score))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun createProviderRegistry(
|
||||
providers: List<InferenceProvider> = emptyList(),
|
||||
): DefaultProviderRegistry = DefaultProviderRegistry(providers)
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package com.correx.infrastructure
|
||||
|
||||
import com.correx.core.config.ModelConfig
|
||||
import com.correx.core.config.ModelsSettings
|
||||
import com.correx.core.events.EventDispatcher
|
||||
import com.correx.core.inference.ModelCapability
|
||||
import com.correx.infrastructure.inference.commons.ResidencyMode
|
||||
import com.correx.infrastructure.persistence.InMemoryEventStore
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.mock.MockEngine
|
||||
import io.ktor.client.engine.mock.respond
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.headersOf
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class InfrastructureModuleModelTest {
|
||||
|
||||
@Test
|
||||
fun `modelConfigToDescriptor maps fields correctly`() {
|
||||
val config = ModelConfig(
|
||||
id = "my-model",
|
||||
modelPath = "/models/my-model.gguf",
|
||||
contextSize = 4096,
|
||||
capabilities = mapOf("General" to 1.0, "Coding" to 0.8),
|
||||
)
|
||||
|
||||
val descriptor = InfrastructureModule.modelConfigToDescriptor(config)
|
||||
|
||||
assertEquals("my-model", descriptor.modelId)
|
||||
assertEquals("/models/my-model.gguf", descriptor.modelPath)
|
||||
assertEquals(4096, descriptor.contextSize)
|
||||
assertEquals(ResidencyMode.PERSISTENT, descriptor.residencyMode)
|
||||
assertEquals(2, descriptor.capabilities.size)
|
||||
val generalScore = descriptor.capabilities.firstOrNull { it.capability == ModelCapability.General }
|
||||
assertNotNull(generalScore)
|
||||
assertEquals(1.0, generalScore!!.score)
|
||||
val codingScore = descriptor.capabilities.firstOrNull { it.capability == ModelCapability.Coding }
|
||||
assertNotNull(codingScore)
|
||||
assertEquals(0.8, codingScore!!.score)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `modelConfigToDescriptor handles empty capabilities`() {
|
||||
val config = ModelConfig(
|
||||
id = "bare-model",
|
||||
modelPath = "/models/bare.gguf",
|
||||
)
|
||||
|
||||
val descriptor = InfrastructureModule.modelConfigToDescriptor(config)
|
||||
|
||||
assertEquals("bare-model", descriptor.modelId)
|
||||
assertEquals(8192, descriptor.contextSize)
|
||||
assertEquals(0, descriptor.capabilities.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createModelManager returns DefaultModelManager with correct settings`() {
|
||||
val settings = ModelsSettings(
|
||||
defaultModel = "test-model",
|
||||
llamaServerBin = "/custom/llama-server",
|
||||
host = "10.0.0.1",
|
||||
port = 12345,
|
||||
)
|
||||
val eventStore = InMemoryEventStore()
|
||||
val dispatcher = EventDispatcher(eventStore)
|
||||
val mockEngine = MockEngine {
|
||||
respond(
|
||||
content = """{"status":"healthy"}""",
|
||||
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()),
|
||||
)
|
||||
}
|
||||
val httpClient = HttpClient(mockEngine)
|
||||
|
||||
val manager = InfrastructureModule.createModelManager(
|
||||
settings = settings,
|
||||
eventStore = eventStore,
|
||||
eventDispatcher = dispatcher,
|
||||
httpClient = httpClient,
|
||||
)
|
||||
|
||||
assertNotNull(manager)
|
||||
// currentModel is null before any load
|
||||
assertEquals(null, manager.currentModel())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user