DEV Community

Adem İşler
Adem İşler

Posted on

How I Ran Custom Model Providers in Separate Codex Desktop Apps

After building Toolboard, I started exploring a different kind of product problem: how to use a custom model provider inside Codex Desktop without turning the main installation into an experimental environment.

The obvious approach was to edit the existing configuration and point it at another endpoint.

I did not want to do that.

My normal Codex setup already had its own sessions, configuration, automations, icon, launcher, and authentication state. A custom provider should not overwrite any of it. It should behave like a separate application that can live beside the original.

That requirement became Codex Desktop Custom Models: an open-source starter kit for creating isolated Codex Desktop apps on macOS and Linux.

The reference implementation is Codex MiniMax, a separately branded Codex Desktop app that uses MiniMax through a local compatibility bridge.

This article explains the architecture behind it, the API mismatch that required a bridge, and why isolation turned out to be the most important product decision.

The real problem was not changing the model

Changing a model name is easy.

Creating a second desktop application that does not interfere with the first one is a larger systems problem.

A custom Codex app needs its own:

  • application name and icon;
  • bundle or desktop application identity;
  • launcher and executable path;
  • webview port;
  • CODEX_HOME directory;
  • provider configuration;
  • API credentials;
  • sessions and local state;
  • bridge process;
  • automation definitions and runner state.

If even one of those boundaries is shared accidentally, the two apps can collide in subtle ways.

They may open with the wrong icon, share session history, overwrite configuration, reuse the same webview port, or make it unclear which provider is active.

So I made isolation the primary invariant:

A custom Codex app should feel familiar, but it should not share identity, state, ports, or provider configuration with the primary installation.

What the isolated setup looks like

A normal installation and a custom profile can exist side by side:

Original Codex Custom Codex app
Default application identity Unique app name, icon, and app ID
Main ~/.codex directory Separate directory such as ~/.codex-minimax
Default provider configuration Custom provider configuration
Existing sessions and state Independent sessions and state
Existing automations Separate local automation layer

For the MiniMax reference profile, the resulting flow is:

Codex MiniMax Desktop
        |
        v
custom launcher and application identity
        |
        v
CODEX_HOME=~/.codex-minimax
        |
        v
local bridge at 127.0.0.1:4007/v1/responses
        |
        v
MiniMax OpenAI-compatible /chat/completions endpoint
Enter fullscreen mode Exit fullscreen mode

The original Codex installation is left untouched.

On macOS, the installer creates a separate app bundle under ~/Applications, command wrappers under ~/.local/bin, an isolated home directory, and a LaunchAgent for the bridge process.

On Linux, it creates a side-by-side desktop shell under the user's local application directories and reuses large runtime assets where practical.

Why a local bridge was necessary

The model provider was not the only compatibility layer.

Codex Desktop communicates with a provider using the Responses API shape. Many third-party providers expose an OpenAI-compatible Chat Completions API instead.

Those APIs overlap, but they are not interchangeable.

The bridge has to translate:

  • Responses input items into chat messages;
  • developer instructions into the correct upstream role;
  • assistant messages and output text;
  • function calls and function-call outputs;
  • namespaced tools;
  • tool-choice behavior;
  • streaming and non-streaming output;
  • provider-specific model names;
  • errors and health checks.

The basic path is:

POST /v1/responses
        |
        v
normalize Codex input and tools
        |
        v
POST <provider>/chat/completions
        |
        v
convert assistant text and tool calls
        |
        v
Responses-compatible output for Codex
Enter fullscreen mode Exit fullscreen mode

Text-only requests are the easy case.

Tool calls are where the design becomes more interesting.

Translating namespaced tools

Codex can represent tools inside namespaces. A Chat Completions provider usually expects a flat function name.

For example, a conceptual Codex tool might look like:

namespace: github
function: fetch_file
Enter fullscreen mode Exit fullscreen mode

The bridge flattens that into a provider-safe function name such as:

github__fetch_file
Enter fullscreen mode Exit fullscreen mode

It keeps an internal map so that when the provider returns a tool call, the original namespace and function name can be restored before the result goes back to Codex.

