anurag008w commited on
Commit
5d11752
Β·
1 Parent(s): 33980b3

Prune stale dataset files after sync upload

Browse files
.env.example CHANGED
@@ -132,7 +132,10 @@ LLM_MODEL=anthropic/claude-sonnet-4-5
132
  # have multiple accounts or want to spread rate-limit quota.
133
  #
134
  # Pattern: <PROVIDER>_API_KEYS=key1,key2,key3
135
- # Fallback order: plural pool β†’ singular key β†’ LLM_API_KEY
 
 
 
136
  #
137
  # Uncomment and fill in only the providers you use:
138
  #
@@ -172,6 +175,13 @@ LLM_MODEL=anthropic/claude-sonnet-4-5
172
  # is also picked up by OpenClaw β†’ that provider's models become
173
  # available in the Control UI for manual selection.
174
  # 3. Rotation pools (*_API_KEYS) work for every active provider
 
 
 
 
 
 
 
175
  # independently and in parallel.
176
  #
177
  # EXAMPLE β€” default Anthropic, also use OpenAI and Groq:
 
132
  # have multiple accounts or want to spread rate-limit quota.
133
  #
134
  # Pattern: <PROVIDER>_API_KEYS=key1,key2,key3
135
+ # Fallback order: plural pool β†’ singular key β†’ LLM_API_KEY (optional)
136
+ # Set false only if you want to disable global LLM_API_KEY fallback
137
+ # across providers.
138
+ LLM_API_KEY_FALLBACK_ENABLED=true
139
  #
140
  # Uncomment and fill in only the providers you use:
141
  #
 
175
  # is also picked up by OpenClaw β†’ that provider's models become
176
  # available in the Control UI for manual selection.
177
  # 3. Rotation pools (*_API_KEYS) work for every active provider
178
+ #
179
+ # Optional: explicitly pin model lists per provider for Control UI visibility
180
+ # when provider keys are configured.
181
+ # Format: comma-separated model IDs
182
+ # NVIDIA_MODELS=meta/llama-3.1-70b-instruct,nvidia/llama-3.1-nemotron-70b-instruct
183
+ # OPENAI_MODELS=gpt-4o-mini,gpt-4.1
184
+ # GROQ_MODELS=llama-3.3-70b-versatile,deepseek-r1-distill-llama-70b
185
  # independently and in parallel.
186
  #
187
  # EXAMPLE β€” default Anthropic, also use OpenAI and Groq:
CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
 
3
  All notable changes to this project will be documented in this file.
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  ## [1.4.0] - 2026-04-25
6
 
7
  ### Added
 
2
 
3
  All notable changes to this project will be documented in this file.
4
 
5
+ ## [1.5.0] - 2026-05-13
6
+
7
+ ### Added
8
+
9
+ - **NVIDIA key-rotation support** β€” added `nvidia-key-rotator.cjs` wiring and startup integration so deployments can rotate NVIDIA credentials similarly to other provider key-rotation flows.
10
+ - **Cloudflare keep-alive automation** β€” added/expanded `cloudflare-keepalive-setup.py` flow and startup wiring to provision keep-alive through Cloudflare Worker automation instead of the older UptimeRobot-first approach.
11
+ - **Sync metadata marker model** β€” introduced a structured workspace marker `(file_count, total_size, newest_mtime, metadata_hash)` to support stronger change introspection in sync code.
12
+
13
+ ### Changed
14
+
15
+ - **Workspace sync script rename finalized** β€” `workspace-sync.py` flow was migrated to `openclaw-sync.py` in Docker/startup/docs so restore/sync behavior is centralized under one script.
16
+ - **Sync trigger behavior hardened for config churn** β€” OpenClaw config sync now debounces until JSON settles before immediate sync, reducing false/partial syncs during rapid config writes.
17
+ - **Gateway restart flow now saves state first** β€” restart path was updated to run a pre-restart one-shot state sync so gateway reloads are less likely to drop recent state.
18
+ - **Shutdown backup now uses a two-step pass** β€” graceful shutdown now attempts `sync-once-settled` then a final `sync-once` pass to better capture last-second writes.
19
+ - **Telegram allowlist simplified** β€” consolidated Telegram allowlist into `TELEGRAM_ALLOWED_USERS` and aligned docs/examples.
20
+ - **Plugin startup behavior aligned** β€” startup-installed plugins are synced into `plugins.allow` before gateway launch so runtime-installed plugins are recognized cleanly.
21
+ - **Cloudflare proxy path matured** β€” multiple iterations improved fetch/proxy behavior (header handling, endpoint scoping, API root routing, URL parsing, and logging noise reduction), then simplified unstable undici patching paths.
22
+ - **Health dashboard polish** β€” sync timestamps now show local time, footer credits were corrected, and status rendering/docs were updated for the Cloudflare keep-alive model.
23
+ - **CI workflow churn documented** β€” GitHub workflow files for HF sync were added/renamed/cleaned multiple times as space/repo naming stabilized.
24
+
25
+ ### Fixed
26
+
27
+ - **Missed rapid backup updates** β€” sync logic now relies on content fingerprint checks for no-op decisions so same-second or quick successive changes are less likely to be skipped.
28
+ - **Non-deterministic metadata hashing** β€” metadata hashing now iterates paths deterministically to avoid hash jitter from traversal order.
29
+ - **Transient file race sync failures** β€” sync fingerprinting/snapshot copy paths now tolerate transient `OSError` (file rotated/deleted mid-scan) instead of aborting the whole sync pass.
30
+ - **State restore migration edge cases** β€” restore flow includes migration/cleanup behavior for legacy hidden state paths and stale backup entries.
31
+ - **Startup/env robustness** β€” fixed shell export formatting/syntax issues (e.g., NVIDIA/XAI lines) and unbound-variable pitfalls in startup scripts.
32
+ - **Proxy runtime errors and noise** β€” fixed specific proxy runtime issues (including `UND_ERR_INVALID_ARG`, fetch duplex handling, and upstream error visibility) and reduced noisy stdout logs that interfered with clean process output.
33
+ - **HF workflow/repo reference mismatches** β€” corrected and later cleaned workflow repository references during repo migration/restructure.
34
+
35
+ ### Docs
36
+
37
+ - README/.env/security docs were refreshed across multiple commits to reflect:
38
+ - Cloudflare keep-alive replacing UptimeRobot setup path,
39
+ - updated secrets and startup environment behavior,
40
+ - provider/key-rotation options,
41
+ - backup/sync behavior and troubleshooting guidance.
42
+
43
  ## [1.4.0] - 2026-04-25
44
 
45
  ### Added
