fix: pass claude cli thinking effort

This commit is contained in:
stainlu
2026-05-05 00:44:58 +08:00
parent 1df2ac442a
commit be17754009
15 changed files with 259 additions and 6 deletions

View File

@@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- fix(qqbot): keep private commands off framework surface [AI]. (#77212) Thanks @pgondhi987.
- Claude CLI: honor non-off `/think` levels by passing Claude Code's session-scoped `--effort` flag through the CLI backend seam, so chat bridges no longer show an inert thinking control. Fixes #77303. Thanks @Petr1t.
- Memory/wiki: preserve representation from both corpora in `corpus=all` searches while backfilling unused result capacity, so memory hits are not starved by numerically higher wiki integer scores. Fixes #77337. Thanks @hclsys.
- Telegram: clean up tool-only draft previews after assistant message boundaries so transient `Surfacing...` tool-status bubbles do not linger when no matching final preview arrives. Thanks @BunsDev.
- Cron: surface failed isolated-run diagnostics in `cron show`, status, and run history when requested tools are unavailable, so blocked cron runs report the actual tool-policy failure instead of a misleading green result. Fixes #75763. Thanks @RyanSandoval.

View File

@@ -1,2 +1,2 @@
f8495c07213012748f099b12ddb02847ffd4eaa1b46f2ae9dfa574fa0ef3299a plugin-sdk-api-baseline.json
815ac868dda35d0af88b9c522233d6065c3eeb70775e19c111162b80390733fa plugin-sdk-api-baseline.jsonl
a7116e6c0cae4c7b9ee7cd6dc48f2978812f4b5be647f3e36eee91ec9a81d85e plugin-sdk-api-baseline.json
2b6c9883d701379761724e21946d417399c1247e6a244d6b00c4a982c8ef5968 plugin-sdk-api-baseline.jsonl

View File

@@ -178,6 +178,12 @@ that agent. To force a different Claude mode, set explicit raw backend args
such as `--permission-mode default` or `--permission-mode acceptEdits` under
`agents.defaults.cliBackends.claude-cli.args` and matching `resumeArgs`.
The bundled Anthropic `claude-cli` backend also maps OpenClaw `/think` levels
to Claude Code's native `--effort` flag for non-off levels. `minimal` and
`low` map to `low`, `adaptive` and `medium` map to `medium`, and `high`,
`xhigh`, and `max` map directly. Other CLI backends need their owning plugin to
declare an equivalent argv mapper before `/think` can affect the spawned CLI.
Before OpenClaw can use the bundled `claude-cli` backend, Claude Code itself
must already be logged in on the same host:

View File

@@ -257,6 +257,9 @@ AI CLI backend such as `codex-cli`.
plugin default before running the CLI.
- Use `normalizeConfig` when a backend needs compatibility rewrites after merge
(for example normalizing old flag shapes).
- Use `resolveExecutionArgs` for request-scoped argv rewrites that belong to
the CLI dialect, such as mapping OpenClaw thinking levels to a native effort
flag.
### Exclusive slots

View File

@@ -54,6 +54,7 @@ title: "Thinking levels"
## Application by agent
- **Embedded Pi**: the resolved level is passed to the in-process Pi agent runtime.
- **Claude CLI backend**: non-off levels are passed to Claude Code as `--effort` when using `claude-cli`; see [CLI backends](/gateway/cli-backends).
## Fast mode (/fast)

View File

@@ -10,6 +10,7 @@ import {
CLAUDE_CLI_MODEL_ALIASES,
CLAUDE_CLI_SESSION_ID_FIELDS,
normalizeClaudeBackendConfig,
resolveClaudeCliExecutionArgs,
} from "./cli-shared.js";
export function buildAnthropicCliBackend(): CliBackendPlugin {
@@ -76,5 +77,6 @@ export function buildAnthropicCliBackend(): CliBackendPlugin {
serialize: true,
},
normalizeConfig: normalizeClaudeBackendConfig,
resolveExecutionArgs: resolveClaudeCliExecutionArgs,
};
}

View File

