Skip to content

GLM-5.2 with LangChain: Direct Z.ai or ChatOpenRouter?

Independent research — not an official Z.ai publication.Identity and provider disclosure

GLM-5.2 LangChain workflow comparing ChatOpenAI with the direct Z.ai endpoint and ChatOpenRouter with a provider policy and fallback routes

Original route diagram based on the dated configurations and observations in this article. The sanitized result image below is generated from the archived Docker evidence.

LangChain can reach GLM-5.2 in two useful ways. A direct Z.ai profile keeps the publisher’s endpoint, model name and account boundary together. OpenRouter’s dedicated integration adds provider selection, fallback and one gateway model namespace. The second path is more flexible, but the endpoint behind the slug can change.

That distinction became operational in our July 29, 2026 test. The direct adapter returned the requested six-token marker. The gateway adapter produced a correct tool call, but a deliberately small text budget exposed a reasoning serialization edge. OpenRouter’s public catalog also contained 33 GLM-5.2 endpoints with materially different context, quantization, prices and parameter support. A working import therefore proves very little by itself.

  1. Match adapter and route
  2. Rebuild the environment
  3. Configure Z.ai direct
  4. Configure ChatOpenRouter
  5. Read the live results
  6. Control reasoning safely
  7. Validate a tool call
  8. Constrain gateway routes
  9. Compare one workload cost
  10. Diagnose contract failures
  11. Require acceptance receipts
  12. Choose the workload path
  13. Review evidence and sources

Start with the account product and data route, not the class name:

Decision Z.ai direct OpenRouter gateway
Python adapter tested langchain-openai==1.4.1 langchain-openrouter==0.2.7
Class ChatOpenAI ChatOpenRouter
Base URL https://api.z.ai/api/paas/v4 Dedicated package default: https://openrouter.ai/api/v1
Model ID glm-5.2 z-ai/glm-5.2
Environment variable ZAI_API_KEY OPENROUTER_API_KEY
Provider selection Z.ai Policy-controlled gateway endpoint
Fallback Application-owned Optional gateway fallback
Best fit First-party behavior and one processor path Multi-provider routing and quick model switching

The official Z.ai API introduction documents an OpenAI-compatible interface, which is why ChatOpenAI is the appropriate direct adapter. The current LangChain OpenRouter integration uses a dedicated package instead of treating the gateway as an arbitrary OpenAI base URL.

Do not mix the identifiers. Sending z-ai/glm-5.2 to Z.ai direct can produce a model lookup error; sending bare glm-5.2 to the gateway loses the publisher namespace. A 401 can likewise mean that a valid key belongs to the other account surface.

The final test used Python 3.13.13 in a digest-pinned Docker image:

Observed package set
langchain-core 1.5.2
langchain-openai 1.4.1
langchain-openrouter 0.2.7
openai 2.50.0
pydantic 2.12.5

Install only the route or routes the application needs:

Create a clean project environment
python -m venv .venv
. .venv/bin/activate
python -m pip install \
"langchain-openai==1.4.1" \
"langchain-openrouter==0.2.7"
python -m pip check

The top-level langchain package was not required for these chat integrations and was not installed in our container. Pinning the provider packages matters more than copying a generic pip install langchain line: adapter fields and their underlying SDKs change independently.

PyPI listed langchain-openrouter 0.2.7 for Python 3.10 through 3.x, with its wheel uploaded July 22. OpenRouter published its current ChatOpenRouter setup tutorial on July 29. That date is a tutorial publication date, not evidence that the Python package first launched that day.

The Docker bridge on this host could not complete its bounded DNS preflight, so the trusted digest-pinned container used host networking with no published port or listener. The OpenRouter wire capture used --network none. This is a host-specific test accommodation, not a recommendation to give arbitrary containers host networking.

Keep the secret in the environment:

Load a first-party API key without printing it
export ZAI_API_KEY="your-zai-api-key"
test -n "$ZAI_API_KEY" && echo "ZAI_API_KEY is set"

Then instantiate the adapter with the first-party base URL and bare model ID:

direct_zai.py
import os
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="glm-5.2",
api_key=os.environ["ZAI_API_KEY"],
base_url="https://api.z.ai/api/paas/v4",
temperature=0,
max_tokens=64,
max_retries=0,
timeout=90,
extra_body={"thinking": {"type": "disabled"}},
)
response = llm.invoke(
"Return exactly LANGCHAIN_ZAI_OK and no other text."
)
assert response.content.strip() == "LANGCHAIN_ZAI_OK"
print(response.content)

