@chrysb/alphaclaw 0.9.16 → 0.9.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +25 -0
  2. package/lib/public/css/tailwind.generated.css +1 -1
  3. package/lib/public/dist/app.bundle.js +1858 -1758
  4. package/lib/public/js/components/agents-tab/agent-overview/model-card.js +59 -7
  5. package/lib/public/js/components/agents-tab/agent-overview/use-model-card.js +124 -0
  6. package/lib/public/js/components/api-feature-panel.js +76 -0
  7. package/lib/public/js/components/envars.js +1 -1
  8. package/lib/public/js/components/general/index.js +6 -0
  9. package/lib/public/js/components/general/use-general-tab.js +69 -0
  10. package/lib/public/js/components/row-accessory-select.js +52 -0
  11. package/lib/public/js/lib/api.js +26 -0
  12. package/lib/public/js/lib/model-catalog.js +6 -0
  13. package/lib/public/js/lib/model-config.js +12 -7
  14. package/lib/public/js/lib/storage-keys.js +4 -0
  15. package/lib/public/js/lib/thinking-levels.js +37 -0
  16. package/lib/server/agents/agents.js +33 -7
  17. package/lib/server/agents/channels.js +4 -2
  18. package/lib/server/alphaclaw-config.js +99 -0
  19. package/lib/server/chat-ws.js +4 -1
  20. package/lib/server/constants.js +73 -0
  21. package/lib/server/cost-utils.js +2 -0
  22. package/lib/server/db/auth/index.js +147 -0
  23. package/lib/server/db/auth/schema.js +17 -0
  24. package/lib/server/gateway.js +321 -20
  25. package/lib/server/helpers.js +1 -3
  26. package/lib/server/init/register-server-routes.js +45 -18
  27. package/lib/server/init/runtime-init.js +4 -0
  28. package/lib/server/init/server-lifecycle.js +1 -24
  29. package/lib/server/login-throttle.js +261 -60
  30. package/lib/server/model-catalog-bootstrap.json +5 -0
  31. package/lib/server/onboarding/index.js +2 -2
  32. package/lib/server/onboarding/openclaw.js +27 -3
  33. package/lib/server/openclaw-thinking.js +103 -0
  34. package/lib/server/openclaw-version.js +1 -1
  35. package/lib/server/routes/agents.js +10 -3
  36. package/lib/server/routes/models.js +35 -1
  37. package/lib/server/routes/onboarding.js +2 -2
  38. package/lib/server/routes/proxy.js +219 -1
  39. package/lib/server/routes/system.js +63 -2
  40. package/lib/server/usage-tracker-config.js +52 -1
  41. package/lib/server.js +60 -22
  42. package/package.json +2 -2
