diff --git a/nemoclaw-blueprint/model-specific-setup/openclaw/kimi-k2.6-managed-inference.json b/nemoclaw-blueprint/model-specific-setup/openclaw/kimi-k2.6-managed-inference.json index 183adacbfca..16927004c40 100644 --- a/nemoclaw-blueprint/model-specific-setup/openclaw/kimi-k2.6-managed-inference.json +++ b/nemoclaw-blueprint/model-specific-setup/openclaw/kimi-k2.6-managed-inference.json @@ -15,6 +15,9 @@ "maxTokensField": "max_tokens", "requiresToolResultName": true }, + "openclawTools": { + "toolSearch": false + }, "openclawPlugins": [ { "id": "nemoclaw-kimi-inference-compat", diff --git a/nemoclaw-blueprint/model-specific-setup/schema.json b/nemoclaw-blueprint/model-specific-setup/schema.json index f84ace6e9b9..f8e728745ff 100644 --- a/nemoclaw-blueprint/model-specific-setup/schema.json +++ b/nemoclaw-blueprint/model-specific-setup/schema.json @@ -66,6 +66,15 @@ "type": "object", "additionalProperties": true }, + "openclawTools": { + "type": "object", + "additionalProperties": false, + "properties": { + "toolSearch": { + "type": "boolean" + } + } + }, "openclawPlugins": { "type": "array", "items": { @@ -136,6 +145,9 @@ }, { "required": ["openclawPlugins"] + }, + { + "required": ["openclawTools"] } ] } diff --git a/scripts/generate-openclaw-config.py b/scripts/generate-openclaw-config.py index 43c8fe7f5e2..28dd98fd853 100755 --- a/scripts/generate-openclaw-config.py +++ b/scripts/generate-openclaw-config.py @@ -52,7 +52,7 @@ KNOWN_MODEL_SETUP_AGENTS = {"openclaw", "hermes"} MODEL_SETUP_EFFECT_KEYS = { - "openclaw": {"openclawCompat", "openclawPlugins"}, + "openclaw": {"openclawCompat", "openclawPlugins", "openclawTools"}, "hermes": {"hermesCompat"}, } DEFAULT_DASHBOARD_PORT = 18789 @@ -238,6 +238,21 @@ def _validate_selected_agent_effects(payload: dict, manifest_path: Path, registr if compat is not None and not isinstance(compat, dict): raise ValueError(f"{manifest_path}: effects.openclawCompat must be an object") + tools = effects.get("openclawTools") + if tools is not None: + if not isinstance(tools, dict): + raise ValueError(f"{manifest_path}: effects.openclawTools must be an object") + unknown_tool_keys = sorted(set(tools) - {"toolSearch"}) + if unknown_tool_keys: + raise ValueError( + f"{manifest_path}: unknown effects.openclawTools keys: " + f"{', '.join(unknown_tool_keys)}" + ) + if "toolSearch" in tools and not isinstance(tools["toolSearch"], bool): + raise ValueError( + f"{manifest_path}: effects.openclawTools.toolSearch must be a boolean" + ) + plugins = effects.get("openclawPlugins", []) if not isinstance(plugins, list): raise ValueError(f"{manifest_path}: effects.openclawPlugins must be an array") @@ -328,7 +343,11 @@ def _coerce_compat_dict(value: object) -> dict: def _apply_openclaw_setup_effects( - setup: dict, inference_compat: dict, openclaw_plugins: list[dict], plugin_ids: set[str] + setup: dict, + inference_compat: dict, + openclaw_plugins: list[dict], + plugin_ids: set[str], + openclaw_tools: dict, ) -> None: effects = setup["effects"] for key, value in effects.get("openclawCompat", {}).items(): @@ -339,6 +358,14 @@ def _apply_openclaw_setup_effects( ) inference_compat[key] = value + for key, value in effects.get("openclawTools", {}).items(): + if key in openclaw_tools and openclaw_tools[key] != value: + raise ValueError( + "model-specific setup " + f"'{setup['id']}' conflicts with OpenClaw tools key '{key}'" + ) + openclaw_tools[key] = value + for plugin in effects.get("openclawPlugins", []): plugin_id = plugin["id"] if plugin_id in plugin_ids: @@ -434,10 +461,16 @@ def build_config(env: dict | None = None) -> dict: ) openclaw_plugins: list[dict] = [] openclaw_plugin_ids: set[str] = set() + openclaw_tool_overrides: dict = {} for setup in model_specific_setups: _apply_openclaw_setup_effects( - setup, inference_compat, openclaw_plugins, openclaw_plugin_ids + setup, + inference_compat, + openclaw_plugins, + openclaw_plugin_ids, + openclaw_tool_overrides, ) + openclaw_tools = {"toolSearch": True, **openclaw_tool_overrides} # Ollama's OpenAI-compatible /v1/chat/completions stream omits the # `usage` chunk by default; OpenAI clients have to send @@ -701,7 +734,7 @@ def _placeholder(channel: str, env_key: str) -> str: }, "models": {"mode": "merge", "providers": providers}, "channels": {"defaults": {}, **_ch_cfg}, - "tools": {"toolSearch": True}, + "tools": openclaw_tools, "update": {"checkOnStart": False}, # Disable bundled plugins/channels that hit the L7 proxy at startup # and either crash or hang the gateway: diff --git a/test/e2e/test-kimi-inference-compat.sh b/test/e2e/test-kimi-inference-compat.sh index 9b46db0b574..607c976fb68 100755 --- a/test/e2e/test-kimi-inference-compat.sh +++ b/test/e2e/test-kimi-inference-compat.sh @@ -466,10 +466,14 @@ if "/usr/local/share/nemoclaw/openclaw-plugins/kimi-inference-compat" not in pat errors.append("Kimi plugin load path missing") if not entries.get("nemoclaw-kimi-inference-compat", {}).get("enabled"): errors.append("Kimi plugin entry is not enabled") +tools = cfg.get("tools", {}) +if tools.get("toolSearch") is not False: + errors.append("tools.toolSearch is %r" % tools.get("toolSearch")) print(json.dumps({ "provider_keys": sorted(providers.keys()) if isinstance(providers, dict) else [], "primary": primary, "plugin_enabled": entries.get("nemoclaw-kimi-inference-compat", {}).get("enabled"), + "toolSearch": tools.get("toolSearch"), "errors": errors, })) sys.exit(1 if errors else 0) @@ -496,14 +500,39 @@ check_inference_route() { } run_agent_prompt() { - local prompt remote_cmd agent_exit=0 + local prompt remote_cmd agent_exit=0 final_text prompt="Use the exec tool to run hostname, date, and uptime. Run each command and then say exactly: hostname, date, and uptime completed successfully." remote_cmd="rm -f /sandbox/.openclaw/agents/main/sessions/${SESSION_ID}.jsonl.lock /sandbox/.openclaw/agents/main/sessions/${SESSION_ID}.trajectory.jsonl 2>/dev/null || true; nemoclaw-start openclaw agent --agent main --json --session-id $(quote_for_remote_sh "$SESSION_ID") -m $(quote_for_remote_sh "$prompt")" run_with_timeout 420 openshell sandbox exec --name "$SANDBOX_NAME" -- sh -lc "$remote_cmd" >"$AGENT_LOG" 2>&1 || agent_exit=$? - if [ "$agent_exit" -eq 0 ] && grep -q "hostname, date, and uptime completed successfully." "$AGENT_LOG"; then + final_text="$( + python3 - "$AGENT_LOG" <<'PY' 2>/dev/null || true +import json +import sys + +text = open(sys.argv[1], encoding="utf-8", errors="replace").read() +for idx, ch in enumerate(text): + if ch != "{": + continue + try: + data = json.loads(text[idx:]) + except Exception: + continue + payloads = data.get("payloads") or [] + texts = [p.get("text") for p in payloads if isinstance(p, dict) and isinstance(p.get("text"), str)] + if texts: + print(texts[-1]) + break + meta_text = data.get("meta", {}).get("finalAssistantVisibleText") + if isinstance(meta_text, str): + print(meta_text) + break +PY + )" + if [ "$agent_exit" -eq 0 ] && [ "$final_text" = "hostname, date, and uptime completed successfully." ]; then pass "K4: OpenClaw agent completed after Kimi tool results" else fail "K4: OpenClaw agent did not complete successfully (exit $agent_exit)" + info "Parsed final assistant text: ${final_text:-}" info "Agent log tail:" tail -120 "$AGENT_LOG" 2>/dev/null || true fi diff --git a/test/generate-openclaw-config.test.ts b/test/generate-openclaw-config.test.ts index f9649bbdbba..c0b33092ee3 100644 --- a/test/generate-openclaw-config.test.ts +++ b/test/generate-openclaw-config.test.ts @@ -690,6 +690,7 @@ describe("generate-openclaw-config.py: config generation", () => { expect(config.plugins.load.paths).toEqual([ "/usr/local/share/nemoclaw/openclaw-plugins/kimi-inference-compat", ]); + expect(config.tools?.toolSearch).toBe(false); }); it("adds registry compat when the incoming compat blob is null", () => { @@ -798,8 +799,9 @@ describe("generate-openclaw-config.py: config generation", () => { expect(providerConfig.models[0].compat).toEqual({ supportsStore: false }); expect(config.plugins.entries["nemoclaw-kimi-inference-compat"]).toBeUndefined(); expect(config.plugins.load).toBeUndefined(); + expect(config.tools?.toolSearch).toBe(true); } - }); + }, 20_000); it("rejects model-specific setup manifests without a known agent", () => { const blueprintDir = path.join(tmpDir, "fixture-blueprint"); @@ -840,7 +842,7 @@ describe("generate-openclaw-config.py: config generation", () => { expect(unknownResult.status).not.toBe(0); expect(unknownResult.stderr).toContain("unknown agent 'sidecar'"); - }); + }, 20_000); it("rejects empty match objects and invalid explicit registry overrides", () => { const missingRegistry = path.join(tmpDir, "missing-registry"); @@ -924,6 +926,26 @@ describe("generate-openclaw-config.py: config generation", () => { expect(missingPluginResult.stderr).toContain("path does not exist"); fs.rmSync(path.join(blueprintDir, "model-specific-setup", "openclaw", "missing-plugin.json")); + const badToolRegistryDir = writeRegistryManifest( + blueprintDir, + "openclaw/bad-tool-effect.json", + { + id: "bad-tool-effect", + agent: "openclaw", + description: "Invalid tool override", + match: { modelIds: ["test-model"] }, + effects: { openclawTools: { toolSearch: "false" } }, + }, + ); + + const badToolResult = runConfigScriptRaw({ + NEMOCLAW_MODEL_SPECIFIC_SETUP_DIR: badToolRegistryDir, + }); + + expect(badToolResult.status).not.toBe(0); + expect(badToolResult.stderr).toContain("effects.openclawTools.toolSearch must be a boolean"); + + fs.rmSync(path.join(blueprintDir, "model-specific-setup", "openclaw", "bad-tool-effect.json")); fs.mkdirSync(path.join(blueprintDir, "openclaw-plugins", "fixture"), { recursive: true }); const badLoadPathRegistryDir = writeRegistryManifest( blueprintDir,