Repository files navigation

codex-zai-proxy

A localhost-only reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API. Translates the Responses API wire format (which Codex CLI requires) into Chat Completions API requests (which Z.ai accepts), handling the full request/response lifecycle including streaming SSE and function/tool call translation.

Why

Codex CLI (@openai/codex) removed support for wire_api = "chat" and now requires wire_api = "responses". Z.ai's coding endpoint speaks the Chat Completions protocol. This proxy sits between them, translating in both directions so Codex works transparently with a Z.ai subscription.

Architecture

 Codex CLI Localhost Proxy Z.ai API
───────── ─────────────── ────────
POST /v1/responses ──► Request translation ──► POST /chat/completions
(Responses API format) • instructions → system msg (Chat Completions format)
• input items → messages
• tool format conversion
• role normalization
SSE stream (Responses) ◄── Response translation ◄── SSE stream (Chat Completions)
• chat chunks → response events
• tool_call deltas tracked
• proper SSE event sequencing

Requirements

  • Podman (rootless)
  • Codex CLI (npm install -g @openai/codex)
  • Z.ai GLM Coding Plan subscription with API key

Quick Start

# Clone
git clone https://github.com/YOUR_USERNAME/codex-zai-proxy.git
cd codex-zai-proxy
# Configure your API key
cp .env.example .env
# Edit .env and set ZAI_API_KEY=your-key-here
chmod 600 .env
# Build and start
podman build -t codex-zai-proxy .
podman run -d \
--name codex-zai-proxy \
--env-file .env \
--publish 127.0.0.1:4891:4891 \
--restart unless-stopped \
--security-opt no-new-privileges \
--cap-drop ALL \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
codex-zai-proxy
# Verify
curl http://127.0.0.1:4891/health

Codex Configuration

Add this to ~/.codex/config.toml:

profile = "glm_proxy"
[model_providers.z_ai_proxy]
name = "z.ai via Local Proxy"base_url = "http://127.0.0.1:4891/v1"env_key = "ZAI_API_KEY"wire_api = "responses"
[profiles.glm_proxy]
model = "glm-5.1"model_provider = "z_ai_proxy"

To switch models, change the model value in your profile (e.g. "glm-5", "glm-4.7"). The proxy passes through whatever model Codex requests — no proxy restart needed. Just start a new Codex session.

Make sure ZAI_API_KEY is set in your shell environment, then run:

codex

Translation Details

Request (Responses → Chat Completions)

Responses APIChat Completions
instructions fieldsystem role message
input[] with type: "message"messages[] with role + content
input[] with type: "function_call"Assistant message with tool_calls[]
input[] with type: "function_call_output"tool role message
input[] with type: "local_shell_call"Mapped to function tool_call
tools[] with {type, name, parameters}tools[] with {type: "function", function: {name, parameters}}
role: "developer"role: "system"

Response (Chat Completions SSE → Responses API SSE)

The proxy consumes the upstream Chat Completions stream and emits properly sequenced Responses API events:

  1. response.created — emitted immediately
  2. response.output_item.added — when text or tool output begins
  3. response.output_text.delta — each text chunk from upstream
  4. response.output_item.done — completed text message or function call
  5. response.completed — final event with usage stats

Tool call arguments are accumulated across multiple deltas and emitted as a complete function_call output item.

Endpoints

MethodPathDescription
GET/healthHealth check, returns upstream URL
POST/v1/responsesMain proxy endpoint (Codex hits this)
GET/v1/modelsMinimal models endpoint for compatibility

Persistent Service (systemd)

For auto-start on login, use the included Quadlet:

# Copy Quadlet file
mkdir -p ~/.config/containers/systemd
cp codex-zai-proxy.container ~/.config/containers/systemd/
# Generate and enable
/usr/libexec/podman/quadlet --user ~/.config/systemd/user/generated
cp ~/.config/systemd/user/generated/codex-zai-proxy.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now codex-zai-proxy

Management:

systemctl --user start codex-zai-proxy
systemctl --user stop codex-zai-proxy
systemctl --user restart codex-zai-proxy
systemctl --user status codex-zai-proxy
journalctl --user -u codex-zai-proxy -f

Management Script

./manage.sh build # Build the container image
./manage.sh start # Start container
./manage.sh stop # Stop and remove container
./manage.sh restart # Stop then start
./manage.sh rebuild # Rebuild image and restart
./manage.sh logs # Follow container logs
./manage.sh status # Show container status
./manage.sh test# Health check

Security

  • Binds to 127.0.0.1 only — Podman --publish 127.0.0.1:4891:4891 enforces localhost-only access
  • Non-root process inside container
  • Read-only container filesystem with tmpfs for /tmp
  • All Linux capabilities dropped (--cap-drop ALL)
  • No new privileges security option set
  • API key loaded from .env file (mode 600) at runtime — never baked into the image
  • Upstream errors are sanitized before returning to the client

Configuration

Environment Variables

VariableDefaultDescription
ZAI_API_KEY(required)Your Z.ai API key
ZAI_BASE_URLhttps://api.z.ai/api/coding/paas/v4Z.ai API base URL
PROXY_PORT4891Port to listen on
LOG_LEVELINFOLogging verbosity

