From e9c4983eba739ea74f844167a818701138add9e8 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Sat, 29 Aug 2026 07:06:09 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(providers):=20=F0=9F=A4=96=20add=20nat?= =?UTF-8?q?ive=20Z.ai=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lock | 11 ++++++++ docs/config/models.mdx | 1 + docs/config/providers.mdx | 1 + package.json | 1 + scripts/gen_docs.ts | 1 + src/browser/assets/icons/zai.svg | 3 +++ .../ProjectSidebar/ProjectSidebar.test.tsx | 1 + .../components/ProviderIcon/ProviderIcon.tsx | 2 ++ .../Settings/Sections/ProvidersSection.tsx | 1 + src/common/config/schemas/providersConfig.ts | 2 ++ src/common/constants/knownModels.test.ts | 8 ++++++ src/common/constants/knownModels.ts | 9 ++++++- src/common/constants/providers.ts | 9 +++++++ src/common/types/thinking.ts | 6 +++++ src/common/utils/ai/modelCapabilities.test.ts | 9 +++++++ src/common/utils/ai/modelDisplay.test.ts | 6 +++++ src/common/utils/ai/modelDisplay.ts | 6 +++++ src/common/utils/ai/providerOptions.test.ts | 22 ++++++++++++++++ src/common/utils/ai/providerOptions.ts | 26 +++++++++++++++++++ src/common/utils/thinking/policy.test.ts | 25 +++++++++++++++++- src/common/utils/thinking/policy.ts | 21 ++++++++------- src/common/utils/tokens/modelStats.test.ts | 10 +++++++ src/common/utils/tokens/models-extra.ts | 14 ++++++++++ .../builtInSkillContent.generated.ts | 2 ++ src/node/services/aiService.ts | 6 ++--- src/node/utils/providerRequirements.test.ts | 10 +++++++ src/node/utils/providerRequirements.ts | 3 +++ 27 files changed, 202 insertions(+), 14 deletions(-) create mode 100644 src/browser/assets/icons/zai.svg diff --git a/bun.lock b/bun.lock index c192f34b86b..c34c2b21c56 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "@ai-sdk/openai": "^4.0.43", "@ai-sdk/openai-compatible": "^3.0.7", "@ai-sdk/xai": "^4.0.37", + "@ai-sdk/zai": "^3.0.2", "@aws-sdk/credential-providers": "^3.940.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", @@ -239,6 +240,8 @@ "@ai-sdk/xai": ["@ai-sdk/xai@4.0.37", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oecSCViDbMiySImn45gD0bVujStu4S7m1CjiPxhCq2tM8Ewi3d6FH/sx4lXJmmdofIdI2KbZMZtbFG+sZi2how=="], + "@ai-sdk/zai": ["@ai-sdk/zai@3.0.2", "", { "dependencies": { "@ai-sdk/openai-compatible": "3.0.40", "@ai-sdk/provider": "4.0.8", "@ai-sdk/provider-utils": "5.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bzSaYWA8Qpk0j3Lg+2ERdGe/24nB7ANHVLfSqq0/oR6YfLPmR6pCERfxjTr3yt09Zgki9tj+w5/6RQSH2IBo1w=="], + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="], @@ -3745,6 +3748,12 @@ "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.27", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw=="], + "@ai-sdk/zai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@3.0.40", "", { "dependencies": { "@ai-sdk/provider": "4.0.8", "@ai-sdk/provider-utils": "5.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4YvVykS7XfZEJMCe4gjxsT3CbCWXCONIK2QSVjEr+Xc4wbJcAxAM5xIWpx4x8ysFLlImhZc4OP4iTzeeGv76dA=="], + + "@ai-sdk/zai/@ai-sdk/provider": ["@ai-sdk/provider@4.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-aWO7iwhFUGf347tCwNGggggfmZigaSu7TF739IZSrWWABUp7zkb4Cr3fMqvBe5EIS7ABJJu3Cadn0g/zs1G0QQ=="], + + "@ai-sdk/zai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.33", "", { "dependencies": { "@ai-sdk/provider": "4.0.8", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TfjJqJmRsQyxAlb+3hGmP1o1xLUIT79yhOgtJuTF4hqsB37IC3CufGsuFhU04EeTOg7R9iptQ6Bzm0MW0n/c3g=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -4331,6 +4340,8 @@ "@ai-sdk/xai/@ai-sdk/provider-utils/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], + "@ai-sdk/zai/@ai-sdk/provider-utils/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], diff --git a/docs/config/models.mdx b/docs/config/models.mdx index 419edfbe1a1..79d2d099063 100644 --- a/docs/config/models.mdx +++ b/docs/config/models.mdx @@ -34,6 +34,7 @@ Xum ships with curated models kept up to date with the frontier. Use any custom | DeepSeek V4 Pro | deepseek:deepseek-v4-pro | `deepseek`, `deepseek-pro`, `deepseek-v4`, `deepseek-v4-pro` | | | DeepSeek V4 Flash | deepseek:deepseek-v4-flash | `deepseek-flash`, `deepseek-v4-flash` | | | Kimi K3 | moonshotai:kimi-k3 | `kimi`, `k3`, `kimi-k3` | | +| GLM 5.3 Flash | zai:glm-5.3-flash | `glm`, `glm-flash`, `glm-5.3-flash` | | {/* END KNOWN_MODELS_TABLE */} diff --git a/docs/config/providers.mdx b/docs/config/providers.mdx index 0e9e40f000f..51d01e2c669 100644 --- a/docs/config/providers.mdx +++ b/docs/config/providers.mdx @@ -45,6 +45,7 @@ Providers also read from environment variables as fallback: | OpenRouter | `OPENROUTER_API_KEY` | | DeepSeek | `DEEPSEEK_API_KEY` | | Moonshot AI | `MOONSHOT_API_KEY` | +| Z.ai | `ZAI_API_KEY` | | github-copilot | `GITHUB_COPILOT_TOKEN` | | Bedrock | `AWS_REGION` (credentials via AWS SDK chain) | diff --git a/package.json b/package.json index 961a1411627..029ab5a7de8 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@ai-sdk/openai": "^4.0.43", "@ai-sdk/openai-compatible": "^3.0.7", "@ai-sdk/xai": "^4.0.37", + "@ai-sdk/zai": "^3.0.2", "@aws-sdk/credential-providers": "^3.940.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/scripts/gen_docs.ts b/scripts/gen_docs.ts index 6cd2209a989..cd7cbb5a85d 100644 --- a/scripts/gen_docs.ts +++ b/scripts/gen_docs.ts @@ -370,6 +370,7 @@ const PROVIDER_DISPLAY_NAMES: Record = { xai: "xAI", deepseek: "DeepSeek", moonshotai: "Moonshot AI", + zai: "Z.ai", openrouter: "OpenRouter", bedrock: "Bedrock", }; diff --git a/src/browser/assets/icons/zai.svg b/src/browser/assets/icons/zai.svg new file mode 100644 index 00000000000..ffa9031fa90 --- /dev/null +++ b/src/browser/assets/icons/zai.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx b/src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx index 0d63abc88b0..d40b767d7ea 100644 --- a/src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx +++ b/src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx @@ -63,6 +63,7 @@ function installProviderIconSvgMocks() { "@/browser/assets/icons/ollama.svg?react", "@/browser/assets/icons/deepseek.svg?react", "@/browser/assets/icons/moonshotai.svg?react", + "@/browser/assets/icons/zai.svg?react", "@/browser/assets/icons/aws.svg?react", "@/browser/assets/icons/github.svg?react", "@/browser/assets/icons/coder.svg?react", diff --git a/src/browser/components/ProviderIcon/ProviderIcon.tsx b/src/browser/components/ProviderIcon/ProviderIcon.tsx index a855b4ef223..5dda8e847b3 100644 --- a/src/browser/components/ProviderIcon/ProviderIcon.tsx +++ b/src/browser/components/ProviderIcon/ProviderIcon.tsx @@ -8,6 +8,7 @@ import OpenRouterIcon from "@/browser/assets/icons/openrouter.svg?react"; import OllamaIcon from "@/browser/assets/icons/ollama.svg?react"; import DeepSeekIcon from "@/browser/assets/icons/deepseek.svg?react"; import MoonshotAIIcon from "@/browser/assets/icons/moonshotai.svg?react"; +import ZaiIcon from "@/browser/assets/icons/zai.svg?react"; import AWSIcon from "@/browser/assets/icons/aws.svg?react"; import GitHubIcon from "@/browser/assets/icons/github.svg?react"; import CoderIcon from "@/browser/assets/icons/coder.svg?react"; @@ -30,6 +31,7 @@ const PROVIDER_ICONS: Partial> = { xai: XAIIcon, deepseek: DeepSeekIcon, moonshotai: MoonshotAIIcon, + zai: ZaiIcon, openrouter: OpenRouterIcon, bedrock: AWSIcon, ollama: OllamaIcon, diff --git a/src/browser/features/Settings/Sections/ProvidersSection.tsx b/src/browser/features/Settings/Sections/ProvidersSection.tsx index 5675f50248a..05cef9e6235 100644 --- a/src/browser/features/Settings/Sections/ProvidersSection.tsx +++ b/src/browser/features/Settings/Sections/ProvidersSection.tsx @@ -265,6 +265,7 @@ const PROVIDER_KEY_URLS: Partial> = { xai: "https://console.x.ai/team/default/api-keys", deepseek: "https://platform.deepseek.com/api_keys", moonshotai: "https://platform.moonshot.ai/console/api-keys", + zai: "https://z.ai/manage-apikey/apikey-list", openrouter: "https://openrouter.ai/settings/keys", // bedrock: AWS credential chain, no simple key URL // ollama: local service, no key needed diff --git a/src/common/config/schemas/providersConfig.ts b/src/common/config/schemas/providersConfig.ts index 3d2fd519036..bad449d075d 100644 --- a/src/common/config/schemas/providersConfig.ts +++ b/src/common/config/schemas/providersConfig.ts @@ -139,6 +139,7 @@ export const CoderProviderConfigSchema = BaseProviderConfigSchema.extend({ export const GoogleProviderConfigSchema = BaseProviderConfigSchema; export const DeepSeekProviderConfigSchema = BaseProviderConfigSchema; export const MoonshotAIProviderConfigSchema = BaseProviderConfigSchema; +export const ZaiProviderConfigSchema = BaseProviderConfigSchema; export const OllamaProviderConfigSchema = BaseProviderConfigSchema; export const GitHubCopilotProviderConfigSchema = BaseProviderConfigSchema; @@ -153,6 +154,7 @@ export const ProvidersConfigSchema = z google: GoogleProviderConfigSchema.optional(), deepseek: DeepSeekProviderConfigSchema.optional(), moonshotai: MoonshotAIProviderConfigSchema.optional(), + zai: ZaiProviderConfigSchema.optional(), ollama: OllamaProviderConfigSchema.optional(), "github-copilot": GitHubCopilotProviderConfigSchema.optional(), coder: CoderProviderConfigSchema.optional(), diff --git a/src/common/constants/knownModels.test.ts b/src/common/constants/knownModels.test.ts index 0b4e15c0b89..9103f767138 100644 --- a/src/common/constants/knownModels.test.ts +++ b/src/common/constants/knownModels.test.ts @@ -49,6 +49,14 @@ describe("Known Models Integration", () => { expect(MODEL_ABBREVIATIONS.k3).toBe("moonshotai:kimi-k3"); }); + test("glm aliases resolve only to the direct Z.ai GLM 5.3 Flash model", () => { + expect(MODEL_ABBREVIATIONS.glm).toBe("zai:glm-5.3-flash"); + expect(MODEL_ABBREVIATIONS["glm-flash"]).toBe("zai:glm-5.3-flash"); + expect(Object.values(KNOWN_MODELS).filter((model) => model.provider === "zai")).toEqual([ + KNOWN_MODELS.GLM_53_FLASH, + ]); + }); + test("known model ids and aliases stay unique across the curated registry", () => { const seenIds = new Set(); const seenAliases = new Set(); diff --git a/src/common/constants/knownModels.ts b/src/common/constants/knownModels.ts index 7fb89c56666..abeafd1dd5d 100644 --- a/src/common/constants/knownModels.ts +++ b/src/common/constants/knownModels.ts @@ -4,7 +4,7 @@ import { formatModelDisplayName } from "../utils/ai/modelDisplay"; -type ModelProvider = "anthropic" | "openai" | "google" | "xai" | "deepseek" | "moonshotai"; +type ModelProvider = "anthropic" | "openai" | "google" | "xai" | "deepseek" | "moonshotai" | "zai"; interface KnownModelDefinition { /** Provider identifier used by SDK factories */ @@ -225,6 +225,13 @@ const MODEL_DEFINITIONS = { // Moonshot tokenizer available) for approximate counting. tokenizerOverride: "moonshotai/kimi-k2", }, + GLM_53_FLASH: { + provider: "zai", + providerModelId: "glm-5.3-flash", + aliases: ["glm", "glm-flash", "glm-5.3-flash"], + // GLM 5.3 is not in ai-tokenizer yet; use the closest published GLM encoding. + tokenizerOverride: "zai/glm-4.5", + }, } as const satisfies Record; export type KnownModelKey = keyof typeof MODEL_DEFINITIONS; diff --git a/src/common/constants/providers.ts b/src/common/constants/providers.ts index 54cb54f05ba..6a0ddc362db 100644 --- a/src/common/constants/providers.ts +++ b/src/common/constants/providers.ts @@ -21,6 +21,7 @@ export type ProviderName = | "xai" | "deepseek" | "moonshotai" + | "zai" | "openrouter" | "github-copilot" | "coder" @@ -158,12 +159,20 @@ export const PROVIDER_DEFINITIONS = { requiresApiKey: true, kind: "direct", }, + zai: { + displayName: "Z.ai", + import: () => import("@ai-sdk/zai"), + factoryName: "createZai", + requiresApiKey: true, + kind: "direct", + }, openrouter: { displayName: "OpenRouter", import: () => import("@openrouter/ai-sdk-provider"), factoryName: "createOpenRouter", requiresApiKey: true, kind: "gateway", + // OpenRouter uses the hyphenated vendor slug "z-ai", so direct "zai" routing is not inferred. routes: ["anthropic", "openai", "google", "xai", "deepseek", "moonshotai"], passthrough: false, toGatewayModelId: toSlashSeparatedGatewayModelId, diff --git a/src/common/types/thinking.ts b/src/common/types/thinking.ts index cc75a758ddd..03af2c3cbf7 100644 --- a/src/common/types/thinking.ts +++ b/src/common/types/thinking.ts @@ -325,6 +325,12 @@ export function isGrok46Model(modelString: string): boolean { return /^grok-4\.6(?:$|-)/.test(withoutPrefix); } +/** GLM 5.3 and GLM 5.3 Flash always reason with low, high, or max effort. */ +export function isGlm53Model(modelString: string): boolean { + const withoutPrefix = stripModelProviderPrefixes(modelString); + return /^glm-5\.3(?:-flash)?(?:$|-(?:latest|\d))/.test(withoutPrefix); +} + /** * Kimi K3 (Moonshot AI) always reasons and supports only the max reasoning * effort; the thinking policy and the Moonshot/OpenRouter provider-options diff --git a/src/common/utils/ai/modelCapabilities.test.ts b/src/common/utils/ai/modelCapabilities.test.ts index f13e6594291..f293149d5cd 100644 --- a/src/common/utils/ai/modelCapabilities.test.ts +++ b/src/common/utils/ai/modelCapabilities.test.ts @@ -46,6 +46,15 @@ describe("getModelCapabilities", () => { } }); + it("reports image-only multimodal input for GLM 5.3 Flash", () => { + const caps = getModelCapabilities("zai:glm-5.3-flash"); + + expect(caps).not.toBeNull(); + expect(caps?.supportsVision).toBe(true); + expect(caps?.supportsPdfInput).toBe(false); + expect(caps?.supportsVideoInput).toBe(false); + }); + it("infers PDF support for OpenAI vision models when models-extra omits the flag", () => { const caps = getModelCapabilities("openai:gpt-5.5"); expect(caps).not.toBeNull(); diff --git a/src/common/utils/ai/modelDisplay.test.ts b/src/common/utils/ai/modelDisplay.test.ts index 0aa441b4403..c2e13fadf9f 100644 --- a/src/common/utils/ai/modelDisplay.test.ts +++ b/src/common/utils/ai/modelDisplay.test.ts @@ -31,6 +31,12 @@ describe("formatModelDisplayName", () => { }); }); + describe("GLM models", () => { + test("preserves uppercase GLM branding", () => { + expect(formatModelDisplayName("glm-5.3-flash")).toBe("GLM 5.3 Flash"); + }); + }); + describe("GPT models", () => { test("formats GPT models", () => { expect(formatModelDisplayName("gpt-5-pro")).toBe("GPT-5 Pro"); diff --git a/src/common/utils/ai/modelDisplay.ts b/src/common/utils/ai/modelDisplay.ts index 58009606c4a..8b6318e6202 100644 --- a/src/common/utils/ai/modelDisplay.ts +++ b/src/common/utils/ai/modelDisplay.ts @@ -81,6 +81,12 @@ export function formatModelDisplayName(modelName: string): string { } } + // GLM models + if (lower.startsWith("glm-")) { + const parts = lower.slice("glm-".length).split("-"); + return `GLM ${parts.map(capitalize).join(" ")}`; + } + // GPT models if (lower.startsWith("gpt-")) { const parts = lower.split("-"); diff --git a/src/common/utils/ai/providerOptions.test.ts b/src/common/utils/ai/providerOptions.test.ts index 8467dbe0ca0..430b0710d52 100644 --- a/src/common/utils/ai/providerOptions.test.ts +++ b/src/common/utils/ai/providerOptions.test.ts @@ -1862,6 +1862,28 @@ describe("buildProviderOptions - Moonshot", () => { }); }); +describe("buildProviderOptions - Z.ai", () => { + test("enables thinking and maps every GLM 5.3 policy level", () => { + for (const [level, reasoningEffort] of [ + ["low", "low"], + ["high", "high"], + ["max", "max"], + ] as const) { + expect(buildProviderOptions("zai:glm-5.3-flash", level)).toEqual({ + zai: { + thinking: { type: "enabled" }, + reasoningEffort, + toolStream: true, + }, + }); + } + }); + + test("does not apply forced GLM 5.3 options to other Z.ai models", () => { + expect(buildProviderOptions("zai:glm-4.7-flash", "high")).toEqual({}); + }); +}); + describe("buildProviderOptions - OpenRouter", () => { test("sends the explicit max effort for OpenRouter-routed Kimi K3", () => { // `enabled: true` alone falls back to OpenRouter's default (medium) effort, diff --git a/src/common/utils/ai/providerOptions.ts b/src/common/utils/ai/providerOptions.ts index 0748d7d3bef..04b863be7e2 100644 --- a/src/common/utils/ai/providerOptions.ts +++ b/src/common/utils/ai/providerOptions.ts @@ -10,6 +10,7 @@ import type { AnthropicProviderOptions } from "@ai-sdk/anthropic"; import type { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google"; import type { OpenAIResponsesProviderOptions } from "@ai-sdk/openai"; import type { JSONValue } from "@ai-sdk/provider"; +import type { ZaiLanguageModelChatOptions } from "@ai-sdk/zai"; import type { XaiProviderOptions, // Chat options alias does not include store; Responses options do (frontier Grok / ZDR). @@ -28,6 +29,7 @@ import { getOpenAIReasoningEffort, isGrok46Model, isGrokFrontierModel, + isGlm53Model, isKimiK3Model, openaiSupportsProMode, OPENROUTER_REASONING_EFFORT, @@ -147,6 +149,7 @@ type ProviderOptions = | { google: GoogleGenerativeAIProviderOptions } | { openrouter: OpenRouterReasoningOptions } | { moonshotai: MoonshotAIProviderOptions } + | { zai: ZaiLanguageModelChatOptions } | { xai: XaiBuiltProviderOptions } | { "github-copilot": OpenAICompatibleGatewayProviderOptions } | Record; // Empty object for unsupported providers @@ -590,6 +593,29 @@ export function buildProviderOptions( return {}; } + // Build Z.ai-specific options + if (formatProvider === "zai") { + if (!isGlm53Model(capabilityModel)) { + return {}; + } + + const reasoningEffort: ZaiLanguageModelChatOptions["reasoningEffort"] = + effectiveThinking === "max" + ? "max" + : effectiveThinking === "high" || effectiveThinking === "xhigh" + ? "high" + : "low"; + const options = { + zai: { + thinking: { type: "enabled" }, + reasoningEffort, + toolStream: true, + }, + } satisfies { zai: ZaiLanguageModelChatOptions }; + log.debug("buildProviderOptions: Returning Z.ai options", options); + return options; + } + // Build OpenRouter-specific options if (formatProvider === "openrouter") { // Kimi K3 always reasons and supports only the max reasoning effort. Send it diff --git a/src/common/utils/thinking/policy.test.ts b/src/common/utils/thinking/policy.test.ts index baa3862272b..d7ea4ff372c 100644 --- a/src/common/utils/thinking/policy.test.ts +++ b/src/common/utils/thinking/policy.test.ts @@ -458,7 +458,7 @@ describe("getThinkingPolicyForModel", () => { expect(enforceThinkingPolicy("anthropic:claude-mythos-5", "off")).toBe("low"); }); - test("resolveEffectiveThinkingLevel clamps unset/off for Mythos-class only", () => { + test("resolveEffectiveThinkingLevel clamps unset/off for forced-thinking models", () => { // Mythos-class cannot disable thinking: unset and "off" both resolve to "low" // so provider options, replay transforms, and metadata stay consistent with // the provider's always-thinking behavior. @@ -956,6 +956,29 @@ describe("Grok 4.6 thinking policy", () => { }); }); +describe("GLM 5.3 thinking policy", () => { + test("offers only Z.ai's forced-thinking effort levels", () => { + for (const model of ["glm-5.3-flash", "zai:glm-5.3", "zai:glm-5.3-flash-2026-08-26"]) { + expect(getThinkingPolicyForModel(model)).toEqual(["low", "high", "max"]); + } + + expect(enforceThinkingPolicy("zai:glm-5.3-flash", "off")).toBe("low"); + expect(enforceThinkingPolicy("zai:glm-5.3-flash", "medium")).toBe("low"); + expect(enforceThinkingPolicy("zai:glm-5.3-flash", "xhigh")).toBe("high"); + expect(resolveEffectiveThinkingLevel("zai:glm-5.3-flash", undefined)).toBe("low"); + expect(resolveEffectiveThinkingLevel("zai:glm-5.3-flash", "off")).toBe("low"); + }); + + test("does not apply the GLM 5.3 policy to named variants", () => { + expect(getThinkingPolicyForModel("zai:glm-5.3-flashx")).toEqual([ + "off", + "low", + "medium", + "high", + ]); + }); +}); + describe("getAvailableThinkingLevels", () => { test("returns the raw capability when no floor is provided", () => { expect(getAvailableThinkingLevels("anthropic:claude-sonnet-4-5")).toEqual([ diff --git a/src/common/utils/thinking/policy.ts b/src/common/utils/thinking/policy.ts index 0fd9e7a2f02..511d17da99f 100644 --- a/src/common/utils/thinking/policy.ts +++ b/src/common/utils/thinking/policy.ts @@ -21,6 +21,7 @@ import { anthropicSupportsNativeXhigh, isGrok46Model, isGrokFrontierModel, + isGlm53Model, isKimiK3Model, openaiSupportsNativeMaxEffort, stripModelProviderPrefixes, @@ -71,6 +72,7 @@ export function isGeminiFlashThinkingLevelModelName(modelName: string): boolean * - gemini-3 Pro variants → ["low", "high"] (thinking level only) * - xai:grok-4.6 → ["low", "medium", "high", "xhigh"] (reasoning cannot be disabled) * - xai:grok-4.5 → ["low", "medium", "high"] (reasoning cannot be disabled) + * - zai:glm-5.3-flash → ["low", "high", "max"] (reasoning cannot be disabled) * - default → ["off", "low", "medium", "high"] (standard 4 levels; xhigh is opt-in per model) * * Tolerates version suffixes (e.g., gpt-5-pro-2025-10-06). @@ -187,6 +189,11 @@ function getExplicitThinkingPolicy(modelString: string): ThinkingPolicy | null { return ["low", "medium", "high"]; } + // GLM 5.3 always reasons and exposes exactly the effort values accepted by Z.ai. + if (isGlm53Model(withoutProviderNamespace)) { + return ["low", "high", "max"]; + } + // Kimi K3 always reasons and supports only the max reasoning effort, so the // policy is a fixed single level. if (isKimiK3Model(withoutProviderNamespace)) { @@ -289,17 +296,13 @@ export function lookupMinThinkingLevelOverride( * Resolve the effective thinking level for an outgoing stream request. * * Most models treat an unset level as "off". Models that reject disabled - * thinking (Mythos-class Anthropic, see {@link anthropicRejectsDisabledThinking}) - * clamp unset/legacy "off" up through the thinking policy instead, so the - * level Xum tracks (provider options, replay transforms, metadata) matches the - * provider's actual always-thinking behavior. Without this, the wire request - * would run adaptive thinking while the message pipeline skips the Anthropic - * thinking replay transforms (`anthropicThinkingEnabled` keys off "off"), - * losing required signed thinking context on follow-up requests. + * thinking clamp unset/legacy "off" through their policy so Xum's tracked level + * matches the provider's always-thinking behavior. This keeps provider options, + * reasoning metadata, and provider-specific replay transforms consistent. * * Pass `providersConfig` so configured aliases (`mappedToModel`, e.g. * `anthropic:internal-fable` -> `anthropic:claude-fable-5`) are resolved to - * their capability model before the Mythos check — matching how + * their capability model before the forced-thinking check, matching how * `buildProviderOptions` detects capabilities. */ export function resolveEffectiveThinkingLevel( @@ -309,7 +312,7 @@ export function resolveEffectiveThinkingLevel( ): ThinkingLevel { const level = requested ?? THINKING_LEVEL_OFF; const capabilityModel = resolveModelForMetadata(modelString, providersConfig ?? null); - return anthropicRejectsDisabledThinking(capabilityModel) + return anthropicRejectsDisabledThinking(capabilityModel) || isGlm53Model(capabilityModel) ? enforceThinkingPolicy(capabilityModel, level) : level; } diff --git a/src/common/utils/tokens/modelStats.test.ts b/src/common/utils/tokens/modelStats.test.ts index af64bd20886..6e101c13b46 100644 --- a/src/common/utils/tokens/modelStats.test.ts +++ b/src/common/utils/tokens/modelStats.test.ts @@ -184,6 +184,16 @@ describe("getModelStats", () => { expect(expectStats("openrouter:moonshotai/kimi-k3")).toEqual(stats); }); + test("resolves GLM 5.3 Flash limits, list pricing, and supported inputs", () => { + const stats = expectStats("zai:glm-5.3-flash"); + + expect(stats.max_input_tokens).toBe(1_048_576); + expect(stats.max_output_tokens).toBe(131_072); + expect(stats.input_cost_per_token).toBe(0.00000015); + expect(stats.output_cost_per_token).toBe(0.0000005); + expect(stats.cache_read_input_token_cost).toBe(0.00000003); + }); + test("resolves the default image generation model pricing", () => { const stats = expectStats(DEFAULT_IMAGE_MODEL); diff --git a/src/common/utils/tokens/models-extra.ts b/src/common/utils/tokens/models-extra.ts index e28a1bc340b..7d3daa4d6ec 100644 --- a/src/common/utils/tokens/models-extra.ts +++ b/src/common/utils/tokens/models-extra.ts @@ -660,6 +660,20 @@ export const modelsExtra: Record = { supports_response_schema: true, }, + // GLM 5.3 Flash list pricing and limits for the direct Z.ai provider. + "zai/glm-5.3-flash": { + max_input_tokens: 1048576, + max_output_tokens: 131072, + input_cost_per_token: 0.00000015, // $0.15 per million input tokens + output_cost_per_token: 0.0000005, // $0.50 per million output tokens + cache_read_input_token_cost: 0.00000003, // $0.03 per million cached input tokens + litellm_provider: "zai", + mode: "chat", + supports_function_calling: true, + supports_vision: true, + supports_reasoning: true, + }, + // Kimi K3 - released July 16, 2026. 1M context, 128K max output, text+image input. // Keyed on the direct provider; OpenRouter-routed requests canonicalize to this id. "moonshotai/kimi-k3": { diff --git a/src/node/services/agentSkills/builtInSkillContent.generated.ts b/src/node/services/agentSkills/builtInSkillContent.generated.ts index 1c2111b2d79..48e3587fabe 100644 --- a/src/node/services/agentSkills/builtInSkillContent.generated.ts +++ b/src/node/services/agentSkills/builtInSkillContent.generated.ts @@ -3871,6 +3871,7 @@ export const BUILTIN_SKILL_FILES: Record> = { "| DeepSeek V4 Pro | deepseek:deepseek-v4-pro | `deepseek`, `deepseek-pro`, `deepseek-v4`, `deepseek-v4-pro` | |", "| DeepSeek V4 Flash | deepseek:deepseek-v4-flash | `deepseek-flash`, `deepseek-v4-flash` | |", "| Kimi K3 | moonshotai:kimi-k3 | `kimi`, `k3`, `kimi-k3` | |", + "| GLM 5.3 Flash | zai:glm-5.3-flash | `glm`, `glm-flash`, `glm-5.3-flash` | |", "", "{/* END KNOWN_MODELS_TABLE */}", "", @@ -4321,6 +4322,7 @@ export const BUILTIN_SKILL_FILES: Record> = { "| OpenRouter | `OPENROUTER_API_KEY` |", "| DeepSeek | `DEEPSEEK_API_KEY` |", "| Moonshot AI | `MOONSHOT_API_KEY` |", + "| Z.ai | `ZAI_API_KEY` |", "| github-copilot | `GITHUB_COPILOT_TOKEN` |", "| Bedrock | `AWS_REGION` (credentials via AWS SDK chain) |", "", diff --git a/src/node/services/aiService.ts b/src/node/services/aiService.ts index d0d192fd51f..c5b7357ceb8 100644 --- a/src/node/services/aiService.ts +++ b/src/node/services/aiService.ts @@ -1470,9 +1470,9 @@ export class AIService extends EventEmitter { modelString, modelResult.data.coderSelectedInstance ); - // FINAL thinking clamp from the pinned snapshot (Mythos-class Anthropic - // cannot disable thinking; aliases mapped to Mythos models get the same - // treatment). Resolved here — not from the pre-factory read — so a + // FINAL thinking clamp from the pinned snapshot. Models that cannot disable + // thinking, including aliases mapped to them, get the same treatment. + // Resolved here — not from the pre-factory read — so a // concurrent instance retag cannot leave the level derived from one // type while options/messages are built for the other's wire. const effectiveThinkingLevel: ThinkingLevel = resolveEffectiveThinkingLevel( diff --git a/src/node/utils/providerRequirements.test.ts b/src/node/utils/providerRequirements.test.ts index 37607d7a317..6fb8ce9c874 100644 --- a/src/node/utils/providerRequirements.test.ts +++ b/src/node/utils/providerRequirements.test.ts @@ -139,6 +139,16 @@ describe("hasAnyConfiguredProvider", () => { }); }); +describe("resolveProviderCredentials - Z.ai", () => { + it("resolves ZAI_API_KEY from the environment", () => { + const result = resolveProviderCredentials("zai", {}, { ZAI_API_KEY: "zai-test-key" }); + + expect(result.isConfigured).toBe(true); + expect(result.apiKey).toBe("zai-test-key"); + expect(result.apiKeySource).toBe("env"); + }); +}); + describe("resolveProviderCredentials - legacy op:// references", () => { it("falls back to the env key when config holds a legacy op:// reference", () => { const result = resolveProviderCredentials( diff --git a/src/node/utils/providerRequirements.ts b/src/node/utils/providerRequirements.ts index b5ae9cbcb52..af26dccbdd0 100644 --- a/src/node/utils/providerRequirements.ts +++ b/src/node/utils/providerRequirements.ts @@ -69,6 +69,9 @@ export const PROVIDER_ENV_VARS: Partial< moonshotai: { apiKey: ["MOONSHOT_API_KEY"], }, + zai: { + apiKey: ["ZAI_API_KEY"], + }, "github-copilot": { apiKey: ["GITHUB_COPILOT_TOKEN"], }, From 82e96fa0e39f860869c72711c7bf67f9c267ce53 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:48:44 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=F0=9F=A4=96=20list=20Z.ai=20in=20s?= =?UTF-8?q?upported=20providers;=20update=20flake=20offline-cache=20hash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/config/providers.mdx | 1 + flake.nix | 2 +- src/node/services/agentSkills/builtInSkillContent.generated.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/config/providers.mdx b/docs/config/providers.mdx index 51d01e2c669..1eedf8219f8 100644 --- a/docs/config/providers.mdx +++ b/docs/config/providers.mdx @@ -24,6 +24,7 @@ Most providers only need an API key. The UI handles validation and shows which p | **xAI** | Grok | [console.x.ai](https://console.x.ai/) | | **DeepSeek** | DeepSeek Chat, Reasoner | [platform.deepseek.com](https://platform.deepseek.com/) | | **Moonshot AI** | Kimi K3 | [platform.moonshot.ai](https://platform.moonshot.ai/) | +| **Z.ai** | GLM 5.3 Flash | [z.ai](https://z.ai/model-api) | | **OpenRouter** | 300+ models | [openrouter.ai](https://openrouter.ai/) | | **Ollama** | Local models | [ollama.com](https://ollama.com/) (no key needed) | | **Bedrock** | Claude via AWS | AWS Console | diff --git a/flake.nix b/flake.nix index 6cbecf07453..1e3cfd0291f 100644 --- a/flake.nix +++ b/flake.nix @@ -84,7 +84,7 @@ outputHashMode = "recursive"; # Marker used by scripts/update_flake_hash.sh to update this hash in place. - outputHash = "sha256-ri3Q1gY4ifnjT9FMg3wNqoIT5OJbfQRKAWw5Zt8DH9k="; # xum-offline-cache-hash + outputHash = "sha256-qZmS1EPW+0IU6S8cQGuj+wjHTgBRB0HhLkM0cJDgoc8="; # xum-offline-cache-hash }; configurePhase = '' diff --git a/src/node/services/agentSkills/builtInSkillContent.generated.ts b/src/node/services/agentSkills/builtInSkillContent.generated.ts index 48e3587fabe..6c87cd129be 100644 --- a/src/node/services/agentSkills/builtInSkillContent.generated.ts +++ b/src/node/services/agentSkills/builtInSkillContent.generated.ts @@ -4301,6 +4301,7 @@ export const BUILTIN_SKILL_FILES: Record> = { "| **xAI** | Grok | [console.x.ai](https://console.x.ai/) |", "| **DeepSeek** | DeepSeek Chat, Reasoner | [platform.deepseek.com](https://platform.deepseek.com/) |", "| **Moonshot AI** | Kimi K3 | [platform.moonshot.ai](https://platform.moonshot.ai/) |", + "| **Z.ai** | GLM 5.3 Flash | [z.ai](https://z.ai/model-api) |", "| **OpenRouter** | 300+ models | [openrouter.ai](https://openrouter.ai/) |", "| **Ollama** | Local models | [ollama.com](https://ollama.com/) (no key needed) |", "| **Bedrock** | Claude via AWS | AWS Console |",