@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.
- package/README.md +25 -0
- package/lib/public/css/tailwind.generated.css +1 -1
- package/lib/public/dist/app.bundle.js +1858 -1758
- package/lib/public/js/components/agents-tab/agent-overview/model-card.js +59 -7
- package/lib/public/js/components/agents-tab/agent-overview/use-model-card.js +124 -0
- package/lib/public/js/components/api-feature-panel.js +76 -0
- package/lib/public/js/components/envars.js +1 -1
- package/lib/public/js/components/general/index.js +6 -0
- package/lib/public/js/components/general/use-general-tab.js +69 -0
- package/lib/public/js/components/row-accessory-select.js +52 -0
- package/lib/public/js/lib/api.js +26 -0
- package/lib/public/js/lib/model-catalog.js +6 -0
- package/lib/public/js/lib/model-config.js +12 -7
- package/lib/public/js/lib/storage-keys.js +4 -0
- package/lib/public/js/lib/thinking-levels.js +37 -0
- package/lib/server/agents/agents.js +33 -7
- package/lib/server/agents/channels.js +4 -2
- package/lib/server/alphaclaw-config.js +99 -0
- package/lib/server/chat-ws.js +4 -1
- package/lib/server/constants.js +73 -0
- package/lib/server/cost-utils.js +2 -0
- package/lib/server/db/auth/index.js +147 -0
- package/lib/server/db/auth/schema.js +17 -0
- package/lib/server/gateway.js +321 -20
- package/lib/server/helpers.js +1 -3
- package/lib/server/init/register-server-routes.js +45 -18
- package/lib/server/init/runtime-init.js +4 -0
- package/lib/server/init/server-lifecycle.js +1 -24
- package/lib/server/login-throttle.js +261 -60
- package/lib/server/model-catalog-bootstrap.json +5 -0
- package/lib/server/onboarding/index.js +2 -2
- package/lib/server/onboarding/openclaw.js +27 -3
- package/lib/server/openclaw-thinking.js +103 -0
- package/lib/server/openclaw-version.js +1 -1
- package/lib/server/routes/agents.js +10 -3
- package/lib/server/routes/models.js +35 -1
- package/lib/server/routes/onboarding.js +2 -2
- package/lib/server/routes/proxy.js +219 -1
- package/lib/server/routes/system.js +63 -2
- package/lib/server/usage-tracker-config.js +52 -1
- package/lib/server.js +60 -22
- 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
|
-
|
|
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
|
-
${
|
|
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
|
+
`;
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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";
|