diff --git a/pkg/providers/openai_compat_adapter.go b/pkg/providers/openai_compat_adapter.go index 9914c26..107fd1d 100644 --- a/pkg/providers/openai_compat_adapter.go +++ b/pkg/providers/openai_compat_adapter.go @@ -183,6 +183,7 @@ func (p *HTTPProvider) buildOpenAICompatChatRequest(messages []Message, tools [] func openAICompatMessages(messages []Message) []map[string]interface{} { out := make([]map[string]interface{}, 0, len(messages)) + pendingCalls := map[string]struct{}{} for _, msg := range messages { role := strings.ToLower(strings.TrimSpace(msg.Role)) content := openAICompatMessageContent(msg) @@ -199,6 +200,9 @@ func openAICompatMessages(messages []Message) []map[string]interface{} { if len(msg.ToolCalls) > 0 { toolCalls := make([]map[string]interface{}, 0, len(msg.ToolCalls)) for _, tc := range msg.ToolCalls { + if strings.TrimSpace(tc.ID) != "" { + pendingCalls[strings.TrimSpace(tc.ID)] = struct{}{} + } args := "" if tc.Function != nil { args = tc.Function.Arguments @@ -224,9 +228,17 @@ func openAICompatMessages(messages []Message) []map[string]interface{} { } out = append(out, item) case "tool": + callID := strings.TrimSpace(msg.ToolCallID) + if callID == "" { + continue + } + if _, ok := pendingCalls[callID]; !ok { + continue + } + delete(pendingCalls, callID) out = append(out, map[string]interface{}{ "role": "tool", - "tool_call_id": msg.ToolCallID, + "tool_call_id": callID, "content": content, }) default: diff --git a/pkg/providers/openai_compat_provider_test.go b/pkg/providers/openai_compat_provider_test.go index bba8220..e247e94 100644 --- a/pkg/providers/openai_compat_provider_test.go +++ b/pkg/providers/openai_compat_provider_test.go @@ -159,6 +159,37 @@ func TestOpenAICompatMessagesPreserveMultimodalContentParts(t *testing.T) { } } +func TestOpenAICompatMessagesDropsOrphanToolOutputs(t *testing.T) { + msgs := openAICompatMessages([]Message{ + {Role: "user", Content: "hi"}, + {Role: "tool", ToolCallID: "call-orphan", Content: "orphan output"}, + { + Role: "assistant", + ToolCalls: []ToolCall{{ + ID: "call-1", + Name: "read_file", + Function: &FunctionCall{ + Name: "read_file", + Arguments: `{"path":"README.md"}`, + }, + }}, + }, + {Role: "tool", ToolCallID: "call-1", Content: "file content"}, + }) + if len(msgs) != 3 { + t.Fatalf("messages = %#v, want 3 items", msgs) + } + if got := msgs[1]["role"]; got != "assistant" { + t.Fatalf("second role = %#v, want assistant", got) + } + if got := msgs[2]["role"]; got != "tool" { + t.Fatalf("third role = %#v, want tool", got) + } + if got := msgs[2]["tool_call_id"]; got != "call-1" { + t.Fatalf("tool_call_id = %#v, want call-1", got) + } +} + func TestBuildOpenAICompatChatRequestAppliesThinkingSuffix(t *testing.T) { base := NewHTTPProvider("openai", "token", "https://example.com/v1", "gpt-5", false, "api_key", 5*time.Second, nil) body := base.buildOpenAICompatChatRequest([]Message{{Role: "user", Content: "hi"}}, nil, "gpt-5(high)", nil)