From 4e995252796f0bd061d323b38e27fe20d2a5f612 Mon Sep 17 00:00:00 2001 From: stefanet Date: Mon, 2 Mar 2026 16:41:29 +0100 Subject: [PATCH] github copilot - update x-initiator header rules --- .../executor/github_copilot_executor.go | 46 +++++++++++++++---- .../executor/github_copilot_executor_test.go | 41 +++++++++++++++-- 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/internal/runtime/executor/github_copilot_executor.go b/internal/runtime/executor/github_copilot_executor.go index af4b7e6a..d22c2e7e 100644 --- a/internal/runtime/executor/github_copilot_executor.go +++ b/internal/runtime/executor/github_copilot_executor.go @@ -490,18 +490,46 @@ func (e *GitHubCopilotExecutor) applyHeaders(r *http.Request, apiToken string, b r.Header.Set("X-Request-Id", uuid.NewString()) initiator := "user" - if len(body) > 0 { - if messages := gjson.GetBytes(body, "messages"); messages.Exists() && messages.IsArray() { - for _, msg := range messages.Array() { - role := msg.Get("role").String() - if role == "assistant" || role == "tool" { - initiator = "agent" - break - } + if role := detectLastConversationRole(body); role == "assistant" || role == "tool" { + initiator = "agent" + } + r.Header.Set("X-Initiator", initiator) +} + +func detectLastConversationRole(body []byte) string { + if len(body) == 0 { + return "" + } + + if messages := gjson.GetBytes(body, "messages"); messages.Exists() && messages.IsArray() { + arr := messages.Array() + for i := len(arr) - 1; i >= 0; i-- { + if role := arr[i].Get("role").String(); role != "" { + return role } } } - r.Header.Set("X-Initiator", initiator) + + if inputs := gjson.GetBytes(body, "input"); inputs.Exists() && inputs.IsArray() { + arr := inputs.Array() + for i := len(arr) - 1; i >= 0; i-- { + item := arr[i] + + // Most Responses input items carry a top-level role. + if role := item.Get("role").String(); role != "" { + return role + } + + switch item.Get("type").String() { + case "function_call", "function_call_arguments": + return "assistant" + case "function_call_output", "function_call_response", "tool_result": + return "tool" + } + } + } + + return "" } // detectVisionContent checks if the request body contains vision/image content. diff --git a/internal/runtime/executor/github_copilot_executor_test.go b/internal/runtime/executor/github_copilot_executor_test.go index 39868ef7..1055eab6 100644 --- a/internal/runtime/executor/github_copilot_executor_test.go +++ b/internal/runtime/executor/github_copilot_executor_test.go @@ -262,15 +262,15 @@ func TestApplyHeaders_XInitiator_UserOnly(t *testing.T) { } } -func TestApplyHeaders_XInitiator_AgentWithAssistantAndUserToolResult(t *testing.T) { +func TestApplyHeaders_XInitiator_UserWhenLastRoleIsUser(t *testing.T) { t.Parallel() e := &GitHubCopilotExecutor{} req, _ := http.NewRequest(http.MethodPost, "https://example.com", nil) - // Claude Code typical flow: last message is user (tool result), but has assistant in history + // Last role governs the initiator decision. body := []byte(`{"messages":[{"role":"user","content":"hello"},{"role":"assistant","content":"I will read the file"},{"role":"user","content":"tool result here"}]}`) e.applyHeaders(req, "token", body) - if got := req.Header.Get("X-Initiator"); got != "agent" { - t.Fatalf("X-Initiator = %q, want agent (assistant exists in messages)", got) + if got := req.Header.Get("X-Initiator"); got != "user" { + t.Fatalf("X-Initiator = %q, want user (last role is user)", got) } } @@ -285,6 +285,39 @@ func TestApplyHeaders_XInitiator_AgentWithToolRole(t *testing.T) { } } +func TestApplyHeaders_XInitiator_InputArrayLastAssistantMessage(t *testing.T) { + t.Parallel() + e := &GitHubCopilotExecutor{} + req, _ := http.NewRequest(http.MethodPost, "https://example.com", nil) + body := []byte(`{"input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"Hi"}]},{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Hello"}]}]}`) + e.applyHeaders(req, "token", body) + if got := req.Header.Get("X-Initiator"); got != "agent" { + t.Fatalf("X-Initiator = %q, want agent (last role is assistant)", got) + } +} + +func TestApplyHeaders_XInitiator_InputArrayLastUserMessage(t *testing.T) { + t.Parallel() + e := &GitHubCopilotExecutor{} + req, _ := http.NewRequest(http.MethodPost, "https://example.com", nil) + body := []byte(`{"input":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"I can help"}]},{"type":"message","role":"user","content":[{"type":"input_text","text":"Do X"}]}]}`) + e.applyHeaders(req, "token", body) + if got := req.Header.Get("X-Initiator"); got != "user" { + t.Fatalf("X-Initiator = %q, want user (last role is user)", got) + } +} + +func TestApplyHeaders_XInitiator_InputArrayLastFunctionCallOutput(t *testing.T) { + t.Parallel() + e := &GitHubCopilotExecutor{} + req, _ := http.NewRequest(http.MethodPost, "https://example.com", nil) + body := []byte(`{"input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"Use tool"}]},{"type":"function_call","call_id":"c1","name":"Read","arguments":"{}"},{"type":"function_call_output","call_id":"c1","output":"ok"}]}`) + e.applyHeaders(req, "token", body) + if got := req.Header.Get("X-Initiator"); got != "agent" { + t.Fatalf("X-Initiator = %q, want agent (last item maps to tool role)", got) + } +} + // --- Tests for x-github-api-version header (Problem M) --- func TestApplyHeaders_GitHubAPIVersion(t *testing.T) {