diff options
| author | paul@buetow.org <paul@buetow.org> | 2026-02-06 16:35:45 +0200 |
|---|---|---|
| committer | paul@buetow.org <paul@buetow.org> | 2026-02-06 16:35:45 +0200 |
| commit | e58ae44d26a26d38150ced1ac09f69a589d96f49 (patch) | |
| tree | 5e9ae4fbd1696d1b668dfe0be791004a87fc7a6a /internal | |
| parent | 356fd474e9454f21853417428df05d99dbcf5c89 (diff) | |
Remove GitHub Copilot provider support
Remove all GitHub Copilot integration from the codebase to streamline
the supported provider set to OpenAI, OpenRouter, Anthropic, and Ollama.
Changes:
- Delete core Copilot implementation (copilot.go) and all related tests
- Remove Copilot configuration fields from App struct and Config
- Remove Copilot from provider factory and API key handling
- Update all test files to replace Copilot references with other providers
- Remove Copilot documentation from README, configuration guide, and examples
- Remove Copilot section from config.toml.example
All tests pass successfully after removal.
Co-authored-by: Cursor <cursoragent@cursor.com>
Diffstat (limited to 'internal')
23 files changed, 52 insertions, 909 deletions
diff --git a/internal/appconfig/config.go b/internal/appconfig/config.go index c9af85e..3077d42 100644 --- a/internal/appconfig/config.go +++ b/internal/appconfig/config.go @@ -69,12 +69,8 @@ type App struct { OllamaModel string `json:"ollama_model" toml:"ollama_model"` // Default temperature for Ollama requests (nil means use provider default) OllamaTemperature *float64 `json:"ollama_temperature" toml:"ollama_temperature"` - CopilotBaseURL string `json:"copilot_base_url" toml:"copilot_base_url"` - CopilotModel string `json:"copilot_model" toml:"copilot_model"` - // Default temperature for Copilot requests (nil means use provider default) - CopilotTemperature *float64 `json:"copilot_temperature" toml:"copilot_temperature"` - AnthropicBaseURL string `json:"anthropic_base_url" toml:"anthropic_base_url"` - AnthropicModel string `json:"anthropic_model" toml:"anthropic_model"` + AnthropicBaseURL string `json:"anthropic_base_url" toml:"anthropic_base_url"` + AnthropicModel string `json:"anthropic_model" toml:"anthropic_model"` // Default temperature for Anthropic requests (nil means use provider default) AnthropicTemperature *float64 `json:"anthropic_temperature" toml:"anthropic_temperature"` @@ -146,7 +142,6 @@ func newDefaultConfig() App { CodingTemperature: &t, OpenAITemperature: &t, OllamaTemperature: &t, - CopilotTemperature: &t, AnthropicTemperature: &t, ManualInvokeMinPrefix: 0, CompletionDebounceMs: 800, @@ -244,7 +239,6 @@ type fileConfig struct { Provider sectionProvider `toml:"provider"` OpenAI sectionOpenAI `toml:"openai"` OpenRouter sectionOpenRouter `toml:"openrouter"` - Copilot sectionCopilot `toml:"copilot"` Ollama sectionOllama `toml:"ollama"` Anthropic sectionAnthropic `toml:"anthropic"` Prompts sectionPrompts `toml:"prompts"` @@ -333,12 +327,6 @@ type sectionOpenRouter struct { Temperature *float64 `toml:"temperature"` } -type sectionCopilot struct { - Model string `toml:"model"` - BaseURL string `toml:"base_url"` - Temperature *float64 `toml:"temperature"` -} - type sectionOllama struct { Model string `toml:"model"` BaseURL string `toml:"base_url"` @@ -489,16 +477,6 @@ func (fc *fileConfig) toApp() App { out.mergeProviderFields(&tmp) } - // copilot - if (fc.Copilot != sectionCopilot{}) || fc.Copilot.Temperature != nil { - tmp := App{ - CopilotBaseURL: fc.Copilot.BaseURL, - CopilotModel: fc.Copilot.Model, - CopilotTemperature: fc.Copilot.Temperature, - } - out.mergeProviderFields(&tmp) - } - // ollama if (fc.Ollama != sectionOllama{}) || fc.Ollama.Temperature != nil { tmp := App{ @@ -658,10 +636,9 @@ func loadFromFile(path string, logger *log.Logger) (*App, error) { "chat_suffix": {}, "chat_prefixes": {}, "coding_temperature": {}, "provider": {}, "openai_model": {}, "openai_base_url": {}, "openai_temperature": {}, "ollama_model": {}, "ollama_base_url": {}, "ollama_temperature": {}, - "copilot_model": {}, "copilot_base_url": {}, "copilot_temperature": {}, } for k := range raw { - if _, isTable := map[string]struct{}{"general": {}, "logging": {}, "completion": {}, "triggers": {}, "inline": {}, "chat": {}, "provider": {}, "models": {}, "openai": {}, "copilot": {}, "ollama": {}, "prompts": {}}[k]; isTable { + if _, isTable := map[string]struct{}{"general": {}, "logging": {}, "completion": {}, "triggers": {}, "inline": {}, "chat": {}, "provider": {}, "models": {}, "openai": {}, "ollama": {}, "prompts": {}}[k]; isTable { continue } if _, isLegacy := legacy[k]; isLegacy { @@ -1103,15 +1080,6 @@ func (a *App) mergeProviderFields(other *App) { if other.OllamaTemperature != nil { // allow explicit 0.0 a.OllamaTemperature = other.OllamaTemperature } - if s := strings.TrimSpace(other.CopilotBaseURL); s != "" { - a.CopilotBaseURL = s - } - if s := strings.TrimSpace(other.CopilotModel); s != "" { - a.CopilotModel = s - } - if other.CopilotTemperature != nil { // allow explicit 0.0 - a.CopilotTemperature = other.CopilotTemperature - } if s := strings.TrimSpace(other.AnthropicBaseURL); s != "" { a.AnthropicBaseURL = s } @@ -1331,19 +1299,6 @@ func loadFromEnv(logger *log.Logger) *App { any = true } - if s := getenv("HEXAI_COPILOT_BASE_URL"); s != "" { - out.CopilotBaseURL = s - any = true - } - if model, ok := pickModel("copilot", getenv("HEXAI_COPILOT_MODEL")); ok { - out.CopilotModel = model - any = true - } - if f, ok := parseFloatPtr("HEXAI_COPILOT_TEMPERATURE"); ok { - out.CopilotTemperature = f - any = true - } - if s := getenv("HEXAI_ANTHROPIC_BASE_URL"); s != "" { out.AnthropicBaseURL = s any = true diff --git a/internal/appconfig/config_env_model_test.go b/internal/appconfig/config_env_model_test.go index 7038819..e10fa5d 100644 --- a/internal/appconfig/config_env_model_test.go +++ b/internal/appconfig/config_env_model_test.go @@ -37,9 +37,9 @@ func TestEnv_ModelForce_OverridesProviderSpecific(t *testing.T) { } func TestEnv_SurfaceModelOverrides(t *testing.T) { - t.Setenv("HEXAI_MODEL_COMPLETION", "gpt-c") + t.Setenv("HEXAI_MODEL_COMPLETION", "claude-c") t.Setenv("HEXAI_TEMPERATURE_COMPLETION", "0.44") - t.Setenv("HEXAI_PROVIDER_COMPLETION", "copilot") + t.Setenv("HEXAI_PROVIDER_COMPLETION", "anthropic") t.Setenv("HEXAI_MODEL_CLI", "gpt-cli") t.Setenv("HEXAI_TEMPERATURE_CLI", "0.22") t.Setenv("HEXAI_PROVIDER_CLI", "ollama") @@ -48,13 +48,13 @@ func TestEnv_SurfaceModelOverrides(t *testing.T) { t.Fatalf("expected single completion entry, got %+v", cfg.CompletionConfigs) } comp := cfg.CompletionConfigs[0] - if comp.Model != "gpt-c" { + if comp.Model != "claude-c" { t.Fatalf("expected completion model override, got %+v", comp) } if comp.Temperature == nil || *comp.Temperature != 0.44 { t.Fatalf("expected completion temperature override, got %+v", comp) } - if comp.Provider != "copilot" { + if comp.Provider != "anthropic" { t.Fatalf("expected completion provider override, got %+v", comp) } if len(cfg.CLIConfigs) != 1 { diff --git a/internal/appconfig/config_test.go b/internal/appconfig/config_test.go index 2c00f68..ff9616b 100644 --- a/internal/appconfig/config_test.go +++ b/internal/appconfig/config_test.go @@ -75,8 +75,8 @@ func TestParseSurfaceModels_CodeActionWarns(t *testing.T) { model = "gpt-4o" [[models.code_action]] - provider = "copilot" - model = "cpt" + provider = "anthropic" + model = "claude" `) var buf bytes.Buffer logger := log.New(&buf, "", 0) @@ -121,9 +121,9 @@ model = "gpt-file-complete" provider = "openai" [[models.code_action]] -model = "gpt-file-action" +model = "claude-file-action" temperature = 0.45 -provider = "copilot" +provider = "anthropic" [[models.chat]] model = "gpt-file-chat" @@ -146,11 +146,6 @@ temperature = 0.0 base_url = "http://ollama" model = "llama" temperature = 0.0 - -[copilot] -base_url = "http://copilot" -model = "ghost" -temperature = 0.0 `) if _, err := loadFromFile(cfgPath, newLogger()); err != nil { @@ -175,18 +170,15 @@ temperature = 0.0 withEnv(t, "HEXAI_OLLAMA_BASE_URL", "http://ollama-override") withEnv(t, "HEXAI_OLLAMA_MODEL", "mistral") withEnv(t, "HEXAI_OLLAMA_TEMPERATURE", "0.6") - withEnv(t, "HEXAI_COPILOT_BASE_URL", "http://copilot-override") - withEnv(t, "HEXAI_COPILOT_MODEL", "ghost-override") - withEnv(t, "HEXAI_COPILOT_TEMPERATURE", "0.3") withEnv(t, "HEXAI_MODEL_COMPLETION", "env-completion") withEnv(t, "HEXAI_TEMPERATURE_COMPLETION", "0.33") - withEnv(t, "HEXAI_PROVIDER_COMPLETION", "copilot") + withEnv(t, "HEXAI_PROVIDER_COMPLETION", "ollama") withEnv(t, "HEXAI_MODEL_CODE_ACTION", "env-action") withEnv(t, "HEXAI_TEMPERATURE_CODE_ACTION", "0.55") withEnv(t, "HEXAI_PROVIDER_CODE_ACTION", "openai") withEnv(t, "HEXAI_MODEL_CHAT", "env-chat") withEnv(t, "HEXAI_TEMPERATURE_CHAT", "0.66") - withEnv(t, "HEXAI_PROVIDER_CHAT", "copilot") + withEnv(t, "HEXAI_PROVIDER_CHAT", "anthropic") withEnv(t, "HEXAI_MODEL_CLI", "env-cli") withEnv(t, "HEXAI_TEMPERATURE_CLI", "0.77") withEnv(t, "HEXAI_PROVIDER_CLI", "ollama") @@ -217,16 +209,13 @@ temperature = 0.0 if cfg.OllamaBaseURL != "http://ollama-override" || cfg.OllamaModel != "mistral" || cfg.OllamaTemperature == nil || *cfg.OllamaTemperature != 0.6 { t.Fatalf("ollama overrides not applied: %+v", cfg) } - if cfg.CopilotBaseURL != "http://copilot-override" || cfg.CopilotModel != "ghost-override" || cfg.CopilotTemperature == nil || *cfg.CopilotTemperature != 0.3 { - t.Fatalf("copilot overrides not applied: %+v", cfg) - } if len(cfg.CompletionConfigs) != 1 || cfg.CompletionConfigs[0].Model != "env-completion" { t.Fatalf("completion overrides not applied: %+v", cfg.CompletionConfigs) } if cfg.CompletionConfigs[0].Temperature == nil || *cfg.CompletionConfigs[0].Temperature != 0.33 { t.Fatalf("completion temperature override missing: %+v", cfg.CompletionConfigs[0]) } - if cfg.CompletionConfigs[0].Provider != "copilot" { + if cfg.CompletionConfigs[0].Provider != "ollama" { t.Fatalf("completion provider override not applied: %+v", cfg.CompletionConfigs[0]) } if len(cfg.CodeActionConfigs) != 1 || cfg.CodeActionConfigs[0].Model != "env-action" { @@ -244,7 +233,7 @@ temperature = 0.0 if cfg.ChatConfigs[0].Temperature == nil || *cfg.ChatConfigs[0].Temperature != 0.66 { t.Fatalf("chat temp override missing: %+v", cfg.ChatConfigs[0]) } - if cfg.ChatConfigs[0].Provider != "copilot" { + if cfg.ChatConfigs[0].Provider != "anthropic" { t.Fatalf("chat provider override not applied: %+v", cfg.ChatConfigs[0]) } if len(cfg.CLIConfigs) != 1 || cfg.CLIConfigs[0].Model != "env-cli" { @@ -260,7 +249,7 @@ temperature = 0.0 // Ensure file values would have applied absent env // Spot-check: reset env and reload for _, k := range []string{ - "HEXAI_MAX_TOKENS", "HEXAI_CONTEXT_MODE", "HEXAI_CONTEXT_WINDOW_LINES", "HEXAI_MAX_CONTEXT_TOKENS", "HEXAI_LOG_PREVIEW_LIMIT", "HEXAI_CODING_TEMPERATURE", "HEXAI_MANUAL_INVOKE_MIN_PREFIX", "HEXAI_COMPLETION_DEBOUNCE_MS", "HEXAI_COMPLETION_THROTTLE_MS", "HEXAI_TRIGGER_CHARACTERS", "HEXAI_PROVIDER", "HEXAI_OPENAI_BASE_URL", "HEXAI_OPENAI_MODEL", "HEXAI_OPENAI_TEMPERATURE", "HEXAI_OLLAMA_BASE_URL", "HEXAI_OLLAMA_MODEL", "HEXAI_OLLAMA_TEMPERATURE", "HEXAI_COPILOT_BASE_URL", "HEXAI_COPILOT_MODEL", "HEXAI_COPILOT_TEMPERATURE", "HEXAI_MODEL_COMPLETION", "HEXAI_TEMPERATURE_COMPLETION", "HEXAI_MODEL_CODE_ACTION", "HEXAI_TEMPERATURE_CODE_ACTION", "HEXAI_MODEL_CHAT", "HEXAI_TEMPERATURE_CHAT", "HEXAI_MODEL_CLI", "HEXAI_TEMPERATURE_CLI", "HEXAI_PROVIDER_COMPLETION", "HEXAI_PROVIDER_CODE_ACTION", "HEXAI_PROVIDER_CHAT", "HEXAI_PROVIDER_CLI", + "HEXAI_MAX_TOKENS", "HEXAI_CONTEXT_MODE", "HEXAI_CONTEXT_WINDOW_LINES", "HEXAI_MAX_CONTEXT_TOKENS", "HEXAI_LOG_PREVIEW_LIMIT", "HEXAI_CODING_TEMPERATURE", "HEXAI_MANUAL_INVOKE_MIN_PREFIX", "HEXAI_COMPLETION_DEBOUNCE_MS", "HEXAI_COMPLETION_THROTTLE_MS", "HEXAI_TRIGGER_CHARACTERS", "HEXAI_PROVIDER", "HEXAI_OPENAI_BASE_URL", "HEXAI_OPENAI_MODEL", "HEXAI_OPENAI_TEMPERATURE", "HEXAI_OLLAMA_BASE_URL", "HEXAI_OLLAMA_MODEL", "HEXAI_OLLAMA_TEMPERATURE", "HEXAI_MODEL_COMPLETION", "HEXAI_TEMPERATURE_COMPLETION", "HEXAI_MODEL_CODE_ACTION", "HEXAI_TEMPERATURE_CODE_ACTION", "HEXAI_MODEL_CHAT", "HEXAI_TEMPERATURE_CHAT", "HEXAI_MODEL_CLI", "HEXAI_TEMPERATURE_CLI", "HEXAI_PROVIDER_COMPLETION", "HEXAI_PROVIDER_CODE_ACTION", "HEXAI_PROVIDER_CHAT", "HEXAI_PROVIDER_CLI", } { t.Setenv(k, "") } @@ -283,13 +272,13 @@ temperature = 0.0 if cfg2.CompletionConfigs[0].Provider != "openai" { t.Fatalf("file merge (completion provider) not applied: %+v", cfg2.CompletionConfigs[0]) } - if len(cfg2.CodeActionConfigs) != 1 || cfg2.CodeActionConfigs[0].Model != "gpt-file-action" { + if len(cfg2.CodeActionConfigs) != 1 || cfg2.CodeActionConfigs[0].Model != "claude-file-action" { t.Fatalf("file merge (code action) not applied: %+v", cfg2.CodeActionConfigs) } if cfg2.CodeActionConfigs[0].Temperature == nil || *cfg2.CodeActionConfigs[0].Temperature != 0.45 { t.Fatalf("expected code action temp 0.45, got %+v", cfg2.CodeActionConfigs[0]) } - if cfg2.CodeActionConfigs[0].Provider != "copilot" { + if cfg2.CodeActionConfigs[0].Provider != "anthropic" { t.Fatalf("file merge (code action provider) not applied: %+v", cfg2.CodeActionConfigs[0]) } if len(cfg2.ChatConfigs) != 1 || cfg2.ChatConfigs[0].Model != "gpt-file-chat" { @@ -384,11 +373,6 @@ temperature = 0.0 model = "mistral" base_url = "http://ollama" temperature = 0.0 - -[copilot] -model = "ghost" -base_url = "http://copilot" -temperature = 0.0 ` writeFile(t, cfgPath, content) @@ -418,9 +402,6 @@ temperature = 0.0 if cfg.OllamaModel != "mistral" || cfg.OllamaBaseURL != "http://ollama" || cfg.OllamaTemperature == nil || *cfg.OllamaTemperature != 0.0 { t.Fatalf("sectioned ollama wrong: %+v", cfg) } - if cfg.CopilotModel != "ghost" || cfg.CopilotBaseURL != "http://copilot" || cfg.CopilotTemperature == nil || *cfg.CopilotTemperature != 0.0 { - t.Fatalf("sectioned copilot wrong: %+v", cfg) - } } func TestLoad_FileTables_Prompts_AllSections(t *testing.T) { diff --git a/internal/hexaiaction/prompts.go b/internal/hexaiaction/prompts.go index a113391..fc743a0 100644 --- a/internal/hexaiaction/prompts.go +++ b/internal/hexaiaction/prompts.go @@ -49,8 +49,8 @@ func defaultModelForProvider(cfg appconfig.App, provider string) string { switch provider { case "ollama": return cfg.OllamaModel - case "copilot": - return cfg.CopilotModel + case "anthropic": + return cfg.AnthropicModel default: return cfg.OpenAIModel } diff --git a/internal/hexaiaction/prompts_more_test.go b/internal/hexaiaction/prompts_more_test.go index cfccd0c..a4410e5 100644 --- a/internal/hexaiaction/prompts_more_test.go +++ b/internal/hexaiaction/prompts_more_test.go @@ -35,8 +35,8 @@ func TestReqOptsFrom_Override(t *testing.T) { cfg := appconfig.App{ MaxTokens: 123, Provider: "openai", - CopilotModel: "gpt-4o", - CodeActionConfigs: []appconfig.SurfaceConfig{{Provider: "copilot", Model: "override", Temperature: ptrFloat(0.6)}}, + AnthropicModel: "claude-3-5-sonnet", + CodeActionConfigs: []appconfig.SurfaceConfig{{Provider: "anthropic", Model: "override", Temperature: ptrFloat(0.6)}}, } req := reqOptsFrom(cfg) if req.model != "override" { diff --git a/internal/hexaiaction/run_more_test.go b/internal/hexaiaction/run_more_test.go index a3e7f25..57bd933 100644 --- a/internal/hexaiaction/run_more_test.go +++ b/internal/hexaiaction/run_more_test.go @@ -14,7 +14,7 @@ import ( // Covers the early error path in Run when no API key is available for the default provider. func TestRun_MissingAPIKey(t *testing.T) { // Ensure no provider API keys in env - for _, k := range []string{"HEXAI_OPENAI_API_KEY", "OPENAI_API_KEY", "HEXAI_COPILOT_API_KEY", "COPILOT_API_KEY"} { + for _, k := range []string{"HEXAI_OPENAI_API_KEY", "OPENAI_API_KEY"} { t.Setenv(k, "") } // Provide minimal stdin to get past empty input check (if reached) diff --git a/internal/hexaicli/run.go b/internal/hexaicli/run.go index 7b360e9..9ea3a40 100644 --- a/internal/hexaicli/run.go +++ b/internal/hexaicli/run.go @@ -77,10 +77,6 @@ func buildCLIJobs(cfg appconfig.App) ([]cliJob, error) { if entry.Model != "" { derived.OpenAIModel = entry.Model } - case "copilot": - if entry.Model != "" { - derived.CopilotModel = entry.Model - } case "ollama": if entry.Model != "" { derived.OllamaModel = entry.Model @@ -151,8 +147,8 @@ func defaultModelForProvider(cfg appconfig.App, provider string) string { switch provider { case "ollama": return cfg.OllamaModel - case "copilot": - return cfg.CopilotModel + case "anthropic": + return cfg.AnthropicModel default: return cfg.OpenAIModel } diff --git a/internal/hexaicli/run_test.go b/internal/hexaicli/run_test.go index 43576cf..34a5c51 100644 --- a/internal/hexaicli/run_test.go +++ b/internal/hexaicli/run_test.go @@ -161,11 +161,11 @@ func TestPrintProviderInfo(t *testing.T) { func TestBuildCLIRequest_Override(t *testing.T) { cfg := appconfig.App{ - Provider: "openai", - CopilotModel: "gpt-4o", + Provider: "openai", + AnthropicModel: "claude-3-5-sonnet", } - entry := appconfig.SurfaceConfig{Provider: "copilot", Model: "override", Temperature: floatPtr(0.7)} - req := buildCLIRequest(entry, "copilot", cfg, &fakeClient{name: "copilot", model: "default"}) + entry := appconfig.SurfaceConfig{Provider: "anthropic", Model: "override", Temperature: floatPtr(0.7)} + req := buildCLIRequest(entry, "anthropic", cfg, &fakeClient{name: "anthropic", model: "default"}) if req.model != "override" { t.Fatalf("expected model override, got %q", req.model) } @@ -199,8 +199,8 @@ func TestBuildCLIJobs_MultiEntries(t *testing.T) { defer func() { newClientFromApp = old }() newClientFromApp = func(cfg appconfig.App) (llm.Client, error) { model := cfg.OpenAIModel - if cfg.Provider == "copilot" { - model = cfg.CopilotModel + if cfg.Provider == "anthropic" { + model = cfg.AnthropicModel } if cfg.Provider == "ollama" { model = cfg.OllamaModel @@ -215,7 +215,7 @@ func TestBuildCLIJobs_MultiEntries(t *testing.T) { OllamaModel: "llama3", CLIConfigs: []appconfig.SurfaceConfig{ {Provider: "openai", Model: "gpt-4o"}, - {Provider: "copilot", Model: "cpt"}, + {Provider: "anthropic", Model: "claude"}, }, } jobs, err := buildCLIJobs(cfg) @@ -228,18 +228,18 @@ func TestBuildCLIJobs_MultiEntries(t *testing.T) { if jobs[0].provider != "openai" || jobs[0].req.model != "gpt-4o" { t.Fatalf("unexpected first job: %+v", jobs[0]) } - if jobs[1].provider != "copilot" || jobs[1].req.model != "cpt" { + if jobs[1].provider != "anthropic" || jobs[1].req.model != "claude" { t.Fatalf("unexpected second job: %+v", jobs[1]) } } func TestFilterJobsBySelection(t *testing.T) { - jobs := []cliJob{{index: 0, provider: "openai"}, {index: 1, provider: "ollama"}, {index: 2, provider: "copilot"}} + jobs := []cliJob{{index: 0, provider: "openai"}, {index: 1, provider: "ollama"}, {index: 2, provider: "anthropic"}} filtered, err := filterJobsBySelection(jobs, []int{2, 0}) if err != nil { t.Fatalf("unexpected error: %v", err) } - if len(filtered) != 2 || filtered[0].provider != "copilot" || filtered[1].provider != "openai" { + if len(filtered) != 2 || filtered[0].provider != "anthropic" || filtered[1].provider != "openai" { t.Fatalf("unexpected filtered order: %+v", filtered) } if filtered[0].index != 0 || filtered[1].index != 1 { diff --git a/internal/hexaicli/testhelpers_test.go b/internal/hexaicli/testhelpers_test.go index 8f6863d..3197880 100644 --- a/internal/hexaicli/testhelpers_test.go +++ b/internal/hexaicli/testhelpers_test.go @@ -63,7 +63,6 @@ func (s *fakeStreamer) ChatStream(ctx context.Context, messages []llm.Message, o return nil } - func writeConfigString(t *testing.T, path string, contents string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { diff --git a/internal/hexailsp/run.go b/internal/hexailsp/run.go index 47ed648..e2aaf9d 100644 --- a/internal/hexailsp/run.go +++ b/internal/hexailsp/run.go @@ -119,9 +119,6 @@ func buildClientIfNil(cfg appconfig.App, client llm.Client) llm.Client { OllamaBaseURL: cfg.OllamaBaseURL, OllamaModel: cfg.OllamaModel, OllamaTemperature: cfg.OllamaTemperature, - CopilotBaseURL: cfg.CopilotBaseURL, - CopilotModel: cfg.CopilotModel, - CopilotTemperature: cfg.CopilotTemperature, AnthropicBaseURL: cfg.AnthropicBaseURL, AnthropicModel: cfg.AnthropicModel, AnthropicTemperature: cfg.AnthropicTemperature, @@ -136,17 +133,12 @@ func buildClientIfNil(cfg appconfig.App, client llm.Client) llm.Client { if strings.TrimSpace(orKey) == "" { orKey = os.Getenv("OPENROUTER_API_KEY") } - // Prefer HEXAI_COPILOT_API_KEY; fall back to COPILOT_API_KEY - cpKey := os.Getenv("HEXAI_COPILOT_API_KEY") - if strings.TrimSpace(cpKey) == "" { - cpKey = os.Getenv("COPILOT_API_KEY") - } // Prefer HEXAI_ANTHROPIC_API_KEY; fall back to ANTHROPIC_API_KEY anKey := os.Getenv("HEXAI_ANTHROPIC_API_KEY") if strings.TrimSpace(anKey) == "" { anKey = os.Getenv("ANTHROPIC_API_KEY") } - if c, err := llm.NewFromConfig(llmCfg, oaKey, orKey, cpKey, anKey); err != nil { + if c, err := llm.NewFromConfig(llmCfg, oaKey, orKey, anKey); err != nil { logging.Logf("lsp ", "llm disabled: %v", err) return nil } else { diff --git a/internal/llm/copilot.go b/internal/llm/copilot.go deleted file mode 100644 index 43419ea..0000000 --- a/internal/llm/copilot.go +++ /dev/null @@ -1,412 +0,0 @@ -// Summary: GitHub Copilot client for chat and Codex-style code completion. -package llm - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "regexp" - "strings" - "time" - - appver "codeberg.org/snonux/hexai/internal" - "codeberg.org/snonux/hexai/internal/logging" -) - -// copilotClient implements Client against GitHub Copilot's Chat Completions API. -type copilotClient struct { - httpClient *http.Client - apiKey string - baseURL string - defaultModel string - chatLogger logging.ChatLogger - defaultTemperature *float64 - - // cached Copilot session token retrieved from GitHub API using apiKey - sessionToken string - tokenExpiry time.Time -} - -type copilotChatRequest struct { - Model string `json:"model"` - Messages []copilotMessage `json:"messages"` - Temperature *float64 `json:"temperature,omitempty"` - MaxTokens *int `json:"max_tokens,omitempty"` - Stop []string `json:"stop,omitempty"` -} - -type copilotMessage struct { - Role string `json:"role"` - Content string `json:"content"` -} - -type copilotChatResponse struct { - Choices []struct { - Index int `json:"index"` - Message struct { - Role string `json:"role"` - Content string `json:"content"` - } `json:"message"` - FinishReason string `json:"finish_reason"` - } `json:"choices"` - Error *struct { - Message string `json:"message"` - Type string `json:"type"` - Param any `json:"param"` - Code any `json:"code"` - } `json:"error,omitempty"` -} - -// Constructor (kept among the first functions by convention) -func newCopilot(baseURL, model, apiKey string, defaultTemp *float64) Client { - return newCopilotWithTimeout(baseURL, model, apiKey, defaultTemp, 0) -} - -func newCopilotWithTimeout(baseURL, model, apiKey string, defaultTemp *float64, timeoutSec int) Client { - if strings.TrimSpace(baseURL) == "" { - baseURL = "https://api.githubcopilot.com" - } - if strings.TrimSpace(model) == "" { - // GitHub Models (Copilot API) commonly supports gpt-4o/gpt-4o-mini. - // Default to a broadly available, cost-effective option. - model = "gpt-4o-mini" - } - if timeoutSec <= 0 { - timeoutSec = 30 - } - return copilotClient{ - httpClient: &http.Client{Timeout: time.Duration(timeoutSec) * time.Second}, - apiKey: apiKey, - baseURL: strings.TrimRight(baseURL, "/"), - defaultModel: model, - chatLogger: logging.NewChatLogger("copilot"), - defaultTemperature: defaultTemp, - } -} - -func (c copilotClient) Chat(ctx context.Context, messages []Message, opts ...RequestOption) (string, error) { - if strings.TrimSpace(c.apiKey) == "" { - return nilStringErr("missing Copilot API key") - } - // Ensure we have a fresh session token - if err := c.ensureSession(ctx); err != nil { - return "", err - } - o := Options{Model: c.defaultModel} - for _, opt := range opts { - opt(&o) - } - if o.Model == "" { - o.Model = c.defaultModel - } - start := time.Now() - logMessages := make([]struct{ Role, Content string }, len(messages)) - for i, m := range messages { - logMessages[i] = struct{ Role, Content string }{m.Role, m.Content} - } - c.chatLogger.LogStart(false, o.Model, o.Temperature, o.MaxTokens, o.Stop, logMessages) - - req := buildCopilotChatRequest(o, messages, c.defaultTemperature) - body, err := json.Marshal(req) - if err != nil { - logging.Logf("llm/copilot ", "marshal error: %v", err) - return "", err - } - - endpoint := c.baseURL + "/chat/completions" - logging.Logf("llm/copilot ", "POST %s", endpoint) - resp, err := c.postJSON(ctx, endpoint, body, c.headersChat()) - if err != nil { - logging.Logf("llm/copilot ", "%shttp error after %s: %v%s", logging.AnsiRed, time.Since(start), err, logging.AnsiBase) - return "", err - } - defer func() { - if err := resp.Body.Close(); err != nil { - logging.Logf("llm/copilot", "failed to close response body: %v", err) - } - }() - if err := handleCopilotNon2xx(resp, start); err != nil { - return "", err - } - out, err := decodeCopilotChat(resp, start) - if err != nil { - return "", err - } - if len(out.Choices) == 0 { - logging.Logf("llm/copilot ", "%sno choices returned duration=%s%s", logging.AnsiRed, time.Since(start), logging.AnsiBase) - return "", errors.New("copilot: no choices returned") - } - content := out.Choices[0].Message.Content - logging.Logf("llm/copilot ", "success choice=0 finish=%s size=%d preview=%s%s%s duration=%s", out.Choices[0].FinishReason, len(content), logging.AnsiGreen, logging.PreviewForLog(content), logging.AnsiBase, time.Since(start)) - return content, nil -} - -// Provider metadata -func (c copilotClient) Name() string { return "copilot" } -func (c copilotClient) DefaultModel() string { return c.defaultModel } - -// helpers -func buildCopilotChatRequest(o Options, messages []Message, defaultTemp *float64) copilotChatRequest { - req := copilotChatRequest{Model: o.Model} - req.Messages = make([]copilotMessage, len(messages)) - for i, m := range messages { - req.Messages[i] = copilotMessage(m) - } - if o.Temperature != 0 { - req.Temperature = &o.Temperature - } else if defaultTemp != nil { - t := *defaultTemp - req.Temperature = &t - } - if o.MaxTokens > 0 { - req.MaxTokens = &o.MaxTokens - } - if len(o.Stop) > 0 { - req.Stop = o.Stop - } - return req -} - -func (c copilotClient) postJSON(ctx context.Context, url string, body []byte, headers map[string]string) (*http.Response, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) - if err != nil { - return nil, err - } - for k, v := range headers { - req.Header.Set(k, v) - } - return c.httpClient.Do(req) -} - -func handleCopilotNon2xx(resp *http.Response, start time.Time) error { - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - return nil - } - var apiErr copilotChatResponse - _ = json.NewDecoder(resp.Body).Decode(&apiErr) - if apiErr.Error != nil && strings.TrimSpace(apiErr.Error.Message) != "" { - logging.Logf("llm/copilot ", "%sapi error status=%d type=%s msg=%s duration=%s%s", logging.AnsiRed, resp.StatusCode, apiErr.Error.Type, apiErr.Error.Message, time.Since(start), logging.AnsiBase) - return fmt.Errorf("copilot error: %s (status %d)", apiErr.Error.Message, resp.StatusCode) - } - logging.Logf("llm/copilot ", "%shttp non-2xx status=%d duration=%s%s", logging.AnsiRed, resp.StatusCode, time.Since(start), logging.AnsiBase) - return fmt.Errorf("copilot http error: status %d", resp.StatusCode) -} - -func decodeCopilotChat(resp *http.Response, start time.Time) (copilotChatResponse, error) { - var out copilotChatResponse - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - logging.Logf("llm/copilot ", "%sdecode error after %s: %v%s", logging.AnsiRed, time.Since(start), err, logging.AnsiBase) - return copilotChatResponse{}, err - } - return out, nil -} - -// --- Copilot session token management --- - -type ghCopilotTokenResp struct { - Token string `json:"token"` -} - -func (c *copilotClient) ensureSession(ctx context.Context) error { - // If token valid for >60s, reuse - if c.sessionToken != "" && time.Now().Add(60*time.Second).Before(c.tokenExpiry) { - return nil - } - if strings.TrimSpace(c.apiKey) == "" { - return errors.New("missing Copilot API key") - } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/copilot_internal/v2/token", nil) - if err != nil { - return err - } - req.Header.Set("Authorization", "Bearer "+c.apiKey) - req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", "hexai/"+appver.Version) - resp, err := c.httpClient.Do(req) - if err != nil { - return err - } - defer func() { - if err := resp.Body.Close(); err != nil { - logging.Logf("llm/copilot", "failed to close response body: %v", err) - } - }() - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Errorf("copilot token http error: %d", resp.StatusCode) - } - var out ghCopilotTokenResp - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return err - } - if strings.TrimSpace(out.Token) == "" { - return errors.New("empty copilot session token") - } - // Parse JWT exp - exp := parseJWTExp(out.Token) - if exp.IsZero() { - exp = time.Now().Add(10 * time.Minute) - } - c.sessionToken = out.Token - c.tokenExpiry = exp - return nil -} - -var jwtExpRe = regexp.MustCompile(`"exp"\s*:\s*([0-9]+)`) // fallback if we can't base64 decode - -func parseJWTExp(token string) time.Time { - parts := strings.Split(token, ".") - if len(parts) < 2 { - return time.Time{} - } - b, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - if m := jwtExpRe.FindStringSubmatch(token); len(m) == 2 { - if n, err2 := parseInt64(m[1]); err2 == nil { - return time.Unix(n, 0) - } - } - return time.Time{} - } - var payload struct { - Exp int64 `json:"exp"` - } - _ = json.Unmarshal(b, &payload) - if payload.Exp == 0 { - return time.Time{} - } - return time.Unix(payload.Exp, 0) -} - -func parseInt64(s string) (int64, error) { var n int64; _, err := fmt.Sscan(s, &n); return n, err } - -// --- Copilot headers --- - -func (c *copilotClient) headersChat() map[string]string { - _ = c.ensureSession(context.Background()) - h := map[string]string{ - "Content-Type": "application/json; charset=utf-8", - "Accept": "application/json", - "Authorization": "Bearer " + c.sessionToken, - "User-Agent": "GitHubCopilotChat/0.8.0", - "Editor-Plug |
