From 7d271e6dc9fb114d48b91a1ed2ed3d414178a883 Mon Sep 17 00:00:00 2001 From: Mike Wallio Date: Wed, 15 Apr 2026 20:22:53 -0400 Subject: [PATCH] cmd/launch: add Copilot CLI integration (#15583) --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: ParthSareen --- README.md | 4 +- cmd/launch/copilot.go | 76 ++++++++++++++ cmd/launch/copilot_test.go | 161 ++++++++++++++++++++++++++++++ cmd/launch/launch.go | 1 + cmd/launch/registry.go | 15 ++- docs/docs.json | 1 + docs/integrations/copilot-cli.mdx | 93 +++++++++++++++++ docs/integrations/index.mdx | 1 + 8 files changed, 349 insertions(+), 3 deletions(-) create mode 100644 cmd/launch/copilot.go create mode 100644 cmd/launch/copilot_test.go create mode 100644 docs/integrations/copilot-cli.mdx diff --git a/README.md b/README.md index e63b50259..0fc1e8ca9 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ The official [Ollama Docker image](https://hub.docker.com/r/ollama/ollama) `olla ollama ``` -You'll be prompted to run a model or connect Ollama to your existing agents or applications such as `claude`, `codex`, `openclaw` and more. +You'll be prompted to run a model or connect Ollama to your existing agents or applications such as `Claude Code`, `OpenClaw`, `OpenCode` , `Codex`, `Copilot`, and more. ### Coding @@ -65,7 +65,7 @@ To launch a specific integration: ollama launch claude ``` -Supported integrations include [Claude Code](https://docs.ollama.com/integrations/claude-code), [Codex](https://docs.ollama.com/integrations/codex), [Droid](https://docs.ollama.com/integrations/droid), and [OpenCode](https://docs.ollama.com/integrations/opencode). +Supported integrations include [Claude Code](https://docs.ollama.com/integrations/claude-code), [Codex](https://docs.ollama.com/integrations/codex), [Copilot CLI](https://docs.ollama.com/integrations/copilot-cli), [Droid](https://docs.ollama.com/integrations/droid), and [OpenCode](https://docs.ollama.com/integrations/opencode). ### AI assistant diff --git a/cmd/launch/copilot.go b/cmd/launch/copilot.go new file mode 100644 index 000000000..d626027d3 --- /dev/null +++ b/cmd/launch/copilot.go @@ -0,0 +1,76 @@ +package launch + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/ollama/ollama/envconfig" +) + +// Copilot implements Runner for GitHub Copilot CLI integration. +type Copilot struct{} + +func (c *Copilot) String() string { return "Copilot CLI" } + +func (c *Copilot) args(model string, extra []string) []string { + var args []string + if model != "" { + args = append(args, "--model", model) + } + args = append(args, extra...) + return args +} + +func (c *Copilot) findPath() (string, error) { + if p, err := exec.LookPath("copilot"); err == nil { + return p, nil + } + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + name := "copilot" + if runtime.GOOS == "windows" { + name = "copilot.exe" + } + fallback := filepath.Join(home, ".local", "bin", name) + if _, err := os.Stat(fallback); err != nil { + return "", err + } + return fallback, nil +} + +func (c *Copilot) Run(model string, args []string) error { + copilotPath, err := c.findPath() + if err != nil { + return fmt.Errorf("copilot is not installed, install from https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli") + } + + cmd := exec.Command(copilotPath, c.args(model, args)...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + cmd.Env = append(os.Environ(), c.envVars(model)...) + + return cmd.Run() +} + +// envVars returns the environment variables that configure Copilot CLI +// to use Ollama as its model provider. +func (c *Copilot) envVars(model string) []string { + env := []string{ + "COPILOT_PROVIDER_BASE_URL=" + envconfig.Host().String() + "/v1", + "COPILOT_PROVIDER_API_KEY=", + "COPILOT_PROVIDER_WIRE_API=responses", + } + + if model != "" { + env = append(env, "COPILOT_MODEL="+model) + } + + return env +} diff --git a/cmd/launch/copilot_test.go b/cmd/launch/copilot_test.go new file mode 100644 index 000000000..fb3cdb62c --- /dev/null +++ b/cmd/launch/copilot_test.go @@ -0,0 +1,161 @@ +package launch + +import ( + "os" + "path/filepath" + "runtime" + "slices" + "strings" + "testing" +) + +func TestCopilotIntegration(t *testing.T) { + c := &Copilot{} + + t.Run("String", func(t *testing.T) { + if got := c.String(); got != "Copilot CLI" { + t.Errorf("String() = %q, want %q", got, "Copilot CLI") + } + }) + + t.Run("implements Runner", func(t *testing.T) { + var _ Runner = c + }) +} + +func TestCopilotFindPath(t *testing.T) { + c := &Copilot{} + + t.Run("finds copilot in PATH", func(t *testing.T) { + tmpDir := t.TempDir() + name := "copilot" + if runtime.GOOS == "windows" { + name = "copilot.exe" + } + fakeBin := filepath.Join(tmpDir, name) + os.WriteFile(fakeBin, []byte("#!/bin/sh\n"), 0o755) + t.Setenv("PATH", tmpDir) + + got, err := c.findPath() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != fakeBin { + t.Errorf("findPath() = %q, want %q", got, fakeBin) + } + }) + + t.Run("returns error when not in PATH", func(t *testing.T) { + t.Setenv("PATH", t.TempDir()) // empty dir, no copilot binary + + _, err := c.findPath() + if err == nil { + t.Fatal("expected error, got nil") + } + }) + + t.Run("falls back to ~/.local/bin/copilot", func(t *testing.T) { + tmpDir := t.TempDir() + setTestHome(t, tmpDir) + t.Setenv("PATH", t.TempDir()) // empty dir, no copilot binary + + name := "copilot" + if runtime.GOOS == "windows" { + name = "copilot.exe" + } + fallback := filepath.Join(tmpDir, ".local", "bin", name) + os.MkdirAll(filepath.Dir(fallback), 0o755) + os.WriteFile(fallback, []byte("#!/bin/sh\n"), 0o755) + + got, err := c.findPath() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != fallback { + t.Errorf("findPath() = %q, want %q", got, fallback) + } + }) + + t.Run("returns error when neither PATH nor fallback exists", func(t *testing.T) { + tmpDir := t.TempDir() + setTestHome(t, tmpDir) + t.Setenv("PATH", t.TempDir()) // empty dir, no copilot binary + + _, err := c.findPath() + if err == nil { + t.Fatal("expected error, got nil") + } + }) +} + +func TestCopilotArgs(t *testing.T) { + c := &Copilot{} + + tests := []struct { + name string + model string + args []string + want []string + }{ + {"with model", "llama3.2", nil, []string{"--model", "llama3.2"}}, + {"empty model", "", nil, nil}, + {"with model and extra", "llama3.2", []string{"--verbose"}, []string{"--model", "llama3.2", "--verbose"}}, + {"empty model with help", "", []string{"--help"}, []string{"--help"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := c.args(tt.model, tt.args) + if !slices.Equal(got, tt.want) { + t.Errorf("args(%q, %v) = %v, want %v", tt.model, tt.args, got, tt.want) + } + }) + } +} + +func TestCopilotEnvVars(t *testing.T) { + c := &Copilot{} + + envMap := func(envs []string) map[string]string { + m := make(map[string]string) + for _, e := range envs { + k, v, _ := strings.Cut(e, "=") + m[k] = v + } + return m + } + + t.Run("sets required provider env vars with model", func(t *testing.T) { + got := envMap(c.envVars("llama3.2")) + if got["COPILOT_PROVIDER_BASE_URL"] == "" { + t.Error("COPILOT_PROVIDER_BASE_URL should be set") + } + if !strings.HasSuffix(got["COPILOT_PROVIDER_BASE_URL"], "/v1") { + t.Errorf("COPILOT_PROVIDER_BASE_URL = %q, want /v1 suffix", got["COPILOT_PROVIDER_BASE_URL"]) + } + if _, ok := got["COPILOT_PROVIDER_API_KEY"]; !ok { + t.Error("COPILOT_PROVIDER_API_KEY should be set (empty)") + } + if got["COPILOT_PROVIDER_WIRE_API"] != "responses" { + t.Errorf("COPILOT_PROVIDER_WIRE_API = %q, want %q", got["COPILOT_PROVIDER_WIRE_API"], "responses") + } + if got["COPILOT_MODEL"] != "llama3.2" { + t.Errorf("COPILOT_MODEL = %q, want %q", got["COPILOT_MODEL"], "llama3.2") + } + }) + + t.Run("omits COPILOT_MODEL when model is empty", func(t *testing.T) { + got := envMap(c.envVars("")) + if _, ok := got["COPILOT_MODEL"]; ok { + t.Errorf("COPILOT_MODEL should not be set for empty model, got %q", got["COPILOT_MODEL"]) + } + }) + + t.Run("uses custom OLLAMA_HOST", func(t *testing.T) { + t.Setenv("OLLAMA_HOST", "http://myhost:9999") + got := envMap(c.envVars("test")) + if !strings.Contains(got["COPILOT_PROVIDER_BASE_URL"], "myhost:9999") { + t.Errorf("COPILOT_PROVIDER_BASE_URL = %q, want custom host", got["COPILOT_PROVIDER_BASE_URL"]) + } + }) +} diff --git a/cmd/launch/launch.go b/cmd/launch/launch.go index 91196cf1f..133398eae 100644 --- a/cmd/launch/launch.go +++ b/cmd/launch/launch.go @@ -206,6 +206,7 @@ Supported integrations: claude Claude Code cline Cline codex Codex + copilot Copilot CLI (aliases: copilot-cli) droid Droid hermes Hermes Agent opencode OpenCode diff --git a/cmd/launch/registry.go b/cmd/launch/registry.go index 77c134d77..fc96a1bab 100644 --- a/cmd/launch/registry.go +++ b/cmd/launch/registry.go @@ -33,7 +33,7 @@ type IntegrationInfo struct { Description string } -var launcherIntegrationOrder = []string{"openclaw", "claude", "opencode", "hermes", "codex", "droid", "pi"} +var launcherIntegrationOrder = []string{"openclaw", "claude", "opencode", "hermes", "codex", "copilot", "droid", "pi"} var integrationSpecs = []*IntegrationSpec{ { @@ -74,6 +74,19 @@ var integrationSpecs = []*IntegrationSpec{ Command: []string{"npm", "install", "-g", "@openai/codex"}, }, }, + { + Name: "copilot", + Runner: &Copilot{}, + Aliases: []string{"copilot-cli"}, + Description: "GitHub's AI coding agent for the terminal", + Install: IntegrationInstallSpec{ + CheckInstalled: func() bool { + _, err := (&Copilot{}).findPath() + return err == nil + }, + URL: "https://github.com/features/copilot/cli/", + }, + }, { Name: "droid", Runner: &Droid{}, diff --git a/docs/docs.json b/docs/docs.json index 3b2e651ff..17884d992 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -120,6 +120,7 @@ "pages": [ "/integrations/claude-code", "/integrations/codex", + "/integrations/copilot-cli", "/integrations/opencode", "/integrations/droid", "/integrations/goose", diff --git a/docs/integrations/copilot-cli.mdx b/docs/integrations/copilot-cli.mdx new file mode 100644 index 000000000..5262234ed --- /dev/null +++ b/docs/integrations/copilot-cli.mdx @@ -0,0 +1,93 @@ +--- +title: Copilot CLI +--- + +GitHub Copilot CLI is GitHub's AI coding agent for the terminal. It can understand your codebase, make edits, run commands, and help you build software faster. + +Open models can be used with Copilot CLI through Ollama, enabling you to use models such as `qwen3.5`, `glm-5.1:cloud`, `kimi-k2.5:cloud`. + +## Install + +Install [Copilot CLI](https://github.com/features/copilot/cli/): + + + +```shell macOS / Linux (Homebrew) +brew install copilot-cli +``` + +```shell npm (all platforms) +npm install -g @github/copilot +``` + +```shell macOS / Linux (script) +curl -fsSL https://gh.io/copilot-install | bash +``` + +```powershell Windows (WinGet) +winget install GitHub.Copilot +``` + + + +## Usage with Ollama + +### Quick setup + +```shell +ollama launch copilot +``` + +### Run directly with a model + +```shell +ollama launch copilot --model kimi-k2.5:cloud +``` + +## Recommended Models + +- `kimi-k2.5:cloud` +- `glm-5:cloud` +- `minimax-m2.7:cloud` +- `qwen3.5:cloud` +- `glm-4.7-flash` +- `qwen3.5` + +Cloud models are also available at [ollama.com/search?c=cloud](https://ollama.com/search?c=cloud). + +## Non-interactive (headless) mode + +Run Copilot CLI without interaction for use in Docker, CI/CD, or scripts: + +```shell +ollama launch copilot --model kimi-k2.5:cloud --yes -- -p "how does this repository work?" +``` + +The `--yes` flag auto-pulls the model, skips selectors, and requires `--model` to be specified. Arguments after `--` are passed directly to Copilot CLI. + +## Manual setup + +Copilot CLI connects to Ollama using the OpenAI-compatible API via environment variables. + +1. Set the environment variables: + +```shell +export COPILOT_PROVIDER_BASE_URL=http://localhost:11434/v1 +export COPILOT_PROVIDER_API_KEY= +export COPILOT_PROVIDER_WIRE_API=responses +export COPILOT_MODEL=qwen3.5 +``` + +1. Run Copilot CLI: + +```shell +copilot +``` + +Or run with environment variables inline: + +```shell +COPILOT_PROVIDER_BASE_URL=http://localhost:11434/v1 COPILOT_PROVIDER_API_KEY= COPILOT_PROVIDER_WIRE_API=responses COPILOT_MODEL=glm-5:cloud copilot +``` + +**Note:** Copilot requires a large context window. We recommend at least 64k tokens. See the [context length documentation](/context-length) for how to adjust context length in Ollama. diff --git a/docs/integrations/index.mdx b/docs/integrations/index.mdx index 2703fc0e2..9bb9d33b5 100644 --- a/docs/integrations/index.mdx +++ b/docs/integrations/index.mdx @@ -10,6 +10,7 @@ Coding assistants that can read, modify, and execute code in your projects. - [Claude Code](/integrations/claude-code) - [Codex](/integrations/codex) +- [Copilot CLI](/integrations/copilot-cli) - [OpenCode](/integrations/opencode) - [Droid](/integrations/droid) - [Goose](/integrations/goose)