mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:50:42 +00:00
fix: pass claude cli thinking effort (#77410)
Summary: - Adds a plugin-owned CLI backend argument rewrite hook and wires Anthropic `claude-cli` to translate non-off `/think` levels into Claude Code `--effort`, with docs, changelog, API baseline, and tests. - Reproducibility: yes. Current main has a high-confidence source reproduction: choose `claude-cli`, set a non ... builds argv from backend args that contain no `--effort` even though `thinkLevel` exists on the run params. Automerge notes: - No ClawSweeper repair was needed after automerge opt-in. Validation: - ClawSweeper review passed for headbe17754009. - Required merge gates passed before the squash merge. Prepared head SHA:be17754009Review: https://github.com/openclaw/openclaw/pull/77410#issuecomment-4372812685 Co-authored-by: stainlu <stainlu@newtype-ai.org>
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,6 +6,9 @@ export type {
|
||||
CliBackendPlugin,
|
||||
CliBackendPreparedExecution,
|
||||
CliBackendPrepareExecutionContext,
|
||||
CliBackendResolveExecutionArgs,
|
||||
CliBackendResolveExecutionArgsContext,
|
||||
CliBackendThinkingLevel,
|
||||
} from "../plugins/types.js";
|
||||
export {
|
||||
CLI_FRESH_WATCHDOG_DEFAULTS,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user