Library Guide
pkg/client is a standalone Go library — everything the CLI does, it does by
calling this package. You can depend on it directly without the CLI at all.
Contents
- Creating a client
- Services
- Chat completions
- Anthropic-compatible Messages API
- Error handling
- Observability hooks
- Multi-account credential management
- Testing your own code against this client
- Architecture notes
go get github.com/SamyRai/go-z-ai
import "github.com/SamyRai/go-z-ai/pkg/client"
Creating a client
c, err := client.NewClient(client.Config{
APIKey: os.Getenv("ZAI_API_KEY"),
})
Or, if you just want the env-var default with no other config:
c, err := client.NewClientFromEnv() // reads ZAI_API_KEY, ZAI_API_BASE_URL
Config fields:
| Field | Default | Notes |
|---|---|---|
APIKey |
— | Required |
BaseURL |
https://api.z.ai/api/paas/v4 |
Override for the coding-plan endpoint, etc. |
HTTPClient |
an internally configured *http.Client |
Bring your own transport if you need custom TLS/proxy behavior |
Timeout |
30s | Bounds dial/TLS/response-header wait — not the whole response body read, so it never truncates a live SSE stream |
MaxRetries |
3 | Retries on 429/5xx/network errors. -1 disables retries entirely |
RetryDelay |
200ms | Base exponential-backoff delay |
ChinaAPIKey |
falls back to APIKey |
Only needed if you hold a separate bigmodel.cn-only credential — see Accounts & Quota |
Region |
RegionGlobal |
Selects the host for monitor/biz/agents/detection: RegionGlobal (api.z.ai) or RegionChina (open.bigmodel.cn). Does not override BaseURL (chat surface) or the Embeddings/Moderations host. See Accounts & Quota. |
UserAgent |
"go-z-ai/<version>" |
Overrides the User-Agent header sent on every request. The default identifies go-z-ai to Z.AI's API — important under the coding endpoint's usage policy. Override only when you need a distinct identifier (a downstream app, a proxy, an MCP server); the override string is sent verbatim. |
Hooks |
nil |
Observability hooks (Hook) that fire on every request/response/error/stream-chunk. Empty by default — the no-hook path is zero-cost. Concrete implementations live in pkg/observe (OpenTelemetry). |
Every service method takes context.Context as its first argument and
propagates it all the way to the HTTP call — cancel it to abort a request or
a pending retry backoff.
Services
Client exposes one method per service, all following the same
c.<Service>().<Method>(ctx, ...) shape:
| Accessor | Covers |
|---|---|
c.Chat() |
Completions — Create, CreateAsync, CreateStream, CreateSimple, RunWithTools |
c.Models() |
List, Get, GetTextModels, GetVisionModels, GetFreeModels, RefreshCache |
c.Images() |
Generate, GenerateAsync |
c.Videos() |
Generate (always async) |
c.Audio() |
Transcribe, Speech |
c.Voice() |
Clone, Delete, List — GLM-TTS voice cloning |
c.Layout() |
Parse, HandwritingOCR |
c.FileParser() |
Create, Sync, Result — document-to-text for RAG |
c.Files() |
Upload, List, Delete, Content |
c.Batch() |
Create, Retrieve, List, Cancel |
c.Agents() |
Invoke, AsyncResult |
c.Anthropic() |
Create, CreateStream — Anthropic-protocol /v1/messages surface |
c.Embeddings() |
Create (routes to open.bigmodel.cn) |
c.Moderations() |
Create (routes to open.bigmodel.cn) |
c.Rerank() |
Create |
c.Tools() |
WebSearch, WebReader, Tokenize |
c.Usage(), c.Quota(), c.Detection(), c.Account() |
GLM Coding Plan usage/quota/account monitoring |
c.GetAsyncResult(ctx, id), c.WaitForResult(ctx, id, interval) |
Shared polling for async image/video/chat tasks |
Every request-validation check (required fields, etc.) happens client-side
before a request is sent — you get a local error immediately rather than a
round trip for something like a missing model.
Chat completions
resp, err := c.Chat().Create(ctx, client.ChatRequest{
Model: "glm-5.2",
Messages: []client.Message{
{Role: "system", Content: "You are a helpful assistant."},
{Role: "user", Content: "Explain goroutines in one paragraph"},
},
Temperature: 0.7,
})
fmt.Println(resp.Choices[0].Message.Content)
Streaming
The recommended streaming API returns a Go 1.23+ iterator you range over:
for chunk, err := range c.Chat().Stream(ctx, req) {
if err != nil {
// Fatal — the stream ends after this iteration.
break
}
if len(chunk.Choices) > 0 {
fmt.Print(chunk.Choices[0].Delta.Content)
}
}
Context cancellation propagates both ways: cancelling ctx stops the range
loop and tears down the in-flight SSE read without leaking the producer
goroutine. Connect-phase transient failures (429/5xx/network) are retried up
to Config.MaxRetries exactly like Create; once a stream has begun,
mid-stream failures surface as the terminal err from the iterator.
The older callback-based API still works but is deprecated and will be removed in v1.0:
// Deprecated: prefer Stream (range-over-func).
err := c.Chat().CreateStream(ctx, req, func(chunk client.StreamChunk) error {
if len(chunk.Choices) > 0 {
fmt.Print(chunk.Choices[0].Delta.Content)
}
return nil // a non-nil return aborts the stream
})
The Anthropic-compatible surface has the same pair: c.Anthropic().Stream
returns iter.Seq2[AnthropicStreamEvent, error]; c.Anthropic().CreateStream
is the deprecated callback variant.
Set req.StreamToolCall = true (GLM-4.6+) to stream tool-call deltas
incrementally in chunk.Choices[0].Delta.ToolCalls across multiple events,
rather than receiving them as a single batch at the end of the turn. Useful
for surfacing "the model is calling a tool…" progress to a UI. NOT VERIFIED
LIVE — see Roadmap.
Async
task, _ := c.Chat().CreateAsync(ctx, req)
result, err := c.WaitForResult(ctx, task.ID, 3*time.Second)
Vision (images in a message)
req.Messages[len(req.Messages)-1].Images = []string{
"https://example.com/photo.jpg", // or a data: URI
}
req.Model = "glm-4.6v"
Structured output
req.ResponseFormat = client.NewJSONSchemaFormat("my_schema", rawJSONSchema, true /* strict */)
Function calling
For manual control, inspect resp.Choices[0].Message.ToolCalls yourself and
append role: "tool" messages before calling Create again. For the common
case, RunWithTools drives that loop for you:
resp, err := c.Chat().RunWithTools(ctx, req, func(name, arguments string) (string, error) {
switch name {
case "get_weather":
return `{"temp_c": 18}`, nil
default:
return "", fmt.Errorf("unknown tool %q", name)
}
})
It executes each tool call, appends the assistant + tool messages, and
repeats until the model returns a non-tool finish reason or
ToolMaxRounds (8) is exceeded — use RunWithToolsLimit to set a different
cap. A tool executor error is reported back to the model as the tool's
result ("error: ..."), not returned to your caller, so the model can
recover instead of the whole exchange failing.
Tool types
A Tool carries one of three payloads, selected by its Type:
| Constructor | Type |
Payload |
|---|---|---|
NewFunctionTool(name, desc, params) |
ToolTypeFunction ("function") |
FunctionDef — a callable the model invokes by name |
NewRetrievalTool(knowledgeID, prompt) |
ToolTypeRetrieval ("retrieval") |
Retrieval — a knowledge base to ground the answer |
NewWebSearchTool(queries...) |
ToolTypeWebSearch ("web_search") |
WebSearch — a search_query list |
retrieval and web_search are documented on docs.z.ai but NOT VERIFIED
LIVE here — only function is confirmed against the live API. The
web_search payload shape ({"search_query":[...]}) follows the official
Python SDK example; see Roadmap.
validateChatRequest enforces three documented rules client-side, so you get
a clear local error instead of the server's opaque one:
- Tool-name pattern —
tools[].function.namemust match^[A-Za-z0-9_-]{1,64}$. - Function cap — at most
ToolMaxFunctions(128) function tools per request. - Per-type payload — a
function/retrieval/web_searchtool must carry its matching payload; unknown types are rejected.
Response fields worth checking
Beyond resp.Choices[0].Message.Content:
resp.Choices[0].FinishReason— compare against theFinishReason*constants (FinishReasonStop,FinishReasonToolCalls,FinishReasonLength,FinishReasonSensitive,FinishReasonModelContextWindowExceeded,FinishReasonNetworkError). The last three are documented live values that signal non-content terminations.resp.WebSearch— the top-levelweb_searcharray the response carries when aweb_searchtool fired (each entry'sLink/Title/Contentare the sources the answer grounded in). NOT VERIFIED LIVE.
Tool-schema compatibility
GLM's chat endpoint uses a strict JSON-Schema parser for tool parameters:
a schema containing anyOf, oneOf, allOf, or a $ref/$defs reference
makes it return HTTP 500 rather than a usable error. Those constructs are
exactly what typed languages emit — a nullable field becomes
anyOf: [{…}, {"type":"null"}], a reused struct becomes a $ref.
By default the client rewrites tool schemas into the flat subset GLM accepts
before every chat request (nullable unions collapse to the underlying type,
allOf merges, $ref inlines), keeping as much type/description information
as possible. It's a no-op for schemas already in the supported subset and
never mutates your req.Tools.
- To normalize a schema yourself (e.g. you build requests elsewhere):
client.SanitizeToolSchemas(tools). - To send schemas through untouched (debugging, or a future endpoint that
supports the full draft): set
Config.DisableToolSchemaCompat = true.
Anthropic-compatible Messages API
Z.AI also exposes an Anthropic-protocol surface at /api/anthropic — the same
endpoint the GLM Coding Plan points Claude Code at. c.Anthropic() is a typed
client for its POST /v1/messages, parallel to c.Chat() for the OpenAI-style
surface. It authenticates with your z.ai key as a Bearer token (not Anthropic's
x-api-key) and sends an anthropic-version header automatically.
resp, err := c.Anthropic().Create(ctx, client.AnthropicMessageRequest{
Model: "glm-4.6",
MaxTokens: 1024, // required by the Messages API
System: "You are concise.",
Messages: []client.AnthropicMessage{
client.AnthropicTextMessage("user", "Explain goroutines in one line"),
},
})
fmt.Println(resp.Text()) // concatenated text blocks
Streaming delivers Anthropic's raw SSE events (message_start,
content_block_delta, …) with the event name and JSON payload, which you
unmarshal per event type:
err := c.Anthropic().CreateStream(ctx, req, func(ev client.AnthropicStreamEvent) error {
if ev.Type == "content_block_delta" {
// ev.Data is {"delta":{"type":"text_delta","text":"…"}, …}
}
return nil
})
Tools declared via AnthropicTool.InputSchema get the same GLM schema
normalization as chat tools (see above). Config.DisableToolSchemaCompat
disables it.
Extended thinking (GLM models are reasoning models) is enabled per request and
read back with resp.Thinking():
req.Thinking = &client.AnthropicThinking{Type: "enabled", BudgetTokens: 2048}
resp, _ := c.Anthropic().Create(ctx, req)
fmt.Println(resp.Thinking()) // thinking blocks, or reasoning_content if the
// endpoint surfaces reasoning that way instead
fmt.Println(resp.Text()) // the answer, without the reasoning mixed in
The success-path response shape is modeled from Anthropic's documented Messages API and is not yet live-verified here — see Roadmap.
Error handling
See Error Handling for the full APIError reference,
error codes, and the retry behavior you get by default.
Observability hooks
Config.Hooks attaches observability hooks (tracing, metrics, logging) that
fire on every request, response, error, and stream chunk — without you
wrapping the http.RoundTripper. The interface is stdlib-only so pkg/client
stays dependency-free; concrete implementations live in pkg/observe
(OpenTelemetry) or you can write your own.
import (
"github.com/SamyRai/go-z-ai/pkg/client"
"github.com/SamyRai/go-z-ai/pkg/observe"
)
c, _ := client.NewClient(client.Config{
APIKey: os.Getenv("ZAI_API_KEY"),
Hooks: []client.Hook{observe.NewOTelHook("my-service")},
})
A Hook fires:
OnRequest(ctx, meta) context.Context— per attempt, before the HTTP send; the returned context replaces the input (attach tracing spans here).OnResponse(ctx, meta)— after a successful non-streaming response, with status code, duration, and token usage (when the response carries one).OnError(ctx, meta, err)— on terminal failure, with the final error.OnStreamChunk(ctx, meta, chunk)— for each chunk fromChat().Stream/Anthropic().Stream.
RequestMeta carries Service, Method, Endpoint, Model, and Attempt
fields. Services stamp Service/Model into the context automatically (chat,
anthropic, embeddings, rerank); other services leave them empty unless you
stamp them yourself via client.WithService(ctx, "...") / client.WithModel(ctx, "...")
before the call.
A nil/empty Hooks slice skips all invocation — the no-hook path is
zero-allocation and has no measurable overhead.
Multi-account credential management
The multi-account credential store and the GLM Coding Plan credential/config
writers live in internal/accounts and internal/coding. They are internal
to this module — not part of the importable public API — so they can evolve
without semver constraints. pkg/client is the only supported public package;
the accounts and coding CLI commands are the stable way to drive that
functionality. (These packages lived under pkg/ before and were importable;
see the CHANGELOG for the move.)
Testing your own code against this client
Every service method is a plain function on an interface-free concrete type,
so the usual Go approach is to point Config.BaseURL at an httptest.Server
you control. If you want to replay real recorded Z.AI traffic instead of a
hand-written stub, see how this repo's own tests do it with
go-vcr — pkg/client/*_test.go and
pkg/client/testdata/cassettes/ — and read
Contributing § the live-verification convention for why.
Architecture notes
For how the services are structured internally (the doRequest facade,
retry/backoff design, why some services authenticate against a different base
URL) see Architecture.