ENV_VARIABLES_FULL_LIST.md ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ENV Variables Full List (Repo Scope)
2
+
3
+ > Internal reference file. Not linked from README/docs navigation.
4
+
5
+ ## 1) User-settable (Space Secrets / `.env`)
6
+
7
+ These are the main variables you can set directly.
8
+
9
+ ### Core startup/auth
10
+ - `LLM_API_KEY` β€” string API key. Example: `LLM_API_KEY=sk-ant-xxxx`
11
+ - `LLM_MODEL` β€” `provider/model-id` string. Examples:
12
+ - `LLM_MODEL=anthropic/claude-sonnet-4-5`
13
+ - `LLM_MODEL=nvidia/meta/llama-3.1-70b-instruct`
14
+ - `GATEWAY_TOKEN` β€” random secret string. Example: `GATEWAY_TOKEN=9d2f...`
15
+ - `OPENCLAW_PASSWORD` β€” plaintext password string. Example: `OPENCLAW_PASSWORD=MyStrongPass123`
16
+
17
+ ### Provider fallback/selection
18
+ - `LLM_API_KEY_FALLBACK_ENABLED` β€” boolean-like:
19
+ - enabled: `true`, `1`, `yes`, `on`
20
+ - disabled: `false`, `0`, `no`, `off`
21
+ - default/example: `LLM_API_KEY_FALLBACK_ENABLED=true`
22
+
23
+ ### Provider-specific keys (single-key form)
24
+ Use plain provider key strings. Examples:
25
+ - `NVIDIA_API_KEY=nvapi-xxxx`
26
+ - `OPENAI_API_KEY=sk-xxxx`
27
+ - `GEMINI_API_KEY=AIzaSyxxxx`
28
+
29
+ Full list:
30
+ - `ANTHROPIC_API_KEY`
31
+ - `OPENAI_API_KEY`
32
+ - `GEMINI_API_KEY`
33
+ - `DEEPSEEK_API_KEY`
34
+ - `OPENROUTER_API_KEY`
35
+ - `KILOCODE_API_KEY`
36
+ - `OPENCODE_API_KEY`
37
+ - `ZAI_API_KEY`
38
+ - `MOONSHOT_API_KEY`
39
+ - `KIMI_API_KEY`
40
+ - `MINIMAX_API_KEY`
41
+ - `MODELSTUDIO_API_KEY`
42
+ - `XIAOMI_API_KEY`
43
+ - `VOLCANO_ENGINE_API_KEY`
44
+ - `BYTEPLUS_API_KEY`
45
+ - `QIANFAN_API_KEY`
46
+ - `MISTRAL_API_KEY`
47
+ - `XAI_API_KEY`
48
+ - `NVIDIA_API_KEY`
49
+ - `COHERE_API_KEY`
50
+ - `GROQ_API_KEY`
51
+ - `TOGETHER_API_KEY`
52
+ - `CEREBRAS_API_KEY`
53
+ - `VENICE_API_KEY`
54
+ - `SYNTHETIC_API_KEY`
55
+ - `COPILOT_GITHUB_TOKEN`
56
+ - `HUGGINGFACE_HUB_TOKEN`
57
+ - `AI_GATEWAY_API_KEY`
58
+
59
+ ### Provider key pools (comma-separated rotation)
60
+ Format: comma-separated keys, no spaces required. Examples:
61
+ - `NVIDIA_API_KEYS=nvapi-key1,nvapi-key2`
62
+ - `OPENAI_API_KEYS=sk-a,sk-b,sk-c`
63
+
64
+ Full list:
65
+ - `ANTHROPIC_API_KEYS`
66
+ - `OPENAI_API_KEYS`
67
+ - `GEMINI_API_KEYS`
68
+ - `DEEPSEEK_API_KEYS`
69
+ - `OPENROUTER_API_KEYS`
70
+ - `KILOCODE_API_KEYS`
71
+ - `OPENCODE_API_KEYS`
72
+ - `ZAI_API_KEYS`
73
+ - `MOONSHOT_API_KEYS`
74
+ - `MINIMAX_API_KEYS`
75
+ - `XIAOMI_API_KEYS`
76
+ - `VOLCANO_ENGINE_API_KEYS`
77
+ - `BYTEPLUS_API_KEYS`
78
+ - `QIANFAN_API_KEYS`
79
+ - `MODELSTUDIO_API_KEYS`
80
+ - `KIMI_API_KEYS`
81
+ - `MISTRAL_API_KEYS`
82
+ - `XAI_API_KEYS`
83
+ - `NVIDIA_API_KEYS`
84
+ - `GROQ_API_KEYS`
85
+ - `COHERE_API_KEYS`
86
+ - `TOGETHER_API_KEYS`
87
+ - `CEREBRAS_API_KEYS`
88
+ - `HUGGINGFACE_HUB_TOKENS`
89
+
90
+ ### Provider model lists (optional, for Control UI visibility)
91
+ Format: comma-separated model IDs; used only when that provider has key(s) configured.
92
+ Examples:
93
+ - `NVIDIA_MODELS=meta/llama-3.1-70b-instruct,nvidia/llama-3.1-nemotron-70b-instruct`
94
+ - `OPENAI_MODELS=gpt-4o-mini,gpt-4.1`
95
+ - `GROQ_MODELS=llama-3.3-70b-versatile,deepseek-r1-distill-llama-70b`
96
+
97
+ Supported:
98
+ - `ANTHROPIC_MODELS`
99
+ - `OPENAI_MODELS`
100
+ - `GEMINI_MODELS`
101
+ - `DEEPSEEK_MODELS`
102
+ - `OPENROUTER_MODELS`
103
+ - `KILOCODE_MODELS`
104
+ - `OPENCODE_MODELS`
105
+ - `ZAI_MODELS`
106
+ - `MOONSHOT_MODELS`
107
+ - `KIMI_MODELS`
108
+ - `MINIMAX_MODELS`
109
+ - `MODELSTUDIO_MODELS`
110
+ - `XIAOMI_MODELS`
111
+ - `VOLCANO_ENGINE_MODELS`
112
+ - `BYTEPLUS_MODELS`
113
+ - `QIANFAN_MODELS`
114
+ - `GROQ_MODELS`
115
+ - `MISTRAL_MODELS`
116
+ - `XAI_MODELS`
117
+ - `NVIDIA_MODELS`
118
+ - `COHERE_MODELS`
119
+ - `TOGETHER_MODELS`
120
+ - `CEREBRAS_MODELS`
121
+
122
+ ### Telegram / WhatsApp
123
+ - `TELEGRAM_BOT_TOKEN` β€” bot token string. Example: `TELEGRAM_BOT_TOKEN=123456:ABC...`
124
+ - `TELEGRAM_ALLOWED_USERS` β€” comma-separated numeric IDs. Example: `TELEGRAM_ALLOWED_USERS=12345,67890`
125
+ - `TELEGRAM_USER_ID` β€” legacy single numeric ID. Example: `TELEGRAM_USER_ID=12345`
126
+ - `TELEGRAM_USER_IDS` β€” legacy comma-separated IDs. Example: `TELEGRAM_USER_IDS=12345,67890`
127
+ - `WHATSAPP_ENABLED` β€” boolean-like (`true/false`, `1/0`, `yes/no`, `on/off`). Example: `WHATSAPP_ENABLED=true`
128
+
129
+ ### Backup/sync
130
+ - `HF_TOKEN` β€” HF token string. Example: `HF_TOKEN=hf_xxxxx`
131
+ - `HF_USERNAME` β€” HF username string. Example: `HF_USERNAME=myhandle`
132
+ - `BACKUP_DATASET_NAME` β€” dataset slug string. Example: `BACKUP_DATASET_NAME=huggingclaw-backup`
133
+ - `SYNC_INTERVAL` β€” integer seconds. Example: `SYNC_INTERVAL=180`
134
+ - `SYNC_START_DELAY` β€” integer seconds. Example: `SYNC_START_DELAY=10`
135
+ - `SYNC_MAX_FILE_BYTES` β€” integer bytes. Example: `SYNC_MAX_FILE_BYTES=52428800`
136
+ - `OPENCLAW_CONFIG_WATCH_INTERVAL` β€” number (supports decimal seconds). Example: `OPENCLAW_CONFIG_WATCH_INTERVAL=1`
137
+ - `OPENCLAW_CONFIG_SETTLE_SECONDS` β€” number (supports decimal seconds). Example: `OPENCLAW_CONFIG_SETTLE_SECONDS=3`
138
+
139
+ ### Gateway/runtime tuning
140
+ - `GATEWAY_HOST` β€” hostname/IP string. Example: `GATEWAY_HOST=127.0.0.1`
141
+ - `GATEWAY_PORT` β€” integer port. Example: `GATEWAY_PORT=3456`
142
+ - `GATEWAY_VERBOSE` β€” boolean-like. Example: `GATEWAY_VERBOSE=true`
143
+ - `GATEWAY_READY_TIMEOUT` β€” integer seconds. Example: `GATEWAY_READY_TIMEOUT=60`
144
+ - `GATEWAY_MAX_RESTARTS` β€” integer count. Example: `GATEWAY_MAX_RESTARTS=10`
145
+ - `GATEWAY_RESTART_DELAY` β€” integer seconds. Example: `GATEWAY_RESTART_DELAY=2`
146
+ - `TRUSTED_PROXIES` β€” comma-separated CIDR/IP list. Example: `TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8`
147
+ - `ALLOWED_ORIGINS` β€” comma-separated origins. Example: `ALLOWED_ORIGINS=https://a.com,https://b.com`
148
+ - `PORT` β€” integer external port. Example: `PORT=7860`
149
+
150
+ ### Browser/plugin controls
151
+ - `BROWSER_PLUGIN_MODE` β€” mode string (`auto`, `builtin`, `external`, etc. depending on runtime). Example: `BROWSER_PLUGIN_MODE=auto`
152
+ - `BROWSER_EXECUTABLE_PATH` β€” absolute path. Example: `BROWSER_EXECUTABLE_PATH=/usr/bin/chromium`
153
+ - `BROWSER_DISABLED` β€” boolean-like. Example: `BROWSER_DISABLED=false`
154
+ - `ACP_PLUGIN_MODE` β€” mode string. Example: `ACP_PLUGIN_MODE=auto`
155
+ - `ACPX_DISABLED` β€” boolean-like. Example: `ACPX_DISABLED=true`
156
+
157
+ ### OpenClaw logging/behavior
158
+ - `OPENCLAW_VERSION` β€” image/app version string. Example: `OPENCLAW_VERSION=latest`
159
+ - `OPENCLAW_RUNTIME_VERSION` β€” runtime tag string. Example: `OPENCLAW_RUNTIME_VERSION=v1.2.3`
160
+ - `OPENCLAW_DISPLAY_VERSION` β€” display label string. Example: `OPENCLAW_DISPLAY_VERSION=Custom`
161
+ - `OPENCLAW_DISABLE_BONJOUR` β€” boolean-like. Example: `OPENCLAW_DISABLE_BONJOUR=true`
162
+ - `OPENCLAW_CONSOLE_LOG_LEVEL` β€” log level (`trace|debug|info|warn|error`). Example: `OPENCLAW_CONSOLE_LOG_LEVEL=warn`
163
+ - `OPENCLAW_CONSOLE_LOG_STYLE` β€” style string. Example: `OPENCLAW_CONSOLE_LOG_STYLE=pretty`
164
+ - `OPENCLAW_FILE_LOG_LEVEL` β€” log level (`trace|debug|info|warn|error`). Example: `OPENCLAW_FILE_LOG_LEVEL=info`
165
+
166
+ ### Custom OpenAI-compatible provider
167
+ - `CUSTOM_PROVIDER_NAME` β€” unique provider prefix. Example: `CUSTOM_PROVIDER_NAME=modal`
168
+ - `CUSTOM_BASE_URL` β€” base URL only (not `/chat/completions`). Example: `CUSTOM_BASE_URL=https://api.modal.com/v1`
169
+ - `CUSTOM_MODEL_ID` β€” model id string. Example: `CUSTOM_MODEL_ID=zai-org/GLM-5.1-FP8`
170
+ - `CUSTOM_MODEL_NAME` β€” display name. Example: `CUSTOM_MODEL_NAME=GLM 5.1 FP8`
171
+ - `CUSTOM_API_KEY` β€” provider key string. Example: `CUSTOM_API_KEY=sk-xxxx`
172
+ - `CUSTOM_API_TYPE` β€” API mode string. Example: `CUSTOM_API_TYPE=openai-completions`
173
+ - `CUSTOM_CONTEXT_WINDOW` β€” integer token window. Example: `CUSTOM_CONTEXT_WINDOW=128000`
174
+ - `CUSTOM_MAX_TOKENS` β€” integer max tokens. Example: `CUSTOM_MAX_TOKENS=500`
175
+
176
+ ### Cloudflare proxy/keepalive
177
+ - `CLOUDFLARE_WORKERS_TOKEN` β€” API token string. Example: `CLOUDFLARE_WORKERS_TOKEN=cf_xxx`
178
+ - `CLOUDFLARE_WORKER_NAME` β€” worker name string. Example: `CLOUDFLARE_WORKER_NAME=huggingclaw-proxy`
179
+ - `CLOUDFLARE_ACCOUNT_ID` β€” account id string. Example: `CLOUDFLARE_ACCOUNT_ID=abc123...`
180
+ - `CLOUDFLARE_PROXY_URL` β€” full proxy URL. Example: `CLOUDFLARE_PROXY_URL=https://my-worker.workers.dev`
181
+ - `CLOUDFLARE_PROXY_SECRET` β€” shared secret string. Example: `CLOUDFLARE_PROXY_SECRET=long-random-secret`
182
+ - `CLOUDFLARE_PROXY_DOMAINS` β€” comma-separated domains. Example: `CLOUDFLARE_PROXY_DOMAINS=api.openai.com,api.anthropic.com`
183
+ - `CLOUDFLARE_PROXY_DEBUG` β€” boolean-like. Example: `CLOUDFLARE_PROXY_DEBUG=false`
184
+ - `CLOUDFLARE_KEEPALIVE_ENABLED` β€” boolean-like. Example: `CLOUDFLARE_KEEPALIVE_ENABLED=true`
185
+ - `CLOUDFLARE_KEEPALIVE_URL` β€” URL string. Example: `CLOUDFLARE_KEEPALIVE_URL=https://space-url.hf.space`
186
+ - `CLOUDFLARE_KEEPALIVE_CRON` β€” cron string. Example: `CLOUDFLARE_KEEPALIVE_CRON=*/5 * * * *`
187
+ - `CLOUDFLARE_KEEPALIVE_WORKER_NAME` β€” worker name string. Example: `CLOUDFLARE_KEEPALIVE_WORKER_NAME=huggingclaw-keepalive`
188
+
189
+ ### Startup command replay/install helpers
190
+ - `HUGGINGCLAW_RUN` β€” shell commands string. Example: `HUGGINGCLAW_RUN=apt-get update && apt-get install -y ffmpeg`
191
+ - `HUGGINGCLAW_APT_PACKAGES` β€” comma/space separated package names. Example: `HUGGINGCLAW_APT_PACKAGES=ffmpeg,git`
192
+ - `HUGGINGCLAW_PIP_PACKAGES` β€” pip package list string. Example: `HUGGINGCLAW_PIP_PACKAGES=yt-dlp,requests`
193
+ - `HUGGINGCLAW_NPM_PACKAGES` β€” npm package list string. Example: `HUGGINGCLAW_NPM_PACKAGES=sharp,axios`
194
+ - `HUGGINGCLAW_OPENCLAW_PLUGINS` β€” plugin ids list string. Example: `HUGGINGCLAW_OPENCLAW_PLUGINS=@openclaw/telegram,@openclaw/whatsapp`
195
+ - `HUGGINGCLAW_STARTUP_SCRIPT` β€” shell script string. Example: `HUGGINGCLAW_STARTUP_SCRIPT=echo hello`
196
+ - `HUGGINGCLAW_STARTUP_SCRIPT_B64` β€” base64 script content. Example: `HUGGINGCLAW_STARTUP_SCRIPT_B64=IyEvYmluL2Jhc2gKZWNobyBoaQ==`
197
+ - `HUGGINGCLAW_STARTUP_COMMANDS` β€” newline/semicolon commands string.
198
+ - `HUGGINGCLAW_STARTUP_STRICT` β€” boolean-like. Example: `HUGGINGCLAW_STARTUP_STRICT=true`
199
+ - `WEBHOOK_URL` β€” webhook endpoint URL. Example: `WEBHOOK_URL=https://hooks.example.com/hc`
200
+
201
+ ---
202
+
203
+ ## 2) Runtime/Internal envs (normally do **not** set manually)
204
+
205
+ These are mostly derived/temporary/process variables used by scripts:
206
+
207
+ - `APP_BASE`
208
+ - `BACKUP_DATASET`
209
+ - `BROWSER_SHOULD_ENABLE`
210
+ - `CF_PROXY_ENV_FILE`
211
+ - `CLEAN_TG_TOKEN`
212
+ - `CONFIG_JSON`
213
+ - `CUSTOM_BASE_URL_NORMALIZED`
214
+ - `CUSTOM_PROVIDER_NORMALIZED`
215
+ - `CUSTOM_PROVIDER_OK`
216
+ - `DEBIAN_FRONTEND`
217
+ - `ERRORS`
218
+ - `EXISTING_CONFIG`
219
+ - `GATEWAY_EXIT_CODE`
220
+ - `GATEWAY_PID`
221
+ - `GATEWAY_RESTART_COUNT`
222
+ - `GUARDIAN_PID`
223
+ - `HC_STARTUP_FAILURES`
224
+ - `HC_STARTUP_INDEX`
225
+ - `HC_STARTUP_STRICT_NORMALIZED`
226
+ - `HC_STARTUP_VAR`
227
+ - `HOME`
228
+ - `IDS_JSON`
229
+ - `INSTALLS`
230
+ - `LLM_PROVIDER`
231
+ - `NODE_OPTIONS`
232
+ - `NPM_CONFIG_PREFIX`
233
+ - `OPENCLAW_APP_DIR`
234
+ - `OPENCLAW_CONSOLE_LOG_LEVEL_CONFIGURED`
235
+ - `OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED`
236
+ - `OPENCLAW_FILE_LOG_LEVEL_CONFIGURED`
237
+ - `ORIGINS_JSON`
238
+ - `PATCHED`
239
+ - `PATH`
240
+ - `PLUGIN_ALLOW_JSON`
241
+ - `PROXIES_JSON`
242
+ - `PROXY_URL`
243
+ - `PYTHONUSERBASE`
244
+ - `RESET_MARKER_PATH`
245
+ - `SPACE_AUTHOR_NAME`
246
+ - `SPACE_HOST`
247
+ - `SPACE_REPO_NAME`
248
+ - `STARTUP_FILE`
249
+ - `SYNC_LOOP_PID`
250
+ - `VIRTUAL_ENV`
251
+ - `WEBHOOK_BODY`
252
+ - `WHATSAPP_CONFIG_ENABLED`
253
+ - `WHATSAPP_ENABLED_CONFIGURED`
254
+ - `WHATSAPP_ENABLED_NORMALIZED`
README.md CHANGED
@@ -250,10 +250,10 @@ GEMINI_API_KEYS=AIza-key1,AIza-key2
250
  **Fallback chain** (per provider):
