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
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:
1 parent
50f59cac79
commit
26cb88ad0e
17 files changed
+453
-53
No files matched your search
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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" } }
|
||||
),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user