From edb07bc687b1f63b801f491dd16150ec0fc33414 Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Thu, 20 Aug 2026 12:02:21 +0500 Subject: [PATCH 1/6] fix(thinking): force GLM 5.3+ thinking on (issue #162) GLM 5.3 and later reject thinking: { type: disabled } with 'This model always engages in thinking and cannot be disabled'. Detect 5.3+ by version and force thinking on (default high), mirroring the Kimi K2.7-code force-on fix. The picker now exposes only high/max and hides off. --- src/thinking/glm.ts | 60 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/src/thinking/glm.ts b/src/thinking/glm.ts index 40ee2c5..da3d5b1 100644 --- a/src/thinking/glm.ts +++ b/src/thinking/glm.ts @@ -10,7 +10,20 @@ import { schemaFromReasoningOptions, effortProperty, type ThinkingSchema } from import type { ResolvedModelMetadata } from "../models/metadata"; import type { ThinkingSettings, ThinkingFamily, BuildThinkingPayloadOptions } from "./types"; +// Standard GLM models accept off/high/max (issue #61). const GLM_EFFORTS = ["off", "high", "max"] as const; +// GLM 5.3+ always engage in thinking and reject `disabled` (issue #162); the +// upstream accepts high/max reasoning effort, so we expose only those. +const GLM_ALWAYS_THINKING_EFFORTS = ["high", "max"] as const; + +/** + * GLM 5.3 and later cannot disable thinking (Zhipu API constraint — the same + * class of issue as Kimi K2.7-code in #25). Detected by version so future 5.x + * releases are covered automatically. + */ +function isAlwaysThinking(modelId: string): boolean { + return /^glm-5[.\-](?:[3-9]|\d{2,})/i.test(modelId); +} export class GlmThinking extends BaseThinkingProvider { readonly family: ThinkingFamily = "glm"; @@ -20,26 +33,53 @@ export class GlmThinking extends BaseThinkingProvider { } schema(metadata?: ResolvedModelMetadata): ThinkingSchema | undefined { - return ( - schemaFromReasoningOptions(metadata) ?? { + const fromOptions = schemaFromReasoningOptions(metadata); + if (fromOptions) return fromOptions; + + // GLM 5.3+ cannot disable thinking — expose only the effort levels the + // upstream accepts, defaulting to "high" (defensive against stale cache). + if (isAlwaysThinking(this.modelId)) { + return { properties: { reasoningEffort: effortProperty({ - enum: GLM_EFFORTS, - labels: ["Off", "High", "Max"], - descriptions: ["Fastest responses", "Greater reasoning depth", "Maximum reasoning effort"], - default: "off", + enum: GLM_ALWAYS_THINKING_EFFORTS, + labels: ["High", "Max"], + descriptions: ["Greater reasoning depth", "Maximum reasoning effort"], + default: "high", }), }, - } - ); + }; + } + + return { + properties: { + reasoningEffort: effortProperty({ + enum: GLM_EFFORTS, + labels: ["Off", "High", "Max"], + descriptions: ["Fastest responses", "Greater reasoning depth", "Maximum reasoning effort"], + default: "off", + }), + }, + }; } applyOverride(settings: ThinkingSettings, override: Record): ThinkingSettings { - let next = this.applyEffort(settings, override, "glm", GLM_EFFORTS); - next = this.applyMode(next, override, "glm", GLM_EFFORTS); + const efforts = isAlwaysThinking(this.modelId) ? GLM_ALWAYS_THINKING_EFFORTS : GLM_EFFORTS; + let next = this.applyEffort(settings, override, "glm", efforts); + next = this.applyMode(next, override, "glm", efforts); return next; } + normalize(settings: ThinkingSettings): ThinkingSettings { + // GLM 5.3+ forces thinking on regardless of picker selection (defensive — + // the picker only exposes effort levels, but VS Code may cache a stale + // "off" value). The upstream rejects `thinking: { type: "disabled" }`. + if (isAlwaysThinking(this.modelId) && settings.glm === "off") { + return { ...settings, glm: "high" }; + } + return settings; + } + buildPayload(thinking: ThinkingSettings, _opts?: BuildThinkingPayloadOptions): Record { if (thinking.glm === "off") { return { thinking: { type: "disabled" } }; From f52786da250d67cad9bcb26faf23bfcd378a64fe Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Thu, 20 Aug 2026 12:02:44 +0500 Subject: [PATCH 2/6] fix(retry): strip thinking on 'cannot be disabled' 400 (issue #162) The retry safety-net had no pattern for the GLM 5.3 error 'This model always engages in thinking and cannot be disabled'. Add a pattern that strips the thinking field so a stale disabled payload is recovered automatically (the model thinks by default). --- src/retry.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/retry.ts b/src/retry.ts index 1973714..26699eb 100644 --- a/src/retry.ts +++ b/src/retry.ts @@ -65,6 +65,16 @@ const RECOVERABLE_ERROR_PATTERNS: { }, describe: () => "removed thinking field (model requires disabled)", }, + // "This model always engages in thinking and cannot be disabled" (GLM 5.3+) + { + pattern: /cannot be disabled/i, + patch: (body) => { + const next = { ...body }; + delete next.thinking; + return next; + }, + describe: () => "removed thinking field (model cannot disable thinking)", + }, // Generic "invalid thinking" — strip the field entirely { pattern: /invalid thinking/i, From 59be7b68c8a693d96f49afa5f08184ff8fc7f34e Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Thu, 20 Aug 2026 12:03:07 +0500 Subject: [PATCH 3/6] test(thinking): cover GLM 5.3 always-thinking (issue #162) Add cases for version detection (dot/hyphen/free/suffix variants), forced-on resolution even with a stale 'off' override, payload shape (reasoning_effort high/max), and picker schema exposing only high/max. Confirms glm-5.2/5.1/5 still allow disabled. --- src/test/thinking.test.ts | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/test/thinking.test.ts b/src/test/thinking.test.ts index ea41510..96a0491 100644 --- a/src/test/thinking.test.ts +++ b/src/test/thinking.test.ts @@ -262,6 +262,64 @@ describe("GLMThinking — picker schema", () => { }); }); +describe("GLMThinking — glm-5.3 always-thinking (issue #162)", () => { + it("forces glm='high' through resolve even with no override (defensive default)", () => { + const resolved = resolveThinkingConfig({ modelId: "glm-5.3", workspace: defaultSettings }); + assert.equal(resolved.settings.glm, "high"); + }); + + it("forces glm='high' even when override requests 'off' (stale cache)", () => { + const resolved = resolveThinkingConfig({ + modelId: "glm-5.3", + workspace: defaultSettings, + modelConfiguration: { reasoningEffort: "off" }, + }); + assert.equal(resolved.settings.glm, "high"); + }); + + it("respects a valid 'max' override for glm-5.3", () => { + const resolved = resolveThinkingConfig({ + modelId: "glm-5.3", + workspace: defaultSettings, + modelConfiguration: { reasoningEffort: "max" }, + }); + assert.equal(resolved.settings.glm, "max"); + }); + + it("glm-5.3 with glm='high' emits reasoning_effort: 'high' (never disabled)", () => { + const payload = thinkingProviderFor("glm-5.3").buildPayload({ ...defaultSettings, glm: "high" }); + assert.deepEqual(payload, { reasoning_effort: "high" }); + }); + + it("glm-5.3 with glm='max' emits reasoning_effort: 'max'", () => { + const payload = thinkingProviderFor("glm-5.3").buildPayload({ ...defaultSettings, glm: "max" }); + assert.deepEqual(payload, { reasoning_effort: "max" }); + }); + + it("picker schema exposes only high/max (no 'off') and defaults to 'high'", () => { + const schema = thinkingProviderFor("glm-5.3").schema(); + assert.ok(schema, "expected schema to be defined"); + const reasoningEffort = schema.properties.reasoningEffort as Record; + assert.deepEqual(reasoningEffort.enum, ["high", "max"]); + assert.deepEqual(reasoningEffort.enumItemLabels, ["High", "Max"]); + assert.equal(reasoningEffort.default, "high"); + }); + + it("detects glm-5.3 variants (dot/hyphen, free/suffix) as always-thinking", () => { + for (const id of ["glm-5.3", "glm-5.3-free", "glm-5-3", "glm-5.10", "glm-5.3-code"]) { + const resolved = resolveThinkingConfig({ modelId: id, workspace: defaultSettings }); + assert.equal(resolved.settings.glm, "high", `expected ${id} to force thinking on`); + } + }); + + it("still allows glm-5.2 / glm-5.1 / glm-5 to disable thinking", () => { + for (const id of ["glm-5.2", "glm-5.1", "glm-5"]) { + const payload = thinkingProviderFor(id).buildPayload({ ...defaultSettings, glm: "off" }); + assert.deepEqual(payload, { thinking: { type: "disabled" } }, `expected ${id} to allow disabled`); + } + }); +}); + describe("QwenThinking — payload per endpoint", () => { it("chat endpoint with qwen='off' emits enable_thinking: false", () => { const payload = thinkingProviderFor("qwen3.6-plus").buildPayload({ ...defaultSettings, qwen: "off" }); From a83086f3b955ecb090de8f9d292c79e7c40f99cd Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Thu, 20 Aug 2026 12:03:20 +0500 Subject: [PATCH 4/6] test(retry): cover GLM cannot-be-disabled 400 (issue #162) Assert analyzeHttp400ForRetry strips the thinking field for the GLM 5.3 'cannot be disabled' error and recovers. --- src/test/retry.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/retry.test.ts b/src/test/retry.test.ts index 08ffaad..e9394f7 100644 --- a/src/test/retry.test.ts +++ b/src/test/retry.test.ts @@ -24,6 +24,17 @@ describe("analyzeHttp400ForRetry — thinking errors", () => { assert.ok(result, "should be recoverable"); assert.deepEqual(result.body, { model: "test", temperature: 0.2 }); }); + + it("patches GLM 'cannot be disabled' by removing thinking (issue #162)", () => { + const body = { model: "glm-5.3", thinking: { type: "disabled" } }; + const result = analyzeHttp400ForRetry( + "Upstream request failed: [1210] This model always engages in thinking and cannot be disabled; please use low, high, or max", + body, + ); + assert.ok(result, "should be recoverable"); + assert.deepEqual(result.body, { model: "glm-5.3" }); + assert.match(result.reason, /cannot disable thinking/i); + }); }); describe("analyzeHttp400ForRetry — temperature errors", () => { From 31dd5bf1ebe8c855359d379374ce16325de6c641 Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Thu, 20 Aug 2026 12:28:54 +0500 Subject: [PATCH 5/6] docs: changelog + thinking doc for GLM 5.3 (issue #162) Add a Fixed entry to CHANGELOG and a note in the per-model thinking controls doc describing the GLM 5.3+ force-on behavior. Also fix pre-existing MD049 underscore emphasis and prettier formatting in both files. --- CHANGELOG.md | 2 ++ docs/features/02-20260517-per-model-thinking-controls.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9846d42..5dcde7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ All notable changes to the **OpenCode Go BYOK Provider** extension are documente - **DeepSeek / Mimo thinking content no longer leaks into the chat transcript.** `treatReasoningAsContent` was mis-detecting native-reasoning families as "no reasoning in body" and echoing their `reasoning_content` as plain chat text. The decision now comes from the provider strategy (always `false` for DeepSeek and Mimo), so chain-of-thought stays in the thinking panel. +- **`[Thinking]` GLM 5.3+ can no longer be disabled (HTTP 400, #162).** GLM 5.3 and later reject `thinking: { type: "disabled" }` with _"This model always engages in thinking and cannot be disabled; please use low, high, or max"_. `GlmThinking` now detects GLM 5.3+ by version and forces thinking on (default `high`; the picker exposes only `high`/`max` and hides `off`), mirroring the Kimi K2.7-code force-on fix. A defensive retry pattern in `retry.ts` also strips `thinking` when the upstream reports it cannot be disabled, so any stale cached `off` is recovered automatically. Unit tests added for version detection, forced-on resolution, payload shape and picker schema. + ## [0.6.0] — 2026-08-13 ### Added diff --git a/docs/features/02-20260517-per-model-thinking-controls.md b/docs/features/02-20260517-per-model-thinking-controls.md index 449e30c..e14a4cd 100644 --- a/docs/features/02-20260517-per-model-thinking-controls.md +++ b/docs/features/02-20260517-per-model-thinking-controls.md @@ -59,6 +59,8 @@ The feature adds explicit settings under the `opencodego.thinking.*` namespace: These settings act as persistent defaults and as a fallback when the VS Code model picker does not expose native model configuration controls. +> **GLM 5.3+ cannot disable thinking (issue #162).** GLM 5.3 and later reject `thinking: { type: "disabled" }` with _"This model always engages in thinking and cannot be disabled"_. The `glm` strategy detects 5.3+ by version, forces thinking on (default `high`), and hides `off` from the picker — mirroring the Kimi K2.7-code force-on behavior. A defensive retry pattern also strips `thinking` when the upstream reports it cannot be disabled. + ### Native Model Configuration Schema Thinking-capable models publish a `configurationSchema` through `LanguageModelChatInformation`. From 4264ce69a7c58ec64f04fa91295e8ebfaf7273ed Mon Sep 17 00:00:00 2001 From: Fahad Iftikhar Date: Fri, 21 Aug 2026 15:22:33 +0500 Subject: [PATCH 6/6] fix(retry): scope GLM cannot-be-disabled pattern to thinking errors (review #166) --- src/retry.ts | 4 +++- src/test/retry.test.ts | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/retry.ts b/src/retry.ts index 26699eb..17f2da4 100644 --- a/src/retry.ts +++ b/src/retry.ts @@ -66,8 +66,10 @@ const RECOVERABLE_ERROR_PATTERNS: { describe: () => "removed thinking field (model requires disabled)", }, // "This model always engages in thinking and cannot be disabled" (GLM 5.3+) + // Scoped to thinking-related phrasing so unrelated "cannot be disabled" + // errors never trigger a thinking-strip retry. { - pattern: /cannot be disabled/i, + pattern: /always engages in thinking|thinking.*cannot be disabled/i, patch: (body) => { const next = { ...body }; delete next.thinking; diff --git a/src/test/retry.test.ts b/src/test/retry.test.ts index e9394f7..2beec1f 100644 --- a/src/test/retry.test.ts +++ b/src/test/retry.test.ts @@ -35,6 +35,12 @@ describe("analyzeHttp400ForRetry — thinking errors", () => { assert.deepEqual(result.body, { model: "glm-5.3" }); assert.match(result.reason, /cannot disable thinking/i); }); + + it("does not patch unrelated errors that merely contain 'cannot be disabled'", () => { + const body = { model: "glm-5.3", thinking: { type: "disabled" } }; + const result = analyzeHttp400ForRetry("feature X cannot be disabled for this account", body); + assert.equal(result, undefined); + }); }); describe("analyzeHttp400ForRetry — temperature errors", () => {