Add ModelService with extra body support

This commit is contained in:
2026-04-26 11:08:45 +08:00
parent 2d3045e110
commit e5d5445a1f
4 changed files with 131 additions and 20 deletions

View File

@@ -25,7 +25,7 @@ repositories {
mavenCentral() mavenCentral()
} }
val openaiClientVersion = "4.0.1" val openaiClientVersion = "4.1.0"
val ktorVersion = "3.0.3" val ktorVersion = "3.0.3"
val jLatexMathVersion = "1.0.7" val jLatexMathVersion = "1.0.7"
val commonTextVersion = "1.13.0" val commonTextVersion = "1.13.0"

View File

@@ -1,14 +1,12 @@
package top.jie65535.mirai package top.jie65535.mirai
import com.aallam.openai.api.http.Timeout import kotlinx.serialization.json.Json
import com.aallam.openai.client.Chat import kotlinx.serialization.json.JsonObject
import com.aallam.openai.client.OpenAI import kotlinx.serialization.json.jsonObject
import com.aallam.openai.client.OpenAIHost
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
object LargeLanguageModels { object LargeLanguageModels {
/** /**
* 系统提示词 * 系统提示词
*/ */
@@ -18,46 +16,62 @@ object LargeLanguageModels {
/** /**
* 聊天助手 * 聊天助手
*/ */
var chat: Chat? = null var chat: ModelService? = null
/** /**
* 推理模型 * 推理模型
*/ */
var reasoning: Chat? = null var reasoning: ModelService? = null
/** /**
* 视觉模型 * 视觉模型
*/ */
var visual: Chat? = null var visual: ModelService? = null
private val json = Json {
isLenient = true
ignoreUnknownKeys = true
}
private fun parseExtraBody(raw: String): JsonObject? {
if (raw.isBlank()) return null
return try {
json.parseToJsonElement(raw).jsonObject
} catch (_: Exception) {
null
}
}
fun reload() { fun reload() {
// 载入超时时间
val timeout = PluginConfig.timeout.milliseconds val timeout = PluginConfig.timeout.milliseconds
// 初始化聊天模型 // 初始化聊天模型
if (PluginConfig.openAiApi.isNotBlank() && PluginConfig.openAiToken.isNotBlank()) { if (PluginConfig.openAiApi.isNotBlank() && PluginConfig.openAiToken.isNotBlank()) {
chat = OpenAI( chat = ModelService(
baseUrl = PluginConfig.openAiApi,
token = PluginConfig.openAiToken, token = PluginConfig.openAiToken,
host = OpenAIHost(baseUrl = PluginConfig.openAiApi), timeout = timeout,
timeout = Timeout(request = timeout, connect = timeout, socket = timeout) extraBody = parseExtraBody(PluginConfig.chatModelExtraBody)
) )
} }
// 初始化推理模型 // 初始化推理模型
if (PluginConfig.reasoningModelApi.isNotBlank() && PluginConfig.reasoningModelToken.isNotBlank()) { if (PluginConfig.reasoningModelApi.isNotBlank() && PluginConfig.reasoningModelToken.isNotBlank()) {
reasoning = OpenAI( reasoning = ModelService(
baseUrl = PluginConfig.reasoningModelApi,
token = PluginConfig.reasoningModelToken, token = PluginConfig.reasoningModelToken,
host = OpenAIHost(baseUrl = PluginConfig.reasoningModelApi), timeout = timeout,
timeout = Timeout(request = timeout, connect = timeout, socket = timeout) extraBody = parseExtraBody(PluginConfig.reasoningModelExtraBody)
) )
} }
// 初始化视觉模型 // 初始化视觉模型
if (PluginConfig.visualModelApi.isNotBlank() && PluginConfig.visualModelToken.isNotBlank()) { if (PluginConfig.visualModelApi.isNotBlank() && PluginConfig.visualModelToken.isNotBlank()) {
visual = OpenAI( visual = ModelService(
baseUrl = PluginConfig.visualModelApi,
token = PluginConfig.visualModelToken, token = PluginConfig.visualModelToken,
host = OpenAIHost(baseUrl = PluginConfig.visualModelApi), timeout = timeout,
timeout = Timeout(request = timeout, connect = timeout, socket = timeout) extraBody = parseExtraBody(PluginConfig.visualModelExtraBody)
) )
} }
@@ -78,4 +92,4 @@ object LargeLanguageModels {
} }
} }
} }
} }

View File

@@ -0,0 +1,88 @@
package top.jie65535.mirai
import com.aallam.openai.api.chat.ChatCompletionChunk
import com.aallam.openai.api.chat.ChatCompletionRequest
import io.ktor.client.*
import io.ktor.client.call.body
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.utils.io.*
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import kotlinx.serialization.json.*
import kotlin.time.Duration
class ModelService(
val baseUrl: String,
val token: String,
val timeout: Duration,
val extraBody: JsonObject? = null
) {
val httpClient: HttpClient by lazy {
HttpClient(OkHttp) {
install(HttpTimeout) {
val millis = timeout.inWholeMilliseconds
requestTimeoutMillis = millis
connectTimeoutMillis = millis
socketTimeoutMillis = millis
}
defaultRequest {
url(baseUrl)
bearerAuth(token)
}
expectSuccess = true
}
}
private val json = Json {
isLenient = true
ignoreUnknownKeys = true
explicitNulls = false
}
fun chatCompletions(request: ChatCompletionRequest): Flow<ChatCompletionChunk> {
val requestJson = json.encodeToJsonElement(ChatCompletionRequest.serializer(), request)
.jsonObject.toMutableMap()
requestJson["stream"] = JsonPrimitive(true)
extraBody?.forEach { (key, value) ->
requestJson[key] = value
}
val body = JsonObject(requestJson).toString()
return flow {
httpClient.post("chat/completions") {
setBody(body)
contentType(ContentType.Application.Json)
accept(ContentType.Text.EventStream)
headers {
append(HttpHeaders.CacheControl, "no-cache")
append(HttpHeaders.Connection, "keep-alive")
}
}.let { response ->
val channel: ByteReadChannel = response.body()
try {
while (currentCoroutineContext().isActive && !channel.isClosedForRead) {
val line = channel.readUTF8Line() ?: continue
when {
line.startsWith("data: [DONE]") -> break
line.startsWith("data: ") -> {
val chunk = json.decodeFromString<ChatCompletionChunk>(
line.removePrefix("data: ")
)
emit(chunk)
}
else -> continue
}
}
} finally {
channel.cancel()
}
}
}
}
}

View File

@@ -38,6 +38,15 @@ object PluginConfig : AutoSavePluginConfig("Config") {
@ValueDescription("视觉模型") @ValueDescription("视觉模型")
var visualModel: String by value("qwen-vl-plus") var visualModel: String by value("qwen-vl-plus")
@ValueDescription("聊天模型额外请求体JSON会合并到请求体中。例如DeepSeek关闭思维: {\"thinking\": {\"type\": \"disabled\"}}")
val chatModelExtraBody: String by value("")
@ValueDescription("推理模型额外请求体JSON会合并到请求体中。例如DeepSeek启用思维: {\"thinking\": {\"type\": \"enabled\"}}")
val reasoningModelExtraBody: String by value("")
@ValueDescription("视觉模型额外请求体JSON会合并到请求体中。")
val visualModelExtraBody: String by value("")
@ValueDescription("百炼平台API KEY") @ValueDescription("百炼平台API KEY")
val dashScopeApiKey: String by value("") val dashScopeApiKey: String by value("")