@arnilo/prism-provider-zai 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,7 +5,24 @@ All notable changes to @arnilo/prism-provider-zai will be documented in this fil
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.0.6] - 2026-07-19
9
+
10
+ ### Added
11
+
12
+ - Caller-gated `listZaiModels()` / `mapZaiModel()` (OpenAI-compatible `GET /models`).
13
+ - Official `clear_thinking` + Preserved Thinking (`reasoning_content` replay).
14
+ - Featured catalog refreshed to official GLM-5.x / 4.7 / 4.6 / 4.5 ids.
15
+
16
+ ### Changed
17
+
18
+ - Default base URL is official international `https://api.z.ai/api/paas/v4`.
19
+ - Docs aligned to official thinking / reasoning_effort / tool_stream fields (removed obsolete compat docs names).
20
+ - Resolved `thinking` / `reasoning_effort` / `tool_stream` win over raw compat spreads.
21
+
22
+ ## [0.0.5] - 2026-07-16
23
+
24
+ - Pinned the required `@arnilo/prism` peer and package metadata to 0.0.5; runtime behavior is unchanged.
25
+
9
26
 
10
27
  ## [0.0.4] - 2026-07-14
11
28
 
package/README.md CHANGED
@@ -1,25 +1,32 @@
1
1
  # @arnilo/prism-provider-zai
2
2
 
3
- Z.AI GLM provider package for Prism.
3
+ Z.AI GLM Chat Completions provider package for Prism.
4
4
 
5
5
  ```ts
6
- import { createZaiProviderPackage } from "@arnilo/prism-provider-zai";
6
+ import { createZaiProviderPackage, listZaiModels } from "@arnilo/prism-provider-zai";
7
7
 
8
8
  api.registerProviderPackage(createZaiProviderPackage({ apiKey: "fake-zai-key" }));
9
+
10
+ // Optional caller-gated discovery (never runs during setup):
11
+ const models = await listZaiModels({ apiKey: "fake-zai-key" });
12
+ api.registerProviderPackage(createZaiProviderPackage({ apiKey: "fake-zai-key", models }));
9
13
  ```
10
14
 
11
15
  Exports:
12
- - `createZaiProviderPackage()`
13
- - `createZaiProvider()`
14
- - `defineZaiModel()`
15
- - `zaiModels`
16
+ - `createZaiProviderPackage()` / `createZaiProvider()`
17
+ - `defineZaiModel()` / `zaiModels` (featured offline bootstrap)
18
+ - `listZaiModels()` / `mapZaiModel()` (caller-gated OpenAI-compatible `GET /models`)
19
+ - `zaiThinking` / `zaiReasoningEffort` / `zaiToolStream` / `zaiClearThinking` / `zaiPreserveThinking`
20
+ - `ZAI_DEFAULT_BASE_URL` (`https://api.z.ai/api/paas/v4`)
16
21
 
17
22
  Security defaults:
18
23
  - No network calls during import, setup, build, or default tests.
19
24
  - No automatic environment, file, keychain, or shell credential lookup.
20
- - API keys are resolved per request from caller-supplied values or resolvers.
25
+ - API keys are resolved per request from caller-supplied values or resolvers and redacted from errors.
21
26
 
22
- Cache behavior:
23
- - `kind: "implicit"` GLM context caching is automatic; no explicit cache payload sent regardless of cache options.
24
- - `prompt_tokens_details.cached_tokens`/`cache_write_tokens` map to `Usage.cacheReadTokens`/`cacheWriteTokens`.
27
+ Thinking / cache:
28
+ - Official body fields: `thinking` (`{ type: "enabled" | "disabled", clear_thinking? }`), `reasoning_effort` (GLM-5.2+), `tool_stream` (GLM-4.6+).
29
+ - Per-turn `options.compat` wins over `model.compat`.
30
+ - Preserved Thinking: set `clear_thinking: false` (and optionally `preserveThinking: true`) so prior thinking is replayed as `reasoning_content`.
31
+ - `kind: "implicit"` — GLM context caching is automatic; no explicit cache payload. `prompt_tokens_details.cached_tokens` / `cache_write_tokens` map to usage.
25
32
  - Provider-owned headers (`content-type`, `authorization`) win over caller headers.
package/dist/index.d.ts CHANGED
@@ -7,5 +7,6 @@ export interface ZaiProviderPackageOptions {
7
7
  readonly models?: readonly ModelConfig[];
8
8
  }
9
9
  export declare function createZaiProviderPackage(options?: ZaiProviderPackageOptions): ProviderPackage;
