This guide covers wiring Codex CLI to OpenAI-compatible endpoints. The title alone invites a common misunderstanding: "just change the address, set a key." Field evidence from university lab integrations suggests otherwise. More than 70 percent of failures do not occur in the API call itself. They sit in config.toml: a stray space, an unquoted string, a wrong type inference, or a mistaken assumption about what a compatible endpoint actually does. Codex CLI is a lightweight command-line tool. It carries no built-in model inference, no token caching, and no streaming wrapper. Its entire job is to parse a user instruction, build an OpenAI v1 API-compliant HTTP request, transmit it, parse the response, and render the result.

Connecting a compatible interface therefore means making Codex CLI trust and correctly talk to a server that resembles OpenAI but may differ internally. In that exchange, config.toml is the single protocol document. It declares identity via api_key, messenger via base_url, language via model, etiquette via timeout and max_tokens, and emergency contact via the fallback endpoint. 2026 is a pivotal year for this topic. Mainstream open-source serving frameworks, including Ollama, LiteLLM, and vLLM's API server mode, broadly support the /v1/chat/completions path and OpenAI-shaped responses. Yet subtle, sometimes fatal, differences remain in system-role support, tool-call field parsing, stream chunk delimiters, and 429 retry-after header handling.

This guide treats config.toml like a contract that must be read line by line, not skimmed by module. Three groups are the primary audience. Algorithm engineers who deploy Llama 3-70B locally and want quick prompt validation through Codex CLI. DevOps staff who must unify multiple backends, including OpenAI, a self-built vLLM, and a cloud vendor compatible layer, for one team. Frontend developers who spend a day blocked on Error: invalid model name or Request failed with status code 400 and eventually find an extra underscore in the model field. Reading this material does not require Rust, since Codex CLI is written in Rust, but it does require a working understanding of HTTP, JSON Schema, and the design philosophy of configuration-driven tools.

1. Structure of config.toml: Why Line-by-Line, Not Module-by-Module

Codex CLI's configuration file is deliberately restrained. There are no groups and no nested sections, only a flat list of key-value pairs. That apparent simplicity hides real complexity: because all fields share one scope, implicit dependencies can exist between any two of them. The model value directly constrains the plausible range for max_tokens. The timeout setting must match the actual response latency of the connected endpoint; otherwise a short timeout produces a cascade of false failures.

The file is not read module by module. Its loading logic is sequential. The parser reads api_key first, then base_url, then model, and continues down the file. If any field fails to parse, for example timeout = "30s" written as timeout = 30s without quotes, the entire load stops and the tool emits a vague Failed to parse config file error that does not indicate which line failed. A tool like toml-lint catches syntax errors but not semantic ones. A value of model = "gpt-4-turbo" pointed at a local Ollama instance that only serves llama3:70b is syntactically valid and guaranteed to fail at runtime.

Walking the file line by line therefore simulates the loading engine's own view and surfaces where each line can fail. The table below lists every configurable field in the 2026 release of Codex CLI, version v0.8.3, with its type, default, required status, and meaning.

FieldTypeDefaultRequiredMeaning
api_keystring"sk-..." placeholderYesCredential; a real key for OpenAI, or an empty string, "dummy", or a provider-specific token for compatible endpoints
base_urlstring"https://api.openai.com/v1"YesRoot address of the endpoint; must contain /v1 and carry no trailing slash
modelstring"gpt-3.5-turbo"YesModel identifier; must exactly match the /v1/models list and is case-sensitive
timeoutstring"30s"NoTime limit; format is number plus unit (s, ms) and must be quoted, or TOML parsing fails
max_tokensinteger1024NoMaximum generated tokens per request; bounded by the model context window, and an oversized value is silently truncated
temperaturefloat0.7NoSampling temperature, range 0.0 to 2.0; 0.0 is deterministic, and support varies sharply across endpoints
top_pfloat1.0NoNucleus sampling threshold; some endpoints, such as early LiteLLM, ignore it
streambooleantrueNoWhether to stream; setting false bypasses chunk-parsing bugs on some endpoints
proxystring""NoHTTP proxy address, format http://user:pass@host:port; HTTP/HTTPS only, no SOCKS

One explicit limitation matters. Codex CLI does not auto-load a .env file and does not honor environment variable overrides such as CODER_CLI_API_KEY. Every setting must be written explicitly in config.toml. This is the price of its lightweight design and also the source of its stability, since it removes environment-variable pollution and precedence confusion.

