@vellumai/assistant 0.8.3 → 0.8.4

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 (342) hide show
  1. package/docker-entrypoint.sh +0 -1
  2. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  3. package/openapi.yaml +610 -16
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
  6. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  7. package/src/__tests__/agent-loop.test.ts +88 -3
  8. package/src/__tests__/anthropic-provider.test.ts +272 -0
  9. package/src/__tests__/approval-cascade.test.ts +1 -1
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  11. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  12. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  13. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  14. package/src/__tests__/compaction-events.test.ts +1 -1
  15. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  16. package/src/__tests__/config-watcher.test.ts +1 -1
  17. package/src/__tests__/context-token-estimator.test.ts +91 -1
  18. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  19. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +25 -7
  22. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  23. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  24. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  25. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  26. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  27. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  28. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  29. package/src/__tests__/conversation-pairing.test.ts +2 -2
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  32. package/src/__tests__/conversation-queue.test.ts +1 -1
  33. package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
  34. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  35. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  36. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  37. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  38. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  39. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  40. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  42. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  43. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  45. package/src/__tests__/dm-backfill.test.ts +64 -0
  46. package/src/__tests__/dm-persistence.test.ts +33 -0
  47. package/src/__tests__/document-find-replace.test.ts +501 -0
  48. package/src/__tests__/first-greeting.test.ts +23 -2
  49. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  50. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  51. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  52. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  53. package/src/__tests__/host-file-proxy.test.ts +8 -1
  54. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  55. package/src/__tests__/identity-routes.test.ts +57 -0
  56. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  57. package/src/__tests__/injector-chain.test.ts +2 -0
  58. package/src/__tests__/injector-document-comments.test.ts +378 -0
  59. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  60. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  61. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  62. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  63. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  64. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  65. package/src/__tests__/llm-resolver.test.ts +85 -1
  66. package/src/__tests__/log-export-routes.test.ts +99 -2
  67. package/src/__tests__/message-queue-steer.test.ts +114 -0
  68. package/src/__tests__/openai-provider.test.ts +105 -0
  69. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  70. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  72. package/src/__tests__/platform.test.ts +0 -3
  73. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  74. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  75. package/src/__tests__/process-message-display-content.test.ts +21 -16
  76. package/src/__tests__/server-history-render.test.ts +83 -4
  77. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  78. package/src/__tests__/system-prompt.test.ts +51 -28
  79. package/src/__tests__/terminal-tools.test.ts +11 -1
  80. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  81. package/src/__tests__/thread-backfill.test.ts +370 -22
  82. package/src/__tests__/tool-executor.test.ts +90 -1
  83. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  84. package/src/__tests__/twilio-routes.test.ts +1 -1
  85. package/src/__tests__/web-fetch.test.ts +2 -2
  86. package/src/__tests__/workspace-git-service.test.ts +88 -5
  87. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  88. package/src/agent/attachments.ts +1 -0
  89. package/src/agent/loop.ts +57 -20
  90. package/src/background-wake/next-wake.test.ts +289 -0
  91. package/src/background-wake/next-wake.ts +172 -0
  92. package/src/browser/operations.ts +15 -0
  93. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  94. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  95. package/src/cli/commands/conversations.ts +128 -1
  96. package/src/cli/commands/inference-providers.ts +147 -1
  97. package/src/cli/commands/memory-v2.ts +308 -0
  98. package/src/cli/commands/notifications.ts +24 -2
  99. package/src/cli/utils/conversation-id.ts +17 -5
  100. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  101. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  102. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  103. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  104. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  105. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  106. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  107. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  108. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  109. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  110. package/src/config/bundled-tool-registry.ts +22 -12
  111. package/src/config/call-site-defaults.ts +19 -0
  112. package/src/config/feature-flag-registry.json +99 -3
  113. package/src/config/llm-resolver.ts +16 -2
  114. package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
  115. package/src/config/schemas/call-site-catalog.ts +21 -0
  116. package/src/config/schemas/llm.ts +3 -0
  117. package/src/config/schemas/memory-v2.ts +48 -1
  118. package/src/context/compactor.ts +8 -1
  119. package/src/context/token-estimator.ts +47 -4
  120. package/src/context/window-manager.ts +25 -0
  121. package/src/credential-health/credential-health-service.ts +34 -19
  122. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  123. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  124. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  125. package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
  126. package/src/daemon/conversation-agent-loop.ts +223 -54
  127. package/src/daemon/conversation-lifecycle.ts +142 -116
  128. package/src/daemon/conversation-messaging.ts +3 -0
  129. package/src/daemon/conversation-process.ts +273 -0
  130. package/src/daemon/conversation-queue-manager.ts +14 -0
  131. package/src/daemon/conversation-runtime-assembly.ts +135 -75
  132. package/src/daemon/conversation-slash.ts +37 -5
  133. package/src/daemon/conversation-surfaces.ts +45 -2
  134. package/src/daemon/conversation-tool-setup.ts +7 -0
  135. package/src/daemon/conversation.ts +42 -5
  136. package/src/daemon/first-greeting.ts +10 -0
  137. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  138. package/src/daemon/handlers/config-a2a.ts +160 -0
  139. package/src/daemon/handlers/config-model.test.ts +1 -0
  140. package/src/daemon/handlers/conversations.ts +79 -0
  141. package/src/daemon/handlers/shared.ts +92 -29
  142. package/src/daemon/host-bash-proxy.ts +1 -1
  143. package/src/daemon/host-cu-proxy.ts +1 -1
  144. package/src/daemon/host-file-proxy.ts +1 -1
  145. package/src/daemon/host-transfer-proxy.ts +1 -1
  146. package/src/daemon/lifecycle.ts +18 -4
  147. package/src/daemon/message-protocol.ts +4 -0
  148. package/src/daemon/message-types/conversations.ts +8 -0
  149. package/src/daemon/message-types/document-comments.ts +50 -0
  150. package/src/daemon/message-types/messages.ts +68 -1
  151. package/src/daemon/message-types/surfaces.ts +3 -1
  152. package/src/daemon/message-types/web-activity.ts +57 -0
  153. package/src/daemon/plugin-source-watcher.ts +135 -3
  154. package/src/daemon/process-message.ts +69 -12
  155. package/src/daemon/query-complexity-router.ts +75 -0
  156. package/src/daemon/trust-context.ts +6 -0
  157. package/src/documents/document-comments-store.test.ts +338 -0
  158. package/src/documents/document-comments-store.ts +237 -0
  159. package/src/documents/document-store.ts +202 -0
  160. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  161. package/src/heartbeat/heartbeat-service.ts +1 -0
  162. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  163. package/src/home/feed-types.ts +6 -1
  164. package/src/home/home-content-refresh.ts +52 -0
  165. package/src/home/home-greeting-cache.ts +69 -0
  166. package/src/home/home-greeting.ts +94 -0
  167. package/src/home/suggested-prompts.ts +177 -9
  168. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  169. package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
  170. package/src/memory/conversation-crud.ts +133 -43
  171. package/src/memory/db-init.ts +16 -0
  172. package/src/memory/delivery-crud.ts +41 -0
  173. package/src/memory/delivery-status.ts +141 -15
  174. package/src/memory/external-conversation-store.ts +32 -1
  175. package/src/memory/jobs-worker.ts +21 -1
  176. package/src/memory/memory-retrospective-constants.ts +28 -0
  177. package/src/memory/memory-retrospective-enqueue.ts +3 -2
  178. package/src/memory/memory-retrospective-job.ts +408 -18
  179. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  180. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  181. package/src/memory/migrations/100-core-tables.ts +1 -0
  182. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  183. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  184. package/src/memory/migrations/253-document-comments.ts +47 -0
  185. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  186. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  187. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  188. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  189. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  190. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  191. package/src/memory/migrations/index.ts +17 -0
  192. package/src/memory/migrations/registry.ts +25 -0
  193. package/src/memory/onboarding-events-store.ts +7 -0
  194. package/src/memory/schema/calls.ts +1 -0
  195. package/src/memory/schema/conversations.ts +3 -0
  196. package/src/memory/schema/infrastructure.ts +1 -0
  197. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  198. package/src/memory/v2/__tests__/injection.test.ts +31 -14
  199. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  200. package/src/memory/v2/__tests__/router.test.ts +489 -1
  201. package/src/memory/v2/consolidation-job.ts +14 -0
  202. package/src/memory/v2/injection-events.ts +101 -0
  203. package/src/memory/v2/injection.ts +21 -10
  204. package/src/memory/v2/page-index.ts +209 -7
  205. package/src/memory/v2/page-store.ts +18 -0
  206. package/src/memory/v2/router.ts +209 -55
  207. package/src/messaging/providers/index.ts +7 -1
  208. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  209. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  210. package/src/messaging/providers/slack/adapter.ts +178 -25
  211. package/src/messaging/providers/slack/api.test.ts +54 -0
  212. package/src/messaging/providers/slack/api.ts +119 -3
  213. package/src/messaging/providers/slack/client.ts +12 -0
  214. package/src/messaging/providers/slack/deep-link.ts +20 -1
  215. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  216. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  217. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  218. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  219. package/src/messaging/providers/slack/send.test.ts +77 -0
  220. package/src/messaging/providers/slack/send.ts +8 -2
  221. package/src/messaging/providers/slack/types.ts +14 -0
  222. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
  223. package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
  224. package/src/notifications/conversation-seed-composer.ts +14 -2
  225. package/src/notifications/deferred-emit.ts +135 -0
  226. package/src/notifications/emit-signal.ts +9 -1
  227. package/src/notifications/home-feed-side-effect.ts +60 -30
  228. package/src/oauth/connect-orchestrator.ts +3 -0
  229. package/src/oauth/credential-token-resolver.ts +2 -0
  230. package/src/oauth/manual-token-connection.ts +19 -0
  231. package/src/oauth/oauth-store.ts +12 -0
  232. package/src/oauth/seed-providers.ts +22 -0
  233. package/src/permissions/prompter.ts +5 -2
  234. package/src/permissions/secret-prompter.ts +4 -1
  235. package/src/plugins/defaults/injectors.ts +82 -9
  236. package/src/prompts/__tests__/system-prompt.test.ts +46 -2
  237. package/src/prompts/normalize-onboarding.ts +40 -0
  238. package/src/prompts/sections.ts +32 -14
  239. package/src/prompts/system-prompt.ts +105 -68
  240. package/src/prompts/template-detection.ts +37 -0
  241. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  242. package/src/prompts/templates/BOOTSTRAP.md +8 -0
  243. package/src/prompts/templates/VOICE.md +3 -0
  244. package/src/prompts/templates/system-sections.ts +53 -3
  245. package/src/providers/anthropic/client.ts +132 -5
  246. package/src/providers/fireworks/client.ts +20 -2
  247. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  248. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  249. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  250. package/src/providers/inference/adapter-factory.ts +15 -1
  251. package/src/providers/inference/auth.ts +3 -3
  252. package/src/providers/inference/codex-token-refresh.ts +128 -0
  253. package/src/providers/inference/resolve-auth.ts +49 -6
  254. package/src/providers/model-catalog.ts +48 -1
  255. package/src/providers/openai/chat-completions-provider.ts +57 -20
  256. package/src/providers/openai/responses-provider.ts +9 -3
  257. package/src/providers/openrouter/client.ts +5 -1
  258. package/src/providers/types.ts +25 -0
  259. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  260. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  261. package/src/runtime/agent-wake.ts +151 -56
  262. package/src/runtime/auth/route-policy.ts +7 -3
  263. package/src/runtime/background-job-runner.ts +26 -0
  264. package/src/runtime/channel-reply-delivery.ts +182 -47
  265. package/src/runtime/channel-retry-sweep.ts +141 -16
  266. package/src/runtime/http-types.ts +7 -4
  267. package/src/runtime/pending-interactions.ts +51 -8
  268. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  269. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
  270. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  271. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  272. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  273. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  274. package/src/runtime/routes/approval-routes.ts +4 -1
  275. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  276. package/src/runtime/routes/content-source-routes.ts +78 -0
  277. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  278. package/src/runtime/routes/conversation-query-routes.ts +60 -1
  279. package/src/runtime/routes/conversation-routes.ts +281 -76
  280. package/src/runtime/routes/document-comments-routes.ts +287 -0
  281. package/src/runtime/routes/documents-routes.ts +33 -0
  282. package/src/runtime/routes/home-feed-routes.ts +6 -3
  283. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  284. package/src/runtime/routes/host-browser-routes.ts +8 -1
  285. package/src/runtime/routes/identity-routes.ts +21 -0
  286. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  287. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  288. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  289. package/src/runtime/routes/index.ts +12 -4
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
  291. package/src/runtime/routes/integrations/a2a.ts +60 -1
  292. package/src/runtime/routes/log-export-routes.ts +39 -0
  293. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  294. package/src/runtime/routes/notification-routes.ts +19 -2
  295. package/src/runtime/routes/question-routes.ts +4 -1
  296. package/src/runtime/routes/sanity-routes.ts +159 -0
  297. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  298. package/src/runtime/services/conversation-serializer.ts +30 -4
  299. package/src/schedule/integration-status.ts +3 -1
  300. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  301. package/src/security/oauth2-device-code.ts +307 -0
  302. package/src/security/oauth2.ts +26 -9
  303. package/src/security/secure-keys.ts +5 -0
  304. package/src/skills/catalog-install.ts +6 -2
  305. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  306. package/src/tools/browser/browser-execution.ts +93 -0
  307. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  308. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  309. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  310. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  311. package/src/tools/browser/cdp-client/factory.ts +87 -3
  312. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  313. package/src/tools/browser/cdp-client/types.ts +36 -0
  314. package/src/tools/browser/pinned-tabs.ts +90 -0
  315. package/src/tools/document/document-comment-tool.test.ts +379 -0
  316. package/src/tools/document/document-comment-tool.ts +156 -0
  317. package/src/tools/document/document-tool.ts +128 -2
  318. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  319. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  320. package/src/tools/network/domain-normalize.ts +17 -0
  321. package/src/tools/network/web-fetch.ts +213 -64
  322. package/src/tools/network/web-search.ts +191 -66
  323. package/src/tools/terminal/safe-env.ts +3 -2
  324. package/src/tools/tool-approval-handler.ts +19 -12
  325. package/src/tools/types.ts +4 -0
  326. package/src/tools/ui-surface/definitions.ts +3 -1
  327. package/src/types/onboarding-context.ts +4 -0
  328. package/src/util/__tests__/favicon.test.ts +84 -0
  329. package/src/util/favicon.ts +40 -0
  330. package/src/util/platform.ts +0 -5
  331. package/src/workspace/git-service.ts +75 -4
  332. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  333. package/src/workspace/migrations/registry.ts +2 -0
  334. package/src/config/bundled-skills/document/SKILL.md +0 -54
  335. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  336. package/src/daemon/seed-files.ts +0 -18
  337. package/src/runtime/routes/interface-routes.ts +0 -43
  338. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  339. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  340. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  341. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  342. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -537,6 +537,55 @@ function formatOrphanedWebSearchResultAsText(block: {
537
537
  *
538
538
  * Builds a fresh result array without mutating the input.
539
539
  */
540
+ /**
541
+ * Find the start index of the active tool-use continuation span at the tail
542
+ * of the formatted message array. Messages from this index onward may contain
543
+ * thinking blocks that must be preserved for Anthropic's tool-use protocol.
544
+ *
545
+ * The active span is the trailing sequence of alternating
546
+ * assistant(tool_use) → user(tool_result) messages. Everything before it is
547
+ * a completed historical turn whose thinking blocks can be safely stripped.
548
+ *
549
+ * Returns `messages.length` when there is no active tool-use continuation
550
+ * (i.e. all messages are historical — strip thinking from everything).
551
+ */
552
+ function findActiveToolUseContinuationStart(
553
+ messages: Anthropic.MessageParam[],
554
+ ): number {
555
+ // Walk backwards from the end. The tail pattern we're looking for is:
556
+ // ... assistant(tool_use) user(tool_result) [assistant(tool_use) user(tool_result)]* ...
557
+ // The last message is typically a user message (the new prompt), so if it
558
+ // doesn't contain tool_result blocks, there's no active continuation.
559
+ let i = messages.length - 1;
560
+
561
+ while (i >= 0) {
562
+ const msg = messages[i];
563
+ if (msg.role === "user") {
564
+ const content = Array.isArray(msg.content) ? msg.content : [];
565
+ const hasToolResult = content.some(
566
+ (b) => typeof b !== "string" && isToolResultBlock(b),
567
+ );
568
+ if (!hasToolResult) break;
569
+ // This user message has tool_result — the preceding assistant message
570
+ // should have the matching tool_use and its thinking blocks preserved.
571
+ i--;
572
+ } else if (msg.role === "assistant") {
573
+ const content = Array.isArray(msg.content) ? msg.content : [];
574
+ const hasToolUse = content.some(
575
+ (b) => typeof b !== "string" && isToolUseBlock(b),
576
+ );
577
+ if (!hasToolUse) break;
578
+ // This assistant message has tool_use — it's part of the active span.
579
+ // Check if the preceding user message continues the chain.
580
+ i--;
581
+ } else {
582
+ break;
583
+ }
584
+ }
585
+
586
+ return i + 1;
587
+ }
588
+
540
589
  function ensureToolPairing(
541
590
  messages: Anthropic.MessageParam[],
542
591
  ): Anthropic.MessageParam[] {
@@ -925,10 +974,32 @@ export class AnthropicProvider implements Provider {
925
974
  }
926
975
  }
927
976
 
928
- // Thinking blocks are stripped at rest by DB migration 209 so
929
- // historical messages are clean when loaded. Within a turn,
930
- // assistant messages have original thinking with valid signatures
931
- // the API accepts them. No provider-side stripping needed.
977
+ // Strip thinking/redacted_thinking blocks from completed historical
978
+ // assistant turns. Anthropic only requires these blocks for active
979
+ // tool-use continuation (the tail span where assistant tool_use is
980
+ // followed by user tool_result). Replaying stale thinking blocks from
981
+ // earlier turns causes 400 errors when the signature is no longer
982
+ // valid (e.g. after a provider/model/profile switch).
983
+ const activeToolUseStart =
984
+ findActiveToolUseContinuationStart(formatted);
985
+ for (let i = 0; i < activeToolUseStart; i++) {
986
+ const msg = formatted[i];
987
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
988
+ const stripped = (
989
+ msg.content as Anthropic.ContentBlockParam[]
990
+ ).filter(
991
+ (b) =>
992
+ typeof b === "string" ||
993
+ (b.type !== "thinking" && b.type !== "redacted_thinking"),
994
+ );
995
+ if (stripped.length === 0) {
996
+ stripped.push({
997
+ type: "text" as const,
998
+ text: PLACEHOLDER_BLOCKS_OMITTED,
999
+ });
1000
+ }
1001
+ formatted[i] = { ...msg, content: stripped };
1002
+ }
932
1003
 
933
1004
  sentMessages = ensureToolPairing(
934
1005
  repairOrphanedServerToolBlocks(formatted),
@@ -1274,6 +1345,19 @@ export class AnthropicProvider implements Provider {
1274
1345
  let lastInputJsonEmitMs = 0;
1275
1346
  let pendingInputJsonFlush: ReturnType<typeof setTimeout> | undefined;
1276
1347
 
1348
+ // Anthropic streams `server_tool_use` block input via `input_json_delta`
1349
+ // events (the block's own `input` field is `{}` at content_block_start).
1350
+ // We accumulate the JSON separately from regular `tool_use` blocks so
1351
+ // the daemon can read the resolved query when the paired
1352
+ // `web_search_tool_result` arrives — without this, downstream activity
1353
+ // metadata sees an empty query.
1354
+ let currentServerToolUseId: string | undefined;
1355
+ let accumulatedServerToolInputJson = "";
1356
+ const resolvedServerToolInputs = new Map<
1357
+ string,
1358
+ Record<string, unknown>
1359
+ >();
1360
+
1277
1361
  stream.on("streamEvent", (event) => {
1278
1362
  // Reset the text sentinel buffer at each content-block boundary.
1279
1363
  // A new block starts fresh; at the end of a block, flush any
@@ -1300,6 +1384,8 @@ export class AnthropicProvider implements Provider {
1300
1384
  event.type === "content_block_start" &&
1301
1385
  event.content_block.type === "server_tool_use"
1302
1386
  ) {
1387
+ currentServerToolUseId = event.content_block.id;
1388
+ accumulatedServerToolInputJson = "";
1303
1389
  onEvent?.({
1304
1390
  type: "server_tool_start",
1305
1391
  name: event.content_block.name,
@@ -1315,11 +1401,21 @@ export class AnthropicProvider implements Provider {
1315
1401
  ) {
1316
1402
  const block = event.content_block as {
1317
1403
  tool_use_id: string;
1318
- content?: { type: "web_search_tool_result_error" } | unknown[];
1404
+ content?:
1405
+ | { type: "web_search_tool_result_error"; error_code?: string }
1406
+ | unknown[];
1319
1407
  };
1320
1408
  const isError =
1321
1409
  !Array.isArray(block.content) &&
1322
1410
  block.content?.type === "web_search_tool_result_error";
1411
+ const errorCode =
1412
+ isError && !Array.isArray(block.content)
1413
+ ? block.content?.error_code
1414
+ : undefined;
1415
+ const resolvedInput = resolvedServerToolInputs.get(
1416
+ block.tool_use_id,
1417
+ );
1418
+ resolvedServerToolInputs.delete(block.tool_use_id);
1323
1419
  onEvent?.({
1324
1420
  type: "server_tool_complete",
1325
1421
  toolUseId: block.tool_use_id,
@@ -1327,6 +1423,8 @@ export class AnthropicProvider implements Provider {
1327
1423
  ...(Array.isArray(block.content)
1328
1424
  ? { content: block.content }
1329
1425
  : {}),
1426
+ ...(resolvedInput ? { resolvedInput } : {}),
1427
+ ...(errorCode ? { errorCode } : {}),
1330
1428
  });
1331
1429
  }
1332
1430
  if (event.type === "content_block_stop") {
@@ -1345,6 +1443,25 @@ export class AnthropicProvider implements Provider {
1345
1443
  currentStreamingToolName = undefined;
1346
1444
  currentStreamingToolUseId = undefined;
1347
1445
  accumulatedInputJson = "";
1446
+ // Finalize the resolved input for a `server_tool_use` block (e.g.
1447
+ // the actual web-search query) so the paired `web_search_tool_result`
1448
+ // emits `server_tool_complete` with `resolvedInput` populated.
1449
+ if (currentServerToolUseId && accumulatedServerToolInputJson) {
1450
+ try {
1451
+ const parsed = JSON.parse(accumulatedServerToolInputJson);
1452
+ if (parsed && typeof parsed === "object") {
1453
+ resolvedServerToolInputs.set(
1454
+ currentServerToolUseId,
1455
+ parsed as Record<string, unknown>,
1456
+ );
1457
+ }
1458
+ } catch {
1459
+ // Malformed partial JSON — drop silently; downstream falls
1460
+ // back to whatever was captured at server_tool_start.
1461
+ }
1462
+ }
1463
+ currentServerToolUseId = undefined;
1464
+ accumulatedServerToolInputJson = "";
1348
1465
  // Flush residual text buffer unless it's exactly a sentinel.
1349
1466
  if (textBuffer.length > 0 && !isCompleteSentinel(textBuffer)) {
1350
1467
  onEvent?.({ type: "text_delta", text: textBuffer });
@@ -1354,6 +1471,13 @@ export class AnthropicProvider implements Provider {
1354
1471
  });
1355
1472
 
1356
1473
  stream.on("inputJson", (partialJson) => {
1474
+ if (currentServerToolUseId) {
1475
+ // Server-tool input (e.g. `web_search` query) — accumulate without
1476
+ // emitting `input_json_delta`; the daemon only consumes the
1477
+ // finalized value from `server_tool_complete.resolvedInput`.
1478
+ accumulatedServerToolInputJson += partialJson;
1479
+ return;
1480
+ }
1357
1481
  if (!currentStreamingToolName) return;
1358
1482
  accumulatedInputJson += partialJson;
1359
1483
  const now = Date.now();
@@ -1544,6 +1668,9 @@ export class AnthropicProvider implements Provider {
1544
1668
  case "text":
1545
1669
  return { type: "text", text: block.text };
1546
1670
  case "thinking":
1671
+ if (!block.signature) {
1672
+ return null;
1673
+ }
1547
1674
  return {
1548
1675
  type: "thinking",
1549
1676
  thinking: block.thinking,
@@ -1,3 +1,4 @@
1
+ import { PROVIDER_CATALOG } from "../model-catalog.js";
1
2
  import { OpenAIChatCompletionsProvider } from "../openai/chat-completions-provider.js";
2
3
 
3
4
  export interface FireworksProviderOptions {
@@ -8,6 +9,15 @@ export interface FireworksProviderOptions {
8
9
 
9
10
  const DEFAULT_FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference/v1";
10
11
 
12
+ const FIREWORKS_MODEL_EFFORT_CEILINGS: ReadonlyMap<
13
+ string,
14
+ "high" | "xhigh" | "max"
15
+ > = new Map(
16
+ PROVIDER_CATALOG.find((p) => p.id === "fireworks")?.models.flatMap((m) =>
17
+ m.maxEffort ? ([[m.id, m.maxEffort]] as const) : [],
18
+ ) ?? [],
19
+ );
20
+
11
21
  export class FireworksProvider extends OpenAIChatCompletionsProvider {
12
22
  constructor(
13
23
  apiKey: string,
@@ -19,9 +29,17 @@ export class FireworksProvider extends OpenAIChatCompletionsProvider {
19
29
  providerName: "fireworks",
20
30
  providerLabel: "Fireworks",
21
31
  streamTimeoutMs: options.streamTimeoutMs,
22
- // Fireworks' OpenAI-compatible chat-completions API documents only
23
- // low|medium|high for reasoning_effort; sending "xhigh" 4xxs upstream.
32
+ // Fallback for models not declared in the catalog. Most Fireworks
33
+ // chat-completions models only document `low|medium|high`; per-model
34
+ // overrides (e.g. DeepSeek V4 → "max") come from
35
+ // {@link resolveMaxReasoningEffort}.
24
36
  maxReasoningEffort: "high",
25
37
  });
26
38
  }
39
+
40
+ protected override resolveMaxReasoningEffort(
41
+ model: string,
42
+ ): "high" | "xhigh" | "max" {
43
+ return FIREWORKS_MODEL_EFFORT_CEILINGS.get(model) ?? "high";
44
+ }
27
45
  }
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Route-level validation tests for base_url provider-type gate and SSRF
3
+ * protection on inference provider connections.
4
+ *
5
+ * These tests exercise the POST (create) and PATCH (update) route handlers
6
+ * with mocked DB, config, and DNS resolution.
7
+ */
8
+ import { Database } from "bun:sqlite";
9
+ import { describe, expect, mock, test } from "bun:test";
10
+
11
+ import { drizzle } from "drizzle-orm/bun-sqlite";
12
+
13
+ import { migrateCreateProviderConnections } from "../../../memory/migrations/243-provider-connections.js";
14
+ import { migrateProviderConnectionStatusLabel } from "../../../memory/migrations/244-provider-connection-status-label.js";
15
+ import { migrateProviderConnectionBaseUrlAndModels } from "../../../memory/migrations/250-provider-connection-base-url-and-models.js";
16
+ import * as schema from "../../../memory/schema.js";
17
+
18
+ function createTestDb() {
19
+ const sqlite = new Database(":memory:");
20
+ sqlite.exec("PRAGMA journal_mode=WAL");
21
+ return drizzle(sqlite, { schema });
22
+ }
23
+
24
+ function bootDb() {
25
+ const db = createTestDb();
26
+ migrateCreateProviderConnections(db);
27
+ migrateProviderConnectionStatusLabel(db);
28
+ migrateProviderConnectionBaseUrlAndModels(db);
29
+ return db;
30
+ }
31
+
32
+ // Create and hold a test DB; the mock returns it from getDb().
33
+ const testDb = bootDb();
34
+
35
+ mock.module("../../../memory/db-connection.js", () => ({
36
+ getDb: () => testDb,
37
+ }));
38
+
39
+ mock.module("../../../config/assistant-feature-flags.js", () => ({
40
+ isAssistantFeatureFlagEnabled: (flag: string) =>
41
+ flag === "openai-compatible-endpoints",
42
+ }));
43
+
44
+ mock.module("../../../config/loader.js", () => ({
45
+ getConfig: () => ({ llm: {} }),
46
+ getConfigReadOnly: () => ({ llm: {} }),
47
+ }));
48
+
49
+ // Mock DNS resolution: all hostnames resolve to a public IP by default.
50
+ // Tests that need private-IP resolution override this per-test.
51
+ let mockResolvedAddresses: string[] = ["93.184.216.34"];
52
+
53
+ // Import the real url-safety module for use inside the mock.
54
+ const {
55
+ isPrivateOrLocalHost: realIsPrivateOrLocalHost,
56
+ isPrivateIPv4,
57
+ isPrivateIPv6,
58
+ isIPv4,
59
+ isIPv6,
60
+ parseUrl,
61
+ unwrapBracketedHostname,
62
+ extractEmbeddedIPv4FromIPv6,
63
+ looksLikeHostPortShorthand,
64
+ looksLikePathOnlyInput,
65
+ buildHostHeader,
66
+ stripUrlUserinfo,
67
+ sanitizeUrlForOutput,
68
+ sanitizeUrlStringForOutput,
69
+ } = await import("../../../tools/network/url-safety.js");
70
+
71
+ mock.module("../../../tools/network/url-safety.js", () => ({
72
+ isPrivateOrLocalHost: realIsPrivateOrLocalHost,
73
+ isPrivateIPv4,
74
+ isPrivateIPv6,
75
+ isIPv4,
76
+ isIPv6,
77
+ parseUrl,
78
+ unwrapBracketedHostname,
79
+ extractEmbeddedIPv4FromIPv6,
80
+ looksLikeHostPortShorthand,
81
+ looksLikePathOnlyInput,
82
+ buildHostHeader,
83
+ stripUrlUserinfo,
84
+ sanitizeUrlForOutput,
85
+ sanitizeUrlStringForOutput,
86
+ resolveHostAddresses: async () => mockResolvedAddresses,
87
+ resolveRequestAddress: async (
88
+ hostname: string,
89
+ _resolveHost: unknown,
90
+ allowPrivateNetwork: boolean,
91
+ ) => {
92
+ if (!allowPrivateNetwork && realIsPrivateOrLocalHost(hostname)) {
93
+ return { addresses: [], blockedAddress: hostname };
94
+ }
95
+ const addresses = mockResolvedAddresses;
96
+ if (!allowPrivateNetwork) {
97
+ for (const addr of addresses) {
98
+ if (realIsPrivateOrLocalHost(addr)) {
99
+ return { addresses: [], blockedAddress: addr };
100
+ }
101
+ }
102
+ }
103
+ return { addresses };
104
+ },
105
+ }));
106
+
107
+ const { ROUTES } = await import(
108
+ "../../../runtime/routes/inference-provider-connection-routes.js"
109
+ );
110
+
111
+ const handleCreate = ROUTES.find(
112
+ (r) => r.operationId === "inference_provider_connections_create",
113
+ )!.handler;
114
+
115
+ const handleUpdate = ROUTES.find(
116
+ (r) => r.operationId === "inference_provider_connections_update",
117
+ )!.handler;
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Provider-type gate: base_url rejected for non-openai-compatible providers
121
+ // ---------------------------------------------------------------------------
122
+
123
+ describe("base_url provider-type gate (create)", () => {
124
+ test("rejects base_url on anthropic provider", async () => {
125
+ await expect(
126
+ handleCreate({
127
+ body: {
128
+ name: "exfil-anthropic",
129
+ provider: "anthropic",
130
+ auth: { type: "api_key", credential: "cred-exfil" },
131
+ base_url: "https://evil.example.com/v1",
132
+ },
133
+ }),
134
+ ).rejects.toThrow(/base_url is only valid for openai-compatible/);
135
+ });
136
+
137
+ test("rejects base_url on openai provider", async () => {
138
+ await expect(
139
+ handleCreate({
140
+ body: {
141
+ name: "exfil-openai",
142
+ provider: "openai",
143
+ auth: { type: "api_key", credential: "cred-exfil" },
144
+ base_url: "https://evil.example.com/v1",
145
+ },
146
+ }),
147
+ ).rejects.toThrow(/base_url is only valid for openai-compatible/);
148
+ });
149
+
150
+ test("rejects base_url on gemini provider", async () => {
151
+ await expect(
152
+ handleCreate({
153
+ body: {
154
+ name: "exfil-gemini",
155
+ provider: "gemini",
156
+ auth: { type: "api_key", credential: "cred-exfil" },
157
+ base_url: "https://evil.example.com/v1",
158
+ },
159
+ }),
160
+ ).rejects.toThrow(/base_url is only valid for openai-compatible/);
161
+ });
162
+
163
+ test("accepts base_url on openai-compatible provider", async () => {
164
+ mockResolvedAddresses = ["93.184.216.34"];
165
+ const result = await handleCreate({
166
+ body: {
167
+ name: "valid-oai-compat",
168
+ provider: "openai-compatible",
169
+ auth: { type: "api_key", credential: "cred-vllm" },
170
+ base_url: "https://my-vllm.example.com/v1",
171
+ models: [{ id: "my-model" }],
172
+ },
173
+ });
174
+ expect(result).toBeDefined();
175
+ expect((result as { baseUrl: string }).baseUrl).toBe(
176
+ "https://my-vllm.example.com/v1",
177
+ );
178
+ });
179
+
180
+ test("allows null base_url on non-openai-compatible provider", async () => {
181
+ // Setting base_url to null is always allowed (it's a no-op clear).
182
+ const result = await handleCreate({
183
+ body: {
184
+ name: "null-baseurl-anthropic",
185
+ provider: "anthropic",
186
+ auth: { type: "api_key", credential: "cred-test" },
187
+ base_url: null,
188
+ },
189
+ });
190
+ expect(result).toBeDefined();
191
+ expect((result as { baseUrl: string | null }).baseUrl).toBeNull();
192
+ });
193
+ });
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // SSRF protection: private/local network addresses blocked
197
+ // ---------------------------------------------------------------------------
198
+
199
+ describe("base_url SSRF protection (create)", () => {
200
+ test("rejects private IP (192.168.x.x)", async () => {
201
+ await expect(
202
+ handleCreate({
203
+ body: {
204
+ name: "ssrf-private-ip",
205
+ provider: "openai-compatible",
206
+ auth: { type: "api_key", credential: "cred-ssrf" },
207
+ base_url: "http://192.168.1.1/v1",
208
+ models: [{ id: "m" }],
209
+ },
210
+ }),
211
+ ).rejects.toThrow(/private or local network/);
212
+ });
213
+
214
+ test("rejects localhost", async () => {
215
+ await expect(
216
+ handleCreate({
217
+ body: {
218
+ name: "ssrf-localhost",
219
+ provider: "openai-compatible",
220
+ auth: { type: "api_key", credential: "cred-ssrf" },
221
+ base_url: "http://localhost:8080/v1",
222
+ models: [{ id: "m" }],
223
+ },
224
+ }),
225
+ ).rejects.toThrow(/private or local network/);
226
+ });
227
+
228
+ test("rejects 0.0.0.0", async () => {
229
+ await expect(
230
+ handleCreate({
231
+ body: {
232
+ name: "ssrf-zero",
233
+ provider: "openai-compatible",
234
+ auth: { type: "api_key", credential: "cred-ssrf" },
235
+ base_url: "http://0.0.0.0:8080/v1",
236
+ models: [{ id: "m" }],
237
+ },
238
+ }),
239
+ ).rejects.toThrow(/private or local network/);
240
+ });
241
+
242
+ test("rejects 10.x.x.x", async () => {
243
+ await expect(
244
+ handleCreate({
245
+ body: {
246
+ name: "ssrf-ten",
247
+ provider: "openai-compatible",
248
+ auth: { type: "api_key", credential: "cred-ssrf" },
249
+ base_url: "http://10.0.0.1/v1",
250
+ models: [{ id: "m" }],
251
+ },
252
+ }),
253
+ ).rejects.toThrow(/private or local network/);
254
+ });
255
+
256
+ test("rejects 127.0.0.1", async () => {
257
+ await expect(
258
+ handleCreate({
259
+ body: {
260
+ name: "ssrf-loopback",
261
+ provider: "openai-compatible",
262
+ auth: { type: "api_key", credential: "cred-ssrf" },
263
+ base_url: "http://127.0.0.1:8080/v1",
264
+ models: [{ id: "m" }],
265
+ },
266
+ }),
267
+ ).rejects.toThrow(/private or local network/);
268
+ });
269
+
270
+ test("rejects metadata.google.internal", async () => {
271
+ await expect(
272
+ handleCreate({
273
+ body: {
274
+ name: "ssrf-metadata",
275
+ provider: "openai-compatible",
276
+ auth: { type: "api_key", credential: "cred-ssrf" },
277
+ base_url: "http://metadata.google.internal/computeMetadata/v1/",
278
+ models: [{ id: "m" }],
279
+ },
280
+ }),
281
+ ).rejects.toThrow(/private or local network/);
282
+ });
283
+
284
+ test("rejects hostname that resolves to a private IP", async () => {
285
+ mockResolvedAddresses = ["10.0.0.5"];
286
+ await expect(
287
+ handleCreate({
288
+ body: {
289
+ name: "ssrf-dns-rebind",
290
+ provider: "openai-compatible",
291
+ auth: { type: "api_key", credential: "cred-ssrf" },
292
+ base_url: "https://dns-rebind.example.com/v1",
293
+ models: [{ id: "m" }],
294
+ },
295
+ }),
296
+ ).rejects.toThrow(/resolves to a private network/);
297
+ });
298
+
299
+ test("accepts a valid public URL", async () => {
300
+ mockResolvedAddresses = ["93.184.216.34"];
301
+ const result = await handleCreate({
302
+ body: {
303
+ name: "valid-public-url",
304
+ provider: "openai-compatible",
305
+ auth: { type: "api_key", credential: "cred-valid" },
306
+ base_url: "https://api.example.com/v1",
307
+ models: [{ id: "my-model" }],
308
+ },
309
+ });
310
+ expect(result).toBeDefined();
311
+ expect((result as { baseUrl: string }).baseUrl).toBe(
312
+ "https://api.example.com/v1",
313
+ );
314
+ });
315
+ });
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Update handler: base_url validation on existing connections
319
+ // ---------------------------------------------------------------------------
320
+
321
+ describe("base_url provider-type gate (update)", () => {
322
+ test("rejects adding base_url to an existing anthropic connection", async () => {
323
+ // Seed a connection first.
324
+ await handleCreate({
325
+ body: {
326
+ name: "update-test-anthropic",
327
+ provider: "anthropic",
328
+ auth: { type: "api_key", credential: "cred-update" },
329
+ },
330
+ });
331
+
332
+ await expect(
333
+ handleUpdate({
334
+ pathParams: { name: "update-test-anthropic" },
335
+ body: {
336
+ auth: { type: "api_key", credential: "cred-update" },
337
+ base_url: "https://evil.example.com/v1",
338
+ },
339
+ }),
340
+ ).rejects.toThrow(/base_url is only valid for openai-compatible/);
341
+ });
342
+ });