Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
99ea0569
authored
Feb 08, 2026
by
Calcium-Ion
Committed by
GitHub
Feb 08, 2026
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2854 from seefs001/fix/claude-tool-index
fix: Claude stream block index/type transitions
parents
bf45df9e
5171070f
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
84 additions
and
23 deletions
+84
-23
relay/common/relay_info.go
+3
-0
service/convert.go
+81
-23
No files found.
relay/common/relay_info.go
View file @
99ea0569
...
@@ -37,6 +37,9 @@ type ClaudeConvertInfo struct {
...
@@ -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
{
...
...
service/convert.go
View file @
99ea0569
...
@@ -207,6 +207,44 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
...
@@ -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
...
@@ -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
...
@@ -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
:
&
i
nfo
.
ClaudeConvertInfo
.
Inde
x
,
Index
:
&
i
d
x
,
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
...
@@ -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
:
&
i
nfo
.
ClaudeConvertInfo
.
Inde
x
,
Index
:
&
i
d
x
,
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
:
&
i
nfo
.
ClaudeConvertInfo
.
Index
,
Index
:
&
i
dx2
,
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
...
@@ -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
:
&
i
nfo
.
ClaudeConvertInfo
.
Inde
x
,
Index
:
&
i
d
x
,
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
:
&
i
nfo
.
ClaudeConvertInfo
.
Index
,
Index
:
&
i
dx2
,
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
...
@@ -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
...
@@ -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
...
@@ -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
...
@@ -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
:
&
i
nfo
.
ClaudeConvertInfo
.
Inde
x
,
Index
:
&
i
d
x
,
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
...
@@ -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
:
&
i
nfo
.
ClaudeConvertInfo
.
Inde
x
,
Index
:
&
i
d
x
,
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
...
@@ -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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment