mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 18:00:22 +00:00
feat: pluginize cli inference backends
This commit is contained in:
@@ -1,7 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { buildAnthropicCliBackend } from "../../extensions/anthropic/cli-backend.js";
|
||||
import { buildGoogleGeminiCliBackend } from "../../extensions/google/cli-backend.js";
|
||||
import { buildOpenAICodexCliBackend } from "../../extensions/openai/cli-backend.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { resolveCliBackendConfig } from "./cli-backends.js";
|
||||
|
||||
beforeEach(() => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.cliBackends = [
|
||||
{
|
||||
pluginId: "anthropic",
|
||||
backend: buildAnthropicCliBackend(),
|
||||
source: "test",
|
||||
},
|
||||
{
|
||||
pluginId: "openai",
|
||||
backend: buildOpenAICodexCliBackend(),
|
||||
source: "test",
|
||||
},
|
||||
{
|
||||
pluginId: "google",
|
||||
backend: buildGoogleGeminiCliBackend(),
|
||||
source: "test",
|
||||
},
|
||||
];
|
||||
setActivePluginRegistry(registry);
|
||||
});
|
||||
|
||||
describe("resolveCliBackendConfig reliability merge", () => {
|
||||
it("defaults codex-cli to workspace-write for fresh and resume runs", () => {
|
||||
const resolved = resolveCliBackendConfig("codex-cli");
|
||||
@@ -166,3 +193,24 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
|
||||
expect(resolved?.config.resumeArgs).not.toContain("bypassPermissions");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveCliBackendConfig google-gemini-cli defaults", () => {
|
||||
it("uses Gemini CLI json args and existing-session resume mode", () => {
|
||||
const resolved = resolveCliBackendConfig("google-gemini-cli");
|
||||
|
||||
expect(resolved).not.toBeNull();
|
||||
expect(resolved?.bundleMcp).toBe(false);
|
||||
expect(resolved?.config.args).toEqual(["--prompt", "--output-format", "json"]);
|
||||
expect(resolved?.config.resumeArgs).toEqual([
|
||||
"--resume",
|
||||
"{sessionId}",
|
||||
"--prompt",
|
||||
"--output-format",
|
||||
"json",
|
||||
]);
|
||||
expect(resolved?.config.modelArg).toBe("--model");
|
||||
expect(resolved?.config.sessionMode).toBe("existing");
|
||||
expect(resolved?.config.sessionIdFields).toEqual(["session_id", "sessionId"]);
|
||||
expect(resolved?.config.modelAliases?.pro).toBe("gemini-3.1-pro-preview");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,110 +1,13 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { CliBackendConfig } from "../config/types.js";
|
||||
import {
|
||||
CLI_FRESH_WATCHDOG_DEFAULTS,
|
||||
CLI_RESUME_WATCHDOG_DEFAULTS,
|
||||
} from "./cli-watchdog-defaults.js";
|
||||
import { resolveRuntimeCliBackends } from "../plugins/cli-backends.runtime.js";
|
||||
import { normalizeProviderId } from "./model-selection.js";
|
||||
|
||||
export type ResolvedCliBackend = {
|
||||
id: string;
|
||||
config: CliBackendConfig;
|
||||
};
|
||||
|
||||
const CLAUDE_MODEL_ALIASES: Record<string, string> = {
|
||||
opus: "opus",
|
||||
"opus-4.6": "opus",
|
||||
"opus-4.5": "opus",
|
||||
"opus-4": "opus",
|
||||
"claude-opus-4-6": "opus",
|
||||
"claude-opus-4-5": "opus",
|
||||
"claude-opus-4": "opus",
|
||||
sonnet: "sonnet",
|
||||
"sonnet-4.6": "sonnet",
|
||||
"sonnet-4.5": "sonnet",
|
||||
"sonnet-4.1": "sonnet",
|
||||
"sonnet-4.0": "sonnet",
|
||||
"claude-sonnet-4-6": "sonnet",
|
||||
"claude-sonnet-4-5": "sonnet",
|
||||
"claude-sonnet-4-1": "sonnet",
|
||||
"claude-sonnet-4-0": "sonnet",
|
||||
haiku: "haiku",
|
||||
"haiku-3.5": "haiku",
|
||||
"claude-haiku-3-5": "haiku",
|
||||
};
|
||||
|
||||
const CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG = "--dangerously-skip-permissions";
|
||||
const CLAUDE_PERMISSION_MODE_ARG = "--permission-mode";
|
||||
const CLAUDE_BYPASS_PERMISSIONS_MODE = "bypassPermissions";
|
||||
|
||||
const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = {
|
||||
command: "claude",
|
||||
args: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions"],
|
||||
resumeArgs: [
|
||||
"-p",
|
||||
"--output-format",
|
||||
"json",
|
||||
"--permission-mode",
|
||||
"bypassPermissions",
|
||||
"--resume",
|
||||
"{sessionId}",
|
||||
],
|
||||
output: "json",
|
||||
input: "arg",
|
||||
modelArg: "--model",
|
||||
modelAliases: CLAUDE_MODEL_ALIASES,
|
||||
sessionArg: "--session-id",
|
||||
sessionMode: "always",
|
||||
sessionIdFields: ["session_id", "sessionId", "conversation_id", "conversationId"],
|
||||
systemPromptArg: "--append-system-prompt",
|
||||
systemPromptMode: "append",
|
||||
systemPromptWhen: "first",
|
||||
clearEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"],
|
||||
reliability: {
|
||||
watchdog: {
|
||||
fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS },
|
||||
resume: { ...CLI_RESUME_WATCHDOG_DEFAULTS },
|
||||
},
|
||||
},
|
||||
serialize: true,
|
||||
};
|
||||
|
||||
const DEFAULT_CODEX_BACKEND: CliBackendConfig = {
|
||||
command: "codex",
|
||||
args: [
|
||||
"exec",
|
||||
"--json",
|
||||
"--color",
|
||||
"never",
|
||||
"--sandbox",
|
||||
"workspace-write",
|
||||
"--skip-git-repo-check",
|
||||
],
|
||||
resumeArgs: [
|
||||
"exec",
|
||||
"resume",
|
||||
"{sessionId}",
|
||||
"--color",
|
||||
"never",
|
||||
"--sandbox",
|
||||
"workspace-write",
|
||||
"--skip-git-repo-check",
|
||||
],
|
||||
output: "jsonl",
|
||||
resumeOutput: "text",
|
||||
input: "arg",
|
||||
modelArg: "--model",
|
||||
sessionIdFields: ["thread_id"],
|
||||
sessionMode: "existing",
|
||||
imageArg: "--image",
|
||||
imageMode: "repeat",
|
||||
reliability: {
|
||||
watchdog: {
|
||||
fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS },
|
||||
resume: { ...CLI_RESUME_WATCHDOG_DEFAULTS },
|
||||
},
|
||||
},
|
||||
serialize: true,
|
||||
bundleMcp: boolean;
|
||||
pluginId?: string;
|
||||
};
|
||||
|
||||
function normalizeBackendKey(key: string): string {
|
||||
@@ -123,6 +26,11 @@ function pickBackendConfig(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveRegisteredBackend(provider: string) {
|
||||
const normalized = normalizeBackendKey(provider);
|
||||
return resolveRuntimeCliBackends().find((entry) => normalizeBackendKey(entry.id) === normalized);
|
||||
}
|
||||
|
||||
function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig): CliBackendConfig {
|
||||
if (!override) {
|
||||
return { ...base };
|
||||
@@ -160,53 +68,11 @@ function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeClaudePermissionArgs(args?: string[]): string[] | undefined {
|
||||
if (!args) {
|
||||
return args;
|
||||
}
|
||||
const normalized: string[] = [];
|
||||
let sawLegacySkip = false;
|
||||
let hasPermissionMode = false;
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) {
|
||||
sawLegacySkip = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === CLAUDE_PERMISSION_MODE_ARG) {
|
||||
hasPermissionMode = true;
|
||||
normalized.push(arg);
|
||||
const maybeValue = args[i + 1];
|
||||
if (typeof maybeValue === "string") {
|
||||
normalized.push(maybeValue);
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith(`${CLAUDE_PERMISSION_MODE_ARG}=`)) {
|
||||
hasPermissionMode = true;
|
||||
}
|
||||
normalized.push(arg);
|
||||
}
|
||||
if (sawLegacySkip && !hasPermissionMode) {
|
||||
normalized.push(CLAUDE_PERMISSION_MODE_ARG, CLAUDE_BYPASS_PERMISSIONS_MODE);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeClaudeBackendConfig(config: CliBackendConfig): CliBackendConfig {
|
||||
return {
|
||||
...config,
|
||||
args: normalizeClaudePermissionArgs(config.args),
|
||||
resumeArgs: normalizeClaudePermissionArgs(config.resumeArgs),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveCliBackendIds(cfg?: OpenClawConfig): Set<string> {
|
||||
const ids = new Set<string>([
|
||||
normalizeBackendKey("claude-cli"),
|
||||
normalizeBackendKey("codex-cli"),
|
||||
]);
|
||||
const ids = new Set<string>();
|
||||
for (const backend of resolveRuntimeCliBackends()) {
|
||||
ids.add(normalizeBackendKey(backend.id));
|
||||
}
|
||||
const configured = cfg?.agents?.defaults?.cliBackends ?? {};
|
||||
for (const key of Object.keys(configured)) {
|
||||
ids.add(normalizeBackendKey(key));
|
||||
@@ -221,23 +87,20 @@ export function resolveCliBackendConfig(
|
||||
const normalized = normalizeBackendKey(provider);
|
||||
const configured = cfg?.agents?.defaults?.cliBackends ?? {};
|
||||
const override = pickBackendConfig(configured, normalized);
|
||||
|
||||
if (normalized === "claude-cli") {
|
||||
const merged = mergeBackendConfig(DEFAULT_CLAUDE_BACKEND, override);
|
||||
const config = normalizeClaudeBackendConfig(merged);
|
||||
const registered = resolveRegisteredBackend(normalized);
|
||||
if (registered) {
|
||||
const merged = mergeBackendConfig(registered.config, override);
|
||||
const config = registered.normalizeConfig ? registered.normalizeConfig(merged) : merged;
|
||||
const command = config.command?.trim();
|
||||
if (!command) {
|
||||
return null;
|
||||
}
|
||||
return { id: normalized, config: { ...config, command } };
|
||||
}
|
||||
if (normalized === "codex-cli") {
|
||||
const merged = mergeBackendConfig(DEFAULT_CODEX_BACKEND, override);
|
||||
const command = merged.command?.trim();
|
||||
if (!command) {
|
||||
return null;
|
||||
}
|
||||
return { id: normalized, config: { ...merged, command } };
|
||||
return {
|
||||
id: normalized,
|
||||
config: { ...config, command },
|
||||
bundleMcp: registered.bundleMcp === true,
|
||||
pluginId: registered.pluginId,
|
||||
};
|
||||
}
|
||||
|
||||
if (!override) {
|
||||
@@ -247,5 +110,5 @@ export function resolveCliBackendConfig(
|
||||
if (!command) {
|
||||
return null;
|
||||
}
|
||||
return { id: normalized, config: { ...override, command } };
|
||||
return { id: normalized, config: { ...override, command }, bundleMcp: false };
|
||||
}
|
||||
|
||||
@@ -50,7 +50,9 @@ import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js";
|
||||
import { buildSystemPromptReport } from "./system-prompt-report.js";
|
||||
import { redactRunIdentifier, resolveRunWorkspaceDir } from "./workspace-run.js";
|
||||
|
||||
const log = createSubsystemLogger("agent/claude-cli");
|
||||
const log = createSubsystemLogger("agent/cli-backend");
|
||||
const CLI_BACKEND_LOG_OUTPUT_ENV = "OPENCLAW_CLI_BACKEND_LOG_OUTPUT";
|
||||
const LEGACY_CLAUDE_CLI_LOG_OUTPUT_ENV = "OPENCLAW_CLAUDE_CLI_LOG_OUTPUT";
|
||||
|
||||
export async function runCliAgent(params: {
|
||||
sessionId: string;
|
||||
@@ -97,7 +99,7 @@ export async function runCliAgent(params: {
|
||||
throw new Error(`Unknown CLI backend: ${params.provider}`);
|
||||
}
|
||||
const preparedBackend = await prepareCliBundleMcpConfig({
|
||||
backendId: backendResolved.id,
|
||||
enabled: backendResolved.bundleMcp,
|
||||
backend: backendResolved.config,
|
||||
workspaceDir,
|
||||
config: params.config,
|
||||
@@ -264,7 +266,9 @@ export async function runCliAgent(params: {
|
||||
log.info(
|
||||
`cli exec: provider=${params.provider} model=${normalizedModel} promptChars=${params.prompt.length}`,
|
||||
);
|
||||
const logOutputText = isTruthyEnvValue(process.env.OPENCLAW_CLAUDE_CLI_LOG_OUTPUT);
|
||||
const logOutputText =
|
||||
isTruthyEnvValue(process.env[CLI_BACKEND_LOG_OUTPUT_ENV]) ||
|
||||
isTruthyEnvValue(process.env[LEGACY_CLAUDE_CLI_LOG_OUTPUT_ENV]);
|
||||
if (logOutputText) {
|
||||
const logArgs: string[] = [];
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
|
||||
@@ -15,7 +15,7 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe("prepareCliBundleMcpConfig", () => {
|
||||
it("injects a merged --mcp-config overlay for claude-cli", async () => {
|
||||
it("injects a merged --mcp-config overlay for bundle-MCP-enabled backends", async () => {
|
||||
const env = captureEnv(["HOME"]);
|
||||
try {
|
||||
const homeDir = await tempHarness.createTempDir("openclaw-cli-bundle-mcp-home-");
|
||||
@@ -33,7 +33,7 @@ describe("prepareCliBundleMcpConfig", () => {
|
||||
};
|
||||
|
||||
const prepared = await prepareCliBundleMcpConfig({
|
||||
backendId: "claude-cli",
|
||||
enabled: true,
|
||||
backend: {
|
||||
command: "node",
|
||||
args: ["./fake-claude.mjs"],
|
||||
@@ -57,4 +57,18 @@ describe("prepareCliBundleMcpConfig", () => {
|
||||
env.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves args untouched when bundle MCP is disabled", async () => {
|
||||
const prepared = await prepareCliBundleMcpConfig({
|
||||
enabled: false,
|
||||
backend: {
|
||||
command: "node",
|
||||
args: ["./fake-cli.mjs"],
|
||||
},
|
||||
workspaceDir: "/tmp/openclaw-bundle-mcp-disabled",
|
||||
});
|
||||
|
||||
expect(prepared.backend.args).toEqual(["./fake-cli.mjs"]);
|
||||
expect(prepared.cleanup).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,13 +63,13 @@ function injectMcpConfigArgs(args: string[] | undefined, mcpConfigPath: string):
|
||||
}
|
||||
|
||||
export async function prepareCliBundleMcpConfig(params: {
|
||||
backendId: string;
|
||||
enabled: boolean;
|
||||
backend: CliBackendConfig;
|
||||
workspaceDir: string;
|
||||
config?: OpenClawConfig;
|
||||
warn?: (message: string) => void;
|
||||
}): Promise<PreparedCliBundleMcpConfig> {
|
||||
if (params.backendId !== "claude-cli") {
|
||||
if (!params.enabled) {
|
||||
return { backend: params.backend };
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
toAgentModelListLike,
|
||||
} from "../config/model-input.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveRuntimeCliBackends } from "../plugins/cli-backends.runtime.js";
|
||||
import { sanitizeForLog } from "../terminal/ansi.js";
|
||||
import {
|
||||
resolveAgentConfig,
|
||||
@@ -81,10 +82,9 @@ export {
|
||||
|
||||
export function isCliProvider(provider: string, cfg?: OpenClawConfig): boolean {
|
||||
const normalized = normalizeProviderId(provider);
|
||||
if (normalized === "claude-cli") {
|
||||
return true;
|
||||
}
|
||||
if (normalized === "codex-cli") {
|
||||
if (
|
||||
resolveRuntimeCliBackends().some((backend) => normalizeProviderId(backend.id) === normalized)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const backends = cfg?.agents?.defaults?.cliBackends ?? {};
|
||||
|
||||
Reference in New Issue
Block a user