mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:10:44 +00:00
fix: centralize provider thinking profiles
This commit is contained in:
@@ -139,8 +139,7 @@ function resolveThinkingTargetModel(state: AppViewState): {
|
||||
}
|
||||
|
||||
function buildThinkingOptions(
|
||||
provider: string | null,
|
||||
model: string | null,
|
||||
labels: readonly string[],
|
||||
currentOverride: string,
|
||||
): ChatThinkingSelectOption[] {
|
||||
const seen = new Set<string>();
|
||||
@@ -160,9 +159,9 @@ function buildThinkingOptions(
|
||||
);
|
||||
};
|
||||
|
||||
for (const label of listThinkingLevelLabels(provider, model)) {
|
||||
for (const label of labels) {
|
||||
const normalized = normalizeThinkLevel(label) ?? normalizeLowercaseStringOrEmpty(label);
|
||||
addOption(normalized);
|
||||
addOption(normalized, label);
|
||||
}
|
||||
if (currentOverride) {
|
||||
addOption(currentOverride);
|
||||
@@ -178,18 +177,22 @@ function resolveChatThinkingSelectState(state: AppViewState): ChatThinkingSelect
|
||||
? (normalizeThinkLevel(persisted) ?? persisted.trim())
|
||||
: "";
|
||||
const { provider, model } = resolveThinkingTargetModel(state);
|
||||
const labels =
|
||||
activeRow?.thinkingOptions ??
|
||||
(provider && model ? listThinkingLevelLabels(provider, model) : listThinkingLevelLabels());
|
||||
const defaultLevel =
|
||||
provider && model
|
||||
activeRow?.thinkingDefault ??
|
||||
(provider && model
|
||||
? resolveThinkingDefaultForModel({
|
||||
provider,
|
||||
model,
|
||||
catalog: state.chatModelCatalog ?? [],
|
||||
})
|
||||
: "off";
|
||||
: "off");
|
||||
return {
|
||||
currentOverride,
|
||||
defaultLabel: `Default (${defaultLevel})`,
|
||||
options: buildThinkingOptions(provider, model, currentOverride),
|
||||
options: buildThinkingOptions(labels, currentOverride),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -527,7 +527,21 @@ describe("executeSlashCommand directives", () => {
|
||||
});
|
||||
|
||||
it("accepts minimal and xhigh thinking levels", async () => {
|
||||
const request = vi.fn().mockResolvedValueOnce({ ok: true }).mockResolvedValueOnce({ ok: true });
|
||||
const request = vi.fn(async (method: string, payload?: unknown) => {
|
||||
if (method === "sessions.list") {
|
||||
return {
|
||||
sessions: [
|
||||
row("agent:main:main", {
|
||||
thinkingOptions: ["off", "minimal", "low", "medium", "high", "xhigh"],
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
if (method === "sessions.patch") {
|
||||
return { ok: true, ...((payload ?? {}) as object) };
|
||||
}
|
||||
throw new Error(`unexpected method: ${method}`);
|
||||
});
|
||||
|
||||
const minimal = await executeSlashCommand(
|
||||
{ request } as unknown as GatewayBrowserClient,
|
||||
@@ -544,11 +558,13 @@ describe("executeSlashCommand directives", () => {
|
||||
|
||||
expect(minimal.content).toBe("Thinking level set to **minimal**.");
|
||||
expect(xhigh.content).toBe("Thinking level set to **xhigh**.");
|
||||
expect(request).toHaveBeenNthCalledWith(1, "sessions.patch", {
|
||||
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {});
|
||||
expect(request).toHaveBeenNthCalledWith(2, "sessions.patch", {
|
||||
key: "agent:main:main",
|
||||
thinkingLevel: "minimal",
|
||||
});
|
||||
expect(request).toHaveBeenNthCalledWith(2, "sessions.patch", {
|
||||
expect(request).toHaveBeenNthCalledWith(3, "sessions.list", {});
|
||||
expect(request).toHaveBeenNthCalledWith(4, "sessions.patch", {
|
||||
key: "agent:main:main",
|
||||
thinkingLevel: "xhigh",
|
||||
});
|
||||
|
||||
@@ -258,7 +258,7 @@ async function executeThink(
|
||||
return {
|
||||
content: formatDirectiveOptions(
|
||||
`Current thinking level: ${resolveCurrentThinkingLevel(session, models)}.`,
|
||||
formatThinkingLevels(session?.modelProvider, session?.model),
|
||||
formatThinkingOptionsForSession(session),
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
@@ -271,7 +271,7 @@ async function executeThink(
|
||||
try {
|
||||
const session = await loadCurrentSession(client, sessionKey);
|
||||
return {
|
||||
content: `Unrecognized thinking level "${rawLevel}". Valid levels: ${formatThinkingLevels(session?.modelProvider, session?.model)}.`,
|
||||
content: `Unrecognized thinking level "${rawLevel}". Valid levels: ${formatThinkingOptionsForSession(session)}.`,
|
||||
};
|
||||
} catch (err) {
|
||||
return { content: `Failed to validate thinking level: ${String(err)}` };
|
||||
@@ -279,6 +279,12 @@ async function executeThink(
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await loadCurrentSession(client, sessionKey);
|
||||
if (!isThinkingLevelOptionForSession(session, level)) {
|
||||
return {
|
||||
content: `Unsupported thinking level "${rawLevel}" for this model. Valid levels: ${formatThinkingOptionsForSession(session)}.`,
|
||||
};
|
||||
}
|
||||
await client.request("sessions.patch", { key: sessionKey, thinkingLevel: level });
|
||||
return {
|
||||
content: `Thinking level set to **${level}**.`,
|
||||
@@ -594,6 +600,26 @@ function formatDirectiveOptions(text: string, options: string): string {
|
||||
return `${text}\nOptions: ${options}.`;
|
||||
}
|
||||
|
||||
function formatThinkingOptionsForSession(
|
||||
session: GatewaySessionRow | undefined,
|
||||
separator = ", ",
|
||||
): string {
|
||||
if (session?.thinkingOptions?.length) {
|
||||
return session.thinkingOptions.join(separator);
|
||||
}
|
||||
return formatThinkingLevels(session?.modelProvider, session?.model);
|
||||
}
|
||||
|
||||
function isThinkingLevelOptionForSession(
|
||||
session: GatewaySessionRow | undefined,
|
||||
level: string,
|
||||
): boolean {
|
||||
const labels = session?.thinkingOptions?.length
|
||||
? session.thinkingOptions
|
||||
: formatThinkingOptionsForSession(session).split(/\s*,\s*/);
|
||||
return labels.some((label) => normalizeThinkLevel(label) === level);
|
||||
}
|
||||
|
||||
async function loadCurrentSession(
|
||||
client: GatewayBrowserClient,
|
||||
sessionKey: string,
|
||||
@@ -651,7 +677,13 @@ function resolveCurrentThinkingLevel(
|
||||
): string {
|
||||
const persisted = normalizeThinkLevel(session?.thinkingLevel);
|
||||
if (persisted) {
|
||||
return persisted;
|
||||
return (
|
||||
session?.thinkingOptions?.find((label) => normalizeThinkLevel(label) === persisted) ??
|
||||
persisted
|
||||
);
|
||||
}
|
||||
if (session?.thinkingDefault) {
|
||||
return session.thinkingDefault;
|
||||
}
|
||||
if (!session?.modelProvider || !session.model) {
|
||||
return "off";
|
||||
|
||||
@@ -7,12 +7,6 @@ export type ThinkingCatalogEntry = {
|
||||
};
|
||||
|
||||
const BASE_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"] as const;
|
||||
const BINARY_THINKING_LEVELS = ["off", "on"] as const;
|
||||
const ANTHROPIC_CLAUDE_46_MODEL_RE = /^claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i;
|
||||
const ANTHROPIC_OPUS_47_MODEL_RE = /^claude-opus-4(?:\.|-)7(?:$|[-.])/i;
|
||||
const AMAZON_BEDROCK_CLAUDE_46_MODEL_RE = /claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i;
|
||||
const OPENAI_XHIGH_MODEL_RE =
|
||||
/^(?:gpt-5\.[2-9](?:\.\d+)?|gpt-5\.[2-9](?:\.\d+)?-pro|gpt-5\.\d+-codex|gpt-5\.\d+-codex-spark|gpt-5\.1-codex|gpt-5\.2-codex)(?:$|-)/i;
|
||||
|
||||
export function normalizeThinkingProviderId(provider?: string | null): string {
|
||||
if (!provider) {
|
||||
@@ -29,7 +23,8 @@ export function normalizeThinkingProviderId(provider?: string | null): string {
|
||||
}
|
||||
|
||||
export function isBinaryThinkingProvider(provider?: string | null): boolean {
|
||||
return normalizeThinkingProviderId(provider) === "zai";
|
||||
void provider;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function normalizeThinkLevel(raw?: string | null): string | undefined {
|
||||
@@ -71,49 +66,13 @@ export function normalizeThinkLevel(raw?: string | null): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function supportsAdaptiveThinking(provider?: string | null, model?: string | null): boolean {
|
||||
const normalizedProvider = normalizeThinkingProviderId(provider);
|
||||
const modelId = model?.trim() ?? "";
|
||||
if (normalizedProvider === "anthropic") {
|
||||
return ANTHROPIC_CLAUDE_46_MODEL_RE.test(modelId) || ANTHROPIC_OPUS_47_MODEL_RE.test(modelId);
|
||||
}
|
||||
if (normalizedProvider === "amazon-bedrock") {
|
||||
return AMAZON_BEDROCK_CLAUDE_46_MODEL_RE.test(modelId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function supportsXHighThinking(provider?: string | null, model?: string | null): boolean {
|
||||
const normalizedProvider = normalizeThinkingProviderId(provider);
|
||||
const modelId = model?.trim() ?? "";
|
||||
if (normalizedProvider === "anthropic") {
|
||||
return ANTHROPIC_OPUS_47_MODEL_RE.test(modelId);
|
||||
}
|
||||
if (["openai", "openai-codex", "github-copilot", "codex"].includes(normalizedProvider)) {
|
||||
return OPENAI_XHIGH_MODEL_RE.test(modelId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function supportsMaxThinking(provider?: string | null, model?: string | null): boolean {
|
||||
return normalizeThinkingProviderId(provider) === "anthropic"
|
||||
? ANTHROPIC_OPUS_47_MODEL_RE.test(model?.trim() ?? "")
|
||||
: false;
|
||||
}
|
||||
|
||||
export function listThinkingLevelLabels(
|
||||
provider?: string | null,
|
||||
model?: string | null,
|
||||
): readonly string[] {
|
||||
if (isBinaryThinkingProvider(provider)) {
|
||||
return BINARY_THINKING_LEVELS;
|
||||
}
|
||||
return [
|
||||
...BASE_THINKING_LEVELS,
|
||||
...(supportsXHighThinking(provider, model) ? ["xhigh"] : []),
|
||||
...(supportsAdaptiveThinking(provider, model) ? ["adaptive"] : []),
|
||||
...(supportsMaxThinking(provider, model) ? ["max"] : []),
|
||||
];
|
||||
void provider;
|
||||
void model;
|
||||
return BASE_THINKING_LEVELS;
|
||||
}
|
||||
|
||||
export function formatThinkingLevels(provider?: string | null, model?: string | null): string {
|
||||
@@ -125,14 +84,6 @@ export function resolveThinkingDefaultForModel(params: {
|
||||
model: string;
|
||||
catalog?: ThinkingCatalogEntry[];
|
||||
}): string {
|
||||
const normalizedProvider = normalizeThinkingProviderId(params.provider);
|
||||
const modelId = params.model.trim();
|
||||
if (normalizedProvider === "anthropic" && ANTHROPIC_CLAUDE_46_MODEL_RE.test(modelId)) {
|
||||
return "adaptive";
|
||||
}
|
||||
if (normalizedProvider === "amazon-bedrock" && AMAZON_BEDROCK_CLAUDE_46_MODEL_RE.test(modelId)) {
|
||||
return "adaptive";
|
||||
}
|
||||
const candidate = params.catalog?.find(
|
||||
(entry) => entry.provider === params.provider && entry.id === params.model,
|
||||
);
|
||||
|
||||
@@ -411,6 +411,8 @@ export type GatewaySessionRow = {
|
||||
systemSent?: boolean;
|
||||
abortedLastRun?: boolean;
|
||||
thinkingLevel?: string;
|
||||
thinkingOptions?: string[];
|
||||
thinkingDefault?: string;
|
||||
fastMode?: boolean;
|
||||
verboseLevel?: string;
|
||||
reasoningLevel?: string;
|
||||
|
||||
@@ -63,8 +63,7 @@ export type SessionsProps = {
|
||||
onRestoreCheckpoint: (sessionKey: string, checkpointId: string) => void | Promise<void>;
|
||||
};
|
||||
|
||||
const THINK_LEVELS = ["", "off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
||||
const BINARY_THINK_LEVELS = ["", "off", "on"] as const;
|
||||
const DEFAULT_THINK_LEVELS = ["off", "minimal", "low", "medium", "high"] as const;
|
||||
const VERBOSE_LEVELS = [
|
||||
{ value: "", label: "inherit" },
|
||||
{ value: "off", label: "off (explicit)" },
|
||||
@@ -79,23 +78,13 @@ const FAST_LEVELS = [
|
||||
const REASONING_LEVELS = ["", "off", "on", "stream"] as const;
|
||||
const PAGE_SIZES = [10, 25, 50, 100] as const;
|
||||
|
||||
function normalizeProviderId(provider?: string | null): string {
|
||||
if (!provider) {
|
||||
return "";
|
||||
}
|
||||
const normalized = normalizeLowercaseStringOrEmpty(provider);
|
||||
if (normalized === "z.ai" || normalized === "z-ai") {
|
||||
return "zai";
|
||||
}
|
||||
return normalized;
|
||||
function resolveThinkLevelOptions(row: GatewaySessionRow): readonly string[] {
|
||||
const options = row.thinkingOptions?.length ? row.thinkingOptions : DEFAULT_THINK_LEVELS;
|
||||
return ["", ...options];
|
||||
}
|
||||
|
||||
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 isBinaryThinkingRow(row: GatewaySessionRow): boolean {
|
||||
return row.thinkingOptions?.includes("on") === true;
|
||||
}
|
||||
|
||||
function withCurrentOption(options: readonly string[], current: string): string[] {
|
||||
@@ -453,9 +442,9 @@ export function renderSessions(props: SessionsProps) {
|
||||
function renderRows(row: GatewaySessionRow, props: SessionsProps) {
|
||||
const updated = row.updatedAt ? formatRelativeTimestamp(row.updatedAt) : t("common.na");
|
||||
const rawThinking = row.thinkingLevel ?? "";
|
||||
const isBinaryThinking = isBinaryThinkingProvider(row.modelProvider);
|
||||
const isBinaryThinking = isBinaryThinkingRow(row);
|
||||
const thinking = resolveThinkLevelDisplay(rawThinking, isBinaryThinking);
|
||||
const thinkLevels = withCurrentOption(resolveThinkLevelOptions(row.modelProvider), thinking);
|
||||
const thinkLevels = withCurrentOption(resolveThinkLevelOptions(row), thinking);
|
||||
const fastMode = row.fastMode === true ? "on" : row.fastMode === false ? "off" : "";
|
||||
const fastLevels = withCurrentLabeledOption(FAST_LEVELS, fastMode);
|
||||
const verbose = row.verboseLevel ?? "";
|
||||
|
||||
Reference in New Issue
Block a user