Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 72 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Portable dotfiles and Claude Code configuration. Clone to `~/.setup` on any mach
- **install.sh** — New-machine bootstrap: installs tooling and links Claude config (see below)
- **omp-setup.sh** — Installs and configures [omp](https://omp.sh) (oh-my-pi) against the Brev-hosted model endpoint (see below)
- **pi-setup.sh** — Same, for the [pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) coding agent; one-liner installable (see below)
- **codex-setup.sh** — Adds Webster models alongside OpenAI models in Codex CLI and Codex Desktop through a localhost-only proxy (see below)
- **codex-setup.sh** — Adds any Responses-compatible model API alongside OpenAI models in Codex CLI and Codex Desktop through a localhost-only proxy (see below)
- **test.sh** — Isolated negative tests for the installers, run by CI and safe to run locally
- **STYLE_GUIDE.md** — Required validation, portability, and agent-compatibility rules
- **AGENTS.md** — Codex repository instructions that reference the shared style guide
Expand Down Expand Up @@ -272,75 +272,115 @@ Nerd mode needs a [Nerd Font](https://nerdfonts.com) selected in your terminal;
without one the status line renders as tofu boxes. Fall back with
`omp config set symbolPreset unicode && omp config set statusLine.preset full`.

## Codex OpenAI + Webster proxy
## Codex OpenAI + custom model API proxy

`codex-setup.sh` installs a localhost-only Responses API router that keeps the
OpenAI/ChatGPT login already managed by Codex for OpenAI models and substitutes
the Webster API key only when a Webster model is selected. Both model families
then appear in the Codex CLI and Codex Desktop model picker. Setup queries
Webster's `/v1/models` endpoint with the supplied key, so each user sees only
the models that key can access; no Webster model IDs are hardcoded.
`codex-setup.sh` installs a localhost-only router that keeps the OpenAI/ChatGPT
login already managed by Codex for OpenAI models and substitutes a separate API
key only when one of the custom endpoint's models is selected. Both model
families then appear in the Codex CLI and Codex Desktop model picker.

The custom endpoint must use the OpenAI Responses protocol and expose an
authenticated model list. In practice, a base URL such as
`https://models.example.com/v1` must accept `GET /models` and
`POST /responses`; this proxy does not translate Chat Completions into
Responses.

First sign in once with `codex login`, then run:

```bash
curl -fsSL https://raw.githubusercontent.com/theFong/setup/main/codex-setup.sh \
| CODEX_MODEL_API_BASE_URL=https://models.example.com/v1 \
CODEX_MODEL_API_KEY=sk-... CODEX_MODEL_API_NAME='Example Cloud' bash
```

`CODEX_MODEL_API_NAME` is optional and defaults to `Custom`. It controls the
label appended to discovered models. Setup queries the endpoint with the
supplied key, so every user sees only the models their key can access; no custom
model IDs are hardcoded.

Webster remains a supported shorthand with its hosted base URL and display name
preselected:

```bash
curl -fsSL https://raw.githubusercontent.com/theFong/setup/main/codex-setup.sh \
| WEBSTER_API_KEY=sk-... bash
```

The environment prefix belongs on `bash`, to the right of the pipe. The script
can also prompt for the key through `/dev/tty`, or read it with `--key-file`:
can also prompt through `/dev/tty` or read the key with `--key-file`. On a fresh
custom install, set `CODEX_MODEL_API_BASE_URL` alongside that flag:

```bash
curl -fsSL https://raw.githubusercontent.com/theFong/setup/main/codex-setup.sh \
| bash -s -- --key-file /path/to/webster-key
| CODEX_MODEL_API_BASE_URL=https://models.example.com/v1 \
CODEX_MODEL_API_NAME='Example Cloud' bash -s -- --key-file /path/to/api-key
```

| Flag | Effect |
|---|---|
| _(none)_ | Install + configure + verify |
| `--check` | Verify source, secret permissions, endpoint, service, catalog, and Codex config without changing anything |
| `--key-file PATH` | Read the Webster key from a file's first line |
| `--restart-app-server` | Install + verify, then explicitly refresh a stale Codex app-server when the installer can do so safely |
| `--key-file PATH` | Read the custom model API key from a file's first line |

What it writes:

| Where | What |
|---|---|
| `~/.codex/model-proxy/` | Dependency-free proxy runtime and Webster key plus its discovered model list; the config is mode `0600` |
| `~/.codex/openai-webster-models.json` | Combined model catalog loaded by Codex at startup (mode `0600`) |
| `~/.codex/config.toml` | Merge-safe `openai_webster` provider and `model_catalog_json` selection |
| `~/.codex/model-proxy/` | Dependency-free proxy runtime and `upstream.json`, containing the custom key plus its discovered model list (mode `0600`) |
| `~/.codex/openai-custom-models.json` | Combined model catalog loaded by Codex at startup (mode `0600`) |
| `~/.codex/config.toml` | Merge-safe `openai_custom` provider and `model_catalog_json` selection |
| `~/Library/LaunchAgents/com.thefong.codex-model-proxy.plist` | Always-on user service on macOS |
| `~/.config/systemd/user/codex-model-proxy.service` | Always-on user service on Linux |

The proxy listens only on `127.0.0.1:4815`. It strips the incoming OpenAI
credential and ChatGPT account header before Webster requests, never logs
credential and ChatGPT account header before custom API requests, never logs
request bodies or headers, and never writes OpenAI credentials anywhere new.
The Webster key lives only in the owner-readable proxy config.
The custom key lives only in the owner-readable proxy config.

The installer preserves the current selected model and unrelated Codex config.
Re-run it to update the proxy and rediscover both the key's Webster access and
the OpenAI portion of the catalog. Setup fails instead of installing an empty
catalog when the key cannot access any models. Generated Webster entries clone
the complete stock Codex catalog shape so strict Codex Desktop versions can
deserialize every entry.
Re-run it to update the proxy and rediscover both the key's custom endpoint
access and the OpenAI portion of the catalog. Existing Webster installs and the
`WEBSTER_API_KEY` / `CODEX_WEBSTER_BASE_URL` variables are migrated
automatically. Setup fails instead of installing an empty catalog when the key
cannot access any models. Generated custom entries clone the complete stock
Codex catalog shape so strict Codex Desktop versions can deserialize every
entry.

The installer changes the catalog only when its contents actually differ. If a
running Codex app-server has therefore become stale, setup detects whether it
is daemon-managed and prints the required order: restart it with
`codex app-server daemon restart`, then disconnect and reconnect that machine
in Codex Desktop (or fully quit and reopen Desktop). For a legacy unmanaged
server, disconnect Desktop before running the restart command. No restart
notice is printed when the catalog and managed provider configuration are
unchanged or when a newer daemon has already loaded them.
running Codex app-server has therefore become stale, setup identifies its
lifecycle and prints instructions that match it:

- A standalone, daemon-managed install uses
`codex app-server daemon restart`, followed by reconnecting the machine in
Codex Desktop.
- A legacy npm/SSH app-server cannot use that daemon command. Re-run the
one-liner with `bash -s -- --restart-app-server`; the installer validates the
user-owned control-socket process before sending it `SIGTERM`, and Desktop
starts a fresh server when the machine reconnects.
- An app-owned local server is not terminated from the shell. Fully quit and
reopen Codex Desktop instead.

For example, to refresh a legacy remote session after setup:

```bash
ssh my-host 'curl -fsSL https://raw.githubusercontent.com/theFong/setup/main/codex-setup.sh | bash -s -- --restart-app-server'
```

No restart notice is printed when the catalog and managed provider
configuration are unchanged or when a newer app-server has already loaded
them. `--check` and `--restart-app-server` are intentionally mutually
exclusive.

After setup or any required reconnect, start a new Codex task before selecting
a Webster model. Codex persists the model provider with each task, so a task
a custom model. Codex persists the model provider with each task, so a task
created before setup remains on the built-in `openai` provider even if its
model is later changed in the picker. Selecting a Webster model in that older
model is later changed in the picker. Selecting a custom model in that older
task sends it to the ChatGPT backend and produces a "model is not supported
when using Codex with a ChatGPT account" error.

It is not wired into `install.sh`, because it needs both a Webster secret and an
existing Codex login. Run it separately after the bootstrap.
It is not wired into `install.sh`, because it needs both a custom API secret and
an existing Codex login. Run it separately after the bootstrap.

## Claude Code + Webster proxy

Expand Down
21 changes: 20 additions & 1 deletion claude-code-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ WEBSTER_CONFIG="$PROXY_DIR/webster.json"
CLAUDE_SETTINGS="$CLAUDE_DIR/settings.json"
GATEWAY_CACHE="$CLAUDE_DIR/cache/gateway-models.json"
CODEX_DIR="${CLAUDE_CODE_SETUP_CODEX_DIR:-${CODEX_HOME:-$HOME/.codex}}"
CODEX_MODEL_API_CONFIG="$CODEX_DIR/model-proxy/upstream.json"
CODEX_WEBSTER_CONFIG="$CODEX_DIR/model-proxy/webster.json"

SETUP_REF="${CLAUDE_CODE_SETUP_REF:-main}"
Expand Down Expand Up @@ -261,6 +262,20 @@ key_from_config() {
' "$config"
}

key_from_codex_webster_config() {
local config="$1"
[ -f "$config" ] || return 0
WEBSTER_BASE_URL="$WEBSTER_BASE_URL" node -e '
const fs = require("fs");
try {
const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
if (value.name !== "Webster") process.exit(0);
if (value.baseUrl?.replace(/\/+$/, "") !== process.env.WEBSTER_BASE_URL) process.exit(0);
if (typeof value.apiKey === "string") process.stdout.write(value.apiKey);
} catch {}
' "$config"
}

resolve_api_key() {
if [ -n "$KEY_FILE" ]; then
[ -r "$KEY_FILE" ] || { warn "cannot read key file: $KEY_FILE"; return 1; }
Expand All @@ -271,7 +286,11 @@ resolve_api_key() {
[ -n "$API_KEY" ] && ok "reusing the installed Claude Code Webster key"
fi
if [ -z "$API_KEY" ]; then
API_KEY=$(key_from_config "$CODEX_WEBSTER_CONFIG")
if [ -f "$CODEX_MODEL_API_CONFIG" ]; then
API_KEY=$(key_from_codex_webster_config "$CODEX_MODEL_API_CONFIG")
else
API_KEY=$(key_from_config "$CODEX_WEBSTER_CONFIG")
fi
[ -n "$API_KEY" ] && ok "reusing the Codex Webster key"
fi
if [ -z "$API_KEY" ]; then
Expand Down
20 changes: 11 additions & 9 deletions codex-model-proxy/README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
# Codex model proxy

This localhost-only Responses API router lets Codex CLI and Codex Desktop use
OpenAI/ChatGPT models and the Brev-hosted Webster models from one provider.
OpenAI/ChatGPT models and models from another Responses-compatible API in one
picker.

Install and configure it from the repository root with `codex-setup.sh`. The
source files in this directory are downloaded to `~/.codex/model-proxy` by the
installer; there are no npm dependencies. During each install, the setup script
queries Webster's authenticated `/v1/models` endpoint and stores the returned
model metadata. Routing and Codex catalog generation use that discovered list,
so keys with different access receive different pickers and future models are
picked up on the next re-run.
queries the custom API's authenticated `/v1/models` endpoint and stores the
returned model metadata. Routing and Codex catalog generation use that
discovered list, so keys with different access receive different pickers and
future models are picked up on the next re-run. The upstream must support the
OpenAI Responses protocol; this proxy does not translate Chat Completions.

## Security

- The server binds to `127.0.0.1` by default.
- OpenAI credentials are forwarded only to OpenAI or the ChatGPT Codex backend.
- The incoming OpenAI credential and ChatGPT account header are removed before
a Webster request, which receives only the Webster key.
a custom API request, which receives only that API's key.
- Request bodies, response bodies, credentials, and headers are not logged.
- `~/.codex/model-proxy/webster.json` is written with mode `0600`.
- `~/.codex/model-proxy/upstream.json` is written with mode `0600`.

Any process running as the same OS user can already read that user's Codex and
Webster credential stores; the loopback listener is not isolation from other
processes running as that user.
custom API credential stores; the loopback listener is not isolation from
other processes running as that user.

## Development

Expand Down
2 changes: 1 addition & 1 deletion codex-model-proxy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Local Responses API router for Codex, OpenAI, and Webster models",
"description": "Local Responses API router for Codex, OpenAI, and a custom model API",
"scripts": {
"catalog": "node write-catalog.mjs",
"start": "node proxy.mjs",
Expand Down
Loading
Loading