Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,9 @@
},
"opencodego.thinking.glm": {
"type": "string",
"enum": ["on", "off"],
"enum": ["off", "high", "max"],
"default": "off",
"markdownDescription": "Thinking mode for GLM models (`glm-5`, `glm-5.1`). Maps to `thinking: { type: 'enabled' | 'disabled' }`."
"markdownDescription": "Thinking mode for GLM models. `high`/`max` enable thinking (interpreted as `on` for models that use toggle, e.g., GLM 5/5.1). Per-model picker shows only options supported by each model."
},
"opencodego.thinking.kimi": {
"type": "string",
Expand Down
5 changes: 3 additions & 2 deletions scripts/validate-models.mts
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,10 @@ function buildThinkingTests(model: ModelInfo): ParamTest[] {
}
tests.push({ name: "no-reasoning (off)", settings: { deepseek: "off" } });
}
// GLM: thinking type
// GLM: thinking type with effort values (GLM 5.2: high/max, older: enabled)
else if (/^glm-/i.test(id)) {
tests.push({ name: "thinking=enabled", settings: { glm: "on" } });
tests.push({ name: "thinking=high", settings: { glm: "high" } });
tests.push({ name: "thinking=max", settings: { glm: "max" } });
tests.push({ name: "thinking=disabled", settings: { glm: "off" } });
}
// Qwen: enable_thinking + budget
Expand Down
15 changes: 8 additions & 7 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ async function showModelPickerDiagnostics(): Promise<void> {
async function showThinkingEffortPicker(): Promise<void> {
const families: { label: string; key: keyof ThinkingSettings; options: string[] }[] = [
{ label: "DeepSeek (deepseek-v4-*)", key: "deepseek", options: ["off", "low", "medium", "high", "max"] },
{ label: "GLM (glm-5, glm-5.1)", key: "glm", options: ["on", "off"] },
{ label: "GLM (glm-5, glm-5.1, glm-5.2)", key: "glm", options: ["off", "high", "max"] },
{ label: "Kimi (kimi-k2.*)", key: "kimi", options: ["on", "off"] },
{ label: "Mimo (mimo-v2.*)", key: "mimo", options: ["off", "low", "medium", "high"] },
{ label: "MiniMax (minimax-m*)", key: "minimax", options: ["off", "on"] },
Expand Down Expand Up @@ -1277,7 +1277,6 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider<OpenCodeModel
const responseText = await response.text();
statusBar.dispose();
this.log(`Test response (${response.status}): ${responseText}`);
this.getOutputChannel().show(true);

if (response.ok) {
vscode.window.showInformationMessage(`${this.definition.displayName}: Connection OK (HTTP ${response.status}). Check Output panel for details.`);
Expand All @@ -1288,7 +1287,6 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider<OpenCodeModel
statusBar.dispose();
const message = error instanceof Error ? error.message : String(error);
this.log(`Test connection error: ${message}`);
this.getOutputChannel().show(true);
vscode.window.showErrorMessage(`${this.definition.displayName}: Connection error - ${message}`);
}
}
Expand Down Expand Up @@ -1398,7 +1396,7 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider<OpenCodeModel
return [];
}

const models = await this.fetchModels();
const models = await this.fetchModels(apiKey);
if (models.length === 0) {
return [];
}
Expand Down Expand Up @@ -1662,7 +1660,6 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider<OpenCodeModel
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.log(`ERROR model=${model.id}: ${message}`);
this.getOutputChannel().show(true);
if (error instanceof OpenCodeRequestError) {
vscode.window.showErrorMessage(error.userMessage);
}
Expand All @@ -1680,9 +1677,13 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider<OpenCodeModel
: estimateChatMessageTokenCount(text);
}

