Skip to content
Open
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions relay/common/relay_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ type ClaudeConvertInfo struct {
Usage *dto.Usage
FinishReason string
Done bool
// ToolCallBlockIndex 记录 OpenAI tool_calls[].index 到 Anthropic content block index 的映射,
// 用于把并行工具调用的增量路由到各自的 content block(修复多 block 流式下参数粘连/丢弃)
ToolCallBlockIndex map[int]int
// ActiveBlocks 记录已 start 且未 stop 的 block index,流结束时统一补发 stop,防止遗漏
ActiveBlocks []int
}

const (
Expand Down
275 changes: 157 additions & 118 deletions service/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"one-api/dto"
"one-api/relay/channel/openrouter"
relaycommon "one-api/relay/common"
"sort"
"strings"
)

Expand Down Expand Up @@ -217,8 +218,29 @@ func generateStopBlock(index int) *dto.ClaudeResponse {
}
}

// addActiveBlock 记录一个已 start 未 stop 的 block index(集合语义,防重复)
func addActiveBlock(convertInfo *relaycommon.ClaudeConvertInfo, index int) {
for _, v := range convertInfo.ActiveBlocks {
if v == index {
return
}
}
convertInfo.ActiveBlocks = append(convertInfo.ActiveBlocks, index)
}

// removeActiveBlock 将已 stop 的 block index 从活跃集合中移除
func removeActiveBlock(convertInfo *relaycommon.ClaudeConvertInfo, index int) {
for i, v := range convertInfo.ActiveBlocks {
if v == index {
convertInfo.ActiveBlocks = append(convertInfo.ActiveBlocks[:i], convertInfo.ActiveBlocks[i+1:]...)
return
}
}
}

func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamResponse, info *relaycommon.RelayInfo) []*dto.ClaudeResponse {
var claudeResponses []*dto.ClaudeResponse
convertInfo := info.ClaudeConvertInfo
if info.SendResponseCount == 1 {
msg := &dto.ClaudeMediaMessage{
Id: openAIResponse.Id,
Expand All @@ -235,145 +257,162 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
Type: "message_start",
Message: msg,
})
claudeResponses = append(claudeResponses)
//claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
// Type: "ping",
//})
if openAIResponse.IsToolCall() {
resp := &dto.ClaudeResponse{
Type: "content_block_start",
ContentBlock: &dto.ClaudeMediaMessage{
Id: openAIResponse.GetFirstToolCall().ID,
Type: "tool_use",
Name: openAIResponse.GetFirstToolCall().Function.Name,
// 首个 chunk 的内容(thinking/text/tool_calls)不再在此提前 return,
// 继续走下方通用逻辑,避免首块即 tool_calls 时丢失增量
}

// 终结调用(Done,由 handleFinalResponse 触发):
// 无论最后一个 chunk 的形态(带 finish_reason / usage-only / 空 choices),都必须补齐收尾序列
if convertInfo.Done {
if len(openAIResponse.Choices) > 0 && openAIResponse.Choices[0].FinishReason != nil && *openAIResponse.Choices[0].FinishReason != "" {
convertInfo.FinishReason = *openAIResponse.Choices[0].FinishReason
}
// 关闭所有活跃 block(按 index 升序),防止 content_block_stop 遗漏
stops := make([]int, len(convertInfo.ActiveBlocks))
copy(stops, convertInfo.ActiveBlocks)
sort.Ints(stops)
for _, idx := range stops {
claudeResponses = append(claudeResponses, generateStopBlock(idx))
}
convertInfo.ActiveBlocks = nil
oaiUsage := convertInfo.Usage
if oaiUsage != nil {
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Type: "message_delta",
Usage: &dto.ClaudeUsage{
InputTokens: oaiUsage.PromptTokens,
OutputTokens: oaiUsage.CompletionTokens,
CacheCreationInputTokens: oaiUsage.PromptTokensDetails.CachedCreationTokens,
CacheReadInputTokens: oaiUsage.PromptTokensDetails.CachedTokens,
},
}
resp.SetIndex(0)
claudeResponses = append(claudeResponses, resp)
} else {
//resp := &dto.ClaudeResponse{
// Type: "content_block_start",
// ContentBlock: &dto.ClaudeMediaMessage{
// Type: "text",
// Text: common.GetPointer[string](""),
// },
//}
//resp.SetIndex(0)
//claudeResponses = append(claudeResponses, resp)
Delta: &dto.ClaudeMediaMessage{
StopReason: common.GetPointer[string](stopReasonOpenAI2Claude(convertInfo.FinishReason)),
},
})
}
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Type: "message_stop",
})
return claudeResponses
}

