Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"maxTokensField": "max_tokens",
"requiresToolResultName": true
},
"openclawTools": {
"toolSearch": false
},
Comment on lines +18 to +20

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add the required SPDX license header to this manifest.

This JSON source file is missing the required SPDX copyright/license header.

💡 Suggested update
 {
+  "$comment": "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0",
   "$schema": "../schema.json",
   "id": "kimi-k2.6-managed-inference",

As per coding guidelines, **/*.{js,ts,tsx,jsx,sh,yaml,yml,json,md,mdx}: Every source file must include an SPDX license header for copyright and Apache-2.0 license.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@nemoclaw-blueprint/model-specific-setup/openclaw/kimi-k2.6-managed-inference.json`
around lines 18 - 20, Add a top-of-file SPDX license header to this JSON
manifest: insert a single-line comment-style SPDX header (e.g., including
copyright owner and "SPDX-License-Identifier: Apache-2.0") at the very top of
the file that contains the "openclawTools" object so the file complies with the
required SPDX header rule for JSON sources.

"openclawPlugins": [
{
"id": "nemoclaw-kimi-inference-compat",
Expand Down
12 changes: 12 additions & 0 deletions nemoclaw-blueprint/model-specific-setup/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@
"type": "object",
"additionalProperties": true
},
"openclawTools": {
"type": "object",
"additionalProperties": false,
"properties": {
"toolSearch": {
"type": "boolean"
}
}
},
"openclawPlugins": {
"type": "array",
"items": {
Expand Down Expand Up @@ -136,6 +145,9 @@
},
{
"required": ["openclawPlugins"]
},
{
"required": ["openclawTools"]
}
]
}
Expand Down
41 changes: 37 additions & 4 deletions scripts/generate-openclaw-config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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():
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
33 changes: 31 additions & 2 deletions test/e2e/test-kimi-inference-compat.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:-<missing>}"
info "Agent log tail:"
tail -120 "$AGENT_LOG" 2>/dev/null || true
fi
Expand Down
26 changes: 24 additions & 2 deletions test/generate-openclaw-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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,
Expand Down
Loading