|
|
|
|
@@ -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
|
|
|
|
|
|