@swarmclawai/swarmclaw 1.9.19 → 1.9.21
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 +23 -3
- package/package.json +3 -3
- package/src/app/api/setup/check-provider/route.test.ts +44 -0
- package/src/app/api/setup/check-provider/route.ts +255 -64
- package/src/components/agents/agent-sheet.tsx +43 -14
- package/src/components/auth/setup-wizard/step-agents.tsx +1 -0
- package/src/components/auth/setup-wizard/step-connect.tsx +19 -3
- package/src/components/auth/setup-wizard/types.ts +2 -9
- package/src/components/providers/provider-diagnostics-list.tsx +58 -0
- package/src/components/providers/provider-sheet.tsx +15 -2
- package/src/features/providers/queries.ts +3 -2
- 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-diagnostics.test.ts +39 -0
- package/src/lib/server/provider-diagnostics.ts +114 -0
- 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 +22 -1
package/README.md
CHANGED
|
@@ -81,8 +81,10 @@ Extension tutorial: https://swarmclaw.ai/docs/extension-tutorial
|
|
|
81
81
|
Download the one-click installer from [swarmclaw.ai/downloads](https://swarmclaw.ai/downloads).
|
|
82
82
|
Available for macOS (Apple Silicon & Intel), Windows, and Linux (AppImage + .deb).
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
The release workflow supports Developer ID signing and notarization when Apple
|
|
85
|
+
credentials are configured. If a macOS build is still ad-hoc signed, first
|
|
86
|
+
launch may need one manual approval:
|
|
87
|
+
- **macOS:** right-click the app in Finder → **Open** → **Open** to bypass Gatekeeper. If macOS instead reports *"SwarmClaw is damaged and can't be opened"* (common when the dmg was quarantined by Safari), strip the quarantine attribute and relaunch:
|
|
86
88
|
```bash
|
|
87
89
|
xattr -dr com.apple.quarantine /Applications/SwarmClaw.app
|
|
88
90
|
```
|
|
@@ -182,7 +184,7 @@ Full hosted deployment guides live at https://swarmclaw.ai/docs/deployment
|
|
|
182
184
|
|
|
183
185
|
## Core Capabilities
|
|
184
186
|
|
|
185
|
-
- **Providers**:
|
|
187
|
+
- **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
188
|
- **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
189
|
- **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
190
|
- **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 +409,24 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
407
409
|
|
|
408
410
|
## Releases
|
|
409
411
|
|
|
412
|
+
### v1.9.21 Highlights
|
|
413
|
+
|
|
414
|
+
Provider diagnostics release: connection checks now return a structured step timeline across setup, provider settings, and agent editing.
|
|
415
|
+
|
|
416
|
+
- **Connection timelines.** Provider checks show endpoint resolution, model discovery, fallback selection, and chat/gateway verification steps.
|
|
417
|
+
- **Safer error details.** Token-like values are redacted before check messages or diagnostics are returned to the UI.
|
|
418
|
+
- **Local runtime debugging.** LM Studio, Ollama, custom OpenAI-compatible endpoints, cloud providers, OpenClaw gateways, and CLI providers all report concise pass/fail diagnostics.
|
|
419
|
+
- **macOS signing path.** Desktop releases now forward Developer ID and Apple notarization credentials when configured, while ad-hoc fallback builds keep the quarantine workaround documented.
|
|
420
|
+
|
|
421
|
+
### v1.9.20 Highlights
|
|
422
|
+
|
|
423
|
+
Provider reliability release: local OpenAI-compatible runtimes now get safer endpoint handling, clearer setup, and first-class LM Studio support.
|
|
424
|
+
|
|
425
|
+
- **LM Studio provider.** LM Studio is available in setup, provider settings, agent editing, model discovery, and connection checks with an optional API key.
|
|
426
|
+
- **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.
|
|
427
|
+
- **Provider switch isolation.** Switching an agent from a local endpoint back to a fixed cloud provider clears stale per-agent endpoints and fallback keys.
|
|
428
|
+
- **Manual model flow.** Provider model saves now preserve explicit empty endpoint resets and optional-key providers can be tested without creating a credential.
|
|
429
|
+
|
|
410
430
|
### v1.9.19 Highlights
|
|
411
431
|
|
|
412
432
|
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.21",
|
|
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",
|
|
@@ -85,10 +85,10 @@
|
|
|
85
85
|
"lint:baseline": "node ./scripts/lint-baseline.mjs check",
|
|
86
86
|
"lint:baseline:update": "node ./scripts/lint-baseline.mjs update",
|
|
87
87
|
"cli": "node ./bin/swarmclaw.js",
|
|
88
|
-
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
88
|
+
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/electron-signing-config.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
89
89
|
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
|
|
90
90
|
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
91
|
-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
91
|
+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
92
92
|
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
|
|
93
93
|
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
|
|
94
94
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
|
|
|
2
2
|
import test from 'node:test'
|
|
3
3
|
|
|
4
4
|
import { normalizeOllamaSetupEndpoint, normalizeOpenClawUrl, parseErrorMessage } from './helpers'
|
|
5
|
+
import { POST } from './route'
|
|
5
6
|
|
|
6
7
|
test('normalizeOllamaSetupEndpoint strips local /v1 suffixes but preserves cloud endpoints', () => {
|
|
7
8
|
assert.equal(
|
|
@@ -117,3 +118,46 @@ test('normalizeOpenClawUrl handles bare IP:port', () => {
|
|
|
117
118
|
assert.equal(httpUrl, 'http://10.0.0.5:18789')
|
|
118
119
|
assert.equal(wsUrl, 'ws://10.0.0.5:18789')
|
|
119
120
|
})
|
|
121
|
+
|
|
122
|
+
test('POST returns provider diagnostics with normalized LM Studio targets and redacted errors', async () => {
|
|
123
|
+
const originalFetch = globalThis.fetch
|
|
124
|
+
const calls: string[] = []
|
|
125
|
+
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
|
126
|
+
const url = String(input)
|
|
127
|
+
calls.push(url)
|
|
128
|
+
if (url.endsWith('/models')) {
|
|
129
|
+
return new Response(JSON.stringify({ data: [{ id: 'google/gemma-4-e4b' }] }), { status: 200 })
|
|
130
|
+
}
|
|
131
|
+
return new Response(
|
|
132
|
+
JSON.stringify({ error: { message: 'Malformed token sk-local-secret provided.' } }),
|
|
133
|
+
{ status: 400 },
|
|
134
|
+
)
|
|
135
|
+
}) as typeof fetch
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const res = await POST(new Request('http://localhost/api/setup/check-provider', {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
provider: 'lmstudio',
|
|
142
|
+
endpoint: 'http://10.2.0.2:1234',
|
|
143
|
+
}),
|
|
144
|
+
}))
|
|
145
|
+
const payload = await res.json()
|
|
146
|
+
|
|
147
|
+
assert.equal(payload.ok, false)
|
|
148
|
+
assert.equal(payload.normalizedEndpoint, 'http://10.2.0.2:1234/v1')
|
|
149
|
+
assert.deepEqual(calls, [
|
|
150
|
+
'http://10.2.0.2:1234/v1/models',
|
|
151
|
+
'http://10.2.0.2:1234/v1/chat/completions',
|
|
152
|
+
])
|
|
153
|
+
assert.ok(Array.isArray(payload.diagnostics))
|
|
154
|
+
assert.equal(payload.diagnostics[0].target, 'http://10.2.0.2:1234/v1')
|
|
155
|
+
assert.equal(payload.message, 'Malformed token sk-... provided.')
|
|
156
|
+
assert.equal(
|
|
157
|
+
payload.diagnostics.some((step: { detail?: string }) => step.detail?.includes('sk-local-secret')),
|
|
158
|
+
false,
|
|
159
|
+
)
|
|
160
|
+
} finally {
|
|
161
|
+
globalThis.fetch = originalFetch
|
|
162
|
+
}
|
|
163
|
+
})
|
|
@@ -4,9 +4,12 @@ import { listCredentialIdsByProvider } from '@/lib/server/credentials/credential
|
|
|
4
4
|
import { getDeviceId, wsConnect, rpcOnConnectedGateway } from '@/lib/providers/openclaw'
|
|
5
5
|
import { isCliProviderId } from '@/lib/providers/cli-provider-metadata'
|
|
6
6
|
import { checkCliProviderReady } from '@/lib/server/cli-provider-readiness'
|
|
7
|
+
import { createProviderDiagnostics, sanitizeProviderDiagnosticText } from '@/lib/server/provider-diagnostics'
|
|
7
8
|
import { OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
|
|
9
|
+
import { normalizeLmStudioEndpoint, normalizeOpenAiCompatibleV1Endpoint } from '@/lib/providers/openai-compatible-endpoint'
|
|
8
10
|
import { resolveOllamaRuntimeConfig } from '@/lib/server/ollama-runtime'
|
|
9
11
|
import { normalizeOllamaSetupEndpoint, normalizeOpenClawUrl, parseErrorMessage } from './helpers'
|
|
12
|
+
import type { ProviderCheckResult } from '@/types/provider'
|
|
10
13
|
|
|
11
14
|
interface SetupCheckBody {
|
|
12
15
|
provider?: string
|
|
@@ -32,15 +35,21 @@ async function checkOpenAiCompatible(
|
|
|
32
35
|
endpointRaw: string,
|
|
33
36
|
defaultEndpoint: string,
|
|
34
37
|
modelHint?: string,
|
|
35
|
-
): Promise<
|
|
38
|
+
): Promise<ProviderCheckResult> {
|
|
39
|
+
const diagnostics = createProviderDiagnostics()
|
|
36
40
|
const normalizedEndpoint = (endpointRaw || defaultEndpoint).replace(/\/+$/, '')
|
|
41
|
+
diagnostics.pass('Endpoint resolved', { target: normalizedEndpoint })
|
|
37
42
|
const authHeaders = apiKey ? { authorization: `Bearer ${apiKey}` } : undefined
|
|
38
43
|
|
|
39
44
|
// First, discover a model to test with (prefer the hint, fall back to the first available model)
|
|
40
45
|
let testModel = modelHint || ''
|
|
41
|
-
if (
|
|
46
|
+
if (testModel) {
|
|
47
|
+
diagnostics.pass('Test model selected', { detail: testModel })
|
|
48
|
+
} else {
|
|
49
|
+
const modelsTarget = `${normalizedEndpoint}/models`
|
|
50
|
+
const startedAt = Date.now()
|
|
42
51
|
try {
|
|
43
|
-
const modelsRes = await fetch(
|
|
52
|
+
const modelsRes = await fetch(modelsTarget, {
|
|
44
53
|
headers: authHeaders,
|
|
45
54
|
signal: AbortSignal.timeout(8_000),
|
|
46
55
|
cache: 'no-store',
|
|
@@ -48,10 +57,33 @@ async function checkOpenAiCompatible(
|
|
|
48
57
|
if (modelsRes.ok) {
|
|
49
58
|
const modelsPayload = await modelsRes.json().catch(() => ({} as Record<string, unknown>))
|
|
50
59
|
const first = Array.isArray(modelsPayload?.data) ? modelsPayload.data[0] : null
|
|
51
|
-
if (first?.id)
|
|
60
|
+
if (first?.id) {
|
|
61
|
+
testModel = String(first.id)
|
|
62
|
+
diagnostics.pass('Model discovery completed', {
|
|
63
|
+
target: modelsTarget,
|
|
64
|
+
detail: `Using ${testModel}`,
|
|
65
|
+
durationMs: Date.now() - startedAt,
|
|
66
|
+
})
|
|
67
|
+
} else {
|
|
68
|
+
diagnostics.warn('Model discovery returned no models', {
|
|
69
|
+
target: modelsTarget,
|
|
70
|
+
durationMs: Date.now() - startedAt,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
const detail = sanitizeProviderDiagnosticText(await parseErrorMessage(modelsRes, `${providerName} models returned ${modelsRes.status}.`))
|
|
75
|
+
diagnostics.warn('Model discovery failed', {
|
|
76
|
+
target: modelsTarget,
|
|
77
|
+
detail: `HTTP ${modelsRes.status}: ${detail}`,
|
|
78
|
+
durationMs: Date.now() - startedAt,
|
|
79
|
+
})
|
|
52
80
|
}
|
|
53
|
-
} catch {
|
|
54
|
-
|
|
81
|
+
} catch (err: unknown) {
|
|
82
|
+
diagnostics.warn('Model discovery request failed', {
|
|
83
|
+
target: modelsTarget,
|
|
84
|
+
detail: err instanceof Error && err.message ? err.message : 'Unable to query models.',
|
|
85
|
+
durationMs: Date.now() - startedAt,
|
|
86
|
+
})
|
|
55
87
|
}
|
|
56
88
|
}
|
|
57
89
|
|
|
@@ -70,57 +102,108 @@ async function checkOpenAiCompatible(
|
|
|
70
102
|
DeepInfra: 'deepseek-ai/DeepSeek-R1-0528',
|
|
71
103
|
OpenRouter: 'openai/gpt-4.1-mini',
|
|
72
104
|
'Hermes Agent': 'hermes-agent',
|
|
105
|
+
'LM Studio': 'local-model',
|
|
73
106
|
}
|
|
74
107
|
testModel = fallbacks[providerName] || 'gpt-4o-mini'
|
|
108
|
+
diagnostics.warn('Fallback test model selected', { detail: testModel })
|
|
75
109
|
}
|
|
76
110
|
|
|
77
111
|
// Test the chat completions endpoint with a minimal request
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
112
|
+
const chatTarget = `${normalizedEndpoint}/chat/completions`
|
|
113
|
+
const chatStartedAt = Date.now()
|
|
114
|
+
let res: Response
|
|
115
|
+
try {
|
|
116
|
+
res = await fetch(chatTarget, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: {
|
|
119
|
+
'content-type': 'application/json',
|
|
120
|
+
...(authHeaders || {}),
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
model: testModel,
|
|
124
|
+
max_completion_tokens: 8,
|
|
125
|
+
messages: [{ role: 'user', content: 'Reply OK' }],
|
|
126
|
+
}),
|
|
127
|
+
signal: AbortSignal.timeout(15_000),
|
|
128
|
+
cache: 'no-store',
|
|
129
|
+
})
|
|
130
|
+
} catch (err: unknown) {
|
|
131
|
+
const message = err instanceof Error && err.name === 'TimeoutError'
|
|
132
|
+
? 'Connection check timed out. Verify endpoint/network and try again.'
|
|
133
|
+
: (err instanceof Error && err.message ? err.message : 'Chat endpoint request failed.')
|
|
134
|
+
diagnostics.fail('Chat completion request failed', {
|
|
135
|
+
target: chatTarget,
|
|
136
|
+
detail: message,
|
|
137
|
+
durationMs: Date.now() - chatStartedAt,
|
|
138
|
+
})
|
|
139
|
+
return { ok: false, message: sanitizeProviderDiagnosticText(message), normalizedEndpoint, diagnostics: diagnostics.toJSON() }
|
|
140
|
+
}
|
|
92
141
|
if (!res.ok) {
|
|
93
|
-
const detail = await parseErrorMessage(res, `${providerName} returned ${res.status}.`)
|
|
94
|
-
|
|
142
|
+
const detail = sanitizeProviderDiagnosticText(await parseErrorMessage(res, `${providerName} returned ${res.status}.`))
|
|
143
|
+
diagnostics.fail('Chat completion check failed', {
|
|
144
|
+
target: chatTarget,
|
|
145
|
+
detail: `HTTP ${res.status}: ${detail}`,
|
|
146
|
+
durationMs: Date.now() - chatStartedAt,
|
|
147
|
+
})
|
|
148
|
+
return { ok: false, message: detail, normalizedEndpoint, diagnostics: diagnostics.toJSON() }
|
|
95
149
|
}
|
|
150
|
+
diagnostics.pass('Chat completion check passed', {
|
|
151
|
+
target: chatTarget,
|
|
152
|
+
detail: `Verified with ${testModel}`,
|
|
153
|
+
durationMs: Date.now() - chatStartedAt,
|
|
154
|
+
})
|
|
96
155
|
return {
|
|
97
156
|
ok: true,
|
|
98
157
|
message: `Connected to ${providerName}. Chat endpoint verified with ${testModel}.`,
|
|
99
158
|
normalizedEndpoint,
|
|
159
|
+
diagnostics: diagnostics.toJSON(),
|
|
100
160
|
}
|
|
101
161
|
}
|
|
102
162
|
|
|
103
|
-
async function checkAnthropic(apiKey: string, endpointRaw: string, modelRaw: string): Promise<
|
|
163
|
+
async function checkAnthropic(apiKey: string, endpointRaw: string, modelRaw: string): Promise<ProviderCheckResult> {
|
|
164
|
+
const diagnostics = createProviderDiagnostics()
|
|
104
165
|
const model = modelRaw || 'claude-sonnet-4-6'
|
|
105
166
|
const baseUrl = (endpointRaw || 'https://api.anthropic.com').replace(/\/+$/, '')
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
167
|
+
diagnostics.pass('Endpoint resolved', { target: baseUrl })
|
|
168
|
+
diagnostics.pass('Test model selected', { detail: model })
|
|
169
|
+
const target = `${baseUrl}/v1/messages`
|
|
170
|
+
const startedAt = Date.now()
|
|
171
|
+
let res: Response
|
|
172
|
+
try {
|
|
173
|
+
res = await fetch(target, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: {
|
|
176
|
+
'x-api-key': apiKey,
|
|
177
|
+
'anthropic-version': '2023-06-01',
|
|
178
|
+
'content-type': 'application/json',
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
model,
|
|
182
|
+
max_tokens: 12,
|
|
183
|
+
messages: [{ role: 'user', content: 'Reply with ANTHROPIC_SETUP_OK' }],
|
|
184
|
+
}),
|
|
185
|
+
signal: AbortSignal.timeout(15_000),
|
|
186
|
+
cache: 'no-store',
|
|
187
|
+
})
|
|
188
|
+
} catch (err: unknown) {
|
|
189
|
+
const message = err instanceof Error && err.name === 'TimeoutError'
|
|
190
|
+
? 'Connection check timed out. Verify endpoint/network and try again.'
|
|
191
|
+
: (err instanceof Error && err.message ? err.message : 'Anthropic request failed.')
|
|
192
|
+
diagnostics.fail('Message check request failed', {
|
|
193
|
+
target,
|
|
194
|
+
detail: message,
|
|
195
|
+
durationMs: Date.now() - startedAt,
|
|
196
|
+
})
|
|
197
|
+
return { ok: false, message: sanitizeProviderDiagnosticText(message), diagnostics: diagnostics.toJSON() }
|
|
198
|
+
}
|
|
121
199
|
if (!res.ok) {
|
|
122
|
-
const detail = await parseErrorMessage(res, `Anthropic returned ${res.status}.`)
|
|
123
|
-
|
|
200
|
+
const detail = sanitizeProviderDiagnosticText(await parseErrorMessage(res, `Anthropic returned ${res.status}.`))
|
|
201
|
+
diagnostics.fail('Message check failed', {
|
|
202
|
+
target,
|
|
203
|
+
detail: `HTTP ${res.status}: ${detail}`,
|
|
204
|
+
durationMs: Date.now() - startedAt,
|
|
205
|
+
})
|
|
206
|
+
return { ok: false, message: detail, diagnostics: diagnostics.toJSON() }
|
|
124
207
|
}
|
|
125
208
|
const payload = await res.json().catch(() => ({} as Record<string, unknown>))
|
|
126
209
|
const content = Array.isArray(payload.content) ? payload.content : []
|
|
@@ -128,7 +211,12 @@ async function checkAnthropic(apiKey: string, endpointRaw: string, modelRaw: str
|
|
|
128
211
|
const text = firstContent && typeof firstContent === 'object' && 'text' in firstContent && typeof firstContent.text === 'string'
|
|
129
212
|
? firstContent.text
|
|
130
213
|
: ''
|
|
131
|
-
|
|
214
|
+
diagnostics.pass('Message check passed', {
|
|
215
|
+
target,
|
|
216
|
+
detail: text ? `Sample: ${text.slice(0, 80)}` : 'Provider returned a successful response.',
|
|
217
|
+
durationMs: Date.now() - startedAt,
|
|
218
|
+
})
|
|
219
|
+
return { ok: true, message: text ? `Connected to Anthropic. Sample: ${text.slice(0, 120)}` : 'Connected to Anthropic.', diagnostics: diagnostics.toJSON() }
|
|
132
220
|
}
|
|
133
221
|
|
|
134
222
|
async function checkOllama(params: {
|
|
@@ -136,7 +224,8 @@ async function checkOllama(params: {
|
|
|
136
224
|
modelRaw: string
|
|
137
225
|
ollamaMode?: string
|
|
138
226
|
apiKey?: string
|
|
139
|
-
}): Promise<
|
|
227
|
+
}): Promise<ProviderCheckResult> {
|
|
228
|
+
const diagnostics = createProviderDiagnostics()
|
|
140
229
|
const runtime = resolveOllamaRuntimeConfig({
|
|
141
230
|
model: params.modelRaw,
|
|
142
231
|
ollamaMode: params.ollamaMode ?? null,
|
|
@@ -144,22 +233,32 @@ async function checkOllama(params: {
|
|
|
144
233
|
apiEndpoint: params.endpointRaw,
|
|
145
234
|
})
|
|
146
235
|
const normalizedEndpoint = normalizeOllamaSetupEndpoint(runtime.endpoint, runtime.useCloud)
|
|
236
|
+
diagnostics.pass('Endpoint resolved', {
|
|
237
|
+
target: normalizedEndpoint,
|
|
238
|
+
detail: runtime.useCloud ? 'Ollama Cloud mode' : 'Local Ollama mode',
|
|
239
|
+
})
|
|
147
240
|
const headers: Record<string, string> = runtime.apiKey ? { authorization: `Bearer ${runtime.apiKey}` } : {}
|
|
148
241
|
if (runtime.useCloud && !runtime.apiKey) {
|
|
242
|
+
diagnostics.fail('Credential required', { detail: 'Ollama Cloud requires an API key.' })
|
|
149
243
|
return {
|
|
150
244
|
ok: false,
|
|
151
245
|
message: 'Ollama Cloud model requires an API key. Set OLLAMA_API_KEY or attach an Ollama credential.',
|
|
152
246
|
normalizedEndpoint,
|
|
247
|
+
diagnostics: diagnostics.toJSON(),
|
|
153
248
|
}
|
|
154
249
|
}
|
|
155
250
|
|
|
156
251
|
// Discover a model to test with
|
|
157
252
|
let testModel = params.modelRaw || ''
|
|
158
253
|
let recommendedModel: string | undefined
|
|
159
|
-
if (
|
|
254
|
+
if (testModel) {
|
|
255
|
+
diagnostics.pass('Test model selected', { detail: testModel })
|
|
256
|
+
} else {
|
|
257
|
+
const tagsPath = runtime.useCloud ? '/v1/models' : '/api/tags'
|
|
258
|
+
const target = `${normalizedEndpoint}${tagsPath}`
|
|
259
|
+
const startedAt = Date.now()
|
|
160
260
|
try {
|
|
161
|
-
const
|
|
162
|
-
const res = await fetch(`${normalizedEndpoint}${tagsPath}`, {
|
|
261
|
+
const res = await fetch(target, {
|
|
163
262
|
headers: headers.authorization ? headers : undefined,
|
|
164
263
|
signal: AbortSignal.timeout(8_000),
|
|
165
264
|
cache: 'no-store',
|
|
@@ -175,63 +274,126 @@ async function checkOllama(params: {
|
|
|
175
274
|
if (firstModel) {
|
|
176
275
|
testModel = firstModel
|
|
177
276
|
recommendedModel = firstModel
|
|
277
|
+
diagnostics.pass('Model discovery completed', {
|
|
278
|
+
target,
|
|
279
|
+
detail: `Using ${firstModel}`,
|
|
280
|
+
durationMs: Date.now() - startedAt,
|
|
281
|
+
})
|
|
178
282
|
}
|
|
179
283
|
if (models.length === 0) {
|
|
284
|
+
diagnostics.warn('Model discovery returned no models', {
|
|
285
|
+
target,
|
|
286
|
+
durationMs: Date.now() - startedAt,
|
|
287
|
+
})
|
|
180
288
|
return {
|
|
181
289
|
ok: true,
|
|
182
290
|
message: runtime.useCloud
|
|
183
291
|
? 'Connected to Ollama Cloud, but no models were returned.'
|
|
184
292
|
: 'Connected to Ollama, but no models are installed yet. Run `ollama pull <model>` to add one.',
|
|
185
293
|
normalizedEndpoint,
|
|
294
|
+
diagnostics: diagnostics.toJSON(),
|
|
186
295
|
}
|
|
187
296
|
}
|
|
297
|
+
} else {
|
|
298
|
+
const detail = sanitizeProviderDiagnosticText(await parseErrorMessage(res, `Ollama model discovery returned ${res.status}.`))
|
|
299
|
+
diagnostics.warn('Model discovery failed', {
|
|
300
|
+
target,
|
|
301
|
+
detail: `HTTP ${res.status}: ${detail}`,
|
|
302
|
+
durationMs: Date.now() - startedAt,
|
|
303
|
+
})
|
|
188
304
|
}
|
|
189
|
-
} catch {
|
|
190
|
-
|
|
305
|
+
} catch (err: unknown) {
|
|
306
|
+
diagnostics.warn('Model discovery request failed', {
|
|
307
|
+
target,
|
|
308
|
+
detail: err instanceof Error && err.message ? err.message : 'Unable to query models.',
|
|
309
|
+
durationMs: Date.now() - startedAt,
|
|
310
|
+
})
|
|
191
311
|
}
|
|
192
312
|
}
|
|
193
313
|
|
|
194
|
-
if (!testModel)
|
|
314
|
+
if (!testModel) {
|
|
315
|
+
testModel = 'llama3.2'
|
|
316
|
+
diagnostics.warn('Fallback test model selected', { detail: testModel })
|
|
317
|
+
}
|
|
195
318
|
|
|
196
319
|
// Test the chat endpoint
|
|
197
320
|
const label = runtime.useCloud ? 'Ollama Cloud' : 'Ollama'
|
|
198
321
|
const chatEndpoint = `${normalizedEndpoint}/v1/chat/completions`
|
|
199
322
|
const chatBody = JSON.stringify({ model: testModel, max_completion_tokens: 8, messages: [{ role: 'user', content: 'Reply OK' }] })
|
|
200
323
|
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
324
|
+
const chatStartedAt = Date.now()
|
|
325
|
+
let chatRes: Response
|
|
326
|
+
try {
|
|
327
|
+
chatRes = await fetch(chatEndpoint, {
|
|
328
|
+
method: 'POST',
|
|
329
|
+
headers: { ...headers, 'content-type': 'application/json' },
|
|
330
|
+
body: chatBody,
|
|
331
|
+
signal: AbortSignal.timeout(30_000),
|
|
332
|
+
cache: 'no-store',
|
|
333
|
+
})
|
|
334
|
+
} catch (err: unknown) {
|
|
335
|
+
const message = err instanceof Error && err.name === 'TimeoutError'
|
|
336
|
+
? 'Connection check timed out. Verify endpoint/network and try again.'
|
|
337
|
+
: (err instanceof Error && err.message ? err.message : 'Ollama chat request failed.')
|
|
338
|
+
diagnostics.fail('Chat completion request failed', {
|
|
339
|
+
target: chatEndpoint,
|
|
340
|
+
detail: message,
|
|
341
|
+
durationMs: Date.now() - chatStartedAt,
|
|
342
|
+
})
|
|
343
|
+
return { ok: false, message: sanitizeProviderDiagnosticText(message), normalizedEndpoint, recommendedModel, diagnostics: diagnostics.toJSON() }
|
|
344
|
+
}
|
|
208
345
|
if (!chatRes.ok) {
|
|
209
|
-
const detail = await parseErrorMessage(chatRes, `${label} chat returned ${chatRes.status}.`)
|
|
210
|
-
|
|
346
|
+
const detail = sanitizeProviderDiagnosticText(await parseErrorMessage(chatRes, `${label} chat returned ${chatRes.status}.`))
|
|
347
|
+
diagnostics.fail('Chat completion check failed', {
|
|
348
|
+
target: chatEndpoint,
|
|
349
|
+
detail: `HTTP ${chatRes.status}: ${detail}`,
|
|
350
|
+
durationMs: Date.now() - chatStartedAt,
|
|
351
|
+
})
|
|
352
|
+
return { ok: false, message: detail, normalizedEndpoint, recommendedModel, diagnostics: diagnostics.toJSON() }
|
|
211
353
|
}
|
|
354
|
+
diagnostics.pass('Chat completion check passed', {
|
|
355
|
+
target: chatEndpoint,
|
|
356
|
+
detail: `Verified with ${testModel}`,
|
|
357
|
+
durationMs: Date.now() - chatStartedAt,
|
|
358
|
+
})
|
|
212
359
|
return {
|
|
213
360
|
ok: true,
|
|
214
361
|
message: `Connected to ${label}. Chat verified with ${testModel}.`,
|
|
215
362
|
normalizedEndpoint,
|
|
216
363
|
recommendedModel: recommendedModel || testModel,
|
|
364
|
+
diagnostics: diagnostics.toJSON(),
|
|
217
365
|
}
|
|
218
366
|
}
|
|
219
367
|
|
|
220
|
-
async function checkOpenClaw(apiKey: string, endpointRaw: string): Promise<
|
|
368
|
+
async function checkOpenClaw(apiKey: string, endpointRaw: string): Promise<ProviderCheckResult> {
|
|
369
|
+
const diagnostics = createProviderDiagnostics()
|
|
221
370
|
const { httpUrl: normalizedEndpoint, wsUrl } = normalizeOpenClawUrl(endpointRaw)
|
|
222
371
|
const token = apiKey || undefined
|
|
223
372
|
const deviceId = getDeviceId()
|
|
373
|
+
diagnostics.pass('Endpoint resolved', { target: normalizedEndpoint })
|
|
224
374
|
|
|
375
|
+
const wsStartedAt = Date.now()
|
|
225
376
|
const result = await wsConnect(wsUrl, token, true, 10_000)
|
|
226
377
|
|
|
227
378
|
if (!result.ok) {
|
|
228
379
|
if (result.ws) try { result.ws.close() } catch {}
|
|
229
|
-
|
|
380
|
+
diagnostics.fail('Gateway websocket check failed', {
|
|
381
|
+
target: wsUrl,
|
|
382
|
+
detail: result.message,
|
|
383
|
+
durationMs: Date.now() - wsStartedAt,
|
|
384
|
+
})
|
|
385
|
+
return { ok: false, message: sanitizeProviderDiagnosticText(result.message), normalizedEndpoint, deviceId, errorCode: result.errorCode, diagnostics: diagnostics.toJSON() }
|
|
230
386
|
}
|
|
387
|
+
diagnostics.pass('Gateway websocket check passed', {
|
|
388
|
+
target: wsUrl,
|
|
389
|
+
detail: deviceId ? `Device ${deviceId}` : undefined,
|
|
390
|
+
durationMs: Date.now() - wsStartedAt,
|
|
391
|
+
})
|
|
231
392
|
|
|
232
393
|
// Attempt model discovery via RPC before closing the connection
|
|
233
394
|
let recommendedModel: string | undefined
|
|
234
395
|
if (result.ws) {
|
|
396
|
+
const modelStartedAt = Date.now()
|
|
235
397
|
try {
|
|
236
398
|
const payload = await rpcOnConnectedGateway(result.ws, 'models.list', {}, 8_000) as Record<string, unknown> | unknown[] | undefined
|
|
237
399
|
const p = payload as Record<string, unknown> | undefined
|
|
@@ -244,13 +406,20 @@ async function checkOpenClaw(apiKey: string, endpointRaw: string): Promise<{ ok:
|
|
|
244
406
|
} else if (typeof first?.name === 'string') {
|
|
245
407
|
recommendedModel = first.name
|
|
246
408
|
}
|
|
247
|
-
|
|
248
|
-
|
|
409
|
+
diagnostics.pass('Gateway model discovery completed', {
|
|
410
|
+
detail: recommendedModel ? `Using ${recommendedModel}` : 'No model recommendation returned.',
|
|
411
|
+
durationMs: Date.now() - modelStartedAt,
|
|
412
|
+
})
|
|
413
|
+
} catch (err: unknown) {
|
|
414
|
+
diagnostics.warn('Gateway model discovery failed', {
|
|
415
|
+
detail: err instanceof Error && err.message ? err.message : 'Model discovery is unavailable.',
|
|
416
|
+
durationMs: Date.now() - modelStartedAt,
|
|
417
|
+
})
|
|
249
418
|
}
|
|
250
419
|
try { result.ws.close() } catch {}
|
|
251
420
|
}
|
|
252
421
|
|
|
253
|
-
return { ok: true, message: 'Connected to OpenClaw gateway.', normalizedEndpoint, deviceId, recommendedModel }
|
|
422
|
+
return { ok: true, message: 'Connected to OpenClaw gateway.', normalizedEndpoint, deviceId, recommendedModel, diagnostics: diagnostics.toJSON() }
|
|
254
423
|
}
|
|
255
424
|
|
|
256
425
|
export async function POST(req: Request) {
|
|
@@ -300,7 +469,12 @@ export async function POST(req: Request) {
|
|
|
300
469
|
|
|
301
470
|
if (isCliProviderId(provider)) {
|
|
302
471
|
const result = checkCliProviderReady(provider)
|
|
303
|
-
|
|
472
|
+
const diagnostics = createProviderDiagnostics()
|
|
473
|
+
diagnostics.add('CLI readiness check', result.ok ? 'pass' : 'fail', {
|
|
474
|
+
detail: result.message,
|
|
475
|
+
target: result.binaryPath || result.binaryName || provider,
|
|
476
|
+
})
|
|
477
|
+
return NextResponse.json({ ...result, diagnostics: diagnostics.toJSON() })
|
|
304
478
|
}
|
|
305
479
|
|
|
306
480
|
if (!provider) {
|
|
@@ -312,7 +486,13 @@ export async function POST(req: Request) {
|
|
|
312
486
|
case 'openai': {
|
|
313
487
|
if (!apiKey) return NextResponse.json({ ok: false, message: 'OpenAI API key is required.' })
|
|
314
488
|
const info = OPENAI_COMPATIBLE_DEFAULTS.openai
|
|
315
|
-
const result = await checkOpenAiCompatible(
|
|
489
|
+
const result = await checkOpenAiCompatible(
|
|
490
|
+
info.name,
|
|
491
|
+
apiKey,
|
|
492
|
+
normalizeOpenAiCompatibleV1Endpoint(endpoint || info.defaultEndpoint, info.defaultEndpoint),
|
|
493
|
+
info.defaultEndpoint,
|
|
494
|
+
model,
|
|
495
|
+
)
|
|
316
496
|
return NextResponse.json(result)
|
|
317
497
|
}
|
|
318
498
|
case 'openrouter': {
|
|
@@ -345,6 +525,17 @@ export async function POST(req: Request) {
|
|
|
345
525
|
const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint, model)
|
|
346
526
|
return NextResponse.json(result)
|
|
347
527
|
}
|
|
528
|
+
case 'lmstudio': {
|
|
529
|
+
const info = OPENAI_COMPATIBLE_DEFAULTS.lmstudio
|
|
530
|
+
const result = await checkOpenAiCompatible(
|
|
531
|
+
info.name,
|
|
532
|
+
apiKey,
|
|
533
|
+
normalizeLmStudioEndpoint(endpoint || info.defaultEndpoint),
|
|
534
|
+
info.defaultEndpoint,
|
|
535
|
+
model,
|
|
536
|
+
)
|
|
537
|
+
return NextResponse.json(result)
|
|
538
|
+
}
|
|
348
539
|
case 'ollama': {
|
|
349
540
|
const result = await checkOllama({
|
|
350
541
|
endpointRaw: endpoint,
|