mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-09 18:06:50 +08:00
feat(translator): ensure tool uses stay adjacent to tool results in message generation
- Refactored `ConvertOpenAIResponsesRequestToClaude` logic to align tool use with corresponding tool results. - Introduced helper functions for appending and flushing pending reasoning and tool use messages. - Expanded tests to validate message order and content consistency when processing tool calls and results.
This commit is contained in:
@@ -170,6 +170,14 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
||||
|
||||
// input array processing
|
||||
var pendingReasoningParts []string
|
||||
type pendingToolUseMessage struct {
|
||||
callID string
|
||||
raw []byte
|
||||
}
|
||||
var pendingToolUseMessages []pendingToolUseMessage
|
||||
appendMessage := func(msg []byte) {
|
||||
out, _ = sjson.SetRawBytes(out, "messages.-1", msg)
|
||||
}
|
||||
flushPendingReasoning := func() {
|
||||
if len(pendingReasoningParts) == 0 {
|
||||
return
|
||||
@@ -178,9 +186,28 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
||||
for _, partJSON := range pendingReasoningParts {
|
||||
asst, _ = sjson.SetRawBytes(asst, "content.-1", []byte(partJSON))
|
||||
}
|
||||
out, _ = sjson.SetRawBytes(out, "messages.-1", asst)
|
||||
appendMessage(asst)
|
||||
pendingReasoningParts = nil
|
||||
}
|
||||
flushPendingToolUses := func() {
|
||||
for _, pending := range pendingToolUseMessages {
|
||||
appendMessage(pending.raw)
|
||||
}
|
||||
pendingToolUseMessages = nil
|
||||
}
|
||||
flushPendingToolUseFor := func(callID string) {
|
||||
if len(pendingToolUseMessages) == 0 {
|
||||
return
|
||||
}
|
||||
for i, pending := range pendingToolUseMessages {
|
||||
if pending.callID == callID {
|
||||
appendMessage(pending.raw)
|
||||
pendingToolUseMessages = append(pendingToolUseMessages[:i], pendingToolUseMessages[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
flushPendingToolUses()
|
||||
}
|
||||
|
||||
if input := root.Get("input"); input.Exists() && input.IsArray() {
|
||||
input.ForEach(func(_, item gjson.Result) bool {
|
||||
@@ -294,6 +321,9 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
||||
}
|
||||
|
||||
hasReasoningParts := false
|
||||
if role != "assistant" {
|
||||
flushPendingToolUses()
|
||||
}
|
||||
if len(pendingReasoningParts) > 0 {
|
||||
if role == "assistant" {
|
||||
if len(partsJSON) == 0 && textAggregate.Len() > 0 {
|
||||
@@ -322,12 +352,12 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
||||
msg, _ = sjson.SetRawBytes(msg, "content.-1", []byte(partJSON))
|
||||
}
|
||||
}
|
||||
out, _ = sjson.SetRawBytes(out, "messages.-1", msg)
|
||||
appendMessage(msg)
|
||||
} else if textAggregate.Len() > 0 || role == "system" {
|
||||
msg := []byte(`{"role":"","content":""}`)
|
||||
msg, _ = sjson.SetBytes(msg, "role", role)
|
||||
msg, _ = sjson.SetBytes(msg, "content", textAggregate.String())
|
||||
out, _ = sjson.SetRawBytes(out, "messages.-1", msg)
|
||||
appendMessage(msg)
|
||||
}
|
||||
|
||||
case "reasoning":
|
||||
@@ -360,12 +390,16 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
||||
}
|
||||
pendingReasoningParts = nil
|
||||
asst, _ = sjson.SetRawBytes(asst, "content.-1", toolUse)
|
||||
out, _ = sjson.SetRawBytes(out, "messages.-1", asst)
|
||||
pendingToolUseMessages = append(pendingToolUseMessages, pendingToolUseMessage{
|
||||
callID: callID,
|
||||
raw: asst,
|
||||
})
|
||||
|
||||
case "function_call_output":
|
||||
flushPendingReasoning()
|
||||
// Map to user tool_result
|
||||
callID := item.Get("call_id").String()
|
||||
flushPendingToolUseFor(callID)
|
||||
outputStr := item.Get("output").String()
|
||||
toolResult := []byte(`{"type":"tool_result","tool_use_id":"","content":""}`)
|
||||
toolResult, _ = sjson.SetBytes(toolResult, "tool_use_id", callID)
|
||||
@@ -373,12 +407,13 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
||||
|
||||
usr := []byte(`{"role":"user","content":[]}`)
|
||||
usr, _ = sjson.SetRawBytes(usr, "content.-1", toolResult)
|
||||
out, _ = sjson.SetRawBytes(out, "messages.-1", usr)
|
||||
appendMessage(usr)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
flushPendingReasoning()
|
||||
flushPendingToolUses()
|
||||
|
||||
includedToolNames := map[string]struct{}{}
|
||||
toolNameMap := map[string]string{}
|
||||
|
||||
@@ -125,6 +125,52 @@ func TestConvertOpenAIResponsesRequestToClaude_DropsIncompatibleReasoningSignatu
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToClaude_KeepsToolUseAdjacentToToolResult(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"model":"claude-test",
|
||||
"input":[
|
||||
{
|
||||
"type":"function_call",
|
||||
"call_id":"call_00_awGuheXs4aRbtedNK8LE3743",
|
||||
"name":"js",
|
||||
"arguments":"{\"code\":\"nodeRepl.write('ok')\",\"title\":\"List Obsidian vault contents\"}"
|
||||
},
|
||||
{
|
||||
"type":"message",
|
||||
"role":"assistant",
|
||||
"content":[{"type":"output_text","text":"I'll check your Obsidian vault for articles."}]
|
||||
},
|
||||
{
|
||||
"type":"function_call_output",
|
||||
"call_id":"call_00_awGuheXs4aRbtedNK8LE3743",
|
||||
"output":"Wall time: 0.1963 seconds\nOutput:\n[{\"type\":\"text\",\"text\":\"\"}]"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
out := ConvertOpenAIResponsesRequestToClaude("claude-test", raw, false)
|
||||
root := gjson.ParseBytes(out)
|
||||
|
||||
if got := root.Get("messages.0.role").String(); got != "assistant" {
|
||||
t.Fatalf("first message role = %q, want assistant. Output: %s", got, string(out))
|
||||
}
|
||||
if got := root.Get("messages.0.content").String(); got != "I'll check your Obsidian vault for articles." {
|
||||
t.Fatalf("first message content = %q, want assistant text. Output: %s", got, string(out))
|
||||
}
|
||||
if got := root.Get("messages.1.content.0.type").String(); got != "tool_use" {
|
||||
t.Fatalf("second message first content type = %q, want tool_use. Output: %s", got, string(out))
|
||||
}
|
||||
if got := root.Get("messages.1.content.0.id").String(); got != "call_00_awGuheXs4aRbtedNK8LE3743" {
|
||||
t.Fatalf("tool_use id = %q, want call_00_awGuheXs4aRbtedNK8LE3743. Output: %s", got, string(out))
|
||||
}
|
||||
if got := root.Get("messages.2.content.0.type").String(); got != "tool_result" {
|
||||
t.Fatalf("third message first content type = %q, want tool_result. Output: %s", got, string(out))
|
||||
}
|
||||
if got := root.Get("messages.2.content.0.tool_use_id").String(); got != "call_00_awGuheXs4aRbtedNK8LE3743" {
|
||||
t.Fatalf("tool_result id = %q, want call_00_awGuheXs4aRbtedNK8LE3743. Output: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func testClaudeResponsesThinkingSignature(t *testing.T) (string, string) {
|
||||
t.Helper()
|
||||
channelBlock := []byte{}
|
||||
|
||||
Reference in New Issue
Block a user