10
- export { defineZaiModel, zaiModels, type ZaiModelConfig } from "./models.js";
11
- export { createZaiProvider, zaiBody, zaiEvents, type ZaiProviderOptions } from "./provider.js";
10
+ export { defineZaiModel, listZaiModels, mapZaiModel, zaiModels, type ListZaiModelsOptions, type ZaiModelConfig, type ZaiModelEntry, } from "./models.js";
11
+ export { createZaiProvider, toZaiMessage, zaiBody, zaiEvents, ZAI_DEFAULT_BASE_URL, type ZaiProviderOptions, } from "./provider.js";
12
+ export { zaiClearThinking, zaiPreserveThinking, zaiReasoningEffort, zaiThinking, zaiToolStream, } from "./thinking.js";
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export function createZaiProviderPackage(options = {}) {
15
15
  },
16
16
  });
17
17
  }
18
- export { defineZaiModel, zaiModels } from "./models.js";
19
- export { createZaiProvider, zaiBody, zaiEvents } from "./provider.js";
18
+ export { defineZaiModel, listZaiModels, mapZaiModel, zaiModels, } from "./models.js";
19
+ export { createZaiProvider, toZaiMessage, zaiBody, zaiEvents, ZAI_DEFAULT_BASE_URL, } from "./provider.js";
20
+ export { zaiClearThinking, zaiPreserveThinking, zaiReasoningEffort, zaiThinking, zaiToolStream, } from "./thinking.js";
20
21
  //# sourceMappingURL=index.js.map
package/dist/models.d.ts CHANGED
@@ -1,11 +1,61 @@
1
- import type { JsonObject, ModelConfig } from "@arnilo/prism";
1
+ import { type CredentialValueSource, type JsonObject, type ModelConfig } from "@arnilo/prism";
2
2
  export interface ZaiModelConfig extends Omit<ModelConfig, "provider" | "compat"> {
3
3
  readonly provider?: "zai";
4
4
  readonly compat?: JsonObject & {
5
+ /** Official deep-thinking switch (`boolean` or `{ type, clear_thinking? }`). */
5
6
  readonly thinking?: boolean | JsonObject;
7
+ /** Official GLM-5.2+ effort: max | xhigh | high | medium | low | minimal | none. */
6
8
  readonly reasoning_effort?: string;
9
+ /** Official GLM-4.6+ streaming tool-call arguments. */
7
10
  readonly tool_stream?: boolean;
11
+ /** Official nested `thinking.clear_thinking` (also accepted at compat root). */
12
+ readonly clear_thinking?: boolean;
13
+ /** Prism-local: replay prior thinking as `reasoning_content` when not clearing. */
14
+ readonly preserveThinking?: boolean;
8
15
  };
9
16
  }
17
+ export interface ListZaiModelsOptions {
18
+ readonly apiKey?: CredentialValueSource;
19
+ readonly fetch?: typeof fetch;
20
+ /** Defaults to official international `https://api.z.ai/api/paas/v4`. */
21
+ readonly baseUrl?: string;
22
+ readonly signal?: AbortSignal;
23
+ readonly headers?: Readonly<Record<string, string>>;
24
+ readonly provider?: string;
25
+ }
26
+ /**
27
+ * Sparse OpenAI-compatible `/models` entry. Z.AI does not publish a first-class
28
+ * list-models API page; this helper follows the OpenAI-compatible convention
29
+ * used by the Chat Completions base (`GET {baseUrl}/models`). Prefer featured
30
+ * `zaiModels` (docs-verified) when discovery is unavailable.
31
+ * @see https://docs.z.ai/api-reference/llm/chat-completion
32
+ * @see https://docs.z.ai/guides/overview/overview
33
+ */
34
+ export interface ZaiModelEntry {
35
+ readonly id: string;
36
+ readonly object?: string;
37
+ readonly created?: number;
38
+ readonly owned_by?: string;
39
+ }
10
40
  export declare function defineZaiModel(config: ZaiModelConfig): ModelConfig;
