@vellumai/assistant 0.11.3 → 0.11.4-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 (324) 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 +140 -38
  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 +2 -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 +55 -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__/user-plugin-loader.test.ts +1 -1
  67. package/src/__tests__/visible-app-context.test.ts +16 -9
  68. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  69. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  70. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  71. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  72. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  73. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  74. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  75. package/src/acp/__tests__/auth-required.test.ts +161 -0
  76. package/src/acp/acp-claude-oauth.ts +19 -2
  77. package/src/acp/agent-process.test.ts +100 -0
  78. package/src/acp/agent-process.ts +29 -26
  79. package/src/acp/auth-required.ts +102 -0
  80. package/src/acp/session-manager.test.ts +119 -0
  81. package/src/acp/session-manager.ts +68 -2
  82. package/src/api/events/acp-auth-required.ts +55 -0
  83. package/src/api/index.ts +7 -0
  84. package/src/apps/app-store.ts +3 -0
  85. package/src/bundler/package-resolver.ts +2 -30
  86. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  87. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  88. package/src/calls/call-controller.ts +9 -2
  89. package/src/calls/call-setup-flow.ts +0 -1
  90. package/src/calls/media-stream-stt-session.ts +15 -0
  91. package/src/calls/voice-session-bridge.ts +71 -16
  92. package/src/calls/voice-triage-escalate.ts +104 -2
  93. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  94. package/src/channels/config.ts +13 -0
  95. package/src/channels/plugin-channel-declarations.ts +108 -0
  96. package/src/channels/types.ts +30 -0
  97. package/src/cli/AGENTS.md +5 -2
  98. package/src/cli/commands/credentials.help.ts +2 -2
  99. package/src/cli/commands/inference-providers.ts +1 -1
  100. package/src/cli/commands/mcp.help.ts +13 -4
  101. package/src/cli/commands/mcp.ts +9 -0
  102. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  103. package/src/cli/commands/memory/index.help.ts +43 -1
  104. package/src/cli/commands/memory/memory-v3.ts +64 -0
  105. package/src/cli/commands/stt.help.ts +27 -2
  106. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  107. package/src/cli/lib/bundled-marketplace.json +13 -0
  108. package/src/cli/lib/upgrade-plugin.ts +42 -0
  109. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  110. package/src/config/__tests__/default-provider.test.ts +6 -1
  111. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  112. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  113. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  114. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  115. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  116. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  117. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  118. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  119. package/src/config/call-site-defaults.ts +4 -5
  120. package/src/config/default-profile-catalog.ts +82 -11
  121. package/src/config/default-profile-names.ts +4 -1
  122. package/src/config/default-provider-resolution.ts +4 -0
  123. package/src/config/llm-context-resolution.ts +11 -3
  124. package/src/config/llm-resolver.ts +28 -1
  125. package/src/config/profile-materialization.ts +70 -22
  126. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  127. package/src/config/schemas/call-site-catalog.ts +4 -4
  128. package/src/config/schemas/live-voice.ts +57 -23
  129. package/src/config/schemas/llm.ts +59 -32
  130. package/src/config/schemas/mcp.ts +23 -0
  131. package/src/config/schemas/plugin-updates.ts +6 -2
  132. package/src/config/schemas/stt.ts +1 -0
  133. package/src/context/outbound-sanitize.ts +96 -1
  134. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  135. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  136. package/src/daemon/conversation-agent-loop.ts +17 -6
  137. package/src/daemon/conversation-messaging.ts +5 -1
  138. package/src/daemon/conversation-notifiers.ts +9 -1
  139. package/src/daemon/conversation-process.ts +9 -6
  140. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  141. package/src/daemon/conversation-tool-setup.ts +1 -2
  142. package/src/daemon/conversation.ts +48 -0
  143. package/src/daemon/mcp-reload-service.ts +36 -6
  144. package/src/daemon/process-message.ts +13 -3
  145. package/src/daemon/providers-setup.ts +6 -3
  146. package/src/daemon/trust-context-types.ts +29 -0
  147. package/src/daemon/wake-conversation-ops.ts +3 -2
  148. package/src/documents/document-store.ts +138 -5
  149. package/src/hooks/hook-loader.ts +3 -3
  150. package/src/hooks/registry.ts +50 -6
  151. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  152. package/src/inbound/oauth-callback-url.ts +61 -0
  153. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  154. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  155. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  156. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  157. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  158. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  159. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  160. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  161. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  162. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  163. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  164. package/src/live-voice/live-voice-archive.ts +2 -0
  165. package/src/live-voice/live-voice-metrics.ts +57 -32
  166. package/src/live-voice/live-voice-photo.ts +1 -2
  167. package/src/live-voice/live-voice-session.ts +535 -314
  168. package/src/live-voice/progress-narration.ts +277 -0
  169. package/src/live-voice/protocol.ts +21 -1
  170. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  171. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  172. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  173. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  174. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  175. package/src/mcp/client.ts +16 -11
  176. package/src/mcp/effective-config.ts +113 -0
  177. package/src/mcp/manager.ts +11 -6
  178. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  179. package/src/mcp/mcp-oauth-provider.ts +205 -240
  180. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  181. package/src/monitoring/plugin-auto-update.ts +128 -24
  182. package/src/notifications/signal.ts +1 -0
  183. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  184. package/src/permissions/confirmation-guardian-request.ts +2 -2
  185. package/src/permissions/question-guardian-request.test.ts +14 -6
  186. package/src/permissions/question-guardian-request.ts +1 -2
  187. package/src/persistence/attachments-store.ts +8 -1
  188. package/src/persistence/bookmark-crud.ts +3 -7
  189. package/src/persistence/conversation-attention-store.ts +16 -45
  190. package/src/persistence/conversation-crud.ts +33 -4
  191. package/src/persistence/conversation-lineage.ts +9 -0
  192. package/src/persistence/conversation-queries.ts +108 -41
  193. package/src/persistence/delivery-crud.ts +38 -29
  194. package/src/persistence/external-conversation-store.ts +32 -4
  195. package/src/persistence/llm-request-log-store.ts +4 -10
  196. package/src/persistence/llm-usage-store.ts +8 -3
  197. package/src/persistence/message-reads.test.ts +197 -0
  198. package/src/persistence/message-reads.ts +211 -0
  199. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  200. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  201. package/src/persistence/real-user-turn-filter.ts +27 -3
  202. package/src/persistence/steps.ts +9 -0
  203. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  204. package/src/plugin-api/conversation-turn.ts +168 -5
  205. package/src/plugin-api/index.ts +21 -5
  206. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  207. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  208. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  209. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  210. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  211. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  212. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  213. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  214. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  215. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  216. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  217. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  218. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  219. package/src/plugins/defaults/memory/indexer.ts +3 -1
  220. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  221. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  222. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  223. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  224. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  225. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  226. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  227. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  228. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  229. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  230. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  231. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  232. package/src/plugins/defaults/memory/worker.ts +6 -3
  233. package/src/plugins/external-plugin-loader.ts +47 -0
  234. package/src/plugins/mcp-servers.ts +361 -0
  235. package/src/plugins/mtime-cache.ts +23 -49
  236. package/src/plugins/worker-plugin-surface.ts +33 -0
  237. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  238. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  239. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  240. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  241. package/src/providers/call-site-routing.ts +30 -3
  242. package/src/providers/connection-resolution.ts +194 -11
  243. package/src/providers/inference/auth.ts +6 -0
  244. package/src/providers/inference/connection-availability.ts +24 -2
  245. package/src/providers/inference/connections.ts +2 -0
  246. package/src/providers/model-intents.ts +26 -6
  247. package/src/providers/openai/codex-models.ts +2 -1
  248. package/src/providers/provider-send-message.ts +32 -3
  249. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  250. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  251. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  252. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  253. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  254. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  255. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  256. package/src/providers/speech-to-text/resolve.ts +25 -2
  257. package/src/routes/worker.ts +17 -5
  258. package/src/runtime/access-request-helper.ts +9 -12
  259. package/src/runtime/agent-wake.ts +3 -3
  260. package/src/runtime/pre-first-message-gate.ts +4 -0
  261. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  262. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  263. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  264. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  265. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  266. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  267. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  268. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  269. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  270. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  271. package/src/runtime/routes/channel-route-shared.ts +0 -6
  272. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  273. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  274. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  275. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  276. package/src/runtime/routes/default-provider-routes.ts +15 -0
  277. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  278. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  279. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  280. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  281. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  282. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  283. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  284. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  285. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  286. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  287. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  288. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  289. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  291. package/src/runtime/routes/log-export-routes.ts +3 -0
  292. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  293. package/src/runtime/routes/plugins-routes.ts +21 -3
  294. package/src/runtime/routes/stt-routes.ts +31 -25
  295. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  296. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  297. package/src/runtime/routes/user-route-import.ts +108 -0
  298. package/src/schedule/worker.ts +6 -0
  299. package/src/security/oauth2.ts +6 -22
  300. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  301. package/src/stt/__tests__/types.test.ts +94 -0
  302. package/src/stt/daemon-batch-transcriber.ts +10 -0
  303. package/src/stt/stt-stream-session.ts +8 -4
  304. package/src/stt/types.ts +103 -0
  305. package/src/subagent/manager.ts +1 -3
  306. package/src/subagent/types.ts +7 -6
  307. package/src/tools/acp/spawn.test.ts +97 -0
  308. package/src/tools/acp/spawn.ts +32 -0
  309. package/src/tools/document/document-tool.ts +12 -3
  310. package/src/tools/registry.ts +2 -1
  311. package/src/tools/workflows/run-workflow.ts +1 -2
  312. package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
  313. package/src/tts/reasoning-tag-filter.ts +110 -0
  314. package/src/workspace/byok-default-profile-ensure.ts +61 -18
  315. package/src/workspace/custom-profile-ensure.ts +4 -24
  316. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  317. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  318. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  319. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  320. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  321. package/src/workspace/migrations/registry.ts +8 -0
  322. package/src/workspace/provider-commit-message-generator.ts +7 -5
  323. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  324. package/src/live-voice/front-decision.ts +0 -476
