Skip to content

Z.AI provider is pinned to Chat Completions although the same key serves the Responses protocol #4297

Description

@lidge-jun

Client or integration

Codex CLI

Provider or upstream service

zai (Z.AI GLM Coding Plan)

OpenCodex version

2.50.0 (installed), dev at 29d632f

Endpoint or capability

/v1/responses upstream wire selection

Current behaviour

The zai row is pinned to Chat Completions:

id: "zai", label: "Z.AI - GLM Coding Plan", baseUrl: "https://api.z.ai/api/coding/paas/v4", adapter: "openai-chat", authKind: "key",

Because the upstream wire is chosen by the routed provider's adapter rather than by the inbound route (src/server/adapter-resolve.ts), every request to a GLM model leaves over Chat Completions no matter which surface the client used. In practice that path is the unstable one.

Z.AI serves the same subscription and the same key over three protocols, and the Responses endpoint is the one its own documentation points Codex-family clients at. OpenCodex already ships a Responses row for the domestic portal (zhipu-bigmodel-responses on open.bigmodel.cn/api/v1) but has no equivalent for api.z.ai.

Expected behaviour

zai should default to the Responses protocol on https://api.z.ai/api/v1, with Chat Completions still reachable as an explicit per-model opt-in.

Switching costs no models. Every id in the current roster answers on the Responses endpoint (measured against a live key, 2026-09-12):

POST /api/v1/responses  glm-5.3        -> 200
POST /api/v1/responses  glm-5.3-flash  -> 200
POST /api/v1/responses  glm-5.2        -> 200
POST /api/v1/responses  glm-5.1        -> 200
POST /api/v1/responses  glm-5          -> 200
POST /api/v1/responses  glm-4.6        -> 200
POST /api/v1/responses  glm-5-turbo    -> 200

GET /api/v1/models also returns a Codex-shaped catalog carrying input_modalities, context_window and supported_reasoning_levels, which is strictly richer than what the Chat endpoint's /models exposes.

Minimal redacted request or reproduction

curl -s -o /dev/null -w '%{http_code}\n' https://api.z.ai/api/v1/responses \
  -H "Authorization: Bearer <redacted>" -H 'Content-Type: application/json' \
  -d '{"model":"glm-5.3","input":"hi","max_output_tokens":16}'

curl -s -o /dev/null -w '%{http_code}\n' https://api.z.ai/api/v1/chat/completions \
  -H "Authorization: Bearer <redacted>" -H 'Content-Type: application/json' \
  -d '{"model":"glm-5.3","messages":[{"role":"user","content":"hi"}],"max_tokens":8}'

Actual response or error

200   # /api/v1/responses
403   # /api/v1/chat/completions -> {"error":{"code":"model_access_denied", ...}}
200   # /api/coding/paas/v4/chat/completions

The two wires live under different path prefixes. That matters for the fix: swapping the adapter alone cannot reach the right endpoint, because resolveWireProtocolOverride changes provider.adapter and leaves provider.baseUrl untouched.

Upstream documentation

https://docs.z.ai/guides/llm/glm-5.3

| Protocol | Base URL |
| OpenAI Chat Completion Protocol | https://api.z.ai/api/coding/paas/v4 |
| OpenAI Response Protocol | https://api.z.ai/api/v1 |
| Anthropic Message Protocol | https://api.z.ai/api/anthropic |

https://docs.z.ai/devpack/latest-model

Claude Code / Goose (Anthropic-compatible): https://api.z.ai/api/anthropic
Codex: https://api.z.ai/api/v1
Other OpenAI-compatible tools: https://api.z.ai/api/coding/paas/v4

The same page also warns that a previously subscribed Coding Plan key may be limited to the Chat protocol. That did not reproduce on the key measured above, but it is the reason Chat has to stay reachable rather than be removed.

Suggested mapping or implementation notes

Add chatCompletionsPath as the mirror of the existing responsesPath, then point one row at both endpoints:

baseUrl: "https://api.z.ai",
adapter: "openai-responses",
responsesPath: "/api/v1/responses",
chatCompletionsPath: "/api/coding/paas/v4/chat/completions",

Chat then becomes the existing per-model opt-in: a user writes modelAdapters with openai-chat, resolveWireProtocolOverride swaps the adapter, and the new path field sends it to the coding prefix. A model that Chat refuses is pinned to Responses through modelWireDefaults, the same mechanism the xAI row uses for grok-4.20-multi-agent.

One dependency worth flagging: modelSuffixBracketStrip is honoured only by the openai-chat and ollama-native adapters today. The roster contains glm-5.3[1m] and glm-5.2[1m], and the bracketed id returns 400 model_not_found upstream, so the strip has to be applied on the Responses path as well before the default flips.

Additional context and attachments

The free-directory glm id is separate and stays on the Chat endpoint. Existing zai users change protocol silently, so this needs a release note plus the modelAdapters opt-in instructions for anyone on a Chat-only key.

Checks

  • I searched existing provider and compatibility issues.
  • The request and response were redacted.
  • The expected behaviour is based on an upstream specification or a concrete client requirement.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    providerProvider adapters, OpenAI-compat presets, upstream API quirksprovider-compatibilityProvider compatibility reports

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions