Skip to content

Fix GitHub Copilot authentication flow #294

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ You can configure OpenCode using environment variables:
| `ANTHROPIC_API_KEY` | For Claude models |
| `OPENAI_API_KEY` | For OpenAI models |
| `GEMINI_API_KEY` | For Google Gemini models |
| `GITHUB_TOKEN` | For Github Copilot models (see [Using Github Copilot](#using-github-copilot)) |
| `GITHUB_COPILOT_TOKEN` | For Github Copilot models (see [Using GitHub Copilot](#using-github-copilot)) |
| `VERTEXAI_PROJECT` | For Google Cloud VertexAI (Gemini) |
| `VERTEXAI_LOCATION` | For Google Cloud VertexAI (Gemini) |
| `GROQ_API_KEY` | For Groq models |
Expand Down Expand Up @@ -237,6 +237,37 @@ OpenCode supports a variety of AI models from different providers:
- Gemini 2.0 Flash
- Gemini 2.5 Pro

#### Using GitHub Copilot

OpenCode supports using GitHub Copilot's models through a streamlined authentication flow:

1. Add Copilot to your configuration:
```json
{
"agents": {
"coder": {
"model": "copilot.claude-3.7-sonnet",
"maxTokens": 16384
}
},
"providers": {
"copilot": {
"disabled": false
}
}
}
```

2. When you first run OpenCode with a Copilot model selected, the application will:
- Check for an existing Copilot token in standard locations (hosts.json, apps.json)
- If no token is found, automatically start the authentication flow
- Prompt you to visit a GitHub URL and enter a device code
- Store the token in the standard GitHub Copilot location (`~/.config/github-copilot/hosts.json`)

3. For subsequent runs, OpenCode will use your saved token automatically.

Note: You need an active GitHub Copilot subscription to use these models.

### Google

- Gemini 2.5
Expand Down Expand Up @@ -617,7 +648,7 @@ the tool with your github account. This should create a github token at one of t
- ~/.config/github-copilot/[hosts,apps].json
- $XDG_CONFIG_HOME/github-copilot/[hosts,apps].json

If using an explicit github token, you may either set the $GITHUB_TOKEN environment variable or add it to the opencode.json config file at `providers.copilot.apiKey`.
If using an explicit github token, you may either set the $GITHUB_COPILOT_TOKEN environment variable or add it to the opencode.json config file at `providers.copilot.apiKey`.

## Using a self-hosted model provider

Expand Down
71 changes: 64 additions & 7 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,35 @@ func validateAgent(cfg *Config, name AgentName, agent Agent) error {
provider := model.Provider
providerCfg, providerExists := cfg.Providers[provider]

// Special handling for Copilot provider - allow empty API key and use device flow
if provider == models.ProviderCopilot {
logging.Debug("Validating Copilot provider", "exists", providerExists)

// If provider doesn't exist in config, add it with empty API key
if !providerExists {
cfg.Providers[provider] = Provider{
APIKey: "", // We'll use device flow for authentication
}
logging.Info("Added Copilot provider to config for device flow authentication")
} else if providerCfg.Disabled {
// Provider explicitly disabled - revert to default model
logging.Warn("Copilot provider is disabled but model requires it",
"agent", name,
"model", agent.Model)

// Set default model based on available providers
if setDefaultModelForAgent(name) {
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
} else {
return fmt.Errorf("no valid provider available for agent %s", name)
}
}

// Copilot provider is valid even without API key (will use device flow)
return nil
}

// For all other providers
if !providerExists {
// Provider not configured, check if we have environment variables
apiKey := getProviderAPIKey(provider)
Expand Down Expand Up @@ -613,15 +642,18 @@ func Validate() error {

// Validate agent models
for name, agent := range cfg.Agents {
logging.Debug("Validating agent", "name", name, "model", agent.Model)
if err := validateAgent(cfg, name, agent); err != nil {
logging.Error("Agent validation failed", "name", name, "error", err)
return err
}
}

// Validate providers
for provider, providerCfg := range cfg.Providers {
if providerCfg.APIKey == "" && !providerCfg.Disabled {
fmt.Printf("provider has no API key, marking as disabled %s", provider)
// Special case for Copilot - we allow it to have no API key
if provider != models.ProviderCopilot && providerCfg.APIKey == "" && !providerCfg.Disabled {
fmt.Printf("provider has no API key, marking as disabled %s\n", provider)
logging.Warn("provider has no API key, marking as disabled", "provider", provider)
providerCfg.Disabled = true
cfg.Providers[provider] = providerCfg
Expand All @@ -637,6 +669,7 @@ func Validate() error {
}
}

logging.Debug("Configuration validation completed successfully")
return nil
}

Expand Down Expand Up @@ -868,6 +901,13 @@ func Get() *Config {
return cfg
}

// SetNonInteractive sets the non-interactive flag in the global viper config
// This helps components detect if they're running in non-interactive mode
func SetNonInteractive(val bool) {
viper.Set("non_interactive", val)
logging.Debug("Set non_interactive mode", "value", val)
}

// WorkingDirectory returns the current working directory from the configuration.
func WorkingDirectory() string {
if cfg == nil {
Expand Down Expand Up @@ -929,10 +969,23 @@ func UpdateTheme(themeName string) error {
})
}

// Tries to load Github token from all possible locations
// LoadGitHubToken loads GitHub Copilot token from config files, environment variables, or other sources
// Returns the token if found, or a special error "no_copilot_token" if no token is found
// This prioritizes GITHUB_COPILOT_TOKEN to avoid conflicts with standard GitHub CLI tools
func LoadGitHubToken() (string, error) {
// First check environment variable
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
logging.Debug("Attempting to load GitHub Copilot token")

// 1. Environment variable (prioritize Copilot-specific token)
var token string
if token = os.Getenv("GITHUB_COPILOT_TOKEN"); token != "" {
logging.Debug("Loaded GitHub Copilot API key from GITHUB_COPILOT_TOKEN environment variable")
return token, nil
}

// 2. API key from config options
cfg := Get()
if token = cfg.Providers[models.ProviderCopilot].APIKey; token != "" {
logging.Debug("Loaded GitHub Copilot API key from the '.opencode.json' configuration file")
return token, nil
}

Expand All @@ -950,7 +1003,7 @@ func LoadGitHubToken() (string, error) {
configDir = filepath.Join(os.Getenv("HOME"), ".config")
}

// Try both hosts.json and apps.json files
// 3. Try both hosts.json and apps.json files
filePaths := []string{
filepath.Join(configDir, "github-copilot", "hosts.json"),
filepath.Join(configDir, "github-copilot", "apps.json"),
Expand All @@ -970,11 +1023,15 @@ func LoadGitHubToken() (string, error) {
for key, value := range config {
if strings.Contains(key, "github.com") {
if oauthToken, ok := value["oauth_token"].(string); ok {
logging.Debug("Loaded GitHub Copilot token from the standard user configuration file")
return oauthToken, nil
}
}
}
}

return "", fmt.Errorf("GitHub token not found in standard locations")
// Return a special error that indicates we need to use device code flow
logging.Debug("No GitHub Copilot token found - will need to use device code flow")
return "", fmt.Errorf("no_copilot_token")
}

Loading