diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index e797553a59..02a63765b3 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -18,6 +18,8 @@ import ai.kilocode.rpc.dto.MessageDto import ai.kilocode.rpc.dto.MessageErrorDto import ai.kilocode.rpc.dto.MessageTimeDto import ai.kilocode.rpc.dto.MessageWithPartsDto +import ai.kilocode.rpc.dto.ModelDto +import ai.kilocode.rpc.dto.ModelLimitDto import ai.kilocode.rpc.dto.ModelSelectionDto import ai.kilocode.rpc.dto.ModelStateDto import ai.kilocode.rpc.dto.PartDto @@ -437,22 +439,7 @@ object KiloCliDataParser { source = item.str("source"), key = item.str("key"), metadata = parseProviderMetadata(item["metadata"].obj()), - models = item["models"]?.jsonObject?.mapValues { (id, v) -> - val model = parseModel(id, v.jsonObject) - ai.kilocode.rpc.dto.ModelDto( - id = model.id, - name = model.name, - attachment = model.attachment, - reasoning = model.reasoning, - temperature = model.temperature, - toolCall = model.toolCall, - free = model.free, - status = model.status, - recommendedIndex = model.recommendedIndex, - variants = model.variants, - limit = model.limit?.let { ai.kilocode.rpc.dto.ModelLimitDto(it.context, it.input, it.output) }, - ) - } ?: emptyMap(), + models = item["models"]?.jsonObject?.mapValues { (id, v) -> parseModelDto(id, v.jsonObject) } ?: emptyMap(), ) } ?: emptyList() val connected = obj["connected"]?.jsonArray?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList() @@ -984,13 +971,30 @@ object KiloCliDataParser { if (obj == null) return null val dto = ProviderMetadataDto( noteKey = obj.str("noteKey"), - note = obj.str("note"), icon = obj.str("icon"), + priority = obj.num("priority")?.toInt(), ) - if (dto.noteKey == null && dto.note == null && dto.icon == null) return null + if (dto.noteKey == null && dto.icon == null && dto.priority == null) return null return dto } + private fun parseModelDto(id: String, obj: JsonObject): ModelDto { + val model = parseModel(id, obj) + return ModelDto( + id = model.id, + name = model.name, + attachment = model.attachment, + reasoning = model.reasoning, + temperature = model.temperature, + toolCall = model.toolCall, + free = model.free, + status = model.status, + recommendedIndex = model.recommendedIndex, + variants = model.variants, + limit = model.limit?.let { ModelLimitDto(it.context, it.input, it.output) }, + ) + } + private fun parseModel(id: String, obj: JsonObject): ModelInfo { val cap = obj["capabilities"]?.jsonObject val limit = obj["limit"]?.jsonObject diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/provider/KiloBackendProviderSettingsManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/provider/KiloBackendProviderSettingsManager.kt index 4f00ecee7d..074183089a 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/provider/KiloBackendProviderSettingsManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/provider/KiloBackendProviderSettingsManager.kt @@ -75,7 +75,7 @@ internal class KiloBackendProviderSettingsManager( result.providers.forEach { provider -> val configured = provider.id in result.connected || provider.key != null || provider.source == "config" || provider.id in result.config LOG.debug { - "provider settings provider: id=${provider.id} source=${provider.source} connected=${provider.id in result.connected} configured=$configured disabled=${provider.id in result.disabled} enabled=${provider.id in result.enabled} hasKey=${provider.key != null} auth=${result.auth[provider.id].orEmpty().map { it.type }.distinct().joinToString(",")} config=${provider.id in result.config} models=${provider.models.size} description=${provider.description?.isNotBlank() == true} note=${provider.metadata?.note?.isNotBlank() == true} noteKey=${provider.metadata?.noteKey} icon=${provider.metadata?.icon}" + "provider settings provider: id=${provider.id} source=${provider.source} connected=${provider.id in result.connected} configured=$configured disabled=${provider.id in result.disabled} enabled=${provider.id in result.enabled} hasKey=${provider.key != null} auth=${result.auth[provider.id].orEmpty().map { it.type }.distinct().joinToString(",")} config=${provider.id in result.config} models=${provider.models.size} description=${provider.description?.isNotBlank() == true} noteKey=${provider.metadata?.noteKey} icon=${provider.metadata?.icon} priority=${provider.metadata?.priority}" } } LOG.debug { "provider settings state: completed dir=$directory providers=${result.providers.size} connected=${result.connected.size} auth=${result.auth.size} errors=${result.errors.size} durationMs=${System.currentTimeMillis() - start}" } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt index 49ec5ffbed..5c824749af 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt @@ -1317,8 +1317,8 @@ class KiloCliDataParserTest { "source": "api", "metadata": { "noteKey": "settings.providers.note.openai", - "note": "GPT and Codex models with API key or ChatGPT login", "icon": "openai", + "priority": 3, "extra": true }, "unknown": "ok", @@ -1333,8 +1333,8 @@ class KiloCliDataParserTest { assertEquals("settings.providers.note.openai", provider.metadata?.noteKey) assertEquals("Build with OpenAI models", provider.description) - assertEquals("GPT and Codex models with API key or ChatGPT login", provider.metadata?.note) assertEquals("openai", provider.metadata?.icon) + assertEquals(3, provider.metadata?.priority) assertEquals(listOf("openai"), result.second) assertEquals(mapOf("code" to "openai/gpt-5"), result.third) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProviderCatalog.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProviderCatalog.kt index c4e260cd9a..b2123822e9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProviderCatalog.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProviderCatalog.kt @@ -18,19 +18,13 @@ import kotlin.math.roundToInt internal const val KILO_PROVIDER_ID = "kilo" internal const val CUSTOM_PROVIDER_PACKAGE = "@ai-sdk/openai-compatible" -internal val POPULAR_PROVIDER_IDS = listOf(KILO_PROVIDER_ID, "anthropic", "deepseek", "openai", "google", "openrouter", "vercel") +internal fun isPopularProvider(provider: ProviderSettingsProviderDto) = provider.metadata?.priority != null -internal fun isPopularProvider(id: String) = id in POPULAR_PROVIDER_IDS - -internal fun popularProviderIndex(id: String): Int { - val index = POPULAR_PROVIDER_IDS.indexOf(id) - return if (index >= 0) index else Int.MAX_VALUE -} +internal fun popularProviderIndex(provider: ProviderSettingsProviderDto): Int = provider.metadata?.priority ?: Int.MAX_VALUE internal fun providerDescription(provider: ProviderSettingsProviderDto): String { provider.description?.takeIf { it.isNotBlank() }?.let { return it } provider.metadata?.noteKey?.let { key -> KiloBundle.optional(key)?.let { return it } } - provider.metadata?.note?.let { return it } return "" } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProviderListRows.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProviderListRows.kt index a907e52923..0ff1d77322 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProviderListRows.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProviderListRows.kt @@ -31,14 +31,14 @@ internal fun providerListRows(state: ProviderSettingsDto, query: String, disable val filtered = state.providers.filter { ModelSearch.matches(q, it.name) } val connected = filtered .filter { configured(it, state, ids) } - .sortedWith(compareBy { popularProviderIndex(it.id) }.thenBy { it.name.lowercase() }.thenBy { it.id }) + .sortedWith(compareBy { popularProviderIndex(it) }.thenBy { it.name.lowercase() }.thenBy { it.id }) val connectedIds = connected.mapTo(mutableSetOf()) { it.id } val popular = filtered .filter { it.id !in connectedIds } .filter { it.id !in disabled } .filter { !hiddenProvider(it) } - .filter { isPopularProvider(it.id) } - .sortedWith(compareBy { popularProviderIndex(it.id) }.thenBy { it.name.lowercase() }.thenBy { it.id }) + .filter { isPopularProvider(it) } + .sortedWith(compareBy { popularProviderIndex(it) }.thenBy { it.name.lowercase() }.thenBy { it.id }) val popularIds = popular.mapTo(mutableSetOf()) { it.id } val all = filtered .filter { it.id !in connectedIds } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt index 003d705786..289eef45c4 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt @@ -147,13 +147,13 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() { val rows = providerListRows( ProviderSettingsDto( providers = listOf( - provider("openrouter", "OpenRouter"), - provider("kilo", "Kilo"), - provider("google", "Google"), - provider("anthropic", "Anthropic"), - provider("vercel", "Vercel"), - provider("openai", "OpenAI"), - provider("deepseek", "DeepSeek"), + provider("openrouter", "OpenRouter", priority = 5), + provider("kilo", "Kilo", priority = 0), + provider("google", "Google", priority = 4), + provider("anthropic", "Anthropic", priority = 1), + provider("vercel", "Vercel", priority = 6), + provider("openai", "OpenAI", priority = 3), + provider("deepseek", "DeepSeek", priority = 2), ), ), "", @@ -166,7 +166,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() { fun `test connected providers appear first and are not duplicated in popular section`() { val rows = providerListRows( ProviderSettingsDto( - providers = listOf(provider("anthropic", "Anthropic"), provider("openai", "OpenAI")), + providers = listOf(provider("anthropic", "Anthropic", priority = 1), provider("openai", "OpenAI", priority = 3)), connected = listOf("anthropic"), ), "", @@ -183,7 +183,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() { val rows = providerListRows( ProviderSettingsDto( providers = listOf( - provider("anthropic", "Anthropic", source = "custom"), + provider("anthropic", "Anthropic", source = "custom", priority = 1), provider("available-custom", "Available Custom", source = "custom"), provider("local-openai", "Local OpenAI", source = "custom"), ), @@ -227,7 +227,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() { fun `test disabled popular provider appears in all providers with enable`() { val rows = providerListRows( ProviderSettingsDto( - providers = listOf(provider("anthropic", "Anthropic"), provider("openai", "OpenAI")), + providers = listOf(provider("anthropic", "Anthropic", priority = 1), provider("openai", "OpenAI", priority = 3)), disabled = listOf("anthropic"), ), "", @@ -244,7 +244,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() { providers = listOf( provider("zeta", "Zeta"), provider("alpha", "Alpha"), - provider("openai", "OpenAI"), + provider("openai", "OpenAI", priority = 3), ), ), "", @@ -260,8 +260,8 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() { content.update( ProviderSettingsDto( providers = listOf( - provider("openai", "OpenAI"), - provider("anthropic", "Anthropic"), + provider("openai", "OpenAI", priority = 3), + provider("anthropic", "Anthropic", priority = 1), provider("alpha", "Alpha Labs"), ), ), @@ -402,7 +402,6 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() { "OpenAI", metadata = ProviderMetadataDto( noteKey = "settings.providers.note.openai", - note = "GPT and Codex models with API key or ChatGPT login", icon = "openai", ), ), @@ -428,7 +427,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() { "openai", "OpenAI", description = "Build with OpenAI models", - metadata = ProviderMetadataDto(note = "Fallback metadata note"), + metadata = ProviderMetadataDto(noteKey = "settings.providers.note.openai"), ), "Popular providers", listOf(ProviderListAction.CONNECT), @@ -781,12 +780,13 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() { description: String? = null, source: String? = null, metadata: ProviderMetadataDto? = null, + priority: Int? = null, ) = ProviderSettingsProviderDto( id = id, name = name, description = description, source = source, - metadata = metadata, + metadata = metadata ?: priority?.let { ProviderMetadataDto(priority = it) }, models = mapOf("model" to ModelDto("model", "Model")), ) diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderSettingsDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderSettingsDto.kt index 4b5ce1ec2c..bfd733b99c 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderSettingsDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderSettingsDto.kt @@ -30,8 +30,8 @@ data class ProviderSettingsProviderDto( @Serializable data class ProviderMetadataDto( val noteKey: String? = null, - val note: String? = null, val icon: String? = null, + val priority: Int? = null, ) @Serializable diff --git a/packages/kilo-vscode/src/shared/provider-model.ts b/packages/kilo-vscode/src/shared/provider-model.ts index 77c74f8ac2..66d8e9099f 100644 --- a/packages/kilo-vscode/src/shared/provider-model.ts +++ b/packages/kilo-vscode/src/shared/provider-model.ts @@ -5,6 +5,7 @@ export type CustomProviderPackage = (typeof CUSTOM_PROVIDER_PACKAGES)[number] export const CUSTOM_PROVIDER_PACKAGE: CustomProviderPackage = "@ai-sdk/openai-compatible" export const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-_]*$/ +// Legacy/static fallback for provider objects created before backend metadata is available. export const PROVIDER_PRIORITY = [ KILO_PROVIDER_ID, "anthropic", @@ -37,6 +38,11 @@ export function createKiloFallbackProvider() { name: "Kilo Gateway", source: "custom" as const, env: ["KILO_API_KEY"], + metadata: { + noteKey: "settings.providers.note.kilo", + icon: KILO_PROVIDER_ID, + priority: 0, + }, models: {}, } } diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ProviderSelectDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ProviderSelectDialog.tsx index 1439ca379d..c25a40d26e 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ProviderSelectDialog.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ProviderSelectDialog.tsx @@ -81,7 +81,7 @@ const ProviderSelectDialog = () => { items={items()} filterKeys={["id", "name"]} groupBy={(item) => - item.id !== CUSTOM_PROVIDER_ID && isPopularProvider(item.id) + item.id !== CUSTOM_PROVIDER_ID && isPopularProvider(item.provider ?? item.id) ? language.t("settings.providers.group.recommended") : language.t("dialog.provider.group.other") } @@ -89,7 +89,7 @@ const ProviderSelectDialog = () => { if (a.id === CUSTOM_PROVIDER_ID) return -1 if (b.id === CUSTOM_PROVIDER_ID) return 1 - const rank = popularProviderIndex(a.id) - popularProviderIndex(b.id) + const rank = popularProviderIndex(a.provider ?? a.id) - popularProviderIndex(b.provider ?? b.id) if (rank !== 0) return rank return a.name.localeCompare(b.name) }} @@ -106,7 +106,12 @@ const ProviderSelectDialog = () => { > {(item) => (
- +
{ return sortProviders( all.filter( (item) => - item.id !== KILO_PROVIDER_ID && - isPopularProvider(item.id) && - !connected.has(item.id) && - !disabled.has(item.id), + item.id !== KILO_PROVIDER_ID && isPopularProvider(item) && !connected.has(item.id) && !disabled.has(item.id), ), ) }) @@ -289,7 +286,6 @@ const ProvidersTab: Component = () => { {(item) => { const noteKey = providerNoteKey(item) - const note = providerNote(item) return (
{ )} - - {(text) => ( - - {text()} - - )} -