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
2 changes: 1 addition & 1 deletion assets/README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ skillclaw setup
第一次最小化验证时,推荐这样选:

- `CLI agent` 选 `none`,先不要自动改外部 agent 配置
- `skills` 目录保持默认值 `~/.skillclaw/skills`;如果你选了 Hermes,默认技能库会变成 `~/.hermes/skills`
- `skills` 目录保持默认值 `~/.skillclaw/skills`;如果你选了 Hermes、Codex 或 Claude Code,默认技能库会变成 `~/.hermes/skills`、`~/.codex/skills` 或 `~/.claude/skills`
- 如果你只是想先验证代理能不能正常用,可以先关闭 shared storage
- 如果你后面想在同一台机器上继续跑本地 evolver 闭环,就把 shared storage 打开并选 `local` backend,例如 `~/.skillclaw/local-share`
- 如果你想先把成本压到最低,可以先关闭 PRM
Expand Down
6 changes: 1 addition & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ dependencies = [
]

[project.optional-dependencies]
# Tokenizer for prompt truncation and PRM/OPD token-level data
tokenizer = [
"transformers>=4.51.1",
]
# Embedding-based skill retrieval
embedding = [
"numpy",
Expand All @@ -44,7 +40,7 @@ server = [
]
# Everything
all = [
"skillclaw[tokenizer,embedding,evolve,sharing,server]",
"skillclaw[embedding,evolve,sharing,server]",
]

[project.scripts]
Expand Down
467 changes: 126 additions & 341 deletions skillclaw/api_server.py

Large diffs are not rendered by default.

75 changes: 60 additions & 15 deletions skillclaw/claw_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
openclaw — runs `openclaw config set …` + `openclaw gateway restart`
opencode — patches ~/.config/opencode/opencode.json to register SkillClaw provider
hermes — patches ~/.hermes/config.yaml to point model traffic at SkillClaw
codex — patches ~/.codex/config.toml to register SkillClaw as a provider
codex — patches ~/.codex/config.toml to register an opt-in SkillClaw profile
claude — patches ~/.claude/settings.json to route Anthropic traffic via SkillClaw
qwenpaw — patches QwenPaw model config, selects SkillClaw as active model
ironclaw — patches ~/.ironclaw/.env, runs `ironclaw service restart`
Expand Down Expand Up @@ -328,6 +328,30 @@ def _upsert_top_level_toml_keys(text: str, updates: dict[str, object]) -> str:
return "\n".join(merged).rstrip() + "\n"


def _remove_top_level_toml_keys(text: str, keys: set[str]) -> str:
"""Remove selected top-level assignments before the first TOML table."""
lines = text.splitlines()
first_table_index = len(lines)
for idx, line in enumerate(lines):
stripped = line.strip()
if stripped.startswith("[") and stripped.endswith("]"):
first_table_index = idx
break

preamble = lines[:first_table_index]
remainder = lines[first_table_index:]
kept: list[str] = []
for line in preamble:
stripped = line.strip()
if stripped.startswith("#") or "=" not in stripped:
kept.append(line)
continue
key = stripped.split("=", 1)[0].strip()
if key not in keys:
kept.append(line)
return "\n".join(kept + remainder).rstrip() + "\n"


def _remove_toml_table(text: str, table_name: str) -> str:
"""Remove a TOML table and its body, if present."""
lines = text.splitlines()
Expand Down Expand Up @@ -630,8 +654,21 @@ def _build_codex_provider_block(base_url: str, api_key: str) -> str:
return "\n".join(lines) + "\n"


def _build_codex_profile_block(model_id: str) -> str:
lines = [
"[profiles.skillclaw]",
f"model = {_format_toml_value(model_id)}",
'model_provider = "skillclaw"',
]
return "\n".join(lines) + "\n"


def _configure_codex(cfg: "SkillClawConfig") -> None:
"""Auto-configure Codex CLI to use the SkillClaw proxy."""
"""Register SkillClaw as an opt-in Codex profile.

Do not change Codex's global ``model`` / ``model_provider`` defaults.
Users opt in explicitly with ``codex --profile skillclaw``.
"""
model_id = cfg.served_model_name or cfg.llm_model_id or "skillclaw-model"
api_key = cfg.proxy_api_key or "skillclaw"
base_url = f"http://127.0.0.1:{cfg.proxy_port}/v1"
Expand All @@ -645,15 +682,13 @@ def _configure_codex(cfg: "SkillClawConfig") -> None:
except Exception as e:
logger.warning("[ClawAdapter] Failed to read Codex config %s: %s", config_path, e)

updated = _upsert_top_level_toml_keys(
existing_text,
{
"model": model_id,
"model_provider": "skillclaw",
},
)
updated = existing_text
if str(_extract_top_level_toml_value(updated, "model_provider") or "") == "skillclaw":
updated = _remove_top_level_toml_keys(updated, {"model", "model_provider"})
updated = _remove_toml_table(updated, "model_providers.skillclaw").rstrip() + "\n\n"
updated = _remove_toml_table(updated, "profiles.skillclaw").rstrip() + "\n\n"
updated += _build_codex_provider_block(base_url, api_key)
updated += "\n" + _build_codex_profile_block(model_id)

_backup_codex_config_if_changed(config_path, updated)
_write_text_atomic(config_path, updated, "Codex config")
Expand Down Expand Up @@ -683,10 +718,13 @@ def inspect_codex_config(cfg: "SkillClawConfig") -> dict[str, object]:
configured_base_url = str(provider_cfg.get("base_url") or "")
configured_wire_api = str(provider_cfg.get("wire_api") or "")
configured_token = str(provider_cfg.get("experimental_bearer_token") or "")
profile_cfg = _extract_toml_table(text, "profiles.skillclaw")
configured_profile_model = str(profile_cfg.get("model") or "")
configured_profile_provider = str(profile_cfg.get("model_provider") or "")

proxy_match = (
configured_model == expected_model
and configured_provider == "skillclaw"
configured_profile_model == expected_model
and configured_profile_provider == "skillclaw"
and configured_base_url == expected_base_url
and configured_wire_api == "responses"
and configured_token == expected_api_key
Expand All @@ -696,7 +734,8 @@ def inspect_codex_config(cfg: "SkillClawConfig") -> dict[str, object]:
skills_dir_match = configured_skillclaw_skills_dir == expected_skills_dir
issues: list[str] = []
notes: list[str] = [
"Codex uses the OpenAI Responses-compatible SkillClaw endpoint via `model_providers.skillclaw`.",
"Codex can opt into SkillClaw with `codex --profile skillclaw`.",
"SkillClaw registers a Codex profile and does not change Codex's global model defaults.",
"Codex session boundaries fall back to proxy-side heuristics because"
" Codex does not send SkillClaw session headers.",
]
Expand All @@ -705,8 +744,11 @@ def inspect_codex_config(cfg: "SkillClawConfig") -> dict[str, object]:
if not config_path.exists():
issues.append("Codex config is missing: ~/.codex/config.toml")
if not proxy_match:
issues.append("Codex model routing is not pointing at the local SkillClaw proxy.")
next_steps.append("Start SkillClaw once with `claw_type=codex` so it can rewrite ~/.codex/config.toml.")
issues.append("Codex SkillClaw profile is missing or not pointing at the local SkillClaw proxy.")
next_steps.append("Start SkillClaw once with `claw_type=codex` so it can register ~/.codex/config.toml.")
if configured_provider == "skillclaw":
issues.append("Codex global model_provider still points at SkillClaw; normal Codex runs may be intercepted.")
next_steps.append("Remove top-level `model_provider = \"skillclaw\"` or run `skillclaw restore codex`.")
if not expected_skills_dir.is_dir():
issues.append(f"Codex skills directory is missing: {expected_skills_dir}")
next_steps.append(f"Create or prepare the Codex skills directory: {expected_skills_dir}")
Expand All @@ -723,9 +765,12 @@ def inspect_codex_config(cfg: "SkillClawConfig") -> dict[str, object]:
"status": "ok" if not issues else "warning",
"config_path": str(config_path),
"config_exists": config_path.exists(),
"integration_scope": "codex-only",
"integration_scope": "codex-profile-only",
"expected_model": expected_model,
"configured_model": configured_model or "(unset)",
"expected_profile": "skillclaw",
"configured_profile_model": configured_profile_model or "(unset)",
"configured_profile_provider": configured_profile_provider or "(unset)",
"expected_base_url": expected_base_url,
"configured_base_url": configured_base_url or "(unset)",
"configured_provider": configured_provider or "(unset)",
Expand Down
15 changes: 13 additions & 2 deletions skillclaw/config_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
_DEFAULT_CODEX_SKILLS_DIR = Path.home() / ".codex" / "skills"
_DEFAULT_CLAUDE_SKILLS_DIR = Path.home() / ".claude" / "skills"
_DEFAULT_OPENCODE_SKILLS_DIR = Path.home() / ".config" / "opencode" / "skills"
_DEFAULT_LLM_API_MODE_BY_CLAW = {
"codex": "responses",
}
_FALLBACK_LLM_API_MODE = "chat"

_DEFAULTS: dict = {
"llm": {
Expand Down Expand Up @@ -161,6 +165,12 @@ def default_skills_dir_for_claw(claw_type: str) -> Path:
return _DEFAULT_SKILLS_DIR


def default_llm_api_mode_for_claw(claw_type: str) -> str:
"""Return the default upstream API mode for the selected agent."""
normalized = str(claw_type or "").strip().lower()
return _DEFAULT_LLM_API_MODE_BY_CLAW.get(normalized, _FALLBACK_LLM_API_MODE)


def resolve_skills_dir(skills_dir: Any, *, claw_type: str) -> str:
"""Normalize a configured skills dir, applying agent-native defaults.

Expand Down Expand Up @@ -254,13 +264,14 @@ def to_skillclaw_config(self) -> SkillClawConfig:
llm_api_base = llm.get("api_base", "")
llm_api_key = llm.get("api_key", "")
llm_model_id = llm.get("model_id", "")
llm_api_mode = str(llm.get("api_mode", "chat") or "chat")
raw_claw_type = str(data.get("claw_type", "openclaw") or "openclaw")
default_api_mode = default_llm_api_mode_for_claw(raw_claw_type)
llm_api_mode = str(llm.get("api_mode", default_api_mode) or default_api_mode)
proxy = data.get("proxy", {})
skills = data.get("skills", {})
orouter = data.get("openrouter", {})
prm = data.get("prm", {})
configure_openclaw = bool(data.get("configure_openclaw", True))
raw_claw_type = str(data.get("claw_type", "openclaw") or "openclaw")
if not configure_openclaw:
raw_claw_type = "none"

Expand Down
23 changes: 0 additions & 23 deletions skillclaw/data_formatter.py

This file was deleted.

16 changes: 13 additions & 3 deletions skillclaw/setup_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pathlib import Path

from .claw_adapter import CLAW_TYPES
from .config_store import CONFIG_DIR, ConfigStore, resolve_skills_dir
from .config_store import CONFIG_DIR, ConfigStore, default_llm_api_mode_for_claw, resolve_skills_dir

_PROVIDER_PRESETS = {
"kimi": {
Expand Down Expand Up @@ -202,7 +202,13 @@ def run(self):
f"Recommended directory: {default_skills_dir}"
)
elif claw_type == "codex":
print(f"Codex reads native skills from ~/.codex/skills.\nRecommended directory: {default_skills_dir}")
print(
"Codex will get a SkillClaw profile without changing its global defaults.\n"
"After starting SkillClaw, run: codex --profile skillclaw\n"
"Normal `codex` runs remain unchanged.\n"
"Codex reads native skills from ~/.codex/skills.\n"
f"Recommended directory: {default_skills_dir}"
)
elif claw_type == "claude":
print(
f"Claude Code reads native skills from ~/.claude/skills.\nRecommended directory: {default_skills_dir}"
Expand Down Expand Up @@ -343,7 +349,8 @@ def run(self):
proxy_config["port"] = proxy_port
proxy_config.setdefault("host", "0.0.0.0")
proxy_config["served_model_name"] = served_model_name or "skillclaw-model"
llm_api_mode = str(current_llm.get("api_mode", "chat") or "chat")
default_api_mode = default_llm_api_mode_for_claw(claw_type)
llm_api_mode = str(current_llm.get("api_mode", default_api_mode) or default_api_mode)
data = {
"claw_type": claw_type,
"llm": {
Expand Down Expand Up @@ -375,4 +382,7 @@ def run(self):

print(f"\nConfig saved to: {cs.config_file}")
print("\nRun 'skillclaw start' to launch SkillClaw.")
if claw_type == "codex":
print("Then run 'codex --profile skillclaw' to use Codex through SkillClaw.")
print("Use 'skillclaw doctor codex' if the profile does not work as expected.")
print("=" * 60 + "\n")
11 changes: 1 addition & 10 deletions tests/test_anthropic_messages_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@


@pytest.fixture
def anthropic_server(monkeypatch, tmp_path):
monkeypatch.setattr(SkillClawAPIServer, "_load_tokenizer", lambda self: None)
def anthropic_server(tmp_path):
return SkillClawAPIServer(
SkillClawConfig(
proxy_api_key="skillclaw",
Expand Down Expand Up @@ -43,14 +42,6 @@ async def test_anthropic_count_tokens_endpoint_returns_local_estimate(anthropic_

@pytest.mark.asyncio
async def test_anthropic_count_tokens_accounts_for_image_content(anthropic_server):
class FakeTokenizer:
def apply_chat_template(self, messages, tools=None, tokenize=False, add_generation_prompt=False):
return "user: screenshot"

def __call__(self, text, add_special_tokens=False):
return {"input_ids": [1, 2, 3]}

anthropic_server._tokenizer = FakeTokenizer()
png_header = (
b"\x89PNG\r\n\x1a\n"
+ struct.pack(">I", 13)
Expand Down
Loading
Loading