Skip to content

Commit c4436a5

Browse files
committed
feat: add github copilot provider
1 parent 23763fb commit c4436a5

File tree

8 files changed

+987
-35
lines changed

8 files changed

+987
-35
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ Thumbs.db
4444
.opencode/
4545

4646
opencode
47+
opencode.md

README.md

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -91,22 +91,23 @@ You can enable or disable this feature in your configuration file:
9191

9292
You can configure OpenCode using environment variables:
9393

94-
| Environment Variable | Purpose |
95-
| -------------------------- | ------------------------------------------------------ |
96-
| `ANTHROPIC_API_KEY` | For Claude models |
97-
| `OPENAI_API_KEY` | For OpenAI models |
98-
| `GEMINI_API_KEY` | For Google Gemini models |
99-
| `VERTEXAI_PROJECT` | For Google Cloud VertexAI (Gemini) |
100-
| `VERTEXAI_LOCATION` | For Google Cloud VertexAI (Gemini) |
101-
| `GROQ_API_KEY` | For Groq models |
102-
| `AWS_ACCESS_KEY_ID` | For AWS Bedrock (Claude) |
103-
| `AWS_SECRET_ACCESS_KEY` | For AWS Bedrock (Claude) |
104-
| `AWS_REGION` | For AWS Bedrock (Claude) |
105-
| `AZURE_OPENAI_ENDPOINT` | For Azure OpenAI models |
106-
| `AZURE_OPENAI_API_KEY` | For Azure OpenAI models (optional when using Entra ID) |
107-
| `AZURE_OPENAI_API_VERSION` | For Azure OpenAI models |
108-
| `LOCAL_ENDPOINT` | For self-hosted models |
109-
| `SHELL` | Default shell to use (if not specified in config) |
94+
| Environment Variable | Purpose |
95+
| -------------------------- | -------------------------------------------------------------------------------- |
96+
| `ANTHROPIC_API_KEY` | For Claude models |
97+
| `OPENAI_API_KEY` | For OpenAI models |
98+
| `GEMINI_API_KEY` | For Google Gemini models |
99+
| `GITHUB_TOKEN` | For Github Copilot models (see [Using Github Copilot](#using-github-copilot)) |
100+
| `VERTEXAI_PROJECT` | For Google Cloud VertexAI (Gemini) |
101+
| `VERTEXAI_LOCATION` | For Google Cloud VertexAI (Gemini) |
102+
| `GROQ_API_KEY` | For Groq models |
103+
| `AWS_ACCESS_KEY_ID` | For AWS Bedrock (Claude) |
104+
| `AWS_SECRET_ACCESS_KEY` | For AWS Bedrock (Claude) |
105+
| `AWS_REGION` | For AWS Bedrock (Claude) |
106+
| `AZURE_OPENAI_ENDPOINT` | For Azure OpenAI models |
107+
| `AZURE_OPENAI_API_KEY` | For Azure OpenAI models (optional when using Entra ID) |
108+
| `AZURE_OPENAI_API_VERSION` | For Azure OpenAI models |
109+
| `LOCAL_ENDPOINT` | For self-hosted models |
110+
| `SHELL` | Default shell to use (if not specified in config) |
110111

111112
### Shell Configuration
112113

@@ -141,6 +142,9 @@ This is useful if you want to use a different shell than your default system she
141142
"apiKey": "your-api-key",
142143
"disabled": false
143144
},
145+
"copilot": {
146+
"disabled": false
147+
},
144148
"groq": {
145149
"apiKey": "your-api-key",
146150
"disabled": false
@@ -211,6 +215,23 @@ OpenCode supports a variety of AI models from different providers:
211215
- Claude 3 Haiku
212216
- Claude 3 Opus
213217

218+
### GitHub Copilot
219+
220+
- GPT-3.5 Turbo
221+
- GPT-4
222+
- GPT-4o
223+
- GPT-4o Mini
224+
- GPT-4.1
225+
- Claude 3.5 Sonnet
226+
- Claude 3.7 Sonnet
227+
- Claude 3.7 Sonnet Thinking
228+
- Claude Sonnet 4
229+
- O1
230+
- O3 Mini
231+
- O4 Mini
232+
- Gemini 2.0 Flash
233+
- Gemini 2.5 Pro
234+
214235
### Google
215236

216237
- Gemini 2.5
@@ -574,6 +595,25 @@ The AI assistant can access LSP features through the `diagnostics` tool, allowin
574595

575596
While the LSP client implementation supports the full LSP protocol (including completions, hover, definition, etc.), currently only diagnostics are exposed to the AI assistant.
576597

598+
## Using Github Copilot
599+
600+
_Copilot support is currently experimental._
601+
602+
### Requirements
603+
- [Copilot chat in the IDE](https://github.com/settings/copilot) enabled in GitHub settings
604+
- One of:
605+
- VSCode Github Copilot chat extension
606+
- Github `gh` CLI
607+
- Neovim Github Copilot plugin (`copilot.vim` or `copilot.lua`)
608+
- Github token with copilot permissions
609+
610+
If using one of the above plugins or cli tools, make sure you use the authenticate
611+
the tool with your github account. This should create a github token at one of the following locations:
612+
- ~/.config/github-copilot/[hosts,apps].json
613+
- $XDG_CONFIG_HOME/github-copilot/[hosts,apps].json
614+
615+
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`.
616+
577617
## Using a self-hosted model provider
578618

579619
OpenCode can also load and use models from a self-hosted (OpenAI-like) provider.

internal/config/config.go

Lines changed: 100 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"log/slog"
88
"os"
99
"path/filepath"
10+
"runtime"
1011
"strings"
1112

1213
"github.com/opencode-ai/opencode/internal/llm/models"
@@ -245,6 +246,7 @@ func setDefaults(debug bool) {
245246
// environment variables and configuration file.
246247
func setProviderDefaults() {
247248
// Set all API keys we can find in the environment
249+
// Note: Viper does not default if the json apiKey is ""
248250
if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
249251
viper.SetDefault("providers.anthropic.apiKey", apiKey)
250252
}
@@ -267,16 +269,32 @@ func setProviderDefaults() {
267269
// api-key may be empty when using Entra ID credentials – that's okay
268270
viper.SetDefault("providers.azure.apiKey", os.Getenv("AZURE_OPENAI_API_KEY"))
269271
}
272+
if apiKey, err := LoadGitHubToken(); err == nil && apiKey != "" {
273+
viper.SetDefault("providers.copilot.apiKey", apiKey)
274+
if viper.GetString("providers.copilot.apiKey") == "" {
275+
viper.Set("providers.copilot.apiKey", apiKey)
276+
}
277+
}
270278

271279
// Use this order to set the default models
272-
// 1. Anthropic
273-
// 2. OpenAI
274-
// 3. Google Gemini
275-
// 4. Groq
276-
// 5. OpenRouter
277-
// 6. AWS Bedrock
278-
// 7. Azure
279-
// 8. Google Cloud VertexAI
280+
// 1. Copilot
281+
// 2. Anthropic
282+
// 3. OpenAI
283+
// 4. Google Gemini
284+
// 5. Groq
285+
// 6. OpenRouter
286+
// 7. AWS Bedrock
287+
// 8. Azure
288+
// 9. Google Cloud VertexAI
289+
290+
// copilot configuration
291+
if key := viper.GetString("providers.copilot.apiKey"); strings.TrimSpace(key) != "" {
292+
viper.SetDefault("agents.coder.model", models.CopilotGPT4o)
293+
viper.SetDefault("agents.summarizer.model", models.CopilotGPT4o)
294+
viper.SetDefault("agents.task.model", models.CopilotGPT4o)
295+
viper.SetDefault("agents.title.model", models.CopilotGPT4o)
296+
return
297+
}
280298

281299
// Anthropic configuration
282300
if key := viper.GetString("providers.anthropic.apiKey"); strings.TrimSpace(key) != "" {
@@ -399,6 +417,14 @@ func hasVertexAICredentials() bool {
399417
return false
400418
}
401419

420+
func hasCopilotCredentials() bool {
421+
// Check for explicit Copilot parameters
422+
if token, _ := LoadGitHubToken(); token != "" {
423+
return true
424+
}
425+
return false
426+
}
427+
402428
// readConfig handles the result of reading a configuration file.
403429
func readConfig(err error) error {
404430
if err == nil {
@@ -440,6 +466,9 @@ func applyDefaultValues() {
440466
// It validates model IDs and providers, ensuring they are supported.
441467
func validateAgent(cfg *Config, name AgentName, agent Agent) error {
442468
// Check if model exists
469+
// TODO: If a copilot model is specified, but model is not found,
470+
// it might be new model. The https://api.githubcopilot.com/models
471+
// endpoint should be queried to validate if the model is supported.
443472
model, modelExists := models.SupportedModels[agent.Model]
444473
if !modelExists {
445474
logging.Warn("unsupported model configured, reverting to default",
@@ -584,6 +613,7 @@ func Validate() error {
584613
// Validate providers
585614
for provider, providerCfg := range cfg.Providers {
586615
if providerCfg.APIKey == "" && !providerCfg.Disabled {
616+
fmt.Printf("provider has no API key, marking as disabled %s", provider)
587617
logging.Warn("provider has no API key, marking as disabled", "provider", provider)
588618
providerCfg.Disabled = true
589619
cfg.Providers[provider] = providerCfg
@@ -631,6 +661,18 @@ func getProviderAPIKey(provider models.ModelProvider) string {
631661

632662
// setDefaultModelForAgent sets a default model for an agent based on available providers
633663
func setDefaultModelForAgent(agent AgentName) bool {
664+
if hasCopilotCredentials() {
665+
maxTokens := int64(5000)
666+
if agent == AgentTitle {
667+
maxTokens = 80
668+
}
669+
670+
cfg.Agents[agent] = Agent{
671+
Model: models.CopilotGPT4o,
672+
MaxTokens: maxTokens,
673+
}
674+
return true
675+
}
634676
// Check providers in order of preference
635677
if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
636678
maxTokens := int64(5000)
@@ -878,3 +920,53 @@ func UpdateTheme(themeName string) error {
878920
config.TUI.Theme = themeName
879921
})
880922
}
923+
924+
// Tries to load Github token from all possible locations
925+
func LoadGitHubToken() (string, error) {
926+
// First check environment variable
927+
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
928+
return token, nil
929+
}
930+
931+
// Get config directory
932+
var configDir string
933+
if xdgConfig := os.Getenv("XDG_CONFIG_HOME"); xdgConfig != "" {
934+
configDir = xdgConfig
935+
} else if runtime.GOOS == "windows" {
936+
if localAppData := os.Getenv("LOCALAPPDATA"); localAppData != "" {
937+
configDir = localAppData
938+
} else {
939+
configDir = filepath.Join(os.Getenv("HOME"), "AppData", "Local")
940+
}
941+
} else {
942+
configDir = filepath.Join(os.Getenv("HOME"), ".config")
943+
}
944+
945+
// Try both hosts.json and apps.json files
946+
filePaths := []string{
947+
filepath.Join(configDir, "github-copilot", "hosts.json"),
948+
filepath.Join(configDir, "github-copilot", "apps.json"),
949+
}
950+
951+
for _, filePath := range filePaths {
952+
data, err := os.ReadFile(filePath)
953+
if err != nil {
954+
continue
955+
}
956+
957+
var config map[string]map[string]interface{}
958+
if err := json.Unmarshal(data, &config); err != nil {
959+
continue
960+
}
961+
962+
for key, value := range config {
963+
if strings.Contains(key, "github.com") {
964+
if oauthToken, ok := value["oauth_token"].(string); ok {
965+
return oauthToken, nil
966+
}
967+
}
968+
}
969+
}
970+
971+
return "", fmt.Errorf("GitHub token not found in standard locations")
972+
}

0 commit comments

Comments
 (0)