Our live result was:

Sanitized Z.ai response receipt
{
"status": "success",
"model": "glm-5.2",
"elapsed_ms": 6319,
"content": "LANGCHAIN_ZAI_OK",
"finish_reason": "stop",
"usage": {
"input_tokens": 18,
"output_tokens": 6,
"total_tokens": 24,
"reasoning_tokens": 0
}
}

The observation proves that this exact package, endpoint, model and configuration completed one request on the test date. It is not a latency percentile or service-level claim. For a complete direct function cycle, including application execution and a second model turn, use the separate GLM-5.2 tool-calling loop.

OpenRouter needs its publisher-qualified model slug and its own secret:

Load the gateway key
export OPENROUTER_API_KEY="your-openrouter-api-key"
test -n "$OPENROUTER_API_KEY" && echo "OPENROUTER_API_KEY is set"

This profile constrains routing to endpoints that accept the requested parameters, denies routes that collect prompts where the provider metadata supports that filter, prefers lower latency and allows fallback:

openrouter_glm.py
import os
from langchain_openrouter import ChatOpenRouter
llm = ChatOpenRouter(
model="z-ai/glm-5.2",
api_key=os.environ["OPENROUTER_API_KEY"],
temperature=0,
max_tokens=1024,
max_retries=0,
timeout=45_000,
openrouter_provider={
"allow_fallbacks": True,
"require_parameters": True,
"data_collection": "deny",
"sort": "latency",
},
)
response = llm.invoke("Name the model in one short sentence.")
if not response.content:
raise RuntimeError(
f"No final text; finish={response.response_metadata.get('finish_reason')}"
)
print(response.content)

The 45_000 value is intentionally in milliseconds for the tested 0.2.7 path. Its source passes the field to the underlying OpenRouter SDK as timeout_ms. Recheck the signature and source after an upgrade instead of assuming that every LangChain adapter uses the same unit.

The provider object is a routing policy, not a provider pin. sort: "latency" can choose a different eligible backend on a later call. Use the current OpenRouter GLM-5.2 endpoint view and stronger provider constraints when reproducibility, region, quantization or a one-million-token window is contractual.

Sanitized Docker results showing exact Z.ai and OpenRouter controls, a ChatOpenRouter output-cap failure, a valid strict tool call and a 33-endpoint capability snapshot

Actual evidence view generated from the sanitized archive. It is not a Z.ai, OpenRouter or LangChain dashboard and contains no API key, Authorization header, cookie, response ID, tool-call ID, model reasoning or account metadata.

Observation Time Finish Measured outcome
ChatOpenAI → Z.ai 6.319 s stop Exact six-token marker; 24 total tokens
Raw OpenRouter control 1.627 s stop Exact marker; CoreWeave selected; provider-reported cost $0.0000282
ChatOpenRouter text 4.577 s length Empty final text; 120 reasoning tokens inside a 128-token output cap
ChatOpenRouter strict tool 11.894 s tool_calls Correct LookupRepo arguments; 268 total tokens

These are four different acceptance observations, not a fair speed race. The raw gateway control disabled reasoning successfully. The dedicated adapter text control did not. The tool prompt also had more input and performed a different task. Comparing their elapsed values as a provider leaderboard would be misleading.

The raw request is a control rather than the recommended application adapter. It proves that the key, gateway model slug and selected route could produce an exact marker. The LangChain tool response proves that the dedicated package reached the gateway and parsed a real tool-call object.

Treat reasoning controls as serialized data

Section titled “Treat reasoning controls as serialized data”

The first dedicated-adapter profile used:

reasoning={"enabled": False}

A network-disabled loopback server captured the final JSON body. The underlying SDK serialized that input as:

{"reasoning": {}}

The live model then spent 120 of 128 output tokens on reasoning, stopped for length, and emitted no final text. This is why a completed HTTP call can still fail an application acceptance check.

In the tested version, this alternative survived serialization:

reasoning={"effort": "none"}
{"reasoning": {"effort": "none"}}

