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

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:

Response fields worth checking

Beyond resp.Choices[0].Message.Content:

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.

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:

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-vcrpkg/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.