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
2 changes: 1 addition & 1 deletion dto/claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type ClaudeMediaMessage struct {
PartialJson *string `json:"partial_json,omitempty"`
Role string `json:"role,omitempty"`
Thinking *string `json:"thinking,omitempty"`
Signature string `json:"signature,omitempty"`
Signature *string `json:"signature,omitempty"`
Delta string `json:"delta,omitempty"`
CacheControl json.RawMessage `json:"cache_control,omitempty"`
// tool_calls
Expand Down
8 changes: 8 additions & 0 deletions dto/openai_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ type Message struct {
Prefix *bool `json:"prefix,omitempty"`
ReasoningContent *string `json:"reasoning_content,omitempty"`
Reasoning *string `json:"reasoning,omitempty"`
ReasoningOpaque *string `json:"reasoning_opaque,omitempty"`
ToolCalls json.RawMessage `json:"tool_calls,omitempty"`
ToolCallId string `json:"tool_call_id,omitempty"`
parsedContent []MediaContent
Expand Down Expand Up @@ -441,6 +442,13 @@ func (m *Message) GetReasoningContent() string {
return *m.Reasoning
}

func (m *Message) GetReasoningOpaque() string {
if m.ReasoningOpaque == nil {
return ""
}
return *m.ReasoningOpaque
}

func (m *Message) GetPrefix() bool {
if m.Prefix == nil {
return false
Expand Down
8 changes: 8 additions & 0 deletions dto/openai_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type ChatCompletionsStreamResponseChoiceDelta struct {
Content *string `json:"content,omitempty"`
ReasoningContent *string `json:"reasoning_content,omitempty"`
Reasoning *string `json:"reasoning,omitempty"`
ReasoningOpaque *string `json:"reasoning_opaque,omitempty"`
Role string `json:"role,omitempty"`
ToolCalls []ToolCallResponse `json:"tool_calls,omitempty"`
}
Expand Down Expand Up @@ -119,6 +120,13 @@ func (c *ChatCompletionsStreamResponseChoiceDelta) SetReasoningContent(s string)
//c.Reasoning = &s
}

func (c *ChatCompletionsStreamResponseChoiceDelta) GetReasoningOpaque() string {
if c.ReasoningOpaque == nil {
return ""
}
return *c.ReasoningOpaque
}

type ToolCallResponse struct {
// Index is not nil only in chat completion chunk object
Index *int `json:"index,omitempty"`
Expand Down
4 changes: 4 additions & 0 deletions relay/common/relay_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ type ClaudeConvertInfo struct {
Usage *dto.Usage
FinishReason string
Done bool
ReasoningContent string
ReasoningOpaque string
SentThinking bool
SentSignature bool

ToolCallBaseIndex int
ToolCallMaxIndexOffset int
Expand Down
61 changes: 58 additions & 3 deletions service/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.Re
openAIRequest.Stream = lo.ToPtr(lo.FromPtr(claudeRequest.Stream))
}

isOpenRouter := info.ChannelType == constant.ChannelTypeOpenRouter
channelType := 0
originModelName := ""
if info != nil && info.ChannelMeta != nil {
channelType = info.ChannelType
}
if info != nil {
originModelName = info.OriginModelName
}
isOpenRouter := channelType == constant.ChannelTypeOpenRouter

if isOpenRouter {
if effort := claudeRequest.GetEfforts(); effort != "" {
Expand All @@ -59,7 +67,7 @@ func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.Re
}
} else {
thinkingSuffix := "-thinking"
if strings.HasSuffix(info.OriginModelName, thinkingSuffix) &&
if strings.HasSuffix(originModelName, thinkingSuffix) &&
!strings.HasSuffix(openAIRequest.Model, thinkingSuffix) {
openAIRequest.Model = openAIRequest.Model + thinkingSuffix
}
Expand Down Expand Up @@ -149,6 +157,13 @@ func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.Re

for _, mediaMsg := range contents {
switch mediaMsg.Type {
case "thinking":
if mediaMsg.Thinking != nil {
openAIMessage.ReasoningContent = mediaMsg.Thinking
}
if mediaMsg.Signature != nil {
openAIMessage.ReasoningOpaque = mediaMsg.Signature
}
case "text", "input_text":
message := dto.MediaContent{
Type: "text",
Expand Down Expand Up @@ -267,7 +282,21 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
// so we may have multiple open blocks and must stop each one explicitly.
stopOpenBlocks := func() {
switch info.ClaudeConvertInfo.LastMessagesType {
case relaycommon.LastMessageTypeText, relaycommon.LastMessageTypeThinking:
case relaycommon.LastMessageTypeThinking:
if !info.ClaudeConvertInfo.SentSignature {
idx := info.ClaudeConvertInfo.Index
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &idx,
Type: "content_block_delta",
Delta: &dto.ClaudeMediaMessage{
Type: "signature_delta",
Signature: common.GetPointer[string](info.ClaudeConvertInfo.ReasoningOpaque),
},
})
info.ClaudeConvertInfo.SentSignature = true
}
claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index))
case relaycommon.LastMessageTypeText:
claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index))
case relaycommon.LastMessageTypeTools:
base := info.ClaudeConvertInfo.ToolCallBaseIndex
Expand Down Expand Up @@ -359,6 +388,12 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
// 判断首个响应是否存在内容(非标准的 OpenAI 响应)
if len(openAIResponse.Choices) > 0 {
reasoning := openAIResponse.Choices[0].Delta.GetReasoningContent()
if reasoning != "" {
info.ClaudeConvertInfo.ReasoningContent += reasoning
}
if opaque := openAIResponse.Choices[0].Delta.GetReasoningOpaque(); opaque != "" {
info.ClaudeConvertInfo.ReasoningOpaque += opaque
}
content := openAIResponse.Choices[0].Delta.GetContentString()

if reasoning != "" {
Expand All @@ -374,6 +409,8 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
Thinking: common.GetPointer[string](""),
},
})
info.ClaudeConvertInfo.SentThinking = true
info.ClaudeConvertInfo.SentSignature = false
idx2 := idx
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &idx2,
Expand Down Expand Up @@ -462,6 +499,12 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
return claudeResponses
} else {
chosenChoice := openAIResponse.Choices[0]
if reasoning := chosenChoice.Delta.GetReasoningContent(); reasoning != "" {
info.ClaudeConvertInfo.ReasoningContent += reasoning
}
if opaque := chosenChoice.Delta.GetReasoningOpaque(); opaque != "" {
info.ClaudeConvertInfo.ReasoningOpaque += opaque
}
doneChunk := chosenChoice.FinishReason != nil && *chosenChoice.FinishReason != ""
if doneChunk {
info.FinishReason = *chosenChoice.FinishReason
Expand Down Expand Up @@ -543,6 +586,8 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
Thinking: common.GetPointer[string](""),
},
})
info.ClaudeConvertInfo.SentThinking = true
info.ClaudeConvertInfo.SentSignature = false
}
info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeThinking
claudeResponse.Delta = &dto.ClaudeMediaMessage{
Expand Down Expand Up @@ -615,6 +660,16 @@ func ResponseOpenAI2Claude(openAIResponse *dto.OpenAITextResponse, info *relayco
}
for _, choice := range openAIResponse.Choices {
stopReason = stopReasonOpenAI2Claude(choice.FinishReason)
if reasoning := choice.Message.GetReasoningContent(); reasoning != "" {
thinkingBlock := dto.ClaudeMediaMessage{
Type: "thinking",
Thinking: common.GetPointer[string](reasoning),
}
if signature := choice.Message.GetReasoningOpaque(); signature != "" {
thinkingBlock.Signature = common.GetPointer[string](signature)
}
contents = append(contents, thinkingBlock)
}
if choice.FinishReason == "tool_calls" {
for _, toolUse := range choice.Message.ParseToolCalls() {
claudeContent := dto.ClaudeMediaMessage{}
Expand Down
Loading