@@ -126,6 +126,12 @@ export const VALID_CONNECTION_PROVIDERS: readonly string[] = [
126
126
  // persisted `vellum` rows — the routing threaded in via `providerOverride`
127
127
  // never runs on a row that fails to load.
128
128
  VELLUM_MANAGED_PROVIDER,
129
+ // The ChatGPT-subscription row stores the "chatgpt" routing identity in its
130
+ // `provider` column for the same reason: the row IS the subscription route
131
+ // (auth modality = provider identity), and dispatch derives the openai
132
+ // upstream per-request. Allowlisted explicitly or the DB loaders would drop
133
+ // the persisted row.
134
+ "chatgpt",
129
135
  ];
130
136
 
131
137
  export type ConnectionProvider = string;
@@ -23,6 +23,10 @@ import {
23
23
  describeSubscriptionModelIncompatibility,
24
24
  isConnectionCompatibleWithModel,
25
25
  } from "../connection-model-compat.js";
26
+ import {
27
+ connectionProviderKind,
28
+ resolveEntryConnectionName,
29
+ } from "../connection-resolution.js";
26
30
  import { PROVIDER_CATALOG } from "../model-catalog.js";
27
31
  import { resolveManagedProxyContext } from "../platform-proxy/context.js";
28
32
  import {
@@ -161,7 +165,12 @@ export async function computeConnectionAvailability(
161
165
  if (resolvedConnectionName === VELLUM_MANAGED_CONNECTION_NAME) {
162
166
  return vellumConnectionAvailability();
163
167
  }
164
- if (connection.provider !== provider) {
168
+ // The ChatGPT-subscription row stores the "chatgpt" identity while its
169
+ // upstream is openai; the credential switch below is the right check for
170
+ // it (the subscription token is its credential).
171
+ const isChatgptRow =
172
+ connection.provider === "chatgpt" && provider === "openai";
173
+ if (!isChatgptRow && connection.provider !== provider) {
165
174
  return {
166
175
  status: "provider_mismatch",
167
176
  message: `Connection "${resolvedConnectionName}" is for provider "${connection.provider}", but the requested provider is "${provider}". Pick a connection for "${provider}" ${SETTINGS_HINT}.`,
@@ -326,9 +335,22 @@ export async function computeProfileAvailability(
326
335
  }
327
336
  }
328
337
 
338
+ // Precedence matches dispatch: an explicit provider_connection wins over
339
+ // an entry-name provider, and the vendor judged is the entry's
340
+ // dispatchable kind either way (the same translation dispatch uses).
341
+ const entryName = resolveEntryConnectionName(provider);
342
+ const entryKind =
343
+ entryName !== null
344
+ ? (connectionProviderKind(entryName, model) ?? provider)
345
+ : provider;
346
+
329
347
  const pinned = entry.provider_connection;
330
348
  if (typeof pinned === "string") {
331
- return computeConnectionAvailability(provider, pinned);
349
+ return computeConnectionAvailability(entryKind, pinned);
350
+ }
351
+
352
+ if (entryName !== null) {
353
+ return computeConnectionAvailability(entryKind, entryName);
332
354
  }
333
355
 
334
356
  // Mirror the dispatch-time auto-resolve (`resolveConfiguredProvider`): with
@@ -348,10 +348,12 @@ export function updateConnection(
348
348
  connection: {
349
349
  ...existing,
350
350
  auth,
351
+ provider: nextProvider,
351
352
  label: input.label !== undefined ? input.label : existing.label,
352
353
  baseUrl: nextBaseUrl,
353
354
  models: nextModels,
354
355
  updatedAt: now,
356
+ isManaged: isManagedRow({ name, provider: nextProvider, auth }),
355
357
  },
356
358
  };
357
359
  }
@@ -1,4 +1,5 @@
1
1
  import { isModelInCatalog, PROVIDER_CATALOG } from "./model-catalog.js";
2
+ import { isCodexSubscriptionModel } from "./openai/codex-models.js";
2
3
  import type { ModelIntent } from "./types.js";
3
4
 
4
5
  /**
@@ -10,8 +11,10 @@ const PROVIDER_DEFAULT_MODELS: Record<string, string> = Object.fromEntries(
10
11
  );
11
12
 
12
13
  // `cost-optimized` is the cheapest model a provider serves, `latency-optimized`
13
- // the fastest to first token. On anthropic, ollama and fireworks the same model
14
- // is both, so those columns repeat by construction rather than by oversight.
14
+ // the fastest to first token. On anthropic, ollama, fireworks and openai the
15
+ // same model is both, so those columns repeat by construction rather than by
16
+ // oversight (openai's balanced repeats it too: the default-profile templates
17
+ // split those tiers by reasoning effort, not model).
15
18
  const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
16
19
  anthropic: {
17
20
  balanced: "claude-sonnet-4-6",
@@ -21,11 +24,11 @@ const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
21
24
  "vision-optimized": "claude-opus-4-6",
22
25
  },
23
26
  openai: {
24
- balanced: "gpt-5.4-mini",
25
- "cost-optimized": "gpt-5.4-nano",
27
+ balanced: "gpt-5.6-luna",
28
+ "cost-optimized": "gpt-5.6-luna",
26
29
  "latency-optimized": "gpt-5.6-luna",
27
- "quality-optimized": "gpt-5.4",
28
- "vision-optimized": "gpt-5.4",
30
+ "quality-optimized": "gpt-5.6-sol",
31
+ "vision-optimized": "gpt-5.6-terra",
29
32
  },
30
33
  gemini: {
31
34
  balanced: "gemini-3-flash-preview",
@@ -89,6 +92,23 @@ for (const [provider, intents] of Object.entries(PROVIDER_MODEL_INTENTS)) {
89
92
  }
90
93
  }
91
94
 
95
+ // The openai column must additionally stay inside the ChatGPT Codex
96
+ // subscription set: the subscription default provider is stored as
97
+ // `llm.defaultProvider = { provider: "openai", connectionName:
98
+ // "chatgpt-subscription" }`, so the materialized default profiles resolve
99
+ // through this column while pinning the subscription connection, which
100
+ // bypasses the auto-resolution compat gate and hard-routes to the Codex
101
+ // endpoint, where a non-Codex model 400s on every request.
102
+ for (const [intent, modelId] of Object.entries(PROVIDER_MODEL_INTENTS.openai)) {
103
+ if (!isCodexSubscriptionModel(modelId)) {
104
+ throw new Error(
105
+ `PROVIDER_MODEL_INTENTS[openai][${intent}] references model "${modelId}" ` +
106
+ `which the ChatGPT subscription cannot serve. Pick a model from ` +
107
+ `CODEX_SUBSCRIPTION_MODEL_IDS in openai/codex-models.ts.`,
108
+ );
109
+ }
110
+ }
111
+
92
112
  export function isModelIntent(value: unknown): value is ModelIntent {
93
113
  return typeof value === "string" && MODEL_INTENTS.has(value as ModelIntent);
94
114
  }
@@ -13,9 +13,10 @@ export const CODEX_SUBSCRIPTION_MODEL_IDS: ReadonlySet<string> = new Set([
13
13
  "gpt-5.6-terra",
14
14
  "gpt-5.6-luna",
15
15
  "gpt-5.5",
16
+ // OpenAI retires these two from ChatGPT sign-in on 2026-08-31; API-key
17
+ // auth is unaffected.
16
18
  "gpt-5.4",
17
19
  "gpt-5.4-mini",
18
- "gpt-5.3-codex",
19
20
  ]);
20
21
 
21
22
  /** True when `model` is allowlisted for Codex subscription routing. */
@@ -17,6 +17,10 @@ import {
17
17
  isConnectionCompatibleWithModel,
18
18
  } from "./connection-model-compat.js";
19
19
  import {
20
+ connectionProviderKind,
21
+ dispatchProviderResolvable,
22
+ expectedVendorProvider,
23
+ resolveEntryConnectionName,
20
24
  resolveRoutingIdentity,
21
25
  tryResolveProviderForConnectionName,
22
26
  } from "./connection-resolution.js";
@@ -37,6 +41,13 @@ const log = getLogger("provider-send-message");
37
41
  export interface ConfiguredProviderResult {
38
42
  provider: Provider;
39
43
  configuredProviderName: string;
44
+ /**
45
+ * True when the profile's provider was an entry label and dispatch routed
46
+ * through that row. The row's own credential already signed resolution, so
47
+ * vendor-keyed preflights (which check the generic per-vendor key slot)
48
+ * do not apply to this result.
49
+ */
50
+ entryRouted: boolean;
40
51
  }
41
52
 
42
53
  export type ConfiguredProviderOptions = Pick<
@@ -140,7 +151,10 @@ export async function resolveConfiguredProvider(
140
151
  }
141
152
  }
142
153
 
143
- const resolved = resolveCallSiteConfig(callSite, config.llm, opts);
154
+ const resolved = resolveCallSiteConfig(callSite, config.llm, {
155
+ ...opts,
156
+ isResolvableProvider: dispatchProviderResolvable,
157
+ });
144
158
  const inferenceProvider = resolved.provider;
145
159
  let connectionName = resolved.provider_connection;
146
160
 
@@ -158,6 +172,14 @@ export async function resolveConfiguredProvider(
158
172
  resolved.model,
159
173
  )?.connectionName;
160
174
  }
175
+ // An entry-name provider IS the connection name: the label points at a
176
+ // row, and the row's own provider drives dispatch.
177
+ const entryRoute = connectionName
178
+ ? null
179
+ : resolveEntryConnectionName(inferenceProvider);
180
+ if (entryRoute) {
181
+ connectionName = entryRoute;
182
+ }
161
183
  if (!connectionName) {
162
184
  if (inferenceProvider) {
163
185
  try {
@@ -202,7 +224,7 @@ export async function resolveConfiguredProvider(
202
224
  const connectionProvider = await tryResolveProviderForConnectionName(
203
225
  connectionName,
204
226
  config,
205
- inferenceProvider,
227
+ expectedVendorProvider(inferenceProvider, resolved.model),
206
228
  resolved.model,
207
229
  );
208
230
  if (!connectionProvider) {
@@ -234,7 +256,14 @@ export async function resolveConfiguredProvider(
234
256
  opts.overrideProfile,
235
257
  opts.forceOverrideProfile,
236
258
  ),
237
- configuredProviderName: inferenceProvider,
259
+ // The dispatched vendor kind, not the config label: an entry-name
260
+ // provider reports its row's kind so provider-keyed consumers
261
+ // (telemetry, pricing) never see a label that matches no catalog id.
262
+ configuredProviderName:
263
+ (entryRoute
264
+ ? connectionProviderKind(entryRoute, resolved.model)
265
+ : undefined) ?? inferenceProvider,
266
+ entryRouted: entryRoute !== null,
238
267
  };
239
268
  }
240
269
 
@@ -0,0 +1,433 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ buildFluxQueryParams,
5
+ parseFluxFrame,
6
+ } from "../deepgram-flux-frames.js";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Fixtures: one frame per documented Flux type
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const CONNECTED = {
13
+ type: "Connected",
14
+ request_id: "req-123",
15
+ sequence_id: 0,
16
+ };
17
+
18
+ const START_OF_TURN = {
19
+ type: "TurnInfo",
20
+ event: "StartOfTurn",
21
+ request_id: "req-123",
22
+ turn_index: 0,
23
+ audio_window_start: 0,
24
+ audio_window_end: 0.32,
25
+ transcript: "",
26
+ words: [],
27
+ };
28
+
29
+ const UPDATE = {
30
+ type: "TurnInfo",
31
+ event: "Update",
32
+ turn_index: 0,
33
+ transcript: "what is the",
34
+ words: [
35
+ { word: "what", confidence: 0.99, start: 0.1, end: 0.28 },
36
+ { word: "is", confidence: 0.98, start: 0.28, end: 0.4 },
37
+ { word: "the", confidence: 0.97, start: 0.4, end: 0.55 },
38
+ ],
39
+ };
40
+
41
+ const EAGER_END_OF_TURN = {
42
+ type: "TurnInfo",
43
+ event: "EagerEndOfTurn",
44
+ turn_index: 0,
45
+ transcript: "what is the weather",
46
+ end_of_turn_confidence: 0.42,
47
+ };
48
+
49
+ const TURN_RESUMED = {
50
+ type: "TurnInfo",
51
+ event: "TurnResumed",
52
+ turn_index: 0,
53
+ };
54
+
55
+ const END_OF_TURN = {
56
+ type: "TurnInfo",
57
+ event: "EndOfTurn",
58
+ turn_index: 0,
59
+ transcript: "what is the weather today",
60
+ end_of_turn_confidence: 0.88,
61
+ words: [{ word: "today", confidence: 0.95, start: 1.1, end: 1.4 }],
62
+ };
63
+
64
+ const FATAL_ERROR = {
65
+ type: "Error",
66
+ sequence_id: 5,
67
+ code: "INTERNAL_SERVER_ERROR",
68
+ description: "An internal server error occurred while processing the request",
69
+ };
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // parseFluxFrame
73
+ // ---------------------------------------------------------------------------
74
+
75
+ describe("parseFluxFrame", () => {
76
+ test("Connected emits no events", () => {
77
+ expect(parseFluxFrame(CONNECTED)).toEqual([]);
78
+ });
79
+
80
+ test("StartOfTurn emits turn-start carrying the turn index", () => {
81
+ expect(parseFluxFrame(START_OF_TURN)).toEqual([
82
+ { type: "turn-start", turnIndex: 0 },
83
+ ]);
84
+ });
85
+
86
+ test("Update emits a partial with the interim transcript", () => {
87
+ expect(parseFluxFrame(UPDATE)).toEqual([
88
+ { type: "partial", text: "what is the" },
89
+ ]);
90
+ });
91
+
92
+ test("a bare TurnInfo frame emits a partial transcript refresh", () => {
93
+ expect(
94
+ parseFluxFrame({ type: "TurnInfo", transcript: "partial text" }),
95
+ ).toEqual([{ type: "partial", text: "partial text" }]);
96
+ });
97
+
98
+ test("EagerEndOfTurn emits eager-turn-end with the speculated text", () => {
99
+ expect(parseFluxFrame(EAGER_END_OF_TURN)).toEqual([
100
+ { type: "eager-turn-end", text: "what is the weather" },
101
+ ]);
102
+ });
103
+
104
+ test("TurnResumed emits turn-resumed", () => {
105
+ expect(parseFluxFrame(TURN_RESUMED)).toEqual([{ type: "turn-resumed" }]);
106
+ });
107
+
108
+ test("EndOfTurn emits final before turn-end", () => {
109
+ const events = parseFluxFrame(END_OF_TURN);
110
+ expect(events).toEqual([
111
+ { type: "final", text: "what is the weather today" },
112
+ {
113
+ type: "turn-end",
114
+ text: "what is the weather today",
115
+ confidence: 0.88,
116
+ turnIndex: 0,
117
+ },
118
+ ]);
119
+ // Consumers that ignore turn events must still see the commit first.
120
+ expect(events[0]?.type).toBe("final");
121
+ expect(events[1]?.type).toBe("turn-end");
122
+ });
123
+
124
+ test("EndOfTurn omits confidence when Deepgram sends none", () => {
125
+ expect(parseFluxFrame({ type: "EndOfTurn", transcript: "done" })).toEqual([
126
+ { type: "final", text: "done" },
127
+ { type: "turn-end", text: "done" },
128
+ ]);
129
+ });
130
+
131
+ test("flat frames without a nested event field are accepted", () => {
132
+ expect(parseFluxFrame({ type: "StartOfTurn" })).toEqual([
133
+ { type: "turn-start" },
134
+ ]);
135
+ expect(parseFluxFrame({ type: "Update", transcript: "hi" })).toEqual([
136
+ { type: "partial", text: "hi" },
137
+ ]);
138
+ expect(parseFluxFrame({ type: "TurnResumed" })).toEqual([
139
+ { type: "turn-resumed" },
140
+ ]);
141
+ });
142
+
143
+ test("JSON string payloads are parsed", () => {
144
+ expect(parseFluxFrame(JSON.stringify(END_OF_TURN))).toEqual([
145
+ { type: "final", text: "what is the weather today" },
146
+ {
147
+ type: "turn-end",
148
+ text: "what is the weather today",
149
+ confidence: 0.88,
150
+ turnIndex: 0,
151
+ },
152
+ ]);
153
+ });
154
+
155
+ test("a later turn keeps its own index on both turn events", () => {
156
+ expect(
157
+ parseFluxFrame({ ...START_OF_TURN, turn_index: 3, transcript: "" }),
158
+ ).toEqual([{ type: "turn-start", turnIndex: 3 }]);
159
+ expect(
160
+ parseFluxFrame({
161
+ type: "TurnInfo",
162
+ event: "EndOfTurn",
163
+ turn_index: 3,
164
+ transcript: "and then some",
165
+ }),
166
+ ).toEqual([
167
+ { type: "final", text: "and then some" },
168
+ { type: "turn-end", text: "and then some", turnIndex: 3 },
169
+ ]);
170
+ });
171
+
172
+ test("turn events omit the index when Deepgram sends none", () => {
173
+ expect(parseFluxFrame({ type: "StartOfTurn" })).toEqual([
174
+ { type: "turn-start" },
175
+ ]);
176
+ expect(parseFluxFrame({ type: "EndOfTurn", transcript: "done" })).toEqual([
177
+ { type: "final", text: "done" },
178
+ { type: "turn-end", text: "done" },
179
+ ]);
180
+ });
181
+
182
+ test("transcripts are trimmed and missing ones become empty strings", () => {
183
+ expect(
184
+ parseFluxFrame({ type: "Update", transcript: " spaced " }),
185
+ ).toEqual([{ type: "partial", text: "spaced" }]);
186
+ expect(parseFluxFrame({ type: "Update" })).toEqual([
187
+ { type: "partial", text: "" },
188
+ ]);
189
+ });
190
+
191
+ test("a fatal Error frame emits exactly one error event", () => {
192
+ const events = parseFluxFrame(FATAL_ERROR);
193
+ expect(events).toHaveLength(1);
194
+ expect(events[0]).toEqual({
195
+ type: "error",
196
+ category: "provider-error",
197
+ message:
198
+ "Deepgram Flux error (INTERNAL_SERVER_ERROR): An internal server error occurred while processing the request",
199
+ });
200
+ });
201
+
202
+ test("an Error frame preserves the provider description in the message", () => {
203
+ const [event] = parseFluxFrame(
204
+ JSON.stringify({
205
+ type: "Error",
206
+ code: "SOME_FUTURE_CODE",
207
+ description: "something specific went wrong upstream",
208
+ }),
209
+ );
210
+ expect(event?.type).toBe("error");
211
+ expect((event as { message: string }).message).toContain(
212
+ "something specific went wrong upstream",
213
+ );
214
+ expect((event as { message: string }).message).toContain(
215
+ "SOME_FUTURE_CODE",
216
+ );
217
+ });
218
+
219
+ test("Error frames are categorized from the provider code", () => {
220
+ const categoryOf = (frame: Record<string, unknown>) => {
221
+ const [event] = parseFluxFrame({ type: "Error", ...frame });
222
+ return (event as { category?: string } | undefined)?.category;
223
+ };
224
+
225
+ expect(categoryOf({ code: "INVALID_AUTH" })).toBe("auth");
226
+ expect(categoryOf({ code: "INSUFFICIENT_PERMISSIONS" })).toBe("auth");
227
+ expect(categoryOf({ code: "TOO_MANY_REQUESTS" })).toBe("rate-limit");
228
+ expect(categoryOf({ code: "REQUEST_TIMEOUT" })).toBe("timeout");
229
+ expect(categoryOf({ code: "ASR_UNPROCESSABLE_ENTITY" })).toBe(
230
+ "invalid-audio",
231
+ );
232
+ expect(categoryOf({ code: "INTERNAL_SERVER_ERROR" })).toBe(
233
+ "provider-error",
234
+ );
235
+ // Unrecognized and bare error frames still surface, as provider-error.
236
+ expect(categoryOf({ code: "SOMETHING_NEW" })).toBe("provider-error");
237
+ expect(categoryOf({})).toBe("provider-error");
238
+ // A complaint about eot_timeout_ms is a config error, not a transport
239
+ // timeout, so the timeout matcher must not claim it.
240
+ expect(
241
+ categoryOf({
242
+ code: "INVALID_EOT_TIMEOUT_MS",
243
+ description: "eot_timeout_ms is out of range",
244
+ }),
245
+ ).toBe("provider-error");
246
+ });
247
+
248
+ test("an Error frame with no description still yields a usable message", () => {
249
+ expect(parseFluxFrame({ type: "Error" })).toEqual([
250
+ {
251
+ type: "error",
252
+ category: "provider-error",
253
+ message: "Deepgram Flux error: no description provided",
254
+ },
255
+ ]);
256
+ });
257
+
258
+ test("unknown frame types emit nothing", () => {
259
+ expect(parseFluxFrame({ type: "SomeFutureFrame", data: 1 })).toEqual([]);
260
+ expect(
261
+ parseFluxFrame({ type: "TurnInfo", event: "FutureTurnState" }),
262
+ ).toEqual([]);
263
+ // Only the documented fatal `Error` frame becomes an error event: an
264
+ // unrecognized frame must never be reported as a stream failure, even
265
+ // when it carries error-shaped fields.
266
+ expect(
267
+ parseFluxFrame({ type: "ConfigureFailure", code: "INVALID_AUTH" }),
268
+ ).toEqual([]);
269
+ expect(parseFluxFrame({ type: "error", description: "lowercase" })).toEqual(
270
+ [],
271
+ );
272
+ });
273
+
274
+ test("malformed and arbitrary input never throws and emits nothing", () => {
275
+ const garbage: unknown[] = [
276
+ undefined,
277
+ null,
278
+ 0,
279
+ 1,
280
+ Number.NaN,
281
+ true,
282
+ "",
283
+ "not json",
284
+ "{ broken json",
285
+ "[]",
286
+ "null",
287
+ "42",
288
+ [],
289
+ [{ type: "EndOfTurn" }],
290
+ {},
291
+ { type: 7 },
292
+ { type: null },
293
+ { event: 7, type: 7 },
294
+ new Date(),
295
+ () => undefined,
296
+ Symbol("x"),
297
+ 10n,
298
+ ];
299
+ for (const value of garbage) {
300
+ expect(() => parseFluxFrame(value)).not.toThrow();
301
+ expect(parseFluxFrame(value)).toEqual([]);
302
+ }
303
+ });
304
+ });
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // buildFluxQueryParams
308
+ // ---------------------------------------------------------------------------
309
+
310
+ describe("buildFluxQueryParams", () => {
311
+ test("builds the raw-audio parameter set", () => {
312
+ const query = buildFluxQueryParams({
313
+ model: "flux-general-en",
314
+ encoding: "linear16",
315
+ sampleRate: 16_000,
316
+ eotThreshold: 0.7,
317
+ eotTimeoutMs: 5_000,
318
+ });
319
+ const params = new URLSearchParams(query);
320
+ expect(params.get("model")).toBe("flux-general-en");
321
+ expect(params.get("encoding")).toBe("linear16");
322
+ expect(params.get("sample_rate")).toBe("16000");
323
+ expect(params.get("eot_threshold")).toBe("0.7");
324
+ expect(params.get("eot_timeout_ms")).toBe("5000");
325
+ });
326
+
327
+ test("omits eager_eot_threshold when unset", () => {
328
+ const query = buildFluxQueryParams({
329
+ model: "flux-general-en",
330
+ eotThreshold: 0.7,
331
+ eotTimeoutMs: 5_000,
332
+ });
333
+ expect(query).not.toContain("eager_eot_threshold");
334
+ expect(new URLSearchParams(query).has("eager_eot_threshold")).toBe(false);
335
+ });
336
+
337
+ test("passes eager_eot_threshold through when set", () => {
338
+ const params = new URLSearchParams(
339
+ buildFluxQueryParams({
340
+ model: "flux-general-en",
341
+ eagerEotThreshold: 0.5,
342
+ }),
343
+ );
344
+ expect(params.get("eager_eot_threshold")).toBe("0.5");
345
+ });
346
+
347
+ test("omits encoding and sample_rate for containerized audio", () => {
348
+ const params = new URLSearchParams(
349
+ buildFluxQueryParams({ model: "flux-general-en" }),
350
+ );
351
+ expect(params.has("encoding")).toBe(false);
352
+ expect(params.has("sample_rate")).toBe(false);
353
+ });
354
+
355
+ test("clamps thresholds and the timeout to the ranges Deepgram accepts", () => {
356
+ const low = new URLSearchParams(
357
+ buildFluxQueryParams({
358
+ model: "flux-general-en",
359
+ eotThreshold: 0.1,
360
+ eagerEotThreshold: 0.05,
361
+ eotTimeoutMs: 10,
362
+ }),
363
+ );
364
+ expect(low.get("eot_threshold")).toBe("0.5");
365
+ expect(low.get("eager_eot_threshold")).toBe("0.3");
366
+ expect(low.get("eot_timeout_ms")).toBe("500");
367
+
368
+ const high = new URLSearchParams(
369
+ buildFluxQueryParams({
370
+ model: "flux-general-en",
371
+ eotThreshold: 5,
372
+ eagerEotThreshold: 5,
373
+ eotTimeoutMs: 999_999,
374
+ }),
375
+ );
376
+ expect(high.get("eot_threshold")).toBe("0.9");
377
+ expect(high.get("eager_eot_threshold")).toBe("0.9");
378
+ expect(high.get("eot_timeout_ms")).toBe("60000");
379
+ });
380
+
381
+ test("clamps eager_eot_threshold down to eot_threshold", () => {
382
+ const params = new URLSearchParams(
383
+ buildFluxQueryParams({
384
+ model: "flux-general-en",
385
+ eotThreshold: 0.5,
386
+ eagerEotThreshold: 0.9,
387
+ }),
388
+ );
389
+ // Deepgram rejects the handshake when eager exceeds the final threshold.
390
+ expect(Number(params.get("eager_eot_threshold"))).toBeLessThanOrEqual(
391
+ Number(params.get("eot_threshold")),
392
+ );
393
+ expect(params.get("eager_eot_threshold")).toBe("0.5");
394
+ expect(params.get("eot_threshold")).toBe("0.5");
395
+ });
396
+
397
+ test("clamps eager_eot_threshold against Deepgram's default when eot_threshold is unset", () => {
398
+ const params = new URLSearchParams(
399
+ buildFluxQueryParams({
400
+ model: "flux-general-en",
401
+ eagerEotThreshold: 0.9,
402
+ }),
403
+ );
404
+ // No eot_threshold is sent, so the server default of 0.7 is in force.
405
+ expect(params.has("eot_threshold")).toBe(false);
406
+ expect(params.get("eager_eot_threshold")).toBe("0.7");
407
+ });
408
+
409
+ test("leaves an already-valid eager/eot pair untouched", () => {
410
+ const params = new URLSearchParams(
411
+ buildFluxQueryParams({
412
+ model: "flux-general-en",
413
+ eotThreshold: 0.9,
414
+ eagerEotThreshold: 0.6,
415
+ }),
416
+ );
417
+ expect(params.get("eot_threshold")).toBe("0.9");
418
+ expect(params.get("eager_eot_threshold")).toBe("0.6");
419
+ });
420
+
421
+ test("the eager clamp never invents an eager_eot_threshold", () => {
422
+ // Omitting the parameter is what disables eager turn-end speculation, so
423
+ // the cross-threshold clamp must not fire when it is unset.
424
+ for (const eotThreshold of [undefined, 0.5, 0.9]) {
425
+ const query = buildFluxQueryParams({
426
+ model: "flux-general-en",
427
+ ...(eotThreshold !== undefined ? { eotThreshold } : {}),
428
+ });
429
+ expect(query).not.toContain("eager_eot_threshold");
430
+ expect(new URLSearchParams(query).has("eager_eot_threshold")).toBe(false);
431
+ }
432
+ });
433
+ });