Skip to content
This repository was archived by the owner on Sep 18, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 7 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
33 changes: 32 additions & 1 deletion 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_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
73 changes: 64 additions & 9 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,11 +969,18 @@ func UpdateTheme(themeName string) error {
})
}

// Tries to load Github token from all possible locations
// LoadGitHubToken loads GitHub 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 follows the 4-step flow: 1. Check if Copilot is enabled, 2. Check for token in config folder
func LoadGitHubToken() (string, error) {
// First check environment variable
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
return token, nil
logging.Debug("Attempting to load GitHub token for Copilot")

// First check environment variables
for _, envName := range []string{"GITHUB_TOKEN", "GITHUB_COPILOT_TOKEN", "GH_COPILOT_TOKEN"} {
Copy link

@mendesbarreto mendesbarreto Jul 5, 2025

Choose a reason for hiding this comment

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

Awesome, I was just about to create a PR for this myself. We should move away from GITHUB_TOKEN for the reason mentioned here. All the other tools like Gemini and ChatGPT use their own environment variables, so it makes sense to give Copilot its own too. Using GITHUB_TOKEN can be misleading as copilot request don't accept them.

For me this method should be something like this:

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

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

	// Get config directory
	var configDir string
	if xdgConfig := os.Getenv("XDG_CONFIG_HOME"); xdgConfig != "" {
		configDir = xdgConfig
	} else if runtime.GOOS == "windows" {
		if localAppData := os.Getenv("LOCALAPPDATA"); localAppData != "" {
			configDir = localAppData
		} else {
			configDir = filepath.Join(os.Getenv("HOME"), "AppData", "Local")
		}
	} else {
		configDir = filepath.Join(os.Getenv("HOME"), ".config")
	}

	// Try both hosts.json and apps.json files
	filePaths := []string{
		filepath.Join(configDir, "github-copilot", "hosts.json"),
		filepath.Join(configDir, "github-copilot", "apps.json"),
	}

	for _, filePath := range filePaths {
		data, err := os.ReadFile(filePath)
		if err != nil {
			continue
		}

		var config map[string]map[string]interface{}
		if err := json.Unmarshal(data, &config); err != nil {
			continue
		}

		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")
}

if token := os.Getenv(envName); token != "" {
logging.Debug("Found GitHub token in environment variable", "env_var", envName)
return token, nil
}
}

// Get config directory
Expand All @@ -950,12 +997,14 @@ func LoadGitHubToken() (string, error) {
configDir = filepath.Join(os.Getenv("HOME"), ".config")
}

// Try both hosts.json and apps.json files
// Check standard Copilot config files
filePaths := []string{
filepath.Join(configDir, "github-copilot", "hosts.json"),
filepath.Join(configDir, "github-copilot", "apps.json"),
}

logging.Debug("Checking Copilot config files")

for _, filePath := range filePaths {
data, err := os.ReadFile(filePath)
if err != nil {
Expand All @@ -967,14 +1016,20 @@ func LoadGitHubToken() (string, error) {
continue
}

// For hosts.json, we expect keys like "github.com"
// For apps.json, we expect keys like "github.com:Iv1.b507a08c87ecfe98"
for key, value := range config {
if strings.Contains(key, "github.com") {
if oauthToken, ok := value["oauth_token"].(string); ok {
if oauthToken, ok := value["oauth_token"].(string); ok && oauthToken != "" {
logging.Debug("Found GitHub token in config file", "path", filePath)
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 Copilot token found - will need to use device code flow")
return "", fmt.Errorf("no_copilot_token")
}

Loading