Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,26 @@ ws-auth: false
# - "*-mini" # wildcard matching suffix (e.g. gpt-5-codex-mini)
# - "*codex*" # wildcard matching substring (e.g. gpt-5-codex-low)

# GitHub Copilot account configuration
# Note: Copilot uses OAuth device code authentication, NOT API keys or tokens.
# Do NOT paste your GitHub access token or Copilot bearer token here.
# Tokens are stored only in auth-dir JSON files, never in config.yaml.
#
# To authenticate:
# - CLI: run with -copilot-login flag
# - Web: use the /copilot-auth-url management endpoint
#
# After OAuth login, tokens are managed automatically and stored in auth-dir.
# The entries below only configure account type and optional proxy settings.
#copilot-api-key:
# - account-type: "individual" # Options: individual, business, enterprise
# proxy-url: "socks5://proxy.example.com:1080" # optional: proxy for Copilot requests

# # When set to true, this flag forces subsequent requests in a session (sharing the same prompt_cache_key)
# # to send the header "X-Initiator: agent" instead of "vscode". This mirrors VS Code's behavior for
# # long-running agent interactions and helps prevent hitting standard rate limits.
# agent-initiator-persist: true

# Claude API keys
#claude-api-key:
# - api-key: "sk-atSM..." # use the official claude API key, no need to set the base url
Expand Down
129 changes: 129 additions & 0 deletions internal/auth/copilot/api_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Package copilot provides authentication and token management for GitHub Copilot API.
// It handles the OAuth2 device code flow, token exchange, and automatic token refresh.
package copilot

import (
"crypto/rand"
"encoding/hex"
"fmt"

copilotshared "github.com/router-for-me/CLIProxyAPI/v6/internal/copilot"
)

const (
GitHubBaseURL = "https://github.com"
GitHubAPIBaseURL = "https://api.github.com"
// GitHubClientID is the PUBLIC OAuth client ID for GitHub Copilot's VS Code extension.
// This is NOT a secret - it's the same client ID used by the official Copilot CLI and
// VS Code extension, publicly visible in their source code and network requests.
GitHubClientID = "Iv1.b507a08c87ecfe98"
GitHubAppScopes = "read:user"
DeviceCodePath = "/login/device/code"
AccessTokenPath = "/login/oauth/access_token"
CopilotTokenPath = "/copilot_internal/v2/token"
CopilotUserPath = "/copilot_internal/user"
UserInfoPath = "/user"

CopilotVersion = "0.0.363"
EditorPluginVersion = "copilot/" + CopilotVersion
CopilotUserAgent = "copilot/" + CopilotVersion + " (linux v22.15.0)"
CopilotAPIVersion = "2025-05-01"
CopilotIntegrationID = "copilot-developer-cli"
DefaultVSCodeVersion = "1.95.0"
)

type AccountType = copilotshared.AccountType

const (
AccountTypeIndividual AccountType = copilotshared.AccountTypeIndividual
AccountTypeBusiness AccountType = copilotshared.AccountTypeBusiness
AccountTypeEnterprise AccountType = copilotshared.AccountTypeEnterprise
)

var ValidAccountTypes = copilotshared.ValidAccountTypes

const DefaultAccountType = copilotshared.DefaultAccountType

func CopilotBaseURL(accountType AccountType) string {
switch accountType {
case AccountTypeBusiness:
return "https://api.business.githubcopilot.com"
case AccountTypeEnterprise:
return "https://api.enterprise.githubcopilot.com"
default:
// Individual accounts use the individual Copilot endpoint.
return "https://api.individual.githubcopilot.com"
}
}

func StandardHeaders() map[string]string {
return map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
}
}

func GitHubHeaders(githubToken, vsCodeVersion string) map[string]string {
if vsCodeVersion == "" {
vsCodeVersion = DefaultVSCodeVersion
}
return map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": fmt.Sprintf("token %s", githubToken),
"Editor-Version": fmt.Sprintf("vscode/%s", vsCodeVersion),
"Editor-Plugin-Version": EditorPluginVersion,
"User-Agent": CopilotUserAgent,
"X-Github-Api-Version": CopilotAPIVersion,
"X-Vscode-User-Agent-Library-Version": "electron-fetch",
}
}

func CopilotHeaders(copilotToken, vsCodeVersion string, enableVision bool) map[string]string {
if vsCodeVersion == "" {
vsCodeVersion = DefaultVSCodeVersion
}
headers := map[string]string{
"Content-Type": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", copilotToken),
"Copilot-Integration-Id": CopilotIntegrationID,
"Editor-Version": fmt.Sprintf("vscode/%s", vsCodeVersion),
"Editor-Plugin-Version": EditorPluginVersion,
"User-Agent": CopilotUserAgent,
"Openai-Intent": "conversation-agent",
"X-Github-Api-Version": CopilotAPIVersion,
"X-Request-Id": generateRequestID(),
"X-Interaction-Id": generateRequestID(),
"X-Vscode-User-Agent-Library-Version": "electron-fetch",
}
if enableVision {
headers["Copilot-Vision-Request"] = "true"
}
return headers
}

func generateRequestID() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(fmt.Sprintf("failed to generate random bytes for request ID: %v", err))
}
Comment on lines +107 to +109

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Calling panic in a server application can cause it to crash, making the service unavailable. The crypto/rand.Read function can fail if the operating system's entropy pool is exhausted. It's crucial to handle this error gracefully instead of panicking. A good approach is to fall back to a different UUID generation method.

Note: This suggestion requires importing github.com/google/uuid and github.com/sirupsen/logrus.

if _, err := rand.Read(b); err != nil {
		log.Errorf("crypto/rand.Read failed for request ID, falling back to UUID: %v", err)
		return uuid.New().String()
	}

return fmt.Sprintf("%s-%s-%s-%s-%s",
hex.EncodeToString(b[0:4]),
hex.EncodeToString(b[4:6]),
hex.EncodeToString(b[6:8]),
hex.EncodeToString(b[8:10]),
hex.EncodeToString(b[10:16]))
}

// MaskToken returns a masked version of a token for safe logging.
// Shows first 2 and last 2 characters with asterisks in between.
// Returns "<empty>" for empty tokens and "<short>" for tokens under 5 chars.
func MaskToken(token string) string {
if token == "" {
return "<empty>"
}
if len(token) < 5 {
return "<short>"
}
return token[:2] + "****" + token[len(token)-2:]
}
Loading