We verified that wire shape without network, but deliberately did not send a further paid call after finding it. OpenRouter’s endpoint snapshot reported reasoning_effort support on all 33 routes on the test date; support metadata is still a changing provider claim. A conservative production sequence is:

  1. leave enough max_tokens for reasoning plus visible output;
  2. capture a sanitized outgoing body in a non-production test;
  3. run an exact-marker acceptance call;
  4. require non-empty content and inspect finish_reason;
  5. record reasoning-token usage when the adapter exposes it.

Use the GLM-5.2 reasoning-effort map for task selection. Do not treat a Python dictionary as proof that the intended wire field survived the adapter and SDK.

Validate the exported tool name and arguments

Section titled “Validate the exported tool name and arguments”

Pydantic makes a compact strict schema:

Strict Pydantic tool
from pydantic import BaseModel, Field
class LookupRepo(BaseModel):
"""Read a named path from a public repository."""
repository: str = Field(description="Owner and repository name")
path: str = Field(description="Repository-relative path")
tool_llm = llm.bind_tools([LookupRepo], strict=True)
answer = tool_llm.invoke(
"Call LookupRepo exactly once for repository zai-org/GLM-5 "
"and path README.md. Do not answer in prose."
)
assert answer.response_metadata["finish_reason"] == "tool_calls"
assert len(answer.tool_calls) == 1
call = answer.tool_calls[0]
assert call["name"] == "LookupRepo"
assert call["args"] == {
"repository": "zai-org/GLM-5",
"path": "README.md",
}

The captured request declared additionalProperties: false, required both fields and named the function LookupRepo. The live response matched that schema and both requested values.

Our initial probe assertion expected lowercase lookup_repo, so its raw exact_call flag was false. That was a test bug: the class had exported LookupRepo, and the model returned the declared name. The archive preserves the original false flag plus a derived valid_against_exported_schema: true finding. Test against the schema actually sent, not a name remembered from a different framework.

This response did not read GitHub. A complete agent must validate the arguments, authorize and execute the function, append the result as a tool message, call the model again and validate the final answer. Tool selection is one stage of the safe GLM-5.2 function loop, not proof of tool execution.

Section titled “Pin gateway capabilities rather than the logo”

The public OpenRouter endpoint API listed 33 eligible endpoints when checked:

Catalog property Dated count or range
Total endpoints 33
Context minimum / median / maximum 96,890 / 1,048,576 / 1,048,576
Endpoints at or above 1M context 24
Endpoints below 262,144 context 2
tools supported 33
tool_choice supported 32
Structured output supported 26
reasoning_effort supported 33
parallel_tool_calls supported 1
Quantization 9 FP4 / 16 FP8 / 8 unspecified

That table explains several “works locally, fails in production” cases. A request that needs tools can route widely; a request that needs parallel tool calls had only one matching endpoint in this snapshot. A one-million-token prompt is not valid merely because the top-level model page displays one million. The selected endpoint owns the actual contract.

Use require_parameters: true to exclude endpoints that do not advertise the requested parameters. Then record the selected provider where the response exposes it. For a strict context, quantization, region or processor contract, pin or order providers rather than using an unconstrained balanced route.

The named Z.ai endpoint in the gateway catalog advertised FP8, 1,048,576 context, 131,072 maximum completion tokens and the same $1.40/M input, $0.26/M cache-read and $4.40/M output list rates as the first-party Z.ai pricing page. Other gateway endpoints had different prices and capabilities.

For 100,000 uncached input tokens and 10,000 output tokens:

Z.ai direct:
100,000 × $1.40 / 1,000,000 = $0.140000
10,000 × $4.40 / 1,000,000 = $0.044000
total $0.184000
OpenRouter catalog rate snapshot:
100,000 × $0.7616 / 1,000,000 = $0.076160
10,000 × $2.3936 / 1,000,000 = $0.023936
total $0.100096

The apparent difference is $0.083904 before account funding fees, cache, retries or routing changes. It is not a like-for-like checkpoint guarantee. The catalog route can choose a different provider or quantization, whereas the named Z.ai gateway endpoint used the first-party rate in this snapshot.

Our tiny raw OpenRouter control reported $0.0000282 for 18 input and six output tokens. That receipt is useful for verifying billing telemetry, not for forecasting an agent workload. Use the GLM-5.2 cost calculator with your actual input, output, cache and retry distribution.

A 401 identifies the wrong account surface

Section titled “A 401 identifies the wrong account surface”