@@ -6,6 +6,7 @@ import {
normalizeClaudePermissionArgs,
normalizeClaudeSettingSourcesArgs,
resolveClaudePermissionMode,
resolveClaudeCliExecutionArgs,
} from "./cli-shared.js";
describe("normalizeClaudePermissionArgs", () => {
@@ -75,6 +76,67 @@ describe("normalizeClaudeSettingSourcesArgs", () => {
});
});
describe("resolveClaudeCliExecutionArgs", () => {
it("omits effort args when thinking is off", () => {
expect(
resolveClaudeCliExecutionArgs({
workspaceDir: "/tmp",
provider: "claude-cli",
modelId: "claude-sonnet-4-6",
thinkingLevel: "off",
useResume: false,
baseArgs: ["-p", "--output-format", "stream-json"],
}),
).toEqual(["-p", "--output-format", "stream-json"]);
});
it("maps OpenClaw thinking levels to Claude effort args", () => {
expect(
resolveClaudeCliExecutionArgs({
workspaceDir: "/tmp",
provider: "claude-cli",
modelId: "claude-opus-4-7",
thinkingLevel: "minimal",
useResume: false,
baseArgs: ["-p"],
}),
).toEqual(["-p", "--effort", "low"]);
expect(
resolveClaudeCliExecutionArgs({
workspaceDir: "/tmp",
provider: "claude-cli",
modelId: "claude-opus-4-7",
thinkingLevel: "adaptive",
useResume: false,
baseArgs: ["-p"],
}),
).toEqual(["-p", "--effort", "medium"]);
expect(
resolveClaudeCliExecutionArgs({
workspaceDir: "/tmp",
provider: "claude-cli",
modelId: "claude-opus-4-7",
thinkingLevel: "xhigh",
useResume: true,
baseArgs: ["-p", "--resume", "{sessionId}"],
}),
).toEqual(["-p", "--resume", "{sessionId}", "--effort", "xhigh"]);
});
it("replaces static effort args when a session thinking level is active", () => {
expect(
resolveClaudeCliExecutionArgs({
workspaceDir: "/tmp",
provider: "claude-cli",
modelId: "claude-opus-4-7",
thinkingLevel: "max",
useResume: false,
baseArgs: ["-p", "--effort", "low", "--effort=high"],
}),
).toEqual(["-p", "--effort", "max"]);
});
});
describe("normalizeClaudeBackendConfig", () => {
it("normalizes both args and resumeArgs for custom overrides", () => {
const normalized = normalizeClaudeBackendConfig({
@@ -196,6 +258,7 @@ describe("normalizeClaudeBackendConfig", () => {
expect(normalized?.resumeArgs).toContain("--permission-mode");
expect(normalized?.resumeArgs).toContain("bypassPermissions");
expect(normalized?.liveSession).toBe("claude-stdio");
expect(backend.resolveExecutionArgs).toBe(resolveClaudeCliExecutionArgs);
});
it("leaves claude cli subscription-managed, restricts setting sources, and clears inherited env overrides", () => {

View File

@@ -1,6 +1,7 @@
import type {
CliBackendConfig,
CliBackendNormalizeConfigContext,
CliBackendResolveExecutionArgsContext,
} from "openclaw/plugin-sdk/cli-backend";
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
import { CLAUDE_CLI_BACKEND_ID } from "./cli-constants.js";
@@ -60,9 +61,12 @@ export const CLAUDE_CLI_CLEAR_ENV = [
const CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG = "--dangerously-skip-permissions";
const CLAUDE_PERMISSION_MODE_ARG = "--permission-mode";
const CLAUDE_SETTING_SOURCES_ARG = "--setting-sources";
const CLAUDE_EFFORT_ARG = "--effort";
const CLAUDE_SAFE_SETTING_SOURCES = "user";
const CLAUDE_BYPASS_PERMISSION_MODE = "bypassPermissions";
type ClaudeCliEffort = "low" | "medium" | "high" | "xhigh" | "max";
export function isClaudeCliProvider(providerId: string): boolean {
return normalizeOptionalLowercaseString(providerId) === CLAUDE_CLI_BACKEND_ID;
}
@@ -168,6 +172,60 @@ export function normalizeClaudeSettingSourcesArgs(args?: string[]): string[] | u
return normalized;
}
export function mapClaudeCliThinkingLevelToEffort(
thinkingLevel?: string | null,
): ClaudeCliEffort | undefined {
switch (normalizeOptionalLowercaseString(thinkingLevel)) {
case "minimal":
case "low":
return "low";
case "adaptive":
case "medium":
return "medium";
case "high":
return "high";
case "xhigh":
return "xhigh";
case "max":
return "max";
default:
return undefined;
}
}
function stripClaudeEffortArgs(args: readonly string[]): string[] {
const normalized: string[] = [];
for (let i = 0; i < args.length; i += 1) {
const arg = args[i] ?? "";
if (arg === CLAUDE_EFFORT_ARG) {
const maybeValue = args[i + 1];
if (
typeof maybeValue === "string" &&
maybeValue.trim().length > 0 &&
!maybeValue.startsWith("-")
) {
i += 1;
}
continue;
}
if (arg.startsWith(`${CLAUDE_EFFORT_ARG}=`)) {
continue;
}
normalized.push(arg);
}
return normalized;
}
export function resolveClaudeCliExecutionArgs(
context: CliBackendResolveExecutionArgsContext,
): string[] {
const effort = mapClaudeCliThinkingLevelToEffort(context.thinkingLevel);
if (!effort) {
return [...context.baseArgs];
}
return [...stripClaudeEffortArgs(context.baseArgs), CLAUDE_EFFORT_ARG, effort];
}
export function normalizeClaudeBackendConfig(
config: CliBackendConfig,
context?: CliBackendNormalizeConfigContext,

View File

@@ -4,6 +4,7 @@ import type { CliBackendConfig } from "../config/types.js";
import type {
CliBackendAuthEpochMode,
CliBackendNormalizeConfigContext,
CliBackendResolveExecutionArgs,
CliBundleMcpMode,
} from "../plugins/types.js";
import {
@@ -31,6 +32,7 @@ function createBackendEntry(params: {
defaultAuthProfileId?: string;
authEpochMode?: CliBackendAuthEpochMode;
prepareExecution?: () => Promise<null>;
resolveExecutionArgs?: CliBackendResolveExecutionArgs;
normalizeConfig?: (
config: CliBackendConfig,
context?: CliBackendNormalizeConfigContext,
@@ -47,6 +49,7 @@ function createBackendEntry(params: {
...(params.defaultAuthProfileId ? { defaultAuthProfileId: params.defaultAuthProfileId } : {}),
...(params.authEpochMode ? { authEpochMode: params.authEpochMode } : {}),
...(params.prepareExecution ? { prepareExecution: params.prepareExecution } : {}),
...(params.resolveExecutionArgs ? { resolveExecutionArgs: params.resolveExecutionArgs } : {}),
...(params.normalizeConfig ? { normalizeConfig: params.normalizeConfig } : {}),
liveTest: {
defaultModelRef:
@@ -968,6 +971,29 @@ describe("resolveCliBackendConfig google-gemini-cli defaults", () => {
expect(resolved?.config.systemPromptWhen).toBe("first");
expect(resolved?.config.imagePathScope).toBe("workspace");
});
it("preserves backend-owned per-run arg resolvers", () => {
const resolveExecutionArgs: CliBackendResolveExecutionArgs = ({ baseArgs }) => [
...baseArgs,
"--effort",
"high",
];
runtimeBackendEntries = [
createRuntimeBackendEntry({
pluginId: "anthropic",
id: "claude-cli",
config: {
command: "claude",
args: ["-p"],
},
resolveExecutionArgs,
}),
];
const resolved = resolveCliBackendConfig("claude-cli");
expect(resolved?.resolveExecutionArgs).toBe(resolveExecutionArgs);
});
});
describe("resolveCliBackendConfig alias precedence", () => {

View File

@@ -38,6 +38,7 @@ export type ResolvedCliBackend = {
defaultAuthProfileId?: string;
authEpochMode?: CliBackendAuthEpochMode;
prepareExecution?: CliBackendPlugin["prepareExecution"];
resolveExecutionArgs?: CliBackendPlugin["resolveExecutionArgs"];
nativeToolMode?: CliBackendNativeToolMode;
};
@@ -62,6 +63,7 @@ type FallbackCliBackendPolicy = {
defaultAuthProfileId?: string;
authEpochMode?: CliBackendAuthEpochMode;
prepareExecution?: CliBackendPlugin["prepareExecution"];
resolveExecutionArgs?: CliBackendPlugin["resolveExecutionArgs"];
nativeToolMode?: CliBackendNativeToolMode;
};
@@ -99,6 +101,7 @@ function resolveSetupCliBackendPolicy(provider: string): FallbackCliBackendPolic
defaultAuthProfileId: entry.backend.defaultAuthProfileId,
authEpochMode: entry.backend.authEpochMode,
prepareExecution: entry.backend.prepareExecution,
resolveExecutionArgs: entry.backend.resolveExecutionArgs,
nativeToolMode: entry.backend.nativeToolMode,
};
}
@@ -237,6 +240,7 @@ export function resolveCliBackendConfig(
defaultAuthProfileId: registered.defaultAuthProfileId,
authEpochMode: registered.authEpochMode,
prepareExecution: registered.prepareExecution,
resolveExecutionArgs: registered.resolveExecutionArgs,
nativeToolMode: registered.nativeToolMode,
};
}
@@ -266,6 +270,7 @@ export function resolveCliBackendConfig(
defaultAuthProfileId: fallbackPolicy.defaultAuthProfileId,
authEpochMode: fallbackPolicy.authEpochMode,
prepareExecution: fallbackPolicy.prepareExecution,
resolveExecutionArgs: fallbackPolicy.resolveExecutionArgs,
nativeToolMode: fallbackPolicy.nativeToolMode,
};
}
@@ -292,6 +297,7 @@ export function resolveCliBackendConfig(
defaultAuthProfileId: fallbackPolicy?.defaultAuthProfileId,
authEpochMode: fallbackPolicy?.authEpochMode,
prepareExecution: fallbackPolicy?.prepareExecution,
resolveExecutionArgs: fallbackPolicy?.resolveExecutionArgs,
nativeToolMode: fallbackPolicy?.nativeToolMode,
};
}

View File

@@ -59,9 +59,11 @@ function buildPreparedCliRunContext(params: {
sessionId?: string;
sessionKey?: string;
backend?: Partial<PreparedCliRunContext["preparedBackend"]["backend"]>;
resolveExecutionArgs?: PreparedCliRunContext["backendResolved"]["resolveExecutionArgs"];
config?: PreparedCliRunContext["params"]["config"];
mcpConfigHash?: string;
skillsSnapshot?: PreparedCliRunContext["params"]["skillsSnapshot"];
thinkLevel?: PreparedCliRunContext["params"]["thinkLevel"];
workspaceDir?: string;
}): PreparedCliRunContext {
const workspaceDir = params.workspaceDir ?? "/tmp";
@@ -103,6 +105,7 @@ function buildPreparedCliRunContext(params: {
prompt: params.prompt ?? "hi",
provider: params.provider,
model: params.model,
thinkLevel: params.thinkLevel,
timeoutMs: 1_000,
runId: params.runId,
skillsSnapshot: params.skillsSnapshot,
@@ -114,6 +117,7 @@ function buildPreparedCliRunContext(params: {
config: backend,
bundleMcp: params.provider === "claude-cli",
pluginId: params.provider === "claude-cli" ? "anthropic" : "openai",
resolveExecutionArgs: params.resolveExecutionArgs,
},
preparedBackend: {
backend,
@@ -329,6 +333,35 @@ describe("runCliAgent spawn path", () => {
expect(input.argv).not.toContain("hi");
});
it("applies backend-owned per-run args before spawning", async () => {
mockSuccessfulCliRun();
const resolveExecutionArgs = vi.fn(({ baseArgs }) => [...baseArgs, "--effort", "high"]);
await executePreparedCliRun(
buildPreparedCliRunContext({
provider: "claude-cli",
model: "sonnet",
runId: "run-claude-thinking-args",
thinkLevel: "high",
resolveExecutionArgs,
}),
);
expect(resolveExecutionArgs).toHaveBeenCalledWith(
expect.objectContaining({
provider: "claude-cli",
modelId: "sonnet",
thinkingLevel: "high",
useResume: false,
baseArgs: ["-p", "--output-format", "stream-json"],
}),
);
const input = supervisorSpawnMock.mock.calls[0]?.[0] as { argv?: string[] };
const effortArgIndex = input.argv?.indexOf("--effort") ?? -1;
expect(effortArgIndex).toBeGreaterThanOrEqual(0);
expect(input.argv?.[effortArgIndex + 1]).toBe("high");
});
it("passes OpenClaw skills to Claude as a session plugin", async () => {
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-skills-"));
const skillDir = path.join(workspaceDir, "skills", "weather");

View File

@@ -279,12 +279,24 @@ export async function executePreparedCliRun(
skillsSnapshot: params.skillsSnapshot,
});
let claudeSkillsPluginCleanupOwned = false;
const baseArgsWithSkills =
claudeSkillsPlugin.args.length > 0
? [...resolvedArgs, ...claudeSkillsPlugin.args]
: resolvedArgs;
const executionBaseArgs =
context.backendResolved.resolveExecutionArgs?.({
config: params.config,
workspaceDir: context.workspaceDir,
provider: params.provider,
modelId: context.modelId,
authProfileId: context.effectiveAuthProfileId,
thinkingLevel: params.thinkLevel,
useResume,
baseArgs: baseArgsWithSkills,
}) ?? baseArgsWithSkills;
const args = buildCliArgs({
backend,
baseArgs:
claudeSkillsPlugin.args.length > 0
? [...resolvedArgs, ...claudeSkillsPlugin.args]
: resolvedArgs,
baseArgs: Array.from(executionBaseArgs),
modelId: context.normalizedModel,
sessionId: resolvedSessionId,
systemPrompt: systemPromptArg,

View File

@@ -6,6 +6,9 @@ export type {
CliBackendPlugin,
CliBackendPreparedExecution,
CliBackendPrepareExecutionContext,
CliBackendResolveExecutionArgs,
CliBackendResolveExecutionArgsContext,
CliBackendThinkingLevel,
} from "../plugins/types.js";
export {
CLI_FRESH_WATCHDOG_DEFAULTS,

View File

@@ -33,6 +33,31 @@ export type CliBackendPreparedExecution = {
cleanup?: () => Promise<void>;
};
export type CliBackendThinkingLevel =
| "off"
| "minimal"
| "low"
| "medium"
| "high"
| "xhigh"
| "adaptive"
| "max";
export type CliBackendResolveExecutionArgsContext = {
config?: OpenClawConfig;
workspaceDir: string;
provider: string;
modelId: string;
authProfileId?: string;
thinkingLevel?: CliBackendThinkingLevel;
useResume: boolean;
baseArgs: readonly string[];
};
export type CliBackendResolveExecutionArgs = (
ctx: CliBackendResolveExecutionArgsContext,
) => readonly string[] | null | undefined;
export type CliBackendAuthEpochMode = "combined" | "profile-only";
export type CliBackendNativeToolMode = "none" | "always-on";
@@ -141,6 +166,14 @@ export type CliBackendPlugin = {
| CliBackendPreparedExecution
| null
| undefined;
/**
* Backend-owned per-run argv rewrite.
*
* Use this for request-scoped CLI dialect flags that should not be modeled
* as static config, such as mapping OpenClaw thinking levels to a backend's
* native effort flag.
*/
resolveExecutionArgs?: CliBackendResolveExecutionArgs;
/**
* Whether this CLI backend can expose native tools outside OpenClaw's tool
* catalog. Backends that cannot provide a true no-tools mode must mark

View File

@@ -84,6 +84,9 @@ import type {
CliBackendNormalizeConfigContext,
CliBackendPreparedExecution,
CliBackendPrepareExecutionContext,
CliBackendResolveExecutionArgs,
CliBackendResolveExecutionArgsContext,
CliBackendThinkingLevel,
CliBackendPlugin,
CliBundleMcpMode,
PluginTextReplacement,
@@ -194,6 +197,9 @@ export type {
CliBackendNativeToolMode,
CliBackendPreparedExecution,
CliBackendPrepareExecutionContext,
CliBackendResolveExecutionArgs,
CliBackendResolveExecutionArgsContext,
CliBackendThinkingLevel,
CliBackendPlugin,
CliBundleMcpMode,
PluginTextReplacement,