summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorpaul@buetow.org <paul@buetow.org>2026-02-06 16:35:45 +0200
committerpaul@buetow.org <paul@buetow.org>2026-02-06 16:35:45 +0200
commit12a249282d5dd9dc2ee1e66f08d6acc26dd29eba (patch)
tree5e9ae4fbd1696d1b668dfe0be791004a87fc7a6a /internal
parent89dc2aab0b6be2620766a4b4b750fa888641b89d (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')
-rw-r--r--internal/appconfig/config.go51
-rw-r--r--internal/appconfig/config_env_model_test.go8
-rw-r--r--internal/appconfig/config_test.go41
-rw-r--r--internal/hexaiaction/prompts.go4
-rw-r--r--internal/hexaiaction/prompts_more_test.go4
-rw-r--r--internal/hexaiaction/run_more_test.go2
-rw-r--r--internal/hexaicli/run.go8
-rw-r--r--internal/hexaicli/run_test.go20
-rw-r--r--internal/hexaicli/testhelpers_test.go1
-rw-r--r--internal/hexailsp/run.go10
-rw-r--r--internal/llm/copilot.go412
-rw-r--r--internal/llm/copilot_http_test.go276
-rw-r--r--internal/llm/copilot_test.go35
-rw-r--r--internal/llm/openai_temp_test.go6
-rw-r--r--internal/llm/provider.go15
-rw-r--r--internal/llm/provider_more2_test.go12
-rw-r--r--internal/llm/provider_more_test.go10
-rw-r--r--internal/llm/provider_test.go8
-rw-r--r--internal/llmutils/client.go9
-rw-r--r--internal/lsp/handlers_utils.go2
-rw-r--r--internal/lsp/llm_request_opts_test.go8
-rw-r--r--internal/lsp/server.go15
-rw-r--r--internal/runtimeconfig/store_test.go4
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-Plugin-Version": "copilot-chat/0.8.0",
- "Editor-Version": "vscode/1.85.1",
- "Openai-Intent": "conversation-panel",
- "Openai-Organization": "github-copilot",
- "VScode-MachineId": randHex(64),
- "VScode-SessionId": randHex(8) + "-" + randHex(4) + "-" + randHex(4) + "-" + randHex(4) + "-" + randHex(12),
- "X-Request-Id": randHex(8) + "-" + randHex(4) + "-" + randHex(4) + "-" + randHex(4) + "-" + randHex(12),
- }
- return h
-}
-
-func (c *copilotClient) headersGhost() map[string]string {
- _ = c.ensureSession(context.Background())
- h := map[string]string{
- "Content-Type": "application/json; charset=utf-8",
- "Accept": "*/*",
- "Authorization": "Bearer " + c.sessionToken,
- "User-Agent": "GithubCopilot/1.155.0",
- "Editor-Plugin-Version": "copilot/1.155.0",
- "Editor-Version": "vscode/1.85.1",
- "Openai-Intent": "copilot-ghost",
- "Openai-Organization": "github-copilot",
- "VScode-MachineId": randHex(64),
- "VScode-SessionId": randHex(8) + "-" + randHex(4) + "-" + randHex(4) + "-" + randHex(4) + "-" + randHex(12),
- "X-Request-Id": randHex(8) + "-" + randHex(4) + "-" + randHex(4) + "-" + randHex(4) + "-" + randHex(12),
- }
- return h
-}
-
-func randHex(n int) string {
- const hex = "0123456789abcdef"
- b := make([]byte, n)
- for i := range b {
- b[i] = hex[int(time.Now().UnixNano()+int64(i))%len(hex)]
- }
- return string(b)
-}
-
-// --- Codex-style code completion ---
-
-// CodeCompletion implements CodeCompleter; returns up to n suggestions.
-func (c copilotClient) CodeCompletion(ctx context.Context, prompt string, suffix string, n int, language string, temperature float64) ([]string, error) {
- if strings.TrimSpace(c.apiKey) == "" {
- return nil, errors.New("missing Copilot API key")
- }
- if err := c.ensureSession(ctx); err != nil {
- return nil, err
- }
- if n <= 0 {
- n = 1
- }
- maxTokens := 500
- body := map[string]any{
- "extra": map[string]any{
- "language": language,
- "next_indent": 0,
- "prompt_tokens": 500,
- "suffix_tokens": 400,
- "trim_by_indentation": true,
- },
- "max_tokens": maxTokens,
- "n": n,
- "nwo": "hexai",
- "prompt": prompt,
- "stop": []string{"\n\n"},
- "stream": true,
- "suffix": suffix,
- "temperature": temperature,
- "top_p": 1,
- }
- buf, _ := json.Marshal(body)
- url := "https://copilot-proxy.githubusercontent.com/v1/engines/copilot-codex/completions"
- resp, err := c.postJSON(ctx, url, buf, c.headersGhost())
- if err != nil {
- return nil, 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 nil, fmt.Errorf("copilot codex http error: %d", resp.StatusCode)
- }
- // Read all and parse lines that start with "data: " accumulating by index
- raw, _ := io.ReadAll(resp.Body)
- byIndex := make(map[int]string)
- lines := strings.Split(string(raw), "\n")
- for _, ln := range lines {
- if !strings.HasPrefix(ln, "data: ") {
- continue
- }
- var evt struct {
- Choices []struct {
- Index int `json:"index"`
- Text string `json:"text"`
- } `json:"choices"`
- }
- if err := json.Unmarshal([]byte(strings.TrimPrefix(ln, "data: ")), &evt); err != nil {
- continue
- }
- for _, ch := range evt.Choices {
- byIndex[ch.Index] += ch.Text
- }
- }
- out := make([]string, 0, len(byIndex))
- for i := 0; i < n; i++ {
- if s, ok := byIndex[i]; ok && strings.TrimSpace(s) != "" {
- out = append(out, s)
- }
- }
- return out, nil
-}
-
-// newLineDataReader wraps a streaming body and exposes a JSON decoder that
-// decodes successive objects from lines prefixed by "data: ".
-// (no streaming decoder needed; we parse whole body lines)
diff --git a/internal/llm/copilot_http_test.go b/internal/llm/copilot_http_test.go
deleted file mode 100644
index 1371f71..0000000
--- a/internal/llm/copilot_http_test.go
+++ /dev/null
@@ -1,276 +0,0 @@
-package llm
-
-import (
- "context"
- "encoding/base64"
- "encoding/json"
- "io"
- "net"
- "net/http"
- "net/http/httptest"
- "os"
- "strings"
- "testing"
- "time"
-)
-
-type rtFunc2 func(*http.Request) (*http.Response, error)
-
-func (f rtFunc2) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
-
-func TestCopilot_EnsureSession_AndChat_Success(t *testing.T) {
- if os.Getenv("HEXAI_TEST_SKIP_NET") == "1" {
- t.Skip("skip network-bound tests in restricted environments")
- }
- // Mock chat endpoint
- chatSrv := newIPv4Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if r.URL.Path != "/chat/completions" {
- t.Fatalf("unexpected path: %s", r.URL.Path)
- }
- _ = json.NewEncoder(w).Encode(map[string]any{"choices": []map[string]any{{"index": 0, "message": map[string]string{"role": "assistant", "content": "OK"}}}})
- }))
- defer chatSrv.Close()
- c := newCopilot(chatSrv.URL, "gpt-4o-mini", "APIKEY", f64p(0.1)).(copilotClient)
- // Intercept token endpoint to return a session token
- tr := rtFunc2(func(r *http.Request) (*http.Response, error) {
- if r.URL.Host == "api.github.com" && r.URL.Path == "/copilot_internal/v2/token" {
- rw := httptest.NewRecorder()
- _ = json.NewEncoder(rw).Encode(map[string]string{"token": "tok"})
- res := rw.Result()
- res.StatusCode = 200
- return res, nil
- }
- // Fallback to default transport for chatSrv
- return http.DefaultTransport.RoundTrip(r)
- })
- c.httpClient = &http.Client{Transport: tr, Timeout: 5 * time.Second}
- out, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}})
- if err != nil || out != "OK" {
- t.Fatalf("copilot chat failed: %v %q", err, out)
- }
-}
-
-func TestCopilot_HandleNon2xx(t *testing.T) {
- b, _ := json.Marshal(map[string]any{"error": map[string]any{"message": "bad", "type": "invalid"}})
- resp := &http.Response{StatusCode: 400, Body: io.NopCloser(bytesReader(b))}
- if err := handleCopilotNon2xx(resp, time.Now()); err == nil {
- t.Fatalf("expected error")
- }
-}
-
-func TestCopilot_CodeCompletion_Success(t *testing.T) {
- c := newCopilot("https://api.githubcopilot.com", "gpt-4o-mini", "API", f64p(0.1)).(copilotClient)
- tr := rtFunc2(func(r *http.Request) (*http.Response, error) {
- // Token endpoint
- if r.URL.Host == "api.github.com" && r.URL.Path == "/copilot_internal/v2/token" {
- rw := httptest.NewRecorder()
- _ = json.NewEncoder(rw).Encode(map[string]string{"token": "tok"})
- res := rw.Result()
- res.StatusCode = 200
- return res, nil
- }
- // Codex completion endpoint
- if r.URL.Host == "copilot-proxy.githubusercontent.com" && strings.HasSuffix(r.URL.Path, "/v1/engines/copilot-codex/completions") {
- rw := httptest.NewRecorder()
- // two choices for index 0 and 1
- _, _ = rw.WriteString("data: {\"choices\":[{\"index\":0,\"text\":\"A\"}]}\n")
- _, _ = rw.WriteString("data: {\"choices\":[{\"index\":1,\"text\":\"B\"}]}\n")
- res := rw.Result()
- res.StatusCode = 200
- return res, nil
- }
- return http.DefaultTransport.RoundTrip(r)
- })
- c.httpClient = &http.Client{Transport: tr, Timeout: 5 * time.Second}
- out, err := c.CodeCompletion(context.Background(), "p", "s", 2, "go", 0.1)
- if err != nil || len(out) != 2 || out[0] != "A" || out[1] != "B" {
- t.Fatalf("codex: %v %#v", err, out)
- }
-}
-
-func TestCopilot_Chat_MultiChoice_And_ErrorBody(t *testing.T) {
- if os.Getenv("HEXAI_TEST_SKIP_NET") == "1" {
- t.Skip("skip network-bound tests in restricted environments")
- }
- // Chat multi-choice: return two choices; client returns first content
- srv := newIPv4Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- _ = json.NewEncoder(w).Encode(map[string]any{
- "choices": []map[string]any{
- {"index": 0, "finish_reason": "stop", "message": map[string]string{"role": "assistant", "content": "FIRST"}},
- {"index": 1, "finish_reason": "length", "message": map[string]string{"role": "assistant", "content": "SECOND"}},
- },
- })
- }))
- defer srv.Close()
- c := newCopilot(srv.URL, "gpt-4o-mini", "KEY", f64p(0.1)).(copilotClient)
- // Token success
- tr := rtFunc2(func(r *http.Request) (*http.Response, error) {
- if r.URL.Host == "api.github.com" && r.URL.Path == "/copilot_internal/v2/token" {
- rw := httptest.NewRecorder()
- _ = json.NewEncoder(rw).Encode(map[string]string{"token": "tok"})
- res := rw.Result()
- res.StatusCode = 200
- return res, nil
- }
- return http.DefaultTransport.RoundTrip(r)
- })
- c.httpClient = &http.Client{Transport: tr, Timeout: 5 * time.Second}
- out, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}})
- if err != nil || out != "FIRST" {
- t.Fatalf("copilot multi-choice: %v %q", err, out)
- }
-
- // Non-2xx with error body
- srv2 := newIPv4Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(403)
- _ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": "denied", "type": "forbidden"}})
- }))
- defer srv2.Close()
- c2 := newCopilot(srv2.URL, "gpt-4o-mini", "KEY", f64p(0.1)).(copilotClient)
- c2.httpClient = &http.Client{Transport: tr, Timeout: 5 * time.Second}
- if _, err := c2.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}}); err == nil {
- t.Fatalf("expected error for copilot non-2xx with error body")
- }
-}
-
-func TestCopilot_Chat_NoChoices_Error(t *testing.T) {
- if os.Getenv("HEXAI_TEST_SKIP_NET") == "1" {
- t.Skip("skip network-bound tests in restricted environments")
- }
- srv := newIPv4Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- _ = json.NewEncoder(w).Encode(map[string]any{"choices": []any{}})
- }))
- defer srv.Close()
- c := newCopilot(srv.URL, "gpt-4o-mini", "KEY", f64p(0.1)).(copilotClient)
- tr := rtFunc2(func(r *http.Request) (*http.Response, error) {
- if r.URL.Host == "api.github.com" && r.URL.Path == "/copilot_internal/v2/token" {
- rw := httptest.NewRecorder()
- _ = json.NewEncoder(rw).Encode(map[string]string{"token": "tok"})
- res := rw.Result()
- res.StatusCode = 200
- return res, nil
- }
- return http.DefaultTransport.RoundTrip(r)
- })
- c.httpClient = &http.Client{Transport: tr, Timeout: 5 * time.Second}
- if _, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}}); err == nil {
- t.Fatalf("expected error when no choices returned")
- }
-}
-
-func TestCopilot_Chat_DecodeError_StatusOK(t *testing.T) {
- if os.Getenv("HEXAI_TEST_SKIP_NET") == "1" {
- t.Skip("skip network-bound tests in restricted environments")
- }
- // Chat returns 200 but invalid JSON; expect decode error
- srv := newIPv4Server(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- _, _ = io.WriteString(w, "{invalid")
- }))
- defer srv.Close()
- c := newCopilot(srv.URL, "gpt-4o-mini", "KEY", f64p(0.1)).(copilotClient)
- tr := rtFunc2(func(r *http.Request) (*http.Response, error) {
- if r.URL.Host == "api.github.com" && r.URL.Path == "/copilot_internal/v2/token" {
- rw := httptest.NewRecorder()
- _ = json.NewEncoder(rw).Encode(map[string]string{"token": "tok"})
- res := rw.Result()
- res.StatusCode = 200
- return res, nil
- }
- return http.DefaultTransport.RoundTrip(r)
- })
- c.httpClient = &http.Client{Transport: tr, Timeout: 5 * time.Second}
- if _, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}}); err == nil {
- t.Fatalf("expected decode error for invalid body")
- }
-}
-
-func TestCopilot_CodeCompletion_MalformedAndEmpty(t *testing.T) {
- c := newCopilot("https://api.githubcopilot.com", "gpt-4o-mini", "API", f64p(0.1)).(copilotClient)
- tr := rtFunc2(func(r *http.Request) (*http.Response, error) {
- if r.URL.Host == "api.github.com" && r.URL.Path == "/copilot_internal/v2/token" {
- rw := httptest.NewRecorder()
- _ = json.NewEncoder(rw).Encode(map[string]string{"token": "tok"})
- res := rw.Result()
- res.StatusCode = 200
- return res, nil
- }
- if r.URL.Host == "copilot-proxy.githubusercontent.com" && strings.HasSuffix(r.URL.Path, "/v1/engines/copilot-codex/completions") {
- rw := httptest.NewRecorder()
- // malformed line
- _, _ = rw.WriteString("data: {bad}\n")
- // done; should produce empty suggestions
- _, _ = rw.WriteString("data: [DONE]\n")
- res := rw.Result()
- res.StatusCode = 200
- return res, nil
- }
- return http.DefaultTransport.RoundTrip(r)
- })
- c.httpClient = &http.Client{Transport: tr, Timeout: 5 * time.Second}
- out, err := c.CodeCompletion(context.Background(), "p", "s", 1, "go", 0.1)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(out) != 0 {
- t.Fatalf("expected empty suggestions, got %#v", out)
- }
-
- // Now include one good chunk after malformed
- tr2 := rtFunc2(func(r *http.Request) (*http.Response, error) {
- if r.URL.Host == "api.github.com" && r.URL.Path == "/copilot_internal/v2/token" {
- rw := httptest.NewRecorder()
- _ = json.NewEncoder(rw).Encode(map[string]string{"token": "tok"})
- res := rw.Result()
- res.StatusCode = 200
- return res, nil
- }
- if r.URL.Host == "copilot-proxy.githubusercontent.com" && strings.HasSuffix(r.URL.Path, "/v1/engines/copilot-codex/completions") {
- rw := httptest.NewRecorder()
- _, _ = rw.WriteString("data: {bad}\n")
- _, _ = rw.WriteString("data: {\"choices\":[{\"index\":0,\"text\":\"OK\"}]}\n")
- _, _ = rw.WriteString("data: [DONE]\n")
- res := rw.Result()
- res.StatusCode = 200
- return res, nil
- }
- return http.DefaultTransport.RoundTrip(r)
- })
- c.httpClient = &http.Client{Transport: tr2, Timeout: 5 * time.Second}
- out2, err := c.CodeCompletion(context.Background(), "p", "s", 1, "go", 0.1)
- if err != nil || len(out2) != 1 || out2[0] != "OK" {
- t.Fatalf("unexpected: %v %#v", err, out2)
- }
-}
-
-func TestParseJWTExp_AndParseInt64(t *testing.T) {
- // Valid base64 payload
- payload := `{"exp": 1700000000}`
- b := base64.RawURLEncoding.EncodeToString([]byte(payload))
- tok := "x." + b + ".y"
- if tm := parseJWTExp(tok); tm.IsZero() {
- t.Fatalf("expected non-zero time")
- }
- if n, err := parseInt64("123"); err != nil || n != 123 {
- t.Fatalf("parseInt64: %v %d", err, n)
- }
-}
-
-func newIPv4Server(t *testing.T, handler http.Handler) *httptest.Server {
- t.Helper()
- l, err := net.Listen("tcp4", "127.0.0.1:0")
- if err != nil {
- t.Fatalf("failed to listen on tcp4: %v", err)
- }
- srv := &httptest.Server{
- Listener: l,
- Config: &http.Server{Handler: handler},
- }
- srv.Start()
- return srv
-}
-
-// bytesReader wraps a byte slice with an io.ReadCloser without importing extra.
-type bytesReader []byte
-
-func (b bytesReader) Read(p []byte) (int, error) { n := copy(p, b); return n, io.EOF }
-func (b bytesReader) Close() error { return nil }
diff --git a/internal/llm/copilot_test.go b/internal/llm/copilot_test.go
deleted file mode 100644
index 8f15347..0000000
--- a/internal/llm/copilot_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package llm
-
-import "testing"
-
-func TestBuildCopilotChatRequest_FieldsAndDefaults(t *testing.T) {
- o := Options{
- Model: "gpt-x",
- Temperature: 0,
- MaxTokens: 123,
- Stop: []string{"X"},
- }
-
- msgs := []Message{{Role: "user", Content: "q"}}
- req := buildCopilotChatRequest(o, msgs, f64p(0.5))
-
- if req.Model != "gpt-x" {
- t.Fatalf("model mismatch: %q", req.Model)
- }
-
- if req.Temperature == nil || *req.Temperature != 0.5 {
- t.Fatalf("default temp not applied")
- }
-
- if req.MaxTokens == nil || *req.MaxTokens != 123 {
- t.Fatalf("max_tokens not applied")
- }
-
- if len(req.Stop) != 1 || req.Stop[0] != "X" {
- t.Fatalf("stop not applied")
- }
-
- if len(req.Messages) != 1 || req.Messages[0].Content != "q" {
- t.Fatalf("messages not copied")
- }
-}
diff --git a/internal/llm/openai_temp_test.go b/internal/llm/openai_temp_test.go
index 07abbd5..3d71b94 100644
--- a/internal/llm/openai_temp_test.go
+++ b/internal/llm/openai_temp_test.go
@@ -5,7 +5,7 @@ import "testing"
func TestNewFromConfig_DefaultTemp_ByModel(t *testing.T) {
// OpenAI, gpt-5.* → default temp 1.0 when not provided
cfg := Config{Provider: "openai", OpenAIModel: "gpt-5.0-preview"}
- c, err := NewFromConfig(cfg, "key", "", "", "")
+ c, err := NewFromConfig(cfg, "key", "", "")
if err != nil {
t.Fatalf("new: %v", err)
}
@@ -18,7 +18,7 @@ func TestNewFromConfig_DefaultTemp_ByModel(t *testing.T) {
}
// OpenAI, gpt-4.* → default temp 0.2 when not provided
cfg2 := Config{Provider: "openai", OpenAIModel: "gpt-4.1"}
- c2, err := NewFromConfig(cfg2, "key", "", "", "")
+ c2, err := NewFromConfig(cfg2, "key", "", "")
if err != nil {
t.Fatalf("new2: %v", err)
}
@@ -32,7 +32,7 @@ func TestNewFromConfig_DefaultTemp_UpgradeWhenGpt5AndDefault02(t *testing.T) {
// Simulate app-default of 0.2 while selecting a gpt-5 model: should upgrade to 1.0
v := 0.2
cfg := Config{Provider: "openai", OpenAIModel: "gpt-5.0", OpenAITemperature: &v}
- c, err := NewFromConfig(cfg, "key", "", "", "")
+ c, err := NewFromConfig(cfg, "key", "", "")
if err != nil {
t.Fatalf("new: %v", err)
}
diff --git a/internal/llm/provider.go b/internal/llm/provider.go
index 297f1f3..8230b53 100644
--- a/internal/llm/provider.go
+++ b/internal/llm/provider.go
@@ -78,10 +78,6 @@ type Config struct {
OllamaBaseURL string
OllamaModel string
OllamaTemperature *float64
- // Copilot options
- CopilotBaseURL string
- CopilotModel string
- CopilotTemperature *float64
// Anthropic options
AnthropicBaseURL string
AnthropicModel string
@@ -91,7 +87,7 @@ type Config struct {
// NewFromConfig creates an LLM client using only the supplied configuration.
// The OpenAI API key is supplied separately and may be read from the environment
// by the caller; other environment-based configuration is not used.
-func NewFromConfig(cfg Config, openAIAPIKey, openRouterAPIKey, copilotAPIKey, anthropicAPIKey string) (Client, error) {
+func NewFromConfig(cfg Config, openAIAPIKey, openRouterAPIKey, anthropicAPIKey string) (Client, error) {
p := strings.ToLower(strings.TrimSpace(cfg.Provider))
if p == "" {
p = "openai"
@@ -136,15 +132,6 @@ func NewFromConfig(cfg Config, openAIAPIKey, openRouterAPIKey, copilotAPIKey, an
cfg.OllamaTemperature = &t
}
return newOllamaWithTimeout(cfg.OllamaBaseURL, cfg.OllamaModel, cfg.OllamaTemperature, cfg.RequestTimeout), nil
- case "copilot":
- if strings.TrimSpace(copilotAPIKey) == "" {
- return nil, errors.New("missing COPILOT_API_KEY for provider copilot")
- }
- if cfg.CopilotTemperature == nil {
- t := 0.2
- cfg.CopilotTemperature = &t
- }
- return newCopilotWithTimeout(cfg.CopilotBaseURL, cfg.CopilotModel, copilotAPIKey, cfg.CopilotTemperature, cfg.RequestTimeout), nil
case "anthropic":
if strings.TrimSpace(anthropicAPIKey) == "" {
return nil, errors.New("missing ANTHROPIC_API_KEY for provider anthropic")
diff --git a/internal/llm/provider_more2_test.go b/internal/llm/provider_more2_test.go
deleted file mode 100644
index 86b149a..0000000
--- a/internal/llm/provider_more2_test.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package llm
-
-import "testing"
-
-func TestNewFromConfig_Copilot(t *testing.T) {
- t.Setenv("COPILOT_API_KEY", "x")
- cfg := Config{Provider: "copilot", CopilotModel: "small"}
- c, err := NewFromConfig(cfg, "", "", "x", "")
- if err != nil || c == nil {
- t.Fatalf("copilot provider failed: %v %v", c, err)
- }
-}
diff --git a/internal/llm/provider_more_test.go b/internal/llm/provider_more_test.go
index caad912..8d7b133 100644
--- a/internal/llm/provider_more_test.go
+++ b/internal/llm/provider_more_test.go
@@ -13,17 +13,11 @@ func TestWithOptions_Apply(t *testing.T) {
}
}
-func TestNewFromConfig_Success_OpenAI_And_Copilot(t *testing.T) {
+func TestNewFromConfig_Success_OpenAI(t *testing.T) {
// OpenAI success
oc := Config{Provider: "openai", OpenAIBaseURL: "http://x", OpenAIModel: "gpt"}
- c, err := NewFromConfig(oc, "KEY", "", "", "")
+ c, err := NewFromConfig(oc, "KEY", "", "")
if err != nil || c == nil || c.Name() != "openai" || c.DefaultModel() == "" {
t.Fatalf("openai new: %v %v", c, err)
}
- // Copilot success
- cc := Config{Provider: "copilot", CopilotBaseURL: "http://x", CopilotModel: "gpt-4o-mini"}
- c2, err := NewFromConfig(cc, "", "", "KEY", "")
- if err != nil || c2 == nil || c2.Name() != "copilot" || c2.DefaultModel() == "" {
- t.Fatalf("copilot new: %v %v", c2, err)
- }
}
diff --git a/internal/llm/provider_test.go b/internal/llm/provider_test.go
index 46c7ea8..8ccba6e 100644
--- a/internal/llm/provider_test.go
+++ b/internal/llm/provider_test.go
@@ -6,15 +6,11 @@ import (
func TestNewFromConfig_DefaultsAndErrors(t *testing.T) {
// Unknown provider
- if _, err := NewFromConfig(Config{Provider: "bogus"}, "", "", "", ""); err == nil {
+ if _, err := NewFromConfig(Config{Provider: "bogus"}, "", "", ""); err == nil {
t.Fatalf("expected error for unknown provider")
}
// OpenAI missing key
- if _, err := NewFromConfig(Config{Provider: "openai", OpenAIModel: "g"}, "", "", "", ""); err == nil {
- t.Fatalf("expected key error")
- }
- // Copilot missing key
- if _, err := NewFromConfig(Config{Provider: "copilot", CopilotModel: "m"}, "", "", "", ""); err == nil {
+ if _, err := NewFromConfig(Config{Provider: "openai", OpenAIModel: "g"}, "", "", ""); err == nil {
t.Fatalf("expected key error")
}
}
diff --git a/internal/llmutils/client.go b/internal/llmutils/client.go
index de65935..c8d9a90 100644
--- a/internal/llmutils/client.go
+++ b/internal/llmutils/client.go
@@ -22,9 +22,6 @@ func NewClientFromApp(cfg appconfig.App) (llm.Client, error) {
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,
@@ -37,13 +34,9 @@ func NewClientFromApp(cfg appconfig.App) (llm.Client, error) {
if strings.TrimSpace(orKey) == "" {
orKey = os.Getenv("OPENROUTER_API_KEY")
}
- cpKey := os.Getenv("HEXAI_COPILOT_API_KEY")
- if strings.TrimSpace(cpKey) == "" {
- cpKey = os.Getenv("COPILOT_API_KEY")
- }
anKey := os.Getenv("HEXAI_ANTHROPIC_API_KEY")
if strings.TrimSpace(anKey) == "" {
anKey = os.Getenv("ANTHROPIC_API_KEY")
}
- return llm.NewFromConfig(llmCfg, oaKey, orKey, cpKey, anKey)
+ return llm.NewFromConfig(llmCfg, oaKey, orKey, anKey)
}
diff --git a/internal/lsp/handlers_utils.go b/internal/lsp/handlers_utils.go
index 6260acd..1ea36c8 100644
--- a/internal/lsp/handlers_utils.go
+++ b/internal/lsp/handlers_utils.go
@@ -110,8 +110,6 @@ func resolveDefaultModel(cfg appconfig.App, provider string) string {
switch provider {
case "ollama":
return strings.TrimSpace(cfg.OllamaModel)
- case "copilot":
- return strings.TrimSpace(cfg.CopilotModel)
case "anthropic":
return strings.TrimSpace(cfg.AnthropicModel)
case "openrouter":
diff --git a/internal/lsp/llm_request_opts_test.go b/internal/lsp/llm_request_opts_test.go
index f4d31b9..ad87cd4 100644
--- a/internal/lsp/llm_request_opts_test.go
+++ b/internal/lsp/llm_request_opts_test.go
@@ -44,10 +44,10 @@ func TestBuildRequestSpecs_MultiEntries(t *testing.T) {
s := newTestServer()
s.cfg.CompletionConfigs = []appconfig.SurfaceConfig{
{Provider: "openai", Model: "gpt-4o"},
- {Provider: "copilot", Model: "cpt", Temperature: floatPtr(0.4)},
+ {Provider: "anthropic", Model: "claude", Temperature: floatPtr(0.4)},
}
s.cfg.OpenAIModel = "gpt-3.5"
- s.cfg.CopilotModel = "cpt-base"
+ s.cfg.AnthropicModel = "claude-base"
s.cfg.MaxTokens = 256
specs := s.buildRequestSpecs(surfaceCompletion)
if len(specs) != 2 {
@@ -56,7 +56,7 @@ func TestBuildRequestSpecs_MultiEntries(t *testing.T) {
if specs[0].provider != "openai" || specs[0].index != 0 {
t.Fatalf("unexpected first spec: %+v", specs[0])
}
- if specs[1].provider != "copilot" || specs[1].index != 1 {
+ if specs[1].provider != "anthropic" || specs[1].index != 1 {
t.Fatalf("unexpected second spec: %+v", specs[1])
}
var opts1, opts2 llm.Options
@@ -69,7 +69,7 @@ func TestBuildRequestSpecs_MultiEntries(t *testing.T) {
if opts1.Model != "gpt-4o" || opts1.MaxTokens != 256 {
t.Fatalf("unexpected opts1: %+v", opts1)
}
- if opts2.Model != "cpt" || opts2.Temperature != 0.4 {
+ if opts2.Model != "claude" || opts2.Temperature != 0.4 {
t.Fatalf("unexpected opts2: %+v", opts2)
}
}
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index bbee64f..c226ab4 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -230,9 +230,6 @@ func newClientForProvider(cfg appconfig.App, provider string) (llm.Client, error
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,
@@ -245,15 +242,11 @@ func newClientForProvider(cfg appconfig.App, provider string) (llm.Client, error
if orKey == "" {
orKey = strings.TrimSpace(os.Getenv("OPENROUTER_API_KEY"))
}
- cpKey := strings.TrimSpace(os.Getenv("HEXAI_COPILOT_API_KEY"))
- if cpKey == "" {
- cpKey = strings.TrimSpace(os.Getenv("COPILOT_API_KEY"))
- }
anKey := strings.TrimSpace(os.Getenv("HEXAI_ANTHROPIC_API_KEY"))
if anKey == "" {
anKey = strings.TrimSpace(os.Getenv("ANTHROPIC_API_KEY"))
}
- return llm.NewFromConfig(llmCfg, oaKey, orKey, cpKey, anKey)
+ return llm.NewFromConfig(llmCfg, oaKey, orKey, anKey)
}
func (s *Server) clientFor(spec requestSpec) llm.Client {
@@ -296,12 +289,6 @@ func (s *Server) clientFor(spec requestSpec) llm.Client {
} else if spec.fallbackModel != "" {
cfg.OpenRouterModel = spec.fallbackModel
}
- case "copilot":
- if modelOverride != "" {
- cfg.CopilotModel = modelOverride
- } else if spec.fallbackModel != "" {
- cfg.CopilotModel = spec.fallbackModel
- }
case "ollama":
if modelOverride != "" {
cfg.OllamaModel = modelOverride
diff --git a/internal/runtimeconfig/store_test.go b/internal/runtimeconfig/store_test.go
index 0a0183a..168d2cd 100644
--- a/internal/runtimeconfig/store_test.go
+++ b/internal/runtimeconfig/store_test.go
@@ -99,7 +99,7 @@ func TestStoreReloadLogsSummary(t *testing.T) {
func TestDiff_SurfaceModel(t *testing.T) {
oldCfg := appconfig.App{CompletionConfigs: []appconfig.SurfaceConfig{{Provider: "openai", Model: "gpt-4o"}}}
- newCfg := appconfig.App{CompletionConfigs: []appconfig.SurfaceConfig{{Provider: "copilot", Model: "gpt-4.1"}}}
+ newCfg := appconfig.App{CompletionConfigs: []appconfig.SurfaceConfig{{Provider: "anthropic", Model: "claude-3-5-sonnet"}}}
changes := Diff(oldCfg, newCfg)
if len(changes) == 0 {
t.Fatalf("expected diff entries, got none")
@@ -107,7 +107,7 @@ func TestDiff_SurfaceModel(t *testing.T) {
found := false
for _, ch := range changes {
if ch.Key == "completion_configs" {
- if !strings.Contains(ch.Old, "gpt-4o") || !strings.Contains(ch.New, "gpt-4.1") {
+ if !strings.Contains(ch.Old, "gpt-4o") || !strings.Contains(ch.New, "claude-3-5-sonnet") {
t.Fatalf("unexpected diff contents: %+v", ch)
}
found = true