Merge pull request #2854 from seefs001/fix/claude-tool-index

fix: Claude stream block index/type transitions
This commit is contained in:
Calcium-Ion
2026-02-08 00:15:20 +08:00
committed by GitHub
2 changed files with 84 additions and 23 deletions

View File

@@ -37,6 +37,9 @@ type ClaudeConvertInfo struct {
Usage *dto.Usage Usage *dto.Usage
FinishReason string FinishReason string
Done bool Done bool
ToolCallBaseIndex int
ToolCallMaxIndexOffset int
} }
type RerankerInfo struct { type RerankerInfo struct {

View File

@@ -207,6 +207,44 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
} }
var claudeResponses []*dto.ClaudeResponse var claudeResponses []*dto.ClaudeResponse
// stopOpenBlocks emits the required content_block_stop event(s) for the currently open block(s)
// according to Anthropic's SSE streaming state machine:
// content_block_start -> content_block_delta* -> content_block_stop (per index).
//
// For text/thinking, there is at most one open block at info.ClaudeConvertInfo.Index.
// For tools, OpenAI tool_calls can stream multiple parallel tool_use blocks (indexed from 0),
// so we may have multiple open blocks and must stop each one explicitly.
stopOpenBlocks := func() {
switch info.ClaudeConvertInfo.LastMessagesType {
case relaycommon.LastMessageTypeText, relaycommon.LastMessageTypeThinking:
claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index))
case relaycommon.LastMessageTypeTools:
base := info.ClaudeConvertInfo.ToolCallBaseIndex
for offset := 0; offset <= info.ClaudeConvertInfo.ToolCallMaxIndexOffset; offset++ {
claudeResponses = append(claudeResponses, generateStopBlock(base+offset))
}
}
}
// stopOpenBlocksAndAdvance closes the currently open block(s) and advances the content block index
// to the next available slot for subsequent content_block_start events.
//
// This prevents invalid streams where a content_block_delta (e.g. thinking_delta) is emitted for an
// index whose active content_block type is different (the typical cause of "Mismatched content block type").
stopOpenBlocksAndAdvance := func() {
if info.ClaudeConvertInfo.LastMessagesType == relaycommon.LastMessageTypeNone {
return
}
stopOpenBlocks()
switch info.ClaudeConvertInfo.LastMessagesType {
case relaycommon.LastMessageTypeTools:
info.ClaudeConvertInfo.Index = info.ClaudeConvertInfo.ToolCallBaseIndex + info.ClaudeConvertInfo.ToolCallMaxIndexOffset + 1
info.ClaudeConvertInfo.ToolCallBaseIndex = 0
info.ClaudeConvertInfo.ToolCallMaxIndexOffset = 0
default:
info.ClaudeConvertInfo.Index++
}
info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeNone
}
if info.SendResponseCount == 1 { if info.SendResponseCount == 1 {
msg := &dto.ClaudeMediaMessage{ msg := &dto.ClaudeMediaMessage{
Id: openAIResponse.Id, Id: openAIResponse.Id,
@@ -228,6 +266,8 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
//}) //})
if openAIResponse.IsToolCall() { if openAIResponse.IsToolCall() {
info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeTools info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeTools
info.ClaudeConvertInfo.ToolCallBaseIndex = 0
info.ClaudeConvertInfo.ToolCallMaxIndexOffset = 0
var toolCall dto.ToolCallResponse var toolCall dto.ToolCallResponse
if len(openAIResponse.Choices) > 0 && len(openAIResponse.Choices[0].Delta.ToolCalls) > 0 { if len(openAIResponse.Choices) > 0 && len(openAIResponse.Choices[0].Delta.ToolCalls) > 0 {
toolCall = openAIResponse.Choices[0].Delta.ToolCalls[0] toolCall = openAIResponse.Choices[0].Delta.ToolCalls[0]
@@ -252,8 +292,9 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
claudeResponses = append(claudeResponses, resp) claudeResponses = append(claudeResponses, resp)
// 首块包含工具 delta则追加 input_json_delta // 首块包含工具 delta则追加 input_json_delta
if toolCall.Function.Arguments != "" { if toolCall.Function.Arguments != "" {
idx := 0
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &info.ClaudeConvertInfo.Index, Index: &idx,
Type: "content_block_delta", Type: "content_block_delta",
Delta: &dto.ClaudeMediaMessage{ Delta: &dto.ClaudeMediaMessage{
Type: "input_json_delta", Type: "input_json_delta",
@@ -270,16 +311,21 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
content := openAIResponse.Choices[0].Delta.GetContentString() content := openAIResponse.Choices[0].Delta.GetContentString()
if reasoning != "" { if reasoning != "" {
if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeThinking {
stopOpenBlocksAndAdvance()
}
idx := info.ClaudeConvertInfo.Index
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &info.ClaudeConvertInfo.Index, Index: &idx,
Type: "content_block_start", Type: "content_block_start",
ContentBlock: &dto.ClaudeMediaMessage{ ContentBlock: &dto.ClaudeMediaMessage{
Type: "thinking", Type: "thinking",
Thinking: common.GetPointer[string](""), Thinking: common.GetPointer[string](""),
}, },
}) })
idx2 := idx
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &info.ClaudeConvertInfo.Index, Index: &idx2,
Type: "content_block_delta", Type: "content_block_delta",
Delta: &dto.ClaudeMediaMessage{ Delta: &dto.ClaudeMediaMessage{
Type: "thinking_delta", Type: "thinking_delta",
@@ -288,16 +334,21 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
}) })
info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeThinking info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeThinking
} else if content != "" { } else if content != "" {
if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeText {
stopOpenBlocksAndAdvance()
}
idx := info.ClaudeConvertInfo.Index
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &info.ClaudeConvertInfo.Index, Index: &idx,
Type: "content_block_start", Type: "content_block_start",
ContentBlock: &dto.ClaudeMediaMessage{ ContentBlock: &dto.ClaudeMediaMessage{
Type: "text", Type: "text",
Text: common.GetPointer[string](""), Text: common.GetPointer[string](""),
}, },
}) })
idx2 := idx
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &info.ClaudeConvertInfo.Index, Index: &idx2,
Type: "content_block_delta", Type: "content_block_delta",
Delta: &dto.ClaudeMediaMessage{ Delta: &dto.ClaudeMediaMessage{
Type: "text_delta", Type: "text_delta",
@@ -311,7 +362,7 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
// 如果首块就带 finish_reason需要立即发送停止块 // 如果首块就带 finish_reason需要立即发送停止块
if len(openAIResponse.Choices) > 0 && openAIResponse.Choices[0].FinishReason != nil && *openAIResponse.Choices[0].FinishReason != "" { if len(openAIResponse.Choices) > 0 && openAIResponse.Choices[0].FinishReason != nil && *openAIResponse.Choices[0].FinishReason != "" {
info.FinishReason = *openAIResponse.Choices[0].FinishReason info.FinishReason = *openAIResponse.Choices[0].FinishReason
claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index)) stopOpenBlocks()
oaiUsage := openAIResponse.Usage oaiUsage := openAIResponse.Usage
if oaiUsage == nil { if oaiUsage == nil {
oaiUsage = info.ClaudeConvertInfo.Usage oaiUsage = info.ClaudeConvertInfo.Usage
@@ -342,7 +393,7 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
// no choices // no choices
// 可能为非标准的 OpenAI 响应,判断是否已经完成 // 可能为非标准的 OpenAI 响应,判断是否已经完成
if info.ClaudeConvertInfo.Done { if info.ClaudeConvertInfo.Done {
claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index)) stopOpenBlocks()
oaiUsage := info.ClaudeConvertInfo.Usage oaiUsage := info.ClaudeConvertInfo.Usage
if oaiUsage != nil { if oaiUsage != nil {
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
@@ -376,18 +427,25 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
if len(chosenChoice.Delta.ToolCalls) > 0 { if len(chosenChoice.Delta.ToolCalls) > 0 {
toolCalls := chosenChoice.Delta.ToolCalls toolCalls := chosenChoice.Delta.ToolCalls
if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeTools { if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeTools {
claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index)) stopOpenBlocksAndAdvance()
info.ClaudeConvertInfo.Index++ info.ClaudeConvertInfo.ToolCallBaseIndex = info.ClaudeConvertInfo.Index
info.ClaudeConvertInfo.ToolCallMaxIndexOffset = 0
} }
info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeTools info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeTools
base := info.ClaudeConvertInfo.ToolCallBaseIndex
maxOffset := info.ClaudeConvertInfo.ToolCallMaxIndexOffset
for i, toolCall := range toolCalls { for i, toolCall := range toolCalls {
blockIndex := info.ClaudeConvertInfo.Index offset := 0
if toolCall.Index != nil { if toolCall.Index != nil {
blockIndex = *toolCall.Index offset = *toolCall.Index
} else if len(toolCalls) > 1 { } else {
blockIndex = info.ClaudeConvertInfo.Index + i offset = i
} }
if offset > maxOffset {
maxOffset = offset
}
blockIndex := base + offset
idx := blockIndex idx := blockIndex
if toolCall.Function.Name != "" { if toolCall.Function.Name != "" {
@@ -413,17 +471,19 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
}, },
}) })
} }
info.ClaudeConvertInfo.Index = blockIndex
} }
info.ClaudeConvertInfo.ToolCallMaxIndexOffset = maxOffset
info.ClaudeConvertInfo.Index = base + maxOffset
} else { } else {
reasoning := chosenChoice.Delta.GetReasoningContent() reasoning := chosenChoice.Delta.GetReasoningContent()
textContent := chosenChoice.Delta.GetContentString() textContent := chosenChoice.Delta.GetContentString()
if reasoning != "" || textContent != "" { if reasoning != "" || textContent != "" {
if reasoning != "" { if reasoning != "" {
if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeThinking { if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeThinking {
stopOpenBlocksAndAdvance()
idx := info.ClaudeConvertInfo.Index
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &info.ClaudeConvertInfo.Index, Index: &idx,
Type: "content_block_start", Type: "content_block_start",
ContentBlock: &dto.ClaudeMediaMessage{ ContentBlock: &dto.ClaudeMediaMessage{
Type: "thinking", Type: "thinking",
@@ -438,12 +498,10 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
} }
} else { } else {
if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeText { if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeText {
if info.ClaudeConvertInfo.LastMessagesType == relaycommon.LastMessageTypeThinking || info.ClaudeConvertInfo.LastMessagesType == relaycommon.LastMessageTypeTools { stopOpenBlocksAndAdvance()
claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index)) idx := info.ClaudeConvertInfo.Index
info.ClaudeConvertInfo.Index++
}
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &info.ClaudeConvertInfo.Index, Index: &idx,
Type: "content_block_start", Type: "content_block_start",
ContentBlock: &dto.ClaudeMediaMessage{ ContentBlock: &dto.ClaudeMediaMessage{
Type: "text", Type: "text",
@@ -462,13 +520,13 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
} }
} }
claudeResponse.Index = &info.ClaudeConvertInfo.Index claudeResponse.Index = common.GetPointer[int](info.ClaudeConvertInfo.Index)
if !isEmpty && claudeResponse.Delta != nil { if !isEmpty && claudeResponse.Delta != nil {
claudeResponses = append(claudeResponses, &claudeResponse) claudeResponses = append(claudeResponses, &claudeResponse)
} }
if doneChunk || info.ClaudeConvertInfo.Done { if doneChunk || info.ClaudeConvertInfo.Done {
claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index)) stopOpenBlocks()
oaiUsage := openAIResponse.Usage oaiUsage := openAIResponse.Usage
if oaiUsage == nil { if oaiUsage == nil {
oaiUsage = info.ClaudeConvertInfo.Usage oaiUsage = info.ClaudeConvertInfo.Usage