mirror of
https://github.com/jie65535/JChatGPT.git
synced 2026-09-15 02:56:10 +08:00
Add chat endpoint failover for resilience against unstable LLM APIs
Configure a list of fallback chat endpoints (chatFallbacks); each blank field inherits the primary, so you can swap just the API key, just the model, or the whole vendor. On LLM streaming failure the retry loop advances to the next endpoint, and a failed endpoint enters a cooldown (fallbackCooldownMinutes) so a dead primary is skipped instead of wasting a timeout on every message. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
@@ -14,9 +14,28 @@ object LargeLanguageModels {
|
||||
private set
|
||||
|
||||
/**
|
||||
* 聊天助手
|
||||
* 一个聊天接入点:封装了请求服务、模型名与温度。
|
||||
* 主接入点为列表第 0 项,其余为备用接入点,用于容灾切换。
|
||||
*/
|
||||
var chat: ModelService? = null
|
||||
data class ChatEndpoint(
|
||||
val service: ModelService,
|
||||
val model: String,
|
||||
val temperature: Double?,
|
||||
/** 唯一标识,用于健康状态跟踪与日志 */
|
||||
val label: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* 聊天接入点列表:index 0 为主接入点,其余按配置顺序为备用接入点。
|
||||
*/
|
||||
var chatEndpoints: List<ChatEndpoint> = emptyList()
|
||||
private set
|
||||
|
||||
/**
|
||||
* 主聊天接入点服务(向后兼容旧用法)。
|
||||
*/
|
||||
val chat: ModelService?
|
||||
get() = chatEndpoints.firstOrNull()?.service
|
||||
|
||||
/**
|
||||
* 推理模型
|
||||
@@ -28,6 +47,40 @@ object LargeLanguageModels {
|
||||
*/
|
||||
var visual: ModelService? = null
|
||||
|
||||
/**
|
||||
* 接入点健康状态:记录各接入点的冷却截止时间戳(毫秒)。
|
||||
* 失败的接入点进入冷却,期间在 [orderedChatEndpoints] 中被排到队尾,
|
||||
* 避免每条消息都先卡在故障接入点上白白等一次超时。
|
||||
*/
|
||||
private val cooldownUntil = HashMap<String, Long>()
|
||||
|
||||
/** 上报某接入点调用失败,使其进入冷却。 */
|
||||
fun reportFailure(endpoint: ChatEndpoint) {
|
||||
val minutes = PluginConfig.fallbackCooldownMinutes
|
||||
// 只有存在备用接入点时冷却才有意义;否则没有可切换的目标,标记冷却反而无益
|
||||
if (minutes > 0 && chatEndpoints.size > 1) {
|
||||
cooldownUntil[endpoint.label] = System.currentTimeMillis() + minutes * 60_000L
|
||||
}
|
||||
}
|
||||
|
||||
/** 上报某接入点调用成功,清除其冷却。 */
|
||||
fun reportSuccess(endpoint: ChatEndpoint) {
|
||||
cooldownUntil.remove(endpoint.label)
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回按健康度排序的接入点:未冷却的保持配置原顺序在前,冷却中的排到后面
|
||||
* (冷却中再按剩余冷却时间升序,优先重试快恢复的)。排序稳定,主接入点健康时始终最先。
|
||||
*/
|
||||
fun orderedChatEndpoints(): List<ChatEndpoint> {
|
||||
if (chatEndpoints.size <= 1) return chatEndpoints
|
||||
val now = System.currentTimeMillis()
|
||||
return chatEndpoints.sortedBy { ep ->
|
||||
val until = cooldownUntil[ep.label] ?: 0L
|
||||
if (until > now) until else 0L
|
||||
}
|
||||
}
|
||||
|
||||
private val json = Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
@@ -46,16 +99,50 @@ object LargeLanguageModels {
|
||||
val timeout = PluginConfig.timeout.milliseconds
|
||||
val firstChunkTimeout = PluginConfig.firstChunkTimeout.milliseconds
|
||||
|
||||
// 初始化聊天模型
|
||||
// 初始化聊天接入点(主 + 备用),并重置健康状态
|
||||
cooldownUntil.clear()
|
||||
val endpoints = mutableListOf<ChatEndpoint>()
|
||||
if (PluginConfig.openAiApi.isNotBlank() && PluginConfig.openAiToken.isNotBlank()) {
|
||||
chat = ModelService(
|
||||
baseUrl = PluginConfig.openAiApi,
|
||||
token = PluginConfig.openAiToken,
|
||||
timeout = timeout,
|
||||
firstChunkTimeout = firstChunkTimeout,
|
||||
extraBody = parseExtraBody(PluginConfig.chatModelExtraBody)
|
||||
endpoints.add(
|
||||
ChatEndpoint(
|
||||
service = ModelService(
|
||||
baseUrl = PluginConfig.openAiApi,
|
||||
token = PluginConfig.openAiToken,
|
||||
timeout = timeout,
|
||||
firstChunkTimeout = firstChunkTimeout,
|
||||
extraBody = parseExtraBody(PluginConfig.chatModelExtraBody)
|
||||
),
|
||||
model = PluginConfig.chatModel,
|
||||
temperature = PluginConfig.chatTemperature,
|
||||
label = "primary",
|
||||
)
|
||||
)
|
||||
|
||||
// 备用接入点:留空字段继承主接入点配置
|
||||
PluginConfig.chatFallbacks.forEachIndexed { i, fb ->
|
||||
val api = fb.api.ifBlank { PluginConfig.openAiApi }
|
||||
val token = fb.token.ifBlank { PluginConfig.openAiToken }
|
||||
val model = fb.model.ifBlank { PluginConfig.chatModel }
|
||||
val extraBody = fb.extraBody.ifBlank { PluginConfig.chatModelExtraBody }
|
||||
if (api.isNotBlank() && token.isNotBlank()) {
|
||||
endpoints.add(
|
||||
ChatEndpoint(
|
||||
service = ModelService(
|
||||
baseUrl = api,
|
||||
token = token,
|
||||
timeout = timeout,
|
||||
firstChunkTimeout = firstChunkTimeout,
|
||||
extraBody = parseExtraBody(extraBody)
|
||||
),
|
||||
model = model,
|
||||
temperature = PluginConfig.chatTemperature,
|
||||
label = "fallback$i:$model",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
chatEndpoints = endpoints
|
||||
|
||||
// 初始化推理模型
|
||||
if (PluginConfig.reasoningModelApi.isNotBlank() && PluginConfig.reasoningModelToken.isNotBlank()) {
|
||||
|
||||
Reference in New Issue
Block a user