Jelajahi Sumber

feat: 智谱AI相关

HapeLee 2 bulan lalu
induk
melakukan
3f8a25f708

+ 35 - 3
app/src/main/java/io/legado/app/data/repository/ai/OpenAiChatHandler.kt

@@ -9,6 +9,7 @@ import io.legado.app.domain.model.AiMessage
 import io.legado.app.domain.model.AiMessageRole
 import io.legado.app.domain.model.AiMessageRole
 import io.legado.app.domain.model.AiProtocol
 import io.legado.app.domain.model.AiProtocol
 import io.legado.app.domain.model.AiProviderConfig
 import io.legado.app.domain.model.AiProviderConfig
+import io.legado.app.domain.model.AiReasoningLevel
 import io.legado.app.domain.model.AiToolDefinition
 import io.legado.app.domain.model.AiToolDefinition
 import io.legado.app.help.http.addHeaders
 import io.legado.app.help.http.addHeaders
 import io.legado.app.help.http.newCallResponse
 import io.legado.app.help.http.newCallResponse
@@ -60,6 +61,7 @@ class OpenAiChatHandler : AiProtocolHandler {
                 body["reasoning_effort"] = it
                 body["reasoning_effort"] = it
             }
             }
         }
         }
+        body.applyZhipuThinking(provider, request.model.modelId, params.reasoningLevel)
 
 
         return retryWithBackoff(maxAttempts = 3, keyRotator = keyRotator) {
         return retryWithBackoff(maxAttempts = 3, keyRotator = keyRotator) {
             val response = aiOkHttpClient.newCallStrResponse {
             val response = aiOkHttpClient.newCallStrResponse {
@@ -76,8 +78,12 @@ class OpenAiChatHandler : AiProtocolHandler {
                 throw Exception("HTTP ${response.code()}: ${response.message()}")
                 throw Exception("HTTP ${response.code()}: ${response.message()}")
             }
             }
             val json = GSON.fromJson(response.body, OpenAiChatResponse::class.java)
             val json = GSON.fromJson(response.body, OpenAiChatResponse::class.java)
-            val text = json?.choices?.firstOrNull()?.message?.content
+            val message = json?.choices?.firstOrNull()?.message
+            val text = message?.content
             if (text.isNullOrBlank()) {
             if (text.isNullOrBlank()) {
+                if (!message?.reasoningContent.isNullOrBlank()) {
+                    throw Exception("AI response contains only reasoning content; disable thinking for this model")
+                }
                 throw Exception("Empty AI response")
                 throw Exception("Empty AI response")
             } else {
             } else {
                 AiGenerateResponse(text = text, rawBody = response.body)
                 AiGenerateResponse(text = text, rawBody = response.body)
@@ -109,6 +115,7 @@ class OpenAiChatHandler : AiProtocolHandler {
                 body["reasoning_effort"] = it
                 body["reasoning_effort"] = it
             }
             }
         }
         }
+        body.applyZhipuThinking(provider, request.model.modelId, params.reasoningLevel)
 
 
         // For streaming, we retry before establishing the SSE connection.
         // For streaming, we retry before establishing the SSE connection.
         // Once streaming starts, errors are not retried (partial output would be confusing).
         // Once streaming starts, errors are not retried (partial output would be confusing).
@@ -190,6 +197,26 @@ class OpenAiChatHandler : AiProtocolHandler {
     }
     }
 }
 }
 
 
+/**
+ * GLM models on Zhipu's Chat Completions API enable thinking by default.
+ * Send the documented object form even when the model was added manually and
+ * therefore has no reasoning capability metadata.
+ */
+internal fun MutableMap<String, Any?>.applyZhipuThinking(
+    provider: AiProviderConfig,
+    modelId: String,
+    reasoningLevel: AiReasoningLevel
+) {
+    val identity = "${provider.id} ${provider.name} ${provider.baseUrl}".lowercase()
+    val isZhipuProvider = "zhipu" in identity || "bigmodel" in identity
+    val isGlmModel = modelId.lowercase().startsWith("glm-")
+    if (isZhipuProvider || isGlmModel) {
+        this["thinking"] = mapOf(
+            "type" to if (reasoningLevel == AiReasoningLevel.OFF) "disabled" else "enabled"
+        )
+    }
+}
+
 // ---- Message & tool format converters ----
 // ---- Message & tool format converters ----
 
 
 internal fun List<AiMessage>.toOpenAiChatMessages(): List<Map<String, Any?>> {
 internal fun List<AiMessage>.toOpenAiChatMessages(): List<Map<String, Any?>> {
@@ -273,8 +300,13 @@ internal data class OpenAiChatChoice(
 
 
 @Keep
 @Keep
 internal data class OpenAiChatMessage(
 internal data class OpenAiChatMessage(
-    val content: String?
-)
+    val content: String?,
+    val reasoning_content: String?,
+    val reasoning: String?
+) {
+    val reasoningContent: String?
+        get() = reasoning_content ?: reasoning
+}
 
 
 @Keep
 @Keep
 internal data class OpenAiModelsResponse(
 internal data class OpenAiModelsResponse(

+ 53 - 0
app/src/test/java/io/legado/app/data/repository/ai/OpenAiChatThinkingTest.kt

@@ -0,0 +1,53 @@
+package io.legado.app.data.repository.ai
+
+import io.legado.app.domain.model.AiProtocol
+import io.legado.app.domain.model.AiProviderConfig
+import io.legado.app.domain.model.AiReasoningLevel
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Test
+
+class OpenAiChatThinkingTest {
+
+    @Test
+    fun zhipuGlmDisablesThinkingWhenReasoningIsOff() {
+        val body = mutableMapOf<String, Any?>()
+
+        body.applyZhipuThinking(zhipuProvider(), "glm-4.7-flash", AiReasoningLevel.OFF)
+
+        assertEquals(mapOf("type" to "disabled"), body["thinking"])
+    }
+
+    @Test
+    fun zhipuGlmEnablesThinkingWhenReasoningIsEnabled() {
+        val body = mutableMapOf<String, Any?>()
+
+        body.applyZhipuThinking(zhipuProvider(), "glm-4.7-flash", AiReasoningLevel.MEDIUM)
+
+        assertEquals(mapOf("type" to "enabled"), body["thinking"])
+    }
+
+    @Test
+    fun unrelatedProviderDoesNotReceiveThinkingParameter() {
+        val body = mutableMapOf<String, Any?>()
+        val provider = AiProviderConfig(
+            id = "openai",
+            name = "OpenAI",
+            protocol = AiProtocol.OPENAI_CHAT_COMPLETIONS,
+            baseUrl = "https://api.openai.com/v1",
+            apiKey = "test"
+        )
+
+        body.applyZhipuThinking(provider, "gpt-5", AiReasoningLevel.OFF)
+
+        assertFalse(body.containsKey("thinking"))
+    }
+
+    private fun zhipuProvider() = AiProviderConfig(
+        id = "zhipu",
+        name = "Zhipu AI",
+        protocol = AiProtocol.OPENAI_CHAT_COMPLETIONS,
+        baseUrl = "https://open.bigmodel.cn/api/paas/v4",
+        apiKey = "test"
+    )
+}