@swarmclawai/swarmclaw 1.2.3 → 1.2.4
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 +6 -0
- package/package.json +1 -1
- package/src/app/api/providers/[id]/models/route.test.ts +60 -0
- package/src/app/api/providers/[id]/models/route.ts +33 -1
- package/src/app/api/providers/[id]/route.ts +30 -1
- package/src/components/agents/agent-sheet.tsx +46 -20
- package/src/components/agents/inspector-panel.tsx +15 -4
- package/src/components/auth/setup-wizard/index.tsx +23 -14
- package/src/components/auth/setup-wizard/shared.tsx +2 -2
- package/src/components/auth/setup-wizard/step-agents.tsx +50 -37
- package/src/components/auth/setup-wizard/step-connect.tsx +47 -16
- package/src/components/auth/setup-wizard/types.ts +6 -4
- package/src/components/auth/setup-wizard/utils.test.ts +38 -8
- package/src/components/auth/setup-wizard/utils.ts +13 -7
- package/src/components/providers/provider-list.tsx +92 -18
- package/src/components/providers/provider-sheet.tsx +36 -29
- package/src/components/shared/model-combobox.tsx +5 -4
- package/src/lib/agent-provider-options.test.ts +152 -0
- package/src/lib/agent-provider-options.ts +84 -0
- package/src/lib/providers/index.test.ts +78 -0
- package/src/lib/providers/index.ts +13 -10
- package/src/lib/server/agents/agent-runtime-config.ts +5 -5
- package/src/lib/server/storage-normalization.test.ts +50 -0
- package/src/lib/server/storage-normalization.ts +23 -0
- package/src/types/index.ts +11 -10
- package/src/views/settings/section-providers.tsx +2 -2
|
@@ -36,9 +36,10 @@ function ModelCombobox({
|
|
|
36
36
|
const fetched = useRef<string | null>(null)
|
|
37
37
|
|
|
38
38
|
const effectiveEndpoint = endpointOverride || provider?.endpoint || null
|
|
39
|
+
const supportsModelDiscovery = Boolean(provider && provider.setupProvider !== 'custom')
|
|
39
40
|
|
|
40
41
|
const fetchModels = useCallback(async (force?: boolean) => {
|
|
41
|
-
if (!provider) return
|
|
42
|
+
if (!provider || !supportsModelDiscovery) return
|
|
42
43
|
const cacheKey = `${provider.provider}|${effectiveEndpoint || ''}|${provider.credentialId || ''}`
|
|
43
44
|
if (!force && fetched.current === cacheKey) return
|
|
44
45
|
fetched.current = cacheKey
|
|
@@ -63,11 +64,21 @@ function ModelCombobox({
|
|
|
63
64
|
} finally {
|
|
64
65
|
setLoading(false)
|
|
65
66
|
}
|
|
66
|
-
}, [provider, effectiveEndpoint])
|
|
67
|
+
}, [provider, effectiveEndpoint, supportsModelDiscovery])
|
|
67
68
|
|
|
68
69
|
useEffect(() => {
|
|
70
|
+
if (!provider) {
|
|
71
|
+
setModels([])
|
|
72
|
+
setFetchError('')
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
if (!supportsModelDiscovery) {
|
|
76
|
+
setModels(provider.defaultModel ? [provider.defaultModel] : [])
|
|
77
|
+
setFetchError('')
|
|
78
|
+
return
|
|
79
|
+
}
|
|
69
80
|
fetchModels()
|
|
70
|
-
}, [fetchModels])
|
|
81
|
+
}, [fetchModels, provider, supportsModelDiscovery])
|
|
71
82
|
|
|
72
83
|
// Close dropdown on outside click
|
|
73
84
|
useEffect(() => {
|
|
@@ -116,38 +127,40 @@ function ModelCombobox({
|
|
|
116
127
|
</svg>
|
|
117
128
|
</a>
|
|
118
129
|
)}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
130
|
+
{supportsModelDiscovery && (
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
onClick={() => {
|
|
134
|
+
if (models.length > 0 && !loading) {
|
|
135
|
+
setOpen(!open)
|
|
136
|
+
if (!open) setSearch(value)
|
|
137
|
+
} else {
|
|
138
|
+
fetchModels(true)
|
|
139
|
+
}
|
|
140
|
+
}}
|
|
141
|
+
disabled={loading}
|
|
142
|
+
className="text-text-3 hover:text-accent-bright transition-colors bg-transparent border-none cursor-pointer disabled:opacity-40"
|
|
143
|
+
title={models.length > 0 ? 'Show models' : 'Fetch available models'}
|
|
144
|
+
>
|
|
145
|
+
{loading ? (
|
|
146
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" className="animate-spin">
|
|
147
|
+
<circle cx="7" cy="7" r="5.5" stroke="currentColor" strokeWidth="1.5" opacity="0.25" />
|
|
148
|
+
<path d="M12.5 7A5.5 5.5 0 0 0 7 1.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
149
|
+
</svg>
|
|
150
|
+
) : models.length > 0 ? (
|
|
151
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
152
|
+
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
153
|
+
</svg>
|
|
154
|
+
) : (
|
|
155
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
156
|
+
<path d="M1.5 7A5.5 5.5 0 1 1 7 12.5" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" />
|
|
157
|
+
<path d="M1.5 12.5V9.5H4.5" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" />
|
|
158
|
+
</svg>
|
|
159
|
+
)}
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
149
162
|
</div>
|
|
150
|
-
{fetchError && models.length === 0 && (
|
|
163
|
+
{fetchError && models.length === 0 && supportsModelDiscovery && (
|
|
151
164
|
<div className="mt-1 text-[11px] text-amber-300/80">{fetchError}</div>
|
|
152
165
|
)}
|
|
153
166
|
{open && filtered.length > 0 && (
|
|
@@ -423,7 +436,7 @@ export function StepAgents({
|
|
|
423
436
|
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
424
437
|
/>
|
|
425
438
|
</div>
|
|
426
|
-
{matchedProvider?.
|
|
439
|
+
{matchedProvider?.setupProvider === 'openclaw' ? (
|
|
427
440
|
<div className="md:col-span-2">
|
|
428
441
|
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Model</label>
|
|
429
442
|
<div className="flex items-center gap-3 px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg">
|
|
@@ -448,7 +461,7 @@ export function StepAgents({
|
|
|
448
461
|
provider={matchedProvider}
|
|
449
462
|
endpointOverride={draft.apiEndpoint}
|
|
450
463
|
onChange={(model) => onUpdateDraft(draft.id, { model })}
|
|
451
|
-
modelLibraryUrl={matchedProvider ? SETUP_PROVIDERS.find((sp) => sp.id === matchedProvider.
|
|
464
|
+
modelLibraryUrl={matchedProvider ? SETUP_PROVIDERS.find((sp) => sp.id === matchedProvider.setupProvider)?.modelLibraryUrl : null}
|
|
452
465
|
/>
|
|
453
466
|
</div>
|
|
454
467
|
)}
|
|
@@ -457,7 +470,7 @@ export function StepAgents({
|
|
|
457
470
|
value={draft.soul}
|
|
458
471
|
onChange={(soul) => onUpdateDraft(draft.id, { soul })}
|
|
459
472
|
/>
|
|
460
|
-
{matchedProvider?.
|
|
473
|
+
{matchedProvider?.setupProvider === 'openclaw' && (
|
|
461
474
|
<p className="mt-1.5 ml-1 text-[11px] text-text-3/70">
|
|
462
475
|
Synced to the gateway as SOUL.md on save.
|
|
463
476
|
</p>
|
|
@@ -5,7 +5,7 @@ import { api } from '@/lib/app/api-client'
|
|
|
5
5
|
import { dedup, errorMessage } from '@/lib/shared-utils'
|
|
6
6
|
import { getDefaultModelForProvider } from '@/lib/setup-defaults'
|
|
7
7
|
import { OpenClawDeployPanel } from '@/components/openclaw/openclaw-deploy-panel'
|
|
8
|
-
import type { Credential, Credentials, GatewayProfile, ProviderConfig } from '@/types'
|
|
8
|
+
import type { Credential, Credentials, GatewayProfile, ProviderId, ProviderConfig } from '@/types'
|
|
9
9
|
import type { StepConnectProps, CheckState, ProviderCheckResponse, ConfiguredProvider } from './types'
|
|
10
10
|
import {
|
|
11
11
|
formatEndpointHost,
|
|
@@ -35,7 +35,9 @@ export function StepConnect({
|
|
|
35
35
|
const [checkMessage, setCheckMessage] = useState('')
|
|
36
36
|
const [checkErrorCode, setCheckErrorCode] = useState<string | null>(null)
|
|
37
37
|
const [openclawDeviceId, setOpenclawDeviceId] = useState<string | null>(null)
|
|
38
|
-
const [providerSuggestedModel, setProviderSuggestedModel] = useState(
|
|
38
|
+
const [providerSuggestedModel, setProviderSuggestedModel] = useState(
|
|
39
|
+
editingProvider?.defaultModel || (provider === 'custom' ? '' : getDefaultModelForProvider(provider)),
|
|
40
|
+
)
|
|
39
41
|
const [saving, setSaving] = useState(false)
|
|
40
42
|
const [error, setError] = useState('')
|
|
41
43
|
const [existingCredentials, setExistingCredentials] = useState<Credential[]>([])
|
|
@@ -63,6 +65,7 @@ export function StepConnect({
|
|
|
63
65
|
const supportsEndpoint = selectedProvider.supportsEndpoint || isCustom
|
|
64
66
|
const keyIsOptional = (selectedProvider.optionalKey && !isOllamaCloud) || isCustom
|
|
65
67
|
const requiresVerifiedConnection = provider === 'openclaw'
|
|
68
|
+
const canCheckConnection = !isCustom
|
|
66
69
|
const openClawEndpointValue = provider === 'openclaw'
|
|
67
70
|
? (endpoint.trim() || selectedProvider.defaultEndpoint || 'http://localhost:18789/v1')
|
|
68
71
|
: null
|
|
@@ -162,6 +165,11 @@ export function StepConnect({
|
|
|
162
165
|
return
|
|
163
166
|
}
|
|
164
167
|
|
|
168
|
+
if (isCustom && !providerSuggestedModel.trim()) {
|
|
169
|
+
setError('Custom providers need a default model ID.')
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
165
173
|
setSaving(true)
|
|
166
174
|
setError('')
|
|
167
175
|
try {
|
|
@@ -187,17 +195,17 @@ export function StepConnect({
|
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
// Custom providers: create a ProviderConfig in the DB so agents can reference it
|
|
190
|
-
let resolvedProvider = provider
|
|
198
|
+
let resolvedProvider: ProviderId = provider
|
|
191
199
|
if (isCustom) {
|
|
192
200
|
const customConfig = await api<ProviderConfig>('POST', '/providers', {
|
|
193
201
|
name: providerLabel.trim() || 'Custom Provider',
|
|
194
202
|
baseUrl: endpoint.trim(),
|
|
195
|
-
models: providerSuggestedModel ? [providerSuggestedModel] : [],
|
|
196
|
-
requiresApiKey:
|
|
203
|
+
models: providerSuggestedModel.trim() ? [providerSuggestedModel.trim()] : [],
|
|
204
|
+
requiresApiKey: hasKeyOrCredential,
|
|
197
205
|
credentialId: nextCredentialId || null,
|
|
198
206
|
isEnabled: true,
|
|
199
207
|
})
|
|
200
|
-
resolvedProvider = customConfig.id as
|
|
208
|
+
resolvedProvider = customConfig.id as ProviderId
|
|
201
209
|
}
|
|
202
210
|
|
|
203
211
|
// Build a tokenized dashboard URL for OpenClaw so step-agents can link to it
|
|
@@ -209,11 +217,12 @@ export function StepConnect({
|
|
|
209
217
|
|
|
210
218
|
const configured: ConfiguredProvider = {
|
|
211
219
|
id: crypto.randomUUID(),
|
|
220
|
+
setupProvider: provider,
|
|
212
221
|
provider: resolvedProvider,
|
|
213
222
|
name: providerLabel.trim() || selectedProvider.name,
|
|
214
223
|
credentialId: nextCredentialId || null,
|
|
215
224
|
endpoint: supportsEndpoint ? (endpoint.trim() || selectedProvider.defaultEndpoint || null) : null,
|
|
216
|
-
defaultModel: providerSuggestedModel || (isCustom ? '' : getDefaultModelForProvider(provider)),
|
|
225
|
+
defaultModel: providerSuggestedModel.trim() || (isCustom ? '' : getDefaultModelForProvider(provider)),
|
|
217
226
|
gatewayProfileId: null,
|
|
218
227
|
notes: providerNotes.trim() || null,
|
|
219
228
|
tags: providerTags,
|
|
@@ -323,6 +332,26 @@ export function StepConnect({
|
|
|
323
332
|
</div>
|
|
324
333
|
)}
|
|
325
334
|
|
|
335
|
+
{isCustom && (
|
|
336
|
+
<div>
|
|
337
|
+
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">
|
|
338
|
+
Default model
|
|
339
|
+
</label>
|
|
340
|
+
<input
|
|
341
|
+
type="text"
|
|
342
|
+
value={providerSuggestedModel}
|
|
343
|
+
onChange={(e) => setProviderSuggestedModel(e.target.value)}
|
|
344
|
+
placeholder="e.g. gpt-4o-mini"
|
|
345
|
+
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-surface
|
|
346
|
+
text-text text-[14px] font-mono outline-none transition-all duration-200
|
|
347
|
+
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
348
|
+
/>
|
|
349
|
+
<p className="mt-1.5 text-[11px] text-text-3">
|
|
350
|
+
Save the model ID you want starter agents to use with this provider. You can change it later per agent.
|
|
351
|
+
</p>
|
|
352
|
+
</div>
|
|
353
|
+
)}
|
|
354
|
+
|
|
326
355
|
{provider === 'openclaw' && (
|
|
327
356
|
<div className="rounded-[14px] border border-white/[0.08] bg-surface p-4 space-y-4">
|
|
328
357
|
<OpenClawDeployPanel
|
|
@@ -536,17 +565,19 @@ export function StepConnect({
|
|
|
536
565
|
>
|
|
537
566
|
Back
|
|
538
567
|
</button>
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
568
|
+
{canCheckConnection && (
|
|
569
|
+
<button
|
|
570
|
+
onClick={runConnectionCheck}
|
|
571
|
+
disabled={checkState === 'checking' || saving}
|
|
572
|
+
className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-white/[0.03] text-text text-[14px]
|
|
573
|
+
font-display font-600 cursor-pointer hover:bg-white/[0.06] transition-all duration-200 disabled:opacity-40"
|
|
574
|
+
>
|
|
575
|
+
{checkState === 'checking' ? 'Checking...' : 'Check Connection'}
|
|
576
|
+
</button>
|
|
577
|
+
)}
|
|
547
578
|
<button
|
|
548
579
|
onClick={saveProvider}
|
|
549
|
-
disabled={(requiresKey && !hasKeyOrCredential) || saving}
|
|
580
|
+
disabled={(requiresKey && !hasKeyOrCredential) || (isCustom && !providerSuggestedModel.trim()) || saving}
|
|
550
581
|
className="px-8 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600
|
|
551
582
|
cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
|
|
552
583
|
shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GatewayProfile } from '@/types'
|
|
1
|
+
import type { GatewayProfile, ProviderId } from '@/types'
|
|
2
2
|
import type { SetupProvider } from '@/lib/setup-defaults'
|
|
3
3
|
|
|
4
4
|
export type SetupStep = 'profile' | 'providers' | 'connect' | 'agents' | 'next' | 'done'
|
|
@@ -34,7 +34,8 @@ export interface SetupWizardProps {
|
|
|
34
34
|
|
|
35
35
|
export interface ConfiguredProvider {
|
|
36
36
|
id: string
|
|
37
|
-
|
|
37
|
+
setupProvider: SetupProvider
|
|
38
|
+
provider: ProviderId
|
|
38
39
|
name: string
|
|
39
40
|
credentialId: string | null
|
|
40
41
|
endpoint: string | null
|
|
@@ -56,7 +57,8 @@ export interface StarterDraftAgent {
|
|
|
56
57
|
systemPrompt: string
|
|
57
58
|
soul: string
|
|
58
59
|
providerConfigId: string | null
|
|
59
|
-
|
|
60
|
+
setupProvider: SetupProvider | null
|
|
61
|
+
provider: ProviderId | null
|
|
60
62
|
model: string
|
|
61
63
|
credentialId: string | null
|
|
62
64
|
apiEndpoint: string | null
|
|
@@ -77,7 +79,7 @@ export interface StarterDraftAgent {
|
|
|
77
79
|
export interface CreatedAgentSummary {
|
|
78
80
|
id: string
|
|
79
81
|
name: string
|
|
80
|
-
provider:
|
|
82
|
+
provider: ProviderId
|
|
81
83
|
providerName: string
|
|
82
84
|
}
|
|
83
85
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
isLocalOpenClawEndpoint,
|
|
7
7
|
resolveOpenClawDashboardUrl,
|
|
8
8
|
getOpenClawErrorHint,
|
|
9
|
+
requiresSetupProviderVerification,
|
|
9
10
|
withHttpScheme,
|
|
10
11
|
buildStarterDrafts,
|
|
11
12
|
preferredConfiguredProvider,
|
|
@@ -165,21 +166,24 @@ test('withHttpScheme preserves wss://', () => {
|
|
|
165
166
|
// buildStarterDrafts — OpenClaw provider handling
|
|
166
167
|
// ---------------------------------------------------------------------------
|
|
167
168
|
|
|
168
|
-
function makeConfiguredProvider(overrides: Partial<ConfiguredProvider> & {
|
|
169
|
+
function makeConfiguredProvider(overrides: Partial<ConfiguredProvider> & { setupProvider: ConfiguredProvider['setupProvider']; provider?: ConfiguredProvider['provider'] }): ConfiguredProvider {
|
|
170
|
+
const { setupProvider, provider = setupProvider, ...rest } = overrides
|
|
169
171
|
return {
|
|
170
172
|
id: 'cp-1',
|
|
173
|
+
setupProvider,
|
|
174
|
+
provider,
|
|
171
175
|
name: 'Test Provider',
|
|
172
176
|
credentialId: null,
|
|
173
177
|
endpoint: null,
|
|
174
178
|
defaultModel: '',
|
|
175
179
|
gatewayProfileId: null,
|
|
176
180
|
verified: true,
|
|
177
|
-
...
|
|
181
|
+
...rest,
|
|
178
182
|
}
|
|
179
183
|
}
|
|
180
184
|
|
|
181
185
|
test('buildStarterDrafts assigns OpenClaw provider to drafts', () => {
|
|
182
|
-
const cp = makeConfiguredProvider({
|
|
186
|
+
const cp = makeConfiguredProvider({ setupProvider: 'openclaw', endpoint: 'http://localhost:18789' })
|
|
183
187
|
const drafts = buildStarterDrafts({
|
|
184
188
|
starterKitId: 'personal_assistant',
|
|
185
189
|
intentText: '',
|
|
@@ -188,12 +192,13 @@ test('buildStarterDrafts assigns OpenClaw provider to drafts', () => {
|
|
|
188
192
|
assert.ok(drafts.length > 0, 'should produce at least one draft')
|
|
189
193
|
for (const d of drafts) {
|
|
190
194
|
assert.equal(d.provider, 'openclaw')
|
|
195
|
+
assert.equal(d.setupProvider, 'openclaw')
|
|
191
196
|
assert.equal(d.providerConfigId, cp.id)
|
|
192
197
|
}
|
|
193
198
|
})
|
|
194
199
|
|
|
195
200
|
test('buildStarterDrafts OpenClaw drafts use empty model (not "default")', () => {
|
|
196
|
-
const cp = makeConfiguredProvider({
|
|
201
|
+
const cp = makeConfiguredProvider({ setupProvider: 'openclaw', defaultModel: '' })
|
|
197
202
|
const drafts = buildStarterDrafts({
|
|
198
203
|
starterKitId: 'personal_assistant',
|
|
199
204
|
intentText: '',
|
|
@@ -206,7 +211,7 @@ test('buildStarterDrafts OpenClaw drafts use empty model (not "default")', () =>
|
|
|
206
211
|
})
|
|
207
212
|
|
|
208
213
|
test('buildStarterDrafts OpenClaw drafts inherit endpoint from provider', () => {
|
|
209
|
-
const cp = makeConfiguredProvider({
|
|
214
|
+
const cp = makeConfiguredProvider({ setupProvider: 'openclaw', endpoint: 'http://10.0.0.5:18789' })
|
|
210
215
|
const drafts = buildStarterDrafts({
|
|
211
216
|
starterKitId: 'personal_assistant',
|
|
212
217
|
intentText: '',
|
|
@@ -219,7 +224,7 @@ test('buildStarterDrafts OpenClaw drafts inherit endpoint from provider', () =>
|
|
|
219
224
|
|
|
220
225
|
test('buildStarterDrafts carries dashboardUrl through from ConfiguredProvider', () => {
|
|
221
226
|
const cp = makeConfiguredProvider({
|
|
222
|
-
|
|
227
|
+
setupProvider: 'openclaw',
|
|
223
228
|
endpoint: 'http://localhost:18789',
|
|
224
229
|
dashboardUrl: 'http://localhost:18789?token=my-secret',
|
|
225
230
|
})
|
|
@@ -228,11 +233,36 @@ test('buildStarterDrafts carries dashboardUrl through from ConfiguredProvider',
|
|
|
228
233
|
})
|
|
229
234
|
|
|
230
235
|
test('preferredConfiguredProvider picks openclaw provider for openclaw template', () => {
|
|
231
|
-
const openclawCp = makeConfiguredProvider({ id: 'oc-1',
|
|
232
|
-
const openaiCp = makeConfiguredProvider({ id: 'oai-1',
|
|
236
|
+
const openclawCp = makeConfiguredProvider({ id: 'oc-1', setupProvider: 'openclaw' })
|
|
237
|
+
const openaiCp = makeConfiguredProvider({ id: 'oai-1', setupProvider: 'openai' })
|
|
233
238
|
const result = preferredConfiguredProvider(
|
|
234
239
|
{ id: 'tmpl-1', name: 'Test', description: '', systemPrompt: '', tools: [], recommendedProviders: ['openclaw'] },
|
|
235
240
|
[openaiCp, openclawCp],
|
|
236
241
|
)
|
|
237
242
|
assert.equal(result?.id, 'oc-1')
|
|
238
243
|
})
|
|
244
|
+
|
|
245
|
+
test('buildStarterDrafts carries custom runtime provider ids alongside custom setup provider state', () => {
|
|
246
|
+
const cp = makeConfiguredProvider({
|
|
247
|
+
setupProvider: 'custom',
|
|
248
|
+
provider: 'custom-openrouter',
|
|
249
|
+
defaultModel: 'openai/gpt-4.1',
|
|
250
|
+
})
|
|
251
|
+
const drafts = buildStarterDrafts({
|
|
252
|
+
starterKitId: 'personal_assistant',
|
|
253
|
+
intentText: '',
|
|
254
|
+
configuredProviders: [cp],
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
for (const draft of drafts) {
|
|
258
|
+
assert.equal(draft.setupProvider, 'custom')
|
|
259
|
+
assert.equal(draft.provider, 'custom-openrouter')
|
|
260
|
+
assert.equal(draft.model, 'openai/gpt-4.1')
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('requiresSetupProviderVerification skips custom providers', () => {
|
|
265
|
+
assert.equal(requiresSetupProviderVerification('custom'), false)
|
|
266
|
+
assert.equal(requiresSetupProviderVerification('openclaw'), false)
|
|
267
|
+
assert.equal(requiresSetupProviderVerification('openai'), true)
|
|
268
|
+
})
|
|
@@ -91,6 +91,10 @@ export function getOpenClawErrorHint(message: string): string | null {
|
|
|
91
91
|
return null
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
export function requiresSetupProviderVerification(provider: SetupProvider | null | undefined): boolean {
|
|
95
|
+
return provider != null && provider !== 'openclaw' && provider !== 'custom'
|
|
96
|
+
}
|
|
97
|
+
|
|
94
98
|
export function preferredConfiguredProvider(
|
|
95
99
|
template: StarterKitAgentTemplate,
|
|
96
100
|
configuredProviders: ConfiguredProvider[],
|
|
@@ -103,12 +107,12 @@ export function preferredConfiguredProvider(
|
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
if (fallbackProvider) {
|
|
106
|
-
const exact = configuredProviders.find((candidate) => candidate.
|
|
110
|
+
const exact = configuredProviders.find((candidate) => candidate.setupProvider === fallbackProvider)
|
|
107
111
|
if (exact) return exact
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
for (const provider of template.recommendedProviders || []) {
|
|
111
|
-
const exact = configuredProviders.find((candidate) => candidate.
|
|
115
|
+
const exact = configuredProviders.find((candidate) => candidate.setupProvider === provider)
|
|
112
116
|
if (exact) return exact
|
|
113
117
|
}
|
|
114
118
|
|
|
@@ -134,14 +138,15 @@ export function buildStarterDrafts(args: {
|
|
|
134
138
|
template,
|
|
135
139
|
configuredProviders,
|
|
136
140
|
previous?.providerConfigId,
|
|
137
|
-
previous?.
|
|
141
|
+
previous?.setupProvider,
|
|
138
142
|
)
|
|
139
|
-
const oldProvider = previous?.
|
|
143
|
+
const oldProvider = previous?.setupProvider || null
|
|
144
|
+
const previousModel = previous?.model || ''
|
|
140
145
|
const oldProviderDefault = oldProvider ? getDefaultModelForProvider(oldProvider) : ''
|
|
141
146
|
const nextProviderDefault = configuredProvider?.defaultModel || ''
|
|
142
147
|
const shouldRefreshModel =
|
|
143
|
-
!
|
|
144
|
-
|| (oldProvider !== configuredProvider?.
|
|
148
|
+
!previousModel.trim()
|
|
149
|
+
|| (oldProvider !== configuredProvider?.setupProvider && previousModel === oldProviderDefault)
|
|
145
150
|
|
|
146
151
|
return {
|
|
147
152
|
id,
|
|
@@ -151,8 +156,9 @@ export function buildStarterDrafts(args: {
|
|
|
151
156
|
systemPrompt: previous?.systemPrompt || applyIntentContext(template.systemPrompt, intentText),
|
|
152
157
|
soul: previous?.soul || '',
|
|
153
158
|
providerConfigId: configuredProvider?.id || null,
|
|
159
|
+
setupProvider: configuredProvider?.setupProvider || null,
|
|
154
160
|
provider: configuredProvider?.provider || null,
|
|
155
|
-
model: shouldRefreshModel ? nextProviderDefault :
|
|
161
|
+
model: shouldRefreshModel ? nextProviderDefault : previousModel,
|
|
156
162
|
credentialId: configuredProvider?.credentialId || null,
|
|
157
163
|
apiEndpoint: configuredProvider?.endpoint || null,
|
|
158
164
|
gatewayProfileId: configuredProvider?.gatewayProfileId || null,
|
|
@@ -66,13 +66,13 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
66
66
|
const handleToggle = async (e: React.MouseEvent, id: string, currentEnabled: boolean) => {
|
|
67
67
|
e.stopPropagation()
|
|
68
68
|
await api('PUT', `/providers/${id}`, { isEnabled: !currentEnabled })
|
|
69
|
-
await loadProviderConfigs()
|
|
69
|
+
await Promise.all([loadProviderConfigs(), loadProviders()])
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const handleDelete = async (e: React.MouseEvent, id: string) => {
|
|
73
73
|
e.stopPropagation()
|
|
74
74
|
await api('DELETE', `/providers/${id}`)
|
|
75
|
-
await loadProviderConfigs()
|
|
75
|
+
await Promise.all([loadProviderConfigs(), loadProviders()])
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
const handleEditGateway = (id: string | null) => {
|
|
@@ -219,18 +219,27 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
const
|
|
222
|
+
const customProviderConfigs = providerConfigs.filter((config) => config.type === 'custom')
|
|
223
|
+
const customConfigIds = new Set(customProviderConfigs.map((config) => config.id))
|
|
224
|
+
const builtinOverrides = new Map(
|
|
225
|
+
providerConfigs
|
|
226
|
+
.filter((config) => config.type === 'builtin')
|
|
227
|
+
.map((config) => [config.id, config]),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
const builtinItems = providers
|
|
231
|
+
.filter((provider) => !customConfigIds.has(String(provider.id)))
|
|
232
|
+
.map((p) => ({
|
|
224
233
|
id: p.id,
|
|
225
234
|
name: p.name,
|
|
226
235
|
type: 'builtin' as const,
|
|
227
236
|
models: p.models,
|
|
228
237
|
requiresApiKey: p.requiresApiKey,
|
|
229
|
-
isEnabled:
|
|
238
|
+
isEnabled: builtinOverrides.get(String(p.id))?.isEnabled !== false,
|
|
230
239
|
isConnected: !p.requiresApiKey || Object.values(credentials).some((c) => c.provider === p.id),
|
|
231
|
-
|
|
240
|
+
}))
|
|
232
241
|
|
|
233
|
-
const customItems =
|
|
242
|
+
const customItems = customProviderConfigs.map((c) => ({
|
|
234
243
|
id: c.id,
|
|
235
244
|
name: c.name,
|
|
236
245
|
type: 'custom' as const,
|
|
@@ -241,6 +250,8 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
241
250
|
}))
|
|
242
251
|
|
|
243
252
|
const allItems = [...builtinItems, ...customItems]
|
|
253
|
+
const enabledItems = allItems.filter((item) => item.isEnabled)
|
|
254
|
+
const disabledItems = allItems.filter((item) => !item.isEnabled)
|
|
244
255
|
const gatewayNameById = new Map(gatewayProfiles.map((gateway) => [gateway.id, gateway.name]))
|
|
245
256
|
const runtimeHealthByGateway = externalAgents.reduce<Record<string, { total: number; active: number; lastHeartbeatAt: number | null }>>((acc, runtime) => {
|
|
246
257
|
if (!runtime.gatewayProfileId) return acc
|
|
@@ -273,7 +284,7 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
273
284
|
)}
|
|
274
285
|
</div>
|
|
275
286
|
<div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
|
|
276
|
-
{
|
|
287
|
+
{enabledItems.map((item, idx) => (
|
|
277
288
|
<div
|
|
278
289
|
key={item.id}
|
|
279
290
|
role="button"
|
|
@@ -299,7 +310,7 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
299
310
|
${item.type === 'builtin' ? 'bg-white/[0.04] text-text-3' : 'bg-accent-bright/10 text-[#6366F1]'}`}>
|
|
300
311
|
{item.type === 'builtin' ? 'Built-in' : 'Custom'}
|
|
301
312
|
</span>
|
|
302
|
-
{!inSidebar &&
|
|
313
|
+
{!inSidebar && (
|
|
303
314
|
<>
|
|
304
315
|
<div
|
|
305
316
|
onClick={(e) => handleToggle(e, item.id, item.isEnabled)}
|
|
@@ -311,15 +322,17 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
311
322
|
style={item.isEnabled ? { animation: 'spring-in 0.3s var(--ease-spring)' } : undefined}
|
|
312
323
|
/>
|
|
313
324
|
</div>
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
<
|
|
321
|
-
|
|
322
|
-
|
|
325
|
+
{item.type === 'custom' && (
|
|
326
|
+
<button
|
|
327
|
+
onClick={(e) => handleDelete(e, item.id)}
|
|
328
|
+
className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
|
|
329
|
+
title="Delete provider"
|
|
330
|
+
>
|
|
331
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
332
|
+
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
333
|
+
</svg>
|
|
334
|
+
</button>
|
|
335
|
+
)}
|
|
323
336
|
</>
|
|
324
337
|
)}
|
|
325
338
|
<StatusDot
|
|
@@ -339,6 +352,67 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
339
352
|
</div>
|
|
340
353
|
))}
|
|
341
354
|
</div>
|
|
355
|
+
{!inSidebar && disabledItems.length > 0 && (
|
|
356
|
+
<>
|
|
357
|
+
<div className="mt-8 mb-4 flex items-center justify-between">
|
|
358
|
+
<div className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">Disabled Providers</div>
|
|
359
|
+
</div>
|
|
360
|
+
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
361
|
+
{disabledItems.map((item, idx) => (
|
|
362
|
+
<div
|
|
363
|
+
key={item.id}
|
|
364
|
+
role="button"
|
|
365
|
+
tabIndex={0}
|
|
366
|
+
onClick={() => handleEdit(item.id)}
|
|
367
|
+
onKeyDown={(e) => {
|
|
368
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
369
|
+
e.preventDefault()
|
|
370
|
+
handleEdit(item.id)
|
|
371
|
+
}
|
|
372
|
+
}}
|
|
373
|
+
className="w-full text-left p-4 rounded-[14px] border transition-all duration-200
|
|
374
|
+
cursor-pointer bg-surface/60 border-white/[0.06] hover:bg-white/[0.02] hover:border-white/[0.12]"
|
|
375
|
+
style={{
|
|
376
|
+
animation: 'spring-in 0.5s var(--ease-spring) both',
|
|
377
|
+
animationDelay: `${(enabledItems.length + idx) * 0.05}s`
|
|
378
|
+
}}
|
|
379
|
+
>
|
|
380
|
+
<div className="flex items-center justify-between mb-1.5">
|
|
381
|
+
<span className="font-display text-[14px] font-600 text-text truncate">{item.name}</span>
|
|
382
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
383
|
+
<span className={`text-[10px] font-600 px-2 py-0.5 rounded-[5px] uppercase tracking-wider
|
|
384
|
+
${item.type === 'builtin' ? 'bg-white/[0.04] text-text-3' : 'bg-accent-bright/10 text-[#6366F1]'}`}>
|
|
385
|
+
{item.type === 'builtin' ? 'Built-in' : 'Custom'}
|
|
386
|
+
</span>
|
|
387
|
+
<div
|
|
388
|
+
onClick={(e) => handleToggle(e, item.id, item.isEnabled)}
|
|
389
|
+
className="w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0 bg-white/[0.08]"
|
|
390
|
+
>
|
|
391
|
+
<div className="absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-all" />
|
|
392
|
+
</div>
|
|
393
|
+
{item.type === 'custom' && (
|
|
394
|
+
<button
|
|
395
|
+
onClick={(e) => handleDelete(e, item.id)}
|
|
396
|
+
className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
|
|
397
|
+
title="Delete provider"
|
|
398
|
+
>
|
|
399
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
400
|
+
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
401
|
+
</svg>
|
|
402
|
+
</button>
|
|
403
|
+
)}
|
|
404
|
+
<StatusDot status="idle" pulse={false} />
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
<div className="text-[12px] text-text-3/60 font-mono truncate">
|
|
408
|
+
{item.models.slice(0, 3).join(', ')}
|
|
409
|
+
{item.models.length > 3 && ` +${item.models.length - 3}`}
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
))}
|
|
413
|
+
</div>
|
|
414
|
+
</>
|
|
415
|
+
)}
|
|
342
416
|
|
|
343
417
|
<div className="mt-8 mb-4 flex items-center justify-between">
|
|
344
418
|
<div className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">OpenClaw Gateways</div>
|