if len(openAIResponse.Choices) == 0 {
// no choices
// TODO: handle this case
return claudeResponses
} else {
chosenChoice := openAIResponse.Choices[0]
if chosenChoice.FinishReason != nil && *chosenChoice.FinishReason != "" {
// should be done
info.FinishReason = *chosenChoice.FinishReason
return claudeResponses
}
chosenChoice := openAIResponse.Choices[0]
if chosenChoice.FinishReason != nil && *chosenChoice.FinishReason != "" {
// should be done
convertInfo.FinishReason = *chosenChoice.FinishReason
return claudeResponses
}

if len(chosenChoice.Delta.ToolCalls) > 0 {
// ===== tool_calls 增量:按上游 tool_calls[].index 路由到各自的 content block =====
if convertInfo.ToolCallBlockIndex == nil {
convertInfo.ToolCallBlockIndex = make(map[int]int)
}
// 从 text/thinking 切换到 tools:先关闭当前文本类 block
if convertInfo.LastMessagesType == relaycommon.LastMessageTypeText ||
convertInfo.LastMessagesType == relaycommon.LastMessageTypeThinking {
claudeResponses = append(claudeResponses, generateStopBlock(convertInfo.Index))
removeActiveBlock(convertInfo, convertInfo.Index)
convertInfo.Index++
}
if info.Done {
claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index))
oaiUsage := info.ClaudeConvertInfo.Usage
if oaiUsage != nil {
convertInfo.LastMessagesType = relaycommon.LastMessageTypeTools
for _, toolCall := range chosenChoice.Delta.ToolCalls {
// 上游 index 缺失时按 0 处理(单工具上游常见省略)
oaiIndex := 0
if toolCall.Index != nil {
oaiIndex = *toolCall.Index
}
blockIndex, ok := convertInfo.ToolCallBlockIndex[oaiIndex]
if !ok {
// 新的 tool call:分配新的 content block(首个 block 沿用当前 Index,后续递增)
if len(convertInfo.ToolCallBlockIndex) > 0 {
convertInfo.Index++
}
blockIndex = convertInfo.Index
convertInfo.ToolCallBlockIndex[oaiIndex] = blockIndex
addActiveBlock(convertInfo, blockIndex)
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Type: "message_delta",
Usage: &dto.ClaudeUsage{
InputTokens: oaiUsage.PromptTokens,
OutputTokens: oaiUsage.CompletionTokens,
CacheCreationInputTokens: oaiUsage.PromptTokensDetails.CachedCreationTokens,
CacheReadInputTokens: oaiUsage.PromptTokensDetails.CachedTokens,
Index: common.GetPointer[int](blockIndex),
Type: "content_block_start",
ContentBlock: &dto.ClaudeMediaMessage{
Id: toolCall.ID,
Type: "tool_use",
Name: toolCall.Function.Name,
Input: map[string]interface{}{},
},
})
}
if toolCall.Function.Arguments != "" {
arguments := toolCall.Function.Arguments
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: common.GetPointer[int](blockIndex),
Type: "content_block_delta",
Delta: &dto.ClaudeMediaMessage{
StopReason: common.GetPointer[string](stopReasonOpenAI2Claude(info.FinishReason)),
Type: "input_json_delta",
PartialJson: &arguments,
},
})
}
}
return claudeResponses
}

