feat(thinking): 思考等级扩展五档(Max/XHigh)+ 按默认模型 support_efforts 能力感知;删除过时的 max→high 迁移;Segmented 支持逐选项禁用/tooltip — bump to v0.7.23
Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
Release / Attach macOS install script (push) Canceled after 0s

This commit is contained in:
KimiSwitch Dev committed 2026-10-02 13:00:35 +08:00
1 parent 50f59cac79
commit 26cb88ad0e
17 files changed
+453 -53

No files matched your search

+21 -4
View File
@@ -20,7 +20,7 @@ function loopOf(raw: unknown): Record<string, unknown> {
}
// ---------------------------------------------------------------------------
// Reading — legacy off values normalized to "off", "max" → "high"
// Reading — legacy off values normalized to "off"; effort tiers pass through
// ---------------------------------------------------------------------------
describe("getAgentSettings — thinking.keep normalization", () => {
@@ -52,12 +52,29 @@ describe("getAgentSettings — thinking.keep normalization", () => {
});
});
describe("getAgentSettings — effort \"max\" read mapping", () => {
it("normalizes a stored \"max\" to \"high\" on read", () => {
describe("getAgentSettings — thinking.effort read mapping", () => {
it("keeps a stored \"max\" as-is (the tier is valid upstream)", () => {
expect(getAgentSettings({ thinking: { effort: "max" } }).thinking?.effort).toBe(
"high"
"max"
);
});
it("passes an \"xhigh\" tier through untouched", () => {
expect(getAgentSettings({ thinking: { effort: "xhigh" } }).thinking?.effort).toBe(
"xhigh"
);
});
it("keeps the default \"medium\" when the key is absent", () => {
expect(getAgentSettings({}).thinking?.effort).toBe("medium");
});
it("round-trips an arbitrary hand-written tier on save", () => {
const next = setAgentSettings({ thinking: { effort: "ultra" } }, {
thinking: { effort: "ultra" },
});
expect(thinkingOf(next).effort).toBe("ultra");
});
});
// ---------------------------------------------------------------------------
-5
View File
@@ -65,11 +65,6 @@ export function getAgentSettings(rawOther: unknown): AgentSettings {
if (thinking.keep !== undefined && thinking.keep !== "all") {
thinking.keep = "off";
}
// The "max" effort tier was removed upstream (auto-migrates to "high");
// old configs still carrying it are shown as "high" (not rewritten on read).
if (thinking.effort === "max") {
thinking.effort = "high";
}
const sectionPermission = getSection<AgentSettings["permission"]>(
rawOther,
"permission"
+139
View File
@@ -0,0 +1,139 @@
import { describe, expect, it } from "vitest";
import {
THINKING_EFFORTS,
normalizeSupportEfforts,
readSupportEfforts,
resolveThinkingEffortSupport,
} from "./thinking-efforts";
// ---------------------------------------------------------------------------
// support_efforts normalization — an absent / malformed key means "unknown",
// never "no tiers supported"
// ---------------------------------------------------------------------------
describe("normalizeSupportEfforts", () => {
it("keeps a non-empty list of strings", () => {
expect(normalizeSupportEfforts(["low", "high"])).toEqual(["low", "high"]);
});
it("drops blanks and non-strings but keeps the order", () => {
expect(normalizeSupportEfforts(["low", "", 3, null, "max"])).toEqual([
"low",
"max",
]);
});
it("returns null for absent / empty / non-array values", () => {
expect(normalizeSupportEfforts(undefined)).toBeNull();
expect(normalizeSupportEfforts([])).toBeNull();
expect(normalizeSupportEfforts([" "])).toBeNull();
expect(normalizeSupportEfforts("low")).toBeNull();
});
});
describe("readSupportEfforts", () => {
const model = (raw_other: unknown) =>
({ alias: "a", provider: "p", model: "m", max_context_size: 0, display_name: null, raw_other }) as const;
it("reads the key from a model entry's preserved config fields", () => {
expect(readSupportEfforts(model({ support_efforts: ["low", "max"] }))).toEqual([
"low",
"max",
]);
});
it("returns null when the entry or the key is absent", () => {
expect(readSupportEfforts(undefined)).toBeNull();
expect(readSupportEfforts(model(undefined))).toBeNull();
expect(readSupportEfforts(model({ other: 1 }))).toBeNull();
});
});
// ---------------------------------------------------------------------------
// Tier resolution — three layers, in priority order
// ---------------------------------------------------------------------------
describe("resolveThinkingEffortSupport — declared support_efforts wins", () => {
const support = resolveThinkingEffortSupport({
supportEfforts: ["low", "high", "max"],
reasoning: true,
});
it("exposes the declared list and keeps thinking enabled", () => {
expect(support.declared).toEqual(["low", "high", "max"]);
expect(support.thinkingSupported).toBe(true);
});
it("enables exactly the declared tiers", () => {
for (const tier of THINKING_EFFORTS) {
expect(support.levels[tier].enabled).toBe(tier === "low" || tier === "high" || tier === "max");
}
});
it("explains a disabled tier with the supported list", () => {
expect(support.levels.medium).toEqual({
enabled: false,
note: { kind: "tierUnsupported", supported: ["low", "high", "max"] },
});
expect(support.levels.xhigh.enabled).toBe(false);
});
it("does not disable thinking even when models.dev says reasoning: false", () => {
const conflicting = resolveThinkingEffortSupport({
supportEfforts: ["high"],
reasoning: false,
});
expect(conflicting.thinkingSupported).toBe(true);
expect(conflicting.levels.low.enabled).toBe(false);
expect(conflicting.levels.high.enabled).toBe(true);
});
it("matches tiers case-insensitively", () => {
const upper = resolveThinkingEffortSupport({ supportEfforts: ["HIGH"] });
expect(upper.levels.high.enabled).toBe(true);
});
});
describe("resolveThinkingEffortSupport — model without thinking", () => {
const support = resolveThinkingEffortSupport({ reasoning: false });
it("disables the whole thinking area", () => {
expect(support.thinkingSupported).toBe(false);
expect(support.declared).toBeNull();
for (const tier of THINKING_EFFORTS) {
expect(support.levels[tier]).toEqual({
enabled: false,
note: { kind: "thinkingUnsupported" },
});
}
});
});
describe("resolveThinkingEffortSupport — nothing known about the model", () => {
it("offers low/medium/high plain and flags max/xhigh as undeclared", () => {
const support = resolveThinkingEffortSupport({});
expect(support.thinkingSupported).toBe(true);
expect(support.declared).toBeNull();
for (const tier of ["low", "medium", "high"] as const) {
expect(support.levels[tier]).toEqual({ enabled: true });
}
for (const tier of ["max", "xhigh"] as const) {
expect(support.levels[tier]).toEqual({
enabled: true,
note: { kind: "tierUndeclared" },
});
}
});
it("treats an undefined reasoning flag as unknown, not as unsupported", () => {
expect(
resolveThinkingEffortSupport({ reasoning: undefined }).thinkingSupported
).toBe(true);
});
it("ignores a malformed support_efforts value and falls back to unknown", () => {
const support = resolveThinkingEffortSupport({ supportEfforts: "low" });
expect(support.declared).toBeNull();
expect(support.levels.low).toEqual({ enabled: true });
});
});
+127
View File
@@ -0,0 +1,127 @@
/**
* Thinking-effort capability resolution.
*
* kimi-code's `[thinking] effort` is a free-form string (low/medium/high/
* max/xhigh in practice) that the CLI forwards verbatim to OpenAI-compatible
* upstreams as `reasoning_effort`. Which tiers a given model accepts is
* declared per model in config.toml (`[models."<alias>"] support_efforts`);
* models.dev only carries a boolean `reasoning` flag with no tier list. The
* two sources are merged here so the UI can grey out tiers the current default
* model would reject, without ever rejecting a stored value on read.
*/
import type { Model } from "../types";
export const THINKING_EFFORTS = [
"low",
"medium",
"high",
"max",
"xhigh",
] as const;
export type ThinkingEffort = (typeof THINKING_EFFORTS)[number];
/** Why a tier is blocked, or (for undeclared models) merely unverified. */
export type EffortNote =
| { kind: "thinkingUnsupported" }
| { kind: "tierUnsupported"; supported: string[] }
| { kind: "tierUndeclared" };
export interface EffortAvailability {
/** Whether the tier can be picked. */
enabled: boolean;
/** Advisory message — set for blocked tiers and for unverified ones. */
note?: EffortNote;
}
export interface ThinkingEffortSupport {
/** False only when the model is known not to support thinking at all. */
thinkingSupported: boolean;
/** Declared tiers, or null when nothing is known about the model. */
declared: string[] | null;
levels: Record<ThinkingEffort, EffortAvailability>;
}
/** Tiers offered unverified when a model declares nothing at all. */
const ALWAYS_OFFERED = new Set<string>(["low", "medium", "high"]);
/**
* Normalize a `support_efforts` value into a comparable list, or null when the
* key is absent / malformed (treating it as "unknown", not "none supported").
*/
export function normalizeSupportEfforts(value: unknown): string[] | null {
if (!Array.isArray(value)) return null;
const tiers = value
.filter((v): v is string => typeof v === "string")
.map((v) => v.trim())
.filter((v) => v.length > 0);
return tiers.length > 0 ? tiers : null;
}
/** Read `support_efforts` from a model entry's preserved config.toml fields. */
export function readSupportEfforts(model: Model | undefined): string[] | null {
if (!model) return null;
const raw = model.raw_other;
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
return normalizeSupportEfforts(
(raw as Record<string, unknown>).support_efforts
);
}
function buildLevels(
resolve: (effort: ThinkingEffort) => EffortAvailability
): Record<ThinkingEffort, EffortAvailability> {
const levels = {} as Record<ThinkingEffort, EffortAvailability>;
for (const effort of THINKING_EFFORTS) levels[effort] = resolve(effort);
return levels;
}
/**
* Resolve per-tier availability from the two capability sources, in priority
* order:
* 1. `supportEfforts` declared → tiers follow the list exactly.
* 2. models.dev `reasoning === false` → the whole thinking area is off.
* 3. Nothing known → low/medium/high are offered plain, max/xhigh stay
* selectable but carry a "not declared" note (upstream may reject them).
*/
export function resolveThinkingEffortSupport(input: {
supportEfforts?: unknown;
reasoning?: boolean;
}): ThinkingEffortSupport {
const declared = normalizeSupportEfforts(input.supportEfforts);
if (declared) {
const supported = new Set(declared.map((t) => t.toLowerCase()));
return {
thinkingSupported: true,
declared,
levels: buildLevels((effort) =>
supported.has(effort)
? { enabled: true }
: {
enabled: false,
note: { kind: "tierUnsupported", supported: declared },
}
),
};
}
if (input.reasoning === false) {
return {
thinkingSupported: false,
declared: null,
levels: buildLevels(() => ({
enabled: false,
note: { kind: "thinkingUnsupported" },
})),
};
}
return {
thinkingSupported: true,
declared: null,
levels: buildLevels((effort) =>
ALWAYS_OFFERED.has(effort)
? { enabled: true }
: { enabled: true, note: { kind: "tierUndeclared" } }
),
};
}