mirror of
https://github.com/ollama/ollama.git
synced 2026-07-23 09:10:53 -05:00
launch: isolate Codex launch configuration (#16437)
This commit is contained in:
@@ -24,6 +24,7 @@ const (
|
||||
codexProfileName = "ollama-launch"
|
||||
codexProviderName = "Ollama"
|
||||
codexFallbackContextWindow = 128_000
|
||||
codexRestoreSuccess = "Codex launch configuration removed."
|
||||
|
||||
codexRootProfileKey = "profile"
|
||||
codexRootModelKey = "model"
|
||||
@@ -31,16 +32,20 @@ const (
|
||||
codexRootModelCatalogJSONKey = "model_catalog_json"
|
||||
)
|
||||
|
||||
func (c *Codex) args(model, modelCatalogPath string, extra []string) []string {
|
||||
args := []string{}
|
||||
if modelCatalogPath != "" {
|
||||
args = append(args, "-c", fmt.Sprintf("%s=%q", codexRootModelCatalogJSONKey, modelCatalogPath))
|
||||
func (c *Codex) args(model, modelCatalogPath string, extra []string) ([]string, error) {
|
||||
if err := codexValidateExtraArgs(extra); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args := []string{"--profile", codexProfileName}
|
||||
for _, override := range codexManagedConfigOverrides(modelCatalogPath) {
|
||||
args = append(args, "-c", override)
|
||||
}
|
||||
if model != "" {
|
||||
args = append(args, "-m", model)
|
||||
}
|
||||
args = append(args, extra...)
|
||||
return args
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (c *Codex) Run(model string, models []LaunchModel, args []string) error {
|
||||
@@ -57,7 +62,12 @@ func (c *Codex) Run(model string, models []LaunchModel, args []string) error {
|
||||
return fmt.Errorf("failed to configure codex: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("codex", c.args(model, catalogPath, args)...)
|
||||
codexArgs, err := c.args(model, catalogPath, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to configure codex: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("codex", codexArgs...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -67,8 +77,134 @@ func (c *Codex) Run(model string, models []LaunchModel, args []string) error {
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// ensureCodexConfig writes Codex root config and model catalog so Codex uses the
|
||||
// local Ollama server and has model metadata available.
|
||||
func (c *Codex) Restore() error {
|
||||
configPath, err := codexConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := removeCodexProfileConfig(); err != nil {
|
||||
return codexRestoreFailure(configPath, err)
|
||||
}
|
||||
if err := removeCodexModelCatalogIfUnused(configPath); err != nil {
|
||||
return codexRestoreFailure(configPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Codex) RestoreSuccessMessage() string {
|
||||
return codexRestoreSuccess
|
||||
}
|
||||
|
||||
func (c *Codex) SkipRestoreInstallCheck() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func codexRestoreFailure(configPath string, err error) error {
|
||||
return fmt.Errorf("restore Codex config: %w\n\nRestore did not complete. Check these files before retrying:\n Codex config: %s\n CLI profile: %s\n CLI model catalog: %s\n Backups: %s",
|
||||
err,
|
||||
configPath,
|
||||
codexProfileConfigPathForConfig(configPath),
|
||||
codexModelCatalogPathForConfig(configPath),
|
||||
fileutil.BackupDir(),
|
||||
)
|
||||
}
|
||||
|
||||
func removeCodexProfileConfig() error {
|
||||
profilePath, err := codexProfileConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return removeCodexFile(profilePath)
|
||||
}
|
||||
|
||||
func removeCodexModelCatalogIfUnused(configPath string) error {
|
||||
catalogPath := codexModelCatalogPathForConfig(configPath)
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
config, parseErr := codexParseConfig(string(data))
|
||||
if parseErr != nil {
|
||||
return parseErr
|
||||
}
|
||||
if config.RootString(codexRootModelCatalogJSONKey) == catalogPath {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return removeCodexFile(catalogPath)
|
||||
}
|
||||
|
||||
func removeCodexFile(path string) error {
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func codexValidateExtraArgs(args []string) error {
|
||||
for i, arg := range args {
|
||||
switch {
|
||||
case arg == "-p", strings.HasPrefix(arg, "-p"):
|
||||
return fmt.Errorf("conflicting extra argument %q: ollama launch codex manages --profile", arg)
|
||||
case arg == "--profile", strings.HasPrefix(arg, "--profile="):
|
||||
return fmt.Errorf("conflicting extra argument %q: ollama launch codex manages --profile", arg)
|
||||
case arg == "-m", strings.HasPrefix(arg, "-m"):
|
||||
return fmt.Errorf("conflicting extra argument %q: ollama launch codex manages --model", arg)
|
||||
case arg == "--model", strings.HasPrefix(arg, "--model="):
|
||||
return fmt.Errorf("conflicting extra argument %q: ollama launch codex manages --model", arg)
|
||||
case arg == "-c", arg == "--config":
|
||||
if i+1 < len(args) && codexConfigOverrideConflicts(args[i+1]) {
|
||||
return fmt.Errorf("conflicting extra config %q: ollama launch codex manages provider and model catalog config", args[i+1])
|
||||
}
|
||||
case strings.HasPrefix(arg, "-c") && len(arg) > len("-c"):
|
||||
if codexConfigOverrideConflicts(strings.TrimPrefix(arg, "-c")) {
|
||||
return fmt.Errorf("conflicting extra config %q: ollama launch codex manages provider and model catalog config", arg)
|
||||
}
|
||||
case strings.HasPrefix(arg, "--config="):
|
||||
if codexConfigOverrideConflicts(strings.TrimPrefix(arg, "--config=")) {
|
||||
return fmt.Errorf("conflicting extra config %q: ollama launch codex manages provider and model catalog config", arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func codexManagedConfigOverrides(modelCatalogPath string) []string {
|
||||
overrides := []string{
|
||||
fmt.Sprintf("%s=%q", codexRootModelProviderKey, codexProfileName),
|
||||
fmt.Sprintf("model_providers.%s.name=%q", codexProfileName, codexProviderName),
|
||||
fmt.Sprintf("model_providers.%s.base_url=%q", codexProfileName, codexBaseURL()),
|
||||
fmt.Sprintf("model_providers.%s.wire_api=%q", codexProfileName, "responses"),
|
||||
}
|
||||
if modelCatalogPath != "" {
|
||||
overrides = append(overrides, fmt.Sprintf("%s=%q", codexRootModelCatalogJSONKey, modelCatalogPath))
|
||||
}
|
||||
return overrides
|
||||
}
|
||||
|
||||
func codexConfigOverrideConflicts(value string) bool {
|
||||
key, _, ok := strings.Cut(strings.TrimSpace(value), "=")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
key = strings.TrimSpace(key)
|
||||
key = strings.Trim(key, `"'`)
|
||||
switch {
|
||||
case key == codexRootProfileKey,
|
||||
key == codexRootModelKey,
|
||||
key == codexRootModelProviderKey,
|
||||
key == codexRootModelCatalogJSONKey:
|
||||
return true
|
||||
case strings.HasPrefix(key, "model_providers."):
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ensureCodexConfig writes a Codex profile file and model catalog so Codex uses
|
||||
// the local Ollama server without changing app-visible root config.
|
||||
func ensureCodexConfig(modelName string, models []LaunchModel) error {
|
||||
configPath, err := codexConfigPath()
|
||||
if err != nil {
|
||||
@@ -85,7 +221,8 @@ func ensureCodexConfig(modelName string, models []LaunchModel) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeCodexConfig(configPath, modelName, catalogPath)
|
||||
profilePath := codexProfileConfigPathForConfig(configPath)
|
||||
return writeCodexProfileConfig(profilePath, modelName, catalogPath)
|
||||
}
|
||||
|
||||
func codexConfigPath() (string, error) {
|
||||
@@ -108,49 +245,60 @@ func codexModelCatalogPathForConfig(configPath string) string {
|
||||
return filepath.Join(filepath.Dir(configPath), "model.json")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func codexProfileConfigPath() (string, error) {
|
||||
configPath, err := codexConfigPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return codexProfileConfigPathForConfig(configPath), nil
|
||||
}
|
||||
|
||||
func codexProfileConfigPathForConfig(configPath string) string {
|
||||
return codexNamedProfileConfigPathForConfig(configPath, codexProfileName)
|
||||
}
|
||||
|
||||
func codexNamedProfileConfigPathForConfig(configPath, profileName string) string {
|
||||
return filepath.Join(filepath.Dir(configPath), profileName+".config.toml")
|
||||
}
|
||||
|
||||
// writeCodexProfileConfig ensures ~/.codex/ollama-launch.config.toml selects
|
||||
// the Ollama provider and catalog for CLI launches without changing root config.
|
||||
func writeCodexProfileConfig(profilePath, model, modelCatalogPath string) error {
|
||||
return writeCodexNamedProfileConfig(profilePath, codexProfileName, model, modelCatalogPath, "")
|
||||
}
|
||||
|
||||
func writeCodexNamedProfileConfig(profilePath, profileName, model, modelCatalogPath, backupSubdir string) error {
|
||||
baseURL := codexBaseURL()
|
||||
|
||||
content, readErr := os.ReadFile(configPath)
|
||||
text := ""
|
||||
if readErr == nil {
|
||||
text = string(content)
|
||||
} else if !os.IsNotExist(readErr) {
|
||||
return readErr
|
||||
}
|
||||
if _, err := codexParseConfig(text); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
text = codexRemoveRootValue(text, codexRootProfileKey)
|
||||
text = codexRemoveSection(text, codexProfileHeaderFor(codexProfileName))
|
||||
var lines []string
|
||||
if strings.TrimSpace(model) != "" {
|
||||
text = codexSetRootStringValue(text, codexRootModelKey, model)
|
||||
lines = append(lines, fmt.Sprintf("%s = %q", codexRootModelKey, model))
|
||||
}
|
||||
text = codexSetRootStringValue(text, codexRootModelProviderKey, codexProfileName)
|
||||
lines = append(lines, fmt.Sprintf("%s = %q", codexRootModelProviderKey, profileName))
|
||||
if strings.TrimSpace(modelCatalogPath) != "" {
|
||||
text = codexSetRootStringValue(text, codexRootModelCatalogJSONKey, modelCatalogPath)
|
||||
lines = append(lines, fmt.Sprintf("%s = %q", codexRootModelCatalogJSONKey, modelCatalogPath))
|
||||
}
|
||||
text = codexUpsertSection(text, codexProviderHeaderFor(codexProfileName), []string{
|
||||
text := strings.Join(lines, "\n") + "\n\n"
|
||||
text += strings.Join([]string{
|
||||
codexProviderHeaderFor(profileName),
|
||||
fmt.Sprintf("name = %q", codexProviderName),
|
||||
fmt.Sprintf("base_url = %q", baseURL),
|
||||
`wire_api = "responses"`,
|
||||
})
|
||||
"",
|
||||
}, "\n")
|
||||
|
||||
parsed, err := codexParseConfig(text)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := codexValidateConfigTextForOllama(parsed, model, modelCatalogPath, baseURL); err != nil {
|
||||
if err := codexValidateProfileConfigText(parsed, profileName, model, modelCatalogPath, baseURL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(profilePath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return fileutil.WriteWithBackup(configPath, []byte(text), "")
|
||||
return fileutil.WriteWithBackup(profilePath, []byte(text), backupSubdir)
|
||||
}
|
||||
|
||||
func codexBaseURL() string {
|
||||
@@ -173,26 +321,28 @@ func codexProviderHeaderFor(profileName string) string {
|
||||
return fmt.Sprintf("[model_providers.%s]", profileName)
|
||||
}
|
||||
|
||||
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)
|
||||
func codexValidateProfileConfigText(config codexParsedConfig, profileName, model, modelCatalogPath, baseURL string) error {
|
||||
if config.Exists("profiles", profileName) {
|
||||
return fmt.Errorf("generated Codex config still contains legacy profiles.%s table", profileName)
|
||||
}
|
||||
for _, check := range []struct {
|
||||
path []string
|
||||
want string
|
||||
}{
|
||||
{[]string{codexRootModelProviderKey}, codexProfileName},
|
||||
{[]string{"model_providers", codexProfileName, "name"}, codexProviderName},
|
||||
{[]string{"model_providers", codexProfileName, "base_url"}, baseURL},
|
||||
{[]string{"model_providers", codexProfileName, "wire_api"}, "responses"},
|
||||
{[]string{"model_providers", profileName, "name"}, codexProviderName},
|
||||
{[]string{"model_providers", profileName, "base_url"}, baseURL},
|
||||
{[]string{"model_providers", profileName, "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 got, ok := config.RootStringOK(codexRootProfileKey); ok {
|
||||
return fmt.Errorf("generated Codex config still contains legacy profile = %q", got)
|
||||
}
|
||||
if got := config.RootString(codexRootModelProviderKey); got != profileName {
|
||||
return fmt.Errorf("generated Codex config missing model_provider = %q", profileName)
|
||||
}
|
||||
if model != "" {
|
||||
if got := config.RootString(codexRootModelKey); got != model {
|
||||
return fmt.Errorf("generated Codex config missing model = %q", model)
|
||||
@@ -586,10 +736,10 @@ func checkCodexVersion() error {
|
||||
}
|
||||
|
||||
version := "v" + fields[len(fields)-1]
|
||||
minVersion := "v0.81.0"
|
||||
minVersion := "v0.134.0"
|
||||
|
||||
if semver.Compare(version, minVersion) < 0 {
|
||||
return fmt.Errorf("codex version %s is too old, minimum required is %s, update with: npm update -g @openai/codex", fields[len(fields)-1], "0.81.0")
|
||||
return fmt.Errorf("codex version %s is too old, minimum required is %s, update with: npm update -g @openai/codex", fields[len(fields)-1], "0.134.0")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -259,7 +259,7 @@ func (c *CodexApp) Run(_ string, _ []LaunchModel, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return fmt.Errorf("codex-app does not accept extra arguments")
|
||||
}
|
||||
return codexAppLaunchOrRestart("Restart Codex to use Ollama?")
|
||||
return codexAppLaunchOrRestart("Restart Codex to use Ollama?", nil)
|
||||
}
|
||||
|
||||
func (c *CodexApp) Restore() error {
|
||||
@@ -277,7 +277,13 @@ func (c *CodexApp) Restore() error {
|
||||
if err := removeCodexAppRestoreState(); err != nil {
|
||||
return codexAppRestoreFailure(configPath, err)
|
||||
}
|
||||
return codexAppLaunchOrRestart("Restart Codex to use your usual profile?")
|
||||
if err := removeCodexAppProfileConfig(); err != nil {
|
||||
return codexAppRestoreFailure(configPath, err)
|
||||
}
|
||||
if err := codexAppRemoveOwnedCatalog(); err != nil {
|
||||
return codexAppRestoreFailure(configPath, err)
|
||||
}
|
||||
return codexAppLaunchOrRestart("Restart Codex to use your usual profile?", nil)
|
||||
}
|
||||
return codexAppRestoreFailure(configPath, err)
|
||||
}
|
||||
@@ -304,13 +310,16 @@ func (c *CodexApp) Restore() error {
|
||||
if err := fileutil.WriteWithBackup(configPath, []byte(text), codexAppIntegrationName); err != nil {
|
||||
return codexAppRestoreFailure(configPath, err)
|
||||
}
|
||||
if err := removeCodexAppProfileConfig(); err != nil {
|
||||
return codexAppRestoreFailure(configPath, err)
|
||||
}
|
||||
if err := codexAppRemoveOwnedCatalogIfUnused(text); err != nil {
|
||||
return codexAppRestoreFailure(configPath, err)
|
||||
}
|
||||
if err := removeCodexAppRestoreState(); err != nil {
|
||||
return codexAppRestoreFailure(configPath, err)
|
||||
}
|
||||
return codexAppLaunchOrRestart("Restart Codex to use your usual profile?")
|
||||
return codexAppLaunchOrRestart("Restart Codex to use your usual profile?", nil)
|
||||
}
|
||||
|
||||
func codexAppRestoreFailure(configPath string, err error) error {
|
||||
@@ -354,6 +363,18 @@ func codexAppModelCatalogPath() (string, error) {
|
||||
return codexAppModelCatalogPathForConfig(configPath), nil
|
||||
}
|
||||
|
||||
func codexAppProfileConfigPath() (string, error) {
|
||||
configPath, err := codexConfigPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return codexAppProfileConfigPathForConfig(configPath), nil
|
||||
}
|
||||
|
||||
func codexAppProfileConfigPathForConfig(configPath string) string {
|
||||
return codexNamedProfileConfigPathForConfig(configPath, codexAppProfileName)
|
||||
}
|
||||
|
||||
func codexAppModelCatalogPathForConfig(configPath string) string {
|
||||
return filepath.Join(filepath.Dir(configPath), codexAppModelCatalogFilename)
|
||||
}
|
||||
@@ -383,14 +404,20 @@ func codexAppCatalogModels(primary string, models []LaunchModel) []LaunchModel {
|
||||
seen := make(map[string]bool, len(models)+1)
|
||||
out := make([]LaunchModel, 0, len(models)+1)
|
||||
add := func(model LaunchModel) {
|
||||
if model.Name == "" || seen[model.Name] {
|
||||
model.Name = strings.TrimSpace(model.Name)
|
||||
if model.Name == "" {
|
||||
return
|
||||
}
|
||||
seen[model.Name] = true
|
||||
key := codexAppCatalogModelKey(model.Name)
|
||||
if seen[key] {
|
||||
return
|
||||
}
|
||||
seen[key] = true
|
||||
out = append(out, model)
|
||||
}
|
||||
|
||||
if model, ok := findLaunchModel(models, primary); ok {
|
||||
model.Name = primary
|
||||
add(model)
|
||||
} else {
|
||||
add(fallbackLaunchModel(primary))
|
||||
@@ -401,6 +428,10 @@ func codexAppCatalogModels(primary string, models []LaunchModel) []LaunchModel {
|
||||
return out
|
||||
}
|
||||
|
||||
func codexAppCatalogModelKey(name string) string {
|
||||
return strings.TrimSuffix(name, ":latest")
|
||||
}
|
||||
|
||||
type codexAppModelMetadata struct {
|
||||
contextWindow int
|
||||
inputModalities []string
|
||||
@@ -579,13 +610,13 @@ func codexAppLocalAppData() (string, error) {
|
||||
return filepath.Join(home, "AppData", "Local"), nil
|
||||
}
|
||||
|
||||
func codexAppLaunchOrRestart(prompt string) error {
|
||||
func codexAppLaunchOrRestart(prompt string, launchArgs []string) error {
|
||||
if !codexAppIsRunning() {
|
||||
return codexAppOpenApp()
|
||||
return codexAppOpenApp(launchArgs)
|
||||
}
|
||||
restartAppID := ""
|
||||
restartAppPath := ""
|
||||
if codexAppGOOS == "windows" {
|
||||
if len(launchArgs) == 0 && codexAppGOOS == "windows" {
|
||||
restartAppID = codexAppStartID()
|
||||
if restartAppID == "" {
|
||||
restartAppPath = codexAppRunPath()
|
||||
@@ -626,7 +657,7 @@ func codexAppLaunchOrRestart(prompt string) error {
|
||||
if restartAppPath != "" {
|
||||
return codexAppOpenPath(restartAppPath)
|
||||
}
|
||||
return codexAppOpenApp()
|
||||
return codexAppOpenApp(launchArgs)
|
||||
}
|
||||
|
||||
func codexAppForceQuitSupported() bool {
|
||||
@@ -659,7 +690,15 @@ func waitForCodexAppCondition(timeout time.Duration, done func() bool) error {
|
||||
return fmt.Errorf("Codex did not quit; quit it manually and re-run the command")
|
||||
}
|
||||
|
||||
func defaultCodexAppOpenApp() error {
|
||||
func defaultCodexAppOpenApp(args []string) error {
|
||||
if len(args) > 0 {
|
||||
cmd := exec.Command("codex", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = append(os.Environ(), "OPENAI_API_KEY=ollama")
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
switch codexAppGOOS {
|
||||
case "windows":
|
||||
if path := codexAppAppPath(); path != "" {
|
||||
@@ -922,6 +961,10 @@ func codexAppRemoveOwnedCatalogIfUnused(text string) error {
|
||||
if codexAppRootReferencesCatalog(text) {
|
||||
return nil
|
||||
}
|
||||
return codexAppRemoveOwnedCatalog()
|
||||
}
|
||||
|
||||
func codexAppRemoveOwnedCatalog() error {
|
||||
if catalogPath, err := codexAppModelCatalogPath(); err == nil {
|
||||
if err := os.Remove(catalogPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
@@ -932,6 +975,17 @@ func codexAppRemoveOwnedCatalogIfUnused(text string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeCodexAppProfileConfig() error {
|
||||
profilePath, err := codexAppProfileConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(profilePath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func codexAppRemoveOwnedRootValues(text string) string {
|
||||
config, err := codexParseConfig(text)
|
||||
if err != nil {
|
||||
@@ -1022,7 +1076,11 @@ func saveCodexAppRestoreState(configPath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeCodexAppRestoreState(codexAppRestoreStateFromText(configText))
|
||||
state := codexAppRestoreStateFromText(configText)
|
||||
if codexAppRootStillManaged(configText) {
|
||||
state = codexAppRestoreState{}
|
||||
}
|
||||
return writeCodexAppRestoreState(state)
|
||||
}
|
||||
|
||||
func codexAppRestoreStateHasRootConfig(data []byte) (bool, error) {
|
||||
|
||||
@@ -39,7 +39,7 @@ func withCodexAppProcessHooks(t *testing.T, isRunning func() bool, quit func() e
|
||||
codexAppIsRunning = isRunning
|
||||
codexAppHasWindow = isRunning
|
||||
codexAppQuitApp = quit
|
||||
codexAppOpenApp = open
|
||||
codexAppOpenApp = func([]string) error { return open() }
|
||||
t.Cleanup(func() {
|
||||
codexAppIsRunning = oldIsRunning
|
||||
codexAppQuitApp = oldQuit
|
||||
@@ -180,15 +180,15 @@ func TestCodexAppConfigureActivatesOllamaProviderWithoutLegacyProfile(t *testing
|
||||
t.Fatalf("ConfigureWithModels returned error: %v", err)
|
||||
}
|
||||
|
||||
catalogPath, err := codexAppModelCatalogPath()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := string(data)
|
||||
catalogPath, err := codexAppModelCatalogPath()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, want := range []string{
|
||||
`model = "llama3.2"`,
|
||||
@@ -291,6 +291,98 @@ func TestCodexAppConfigureUsesAppSpecificProfileWithoutTouchingCLIProfile(t *tes
|
||||
assertBackupContains(t, filepath.Join(fileutil.BackupDir(), codexAppIntegrationName, "config.toml.*"), `profile = "default"`)
|
||||
}
|
||||
|
||||
func TestCodexCLIConfigRefreshLeavesCodexAppConfigActive(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
t.Setenv("OLLAMA_HOST", "http://127.0.0.1:9999")
|
||||
|
||||
appModels := testLaunchModels("llama3.2", "gemma4")
|
||||
if err := (&CodexApp{}).ConfigureWithModels("llama3.2", appModels); err != nil {
|
||||
t.Fatalf("ConfigureWithModels returned error: %v", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tmpDir, ".codex", "config.toml")
|
||||
appCatalogPath := mustCodexAppModelCatalogPath(t)
|
||||
if err := ensureCodexConfig("qwen3:8b", testLaunchModels("qwen3:8b")); err != nil {
|
||||
t.Fatalf("ensureCodexConfig returned error: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := string(data)
|
||||
if got, ok := codexRootStringValueOK(content, "profile"); ok {
|
||||
t.Fatalf("CLI config refresh should not activate a root profile, got %q in:\n%s", got, content)
|
||||
}
|
||||
for key, want := range map[string]string{
|
||||
"model": "llama3.2",
|
||||
"model_provider": codexAppProfileName,
|
||||
"model_catalog_json": appCatalogPath,
|
||||
} {
|
||||
if got := codexRootStringValue(content, key); got != want {
|
||||
t.Fatalf("root %s = %q, want %q in:\n%s", key, got, want, content)
|
||||
}
|
||||
}
|
||||
if got := codexSectionStringValue(content, codexProviderHeaderFor(codexAppProfileName), "base_url"); got != "http://127.0.0.1:9999/v1/" {
|
||||
t.Fatalf("app provider base URL = %q", got)
|
||||
}
|
||||
cliCatalogPath := filepath.Join(tmpDir, ".codex", "model.json")
|
||||
if strings.Contains(content, codexProfileHeader()) {
|
||||
t.Fatalf("CLI legacy profile section should not be generated, got:\n%s", content)
|
||||
}
|
||||
if strings.Contains(content, codexProviderHeader()) {
|
||||
t.Fatalf("CLI provider should be isolated from app root config, got:\n%s", content)
|
||||
}
|
||||
|
||||
cliProfilePath := filepath.Join(tmpDir, ".codex", "ollama-launch.config.toml")
|
||||
cliProfileData, err := os.ReadFile(cliProfilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("CLI profile config not created: %v", err)
|
||||
}
|
||||
cliProfile := string(cliProfileData)
|
||||
for key, want := range map[string]string{
|
||||
"model": "qwen3:8b",
|
||||
"model_provider": codexProfileName,
|
||||
"model_catalog_json": cliCatalogPath,
|
||||
} {
|
||||
if got := codexRootStringValue(cliProfile, key); got != want {
|
||||
t.Fatalf("CLI profile %s = %q, want %q in:\n%s", key, got, want, cliProfile)
|
||||
}
|
||||
}
|
||||
if got := codexSectionStringValue(cliProfile, codexProviderHeader(), "base_url"); got != "http://127.0.0.1:9999/v1/" {
|
||||
t.Fatalf("CLI profile provider base URL = %q", got)
|
||||
}
|
||||
|
||||
appCatalogData, err := os.ReadFile(appCatalogPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var appCatalog struct {
|
||||
Models []map[string]any `json:"models"`
|
||||
}
|
||||
if err := json.Unmarshal(appCatalogData, &appCatalog); err != nil {
|
||||
t.Fatalf("app catalog should be valid JSON: %v", err)
|
||||
}
|
||||
if got := catalogSlugs(appCatalog.Models); strings.Join(got, ",") != "llama3.2,gemma4" {
|
||||
t.Fatalf("app catalog slugs = %v, want original app models", got)
|
||||
}
|
||||
|
||||
cliCatalogData, err := os.ReadFile(cliCatalogPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cliCatalog struct {
|
||||
Models []map[string]any `json:"models"`
|
||||
}
|
||||
if err := json.Unmarshal(cliCatalogData, &cliCatalog); err != nil {
|
||||
t.Fatalf("CLI catalog should be valid JSON: %v", err)
|
||||
}
|
||||
if got := catalogSlugs(cliCatalog.Models); strings.Join(got, ",") != "qwen3:8b" {
|
||||
t.Fatalf("CLI catalog slugs = %v, want qwen3:8b", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexAppConfigureUsesConnectableHostForUnspecifiedBindAddress(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
@@ -595,6 +687,52 @@ func TestCodexAppConfigurePopulatesCatalogFromEnrichedModels(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexAppConfigureCatalogIncludesExactSelectedModel(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
|
||||
models := []LaunchModel{
|
||||
{Name: "llama3.2:latest", ContextLength: 65_536},
|
||||
{Name: "qwen3:8b"},
|
||||
}
|
||||
if err := (&CodexApp{}).ConfigureWithModels("llama3.2", models); err != nil {
|
||||
t.Fatalf("ConfigureWithModels returned error: %v", err)
|
||||
}
|
||||
|
||||
configPath, err := codexConfigPath()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
configData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := codexRootStringValue(string(configData), codexRootModelKey); got != "llama3.2" {
|
||||
t.Fatalf("root model = %q, want llama3.2", got)
|
||||
}
|
||||
|
||||
catalogPath, err := codexAppModelCatalogPath()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := os.ReadFile(catalogPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var catalog struct {
|
||||
Models []map[string]any `json:"models"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &catalog); err != nil {
|
||||
t.Fatalf("catalog should be valid JSON: %v", err)
|
||||
}
|
||||
if got := catalogSlugs(catalog.Models); strings.Join(got, ",") != "llama3.2,qwen3:8b" {
|
||||
t.Fatalf("catalog slugs = %v, want exact selected model without :latest duplicate", got)
|
||||
}
|
||||
if got := catalog.Models[0]["context_window"]; got != float64(65_536) {
|
||||
t.Fatalf("selected model context_window = %v, want 65536", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexAppConfigureUpgradesLegacyRestoreState(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
@@ -1339,7 +1477,7 @@ func TestCodexAppRunRestartsWindowsStartAppID(t *testing.T) {
|
||||
defer restoreConfirm()
|
||||
|
||||
running := true
|
||||
var quitCalls int
|
||||
var quitCalls, openCalls int
|
||||
withCodexAppProcessHooks(t,
|
||||
func() bool { return running },
|
||||
func() error {
|
||||
@@ -1348,7 +1486,7 @@ func TestCodexAppRunRestartsWindowsStartAppID(t *testing.T) {
|
||||
return nil
|
||||
},
|
||||
func() error {
|
||||
t.Fatal("open app fallback should not be used")
|
||||
openCalls++
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package launch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -14,10 +15,30 @@ import (
|
||||
modelpkg "github.com/ollama/ollama/types/model"
|
||||
)
|
||||
|
||||
func TestCodexIntegration(t *testing.T) {
|
||||
c := &Codex{}
|
||||
|
||||
t.Run("implements runner", func(t *testing.T) {
|
||||
var _ Runner = c
|
||||
})
|
||||
t.Run("implements restore", func(t *testing.T) {
|
||||
var _ RestorableIntegration = c
|
||||
var _ RestoreSuccessIntegration = c
|
||||
var _ RestoreInstallCheckSkipper = c
|
||||
})
|
||||
}
|
||||
|
||||
func TestCodexArgs(t *testing.T) {
|
||||
c := &Codex{}
|
||||
catalogPath := filepath.Join("tmp", "model.json")
|
||||
catalogArg := fmt.Sprintf("%s=%q", codexRootModelCatalogJSONKey, catalogPath)
|
||||
managedArgs := []string{
|
||||
"--profile", "ollama-launch",
|
||||
"-c", fmt.Sprintf("%s=%q", codexRootModelProviderKey, codexProfileName),
|
||||
"-c", fmt.Sprintf("model_providers.%s.name=%q", codexProfileName, codexProviderName),
|
||||
"-c", fmt.Sprintf("model_providers.%s.base_url=%q", codexProfileName, codexBaseURL()),
|
||||
"-c", fmt.Sprintf("model_providers.%s.wire_api=%q", codexProfileName, "responses"),
|
||||
"-c", fmt.Sprintf("%s=%q", codexRootModelCatalogJSONKey, catalogPath),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -25,15 +46,17 @@ func TestCodexArgs(t *testing.T) {
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{"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"}},
|
||||
{"with model", "llama3.2", nil, append(slices.Clone(managedArgs), "-m", "llama3.2")},
|
||||
{"empty model", "", nil, managedArgs},
|
||||
{"with sandbox flag", "llama3.2", []string{"--sandbox", "workspace-write"}, append(append(slices.Clone(managedArgs), "-m", "llama3.2"), "--sandbox", "workspace-write")},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := c.args(tt.model, catalogPath, tt.args)
|
||||
got, err := c.args(tt.model, catalogPath, tt.args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !slices.Equal(got, tt.want) {
|
||||
t.Errorf("args(%q, %v) = %v, want %v", tt.model, tt.args, got, tt.want)
|
||||
}
|
||||
@@ -41,17 +64,54 @@ func TestCodexArgs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteCodexConfig(t *testing.T) {
|
||||
func TestCodexArgsRejectManagedProfile(t *testing.T) {
|
||||
c := &Codex{}
|
||||
for _, extra := range [][]string{
|
||||
{"-p", "myprofile"},
|
||||
{"-pmyprofile"},
|
||||
{"--profile", "myprofile"},
|
||||
{"--profile=myprofile"},
|
||||
} {
|
||||
t.Run(strings.Join(extra, " "), func(t *testing.T) {
|
||||
_, err := c.args("llama3.2", "", extra)
|
||||
if err == nil || !strings.Contains(err.Error(), "manages --profile") {
|
||||
t.Fatalf("args error = %v, want profile conflict", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexArgsRejectManagedOverrides(t *testing.T) {
|
||||
c := &Codex{}
|
||||
for _, extra := range [][]string{
|
||||
{"-m", "other"},
|
||||
{"-mother"},
|
||||
{"--model", "other"},
|
||||
{"--model=other"},
|
||||
{"-c", `model_catalog_json="/tmp/other.json"`},
|
||||
{"--config", `model_provider="openai"`},
|
||||
{"--config=model_providers.ollama-launch.base_url=\"http://other.invalid/v1/\""},
|
||||
} {
|
||||
t.Run(strings.Join(extra, " "), func(t *testing.T) {
|
||||
_, err := c.args("llama3.2", "", extra)
|
||||
if err == nil {
|
||||
t.Fatalf("args error = nil, want managed config conflict")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteCodexProfileConfig(t *testing.T) {
|
||||
t.Run("creates new file when none exists", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.toml")
|
||||
profilePath := filepath.Join(tmpDir, "ollama-launch.config.toml")
|
||||
catalogPath := filepath.Join(tmpDir, "model.json")
|
||||
|
||||
if err := writeCodexConfig(configPath, "llama3.2", catalogPath); err != nil {
|
||||
if err := writeCodexProfileConfig(profilePath, "llama3.2", catalogPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
data, err := os.ReadFile(profilePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -81,263 +141,40 @@ func TestWriteCodexConfig(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 := writeCodexConfig(configPath, "llama3.2", catalogPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(configPath)
|
||||
content := string(data)
|
||||
|
||||
if !strings.Contains(content, "[some_other_section]") {
|
||||
t.Error("existing section was removed")
|
||||
}
|
||||
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("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 := writeCodexConfig(configPath, "llama3.2", catalogPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(configPath)
|
||||
content := string(data)
|
||||
|
||||
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.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]"))
|
||||
}
|
||||
if err := codexValidateConfigText(content); err != nil {
|
||||
t.Fatalf("generated config should be valid TOML: %v\n%s", err, content)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("removes equivalent quoted profile table", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.toml")
|
||||
existing := "" +
|
||||
`profile = "default"` + "\n\n" +
|
||||
`[profiles."ollama-launch"]` + "\n" +
|
||||
`openai_base_url = "http://old:1234/v1/"` + "\n\n" +
|
||||
`[model_providers."ollama-launch"]` + "\n" +
|
||||
`name = "Old"` + "\n" +
|
||||
`base_url = "http://old:1234/v1/"` + "\n\n" +
|
||||
`[profiles.default]` + "\n" +
|
||||
`model = "gpt-5.5"` + "\n"
|
||||
os.WriteFile(configPath, []byte(existing), 0o644)
|
||||
|
||||
if err := writeCodexConfig(configPath, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(configPath)
|
||||
content := string(data)
|
||||
|
||||
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, 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)
|
||||
}
|
||||
if err := codexValidateConfigText(content); err != nil {
|
||||
t.Fatalf("generated config should be valid TOML: %v\n%s", err, content)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects invalid existing toml without writing", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.toml")
|
||||
existing := "profile = \n"
|
||||
os.WriteFile(configPath, []byte(existing), 0o644)
|
||||
|
||||
err := writeCodexConfig(configPath, "", "")
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid Codex config TOML") {
|
||||
t.Fatalf("writeCodexConfig error = %v, want invalid TOML", err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(configPath)
|
||||
if string(data) != existing {
|
||||
t.Fatalf("invalid config should be left untouched, got:\n%s", data)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects malformed existing toml variants without writing", func(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"duplicate root key": "profile = \"default\"\nprofile = \"other\"\n",
|
||||
"unterminated string": "model = \"gpt-5.5\n",
|
||||
"bad table": "[profiles.ollama-launch\nmodel = \"llama3.2\"\n",
|
||||
"duplicate table key": "[profiles.ollama-launch]\nmodel = \"a\"\nmodel = \"b\"\n",
|
||||
}
|
||||
for name, existing := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.toml")
|
||||
if err := os.WriteFile(configPath, []byte(existing), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err := writeCodexConfig(configPath, "", "")
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid Codex config TOML") {
|
||||
t.Fatalf("writeCodexConfig error = %v, want invalid TOML", err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(configPath)
|
||||
if string(data) != existing {
|
||||
t.Fatalf("invalid config should be left untouched, got:\n%s", data)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("backs up previous config before overwrite", func(t *testing.T) {
|
||||
t.Run("overwrites owned profile and backs up previous profile", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
configPath := filepath.Join(tmpDir, ".codex", "config.toml")
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
profilePath := filepath.Join(tmpDir, ".codex", "ollama-launch.config.toml")
|
||||
if err := os.MkdirAll(filepath.Dir(profilePath), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
existing := "# original-codex-backup-marker\n[profiles.default]\nmodel = \"gpt-5.5\"\n"
|
||||
if err := os.WriteFile(configPath, []byte(existing), 0o644); err != nil {
|
||||
existing := "# original-codex-profile-backup-marker\nmodel = \"old\"\nmodel_provider = \"old-provider\"\n"
|
||||
if err := os.WriteFile(profilePath, []byte(existing), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := writeCodexConfig(configPath, "", ""); err != nil {
|
||||
if err := writeCodexProfileConfig(profilePath, "llama3.2", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assertBackupContains(t, filepath.Join(fileutil.BackupDir(), "config.toml.*"), "original-codex-backup-marker")
|
||||
})
|
||||
|
||||
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 := "" +
|
||||
`"profile" = "default"` + "\n" +
|
||||
`"model" = "gpt-5.5"` + "\n" +
|
||||
`"model_provider" = "openai"` + "\n\n" +
|
||||
`[profiles.default]` + "\n" +
|
||||
`model = "gpt-5.5"` + "\n"
|
||||
os.WriteFile(configPath, []byte(existing), 0o644)
|
||||
|
||||
err := writeCodexConfig(configPath, "llama3.2", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(configPath)
|
||||
data, _ := os.ReadFile(profilePath)
|
||||
content := string(data)
|
||||
for key, want := range map[string]string{
|
||||
"model": "llama3.2",
|
||||
"model_provider": codexProfileName,
|
||||
} {
|
||||
if got := codexRootStringValue(content, key); got != want {
|
||||
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 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("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 := writeCodexConfig(configPath, "llama3.2", catalogPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(configPath)
|
||||
content := string(data)
|
||||
|
||||
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")
|
||||
}
|
||||
if !strings.Contains(content, "foo = \"bar\"") {
|
||||
t.Error("following section content was removed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("appends newline to file not ending with newline", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.toml")
|
||||
catalogPath := filepath.Join(tmpDir, "model.json")
|
||||
existing := "[other]\nkey = \"val\""
|
||||
os.WriteFile(configPath, []byte(existing), 0o644)
|
||||
|
||||
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.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") {
|
||||
t.Error("unexpected triple newline in output")
|
||||
if strings.Contains(content, "old-provider") {
|
||||
t.Fatalf("profile should be replaced, got:\n%s", content)
|
||||
}
|
||||
assertBackupContains(t, filepath.Join(fileutil.BackupDir(), "ollama-launch.config.toml.*"), "original-codex-profile-backup-marker")
|
||||
})
|
||||
|
||||
t.Run("uses custom OLLAMA_HOST", func(t *testing.T) {
|
||||
t.Setenv("OLLAMA_HOST", "http://myhost:9999")
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.toml")
|
||||
catalogPath := filepath.Join(tmpDir, "model.json")
|
||||
profilePath := filepath.Join(tmpDir, "ollama-launch.config.toml")
|
||||
|
||||
if err := writeCodexConfig(configPath, "llama3.2", catalogPath); err != nil {
|
||||
if err := writeCodexProfileConfig(profilePath, "llama3.2", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(configPath)
|
||||
data, _ := os.ReadFile(profilePath)
|
||||
content := string(data)
|
||||
|
||||
if !strings.Contains(content, "myhost:9999/v1/") {
|
||||
@@ -348,13 +185,13 @@ func TestWriteCodexConfig(t *testing.T) {
|
||||
t.Run("uses connectable host for unspecified bind address", func(t *testing.T) {
|
||||
t.Setenv("OLLAMA_HOST", "http://0.0.0.0:11434")
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.toml")
|
||||
profilePath := filepath.Join(tmpDir, "ollama-launch.config.toml")
|
||||
|
||||
if err := writeCodexConfig(configPath, "", ""); err != nil {
|
||||
if err := writeCodexProfileConfig(profilePath, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(configPath)
|
||||
data, _ := os.ReadFile(profilePath)
|
||||
content := string(data)
|
||||
|
||||
if strings.Contains(content, "0.0.0.0") {
|
||||
@@ -367,7 +204,7 @@ func TestWriteCodexConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEnsureCodexConfig(t *testing.T) {
|
||||
t.Run("creates .codex dir and config.toml", func(t *testing.T) {
|
||||
t.Run("creates .codex dir, profile config, and model catalog", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
|
||||
@@ -376,26 +213,33 @@ func TestEnsureCodexConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tmpDir, ".codex", "config.toml")
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("config.toml not created: %v", err)
|
||||
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("root config.toml should not be created by CLI config refresh, err=%v", err)
|
||||
}
|
||||
|
||||
profilePath := filepath.Join(tmpDir, ".codex", "ollama-launch.config.toml")
|
||||
data, err := os.ReadFile(profilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("profile config not created: %v", err)
|
||||
}
|
||||
content := string(data)
|
||||
if strings.Contains(content, "[profiles.ollama-launch]") {
|
||||
t.Fatalf("legacy profile section should not be generated, got:\n%s", content)
|
||||
}
|
||||
if got := codexRootStringValue(content, "model"); got != "llama3.2" {
|
||||
t.Fatalf("root model = %q, want llama3.2 in:\n%s", got, content)
|
||||
t.Fatalf("profile 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)
|
||||
t.Fatalf("profile model_provider = %q, want %q in:\n%s", got, codexProfileName, content)
|
||||
}
|
||||
catalogPath := filepath.Join(tmpDir, ".codex", "model.json")
|
||||
if got := codexRootStringValue(content, "model_catalog_json"); got != catalogPath {
|
||||
t.Fatalf("profile model_catalog_json = %q, want %q in:\n%s", got, catalogPath, 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")
|
||||
data, err = os.ReadFile(catalogPath)
|
||||
if err != nil {
|
||||
t.Fatalf("model.json not created: %v", err)
|
||||
@@ -451,7 +295,14 @@ func TestEnsureCodexConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tmpDir, ".codex", "config.toml")
|
||||
data, _ := os.ReadFile(configPath)
|
||||
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("root config.toml should not be created by CLI config refresh, err=%v", err)
|
||||
}
|
||||
profilePath := filepath.Join(tmpDir, ".codex", "ollama-launch.config.toml")
|
||||
data, err := os.ReadFile(profilePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := string(data)
|
||||
|
||||
if strings.Contains(content, "[profiles.ollama-launch]") {
|
||||
@@ -463,6 +314,184 @@ func TestEnsureCodexConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCodexRestoreRemovesCLIProfileAndCatalogWithoutChangingUserRootConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
|
||||
configPath := filepath.Join(tmpDir, ".codex", "config.toml")
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
userConfig := "" +
|
||||
`model = "gpt-5.5"` + "\n" +
|
||||
`model_provider = "openai"` + "\n\n" +
|
||||
"[model_providers.openai]\n" +
|
||||
`name = "OpenAI"` + "\n"
|
||||
if err := os.WriteFile(configPath, []byte(userConfig), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ensureCodexConfig("llama3.2", launchModelsFromNames([]string{"llama3.2"})); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := (&Codex{}).Restore(); err != nil {
|
||||
t.Fatalf("Restore returned error: %v", err)
|
||||
}
|
||||
|
||||
profilePath := filepath.Join(tmpDir, ".codex", "ollama-launch.config.toml")
|
||||
if _, err := os.Stat(profilePath); !os.IsNotExist(err) {
|
||||
t.Fatalf("CLI profile should be removed, got err=%v", err)
|
||||
}
|
||||
catalogPath := filepath.Join(tmpDir, ".codex", "model.json")
|
||||
if _, err := os.Stat(catalogPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("CLI catalog should be removed, got err=%v", err)
|
||||
}
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != userConfig {
|
||||
t.Fatalf("user root config should be unchanged, got:\n%s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexRestoreDoesNotRewriteRootConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
|
||||
configPath := filepath.Join(tmpDir, ".codex", "config.toml")
|
||||
catalogPath := filepath.Join(tmpDir, ".codex", "model.json")
|
||||
profilePath := filepath.Join(tmpDir, ".codex", "ollama-launch.config.toml")
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
legacyConfig := "" +
|
||||
`profile = "ollama-launch"` + "\n" +
|
||||
`model = "llama3.2"` + "\n" +
|
||||
`model_provider = "ollama-launch"` + "\n" +
|
||||
fmt.Sprintf("model_catalog_json = %q\n\n", catalogPath) +
|
||||
"[model_providers.ollama-launch]\n" +
|
||||
`name = "Ollama"` + "\n" +
|
||||
`base_url = "http://127.0.0.1:11434/v1/"` + "\n" +
|
||||
`wire_api = "responses"` + "\n\n" +
|
||||
"[profiles.ollama-launch]\n" +
|
||||
`model = "llama3.2"` + "\n\n" +
|
||||
"[tools]\n" +
|
||||
`web_search = true` + "\n"
|
||||
if err := os.WriteFile(configPath, []byte(legacyConfig), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(catalogPath, []byte(`{"models":[]}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(profilePath, []byte(`model_provider = "ollama-launch"`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := (&Codex{}).Restore(); err != nil {
|
||||
t.Fatalf("Restore returned error: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != legacyConfig {
|
||||
t.Fatalf("root config should be left untouched, got:\n%s", data)
|
||||
}
|
||||
if _, err := os.Stat(profilePath); !os.IsNotExist(err) {
|
||||
t.Fatalf("CLI profile should be removed, got err=%v", err)
|
||||
}
|
||||
if _, err := os.Stat(catalogPath); err != nil {
|
||||
t.Fatalf("CLI catalog should be left while root config references it: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexRestoreDoesNotTouchCodexAppConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
|
||||
configPath := filepath.Join(tmpDir, ".codex", "config.toml")
|
||||
cliCatalogPath := filepath.Join(tmpDir, ".codex", "model.json")
|
||||
appCatalogPath := filepath.Join(tmpDir, ".codex", codexAppModelCatalogFilename)
|
||||
cliProfilePath := filepath.Join(tmpDir, ".codex", "ollama-launch.config.toml")
|
||||
appProfilePath := filepath.Join(tmpDir, ".codex", codexAppProfileName+".config.toml")
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
appManagedConfig := "" +
|
||||
`model = "llama3.2"` + "\n" +
|
||||
fmt.Sprintf("model_provider = %q\n", codexAppProfileName) +
|
||||
fmt.Sprintf("model_catalog_json = %q\n\n", appCatalogPath) +
|
||||
codexProviderHeaderFor(codexAppProfileName) + "\n" +
|
||||
`name = "Ollama"` + "\n" +
|
||||
`base_url = "http://127.0.0.1:11434/v1/"` + "\n" +
|
||||
`wire_api = "responses"` + "\n\n" +
|
||||
codexProviderHeader() + "\n" +
|
||||
`name = "Ollama"` + "\n" +
|
||||
`base_url = "http://127.0.0.1:11434/v1/"` + "\n" +
|
||||
`wire_api = "responses"` + "\n"
|
||||
if err := os.WriteFile(configPath, []byte(appManagedConfig), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
restoreState := fmt.Sprintf(`{"had_profile":false,"had_model":true,"model":"qwen3:8b","had_model_provider":true,"model_provider":%q,"had_model_catalog_json":true,"model_catalog_json":%q}`, codexProfileName, cliCatalogPath)
|
||||
if err := os.MkdirAll(filepath.Dir(codexAppRestoreStatePath()), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(codexAppRestoreStatePath(), []byte(restoreState), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, path := range []string{cliCatalogPath, appCatalogPath, cliProfilePath, appProfilePath} {
|
||||
if err := os.WriteFile(path, []byte(`{"models":[]}`), 0o644); err != nil {
|
||||
t.Fatalf("write %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := (&Codex{}).Restore(); err != nil {
|
||||
t.Fatalf("Restore returned error: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != appManagedConfig {
|
||||
t.Fatalf("Codex App root config should be left untouched, got:\n%s", data)
|
||||
}
|
||||
if _, err := os.Stat(cliProfilePath); !os.IsNotExist(err) {
|
||||
t.Fatalf("CLI profile should be removed, got err=%v", err)
|
||||
}
|
||||
if _, err := os.Stat(cliCatalogPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("CLI catalog should be removed when root config does not reference it, got err=%v", err)
|
||||
}
|
||||
for _, path := range []string{appCatalogPath, appProfilePath, codexAppRestoreStatePath()} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("%s should be left untouched, got err=%v", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchIntegrationCodexRestoreDoesNotRequireInstalledCLI(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
t.Setenv("PATH", tmpDir)
|
||||
|
||||
profilePath := filepath.Join(tmpDir, ".codex", "ollama-launch.config.toml")
|
||||
if err := os.MkdirAll(filepath.Dir(profilePath), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(profilePath, []byte(`model_provider = "ollama-launch"`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{Name: "codex", Restore: true}); err != nil {
|
||||
t.Fatalf("LaunchIntegration returned error: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(profilePath); !os.IsNotExist(err) {
|
||||
t.Fatalf("CLI restore should run without codex installed and remove profile, got err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertBackupContains(t *testing.T, pattern, marker string) {
|
||||
t.Helper()
|
||||
backups, err := filepath.Glob(pattern)
|
||||
|
||||
@@ -204,6 +204,12 @@ type RestoreSuccessIntegration interface {
|
||||
RestoreSuccessMessage() string
|
||||
}
|
||||
|
||||
// RestoreInstallCheckSkipper lets cleanup-only restore flows run even when the
|
||||
// external integration binary has already been removed.
|
||||
type RestoreInstallCheckSkipper interface {
|
||||
SkipRestoreInstallCheck() bool
|
||||
}
|
||||
|
||||
// ManagedRuntimeRefresher lets managed integrations refresh any long-lived
|
||||
// background runtime after launch rewrites their config.
|
||||
type ManagedRuntimeRefresher interface {
|
||||
@@ -303,7 +309,7 @@ Examples:
|
||||
ollama launch codex-app --restore
|
||||
ollama launch hermes
|
||||
ollama launch droid --config (does not auto-launch)
|
||||
ollama launch codex -- -p myprofile (pass extra args to integration)
|
||||
ollama launch codex --restore
|
||||
ollama launch codex -- --sandbox workspace-write`,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -527,8 +533,10 @@ func restoreIntegration(name string, runner Runner, req IntegrationLaunchRequest
|
||||
if !ok {
|
||||
return fmt.Errorf("%s does not support --restore", name)
|
||||
}
|
||||
if err := EnsureIntegrationInstalled(name, runner); err != nil {
|
||||
return err
|
||||
if skipper, ok := runner.(RestoreInstallCheckSkipper); !ok || !skipper.SkipRestoreInstallCheck() {
|
||||
if err := EnsureIntegrationInstalled(name, runner); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := restorable.Restore(); err != nil {
|
||||
return err
|
||||
|
||||
@@ -22,7 +22,7 @@ ollama launch codex
|
||||
```
|
||||
|
||||
When launched through `ollama launch codex`, Ollama refreshes the model catalog
|
||||
and passes it to Codex for that session.
|
||||
and uses a dedicated Codex profile for that session.
|
||||
|
||||
To configure without launching:
|
||||
|
||||
@@ -30,6 +30,12 @@ To configure without launching:
|
||||
ollama launch codex --config
|
||||
```
|
||||
|
||||
To remove the Ollama launch profile and generated model catalog:
|
||||
|
||||
```shell
|
||||
ollama launch codex --restore
|
||||
```
|
||||
|
||||
### Manual setup
|
||||
|
||||
To use `codex` with Ollama, use the `--oss` flag:
|
||||
@@ -52,25 +58,21 @@ codex --oss -m gpt-oss:120b-cloud
|
||||
|
||||
### Profile-based setup
|
||||
|
||||
For a persistent configuration, add an Ollama provider and profiles to `~/.codex/config.toml`:
|
||||
For a persistent Codex CLI configuration, create `~/.codex/ollama-launch.config.toml`:
|
||||
|
||||
```toml
|
||||
[model_providers.ollama-launch]
|
||||
name = "Ollama"
|
||||
base_url = "http://localhost:11434/v1"
|
||||
|
||||
[profiles.ollama-launch]
|
||||
model = "gpt-oss:120b"
|
||||
model_provider = "ollama-launch"
|
||||
model_catalog_json = "/Users/you/.codex/model.json"
|
||||
|
||||
[profiles.ollama-cloud]
|
||||
model = "gpt-oss:120b-cloud"
|
||||
model_provider = "ollama-launch"
|
||||
[model_providers.ollama-launch]
|
||||
name = "Ollama"
|
||||
base_url = "http://localhost:11434/v1/"
|
||||
wire_api = "responses"
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```
|
||||
codex --profile ollama-launch
|
||||
codex --profile ollama-cloud
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user