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
0d81053e
authored
Mar 02, 2026
by
Calcium-Ion
Committed by
GitHub
Mar 02, 2026
Browse files
Options
Browse Files
Download
Plain Diff
fix: tool responses (#3080)
parents
ad326beb
7cc8ec2c
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
131 additions
and
24 deletions
+131
-24
relay/channel/claude/relay-claude.go
+66
-16
relay/channel/claude/relay_claude_test.go
+65
-8
No files found.
relay/channel/claude/relay-claude.go
View file @
0d81053e
...
@@ -404,12 +404,15 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe
...
@@ -404,12 +404,15 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe
return
&
claudeRequest
,
nil
return
&
claudeRequest
,
nil
}
}
func
StreamResponseClaude2OpenAI
(
claudeResponse
*
dto
.
ClaudeResponse
)
*
dto
.
ChatCompletionsStreamResponse
{
func
StreamResponseClaude2OpenAI
(
claudeResponse
*
dto
.
ClaudeResponse
,
claudeInfo
*
ClaudeResponseInfo
)
*
dto
.
ChatCompletionsStreamResponse
{
var
response
dto
.
ChatCompletionsStreamResponse
var
response
dto
.
ChatCompletionsStreamResponse
response
.
Object
=
"chat.completion.chunk"
response
.
Object
=
"chat.completion.chunk"
response
.
Model
=
claudeResponse
.
Model
response
.
Model
=
claudeResponse
.
Model
response
.
Choices
=
make
([]
dto
.
ChatCompletionsStreamResponseChoice
,
0
)
response
.
Choices
=
make
([]
dto
.
ChatCompletionsStreamResponseChoice
,
0
)
tools
:=
make
([]
dto
.
ToolCallResponse
,
0
)
tools
:=
make
([]
dto
.
ToolCallResponse
,
0
)
if
claudeInfo
!=
nil
&&
claudeInfo
.
ToolCallStreamStates
==
nil
{
claudeInfo
.
ToolCallStreamStates
=
make
(
map
[
int
]
*
ToolCallStreamState
)
}
fcIdx
:=
0
fcIdx
:=
0
if
claudeResponse
.
Index
!=
nil
{
if
claudeResponse
.
Index
!=
nil
{
fcIdx
=
*
claudeResponse
.
Index
-
1
fcIdx
=
*
claudeResponse
.
Index
-
1
...
@@ -433,6 +436,13 @@ func StreamResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.ChatCo
...
@@ -433,6 +436,13 @@ func StreamResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.ChatCo
choice
.
Delta
.
SetContentString
(
*
claudeResponse
.
ContentBlock
.
Text
)
choice
.
Delta
.
SetContentString
(
*
claudeResponse
.
ContentBlock
.
Text
)
}
}
if
claudeResponse
.
ContentBlock
.
Type
==
"tool_use"
{
if
claudeResponse
.
ContentBlock
.
Type
==
"tool_use"
{
if
claudeInfo
!=
nil
{
claudeInfo
.
ToolCallStreamStates
[
fcIdx
]
=
&
ToolCallStreamState
{
ID
:
claudeResponse
.
ContentBlock
.
Id
,
Name
:
claudeResponse
.
ContentBlock
.
Name
,
}
return
nil
}
tools
=
append
(
tools
,
dto
.
ToolCallResponse
{
tools
=
append
(
tools
,
dto
.
ToolCallResponse
{
Index
:
common
.
GetPointer
(
fcIdx
),
Index
:
common
.
GetPointer
(
fcIdx
),
ID
:
claudeResponse
.
ContentBlock
.
Id
,
ID
:
claudeResponse
.
ContentBlock
.
Id
,
...
@@ -451,19 +461,28 @@ func StreamResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.ChatCo
...
@@ -451,19 +461,28 @@ func StreamResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.ChatCo
choice
.
Delta
.
Content
=
claudeResponse
.
Delta
.
Text
choice
.
Delta
.
Content
=
claudeResponse
.
Delta
.
Text
switch
claudeResponse
.
Delta
.
Type
{
switch
claudeResponse
.
Delta
.
Type
{
case
"input_json_delta"
:
case
"input_json_delta"
:
arguments
:=
"{}"
if
claudeResponse
.
Delta
.
PartialJson
==
nil
{
if
claudeResponse
.
Delta
.
PartialJson
!=
nil
{
return
nil
if
partial
:=
strings
.
TrimSpace
(
*
claudeResponse
.
Delta
.
PartialJson
);
partial
!=
""
{
arguments
=
partial
}
}
}
tools
=
append
(
tools
,
dto
.
ToolCallResponse
{
arguments
:=
*
claudeResponse
.
Delta
.
PartialJson
if
strings
.
TrimSpace
(
arguments
)
==
""
{
return
nil
}
toolCall
:=
dto
.
ToolCallResponse
{
Type
:
"function"
,
Type
:
"function"
,
Index
:
common
.
GetPointer
(
fcIdx
),
Index
:
common
.
GetPointer
(
fcIdx
),
Function
:
dto
.
FunctionResponse
{
Function
:
dto
.
FunctionResponse
{
Arguments
:
arguments
,
Arguments
:
arguments
,
},
},
})
}
if
claudeInfo
!=
nil
{
if
state
,
ok
:=
claudeInfo
.
ToolCallStreamStates
[
fcIdx
];
ok
{
state
.
Emitted
=
true
toolCall
.
ID
=
state
.
ID
toolCall
.
Function
.
Name
=
state
.
Name
}
}
tools
=
append
(
tools
,
toolCall
)
case
"signature_delta"
:
case
"signature_delta"
:
// 加密的不处理
// 加密的不处理
signatureContent
:=
"
\n
"
signatureContent
:=
"
\n
"
...
@@ -472,6 +491,27 @@ func StreamResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.ChatCo
...
@@ -472,6 +491,27 @@ func StreamResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.ChatCo
choice
.
Delta
.
ReasoningContent
=
claudeResponse
.
Delta
.
Thinking
choice
.
Delta
.
ReasoningContent
=
claudeResponse
.
Delta
.
Thinking
}
}
}
}
}
else
if
claudeResponse
.
Type
==
"content_block_stop"
{
if
claudeInfo
==
nil
{
return
nil
}
state
,
ok
:=
claudeInfo
.
ToolCallStreamStates
[
fcIdx
]
if
!
ok
{
return
nil
}
delete
(
claudeInfo
.
ToolCallStreamStates
,
fcIdx
)
if
state
.
Emitted
{
return
nil
}
tools
=
append
(
tools
,
dto
.
ToolCallResponse
{
ID
:
state
.
ID
,
Type
:
"function"
,
Index
:
common
.
GetPointer
(
fcIdx
),
Function
:
dto
.
FunctionResponse
{
Name
:
state
.
Name
,
Arguments
:
"{}"
,
},
})
}
else
if
claudeResponse
.
Type
==
"message_delta"
{
}
else
if
claudeResponse
.
Type
==
"message_delta"
{
if
claudeResponse
.
Delta
!=
nil
&&
claudeResponse
.
Delta
.
StopReason
!=
nil
{
if
claudeResponse
.
Delta
!=
nil
&&
claudeResponse
.
Delta
.
StopReason
!=
nil
{
finishReason
:=
stopReasonClaude2OpenAI
(
*
claudeResponse
.
Delta
.
StopReason
)
finishReason
:=
stopReasonClaude2OpenAI
(
*
claudeResponse
.
Delta
.
StopReason
)
...
@@ -556,12 +596,19 @@ func ResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.OpenAITextRe
...
@@ -556,12 +596,19 @@ func ResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.OpenAITextRe
}
}
type
ClaudeResponseInfo
struct
{
type
ClaudeResponseInfo
struct
{
ResponseId
string
ResponseId
string
Created
int64
Created
int64
Model
string
Model
string
ResponseText
strings
.
Builder
ResponseText
strings
.
Builder
Usage
*
dto
.
Usage
Usage
*
dto
.
Usage
Done
bool
Done
bool
ToolCallStreamStates
map
[
int
]
*
ToolCallStreamState
}
type
ToolCallStreamState
struct
{
ID
string
Name
string
Emitted
bool
}
}
func
buildMessageDeltaPatchUsage
(
claudeResponse
*
dto
.
ClaudeResponse
,
claudeInfo
*
ClaudeResponseInfo
)
*
dto
.
ClaudeUsage
{
func
buildMessageDeltaPatchUsage
(
claudeResponse
*
dto
.
ClaudeResponse
,
claudeInfo
*
ClaudeResponseInfo
)
*
dto
.
ClaudeUsage
{
...
@@ -694,7 +741,7 @@ func FormatClaudeResponseInfo(claudeResponse *dto.ClaudeResponse, oaiResponse *d
...
@@ -694,7 +741,7 @@ func FormatClaudeResponseInfo(claudeResponse *dto.ClaudeResponse, oaiResponse *d
// 判断是否完整
// 判断是否完整
claudeInfo
.
Done
=
true
claudeInfo
.
Done
=
true
}
else
if
claudeResponse
.
Type
==
"content_block_start"
{
}
else
if
claudeResponse
.
Type
==
"content_block_start"
||
claudeResponse
.
Type
==
"content_block_stop"
{
}
else
{
}
else
{
return
false
return
false
}
}
...
@@ -739,7 +786,10 @@ func HandleStreamResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
...
@@ -739,7 +786,10 @@ func HandleStreamResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
}
}
helper
.
ClaudeChunkData
(
c
,
claudeResponse
,
data
)
helper
.
ClaudeChunkData
(
c
,
claudeResponse
,
data
)
}
else
if
info
.
RelayFormat
==
types
.
RelayFormatOpenAI
{
}
else
if
info
.
RelayFormat
==
types
.
RelayFormatOpenAI
{
response
:=
StreamResponseClaude2OpenAI
(
&
claudeResponse
)
response
:=
StreamResponseClaude2OpenAI
(
&
claudeResponse
,
claudeInfo
)
if
response
==
nil
{
return
nil
}
if
!
FormatClaudeResponseInfo
(
&
claudeResponse
,
response
,
claudeInfo
)
{
if
!
FormatClaudeResponseInfo
(
&
claudeResponse
,
response
,
claudeInfo
)
{
return
nil
return
nil
...
...
relay/channel/claude/relay_claude_test.go
View file @
0d81053e
...
@@ -216,7 +216,7 @@ func TestRequestOpenAI2ClaudeMessage_AssistantToolCallWithMalformedArguments(t *
...
@@ -216,7 +216,7 @@ func TestRequestOpenAI2ClaudeMessage_AssistantToolCallWithMalformedArguments(t *
assert
.
Empty
(
t
,
inputObj
)
assert
.
Empty
(
t
,
inputObj
)
}
}
func
TestStreamResponseClaude2OpenAI_EmptyInputJSONDelta
Fallback
(
t
*
testing
.
T
)
{
func
TestStreamResponseClaude2OpenAI_EmptyInputJSONDelta
Ignored
(
t
*
testing
.
T
)
{
empty
:=
""
empty
:=
""
resp
:=
&
dto
.
ClaudeResponse
{
resp
:=
&
dto
.
ClaudeResponse
{
Type
:
"content_block_delta"
,
Type
:
"content_block_delta"
,
...
@@ -227,12 +227,8 @@ func TestStreamResponseClaude2OpenAI_EmptyInputJSONDeltaFallback(t *testing.T) {
...
@@ -227,12 +227,8 @@ func TestStreamResponseClaude2OpenAI_EmptyInputJSONDeltaFallback(t *testing.T) {
},
},
}
}
chunk
:=
StreamResponseClaude2OpenAI
(
resp
)
chunk
:=
StreamResponseClaude2OpenAI
(
resp
,
&
ClaudeResponseInfo
{})
require
.
NotNil
(
t
,
chunk
)
require
.
Nil
(
t
,
chunk
)
require
.
Len
(
t
,
chunk
.
Choices
,
1
)
require
.
NotNil
(
t
,
chunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
)
require
.
Len
(
t
,
chunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
,
1
)
assert
.
Equal
(
t
,
"{}"
,
chunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
[
0
]
.
Function
.
Arguments
)
}
}
func
TestStreamResponseClaude2OpenAI_NonEmptyInputJSONDeltaPreserved
(
t
*
testing
.
T
)
{
func
TestStreamResponseClaude2OpenAI_NonEmptyInputJSONDeltaPreserved
(
t
*
testing
.
T
)
{
...
@@ -246,10 +242,71 @@ func TestStreamResponseClaude2OpenAI_NonEmptyInputJSONDeltaPreserved(t *testing.
...
@@ -246,10 +242,71 @@ func TestStreamResponseClaude2OpenAI_NonEmptyInputJSONDeltaPreserved(t *testing.
},
},
}
}
chunk
:=
StreamResponseClaude2OpenAI
(
resp
)
chunk
:=
StreamResponseClaude2OpenAI
(
resp
,
&
ClaudeResponseInfo
{}
)
require
.
NotNil
(
t
,
chunk
)
require
.
NotNil
(
t
,
chunk
)
require
.
Len
(
t
,
chunk
.
Choices
,
1
)
require
.
Len
(
t
,
chunk
.
Choices
,
1
)
require
.
NotNil
(
t
,
chunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
)
require
.
NotNil
(
t
,
chunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
)
require
.
Len
(
t
,
chunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
,
1
)
require
.
Len
(
t
,
chunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
,
1
)
assert
.
Equal
(
t
,
partial
,
chunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
[
0
]
.
Function
.
Arguments
)
assert
.
Equal
(
t
,
partial
,
chunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
[
0
]
.
Function
.
Arguments
)
}
}
func
TestStreamResponseClaude2OpenAI_NoArgToolEmitsObjectAtStop
(
t
*
testing
.
T
)
{
claudeInfo
:=
&
ClaudeResponseInfo
{}
start
:=
&
dto
.
ClaudeResponse
{
Type
:
"content_block_start"
,
Index
:
func
()
*
int
{
v
:=
1
;
return
&
v
}(),
ContentBlock
:
&
dto
.
ClaudeMediaMessage
{
Type
:
"tool_use"
,
Id
:
"toolu_1"
,
Name
:
"get_current_time"
,
},
}
stop
:=
&
dto
.
ClaudeResponse
{
Type
:
"content_block_stop"
,
Index
:
func
()
*
int
{
v
:=
1
;
return
&
v
}(),
}
startChunk
:=
StreamResponseClaude2OpenAI
(
start
,
claudeInfo
)
require
.
Nil
(
t
,
startChunk
)
stopChunk
:=
StreamResponseClaude2OpenAI
(
stop
,
claudeInfo
)
require
.
NotNil
(
t
,
stopChunk
)
require
.
Len
(
t
,
stopChunk
.
Choices
,
1
)
require
.
Len
(
t
,
stopChunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
,
1
)
assert
.
Equal
(
t
,
"toolu_1"
,
stopChunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
[
0
]
.
ID
)
assert
.
Equal
(
t
,
"get_current_time"
,
stopChunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
[
0
]
.
Function
.
Name
)
assert
.
Equal
(
t
,
"{}"
,
stopChunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
[
0
]
.
Function
.
Arguments
)
}
func
TestStreamResponseClaude2OpenAI_ArgToolKeepsIDNameOnDelta
(
t
*
testing
.
T
)
{
claudeInfo
:=
&
ClaudeResponseInfo
{}
start
:=
&
dto
.
ClaudeResponse
{
Type
:
"content_block_start"
,
Index
:
func
()
*
int
{
v
:=
1
;
return
&
v
}(),
ContentBlock
:
&
dto
.
ClaudeMediaMessage
{
Type
:
"tool_use"
,
Id
:
"toolu_2"
,
Name
:
"search_notes"
,
},
}
partial
:=
`{"query":"today"}`
delta
:=
&
dto
.
ClaudeResponse
{
Type
:
"content_block_delta"
,
Index
:
func
()
*
int
{
v
:=
1
;
return
&
v
}(),
Delta
:
&
dto
.
ClaudeMediaMessage
{
Type
:
"input_json_delta"
,
PartialJson
:
&
partial
,
},
}
startChunk
:=
StreamResponseClaude2OpenAI
(
start
,
claudeInfo
)
require
.
Nil
(
t
,
startChunk
)
deltaChunk
:=
StreamResponseClaude2OpenAI
(
delta
,
claudeInfo
)
require
.
NotNil
(
t
,
deltaChunk
)
require
.
Len
(
t
,
deltaChunk
.
Choices
,
1
)
require
.
Len
(
t
,
deltaChunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
,
1
)
assert
.
Equal
(
t
,
"toolu_2"
,
deltaChunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
[
0
]
.
ID
)
assert
.
Equal
(
t
,
"search_notes"
,
deltaChunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
[
0
]
.
Function
.
Name
)
assert
.
Equal
(
t
,
partial
,
deltaChunk
.
Choices
[
0
]
.
Delta
.
ToolCalls
[
0
]
.
Function
.
Arguments
)
}
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