launch/codex: detect model drift when Codex App UI switches away from Ollama (#16864)

ollama launch codex-app sets root-level model_provider = "ollama-launch-codex-app"
in ~/.codex/config.toml to route requests through the local Ollama server.
In Codex, model_provider is a global config key, there is no per-model provider
in the catalog schema (ModelInfo has no model_provider field), so it applies to
every model, not just Ollama ones.

When a user switches to a built-in OpenAI model (e.g. gpt-5.5) in the Codex App
UI, the UI writes model = "gpt-5.5" to config.toml but does NOT update
model_provider. The root model_provider stays "ollama-launch-codex-app", so the
OpenAI model request goes to http://localhost:11434/v1/responses instead of
OpenAI API, resulting in a 404 ("model gpt-5.5 not found"). The user is
stuck: OpenAI models silently route to localhost until they know to run
"ollama launch codex-app --restore".

Fix: CurrentModel() now verifies the configured model appears as a slug in the
Ollama-managed catalog before reporting the integration as active. When the
model has drifted (user selected a non-Ollama model in the UI), CurrentModel()
returns empty, so the launcher accurately shows the integration as inactive and
the user is directed to restore or re-launch.
This commit is contained in:
Bruce MacDonald
2026-06-22 15:38:19 -07:00
committed by GitHub
parent 505e35f2b9
commit 2a8b31531e
2 changed files with 97 additions and 2 deletions

View File

@@ -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()

View File

@@ -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)