251
  1. `{PROVIDER}_API_KEYS` β€” comma-separated pool *(preferred)*
252
  2. `{PROVIDER}_API_KEY` β€” single dedicated key
253
- 3. `LLM_API_KEY` β€” universal fallback *(default for all providers)*
254
 
255
  > [!TIP]
256
- > If you only set `LLM_API_KEY`, all providers use it as a fallback automatically β€” no extra config needed. Add per-provider pools only when you need multi-key rotation.
257
 
258
  Supported per-provider variables: `ANTHROPIC_API_KEYS`, `OPENAI_API_KEYS`, `GEMINI_API_KEYS`, `DEEPSEEK_API_KEYS`, `GROQ_API_KEYS`, `MISTRAL_API_KEYS`, `OPENROUTER_API_KEYS`, `XAI_API_KEYS`, `NVIDIA_API_KEYS`, `COHERE_API_KEYS`, `TOGETHER_API_KEYS`, `CEREBRAS_API_KEYS`, and more β€” see `.env.example` for the full list.
259
 
 
250
  **Fallback chain** (per provider):
251
  1. `{PROVIDER}_API_KEYS` β€” comma-separated pool *(preferred)*
252
  2. `{PROVIDER}_API_KEY` β€” single dedicated key
253
+ 3. `LLM_API_KEY` β€” universal fallback *(enabled by default; disable with `LLM_API_KEY_FALLBACK_ENABLED=false`)*
254
 