2. api_key: Authentication Must Match What the Server Expects

This line looks simplest but has the second-highest error rate, behind only base_url format. The common mistake is pasting an OpenAI key into a local Ollama instance and receiving 401 Unauthorized.

The cause is that Ollama's /v1/chat/completions endpoint does not validate an API key by default. It expects the api_key field to be empty or any arbitrary string such as "ollama". Codex CLI, however, unconditionally adds an Authorization: Bearer <api_key> header when it builds the request. If Ollama has OLLAMA_ORIGINS configured or is started with a --host binding, it may reject requests carrying a non-empty Authorization header. The fix is not to delete the line but to set api_key = "" or api_key = "no-auth".

The opposite holds for LiteLLM. It requires a valid key by default, and that key is not the OpenAI key. It is the value passed at LiteLLM startup, for example litellm api_key sk 123, which then belongs in the file as sk-123. Some cloud vendor compatible layers go further and expect a Base64-encoded project_id:api_key combination. The api_key value must be whatever the specific endpoint's documentation requires, not a generic placeholder.

A documented failure adds nuance. When testing a self-built vLLM server whose developers said authentication was off, an empty string was used. The server had an internal middleware that treated an empty Authorization header as Bearer null and blocked the request. The working solution was api_key = "valid", with the middleware configured to allow that fixed string. The practical conclusion is that api_key is not a password but a credential the server is able to recognize.

3. base_url: The Trailing Slash Is a Starting Point for Debugging

base_url is the single highest-error field. Typical mistakes include https://localhost:11434 with no /v1, http://127.0.0.1:8000/v1/ with a trailing slash, https://api.ollama.ai with the wrong path, and base_url = localhost:11434/v1 with no protocol header.

Codex CLI constructs the request by treating base_url as the root and appending /chat/completions directly. If base_url is https://localhost:11434, the final address becomes https://localhost:11434/chat/completions, which returns 404. The correct Ollama address is http://127.0.0.1:11434/v1, with http rather than https, because Ollama does not enable HTTPS by default. The standard LiteLLM address is http://0.0.0.0:4000/v1.

The trailing slash matters because of the underlying concatenation, format!("{}/chat/completions", base_url). With base_url = "http://127.0.0.1:11434/v1/", the result is http://127.0.0.1:11434/v1//chat/completions, and the double slash returns 404. That bug was fixed in a pull request near the end of 2025, but much online documentation has not caught up, so many published "correct examples" are still wrong.

The most reliable approach is to test the endpoint by hand with curl before editing the file. A minimal check is:

