diff --git a/build.gradle.kts b/build.gradle.kts index 9d82809..0bd50f4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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" diff --git a/src/main/kotlin/LargeLanguageModels.kt b/src/main/kotlin/LargeLanguageModels.kt index 378b3a3..55e6d31 100644 --- a/src/main/kotlin/LargeLanguageModels.kt +++ b/src/main/kotlin/LargeLanguageModels.kt @@ -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 { } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/ModelService.kt b/src/main/kotlin/ModelService.kt new file mode 100644 index 0000000..2508faf --- /dev/null +++ b/src/main/kotlin/ModelService.kt @@ -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 { + 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( + line.removePrefix("data: ") + ) + emit(chunk) + } + else -> continue + } + } + } finally { + channel.cancel() + } + } + } + } +} diff --git a/src/main/kotlin/PluginConfig.kt b/src/main/kotlin/PluginConfig.kt index bbd04f9..8bd0e6a 100644 --- a/src/main/kotlin/PluginConfig.kt +++ b/src/main/kotlin/PluginConfig.kt @@ -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("")