diff --git a/cmd/launch/codex.go b/cmd/launch/codex.go index 15bc98dc1..1b48476d7 100644 --- a/cmd/launch/codex.go +++ b/cmd/launch/codex.go @@ -32,7 +32,7 @@ const ( ) func (c *Codex) args(model, modelCatalogPath string, extra []string) []string { - args := []string{"--profile", codexProfileName} + args := []string{} if modelCatalogPath != "" { args = append(args, "-c", fmt.Sprintf("%s=%q", codexRootModelCatalogJSONKey, modelCatalogPath)) } @@ -67,7 +67,7 @@ func (c *Codex) Run(model string, models []LaunchModel, args []string) error { return cmd.Run() } -// ensureCodexConfig writes a Codex profile and model catalog so Codex uses the +// ensureCodexConfig writes Codex root config and model catalog so Codex uses the // local Ollama server and has model metadata available. func ensureCodexConfig(modelName string, models []LaunchModel) error { configPath, err := codexConfigPath() @@ -85,7 +85,7 @@ func ensureCodexConfig(modelName string, models []LaunchModel) error { return err } - return writeCodexProfile(configPath, catalogPath) + return writeCodexConfig(configPath, modelName, catalogPath) } func codexConfigPath() (string, error) { @@ -108,33 +108,10 @@ func codexModelCatalogPathForConfig(configPath string) string { return filepath.Join(filepath.Dir(configPath), "model.json") } -// writeCodexProfile ensures ~/.codex/config.toml has the ollama-launch profile -// and model provider sections with the correct base URL. -func writeCodexProfile(configPath string, modelCatalogPath ...string) error { - opts := codexLaunchProfileOptions{ - forceAPIAuth: true, - } - if len(modelCatalogPath) > 0 { - opts.modelCatalogPath = modelCatalogPath[0] - } - return writeCodexLaunchProfile(configPath, opts) -} - -type codexLaunchProfileOptions struct { - activate bool - profileName string - forceAPIAuth bool - setRootModelConfig bool - model string - modelCatalogPath string - backupIntegration string -} - -func writeCodexLaunchProfile(configPath string, opts codexLaunchProfileOptions) error { +// writeCodexConfig ensures ~/.codex/config.toml uses Ollama as the root model +// provider without relying on Codex's legacy profile config. +func writeCodexConfig(configPath, model, modelCatalogPath string) error { baseURL := codexBaseURL() - profileName := codexLaunchProfileName(opts) - profileHeader := codexProfileHeaderFor(profileName) - providerHeader := codexProviderHeaderFor(profileName) content, readErr := os.ReadFile(configPath) text := "" @@ -143,88 +120,37 @@ func writeCodexLaunchProfile(configPath string, opts codexLaunchProfileOptions) } else if !os.IsNotExist(readErr) { return readErr } + if _, err := codexParseConfig(text); err != nil { + return err + } + + text = codexRemoveRootValue(text, codexRootProfileKey) + text = codexRemoveSection(text, codexProfileHeaderFor(codexProfileName)) + if strings.TrimSpace(model) != "" { + text = codexSetRootStringValue(text, codexRootModelKey, model) + } + text = codexSetRootStringValue(text, codexRootModelProviderKey, codexProfileName) + if strings.TrimSpace(modelCatalogPath) != "" { + text = codexSetRootStringValue(text, codexRootModelCatalogJSONKey, modelCatalogPath) + } + text = codexUpsertSection(text, codexProviderHeaderFor(codexProfileName), []string{ + fmt.Sprintf("name = %q", codexProviderName), + fmt.Sprintf("base_url = %q", baseURL), + `wire_api = "responses"`, + }) + parsed, err := codexParseConfig(text) if err != nil { return err } - - model := strings.TrimSpace(opts.model) - if model == "" { - model = parsed.ProfileString(profileName, codexRootModelKey) - } - modelCatalogPath := strings.TrimSpace(opts.modelCatalogPath) - if modelCatalogPath == "" { - modelCatalogPath = parsed.ProfileString(profileName, codexRootModelCatalogJSONKey) - } - - profileLines := []string{} - if model != "" { - profileLines = append(profileLines, fmt.Sprintf("%s = %q", codexRootModelKey, model)) - } - profileLines = append(profileLines, - fmt.Sprintf("openai_base_url = %q", baseURL), - fmt.Sprintf("%s = %q", codexRootModelProviderKey, profileName), - ) - if opts.forceAPIAuth { - profileLines = append(profileLines, `forced_login_method = "api"`) - } - if modelCatalogPath != "" { - profileLines = append(profileLines, fmt.Sprintf("%s = %q", codexRootModelCatalogJSONKey, modelCatalogPath)) - } - - sections := []struct { - header string - lines []string - }{ - { - header: profileHeader, - lines: profileLines, - }, - { - header: providerHeader, - lines: []string{ - fmt.Sprintf("name = %q", codexProviderName), - fmt.Sprintf("base_url = %q", baseURL), - `wire_api = "responses"`, - }, - }, - } - - if opts.activate { - text = codexSetRootStringValue(text, codexRootProfileKey, profileName) - } - if opts.setRootModelConfig { - if model != "" { - text = codexSetRootStringValue(text, codexRootModelKey, model) - } - text = codexSetRootStringValue(text, codexRootModelProviderKey, profileName) - if modelCatalogPath != "" { - text = codexSetRootStringValue(text, codexRootModelCatalogJSONKey, modelCatalogPath) - } - } - - for _, s := range sections { - text = codexUpsertSection(text, s.header, s.lines) - } - parsed, err = codexParseConfig(text) - if err != nil { - return err - } - if err := codexValidateLaunchProfileText(parsed, profileName, opts, model, modelCatalogPath, baseURL); err != nil { + if err := codexValidateConfigTextForOllama(parsed, model, modelCatalogPath, baseURL); err != nil { return err } if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { return err } - return fileutil.WriteWithBackup(configPath, []byte(text), opts.backupIntegration) -} - -func codexLaunchProfileName(opts codexLaunchProfileOptions) string { - if name := strings.TrimSpace(opts.profileName); name != "" { - return name - } - return codexProfileName + return fileutil.WriteWithBackup(configPath, []byte(text), "") } func codexBaseURL() string { @@ -247,54 +173,34 @@ func codexProviderHeaderFor(profileName string) string { return fmt.Sprintf("[model_providers.%s]", profileName) } -func codexValidateLaunchProfileText(config codexParsedConfig, profileName string, opts codexLaunchProfileOptions, model, modelCatalogPath, baseURL string) error { +func codexValidateConfigTextForOllama(config codexParsedConfig, model, modelCatalogPath, baseURL string) error { + if got, ok := config.RootStringOK(codexRootProfileKey); ok { + return fmt.Errorf("generated Codex config still contains legacy profile = %q", got) + } + if config.Exists("profiles", codexProfileName) { + return fmt.Errorf("generated Codex config still contains legacy profiles.%s table", codexProfileName) + } for _, check := range []struct { path []string want string }{ - {[]string{"profiles", profileName, "openai_base_url"}, baseURL}, - {[]string{"profiles", profileName, codexRootModelProviderKey}, profileName}, - {[]string{"model_providers", profileName, "name"}, codexProviderName}, - {[]string{"model_providers", profileName, "base_url"}, baseURL}, - {[]string{"model_providers", profileName, "wire_api"}, "responses"}, + {[]string{codexRootModelProviderKey}, codexProfileName}, + {[]string{"model_providers", codexProfileName, "name"}, codexProviderName}, + {[]string{"model_providers", codexProfileName, "base_url"}, baseURL}, + {[]string{"model_providers", codexProfileName, "wire_api"}, "responses"}, } { if got, ok := config.String(check.path...); !ok || got != check.want { return fmt.Errorf("generated Codex config missing %s = %q", strings.Join(check.path, "."), check.want) } } - if opts.forceAPIAuth { - if got, ok := config.String("profiles", profileName, "forced_login_method"); !ok || got != "api" { - return fmt.Errorf("generated Codex config missing profiles.%s.forced_login_method = %q", profileName, "api") - } - } if model != "" { - if got, ok := config.String("profiles", profileName, codexRootModelKey); !ok || got != model { - return fmt.Errorf("generated Codex config missing profiles.%s.model = %q", profileName, model) + if got := config.RootString(codexRootModelKey); got != model { + return fmt.Errorf("generated Codex config missing model = %q", model) } } if modelCatalogPath != "" { - if got, ok := config.String("profiles", profileName, codexRootModelCatalogJSONKey); !ok || got != modelCatalogPath { - return fmt.Errorf("generated Codex config missing profiles.%s.model_catalog_json = %q", profileName, modelCatalogPath) - } - } - if opts.activate { - if got := config.RootString(codexRootProfileKey); got != profileName { - return fmt.Errorf("generated Codex config missing profile = %q", profileName) - } - } - if opts.setRootModelConfig { - if model != "" { - if got := config.RootString(codexRootModelKey); got != model { - return fmt.Errorf("generated Codex config missing model = %q", model) - } - } - if got := config.RootString(codexRootModelProviderKey); got != profileName { - return fmt.Errorf("generated Codex config missing model_provider = %q", profileName) - } - if modelCatalogPath != "" { - if got := config.RootString(codexRootModelCatalogJSONKey); got != modelCatalogPath { - return fmt.Errorf("generated Codex config missing model_catalog_json = %q", modelCatalogPath) - } + if got := config.RootString(codexRootModelCatalogJSONKey); got != modelCatalogPath { + return fmt.Errorf("generated Codex config missing model_catalog_json = %q", modelCatalogPath) } } return nil @@ -592,6 +498,7 @@ func codexRootLineHasKey(line, key string) bool { func codexCatalogModel(modelName string, models []LaunchModel) LaunchModel { if model, ok := findLaunchModel(models, modelName); ok { + model.Name = modelName return model.WithCloudLimits() } return fallbackLaunchModel(modelName) diff --git a/cmd/launch/codex_test.go b/cmd/launch/codex_test.go index 3d59e4bf4..9f8239cd2 100644 --- a/cmd/launch/codex_test.go +++ b/cmd/launch/codex_test.go @@ -25,10 +25,10 @@ func TestCodexArgs(t *testing.T) { args []string want []string }{ - {"with model", "llama3.2", nil, []string{"--profile", "ollama-launch", "-c", catalogArg, "-m", "llama3.2"}}, - {"empty model", "", nil, []string{"--profile", "ollama-launch", "-c", catalogArg}}, - {"with model and extra args", "qwen3.5", []string{"-p", "myprofile"}, []string{"--profile", "ollama-launch", "-c", catalogArg, "-m", "qwen3.5", "-p", "myprofile"}}, - {"with sandbox flag", "llama3.2", []string{"--sandbox", "workspace-write"}, []string{"--profile", "ollama-launch", "-c", catalogArg, "-m", "llama3.2", "--sandbox", "workspace-write"}}, + {"with model", "llama3.2", nil, []string{"-c", catalogArg, "-m", "llama3.2"}}, + {"empty model", "", nil, []string{"-c", catalogArg}}, + {"with profile flag", "qwen3.5", []string{"-p", "myprofile"}, []string{"-c", catalogArg, "-m", "qwen3.5", "-p", "myprofile"}}, + {"with sandbox flag", "llama3.2", []string{"--sandbox", "workspace-write"}, []string{"-c", catalogArg, "-m", "llama3.2", "--sandbox", "workspace-write"}}, } for _, tt := range tests { @@ -41,13 +41,13 @@ func TestCodexArgs(t *testing.T) { } } -func TestWriteCodexProfile(t *testing.T) { +func TestWriteCodexConfig(t *testing.T) { t.Run("creates new file when none exists", func(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.toml") catalogPath := filepath.Join(tmpDir, "model.json") - if err := writeCodexProfile(configPath, catalogPath); err != nil { + if err := writeCodexConfig(configPath, "llama3.2", catalogPath); err != nil { t.Fatal(err) } @@ -57,43 +57,38 @@ func TestWriteCodexProfile(t *testing.T) { } content := string(data) - if !strings.Contains(content, "[profiles.ollama-launch]") { - t.Error("missing [profiles.ollama-launch] header") + for _, want := range []string{ + `model = "llama3.2"`, + `model_provider = "ollama-launch"`, + fmt.Sprintf("model_catalog_json = %q", catalogPath), + "[model_providers.ollama-launch]", + `name = "Ollama"`, + `base_url = "http://127.0.0.1:11434/v1/"`, + `wire_api = "responses"`, + } { + if !strings.Contains(content, want) { + t.Errorf("missing %q in:\n%s", want, content) + } } - if !strings.Contains(content, "openai_base_url") { - t.Error("missing openai_base_url key") + if got, ok := codexRootStringValueOK(content, "profile"); ok { + t.Fatalf("legacy root profile should not be generated, got %q in:\n%s", got, content) } - if !strings.Contains(content, "/v1/") { - t.Error("missing /v1/ suffix in base URL") - } - if !strings.Contains(content, `forced_login_method = "api"`) { - t.Error("missing forced_login_method key") - } - if !strings.Contains(content, `model_provider = "ollama-launch"`) { - t.Error("missing model_provider key") - } - if !strings.Contains(content, fmt.Sprintf("model_catalog_json = %q", catalogPath)) { - t.Error("missing model_catalog_json key") - } - if !strings.Contains(content, "[model_providers.ollama-launch]") { - t.Error("missing [model_providers.ollama-launch] section") - } - if !strings.Contains(content, `name = "Ollama"`) { - t.Error("missing model provider name") + if strings.Contains(content, "[profiles.ollama-launch]") { + t.Fatalf("legacy profile section should not be generated, got:\n%s", content) } if err := codexValidateConfigText(content); err != nil { t.Fatalf("generated config should be valid TOML: %v\n%s", err, content) } }) - t.Run("appends profile to existing file without profile", func(t *testing.T) { + t.Run("appends provider to existing file without provider", func(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.toml") catalogPath := filepath.Join(tmpDir, "model.json") existing := "[some_other_section]\nkey = \"value\"\n" os.WriteFile(configPath, []byte(existing), 0o644) - if err := writeCodexProfile(configPath, catalogPath); err != nil { + if err := writeCodexConfig(configPath, "llama3.2", catalogPath); err != nil { t.Fatal(err) } @@ -103,19 +98,22 @@ func TestWriteCodexProfile(t *testing.T) { if !strings.Contains(content, "[some_other_section]") { t.Error("existing section was removed") } - if !strings.Contains(content, "[profiles.ollama-launch]") { - t.Error("missing [profiles.ollama-launch] header") + if strings.Contains(content, "[profiles.ollama-launch]") { + t.Fatalf("legacy profile section should not be generated, got:\n%s", content) + } + if !strings.Contains(content, "[model_providers.ollama-launch]") { + t.Error("missing [model_providers.ollama-launch] header") } }) - t.Run("replaces existing profile section", func(t *testing.T) { + t.Run("removes existing profile section and replaces provider section", func(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.toml") catalogPath := filepath.Join(tmpDir, "model.json") existing := "[profiles.ollama-launch]\nopenai_base_url = \"http://old:1234/v1/\"\n\n[model_providers.ollama-launch]\nname = \"Ollama\"\nbase_url = \"http://old:1234/v1/\"\n" os.WriteFile(configPath, []byte(existing), 0o644) - if err := writeCodexProfile(configPath, catalogPath); err != nil { + if err := writeCodexConfig(configPath, "llama3.2", catalogPath); err != nil { t.Fatal(err) } @@ -125,8 +123,8 @@ func TestWriteCodexProfile(t *testing.T) { if strings.Contains(content, "old:1234") { t.Error("old URL was not replaced") } - if strings.Count(content, "[profiles.ollama-launch]") != 1 { - t.Errorf("expected exactly one [profiles.ollama-launch] section, got %d", strings.Count(content, "[profiles.ollama-launch]")) + if strings.Contains(content, "[profiles.ollama-launch]") { + t.Fatalf("legacy profile section should be removed, got:\n%s", content) } if strings.Count(content, "[model_providers.ollama-launch]") != 1 { t.Errorf("expected exactly one [model_providers.ollama-launch] section, got %d", strings.Count(content, "[model_providers.ollama-launch]")) @@ -136,7 +134,7 @@ func TestWriteCodexProfile(t *testing.T) { } }) - t.Run("replaces equivalent quoted profile table", func(t *testing.T) { + t.Run("removes equivalent quoted profile table", func(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.toml") existing := "" + @@ -150,21 +148,24 @@ func TestWriteCodexProfile(t *testing.T) { `model = "gpt-5.5"` + "\n" os.WriteFile(configPath, []byte(existing), 0o644) - if err := writeCodexProfile(configPath); err != nil { + if err := writeCodexConfig(configPath, "", ""); err != nil { t.Fatal(err) } data, _ := os.ReadFile(configPath) content := string(data) - if strings.Contains(content, `profiles."ollama-launch"`) { - t.Fatalf("quoted profile table should be replaced, got:\n%s", content) + if strings.Contains(content, `profiles."ollama-launch"`) || strings.Contains(content, "[profiles.ollama-launch]") { + t.Fatalf("quoted profile table should be removed, got:\n%s", content) } if strings.Contains(content, "old:1234") { t.Fatalf("old URL was not replaced, got:\n%s", content) } - if got := codexSectionStringValue(content, codexProfileHeader(), "model_provider"); got != codexProfileName { - t.Fatalf("profile model_provider = %q, want %q", got, codexProfileName) + if got, ok := codexRootStringValueOK(content, "profile"); ok { + t.Fatalf("legacy root profile should be removed, got %q in:\n%s", got, content) + } + if got := codexRootStringValue(content, "model_provider"); got != codexProfileName { + t.Fatalf("root model_provider = %q, want %q", got, codexProfileName) } if got := codexSectionStringValue(content, codexProviderHeader(), "base_url"); !strings.Contains(got, "/v1/") { t.Fatalf("provider base_url = %q, want /v1/ URL", got) @@ -180,9 +181,9 @@ func TestWriteCodexProfile(t *testing.T) { existing := "profile = \n" os.WriteFile(configPath, []byte(existing), 0o644) - err := writeCodexProfile(configPath) + err := writeCodexConfig(configPath, "", "") if err == nil || !strings.Contains(err.Error(), "invalid Codex config TOML") { - t.Fatalf("writeCodexProfile error = %v, want invalid TOML", err) + t.Fatalf("writeCodexConfig error = %v, want invalid TOML", err) } data, _ := os.ReadFile(configPath) @@ -206,9 +207,9 @@ func TestWriteCodexProfile(t *testing.T) { t.Fatal(err) } - err := writeCodexProfile(configPath) + err := writeCodexConfig(configPath, "", "") if err == nil || !strings.Contains(err.Error(), "invalid Codex config TOML") { - t.Fatalf("writeCodexProfile error = %v, want invalid TOML", err) + t.Fatalf("writeCodexConfig error = %v, want invalid TOML", err) } data, _ := os.ReadFile(configPath) @@ -231,14 +232,14 @@ func TestWriteCodexProfile(t *testing.T) { t.Fatal(err) } - if err := writeCodexProfile(configPath); err != nil { + if err := writeCodexConfig(configPath, "", ""); err != nil { t.Fatal(err) } assertBackupContains(t, filepath.Join(fileutil.BackupDir(), "config.toml.*"), "original-codex-backup-marker") }) - t.Run("updates equivalent quoted root keys", func(t *testing.T) { + t.Run("updates equivalent quoted root keys and removes legacy profile", func(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.toml") existing := "" + @@ -249,11 +250,7 @@ func TestWriteCodexProfile(t *testing.T) { `model = "gpt-5.5"` + "\n" os.WriteFile(configPath, []byte(existing), 0o644) - err := writeCodexLaunchProfile(configPath, codexLaunchProfileOptions{ - activate: true, - setRootModelConfig: true, - model: "llama3.2", - }) + err := writeCodexConfig(configPath, "llama3.2", "") if err != nil { t.Fatal(err) } @@ -261,7 +258,6 @@ func TestWriteCodexProfile(t *testing.T) { data, _ := os.ReadFile(configPath) content := string(data) for key, want := range map[string]string{ - "profile": codexProfileName, "model": "llama3.2", "model_provider": codexProfileName, } { @@ -269,22 +265,25 @@ func TestWriteCodexProfile(t *testing.T) { t.Fatalf("root %s = %q, want %q in:\n%s", key, got, want, content) } } + if got, ok := codexRootStringValueOK(content, "profile"); ok { + t.Fatalf("legacy root profile should be removed, got %q in:\n%s", got, content) + } if strings.Contains(content, `"profile"`) || strings.Contains(content, `"model_provider"`) { - t.Fatalf("quoted root keys should be rewritten once, got:\n%s", content) + t.Fatalf("quoted root keys should be removed or rewritten once, got:\n%s", content) } if err := codexValidateConfigText(content); err != nil { t.Fatalf("generated config should be valid TOML: %v\n%s", err, content) } }) - t.Run("replaces profile while preserving following sections", func(t *testing.T) { + t.Run("removes profile while preserving following sections", func(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.toml") catalogPath := filepath.Join(tmpDir, "model.json") existing := "[profiles.ollama-launch]\nopenai_base_url = \"http://old:1234/v1/\"\n[another_section]\nfoo = \"bar\"\n" os.WriteFile(configPath, []byte(existing), 0o644) - if err := writeCodexProfile(configPath, catalogPath); err != nil { + if err := writeCodexConfig(configPath, "llama3.2", catalogPath); err != nil { t.Fatal(err) } @@ -294,6 +293,9 @@ func TestWriteCodexProfile(t *testing.T) { if strings.Contains(content, "old:1234") { t.Error("old URL was not replaced") } + if strings.Contains(content, "[profiles.ollama-launch]") { + t.Fatalf("legacy profile section should be removed, got:\n%s", content) + } if !strings.Contains(content, "[another_section]") { t.Error("following section was removed") } @@ -309,15 +311,15 @@ func TestWriteCodexProfile(t *testing.T) { existing := "[other]\nkey = \"val\"" os.WriteFile(configPath, []byte(existing), 0o644) - if err := writeCodexProfile(configPath, catalogPath); err != nil { + if err := writeCodexConfig(configPath, "llama3.2", catalogPath); err != nil { t.Fatal(err) } data, _ := os.ReadFile(configPath) content := string(data) - if !strings.Contains(content, "[profiles.ollama-launch]") { - t.Error("missing [profiles.ollama-launch] header") + if strings.Contains(content, "[profiles.ollama-launch]") { + t.Fatalf("legacy profile section should not be generated, got:\n%s", content) } // Should not have double blank lines from missing trailing newline if strings.Contains(content, "\n\n\n") { @@ -331,7 +333,7 @@ func TestWriteCodexProfile(t *testing.T) { configPath := filepath.Join(tmpDir, "config.toml") catalogPath := filepath.Join(tmpDir, "model.json") - if err := writeCodexProfile(configPath, catalogPath); err != nil { + if err := writeCodexConfig(configPath, "llama3.2", catalogPath); err != nil { t.Fatal(err) } @@ -348,7 +350,7 @@ func TestWriteCodexProfile(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.toml") - if err := writeCodexProfile(configPath); err != nil { + if err := writeCodexConfig(configPath, "", ""); err != nil { t.Fatal(err) } @@ -380,11 +382,17 @@ func TestEnsureCodexConfig(t *testing.T) { } content := string(data) - if !strings.Contains(content, "[profiles.ollama-launch]") { - t.Error("missing [profiles.ollama-launch] header") + if strings.Contains(content, "[profiles.ollama-launch]") { + t.Fatalf("legacy profile section should not be generated, got:\n%s", content) } - if !strings.Contains(content, "openai_base_url") { - t.Error("missing openai_base_url key") + if got := codexRootStringValue(content, "model"); got != "llama3.2" { + t.Fatalf("root model = %q, want llama3.2 in:\n%s", got, content) + } + if got := codexRootStringValue(content, "model_provider"); got != codexProfileName { + t.Fatalf("root model_provider = %q, want %q in:\n%s", got, codexProfileName, content) + } + if got := codexSectionStringValue(content, codexProviderHeader(), "base_url"); !strings.Contains(got, "/v1/") { + t.Fatalf("provider base_url = %q, want /v1/ URL", got) } catalogPath := filepath.Join(tmpDir, ".codex", "model.json") @@ -397,6 +405,40 @@ func TestEnsureCodexConfig(t *testing.T) { } }) + t.Run("writes requested local alias as catalog slug", func(t *testing.T) { + tmpDir := t.TempDir() + setTestHome(t, tmpDir) + + models := []LaunchModel{ + {Name: "gemma4:latest", ContextLength: 65_536, Details: api.ModelDetails{Format: "gguf"}}, + } + if err := ensureCodexConfig("gemma4", models); err != nil { + t.Fatal(err) + } + + catalogPath := filepath.Join(tmpDir, ".codex", "model.json") + data, err := os.ReadFile(catalogPath) + if err != nil { + t.Fatalf("model.json not created: %v", err) + } + + var catalog struct { + Models []map[string]any `json:"models"` + } + if err := json.Unmarshal(data, &catalog); err != nil { + t.Fatalf("model catalog should be valid JSON: %v", err) + } + if len(catalog.Models) != 1 { + t.Fatalf("catalog model count = %d, want 1", len(catalog.Models)) + } + if got := catalog.Models[0]["slug"]; got != "gemma4" { + t.Fatalf("catalog slug = %v, want gemma4", got) + } + if got := catalog.Models[0]["context_window"]; got != float64(65_536) { + t.Fatalf("context_window = %v, want 65536", got) + } + }) + t.Run("is idempotent", func(t *testing.T) { tmpDir := t.TempDir() setTestHome(t, tmpDir) @@ -412,8 +454,8 @@ func TestEnsureCodexConfig(t *testing.T) { data, _ := os.ReadFile(configPath) content := string(data) - if strings.Count(content, "[profiles.ollama-launch]") != 1 { - t.Errorf("expected exactly one [profiles.ollama-launch] section after two calls, got %d", strings.Count(content, "[profiles.ollama-launch]")) + if strings.Contains(content, "[profiles.ollama-launch]") { + t.Fatalf("legacy profile section should not be generated, got:\n%s", content) } if strings.Count(content, "[model_providers.ollama-launch]") != 1 { t.Errorf("expected exactly one [model_providers.ollama-launch] section after two calls, got %d", strings.Count(content, "[model_providers.ollama-launch]"))