mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
fix(translator): emit tool_result messages before user content in Claude-to-OpenAI conversion
This commit is contained in:
@@ -181,6 +181,14 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
hasContent := len(contentItems) > 0
|
hasContent := len(contentItems) > 0
|
||||||
hasReasoning := reasoningContent != ""
|
hasReasoning := reasoningContent != ""
|
||||||
hasToolCalls := len(toolCalls) > 0
|
hasToolCalls := len(toolCalls) > 0
|
||||||
|
hasToolResults := len(toolResults) > 0
|
||||||
|
|
||||||
|
// OpenAI requires: tool messages MUST immediately follow the assistant message with tool_calls.
|
||||||
|
// Therefore, we emit tool_result messages FIRST (they respond to the previous assistant's tool_calls),
|
||||||
|
// then emit the current message's content.
|
||||||
|
for _, toolResultJSON := range toolResults {
|
||||||
|
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(toolResultJSON).Value())
|
||||||
|
}
|
||||||
|
|
||||||
// For assistant messages: emit a single unified message with content, tool_calls, and reasoning_content
|
// For assistant messages: emit a single unified message with content, tool_calls, and reasoning_content
|
||||||
// This avoids splitting into multiple assistant messages which breaks OpenAI tool-call adjacency
|
// This avoids splitting into multiple assistant messages which breaks OpenAI tool-call adjacency
|
||||||
@@ -214,6 +222,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For non-assistant roles: emit content message if we have content
|
// For non-assistant roles: emit content message if we have content
|
||||||
|
// If the message only contains tool_results (no text/image), we still processed them above
|
||||||
if hasContent {
|
if hasContent {
|
||||||
msgJSON := `{"role":""}`
|
msgJSON := `{"role":""}`
|
||||||
msgJSON, _ = sjson.Set(msgJSON, "role", role)
|
msgJSON, _ = sjson.Set(msgJSON, "role", role)
|
||||||
@@ -225,14 +234,11 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
msgJSON, _ = sjson.SetRaw(msgJSON, "content", contentArrayJSON)
|
msgJSON, _ = sjson.SetRaw(msgJSON, "content", contentArrayJSON)
|
||||||
|
|
||||||
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(msgJSON).Value())
|
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(msgJSON).Value())
|
||||||
|
} else if hasToolResults && !hasContent {
|
||||||
|
// tool_results already emitted above, no additional user message needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit tool_result messages after the main message (ensures proper OpenAI ordering)
|
|
||||||
for _, toolResultJSON := range toolResults {
|
|
||||||
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(toolResultJSON).Value())
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if contentResult.Exists() && contentResult.Type == gjson.String {
|
} else if contentResult.Exists() && contentResult.Type == gjson.String {
|
||||||
// Simple string content
|
// Simple string content
|
||||||
msgJSON := `{"role":"","content":""}`
|
msgJSON := `{"role":"","content":""}`
|
||||||
|
|||||||
@@ -317,8 +317,8 @@ func TestConvertClaudeRequestToOpenAI_ToolResultOrderAndContent(t *testing.T) {
|
|||||||
resultJSON := gjson.ParseBytes(result)
|
resultJSON := gjson.ParseBytes(result)
|
||||||
messages := resultJSON.Get("messages").Array()
|
messages := resultJSON.Get("messages").Array()
|
||||||
|
|
||||||
// New behavior: user text is combined, tool_result emitted after user message
|
// OpenAI requires: tool messages MUST immediately follow assistant(tool_calls).
|
||||||
// Expect: system + assistant(tool_calls) + user(before+after) + tool(result)
|
// Correct order: system + assistant(tool_calls) + tool(result) + user(before+after)
|
||||||
if len(messages) != 4 {
|
if len(messages) != 4 {
|
||||||
t.Fatalf("Expected 4 messages, got %d. Messages: %s", len(messages), resultJSON.Get("messages").Raw)
|
t.Fatalf("Expected 4 messages, got %d. Messages: %s", len(messages), resultJSON.Get("messages").Raw)
|
||||||
}
|
}
|
||||||
@@ -331,26 +331,28 @@ func TestConvertClaudeRequestToOpenAI_ToolResultOrderAndContent(t *testing.T) {
|
|||||||
t.Fatalf("Expected messages[1] to be assistant tool_calls, got %s: %s", messages[1].Get("role").String(), messages[1].Raw)
|
t.Fatalf("Expected messages[1] to be assistant tool_calls, got %s: %s", messages[1].Get("role").String(), messages[1].Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
if messages[2].Get("role").String() != "user" {
|
// tool message MUST immediately follow assistant(tool_calls) per OpenAI spec
|
||||||
t.Fatalf("Expected messages[2] to be user, got %s", messages[2].Get("role").String())
|
if messages[2].Get("role").String() != "tool" {
|
||||||
|
t.Fatalf("Expected messages[2] to be tool (must follow tool_calls), got %s", messages[2].Get("role").String())
|
||||||
}
|
}
|
||||||
// User message should contain both "before" and "after" text
|
if got := messages[2].Get("tool_call_id").String(); got != "call_1" {
|
||||||
if got := messages[2].Get("content.0.text").String(); got != "before" {
|
|
||||||
t.Fatalf("Expected user text[0] %q, got %q", "before", got)
|
|
||||||
}
|
|
||||||
if got := messages[2].Get("content.1.text").String(); got != "after" {
|
|
||||||
t.Fatalf("Expected user text[1] %q, got %q", "after", got)
|
|
||||||
}
|
|
||||||
|
|
||||||
if messages[3].Get("role").String() != "tool" {
|
|
||||||
t.Fatalf("Expected messages[3] to be tool, got %s", messages[3].Get("role").String())
|
|
||||||
}
|
|
||||||
if got := messages[3].Get("tool_call_id").String(); got != "call_1" {
|
|
||||||
t.Fatalf("Expected tool_call_id %q, got %q", "call_1", got)
|
t.Fatalf("Expected tool_call_id %q, got %q", "call_1", got)
|
||||||
}
|
}
|
||||||
if got := messages[3].Get("content").String(); got != "tool ok" {
|
if got := messages[2].Get("content").String(); got != "tool ok" {
|
||||||
t.Fatalf("Expected tool content %q, got %q", "tool ok", got)
|
t.Fatalf("Expected tool content %q, got %q", "tool ok", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User message comes after tool message
|
||||||
|
if messages[3].Get("role").String() != "user" {
|
||||||
|
t.Fatalf("Expected messages[3] to be user, got %s", messages[3].Get("role").String())
|
||||||
|
}
|
||||||
|
// User message should contain both "before" and "after" text
|
||||||
|
if got := messages[3].Get("content.0.text").String(); got != "before" {
|
||||||
|
t.Fatalf("Expected user text[0] %q, got %q", "before", got)
|
||||||
|
}
|
||||||
|
if got := messages[3].Get("content.1.text").String(); got != "after" {
|
||||||
|
t.Fatalf("Expected user text[1] %q, got %q", "after", got)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertClaudeRequestToOpenAI_ToolResultObjectContent(t *testing.T) {
|
func TestConvertClaudeRequestToOpenAI_ToolResultObjectContent(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user