From 33ab3a99f085bf6bf8e0ebb3fa1cb45663b519bd Mon Sep 17 00:00:00 2001 From: cybit Date: Tue, 27 Jan 2026 15:13:54 +0800 Subject: [PATCH] fix: add Copilot-Vision-Request header for vision requests **Problem:** GitHub Copilot API returns 400 error "missing required Copilot-Vision-Request header for vision requests" when requests contain image content blocks, even though the requests are valid Claude API calls. **Root Cause:** The GitHub Copilot executor was not detecting vision content in requests and did not add the required `Copilot-Vision-Request: true` header. **Solution:** - Added `detectVisionContent()` function to check for image_url/image content blocks - Automatically add `Copilot-Vision-Request: true` header when vision content is detected - Applied fix to both `Execute()` and `ExecuteStream()` methods **Testing:** - Tested with Claude Code IDE requests containing code context screenshots - Vision requests now succeed instead of failing with 400 errors - Non-vision requests remain unchanged Fixes issue where GitHub Copilot executor fails all vision-enabled requests, causing unnecessary fallback to other providers and 0% utilization. Co-Authored-By: Claude (claude-sonnet-4.5) --- .../executor/github_copilot_executor.go | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/internal/runtime/executor/github_copilot_executor.go b/internal/runtime/executor/github_copilot_executor.go index 147b32cd..ad93f488 100644 --- a/internal/runtime/executor/github_copilot_executor.go +++ b/internal/runtime/executor/github_copilot_executor.go @@ -17,6 +17,7 @@ import ( cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -134,6 +135,11 @@ func (e *GitHubCopilotExecutor) Execute(ctx context.Context, auth *cliproxyauth. } e.applyHeaders(httpReq, apiToken) + // Add Copilot-Vision-Request header if the request contains vision content + if detectVisionContent(body) { + httpReq.Header.Set("Copilot-Vision-Request", "true") + } + var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID @@ -238,6 +244,11 @@ func (e *GitHubCopilotExecutor) ExecuteStream(ctx context.Context, auth *cliprox } e.applyHeaders(httpReq, apiToken) + // Add Copilot-Vision-Request header if the request contains vision content + if detectVisionContent(body) { + httpReq.Header.Set("Copilot-Vision-Request", "true") + } + var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID @@ -415,6 +426,34 @@ func (e *GitHubCopilotExecutor) applyHeaders(r *http.Request, apiToken string) { r.Header.Set("X-Request-Id", uuid.NewString()) } +// detectVisionContent checks if the request body contains vision/image content. +// Returns true if the request includes image_url or image type content blocks. +func detectVisionContent(body []byte) bool { + // Parse messages array + messagesResult := gjson.GetBytes(body, "messages") + if !messagesResult.Exists() || !messagesResult.IsArray() { + return false + } + + // Check each message for vision content + for _, message := range messagesResult.Array() { + content := message.Get("content") + + // If content is an array, check each content block + if content.IsArray() { + for _, block := range content.Array() { + blockType := block.Get("type").String() + // Check for image_url or image type + if blockType == "image_url" || blockType == "image" { + return true + } + } + } + } + + return false +} + // normalizeModel is a no-op as GitHub Copilot accepts model names directly. // Model mapping should be done at the registry level if needed. func (e *GitHubCopilotExecutor) normalizeModel(_ string, body []byte) []byte {