NativePort
← How-to

How to Use GLM-5.2 Through an OpenAI-Compatible API in Python

Call Z.ai's GLM-5.2 — a 744B-parameter, 40B-active MoE model with a 1,048,576-token context window — from Python using the openai package pointed at NativePort's OpenAI-compatible chat-completions endpoint: model discovery, streaming, tool calls, provider pinning, and cost math.

GLM-5.2, Z.ai’s 744-billion-parameter mixture-of-experts model (40B active per token, MIT-licensed), is cataloged on NativePort’s Hugging Face route as huggingface/zai-org/GLM-5.2, pinned by default to the novita serving backend with deepinfra admitted as an explicit alternative. Like every model on this route, it speaks OpenAI’s chat-completions contract, so the standard openai Python package works unmodified once its base_url points at the gateway, with no Z.ai SDK and no Hugging Face token required. This tutorial covers discovering what the route actually supports before you build against it, a basic call, streaming, provider pinning, tool calls, why reasoning_effort doesn’t work here yet, the errors this setup produces, and the arithmetic behind what a call costs.

What you’ll need

  • A NativePort API key (sign up; $5 of credit is seeded automatically).
  • pip install openai: used purely as an HTTP client here; no OpenAI account or key involved.
  • Your key exported as an environment variable, never hardcoded:
export NATIVEPORT_API_KEY="np_..."

Two ways to reach the same model

  • Unified endpoint (this tutorial): POST https://api.nativeport.ai/inference/v1/chat/completions, model huggingface/zai-org/GLM-5.2. This is the canonical id. The same request shape works for OpenAI, Anthropic, Grok and Hugging Face models by swapping the model string, and only the first / after the provider prefix is significant, so everything after huggingface/ is Hugging Face’s own model id, colon-pinned provider suffix included.
  • Direct Hugging Face passthrough: POST https://api.nativeport.ai/huggingface/v1/chat/completions, model zai-org/GLM-5.2 (bare, resolves to the cataloged default, novita) or zai-org/GLM-5.2:deepinfra (pinned explicitly). Use this if you want Hugging Face’s own response shape untouched by the unified layer.

The examples below use the unified endpoint, since that’s what makes the openai SDK usable as-is.

A basic call

import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["NATIVEPORT_API_KEY"],
    base_url="https://api.nativeport.ai/inference/v1",
)

response = client.chat.completions.create(
    model="huggingface/zai-org/GLM-5.2",
    messages=[{"role": "user", "content": "In one sentence, what does a 1M-token context window let you do that a 128K one doesn't?"}],
    max_tokens=300,
)
print(response.choices[0].message.content)

The response is shaped exactly like chat.completions.create returns for any OpenAI-compatible model on this gateway: choices[0].message.content, usage.prompt_tokens/completion_tokens/total_tokens, finish_reason.

Set max_tokens explicitly, and set it high enough. GLM-5.2 reasons by default before it answers, and that reasoning consumes the same token budget as the visible reply. This failure mode is real and easy to hit: a bare call capped at max_tokens=16 returns 200, but generation ends while still inside the model’s reasoning_content, leaving content empty. Raising the cap to max_tokens=300 on the same request is enough to complete normally. There’s no per-request way to shorten or skip the reasoning phase (see reasoning_effort below), so a small max_tokens doesn’t get you a short reply, it gets you no reply. 300 is a reasonable floor for short answers; budget higher for anything that needs a longer response on top of the reasoning.

Check what the model supports before you rely on it

Ask the gateway directly rather than assume:

import os
import urllib.request
import json

req = urllib.request.Request(
    "https://api.nativeport.ai/inference/v1/models/huggingface/zai-org/GLM-5.2"
)
req.add_header("Authorization", f"Bearer {os.environ['NATIVEPORT_API_KEY']}")
with urllib.request.urlopen(req, timeout=30) as resp:
    print(json.dumps(json.loads(resp.read()), indent=2))

This returns a capabilities object, a supported_parameters array, and pricing. For GLM-5.2, capabilities reports streaming: true, tools: true, and tool_choice: true, the same three fields the unified endpoint reports true for every model on the route, since they’re a route-level guarantee rather than something looked up per model. vision reports false: unlike some other models on the Hugging Face route, GLM-5.2 is not flagged vision-capable here, so don’t send image_url content parts to it through NativePort. The gateway rejects that structurally with 400 unsupported_parameter regardless of what any particular backend might otherwise accept.

supported_parameters for this model, as reported by the gateway, is exactly: max_tokens, max_completion_tokens, temperature, top_p, stop, stream, stream_options, tools, tool_choice, parallel_tool_calls. Two fields that show up for other kinds of models are absent from that list: response_format (structured output / JSON mode) and reasoning_effort. Both are outside the unified /inference API’s common field set by design (the endpoint only admits the parameter intersection every backend provider on the route can express), not a GLM-5.2-specific gap. That absence doesn’t mean GLM-5.2 doesn’t reason. It does, by default, on every request, as the reasoning_content behavior above shows; it just means you have no server-accepted way to dial that reasoning up or down. GLM-5.2 upstream ships “multiple thinking effort levels,” but that control isn’t exposed through this route today, and NativePort’s endpoint rejects reasoning_effort outright rather than silently ignoring it. If your workload depends on schema-constrained output, use a forced tool call instead (below).

Streaming

stream = client.chat.completions.create(
    model="huggingface/zai-org/GLM-5.2",
    messages=[{"role": "user", "content": "List three uses for a long context window, one line each."}],
    stream=True,
)
for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)
print()

