diff --git a/cmd/launch/codex_app.go b/cmd/launch/codex_app.go index f57e6b4f..6c3c5093 100644 --- a/cmd/launch/codex_app.go +++ b/cmd/launch/codex_app.go @@ -106,7 +106,10 @@ func (c *CodexApp) CurrentModel() string { if parsed.RootString(codexRootModelProviderKey) == profileName { baseURL := parsed.ProviderString(profileName, "base_url") if codexNormalizeURL(baseURL) == codexNormalizeURL(codexBaseURL()) && codexAppCatalogHealthy(parsed, profileName) { - return strings.TrimSpace(parsed.RootString(codexRootModelKey)) + model := strings.TrimSpace(parsed.RootString(codexRootModelKey)) + if codexAppCatalogContainsModel(model) { + return model + } } } } @@ -125,7 +128,11 @@ func (c *CodexApp) CurrentModel() string { if !codexAppCatalogHealthy(parsed, profileName) { return "" } - return strings.TrimSpace(parsed.ProfileString(profileName, codexRootModelKey)) + model := strings.TrimSpace(parsed.ProfileString(profileName, codexRootModelKey)) + if !codexAppCatalogContainsModel(model) { + return "" + } + return model } func codexAppManagedProfileNames() []string { @@ -169,6 +176,40 @@ func codexAppCatalogHealthy(config codexParsedConfig, profileName string) bool { return len(catalog.Models) > 0 } +// codexAppCatalogContainsModel reports whether model appears as a slug in the +// Ollama-managed model catalog. When the configured model is not in the catalog +// the user has drifted away from the launch-managed model (e.g. by selecting a +// built-in OpenAI model in the Codex App UI), and the launch config should be +// treated as inactive. +func codexAppCatalogContainsModel(model string) bool { + if strings.TrimSpace(model) == "" { + return false + } + catalogPath, err := codexAppModelCatalogPath() + if err != nil { + return false + } + data, err := os.ReadFile(catalogPath) + if err != nil { + return false + } + var catalog struct { + Models []struct { + Slug string `json:"slug"` + } `json:"models"` + } + if err := json.Unmarshal(data, &catalog); err != nil { + return false + } + target := codexAppCatalogModelKey(model) + for _, m := range catalog.Models { + if codexAppCatalogModelKey(m.Slug) == target { + return true + } + } + return false +} + func writeCodexAppConfig(configPath, model, modelCatalogPath string) error { baseURL := codexBaseURL() diff --git a/cmd/launch/codex_app_test.go b/cmd/launch/codex_app_test.go index 83b8472f..4693e176 100644 --- a/cmd/launch/codex_app_test.go +++ b/cmd/launch/codex_app_test.go @@ -626,6 +626,60 @@ func TestCodexAppCurrentModelRequiresHealthyCatalog(t *testing.T) { } } +func TestCodexAppCurrentModelDetectsDriftedModel(t *testing.T) { + tmpDir := t.TempDir() + setTestHome(t, tmpDir) + t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434") + + catalogPath := mustWriteCodexAppTestCatalog(t, "llama3.2") + configPath := filepath.Join(tmpDir, ".codex", "config.toml") + if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { + t.Fatal(err) + } + content := "" + + `model = "gpt-5.5"` + "\n" + + fmt.Sprintf(`model_provider = %q`, codexAppProfileName) + "\n\n" + + fmt.Sprintf(`model_catalog_json = %q`, catalogPath) + "\n\n" + + codexProviderHeaderFor(codexAppProfileName) + "\n" + + `name = "Ollama"` + "\n" + + `base_url = "http://127.0.0.1:11434/v1/"` + "\n" + + `wire_api = "responses"` + "\n" + if err := os.WriteFile(configPath, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + + if got := (&CodexApp{}).CurrentModel(); got != "" { + t.Fatalf("CurrentModel = %q, want empty when model has drifted from the Ollama catalog", got) + } +} + +func TestCodexAppCurrentModelAcceptsLatestSuffixDrift(t *testing.T) { + tmpDir := t.TempDir() + setTestHome(t, tmpDir) + t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434") + + catalogPath := mustWriteCodexAppTestCatalog(t, "llama3.2") + configPath := filepath.Join(tmpDir, ".codex", "config.toml") + if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { + t.Fatal(err) + } + content := "" + + `model = "llama3.2:latest"` + "\n" + + fmt.Sprintf(`model_provider = %q`, codexAppProfileName) + "\n\n" + + fmt.Sprintf(`model_catalog_json = %q`, catalogPath) + "\n\n" + + codexProviderHeaderFor(codexAppProfileName) + "\n" + + `name = "Ollama"` + "\n" + + `base_url = "http://127.0.0.1:11434/v1/"` + "\n" + + `wire_api = "responses"` + "\n" + if err := os.WriteFile(configPath, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + + if got := (&CodexApp{}).CurrentModel(); got != "llama3.2:latest" { + t.Fatalf("CurrentModel = %q, want llama3.2:latest (:latest suffix should not be treated as drift)", got) + } +} + func TestCodexAppConfigurePopulatesCatalogFromEnrichedModels(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir)