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:
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user