@vellumai/assistant 0.11.3 → 0.11.4-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/docs/architecture/memory.md +11 -0
  3. package/docs/architecture/turn-actor.md +70 -0
  4. package/docs/flux-turn-detection-spike.md +243 -0
  5. package/docs/stt-provider-onboarding.md +3 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  8. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  10. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  11. package/openapi.yaml +139 -37
  12. package/package.json +1 -1
  13. package/scripts/voice-ttft-spike.ts +3 -3
  14. package/src/__tests__/app-compiler.test.ts +38 -3
  15. package/src/__tests__/attachments-store.test.ts +21 -12
  16. package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
  17. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  18. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  19. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  20. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  21. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  22. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  23. package/src/__tests__/config-schema.test.ts +25 -10
  24. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  26. package/src/__tests__/conversation-agent-loop.test.ts +28 -20
  27. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  28. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  29. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  30. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  31. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  32. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  33. package/src/__tests__/conversation-queue.test.ts +177 -4
  34. package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
  35. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  36. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  37. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  38. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  39. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  40. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  41. package/src/__tests__/edit-propagation.test.ts +0 -7
  42. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  43. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  44. package/src/__tests__/injector-chain.test.ts +63 -41
  45. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  46. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  47. package/src/__tests__/llm-schema.test.ts +5 -2
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  49. package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
  50. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  51. package/src/__tests__/mtime-cache.test.ts +1 -1
  52. package/src/__tests__/non-member-access-request.test.ts +0 -20
  53. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
  55. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  56. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  57. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  58. package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
  59. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  60. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  61. package/src/__tests__/skills.test.ts +32 -0
  62. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  63. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  64. package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
  65. package/src/__tests__/turn-events-store.test.ts +43 -0
  66. package/src/__tests__/ui-shape-teaching.test.ts +33 -0
  67. package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
  68. package/src/__tests__/user-plugin-loader.test.ts +1 -1
  69. package/src/__tests__/visible-app-context.test.ts +16 -9
  70. package/src/__tests__/voice-config-update.test.ts +40 -0
  71. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  72. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  73. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  74. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  75. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  76. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  77. package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
  78. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  79. package/src/acp/__tests__/auth-required.test.ts +161 -0
  80. package/src/acp/acp-claude-oauth.ts +19 -2
  81. package/src/acp/agent-process.test.ts +100 -0
  82. package/src/acp/agent-process.ts +29 -26
  83. package/src/acp/auth-required.ts +102 -0
  84. package/src/acp/session-manager.test.ts +119 -0
  85. package/src/acp/session-manager.ts +68 -2
  86. package/src/api/events/acp-auth-required.ts +55 -0
  87. package/src/api/index.ts +7 -0
  88. package/src/api/surfaces.ts +7 -3
  89. package/src/apps/app-store.ts +3 -0
  90. package/src/bundler/package-resolver.ts +2 -30
  91. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  92. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  93. package/src/calls/call-controller.ts +19 -3
  94. package/src/calls/call-setup-flow.ts +0 -1
  95. package/src/calls/media-stream-stt-session.ts +15 -0
  96. package/src/calls/voice-session-bridge.ts +71 -16
  97. package/src/calls/voice-triage-escalate.ts +104 -2
  98. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  99. package/src/channels/config.ts +13 -0
  100. package/src/channels/plugin-channel-declarations.ts +108 -0
  101. package/src/channels/types.ts +30 -0
  102. package/src/cli/AGENTS.md +5 -2
  103. package/src/cli/commands/credentials.help.ts +2 -2
  104. package/src/cli/commands/inference-providers.ts +1 -1
  105. package/src/cli/commands/mcp.help.ts +13 -4
  106. package/src/cli/commands/mcp.ts +9 -0
  107. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  108. package/src/cli/commands/memory/index.help.ts +43 -1
  109. package/src/cli/commands/memory/memory-v3.ts +64 -0
  110. package/src/cli/commands/stt.help.ts +27 -2
  111. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  112. package/src/cli/lib/bundled-marketplace.json +13 -0
  113. package/src/cli/lib/upgrade-plugin.ts +42 -0
  114. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  115. package/src/config/__tests__/default-provider.test.ts +6 -1
  116. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  117. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  118. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  119. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  120. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  121. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  122. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
  123. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
  124. package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
  125. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
  126. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  127. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  128. package/src/config/call-site-defaults.ts +4 -5
  129. package/src/config/default-profile-catalog.ts +83 -12
  130. package/src/config/default-profile-names.ts +4 -1
  131. package/src/config/default-provider-resolution.ts +4 -0
  132. package/src/config/llm-context-resolution.ts +11 -3
  133. package/src/config/llm-resolver.ts +28 -1
  134. package/src/config/profile-materialization.ts +70 -22
  135. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  136. package/src/config/schemas/call-site-catalog.ts +4 -4
  137. package/src/config/schemas/live-voice.ts +57 -23
  138. package/src/config/schemas/llm.ts +59 -32
  139. package/src/config/schemas/mcp.ts +23 -0
  140. package/src/config/schemas/plugin-updates.ts +6 -2
  141. package/src/config/schemas/stt.ts +1 -0
  142. package/src/context/outbound-sanitize.ts +96 -1
  143. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  144. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  145. package/src/daemon/conversation-agent-loop.ts +17 -6
  146. package/src/daemon/conversation-messaging.ts +5 -1
  147. package/src/daemon/conversation-notifiers.ts +9 -1
  148. package/src/daemon/conversation-process.ts +36 -6
  149. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  150. package/src/daemon/conversation-surfaces.ts +27 -5
  151. package/src/daemon/conversation-tool-setup.ts +1 -2
  152. package/src/daemon/conversation.ts +48 -0
  153. package/src/daemon/interactive-turn-sender.ts +59 -0
  154. package/src/daemon/mcp-reload-service.ts +36 -6
  155. package/src/daemon/process-message.ts +24 -24
  156. package/src/daemon/providers-setup.ts +6 -3
  157. package/src/daemon/trust-context-types.ts +29 -0
  158. package/src/daemon/wake-conversation-ops.ts +3 -2
  159. package/src/documents/document-store.ts +138 -5
  160. package/src/hooks/hook-loader.ts +3 -3
  161. package/src/hooks/registry.ts +50 -6
  162. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  163. package/src/inbound/oauth-callback-url.ts +61 -0
  164. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  165. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  166. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  167. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  168. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  169. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  170. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  171. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  172. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  173. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  174. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  175. package/src/live-voice/live-voice-archive.ts +2 -0
  176. package/src/live-voice/live-voice-metrics.ts +57 -32
  177. package/src/live-voice/live-voice-photo.ts +1 -2
  178. package/src/live-voice/live-voice-session.ts +535 -314
  179. package/src/live-voice/progress-narration.ts +277 -0
  180. package/src/live-voice/protocol.ts +21 -1
  181. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  182. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  183. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  184. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  185. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  186. package/src/mcp/client.ts +16 -11
  187. package/src/mcp/effective-config.ts +113 -0
  188. package/src/mcp/manager.ts +11 -6
  189. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  190. package/src/mcp/mcp-oauth-provider.ts +205 -240
  191. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  192. package/src/monitoring/plugin-auto-update.ts +128 -24
  193. package/src/notifications/signal.ts +1 -0
  194. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  195. package/src/permissions/confirmation-guardian-request.ts +2 -2
  196. package/src/permissions/question-guardian-request.test.ts +14 -6
  197. package/src/permissions/question-guardian-request.ts +1 -2
  198. package/src/persistence/attachments-store.ts +8 -1
  199. package/src/persistence/bookmark-crud.ts +3 -7
  200. package/src/persistence/conversation-attention-store.ts +16 -45
  201. package/src/persistence/conversation-crud.ts +33 -4
  202. package/src/persistence/conversation-lineage.ts +9 -0
  203. package/src/persistence/conversation-queries.ts +108 -41
  204. package/src/persistence/delivery-crud.ts +38 -29
  205. package/src/persistence/external-conversation-store.ts +32 -4
  206. package/src/persistence/llm-request-log-store.ts +4 -10
  207. package/src/persistence/llm-usage-store.ts +8 -3
  208. package/src/persistence/message-reads.test.ts +197 -0
  209. package/src/persistence/message-reads.ts +211 -0
  210. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  211. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  212. package/src/persistence/real-user-turn-filter.ts +27 -3
  213. package/src/persistence/steps.ts +9 -0
  214. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  215. package/src/plugin-api/conversation-turn.ts +168 -5
  216. package/src/plugin-api/index.ts +21 -5
  217. package/src/plugin-api/vision-support.test.ts +1 -1
  218. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  219. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  220. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  224. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  225. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  226. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  227. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  228. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  229. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  230. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  231. package/src/plugins/defaults/memory/indexer.ts +3 -1
  232. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  233. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  234. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  238. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  239. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  240. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  241. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  242. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  243. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  244. package/src/plugins/defaults/memory/worker.ts +6 -3
  245. package/src/plugins/external-plugin-loader.ts +47 -0
  246. package/src/plugins/mcp-servers.ts +361 -0
  247. package/src/plugins/mtime-cache.ts +23 -49
  248. package/src/plugins/worker-plugin-surface.ts +33 -0
  249. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  250. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  251. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  252. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  253. package/src/providers/call-site-routing.ts +30 -3
  254. package/src/providers/connection-resolution.ts +194 -11
  255. package/src/providers/inference/auth.ts +6 -0
  256. package/src/providers/inference/connection-availability.ts +24 -2
  257. package/src/providers/inference/connections.ts +2 -0
  258. package/src/providers/model-catalog.ts +3 -3
  259. package/src/providers/model-intents.ts +28 -8
  260. package/src/providers/openai/chat-completions-provider.ts +5 -6
  261. package/src/providers/openai/codex-models.ts +2 -1
  262. package/src/providers/provider-send-message.ts +32 -3
  263. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  264. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  265. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  266. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  267. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  268. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  269. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  270. package/src/providers/speech-to-text/resolve.ts +25 -2
  271. package/src/routes/worker.ts +17 -5
  272. package/src/runtime/access-request-helper.ts +9 -12
  273. package/src/runtime/agent-wake.ts +3 -3
  274. package/src/runtime/pre-first-message-gate.ts +4 -0
  275. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  276. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  277. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  278. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  279. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  280. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  281. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  282. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  283. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  284. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  285. package/src/runtime/routes/channel-route-shared.ts +0 -6
  286. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  287. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  288. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  289. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  290. package/src/runtime/routes/default-provider-routes.ts +15 -0
  291. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  292. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  294. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  295. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  296. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  297. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  298. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  299. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  300. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  301. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  302. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  303. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  304. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  305. package/src/runtime/routes/log-export-routes.ts +3 -0
  306. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  307. package/src/runtime/routes/plugins-routes.ts +21 -3
  308. package/src/runtime/routes/stt-routes.ts +31 -25
  309. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  310. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  311. package/src/runtime/routes/user-route-import.ts +108 -0
  312. package/src/schedule/worker.ts +6 -0
  313. package/src/security/oauth2.ts +6 -22
  314. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  315. package/src/stt/__tests__/types.test.ts +94 -0
  316. package/src/stt/daemon-batch-transcriber.ts +10 -0
  317. package/src/stt/stt-stream-session.ts +8 -4
  318. package/src/stt/types.ts +103 -0
  319. package/src/subagent/manager.ts +1 -3
  320. package/src/subagent/types.ts +7 -6
  321. package/src/tools/acp/spawn.test.ts +97 -0
  322. package/src/tools/acp/spawn.ts +32 -0
  323. package/src/tools/document/document-tool.ts +12 -3
  324. package/src/tools/registry.ts +2 -1
  325. package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
  326. package/src/tools/workflows/run-workflow.ts +1 -2
  327. package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
  328. package/src/tts/reasoning-tag-filter.ts +89 -0
  329. package/src/util/think-tag-stream.ts +95 -0
  330. package/src/workspace/byok-default-profile-ensure.ts +76 -24
  331. package/src/workspace/custom-profile-ensure.ts +4 -24
  332. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  333. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  334. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  335. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  336. package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
  337. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  338. package/src/workspace/migrations/registry.ts +10 -0
  339. package/src/workspace/provider-commit-message-generator.ts +7 -5
  340. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  341. package/src/live-voice/front-decision.ts +0 -476
