@swarmclawai/swarmclaw 0.2.0

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.
Files changed (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,351 @@
1
+ import { streamClaudeCliChat } from './claude-cli'
2
+ import { streamCodexCliChat } from './codex-cli'
3
+ import { streamOpenCodeCliChat } from './opencode-cli'
4
+ import { streamOpenAiChat } from './openai'
5
+ import { streamOllamaChat } from './ollama'
6
+ import { streamAnthropicChat } from './anthropic'
7
+ import { streamOpenClawChat } from './openclaw'
8
+ import type { ProviderInfo, ProviderConfig as CustomProviderConfig } from '../../types'
9
+
10
+ const RETRYABLE_STATUS_CODES = [401, 429, 500, 502, 503]
11
+
12
+ export interface ProviderHandler {
13
+ streamChat: (opts: StreamChatOptions) => Promise<string>
14
+ }
15
+
16
+ export interface StreamChatOptions {
17
+ session: any
18
+ message: string
19
+ imagePath?: string
20
+ apiKey?: string | null
21
+ systemPrompt?: string
22
+ write: (data: string) => void
23
+ active: Map<string, any>
24
+ loadHistory: (sessionId: string) => any[]
25
+ }
26
+
27
+ interface BuiltinProviderConfig extends ProviderInfo {
28
+ handler: ProviderHandler
29
+ }
30
+
31
+ const PROVIDERS: Record<string, BuiltinProviderConfig> = {
32
+ 'claude-cli': {
33
+ id: 'claude-cli',
34
+ name: 'Claude Code CLI',
35
+ models: ['claude-sonnet-4-6', 'claude-opus-4-6', 'claude-haiku-4-5-20251001', 'claude-sonnet-4-5-20250514'],
36
+ requiresApiKey: false,
37
+ requiresEndpoint: false,
38
+ handler: { streamChat: streamClaudeCliChat },
39
+ },
40
+ 'codex-cli': {
41
+ id: 'codex-cli',
42
+ name: 'OpenAI Codex CLI',
43
+ models: ['gpt-5.3-codex', 'gpt-5.2-codex', 'gpt-5.1-codex', 'gpt-5-codex', 'gpt-5-codex-mini'],
44
+ requiresApiKey: false,
45
+ requiresEndpoint: false,
46
+ handler: { streamChat: streamCodexCliChat },
47
+ },
48
+ openai: {
49
+ id: 'openai',
50
+ name: 'OpenAI',
51
+ models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', 'o3', 'o3-mini', 'o4-mini'],
52
+ requiresApiKey: true,
53
+ requiresEndpoint: false,
54
+ handler: { streamChat: streamOpenAiChat },
55
+ },
56
+ anthropic: {
57
+ id: 'anthropic',
58
+ name: 'Anthropic',
59
+ models: ['claude-sonnet-4-6', 'claude-opus-4-6', 'claude-haiku-4-5-20251001'],
60
+ requiresApiKey: true,
61
+ requiresEndpoint: false,
62
+ handler: { streamChat: streamAnthropicChat },
63
+ },
64
+ openclaw: {
65
+ id: 'openclaw',
66
+ name: 'OpenClaw',
67
+ models: ['default'],
68
+ requiresApiKey: true,
69
+ requiresEndpoint: true,
70
+ defaultEndpoint: 'http://localhost:18789',
71
+ handler: { streamChat: streamOpenClawChat },
72
+ },
73
+ 'opencode-cli': {
74
+ id: 'opencode-cli',
75
+ name: 'OpenCode CLI',
76
+ models: ['claude-sonnet-4-6', 'gpt-4.1', 'gemini-2.5-pro', 'gemini-2.5-flash'],
77
+ requiresApiKey: false,
78
+ requiresEndpoint: false,
79
+ handler: { streamChat: streamOpenCodeCliChat },
80
+ },
81
+ google: {
82
+ id: 'google',
83
+ name: 'Google Gemini',
84
+ models: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite'],
85
+ requiresApiKey: true,
86
+ requiresEndpoint: false,
87
+ defaultEndpoint: 'https://generativelanguage.googleapis.com/v1beta/openai',
88
+ handler: {
89
+ streamChat: (opts) => {
90
+ const patchedSession = {
91
+ ...opts.session,
92
+ apiEndpoint: opts.session.apiEndpoint || 'https://generativelanguage.googleapis.com/v1beta/openai',
93
+ }
94
+ return streamOpenAiChat({ ...opts, session: patchedSession })
95
+ },
96
+ },
97
+ },
98
+ deepseek: {
99
+ id: 'deepseek',
100
+ name: 'DeepSeek',
101
+ models: ['deepseek-chat', 'deepseek-reasoner'],
102
+ requiresApiKey: true,
103
+ requiresEndpoint: false,
104
+ defaultEndpoint: 'https://api.deepseek.com/v1',
105
+ handler: {
106
+ streamChat: (opts) => {
107
+ const patchedSession = {
108
+ ...opts.session,
109
+ apiEndpoint: opts.session.apiEndpoint || 'https://api.deepseek.com/v1',
110
+ }
111
+ return streamOpenAiChat({ ...opts, session: patchedSession })
112
+ },
113
+ },
114
+ },
115
+ groq: {
116
+ id: 'groq',
117
+ name: 'Groq',
118
+ models: ['llama-3.3-70b-versatile', 'deepseek-r1-distill-llama-70b', 'qwen-qwq-32b', 'gemma2-9b-it'],
119
+ requiresApiKey: true,
120
+ requiresEndpoint: false,
121
+ defaultEndpoint: 'https://api.groq.com/openai/v1',
122
+ handler: {
123
+ streamChat: (opts) => {
124
+ const patchedSession = {
125
+ ...opts.session,
126
+ apiEndpoint: opts.session.apiEndpoint || 'https://api.groq.com/openai/v1',
127
+ }
128
+ return streamOpenAiChat({ ...opts, session: patchedSession })
129
+ },
130
+ },
131
+ },
132
+ together: {
133
+ id: 'together',
134
+ name: 'Together AI',
135
+ models: ['meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8', 'deepseek-ai/DeepSeek-R1', 'Qwen/Qwen2.5-72B-Instruct'],
136
+ requiresApiKey: true,
137
+ requiresEndpoint: false,
138
+ defaultEndpoint: 'https://api.together.xyz/v1',
139
+ handler: {
140
+ streamChat: (opts) => {
141
+ const patchedSession = {
142
+ ...opts.session,
143
+ apiEndpoint: opts.session.apiEndpoint || 'https://api.together.xyz/v1',
144
+ }
145
+ return streamOpenAiChat({ ...opts, session: patchedSession })
146
+ },
147
+ },
148
+ },
149
+ mistral: {
150
+ id: 'mistral',
151
+ name: 'Mistral AI',
152
+ models: ['mistral-large-latest', 'mistral-small-latest', 'magistral-medium-2506', 'devstral-small-latest'],
153
+ requiresApiKey: true,
154
+ requiresEndpoint: false,
155
+ defaultEndpoint: 'https://api.mistral.ai/v1',
156
+ handler: {
157
+ streamChat: (opts) => {
158
+ const patchedSession = {
159
+ ...opts.session,
160
+ apiEndpoint: opts.session.apiEndpoint || 'https://api.mistral.ai/v1',
161
+ }
162
+ return streamOpenAiChat({ ...opts, session: patchedSession })
163
+ },
164
+ },
165
+ },
166
+ xai: {
167
+ id: 'xai',
168
+ name: 'xAI (Grok)',
169
+ models: ['grok-3', 'grok-3-fast', 'grok-3-mini', 'grok-3-mini-fast'],
170
+ requiresApiKey: true,
171
+ requiresEndpoint: false,
172
+ defaultEndpoint: 'https://api.x.ai/v1',
173
+ handler: {
174
+ streamChat: (opts) => {
175
+ const patchedSession = {
176
+ ...opts.session,
177
+ apiEndpoint: opts.session.apiEndpoint || 'https://api.x.ai/v1',
178
+ }
179
+ return streamOpenAiChat({ ...opts, session: patchedSession })
180
+ },
181
+ },
182
+ },
183
+ fireworks: {
184
+ id: 'fireworks',
185
+ name: 'Fireworks AI',
186
+ models: ['accounts/fireworks/models/deepseek-r1-0528', 'accounts/fireworks/models/llama-v3p3-70b-instruct', 'accounts/fireworks/models/qwen3-235b-a22b'],
187
+ requiresApiKey: true,
188
+ requiresEndpoint: false,
189
+ defaultEndpoint: 'https://api.fireworks.ai/inference/v1',
190
+ handler: {
191
+ streamChat: (opts) => {
192
+ const patchedSession = {
193
+ ...opts.session,
194
+ apiEndpoint: opts.session.apiEndpoint || 'https://api.fireworks.ai/inference/v1',
195
+ }
196
+ return streamOpenAiChat({ ...opts, session: patchedSession })
197
+ },
198
+ },
199
+ },
200
+ ollama: {
201
+ id: 'ollama',
202
+ name: 'Ollama',
203
+ models: [
204
+ 'qwen3.5', 'qwen3-coder-next', 'qwen3-coder', 'qwen3-next', 'qwen3-vl',
205
+ 'glm-5', 'glm-4.7', 'glm-4.6',
206
+ 'kimi-k2.5', 'kimi-k2', 'kimi-k2-thinking',
207
+ 'minimax-m2.5', 'minimax-m2.1', 'minimax-m2',
208
+ 'deepseek-v3.2', 'deepseek-r1',
209
+ 'gemini-3-flash-preview', 'gemma3',
210
+ 'devstral-2', 'devstral-small-2', 'ministral-3', 'mistral-large-3',
211
+ 'gpt-oss', 'cogito-2.1', 'rnj-1', 'nemotron-3-nano',
212
+ 'llama3.3', 'llama3.2', 'llama3.1',
213
+ ],
214
+ requiresApiKey: false,
215
+ optionalApiKey: true,
216
+ requiresEndpoint: true,
217
+ defaultEndpoint: 'http://localhost:11434',
218
+ handler: { streamChat: streamOllamaChat },
219
+ },
220
+ }
221
+
222
+ /** Merge built-in providers with custom providers from storage */
223
+ function getCustomProviders(): Record<string, CustomProviderConfig> {
224
+ try {
225
+ const { loadProviderConfigs } = require('../server/storage')
226
+ return loadProviderConfigs() as Record<string, CustomProviderConfig>
227
+ } catch {
228
+ return {}
229
+ }
230
+ }
231
+
232
+ function getModelOverrides(): Record<string, string[]> {
233
+ try {
234
+ const { loadModelOverrides } = require('../server/storage')
235
+ return loadModelOverrides()
236
+ } catch {
237
+ return {}
238
+ }
239
+ }
240
+
241
+ export function getProviderList(): ProviderInfo[] {
242
+ const overrides = getModelOverrides()
243
+ const builtins = Object.values(PROVIDERS)
244
+ .filter(({ id }) => id !== 'openclaw')
245
+ .map(({ handler, ...info }) => ({
246
+ ...info,
247
+ models: overrides[info.id] || info.models,
248
+ }))
249
+ const customs = Object.values(getCustomProviders())
250
+ .filter((c) => c.isEnabled)
251
+ .map((c) => ({
252
+ id: c.id as any,
253
+ name: c.name,
254
+ models: c.models,
255
+ requiresApiKey: c.requiresApiKey,
256
+ requiresEndpoint: false,
257
+ defaultEndpoint: c.baseUrl,
258
+ }))
259
+ return [...builtins, ...customs]
260
+ }
261
+
262
+ export function getProvider(id: string): BuiltinProviderConfig | null {
263
+ if (PROVIDERS[id]) return PROVIDERS[id]
264
+ // Check custom providers — they use OpenAI-compatible handler with custom baseUrl
265
+ const customs = getCustomProviders()
266
+ const custom = customs[id]
267
+ if (custom?.isEnabled) {
268
+ return {
269
+ id: custom.id as any,
270
+ name: custom.name,
271
+ models: custom.models,
272
+ requiresApiKey: custom.requiresApiKey,
273
+ requiresEndpoint: false,
274
+ handler: {
275
+ streamChat: (opts) => {
276
+ // Custom providers use OpenAI handler with custom baseUrl
277
+ const patchedSession = { ...opts.session, apiEndpoint: custom.baseUrl }
278
+ return streamOpenAiChat({ ...opts, session: patchedSession })
279
+ },
280
+ },
281
+ }
282
+ }
283
+ return null
284
+ }
285
+
286
+ /**
287
+ * Stream chat with automatic failover to fallback credentials on retryable errors.
288
+ * Falls back through fallbackCredentialIds on 401/429/500/502/503 errors.
289
+ */
290
+ export async function streamChatWithFailover(
291
+ opts: StreamChatOptions & { fallbackCredentialIds?: string[] },
292
+ ): Promise<string> {
293
+ const provider = getProvider(opts.session.provider)
294
+ if (!provider) throw new Error(`Unknown provider: ${opts.session.provider}`)
295
+
296
+ const credentialIds = [
297
+ opts.session.credentialId,
298
+ ...(opts.fallbackCredentialIds || []),
299
+ ].filter(Boolean) as string[]
300
+
301
+ // If no fallbacks, just call directly
302
+ if (credentialIds.length <= 1) {
303
+ return provider.handler.streamChat(opts)
304
+ }
305
+
306
+ let lastError: any = null
307
+
308
+ for (let i = 0; i < credentialIds.length; i++) {
309
+ const credId = credentialIds[i]
310
+ try {
311
+ // Resolve API key for this credential
312
+ let apiKey: string | null = opts.apiKey || null
313
+ if (credId && i > 0) {
314
+ // Need to decrypt fallback credential
315
+ const { loadCredentials, decryptKey } = require('../server/storage')
316
+ const creds = loadCredentials()
317
+ const cred = creds[credId]
318
+ if (cred?.encryptedKey) {
319
+ try { apiKey = decryptKey(cred.encryptedKey) } catch { /* skip */ }
320
+ }
321
+ }
322
+
323
+ const result = await provider.handler.streamChat({
324
+ ...opts,
325
+ apiKey,
326
+ })
327
+ return result // success
328
+ } catch (err: any) {
329
+ lastError = err
330
+ const statusCode = err.status || err.statusCode || 0
331
+ const isRetryable = RETRYABLE_STATUS_CODES.includes(statusCode)
332
+ || err.message?.includes('rate limit')
333
+ || err.message?.includes('Rate limit')
334
+ || err.message?.includes('429')
335
+ || err.message?.includes('401')
336
+
337
+ if (isRetryable && i < credentialIds.length - 1) {
338
+ console.log(`[failover] Credential ${credId} failed (${statusCode || err.message}), trying fallback...`)
339
+ // Send a metadata event to inform the client
340
+ opts.write(`data: ${JSON.stringify({
341
+ t: 'md',
342
+ text: JSON.stringify({ failover: { from: credId, reason: err.message?.slice(0, 100) } }),
343
+ })}\n\n`)
344
+ continue
345
+ }
346
+ throw err
347
+ }
348
+ }
349
+
350
+ throw lastError || new Error('All credentials exhausted')
351
+ }
@@ -0,0 +1,131 @@
1
+ import fs from 'fs'
2
+ import http from 'http'
3
+ import https from 'https'
4
+ import type { StreamChatOptions } from './index'
5
+
6
+ const IMAGE_EXTS = /\.(png|jpg|jpeg|gif|webp|bmp)$/i
7
+ const TEXT_EXTS = /\.(txt|md|csv|json|xml|html|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|yml|yaml|toml|env|log|sh|sql|css|scss)$/i
8
+
9
+ export function streamOllamaChat({ session, message, imagePath, apiKey, write, active, loadHistory }: StreamChatOptions): Promise<string> {
10
+ return new Promise((resolve) => {
11
+ const messages = buildMessages(session, message, imagePath, loadHistory)
12
+ const model = session.model || 'llama3'
13
+ // Cloud: no endpoint but API key present → use Ollama cloud
14
+ const endpoint = session.apiEndpoint || (apiKey ? 'https://ollama.com' : 'http://localhost:11434')
15
+
16
+ const parsed = new URL(endpoint)
17
+ const isHttps = parsed.protocol === 'https:'
18
+ const transport = isHttps ? https : http
19
+ const defaultPort = isHttps ? 443 : 11434
20
+
21
+ const payload = JSON.stringify({
22
+ model,
23
+ messages,
24
+ stream: true,
25
+ })
26
+
27
+ const abortController = { aborted: false }
28
+ let fullResponse = ''
29
+
30
+ const headers: Record<string, string> = {
31
+ 'Content-Type': 'application/json',
32
+ }
33
+ if (apiKey) {
34
+ headers['Authorization'] = `Bearer ${apiKey}`
35
+ }
36
+
37
+ const apiReq = transport.request({
38
+ hostname: parsed.hostname,
39
+ port: parsed.port || defaultPort,
40
+ path: '/api/chat',
41
+ method: 'POST',
42
+ headers,
43
+ }, (apiRes) => {
44
+ if (apiRes.statusCode !== 200) {
45
+ let errBody = ''
46
+ apiRes.on('data', (c: Buffer) => errBody += c)
47
+ apiRes.on('end', () => {
48
+ console.error(`[${session.id}] ollama error ${apiRes.statusCode}:`, errBody.slice(0, 200))
49
+ write(`data: ${JSON.stringify({ t: 'err', text: `Ollama error (${apiRes.statusCode}): ${errBody.slice(0, 100)}` })}\n\n`)
50
+ active.delete(session.id)
51
+ resolve(fullResponse)
52
+ })
53
+ return
54
+ }
55
+
56
+ let buf = ''
57
+ apiRes.on('data', (chunk: Buffer) => {
58
+ if (abortController.aborted) return
59
+ buf += chunk.toString()
60
+ const lines = buf.split('\n')
61
+ buf = lines.pop()!
62
+
63
+ for (const line of lines) {
64
+ if (!line.trim()) continue
65
+ try {
66
+ const parsed = JSON.parse(line)
67
+ const content = parsed.message?.content
68
+ if (content) {
69
+ fullResponse += content
70
+ write(`data: ${JSON.stringify({ t: 'd', text: content })}\n\n`)
71
+ }
72
+ } catch {}
73
+ }
74
+ })
75
+
76
+ apiRes.on('end', () => {
77
+ active.delete(session.id)
78
+ resolve(fullResponse)
79
+ })
80
+ })
81
+
82
+ active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
83
+
84
+ apiReq.on('error', (e: NodeJS.ErrnoException) => {
85
+ console.error(`[${session.id}] ollama request error:`, e.message)
86
+ let errMsg = e.message
87
+ if (e.code === 'ECONNREFUSED') {
88
+ errMsg = `Cannot connect to Ollama at ${endpoint}. Is Ollama running?`
89
+ }
90
+ write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
91
+ active.delete(session.id)
92
+ resolve(fullResponse)
93
+ })
94
+
95
+ apiReq.end(payload)
96
+ })
97
+ }
98
+
99
+ function fileToOllamaMsg(text: string, filePath?: string): { content: string; images?: string[] } {
100
+ if (!filePath || !fs.existsSync(filePath)) return { content: text }
101
+ if (IMAGE_EXTS.test(filePath)) {
102
+ const data = fs.readFileSync(filePath).toString('base64')
103
+ return { content: text, images: [data] }
104
+ }
105
+ if (TEXT_EXTS.test(filePath) || filePath.endsWith('.pdf')) {
106
+ try {
107
+ const fileContent = fs.readFileSync(filePath, 'utf-8')
108
+ const name = filePath.split('/').pop() || 'file'
109
+ return { content: `[Attached file: ${name}]\n\n${fileContent}\n\n${text}` }
110
+ } catch { return { content: text } }
111
+ }
112
+ return { content: `[Attached file: ${filePath.split('/').pop()}]\n\n${text}` }
113
+ }
114
+
115
+ function buildMessages(session: any, message: string, imagePath: string | undefined, loadHistory: (id: string) => any[]) {
116
+ const msgs: Array<{ role: string; content: string; images?: string[] }> = []
117
+
118
+ if (loadHistory) {
119
+ const history = loadHistory(session.id)
120
+ for (const m of history) {
121
+ if (m.role === 'user' && m.imagePath) {
122
+ msgs.push({ role: 'user', ...fileToOllamaMsg(m.text, m.imagePath) })
123
+ } else {
124
+ msgs.push({ role: m.role, content: m.text })
125
+ }
126
+ }
127
+ }
128
+
129
+ msgs.push({ role: 'user', ...fileToOllamaMsg(message, imagePath) })
130
+ return msgs
131
+ }
@@ -0,0 +1,164 @@
1
+ import fs from 'fs'
2
+ import type { StreamChatOptions } from './index'
3
+
4
+ const IMAGE_EXTS = /\.(png|jpg|jpeg|gif|webp|bmp)$/i
5
+ const TEXT_EXTS = /\.(txt|md|csv|json|xml|html|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|yml|yaml|toml|env|log|sh|sql|css|scss)$/i
6
+
7
+ function fileToContentParts(filePath: string): any[] {
8
+ if (!filePath || !fs.existsSync(filePath)) return []
9
+ if (IMAGE_EXTS.test(filePath)) {
10
+ const data = fs.readFileSync(filePath).toString('base64')
11
+ const ext = filePath.split('.').pop()?.toLowerCase() || 'png'
12
+ const mimeType = ext === 'jpg' ? 'image/jpeg' : `image/${ext}`
13
+ return [{ type: 'image_url', image_url: { url: `data:${mimeType};base64,${data}` } }]
14
+ }
15
+ if (TEXT_EXTS.test(filePath) || filePath.endsWith('.pdf')) {
16
+ try {
17
+ const text = fs.readFileSync(filePath, 'utf-8')
18
+ const name = filePath.split('/').pop() || 'file'
19
+ return [{ type: 'text', text: `[Attached file: ${name}]\n\n${text}` }]
20
+ } catch { return [] }
21
+ }
22
+ return [{ type: 'text', text: `[Attached file: ${filePath.split('/').pop()}]` }]
23
+ }
24
+
25
+ export function streamOpenAiChat({ session, message, imagePath, apiKey, systemPrompt, write, active, loadHistory }: StreamChatOptions): Promise<string> {
26
+ return new Promise(async (resolve) => {
27
+ const messages = buildMessages(session, message, imagePath, systemPrompt, loadHistory)
28
+ const model = session.model || 'gpt-4o'
29
+
30
+ const payload = JSON.stringify({
31
+ model,
32
+ messages,
33
+ stream: true,
34
+ })
35
+
36
+ let fullResponse = ''
37
+
38
+ // Support custom base URLs for custom providers
39
+ const baseUrl = session.apiEndpoint || 'https://api.openai.com/v1'
40
+ const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`
41
+
42
+ // OpenClaw endpoints behind Hostinger's proxy use express.json() middleware
43
+ // which consumes the request body before http-proxy-middleware can forward it.
44
+ // Sending as text/plain bypasses the body parser while the gateway still parses JSON.
45
+ const contentType = session.contentType || 'application/json'
46
+
47
+ const abortController = new AbortController()
48
+ active.set(session.id, { kill: () => abortController.abort() })
49
+
50
+ try {
51
+ const res = await fetch(url, {
52
+ method: 'POST',
53
+ headers: {
54
+ 'Authorization': `Bearer ${apiKey}`,
55
+ 'Content-Type': contentType,
56
+ },
57
+ body: payload,
58
+ signal: abortController.signal,
59
+ })
60
+
61
+ // Detect HTML responses (e.g. landing page returned instead of API)
62
+ const resContentType = res.headers.get('content-type') || ''
63
+ if (resContentType.includes('text/html')) {
64
+ console.error(`[${session.id}] received HTML instead of API response from ${baseUrl} (provider: ${session.provider})`)
65
+ write(`data: ${JSON.stringify({ t: 'err', text: 'Received HTML instead of API response. The endpoint may be misconfigured or returning a landing page.' })}\n\n`)
66
+ active.delete(session.id)
67
+ resolve(fullResponse)
68
+ return
69
+ }
70
+
71
+ if (!res.ok) {
72
+ const errBody = await res.text().catch(() => '')
73
+ console.error(`[${session.id}] openai error ${res.status}:`, errBody.slice(0, 200))
74
+ let errMsg = `API error (${res.status})`
75
+ try {
76
+ const parsed = JSON.parse(errBody)
77
+ if (parsed.error?.message) errMsg = parsed.error.message
78
+ else if (parsed.message) errMsg = parsed.message
79
+ else if (parsed.detail) errMsg = parsed.detail
80
+ } catch {}
81
+ write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
82
+ active.delete(session.id)
83
+ resolve(fullResponse)
84
+ return
85
+ }
86
+
87
+ if (!res.body) {
88
+ console.error(`[${session.id}] no response body from ${baseUrl}`)
89
+ active.delete(session.id)
90
+ resolve(fullResponse)
91
+ return
92
+ }
93
+
94
+ const reader = res.body.getReader()
95
+ const decoder = new TextDecoder()
96
+ let buf = ''
97
+
98
+ while (true) {
99
+ const { done, value } = await reader.read()
100
+ if (done) break
101
+ if (abortController.signal.aborted) break
102
+
103
+ buf += decoder.decode(value, { stream: true })
104
+ const lines = buf.split('\n')
105
+ buf = lines.pop()!
106
+
107
+ for (const line of lines) {
108
+ if (!line.startsWith('data: ')) continue
109
+ const data = line.slice(6).trim()
110
+ if (data === '[DONE]') continue
111
+ try {
112
+ const parsed = JSON.parse(data)
113
+ const delta = parsed.choices?.[0]?.delta?.content
114
+ if (delta) {
115
+ fullResponse += delta
116
+ write(`data: ${JSON.stringify({ t: 'd', text: delta })}\n\n`)
117
+ }
118
+ } catch {}
119
+ }
120
+ }
121
+
122
+ if (!fullResponse) {
123
+ console.error(`[${session.id}] openai stream ended with no content (provider: ${session.provider}, endpoint: ${baseUrl})`)
124
+ }
125
+ } catch (err: any) {
126
+ if (err.name !== 'AbortError') {
127
+ console.error(`[${session.id}] openai request error:`, err.message)
128
+ write(`data: ${JSON.stringify({ t: 'err', text: `Connection failed: ${err.message}` })}\n\n`)
129
+ }
130
+ } finally {
131
+ active.delete(session.id)
132
+ resolve(fullResponse)
133
+ }
134
+ })
135
+ }
136
+
137
+ function buildMessages(session: any, message: string, imagePath: string | undefined, systemPrompt: string | undefined, loadHistory: (id: string) => any[]) {
138
+ const msgs: Array<{ role: string; content: any }> = []
139
+
140
+ if (systemPrompt) {
141
+ msgs.push({ role: 'system', content: systemPrompt })
142
+ }
143
+
144
+ if (loadHistory) {
145
+ const history = loadHistory(session.id)
146
+ for (const m of history) {
147
+ if (m.role === 'user' && m.imagePath) {
148
+ const parts = fileToContentParts(m.imagePath)
149
+ msgs.push({ role: 'user', content: [...parts, { type: 'text', text: m.text }] })
150
+ } else {
151
+ msgs.push({ role: m.role, content: m.text })
152
+ }
153
+ }
154
+ }
155
+
156
+ // Current message with optional attachment
157
+ if (imagePath) {
158
+ const parts = fileToContentParts(imagePath)
159
+ msgs.push({ role: 'user', content: [...parts, { type: 'text', text: message }] })
160
+ } else {
161
+ msgs.push({ role: 'user', content: message })
162
+ }
163
+ return msgs
164
+ }