epic-12: after epic audit and init commit
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
package com.correx.infrastructure.inference
|
||||
|
||||
import com.correx.core.events.types.ProviderId
|
||||
import com.correx.core.inference.InferenceProvider
|
||||
import com.correx.core.inference.ModelCapability
|
||||
import com.correx.core.inference.ProviderHealth
|
||||
import com.correx.core.inference.ProviderRegistry
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* In-memory [ProviderRegistry] backed by a [CopyOnWriteArrayList] for safe concurrent reads.
|
||||
*
|
||||
* Providers supplied at construction are registered immediately.
|
||||
* Additional providers may be registered at any time via [register].
|
||||
*/
|
||||
class DefaultProviderRegistry(
|
||||
initialProviders: List<InferenceProvider> = emptyList(),
|
||||
) : ProviderRegistry {
|
||||
|
||||
private val providers: CopyOnWriteArrayList<InferenceProvider> =
|
||||
CopyOnWriteArrayList(initialProviders)
|
||||
|
||||
override fun register(provider: InferenceProvider) {
|
||||
providers.add(provider)
|
||||
}
|
||||
|
||||
override fun resolve(capability: ModelCapability): List<InferenceProvider> =
|
||||
providers
|
||||
.filter { p -> p.capabilities().any { it.capability == capability } }
|
||||
.sortedByDescending { p ->
|
||||
p.capabilities().firstOrNull { it.capability == capability }?.score ?: 0.0
|
||||
}
|
||||
|
||||
override fun listAll(): List<InferenceProvider> = providers.toList()
|
||||
|
||||
override suspend fun healthCheckAll(): Map<ProviderId, ProviderHealth> =
|
||||
providers.associate { it.id to it.healthCheck() }
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.correx.infrastructure.inference
|
||||
|
||||
import com.correx.core.inference.InferenceProvider
|
||||
import com.correx.core.inference.ModelCapability
|
||||
import com.correx.core.inference.NoEligibleProviderException
|
||||
import com.correx.core.inference.RoutingStrategy
|
||||
import com.correx.core.events.types.StageId
|
||||
|
||||
/**
|
||||
* Selects the first [InferenceProvider] from [candidates] whose declared capabilities
|
||||
* cover all [requiredCapabilities].
|
||||
*
|
||||
* "First" means the list order as supplied by [ProviderRegistry.resolve] — callers that
|
||||
* want score-ordered selection should pass a score-sorted list.
|
||||
*
|
||||
* @throws NoEligibleProviderException if no candidate satisfies all required capabilities.
|
||||
*/
|
||||
class FirstAvailableRoutingStrategy : RoutingStrategy {
|
||||
override fun select(
|
||||
candidates: List<InferenceProvider>,
|
||||
requiredCapabilities: Set<ModelCapability>,
|
||||
): InferenceProvider {
|
||||
val declared = candidates.firstOrNull { provider ->
|
||||
val providerCapabilities = provider.capabilities().map { it.capability }.toSet()
|
||||
providerCapabilities.containsAll(requiredCapabilities)
|
||||
}
|
||||
return declared ?: throw NoEligibleProviderException(
|
||||
StageId("routing"),
|
||||
requiredCapabilities,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.correx.infrastructure.inference
|
||||
|
||||
object Module
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.correx.infrastructure.inference
|
||||
|
||||
import com.correx.core.events.types.ProviderId
|
||||
import com.correx.core.inference.CapabilityScore
|
||||
import com.correx.core.inference.InferenceProvider
|
||||
import com.correx.core.inference.InferenceRequest
|
||||
import com.correx.core.inference.InferenceResponse
|
||||
import com.correx.core.inference.ModelCapability
|
||||
import com.correx.core.inference.ProviderHealth
|
||||
import com.correx.core.inference.Tokenizer
|
||||
import com.correx.core.inference.Token
|
||||
import com.correx.core.utils.TypeId
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DefaultProviderRegistryTest {
|
||||
|
||||
private fun fakeProvider(
|
||||
id: String,
|
||||
vararg caps: Pair<ModelCapability, Double>,
|
||||
): InferenceProvider = object : InferenceProvider {
|
||||
override val id: ProviderId = TypeId(id)
|
||||
override val name: String = id
|
||||
override val tokenizer: Tokenizer = object : Tokenizer {
|
||||
override suspend fun tokenize(text: String): List<Token> = emptyList()
|
||||
override suspend fun countTokens(text: String): Int = 0
|
||||
}
|
||||
override suspend fun infer(request: InferenceRequest): InferenceResponse =
|
||||
error("not implemented in test")
|
||||
override suspend fun healthCheck(): ProviderHealth = ProviderHealth.Healthy
|
||||
override fun capabilities(): Set<CapabilityScore> =
|
||||
caps.map { (cap, score) -> CapabilityScore(cap, score) }.toSet()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `listAll returns all registered providers`() {
|
||||
val p1 = fakeProvider("p1", ModelCapability.Coding to 0.9)
|
||||
val p2 = fakeProvider("p2", ModelCapability.General to 0.8)
|
||||
val registry = DefaultProviderRegistry(listOf(p1, p2))
|
||||
assertEquals(2, registry.listAll().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register adds provider`() {
|
||||
val registry = DefaultProviderRegistry()
|
||||
val p = fakeProvider("p1", ModelCapability.Coding to 0.9)
|
||||
registry.register(p)
|
||||
assertEquals(1, registry.listAll().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve returns providers with matching capability ordered by score desc`() {
|
||||
val low = fakeProvider("low", ModelCapability.Coding to 0.5)
|
||||
val high = fakeProvider("high", ModelCapability.Coding to 0.9)
|
||||
val other = fakeProvider("other", ModelCapability.General to 0.8)
|
||||
val registry = DefaultProviderRegistry(listOf(low, high, other))
|
||||
|
||||
val resolved = registry.resolve(ModelCapability.Coding)
|
||||
assertEquals(2, resolved.size)
|
||||
assertEquals("high", resolved.first().id.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve returns empty list when no provider matches`() {
|
||||
val registry = DefaultProviderRegistry(listOf(fakeProvider("p", ModelCapability.General to 1.0)))
|
||||
assertTrue(registry.resolve(ModelCapability.Reasoning).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `healthCheckAll returns health for all providers`() = runTest {
|
||||
val p1 = fakeProvider("p1", ModelCapability.Coding to 1.0)
|
||||
val p2 = fakeProvider("p2", ModelCapability.General to 1.0)
|
||||
val registry = DefaultProviderRegistry(listOf(p1, p2))
|
||||
val health = registry.healthCheckAll()
|
||||
assertEquals(2, health.size)
|
||||
assertTrue(health.values.all { it is ProviderHealth.Healthy })
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.correx.infrastructure.inference
|
||||
|
||||
import com.correx.core.events.types.ProviderId
|
||||
import com.correx.core.inference.CapabilityScore
|
||||
import com.correx.core.inference.InferenceProvider
|
||||
import com.correx.core.inference.InferenceRequest
|
||||
import com.correx.core.inference.InferenceResponse
|
||||
import com.correx.core.inference.ModelCapability
|
||||
import com.correx.core.inference.NoEligibleProviderException
|
||||
import com.correx.core.inference.ProviderHealth
|
||||
import com.correx.core.inference.Token
|
||||
import com.correx.core.inference.Tokenizer
|
||||
import com.correx.core.utils.TypeId
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
|
||||
class FirstAvailableRoutingStrategyTest {
|
||||
|
||||
private val strategy = FirstAvailableRoutingStrategy()
|
||||
|
||||
private fun fakeProvider(id: String, vararg caps: ModelCapability): InferenceProvider =
|
||||
object : InferenceProvider {
|
||||
override val id: ProviderId = TypeId(id)
|
||||
override val name: String = id
|
||||
override val tokenizer: Tokenizer = object : Tokenizer {
|
||||
override suspend fun tokenize(text: String): List<Token> = emptyList()
|
||||
override suspend fun countTokens(text: String): Int = 0
|
||||
}
|
||||
override suspend fun infer(request: InferenceRequest): InferenceResponse =
|
||||
error("not implemented in test")
|
||||
override suspend fun healthCheck(): ProviderHealth = ProviderHealth.Healthy
|
||||
override fun capabilities(): Set<CapabilityScore> =
|
||||
caps.map { CapabilityScore(it, 1.0) }.toSet()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selects first provider satisfying all required capabilities`() {
|
||||
val coding = fakeProvider("codingOnly", ModelCapability.Coding)
|
||||
val general = fakeProvider("generalAndCoding", ModelCapability.General, ModelCapability.Coding)
|
||||
val required = setOf(ModelCapability.Coding, ModelCapability.General)
|
||||
|
||||
val selected = strategy.select(listOf(coding, general), required)
|
||||
assertEquals("generalAndCoding", selected.id.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selects first matching provider when multiple satisfy requirements`() {
|
||||
val p1 = fakeProvider("p1", ModelCapability.Coding)
|
||||
val p2 = fakeProvider("p2", ModelCapability.Coding)
|
||||
|
||||
val selected = strategy.select(listOf(p1, p2), setOf(ModelCapability.Coding))
|
||||
assertEquals("p1", selected.id.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws NoEligibleProviderException when no candidate satisfies required capabilities`() {
|
||||
val general = fakeProvider("general", ModelCapability.General)
|
||||
assertThrows<NoEligibleProviderException> {
|
||||
strategy.select(listOf(general), setOf(ModelCapability.Coding))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws NoEligibleProviderException when candidates list is empty`() {
|
||||
assertThrows<NoEligibleProviderException> {
|
||||
strategy.select(emptyList(), setOf(ModelCapability.Coding))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selects any provider when required capabilities is empty`() {
|
||||
val p = fakeProvider("p", ModelCapability.General)
|
||||
val selected = strategy.select(listOf(p), emptySet())
|
||||
assertEquals("p", selected.id.value)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user