@vellumai/assistant 0.11.4 → 0.11.5-staging.1

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 (358) 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__/container-cpu-sampler.test.ts +153 -0
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/conversation-attachments.test.ts +0 -1
  38. package/src/__tests__/conversation-error.test.ts +17 -0
  39. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  40. package/src/__tests__/conversation-pairing.test.ts +186 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  42. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  43. package/src/__tests__/credential-routes.test.ts +22 -3
  44. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  45. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  46. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  47. package/src/__tests__/dm-backfill.test.ts +63 -0
  48. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  49. package/src/__tests__/edit-propagation.test.ts +107 -4
  50. package/src/__tests__/events-client-registration.test.ts +28 -0
  51. package/src/__tests__/file-write-tool.test.ts +4 -2
  52. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  53. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  54. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  55. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  56. package/src/__tests__/host-shell-tool.test.ts +23 -4
  57. package/src/__tests__/image-conversion.test.ts +143 -1
  58. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  59. package/src/__tests__/input-repairs.test.ts +207 -0
  60. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  61. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  62. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  63. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  64. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  65. package/src/__tests__/mtime-cache.test.ts +2 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  67. package/src/__tests__/openai-provider.test.ts +22 -0
  68. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  69. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  70. package/src/__tests__/platform.test.ts +16 -1
  71. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  72. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  73. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  74. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  75. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  76. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  77. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  78. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  79. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  80. package/src/__tests__/reaction-persistence.test.ts +200 -7
  81. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  82. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  83. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  84. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  85. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  86. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  87. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  88. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  89. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  90. package/src/__tests__/tool-executor.test.ts +63 -38
  91. package/src/__tests__/tool-policy.test.ts +97 -1
  92. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  93. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  94. package/src/__tests__/validate-input.test.ts +243 -7
  95. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  97. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  98. package/src/acp/acp-claude-oauth.ts +38 -21
  99. package/src/acp/prepare-agent-env.ts +146 -55
  100. package/src/agent/loop-tool-dedup.test.ts +161 -0
  101. package/src/agent/loop.ts +68 -22
  102. package/src/api/constants/app-tools.ts +34 -0
  103. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  104. package/src/api/index.ts +19 -0
  105. package/src/api/responses/resource-pressure-status.ts +25 -0
  106. package/src/approvals/guardian-channel-delivery.ts +70 -6
  107. package/src/approvals/guardian-request-resolvers.ts +32 -66
  108. package/src/channels/__tests__/message-audience.test.ts +49 -0
  109. package/src/channels/__tests__/types.test.ts +17 -3
  110. package/src/channels/message-audience.ts +40 -0
  111. package/src/channels/types.ts +16 -12
  112. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  113. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  114. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  115. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  116. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  117. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  118. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  119. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  120. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  121. package/src/cli/commands/conversations.help.ts +34 -0
  122. package/src/cli/commands/conversations.ts +125 -0
  123. package/src/cli/commands/inference-providers.ts +9 -4
  124. package/src/cli/commands/inference.help.ts +9 -4
  125. package/src/cli/commands/keys.help.ts +13 -1
  126. package/src/cli/commands/keys.ts +26 -15
  127. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  128. package/src/cli/commands/memory/index.help.ts +41 -27
  129. package/src/cli/commands/memory/index.ts +2 -0
  130. package/src/cli/commands/memory/memory-v2.ts +58 -54
  131. package/src/cli/commands/memory/memory-validate.ts +18 -0
  132. package/src/cli/commands/monitoring.ts +1 -0
  133. package/src/cli/commands/plugins.help.ts +28 -31
  134. package/src/cli/commands/plugins.ts +10 -0
  135. package/src/cli/commands/skills.help.ts +13 -16
  136. package/src/cli/commands/trust.ts +3 -14
  137. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  138. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  139. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  140. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  141. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  142. package/src/cli/lib/bundled-marketplace.json +13 -0
  143. package/src/cli/lib/daemon-credential-client.ts +25 -3
  144. package/src/cli/lib/install-from-github.ts +35 -24
  145. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  146. package/src/cli/lib/plugin-surfaces.ts +27 -0
  147. package/src/config/feature-flag-registry.json +35 -19
  148. package/src/config/webhook-routing.ts +8 -0
  149. package/src/context/compactor.ts +31 -2
  150. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  151. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  152. package/src/daemon/conversation-error.ts +13 -0
  153. package/src/daemon/conversation-messaging.ts +22 -2
  154. package/src/daemon/conversation-process.ts +11 -0
  155. package/src/daemon/conversation-store.ts +10 -0
  156. package/src/daemon/conversation-surfaces.ts +21 -8
  157. package/src/daemon/conversation.ts +12 -10
  158. package/src/daemon/lifecycle.ts +6 -2
  159. package/src/daemon/message-types/conversations.ts +5 -7
  160. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  161. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  162. package/src/daemon/resource-pressure-guard.ts +387 -0
  163. package/src/daemon/shutdown-handlers.ts +2 -0
  164. package/src/daemon/startup-error.ts +16 -0
  165. package/src/daemon/trust-context.ts +14 -10
  166. package/src/daemon/unsendable-image-notice.ts +76 -0
  167. package/src/hooks/registry.ts +65 -51
  168. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  169. package/src/ipc/gateway-client.test.ts +1 -1
  170. package/src/ipc/gateway-client.ts +14 -21
  171. package/src/ipc/socket-cleanup.ts +2 -11
  172. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  173. package/src/messaging/provider-message-metadata.ts +128 -0
  174. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  175. package/src/messaging/providers/channel-transport.ts +90 -13
  176. package/src/messaging/providers/discord/send.ts +16 -0
  177. package/src/messaging/providers/discord/transport.ts +11 -1
  178. package/src/messaging/providers/index.ts +109 -16
  179. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  180. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  181. package/src/messaging/providers/slack/send.test.ts +6 -9
  182. package/src/messaging/providers/slack/send.ts +40 -28
  183. package/src/messaging/providers/slack/transport.ts +22 -26
  184. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  185. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  186. package/src/messaging/read-provider-metadata.test.ts +157 -0
  187. package/src/messaging/read-provider-metadata.ts +51 -0
  188. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  189. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  190. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  191. package/src/notifications/adapters/macos.ts +55 -0
  192. package/src/notifications/adapters/slack.ts +10 -5
  193. package/src/notifications/assistant-reply-producer.ts +37 -4
  194. package/src/notifications/conversation-pairing.ts +111 -21
  195. package/src/notifications/edit-notification.ts +38 -8
  196. package/src/notifications/emit-signal.ts +12 -14
  197. package/src/notifications/home-feed-side-effect.ts +218 -15
  198. package/src/notifications/types.ts +5 -0
  199. package/src/permissions/AGENTS.md +16 -0
  200. package/src/permissions/checker.test.ts +75 -122
  201. package/src/permissions/checker.ts +44 -560
  202. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  203. package/src/permissions/confirmation-guardian-request.ts +5 -1
  204. package/src/persistence/conversation-crud.ts +23 -20
  205. package/src/persistence/conversation-queries.ts +14 -1
  206. package/src/persistence/conversation-types.ts +22 -0
  207. package/src/persistence/delivery-crud.ts +122 -15
  208. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  209. package/src/plugin-api/conversation-turn.ts +27 -0
  210. package/src/plugin-api/credential-scope.test.ts +15 -0
  211. package/src/plugin-api/credential-scope.ts +14 -0
  212. package/src/plugin-api/index.ts +19 -1
  213. package/src/plugin-api/resolve-credential.ts +15 -10
  214. package/src/plugin-api/store-credential.ts +148 -0
  215. package/src/plugin-api/system-card.ts +36 -0
  216. package/src/plugin-api/vision-support.test.ts +82 -0
  217. package/src/plugin-api/vision-support.ts +14 -2
  218. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  219. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  220. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  221. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  222. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  223. package/src/plugins/defaults/main.ts +15 -0
  224. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  225. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  226. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  227. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  228. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  229. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  230. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  231. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  232. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  233. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  234. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  238. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  239. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  240. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  241. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  242. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  243. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  244. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  245. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  246. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  247. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  248. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  249. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  250. package/src/plugins/mtime-cache.ts +1 -1
  251. package/src/plugins/pipeline.ts +14 -7
  252. package/src/plugins/plugin-execution-context.ts +35 -11
  253. package/src/plugins/plugin-tree-walk.ts +63 -4
  254. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  255. package/src/providers/anthropic/client.ts +332 -241
  256. package/src/providers/connection-resolution.ts +2 -1
  257. package/src/providers/content-blocks.ts +22 -0
  258. package/src/providers/gemini/client.ts +5 -2
  259. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  260. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  261. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  262. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  263. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  264. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  265. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  266. package/src/providers/inference/adapter-factory.ts +33 -2
  267. package/src/providers/inference/auth.ts +13 -2
  268. package/src/providers/inference/credential-usage.ts +37 -0
  269. package/src/providers/inference/missing-credential-guard.ts +110 -0
  270. package/src/providers/inference/resolve-auth.ts +7 -7
  271. package/src/providers/media-resolve.ts +176 -15
  272. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  273. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  274. package/src/providers/openai/chat-completions-provider.ts +322 -42
  275. package/src/routes/route-host-protocol.ts +7 -0
  276. package/src/routes/worker.ts +31 -10
  277. package/src/runtime/AGENTS.md +35 -0
  278. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  279. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  280. package/src/runtime/assistant-event-hub.ts +38 -0
  281. package/src/runtime/channel-reply-delivery.ts +54 -31
  282. package/src/runtime/channel-retry-sweep.ts +6 -4
  283. package/src/runtime/effective-capabilities.test.ts +19 -0
  284. package/src/runtime/effective-capabilities.ts +25 -0
  285. package/src/runtime/guardian-reply-router.ts +6 -2
  286. package/src/runtime/http-errors.ts +1 -0
  287. package/src/runtime/http-server.ts +20 -5
  288. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  289. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  290. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  291. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  292. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  293. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  294. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  295. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  296. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  297. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  298. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  299. package/src/runtime/routes/approval-routes.ts +37 -4
  300. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  301. package/src/runtime/routes/canned-message-complete.ts +68 -29
  302. package/src/runtime/routes/client-routes.ts +113 -0
  303. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  304. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  305. package/src/runtime/routes/conversation-routes.ts +36 -22
  306. package/src/runtime/routes/credential-in-use.ts +85 -0
  307. package/src/runtime/routes/credential-routes.ts +55 -85
  308. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  309. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  310. package/src/runtime/routes/identity-routes.ts +5 -121
  311. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  312. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  313. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  314. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  315. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  316. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  317. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  318. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  319. package/src/runtime/routes/index.ts +2 -0
  320. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  321. package/src/runtime/routes/monitoring-routes.ts +9 -1
  322. package/src/runtime/routes/plugins-routes.ts +17 -5
  323. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  324. package/src/runtime/routes/secret-routes.ts +39 -8
  325. package/src/runtime/routes/settings-routes.ts +6 -6
  326. package/src/runtime/routes/surface-action-routes.ts +20 -19
  327. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  328. package/src/runtime/routes/user-route-resolution.ts +18 -1
  329. package/src/runtime/slack-reply-session.test.ts +23 -13
  330. package/src/runtime/slack-reply-session.ts +38 -55
  331. package/src/runtime/web-presence.ts +90 -0
  332. package/src/skills/validate-input.ts +177 -40
  333. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  334. package/src/subagent/consult-context.ts +7 -23
  335. package/src/tools/credentials/broker.ts +24 -79
  336. package/src/tools/credentials/ref-parse.ts +35 -0
  337. package/src/tools/credentials/resolve.ts +4 -10
  338. package/src/tools/credentials/store.ts +168 -0
  339. package/src/tools/credentials/tool-policy.ts +67 -0
  340. package/src/tools/executor.ts +53 -15
  341. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  342. package/src/tools/network/url-safety.ts +9 -16
  343. package/src/tools/network/web-search.ts +21 -3
  344. package/src/tools/permission-checker.ts +43 -52
  345. package/src/tools/schema-transforms.ts +40 -5
  346. package/src/tools/shared/input-repairs.ts +160 -0
  347. package/src/tools/skills/skill-tool-factory.ts +18 -4
  348. package/src/tools/subagent/spawn.ts +0 -1
  349. package/src/tools/tool-approval-handler.ts +16 -10
  350. package/src/tools/tool-types.ts +26 -13
  351. package/src/tools/types.ts +6 -6
  352. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  353. package/src/util/cgroup-memory.ts +3 -0
  354. package/src/util/container-cpu-sampler.ts +250 -0
  355. package/src/util/image-conversion.ts +178 -14
  356. package/src/util/platform.ts +93 -10
  357. package/src/permissions/ipc-risk-types.ts +0 -143
  358. package/src/permissions/risk-types.ts +0 -76
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import type { Message, ToolDefinition } from "../../types.js";
4
4
  import {
5
+ isThinkingEnabledOnWire,
5
6
  mapNeutralToolChoice,
6
7
  OpenAIChatCompletionsProvider,
7
8
  } from "../chat-completions-provider.js";