11
- export declare const zaiModels: readonly [ModelConfig, ModelConfig];
41
+ /**
42
+ * Caller-gated Z.AI model discovery via OpenAI-compatible `GET /models`.
43
+ * Never invoked by `createZaiProviderPackage` — hosts call this and pass results
44
+ * via `models:` (or register themselves). Official docs list model codes on the
45
+ * Chat Completions page / overview; use featured `zaiModels` as offline bootstrap.
46
+ */
47
+ export declare function listZaiModels(options?: ListZaiModelsOptions): Promise<ModelConfig[]>;
48
+ /**
49
+ * Map a sparse OpenAI-compatible `/models` entry to Prism `ModelConfig`.
50
+ * Limits / thinking defaults are inferred from official Chat Completions model codes.
51
+ */
52
+ export declare function mapZaiModel(entry: ZaiModelEntry, options?: {
53
+ readonly provider?: string;
54
+ }): ModelConfig;
55
+ /**
56
+ * Featured offline bootstrap aliases — official Chat Completions model codes from
57
+ * https://docs.z.ai/api-reference/llm/chat-completion and overview context sizes.
58
+ * Refresh live ids via `listZaiModels()` when the OpenAI-compatible list endpoint
59
+ * is available to the account.
60
+ */
61
+ export declare const zaiModels: readonly [ModelConfig, ModelConfig, ModelConfig, ModelConfig, ModelConfig, ModelConfig, ModelConfig];
package/dist/models.js CHANGED
@@ -1,20 +1,189 @@
1
+ import { redactSecrets, resolveCredentialValue, } from "@arnilo/prism";
2
+ import { readBoundedResponseText } from "@arnilo/prism/providers/transport";
1
3
  export function defineZaiModel(config) {
2
- return { ...config, provider: "zai", capabilities: { input: ["text"], output: ["text"], reasoning: true, tools: true, streaming: true, structuredOutput: "json_schema", ...config.capabilities } };
4
+ return {
5
+ ...config,
6
+ provider: "zai",
7
+ capabilities: {
8
+ input: ["text"],
9
+ output: ["text"],
10
+ reasoning: true,
11
+ tools: true,
12
+ streaming: true,
13
+ structuredOutput: "json_schema",
14
+ ...config.capabilities,
15
+ },
16
+ cache: config.cache ?? { kind: "implicit" },
17
+ };
3
18
  }
19
+ /**
20
+ * Caller-gated Z.AI model discovery via OpenAI-compatible `GET /models`.
21
+ * Never invoked by `createZaiProviderPackage` — hosts call this and pass results
22
+ * via `models:` (or register themselves). Official docs list model codes on the
23
+ * Chat Completions page / overview; use featured `zaiModels` as offline bootstrap.
24
+ */
25
+ export async function listZaiModels(options = {}) {
26
+ const provider = options.provider ?? "zai";
27
+ const baseUrl = (options.baseUrl ?? "https://api.z.ai/api/paas/v4").replace(/\/$/, "");
28
+ const token = await resolveCredentialValue(options.apiKey, { provider, name: "apiKey" });
29
+ const response = await (options.fetch ?? fetch)(`${baseUrl}/models`, {
30
+ method: "GET",
31
+ headers: { ...options.headers, ...(token ? { authorization: `Bearer ${token}` } : {}) },
32
+ signal: options.signal,
33
+ });
34
+ if (!response.ok) {
35
+ const body = await readBoundedResponseText(response, { secrets: [token] });
36
+ throw new Error(`Z.AI model discovery failed: ${response.status} ${redactSecrets(body, [token])}`);
37
+ }
38
+ const payload = (await response.json());
39
+ if (!Array.isArray(payload.data))
40
+ throw new Error("Z.AI model discovery response missing data array");
41
+ return payload.data.map((entry) => mapZaiModel(entry, { provider }));
42
+ }
43
+ /**
44
+ * Map a sparse OpenAI-compatible `/models` entry to Prism `ModelConfig`.
45
+ * Limits / thinking defaults are inferred from official Chat Completions model codes.
46
+ */
47
+ export function mapZaiModel(entry, options = {}) {
48
+ if (!entry || typeof entry.id !== "string" || entry.id.length === 0) {
49
+ throw new Error("Z.AI model entry missing id");
50
+ }
51
+ const id = entry.id;
52
+ const limits = limitsForZaiModel(id);
53
+ const reasoning = looksLikeReasoningModel(id);
54
+ return defineZaiModel({
55
+ provider: options.provider ?? "zai",
56
+ model: id,
57
+ displayName: id,
58
+ capabilities: {
59
+ input: looksLikeVisionModel(id) ? ["text", "image"] : ["text"],
60
+ output: ["text"],
61
+ reasoning,
62
+ tools: true,
63
+ streaming: true,
64
+ structuredOutput: "json_schema",
65
+ },
66
+ limits,
67
+ cache: { kind: "implicit" },
68
+ compat: cleanJson({
69
+ ...thinkingDefaultsForModel(id),
70
+ zai: cleanJson({
71
+ owned_by: entry.owned_by,
72
+ created: entry.created,
73
+ }),
74
+ }),
75
+ });
76
+ }
77
+ /**
78
+ * Featured offline bootstrap aliases — official Chat Completions model codes from
79
+ * https://docs.z.ai/api-reference/llm/chat-completion and overview context sizes.
80
+ * Refresh live ids via `listZaiModels()` when the OpenAI-compatible list endpoint
81
+ * is available to the account.
82
+ */
4
83
  export const zaiModels = [
84
+ defineZaiModel({
85
+ model: "glm-5.2",
86
+ displayName: "GLM-5.2",
87
+ limits: { contextWindow: 1_000_000, maxOutputTokens: 128_000 },
88
+ cache: { kind: "implicit" },
89
+ compat: { thinking: true, reasoning_effort: "max", tool_stream: true },
90
+ }),
91
+ defineZaiModel({
92
+ model: "glm-5.1",
93
+ displayName: "GLM-5.1",
94
+ limits: { contextWindow: 200_000, maxOutputTokens: 128_000 },
95
+ cache: { kind: "implicit" },
96
+ compat: { thinking: true, tool_stream: true },
97
+ }),
98
+ defineZaiModel({
99
+ model: "glm-5",
100
+ displayName: "GLM-5",
101
+ limits: { contextWindow: 200_000, maxOutputTokens: 128_000 },
102
+ cache: { kind: "implicit" },
103
+ compat: { thinking: true, tool_stream: true },
104
+ }),
105
+ defineZaiModel({
106
+ model: "glm-5-turbo",
107
+ displayName: "GLM-5-Turbo",
108
+ limits: { contextWindow: 200_000, maxOutputTokens: 128_000 },
109
+ cache: { kind: "implicit" },
110
+ compat: { thinking: true, tool_stream: true },
111
+ }),
5
112
  defineZaiModel({
6
113
  model: "glm-4.7",
7
114
  displayName: "GLM-4.7",
8
- limits: { contextWindow: 128_000, maxOutputTokens: 32_000 },
115
+ limits: { contextWindow: 200_000, maxOutputTokens: 128_000 },
116
+ cache: { kind: "implicit" },
117
+ // Official: GLM-4.7 forced thinking when enabled; tool_stream supported (4.6+).
118
+ compat: { thinking: true, tool_stream: true },
119
+ }),
120
+ defineZaiModel({
121
+ model: "glm-4.6",
122
+ displayName: "GLM-4.6",
123
+ limits: { contextWindow: 200_000, maxOutputTokens: 128_000 },
9
124
  cache: { kind: "implicit" },
10
- compat: { thinking: true, reasoning_effort: "medium", tool_stream: true },
125
+ compat: { thinking: true, tool_stream: true },
11
126
  }),
12
127
  defineZaiModel({
13
128
  model: "glm-4.5",
14
129
  displayName: "GLM-4.5",
15
- limits: { contextWindow: 128_000, maxOutputTokens: 32_000 },
130
+ limits: { contextWindow: 128_000, maxOutputTokens: 96_000 },
16
131
  cache: { kind: "implicit" },
17
- compat: { thinking: true, reasoning_effort: "medium", tool_stream: true },
132
+ compat: { thinking: true },
18
133
  }),
19
134
  ];
