diff --git a/CHANGELOG.md b/CHANGELOG.md index 153143b1ae..37612542fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ - Daemon: fix profile-aware service label resolution (env-driven) and add coverage for launchd/systemd/schtasks. (#969) — thanks @bjesuiter. - Agents: avoid false positives when logging unsupported Google tool schema keywords. - Agents: skip Gemini history downgrades for google-antigravity to preserve tool calls. (#894) — thanks @mukhtharcm. +- Agents: map Z.AI thinking to on/off in UI/TUI and drop the pi-agent-core patch now that upstream handles binary thinking. - Status: restore usage summary line for current provider when no OAuth profiles exist. - Fix: guard model fallback against undefined provider/model values. (#954) — thanks @roshanasingh4. - Fix: refactor session store updates, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204. diff --git a/docs/tools/thinking.md b/docs/tools/thinking.md index 8fde4fa3ee..8b58046a7c 100644 --- a/docs/tools/thinking.md +++ b/docs/tools/thinking.md @@ -14,6 +14,8 @@ read_when: - high → “ultrathink” (max budget) - xhigh → “ultrathink+” (GPT-5.2 + Codex models only) - `highest`, `max` map to `high`. +- Provider notes: + - Z.AI (`zai/*`) only supports binary thinking (`on`/`off`). Any non-`off` level is treated as `on` (mapped to `low`). ## Resolution order 1. Inline directive on the message (applies only to that message). diff --git a/package.json b/package.json index d4d5ffb4c9..7a60431fb2 100644 --- a/package.json +++ b/package.json @@ -214,9 +214,6 @@ "@sinclair/typebox": "0.34.47", "hono": "4.11.4", "tar": "7.5.3" - }, - "patchedDependencies": { - "@mariozechner/pi-agent-core@0.46.0": "patches/@mariozechner__pi-agent-core.patch" } }, "vitest": { diff --git a/patches/@mariozechner__pi-agent-core.patch b/patches/@mariozechner__pi-agent-core.patch deleted file mode 100644 index 80833d7031..0000000000 --- a/patches/@mariozechner__pi-agent-core.patch +++ /dev/null @@ -1,54 +0,0 @@ -diff --git a/dist/agent.d.ts b/dist/agent.d.ts -index fcfb19924ef6ce233aa55795e3687ce23938c5a6..a63daea868c5b3b7f7bb9272576c65c6ad95da8a 100644 ---- a/dist/agent.d.ts -+++ b/dist/agent.d.ts -@@ -38,6 +38,10 @@ export interface AgentOptions { - * Useful for expiring tokens (e.g., GitHub Copilot OAuth). - */ - getApiKey?: (provider: string) => Promise | string | undefined; -+ /** -+ * Extra params to pass to the provider API (e.g., Z.AI GLM thinking mode params). -+ */ -+ extraParams?: Record; - /** - * Custom token budgets for thinking levels (token-based providers only). - */ -@@ -56,6 +60,8 @@ export declare class Agent { - streamFn: StreamFn; - private _sessionId?; - getApiKey?: (provider: string) => Promise | string | undefined; -+ /** Extra params to pass to the provider API. */ -+ extraParams?: Record; - private runningPrompt?; - private resolveRunningPrompt?; - private _thinkingBudgets?; -diff --git a/dist/agent.js b/dist/agent.js -index 34ceb4ddcbc53d83edd82d774a76d9bf469b42f3..ecd8b7641c71523296890e11ac0cf0855a0dadd5 100644 ---- a/dist/agent.js -+++ b/dist/agent.js -@@ -33,6 +33,7 @@ export class Agent { - streamFn; - _sessionId; - getApiKey; -+ extraParams; - runningPrompt; - resolveRunningPrompt; - _thinkingBudgets; -@@ -45,6 +46,8 @@ export class Agent { - this.streamFn = opts.streamFn || streamSimple; - this._sessionId = opts.sessionId; - this.getApiKey = opts.getApiKey; -+ // PATCH: Support extraParams for provider-specific features (e.g., GLM-4.7 thinking mode) -+ this.extraParams = opts.extraParams; - this._thinkingBudgets = opts.thinkingBudgets; - } - /** -@@ -225,6 +228,8 @@ export class Agent { - convertToLlm: this.convertToLlm, - transformContext: this.transformContext, - getApiKey: this.getApiKey, -+ // PATCH: Pass extraParams through to stream function -+ extraParams: this.extraParams, - getSteeringMessages: async () => { - if (this.steeringMode === "one-at-a-time") { - if (this.steeringQueue.length > 0) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd7c5cba8d..bab4865953 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,6 @@ overrides: hono: 4.11.4 tar: 7.5.3 -patchedDependencies: - '@mariozechner/pi-agent-core@0.46.0': - hash: 01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4 - path: patches/@mariozechner__pi-agent-core.patch - importers: .: @@ -35,7 +30,7 @@ importers: version: 1.3.4 '@mariozechner/pi-agent-core': specifier: 0.46.0 - version: 0.46.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5) + version: 0.46.0(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-ai': specifier: 0.46.0 version: 0.46.0(ws@8.19.0)(zod@4.3.5) @@ -5147,7 +5142,7 @@ snapshots: transitivePeerDependencies: - tailwindcss - '@mariozechner/pi-agent-core@0.46.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)': + '@mariozechner/pi-agent-core@0.46.0(ws@8.19.0)(zod@4.3.5)': dependencies: '@mariozechner/pi-ai': 0.46.0(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-tui': 0.46.0 @@ -5186,7 +5181,7 @@ snapshots: dependencies: '@mariozechner/clipboard': 0.3.0 '@mariozechner/jiti': 2.6.2 - '@mariozechner/pi-agent-core': 0.46.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5) + '@mariozechner/pi-agent-core': 0.46.0(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-ai': 0.46.0(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-tui': 0.46.0 '@silvia-odwyer/photon-node': 0.3.4 diff --git a/src/agents/pi-embedded-runner-extraparams.live.test.ts b/src/agents/pi-embedded-runner-extraparams.live.test.ts index 92b8eeb534..6341670d10 100644 --- a/src/agents/pi-embedded-runner-extraparams.live.test.ts +++ b/src/agents/pi-embedded-runner-extraparams.live.test.ts @@ -30,7 +30,7 @@ describeLive("pi embedded extra params (live)", () => { const agent = { streamFn: streamSimple }; - applyExtraParamsToAgent(agent, cfg, "openai", model.id, "off"); + applyExtraParamsToAgent(agent, cfg, "openai", model.id); const stream = agent.streamFn( model, diff --git a/src/agents/pi-embedded-runner-extraparams.test.ts b/src/agents/pi-embedded-runner-extraparams.test.ts index d1243da61f..574bb550de 100644 --- a/src/agents/pi-embedded-runner-extraparams.test.ts +++ b/src/agents/pi-embedded-runner-extraparams.test.ts @@ -1,460 +1,62 @@ import { describe, expect, it } from "vitest"; import { resolveExtraParams } from "./pi-embedded-runner.js"; -/** - * Tests for resolveExtraParams - the function that auto-enables GLM-4.x thinking mode. - * - * Z.AI Cloud API format: thinking: { type: "enabled", clear_thinking: boolean } - * - GLM-4.7: Preserved thinking (clear_thinking: false) - reasoning kept across turns - * - GLM-4.5/4.6: Interleaved thinking (clear_thinking: true) - reasoning cleared each turn - * - * @see https://docs.z.ai/guides/capabilities/thinking-mode - */ - describe("resolveExtraParams", () => { - describe("GLM-4.7 preserved thinking (clear_thinking: false)", () => { - it("auto-enables preserved thinking for zai/glm-4.7 with no config", () => { - const result = resolveExtraParams({ - cfg: undefined, - provider: "zai", - modelId: "glm-4.7", - }); - - expect(result).toEqual({ - thinking: { - type: "enabled", - clear_thinking: false, // Preserved thinking for GLM-4.7 - }, - }); + it("returns undefined with no model config", () => { + const result = resolveExtraParams({ + cfg: undefined, + provider: "zai", + modelId: "glm-4.7", }); - it("auto-enables preserved thinking for zai/GLM-4.7 (case insensitive)", () => { - const result = resolveExtraParams({ - cfg: undefined, - provider: "zai", - modelId: "GLM-4.7", - }); + expect(result).toBeUndefined(); + }); - expect(result).toEqual({ - thinking: { - type: "enabled", - clear_thinking: false, + it("returns params for exact provider/model key", () => { + const result = resolveExtraParams({ + cfg: { + agents: { + defaults: { + models: { + "openai/gpt-4": { + params: { + temperature: 0.7, + maxTokens: 2048, + }, + }, + }, + }, }, - }); + }, + provider: "openai", + modelId: "gpt-4", + }); + + expect(result).toEqual({ + temperature: 0.7, + maxTokens: 2048, }); }); - describe("GLM-4.5/4.6 interleaved thinking (clear_thinking: true)", () => { - it("auto-enables interleaved thinking for zai/glm-4.5", () => { - const result = resolveExtraParams({ - cfg: undefined, - provider: "zai", - modelId: "glm-4.5", - }); - - expect(result).toEqual({ - thinking: { - type: "enabled", - clear_thinking: true, // Interleaved thinking for GLM-4.5 - }, - }); - }); - - it("auto-enables interleaved thinking for zai/glm-4.6", () => { - const result = resolveExtraParams({ - cfg: undefined, - provider: "zai", - modelId: "glm-4.6", - }); - - expect(result).toEqual({ - thinking: { - type: "enabled", - clear_thinking: true, // Interleaved thinking for GLM-4.6 - }, - }); - }); - - it("auto-enables interleaved thinking for zai/glm-4-flash", () => { - const result = resolveExtraParams({ - cfg: undefined, - provider: "zai", - modelId: "glm-4-flash", - }); - - expect(result).toEqual({ - thinking: { - type: "enabled", - clear_thinking: true, // Non-4.7 gets interleaved - }, - }); - }); - - it("auto-enables interleaved thinking for zai/glm-4.5-air", () => { - const result = resolveExtraParams({ - cfg: undefined, - provider: "zai", - modelId: "glm-4.5-air", - }); - - expect(result).toEqual({ - thinking: { - type: "enabled", - clear_thinking: true, - }, - }); - }); - }); - - describe("config overrides", () => { - it("respects explicit thinking config from user (disable thinking)", () => { - const result = resolveExtraParams({ - cfg: { - agents: { - defaults: { - models: { - "zai/glm-4.7": { - params: { - thinking: { - type: "disabled", - }, - }, + it("ignores unrelated model entries", () => { + const result = resolveExtraParams({ + cfg: { + agents: { + defaults: { + models: { + "openai/gpt-4": { + params: { + temperature: 0.7, }, }, }, }, }, - provider: "zai", - modelId: "glm-4.7", - }); - - expect(result).toEqual({ - thinking: { - type: "disabled", - }, - }); + }, + provider: "openai", + modelId: "gpt-4.1-mini", }); - it("preserves other params while adding thinking config", () => { - const result = resolveExtraParams({ - cfg: { - agents: { - defaults: { - models: { - "zai/glm-4.7": { - params: { - temperature: 0.7, - max_tokens: 4096, - }, - }, - }, - }, - }, - }, - provider: "zai", - modelId: "glm-4.7", - }); - - expect(result).toEqual({ - temperature: 0.7, - max_tokens: 4096, - thinking: { - type: "enabled", - clear_thinking: false, - }, - }); - }); - - it("does not override explicit thinking config even if partial", () => { - const result = resolveExtraParams({ - cfg: { - agents: { - defaults: { - models: { - "zai/glm-4.7": { - params: { - thinking: { - type: "enabled", - // User explicitly omitted clear_thinking - }, - }, - }, - }, - }, - }, - }, - provider: "zai", - modelId: "glm-4.7", - }); - - // Should use user's config exactly, not merge defaults - expect(result).toEqual({ - thinking: { - type: "enabled", - }, - }); - }); - }); - - describe("non-GLM models", () => { - it("returns undefined for anthropic/claude with no config", () => { - const result = resolveExtraParams({ - cfg: undefined, - provider: "anthropic", - modelId: "claude-3-opus", - }); - - expect(result).toBeUndefined(); - }); - - it("returns undefined for openai/gpt-4 with no config", () => { - const result = resolveExtraParams({ - cfg: undefined, - provider: "openai", - modelId: "gpt-4", - }); - - expect(result).toBeUndefined(); - }); - - it("passes through params for non-GLM models without modification", () => { - const result = resolveExtraParams({ - cfg: { - agents: { - defaults: { - models: { - "openai/gpt-4": { - params: { - logprobs: true, - top_logprobs: 5, - }, - }, - }, - }, - }, - }, - provider: "openai", - modelId: "gpt-4", - }); - - expect(result).toEqual({ - logprobs: true, - top_logprobs: 5, - }); - }); - - it("does not auto-enable thinking for non-zai provider even with glm-4 model id", () => { - const result = resolveExtraParams({ - cfg: undefined, - provider: "openai", - modelId: "glm-4.7", // Even if model ID contains glm-4 - }); - - expect(result).toBeUndefined(); - }); - }); - - describe("edge cases", () => { - it("handles empty config gracefully", () => { - const result = resolveExtraParams({ - cfg: {}, - provider: "zai", - modelId: "glm-4.7", - }); - - expect(result).toEqual({ - thinking: { - type: "enabled", - clear_thinking: false, - }, - }); - }); - - it("handles config with empty models gracefully", () => { - const result = resolveExtraParams({ - cfg: { agents: { defaults: { models: {} } } }, - provider: "zai", - modelId: "glm-4.7", - }); - - expect(result).toEqual({ - thinking: { - type: "enabled", - clear_thinking: false, - }, - }); - }); - - it("model alias lookup uses exact provider/model key", () => { - const result = resolveExtraParams({ - cfg: { - agents: { - defaults: { - models: { - "zai/glm-4.7": { - alias: "smart", - params: { - custom_param: "value", - }, - }, - }, - }, - }, - }, - provider: "zai", - modelId: "glm-4.7", - }); - - expect(result).toEqual({ - custom_param: "value", - thinking: { - type: "enabled", - clear_thinking: false, - }, - }); - }); - - it("treats thinking: null as explicit config (no auto-enable)", () => { - const result = resolveExtraParams({ - cfg: { - agents: { - defaults: { - models: { - "zai/glm-4.7": { - params: { - thinking: null, - }, - }, - }, - }, - }, - }, - provider: "zai", - modelId: "glm-4.7", - }); - - // null is !== undefined, so we respect the explicit null config - expect(result).toEqual({ - thinking: null, - }); - }); - - it("handles GLM-4.7 variants (glm-4.7-flash, glm-4.7-plus)", () => { - // GLM-4.7-flash should get preserved thinking (contains "glm-4.7") - const flashResult = resolveExtraParams({ - cfg: undefined, - provider: "zai", - modelId: "glm-4.7-flash", - }); - - expect(flashResult).toEqual({ - thinking: { - type: "enabled", - clear_thinking: false, // Preserved thinking for GLM-4.7 variants - }, - }); - - // GLM-4.7-plus should also get preserved thinking - const plusResult = resolveExtraParams({ - cfg: undefined, - provider: "zai", - modelId: "glm-4.7-plus", - }); - - expect(plusResult).toEqual({ - thinking: { - type: "enabled", - clear_thinking: false, - }, - }); - }); - }); - - describe("thinkLevel parameter", () => { - it("thinkLevel: 'off' disables auto-enable for GLM-4.x", () => { - const result = resolveExtraParams({ - cfg: undefined, - provider: "zai", - modelId: "glm-4.7", - thinkLevel: "off", - }); - - // Should NOT auto-enable thinking when user explicitly disabled it - expect(result).toBeUndefined(); - }); - - it("thinkLevel: 'off' still passes through explicit config", () => { - const result = resolveExtraParams({ - cfg: { - agents: { - defaults: { - models: { - "zai/glm-4.7": { - params: { - custom_param: "value", - }, - }, - }, - }, - }, - }, - provider: "zai", - modelId: "glm-4.7", - thinkLevel: "off", - }); - - // Should pass through config params but NOT auto-add thinking - expect(result).toEqual({ - custom_param: "value", - }); - }); - - it("thinkLevel: 'low' allows auto-enable", () => { - const result = resolveExtraParams({ - cfg: undefined, - provider: "zai", - modelId: "glm-4.7", - thinkLevel: "low", - }); - - expect(result).toEqual({ - thinking: { - type: "enabled", - clear_thinking: false, - }, - }); - }); - - it("thinkLevel: 'high' allows auto-enable", () => { - const result = resolveExtraParams({ - cfg: undefined, - provider: "zai", - modelId: "glm-4.5", - thinkLevel: "high", - }); - - expect(result).toEqual({ - thinking: { - type: "enabled", - clear_thinking: true, - }, - }); - }); - - it("thinkLevel: undefined (not specified) allows auto-enable", () => { - const result = resolveExtraParams({ - cfg: undefined, - provider: "zai", - modelId: "glm-4.7", - // thinkLevel not specified - }); - - expect(result).toEqual({ - thinking: { - type: "enabled", - clear_thinking: false, - }, - }); - }); + expect(result).toBeUndefined(); }); }); diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/pi-embedded-runner/extra-params.ts index 220d1c2d63..a58ba1c714 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -6,62 +6,19 @@ import type { ClawdbotConfig } from "../../config/config.js"; import { log } from "./logger.js"; /** - * Resolve provider-specific extraParams from model config. - * Auto-enables thinking mode for GLM-4.x models unless explicitly disabled. + * Resolve provider-specific extra params from model config. + * Used to pass through stream params like temperature/maxTokens. * - * For ZAI GLM-4.x models, we auto-enable thinking via the Z.AI Cloud API format: - * thinking: { type: "enabled", clear_thinking: boolean } - * - * - GLM-4.7: Preserved thinking (clear_thinking: false) - reasoning kept across turns - * - GLM-4.5/4.6: Interleaved thinking (clear_thinking: true) - reasoning cleared each turn - * - * Users can override via config: - * agents.defaults.models["zai/glm-4.7"].params.thinking = { type: "disabled" } - * - * Or disable via runtime flag: --thinking off - * - * @see https://docs.z.ai/guides/capabilities/thinking-mode * @internal Exported for testing only */ export function resolveExtraParams(params: { cfg: ClawdbotConfig | undefined; provider: string; modelId: string; - thinkLevel?: string; }): Record | undefined { const modelKey = `${params.provider}/${params.modelId}`; const modelConfig = params.cfg?.agents?.defaults?.models?.[modelKey]; - let extraParams = modelConfig?.params ? { ...modelConfig.params } : undefined; - - // Auto-enable thinking for ZAI GLM-4.x models when not explicitly configured - // Skip if user explicitly disabled thinking via --thinking off - if (params.provider === "zai" && params.thinkLevel !== "off") { - const modelIdLower = params.modelId.toLowerCase(); - const isGlm4 = modelIdLower.includes("glm-4"); - - if (isGlm4) { - const hasThinkingConfig = extraParams?.thinking !== undefined; - if (!hasThinkingConfig) { - // GLM-4.7 supports preserved thinking; GLM-4.5/4.6 clear each turn. - const isGlm47 = modelIdLower.includes("glm-4.7"); - const clearThinking = !isGlm47; - - extraParams = { - ...extraParams, - thinking: { - type: "enabled", - clear_thinking: clearThinking, - }, - }; - - log.debug( - `auto-enabled thinking for ${modelKey}: type=enabled, clear_thinking=${clearThinking}`, - ); - } - } - } - - return extraParams; + return modelConfig?.params ? { ...modelConfig.params } : undefined; } function createStreamFnWithExtraParams( @@ -106,13 +63,11 @@ export function applyExtraParamsToAgent( cfg: ClawdbotConfig | undefined, provider: string, modelId: string, - thinkLevel?: string, ): void { const extraParams = resolveExtraParams({ cfg, provider, modelId, - thinkLevel, }); const wrappedStreamFn = createStreamFnWithExtraParams(agent.streamFn, extraParams); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 2ca91ac7ba..12d19087e2 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -330,7 +330,6 @@ export async function runEmbeddedAttempt( params.config, params.provider, params.modelId, - params.thinkLevel, ); try { diff --git a/src/auto-reply/thinking.test.ts b/src/auto-reply/thinking.test.ts index 6d49b7cced..c888387a18 100644 --- a/src/auto-reply/thinking.test.ts +++ b/src/auto-reply/thinking.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from "vitest"; -import { listThinkingLevels, normalizeReasoningLevel, normalizeThinkLevel } from "./thinking.js"; +import { + listThinkingLevelLabels, + listThinkingLevels, + normalizeReasoningLevel, + normalizeThinkLevel, +} from "./thinking.js"; describe("normalizeThinkLevel", () => { it("accepts mid as medium", () => { @@ -9,6 +14,10 @@ describe("normalizeThinkLevel", () => { it("accepts xhigh", () => { expect(normalizeThinkLevel("xhigh")).toBe("xhigh"); }); + + it("accepts on as low", () => { + expect(normalizeThinkLevel("on")).toBe("low"); + }); }); describe("listThinkingLevels", () => { @@ -25,6 +34,17 @@ describe("listThinkingLevels", () => { }); }); +describe("listThinkingLevelLabels", () => { + it("returns on/off for ZAI", () => { + expect(listThinkingLevelLabels("zai", "glm-4.7")).toEqual(["off", "on"]); + }); + + it("returns full levels for non-ZAI", () => { + expect(listThinkingLevelLabels("openai", "gpt-4.1-mini")).toContain("low"); + expect(listThinkingLevelLabels("openai", "gpt-4.1-mini")).not.toContain("on"); + }); +}); + describe("normalizeReasoningLevel", () => { it("accepts on/off", () => { expect(normalizeReasoningLevel("on")).toBe("on"); diff --git a/src/auto-reply/thinking.ts b/src/auto-reply/thinking.ts index 8e99c2d46f..297a8d6ca6 100644 --- a/src/auto-reply/thinking.ts +++ b/src/auto-reply/thinking.ts @@ -4,6 +4,17 @@ export type ElevatedLevel = "off" | "on"; export type ReasoningLevel = "off" | "on" | "stream"; export type UsageDisplayLevel = "off" | "on"; +function normalizeProviderId(provider?: string | null): string { + if (!provider) return ""; + const normalized = provider.trim().toLowerCase(); + if (normalized === "z.ai" || normalized === "z-ai") return "zai"; + return normalized; +} + +export function isBinaryThinkingProvider(provider?: string | null): boolean { + return normalizeProviderId(provider) === "zai"; +} + export const XHIGH_MODEL_REFS = [ "openai/gpt-5.2", "openai-codex/gpt-5.2-codex", @@ -22,6 +33,7 @@ export function normalizeThinkLevel(raw?: string | null): ThinkLevel | undefined if (!raw) return undefined; const key = raw.toLowerCase(); if (["off"].includes(key)) return "off"; + if (["on", "enable", "enabled"].includes(key)) return "low"; if (["min", "minimal"].includes(key)) return "minimal"; if (["low", "thinkhard", "think-hard", "think_hard"].includes(key)) return "low"; if (["mid", "med", "medium", "thinkharder", "think-harder", "harder"].includes(key)) @@ -49,12 +61,20 @@ export function listThinkingLevels(provider?: string | null, model?: string | nu return levels; } +export function listThinkingLevelLabels( + provider?: string | null, + model?: string | null, +): string[] { + if (isBinaryThinkingProvider(provider)) return ["off", "on"]; + return listThinkingLevels(provider, model); +} + export function formatThinkingLevels( provider?: string | null, model?: string | null, separator = ", ", ): string { - return listThinkingLevels(provider, model).join(separator); + return listThinkingLevelLabels(provider, model).join(separator); } export function formatXHighModelHint(): string { diff --git a/src/tui/commands.ts b/src/tui/commands.ts index 87512246c7..1e07c8e182 100644 --- a/src/tui/commands.ts +++ b/src/tui/commands.ts @@ -1,5 +1,5 @@ import type { SlashCommand } from "@mariozechner/pi-tui"; -import { formatThinkingLevels, listThinkingLevels } from "../auto-reply/thinking.js"; +import { formatThinkingLevels, listThinkingLevelLabels } from "../auto-reply/thinking.js"; const VERBOSE_LEVELS = ["on", "off"]; const REASONING_LEVELS = ["on", "off"]; @@ -33,7 +33,7 @@ export function parseCommand(input: string): ParsedCommand { } export function getSlashCommands(options: SlashCommandOptions = {}): SlashCommand[] { - const thinkLevels = listThinkingLevels(options.provider, options.model); + const thinkLevels = listThinkingLevelLabels(options.provider, options.model); return [ { name: "help", description: "Show slash command help" }, { name: "status", description: "Show gateway status summary" }, diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts index 3f18925940..8055d77ddc 100644 --- a/ui/src/ui/types.ts +++ b/ui/src/ui/types.ts @@ -282,6 +282,7 @@ export type GatewaySessionRow = { outputTokens?: number; totalTokens?: number; model?: string; + modelProvider?: string; contextTokens?: number; }; diff --git a/ui/src/ui/views/sessions.ts b/ui/src/ui/views/sessions.ts index 32106e2bac..1c15a25556 100644 --- a/ui/src/ui/views/sessions.ts +++ b/ui/src/ui/views/sessions.ts @@ -32,6 +32,7 @@ export type SessionsProps = { }; const THINK_LEVELS = ["", "off", "minimal", "low", "medium", "high"] as const; +const BINARY_THINK_LEVELS = ["", "off", "on"] as const; const VERBOSE_LEVELS = [ { value: "", label: "inherit" }, { value: "off", label: "off (explicit)" }, @@ -39,6 +40,34 @@ const VERBOSE_LEVELS = [ ] as const; const REASONING_LEVELS = ["", "off", "on", "stream"] as const; +function normalizeProviderId(provider?: string | null): string { + if (!provider) return ""; + const normalized = provider.trim().toLowerCase(); + if (normalized === "z.ai" || normalized === "z-ai") return "zai"; + return normalized; +} + +function isBinaryThinkingProvider(provider?: string | null): boolean { + return normalizeProviderId(provider) === "zai"; +} + +function resolveThinkLevelOptions(provider?: string | null): readonly string[] { + return isBinaryThinkingProvider(provider) ? BINARY_THINK_LEVELS : THINK_LEVELS; +} + +function resolveThinkLevelDisplay(value: string, isBinary: boolean): string { + if (!isBinary) return value; + if (!value || value === "off") return value; + return "on"; +} + +function resolveThinkLevelPatchValue(value: string, isBinary: boolean): string | null { + if (!value) return null; + if (!isBinary) return value; + if (value === "on") return "low"; + return value; +} + export function renderSessions(props: SessionsProps) { const rows = props.result?.sessions ?? []; return html` @@ -143,7 +172,10 @@ function renderRow( onPatch: SessionsProps["onPatch"], ) { const updated = row.updatedAt ? formatAgo(row.updatedAt) : "n/a"; - const thinking = row.thinkingLevel ?? ""; + const rawThinking = row.thinkingLevel ?? ""; + const isBinaryThinking = isBinaryThinkingProvider(row.modelProvider); + const thinking = resolveThinkLevelDisplay(rawThinking, isBinaryThinking); + const thinkLevels = resolveThinkLevelOptions(row.modelProvider); const verbose = row.verboseLevel ?? ""; const reasoning = row.reasoningLevel ?? ""; const displayName = row.displayName ?? row.key; @@ -166,10 +198,12 @@ function renderRow( .value=${thinking} @change=${(e: Event) => { const value = (e.target as HTMLSelectElement).value; - onPatch(row.key, { thinkingLevel: value || null }); + onPatch(row.key, { + thinkingLevel: resolveThinkLevelPatchValue(value, isBinaryThinking), + }); }} > - ${THINK_LEVELS.map((level) => + ${thinkLevels.map((level) => html``, )}