@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
package/README.md
CHANGED
|
@@ -190,6 +190,12 @@ The building blocks are the same: **agents, tools, memory, delegation, schedules
|
|
|
190
190
|
|
|
191
191
|
## Release Notes
|
|
192
192
|
|
|
193
|
+
### v1.2.4 Highlights
|
|
194
|
+
|
|
195
|
+
- **Custom providers in agent config**: agent setup and inline model switching now merge saved custom provider configs into the selectable provider list, so custom providers show up reliably even when the built-in provider feed is stale or incomplete.
|
|
196
|
+
- **Custom provider save-only flow**: the Providers screen no longer forces connection tests or live model discovery for custom providers; operators can save the endpoint, linked key, and manual model list directly.
|
|
197
|
+
- **Custom provider runtime routing**: saved custom-provider model lists and linked credentials now flow through the agent UI and runtime resolution paths consistently, including legacy `provider_configs` records normalized on load.
|
|
198
|
+
|
|
193
199
|
### v1.2.3 Highlights
|
|
194
200
|
|
|
195
201
|
- **Standalone asset staging repair**: `swarmclaw server` now copies `.next/static` and `public/` into the Next.js standalone runtime after the first build, preventing blank UI loads and 503s for CSS, JS, and image assets.
|
package/package.json
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
5
|
+
|
|
6
|
+
test('provider models route updates custom provider configs without creating model overrides', () => {
|
|
7
|
+
const output = runWithTempDataDir<{
|
|
8
|
+
customModels: string[]
|
|
9
|
+
overrideKeys: string[]
|
|
10
|
+
getPayload: { models: string[]; hasOverride: boolean }
|
|
11
|
+
}>(`
|
|
12
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
13
|
+
const routeMod = await import('./src/app/api/providers/[id]/models/route')
|
|
14
|
+
const storage = storageMod.default || storageMod
|
|
15
|
+
const route = routeMod.default || routeMod
|
|
16
|
+
|
|
17
|
+
const now = Date.now()
|
|
18
|
+
storage.saveProviderConfigs({
|
|
19
|
+
'custom-llama': {
|
|
20
|
+
id: 'custom-llama',
|
|
21
|
+
name: 'Llama.cpp',
|
|
22
|
+
type: 'custom',
|
|
23
|
+
baseUrl: 'http://localhost:8080/v1',
|
|
24
|
+
models: ['old-model'],
|
|
25
|
+
requiresApiKey: false,
|
|
26
|
+
credentialId: null,
|
|
27
|
+
isEnabled: true,
|
|
28
|
+
createdAt: now,
|
|
29
|
+
updatedAt: now,
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
await route.PUT(
|
|
34
|
+
new Request('http://local/api/providers/custom-llama/models', {
|
|
35
|
+
method: 'PUT',
|
|
36
|
+
headers: { 'content-type': 'application/json' },
|
|
37
|
+
body: JSON.stringify({ models: ['llama-3.1-8b', 'llama-3.1-70b'] }),
|
|
38
|
+
}),
|
|
39
|
+
{ params: Promise.resolve({ id: 'custom-llama' }) },
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const getResponse = await route.GET(
|
|
43
|
+
new Request('http://local/api/providers/custom-llama/models'),
|
|
44
|
+
{ params: Promise.resolve({ id: 'custom-llama' }) },
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
console.log(JSON.stringify({
|
|
48
|
+
customModels: storage.loadProviderConfigs()['custom-llama'].models,
|
|
49
|
+
overrideKeys: Object.keys(storage.loadModelOverrides()),
|
|
50
|
+
getPayload: await getResponse.json(),
|
|
51
|
+
}))
|
|
52
|
+
`, { prefix: 'swarmclaw-provider-model-route-test-' })
|
|
53
|
+
|
|
54
|
+
assert.deepEqual(output.customModels, ['llama-3.1-8b', 'llama-3.1-70b'])
|
|
55
|
+
assert.deepEqual(output.overrideKeys, [])
|
|
56
|
+
assert.deepEqual(output.getPayload, {
|
|
57
|
+
models: ['llama-3.1-8b', 'llama-3.1-70b'],
|
|
58
|
+
hasOverride: false,
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { loadModelOverrides, saveModelOverrides } from '@/lib/server/storage'
|
|
2
|
+
import { loadModelOverrides, loadProviderConfigs, saveModelOverrides, saveProviderConfigs } from '@/lib/server/storage'
|
|
3
3
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
4
|
import { getProviderList } from '@/lib/providers'
|
|
5
5
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
6
6
|
|
|
7
7
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
8
|
const { id } = await params
|
|
9
|
+
const providerConfigs = loadProviderConfigs()
|
|
10
|
+
const customProvider = providerConfigs[id]
|
|
11
|
+
if (customProvider?.type === 'custom') {
|
|
12
|
+
return NextResponse.json({ models: customProvider.models || [], hasOverride: false })
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
const overrides = loadModelOverrides()
|
|
10
16
|
const providers = getProviderList()
|
|
11
17
|
const provider = providers.find((p) => p.id === id)
|
|
@@ -17,6 +23,19 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
17
23
|
const { id } = await params
|
|
18
24
|
const { data: body, error } = await safeParseBody<{ models?: string[] }>(req)
|
|
19
25
|
if (error) return error
|
|
26
|
+
|
|
27
|
+
const providerConfigs = loadProviderConfigs()
|
|
28
|
+
const customProvider = providerConfigs[id]
|
|
29
|
+
if (customProvider?.type === 'custom') {
|
|
30
|
+
providerConfigs[id] = {
|
|
31
|
+
...customProvider,
|
|
32
|
+
models: body.models || [],
|
|
33
|
+
updatedAt: Date.now(),
|
|
34
|
+
}
|
|
35
|
+
saveProviderConfigs(providerConfigs)
|
|
36
|
+
return NextResponse.json({ models: providerConfigs[id].models })
|
|
37
|
+
}
|
|
38
|
+
|
|
20
39
|
const overrides = loadModelOverrides()
|
|
21
40
|
overrides[id] = body.models || []
|
|
22
41
|
saveModelOverrides(overrides)
|
|
@@ -25,6 +44,19 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
25
44
|
|
|
26
45
|
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
27
46
|
const { id } = await params
|
|
47
|
+
|
|
48
|
+
const providerConfigs = loadProviderConfigs()
|
|
49
|
+
const customProvider = providerConfigs[id]
|
|
50
|
+
if (customProvider?.type === 'custom') {
|
|
51
|
+
providerConfigs[id] = {
|
|
52
|
+
...customProvider,
|
|
53
|
+
models: [],
|
|
54
|
+
updatedAt: Date.now(),
|
|
55
|
+
}
|
|
56
|
+
saveProviderConfigs(providerConfigs)
|
|
57
|
+
return NextResponse.json({ ok: true })
|
|
58
|
+
}
|
|
59
|
+
|
|
28
60
|
const overrides = loadModelOverrides()
|
|
29
61
|
delete overrides[id]
|
|
30
62
|
saveModelOverrides(overrides)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
+
import { PROVIDERS } from '@/lib/providers'
|
|
2
3
|
import { loadProviderConfigs, saveProviderConfigs } from '@/lib/server/storage'
|
|
3
4
|
import { mutateItem, deleteItem, notFound, badRequest, type CollectionOps } from '@/lib/server/collection-helpers'
|
|
4
5
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
6
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
7
|
|
|
6
8
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
9
|
const ops: CollectionOps<any> = { load: loadProviderConfigs, save: saveProviderConfigs, topic: 'providers' }
|
|
@@ -18,8 +20,35 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
18
20
|
const { id } = await params
|
|
19
21
|
const { data: body, error } = await safeParseBody<Record<string, unknown>>(req)
|
|
20
22
|
if (error) return error
|
|
23
|
+
if (!ops.load()[id]) {
|
|
24
|
+
const builtin = PROVIDERS[id]
|
|
25
|
+
if (!builtin) return notFound()
|
|
26
|
+
|
|
27
|
+
const now = Date.now()
|
|
28
|
+
const configs = loadProviderConfigs()
|
|
29
|
+
configs[id] = {
|
|
30
|
+
...body,
|
|
31
|
+
id,
|
|
32
|
+
name: builtin.name,
|
|
33
|
+
type: 'builtin',
|
|
34
|
+
baseUrl: builtin.defaultEndpoint || '',
|
|
35
|
+
models: [...builtin.models],
|
|
36
|
+
requiresApiKey: builtin.requiresApiKey,
|
|
37
|
+
credentialId: null,
|
|
38
|
+
isEnabled: body.isEnabled !== false,
|
|
39
|
+
createdAt: now,
|
|
40
|
+
updatedAt: now,
|
|
41
|
+
}
|
|
42
|
+
saveProviderConfigs(configs)
|
|
43
|
+
notify('providers')
|
|
44
|
+
return NextResponse.json(configs[id])
|
|
45
|
+
}
|
|
21
46
|
const result = mutateItem(ops, id, (existing) => ({
|
|
22
|
-
...existing,
|
|
47
|
+
...existing,
|
|
48
|
+
...body,
|
|
49
|
+
id,
|
|
50
|
+
type: existing.type === 'builtin' ? 'builtin' : 'custom',
|
|
51
|
+
updatedAt: Date.now(),
|
|
23
52
|
}))
|
|
24
53
|
if (!result) return notFound()
|
|
25
54
|
return NextResponse.json(result)
|
|
@@ -28,6 +28,7 @@ import { resolveStoredOllamaMode } from '@/lib/ollama-mode'
|
|
|
28
28
|
import { errorMessage } from '@/lib/shared-utils'
|
|
29
29
|
import { getDefaultAgentToolIds } from '@/lib/agent-default-tools'
|
|
30
30
|
import { getEnabledExtensionIds, getEnabledToolIds } from '@/lib/capability-selection'
|
|
31
|
+
import { buildAgentSelectableProviders, resolveAgentSelectableProviderCredentials } from '@/lib/agent-provider-options'
|
|
31
32
|
|
|
32
33
|
const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
|
|
33
34
|
const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
|
|
@@ -46,6 +47,7 @@ const AUTO_SYNC_MODEL_PROVIDER_IDS = new Set<ProviderType>([
|
|
|
46
47
|
'ollama',
|
|
47
48
|
])
|
|
48
49
|
const CONNECTION_TEST_TIMEOUT_MS = 40_000
|
|
50
|
+
type AgentProviderId = string
|
|
49
51
|
|
|
50
52
|
type SafeAgentWallet = Omit<AgentWallet, 'encryptedPrivateKey'> & {
|
|
51
53
|
balanceAtomic?: string
|
|
@@ -171,6 +173,8 @@ export function AgentSheet() {
|
|
|
171
173
|
const loadProjects = useAppStore((s) => s.loadProjects)
|
|
172
174
|
const providers = useAppStore((s) => s.providers)
|
|
173
175
|
const loadProviders = useAppStore((s) => s.loadProviders)
|
|
176
|
+
const providerConfigs = useAppStore((s) => s.providerConfigs)
|
|
177
|
+
const loadProviderConfigs = useAppStore((s) => s.loadProviderConfigs)
|
|
174
178
|
const gatewayProfiles = useAppStore((s) => s.gatewayProfiles)
|
|
175
179
|
const loadGatewayProfiles = useAppStore((s) => s.loadGatewayProfiles)
|
|
176
180
|
const credentials = useAppStore((s) => s.credentials)
|
|
@@ -197,7 +201,7 @@ export function AgentSheet() {
|
|
|
197
201
|
const [soulInitial, setSoulInitial] = useState('')
|
|
198
202
|
const [soulSaveState, setSoulSaveState] = useState<'idle' | 'saved'>('idle')
|
|
199
203
|
const [systemPrompt, setSystemPrompt] = useState('')
|
|
200
|
-
const [provider, setProvider] = useState<
|
|
204
|
+
const [provider, setProvider] = useState<AgentProviderId>('claude-cli')
|
|
201
205
|
const [model, setModel] = useState('')
|
|
202
206
|
const [credentialId, setCredentialId] = useState<string | null>(null)
|
|
203
207
|
const [apiEndpoint, setApiEndpoint] = useState<string | null>(null)
|
|
@@ -308,8 +312,15 @@ export function AgentSheet() {
|
|
|
308
312
|
}
|
|
309
313
|
}, [])
|
|
310
314
|
|
|
311
|
-
const
|
|
312
|
-
|
|
315
|
+
const agentSelectableProviders = useMemo(
|
|
316
|
+
() => buildAgentSelectableProviders(providers, providerConfigs),
|
|
317
|
+
[providers, providerConfigs],
|
|
318
|
+
)
|
|
319
|
+
const currentProvider = agentSelectableProviders.find((p) => p.id === provider)
|
|
320
|
+
const providerCredentials = useMemo(
|
|
321
|
+
() => resolveAgentSelectableProviderCredentials(provider, credentials, providerConfigs),
|
|
322
|
+
[credentials, provider, providerConfigs],
|
|
323
|
+
)
|
|
313
324
|
const openclawCredentials = Object.values(credentials).filter((c) => c.provider === 'openclaw')
|
|
314
325
|
const openclawGatewayProfiles = gatewayProfiles.filter((item) => item.provider === 'openclaw')
|
|
315
326
|
const setAgentPrefill = useAppStore((s) => s.setAgentPrefill)
|
|
@@ -327,15 +338,15 @@ export function AgentSheet() {
|
|
|
327
338
|
? 'Global default'
|
|
328
339
|
: 'Built-in fallback'
|
|
329
340
|
const syncLiveProviderModels = useCallback(async (
|
|
330
|
-
providerId:
|
|
341
|
+
providerId: string,
|
|
331
342
|
nextCredentialId: string | null,
|
|
332
343
|
nextEndpoint: string | null,
|
|
333
344
|
nextOllamaMode: 'local' | 'cloud',
|
|
334
345
|
force = false,
|
|
335
346
|
): Promise<{ synced: boolean; models: string[] } | null> => {
|
|
336
347
|
if (openclawEnabled) return null
|
|
337
|
-
if (!AUTO_SYNC_MODEL_PROVIDER_IDS.has(providerId)) return null
|
|
338
|
-
const providerInfo =
|
|
348
|
+
if (!AUTO_SYNC_MODEL_PROVIDER_IDS.has(providerId as ProviderType)) return null
|
|
349
|
+
const providerInfo = agentSelectableProviders.find((item) => item.id === providerId)
|
|
339
350
|
if (!providerInfo?.supportsModelDiscovery) return null
|
|
340
351
|
|
|
341
352
|
const result = await fetchProviderModelDiscovery({
|
|
@@ -358,7 +369,7 @@ export function AgentSheet() {
|
|
|
358
369
|
|
|
359
370
|
setModel((currentModel) => currentModel.trim() || result.models[0] || '')
|
|
360
371
|
return { synced: !sameModels, models: result.models }
|
|
361
|
-
}, [loadProviders, openclawEnabled
|
|
372
|
+
}, [agentSelectableProviders, loadProviders, openclawEnabled])
|
|
362
373
|
|
|
363
374
|
const providerNeedsKey = !editing && (
|
|
364
375
|
(currentProvider?.requiresApiKey && providerCredentials.length === 0 && !addingKey) ||
|
|
@@ -371,7 +382,7 @@ export function AgentSheet() {
|
|
|
371
382
|
return
|
|
372
383
|
}
|
|
373
384
|
if (openclawEnabled) return
|
|
374
|
-
if (!AUTO_SYNC_MODEL_PROVIDER_IDS.has(provider)) return
|
|
385
|
+
if (!AUTO_SYNC_MODEL_PROVIDER_IDS.has(provider as ProviderType)) return
|
|
375
386
|
if (!currentProvider?.supportsModelDiscovery) return
|
|
376
387
|
|
|
377
388
|
const requiresCredential = currentProvider.requiresApiKey || (provider === 'ollama' && ollamaMode === 'cloud')
|
|
@@ -388,6 +399,7 @@ export function AgentSheet() {
|
|
|
388
399
|
if (open) {
|
|
389
400
|
loadSettings()
|
|
390
401
|
loadProviders()
|
|
402
|
+
loadProviderConfigs()
|
|
391
403
|
loadGatewayProfiles()
|
|
392
404
|
loadCredentials()
|
|
393
405
|
loadSkills()
|
|
@@ -637,7 +649,7 @@ export function AgentSheet() {
|
|
|
637
649
|
setModel(currentProvider.models[0])
|
|
638
650
|
}
|
|
639
651
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
640
|
-
}, [provider,
|
|
652
|
+
}, [provider, agentSelectableProviders])
|
|
641
653
|
|
|
642
654
|
// Reset test status when connection params change
|
|
643
655
|
useEffect(() => {
|
|
@@ -873,13 +885,18 @@ export function AgentSheet() {
|
|
|
873
885
|
|
|
874
886
|
const handleExport = () => {
|
|
875
887
|
if (!editing) return
|
|
888
|
+
const recommendedProviders = agentSelectableProviders.some((providerOption) => (
|
|
889
|
+
providerOption.id === editing.provider && providerOption.type === 'builtin'
|
|
890
|
+
))
|
|
891
|
+
? [editing.provider as ProviderType]
|
|
892
|
+
: undefined
|
|
876
893
|
const pack: AgentPackManifest = {
|
|
877
894
|
schemaVersion: 1,
|
|
878
895
|
kind: 'swarmclaw-agent-pack',
|
|
879
896
|
name: `${editing.name} Pack`,
|
|
880
897
|
description: editing.description || undefined,
|
|
881
898
|
exportedAt: Date.now(),
|
|
882
|
-
recommendedProviders
|
|
899
|
+
recommendedProviders,
|
|
883
900
|
agents: [{
|
|
884
901
|
id: editing.name.replace(/\s+/g, '-').toLowerCase(),
|
|
885
902
|
name: editing.name,
|
|
@@ -1213,7 +1230,7 @@ export function AgentSheet() {
|
|
|
1213
1230
|
if (!apiEndpoint) setApiEndpoint('http://localhost:18789')
|
|
1214
1231
|
} else {
|
|
1215
1232
|
setOpenclawEnabled(false)
|
|
1216
|
-
const first =
|
|
1233
|
+
const first = agentSelectableProviders[0]?.id || 'claude-cli'
|
|
1217
1234
|
setProvider(first)
|
|
1218
1235
|
setModel('')
|
|
1219
1236
|
setApiEndpoint(null)
|
|
@@ -1445,13 +1462,17 @@ export function AgentSheet() {
|
|
|
1445
1462
|
{!openclawEnabled && <div className="mb-8">
|
|
1446
1463
|
<SectionLabel>Provider</SectionLabel>
|
|
1447
1464
|
<div className="grid grid-cols-3 gap-3">
|
|
1448
|
-
{
|
|
1449
|
-
const
|
|
1465
|
+
{agentSelectableProviders.map((p) => {
|
|
1466
|
+
const nextCredentials = resolveAgentSelectableProviderCredentials(p.id, credentials, providerConfigs)
|
|
1467
|
+
const isConnected = !p.requiresApiKey || nextCredentials.length > 0
|
|
1450
1468
|
return (
|
|
1451
1469
|
<button
|
|
1452
1470
|
key={p.id}
|
|
1453
1471
|
onClick={() => {
|
|
1454
1472
|
setProvider(p.id)
|
|
1473
|
+
if (!nextCredentials.some((item) => item.id === credentialId)) {
|
|
1474
|
+
setCredentialId(nextCredentials[0]?.id || null)
|
|
1475
|
+
}
|
|
1455
1476
|
setGatewayProfileId(null)
|
|
1456
1477
|
}}
|
|
1457
1478
|
className={`relative py-3.5 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
|
|
@@ -2161,7 +2182,7 @@ export function AgentSheet() {
|
|
|
2161
2182
|
</div>
|
|
2162
2183
|
<div className="space-y-3">
|
|
2163
2184
|
{routingTargets.map((target, index) => {
|
|
2164
|
-
const targetCredentials =
|
|
2185
|
+
const targetCredentials = resolveAgentSelectableProviderCredentials(target.provider, credentials, providerConfigs)
|
|
2165
2186
|
return (
|
|
2166
2187
|
<div key={target.id} className="p-4 rounded-[12px] border border-white/[0.08] bg-white/[0.02] space-y-3">
|
|
2167
2188
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
@@ -2182,19 +2203,24 @@ export function AgentSheet() {
|
|
|
2182
2203
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
2183
2204
|
<select
|
|
2184
2205
|
value={target.provider}
|
|
2185
|
-
onChange={(e) =>
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2206
|
+
onChange={(e) => {
|
|
2207
|
+
const nextProviderId = e.target.value
|
|
2208
|
+
const nextCredentials = resolveAgentSelectableProviderCredentials(nextProviderId, credentials, providerConfigs)
|
|
2209
|
+
updateRoutingTarget(target.id, {
|
|
2210
|
+
provider: nextProviderId,
|
|
2211
|
+
credentialId: nextCredentials[0]?.id || null,
|
|
2212
|
+
gatewayProfileId: nextProviderId === 'openclaw' ? target.gatewayProfileId : null,
|
|
2213
|
+
ollamaMode: nextProviderId === 'ollama'
|
|
2189
2214
|
? resolveStoredOllamaMode({
|
|
2190
2215
|
ollamaMode: target.ollamaMode ?? null,
|
|
2191
2216
|
apiEndpoint: target.apiEndpoint ?? null,
|
|
2192
2217
|
})
|
|
2193
2218
|
: null,
|
|
2194
|
-
|
|
2219
|
+
})
|
|
2220
|
+
}}
|
|
2195
2221
|
className={inputClass}
|
|
2196
2222
|
>
|
|
2197
|
-
{
|
|
2223
|
+
{agentSelectableProviders.map((item) => (
|
|
2198
2224
|
<option key={item.id} value={item.id}>{item.name}</option>
|
|
2199
2225
|
))}
|
|
2200
2226
|
</select>
|
|
@@ -26,6 +26,7 @@ import { ModelCombobox } from '@/components/shared/model-combobox'
|
|
|
26
26
|
import { buildOpenClawMainSessionKey } from '@/lib/openclaw/openclaw-agent-id'
|
|
27
27
|
import { StructuredSessionLauncher } from '@/components/protocols/structured-session-launcher'
|
|
28
28
|
import { useWs } from '@/hooks/use-ws'
|
|
29
|
+
import { buildAgentSelectableProviders } from '@/lib/agent-provider-options'
|
|
29
30
|
|
|
30
31
|
interface Props {
|
|
31
32
|
agent: Agent
|
|
@@ -61,17 +62,27 @@ const PROVIDER_LABELS: Record<string, string> = {
|
|
|
61
62
|
function ModelSwitcherInline({ session, agent }: { session: Session; agent: Agent }) {
|
|
62
63
|
const providers = useAppStore((s) => s.providers)
|
|
63
64
|
const loadProviders = useAppStore((s) => s.loadProviders)
|
|
65
|
+
const providerConfigs = useAppStore((s) => s.providerConfigs)
|
|
66
|
+
const loadProviderConfigs = useAppStore((s) => s.loadProviderConfigs)
|
|
64
67
|
const refreshSession = useAppStore((s) => s.refreshSession)
|
|
65
68
|
const streaming = useChatStore((s) => s.streaming)
|
|
66
69
|
const [expanded, setExpanded] = useState(false)
|
|
67
70
|
const [selectedProvider, setSelectedProvider] = useState(agent.provider)
|
|
68
71
|
const [saving, setSaving] = useState(false)
|
|
69
72
|
|
|
70
|
-
useEffect(() => {
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
void loadProviders()
|
|
75
|
+
void loadProviderConfigs()
|
|
76
|
+
}, [loadProviderConfigs, loadProviders])
|
|
71
77
|
useEffect(() => { setSelectedProvider(agent.provider) }, [agent.provider])
|
|
72
78
|
|
|
73
|
-
const
|
|
74
|
-
|
|
79
|
+
const agentSelectableProviders = useMemo(
|
|
80
|
+
() => buildAgentSelectableProviders(providers, providerConfigs),
|
|
81
|
+
[providerConfigs, providers],
|
|
82
|
+
)
|
|
83
|
+
const currentProviderInfo = agentSelectableProviders.find((p) => p.id === selectedProvider)
|
|
84
|
+
const activeAgentProvider = agentSelectableProviders.find((p) => p.id === agent.provider)
|
|
85
|
+
const providerLabel = PROVIDER_LABELS[agent.provider] || activeAgentProvider?.name || agent.provider.replace(/-/g, ' ')
|
|
75
86
|
|
|
76
87
|
const handleModelChange = async (model: string) => {
|
|
77
88
|
if (saving) return
|
|
@@ -122,7 +133,7 @@ function ModelSwitcherInline({ session, agent }: { session: Session; agent: Agen
|
|
|
122
133
|
</button>
|
|
123
134
|
</div>
|
|
124
135
|
<div className="flex flex-wrap gap-1 mb-2">
|
|
125
|
-
{
|
|
136
|
+
{agentSelectableProviders.filter((p) => p.models.length > 0).map((p) => (
|
|
126
137
|
<button
|
|
127
138
|
key={p.id}
|
|
128
139
|
type="button"
|
|
@@ -4,7 +4,7 @@ import { useMemo, useState } from 'react'
|
|
|
4
4
|
import { api } from '@/lib/app/api-client'
|
|
5
5
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
6
|
import { dedup, errorMessage } from '@/lib/shared-utils'
|
|
7
|
-
import type {
|
|
7
|
+
import type { ProviderId, GatewayProfile } from '@/types'
|
|
8
8
|
import {
|
|
9
9
|
SETUP_PROVIDERS,
|
|
10
10
|
SWARMCLAW_ASSISTANT_PROMPT,
|
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
ProviderCheckResponse,
|
|
22
22
|
} from './types'
|
|
23
23
|
import { STEP_ORDER } from './types'
|
|
24
|
-
import { stepIndex } from './utils'
|
|
24
|
+
import { requiresSetupProviderVerification, stepIndex } from './utils'
|
|
25
25
|
import { SparkleIcon } from './shared'
|
|
26
26
|
import { StepProgress } from './step-progress'
|
|
27
27
|
import { StepProviders } from './step-providers'
|
|
@@ -53,7 +53,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
53
53
|
[editingProviderId, configuredProviders],
|
|
54
54
|
)
|
|
55
55
|
const totalSteps = STEP_ORDER.length
|
|
56
|
-
const configuredProviderIds = new Set(configuredProviders.map((cp) => cp.
|
|
56
|
+
const configuredProviderIds = new Set(configuredProviders.map((cp) => cp.setupProvider))
|
|
57
57
|
const canContinueFromProviders = configuredProviders.length > 0
|
|
58
58
|
|
|
59
59
|
const skip = async () => {
|
|
@@ -76,7 +76,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
76
76
|
|
|
77
77
|
const selectProvider = (nextProvider: SetupProvider) => {
|
|
78
78
|
const meta = SETUP_PROVIDERS.find((candidate) => candidate.id === nextProvider)
|
|
79
|
-
const existing = configuredProviders.find((cp) => cp.
|
|
79
|
+
const existing = configuredProviders.find((cp) => cp.setupProvider === nextProvider)
|
|
80
80
|
setActiveProvider(nextProvider)
|
|
81
81
|
setEditingProviderId(existing?.id || null)
|
|
82
82
|
setActiveProviderLabel(existing?.name || meta?.name || '')
|
|
@@ -94,6 +94,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
94
94
|
return {
|
|
95
95
|
...draft,
|
|
96
96
|
providerConfigId: fallback?.id || null,
|
|
97
|
+
setupProvider: fallback?.setupProvider || null,
|
|
97
98
|
provider: fallback?.provider || null,
|
|
98
99
|
credentialId: fallback?.credentialId || null,
|
|
99
100
|
apiEndpoint: fallback?.endpoint || null,
|
|
@@ -125,6 +126,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
125
126
|
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
126
127
|
soul: '',
|
|
127
128
|
providerConfigId: cp.id,
|
|
129
|
+
setupProvider: cp.setupProvider,
|
|
128
130
|
provider: cp.provider,
|
|
129
131
|
model: cp.defaultModel,
|
|
130
132
|
credentialId: cp.credentialId,
|
|
@@ -149,6 +151,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
149
151
|
if (draft.providerConfigId !== editingProviderId) return draft
|
|
150
152
|
return {
|
|
151
153
|
...draft,
|
|
154
|
+
setupProvider: configured.setupProvider,
|
|
152
155
|
provider: configured.provider,
|
|
153
156
|
credentialId: configured.credentialId,
|
|
154
157
|
apiEndpoint: configured.endpoint,
|
|
@@ -186,6 +189,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
186
189
|
systemPrompt: '',
|
|
187
190
|
soul: '',
|
|
188
191
|
providerConfigId: defaultProvider?.id || null,
|
|
192
|
+
setupProvider: defaultProvider?.setupProvider || null,
|
|
189
193
|
provider: defaultProvider?.provider || null,
|
|
190
194
|
model: defaultProvider?.defaultModel || '',
|
|
191
195
|
credentialId: defaultProvider?.credentialId || null,
|
|
@@ -224,13 +228,14 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
224
228
|
|
|
225
229
|
setDraftAgents((current) => current.map((draft) => {
|
|
226
230
|
if (draft.id !== id) return draft
|
|
227
|
-
const previousDefault = draft.
|
|
231
|
+
const previousDefault = draft.setupProvider ? getDefaultModelForProvider(draft.setupProvider) : ''
|
|
228
232
|
const nextModel = !draft.model || draft.model === previousDefault
|
|
229
233
|
? configuredProvider.defaultModel
|
|
230
234
|
: draft.model
|
|
231
235
|
return {
|
|
232
236
|
...draft,
|
|
233
237
|
providerConfigId: configuredProvider.id,
|
|
238
|
+
setupProvider: configuredProvider.setupProvider,
|
|
234
239
|
provider: configuredProvider.provider,
|
|
235
240
|
credentialId: configuredProvider.credentialId,
|
|
236
241
|
apiEndpoint: configuredProvider.endpoint,
|
|
@@ -246,6 +251,10 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
246
251
|
setError('Every enabled agent needs a provider assignment before you continue.')
|
|
247
252
|
return
|
|
248
253
|
}
|
|
254
|
+
if (enabledDrafts.some((draft) => draft.setupProvider === 'custom' && !draft.model.trim())) {
|
|
255
|
+
setError('Every custom-provider agent needs a model before you continue.')
|
|
256
|
+
return
|
|
257
|
+
}
|
|
249
258
|
|
|
250
259
|
setSaving(true)
|
|
251
260
|
setError('')
|
|
@@ -254,12 +263,12 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
254
263
|
const checkedCombos = new Map<string, ProviderCheckResponse>()
|
|
255
264
|
for (const draft of enabledDrafts) {
|
|
256
265
|
const cp = configuredProviders.find((c) => c.id === draft.providerConfigId)
|
|
257
|
-
if (!cp || cp.
|
|
258
|
-
const comboKey = `${cp.
|
|
266
|
+
if (!cp || !requiresSetupProviderVerification(cp.setupProvider)) continue
|
|
267
|
+
const comboKey = `${cp.setupProvider}|${draft.apiEndpoint || cp.endpoint || ''}|${draft.model}`
|
|
259
268
|
if (checkedCombos.has(comboKey)) continue
|
|
260
269
|
try {
|
|
261
270
|
const result = await api<ProviderCheckResponse>('POST', '/setup/check-provider', {
|
|
262
|
-
provider: cp.
|
|
271
|
+
provider: cp.setupProvider,
|
|
263
272
|
credentialId: cp.credentialId || undefined,
|
|
264
273
|
endpoint: draft.apiEndpoint || cp.endpoint || undefined,
|
|
265
274
|
model: draft.model || undefined,
|
|
@@ -276,7 +285,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
276
285
|
}
|
|
277
286
|
|
|
278
287
|
const gatewayProfileIdsByProviderConfig = new Map<string, string>()
|
|
279
|
-
const openClawProviders = configuredProviders.filter((candidate) => candidate.
|
|
288
|
+
const openClawProviders = configuredProviders.filter((candidate) => candidate.setupProvider === 'openclaw')
|
|
280
289
|
if (openClawProviders.length > 0) {
|
|
281
290
|
const existingGateways = await api<GatewayProfile[]>('GET', '/gateways')
|
|
282
291
|
let shouldCreateDefault = existingGateways.length === 0
|
|
@@ -322,8 +331,8 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
322
331
|
description: draft.description.trim(),
|
|
323
332
|
systemPrompt: draft.systemPrompt.trim(),
|
|
324
333
|
soul: draft.soul.trim() || undefined,
|
|
325
|
-
provider: draft.provider
|
|
326
|
-
model: draft.model.trim() || getDefaultModelForProvider(draft.
|
|
334
|
+
provider: draft.provider,
|
|
335
|
+
model: draft.model.trim() || (draft.setupProvider ? getDefaultModelForProvider(draft.setupProvider) : ''),
|
|
327
336
|
credentialId: draft.credentialId || null,
|
|
328
337
|
tools: draft.tools,
|
|
329
338
|
capabilities: draft.capabilities,
|
|
@@ -355,7 +364,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
355
364
|
}
|
|
356
365
|
|
|
357
366
|
// Push soul and identity files to the OpenClaw gateway (non-fatal)
|
|
358
|
-
if (draft.
|
|
367
|
+
if (draft.setupProvider === 'openclaw') {
|
|
359
368
|
try {
|
|
360
369
|
if (draft.soul.trim()) {
|
|
361
370
|
await api('PUT', '/openclaw/agent-files', { agentId, filename: 'SOUL.md', content: draft.soul.trim() })
|
|
@@ -373,8 +382,8 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
373
382
|
created.push({
|
|
374
383
|
id: agentId,
|
|
375
384
|
name: draft.name.trim(),
|
|
376
|
-
provider: draft.provider as
|
|
377
|
-
providerName: configuredProviders.find((candidate) => candidate.id === draft.providerConfigId)?.name || draft.provider
|
|
385
|
+
provider: draft.provider as ProviderId,
|
|
386
|
+
providerName: configuredProviders.find((candidate) => candidate.id === draft.providerConfigId)?.name || String(draft.provider || ''),
|
|
378
387
|
})
|
|
379
388
|
}
|
|
380
389
|
|
|
@@ -69,10 +69,10 @@ export function ConfiguredProviderChips({
|
|
|
69
69
|
{formatEndpointHost(cp.endpoint)
|
|
70
70
|
? `· ${formatEndpointHost(cp.endpoint)}`
|
|
71
71
|
: ''}
|
|
72
|
-
{cp.
|
|
72
|
+
{cp.setupProvider === 'openclaw' && cp.deployment?.useCase
|
|
73
73
|
? ` · ${OPENCLAW_USE_CASE_LABELS[cp.deployment.useCase]}`
|
|
74
74
|
: ''}
|
|
75
|
-
{cp.
|
|
75
|
+
{cp.setupProvider === 'openclaw' && cp.deployment?.exposure
|
|
76
76
|
? ` · ${OPENCLAW_EXPOSURE_LABELS[cp.deployment.exposure]}`
|
|
77
77
|
: ''}
|
|
78
78
|
{cp.defaultModel ? ` · ${cp.defaultModel}` : ''}
|