Check the environment-variable name, key source and base URL together. ZAI_API_KEY belongs with api.z.ai; OPENROUTER_API_KEY belongs with openrouter.ai. Do not print the key to prove it exists. Check only that the variable is non-empty, then run a minimal request. A valid Coding Plan key is also not automatically general pay-as-you-go API credit.

A model lookup error often exposes namespace mismatch

Section titled “A model lookup error often exposes namespace mismatch”

Use glm-5.2 on Z.ai direct and z-ai/glm-5.2 on OpenRouter. Log the model string and base host, not credentials. If a gateway catalog no longer lists the slug, stop instead of silently substituting a nearby model.

A context error belongs to the selected endpoint

Section titled “A context error belongs to the selected endpoint”

Count input plus requested completion budget against the actual routed endpoint. In the dated catalog, endpoint context ranged from 96,890 to 1,048,576 tokens. Provider fallback can move a request to another contract. Require the needed context and leave room for tool results and the final answer.

A tool failure can be schema, route or loop

Section titled “A tool failure can be schema, route or loop”

Inspect four checkpoints separately:

  1. the outgoing tools array names the intended function;
  2. the route advertises tools, tool_choice or parallel calls as required;
  3. the response finishes with a tool call and its JSON validates;
  4. the application executes the authorized function and returns a tool message for a final model turn.

The tested OpenRouter route completed checkpoint three. It did not execute a repository reader.

A silent final answer may be a reasoning budget failure

Section titled “A silent final answer may be a reasoning budget failure”

Require non-empty response.content, inspect finish_reason, and read reasoning-token details when present. Our empty answer was not a network timeout: it ended normally with length after consuming the cap. Raise the budget or use a verified reasoning control rather than retrying the same request blindly.

A local fixture can violate a new SDK contract

Section titled “A local fixture can violate a new SDK contract”

Our first network-disabled mock response omitted system_fingerprint. langchain-openrouter 0.2.7 raised ResponseValidationError even though many OpenAI-compatible fixtures omit that field. Adding an explicit null value made the fixture validate. When an upgrade breaks tests before any network call, compare the SDK response schema before blaming the model endpoint.

Promote the integration with acceptance receipts

Section titled “Promote the integration with acceptance receipts”

A production gate should record behavior, not merely config:

Bounded acceptance checks
text = llm.invoke("Return exactly GLM52_ACCEPTED.")
assert text.response_metadata.get("finish_reason") == "stop"
assert text.content.strip() == "GLM52_ACCEPTED."
tool = llm.bind_tools([LookupRepo], strict=True).invoke(
"Call LookupRepo for repository zai-org/GLM-5 and path README.md."
)
assert tool.response_metadata.get("finish_reason") == "tool_calls"
assert tool.tool_calls[0]["name"] == "LookupRepo"
LookupRepo.model_validate(tool.tool_calls[0]["args"])

Store package versions, route, model, sanitized timing, token usage, finish reason and schema-validation result. Do not store secrets, Authorization headers, cookies, raw confidential prompts, response IDs or model reasoning. Add a budget ceiling and fail closed if the response model or selected provider violates policy.

Run this canary after adapter upgrades and before a major route change. A constructor test catches syntax errors. An exact marker catches missing final text. A strict tool test catches name and schema drift. A full tool loop catches application execution and message-order problems.

Choose Z.ai direct when:

  • first-party model behavior and documentation are the primary contract;
  • one provider and processor path simplify review;
  • the application owns failover and does not need gateway model switching;
  • direct pricing and account support fit the deployment.

Choose ChatOpenRouter when:

  • multiple eligible providers and fallback reduce integration work;
  • the application benefits from one gateway namespace across models;
  • provider policy filters and routing metadata are operational requirements;
  • the team can test route-dependent context, quantization and parameters.

Choose neither merely because a snippet imports. Compare current privacy, rate-limit and reliability terms in the broader GLM-5.2 API provider guide. Use the OpenCode-specific configuration for an interactive coding agent; LangChain is an application framework, not a drop-in replacement for that client.

The sanitized machine-readable result preserves the acceptance outcomes and endpoint counts without credentials or response identifiers. The repository archive also contains the probe, package pins, full sanitized evidence, wire fixture and image sources.

Sources checked July 29, 2026:

OpenRouter’s tutorial and catalog are vendor sources. The live responses, wire capture and arithmetic are original dated observations. Prices, routes, package behavior, context limits and parameter support can change; recheck the linked first-party pages and rerun the acceptance sequence before a production decision.