diff --git a/internal/config/chatgpt_defaults_test.go b/internal/config/chatgpt_defaults_test.go new file mode 100644 index 000000000..473c00ef0 --- /dev/null +++ b/internal/config/chatgpt_defaults_test.go @@ -0,0 +1,51 @@ +package config + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +func TestEnsureChatGPTDefaultPreservesSavedSelection(t *testing.T) { + for _, saved := range []string{"", "gpt-explicit-saved"} { + name := "new profile" + if saved != "" { + name = "saved selection" + } + t.Run(name, func(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.json") + if saved != "" { + body, err := json.Marshal(FileConfig{ActiveProvider: "codex", Providers: []ProviderProfile{{Name: "codex", CatalogID: "chatgpt", Model: saved}}}) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, body, 0600); err != nil { + t.Fatal(err) + } + } + ensured, err := EnsureCatalogProvider(path, "chatgpt") + if err != nil { + t.Fatal(err) + } + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var cfg FileConfig + if err := json.Unmarshal(body, &cfg); err != nil { + t.Fatal(err) + } + want := "gpt-6.1-sol" + if saved != "" { + want = saved + } + if len(cfg.Providers) != 1 || cfg.Providers[0].Model != want { + t.Fatalf("ChatGPT default/selection = %#v, want model %q", cfg.Providers, want) + } + if saved != "" && (ensured.Name != "codex" || ensured.Created) { + t.Fatalf("saved subscription profile was replaced: %#v", ensured) + } + }) + } +} diff --git a/internal/providercatalog/catalog.go b/internal/providercatalog/catalog.go index 6cd1c21a8..ba67212b1 100644 --- a/internal/providercatalog/catalog.go +++ b/internal/providercatalog/catalog.go @@ -132,7 +132,9 @@ var descriptors = []Descriptor{ // way in; RequiresAuth is forced true so catalog consumers know the provider // needs an interactive login before a request will succeed. func() Descriptor { - d := openAICompat("chatgpt", "ChatGPT", "https://chatgpt.com/backend-api/codex", "gpt-5.5", nil) + // New ChatGPT profiles follow the vendor's GPT-5.5 replacement guidance: + // https://learn.chatgpt.com/docs/models#gpt-55-retirement + d := openAICompat("chatgpt", "ChatGPT", "https://chatgpt.com/backend-api/codex", "gpt-6.1-sol", nil) d.RequiresAuth = true return oauthProvider(d, false, false) }(), diff --git a/internal/providermodelcatalog/catalog.go b/internal/providermodelcatalog/catalog.go index 5bf5e27d4..984f56de9 100644 --- a/internal/providermodelcatalog/catalog.go +++ b/internal/providermodelcatalog/catalog.go @@ -113,10 +113,12 @@ var curatedModels = map[string][]Model{ {ID: "deepseek/deepseek-chat", Description: "coding model"}, }, "chatgpt": { - {ID: "gpt-5.5", Description: "recommended Codex model"}, - {ID: "gpt-5.4", Description: "strong Codex model"}, - {ID: "gpt-5.4-mini", Description: "fast Codex model"}, - {ID: "gpt-5.3-codex-spark", Description: "research preview fast Codex model"}, + // Suggestions only: availability comes from the authenticated Codex catalog. + // https://learn.chatgpt.com/docs/models + {ID: "gpt-6.1-sol", Description: "balanced Codex model"}, + {ID: "gpt-6.1-terra", Description: "fast model for writing, visual understanding, and computer use"}, + {ID: "gpt-6-astra", Description: "advanced reasoning model"}, + {ID: "gpt-6-luna", Description: "efficient coding model"}, }, "deepseek": { {ID: "deepseek-chat", Description: "catalog default"}, diff --git a/internal/providermodelcatalog/catalog_test.go b/internal/providermodelcatalog/catalog_test.go index f952db52f..3a5a86ba0 100644 --- a/internal/providermodelcatalog/catalog_test.go +++ b/internal/providermodelcatalog/catalog_test.go @@ -35,8 +35,8 @@ func TestModelsAreProviderScoped(t *testing.T) { }, { provider: "chatgpt", - want: []string{"gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark"}, - notWant: []string{"gpt-5", "gpt-4.1", "openai/gpt-4.1"}, + want: []string{"gpt-6.1-sol", "gpt-6.1-terra", "gpt-6-astra", "gpt-6-luna"}, + notWant: []string{"gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark", "openai/gpt-4.1"}, }, { provider: "aimlapi", diff --git a/internal/providermodeldiscovery/chatgpt_defaults_test.go b/internal/providermodeldiscovery/chatgpt_defaults_test.go new file mode 100644 index 000000000..23ddced73 --- /dev/null +++ b/internal/providermodeldiscovery/chatgpt_defaults_test.go @@ -0,0 +1,31 @@ +package providermodeldiscovery + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Twigpine/zero/internal/config" +) + +func TestChatGPTDiscoveryUsesCurrentCompatibilityVersion(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.URL.Query().Get("client_version"); got != "0.159.0" { + t.Errorf("client_version = %q, want documented GPT-6.1 Terra compatibility floor 0.159.0", got) + } + _, _ = w.Write([]byte(`{"models":[{"slug":"gpt-future-coder"}]}`)) + })) + defer server.Close() + profile := config.ProviderProfile{ + CatalogID: "chatgpt", ProviderKind: config.ProviderKindOpenAICompatible, + BaseURL: server.URL + "/backend-api/codex", APIKey: "test-bearer", + } + models, err := Discover(context.Background(), profile, Options{HTTPClient: server.Client()}) + if err != nil { + t.Fatal(err) + } + if len(models) != 1 || models[0].ID != "gpt-future-coder" { + t.Fatalf("models = %#v, want only gpt-future-coder", models) + } +} diff --git a/internal/providermodeldiscovery/discovery.go b/internal/providermodeldiscovery/discovery.go index 3a78121ac..3e1dbd250 100644 --- a/internal/providermodeldiscovery/discovery.go +++ b/internal/providermodeldiscovery/discovery.go @@ -20,8 +20,9 @@ import ( ) const ( - anthropicVersion = "2023-06-01" - chatGPTModelsProtocolVersion = "0.146.0" + anthropicVersion = "2023-06-01" + // GPT-6.1 Terra's documented Codex client floor: https://learn.chatgpt.com/docs/models. + chatGPTModelsProtocolVersion = "0.159.0" ) type Model struct { diff --git a/internal/tui/provider_wizard_oauth_test.go b/internal/tui/provider_wizard_oauth_test.go index 56bcb002b..c9777f833 100644 --- a/internal/tui/provider_wizard_oauth_test.go +++ b/internal/tui/provider_wizard_oauth_test.go @@ -258,7 +258,7 @@ func TestProviderWizardChatGPTOAuthModelsUseCodexSet(t *testing.T) { for _, model := range models { got[model.ID] = true } - for _, want := range []string{"gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark"} { + for _, want := range []string{"gpt-6.1-sol", "gpt-6.1-terra", "gpt-6-astra", "gpt-6-luna"} { if !got[want] { t.Fatalf("ChatGPT OAuth models missing %q; got %#v", want, providerWizardModelIDs(models)) } @@ -266,8 +266,8 @@ func TestProviderWizardChatGPTOAuthModelsUseCodexSet(t *testing.T) { if got["gpt-5"] { t.Fatalf("ChatGPT OAuth models should not include stale gpt-5 fallback; got %#v", providerWizardModelIDs(models)) } - if models[0].ID != "gpt-5.5" { - t.Fatalf("default ChatGPT OAuth model = %q, want gpt-5.5", models[0].ID) + if models[0].ID != "gpt-6.1-sol" { + t.Fatalf("default ChatGPT OAuth model = %q, want gpt-6.1-sol", models[0].ID) } }