135
+ function thinkingDefaultsForModel(modelId) {
136
+ const id = modelId.toLowerCase();
137
+ if (!looksLikeReasoningModel(id))
138
+ return {};
139
+ const compat = { thinking: true };
140
+ if (supportsToolStream(id))
141
+ compat.tool_stream = true;
142
+ // Official: reasoning_effort only for GLM-5.2+.
143
+ if (supportsReasoningEffort(id))
144
+ compat.reasoning_effort = "max";
145
+ return compat;
146
+ }
147
+ function limitsForZaiModel(modelId) {
148
+ const id = modelId.toLowerCase();
149
+ if (id.includes("glm-5.2") || id.includes("glm-5-2")) {
150
+ return { contextWindow: 1_000_000, maxOutputTokens: 128_000 };
151
+ }
152
+ if (id.includes("glm-5") || id.includes("glm-4.7") || id.includes("glm-4.6")) {
153
+ return { contextWindow: 200_000, maxOutputTokens: 128_000 };
154
+ }
155
+ if (id.includes("glm-4.5v")) {
156
+ return { contextWindow: 64_000, maxOutputTokens: 16_000 };
157
+ }
158
+ if (id.includes("glm-4.5")) {
159
+ return { contextWindow: 128_000, maxOutputTokens: 96_000 };
160
+ }
161
+ if (id.includes("glm-4-32b") || id.includes("128k")) {
162
+ return { contextWindow: 128_000, maxOutputTokens: 16_000 };
163
+ }
164
+ return undefined;
165
+ }
166
+ function looksLikeReasoningModel(modelId) {
167
+ const id = modelId.toLowerCase();
168
+ return (id.includes("glm-5")
169
+ || id.includes("glm-4.7")
170
+ || id.includes("glm-4.6")
171
+ || id.includes("glm-4.5"));
172
+ }
173
+ function looksLikeVisionModel(modelId) {
174
+ const id = modelId.toLowerCase();
175
+ return id.includes("glm-5v") || id.includes("glm-4.6v") || id.includes("glm-4.5v") || id.endsWith("v");
176
+ }
177
+ function supportsToolStream(modelId) {
178
+ const id = modelId.toLowerCase();
179
+ // Official: tool_stream supported by GLM-4.6 and above.
180
+ return id.includes("glm-5") || id.includes("glm-4.7") || id.includes("glm-4.6");
181
+ }
182
+ function supportsReasoningEffort(modelId) {
183
+ const id = modelId.toLowerCase();
184
+ return id.includes("glm-5.2") || id.includes("glm-5-2");
185
+ }
186
+ function cleanJson(value) {
187
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
188
+ }
20
189
  //# sourceMappingURL=models.js.map
