feat: switchable router embedder and L3 backends via config

Adds [router.embedder] and [router.l3] sections to CorrexConfig with
backend selectors. Ships LlamaCppEmbedder that hits llama.cpp's
/embedding endpoint (handles OpenAI-compatible, simple, and array
response shapes; validates dimension). InfrastructureModule gains
createEmbedderFromConfig and createL3MemoryStoreFromConfig that
dispatch on backend value.

Defaults preserve current behavior (noop embedder + in-memory L3).
Switching to "llamacpp" / "turbovec" is a config-only change — no code
edits required. For turbovec backend, the bundled python sidecar
script is extracted from classpath to ~/.cache/correx/ on first use.
This commit is contained in:
2026-05-30 01:23:14 +04:00
parent 84a7568e15
commit d68c76ee3c
11 changed files with 496 additions and 2 deletions
@@ -210,6 +210,9 @@ object ConfigLoader {
val toolsFileReadSection = sections["tools.file_read"] ?: emptyMap()
val toolsFileWriteSection = sections["tools.file_write"] ?: emptyMap()
val toolsFileEditSection = sections["tools.file_edit"] ?: emptyMap()
val routerSection = sections["router"] ?: emptyMap()
val routerEmbedderSection = sections["router.embedder"] ?: emptyMap()
val routerL3Section = sections["router.l3"] ?: emptyMap()
val server = ServerConfig(
host = asString(serverSection["host"], "localhost"),
@@ -322,6 +325,27 @@ object ConfigLoader {
)
}
val embedder = EmbedderConfig(
backend = asString(routerEmbedderSection["backend"], "noop"),
dimension = asInt(routerEmbedderSection["dimension"], 1536),
url = asStringOrNull(routerEmbedderSection["url"]),
modelId = asStringOrNull(routerEmbedderSection["model_id"]),
)
val l3 = L3Config(
backend = asString(routerL3Section["backend"], "in_memory"),
persistPath = asStringOrNull(routerL3Section["persist_path"]),
pythonExecutable = asString(routerL3Section["python_executable"], "python3"),
scriptPath = asStringOrNull(routerL3Section["script_path"]),
dim = asInt(routerL3Section["dim"], 1536),
bitWidth = asInt(routerL3Section["bit_width"], 4),
)
val router = RouterConfig(
embedder = embedder,
l3 = l3,
)
return CorrexConfig(
server = server,
tui = tui,
@@ -329,6 +353,7 @@ object ConfigLoader {
approval = approval,
tools = tools,
providers = providers,
router = router,
)
}
@@ -10,6 +10,7 @@ data class CorrexConfig(
val approval: ApprovalConfig = ApprovalConfig(),
val tools: ToolsConfig = ToolsConfig(),
val providers: List<ProviderConfig> = emptyList(),
val router: RouterConfig = RouterConfig(),
)
@Serializable
@@ -55,3 +56,27 @@ data class ProviderConfig(
val url: String = "http://127.0.0.1:10000",
val capabilities: Map<String, Double> = emptyMap(),
)
@Serializable
data class RouterConfig(
val embedder: EmbedderConfig = EmbedderConfig(),
val l3: L3Config = L3Config(),
)
@Serializable
data class EmbedderConfig(
val backend: String = "noop",
val dimension: Int = 1536,
val url: String? = null,
val modelId: String? = null,
)
@Serializable
data class L3Config(
val backend: String = "in_memory",
val persistPath: String? = null,
val pythonExecutable: String = "python3",
val scriptPath: String? = null,
val dim: Int = 1536,
val bitWidth: Int = 4,
)
@@ -217,4 +217,62 @@ class ConfigLoaderTest {
assertEquals(1.0, result.providers[0].capabilities["General"])
assertEquals(0.7, result.providers[0].capabilities["Coding"])
}
@Test
fun `parseToml parses router embedder and l3 config`() {
val toml = """
[router.embedder]
backend = "llamacpp"
dimension = 1536
url = "http://localhost:11000"
model_id = "nomic-embed-text"
[router.l3]
backend = "turbovec"
persist_path = "/tmp/router/l3.tq"
python_executable = "python3"
dim = 1536
bit_width = 4
""".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("llamacpp", result.router.embedder.backend)
assertEquals(1536, result.router.embedder.dimension)
assertEquals("http://localhost:11000", result.router.embedder.url)
assertEquals("nomic-embed-text", result.router.embedder.modelId)
assertEquals("turbovec", result.router.l3.backend)
assertEquals("/tmp/router/l3.tq", result.router.l3.persistPath)
assertEquals("python3", result.router.l3.pythonExecutable)
assertEquals(1536, result.router.l3.dim)
assertEquals(4, result.router.l3.bitWidth)
}
@Test
fun `parseToml applies router defaults 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("noop", result.router.embedder.backend)
assertEquals(1536, result.router.embedder.dimension)
assertEquals(null, result.router.embedder.url)
assertEquals(null, result.router.embedder.modelId)
assertEquals("in_memory", result.router.l3.backend)
assertEquals(null, result.router.l3.persistPath)
assertEquals("python3", result.router.l3.pythonExecutable)
assertEquals(1536, result.router.l3.dim)
assertEquals(4, result.router.l3.bitWidth)
}
}