This sounds like a small transformation, but it is essential. Without a reversible mapping, the model may call a function successfully while Codex cannot route the result to the correct tool.

The bridge also sanitizes names because provider function-name rules can be stricter than the identifiers used by the source tool system.

Why I used a local HTTP service

The bridge runs only on 127.0.0.1.

That decision provided a few useful properties:

  • Codex can treat it like a normal provider endpoint;
  • the upstream API key stays in the custom profile environment;
  • provider-specific translation logic remains outside the desktop application bundle;
  • health checks and test requests are easy to run;
  • the bridge can be restarted independently;
  • another OpenAI-compatible provider can reuse the same architecture.

The custom configuration points Codex to the local Responses endpoint, while the bridge points to the actual provider's Chat Completions endpoint.

The reference profile uses MiniMax, but the architecture is deliberately generic. A different provider mostly requires changing:

  • the upstream base URL;
  • the default model name;
  • the supported model list;
  • the API-key environment variable;
  • any provider-specific response normalization.

Application isolation on macOS

macOS application identity is more than a folder name.

A side-by-side application needs a unique bundle identifier, display name, icon, helper identity, launcher behavior, and webview port. Copying the base .app bundle gives the custom profile a controlled place to patch those values.

The installer then creates wrappers such as:

codex-minimax
codex-minimax-desktop
codex-minimax-proxy
Enter fullscreen mode Exit fullscreen mode

Each wrapper has one narrow responsibility:

  • invoke the Codex CLI with the isolated home;
  • launch the custom desktop bundle;
  • manage, test, and inspect the bridge process.

The bridge is registered through a LaunchAgent so it can survive beyond the shell session that created it.

This matters because a desktop app should not depend on the user remembering to keep a terminal command running.

Application isolation on Linux

The Linux path has a different set of constraints.

Many large runtime files can be reused, but identity-sensitive files should be copied rather than linked.

The custom app keeps independent versions of things such as:

  • the launcher script;
  • executable identity;
  • branding assets;
  • desktop entry metadata;
  • the application directory structure.

Large internal runtime assets can be linked where doing so does not create shared writable state.

The rule is not “copy everything” or “symlink everything.” The rule is:

Copy the files that define application identity, and reuse only the files that are effectively immutable runtime dependencies.

That avoids unnecessary duplication without creating Dock, taskbar, icon, or profile collisions.

Separate CODEX_HOME is the real boundary

The most important directory is not the app bundle. It is the custom CODEX_HOME.

A profile such as ~/.codex-minimax contains the custom app's:

  • config.toml;
  • provider environment files;
  • bridge scripts;
  • automation definitions;
  • session and application state;
  • local logs and SQLite files.

The launcher exports this directory before invoking Codex.

That means the custom app does not need to mutate ~/.codex, and removing the custom application does not require deleting the user's main Codex state.

The uninstall process intentionally keeps the custom home by default. Removing the app and deleting its history are separate decisions.

That distinction is valuable in any desktop tool: uninstalling executable files should not silently destroy user data.

Automations required a second compatibility layer

A custom Codex profile does not automatically inherit automation tools supplied by the host environment.

So the project includes a small local MCP server and runner.

The MCP server exposes an automation update tool and stores definitions under the isolated custom home. It also mirrors enough metadata into local SQLite state for the app to display those automations.

The runner polls for due jobs and supports two patterns:

  1. background or inbox-style tasks;
  2. same-thread follow-ups.

Same-thread delivery was the important detail.

Creating a scheduled record is not the same as making the result appear in the conversation where the user requested it.

For same-thread reminders, the automation stores the target thread ID and later runs a command conceptually equivalent to:

codex exec resume <thread-id> ...
Enter fullscreen mode Exit fullscreen mode

That allows the follow-up to return to the original thread instead of creating disconnected output elsewhere.

The scheduler is intentionally limited to common minutely, hourly, and daily patterns. It is not trying to replace a full calendar system.

Safety and repository hygiene

A project that works with custom model providers and local Codex state has an obvious publishing risk: accidentally committing credentials or private session data.

