Skip to content
Merged
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
11 changes: 11 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/config/models.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 */}

Expand Down
2 changes: 2 additions & 0 deletions docs/config/providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -45,6 +46,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` |
Comment thread
ibetitsmike marked this conversation as resolved.
| github-copilot | `GITHUB_COPILOT_TOKEN` |
| Bedrock | `AWS_REGION` (credentials via AWS SDK chain) |

Expand Down
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ''
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions scripts/gen_docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
xai: "xAI",
deepseek: "DeepSeek",
moonshotai: "Moonshot AI",
zai: "Z.ai",
openrouter: "OpenRouter",
bedrock: "Bedrock",
};
Expand Down
3 changes: 3 additions & 0 deletions src/browser/assets/icons/zai.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/browser/components/ProviderIcon/ProviderIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -30,6 +31,7 @@ const PROVIDER_ICONS: Partial<Record<ProviderName, React.FC>> = {
xai: XAIIcon,
deepseek: DeepSeekIcon,
moonshotai: MoonshotAIIcon,
zai: ZaiIcon,
openrouter: OpenRouterIcon,
bedrock: AWSIcon,
ollama: OllamaIcon,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ const PROVIDER_KEY_URLS: Partial<Record<ProviderName, string>> = {
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
Expand Down
2 changes: 2 additions & 0 deletions src/common/config/schemas/providersConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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(),
Expand Down
8 changes: 8 additions & 0 deletions src/common/constants/knownModels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();
const seenAliases = new Set<string>();
Expand Down
9 changes: 8 additions & 1 deletion src/common/constants/knownModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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<string, KnownModelDefinition>;

export type KnownModelKey = keyof typeof MODEL_DEFINITIONS;
Expand Down
9 changes: 9 additions & 0 deletions src/common/constants/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type ProviderName =
| "xai"
| "deepseek"
| "moonshotai"
| "zai"
| "openrouter"
| "github-copilot"
| "coder"
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/common/types/thinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/common/utils/ai/modelCapabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions src/common/utils/ai/modelDisplay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
6 changes: 6 additions & 0 deletions src/common/utils/ai/modelDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("-");
Expand Down
22 changes: 22 additions & 0 deletions src/common/utils/ai/providerOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions src/common/utils/ai/providerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -28,6 +29,7 @@ import {
getOpenAIReasoningEffort,
isGrok46Model,
isGrokFrontierModel,
isGlm53Model,
isKimiK3Model,
openaiSupportsProMode,
OPENROUTER_REASONING_EFFORT,
Expand Down Expand Up @@ -147,6 +149,7 @@ type ProviderOptions =
| { google: GoogleGenerativeAIProviderOptions }
| { openrouter: OpenRouterReasoningOptions }
| { moonshotai: MoonshotAIProviderOptions }
| { zai: ZaiLanguageModelChatOptions }
| { xai: XaiBuiltProviderOptions }
| { "github-copilot": OpenAICompatibleGatewayProviderOptions }
| Record<string, never>; // Empty object for unsupported providers
Expand Down Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion src/common/utils/thinking/policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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([
Expand Down
Loading
Loading