diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 02efc9c3..d52d0c72 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -6,6 +6,10 @@ - Added Vercel AI Gateway provider with model discovery and `AI_GATEWAY_API_KEY` env support ([#689](https://github.com/badlogic/pi-mono/pull/689) by [@timolins](https://github.com/timolins)) +### Fixed + +- Fixed z.ai thinking/reasoning: z.ai uses `thinking: { type: "enabled" }` instead of OpenAI's `reasoning_effort`. Added `thinkingFormat` compat flag to handle this. ([#688](https://github.com/badlogic/pi-mono/issues/688)) + ## [0.45.3] - 2026-01-13 ## [0.45.2] - 2026-01-13 diff --git a/packages/ai/README.md b/packages/ai/README.md index 9a218115..3759fc9c 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -711,6 +711,7 @@ interface OpenAICompat { supportsDeveloperRole?: boolean; // Whether provider supports `developer` role vs `system` (default: true) supportsReasoningEffort?: boolean; // Whether provider supports `reasoning_effort` (default: true) maxTokensField?: 'max_completion_tokens' | 'max_tokens'; // Which field name to use (default: max_completion_tokens) + thinkingFormat?: 'openai' | 'zai'; // Format for reasoning param: 'openai' uses reasoning_effort, 'zai' uses thinking: { type: "enabled" } (default: openai) } ``` diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 7548ff17..6695e636 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -440,6 +440,7 @@ async function loadModelsDevData(): Promise[]> { }, compat: { supportsDeveloperRole: false, + thinkingFormat: "zai", }, contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index cb2fdab6..ddabd353 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -10733,7 +10733,7 @@ export const MODELS = { api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false}, + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, reasoning: true, input: ["text"], cost: { @@ -10751,7 +10751,7 @@ export const MODELS = { api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false}, + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, reasoning: true, input: ["text"], cost: { @@ -10769,7 +10769,7 @@ export const MODELS = { api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false}, + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, reasoning: true, input: ["text"], cost: { @@ -10787,7 +10787,7 @@ export const MODELS = { api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false}, + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, reasoning: true, input: ["text", "image"], cost: { @@ -10805,7 +10805,7 @@ export const MODELS = { api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false}, + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, reasoning: true, input: ["text"], cost: { @@ -10823,7 +10823,7 @@ export const MODELS = { api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false}, + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, reasoning: true, input: ["text", "image"], cost: { @@ -10841,7 +10841,7 @@ export const MODELS = { api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false}, + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, reasoning: true, input: ["text"], cost: { diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 0ff4f1cd..a90ed563 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -404,7 +404,12 @@ function buildParams(model: Model<"openai-completions">, context: Context, optio params.tool_choice = options.toolChoice; } - if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) { + if (compat.thinkingFormat === "zai" && model.reasoning) { + // Z.ai uses binary thinking: { type: "enabled" | "disabled" } + // Must explicitly disable since z.ai defaults to thinking enabled + (params as any).thinking = { type: options?.reasoningEffort ? "enabled" : "disabled" }; + } else if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) { + // OpenAI-style reasoning_effort params.reasoning_effort = options.reasoningEffort; } @@ -678,11 +683,14 @@ function mapStopReason(reason: ChatCompletionChunk.Choice["finish_reason"]): Sto * Returns a fully resolved OpenAICompat object with all fields set. */ function detectCompatFromUrl(baseUrl: string): Required { + const isZai = baseUrl.includes("api.z.ai"); + const isNonStandard = baseUrl.includes("cerebras.ai") || baseUrl.includes("api.x.ai") || baseUrl.includes("mistral.ai") || - baseUrl.includes("chutes.ai"); + baseUrl.includes("chutes.ai") || + isZai; const useMaxTokens = baseUrl.includes("mistral.ai") || baseUrl.includes("chutes.ai"); @@ -693,13 +701,14 @@ function detectCompatFromUrl(baseUrl: string): Required { return { supportsStore: !isNonStandard, supportsDeveloperRole: !isNonStandard, - supportsReasoningEffort: !isGrok, + supportsReasoningEffort: !isGrok && !isZai, supportsUsageInStreaming: true, maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens", requiresToolResultName: isMistral, requiresAssistantAfterToolResult: false, // Mistral no longer requires this as of Dec 2024 requiresThinkingAsText: isMistral, requiresMistralToolIds: isMistral, + thinkingFormat: isZai ? "zai" : "openai", }; } @@ -722,5 +731,6 @@ function getCompat(model: Model<"openai-completions">): Required { model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult, requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText, requiresMistralToolIds: model.compat.requiresMistralToolIds ?? detected.requiresMistralToolIds, + thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat, }; } diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 63f0cbfb..9f3897f3 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -225,6 +225,8 @@ export interface OpenAICompat { requiresThinkingAsText?: boolean; /** Whether tool call IDs must be normalized to Mistral format (exactly 9 alphanumeric chars). Default: auto-detected from URL. */ requiresMistralToolIds?: boolean; + /** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "zai" uses thinking: { type: "enabled" }. Default: "openai". */ + thinkingFormat?: "openai" | "zai"; } // Model interface for the unified model system diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 55513ea9..06f4e91a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -14,6 +14,7 @@ ### Fixed - Fix API key resolution after model switches by using provider argument ([#691](https://github.com/badlogic/pi-mono/pull/691) by [@joshp123](https://github.com/joshp123)) +- Fixed z.ai thinking/reasoning: thinking toggle now correctly enables/disables thinking for z.ai models ([#688](https://github.com/badlogic/pi-mono/issues/688)) ## [0.45.3] - 2026-01-13