From d468eec6ecefbec0c9a50f821ae33fb9e1221c78 Mon Sep 17 00:00:00 2001 From: rico <565636992@qq.com> Date: Sun, 8 Feb 2026 02:22:10 +0800 Subject: [PATCH] fix(copilot): prevent premium request count inflation for Claude models > Copilot Premium usage significantly amplified when using amp - Add X-Initiator header (user/agent) based on last message role to prevent Copilot from billing all requests as premium user-initiated - Add flattenAssistantContent() to convert assistant content from array to string, preventing Claude from re-answering all previous prompts - Align Copilot headers (User-Agent, Editor-Version, Openai-Intent) with pi-ai reference implementation Closes #113 Amp-Thread-ID: https://ampcode.com/threads/T-019c392b-736e-7489-a06b-f94f7c75f7c0 Co-authored-by: Amp --- .../executor/github_copilot_executor.go | 65 ++++++++++++++++--- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/internal/runtime/executor/github_copilot_executor.go b/internal/runtime/executor/github_copilot_executor.go index ad93f488..b43e1909 100644 --- a/internal/runtime/executor/github_copilot_executor.go +++ b/internal/runtime/executor/github_copilot_executor.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "strings" "sync" "time" @@ -33,11 +34,11 @@ const ( maxScannerBufferSize = 20_971_520 // Copilot API header values. - copilotUserAgent = "GithubCopilot/1.0" - copilotEditorVersion = "vscode/1.100.0" - copilotPluginVersion = "copilot/1.300.0" + copilotUserAgent = "GitHubCopilotChat/0.35.0" + copilotEditorVersion = "vscode/1.107.0" + copilotPluginVersion = "copilot-chat/0.35.0" copilotIntegrationID = "vscode-chat" - copilotOpenAIIntent = "conversation-panel" + copilotOpenAIIntent = "conversation-edits" ) // GitHubCopilotExecutor handles requests to the GitHub Copilot API. @@ -77,7 +78,7 @@ func (e *GitHubCopilotExecutor) PrepareRequest(req *http.Request, auth *cliproxy if errToken != nil { return errToken } - e.applyHeaders(req, apiToken) + e.applyHeaders(req, apiToken, nil) return nil } @@ -120,6 +121,7 @@ func (e *GitHubCopilotExecutor) Execute(ctx context.Context, auth *cliproxyauth. originalTranslated := sdktranslator.TranslateRequest(from, to, req.Model, originalPayload, false) body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) body = e.normalizeModel(req.Model, body) + body = flattenAssistantContent(body) requestedModel := payloadRequestedModel(opts, req.Model) body = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", body, originalTranslated, requestedModel) body, _ = sjson.SetBytes(body, "stream", false) @@ -133,7 +135,7 @@ func (e *GitHubCopilotExecutor) Execute(ctx context.Context, auth *cliproxyauth. if err != nil { return resp, err } - e.applyHeaders(httpReq, apiToken) + e.applyHeaders(httpReq, apiToken, body) // Add Copilot-Vision-Request header if the request contains vision content if detectVisionContent(body) { @@ -225,6 +227,7 @@ func (e *GitHubCopilotExecutor) ExecuteStream(ctx context.Context, auth *cliprox originalTranslated := sdktranslator.TranslateRequest(from, to, req.Model, originalPayload, false) body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) body = e.normalizeModel(req.Model, body) + body = flattenAssistantContent(body) requestedModel := payloadRequestedModel(opts, req.Model) body = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", body, originalTranslated, requestedModel) body, _ = sjson.SetBytes(body, "stream", true) @@ -242,7 +245,7 @@ func (e *GitHubCopilotExecutor) ExecuteStream(ctx context.Context, auth *cliprox if err != nil { return nil, err } - e.applyHeaders(httpReq, apiToken) + e.applyHeaders(httpReq, apiToken, body) // Add Copilot-Vision-Request header if the request contains vision content if detectVisionContent(body) { @@ -414,7 +417,7 @@ func (e *GitHubCopilotExecutor) ensureAPIToken(ctx context.Context, auth *clipro } // applyHeaders sets the required headers for GitHub Copilot API requests. -func (e *GitHubCopilotExecutor) applyHeaders(r *http.Request, apiToken string) { +func (e *GitHubCopilotExecutor) applyHeaders(r *http.Request, apiToken string, body []byte) { r.Header.Set("Content-Type", "application/json") r.Header.Set("Authorization", "Bearer "+apiToken) r.Header.Set("Accept", "application/json") @@ -424,6 +427,20 @@ func (e *GitHubCopilotExecutor) applyHeaders(r *http.Request, apiToken string) { r.Header.Set("Openai-Intent", copilotOpenAIIntent) r.Header.Set("Copilot-Integration-Id", copilotIntegrationID) r.Header.Set("X-Request-Id", uuid.NewString()) + + initiator := "user" + if len(body) > 0 { + if messages := gjson.GetBytes(body, "messages"); messages.Exists() && messages.IsArray() { + arr := messages.Array() + if len(arr) > 0 { + lastRole := arr[len(arr)-1].Get("role").String() + if lastRole != "" && lastRole != "user" { + initiator = "agent" + } + } + } + } + r.Header.Set("X-Initiator", initiator) } // detectVisionContent checks if the request body contains vision/image content. @@ -464,6 +481,38 @@ func useGitHubCopilotResponsesEndpoint(sourceFormat sdktranslator.Format) bool { return sourceFormat.String() == "openai-response" } +// flattenAssistantContent converts assistant message content from array format +// to a joined string. GitHub Copilot requires assistant content as a string; +// sending it as an array causes Claude models to re-answer all previous prompts. +func flattenAssistantContent(body []byte) []byte { + messages := gjson.GetBytes(body, "messages") + if !messages.Exists() || !messages.IsArray() { + return body + } + result := body + for i, msg := range messages.Array() { + if msg.Get("role").String() != "assistant" { + continue + } + content := msg.Get("content") + if !content.Exists() || !content.IsArray() { + continue + } + var textParts []string + for _, part := range content.Array() { + if part.Get("type").String() == "text" { + if t := part.Get("text").String(); t != "" { + textParts = append(textParts, t) + } + } + } + joined := strings.Join(textParts, "") + path := fmt.Sprintf("messages.%d.content", i) + result, _ = sjson.SetBytes(result, path, joined) + } + return result +} + // isHTTPSuccess checks if the status code indicates success (2xx). func isHTTPSuccess(statusCode int) bool { return statusCode >= 200 && statusCode < 300