This guide adds z-ai/glm-5.2 to Codex through OpenRouter's Responses API. The
example route tries Fireworks first, then Z.ai, and does not fall back to any
other provider.
Tested with Codex Desktop/CLI 0.142.0-alpha.1 on macOS.
- Uses OpenRouter's
/responsesendpoint, because current Codex provider integration requires Responses API. - Stores the OpenRouter API key in macOS Keychain, not in
config.toml. - Creates an OpenRouter preset for
z-ai/glm-5.2. - Adds a Codex model catalog entry for GLM 5.2.
- Points Codex at OpenRouter as the active provider.
Important caveat: Codex uses one active model_provider per run. With this
config active, the Codex run uses OpenRouter as the provider. The model selector
does not automatically route GPT models through OpenAI and GLM through
OpenRouter per model.
Use the same values consistently across all steps:
export CODEX_BIN="/Applications/Codex.app/Contents/Resources/codex"
export OPENROUTER_KEYCHAIN_SERVICE="codex-openrouter-glm52"
export OPENROUTER_PRESET="codex-glm52-openrouter"
export OPENROUTER_MODEL="z-ai/glm-5.2"
export CODEX_MODEL="${OPENROUTER_MODEL}@preset/${OPENROUTER_PRESET}"If your Codex binary is elsewhere, adjust CODEX_BIN.
In OpenRouter:
- Open OpenRouter API Keys.
- Create a dedicated API key for Codex.
- Optional but recommended: set a daily spend limit.
- Copy the key once.
Store it in macOS Keychain:
read -rs OPENROUTER_API_KEY
security add-generic-password \
-a "$USER" \
-s "$OPENROUTER_KEYCHAIN_SERVICE" \
-w "$OPENROUTER_API_KEY" \
-U
unset OPENROUTER_API_KEYDo not put the raw key into Markdown, shell scripts, config.toml, or shared
dotfiles.
Codex custom providers can fetch bearer tokens through a command. Create a helper that prints the OpenRouter key from Keychain:
mkdir -p ~/.codex/bin
cat > ~/.codex/bin/openrouter-glm52-token <<'EOF'
#!/bin/zsh
exec /usr/bin/security find-generic-password -a "${USER}" -s "codex-openrouter-glm52" -w
EOF
chmod 700 ~/.codex/bin/openrouter-glm52-tokenIf you changed OPENROUTER_KEYCHAIN_SERVICE, update the service name inside the
helper script too.
Validate without printing the secret:
test -n "$(~/.codex/bin/openrouter-glm52-token)" && echo "OpenRouter key helper OK"Create a preset that routes GLM 5.2 to Fireworks first and Z.ai second:
OPENROUTER_API_KEY="$(~/.codex/bin/openrouter-glm52-token)"
curl -fsS \
-X POST "https://openrouter.ai/api/v1/presets/${OPENROUTER_PRESET}/responses" \
-H "Authorization: Bearer ${OPENROUTER_API_KEY}" \
-H "Content-Type: application/json" \
--data '{
"model": "z-ai/glm-5.2",
"provider": {
"order": ["fireworks", "z-ai"],
"allow_fallbacks": false
}
}'
unset OPENROUTER_API_KEYDo not set require_parameters: true.
Codex sends tool schemas (tools, tool_choice, etc.) with agent requests. With
require_parameters: true, OpenRouter may reject the request before inference
with:
404 Not Found: No endpoints found that can handle the requested parameters
Create a GLM-only catalog:
mkdir -p ~/.codex/model-catalogs
cat > ~/.codex/model-catalogs/glm52-openrouter.json <<EOF
{
"models": [
{
"slug": "${CODEX_MODEL}",
"display_name": "Z.ai GLM 5.2 (OpenRouter Fireworks + Z.ai)",
"description": "GLM 5.2 via OpenRouter preset ${OPENROUTER_PRESET}. The preset tries Fireworks first, then Z.ai, with fallbacks beyond that disabled.",
"base_instructions": "",
"model_messages": null,
"default_reasoning_level": "xhigh",
"supported_reasoning_levels": [
{ "effort": "minimal", "description": "Minimal reasoning" },
{ "effort": "low", "description": "Fast responses with lighter reasoning" },
{ "effort": "medium", "description": "Balances speed and reasoning depth" },
{ "effort": "high", "description": "Greater reasoning depth for complex problems" },
{ "effort": "xhigh", "description": "Extra high reasoning depth for complex problems" }
],
"shell_type": "shell_command",
"visibility": "list",
"supported_in_api": true,
"priority": 50,
"additional_speed_tiers": [],
"service_tiers": [],
"availability_nux": null,
"upgrade": null,
"context_window": 1048576,
"max_context_window": 1048576,
"effective_context_window_percent": 95,
"truncation_policy": { "mode": "tokens", "limit": 10000 },
"supports_parallel_tool_calls": true,
"supports_reasoning_summaries": false,
"default_reasoning_summary": "none",
"support_verbosity": false,
"default_verbosity": "medium",
"apply_patch_tool_type": "freeform",
"experimental_supported_tools": [],
"input_modalities": ["text"],
"supports_search_tool": false,
"web_search_tool_type": "text",
"supports_image_detail_original": false,
"use_responses_lite": false
}
]
}
EOFsupport_verbosity must be false. If global Codex config has
model_verbosity set, Codex should then ignore it for GLM instead of sending
OpenAI-specific verbosity fields to OpenRouter.
model_catalog_json replaces the catalog; it does not merge automatically. To
keep the existing Codex models in the selector, merge the GLM entry into the
current catalog:
"$CODEX_BIN" debug models > /tmp/codex-models.json
node <<'NODE'
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const home = os.homedir();
const basePath = '/tmp/codex-models.json';
const extraPath = path.join(home, '.codex/model-catalogs/glm52-openrouter.json');
const outPath = path.join(home, '.codex/model-catalogs/codex-plus-glm52-openrouter.json');
const base = JSON.parse(fs.readFileSync(basePath, 'utf8'));
const extra = JSON.parse(fs.readFileSync(extraPath, 'utf8')).models[0];
const models = Array.isArray(base) ? base : base.models;
if (!Array.isArray(models)) {
throw new Error('Unexpected Codex model catalog shape');
}
const mergedModels = models
.filter((model) => model.slug !== extra.slug)
.concat(extra);
const merged = Array.isArray(base)
? mergedModels
: { ...base, models: mergedModels };
fs.writeFileSync(outPath, JSON.stringify(merged, null, 2) + '\n');
console.log(outPath);
NODEBack up first:
cp -p ~/.codex/config.toml ~/.codex/config.toml.bak-glm52-openrouter-$(date +%Y%m%d-%H%M%S)Then add or update these top-level keys in ~/.codex/config.toml:
# Replace /Users/YOUR_USERNAME with your actual macOS home path.
model = "z-ai/glm-5.2@preset/codex-glm52-openrouter"
model_provider = "openrouter"
model_catalog_json = "/Users/YOUR_USERNAME/.codex/model-catalogs/codex-plus-glm52-openrouter.json"
model_reasoning_effort = "xhigh"If you already have model_verbosity globally set, you can leave it. The GLM
catalog entry above marks verbosity as unsupported, so Codex should ignore it for
this model.
Add or update the provider block:
[model_providers.openrouter]
name = "OpenRouter"
base_url = "https://openrouter.ai/api/v1"
wire_api = "responses"
http_headers = { "HTTP-Referer" = "https://codex.local", "X-Title" = "Codex GLM 5.2 via OpenRouter" }
[model_providers.openrouter.auth]
command = "/Users/YOUR_USERNAME/.codex/bin/openrouter-glm52-token"
timeout_ms = 5000
refresh_interval_ms = 0Use wire_api = "responses". Current Codex no longer supports using Chat
Completions as the provider wire API for this path.
Check that Codex can load the merged catalog:
"$CODEX_BIN" debug models | rg "GLM 5.2|OpenRouter"Optional smoke test:
"$CODEX_BIN" exec \
--ephemeral \
--skip-git-repo-check \
-C /private/tmp \
"Reply OK only."Expected output:
OK
Restart Codex Desktop after changing ~/.codex/config.toml. GLM 5.2 should now
be the active model and should appear in the model selector.
If Codex Desktop cannot start, restore the backup manually:
mv ~/.codex/config.toml ~/.codex/config.toml.broken-glm52-openrouter
cp -p ~/.codex/config.toml.bak-glm52-openrouter-YYYYMMDD-HHMMSS ~/.codex/config.tomlThen restart Codex Desktop.
Usually means the OpenRouter preset has require_parameters: true, or the
request is being routed to providers that OpenRouter does not consider compatible
with Codex's tool-heavy Responses request.
Fix: remove require_parameters from the preset.
Usually means the selected upstream provider is rate-limiting. With the example route, OpenRouter may try Fireworks and then Z.ai, but it will not use providers outside that ordered list.
Options:
- wait and retry
- add more providers to the
orderlist - temporarily allow broader fallbacks
- use BYOK/provider integrations if available
Expected when the catalog entry has:
"support_verbosity": falseThat is intentional for this setup.