@vellumai/assistant 0.11.4 → 0.11.5-staging.2

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 (367) hide show
  1. package/AGENTS.md +2 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  16. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  17. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
  19. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/openapi.yaml +193 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/anthropic-provider.test.ts +10 -3
  26. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  27. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  28. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  29. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  30. package/src/__tests__/channel-reply-delivery.test.ts +26 -13
  31. package/src/__tests__/checker.test.ts +20 -314
  32. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  33. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  34. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  35. package/src/__tests__/config-loader-backfill.test.ts +19 -10
  36. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  37. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  38. package/src/__tests__/conversation-attachments.test.ts +0 -1
  39. package/src/__tests__/conversation-error.test.ts +17 -0
  40. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  41. package/src/__tests__/conversation-pairing.test.ts +186 -0
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  43. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  44. package/src/__tests__/credential-routes.test.ts +22 -3
  45. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  46. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  47. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  48. package/src/__tests__/dm-backfill.test.ts +63 -0
  49. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  50. package/src/__tests__/edit-propagation.test.ts +107 -4
  51. package/src/__tests__/events-client-registration.test.ts +28 -0
  52. package/src/__tests__/file-write-tool.test.ts +4 -2
  53. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  54. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  55. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  56. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  57. package/src/__tests__/host-shell-tool.test.ts +23 -4
  58. package/src/__tests__/image-conversion.test.ts +143 -1
  59. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  60. package/src/__tests__/input-repairs.test.ts +207 -0
  61. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  62. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  63. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  64. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  65. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  66. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  67. package/src/__tests__/mtime-cache.test.ts +2 -0
  68. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  69. package/src/__tests__/openai-provider.test.ts +22 -0
  70. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  71. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  72. package/src/__tests__/platform.test.ts +16 -1
  73. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  74. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  75. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  76. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  77. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  78. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  79. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  80. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  81. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  82. package/src/__tests__/reaction-persistence.test.ts +200 -7
  83. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  84. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  85. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  86. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  87. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  88. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  89. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  90. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  91. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  92. package/src/__tests__/tool-executor.test.ts +63 -38
  93. package/src/__tests__/tool-policy.test.ts +97 -1
  94. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  95. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  96. package/src/__tests__/validate-input.test.ts +243 -7
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  99. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  100. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  101. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  102. package/src/acp/acp-claude-oauth.ts +38 -21
  103. package/src/acp/prepare-agent-env.ts +146 -55
  104. package/src/agent/loop-tool-dedup.test.ts +161 -0
  105. package/src/agent/loop.ts +68 -22
  106. package/src/api/constants/app-tools.ts +34 -0
  107. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  108. package/src/api/index.ts +19 -0
  109. package/src/api/responses/resource-pressure-status.ts +25 -0
  110. package/src/approvals/guardian-channel-delivery.ts +70 -6
  111. package/src/approvals/guardian-request-resolvers.ts +32 -66
  112. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  113. package/src/calls/voice-session-bridge.ts +60 -6
  114. package/src/channels/__tests__/message-audience.test.ts +49 -0
  115. package/src/channels/__tests__/types.test.ts +17 -3
  116. package/src/channels/message-audience.ts +40 -0
  117. package/src/channels/types.ts +16 -12
  118. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  119. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  120. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  121. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  122. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  123. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  124. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  125. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  126. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  127. package/src/cli/commands/conversations.help.ts +34 -0
  128. package/src/cli/commands/conversations.ts +125 -0
  129. package/src/cli/commands/inference-providers.ts +9 -4
  130. package/src/cli/commands/inference.help.ts +9 -4
  131. package/src/cli/commands/keys.help.ts +13 -1
  132. package/src/cli/commands/keys.ts +26 -15
  133. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  134. package/src/cli/commands/memory/index.help.ts +41 -27
  135. package/src/cli/commands/memory/index.ts +2 -0
  136. package/src/cli/commands/memory/memory-v2.ts +58 -54
  137. package/src/cli/commands/memory/memory-validate.ts +18 -0
  138. package/src/cli/commands/monitoring.ts +1 -0
  139. package/src/cli/commands/plugins.help.ts +28 -31
  140. package/src/cli/commands/plugins.ts +10 -0
  141. package/src/cli/commands/skills.help.ts +13 -16
  142. package/src/cli/commands/trust.ts +3 -14
  143. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  144. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  145. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  146. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  147. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  148. package/src/cli/lib/bundled-marketplace.json +13 -0
  149. package/src/cli/lib/daemon-credential-client.ts +25 -3
  150. package/src/cli/lib/install-from-github.ts +35 -24
  151. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  152. package/src/cli/lib/plugin-surfaces.ts +27 -0
  153. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  154. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  155. package/src/config/default-profile-catalog.ts +5 -6
  156. package/src/config/feature-flag-registry.json +32 -32
  157. package/src/config/webhook-routing.ts +8 -0
  158. package/src/context/compactor.ts +31 -2
  159. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  160. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  161. package/src/daemon/conversation-error.ts +13 -0
  162. package/src/daemon/conversation-messaging.ts +22 -2
  163. package/src/daemon/conversation-process.ts +11 -0
  164. package/src/daemon/conversation-store.ts +10 -0
  165. package/src/daemon/conversation-surfaces.ts +21 -8
  166. package/src/daemon/conversation.ts +12 -10
  167. package/src/daemon/lifecycle.ts +6 -2
  168. package/src/daemon/message-types/conversations.ts +5 -7
  169. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  170. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  171. package/src/daemon/resource-pressure-guard.ts +387 -0
  172. package/src/daemon/shutdown-handlers.ts +2 -0
  173. package/src/daemon/startup-error.ts +16 -0
  174. package/src/daemon/trust-context.ts +14 -10
  175. package/src/daemon/unsendable-image-notice.ts +76 -0
  176. package/src/hooks/registry.ts +65 -51
  177. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  178. package/src/ipc/gateway-client.test.ts +1 -1
  179. package/src/ipc/gateway-client.ts +14 -21
  180. package/src/ipc/socket-cleanup.ts +2 -11
  181. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  182. package/src/messaging/provider-message-metadata.ts +128 -0
  183. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  184. package/src/messaging/providers/channel-transport.ts +90 -13
  185. package/src/messaging/providers/discord/send.ts +16 -0
  186. package/src/messaging/providers/discord/transport.ts +11 -1
  187. package/src/messaging/providers/index.ts +109 -16
  188. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  189. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  190. package/src/messaging/providers/slack/send.test.ts +6 -9
  191. package/src/messaging/providers/slack/send.ts +40 -28
  192. package/src/messaging/providers/slack/transport.ts +22 -26
  193. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  194. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  195. package/src/messaging/read-provider-metadata.test.ts +157 -0
  196. package/src/messaging/read-provider-metadata.ts +51 -0
  197. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  198. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  199. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  200. package/src/notifications/adapters/macos.ts +55 -0
  201. package/src/notifications/adapters/slack.ts +10 -5
  202. package/src/notifications/assistant-reply-producer.ts +37 -4
  203. package/src/notifications/conversation-pairing.ts +111 -21
  204. package/src/notifications/edit-notification.ts +38 -8
  205. package/src/notifications/emit-signal.ts +12 -14
  206. package/src/notifications/home-feed-side-effect.ts +218 -15
  207. package/src/notifications/types.ts +5 -0
  208. package/src/permissions/AGENTS.md +16 -0
  209. package/src/permissions/checker.test.ts +75 -122
  210. package/src/permissions/checker.ts +44 -560
  211. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  212. package/src/permissions/confirmation-guardian-request.ts +5 -1
  213. package/src/persistence/conversation-crud.ts +23 -20
  214. package/src/persistence/conversation-queries.ts +14 -1
  215. package/src/persistence/conversation-types.ts +22 -0
  216. package/src/persistence/delivery-crud.ts +122 -15
  217. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  218. package/src/plugin-api/conversation-turn.ts +27 -0
  219. package/src/plugin-api/credential-scope.test.ts +15 -0
  220. package/src/plugin-api/credential-scope.ts +14 -0
  221. package/src/plugin-api/index.ts +19 -1
  222. package/src/plugin-api/resolve-credential.ts +15 -10
  223. package/src/plugin-api/store-credential.ts +148 -0
  224. package/src/plugin-api/system-card.ts +36 -0
  225. package/src/plugin-api/vision-support.test.ts +82 -0
  226. package/src/plugin-api/vision-support.ts +14 -2
  227. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  228. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  229. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  230. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  231. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  232. package/src/plugins/defaults/main.ts +15 -0
  233. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  234. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  235. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  237. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  238. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  239. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  240. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  241. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  242. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  243. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  244. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  245. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  246. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  247. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  248. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  249. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  250. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  251. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  252. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  253. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  254. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  255. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  256. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  257. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  258. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  259. package/src/plugins/mtime-cache.ts +1 -1
  260. package/src/plugins/pipeline.ts +14 -7
  261. package/src/plugins/plugin-execution-context.ts +35 -11
  262. package/src/plugins/plugin-tree-walk.ts +63 -4
  263. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  264. package/src/providers/anthropic/client.ts +332 -241
  265. package/src/providers/connection-resolution.ts +2 -1
  266. package/src/providers/content-blocks.ts +22 -0
  267. package/src/providers/gemini/client.ts +5 -2
  268. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  269. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  270. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  271. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  272. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  273. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  274. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  275. package/src/providers/inference/adapter-factory.ts +33 -2
  276. package/src/providers/inference/auth.ts +13 -2
  277. package/src/providers/inference/credential-usage.ts +37 -0
  278. package/src/providers/inference/missing-credential-guard.ts +110 -0
  279. package/src/providers/inference/resolve-auth.ts +7 -7
  280. package/src/providers/media-resolve.ts +176 -15
  281. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  282. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  283. package/src/providers/openai/chat-completions-provider.ts +322 -42
  284. package/src/routes/route-host-protocol.ts +7 -0
  285. package/src/routes/worker.ts +31 -10
  286. package/src/runtime/AGENTS.md +35 -0
  287. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  288. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  289. package/src/runtime/assistant-event-hub.ts +38 -0
  290. package/src/runtime/channel-reply-delivery.ts +54 -31
  291. package/src/runtime/channel-retry-sweep.ts +6 -4
  292. package/src/runtime/effective-capabilities.test.ts +19 -0
  293. package/src/runtime/effective-capabilities.ts +25 -0
  294. package/src/runtime/guardian-reply-router.ts +6 -2
  295. package/src/runtime/http-errors.ts +1 -0
  296. package/src/runtime/http-server.ts +20 -5
  297. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  298. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  299. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  300. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  301. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  302. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  303. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  304. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  305. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  306. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  307. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  308. package/src/runtime/routes/approval-routes.ts +37 -4
  309. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  310. package/src/runtime/routes/canned-message-complete.ts +68 -29
  311. package/src/runtime/routes/client-routes.ts +113 -0
  312. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  313. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  314. package/src/runtime/routes/conversation-routes.ts +36 -22
  315. package/src/runtime/routes/credential-in-use.ts +85 -0
  316. package/src/runtime/routes/credential-routes.ts +55 -85
  317. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  318. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  319. package/src/runtime/routes/identity-routes.ts +5 -121
  320. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  321. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  322. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  323. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  324. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  325. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  326. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  327. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  328. package/src/runtime/routes/index.ts +2 -0
  329. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  330. package/src/runtime/routes/monitoring-routes.ts +9 -1
  331. package/src/runtime/routes/plugins-routes.ts +17 -5
  332. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  333. package/src/runtime/routes/secret-routes.ts +39 -8
  334. package/src/runtime/routes/settings-routes.ts +6 -6
  335. package/src/runtime/routes/surface-action-routes.ts +20 -19
  336. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  337. package/src/runtime/routes/user-route-resolution.ts +18 -1
  338. package/src/runtime/slack-reply-session.test.ts +23 -13
  339. package/src/runtime/slack-reply-session.ts +38 -55
  340. package/src/runtime/web-presence.ts +90 -0
  341. package/src/skills/validate-input.ts +177 -40
  342. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  343. package/src/subagent/consult-context.ts +7 -23
  344. package/src/tools/credentials/broker.ts +24 -79
  345. package/src/tools/credentials/ref-parse.ts +35 -0
  346. package/src/tools/credentials/resolve.ts +4 -10
  347. package/src/tools/credentials/store.ts +168 -0
  348. package/src/tools/credentials/tool-policy.ts +67 -0
  349. package/src/tools/executor.ts +53 -15
  350. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  351. package/src/tools/network/url-safety.ts +9 -16
  352. package/src/tools/network/web-search.ts +21 -3
  353. package/src/tools/permission-checker.ts +43 -52
  354. package/src/tools/schema-transforms.ts +40 -5
  355. package/src/tools/shared/input-repairs.ts +160 -0
  356. package/src/tools/skills/skill-tool-factory.ts +18 -4
  357. package/src/tools/subagent/spawn.ts +0 -1
  358. package/src/tools/tool-approval-handler.ts +16 -10
  359. package/src/tools/tool-types.ts +26 -13
  360. package/src/tools/types.ts +6 -6
  361. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  362. package/src/util/cgroup-memory.ts +3 -0
  363. package/src/util/container-cpu-sampler.ts +250 -0
  364. package/src/util/image-conversion.ts +178 -14
  365. package/src/util/platform.ts +93 -10
  366. package/src/permissions/ipc-risk-types.ts +0 -143
  367. package/src/permissions/risk-types.ts +0 -76
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Shared predicates over the raw model-loop {@link ContentBlock} union.
3
+ *
4
+ * The agent loop and the receive-side credential guard judge "did this turn
5
+ * produce anything" the same way, so the predicates live here rather than in
6
+ * each consumer. Plugins keep their own copies: a plugin may not import from
7
+ * outside its own directory.
8
+ */
9
+
10
+ import type { ContentBlock } from "./types.js";
11
+
12
+ /** Whether `content` carries at least one non-empty `text` block. */
13
+ export function hasVisibleText(content: ReadonlyArray<ContentBlock>): boolean {
14
+ return content.some(
15
+ (block) => block.type === "text" && block.text.trim().length > 0,
16
+ );
17
+ }
18
+
19
+ /** Whether `content` carries at least one tool call. */
20
+ export function hasToolUse(content: ReadonlyArray<ContentBlock>): boolean {
21
+ return content.some((block) => block.type === "tool_use");
22
+ }
@@ -694,8 +694,11 @@ export class GeminiProvider implements Provider {
694
694
  model: string,
695
695
  ): Promise<genai.Content[]> {
696
696
  // Swap any persisted attachment references back to inline base64 before
697
- // building parts, so the transforms below can read `source.data`.
698
- messages = await resolveMediaReferences(messages);
697
+ // building parts, so the transforms below can read `source.data`. Gemini
698
+ // decodes HEIF itself, so an untranscoded HEIC photo stays an image here
699
+ // rather than being replaced by an omission note:
700
+ // https://ai.google.dev/gemini-api/docs/image-understanding
701
+ messages = await resolveMediaReferences(messages, { acceptsHeif: true });
699
702
  const result: genai.Content[] = [];
700
703
 
701
704
  // Build a map from tool_use id → function name so tool_result blocks
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import { OpenAIChatCompletionsProvider } from "../../openai/chat-completions-provider.js";
3
+ import {
4
+ EMPTY_ASSISTANT_TURN_PLACEHOLDER,
5
+ OpenAIChatCompletionsProvider,
6
+ } from "../../openai/chat-completions-provider.js";
4
7
  import {
5
8
  buildProviderAdapter,
6
9
  createAdapterFromConnection,
@@ -19,6 +22,49 @@ describe("litellm adapter factory", () => {
19
22
  expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
20
23
  });
21
24
 
25
+ test("backfills placeholder content after an aborted empty assistant turn", async () => {
26
+ const adapter = buildProviderAdapter("litellm", {
27
+ apiKey: "sk-litellm-test",
28
+ model: "deepseek/deepseek-v4",
29
+ streamTimeoutMs: 60_000,
30
+ baseURL: "http://localhost:4000/v1",
31
+ useNativeWebSearch: false,
32
+ });
33
+ expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
34
+ const provider = adapter as OpenAIChatCompletionsProvider;
35
+ const requests: unknown[] = [];
36
+ (provider as unknown as { client: unknown }).client = {
37
+ chat: {
38
+ completions: {
39
+ create: async (params: unknown) => {
40
+ requests.push(params);
41
+ return {
42
+ async *[Symbol.asyncIterator]() {
43
+ yield {
44
+ choices: [
45
+ { delta: { content: "ok" }, finish_reason: "stop" },
46
+ ],
47
+ usage: { prompt_tokens: 2, completion_tokens: 1 },
48
+ };
49
+ },
50
+ };
51
+ },
52
+ },
53
+ },
54
+ };
55
+
56
+ await provider.sendMessage([
57
+ { role: "user", content: [{ type: "text", text: "question" }] },
58
+ { role: "assistant", content: [] },
59
+ ]);
60
+
61
+ const params = requests[0] as {
62
+ messages: Array<{ role: string; content: string | null }>;
63
+ };
64
+ const assistantMsg = params.messages.find((m) => m.role === "assistant")!;
65
+ expect(assistantMsg.content).toBe(EMPTY_ASSISTANT_TURN_PLACEHOLDER);
66
+ });
67
+
22
68
  test("buildProviderAdapter works without baseURL (uses proxy default)", () => {
23
69
  const adapter = buildProviderAdapter("litellm", {
24
70
  apiKey: "sk-litellm-test",
@@ -0,0 +1,83 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { OllamaProvider } from "../../ollama/client.js";
4
+ import {
5
+ buildProviderAdapter,
6
+ createAdapterFromConnection,
7
+ } from "../adapter-factory.js";
8
+ import type { ProviderConnection, ResolvedAuth } from "../auth.js";
9
+
10
+ interface RetryOptions {
11
+ credentialSource?: string;
12
+ connectionName?: string;
13
+ }
14
+
15
+ function retryOptions(adapter: unknown): RetryOptions {
16
+ let node = adapter;
17
+ for (let depth = 0; node && depth < 8; depth++) {
18
+ const { options, inner } = node as {
19
+ options?: RetryOptions;
20
+ inner?: unknown;
21
+ };
22
+ if (options) {
23
+ return options;
24
+ }
25
+ node = inner;
26
+ }
27
+ throw new Error("no RetryProvider found in the adapter wrapper chain");
28
+ }
29
+
30
+ function innermostAdapter(adapter: unknown): unknown {
31
+ let node = adapter;
32
+ for (let depth = 0; node && depth < 8; depth++) {
33
+ const inner = (node as { inner?: unknown }).inner;
34
+ if (!inner) {
35
+ return node;
36
+ }
37
+ node = inner;
38
+ }
39
+ throw new Error("wrapper chain too deep");
40
+ }
41
+
42
+ describe("adapter factory ollama", () => {
43
+ test("buildProviderAdapter returns an OllamaProvider", () => {
44
+ const adapter = buildProviderAdapter("ollama", {
45
+ apiKey: "",
46
+ model: "llama3.2",
47
+ streamTimeoutMs: 60_000,
48
+ baseURL: "http://192.168.1.50:11434/v1",
49
+ useNativeWebSearch: false,
50
+ });
51
+ expect(adapter).toBeInstanceOf(OllamaProvider);
52
+ });
53
+
54
+ test("createAdapterFromConnection accepts keyless ollama with a stored baseUrl", () => {
55
+ const connection: ProviderConnection = {
56
+ name: "ollama",
57
+ provider: "ollama",
58
+ auth: { type: "none" },
59
+ label: null,
60
+ baseUrl: "http://192.168.1.50:11434/v1",
61
+ models: null,
62
+ createdAt: Date.now(),
63
+ updatedAt: Date.now(),
64
+ isManaged: false,
65
+ };
66
+
67
+ const resolvedAuth: ResolvedAuth = {
68
+ kind: "none",
69
+ baseUrl: "http://192.168.1.50:11434/v1",
70
+ };
71
+
72
+ const adapter = createAdapterFromConnection(connection, resolvedAuth, {
73
+ model: "llama3.2",
74
+ });
75
+
76
+ expect(adapter).not.toBeNull();
77
+ expect(innermostAdapter(adapter)).toBeInstanceOf(OllamaProvider);
78
+ expect(retryOptions(adapter)).toMatchObject({
79
+ credentialSource: "no-auth",
80
+ connectionName: "ollama",
81
+ });
82
+ });
83
+ });
@@ -1,12 +1,37 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import { OpenAIChatCompletionsProvider } from "../../openai/chat-completions-provider.js";
3
+ import {
4
+ EMPTY_ASSISTANT_TURN_PLACEHOLDER,
5
+ OpenAIChatCompletionsProvider,
6
+ } from "../../openai/chat-completions-provider.js";
4
7
  import {
5
8
  buildProviderAdapter,
6
9
  createAdapterFromConnection,
7
10
  } from "../adapter-factory.js";
8
11
  import type { ProviderConnection, ResolvedAuth } from "../auth.js";
9
12
 
13
+ function stubChatCreate(provider: OpenAIChatCompletionsProvider): unknown[] {
14
+ const requests: unknown[] = [];
15
+ (provider as unknown as { client: unknown }).client = {
16
+ chat: {
17
+ completions: {
18
+ create: async (params: unknown) => {
19
+ requests.push(params);
20
+ return {
21
+ async *[Symbol.asyncIterator]() {
22
+ yield {
23
+ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
24
+ usage: { prompt_tokens: 2, completion_tokens: 1 },
25
+ };
26
+ },
27
+ };
28
+ },
29
+ },
30
+ },
31
+ };
32
+ return requests;
33
+ }
34
+
10
35
  interface RetryOptions {
11
36
  credentialSource?: string;
12
37
  connectionName?: string;
@@ -43,6 +68,78 @@ describe("adapter factory", () => {
43
68
  useNativeWebSearch: false,
44
69
  });
45
70
  expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
71
+ expect(
72
+ (adapter as unknown as { omitToolChoiceWhenReasoning: boolean })
73
+ .omitToolChoiceWhenReasoning,
74
+ ).toBe(true);
75
+ });
76
+
77
+ test("openai-compatible round-trips thinking as reasoning_content", () => {
78
+ const adapter = buildProviderAdapter("openai-compatible", {
79
+ apiKey: "test-key",
80
+ model: "deepseek-reasoner",
81
+ streamTimeoutMs: 60_000,
82
+ baseURL: "https://example.com/v1",
83
+ useNativeWebSearch: false,
84
+ });
85
+ expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
86
+ expect(
87
+ (adapter as unknown as { assistantReasoningField?: string })
88
+ .assistantReasoningField,
89
+ ).toBe("reasoning_content");
90
+ });
91
+
92
+ test("litellm round-trips thinking as reasoning_content", () => {
93
+ const adapter = buildProviderAdapter("litellm", {
94
+ apiKey: "test-key",
95
+ model: "deepseek-reasoner",
96
+ streamTimeoutMs: 60_000,
97
+ baseURL: "https://example.com/v1",
98
+ useNativeWebSearch: false,
99
+ });
100
+ expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
101
+ expect(
102
+ (adapter as unknown as { assistantReasoningField?: string })
103
+ .assistantReasoningField,
104
+ ).toBe("reasoning_content");
105
+ });
106
+
107
+ test("backfills placeholder content after an aborted empty assistant turn", async () => {
108
+ const adapter = buildProviderAdapter("openai-compatible", {
109
+ apiKey: "test-key",
110
+ model: "deepseek-v4",
111
+ streamTimeoutMs: 60_000,
112
+ baseURL: "http://localhost:8080/v1",
113
+ useNativeWebSearch: false,
114
+ });
115
+ expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
116
+ const provider = adapter as OpenAIChatCompletionsProvider;
117
+ const requests = stubChatCreate(provider);
118
+
119
+ await provider.sendMessage([
120
+ { role: "user", content: [{ type: "text", text: "question" }] },
121
+ {
122
+ role: "assistant",
123
+ content: [
124
+ {
125
+ type: "thinking",
126
+ thinking: "aborted mid-thought",
127
+ signature: "",
128
+ },
129
+ ],
130
+ },
131
+ ]);
132
+
133
+ const params = requests[0] as {
134
+ messages: Array<{
135
+ role: string;
136
+ content: string | null;
137
+ tool_calls?: unknown;
138
+ }>;
139
+ };
140
+ const assistantMsg = params.messages.find((m) => m.role === "assistant")!;
141
+ expect(assistantMsg.content).toBe(EMPTY_ASSISTANT_TURN_PLACEHOLDER);
142
+ expect(assistantMsg.tool_calls).toBeUndefined();
46
143
  });
47
144
 
48
145
  test("createAdapterFromConnection wires baseURL from ResolvedAuth", () => {
@@ -184,6 +184,22 @@ describe("base_url provider-type gate (create)", () => {
184
184
  );
185
185
  });
186
186
 
187
+ test("accepts base_url on ollama provider", async () => {
188
+ mockResolvedAddresses = ["93.184.216.34"];
189
+ const result = await handleCreate({
190
+ body: {
191
+ name: "valid-ollama-remote",
192
+ provider: "ollama",
193
+ auth: { type: "none" },
194
+ base_url: "https://ollama.example.com/v1",
195
+ },
196
+ });
197
+ expect(result).toBeDefined();
198
+ expect((result as { baseUrl: string }).baseUrl).toBe(
199
+ "https://ollama.example.com/v1",
200
+ );
201
+ });
202
+
187
203
  test("allows null base_url on non-openai-compatible provider", async () => {
188
204
  // Setting base_url to null is always allowed (it's a no-op clear).
189
205
  const result = await handleCreate({
@@ -433,4 +449,26 @@ describe("base_url provider-type gate (update)", () => {
433
449
  }),
434
450
  ).rejects.toThrow(/base_url is only valid for openai-compatible/);
435
451
  });
452
+
453
+ test("accepts adding base_url to an existing ollama connection", async () => {
454
+ mockResolvedAddresses = ["93.184.216.34"];
455
+ await handleCreate({
456
+ body: {
457
+ name: "update-test-ollama",
458
+ provider: "ollama",
459
+ auth: { type: "none" },
460
+ },
461
+ });
462
+
463
+ const result = await handleUpdate({
464
+ pathParams: { name: "update-test-ollama" },
465
+ body: {
466
+ auth: { type: "none" },
467
+ base_url: "https://ollama.example.com/v1",
468
+ },
469
+ });
470
+ expect((result as { baseUrl: string }).baseUrl).toBe(
471
+ "https://ollama.example.com/v1",
472
+ );
473
+ });
436
474
  });
@@ -174,6 +174,16 @@ describe("resolveAuth defense-in-depth", () => {
174
174
  }
175
175
  });
176
176
 
177
+ test("preserves baseUrl for ollama none-auth", async () => {
178
+ const result = await resolveAuth({ type: "none" }, "ollama", {
179
+ baseUrl: "http://192.168.1.50:11434/v1",
180
+ });
181
+ expect(result.ok).toBe(true);
182
+ if (result.ok && result.resolved.kind === "none") {
183
+ expect(result.resolved.baseUrl).toBe("http://192.168.1.50:11434/v1");
184
+ }
185
+ });
186
+
177
187
  test("handles null baseUrl gracefully for any provider", async () => {
178
188
  const result = await resolveAuth(
179
189
  { type: "api_key", credential: "cred-test" },
@@ -0,0 +1,90 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import { migrateCreateProviderConnections } from "../../../persistence/migrations/243-provider-connections.js";
7
+ import { migrateProviderConnectionStatusLabel } from "../../../persistence/migrations/244-provider-connection-status-label.js";
8
+ import { migrateProviderConnectionBaseUrlAndModels } from "../../../persistence/migrations/250-provider-connection-base-url-and-models.js";
9
+ import * as schema from "../../../persistence/schema/index.js";
10
+ import {
11
+ createConnection,
12
+ getConnection,
13
+ updateConnection,
14
+ } from "../connections.js";
15
+
16
+ function createTestDb() {
17
+ const sqlite = new Database(":memory:");
18
+ sqlite.exec("PRAGMA journal_mode=WAL");
19
+ return drizzle(sqlite, { schema });
20
+ }
21
+
22
+ function bootDb() {
23
+ const db = createTestDb();
24
+ migrateCreateProviderConnections(db);
25
+ migrateProviderConnectionStatusLabel(db);
26
+ migrateProviderConnectionBaseUrlAndModels(db);
27
+ return db;
28
+ }
29
+
30
+ describe("ollama connection CRUD", () => {
31
+ test("create without baseUrl leaves it null and does not require models", () => {
32
+ const db = bootDb();
33
+ const result = createConnection(db, {
34
+ name: "ollama-local",
35
+ provider: "ollama",
36
+ auth: { type: "none" },
37
+ });
38
+ expect(result.ok).toBe(true);
39
+ if (result.ok) {
40
+ expect(result.connection.baseUrl).toBeNull();
41
+ expect(result.connection.models).toBeNull();
42
+ }
43
+ });
44
+
45
+ test("create persists an optional baseUrl", () => {
46
+ const db = bootDb();
47
+ const result = createConnection(db, {
48
+ name: "ollama-remote",
49
+ provider: "ollama",
50
+ auth: { type: "none" },
51
+ baseUrl: "http://192.168.1.50:11434/v1",
52
+ });
53
+ expect(result.ok).toBe(true);
54
+ if (result.ok) {
55
+ expect(result.connection.baseUrl).toBe("http://192.168.1.50:11434/v1");
56
+ expect(result.connection.models).toBeNull();
57
+ }
58
+
59
+ const fetched = getConnection(db, "ollama-remote");
60
+ expect(fetched).not.toBeNull();
61
+ expect(fetched!.baseUrl).toBe("http://192.168.1.50:11434/v1");
62
+ });
63
+
64
+ test("update can change and then clear baseUrl", () => {
65
+ const db = bootDb();
66
+ createConnection(db, {
67
+ name: "ollama-local",
68
+ provider: "ollama",
69
+ auth: { type: "none" },
70
+ });
71
+
72
+ const updated = updateConnection(db, "ollama-local", {
73
+ auth: { type: "none" },
74
+ baseUrl: "http://127.0.0.1:11434/v1",
75
+ });
76
+ expect(updated.ok).toBe(true);
77
+ if (updated.ok) {
78
+ expect(updated.connection.baseUrl).toBe("http://127.0.0.1:11434/v1");
79
+ }
80
+
81
+ const cleared = updateConnection(db, "ollama-local", {
82
+ auth: { type: "none" },
83
+ baseUrl: null,
84
+ });
85
+ expect(cleared.ok).toBe(true);
86
+ if (cleared.ok) {
87
+ expect(cleared.connection.baseUrl).toBeNull();
88
+ }
89
+ });
90
+ });
@@ -0,0 +1,117 @@
1
+ /**
2
+ * A connection whose credential was force-deleted must fail its next send with
3
+ * an actionable configuration error. Upstreams that answer an unusable key with
4
+ * `200` and empty content otherwise reach the user as a blank assistant turn.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+
9
+ import { classifyConversationError } from "../../../daemon/conversation-error.js";
10
+ import { credentialKey } from "../../../security/credential-key.js";
11
+ import {
12
+ deleteSecureKeyAsync,
13
+ setSecureKeyAsync,
14
+ } from "../../../security/secure-keys.js";
15
+ import { ConnectionResolutionError } from "../../routing-identity.js";
16
+ import type {
17
+ ContentBlock,
18
+ Message,
19
+ Provider,
20
+ ProviderResponse,
21
+ } from "../../types.js";
22
+ import { MissingCredentialGuardProvider } from "../missing-credential-guard.js";
23
+
24
+ const CONNECTION_NAME = "agentrouter-primary";
25
+ const ACCOUNT = credentialKey("agentrouter", "api_key");
26
+
27
+ function respondingProvider(content: ContentBlock[]): Provider {
28
+ return {
29
+ name: "openai-compatible",
30
+ sendMessage: (_messages: Message[]): Promise<ProviderResponse> =>
31
+ Promise.resolve({
32
+ content,
33
+ model: "gpt-4o-mini",
34
+ usage: { inputTokens: 12, outputTokens: 0 },
35
+ stopReason: "end_turn",
36
+ }),
37
+ };
38
+ }
39
+
40
+ function guard(inner: Provider): Provider {
41
+ return new MissingCredentialGuardProvider(inner, {
42
+ name: CONNECTION_NAME,
43
+ credentialAccount: ACCOUNT,
44
+ });
45
+ }
46
+
47
+ describe("missing credential guard", () => {
48
+ test("turns an empty upstream turn into an actionable error once the credential is gone", async () => {
49
+ // GIVEN a connection whose credential has been force-deleted
50
+ await deleteSecureKeyAsync(ACCOUNT);
51
+
52
+ // AND an upstream that answers the unusable key with 200 and no content
53
+ const provider = guard(respondingProvider([]));
54
+
55
+ // WHEN the assistant sends through that connection
56
+ const sending = provider.sendMessage([
57
+ {
58
+ role: "user",
59
+ content: [{ type: "text", text: "посмотри последние ошибки" }],
60
+ },
61
+ ]);
62
+
63
+ // THEN the send fails as a missing-credential error naming the connection
64
+ const error = await sending.then(
65
+ () => null,
66
+ (err: unknown) => err,
67
+ );
68
+ expect(error).toBeInstanceOf(ConnectionResolutionError);
69
+ const resolution = error as ConnectionResolutionError;
70
+ expect(resolution.reason).toBe("missing_credential");
71
+ expect(resolution.connectionName).toBe(CONNECTION_NAME);
72
+
73
+ // AND the conversation layer renders it as a client-visible error telling
74
+ // the user where to restore the credential, instead of an empty message
75
+ const classified = classifyConversationError(error, {
76
+ phase: "agent_loop",
77
+ });
78
+ expect(classified.userMessage).toContain(CONNECTION_NAME);
79
+ expect(classified.userMessage).toMatch(/credential|API key/i);
80
+ expect(classified.userMessage).toContain("Settings");
81
+ });
82
+
83
+ test("passes an empty turn through while the credential is still stored", async () => {
84
+ // GIVEN a connection whose credential is present
85
+ await setSecureKeyAsync(ACCOUNT, "sk-test-value");
86
+
87
+ // AND an upstream that legitimately returns no content
88
+ const provider = guard(respondingProvider([]));
89
+
90
+ // WHEN the assistant sends through that connection
91
+ const response = await provider.sendMessage([
92
+ { role: "user", content: [{ type: "text", text: "hello" }] },
93
+ ]);
94
+
95
+ // THEN the empty response is left alone: it is not a credential problem
96
+ expect(response.content).toEqual([]);
97
+ await deleteSecureKeyAsync(ACCOUNT);
98
+ });
99
+
100
+ test("passes a normal answer through when the credential is gone", async () => {
101
+ // GIVEN a connection with no stored credential
102
+ await deleteSecureKeyAsync(ACCOUNT);
103
+
104
+ // AND an upstream that still answered with text (e.g. a keyless local model)
105
+ const provider = guard(
106
+ respondingProvider([{ type: "text", text: "все хорошо" }]),
107
+ );
108
+
109
+ // WHEN the assistant sends through that connection
110
+ const response = await provider.sendMessage([
111
+ { role: "user", content: [{ type: "text", text: "hello" }] },
112
+ ]);
113
+
114
+ // THEN the answer reaches the user untouched
115
+ expect(response.content).toEqual([{ type: "text", text: "все хорошо" }]);
116
+ });
117
+ });
@@ -18,6 +18,7 @@
18
18
  * 3. Register the client in `ADAPTER_FACTORIES` below.
19
19
  */
20
20
 
21
+ import { normalizeCredentialRef } from "../../security/credential-key.js";
21
22
  import { AnthropicProvider } from "../anthropic/client.js";
22
23
  import { AtlasCloudProvider } from "../atlascloud/client.js";
23
24
  import { BasetenProvider } from "../baseten/client.js";
@@ -39,6 +40,7 @@ import { VercelAIGatewayProvider } from "../vercel-ai-gateway/client.js";
39
40
  import type { ResolvedAuth } from "./auth.js";
40
41
  import type { ProviderConnection } from "./auth.js";
41
42
  import { effectiveConnectionAuth } from "./auth.js";
43
+ import { MissingCredentialGuardProvider } from "./missing-credential-guard.js";
42
44
  import { resolveAuth } from "./resolve-auth.js";
43
45
 
44
46
  /** Unified construction opts. Adapters ignore fields they don't consume. */
@@ -96,12 +98,13 @@ const ADAPTER_FACTORIES: Record<string, AdapterFactory> = {
96
98
  // Gemini routes managed proxies through `managedBaseUrl`, not `baseURL`.
97
99
  ...(baseURL ? { managedBaseUrl: baseURL } : {}),
98
100
  }),
99
- ollama: ({ apiKey, model, streamTimeoutMs }) =>
101
+ ollama: ({ apiKey, model, streamTimeoutMs, baseURL }) =>
100
102
  new OllamaProvider(model, {
101
103
  // Empty string means keyless — Ollama's client treats undefined as
102
104
  // "no key provided" and defaults its internal placeholder.
103
105
  apiKey: apiKey || undefined,
104
106
  streamTimeoutMs,
107
+ ...(baseURL ? { baseURL } : {}),
105
108
  }),
106
109
  fireworks: ({ apiKey, model, streamTimeoutMs, baseURL }) =>
107
110
  new FireworksProvider(apiKey, model, {
@@ -130,6 +133,13 @@ const ADAPTER_FACTORIES: Record<string, AdapterFactory> = {
130
133
  providerName: "litellm",
131
134
  providerLabel: "LiteLLM",
132
135
  streamTimeoutMs,
136
+ // Replay thinking as `reasoning_content` so DeepSeek-compatible
137
+ // thinking-mode upstreams accept follow-up requests that include tools.
138
+ assistantReasoningField: "reasoning_content",
139
+ // Generic OpenAI-compat proxies may front a strict backend (vLLM,
140
+ // DeepSeek, Portkey). Backfill empty assistant turns so the request
141
+ // satisfies `content or tool_calls must be set`.
142
+ backfillEmptyAssistantContent: true,
133
143
  ...(baseURL ? { baseURL } : {}),
134
144
  }),
135
145
  // Keyless openai-compatible endpoints (e.g. LM Studio) ignore the key; the
@@ -139,6 +149,17 @@ const ADAPTER_FACTORIES: Record<string, AdapterFactory> = {
139
149
  providerName: "openai-compatible",
140
150
  providerLabel: "OpenAI-compatible",
141
151
  streamTimeoutMs,
152
+ // Replay thinking as `reasoning_content` so DeepSeek-compatible
153
+ // thinking-mode endpoints accept follow-up requests that include tools.
154
+ assistantReasoningField: "reasoning_content",
155
+ // Custom endpoints (Portkey, vLLM, LM Studio, DeepSeek-compat) often
156
+ // reject `{ role: "assistant", content: null }` after a Stop mid-stream
157
+ // or a reasoning-only turn. Same guard as OpenRouter and Vercel AI
158
+ // Gateway.
159
+ backfillEmptyAssistantContent: true,
160
+ // Custom OpenAI-compatible endpoints may front strict reasoning
161
+ // models (DeepSeek thinking) that 400 on any explicit tool_choice.
162
+ omitToolChoiceWhenReasoning: true,
142
163
  ...(baseURL ? { baseURL } : {}),
143
164
  }),
144
165
  minimax: ({ apiKey, model, streamTimeoutMs }) =>
@@ -270,7 +291,7 @@ export function createAdapterFromConnection(
270
291
  // identity, the only route that flows through our proxy.
271
292
  const isManagedProxy = isVellumManagedConnection(connection);
272
293
  const effectiveAuth = effectiveConnectionAuth(connection);
273
- return new UsageTrackingProvider(
294
+ const tracked = new UsageTrackingProvider(
274
295
  new RetryProvider(adapter, {
275
296
  forwardUsageAttributionHeaders: isManagedProxy,
276
297
  credentialSource: isManagedProxy
@@ -288,6 +309,16 @@ export function createAdapterFromConnection(
288
309
  : {}),
289
310
  }),
290
311
  );
312
+
313
+ // A credential-backed connection can lose its key between construction and
314
+ // dispatch. Some upstreams answer that with 200 + empty content rather than
315
+ // a 401, which would otherwise land as a blank assistant turn.
316
+ return "credential" in effectiveAuth
317
+ ? new MissingCredentialGuardProvider(tracked, {
318
+ name: connection.name,
319
+ credentialAccount: normalizeCredentialRef(effectiveAuth.credential),
320
+ })
321
+ : tracked;
291
322
  }
292
323
 
293
324
  function buildConnectionAdapter(