255
  > [!TIP]
256
+ > By default, `LLM_API_KEY` fallback is enabled for compatibility. Set `LLM_API_KEY_FALLBACK_ENABLED=false` if you want strict provider-only activation.
257
 
258
  Supported per-provider variables: `ANTHROPIC_API_KEYS`, `OPENAI_API_KEYS`, `GEMINI_API_KEYS`, `DEEPSEEK_API_KEYS`, `GROQ_API_KEYS`, `MISTRAL_API_KEYS`, `OPENROUTER_API_KEYS`, `XAI_API_KEYS`, `NVIDIA_API_KEYS`, `COHERE_API_KEYS`, `TOGETHER_API_KEYS`, `CEREBRAS_API_KEYS`, and more β€” see `.env.example` for the full list.
259
 
multi-provider-key-rotator.cjs CHANGED
@@ -8,7 +8,7 @@
8
  *
9
  * For each provider you can supply a comma-separated pool:
10
  * ANTHROPIC_API_KEYS=key1,key2,key3
11
- * Falls back to the singular env var, then to LLM_API_KEY.
12
  *
13
  * Keys are rotated round-robin per provider independently.
14
  *
@@ -30,7 +30,9 @@ const log = (...args) => console.error(...args);
30
  // envPlural – env var that holds a comma-separated key pool (preferred)
