diff --git a/.changeset/zai-chat-completions-adapter.md b/.changeset/zai-chat-completions-adapter.md
new file mode 100644
index 0000000000..e0069fa67d
--- /dev/null
+++ b/.changeset/zai-chat-completions-adapter.md
@@ -0,0 +1,7 @@
+---
+'@tanstack/ai-zai': minor
+---
+
+Add the Z.AI adapter. It drives Z.AI's OpenAI-compatible Chat Completions API through `@tanstack/openai-base` and ships the GLM-5 and GLM-4 model catalog (`glm-5.3`, `glm-5.3-flash`, and `glm-5.2` first), `createZAIText`/`zaiText`, `createZAISummarize`/`zaiSummarize`, the branded `zaiWebSearchTool`, and a `zaiByok` descriptor at `@tanstack/ai-zai/byok` so a browser-supplied key wins over `ZAI_API_KEY`.
+
+The adapter sets the coding-plan base URL when `coding: true` is passed. It sends `json_object` plus a schema system prompt because Z.AI rejects `json_schema`. Z.AI accepts a `tool_choice` of `auto` only, so `toolChoice: 'none'` sends no tools and `'required'` or a named tool warns once and falls back to `auto`. It streams `delta.reasoning_content` as AG-UI reasoning events.
diff --git a/docs/adapters/zai.md b/docs/adapters/zai.md
new file mode 100644
index 0000000000..613684a452
--- /dev/null
+++ b/docs/adapters/zai.md
@@ -0,0 +1,426 @@
+---
+title: "Z.AI"
+id: zai-adapter
+order: 9
+description: "Use Z.AI GLM models in TanStack AI with @tanstack/ai-zai: streaming chat, tool calling, web search, reasoning effort, vision, and summarization over the OpenAI-compatible Chat Completions API."
+keywords:
+ - tanstack ai
+ - zai
+ - z.ai
+ - glm
+ - glm-5.3
+ - adapter
+ - reasoning
+ - web search
+---
+
+The Z.AI adapter calls the GLM chat models. It uses Z.AI's OpenAI-compatible
+Chat Completions endpoint.
+
+## 1. Install
+
+
+
+react: @tanstack/ai-zai
+vue: @tanstack/ai-zai
+solid: @tanstack/ai-zai
+svelte: @tanstack/ai-zai
+preact: @tanstack/ai-zai
+angular: @tanstack/ai-zai
+vanilla: @tanstack/ai-zai
+octane: @tanstack/ai-zai
+
+
+
+## 2. Set the API key
+
+Put the key in the environment of your server:
+
+```bash
+ZAI_API_KEY=...
+```
+
+`zaiText` reads `ZAI_API_KEY`. To pass the key in code, use `createZAIText`:
+
+```typescript
+import { createZAIText } from "@tanstack/ai-zai";
+
+const adapter = createZAIText("glm-5.3", process.env.ZAI_API_KEY!);
+```
+
+## 3. Stream a chat from the server
+
+```typescript
+import {
+ chat,
+ chatParamsFromRequest,
+ toServerSentEventsResponse,
+} from "@tanstack/ai";
+import { zaiText } from "@tanstack/ai-zai";
+
+export async function POST(request: Request) {
+ const { messages, threadId, runId } = await chatParamsFromRequest(request);
+
+ const stream = chat({
+ adapter: zaiText("glm-5.3"),
+ messages,
+ threadId,
+ runId,
+ });
+
+ return toServerSentEventsResponse(stream);
+}
+```
+
+## 4. Render the chat
+
+The client is the same for every provider. Point `useChat` at your route:
+
+```tsx
+import { useState } from "react";
+import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
+
+export function Chat() {
+ const [input, setInput] = useState("");
+ const { messages, sendMessage } = useChat({
+ connection: fetchServerSentEvents("/api/chat"),
+ threadId: "zai-chat",
+ });
+
+ return (
+
+ );
+}
+```
+
+For Vue, Solid, Svelte, Angular, and the other frameworks, see
+[Quick Start](../getting-started/quick-start#3-render-the-chat).
+
+You now have a streaming chat on Z.AI. [Tools](../tools/tools) and
+[Structured Outputs](../structured-outputs/overview) work the same as with
+every adapter. For the Z.AI details, see
+[Formats the endpoint rejects](#formats-the-endpoint-rejects).
+
+## Models and options
+
+### Model options
+
+Provider options go in `modelOptions`. The sampling options are `temperature`,
+`top_p`, `seed`, `stop`, `frequency_penalty`, `presence_penalty`, and
+`max_tokens`:
+
+```typescript
+import { chat } from "@tanstack/ai";
+import { zaiText } from "@tanstack/ai-zai";
+
+const stream = chat({
+ adapter: zaiText("glm-5.3"),
+ messages: [{ role: "user", content: "Hello!" }],
+ threadId: "thread-1",
+ modelOptions: {
+ temperature: 0.7,
+ max_tokens: 1024,
+ },
+});
+```
+
+Sampling options do not go at the root of `chat()`. See
+[Moving Sampling Options into modelOptions](../migration/sampling-options-to-model-options).
+
+### Reasoning
+
+Set how hard a reasoning model thinks with `reasoning` on `chat()`:
+
+```typescript
+import { chat } from "@tanstack/ai";
+import { zaiText } from "@tanstack/ai-zai";
+
+const stream = chat({
+ adapter: zaiText("glm-5.3"),
+ messages: [{ role: "user", content: "Hello!" }],
+ threadId: "thread-1",
+ reasoning: "high",
+});
+```
+
+The adapter sends the level as `reasoning_effort`. It clamps a level the model
+does not have to the nearest one that it does:
+
+- `glm-5.2`: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`.
+- `glm-5.3` and `glm-5.3-flash`: `low`, `high`, `max`. A lower level becomes
+ `low`.
+
+GLM-4.7 and GLM-4.6 have no `reasoning_effort`. Turn their thinking on with the
+provider option instead:
+
+```typescript
+import { chat } from "@tanstack/ai";
+import { zaiText } from "@tanstack/ai-zai";
+
+const stream = chat({
+ adapter: zaiText("glm-4.7"),
+ messages: [{ role: "user", content: "Hello!" }],
+ threadId: "thread-1",
+ modelOptions: { thinking: { type: "enabled", clear_thinking: false } },
+});
+```
+
+`clear_thinking: false` keeps the reasoning of earlier turns. The thinking text
+arrives as `delta.reasoning_content`, and the adapter emits it as AG-UI
+reasoning events.
+
+### Structured output
+
+`chat({ outputSchema })` works on every model in the catalog. Z.AI rejects
+`response_format: { type: "json_schema" }`, so the adapter sends JSON mode and
+adds your schema to the system prompt. It then parses the reply with your
+schema. Read
+[When strict JSON schema is rejected](../structured-outputs/overview.md#when-strict-json-schema-is-rejected).
+Without `stream: true`, `chat()` resolves to the parsed object, so await it:
+
+```typescript
+import { chat } from "@tanstack/ai";
+import { zaiText } from "@tanstack/ai-zai";
+import { z } from "zod";
+
+const orderSchema = z.object({
+ status: z.enum(["open", "closed"]),
+ total: z.number(),
+});
+
+const order = await chat({
+ adapter: zaiText("glm-5.3"),
+ messages: [{ role: "user", content: "Summarize my open orders" }],
+ threadId: "thread-1",
+ outputSchema: orderSchema,
+});
+```
+
+### Tools
+
+Function tools work on every model. Z.AI accepts a `tool_choice` of `auto`
+only, so the adapter cannot force a call. With `toolChoice: 'none'` it sends
+no tools at all, which gives you the text-only answer you asked for. With
+`'required'` or a named tool it warns once and sends `auto`, so the model
+decides. See [Choose when the model calls a tool](../tools/tools#choose-when-the-model-calls-a-tool).
+
+Z.AI also runs a search on the provider side. Add it with `zaiWebSearchTool`:
+
+```typescript
+import { chat } from "@tanstack/ai";
+import { zaiText, zaiWebSearchTool } from "@tanstack/ai-zai";
+
+const stream = chat({
+ adapter: zaiText("glm-5.3"),
+ messages: [{ role: "user", content: "What is new in TanStack AI?" }],
+ threadId: "thread-1",
+ tools: [zaiWebSearchTool({ search_result: true })],
+});
+```
+
+GLM-4.7 and newer can stream tool-call arguments as the model writes them. Set
+`tool_stream: true` in `modelOptions`.
+
+### Images and video
+
+The vision models take `image` and `video` content parts:
+
+```typescript
+import { chat } from "@tanstack/ai";
+import { zaiText } from "@tanstack/ai-zai";
+
+const stream = chat({
+ adapter: zaiText("glm-5.3-flash"),
+ messages: [
+ {
+ role: "user",
+ content: [
+ { type: "text", content: "What is in this image?" },
+ { type: "image", source: { type: "url", value: "https://example.com/guitar.jpg" } },
+ ],
+ },
+ ],
+ threadId: "thread-1",
+});
+```
+
+Vision models: `glm-5.3-flash`, `glm-5v-turbo`, `glm-4.6v`.
+A video source must be an HTTP(S) URL. Z.AI has no base64 video input, so a
+`data` source throws before the request.
+`adapter.inputModalities` lists the input kinds of the selected model. See
+[Check what a model accepts](../advanced/extend-adapter#check-what-a-model-accepts).
+
+### Summarization
+
+```typescript
+import { summarize } from "@tanstack/ai";
+import { zaiSummarize } from "@tanstack/ai-zai";
+
+const result = await summarize({
+ adapter: zaiSummarize("glm-5.3"),
+ text: "Your long text to summarize...",
+ threadId: "thread-1",
+ maxLength: 100,
+ style: "concise", // "concise" | "bullet-points" | "paragraph"
+});
+
+console.log(result.summary);
+```
+
+### Coding endpoint
+
+A coding-plan key uses a different host. Pass `coding: true`:
+
+```typescript
+import { zaiText } from "@tanstack/ai-zai";
+
+const adapter = zaiText("glm-5.3", { coding: true });
+```
+
+That selects `https://api.z.ai/api/coding/paas/v4`. A `baseURL` you set wins.
+
+### BYOK
+
+Import `zaiByok` from `@tanstack/ai-zai/byok` and add it to your store:
+
+```typescript
+import { defaultByokStorage, defineByok } from "@tanstack/ai-react/byok";
+import { zaiByok } from "@tanstack/ai-zai/byok";
+
+export const byok = defineByok({
+ storage: defaultByokStorage(),
+ providers: [zaiByok],
+});
+```
+
+Registering `zaiByok` is the Z.AI part. Save the key with
+`byok.update("zai", value)` and pass the store to `useChat`, with
+`forwardedProps.provider` set to `"zai"` so the send carries that key and
+nothing else:
+
+```tsx
+import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
+import { byok } from "./byok";
+
+export function Chat() {
+ const { sendMessage } = useChat({
+ connection: fetchServerSentEvents("/api/chat"),
+ threadId: "zai-chat",
+ byok,
+ forwardedProps: { provider: "zai" },
+ });
+
+ return (
+ void sendMessage("Hello")}>
+ Send
+
+ );
+}
+```
+
+Save a key, build a dialog, and use the server fallback in
+[BYOK](../advanced/byok).
+
+On the server, one call reads the key. The `x-byok-zai` header from the browser
+wins over `ZAI_API_KEY`:
+
+```typescript
+import {
+ chat,
+ chatParamsFromRequest,
+ toServerSentEventsResponse,
+} from "@tanstack/ai";
+import { byokMissing, getByokKey } from "@tanstack/ai/byok/server";
+import { createZAIText } from "@tanstack/ai-zai";
+import { zaiByok } from "@tanstack/ai-zai/byok";
+
+export async function POST(request: Request) {
+ const apiKey = getByokKey(request, zaiByok);
+ if (!apiKey) return byokMissing(zaiByok);
+
+ const { messages, threadId, runId } = await chatParamsFromRequest(request);
+
+ const stream = chat({
+ adapter: createZAIText("glm-5.3", apiKey),
+ messages,
+ threadId,
+ runId,
+ });
+
+ return toServerSentEventsResponse(stream);
+}
+```
+
+### Model list
+
+The package exports the list as `ZAI_CHAT_MODELS` and the full metadata,
+including pricing, as `ZAI_MODEL_META`.
+
+| Group | Models |
+| --- | --- |
+| GLM-5.3 | `glm-5.3` (reasoning), `glm-5.3-flash` (reasoning, vision) |
+| GLM-5 | `glm-5.2` (reasoning), `glm-5.1`, `glm-5-turbo`, `glm-5`, `glm-5v-turbo` (vision) |
+| GLM-4 | `glm-4.7`, `glm-4.6`, `glm-4.6v` (vision) |
+
+Every model streams, calls tools, and accepts JSON mode. The largest context
+window is 1,000,000 tokens on `glm-5.3`, `glm-5.3-flash`, and `glm-5.2`.
+
+## Advanced
+
+### Formats the endpoint rejects
+
+The adapter hides two endpoint limits, so you do not hit them by hand:
+
+- `response_format` accepts `json_object` but not `json_schema`. Structured
+ output uses JSON mode plus a schema system prompt.
+- `tool_choice` accepts `auto` only. A named or required choice fails the
+ request.
+
+### Configuration
+
+```typescript
+import { createZAIText, type ZAITextConfig } from "@tanstack/ai-zai";
+
+const config: Omit = {
+ baseURL: "https://api.z.ai/api/paas/v4", // Optional, for a custom endpoint
+ timeout: 60_000,
+ maxRetries: 2,
+};
+
+const adapter = createZAIText("glm-5.3", process.env.ZAI_API_KEY!, config);
+```
+
+The config extends the OpenAI SDK client options, because the adapter drives
+that SDK against the Z.AI base URL. Need a header on each request, or a log of
+each model call? This adapter supports
+[`wrapFetch`](../advanced/middleware#change-the-http-requests-of-a-call).
+
+### API reference
+
+Each factory pair has the same shape. The short factory reads `ZAI_API_KEY`
+from the environment. The `create*` factory takes an explicit API key. Both
+take `model` as the first argument.
+
+| Factories | Creates |
+| --- | --- |
+| `zaiText(model, config?)` / `createZAIText(model, apiKey, config?)` | Chat adapter |
+| `zaiSummarize(model, config?)` / `createZAISummarize(model, apiKey, config?)` | Summarization adapter |
+| `zaiWebSearchTool(config?)` | Provider tool: Z.AI runs the web search |
+
+`zaiByok` at `@tanstack/ai-zai/byok` is the BYOK descriptor for the key dialog
+and the relay header.
diff --git a/docs/config.json b/docs/config.json
index 5ebdf1256b..c1d76581f9 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -1887,6 +1887,11 @@
"label": "BytePlus: Advanced",
"to": "adapters/byteplus-advanced",
"addedAt": "2026-10-21"
+ },
+ {
+ "label": "Z.AI",
+ "to": "adapters/zai",
+ "addedAt": "2026-10-30"
}
],
"tab": "adapters"
diff --git a/examples/ts-code-mode-web/.env.example b/examples/ts-code-mode-web/.env.example
index 33c2ca8eae..3da8a967c5 100644
--- a/examples/ts-code-mode-web/.env.example
+++ b/examples/ts-code-mode-web/.env.example
@@ -7,3 +7,4 @@ GITHUB_TOKEN=ghp_your_token_here
ANTHROPIC_API_KEY=sk-ant-your_key_here
OPENAI_API_KEY=sk-your_key_here
GEMINI_API_KEY=your_key_here
+ZAI_API_KEY=
diff --git a/examples/ts-code-mode-web/package.json b/examples/ts-code-mode-web/package.json
index 970f1fa451..3e3ca30e27 100644
--- a/examples/ts-code-mode-web/package.json
+++ b/examples/ts-code-mode-web/package.json
@@ -32,6 +32,7 @@
"@tanstack/ai-ollama": "workspace:*",
"@tanstack/ai-openai": "workspace:*",
"@tanstack/ai-react": "workspace:*",
+ "@tanstack/ai-zai": "workspace:*",
"@tanstack/react-router": "^1.170.41",
"@tanstack/react-start": "^1.168.60",
"@tanstack/router-plugin": "^1.168.42",
diff --git a/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts b/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts
index 3139c57b15..9106016c86 100644
--- a/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts
+++ b/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts
@@ -4,6 +4,7 @@ import { createCodeMode } from '@tanstack/ai-code-mode'
import { anthropicText } from '@tanstack/ai-anthropic'
import { openaiText } from '@tanstack/ai-openai'
import { geminiText } from '@tanstack/ai-gemini'
+import { zaiText } from '@tanstack/ai-zai'
import type { AnyTextAdapter } from '@tanstack/ai'
import { maxTokensModelOptions } from '@/lib/max-tokens-model-options'
@@ -12,7 +13,7 @@ import { CODE_MODE_SYSTEM_PROMPT } from '@/lib/prompts'
import { reportTools } from '@/lib/reports/tools'
import { createReportBindings } from '@/lib/reports/create-report-bindings'
-type Provider = 'anthropic' | 'openai' | 'gemini'
+type Provider = 'anthropic' | 'openai' | 'gemini' | 'zai'
function getAdapter(provider: Provider, model?: string): AnyTextAdapter {
switch (provider) {
@@ -20,6 +21,8 @@ function getAdapter(provider: Provider, model?: string): AnyTextAdapter {
return openaiText((model || 'gpt-4o') as 'gpt-4o')
case 'gemini':
return geminiText((model || 'gemini-2.5-flash') as 'gemini-2.5-flash')
+ case 'zai':
+ return zaiText((model || 'glm-4.7') as 'glm-4.7')
case 'anthropic':
default:
return anthropicText((model || 'claude-haiku-4-5') as 'claude-haiku-4-5')
diff --git a/examples/ts-code-mode-web/src/routes/_banking-demo/banking-demo.tsx b/examples/ts-code-mode-web/src/routes/_banking-demo/banking-demo.tsx
index 54bdcbeca5..7e27b69e51 100644
--- a/examples/ts-code-mode-web/src/routes/_banking-demo/banking-demo.tsx
+++ b/examples/ts-code-mode-web/src/routes/_banking-demo/banking-demo.tsx
@@ -32,7 +32,7 @@ export const Route = createFileRoute('/_banking-demo/banking-demo')({
component: BankingDemoPage,
})
-type Provider = 'anthropic' | 'openai' | 'gemini'
+type Provider = 'anthropic' | 'openai' | 'gemini' | 'zai'
interface ModelOption {
provider: Provider
@@ -255,6 +255,8 @@ const MODEL_OPTIONS: Array = [
},
{ provider: 'openai', model: 'gpt-4o', label: 'GPT-4o' },
{ provider: 'gemini', model: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
+ { provider: 'zai', model: 'glm-4.7', label: 'Z.AI GLM-4.7' },
+ { provider: 'zai', model: 'glm-5-turbo', label: 'Z.AI GLM-5 Turbo' },
]
interface ToastItem {
diff --git a/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts b/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts
index 67b0a1bd7f..ca77d9e141 100644
--- a/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts
+++ b/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts
@@ -13,13 +13,14 @@ import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storag
import { anthropicText } from '@tanstack/ai-anthropic'
import { openaiText } from '@tanstack/ai-openai'
import { geminiText } from '@tanstack/ai-gemini'
+import { zaiText } from '@tanstack/ai-zai'
import type { AnyServerTool, AnyTextAdapter, StreamChunk } from '@tanstack/ai'
import type { IsolateDriver } from '@tanstack/ai-code-mode'
import { databaseTools, getSchemaInfoTool } from '@/lib/tools/database-tools'
import { maxTokensModelOptions } from '@/lib/max-tokens-model-options'
-type Provider = 'anthropic' | 'openai' | 'gemini'
+type Provider = 'anthropic' | 'openai' | 'gemini' | 'zai'
function getAdapter(provider: Provider, model?: string): AnyTextAdapter {
switch (provider) {
@@ -27,6 +28,8 @@ function getAdapter(provider: Provider, model?: string): AnyTextAdapter {
return openaiText((model || 'gpt-4o') as 'gpt-4o')
case 'gemini':
return geminiText((model || 'gemini-2.5-flash') as 'gemini-2.5-flash')
+ case 'zai':
+ return zaiText((model || 'glm-4.7') as 'glm-4.7')
case 'anthropic':
default:
return anthropicText((model || 'claude-haiku-4-5') as 'claude-haiku-4-5')
diff --git a/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx b/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx
index 4b54dca060..1f2532ac2f 100644
--- a/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx
+++ b/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx
@@ -29,7 +29,7 @@ export const Route = createFileRoute('/_database-demo/database-demo')({
component: DatabaseDemoPage,
})
-type Provider = 'anthropic' | 'openai' | 'gemini'
+type Provider = 'anthropic' | 'openai' | 'gemini' | 'zai'
interface ModelOption {
provider: Provider
@@ -49,11 +49,9 @@ const MODEL_OPTIONS: Array = [
label: 'Claude Haiku 4',
},
{ provider: 'openai', model: 'gpt-4o', label: 'GPT-4o' },
- {
- provider: 'gemini',
- model: 'gemini-2.5-flash',
- label: 'Gemini 2.5 Flash',
- },
+ { provider: 'gemini', model: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
+ { provider: 'zai', model: 'glm-4.7', label: 'Z.AI GLM-4.7' },
+ { provider: 'zai', model: 'glm-5-turbo', label: 'Z.AI GLM-5 Turbo' },
]
interface SnippetWithCode {
diff --git a/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts b/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts
index 6efb62fe43..9bbce202a9 100644
--- a/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts
+++ b/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts
@@ -6,6 +6,7 @@ import { createCodeMode } from '@tanstack/ai-code-mode'
import { anthropicText } from '@tanstack/ai-anthropic'
import { openaiText } from '@tanstack/ai-openai'
import { geminiText } from '@tanstack/ai-gemini'
+import { zaiText } from '@tanstack/ai-zai'
import {
createAlwaysTrustedStrategy,
createSnippetManagementTools,
@@ -18,7 +19,7 @@ import type { AnyServerTool, AnyTextAdapter, StreamChunk } from '@tanstack/ai'
import type { IsolateDriver } from '@tanstack/ai-code-mode'
import { productTools } from '@/lib/tools/product-tools'
-type Provider = 'anthropic' | 'openai' | 'gemini'
+type Provider = 'anthropic' | 'openai' | 'gemini' | 'zai'
function getAdapter(provider: Provider, model?: string): AnyTextAdapter {
switch (provider) {
@@ -26,6 +27,8 @@ function getAdapter(provider: Provider, model?: string): AnyTextAdapter {
return openaiText((model || 'gpt-4o') as 'gpt-4o')
case 'gemini':
return geminiText((model || 'gemini-2.5-flash') as 'gemini-2.5-flash')
+ case 'zai':
+ return zaiText((model || 'glm-4.7') as 'glm-4.7')
case 'anthropic':
default:
return anthropicText((model || 'claude-haiku-4-5') as 'claude-haiku-4-5')
diff --git a/examples/ts-code-mode-web/src/routes/_home/api.product-regular.ts b/examples/ts-code-mode-web/src/routes/_home/api.product-regular.ts
index 219e3aaf40..829ef7f807 100644
--- a/examples/ts-code-mode-web/src/routes/_home/api.product-regular.ts
+++ b/examples/ts-code-mode-web/src/routes/_home/api.product-regular.ts
@@ -3,11 +3,12 @@ import { chat, maxIterations, toServerSentEventsStream } from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { openaiText } from '@tanstack/ai-openai'
import { geminiText } from '@tanstack/ai-gemini'
+import { zaiText } from '@tanstack/ai-zai'
import type { AnyTextAdapter, StreamChunk } from '@tanstack/ai'
import { productTools } from '@/lib/tools/product-tools'
import { maxTokensModelOptions } from '@/lib/max-tokens-model-options'
-type Provider = 'anthropic' | 'openai' | 'gemini'
+type Provider = 'anthropic' | 'openai' | 'gemini' | 'zai'
function getAdapter(provider: Provider, model?: string): AnyTextAdapter {
switch (provider) {
@@ -15,6 +16,8 @@ function getAdapter(provider: Provider, model?: string): AnyTextAdapter {
return openaiText((model || 'gpt-4o') as 'gpt-4o')
case 'gemini':
return geminiText((model || 'gemini-2.5-flash') as 'gemini-2.5-flash')
+ case 'zai':
+ return zaiText((model || 'glm-4.7') as 'glm-4.7')
case 'anthropic':
default:
return anthropicText((model || 'claude-haiku-4-5') as 'claude-haiku-4-5')
diff --git a/examples/ts-code-mode-web/src/routes/_home/index.tsx b/examples/ts-code-mode-web/src/routes/_home/index.tsx
index cf6e1098c0..d31a46190b 100644
--- a/examples/ts-code-mode-web/src/routes/_home/index.tsx
+++ b/examples/ts-code-mode-web/src/routes/_home/index.tsx
@@ -24,7 +24,7 @@ export const Route = createFileRoute('/_home/')({
component: ProductDemoPage,
})
-type Provider = 'anthropic' | 'openai' | 'gemini'
+type Provider = 'anthropic' | 'openai' | 'gemini' | 'zai'
interface ModelOption {
provider: Provider
@@ -59,6 +59,8 @@ const MODEL_OPTIONS: Array = [
model: 'gemini-2.5-flash',
label: 'Gemini 2.5 Flash',
},
+ { provider: 'zai', model: 'glm-4.7', label: 'Z.AI GLM-4.7' },
+ { provider: 'zai', model: 'glm-5-turbo', label: 'Z.AI GLM-5 Turbo' },
]
const PROMPT_SUGGESTIONS = [
diff --git a/examples/ts-code-mode-web/src/routes/_npm-github-chat/api.codemode.ts b/examples/ts-code-mode-web/src/routes/_npm-github-chat/api.codemode.ts
index 40a68b7ec9..aba52d8219 100644
--- a/examples/ts-code-mode-web/src/routes/_npm-github-chat/api.codemode.ts
+++ b/examples/ts-code-mode-web/src/routes/_npm-github-chat/api.codemode.ts
@@ -4,13 +4,14 @@ import { createCodeMode } from '@tanstack/ai-code-mode'
import { anthropicText } from '@tanstack/ai-anthropic'
import { openaiText } from '@tanstack/ai-openai'
import { geminiText } from '@tanstack/ai-gemini'
+import { zaiText } from '@tanstack/ai-zai'
import type { AnyTextAdapter, StreamChunk } from '@tanstack/ai'
import { allTools } from '@/lib/tools'
import { CODE_MODE_SYSTEM_PROMPT } from '@/lib/prompts'
import { exportConversationToPdfTool } from '@/lib/tools/export-pdf-tool'
import { maxTokensModelOptions } from '@/lib/max-tokens-model-options'
-type Provider = 'anthropic' | 'openai' | 'gemini'
+type Provider = 'anthropic' | 'openai' | 'gemini' | 'zai'
function getAdapter(provider: Provider, model?: string): AnyTextAdapter {
switch (provider) {
@@ -18,6 +19,8 @@ function getAdapter(provider: Provider, model?: string): AnyTextAdapter {
return openaiText((model || 'gpt-4o') as 'gpt-4o')
case 'gemini':
return geminiText((model || 'gemini-2.5-flash') as 'gemini-2.5-flash')
+ case 'zai':
+ return zaiText((model || 'glm-4.7') as 'glm-4.7')
case 'anthropic':
default:
return anthropicText((model || 'claude-haiku-4-5') as 'claude-haiku-4-5')
diff --git a/examples/ts-code-mode-web/src/routes/_npm-github-chat/npm-github-chat.tsx b/examples/ts-code-mode-web/src/routes/_npm-github-chat/npm-github-chat.tsx
index 3a511a3918..56c5d7e135 100644
--- a/examples/ts-code-mode-web/src/routes/_npm-github-chat/npm-github-chat.tsx
+++ b/examples/ts-code-mode-web/src/routes/_npm-github-chat/npm-github-chat.tsx
@@ -34,7 +34,7 @@ export const Route = createFileRoute('/_npm-github-chat/npm-github-chat')({
component: CodeModePage,
})
-type Provider = 'anthropic' | 'openai' | 'gemini'
+type Provider = 'anthropic' | 'openai' | 'gemini' | 'zai'
interface ModelOption {
provider: Provider
@@ -55,6 +55,8 @@ const MODEL_OPTIONS: Array = [
},
{ provider: 'openai', model: 'gpt-4o', label: 'GPT-4o' },
{ provider: 'gemini', model: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
+ { provider: 'zai', model: 'glm-4.7', label: 'Z.AI GLM-4.7' },
+ { provider: 'zai', model: 'glm-5-turbo', label: 'Z.AI GLM-5 Turbo' },
]
const PROMPT_SUGGESTIONS = [
diff --git a/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts b/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts
index 1179da218f..b46c4723ab 100644
--- a/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts
+++ b/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts
@@ -4,6 +4,7 @@ import { createCodeMode } from '@tanstack/ai-code-mode'
import { anthropicText } from '@tanstack/ai-anthropic'
import { openaiText } from '@tanstack/ai-openai'
import { geminiText } from '@tanstack/ai-gemini'
+import { zaiText } from '@tanstack/ai-zai'
import type { AnyTextAdapter } from '@tanstack/ai'
import { allTools } from '@/lib/tools'
@@ -12,7 +13,7 @@ import { reportTools } from '@/lib/reports/tools'
import { createReportBindings } from '@/lib/reports/create-report-bindings'
import { maxTokensModelOptions } from '@/lib/max-tokens-model-options'
-type Provider = 'anthropic' | 'openai' | 'gemini'
+type Provider = 'anthropic' | 'openai' | 'gemini' | 'zai'
function getAdapter(provider: Provider, model?: string): AnyTextAdapter {
switch (provider) {
@@ -20,6 +21,8 @@ function getAdapter(provider: Provider, model?: string): AnyTextAdapter {
return openaiText((model || 'gpt-4o') as 'gpt-4o')
case 'gemini':
return geminiText((model || 'gemini-2.5-flash') as 'gemini-2.5-flash')
+ case 'zai':
+ return zaiText((model || 'glm-4.7') as 'glm-4.7')
case 'anthropic':
default:
return anthropicText((model || 'claude-haiku-4-5') as 'claude-haiku-4-5')
diff --git a/examples/ts-code-mode-web/src/routes/_reporting/reporting-agent.tsx b/examples/ts-code-mode-web/src/routes/_reporting/reporting-agent.tsx
index f8e4a996b7..a2ad5f7233 100644
--- a/examples/ts-code-mode-web/src/routes/_reporting/reporting-agent.tsx
+++ b/examples/ts-code-mode-web/src/routes/_reporting/reporting-agent.tsx
@@ -32,7 +32,7 @@ export const Route = createFileRoute('/_reporting/reporting-agent')({
component: ReportingAgentPage,
})
-type Provider = 'anthropic' | 'openai' | 'gemini'
+type Provider = 'anthropic' | 'openai' | 'gemini' | 'zai'
interface ModelOption {
provider: Provider
diff --git a/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output.ts b/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output.ts
index 3cea32624c..57ac8e3dd5 100644
--- a/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output.ts
+++ b/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output.ts
@@ -7,6 +7,7 @@ import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storag
import { anthropicText } from '@tanstack/ai-anthropic'
import { openaiText } from '@tanstack/ai-openai'
import { geminiText } from '@tanstack/ai-gemini'
+import { zaiText } from '@tanstack/ai-zai'
import { z } from 'zod'
import type { AnyTextAdapter } from '@tanstack/ai'
@@ -15,7 +16,7 @@ import type { IsolateDriver } from '@tanstack/ai-code-mode'
import { cityTools } from '@/lib/tools/city-tools'
import { structuredOutput } from '@/lib/structured-output'
-type Provider = 'anthropic' | 'openai' | 'gemini'
+type Provider = 'anthropic' | 'openai' | 'gemini' | 'zai'
const TravelReportSchema = z.object({
title: z.string().describe('Short title for the report'),
@@ -45,6 +46,8 @@ function getAdapter(provider: Provider, model?: string): AnyTextAdapter {
return openaiText((model || 'gpt-4o') as 'gpt-4o')
case 'gemini':
return geminiText((model || 'gemini-2.5-flash') as 'gemini-2.5-flash')
+ case 'zai':
+ return zaiText((model || 'glm-4.7') as 'glm-4.7')
case 'anthropic':
default:
return anthropicText(
diff --git a/examples/ts-react-chat/package.json b/examples/ts-react-chat/package.json
index 13ddbcc526..e3a605f28c 100644
--- a/examples/ts-react-chat/package.json
+++ b/examples/ts-react-chat/package.json
@@ -49,6 +49,7 @@
"@tanstack/ai-sandbox-local-process": "workspace:*",
"@tanstack/ai-sandbox-railway": "workspace:*",
"@tanstack/ai-sandbox-vercel": "workspace:*",
+ "@tanstack/ai-zai": "workspace:*",
"@tanstack/react-devtools": "^0.9.10",
"@tanstack/react-router": "^1.170.41",
"@tanstack/react-router-devtools": "^1.158.4",
diff --git a/examples/ts-react-chat/src/lib/byok.ts b/examples/ts-react-chat/src/lib/byok.ts
index 418931bc8c..7e49879d9c 100644
--- a/examples/ts-react-chat/src/lib/byok.ts
+++ b/examples/ts-react-chat/src/lib/byok.ts
@@ -10,6 +10,7 @@ import { grokByok } from '@tanstack/ai-grok/byok'
import { groqByok } from '@tanstack/ai-groq/byok'
import { openaiByok } from '@tanstack/ai-openai/byok'
import { openrouterByok } from '@tanstack/ai-openrouter/byok'
+import { zaiByok } from '@tanstack/ai-zai/byok'
import { createServerFn } from '@tanstack/react-start'
import type { ProviderId } from '@tanstack/ai/byok'
import type { Provider } from '@/lib/model-selection'
@@ -25,6 +26,7 @@ export const KEYED_PROVIDERS = [
byteplusVoiceByok,
cloudflareByok,
cloudflareAccountByok,
+ zaiByok,
] as const
/**
diff --git a/examples/ts-react-chat/src/lib/model-selection.ts b/examples/ts-react-chat/src/lib/model-selection.ts
index bf175dce10..1b5ec5dee7 100644
--- a/examples/ts-react-chat/src/lib/model-selection.ts
+++ b/examples/ts-react-chat/src/lib/model-selection.ts
@@ -10,6 +10,7 @@ export type Provider =
| 'bedrock'
| 'byteplus'
| 'cloudflare'
+ | 'zai'
export interface ModelOption {
provider: Provider
@@ -56,6 +57,12 @@ export const MODEL_OPTIONS: Array = [
label: 'Cloudflare Gateway - GPT-5.5',
},
+ // Z.AI direct API. BYOK key from the browser, ZAI_API_KEY in .env as fallback.
+ { provider: 'zai', model: 'glm-5.3', label: 'Z.AI - GLM-5.3' },
+ { provider: 'zai', model: 'glm-5.3-flash', label: 'Z.AI - GLM-5.3 Flash' },
+ { provider: 'zai', model: 'glm-5.2', label: 'Z.AI - GLM-5.2' },
+ { provider: 'zai', model: 'glm-4.7', label: 'Z.AI - GLM-4.7' },
+
// Anthropic
{
provider: 'anthropic',
diff --git a/examples/ts-react-chat/src/routes/api.tanchat.ts b/examples/ts-react-chat/src/routes/api.tanchat.ts
index 3f05b9ab37..c7a79bc23e 100644
--- a/examples/ts-react-chat/src/routes/api.tanchat.ts
+++ b/examples/ts-react-chat/src/routes/api.tanchat.ts
@@ -30,6 +30,8 @@ import {
import { BEDROCK_CONVERSE_MODELS, bedrockText } from '@tanstack/ai-bedrock'
import { BYTEPLUS_CHAT_MODELS, createBytePlusText } from '@tanstack/ai-byteplus'
import { byteplusByok } from '@tanstack/ai-byteplus/byok'
+import { createZAIText, ZAI_CHAT_MODELS } from '@tanstack/ai-zai'
+import { zaiByok } from '@tanstack/ai-zai/byok'
import { byokMissing, getByokKey } from '@tanstack/ai/byok/server'
import { z } from 'zod'
import type { AnyTextAdapter, ChatMiddleware } from '@tanstack/ai'
@@ -104,6 +106,10 @@ const modelSelectionSchema = z
provider: z.literal('cloudflare'),
model: z.string().catch('@cf/zai-org/glm-5.3-flash'),
}),
+ z.object({
+ provider: z.literal('zai'),
+ model: z.enum(ZAI_CHAT_MODELS).catch('glm-5.3'),
+ }),
])
.catch({ provider: 'openai', model: 'gpt-5.2' })
@@ -119,6 +125,7 @@ const BYOK_PROVIDERS: Partial> = {
grok: grokByok,
byteplus: byteplusByok,
cloudflare: cloudflareByok,
+ zai: zaiByok,
}
function chatByokProvider(provider: Provider): ByokProvider | undefined {
@@ -445,6 +452,13 @@ export const Route = createFileRoute('/api/tanchat')({
modelOptions: { options: { top_k: 1 } },
reasoning: 'low',
})
+ case 'zai':
+ return createChatOptions({
+ threadId: params.threadId,
+ // Z.AI direct: the browser key wins, ZAI_API_KEY in .env is the
+ // server fallback.
+ adapter: createZAIText(model, requireApiKey(apiKey)),
+ })
case 'openai':
return createChatOptions({
threadId: params.threadId,
diff --git a/examples/ts-react-search/.env.example b/examples/ts-react-search/.env.example
new file mode 100644
index 0000000000..9b674287cd
--- /dev/null
+++ b/examples/ts-react-search/.env.example
@@ -0,0 +1 @@
+ZAI_API_KEY=
diff --git a/examples/ts-react-search/package.json b/examples/ts-react-search/package.json
index 2c377f7b1a..23cc59fe57 100644
--- a/examples/ts-react-search/package.json
+++ b/examples/ts-react-search/package.json
@@ -13,8 +13,8 @@
"@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/ai": "workspace:*",
- "@tanstack/ai-groq": "workspace:*",
"@tanstack/ai-react": "workspace:*",
+ "@tanstack/ai-zai": "workspace:*",
"@tanstack/query-db-collection": "^1.0.6",
"@tanstack/react-db": "^0.1.55",
"@tanstack/react-devtools": "^0.9.10",
diff --git a/examples/ts-react-search/src/routes/api/search.ts b/examples/ts-react-search/src/routes/api/search.ts
index 6e65b722c7..dd987c8895 100644
--- a/examples/ts-react-search/src/routes/api/search.ts
+++ b/examples/ts-react-search/src/routes/api/search.ts
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { chat } from '@tanstack/ai'
-import { groqText } from '@tanstack/ai-groq'
+import { zaiText } from '@tanstack/ai-zai'
import z from 'zod'
import {
ORDER_STATUS_MAP,
@@ -11,7 +11,6 @@ import {
DISPUTE_STATUS_MAP,
} from '@/features/disputes/constants'
import { SETTLEMENT_CURRENCY_MAP } from '@/features/settlements/constants'
-import toGroqCompatibleSchema from '@/utils/toGroqCompatibleSchema'
const ORDER_STATUS_KEYS = Object.keys(ORDER_STATUS_MAP)
const PAYMENT_METHOD_KEYS = Object.keys(PAYMENT_METHOD_MAP)
@@ -51,10 +50,6 @@ const outputSchema = z.object({
]),
})
-// Only needed for Groq, since it doesn't support additionalProperties:false on anyOf (union types)
-// Otherwise just use the outputSchema directly
-const groqOutputSchema = toGroqCompatibleSchema(z.toJSONSchema(outputSchema))
-
const SYSTEM_PROMPT = `
JSON API: Convert prompts to structured data. No prose, fences, or comments.
@@ -75,10 +70,10 @@ export const Route = createFileRoute('/api/search')({
server: {
handlers: {
POST: async ({ request }) => {
- if (!process.env.GROQ_API_KEY) {
+ if (!process.env.ZAI_API_KEY) {
return new Response(
JSON.stringify({
- error: 'GROQ_API_KEY not configured',
+ error: 'ZAI_API_KEY not configured',
}),
{
status: 500,
@@ -91,11 +86,11 @@ export const Route = createFileRoute('/api/search')({
try {
const response = await chat({
- adapter: groqText('openai/gpt-oss-20b'),
+ adapter: zaiText('glm-5.3'),
threadId: 'search',
messages: [{ role: 'user', content }],
systemPrompts: [SYSTEM_PROMPT],
- outputSchema: groqOutputSchema,
+ outputSchema,
})
return new Response(JSON.stringify(response), {
diff --git a/examples/ts-react-search/src/utils/toGroqCompatibleSchema.ts b/examples/ts-react-search/src/utils/toGroqCompatibleSchema.ts
deleted file mode 100644
index f6aeb775e9..0000000000
--- a/examples/ts-react-search/src/utils/toGroqCompatibleSchema.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-// Groq structured output requires additionalProperties:false on every object, including
-// those nested inside anyOf. The @tanstack/ai-groq adapter only recurses into direct
-// object properties, not anyOf/oneOf/allOf, so we pre-process the JSON Schema here.
-function toGroqCompatibleSchema(
- schema: Record,
-): Record {
- if (typeof schema !== 'object' || Array.isArray(schema)) return schema
- const result = { ...schema }
-
- for (const combiner of ['anyOf', 'oneOf', 'allOf']) {
- if (Array.isArray(result[combiner])) {
- result[combiner] = result[combiner].map((s: any) =>
- toGroqCompatibleSchema(s),
- )
- }
- }
-
- if (result.$defs) {
- result.$defs = Object.fromEntries(
- Object.entries(result.$defs).map(([k, v]) => [
- k,
- toGroqCompatibleSchema(v as any),
- ]),
- )
- }
-
- if (result.type === 'object') {
- const properties = Object.fromEntries(
- Object.entries(result.properties ?? {}).map(([k, v]) => [
- k,
- toGroqCompatibleSchema(v as any),
- ]),
- )
- result.properties = properties
- result.required = Object.keys(properties)
- result.additionalProperties = false
- }
-
- if (result.type === 'array' && result.items) {
- result.items = toGroqCompatibleSchema(result.items)
- }
-
- return result
-}
-
-export default toGroqCompatibleSchema
diff --git a/examples/ts-solid-chat/.env.example b/examples/ts-solid-chat/.env.example
index 613cb664b2..b8ea691f2c 100644
--- a/examples/ts-solid-chat/.env.example
+++ b/examples/ts-solid-chat/.env.example
@@ -1,3 +1,18 @@
# OpenAI API Key
# Get yours at: https://platform.openai.com/api-keys
-OPENAI_API_KEY=sk-...
\ No newline at end of file
+OPENAI_API_KEY=sk-...
+
+# Anthropic API Key
+# Get yours at: https://console.anthropic.com/
+ANTHROPIC_API_KEY=sk-ant-...
+
+# Gemini API Key
+# Get yours at: https://aistudio.google.com/
+GEMINI_API_KEY=...
+
+# Z.AI API Key
+# Get yours at: https://open.bigmodel.cn/
+ZAI_API_KEY=...
+
+# Ollama (local, no API key needed)
+# Make sure Ollama is running: ollama serve
\ No newline at end of file
diff --git a/examples/ts-solid-chat/package.json b/examples/ts-solid-chat/package.json
index ebdeb83aac..9d848a5ed5 100644
--- a/examples/ts-solid-chat/package.json
+++ b/examples/ts-solid-chat/package.json
@@ -16,9 +16,13 @@
"@tanstack/ai-client": "workspace:*",
"@tanstack/ai-devtools-core": "workspace:*",
"@tanstack/ai-gemini": "workspace:*",
+ "@tanstack/ai-grok": "workspace:*",
+ "@tanstack/ai-groq": "workspace:*",
"@tanstack/ai-ollama": "workspace:*",
"@tanstack/ai-openai": "workspace:*",
+ "@tanstack/ai-openrouter": "workspace:*",
"@tanstack/ai-solid": "workspace:*",
+ "@tanstack/ai-zai": "workspace:*",
"@tanstack/nitro-v2-vite-plugin": "^1.155.0",
"@tanstack/router-plugin": "^1.168.42",
"@tanstack/solid-ai-devtools": "workspace:*",
diff --git a/examples/ts-solid-chat/src/lib/model-selection.ts b/examples/ts-solid-chat/src/lib/model-selection.ts
new file mode 100644
index 0000000000..e8d994cc15
--- /dev/null
+++ b/examples/ts-solid-chat/src/lib/model-selection.ts
@@ -0,0 +1,183 @@
+export type Provider =
+ | 'openai'
+ | 'anthropic'
+ | 'gemini'
+ | 'ollama'
+ | 'grok'
+ | 'groq'
+ | 'openrouter'
+ | 'zai'
+
+export interface ModelOption {
+ provider: Provider
+ model: string
+ label: string
+}
+
+export const MODEL_OPTIONS: Array = [
+ { provider: 'openai', model: 'gpt-4o', label: 'OpenAI - GPT-4o' },
+ { provider: 'openai', model: 'gpt-4o-mini', label: 'OpenAI - GPT-4o Mini' },
+ { provider: 'openai', model: 'gpt-5', label: 'OpenAI - GPT-5' },
+
+ {
+ provider: 'anthropic',
+ model: 'claude-sonnet-4-6',
+ label: 'Anthropic - Claude Sonnet 4.6',
+ },
+ {
+ provider: 'anthropic',
+ model: 'claude-sonnet-4-5-20250929',
+ label: 'Anthropic - Claude Sonnet 4.5',
+ },
+ {
+ provider: 'anthropic',
+ model: 'claude-opus-4-5-20251101',
+ label: 'Anthropic - Claude Opus 4.5',
+ },
+ {
+ provider: 'anthropic',
+ model: 'claude-haiku-4-0-20250514',
+ label: 'Anthropic - Claude Haiku 4.0',
+ },
+
+ {
+ provider: 'gemini',
+ model: 'gemini-2.0-flash',
+ label: 'Gemini - 2.0 Flash',
+ },
+ {
+ provider: 'gemini',
+ model: 'gemini-2.5-flash',
+ label: 'Gemini - 2.5 Flash',
+ },
+ {
+ provider: 'gemini',
+ model: 'gemini-2.5-pro',
+ label: 'Gemini - 2.5 Pro',
+ },
+
+ {
+ provider: 'ollama',
+ model: 'mistral:7b',
+ label: 'Ollama - Mistral 7B',
+ },
+ {
+ provider: 'ollama',
+ model: 'mistral',
+ label: 'Ollama - Mistral',
+ },
+ {
+ provider: 'ollama',
+ model: 'gpt-oss:20b',
+ label: 'Ollama - GPT-OSS 20B',
+ },
+ {
+ provider: 'ollama',
+ model: 'granite4:3b',
+ label: 'Ollama - Granite4 3B',
+ },
+ {
+ provider: 'ollama',
+ model: 'smollm',
+ label: 'Ollama - SmolLM',
+ },
+
+ { provider: 'zai', model: 'glm-5.3', label: 'Z.AI - GLM-5.3' },
+ { provider: 'zai', model: 'glm-5.3-flash', label: 'Z.AI - GLM-5.3 Flash' },
+ { provider: 'zai', model: 'glm-5.2', label: 'Z.AI - GLM-5.2' },
+ { provider: 'zai', model: 'glm-5.1', label: 'Z.AI - GLM-5.1' },
+ { provider: 'zai', model: 'glm-5-turbo', label: 'Z.AI - GLM-5 Turbo' },
+ { provider: 'zai', model: 'glm-5', label: 'Z.AI - GLM-5' },
+ { provider: 'zai', model: 'glm-5v-turbo', label: 'Z.AI - GLM-5V Turbo' },
+ { provider: 'zai', model: 'glm-4.7', label: 'Z.AI - GLM-4.7' },
+ { provider: 'zai', model: 'glm-4.6v', label: 'Z.AI - GLM-4.6V' },
+ { provider: 'zai', model: 'glm-4.6', label: 'Z.AI - GLM-4.6' },
+
+ {
+ provider: 'openrouter',
+ model: 'openai/chatgpt-4o-latest',
+ label: 'Openrouter - ChatGPT 4o Latest',
+ },
+ {
+ provider: 'openrouter',
+ model: 'openai/chatgpt-4o-mini',
+ label: 'Openrouter - ChatGPT 4o Mini',
+ },
+
+ {
+ provider: 'groq',
+ model: 'llama-3.3-70b-versatile',
+ label: 'Groq - Llama 3.3 70B',
+ },
+ {
+ provider: 'groq',
+ model: 'meta-llama/llama-4-maverick-17b-128e-instruct',
+ label: 'Groq - Llama 4 Maverick',
+ },
+ {
+ provider: 'groq',
+ model: 'meta-llama/llama-4-scout-17b-16e-instruct',
+ label: 'Groq - Llama 4 Scout',
+ },
+
+ {
+ provider: 'grok',
+ model: 'grok-4',
+ label: 'Grok - Grok 4',
+ },
+ {
+ provider: 'grok',
+ model: 'grok-4-fast-non-reasoning',
+ label: 'Grok - Grok 4 Fast',
+ },
+ {
+ provider: 'grok',
+ model: 'grok-3',
+ label: 'Grok - Grok 3',
+ },
+ {
+ provider: 'grok',
+ model: 'grok-3-mini',
+ label: 'Grok - Grok 3 Mini',
+ },
+]
+
+export const DEFAULT_MODEL_OPTION = MODEL_OPTIONS[0]
+
+const STORAGE_KEY = 'tanstack-ai-model-preference'
+
+export function getStoredModelPreference(): ModelOption | null {
+ if (typeof window === 'undefined') return null
+
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY)
+ if (!stored) return null
+
+ const parsed = JSON.parse(stored) as { provider: Provider; model: string }
+ const option = MODEL_OPTIONS.find(
+ (opt) => opt.provider === parsed.provider && opt.model === parsed.model,
+ )
+
+ return option || null
+ } catch {
+ return null
+ }
+}
+
+export function setStoredModelPreference(option: ModelOption): void {
+ if (typeof window === 'undefined') return
+
+ try {
+ localStorage.setItem(
+ STORAGE_KEY,
+ JSON.stringify({ provider: option.provider, model: option.model }),
+ )
+ } catch {
+ return
+ }
+}
+
+export function getDefaultModelOption(): ModelOption {
+ const stored = getStoredModelPreference()
+ return stored || MODEL_OPTIONS[0]
+}
diff --git a/examples/ts-solid-chat/src/routes/api.chat.ts b/examples/ts-solid-chat/src/routes/api.chat.ts
index 29bb834637..e77d777dc2 100644
--- a/examples/ts-solid-chat/src/routes/api.chat.ts
+++ b/examples/ts-solid-chat/src/routes/api.chat.ts
@@ -2,12 +2,22 @@ import { createFileRoute } from '@tanstack/solid-router'
import {
chat,
chatParamsFromRequestBody,
+ createChatOptions,
maxIterations,
mergeAgentTools,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'
+import { geminiText } from '@tanstack/ai-gemini'
+import { openRouterText } from '@tanstack/ai-openrouter'
+import { grokText } from '@tanstack/ai-grok'
+import { groqText } from '@tanstack/ai-groq'
+import { ollamaText } from '@tanstack/ai-ollama'
+import { openaiText } from '@tanstack/ai-openai'
+import { zaiText } from '@tanstack/ai-zai'
import { serverTools } from '@/lib/guitar-tools'
+import type { Provider } from '@/lib/model-selection'
+import type { AnyTextAdapter } from '@tanstack/ai'
const SYSTEM_PROMPT = `You are a helpful assistant for a guitar store.
@@ -32,23 +42,51 @@ Step 2: Call recommendGuitar(id: "6")
Step 3: Done - do NOT add any text after calling recommendGuitar
`
+const createProviderAdapters = (
+ model: string,
+): Record { adapter: AnyTextAdapter }> => ({
+ anthropic: () =>
+ createChatOptions({
+ adapter: anthropicText(
+ (model || 'claude-sonnet-4-5') as 'claude-sonnet-4-5',
+ ),
+ }),
+ openrouter: () =>
+ createChatOptions({
+ adapter: openRouterText('openai/gpt-5.1'),
+ }),
+ gemini: () =>
+ createChatOptions({
+ adapter: geminiText((model || 'gemini-2.5-flash') as 'gemini-2.5-flash'),
+ }),
+ grok: () =>
+ createChatOptions({
+ adapter: grokText((model || 'grok-build-0.1') as 'grok-build-0.1'),
+ }),
+ groq: () =>
+ createChatOptions({
+ adapter: groqText(
+ (model || 'openai/gpt-oss-120b') as 'openai/gpt-oss-120b',
+ ),
+ }),
+ ollama: () =>
+ createChatOptions({
+ adapter: ollamaText((model || 'gpt-oss:20b') as 'gpt-oss:20b'),
+ }),
+ openai: () =>
+ createChatOptions({
+ adapter: openaiText((model || 'gpt-4o') as 'gpt-4o'),
+ }),
+ zai: () =>
+ createChatOptions({
+ adapter: zaiText((model || 'glm-5.3') as 'glm-5.3'),
+ }),
+})
+
export const Route = createFileRoute('/api/chat')({
server: {
handlers: {
POST: async ({ request }) => {
- if (!process.env.ANTHROPIC_API_KEY) {
- return new Response(
- JSON.stringify({
- error:
- 'ANTHROPIC_API_KEY not configured. Please add it to .env or .env.local',
- }),
- {
- status: 500,
- headers: { 'Content-Type': 'application/json' },
- },
- )
- }
-
// Capture request signal before reading body (it may be aborted after body is consumed)
const requestSignal = request.signal
@@ -70,17 +108,28 @@ export const Route = createFileRoute('/api/chat')({
}
try {
+ const model =
+ typeof params.forwardedProps.model === 'string'
+ ? params.forwardedProps.model
+ : ''
+ const providerAdapters = createProviderAdapters(model)
+ const provider: Provider =
+ typeof params.forwardedProps.provider === 'string' &&
+ Object.hasOwn(providerAdapters, params.forwardedProps.provider)
+ ? (params.forwardedProps.provider as Provider)
+ : 'openai'
+
const mergedTools = mergeAgentTools(serverTools, params.tools)
- // Use the stream abort signal for proper cancellation handling
+ const options = providerAdapters[provider]()
+
const stream = chat({
- adapter: anthropicText('claude-sonnet-4-5'),
+ ...options,
tools: mergedTools,
systemPrompts: [SYSTEM_PROMPT],
agentLoopStrategy: maxIterations(20),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
- reasoning: { level: 'high', budgetTokens: 10000 },
abortController,
})
diff --git a/examples/ts-solid-chat/src/routes/index.tsx b/examples/ts-solid-chat/src/routes/index.tsx
index b7bacf1bd1..91cbd8308f 100644
--- a/examples/ts-solid-chat/src/routes/index.tsx
+++ b/examples/ts-solid-chat/src/routes/index.tsx
@@ -4,7 +4,7 @@ import Square from 'lucide-solid/icons/square'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-solid'
import { createChatClientOptions } from '@tanstack/ai-client'
import { TextPart, ThinkingPart } from '@tanstack/ai-solid/ui'
-import { For, createSignal } from 'solid-js'
+import { For, createSignal, onMount } from 'solid-js'
import type {
BoundInterrupts,
@@ -15,6 +15,13 @@ import type {
import type { JSXElement } from 'solid-js'
import GuitarRecommendation from '@/components/example-GuitarRecommendation'
import { clientTools } from '@/lib/guitar-tools'
+import {
+ DEFAULT_MODEL_OPTION,
+ MODEL_OPTIONS,
+ getDefaultModelOption,
+ setStoredModelPreference,
+ type ModelOption,
+} from '@/lib/model-selection'
// Create typed chat options for type inference
const chatOptions = createChatClientOptions({
@@ -298,6 +305,20 @@ function DebugPanel(props: {
function ChatPage() {
const [chunks, setChunks] = createSignal>([])
+ const [selectedModel, setSelectedModel] =
+ createSignal(DEFAULT_MODEL_OPTION)
+
+ onMount(() => setSelectedModel(getDefaultModelOption()))
+
+ const chooseModel = (option: ModelOption) => {
+ setSelectedModel(option)
+ setStoredModelPreference(option)
+ }
+
+ const bodyData = () => ({
+ provider: selectedModel().provider,
+ model: selectedModel().model,
+ })
const { messages, sendMessage, isLoading, interrupts, stop } = useChat({
...chatOptions,
@@ -313,10 +334,39 @@ function ChatPage() {
{/* Left side - Chat (1/4 width) */}
-
-
+
+
TanStack AI on SolidJS
+
+
+
+ Select Model:
+
+
+ opt.provider === selectedModel().provider &&
+ opt.model === selectedModel().model,
+ )}
+ onChange={(e) => {
+ chooseModel(MODEL_OPTIONS[parseInt(e.currentTarget.value)])
+ }}
+ disabled={isLoading()}
+ class="w-full rounded-lg border border-orange-500/20 bg-gray-900 px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-orange-500/50 disabled:opacity-50"
+ >
+
+ {(option, index) => (
+ {option.label}
+ )}
+
+
+
+
@@ -352,7 +402,7 @@ function ChatPage() {
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && input().trim()) {
e.preventDefault()
- sendMessage(input())
+ sendMessage(input(), { body: bodyData() })
setInput('')
}
}}
@@ -360,7 +410,7 @@ function ChatPage() {
{
if (input().trim()) {
- sendMessage(input())
+ sendMessage(input(), { body: bodyData() })
setInput('')
}
}}
diff --git a/examples/ts-svelte-chat/.env.example b/examples/ts-svelte-chat/.env.example
new file mode 100644
index 0000000000..9b674287cd
--- /dev/null
+++ b/examples/ts-svelte-chat/.env.example
@@ -0,0 +1 @@
+ZAI_API_KEY=
diff --git a/examples/ts-svelte-chat/package.json b/examples/ts-svelte-chat/package.json
index 80a3e08237..aedc32d9bc 100644
--- a/examples/ts-svelte-chat/package.json
+++ b/examples/ts-svelte-chat/package.json
@@ -20,6 +20,7 @@
"@tanstack/ai-ollama": "workspace:*",
"@tanstack/ai-openai": "workspace:*",
"@tanstack/ai-svelte": "workspace:*",
+ "@tanstack/ai-zai": "workspace:*",
"highlight.js": "^11.11.1",
"lucide-svelte": "^0.468.0",
"marked": "^15.0.6",
diff --git a/examples/ts-svelte-chat/src/lib/model-selection.ts b/examples/ts-svelte-chat/src/lib/model-selection.ts
index b8c58ffd7f..abc9743e78 100644
--- a/examples/ts-svelte-chat/src/lib/model-selection.ts
+++ b/examples/ts-svelte-chat/src/lib/model-selection.ts
@@ -1,4 +1,4 @@
-export type Provider = 'openai' | 'anthropic' | 'gemini' | 'ollama'
+export type Provider = 'openai' | 'anthropic' | 'gemini' | 'ollama' | 'zai'
export interface ModelOption {
provider: Provider
@@ -76,6 +76,18 @@ export const MODEL_OPTIONS: Array = [
model: 'smollm',
label: 'Ollama - SmolLM',
},
+
+ // Z.AI (GLM)
+ { provider: 'zai', model: 'glm-5.3', label: 'Z.AI - GLM-5.3' },
+ { provider: 'zai', model: 'glm-5.3-flash', label: 'Z.AI - GLM-5.3 Flash' },
+ { provider: 'zai', model: 'glm-5.2', label: 'Z.AI - GLM-5.2' },
+ { provider: 'zai', model: 'glm-5.1', label: 'Z.AI - GLM-5.1' },
+ { provider: 'zai', model: 'glm-5-turbo', label: 'Z.AI - GLM-5 Turbo' },
+ { provider: 'zai', model: 'glm-5', label: 'Z.AI - GLM-5' },
+ { provider: 'zai', model: 'glm-5v-turbo', label: 'Z.AI - GLM-5V Turbo' },
+ { provider: 'zai', model: 'glm-4.7', label: 'Z.AI - GLM-4.7' },
+ { provider: 'zai', model: 'glm-4.6v', label: 'Z.AI - GLM-4.6V' },
+ { provider: 'zai', model: 'glm-4.6', label: 'Z.AI - GLM-4.6' },
]
const STORAGE_KEY = 'tanstack-ai-model-preference'
diff --git a/examples/ts-svelte-chat/src/routes/api/chat/+server.ts b/examples/ts-svelte-chat/src/routes/api/chat/+server.ts
index ef57f87382..0e9219e60e 100644
--- a/examples/ts-svelte-chat/src/routes/api/chat/+server.ts
+++ b/examples/ts-svelte-chat/src/routes/api/chat/+server.ts
@@ -10,10 +10,10 @@ import { openaiText } from '@tanstack/ai-openai'
import { ollamaText } from '@tanstack/ai-ollama'
import { anthropicText } from '@tanstack/ai-anthropic'
import { geminiText } from '@tanstack/ai-gemini'
+import { zaiText } from '@tanstack/ai-zai'
import type { RequestHandler } from './$types'
import { env } from '$env/dynamic/private'
-
import {
addToCartToolDef,
addToWishListToolDef,
@@ -22,13 +22,14 @@ import {
recommendGuitarToolDef,
} from '$lib/guitar-tools'
-type Provider = 'openai' | 'anthropic' | 'gemini' | 'ollama'
+type Provider = 'openai' | 'anthropic' | 'gemini' | 'ollama' | 'zai'
// Populate process.env with the SvelteKit environment variables
// This is needed because the TanStack AI adapters read from process.env
if (env.OPENAI_API_KEY) process.env.OPENAI_API_KEY = env.OPENAI_API_KEY
if (env.ANTHROPIC_API_KEY) process.env.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY
if (env.GEMINI_API_KEY) process.env.GEMINI_API_KEY = env.GEMINI_API_KEY
+if (env.ZAI_API_KEY) process.env.ZAI_API_KEY = env.ZAI_API_KEY
// Pre-define typed adapter configurations with full type inference
// This pattern gives you model autocomplete at definition time
@@ -49,6 +50,10 @@ const adapterConfig = {
createChatOptions({
adapter: openaiText('gpt-4o'),
}),
+ zai: (model?: string) =>
+ createChatOptions({
+ adapter: zaiText((model || 'glm-5.3') as 'glm-5.3'),
+ }),
}
const SYSTEM_PROMPT = `You are a helpful assistant for a guitar store.
@@ -114,9 +119,13 @@ export const POST: RequestHandler = async ({ request }) => {
// Extract provider from forwardedProps (sent by the client)
const provider: Provider =
typeof params.forwardedProps?.provider === 'string' &&
- params.forwardedProps.provider in adapterConfig
+ Object.hasOwn(adapterConfig, params.forwardedProps.provider)
? (params.forwardedProps.provider as Provider)
: 'openai'
+ const model =
+ typeof params.forwardedProps?.model === 'string'
+ ? params.forwardedProps.model
+ : ''
try {
const mergedTools = mergeAgentTools(serverTools, params.tools)
@@ -141,7 +150,9 @@ export const POST: RequestHandler = async ({ request }) => {
? chat({ ...adapterConfig.gemini(), ...sharedOptions })
: provider === 'ollama'
? chat({ ...adapterConfig.ollama(), ...sharedOptions })
- : chat({ ...adapterConfig.openai(), ...sharedOptions })
+ : provider === 'zai'
+ ? chat({ ...adapterConfig.zai(model), ...sharedOptions })
+ : chat({ ...adapterConfig.openai(), ...sharedOptions })
return toServerSentEventsResponse(stream, { abortController })
} catch (error: any) {
diff --git a/examples/ts-vue-chat/.env.example b/examples/ts-vue-chat/.env.example
new file mode 100644
index 0000000000..9b674287cd
--- /dev/null
+++ b/examples/ts-vue-chat/.env.example
@@ -0,0 +1 @@
+ZAI_API_KEY=
diff --git a/examples/ts-vue-chat/package.json b/examples/ts-vue-chat/package.json
index fdc51a7b56..670d5b8fa2 100644
--- a/examples/ts-vue-chat/package.json
+++ b/examples/ts-vue-chat/package.json
@@ -18,6 +18,7 @@
"@tanstack/ai-ollama": "workspace:*",
"@tanstack/ai-openai": "workspace:*",
"@tanstack/ai-vue": "workspace:*",
+ "@tanstack/ai-zai": "workspace:*",
"marked": "^15.0.6",
"vue": "^3.5.25",
"vue-router": "^4.5.0",
diff --git a/examples/ts-vue-chat/src/lib/model-selection.ts b/examples/ts-vue-chat/src/lib/model-selection.ts
index b8c58ffd7f..abc9743e78 100644
--- a/examples/ts-vue-chat/src/lib/model-selection.ts
+++ b/examples/ts-vue-chat/src/lib/model-selection.ts
@@ -1,4 +1,4 @@
-export type Provider = 'openai' | 'anthropic' | 'gemini' | 'ollama'
+export type Provider = 'openai' | 'anthropic' | 'gemini' | 'ollama' | 'zai'
export interface ModelOption {
provider: Provider
@@ -76,6 +76,18 @@ export const MODEL_OPTIONS: Array = [
model: 'smollm',
label: 'Ollama - SmolLM',
},
+
+ // Z.AI (GLM)
+ { provider: 'zai', model: 'glm-5.3', label: 'Z.AI - GLM-5.3' },
+ { provider: 'zai', model: 'glm-5.3-flash', label: 'Z.AI - GLM-5.3 Flash' },
+ { provider: 'zai', model: 'glm-5.2', label: 'Z.AI - GLM-5.2' },
+ { provider: 'zai', model: 'glm-5.1', label: 'Z.AI - GLM-5.1' },
+ { provider: 'zai', model: 'glm-5-turbo', label: 'Z.AI - GLM-5 Turbo' },
+ { provider: 'zai', model: 'glm-5', label: 'Z.AI - GLM-5' },
+ { provider: 'zai', model: 'glm-5v-turbo', label: 'Z.AI - GLM-5V Turbo' },
+ { provider: 'zai', model: 'glm-4.7', label: 'Z.AI - GLM-4.7' },
+ { provider: 'zai', model: 'glm-4.6v', label: 'Z.AI - GLM-4.6V' },
+ { provider: 'zai', model: 'glm-4.6', label: 'Z.AI - GLM-4.6' },
]
const STORAGE_KEY = 'tanstack-ai-model-preference'
diff --git a/examples/ts-vue-chat/vite.config.ts b/examples/ts-vue-chat/vite.config.ts
index 3d6237133d..19818edf96 100644
--- a/examples/ts-vue-chat/vite.config.ts
+++ b/examples/ts-vue-chat/vite.config.ts
@@ -13,6 +13,7 @@ import { openaiText } from '@tanstack/ai-openai'
import { anthropicText } from '@tanstack/ai-anthropic'
import { geminiText } from '@tanstack/ai-gemini'
import { ollamaText } from '@tanstack/ai-ollama'
+import { zaiText } from '@tanstack/ai-zai'
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
import dotenv from 'dotenv'
@@ -181,7 +182,7 @@ IMPORTANT:
- Do NOT describe the guitar yourself - let the recommendGuitar tool do it
`
-type Provider = 'openai' | 'anthropic' | 'gemini' | 'ollama'
+type Provider = 'openai' | 'anthropic' | 'gemini' | 'ollama' | 'zai'
export default defineConfig({
plugins: [
@@ -237,6 +238,10 @@ export default defineConfig({
selectedModel = model || 'mistral:7b'
adapter = ollamaText(selectedModel)
break
+ case 'zai':
+ selectedModel = model || 'glm-4.7'
+ adapter = zaiText(selectedModel)
+ break
case 'openai':
default:
selectedModel = model || 'gpt-4o'
diff --git a/packages/ai-zai/README.md b/packages/ai-zai/README.md
new file mode 100644
index 0000000000..d7ff7dbc88
--- /dev/null
+++ b/packages/ai-zai/README.md
@@ -0,0 +1,322 @@
+
+
+
+
+
+
+
+
+
+
+# @tanstack/ai-zai
+
+Z.AI adapter for TanStack AI. It calls the GLM chat models over Z.AI's
+OpenAI-compatible Chat Completions API.
+
+- Z.AI API docs: https://docs.z.ai/api-reference/llm/chat-completion
+
+## Installation
+
+```bash
+npm install @tanstack/ai-zai
+# or
+pnpm add @tanstack/ai-zai
+# or
+yarn add @tanstack/ai-zai
+```
+
+## Setup
+
+Get a key from https://z.ai/manage-apikey/apikey-list and put it in the
+environment of your server:
+
+```bash
+ZAI_API_KEY=...
+```
+
+`zaiText` reads `ZAI_API_KEY`. To pass the key in code, use `createZAIText`:
+
+```ts
+import { createZAIText } from '@tanstack/ai-zai'
+
+const adapter = createZAIText('glm-5.3', apiKey)
+```
+
+## Usage
+
+### Stream a chat from the server
+
+```ts
+import {
+ chat,
+ chatParamsFromRequest,
+ toServerSentEventsResponse,
+} from '@tanstack/ai'
+import { zaiText } from '@tanstack/ai-zai'
+
+export async function POST(request: Request) {
+ const { messages, threadId, runId } = await chatParamsFromRequest(request)
+
+ const stream = chat({
+ adapter: zaiText('glm-5.3'),
+ messages,
+ threadId,
+ runId,
+ })
+
+ return toServerSentEventsResponse(stream)
+}
+```
+
+The client is the same as for every other provider. See the Groq guide in
+`docs/adapters/groq.md` for the `useChat` half.
+
+### Reasoning
+
+GLM-5.3, GLM-5.3 Flash, and GLM-5.2 accept a reasoning effort. Pass a level to
+`chat()`. The adapter clamps it to the nearest level the model supports, then
+sends it as `reasoning_effort`.
+
+```ts
+const stream = chat({
+ adapter: zaiText('glm-5.3'),
+ messages,
+ reasoning: 'high',
+})
+```
+
+| Model | `reasoning_effort` values |
+| ------------------------------------ | ------------------------------------------------------ |
+| `glm-5.2` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` |
+| `glm-5.3`, `glm-5.3-flash` | `low`, `high`, `max` |
+
+For GLM-4.7 and GLM-4.6, turn thinking on with the provider option instead:
+
+```ts
+const stream = chat({
+ adapter: zaiText('glm-4.7'),
+ messages,
+ modelOptions: { thinking: { type: 'enabled', clear_thinking: false } },
+})
+```
+
+### Structured output
+
+Pass `outputSchema`. Z.AI rejects `response_format: { type: 'json_schema' }`,
+so the adapter sends JSON mode plus a schema system prompt and validates the
+reply against your schema. Without `stream: true`, `chat()` resolves to the
+parsed object, so await it.
+
+```ts
+import { z } from 'zod'
+
+const orderSchema = z.object({
+ status: z.enum(['open', 'closed']),
+ total: z.number(),
+})
+
+const order = await chat({
+ adapter: zaiText('glm-5.3'),
+ messages,
+ outputSchema: orderSchema,
+})
+```
+
+### Tools
+
+Function tools and Z.AI's built-in web search both work. Z.AI accepts a
+`tool_choice` of `auto` only. `toolChoice: 'none'` sends no tools; `'required'`
+or a named tool warns once and falls back to `auto`.
+
+```ts
+import { zaiText, zaiWebSearchTool } from '@tanstack/ai-zai'
+
+const stream = chat({
+ adapter: zaiText('glm-5.3'),
+ messages,
+ tools: [zaiWebSearchTool({ search_result: true })],
+})
+```
+
+### Images and video
+
+Vision models take `image` and `video` content parts:
+
+```ts
+const stream = chat({
+ adapter: zaiText('glm-5.3-flash'),
+ messages: [
+ {
+ role: 'user',
+ content: [
+ { type: 'text', content: 'What is in this image?' },
+ { type: 'image', source: { type: 'url', value: imageUrl } },
+ ],
+ },
+ ],
+})
+```
+
+Vision models are `glm-5.3-flash`, `glm-5v-turbo`, and `glm-4.6v`. A video
+source must be an HTTP(S) URL; Z.AI has no base64 video input, so a `data`
+source throws before the request.
+
+### Summarization
+
+```ts
+import { summarize } from '@tanstack/ai'
+import { zaiSummarize } from '@tanstack/ai-zai'
+
+const result = await summarize({
+ adapter: zaiSummarize('glm-5.3'),
+ text: longText,
+ threadId: 'article-summary',
+ style: 'bullet-points',
+ maxLength: 500,
+})
+```
+
+### Coding endpoint
+
+A coding-plan key talks to a different host. Pass `coding: true`:
+
+```ts
+const adapter = zaiText('glm-5.3', { coding: true })
+```
+
+That sets the base URL to `https://api.z.ai/api/coding/paas/v4`. A `baseURL`
+you set yourself wins.
+
+### Bring your own key
+
+`@tanstack/ai-zai/byok` exports a BYOK descriptor. Add it to your store so the
+key dialog shows a Z.AI field:
+
+```ts
+import { defaultByokStorage, defineByok } from '@tanstack/ai-react/byok'
+import { zaiByok } from '@tanstack/ai-zai/byok'
+
+export const byok = defineByok({
+ storage: defaultByokStorage(),
+ providers: [zaiByok],
+})
+```
+
+Pass the store to `useChat` and set `forwardedProps.provider` to `'zai'`, so the
+send carries that key and nothing else:
+
+```tsx
+const { messages, sendMessage } = useChat({
+ connection,
+ threadId: 'zai-chat',
+ byok,
+ forwardedProps: { provider: 'zai' },
+})
+```
+
+Then read the key in the route. The `x-byok-zai` header from the browser wins
+over `ZAI_API_KEY`:
+
+```ts
+import {
+ chat,
+ chatParamsFromRequest,
+ toServerSentEventsResponse,
+} from '@tanstack/ai'
+import { byokMissing, getByokKey } from '@tanstack/ai/byok/server'
+import { createZAIText } from '@tanstack/ai-zai'
+import { zaiByok } from '@tanstack/ai-zai/byok'
+
+export async function POST(request: Request) {
+ const apiKey = getByokKey(request, zaiByok)
+ if (!apiKey) return byokMissing(zaiByok)
+
+ const { messages, threadId, runId } = await chatParamsFromRequest(request)
+
+ return toServerSentEventsResponse(
+ chat({
+ adapter: createZAIText('glm-5.3', apiKey),
+ messages,
+ threadId,
+ runId,
+ }),
+ )
+}
+```
+
+See
+[BYOK](https://tanstack.com/ai/latest/docs/advanced/byok).
+
+## Models
+
+| Model | Context | Max output | Input |
+| --------------- | --------- | ---------- | ------------------ |
+| `glm-5.3` | 1,000,000 | 128,000 | text |
+| `glm-5.3-flash` | 1,000,000 | 128,000 | text, image, video |
+| `glm-5.2` | 1,000,000 | 131,072 | text |
+| `glm-5.1` | 200,000 | 128,000 | text |
+| `glm-5-turbo` | 200,000 | 128,000 | text |
+| `glm-5` | 200,000 | 128,000 | text |
+| `glm-5v-turbo` | 200,000 | 128,000 | text, image, video |
+| `glm-4.7` | 200,000 | 128,000 | text |
+| `glm-4.6v` | 128,000 | 128,000 | text, image, video |
+| `glm-4.6` | 128,000 | 128,000 | text |
+
+Every model in the table streams, calls tools, and accepts JSON mode. Import
+`ZAI_CHAT_MODELS` for the list at runtime and `ZAI_MODEL_META` for the full
+metadata, including pricing.
+
+## Features
+
+- ✅ Streaming chat completions
+- ✅ Function/tool calling
+- ✅ Web search tool (provider runs the search)
+- ✅ Reasoning events (`delta.reasoning_content`)
+- ✅ Structured output (JSON mode plus a schema prompt)
+- ✅ Multimodal input (image and video on vision models)
+- ✅ Summarization (`zaiSummarize`)
+- ❌ Embeddings (Z.AI has none on this endpoint)
+- ❌ Image generation (not part of Chat Completions)
+
+## Configuration
+
+- `ZAI_API_KEY`: read by `zaiText()` and `zaiSummarize()`.
+- `ZAI_API_KEY_TEST`: read by the live integration tests in `tests/`.
+- Base URL: `https://api.z.ai/api/paas/v4` by default,
+ `https://api.z.ai/api/coding/paas/v4` with `coding: true`, or your own
+ `baseURL`.
+
+The config object extends the OpenAI SDK client options, so `timeout`,
+`maxRetries`, and `defaultHeaders` pass straight through.
+
+## Testing
+
+```bash
+pnpm --filter @tanstack/ai-zai test:lib
+```
+
+The unit tests run offline. The live tests in
+`tests/zai-adapter.integration.test.ts` need a real key in
+`ZAI_API_KEY_TEST`.
+
+## Contributing
+
+We welcome issues and pull requests.
+
+- GitHub: https://github.com/TanStack/ai
+- Discussions: https://github.com/TanStack/ai/discussions
+- Contribution guidelines: https://github.com/TanStack/ai/blob/main/CONTRIBUTING.md
+
+## License
+
+MIT © TanStack
diff --git a/packages/ai-zai/package.json b/packages/ai-zai/package.json
new file mode 100644
index 0000000000..d4d2089fa7
--- /dev/null
+++ b/packages/ai-zai/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "@tanstack/ai-zai",
+ "version": "0.1.0",
+ "description": "Z.AI adapter for TanStack AI",
+ "author": "",
+ "license": "MIT",
+ "homepage": "https://tanstack.com/ai",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/TanStack/ai.git",
+ "directory": "packages/ai-zai"
+ },
+ "bugs": {
+ "url": "https://github.com/TanStack/ai/issues"
+ },
+ "type": "module",
+ "module": "./dist/esm/index.js",
+ "types": "./dist/esm/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/esm/index.d.ts",
+ "import": "./dist/esm/index.js"
+ },
+ "./byok": {
+ "types": "./dist/esm/byok.d.ts",
+ "import": "./dist/esm/byok.js"
+ },
+ "./tools": {
+ "types": "./dist/esm/tools/index.d.ts",
+ "import": "./dist/esm/tools/index.js"
+ }
+ },
+ "files": [
+ "dist",
+ "src"
+ ],
+ "scripts": {
+ "build": "vite build",
+ "clean": "premove ./build ./dist",
+ "lint:fix": "oxlint src --type-aware --fix",
+ "test:build": "publint --strict",
+ "test:oxlint": "oxlint src --type-aware",
+ "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary",
+ "test:lib": "vitest run",
+ "test:lib:dev": "pnpm test:lib --watch",
+ "test:types": "tsc"
+ },
+ "keywords": [
+ "ai",
+ "zai",
+ "tanstack",
+ "adapter"
+ ],
+ "dependencies": {
+ "@tanstack/ai-utils": "workspace:^",
+ "@tanstack/openai-base": "workspace:^",
+ "openai": "^7.30.0"
+ },
+ "peerDependencies": {
+ "@tanstack/ai": "workspace:^"
+ },
+ "devDependencies": {
+ "@vitest/coverage-v8": "4.1.10",
+ "vite": "^8.2.1"
+ }
+}
diff --git a/packages/ai-zai/src/adapters/index.ts b/packages/ai-zai/src/adapters/index.ts
new file mode 100644
index 0000000000..628c71d2d8
--- /dev/null
+++ b/packages/ai-zai/src/adapters/index.ts
@@ -0,0 +1,15 @@
+export {
+ ZAITextAdapter,
+ createZAIText,
+ zaiText,
+ type ZAITextConfig,
+ type ZAITextProviderOptions,
+} from './text'
+export {
+ type ZAISummarizeAdapter,
+ createZAISummarize,
+ zaiSummarize,
+ type ZAISummarizeConfig,
+ type ZAISummarizeModel,
+ type ZAISummarizeProviderOptions,
+} from './summarize'
diff --git a/packages/ai-zai/src/adapters/summarize.ts b/packages/ai-zai/src/adapters/summarize.ts
new file mode 100644
index 0000000000..c59ff33c5a
--- /dev/null
+++ b/packages/ai-zai/src/adapters/summarize.ts
@@ -0,0 +1,39 @@
+import { ChatStreamSummarizeAdapter } from '@tanstack/ai/adapters'
+import { getZAIApiKeyFromEnv } from '../utils/client'
+import { ZAITextAdapter } from './text'
+import type { InferTextProviderOptions } from '@tanstack/ai/adapters'
+import type { ZAI_CHAT_MODELS } from '../model-meta'
+import type { ZAITextConfig } from './text'
+
+export interface ZAISummarizeConfig extends ZAITextConfig {}
+
+export type ZAISummarizeModel = (typeof ZAI_CHAT_MODELS)[number]
+
+export type ZAISummarizeProviderOptions = InferTextProviderOptions<
+ ZAITextAdapter
+>
+
+export type ZAISummarizeAdapter =
+ ChatStreamSummarizeAdapter<
+ TModel,
+ InferTextProviderOptions>
+ >
+
+export function createZAISummarize(
+ model: TModel,
+ apiKey: string,
+ config?: Omit,
+): ZAISummarizeAdapter {
+ return new ChatStreamSummarizeAdapter(
+ new ZAITextAdapter({ apiKey, ...config }, model),
+ model,
+ 'zai',
+ )
+}
+
+export function zaiSummarize(
+ model: TModel,
+ config?: Omit,
+): ZAISummarizeAdapter {
+ return createZAISummarize(model, getZAIApiKeyFromEnv(), config)
+}
diff --git a/packages/ai-zai/src/adapters/text.ts b/packages/ai-zai/src/adapters/text.ts
new file mode 100644
index 0000000000..904387aa06
--- /dev/null
+++ b/packages/ai-zai/src/adapters/text.ts
@@ -0,0 +1,208 @@
+import OpenAI from 'openai'
+import { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base'
+import {
+ ZAI_MODEL_INPUT_MODALITIES,
+ ZAI_MODEL_REASONING,
+} from '../model-meta'
+import { convertToolsToProviderFormat } from '../tools/tool-converter'
+import { getZAIApiKeyFromEnv, withZaiDefaults } from '../utils/client'
+import type {
+ ZAI_CHAT_MODELS,
+ ZAIChatModelToolCapabilitiesByName,
+ ZAIModelReasoningByName,
+ ResolveInputModalities,
+ ResolveProviderOptions,
+} from '../model-meta'
+import type { ContentPart, Modality, ModelReasoning, TextOptions } from '@tanstack/ai'
+import type { ZAIMessageMetadataByModality } from '../message-types'
+import type { ZAIClientConfig } from '../utils/client'
+import type { OpenAIBaseTextAdapterOptions } from '@tanstack/openai-base'
+import type {
+ ChatCompletionContentPart,
+ ChatCompletionCreateParamsStreaming,
+} from 'openai/resources/chat/completions/completions'
+
+/** The reasoning levels of a model, for `chat({ reasoning })`. `never`: none. */
+type ResolveReasoning =
+ TModel extends keyof ZAIModelReasoningByName
+ ? ZAIModelReasoningByName[TModel]
+ : never
+
+type ResolveToolCapabilities =
+ TModel extends keyof ZAIChatModelToolCapabilitiesByName
+ ? NonNullable
+ : readonly []
+
+/**
+ * Configuration for the Z.AI text adapter.
+ */
+export interface ZAITextConfig
+ extends ZAIClientConfig, OpenAIBaseTextAdapterOptions {}
+
+/**
+ * Re-export of the public provider options type.
+ */
+export type { ExternalTextProviderOptions as ZAITextProviderOptions } from '../text/text-provider-options'
+
+/**
+ * Z.AI Text (Chat) Adapter
+ *
+ * Z.AI exposes an OpenAI-compatible Chat Completions API, so the adapter
+ * drives it with the OpenAI SDK via a `baseURL` override and delegates the
+ * AG-UI lifecycle to {@link OpenAIBaseChatCompletionsTextAdapter}.
+ *
+ * Z.AI quirks handled here:
+ * - `response_format` rejects `json_schema`, so every model reports
+ * `json_object` mode. The base then sends the schema as a system prompt
+ * plus `response_format: { type: 'json_object' }`.
+ * - `tool_choice` accepts only `"auto"`. A `required` or named choice warns and
+ * falls back to `auto`; `none` drops the tools instead of sending the value.
+ * - Vision models accept `video_url` content parts, which the OpenAI SDK's
+ * content-part union does not model. The URL must be HTTP(S); a base64 video
+ * source throws instead of sending a request Z.AI rejects.
+ */
+export class ZAITextAdapter<
+ TModel extends (typeof ZAI_CHAT_MODELS)[number],
+ TProviderOptions extends Record = ResolveProviderOptions,
+ TInputModalities extends ReadonlyArray =
+ ResolveInputModalities,
+ TToolCapabilities extends ReadonlyArray =
+ ResolveToolCapabilities,
+> extends OpenAIBaseChatCompletionsTextAdapter<
+ TModel,
+ TProviderOptions,
+ TInputModalities,
+ ZAIMessageMetadataByModality,
+ TToolCapabilities,
+ ResolveReasoning
+> {
+ override readonly kind = 'text' as const
+ override readonly name = 'zai' as const
+ override readonly inputModalities = ZAI_MODEL_INPUT_MODALITIES[this.model]
+
+ /** Set after the first warning that Z.AI cannot force a tool call. */
+ private warnedToolChoice = false
+ readonly maxTokensKey = 'max_tokens'
+
+ constructor(config: ZAITextConfig, model: TModel) {
+ super(model, 'zai', new OpenAI(withZaiDefaults(config)), config)
+ }
+
+ protected override modelReasoning(model: string): ModelReasoning | undefined {
+ return ZAI_MODEL_REASONING[model]
+ }
+
+ /** Z.AI accepts `json_object` only; never advertise native `json_schema`. */
+ protected override modelPrefersJsonObject(): boolean {
+ return true
+ }
+
+ protected override mapOptionsToRequest(
+ options: TextOptions,
+ ): ChatCompletionCreateParamsStreaming {
+ const { tools: _baseTools, ...request } = super.mapOptionsToRequest({
+ ...options,
+ tools: undefined,
+ })
+ void _baseTools
+
+ // `none` cannot be sent, so drop the tools instead of letting the model
+ // call them anyway.
+ const tools =
+ options.tools && options.toolChoice !== 'none'
+ ? convertToolsToProviderFormat(options.tools)
+ : undefined
+
+ if (
+ tools &&
+ options.toolChoice &&
+ options.toolChoice !== 'auto' &&
+ !this.warnedToolChoice
+ ) {
+ this.warnedToolChoice = true
+ options.logger.warn(
+ "zai: Z.AI accepts tool_choice 'auto' only. The model decides which tools to call.",
+ )
+ }
+
+ return {
+ ...request,
+ ...(tools &&
+ tools.length > 0 && {
+ tools: tools as ChatCompletionCreateParamsStreaming['tools'],
+ // Z.AI only accepts `auto`; `required` and named choices 400.
+ tool_choice: 'auto' as const,
+ }),
+ }
+ }
+
+ protected override convertContentPart(
+ part: ContentPart,
+ ): ChatCompletionContentPart | null {
+ if (part.type === 'video') {
+ if (part.source.type === 'data') {
+ throw new Error(
+ 'zai: Z.AI video input needs an HTTP(S) URL. Base64 video is not supported.',
+ )
+ }
+ // oxlint-disable-next-line eslint-js/no-restricted-syntax -- Z.AI's video_url part is absent from the OpenAI SDK's content-part union
+ return {
+ type: 'video_url',
+ video_url: { url: part.source.value },
+ } as unknown as ChatCompletionContentPart
+ }
+ return super.convertContentPart(part)
+ }
+
+ /**
+ * Z.AI streams thinking as `delta.reasoning_content`.
+ */
+ protected override extractReasoning(
+ chunk: unknown,
+ ): { text: string } | undefined {
+ const delta = (
+ chunk as {
+ choices?: ReadonlyArray<{
+ delta?: { reasoning_content?: unknown }
+ }>
+ }
+ ).choices?.[0]?.delta
+ const raw = delta?.reasoning_content
+ return typeof raw === 'string' && raw.length > 0
+ ? { text: raw }
+ : undefined
+ }
+}
+
+/**
+ * Creates a Z.AI text adapter with an explicit API key.
+ *
+ * @example
+ * ```typescript
+ * const adapter = createZAIText('glm-5.2', 'your-zai-api-key')
+ * ```
+ */
+export function createZAIText<
+ TModel extends (typeof ZAI_CHAT_MODELS)[number],
+>(
+ model: TModel,
+ apiKey: string,
+ config?: Omit,
+): ZAITextAdapter {
+ return new ZAITextAdapter({ apiKey, ...config }, model)
+}
+
+/**
+ * Creates a Z.AI text adapter with the API key from `ZAI_API_KEY`.
+ *
+ * @example
+ * ```typescript
+ * const adapter = zaiText('glm-5.2')
+ * ```
+ */
+export function zaiText(
+ model: TModel,
+ config?: Omit,
+): ZAITextAdapter {
+ return createZAIText(model, getZAIApiKeyFromEnv(), config)
+}
diff --git a/packages/ai-zai/src/byok.ts b/packages/ai-zai/src/byok.ts
new file mode 100644
index 0000000000..863562e749
--- /dev/null
+++ b/packages/ai-zai/src/byok.ts
@@ -0,0 +1,7 @@
+import { defineByokProvider } from '@tanstack/ai/byok'
+
+export const zaiByok = defineByokProvider({
+ id: 'zai',
+ label: 'Z.AI',
+ env: 'ZAI_API_KEY',
+})
diff --git a/packages/ai-zai/src/index.ts b/packages/ai-zai/src/index.ts
new file mode 100644
index 0000000000..51f5b67031
--- /dev/null
+++ b/packages/ai-zai/src/index.ts
@@ -0,0 +1,42 @@
+// Text (Chat) adapter
+export {
+ ZAITextAdapter,
+ createZAIText,
+ zaiText,
+ type ZAITextConfig,
+ type ZAITextProviderOptions,
+} from './adapters/index'
+
+// Summarize adapter
+export {
+ type ZAISummarizeAdapter,
+ createZAISummarize,
+ zaiSummarize,
+ type ZAISummarizeConfig,
+ type ZAISummarizeModel,
+ type ZAISummarizeProviderOptions,
+} from './adapters/index'
+
+// Endpoint constants
+export { ZAI_GENERAL_BASE_URL, ZAI_CODING_BASE_URL } from './utils/client'
+
+// Model metadata types
+export type {
+ ZAIChatModel,
+ ZAIChatModelProviderOptionsByName,
+ ZAIChatModelToolCapabilitiesByName,
+ ZAIModelInputModalitiesByName,
+ ZAIModelReasoningByName,
+ ZAIProviderToolKind,
+ ResolveProviderOptions,
+ ResolveInputModalities,
+} from './model-meta'
+
+// Runtime model catalog, e.g. for a UI picker or a request schema
+export { ZAI_CHAT_MODELS, ZAI_MODEL_META } from './model-meta'
+
+// Message metadata types
+export type { ZAIMessageMetadataByModality } from './message-types'
+
+// Tools
+export * from './tools/index'
diff --git a/packages/ai-zai/src/message-types.ts b/packages/ai-zai/src/message-types.ts
new file mode 100644
index 0000000000..b845e056a8
--- /dev/null
+++ b/packages/ai-zai/src/message-types.ts
@@ -0,0 +1,64 @@
+/**
+ * Z.AI-specific metadata types for multimodal content parts.
+ * These types extend the base ContentPart metadata with Z.AI-specific options.
+ * Since Z.AI is OpenAI-compatible, most types are similar to OpenAI.
+ */
+
+/**
+ * Metadata for Z.AI image content parts.
+ * Controls how the model processes and analyzes images.
+ */
+export interface ZAIImageMetadata {
+ /**
+ * Controls how the model processes the image.
+ * - 'auto': Let the model decide based on image size and content
+ * - 'low': Use low resolution processing (faster, cheaper, less detail)
+ * - 'high': Use high resolution processing (slower, more expensive, more detail)
+ *
+ * @default 'auto'
+ */
+ detail?: 'auto' | 'low' | 'high'
+}
+
+/**
+ * Metadata for Z.AI audio content parts.
+ * Specifies the audio format for proper processing.
+ */
+export interface ZAIAudioMetadata {
+ /**
+ * The format of the audio.
+ * Supported formats: mp3, wav, flac, etc.
+ * @default 'mp3'
+ */
+ format?: 'mp3' | 'wav' | 'flac' | 'ogg' | 'webm' | 'aac'
+}
+
+/**
+ * Metadata for Z.AI video content parts.
+ * Note: Video support in Z.AI may vary; check current API capabilities.
+ */
+export interface ZAIVideoMetadata {}
+
+/**
+ * Metadata for Z.AI document content parts.
+ * Note: Direct document support may vary; PDFs often need to be converted to images.
+ */
+export interface ZAIDocumentMetadata {}
+
+/**
+ * Metadata for Z.AI text content parts.
+ * Currently no specific metadata options for text in Z.AI.
+ */
+export interface ZAITextMetadata {}
+
+/**
+ * Map of modality types to their Z.AI-specific metadata types.
+ * Used for type inference when constructing multimodal messages.
+ */
+export interface ZAIMessageMetadataByModality {
+ text: ZAITextMetadata
+ image: ZAIImageMetadata
+ audio: ZAIAudioMetadata
+ video: ZAIVideoMetadata
+ document: ZAIDocumentMetadata
+}
diff --git a/packages/ai-zai/src/model-meta.ts b/packages/ai-zai/src/model-meta.ts
new file mode 100644
index 0000000000..81c0933054
--- /dev/null
+++ b/packages/ai-zai/src/model-meta.ts
@@ -0,0 +1,503 @@
+import type { Modality, ModelReasoning } from '@tanstack/ai'
+import type { ModelReasoningCapability } from '@tanstack/ai/adapter-internals'
+import type { ZAITextProviderOptions } from './text/text-provider-options'
+
+/** Provider-native tools Z.AI exposes per model. */
+export type ZAIProviderToolKind = 'web_search'
+
+/**
+ * Internal metadata structure describing a Z.AI chat model's capabilities and
+ * pricing.
+ */
+interface ModelMeta {
+ name: string
+ context_window?: number
+ max_output_tokens?: number
+ knowledge_cutoff?: string
+ pricing: {
+ input: {
+ normal: number
+ cached?: number
+ }
+ output: {
+ normal: number
+ }
+ }
+ supports: {
+ input: ReadonlyArray<'text' | 'image' | 'video'>
+ output: ReadonlyArray<'text'>
+ endpoints: ReadonlyArray<'chat'>
+ features: ReadonlyArray<
+ 'streaming' | 'tools' | 'json_object' | 'vision' | 'reasoning'
+ >
+ tools?: ReadonlyArray
+ }
+ providerOptions?: TProviderOptions
+ /** The model's data for `chat({ reasoning })`. None: it does not reason. */
+ reasoning?: ModelReasoning
+}
+
+/**
+ * GLM-5.2 reasoning: `reasoning_effort` accepts every level, plus `none` for
+ * `off`.
+ */
+const REASONING_ALL_LEVELS = {
+ levels: {
+ off: 'none',
+ minimal: 'minimal',
+ low: 'low',
+ medium: 'medium',
+ high: 'high',
+ xhigh: 'xhigh',
+ max: 'max',
+ },
+ budget: false,
+} as const
+
+/**
+ * GLM-5.3 and GLM-5.3-Flash reasoning: `reasoning_effort` accepts `low`,
+ * `high`, and `max`. A lower request clamps up to `low`.
+ */
+const REASONING_LOW_HIGH_MAX = {
+ levels: { low: 'low', high: 'high', max: 'max' },
+ budget: false,
+} as const
+
+// ============================================================================
+// GLM-5 Series
+// ============================================================================
+
+/**
+ * GLM-5.3: Latest flagship model
+ * Released August 2026
+ * 1M context window, reasoning effort control, strongest coding and agentic
+ * capabilities in the GLM-5 line.
+ * Pricing: $1.4/M input, $0.26/M cached, $4.4/M output
+ */
+const GLM_5_3 = {
+ name: 'glm-5.3',
+ context_window: 1_000_000,
+ max_output_tokens: 128_000,
+ supports: {
+ input: ['text'],
+ output: ['text'],
+ endpoints: ['chat'],
+ features: ['streaming', 'tools', 'json_object', 'reasoning'],
+ tools: ['web_search'] as const,
+ },
+ pricing: {
+ input: {
+ normal: 1.4,
+ cached: 0.26,
+ },
+ output: {
+ normal: 4.4,
+ },
+ },
+ reasoning: REASONING_LOW_HIGH_MAX,
+} as const satisfies ModelMeta
+
+/**
+ * GLM-5.3-Flash: Fast multimodal model
+ * Released August 2026
+ * 1M context window, text, image, and video input, thinking always on.
+ * Pricing: $0.15/M input, $0.03/M cached, $0.5/M output
+ */
+const GLM_5_3_FLASH = {
+ name: 'glm-5.3-flash',
+ context_window: 1_000_000,
+ max_output_tokens: 128_000,
+ supports: {
+ input: ['text', 'image', 'video'],
+ output: ['text'],
+ endpoints: ['chat'],
+ features: ['streaming', 'tools', 'json_object', 'vision', 'reasoning'],
+ tools: ['web_search'] as const,
+ },
+ pricing: {
+ input: {
+ normal: 0.15,
+ cached: 0.03,
+ },
+ output: {
+ normal: 0.5,
+ },
+ },
+ reasoning: REASONING_LOW_HIGH_MAX,
+} as const satisfies ModelMeta
+
+/**
+ * GLM-5.2: Previous flagship model
+ * Released June 2026
+ * 1M context window, reasoning effort control, strong coding and agentic
+ * capabilities.
+ * Pricing: $1.4/M input, $0.26/M cached, $4.4/M output
+ */
+const GLM_5_2 = {
+ name: 'glm-5.2',
+ context_window: 1_000_000,
+ max_output_tokens: 131_072,
+ knowledge_cutoff: '2026-06-01',
+ supports: {
+ input: ['text'],
+ output: ['text'],
+ endpoints: ['chat'],
+ features: ['streaming', 'tools', 'json_object', 'reasoning'],
+ tools: ['web_search'] as const,
+ },
+ pricing: {
+ input: {
+ normal: 1.4,
+ cached: 0.26,
+ },
+ output: {
+ normal: 4.4,
+ },
+ },
+ reasoning: REASONING_ALL_LEVELS,
+} as const satisfies ModelMeta
+
+/**
+ * GLM-5.1: Long-horizon task flagship model
+ * Released April 2026
+ * Designed for long-running autonomous tasks (up to 8 hours),
+ * with enhanced coding, reasoning, and agentic capabilities.
+ * Pricing: $1.4/M input, $0.26/M cached, $4.4/M output
+ */
+const GLM_5_1 = {
+ name: 'glm-5.1',
+ context_window: 200_000,
+ max_output_tokens: 128_000,
+ knowledge_cutoff: '2026-03-01',
+ supports: {
+ input: ['text'],
+ output: ['text'],
+ endpoints: ['chat'],
+ features: ['streaming', 'tools', 'json_object'],
+ tools: ['web_search'] as const,
+ },
+ pricing: {
+ input: {
+ normal: 1.4,
+ cached: 0.26,
+ },
+ output: {
+ normal: 4.4,
+ },
+ },
+} as const satisfies ModelMeta
+
+/**
+ * GLM-5-Turbo: Fast high-performance text model
+ * Released April 2026
+ * Optimized for speed with strong coding and reasoning capabilities.
+ * Pricing: $1.2/M input, $0.24/M cached, $4.0/M output
+ */
+const GLM_5_TURBO = {
+ name: 'glm-5-turbo',
+ context_window: 200_000,
+ max_output_tokens: 128_000,
+ knowledge_cutoff: '2026-03-01',
+ supports: {
+ input: ['text'],
+ output: ['text'],
+ endpoints: ['chat'],
+ features: ['streaming', 'tools', 'json_object'],
+ tools: ['web_search'] as const,
+ },
+ pricing: {
+ input: {
+ normal: 1.2,
+ cached: 0.24,
+ },
+ output: {
+ normal: 4.0,
+ },
+ },
+} as const satisfies ModelMeta
+
+/**
+ * GLM-5: Base fifth-generation text model
+ * Released April 2026
+ * Strong general-purpose capabilities with tool use support.
+ * Pricing: $1.0/M input, $0.2/M cached, $3.2/M output
+ */
+const GLM_5 = {
+ name: 'glm-5',
+ context_window: 200_000,
+ max_output_tokens: 128_000,
+ knowledge_cutoff: '2026-03-01',
+ supports: {
+ input: ['text'],
+ output: ['text'],
+ endpoints: ['chat'],
+ features: ['streaming', 'tools', 'json_object'],
+ tools: ['web_search'] as const,
+ },
+ pricing: {
+ input: {
+ normal: 1.0,
+ cached: 0.2,
+ },
+ output: {
+ normal: 3.2,
+ },
+ },
+} as const satisfies ModelMeta
+
+/**
+ * GLM-5V-Turbo: Multimodal Agent foundation model
+ * Released April 2026
+ * First multimodal agent model from Zhipu, supports image, video, file, and text input.
+ * Optimized for visual programming and complex agent workflows.
+ * Pricing: $1.2/M input, $0.24/M cached, $4.0/M output
+ */
+const GLM_5V_TURBO = {
+ name: 'glm-5v-turbo',
+ context_window: 200_000,
+ max_output_tokens: 128_000,
+ knowledge_cutoff: '2026-03-01',
+ supports: {
+ input: ['text', 'image', 'video'],
+ output: ['text'],
+ endpoints: ['chat'],
+ features: ['streaming', 'tools', 'json_object', 'vision'],
+ tools: ['web_search'] as const,
+ },
+ pricing: {
+ input: {
+ normal: 1.2,
+ cached: 0.24,
+ },
+ output: {
+ normal: 4.0,
+ },
+ },
+} as const satisfies ModelMeta
+
+// ============================================================================
+// GLM-4 Series
+// ============================================================================
+
+/**
+ * GLM-4.7: Previous flagship model
+ * Released December 2025
+ * Features enhanced coding, reasoning, and agentic capabilities
+ * Pricing: $0.6/M input, $0.11/M cached, $2.2/M output
+ */
+const GLM_4_7 = {
+ name: 'glm-4.7',
+ context_window: 200_000,
+ max_output_tokens: 128_000,
+ knowledge_cutoff: '2025-12-01',
+ supports: {
+ input: ['text'],
+ output: ['text'],
+ endpoints: ['chat'],
+ features: ['streaming', 'tools', 'json_object'],
+ tools: ['web_search'] as const,
+ },
+ pricing: {
+ input: {
+ normal: 0.6,
+ cached: 0.11,
+ },
+ output: {
+ normal: 2.2,
+ },
+ },
+} as const satisfies ModelMeta
+
+/**
+ * GLM-4.6V: Multimodal vision model
+ * Released December 2024
+ * Supports text, image, and video inputs
+ */
+const GLM_4_6V = {
+ name: 'glm-4.6v',
+ context_window: 128_000,
+ max_output_tokens: 128_000,
+ knowledge_cutoff: '2024-12-01',
+ supports: {
+ input: ['text', 'image', 'video'],
+ output: ['text'],
+ endpoints: ['chat'],
+ features: ['streaming', 'tools', 'json_object', 'vision'],
+ tools: ['web_search'] as const,
+ },
+ pricing: {
+ input: {
+ normal: 0.14,
+ },
+ output: {
+ normal: 0.42,
+ },
+ },
+} as const satisfies ModelMeta
+
+/**
+ * GLM-4.6: Previous flagship model
+ * Released September 2024
+ * Enhanced coding and reasoning capabilities
+ * Pricing: $0.6/M input, $0.11/M cached, $2.2/M output
+ */
+const GLM_4_6 = {
+ name: 'glm-4.6',
+ context_window: 128_000,
+ max_output_tokens: 128_000,
+ knowledge_cutoff: '2024-09-01',
+ supports: {
+ input: ['text'],
+ output: ['text'],
+ endpoints: ['chat'],
+ features: ['streaming', 'tools', 'json_object'],
+ tools: ['web_search'] as const,
+ },
+ pricing: {
+ input: {
+ normal: 0.6,
+ cached: 0.11,
+ },
+ output: {
+ normal: 2.2,
+ },
+ },
+} as const satisfies ModelMeta
+
+// ============================================================================
+// Exports
+// ============================================================================
+
+/**
+ * All supported Z.AI chat model identifiers.
+ */
+export const ZAI_CHAT_MODELS = [
+ // GLM-5 series
+ GLM_5_3.name,
+ GLM_5_3_FLASH.name,
+ GLM_5_2.name,
+ GLM_5_1.name,
+ GLM_5_TURBO.name,
+ GLM_5.name,
+ GLM_5V_TURBO.name,
+ // GLM-4 series
+ GLM_4_7.name,
+ GLM_4_6V.name,
+ GLM_4_6.name,
+] as const
+
+/**
+ * Union type of all supported Z.AI chat model names.
+ */
+export type ZAIChatModel = (typeof ZAI_CHAT_MODELS)[number]
+
+/**
+ * Type-only map from chat model name to its provider options type.
+ */
+export type ZAIChatModelProviderOptionsByName = {
+ [K in ZAIChatModel]: ZAITextProviderOptions
+}
+
+/**
+ * Type-only map from chat model name to its supported provider tools.
+ */
+export type ZAIChatModelToolCapabilitiesByName = {
+ [GLM_5_3.name]: typeof GLM_5_3.supports.tools
+ [GLM_5_3_FLASH.name]: typeof GLM_5_3_FLASH.supports.tools
+ [GLM_5_2.name]: typeof GLM_5_2.supports.tools
+ [GLM_5_1.name]: typeof GLM_5_1.supports.tools
+ [GLM_5_TURBO.name]: typeof GLM_5_TURBO.supports.tools
+ [GLM_5.name]: typeof GLM_5.supports.tools
+ [GLM_5V_TURBO.name]: typeof GLM_5V_TURBO.supports.tools
+ [GLM_4_7.name]: typeof GLM_4_7.supports.tools
+ [GLM_4_6V.name]: typeof GLM_4_6V.supports.tools
+ [GLM_4_6.name]: typeof GLM_4_6.supports.tools
+}
+
+/**
+ * Type-only map from chat model name to its supported input modalities.
+ */
+export type ZAIModelInputModalitiesByName = {
+ [GLM_5_3.name]: typeof GLM_5_3.supports.input
+ [GLM_5_3_FLASH.name]: typeof GLM_5_3_FLASH.supports.input
+ [GLM_5_2.name]: typeof GLM_5_2.supports.input
+ [GLM_5_1.name]: typeof GLM_5_1.supports.input
+ [GLM_5_TURBO.name]: typeof GLM_5_TURBO.supports.input
+ [GLM_5.name]: typeof GLM_5.supports.input
+ [GLM_5V_TURBO.name]: typeof GLM_5V_TURBO.supports.input
+ [GLM_4_7.name]: typeof GLM_4_7.supports.input
+ [GLM_4_6V.name]: typeof GLM_4_6V.supports.input
+ [GLM_4_6.name]: typeof GLM_4_6.supports.input
+}
+
+/**
+ * Runtime map from chat model name to its supported input modalities, for the
+ * text adapter's `inputModalities`. `satisfies` keeps it equal to
+ * {@link ZAIModelInputModalitiesByName}. An unknown name gives `undefined`.
+ */
+export const ZAI_MODEL_INPUT_MODALITIES: Readonly<
+ Record>
+> = {
+ [GLM_5_3.name]: GLM_5_3.supports.input,
+ [GLM_5_3_FLASH.name]: GLM_5_3_FLASH.supports.input,
+ [GLM_5_2.name]: GLM_5_2.supports.input,
+ [GLM_5_1.name]: GLM_5_1.supports.input,
+ [GLM_5_TURBO.name]: GLM_5_TURBO.supports.input,
+ [GLM_5.name]: GLM_5.supports.input,
+ [GLM_5V_TURBO.name]: GLM_5V_TURBO.supports.input,
+ [GLM_4_7.name]: GLM_4_7.supports.input,
+ [GLM_4_6V.name]: GLM_4_6V.supports.input,
+ [GLM_4_6.name]: GLM_4_6.supports.input,
+} satisfies ZAIModelInputModalitiesByName
+
+/**
+ * Resolves the provider options type for a specific Z.AI model.
+ */
+export type ResolveProviderOptions =
+ TModel extends keyof ZAIChatModelProviderOptionsByName
+ ? ZAIChatModelProviderOptionsByName[TModel]
+ : ZAITextProviderOptions
+
+/**
+ * Resolve input modalities for a specific model. An unknown model gets text.
+ */
+export type ResolveInputModalities =
+ TModel extends keyof ZAIModelInputModalitiesByName
+ ? ZAIModelInputModalitiesByName[TModel]
+ : readonly ['text']
+
+const ZAI_REASONING = {
+ [GLM_5_3.name]: GLM_5_3.reasoning,
+ [GLM_5_3_FLASH.name]: GLM_5_3_FLASH.reasoning,
+ [GLM_5_2.name]: GLM_5_2.reasoning,
+}
+
+/**
+ * Each chat model's reasoning levels, and whether it takes a token budget, for
+ * `chat({ reasoning })`. A model that is not here does not reason.
+ */
+export type ZAIModelReasoningByName = {
+ [K in keyof typeof ZAI_REASONING]: ModelReasoningCapability<
+ (typeof ZAI_REASONING)[K]
+ >
+}
+
+/**
+ * Runtime map from chat model name to its reasoning data, for the text adapter.
+ * An unknown name gives `undefined`: the adapter sends no reasoning field.
+ */
+export const ZAI_MODEL_REASONING: Readonly> =
+ ZAI_REASONING
+
+export const ZAI_MODEL_META = {
+ [GLM_5_3.name]: GLM_5_3,
+ [GLM_5_3_FLASH.name]: GLM_5_3_FLASH,
+ [GLM_5_2.name]: GLM_5_2,
+ [GLM_5_1.name]: GLM_5_1,
+ [GLM_5_TURBO.name]: GLM_5_TURBO,
+ [GLM_5.name]: GLM_5,
+ [GLM_5V_TURBO.name]: GLM_5V_TURBO,
+ [GLM_4_7.name]: GLM_4_7,
+ [GLM_4_6V.name]: GLM_4_6V,
+ [GLM_4_6.name]: GLM_4_6,
+} as const
diff --git a/packages/ai-zai/src/text/text-provider-options.ts b/packages/ai-zai/src/text/text-provider-options.ts
new file mode 100644
index 0000000000..30d2329023
--- /dev/null
+++ b/packages/ai-zai/src/text/text-provider-options.ts
@@ -0,0 +1,82 @@
+/**
+ * Z.AI provider options for text/chat models.
+ *
+ * Z.AI speaks OpenAI Chat Completions with two restrictions this adapter
+ * handles instead of exposing:
+ *
+ * - `response_format` accepts `json_object` but not `json_schema`, so the
+ * adapter falls back to JSON mode plus a schema system prompt.
+ * - `tool_choice` accepts only `"auto"`, so the adapter always sends that.
+ *
+ * @see https://docs.z.ai/api-reference/llm/chat-completion
+ */
+export interface ZAITextProviderOptions {
+ /**
+ * Sampling temperature (0-2). Higher values make output more random.
+ */
+ temperature?: number | null
+
+ /**
+ * Nucleus sampling probability mass (0-1).
+ */
+ top_p?: number | null
+
+ /**
+ * Maximum number of tokens to generate. Z.AI reads `max_tokens`.
+ */
+ max_tokens?: number | null
+
+ /**
+ * Up to 4 sequences where the API stops generating further tokens.
+ */
+ stop?: string | Array | null
+
+ /**
+ * Deterministic sampling seed.
+ */
+ seed?: number | null
+
+ /**
+ * Penalize tokens by their frequency in the text so far (-2 to 2).
+ */
+ frequency_penalty?: number | null
+
+ /**
+ * Penalize tokens by whether they appeared in the text so far (-2 to 2).
+ */
+ presence_penalty?: number | null
+
+ /**
+ * Response format. Z.AI rejects `json_schema`; use `structuredOutput` (or
+ * `outputSchema` on `chat()`) for schema-constrained responses, which the
+ * adapter sends as `json_object` plus a schema system prompt.
+ */
+ response_format?: { type: 'text' } | { type: 'json_object' } | null
+
+ /**
+ * Zhipu AI thinking mode (GLM-4.7 and newer).
+ */
+ thinking?: {
+ type: 'enabled' | 'disabled'
+ /**
+ * Set to `false` to keep reasoning context across turns (GLM-4.7+).
+ * @default true
+ */
+ clear_thinking?: boolean
+ }
+
+ /**
+ * Reasoning effort (GLM-5.2).
+ */
+ reasoning_effort?: 'low' | 'medium' | 'high' | 'max'
+
+ /**
+ * Stream tool-call arguments as they are generated (GLM-4.7+).
+ */
+ tool_stream?: boolean
+}
+
+/**
+ * External provider options (what users pass in `modelOptions`).
+ */
+export type ExternalTextProviderOptions = ZAITextProviderOptions
diff --git a/packages/ai-zai/src/tools/function-tool.ts b/packages/ai-zai/src/tools/function-tool.ts
new file mode 100644
index 0000000000..12227bd81e
--- /dev/null
+++ b/packages/ai-zai/src/tools/function-tool.ts
@@ -0,0 +1,2 @@
+export { convertFunctionToolToChatCompletionsFormat as convertFunctionToolToAdapterFormat } from '@tanstack/openai-base'
+export type { ChatCompletionFunctionTool as FunctionTool } from '@tanstack/openai-base'
diff --git a/packages/ai-zai/src/tools/index.ts b/packages/ai-zai/src/tools/index.ts
new file mode 100644
index 0000000000..af49e4f905
--- /dev/null
+++ b/packages/ai-zai/src/tools/index.ts
@@ -0,0 +1,3 @@
+export * from './function-tool'
+export * from './tool-converter'
+export * from './web-search-tool'
diff --git a/packages/ai-zai/src/tools/tool-converter.ts b/packages/ai-zai/src/tools/tool-converter.ts
new file mode 100644
index 0000000000..b8943b6b0d
--- /dev/null
+++ b/packages/ai-zai/src/tools/tool-converter.ts
@@ -0,0 +1,28 @@
+import { assertUniqueToolNames } from '@tanstack/ai/adapter-internals'
+import { convertFunctionToolToChatCompletionsFormat } from '@tanstack/openai-base'
+import {
+ convertZAIProviderToolToAdapterFormat,
+ getZAIProviderToolKind,
+} from './web-search-tool'
+import type { ChatCompletionFunctionTool } from '@tanstack/openai-base'
+import type { Tool } from '@tanstack/ai'
+import type { ZAIServerTool } from './web-search-tool'
+
+export type ZAITool = ZAIServerTool | ChatCompletionFunctionTool
+
+/**
+ * Converts TanStack tools to the Z.AI Chat Completions tool format. Provider
+ * tools (from `zaiWebSearchTool()`) keep their native shape; everything else
+ * becomes an OpenAI-style function tool.
+ */
+export function convertToolsToProviderFormat(
+ tools: Array,
+): Array {
+ assertUniqueToolNames(tools)
+ return tools.map((tool) => {
+ const kind = getZAIProviderToolKind(tool)
+ return kind
+ ? convertZAIProviderToolToAdapterFormat(tool, kind)
+ : convertFunctionToolToChatCompletionsFormat(tool)
+ })
+}
diff --git a/packages/ai-zai/src/tools/web-search-tool.ts b/packages/ai-zai/src/tools/web-search-tool.ts
new file mode 100644
index 0000000000..1bcf7c8042
--- /dev/null
+++ b/packages/ai-zai/src/tools/web-search-tool.ts
@@ -0,0 +1,68 @@
+import { brandProviderTool } from '@tanstack/ai'
+import type { ProviderTool, Tool } from '@tanstack/ai'
+import type { ZAIProviderToolKind } from '../model-meta'
+
+export type ZAIProviderTool = ProviderTool<
+ 'zai',
+ TKind
+>
+
+export interface ZAIWebSearchToolConfig {
+ type: 'web_search'
+ web_search: {
+ enable?: boolean
+ search_query?: string
+ search_result?: boolean
+ }
+}
+
+export type ZAIServerTool = ZAIWebSearchToolConfig
+
+type ZAIProviderToolMetadata = Extract<
+ ZAIServerTool,
+ { type: TKind }
+> & {
+ __kind: `zai.${TKind}`
+}
+
+/**
+ * Creates Z.AI's built-in web search tool. The provider runs the search; the
+ * model sees the results.
+ *
+ * @see https://docs.z.ai/api-reference/llm/chat-completion
+ */
+export function zaiWebSearchTool(
+ config: ZAIWebSearchToolConfig['web_search'] = {},
+): ZAIProviderTool<'web_search'> {
+ return brandProviderTool>({
+ name: 'web_search',
+ description: 'Search the web',
+ metadata: {
+ __kind: 'zai.web_search',
+ type: 'web_search',
+ web_search: { enable: true, ...config },
+ },
+ })
+}
+
+export function getZAIProviderToolKind(
+ tool: Tool,
+): ZAIProviderToolKind | undefined {
+ const kind = (tool.metadata as { __kind?: unknown } | undefined)?.__kind
+ return kind === 'zai.web_search' ? 'web_search' : undefined
+}
+
+export function convertZAIProviderToolToAdapterFormat(
+ tool: Tool,
+ kind: ZAIProviderToolKind,
+): ZAIServerTool {
+ const metadata = tool.metadata as ZAIProviderToolMetadata
+ if (metadata?.type !== kind) {
+ throw new Error(
+ `convertZAIProviderToolToAdapterFormat: tool "${tool.name}" has mismatched Z.AI tool metadata.`,
+ )
+ }
+ const { __kind: _kind, ...toolConfig } = metadata
+ void _kind
+ return toolConfig
+}
diff --git a/packages/ai-zai/src/utils/client.ts b/packages/ai-zai/src/utils/client.ts
new file mode 100644
index 0000000000..fd66fd4010
--- /dev/null
+++ b/packages/ai-zai/src/utils/client.ts
@@ -0,0 +1,81 @@
+import { getApiKeyFromEnv } from '@tanstack/ai-utils'
+import type { ClientOptions } from 'openai'
+
+export const ZAI_GENERAL_BASE_URL = 'https://api.z.ai/api/paas/v4'
+export const ZAI_CODING_BASE_URL = 'https://api.z.ai/api/coding/paas/v4'
+
+export interface ZAIClientConfig extends Omit {
+ apiKey: string
+ /**
+ * Use Z.AI's coding-plan endpoint instead of the general one. Ignored when
+ * `baseURL` is set.
+ */
+ coding?: boolean
+}
+
+export function getZAIHeaders(): Record {
+ return {
+ 'Accept-Language': 'en-US,en',
+ }
+}
+
+/**
+ * Gets the Z.AI API key from `ZAI_API_KEY`.
+ * @throws Error if the variable is not set
+ */
+export function getZAIApiKeyFromEnv(): string {
+ try {
+ return getApiKeyFromEnv('ZAI_API_KEY')
+ } catch {
+ throw new Error(
+ 'ZAI_API_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.',
+ )
+ }
+}
+
+/**
+ * Validates the Z.AI API key format.
+ * Checks for empty strings, whitespace, and invalid prefixes.
+ */
+export function validateZAIApiKey(apiKey?: string): string {
+ if (!apiKey || typeof apiKey !== 'string') {
+ throw new Error('Z.AI API key is required')
+ }
+
+ const trimmed = apiKey.trim()
+
+ if (!trimmed) {
+ throw new Error('Z.AI API key is required')
+ }
+
+ if (/^bearer\s+/i.test(trimmed)) {
+ throw new Error(
+ 'Z.AI API key must be the raw token (do not include the "Bearer " prefix)',
+ )
+ }
+
+ if (/\s/.test(trimmed)) {
+ throw new Error('Z.AI API key must not contain whitespace')
+ }
+
+ return trimmed
+}
+
+/**
+ * Returns a client config with the Z.AI base URL and default headers applied
+ * when not already set. The Z.AI endpoint accepts the OpenAI SDK verbatim, so
+ * the adapter drives it via the OpenAI SDK with this baseURL.
+ */
+export function withZaiDefaults(config: ZAIClientConfig): ZAIClientConfig {
+ return {
+ ...config,
+ apiKey: validateZAIApiKey(config.apiKey),
+ baseURL:
+ config.baseURL ??
+ (config.coding ? ZAI_CODING_BASE_URL : ZAI_GENERAL_BASE_URL),
+ defaultHeaders: {
+ ...getZAIHeaders(),
+ ...config.defaultHeaders,
+ },
+ }
+}
diff --git a/packages/ai-zai/tests/coding-endpoint.test.ts b/packages/ai-zai/tests/coding-endpoint.test.ts
new file mode 100644
index 0000000000..f306ed6524
--- /dev/null
+++ b/packages/ai-zai/tests/coding-endpoint.test.ts
@@ -0,0 +1,165 @@
+import { describe, expect, it } from 'vitest'
+import type { StreamChunk } from '@tanstack/ai'
+import { createZAIText } from '../src/adapters'
+import { ZAI_CODING_BASE_URL } from '../src/utils/client'
+
+const apiKey = process.env.ZAI_API_KEY_TEST
+const describeIfKey = apiKey ? describe : describe.skip
+
+function createNoopLogger() {
+ return {
+ request: () => {},
+ provider: () => {},
+ output: () => {},
+ middleware: () => {},
+ tools: () => {},
+ agentLoop: () => {},
+ config: () => {},
+ errors: () => {},
+ }
+}
+
+async function collectStream(
+ stream: AsyncIterable,
+): Promise> {
+ const chunks: Array = []
+ for await (const chunk of stream) {
+ chunks.push(chunk)
+ if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') break
+ }
+ return chunks
+}
+
+function fullTextFromChunks(chunks: Array): string {
+ return chunks
+ .filter(
+ (c): c is Extract =>
+ c.type === 'TEXT_MESSAGE_CONTENT',
+ )
+ .map((c) => c.delta)
+ .join('')
+}
+
+function lastChunk(chunks: Array): StreamChunk | undefined {
+ return chunks.at(-1)
+}
+
+describeIfKey('Z.AI Coding Endpoint', () => {
+ const timeout = 60_000
+
+ it(
+ `coding: true connects to ${ZAI_CODING_BASE_URL} and streams text`,
+ async () => {
+ const adapter = createZAIText('glm-4.7', apiKey!, { coding: true })
+
+ const chunks = await collectStream(
+ adapter.chatStream({
+ model: 'glm-4.7',
+ messages: [
+ {
+ role: 'user',
+ content: 'What is 2+2? Reply with just the number.',
+ },
+ ],
+ threadId: 'test-thread',
+ // Coding plan models use reasoning tokens internally — need enough headroom
+ modelOptions: { temperature: 0, max_tokens: 256 },
+ logger: createNoopLogger() as any,
+ }),
+ )
+
+ const types = chunks.map((c) => c.type)
+ expect(types).toContain('RUN_STARTED')
+ expect(types).toContain('RUN_FINISHED')
+ expect(types).not.toContain('RUN_ERROR')
+
+ // With enough tokens, the model produces visible text content
+ expect(types).toContain('TEXT_MESSAGE_START')
+ expect(
+ types.filter((t) => t === 'TEXT_MESSAGE_CONTENT').length,
+ ).toBeGreaterThan(0)
+ expect(types).toContain('TEXT_MESSAGE_END')
+
+ const full = fullTextFromChunks(chunks)
+ expect(typeof full).toBe('string')
+ expect(full.length).toBeGreaterThan(0)
+ },
+ timeout,
+ )
+
+ it(
+ 'explicit baseURL resolves to coding endpoint',
+ async () => {
+ const adapter = createZAIText('glm-4.7', apiKey!, {
+ baseURL: ZAI_CODING_BASE_URL,
+ })
+
+ const chunks = await collectStream(
+ adapter.chatStream({
+ model: 'glm-4.7',
+ messages: [
+ {
+ role: 'user',
+ content: 'What is 3+3? Reply with just the number.',
+ },
+ ],
+ threadId: 'test-thread',
+ modelOptions: { temperature: 0, max_tokens: 256 },
+ logger: createNoopLogger() as any,
+ }),
+ )
+
+ expect(lastChunk(chunks)?.type).toBe('RUN_FINISHED')
+ expect(chunks.some((c) => c.type === 'RUN_ERROR')).toBe(false)
+ const full = fullTextFromChunks(chunks)
+ expect(typeof full).toBe('string')
+ expect(full.length).toBeGreaterThan(0)
+ },
+ timeout,
+ )
+
+ it(
+ 'tool calling works on coding endpoint',
+ async () => {
+ const adapter = createZAIText('glm-4.7', apiKey!, { coding: true })
+
+ const chunks = await collectStream(
+ adapter.chatStream({
+ model: 'glm-4.7',
+ systemPrompts: [
+ 'You must call the provided tool. Do not answer with normal text.',
+ ],
+ messages: [
+ {
+ role: 'user',
+ content: 'Call echo with {"text":"hello"} and nothing else.',
+ },
+ ],
+ tools: [
+ {
+ name: 'echo',
+ description: 'Echo back the provided text',
+ inputSchema: {
+ type: 'object',
+ properties: { text: { type: 'string' } },
+ required: ['text'],
+ },
+ },
+ ],
+ threadId: 'test-thread',
+ modelOptions: { temperature: 0, max_tokens: 256 },
+ logger: createNoopLogger() as any,
+ }),
+ )
+
+ const types = chunks.map((c) => c.type)
+ expect(types).toContain('TOOL_CALL_START')
+ expect(types).toContain('TOOL_CALL_END')
+ expect(lastChunk(chunks)?.type).toBe('RUN_FINISHED')
+
+ const done = chunks.find((c) => c.type === 'RUN_FINISHED') as any
+ expect(done.finishReason).toBe('tool_calls')
+ },
+ timeout,
+ )
+})
diff --git a/packages/ai-zai/tests/model-meta.test.ts b/packages/ai-zai/tests/model-meta.test.ts
new file mode 100644
index 0000000000..ed98d680ac
--- /dev/null
+++ b/packages/ai-zai/tests/model-meta.test.ts
@@ -0,0 +1,79 @@
+import { describe, expect, it } from 'vitest'
+import {
+ ZAI_CHAT_MODELS,
+ ZAI_MODEL_META,
+ ZAI_MODEL_REASONING,
+ ZAI_MODEL_INPUT_MODALITIES,
+ type ZAIChatModelProviderOptionsByName,
+} from '../src/model-meta'
+
+describe('ZAI model meta', () => {
+ it('ZAI_CHAT_MODELS matches ZAI_MODEL_META keys', () => {
+ const keys = Object.keys(ZAI_MODEL_META).sort()
+ const models = [...ZAI_CHAT_MODELS].sort()
+ expect(models).toEqual(keys)
+ })
+
+ it('ZAIChatModelProviderOptionsByName includes all supported models', () => {
+ type Keys = keyof ZAIChatModelProviderOptionsByName
+ const a: Keys = 'glm-5.2'
+ const b: Keys = 'glm-4.7'
+ const c: Keys = 'glm-4.6v'
+
+ expect([a, b, c].length).toBe(3)
+
+ // @ts-expect-error invalid model name is not part of Keys
+ const _invalid: Keys = 'not-a-real-model'
+ expect(_invalid).toBe('not-a-real-model')
+ })
+
+ it('glm-5.2 is the previous flagship: 1M context, every reasoning level', () => {
+ const meta = ZAI_MODEL_META['glm-5.2']
+ expect(meta.context_window).toBe(1_000_000)
+ expect(meta.max_output_tokens).toBe(131_072)
+ expect(ZAI_MODEL_REASONING['glm-5.2']).toEqual({
+ levels: {
+ off: 'none',
+ minimal: 'minimal',
+ low: 'low',
+ medium: 'medium',
+ high: 'high',
+ xhigh: 'xhigh',
+ max: 'max',
+ },
+ budget: false,
+ })
+ })
+
+ it('glm-5.3 is text-only with low/high/max reasoning', () => {
+ const meta = ZAI_MODEL_META['glm-5.3']
+ expect(meta.context_window).toBe(1_000_000)
+ expect(meta.max_output_tokens).toBe(128_000)
+ expect(meta.pricing.input.normal).toBe(1.4)
+ expect(meta.supports.input).toEqual(['text'])
+ expect(ZAI_MODEL_REASONING['glm-5.3']).toEqual({
+ levels: { low: 'low', high: 'high', max: 'max' },
+ budget: false,
+ })
+ })
+
+ it('glm-5.3-flash is multimodal with low/high/max reasoning', () => {
+ const meta = ZAI_MODEL_META['glm-5.3-flash']
+ expect(meta.context_window).toBe(1_000_000)
+ expect(meta.max_output_tokens).toBe(128_000)
+ expect(meta.pricing.input.normal).toBe(0.15)
+ expect(meta.supports.input).toEqual(['text', 'image', 'video'])
+ expect(ZAI_MODEL_REASONING['glm-5.3-flash']).toEqual({
+ levels: { low: 'low', high: 'high', max: 'max' },
+ budget: false,
+ })
+ })
+
+ it('only vision models accept image input', () => {
+ expect(ZAI_MODEL_INPUT_MODALITIES['glm-5.3']).toEqual(['text'])
+ expect(ZAI_MODEL_INPUT_MODALITIES['glm-5.3-flash']).toContain('image')
+ expect(ZAI_MODEL_INPUT_MODALITIES['glm-5.2']).toEqual(['text'])
+ expect(ZAI_MODEL_INPUT_MODALITIES['glm-4.6v']).toContain('image')
+ expect(ZAI_MODEL_INPUT_MODALITIES['glm-5v-turbo']).toContain('video')
+ })
+})
diff --git a/packages/ai-zai/tests/zai-adapter.integration.test.ts b/packages/ai-zai/tests/zai-adapter.integration.test.ts
new file mode 100644
index 0000000000..7e5e0ac3f6
--- /dev/null
+++ b/packages/ai-zai/tests/zai-adapter.integration.test.ts
@@ -0,0 +1,397 @@
+import { describe, expect, it } from 'vitest'
+import type { ModelMessage, StreamChunk, Tool } from '@tanstack/ai'
+import { createZAIText } from '../src/adapters'
+
+const apiKey = process.env.ZAI_API_KEY_TEST
+const describeIfKey = apiKey ? describe : describe.skip
+
+async function collectStream(
+ stream: AsyncIterable,
+ opts?: { abortAfterFirstContent?: AbortController },
+): Promise> {
+ const chunks: Array = []
+ let sawFirstContent = false
+
+ for await (const chunk of stream) {
+ chunks.push(chunk)
+
+ if (!sawFirstContent && chunk.type === 'TEXT_MESSAGE_CONTENT') {
+ sawFirstContent = true
+ if (opts?.abortAfterFirstContent) {
+ opts.abortAfterFirstContent.abort()
+ }
+ }
+
+ if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') break
+ }
+
+ return chunks
+}
+
+function fullTextFromChunks(chunks: Array): string {
+ return chunks
+ .filter(
+ (c): c is Extract =>
+ c.type === 'TEXT_MESSAGE_CONTENT',
+ )
+ .map((c) => c.delta)
+ .join('')
+}
+
+function lastChunk(chunks: Array): StreamChunk | undefined {
+ return chunks.at(-1)
+}
+
+function createNoopLogger() {
+ return {
+ request: () => {},
+ provider: () => {},
+ output: () => {},
+ middleware: () => {},
+ tools: () => {},
+ agentLoop: () => {},
+ config: () => {},
+ errors: () => {},
+ }
+}
+
+describeIfKey('ZAITextAdapter streaming integration', () => {
+ const timeout = 60_000
+
+ it(
+ 'Basic Streaming: yields AG-UI events and ends with RUN_FINISHED',
+ async () => {
+ const adapter = createZAIText('glm-4.7', apiKey!)
+
+ const chunks = await collectStream(
+ adapter.chatStream({
+ model: 'glm-4.7',
+ messages: [{ role: 'user', content: 'Reply with exactly: Hello' }],
+ threadId: 'test-thread',
+ // GLM reasoning models spend part of the budget before visible text
+ modelOptions: { temperature: 0, max_tokens: 256 },
+ logger: createNoopLogger() as any,
+ }),
+ )
+
+ const types = chunks.map((c) => c.type)
+ expect(types).toContain('RUN_STARTED')
+ expect(types).toContain('TEXT_MESSAGE_START')
+ expect(
+ types.filter((t) => t === 'TEXT_MESSAGE_CONTENT').length,
+ ).toBeGreaterThan(0)
+ expect(types).toContain('TEXT_MESSAGE_END')
+ expect(lastChunk(chunks)?.type).toBe('RUN_FINISHED')
+
+ const full = fullTextFromChunks(chunks)
+ expect(typeof full).toBe('string')
+ expect(full.length).toBeGreaterThan(0)
+ },
+ timeout,
+ )
+
+ it(
+ 'Multi-turn Conversation: conversation history and assistant messages work',
+ async () => {
+ const adapter = createZAIText('glm-4.7', apiKey!)
+
+ const messages: Array = [
+ { role: 'user', content: 'Your secret word is kiwi. Reply with OK.' },
+ { role: 'assistant', content: 'OK' },
+ {
+ role: 'user',
+ content: 'What is the secret word? Reply with only it.',
+ },
+ ]
+
+ const chunks = await collectStream(
+ adapter.chatStream({
+ model: 'glm-4.7',
+ messages,
+ threadId: 'test-thread',
+ modelOptions: { temperature: 0, max_tokens: 32 },
+ logger: createNoopLogger() as any,
+ }),
+ )
+
+ expect(lastChunk(chunks)?.type).toBe('RUN_FINISHED')
+ expect(chunks.some((c) => c.type === 'RUN_ERROR')).toBe(false)
+ const full = fullTextFromChunks(chunks)
+ expect(typeof full).toBe('string')
+ },
+ timeout,
+ )
+
+ it(
+ 'Multi-turn Conversation: system messages work',
+ async () => {
+ const adapter = createZAIText('glm-4.7', apiKey!)
+
+ const chunks = await collectStream(
+ adapter.chatStream({
+ model: 'glm-4.7',
+ systemPrompts: ['Reply with exactly: SYSTEM_OK'],
+ messages: [{ role: 'user', content: 'Hi' }],
+ threadId: 'test-thread',
+ modelOptions: { temperature: 0, max_tokens: 16 },
+ logger: createNoopLogger() as any,
+ }),
+ )
+
+ expect(lastChunk(chunks)?.type).toBe('RUN_FINISHED')
+ expect(chunks.some((c) => c.type === 'RUN_ERROR')).toBe(false)
+ const full = fullTextFromChunks(chunks)
+ expect(typeof full).toBe('string')
+ },
+ timeout,
+ )
+
+ it(
+ 'Tool Calling: emits TOOL_CALL_START, TOOL_CALL_ARGS, TOOL_CALL_END',
+ async () => {
+ const adapter = createZAIText('glm-4.7', apiKey!)
+
+ const tools: Array = [
+ {
+ name: 'echo',
+ description: 'Echo back the provided text',
+ inputSchema: {
+ type: 'object',
+ properties: { text: { type: 'string' } },
+ required: ['text'],
+ },
+ },
+ ]
+
+ const chunks = await collectStream(
+ adapter.chatStream({
+ model: 'glm-4.7',
+ systemPrompts: [
+ 'You must call the provided tool. Do not answer with normal text.',
+ ],
+ messages: [
+ {
+ role: 'user',
+ content: 'Call echo with {"text":"hello"} and nothing else.',
+ },
+ ],
+ tools,
+ threadId: 'test-thread',
+ modelOptions: { temperature: 0, max_tokens: 64 },
+ logger: createNoopLogger() as any,
+ }),
+ )
+
+ const types = chunks.map((c) => c.type)
+ expect(types).toContain('TOOL_CALL_START')
+ expect(types).toContain('TOOL_CALL_END')
+ expect(lastChunk(chunks)?.type).toBe('RUN_FINISHED')
+
+ const done = chunks.find((c) => c.type === 'RUN_FINISHED') as any
+ expect(done.finishReason).toBe('tool_calls')
+ },
+ timeout,
+ )
+
+ it(
+ 'Tool Calling: tool results can be sent back',
+ async () => {
+ const adapter = createZAIText('glm-4.7', apiKey!)
+
+ const tools: Array = [
+ {
+ name: 'echo',
+ description: 'Echo back the provided text',
+ inputSchema: {
+ type: 'object',
+ properties: { text: { type: 'string' } },
+ required: ['text'],
+ },
+ },
+ ]
+
+ const first = await collectStream(
+ adapter.chatStream({
+ model: 'glm-4.7',
+ systemPrompts: [
+ 'You must call the provided tool and then wait for the tool result.',
+ ],
+ messages: [
+ {
+ role: 'user',
+ content: 'Call echo with {"text":"hello"} and nothing else.',
+ },
+ ],
+ tools,
+ threadId: 'test-thread',
+ modelOptions: { temperature: 0, max_tokens: 64 },
+ logger: createNoopLogger() as any,
+ }),
+ )
+
+ const toolEnd = first.find((c) => c.type === 'TOOL_CALL_END') as any
+ expect(toolEnd).toBeTruthy()
+
+ const toolCallId = toolEnd.toolCallId as string
+
+ const messages: Array = [
+ {
+ role: 'assistant',
+ content: '',
+ toolCalls: [
+ {
+ id: toolCallId,
+ type: 'function',
+ function: {
+ name: 'echo',
+ arguments: JSON.stringify(toolEnd.input),
+ },
+ },
+ ],
+ } as any,
+ {
+ role: 'tool',
+ toolCallId,
+ content: JSON.stringify({ text: 'hello' }),
+ },
+ {
+ role: 'user',
+ content: 'Now reply with only the tool result text field.',
+ },
+ ]
+
+ const second = await collectStream(
+ adapter.chatStream({
+ model: 'glm-4.7',
+ messages,
+ threadId: 'test-thread',
+ modelOptions: { temperature: 0, max_tokens: 32 },
+ logger: createNoopLogger() as any,
+ }),
+ )
+
+ expect(lastChunk(second)?.type).toBe('RUN_FINISHED')
+ expect(second.some((c) => c.type === 'RUN_ERROR')).toBe(false)
+ const full = fullTextFromChunks(second)
+ expect(typeof full).toBe('string')
+ },
+ timeout,
+ )
+
+ it(
+ 'Stream Interruption: partial responses are handled when aborted mid-stream',
+ async () => {
+ const adapter = createZAIText('glm-4.7', apiKey!)
+ const abortController = new AbortController()
+
+ const chunks = await collectStream(
+ adapter.chatStream({
+ model: 'glm-4.7',
+ messages: [
+ {
+ role: 'user',
+ content:
+ 'Write a long response of at least 200 characters about cats.',
+ },
+ ],
+ threadId: 'test-thread',
+ modelOptions: { temperature: 0.7, max_tokens: 256 },
+ abortController,
+ logger: createNoopLogger() as any,
+ } as any),
+ { abortAfterFirstContent: abortController },
+ )
+
+ expect(chunks.length).toBeGreaterThan(0)
+ expect(typeof fullTextFromChunks(chunks)).toBe('string')
+
+ const tail = lastChunk(chunks)
+ expect(
+ tail && (tail.type === 'RUN_ERROR' || tail.type === 'RUN_FINISHED'),
+ ).toBe(true)
+ },
+ timeout,
+ )
+
+ it(
+ 'Stream Interruption: connection errors surface as RUN_ERROR',
+ async () => {
+ const adapter = createZAIText('glm-4.7', apiKey!, {
+ baseURL: 'http://127.0.0.1:1',
+ })
+
+ const chunks = await collectStream(
+ adapter.chatStream({
+ model: 'glm-4.7',
+ messages: [{ role: 'user', content: 'Hi' }],
+ threadId: 'test-thread',
+ modelOptions: { max_tokens: 16 },
+ logger: createNoopLogger() as any,
+ }),
+ )
+
+ expect(lastChunk(chunks)?.type).toBe('RUN_ERROR')
+ },
+ timeout,
+ )
+
+ it(
+ 'Different Models: glm-4.7 works',
+ async () => {
+ const adapter = createZAIText('glm-4.7', apiKey!)
+ const chunks = await collectStream(
+ adapter.chatStream({
+ model: 'glm-4.7',
+ messages: [{ role: 'user', content: 'Reply with pong' }],
+ threadId: 'test-thread',
+ modelOptions: { temperature: 0, max_tokens: 16 },
+ logger: createNoopLogger() as any,
+ }),
+ )
+ expect(lastChunk(chunks)?.type).toBe('RUN_FINISHED')
+ expect(chunks.some((c) => c.type === 'RUN_ERROR')).toBe(false)
+ expect(typeof fullTextFromChunks(chunks)).toBe('string')
+ },
+ timeout,
+ )
+
+ it(
+ 'Different Models: glm-4.6v works',
+ async () => {
+ const adapter = createZAIText('glm-4.6v', apiKey!)
+ const chunks = await collectStream(
+ adapter.chatStream({
+ model: 'glm-4.6v',
+ messages: [{ role: 'user', content: 'Reply with pong' }],
+ threadId: 'test-thread',
+ modelOptions: { temperature: 0, max_tokens: 16 },
+ logger: createNoopLogger() as any,
+ } as any),
+ )
+ expect(lastChunk(chunks)?.type).toBe('RUN_FINISHED')
+ expect(chunks.some((c) => c.type === 'RUN_ERROR')).toBe(false)
+ expect(typeof fullTextFromChunks(chunks)).toBe('string')
+ },
+ timeout,
+ )
+
+ it(
+ 'Different Models: glm-4.6 works',
+ async () => {
+ const adapter = createZAIText('glm-4.6', apiKey!)
+ const chunks = await collectStream(
+ adapter.chatStream({
+ model: 'glm-4.6',
+ messages: [{ role: 'user', content: 'Reply with pong' }],
+ threadId: 'test-thread',
+ modelOptions: { temperature: 0, max_tokens: 16 },
+ logger: createNoopLogger() as any,
+ }),
+ )
+ expect(lastChunk(chunks)?.type).toBe('RUN_FINISHED')
+ expect(chunks.some((c) => c.type === 'RUN_ERROR')).toBe(false)
+ expect(typeof fullTextFromChunks(chunks)).toBe('string')
+ },
+ timeout,
+ )
+})
diff --git a/packages/ai-zai/tests/zai-adapter.test.ts b/packages/ai-zai/tests/zai-adapter.test.ts
new file mode 100644
index 0000000000..dd802084f8
--- /dev/null
+++ b/packages/ai-zai/tests/zai-adapter.test.ts
@@ -0,0 +1,715 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import type { StreamChunk, TextOptions, Tool } from '@tanstack/ai'
+import { ZAITextAdapter } from '../src/adapters/text'
+import { zaiWebSearchTool } from '../src/tools/web-search-tool'
+import type { ZAIChatModel } from '../src/model-meta'
+
+const openAIState = {
+ lastOptions: undefined as any,
+ create: vi.fn(),
+}
+
+vi.mock('openai', () => {
+ class OpenAI {
+ chat: any
+ constructor(opts: any) {
+ openAIState.lastOptions = opts
+ this.chat = {
+ completions: {
+ create: openAIState.create,
+ },
+ }
+ }
+ }
+
+ return { default: OpenAI }
+})
+
+function createAdapter(
+ overrides?: {
+ apiKey?: string
+ baseURL?: string
+ coding?: boolean
+ },
+ model: ZAIChatModel = 'glm-5.2',
+) {
+ return new ZAITextAdapter(
+ {
+ apiKey: overrides?.apiKey ?? 'test_api_key',
+ baseURL: overrides?.baseURL,
+ coding: overrides?.coding,
+ },
+ model,
+ )
+}
+
+async function collect(iterable: AsyncIterable): Promise> {
+ const result: Array = []
+ for await (const item of iterable) result.push(item)
+ return result
+}
+
+async function* streamOf(chunks: Array) {
+ for (const c of chunks) yield c
+}
+
+function createNoopLogger() {
+ return {
+ request: vi.fn(),
+ provider: vi.fn(),
+ output: vi.fn(),
+ middleware: vi.fn(),
+ tools: vi.fn(),
+ agentLoop: vi.fn(),
+ config: vi.fn(),
+ errors: vi.fn(),
+ warn: vi.fn(),
+ }
+}
+
+function baseOptions(overrides?: Partial): TextOptions {
+ return {
+ model: 'glm-5.2',
+ messages: [{ role: 'user', content: 'hi' }],
+ threadId: 'test-thread',
+ logger: createNoopLogger() as any,
+ ...overrides,
+ }
+}
+
+function mapOptions(
+ overrides?: Partial,
+ model: ZAIChatModel = 'glm-5.2',
+) {
+ const adapter = createAdapter(undefined, model) as any
+ return adapter.mapOptionsToRequest({ ...baseOptions(), ...overrides } as any)
+}
+
+const weatherTool: Tool = {
+ name: 'get_weather',
+ description: 'Get weather',
+ inputSchema: {
+ type: 'object',
+ properties: { location: { type: 'string' } },
+ required: ['location'],
+ },
+}
+
+describe('ZAITextAdapter', () => {
+ beforeEach(() => {
+ openAIState.lastOptions = undefined
+ openAIState.create.mockReset()
+ })
+
+ describe('Constructor & Initialization', () => {
+ it('initializes the OpenAI SDK with the default Z.AI baseURL', () => {
+ createAdapter()
+ expect(openAIState.lastOptions).toBeTruthy()
+ expect(openAIState.lastOptions.baseURL).toBe(
+ 'https://api.z.ai/api/paas/v4',
+ )
+ })
+
+ it('supports a custom baseURL', () => {
+ createAdapter({ baseURL: 'https://example.invalid/zai' })
+ expect(openAIState.lastOptions.baseURL).toBe(
+ 'https://example.invalid/zai',
+ )
+ })
+
+ it('sets default headers (Accept-Language)', () => {
+ createAdapter()
+ expect(openAIState.lastOptions.defaultHeaders['Accept-Language']).toBe(
+ 'en-US,en',
+ )
+ })
+
+ it('rejects a Bearer-prefixed API key', () => {
+ expect(() => createAdapter({ apiKey: 'Bearer abc' })).toThrowError(
+ /raw token/i,
+ )
+ })
+
+ it('rejects an API key with whitespace', () => {
+ expect(() => createAdapter({ apiKey: 'abc def' })).toThrowError(
+ /whitespace/i,
+ )
+ })
+
+ it('uses the coding endpoint when coding: true', () => {
+ createAdapter({ coding: true })
+ expect(openAIState.lastOptions.baseURL).toBe(
+ 'https://api.z.ai/api/coding/paas/v4',
+ )
+ })
+
+ it('lets an explicit baseURL override the coding flag', () => {
+ createAdapter({ coding: true, baseURL: 'https://example.invalid/zai' })
+ expect(openAIState.lastOptions.baseURL).toBe(
+ 'https://example.invalid/zai',
+ )
+ })
+ })
+
+ describe('Options Mapping', () => {
+ it('passes sampling modelOptions through with Z.AI wire names', () => {
+ const mapped = mapOptions({
+ modelOptions: { max_tokens: 123, temperature: 0.7, top_p: 0.9 },
+ })
+
+ expect(mapped).toMatchObject({
+ model: 'glm-5.2',
+ max_tokens: 123,
+ temperature: 0.7,
+ top_p: 0.9,
+ stream: true,
+ })
+ expect(mapped.messages).toEqual([{ role: 'user', content: 'hi' }])
+ })
+
+ it('converts function tools and pins tool_choice to auto', () => {
+ const mapped = mapOptions({ tools: [weatherTool] })
+
+ expect(mapped.tools).toHaveLength(1)
+ expect(mapped.tools[0]).toMatchObject({
+ type: 'function',
+ function: { name: 'get_weather', strict: true },
+ })
+ expect(mapped.tools[0].function.parameters.additionalProperties).toBe(
+ false,
+ )
+ expect(mapped.tool_choice).toBe('auto')
+ })
+
+ it('converts the branded web_search tool to its native wire shape', () => {
+ const mapped = mapOptions({
+ tools: [zaiWebSearchTool({ search_query: 'tanstack' })],
+ })
+
+ expect(mapped.tools).toEqual([
+ {
+ type: 'web_search',
+ web_search: { enable: true, search_query: 'tanstack' },
+ },
+ ])
+ expect(mapped.tool_choice).toBe('auto')
+ })
+
+ it('forces tool_choice auto even when the caller asks for required', () => {
+ const mapped = mapOptions({
+ tools: [weatherTool],
+ toolChoice: 'required',
+ })
+
+ expect(mapped.tool_choice).toBe('auto')
+ })
+
+ it('forces tool_choice auto over a modelOptions.tool_choice', () => {
+ const mapped = mapOptions({
+ tools: [weatherTool],
+ modelOptions: { tool_choice: 'none' } as any,
+ })
+
+ expect(mapped.tool_choice).toBe('auto')
+ })
+
+ it('sends no tool_choice when the request has no tools', () => {
+ const mapped = mapOptions()
+ expect(mapped).not.toHaveProperty('tool_choice')
+ })
+
+ it('drops the tools when the caller asks for toolChoice none', () => {
+ const mapped = mapOptions({
+ tools: [weatherTool],
+ toolChoice: 'none',
+ })
+
+ expect(mapped).not.toHaveProperty('tools')
+ expect(mapped).not.toHaveProperty('tool_choice')
+ })
+
+ it('warns once when a forcing choice falls back to auto', () => {
+ const warn = vi.fn()
+ const adapter = createAdapter() as any
+ const options = {
+ ...baseOptions(),
+ tools: [weatherTool],
+ toolChoice: 'required',
+ logger: { ...createNoopLogger(), warn },
+ }
+
+ adapter.mapOptionsToRequest(options)
+ adapter.mapOptionsToRequest(options)
+
+ expect(warn).toHaveBeenCalledTimes(1)
+ })
+
+ it('maps a reasoning level to reasoning_effort for glm-5.2', () => {
+ const mapped = mapOptions({
+ reasoning: { level: 'max', summary: false },
+ })
+ expect(mapped.reasoning_effort).toBe('max')
+ })
+
+ it('maps every reasoning level for glm-5.2', () => {
+ const mapped = mapOptions({
+ reasoning: { level: 'low', summary: true },
+ })
+ expect(mapped.reasoning_effort).toBe('low')
+
+ const off = mapOptions({ reasoning: { level: 'off', summary: false } })
+ expect(off.reasoning_effort).toBe('none')
+ })
+
+ it('passes low effort through for glm-5.3', () => {
+ const mapped = mapOptions({
+ model: 'glm-5.3',
+ reasoning: { level: 'low', summary: true },
+ })
+ expect(mapped.reasoning_effort).toBe('low')
+ })
+
+ it('passes low effort through for glm-5.3-flash', () => {
+ const mapped = mapOptions({
+ model: 'glm-5.3-flash',
+ reasoning: { level: 'low', summary: true },
+ })
+ expect(mapped.reasoning_effort).toBe('low')
+ })
+
+ it('clamps a level below the model minimum up to low', () => {
+ const mapped = mapOptions({
+ model: 'glm-5.3',
+ reasoning: { level: 'off', summary: false },
+ })
+ expect(mapped.reasoning_effort).toBe('low')
+ })
+
+ it('sends no reasoning_effort for a model that does not reason', () => {
+ const mapped = mapOptions({
+ model: 'glm-4.7',
+ reasoning: { level: 'high', summary: true },
+ })
+ expect(mapped).not.toHaveProperty('reasoning_effort')
+ })
+ })
+
+ describe('Structured Output', () => {
+ it('sends json_object plus the schema as a system prompt, not json_schema', async () => {
+ const adapter = createAdapter()
+ openAIState.create.mockResolvedValueOnce({
+ id: 'resp_1',
+ model: 'glm-5.2',
+ choices: [
+ {
+ message: { content: '{"name":"Ada"}' },
+ finish_reason: 'stop',
+ },
+ ],
+ usage: { prompt_tokens: 1, completion_tokens: 2, total_tokens: 3 },
+ })
+
+ const result = await adapter.structuredOutput({
+ chatOptions: baseOptions({
+ messages: [{ role: 'user', content: 'Return a person as json' }],
+ }),
+ outputSchema: {
+ type: 'object',
+ properties: { name: { type: 'string' } },
+ required: ['name'],
+ },
+ })
+
+ expect(result.data).toEqual({ name: 'Ada' })
+
+ const params = openAIState.create.mock.calls[0]![0] as any
+ expect(params.response_format).toEqual({ type: 'json_object' })
+ expect(params.messages[0]).toEqual({
+ role: 'system',
+ content: expect.stringContaining('"required":["name"]'),
+ })
+ expect(params.messages[1]).toEqual({
+ role: 'user',
+ content: 'Return a person as json',
+ })
+ })
+
+ it('opts out of combined tools+schema mode', () => {
+ expect(createAdapter().supportsCombinedToolsAndSchema()).toBe(false)
+ })
+ })
+
+ describe('Message Conversion', () => {
+ it('passes a simple user text message through', () => {
+ const mapped = mapOptions({
+ messages: [{ role: 'user', content: 'hi' }],
+ })
+ expect(mapped.messages).toEqual([{ role: 'user', content: 'hi' }])
+ })
+
+ it('merges system prompts into a leading system message', () => {
+ const mapped = mapOptions({
+ systemPrompts: ['You are helpful', 'Be concise'],
+ })
+
+ expect(mapped.messages).toEqual([
+ { role: 'system', content: 'You are helpful\nBe concise' },
+ { role: 'user', content: 'hi' },
+ ])
+ })
+
+ it('converts tool result messages', () => {
+ const mapped = mapOptions({
+ messages: [
+ {
+ role: 'tool',
+ toolCallId: 'call_1',
+ content: '{"ok":true}',
+ },
+ ],
+ })
+
+ expect(mapped.messages).toEqual([
+ {
+ role: 'tool',
+ tool_call_id: 'call_1',
+ content: '{"ok":true}',
+ },
+ ])
+ })
+
+ it('keeps multi-turn text history', () => {
+ const mapped = mapOptions({
+ messages: [
+ { role: 'user', content: 'hi' },
+ { role: 'assistant', content: 'hello' },
+ { role: 'user', content: 'how are you' },
+ ],
+ })
+
+ expect(mapped.messages).toEqual([
+ { role: 'user', content: 'hi' },
+ { role: 'assistant', content: 'hello' },
+ { role: 'user', content: 'how are you' },
+ ])
+ })
+
+ it('maps image parts to image_url for a vision model', () => {
+ const mapped = mapOptions(
+ {
+ messages: [
+ {
+ role: 'user',
+ content: [
+ {
+ type: 'image',
+ source: { type: 'url', value: 'https://x/y.png' },
+ },
+ { type: 'text', content: 'hello' },
+ ],
+ } as any,
+ ],
+ },
+ 'glm-4.6v',
+ )
+
+ expect(mapped.messages).toEqual([
+ {
+ role: 'user',
+ content: [
+ {
+ type: 'image_url',
+ image_url: { url: 'https://x/y.png', detail: 'auto' },
+ },
+ { type: 'text', text: 'hello' },
+ ],
+ },
+ ])
+ })
+
+ it('maps video parts to video_url', () => {
+ const mapped = mapOptions(
+ {
+ messages: [
+ {
+ role: 'user',
+ content: [
+ {
+ type: 'video',
+ source: { type: 'url', value: 'https://x/clip.mp4' },
+ },
+ { type: 'text', content: 'describe' },
+ ],
+ } as any,
+ ],
+ },
+ 'glm-4.6v',
+ )
+
+ expect(mapped.messages).toEqual([
+ {
+ role: 'user',
+ content: [
+ {
+ type: 'video_url',
+ video_url: { url: 'https://x/clip.mp4' },
+ },
+ { type: 'text', text: 'describe' },
+ ],
+ },
+ ])
+ })
+ it('rejects a base64 video source', () => {
+ expect(() =>
+ mapOptions(
+ {
+ messages: [
+ {
+ role: 'user',
+ content: [
+ { type: 'video', source: { type: 'data', value: 'AAAA' } },
+ ],
+ } as any,
+ ],
+ },
+ 'glm-4.6v',
+ ),
+ ).toThrow('Z.AI video input needs an HTTP(S) URL')
+ })
+ })
+
+ describe('Streaming Behavior (AG-UI Protocol)', () => {
+ it('emits the text lifecycle and picks up the trailing usage-only chunk', async () => {
+ const adapter = createAdapter()
+ openAIState.create.mockResolvedValueOnce(
+ streamOf([
+ {
+ id: 'resp_1',
+ model: 'glm-5.2',
+ choices: [{ delta: { content: 'He' } }],
+ },
+ {
+ id: 'resp_1',
+ model: 'glm-5.2',
+ choices: [{ delta: { content: 'llo' }, finish_reason: 'stop' }],
+ },
+ {
+ id: 'resp_1',
+ model: 'glm-5.2',
+ choices: [],
+ usage: { prompt_tokens: 1, completion_tokens: 2, total_tokens: 3 },
+ },
+ ]),
+ )
+
+ const chunks = await collect(adapter.chatStream(baseOptions()))
+ const types = chunks.map((c) => c.type)
+
+ expect(types).toContain('RUN_STARTED')
+ expect(types).toContain('TEXT_MESSAGE_START')
+ expect(types.filter((t) => t === 'TEXT_MESSAGE_CONTENT')).toHaveLength(2)
+ expect(types).toContain('TEXT_MESSAGE_END')
+ expect(types).toContain('RUN_FINISHED')
+
+ const contentChunks = chunks.filter(
+ (c): c is Extract =>
+ c.type === 'TEXT_MESSAGE_CONTENT',
+ )
+ expect(contentChunks[0]!.delta).toBe('He')
+ expect(contentChunks[1]!.delta).toBe('llo')
+
+ const done = chunks.find((c) => c.type === 'RUN_FINISHED') as any
+ expect(done.finishReason).toBe('stop')
+ expect(done.usage).toEqual({
+ promptTokens: 1,
+ completionTokens: 2,
+ totalTokens: 3,
+ })
+ })
+
+ it('streams reasoning_content before the answer text', async () => {
+ const adapter = createAdapter()
+ openAIState.create.mockResolvedValueOnce(
+ streamOf([
+ {
+ id: 'resp_1',
+ model: 'glm-5.2',
+ choices: [{ delta: { reasoning_content: 'think ' } }],
+ },
+ {
+ id: 'resp_1',
+ model: 'glm-5.2',
+ choices: [{ delta: { reasoning_content: 'hard' } }],
+ },
+ {
+ id: 'resp_1',
+ model: 'glm-5.2',
+ choices: [{ delta: { content: 'Answer' }, finish_reason: 'stop' }],
+ },
+ ]),
+ )
+
+ const chunks = await collect(adapter.chatStream(baseOptions()))
+ const types: Array = chunks.map((c) => c.type)
+
+ expect(types).toContain('REASONING_START')
+ expect(types).toContain('REASONING_MESSAGE_START')
+ expect(
+ types.filter((t) => t === 'REASONING_MESSAGE_CONTENT'),
+ ).toHaveLength(2)
+ expect(types).toContain('REASONING_MESSAGE_END')
+ expect(types).toContain('REASONING_END')
+ expect(types).toContain('TEXT_MESSAGE_START')
+
+ expect(types.indexOf('REASONING_MESSAGE_END')).toBeLessThan(
+ types.indexOf('TEXT_MESSAGE_START'),
+ )
+ })
+
+ it('emits the tool call lifecycle', async () => {
+ const adapter = createAdapter()
+ openAIState.create.mockResolvedValueOnce(
+ streamOf([
+ {
+ id: 'resp_1',
+ model: 'glm-5.2',
+ choices: [
+ {
+ delta: {
+ tool_calls: [
+ {
+ index: 0,
+ id: 'call_1',
+ function: { name: 'get_weather', arguments: '{"q":' },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ {
+ id: 'resp_1',
+ model: 'glm-5.2',
+ choices: [
+ {
+ delta: {
+ tool_calls: [{ index: 0, function: { arguments: '"SF"}' } }],
+ },
+ finish_reason: 'tool_calls',
+ },
+ ],
+ },
+ ]),
+ )
+
+ const chunks = await collect(
+ adapter.chatStream(baseOptions({ tools: [weatherTool] })),
+ )
+ const types = chunks.map((c) => c.type)
+
+ expect(types).toContain('TOOL_CALL_START')
+ expect(types).toContain('TOOL_CALL_ARGS')
+ expect(types).toContain('TOOL_CALL_END')
+ expect(types).toContain('RUN_FINISHED')
+
+ const toolStart = chunks.find((c) => c.type === 'TOOL_CALL_START') as any
+ expect(toolStart.toolCallId).toBe('call_1')
+ expect(toolStart.toolCallName).toBe('get_weather')
+
+ const toolEnd = chunks.find((c) => c.type === 'TOOL_CALL_END') as any
+ expect(toolEnd.toolCallId).toBe('call_1')
+ expect(toolEnd.toolCallName).toBe('get_weather')
+ expect(toolEnd.input).toEqual({ q: 'SF' })
+
+ const done = chunks.find((c) => c.type === 'RUN_FINISHED') as any
+ expect(done.finishReason).toBe('tool_calls')
+ })
+
+ it('yields RUN_ERROR instead of throwing on a client error', async () => {
+ const adapter = createAdapter()
+ openAIState.create.mockRejectedValueOnce(new Error('network down'))
+
+ const chunks = await collect(adapter.chatStream(baseOptions()))
+ const error = chunks.find((c) => c.type === 'RUN_ERROR') as any
+
+ expect(error).toBeTruthy()
+ expect(error.message).toMatch(/network down/i)
+ })
+
+ it('flags an exhausted stream with no finish_reason as incomplete', async () => {
+ const adapter = createAdapter()
+ openAIState.create.mockResolvedValueOnce(
+ streamOf([{ id: 'resp_1', model: 'glm-5.2' }]),
+ )
+
+ const chunks = await collect(adapter.chatStream(baseOptions()))
+ const error = chunks.find((c) => c.type === 'RUN_ERROR') as any
+
+ expect(error).toBeTruthy()
+ expect(error.code).toBe('incomplete-stream')
+ expect(chunks.some((c) => c.type === 'RUN_FINISHED')).toBe(false)
+ })
+
+ it('handles an empty messages array without crashing', async () => {
+ const adapter = createAdapter()
+ openAIState.create.mockResolvedValueOnce(
+ streamOf([
+ {
+ id: 'resp_1',
+ model: 'glm-5.2',
+ choices: [{ delta: { content: 'ok' }, finish_reason: 'stop' }],
+ },
+ ]),
+ )
+
+ const chunks = await collect(
+ adapter.chatStream(baseOptions({ messages: [] })),
+ )
+
+ const callArgs = openAIState.create.mock.calls[0]!
+ expect(callArgs[0].messages).toEqual([])
+ expect(chunks.some((c) => c.type === 'RUN_FINISHED')).toBe(true)
+ })
+
+ it('passes request headers through to the SDK call', async () => {
+ const adapter = createAdapter()
+ openAIState.create.mockResolvedValueOnce(
+ streamOf([
+ {
+ id: 'resp_1',
+ model: 'glm-5.2',
+ choices: [{ delta: { content: 'ok' }, finish_reason: 'stop' }],
+ },
+ ]),
+ )
+
+ await collect(
+ adapter.chatStream(
+ baseOptions({ request: { headers: { 'X-Test': '1' } } }),
+ ),
+ )
+
+ const callArgs = openAIState.create.mock.calls[0]!
+ expect(callArgs[1].headers).toEqual({ 'X-Test': '1' })
+ })
+
+ it('logs the request with the provider name', async () => {
+ const logger = createNoopLogger()
+ const adapter = createAdapter()
+ openAIState.create.mockResolvedValueOnce(
+ streamOf([
+ {
+ id: 'resp_1',
+ model: 'glm-5.2',
+ choices: [{ delta: { content: 'ok' }, finish_reason: 'stop' }],
+ },
+ ]),
+ )
+
+ await collect(adapter.chatStream(baseOptions({ logger: logger as any })))
+
+ expect(logger.request).toHaveBeenCalled()
+ expect(logger.request.mock.calls[0]![0]).toContain('provider=zai')
+ })
+ })
+})
diff --git a/packages/ai-zai/tests/zai-factory.test.ts b/packages/ai-zai/tests/zai-factory.test.ts
new file mode 100644
index 0000000000..2a3ceaa184
--- /dev/null
+++ b/packages/ai-zai/tests/zai-factory.test.ts
@@ -0,0 +1,150 @@
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { createZAIText, zaiText } from '../src/adapters/text'
+import {
+ createZAISummarize,
+ zaiSummarize,
+} from '../src/adapters/summarize'
+import { ZAITextAdapter } from '../src/adapters/text'
+
+const openAIState = {
+ lastOptions: undefined as any,
+}
+
+vi.mock('openai', () => {
+ class OpenAI {
+ chat: any
+ constructor(opts: any) {
+ openAIState.lastOptions = opts
+ this.chat = {
+ completions: {
+ create: vi.fn(),
+ },
+ }
+ }
+ }
+
+ return { default: OpenAI }
+})
+
+describe('Z.AI provider factories', () => {
+ afterEach(() => {
+ vi.unstubAllEnvs()
+ openAIState.lastOptions = undefined
+ })
+
+ describe('createZAIText', () => {
+ it('creates an adapter with an explicit API key', () => {
+ const adapter = createZAIText('glm-5.2', 'test_key')
+ expect(adapter).toBeInstanceOf(ZAITextAdapter)
+ expect(adapter.kind).toBe('text')
+ expect(adapter.name).toBe('zai')
+ expect(adapter.model).toBe('glm-5.2')
+ })
+
+ it('throws when the API key is empty', () => {
+ expect(() => createZAIText('glm-5.2', '')).toThrowError(
+ /API key is required/i,
+ )
+ })
+
+ it('accepts a custom baseURL', () => {
+ createZAIText('glm-5.2', 'test_key', {
+ baseURL: 'https://example.invalid/zai',
+ })
+ expect(openAIState.lastOptions.baseURL).toBe(
+ 'https://example.invalid/zai',
+ )
+ })
+
+ it('sets the Z.AI default headers', () => {
+ createZAIText('glm-5.2', 'test_key')
+ expect(openAIState.lastOptions.defaultHeaders['Accept-Language']).toBe(
+ 'en-US,en',
+ )
+ })
+
+ it('uses the coding endpoint when coding: true', () => {
+ createZAIText('glm-5.2', 'test_key', { coding: true })
+ expect(openAIState.lastOptions.baseURL).toBe(
+ 'https://api.z.ai/api/coding/paas/v4',
+ )
+ })
+
+ it('lets an explicit baseURL override the coding flag', () => {
+ createZAIText('glm-5.2', 'test_key', {
+ coding: true,
+ baseURL: 'https://custom.url',
+ })
+ expect(openAIState.lastOptions.baseURL).toBe('https://custom.url')
+ })
+ })
+
+ describe('zaiText', () => {
+ it('reads the key from ZAI_API_KEY', () => {
+ vi.stubEnv('ZAI_API_KEY', 'env_key')
+ const adapter = zaiText('glm-5.2')
+ expect(adapter).toBeInstanceOf(ZAITextAdapter)
+ expect(adapter.model).toBe('glm-5.2')
+ })
+
+ it('throws when ZAI_API_KEY is not set', () => {
+ vi.stubEnv('ZAI_API_KEY', '')
+ expect(() => zaiText('glm-5.2')).toThrowError(/ZAI_API_KEY is required/i)
+ })
+
+ it('accepts a custom baseURL', () => {
+ vi.stubEnv('ZAI_API_KEY', 'env_key')
+ zaiText('glm-5.2', { baseURL: 'https://example.invalid/zai' })
+ expect(openAIState.lastOptions.baseURL).toBe(
+ 'https://example.invalid/zai',
+ )
+ })
+
+ it('uses the coding endpoint when coding: true', () => {
+ vi.stubEnv('ZAI_API_KEY', 'env_key')
+ zaiText('glm-5.2', { coding: true })
+ expect(openAIState.lastOptions.baseURL).toBe(
+ 'https://api.z.ai/api/coding/paas/v4',
+ )
+ })
+ })
+
+ describe('Summarize factories', () => {
+ it('creates a summarize adapter wrapping the text adapter', () => {
+ const adapter = createZAISummarize('glm-5.2', 'test_key')
+ expect(adapter.kind).toBe('summarize')
+ expect(adapter.name).toBe('zai')
+ expect(adapter.model).toBe('glm-5.2')
+ })
+
+ it('reads the key from ZAI_API_KEY', () => {
+ vi.stubEnv('ZAI_API_KEY', 'env_key')
+ const adapter = zaiSummarize('glm-5.2')
+ expect(adapter.kind).toBe('summarize')
+ })
+
+ it('throws when ZAI_API_KEY is not set', () => {
+ vi.stubEnv('ZAI_API_KEY', '')
+ expect(() => zaiSummarize('glm-5.2')).toThrowError(
+ /ZAI_API_KEY is required/i,
+ )
+ })
+ })
+
+ describe('Type Safety', () => {
+ it('rejects an unknown model at the type level', () => {
+ const adapter = createZAIText('glm-5.2', 'test_key')
+ expect(adapter.model).toBe('glm-5.2')
+
+ // @ts-expect-error invalid model name is caught by types
+ createZAIText('not-a-real-model', 'test_key')
+ })
+
+ it('rejects a non-string baseURL at the type level', () => {
+ vi.stubEnv('ZAI_API_KEY', 'env_key')
+
+ // @ts-expect-error baseURL must be a string if provided
+ zaiText('glm-5.2', { baseURL: 123 })
+ })
+ })
+})
diff --git a/packages/ai-zai/tsconfig.json b/packages/ai-zai/tsconfig.json
new file mode 100644
index 0000000000..c38689f4ea
--- /dev/null
+++ b/packages/ai-zai/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "dist"
+ },
+ "include": ["src", "tests"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/ai-zai/vite.config.ts b/packages/ai-zai/vite.config.ts
new file mode 100644
index 0000000000..a093da4863
--- /dev/null
+++ b/packages/ai-zai/vite.config.ts
@@ -0,0 +1,45 @@
+import { defineConfig, mergeConfig } from 'vitest/config'
+import { loadEnv } from 'vite'
+import { tanstackViteConfig } from '@tanstack/vite-config'
+import packageJson from './package.json'
+
+const mode = process.env.NODE_ENV ?? 'test'
+const env = loadEnv(mode, process.cwd(), '')
+for (const [key, value] of Object.entries(env)) {
+ if (process.env[key] === undefined || process.env[key] === '') {
+ process.env[key] = value
+ }
+}
+
+const config = defineConfig({
+ test: {
+ name: packageJson.name,
+ dir: './',
+ watch: false,
+ globals: true,
+ environment: 'node',
+ include: ['tests/**/*.test.ts'],
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'json', 'html', 'lcov'],
+ exclude: [
+ 'node_modules/',
+ 'dist/',
+ 'tests/',
+ '**/*.test.ts',
+ '**/*.config.ts',
+ '**/types.ts',
+ ],
+ include: ['src/**/*.ts'],
+ },
+ },
+})
+
+export default mergeConfig(
+ config,
+ tanstackViteConfig({
+ entry: ['./src/index.ts', './src/byok.ts', './src/tools/index.ts'],
+ srcDir: './src',
+ cjs: false,
+ }),
+)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5ef6b0003b..94917419e0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -846,7 +846,7 @@ importers:
version: 1.170.41(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tanstack/react-start':
specifier: ^1.168.60
- version: 1.168.60(crossws@0.4.6(srvx@0.11.22))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.9)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
+ version: 1.168.60(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.9)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
lucide-react:
specifier: ^0.561.0
version: 0.561.0(react@19.2.3)
@@ -1029,6 +1029,9 @@ importers:
'@tanstack/ai-react':
specifier: workspace:*
version: link:../../packages/ai-react
+ '@tanstack/ai-zai':
+ specifier: workspace:*
+ version: link:../../packages/ai-zai
'@tanstack/react-router':
specifier: ^1.170.41
version: 1.170.41(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -1146,7 +1149,7 @@ importers:
version: 1.159.5(@tanstack/react-router@1.170.41(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.171.34)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tanstack/react-start':
specifier: ^1.168.60
- version: 1.168.60(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.9)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
+ version: 1.168.60(crossws@0.4.6(srvx@0.11.22))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.9)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
'@tanstack/router-plugin':
specifier: ^1.168.42
version: 1.168.42(@tanstack/react-router@1.170.41(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.1)(rolldown@1.2.9)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
@@ -1222,7 +1225,7 @@ importers:
version: 7.1.1(@cfworker/json-schema@4.1.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@modelcontextprotocol/sdk':
specifier: ^1.29.0
- version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.2.1)
+ version: 1.29.0(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.2.1)
'@ngrok/ngrok':
specifier: ^1.7.0
version: 1.7.0
@@ -1322,6 +1325,9 @@ importers:
'@tanstack/ai-sandbox-vercel':
specifier: workspace:*
version: link:../../packages/ai-sandbox-vercel
+ '@tanstack/ai-zai':
+ specifier: workspace:*
+ version: link:../../packages/ai-zai
'@tanstack/react-devtools':
specifier: ^0.9.10
version: 0.9.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10)
@@ -1647,12 +1653,12 @@ importers:
'@tanstack/ai':
specifier: workspace:*
version: link:../../packages/ai
- '@tanstack/ai-groq':
- specifier: workspace:*
- version: link:../../packages/ai-groq
'@tanstack/ai-react':
specifier: workspace:*
version: link:../../packages/ai-react
+ '@tanstack/ai-zai':
+ specifier: workspace:*
+ version: link:../../packages/ai-zai
'@tanstack/query-db-collection':
specifier: ^1.0.6
version: 1.0.36(@tanstack/query-core@5.90.12)(typescript@5.9.3)
@@ -1905,18 +1911,30 @@ importers:
'@tanstack/ai-gemini':
specifier: workspace:*
version: link:../../packages/ai-gemini
+ '@tanstack/ai-grok':
+ specifier: workspace:*
+ version: link:../../packages/ai-grok
+ '@tanstack/ai-groq':
+ specifier: workspace:*
+ version: link:../../packages/ai-groq
'@tanstack/ai-ollama':
specifier: workspace:*
version: link:../../packages/ai-ollama
'@tanstack/ai-openai':
specifier: workspace:*
version: link:../../packages/ai-openai
+ '@tanstack/ai-openrouter':
+ specifier: workspace:*
+ version: link:../../packages/ai-openrouter
'@tanstack/ai-solid':
specifier: workspace:*
version: link:../../packages/ai-solid
+ '@tanstack/ai-zai':
+ specifier: workspace:*
+ version: link:../../packages/ai-zai
'@tanstack/nitro-v2-vite-plugin':
specifier: ^1.155.0
- version: 1.155.0(@electric-sql/pglite@0.5.11)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)(rolldown@1.2.4)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
+ version: 1.155.0(@electric-sql/pglite@0.5.11)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)(rolldown@1.2.4)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
'@tanstack/router-plugin':
specifier: ^1.168.42
version: 1.168.42(@tanstack/react-router@1.170.41(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.1)(rolldown@1.2.9)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
@@ -2020,6 +2038,9 @@ importers:
'@tanstack/ai-svelte':
specifier: workspace:*
version: link:../../packages/ai-svelte
+ '@tanstack/ai-zai':
+ specifier: workspace:*
+ version: link:../../packages/ai-zai
highlight.js:
specifier: ^11.11.1
version: 11.11.1
@@ -2093,6 +2114,9 @@ importers:
'@tanstack/ai-vue':
specifier: workspace:*
version: link:../../packages/ai-vue
+ '@tanstack/ai-zai':
+ specifier: workspace:*
+ version: link:../../packages/ai-zai
marked:
specifier: ^15.0.6
version: 15.0.12
@@ -2226,7 +2250,7 @@ importers:
devDependencies:
'@analogjs/vite-plugin-angular':
specifier: ^2.6.2
- version: 2.6.3(@angular/build@21.2.21(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.20)(typescript@5.9.3))(@angular/compiler@21.2.20)(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.20(@angular/common@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1)))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.10.3)(chokidar@5.0.0)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.33.0)(ng-packagr@21.2.5(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.20)(typescript@5.9.3))(tailwindcss@4.1.18)(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.26)(tailwindcss@4.1.18)(terser@5.44.1)(tslib@2.8.1)(tsx@4.23.12)(typescript@5.9.3)(vitest@4.1.10)(yaml@2.9.0))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(vite@8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
+ version: 2.6.3(@angular/build@21.2.21(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.20)(typescript@5.9.3))(@angular/compiler@21.2.20)(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.20(@angular/common@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1)))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.10.3)(chokidar@5.0.0)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.33.0)(ng-packagr@21.2.5(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.20)(typescript@5.9.3))(tailwindcss@4.1.18)(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.26)(tailwindcss@4.1.18)(terser@5.44.1)(tslib@2.8.1)(tsx@4.23.12)(typescript@5.9.3)(vitest@4.1.10)(yaml@2.9.0))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(vite@8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
'@angular/build':
specifier: ^21.2.21
version: 21.2.21(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.20)(typescript@5.9.3))(@angular/compiler@21.2.20)(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.20(@angular/common@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1)))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.10.3)(chokidar@5.0.0)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.33.0)(ng-packagr@21.2.5(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.20)(typescript@5.9.3))(tailwindcss@4.1.18)(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.26)(tailwindcss@4.1.18)(terser@5.44.1)(tslib@2.8.1)(tsx@4.23.12)(typescript@5.9.3)(vitest@4.1.10)(yaml@2.9.0)
@@ -2262,7 +2286,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^4.1.10
- version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.2)(jsdom@27.4.0(@noble/hashes@2.3.0))(vite@8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
+ version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.2)(jsdom@27.4.0(@noble/hashes@2.3.0))(vite@8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
zod:
specifier: ^4.2.0
version: 4.3.6
@@ -3046,7 +3070,7 @@ importers:
version: link:../ai-event-client
ioredis:
specifier: '>=5.0.0'
- version: 5.9.2(supports-color@7.2.0)
+ version: 5.9.2
devDependencies:
'@honcho-ai/sdk':
specifier: ^2.1.1
@@ -3062,7 +3086,7 @@ importers:
version: 4.1.10(vitest@4.1.10)
ioredis-mock:
specifier: ^8.9.0
- version: 8.13.1(@types/ioredis-mock@8.2.7(ioredis@5.9.2(supports-color@7.2.0)))(ioredis@5.9.2(supports-color@7.2.0))
+ version: 8.13.1(@types/ioredis-mock@8.2.7(ioredis@5.9.2))(ioredis@5.9.2)
redis:
specifier: ^4.7.0
version: 4.7.1
@@ -3836,6 +3860,28 @@ importers:
specifier: ^8.2.1
version: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)
+ packages/ai-zai:
+ dependencies:
+ '@tanstack/ai':
+ specifier: workspace:^
+ version: link:../ai
+ '@tanstack/ai-utils':
+ specifier: workspace:^
+ version: link:../ai-utils
+ '@tanstack/openai-base':
+ specifier: workspace:^
+ version: link:../openai-base
+ openai:
+ specifier: ^7.30.0
+ version: 7.30.0(@aws-sdk/credential-provider-node@3.972.58)(@smithy/signature-v4@5.5.2)(undici@8.10.2)(ws@8.21.3)(zod@4.3.6)
+ devDependencies:
+ '@vitest/coverage-v8':
+ specifier: 4.1.10
+ version: 4.1.10(vitest@4.1.11)
+ vite:
+ specifier: ^8.2.1
+ version: 8.3.0(@types/node@24.13.6)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)
+
packages/openai-base:
dependencies:
'@tanstack/ai-utils':
@@ -4101,6 +4147,9 @@ importers:
'@tanstack/ai-worldlabs':
specifier: workspace:*
version: link:../../packages/ai-worldlabs
+ '@tanstack/ai-zai':
+ specifier: workspace:*
+ version: link:../../packages/ai-zai
'@tanstack/devtools-event-bus':
specifier: ^0.4.1
version: 0.4.1
@@ -19598,7 +19647,7 @@ snapshots:
- '@emnapi/core'
- '@emnapi/runtime'
- ? '@analogjs/vite-plugin-angular@2.6.3(@angular/build@21.2.21(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.20)(typescript@5.9.3))(@angular/compiler@21.2.20)(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.20(@angular/common@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1)))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.10.3)(chokidar@5.0.0)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.33.0)(ng-packagr@21.2.5(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.20)(typescript@5.9.3))(tailwindcss@4.1.18)(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.26)(tailwindcss@4.1.18)(terser@5.44.1)(tslib@2.8.1)(tsx@4.23.12)(typescript@5.9.3)(vitest@4.1.10)(yaml@2.9.0))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(vite@8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))'
+ ? '@analogjs/vite-plugin-angular@2.6.3(@angular/build@21.2.21(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.20)(typescript@5.9.3))(@angular/compiler@21.2.20)(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.20(@angular/common@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1)))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.10.3)(chokidar@5.0.0)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.33.0)(ng-packagr@21.2.5(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.20)(typescript@5.9.3))(tailwindcss@4.1.18)(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.26)(tailwindcss@4.1.18)(terser@5.44.1)(tslib@2.8.1)(tsx@4.23.12)(typescript@5.9.3)(vitest@4.1.10)(yaml@2.9.0))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(vite@8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))'
: dependencies:
magic-string: 0.30.21
obug: 2.1.1
@@ -19608,7 +19657,7 @@ snapshots:
typescript: 5.9.3
optionalDependencies:
'@angular/build': 21.2.21(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.20)(typescript@5.9.3))(@angular/compiler@21.2.20)(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.20(@angular/common@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)(zone.js@0.15.1)))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.10.3)(chokidar@5.0.0)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.33.0)(ng-packagr@21.2.5(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.20)(typescript@5.9.3))(tailwindcss@4.1.18)(tslib@2.8.1)(typescript@5.9.3))(postcss@8.5.26)(tailwindcss@4.1.18)(terser@5.44.1)(tslib@2.8.1)(tsx@4.23.12)(typescript@5.9.3)(vitest@4.1.10)(yaml@2.9.0)
- vite: 8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)
+ vite: 8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -19729,7 +19778,7 @@ snapshots:
ng-packagr: 21.2.5(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.20)(typescript@5.9.3))(tailwindcss@4.1.18)(tslib@2.8.1)(typescript@5.9.3)
postcss: 8.5.26
tailwindcss: 4.1.18
- vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.2)(jsdom@27.4.0(@noble/hashes@2.3.0))(vite@8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
+ vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.2)(jsdom@27.4.0(@noble/hashes@2.3.0))(vite@8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -23209,7 +23258,7 @@ snapshots:
'@mcp-ui/client@7.1.1(@cfworker/json-schema@4.1.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
- '@modelcontextprotocol/ext-apps': 1.7.4(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)
+ '@modelcontextprotocol/ext-apps': 1.7.4(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.2.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)
'@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -23310,16 +23359,16 @@ snapshots:
dependencies:
zod: 4.3.6
- '@modelcontextprotocol/ext-apps@1.7.4(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)':
+ '@modelcontextprotocol/ext-apps@1.7.4(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.2.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)':
dependencies:
- '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)
+ '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.2.1)
'@standard-schema/spec': 1.1.0
zod: 3.25.76
optionalDependencies:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
- '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)':
+ '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.2.1)':
dependencies:
'@hono/node-server': 1.19.17(hono@4.13.8)
ajv: 8.20.0
@@ -23330,20 +23379,20 @@ snapshots:
eventsource: 3.0.7
eventsource-parser: 3.1.1
express: 5.2.1
- express-rate-limit: 8.7.0(express@5.2.1)
+ express-rate-limit: 8.7.0(express@5.2.1)(supports-color@7.2.0)
hono: 4.13.8
jose: 6.2.12
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
raw-body: 3.0.2
- zod: 3.25.76
- zod-to-json-schema: 3.25.2(zod@3.25.76)
+ zod: 4.2.1
+ zod-to-json-schema: 3.25.2(zod@4.2.1)
optionalDependencies:
'@cfworker/json-schema': 4.1.1
transitivePeerDependencies:
- supports-color
- '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.2.1)':
+ '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)':
dependencies:
'@hono/node-server': 1.19.17(hono@4.13.8)
ajv: 8.20.0
@@ -23354,14 +23403,14 @@ snapshots:
eventsource: 3.0.7
eventsource-parser: 3.1.1
express: 5.2.1
- express-rate-limit: 8.7.0(express@5.2.1)
+ express-rate-limit: 8.7.0(express@5.2.1)(supports-color@7.2.0)
hono: 4.13.8
jose: 6.2.12
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
raw-body: 3.0.2
- zod: 4.2.1
- zod-to-json-schema: 3.25.2(zod@4.2.1)
+ zod: 3.25.76
+ zod-to-json-schema: 3.25.2(zod@3.25.76)
optionalDependencies:
'@cfworker/json-schema': 4.1.1
transitivePeerDependencies:
@@ -23378,7 +23427,7 @@ snapshots:
eventsource: 3.0.7
eventsource-parser: 3.1.1
express: 5.2.1
- express-rate-limit: 8.7.0(express@5.2.1)
+ express-rate-limit: 8.7.0(express@5.2.1)(supports-color@7.2.0)
hono: 4.13.8
jose: 6.2.12
json-schema-typed: 8.0.2
@@ -23402,7 +23451,7 @@ snapshots:
eventsource: 3.0.7
eventsource-parser: 3.1.1
express: 5.2.1
- express-rate-limit: 8.7.0(express@5.2.1)
+ express-rate-limit: 8.7.0(express@5.2.1)(supports-color@7.2.0)
hono: 4.13.8
jose: 6.2.12
json-schema-typed: 8.0.2
@@ -24710,20 +24759,20 @@ snapshots:
typescript: 5.9.3
optional: true
- '@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3)':
+ '@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2))(typescript@7.0.2)':
dependencies:
'@prisma/client-runtime-utils': 7.9.0
optionalDependencies:
- prisma: 7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
- typescript: 5.9.3
+ prisma: 7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2)
+ typescript: 7.0.2
optional: true
- '@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2))(typescript@7.0.2)':
+ '@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3)':
dependencies:
'@prisma/client-runtime-utils': 7.9.0
optionalDependencies:
- prisma: 7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2)
- typescript: 7.0.2
+ prisma: 7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
+ typescript: 5.9.3
optional: true
'@prisma/config@7.9.0(magicast@0.5.2)':
@@ -27845,9 +27894,9 @@ snapshots:
- uploadthing
- xml2js
- '@tanstack/nitro-v2-vite-plugin@1.155.0(@electric-sql/pglite@0.5.11)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)(rolldown@1.2.4)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))':
+ '@tanstack/nitro-v2-vite-plugin@1.155.0(@electric-sql/pglite@0.5.11)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)(rolldown@1.2.4)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))':
dependencies:
- nitropack: 2.13.1(@electric-sql/pglite@0.5.11)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)(rolldown@1.2.4)
+ nitropack: 2.13.1(@electric-sql/pglite@0.5.11)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)(rolldown@1.2.4)
pathe: 2.0.3
vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)
transitivePeerDependencies:
@@ -28823,9 +28872,9 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
- '@types/ioredis-mock@8.2.7(ioredis@5.9.2(supports-color@7.2.0))':
+ '@types/ioredis-mock@8.2.7(ioredis@5.9.2)':
dependencies:
- ioredis: 5.9.2(supports-color@7.2.0)
+ ioredis: 5.9.2
'@types/istanbul-lib-coverage@2.0.6': {}
@@ -29339,7 +29388,7 @@ snapshots:
obug: 2.2.1
std-env: 4.2.0
tinyrainbow: 3.1.1
- vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.2)(jsdom@27.4.0(@noble/hashes@2.3.0))(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
+ vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.2)(jsdom@27.4.0(@noble/hashes@2.3.0))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
'@vitest/coverage-v8@4.1.10(vitest@4.1.11)':
dependencies:
@@ -29389,14 +29438,6 @@ snapshots:
optionalDependencies:
vite: 8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)
- '@vitest/mocker@4.1.10(vite@8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))':
- dependencies:
- '@vitest/spy': 4.1.10
- estree-walker: 3.0.3
- magic-string: 0.30.21
- optionalDependencies:
- vite: 8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)
-
'@vitest/mocker@4.1.10(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 4.1.10
@@ -30895,11 +30936,11 @@ snapshots:
dayjs@1.11.19: {}
- db0@0.3.4(@electric-sql/pglite@0.5.11)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3):
+ db0@0.3.4(@electric-sql/pglite@0.5.11)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3):
optionalDependencies:
'@electric-sql/pglite': 0.5.11
better-sqlite3: 12.11.1
- drizzle-orm: 0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))
+ drizzle-orm: 0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))
mysql2: 3.15.3
db0@0.3.4(@electric-sql/pglite@0.5.11)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.18))(@types/react@19.2.18)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.18))(@types/react@19.2.18)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3):
@@ -31151,34 +31192,34 @@ snapshots:
prisma: 7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
optional: true
- drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)):
+ drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2))(typescript@7.0.2))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2)):
optionalDependencies:
'@cloudflare/workers-types': 4.20260317.1
'@electric-sql/pglite': 0.5.11
'@opentelemetry/api': 1.9.1
- '@prisma/client': 7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3)
+ '@prisma/client': 7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2))(typescript@7.0.2)
'@types/pg': 8.23.1
better-sqlite3: 12.11.1
bun-types: 1.3.14
mysql2: 3.15.3
pg: 8.23.1
postgres: 3.4.7
- prisma: 7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
- optional: true
+ prisma: 7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2)
- drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2))(typescript@7.0.2))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2)):
+ drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)):
optionalDependencies:
'@cloudflare/workers-types': 4.20260317.1
'@electric-sql/pglite': 0.5.11
'@opentelemetry/api': 1.9.1
- '@prisma/client': 7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2))(typescript@7.0.2)
+ '@prisma/client': 7.9.0(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3)
'@types/pg': 8.23.1
better-sqlite3: 12.11.1
bun-types: 1.3.14
mysql2: 3.15.3
pg: 8.23.1
postgres: 3.4.7
- prisma: 7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2)
+ prisma: 7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
+ optional: true
drizzle-orm@0.45.4(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.18))(@types/react@19.2.18)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.18))(@types/react@19.2.18)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)):
optionalDependencies:
@@ -31817,7 +31858,7 @@ snapshots:
exponential-backoff@3.1.3: {}
- express-rate-limit@8.7.0(express@5.2.1):
+ express-rate-limit@8.7.0(express@5.2.1)(supports-color@7.2.0):
dependencies:
debug: 4.4.3(supports-color@7.2.0)
express: 5.2.1
@@ -32704,17 +32745,17 @@ snapshots:
dependencies:
loose-envify: 1.4.0
- ioredis-mock@8.13.1(@types/ioredis-mock@8.2.7(ioredis@5.9.2(supports-color@7.2.0)))(ioredis@5.9.2(supports-color@7.2.0)):
+ ioredis-mock@8.13.1(@types/ioredis-mock@8.2.7(ioredis@5.9.2))(ioredis@5.9.2):
dependencies:
'@ioredis/as-callback': 3.0.0
'@ioredis/commands': 1.5.0
- '@types/ioredis-mock': 8.2.7(ioredis@5.9.2(supports-color@7.2.0))
+ '@types/ioredis-mock': 8.2.7(ioredis@5.9.2)
fengari: 0.1.5
fengari-interop: 0.1.4(fengari@0.1.5)
- ioredis: 5.9.2(supports-color@7.2.0)
+ ioredis: 5.9.2
semver: 7.8.4
- ioredis@5.9.2(supports-color@7.2.0):
+ ioredis@5.9.2:
dependencies:
'@ioredis/commands': 1.5.0
cluster-key-slot: 1.1.2
@@ -34557,7 +34598,7 @@ snapshots:
h3: 1.15.5
hookable: 5.5.3
httpxy: 0.1.7
- ioredis: 5.9.2(supports-color@7.2.0)
+ ioredis: 5.9.2
jiti: 2.7.0
klona: 2.0.6
knitwork: 1.3.0
@@ -34625,7 +34666,7 @@ snapshots:
- supports-color
- uploadthing
- nitropack@2.13.1(@electric-sql/pglite@0.5.11)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)(rolldown@1.2.4):
+ nitropack@2.13.1(@electric-sql/pglite@0.5.11)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)(rolldown@1.2.4):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.2
'@rollup/plugin-alias': 6.0.0(rollup@4.60.1)
@@ -34646,7 +34687,7 @@ snapshots:
cookie-es: 2.0.1
croner: 9.1.0
crossws: 0.3.5
- db0: 0.3.4(@electric-sql/pglite@0.5.11)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)
+ db0: 0.3.4(@electric-sql/pglite@0.5.11)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)
defu: 6.1.4
destr: 2.0.5
dot-prop: 10.1.0
@@ -34659,7 +34700,7 @@ snapshots:
h3: 1.15.5
hookable: 5.5.3
httpxy: 0.1.7
- ioredis: 5.9.2(supports-color@7.2.0)
+ ioredis: 5.9.2
jiti: 2.7.0
klona: 2.0.6
knitwork: 1.3.0
@@ -34692,7 +34733,7 @@ snapshots:
unenv: 2.0.0-rc.24
unimport: 5.6.0
unplugin-utils: 0.3.1
- unstorage: 1.17.4(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.5.11)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3))(ioredis@5.9.2)
+ unstorage: 1.17.4(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.5.11)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3))(ioredis@5.9.2)
untyped: 2.0.0
unwasm: 0.5.3
youch: 4.1.0-beta.13
@@ -35798,17 +35839,17 @@ snapshots:
- react-dom
optional: true
- prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3):
+ prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2):
dependencies:
'@prisma/config': 7.9.0(magicast@0.5.2)
- '@prisma/dev': 0.24.14(typescript@5.9.3)
+ '@prisma/dev': 0.24.14(typescript@7.0.2)
'@prisma/engines': 7.9.0
'@prisma/studio-core': 0.33.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
mysql2: 3.15.3
postgres: 3.4.7
optionalDependencies:
better-sqlite3: 12.11.1
- typescript: 5.9.3
+ typescript: 7.0.2
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
@@ -35817,17 +35858,17 @@ snapshots:
- react-dom
optional: true
- prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2):
+ prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3):
dependencies:
'@prisma/config': 7.9.0(magicast@0.5.2)
- '@prisma/dev': 0.24.14(typescript@7.0.2)
+ '@prisma/dev': 0.24.14(typescript@5.9.3)
'@prisma/engines': 7.9.0
'@prisma/studio-core': 0.33.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
mysql2: 3.15.3
postgres: 3.4.7
optionalDependencies:
better-sqlite3: 12.11.1
- typescript: 7.0.2
+ typescript: 5.9.3
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
@@ -38252,9 +38293,9 @@ snapshots:
optionalDependencies:
aws4fetch: 1.0.20
db0: 0.3.4(@electric-sql/pglite@0.5.11)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)
- ioredis: 5.9.2(supports-color@7.2.0)
+ ioredis: 5.9.2
- unstorage@1.17.4(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.5.11)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3))(ioredis@5.9.2):
+ unstorage@1.17.4(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.5.11)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3))(ioredis@5.9.2):
dependencies:
anymatch: 3.1.3
chokidar: 5.0.0
@@ -38266,8 +38307,8 @@ snapshots:
ufo: 1.6.3
optionalDependencies:
aws4fetch: 1.0.20
- db0: 0.3.4(@electric-sql/pglite@0.5.11)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.3.0))(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)
- ioredis: 5.9.2(supports-color@7.2.0)
+ db0: 0.3.4(@electric-sql/pglite@0.5.11)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.3.0)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)
+ ioredis: 5.9.2
unstorage@2.0.0-alpha.7(aws4fetch@1.0.20)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.5.11)(better-sqlite3@12.11.1)(drizzle-orm@0.45.4(@electric-sql/pglite@0.5.11)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.18))(@types/react@19.2.18)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.23.1)(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(pg@8.23.1)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.18))(@types/react@19.2.18)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3))(lru-cache@11.5.3)(ofetch@2.0.0-alpha.3):
optionalDependencies:
@@ -38574,24 +38615,6 @@ snapshots:
tsx: 4.23.12
yaml: 2.9.0
- vite@8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0):
- dependencies:
- lightningcss: 1.33.0
- picomatch: 4.0.7
- postcss: 8.5.28
- rolldown: 1.2.9
- tinyglobby: 0.2.17
- optionalDependencies:
- '@types/node': 24.10.3
- esbuild: 0.28.1
- fsevents: 2.3.3
- jiti: 2.7.0
- less: 4.6.6
- sass: 1.97.3
- terser: 5.44.1
- tsx: 4.23.12
- yaml: 2.9.0
-
vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0):
dependencies:
lightningcss: 1.33.0
@@ -38680,37 +38703,6 @@ snapshots:
transitivePeerDependencies:
- msw
- vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.2)(jsdom@27.4.0(@noble/hashes@2.3.0))(vite@8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)):
- dependencies:
- '@vitest/expect': 4.1.10
- '@vitest/mocker': 4.1.10(vite@8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))
- '@vitest/pretty-format': 4.1.10
- '@vitest/runner': 4.1.10
- '@vitest/snapshot': 4.1.10
- '@vitest/spy': 4.1.10
- '@vitest/utils': 4.1.10
- es-module-lexer: 2.0.0
- expect-type: 1.3.0
- magic-string: 0.30.21
- obug: 2.1.1
- pathe: 2.0.3
- picomatch: 4.0.5
- std-env: 4.0.0
- tinybench: 2.9.0
- tinyexec: 1.1.1
- tinyglobby: 0.2.17
- tinyrainbow: 3.1.0
- vite: 8.3.0(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)
- why-is-node-running: 2.3.0
- optionalDependencies:
- '@opentelemetry/api': 1.9.1
- '@types/node': 24.10.3
- '@vitest/coverage-v8': 4.1.10(vitest@4.1.10)
- happy-dom: 20.11.2
- jsdom: 27.4.0(@noble/hashes@2.3.0)
- transitivePeerDependencies:
- - msw
-
vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.2)(jsdom@27.4.0(@noble/hashes@2.3.0))(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.10
diff --git a/testing/e2e/README.md b/testing/e2e/README.md
index e28cb81663..8550257dad 100644
--- a/testing/e2e/README.md
+++ b/testing/e2e/README.md
@@ -4,7 +4,7 @@ End-to-end tests for TanStack AI using Playwright and [aimock](https://github.co
**Architecture:** Playwright drives a TanStack Start app (`testing/e2e/`) which routes requests through provider adapters pointing at aimock. Fixtures define mock responses. No real API keys needed. All scenarios (including tool execution flows) use aimock fixtures. Tests run in parallel with per-test `X-Test-Id` isolation.
-**Providers tested in CI:** openai, anthropic, gemini, vertex, vertex-grok, vertex-mistral, ollama, groq, grok, openrouter, openrouter-responses, vercel-gateway, vercel-gateway-responses, lovable, lovable-responses, bedrock, bedrock-responses, openai-compatible, openai-compatible-legacy, mistral, byteplus, elevenlabs, llmgateway, cloudflare, neon, neon-anthropic, neon-responses
+**Providers tested in CI:** openai, anthropic, gemini, vertex, vertex-grok, vertex-mistral, ollama, groq, grok, openrouter, openrouter-responses, vercel-gateway, vercel-gateway-responses, lovable, lovable-responses, bedrock, bedrock-responses, openai-compatible, openai-compatible-legacy, mistral, byteplus, elevenlabs, llmgateway, cloudflare, neon, neon-anthropic, neon-responses, zai
**Local `pnpm test:e2e`** runs openai, anthropic, and gemini. If none of those providers support a feature, the feature still runs on the providers that do. An explicit `E2E_PROVIDERS` list runs only those providers. Use `E2E_PROVIDERS=*` for the full matrix, or `E2E_PROVIDERS=grok` (comma-separated) to pick providers. Playwright does not retry or record video locally; CI retries twice and keeps the video of a failed test.
@@ -333,6 +333,7 @@ Three endpoints have no aimock equivalent and are mounted in `global-setup.ts`,
- Ollama: `{ host: LLMOCK_BASE, headers }` (config object)
- OpenRouter: `serverURL` with `?testId=` query param (SDK doesn't support headers)
- BytePlus: `LLMOCK_BASE + /api/v3` + `defaultHeaders` for Ark (chat, image, video); bare `LLMOCK_BASE` + `defaultHeaders` for Seed Speech (TTS, ASR)
+- Z.AI: `LLMOCK_OPENAI` (OpenAI-compatible chat completions) + `defaultHeaders`
## 7. Adding a Tool Test Scenario
diff --git a/testing/e2e/package.json b/testing/e2e/package.json
index ca1b4ed91f..06c5e1118a 100644
--- a/testing/e2e/package.json
+++ b/testing/e2e/package.json
@@ -57,6 +57,7 @@
"@tanstack/ai-vercel-gateway": "workspace:*",
"@tanstack/ai-vertex": "workspace:*",
"@tanstack/ai-worldlabs": "workspace:*",
+ "@tanstack/ai-zai": "workspace:*",
"@tanstack/devtools-event-bus": "^0.4.1",
"@tanstack/nitro-v2-vite-plugin": "^1.155.0",
"@tanstack/react-ai-devtools": "workspace:*",
diff --git a/testing/e2e/src/lib/feature-support.ts b/testing/e2e/src/lib/feature-support.ts
index 7c28351025..27fa46f61a 100644
--- a/testing/e2e/src/lib/feature-support.ts
+++ b/testing/e2e/src/lib/feature-support.ts
@@ -30,6 +30,7 @@ export const matrix: Record> = {
'cloudflare',
'neon',
'neon-anthropic',
+ 'zai',
]),
'one-shot-text': new Set([
'openai',
@@ -53,6 +54,7 @@ export const matrix: Record> = {
'cloudflare',
'neon',
'neon-anthropic',
+ 'zai',
]),
// BytePlus, LLM Gateway and Neon all stream their reasoning trace as
// `delta.reasoning_content`, which is exactly the field aimock's
@@ -77,6 +79,7 @@ export const matrix: Record> = {
'openai-compatible-legacy',
'neon',
'neon-anthropic',
+ 'zai',
]),
'multi-turn-reasoning': new Set(['anthropic']),
'multi-turn': new Set([
@@ -101,6 +104,7 @@ export const matrix: Record> = {
'cloudflare',
'neon',
'neon-anthropic',
+ 'zai',
]),
'tool-calling': new Set([
'openai',
@@ -128,6 +132,7 @@ export const matrix: Record> = {
'neon',
'neon-anthropic',
'neon-responses',
+ 'zai',
]),
// OpenAI only: aimock's journal keeps `tool_choice` for Responses requests,
// but stores Anthropic requests in OpenAI shape without it. Anthropic is
@@ -154,6 +159,7 @@ export const matrix: Record> = {
'cloudflare',
'neon',
'neon-anthropic',
+ 'zai',
'ollama',
]),
// Gemini excluded: approval flow timing issues with Gemini's streaming format
@@ -178,6 +184,7 @@ export const matrix: Record> = {
'cloudflare',
'neon',
'neon-anthropic',
+ 'zai',
]),
// Ollama excluded: aimock doesn't support content+toolCalls for /api/chat format
'text-tool-text': new Set([
@@ -201,6 +208,7 @@ export const matrix: Record> = {
'cloudflare',
'neon',
'neon-anthropic',
+ 'zai',
]),
'structured-output': new Set([
'openai',
@@ -224,6 +232,7 @@ export const matrix: Record> = {
'cloudflare',
'neon',
'neon-anthropic',
+ 'zai',
]),
// Streaming structured output: only providers with native streaming JSON
// schema support are listed here. Other providers fall back to the
@@ -245,6 +254,7 @@ export const matrix: Record> = {
'llmgateway',
'cloudflare',
'neon',
+ 'zai',
]),
// Multi-turn structured output: every turn produces its own typed
// `structured-output` part on the assistant message, and historical
@@ -277,6 +287,7 @@ export const matrix: Record> = {
'cloudflare',
'neon',
'neon-anthropic',
+ 'zai',
]),
'agentic-structured': new Set([
'openai',
@@ -300,6 +311,7 @@ export const matrix: Record> = {
'cloudflare',
'neon',
'neon-anthropic',
+ 'zai',
]),
// Native-combined-mode adapters only. Each provider's default test model
// (or per-feature override in `features.ts`) must opt into combined mode
@@ -337,6 +349,7 @@ export const matrix: Record> = {
'cloudflare',
'neon',
'neon-anthropic',
+ 'zai',
]),
// OpenAI only: this feature exercises the Responses adapter's PDF
// `input_file` conversion (base64 `file_data` + filename).
@@ -356,6 +369,7 @@ export const matrix: Record> = {
'cloudflare',
'neon',
'neon-anthropic',
+ 'zai',
]),
// byteplus excluded: @tanstack/ai-byteplus ships no summarize adapter —
// Ark has no summarization endpoint, and api.summarize.ts builds a
@@ -382,6 +396,7 @@ export const matrix: Record> = {
'neon',
'neon-anthropic',
'neon-responses',
+ 'zai',
]),
'summarize-stream': new Set([
'openai',
@@ -404,6 +419,7 @@ export const matrix: Record> = {
'neon',
'neon-anthropic',
'neon-responses',
+ 'zai',
]),
// Embedding (Promise-based `embed()` activity, no streaming). aimock 1.34
// natively mocks OpenAI's /v1/embeddings (JSON fixture in
diff --git a/testing/e2e/src/lib/features.ts b/testing/e2e/src/lib/features.ts
index 5dbaaebff6..f4bf987268 100644
--- a/testing/e2e/src/lib/features.ts
+++ b/testing/e2e/src/lib/features.ts
@@ -127,6 +127,8 @@ export const featureConfigs: Record = {
'multimodal-image': {
tools: [],
modelOptions: {},
+ // glm-5.2 is text-only; the 5V model accepts image and video input.
+ modelOverrides: { zai: 'glm-5v-turbo' },
},
'multimodal-document': {
tools: [],
@@ -135,7 +137,10 @@ export const featureConfigs: Record = {
'multimodal-structured': {
tools: [],
modelOptions: {},
- modelOverrides: { byteplus: BYTEPLUS_STRUCTURED_MODEL },
+ modelOverrides: {
+ byteplus: BYTEPLUS_STRUCTURED_MODEL,
+ zai: 'glm-5v-turbo',
+ },
},
summarize: {
tools: [],
diff --git a/testing/e2e/src/lib/providers.ts b/testing/e2e/src/lib/providers.ts
index 1320e006b0..7cdd0de209 100644
--- a/testing/e2e/src/lib/providers.ts
+++ b/testing/e2e/src/lib/providers.ts
@@ -24,6 +24,7 @@ import { createBytePlusText } from '@tanstack/ai-byteplus'
import { createLLMGatewayText } from '@tanstack/ai-llmgateway'
import { createCloudflareText } from '@tanstack/ai-cloudflare'
import { createNeonText } from '@tanstack/ai-neon'
+import { createZAIText } from '@tanstack/ai-zai'
import { HTTPClient } from '@openrouter/sdk'
import type { AnyTextAdapter } from '@tanstack/ai'
import type { BytePlusChatModel } from '@tanstack/ai-byteplus'
@@ -119,6 +120,7 @@ const defaultModels: Record = {
neon: 'gemini-3-8-flash',
'neon-anthropic': 'claude-sonnet-4-5',
'neon-responses': 'gpt-5-3-codex',
+ zai: 'glm-5.2',
}
export function createTextAdapter(
@@ -442,6 +444,15 @@ export function createTextAdapter(
defaultHeaders: testHeaders,
}),
}),
+ // Z.AI is OpenAI-compatible, so the adapter posts to aimock's native
+ // `/v1/chat/completions` handler.
+ zai: () =>
+ createChatOptions({
+ adapter: createZAIText(model as 'glm-5.2', DUMMY_KEY, {
+ baseURL: openaiUrl,
+ defaultHeaders: testHeaders,
+ }),
+ }),
}
return factories[provider]()
diff --git a/testing/e2e/src/lib/types.ts b/testing/e2e/src/lib/types.ts
index c44f44c583..38e32e6178 100644
--- a/testing/e2e/src/lib/types.ts
+++ b/testing/e2e/src/lib/types.ts
@@ -28,6 +28,7 @@ export type Provider =
| 'neon'
| 'neon-anthropic'
| 'neon-responses'
+ | 'zai'
export type Feature =
| 'chat'
@@ -93,6 +94,7 @@ export const ALL_PROVIDERS: Provider[] = [
'neon',
'neon-anthropic',
'neon-responses',
+ 'zai',
]
export const ALL_FEATURES: Feature[] = [
diff --git a/testing/e2e/src/routes/api.summarize.ts b/testing/e2e/src/routes/api.summarize.ts
index 7e8040d7bf..17abb6b3b3 100644
--- a/testing/e2e/src/routes/api.summarize.ts
+++ b/testing/e2e/src/routes/api.summarize.ts
@@ -17,6 +17,7 @@ import { createOpenRouterSummarize } from '@tanstack/ai-openrouter'
import { createVercelGatewaySummarize } from '@tanstack/ai-vercel-gateway'
import { createLovableSummarize } from '@tanstack/ai-lovable'
import { createNeonSummarize } from '@tanstack/ai-neon'
+import { createZAISummarize } from '@tanstack/ai-zai'
import { HTTPClient } from '@openrouter/sdk'
import type { Provider } from '@/lib/types'
@@ -168,6 +169,11 @@ function createSummarizeAdapter(
baseURL: llmockBase(aimockPort),
defaultHeaders: headers,
}),
+ zai: () =>
+ createZAISummarize('glm-5.2', DUMMY_KEY, {
+ baseURL: openaiUrl(aimockPort),
+ defaultHeaders: headers,
+ }),
}
return factories[provider]?.()
}
diff --git a/testing/e2e/tests/test-matrix.ts b/testing/e2e/tests/test-matrix.ts
index 52350d7a0e..aa78f88265 100644
--- a/testing/e2e/tests/test-matrix.ts
+++ b/testing/e2e/tests/test-matrix.ts
@@ -41,6 +41,7 @@ export const providers: Provider[] = [
'neon',
'neon-anthropic',
'neon-responses',
+ 'zai',
]
export { isSupported }