Is this a type system bug or am I missing somethin...
# getting-started
s
Is this a type system bug or am I missing something? (kotlin version: 2.2.21) So this code works: (notice that
OpenRouterModels
is an object, so
OpenRouterModels
can denote a type or an instance)
Copy code
import ai.koog.prompt.llm.LLModel
import ai.koog.prompt.executor.clients.openrouter.OpenRouterModels
import kotlin.reflect.full.memberProperties

private val openRouterModelsByName: Map<String, LLModel> = OpenRouterModels::class.memberProperties
    .filter { it.returnType.classifier == LLModel::class }
    .associate { it.name to it.get(OpenRouterModels) as LLModel }
and I would expect that the following code is equivalent (it's another way of doing the filtering):
Copy code
private val openRouterModelsByName: Map<String, LLModel> = OpenRouterModels::class.memberProperties
    .mapNotNull {
        val name: String = it.name
        val model: LLModel? = it.get(OpenRouterModels) as? LLModel
        model?.let { name to model }
    }.toMap()
alas the
it.get(OpenRouterModels)
fails 🤯with Caused by: java.lang.IllegalArgumentException: Callable expects 0 arguments, but 1 were provided. even though IntelliJ shows no problem with the code.
OpenRouterModels
is defined in "ai.koogkoog agents0.5.3" but I don't expect this issue to be linked to koog.
1
c
These two code samples are only equivalent if `it.get(OpenRouterModels)`never throws an exception, but that's apparently what's going on. Try:
Copy code
private val openRouterModelsByName: Map<String, LLModel> = OpenRouterModels::class.memberProperties
    .mapNotNull {
        if (it.returnType.classifier != LLModel::class)
            return@mapNotNull null

        val name: String = it.name
        val model: LLModel? = it.get(OpenRouterModels) as? LLModel
        model?.let { name to model }
    }.toMap()
1
Maybe log the
name
of the field that complains and see what its signature is?
1
y
Yeah it seems like that some
memberProperty
is somehow not actually a member... I think you could check if the underlying java function is static or not. The first debugging step I'd do is just print all the member properties
s
The code fails for the property:
Copy code
private val additionalCapabilities: List<LLMCapability> = listOf(
        LLMCapability.Schema.JSON.Standard,
        LLMCapability.ToolChoice
    )
which is the first private property that is iterated over, but that isn't an explaination to way
get
shouldn't have a single parameter (the instance that this property is to be extracted from) !?
@Youssef Shoaib [MOD] memberProperty is defined in KClasses.kt:
Copy code
@SinceKotlin("1.1")
val <T : Any> KClass<T>.memberProperties: Collection<KProperty1<T, *>>
    get() = (this as KClassImpl<T>).data.value.allNonStaticMembers.filter { it.isNotExtension && it is KProperty1<*, *> } as Collection<KProperty1<T, *>>
y
That's really strange. I mean, this code shouldn't work for private properties anyways since you haven't marked their field accessible. But somehow, for some bizarre reason, this 0-arg error is thrown instead of complaining about access violation. Maybe see where exactly the error is thrown? That might help explain why it's behaving so weirdly
s
gradle completely swallows the StackTrace, it only shows errors in Gradle. I tried to print the stacktrace myself via:
Copy code
val OpenRouterModels.modelsByName: Map<String, LLModel>
    get() {
        return runCatching {
            openRouterModelsByName
        }.getOrElse{e->
            e.printStackTrace(System.out)
            throw e
        }
    }
after making the computation lazy
Copy code
private val openRouterModelsByName: Map<String, LLModel> by lazy {
    OpenRouterModels::class.memberProperties
        .mapNotNull {
            val name: String = it.name
            println("name: $name")
            val model: LLModel? = it.get(OpenRouterModels) as? LLModel
            model?.let { name to model }
        }.toMap().also { map ->
            require(map.isNotEmpty()) { "No LLModels were found in OpenRouterModels." }
        }
}
but still no stacktrace is printed 🤯🤷‍♂️
Is Koog's
additionalCapabilities
marked
@JvmField
? If so, then it's https://youtrack.jetbrains.com/issue/KT-55872/Reflection-redundant-instanceParameter-on-property-annotated-with-JvmField As a workaround, I'd add a try catch around the
get
call that checks for an IAE with that specific message, and then recover by doing a call with no arguments
s
I pushed my code to Codeberg.
y
Copy code
runCatching { it.get(OpenRoutersModels) }.getOrElse { if (it is IllegalArgumentException && it.message == "Callable expects 0 arguments, but 1 were provided.") it.get() else throw it }
s
why would printing this specific error work when printing any error doesn't?
I can break on that error:
y
Not printing. It's a workaround for the bug in Kotlin reflection. The linked issue shows that it messes up and shows some properties as KProperty1, that then error when called. It seems to happen with
const
and
@JvmField
properties, but some mockk issues mention something similar for private properties. Either way, it's a pretty solid workaround
c
As a workaround, I'd add a try catch around the
get
call that checks for an IAE with that specific message, and then recover by doing a call with no arguments
Honestly, in this situation, I think keeping the initial code is better. Testing if the declared output is a given type is much cheaper than calling all the properties (which would trigger lazy initialization etc) when you're only interested by the result of a few of them
y
Well, if
OpenRoutersModels
adds a private or
@JvmField
property in the future, it'll retrigger the bug...
s
I've already got a workaround, filtering by return type first:
Copy code
private val openRouterModelsByName: Map<String, LLModel> by lazy {
    OpenRouterModels::class.memberProperties
        .filter { it.returnType.classifier == LLModel::class }
        .associate { it.name to it.get(OpenRouterModels) as LLModel }
}
I've added a link to my example to the bug report you mentioned 👍
y
Again, this could theoretically break if
OpenRouterModels
gains a new
private
or
@JvmField
property. At the very least, I'd filter out
private
properties, since you likely don't want them anyway (and they'll fail, I think, since you'll be trying to access a JVM Field without calling
setAccessible
on it first)
s
so
isAccessible
is correct for that, isn't it?
Copy code
.filter { it.isAccessible && it.returnType.classifier == LLModel::class }
or
Copy code
.filter { it.visibility == KVisibility.PUBLIC && it.returnType.classifier == LLModel::class }
✔️ 1
ok, i checked and
Copy code
private val openRouterModelsByName: Map<String, LLModel> = OpenRouterModels::class.memberProperties
    .mapNotNull {
        if (it.visibility == KVisibility.PUBLIC) {
            val name: String = it.name
            val model: LLModel? = it.get(OpenRouterModels) as? LLModel
            model?.let { name to model }
       } else null
    }.toMap()
now works as well 👍 even though I prefer the version with
associate
.