Spaces:
Running
Running
fix: stop overriding OpenClaw provider config — pass all env vars through
Browse filessync_hf.py was hardcoding a provider registry and overwriting
models.providers on every startup, blocking OpenClaw's native env var
support. Users setting NVIDIA_API_KEY or other provider keys got no
effect because the code didn't know about them.
Now HuggingClaw acts as a transparent deployment layer:
- Removed PROVIDER_REGISTRY and all per-provider if/else blocks
- Removed redundant env var pass-through (os.environ.copy() already
includes everything)
- Only clean up stale providers from dataset backup (keys no longer
in environment)
- Let OpenClaw handle provider discovery from env vars natively
Fixes #2
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- .env.example +11 -25
- scripts/sync_hf.py +37 -84
.env.example
CHANGED
|
@@ -71,30 +71,20 @@ OPENCLAW_DATASET_REPO=your-username/HuggingClaw-data
|
|
| 71 |
# SYNC_INTERVAL=60
|
| 72 |
|
| 73 |
|
| 74 |
-
# ─── LLM
|
| 75 |
#
|
| 76 |
-
#
|
| 77 |
-
# provider
|
|
|
|
| 78 |
#
|
| 79 |
-
#
|
| 80 |
-
# Use OPENAI_API_KEY alone for api.openai.com, or set OPENAI_BASE_URL for
|
| 81 |
-
# compatible endpoints (e.g. OpenRouter, local LLM servers, Azure OpenAI).
|
| 82 |
#
|
| 83 |
# [RECOMMENDED] At least one of the following for AI conversations
|
| 84 |
#
|
| 85 |
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
| 86 |
-
|
| 87 |
-
# Optional: base URL for OpenAI-compatible API (default: https://api.openai.com/v1)
|
| 88 |
-
# Examples: https://openrouter.ai/api/v1, http://localhost:11434/v1 (Ollama), etc.
|
| 89 |
-
#
|
| 90 |
# OPENAI_BASE_URL=https://api.openai.com/v1
|
| 91 |
-
|
| 92 |
-
#
|
| 93 |
-
#
|
| 94 |
-
OPENROUTER_API_KEY=sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
| 95 |
-
|
| 96 |
-
# Other providers (OpenClaw reads these from the environment)
|
| 97 |
-
#
|
| 98 |
# ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
| 99 |
# GOOGLE_API_KEY=AIza...
|
| 100 |
# MISTRAL_API_KEY=mis-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
@@ -185,14 +175,10 @@ OPENROUTER_API_KEY=sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
| 185 |
# HF_HUB_UPLOAD_TIMEOUT [可选] 上传超时(秒),默认 600
|
| 186 |
#
|
| 187 |
# ─── LLM / 对话 API(至少配置其一以启用 AI 对话)────────────────────────
|
| 188 |
-
#
|
| 189 |
-
#
|
| 190 |
-
# OPENROUTER_API_KEY
|
| 191 |
-
#
|
| 192 |
-
# GOOGLE_API_KEY [可选] Google / Gemini
|
| 193 |
-
# MISTRAL_API_KEY [可选] Mistral
|
| 194 |
-
# COHERE_API_KEY [可选] Cohere
|
| 195 |
-
# OPENCLAW_DEFAULT_MODEL [可选] 默认模型 ID
|
| 196 |
#
|
| 197 |
# ─── 消息渠道 ─────────────────────────────────────────────────────────
|
| 198 |
# Telegram、WhatsApp 等消息渠道均可在 Control UI 中配置,无需环境变量。
|
|
|
|
| 71 |
# SYNC_INTERVAL=60
|
| 72 |
|
| 73 |
|
| 74 |
+
# ─── LLM PROVIDERS ────────────────────────────────────────────────────────
|
| 75 |
#
|
| 76 |
+
# HuggingClaw passes ALL environment variables directly to OpenClaw.
|
| 77 |
+
# Any provider that OpenClaw supports works out of the box — just set the
|
| 78 |
+
# API key as a Secret in your HF Space settings.
|
| 79 |
#
|
| 80 |
+
# See OpenClaw docs: https://openclawdoc.com/docs/reference/environment-variables
|
|
|
|
|
|
|
| 81 |
#
|
| 82 |
# [RECOMMENDED] At least one of the following for AI conversations
|
| 83 |
#
|
| 84 |
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
# OPENAI_BASE_URL=https://api.openai.com/v1
|
| 86 |
+
# OPENROUTER_API_KEY=sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
| 87 |
+
# NVIDIA_API_KEY=nvapi-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
# ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
| 89 |
# GOOGLE_API_KEY=AIza...
|
| 90 |
# MISTRAL_API_KEY=mis-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
|
|
| 175 |
# HF_HUB_UPLOAD_TIMEOUT [可选] 上传超时(秒),默认 600
|
| 176 |
#
|
| 177 |
# ─── LLM / 对话 API(至少配置其一以启用 AI 对话)────────────────────────
|
| 178 |
+
# 所有 OpenClaw 支持的 API Key 环境变量均可直接使用,无需额外配置。
|
| 179 |
+
# 完整列表见 OpenClaw 官方文档。常见:
|
| 180 |
+
# OPENAI_API_KEY, OPENROUTER_API_KEY, NVIDIA_API_KEY, ANTHROPIC_API_KEY,
|
| 181 |
+
# GOOGLE_API_KEY, MISTRAL_API_KEY, COHERE_API_KEY, OPENCLAW_DEFAULT_MODEL
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
#
|
| 183 |
# ─── 消息渠道 ─────────────────────────────────────────────────────────
|
| 184 |
# Telegram、WhatsApp 等消息渠道均可在 Control UI 中配置,无需环境变量。
|
scripts/sync_hf.py
CHANGED
|
@@ -66,16 +66,6 @@ APP_DIR = Path("/app/openclaw")
|
|
| 66 |
# Use ".openclaw" - directly read/write the .openclaw folder in dataset
|
| 67 |
DATASET_PATH = ".openclaw"
|
| 68 |
|
| 69 |
-
# OpenAI-compatible API (OpenAI, OpenRouter, or any compatible endpoint)
|
| 70 |
-
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
| 71 |
-
OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1").rstrip("/")
|
| 72 |
-
|
| 73 |
-
# OpenRouter API key (optional; alternative to OPENAI_API_KEY + OPENAI_BASE_URL)
|
| 74 |
-
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
|
| 75 |
-
|
| 76 |
-
# Zhipu AI (z.ai) API key (optional; GLM-4 series, Anthropic-compatible endpoint)
|
| 77 |
-
ZHIPU_API_KEY = os.environ.get("ZHIPU_API_KEY", "")
|
| 78 |
-
|
| 79 |
# Z.AI API key (optional; used by Claude Code backend via api.z.ai)
|
| 80 |
ZAI_API_KEY = os.environ.get("ZAI_API_KEY", "")
|
| 81 |
|
|
@@ -86,12 +76,8 @@ GATEWAY_TOKEN = os.environ.get("GATEWAY_TOKEN", "huggingclaw")
|
|
| 86 |
AGENT_NAME = os.environ.get("AGENT_NAME", "HuggingClaw")
|
| 87 |
A2A_PEERS = os.environ.get("A2A_PEERS", "") # comma-separated peer URLs
|
| 88 |
|
| 89 |
-
# Default model
|
| 90 |
-
OPENCLAW_DEFAULT_MODEL = os.environ.get("OPENCLAW_DEFAULT_MODEL"
|
| 91 |
-
"openai/gpt-5-nano" if OPENAI_API_KEY
|
| 92 |
-
else "zhipu/glm-4.5-air" if ZHIPU_API_KEY
|
| 93 |
-
else "openrouter/openai/gpt-oss-20b:free"
|
| 94 |
-
)
|
| 95 |
|
| 96 |
# HF Spaces built-in env vars (auto-set by HF runtime)
|
| 97 |
SPACE_HOST = os.environ.get("SPACE_HOST", "") # e.g. "tao-shen-huggingclaw.hf.space"
|
|
@@ -363,20 +349,13 @@ class OpenClawFullSync:
|
|
| 363 |
# Set gateway token
|
| 364 |
if "gateway" in cfg:
|
| 365 |
cfg["gateway"]["auth"] = {"token": GATEWAY_TOKEN}
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
if
|
| 372 |
-
|
| 373 |
-
if OPENROUTER_API_KEY:
|
| 374 |
-
if "models" in cfg and "providers" in cfg["models"] and "openrouter" in cfg["models"]["providers"]:
|
| 375 |
-
cfg["models"]["providers"]["openrouter"]["apiKey"] = OPENROUTER_API_KEY
|
| 376 |
-
else:
|
| 377 |
-
if "models" in cfg and "providers" in cfg["models"]:
|
| 378 |
-
cfg["models"]["providers"].pop("openrouter", None)
|
| 379 |
-
print("[SYNC] No OPENROUTER_API_KEY — removed openrouter provider from config")
|
| 380 |
with open(config_path, "w") as f:
|
| 381 |
json.dump(cfg, f, indent=2)
|
| 382 |
except Exception as e:
|
|
@@ -396,7 +375,6 @@ class OpenClawFullSync:
|
|
| 396 |
}
|
| 397 |
},
|
| 398 |
"session": {"scope": "global"},
|
| 399 |
-
"models": {"mode": "merge", "providers": {}},
|
| 400 |
"agents": {"defaults": {"workspace": "~/.openclaw/workspace"}}
|
| 401 |
}, f)
|
| 402 |
print("[SYNC] Created minimal openclaw.json")
|
|
@@ -469,44 +447,30 @@ class OpenClawFullSync:
|
|
| 469 |
data.setdefault("agents", {}).setdefault("defaults", {}).setdefault("model", {})
|
| 470 |
data.setdefault("session", {})["scope"] = "global"
|
| 471 |
|
| 472 |
-
#
|
| 473 |
-
#
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
"apiKey": ZHIPU_API_KEY,
|
| 497 |
-
"api": "anthropic-messages",
|
| 498 |
-
"models": [
|
| 499 |
-
{"id": "glm-4.5-air", "name": "GLM-4.5 Air"},
|
| 500 |
-
{"id": "glm-4.5", "name": "GLM-4.5"},
|
| 501 |
-
{"id": "glm-4.6", "name": "GLM-4.6"},
|
| 502 |
-
{"id": "glm-4.7", "name": "GLM-4.7"},
|
| 503 |
-
]
|
| 504 |
-
}
|
| 505 |
-
print("[SYNC] Set Zhipu AI provider")
|
| 506 |
-
if not providers:
|
| 507 |
-
print("[SYNC] WARNING: No API key set (OPENAI/OPENROUTER/ZHIPU), LLM features may not work")
|
| 508 |
-
data.setdefault("models", {})["providers"] = providers
|
| 509 |
-
data["agents"]["defaults"]["model"]["primary"] = OPENCLAW_DEFAULT_MODEL
|
| 510 |
|
| 511 |
# ── ACP (Agent Client Protocol) — native Claude Code integration ──
|
| 512 |
data["acp"] = {
|
|
@@ -545,10 +509,10 @@ class OpenClawFullSync:
|
|
| 545 |
"targetSpace": CODING_TARGET_SPACE,
|
| 546 |
"targetDataset": CODING_TARGET_DATASET,
|
| 547 |
"hfToken": HF_TOKEN or "",
|
| 548 |
-
"zaiApiKey": ZAI_API_KEY or ZHIPU_API_KEY
|
| 549 |
}
|
| 550 |
}
|
| 551 |
-
print(f"[SYNC] Coding agent configured: space={CODING_TARGET_SPACE}, dataset={CODING_TARGET_DATASET}, zaiKey={'set' if (ZAI_API_KEY or ZHIPU_API_KEY) else 'missing'}")
|
| 552 |
if "telegram" not in data["plugins"]["entries"]:
|
| 553 |
data["plugins"]["entries"]["telegram"] = {"enabled": True}
|
| 554 |
elif isinstance(data["plugins"]["entries"]["telegram"], dict):
|
|
@@ -689,19 +653,8 @@ class OpenClawFullSync:
|
|
| 689 |
# Open log file
|
| 690 |
log_fh = open(log_file, "a")
|
| 691 |
|
| 692 |
-
#
|
| 693 |
env = os.environ.copy()
|
| 694 |
-
if OPENAI_API_KEY:
|
| 695 |
-
env["OPENAI_API_KEY"] = OPENAI_API_KEY
|
| 696 |
-
env["OPENAI_BASE_URL"] = OPENAI_BASE_URL
|
| 697 |
-
if OPENROUTER_API_KEY:
|
| 698 |
-
env["OPENROUTER_API_KEY"] = OPENROUTER_API_KEY
|
| 699 |
-
if ZHIPU_API_KEY:
|
| 700 |
-
env["ZHIPU_API_KEY"] = ZHIPU_API_KEY
|
| 701 |
-
if ZAI_API_KEY:
|
| 702 |
-
env["ZAI_API_KEY"] = ZAI_API_KEY
|
| 703 |
-
if not OPENAI_API_KEY and not OPENROUTER_API_KEY and not ZHIPU_API_KEY:
|
| 704 |
-
print(f"[SYNC] WARNING: No API key set (OPENAI/OPENROUTER/ZHIPU), LLM features may not work")
|
| 705 |
|
| 706 |
# ── Telegram API base probe ──────────────────────────────────────
|
| 707 |
# Determine working Telegram API endpoint and set env var for
|
|
@@ -731,7 +684,7 @@ class OpenClawFullSync:
|
|
| 731 |
stderr=subprocess.STDOUT,
|
| 732 |
text=True,
|
| 733 |
bufsize=1, # Line buffered
|
| 734 |
-
env=env # Pass environment
|
| 735 |
)
|
| 736 |
|
| 737 |
# Create a thread to copy output to log file; only print key lines to console
|
|
|
|
| 66 |
# Use ".openclaw" - directly read/write the .openclaw folder in dataset
|
| 67 |
DATASET_PATH = ".openclaw"
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
# Z.AI API key (optional; used by Claude Code backend via api.z.ai)
|
| 70 |
ZAI_API_KEY = os.environ.get("ZAI_API_KEY", "")
|
| 71 |
|
|
|
|
| 76 |
AGENT_NAME = os.environ.get("AGENT_NAME", "HuggingClaw")
|
| 77 |
A2A_PEERS = os.environ.get("A2A_PEERS", "") # comma-separated peer URLs
|
| 78 |
|
| 79 |
+
# Default model — only set if user explicitly provides it; otherwise let OpenClaw decide.
|
| 80 |
+
OPENCLAW_DEFAULT_MODEL = os.environ.get("OPENCLAW_DEFAULT_MODEL", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
# HF Spaces built-in env vars (auto-set by HF runtime)
|
| 83 |
SPACE_HOST = os.environ.get("SPACE_HOST", "") # e.g. "tao-shen-huggingclaw.hf.space"
|
|
|
|
| 349 |
# Set gateway token
|
| 350 |
if "gateway" in cfg:
|
| 351 |
cfg["gateway"]["auth"] = {"token": GATEWAY_TOKEN}
|
| 352 |
+
# Don't manage providers — leave them for OpenClaw to handle.
|
| 353 |
+
# Just remove template entries with unresolved ${...} placeholders
|
| 354 |
+
# so OpenClaw starts clean and discovers providers from env vars.
|
| 355 |
+
providers = cfg.get("models", {}).get("providers", {})
|
| 356 |
+
for pid in list(providers.keys()):
|
| 357 |
+
if str(providers[pid].get("apiKey", "")).startswith("${"):
|
| 358 |
+
providers.pop(pid, None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
with open(config_path, "w") as f:
|
| 360 |
json.dump(cfg, f, indent=2)
|
| 361 |
except Exception as e:
|
|
|
|
| 375 |
}
|
| 376 |
},
|
| 377 |
"session": {"scope": "global"},
|
|
|
|
| 378 |
"agents": {"defaults": {"workspace": "~/.openclaw/workspace"}}
|
| 379 |
}, f)
|
| 380 |
print("[SYNC] Created minimal openclaw.json")
|
|
|
|
| 447 |
data.setdefault("agents", {}).setdefault("defaults", {}).setdefault("model", {})
|
| 448 |
data.setdefault("session", {})["scope"] = "global"
|
| 449 |
|
| 450 |
+
# ── Provider cleanup ─────────────────────────────────────────────
|
| 451 |
+
# Don't manage providers — let OpenClaw handle them natively from
|
| 452 |
+
# its own env var support. We only clean up stale providers that
|
| 453 |
+
# were restored from the dataset backup with hardcoded API keys
|
| 454 |
+
# that no longer exist in the current environment.
|
| 455 |
+
restored_providers = data.get("models", {}).get("providers", {})
|
| 456 |
+
if restored_providers:
|
| 457 |
+
stale = []
|
| 458 |
+
for pid, pcfg in list(restored_providers.items()):
|
| 459 |
+
api_key = pcfg.get("apiKey", "")
|
| 460 |
+
# Skip placeholder-style keys (${VAR}) — OpenClaw resolves these
|
| 461 |
+
if not api_key or str(api_key).startswith("${"):
|
| 462 |
+
continue
|
| 463 |
+
# Hardcoded key from backup — check if it's still in env
|
| 464 |
+
# We can't know which env var it came from, so check if
|
| 465 |
+
# the exact key value exists in any current env var
|
| 466 |
+
if api_key not in os.environ.values():
|
| 467 |
+
stale.append(pid)
|
| 468 |
+
for pid in stale:
|
| 469 |
+
del restored_providers[pid]
|
| 470 |
+
print(f"[SYNC] Removed stale provider '{pid}' (API key no longer in environment)")
|
| 471 |
+
|
| 472 |
+
if OPENCLAW_DEFAULT_MODEL:
|
| 473 |
+
data["agents"]["defaults"]["model"]["primary"] = OPENCLAW_DEFAULT_MODEL
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
|
| 475 |
# ── ACP (Agent Client Protocol) — native Claude Code integration ──
|
| 476 |
data["acp"] = {
|
|
|
|
| 509 |
"targetSpace": CODING_TARGET_SPACE,
|
| 510 |
"targetDataset": CODING_TARGET_DATASET,
|
| 511 |
"hfToken": HF_TOKEN or "",
|
| 512 |
+
"zaiApiKey": ZAI_API_KEY or os.environ.get("ZHIPU_API_KEY", ""),
|
| 513 |
}
|
| 514 |
}
|
| 515 |
+
print(f"[SYNC] Coding agent configured: space={CODING_TARGET_SPACE}, dataset={CODING_TARGET_DATASET}, zaiKey={'set' if (ZAI_API_KEY or os.environ.get('ZHIPU_API_KEY')) else 'missing'}")
|
| 516 |
if "telegram" not in data["plugins"]["entries"]:
|
| 517 |
data["plugins"]["entries"]["telegram"] = {"enabled": True}
|
| 518 |
elif isinstance(data["plugins"]["entries"]["telegram"], dict):
|
|
|
|
| 653 |
# Open log file
|
| 654 |
log_fh = open(log_file, "a")
|
| 655 |
|
| 656 |
+
# Pass entire environment to OpenClaw (all API keys are already in os.environ)
|
| 657 |
env = os.environ.copy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
|
| 659 |
# ── Telegram API base probe ──────────────────────────────────────
|
| 660 |
# Determine working Telegram API endpoint and set env var for
|
|
|
|
| 684 |
stderr=subprocess.STDOUT,
|
| 685 |
text=True,
|
| 686 |
bufsize=1, # Line buffered
|
| 687 |
+
env=env # Pass full environment (all API keys)
|
| 688 |
)
|
| 689 |
|
| 690 |
# Create a thread to copy output to log file; only print key lines to console
|