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()
}
val openaiClientVersion = "4.0.1"
val openaiClientVersion = "4.1.0"
val ktorVersion = "3.0.3"
val jLatexMathVersion = "1.0.7"
val commonTextVersion = "1.13.0"

View File

@@ -1,14 +1,12 @@
package top.jie65535.mirai
import com.aallam.openai.api.http.Timeout
import com.aallam.openai.client.Chat
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIHost
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import kotlin.time.Duration.Companion.milliseconds
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() {
// 载入超时时间
val timeout = PluginConfig.timeout.milliseconds
// 初始化聊天模型
if (PluginConfig.openAiApi.isNotBlank() && PluginConfig.openAiToken.isNotBlank()) {
chat = OpenAI(
chat = ModelService(
baseUrl = PluginConfig.openAiApi,
token = PluginConfig.openAiToken,
host = OpenAIHost(baseUrl = PluginConfig.openAiApi),
timeout = Timeout(request = timeout, connect = timeout, socket = timeout)
timeout = timeout,
extraBody = parseExtraBody(PluginConfig.chatModelExtraBody)
)
}
// 初始化推理模型
if (PluginConfig.reasoningModelApi.isNotBlank() && PluginConfig.reasoningModelToken.isNotBlank()) {
reasoning = OpenAI(
reasoning = ModelService(
baseUrl = PluginConfig.reasoningModelApi,
token = PluginConfig.reasoningModelToken,
host = OpenAIHost(baseUrl = PluginConfig.reasoningModelApi),
timeout = Timeout(request = timeout, connect = timeout, socket = timeout)
timeout = timeout,
extraBody = parseExtraBody(PluginConfig.reasoningModelExtraBody)
)
}
// 初始化视觉模型
if (PluginConfig.visualModelApi.isNotBlank() && PluginConfig.visualModelToken.isNotBlank()) {
visual = OpenAI(
visual = ModelService(
baseUrl = PluginConfig.visualModelApi,
token = PluginConfig.visualModelToken,
host = OpenAIHost(baseUrl = PluginConfig.visualModelApi),
timeout = Timeout(request = timeout, connect = timeout, socket = timeout)
timeout = 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("视觉模型")
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")
val dashScopeApiKey: String by value("")