Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,15 @@ inst/doc
/.quarto/
_dev/
plans/

# Local secrets
.Renviron

# Local scratch scripts / notes (do not commit)
ellmer_test_zai.R
official-glm-doc.txt
test-zai-api.R
test-zai-simple.R
test_zai_tutor.R
test_zai_all_endpoints.R
zai_chat_example.R
1 change: 1 addition & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ Collate:
'provider-portkey.R'
'provider-snowflake.R'
'provider-vllm.R'
'provider-zai.R'
'schema.R'
'tokens.R'
'tools-built-in.R'
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export(chat_perplexity)
export(chat_portkey)
export(chat_snowflake)
export(chat_vllm)
export(chat_zai)
export(claude_file_delete)
export(claude_file_download)
export(claude_file_get)
Expand Down Expand Up @@ -94,6 +95,7 @@ export(models_ollama)
export(models_openai)
export(models_portkey)
export(models_vllm)
export(models_zai)
export(openai_tool_web_search)
export(parallel_chat)
export(parallel_chat_structured)
Expand Down
273 changes: 273 additions & 0 deletions R/provider-zai.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
#' @include provider.R
#' @include provider-openai-compatible.R
#' @include content.R
#' @include turns.R
#' @include tools-def.R
NULL

#' Chat with a Z AI model
#'
#' @description
#' [Z AI](https://z.ai/) provides GLM (General Language Model) models
#' optimized for agentic coding and multi-step reasoning.
#'
#' ## API Endpoints
#'
#' Z AI offers two domains/regions, each with a regular and coding endpoint.
#'
#' **International (api.z.ai):**
#' - Regular API: `https://api.z.ai/api/paas/v4`
#' - Coding API (default): `https://api.z.ai/api/coding/paas/v4`
#'
#' **China / BigModel (open.bigmodel.cn):**
#' - Regular API: `https://open.bigmodel.cn/api/paas/v4`
#' - Coding API: `https://open.bigmodel.cn/api/coding/paas/v4`
#'
#' **Coding API (Default):**
#' - Endpoint: `https://api.z.ai/api/coding/paas/v4` (or China: `https://open.bigmodel.cn/api/coding/paas/v4`)
#' - Plan: GLM Coding Lite-Quarterly (subscription-based)
#' - Best for: Code generation, debugging, technical tasks
#' - Usage: `chat_zai()` (uses this endpoint by default)
#'
#' **Regular API:**
#' - Endpoint: `https://api.z.ai/api/paas/v4` (or China: `https://open.bigmodel.cn/api/paas/v4`)
#' - Plan: Usage-based with credits
#' - Best for: General questions, conversations, non-coding tasks
#' - Usage: `chat_zai(base_url = "https://api.z.ai/api/paas/v4")`
#'
#' Both endpoints support the same models (glm-4.7, glm-4.6, glm-4.5, glm-4.5-Air)
#' and features (reasoning content, tool calling, structured data).
#'
#' ## Authentication
#'
#' You'll need an API key from https://z.ai/manage-apikey/apikey-list
#' Set it as the `ZAI_API_KEY` environment variable in your `.Renviron` file.
#'
#' @inheritParams chat_openai
#' @inherit chat_openai return
#' @param model `r param_model("glm-4.7", "zai")`
#' @param base_url The base URL to the endpoint.
#'
#' Defaults to the international coding endpoint (`https://api.z.ai/api/coding/paas/v4`).
#' To use the international regular API, set to `https://api.z.ai/api/paas/v4`.
#'
#' To use the China/BigModel endpoints, use `https://open.bigmodel.cn/api/paas/v4`
#' (regular) or `https://open.bigmodel.cn/api/coding/paas/v4` (coding).
#' @family chatbots
#' @export
#' @returns A [Chat] object.
#' @examples
#' if (nzchar(Sys.getenv("ZAI_API_KEY"))) {
#' # Use coding API (default - for GLM Coding Lite-Quarterly plan)
#' chat <- chat_zai()
#' chat$chat("What is 2 + 2?")
#'
#' # Use general API (for usage-based plans)
#' chat <- chat_zai(base_url = "https://api.z.ai/api/paas/v4")
#' chat$chat("What is 2 + 2?")
#' }
chat_zai <- function(
system_prompt = NULL,
base_url = "https://api.z.ai/api/coding/paas/v4",
api_key = NULL,
credentials = NULL,
model = NULL,
params = NULL,
api_args = list(),
api_headers = character(),
echo = c("none", "output", "all")
) {
model <- set_default(model, "glm-4.7")
echo <- check_echo(echo)

credentials <- as_credentials(
"chat_zai",
function() zai_key(),
credentials = credentials,
api_key = api_key
)

provider <- ProviderZAI(
name = "Z AI",
base_url = base_url,
model = model,
params = params %||% params(),
extra_args = api_args,
extra_headers = api_headers,
credentials = credentials
)
Chat$new(provider = provider, system_prompt = system_prompt, echo = echo)
}