31
  // envSingular – env var that holds a single key (fallback)
32
  //
33
- // LLM_API_KEY is the final fallback for every provider.
 
 
34
  //
35
  const PROVIDERS = [
36
  {
@@ -180,6 +182,8 @@ function normalizeKeys(...inputs) {
180
 
181
  // Build per-provider key pools + rotation indices
182
  const providerState = PROVIDERS.map(p => {
 
 
183
  const dedicatedKeys = normalizeKeys(
184
  process.env[p.envPlural] || '',
185
  process.env[p.envSingular] || '',
@@ -187,7 +191,11 @@ const providerState = PROVIDERS.map(p => {
187
  const hasDedicated = dedicatedKeys.length > 0;
188
  const keys = hasDedicated
189
  ? dedicatedKeys
190
- : normalizeKeys(process.env.LLM_API_KEY || '');
 
 
 
 
191
 
192
  if (hasDedicated) {
193
  log(`[key-rotator] ${p.name}: ${keys.length} key${keys.length === 1 ? '' : 's'}`);
@@ -208,6 +216,8 @@ const fallbackCount = providerState.filter(p => {
208
  }).length;
209
  if (fallbackCount > 0) {
210
  log(`[key-rotator] ${fallbackCount} provider(s) using LLM_API_KEY fallback`);
 
 
211
  }
212
 
213
  // ─── Runtime helpers ─────────────────────────────────────────────────────────
 
8
  *
9
  * For each provider you can supply a comma-separated pool:
10
  * ANTHROPIC_API_KEYS=key1,key2,key3
11
+ * Falls back to the singular env var, and optionally to LLM_API_KEY.
12
  *
13
  * Keys are rotated round-robin per provider independently.
14
  *
 
30
  // envPlural – env var that holds a comma-separated key pool (preferred)
31
  // envSingular – env var that holds a single key (fallback)
32
  //
33
+ // LLM_API_KEY fallback can be controlled via:
34
+ // LLM_API_KEY_FALLBACK_ENABLED=true|false
35
+ // Default is enabled for backwards compatibility.
36
  //
37
  const PROVIDERS = [
38
  {
 
182
 
183
  // Build per-provider key pools + rotation indices
184
  const providerState = PROVIDERS.map(p => {
185
+ const llmFallbackRaw = String(process.env.LLM_API_KEY_FALLBACK_ENABLED || '').trim().toLowerCase();
186
+ const llmFallbackEnabled = !/^(0|false|no|off)$/.test(llmFallbackRaw);
187
  const dedicatedKeys = normalizeKeys(
188
  process.env[p.envPlural] || '',
189
  process.env[p.envSingular] || '',
 
191
  const hasDedicated = dedicatedKeys.length > 0;
192
  const keys = hasDedicated
193
  ? dedicatedKeys
194
+ : (
195
+ llmFallbackEnabled
196
+ ? normalizeKeys(process.env.LLM_API_KEY || '')
197
+ : []
198
+ );
199
 
200
  if (hasDedicated) {
201
  log(`[key-rotator] ${p.name}: ${keys.length} key${keys.length === 1 ? '' : 's'}`);
 
216
  }).length;
217
  if (fallbackCount > 0) {
218
  log(`[key-rotator] ${fallbackCount} provider(s) using LLM_API_KEY fallback`);
219
+ } else if (process.env.LLM_API_KEY && /^(0|false|no|off)$/i.test(String(process.env.LLM_API_KEY_FALLBACK_ENABLED || ''))) {
220
+ log('[key-rotator] LLM_API_KEY fallback disabled (set LLM_API_KEY_FALLBACK_ENABLED=true to re-enable)');
221
  }
222
 
223
  // ─── Runtime helpers ─────────────────────────────────────────────────────────
openclaw-sync.py CHANGED
@@ -18,6 +18,7 @@ import sys
18
  import tempfile
19
  import threading
20
  import time
 
21
  from pathlib import Path
22
 
23
  os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
@@ -69,6 +70,7 @@ EXCLUDED_STATE_NAMES = {
69
  "openclaw-app",
70
  "gateway.log",
71
  "browser",
 
72
  }
73
  WHATSAPP_CREDS_DIR = OPENCLAW_HOME / "credentials" / "whatsapp" / "default"
74
  WHATSAPP_BACKUP_DIR = STATE_DIR / "credentials" / "whatsapp" / "default"
@@ -76,6 +78,7 @@ RESET_MARKER = WORKSPACE / ".reset_credentials"
76
  HF_API = HfApi(token=HF_TOKEN) if HF_TOKEN else None
77
  STOP_EVENT = threading.Event()
78
  _REPO_ID_CACHE: str | None = None
 
79
 
80
 
81
  def write_status(status: str, message: str) -> None:
@@ -280,14 +283,15 @@ def file_marker(path: Path) -> tuple[int, int, int]:
280
  return (1, int(stat.st_size), int(stat.st_mtime_ns))
281
 
282
 
283
- def metadata_marker(root: Path) -> tuple[int, int, int]:
284
  if not root.exists():
285
- return (0, 0, 0)
286
 
287
  file_count = 0
288
  total_size = 0
289
  newest_mtime = 0
290
- for path in root.rglob("*"):
 
291
  if not path.is_file():
292
  continue
293
  rel = path.relative_to(root).as_posix()
@@ -298,9 +302,17 @@ def metadata_marker(root: Path) -> tuple[int, int, int]:
298
  except OSError:
299
  continue
300
  file_count += 1
301
- total_size += int(stat.st_size)
302
- newest_mtime = max(newest_mtime, int(stat.st_mtime_ns))
303
- return (file_count, total_size, newest_mtime)
 
 
 
 
 
 
 
 
304
 
305
 
306
  def fingerprint_dir(root: Path) -> str:
@@ -313,9 +325,16 @@ def fingerprint_dir(root: Path) -> str:
313
  if _should_exclude(rel, path):
314
  continue
315
  hasher.update(rel.encode("utf-8"))
316
- with path.open("rb") as handle:
317
- for chunk in iter(lambda: handle.read(1024 * 1024), b""):
318
- hasher.update(chunk)
 
 
 
 
 
 
 
319
  return hasher.hexdigest()
320
 
321
 
@@ -331,10 +350,40 @@ def create_snapshot_dir(source_root: Path) -> Path:
331
  target.mkdir(parents=True, exist_ok=True)
332
  continue
333
  target.parent.mkdir(parents=True, exist_ok=True)
334
- shutil.copy2(path, target)
 
 
 
 
 
 
335
  return staging_root
336
 
337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  def restore_workspace() -> bool:
339
  if not HF_TOKEN:
340
  write_status("disabled", "HF_TOKEN is not configured.")
@@ -396,16 +445,15 @@ def restore_workspace() -> bool:
396
 
397
  def _sync_once_unlocked(
398
  last_fingerprint: str | None = None,
399
- last_marker: tuple[int, int, int] | None = None,
400
- ) -> tuple[str, tuple[int, int, int]]:
401
  if not HF_TOKEN:
402
  write_status("disabled", "HF_TOKEN is not configured.")
403
- return (last_fingerprint or "", last_marker or (0, 0, 0))
404
 
405
  snapshot_state_into_workspace()
406
  repo_id = ensure_repo_exists()
407
  current_marker = metadata_marker(WORKSPACE)
408
-
409
  if last_marker is not None and current_marker == last_marker:
410
  write_status("synced", "No workspace changes detected.")
411
  return (last_fingerprint or "", current_marker)
@@ -435,6 +483,7 @@ def _sync_once_unlocked(
435
  commit_message=f"HuggingClaw sync {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}",
436
  ignore_patterns=[".git/*", ".git"],
437
  )
 
438
  finally:
439
  shutil.rmtree(snapshot_dir, ignore_errors=True)
440
 
@@ -444,8 +493,8 @@ def _sync_once_unlocked(
444
 
445
  def sync_once(
446
  last_fingerprint: str | None = None,
447
- last_marker: tuple[int, int, int] | None = None,
448
- ) -> tuple[str, tuple[int, int, int]]:
449
  SYNC_LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
450
  with SYNC_LOCK_FILE.open("w", encoding="utf-8") as lock_handle:
451
  fcntl.flock(lock_handle, fcntl.LOCK_EX)
 
18
  import tempfile
19
  import threading
20
  import time
21
+ from typing import TypeAlias
22
  from pathlib import Path
23
 
24
  os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
 
70
  "openclaw-app",
71
  "gateway.log",
72
  "browser",
73
+ "npm",
74
  }
75
  WHATSAPP_CREDS_DIR = OPENCLAW_HOME / "credentials" / "whatsapp" / "default"
76
  WHATSAPP_BACKUP_DIR = STATE_DIR / "credentials" / "whatsapp" / "default"
 
78
  HF_API = HfApi(token=HF_TOKEN) if HF_TOKEN else None
79
  STOP_EVENT = threading.Event()
80
  _REPO_ID_CACHE: str | None = None
81
+ WorkspaceMarker: TypeAlias = tuple[int, int, int, str]
82
 
83
 
84
  def write_status(status: str, message: str) -> None:
 
283
  return (1, int(stat.st_size), int(stat.st_mtime_ns))
284
 
285
 
286
+ def metadata_marker(root: Path) -> WorkspaceMarker:
287
  if not root.exists():
288
+ return (0, 0, 0, "")
289
 
290
  file_count = 0
291
  total_size = 0
292
  newest_mtime = 0
293
+ metadata_hasher = hashlib.sha256()
294
+ for path in sorted(root.rglob("*")):
295
  if not path.is_file():
296
  continue
297
  rel = path.relative_to(root).as_posix()
 
302
  except OSError:
303
  continue
304
  file_count += 1
305
+ size = int(stat.st_size)
306
+ mtime_ns = int(stat.st_mtime_ns)
307
+ total_size += size
308
+ newest_mtime = max(newest_mtime, mtime_ns)
309
+ metadata_hasher.update(rel.encode("utf-8"))
310
+ metadata_hasher.update(b"\0")
311
+ metadata_hasher.update(str(size).encode("ascii"))
312
+ metadata_hasher.update(b"\0")
313
+ metadata_hasher.update(str(mtime_ns).encode("ascii"))
314
+ metadata_hasher.update(b"\0")
315
+ return (file_count, total_size, newest_mtime, metadata_hasher.hexdigest())
316
 
317
 
318
  def fingerprint_dir(root: Path) -> str:
 
325
  if _should_exclude(rel, path):
326
  continue
327
  hasher.update(rel.encode("utf-8"))
328
+ try:
329
+ with path.open("rb") as handle:
330
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
331
+ hasher.update(chunk)
332
+ except (FileNotFoundError, IsADirectoryError, NotADirectoryError):
333
+ # Fingerprint must represent a complete view of the workspace.
334
+ # Retry next sync pass instead of silently hashing a partial tree.
335
+ raise RuntimeError(
336
+ f"Workspace changed while hashing {rel}; retrying next sync pass."
337
+ )
338
  return hasher.hexdigest()
339
 
340
 
 
350
  target.mkdir(parents=True, exist_ok=True)
351
  continue
352
  target.parent.mkdir(parents=True, exist_ok=True)
353
+ try:
354
+ shutil.copy2(path, target)
355
+ except (FileNotFoundError, IsADirectoryError, NotADirectoryError):
356
+ # Do not upload a partial snapshot; let caller retry on next loop.
357
+ raise RuntimeError(
358
+ f"Snapshot changed while copying {rel_posix}; retrying next sync pass."
359
+ )
360
  return staging_root
361
 
362
 
363
+ def prune_remote_deleted_files(repo_id: str, snapshot_dir: Path) -> None:
364
+ if HF_API is None:
365
+ return
366
+
367
+ local_files = {
368
+ path.relative_to(snapshot_dir).as_posix()
369
+ for path in snapshot_dir.rglob("*")
370
+ if path.is_file()
371
+ }
372
+
373
+ remote_files = HF_API.list_repo_files(repo_id=repo_id, repo_type="dataset")
374
+ stale_files = [
375
+ path for path in remote_files
376
+ if path not in local_files and path not in {".gitattributes"}
377
+ ]
378
+ for stale_path in stale_files:
379
+ HF_API.delete_file(
380
+ path_in_repo=stale_path,
381
+ repo_id=repo_id,
382
+ repo_type="dataset",
383
+ commit_message=f"Delete stale file {stale_path}",
384
+ )
385
+
386
+
387
  def restore_workspace() -> bool:
388
  if not HF_TOKEN:
389
  write_status("disabled", "HF_TOKEN is not configured.")
 
445
 
446
  def _sync_once_unlocked(
447
  last_fingerprint: str | None = None,
448
+ last_marker: WorkspaceMarker | None = None,
449
+ ) -> tuple[str, WorkspaceMarker]:
450
  if not HF_TOKEN:
451
  write_status("disabled", "HF_TOKEN is not configured.")
452
+ return (last_fingerprint or "", last_marker or (0, 0, 0, ""))
453
 
454
  snapshot_state_into_workspace()
455
  repo_id = ensure_repo_exists()
456
  current_marker = metadata_marker(WORKSPACE)
 
457
  if last_marker is not None and current_marker == last_marker:
458
  write_status("synced", "No workspace changes detected.")
459
  return (last_fingerprint or "", current_marker)
 
483
  commit_message=f"HuggingClaw sync {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}",
484
  ignore_patterns=[".git/*", ".git"],
485
  )
486
+ prune_remote_deleted_files(repo_id, snapshot_dir)
487
  finally:
488
  shutil.rmtree(snapshot_dir, ignore_errors=True)
489
 
 
493
 
494
  def sync_once(
495
  last_fingerprint: str | None = None,
496
+ last_marker: WorkspaceMarker | None = None,
497
+ ) -> tuple[str, WorkspaceMarker]:
498
  SYNC_LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
499
  with SYNC_LOCK_FILE.open("w", encoding="utf-8") as lock_handle:
500
  fcntl.flock(lock_handle, fcntl.LOCK_EX)
start.sh CHANGED
@@ -131,6 +131,48 @@ case "$LLM_PROVIDER" in
131
  ;;
132
  esac
133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  # ── Setup directories ──
135
  mkdir -p /home/node/.openclaw/agents/main/sessions
136
  mkdir -p /home/node/.openclaw/credentials
@@ -285,6 +327,67 @@ if [ -n "$CUSTOM_PROVIDER_NAME" ] || [ -n "$CUSTOM_BASE_URL" ] || [ -n "$CUSTOM_
285
  fi
286
  fi
287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  # Browser configuration (managed local Chromium in HF/Docker)
289
  BROWSER_EXECUTABLE_PATH=""
290
  for candidate in /usr/bin/chromium /usr/bin/chromium-browser /snap/bin/chromium; do
@@ -540,10 +643,15 @@ fi
540
  # ── Trap SIGTERM for graceful shutdown ──
541
  graceful_shutdown() {
542
  echo "Shutting down..."
543
- if [ -f "/home/node/app/openclaw-sync.py" ]; then
544
  echo "Saving state before exit..."
 
 
 
545
  python3 /home/node/app/openclaw-sync.py sync-once || \
546
- echo "Warning: could not complete shutdown sync"
 
 
547
  fi
548
  kill $(jobs -p) 2>/dev/null
549
  exit 0
 
131
  ;;
132
  esac
133
 
134
+ # Ensure OpenClaw provider discovery can see per-provider keys even when users
135
+ # configure only *_API_KEYS pools. Mirror first pool key into singular env.
136
+ promote_first_pool_key() {
137
+ local singular_var="$1"
138
+ local pool_var="$2"
139
+ local singular_val="${!singular_var:-}"
140
+ local pool_val="${!pool_var:-}"
141
+
142
+ [ -n "$singular_val" ] && return 0
143
+ [ -n "$pool_val" ] || return 0
144
+
145
+ local first
146
+ first=$(printf '%s' "$pool_val" | tr ',' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | awk 'NF{print; exit}')
147
+ [ -n "$first" ] || return 0
148
+ export "${singular_var}=$first"
149
+ }
150
+
151
+ promote_first_pool_key "ANTHROPIC_API_KEY" "ANTHROPIC_API_KEYS"
152
+ promote_first_pool_key "OPENAI_API_KEY" "OPENAI_API_KEYS"
153
+ promote_first_pool_key "GEMINI_API_KEY" "GEMINI_API_KEYS"
154
+ promote_first_pool_key "DEEPSEEK_API_KEY" "DEEPSEEK_API_KEYS"
155
+ promote_first_pool_key "OPENROUTER_API_KEY" "OPENROUTER_API_KEYS"
156
+ promote_first_pool_key "KILOCODE_API_KEY" "KILOCODE_API_KEYS"
157
+ promote_first_pool_key "OPENCODE_API_KEY" "OPENCODE_API_KEYS"
158
+ promote_first_pool_key "ZAI_API_KEY" "ZAI_API_KEYS"
159
+ promote_first_pool_key "MOONSHOT_API_KEY" "MOONSHOT_API_KEYS"
160
+ promote_first_pool_key "MINIMAX_API_KEY" "MINIMAX_API_KEYS"
161
+ promote_first_pool_key "XIAOMI_API_KEY" "XIAOMI_API_KEYS"
162
+ promote_first_pool_key "VOLCANO_ENGINE_API_KEY" "VOLCANO_ENGINE_API_KEYS"
163
+ promote_first_pool_key "BYTEPLUS_API_KEY" "BYTEPLUS_API_KEYS"
164
+ promote_first_pool_key "QIANFAN_API_KEY" "QIANFAN_API_KEYS"
165
+ promote_first_pool_key "MODELSTUDIO_API_KEY" "MODELSTUDIO_API_KEYS"
166
+ promote_first_pool_key "KIMI_API_KEY" "KIMI_API_KEYS"
167
+ promote_first_pool_key "MISTRAL_API_KEY" "MISTRAL_API_KEYS"
168
+ promote_first_pool_key "XAI_API_KEY" "XAI_API_KEYS"
169
+ promote_first_pool_key "NVIDIA_API_KEY" "NVIDIA_API_KEYS"
170
+ promote_first_pool_key "GROQ_API_KEY" "GROQ_API_KEYS"
171
+ promote_first_pool_key "COHERE_API_KEY" "COHERE_API_KEYS"
172
+ promote_first_pool_key "TOGETHER_API_KEY" "TOGETHER_API_KEYS"
173
+ promote_first_pool_key "CEREBRAS_API_KEY" "CEREBRAS_API_KEYS"
174
+ promote_first_pool_key "HUGGINGFACE_HUB_TOKEN" "HUGGINGFACE_HUB_TOKENS"
175
+
176
  # ── Setup directories ──
177
  mkdir -p /home/node/.openclaw/agents/main/sessions
178
  mkdir -p /home/node/.openclaw/credentials
 
327
  fi
328
  fi
329
 
330
+ # Optional: explicitly expose provider model lists in Control UI when
331
+ # provider keys are configured. Format:
332
+ # NVIDIA_MODELS=model1,model2
333
+ # OPENAI_MODELS=gpt-4o-mini,gpt-4.1
334
+ # This helps when provider auto-discovery does not populate models reliably.
335
+ inject_provider_models_from_env() {
336
+ local provider="$1"
337
+ local models_env="$2"
338
+ local key_env_single="$3"
339
+ local key_env_pool="$4"
340
+ local models_csv="${!models_env:-}"
341
+ local single_key="${!key_env_single:-}"
342
+ local pool_keys="${!key_env_pool:-}"
343
+
344
+ # Only inject when both:
345
+ # 1) provider has at least one configured key
346
+ # 2) explicit model list env is provided
347
+ if [ -z "$models_csv" ] || { [ -z "$single_key" ] && [ -z "$pool_keys" ]; }; then
348
+ return 0
349
+ fi
350
+
351
+ local models_json
352
+ models_json=$(printf '%s' "$models_csv" \
353
+ | tr ',' '\n' \
354
+ | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' \
355
+ | awk 'NF' \
356
+ | jq -R . \
357
+ | jq -s 'map({id: ., name: .}) | unique_by(.id)')
358
+
359
+ CONFIG_JSON=$(jq \
360
+ --arg provider "$provider" \
361
+ --argjson models "$models_json" \
362
+ '.models.mode = "merge"
363
+ | .models.providers[$provider] = ((.models.providers[$provider] // {}) + {models: $models})' <<<"$CONFIG_JSON")
364
+ }
365
+
366
+ # Built-in provider model envs (optional)
367
+ inject_provider_models_from_env "anthropic" "ANTHROPIC_MODELS" "ANTHROPIC_API_KEY" "ANTHROPIC_API_KEYS"
368
+ inject_provider_models_from_env "openai" "OPENAI_MODELS" "OPENAI_API_KEY" "OPENAI_API_KEYS"
369
+ inject_provider_models_from_env "google" "GEMINI_MODELS" "GEMINI_API_KEY" "GEMINI_API_KEYS"
370
+ inject_provider_models_from_env "deepseek" "DEEPSEEK_MODELS" "DEEPSEEK_API_KEY" "DEEPSEEK_API_KEYS"
371
+ inject_provider_models_from_env "openrouter" "OPENROUTER_MODELS" "OPENROUTER_API_KEY" "OPENROUTER_API_KEYS"
372
+ inject_provider_models_from_env "kilocode" "KILOCODE_MODELS" "KILOCODE_API_KEY" "KILOCODE_API_KEYS"
373
+ inject_provider_models_from_env "opencode" "OPENCODE_MODELS" "OPENCODE_API_KEY" "OPENCODE_API_KEYS"
374
+ inject_provider_models_from_env "zai" "ZAI_MODELS" "ZAI_API_KEY" "ZAI_API_KEYS"
375
+ inject_provider_models_from_env "moonshot" "MOONSHOT_MODELS" "MOONSHOT_API_KEY" "MOONSHOT_API_KEYS"
376
+ inject_provider_models_from_env "kimi-coding" "KIMI_MODELS" "KIMI_API_KEY" "KIMI_API_KEYS"
377
+ inject_provider_models_from_env "minimax" "MINIMAX_MODELS" "MINIMAX_API_KEY" "MINIMAX_API_KEYS"
378
+ inject_provider_models_from_env "modelstudio" "MODELSTUDIO_MODELS" "MODELSTUDIO_API_KEY" "MODELSTUDIO_API_KEYS"
379
+ inject_provider_models_from_env "xiaomi" "XIAOMI_MODELS" "XIAOMI_API_KEY" "XIAOMI_API_KEYS"
380
+ inject_provider_models_from_env "volcengine" "VOLCANO_ENGINE_MODELS" "VOLCANO_ENGINE_API_KEY" "VOLCANO_ENGINE_API_KEYS"
381
+ inject_provider_models_from_env "byteplus" "BYTEPLUS_MODELS" "BYTEPLUS_API_KEY" "BYTEPLUS_API_KEYS"
382
+ inject_provider_models_from_env "qianfan" "QIANFAN_MODELS" "QIANFAN_API_KEY" "QIANFAN_API_KEYS"
383
+ inject_provider_models_from_env "groq" "GROQ_MODELS" "GROQ_API_KEY" "GROQ_API_KEYS"
384
+ inject_provider_models_from_env "mistral" "MISTRAL_MODELS" "MISTRAL_API_KEY" "MISTRAL_API_KEYS"
385
+ inject_provider_models_from_env "xai" "XAI_MODELS" "XAI_API_KEY" "XAI_API_KEYS"
386
+ inject_provider_models_from_env "nvidia" "NVIDIA_MODELS" "NVIDIA_API_KEY" "NVIDIA_API_KEYS"
387
+ inject_provider_models_from_env "cohere" "COHERE_MODELS" "COHERE_API_KEY" "COHERE_API_KEYS"
388
+ inject_provider_models_from_env "together" "TOGETHER_MODELS" "TOGETHER_API_KEY" "TOGETHER_API_KEYS"
389
+ inject_provider_models_from_env "cerebras" "CEREBRAS_MODELS" "CEREBRAS_API_KEY" "CEREBRAS_API_KEYS"
390
+
391
  # Browser configuration (managed local Chromium in HF/Docker)
392
  BROWSER_EXECUTABLE_PATH=""
393
  for candidate in /usr/bin/chromium /usr/bin/chromium-browser /snap/bin/chromium; do
 
643
  # ── Trap SIGTERM for graceful shutdown ──
644
  graceful_shutdown() {
645
  echo "Shutting down..."
646
+ if [ -f "/home/node/app/openclaw-sync.py" ] && [ -n "${HF_TOKEN:-}" ]; then
647
  echo "Saving state before exit..."
648
+ timeout 8s python3 /home/node/app/openclaw-sync.py sync-once-settled || \
649
+ echo "Warning: could not complete settled shutdown sync"
650
+ sleep 1
651
  python3 /home/node/app/openclaw-sync.py sync-once || \
652
+ echo "Warning: could not complete final shutdown sync"
653
+ elif [ -f "/home/node/app/openclaw-sync.py" ]; then
654
+ echo "HF_TOKEN not set; skipping shutdown backup sync."
655
  fi
656
  kill $(jobs -p) 2>/dev/null
657
  exit 0