mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-20 13:07:00 +08:00
trimReplayDanglingAssistantToolCalls only collected tool results that immediately followed the assistant tool-call message. Models such as gpt-5.3-codex-spark may emit the function_call item before the explanation text within one response, so history replay order becomes assistant[tool_call] -> assistant[text] -> tool[result]. The call was misjudged as dangling and stripped while the tool result survived, producing a function_call_output without a matching function_call that the Responses API rejects with 400. - widen the response collection window to skip interleaved plain assistant text messages, and drop orphan tool results in the same pass - synthesize a placeholder function_call (or drop the output when the tool name is unknown) in normalizeOpenAIResponsesInput so conversations already persisted with corrupted history can resume
89 lines
3.0 KiB
Go
89 lines
3.0 KiB
Go
package modeladapter
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func TestToolImageProviderEncodings(t *testing.T) {
|
||
message := toolImageMessageForTest()
|
||
|
||
t.Run("openai_chat", func(t *testing.T) {
|
||
items, err := normalizeOpenAIProviderMessages([]Message{message}, false)
|
||
if err != nil {
|
||
t.Fatalf("normalizeOpenAIProviderMessages() error = %v", err)
|
||
}
|
||
if len(items) != 1 || items[0]["role"] != "tool" || items[0]["tool_call_id"] != "call-1" {
|
||
t.Fatalf("openai chat tool message = %#v", items)
|
||
}
|
||
content, ok := items[0]["content"].([]map[string]any)
|
||
if !ok || len(content) != 2 {
|
||
t.Fatalf("openai chat content = %#v", items[0]["content"])
|
||
}
|
||
imageURL, ok := content[1]["image_url"].(map[string]any)
|
||
if content[1]["type"] != "image_url" || !ok || !strings.HasPrefix(imageURL["url"].(string), "data:image/png;base64,") {
|
||
t.Fatalf("openai chat image part = %#v", content[1])
|
||
}
|
||
})
|
||
|
||
t.Run("openai_responses", func(t *testing.T) {
|
||
_, items, err := normalizeOpenAIResponsesInput([]Message{message})
|
||
if err != nil {
|
||
t.Fatalf("normalizeOpenAIResponsesInput() error = %v", err)
|
||
}
|
||
// 孤儿 tool 结果(无前置 assistant 调用)会补一个占位 function_call,
|
||
// 保证每个 function_call_output 都有配对调用。
|
||
if len(items) != 2 || items[0]["type"] != "function_call" || items[0]["call_id"] != "call-1" || items[1]["type"] != "function_call_output" {
|
||
t.Fatalf("openai responses items = %#v", items)
|
||
}
|
||
content, ok := items[1]["output"].([]map[string]any)
|
||
if !ok || len(content) != 2 {
|
||
t.Fatalf("openai responses output = %#v", items[1]["output"])
|
||
}
|
||
if content[0]["type"] != "input_text" || content[1]["type"] != "input_image" {
|
||
t.Fatalf("openai responses content = %#v", content)
|
||
}
|
||
})
|
||
|
||
t.Run("anthropic", func(t *testing.T) {
|
||
_, messages, err := normalizeAnthropicProviderMessages([]Message{message}, false, false)
|
||
if err != nil {
|
||
t.Fatalf("normalizeAnthropicProviderMessages() error = %v", err)
|
||
}
|
||
if len(messages) != 1 || messages[0].Role != "user" || len(messages[0].Content) != 1 {
|
||
t.Fatalf("anthropic messages = %#v", messages)
|
||
}
|
||
toolResult := messages[0].Content[0]
|
||
if toolResult["type"] != "tool_result" || toolResult["tool_use_id"] != "call-1" {
|
||
t.Fatalf("anthropic tool result = %#v", toolResult)
|
||
}
|
||
content, ok := toolResult["content"].([]map[string]any)
|
||
if !ok || len(content) != 2 {
|
||
t.Fatalf("anthropic tool content = %#v", toolResult["content"])
|
||
}
|
||
if content[0]["type"] != "text" || content[1]["type"] != "image" {
|
||
t.Fatalf("anthropic content blocks = %#v", content)
|
||
}
|
||
})
|
||
}
|
||
|
||
func toolImageMessageForTest() Message {
|
||
return Message{
|
||
Role: "tool",
|
||
Content: "read binary bytes=16",
|
||
ToolCallID: "call-1",
|
||
Name: "Read",
|
||
ContentParts: []ContentPart{
|
||
{Type: "text", Text: "read binary bytes=16"},
|
||
{
|
||
Type: "image",
|
||
Image: &ImageContent{
|
||
MIMEType: "image/png",
|
||
Path: "diagram.png",
|
||
Data: []byte("\x89PNG\r\n\x1a\nimage"),
|
||
},
|
||
},
|
||
},
|
||
}
|
||
}
|