chat_zai_test <- function(
...,
system_prompt = "Be terse.",
model = "glm-4.7",
params = NULL,
echo = "none"
) {
params <- params %||% params()
params$temperature <- params$temperature %||% 0

chat_zai(
system_prompt = system_prompt,
model = model,
params = params,
...,
echo = echo
)
}

#' @rdname chat_zai
#' @export
models_zai <- function(
base_url = "https://api.z.ai/api/coding/paas/v4",
api_key = NULL,
credentials = NULL
) {
credentials <- as_credentials(
"models_zai",
function() zai_key(),
credentials = credentials,
api_key = api_key
)

provider <- ProviderZAI(
name = "Z AI",
model = "",
base_url = base_url,
credentials = credentials
)

req <- base_request(provider)
req <- req_url_path_append(req, "/models")
resp <- req_perform(req)

json <- resp_body_json(resp)

id <- map_chr(json$data, "[[", "id")
created <- as.Date(.POSIXct(map_int(json$data, "[[", "created")))
owned_by <- map_chr(json$data, "[[", "owned_by")

df <- data.frame(
id = id,
created_at = created,
owned_by = owned_by
)
df <- cbind(df, match_prices(provider@name, df$id))
df[order(-xtfrm(df$created_at)), ]
}

# Provider class ---------------------------------------------------------------

ProviderZAI <- new_class(
"ProviderZAI",
parent = ProviderOpenAICompatible,
properties = list(
# No additional properties needed - inherits all from ProviderOpenAICompatible
)
)

# Authentication helpers -------------------------------------------

zai_key <- function() {
key_get("ZAI_API_KEY")
}

zai_key_exists <- function() {
key_exists("ZAI_API_KEY")
}

# Override chat_params to handle Z AI specific parameters --------------

method(chat_params, ProviderZAI) <- function(provider, params) {
params <- standardise_params(
params,
c(
temperature = "temperature",
top_p = "top_p",
frequency_penalty = "frequency_penalty",
presence_penalty = "presence_penalty",
max_tokens = "max_tokens",
max_completion_tokens = "max_completion_tokens",
top_logprobs = "top_logprobs",
n = "n"
)
)

# Z AI doesn't require max_tokens to be set
params$max_tokens <- NULL

params
}

# Override value_tokens to extract reasoning tokens -----------------

method(value_tokens, ProviderZAI) <- function(provider, json) {
usage <- json$usage

# Extract reasoning tokens if available
reasoning_tokens <- usage$completion_tokens_details$reasoning_tokens %||% 0
cached_tokens <- usage$prompt_tokens_details$cached_tokens %||% 0

tokens(
input = (usage$prompt_tokens %||% 0) - cached_tokens,
output = usage$completion_tokens %||% 0,
cached_input = cached_tokens
)
}

# Override value_turn to handle reasoning content -----------------

method(value_turn, ProviderZAI) <- function(
provider,
result,
has_type = FALSE
) {
msg <- result$choices[[1]]$message
msg_content <- msg$content

# Z AI generally follows OpenAI-compatible conventions, but some endpoints
# return `message$content` as a plain string instead of a list of objects.
if (is.character(msg_content)) {
contents <- list(ContentText(msg_content))
} else {
contents <- lapply(msg_content, function(content) {
if (is.character(content)) {
return(ContentText(content))
}
if (
is.list(content) && !is.null(content$type) && content$type == "text"
) {
return(ContentText(content$text %||% ""))
}
if (
is.list(content) && !is.null(content$type) && content$type == "refusal"
) {
return(ContentText(content$refusal %||% ""))
}
NULL
})
contents <- Filter(Negate(is.null), contents)
}

# Check for reasoning content
if (!is.null(msg$reasoning_content)) {
contents <- c(
list(ContentThinking(
thinking = msg$reasoning_content
)),
contents
)
}

tokens <- value_tokens(provider, result)
cost <- get_token_cost(provider, tokens)

AssistantTurn(
contents = contents,
json = result,
tokens = unlist(tokens),
cost = cost
)
}
Loading