@swarmclawai/swarmclaw 1.9.20 → 1.9.22
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 +22 -2
- 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 +235 -63
- package/src/components/agents/agent-sheet.tsx +18 -5
- package/src/components/auth/setup-wizard/step-connect.tsx +13 -3
- package/src/components/auth/setup-wizard/types.ts +2 -9
- package/src/components/chat/activity-moment.tsx +4 -0
- package/src/components/chat/tool-call-bubble.tsx +6 -0
- package/src/components/providers/provider-diagnostics-list.tsx +58 -0
- package/src/components/providers/provider-sheet.tsx +11 -1
- package/src/features/providers/queries.ts +2 -1
- package/src/lib/server/capability-router.test.ts +4 -4
- package/src/lib/server/capability-router.ts +1 -0
- package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +27 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +21 -0
- package/src/lib/server/chat-execution/iteration-event-handler.ts +1 -1
- package/src/lib/server/chat-execution/stream-continuation.ts +6 -2
- package/src/lib/server/plugins-advanced.test.ts +7 -3
- package/src/lib/server/provider-diagnostics.test.ts +39 -0
- package/src/lib/server/provider-diagnostics.ts +114 -0
- package/src/lib/server/session-tools/web-crawl.test.ts +106 -0
- package/src/lib/server/session-tools/web-inputs.test.ts +5 -0
- package/src/lib/server/session-tools/web-utils.ts +8 -2
- package/src/lib/server/session-tools/web.ts +256 -29
- package/src/lib/server/storage.ts +2 -0
- package/src/lib/server/tool-aliases.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +3 -3
- package/src/lib/server/tool-capability-policy.ts +4 -1
- package/src/lib/server/tool-planning.test.ts +2 -1
- package/src/lib/server/tool-planning.ts +31 -0
- package/src/lib/server/untrusted-content.ts +2 -2
- package/src/types/provider.ts +21 -0
- package/src/types/session.ts +2 -0
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
|
```
|
|
@@ -407,6 +409,24 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
407
409
|
|
|
408
410
|
## Releases
|
|
409
411
|
|
|
412
|
+
### v1.9.22 Highlights
|
|
413
|
+
|
|
414
|
+
Research tools release: agents now get direct `web_extract` and `web_crawl` tools alongside `web_search`, `web_fetch`, and the unified `web` tool.
|
|
415
|
+
|
|
416
|
+
- **Source-grounded extraction.** `web_extract` returns a page title, canonical URL, and readable content for known source URLs.
|
|
417
|
+
- **Bounded crawls.** `web_crawl` walks same-origin links by default with conservative page and depth caps, plus an explicit external-link opt-in.
|
|
418
|
+
- **Better routing.** Tool aliases, capability policy, planning hints, continuation recovery, and the chat UI all recognize the granular research tools.
|
|
419
|
+
- **Regression coverage.** New tests cover action inference, tool-call translation, direct tool registration, extraction cleanup, and same-origin crawl bounds.
|
|
420
|
+
|
|
421
|
+
### v1.9.21 Highlights
|
|
422
|
+
|
|
423
|
+
Provider diagnostics release: connection checks now return a structured step timeline across setup, provider settings, and agent editing.
|
|
424
|
+
|
|
425
|
+
- **Connection timelines.** Provider checks show endpoint resolution, model discovery, fallback selection, and chat/gateway verification steps.
|
|
426
|
+
- **Safer error details.** Token-like values are redacted before check messages or diagnostics are returned to the UI.
|
|
427
|
+
- **Local runtime debugging.** LM Studio, Ollama, custom OpenAI-compatible endpoints, cloud providers, OpenClaw gateways, and CLI providers all report concise pass/fail diagnostics.
|
|
428
|
+
- **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.
|
|
429
|
+
|
|
410
430
|
### v1.9.20 Highlights
|
|
411
431
|
|
|
412
432
|
Provider reliability release: local OpenAI-compatible runtimes now get safer endpoint handling, clearer setup, and first-class LM Studio support.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.22",
|
|
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/server/session-tools/web-crawl.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,10 +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'
|
|
8
9
|
import { normalizeLmStudioEndpoint, normalizeOpenAiCompatibleV1Endpoint } from '@/lib/providers/openai-compatible-endpoint'
|
|
9
10
|
import { resolveOllamaRuntimeConfig } from '@/lib/server/ollama-runtime'
|
|
10
11
|
import { normalizeOllamaSetupEndpoint, normalizeOpenClawUrl, parseErrorMessage } from './helpers'
|
|
12
|
+
import type { ProviderCheckResult } from '@/types/provider'
|
|
11
13
|
|
|
12
14
|
interface SetupCheckBody {
|
|
13
15
|
provider?: string
|
|
@@ -33,15 +35,21 @@ async function checkOpenAiCompatible(
|
|
|
33
35
|
endpointRaw: string,
|
|
34
36
|
defaultEndpoint: string,
|
|
35
37
|
modelHint?: string,
|
|
36
|
-
): Promise<
|
|
38
|
+
): Promise<ProviderCheckResult> {
|
|
39
|
+
const diagnostics = createProviderDiagnostics()
|
|
37
40
|
const normalizedEndpoint = (endpointRaw || defaultEndpoint).replace(/\/+$/, '')
|
|
41
|
+
diagnostics.pass('Endpoint resolved', { target: normalizedEndpoint })
|
|
38
42
|
const authHeaders = apiKey ? { authorization: `Bearer ${apiKey}` } : undefined
|
|
39
43
|
|
|
40
44
|
// First, discover a model to test with (prefer the hint, fall back to the first available model)
|
|
41
45
|
let testModel = modelHint || ''
|
|
42
|
-
if (
|
|
46
|
+
if (testModel) {
|
|
47
|
+
diagnostics.pass('Test model selected', { detail: testModel })
|
|
48
|
+
} else {
|
|
49
|
+
const modelsTarget = `${normalizedEndpoint}/models`
|
|
50
|
+
const startedAt = Date.now()
|
|
43
51
|
try {
|
|
44
|
-
const modelsRes = await fetch(
|
|
52
|
+
const modelsRes = await fetch(modelsTarget, {
|
|
45
53
|
headers: authHeaders,
|
|
46
54
|
signal: AbortSignal.timeout(8_000),
|
|
47
55
|
cache: 'no-store',
|
|
@@ -49,10 +57,33 @@ async function checkOpenAiCompatible(
|
|
|
49
57
|
if (modelsRes.ok) {
|
|
50
58
|
const modelsPayload = await modelsRes.json().catch(() => ({} as Record<string, unknown>))
|
|
51
59
|
const first = Array.isArray(modelsPayload?.data) ? modelsPayload.data[0] : null
|
|
52
|
-
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
|
+
})
|
|
53
80
|
}
|
|
54
|
-
} catch {
|
|
55
|
-
|
|
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
|
+
})
|
|
56
87
|
}
|
|
57
88
|
}
|
|
58
89
|
|
|
@@ -74,55 +105,105 @@ async function checkOpenAiCompatible(
|
|
|
74
105
|
'LM Studio': 'local-model',
|
|
75
106
|
}
|
|
76
107
|
testModel = fallbacks[providerName] || 'gpt-4o-mini'
|
|
108
|
+
diagnostics.warn('Fallback test model selected', { detail: testModel })
|
|
77
109
|
}
|
|
78
110
|
|
|
79
111
|
// Test the chat completions endpoint with a minimal request
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
}
|
|
94
141
|
if (!res.ok) {
|
|
95
|
-
const detail = await parseErrorMessage(res, `${providerName} returned ${res.status}.`)
|
|
96
|
-
|
|
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() }
|
|
97
149
|
}
|
|
150
|
+
diagnostics.pass('Chat completion check passed', {
|
|
151
|
+
target: chatTarget,
|
|
152
|
+
detail: `Verified with ${testModel}`,
|
|
153
|
+
durationMs: Date.now() - chatStartedAt,
|
|
154
|
+
})
|
|
98
155
|
return {
|
|
99
156
|
ok: true,
|
|
100
157
|
message: `Connected to ${providerName}. Chat endpoint verified with ${testModel}.`,
|
|
101
158
|
normalizedEndpoint,
|
|
159
|
+
diagnostics: diagnostics.toJSON(),
|
|
102
160
|
}
|
|
103
161
|
}
|
|
104
162
|
|
|
105
|
-
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()
|
|
106
165
|
const model = modelRaw || 'claude-sonnet-4-6'
|
|
107
166
|
const baseUrl = (endpointRaw || 'https://api.anthropic.com').replace(/\/+$/, '')
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
}
|
|
123
199
|
if (!res.ok) {
|
|
124
|
-
const detail = await parseErrorMessage(res, `Anthropic returned ${res.status}.`)
|
|
125
|
-
|
|
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() }
|
|
126
207
|
}
|
|
127
208
|
const payload = await res.json().catch(() => ({} as Record<string, unknown>))
|
|
128
209
|
const content = Array.isArray(payload.content) ? payload.content : []
|
|
@@ -130,7 +211,12 @@ async function checkAnthropic(apiKey: string, endpointRaw: string, modelRaw: str
|
|
|
130
211
|
const text = firstContent && typeof firstContent === 'object' && 'text' in firstContent && typeof firstContent.text === 'string'
|
|
131
212
|
? firstContent.text
|
|
132
213
|
: ''
|
|
133
|
-
|
|
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() }
|
|
134
220
|
}
|
|
135
221
|
|
|
136
222
|
async function checkOllama(params: {
|
|
@@ -138,7 +224,8 @@ async function checkOllama(params: {
|
|
|
138
224
|
modelRaw: string
|
|
139
225
|
ollamaMode?: string
|
|
140
226
|
apiKey?: string
|
|
141
|
-
}): Promise<
|
|
227
|
+
}): Promise<ProviderCheckResult> {
|
|
228
|
+
const diagnostics = createProviderDiagnostics()
|
|
142
229
|
const runtime = resolveOllamaRuntimeConfig({
|
|
143
230
|
model: params.modelRaw,
|
|
144
231
|
ollamaMode: params.ollamaMode ?? null,
|
|
@@ -146,22 +233,32 @@ async function checkOllama(params: {
|
|
|
146
233
|
apiEndpoint: params.endpointRaw,
|
|
147
234
|
})
|
|
148
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
|
+
})
|
|
149
240
|
const headers: Record<string, string> = runtime.apiKey ? { authorization: `Bearer ${runtime.apiKey}` } : {}
|
|
150
241
|
if (runtime.useCloud && !runtime.apiKey) {
|
|
242
|
+
diagnostics.fail('Credential required', { detail: 'Ollama Cloud requires an API key.' })
|
|
151
243
|
return {
|
|
152
244
|
ok: false,
|
|
153
245
|
message: 'Ollama Cloud model requires an API key. Set OLLAMA_API_KEY or attach an Ollama credential.',
|
|
154
246
|
normalizedEndpoint,
|
|
247
|
+
diagnostics: diagnostics.toJSON(),
|
|
155
248
|
}
|
|
156
249
|
}
|
|
157
250
|
|
|
158
251
|
// Discover a model to test with
|
|
159
252
|
let testModel = params.modelRaw || ''
|
|
160
253
|
let recommendedModel: string | undefined
|
|
161
|
-
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()
|
|
162
260
|
try {
|
|
163
|
-
const
|
|
164
|
-
const res = await fetch(`${normalizedEndpoint}${tagsPath}`, {
|
|
261
|
+
const res = await fetch(target, {
|
|
165
262
|
headers: headers.authorization ? headers : undefined,
|
|
166
263
|
signal: AbortSignal.timeout(8_000),
|
|
167
264
|
cache: 'no-store',
|
|
@@ -177,63 +274,126 @@ async function checkOllama(params: {
|
|
|
177
274
|
if (firstModel) {
|
|
178
275
|
testModel = firstModel
|
|
179
276
|
recommendedModel = firstModel
|
|
277
|
+
diagnostics.pass('Model discovery completed', {
|
|
278
|
+
target,
|
|
279
|
+
detail: `Using ${firstModel}`,
|
|
280
|
+
durationMs: Date.now() - startedAt,
|
|
281
|
+
})
|
|
180
282
|
}
|
|
181
283
|
if (models.length === 0) {
|
|
284
|
+
diagnostics.warn('Model discovery returned no models', {
|
|
285
|
+
target,
|
|
286
|
+
durationMs: Date.now() - startedAt,
|
|
287
|
+
})
|
|
182
288
|
return {
|
|
183
289
|
ok: true,
|
|
184
290
|
message: runtime.useCloud
|
|
185
291
|
? 'Connected to Ollama Cloud, but no models were returned.'
|
|
186
292
|
: 'Connected to Ollama, but no models are installed yet. Run `ollama pull <model>` to add one.',
|
|
187
293
|
normalizedEndpoint,
|
|
294
|
+
diagnostics: diagnostics.toJSON(),
|
|
188
295
|
}
|
|
189
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
|
+
})
|
|
190
304
|
}
|
|
191
|
-
} catch {
|
|
192
|
-
|
|
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
|
+
})
|
|
193
311
|
}
|
|
194
312
|
}
|
|
195
313
|
|
|
196
|
-
if (!testModel)
|
|
314
|
+
if (!testModel) {
|
|
315
|
+
testModel = 'llama3.2'
|
|
316
|
+
diagnostics.warn('Fallback test model selected', { detail: testModel })
|
|
317
|
+
}
|
|
197
318
|
|
|
198
319
|
// Test the chat endpoint
|
|
199
320
|
const label = runtime.useCloud ? 'Ollama Cloud' : 'Ollama'
|
|
200
321
|
const chatEndpoint = `${normalizedEndpoint}/v1/chat/completions`
|
|
201
322
|
const chatBody = JSON.stringify({ model: testModel, max_completion_tokens: 8, messages: [{ role: 'user', content: 'Reply OK' }] })
|
|
202
323
|
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
}
|
|
210
345
|
if (!chatRes.ok) {
|
|
211
|
-
const detail = await parseErrorMessage(chatRes, `${label} chat returned ${chatRes.status}.`)
|
|
212
|
-
|
|
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() }
|
|
213
353
|
}
|
|
354
|
+
diagnostics.pass('Chat completion check passed', {
|
|
355
|
+
target: chatEndpoint,
|
|
356
|
+
detail: `Verified with ${testModel}`,
|
|
357
|
+
durationMs: Date.now() - chatStartedAt,
|
|
358
|
+
})
|
|
214
359
|
return {
|
|
215
360
|
ok: true,
|
|
216
361
|
message: `Connected to ${label}. Chat verified with ${testModel}.`,
|
|
217
362
|
normalizedEndpoint,
|
|
218
363
|
recommendedModel: recommendedModel || testModel,
|
|
364
|
+
diagnostics: diagnostics.toJSON(),
|
|
219
365
|
}
|
|
220
366
|
}
|
|
221
367
|
|
|
222
|
-
async function checkOpenClaw(apiKey: string, endpointRaw: string): Promise<
|
|
368
|
+
async function checkOpenClaw(apiKey: string, endpointRaw: string): Promise<ProviderCheckResult> {
|
|
369
|
+
const diagnostics = createProviderDiagnostics()
|
|
223
370
|
const { httpUrl: normalizedEndpoint, wsUrl } = normalizeOpenClawUrl(endpointRaw)
|
|
224
371
|
const token = apiKey || undefined
|
|
225
372
|
const deviceId = getDeviceId()
|
|
373
|
+
diagnostics.pass('Endpoint resolved', { target: normalizedEndpoint })
|
|
226
374
|
|
|
375
|
+
const wsStartedAt = Date.now()
|
|
227
376
|
const result = await wsConnect(wsUrl, token, true, 10_000)
|
|
228
377
|
|
|
229
378
|
if (!result.ok) {
|
|
230
379
|
if (result.ws) try { result.ws.close() } catch {}
|
|
231
|
-
|
|
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() }
|
|
232
386
|
}
|
|
387
|
+
diagnostics.pass('Gateway websocket check passed', {
|
|
388
|
+
target: wsUrl,
|
|
389
|
+
detail: deviceId ? `Device ${deviceId}` : undefined,
|
|
390
|
+
durationMs: Date.now() - wsStartedAt,
|
|
391
|
+
})
|
|
233
392
|
|
|
234
393
|
// Attempt model discovery via RPC before closing the connection
|
|
235
394
|
let recommendedModel: string | undefined
|
|
236
395
|
if (result.ws) {
|
|
396
|
+
const modelStartedAt = Date.now()
|
|
237
397
|
try {
|
|
238
398
|
const payload = await rpcOnConnectedGateway(result.ws, 'models.list', {}, 8_000) as Record<string, unknown> | unknown[] | undefined
|
|
239
399
|
const p = payload as Record<string, unknown> | undefined
|
|
@@ -246,13 +406,20 @@ async function checkOpenClaw(apiKey: string, endpointRaw: string): Promise<{ ok:
|
|
|
246
406
|
} else if (typeof first?.name === 'string') {
|
|
247
407
|
recommendedModel = first.name
|
|
248
408
|
}
|
|
249
|
-
|
|
250
|
-
|
|
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
|
+
})
|
|
251
418
|
}
|
|
252
419
|
try { result.ws.close() } catch {}
|
|
253
420
|
}
|
|
254
421
|
|
|
255
|
-
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() }
|
|
256
423
|
}
|
|
257
424
|
|
|
258
425
|
export async function POST(req: Request) {
|
|
@@ -302,7 +469,12 @@ export async function POST(req: Request) {
|
|
|
302
469
|
|
|
303
470
|
if (isCliProviderId(provider)) {
|
|
304
471
|
const result = checkCliProviderReady(provider)
|
|
305
|
-
|
|
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() })
|
|
306
478
|
}
|
|
307
479
|
|
|
308
480
|
if (!provider) {
|