mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-27 05:15:48 +00:00
441 lines
14 KiB
Go
441 lines
14 KiB
Go
package openai
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
// TestToolResultsAttachedToCurrentMessage verifies that tool results from "tool" role messages
|
|
// are properly attached to the current user message (the last message in the conversation).
|
|
// This is critical for LiteLLM-translated requests where tool results appear as separate messages.
|
|
func TestToolResultsAttachedToCurrentMessage(t *testing.T) {
|
|
// OpenAI format request simulating LiteLLM's translation from Anthropic format
|
|
// Sequence: user -> assistant (with tool_calls) -> tool (result) -> user
|
|
// The last user message should have the tool results attached
|
|
input := []byte(`{
|
|
"model": "kiro-claude-opus-4-5-agentic",
|
|
"messages": [
|
|
{"role": "user", "content": "Hello, can you read a file for me?"},
|
|
{
|
|
"role": "assistant",
|
|
"content": "I'll read that file for you.",
|
|
"tool_calls": [
|
|
{
|
|
"id": "call_abc123",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "Read",
|
|
"arguments": "{\"file_path\": \"/tmp/test.txt\"}"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"role": "tool",
|
|
"tool_call_id": "call_abc123",
|
|
"content": "File contents: Hello World!"
|
|
},
|
|
{"role": "user", "content": "What did the file say?"}
|
|
]
|
|
}`)
|
|
|
|
result, _ := BuildKiroPayloadFromOpenAI(input, "kiro-model", "", "CLI", false, false, nil, nil)
|
|
|
|
var payload KiroPayload
|
|
if err := json.Unmarshal(result, &payload); err != nil {
|
|
t.Fatalf("Failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
// The last user message becomes currentMessage
|
|
// History should have: user (first), assistant (with tool_calls)
|
|
t.Logf("History count: %d", len(payload.ConversationState.History))
|
|
if len(payload.ConversationState.History) != 2 {
|
|
t.Errorf("Expected 2 history entries (user + assistant), got %d", len(payload.ConversationState.History))
|
|
}
|
|
|
|
// Tool results should be attached to currentMessage (the last user message)
|
|
ctx := payload.ConversationState.CurrentMessage.UserInputMessage.UserInputMessageContext
|
|
if ctx == nil {
|
|
t.Fatal("Expected currentMessage to have UserInputMessageContext with tool results")
|
|
}
|
|
|
|
if len(ctx.ToolResults) != 1 {
|
|
t.Fatalf("Expected 1 tool result in currentMessage, got %d", len(ctx.ToolResults))
|
|
}
|
|
|
|
tr := ctx.ToolResults[0]
|
|
if tr.ToolUseID != "call_abc123" {
|
|
t.Errorf("Expected toolUseId 'call_abc123', got '%s'", tr.ToolUseID)
|
|
}
|
|
if len(tr.Content) == 0 || tr.Content[0].Text != "File contents: Hello World!" {
|
|
t.Errorf("Tool result content mismatch, got: %+v", tr.Content)
|
|
}
|
|
}
|
|
|
|
// TestToolResultsInHistoryUserMessage verifies that when there are multiple user messages
|
|
// after tool results, the tool results are attached to the correct user message in history.
|
|
func TestToolResultsInHistoryUserMessage(t *testing.T) {
|
|
// Sequence: user -> assistant (with tool_calls) -> tool (result) -> user -> assistant -> user
|
|
// The first user after tool should have tool results in history
|
|
input := []byte(`{
|
|
"model": "kiro-claude-opus-4-5-agentic",
|
|
"messages": [
|
|
{"role": "user", "content": "Hello"},
|
|
{
|
|
"role": "assistant",
|
|
"content": "I'll read the file.",
|
|
"tool_calls": [
|
|
{
|
|
"id": "call_1",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "Read",
|
|
"arguments": "{}"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"role": "tool",
|
|
"tool_call_id": "call_1",
|
|
"content": "File result"
|
|
},
|
|
{"role": "user", "content": "Thanks for the file"},
|
|
{"role": "assistant", "content": "You're welcome"},
|
|
{"role": "user", "content": "Bye"}
|
|
]
|
|
}`)
|
|
|
|
result, _ := BuildKiroPayloadFromOpenAI(input, "kiro-model", "", "CLI", false, false, nil, nil)
|
|
|
|
var payload KiroPayload
|
|
if err := json.Unmarshal(result, &payload); err != nil {
|
|
t.Fatalf("Failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
// History should have: user, assistant, user (with tool results), assistant
|
|
// CurrentMessage should be: last user "Bye"
|
|
t.Logf("History count: %d", len(payload.ConversationState.History))
|
|
|
|
// Find the user message in history with tool results
|
|
foundToolResults := false
|
|
for i, h := range payload.ConversationState.History {
|
|
if h.UserInputMessage != nil {
|
|
t.Logf("History[%d]: user message content=%q", i, h.UserInputMessage.Content)
|
|
if h.UserInputMessage.UserInputMessageContext != nil {
|
|
if len(h.UserInputMessage.UserInputMessageContext.ToolResults) > 0 {
|
|
foundToolResults = true
|
|
t.Logf(" Found %d tool results", len(h.UserInputMessage.UserInputMessageContext.ToolResults))
|
|
tr := h.UserInputMessage.UserInputMessageContext.ToolResults[0]
|
|
if tr.ToolUseID != "call_1" {
|
|
t.Errorf("Expected toolUseId 'call_1', got '%s'", tr.ToolUseID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if h.AssistantResponseMessage != nil {
|
|
t.Logf("History[%d]: assistant message content=%q", i, h.AssistantResponseMessage.Content)
|
|
}
|
|
}
|
|
|
|
if !foundToolResults {
|
|
t.Error("Tool results were not attached to any user message in history")
|
|
}
|
|
}
|
|
|
|
// TestToolResultsWithMultipleToolCalls verifies handling of multiple tool calls
|
|
func TestToolResultsWithMultipleToolCalls(t *testing.T) {
|
|
input := []byte(`{
|
|
"model": "kiro-claude-opus-4-5-agentic",
|
|
"messages": [
|
|
{"role": "user", "content": "Read two files for me"},
|
|
{
|
|
"role": "assistant",
|
|
"content": "I'll read both files.",
|
|
"tool_calls": [
|
|
{
|
|
"id": "call_1",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "Read",
|
|
"arguments": "{\"file_path\": \"/tmp/file1.txt\"}"
|
|
}
|
|
},
|
|
{
|
|
"id": "call_2",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "Read",
|
|
"arguments": "{\"file_path\": \"/tmp/file2.txt\"}"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"role": "tool",
|
|
"tool_call_id": "call_1",
|
|
"content": "Content of file 1"
|
|
},
|
|
{
|
|
"role": "tool",
|
|
"tool_call_id": "call_2",
|
|
"content": "Content of file 2"
|
|
},
|
|
{"role": "user", "content": "What do they say?"}
|
|
]
|
|
}`)
|
|
|
|
result, _ := BuildKiroPayloadFromOpenAI(input, "kiro-model", "", "CLI", false, false, nil, nil)
|
|
|
|
var payload KiroPayload
|
|
if err := json.Unmarshal(result, &payload); err != nil {
|
|
t.Fatalf("Failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
t.Logf("History count: %d", len(payload.ConversationState.History))
|
|
t.Logf("CurrentMessage content: %q", payload.ConversationState.CurrentMessage.UserInputMessage.Content)
|
|
|
|
// Check if there are any tool results anywhere
|
|
var totalToolResults int
|
|
for i, h := range payload.ConversationState.History {
|
|
if h.UserInputMessage != nil && h.UserInputMessage.UserInputMessageContext != nil {
|
|
count := len(h.UserInputMessage.UserInputMessageContext.ToolResults)
|
|
t.Logf("History[%d] user message has %d tool results", i, count)
|
|
totalToolResults += count
|
|
}
|
|
}
|
|
|
|
ctx := payload.ConversationState.CurrentMessage.UserInputMessage.UserInputMessageContext
|
|
if ctx != nil {
|
|
t.Logf("CurrentMessage has %d tool results", len(ctx.ToolResults))
|
|
totalToolResults += len(ctx.ToolResults)
|
|
} else {
|
|
t.Logf("CurrentMessage has no UserInputMessageContext")
|
|
}
|
|
|
|
if totalToolResults != 2 {
|
|
t.Errorf("Expected 2 tool results total, got %d", totalToolResults)
|
|
}
|
|
}
|
|
|
|
// TestToolResultsAtEndOfConversation verifies tool results are handled when
|
|
// the conversation ends with tool results (no following user message)
|
|
func TestToolResultsAtEndOfConversation(t *testing.T) {
|
|
input := []byte(`{
|
|
"model": "kiro-claude-opus-4-5-agentic",
|
|
"messages": [
|
|
{"role": "user", "content": "Read a file"},
|
|
{
|
|
"role": "assistant",
|
|
"content": "Reading the file.",
|
|
"tool_calls": [
|
|
{
|
|
"id": "call_end",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "Read",
|
|
"arguments": "{\"file_path\": \"/tmp/test.txt\"}"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"role": "tool",
|
|
"tool_call_id": "call_end",
|
|
"content": "File contents here"
|
|
}
|
|
]
|
|
}`)
|
|
|
|
result, _ := BuildKiroPayloadFromOpenAI(input, "kiro-model", "", "CLI", false, false, nil, nil)
|
|
|
|
var payload KiroPayload
|
|
if err := json.Unmarshal(result, &payload); err != nil {
|
|
t.Fatalf("Failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
// When the last message is a tool result, a synthetic user message is created
|
|
// and tool results should be attached to it
|
|
ctx := payload.ConversationState.CurrentMessage.UserInputMessage.UserInputMessageContext
|
|
if ctx == nil || len(ctx.ToolResults) == 0 {
|
|
t.Error("Expected tool results to be attached to current message when conversation ends with tool result")
|
|
} else {
|
|
if ctx.ToolResults[0].ToolUseID != "call_end" {
|
|
t.Errorf("Expected toolUseId 'call_end', got '%s'", ctx.ToolResults[0].ToolUseID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestToolResultsFollowedByAssistant verifies handling when tool results are followed
|
|
// by an assistant message (no intermediate user message).
|
|
// This is the pattern from LiteLLM translation of Anthropic format where:
|
|
// user message has ONLY tool_result blocks -> LiteLLM creates tool messages
|
|
// then the next message is assistant
|
|
func TestToolResultsFollowedByAssistant(t *testing.T) {
|
|
// Sequence: user -> assistant (with tool_calls) -> tool -> tool -> assistant -> user
|
|
// This simulates LiteLLM's translation of:
|
|
// user: "Read files"
|
|
// assistant: [tool_use, tool_use]
|
|
// user: [tool_result, tool_result] <- becomes multiple "tool" role messages
|
|
// assistant: "I've read them"
|
|
// user: "What did they say?"
|
|
input := []byte(`{
|
|
"model": "kiro-claude-opus-4-5-agentic",
|
|
"messages": [
|
|
{"role": "user", "content": "Read two files for me"},
|
|
{
|
|
"role": "assistant",
|
|
"content": "I'll read both files.",
|
|
"tool_calls": [
|
|
{
|
|
"id": "call_1",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "Read",
|
|
"arguments": "{\"file_path\": \"/tmp/a.txt\"}"
|
|
}
|
|
},
|
|
{
|
|
"id": "call_2",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "Read",
|
|
"arguments": "{\"file_path\": \"/tmp/b.txt\"}"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"role": "tool",
|
|
"tool_call_id": "call_1",
|
|
"content": "Contents of file A"
|
|
},
|
|
{
|
|
"role": "tool",
|
|
"tool_call_id": "call_2",
|
|
"content": "Contents of file B"
|
|
},
|
|
{
|
|
"role": "assistant",
|
|
"content": "I've read both files."
|
|
},
|
|
{"role": "user", "content": "What did they say?"}
|
|
]
|
|
}`)
|
|
|
|
result, _ := BuildKiroPayloadFromOpenAI(input, "kiro-model", "", "CLI", false, false, nil, nil)
|
|
|
|
var payload KiroPayload
|
|
if err := json.Unmarshal(result, &payload); err != nil {
|
|
t.Fatalf("Failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
t.Logf("History count: %d", len(payload.ConversationState.History))
|
|
|
|
// Tool results should be attached to a synthetic user message or the history should be valid
|
|
var totalToolResults int
|
|
for i, h := range payload.ConversationState.History {
|
|
if h.UserInputMessage != nil {
|
|
t.Logf("History[%d]: user message content=%q", i, h.UserInputMessage.Content)
|
|
if h.UserInputMessage.UserInputMessageContext != nil {
|
|
count := len(h.UserInputMessage.UserInputMessageContext.ToolResults)
|
|
t.Logf(" Has %d tool results", count)
|
|
totalToolResults += count
|
|
}
|
|
}
|
|
if h.AssistantResponseMessage != nil {
|
|
t.Logf("History[%d]: assistant message content=%q", i, h.AssistantResponseMessage.Content)
|
|
}
|
|
}
|
|
|
|
ctx := payload.ConversationState.CurrentMessage.UserInputMessage.UserInputMessageContext
|
|
if ctx != nil {
|
|
t.Logf("CurrentMessage has %d tool results", len(ctx.ToolResults))
|
|
totalToolResults += len(ctx.ToolResults)
|
|
}
|
|
|
|
if totalToolResults != 2 {
|
|
t.Errorf("Expected 2 tool results total, got %d", totalToolResults)
|
|
}
|
|
}
|
|
|
|
// TestAssistantEndsConversation verifies handling when assistant is the last message
|
|
func TestAssistantEndsConversation(t *testing.T) {
|
|
input := []byte(`{
|
|
"model": "kiro-claude-opus-4-5-agentic",
|
|
"messages": [
|
|
{"role": "user", "content": "Hello"},
|
|
{
|
|
"role": "assistant",
|
|
"content": "Hi there!"
|
|
}
|
|
]
|
|
}`)
|
|
|
|
result, _ := BuildKiroPayloadFromOpenAI(input, "kiro-model", "", "CLI", false, false, nil, nil)
|
|
|
|
var payload KiroPayload
|
|
if err := json.Unmarshal(result, &payload); err != nil {
|
|
t.Fatalf("Failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
// When assistant is last, a "Continue" user message should be created
|
|
if payload.ConversationState.CurrentMessage.UserInputMessage.Content == "" {
|
|
t.Error("Expected a 'Continue' message to be created when assistant is last")
|
|
}
|
|
}
|
|
|
|
func TestFilterOrphanedToolResults_RemovesHistoryAndCurrentOrphans(t *testing.T) {
|
|
history := []KiroHistoryMessage{
|
|
{
|
|
AssistantResponseMessage: &KiroAssistantResponseMessage{
|
|
Content: "assistant",
|
|
ToolUses: []KiroToolUse{
|
|
{ToolUseID: "keep-1", Name: "Read", Input: map[string]interface{}{}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
UserInputMessage: &KiroUserInputMessage{
|
|
Content: "user-with-mixed-results",
|
|
UserInputMessageContext: &KiroUserInputMessageContext{
|
|
ToolResults: []KiroToolResult{
|
|
{ToolUseID: "keep-1", Status: "success", Content: []KiroTextContent{{Text: "ok"}}},
|
|
{ToolUseID: "orphan-1", Status: "success", Content: []KiroTextContent{{Text: "bad"}}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
UserInputMessage: &KiroUserInputMessage{
|
|
Content: "user-only-orphans",
|
|
UserInputMessageContext: &KiroUserInputMessageContext{
|
|
ToolResults: []KiroToolResult{
|
|
{ToolUseID: "orphan-2", Status: "success", Content: []KiroTextContent{{Text: "bad"}}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
currentToolResults := []KiroToolResult{
|
|
{ToolUseID: "keep-1", Status: "success", Content: []KiroTextContent{{Text: "ok"}}},
|
|
{ToolUseID: "orphan-3", Status: "success", Content: []KiroTextContent{{Text: "bad"}}},
|
|
}
|
|
|
|
filteredHistory, filteredCurrent := filterOrphanedToolResults(history, currentToolResults)
|
|
|
|
ctx1 := filteredHistory[1].UserInputMessage.UserInputMessageContext
|
|
if ctx1 == nil || len(ctx1.ToolResults) != 1 || ctx1.ToolResults[0].ToolUseID != "keep-1" {
|
|
t.Fatalf("expected mixed history message to keep only keep-1, got: %+v", ctx1)
|
|
}
|
|
|
|
if filteredHistory[2].UserInputMessage.UserInputMessageContext != nil {
|
|
t.Fatalf("expected orphan-only history context to be removed")
|
|
}
|
|
|
|
if len(filteredCurrent) != 1 || filteredCurrent[0].ToolUseID != "keep-1" {
|
|
t.Fatalf("expected current tool results to keep only keep-1, got: %+v", filteredCurrent)
|
|
}
|
|
}
|