@@ -0,0 +1,620 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { setConfig } from "../../../__tests__/helpers/set-config.js";
4
+ import type {
5
+ StreamingTranscriber,
6
+ SttStreamServerEvent,
7
+ } from "../../../stt/types.js";
8
+ import { SttError } from "../../../stt/types.js";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Logger mock: must be declared before the subject import
12
+ //
13
+ // The adapter's only observable output for the chunk-cadence measurement is a
14
+ // log line, so it is asserted through captured logs.
15
+ // ---------------------------------------------------------------------------
16
+
17
+ interface CapturedLog {
18
+ data: unknown;
19
+ message: string;
20
+ }
21
+
22
+ const infoLogs: CapturedLog[] = [];
23
+
24
+ function capture(sink: CapturedLog[]) {
25
+ return (data: unknown, message?: string) => {
26
+ sink.push(
27
+ typeof data === "string"
28
+ ? { data: undefined, message: data }
29
+ : { data, message: message ?? "" },
30
+ );
31
+ };
32
+ }
33
+
34
+ mock.module("../../../util/logger.js", () => {
35
+ const logger = {
36
+ info: capture(infoLogs),
37
+ debug: () => {},
38
+ warn: () => {},
39
+ error: () => {},
40
+ trace: () => {},
41
+ fatal: () => {},
42
+ child: () => logger,
43
+ };
44
+ return { getLogger: () => logger };
45
+ });
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Mock WebSocket
49
+ // ---------------------------------------------------------------------------
50
+
51
+ type WsEventType = "open" | "close" | "error" | "message";
52
+ type WsListener = (...args: unknown[]) => void;
53
+
54
+ /** Minimal mock WebSocket standing in for Deepgram's `/v2/listen` endpoint. */
55
+ class MockWebSocket {
56
+ readyState = 0; // CONNECTING
57
+ bufferedAmount = 0;
58
+
59
+ /** Everything passed to `.send()`, in order. */
60
+ sentData: (string | Uint8Array)[] = [];
61
+
62
+ private listeners = new Map<WsEventType, WsListener[]>();
63
+
64
+ addEventListener(type: WsEventType, listener: WsListener): void {
65
+ const list = this.listeners.get(type) ?? [];
66
+ list.push(listener);
67
+ this.listeners.set(type, list);
68
+ }
69
+
70
+ removeEventListener(type: string, listener: unknown): void {
71
+ const list = this.listeners.get(type as WsEventType);
72
+ const idx = list?.indexOf(listener as WsListener) ?? -1;
73
+ if (list && idx !== -1) {
74
+ list.splice(idx, 1);
75
+ }
76
+ }
77
+
78
+ send(data: string | Uint8Array): void {
79
+ if (this.readyState !== 1) {
80
+ throw new Error("WebSocket is not open");
81
+ }
82
+ this.sentData.push(data);
83
+ }
84
+
85
+ close(): void {
86
+ this.readyState = 3; // CLOSED
87
+ }
88
+
89
+ // ── Test helpers ──────────────────────────────────────────────────
90
+
91
+ simulateOpen(): void {
92
+ this.readyState = 1; // OPEN
93
+ for (const l of this.listeners.get("open") ?? []) {
94
+ l();
95
+ }
96
+ }
97
+
98
+ simulateMessage(data: string): void {
99
+ for (const l of this.listeners.get("message") ?? []) {
100
+ l({ data });
101
+ }
102
+ }
103
+
104
+ simulateClose(code = 1000, reason = ""): void {
105
+ this.readyState = 3;
106
+ for (const l of this.listeners.get("close") ?? []) {
107
+ l({ code, reason });
108
+ }
109
+ }
110
+
111
+ simulateError(err: unknown): void {
112
+ for (const l of this.listeners.get("error") ?? []) {
113
+ l(err);
114
+ }
115
+ }
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Subject import (after mocks)
120
+ // ---------------------------------------------------------------------------
121
+
122
+ const { DeepgramFluxRealtimeTranscriber } =
123
+ await import("../deepgram-flux-realtime.js");
124
+
125
+ const TEST_API_KEY = "dg-flux-test-key";
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Helpers
129
+ // ---------------------------------------------------------------------------
130
+
131
+ /** Build a Flux `TurnInfo` frame carrying the given turn state. */
132
+ function turnInfoFrame(
133
+ event: string,
134
+ extra: Record<string, unknown> = {},
135
+ ): string {
136
+ return JSON.stringify({
137
+ type: "TurnInfo",
138
+ event,
139
+ request_id: "flux-request-id",
140
+ turn_index: 0,
141
+ audio_window_start: 0,
142
+ audio_window_end: 1.5,
143
+ ...extra,
144
+ });
145
+ }
146
+
147
+ describe("DeepgramFluxRealtimeTranscriber", () => {
148
+ let mockWs: MockWebSocket;
149
+ let dialedUrls: string[];
150
+ let dialedOptions: ({ headers?: Record<string, string> } | undefined)[];
151
+ let originalWebSocket: unknown;
152
+
153
+ beforeEach(() => {
154
+ // A fully default config: the acceptance criteria are about what the
155
+ // shipped defaults dial.
156
+ setConfig("liveVoice", {});
157
+
158
+ mockWs = new MockWebSocket();
159
+ dialedUrls = [];
160
+ dialedOptions = [];
161
+ infoLogs.length = 0;
162
+
163
+ originalWebSocket = (globalThis as Record<string, unknown>).WebSocket;
164
+ (globalThis as Record<string, unknown>).WebSocket = class {
165
+ constructor(url: string, options?: { headers?: Record<string, string> }) {
166
+ dialedUrls.push(url);
167
+ dialedOptions.push(options);
168
+ return mockWs;
169
+ }
170
+ };
171
+ });
172
+
173
+ afterEach(() => {
174
+ (globalThis as Record<string, unknown>).WebSocket = originalWebSocket;
175
+ });
176
+
177
+ async function startSession(
178
+ options: ConstructorParameters<
179
+ typeof DeepgramFluxRealtimeTranscriber
180
+ >[1] = {},
181
+ ): Promise<{
182
+ transcriber: InstanceType<typeof DeepgramFluxRealtimeTranscriber>;
183
+ events: SttStreamServerEvent[];
184
+ }> {
185
+ const transcriber = new DeepgramFluxRealtimeTranscriber(TEST_API_KEY, {
186
+ // Long enough that no watchdog fires mid-test.
187
+ inactivityTimeoutMs: 60_000,
188
+ keepaliveIntervalMs: 0,
189
+ ...options,
190
+ });
191
+ const events: SttStreamServerEvent[] = [];
192
+
193
+ const startPromise = transcriber.start((event) => events.push(event));
194
+ mockWs.simulateOpen();
195
+ await startPromise;
196
+
197
+ return { transcriber, events };
198
+ }
199
+
200
+ // ─────────────────────────────────────────────────────────────────
201
+ // Dialed URL
202
+ // ─────────────────────────────────────────────────────────────────
203
+
204
+ describe("session URL", () => {
205
+ test("dials /v2/listen with the default Flux tuning and no eager threshold", async () => {
206
+ await startSession();
207
+
208
+ expect(dialedUrls).toHaveLength(1);
209
+ const url = new URL(dialedUrls[0]!);
210
+
211
+ expect(url.protocol).toBe("wss:");
212
+ expect(url.host).toBe("api.deepgram.com");
213
+ expect(url.pathname).toBe("/v2/listen");
214
+ expect(url.searchParams.get("model")).toBe("flux-general-en");
215
+ expect(url.searchParams.get("eot_threshold")).toBe("0.7");
216
+ expect(url.searchParams.get("eot_timeout_ms")).toBe("5000");
217
+ // Unset eager threshold is what keeps Deepgram from speculating turns.
218
+ expect(url.searchParams.has("eager_eot_threshold")).toBe(false);
219
+ });
220
+
221
+ test("sends the negotiated raw-audio format", async () => {
222
+ await startSession({ sampleRate: 24_000 });
223
+
224
+ const url = new URL(dialedUrls[0]!);
225
+ expect(url.searchParams.get("encoding")).toBe("linear16");
226
+ expect(url.searchParams.get("sample_rate")).toBe("24000");
227
+ });
228
+
229
+ test("authenticates with the Token header, never the query string", async () => {
230
+ await startSession();
231
+
232
+ expect(dialedOptions[0]?.headers?.Authorization).toBe(
233
+ `Token ${TEST_API_KEY}`,
234
+ );
235
+ expect(dialedUrls[0]).not.toContain(TEST_API_KEY);
236
+ });
237
+
238
+ test("takes its tuning from liveVoice.flux", async () => {
239
+ setConfig("liveVoice", {
240
+ flux: {
241
+ model: "flux-general-multi",
242
+ eotThreshold: 0.9,
243
+ eagerEotThreshold: 0.4,
244
+ eotTimeoutMs: 1_500,
245
+ },
246
+ });
247
+
248
+ await startSession();
249
+
250
+ const url = new URL(dialedUrls[0]!);
251
+ expect(url.searchParams.get("model")).toBe("flux-general-multi");
252
+ expect(url.searchParams.get("eot_threshold")).toBe("0.9");
253
+ expect(url.searchParams.get("eager_eot_threshold")).toBe("0.4");
254
+ expect(url.searchParams.get("eot_timeout_ms")).toBe("1500");
255
+ });
256
+ });
257
+
258
+ // ─────────────────────────────────────────────────────────────────
259
+ // Audio forwarding
260
+ // ─────────────────────────────────────────────────────────────────
261
+
262
+ describe("audio forwarding", () => {
263
+ test("forwards audio chunks to the socket as raw bytes", async () => {
264
+ const { transcriber } = await startSession();
265
+
266
+ transcriber.sendAudio(Buffer.from([1, 2, 3, 4]), "audio/pcm");
267
+ transcriber.sendAudio(Buffer.from([5, 6]), "audio/pcm");
268
+
269
+ expect(mockWs.sentData).toHaveLength(2);
270
+ expect(Array.from(mockWs.sentData[0] as Uint8Array)).toEqual([
271
+ 1, 2, 3, 4,
272
+ ]);
273
+ expect(Array.from(mockWs.sentData[1] as Uint8Array)).toEqual([5, 6]);
274
+ });
275
+
276
+ test("drops audio once the outbound buffer is saturated", async () => {
277
+ const { transcriber } = await startSession();
278
+ mockWs.bufferedAmount = 8 * 1024 * 1024;
279
+
280
+ transcriber.sendAudio(Buffer.from([1, 2]), "audio/pcm");
281
+
282
+ expect(mockWs.sentData).toHaveLength(0);
283
+ });
284
+
285
+ test("logs the observed chunk duration once per session at info", async () => {
286
+ const { transcriber } = await startSession({ sampleRate: 16_000 });
287
+
288
+ // 2560 bytes of mono linear16 at 16kHz is exactly 80ms.
289
+ transcriber.sendAudio(Buffer.alloc(2560), "audio/pcm");
290
+ transcriber.sendAudio(Buffer.alloc(2560), "audio/pcm");
291
+
292
+ // The runbook reads this line off a default daemon, whose pino level is
293
+ // `info`: at debug it would never reach the log it is read from.
294
+ const cadenceLogs = infoLogs.filter((entry) =>
295
+ entry.message.includes("chunk cadence"),
296
+ );
297
+ expect(cadenceLogs).toHaveLength(1);
298
+ expect(cadenceLogs[0]!.data).toMatchObject({
299
+ observedChunkMs: 80,
300
+ recommendedChunkMs: 80,
301
+ sampleRate: 16_000,
302
+ });
303
+ });
304
+ });
305
+
306
+ // ─────────────────────────────────────────────────────────────────
307
+ // Frame handling
308
+ // ─────────────────────────────────────────────────────────────────
309
+
310
+ describe("frame handling", () => {
311
+ test("a scripted turn produces the expected event stream", async () => {
312
+ const { events } = await startSession();
313
+
314
+ mockWs.simulateMessage(
315
+ JSON.stringify({ type: "Connected", request_id: "flux-request-id" }),
316
+ );
317
+ mockWs.simulateMessage(turnInfoFrame("StartOfTurn"));
318
+ mockWs.simulateMessage(
319
+ turnInfoFrame("Update", { transcript: "what is the" }),
320
+ );
321
+ mockWs.simulateMessage(
322
+ turnInfoFrame("EndOfTurn", {
323
+ transcript: "what is the weather",
324
+ end_of_turn_confidence: 0.91,
325
+ }),
326
+ );
327
+
328
+ expect(events).toEqual([
329
+ { type: "turn-start", turnIndex: 0 },
330
+ { type: "partial", text: "what is the" },
331
+ // The final comes first so a consumer that ignores turn events still
332
+ // commits the transcript exactly as it does for any other provider.
333
+ { type: "final", text: "what is the weather" },
334
+ {
335
+ type: "turn-end",
336
+ text: "what is the weather",
337
+ confidence: 0.91,
338
+ turnIndex: 0,
339
+ },
340
+ ]);
341
+ });
342
+
343
+ test("an EndOfTurn frame emits final before turn-end", async () => {
344
+ const { events } = await startSession();
345
+
346
+ mockWs.simulateMessage(
347
+ turnInfoFrame("EndOfTurn", { transcript: "done" }),
348
+ );
349
+
350
+ expect(events.map((event) => event.type)).toEqual(["final", "turn-end"]);
351
+ });
352
+
353
+ test("a fatal Error frame surfaces its category", async () => {
354
+ const { events } = await startSession();
355
+
356
+ mockWs.simulateMessage(
357
+ JSON.stringify({
358
+ type: "Error",
359
+ code: "INVALID_AUTH",
360
+ description: "Invalid credentials",
361
+ }),
362
+ );
363
+
364
+ expect(events).toHaveLength(1);
365
+ expect(events[0]).toMatchObject({ type: "error", category: "auth" });
366
+ });
367
+
368
+ test("unrecognized and malformed frames leave the session transcribing", async () => {
369
+ const { events } = await startSession();
370
+
371
+ // Deepgram can add frame types without warning, so anything the parser
372
+ // does not recognize is dropped rather than raised as a stream failure.
373
+ mockWs.simulateMessage("not json at all");
374
+ mockWs.simulateMessage(JSON.stringify([1, 2, 3]));
375
+ mockWs.simulateMessage(
376
+ JSON.stringify({ type: "SomethingNewFromDeepgram" }),
377
+ );
378
+
379
+ expect(events).toEqual([]);
380
+
381
+ mockWs.simulateMessage(
382
+ turnInfoFrame("EndOfTurn", { transcript: "still here" }),
383
+ );
384
+ expect(events).toEqual([
385
+ { type: "final", text: "still here" },
386
+ { type: "turn-end", text: "still here", turnIndex: 0 },
387
+ ]);
388
+ });
389
+ });
390
+
391
+ // ─────────────────────────────────────────────────────────────────
392
+ // Error mapping
393
+ // ─────────────────────────────────────────────────────────────────
394
+
395
+ describe("error mapping", () => {
396
+ test("an auth rejection during connect rejects with an auth SttError", async () => {
397
+ const transcriber = new DeepgramFluxRealtimeTranscriber(TEST_API_KEY);
398
+ const startPromise = transcriber.start(() => {});
399
+ mockWs.simulateClose(1008, "invalid credentials");
400
+
401
+ const err = await startPromise.catch((e: unknown) => e);
402
+ expect(err).toBeInstanceOf(SttError);
403
+ expect((err as SttError).category).toBe("auth");
404
+ });
405
+
406
+ test("a connect timeout rejects with a timeout SttError", async () => {
407
+ const transcriber = new DeepgramFluxRealtimeTranscriber(TEST_API_KEY, {
408
+ connectTimeoutMs: 20,
409
+ });
410
+
411
+ const err = await transcriber.start(() => {}).catch((e: unknown) => e);
412
+ expect(err).toBeInstanceOf(SttError);
413
+ expect((err as SttError).category).toBe("timeout");
414
+ });
415
+
416
+ test("an in-session auth close emits an auth error then closed", async () => {
417
+ const { events } = await startSession();
418
+
419
+ mockWs.simulateClose(1008, "invalid credentials");
420
+
421
+ expect(events).toHaveLength(2);
422
+ expect(events[0]).toMatchObject({ type: "error", category: "auth" });
423
+ expect(events[1]).toEqual({ type: "closed" });
424
+ });
425
+
426
+ test("a load-shedding close maps to rate-limit", async () => {
427
+ const { events } = await startSession();
428
+
429
+ mockWs.simulateClose(1013, "try again later");
430
+
431
+ expect(events[0]).toMatchObject({
432
+ type: "error",
433
+ category: "rate-limit",
434
+ });
435
+ });
436
+
437
+ test("any other unexpected close maps to provider-error", async () => {
438
+ const { events } = await startSession();
439
+
440
+ mockWs.simulateClose(1006, "abnormal");
441
+
442
+ expect(events[0]).toMatchObject({
443
+ type: "error",
444
+ category: "provider-error",
445
+ });
446
+ });
447
+
448
+ test("a socket error emits provider-error then closed", async () => {
449
+ const { events } = await startSession();
450
+
451
+ mockWs.simulateError(new Error("connection reset"));
452
+
453
+ expect(events[0]).toMatchObject({
454
+ type: "error",
455
+ category: "provider-error",
456
+ });
457
+ expect(events[1]).toEqual({ type: "closed" });
458
+ });
459
+
460
+ test("a normal close after a fatal Error frame adds no second error", async () => {
461
+ const { events } = await startSession();
462
+
463
+ mockWs.simulateMessage(
464
+ JSON.stringify({
465
+ type: "Error",
466
+ code: "INVALID_AUTH",
467
+ description: "Invalid credentials",
468
+ }),
469
+ );
470
+ mockWs.simulateClose(1000, "closing");
471
+
472
+ expect(events).toHaveLength(2);
473
+ expect(events[0]).toMatchObject({
474
+ type: "error",
475
+ category: "auth",
476
+ message: "Deepgram Flux error (INVALID_AUTH): Invalid credentials",
477
+ });
478
+ expect(events[1]).toEqual({ type: "closed" });
479
+ });
480
+
481
+ test("an abnormal close after a fatal Error frame adds no second error", async () => {
482
+ const { events } = await startSession();
483
+
484
+ mockWs.simulateMessage(
485
+ JSON.stringify({
486
+ type: "Error",
487
+ code: "RATE_LIMIT_EXCEEDED",
488
+ description: "Too many requests",
489
+ }),
490
+ );
491
+ mockWs.simulateClose(1006, "abnormal");
492
+
493
+ expect(events).toHaveLength(2);
494
+ expect(events[0]).toMatchObject({
495
+ type: "error",
496
+ category: "rate-limit",
497
+ message: "Deepgram Flux error (RATE_LIMIT_EXCEEDED): Too many requests",
498
+ });
499
+ expect(events[1]).toEqual({ type: "closed" });
500
+ });
501
+
502
+ test("a socket error after a fatal Error frame adds no second error", async () => {
503
+ const { events } = await startSession();
504
+
505
+ mockWs.simulateMessage(
506
+ JSON.stringify({
507
+ type: "Error",
508
+ code: "INTERNAL_SERVER_ERROR",
509
+ description: "Something broke",
510
+ }),
511
+ );
512
+ mockWs.simulateError(new Error("connection reset"));
513
+
514
+ expect(events).toHaveLength(2);
515
+ expect(events[0]).toMatchObject({ type: "error" });
516
+ expect(events[1]).toEqual({ type: "closed" });
517
+ });
518
+
519
+ test("the inactivity watchdog only fires while audio awaits a response", async () => {
520
+ const { transcriber, events } = await startSession({
521
+ inactivityTimeoutMs: 20,
522
+ });
523
+
524
+ // Idle with nothing owed: the watchdog re-arms rather than firing.
525
+ await Bun.sleep(60);
526
+ expect(events).toEqual([]);
527
+
528
+ transcriber.sendAudio(Buffer.from([1, 2]), "audio/pcm");
529
+ await Bun.sleep(80);
530
+
531
+ expect(events[0]).toMatchObject({ type: "error", category: "timeout" });
532
+ expect(events.at(-1)).toEqual({ type: "closed" });
533
+ });
534
+ });
535
+
536
+ // ─────────────────────────────────────────────────────────────────
537
+ // Lifecycle
538
+ // ─────────────────────────────────────────────────────────────────
539
+
540
+ describe("lifecycle", () => {
541
+ test("start() throws when called twice", async () => {
542
+ const { transcriber } = await startSession();
543
+
544
+ await expect(transcriber.start(() => {})).rejects.toThrow(
545
+ "start() called twice",
546
+ );
547
+ });
548
+
549
+ test("stop() sends CloseStream and reports closed once the socket closes", async () => {
550
+ const { transcriber, events } = await startSession();
551
+
552
+ transcriber.stop();
553
+
554
+ expect(mockWs.sentData).toEqual([
555
+ JSON.stringify({ type: "CloseStream" }),
556
+ ]);
557
+ expect(events).toEqual([]);
558
+
559
+ mockWs.simulateClose(1000, "");
560
+ expect(events).toEqual([{ type: "closed" }]);
561
+ });
562
+
563
+ test("stop() is idempotent and closed is emitted exactly once", async () => {
564
+ const { transcriber, events } = await startSession();
565
+
566
+ transcriber.stop();
567
+ mockWs.simulateClose(1000, "");
568
+ transcriber.stop();
569
+ mockWs.simulateClose(1000, "");
570
+
571
+ expect(events).toEqual([{ type: "closed" }]);
572
+ });
573
+
574
+ test("keepalive frames go out on the configured interval", async () => {
575
+ const { transcriber } = await startSession({ keepaliveIntervalMs: 10 });
576
+
577
+ await Bun.sleep(35);
578
+ transcriber.stop();
579
+
580
+ const keepalives = mockWs.sentData.filter(
581
+ (data) => data === JSON.stringify({ type: "KeepAlive" }),
582
+ );
583
+ expect(keepalives.length).toBeGreaterThanOrEqual(2);
584
+ });
585
+
586
+ test("finalizeUtterance is absent, Flux has no mid-stream flush", async () => {
587
+ const { transcriber } = await startSession();
588
+
589
+ // Flux commits a transcript only on EndOfTurn and its protocol has no
590
+ // flush that keeps the socket open, so a finalizeUtterance here could
591
+ // only claim a commit that never happened and lose the tail of every
592
+ // turn released on a caller-side boundary. Callers feature-detect the
593
+ // method and fall back to stop(). The contract type is where the
594
+ // optional method lives, so the check goes through it.
595
+ const asContract: StreamingTranscriber = transcriber;
596
+ expect(asContract.finalizeUtterance).toBeUndefined();
597
+ });
598
+
599
+ test("audio sent after stop() never reaches the socket", async () => {
600
+ const { transcriber } = await startSession();
601
+
602
+ transcriber.stop();
603
+ mockWs.sentData.length = 0;
604
+ transcriber.sendAudio(Buffer.from([1, 2]), "audio/pcm");
605
+
606
+ expect(mockWs.sentData).toEqual([]);
607
+ });
608
+ });
609
+
610
+ // ─────────────────────────────────────────────────────────────────
611
+ // Identity
612
+ // ─────────────────────────────────────────────────────────────────
613
+
614
+ test("reports the deepgram-flux provider on the daemon-streaming boundary", () => {
615
+ const transcriber = new DeepgramFluxRealtimeTranscriber(TEST_API_KEY);
616
+
617
+ expect(transcriber.providerId).toBe("deepgram-flux");
618
+ expect(transcriber.boundaryId).toBe("daemon-streaming");
619
+ });
620
+ });
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
+ batchBoundaryGapReason,
4
5
  getCredentialProvider,
5
6
  getProviderEntry,
6
7
  listCredentialProviderNames,
@@ -23,6 +24,7 @@ describe("STT provider catalog", () => {
23
24
  const ids = listProviderIds();
24
25
  expect(ids).toContain("openai-whisper");
25
26
  expect(ids).toContain("deepgram");
27
+ expect(ids).toContain("deepgram-flux");
26
28
  expect(ids).toContain("google-gemini");
27
29
  });
28
30
 
@@ -101,6 +103,27 @@ describe("STT provider catalog", () => {
101
103
  expect(supportsBoundary("openai-whisper", "daemon-streaming")).toBe(true);
102
104
  });
103
105
 
106
+ test("deepgram-flux is streaming-only", () => {
107
+ // Flux has no batch endpoint; the batch transcriber factory rejects it
108
+ // explicitly rather than silently falling through.
109
+ expect(supportsBoundary("deepgram-flux", "daemon-streaming")).toBe(true);
110
+ expect(supportsBoundary("deepgram-flux", "daemon-batch")).toBe(false);
111
+ });
112
+
113
+ test("batchBoundaryGapReason names the batch provider on the same credential", () => {
114
+ const reason = batchBoundaryGapReason("deepgram-flux");
115
+ expect(reason).toBe(
116
+ 'Deepgram Flux is streaming-only. Batch transcription requires the deepgram provider: set services.stt.provider to "deepgram".',
117
+ );
118
+ });
119
+
120
+ test("batchBoundaryGapReason falls back to generic guidance for an unknown provider", () => {
121
+ // No catalog entry means no credential to search a batch-capable peer on.
122
+ const reason = batchBoundaryGapReason("nonexistent" as never);
123
+ expect(reason).toContain("nonexistent is streaming-only");
124
+ expect(reason).toContain("supports batch transcription");
125
+ });
126
+
104
127
  test("supportsBoundary returns false for unknown provider IDs", () => {
105
128
  // Cast to bypass type checking for the test
106
129
  expect(supportsBoundary("nonexistent" as never, "daemon-batch")).toBe(
@@ -150,6 +173,8 @@ describe("STT provider catalog", () => {
150
173
  ["deepgram", "manual"],
151
174
  ["vellum", "manual"],
152
175
  ["xai", "manual"],
176
+ // Flux takes no language parameter: its model is monolingual English.
177
+ ["deepgram-flux", "auto"],
153
178
  ["google-gemini", "auto"],
154
179
  ["openai-whisper", "auto"],
155
180
  ] as const;
@@ -171,6 +196,15 @@ describe("STT provider catalog", () => {
171
196
  expect(getCredentialProvider("google-gemini")).toBe("gemini");
172
197
  });
173
198
 
199
+ test("deepgram-flux shares the deepgram credential", () => {
200
+ expect(getCredentialProvider("deepgram-flux")).toBe("deepgram");
201
+ // Sharing must not duplicate the key in the API-key provider list.
202
+ const deepgramEntries = listCredentialProviderNames().filter(
203
+ (name) => name === "deepgram",
204
+ );
205
+ expect(deepgramEntries).toEqual(["deepgram"]);
206
+ });
207
+
174
208
  test("getCredentialProvider returns undefined for unknown ID", () => {
175
209
  expect(getCredentialProvider("nonexistent" as never)).toBeUndefined();
176
210
  });