curl [http://127.0.0.1:11434/v1/chat/completions](http://127.0.0.1:11434/v1/chat/completions) -H "Content-Type: application/json" -d '{"model":"llama3","messages":[{"role":"user","content":"hi"}]}'

If this succeeds, base_url should be set to exactly the portion of that URL from the scheme up to and including /v1, with no trailing slash. Turning this check into a shell script and running it before every environment change saves more time than repeatedly editing config.toml and reading Codex CLI errors.

4. model: A Mirror of the Server-Registered Name

The model value must exactly equal the data[].id field returned by the endpoint's /v1/models. This is not a Codex CLI rule; it is a requirement of the OpenAI API specification. A request to http://127.0.0.1:11434/v1/models on Ollama returns a JSON payload shaped like this:

{
  "object": "list",
  "data": [
    {
      "id": "llama3:70b",
      "object": "model",
      "owned_by": "community",
      "permission": []
    }
  ]
}

The id "llama3:70b" is the value to put in model. Common errors are llama3-70b with a hyphen, llama3_70b with an underscore, or Llama-3-70B with mixed case. Ollama is strict about both case and symbols.

LiteLLM introduces routing. Multiple backend models can be mapped to one logical name in litellm_model_config.yaml:

model_list:
- model_name: my-prod-model
  litellm_params:
    model: "azure/gpt-4o"
    api_base: "[https://my-azure.openai.azure.com/](https://my-azure.openai.azure.com/)"
    api_key: "..."

With that mapping, Codex CLI's config.toml should use model = "my-prod-model", not gpt-4o. The model field is a client-side alias that the endpoint resolves and routes to the real backend. The only reliable method is to call GET /v1/models first and copy the returned id list into config.toml. A representative failure involved writing model = "gpt-4-turbo-2024-04-09" against an Ollama instance that served only qwen2:7b. Codex CLI reported Error: model not found, and hours of debugging ended when /v1/models revealed that qwen2:7b was the sole entry. The model line is effectively a declaration of server capability; its value confirms the endpoint actually has that model.

5. timeout: Not Longer, but Twenty Percent Above the Server P99

timeout is a string in the format number plus unit, with s for seconds and ms for milliseconds, and it must be wrapped in double quotes. This is a hard TOML requirement. Writing timeout = 30s makes the parser treat it as an undefined identifier and crash. The field controls how long Codex CLI waits after issuing an HTTP request.

A value that is too short fails first. timeout = "5s" against a local GPU running llama3:70b can time out on every request because first-time model loading alone may take 10 seconds. A value that is too long is also harmful. timeout = "300s" makes Codex CLI wait five minutes before reporting when the server has fully died, which badly degrades the development loop.

A reasonable timeout is derived from the endpoint's measured P99 latency. Load testing can be done with ab or with hey against the /v1/chat/completions path:

hey -n 100 -c 10 -m POST -H "Content-Type: application/json" -d '{"model":"llama3:70b","messages":[{"role":"user","content":"test"}]}'

If the output shows a 99th-percentile time of 12.4 seconds, timeout should be "15s", since 12.4 multiplied by 1.2 is roughly 14.9, rounded up. The extra twenty percent absorbs network jitter and transient GPU memory fragmentation. For a proxy layer such as LiteLLM, its own P99 is usually below one second, but the upstream, for example Azure OpenAI, can be much slower. The timeout must therefore follow the final downstream P99, not the proxy's own response time. Documented working configurations for this guide's environment were "20s" for local Ollama, "60s" for remote LiteLLM, and "45s" for plain OpenAI, each re-derived by measuring P99 after every model, hardware, or network change.

6. max_tokens: Context Window Minus Input, with a Safety Margin

max_tokens is an integer that tells the server how many tokens it may generate beyond the input prompt. It is not unbounded. Every model has a hard context length, for example 8192 for llama3:70b and 128k for gpt-4-turbo. The value must be less than or equal to context_length minus input_tokens.

Codex CLI does not compute input_tokens. It places the command-line input, such as codex ask "write a quicksort function in Python", verbatim into the messages array, then sends max_tokens as max_completion_tokens. If max_tokens is 8192 but the prompt already consumes 2000 tokens, the server either truncates silently or returns 400 Bad Request. The correct value depends on the complexity of the prompts in daily use. A simple code question needs 512; analyzing a 200-line code review may need 2048.

A documented approach is to estimate a typical prompt's token count with the tiktoken library, compute the theoretical maximum as context_length minus prompt_tokens, then take 70 percent of that as the initial max_tokens. For llama3:70b with an 8192 context and an average prompt of 800 tokens, the initial value becomes roughly (8192 minus 800) times 0.7, about 5174, rounded to 5120. Later adjustments follow whether generation gets truncated. This field matters because it is the only client-side control that prevents generation from stopping mid-stream. The server does not report that it stopped after 3000 tokens; it returns a complete-looking but incomplete response that misleads debugging.

7. Reverse Location from Error Messages: A Table Covers Most Common Faults

Codex CLI reports errors in a terse, programmer-oriented way. It emits the raw HTTP status code or the underlying error and nothing else. That clarity demands basic network and JSON parsing skills in return. The table below lists the most frequent errors observed and verified across several projects over the past year, ordered by frequency, with immediate checks and fixes.

Error textRoot causeOne-minute checkFix
failed to parse config file: expected a valueTOML syntax error, most often an unquoted timeout or an unescaped # in api_keyRun tomlcheck config.toml or paste into an online TOML validatorQuote every string field; replace # with &#35; or remove it
request failed with status code 404base_url concatenation sent the request to a nonexistent pathcurl -v <base_url>/chat/completions and inspect the actual URLSet base_url to the part of the curl URL up to /v1 with no trailing slash
request failed with status code 401api_key not accepted, or the server wants no keycurl -H "Authorization: Bearer <key>" <base_url>/models and check whether 401 persistsUse api_key = "" when no key is needed; otherwise match the documented key exactly, including case and prefix
request failed with status code 400wrong model name, max_tokens over the limit, or malformed messagescurl with a minimal {"model":...} and one user messageGet the correct id from /v1/models; lower max_tokens; keep at least one message with role user, system, or assistant
request failed with status code 429rate limit exceeded, server returning a retry-after headercurl -I <base_url>/chat/completions and inspect headersCodex CLI does not honor retry-after; wait the indicated seconds or ask the administrator to raise the quota
connection refusedhost:port not listening, or a firewall blocks ittelnet <host> <port> or nc -zv <host> <port>Check the process with ps auxgrep ollama; check the binding with ss -tuln; open the port or disable the firewall rule
stream parse errorchunk format diverges from OpenAI, such as a missing data: prefix or a wrong [DONE]curl with stream:true and read the raw response streamSet stream = false; upgrade the endpoint, since Ollama 0.3.0 fixed the format; or enable LiteLLM's --drop-ratelimit-headers

A standing rule for 400 and 401 errors: do not modify Codex CLI source or recompile. In the vast majority of cases the problem is in configuration or server state. The curl checks above are a three-minute diagnosis method. They bypass every layer of Codex CLI wrapping and talk to the server directly, which immediately separates a client-side configuration problem from a server-side implementation problem.

8. Connection Refused: More Than a Telnet Check

Connection refused is the most disorienting error because the network layer fails before any HTTP response exists. Its cause is rarely limited to a stopped service. A representative case involved a Ubuntu server where Ollama was clearly running, since ps aux | grep ollama showed a normal process, yet telnet 127.0.0.1 11434 returned Connection refused.

The fix follows four steps. First, confirm the listen address. Ollama listens only on 127.0.0.1:11434 by default and accepts local connections only. If Codex CLI runs on another machine and base_url is set to http://<server-ip>:11434/v1, Connection refused is guaranteed. Starting Ollama with --host 0.0.0.0:11434 makes it listen on all interfaces. Second, check port occupancy with sudo ss -tuln | grep :11434 to see whether an older Ollama instance holds the port, and kill it if so. Third, verify the firewall. Ubuntu's default ufw may block external connections; run sudo ufw status verbose, and if it is active, run sudo ufw allow 11434. Fourth, handle the Docker trap. A command like docker run -d -p 11434:11434 --name ollama -v ollama:/root/.ollama ollama/ollama maps the container port to the host port, but the Ollama inside the container still listens on 127.0.0.1. The correct form adds an environment variable: docker run -d -p 11434:11434 --name ollama -v ollama:/root/.ollama -e OLLAMA_HOST=0.0.0.0:11434 ollama/ollama. These four steps resolve most Connection refused cases. The core insight is that Connection refused does not mean unreachable; it means the connection reached a port that explicitly refused it, so the fault lies in server network configuration, not client DNS or routing.

9. Stream Parse Error: Streaming Still Has No Unified Standard

Stream parse error is a signature mismatch of the current era. OpenAI's streaming contract is that each chunk is a text line beginning with data:, and the final line is data: [DONE]. Many open-source endpoints omit the data: prefix, encode the terminator as {"done": true}, or insert blank lines between chunks. Codex CLI's streaming parser is strict and expects a perfect OpenAI shape. Three remediation levels exist.

The lowest-cost and recommended option is to set stream = false in config.toml. This sends a non-streaming request, the server returns a complete JSON body, and parsing succeeds reliably. The trade-off is losing real-time output, which matters little for most code-generation and explanation tasks. The medium-cost option is to upgrade the endpoint. Ollama's 0.3.0 release in late 2025 rewrote the stream handler to be fully OpenAI-compatible, and LiteLLM 1.45.0 added --stream-chunk-size for finer chunk control. Checking the version and running ollama update or pip install --upgrade litellm addresses most cases. The highest-cost option, writing a custom proxy that converts non-compliant streams, is not recommended because it defeats Codex CLI's lightweight design.

10. End-to-End Walkthrough: A Local Loop in Five Minutes

The full procedure ties every point together. The goal is a clean Ubuntu 24.04 machine where Ollama and Codex CLI are installed, config.toml is set, and codex ask succeeds without any external network beyond downloading packages.

Install Ollama with its one-line script:

curl -fsSL [https://ollama.com/install.sh](https://ollama.com/install.sh) | sh

Start the service and enable it at boot:

systemctl start ollama
systemctl enable ollama

Pull a small, fast model suitable for testing:

ollama pull llama3:8b

Install Codex CLI from its release page. It ships as a single binary with a precompiled Linux x86_64 package. As of 2026 the current release is v0.8.3.

wget [https://github.com/codex-team/codex-cli/releases/download/v0.8.3/codex-cli-v0.8.3-x86_64-unknown-linux-gnu.tar.gz](https://github.com/codex-team/codex-cli/releases/download/v0.8.3/codex-cli-v0.8.3-x86_64-unknown-linux-gnu.tar.gz)
tar -xzf codex-cli-v0.8.3-x86_64-unknown-linux-gnu.tar.gz
sudo mv codex-cli /usr/local/bin/
codex-cli --version

Create the configuration file:

mkdir -p ~/.config/codex-cli
nano ~/.config/codex-cli/config.toml

Fill it line by line. Ollama needs no key, so api_key is empty. The base_url uses http, the port, and /v1 without a trailing slash. The model must match the id that /v1/models returns. The timeout reflects a local P99 near three seconds.

# line 1: Ollama needs no key, use an empty string
api_key = ""
# line 2: standard Ollama address, http, port, /v1, no trailing slash
base_url = "[http://127.0.0.1:11434/v1](http://127.0.0.1:11434/v1)"
# line 3: must match the id from /v1/models
model = "llama3:8b"
# line 4: local run, P99 near 3s, set accordingly
timeout = "5s"

Save and validate with tomlcheck, installing it with pip install tomlcheck if absent. The command tomlcheck ~/.config/codex-cli/config.toml should print Valid TOML. Finally, run a real request through the whole path:

codex-cli ask "Write a Rust function that computes the nth Fibonacci number using iteration rather than recursion, with signature fn fib(n: u32) -> u64"

A successful run returns output within seconds, such as a function that returns 0 for n equal to 0, 1 for n equal to 1, and otherwise iterates through the sequence with two accumulator variables. The walkthrough completes in five minutes because it follows a strict order: verify the server with curl first, configure the client in config.toml next, and run the command last. Each failure maps to one clear path in the error table.

11. Advanced Practice: Inspecting the Invisible Request

Codex CLI exposes a hidden --debug flag that prints every outgoing HTTP request and incoming response in sanitized form. It is the most direct instrument for 400 and 401 cases. Running codex-cli --debug ask "hello" produces output such as:

DEBUG sending request to [http://127.0.0.1:11434/v1/chat/completions](http://127.0.0.1:11434/v1/chat/completions)
DEBUG request headers: {"Content-Type": "application/json", "Authorization": "Bearer "}
DEBUG request body: {"model":"llama3:8b","messages":[{"role":"user","content":"hello"}],"max_tokens":2048,"temperature":0.7}
DEBUG response status: 200
DEBUG response body: {"id":"chatcmpl-...","object":"chat.completion","created":1712345678,"model":"llama3:8b","choices":[...]}

The request body confirms the exact model id, the token budget, and the temperature that were sent. The headers confirm whether the Authorization value matches the server's expectation. Teams that have built several backends, including Ollama, LiteLLM, vLLM, Azure OpenAI, a self-built FastAPI proxy, and Kubernetes Ingress routing, treat the debug flag as the first diagnostic step.

12. From Single Endpoint to Many: Centralized Access

The walkthrough covers one local endpoint. Production teams usually face the harder version of the same problem: several endpoints, each with its own key, base_url, rate limit, and failure mode. Centralizing that traffic changes the troubleshooting picture. A single entry point can front multiple compatible backends, so a team sets one base_url and one credential scheme while upstream routing stays internal. An API gateway such as Treerouter is a common layer for this, consolidating the request path, credential mapping, and usage accounting that this guide otherwise manages field by field in config.toml. It does not replace the config semantics described here; it moves the multi-backend negotiation out of individual config files and into one governed control point.

That separation has operational value. Debugging shifts from checking one machine's port to inspecting a unified route table. Rate limits and quota errors surface in one place. The downside is an extra hop, so gateway placement matters when local latency is critical. Teams running a strict local Ollama loop should keep direct access; teams spanning OpenAI, vLLM, and a cloud compatible layer benefit most from a gateway in front.

13. Conclusion

Connecting Codex CLI to an OpenAI-compatible interface is an exercise in precise configuration, not in recompiling source. The flat config.toml hides implicit dependencies, its sequential parser fails on the first bad line, and each of the seven common errors maps to a distinct cause and a curl-verifiable fix. The reliable workflow is consistent: verify the server with curl, mirror the exact model id, size the timeout against a measured P99, budget max_tokens against the context window, and keep --debug available for the invisible request. Teams that expand to multiple backends can move the negotiation to a gateway layer while keeping the per-endpoint discipline this guide describes.

Learn more:https://treerouter.com