mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-03-30 02:31:33 +00:00
fix: patch orphaned tool_use blocks missing tool_result in Claude relay
Clients (e.g. opencode) may truncate conversation history in long sessions, leaving tool_use blocks without corresponding tool_result. The upstream Claude API strictly validates this pairing and returns 400. This adds _patchOrphanedToolUse() to claudeRelayService._processRequestBody(), which detects orphaned tool_use IDs and synthesizes error tool_result blocks — the same approach already used in anthropicGeminiBridgeService for the Antigravity path. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -962,6 +962,89 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔧 修补孤立的 tool_use(缺少对应 tool_result)
|
||||||
|
// 客户端在长对话中可能截断历史消息,导致 tool_use 丢失对应的 tool_result,
|
||||||
|
// 上游 Claude API 严格校验每个 tool_use 必须紧跟 tool_result,否则返回 400。
|
||||||
|
_patchOrphanedToolUse(messages) {
|
||||||
|
if (!Array.isArray(messages) || messages.length === 0) {
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
|
||||||
|
const SYNTHETIC_TEXT = '[tool_result missing; tool execution interrupted]'
|
||||||
|
const makeSyntheticResult = (toolUseId) => ({
|
||||||
|
type: 'tool_result',
|
||||||
|
tool_use_id: toolUseId,
|
||||||
|
is_error: true,
|
||||||
|
content: [{ type: 'text', text: SYNTHETIC_TEXT }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const pendingToolUseIds = []
|
||||||
|
const patched = []
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
if (!message || !Array.isArray(message.content)) {
|
||||||
|
patched.push(message)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.role === 'assistant') {
|
||||||
|
if (pendingToolUseIds.length > 0) {
|
||||||
|
patched.push({
|
||||||
|
role: 'user',
|
||||||
|
content: pendingToolUseIds.map(makeSyntheticResult)
|
||||||
|
})
|
||||||
|
logger.warn(
|
||||||
|
`🔧 Patched ${pendingToolUseIds.length} orphaned tool_use(s): ${pendingToolUseIds.join(', ')}`
|
||||||
|
)
|
||||||
|
pendingToolUseIds.length = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolUseIds = message.content
|
||||||
|
.filter((part) => part?.type === 'tool_use' && typeof part.id === 'string')
|
||||||
|
.map((part) => part.id)
|
||||||
|
if (toolUseIds.length > 0) {
|
||||||
|
pendingToolUseIds.push(...toolUseIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
patched.push(message)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.role === 'user' && pendingToolUseIds.length > 0) {
|
||||||
|
const toolResultIds = new Set(
|
||||||
|
message.content
|
||||||
|
.filter((p) => p?.type === 'tool_result' && typeof p.tool_use_id === 'string')
|
||||||
|
.map((p) => p.tool_use_id)
|
||||||
|
)
|
||||||
|
const missing = pendingToolUseIds.filter((id) => !toolResultIds.has(id))
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
const synthetic = missing.map(makeSyntheticResult)
|
||||||
|
logger.warn(
|
||||||
|
`🔧 Patched ${missing.length} missing tool_result(s) in user message: ${missing.join(', ')}`
|
||||||
|
)
|
||||||
|
message.content = [...synthetic, ...message.content]
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingToolUseIds.length = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
patched.push(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingToolUseIds.length > 0) {
|
||||||
|
patched.push({
|
||||||
|
role: 'user',
|
||||||
|
content: pendingToolUseIds.map(makeSyntheticResult)
|
||||||
|
})
|
||||||
|
logger.warn(
|
||||||
|
`🔧 Patched ${pendingToolUseIds.length} trailing orphaned tool_use(s): ${pendingToolUseIds.join(', ')}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return patched
|
||||||
|
}
|
||||||
|
|
||||||
// 🔄 处理请求体
|
// 🔄 处理请求体
|
||||||
_processRequestBody(body, account = null) {
|
_processRequestBody(body, account = null) {
|
||||||
if (!body) {
|
if (!body) {
|
||||||
@@ -971,6 +1054,8 @@ class ClaudeRelayService {
|
|||||||
// 使用 safeClone 替代 JSON.parse(JSON.stringify()) 提升性能
|
// 使用 safeClone 替代 JSON.parse(JSON.stringify()) 提升性能
|
||||||
const processedBody = safeClone(body)
|
const processedBody = safeClone(body)
|
||||||
|
|
||||||
|
processedBody.messages = this._patchOrphanedToolUse(processedBody.messages)
|
||||||
|
|
||||||
// 验证并限制max_tokens参数
|
// 验证并限制max_tokens参数
|
||||||
this._validateAndLimitMaxTokens(processedBody)
|
this._validateAndLimitMaxTokens(processedBody)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user