feat: correx-managed model lifecycle slice 2 — per-stage model selection

StageConfig.modelId; InferenceRouter gains a backward-compatible model-aware
route() overload (default ignores modelId, so static routers and test fakes are
unaffected). New ManagedInferenceRouter (infra commons) resolves a target model
per stage (stage.modelId > capability match > default) and ensureLoaded()s it
before routing, returning the live managed provider — its id changes with the
resident model, so there is no stale health cache to invalidate on swap.
ModelManager.ensureLoaded default delegates to the idempotent load(). Main wires
the managed router on the [[models]] path; the static path keeps DefaultInferenceRouter.

Plan: docs/plans/2026-05-31-model-lifecycle-management.md (slice 2 of 5).
This commit is contained in:
2026-06-01 11:35:51 +04:00
parent e45a626cc4
commit 382194b498
8 changed files with 240 additions and 2 deletions
@@ -16,6 +16,7 @@ import com.correx.core.inference.CapabilityScore
import com.correx.core.inference.DefaultInferenceRouter
import com.correx.core.inference.InferenceProjector
import com.correx.core.inference.InferenceRepository
import com.correx.core.inference.InferenceRouter
import com.correx.core.inference.ModelCapability
import com.correx.core.kernel.orchestration.DefaultOrchestrationReducer
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
@@ -47,6 +48,7 @@ import com.correx.core.toolintent.rules.PathContainmentRule
import com.correx.infrastructure.InfrastructureModule
import com.correx.infrastructure.inference.DefaultProviderRegistry
import com.correx.infrastructure.inference.FirstAvailableRoutingStrategy
import com.correx.infrastructure.inference.commons.ManagedInferenceRouter
import com.correx.core.inference.InferenceProvider
import com.correx.infrastructure.inference.llama.cpp.LlamaCppInferenceProvider
import com.correx.infrastructure.tools.FileEditConfig
@@ -78,10 +80,12 @@ fun main() {
// Static path: [[models]] absent → legacy [[providers]] / env-var behavior
val firstProvider: InferenceProvider
val infraRegistry: DefaultProviderRegistry
val inferenceRouter: InferenceRouter
if (correxConfig.models.isNotEmpty()) {
val settings = correxConfig.modelsSettings
val modelManager = InfrastructureModule.createModelManager(settings, eventStore, eventDispatcher)
val descriptors = correxConfig.models.map { InfrastructureModule.modelConfigToDescriptor(it) }
val targetId = settings.defaultModel ?: correxConfig.models.first().id
val targetModelConfig = correxConfig.models.firstOrNull { it.id == targetId }
?: correxConfig.models.first()
@@ -97,6 +101,9 @@ fun main() {
else -> { log.error("Unknown provider type: {}", providerConfig.type); null }
}
}.forEach { infraRegistry.register(it) }
// Managed router owns per-stage model selection: it ensures the resolved model is resident
// before routing (stage.modelId > capability match > default), evicting the prior model.
inferenceRouter = ManagedInferenceRouter(modelManager, descriptors, targetModelConfig.id)
// Shutdown hook to kill the managed llama-server
Runtime.getRuntime().addShutdownHook(Thread {
log.info("Shutdown: unloading managed model '{}'", targetModelConfig.id)
@@ -110,6 +117,7 @@ fun main() {
require(staticProviders.isNotEmpty()) { "At least one provider must be configured" }
firstProvider = staticProviders.first()
infraRegistry = InfrastructureModule.createProviderRegistry(staticProviders)
inferenceRouter = DefaultInferenceRouter(infraRegistry, FirstAvailableRoutingStrategy())
}
val modelId = firstProvider.id.value
@@ -161,7 +169,6 @@ fun main() {
),
)
val inferenceRouter = DefaultInferenceRouter(infraRegistry, FirstAvailableRoutingStrategy())
val engines = OrchestratorEngines(
transitionResolver = DefaultTransitionResolver { condition, ctx -> condition.evaluate(ctx) },
contextPackBuilder = DefaultContextPackBuilder(DefaultContextCompressor()),