The repository includes a verification command that checks syntax and scans for patterns such as:

  • API keys;
  • personal home-directory paths;
  • local project names;
  • private state files;
  • provider environment files.

The public starter kit contains templates and examples, not a copy of a real Codex profile.

The files that must remain private include provider keys, session indexes, shell snapshots, logs, and application state databases.

This is also why I prefer isolated profiles to modifying the primary installation. Isolation makes the security boundary easier to understand and easier to audit.

What I learned

1. Compatibility is broader than API syntax

The request and response formats were only one layer. A real desktop integration also needed process management, state isolation, app identity, ports, icons, launchers, and automation behavior.

2. Side-by-side products need explicit identity

A renamed folder is not a separate application. Operating systems use bundle IDs, executable paths, helper identities, and desktop metadata to decide how apps are grouped and launched.

3. Tool calling makes provider bridges significantly harder

Text can often be mapped directly. Namespaces, function-call IDs, tool outputs, and provider naming restrictions require reversible and carefully validated transformations.

4. Local bridges are useful architectural seams

Keeping provider adaptation in a small localhost service allowed the desktop shell, Codex configuration, and upstream provider logic to remain independently understandable.

5. Isolation is a product feature

The strongest benefit is not that the project can connect to MiniMax. It is that a user can experiment without destabilizing the setup they already rely on.

Who this project is for

Codex Desktop Custom Models is not a one-click consumer application.

It is an operator-friendly starter kit for developers who:

  • already have Codex Desktop working;
  • understand that upstream internals may change;
  • want a separate app for an OpenAI-compatible provider;
  • prefer inspectable scripts over modifying their primary environment;
  • are comfortable testing integrations after Codex updates.

It is unofficial and is not an OpenAI, MiniMax, or Ollama project.

The repository

The repository includes the provider bridge, macOS and Linux installers, configuration templates, automation MCP and runner, diagrams, screenshots, troubleshooting notes, and a publishing checklist.

GitHub logo ademisler / codex-desktop-custom-models

Open-source starter kit for isolated Codex Desktop apps for custom model providers on macOS and Linux, with provider bridges, custom icons, and local automations.

Codex Desktop Custom Models

Codex Desktop Custom Models

Create isolated Codex Desktop apps for custom model providers on macOS and Linux, with separate icons, state, provider bridges, and automations

An open-source project by Adem İşler Released under the MIT License.


What This Is

Codex Desktop Custom Models is a starter kit for people who already have Codex Desktop installed and want a second, independent Codex app for a custom model provider.

The reference app is Codex MiniMax: a red-icon Codex Desktop profile that uses MiniMax M2.7 through a local OpenAI-compatible bridge.

It does not modify your original Codex app. Your normal Codex install keeps its own config, data, sessions, provider, icon, and launcher.

Codex MiniMax running as a separate Codex Desktop app

Open Source

This project is released under the MIT License. You can use it, fork it, adapt it for other providers, and contribute fixes through pull requests.

What You Get

Part Purpose
Isolated app clone Creates a side-by-side Codex

I am interested in feedback on a few areas:

  • Which other OpenAI-compatible providers would be useful as documented examples?
  • Should provider adapters remain configuration-driven, or should complex providers get separate bridge modules?
  • Which parts of desktop-app isolation have caused the most trouble in your own tooling?

This is the second article in my series documenting the products I have built, moving from browser utilities toward AI-native developer tools and desktop systems.

Top comments (1)

Collapse
 
valentin_monteiro profile image
Valentin Monteiro

On the config-vs-module question: the line I'd draw is where the difference stops being a value and starts being behaviour. Base URL, model list, key env var, name sanitising rules, all config. But tool_choice semantics and how a provider chunks tool_calls across streaming deltas aren't expressible as fields, and forcing them into config gives you a config format that slowly turns into a programming language. Separate question on the namespace map: is it rebuilt per request from the tools array, or held in bridge memory? If it's in memory, a LaunchAgent restart mid-conversation would lose the ability to route a returning tool call back to the right namespace.