fix: remove provider metadata duplication

This commit is contained in:
kirillk
2026-06-16 18:39:47 -04:00
parent 46b24e0c07
commit 6455aa0da4
37 changed files with 102 additions and 311 deletions
@@ -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
@@ -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}" }
@@ -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)
}
@@ -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 ""
}
@@ -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<ProviderSettingsProviderDto> { popularProviderIndex(it.id) }.thenBy { it.name.lowercase() }.thenBy { it.id })
.sortedWith(compareBy<ProviderSettingsProviderDto> { 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<ProviderSettingsProviderDto> { popularProviderIndex(it.id) }.thenBy { it.name.lowercase() }.thenBy { it.id })
.filter { isPopularProvider(it) }
.sortedWith(compareBy<ProviderSettingsProviderDto> { popularProviderIndex(it) }.thenBy { it.name.lowercase() }.thenBy { it.id })
val popularIds = popular.mapTo(mutableSetOf()) { it.id }
val all = filtered
.filter { it.id !in connectedIds }
@@ -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")),
)
@@ -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
@@ -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: {},
}
}
@@ -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) => (
<div style={{ display: "flex", gap: "10px", "align-items": "center", width: "100%", "min-width": 0 }}>
<ProviderIcon id={providerIcon(item.provider ?? item.id)} width={18} height={18} data-slot="list-item-extra-icon" />
<ProviderIcon
id={providerIcon(item.provider ?? item.id)}
width={18}
height={18}
data-slot="list-item-extra-icon"
/>
<div
style={{
display: "flex",
@@ -15,7 +15,7 @@ import type { Provider } from "../../types/messages"
import CustomProviderDialog from "./CustomProviderDialog"
import ProviderConnectDialog from "./ProviderConnectDialog"
import ProviderSelectDialog from "./ProviderSelectDialog"
import { CUSTOM_PROVIDER_ID, isPopularProvider, providerIcon, providerNote, providerNoteKey, sortProviders } from "./provider-catalog"
import { CUSTOM_PROVIDER_ID, isPopularProvider, providerIcon, providerNoteKey, sortProviders } from "./provider-catalog"
import { disabledProviderOptions, providersWithKiloFallback, visibleConnectedIds } from "./provider-visibility"
import { isCustomProviderPackage, KILO_PROVIDER_ID } from "../../../../src/shared/provider-model"
import { createProviderAction } from "../../utils/provider-action"
@@ -53,10 +53,7 @@ const ProvidersTab: Component = () => {
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 = () => {
<For each={popularProviders()}>
{(item) => {
const noteKey = providerNoteKey(item)
const note = providerNote(item)
return (
<div
style={{
@@ -329,19 +325,6 @@ const ProvidersTab: Component = () => {
</span>
)}
</Show>
<Show when={!noteKey && note}>
{(text) => (
<span
style={{
"font-size": "var(--kilo-font-size-12)",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
"margin-top": "2px",
}}
>
{text()}
</span>
)}
</Show>
</div>
<Button size="large" variant="secondary" icon="plus-small" onClick={() => connectProvider(item)}>
{language.t("common.connect")}
@@ -2,22 +2,23 @@ import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
import type { Provider } from "../../types/messages"
import {
KILO_PROVIDER_ID,
PROVIDER_PRIORITY as POPULAR_PROVIDER_IDS,
PROVIDER_PRIORITY as FALLBACK_PROVIDER_IDS,
createKiloFallbackProvider,
providerOrderIndex,
} from "../../../../src/shared/provider-model"
export const CUSTOM_PROVIDER_ID = "_custom"
export { POPULAR_PROVIDER_IDS }
const POPULAR_PROVIDER_SET = new Set<string>(POPULAR_PROVIDER_IDS)
const fallback = new Set<string>(FALLBACK_PROVIDER_IDS)
export function isPopularProvider(providerID: string) {
return POPULAR_PROVIDER_SET.has(providerID)
export function isPopularProvider(provider: Provider | string) {
if (typeof provider !== "string") return provider.metadata?.priority !== undefined
return fallback.has(provider)
}
export function popularProviderIndex(providerID: string) {
return providerOrderIndex(providerID, POPULAR_PROVIDER_IDS)
export function popularProviderIndex(provider: Provider | string) {
if (typeof provider !== "string") return provider.metadata?.priority ?? Number.MAX_SAFE_INTEGER
return providerOrderIndex(provider, FALLBACK_PROVIDER_IDS)
}
function validIcon(id: string | undefined): IconName | undefined {
@@ -42,26 +43,13 @@ export function kiloFallbackProvider(): Provider {
export function providerNoteKey(provider: Provider | string) {
if (typeof provider !== "string" && provider.metadata?.noteKey) return provider.metadata.noteKey
const providerID = typeof provider === "string" ? provider : provider.id
if (providerID === "kilo") return "settings.providers.note.kilo"
if (providerID === "opencode") return "settings.providers.note.opencode"
if (providerID === "anthropic") return "settings.providers.note.anthropic"
if (providerID === "deepseek") return "settings.providers.note.deepseek"
if (providerID.startsWith("github-copilot")) return "settings.providers.note.copilot"
if (providerID === "openai") return "settings.providers.note.openai"
if (providerID === "google") return "settings.providers.note.google"
if (providerID === "openrouter") return "settings.providers.note.openrouter"
if (providerID === "vercel") return "settings.providers.note.vercel"
if (provider === KILO_PROVIDER_ID) return "settings.providers.note.kilo"
return undefined
}
export function providerNote(provider: Provider) {
return provider.metadata?.note
}
export function sortProviders(items: Provider[]) {
return items.slice().sort((a, b) => {
const rank = popularProviderIndex(a.id) - popularProviderIndex(b.id)
const rank = popularProviderIndex(a) - popularProviderIndex(b)
if (rank !== 0) return rank
return a.name.localeCompare(b.name)
})
-9
View File
@@ -101,17 +101,8 @@ export const dict = {
"dialog.provider.search.placeholder": "البحث عن موفرين",
"dialog.provider.empty": "لم يتم العثور على موفرين",
"dialog.provider.group.popular": "شائع",
"dialog.provider.group.other": "آخر",
"dialog.provider.tag.recommended": "موصى به",
"dialog.provider.opencode.note": "نماذج مختارة تشمل Claude وGPT وGemini والمزيد",
"dialog.provider.anthropic.note": "اتصل باستخدام Claude Pro/Max أو مفتاح API",
"dialog.provider.deepseek.note": "نماذج DeepSeek لمهام الاستدلال والبرمجة",
"dialog.provider.openai.note": "اتصل باستخدام ChatGPT Pro/Plus أو مفتاح API",
"dialog.provider.google.note": "نماذج Gemini للاستجابات السريعة والمنظمة",
"dialog.provider.openrouter.note": "الوصول إلى جميع النماذج المدعومة من موفر واحد",
"dialog.provider.vercel.note": "وصول موحد إلى نماذج الذكاء الاصطناعي مع توجيه ذكي",
"dialog.provider.copilot.note": "اتصل باستخدام Copilot أو مفتاح API",
"dialog.model.select.title": "تحديد نموذج",
"dialog.model.search.placeholder": "البحث عن نماذج",
-9
View File
@@ -101,17 +101,8 @@ export const dict = {
"dialog.provider.search.placeholder": "Buscar provedores",
"dialog.provider.empty": "Nenhum provedor encontrado",
"dialog.provider.group.popular": "Popular",
"dialog.provider.group.other": "Outro",
"dialog.provider.tag.recommended": "Recomendado",
"dialog.provider.opencode.note": "Modelos selecionados incluindo Claude, GPT, Gemini e mais",
"dialog.provider.anthropic.note": "Conectar com Claude Pro/Max ou chave de API",
"dialog.provider.deepseek.note": "Modelos DeepSeek para tarefas de raciocínio e programação",
"dialog.provider.openai.note": "Conectar com ChatGPT Pro/Plus ou chave de API",
"dialog.provider.google.note": "Modelos Gemini para respostas rápidas e estruturadas",
"dialog.provider.openrouter.note": "Acesse todos os modelos suportados a partir de um único provedor",
"dialog.provider.vercel.note": "Acesso unificado a modelos de IA com roteamento inteligente",
"dialog.provider.copilot.note": "Conectar com Copilot ou chave de API",
"dialog.model.select.title": "Selecionar modelo",
"dialog.model.search.placeholder": "Buscar modelos",
-9
View File
@@ -101,17 +101,8 @@ export const dict = {
"dialog.provider.search.placeholder": "Pretraži provajdere",
"dialog.provider.empty": "Nema pronađenih provajdera",
"dialog.provider.group.popular": "Popularno",
"dialog.provider.group.other": "Ostalo",
"dialog.provider.tag.recommended": "Preporučeno",
"dialog.provider.opencode.note": "Kurirani modeli uključujući Claude, GPT, Gemini i druge",
"dialog.provider.anthropic.note": "Direktan pristup Claude modelima, uključujući Pro i Max",
"dialog.provider.deepseek.note": "DeepSeek modeli za zadatke zaključivanja i kodiranja",
"dialog.provider.copilot.note": "Claude modeli za pomoć pri kodiranju",
"dialog.provider.openai.note": "GPT modeli za brze, sposobne opšte AI zadatke",
"dialog.provider.google.note": "Gemini modeli za brze, strukturirane odgovore",
"dialog.provider.openrouter.note": "Pristup svim podržanim modelima preko jednog provajdera",
"dialog.provider.vercel.note": "Jedinstven pristup AI modelima uz pametno rutiranje",
"dialog.model.select.title": "Odaberi model",
"dialog.model.search.placeholder": "Pretraži modele",
-9
View File
@@ -101,17 +101,8 @@ export const dict = {
"dialog.provider.search.placeholder": "Søg udbydere",
"dialog.provider.empty": "Ingen udbydere fundet",
"dialog.provider.group.popular": "Populære",
"dialog.provider.group.other": "Andre",
"dialog.provider.tag.recommended": "Anbefalet",
"dialog.provider.opencode.note": "Udvalgte modeller inkl. Claude, GPT, Gemini og flere",
"dialog.provider.anthropic.note": "Forbind med Claude Pro/Max eller API-nøgle",
"dialog.provider.deepseek.note": "DeepSeek-modeller til ræsonnering og kodningsopgaver",
"dialog.provider.openai.note": "Forbind med ChatGPT Pro/Plus eller API-nøgle",
"dialog.provider.google.note": "Gemini-modeller til hurtige, strukturerede svar",
"dialog.provider.openrouter.note": "Adgang til alle understøttede modeller fra én udbyder",
"dialog.provider.vercel.note": "Samlet adgang til AI-modeller med smart routing",
"dialog.provider.copilot.note": "Forbind med Copilot eller API-nøgle",
"dialog.model.select.title": "Vælg model",
"dialog.model.search.placeholder": "Søg modeller",
-9
View File
@@ -105,17 +105,8 @@ export const dict = {
"dialog.provider.search.placeholder": "Anbieter durchsuchen",
"dialog.provider.empty": "Keine Anbieter gefunden",
"dialog.provider.group.popular": "Beliebt",
"dialog.provider.group.other": "Andere",
"dialog.provider.tag.recommended": "Empfohlen",
"dialog.provider.opencode.note": "Kuratierte Modelle wie Claude, GPT, Gemini und mehr",
"dialog.provider.anthropic.note": "Mit Claude Pro/Max oder API-Schlüssel verbinden",
"dialog.provider.deepseek.note": "DeepSeek-Modelle für Reasoning- und Programmieraufgaben",
"dialog.provider.openai.note": "Mit ChatGPT Pro/Plus oder API-Schlüssel verbinden",
"dialog.provider.google.note": "Gemini-Modelle für schnelle, strukturierte Antworten",
"dialog.provider.openrouter.note": "Zugriff auf alle unterstützten Modelle über einen Anbieter",
"dialog.provider.vercel.note": "Einheitlicher Zugriff auf KI-Modelle mit intelligentem Routing",
"dialog.provider.copilot.note": "Mit Copilot oder API-Schlüssel verbinden",
"dialog.model.select.title": "Modell auswählen",
"dialog.model.search.placeholder": "Modelle durchsuchen",
@@ -101,17 +101,8 @@ export const dict = {
"dialog.provider.search.placeholder": "Search providers",
"dialog.provider.empty": "No providers found",
"dialog.provider.group.popular": "Popular",
"dialog.provider.group.other": "Other",
"dialog.provider.tag.recommended": "Recommended",
"dialog.provider.opencode.note": "Curated models including Claude, GPT, Gemini and more",
"dialog.provider.anthropic.note": "Direct access to Claude models, including Pro and Max",
"dialog.provider.deepseek.note": "DeepSeek models for reasoning and coding tasks",
"dialog.provider.copilot.note": "Claude models for coding assistance",
"dialog.provider.openai.note": "GPT and Codex models with API key or ChatGPT login",
"dialog.provider.google.note": "Gemini models for fast, structured responses",
"dialog.provider.openrouter.note": "Access all supported models from one provider",
"dialog.provider.vercel.note": "Unified access to AI models with smart routing",
"dialog.model.select.title": "Select model",
"dialog.model.search.placeholder": "Search models",
-9
View File
@@ -101,17 +101,8 @@ export const dict = {
"dialog.provider.search.placeholder": "Buscar proveedores",
"dialog.provider.empty": "No se encontraron proveedores",
"dialog.provider.group.popular": "Popular",
"dialog.provider.group.other": "Otro",
"dialog.provider.tag.recommended": "Recomendado",
"dialog.provider.opencode.note": "Modelos curados incluyendo Claude, GPT, Gemini y más",
"dialog.provider.anthropic.note": "Conectar con Claude Pro/Max o clave API",
"dialog.provider.deepseek.note": "Modelos DeepSeek para tareas de razonamiento y programación",
"dialog.provider.openai.note": "Conectar con ChatGPT Pro/Plus o clave API",
"dialog.provider.google.note": "Modelos Gemini para respuestas rápidas y estructuradas",
"dialog.provider.openrouter.note": "Accede a todos los modelos soportados desde un solo proveedor",
"dialog.provider.vercel.note": "Acceso unificado a modelos de IA con enrutamiento inteligente",
"dialog.provider.copilot.note": "Conectar con Copilot o clave API",
"dialog.model.select.title": "Seleccionar modelo",
"dialog.model.search.placeholder": "Buscar modelos",
-9
View File
@@ -102,17 +102,8 @@ export const dict = {
"dialog.provider.search.placeholder": "Rechercher des fournisseurs",
"dialog.provider.empty": "Aucun fournisseur trouvé",
"dialog.provider.group.popular": "Populaire",
"dialog.provider.group.other": "Autre",
"dialog.provider.tag.recommended": "Recommandé",
"dialog.provider.opencode.note": "Modèles sélectionnés incluant Claude, GPT, Gemini et plus",
"dialog.provider.anthropic.note": "Connectez-vous avec Claude Pro/Max ou une clé API",
"dialog.provider.deepseek.note": "Modèles DeepSeek pour les tâches de raisonnement et de codage",
"dialog.provider.openai.note": "Connectez-vous avec ChatGPT Pro/Plus ou une clé API",
"dialog.provider.google.note": "Modèles Gemini pour des réponses rapides et structurées",
"dialog.provider.openrouter.note": "Accédez à tous les modèles supportés depuis un seul fournisseur",
"dialog.provider.vercel.note": "Accès unifié aux modèles IA avec routage intelligent",
"dialog.provider.copilot.note": "Connectez-vous avec Copilot ou une clé API",
"dialog.model.select.title": "Sélectionner un modèle",
"dialog.model.search.placeholder": "Rechercher des modèles",
-9
View File
@@ -92,17 +92,8 @@ export const dict = {
"palette.group.files": "File",
"dialog.provider.search.placeholder": "Cerca provider",
"dialog.provider.empty": "Nessun provider trovato",
"dialog.provider.group.popular": "Popolari",
"dialog.provider.group.other": "Altro",
"dialog.provider.tag.recommended": "Consigliato",
"dialog.provider.opencode.note": "Modelli curati, inclusi Claude, GPT, Gemini e altri",
"dialog.provider.anthropic.note": "Accesso diretto ai modelli Claude, inclusi Pro e Max",
"dialog.provider.copilot.note": "Modelli AI per assistenza al coding tramite GitHub Copilot",
"dialog.provider.openai.note": "Modelli GPT per attività AI generali rapide e capaci",
"dialog.provider.google.note": "Modelli Gemini per risposte rapide e strutturate",
"dialog.provider.deepseek.note": "Modelli DeepSeek per ragionamento e attività di coding",
"dialog.provider.openrouter.note": "Accesso a tutti i modelli supportati da un solo provider",
"dialog.provider.vercel.note": "Accesso unificato ai modelli AI con routing intelligente",
"dialog.model.select.title": "Seleziona modello",
"dialog.model.search.placeholder": "Cerca modelli",
"dialog.model.empty": "Nessun risultato modello",
-9
View File
@@ -101,17 +101,8 @@ export const dict = {
"dialog.provider.search.placeholder": "プロバイダーを検索",
"dialog.provider.empty": "プロバイダーが見つかりません",
"dialog.provider.group.popular": "人気",
"dialog.provider.group.other": "その他",
"dialog.provider.tag.recommended": "推奨",
"dialog.provider.opencode.note": "Claude、GPT、Geminiなどの厳選されたモデル",
"dialog.provider.anthropic.note": "Claude Pro/MaxまたはAPIキーで接続",
"dialog.provider.deepseek.note": "推論とコーディングタスク向けのDeepSeekモデル",
"dialog.provider.openai.note": "ChatGPT Pro/PlusまたはAPIキーで接続",
"dialog.provider.google.note": "高速で構造化された応答のためのGeminiモデル",
"dialog.provider.openrouter.note": "1つのプロバイダーからすべてのモデルにアクセス",
"dialog.provider.vercel.note": "スマートルーティングによるAIモデルへの統合アクセス",
"dialog.provider.copilot.note": "CopilotまたはAPIキーで接続",
"dialog.model.select.title": "モデルを選択",
"dialog.model.search.placeholder": "モデルを検索",
-9
View File
@@ -105,17 +105,8 @@ export const dict = {
"dialog.provider.search.placeholder": "공급자 검색",
"dialog.provider.empty": "공급자 없음",
"dialog.provider.group.popular": "인기",
"dialog.provider.group.other": "기타",
"dialog.provider.tag.recommended": "추천",
"dialog.provider.opencode.note": "Claude, GPT, Gemini 등 엄선된 모델",
"dialog.provider.anthropic.note": "Claude Pro/Max 또는 API 키로 연결",
"dialog.provider.deepseek.note": "추론 및 코딩 작업을 위한 DeepSeek 모델",
"dialog.provider.openai.note": "ChatGPT Pro/Plus 또는 API 키로 연결",
"dialog.provider.google.note": "빠르고 구조화된 응답을 위한 Gemini 모델",
"dialog.provider.openrouter.note": "하나의 공급자에서 모든 지원 모델에 액세스",
"dialog.provider.vercel.note": "스마트 라우팅으로 AI 모델에 통합 액세스",
"dialog.provider.copilot.note": "Copilot 또는 API 키로 연결",
"dialog.model.select.title": "모델 선택",
"dialog.model.search.placeholder": "모델 검색",
-9
View File
@@ -101,17 +101,8 @@ export const dict = {
"dialog.provider.search.placeholder": "Providers zoeken",
"dialog.provider.empty": "Geen providers gevonden",
"dialog.provider.group.popular": "Populair",
"dialog.provider.group.other": "Overige",
"dialog.provider.tag.recommended": "Aanbevolen",
"dialog.provider.opencode.note": "Geselecteerde modellen waaronder Claude, GPT, Gemini en meer",
"dialog.provider.anthropic.note": "Directe toegang tot Claude-modellen, inclusief Pro en Max",
"dialog.provider.deepseek.note": "DeepSeek-modellen voor redeneer- en programmeertaken",
"dialog.provider.copilot.note": "Claude-modellen voor programmeerhulp",
"dialog.provider.openai.note": "GPT-modellen voor snelle, capabele algemene AI-taken",
"dialog.provider.google.note": "Gemini-modellen voor snelle, gestructureerde antwoorden",
"dialog.provider.openrouter.note": "Toegang tot alle ondersteunde modellen via één provider",
"dialog.provider.vercel.note": "Gecentraliseerde toegang tot AI-modellen met slimme routering",
"dialog.model.select.title": "Model selecteren",
"dialog.model.search.placeholder": "Modellen zoeken",
-9
View File
@@ -104,17 +104,8 @@ export const dict = {
"dialog.provider.search.placeholder": "Søk etter leverandører",
"dialog.provider.empty": "Ingen leverandører funnet",
"dialog.provider.group.popular": "Populære",
"dialog.provider.group.other": "Andre",
"dialog.provider.tag.recommended": "Anbefalt",
"dialog.provider.opencode.note": "Utvalgte modeller inkludert Claude, GPT, Gemini og flere",
"dialog.provider.anthropic.note": "Koble til med Claude Pro/Max eller API-nøkkel",
"dialog.provider.deepseek.note": "DeepSeek-modeller for resonnering og kodeoppgaver",
"dialog.provider.openai.note": "Koble til med ChatGPT Pro/Plus eller API-nøkkel",
"dialog.provider.google.note": "Gemini-modeller for raske, strukturerte svar",
"dialog.provider.openrouter.note": "Tilgang til alle støttede modeller fra én leverandør",
"dialog.provider.vercel.note": "Samlet tilgang til AI-modeller med smart ruting",
"dialog.provider.copilot.note": "Koble til med Copilot eller API-nøkkel",
"dialog.model.select.title": "Velg modell",
"dialog.model.search.placeholder": "Søk etter modeller",
-9
View File
@@ -101,17 +101,8 @@ export const dict = {
"dialog.provider.search.placeholder": "Szukaj dostawców",
"dialog.provider.empty": "Nie znaleziono dostawców",
"dialog.provider.group.popular": "Popularne",
"dialog.provider.group.other": "Inne",
"dialog.provider.tag.recommended": "Zalecane",
"dialog.provider.opencode.note": "Wybrane modele, w tym Claude, GPT, Gemini i więcej",
"dialog.provider.anthropic.note": "Połącz z Claude Pro/Max lub kluczem API",
"dialog.provider.deepseek.note": "Modele DeepSeek do zadań wymagających rozumowania i kodowania",
"dialog.provider.openai.note": "Połącz z ChatGPT Pro/Plus lub kluczem API",
"dialog.provider.google.note": "Modele Gemini do szybkich, strukturalnych odpowiedzi",
"dialog.provider.openrouter.note": "Dostęp do wszystkich obsługiwanych modeli od jednego dostawcy",
"dialog.provider.vercel.note": "Ujednolicony dostęp do modeli AI z inteligentnym routingiem",
"dialog.provider.copilot.note": "Połącz z Copilot lub kluczem API",
"dialog.model.select.title": "Wybierz model",
"dialog.model.search.placeholder": "Szukaj modeli",
-9
View File
@@ -101,17 +101,8 @@ export const dict = {
"dialog.provider.search.placeholder": "Поиск провайдеров",
"dialog.provider.empty": "Провайдеры не найдены",
"dialog.provider.group.popular": "Популярные",
"dialog.provider.group.other": "Другие",
"dialog.provider.tag.recommended": "Рекомендуемые",
"dialog.provider.opencode.note": "Отобранные модели, включая Claude, GPT, Gemini и другие",
"dialog.provider.anthropic.note": "Подключитесь с помощью Claude Pro/Max или API ключа",
"dialog.provider.deepseek.note": "Модели DeepSeek для задач рассуждения и программирования",
"dialog.provider.openai.note": "Подключитесь с помощью ChatGPT Pro/Plus или API ключа",
"dialog.provider.google.note": "Модели Gemini для быстрых структурированных ответов",
"dialog.provider.openrouter.note": "Доступ ко всем поддерживаемым моделям через одного провайдера",
"dialog.provider.vercel.note": "Единый доступ к AI-моделям с интеллектуальной маршрутизацией",
"dialog.provider.copilot.note": "Подключитесь с помощью Copilot или API ключа",
"dialog.model.select.title": "Выбрать модель",
"dialog.model.search.placeholder": "Поиск моделей",
-9
View File
@@ -101,17 +101,8 @@ export const dict = {
"dialog.provider.search.placeholder": "ค้นหาผู้ให้บริการ",
"dialog.provider.empty": "ไม่พบผู้ให้บริการ",
"dialog.provider.group.popular": "ยอดนิยม",
"dialog.provider.group.other": "อื่น ๆ",
"dialog.provider.tag.recommended": "แนะนำ",
"dialog.provider.opencode.note": "โมเดลที่คัดสรร รวมถึง Claude, GPT, Gemini และอื่น ๆ",
"dialog.provider.anthropic.note": "เข้าถึงโมเดล Claude โดยตรง รวมถึง Pro และ Max",
"dialog.provider.deepseek.note": "โมเดล DeepSeek สำหรับงานการให้เหตุผลและการเขียนโค้ด",
"dialog.provider.copilot.note": "โมเดล Claude สำหรับการช่วยเหลือในการเขียนโค้ด",
"dialog.provider.openai.note": "โมเดล GPT สำหรับงาน AI ทั่วไปที่รวดเร็วและมีความสามารถ",
"dialog.provider.google.note": "โมเดล Gemini สำหรับการตอบสนองที่รวดเร็วและมีโครงสร้าง",
"dialog.provider.openrouter.note": "เข้าถึงโมเดลที่รองรับทั้งหมดจากผู้ให้บริการเดียว",
"dialog.provider.vercel.note": "การเข้าถึงโมเดล AI แบบรวมด้วยการกำหนดเส้นทางอัจฉริยะ",
"dialog.model.select.title": "เลือกโมเดล",
"dialog.model.search.placeholder": "ค้นหาโมเดล",
-9
View File
@@ -101,17 +101,8 @@ export const dict = {
"dialog.provider.search.placeholder": "Sağlayıcı ara",
"dialog.provider.empty": "Sağlayıcı bulunamadı",
"dialog.provider.group.popular": "Popüler",
"dialog.provider.group.other": "Diğer",
"dialog.provider.tag.recommended": "Önerilen",
"dialog.provider.opencode.note": "Claude, GPT, Gemini ve daha fazlasını içeren seçilmiş modeller",
"dialog.provider.anthropic.note": "Pro ve Max dahil Claude modellerine doğrudan erişim",
"dialog.provider.deepseek.note": "Muhakeme ve kodlama görevleri için DeepSeek modelleri",
"dialog.provider.copilot.note": "Kodlama yardımı için Claude modelleri",
"dialog.provider.openai.note": "Hızlı ve yetenekli genel yapay zeka görevleri için GPT modelleri",
"dialog.provider.google.note": "Hızlı ve yapılandırılmış yanıtlar için Gemini modelleri",
"dialog.provider.openrouter.note": "Tek bir sağlayıcıdan tüm desteklenen modellere eriş",
"dialog.provider.vercel.note": "Akıllı yönlendirme ile yapay zeka modellerine birleşik erişim",
"dialog.model.select.title": "Model seç",
"dialog.model.search.placeholder": "Model ara",
-9
View File
@@ -101,17 +101,8 @@ export const dict = {
"dialog.provider.search.placeholder": "Пошук провайдера",
"dialog.provider.empty": "Провайдерів не знайдено",
"dialog.provider.group.popular": "Популярні",
"dialog.provider.group.other": "Інші",
"dialog.provider.tag.recommended": "Рекомендовано",
"dialog.provider.opencode.note": "Добірка моделей включаючи Claude, GPT, Gemini та інші",
"dialog.provider.anthropic.note": "Прямий доступ до моделей Claude включаючи Pro та Max",
"dialog.provider.deepseek.note": "Моделі DeepSeek для завдань міркування та програмування",
"dialog.provider.copilot.note": "Моделі Claude для допомоги з кодуванням",
"dialog.provider.openai.note": "Моделі GPT для швидких і потужних загальних завдань ШІ",
"dialog.provider.google.note": "Моделі Gemini для швидких і структурованих відповідей",
"dialog.provider.openrouter.note": "Доступ до всіх підтримуваних моделей від одного провайдера",
"dialog.provider.vercel.note": "Уніфікований доступ до моделей ШІ з інтелектуальною маршрутизацією",
"dialog.model.select.title": "Вибір моделі",
"dialog.model.search.placeholder": "Пошук моделі",
-9
View File
@@ -105,17 +105,8 @@ export const dict = {
"dialog.provider.search.placeholder": "搜索提供商",
"dialog.provider.empty": "未找到提供商",
"dialog.provider.group.popular": "热门",
"dialog.provider.group.other": "其他",
"dialog.provider.tag.recommended": "推荐",
"dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接",
"dialog.provider.deepseek.note": "用于推理和编程任务的 DeepSeek 模型",
"dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接",
"dialog.provider.copilot.note": "使用 Copilot 或 API 密钥连接",
"dialog.provider.opencode.note": "使用 OpenCode Zen 或 API 密钥连接",
"dialog.provider.google.note": "使用 Google 账号或 API 密钥连接",
"dialog.provider.openrouter.note": "使用 OpenRouter 账号或 API 密钥连接",
"dialog.provider.vercel.note": "使用 Vercel 账号或 API 密钥连接",
"dialog.model.select.title": "选择模型",
"dialog.model.search.placeholder": "搜索模型",
-9
View File
@@ -105,17 +105,8 @@ export const dict = {
"dialog.provider.search.placeholder": "搜尋供應商",
"dialog.provider.empty": "找不到供應商",
"dialog.provider.group.popular": "熱門",
"dialog.provider.group.other": "其他",
"dialog.provider.tag.recommended": "推薦",
"dialog.provider.opencode.note": "精選模型,包含 Claude、GPT、Gemini 等",
"dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 金鑰連線",
"dialog.provider.deepseek.note": "用於推理和程式設計任務的 DeepSeek 模型",
"dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 金鑰連線",
"dialog.provider.copilot.note": "使用 Copilot 或 API 金鑰連線",
"dialog.provider.google.note": "Gemini 模型,提供快速且結構化的回應",
"dialog.provider.openrouter.note": "透過單一供應商存取所有支援模型",
"dialog.provider.vercel.note": "透過智慧路由統一存取 AI 模型",
"dialog.model.select.title": "選擇模型",
"dialog.model.search.placeholder": "搜尋模型",
@@ -41,8 +41,8 @@ export interface Provider {
env?: string[]
metadata?: {
noteKey?: string
note?: string
icon?: string
priority?: number
}
}
@@ -2,34 +2,26 @@ import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
export type ProviderMetadata = {
noteKey?: string
note?: string
icon?: string
priority?: number
}
const notes: Record<string, { key: string; note: string }> = {
kilo: { key: "settings.providers.note.kilo", note: "Access 500+ AI models" },
opencode: {
key: "settings.providers.note.opencode",
note: "Curated models including Claude, GPT, Gemini and more",
},
anthropic: {
key: "settings.providers.note.anthropic",
note: "Direct access to Claude models, including Pro and Max",
},
deepseek: {
key: "settings.providers.note.deepseek",
note: "DeepSeek models for reasoning and coding tasks",
},
"github-copilot": { key: "settings.providers.note.copilot", note: "Claude models for coding assistance" },
openai: {
key: "settings.providers.note.openai",
note: "GPT and Codex models with API key or ChatGPT login",
},
google: { key: "settings.providers.note.google", note: "Gemini models for fast, structured responses" },
openrouter: { key: "settings.providers.note.openrouter", note: "Access all supported models from one provider" },
vercel: { key: "settings.providers.note.vercel", note: "Unified access to AI models with smart routing" },
const notes: Record<string, string> = {
kilo: "settings.providers.note.kilo",
opencode: "settings.providers.note.opencode",
anthropic: "settings.providers.note.anthropic",
deepseek: "settings.providers.note.deepseek",
"github-copilot": "settings.providers.note.copilot",
openai: "settings.providers.note.openai",
google: "settings.providers.note.google",
openrouter: "settings.providers.note.openrouter",
vercel: "settings.providers.note.vercel",
}
const order = ["kilo", "anthropic", "deepseek", "openai", "google", "openrouter", "vercel"] as const
const priority = new Map<string, number>(order.map((id, index) => [id, index]))
const icons = new Set<string>(iconNames)
function key(id: string) {
@@ -41,8 +33,8 @@ export function providerMetadata(id: string): ProviderMetadata {
const name = key(id)
const note = notes[name]
return {
noteKey: note?.key,
note: note?.note,
noteKey: note,
icon: icons.has(name as IconName) ? name : "synthetic",
priority: priority.get(name),
}
}
+1 -1
View File
@@ -938,8 +938,8 @@ const ProviderLimit = Schema.Struct({
// kilocode_change start
const ProviderMetadata = Schema.Struct({
noteKey: optionalOmitUndefined(Schema.String),
note: optionalOmitUndefined(Schema.String),
icon: optionalOmitUndefined(Schema.String),
priority: optionalOmitUndefined(Schema.Int),
})
// kilocode_change end
@@ -2,18 +2,17 @@ import { describe, expect, test } from "bun:test"
import { providerMetadata } from "../../src/kilocode/provider/metadata"
describe("providerMetadata", () => {
test("returns shared provider note and icon metadata", () => {
test("returns shared provider key, icon, and priority metadata", () => {
expect(providerMetadata("openai")).toEqual({
noteKey: "settings.providers.note.openai",
note: "GPT and Codex models with API key or ChatGPT login",
icon: "openai",
priority: 3,
})
})
test("maps github copilot aliases to stable metadata", () => {
expect(providerMetadata("github-copilot-custom")).toEqual({
noteKey: "settings.providers.note.copilot",
note: "Claude models for coding assistance",
icon: "github-copilot",
})
})
@@ -21,8 +20,8 @@ describe("providerMetadata", () => {
test("uses the Kilo icon for Kilo Gateway", () => {
expect(providerMetadata("kilo")).toEqual({
noteKey: "settings.providers.note.kilo",
note: "Access 500+ AI models",
icon: "kilo",
priority: 0,
})
})
+1 -1
View File
@@ -1615,8 +1615,8 @@ export type Provider = {
key?: string
metadata?: {
noteKey?: string
note?: string
icon?: string
priority?: number
}
options: {
[key: string]: unknown
+6 -6
View File
@@ -16779,9 +16779,6 @@
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"source": {
"type": "object",
"properties": {
@@ -19242,6 +19239,9 @@
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"source": {
"type": "string",
"enum": ["env", "config", "custom", "api"]
@@ -19261,11 +19261,11 @@
"noteKey": {
"type": "string"
},
"note": {
"type": "string"
},
"icon": {
"type": "string"
},
"priority": {
"type": "integer"
}
},
"additionalProperties": false