Limitations

  • Reasoning summaries — The reasoning field is stripped from requests since GLM models don't support it
  • Built-in tools — OpenAI-specific tools (web_search, file_search, code_interpreter) are not forwarded
  • previous_response_id — Not supported; full conversation history is sent each turn (same as Codex's HTTP transport behavior)
  • Model-specific features — Any feature requiring OpenAI server-side infrastructure won't work

Rebuilding After Changes

podman build -t codex-zai-proxy .
systemctl --user restart codex-zai-proxy

License

MIT

About

Localhost reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

codex-zai-proxy

A localhost-only reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API. Translates the Responses API wire format (which Codex CLI requires) into Chat Completions API requests (which Z.ai accepts), handling the full request/response lifecycle including streaming SSE and function/tool call translation.

Why

Codex CLI (@openai/codex) removed support for wire_api = "chat" and now requires wire_api = "responses". Z.ai's coding endpoint speaks the Chat Completions protocol. This proxy sits between them, translating in both directions so Codex works transparently with a Z.ai subscription.

Architecture

 Codex CLI Localhost Proxy Z.ai API
───────── ─────────────── ────────
POST /v1/responses ──► Request translation ──► POST /chat/completions
(Responses API format) • instructions → system msg (Chat Completions format)
• input items → messages
• tool format conversion
• role normalization
SSE stream (Responses) ◄── Response translation ◄── SSE stream (Chat Completions)
• chat chunks → response events
• tool_call deltas tracked
• proper SSE event sequencing

Requirements

  • Podman (rootless)
  • Codex CLI (npm install -g @openai/codex)
  • Z.ai GLM Coding Plan subscription with API key

Quick Start

# Clone
git clone https://github.com/YOUR_USERNAME/codex-zai-proxy.git
cd codex-zai-proxy
# Configure your API key
cp .env.example .env
# Edit .env and set ZAI_API_KEY=your-key-here
chmod 600 .env
# Build and start
podman build -t codex-zai-proxy .
podman run -d \
--name codex-zai-proxy \
--env-file .env \
--publish 127.0.0.1:4891:4891 \
--restart unless-stopped \
--security-opt no-new-privileges \
--cap-drop ALL \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
codex-zai-proxy
# Verify
curl http://127.0.0.1:4891/health

Codex Configuration

Add this to ~/.codex/config.toml:

profile = "glm_proxy"
[model_providers.z_ai_proxy]
name = "z.ai via Local Proxy"base_url = "http://127.0.0.1:4891/v1"env_key = "ZAI_API_KEY"wire_api = "responses"
[profiles.glm_proxy]
model = "glm-5.1"model_provider = "z_ai_proxy"

To switch models, change the model value in your profile (e.g. "glm-5", "glm-4.7"). The proxy passes through whatever model Codex requests — no proxy restart needed. Just start a new Codex session.

Make sure ZAI_API_KEY is set in your shell environment, then run:

codex

Translation Details

Request (Responses → Chat Completions)

Responses APIChat Completions
instructions fieldsystem role message
input[] with type: "message"messages[] with role + content
input[] with type: "function_call"Assistant message with tool_calls[]
input[] with type: "function_call_output"tool role message
input[] with type: "local_shell_call"Mapped to function tool_call
tools[] with {type, name, parameters}tools[] with {type: "function", function: {name, parameters}}
role: "developer"role: "system"

Response (Chat Completions SSE → Responses API SSE)

The proxy consumes the upstream Chat Completions stream and emits properly sequenced Responses API events:

  1. response.created — emitted immediately
  2. response.output_item.added — when text or tool output begins
  3. response.output_text.delta — each text chunk from upstream
  4. response.output_item.done — completed text message or function call
  5. response.completed — final event with usage stats

Tool call arguments are accumulated across multiple deltas and emitted as a complete function_call output item.

Endpoints

MethodPathDescription
GET/healthHealth check, returns upstream URL
POST/v1/responsesMain proxy endpoint (Codex hits this)
GET/v1/modelsMinimal models endpoint for compatibility

Persistent Service (systemd)

For auto-start on login, use the included Quadlet:

# Copy Quadlet file
mkdir -p ~/.config/containers/systemd
cp codex-zai-proxy.container ~/.config/containers/systemd/
# Generate and enable
/usr/libexec/podman/quadlet --user ~/.config/systemd/user/generated
cp ~/.config/systemd/user/generated/codex-zai-proxy.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now codex-zai-proxy

Management:

systemctl --user start codex-zai-proxy
systemctl --user stop codex-zai-proxy
systemctl --user restart codex-zai-proxy
systemctl --user status codex-zai-proxy
journalctl --user -u codex-zai-proxy -f

Management Script

./manage.sh build # Build the container image
./manage.sh start # Start container
./manage.sh stop # Stop and remove container
./manage.sh restart # Stop then start
./manage.sh rebuild # Rebuild image and restart
./manage.sh logs # Follow container logs
./manage.sh status # Show container status
./manage.sh test# Health check

Security

  • Binds to 127.0.0.1 only — Podman --publish 127.0.0.1:4891:4891 enforces localhost-only access
  • Non-root process inside container
  • Read-only container filesystem with tmpfs for /tmp
  • All Linux capabilities dropped (--cap-drop ALL)
  • No new privileges security option set
  • API key loaded from .env file (mode 600) at runtime — never baked into the image
  • Upstream errors are sanitized before returning to the client

Configuration

Environment Variables

VariableDefaultDescription
ZAI_API_KEY(required)Your Z.ai API key
ZAI_BASE_URLhttps://api.z.ai/api/coding/paas/v4Z.ai API base URL
PROXY_PORT4891Port to listen on
LOG_LEVELINFOLogging verbosity

Limitations

  • Reasoning summaries — The reasoning field is stripped from requests since GLM models don't support it
  • Built-in tools — OpenAI-specific tools (web_search, file_search, code_interpreter) are not forwarded
  • previous_response_id — Not supported; full conversation history is sent each turn (same as Codex's HTTP transport behavior)
  • Model-specific features — Any feature requiring OpenAI server-side infrastructure won't work

Rebuilding After Changes

podman build -t codex-zai-proxy .
systemctl --user restart codex-zai-proxy

License

MIT

About

Localhost reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

codex-zai-proxy

A localhost-only reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API. Translates the Responses API wire format (which Codex CLI requires) into Chat Completions API requests (which Z.ai accepts), handling the full request/response lifecycle including streaming SSE and function/tool call translation.

Why

Codex CLI (@openai/codex) removed support for wire_api = "chat" and now requires wire_api = "responses". Z.ai's coding endpoint speaks the Chat Completions protocol. This proxy sits between them, translating in both directions so Codex works transparently with a Z.ai subscription.

Architecture

 Codex CLI Localhost Proxy Z.ai API
───────── ─────────────── ────────
POST /v1/responses ──► Request translation ──► POST /chat/completions
(Responses API format) • instructions → system msg (Chat Completions format)
• input items → messages
• tool format conversion
• role normalization
SSE stream (Responses) ◄── Response translation ◄── SSE stream (Chat Completions)
• chat chunks → response events
• tool_call deltas tracked
• proper SSE event sequencing

Requirements

  • Podman (rootless)
  • Codex CLI (npm install -g @openai/codex)
  • Z.ai GLM Coding Plan subscription with API key

Quick Start

# Clone
git clone https://github.com/YOUR_USERNAME/codex-zai-proxy.git
cd codex-zai-proxy
# Configure your API key
cp .env.example .env
# Edit .env and set ZAI_API_KEY=your-key-here
chmod 600 .env
# Build and start
podman build -t codex-zai-proxy .
podman run -d \
--name codex-zai-proxy \
--env-file .env \
--publish 127.0.0.1:4891:4891 \
--restart unless-stopped \
--security-opt no-new-privileges \
--cap-drop ALL \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
codex-zai-proxy
# Verify
curl http://127.0.0.1:4891/health

Codex Configuration

Add this to ~/.codex/config.toml:

profile = "glm_proxy"
[model_providers.z_ai_proxy]
name = "z.ai via Local Proxy"base_url = "http://127.0.0.1:4891/v1"env_key = "ZAI_API_KEY"wire_api = "responses"
[profiles.glm_proxy]
model = "glm-5.1"model_provider = "z_ai_proxy"

To switch models, change the model value in your profile (e.g. "glm-5", "glm-4.7"). The proxy passes through whatever model Codex requests — no proxy restart needed. Just start a new Codex session.

Make sure ZAI_API_KEY is set in your shell environment, then run:

codex

Translation Details

Request (Responses → Chat Completions)

Responses APIChat Completions
instructions fieldsystem role message
input[] with type: "message"messages[] with role + content
input[] with type: "function_call"Assistant message with tool_calls[]
input[] with type: "function_call_output"tool role message
input[] with type: "local_shell_call"Mapped to function tool_call
tools[] with {type, name, parameters}tools[] with {type: "function", function: {name, parameters}}
role: "developer"role: "system"

Response (Chat Completions SSE → Responses API SSE)

The proxy consumes the upstream Chat Completions stream and emits properly sequenced Responses API events:

  1. response.created — emitted immediately
  2. response.output_item.added — when text or tool output begins
  3. response.output_text.delta — each text chunk from upstream
  4. response.output_item.done — completed text message or function call
  5. response.completed — final event with usage stats

Tool call arguments are accumulated across multiple deltas and emitted as a complete function_call output item.

Endpoints

MethodPathDescription
GET/healthHealth check, returns upstream URL
POST/v1/responsesMain proxy endpoint (Codex hits this)
GET/v1/modelsMinimal models endpoint for compatibility

Persistent Service (systemd)

For auto-start on login, use the included Quadlet:

# Copy Quadlet file
mkdir -p ~/.config/containers/systemd
cp codex-zai-proxy.container ~/.config/containers/systemd/
# Generate and enable
/usr/libexec/podman/quadlet --user ~/.config/systemd/user/generated
cp ~/.config/systemd/user/generated/codex-zai-proxy.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now codex-zai-proxy

Management:

systemctl --user start codex-zai-proxy
systemctl --user stop codex-zai-proxy
systemctl --user restart codex-zai-proxy
systemctl --user status codex-zai-proxy
journalctl --user -u codex-zai-proxy -f

Management Script

./manage.sh build # Build the container image
./manage.sh start # Start container
./manage.sh stop # Stop and remove container
./manage.sh restart # Stop then start
./manage.sh rebuild # Rebuild image and restart
./manage.sh logs # Follow container logs
./manage.sh status # Show container status
./manage.sh test# Health check

Security

  • Binds to 127.0.0.1 only — Podman --publish 127.0.0.1:4891:4891 enforces localhost-only access
  • Non-root process inside container
  • Read-only container filesystem with tmpfs for /tmp
  • All Linux capabilities dropped (--cap-drop ALL)
  • No new privileges security option set
  • API key loaded from .env file (mode 600) at runtime — never baked into the image
  • Upstream errors are sanitized before returning to the client

Configuration

Environment Variables

VariableDefaultDescription
ZAI_API_KEY(required)Your Z.ai API key
ZAI_BASE_URLhttps://api.z.ai/api/coding/paas/v4Z.ai API base URL
PROXY_PORT4891Port to listen on
LOG_LEVELINFOLogging verbosity

Limitations

  • Reasoning summaries — The reasoning field is stripped from requests since GLM models don't support it
  • Built-in tools — OpenAI-specific tools (web_search, file_search, code_interpreter) are not forwarded
  • previous_response_id — Not supported; full conversation history is sent each turn (same as Codex's HTTP transport behavior)
  • Model-specific features — Any feature requiring OpenAI server-side infrastructure won't work

Rebuilding After Changes

podman build -t codex-zai-proxy .
systemctl --user restart codex-zai-proxy

License

MIT

About

Localhost reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

codex-zai-proxy

A localhost-only reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API. Translates the Responses API wire format (which Codex CLI requires) into Chat Completions API requests (which Z.ai accepts), handling the full request/response lifecycle including streaming SSE and function/tool call translation.

Why

Codex CLI (@openai/codex) removed support for wire_api = "chat" and now requires wire_api = "responses". Z.ai's coding endpoint speaks the Chat Completions protocol. This proxy sits between them, translating in both directions so Codex works transparently with a Z.ai subscription.

Architecture

 Codex CLI Localhost Proxy Z.ai API
───────── ─────────────── ────────
POST /v1/responses ──► Request translation ──► POST /chat/completions
(Responses API format) • instructions → system msg (Chat Completions format)
• input items → messages
• tool format conversion
• role normalization
SSE stream (Responses) ◄── Response translation ◄── SSE stream (Chat Completions)
• chat chunks → response events
• tool_call deltas tracked
• proper SSE event sequencing

Requirements

  • Podman (rootless)
  • Codex CLI (npm install -g @openai/codex)
  • Z.ai GLM Coding Plan subscription with API key

Quick Start

# Clone
git clone https://github.com/YOUR_USERNAME/codex-zai-proxy.git
cd codex-zai-proxy
# Configure your API key
cp .env.example .env
# Edit .env and set ZAI_API_KEY=your-key-here
chmod 600 .env
# Build and start
podman build -t codex-zai-proxy .
podman run -d \
--name codex-zai-proxy \
--env-file .env \
--publish 127.0.0.1:4891:4891 \
--restart unless-stopped \
--security-opt no-new-privileges \
--cap-drop ALL \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
codex-zai-proxy
# Verify
curl http://127.0.0.1:4891/health

Codex Configuration

Add this to ~/.codex/config.toml:

profile = "glm_proxy"
[model_providers.z_ai_proxy]
name = "z.ai via Local Proxy"base_url = "http://127.0.0.1:4891/v1"env_key = "ZAI_API_KEY"wire_api = "responses"
[profiles.glm_proxy]
model = "glm-5.1"model_provider = "z_ai_proxy"

To switch models, change the model value in your profile (e.g. "glm-5", "glm-4.7"). The proxy passes through whatever model Codex requests — no proxy restart needed. Just start a new Codex session.

Make sure ZAI_API_KEY is set in your shell environment, then run:

codex

Translation Details

Request (Responses → Chat Completions)

Responses APIChat Completions
instructions fieldsystem role message
input[] with type: "message"messages[] with role + content
input[] with type: "function_call"Assistant message with tool_calls[]
input[] with type: "function_call_output"tool role message
input[] with type: "local_shell_call"Mapped to function tool_call
tools[] with {type, name, parameters}tools[] with {type: "function", function: {name, parameters}}
role: "developer"role: "system"

Response (Chat Completions SSE → Responses API SSE)

The proxy consumes the upstream Chat Completions stream and emits properly sequenced Responses API events:

  1. response.created — emitted immediately
  2. response.output_item.added — when text or tool output begins
  3. response.output_text.delta — each text chunk from upstream
  4. response.output_item.done — completed text message or function call
  5. response.completed — final event with usage stats

Tool call arguments are accumulated across multiple deltas and emitted as a complete function_call output item.

Endpoints

MethodPathDescription
GET/healthHealth check, returns upstream URL
POST/v1/responsesMain proxy endpoint (Codex hits this)
GET/v1/modelsMinimal models endpoint for compatibility

Persistent Service (systemd)

For auto-start on login, use the included Quadlet:

# Copy Quadlet file
mkdir -p ~/.config/containers/systemd
cp codex-zai-proxy.container ~/.config/containers/systemd/
# Generate and enable
/usr/libexec/podman/quadlet --user ~/.config/systemd/user/generated
cp ~/.config/systemd/user/generated/codex-zai-proxy.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now codex-zai-proxy

Management:

systemctl --user start codex-zai-proxy
systemctl --user stop codex-zai-proxy
systemctl --user restart codex-zai-proxy
systemctl --user status codex-zai-proxy
journalctl --user -u codex-zai-proxy -f

Management Script

./manage.sh build # Build the container image
./manage.sh start # Start container
./manage.sh stop # Stop and remove container
./manage.sh restart # Stop then start
./manage.sh rebuild # Rebuild image and restart
./manage.sh logs # Follow container logs
./manage.sh status # Show container status
./manage.sh test# Health check

Security

  • Binds to 127.0.0.1 only — Podman --publish 127.0.0.1:4891:4891 enforces localhost-only access
  • Non-root process inside container
  • Read-only container filesystem with tmpfs for /tmp
  • All Linux capabilities dropped (--cap-drop ALL)
  • No new privileges security option set
  • API key loaded from .env file (mode 600) at runtime — never baked into the image
  • Upstream errors are sanitized before returning to the client

Configuration

Environment Variables

VariableDefaultDescription
ZAI_API_KEY(required)Your Z.ai API key
ZAI_BASE_URLhttps://api.z.ai/api/coding/paas/v4Z.ai API base URL
PROXY_PORT4891Port to listen on
LOG_LEVELINFOLogging verbosity

Limitations

  • Reasoning summaries — The reasoning field is stripped from requests since GLM models don't support it
  • Built-in tools — OpenAI-specific tools (web_search, file_search, code_interpreter) are not forwarded
  • previous_response_id — Not supported; full conversation history is sent each turn (same as Codex's HTTP transport behavior)
  • Model-specific features — Any feature requiring OpenAI server-side infrastructure won't work

Rebuilding After Changes

podman build -t codex-zai-proxy .
systemctl --user restart codex-zai-proxy

License

MIT

About

Localhost reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

codex-zai-proxy

A localhost-only reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API. Translates the Responses API wire format (which Codex CLI requires) into Chat Completions API requests (which Z.ai accepts), handling the full request/response lifecycle including streaming SSE and function/tool call translation.

Why

Codex CLI (@openai/codex) removed support for wire_api = "chat" and now requires wire_api = "responses". Z.ai's coding endpoint speaks the Chat Completions protocol. This proxy sits between them, translating in both directions so Codex works transparently with a Z.ai subscription.

Architecture

 Codex CLI Localhost Proxy Z.ai API
───────── ─────────────── ────────
POST /v1/responses ──► Request translation ──► POST /chat/completions
(Responses API format) • instructions → system msg (Chat Completions format)
• input items → messages
• tool format conversion
• role normalization
SSE stream (Responses) ◄── Response translation ◄── SSE stream (Chat Completions)
• chat chunks → response events
• tool_call deltas tracked
• proper SSE event sequencing

Requirements

  • Podman (rootless)
  • Codex CLI (npm install -g @openai/codex)
  • Z.ai GLM Coding Plan subscription with API key

Quick Start

# Clone
git clone https://github.com/YOUR_USERNAME/codex-zai-proxy.git
cd codex-zai-proxy
# Configure your API key
cp .env.example .env
# Edit .env and set ZAI_API_KEY=your-key-here
chmod 600 .env
# Build and start
podman build -t codex-zai-proxy .
podman run -d \
--name codex-zai-proxy \
--env-file .env \
--publish 127.0.0.1:4891:4891 \
--restart unless-stopped \
--security-opt no-new-privileges \
--cap-drop ALL \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
codex-zai-proxy
# Verify
curl http://127.0.0.1:4891/health

Codex Configuration

Add this to ~/.codex/config.toml:

profile = "glm_proxy"
[model_providers.z_ai_proxy]
name = "z.ai via Local Proxy"base_url = "http://127.0.0.1:4891/v1"env_key = "ZAI_API_KEY"wire_api = "responses"
[profiles.glm_proxy]
model = "glm-5.1"model_provider = "z_ai_proxy"

To switch models, change the model value in your profile (e.g. "glm-5", "glm-4.7"). The proxy passes through whatever model Codex requests — no proxy restart needed. Just start a new Codex session.

Make sure ZAI_API_KEY is set in your shell environment, then run:

codex

Translation Details

Request (Responses → Chat Completions)

Responses APIChat Completions
instructions fieldsystem role message
input[] with type: "message"messages[] with role + content
input[] with type: "function_call"Assistant message with tool_calls[]
input[] with type: "function_call_output"tool role message
input[] with type: "local_shell_call"Mapped to function tool_call
tools[] with {type, name, parameters}tools[] with {type: "function", function: {name, parameters}}
role: "developer"role: "system"

Response (Chat Completions SSE → Responses API SSE)

The proxy consumes the upstream Chat Completions stream and emits properly sequenced Responses API events:

  1. response.created — emitted immediately
  2. response.output_item.added — when text or tool output begins
  3. response.output_text.delta — each text chunk from upstream
  4. response.output_item.done — completed text message or function call
  5. response.completed — final event with usage stats

Tool call arguments are accumulated across multiple deltas and emitted as a complete function_call output item.

Endpoints

MethodPathDescription
GET/healthHealth check, returns upstream URL
POST/v1/responsesMain proxy endpoint (Codex hits this)
GET/v1/modelsMinimal models endpoint for compatibility

Persistent Service (systemd)

For auto-start on login, use the included Quadlet:

# Copy Quadlet file
mkdir -p ~/.config/containers/systemd
cp codex-zai-proxy.container ~/.config/containers/systemd/
# Generate and enable
/usr/libexec/podman/quadlet --user ~/.config/systemd/user/generated
cp ~/.config/systemd/user/generated/codex-zai-proxy.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now codex-zai-proxy

Management:

systemctl --user start codex-zai-proxy
systemctl --user stop codex-zai-proxy
systemctl --user restart codex-zai-proxy
systemctl --user status codex-zai-proxy
journalctl --user -u codex-zai-proxy -f

Management Script

./manage.sh build # Build the container image
./manage.sh start # Start container
./manage.sh stop # Stop and remove container
./manage.sh restart # Stop then start
./manage.sh rebuild # Rebuild image and restart
./manage.sh logs # Follow container logs
./manage.sh status # Show container status
./manage.sh test# Health check

Security

  • Binds to 127.0.0.1 only — Podman --publish 127.0.0.1:4891:4891 enforces localhost-only access
  • Non-root process inside container
  • Read-only container filesystem with tmpfs for /tmp
  • All Linux capabilities dropped (--cap-drop ALL)
  • No new privileges security option set
  • API key loaded from .env file (mode 600) at runtime — never baked into the image
  • Upstream errors are sanitized before returning to the client

Configuration

Environment Variables

VariableDefaultDescription
ZAI_API_KEY(required)Your Z.ai API key
ZAI_BASE_URLhttps://api.z.ai/api/coding/paas/v4Z.ai API base URL
PROXY_PORT4891Port to listen on
LOG_LEVELINFOLogging verbosity

Limitations

  • Reasoning summaries — The reasoning field is stripped from requests since GLM models don't support it
  • Built-in tools — OpenAI-specific tools (web_search, file_search, code_interpreter) are not forwarded
  • previous_response_id — Not supported; full conversation history is sent each turn (same as Codex's HTTP transport behavior)
  • Model-specific features — Any feature requiring OpenAI server-side infrastructure won't work

Rebuilding After Changes

podman build -t codex-zai-proxy .
systemctl --user restart codex-zai-proxy

License

MIT

About

Localhost reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

codex-zai-proxy

A localhost-only reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API. Translates the Responses API wire format (which Codex CLI requires) into Chat Completions API requests (which Z.ai accepts), handling the full request/response lifecycle including streaming SSE and function/tool call translation.

Why

Codex CLI (@openai/codex) removed support for wire_api = "chat" and now requires wire_api = "responses". Z.ai's coding endpoint speaks the Chat Completions protocol. This proxy sits between them, translating in both directions so Codex works transparently with a Z.ai subscription.

Architecture

 Codex CLI Localhost Proxy Z.ai API
───────── ─────────────── ────────
POST /v1/responses ──► Request translation ──► POST /chat/completions
(Responses API format) • instructions → system msg (Chat Completions format)
• input items → messages
• tool format conversion
• role normalization
SSE stream (Responses) ◄── Response translation ◄── SSE stream (Chat Completions)
• chat chunks → response events
• tool_call deltas tracked
• proper SSE event sequencing

Requirements

  • Podman (rootless)
  • Codex CLI (npm install -g @openai/codex)
  • Z.ai GLM Coding Plan subscription with API key

Quick Start

# Clone
git clone https://github.com/YOUR_USERNAME/codex-zai-proxy.git
cd codex-zai-proxy
# Configure your API key
cp .env.example .env
# Edit .env and set ZAI_API_KEY=your-key-here
chmod 600 .env
# Build and start
podman build -t codex-zai-proxy .
podman run -d \
--name codex-zai-proxy \
--env-file .env \
--publish 127.0.0.1:4891:4891 \
--restart unless-stopped \
--security-opt no-new-privileges \
--cap-drop ALL \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
codex-zai-proxy
# Verify
curl http://127.0.0.1:4891/health

Codex Configuration

Add this to ~/.codex/config.toml:

profile = "glm_proxy"
[model_providers.z_ai_proxy]
name = "z.ai via Local Proxy"base_url = "http://127.0.0.1:4891/v1"env_key = "ZAI_API_KEY"wire_api = "responses"
[profiles.glm_proxy]
model = "glm-5.1"model_provider = "z_ai_proxy"

To switch models, change the model value in your profile (e.g. "glm-5", "glm-4.7"). The proxy passes through whatever model Codex requests — no proxy restart needed. Just start a new Codex session.

Make sure ZAI_API_KEY is set in your shell environment, then run:

codex

Translation Details

Request (Responses → Chat Completions)

Responses APIChat Completions
instructions fieldsystem role message
input[] with type: "message"messages[] with role + content
input[] with type: "function_call"Assistant message with tool_calls[]
input[] with type: "function_call_output"tool role message
input[] with type: "local_shell_call"Mapped to function tool_call
tools[] with {type, name, parameters}tools[] with {type: "function", function: {name, parameters}}
role: "developer"role: "system"

Response (Chat Completions SSE → Responses API SSE)

The proxy consumes the upstream Chat Completions stream and emits properly sequenced Responses API events:

  1. response.created — emitted immediately
  2. response.output_item.added — when text or tool output begins
  3. response.output_text.delta — each text chunk from upstream
  4. response.output_item.done — completed text message or function call
  5. response.completed — final event with usage stats

Tool call arguments are accumulated across multiple deltas and emitted as a complete function_call output item.

Endpoints

MethodPathDescription
GET/healthHealth check, returns upstream URL
POST/v1/responsesMain proxy endpoint (Codex hits this)
GET/v1/modelsMinimal models endpoint for compatibility

Persistent Service (systemd)

For auto-start on login, use the included Quadlet:

# Copy Quadlet file
mkdir -p ~/.config/containers/systemd
cp codex-zai-proxy.container ~/.config/containers/systemd/
# Generate and enable
/usr/libexec/podman/quadlet --user ~/.config/systemd/user/generated
cp ~/.config/systemd/user/generated/codex-zai-proxy.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now codex-zai-proxy

Management:

systemctl --user start codex-zai-proxy
systemctl --user stop codex-zai-proxy
systemctl --user restart codex-zai-proxy
systemctl --user status codex-zai-proxy
journalctl --user -u codex-zai-proxy -f

Management Script

./manage.sh build # Build the container image
./manage.sh start # Start container
./manage.sh stop # Stop and remove container
./manage.sh restart # Stop then start
./manage.sh rebuild # Rebuild image and restart
./manage.sh logs # Follow container logs
./manage.sh status # Show container status
./manage.sh test# Health check

Security

  • Binds to 127.0.0.1 only — Podman --publish 127.0.0.1:4891:4891 enforces localhost-only access
  • Non-root process inside container
  • Read-only container filesystem with tmpfs for /tmp
  • All Linux capabilities dropped (--cap-drop ALL)
  • No new privileges security option set
  • API key loaded from .env file (mode 600) at runtime — never baked into the image
  • Upstream errors are sanitized before returning to the client

Configuration

Environment Variables

VariableDefaultDescription
ZAI_API_KEY(required)Your Z.ai API key
ZAI_BASE_URLhttps://api.z.ai/api/coding/paas/v4Z.ai API base URL
PROXY_PORT4891Port to listen on
LOG_LEVELINFOLogging verbosity

Limitations

  • Reasoning summaries — The reasoning field is stripped from requests since GLM models don't support it
  • Built-in tools — OpenAI-specific tools (web_search, file_search, code_interpreter) are not forwarded
  • previous_response_id — Not supported; full conversation history is sent each turn (same as Codex's HTTP transport behavior)
  • Model-specific features — Any feature requiring OpenAI server-side infrastructure won't work

Rebuilding After Changes

podman build -t codex-zai-proxy .
systemctl --user restart codex-zai-proxy

License

MIT

About

Localhost reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

codex-zai-proxy

A localhost-only reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API. Translates the Responses API wire format (which Codex CLI requires) into Chat Completions API requests (which Z.ai accepts), handling the full request/response lifecycle including streaming SSE and function/tool call translation.

Why

Codex CLI (@openai/codex) removed support for wire_api = "chat" and now requires wire_api = "responses". Z.ai's coding endpoint speaks the Chat Completions protocol. This proxy sits between them, translating in both directions so Codex works transparently with a Z.ai subscription.

Architecture

 Codex CLI Localhost Proxy Z.ai API
───────── ─────────────── ────────
POST /v1/responses ──► Request translation ──► POST /chat/completions
(Responses API format) • instructions → system msg (Chat Completions format)
• input items → messages
• tool format conversion
• role normalization
SSE stream (Responses) ◄── Response translation ◄── SSE stream (Chat Completions)
• chat chunks → response events
• tool_call deltas tracked
• proper SSE event sequencing

Requirements

  • Podman (rootless)
  • Codex CLI (npm install -g @openai/codex)
  • Z.ai GLM Coding Plan subscription with API key

Quick Start

# Clone
git clone https://github.com/YOUR_USERNAME/codex-zai-proxy.git
cd codex-zai-proxy
# Configure your API key
cp .env.example .env
# Edit .env and set ZAI_API_KEY=your-key-here
chmod 600 .env
# Build and start
podman build -t codex-zai-proxy .
podman run -d \
--name codex-zai-proxy \
--env-file .env \
--publish 127.0.0.1:4891:4891 \
--restart unless-stopped \
--security-opt no-new-privileges \
--cap-drop ALL \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
codex-zai-proxy
# Verify
curl http://127.0.0.1:4891/health

Codex Configuration

Add this to ~/.codex/config.toml:

profile = "glm_proxy"
[model_providers.z_ai_proxy]
name = "z.ai via Local Proxy"base_url = "http://127.0.0.1:4891/v1"env_key = "ZAI_API_KEY"wire_api = "responses"
[profiles.glm_proxy]
model = "glm-5.1"model_provider = "z_ai_proxy"

To switch models, change the model value in your profile (e.g. "glm-5", "glm-4.7"). The proxy passes through whatever model Codex requests — no proxy restart needed. Just start a new Codex session.

Make sure ZAI_API_KEY is set in your shell environment, then run:

codex

Translation Details

Request (Responses → Chat Completions)

Responses APIChat Completions
instructions fieldsystem role message
input[] with type: "message"messages[] with role + content
input[] with type: "function_call"Assistant message with tool_calls[]
input[] with type: "function_call_output"tool role message
input[] with type: "local_shell_call"Mapped to function tool_call
tools[] with {type, name, parameters}tools[] with {type: "function", function: {name, parameters}}
role: "developer"role: "system"

Response (Chat Completions SSE → Responses API SSE)

The proxy consumes the upstream Chat Completions stream and emits properly sequenced Responses API events:

  1. response.created — emitted immediately
  2. response.output_item.added — when text or tool output begins
  3. response.output_text.delta — each text chunk from upstream
  4. response.output_item.done — completed text message or function call
  5. response.completed — final event with usage stats

Tool call arguments are accumulated across multiple deltas and emitted as a complete function_call output item.

Endpoints

MethodPathDescription
GET/healthHealth check, returns upstream URL
POST/v1/responsesMain proxy endpoint (Codex hits this)
GET/v1/modelsMinimal models endpoint for compatibility

Persistent Service (systemd)

For auto-start on login, use the included Quadlet:

# Copy Quadlet file
mkdir -p ~/.config/containers/systemd
cp codex-zai-proxy.container ~/.config/containers/systemd/
# Generate and enable
/usr/libexec/podman/quadlet --user ~/.config/systemd/user/generated
cp ~/.config/systemd/user/generated/codex-zai-proxy.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now codex-zai-proxy

Management:

systemctl --user start codex-zai-proxy
systemctl --user stop codex-zai-proxy
systemctl --user restart codex-zai-proxy
systemctl --user status codex-zai-proxy
journalctl --user -u codex-zai-proxy -f

Management Script

./manage.sh build # Build the container image
./manage.sh start # Start container
./manage.sh stop # Stop and remove container
./manage.sh restart # Stop then start
./manage.sh rebuild # Rebuild image and restart
./manage.sh logs # Follow container logs
./manage.sh status # Show container status
./manage.sh test# Health check

Security

  • Binds to 127.0.0.1 only — Podman --publish 127.0.0.1:4891:4891 enforces localhost-only access
  • Non-root process inside container
  • Read-only container filesystem with tmpfs for /tmp
  • All Linux capabilities dropped (--cap-drop ALL)
  • No new privileges security option set
  • API key loaded from .env file (mode 600) at runtime — never baked into the image
  • Upstream errors are sanitized before returning to the client

Configuration

Environment Variables

VariableDefaultDescription
ZAI_API_KEY(required)Your Z.ai API key
ZAI_BASE_URLhttps://api.z.ai/api/coding/paas/v4Z.ai API base URL
PROXY_PORT4891Port to listen on
LOG_LEVELINFOLogging verbosity

Limitations

  • Reasoning summaries — The reasoning field is stripped from requests since GLM models don't support it
  • Built-in tools — OpenAI-specific tools (web_search, file_search, code_interpreter) are not forwarded
  • previous_response_id — Not supported; full conversation history is sent each turn (same as Codex's HTTP transport behavior)
  • Model-specific features — Any feature requiring OpenAI server-side infrastructure won't work

Rebuilding After Changes

podman build -t codex-zai-proxy .
systemctl --user restart codex-zai-proxy

License

MIT

About

Localhost reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

codex-zai-proxy

A localhost-only reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API. Translates the Responses API wire format (which Codex CLI requires) into Chat Completions API requests (which Z.ai accepts), handling the full request/response lifecycle including streaming SSE and function/tool call translation.

Why

Codex CLI (@openai/codex) removed support for wire_api = "chat" and now requires wire_api = "responses". Z.ai's coding endpoint speaks the Chat Completions protocol. This proxy sits between them, translating in both directions so Codex works transparently with a Z.ai subscription.

Architecture

 Codex CLI Localhost Proxy Z.ai API
───────── ─────────────── ────────
POST /v1/responses ──► Request translation ──► POST /chat/completions
(Responses API format) • instructions → system msg (Chat Completions format)
• input items → messages
• tool format conversion
• role normalization
SSE stream (Responses) ◄── Response translation ◄── SSE stream (Chat Completions)
• chat chunks → response events
• tool_call deltas tracked
• proper SSE event sequencing

Requirements

  • Podman (rootless)
  • Codex CLI (npm install -g @openai/codex)
  • Z.ai GLM Coding Plan subscription with API key

Quick Start

# Clone
git clone https://github.com/YOUR_USERNAME/codex-zai-proxy.git
cd codex-zai-proxy
# Configure your API key
cp .env.example .env
# Edit .env and set ZAI_API_KEY=your-key-here
chmod 600 .env
# Build and start
podman build -t codex-zai-proxy .
podman run -d \
--name codex-zai-proxy \
--env-file .env \
--publish 127.0.0.1:4891:4891 \
--restart unless-stopped \
--security-opt no-new-privileges \
--cap-drop ALL \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
codex-zai-proxy
# Verify
curl http://127.0.0.1:4891/health

Codex Configuration

Add this to ~/.codex/config.toml:

profile = "glm_proxy"
[model_providers.z_ai_proxy]
name = "z.ai via Local Proxy"base_url = "http://127.0.0.1:4891/v1"env_key = "ZAI_API_KEY"wire_api = "responses"
[profiles.glm_proxy]
model = "glm-5.1"model_provider = "z_ai_proxy"

To switch models, change the model value in your profile (e.g. "glm-5", "glm-4.7"). The proxy passes through whatever model Codex requests — no proxy restart needed. Just start a new Codex session.

Make sure ZAI_API_KEY is set in your shell environment, then run:

codex

Translation Details

Request (Responses → Chat Completions)

Responses APIChat Completions
instructions fieldsystem role message
input[] with type: "message"messages[] with role + content
input[] with type: "function_call"Assistant message with tool_calls[]
input[] with type: "function_call_output"tool role message
input[] with type: "local_shell_call"Mapped to function tool_call
tools[] with {type, name, parameters}tools[] with {type: "function", function: {name, parameters}}
role: "developer"role: "system"

Response (Chat Completions SSE → Responses API SSE)

The proxy consumes the upstream Chat Completions stream and emits properly sequenced Responses API events:

  1. response.created — emitted immediately
  2. response.output_item.added — when text or tool output begins
  3. response.output_text.delta — each text chunk from upstream
  4. response.output_item.done — completed text message or function call
  5. response.completed — final event with usage stats

Tool call arguments are accumulated across multiple deltas and emitted as a complete function_call output item.

Endpoints

MethodPathDescription
GET/healthHealth check, returns upstream URL
POST/v1/responsesMain proxy endpoint (Codex hits this)
GET/v1/modelsMinimal models endpoint for compatibility

Persistent Service (systemd)

For auto-start on login, use the included Quadlet:

# Copy Quadlet file
mkdir -p ~/.config/containers/systemd
cp codex-zai-proxy.container ~/.config/containers/systemd/
# Generate and enable
/usr/libexec/podman/quadlet --user ~/.config/systemd/user/generated
cp ~/.config/systemd/user/generated/codex-zai-proxy.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now codex-zai-proxy

Management:

systemctl --user start codex-zai-proxy
systemctl --user stop codex-zai-proxy
systemctl --user restart codex-zai-proxy
systemctl --user status codex-zai-proxy
journalctl --user -u codex-zai-proxy -f

Management Script

./manage.sh build # Build the container image
./manage.sh start # Start container
./manage.sh stop # Stop and remove container
./manage.sh restart # Stop then start
./manage.sh rebuild # Rebuild image and restart
./manage.sh logs # Follow container logs
./manage.sh status # Show container status
./manage.sh test# Health check

Security

  • Binds to 127.0.0.1 only — Podman --publish 127.0.0.1:4891:4891 enforces localhost-only access
  • Non-root process inside container
  • Read-only container filesystem with tmpfs for /tmp
  • All Linux capabilities dropped (--cap-drop ALL)
  • No new privileges security option set
  • API key loaded from .env file (mode 600) at runtime — never baked into the image
  • Upstream errors are sanitized before returning to the client

Configuration

Environment Variables

VariableDefaultDescription
ZAI_API_KEY(required)Your Z.ai API key
ZAI_BASE_URLhttps://api.z.ai/api/coding/paas/v4Z.ai API base URL
PROXY_PORT4891Port to listen on
LOG_LEVELINFOLogging verbosity

Limitations

  • Reasoning summaries — The reasoning field is stripped from requests since GLM models don't support it
  • Built-in tools — OpenAI-specific tools (web_search, file_search, code_interpreter) are not forwarded
  • previous_response_id — Not supported; full conversation history is sent each turn (same as Codex's HTTP transport behavior)
  • Model-specific features — Any feature requiring OpenAI server-side infrastructure won't work

Rebuilding After Changes

podman build -t codex-zai-proxy .
systemctl --user restart codex-zai-proxy

License

MIT

About

Localhost reverse proxy that bridges OpenAI Codex CLI with Z.ai's GLM Coding Plan API

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages