@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
@@ -3,6 +3,7 @@ import htm from "htm";
3
3
  import { Badge } from "../../badge.js";
4
4
  import { LoadingSpinner } from "../../loading-spinner.js";
5
5
  import { OverflowMenu, OverflowMenuItem } from "../../overflow-menu.js";
6
+ import { RowAccessorySelect } from "../../row-accessory-select.js";
6
7
  import {
7
8
  getModelDisplayLabel,
8
9
  SearchableModelPicker,
@@ -22,16 +23,25 @@ export const AgentModelCard = ({
22
23
  canEditModel,
23
24
  effectiveModel,
24
25
  effectiveModelEntry,
26
+ formatInheritedThinkingLabel,
25
27
  handleClearModelOverride,
26
28
  handleSelectModel,
29
+ handleSelectThinkingDefault,
27
30
  hasDistinctModelOverride,
31
+ hasDistinctThinkingOverride,
32
+ inheritedThinkingDefault,
28
33
  loading,
29
34
  menuOpen,
30
35
  modelEntries,
31
36
  popularModels,
32
37
  remainingModelOptions,
33
38
  setMenuOpen,
39
+ showThinkingSelect,
40
+ thinkingOptionsLoading,
41
+ thinkingSelectOptions,
42
+ thinkingSelectValue,
34
43
  updatingModel,
44
+ updatingThinking,
35
45
  } = useModelCard({
36
46
  agent,
37
47
  onUpdateAgent,
@@ -63,7 +73,19 @@ export const AgentModelCard = ({
63
73
  handleClearModelOverride();
64
74
  }}
65
75
  >
66
- Inherit from defaults
76
+ Inherit model from defaults
77
+ </${OverflowMenuItem}>
78
+ `
79
+ : null}
80
+ ${hasDistinctThinkingOverride
81
+ ? html`
82
+ <${OverflowMenuItem}
83
+ onClick=${() => {
84
+ setMenuOpen(false);
85
+ handleSelectThinkingDefault("");
86
+ }}
87
+ >
88
+ Inherit thinking from defaults
67
89
  </${OverflowMenuItem}>
68
90
  `
69
91
  : null}
@@ -93,17 +115,20 @@ export const AgentModelCard = ({
93
115
  </p>`
94
116
  : html`
95
117
  <div class="space-y-1">
96
- ${modelEntries.map(
97
- (entry) => html`
118
+ ${modelEntries.map((entry) => {
119
+ const isPrimary = entry.key === effectiveModel;
120
+ const showThinkingPicker =
121
+ isPrimary && showThinkingSelect && !thinkingOptionsLoading;
122
+ return html`
98
123
  <div
99
124
  key=${entry.key}
100
- class="flex items-center justify-between py-1"
125
+ class="flex items-center justify-between gap-3 py-1"
101
126
  >
102
127
  <div class="flex items-center gap-2 min-w-0">
103
128
  <span class="text-sm text-body truncate">
104
129
  ${getModelDisplayLabel(entry)}
105
130
  </span>
106
- ${entry.key === effectiveModel
131
+ ${isPrimary
107
132
  ? html`<${Badge} tone="cyan">Primary</${Badge}>`
108
133
  : html`
109
134
  <button
@@ -115,9 +140,36 @@ export const AgentModelCard = ({
115
140
  </button>
116
141
  `}
117
142
  </div>
143
+ ${showThinkingPicker
144
+ ? html`
145
+ <${RowAccessorySelect}
146
+ ariaLabel="Agent thinking level"
147
+ title="Agent thinking level"
148
+ value=${thinkingSelectValue}
149
+ disabled=${saving ||
150
+ updatingModel ||
151
+ updatingThinking ||
152
+ !canEditModel}
153
+ onChange=${handleSelectThinkingDefault}
154
+ >
155
+ <option value="">
156
+ ${formatInheritedThinkingLabel(
157
+ inheritedThinkingDefault,
158
+ )}
159
+ </option>
160
+ ${thinkingSelectOptions.map(
161
+ (option) => html`
162
+ <option value=${option.value}>
163
+ ${option.label}
164
+ </option>
165
+ `,
166
+ )}
167
+ </${RowAccessorySelect}>
168
+ `
169
+ : null}
118
170
  </div>
119
- `,
120
- )}
171
+ `;
172
+ })}
121
173
  </div>
122
174
  `}
123
175
  ${loading
@@ -1,4 +1,10 @@
1
1
  import { useEffect, useMemo, useState } from "preact/hooks";
2
+ import { fetchThinkingOptions } from "../../../lib/api.js";
3
+ import {
4
+ formatInheritedThinkingLabel,
5
+ formatThinkingLevelLabel,
6
+ shouldShowThinkingLevelSelect,
7
+ } from "../../../lib/thinking-levels.js";
2
8
  import { useModels } from "../../models-tab/use-models.js";
3
9
  import {
4
10
  buildProviderHasAuth,
@@ -25,7 +31,14 @@ export const useModelCard = ({
25
31
  onUpdateAgent = async () => {},
26
32
  }) => {
27
33
  const [updatingModel, setUpdatingModel] = useState(false);
34
+ const [updatingThinking, setUpdatingThinking] = useState(false);
28
35
  const [menuOpen, setMenuOpen] = useState(false);
36
+ const [thinkingOptions, setThinkingOptions] = useState({
37
+ levels: [],
38
+ inheritedDefault: "off",
39
+ modelDefault: "off",
40
+ });
41
+ const [thinkingOptionsLoading, setThinkingOptionsLoading] = useState(false);
29
42
  const {
30
43
  catalog,
31
44
  primary: defaultPrimaryModel,
@@ -41,6 +54,45 @@ export const useModelCard = ({
41
54
  const hasDistinctModelOverride =
42
55
  !!explicitModel &&
43
56
  String(explicitModel).trim() !== String(defaultPrimaryModel || "").trim();
57
+ const explicitThinkingDefault = String(agent.thinkingDefault || "").trim();
58
+ const inheritedThinkingDefault = String(
59
+ thinkingOptions.inheritedDefault || thinkingOptions.modelDefault || "off",
60
+ ).trim();
61
+ const hasDistinctThinkingOverride =
62
+ !!explicitThinkingDefault &&
63
+ explicitThinkingDefault !== inheritedThinkingDefault;
64
+ const showThinkingSelect = shouldShowThinkingLevelSelect(
65
+ thinkingOptions.levels,
66
+ );
67
+
68
+ useEffect(() => {
69
+ const modelKey = String(effectiveModel || "").trim();
70
+ if (!modelKey.includes("/")) {
71
+ setThinkingOptions({
72
+ levels: [],
73
+ inheritedDefault: "off",
74
+ modelDefault: "off",
75
+ });
76
+ return undefined;
77
+ }
78
+ let cancelled = false;
79
+ setThinkingOptionsLoading(true);
80
+ fetchThinkingOptions(modelKey)
81
+ .then((payload) => {
82
+ if (cancelled || !payload?.ok) return;
83
+ setThinkingOptions({
84
+ levels: Array.isArray(payload.levels) ? payload.levels : [],
85
+ inheritedDefault: String(payload.inheritedDefault || "off").trim(),
86
+ modelDefault: String(payload.modelDefault || "off").trim(),
87
+ });
88
+ })
89
+ .finally(() => {
90
+ if (!cancelled) setThinkingOptionsLoading(false);
91
+ });
92
+ return () => {
93
+ cancelled = true;
94
+ };
95
+ }, [effectiveModel]);
44
96
 
45
97
  const providerHasAuth = useMemo(
46
98
  () => buildProviderHasAuth({ authProfiles, codexStatus }),
@@ -149,6 +201,69 @@ export const useModelCard = ({
149
201
  }
150
202
  };
151
203
 
204
+ const handleSelectThinkingDefault = async (nextValue) => {
205
+ const normalizedValue = String(nextValue || "").trim();
206
+ const isInherit = !normalizedValue;
207
+ if (isInherit) {
208
+ if (!hasDistinctThinkingOverride) return;
209
+ setUpdatingThinking(true);
210
+ try {
211
+ await onUpdateAgent(
212
+ String(agent.id || "").trim(),
213
+ { thinkingDefault: null },
214
+ "Agent thinking level reset to default",
215
+ );
216
+ } finally {
217
+ setUpdatingThinking(false);
218
+ }
219
+ return;
220
+ }
221
+ if (normalizedValue === explicitThinkingDefault) return;
222
+ setUpdatingThinking(true);
223
+ try {
224
+ await onUpdateAgent(
225
+ String(agent.id || "").trim(),
226
+ { thinkingDefault: normalizedValue },
227
+ "Agent thinking level updated",
228
+ );
229
+ } finally {
230
+ setUpdatingThinking(false);
231
+ }
232
+ };
233
+
234
+ const thinkingSelectValue = hasDistinctThinkingOverride
235
+ ? explicitThinkingDefault
236
+ : "";
237
+ const thinkingSelectOptions = useMemo(() => {
238
+ const seen = new Set();
239
+ const options = [];
240
+ const addOption = (value, label) => {
241
+ const normalizedValue = String(value || "").trim();
242
+ if (!normalizedValue || seen.has(normalizedValue)) return;
243
+ seen.add(normalizedValue);
244
+ options.push({
245
+ value: normalizedValue,
246
+ label: String(label || formatThinkingLevelLabel(normalizedValue)).trim(),
247
+ });
248
+ };
249
+ for (const entry of thinkingOptions.levels) {
250
+ addOption(
251
+ entry?.id,
252
+ formatThinkingLevelLabel(entry?.label || entry?.id),
253
+ );
254
+ }
255
+ if (
256
+ explicitThinkingDefault &&
257
+ !seen.has(explicitThinkingDefault)
258
+ ) {
259
+ addOption(
260
+ explicitThinkingDefault,
261
+ `${formatThinkingLevelLabel(explicitThinkingDefault)} (custom)`,
262
+ );
263
+ }
264
+ return options;
265
+ }, [explicitThinkingDefault, thinkingOptions.levels]);
266
+
152
267
  return {
153
268
  authorizedModelOptions,
154
269
  canEditModel: modelsReady && !loadingModels,
@@ -156,13 +271,22 @@ export const useModelCard = ({
156
271
  effectiveModelEntry,
157
272
  handleClearModelOverride,
158
273
  handleSelectModel,
274
+ handleSelectThinkingDefault,
159
275
  hasDistinctModelOverride,
276
+ hasDistinctThinkingOverride,
277
+ inheritedThinkingDefault,
160
278
  loading: !modelsReady || loadingModels,
161
279
  menuOpen,
162
280
  modelEntries,
163
281
  popularModels,
164
282
  remainingModelOptions,
165
283
  setMenuOpen,
284
+ showThinkingSelect,
285
+ thinkingOptionsLoading,
286
+ thinkingSelectOptions,
287
+ thinkingSelectValue,
288
+ formatInheritedThinkingLabel,
166
289
  updatingModel,
290
+ updatingThinking,
167
291
  };
168
292
  };
@@ -0,0 +1,76 @@
1
+ import { h } from "preact";
2
+ import htm from "htm";
3
+ import { copyTextToClipboard } from "../lib/clipboard.js";
4
+ import { showToast } from "./toast.js";
5
+ import { FileCopyLineIcon } from "./icons.js";
6
+ import { InfoTooltip } from "./info-tooltip.js";
7
+ import { ToggleSwitch } from "./toggle-switch.js";
8
+
9
+ const html = htm.bind(h);
10
+
11
+ const getApiUrl = () => {
12
+ if (typeof window === "undefined" || !window.location?.origin) return "/v1";
13
+ return `${window.location.origin}/v1`;
14
+ };
15
+
16
+ export const ApiFeaturePanel = ({
17
+ openAiCompatApi = { enabled: false },
18
+ savingOpenAiCompatApi = false,
19
+ onToggleOpenAiCompatApi = () => {},
20
+ }) => {
21
+ const apiHydrated = openAiCompatApi?.hydrated === true;
22
+ const apiEnabled = openAiCompatApi?.enabled === true;
23
+ const apiUrl = getApiUrl();
24
+ const handleCopy = async () => {
25
+ const copied = await copyTextToClipboard(apiUrl);
26
+ showToast(
27
+ copied ? "API URL copied" : "Could not copy API URL",
28
+ copied ? "success" : "error",
29
+ );
30
+ };
31
+
32
+ return html`
33
+ <div class="bg-surface border border-border rounded-xl p-4">
34
+ <div class="flex items-center justify-between gap-3">
35
+ <div class="flex items-center gap-1.5 min-w-0">
36
+ <h2 class="card-label">API</h2>
37
+ <${InfoTooltip}
38
+ text="Allows trusted server-side clients to call OpenClaw via an OpenAI compatible API."
39
+ widthClass="w-72"
40
+ />
41
+ </div>
42
+ <${ToggleSwitch}
43
+ checked=${apiEnabled}
44
+ disabled=${savingOpenAiCompatApi || !apiHydrated}
45
+ label=${savingOpenAiCompatApi
46
+ ? "Saving..."
47
+ : !apiHydrated
48
+ ? "Loading..."
49
+ : apiEnabled
50
+ ? "Enabled"
51
+ : "Disabled"}
52
+ onChange=${onToggleOpenAiCompatApi}
53
+ />
54
+ </div>
55
+ ${apiHydrated && apiEnabled
56
+ ? html`
57
+ <div class="mt-4 text-xs text-fg-muted mb-2">OpenAI compatible URL</div>
58
+ <div class="flex items-center gap-2">
59
+ <code class="flex-1 min-w-0 bg-field border border-border rounded-lg px-3 py-2 text-xs text-body font-mono break-all">
60
+ ${apiUrl}
61
+ </code>
62
+ <button
63
+ type="button"
64
+ class="ac-btn-secondary text-xs p-2 rounded-lg shrink-0"
65
+ title="Copy URL"
66
+ aria-label="Copy API URL"
67
+ onclick=${handleCopy}
68
+ >
69
+ <${FileCopyLineIcon} className="w-4 h-4" />
70
+ </button>
71
+ </div>
72
+ `
73
+ : null}
74
+ </div>
75
+ `;
76
+ };
@@ -58,7 +58,7 @@ const normalizeEnvVarKey = (raw) =>
58
58
  .toUpperCase()
59
59
  .replace(/[^A-Z0-9_]/g, "_");
60
60
  const kManagedChannelTokenPattern =
61
- /^(?:TELEGRAM_BOT_TOKEN|DISCORD_BOT_TOKEN|SLACK_BOT_TOKEN|SLACK_APP_TOKEN)(?:_[A-Z0-9_]+)?$/;
61
+ /^(?:TELEGRAM_BOT_TOKEN|DISCORD_BOT_TOKEN|SLACK_BOT_TOKEN|SLACK_APP_TOKEN|WHATSAPP_OWNER_NUMBER)(?:_[A-Z0-9_]+)?$/;
62
62
  const stripSurroundingQuotes = (raw) => {
63
63
  const value = String(raw || "").trim();
64
64
  if (value.length < 2) return value;
@@ -8,6 +8,7 @@ import { DevicePairings } from "../device-pairings.js";
8
8
  import { ActionButton } from "../action-button.js";
9
9
  import { Google } from "../google/index.js";
10
10
  import { Features } from "../features.js";
11
+ import { ApiFeaturePanel } from "../api-feature-panel.js";
11
12
  import { GeneralDoctorWarning } from "../doctor/general-warning.js";
12
13
  import { ChevronDownIcon } from "../icons.js";
13
14
  import { UpdateActionButton } from "../update-action-button.js";
@@ -136,6 +137,11 @@ export const GeneralTab = ({
136
137
  onRestartRequired=${onRestartRequired}
137
138
  onOpenGmailWebhook=${onOpenGmailWebhook}
138
139
  />
140
+ <${ApiFeaturePanel}
141
+ openAiCompatApi=${state.openAiCompatApi}
142
+ savingOpenAiCompatApi=${state.savingOpenAiCompatApi}
143
+ onToggleOpenAiCompatApi=${actions.handleOpenAiCompatApiToggle}
144
+ />
139
145
 
140
146
  ${state.repo &&
141
147
  html`
@@ -8,14 +8,36 @@ import {
8
8
  rejectDevice,
9
9
  rejectPairing,
10
10
  triggerWatchdogRepair,
11
+ updateOpenAiCompatApiFeature,
11
12
  updateSyncCron,
12
13
  } from "../../lib/api.js";
13
14
  import { usePolling } from "../../hooks/usePolling.js";
15
+ import {
16
+ kOpenAiCompatApiFeatureCacheKey,
17
+ } from "../../lib/storage-keys.js";
14
18
  import { showToast } from "../toast.js";
15
19
  import { ALL_CHANNELS } from "../channels.js";
16
20
 
17
21
  const kDefaultSyncCronSchedule = "0 * * * *";
18
22
 
23
+ const readCachedOpenAiCompatApi = () => {
24
+ try {
25
+ const rawValue = window.localStorage.getItem(kOpenAiCompatApiFeatureCacheKey);
26
+ if (rawValue === "true") return { enabled: true, hydrated: true };
27
+ if (rawValue === "false") return { enabled: false, hydrated: true };
28
+ } catch {}
29
+ return { enabled: false, hydrated: false };
30
+ };
31
+
32
+ const writeCachedOpenAiCompatApi = (enabled) => {
33
+ try {
34
+ window.localStorage.setItem(
35
+ kOpenAiCompatApiFeatureCacheKey,
36
+ enabled ? "true" : "false",
37
+ );
38
+ } catch {}
39
+ };
40
+
19
41
  export const useGeneralTab = ({
20
42
  statusData = null,
21
43
  watchdogData = null,
@@ -30,6 +52,14 @@ export const useGeneralTab = ({
30
52
  const [syncCronSchedule, setSyncCronSchedule] = useState(kDefaultSyncCronSchedule);
31
53
  const [savingSyncCron, setSavingSyncCron] = useState(false);
32
54
  const [syncCronChoice, setSyncCronChoice] = useState(kDefaultSyncCronSchedule);
55
+ const [cachedOpenAiCompatApi] = useState(readCachedOpenAiCompatApi);
56
+ const [openAiCompatApiEnabled, setOpenAiCompatApiEnabled] = useState(
57
+ cachedOpenAiCompatApi.enabled,
58
+ );
59
+ const [openAiCompatApiHydrated, setOpenAiCompatApiHydrated] = useState(
60
+ cachedOpenAiCompatApi.hydrated,
61
+ );
62
+ const [savingOpenAiCompatApi, setSavingOpenAiCompatApi] = useState(false);
33
63
  const [pairingStatusRefreshing, setPairingStatusRefreshing] = useState(false);
34
64
  const [devicePollingEnabled, setDevicePollingEnabled] = useState(false);
35
65
  const [cliAutoApproveComplete, setCliAutoApproveComplete] = useState(false);
@@ -42,6 +72,8 @@ export const useGeneralTab = ({
42
72
  const channels = status?.channels ?? null;
43
73
  const repo = status?.repo || null;
44
74
  const syncCron = status?.syncCron || null;
75
+ const openAiCompatApi = status?.alphaclaw?.features?.openaiCompatApi || null;
76
+ const hasOpenAiCompatApiStatus = typeof openAiCompatApi?.enabled === "boolean";
45
77
  const openclawVersion = status?.openclawVersion || null;
46
78
 
47
79
  const hasUnpaired = ALL_CHANNELS.some((channel) => {
@@ -134,6 +166,14 @@ export const useGeneralTab = ({
134
166
  );
135
167
  }, [syncCron?.enabled, syncCron?.schedule]);
136
168
 
169
+ useEffect(() => {
170
+ if (!hasOpenAiCompatApiStatus) return;
171
+ const nextEnabled = openAiCompatApi.enabled === true;
172
+ setOpenAiCompatApiEnabled(nextEnabled);
173
+ setOpenAiCompatApiHydrated(true);
174
+ writeCachedOpenAiCompatApi(nextEnabled);
175
+ }, [hasOpenAiCompatApiStatus, openAiCompatApi?.enabled]);
176
+
137
177
  useEffect(
138
178
  () => () => {
139
179
  if (pairingRefreshTimerRef.current) {
@@ -196,6 +236,28 @@ export const useGeneralTab = ({
196
236
  });
197
237
  };
198
238
 
239
+ const handleOpenAiCompatApiToggle = async (enabled) => {
240
+ if (savingOpenAiCompatApi) return;
241
+ const previousEnabled = openAiCompatApiEnabled;
242
+ setOpenAiCompatApiEnabled(enabled);
243
+ setSavingOpenAiCompatApi(true);
244
+ try {
245
+ const data = await updateOpenAiCompatApiFeature(enabled);
246
+ if (!data.ok) {
247
+ throw new Error(data.error || "Could not save API setting");
248
+ }
249
+ writeCachedOpenAiCompatApi(enabled);
250
+ setOpenAiCompatApiHydrated(true);
251
+ showToast(`API ${enabled ? "enabled" : "disabled"}`, "success");
252
+ onRefreshStatuses();
253
+ } catch (err) {
254
+ setOpenAiCompatApiEnabled(previousEnabled);
255
+ showToast(err.message || "Could not save API setting", "error");
256
+ } finally {
257
+ setSavingOpenAiCompatApi(false);
258
+ }
259
+ };
260
+
199
261
  const handleApprove = async (id, channel, accountId = "") => {
200
262
  try {
201
263
  const result = await approvePairing(id, channel, accountId);
@@ -288,12 +350,18 @@ export const useGeneralTab = ({
288
350
  gatewayStatus,
289
351
  hasUnpaired,
290
352
  openclawVersion,
353
+ openAiCompatApi: {
354
+ ...(openAiCompatApi || {}),
355
+ enabled: openAiCompatApiEnabled,
356
+ hydrated: openAiCompatApiHydrated,
357
+ },
291
358
  pending,
292
359
  pairingsPolling: pairingsPoll.isPolling,
293
360
  pairingStatusRefreshing,
294
361
  repairingWatchdog,
295
362
  repo,
296
363
  savingSyncCron,
364
+ savingOpenAiCompatApi,
297
365
  syncCron,
298
366
  syncCronChoice,
299
367
  syncCronEnabled,
@@ -306,6 +374,7 @@ export const useGeneralTab = ({
306
374
  handleDeviceApprove,
307
375
  handleDeviceReject,
308
376
  handleOpenDashboard,
377
+ handleOpenAiCompatApiToggle,
309
378
  handleReject,
310
379
  handleSyncCronChoiceChange,
311
380
  handleWatchdogRepair,
@@ -0,0 +1,52 @@
1
+ import { h } from "preact";
2
+ import htm from "htm";
3
+
4
+ const html = htm.bind(h);
5
+
6
+ const RowAccessoryChevron = () => html`
7
+ <svg
8
+ width="14"
9
+ height="14"
10
+ viewBox="0 0 16 16"
11
+ fill="none"
12
+ class="text-fg-dim"
13
+ aria-hidden="true"
14
+ >
15
+ <path
16
+ d="M3.5 6L8 10.5L12.5 6"
17
+ stroke="currentColor"
18
+ stroke-width="2"
19
+ stroke-linecap="round"
20
+ stroke-linejoin="round"
21
+ />
22
+ </svg>
23
+ `;
24
+
25
+ export const RowAccessorySelect = ({
26
+ ariaLabel = "",
27
+ title = "",
28
+ value = "",
29
+ disabled = false,
30
+ onChange = () => {},
31
+ children = null,
32
+ }) => html`
33
+ <label
34
+ class=${`relative inline-flex shrink-0 items-center justify-end max-w-[12rem] min-w-[5.5rem] ${disabled
35
+ ? "opacity-50 cursor-not-allowed"
36
+ : "cursor-pointer"}`}
37
+ >
38
+ <select
39
+ aria-label=${ariaLabel}
40
+ title=${title || ariaLabel}
41
+ value=${value}
42
+ disabled=${disabled}
43
+ onInput=${(event) => onChange(String(event.currentTarget?.value ?? ""))}
44
+ class="appearance-none bg-transparent border-0 py-0 pl-0 pr-5 w-full text-right text-xs text-fg-muted hover:text-body cursor-pointer focus:outline-none focus-visible:ring-1 focus-visible:ring-border rounded disabled:cursor-not-allowed truncate"
45
+ >
46
+ ${children}
47
+ </select>
48
+ <span class="pointer-events-none absolute right-0 top-1/2 -translate-y-1/2">
49
+ <${RowAccessoryChevron} />
50
+ </span>
51
+ </label>
52
+ `;
@@ -550,6 +550,25 @@ export async function updateSyncCron(payload) {
550
550
  return data;
551
551
  }
552
552
 
553
+ export async function updateOpenAiCompatApiFeature(enabled) {
554
+ const res = await authFetch("/api/alphaclaw/config/features/openai-compat-api", {
555
+ method: "PUT",
556
+ headers: { "Content-Type": "application/json" },
557
+ body: JSON.stringify({ enabled }),
558
+ });
559
+ const text = await res.text();
560
+ let data;
561
+ try {
562
+ data = text ? JSON.parse(text) : {};
563
+ } catch {
564
+ throw new Error(text || "Could not parse AlphaClaw config response");
565
+ }
566
+ if (!res.ok) {
567
+ throw new Error(data.error || text || `HTTP ${res.status}`);
568
+ }
569
+ return data;
570
+ }
571
+
553
572
  export async function fetchCronJobs({ sortBy = "nextRunAtMs", sortDir = "asc" } = {}) {
554
573
  const params = new URLSearchParams();
555
574
  if (sortBy) params.set("sortBy", String(sortBy));
@@ -857,6 +876,13 @@ export const fetchModelStatus = async () => {
857
876
  return res.json();
858
877
  };
859
878
 
879
+ export const fetchThinkingOptions = async (modelKey) => {
880
+ const normalized = String(modelKey || "").trim();
881
+ const qs = new URLSearchParams({ modelKey: normalized });
882
+ const res = await authFetch(`/api/models/thinking-options?${qs.toString()}`);
883
+ return res.json();
884
+ };
885
+
860
886
  export const setPrimaryModel = async (modelKey) => {
861
887
  const res = await authFetch("/api/models/set", {
862
888
  method: "POST",
@@ -4,6 +4,7 @@ import { getFeaturedModels } from "./model-config.js";
4
4
 
5
5
  export const kModelCatalogCacheKey = "/api/models";
6
6
  export const kModelCatalogPollIntervalMs = 3000;
7
+ export const kDefaultOnboardingModelKey = "anthropic/claude-opus-4-8";
7
8
 
8
9
  export const getModelCatalogModels = (payload) =>
9
10
  Array.isArray(payload?.models) ? payload.models : [];
@@ -26,6 +27,11 @@ export const getInitialOnboardingModelKey = ({
26
27
  } = {}) => {
27
28
  const normalizedCurrent = String(currentModelKey || "").trim();
28
29
  if (normalizedCurrent) return normalizedCurrent;
30
+ const catalogHasKey = (key) =>
31
+ catalog.some((model) => String(model?.key || "") === key);
32
+ if (catalogHasKey(kDefaultOnboardingModelKey)) {
33
+ return kDefaultOnboardingModelKey;
34
+ }
29
35
  const featuredModels = getFeaturedModels(catalog);
30
36
  return String(featuredModels[0]?.key || catalog[0]?.key || "");
31
37
  };
@@ -9,6 +9,10 @@ export const getAuthProviderFromModelProvider = (provider) => {
9
9
  };
10
10
 
11
11
  export const kFeaturedModelDefs = [
12
+ {
13
+ label: "Opus 4.8",
14
+ preferredKeys: ["anthropic/claude-opus-4-8"],
15
+ },
12
16
  {
13
17
  label: "Opus 4.7",
14
18
  preferredKeys: ["anthropic/claude-opus-4-7"],
@@ -58,13 +62,14 @@ export const kProviderAuthFields = {
58
62
  linkText: "Get key",
59
63
  placeholder: "sk-ant-...",
60
64
  },
61
- {
62
- key: "ANTHROPIC_TOKEN",
63
- label: "Anthropic Setup Token",
64
- hint: "From claude setup-token (uses your Claude subscription)",
65
- linkText: "Get token",
66
- placeholder: "Token...",
67
- },
65
+ // Temporarily hidden — setup-token flow is not supported in onboarding yet.
66
+ // {
67
+ // key: "ANTHROPIC_TOKEN",
68
+ // label: "Anthropic Setup Token",
69
+ // hint: "From claude setup-token (uses your Claude subscription)",
70
+ // linkText: "Get token",
71
+ // placeholder: "Token...",
72
+ // },
68
73
  ],
69
74
  openai: [
70
75
  {
@@ -31,3 +31,7 @@ export const kAgentLastSessionKey = "alphaclaw.agent.lastSessionKey";
31
31
 
32
32
  // --- Chat ---
33
33
  export const kChatSessionDraftsStorageKey = "alphaclaw.chat.sessionDrafts";
34
+
35
+ // --- Features ---
36
+ export const kOpenAiCompatApiFeatureCacheKey =
37
+ "alphaclaw.features.openAiCompatApi.enabled";