@full-self-developing/fsd 1.0.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 (1191) hide show
  1. package/.engine/engine-config.json +27 -0
  2. package/CODEBASE_CONTEXT.md +152 -0
  3. package/README.md +111 -0
  4. package/README_zh.md +111 -0
  5. package/UI_SPEC.md +57 -0
  6. package/agents/api-proxy.js +542 -0
  7. package/agents/base.js +280 -0
  8. package/agents/branch-manager.js +135 -0
  9. package/agents/cli-models.json +48 -0
  10. package/agents/coder.js +128 -0
  11. package/agents/core-request.js +174 -0
  12. package/agents/dispatcher.js +491 -0
  13. package/agents/drivers/.atomcode/graph.bin +0 -0
  14. package/agents/drivers/atomcode.js +143 -0
  15. package/agents/drivers/gemini-cli.js +195 -0
  16. package/agents/drivers/index.js +65 -0
  17. package/agents/drivers/openrouter.js +390 -0
  18. package/agents/engine-config.js +444 -0
  19. package/agents/log-fixer.js +72 -0
  20. package/agents/mcp-client-manager.js +159 -0
  21. package/agents/optimizer.js +54 -0
  22. package/agents/path-validator.js +43 -0
  23. package/agents/planner.js +81 -0
  24. package/agents/prompt-manager.js +170 -0
  25. package/agents/skeptic.js +79 -0
  26. package/agents/skills-manager.js +130 -0
  27. package/agents/summarizer.js +34 -0
  28. package/agents/test-runner.js +85 -0
  29. package/bin/cli.js +166 -0
  30. package/client/eslint.config.js +21 -0
  31. package/client/index.html +12 -0
  32. package/client/package-lock.json +3339 -0
  33. package/client/package.json +35 -0
  34. package/client/src/App.jsx +745 -0
  35. package/client/src/api.js +78 -0
  36. package/client/src/components/ChatPanel.jsx +277 -0
  37. package/client/src/components/ConfirmationModal.jsx +61 -0
  38. package/client/src/components/ErrorBoundary.jsx +66 -0
  39. package/client/src/components/FolderPicker.jsx +200 -0
  40. package/client/src/components/LoopPanel.jsx +863 -0
  41. package/client/src/components/NotFound.jsx +52 -0
  42. package/client/src/components/SettingsPanel.jsx +966 -0
  43. package/client/src/components/Sidebar.jsx +318 -0
  44. package/client/src/context/SettingsContext.jsx +353 -0
  45. package/client/src/i18n.js +462 -0
  46. package/client/src/index.css +31 -0
  47. package/client/src/main.jsx +17 -0
  48. package/client/vite.config.js +19 -0
  49. package/design.md +875 -0
  50. package/extensions/alibaba/index.ts +11 -0
  51. package/extensions/alibaba/openclaw.plugin.json +34 -0
  52. package/extensions/alibaba/package.json +15 -0
  53. package/extensions/alibaba/plugin-registration.contract.test.ts +7 -0
  54. package/extensions/alibaba/tsconfig.json +16 -0
  55. package/extensions/alibaba/video-generation-provider.test.ts +92 -0
  56. package/extensions/alibaba/video-generation-provider.ts +83 -0
  57. package/extensions/amazon-bedrock/api.ts +6 -0
  58. package/extensions/amazon-bedrock/aws-credential-refresh.ts +42 -0
  59. package/extensions/amazon-bedrock/config-api.ts +4 -0
  60. package/extensions/amazon-bedrock/config-compat.test.ts +81 -0
  61. package/extensions/amazon-bedrock/config-compat.ts +107 -0
  62. package/extensions/amazon-bedrock/discovery-shared.ts +28 -0
  63. package/extensions/amazon-bedrock/discovery.test.ts +608 -0
  64. package/extensions/amazon-bedrock/discovery.ts +616 -0
  65. package/extensions/amazon-bedrock/embedding-provider.test.ts +109 -0
  66. package/extensions/amazon-bedrock/embedding-provider.ts +470 -0
  67. package/extensions/amazon-bedrock/index.test.ts +1249 -0
  68. package/extensions/amazon-bedrock/index.ts +11 -0
  69. package/extensions/amazon-bedrock/lazy-import.test.ts +56 -0
  70. package/extensions/amazon-bedrock/memory-embedding-adapter.test.ts +105 -0
  71. package/extensions/amazon-bedrock/memory-embedding-adapter.ts +47 -0
  72. package/extensions/amazon-bedrock/npm-shrinkwrap.json +1241 -0
  73. package/extensions/amazon-bedrock/openclaw.plugin.json +80 -0
  74. package/extensions/amazon-bedrock/package.json +41 -0
  75. package/extensions/amazon-bedrock/provider-policy-api.test.ts +46 -0
  76. package/extensions/amazon-bedrock/provider-policy-api.ts +9 -0
  77. package/extensions/amazon-bedrock/register.sync.runtime.ts +659 -0
  78. package/extensions/amazon-bedrock/setup-api.ts +18 -0
  79. package/extensions/amazon-bedrock/thinking-policy.ts +32 -0
  80. package/extensions/amazon-bedrock/tsconfig.json +16 -0
  81. package/extensions/anthropic/api.ts +11 -0
  82. package/extensions/anthropic/claude-model-refs.ts +104 -0
  83. package/extensions/anthropic/cli-auth-seam.ts +13 -0
  84. package/extensions/anthropic/cli-backend-api.ts +6 -0
  85. package/extensions/anthropic/cli-backend.ts +83 -0
  86. package/extensions/anthropic/cli-catalog.ts +42 -0
  87. package/extensions/anthropic/cli-constants.ts +41 -0
  88. package/extensions/anthropic/cli-migration.test.ts +487 -0
  89. package/extensions/anthropic/cli-migration.ts +266 -0
  90. package/extensions/anthropic/cli-shared.test.ts +300 -0
  91. package/extensions/anthropic/cli-shared.ts +248 -0
  92. package/extensions/anthropic/config-defaults.ts +428 -0
  93. package/extensions/anthropic/contract-api.ts +9 -0
  94. package/extensions/anthropic/doctor-contract-api.ts +14 -0
  95. package/extensions/anthropic/index.test.ts +663 -0
  96. package/extensions/anthropic/index.ts +11 -0
  97. package/extensions/anthropic/media-understanding-provider.ts +15 -0
  98. package/extensions/anthropic/openclaw.plugin.json +112 -0
  99. package/extensions/anthropic/package.json +18 -0
  100. package/extensions/anthropic/provider-contract-api.ts +59 -0
  101. package/extensions/anthropic/provider-discovery.ts +35 -0
  102. package/extensions/anthropic/provider-policy-api.test.ts +135 -0
  103. package/extensions/anthropic/provider-policy-api.ts +24 -0
  104. package/extensions/anthropic/provider-runtime.contract.test.ts +3 -0
  105. package/extensions/anthropic/register.runtime.ts +668 -0
  106. package/extensions/anthropic/replay-policy.ts +9 -0
  107. package/extensions/anthropic/setup-api.ts +11 -0
  108. package/extensions/anthropic/stream-wrappers.test.ts +233 -0
  109. package/extensions/anthropic/stream-wrappers.ts +228 -0
  110. package/extensions/anthropic/test-api.ts +3 -0
  111. package/extensions/anthropic/tsconfig.json +16 -0
  112. package/extensions/arcee/api.ts +8 -0
  113. package/extensions/arcee/index.test.ts +195 -0
  114. package/extensions/arcee/index.ts +142 -0
  115. package/extensions/arcee/models.ts +68 -0
  116. package/extensions/arcee/onboard.ts +43 -0
  117. package/extensions/arcee/openclaw.plugin.json +46 -0
  118. package/extensions/arcee/package.json +15 -0
  119. package/extensions/arcee/provider-catalog.ts +54 -0
  120. package/extensions/arcee/tsconfig.json +16 -0
  121. package/extensions/azure-speech/azure-speech.live.test.ts +92 -0
  122. package/extensions/azure-speech/index.ts +11 -0
  123. package/extensions/azure-speech/openclaw.plugin.json +66 -0
  124. package/extensions/azure-speech/package.json +15 -0
  125. package/extensions/azure-speech/speech-provider.test.ts +242 -0
  126. package/extensions/azure-speech/speech-provider.ts +306 -0
  127. package/extensions/azure-speech/tsconfig.json +16 -0
  128. package/extensions/azure-speech/tts.test.ts +127 -0
  129. package/extensions/azure-speech/tts.ts +209 -0
  130. package/extensions/byteplus/api.ts +8 -0
  131. package/extensions/byteplus/index.test.ts +60 -0
  132. package/extensions/byteplus/index.ts +84 -0
  133. package/extensions/byteplus/live.test.ts +60 -0
  134. package/extensions/byteplus/models.ts +35 -0
  135. package/extensions/byteplus/openclaw.plugin.json +165 -0
  136. package/extensions/byteplus/package.json +15 -0
  137. package/extensions/byteplus/plugin-registration.contract.test.ts +8 -0
  138. package/extensions/byteplus/provider-catalog.ts +17 -0
  139. package/extensions/byteplus/provider-discovery.ts +31 -0
  140. package/extensions/byteplus/tsconfig.json +16 -0
  141. package/extensions/byteplus/video-generation-provider.test.ts +223 -0
  142. package/extensions/byteplus/video-generation-provider.ts +389 -0
  143. package/extensions/cerebras/api.ts +7 -0
  144. package/extensions/cerebras/index.ts +41 -0
  145. package/extensions/cerebras/models.ts +25 -0
  146. package/extensions/cerebras/onboard.ts +26 -0
  147. package/extensions/cerebras/openclaw.plugin.json +111 -0
  148. package/extensions/cerebras/package.json +15 -0
  149. package/extensions/cerebras/provider-catalog.ts +10 -0
  150. package/extensions/cerebras/tsconfig.json +16 -0
  151. package/extensions/chutes/api.ts +14 -0
  152. package/extensions/chutes/implicit-provider.test.ts +107 -0
  153. package/extensions/chutes/index.ts +194 -0
  154. package/extensions/chutes/model-discovery-env.ts +5 -0
  155. package/extensions/chutes/models.test.ts +289 -0
  156. package/extensions/chutes/models.ts +632 -0
  157. package/extensions/chutes/oauth.ts +235 -0
  158. package/extensions/chutes/onboard.ts +63 -0
  159. package/extensions/chutes/openclaw.plugin.json +726 -0
  160. package/extensions/chutes/package.json +15 -0
  161. package/extensions/chutes/provider-catalog.ts +29 -0
  162. package/extensions/chutes/tsconfig.json +16 -0
  163. package/extensions/cloudflare-ai-gateway/api.ts +14 -0
  164. package/extensions/cloudflare-ai-gateway/catalog-provider.ts +73 -0
  165. package/extensions/cloudflare-ai-gateway/index.test.ts +60 -0
  166. package/extensions/cloudflare-ai-gateway/index.ts +233 -0
  167. package/extensions/cloudflare-ai-gateway/models.ts +44 -0
  168. package/extensions/cloudflare-ai-gateway/onboard.ts +91 -0
  169. package/extensions/cloudflare-ai-gateway/openclaw.plugin.json +44 -0
  170. package/extensions/cloudflare-ai-gateway/package.json +15 -0
  171. package/extensions/cloudflare-ai-gateway/provider-discovery.contract.test.ts +3 -0
  172. package/extensions/cloudflare-ai-gateway/stream-wrappers.test.ts +160 -0
  173. package/extensions/cloudflare-ai-gateway/stream-wrappers.ts +32 -0
  174. package/extensions/cloudflare-ai-gateway/tsconfig.json +16 -0
  175. package/extensions/codex/doctor-contract-api.test.ts +44 -0
  176. package/extensions/codex/doctor-contract-api.ts +68 -0
  177. package/extensions/codex/harness.ts +85 -0
  178. package/extensions/codex/index.test.ts +230 -0
  179. package/extensions/codex/index.ts +125 -0
  180. package/extensions/codex/media-understanding-provider.test.ts +496 -0
  181. package/extensions/codex/media-understanding-provider.ts +524 -0
  182. package/extensions/codex/npm-shrinkwrap.json +1949 -0
  183. package/extensions/codex/openclaw.plugin.json +403 -0
  184. package/extensions/codex/package.json +41 -0
  185. package/extensions/codex/prompt-overlay-runtime-contract.test.ts +48 -0
  186. package/extensions/codex/prompt-overlay.ts +21 -0
  187. package/extensions/codex/provider-catalog.ts +83 -0
  188. package/extensions/codex/provider-discovery.ts +45 -0
  189. package/extensions/codex/provider.test.ts +384 -0
  190. package/extensions/codex/provider.ts +243 -0
  191. package/extensions/codex/src/app-server/app-inventory-cache.test.ts +176 -0
  192. package/extensions/codex/src/app-server/app-inventory-cache.ts +324 -0
  193. package/extensions/codex/src/app-server/approval-bridge.test.ts +1472 -0
  194. package/extensions/codex/src/app-server/approval-bridge.ts +1211 -0
  195. package/extensions/codex/src/app-server/auth-bridge.test.ts +1449 -0
  196. package/extensions/codex/src/app-server/auth-bridge.ts +614 -0
  197. package/extensions/codex/src/app-server/auth-profile-runtime-contract.test.ts +242 -0
  198. package/extensions/codex/src/app-server/capabilities.ts +27 -0
  199. package/extensions/codex/src/app-server/client-factory.ts +24 -0
  200. package/extensions/codex/src/app-server/client.test.ts +563 -0
  201. package/extensions/codex/src/app-server/client.ts +721 -0
  202. package/extensions/codex/src/app-server/compact.test.ts +1029 -0
  203. package/extensions/codex/src/app-server/compact.ts +662 -0
  204. package/extensions/codex/src/app-server/computer-use.test.ts +788 -0
  205. package/extensions/codex/src/app-server/computer-use.ts +683 -0
  206. package/extensions/codex/src/app-server/config.test.ts +948 -0
  207. package/extensions/codex/src/app-server/config.ts +1093 -0
  208. package/extensions/codex/src/app-server/context-engine-projection.test.ts +252 -0
  209. package/extensions/codex/src/app-server/context-engine-projection.ts +403 -0
  210. package/extensions/codex/src/app-server/delivery-no-reply-runtime-contract.test.ts +80 -0
  211. package/extensions/codex/src/app-server/dynamic-tool-diagnostics.ts +73 -0
  212. package/extensions/codex/src/app-server/dynamic-tool-profile.ts +70 -0
  213. package/extensions/codex/src/app-server/dynamic-tools.test.ts +1357 -0
  214. package/extensions/codex/src/app-server/dynamic-tools.ts +646 -0
  215. package/extensions/codex/src/app-server/elicitation-bridge.test.ts +1281 -0
  216. package/extensions/codex/src/app-server/elicitation-bridge.ts +828 -0
  217. package/extensions/codex/src/app-server/event-projector.test.ts +2885 -0
  218. package/extensions/codex/src/app-server/event-projector.ts +2047 -0
  219. package/extensions/codex/src/app-server/image-payload-sanitizer.test.ts +49 -0
  220. package/extensions/codex/src/app-server/image-payload-sanitizer.ts +195 -0
  221. package/extensions/codex/src/app-server/local-runtime-attribution.ts +39 -0
  222. package/extensions/codex/src/app-server/managed-binary.test.ts +141 -0
  223. package/extensions/codex/src/app-server/managed-binary.ts +193 -0
  224. package/extensions/codex/src/app-server/models.test.ts +246 -0
  225. package/extensions/codex/src/app-server/models.ts +172 -0
  226. package/extensions/codex/src/app-server/native-hook-relay.test.ts +274 -0
  227. package/extensions/codex/src/app-server/native-hook-relay.ts +150 -0
  228. package/extensions/codex/src/app-server/native-subagent-monitor.test.ts +1125 -0
  229. package/extensions/codex/src/app-server/native-subagent-monitor.ts +1061 -0
  230. package/extensions/codex/src/app-server/native-subagent-notification.test.ts +176 -0
  231. package/extensions/codex/src/app-server/native-subagent-notification.ts +222 -0
  232. package/extensions/codex/src/app-server/native-subagent-task-ids.ts +3 -0
  233. package/extensions/codex/src/app-server/native-subagent-task-mirror.test.ts +625 -0
  234. package/extensions/codex/src/app-server/native-subagent-task-mirror.ts +460 -0
  235. package/extensions/codex/src/app-server/notification-correlation.ts +91 -0
  236. package/extensions/codex/src/app-server/openclaw-owned-tool-runtime-contract.test.ts +456 -0
  237. package/extensions/codex/src/app-server/outcome-fallback-runtime-contract.test.ts +404 -0
  238. package/extensions/codex/src/app-server/plugin-activation.test.ts +336 -0
  239. package/extensions/codex/src/app-server/plugin-activation.ts +283 -0
  240. package/extensions/codex/src/app-server/plugin-app-cache-key.ts +74 -0
  241. package/extensions/codex/src/app-server/plugin-approval-roundtrip.ts +122 -0
  242. package/extensions/codex/src/app-server/plugin-inventory.test.ts +355 -0
  243. package/extensions/codex/src/app-server/plugin-inventory.ts +357 -0
  244. package/extensions/codex/src/app-server/plugin-thread-config.test.ts +865 -0
  245. package/extensions/codex/src/app-server/plugin-thread-config.ts +455 -0
  246. package/extensions/codex/src/app-server/protocol-generated/json/DynamicToolCallParams.json +33 -0
  247. package/extensions/codex/src/app-server/protocol-generated/json/v2/ErrorNotification.json +199 -0
  248. package/extensions/codex/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +102 -0
  249. package/extensions/codex/src/app-server/protocol-generated/json/v2/ModelListResponse.json +227 -0
  250. package/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +2630 -0
  251. package/extensions/codex/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +2630 -0
  252. package/extensions/codex/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +1659 -0
  253. package/extensions/codex/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +1655 -0
  254. package/extensions/codex/src/app-server/protocol-validators.test.ts +75 -0
  255. package/extensions/codex/src/app-server/protocol-validators.ts +203 -0
  256. package/extensions/codex/src/app-server/protocol.ts +537 -0
  257. package/extensions/codex/src/app-server/rate-limit-cache.ts +48 -0
  258. package/extensions/codex/src/app-server/rate-limits.test.ts +202 -0
  259. package/extensions/codex/src/app-server/rate-limits.ts +583 -0
  260. package/extensions/codex/src/app-server/request.test.ts +68 -0
  261. package/extensions/codex/src/app-server/request.ts +90 -0
  262. package/extensions/codex/src/app-server/run-attempt-thread-cleanup.test.ts +197 -0
  263. package/extensions/codex/src/app-server/run-attempt.context-engine.test.ts +1246 -0
  264. package/extensions/codex/src/app-server/run-attempt.test.ts +10799 -0
  265. package/extensions/codex/src/app-server/run-attempt.ts +5264 -0
  266. package/extensions/codex/src/app-server/run-attempt.vision-tools.test.ts +35 -0
  267. package/extensions/codex/src/app-server/sandbox-exec-server/filesystem.ts +261 -0
  268. package/extensions/codex/src/app-server/sandbox-exec-server/fs-policy.ts +346 -0
  269. package/extensions/codex/src/app-server/sandbox-exec-server/http.ts +312 -0
  270. package/extensions/codex/src/app-server/sandbox-exec-server/json-rpc.ts +93 -0
  271. package/extensions/codex/src/app-server/sandbox-exec-server/processes.ts +411 -0
  272. package/extensions/codex/src/app-server/sandbox-exec-server/runtime.ts +22 -0
  273. package/extensions/codex/src/app-server/sandbox-exec-server/types.ts +80 -0
  274. package/extensions/codex/src/app-server/sandbox-exec-server.fs.test.ts +527 -0
  275. package/extensions/codex/src/app-server/sandbox-exec-server.http.test.ts +210 -0
  276. package/extensions/codex/src/app-server/sandbox-exec-server.test-helpers.ts +236 -0
  277. package/extensions/codex/src/app-server/sandbox-exec-server.test.ts +460 -0
  278. package/extensions/codex/src/app-server/sandbox-exec-server.ts +355 -0
  279. package/extensions/codex/src/app-server/sandbox-guard.ts +153 -0
  280. package/extensions/codex/src/app-server/schema-normalization-runtime-contract.test.ts +206 -0
  281. package/extensions/codex/src/app-server/session-binding.test.ts +303 -0
  282. package/extensions/codex/src/app-server/session-binding.ts +407 -0
  283. package/extensions/codex/src/app-server/session-history.ts +44 -0
  284. package/extensions/codex/src/app-server/shared-client.test.ts +591 -0
  285. package/extensions/codex/src/app-server/shared-client.ts +289 -0
  286. package/extensions/codex/src/app-server/side-question.test.ts +1243 -0
  287. package/extensions/codex/src/app-server/side-question.ts +1019 -0
  288. package/extensions/codex/src/app-server/test-support.ts +48 -0
  289. package/extensions/codex/src/app-server/thread-lifecycle.test.ts +447 -0
  290. package/extensions/codex/src/app-server/thread-lifecycle.ts +1004 -0
  291. package/extensions/codex/src/app-server/thread-lifecycle.user-mcp-servers.test.ts +442 -0
  292. package/extensions/codex/src/app-server/timeout.ts +9 -0
  293. package/extensions/codex/src/app-server/tool-progress-normalization.ts +77 -0
  294. package/extensions/codex/src/app-server/trajectory.test.ts +205 -0
  295. package/extensions/codex/src/app-server/trajectory.ts +368 -0
  296. package/extensions/codex/src/app-server/transcript-mirror.test.ts +527 -0
  297. package/extensions/codex/src/app-server/transcript-mirror.ts +208 -0
  298. package/extensions/codex/src/app-server/transcript-repair-runtime-contract.test.ts +44 -0
  299. package/extensions/codex/src/app-server/transport-stdio.test.ts +184 -0
  300. package/extensions/codex/src/app-server/transport-stdio.ts +107 -0
  301. package/extensions/codex/src/app-server/transport-websocket.test.ts +71 -0
  302. package/extensions/codex/src/app-server/transport-websocket.ts +90 -0
  303. package/extensions/codex/src/app-server/transport.ts +117 -0
  304. package/extensions/codex/src/app-server/user-input-bridge.test.ts +249 -0
  305. package/extensions/codex/src/app-server/user-input-bridge.ts +316 -0
  306. package/extensions/codex/src/app-server/version.ts +5 -0
  307. package/extensions/codex/src/app-server/vision-tools.ts +12 -0
  308. package/extensions/codex/src/command-account.ts +589 -0
  309. package/extensions/codex/src/command-formatters.ts +426 -0
  310. package/extensions/codex/src/command-handlers.ts +2092 -0
  311. package/extensions/codex/src/command-plugins-management.test.ts +172 -0
  312. package/extensions/codex/src/command-plugins-management.ts +137 -0
  313. package/extensions/codex/src/command-rpc.test.ts +16 -0
  314. package/extensions/codex/src/command-rpc.ts +146 -0
  315. package/extensions/codex/src/commands.test.ts +3737 -0
  316. package/extensions/codex/src/commands.ts +65 -0
  317. package/extensions/codex/src/conversation-binding-data.ts +124 -0
  318. package/extensions/codex/src/conversation-binding.test.ts +697 -0
  319. package/extensions/codex/src/conversation-binding.ts +575 -0
  320. package/extensions/codex/src/conversation-control.test.ts +126 -0
  321. package/extensions/codex/src/conversation-control.ts +303 -0
  322. package/extensions/codex/src/conversation-turn-collector.test.ts +191 -0
  323. package/extensions/codex/src/conversation-turn-collector.ts +190 -0
  324. package/extensions/codex/src/conversation-turn-input.test.ts +141 -0
  325. package/extensions/codex/src/conversation-turn-input.ts +106 -0
  326. package/extensions/codex/src/manifest.test.ts +20 -0
  327. package/extensions/codex/src/migration/apply.ts +501 -0
  328. package/extensions/codex/src/migration/helpers.ts +55 -0
  329. package/extensions/codex/src/migration/plan.ts +461 -0
  330. package/extensions/codex/src/migration/provider.test.ts +1741 -0
  331. package/extensions/codex/src/migration/provider.ts +41 -0
  332. package/extensions/codex/src/migration/source.ts +643 -0
  333. package/extensions/codex/src/migration/targets.ts +25 -0
  334. package/extensions/codex/src/node-cli-sessions.test.ts +180 -0
  335. package/extensions/codex/src/node-cli-sessions.ts +711 -0
  336. package/extensions/codex/test-api.ts +95 -0
  337. package/extensions/codex/tsconfig.json +16 -0
  338. package/extensions/comfy/comfy.live.test.ts +128 -0
  339. package/extensions/comfy/image-generation-provider.test.ts +457 -0
  340. package/extensions/comfy/image-generation-provider.ts +79 -0
  341. package/extensions/comfy/index.test.ts +51 -0
  342. package/extensions/comfy/index.ts +45 -0
  343. package/extensions/comfy/music-generation-provider.test.ts +101 -0
  344. package/extensions/comfy/music-generation-provider.ts +88 -0
  345. package/extensions/comfy/openclaw.plugin.json +268 -0
  346. package/extensions/comfy/package.json +15 -0
  347. package/extensions/comfy/plugin-registration.contract.test.ts +11 -0
  348. package/extensions/comfy/test-helpers.ts +113 -0
  349. package/extensions/comfy/tsconfig.json +16 -0
  350. package/extensions/comfy/video-generation-provider.test.ts +184 -0
  351. package/extensions/comfy/video-generation-provider.ts +104 -0
  352. package/extensions/comfy/workflow-runtime.ts +827 -0
  353. package/extensions/deepgram/audio.live.test.ts +75 -0
  354. package/extensions/deepgram/audio.test.ts +146 -0
  355. package/extensions/deepgram/audio.ts +109 -0
  356. package/extensions/deepgram/index.ts +13 -0
  357. package/extensions/deepgram/media-understanding-provider.ts +10 -0
  358. package/extensions/deepgram/openclaw.plugin.json +30 -0
  359. package/extensions/deepgram/package.json +15 -0
  360. package/extensions/deepgram/realtime-transcription-provider.test.ts +69 -0
  361. package/extensions/deepgram/realtime-transcription-provider.ts +283 -0
  362. package/extensions/deepgram/test-api.ts +2 -0
  363. package/extensions/deepgram/tsconfig.json +16 -0
  364. package/extensions/deepinfra/api.ts +8 -0
  365. package/extensions/deepinfra/embedding-provider.ts +33 -0
  366. package/extensions/deepinfra/image-generation-provider.test.ts +224 -0
  367. package/extensions/deepinfra/image-generation-provider.ts +89 -0
  368. package/extensions/deepinfra/index.test.ts +113 -0
  369. package/extensions/deepinfra/index.ts +84 -0
  370. package/extensions/deepinfra/media-models.ts +50 -0
  371. package/extensions/deepinfra/media-understanding-provider.test.ts +73 -0
  372. package/extensions/deepinfra/media-understanding-provider.ts +37 -0
  373. package/extensions/deepinfra/memory-embedding-adapter.test.ts +31 -0
  374. package/extensions/deepinfra/memory-embedding-adapter.ts +35 -0
  375. package/extensions/deepinfra/onboard.test.ts +172 -0
  376. package/extensions/deepinfra/onboard.ts +36 -0
  377. package/extensions/deepinfra/openclaw.plugin.json +203 -0
  378. package/extensions/deepinfra/package.json +15 -0
  379. package/extensions/deepinfra/provider-catalog.ts +24 -0
  380. package/extensions/deepinfra/provider-models.test.ts +217 -0
  381. package/extensions/deepinfra/provider-models.ts +167 -0
  382. package/extensions/deepinfra/provider-policy-api.test.ts +41 -0
  383. package/extensions/deepinfra/provider-policy-api.ts +21 -0
  384. package/extensions/deepinfra/provider.contract.test.ts +3 -0
  385. package/extensions/deepinfra/speech-provider.test.ts +169 -0
  386. package/extensions/deepinfra/speech-provider.ts +41 -0
  387. package/extensions/deepinfra/tsconfig.json +16 -0
  388. package/extensions/deepinfra/video-generation-provider.test.ts +194 -0
  389. package/extensions/deepinfra/video-generation-provider.ts +262 -0
  390. package/extensions/deepseek/api.ts +7 -0
  391. package/extensions/deepseek/deepseek.live.test.ts +232 -0
  392. package/extensions/deepseek/index.test.ts +488 -0
  393. package/extensions/deepseek/index.ts +58 -0
  394. package/extensions/deepseek/models.ts +33 -0
  395. package/extensions/deepseek/onboard.ts +31 -0
  396. package/extensions/deepseek/openclaw.plugin.json +132 -0
  397. package/extensions/deepseek/package.json +15 -0
  398. package/extensions/deepseek/provider-catalog.ts +14 -0
  399. package/extensions/deepseek/provider-discovery.ts +17 -0
  400. package/extensions/deepseek/provider-policy-api.test.ts +264 -0
  401. package/extensions/deepseek/provider-policy-api.ts +104 -0
  402. package/extensions/deepseek/stream.ts +14 -0
  403. package/extensions/deepseek/thinking.ts +19 -0
  404. package/extensions/deepseek/tsconfig.json +16 -0
  405. package/extensions/elevenlabs/config-api.ts +8 -0
  406. package/extensions/elevenlabs/config-compat.test.ts +75 -0
  407. package/extensions/elevenlabs/config-compat.ts +181 -0
  408. package/extensions/elevenlabs/contract-api.ts +8 -0
  409. package/extensions/elevenlabs/doctor-contract.ts +34 -0
  410. package/extensions/elevenlabs/elevenlabs.live.test.ts +91 -0
  411. package/extensions/elevenlabs/index.ts +15 -0
  412. package/extensions/elevenlabs/media-understanding-provider.test.ts +95 -0
  413. package/extensions/elevenlabs/media-understanding-provider.ts +85 -0
  414. package/extensions/elevenlabs/openclaw.plugin.json +40 -0
  415. package/extensions/elevenlabs/package.json +15 -0
  416. package/extensions/elevenlabs/realtime-transcription-provider.test.ts +60 -0
  417. package/extensions/elevenlabs/realtime-transcription-provider.ts +284 -0
  418. package/extensions/elevenlabs/setup-api.ts +11 -0
  419. package/extensions/elevenlabs/shared.ts +10 -0
  420. package/extensions/elevenlabs/speech-provider.test.ts +124 -0
  421. package/extensions/elevenlabs/speech-provider.ts +594 -0
  422. package/extensions/elevenlabs/test-api.ts +6 -0
  423. package/extensions/elevenlabs/tsconfig.json +16 -0
  424. package/extensions/elevenlabs/tts.test.ts +212 -0
  425. package/extensions/elevenlabs/tts.ts +198 -0
  426. package/extensions/fal/image-generation-provider.test.ts +710 -0
  427. package/extensions/fal/image-generation-provider.ts +463 -0
  428. package/extensions/fal/index.ts +19 -0
  429. package/extensions/fal/music-generation-provider.test.ts +200 -0
  430. package/extensions/fal/music-generation-provider.ts +219 -0
  431. package/extensions/fal/onboard.ts +21 -0
  432. package/extensions/fal/openclaw.plugin.json +42 -0
  433. package/extensions/fal/package.json +15 -0
  434. package/extensions/fal/plugin-registration.contract.test.ts +11 -0
  435. package/extensions/fal/provider-contract-api.ts +31 -0
  436. package/extensions/fal/provider-registration.ts +38 -0
  437. package/extensions/fal/test-api.ts +3 -0
  438. package/extensions/fal/tsconfig.json +16 -0
  439. package/extensions/fal/video-generation-provider.test.ts +566 -0
  440. package/extensions/fal/video-generation-provider.ts +648 -0
  441. package/extensions/fireworks/index.test.ts +181 -0
  442. package/extensions/fireworks/index.ts +85 -0
  443. package/extensions/fireworks/model-id.ts +5 -0
  444. package/extensions/fireworks/onboard.ts +30 -0
  445. package/extensions/fireworks/openclaw.plugin.json +73 -0
  446. package/extensions/fireworks/package.json +18 -0
  447. package/extensions/fireworks/provider-catalog.ts +50 -0
  448. package/extensions/fireworks/provider-policy-api.ts +8 -0
  449. package/extensions/fireworks/stream.test.ts +184 -0
  450. package/extensions/fireworks/stream.ts +39 -0
  451. package/extensions/fireworks/thinking-policy.ts +17 -0
  452. package/extensions/fireworks/tsconfig.json +16 -0
  453. package/extensions/github-copilot/api.ts +1 -0
  454. package/extensions/github-copilot/auth.test.ts +109 -0
  455. package/extensions/github-copilot/auth.ts +65 -0
  456. package/extensions/github-copilot/connection-bound-ids.live.test.ts +231 -0
  457. package/extensions/github-copilot/connection-bound-ids.test.ts +96 -0
  458. package/extensions/github-copilot/connection-bound-ids.ts +81 -0
  459. package/extensions/github-copilot/embeddings.test.ts +287 -0
  460. package/extensions/github-copilot/embeddings.ts +342 -0
  461. package/extensions/github-copilot/index.test.ts +660 -0
  462. package/extensions/github-copilot/index.ts +492 -0
  463. package/extensions/github-copilot/login.ts +323 -0
  464. package/extensions/github-copilot/model-metadata.ts +51 -0
  465. package/extensions/github-copilot/models-defaults.ts +61 -0
  466. package/extensions/github-copilot/models.test.ts +695 -0
  467. package/extensions/github-copilot/models.ts +274 -0
  468. package/extensions/github-copilot/openclaw.plugin.json +270 -0
  469. package/extensions/github-copilot/package.json +19 -0
  470. package/extensions/github-copilot/provider-auth.contract.test.ts +3 -0
  471. package/extensions/github-copilot/provider-discovery.contract.test.ts +7 -0
  472. package/extensions/github-copilot/provider-runtime.contract.test.ts +3 -0
  473. package/extensions/github-copilot/register.runtime.ts +24 -0
  474. package/extensions/github-copilot/replay-policy.ts +9 -0
  475. package/extensions/github-copilot/stream.test.ts +282 -0
  476. package/extensions/github-copilot/stream.ts +157 -0
  477. package/extensions/github-copilot/token.ts +6 -0
  478. package/extensions/github-copilot/tsconfig.json +16 -0
  479. package/extensions/github-copilot/usage.ts +68 -0
  480. package/extensions/google/api.test.ts +249 -0
  481. package/extensions/google/api.ts +91 -0
  482. package/extensions/google/cli-backend.ts +58 -0
  483. package/extensions/google/default-model.test.ts +115 -0
  484. package/extensions/google/doctor-contract-api.ts +18 -0
  485. package/extensions/google/embedding-batch.ts +379 -0
  486. package/extensions/google/embedding-provider.test.ts +264 -0
  487. package/extensions/google/embedding-provider.ts +441 -0
  488. package/extensions/google/gemini-auth.ts +20 -0
  489. package/extensions/google/gemini-cli-provider.ts +145 -0
  490. package/extensions/google/generation-provider-metadata.ts +121 -0
  491. package/extensions/google/google-genai-runtime.ts +8 -0
  492. package/extensions/google/google-shared.test-helpers.ts +99 -0
  493. package/extensions/google/google-shared.test.ts +380 -0
  494. package/extensions/google/google.live.test.ts +179 -0
  495. package/extensions/google/image-generation-provider.test.ts +503 -0
  496. package/extensions/google/image-generation-provider.ts +272 -0
  497. package/extensions/google/index.test.ts +310 -0
  498. package/extensions/google/index.ts +354 -0
  499. package/extensions/google/manifest.test.ts +104 -0
  500. package/extensions/google/media-understanding-provider.ts +164 -0
  501. package/extensions/google/media-understanding-provider.video.test.ts +158 -0
  502. package/extensions/google/memory-embedding-adapter.ts +79 -0
  503. package/extensions/google/model-id.test.ts +42 -0
  504. package/extensions/google/model-id.ts +35 -0
  505. package/extensions/google/music-generation-provider.test.ts +278 -0
  506. package/extensions/google/music-generation-provider.ts +176 -0
  507. package/extensions/google/oauth-token-shared.test.ts +39 -0
  508. package/extensions/google/oauth-token-shared.ts +42 -0
  509. package/extensions/google/oauth.credentials.ts +273 -0
  510. package/extensions/google/oauth.flow.ts +61 -0
  511. package/extensions/google/oauth.http.ts +24 -0
  512. package/extensions/google/oauth.project.ts +232 -0
  513. package/extensions/google/oauth.runtime.ts +1 -0
  514. package/extensions/google/oauth.settings.ts +72 -0
  515. package/extensions/google/oauth.shared.ts +44 -0
  516. package/extensions/google/oauth.test.ts +922 -0
  517. package/extensions/google/oauth.token.ts +138 -0
  518. package/extensions/google/oauth.ts +104 -0
  519. package/extensions/google/onboard.ts +78 -0
  520. package/extensions/google/openclaw.plugin.json +706 -0
  521. package/extensions/google/package.json +19 -0
  522. package/extensions/google/plugin-registration.contract.test.ts +12 -0
  523. package/extensions/google/provider-contract-api.ts +77 -0
  524. package/extensions/google/provider-hooks.ts +18 -0
  525. package/extensions/google/provider-models.test.ts +513 -0
  526. package/extensions/google/provider-models.ts +237 -0
  527. package/extensions/google/provider-policy-api.test.ts +201 -0
  528. package/extensions/google/provider-policy-api.ts +11 -0
  529. package/extensions/google/provider-policy.ts +208 -0
  530. package/extensions/google/provider-registration.ts +72 -0
  531. package/extensions/google/provider-runtime.contract.test.ts +3 -0
  532. package/extensions/google/realtime-voice-provider.test.ts +857 -0
  533. package/extensions/google/realtime-voice-provider.ts +952 -0
  534. package/extensions/google/runtime-api.ts +19 -0
  535. package/extensions/google/setup-api.test.ts +23 -0
  536. package/extensions/google/setup-api.ts +13 -0
  537. package/extensions/google/speech-provider.test.ts +682 -0
  538. package/extensions/google/speech-provider.ts +683 -0
  539. package/extensions/google/src/gemini-web-search-provider.runtime.ts +367 -0
  540. package/extensions/google/src/gemini-web-search-provider.shared.ts +45 -0
  541. package/extensions/google/src/gemini-web-search-provider.ts +151 -0
  542. package/extensions/google/test-api.ts +6 -0
  543. package/extensions/google/thinking-api.ts +14 -0
  544. package/extensions/google/thinking.test.ts +153 -0
  545. package/extensions/google/thinking.ts +14 -0
  546. package/extensions/google/transport-stream.test.ts +1726 -0
  547. package/extensions/google/transport-stream.ts +1396 -0
  548. package/extensions/google/tsconfig.json +16 -0
  549. package/extensions/google/vertex-adc.ts +188 -0
  550. package/extensions/google/video-generation-provider.test.ts +573 -0
  551. package/extensions/google/video-generation-provider.ts +591 -0
  552. package/extensions/google/web-search-contract-api.ts +1 -0
  553. package/extensions/google/web-search-provider.test.ts +548 -0
  554. package/extensions/google/web-search-provider.ts +1 -0
  555. package/extensions/groq/api.ts +60 -0
  556. package/extensions/groq/index.test.ts +90 -0
  557. package/extensions/groq/index.ts +21 -0
  558. package/extensions/groq/media-understanding-provider.ts +21 -0
  559. package/extensions/groq/openclaw.plugin.json +314 -0
  560. package/extensions/groq/package.json +15 -0
  561. package/extensions/groq/test-api.ts +1 -0
  562. package/extensions/groq/tsconfig.json +16 -0
  563. package/extensions/huggingface/api.ts +10 -0
  564. package/extensions/huggingface/index.test.ts +81 -0
  565. package/extensions/huggingface/index.ts +60 -0
  566. package/extensions/huggingface/model-discovery-env.ts +5 -0
  567. package/extensions/huggingface/models.test.ts +98 -0
  568. package/extensions/huggingface/models.ts +218 -0
  569. package/extensions/huggingface/onboard.ts +26 -0
  570. package/extensions/huggingface/openclaw.plugin.json +57 -0
  571. package/extensions/huggingface/package.json +15 -0
  572. package/extensions/huggingface/provider-catalog.ts +22 -0
  573. package/extensions/huggingface/tsconfig.json +16 -0
  574. package/extensions/image-generation-core/api.ts +30 -0
  575. package/extensions/image-generation-core/package.json +10 -0
  576. package/extensions/image-generation-core/runtime-api.ts +6 -0
  577. package/extensions/image-generation-core/src/runtime.test.ts +29 -0
  578. package/extensions/image-generation-core/src/runtime.ts +6 -0
  579. package/extensions/image-generation-core/tsconfig.json +16 -0
  580. package/extensions/kimi-coding/api.ts +8 -0
  581. package/extensions/kimi-coding/implicit-provider.test.ts +116 -0
  582. package/extensions/kimi-coding/index.test.ts +45 -0
  583. package/extensions/kimi-coding/index.ts +113 -0
  584. package/extensions/kimi-coding/onboard.test.ts +44 -0
  585. package/extensions/kimi-coding/onboard.ts +42 -0
  586. package/extensions/kimi-coding/openclaw.plugin.json +64 -0
  587. package/extensions/kimi-coding/package.json +18 -0
  588. package/extensions/kimi-coding/provider-catalog.test.ts +23 -0
  589. package/extensions/kimi-coding/provider-catalog.ts +58 -0
  590. package/extensions/kimi-coding/replay-policy.test.ts +10 -0
  591. package/extensions/kimi-coding/replay-policy.ts +3 -0
  592. package/extensions/kimi-coding/stream.test.ts +603 -0
  593. package/extensions/kimi-coding/stream.ts +399 -0
  594. package/extensions/kimi-coding/tsconfig.json +16 -0
  595. package/extensions/litellm/api.ts +8 -0
  596. package/extensions/litellm/image-generation-provider.test.ts +348 -0
  597. package/extensions/litellm/image-generation-provider.ts +142 -0
  598. package/extensions/litellm/index.test.ts +107 -0
  599. package/extensions/litellm/index.ts +108 -0
  600. package/extensions/litellm/onboard.test.ts +21 -0
  601. package/extensions/litellm/onboard.ts +55 -0
  602. package/extensions/litellm/openclaw.plugin.json +35 -0
  603. package/extensions/litellm/package.json +15 -0
  604. package/extensions/litellm/provider-catalog.ts +10 -0
  605. package/extensions/litellm/tsconfig.json +16 -0
  606. package/extensions/lmstudio/README.md +3 -0
  607. package/extensions/lmstudio/api.ts +36 -0
  608. package/extensions/lmstudio/index.test.ts +207 -0
  609. package/extensions/lmstudio/index.ts +137 -0
  610. package/extensions/lmstudio/memory-embedding-adapter.ts +36 -0
  611. package/extensions/lmstudio/openclaw.plugin.json +53 -0
  612. package/extensions/lmstudio/package.json +15 -0
  613. package/extensions/lmstudio/plugin-registration.contract.test.ts +6 -0
  614. package/extensions/lmstudio/runtime-api.ts +35 -0
  615. package/extensions/lmstudio/src/api.ts +42 -0
  616. package/extensions/lmstudio/src/defaults.ts +14 -0
  617. package/extensions/lmstudio/src/embedding-provider.ts +147 -0
  618. package/extensions/lmstudio/src/models.fetch.ts +277 -0
  619. package/extensions/lmstudio/src/models.test.ts +491 -0
  620. package/extensions/lmstudio/src/models.ts +536 -0
  621. package/extensions/lmstudio/src/plain-text-tool-calls.ts +24 -0
  622. package/extensions/lmstudio/src/provider-auth.ts +59 -0
  623. package/extensions/lmstudio/src/runtime.test.ts +357 -0
  624. package/extensions/lmstudio/src/runtime.ts +276 -0
  625. package/extensions/lmstudio/src/setup.test.ts +1543 -0
  626. package/extensions/lmstudio/src/setup.ts +878 -0
  627. package/extensions/lmstudio/src/stream.test.ts +658 -0
  628. package/extensions/lmstudio/src/stream.ts +493 -0
  629. package/extensions/media-understanding-core/image-ops.ts +137 -0
  630. package/extensions/media-understanding-core/package.json +14 -0
  631. package/extensions/media-understanding-core/runtime-api.ts +9 -0
  632. package/extensions/media-understanding-core/src/runtime.ts +9 -0
  633. package/extensions/media-understanding-core/tsconfig.json +16 -0
  634. package/extensions/microsoft/index.ts +11 -0
  635. package/extensions/microsoft/microsoft.live.test.ts +14 -0
  636. package/extensions/microsoft/openclaw.plugin.json +15 -0
  637. package/extensions/microsoft/package.json +18 -0
  638. package/extensions/microsoft/speech-provider.test.ts +298 -0
  639. package/extensions/microsoft/speech-provider.ts +295 -0
  640. package/extensions/microsoft/test-api.ts +1 -0
  641. package/extensions/microsoft/tsconfig.json +16 -0
  642. package/extensions/microsoft/tts.test.ts +193 -0
  643. package/extensions/microsoft/tts.ts +137 -0
  644. package/extensions/minimax/README.md +37 -0
  645. package/extensions/minimax/api.ts +27 -0
  646. package/extensions/minimax/image-generation-provider.test.ts +313 -0
  647. package/extensions/minimax/image-generation-provider.ts +216 -0
  648. package/extensions/minimax/index.test.ts +408 -0
  649. package/extensions/minimax/index.ts +39 -0
  650. package/extensions/minimax/media-understanding-provider.ts +23 -0
  651. package/extensions/minimax/minimax.live.test.ts +115 -0
  652. package/extensions/minimax/model-definitions.test.ts +101 -0
  653. package/extensions/minimax/model-definitions.ts +91 -0
  654. package/extensions/minimax/music-generation-provider.test.ts +198 -0
  655. package/extensions/minimax/music-generation-provider.ts +259 -0
  656. package/extensions/minimax/oauth.runtime.ts +1 -0
  657. package/extensions/minimax/oauth.ts +233 -0
  658. package/extensions/minimax/onboard.test.ts +126 -0
  659. package/extensions/minimax/onboard.ts +104 -0
  660. package/extensions/minimax/openclaw.plugin.json +133 -0
  661. package/extensions/minimax/package.json +15 -0
  662. package/extensions/minimax/plugin-registration.contract.test.ts +15 -0
  663. package/extensions/minimax/provider-catalog.ts +86 -0
  664. package/extensions/minimax/provider-contract-api.ts +84 -0
  665. package/extensions/minimax/provider-discovery.contract.test.ts +3 -0
  666. package/extensions/minimax/provider-http.test-helpers.ts +142 -0
  667. package/extensions/minimax/provider-models.ts +21 -0
  668. package/extensions/minimax/provider-registration.ts +285 -0
  669. package/extensions/minimax/speech-provider.test.ts +576 -0
  670. package/extensions/minimax/speech-provider.ts +312 -0
  671. package/extensions/minimax/src/minimax-web-search-provider.runtime.ts +270 -0
  672. package/extensions/minimax/src/minimax-web-search-provider.test.ts +177 -0
  673. package/extensions/minimax/src/minimax-web-search-provider.ts +64 -0
  674. package/extensions/minimax/test-api.ts +11 -0
  675. package/extensions/minimax/tsconfig.json +16 -0
  676. package/extensions/minimax/tts.ts +116 -0
  677. package/extensions/minimax/video-generation-provider.test.ts +214 -0
  678. package/extensions/minimax/video-generation-provider.ts +456 -0
  679. package/extensions/minimax/web-search-contract-api.ts +35 -0
  680. package/extensions/minimax/web-search-provider.ts +1 -0
  681. package/extensions/mistral/api.test.ts +195 -0
  682. package/extensions/mistral/api.ts +81 -0
  683. package/extensions/mistral/embedding-provider.ts +52 -0
  684. package/extensions/mistral/index.ts +61 -0
  685. package/extensions/mistral/media-understanding-provider.test.ts +46 -0
  686. package/extensions/mistral/media-understanding-provider.ts +21 -0
  687. package/extensions/mistral/memory-embedding-adapter.ts +35 -0
  688. package/extensions/mistral/mistral.live.test.ts +62 -0
  689. package/extensions/mistral/model-definitions.test.ts +65 -0
  690. package/extensions/mistral/model-definitions.ts +37 -0
  691. package/extensions/mistral/onboard.test.ts +54 -0
  692. package/extensions/mistral/onboard.ts +31 -0
  693. package/extensions/mistral/openclaw.plugin.json +180 -0
  694. package/extensions/mistral/package.json +15 -0
  695. package/extensions/mistral/provider-catalog.ts +10 -0
  696. package/extensions/mistral/provider-compat.ts +62 -0
  697. package/extensions/mistral/realtime-transcription-provider.test.ts +61 -0
  698. package/extensions/mistral/realtime-transcription-provider.ts +280 -0
  699. package/extensions/mistral/test-api.ts +2 -0
  700. package/extensions/mistral/tsconfig.json +16 -0
  701. package/extensions/moonshot/api.ts +9 -0
  702. package/extensions/moonshot/index.test.ts +73 -0
  703. package/extensions/moonshot/index.ts +81 -0
  704. package/extensions/moonshot/media-understanding-provider.test.ts +92 -0
  705. package/extensions/moonshot/media-understanding-provider.ts +85 -0
  706. package/extensions/moonshot/moonshot.live.test.ts +56 -0
  707. package/extensions/moonshot/onboard.ts +38 -0
  708. package/extensions/moonshot/openclaw.plugin.json +209 -0
  709. package/extensions/moonshot/package.json +15 -0
  710. package/extensions/moonshot/provider-catalog.test.ts +84 -0
  711. package/extensions/moonshot/provider-catalog.ts +34 -0
  712. package/extensions/moonshot/provider-contract-api.ts +33 -0
  713. package/extensions/moonshot/provider-discovery.ts +17 -0
  714. package/extensions/moonshot/src/kimi-web-search-provider.runtime.ts +513 -0
  715. package/extensions/moonshot/src/kimi-web-search-provider.test.ts +297 -0
  716. package/extensions/moonshot/src/kimi-web-search-provider.ts +71 -0
  717. package/extensions/moonshot/test-api.ts +2 -0
  718. package/extensions/moonshot/tsconfig.json +16 -0
  719. package/extensions/moonshot/web-search-contract-api.ts +28 -0
  720. package/extensions/moonshot/web-search-provider.ts +1 -0
  721. package/extensions/nvidia/api.ts +6 -0
  722. package/extensions/nvidia/index.test.ts +180 -0
  723. package/extensions/nvidia/index.ts +64 -0
  724. package/extensions/nvidia/onboard.test.ts +49 -0
  725. package/extensions/nvidia/onboard.ts +30 -0
  726. package/extensions/nvidia/openclaw.plugin.json +122 -0
  727. package/extensions/nvidia/package.json +15 -0
  728. package/extensions/nvidia/plugin-registration.contract.test.ts +14 -0
  729. package/extensions/nvidia/provider-catalog.test.ts +21 -0
  730. package/extensions/nvidia/provider-catalog.ts +15 -0
  731. package/extensions/nvidia/tsconfig.json +16 -0
  732. package/extensions/ollama/README.md +3 -0
  733. package/extensions/ollama/api.ts +34 -0
  734. package/extensions/ollama/index.test.ts +979 -0
  735. package/extensions/ollama/index.ts +336 -0
  736. package/extensions/ollama/ollama.live.test.ts +287 -0
  737. package/extensions/ollama/openclaw.plugin.json +67 -0
  738. package/extensions/ollama/package.json +19 -0
  739. package/extensions/ollama/plugin-registration.contract.test.ts +7 -0
  740. package/extensions/ollama/provider-discovery.import-guard.test.ts +29 -0
  741. package/extensions/ollama/provider-discovery.test.ts +657 -0
  742. package/extensions/ollama/provider-discovery.ts +69 -0
  743. package/extensions/ollama/provider-policy-api.test.ts +72 -0
  744. package/extensions/ollama/provider-policy-api.ts +59 -0
  745. package/extensions/ollama/runtime-api.ts +22 -0
  746. package/extensions/ollama/src/defaults.ts +14 -0
  747. package/extensions/ollama/src/discovery-shared.test.ts +41 -0
  748. package/extensions/ollama/src/discovery-shared.ts +322 -0
  749. package/extensions/ollama/src/embedding-provider.test.ts +557 -0
  750. package/extensions/ollama/src/embedding-provider.ts +393 -0
  751. package/extensions/ollama/src/media-understanding-provider.ts +18 -0
  752. package/extensions/ollama/src/memory-embedding-adapter.ts +30 -0
  753. package/extensions/ollama/src/model-id.ts +24 -0
  754. package/extensions/ollama/src/ollama-json.ts +143 -0
  755. package/extensions/ollama/src/provider-base-url.test.ts +44 -0
  756. package/extensions/ollama/src/provider-base-url.ts +23 -0
  757. package/extensions/ollama/src/provider-models.ssrf.test.ts +41 -0
  758. package/extensions/ollama/src/provider-models.test.ts +312 -0
  759. package/extensions/ollama/src/provider-models.ts +327 -0
  760. package/extensions/ollama/src/setup.test.ts +771 -0
  761. package/extensions/ollama/src/setup.ts +743 -0
  762. package/extensions/ollama/src/stream-runtime.test.ts +2218 -0
  763. package/extensions/ollama/src/stream.test.ts +252 -0
  764. package/extensions/ollama/src/stream.ts +1347 -0
  765. package/extensions/ollama/src/web-search-provider.test.ts +488 -0
  766. package/extensions/ollama/src/web-search-provider.ts +350 -0
  767. package/extensions/ollama/src/wsl2-crash-loop-check.test.ts +157 -0
  768. package/extensions/ollama/src/wsl2-crash-loop-check.ts +84 -0
  769. package/extensions/ollama/tsconfig.json +16 -0
  770. package/extensions/ollama/web-search-contract-api.ts +26 -0
  771. package/extensions/ollama/web-search-provider.ts +1 -0
  772. package/extensions/openai/api.ts +16 -0
  773. package/extensions/openai/auth-choice-copy.ts +33 -0
  774. package/extensions/openai/base-url.test.ts +60 -0
  775. package/extensions/openai/base-url.ts +23 -0
  776. package/extensions/openai/default-models.test.ts +36 -0
  777. package/extensions/openai/default-models.ts +40 -0
  778. package/extensions/openai/embedding-batch.test.ts +10 -0
  779. package/extensions/openai/embedding-batch.ts +274 -0
  780. package/extensions/openai/embedding-provider.test.ts +102 -0
  781. package/extensions/openai/embedding-provider.ts +110 -0
  782. package/extensions/openai/image-generation-provider.test.ts +1624 -0
  783. package/extensions/openai/image-generation-provider.ts +903 -0
  784. package/extensions/openai/index.test.ts +630 -0
  785. package/extensions/openai/index.ts +58 -0
  786. package/extensions/openai/media-understanding-provider.test.ts +119 -0
  787. package/extensions/openai/media-understanding-provider.ts +51 -0
  788. package/extensions/openai/memory-embedding-adapter.test.ts +82 -0
  789. package/extensions/openai/memory-embedding-adapter.ts +68 -0
  790. package/extensions/openai/native-web-search.ts +103 -0
  791. package/extensions/openai/openai-codex-auth-identity.test.ts +77 -0
  792. package/extensions/openai/openai-codex-auth-identity.ts +100 -0
  793. package/extensions/openai/openai-codex-catalog.ts +12 -0
  794. package/extensions/openai/openai-codex-device-code.test.ts +248 -0
  795. package/extensions/openai/openai-codex-device-code.ts +309 -0
  796. package/extensions/openai/openai-codex-oauth.runtime.ts +348 -0
  797. package/extensions/openai/openai-codex-provider.runtime.ts +45 -0
  798. package/extensions/openai/openai-codex-provider.test.ts +883 -0
  799. package/extensions/openai/openai-codex-provider.ts +636 -0
  800. package/extensions/openai/openai-codex-shared.ts +3 -0
  801. package/extensions/openai/openai-provider.live.test.ts +196 -0
  802. package/extensions/openai/openai-provider.test.ts +929 -0
  803. package/extensions/openai/openai-provider.ts +325 -0
  804. package/extensions/openai/openai-tts.live.test.ts +44 -0
  805. package/extensions/openai/openai.live.test.ts +493 -0
  806. package/extensions/openai/openclaw.plugin.json +897 -0
  807. package/extensions/openai/openclaw.plugin.test.ts +181 -0
  808. package/extensions/openai/package.json +19 -0
  809. package/extensions/openai/plugin-registration.contract.test.ts +9 -0
  810. package/extensions/openai/prompt-overlay.ts +51 -0
  811. package/extensions/openai/provider-auth.contract.test.ts +12 -0
  812. package/extensions/openai/provider-catalog.contract.test.ts +3 -0
  813. package/extensions/openai/provider-contract-api.ts +83 -0
  814. package/extensions/openai/provider-policy-api.ts +20 -0
  815. package/extensions/openai/provider-runtime.contract.test.ts +3 -0
  816. package/extensions/openai/realtime-provider-shared.ts +168 -0
  817. package/extensions/openai/realtime-transcription-provider.test.ts +356 -0
  818. package/extensions/openai/realtime-transcription-provider.ts +307 -0
  819. package/extensions/openai/realtime-voice-provider.test.ts +1924 -0
  820. package/extensions/openai/realtime-voice-provider.ts +1315 -0
  821. package/extensions/openai/register.runtime.ts +15 -0
  822. package/extensions/openai/replay-policy.ts +32 -0
  823. package/extensions/openai/setup-api.test.ts +29 -0
  824. package/extensions/openai/setup-api.ts +166 -0
  825. package/extensions/openai/shared.ts +131 -0
  826. package/extensions/openai/speech-provider.test.ts +324 -0
  827. package/extensions/openai/speech-provider.ts +347 -0
  828. package/extensions/openai/test-api.ts +9 -0
  829. package/extensions/openai/test-support/provider-catalog.contract-test-support.ts +134 -0
  830. package/extensions/openai/thinking-policy.ts +55 -0
  831. package/extensions/openai/transport-policy.test.ts +128 -0
  832. package/extensions/openai/transport-policy.ts +111 -0
  833. package/extensions/openai/tsconfig.json +16 -0
  834. package/extensions/openai/tts.test.ts +444 -0
  835. package/extensions/openai/tts.ts +184 -0
  836. package/extensions/openai/video-generation-provider.test.ts +254 -0
  837. package/extensions/openai/video-generation-provider.ts +382 -0
  838. package/extensions/opencode/api.ts +9 -0
  839. package/extensions/opencode/index.test.ts +84 -0
  840. package/extensions/opencode/index.ts +74 -0
  841. package/extensions/opencode/media-understanding-provider.test.ts +44 -0
  842. package/extensions/opencode/media-understanding-provider.ts +42 -0
  843. package/extensions/opencode/onboard.test.ts +25 -0
  844. package/extensions/opencode/onboard.ts +29 -0
  845. package/extensions/opencode/openclaw.plugin.json +55 -0
  846. package/extensions/opencode/package.json +15 -0
  847. package/extensions/opencode/plugin-registration.contract.test.ts +8 -0
  848. package/extensions/opencode/provider-policy-api.test.ts +44 -0
  849. package/extensions/opencode/provider-policy-api.ts +5 -0
  850. package/extensions/opencode/tsconfig.json +16 -0
  851. package/extensions/opencode-go/api.ts +27 -0
  852. package/extensions/opencode-go/index.test.ts +305 -0
  853. package/extensions/opencode-go/index.ts +101 -0
  854. package/extensions/opencode-go/media-understanding-provider.test.ts +12 -0
  855. package/extensions/opencode-go/media-understanding-provider.ts +15 -0
  856. package/extensions/opencode-go/onboard.test.ts +28 -0
  857. package/extensions/opencode-go/onboard.ts +17 -0
  858. package/extensions/opencode-go/openclaw.plugin.json +106 -0
  859. package/extensions/opencode-go/package.json +15 -0
  860. package/extensions/opencode-go/plugin-registration.contract.test.ts +8 -0
  861. package/extensions/opencode-go/provider-catalog.ts +135 -0
  862. package/extensions/opencode-go/stream.ts +51 -0
  863. package/extensions/opencode-go/tsconfig.json +16 -0
  864. package/extensions/openrouter/api.ts +12 -0
  865. package/extensions/openrouter/image-generation-provider.test.ts +361 -0
  866. package/extensions/openrouter/image-generation-provider.ts +345 -0
  867. package/extensions/openrouter/index.test.ts +650 -0
  868. package/extensions/openrouter/index.ts +184 -0
  869. package/extensions/openrouter/media-understanding-provider.test.ts +260 -0
  870. package/extensions/openrouter/media-understanding-provider.ts +176 -0
  871. package/extensions/openrouter/models.ts +18 -0
  872. package/extensions/openrouter/music-generation-provider.test.ts +226 -0
  873. package/extensions/openrouter/music-generation-provider.ts +344 -0
  874. package/extensions/openrouter/onboard.test.ts +27 -0
  875. package/extensions/openrouter/onboard.ts +32 -0
  876. package/extensions/openrouter/openclaw.plugin.json +81 -0
  877. package/extensions/openrouter/openrouter.live.test.ts +118 -0
  878. package/extensions/openrouter/package.json +15 -0
  879. package/extensions/openrouter/provider-catalog.ts +88 -0
  880. package/extensions/openrouter/provider-contract-api.ts +27 -0
  881. package/extensions/openrouter/provider-policy-api.ts +5 -0
  882. package/extensions/openrouter/provider-routing.ts +87 -0
  883. package/extensions/openrouter/provider-runtime.contract.test.ts +3 -0
  884. package/extensions/openrouter/speech-provider.test.ts +218 -0
  885. package/extensions/openrouter/speech-provider.ts +46 -0
  886. package/extensions/openrouter/stream.ts +247 -0
  887. package/extensions/openrouter/test-api.ts +4 -0
  888. package/extensions/openrouter/thinking-policy.ts +34 -0
  889. package/extensions/openrouter/tsconfig.json +16 -0
  890. package/extensions/openrouter/video-generation-provider.test.ts +722 -0
  891. package/extensions/openrouter/video-generation-provider.ts +530 -0
  892. package/extensions/openrouter/video-http.ts +48 -0
  893. package/extensions/openrouter/video-model-catalog.ts +299 -0
  894. package/extensions/openshell/index.ts +28 -0
  895. package/extensions/openshell/npm-shrinkwrap.json +24 -0
  896. package/extensions/openshell/openclaw.plugin.json +118 -0
  897. package/extensions/openshell/package.json +37 -0
  898. package/extensions/openshell/src/backend.e2e.test.ts +595 -0
  899. package/extensions/openshell/src/backend.test.ts +40 -0
  900. package/extensions/openshell/src/backend.ts +512 -0
  901. package/extensions/openshell/src/backend.types.ts +11 -0
  902. package/extensions/openshell/src/cli.ts +85 -0
  903. package/extensions/openshell/src/config.test.ts +80 -0
  904. package/extensions/openshell/src/config.ts +194 -0
  905. package/extensions/openshell/src/fs-bridge.ts +370 -0
  906. package/extensions/openshell/src/mirror.test.ts +194 -0
  907. package/extensions/openshell/src/mirror.ts +141 -0
  908. package/extensions/openshell/src/openshell-core.test.ts +529 -0
  909. package/extensions/openshell/tsconfig.json +16 -0
  910. package/extensions/perplexity/index.ts +11 -0
  911. package/extensions/perplexity/openclaw.plugin.json +52 -0
  912. package/extensions/perplexity/package.json +15 -0
  913. package/extensions/perplexity/src/perplexity-web-search-provider.runtime.ts +551 -0
  914. package/extensions/perplexity/src/perplexity-web-search-provider.shared.ts +124 -0
  915. package/extensions/perplexity/src/perplexity-web-search-provider.test.ts +151 -0
  916. package/extensions/perplexity/src/perplexity-web-search-provider.ts +127 -0
  917. package/extensions/perplexity/test-api.ts +1 -0
  918. package/extensions/perplexity/tsconfig.json +16 -0
  919. package/extensions/perplexity/web-search-contract-api.ts +13 -0
  920. package/extensions/perplexity/web-search-provider.ts +1 -0
  921. package/extensions/qianfan/api.ts +6 -0
  922. package/extensions/qianfan/index.test.ts +133 -0
  923. package/extensions/qianfan/index.ts +31 -0
  924. package/extensions/qianfan/onboard.ts +61 -0
  925. package/extensions/qianfan/openclaw.plugin.json +78 -0
  926. package/extensions/qianfan/package.json +15 -0
  927. package/extensions/qianfan/provider-catalog.ts +13 -0
  928. package/extensions/qianfan/tsconfig.json +16 -0
  929. package/extensions/qwen/api.ts +34 -0
  930. package/extensions/qwen/index.test.ts +31 -0
  931. package/extensions/qwen/index.ts +181 -0
  932. package/extensions/qwen/media-understanding-provider.test.ts +76 -0
  933. package/extensions/qwen/media-understanding-provider.ts +88 -0
  934. package/extensions/qwen/model-definitions.ts +20 -0
  935. package/extensions/qwen/models.ts +202 -0
  936. package/extensions/qwen/onboard.ts +73 -0
  937. package/extensions/qwen/openclaw.plugin.json +143 -0
  938. package/extensions/qwen/package.json +15 -0
  939. package/extensions/qwen/plugin-registration.contract.test.ts +10 -0
  940. package/extensions/qwen/provider-catalog.test.ts +62 -0
  941. package/extensions/qwen/provider-catalog.ts +13 -0
  942. package/extensions/qwen/provider-discovery.contract.test.ts +3 -0
  943. package/extensions/qwen/stream.test.ts +171 -0
  944. package/extensions/qwen/stream.ts +87 -0
  945. package/extensions/qwen/test-api.ts +2 -0
  946. package/extensions/qwen/tsconfig.json +16 -0
  947. package/extensions/qwen/video-generation-provider.test.ts +155 -0
  948. package/extensions/qwen/video-generation-provider.ts +111 -0
  949. package/extensions/runway/index.ts +11 -0
  950. package/extensions/runway/openclaw.plugin.json +34 -0
  951. package/extensions/runway/package.json +15 -0
  952. package/extensions/runway/plugin-registration.contract.test.ts +7 -0
  953. package/extensions/runway/tsconfig.json +16 -0
  954. package/extensions/runway/video-generation-provider.test.ts +248 -0
  955. package/extensions/runway/video-generation-provider.ts +462 -0
  956. package/extensions/senseaudio/index.ts +11 -0
  957. package/extensions/senseaudio/media-understanding-provider.test.ts +136 -0
  958. package/extensions/senseaudio/media-understanding-provider.ts +25 -0
  959. package/extensions/senseaudio/openclaw.plugin.json +18 -0
  960. package/extensions/senseaudio/package.json +15 -0
  961. package/extensions/senseaudio/test-api.ts +1 -0
  962. package/extensions/sglang/README.md +3 -0
  963. package/extensions/sglang/api.ts +7 -0
  964. package/extensions/sglang/defaults.ts +4 -0
  965. package/extensions/sglang/index.test.ts +34 -0
  966. package/extensions/sglang/index.ts +95 -0
  967. package/extensions/sglang/models.ts +23 -0
  968. package/extensions/sglang/openclaw.plugin.json +45 -0
  969. package/extensions/sglang/package.json +15 -0
  970. package/extensions/sglang/provider-discovery.contract.test.ts +7 -0
  971. package/extensions/sglang/tsconfig.json +16 -0
  972. package/extensions/skill-workshop/api.ts +3 -0
  973. package/extensions/skill-workshop/index.test.ts +990 -0
  974. package/extensions/skill-workshop/index.ts +170 -0
  975. package/extensions/skill-workshop/openclaw.plugin.json +83 -0
  976. package/extensions/skill-workshop/package.json +18 -0
  977. package/extensions/skill-workshop/src/config.ts +50 -0
  978. package/extensions/skill-workshop/src/prompt.ts +18 -0
  979. package/extensions/skill-workshop/src/reviewer.ts +290 -0
  980. package/extensions/skill-workshop/src/scanner.ts +69 -0
  981. package/extensions/skill-workshop/src/signals.ts +95 -0
  982. package/extensions/skill-workshop/src/skills.ts +186 -0
  983. package/extensions/skill-workshop/src/store.ts +184 -0
  984. package/extensions/skill-workshop/src/text.ts +59 -0
  985. package/extensions/skill-workshop/src/tool.ts +200 -0
  986. package/extensions/skill-workshop/src/types.ts +42 -0
  987. package/extensions/skill-workshop/src/workshop.ts +85 -0
  988. package/extensions/speech-core/api.ts +54 -0
  989. package/extensions/speech-core/package.json +10 -0
  990. package/extensions/speech-core/runtime-api.ts +42 -0
  991. package/extensions/speech-core/src/tts.test.ts +1025 -0
  992. package/extensions/speech-core/src/tts.ts +1929 -0
  993. package/extensions/speech-core/tsconfig.json +16 -0
  994. package/extensions/stepfun/index.ts +252 -0
  995. package/extensions/stepfun/onboard.ts +73 -0
  996. package/extensions/stepfun/openclaw.plugin.json +148 -0
  997. package/extensions/stepfun/package.json +15 -0
  998. package/extensions/stepfun/provider-catalog.ts +40 -0
  999. package/extensions/stepfun/tsconfig.json +16 -0
  1000. package/extensions/tencent/api.ts +7 -0
  1001. package/extensions/tencent/index.ts +64 -0
  1002. package/extensions/tencent/models.ts +25 -0
  1003. package/extensions/tencent/onboard.ts +38 -0
  1004. package/extensions/tencent/openclaw.plugin.json +86 -0
  1005. package/extensions/tencent/package.json +15 -0
  1006. package/extensions/tencent/provider-catalog.ts +14 -0
  1007. package/extensions/tencent/provider-discovery.ts +17 -0
  1008. package/extensions/tencent/tsconfig.json +16 -0
  1009. package/extensions/together/api.ts +7 -0
  1010. package/extensions/together/index.ts +42 -0
  1011. package/extensions/together/models.ts +23 -0
  1012. package/extensions/together/onboard.ts +26 -0
  1013. package/extensions/together/openclaw.plugin.json +160 -0
  1014. package/extensions/together/package.json +15 -0
  1015. package/extensions/together/plugin-registration.contract.test.ts +8 -0
  1016. package/extensions/together/provider-catalog.ts +10 -0
  1017. package/extensions/together/tsconfig.json +16 -0
  1018. package/extensions/together/video-generation-provider.test.ts +130 -0
  1019. package/extensions/together/video-generation-provider.ts +281 -0
  1020. package/extensions/tts-local-cli/index.ts +11 -0
  1021. package/extensions/tts-local-cli/openclaw.plugin.json +15 -0
  1022. package/extensions/tts-local-cli/package.json +15 -0
  1023. package/extensions/tts-local-cli/speech-provider.test.ts +307 -0
  1024. package/extensions/tts-local-cli/speech-provider.ts +455 -0
  1025. package/extensions/venice/api.ts +8 -0
  1026. package/extensions/venice/index.test.ts +109 -0
  1027. package/extensions/venice/index.ts +70 -0
  1028. package/extensions/venice/models.test.ts +291 -0
  1029. package/extensions/venice/models.ts +302 -0
  1030. package/extensions/venice/onboard.ts +27 -0
  1031. package/extensions/venice/openclaw.plugin.json +504 -0
  1032. package/extensions/venice/package.json +15 -0
  1033. package/extensions/venice/provider-catalog.ts +11 -0
  1034. package/extensions/venice/provider-runtime.contract.test.ts +3 -0
  1035. package/extensions/venice/stream.ts +37 -0
  1036. package/extensions/venice/tsconfig.json +16 -0
  1037. package/extensions/vercel-ai-gateway/api.ts +12 -0
  1038. package/extensions/vercel-ai-gateway/index.ts +41 -0
  1039. package/extensions/vercel-ai-gateway/models.ts +226 -0
  1040. package/extensions/vercel-ai-gateway/onboard.ts +32 -0
  1041. package/extensions/vercel-ai-gateway/openclaw.plugin.json +61 -0
  1042. package/extensions/vercel-ai-gateway/package.json +15 -0
  1043. package/extensions/vercel-ai-gateway/provider-catalog.test.ts +96 -0
  1044. package/extensions/vercel-ai-gateway/provider-catalog.ts +22 -0
  1045. package/extensions/vercel-ai-gateway/thinking.test.ts +100 -0
  1046. package/extensions/vercel-ai-gateway/thinking.ts +77 -0
  1047. package/extensions/vercel-ai-gateway/tsconfig.json +16 -0
  1048. package/extensions/vllm/README.md +3 -0
  1049. package/extensions/vllm/api.ts +8 -0
  1050. package/extensions/vllm/defaults.ts +4 -0
  1051. package/extensions/vllm/index.ts +96 -0
  1052. package/extensions/vllm/models.ts +23 -0
  1053. package/extensions/vllm/openclaw.plugin.json +45 -0
  1054. package/extensions/vllm/package.json +15 -0
  1055. package/extensions/vllm/provider-discovery.contract.test.ts +7 -0
  1056. package/extensions/vllm/register.runtime.ts +7 -0
  1057. package/extensions/vllm/stream.test.ts +282 -0
  1058. package/extensions/vllm/stream.ts +164 -0
  1059. package/extensions/vllm/tsconfig.json +16 -0
  1060. package/extensions/volcengine/api.ts +56 -0
  1061. package/extensions/volcengine/index.test.ts +92 -0
  1062. package/extensions/volcengine/index.ts +87 -0
  1063. package/extensions/volcengine/models.ts +28 -0
  1064. package/extensions/volcengine/openclaw.plugin.json +221 -0
  1065. package/extensions/volcengine/package.json +15 -0
  1066. package/extensions/volcengine/provider-catalog.ts +17 -0
  1067. package/extensions/volcengine/provider-discovery.ts +31 -0
  1068. package/extensions/volcengine/speech-provider.ts +229 -0
  1069. package/extensions/volcengine/tsconfig.json +16 -0
  1070. package/extensions/volcengine/tts.live.test.ts +30 -0
  1071. package/extensions/volcengine/tts.test.ts +279 -0
  1072. package/extensions/volcengine/tts.ts +266 -0
  1073. package/extensions/voyage/embedding-batch.ts +315 -0
  1074. package/extensions/voyage/embedding-provider.ts +90 -0
  1075. package/extensions/voyage/index.ts +11 -0
  1076. package/extensions/voyage/memory-embedding-adapter.ts +56 -0
  1077. package/extensions/voyage/openclaw.plugin.json +18 -0
  1078. package/extensions/voyage/package.json +15 -0
  1079. package/extensions/xai/.boundary-stubs/anthropic-vertex-api.d.ts +2 -0
  1080. package/extensions/xai/.boundary-stubs/ollama-api.d.ts +1 -0
  1081. package/extensions/xai/.boundary-stubs/ollama-runtime-api.d.ts +16 -0
  1082. package/extensions/xai/.boundary-stubs/speech-core-runtime-api.d.ts +33 -0
  1083. package/extensions/xai/api.test.ts +51 -0
  1084. package/extensions/xai/api.ts +119 -0
  1085. package/extensions/xai/code-execution.test.ts +262 -0
  1086. package/extensions/xai/code-execution.ts +146 -0
  1087. package/extensions/xai/image-generation-provider.test.ts +293 -0
  1088. package/extensions/xai/image-generation-provider.ts +124 -0
  1089. package/extensions/xai/index.test.ts +263 -0
  1090. package/extensions/xai/index.ts +233 -0
  1091. package/extensions/xai/model-compat.ts +34 -0
  1092. package/extensions/xai/model-definitions.ts +346 -0
  1093. package/extensions/xai/model-id.test.ts +32 -0
  1094. package/extensions/xai/model-id.ts +24 -0
  1095. package/extensions/xai/onboard.test.ts +91 -0
  1096. package/extensions/xai/onboard.ts +56 -0
  1097. package/extensions/xai/openclaw.plugin.json +274 -0
  1098. package/extensions/xai/package.json +20 -0
  1099. package/extensions/xai/plugin-registration.contract.test.ts +11 -0
  1100. package/extensions/xai/provider-catalog.ts +12 -0
  1101. package/extensions/xai/provider-contract-api.ts +22 -0
  1102. package/extensions/xai/provider-discovery.ts +27 -0
  1103. package/extensions/xai/provider-models.ts +45 -0
  1104. package/extensions/xai/provider-policy-api.test.ts +37 -0
  1105. package/extensions/xai/provider-policy-api.ts +18 -0
  1106. package/extensions/xai/realtime-transcription-provider.test.ts +273 -0
  1107. package/extensions/xai/realtime-transcription-provider.ts +306 -0
  1108. package/extensions/xai/runtime-model-compat.test.ts +60 -0
  1109. package/extensions/xai/runtime-model-compat.ts +72 -0
  1110. package/extensions/xai/setup-api.ts +22 -0
  1111. package/extensions/xai/speech-provider.test.ts +184 -0
  1112. package/extensions/xai/speech-provider.ts +275 -0
  1113. package/extensions/xai/src/code-execution-shared.ts +110 -0
  1114. package/extensions/xai/src/responses-tool-shared.test.ts +107 -0
  1115. package/extensions/xai/src/responses-tool-shared.ts +163 -0
  1116. package/extensions/xai/src/tool-auth-shared.test.ts +326 -0
  1117. package/extensions/xai/src/tool-auth-shared.ts +219 -0
  1118. package/extensions/xai/src/tool-config-shared.test.ts +36 -0
  1119. package/extensions/xai/src/tool-config-shared.ts +32 -0
  1120. package/extensions/xai/src/web-search-provider.runtime.ts +429 -0
  1121. package/extensions/xai/src/web-search-response.types.ts +25 -0
  1122. package/extensions/xai/src/web-search-shared.ts +124 -0
  1123. package/extensions/xai/src/x-search-config.ts +78 -0
  1124. package/extensions/xai/src/x-search-shared.ts +146 -0
  1125. package/extensions/xai/src/xai-user-agent.test.ts +59 -0
  1126. package/extensions/xai/src/xai-user-agent.ts +52 -0
  1127. package/extensions/xai/stream.test.ts +410 -0
  1128. package/extensions/xai/stream.ts +359 -0
  1129. package/extensions/xai/stt.test.ts +106 -0
  1130. package/extensions/xai/stt.ts +91 -0
  1131. package/extensions/xai/test-api.ts +1 -0
  1132. package/extensions/xai/test-helpers.ts +73 -0
  1133. package/extensions/xai/tsconfig.json +64 -0
  1134. package/extensions/xai/tts.test.ts +125 -0
  1135. package/extensions/xai/tts.ts +97 -0
  1136. package/extensions/xai/video-generation-provider.test.ts +443 -0
  1137. package/extensions/xai/video-generation-provider.ts +499 -0
  1138. package/extensions/xai/web-search-contract-api.ts +29 -0
  1139. package/extensions/xai/web-search.test.ts +1242 -0
  1140. package/extensions/xai/web-search.ts +68 -0
  1141. package/extensions/xai/x-search-tool-shared.ts +48 -0
  1142. package/extensions/xai/x-search.live.test.ts +76 -0
  1143. package/extensions/xai/x-search.test.ts +484 -0
  1144. package/extensions/xai/x-search.ts +230 -0
  1145. package/extensions/xai/xai-oauth.test.ts +387 -0
  1146. package/extensions/xai/xai-oauth.ts +752 -0
  1147. package/extensions/xai/xai.live.test.ts +323 -0
  1148. package/launcher.js +97 -0
  1149. package/logger.js +87 -0
  1150. package/package.json +21 -0
  1151. package/server.js +1800 -0
  1152. package/skills/ai-error-prevention/SKILL.md +105 -0
  1153. package/skills/api-design/SKILL.md +523 -0
  1154. package/skills/architecture-decision-records/SKILL.md +179 -0
  1155. package/skills/autonomous-loops/SKILL.md +610 -0
  1156. package/skills/backend-patterns/SKILL.md +598 -0
  1157. package/skills/codebase-onboarding/SKILL.md +233 -0
  1158. package/skills/coding-standards/SKILL.md +530 -0
  1159. package/skills/database-migrations/SKILL.md +429 -0
  1160. package/skills/deep-research/SKILL.md +155 -0
  1161. package/skills/error-prevention/SKILL.md +61 -0
  1162. package/skills/exa-search/SKILL.md +103 -0
  1163. package/skills/frontend-slides/SKILL.md +184 -0
  1164. package/skills/frontend-slides/STYLE_PRESETS.md +330 -0
  1165. package/skills/git-workflow/SKILL.md +715 -0
  1166. package/skills/iterative-retrieval/SKILL.md +211 -0
  1167. package/skills/php-security/SKILL.md +70 -0
  1168. package/skills/php-security/rules/thinkphp-security.rules +23 -0
  1169. package/skills/requirement-ears/SKILL.md +31 -0
  1170. package/skills/rules-distill/SKILL.md +264 -0
  1171. package/skills/rules-distill/scripts/scan-rules.sh +58 -0
  1172. package/skills/rules-distill/scripts/scan-skills.sh +129 -0
  1173. package/skills/search-first/SKILL.md +161 -0
  1174. package/skills/security-review/SKILL.md +495 -0
  1175. package/skills/security-review/cloud-infrastructure-security.md +361 -0
  1176. package/skills/security-scan/SKILL.md +68 -0
  1177. package/skills/security-scan/scripts/scan-config.ps1 +31 -0
  1178. package/skills/security-scan/scripts/scan-sqli.ps1 +21 -0
  1179. package/skills/skill-stocktake/SKILL.md +193 -0
  1180. package/skills/skill-stocktake/scripts/quick-diff.sh +87 -0
  1181. package/skills/skill-stocktake/scripts/save-results.sh +56 -0
  1182. package/skills/skill-stocktake/scripts/scan.sh +170 -0
  1183. package/skills/strategic-compact/SKILL.md +131 -0
  1184. package/skills/strategic-compact/suggest-compact.sh +54 -0
  1185. package/skills/tdd-workflow/SKILL.md +90 -0
  1186. package/skills/tdd-workflow/examples/IntegrationTestExample.php +35 -0
  1187. package/skills/tdd-workflow/examples/UnitTestExample.php +39 -0
  1188. package/skills/ui-spec-guider/ui-spec-guider/SKILL.md +37 -0
  1189. package/skills/ui-spec-guider/ui-spec-guider.skill +0 -0
  1190. package/skills/verification-loop/SKILL.md +126 -0
  1191. package/start.bat +29 -0
