@swarmclawai/swarmclaw 1.4.6 → 1.4.7
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 +17 -1
- package/package.json +1 -1
- package/public/provider-logos/hermes-agent.png +0 -0
- package/public/provider-logos/openrouter.png +0 -0
- package/src/app/api/setup/check-provider/route.ts +18 -2
- package/src/components/agents/agent-sheet.tsx +10 -3
- package/src/components/auth/setup-wizard/step-connect.tsx +6 -0
- package/src/components/auth/setup-wizard/utils.test.ts +2 -0
- package/src/lib/app/view-constants.ts +1 -1
- package/src/lib/orchestrator-config.test.ts +1 -0
- package/src/lib/orchestrator-config.ts +1 -0
- package/src/lib/provider-sets.ts +6 -3
- package/src/lib/providers/index.ts +35 -0
- package/src/lib/providers/openai.ts +5 -4
- package/src/lib/server/agents/agent-availability.ts +2 -2
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +3 -1
- package/src/lib/server/provider-health.test.ts +9 -2
- package/src/lib/server/provider-health.ts +8 -3
- package/src/lib/server/provider-model-discovery.test.ts +20 -0
- package/src/lib/server/storage.ts +1 -1
- package/src/lib/setup-defaults.test.ts +10 -0
- package/src/lib/setup-defaults.ts +42 -1
- package/src/types/provider.ts +1 -1
package/README.md
CHANGED
|
@@ -211,6 +211,20 @@ SwarmClaw agents can join [SwarmFeed](https://swarmfeed.ai) — a social network
|
|
|
211
211
|
|
|
212
212
|
Read the docs at [swarmclaw.ai/docs/swarmfeed](https://swarmclaw.ai/docs/swarmfeed) and visit [swarmfeed.ai](https://swarmfeed.ai) for the platform itself.
|
|
213
213
|
|
|
214
|
+
### v1.4.7 Highlights
|
|
215
|
+
|
|
216
|
+
- **Hermes Agent built-in provider**: Added first-class Hermes support through the Hermes API server, including optional auth, local or remote `/v1` endpoints, and runtime-managed agent handling.
|
|
217
|
+
- **OpenRouter built-in provider**: OpenRouter is now a built-in provider instead of living only behind the generic custom-provider path.
|
|
218
|
+
- **Runtime-managed provider handling**: Hermes now skips SwarmClaw's local extension/tool injection path so its own runtime stays in control, while setup and model discovery still work through the normal provider flow.
|
|
219
|
+
- **Provider docs refresh**: README and docs now reflect the new provider list, remote Hermes API-server support, and logo assets for OpenRouter and Hermes Agent.
|
|
220
|
+
|
|
221
|
+
### v1.4.6 Highlights
|
|
222
|
+
|
|
223
|
+
- **SwarmDock startup sync**: Existing SwarmDock agents now authenticate and reconcile their live marketplace profile on connector start, updating stale description, skills, framework/model metadata, and payout wallet fields
|
|
224
|
+
- **Agent wallet fallback**: SwarmDock connectors now fall back to the agent's selected marketplace wallet when no connector-level wallet address is configured
|
|
225
|
+
- **Task filter fix**: The built-in `swarmdock` tool now uses the correct `skills=` task filter when browsing marketplace tasks from chat
|
|
226
|
+
- **SwarmDock SDK bump**: Updated `@swarmdock/sdk` from `0.5.2` to `0.5.3`, aligning the connector with the published metadata-sync fixes
|
|
227
|
+
|
|
214
228
|
### v1.4.5 Highlights
|
|
215
229
|
|
|
216
230
|
- **OpenClaw 2026.4.x compatibility**: Fixed WebSocket protocol errors when connecting to OpenClaw 2026.4.2+ gateways (`profileId` was incorrectly included in RPC params)
|
|
@@ -320,7 +334,9 @@ Then open `http://localhost:3456`.
|
|
|
320
334
|
|
|
321
335
|
## Core Capabilities
|
|
322
336
|
|
|
323
|
-
- **Providers**: OpenClaw, OpenAI, Anthropic, Ollama, Google, DeepSeek, Groq, Together, Mistral, xAI, Fireworks, Nebius, DeepInfra, plus compatible custom endpoints.
|
|
337
|
+
- **Providers**: OpenClaw, OpenAI, OpenRouter, Anthropic, Ollama, Hermes Agent, Google, DeepSeek, Groq, Together, Mistral, xAI, Fireworks, Nebius, DeepInfra, plus compatible custom endpoints.
|
|
338
|
+
- **OpenRouter**: <img src="public/provider-logos/openrouter.png" alt="OpenRouter logo" width="20" height="20" /> Use OpenRouter as a first-class built-in provider with its standard OpenAI-compatible endpoint and routed model IDs such as `openai/gpt-4.1-mini`.
|
|
339
|
+
- **Hermes Agent**: <img src="public/provider-logos/hermes-agent.png" alt="Hermes Agent logo" width="20" height="20" /> Connect Hermes through its OpenAI-compatible API server, locally or through a reachable remote `/v1` endpoint.
|
|
324
340
|
- **Delegation**: built-in delegation to Claude Code, Codex CLI, OpenCode CLI, Gemini CLI, and native SwarmClaw subagents.
|
|
325
341
|
- **Autonomy**: heartbeat loops, schedules, background jobs, task execution, supervisor recovery, and agent wakeups.
|
|
326
342
|
- **Orchestration**: durable structured execution with branching, repeat loops, parallel branches, explicit joins, restart-safe run state, and contextual launch from chats, chatrooms, tasks, schedules, and API flows.
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -7,6 +7,7 @@ import { normalizeOllamaSetupEndpoint, normalizeOpenClawUrl, parseErrorMessage }
|
|
|
7
7
|
|
|
8
8
|
type SetupProvider =
|
|
9
9
|
| 'openai'
|
|
10
|
+
| 'openrouter'
|
|
10
11
|
| 'anthropic'
|
|
11
12
|
| 'google'
|
|
12
13
|
| 'deepseek'
|
|
@@ -19,6 +20,7 @@ type SetupProvider =
|
|
|
19
20
|
| 'deepinfra'
|
|
20
21
|
| 'ollama'
|
|
21
22
|
| 'openclaw'
|
|
23
|
+
| 'hermes'
|
|
22
24
|
|
|
23
25
|
interface SetupCheckBody {
|
|
24
26
|
provider?: string
|
|
@@ -46,13 +48,14 @@ async function checkOpenAiCompatible(
|
|
|
46
48
|
modelHint?: string,
|
|
47
49
|
): Promise<{ ok: boolean; message: string; normalizedEndpoint: string }> {
|
|
48
50
|
const normalizedEndpoint = (endpointRaw || defaultEndpoint).replace(/\/+$/, '')
|
|
51
|
+
const authHeaders = apiKey ? { authorization: `Bearer ${apiKey}` } : undefined
|
|
49
52
|
|
|
50
53
|
// First, discover a model to test with (prefer the hint, fall back to the first available model)
|
|
51
54
|
let testModel = modelHint || ''
|
|
52
55
|
if (!testModel) {
|
|
53
56
|
try {
|
|
54
57
|
const modelsRes = await fetch(`${normalizedEndpoint}/models`, {
|
|
55
|
-
headers:
|
|
58
|
+
headers: authHeaders,
|
|
56
59
|
signal: AbortSignal.timeout(8_000),
|
|
57
60
|
cache: 'no-store',
|
|
58
61
|
})
|
|
@@ -79,6 +82,8 @@ async function checkOpenAiCompatible(
|
|
|
79
82
|
'Fireworks AI': 'accounts/fireworks/models/llama4-scout-instruct-basic',
|
|
80
83
|
Nebius: 'deepseek-ai/DeepSeek-R1-0528',
|
|
81
84
|
DeepInfra: 'deepseek-ai/DeepSeek-R1-0528',
|
|
85
|
+
OpenRouter: 'openai/gpt-4.1-mini',
|
|
86
|
+
'Hermes Agent': 'hermes-agent',
|
|
82
87
|
}
|
|
83
88
|
testModel = fallbacks[providerName] || 'gpt-4o-mini'
|
|
84
89
|
}
|
|
@@ -87,8 +92,8 @@ async function checkOpenAiCompatible(
|
|
|
87
92
|
const res = await fetch(`${normalizedEndpoint}/chat/completions`, {
|
|
88
93
|
method: 'POST',
|
|
89
94
|
headers: {
|
|
90
|
-
authorization: `Bearer ${apiKey}`,
|
|
91
95
|
'content-type': 'application/json',
|
|
96
|
+
...(authHeaders || {}),
|
|
92
97
|
},
|
|
93
98
|
body: JSON.stringify({
|
|
94
99
|
model: testModel,
|
|
@@ -294,6 +299,12 @@ export async function POST(req: Request) {
|
|
|
294
299
|
const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint, model)
|
|
295
300
|
return NextResponse.json(result)
|
|
296
301
|
}
|
|
302
|
+
case 'openrouter': {
|
|
303
|
+
if (!apiKey) return NextResponse.json({ ok: false, message: 'OpenRouter API key is required.' })
|
|
304
|
+
const info = OPENAI_COMPATIBLE_DEFAULTS.openrouter
|
|
305
|
+
const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint, model)
|
|
306
|
+
return NextResponse.json(result)
|
|
307
|
+
}
|
|
297
308
|
case 'anthropic': {
|
|
298
309
|
if (!apiKey) return NextResponse.json({ ok: false, message: 'Anthropic API key is required.' })
|
|
299
310
|
const result = await checkAnthropic(apiKey, model)
|
|
@@ -313,6 +324,11 @@ export async function POST(req: Request) {
|
|
|
313
324
|
const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint, model)
|
|
314
325
|
return NextResponse.json(result)
|
|
315
326
|
}
|
|
327
|
+
case 'hermes': {
|
|
328
|
+
const info = OPENAI_COMPATIBLE_DEFAULTS.hermes
|
|
329
|
+
const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint, model)
|
|
330
|
+
return NextResponse.json(result)
|
|
331
|
+
}
|
|
316
332
|
case 'ollama': {
|
|
317
333
|
const result = await checkOllama({
|
|
318
334
|
endpointRaw: endpoint,
|
|
@@ -35,6 +35,7 @@ const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
|
|
|
35
35
|
const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
|
|
36
36
|
const AUTO_SYNC_MODEL_PROVIDER_IDS = new Set<ProviderType>([
|
|
37
37
|
'openai',
|
|
38
|
+
'openrouter',
|
|
38
39
|
'anthropic',
|
|
39
40
|
'google',
|
|
40
41
|
'deepseek',
|
|
@@ -45,6 +46,7 @@ const AUTO_SYNC_MODEL_PROVIDER_IDS = new Set<ProviderType>([
|
|
|
45
46
|
'fireworks',
|
|
46
47
|
'nebius',
|
|
47
48
|
'deepinfra',
|
|
49
|
+
'hermes',
|
|
48
50
|
'ollama',
|
|
49
51
|
])
|
|
50
52
|
const CONNECTION_TEST_TIMEOUT_MS = 40_000
|
|
@@ -1645,13 +1647,16 @@ export function AgentSheet() {
|
|
|
1645
1647
|
</div>
|
|
1646
1648
|
)}
|
|
1647
1649
|
|
|
1648
|
-
{currentProvider?.requiresEndpoint && (provider
|
|
1650
|
+
{currentProvider?.requiresEndpoint && (provider !== 'ollama' || ollamaMode === 'local') && (
|
|
1649
1651
|
<div className="mb-8">
|
|
1650
|
-
<SectionLabel>{provider === 'openclaw' ? 'OpenClaw Endpoint' : 'Endpoint'}</SectionLabel>
|
|
1652
|
+
<SectionLabel>{provider === 'openclaw' ? 'OpenClaw Endpoint' : provider === 'hermes' ? 'Hermes API Endpoint' : 'Endpoint'}</SectionLabel>
|
|
1651
1653
|
<input type="text" value={apiEndpoint || ''} onChange={(e) => setApiEndpoint(e.target.value || null)} placeholder={currentProvider.defaultEndpoint || 'http://localhost:11434'} className={`${inputClass} font-mono text-[14px]`} />
|
|
1652
1654
|
{provider === 'openclaw' && (
|
|
1653
1655
|
<p className="text-[13px] text-text-3/70 mt-2">The URL of your OpenClaw gateway</p>
|
|
1654
1656
|
)}
|
|
1657
|
+
{provider === 'hermes' && (
|
|
1658
|
+
<p className="text-[13px] text-text-3/70 mt-2">Point this at the Hermes API server, usually <code className="text-text-2">http://127.0.0.1:8642/v1</code>.</p>
|
|
1659
|
+
)}
|
|
1655
1660
|
</div>
|
|
1656
1661
|
)}
|
|
1657
1662
|
|
|
@@ -2502,7 +2507,9 @@ export function AgentSheet() {
|
|
|
2502
2507
|
? 'Claude CLI uses its own built-in capabilities — no additional local tool/platform configuration is needed.'
|
|
2503
2508
|
: provider === 'codex-cli'
|
|
2504
2509
|
? 'OpenAI Codex CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'
|
|
2505
|
-
:
|
|
2510
|
+
: provider === 'hermes'
|
|
2511
|
+
? 'Hermes Agent runs behind its own API server and tool runtime. SwarmClaw sends prompts to Hermes directly instead of injecting local platform tools.'
|
|
2512
|
+
: 'OpenCode CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'}
|
|
2506
2513
|
</p>
|
|
2507
2514
|
</div>
|
|
2508
2515
|
)}
|
|
@@ -329,6 +329,12 @@ export function StepConnect({
|
|
|
329
329
|
<p className="text-[12px] text-text-3">Remote example: <code className="text-text-2">https://your-gateway.ts.net/v1</code>.</p>
|
|
330
330
|
</div>
|
|
331
331
|
)}
|
|
332
|
+
{provider === 'hermes' && (
|
|
333
|
+
<div className="mt-2 space-y-0.5">
|
|
334
|
+
<p className="text-[12px] text-text-3">Hermes Agent's API server defaults to <code className="text-text-2">http://127.0.0.1:8642/v1</code>.</p>
|
|
335
|
+
<p className="text-[12px] text-text-3">Use any reachable local or remote API-server endpoint exposed by Hermes.</p>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
332
338
|
</div>
|
|
333
339
|
)}
|
|
334
340
|
|
|
@@ -265,4 +265,6 @@ test('requiresSetupProviderVerification skips custom providers', () => {
|
|
|
265
265
|
assert.equal(requiresSetupProviderVerification('custom'), false)
|
|
266
266
|
assert.equal(requiresSetupProviderVerification('openclaw'), false)
|
|
267
267
|
assert.equal(requiresSetupProviderVerification('openai'), true)
|
|
268
|
+
assert.equal(requiresSetupProviderVerification('openrouter'), true)
|
|
269
|
+
assert.equal(requiresSetupProviderVerification('hermes'), true)
|
|
268
270
|
})
|
|
@@ -131,7 +131,7 @@ export const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents' | 'home'>, { ic
|
|
|
131
131
|
icon: 'zap',
|
|
132
132
|
title: 'Providers',
|
|
133
133
|
description: 'Manage LLM providers including built-in and custom OpenAI-compatible endpoints.',
|
|
134
|
-
features: ['Built-in support for
|
|
134
|
+
features: ['Built-in support for OpenAI, OpenRouter, Anthropic, Ollama, Hermes Agent, and CLI runtimes', 'Add custom OpenAI-compatible providers for local or internal gateways', 'Configure base URLs, models, and API keys per provider', 'Built-in and custom providers work seamlessly with all features'],
|
|
135
135
|
},
|
|
136
136
|
skills: {
|
|
137
137
|
icon: 'book',
|
|
@@ -6,6 +6,7 @@ describe('orchestrator-config', () => {
|
|
|
6
6
|
it('marks CLI and OpenClaw providers as ineligible', () => {
|
|
7
7
|
assert.equal(isOrchestratorProviderEligible('openai'), true)
|
|
8
8
|
assert.equal(isOrchestratorProviderEligible('openclaw'), false)
|
|
9
|
+
assert.equal(isOrchestratorProviderEligible('hermes'), false)
|
|
9
10
|
assert.equal(isOrchestratorProviderEligible('codex-cli'), false)
|
|
10
11
|
})
|
|
11
12
|
|
package/src/lib/provider-sets.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/** CLI providers that use their own tool execution outside the shared tool-runtime path. */
|
|
2
2
|
export const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli'])
|
|
3
3
|
|
|
4
|
-
/** Providers
|
|
5
|
-
export const
|
|
4
|
+
/** Providers that manage their own runtime/tool loop even when reached over an API endpoint. */
|
|
5
|
+
export const RUNTIME_MANAGED_PROVIDER_IDS = new Set(['hermes'])
|
|
6
|
+
|
|
7
|
+
/** Providers with native tool/capability support (CLI providers + OpenClaw + Hermes). */
|
|
8
|
+
export const NATIVE_CAPABILITY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'openclaw', 'hermes'])
|
|
6
9
|
|
|
7
10
|
/** Providers that can only act as workers — no coordinator role, no heartbeat, no advanced settings. */
|
|
8
|
-
export const WORKER_ONLY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'openclaw'])
|
|
11
|
+
export const WORKER_ONLY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'openclaw', 'hermes'])
|
|
@@ -71,6 +71,23 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
71
71
|
requiresEndpoint: false,
|
|
72
72
|
handler: { streamChat: streamOpenAiChat },
|
|
73
73
|
},
|
|
74
|
+
openrouter: {
|
|
75
|
+
id: 'openrouter',
|
|
76
|
+
name: 'OpenRouter',
|
|
77
|
+
models: ['openai/gpt-4.1-mini'],
|
|
78
|
+
requiresApiKey: true,
|
|
79
|
+
requiresEndpoint: false,
|
|
80
|
+
defaultEndpoint: 'https://openrouter.ai/api/v1',
|
|
81
|
+
handler: {
|
|
82
|
+
streamChat: (opts) => {
|
|
83
|
+
const patchedSession = {
|
|
84
|
+
...opts.session,
|
|
85
|
+
apiEndpoint: opts.session.apiEndpoint || 'https://openrouter.ai/api/v1',
|
|
86
|
+
}
|
|
87
|
+
return streamOpenAiChat({ ...opts, session: patchedSession })
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
74
91
|
anthropic: {
|
|
75
92
|
id: 'anthropic',
|
|
76
93
|
name: 'Anthropic',
|
|
@@ -89,6 +106,24 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
89
106
|
defaultEndpoint: 'http://localhost:18789',
|
|
90
107
|
handler: { streamChat: streamOpenClawChat },
|
|
91
108
|
},
|
|
109
|
+
hermes: {
|
|
110
|
+
id: 'hermes',
|
|
111
|
+
name: 'Hermes Agent',
|
|
112
|
+
models: ['hermes-agent'],
|
|
113
|
+
requiresApiKey: false,
|
|
114
|
+
optionalApiKey: true,
|
|
115
|
+
requiresEndpoint: true,
|
|
116
|
+
defaultEndpoint: 'http://127.0.0.1:8642/v1',
|
|
117
|
+
handler: {
|
|
118
|
+
streamChat: (opts) => {
|
|
119
|
+
const patchedSession = {
|
|
120
|
+
...opts.session,
|
|
121
|
+
apiEndpoint: opts.session.apiEndpoint || 'http://127.0.0.1:8642/v1',
|
|
122
|
+
}
|
|
123
|
+
return streamOpenAiChat({ ...opts, session: patchedSession })
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
92
127
|
'opencode-cli': {
|
|
93
128
|
id: 'opencode-cli',
|
|
94
129
|
name: 'OpenCode CLI',
|
|
@@ -73,6 +73,10 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
73
73
|
// Try with stream_options first; if the provider rejects with 400, retry without it
|
|
74
74
|
let res: Response | undefined
|
|
75
75
|
let usageEnabled = true
|
|
76
|
+
const headers: Record<string, string> = {
|
|
77
|
+
'Content-Type': contentType,
|
|
78
|
+
}
|
|
79
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`
|
|
76
80
|
for (const includeStreamOptions of [true, false]) {
|
|
77
81
|
const payloadObj: Record<string, unknown> = {
|
|
78
82
|
model,
|
|
@@ -86,10 +90,7 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
86
90
|
|
|
87
91
|
res = await fetch(url, {
|
|
88
92
|
method: 'POST',
|
|
89
|
-
headers
|
|
90
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
91
|
-
'Content-Type': contentType,
|
|
92
|
-
},
|
|
93
|
+
headers,
|
|
93
94
|
body: payload,
|
|
94
95
|
signal: abortController.signal,
|
|
95
96
|
})
|
|
@@ -27,6 +27,6 @@ export function buildWorkerOnlyAgentMessage(
|
|
|
27
27
|
const name = typeof agent?.name === 'string' && agent.name.trim()
|
|
28
28
|
? agent.name.trim()
|
|
29
29
|
: 'This agent'
|
|
30
|
-
if (action) return `${name}
|
|
31
|
-
return `${name}
|
|
30
|
+
if (action) return `${name} uses a runtime-managed provider and cannot ${action}. Runtime-managed agents can only be used for direct chats and delegation.`
|
|
31
|
+
return `${name} uses a runtime-managed provider and cannot join chatrooms. Runtime-managed agents can only be used for direct chats and delegation.`
|
|
32
32
|
}
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
splitCapabilityIds,
|
|
45
45
|
} from '@/lib/capability-selection'
|
|
46
46
|
import { normalizeProviderEndpoint, isLocalOpenClawEndpoint } from '@/lib/openclaw/openclaw-endpoint'
|
|
47
|
-
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
47
|
+
import { NON_LANGGRAPH_PROVIDER_IDS, RUNTIME_MANAGED_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
48
48
|
import {
|
|
49
49
|
bridgeHumanReplyFromChat,
|
|
50
50
|
} from '@/lib/server/chatrooms/session-mailbox'
|
|
@@ -815,9 +815,11 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
|
|
|
815
815
|
const hideAssistantTranscript = internal && source === 'main-loop-followup'
|
|
816
816
|
|
|
817
817
|
const useLocalOpenClawNativeRuntime = providerType === 'openclaw' && isLocalOpenClawEndpoint(sessionForRun.apiEndpoint)
|
|
818
|
+
const useProviderManagedRuntime = RUNTIME_MANAGED_PROVIDER_IDS.has(providerType)
|
|
818
819
|
const enabledSessionExtensions = getEnabledCapabilityIds(sessionForRun)
|
|
819
820
|
const hasExtensions = enabledSessionExtensions.length > 0
|
|
820
821
|
&& !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
|
|
822
|
+
&& !useProviderManagedRuntime
|
|
821
823
|
&& !useLocalOpenClawNativeRuntime
|
|
822
824
|
|
|
823
825
|
const systemPrompt = heartbeatLightContext
|
|
@@ -120,6 +120,7 @@ describe('provider-health', () => {
|
|
|
120
120
|
it('OPENAI_COMPATIBLE_DEFAULTS has expected providers', () => {
|
|
121
121
|
const defaults = providerHealth.OPENAI_COMPATIBLE_DEFAULTS
|
|
122
122
|
assert.ok(defaults.openai)
|
|
123
|
+
assert.ok(defaults.openrouter)
|
|
123
124
|
assert.ok(defaults.google)
|
|
124
125
|
assert.ok(defaults.deepseek)
|
|
125
126
|
assert.ok(defaults.groq)
|
|
@@ -127,11 +128,17 @@ describe('provider-health', () => {
|
|
|
127
128
|
assert.ok(defaults.mistral)
|
|
128
129
|
assert.ok(defaults.xai)
|
|
129
130
|
assert.ok(defaults.fireworks)
|
|
131
|
+
assert.ok(defaults.hermes)
|
|
130
132
|
|
|
131
133
|
// Each entry has name and defaultEndpoint
|
|
132
|
-
for (const [, val] of Object.entries(defaults)) {
|
|
134
|
+
for (const [key, val] of Object.entries(defaults)) {
|
|
133
135
|
assert.ok(typeof val.name === 'string' && val.name.length > 0)
|
|
134
|
-
assert.ok(typeof val.defaultEndpoint === 'string' && val.defaultEndpoint.
|
|
136
|
+
assert.ok(typeof val.defaultEndpoint === 'string' && val.defaultEndpoint.length > 0)
|
|
137
|
+
if (key === 'hermes') {
|
|
138
|
+
assert.ok(val.defaultEndpoint.startsWith('http://'))
|
|
139
|
+
} else {
|
|
140
|
+
assert.ok(val.defaultEndpoint.startsWith('https://'))
|
|
141
|
+
}
|
|
135
142
|
}
|
|
136
143
|
})
|
|
137
144
|
|
|
@@ -246,6 +246,7 @@ async function parseErrorMessage(res: Response, fallback: string): Promise<strin
|
|
|
246
246
|
|
|
247
247
|
export const OPENAI_COMPATIBLE_DEFAULTS: Record<string, { name: string; defaultEndpoint: string }> = {
|
|
248
248
|
openai: { name: 'OpenAI', defaultEndpoint: 'https://api.openai.com/v1' },
|
|
249
|
+
openrouter: { name: 'OpenRouter', defaultEndpoint: 'https://openrouter.ai/api/v1' },
|
|
249
250
|
google: { name: 'Google Gemini', defaultEndpoint: 'https://generativelanguage.googleapis.com/v1beta/openai' },
|
|
250
251
|
deepseek: { name: 'DeepSeek', defaultEndpoint: 'https://api.deepseek.com/v1' },
|
|
251
252
|
groq: { name: 'Groq', defaultEndpoint: 'https://api.groq.com/openai/v1' },
|
|
@@ -255,15 +256,18 @@ export const OPENAI_COMPATIBLE_DEFAULTS: Record<string, { name: string; defaultE
|
|
|
255
256
|
fireworks: { name: 'Fireworks AI', defaultEndpoint: 'https://api.fireworks.ai/inference/v1' },
|
|
256
257
|
nebius: { name: 'Nebius', defaultEndpoint: 'https://api.tokenfactory.nebius.com/v1' },
|
|
257
258
|
deepinfra: { name: 'DeepInfra', defaultEndpoint: 'https://api.deepinfra.com/v1/openai' },
|
|
259
|
+
hermes: { name: 'Hermes Agent', defaultEndpoint: 'http://127.0.0.1:8642/v1' },
|
|
258
260
|
}
|
|
259
261
|
|
|
260
262
|
export async function pingOpenAiCompatible(
|
|
261
|
-
apiKey: string,
|
|
263
|
+
apiKey: string | undefined,
|
|
262
264
|
endpoint: string,
|
|
263
265
|
): Promise<{ ok: boolean; message: string }> {
|
|
264
266
|
const normalizedEndpoint = endpoint.replace(/\/+$/, '')
|
|
267
|
+
const headers: Record<string, string> = {}
|
|
268
|
+
if (apiKey) headers.authorization = `Bearer ${apiKey}`
|
|
265
269
|
const res = await fetch(`${normalizedEndpoint}/models`, {
|
|
266
|
-
headers:
|
|
270
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
267
271
|
signal: AbortSignal.timeout(PING_TIMEOUT_MS),
|
|
268
272
|
cache: 'no-store',
|
|
269
273
|
})
|
|
@@ -335,6 +339,7 @@ export async function pingProvider(
|
|
|
335
339
|
endpoint: string | undefined,
|
|
336
340
|
): Promise<{ ok: boolean; message: string }> {
|
|
337
341
|
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli']
|
|
342
|
+
const OPTIONAL_OPENAI_COMPATIBLE_KEY_PROVIDERS = new Set(['hermes'])
|
|
338
343
|
if (CLI_PROVIDERS.includes(provider)) return { ok: true, message: 'CLI provider — skipped.' }
|
|
339
344
|
|
|
340
345
|
try {
|
|
@@ -352,7 +357,7 @@ export async function pingProvider(
|
|
|
352
357
|
const defaults = OPENAI_COMPATIBLE_DEFAULTS[provider]
|
|
353
358
|
const resolvedEndpoint = endpoint || defaults?.defaultEndpoint
|
|
354
359
|
if (!resolvedEndpoint) return { ok: false, message: `No endpoint for provider "${provider}".` }
|
|
355
|
-
if (!apiKey) return { ok: false, message: 'No API key configured.' }
|
|
360
|
+
if (!apiKey && !OPTIONAL_OPENAI_COMPATIBLE_KEY_PROVIDERS.has(provider)) return { ok: false, message: 'No API key configured.' }
|
|
356
361
|
return await pingOpenAiCompatible(apiKey, resolvedEndpoint)
|
|
357
362
|
} catch (err: unknown) {
|
|
358
363
|
const msg = err instanceof Error && err.name === 'TimeoutError'
|
|
@@ -208,6 +208,26 @@ test('resolveDescriptor uses explicit cloud Ollama discovery only when cloud mod
|
|
|
208
208
|
assert.equal(descriptor?.requiresApiKey, true)
|
|
209
209
|
})
|
|
210
210
|
|
|
211
|
+
test('resolveDescriptor uses OpenRouter as an OpenAI-compatible provider', () => {
|
|
212
|
+
const descriptor = resolveDescriptor({
|
|
213
|
+
providerId: 'openrouter',
|
|
214
|
+
})
|
|
215
|
+
assert.equal(descriptor?.strategy, 'openai-compatible')
|
|
216
|
+
assert.equal(descriptor?.endpoint, 'https://openrouter.ai/api/v1')
|
|
217
|
+
assert.equal(descriptor?.requiresApiKey, true)
|
|
218
|
+
assert.equal(descriptor?.optionalApiKey, false)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('resolveDescriptor uses Hermes as an OpenAI-compatible provider with optional auth', () => {
|
|
222
|
+
const descriptor = resolveDescriptor({
|
|
223
|
+
providerId: 'hermes',
|
|
224
|
+
})
|
|
225
|
+
assert.equal(descriptor?.strategy, 'openai-compatible')
|
|
226
|
+
assert.equal(descriptor?.endpoint, 'http://127.0.0.1:8642/v1')
|
|
227
|
+
assert.equal(descriptor?.requiresApiKey, false)
|
|
228
|
+
assert.equal(descriptor?.optionalApiKey, true)
|
|
229
|
+
})
|
|
230
|
+
|
|
211
231
|
// ---------------------------------------------------------------------------
|
|
212
232
|
// ttlForDescriptor
|
|
213
233
|
// ---------------------------------------------------------------------------
|
|
@@ -669,7 +669,7 @@ if (!IS_BUILD_BOOTSTRAP) {
|
|
|
669
669
|
## Platform
|
|
670
670
|
|
|
671
671
|
- **Agents** — Create specialized AI agents (Agents tab → "+") with a provider, model, system prompt, and tools. "Generate with AI" scaffolds agents from a description. Enable cross-agent delegation when an agent should assign work to others.
|
|
672
|
-
- **Providers** — Configure LLM backends in Settings → Providers: Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama, OpenClaw, or custom OpenAI-compatible endpoints.
|
|
672
|
+
- **Providers** — Configure LLM backends in Settings → Providers: Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Gemini CLI, GitHub Copilot CLI, Anthropic, OpenAI, OpenRouter, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Nebius, DeepInfra, Ollama, OpenClaw, Hermes Agent, or custom OpenAI-compatible endpoints.
|
|
673
673
|
- **Tasks** — The Task Board tracks work items. Assign agents and they'll execute autonomously.
|
|
674
674
|
- **Schedules** — Cron-based recurring jobs that run agents or tasks automatically.
|
|
675
675
|
- **Skills** — Reusable markdown instruction files agents can discover and use by default; pin them to keep favorite workflows always-on.
|
|
@@ -27,11 +27,21 @@ test('getDefaultModelForProvider returns non-empty for openai', () => {
|
|
|
27
27
|
assert.ok(model, 'openai model should be truthy')
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
+
test('getDefaultModelForProvider returns non-empty for openrouter', () => {
|
|
31
|
+
const model = getDefaultModelForProvider('openrouter')
|
|
32
|
+
assert.ok(model, 'openrouter model should be truthy')
|
|
33
|
+
})
|
|
34
|
+
|
|
30
35
|
test('getDefaultModelForProvider returns non-empty for anthropic', () => {
|
|
31
36
|
const model = getDefaultModelForProvider('anthropic')
|
|
32
37
|
assert.ok(model, 'anthropic model should be truthy')
|
|
33
38
|
})
|
|
34
39
|
|
|
40
|
+
test('getDefaultModelForProvider returns non-empty for hermes', () => {
|
|
41
|
+
const model = getDefaultModelForProvider('hermes')
|
|
42
|
+
assert.ok(model, 'hermes model should be truthy')
|
|
43
|
+
})
|
|
44
|
+
|
|
35
45
|
test('getDefaultModelForProvider returns non-empty for ollama', () => {
|
|
36
46
|
const model = getDefaultModelForProvider('ollama')
|
|
37
47
|
assert.ok(model, 'ollama model should be truthy')
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export type SetupProvider =
|
|
7
7
|
| 'anthropic'
|
|
8
8
|
| 'openai'
|
|
9
|
+
| 'openrouter'
|
|
9
10
|
| 'google'
|
|
10
11
|
| 'deepseek'
|
|
11
12
|
| 'groq'
|
|
@@ -17,6 +18,7 @@ export type SetupProvider =
|
|
|
17
18
|
| 'deepinfra'
|
|
18
19
|
| 'ollama'
|
|
19
20
|
| 'openclaw'
|
|
21
|
+
| 'hermes'
|
|
20
22
|
| 'custom'
|
|
21
23
|
|
|
22
24
|
export interface SetupProviderOption {
|
|
@@ -52,6 +54,19 @@ export const SETUP_PROVIDERS: SetupProviderOption[] = [
|
|
|
52
54
|
icon: 'O',
|
|
53
55
|
modelLibraryUrl: 'https://platform.openai.com/docs/models',
|
|
54
56
|
},
|
|
57
|
+
{
|
|
58
|
+
id: 'openrouter',
|
|
59
|
+
name: 'OpenRouter',
|
|
60
|
+
description: 'One API key for a broad multi-provider model catalog through an OpenAI-compatible API.',
|
|
61
|
+
requiresKey: true,
|
|
62
|
+
supportsEndpoint: false,
|
|
63
|
+
defaultEndpoint: 'https://openrouter.ai/api/v1',
|
|
64
|
+
keyUrl: 'https://openrouter.ai/keys',
|
|
65
|
+
keyLabel: 'openrouter.ai',
|
|
66
|
+
badge: 'Catalog',
|
|
67
|
+
icon: 'R',
|
|
68
|
+
modelLibraryUrl: 'https://openrouter.ai/models',
|
|
69
|
+
},
|
|
55
70
|
{
|
|
56
71
|
id: 'openclaw',
|
|
57
72
|
name: 'OpenClaw',
|
|
@@ -64,6 +79,18 @@ export const SETUP_PROVIDERS: SetupProviderOption[] = [
|
|
|
64
79
|
badge: 'First-Tier',
|
|
65
80
|
icon: 'C',
|
|
66
81
|
},
|
|
82
|
+
{
|
|
83
|
+
id: 'hermes',
|
|
84
|
+
name: 'Hermes Agent',
|
|
85
|
+
description: 'Connect Hermes Agent through its local or remote OpenAI-compatible API server runtime.',
|
|
86
|
+
requiresKey: false,
|
|
87
|
+
supportsEndpoint: true,
|
|
88
|
+
allowMultiple: true,
|
|
89
|
+
defaultEndpoint: 'http://127.0.0.1:8642/v1',
|
|
90
|
+
optionalKey: true,
|
|
91
|
+
badge: 'API Server',
|
|
92
|
+
icon: 'H',
|
|
93
|
+
},
|
|
67
94
|
{
|
|
68
95
|
id: 'anthropic',
|
|
69
96
|
name: 'Anthropic',
|
|
@@ -192,7 +219,7 @@ export const SETUP_PROVIDERS: SetupProviderOption[] = [
|
|
|
192
219
|
{
|
|
193
220
|
id: 'custom',
|
|
194
221
|
name: 'Custom Provider',
|
|
195
|
-
description: 'Any OpenAI-compatible API endpoint (
|
|
222
|
+
description: 'Any OpenAI-compatible API endpoint (LM Studio, vLLM, local gateways, etc.).',
|
|
196
223
|
requiresKey: false,
|
|
197
224
|
supportsEndpoint: true,
|
|
198
225
|
allowMultiple: true,
|
|
@@ -598,6 +625,13 @@ export const DEFAULT_AGENTS: Record<SetupProvider, DefaultAgentConfig> = {
|
|
|
598
625
|
model: 'gpt-4o',
|
|
599
626
|
tools: STARTER_AGENT_TOOLS,
|
|
600
627
|
},
|
|
628
|
+
openrouter: {
|
|
629
|
+
name: 'Router',
|
|
630
|
+
description: 'A helpful assistant powered through OpenRouter.',
|
|
631
|
+
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
632
|
+
model: 'openai/gpt-4.1-mini',
|
|
633
|
+
tools: STARTER_AGENT_TOOLS,
|
|
634
|
+
},
|
|
601
635
|
google: {
|
|
602
636
|
name: 'Gemini',
|
|
603
637
|
description: 'A helpful Gemini-powered assistant.',
|
|
@@ -675,6 +709,13 @@ export const DEFAULT_AGENTS: Record<SetupProvider, DefaultAgentConfig> = {
|
|
|
675
709
|
model: '',
|
|
676
710
|
tools: STARTER_AGENT_TOOLS,
|
|
677
711
|
},
|
|
712
|
+
hermes: {
|
|
713
|
+
name: 'Hermes',
|
|
714
|
+
description: 'A runtime-backed assistant powered by Hermes Agent.',
|
|
715
|
+
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
716
|
+
model: 'hermes-agent',
|
|
717
|
+
tools: STARTER_AGENT_TOOLS,
|
|
718
|
+
},
|
|
678
719
|
custom: {
|
|
679
720
|
name: 'Custom Agent',
|
|
680
721
|
description: 'An assistant powered by a custom OpenAI-compatible provider.',
|
package/src/types/provider.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ProviderType = 'claude-cli' | 'codex-cli' | 'opencode-cli' | 'gemini-cli' | 'copilot-cli' | 'openai' | 'ollama' | 'anthropic' | 'openclaw' | 'google' | 'deepseek' | 'groq' | 'together' | 'mistral' | 'xai' | 'fireworks' | 'nebius' | 'deepinfra'
|
|
1
|
+
export type ProviderType = 'claude-cli' | 'codex-cli' | 'opencode-cli' | 'gemini-cli' | 'copilot-cli' | 'openai' | 'openrouter' | 'ollama' | 'anthropic' | 'openclaw' | 'hermes' | 'google' | 'deepseek' | 'groq' | 'together' | 'mistral' | 'xai' | 'fireworks' | 'nebius' | 'deepinfra'
|
|
2
2
|
export type ProviderId = ProviderType | (string & {})
|
|
3
3
|
|
|
4
4
|
export interface ProviderInfo {
|