capabilities.streaming: true applies here as it does for the whole route. Server-sent-events chunks arrive in the standard OpenAI delta shape, terminated by a final chunk with finish_reason set and a [DONE] marker, same as any other model behind this gateway.

Provider pinning and pricing

GLM-5.2 is admitted on two backing providers, each with its own real, pass-through rate, with no NativePort markup on either:

  • novita (default): $1.40 / 1M input, $4.40 / 1M output, 1,048,576 context.
  • deepinfra: $0.93 / 1M input, $3.00 / 1M output, 1,048,576 context.

A bare huggingface/zai-org/GLM-5.2 resolves to the default, novita. To pin deepinfra explicitly (worthwhile here, since it’s meaningfully cheaper on both input and output at the same context length), append :deepinfra to the Hugging Face model id, after the huggingface/ prefix:

response = client.chat.completions.create(
    model="huggingface/zai-org/GLM-5.2:deepinfra",
    messages=[{"role": "user", "content": "Summarize the tradeoffs of pinning a serving provider."}],
)

Only novita and deepinfra are cataloged for this model, so pinning any other provider string fails, the same way requesting an uncataloged model id would. Both routes work as described: a bare default call (resolving to novita) and an explicit :deepinfra pin both return 200, and usage on both is billed at the published rates above.

Cataloging novita and deepinfra at the same price-and-context grain doesn’t mean the two behave identically end to end: that’s a pricing/context parity, not a behavioral guarantee. The max_tokens/reasoning_content truncation behavior described above, for instance, is documented against the default novita pin specifically; it isn’t confirmed on deepinfra. If you pin deepinfra for a workload where that edge case matters, confirm the token-budget behavior yourself rather than assuming parity with novita.

Tool calls

GLM-5.2’s catalog entry reports capabilities.tools: true and tool_choice: true, and the gateway forwards tool_choice: "required" to the backing provider unchanged, so a forced call is structurally available the same way it is for every tool-capable model on the route:

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Look up the current weather for a city.",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    },
}]

response = client.chat.completions.create(
    model="huggingface/zai-org/GLM-5.2",
    messages=[{"role": "user", "content": "What's the weather in Paris?"}],
    tools=tools,
    tool_choice="required",
)
call = response.choices[0].message.tool_calls[0]
print(call.function.name, call.function.arguments)

tools must be type: "function". The gateway rejects other tool types before the request reaches novita or deepinfra. This exact shape works as expected: a forced tool_choice="required" call against GLM-5.2 returns 200 with a valid get_weather call and Paris in the arguments.

Errors you’ll actually hit

import openai

try:
    response = client.chat.completions.create(
        model="huggingface/zai-org/GLM-5.2",
        messages=[{"role": "user", "content": "Hello"}],
    )
except openai.AuthenticationError:
    raise SystemExit("401 — NATIVEPORT_API_KEY is missing, malformed, or revoked.")
except openai.PermissionDeniedError:
    raise SystemExit("403 — the account behind this key isn't active (e.g. suspended).")
except openai.NotFoundError:
    raise SystemExit("404 — model_not_found: check the model id for typos.")
except openai.RateLimitError:
    raise SystemExit("429 — gateway_rate_limited: back off and retry.")
except openai.BadRequestError as e:
    raise SystemExit(f"400 — {e.body.get('code', 'invalid request')}: {e.message}")
except openai.APIStatusError as e:
    if e.status_code == 402:
        raise SystemExit("402 — NativePort balance is $0. Every request fails closed instead of degrading silently; top up and retry.")
    raise

Specifics worth knowing:

  • 401 comes back as {"error": "Unauthorized."}, a plain string, not the nested error.code object the 400/402/429 cases use.
  • 403 is also a plain string body, an account-status problem, not a credentials problem, so retrying with the same key won’t help.
  • 400 unsupported_parameter is what you’ll see for response_format or reasoning_effort, and for image_url content sent to this model. All three are rejected before the request leaves the gateway.
  • 404 model_not_found: GLM-5.2’s catalog entry is recent. Double-check huggingface/zai-org/GLM-5.2 for typos, or the :deepinfra suffix if you’re pinning.
  • 502 means the upstream provider itself was unreachable, a Novita or DeepInfra problem (whichever you’re pinned to), not a NativePort or GLM-5.2 problem specifically.

What this costs

Both providers meter against the same 1,048,576-token context window, so that specific dimension is a fair price comparison, even though (as noted above) it isn’t a guarantee the two behave identically elsewhere. Worked example: a call with 50,000 input tokens and 2,000 output tokens:

  • novita (default): 50,000 × $1.40/1M = $0.0700 input, 2,000 × $4.40/1M = $0.0088 output → $0.0788 total.
  • deepinfra (pinned): 50,000 × $0.93/1M = $0.0465 input, 2,000 × $3.00/1M = $0.0060 output → $0.0525 total, about a third cheaper for this mix.

Check usage.prompt_tokens and usage.completion_tokens on the response to run this math against your own workload rather than an estimate. Output tokens dominate the bill for most chat-style traffic, so the output-price gap between the two providers usually matters more than the input-price gap. Sign-up seeds $5 of credit automatically, and a zero balance fails every request with 402 rather than degrading output quality. Adding credit is the only place a fee applies: 5.5%, on top-ups of $10 to $5,000, never on the calls themselves. Full breakdown: pricing.

Where to go next