Repository navigation
Serve the generated Codex model catalog over the management API for remote clients #709
Description
Area
Proxy and routing
What are you trying to accomplish?
Run one opencodex instance centrally and let Codex clients on other machines see the routed model picker, without any client holding provider credentials. The server owns the providers and OAuth accounts; each client should only need base_url, the admission token, and the model catalog.
Everything except the catalog already works with the setup recommended in #95: a reverse proxy terminates TLS, injects x-opencodex-api-key, and forwards Authorization untouched, so ChatGPT passthrough keeps working per machine. Routed models are reachable by exact id (codex -m anthropic/claude-opus-5). What is missing is a supported way for a client to obtain the catalog that makes those models appear in the picker.
What prevents this today?
Codex builds its picker from files in the client's CODEX_HOME, not from a call to the proxy. The proxy already generates exactly those files on the server:
[opencodex] Codex runtime: /usr/local/bin/codex (version=0.146.0, source=configured)
+ 2 models appended to Codex catalog (/home/node/.opencodex/codex-home/opencodex-catalog.json)
There is no HTTP route that returns them. I went through the /api/* surface in 2.7.42 and there is no catalog route.
GET /v1/models is not a substitute. It returns identifiers only:
{ "id": "fakeprov/cool-model", "object": "model", "created": 0, "owned_by": "openai" }A generated catalog entry carries roughly 29 fields, including base_instructions, comp_hash, context_window, max_context_window, supported_reasoning_levels, truncation_policy, and input_modalities. Reconstructing that client-side would mean reimplementing the catalog builder, and it still could not be done without provider credentials.
Running ocx on the client does not help either, and quietly makes things worse. On a credential-less client, ocx sync rebuilds routed entries from the local provider list and therefore deletes them. Measured in an isolated CODEX_HOME/OPENCODEX_HOME:
routed entries in catalog → ["fakeprov/cool-model"]
routed entries after a provider-less ocx sync → []
The injector itself works well remotely — with hostname pointing at the server, ocx sync writes a correct provider table including env_http_headers — but injection and catalog rebuild are bundled in every command that exposes them (sync, init, ensure, restore back), so the injector cannot be used on its own. ocx ensure additionally tries to start a local proxy, which is wrong on a client.
The net effect is that every operator who follows the #95 recommendation has to build their own file distribution mechanism, which is also what the caveat in that thread currently advises.
What should OpenCodex do?
Expose the catalog the proxy already generates through the existing token-authenticated management API:
GET /api/catalog → the current opencodex-catalog.json content
Subject to the same admission rules as the rest of /api/*, so on a non-loopback bind it requires x-opencodex-api-key or Authorization: Bearer and nothing new is opened up.
It would help if the response let a client detect Codex version skew, since the catalog embeds base_instructions and comp_hash from the Codex build on the server, and a client running a different Codex version can end up with subtly wrong entries. A response header or a small JSON envelope would both work; I have no preference on the shape.
A second route for the stale-stamped models_cache.json variant would be convenient but is not strictly required, because ocx sync-cache already rebuilds it locally from the on-disk catalog without touching routed entries. Measured on the same isolated setup:
after sync-cache (no providers): ["fakeprov/cool-model"]
models_cache stamp: 2000-01-01T00:00:00Z 0.0.0 1 routed
So clients that have ocx installed need only the single catalog route. Clients that only have Codex would benefit from the second one.
This is deliberately read-only exposure of a file the proxy already writes. It does not touch tenant identity, per-user authorization, or attribution — the parts described as project-scale work in #95.
Example usage or interface
On a client machine, after the one-time config injection:
curl -fsS -H "x-opencodex-api-key: $OPENCODEX_API_AUTH_TOKEN" \
https://proxy.example.com/api/catalog > "${CODEX_HOME:-$HOME/.codex}/opencodex-catalog.json"
ocx sync-cacheThe client config.toml is unchanged from what non-loopback injection already produces, except that a reverse proxy allows an https base URL:
model_provider = "opencodex"
model_catalog_json = "/home/me/.codex/opencodex-catalog.json"
[model_providers.opencodex]
name = "OpenCodex Proxy"
base_url = "https://proxy.example.com/v1"
wire_api = "responses"
requires_openai_auth = true
env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" }Before and after, for an operator adding a provider on the server:
today add provider in the dashboard → resync → copy opencodex-catalog.json out of the
server by whatever mechanism the operator built → distribute to every client
after add provider in the dashboard → resync → each client runs one authenticated GET
Alternatives or workarounds
What I run today: a static file server sidecar that mounts the catalog directory read-only and is exposed under a path prefix on the same reverse-proxy host, plus a small client script that fetches opencodex-catalog.json and models_cache.json. It works, and it is a reasonable thing for an operator to build, but it is a second service to run and it is re-invented independently by everyone who hits this.
scp/rsync from the proxy host also works, but it requires SSH access to the server from every client machine, which is a much larger grant than an already-issued admission token.
Client-side ocx does not work, for the reasons measured above.
One small note in case it is useful: the guidance in #95 mentions that clients "can fetch it from the proxy's /v1/models". In practice that response carries identifiers only, so a catalog file is still required. Mentioning it only because that sentence is the natural first thing an operator tries.
Additional context
- [Roadmap] Centrally hosted multi-user OpenCodex with tenant isolation #95 tracks centralized multi-user hosting and lists catalog distribution as a caveat. This request is scoped to that caveat alone.
- [Feature Request] Complete the external aggregated model API for third-party agents #357 completed the external aggregated model API for third-party agents; this is a similar shape of change, exposing something the proxy already computes.
- Model catalog changes are not reflected in running Codex app-server until it is restarted #476 still applies afterwards: a running Codex
app-serverkeeps its previous model list until restarted. This request does not change that.
Measured on 2.7.42 running in Docker, with the Codex CLI installed in the image as the catalog source. Happy to open a PR against dev if the endpoint shape is agreed.
Checks
- I searched existing issues and documentation.
- This request describes a concrete OpenCodex workflow rather than merely naming a desired technology.
- I removed secrets and personal data.
Activity
github-actions commentedon Jul 29, 2026
Automated translation bookkeeping — detected language: unknown.
Wibias commentedon Jul 30, 2026
[shipping-github] Research review
Claim: Expose the generated
opencodex-catalog.jsonvia authenticatedGET /api/catalogfor remote Codex clients.Checked against: dev@be177ea5
GET /v1/models/GET /api/modelsare not substitutesbase_instructions/ model metadata); must stay behind existing/api/*admissionEvidence: Management route inventory under
src/server/management*.tshas no/api/catalog. Catalog still written only to CODEX_HOME paths.Gaps: Shape of envelope/version-skew header not decided (reporter offered either).
Wibias commentedon Jul 30, 2026
[shipping-github] Security review
Checked against: dev@be177ea5
Scope: Proposed
GET /api/catalog(and credential class for remote catalog distribution)/api/*must not silently widen what a data-plane admission credential can read. Today/api/*is management-plane only; the issue text describes clients using the same header pattern as data-plane admission. Collapsing those classes would expand management-adjacent disclosure (model metadata /base_instructions) to every inference credential holder./api/*.GET /v1/modelsand should remain behind authenticated non-loopback admission; no credentials/tokens must ever be embedded in the payload.apiKey/ admission prefixes.Summary: No live vulnerability on tip (route does not exist yet). The security work is in the admission design of the feature.
Residual: Version-skew /
base_instructionstrust is an operator concern, not an auth bypass.Wibias commentedon Jul 30, 2026
[shipping-github] Opened PR #772 to address this.
feat(api): serve the generated Codex catalog at GET /api/catalog (#709)
lidge-jun commentedon Jul 31, 2026
Implemented on
dev(verified atbf9bc1ac8), landed in5d7ab0cb7.GET /api/catalognow serves the generated Codex catalog to remote clients —src/server/management/model-routes.ts:101-112. It reads the configured catalog path, returns 404 when no catalog has been generated yet, and sets a version header when one is available. Management authentication applies before dispatch (src/server/index.ts:391-395), so it is not an unauthenticated route.Two notes on scope, so nothing is implied that was not built:
models_cache.jsonroute mentioned in the thread was not implemented.If either matters for your use case, a follow-up issue is welcome. Closing the core request.
feat(api): serve the generated Codex catalog at GET /api/catalog (lid…