private async fetchModels(): Promise<string[]> {
private async fetchModels(apiKey?: string): Promise<string[]> {
try {
const response = await fetch(this.definition.modelsUrl);
const headers: Record<string, string> = {};
if (apiKey) {
headers["Authorization"] = `Bearer ${apiKey}`;
}
const response = await fetch(this.definition.modelsUrl, { headers });

if (!response.ok) {
throw new Error(`Model list request failed (${response.status}): ${response.statusText}`);
Expand Down
102 changes: 102 additions & 0 deletions src/test/thinking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,105 @@ describe("thinkingFamily — detection", () => {
assert.equal(thinkingFamily("unknown-model"), null);
});
});

/**
* Tests for GLM models with effort-style reasoning (issue #61).
*
* models.dev reports:
* glm-5.2 → reasoning_options = [{ type: "effort", values: ["high", "max"] }]
* glm-5.1 → no reasoning_options (toggle-based)
* glm-5 → no reasoning_options (toggle-based)
*
* The new "high"/"max" values must map to thinking enabled in the payload,
* and the per-model picker should expose only the relevant options.
*/
describe("buildThinkingPayload — GLM with effort values (issue #61)", () => {
it("glm-5.2 with glm='high' emits reasoning_effort: 'high'", () => {
const payload = buildThinkingPayload("glm-5.2", { ...defaultSettings, glm: "high" });
assert.deepEqual(payload, { reasoning_effort: "high" });
});

it("glm-5.2 with glm='max' emits reasoning_effort: 'max'", () => {
const payload = buildThinkingPayload("glm-5.2", { ...defaultSettings, glm: "max" });
assert.deepEqual(payload, { reasoning_effort: "max" });
});

it("glm-5.2 with glm='off' emits thinking disabled", () => {
const payload = buildThinkingPayload("glm-5.2", { ...defaultSettings, glm: "off" });
assert.deepEqual(payload, { thinking: { type: "disabled" } });
});

it("glm-5 (toggle-only) with glm='high' sends reasoning_effort (gateway resolves)", () => {
const payload = buildThinkingPayload("glm-5", { ...defaultSettings, glm: "high" });
assert.deepEqual(payload, { reasoning_effort: "high" });
});

it("glm-5 (toggle-only) with glm='off' emits thinking disabled", () => {
const payload = buildThinkingPayload("glm-5", { ...defaultSettings, glm: "off" });
assert.deepEqual(payload, { thinking: { type: "disabled" } });
});
});

describe("buildFamilyThinkingSchema — GLM 5.2 with reasoning_options metadata", () => {
it("exposes off, high, max when reasoning_options has effort values", () => {
const metadata = {
reasoning: true,
reasoningOptions: [{ type: "effort" as const, values: ["high", "max"] }],
contextWindow: 202752,
maxOutputTokens: 32768,
supportsVision: false,
supportsAudio: false,
supportsVideo: false,
supportsPdf: false,
source: "models.dev" as const,
};
const schema = buildFamilyThinkingSchema("glm-5.2", metadata);
assert.ok(schema, "expected schema to be defined");
const reasoningEffort = schema!.properties.reasoningEffort as Record<string, unknown>;
assert.deepEqual(reasoningEffort.enum, ["off", "high", "max"]);
assert.deepEqual(reasoningEffort.enumItemLabels, ["Off", "High", "Max"]);
assert.equal(reasoningEffort.default, "off");
});

it("falls back to off/high/max for GLM models without reasoning_options (no invalid 'on')", () => {
const schema = buildFamilyThinkingSchema("glm-5");
assert.ok(schema, "expected schema to be defined");
const reasoningEffort = schema!.properties.reasoningEffort as Record<string, unknown>;
assert.deepEqual(reasoningEffort.enum, ["off", "high", "max"]);
assert.deepEqual(reasoningEffort.enumItemLabels, ["Off", "High", "Max"]);
});
});

describe("applyRequestThinkingOverride — GLM with effort values (issue #61)", () => {
it("accepts 'high' override for glm-5.2", () => {
const result = applyRequestThinkingOverride("glm-5.2", defaultSettings, {
reasoningEffort: "high",
});
assert.equal(result.glm, "high");
});

it("accepts 'max' override for glm-5.2", () => {
const result = applyRequestThinkingOverride("glm-5.2", defaultSettings, {
reasoningEffort: "max",
});
assert.equal(result.glm, "max");
});

it("accepts 'off' override for glm-5.2", () => {
const result = applyRequestThinkingOverride("glm-5.2", defaultSettings, {
reasoningEffort: "off",
});
assert.equal(result.glm, "off");
});

it("rejects invalid values like 'on' and 'medium' for glm", () => {
const resultOn = applyRequestThinkingOverride("glm-5.2", defaultSettings, {
reasoningEffort: "on",
});
assert.equal(resultOn.glm, "off"); // stays at default
const resultMed = applyRequestThinkingOverride("glm-5.2", defaultSettings, {
reasoningEffort: "medium",
});
assert.equal(resultMed.glm, "off"); // stays at default
});
});
37 changes: 32 additions & 5 deletions src/thinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { ResolvedModelMetadata } from "./metadata";
/** Per-family thinking settings stored in the workspace configuration. */
export interface ThinkingSettings {
deepseek: "off" | "low" | "medium" | "high" | "max";
glm: "on" | "off";
glm: "off" | "high" | "max";
kimi: "on" | "off";
minimax: "off" | "on";
qwen: "auto" | "on" | "off";
Expand Down Expand Up @@ -191,7 +191,27 @@ export function buildFamilyThinkingSchema(
};
}

if (family === "glm" || family === "kimi") {
if (family === "glm") {
return {
properties: {
reasoningEffort: {
type: "string",
title: "Thinking Effort",
enum: ["off", "high", "max"],
enumItemLabels: ["Off", "High", "Max"],
enumDescriptions: [
"Fastest responses",
"Greater reasoning depth",
"Maximum reasoning effort"
],
default: "off",
group: "navigation"
}
}
};
}

if (family === "kimi") {
return {
properties: {
reasoningEffort: {
Expand Down Expand Up @@ -313,10 +333,10 @@ export function applyRequestThinkingOverride(
}
}
if (family === "glm" && typeof thinkingMode === "string") {
if (thinkingMode === "on" || thinkingMode === "off") next.glm = thinkingMode;
if (["off", "high", "max"].includes(thinkingMode)) next.glm = thinkingMode as ThinkingSettings["glm"];
}
if (family === "glm" && typeof reasoningEffort === "string") {
if (reasoningEffort === "on" || reasoningEffort === "off") next.glm = reasoningEffort;
if (["off", "high", "max"].includes(reasoningEffort)) next.glm = reasoningEffort as ThinkingSettings["glm"];
}
if (family === "kimi" && typeof thinkingMode === "string") {
if (thinkingMode === "on" || thinkingMode === "off") next.kimi = thinkingMode;
Expand Down Expand Up @@ -384,7 +404,14 @@ export function buildThinkingPayload(modelId: string, thinking: ThinkingSettings
// The gateway's transform.ts variants() returns {} for GLM — no variants
// are exposed, meaning the gateway doesn't validate or transform GLM
// thinking parameters. We send through as-is to the upstream API.
return { thinking: { type: thinking.glm === "on" ? "enabled" : "disabled" } };
//
// When the value is a concrete effort level (e.g. "high", "max", or future
// "low"/"medium"), send reasoning_effort directly — the gateway or upstream
// API determines support. Only "off" maps to disabled.
if (thinking.glm === "off") {
return { thinking: { type: "disabled" } };
}
return { reasoning_effort: thinking.glm };
}

if (/^kimi-/i.test(modelId)) {
Expand Down
Loading