@@ -0,0 +1,2218 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({
4
+ fetchWithSsrFGuardMock: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
8
+ fetchWithSsrFGuard: fetchWithSsrFGuardMock,
9
+ }));
10
+
11
+ import {
12
+ buildOllamaChatRequest,
13
+ createConfiguredOllamaCompatStreamWrapper,
14
+ createConfiguredOllamaStreamFn,
15
+ createOllamaStreamFn,
16
+ convertToOllamaMessages,
17
+ buildAssistantMessage,
18
+ parseNdjsonStream,
19
+ resolveOllamaBaseUrlForRun,
20
+ } from "./stream.js";
21
+
22
+ type GuardedFetchCall = {
23
+ url: string;
24
+ init?: RequestInit;
25
+ policy?: unknown;
26
+ signal?: AbortSignal;
27
+ timeoutMs?: number;
28
+ auditContext?: string;
29
+ };
30
+
31
+ function requireRecord(value: unknown, label: string): Record<string, unknown> {
32
+ if (!value || typeof value !== "object") {
33
+ throw new Error(`expected ${label}`);
34
+ }
35
+ return value as Record<string, unknown>;
36
+ }
37
+
38
+ function requireHeaders(value: unknown): Record<string, string> {
39
+ return requireRecord(value, "request headers") as Record<string, string>;
40
+ }
41
+
42
+ function expectToolCallContent(
43
+ value: unknown,
44
+ expected: { name: string; arguments: Record<string, unknown> },
45
+ ) {
46
+ const content = requireRecord(value, "tool call content");
47
+ expect(content.type).toBe("toolCall");
48
+ expect(content.name).toBe(expected.name);
49
+ expect(content.arguments).toEqual(expected.arguments);
50
+ }
51
+
52
+ function expectIteratorEvent(
53
+ value: unknown,
54
+ expected: { type?: string; delta?: string; content?: string; done: boolean },
55
+ ) {
56
+ const result = requireRecord(value, "iterator result");
57
+ expect(result.done).toBe(expected.done);
58
+ if (expected.type !== undefined) {
59
+ const event = requireRecord(result.value, "iterator result value");
60
+ expect(event.type).toBe(expected.type);
61
+ if (expected.delta !== undefined) {
62
+ expect(event.delta).toBe(expected.delta);
63
+ }
64
+ if (expected.content !== undefined) {
65
+ expect(event.content).toBe(expected.content);
66
+ }
67
+ } else {
68
+ expect(result.value).toBeUndefined();
69
+ }
70
+ }
71
+
72
+ afterEach(() => {
73
+ fetchWithSsrFGuardMock.mockReset();
74
+ });
75
+
76
+ describe("buildOllamaChatRequest", () => {
77
+ it("omits tools when none are provided", () => {
78
+ expect(
79
+ buildOllamaChatRequest({
80
+ modelId: "qwen3.5:9b",
81
+ messages: [{ role: "user", content: "hello" }],
82
+ options: { num_ctx: 65536 },
83
+ }),
84
+ ).toEqual({
85
+ model: "qwen3.5:9b",
86
+ messages: [{ role: "user", content: "hello" }],
87
+ stream: true,
88
+ options: { num_ctx: 65536 },
89
+ });
90
+ });
91
+
92
+ it("strips the ollama/ prefix from chat model ids", () => {
93
+ const request = buildOllamaChatRequest({
94
+ modelId: "ollama/qwen3:14b-q8_0",
95
+ messages: [{ role: "user", content: "hello" }],
96
+ });
97
+ expect(request.model).toBe("qwen3:14b-q8_0");
98
+ });
99
+
100
+ it("strips the active custom provider prefix from chat model ids", () => {
101
+ const request = buildOllamaChatRequest({
102
+ modelId: "ollama-spark/qwen3:32b",
103
+ providerId: "ollama-spark",
104
+ messages: [{ role: "user", content: "hello" }],
105
+ });
106
+ expect(request.model).toBe("qwen3:32b");
107
+ });
108
+
109
+ it("keeps unrelated slash-containing Ollama model ids intact", () => {
110
+ const request = buildOllamaChatRequest({
111
+ modelId: "library/qwen3:32b",
112
+ providerId: "ollama-spark",
113
+ messages: [{ role: "user", content: "hello" }],
114
+ });
115
+ expect(request.model).toBe("library/qwen3:32b");
116
+ });
117
+ });
118
+
119
+ describe("createConfiguredOllamaCompatStreamWrapper", () => {
120
+ it("adds Moonshot thinking config for Ollama cloud Kimi compat requests", async () => {
121
+ let patchedPayload: Record<string, unknown> | undefined;
122
+ const baseStreamFn = vi.fn((_model, _context, options) => {
123
+ options?.onPayload?.({ tool_choice: "auto" });
124
+ return (async function* () {})();
125
+ });
126
+ const model = {
127
+ api: "openai-completions",
128
+ provider: "ollama",
129
+ id: "kimi-k2.5:cloud",
130
+ contextWindow: 262144,
131
+ params: { num_ctx: 65536 },
132
+ };
133
+
134
+ const wrapped = createConfiguredOllamaCompatStreamWrapper({
135
+ provider: "ollama",
136
+ modelId: "kimi-k2.5:cloud",
137
+ model,
138
+ streamFn: baseStreamFn,
139
+ thinkingLevel: "high",
140
+ extraParams: {},
141
+ } as never);
142
+
143
+ await wrapped?.(
144
+ model as never,
145
+ { messages: [] } as never,
146
+ {
147
+ onPayload: (payload: unknown) => {
148
+ patchedPayload = payload as Record<string, unknown>;
149
+ },
150
+ } as never,
151
+ );
152
+
153
+ const payload = requireRecord(patchedPayload, "patched payload");
154
+ expect(payload.thinking).toEqual({ type: "enabled" });
155
+ expect(payload.options).toEqual({ num_ctx: 65536 });
156
+ });
157
+
158
+ it("falls back to contextWindow when configured num_ctx is invalid", async () => {
159
+ let patchedPayload: Record<string, unknown> | undefined;
160
+ const baseStreamFn = vi.fn((_model, _context, options) => {
161
+ options?.onPayload?.({});
162
+ return (async function* () {})();
163
+ });
164
+ const model = {
165
+ api: "openai-completions",
166
+ provider: "ollama",
167
+ id: "qwen3:32b",
168
+ contextWindow: 131072,
169
+ params: { num_ctx: 0 },
170
+ };
171
+
172
+ const wrapped = createConfiguredOllamaCompatStreamWrapper({
173
+ provider: "ollama",
174
+ modelId: "qwen3:32b",
175
+ model,
176
+ streamFn: baseStreamFn,
177
+ } as never);
178
+
179
+ await wrapped?.(
180
+ model as never,
181
+ { messages: [] } as never,
182
+ {
183
+ onPayload: (payload: unknown) => {
184
+ patchedPayload = payload as Record<string, unknown>;
185
+ },
186
+ } as never,
187
+ );
188
+
189
+ const payload = requireRecord(patchedPayload, "patched payload");
190
+ expect(payload.options).toEqual({ num_ctx: 131072 });
191
+ });
192
+
193
+ it("forwards think=false on native Ollama chat requests when thinking is off", async () => {
194
+ await withMockNdjsonFetch(
195
+ [
196
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
197
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
198
+ ],
199
+ async (fetchMock) => {
200
+ const baseStreamFn = createOllamaStreamFn("http://ollama-host:11434");
201
+ const model = {
202
+ api: "ollama",
203
+ provider: "ollama",
204
+ id: "qwen3:32b",
205
+ contextWindow: 131072,
206
+ };
207
+
208
+ const wrapped = createConfiguredOllamaCompatStreamWrapper({
209
+ provider: "ollama",
210
+ modelId: "qwen3:32b",
211
+ model,
212
+ streamFn: baseStreamFn,
213
+ thinkingLevel: "off",
214
+ } as never);
215
+ if (!wrapped) {
216
+ throw new Error("Expected wrapped Ollama stream function");
217
+ }
218
+
219
+ const stream = await Promise.resolve(
220
+ wrapped(
221
+ model as never,
222
+ {
223
+ messages: [{ role: "user", content: "hello" }],
224
+ } as never,
225
+ {} as never,
226
+ ),
227
+ );
228
+
229
+ await collectStreamEvents(stream);
230
+
231
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
232
+ if (typeof requestInit.body !== "string") {
233
+ throw new Error("Expected string request body");
234
+ }
235
+ const requestBody = JSON.parse(requestInit.body) as {
236
+ think?: boolean;
237
+ options?: { think?: boolean; num_ctx?: number };
238
+ };
239
+ expect(requestBody.think).toBe(false);
240
+ expect(requestBody.options?.think).toBeUndefined();
241
+ expect(requestBody.options?.num_ctx).toBeUndefined();
242
+ },
243
+ );
244
+ });
245
+
246
+ it("does not overwrite configured native Ollama params.thinking with implicit off", async () => {
247
+ await withMockNdjsonFetch(
248
+ [
249
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
250
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
251
+ ],
252
+ async (fetchMock) => {
253
+ const baseStreamFn = createOllamaStreamFn("http://ollama-host:11434");
254
+ const model = {
255
+ api: "ollama",
256
+ provider: "ollama",
257
+ id: "qwen3:32b",
258
+ contextWindow: 131072,
259
+ params: { thinking: "medium" },
260
+ };
261
+
262
+ const wrapped = createConfiguredOllamaCompatStreamWrapper({
263
+ provider: "ollama",
264
+ modelId: "qwen3:32b",
265
+ model,
266
+ streamFn: baseStreamFn,
267
+ thinkingLevel: "off",
268
+ } as never);
269
+ if (!wrapped) {
270
+ throw new Error("Expected wrapped Ollama stream function");
271
+ }
272
+
273
+ const stream = await Promise.resolve(
274
+ wrapped(
275
+ model as never,
276
+ {
277
+ messages: [{ role: "user", content: "hello" }],
278
+ } as never,
279
+ {} as never,
280
+ ),
281
+ );
282
+
283
+ await collectStreamEvents(stream);
284
+
285
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
286
+ if (typeof requestInit.body !== "string") {
287
+ throw new Error("Expected string request body");
288
+ }
289
+ const requestBody = JSON.parse(requestInit.body) as { think?: string };
290
+ expect(requestBody.think).toBe("medium");
291
+ },
292
+ );
293
+ });
294
+
295
+ it("does not forward truthy configured native Ollama thinking for non-reasoning models", async () => {
296
+ await withMockNdjsonFetch(
297
+ [
298
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
299
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
300
+ ],
301
+ async (fetchMock) => {
302
+ const baseStreamFn = createOllamaStreamFn("http://ollama-host:11434");
303
+ const model = {
304
+ api: "ollama",
305
+ provider: "ollama",
306
+ id: "llama3.2:latest",
307
+ contextWindow: 8192,
308
+ reasoning: false,
309
+ params: { thinking: "medium" },
310
+ };
311
+
312
+ const wrapped = createConfiguredOllamaCompatStreamWrapper({
313
+ provider: "ollama",
314
+ modelId: "llama3.2:latest",
315
+ model,
316
+ streamFn: baseStreamFn,
317
+ thinkingLevel: "off",
318
+ } as never);
319
+ if (!wrapped) {
320
+ throw new Error("Expected wrapped Ollama stream function");
321
+ }
322
+
323
+ const stream = await Promise.resolve(
324
+ wrapped(
325
+ model as never,
326
+ {
327
+ messages: [{ role: "user", content: "hello" }],
328
+ } as never,
329
+ {} as never,
330
+ ),
331
+ );
332
+
333
+ await collectStreamEvents(stream);
334
+
335
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
336
+ if (typeof requestInit.body !== "string") {
337
+ throw new Error("Expected string request body");
338
+ }
339
+ const requestBody = JSON.parse(requestInit.body) as {
340
+ think?: string;
341
+ options?: { think?: string };
342
+ };
343
+ expect(requestBody.think).toBeUndefined();
344
+ expect(requestBody.options?.think).toBeUndefined();
345
+ },
346
+ );
347
+ });
348
+
349
+ it("does not forward runtime native Ollama thinking for non-reasoning models", async () => {
350
+ await withMockNdjsonFetch(
351
+ [
352
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
353
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
354
+ ],
355
+ async (fetchMock) => {
356
+ const baseStreamFn = createOllamaStreamFn("http://ollama-host:11434");
357
+ const model = {
358
+ api: "ollama",
359
+ provider: "ollama",
360
+ id: "llama3.2:latest",
361
+ contextWindow: 8192,
362
+ reasoning: false,
363
+ };
364
+
365
+ const wrapped = createConfiguredOllamaCompatStreamWrapper({
366
+ provider: "ollama",
367
+ modelId: "llama3.2:latest",
368
+ model,
369
+ streamFn: baseStreamFn,
370
+ thinkingLevel: "low",
371
+ } as never);
372
+ if (!wrapped) {
373
+ throw new Error("Expected wrapped Ollama stream function");
374
+ }
375
+
376
+ const stream = await Promise.resolve(
377
+ wrapped(
378
+ model as never,
379
+ {
380
+ messages: [{ role: "user", content: "hello" }],
381
+ } as never,
382
+ {} as never,
383
+ ),
384
+ );
385
+
386
+ await collectStreamEvents(stream);
387
+
388
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
389
+ if (typeof requestInit.body !== "string") {
390
+ throw new Error("Expected string request body");
391
+ }
392
+ const requestBody = JSON.parse(requestInit.body) as {
393
+ think?: string;
394
+ options?: { think?: string };
395
+ };
396
+ expect(requestBody.think).toBeUndefined();
397
+ expect(requestBody.options?.think).toBeUndefined();
398
+ },
399
+ );
400
+ });
401
+
402
+ it("forwards the native think effort on native Ollama chat requests when thinking is enabled", async () => {
403
+ await withMockNdjsonFetch(
404
+ [
405
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
406
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
407
+ ],
408
+ async (fetchMock) => {
409
+ const baseStreamFn = createOllamaStreamFn("http://ollama-host:11434");
410
+ const model = {
411
+ api: "ollama",
412
+ provider: "ollama",
413
+ id: "qwen3:32b",
414
+ contextWindow: 131072,
415
+ };
416
+
417
+ const wrapped = createConfiguredOllamaCompatStreamWrapper({
418
+ provider: "ollama",
419
+ modelId: "qwen3:32b",
420
+ model,
421
+ streamFn: baseStreamFn,
422
+ thinkingLevel: "low",
423
+ } as never);
424
+ if (!wrapped) {
425
+ throw new Error("Expected wrapped Ollama stream function");
426
+ }
427
+
428
+ const stream = await Promise.resolve(
429
+ wrapped(
430
+ model as never,
431
+ {
432
+ messages: [{ role: "user", content: "hello" }],
433
+ } as never,
434
+ {} as never,
435
+ ),
436
+ );
437
+
438
+ await collectStreamEvents(stream);
439
+
440
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
441
+ if (typeof requestInit.body !== "string") {
442
+ throw new Error("Expected string request body");
443
+ }
444
+ const requestBody = JSON.parse(requestInit.body) as {
445
+ think?: boolean | string;
446
+ options?: { think?: boolean | string; num_ctx?: number };
447
+ };
448
+ expect(requestBody.think).toBe("low");
449
+ expect(requestBody.options?.think).toBeUndefined();
450
+ expect(requestBody.options?.num_ctx).toBeUndefined();
451
+ },
452
+ );
453
+ });
454
+
455
+ it("passes resolved provider request timeouts to native Ollama chat fetches", async () => {
456
+ await withMockNdjsonFetch(
457
+ [
458
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
459
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
460
+ ],
461
+ async (fetchMock) => {
462
+ const stream = await createOllamaTestStream({
463
+ baseUrl: "http://ollama-host:11434",
464
+ model: { requestTimeoutMs: 450_000 },
465
+ });
466
+
467
+ await collectStreamEvents(stream);
468
+
469
+ expect(getGuardedFetchCall(fetchMock).timeoutMs).toBe(450_000);
470
+ },
471
+ );
472
+ });
473
+
474
+ it("passes caller abort signals at guard level when a timeout is present", async () => {
475
+ await withMockNdjsonFetch(
476
+ [
477
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
478
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
479
+ ],
480
+ async (fetchMock) => {
481
+ const signal = new AbortController().signal;
482
+ const stream = await createOllamaTestStream({
483
+ baseUrl: "http://ollama-host:11434",
484
+ options: { signal, timeoutMs: 123_456 },
485
+ });
486
+
487
+ await collectStreamEvents(stream);
488
+
489
+ const request = getGuardedFetchCall(fetchMock);
490
+ expect(request.timeoutMs).toBe(123_456);
491
+ expect(request.signal).toBe(signal);
492
+ expect(request.init?.signal).toBeUndefined();
493
+ },
494
+ );
495
+ });
496
+
497
+ it("maps native Ollama max thinking to think=high on the wire", async () => {
498
+ await withMockNdjsonFetch(
499
+ [
500
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
501
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
502
+ ],
503
+ async (fetchMock) => {
504
+ const baseStreamFn = createOllamaStreamFn("http://ollama-host:11434");
505
+ const model = {
506
+ api: "ollama",
507
+ provider: "ollama",
508
+ id: "gpt-oss:20b",
509
+ contextWindow: 131072,
510
+ };
511
+
512
+ const wrapped = createConfiguredOllamaCompatStreamWrapper({
513
+ provider: "ollama",
514
+ modelId: "gpt-oss:20b",
515
+ model,
516
+ streamFn: baseStreamFn,
517
+ thinkingLevel: "max",
518
+ } as never);
519
+ if (!wrapped) {
520
+ throw new Error("Expected wrapped Ollama stream function");
521
+ }
522
+
523
+ const stream = await Promise.resolve(
524
+ wrapped(
525
+ model as never,
526
+ {
527
+ messages: [{ role: "user", content: "hello" }],
528
+ } as never,
529
+ {} as never,
530
+ ),
531
+ );
532
+
533
+ await collectStreamEvents(stream);
534
+
535
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
536
+ if (typeof requestInit.body !== "string") {
537
+ throw new Error("Expected string request body");
538
+ }
539
+ const requestBody = JSON.parse(requestInit.body) as {
540
+ think?: boolean | string;
541
+ options?: { think?: boolean | string; num_ctx?: number };
542
+ };
543
+ expect(requestBody.think).toBe("high");
544
+ expect(requestBody.options?.think).toBeUndefined();
545
+ expect(requestBody.options?.num_ctx).toBeUndefined();
546
+ },
547
+ );
548
+ });
549
+
550
+ it("sends custom-provider Ollama chat requests with the bare Ollama model id", async () => {
551
+ await withMockNdjsonFetch(
552
+ [
553
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
554
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
555
+ ],
556
+ async (fetchMock) => {
557
+ const streamFn = createOllamaStreamFn("http://ollama-host:11434");
558
+ const model = {
559
+ api: "ollama",
560
+ provider: "ollama-spark",
561
+ id: "ollama-spark/qwen3:32b",
562
+ contextWindow: 131072,
563
+ };
564
+
565
+ const stream = await Promise.resolve(
566
+ streamFn(
567
+ model as never,
568
+ {
569
+ messages: [{ role: "user", content: "hello" }],
570
+ } as never,
571
+ {} as never,
572
+ ),
573
+ );
574
+
575
+ await collectStreamEvents(stream);
576
+
577
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
578
+ if (typeof requestInit.body !== "string") {
579
+ throw new Error("Expected string request body");
580
+ }
581
+ const requestBody = JSON.parse(requestInit.body) as { model?: string };
582
+ expect(requestBody.model).toBe("qwen3:32b");
583
+ },
584
+ );
585
+ });
586
+
587
+ it("adds direct type hints to native Ollama tool schemas before sending them", async () => {
588
+ await withMockNdjsonFetch(
589
+ [
590
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
591
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
592
+ ],
593
+ async (fetchMock) => {
594
+ const streamFn = createOllamaStreamFn("http://ollama-host:11434");
595
+ const model = {
596
+ api: "ollama",
597
+ provider: "ollama",
598
+ id: "qwen3:32b",
599
+ contextWindow: 131072,
600
+ };
601
+
602
+ const stream = await Promise.resolve(
603
+ streamFn(
604
+ model as never,
605
+ {
606
+ messages: [{ role: "user", content: "hello" }],
607
+ tools: [
608
+ {
609
+ name: "search",
610
+ description: "search",
611
+ parameters: {
612
+ properties: {
613
+ query: {
614
+ anyOf: [{ type: "string" }, { type: "null" }],
615
+ },
616
+ tags: {
617
+ items: { type: "string" },
618
+ },
619
+ },
620
+ required: ["query"],
621
+ },
622
+ },
623
+ ],
624
+ } as never,
625
+ {} as never,
626
+ ),
627
+ );
628
+
629
+ await collectStreamEvents(stream);
630
+
631
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
632
+ if (typeof requestInit.body !== "string") {
633
+ throw new Error("Expected string request body");
634
+ }
635
+ const requestBody = JSON.parse(requestInit.body) as {
636
+ tools?: Array<{
637
+ function?: {
638
+ parameters?: {
639
+ type?: string;
640
+ properties?: Record<string, { type?: string }>;
641
+ };
642
+ };
643
+ }>;
644
+ };
645
+ const parameters = requestBody.tools?.[0]?.function?.parameters;
646
+ expect(parameters?.type).toBe("object");
647
+ expect(parameters?.properties?.query?.type).toBe("string");
648
+ expect(parameters?.properties?.tags?.type).toBe("array");
649
+ },
650
+ );
651
+ });
652
+ });
653
+
654
+ describe("convertToOllamaMessages", () => {
655
+ it("converts user text messages", () => {
656
+ const messages = [{ role: "user", content: "hello" }];
657
+ const result = convertToOllamaMessages(messages);
658
+ expect(result).toEqual([{ role: "user", content: "hello" }]);
659
+ });
660
+
661
+ it("converts user messages with content parts", () => {
662
+ const messages = [
663
+ {
664
+ role: "user",
665
+ content: [
666
+ { type: "text", text: "describe this" },
667
+ { type: "image", data: "base64data" },
668
+ ],
669
+ },
670
+ ];
671
+ const result = convertToOllamaMessages(messages);
672
+ expect(result).toEqual([{ role: "user", content: "describe this", images: ["base64data"] }]);
673
+ });
674
+
675
+ it("prepends system message when provided", () => {
676
+ const messages = [{ role: "user", content: "hello" }];
677
+ const result = convertToOllamaMessages(messages, "You are helpful.");
678
+ expect(result[0]).toEqual({ role: "system", content: "You are helpful." });
679
+ expect(result[1]).toEqual({ role: "user", content: "hello" });
680
+ });
681
+
682
+ it("converts assistant messages with toolCall content blocks", () => {
683
+ const messages = [
684
+ {
685
+ role: "assistant",
686
+ content: [
687
+ { type: "text", text: "Let me check." },
688
+ { type: "toolCall", id: "call_1", name: "bash", arguments: { command: "ls" } },
689
+ ],
690
+ },
691
+ ];
692
+ const result = convertToOllamaMessages(messages);
693
+ expect(result[0].role).toBe("assistant");
694
+ expect(result[0].content).toBe("Let me check.");
695
+ expect(result[0].tool_calls).toEqual([
696
+ { id: "call_1", function: { name: "bash", arguments: { command: "ls" } } },
697
+ ]);
698
+ });
699
+
700
+ it("preserves assistant tool-call ids before Ollama replay", () => {
701
+ const messages = [
702
+ {
703
+ role: "assistant",
704
+ content: [
705
+ {
706
+ type: "toolCall",
707
+ id: "fc_ollama_123",
708
+ name: "bash",
709
+ arguments: { command: "pwd" },
710
+ },
711
+ ],
712
+ },
713
+ ];
714
+ const result = convertToOllamaMessages(messages);
715
+ expect(result[0].tool_calls).toEqual([
716
+ { id: "fc_ollama_123", function: { name: "bash", arguments: { command: "pwd" } } },
717
+ ]);
718
+ });
719
+
720
+ it("normalizes provider-prefixed tool-call names before Ollama replay", () => {
721
+ const messages = [
722
+ {
723
+ role: "assistant",
724
+ content: [
725
+ { type: "toolCall", id: "call_1", name: "functions.exec", arguments: { command: "pwd" } },
726
+ { type: "tool_use", id: "call_2", name: "tools/read", input: { path: "README.md" } },
727
+ ],
728
+ },
729
+ ];
730
+ const result = convertToOllamaMessages(messages);
731
+ expect(result[0].tool_calls).toEqual([
732
+ { id: "call_1", function: { name: "exec", arguments: { command: "pwd" } } },
733
+ { id: "call_2", function: { name: "read", arguments: { path: "README.md" } } },
734
+ ]);
735
+ });
736
+
737
+ it("preserves exact allowlisted tool-prefix names before Ollama replay", () => {
738
+ const messages = [
739
+ {
740
+ role: "assistant",
741
+ content: [
742
+ { type: "toolCall", id: "call_1", name: "tool_a", arguments: { value: 1 } },
743
+ { type: "tool_use", id: "call_2", name: "tools_invoke_test", input: { value: 2 } },
744
+ { type: "toolCall", id: "call_3", name: "function-run", arguments: { value: 3 } },
745
+ ],
746
+ },
747
+ ];
748
+ const result = convertToOllamaMessages(messages, undefined, {
749
+ availableToolNames: new Set(["tool_a", "tools_invoke_test", "function-run"]),
750
+ });
751
+ expect(result[0].tool_calls).toEqual([
752
+ { id: "call_1", function: { name: "tool_a", arguments: { value: 1 } } },
753
+ { id: "call_2", function: { name: "tools_invoke_test", arguments: { value: 2 } } },
754
+ { id: "call_3", function: { name: "function-run", arguments: { value: 3 } } },
755
+ ]);
756
+ });
757
+
758
+ it("strips underscore and dash provider prefixes only when the suffix is allowlisted", () => {
759
+ const messages = [
760
+ {
761
+ role: "assistant",
762
+ content: [
763
+ { type: "toolCall", id: "call_1", name: "tools_exec", arguments: { command: "pwd" } },
764
+ { type: "tool_use", id: "call_2", name: "function-read", input: { path: "." } },
765
+ { type: "toolCall", id: "call_3", name: "tool_missing", arguments: {} },
766
+ ],
767
+ },
768
+ ];
769
+ const result = convertToOllamaMessages(messages, undefined, {
770
+ availableToolNames: new Set(["exec", "read"]),
771
+ });
772
+ expect(result[0].tool_calls).toEqual([
773
+ { id: "call_1", function: { name: "exec", arguments: { command: "pwd" } } },
774
+ { id: "call_2", function: { name: "read", arguments: { path: "." } } },
775
+ { id: "call_3", function: { name: "tool_missing", arguments: {} } },
776
+ ]);
777
+ });
778
+
779
+ it("keeps non-prefixed Ollama replay tool names intact", () => {
780
+ const messages = [
781
+ {
782
+ role: "assistant",
783
+ content: [
784
+ { type: "toolCall", id: "call_1", name: "functionshell", arguments: {} },
785
+ { type: "toolCall", id: "call_2", name: "tooling", arguments: {} },
786
+ { type: "toolCall", id: "call_3", name: "tools", arguments: {} },
787
+ { type: "toolCall", id: "call_4", name: "tool_a", arguments: {} },
788
+ ],
789
+ },
790
+ ];
791
+ const result = convertToOllamaMessages(messages);
792
+ expect(result[0].tool_calls).toEqual([
793
+ { id: "call_1", function: { name: "functionshell", arguments: {} } },
794
+ { id: "call_2", function: { name: "tooling", arguments: {} } },
795
+ { id: "call_3", function: { name: "tools", arguments: {} } },
796
+ { id: "call_4", function: { name: "tool_a", arguments: {} } },
797
+ ]);
798
+ });
799
+
800
+ it("deserializes string arguments back to objects for Ollama (round-trip fix)", () => {
801
+ // When tool calls round-trip through OpenAI-format storage, arguments
802
+ // are serialized as a JSON string. Ollama expects an object.
803
+ const messages = [
804
+ {
805
+ role: "assistant",
806
+ content: [
807
+ {
808
+ type: "toolCall",
809
+ id: "call_2",
810
+ name: "Read",
811
+ arguments: '{"file_path":"/tmp/test.txt"}',
812
+ },
813
+ ],
814
+ },
815
+ ];
816
+ const result = convertToOllamaMessages(messages);
817
+ expect(result[0].tool_calls).toEqual([
818
+ { id: "call_2", function: { name: "Read", arguments: { file_path: "/tmp/test.txt" } } },
819
+ ]);
820
+ });
821
+
822
+ it("handles tool_use blocks with string input (Anthropic format round-trip)", () => {
823
+ const messages = [
824
+ {
825
+ role: "assistant",
826
+ content: [
827
+ { type: "tool_use", id: "toolu_1", name: "exec", input: '{"command":"echo hello"}' },
828
+ ],
829
+ },
830
+ ];
831
+ const result = convertToOllamaMessages(messages);
832
+ expect(result[0].tool_calls).toEqual([
833
+ { id: "toolu_1", function: { name: "exec", arguments: { command: "echo hello" } } },
834
+ ]);
835
+ });
836
+
837
+ it("preserves unsafe integers as strings when replay args are deserialized", () => {
838
+ const messages = [
839
+ {
840
+ role: "assistant",
841
+ content: [
842
+ {
843
+ type: "toolCall",
844
+ id: "call_3",
845
+ name: "read",
846
+ arguments: '{"path":9223372036854775807,"nested":{"thread":1234567890123456789}}',
847
+ },
848
+ ],
849
+ },
850
+ ];
851
+ const result = convertToOllamaMessages(messages);
852
+ expect(result[0].tool_calls).toEqual([
853
+ {
854
+ id: "call_3",
855
+ function: {
856
+ name: "read",
857
+ arguments: {
858
+ path: "9223372036854775807",
859
+ nested: { thread: "1234567890123456789" },
860
+ },
861
+ },
862
+ },
863
+ ]);
864
+ });
865
+ it("converts tool result messages with 'tool' role", () => {
866
+ const messages = [{ role: "tool", content: "file1.txt\nfile2.txt" }];
867
+ const result = convertToOllamaMessages(messages);
868
+ expect(result).toEqual([{ role: "tool", content: "file1.txt\nfile2.txt" }]);
869
+ });
870
+
871
+ it("converts SDK 'toolResult' role to Ollama 'tool' role", () => {
872
+ const messages = [{ role: "toolResult", content: "command output here" }];
873
+ const result = convertToOllamaMessages(messages);
874
+ expect(result).toEqual([{ role: "tool", content: "command output here" }]);
875
+ });
876
+
877
+ it("includes tool_name from SDK toolResult messages", () => {
878
+ const messages = [{ role: "toolResult", content: "file contents here", toolName: "read" }];
879
+ const result = convertToOllamaMessages(messages);
880
+ expect(result).toEqual([{ role: "tool", content: "file contents here", tool_name: "read" }]);
881
+ });
882
+
883
+ it("omits tool_name when not provided in toolResult", () => {
884
+ const messages = [{ role: "toolResult", content: "output" }];
885
+ const result = convertToOllamaMessages(messages);
886
+ expect(result).toEqual([{ role: "tool", content: "output" }]);
887
+ expect(result[0]).not.toHaveProperty("tool_name");
888
+ });
889
+
890
+ it("handles empty messages array", () => {
891
+ const result = convertToOllamaMessages([]);
892
+ expect(result).toStrictEqual([]);
893
+ });
894
+ });
895
+
896
+ describe("buildAssistantMessage", () => {
897
+ const modelInfo = { api: "ollama", provider: "ollama", id: "qwen3:32b" };
898
+
899
+ it("builds text-only response", () => {
900
+ const response = {
901
+ model: "qwen3:32b",
902
+ created_at: "2026-01-01T00:00:00Z",
903
+ message: { role: "assistant" as const, content: "Hello!" },
904
+ done: true,
905
+ prompt_eval_count: 10,
906
+ eval_count: 5,
907
+ };
908
+ const result = buildAssistantMessage(response, modelInfo);
909
+ expect(result.role).toBe("assistant");
910
+ expect(result.content).toEqual([{ type: "text", text: "Hello!" }]);
911
+ expect(result.stopReason).toBe("stop");
912
+ expect(result.usage.input).toBe(10);
913
+ expect(result.usage.output).toBe(5);
914
+ expect(result.usage.totalTokens).toBe(15);
915
+ });
916
+
917
+ it("keeps thinking-only output when content is empty", () => {
918
+ const response = {
919
+ model: "qwen3:32b",
920
+ created_at: "2026-01-01T00:00:00Z",
921
+ message: {
922
+ role: "assistant" as const,
923
+ content: "",
924
+ thinking: "Thinking output",
925
+ },
926
+ done: true,
927
+ };
928
+ const result = buildAssistantMessage(response, modelInfo);
929
+ expect(result.stopReason).toBe("stop");
930
+ expect(result.content).toEqual([{ type: "thinking", thinking: "Thinking output" }]);
931
+ });
932
+
933
+ it("keeps reasoning-only output when content and thinking are empty", () => {
934
+ const response = {
935
+ model: "qwen3:32b",
936
+ created_at: "2026-01-01T00:00:00Z",
937
+ message: {
938
+ role: "assistant" as const,
939
+ content: "",
940
+ reasoning: "Reasoning output",
941
+ },
942
+ done: true,
943
+ };
944
+ const result = buildAssistantMessage(response, modelInfo);
945
+ expect(result.stopReason).toBe("stop");
946
+ expect(result.content).toEqual([{ type: "thinking", thinking: "Reasoning output" }]);
947
+ });
948
+
949
+ it("estimates usage when Ollama omits eval counters", () => {
950
+ const response = {
951
+ model: "qwen3:32b",
952
+ created_at: "2026-01-01T00:00:00Z",
953
+ message: { role: "assistant" as const, content: "Estimated output" },
954
+ done: true,
955
+ };
956
+ const result = buildAssistantMessage(response, modelInfo, { input: 11, output: 4 });
957
+ expect(result.usage.input).toBe(11);
958
+ expect(result.usage.output).toBe(4);
959
+ expect(result.usage.totalTokens).toBe(15);
960
+ });
961
+
962
+ it("preserves explicit zero usage counters from Ollama", () => {
963
+ const response = {
964
+ model: "qwen3:32b",
965
+ created_at: "2026-01-01T00:00:00Z",
966
+ message: { role: "assistant" as const, content: "" },
967
+ done: true,
968
+ prompt_eval_count: 0,
969
+ eval_count: 0,
970
+ };
971
+ const result = buildAssistantMessage(response, modelInfo, { input: 11, output: 4 });
972
+ expect(result.usage.input).toBe(0);
973
+ expect(result.usage.output).toBe(0);
974
+ expect(result.usage.totalTokens).toBe(0);
975
+ });
976
+
977
+ it("builds response with tool calls", () => {
978
+ const response = {
979
+ model: "qwen3:32b",
980
+ created_at: "2026-01-01T00:00:00Z",
981
+ message: {
982
+ role: "assistant" as const,
983
+ content: "",
984
+ tool_calls: [{ function: { name: "bash", arguments: { command: "ls -la" } } }],
985
+ },
986
+ done: true,
987
+ prompt_eval_count: 20,
988
+ eval_count: 10,
989
+ };
990
+ const result = buildAssistantMessage(response, modelInfo);
991
+ expect(result.stopReason).toBe("toolUse");
992
+ expect(result.content.length).toBe(1); // toolCall only (empty content is skipped)
993
+ expect(result.content[0].type).toBe("toolCall");
994
+ const toolCall = result.content[0] as {
995
+ type: "toolCall";
996
+ id: string;
997
+ name: string;
998
+ arguments: Record<string, unknown>;
999
+ };
1000
+ expect(toolCall.name).toBe("bash");
1001
+ expect(toolCall.arguments).toEqual({ command: "ls -la" });
1002
+ expect(toolCall.id).toMatch(/^ollama_call_[0-9a-f-]{36}$/);
1003
+ });
1004
+
1005
+ it("preserves Ollama response tool-call ids", () => {
1006
+ const response = {
1007
+ model: "gemini-3-flash-preview:cloud",
1008
+ created_at: "2026-01-01T00:00:00Z",
1009
+ message: {
1010
+ role: "assistant" as const,
1011
+ content: "",
1012
+ tool_calls: [
1013
+ { id: "fc_ollama_real_1", function: { name: "bash", arguments: { command: "pwd" } } },
1014
+ ],
1015
+ },
1016
+ done: true,
1017
+ };
1018
+ const result = buildAssistantMessage(response, modelInfo);
1019
+ expectToolCallContent(result.content[0], { name: "bash", arguments: { command: "pwd" } });
1020
+ expect((result.content[0] as { id?: string }).id).toBe("fc_ollama_real_1");
1021
+ });
1022
+
1023
+ it("preserves parallel Ollama response tool-call ids independently", () => {
1024
+ const response = {
1025
+ model: "gemini-3-flash-preview:cloud",
1026
+ created_at: "2026-01-01T00:00:00Z",
1027
+ message: {
1028
+ role: "assistant" as const,
1029
+ content: "",
1030
+ tool_calls: [
1031
+ { id: "fc_ollama_real_1", function: { name: "read", arguments: { path: "a.txt" } } },
1032
+ { id: "fc_ollama_real_2", function: { name: "exec", arguments: { command: "date" } } },
1033
+ ],
1034
+ },
1035
+ done: true,
1036
+ };
1037
+ const result = buildAssistantMessage(response, modelInfo);
1038
+ expect(result.content.map((part) => (part as { id?: string }).id)).toEqual([
1039
+ "fc_ollama_real_1",
1040
+ "fc_ollama_real_2",
1041
+ ]);
1042
+ expectToolCallContent(result.content[0], { name: "read", arguments: { path: "a.txt" } });
1043
+ expectToolCallContent(result.content[1], { name: "exec", arguments: { command: "date" } });
1044
+ });
1045
+
1046
+ it("normalizes provider-prefixed tool-call names in Ollama responses", () => {
1047
+ const response = {
1048
+ model: "qwen3:32b",
1049
+ created_at: "2026-01-01T00:00:00Z",
1050
+ message: {
1051
+ role: "assistant" as const,
1052
+ content: "",
1053
+ tool_calls: [
1054
+ { function: { name: "functions.exec", arguments: { command: "pwd" } } },
1055
+ { function: { name: "tools/read", arguments: { path: "README.md" } } },
1056
+ ],
1057
+ },
1058
+ done: true,
1059
+ };
1060
+ const result = buildAssistantMessage(response, modelInfo);
1061
+ expect(result.content).toHaveLength(2);
1062
+ expectToolCallContent(result.content[0], { name: "exec", arguments: { command: "pwd" } });
1063
+ expectToolCallContent(result.content[1], { name: "read", arguments: { path: "README.md" } });
1064
+ });
1065
+
1066
+ it("preserves exact allowlisted tool-prefix names in Ollama responses", () => {
1067
+ const response = {
1068
+ model: "qwen3:32b",
1069
+ created_at: "2026-01-01T00:00:00Z",
1070
+ message: {
1071
+ role: "assistant" as const,
1072
+ content: "",
1073
+ tool_calls: [
1074
+ { function: { name: "tool_a", arguments: { value: 1 } } },
1075
+ { function: { name: "tools_invoke_test", arguments: { value: 2 } } },
1076
+ { function: { name: "function-run", arguments: { value: 3 } } },
1077
+ ],
1078
+ },
1079
+ done: true,
1080
+ };
1081
+ const result = buildAssistantMessage(response, modelInfo, undefined, {
1082
+ availableToolNames: new Set(["tool_a", "tools_invoke_test", "function-run"]),
1083
+ });
1084
+ expect(result.content).toHaveLength(3);
1085
+ expectToolCallContent(result.content[0], { name: "tool_a", arguments: { value: 1 } });
1086
+ expectToolCallContent(result.content[1], {
1087
+ name: "tools_invoke_test",
1088
+ arguments: { value: 2 },
1089
+ });
1090
+ expectToolCallContent(result.content[2], { name: "function-run", arguments: { value: 3 } });
1091
+ });
1092
+
1093
+ it("keeps non-prefixed Ollama response tool names intact", () => {
1094
+ const response = {
1095
+ model: "qwen3:32b",
1096
+ created_at: "2026-01-01T00:00:00Z",
1097
+ message: {
1098
+ role: "assistant" as const,
1099
+ content: "",
1100
+ tool_calls: [
1101
+ { function: { name: "functionshell", arguments: {} } },
1102
+ { function: { name: "tooling", arguments: {} } },
1103
+ { function: { name: "tools", arguments: {} } },
1104
+ { function: { name: "tool_a", arguments: {} } },
1105
+ ],
1106
+ },
1107
+ done: true,
1108
+ };
1109
+ const result = buildAssistantMessage(response, modelInfo);
1110
+ expect(result.content).toHaveLength(4);
1111
+ expectToolCallContent(result.content[0], { name: "functionshell", arguments: {} });
1112
+ expectToolCallContent(result.content[1], { name: "tooling", arguments: {} });
1113
+ expectToolCallContent(result.content[2], { name: "tools", arguments: {} });
1114
+ expectToolCallContent(result.content[3], { name: "tool_a", arguments: {} });
1115
+ });
1116
+
1117
+ it("parses stringified tool call arguments from Ollama responses", () => {
1118
+ const response = {
1119
+ model: "qwen3:32b",
1120
+ created_at: "2026-01-01T00:00:00Z",
1121
+ message: {
1122
+ role: "assistant" as const,
1123
+ content: "",
1124
+ tool_calls: [{ function: { name: "bash", arguments: '{"command":"ls","path":"/tmp"}' } }],
1125
+ },
1126
+ done: true,
1127
+ };
1128
+ const result = buildAssistantMessage(response, modelInfo);
1129
+ expectToolCallContent(result.content[0], {
1130
+ name: "bash",
1131
+ arguments: { command: "ls", path: "/tmp" },
1132
+ });
1133
+ });
1134
+
1135
+ it("preserves unsafe integers in stringified tool call arguments", () => {
1136
+ const response = {
1137
+ model: "qwen3:32b",
1138
+ created_at: "2026-01-01T00:00:00Z",
1139
+ message: {
1140
+ role: "assistant" as const,
1141
+ content: "",
1142
+ tool_calls: [
1143
+ {
1144
+ function: {
1145
+ name: "send",
1146
+ arguments: '{"target":9223372036854775807,"nested":{"thread":1234567890123456789}}',
1147
+ },
1148
+ },
1149
+ ],
1150
+ },
1151
+ done: true,
1152
+ };
1153
+ const result = buildAssistantMessage(response, modelInfo);
1154
+ expectToolCallContent(result.content[0], {
1155
+ name: "send",
1156
+ arguments: {
1157
+ target: "9223372036854775807",
1158
+ nested: { thread: "1234567890123456789" },
1159
+ },
1160
+ });
1161
+ });
1162
+
1163
+ it("falls back to empty arguments for malformed stringified tool call arguments", () => {
1164
+ const response = {
1165
+ model: "qwen3:32b",
1166
+ created_at: "2026-01-01T00:00:00Z",
1167
+ message: {
1168
+ role: "assistant" as const,
1169
+ content: "",
1170
+ tool_calls: [{ function: { name: "bash", arguments: '{"command":"ls"' } }],
1171
+ },
1172
+ done: true,
1173
+ };
1174
+ const result = buildAssistantMessage(response, modelInfo);
1175
+ expectToolCallContent(result.content[0], { name: "bash", arguments: {} });
1176
+ });
1177
+
1178
+ it("sets all costs to zero for local models", () => {
1179
+ const response = {
1180
+ model: "qwen3:32b",
1181
+ created_at: "2026-01-01T00:00:00Z",
1182
+ message: { role: "assistant" as const, content: "ok" },
1183
+ done: true,
1184
+ };
1185
+ const result = buildAssistantMessage(response, modelInfo);
1186
+ expect(result.usage.cost).toEqual({
1187
+ input: 0,
1188
+ output: 0,
1189
+ cacheRead: 0,
1190
+ cacheWrite: 0,
1191
+ total: 0,
1192
+ });
1193
+ });
1194
+ });
1195
+
1196
+ // Helper: build a ReadableStreamDefaultReader from NDJSON lines
1197
+ function mockNdjsonReader(lines: string[]): ReadableStreamDefaultReader<Uint8Array> {
1198
+ const encoder = new TextEncoder();
1199
+ const payload = lines.join("\n") + "\n";
1200
+ let consumed = false;
1201
+ return {
1202
+ read: async () => {
1203
+ if (consumed) {
1204
+ return { done: true as const, value: undefined };
1205
+ }
1206
+ consumed = true;
1207
+ return { done: false as const, value: encoder.encode(payload) };
1208
+ },
1209
+ releaseLock: () => {},
1210
+ cancel: async () => {},
1211
+ closed: Promise.resolve(undefined),
1212
+ } as unknown as ReadableStreamDefaultReader<Uint8Array>;
1213
+ }
1214
+
1215
+ async function expectDoneEventContent(lines: string[], expectedContent: unknown) {
1216
+ await withMockNdjsonFetch(lines, async () => {
1217
+ const stream = await createOllamaTestStream({ baseUrl: "http://ollama-host:11434" });
1218
+ const events = await collectStreamEvents(stream);
1219
+
1220
+ const doneEvent = events.at(-1);
1221
+ if (!doneEvent || doneEvent.type !== "done") {
1222
+ throw new Error("Expected done event");
1223
+ }
1224
+
1225
+ expect(doneEvent.message.content).toEqual(expectedContent);
1226
+ });
1227
+ }
1228
+
1229
+ describe("parseNdjsonStream", () => {
1230
+ it("parses text-only streaming chunks", async () => {
1231
+ const reader = mockNdjsonReader([
1232
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"Hello"},"done":false}',
1233
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":" world"},"done":false}',
1234
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":5,"eval_count":2}',
1235
+ ]);
1236
+ const chunks = [];
1237
+ for await (const chunk of parseNdjsonStream(reader)) {
1238
+ chunks.push(chunk);
1239
+ }
1240
+ expect(chunks).toHaveLength(3);
1241
+ expect(chunks[0].message.content).toBe("Hello");
1242
+ expect(chunks[1].message.content).toBe(" world");
1243
+ expect(chunks[2].done).toBe(true);
1244
+ });
1245
+
1246
+ it("parses tool_calls from intermediate chunk (not final)", async () => {
1247
+ // Ollama sends tool_calls in done:false chunk, final done:true has no tool_calls
1248
+ const reader = mockNdjsonReader([
1249
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"bash","arguments":{"command":"ls"}}}]},"done":false}',
1250
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":10,"eval_count":5}',
1251
+ ]);
1252
+ const chunks = [];
1253
+ for await (const chunk of parseNdjsonStream(reader)) {
1254
+ chunks.push(chunk);
1255
+ }
1256
+ expect(chunks).toHaveLength(2);
1257
+ expect(chunks[0].done).toBe(false);
1258
+ expect(chunks[0].message.tool_calls).toHaveLength(1);
1259
+ expect(chunks[0].message.tool_calls![0].function.name).toBe("bash");
1260
+ expect(chunks[1].done).toBe(true);
1261
+ expect(chunks[1].message.tool_calls).toBeUndefined();
1262
+ });
1263
+
1264
+ it("accumulates tool_calls across multiple intermediate chunks", async () => {
1265
+ const reader = mockNdjsonReader([
1266
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"read","arguments":{"path":"/tmp/a"}}}]},"done":false}',
1267
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"bash","arguments":{"command":"ls"}}}]},"done":false}',
1268
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true}',
1269
+ ]);
1270
+
1271
+ // Simulate the accumulation logic from createOllamaStreamFn
1272
+ const accumulatedToolCalls: Array<{
1273
+ function: { name: string; arguments: unknown };
1274
+ }> = [];
1275
+ const chunks = [];
1276
+ for await (const chunk of parseNdjsonStream(reader)) {
1277
+ chunks.push(chunk);
1278
+ if (chunk.message?.tool_calls) {
1279
+ accumulatedToolCalls.push(...chunk.message.tool_calls);
1280
+ }
1281
+ }
1282
+ expect(accumulatedToolCalls).toHaveLength(2);
1283
+ expect(accumulatedToolCalls[0].function.name).toBe("read");
1284
+ expect(accumulatedToolCalls[1].function.name).toBe("bash");
1285
+ // Final done:true chunk has no tool_calls
1286
+ expect(chunks[2].message.tool_calls).toBeUndefined();
1287
+ });
1288
+
1289
+ it("preserves unsafe integer tool arguments as exact strings", async () => {
1290
+ const reader = mockNdjsonReader([
1291
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"send","arguments":{"target":1234567890123456789,"nested":{"thread":9223372036854775807}}}}]},"done":false}',
1292
+ ]);
1293
+
1294
+ const chunks = [];
1295
+ for await (const chunk of parseNdjsonStream(reader)) {
1296
+ chunks.push(chunk);
1297
+ }
1298
+
1299
+ const args = chunks[0]?.message.tool_calls?.[0]?.function.arguments as
1300
+ | { target?: unknown; nested?: { thread?: unknown } }
1301
+ | undefined;
1302
+ expect(args?.target).toBe("1234567890123456789");
1303
+ expect(args?.nested?.thread).toBe("9223372036854775807");
1304
+ });
1305
+
1306
+ it("keeps safe integer tool arguments as numbers", async () => {
1307
+ const reader = mockNdjsonReader([
1308
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"send","arguments":{"retries":3,"delayMs":2500}}}]},"done":false}',
1309
+ ]);
1310
+
1311
+ const chunks = [];
1312
+ for await (const chunk of parseNdjsonStream(reader)) {
1313
+ chunks.push(chunk);
1314
+ }
1315
+
1316
+ const args = chunks[0]?.message.tool_calls?.[0]?.function.arguments as
1317
+ | { retries?: unknown; delayMs?: unknown }
1318
+ | undefined;
1319
+ expect(args?.retries).toBe(3);
1320
+ expect(args?.delayMs).toBe(2500);
1321
+ });
1322
+ });
1323
+
1324
+ async function withMockNdjsonFetch(
1325
+ lines: string[],
1326
+ run: (fetchMock: typeof fetchWithSsrFGuardMock) => Promise<void>,
1327
+ ): Promise<void> {
1328
+ fetchWithSsrFGuardMock.mockImplementation(async () => {
1329
+ const payload = lines.join("\n");
1330
+ return {
1331
+ response: new Response(`${payload}\n`, {
1332
+ status: 200,
1333
+ headers: { "Content-Type": "application/x-ndjson" },
1334
+ }),
1335
+ release: vi.fn(async () => undefined),
1336
+ };
1337
+ });
1338
+ await run(fetchWithSsrFGuardMock);
1339
+ }
1340
+
1341
+ function createControlledNdjsonFetch(): {
1342
+ fetchImpl: () => Promise<{ response: Response; release: () => Promise<void> }>;
1343
+ pushLine: (line: string) => void;
1344
+ close: () => void;
1345
+ } {
1346
+ const encoder = new TextEncoder();
1347
+ let controller: ReadableStreamDefaultController<Uint8Array> | undefined;
1348
+ const body = new ReadableStream<Uint8Array>({
1349
+ start(streamController) {
1350
+ controller = streamController;
1351
+ },
1352
+ });
1353
+ return {
1354
+ fetchImpl: async () => ({
1355
+ response: new Response(body, {
1356
+ status: 200,
1357
+ headers: { "Content-Type": "application/x-ndjson" },
1358
+ }),
1359
+ release: vi.fn(async () => undefined),
1360
+ }),
1361
+ pushLine(line: string) {
1362
+ if (!controller) {
1363
+ throw new Error("NDJSON controller not initialized");
1364
+ }
1365
+ controller.enqueue(encoder.encode(`${line}\n`));
1366
+ },
1367
+ close() {
1368
+ if (!controller) {
1369
+ throw new Error("NDJSON controller not initialized");
1370
+ }
1371
+ controller.close();
1372
+ },
1373
+ };
1374
+ }
1375
+
1376
+ function getGuardedFetchCall(fetchMock: typeof fetchWithSsrFGuardMock): GuardedFetchCall {
1377
+ return (fetchMock.mock.calls.at(0)?.[0] as GuardedFetchCall | undefined) ?? { url: "" };
1378
+ }
1379
+
1380
+ async function createOllamaTestStream(params: {
1381
+ baseUrl: string;
1382
+ defaultHeaders?: Record<string, string>;
1383
+ model?: Record<string, unknown>;
1384
+ options?: {
1385
+ apiKey?: string;
1386
+ maxTokens?: number;
1387
+ temperature?: number;
1388
+ signal?: AbortSignal;
1389
+ timeoutMs?: number;
1390
+ headers?: Record<string, string>;
1391
+ };
1392
+ }) {
1393
+ const streamFn = createOllamaStreamFn(params.baseUrl, params.defaultHeaders);
1394
+ return streamFn(
1395
+ {
1396
+ id: "qwen3:32b",
1397
+ api: "ollama",
1398
+ provider: "custom-ollama",
1399
+ contextWindow: 131072,
1400
+ ...params.model,
1401
+ } as unknown as Parameters<typeof streamFn>[0],
1402
+ {
1403
+ messages: [{ role: "user", content: "hello" }],
1404
+ } as unknown as Parameters<typeof streamFn>[1],
1405
+ (params.options ?? {}) as unknown as Parameters<typeof streamFn>[2],
1406
+ );
1407
+ }
1408
+
1409
+ async function collectStreamEvents<T>(stream: AsyncIterable<T>): Promise<T[]> {
1410
+ const events: T[] = [];
1411
+ for await (const event of stream) {
1412
+ events.push(event);
1413
+ }
1414
+ return events;
1415
+ }
1416
+
1417
+ async function nextEventWithin<T>(
1418
+ iterator: AsyncIterator<T>,
1419
+ timeoutMs = 100,
1420
+ ): Promise<IteratorResult<T> | "timeout"> {
1421
+ let timer: NodeJS.Timeout | undefined;
1422
+ try {
1423
+ return await Promise.race([
1424
+ iterator.next(),
1425
+ new Promise<"timeout">((resolve) => {
1426
+ timer = setTimeout(() => resolve("timeout"), timeoutMs);
1427
+ }),
1428
+ ]);
1429
+ } finally {
1430
+ if (timer) {
1431
+ clearTimeout(timer);
1432
+ }
1433
+ }
1434
+ }
1435
+
1436
+ describe("createOllamaStreamFn streaming events", () => {
1437
+ it("emits start, text_start, text_delta, text_end, done for text responses", async () => {
1438
+ await withMockNdjsonFetch(
1439
+ [
1440
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"Hello"},"done":false}',
1441
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":" world"},"done":false}',
1442
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":5,"eval_count":2}',
1443
+ ],
1444
+ async () => {
1445
+ const stream = await createOllamaTestStream({ baseUrl: "http://ollama-host:11434" });
1446
+ const events = await collectStreamEvents(stream);
1447
+
1448
+ const types = events.map((e) => e.type);
1449
+ expect(types).toEqual([
1450
+ "start",
1451
+ "text_start",
1452
+ "text_delta",
1453
+ "text_delta",
1454
+ "text_end",
1455
+ "done",
1456
+ ]);
1457
+
1458
+ // text_delta events carry incremental deltas
1459
+ const deltas = events.filter((e) => e.type === "text_delta");
1460
+ expect(deltas[0]?.contentIndex).toBe(0);
1461
+ expect(deltas[0]?.delta).toBe("Hello");
1462
+ expect(deltas[1]?.contentIndex).toBe(0);
1463
+ expect(deltas[1]?.delta).toBe(" world");
1464
+
1465
+ // text_end carries the full accumulated content
1466
+ const textEnd = events.find((e) => e.type === "text_end");
1467
+ expect(textEnd?.contentIndex).toBe(0);
1468
+ expect(textEnd?.content).toBe("Hello world");
1469
+
1470
+ // start/text_start carry empty partials (before any content accumulates)
1471
+ const startEvent = events.find((e) => e.type === "start");
1472
+ expect(startEvent?.partial.content).toStrictEqual([]);
1473
+ const textStartEvent = events.find((e) => e.type === "text_start");
1474
+ expect(textStartEvent?.partial.content).toStrictEqual([]);
1475
+
1476
+ // text_delta partials accumulate content progressively
1477
+ expect(deltas[0].partial.content).toEqual([{ type: "text", text: "Hello" }]);
1478
+ expect(deltas[1].partial.content).toEqual([{ type: "text", text: "Hello world" }]);
1479
+
1480
+ // done event contains the final message
1481
+ const doneEvent = events.at(-1);
1482
+ expect(doneEvent?.type).toBe("done");
1483
+ if (doneEvent?.type === "done") {
1484
+ expect(doneEvent.message.content).toEqual([{ type: "text", text: "Hello world" }]);
1485
+ }
1486
+ },
1487
+ );
1488
+ });
1489
+
1490
+ it("emits only done for tool-call-only responses (no text content)", async () => {
1491
+ await withMockNdjsonFetch(
1492
+ [
1493
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"bash","arguments":{"command":"ls"}}}]},"done":false}',
1494
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":10,"eval_count":5}',
1495
+ ],
1496
+ async () => {
1497
+ const stream = await createOllamaTestStream({ baseUrl: "http://ollama-host:11434" });
1498
+ const events = await collectStreamEvents(stream);
1499
+
1500
+ // No text content means no start/text_start/text_delta/text_end events
1501
+ const types = events.map((e) => e.type);
1502
+ expect(types).toEqual(["done"]);
1503
+ const doneEvent = events[0];
1504
+ if (doneEvent.type === "done") {
1505
+ expect(doneEvent.reason).toBe("toolUse");
1506
+ }
1507
+ },
1508
+ );
1509
+ });
1510
+
1511
+ it("estimates usage when the final Ollama chunk omits counters", async () => {
1512
+ await withMockNdjsonFetch(
1513
+ [
1514
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"Estimated answer"},"done":false}',
1515
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true}',
1516
+ ],
1517
+ async () => {
1518
+ const stream = await createOllamaTestStream({ baseUrl: "http://ollama-host:11434" });
1519
+ const events = await collectStreamEvents(stream);
1520
+
1521
+ const doneEvent = events.at(-1);
1522
+ expect(doneEvent?.type).toBe("done");
1523
+ if (doneEvent?.type === "done") {
1524
+ expect(doneEvent.message.usage.input).toBeGreaterThan(0);
1525
+ expect(doneEvent.message.usage.output).toBeGreaterThan(0);
1526
+ expect(doneEvent.message.usage.totalTokens).toBeGreaterThan(0);
1527
+ }
1528
+ },
1529
+ );
1530
+ });
1531
+
1532
+ it("counts image payloads in prompt usage estimates when Ollama omits counters", async () => {
1533
+ await withMockNdjsonFetch(
1534
+ [
1535
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"vision answer"},"done":false}',
1536
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true}',
1537
+ ],
1538
+ async () => {
1539
+ const streamFn = createOllamaStreamFn("http://ollama-host:11434");
1540
+ const stream = await Promise.resolve(
1541
+ streamFn(
1542
+ {
1543
+ id: "llava",
1544
+ api: "ollama",
1545
+ provider: "custom-ollama",
1546
+ contextWindow: 131072,
1547
+ } as never,
1548
+ {
1549
+ messages: [
1550
+ {
1551
+ role: "user",
1552
+ content: [{ type: "image", data: "a".repeat(400) }],
1553
+ },
1554
+ ],
1555
+ } as never,
1556
+ {} as never,
1557
+ ),
1558
+ );
1559
+ const events = await collectStreamEvents(stream);
1560
+
1561
+ const doneEvent = events.at(-1);
1562
+ expect(doneEvent?.type).toBe("done");
1563
+ if (doneEvent?.type === "done") {
1564
+ expect(doneEvent.message.usage.input).toBeGreaterThan(50);
1565
+ }
1566
+ },
1567
+ );
1568
+ });
1569
+
1570
+ it("emits text streaming events before done for mixed text + tool responses", async () => {
1571
+ await withMockNdjsonFetch(
1572
+ [
1573
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"Let me check."},"done":false}',
1574
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"bash","arguments":{"command":"ls"}}}]},"done":false}',
1575
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":10,"eval_count":5}',
1576
+ ],
1577
+ async () => {
1578
+ const stream = await createOllamaTestStream({ baseUrl: "http://ollama-host:11434" });
1579
+ const events = await collectStreamEvents(stream);
1580
+
1581
+ const types = events.map((e) => e.type);
1582
+ expect(types).toEqual(["start", "text_start", "text_delta", "text_end", "done"]);
1583
+ const doneEvent = events.at(-1);
1584
+ if (doneEvent?.type === "done") {
1585
+ expect(doneEvent.reason).toBe("toolUse");
1586
+ }
1587
+ },
1588
+ );
1589
+ });
1590
+
1591
+ it("emits text_end as soon as Ollama switches from text to tool calls", async () => {
1592
+ const controlledFetch = createControlledNdjsonFetch();
1593
+ fetchWithSsrFGuardMock.mockImplementation(controlledFetch.fetchImpl);
1594
+
1595
+ try {
1596
+ const stream = await createOllamaTestStream({ baseUrl: "http://ollama-host:11434" });
1597
+ const iterator = stream[Symbol.asyncIterator]();
1598
+
1599
+ controlledFetch.pushLine(
1600
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"Let me check."},"done":false}',
1601
+ );
1602
+
1603
+ const startEvent = await nextEventWithin(iterator);
1604
+ const textStartEvent = await nextEventWithin(iterator);
1605
+ const textDeltaEvent = await nextEventWithin(iterator);
1606
+
1607
+ expect(startEvent).not.toBe("timeout");
1608
+ expect(textStartEvent).not.toBe("timeout");
1609
+ expect(textDeltaEvent).not.toBe("timeout");
1610
+ expectIteratorEvent(startEvent, { type: "start", done: false });
1611
+ expectIteratorEvent(textStartEvent, { type: "text_start", done: false });
1612
+ expectIteratorEvent(textDeltaEvent, {
1613
+ type: "text_delta",
1614
+ delta: "Let me check.",
1615
+ done: false,
1616
+ });
1617
+
1618
+ controlledFetch.pushLine(
1619
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"bash","arguments":{"command":"ls"}}}]},"done":false}',
1620
+ );
1621
+
1622
+ const textEndEvent = await nextEventWithin(iterator);
1623
+ expect(textEndEvent).not.toBe("timeout");
1624
+ expectIteratorEvent(textEndEvent, {
1625
+ type: "text_end",
1626
+ content: "Let me check.",
1627
+ done: false,
1628
+ });
1629
+ if (textEndEvent !== "timeout") {
1630
+ const textEndValue = requireRecord(textEndEvent.value, "text_end value");
1631
+ expect(textEndValue.contentIndex).toBe(0);
1632
+ expect(requireRecord(textEndValue.partial, "text_end partial").content).toEqual([
1633
+ { type: "text", text: "Let me check." },
1634
+ ]);
1635
+ }
1636
+
1637
+ controlledFetch.pushLine(
1638
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":10,"eval_count":5}',
1639
+ );
1640
+ controlledFetch.close();
1641
+
1642
+ const doneEvent = await nextEventWithin(iterator);
1643
+ expect(doneEvent).not.toBe("timeout");
1644
+ if (doneEvent !== "timeout" && doneEvent.done === false) {
1645
+ expectIteratorEvent(doneEvent, { type: "done", done: false });
1646
+ expect(requireRecord(doneEvent.value, "done value").reason).toBe("toolUse");
1647
+
1648
+ const streamEnd = await nextEventWithin(iterator);
1649
+ expect(streamEnd).not.toBe("timeout");
1650
+ expectIteratorEvent(streamEnd, { done: true });
1651
+ } else {
1652
+ expectIteratorEvent(doneEvent, { done: true });
1653
+ }
1654
+ } finally {
1655
+ fetchWithSsrFGuardMock.mockReset();
1656
+ }
1657
+ });
1658
+
1659
+ it("emits error without text_end when stream fails mid-response", async () => {
1660
+ // Simulate a stream that sends one content chunk then ends without done:true.
1661
+ // The stream function throws "Ollama API stream ended without a final response".
1662
+ await withMockNdjsonFetch(
1663
+ [
1664
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"partial"},"done":false}',
1665
+ ],
1666
+ async () => {
1667
+ const stream = await createOllamaTestStream({ baseUrl: "http://ollama-host:11434" });
1668
+ const events = await collectStreamEvents(stream);
1669
+
1670
+ const types = events.map((e) => e.type);
1671
+ // Should have streaming events for the partial content, then error (no text_end).
1672
+ expect(types).toEqual(["start", "text_start", "text_delta", "error"]);
1673
+ const errorEvent = events.at(-1);
1674
+ expect(errorEvent?.type).toBe("error");
1675
+ },
1676
+ );
1677
+ });
1678
+
1679
+ it("emits an error instead of accepting garbled Kimi visible text", async () => {
1680
+ const garbled =
1681
+ '$$"##"%#"##"####""$""""##""$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$' +
1682
+ '#"$"$"""$""""#$"""$"""%"%###"""#%""""&"#"""$"""#"#""""%#""""&"#"""$"""$"""#%"""';
1683
+ await withMockNdjsonFetch(
1684
+ [
1685
+ JSON.stringify({
1686
+ model: "kimi-k2.5:cloud",
1687
+ created_at: "t",
1688
+ message: { role: "assistant", content: garbled },
1689
+ done: false,
1690
+ }),
1691
+ '{"model":"kimi-k2.5:cloud","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":20,"eval_count":40}',
1692
+ ],
1693
+ async () => {
1694
+ const stream = await createOllamaTestStream({
1695
+ baseUrl: "http://ollama-host:11434",
1696
+ model: { id: "kimi-k2.5:cloud", provider: "ollama" },
1697
+ });
1698
+ const events = await collectStreamEvents(stream);
1699
+
1700
+ const types = events.map((e) => e.type);
1701
+ expect(types).toEqual(["start", "text_start", "text_delta", "error"]);
1702
+ const errorEvent = events.at(-1);
1703
+ expect(errorEvent?.type).toBe("error");
1704
+ if (errorEvent?.type === "error") {
1705
+ expect(errorEvent.error.errorMessage).toContain("garbled visible text");
1706
+ }
1707
+ },
1708
+ );
1709
+ });
1710
+
1711
+ it("does not reject punctuation-heavy text from unrelated Ollama models", async () => {
1712
+ const punctuationHeavy =
1713
+ '$$"##"%#"##"####""$""""##""$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$' +
1714
+ '#"$"$"""$""""#$"""$"""%"%###"""#%""""&"#"""$"""#"#""""%#""""&"#"""$"""$"""#%"""';
1715
+ await withMockNdjsonFetch(
1716
+ [
1717
+ JSON.stringify({
1718
+ model: "qwen3:32b",
1719
+ created_at: "t",
1720
+ message: { role: "assistant", content: punctuationHeavy },
1721
+ done: false,
1722
+ }),
1723
+ '{"model":"qwen3:32b","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":20,"eval_count":40}',
1724
+ ],
1725
+ async () => {
1726
+ const stream = await createOllamaTestStream({ baseUrl: "http://ollama-host:11434" });
1727
+ const events = await collectStreamEvents(stream);
1728
+
1729
+ expect(events.map((e) => e.type)).toEqual([
1730
+ "start",
1731
+ "text_start",
1732
+ "text_delta",
1733
+ "text_end",
1734
+ "done",
1735
+ ]);
1736
+ },
1737
+ );
1738
+ });
1739
+
1740
+ it("emits a single text_delta for single-chunk responses", async () => {
1741
+ await withMockNdjsonFetch(
1742
+ [
1743
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"one shot"},"done":false}',
1744
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
1745
+ ],
1746
+ async () => {
1747
+ const stream = await createOllamaTestStream({ baseUrl: "http://ollama-host:11434" });
1748
+ const events = await collectStreamEvents(stream);
1749
+
1750
+ const types = events.map((e) => e.type);
1751
+ expect(types).toEqual(["start", "text_start", "text_delta", "text_end", "done"]);
1752
+
1753
+ const delta = events.find((e) => e.type === "text_delta");
1754
+ expect(delta?.delta).toBe("one shot");
1755
+ },
1756
+ );
1757
+ });
1758
+ });
1759
+
1760
+ describe("createOllamaStreamFn", () => {
1761
+ it("normalizes /v1 baseUrl and maps maxTokens + signal", async () => {
1762
+ await withMockNdjsonFetch(
1763
+ [
1764
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
1765
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
1766
+ ],
1767
+ async (fetchMock) => {
1768
+ const signal = new AbortController().signal;
1769
+ const stream = await createOllamaTestStream({
1770
+ baseUrl: "http://ollama-host:11434/v1/",
1771
+ options: { maxTokens: 123, signal },
1772
+ });
1773
+
1774
+ const events = await collectStreamEvents(stream);
1775
+ expect(events.at(-1)?.type).toBe("done");
1776
+
1777
+ expect(fetchMock).toHaveBeenCalledTimes(1);
1778
+ const request = getGuardedFetchCall(fetchMock);
1779
+ expect(request.url).toBe("http://ollama-host:11434/api/chat");
1780
+ expect(request.auditContext).toBe("ollama-stream.chat");
1781
+ expect(request.signal).toBe(signal);
1782
+ const requestInit = request.init ?? {};
1783
+ expect(requestInit.signal).toBeUndefined();
1784
+ if (typeof requestInit.body !== "string") {
1785
+ throw new Error("Expected string request body");
1786
+ }
1787
+
1788
+ const requestBody = JSON.parse(requestInit.body) as {
1789
+ options?: { num_ctx?: number; num_predict?: number };
1790
+ };
1791
+ if (!requestBody.options) {
1792
+ throw new Error("Expected Ollama request options");
1793
+ }
1794
+ expect(requestBody.options?.num_ctx).toBeUndefined();
1795
+ expect(requestBody.options.num_predict).toBe(123);
1796
+ },
1797
+ );
1798
+ });
1799
+
1800
+ it("uses configured params.num_ctx for native Ollama chat options", async () => {
1801
+ await withMockNdjsonFetch(
1802
+ [
1803
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
1804
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
1805
+ ],
1806
+ async (fetchMock) => {
1807
+ const stream = await createOllamaTestStream({
1808
+ baseUrl: "http://ollama-host:11434",
1809
+ model: {
1810
+ params: {
1811
+ num_ctx: 32768,
1812
+ temperature: 0.2,
1813
+ top_p: 0.9,
1814
+ thinking: false,
1815
+ streaming: false,
1816
+ },
1817
+ contextWindow: 131072,
1818
+ },
1819
+ options: { temperature: 0.7, maxTokens: 55 },
1820
+ });
1821
+
1822
+ const events = await collectStreamEvents(stream);
1823
+ expect(events.at(-1)?.type).toBe("done");
1824
+
1825
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
1826
+ if (typeof requestInit.body !== "string") {
1827
+ throw new Error("Expected string request body");
1828
+ }
1829
+ const requestBody = JSON.parse(requestInit.body) as {
1830
+ think?: boolean;
1831
+ options: {
1832
+ num_ctx?: number;
1833
+ num_predict?: number;
1834
+ temperature?: number;
1835
+ top_p?: number;
1836
+ streaming?: boolean;
1837
+ };
1838
+ };
1839
+ expect(requestBody.options.num_ctx).toBe(32768);
1840
+ expect(requestBody.options.num_predict).toBe(55);
1841
+ expect(requestBody.options.temperature).toBe(0.7);
1842
+ expect(requestBody.options.top_p).toBe(0.9);
1843
+ expect(requestBody.options.streaming).toBeUndefined();
1844
+ expect(requestBody.think).toBe(false);
1845
+ },
1846
+ );
1847
+ });
1848
+
1849
+ it("omits num_ctx when the model has no params.num_ctx and no catalog window", async () => {
1850
+ await withMockNdjsonFetch(
1851
+ [
1852
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
1853
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
1854
+ ],
1855
+ async (fetchMock) => {
1856
+ const stream = await createOllamaTestStream({
1857
+ baseUrl: "http://ollama-host:11434",
1858
+ // Override the helper default contextWindow back to undefined so the
1859
+ // request body should leave Ollama's Modelfile to decide num_ctx.
1860
+ model: { contextWindow: undefined },
1861
+ });
1862
+
1863
+ await collectStreamEvents(stream);
1864
+
1865
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
1866
+ if (typeof requestInit.body !== "string") {
1867
+ throw new Error("Expected string request body");
1868
+ }
1869
+ const requestBody = JSON.parse(requestInit.body) as {
1870
+ options?: { num_ctx?: number };
1871
+ };
1872
+ expect(requestBody.options?.num_ctx).toBeUndefined();
1873
+ },
1874
+ );
1875
+ });
1876
+
1877
+ it("does not fall back to catalog contextWindow as native Ollama num_ctx", async () => {
1878
+ await withMockNdjsonFetch(
1879
+ [
1880
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
1881
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
1882
+ ],
1883
+ async (fetchMock) => {
1884
+ const stream = await createOllamaTestStream({
1885
+ baseUrl: "http://ollama-host:11434",
1886
+ model: { contextWindow: 32768 },
1887
+ });
1888
+
1889
+ await collectStreamEvents(stream);
1890
+
1891
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
1892
+ if (typeof requestInit.body !== "string") {
1893
+ throw new Error("Expected string request body");
1894
+ }
1895
+ const requestBody = JSON.parse(requestInit.body) as {
1896
+ options?: { num_ctx?: number };
1897
+ };
1898
+ expect(requestBody.options?.num_ctx).toBeUndefined();
1899
+ },
1900
+ );
1901
+ });
1902
+
1903
+ it("does not fall back to catalog maxTokens as native Ollama num_ctx", async () => {
1904
+ await withMockNdjsonFetch(
1905
+ [
1906
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
1907
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
1908
+ ],
1909
+ async (fetchMock) => {
1910
+ const stream = await createOllamaTestStream({
1911
+ baseUrl: "http://ollama-host:11434",
1912
+ // The helper default contextWindow is overridden back to undefined so
1913
+ // the right side of `model.contextWindow ?? model.maxTokens` is the
1914
+ // load-bearing branch.
1915
+ model: { contextWindow: undefined, maxTokens: 65536 },
1916
+ });
1917
+
1918
+ await collectStreamEvents(stream);
1919
+
1920
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
1921
+ if (typeof requestInit.body !== "string") {
1922
+ throw new Error("Expected string request body");
1923
+ }
1924
+ const requestBody = JSON.parse(requestInit.body) as {
1925
+ options?: { num_ctx?: number };
1926
+ };
1927
+ expect(requestBody.options?.num_ctx).toBeUndefined();
1928
+ },
1929
+ );
1930
+ });
1931
+
1932
+ it("maps configured native Ollama params.thinking=max to the stable top-level think value", async () => {
1933
+ await withMockNdjsonFetch(
1934
+ [
1935
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
1936
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
1937
+ ],
1938
+ async (fetchMock) => {
1939
+ const stream = await createOllamaTestStream({
1940
+ baseUrl: "http://ollama-host:11434",
1941
+ model: { params: { thinking: "max" } },
1942
+ });
1943
+
1944
+ const events = await collectStreamEvents(stream);
1945
+ expect(events.at(-1)?.type).toBe("done");
1946
+
1947
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
1948
+ if (typeof requestInit.body !== "string") {
1949
+ throw new Error("Expected string request body");
1950
+ }
1951
+ const requestBody = JSON.parse(requestInit.body) as {
1952
+ think?: string;
1953
+ options?: { think?: string };
1954
+ };
1955
+ expect(requestBody.think).toBe("high");
1956
+ expect(requestBody.options?.think).toBeUndefined();
1957
+ },
1958
+ );
1959
+ });
1960
+
1961
+ it("uses the default loopback policy when baseUrl is empty", async () => {
1962
+ await withMockNdjsonFetch(
1963
+ [
1964
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
1965
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
1966
+ ],
1967
+ async (fetchMock) => {
1968
+ const stream = await createOllamaTestStream({ baseUrl: "" });
1969
+
1970
+ const events = await collectStreamEvents(stream);
1971
+ expect(events.at(-1)?.type).toBe("done");
1972
+
1973
+ const request = getGuardedFetchCall(fetchMock);
1974
+ expect(request.url).toBe("http://127.0.0.1:11434/api/chat");
1975
+ const policy = requireRecord(request.policy, "ssrf policy");
1976
+ expect(policy.hostnameAllowlist).toEqual(["127.0.0.1"]);
1977
+ expect(policy.allowPrivateNetwork).toBe(true);
1978
+ },
1979
+ );
1980
+ });
1981
+
1982
+ it("merges default headers and allows request headers to override them", async () => {
1983
+ await withMockNdjsonFetch(
1984
+ [
1985
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
1986
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
1987
+ ],
1988
+ async (fetchMock) => {
1989
+ const stream = await createOllamaTestStream({
1990
+ baseUrl: "http://ollama-host:11434",
1991
+ defaultHeaders: {
1992
+ "X-OLLAMA-KEY": "provider-secret",
1993
+ "X-Trace": "default",
1994
+ },
1995
+ options: {
1996
+ headers: {
1997
+ "X-Trace": "request",
1998
+ "X-Request-Only": "1",
1999
+ },
2000
+ },
2001
+ });
2002
+
2003
+ const events = await collectStreamEvents(stream);
2004
+ expect(events.at(-1)?.type).toBe("done");
2005
+
2006
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
2007
+ const headers = requireHeaders(requestInit.headers);
2008
+ expect(headers["Content-Type"]).toBe("application/json");
2009
+ expect(headers["X-OLLAMA-KEY"]).toBe("provider-secret");
2010
+ expect(headers["X-Trace"]).toBe("request");
2011
+ expect(headers["X-Request-Only"]).toBe("1");
2012
+ },
2013
+ );
2014
+ });
2015
+
2016
+ it("preserves an explicit Authorization header when apiKey is a local marker", async () => {
2017
+ await withMockNdjsonFetch(
2018
+ [
2019
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
2020
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
2021
+ ],
2022
+ async (fetchMock) => {
2023
+ const stream = await createOllamaTestStream({
2024
+ baseUrl: "http://ollama-host:11434",
2025
+ defaultHeaders: {
2026
+ Authorization: "Bearer proxy-token",
2027
+ },
2028
+ options: {
2029
+ apiKey: "ollama-local", // pragma: allowlist secret
2030
+ headers: {
2031
+ Authorization: "Bearer proxy-token",
2032
+ },
2033
+ },
2034
+ });
2035
+
2036
+ await collectStreamEvents(stream);
2037
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
2038
+ expect(requireHeaders(requestInit.headers).Authorization).toBe("Bearer proxy-token");
2039
+ },
2040
+ );
2041
+ });
2042
+
2043
+ it("allows a real apiKey to override an explicit Authorization header", async () => {
2044
+ await withMockNdjsonFetch(
2045
+ [
2046
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
2047
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
2048
+ ],
2049
+ async (fetchMock) => {
2050
+ const streamFn = createOllamaStreamFn("http://ollama-host:11434", {
2051
+ Authorization: "Bearer proxy-token",
2052
+ });
2053
+ const stream = await Promise.resolve(
2054
+ streamFn(
2055
+ {
2056
+ id: "qwen3:32b",
2057
+ api: "ollama",
2058
+ provider: "custom-ollama",
2059
+ contextWindow: 131072,
2060
+ } as never,
2061
+ {
2062
+ messages: [{ role: "user", content: "hello" }],
2063
+ } as never,
2064
+ {
2065
+ apiKey: "real-token", // pragma: allowlist secret
2066
+ } as never,
2067
+ ),
2068
+ );
2069
+
2070
+ await collectStreamEvents(stream);
2071
+ const requestInit = getGuardedFetchCall(fetchMock).init ?? {};
2072
+ expect(requireHeaders(requestInit.headers).Authorization).toBe("Bearer real-token");
2073
+ },
2074
+ );
2075
+ });
2076
+
2077
+ it("surfaces non-2xx HTTP response as status-prefixed error", async () => {
2078
+ fetchWithSsrFGuardMock.mockResolvedValue({
2079
+ response: new Response("Service Unavailable", {
2080
+ status: 503,
2081
+ statusText: "Service Unavailable",
2082
+ }),
2083
+ release: vi.fn(async () => undefined),
2084
+ });
2085
+ try {
2086
+ const stream = await createOllamaTestStream({ baseUrl: "http://ollama-host:11434" });
2087
+ const events = await collectStreamEvents(stream);
2088
+
2089
+ const errorEvent = events.find((e) => e.type === "error") as
2090
+ | { type: "error"; error: { errorMessage?: string } }
2091
+ | undefined;
2092
+ if (!errorEvent) {
2093
+ throw new Error("expected Ollama stream error event");
2094
+ }
2095
+ // The error message must start with the HTTP status code so that
2096
+ // extractLeadingHttpStatus can parse it for failover/retry logic.
2097
+ expect(errorEvent.error.errorMessage).toMatch(/^503\b/);
2098
+ } finally {
2099
+ fetchWithSsrFGuardMock.mockReset();
2100
+ }
2101
+ });
2102
+
2103
+ it("keeps thinking chunks when no final content is emitted", async () => {
2104
+ await expectDoneEventContent(
2105
+ [
2106
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"","thinking":"reasoned"},"done":false}',
2107
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"","thinking":" output"},"done":false}',
2108
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":2}',
2109
+ ],
2110
+ [{ type: "thinking", thinking: "reasoned output" }],
2111
+ );
2112
+ });
2113
+
2114
+ it("keeps streamed content after earlier thinking chunks", async () => {
2115
+ await expectDoneEventContent(
2116
+ [
2117
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"","thinking":"internal"},"done":false}',
2118
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"final"},"done":false}',
2119
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":" answer"},"done":false}',
2120
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":2}',
2121
+ ],
2122
+ [
2123
+ { type: "thinking", thinking: "internal" },
2124
+ { type: "text", text: "final answer" },
2125
+ ],
2126
+ );
2127
+ });
2128
+
2129
+ it("keeps reasoning chunks when no final content is emitted", async () => {
2130
+ await expectDoneEventContent(
2131
+ [
2132
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"","reasoning":"reasoned"},"done":false}',
2133
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"","reasoning":" output"},"done":false}',
2134
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":2}',
2135
+ ],
2136
+ [{ type: "thinking", thinking: "reasoned output" }],
2137
+ );
2138
+ });
2139
+
2140
+ it("keeps streamed content after earlier reasoning chunks", async () => {
2141
+ await expectDoneEventContent(
2142
+ [
2143
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"","reasoning":"internal"},"done":false}',
2144
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"final"},"done":false}',
2145
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":" answer"},"done":false}',
2146
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":2}',
2147
+ ],
2148
+ [
2149
+ { type: "thinking", thinking: "internal" },
2150
+ { type: "text", text: "final answer" },
2151
+ ],
2152
+ );
2153
+ });
2154
+ });
2155
+
2156
+ describe("resolveOllamaBaseUrlForRun", () => {
2157
+ it("prefers provider baseUrl over model baseUrl", () => {
2158
+ expect(
2159
+ resolveOllamaBaseUrlForRun({
2160
+ modelBaseUrl: "http://model-host:11434",
2161
+ providerBaseUrl: "http://provider-host:11434",
2162
+ }),
2163
+ ).toBe("http://provider-host:11434");
2164
+ });
2165
+
2166
+ it("falls back to model baseUrl when provider baseUrl is missing", () => {
2167
+ expect(
2168
+ resolveOllamaBaseUrlForRun({
2169
+ modelBaseUrl: "http://model-host:11434",
2170
+ }),
2171
+ ).toBe("http://model-host:11434");
2172
+ });
2173
+
2174
+ it("falls back to native default when neither baseUrl is configured", () => {
2175
+ expect(resolveOllamaBaseUrlForRun({})).toBe("http://127.0.0.1:11434");
2176
+ });
2177
+ });
2178
+
2179
+ describe("createConfiguredOllamaStreamFn", () => {
2180
+ it("uses provider-level baseUrl when model baseUrl is absent", async () => {
2181
+ await withMockNdjsonFetch(
2182
+ [
2183
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":"ok"},"done":false}',
2184
+ '{"model":"m","created_at":"t","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":1,"eval_count":1}',
2185
+ ],
2186
+ async (fetchMock) => {
2187
+ const streamFn = createConfiguredOllamaStreamFn({
2188
+ model: {
2189
+ headers: { Authorization: "Bearer proxy-token" },
2190
+ },
2191
+ providerBaseUrl: "http://provider-host:11434/v1",
2192
+ });
2193
+ const stream = await Promise.resolve(
2194
+ streamFn(
2195
+ {
2196
+ id: "qwen3:32b",
2197
+ api: "ollama",
2198
+ provider: "custom-ollama",
2199
+ contextWindow: 131072,
2200
+ } as never,
2201
+ {
2202
+ messages: [{ role: "user", content: "hello" }],
2203
+ } as never,
2204
+ {
2205
+ apiKey: "ollama-local", // pragma: allowlist secret
2206
+ } as never,
2207
+ ),
2208
+ );
2209
+
2210
+ await collectStreamEvents(stream);
2211
+ const request = getGuardedFetchCall(fetchMock);
2212
+ expect(request.url).toBe("http://provider-host:11434/api/chat");
2213
+ const requestInit = request.init ?? {};
2214
+ expect(requireHeaders(requestInit.headers).Authorization).toBe("Bearer proxy-token");
2215
+ },
2216
+ );
2217
+ });
2218
+ });