Unify model configuration and usage accounting

This commit is contained in:
2026-08-07 17:18:04 +08:00
parent c0dd34741a
commit f508373d02
23 changed files with 3433 additions and 227 deletions
+187
View File
@@ -0,0 +1,187 @@
package top.jie65535.mirai.llm
import top.jie65535.mirai.config.ModelConfig
import top.jie65535.mirai.config.ModelDefinition
import top.jie65535.mirai.config.ModelProviderDefinition
import top.jie65535.mirai.config.PluginConfig
data class ResolvedModelDefinition(
val alias: String,
val provider: String,
val providerType: String,
val api: String,
val token: String,
val model: String,
val extraBody: String,
)
/** Resolves shared model aliases and keeps legacy Config.yml fallback logic in one place. */
object ModelCatalog {
private const val DEFAULT_DASHSCOPE_IMAGE_API =
"https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
private val openAiProviderTypes = setOf(
"openai",
"openai-compatible",
"openai_compatible",
)
private val supportedProviderTypes = openAiProviderTypes + setOf(
"dashscope",
)
fun resolve(alias: String): ResolvedModelDefinition? =
resolve(alias, ModelConfig.providers, ModelConfig.models)
internal fun resolve(
alias: String,
providers: List<ModelProviderDefinition>,
models: List<ModelDefinition>,
): ResolvedModelDefinition? {
val normalizedAlias = alias.trim()
if (normalizedAlias.isEmpty()) return null
val model = models.filter { it.name.trim() == normalizedAlias }.singleOrNull() ?: return null
val provider = providers.filter { it.name.trim() == model.provider.trim() }.singleOrNull()
?: return null
return model.resolve(provider, normalizedAlias)
}
fun validationIssues(): List<String> = validationIssues(ModelConfig.providers, ModelConfig.models)
fun roleValidationIssues(): List<String> {
val bindings = buildList {
add(Triple("主聊天", PluginConfig.chatModelAlias, openAiProviderTypes))
PluginConfig.chatFallbackModelAliases.forEachIndexed { index, alias ->
add(Triple("聊天备用 ${index + 1}", alias, openAiProviderTypes))
}
add(
Triple(
"画像",
PluginConfig.profileModelAlias.ifBlank { PluginConfig.chatModelAlias },
openAiProviderTypes,
)
)
add(Triple("推理", PluginConfig.reasoningModelAlias, openAiProviderTypes))
add(Triple("视觉", PluginConfig.visualModelAlias, openAiProviderTypes))
add(Triple("网页摘要", PluginConfig.webSummaryModelAlias, openAiProviderTypes))
add(Triple("图像", PluginConfig.imageModelAlias, setOf("dashscope")))
add(Triple("TTS", PluginConfig.ttsModelAlias, setOf("dashscope")))
}
return bindings.mapNotNull { (role, alias, allowedTypes) ->
bindingValidationIssue(
role = role,
alias = alias,
allowedTypes = allowedTypes,
providers = ModelConfig.providers,
models = ModelConfig.models,
)
}.distinct()
}
internal fun bindingValidationIssue(
role: String,
alias: String,
allowedTypes: Set<String>,
providers: List<ModelProviderDefinition>,
models: List<ModelDefinition>,
): String? {
val normalizedAlias = alias.trim()
if (normalizedAlias.isEmpty()) return null
val definition = resolve(normalizedAlias, providers, models)
?: return "Models.yml 的 $role 角色引用了无效模型别名:$normalizedAlias"
return if (definition.providerType !in allowedTypes) {
"Models.yml 的 $role 角色不能使用 provider type ${definition.providerType}$normalizedAlias"
} else {
null
}
}
internal fun validationIssues(
providers: List<ModelProviderDefinition>,
models: List<ModelDefinition>,
): List<String> = buildList {
val providerNames = providers.map { it.name.trim() }
providerNames.filter(String::isEmpty).forEach { add("Models.yml 存在空 provider 名称") }
providerNames.groupingBy(String::toString).eachCount()
.filterValues { it > 1 }
.keys
.forEach { add("Models.yml provider 名称重复:$it") }
val modelNames = models.map { it.name.trim() }
modelNames.filter(String::isEmpty).forEach { add("Models.yml 存在空模型别名") }
modelNames.groupingBy(String::toString).eachCount()
.filterValues { it > 1 }
.keys
.forEach { add("Models.yml 模型别名重复:$it") }
providers.forEach { provider ->
val name = provider.name.trim().ifBlank { "<empty>" }
val type = provider.type.trim().lowercase().ifBlank { "openai" }
if (type !in supportedProviderTypes) add("Models.yml provider $name 的 type 不受支持:$type")
if (type != "dashscope" && provider.api.isBlank()) add("Models.yml provider $name 未配置 api")
if (provider.token.isBlank()) add("Models.yml provider $name 未配置 token")
}
models.forEach { model ->
val alias = model.name.trim().ifBlank { "<empty>" }
val providerName = model.provider.trim()
if (providerName.isEmpty() || providerNames.count { it == providerName } != 1) {
add("Models.yml 模型 $alias 引用的 provider 无效:${providerName.ifBlank { "<empty>" }}")
}
if (model.model.isBlank()) add("Models.yml 模型 $alias 未配置实际模型名")
}
}.distinct()
fun resolveImage(): ResolvedModelDefinition? =
resolveDashScope(PluginConfig.imageModelAlias)
?: legacy(
alias = "legacy-image",
provider = "dashscope",
providerType = "dashscope",
api = DEFAULT_DASHSCOPE_IMAGE_API,
token = PluginConfig.dashScopeApiKey,
model = PluginConfig.imageModel,
)
fun resolveTts(): ResolvedModelDefinition? =
resolveDashScope(PluginConfig.ttsModelAlias)
?: legacy(
alias = "legacy-tts",
provider = "dashscope",
providerType = "dashscope",
api = DEFAULT_DASHSCOPE_IMAGE_API,
token = PluginConfig.dashScopeApiKey,
model = PluginConfig.ttsModel,
)
fun normalizeOpenAiApi(api: String): String = api.trim().trimEnd('/') + "/"
private fun resolveDashScope(alias: String): ResolvedModelDefinition? =
resolve(alias)
?.takeIf { it.providerType == "dashscope" && it.token.isNotBlank() && it.model.isNotBlank() }
?.let { it.copy(api = it.api.ifBlank { DEFAULT_DASHSCOPE_IMAGE_API }) }
private fun ModelDefinition.resolve(
provider: ModelProviderDefinition,
alias: String,
): ResolvedModelDefinition = ResolvedModelDefinition(
alias = alias,
provider = provider.name.trim(),
providerType = provider.type.trim().lowercase().ifBlank { "openai" },
api = provider.api.trim(),
token = provider.token.trim(),
model = model.trim(),
extraBody = extraBody,
)
private fun legacy(
alias: String,
provider: String,
providerType: String,
api: String,
token: String,
model: String,
extraBody: String = "",
): ResolvedModelDefinition? {
if (api.isBlank() || token.isBlank() || model.isBlank()) return null
return ResolvedModelDefinition(alias, provider, providerType, api, token, model.trim(), extraBody)
}
}