@@ -1,77 +1,274 @@
1
- const { kLoginWindowMs, kLoginMaxAttempts, kLoginBaseLockMs, kLoginMaxLockMs, kLoginStateTtlMs } = require("./constants");
2
-
3
- const createLoginThrottle = () => {
4
- const kLoginAttemptStates = new Map();
5
-
6
- const getOrCreateLoginAttemptState = (clientKey, now) => {
7
- const existing = kLoginAttemptStates.get(clientKey);
8
- if (existing) {
9
- existing.lastSeenAt = now;
10
- return existing;
11
- }
12
- const next = {
13
- attempts: 0,
14
- windowStart: now,
15
- lockUntil: 0,
16
- failStreak: 0,
17
- lastSeenAt: now,
18
- };
19
- kLoginAttemptStates.set(clientKey, next);
20
- return next;
21
- };
1
+ const {
2
+ kLoginWindowMs,
3
+ kLoginMaxAttempts,
4
+ kLoginBaseLockMs,
5
+ kLoginMaxLockMs,
6
+ kLoginGlobalWindowMs,
7
+ kLoginGlobalMaxAttempts,
8
+ kLoginGlobalBaseLockMs,
9
+ kLoginGlobalMaxLockMs,
10
+ kLoginStateTtlMs,
11
+ } = require("./constants");
22
12
 
23
- const evaluateLoginThrottle = (state, now) => {
24
- if (!state) return { blocked: false, retryAfterSec: 0 };
25
- if (state.lockUntil > now) {
26
- return {
27
- blocked: true,
28
- retryAfterSec: Math.max(1, Math.ceil((state.lockUntil - now) / 1000)),
29
- };
30
- }
31
- if (now - state.windowStart >= kLoginWindowMs) {
32
- state.attempts = 0;
33
- state.windowStart = now;
34
- }
35
- return { blocked: false, retryAfterSec: 0 };
13
+ const kGlobalStateKey = "global:login";
14
+ const kLoginThrottleScope = "login";
15
+
16
+ const createLoginAttemptState = (now) => ({
17
+ attempts: 0,
18
+ windowStart: now,
19
+ lockUntil: 0,
20
+ failStreak: 0,
21
+ lastSeenAt: now,
22
+ });
23
+
24
+ const normalizeState = (state, now) => ({
25
+ attempts: Number.parseInt(String(state?.attempts ?? 0), 10) || 0,
26
+ windowStart: Number.parseInt(String(state?.windowStart ?? now), 10) || now,
27
+ lockUntil: Number.parseInt(String(state?.lockUntil ?? 0), 10) || 0,
28
+ failStreak: Number.parseInt(String(state?.failStreak ?? 0), 10) || 0,
29
+ lastSeenAt: Number.parseInt(String(state?.lastSeenAt ?? now), 10) || now,
30
+ });
31
+
32
+ const createMemoryLoginThrottleStore = () => {
33
+ const states = new Map();
34
+
35
+ return {
36
+ get: (stateKey) => states.get(stateKey) || null,
37
+ set: (stateKey, state) => {
38
+ states.set(stateKey, { ...state });
39
+ },
40
+ delete: (stateKey) => {
41
+ states.delete(stateKey);
42
+ },
43
+ entries: () =>
44
+ Array.from(states.entries()).map(([stateKey, state]) => [
45
+ stateKey,
46
+ { ...state },
47
+ ]),
48
+ runExclusive: (callback) => callback(),
36
49
  };
50
+ };
37
51
 
38
- const recordLoginFailure = (state, now) => {
39
- if (!state) return { lockMs: 0, locked: false };
40
- if (now - state.windowStart >= kLoginWindowMs) {
41
- state.attempts = 0;
42
- state.windowStart = now;
43
- }
44
- state.attempts += 1;
52
+ const getClientStateKey = (clientKey, scope = kLoginThrottleScope) => {
53
+ const normalizedClientKey = String(clientKey || "unknown");
54
+ return scope === kLoginThrottleScope
55
+ ? `client:${normalizedClientKey}`
56
+ : `client:${scope}:${normalizedClientKey}`;
57
+ };
58
+
59
+ const getGlobalStateKey = (scope = kLoginThrottleScope) =>
60
+ scope === kLoginThrottleScope ? kGlobalStateKey : `global:${scope}`;
61
+
62
+ const getOrCreateState = (store, stateKey, now) => {
63
+ const existing = store.get(stateKey);
64
+ if (existing) {
65
+ const state = normalizeState(existing, now);
45
66
  state.lastSeenAt = now;
46
- if (state.attempts < kLoginMaxAttempts) {
47
- return { lockMs: 0, locked: false };
48
- }
49
- state.failStreak += 1;
67
+ store.set(stateKey, state);
68
+ return state;
69
+ }
70
+ const next = createLoginAttemptState(now);
71
+ store.set(stateKey, next);
72
+ return next;
73
+ };
74
+
75
+ const evaluateState = ({ store, stateKey, now, windowMs }) => {
76
+ const state = getOrCreateState(store, stateKey, now);
77
+ if (state.lockUntil > now) {
78
+ return {
79
+ state,
80
+ blocked: true,
81
+ retryAfterSec: Math.max(1, Math.ceil((state.lockUntil - now) / 1000)),
82
+ };
83
+ }
84
+ if (now - state.windowStart >= windowMs) {
85
+ state.attempts = 0;
86
+ state.windowStart = now;
87
+ store.set(stateKey, state);
88
+ }
89
+ return { state, blocked: false, retryAfterSec: 0 };
90
+ };
91
+
92
+ const recordStateFailure = ({
93
+ store,
94
+ stateKey,
95
+ now,
96
+ windowMs,
97
+ maxAttempts,
98
+ baseLockMs,
99
+ maxLockMs,
100
+ }) => {
101
+ const state = getOrCreateState(store, stateKey, now);
102
+ if (state.lockUntil > now) {
103
+ return {
104
+ state,
105
+ lockMs: state.lockUntil - now,
106
+ locked: true,
107
+ retryAfterSec: Math.max(1, Math.ceil((state.lockUntil - now) / 1000)),
108
+ };
109
+ }
110
+ if (now - state.windowStart >= windowMs) {
50
111
  state.attempts = 0;
51
112
  state.windowStart = now;
52
- const lockMultiplier = Math.max(1, 2 ** (state.failStreak - 1));
53
- const lockMs = Math.min(kLoginBaseLockMs * lockMultiplier, kLoginMaxLockMs);
54
- state.lockUntil = now + lockMs;
55
- return { lockMs, locked: true };
113
+ }
114
+ state.attempts += 1;
115
+ state.lastSeenAt = now;
116
+ if (state.attempts < maxAttempts) {
117
+ store.set(stateKey, state);
118
+ return { state, lockMs: 0, locked: false, retryAfterSec: 0 };
119
+ }
120
+ state.failStreak += 1;
121
+ state.attempts = 0;
122
+ state.windowStart = now;
123
+ const lockMultiplier = Math.max(1, 2 ** (state.failStreak - 1));
124
+ const lockMs = Math.min(baseLockMs * lockMultiplier, maxLockMs);
125
+ state.lockUntil = now + lockMs;
126
+ store.set(stateKey, state);
127
+ return {
128
+ state,
129
+ lockMs,
130
+ locked: true,
131
+ retryAfterSec: Math.max(1, Math.ceil(lockMs / 1000)),
132
+ };
133
+ };
134
+
135
+ const chooseThrottleResult = (...results) => {
136
+ const blockedResults = results.filter((result) => result.blocked);
137
+ if (blockedResults.length === 0) {
138
+ return { blocked: false, retryAfterSec: 0 };
139
+ }
140
+ return {
141
+ blocked: true,
142
+ retryAfterSec: Math.max(
143
+ ...blockedResults.map((result) => result.retryAfterSec || 0),
144
+ ),
56
145
  };
146
+ };
147
+
148
+ const chooseFailureResult = (...results) => {
149
+ const lockedResults = results.filter((result) => result.locked);
150
+ if (lockedResults.length === 0) {
151
+ return { lockMs: 0, locked: false, retryAfterSec: 0 };
152
+ }
153
+ return {
154
+ lockMs: Math.max(...lockedResults.map((result) => result.lockMs || 0)),
155
+ locked: true,
156
+ retryAfterSec: Math.max(
157
+ ...lockedResults.map((result) => result.retryAfterSec || 0),
158
+ ),
159
+ };
160
+ };
161
+
162
+ const createLoginThrottle = ({
163
+ store = createMemoryLoginThrottleStore(),
164
+ scope = kLoginThrottleScope,
165
+ windowMs = kLoginWindowMs,
166
+ maxAttempts = kLoginMaxAttempts,
167
+ baseLockMs = kLoginBaseLockMs,
168
+ maxLockMs = kLoginMaxLockMs,
169
+ globalWindowMs = kLoginGlobalWindowMs,
170
+ globalMaxAttempts = kLoginGlobalMaxAttempts,
171
+ globalBaseLockMs = kLoginGlobalBaseLockMs,
172
+ globalMaxLockMs = kLoginGlobalMaxLockMs,
173
+ stateTtlMs = kLoginStateTtlMs,
174
+ } = {}) => {
175
+ const runExclusive =
176
+ typeof store.runExclusive === "function"
177
+ ? (callback) => store.runExclusive(callback)
178
+ : (callback) => callback();
179
+
180
+ const getOrCreateLoginAttemptState = (clientKey, now) =>
181
+ runExclusive(() => {
182
+ const clientStateKey = getClientStateKey(clientKey, scope);
183
+ const globalStateKey = getGlobalStateKey(scope);
184
+ return {
185
+ clientKey,
186
+ clientStateKey,
187
+ globalStateKey,
188
+ client: getOrCreateState(store, clientStateKey, now),
189
+ global: getOrCreateState(store, globalStateKey, now),
190
+ };
191
+ });
192
+
193
+ const evaluateLoginThrottle = (stateBundle, now) =>
194
+ runExclusive(() => {
195
+ const clientStateKey =
196
+ stateBundle?.clientStateKey ||
197
+ getClientStateKey(stateBundle?.clientKey, scope);
198
+ const globalStateKey = stateBundle?.globalStateKey || getGlobalStateKey(scope);
199
+ const clientResult = evaluateState({
200
+ store,
201
+ stateKey: clientStateKey,
202
+ now,
203
+ windowMs,
204
+ });
205
+ const globalResult = evaluateState({
206
+ store,
207
+ stateKey: globalStateKey,
208
+ now,
209
+ windowMs: globalWindowMs,
210
+ });
211
+ if (stateBundle) {
212
+ stateBundle.client = clientResult.state;
213
+ stateBundle.global = globalResult.state;
214
+ }
215
+ return chooseThrottleResult(clientResult, globalResult);
216
+ });
217
+
218
+ const recordLoginFailure = (stateBundle, now) =>
219
+ runExclusive(() => {
220
+ const clientStateKey =
221
+ stateBundle?.clientStateKey ||
222
+ getClientStateKey(stateBundle?.clientKey, scope);
223
+ const globalStateKey = stateBundle?.globalStateKey || getGlobalStateKey(scope);
224
+ const clientResult = recordStateFailure({
225
+ store,
226
+ stateKey: clientStateKey,
227
+ now,
228
+ windowMs,
229
+ maxAttempts,
230
+ baseLockMs,
231
+ maxLockMs,
232
+ });
233
+ const globalResult = recordStateFailure({
234
+ store,
235
+ stateKey: globalStateKey,
236
+ now,
237
+ windowMs: globalWindowMs,
238
+ maxAttempts: globalMaxAttempts,
239
+ baseLockMs: globalBaseLockMs,
240
+ maxLockMs: globalMaxLockMs,
241
+ });
242
+ if (stateBundle) {
243
+ stateBundle.client = clientResult.state;
244
+ stateBundle.global = globalResult.state;
245
+ }
246
+ return chooseFailureResult(clientResult, globalResult);
247
+ });
57
248
 
58
249
  const recordLoginSuccess = (clientKey) => {
59
250
  if (!clientKey) return;
60
- kLoginAttemptStates.delete(clientKey);
251
+ runExclusive(() => {
252
+ store.delete(getClientStateKey(clientKey, scope));
253
+ store.delete(getGlobalStateKey(scope));
254
+ });
61
255
  };
62
256
 
63
257
  const cleanupLoginAttemptStates = () => {
64
258
  const now = Date.now();
65
- for (const [key, state] of kLoginAttemptStates.entries()) {
66
- if (!state) {
67
- kLoginAttemptStates.delete(key);
68
- continue;
259
+ runExclusive(() => {
260
+ for (const [stateKey, rawState] of store.entries()) {
261
+ if (!rawState) {
262
+ store.delete(stateKey);
263
+ continue;
264
+ }
265
+ const state = normalizeState(rawState, now);
266
+ if (state.lockUntil > now) continue;
267
+ if (now - state.lastSeenAt > stateTtlMs) {
268
+ store.delete(stateKey);
269
+ }
69
270
  }
70
- if (state.lockUntil > now) continue;
71
- if (now - state.lastSeenAt > kLoginStateTtlMs) {
72
- kLoginAttemptStates.delete(key);
73
- }
74
- }
271
+ });
75
272
  };
76
273
 
77
274
  return {
@@ -83,4 +280,8 @@ const createLoginThrottle = () => {
83
280
  };
84
281
  };
85
282
 
86
- module.exports = { createLoginThrottle };
283
+ module.exports = {
284
+ createLoginThrottle,
285
+ createMemoryLoginThrottleStore,
286
+ kGlobalStateKey,
287
+ };
@@ -554,6 +554,11 @@
554
554
  "provider": "anthropic",
555
555
  "label": "Claude Opus 4.6"
556
556
  },
557
+ {
558
+ "key": "anthropic/claude-opus-4-8",
559
+ "provider": "anthropic",
560
+ "label": "Claude Opus 4.8"
561
+ },
557
562
  {
558
563
  "key": "anthropic/claude-opus-4-7",
559
564
  "provider": "anthropic",
@@ -327,7 +327,7 @@ const createOnboardingService = ({
327
327
  authProfiles,
328
328
  ensureGatewayProxyConfig,
329
329
  getBaseUrl,
330
- startGateway,
330
+ runOnboardedBootSequence,
331
331
  }) => {
332
332
  const { OPENCLAW_DIR, WORKSPACE_DIR, kOnboardingMarkerPath } = constants;
333
333
 
@@ -567,7 +567,7 @@ const createOnboardingService = ({
567
567
  console.error("[onboard] Git push error:", e.message);
568
568
  }
569
569
 
570
- startGateway();
570
+ runOnboardedBootSequence();
571
571
  return { status: 200, body: { ok: true } };
572
572
  };
573
573
 
@@ -4,6 +4,7 @@ const {
4
4
  ensurePluginAllowed,
5
5
  ensureUsageTrackerPluginEntry,
6
6
  } = require("../usage-tracker-config");
7
+ const { isOpenAiCompatApiEnabled } = require("../alphaclaw-config");
7
8
 
8
9
  const kDefaultToolsProfile = "full";
9
10
  const kBootstrapExtraFiles = [
@@ -133,16 +134,29 @@ const buildOnboardArgs = ({
133
134
  return onboardArgs;
134
135
  };
135
136
 
136
- const ensureManagedConfigShell = (cfg) => {
137
+ const ensureManagedConfigShell = (cfg, { openAiCompatApiEnabled = false } = {}) => {
137
138
  if (!cfg.channels) cfg.channels = {};
138
139
  ensurePluginsShell(cfg);
139
140
  if (!cfg.commands) cfg.commands = {};
140
141
  if (!cfg.tools) cfg.tools = {};
142
+ if (!cfg.gateway) cfg.gateway = {};
141
143
  if (!cfg.hooks) cfg.hooks = {};
142
144
  if (!cfg.hooks.internal) cfg.hooks.internal = {};
143
145
  if (!cfg.hooks.internal.entries) cfg.hooks.internal.entries = {};
144
146
  cfg.commands.restart = true;
145
147
  cfg.tools.profile = kDefaultToolsProfile;
148
+ if (openAiCompatApiEnabled) {
149
+ if (!cfg.gateway.http) cfg.gateway.http = {};
150
+ if (!cfg.gateway.http.endpoints) cfg.gateway.http.endpoints = {};
151
+ cfg.gateway.http.endpoints.chatCompletions = {
152
+ ...(cfg.gateway.http.endpoints.chatCompletions || {}),
153
+ enabled: true,
154
+ };
155
+ cfg.gateway.http.endpoints.responses = {
156
+ ...(cfg.gateway.http.endpoints.responses || {}),
157
+ enabled: true,
158
+ };
159
+ }
146
160
  cfg.hooks.internal.enabled = true;
147
161
  cfg.hooks.internal.entries["bootstrap-extra-files"] = {
148
162
  ...(cfg.hooks.internal.entries["bootstrap-extra-files"] || {}),
@@ -224,7 +238,12 @@ const writeSanitizedOpenclawConfig = ({
224
238
  }) => {
225
239
  const configPath = `${openclawDir}/openclaw.json`;
226
240
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
227
- ensureManagedConfigShell(cfg);
241
+ ensureManagedConfigShell(cfg, {
242
+ openAiCompatApiEnabled: isOpenAiCompatApiEnabled({
243
+ fsModule: fs,
244
+ openclawDir,
245
+ }),
246
+ });
228
247
  applyFreshOnboardingChannels({
229
248
  cfg,
230
249
  varMap,
@@ -256,7 +275,12 @@ const writeManagedImportOpenclawConfig = ({
256
275
  }) => {
257
276
  const configPath = `${openclawDir}/openclaw.json`;
258
277
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
259
- ensureManagedConfigShell(cfg);
278
+ ensureManagedConfigShell(cfg, {
279
+ openAiCompatApiEnabled: isOpenAiCompatApiEnabled({
280
+ fsModule: fs,
281
+ openclawDir,
282
+ }),
283
+ });
260
284
 
261
285
  ensureUsageTrackerPluginEntry(cfg);
262
286
 
@@ -0,0 +1,103 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { pathToFileURL } = require("url");
4
+
5
+ const kThinkingModuleSentinel = "listThinkingLevelOptions";
6
+
7
+ let thinkingModulePromise = null;
8
+
9
+ const resolveOpenclawDistDir = () => path.dirname(require.resolve("openclaw"));
10
+
11
+ const resolveThinkingModulePath = (distDir = resolveOpenclawDistDir()) => {
12
+ for (const name of fs.readdirSync(distDir)) {
13
+ if (!/^thinking-.*\.js$/.test(name)) continue;
14
+ if (name.includes("api") || name.includes("policy")) continue;
15
+ const fullPath = path.join(distDir, name);
16
+ const source = fs.readFileSync(fullPath, "utf8");
17
+ if (source.includes(kThinkingModuleSentinel)) return fullPath;
18
+ }
19
+ throw new Error("OpenClaw thinking module not found");
20
+ };
21
+
22
+ const loadThinkingModule = async () => {
23
+ if (!thinkingModulePromise) {
24
+ const modulePath = resolveThinkingModulePath();
25
+ thinkingModulePromise = import(pathToFileURL(modulePath).href);
26
+ }
27
+ return thinkingModulePromise;
28
+ };
29
+
30
+ const splitModelKey = (modelKey = "") => {
31
+ const normalized = String(modelKey || "").trim();
32
+ const slashIndex = normalized.indexOf("/");
33
+ if (slashIndex <= 0) return { provider: "", model: normalized };
34
+ return {
35
+ provider: normalized.slice(0, slashIndex),
36
+ model: normalized.slice(slashIndex + 1),
37
+ };
38
+ };
39
+
40
+ const buildCatalogEntry = ({ provider, model, reasoning, compat } = {}) => {
41
+ const normalizedProvider = String(provider || "").trim();
42
+ const normalizedModel = String(model || "").trim();
43
+ if (!normalizedProvider || !normalizedModel) return null;
44
+ const entry = {
45
+ provider: normalizedProvider,
46
+ id: normalizedModel,
47
+ };
48
+ if (typeof reasoning === "boolean") entry.reasoning = reasoning;
49
+ if (compat && typeof compat === "object") entry.compat = compat;
50
+ return entry;
51
+ };
52
+
53
+ const resolveThinkingApi = async () => {
54
+ const mod = await loadThinkingModule();
55
+ return {
56
+ listThinkingLevelOptions: mod.listThinkingLevelOptions || mod.i,
57
+ resolveThinkingDefaultForModel: mod.resolveThinkingDefaultForModel || mod.s,
58
+ normalizeThinkLevel: mod.normalizeThinkLevel || mod.p,
59
+ };
60
+ };
61
+
62
+ const resolveThinkingOptionsForModel = async ({
63
+ modelKey = "",
64
+ catalog = [],
65
+ } = {}) => {
66
+ const { provider, model } = splitModelKey(modelKey);
67
+ if (!provider || !model) {
68
+ return {
69
+ levels: [],
70
+ modelDefault: "off",
71
+ };
72
+ }
73
+ const api = await resolveThinkingApi();
74
+ const levels = api.listThinkingLevelOptions(provider, model, catalog) || [];
75
+ const modelDefault =
76
+ api.resolveThinkingDefaultForModel({
77
+ provider,
78
+ model,
79
+ catalog,
80
+ }) || "off";
81
+ return {
82
+ levels: levels.map((entry) => ({
83
+ id: String(entry?.id || "").trim(),
84
+ label: String(entry?.label || entry?.id || "").trim(),
85
+ })),
86
+ modelDefault: String(modelDefault || "off").trim() || "off",
87
+ };
88
+ };
89
+
90
+ const normalizeThinkingDefaultValue = async (raw) => {
91
+ if (raw === null || raw === undefined || raw === "") return null;
92
+ const api = await resolveThinkingApi();
93
+ const normalized = api.normalizeThinkLevel(String(raw || "").trim());
94
+ return normalized || null;
95
+ };
96
+
97
+ module.exports = {
98
+ buildCatalogEntry,
99
+ loadThinkingModule,
100
+ normalizeThinkingDefaultValue,
101
+ resolveThinkingOptionsForModel,
102
+ splitModelKey,
103
+ };
@@ -237,7 +237,7 @@ const createOpenclawVersionService = ({
237
237
  });
238
238
  let restarted = false;
239
239
  if (isOnboarded()) {
240
- restartGateway();
240
+ await restartGateway();
241
241
  restarted = true;
242
242
  }
243
243
  return {
@@ -178,7 +178,11 @@ const registerAgentRoutes = ({
178
178
 
179
179
  app.get("/api/agents", (_req, res) => {
180
180
  try {
181
- res.json({ ok: true, agents: agentsService.listAgents() });
181
+ res.json({
182
+ ok: true,
183
+ agents: agentsService.listAgents(),
184
+ defaults: agentsService.getAgentDefaults?.() || {},
185
+ });
182
186
  } catch (error) {
183
187
  res.status(500).json({ ok: false, error: error.message });
184
188
  }
@@ -237,9 +241,12 @@ const registerAgentRoutes = ({
237
241
  }
238
242
  });
239
243
 
240
- app.put("/api/agents/:id", (req, res) => {
244
+ app.put("/api/agents/:id", async (req, res) => {
241
245
  try {
242
- const agent = agentsService.updateAgent(req.params.id, req.body || {});
246
+ const agent = await agentsService.updateAgent(
247
+ req.params.id,
248
+ req.body || {},
249
+ );
243
250
  return res.json({ ok: true, agent });
244
251
  } catch (error) {
245
252
  const status = String(error.message || "").includes("not found")
@@ -1,5 +1,7 @@
1
- const { kFallbackOnboardingModels } = require("../constants");
1
+ const { kFallbackOnboardingModels, OPENCLAW_DIR } = require("../constants");
2
2
  const { createModelCatalogCache } = require("../model-catalog-cache");
3
+ const { resolveThinkingOptionsForModel } = require("../openclaw-thinking");
4
+ const { readOpenclawConfig } = require("../openclaw-config");
3
5
  const { getCommandOutputCandidates } = require("../utils/command-output");
4
6
 
5
7
  const runModelsGitSync = async (shellCmd) => {
@@ -182,6 +184,38 @@ const registerModelRoutes = ({
182
184
  return res.json(response);
183
185
  });
184
186
 
187
+ app.get("/api/models/thinking-options", async (req, res) => {
188
+ const modelKey = String(req.query?.modelKey || "").trim();
189
+ if (!modelKey.includes("/")) {
190
+ return res
191
+ .status(400)
192
+ .json({ ok: false, error: "modelKey is required (provider/model)" });
193
+ }
194
+ try {
195
+ const cfg = readOpenclawConfig({ openclawDir: OPENCLAW_DIR });
196
+ const globalThinkingDefault =
197
+ typeof cfg.agents?.defaults?.thinkingDefault === "string"
198
+ ? cfg.agents.defaults.thinkingDefault.trim()
199
+ : null;
200
+ const { levels, modelDefault } = await resolveThinkingOptionsForModel({
201
+ modelKey,
202
+ });
203
+ const inheritedDefault = globalThinkingDefault || modelDefault || "off";
204
+ return res.json({
205
+ ok: true,
206
+ modelKey,
207
+ levels,
208
+ modelDefault,
209
+ inheritedDefault,
210
+ });
211
+ } catch (err) {
212
+ return res.status(500).json({
213
+ ok: false,
214
+ error: err.message || "Failed to resolve thinking options",
215
+ });
216
+ }
217
+ });
218
+
185
219
  app.get("/api/models/status", async (req, res) => {
186
220
  try {
187
221
  const output = await shellCmd("openclaw models status --json", {
@@ -93,7 +93,7 @@ const registerOnboardingRoutes = ({
93
93
  authProfiles,
94
94
  ensureGatewayProxyConfig,
95
95
  getBaseUrl,
96
- startGateway,
96
+ runOnboardedBootSequence,
97
97
  }) => {
98
98
  // Keep mutating onboarding routes marker-gated so in-progress imports
99
99
  // can promote files before the final completion marker is written.
@@ -114,7 +114,7 @@ const registerOnboardingRoutes = ({
114
114
  authProfiles,
115
115
  ensureGatewayProxyConfig,
116
116
  getBaseUrl,
117
- startGateway,
117
+ runOnboardedBootSequence,
118
118
  });
119
119
 
120
120
  const kEnvVarNamePattern = /^[A-Z_][A-Z0-9_]*$/;