@@ -23,11 +24,17 @@ const USER_MESSAGE: Message[] = [
23
24
  * Stub the SDK client so we can capture the outgoing chat.completions.create
24
25
  * params without hitting the network.
25
26
  */
26
- function stubChatProvider(): {
27
+ function stubChatProvider(
28
+ options?: ConstructorParameters<typeof OpenAIChatCompletionsProvider>[2],
29
+ ): {
27
30
  provider: OpenAIChatCompletionsProvider;
28
31
  requests: Array<Record<string, unknown>>;
29
32
  } {
30
- const provider = new OpenAIChatCompletionsProvider("test-key", "test-model");
33
+ const provider = new OpenAIChatCompletionsProvider(
34
+ "test-key",
35
+ "test-model",
36
+ options,
37
+ );
31
38
  const requests: Array<Record<string, unknown>> = [];
32
39
  (provider as unknown as { client: unknown }).client = {
33
40
  chat: {
@@ -49,6 +56,23 @@ function stubChatProvider(): {
49
56
  return { provider, requests };
50
57
  }
51
58
 
59
+ describe("isThinkingEnabledOnWire", () => {
60
+ test("detects flat reasoning_effort and nested reasoning flags", () => {
61
+ expect(isThinkingEnabledOnWire({})).toBe(false);
62
+ expect(isThinkingEnabledOnWire({ reasoning_effort: "none" })).toBe(false);
63
+ expect(isThinkingEnabledOnWire({ reasoning_effort: "high" })).toBe(true);
64
+ expect(isThinkingEnabledOnWire({ reasoning: { effort: "none" } })).toBe(
65
+ false,
66
+ );
67
+ expect(isThinkingEnabledOnWire({ reasoning: { effort: "medium" } })).toBe(
68
+ true,
69
+ );
70
+ expect(isThinkingEnabledOnWire({ reasoning: { enabled: true } })).toBe(
71
+ true,
72
+ );
73
+ });
74
+ });
75
+
52
76
  describe("mapNeutralToolChoice (chat-completions wire format)", () => {
53
77
  // Each neutral tool_choice variant maps to its OpenAI-compatible form.
54
78
  test("maps the Anthropic-shaped union to OpenAI's tool_choice values", () => {
@@ -144,4 +168,103 @@ describe("OpenAIChatCompletionsProvider tool_choice wiring", () => {
144
168
  expect(requests[0].tools).toBeUndefined();
145
169
  expect(requests[0].tool_choice).toBeUndefined();
146
170
  });
171
+
172
+ // `"auto"` is the chat-completions default when tools are present. Sending it
173
+ // alongside reasoning_effort 400s DeepSeek thinking mode, so it is omitted.
174
+ test("omits redundant tool_choice auto when thinking is on the wire", async () => {
175
+ const { provider, requests } = stubChatProvider();
176
+
177
+ await provider.sendMessage(USER_MESSAGE, {
178
+ tools: TOOLS,
179
+ config: { tool_choice: { type: "auto" }, effort: "high" },
180
+ });
181
+
182
+ expect(requests[0].reasoning_effort).toBe("high");
183
+ expect(requests[0].tool_choice).toBeUndefined();
184
+ expect(requests[0].tools).toBeDefined();
185
+ });
186
+
187
+ test("omits redundant tool_choice auto when nested reasoning.enabled is set", async () => {
188
+ const { provider, requests } = stubChatProvider({
189
+ extraCreateParams: { reasoning: { enabled: true } },
190
+ });
191
+
192
+ await provider.sendMessage(USER_MESSAGE, {
193
+ tools: TOOLS,
194
+ config: { tool_choice: { type: "auto" } },
195
+ });
196
+
197
+ expect(requests[0].tool_choice).toBeUndefined();
198
+ });
199
+
200
+ test("forwards tool_choice auto when thinking is off", async () => {
201
+ const { provider, requests } = stubChatProvider();
202
+
203
+ await provider.sendMessage(USER_MESSAGE, {
204
+ tools: TOOLS,
205
+ config: { tool_choice: { type: "auto" } },
206
+ });
207
+
208
+ expect(requests[0].tool_choice).toBe("auto");
209
+ });
210
+
211
+ // Catalog providers (Fireworks, Together) honor `none` with thinking.
212
+ test("forwards tool_choice none with thinking when omitToolChoiceWhenReasoning is off", async () => {
213
+ const { provider, requests } = stubChatProvider();
214
+
215
+ await provider.sendMessage(USER_MESSAGE, {
216
+ tools: TOOLS,
217
+ config: { tool_choice: { type: "none" }, effort: "high" },
218
+ });
219
+
220
+ expect(requests[0].reasoning_effort).toBe("high");
221
+ expect(requests[0].tool_choice).toBe("none");
222
+ });
223
+
224
+ test("forwards a forced tool_choice with thinking when omitToolChoiceWhenReasoning is off", async () => {
225
+ const { provider, requests } = stubChatProvider();
226
+
227
+ await provider.sendMessage(USER_MESSAGE, {
228
+ tools: TOOLS,
229
+ config: { tool_choice: { type: "tool", name: "bash" }, effort: "high" },
230
+ });
231
+
232
+ expect(requests[0].tool_choice).toEqual({
233
+ type: "function",
234
+ function: { name: "bash" },
235
+ });
236
+ });
237
+
238
+ test("omits every tool_choice in thinking mode when omitToolChoiceWhenReasoning is on", async () => {
239
+ const { provider, requests } = stubChatProvider({
240
+ omitToolChoiceWhenReasoning: true,
241
+ });
242
+
243
+ await provider.sendMessage(USER_MESSAGE, {
244
+ tools: TOOLS,
245
+ config: { tool_choice: { type: "none" }, effort: "high" },
246
+ });
247
+ await provider.sendMessage(USER_MESSAGE, {
248
+ tools: TOOLS,
249
+ config: { tool_choice: { type: "tool", name: "bash" }, effort: "high" },
250
+ });
251
+
252
+ expect(requests[0].reasoning_effort).toBe("high");
253
+ expect(requests[0].tool_choice).toBeUndefined();
254
+ expect(requests[1].tool_choice).toBeUndefined();
255
+ });
256
+
257
+ test("still forwards tool_choice when omitToolChoiceWhenReasoning is on but thinking is off", async () => {
258
+ const { provider, requests } = stubChatProvider({
259
+ omitToolChoiceWhenReasoning: true,
260
+ });
261
+
262
+ await provider.sendMessage(USER_MESSAGE, {
263
+ tools: TOOLS,
264
+ config: { tool_choice: { type: "none" } },
265
+ });
266
+
267
+ expect(requests[0].reasoning_effort).toBeUndefined();
268
+ expect(requests[0].tool_choice).toBe("none");
269
+ });
147
270
  });
@@ -162,14 +162,19 @@ export interface OpenAIChatCompletionsProviderOptions {
162
162
  parseThinkTags?: boolean;
163
163
  /** Wire field used to replay prior assistant thinking on multi-turn requests.
164
164
  * DeepSeek/Fireworks use `"reasoning_content"`; OpenRouter uses `"reasoning"`.
165
- * When unset, thinking blocks are dropped from outbound assistant messages. */
165
+ * When unset, thinking blocks are dropped from outbound assistant messages.
166
+ * When set, the field is included only if there is thinking to replay, so a
167
+ * standard Chat Completions endpoint does not see an extra key on ordinary
168
+ * tool-call turns. DeepSeek thinking mode that requires the field even when
169
+ * empty is handled by a one-shot retry. */
166
170
  assistantReasoningField?: "reasoning" | "reasoning_content";
167
171
  /** Backfill a non-empty placeholder for assistant turns that would otherwise
168
172
  * serialize with neither `content` nor `tool_calls` (e.g. reasoning-only
169
- * turns). Off by default; enabled for OpenRouter, whose downstream providers
170
- * (e.g. DeepSeek) reject such messages with `Invalid assistant message:
171
- * content or tool_calls must be set`. See {@link
172
- * EMPTY_ASSISTANT_TURN_PLACEHOLDER}. */
173
+ * turns, or a Stop mid-stream before any text). Off by default; enabled for
174
+ * OpenRouter, Vercel AI Gateway, LiteLLM, and custom `openai-compatible`
175
+ * endpoints, whose downstream providers (e.g. DeepSeek, vLLM, Portkey)
176
+ * reject such messages with `Invalid assistant message: content or
177
+ * tool_calls must be set`. See {@link EMPTY_ASSISTANT_TURN_PLACEHOLDER}. */
173
178
  backfillEmptyAssistantContent?: boolean;
174
179
  /** Present object-typed tool params to the model as JSON-string params and
175
180
  * decode them back to objects on the response. Works around models whose
@@ -177,6 +182,13 @@ export interface OpenAIChatCompletionsProviderOptions {
177
182
  * with minimax-m3 on Fireworks). Off by default; scalars/arrays unaffected.
178
183
  * See {@link coerceObjectParamsToJsonString}. */
179
184
  coerceObjectArgsToJsonString?: boolean;
185
+ /** Drop `tool_choice` when thinking/reasoning is on the wire. Strict
186
+ * OpenAI-compatible reasoning upstreams (DeepSeek thinking mode) reject any
187
+ * explicit `tool_choice` with `Thinking mode does not support this
188
+ * tool_choice`. Off by default so catalog providers that honor the combo
189
+ * (Fireworks, Together) keep sending `none` / forced choices. Enabled for
190
+ * the generic `openai-compatible` adapter, whose upstream is unknown. */
191
+ omitToolChoiceWhenReasoning?: boolean;
180
192
  }
181
193
 
182
194
  const log = getLogger("chat-completions");
@@ -219,12 +231,29 @@ export function clampReasoningEffort(
219
231
  : value;
220
232
  }
221
233
 
234
+ /** Human-readable text from an OpenAI-compatible error, including wrapped
235
+ * upstream detail (OpenRouter `metadata.raw`). Used by the one-shot
236
+ * compatibility retries so a generic SDK wrapper message cannot hide the
237
+ * real reason. */
238
+ function openaiCompatErrorHaystack(error: unknown): string {
239
+ return error instanceof OpenAI.APIError
240
+ ? normalizedErrorText(normalizeOpenAIAPIError(error))
241
+ : error instanceof Error
242
+ ? error.message
243
+ : String(error);
244
+ }
245
+
246
+ function isClientErrorStatus(error: unknown): boolean {
247
+ const status = (error as { status?: unknown }).status;
248
+ return typeof status === "number" && status >= 400 && status < 500;
249
+ }
250
+
222
251
  /**
223
252
  * True when the request carried an explicit reasoning opt-out (`"none"` sent
224
253
  * as flat `reasoning_effort` or nested `reasoning.effort`) and the provider
225
254
  * rejected it with a 4xx that names the reasoning field. Reasoning-only
226
255
  * models (e.g. DeepSeek R1) reject the opt-out rather than ignore it; that
227
- * one case is worth a single retry with the reasoning params stripped —
256
+ * one case is worth a single retry with the reasoning params stripped:
228
257
  * model-default reasoning beats a hard failure.
229
258
  */
230
259
  function isReasoningOptOutRejection(error: unknown, params: unknown): boolean {
@@ -240,22 +269,217 @@ function isReasoningOptOutRejection(error: unknown, params: unknown): boolean {
240
269
  if (!optedOut) {
241
270
  return false;
242
271
  }
243
- const status = (error as { status?: unknown }).status;
244
- if (typeof status !== "number" || status < 400 || status >= 500) {
272
+ if (!isClientErrorStatus(error)) {
273
+ return false;
274
+ }
275
+ return /reasoning/i.test(openaiCompatErrorHaystack(error));
276
+ }
277
+
278
+ /**
279
+ * True when thinking/reasoning is active on the outbound chat-completions
280
+ * body: a non-`"none"` `reasoning_effort`, a nested `reasoning.effort`, or
281
+ * `reasoning.enabled: true` (OpenRouter / Vercel AI Gateway).
282
+ */
283
+ export function isThinkingEnabledOnWire(params: unknown): boolean {
284
+ const p = params as {
285
+ reasoning_effort?: unknown;
286
+ reasoning?: { effort?: unknown; enabled?: unknown } | null;
287
+ };
288
+ const nested = p.reasoning;
289
+ if (nested && typeof nested === "object") {
290
+ if (nested.enabled === true) {
291
+ return true;
292
+ }
293
+ if (typeof nested.effort === "string" && nested.effort !== "none") {
294
+ return true;
295
+ }
296
+ }
297
+ return (
298
+ typeof p.reasoning_effort === "string" && p.reasoning_effort !== "none"
299
+ );
300
+ }
301
+
302
+ /**
303
+ * True when the request sent an explicit `tool_choice` and the provider
304
+ * rejected it because thinking/reasoning mode forbids that parameter.
305
+ * DeepSeek thinking mode 400s with `Thinking mode does not support this
306
+ * tool_choice` for any explicit value, including `"auto"` and `"none"`.
307
+ * One retry without `tool_choice` lets the same provider succeed instead of
308
+ * failing over to a different backend.
309
+ */
310
+ function isThinkingModeToolChoiceRejection(
311
+ error: unknown,
312
+ params: unknown,
313
+ ): boolean {
314
+ const p = params as { tool_choice?: unknown };
315
+ if (p.tool_choice === undefined) {
316
+ return false;
317
+ }
318
+ if (!isClientErrorStatus(error)) {
319
+ return false;
320
+ }
321
+ return /does not support this tool_choice/i.test(
322
+ openaiCompatErrorHaystack(error),
323
+ );
324
+ }
325
+
326
+ type AssistantReasoningExtras = {
327
+ reasoning?: string;
328
+ reasoning_content?: string;
329
+ };
330
+
331
+ function assistantReasoningExtras(
332
+ msg: OpenAI.Chat.Completions.ChatCompletionMessageParam,
333
+ ): AssistantReasoningExtras | null {
334
+ if (msg.role !== "assistant") {
335
+ return null;
336
+ }
337
+ return msg as OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam &
338
+ AssistantReasoningExtras;
339
+ }
340
+
341
+ function paramsMessages(
342
+ params: unknown,
343
+ ): OpenAI.Chat.Completions.ChatCompletionMessageParam[] | undefined {
344
+ const messages = (params as { messages?: unknown }).messages;
345
+ return Array.isArray(messages)
346
+ ? (messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[])
347
+ : undefined;
348
+ }
349
+
350
+ function messagesCarryAssistantReasoningField(params: unknown): boolean {
351
+ const messages = paramsMessages(params);
352
+ if (!messages) {
353
+ return false;
354
+ }
355
+ return messages.some((msg) => {
356
+ const extra = assistantReasoningExtras(msg);
357
+ return (
358
+ extra !== null &&
359
+ (extra.reasoning !== undefined || extra.reasoning_content !== undefined)
360
+ );
361
+ });
362
+ }
363
+
364
+ function assistantMessagesNeedReasoningContentBackfill(
365
+ params: unknown,
366
+ ): boolean {
367
+ const messages = paramsMessages(params);
368
+ if (!messages) {
369
+ return false;
370
+ }
371
+ return messages.some((msg) => {
372
+ const extra = assistantReasoningExtras(msg);
373
+ return (
374
+ extra !== null &&
375
+ extra.reasoning_content === undefined &&
376
+ extra.reasoning === undefined
377
+ );
378
+ });
379
+ }
380
+
381
+ function stripAssistantReasoningFields(params: unknown): boolean {
382
+ const messages = paramsMessages(params);
383
+ if (!messages) {
384
+ return false;
385
+ }
386
+ let stripped = false;
387
+ for (const msg of messages) {
388
+ const extra = assistantReasoningExtras(msg);
389
+ if (extra === null) {
390
+ continue;
391
+ }
392
+ if (extra.reasoning_content !== undefined) {
393
+ delete extra.reasoning_content;
394
+ stripped = true;
395
+ }
396
+ if (extra.reasoning !== undefined) {
397
+ delete extra.reasoning;
398
+ stripped = true;
399
+ }
400
+ }
401
+ return stripped;
402
+ }
403
+
404
+ function backfillEmptyReasoningContent(params: unknown): boolean {
405
+ const messages = paramsMessages(params);
406
+ if (!messages) {
407
+ return false;
408
+ }
409
+ let added = false;
410
+ for (const msg of messages) {
411
+ const extra = assistantReasoningExtras(msg);
412
+ if (extra === null) {
413
+ continue;
414
+ }
415
+ if (
416
+ extra.reasoning_content !== undefined ||
417
+ extra.reasoning !== undefined
418
+ ) {
419
+ continue;
420
+ }
421
+ extra.reasoning_content = "";
422
+ added = true;
423
+ }
424
+ return added;
425
+ }
426
+
427
+ function haystackNamesAssistantReasoningField(haystack: string): boolean {
428
+ if (/reasoning_content/i.test(haystack)) {
429
+ return true;
430
+ }
431
+ return /\breasoning\b/i.test(haystack) && !/reasoning_effort/i.test(haystack);
432
+ }
433
+
434
+ /**
435
+ * True when thinking-mode requires `reasoning_content` on subsequent
436
+ * assistant messages and this request omitted it. DeepSeek 400s with
437
+ * `The reasoning_content in the thinking mode must be passed back to the API`.
438
+ * One retry with an empty string on those assistant messages satisfies the
439
+ * presence check without putting the extra key on every custom-endpoint turn.
440
+ */
441
+ function isMissingReasoningContentRejection(
442
+ error: unknown,
443
+ params: unknown,
444
+ ): boolean {
445
+ if (!isClientErrorStatus(error)) {
446
+ return false;
447
+ }
448
+ const haystack = openaiCompatErrorHaystack(error);
449
+ if (
450
+ !/reasoning_content/i.test(haystack) ||
451
+ !/must be passed back/i.test(haystack)
452
+ ) {
453
+ return false;
454
+ }
455
+ return assistantMessagesNeedReasoningContentBackfill(params);
456
+ }
457
+
458
+ /**
459
+ * True when the request included an assistant `reasoning` / `reasoning_content`
460
+ * extra and the provider rejected it as an unknown message property. One retry
461
+ * without those extras lets a strict Chat Completions schema succeed.
462
+ */
463
+ function isUnknownAssistantReasoningFieldRejection(
464
+ error: unknown,
465
+ params: unknown,
466
+ ): boolean {
467
+ if (!isClientErrorStatus(error)) {
468
+ return false;
469
+ }
470
+ if (!messagesCarryAssistantReasoningField(params)) {
471
+ return false;
472
+ }
473
+ const haystack = openaiCompatErrorHaystack(error);
474
+ if (/must be passed back/i.test(haystack)) {
245
475
  return false;
246
476
  }
247
- // OpenRouter wraps upstream 4xxs in a generic "Provider returned error" and
248
- // stashes the real reason under `metadata.raw`, which `normalizeOpenAIAPIError`
249
- // promotes into the normalized fields. Scan those, not just `error.message` —
250
- // otherwise the wrapped reasoning rejection is missed and this one-shot
251
- // fallback never fires.
252
- const haystack =
253
- error instanceof OpenAI.APIError
254
- ? normalizedErrorText(normalizeOpenAIAPIError(error))
255
- : error instanceof Error
256
- ? error.message
257
- : String(error);
258
- return /reasoning/i.test(haystack);
477
+ if (!haystackNamesAssistantReasoningField(haystack)) {
478
+ return false;
479
+ }
480
+ return /unknown|unexpected|unrecognized|additional propert|extra (?:field|property)|not (?:a )?valid|invalid (?:argument|parameter|field|property)/i.test(
481
+ haystack,
482
+ );
259
483
  }
260
484
 
261
485
  /**
@@ -334,6 +558,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
334
558
  | undefined;
335
559
  private backfillEmptyAssistantContent: boolean;
336
560
  private coerceObjectArgsToJsonString: boolean;
561
+ private omitToolChoiceWhenReasoning: boolean;
337
562
 
338
563
  constructor(
339
564
  apiKey: string,
@@ -363,6 +588,8 @@ export class OpenAIChatCompletionsProvider implements Provider {
363
588
  options.backfillEmptyAssistantContent ?? false;
364
589
  this.coerceObjectArgsToJsonString =
365
590
  options.coerceObjectArgsToJsonString ?? false;
591
+ this.omitToolChoiceWhenReasoning =
592
+ options.omitToolChoiceWhenReasoning ?? false;
366
593
  }
367
594
 
368
595
  get defaultModel(): string {
@@ -469,11 +696,24 @@ export class OpenAIChatCompletionsProvider implements Provider {
469
696
 
470
697
  // Honor a caller-supplied tool_choice (e.g. `{ type: "none" }` to force
471
698
  // a text-only answer, or `{ type: "tool", name }` for a forced call).
472
- // Only meaningful when tools are present — OpenAI rejects a named or
699
+ // Only meaningful when tools are present: OpenAI rejects a named or
473
700
  // "required" choice with no tools.
701
+ //
702
+ // Strict reasoning upstreams (DeepSeek thinking mode) reject any
703
+ // explicit tool_choice, including `"auto"` (the API default when tools
704
+ // are present) and `"none"`. Omit `"auto"` whenever thinking is on the
705
+ // wire. Catalog providers that honor `none` / forced choices still
706
+ // receive them; the generic openai-compatible adapter drops every
707
+ // explicit value in thinking mode via `omitToolChoiceWhenReasoning`.
474
708
  const toolChoice = mapNeutralToolChoice(configObj?.tool_choice);
475
709
  if (toolChoice !== undefined) {
476
- params.tool_choice = toolChoice;
710
+ const thinkingOn = isThinkingEnabledOnWire(params);
711
+ const skipAutoDefault = thinkingOn && toolChoice === "auto";
712
+ const skipAllChoices =
713
+ thinkingOn && this.omitToolChoiceWhenReasoning;
714
+ if (!skipAutoDefault && !skipAllChoices) {
715
+ params.tool_choice = toolChoice;
716
+ }
477
717
  }
478
718
  }
479
719
 
@@ -573,20 +813,54 @@ export class OpenAIChatCompletionsProvider implements Provider {
573
813
  try {
574
814
  stream = await createStream();
575
815
  } catch (error) {
576
- if (!isReasoningOptOutRejection(error, params)) {
816
+ if (isReasoningOptOutRejection(error, params)) {
817
+ log.warn(
818
+ {
819
+ provider: this.name,
820
+ model: modelOverride ?? this.model,
821
+ error: error instanceof Error ? error.message : String(error),
822
+ },
823
+ "Model rejected the explicit reasoning opt-out; retrying without reasoning params",
824
+ );
825
+ delete params.reasoning_effort;
826
+ delete (params as unknown as Record<string, unknown>).reasoning;
827
+ stream = await createStream();
828
+ } else if (isThinkingModeToolChoiceRejection(error, params)) {
829
+ log.warn(
830
+ {
831
+ provider: this.name,
832
+ model: modelOverride ?? this.model,
833
+ error: error instanceof Error ? error.message : String(error),
834
+ },
835
+ "Upstream rejected tool_choice in thinking mode; retrying without tool_choice",
836
+ );
837
+ delete params.tool_choice;
838
+ stream = await createStream();
839
+ } else if (isMissingReasoningContentRejection(error, params)) {
840
+ log.warn(
841
+ {
842
+ provider: this.name,
843
+ model: modelOverride ?? this.model,
844
+ error: error instanceof Error ? error.message : String(error),
845
+ },
846
+ "Upstream requires reasoning_content round-trip; retrying with empty field on assistant messages",
847
+ );
848
+ backfillEmptyReasoningContent(params);
849
+ stream = await createStream();
850
+ } else if (isUnknownAssistantReasoningFieldRejection(error, params)) {
851
+ log.warn(
852
+ {
853
+ provider: this.name,
854
+ model: modelOverride ?? this.model,
855
+ error: error instanceof Error ? error.message : String(error),
856
+ },
857
+ "Upstream rejected assistant reasoning field; retrying without it",
858
+ );
859
+ stripAssistantReasoningFields(params);
860
+ stream = await createStream();
861
+ } else {
577
862
  throw error;
578
863
  }
579
- log.warn(
580
- {
581
- provider: this.name,
582
- model: modelOverride ?? this.model,
583
- error: error instanceof Error ? error.message : String(error),
584
- },
585
- "Model rejected the explicit reasoning opt-out; retrying without reasoning params",
586
- );
587
- delete params.reasoning_effort;
588
- delete (params as unknown as Record<string, unknown>).reasoning;
589
- stream = await createStream();
590
864
  }
591
865
 
592
866
  for await (const chunk of stream) {
@@ -1095,6 +1369,14 @@ export class OpenAIChatCompletionsProvider implements Provider {
1095
1369
  content: textParts.length > 0 ? textParts.join("") : null,
1096
1370
  };
1097
1371
 
1372
+ if (toolCalls.length > 0) {
1373
+ result.tool_calls = toolCalls;
1374
+ }
1375
+
1376
+ // Include the configured wire field only when there is thinking to replay.
1377
+ // Ordinary tool-call turns omit it so a strict Chat Completions schema
1378
+ // does not reject an extra key. Empty-field presence for DeepSeek is a
1379
+ // one-shot retry, not the default serialization.
1098
1380
  if (reasoningParts.length > 0 && this.assistantReasoningField) {
1099
1381
  (
1100
1382
  result as OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam & {
@@ -1104,15 +1386,13 @@ export class OpenAIChatCompletionsProvider implements Provider {
1104
1386
  )[this.assistantReasoningField] = reasoningParts.join("");
1105
1387
  }
1106
1388
 
1107
- if (toolCalls.length > 0) {
1108
- result.tool_calls = toolCalls;
1109
- }
1110
-
1111
1389
  // An assistant message must carry `content` or `tool_calls`. A turn with
1112
- // neither (e.g. reasoning-only) would serialize to null/empty content with
1113
- // no tool calls, which strict OpenAI-compatible backends reject. Reasoning
1114
- // lives in a separate field and does not satisfy this constraint. Scoped to
1115
- // providers that need it (OpenRouter) via `backfillEmptyAssistantContent`.
1390
+ // neither (e.g. reasoning-only, or a Stop before any text) would serialize
1391
+ // to null/empty content with no tool calls, which strict OpenAI-compatible
1392
+ // backends reject. Reasoning lives in a separate field and does not
1393
+ // satisfy this constraint. Scoped to providers that need it (OpenRouter,
1394
+ // Vercel AI Gateway, LiteLLM, openai-compatible) via
1395
+ // `backfillEmptyAssistantContent`.
1116
1396
  if (
1117
1397
  this.backfillEmptyAssistantContent &&
1118
1398
  !result.tool_calls &&
@@ -33,6 +33,13 @@ export interface RouteInvokeParams {
33
33
  readonly url: string;
34
34
  /** Request header entries as `[name, value]` pairs (preserves duplicates). */
35
35
  readonly headers: ReadonlyArray<readonly [string, string]>;
36
+ /**
37
+ * Manifest name of the plugin whose namespace the route falls in, absent for
38
+ * a workspace route. The host marks it as the plugin in context for the
39
+ * handler's execution, so plugin-scoped host APIs behave the same whether the
40
+ * handler runs in the host or in-thread on the daemon.
41
+ */
42
+ readonly pluginName?: string;
36
43
  }
37
44
 
38
45
  /**
@@ -24,6 +24,7 @@ import { createServer, type Server, type Socket } from "node:net";
24
24
  import type { IpcEnvelope } from "@vellumai/ipc-server-utils";
25
25
  import { IpcFrameReader, writeMessage } from "@vellumai/ipc-server-utils";
26
26
 
27
+ import { runInPluginContext } from "../plugins/plugin-execution-context.js";
27
28
  import { disableStreamSeqStamping } from "../runtime/assistant-stream-state.js";
28
29
  import {
29
30
  evictRouteSourceTree,
@@ -126,26 +127,46 @@ async function handleInvoke(
126
127
  evictRouteSourceTree(sourceRootForHandler(params.filePath));
127
128
  lastHandlerMtime.set(params.filePath, params.mtimeMs);
128
129
  }
129
- const mod = await importRouteModule(params.filePath);
130
130
 
131
- const handler = mod[params.method];
132
- if (typeof handler !== "function") {
133
- const allowed = HTTP_METHODS.filter((m) => typeof mod[m] === "function");
131
+ // A plugin's own routes execute as that plugin, matching the daemon's
132
+ // in-thread path, so plugin-scoped host APIs a handler reaches
133
+ // (`resolveCredential`, `indexDocument`) scope to the owning plugin rather
134
+ // than falling through to their unscoped branch. The context covers the
135
+ // import as well as the call: a route module can reach a scoped API at
136
+ // evaluation time, and it is imported once per mtime.
137
+ const serve = async (): Promise<
138
+ { ok: true; response: Response } | { ok: false; allowed: string[] }
139
+ > => {
140
+ const mod = await importRouteModule(params.filePath);
141
+ const handler = mod[params.method];
142
+ if (typeof handler !== "function") {
143
+ return {
144
+ ok: false,
145
+ allowed: HTTP_METHODS.filter((m) => typeof mod[m] === "function"),
146
+ };
147
+ }
148
+ const request = reconstructRequest(params, body);
149
+ const response = (await (handler as (req: Request) => unknown)(
150
+ request,
151
+ )) as Response;
152
+ return { ok: true, response };
153
+ };
154
+ const outcome = await (params.pluginName
155
+ ? runInPluginContext(params.pluginName, serve)
156
+ : serve());
157
+
158
+ if (!outcome.ok) {
134
159
  replyResult(
135
160
  socket,
136
161
  id,
137
162
  405,
138
- allowed.length ? [["allow", allowed.join(", ")]] : [],
163
+ outcome.allowed.length ? [["allow", outcome.allowed.join(", ")]] : [],
139
164
  null,
140
165
  );
141
166
  return;
142
167
  }
143
168
 
144
- const request = reconstructRequest(params, body);
145
- const response = (await (handler as (req: Request) => unknown)(
146
- request,
147
- )) as Response;
148
-
169
+ const { response } = outcome;
149
170
  const buffer = new Uint8Array(await response.arrayBuffer());
150
171
  const headers: [string, string][] = [];
151
172
  response.headers.forEach((value, name) => {