@@ -1,5 +1,7 @@
1
- import type { AIProvider, JsonObject, ProviderEvent, ProviderRequest } from "@arnilo/prism";
1
+ import type { AIProvider, JsonObject, Message, ModelConfig, ProviderEvent, ProviderRequest } from "@arnilo/prism";
2
2
  import { type CredentialValueSource } from "@arnilo/prism";
3
+ /** Official international Chat Completions base (China `open.bigmodel.cn` remains overridable). */
4
+ export declare const ZAI_DEFAULT_BASE_URL = "https://api.z.ai/api/paas/v4";
3
5
  export interface ZaiProviderOptions {
4
6
  readonly id?: string;
5
7
  readonly baseUrl?: string;
@@ -9,3 +11,10 @@ export interface ZaiProviderOptions {
9
11
  export declare function createZaiProvider(options?: ZaiProviderOptions): AIProvider;
10
12
  export declare function zaiBody(request: ProviderRequest): JsonObject;
11
13
  export declare function zaiEvents(body: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncIterable<ProviderEvent>;
14
+ /**
15
+ * Serialize Prism messages for Z.AI Chat Completions.
16
+ * Prior thinking blocks become `reasoning_content` when Preserved Thinking is active;
17
+ * otherwise they are dropped (never flattened into visible text).
18
+ * @see https://docs.z.ai/guides/capabilities/thinking-mode
19
+ */
20
+ export declare function toZaiMessage(message: Message, model: ModelConfig, preserveThinking?: boolean): JsonObject;
package/dist/provider.js CHANGED
@@ -1,10 +1,12 @@
1
- import { assertStructuredOutputRequestSupported, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, resolveCredentialValue, toolCallContent } from "@arnilo/prism";
2
- import { applyOpenAIChatStructuredOutput, mapOpenAIChatUsage, serializeOpenAIChatMessage, serializeOpenAITool, } from "@arnilo/prism/providers/openai";
1
+ import { assertStructuredOutputRequestSupported, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, resolveCredentialValue, toolCallContent, } from "@arnilo/prism";
2
+ import { applyOpenAIChatStructuredOutput, mapOpenAIChatUsage, serializeOpenAITool, } from "@arnilo/prism/providers/openai";
3
3
  import { parseJsonObjectArguments, readBoundedResponseText, readSseData, } from "@arnilo/prism/providers/transport";
4
- import { zaiReasoningEffort, zaiThinking, zaiToolStream } from "./thinking.js";
4
+ import { zaiPreserveThinking, zaiReasoningEffort, zaiThinking, zaiToolStream, } from "./thinking.js";
5
+ /** Official international Chat Completions base (China `open.bigmodel.cn` remains overridable). */
6
+ export const ZAI_DEFAULT_BASE_URL = "https://api.z.ai/api/paas/v4";
5
7
  export function createZaiProvider(options = {}) {
6
8
  const id = options.id ?? "zai";
7
- const baseUrl = (options.baseUrl ?? "https://open.bigmodel.cn/api/paas/v4").replace(/\/$/, "");
9
+ const baseUrl = (options.baseUrl ?? ZAI_DEFAULT_BASE_URL).replace(/\/$/, "");
8
10
  return {
9
11
  id,
10
12
  async *generate(request) {
@@ -15,7 +17,11 @@ export function createZaiProvider(options = {}) {
15
17
  try {
16
18
  const response = await (options.fetch ?? fetch)(`${baseUrl}/chat/completions`, {
17
19
  method: "POST",
18
- headers: { ...request.options?.headers, "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
20
+ headers: {
21
+ ...request.options?.headers,
22
+ "content-type": "application/json",
23
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
24
+ },
19
25
  body: JSON.stringify(zaiBody(request)),
20
26
  signal: request.signal,
21
27
  });
@@ -35,18 +41,21 @@ export function createZaiProvider(options = {}) {
35
41
  export function zaiBody(request) {
36
42
  assertStructuredOutputRequestSupported(request.model, request.options);
37
43
  const { maxTokens, ...parameters } = request.model.parameters ?? {};
44
+ const compatRest = stripZaiManagedCompat(request.options?.compat);
45
+ const preserveThinking = zaiPreserveThinking(request);
38
46
  const body = {
39
47
  model: request.model.model,
40
- messages: request.messages.map((message) => serializeOpenAIChatMessage(message, request.model.capabilities ?? {})),
48
+ messages: request.messages.map((message) => toZaiMessage(message, request.model, preserveThinking)),
41
49
  tools: request.tools?.map(serializeOpenAITool),
42
50
  stream: true,
43
- tool_stream: zaiToolStream(request),
44
- thinking: zaiThinking(request),
45
- reasoning_effort: zaiReasoningEffort(request),
46
51
  ...parameters,
47
52
  max_tokens: maxTokens ?? request.model.limits?.maxOutputTokens,
48
- ...request.options?.compat,
53
+ ...compatRest,
49
54
  ...request.options?.extra,
55
+ // Resolved official fields win over raw compat/extra escape hatches.
56
+ thinking: zaiThinking(request),
57
+ reasoning_effort: zaiReasoningEffort(request),
58
+ tool_stream: zaiToolStream(request),
50
59
  };
51
60
  applyOpenAIChatStructuredOutput(body, request.options?.structuredOutput);
52
61
  return clean(body);
@@ -58,11 +67,12 @@ export async function* zaiEvents(body, signal) {
58
67
  if (data === "[DONE]")
59
68
  break;
60
69
  const chunk = JSON.parse(data);
61
- usage = mapOpenAIChatUsage(chunk.usage) ?? usage;
62
70
  if (chunk.usage) {
63
71
  const mapped = mapOpenAIChatUsage(chunk.usage);
64
- if (mapped)
72
+ if (mapped) {
73
+ usage = mapped;
65
74
  yield providerUsage(mapped);
75
+ }
66
76
  }
67
77
  for (const choice of chunk.choices ?? []) {
68
78
  const delta = choice.delta ?? {};
@@ -77,7 +87,12 @@ export async function* zaiEvents(body, signal) {
77
87
  current.name = tool.function?.name ?? current.name;
78
88
  current.argumentsText += tool.function?.arguments ?? "";
79
89
  tools.set(index, current);
80
- yield providerToolCallDelta({ index, id: tool.id, name: tool.function?.name, argumentsText: tool.function?.arguments });
90
+ yield providerToolCallDelta({
91
+ index,
92
+ id: tool.id,
93
+ name: tool.function?.name,
94
+ argumentsText: tool.function?.arguments,
95
+ });
81
96
  }
82
97
  }
83
98
  }
@@ -88,6 +103,82 @@ export async function* zaiEvents(body, signal) {
88
103
  }
89
104
  yield providerDone(usage);
90
105
  }
106
+ /**
107
+ * Serialize Prism messages for Z.AI Chat Completions.
108
+ * Prior thinking blocks become `reasoning_content` when Preserved Thinking is active;
109
+ * otherwise they are dropped (never flattened into visible text).
110
+ * @see https://docs.z.ai/guides/capabilities/thinking-mode
111
+ */
112
+ export function toZaiMessage(message, model, preserveThinking = false) {
113
+ const capabilities = model.capabilities ?? {};
114
+ const thinkingParts = message.content.filter((part) => part.type === "thinking");
115
+ const reasoningContent = preserveThinking && thinkingParts.length > 0
116
+ ? thinkingParts.map((part) => part.text).join("\n")
117
+ : undefined;
118
+ if (message.role === "tool") {
119
+ const result = message.content.find((part) => part.type === "tool_result");
120
+ return {
121
+ role: "tool",
122
+ tool_call_id: result?.toolCallId ?? "",
123
+ content: result ? JSON.stringify(result.result ?? result.error ?? null) : "",
124
+ };
125
+ }
126
+ if (message.role === "assistant") {
127
+ const toolCalls = message.content.filter((part) => part.type === "tool_call");
128
+ const textParts = message.content.filter((part) => part.type === "text");
129
+ if (toolCalls.length > 0) {
130
+ return clean({
131
+ role: "assistant",
132
+ content: textParts.map((part) => part.text).join("\n") || null,
133
+ tool_calls: toolCalls.map((call) => ({
134
+ id: call.id,
135
+ type: "function",
136
+ function: { name: call.name, arguments: JSON.stringify(call.arguments) },
137
+ })),
138
+ reasoning_content: reasoningContent,
139
+ });
140
+ }
141
+ }
142
+ const content = [];
143
+ for (const part of message.content) {
144
+ if (part.type === "text") {
145
+ content.push({ type: "text", text: part.text });
146
+ }
147
+ else if (part.type === "thinking") {
148
+ // Handled via reasoning_content when preserving; otherwise dropped.
149
+ continue;
150
+ }
151
+ else if (part.type === "image") {
152
+ if (!capabilities.input?.includes("image")) {
153
+ throw new Error("Z.AI request includes image but model does not declare image input capability");
154
+ }
155
+ const url = part.url ?? (part.data ? `data:${part.mimeType ?? "image/png"};base64,${part.data}` : undefined);
156
+ if (!url)
157
+ throw new Error("Z.AI image block missing url or data");
158
+ content.push({ type: "image_url", image_url: { url } });
159
+ }
160
+ else if (part.type === "audio" || part.type === "file" || part.type === "document") {
161
+ throw new Error(`Z.AI Chat Completions does not support ${part.type} content blocks`);
162
+ }
163
+ else if (part.type === "tool_call") {
164
+ throw new Error("Z.AI assistant tool_call blocks must be the only content on the message");
165
+ }
166
+ else if (part.type === "tool_result") {
167
+ throw new Error("Z.AI tool_result blocks must appear in role=tool messages");
168
+ }
169
+ }
170
+ if (content.length === 1 && content[0].type === "text") {
171
+ return clean({ role: message.role, content: content[0].text, reasoning_content: reasoningContent });
172
+ }
173
+ return clean({ role: message.role, content, reasoning_content: reasoningContent });
174
+ }
175
+ /** Drop Prism-managed compat keys so they are not double-emitted / overwrite resolved fields. */
176
+ function stripZaiManagedCompat(compat) {
177
+ if (!compat)
178
+ return {};
179
+ const { thinking: _thinking, reasoning_effort: _reasoningEffort, reasoningEffort: _reasoningEffortCamel, tool_stream: _toolStream, clear_thinking: _clearThinking, clearThinking: _clearThinkingCamel, preserveThinking: _preserveThinking, preserve_thinking: _preserveThinkingSnake, ...rest } = compat;
180
+ return rest;
181
+ }
91
182
  function clean(value) {
92
183
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
93
184
  }
@@ -1,4 +1,30 @@
1
1
  import type { JsonObject, ProviderRequest } from "@arnilo/prism";
2
+ /**
3
+ * Map Prism compat → official Z.AI `thinking` body object.
4
+ * @see https://docs.z.ai/guides/capabilities/thinking
5
+ * @see https://docs.z.ai/api-reference/llm/chat-completion
6
+ */
2
7
  export declare function zaiThinking(request: ProviderRequest): JsonObject | undefined;
8
+ /**
9
+ * Official `reasoning_effort` (GLM-5.2+). Request `options.compat` wins over model defaults.
10
+ * Allowed: max | xhigh | high | medium | low | minimal | none.
11
+ * @see https://docs.z.ai/guides/capabilities/thinking
12
+ */
3
13
  export declare function zaiReasoningEffort(request: ProviderRequest): string | undefined;
14
+ /**
15
+ * Official `tool_stream` (GLM-4.6+). Request wins over model defaults.
16
+ * @see https://docs.z.ai/guides/capabilities/stream-tool
17
+ */
4
18
  export declare function zaiToolStream(request: ProviderRequest): boolean | undefined;
19
+ /**
20
+ * Official nested `thinking.clear_thinking` (default true on the wire).
21
+ * When false, prior `reasoning_content` must be replayed for Preserved Thinking.
22
+ * @see https://docs.z.ai/guides/capabilities/thinking-mode
23
+ */
24
+ export declare function zaiClearThinking(request: ProviderRequest): boolean | undefined;
25
+ /**
26
+ * Prism-local: when true (and clear_thinking is not true), replay prior thinking
27
+ * blocks as assistant `reasoning_content`. Official Preserved Thinking also requires
28
+ * `thinking.clear_thinking: false`.
29
+ */
30
+ export declare function zaiPreserveThinking(request: ProviderRequest): boolean;
package/dist/thinking.js CHANGED
@@ -1,17 +1,87 @@
1
+ /**
2
+ * Map Prism compat → official Z.AI `thinking` body object.
3
+ * @see https://docs.z.ai/guides/capabilities/thinking
4
+ * @see https://docs.z.ai/api-reference/llm/chat-completion
5
+ */
1
6
  export function zaiThinking(request) {
2
7
  const value = request.options?.compat?.thinking ?? request.model.compat?.thinking;
3
- if (value === false || request.options?.cacheRetention === "none")
4
- return { type: "disabled" };
5
- if (value && typeof value === "object")
6
- return value;
7
- return value === true ? { type: "enabled" } : undefined;
8
+ const clearThinking = zaiClearThinking(request);
9
+ if (value === false || request.options?.cacheRetention === "none") {
10
+ return cleanThinking({ type: "disabled", clear_thinking: clearThinking });
11
+ }
12
+ if (value && typeof value === "object") {
13
+ return cleanThinking({
14
+ ...value,
15
+ ...(clearThinking !== undefined ? { clear_thinking: clearThinking } : {}),
16
+ });
17
+ }
18
+ if (value === true) {
19
+ return cleanThinking({ type: "enabled", clear_thinking: clearThinking });
20
+ }
21
+ // Hosts may set only `clear_thinking` / preserve flags without an explicit thinking switch.
22
+ if (clearThinking !== undefined) {
23
+ return cleanThinking({ type: "enabled", clear_thinking: clearThinking });
24
+ }
25
+ return undefined;
8
26
  }
27
+ /**
28
+ * Official `reasoning_effort` (GLM-5.2+). Request `options.compat` wins over model defaults.
29
+ * Allowed: max | xhigh | high | medium | low | minimal | none.
30
+ * @see https://docs.z.ai/guides/capabilities/thinking
31
+ */
9
32
  export function zaiReasoningEffort(request) {
10
- const effort = request.options?.compat?.reasoning_effort ?? request.options?.compat?.reasoningEffort ?? request.model.compat?.reasoning_effort;
33
+ const effort = request.options?.compat?.reasoning_effort
34
+ ?? request.options?.compat?.reasoningEffort
35
+ ?? request.model.compat?.reasoning_effort;
11
36
  return typeof effort === "string" ? effort : undefined;
12
37
  }
38
+ /**
39
+ * Official `tool_stream` (GLM-4.6+). Request wins over model defaults.
40
+ * @see https://docs.z.ai/guides/capabilities/stream-tool
41
+ */
13
42
  export function zaiToolStream(request) {
14
43
  const value = request.options?.compat?.tool_stream ?? request.model.compat?.tool_stream;
15
44
  return typeof value === "boolean" ? value : undefined;
16
45
  }
46
+ /**
47
+ * Official nested `thinking.clear_thinking` (default true on the wire).
48
+ * When false, prior `reasoning_content` must be replayed for Preserved Thinking.
49
+ * @see https://docs.z.ai/guides/capabilities/thinking-mode
50
+ */
51
+ export function zaiClearThinking(request) {
52
+ const fromThinkingObject = readClearThinkingFromObject(request.options?.compat?.thinking ?? request.model.compat?.thinking);
53
+ if (fromThinkingObject !== undefined)
54
+ return fromThinkingObject;
55
+ const value = request.options?.compat?.clear_thinking
56
+ ?? request.options?.compat?.clearThinking
57
+ ?? request.model.compat?.clear_thinking;
58
+ return typeof value === "boolean" ? value : undefined;
59
+ }
60
+ /**
61
+ * Prism-local: when true (and clear_thinking is not true), replay prior thinking
62
+ * blocks as assistant `reasoning_content`. Official Preserved Thinking also requires
63
+ * `thinking.clear_thinking: false`.
64
+ */
65
+ export function zaiPreserveThinking(request) {
66
+ const clear = zaiClearThinking(request);
67
+ if (clear === true)
68
+ return false;
69
+ const value = request.options?.compat?.preserveThinking
70
+ ?? request.options?.compat?.preserve_thinking
71
+ ?? request.model.compat?.preserveThinking
72
+ ?? request.model.compat?.preserve_thinking;
73
+ if (typeof value === "boolean")
74
+ return value;
75
+ // clear_thinking:false implies preserved thinking even without an explicit preserve flag.
76
+ return clear === false;
77
+ }
78
+ function readClearThinkingFromObject(value) {
79
+ if (!value || typeof value !== "object" || Array.isArray(value))
80
+ return undefined;
81
+ const clear = value.clear_thinking ?? value.clearThinking;
82
+ return typeof clear === "boolean" ? clear : undefined;
83
+ }
84
+ function cleanThinking(value) {
85
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
86
+ }
17
87
  //# sourceMappingURL=thinking.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism-provider-zai",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Z.AI provider package for Prism.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,7 +25,7 @@
25
25
  "pack:dry-run": "npm pack --dry-run"
26
26
  },
27
27
  "peerDependencies": {
28
- "@arnilo/prism": "0.0.4"
28
+ "@arnilo/prism": "0.0.6"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@arnilo/prism": "file:../.."