@swarmclawai/swarmclaw 1.9.19 → 1.9.20
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 +10 -1
- package/package.json +1 -1
- package/src/app/api/setup/check-provider/route.ts +20 -1
- package/src/components/agents/agent-sheet.tsx +25 -9
- package/src/components/auth/setup-wizard/step-agents.tsx +1 -0
- package/src/components/auth/setup-wizard/step-connect.tsx +6 -0
- package/src/components/providers/provider-sheet.tsx +4 -1
- package/src/features/providers/queries.ts +1 -1
- package/src/lib/providers/index.test.ts +28 -0
- package/src/lib/providers/index.ts +46 -14
- package/src/lib/providers/openai-compatible-endpoint.ts +67 -0
- package/src/lib/providers/openai.ts +6 -1
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +9 -1
- package/src/lib/server/provider-endpoint.ts +26 -7
- package/src/lib/server/provider-health.test.ts +2 -1
- package/src/lib/server/provider-health.ts +8 -2
- package/src/lib/server/provider-model-discovery.test.ts +21 -0
- package/src/lib/server/provider-model-discovery.ts +6 -1
- package/src/lib/setup-defaults.ts +21 -0
- package/src/types/provider.ts +1 -1
package/README.md
CHANGED
|
@@ -182,7 +182,7 @@ Full hosted deployment guides live at https://swarmclaw.ai/docs/deployment
|
|
|
182
182
|
|
|
183
183
|
## Core Capabilities
|
|
184
184
|
|
|
185
|
-
- **Providers**:
|
|
185
|
+
- **Providers**: 24+ built-in — Claude Code CLI, Codex CLI, OpenCode CLI, Gemini CLI, Copilot CLI, Cursor Agent CLI, Qwen Code CLI, Goose, Anthropic, OpenAI, OpenRouter, Google Gemini, DeepSeek, Groq, Together, Mistral, xAI, Fireworks, Nebius, DeepInfra, Ollama, LM Studio, OpenClaw, and Hermes Agent, plus compatible custom endpoints.
|
|
186
186
|
- **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`.
|
|
187
187
|
- **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.
|
|
188
188
|
- **Delegation**: built-in delegation to Claude Code, Codex CLI, OpenCode CLI, Gemini CLI, Cursor Agent CLI, Qwen Code CLI, and native SwarmClaw subagents.
|
|
@@ -407,6 +407,15 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
407
407
|
|
|
408
408
|
## Releases
|
|
409
409
|
|
|
410
|
+
### v1.9.20 Highlights
|
|
411
|
+
|
|
412
|
+
Provider reliability release: local OpenAI-compatible runtimes now get safer endpoint handling, clearer setup, and first-class LM Studio support.
|
|
413
|
+
|
|
414
|
+
- **LM Studio provider.** LM Studio is available in setup, provider settings, agent editing, model discovery, and connection checks with an optional API key.
|
|
415
|
+
- **Endpoint normalization.** LM Studio and OpenAI-compatible OpenAI overrides normalize bare hosts like `http://127.0.0.1:1234` to `/v1` before calling models or chat completions.
|
|
416
|
+
- **Provider switch isolation.** Switching an agent from a local endpoint back to a fixed cloud provider clears stale per-agent endpoints and fallback keys.
|
|
417
|
+
- **Manual model flow.** Provider model saves now preserve explicit empty endpoint resets and optional-key providers can be tested without creating a credential.
|
|
418
|
+
|
|
410
419
|
### v1.9.19 Highlights
|
|
411
420
|
|
|
412
421
|
Output hygiene release: final assistant responses now use the shared internal metadata scrubber before persistence, UI reset, connector delivery, and completion hooks.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.20",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -5,6 +5,7 @@ import { getDeviceId, wsConnect, rpcOnConnectedGateway } from '@/lib/providers/o
|
|
|
5
5
|
import { isCliProviderId } from '@/lib/providers/cli-provider-metadata'
|
|
6
6
|
import { checkCliProviderReady } from '@/lib/server/cli-provider-readiness'
|
|
7
7
|
import { OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
|
|
8
|
+
import { normalizeLmStudioEndpoint, normalizeOpenAiCompatibleV1Endpoint } from '@/lib/providers/openai-compatible-endpoint'
|
|
8
9
|
import { resolveOllamaRuntimeConfig } from '@/lib/server/ollama-runtime'
|
|
9
10
|
import { normalizeOllamaSetupEndpoint, normalizeOpenClawUrl, parseErrorMessage } from './helpers'
|
|
10
11
|
|
|
@@ -70,6 +71,7 @@ async function checkOpenAiCompatible(
|
|
|
70
71
|
DeepInfra: 'deepseek-ai/DeepSeek-R1-0528',
|
|
71
72
|
OpenRouter: 'openai/gpt-4.1-mini',
|
|
72
73
|
'Hermes Agent': 'hermes-agent',
|
|
74
|
+
'LM Studio': 'local-model',
|
|
73
75
|
}
|
|
74
76
|
testModel = fallbacks[providerName] || 'gpt-4o-mini'
|
|
75
77
|
}
|
|
@@ -312,7 +314,13 @@ export async function POST(req: Request) {
|
|
|
312
314
|
case 'openai': {
|
|
313
315
|
if (!apiKey) return NextResponse.json({ ok: false, message: 'OpenAI API key is required.' })
|
|
314
316
|
const info = OPENAI_COMPATIBLE_DEFAULTS.openai
|
|
315
|
-
const result = await checkOpenAiCompatible(
|
|
317
|
+
const result = await checkOpenAiCompatible(
|
|
318
|
+
info.name,
|
|
319
|
+
apiKey,
|
|
320
|
+
normalizeOpenAiCompatibleV1Endpoint(endpoint || info.defaultEndpoint, info.defaultEndpoint),
|
|
321
|
+
info.defaultEndpoint,
|
|
322
|
+
model,
|
|
323
|
+
)
|
|
316
324
|
return NextResponse.json(result)
|
|
317
325
|
}
|
|
318
326
|
case 'openrouter': {
|
|
@@ -345,6 +353,17 @@ export async function POST(req: Request) {
|
|
|
345
353
|
const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint, model)
|
|
346
354
|
return NextResponse.json(result)
|
|
347
355
|
}
|
|
356
|
+
case 'lmstudio': {
|
|
357
|
+
const info = OPENAI_COMPATIBLE_DEFAULTS.lmstudio
|
|
358
|
+
const result = await checkOpenAiCompatible(
|
|
359
|
+
info.name,
|
|
360
|
+
apiKey,
|
|
361
|
+
normalizeLmStudioEndpoint(endpoint || info.defaultEndpoint),
|
|
362
|
+
info.defaultEndpoint,
|
|
363
|
+
model,
|
|
364
|
+
)
|
|
365
|
+
return NextResponse.json(result)
|
|
366
|
+
}
|
|
348
367
|
case 'ollama': {
|
|
349
368
|
const result = await checkOllama({
|
|
350
369
|
endpointRaw: endpoint,
|
|
@@ -50,6 +50,7 @@ const AUTO_SYNC_MODEL_PROVIDER_IDS = new Set<ProviderType>([
|
|
|
50
50
|
'nebius',
|
|
51
51
|
'deepinfra',
|
|
52
52
|
'hermes',
|
|
53
|
+
'lmstudio',
|
|
53
54
|
'ollama',
|
|
54
55
|
])
|
|
55
56
|
const CONNECTION_TEST_TIMEOUT_MS = 40_000
|
|
@@ -745,6 +746,23 @@ export function AgentSheet() {
|
|
|
745
746
|
if (!model) setModel('default')
|
|
746
747
|
}
|
|
747
748
|
|
|
749
|
+
const applyDirectProviderSelection = (nextProviderId: string) => {
|
|
750
|
+
const nextProvider = agentSelectableProviders.find((item) => item.id === nextProviderId)
|
|
751
|
+
const nextCredentials = resolveAgentSelectableProviderCredentials(nextProviderId, credentials, providerConfigs)
|
|
752
|
+
setProvider(nextProviderId)
|
|
753
|
+
setModel(nextProvider?.models[0] || '')
|
|
754
|
+
setCredentialId(nextCredentials[0]?.id || null)
|
|
755
|
+
setFallbackCredentialIds([])
|
|
756
|
+
setGatewayProfileId(null)
|
|
757
|
+
setApiEndpoint(nextProvider?.requiresEndpoint ? nextProvider.defaultEndpoint || null : null)
|
|
758
|
+
setTestStatus('idle')
|
|
759
|
+
setTestMessage('')
|
|
760
|
+
setTestErrorCode(null)
|
|
761
|
+
setAddingKey(false)
|
|
762
|
+
setNewKeyName('')
|
|
763
|
+
setNewKeyValue('')
|
|
764
|
+
}
|
|
765
|
+
|
|
748
766
|
const updateRoutingTarget = (targetId: string, patch: Partial<AgentRoutingTarget>) => {
|
|
749
767
|
setRoutingTargets((current) => current.map((target) => (
|
|
750
768
|
target.id === targetId
|
|
@@ -778,7 +796,8 @@ export function AgentSheet() {
|
|
|
778
796
|
|
|
779
797
|
const handleSave = async () => {
|
|
780
798
|
// For any endpoint, just ensure bare host:port gets a protocol prepended
|
|
781
|
-
|
|
799
|
+
const providerAllowsAgentEndpoint = Boolean(openclawEnabled || currentProvider?.requiresEndpoint || currentProvider?.optionalEndpoint)
|
|
800
|
+
let normalizedEndpoint = providerAllowsAgentEndpoint ? apiEndpoint : null
|
|
782
801
|
if (normalizedEndpoint) {
|
|
783
802
|
const url = normalizedEndpoint.trim().replace(/\/+$/, '')
|
|
784
803
|
normalizedEndpoint = /^(https?|wss?):\/\//i.test(url) ? url : `http://${url}`
|
|
@@ -1543,13 +1562,7 @@ export function AgentSheet() {
|
|
|
1543
1562
|
return (
|
|
1544
1563
|
<button
|
|
1545
1564
|
key={p.id}
|
|
1546
|
-
onClick={() =>
|
|
1547
|
-
setProvider(p.id)
|
|
1548
|
-
if (!nextCredentials.some((item) => item.id === credentialId)) {
|
|
1549
|
-
setCredentialId(nextCredentials[0]?.id || null)
|
|
1550
|
-
}
|
|
1551
|
-
setGatewayProfileId(null)
|
|
1552
|
-
}}
|
|
1565
|
+
onClick={() => applyDirectProviderSelection(p.id)}
|
|
1553
1566
|
className={`relative py-3.5 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
|
|
1554
1567
|
active:scale-[0.97] text-[14px] font-600 border
|
|
1555
1568
|
${provider === p.id
|
|
@@ -1731,7 +1744,7 @@ export function AgentSheet() {
|
|
|
1731
1744
|
|
|
1732
1745
|
{(currentProvider?.requiresEndpoint || currentProvider?.optionalEndpoint) && (provider !== 'ollama' || ollamaMode === 'local') && (
|
|
1733
1746
|
<div className="mb-8">
|
|
1734
|
-
<SectionLabel>{provider === 'openclaw' ? 'OpenClaw Endpoint' : provider === 'hermes' ? 'Hermes API Endpoint' : 'Endpoint'}</SectionLabel>
|
|
1747
|
+
<SectionLabel>{provider === 'openclaw' ? 'OpenClaw Endpoint' : provider === 'hermes' ? 'Hermes API Endpoint' : provider === 'lmstudio' ? 'LM Studio Endpoint' : 'Endpoint'}</SectionLabel>
|
|
1735
1748
|
<input type="text" value={apiEndpoint || ''} onChange={(e) => setApiEndpoint(e.target.value || null)} placeholder={currentProvider.defaultEndpoint || 'http://localhost:11434'} className={`${inputClass} font-mono text-[14px]`} />
|
|
1736
1749
|
{provider === 'openclaw' && (
|
|
1737
1750
|
<p className="text-[13px] text-text-3/70 mt-2">The URL of your OpenClaw gateway</p>
|
|
@@ -1739,6 +1752,9 @@ export function AgentSheet() {
|
|
|
1739
1752
|
{provider === 'hermes' && (
|
|
1740
1753
|
<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>
|
|
1741
1754
|
)}
|
|
1755
|
+
{provider === 'lmstudio' && (
|
|
1756
|
+
<p className="text-[13px] text-text-3/70 mt-2">Point this at the LM Studio local server. A bare host is normalized to <code className="text-text-2">/v1</code>.</p>
|
|
1757
|
+
)}
|
|
1742
1758
|
</div>
|
|
1743
1759
|
)}
|
|
1744
1760
|
|
|
@@ -335,6 +335,12 @@ export function StepConnect({
|
|
|
335
335
|
<p className="text-[12px] text-text-3">Use any reachable local or remote API-server endpoint exposed by Hermes.</p>
|
|
336
336
|
</div>
|
|
337
337
|
)}
|
|
338
|
+
{provider === 'lmstudio' && (
|
|
339
|
+
<div className="mt-2 space-y-0.5">
|
|
340
|
+
<p className="text-[12px] text-text-3">LM Studio's local server defaults to <code className="text-text-2">http://127.0.0.1:1234/v1</code>.</p>
|
|
341
|
+
<p className="text-[12px] text-text-3">If you paste a host without <code className="text-text-2">/v1</code>, SwarmClaw normalizes it before testing and chat.</p>
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
338
344
|
</div>
|
|
339
345
|
)}
|
|
340
346
|
|
|
@@ -271,7 +271,10 @@ export function ProviderSheet() {
|
|
|
271
271
|
const modelList = models.split(',').map((m) => m.trim()).filter(Boolean)
|
|
272
272
|
const showApiKey = isBuiltin ? editingBuiltin?.requiresApiKey || editingBuiltin?.optionalApiKey : requiresApiKey
|
|
273
273
|
const canDiscoverModels = Boolean(isBuiltin && editingBuiltin?.supportsModelDiscovery)
|
|
274
|
-
const showTestButton = Boolean(
|
|
274
|
+
const showTestButton = Boolean(
|
|
275
|
+
isBuiltin
|
|
276
|
+
&& (editingBuiltin?.requiresApiKey ? credentialId : (showApiKey || editingBuiltin?.requiresEndpoint || editingBuiltin?.optionalEndpoint)),
|
|
277
|
+
)
|
|
275
278
|
|
|
276
279
|
const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
|
|
277
280
|
|
|
@@ -86,7 +86,7 @@ export function useSaveBuiltinProviderMutation() {
|
|
|
86
86
|
await api('PUT', `/providers/${id}/models`, { models })
|
|
87
87
|
return api('PUT', `/providers/${id}`, {
|
|
88
88
|
isEnabled,
|
|
89
|
-
...(baseUrl ? { baseUrl } : {}),
|
|
89
|
+
...(typeof baseUrl === 'string' ? { baseUrl } : {}),
|
|
90
90
|
})
|
|
91
91
|
},
|
|
92
92
|
onSuccess: async () => {
|
|
@@ -77,6 +77,34 @@ test('builtin provider override records do not surface as custom providers', ()
|
|
|
77
77
|
assert.equal(output.openAiCount, 1)
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
+
test('LM Studio is available as a first-class local OpenAI-compatible provider', () => {
|
|
81
|
+
const output = runWithTempDataDir<{
|
|
82
|
+
providerName: string | null
|
|
83
|
+
defaultEndpoint: string | null
|
|
84
|
+
requiresApiKey: boolean | null
|
|
85
|
+
optionalApiKey: boolean | null
|
|
86
|
+
supportsModelDiscovery: boolean | null
|
|
87
|
+
}>(`
|
|
88
|
+
const providersModule = await import('@/lib/providers/index')
|
|
89
|
+
const providers = providersModule.default || providersModule
|
|
90
|
+
const provider = providers.getProviderList().find((entry) => entry.id === 'lmstudio')
|
|
91
|
+
|
|
92
|
+
console.log(JSON.stringify({
|
|
93
|
+
providerName: provider?.name ?? null,
|
|
94
|
+
defaultEndpoint: provider?.defaultEndpoint ?? null,
|
|
95
|
+
requiresApiKey: provider?.requiresApiKey ?? null,
|
|
96
|
+
optionalApiKey: provider?.optionalApiKey ?? null,
|
|
97
|
+
supportsModelDiscovery: provider?.supportsModelDiscovery ?? null,
|
|
98
|
+
}))
|
|
99
|
+
`)
|
|
100
|
+
|
|
101
|
+
assert.equal(output.providerName, 'LM Studio')
|
|
102
|
+
assert.equal(output.defaultEndpoint, 'http://127.0.0.1:1234/v1')
|
|
103
|
+
assert.equal(output.requiresApiKey, false)
|
|
104
|
+
assert.equal(output.optionalApiKey, true)
|
|
105
|
+
assert.equal(output.supportsModelDiscovery, true)
|
|
106
|
+
})
|
|
107
|
+
|
|
80
108
|
test('custom provider resolution includes defaultEndpoint and optionalApiKey', () => {
|
|
81
109
|
const output = runWithTempDataDir<{
|
|
82
110
|
defaultEndpoint: string | null
|
|
@@ -14,6 +14,7 @@ import { streamOpenAiChat } from './openai'
|
|
|
14
14
|
import { streamOllamaChat } from './ollama'
|
|
15
15
|
import { streamAnthropicChat } from './anthropic'
|
|
16
16
|
import { streamOpenClawChat } from './openclaw'
|
|
17
|
+
import { normalizeLmStudioEndpoint, normalizeOpenAiCompatibleV1Endpoint } from './openai-compatible-endpoint'
|
|
17
18
|
import { errorMessage, sleep, jitteredBackoff } from '@/lib/shared-utils'
|
|
18
19
|
import { classifyProviderError } from './error-classification'
|
|
19
20
|
import { log } from '@/lib/server/logger'
|
|
@@ -173,6 +174,24 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
173
174
|
},
|
|
174
175
|
},
|
|
175
176
|
},
|
|
177
|
+
lmstudio: {
|
|
178
|
+
id: 'lmstudio',
|
|
179
|
+
name: 'LM Studio',
|
|
180
|
+
models: ['local-model', 'google/gemma-4-e4b'],
|
|
181
|
+
requiresApiKey: false,
|
|
182
|
+
optionalApiKey: true,
|
|
183
|
+
requiresEndpoint: true,
|
|
184
|
+
defaultEndpoint: 'http://127.0.0.1:1234/v1',
|
|
185
|
+
handler: {
|
|
186
|
+
streamChat: (opts) => {
|
|
187
|
+
const patchedSession = {
|
|
188
|
+
...opts.session,
|
|
189
|
+
apiEndpoint: normalizeLmStudioEndpoint(opts.session.apiEndpoint),
|
|
190
|
+
}
|
|
191
|
+
return streamOpenAiChat({ ...opts, session: patchedSession })
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
176
195
|
'opencode-cli': {
|
|
177
196
|
id: 'opencode-cli',
|
|
178
197
|
name: 'OpenCode CLI',
|
|
@@ -560,22 +579,29 @@ export function getProvider(id: string): BuiltinProviderConfig | null {
|
|
|
560
579
|
if (builtin) {
|
|
561
580
|
const pConfigs = loadProviderConfigs()
|
|
562
581
|
const pConfig = pConfigs[id]
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
582
|
+
const originalHandler = builtin.handler
|
|
583
|
+
return {
|
|
584
|
+
...builtin,
|
|
585
|
+
handler: {
|
|
586
|
+
streamChat: (opts) => {
|
|
587
|
+
const configuredEndpoint = typeof pConfig?.baseUrl === 'string' && pConfig.baseUrl.trim()
|
|
588
|
+
? normalizeProviderRuntimeEndpoint(id, pConfig.baseUrl)
|
|
589
|
+
: null
|
|
590
|
+
const sessionEndpoint = typeof opts.session.apiEndpoint === 'string' && opts.session.apiEndpoint.trim()
|
|
591
|
+
? normalizeProviderRuntimeEndpoint(id, opts.session.apiEndpoint)
|
|
592
|
+
: null
|
|
593
|
+
const honorsAgentEndpoint = Boolean(builtin.requiresEndpoint || builtin.optionalEndpoint)
|
|
594
|
+
const apiEndpoint = honorsAgentEndpoint
|
|
595
|
+
? sessionEndpoint || configuredEndpoint
|
|
596
|
+
: configuredEndpoint
|
|
597
|
+
const patchedSession = {
|
|
598
|
+
...opts.session,
|
|
599
|
+
apiEndpoint: apiEndpoint || undefined,
|
|
600
|
+
}
|
|
601
|
+
return originalHandler.streamChat({ ...opts, session: patchedSession })
|
|
575
602
|
},
|
|
576
|
-
}
|
|
603
|
+
},
|
|
577
604
|
}
|
|
578
|
-
return builtin
|
|
579
605
|
}
|
|
580
606
|
|
|
581
607
|
// Check custom providers
|
|
@@ -619,6 +645,12 @@ export function getProvider(id: string): BuiltinProviderConfig | null {
|
|
|
619
645
|
return null
|
|
620
646
|
}
|
|
621
647
|
|
|
648
|
+
function normalizeProviderRuntimeEndpoint(providerId: string, endpoint: string): string {
|
|
649
|
+
if (providerId === 'lmstudio') return normalizeLmStudioEndpoint(endpoint)
|
|
650
|
+
if (providerId === 'openai') return normalizeOpenAiCompatibleV1Endpoint(endpoint, 'https://api.openai.com/v1')
|
|
651
|
+
return endpoint.replace(/\/+$/, '')
|
|
652
|
+
}
|
|
653
|
+
|
|
622
654
|
/**
|
|
623
655
|
* Stream chat with automatic failover to fallback credentials on retryable errors.
|
|
624
656
|
* Falls back through fallbackCredentialIds on 401/429/500/502/503 errors.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const DEFAULT_LM_STUDIO_ENDPOINT = 'http://127.0.0.1:1234/v1'
|
|
2
|
+
|
|
3
|
+
const OPENAI_COMPATIBLE_ENDPOINT_SUFFIXES = [
|
|
4
|
+
'/chat/completions',
|
|
5
|
+
'/responses',
|
|
6
|
+
'/models',
|
|
7
|
+
'/completions',
|
|
8
|
+
'/embeddings',
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
function clean(value: string | null | undefined): string {
|
|
12
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function trimTrailingSlashes(value: string): string {
|
|
16
|
+
let output = value
|
|
17
|
+
while (output.endsWith('/') && output.length > 1) output = output.slice(0, -1)
|
|
18
|
+
return output
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toUrl(value: string): URL | null {
|
|
22
|
+
const trimmed = clean(value)
|
|
23
|
+
if (!trimmed) return null
|
|
24
|
+
try {
|
|
25
|
+
return new URL(trimmed)
|
|
26
|
+
} catch {
|
|
27
|
+
try {
|
|
28
|
+
return new URL(`http://${trimmed}`)
|
|
29
|
+
} catch {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function stripKnownEndpointPath(pathname: string): string {
|
|
36
|
+
let path = trimTrailingSlashes(pathname || '/')
|
|
37
|
+
const lower = path.toLowerCase()
|
|
38
|
+
for (const suffix of OPENAI_COMPATIBLE_ENDPOINT_SUFFIXES) {
|
|
39
|
+
if (lower === suffix || lower.endsWith(suffix)) {
|
|
40
|
+
path = path.slice(0, path.length - suffix.length)
|
|
41
|
+
break
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
path = trimTrailingSlashes(path)
|
|
45
|
+
return path || '/'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeOpenAiCompatibleV1Endpoint(
|
|
49
|
+
input: string | null | undefined,
|
|
50
|
+
fallback = DEFAULT_LM_STUDIO_ENDPOINT,
|
|
51
|
+
): string {
|
|
52
|
+
const parsed = toUrl(clean(input) || fallback) || toUrl(fallback)
|
|
53
|
+
if (!parsed) return trimTrailingSlashes(clean(input) || fallback)
|
|
54
|
+
|
|
55
|
+
const cleanedPath = stripKnownEndpointPath(parsed.pathname)
|
|
56
|
+
parsed.pathname = cleanedPath.toLowerCase().endsWith('/v1')
|
|
57
|
+
? cleanedPath
|
|
58
|
+
: `${cleanedPath === '/' ? '' : cleanedPath}/v1`
|
|
59
|
+
parsed.search = ''
|
|
60
|
+
parsed.hash = ''
|
|
61
|
+
return trimTrailingSlashes(parsed.toString())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function normalizeLmStudioEndpoint(input?: string | null): string {
|
|
65
|
+
return normalizeOpenAiCompatibleV1Endpoint(input, DEFAULT_LM_STUDIO_ENDPOINT)
|
|
66
|
+
}
|
|
67
|
+
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
createReasoningContentMetadata,
|
|
8
8
|
shouldUseDeepSeekReasoningBridge,
|
|
9
9
|
} from '@/lib/providers/deepseek-reasoning-chat-openai'
|
|
10
|
+
import { normalizeLmStudioEndpoint, normalizeOpenAiCompatibleV1Endpoint } from '@/lib/providers/openai-compatible-endpoint'
|
|
10
11
|
|
|
11
12
|
const TAG = 'provider-openai'
|
|
12
13
|
|
|
@@ -58,7 +59,11 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
58
59
|
let fullResponse = ''
|
|
59
60
|
|
|
60
61
|
// Support custom base URLs for custom providers
|
|
61
|
-
const baseUrl = session.
|
|
62
|
+
const baseUrl = session.provider === 'lmstudio'
|
|
63
|
+
? normalizeLmStudioEndpoint(session.apiEndpoint)
|
|
64
|
+
: session.provider === 'openai'
|
|
65
|
+
? normalizeOpenAiCompatibleV1Endpoint(session.apiEndpoint, PROVIDER_DEFAULTS.openai)
|
|
66
|
+
: session.apiEndpoint || PROVIDER_DEFAULTS.openai
|
|
62
67
|
const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`
|
|
63
68
|
|
|
64
69
|
// OpenClaw endpoints behind Hostinger's proxy use express.json() middleware
|
|
@@ -263,6 +263,42 @@ test('buildChatModel uses a reasoning_content-preserving bridge for DeepSeek', (
|
|
|
263
263
|
assert.equal(completionBridge.completions?.constructor?.name, 'DeepSeekReasoningChatOpenAICompletions')
|
|
264
264
|
})
|
|
265
265
|
|
|
266
|
+
test('buildChatModel ignores stale per-agent endpoints for fixed cloud providers', () => {
|
|
267
|
+
const llm = buildChatModel({
|
|
268
|
+
provider: 'deepseek',
|
|
269
|
+
model: 'deepseek-chat',
|
|
270
|
+
apiKey: 'deepseek-key',
|
|
271
|
+
apiEndpoint: 'http://127.0.0.1:1234/v1',
|
|
272
|
+
}) as ChatOpenAI
|
|
273
|
+
|
|
274
|
+
assert.equal(llm.model, 'deepseek-chat')
|
|
275
|
+
assert.equal(llm.clientConfig?.baseURL, 'https://api.deepseek.com/v1')
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test('buildChatModel normalizes OpenAI-compatible OpenAI overrides to the v1 API', () => {
|
|
279
|
+
const llm = buildChatModel({
|
|
280
|
+
provider: 'openai',
|
|
281
|
+
model: 'google/gemma-4-e4b',
|
|
282
|
+
apiKey: 'local-key',
|
|
283
|
+
apiEndpoint: 'http://10.2.0.2:1234',
|
|
284
|
+
}) as ChatOpenAI
|
|
285
|
+
|
|
286
|
+
assert.equal(llm.model, 'google/gemma-4-e4b')
|
|
287
|
+
assert.equal(llm.clientConfig?.baseURL, 'http://10.2.0.2:1234/v1')
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
test('buildChatModel normalizes LM Studio base URLs to the OpenAI-compatible v1 API', () => {
|
|
291
|
+
const llm = buildChatModel({
|
|
292
|
+
provider: 'lmstudio',
|
|
293
|
+
model: 'google/gemma-4-e4b',
|
|
294
|
+
apiKey: 'lm-studio-key',
|
|
295
|
+
apiEndpoint: 'http://10.2.0.2:1234',
|
|
296
|
+
}) as ChatOpenAI
|
|
297
|
+
|
|
298
|
+
assert.equal(llm.model, 'google/gemma-4-e4b')
|
|
299
|
+
assert.equal(llm.clientConfig?.baseURL, 'http://10.2.0.2:1234/v1')
|
|
300
|
+
})
|
|
301
|
+
|
|
266
302
|
test('buildChatModel uses Ollama Cloud only when explicit cloud mode is selected', () => {
|
|
267
303
|
saveCredentials({
|
|
268
304
|
'cred-1': {
|
|
@@ -74,7 +74,15 @@ export function buildChatModel(opts: {
|
|
|
74
74
|
const resolvedApiKey = apiKey ?? resolveApiKeyFromCredential(resolvedCredentialId)
|
|
75
75
|
const providers = getProviderList()
|
|
76
76
|
const providerInfo = providers.find((p) => p.id === provider)
|
|
77
|
-
const endpointRaw =
|
|
77
|
+
const endpointRaw = provider === 'ollama'
|
|
78
|
+
? apiEndpoint || providerInfo?.defaultEndpoint || null
|
|
79
|
+
: resolveProviderApiEndpoint({
|
|
80
|
+
provider,
|
|
81
|
+
model,
|
|
82
|
+
ollamaMode: ollamaMode ?? null,
|
|
83
|
+
credentialId: resolvedCredentialId,
|
|
84
|
+
apiEndpoint,
|
|
85
|
+
}) || providerInfo?.defaultEndpoint || null
|
|
78
86
|
const endpoint = provider === 'openclaw'
|
|
79
87
|
? normalizeOpenClawEndpoint(endpointRaw)
|
|
80
88
|
: endpointRaw
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
|
|
2
2
|
import { getProvider } from '@/lib/providers'
|
|
3
|
+
import { normalizeLmStudioEndpoint, normalizeOpenAiCompatibleV1Endpoint } from '@/lib/providers/openai-compatible-endpoint'
|
|
3
4
|
import { loadCredential } from '@/lib/server/credentials/credential-repository'
|
|
4
5
|
import { listCredentialIdsByProvider, resolveCredentialSecret } from '@/lib/server/credentials/credential-service'
|
|
5
6
|
import { resolveOllamaRuntimeConfig } from '@/lib/server/ollama-runtime'
|
|
@@ -71,8 +72,17 @@ export function resolveProviderApiEndpoint(input: {
|
|
|
71
72
|
const provider = clean(input.provider)
|
|
72
73
|
if (!provider) return null
|
|
73
74
|
|
|
74
|
-
const
|
|
75
|
-
|
|
75
|
+
const pConfigs = loadProviderConfigs()
|
|
76
|
+
const pConfig = pConfigs[provider]
|
|
77
|
+
const providerInfo = getProvider(provider)
|
|
78
|
+
const honorsAgentEndpoint = Boolean(
|
|
79
|
+
pConfig?.type === 'custom'
|
|
80
|
+
|| providerInfo?.requiresEndpoint
|
|
81
|
+
|| providerInfo?.optionalEndpoint,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const explicitEndpoint = normalizeRuntimeEndpoint(provider, input.apiEndpoint ?? null)
|
|
85
|
+
if (explicitEndpoint && honorsAgentEndpoint) return explicitEndpoint
|
|
76
86
|
|
|
77
87
|
if (provider === 'ollama') {
|
|
78
88
|
const credentialId = resolveProviderCredentialId(input)
|
|
@@ -86,15 +96,24 @@ export function resolveProviderApiEndpoint(input: {
|
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
// Prefer provider config's custom baseUrl over the hardcoded defaultEndpoint
|
|
89
|
-
const pConfigs = loadProviderConfigs()
|
|
90
|
-
const pConfig = pConfigs[provider]
|
|
91
99
|
if (pConfig?.baseUrl) {
|
|
92
|
-
const customNormalized =
|
|
100
|
+
const customNormalized = normalizeRuntimeEndpoint(provider, pConfig.baseUrl)
|
|
93
101
|
if (customNormalized) return customNormalized
|
|
94
102
|
return pConfig.baseUrl.replace(/\/+$/, '')
|
|
95
103
|
}
|
|
96
104
|
|
|
97
|
-
const providerInfo = getProvider(provider)
|
|
98
105
|
if (!providerInfo?.defaultEndpoint) return null
|
|
99
|
-
return
|
|
106
|
+
return normalizeRuntimeEndpoint(provider, providerInfo.defaultEndpoint) || providerInfo.defaultEndpoint.replace(/\/+$/, '')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeRuntimeEndpoint(provider: string, endpoint: string | null | undefined): string | null {
|
|
110
|
+
if (provider === 'lmstudio') {
|
|
111
|
+
const value = typeof endpoint === 'string' ? endpoint.trim() : ''
|
|
112
|
+
return value ? normalizeLmStudioEndpoint(value) : null
|
|
113
|
+
}
|
|
114
|
+
if (provider === 'openai') {
|
|
115
|
+
const value = typeof endpoint === 'string' ? endpoint.trim() : ''
|
|
116
|
+
return value ? normalizeOpenAiCompatibleV1Endpoint(value, 'https://api.openai.com/v1') : null
|
|
117
|
+
}
|
|
118
|
+
return normalizeProviderEndpoint(provider, endpoint)
|
|
100
119
|
}
|
|
@@ -129,12 +129,13 @@ describe('provider-health', () => {
|
|
|
129
129
|
assert.ok(defaults.xai)
|
|
130
130
|
assert.ok(defaults.fireworks)
|
|
131
131
|
assert.ok(defaults.hermes)
|
|
132
|
+
assert.ok(defaults.lmstudio)
|
|
132
133
|
|
|
133
134
|
// Each entry has name and defaultEndpoint
|
|
134
135
|
for (const [key, val] of Object.entries(defaults)) {
|
|
135
136
|
assert.ok(typeof val.name === 'string' && val.name.length > 0)
|
|
136
137
|
assert.ok(typeof val.defaultEndpoint === 'string' && val.defaultEndpoint.length > 0)
|
|
137
|
-
if (key === 'hermes') {
|
|
138
|
+
if (key === 'hermes' || key === 'lmstudio') {
|
|
138
139
|
assert.ok(val.defaultEndpoint.startsWith('http://'))
|
|
139
140
|
} else {
|
|
140
141
|
assert.ok(val.defaultEndpoint.startsWith('https://'))
|
|
@@ -3,6 +3,7 @@ import { errorMessage, hmrSingleton, jitteredBackoff } from '@/lib/shared-utils'
|
|
|
3
3
|
import { upsertStoredItem, loadCollection } from './storage'
|
|
4
4
|
import { log } from './logger'
|
|
5
5
|
import { isCliProviderId } from '@/lib/providers/cli-provider-metadata'
|
|
6
|
+
import { normalizeLmStudioEndpoint, normalizeOpenAiCompatibleV1Endpoint } from '@/lib/providers/openai-compatible-endpoint'
|
|
6
7
|
|
|
7
8
|
const TAG = 'provider-health'
|
|
8
9
|
|
|
@@ -270,6 +271,7 @@ export const OPENAI_COMPATIBLE_DEFAULTS: Record<string, { name: string; defaultE
|
|
|
270
271
|
nebius: { name: 'Nebius', defaultEndpoint: 'https://api.tokenfactory.nebius.com/v1' },
|
|
271
272
|
deepinfra: { name: 'DeepInfra', defaultEndpoint: 'https://api.deepinfra.com/v1/openai' },
|
|
272
273
|
hermes: { name: 'Hermes Agent', defaultEndpoint: 'http://127.0.0.1:8642/v1' },
|
|
274
|
+
lmstudio: { name: 'LM Studio', defaultEndpoint: 'http://127.0.0.1:1234/v1' },
|
|
273
275
|
}
|
|
274
276
|
|
|
275
277
|
export async function pingOpenAiCompatible(
|
|
@@ -353,7 +355,7 @@ export async function pingProvider(
|
|
|
353
355
|
apiKey: string | undefined,
|
|
354
356
|
endpoint: string | undefined,
|
|
355
357
|
): Promise<{ ok: boolean; message: string }> {
|
|
356
|
-
const OPTIONAL_OPENAI_COMPATIBLE_KEY_PROVIDERS = new Set(['hermes'])
|
|
358
|
+
const OPTIONAL_OPENAI_COMPATIBLE_KEY_PROVIDERS = new Set(['hermes', 'lmstudio'])
|
|
357
359
|
if (isCliProviderId(provider)) return { ok: true, message: 'CLI provider - skipped.' }
|
|
358
360
|
|
|
359
361
|
try {
|
|
@@ -369,7 +371,11 @@ export async function pingProvider(
|
|
|
369
371
|
}
|
|
370
372
|
// OpenAI-compatible providers (openai, google, deepseek, groq, together, mistral, xai, fireworks, custom)
|
|
371
373
|
const defaults = OPENAI_COMPATIBLE_DEFAULTS[provider]
|
|
372
|
-
const resolvedEndpoint =
|
|
374
|
+
const resolvedEndpoint = provider === 'lmstudio'
|
|
375
|
+
? normalizeLmStudioEndpoint(endpoint || defaults?.defaultEndpoint)
|
|
376
|
+
: provider === 'openai'
|
|
377
|
+
? normalizeOpenAiCompatibleV1Endpoint(endpoint || defaults?.defaultEndpoint, defaults?.defaultEndpoint || 'https://api.openai.com/v1')
|
|
378
|
+
: endpoint || defaults?.defaultEndpoint
|
|
373
379
|
if (!resolvedEndpoint) return { ok: false, message: `No endpoint for provider "${provider}".` }
|
|
374
380
|
if (!apiKey && !OPTIONAL_OPENAI_COMPATIBLE_KEY_PROVIDERS.has(provider)) return { ok: false, message: 'No API key configured.' }
|
|
375
381
|
return await pingOpenAiCompatible(apiKey, resolvedEndpoint)
|
|
@@ -218,6 +218,16 @@ test('resolveDescriptor uses OpenRouter as an OpenAI-compatible provider', () =>
|
|
|
218
218
|
assert.equal(descriptor?.optionalApiKey, false)
|
|
219
219
|
})
|
|
220
220
|
|
|
221
|
+
test('resolveDescriptor normalizes OpenAI-compatible OpenAI endpoints to /v1', () => {
|
|
222
|
+
const descriptor = resolveDescriptor({
|
|
223
|
+
providerId: 'openai',
|
|
224
|
+
endpoint: 'http://10.2.0.2:1234',
|
|
225
|
+
})
|
|
226
|
+
assert.equal(descriptor?.strategy, 'openai-compatible')
|
|
227
|
+
assert.equal(descriptor?.endpoint, 'http://10.2.0.2:1234/v1')
|
|
228
|
+
assert.equal(descriptor?.requiresApiKey, true)
|
|
229
|
+
})
|
|
230
|
+
|
|
221
231
|
test('resolveDescriptor uses Hermes as an OpenAI-compatible provider with optional auth', () => {
|
|
222
232
|
const descriptor = resolveDescriptor({
|
|
223
233
|
providerId: 'hermes',
|
|
@@ -228,6 +238,17 @@ test('resolveDescriptor uses Hermes as an OpenAI-compatible provider with option
|
|
|
228
238
|
assert.equal(descriptor?.optionalApiKey, true)
|
|
229
239
|
})
|
|
230
240
|
|
|
241
|
+
test('resolveDescriptor normalizes LM Studio discovery endpoints to /v1 with optional auth', () => {
|
|
242
|
+
const descriptor = resolveDescriptor({
|
|
243
|
+
providerId: 'lmstudio',
|
|
244
|
+
endpoint: 'http://10.2.0.2:1234',
|
|
245
|
+
})
|
|
246
|
+
assert.equal(descriptor?.strategy, 'openai-compatible')
|
|
247
|
+
assert.equal(descriptor?.endpoint, 'http://10.2.0.2:1234/v1')
|
|
248
|
+
assert.equal(descriptor?.requiresApiKey, false)
|
|
249
|
+
assert.equal(descriptor?.optionalApiKey, true)
|
|
250
|
+
})
|
|
251
|
+
|
|
231
252
|
test('resolveDescriptor disables model discovery for local CLI-backed providers without live model catalogs', () => {
|
|
232
253
|
for (const providerId of ['copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose']) {
|
|
233
254
|
const descriptor = resolveDescriptor({ providerId })
|
|
@@ -4,6 +4,7 @@ import { getProviderList } from '@/lib/providers'
|
|
|
4
4
|
import { isOllamaCloudEndpoint, resolveStoredOllamaMode } from '@/lib/ollama-mode'
|
|
5
5
|
import { OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
|
|
6
6
|
import { resolveCredentialSecret } from '@/lib/server/credentials/credential-service'
|
|
7
|
+
import { normalizeLmStudioEndpoint, normalizeOpenAiCompatibleV1Endpoint } from '@/lib/providers/openai-compatible-endpoint'
|
|
7
8
|
import type { ProviderInfo, ProviderModelDiscoveryResult } from '@/types'
|
|
8
9
|
|
|
9
10
|
type DiscoveryStrategy = 'openai-compatible' | 'anthropic' | 'google' | 'ollama' | 'openclaw'
|
|
@@ -164,7 +165,11 @@ export function resolveDescriptor(input: DiscoverProviderModelsInput): Discovery
|
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
const openAiDefault = OPENAI_COMPATIBLE_DEFAULTS[providerId as keyof typeof OPENAI_COMPATIBLE_DEFAULTS]?.defaultEndpoint
|
|
167
|
-
const endpoint =
|
|
168
|
+
const endpoint = providerId === 'lmstudio'
|
|
169
|
+
? normalizeLmStudioEndpoint(input.endpoint || provider.defaultEndpoint || openAiDefault || '')
|
|
170
|
+
: providerId === 'openai'
|
|
171
|
+
? normalizeOpenAiCompatibleV1Endpoint(input.endpoint || provider.defaultEndpoint || openAiDefault || '', openAiDefault || 'https://api.openai.com/v1')
|
|
172
|
+
: normalizeEndpoint(input.endpoint, provider.defaultEndpoint || openAiDefault || '')
|
|
168
173
|
return {
|
|
169
174
|
providerId,
|
|
170
175
|
providerName: provider.name,
|
|
@@ -112,6 +112,20 @@ export const SETUP_PROVIDERS: SetupProviderOption[] = [
|
|
|
112
112
|
icon: 'H',
|
|
113
113
|
category: 'gateway',
|
|
114
114
|
},
|
|
115
|
+
{
|
|
116
|
+
id: 'lmstudio',
|
|
117
|
+
name: 'LM Studio',
|
|
118
|
+
description: 'Use locally served LM Studio models through the OpenAI-compatible API.',
|
|
119
|
+
requiresKey: false,
|
|
120
|
+
supportsEndpoint: true,
|
|
121
|
+
allowMultiple: true,
|
|
122
|
+
defaultEndpoint: 'http://127.0.0.1:1234/v1',
|
|
123
|
+
optionalKey: true,
|
|
124
|
+
badge: 'Local API',
|
|
125
|
+
icon: 'L',
|
|
126
|
+
modelLibraryUrl: 'https://lmstudio.ai/docs/developer/openai-compat',
|
|
127
|
+
category: 'local',
|
|
128
|
+
},
|
|
115
129
|
{
|
|
116
130
|
id: 'anthropic',
|
|
117
131
|
name: 'Anthropic',
|
|
@@ -851,6 +865,13 @@ export const DEFAULT_AGENTS = {
|
|
|
851
865
|
model: 'hermes-agent',
|
|
852
866
|
tools: STARTER_AGENT_TOOLS,
|
|
853
867
|
},
|
|
868
|
+
lmstudio: {
|
|
869
|
+
name: 'LM Studio',
|
|
870
|
+
description: 'A local assistant running through LM Studio.',
|
|
871
|
+
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
872
|
+
model: 'local-model',
|
|
873
|
+
tools: STARTER_AGENT_TOOLS,
|
|
874
|
+
},
|
|
854
875
|
custom: {
|
|
855
876
|
name: 'Custom Agent',
|
|
856
877
|
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' | 'opencode-web' | 'gemini-cli' | 'copilot-cli' | 'droid-cli' | 'cursor-cli' | 'qwen-code-cli' | 'goose' | 'aider-cli' | 'amp-cli' | 'augment-cli' | 'adal-cli' | 'bob-cli' | 'cline-cli' | 'codebuddy-cli' | 'command-code-cli' | 'continue-cli' | 'cortex-cli' | 'crush-cli' | 'deepagents-cli' | 'firebender-cli' | 'iflow-cli' | 'junie-cli' | 'kilo-code-cli' | 'kimi-cli' | 'kode-cli' | 'mcpjam-cli' | 'mistral-vibe-cli' | 'mux-cli' | 'neovate-cli' | 'openhands-cli' | 'pochi-cli' | 'qoder-cli' | 'replit-cli' | 'roo-code-cli' | 'trae-cn-cli' | 'warp-cli' | 'windsurf-cli' | 'zencoder-cli' | 'openai' | 'openrouter' | 'ollama' | 'anthropic' | 'openclaw' | 'hermes' | 'google' | 'deepseek' | 'groq' | 'together' | 'mistral' | 'xai' | 'fireworks' | 'nebius' | 'deepinfra'
|
|
1
|
+
export type ProviderType = 'claude-cli' | 'codex-cli' | 'opencode-cli' | 'opencode-web' | 'gemini-cli' | 'copilot-cli' | 'droid-cli' | 'cursor-cli' | 'qwen-code-cli' | 'goose' | 'aider-cli' | 'amp-cli' | 'augment-cli' | 'adal-cli' | 'bob-cli' | 'cline-cli' | 'codebuddy-cli' | 'command-code-cli' | 'continue-cli' | 'cortex-cli' | 'crush-cli' | 'deepagents-cli' | 'firebender-cli' | 'iflow-cli' | 'junie-cli' | 'kilo-code-cli' | 'kimi-cli' | 'kode-cli' | 'mcpjam-cli' | 'mistral-vibe-cli' | 'mux-cli' | 'neovate-cli' | 'openhands-cli' | 'pochi-cli' | 'qoder-cli' | 'replit-cli' | 'roo-code-cli' | 'trae-cn-cli' | 'warp-cli' | 'windsurf-cli' | 'zencoder-cli' | 'openai' | 'openrouter' | 'ollama' | 'anthropic' | 'openclaw' | 'hermes' | 'lmstudio' | 'google' | 'deepseek' | 'groq' | 'together' | 'mistral' | 'xai' | 'fireworks' | 'nebius' | 'deepinfra'
|
|
2
2
|
export type ProviderId = ProviderType | (string & {})
|
|
3
3
|
|
|
4
4
|
export interface ProviderInfo {
|