// ===== text / thinking 增量 =====
reasoning := chosenChoice.Delta.GetReasoningContent()
textContent := chosenChoice.Delta.GetContentString()
if reasoning == "" && textContent == "" {
return claudeResponses
}
var claudeResponse dto.ClaudeResponse
claudeResponse.Type = "content_block_delta"
if reasoning != "" {
if convertInfo.LastMessagesType != relaycommon.LastMessageTypeThinking {
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Type: "message_stop",
Index: common.GetPointer[int](convertInfo.Index),
Type: "content_block_start",
ContentBlock: &dto.ClaudeMediaMessage{
Type: "thinking",
Thinking: "",
},
})
} else {
var claudeResponse dto.ClaudeResponse
var isEmpty bool
claudeResponse.Type = "content_block_delta"
if len(chosenChoice.Delta.ToolCalls) > 0 {
if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeTools {
claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index))
info.ClaudeConvertInfo.Index++
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &info.ClaudeConvertInfo.Index,
Type: "content_block_start",
ContentBlock: &dto.ClaudeMediaMessage{
Id: openAIResponse.GetFirstToolCall().ID,
Type: "tool_use",
Name: openAIResponse.GetFirstToolCall().Function.Name,
Input: map[string]interface{}{},
},
})
}
info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeTools
// tools delta
claudeResponse.Delta = &dto.ClaudeMediaMessage{
Type: "input_json_delta",
PartialJson: &chosenChoice.Delta.ToolCalls[0].Function.Arguments,
}
} else {
reasoning := chosenChoice.Delta.GetReasoningContent()
textContent := chosenChoice.Delta.GetContentString()
if reasoning != "" || textContent != "" {
if reasoning != "" {
if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeThinking {
//info.ClaudeConvertInfo.Index++
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &info.ClaudeConvertInfo.Index,
Type: "content_block_start",
ContentBlock: &dto.ClaudeMediaMessage{
Type: "thinking",
Thinking: "",
},
})
}
info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeThinking
// text delta
claudeResponse.Delta = &dto.ClaudeMediaMessage{
Type: "thinking_delta",
Thinking: reasoning,
}
} else {
if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeText {
if info.LastMessagesType == relaycommon.LastMessageTypeThinking || info.LastMessagesType == relaycommon.LastMessageTypeTools {
claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index))
info.ClaudeConvertInfo.Index++
}
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &info.ClaudeConvertInfo.Index,
Type: "content_block_start",
ContentBlock: &dto.ClaudeMediaMessage{
Type: "text",
Text: common.GetPointer[string](""),
},
})
}
info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeText
// text delta
claudeResponse.Delta = &dto.ClaudeMediaMessage{
Type: "text_delta",
Text: common.GetPointer[string](textContent),
}
}
} else {
isEmpty = true
}
}
claudeResponse.Index = &info.ClaudeConvertInfo.Index
if !isEmpty {
claudeResponses = append(claudeResponses, &claudeResponse)
addActiveBlock(convertInfo, convertInfo.Index)
}
convertInfo.LastMessagesType = relaycommon.LastMessageTypeThinking
claudeResponse.Index = common.GetPointer[int](convertInfo.Index)
claudeResponse.Delta = &dto.ClaudeMediaMessage{
Type: "thinking_delta",
Thinking: reasoning,
}
} else {
if convertInfo.LastMessagesType != relaycommon.LastMessageTypeText {
if convertInfo.LastMessagesType == relaycommon.LastMessageTypeThinking || convertInfo.LastMessagesType == relaycommon.LastMessageTypeTools {
claudeResponses = append(claudeResponses, generateStopBlock(convertInfo.Index))
removeActiveBlock(convertInfo, convertInfo.Index)
convertInfo.Index++
}
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: common.GetPointer[int](convertInfo.Index),
Type: "content_block_start",
ContentBlock: &dto.ClaudeMediaMessage{
Type: "text",
Text: common.GetPointer[string](""),
},
})
addActiveBlock(convertInfo, convertInfo.Index)
}
convertInfo.LastMessagesType = relaycommon.LastMessageTypeText
claudeResponse.Index = common.GetPointer[int](convertInfo.Index)
claudeResponse.Delta = &dto.ClaudeMediaMessage{
Type: "text_delta",
Text: common.GetPointer[string](textContent),
}
}
claudeResponses = append(claudeResponses, &claudeResponse)

return claudeResponses
}
Expand Down
Loading