Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ class ClaudeProvider(
private var activeResponse: Response? = null
@Volatile private var isManuallyCancelled = false

/**
* Thinking 格式模式。
* - ADAPTIVE: thinking.type="adaptive" + display=summarized(新模型)
* - ENABLED: thinking.type="enabled" + budget_tokens(旧模型)
*/
private enum class ThinkingFormat { ADAPTIVE, ENABLED }

/**
* 缓存:当前模型对应的 thinking 格式。
* 初始由模型名启发式决定;若 API 返回 thinking 类型不兼容的 400 错误,
* 会自动翻转并缓存,后续请求直接使用正确格式,避免重复失败。
*/
@Volatile
private var cachedThinkingFormat: ThinkingFormat? = null

/**
* 由客户端错误(如4xx状态码)触发的API异常,是否重试由统一策略决定
*/
Expand Down Expand Up @@ -970,6 +985,7 @@ class ClaudeProvider(

// 创建Claude API请求体
private fun createRequestBody(
context: Context,
chatHistory: List<PromptTurn>,
modelParameters: List<ModelParameter<*>> = emptyList(),
enableThinking: Boolean,
Expand Down Expand Up @@ -1017,18 +1033,35 @@ class ClaudeProvider(

// 添加extended thinking支持
if (enableThinking) {
val thinkingObject = JSONObject()
thinkingObject.put("type", "enabled")

val budgetTokensFromParams = modelParameters
.firstOrNull { it.apiName == "budget_tokens" }
?.currentValue
val budgetTokensValue = (budgetTokensFromParams as? Number)?.toInt()?.takeIf { it > 0 }
?: minOf(1024, maxTokensValue ?: DEFAULT_MAX_TOKENS)
thinkingObject.put("budget_tokens", budgetTokensValue)

jsonObject.put("thinking", thinkingObject)
AppLogger.d("AIService", "启用Claude的extended thinking功能")
val format = getThinkingFormat()
when (format) {
ThinkingFormat.ADAPTIVE -> {
// adaptive thinking: thinking.type=adaptive + display=summarized
// Opus 4.8/4.7 default display to "omitted" (empty thinking),
// must explicitly set "summarized" to receive thinking content.
val thinkingObject = JSONObject()
thinkingObject.put("type", "adaptive")
thinkingObject.put("display", "summarized")
jsonObject.put("thinking", thinkingObject)

AppLogger.d("AIService", "启用Claude adaptive thinking, display=summarized")
}
ThinkingFormat.ENABLED -> {
// enabled thinking: thinking.type=enabled + budget_tokens
val thinkingObject = JSONObject()
thinkingObject.put("type", "enabled")

val budgetTokensFromParams = modelParameters
.firstOrNull { it.apiName == "budget_tokens" }
?.currentValue
val budgetTokensValue = (budgetTokensFromParams as? Number)?.toInt()?.takeIf { it > 0 }
?: minOf(1024, maxTokensValue ?: DEFAULT_MAX_TOKENS)
thinkingObject.put("budget_tokens", budgetTokensValue)

jsonObject.put("thinking", thinkingObject)
AppLogger.d("AIService", "启用Claude extended thinking, budget_tokens=$budgetTokensValue")
}
}
}

// 日志输出时省略过长的tools字段
Expand Down Expand Up @@ -1060,6 +1093,75 @@ class ClaudeProvider(
}
}


/**
* 判断模型是否推荐使用 adaptive thinking 格式。
* 仅做启发式匹配,覆盖已知的官方模型名;
* 对于中转/代理平台的自定义模型名,将在首次请求失败时由回退逻辑自动修正。
*/
private fun prefersAdaptiveThinking(): Boolean {
val name = modelName.trim().lowercase()
return name.contains("opus-4-8") ||
name.contains("opus-4-7") ||
name.contains("opus-4-6") ||
name.contains("sonnet-4-6")
}

/**
* 获取当前模型应使用的 thinking 格式。
* 优先返回缓存值(包含回退后的正确结果);
* 无缓存时根据模型名启发式推断。
*/
private fun getThinkingFormat(): ThinkingFormat {
return cachedThinkingFormat
?: if (prefersAdaptiveThinking()) ThinkingFormat.ADAPTIVE
else ThinkingFormat.ENABLED
}

/**
* 在检测到 API 返回 thinking type 不兼容的 400 错误后,
* 翻转当前缓存的 thinking 格式并记录日志。
*/
private fun flipThinkingFormat(): ThinkingFormat {
val current = getThinkingFormat()
val flipped = if (current == ThinkingFormat.ADAPTIVE) ThinkingFormat.ENABLED
else ThinkingFormat.ADAPTIVE
cachedThinkingFormat = flipped
AppLogger.w(
"AIService",
"【Claude Thinking 回退】$modelName detected thinking type incompatibility, " +
"flipped $current → $flipped (cached for subsequent requests)"
)
return flipped
}

/**
* 检测异常是否由 thinking type 不兼容导致(API 返回400)。
* 匹配关键词:thinking.type / thinking_type / "enabled" is not supported / "adaptive" is not supported
* 同时检查 Anthropic 直接错误和通过中转平台转发的错误。
*/
private fun isThinkingTypeError(e: Exception): Boolean {
if (e !is NonRetriableException && e !is IOException) return false
val msg = e.message?.lowercase() ?: return false
// Anthropic 官方 / AWS Bedrock 的错误格式
return msg.contains("thinking") && (
msg.contains("is not supported") ||
msg.contains("not supported for this model") ||
msg.contains("type.") ||
msg.contains("unsupported") ||
msg.contains("invalid")
)
}

return when (qualityLevel.coerceIn(1, 4)) {
1 -> "low"
2 -> "medium"
3 -> "high"
4 -> "max"
else -> "high"
}
}

// 添加模型参数
private fun addParameters(jsonObject: JSONObject, modelParameters: List<ModelParameter<*>>) {
for (param in modelParameters) {
Expand Down Expand Up @@ -1087,7 +1189,8 @@ class ClaudeProvider(
}
// 忽略thinking相关参数,因为它们会在单独的部分处理
"thinking",
"budget_tokens" -> {
"budget_tokens",
"output_config" -> {
// 忽略,在特定部分处理
}
else -> {
Expand Down Expand Up @@ -1220,6 +1323,7 @@ class ClaudeProvider(
var lastException: Exception? = null
val receivedContent = StringBuilder()
val requestSavepointId = "attempt_${UUID.randomUUID().toString().replace("-", "")}"
var thinkingFormatFlipped = false // limit thinking format flip to once

suspend fun emitSavepoint(id: String) {
eventChannel.emit(TextStreamEvent(TextStreamEventType.SAVEPOINT, id))
Expand Down Expand Up @@ -1312,6 +1416,7 @@ class ClaudeProvider(
}

val requestBody = createRequestBody(
context,
chatHistory,
modelParameters,
enableThinking,
Expand Down Expand Up @@ -1736,15 +1841,29 @@ class ClaudeProvider(
} catch (e: Exception) {
lastException = e
emitRollback(requestSavepointId)
retryCount = handleRetryableError(
context,
e,
retryCount,
maxRetries,
enableRetry,
onNonFatalError
) { errorText, retryNumber ->
context.getString(R.string.provider_error_retry_message, errorText, retryNumber)

// 检测 thinking type 不兼容错误,自动翻转格式并立即重试
if (enableThinking && !thinkingFormatFlipped && isThinkingTypeError(e)) {
flipThinkingFormat()
thinkingFormatFlipped = true
onNonFatalError(
context.getString(R.string.provider_error_retry_message,
"Thinking format incompatibility detected, switching format",
retryCount + 1)
)
// 不增加 retryCount,因为这是格式问题而非网络问题
AppLogger.w("AIService", "【Claude】Thinking格式不兼容,已自动切换,准备立即重试")
} else {
retryCount = handleRetryableError(
context,
e,
retryCount,
maxRetries,
enableRetry,
onNonFatalError
) { errorText, retryNumber ->
context.getString(R.string.provider_error_retry_message, errorText, retryNumber)
}
}
} finally {
activeCall = null
Expand Down