@vellumai/assistant 0.11.6 → 0.11.7-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 (478) hide show
  1. package/ARCHITECTURE.md +31 -19
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +3 -3
  4. package/docs/flux-turn-detection-spike.md +8 -7
  5. package/docs/runbook-trusted-contacts.md +14 -0
  6. package/docs/stt-provider-onboarding.md +8 -4
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
  9. package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
  10. package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
  11. package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
  12. package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
  13. package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
  14. package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
  15. package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  19. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  20. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  21. package/node_modules/@vellumai/ces-client/src/http-credentials.ts +161 -0
  22. package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
  23. package/node_modules/@vellumai/environments/package.json +2 -1
  24. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  25. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  27. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  28. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  29. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  30. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  32. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  33. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  34. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  35. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  36. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  37. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  38. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  39. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  40. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  41. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  42. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  43. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  44. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  45. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  46. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  47. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  48. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  49. package/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  50. package/openapi.yaml +419 -55
  51. package/package.json +4 -1
  52. package/scripts/sync-llm-catalog.ts +6 -0
  53. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  54. package/src/__tests__/approval-card-edit.test.ts +153 -0
  55. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  56. package/src/__tests__/available-skills.test.ts +54 -0
  57. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  58. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  59. package/src/__tests__/channel-policy.test.ts +1 -3
  60. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  61. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  62. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  63. package/src/__tests__/config-schema.test.ts +2 -3
  64. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  65. package/src/__tests__/conversation-attachments.test.ts +0 -1
  66. package/src/__tests__/conversation-error.test.ts +44 -0
  67. package/src/__tests__/conversation-evictor.test.ts +19 -0
  68. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  69. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  70. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  71. package/src/__tests__/conversation-slash.test.ts +9 -0
  72. package/src/__tests__/conversation-title-service.test.ts +97 -0
  73. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  74. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  75. package/src/__tests__/credential-broker.test.ts +43 -420
  76. package/src/__tests__/credential-record-write-through.test.ts +78 -0
  77. package/src/__tests__/credential-routes.test.ts +17 -1
  78. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  79. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  80. package/src/__tests__/delete-propagation.test.ts +92 -2
  81. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  82. package/src/__tests__/discord-channel-config.test.ts +237 -0
  83. package/src/__tests__/edit-propagation.test.ts +42 -2
  84. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  85. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  86. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  87. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  88. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  89. package/src/__tests__/image-conversion.test.ts +38 -4
  90. package/src/__tests__/install-skill-routing.test.ts +25 -0
  91. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  92. package/src/__tests__/llm-resolver.test.ts +74 -0
  93. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  94. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  95. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  96. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  97. package/src/__tests__/mcp-cli.test.ts +23 -1
  98. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  99. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  100. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  101. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  102. package/src/__tests__/oauth-connect-orchestrator.test.ts +95 -0
  103. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  104. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  105. package/src/__tests__/platform.test.ts +19 -1
  106. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  107. package/src/__tests__/provider-catalog-visibility.test.ts +17 -0
  108. package/src/__tests__/provider-platform-proxy-integration.test.ts +8 -1
  109. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  110. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  111. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  112. package/src/__tests__/schedule-retry.test.ts +183 -4
  113. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  115. package/src/__tests__/skill-load-tool.test.ts +34 -0
  116. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  117. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  118. package/src/__tests__/skills.test.ts +25 -0
  119. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  120. package/src/__tests__/terminal-tools.test.ts +2 -0
  121. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  122. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  123. package/src/__tests__/tool-audit.test.ts +111 -40
  124. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  125. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  126. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  127. package/src/__tests__/voice-config-update.test.ts +43 -0
  128. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  129. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  130. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  131. package/src/agent/loop.ts +31 -2
  132. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  133. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  134. package/src/avatar/avatar-manifest.ts +48 -100
  135. package/src/avatar/avatar-store.ts +7 -7
  136. package/src/avatar/ensure-raster.ts +146 -0
  137. package/src/avatar/resvg-lazy.test.ts +0 -1
  138. package/src/avatar/traits-png-sync.ts +9 -10
  139. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  140. package/src/backup/__tests__/paths.test.ts +6 -2
  141. package/src/backup/paths.ts +6 -4
  142. package/src/bundler/app-compiler.ts +2 -2
  143. package/src/bundler/compiler-tools.test.ts +36 -0
  144. package/src/bundler/compiler-tools.ts +54 -18
  145. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  146. package/src/calls/__tests__/voice-session-bridge.test.ts +57 -0
  147. package/src/calls/media-stream-stt-session.ts +2 -1
  148. package/src/calls/telephony-synthesis-language.ts +13 -5
  149. package/src/calls/voice-session-bridge.ts +19 -4
  150. package/src/channels/config.ts +23 -12
  151. package/src/cli/AGENTS.md +3 -1
  152. package/src/cli/commands/backup.help.ts +10 -13
  153. package/src/cli/commands/contacts.help.ts +16 -4
  154. package/src/cli/commands/contacts.ts +5 -1
  155. package/src/cli/commands/credentials.help.ts +1 -1
  156. package/src/cli/commands/inference-providers.ts +18 -53
  157. package/src/cli/commands/mcp.help.ts +6 -5
  158. package/src/cli/commands/mcp.ts +1 -1
  159. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  160. package/src/cli/commands/stt.help.ts +2 -2
  161. package/src/config/__tests__/backup-schema.test.ts +1 -1
  162. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  163. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  164. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  165. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  166. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  167. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  168. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  169. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  170. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  171. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  172. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  173. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  174. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  175. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  176. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  177. package/src/config/default-profile-catalog.ts +195 -10
  178. package/src/config/default-profile-names.ts +41 -0
  179. package/src/config/feature-flag-registry.json +34 -2
  180. package/src/config/inference-profile-validation.ts +11 -2
  181. package/src/config/llm-resolver.ts +25 -0
  182. package/src/config/loader.ts +3 -0
  183. package/src/config/managed-speech-defaults.ts +160 -10
  184. package/src/config/schema.ts +2 -0
  185. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  186. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  187. package/src/config/schemas/backup.ts +1 -1
  188. package/src/config/schemas/channels.ts +32 -0
  189. package/src/config/schemas/live-voice.ts +13 -4
  190. package/src/config/schemas/llm.ts +263 -15
  191. package/src/config/schemas/mcp.ts +10 -2
  192. package/src/config/schemas/memory-retrospective.ts +1 -19
  193. package/src/config/schemas/services.ts +24 -0
  194. package/src/config/schemas/stt.ts +128 -5
  195. package/src/config/seed-inference-profiles.ts +32 -3
  196. package/src/config/skill-state.ts +5 -0
  197. package/src/config/skills.ts +21 -0
  198. package/src/contacts/notify-contacts-changed.ts +5 -2
  199. package/src/context/compactor.ts +74 -1
  200. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  201. package/src/daemon/config-watcher.ts +2 -1
  202. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  203. package/src/daemon/conversation-agent-loop.ts +86 -10
  204. package/src/daemon/conversation-error.ts +25 -0
  205. package/src/daemon/conversation-evictor.ts +2 -4
  206. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  207. package/src/daemon/conversation-skill-tools.ts +5 -1
  208. package/src/daemon/conversation-slash.ts +2 -2
  209. package/src/daemon/conversation-store.ts +8 -1
  210. package/src/daemon/conversation-tool-setup.ts +37 -0
  211. package/src/daemon/conversation.ts +0 -4
  212. package/src/daemon/handlers/channel-config-result.ts +30 -0
  213. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  214. package/src/daemon/handlers/config-ingress.ts +9 -0
  215. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  216. package/src/daemon/handlers/config-telegram.ts +2 -5
  217. package/src/daemon/handlers/conversations.ts +0 -2
  218. package/src/daemon/handlers/skills.ts +25 -7
  219. package/src/daemon/lifecycle.ts +3 -1
  220. package/src/daemon/message-types/messages.ts +0 -2
  221. package/src/daemon/tool-setup-types.ts +5 -3
  222. package/src/daemon/trust-context-types.test.ts +33 -0
  223. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  224. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  225. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  226. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  227. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  228. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +64 -0
  229. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  230. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  231. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  232. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  233. package/src/live-voice/live-voice-session-manager.ts +39 -2
  234. package/src/live-voice/live-voice-session.ts +224 -9
  235. package/src/live-voice/protocol.ts +187 -1
  236. package/src/messaging/providers/discord/api.ts +2 -1
  237. package/src/messaging/providers/discord/withdraw.ts +62 -0
  238. package/src/messaging/read-provider-metadata.ts +32 -0
  239. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  240. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  241. package/src/monitoring/db-integrity-sample.ts +1 -0
  242. package/src/monitoring/file-descriptors.ts +39 -6
  243. package/src/monitoring/process-memory.ts +37 -5
  244. package/src/monitoring/resource-sampler.ts +25 -8
  245. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  246. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  247. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  248. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  249. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  250. package/src/notifications/activity-failed-dedupe.ts +95 -0
  251. package/src/notifications/adapters/discord.ts +132 -0
  252. package/src/notifications/adapters/shared.ts +16 -0
  253. package/src/notifications/adapters/slack.ts +6 -1
  254. package/src/notifications/adapters/telegram.ts +5 -8
  255. package/src/notifications/approval-card-data.ts +94 -1
  256. package/src/notifications/background-failure-signal.ts +116 -0
  257. package/src/notifications/broadcaster.ts +3 -9
  258. package/src/notifications/copy-composer.ts +41 -8
  259. package/src/notifications/decision-engine.ts +55 -0
  260. package/src/notifications/destination-resolver.ts +45 -5
  261. package/src/notifications/emit-signal.ts +19 -5
  262. package/src/notifications/guardian-question-mode.ts +57 -0
  263. package/src/notifications/types.ts +2 -8
  264. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  265. package/src/oauth/connect-orchestrator.ts +16 -0
  266. package/src/oauth/seed-providers.ts +262 -0
  267. package/src/permissions/checker.test.ts +21 -0
  268. package/src/permissions/checker.ts +6 -0
  269. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  270. package/src/permissions/gateway-threshold-reader.ts +144 -10
  271. package/src/permissions/prompter.ts +0 -6
  272. package/src/permissions/types.ts +5 -4
  273. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  274. package/src/persistence/bulk-write-gate.ts +9 -10
  275. package/src/persistence/conversation-crud.ts +88 -134
  276. package/src/persistence/conversation-title-service.ts +43 -29
  277. package/src/persistence/embeddings/embedding-local.ts +14 -85
  278. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  279. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  280. package/src/persistence/lifecycle-events-store.ts +75 -5
  281. package/src/platform/platform-patch-queue.test.ts +368 -0
  282. package/src/platform/platform-patch-queue.ts +190 -0
  283. package/src/platform/sync-avatar.test.ts +527 -0
  284. package/src/platform/sync-avatar.ts +209 -0
  285. package/src/platform/sync-identity.test.ts +75 -0
  286. package/src/platform/sync-identity.ts +17 -110
  287. package/src/plugin-api/model-profiles.test.ts +2 -2
  288. package/src/plugin-api/model-profiles.ts +7 -7
  289. package/src/plugin-api/transcription-session.ts +1 -1
  290. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  291. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  292. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  293. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  294. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  295. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  296. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  297. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  298. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  299. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  300. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  301. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  302. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  303. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  304. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  305. package/src/plugins/defaults/memory/worker.ts +7 -0
  306. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  307. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  308. package/src/providers/__tests__/vellum-connection-routing.test.ts +21 -3
  309. package/src/providers/connection-resolution.ts +2 -17
  310. package/src/providers/fallback-breaker.ts +389 -0
  311. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  312. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  313. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  314. package/src/providers/inference/adapter-factory.ts +246 -3
  315. package/src/providers/inference/auth.ts +15 -14
  316. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  317. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  318. package/src/providers/inference/resolve-auth.ts +1 -1
  319. package/src/providers/media-resolve.ts +2 -3
  320. package/src/providers/model-catalog.ts +100 -0
  321. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  322. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  323. package/src/providers/openai/api-error-normalization.ts +13 -0
  324. package/src/providers/openai/chat-completions-provider.ts +84 -0
  325. package/src/providers/opencode/client.test.ts +150 -0
  326. package/src/providers/opencode/client.ts +61 -0
  327. package/src/providers/platform-proxy/constants.ts +5 -0
  328. package/src/providers/provider-resolvability.ts +20 -0
  329. package/src/providers/provider-secret-catalog.ts +3 -2
  330. package/src/providers/retry.ts +956 -68
  331. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  332. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  333. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  334. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  335. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  336. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  337. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  338. package/src/providers/speech-to-text/resolve.ts +132 -24
  339. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  340. package/src/providers/types.ts +39 -0
  341. package/src/providers/usage-tracking.ts +12 -1
  342. package/src/providers/vellum/client.ts +29 -0
  343. package/src/providers/vellum-model-routing.test.ts +2 -0
  344. package/src/providers/vellum-model-routing.ts +3 -3
  345. package/src/runtime/AGENTS.md +1 -2
  346. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  347. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  348. package/src/runtime/agent-wake.ts +44 -5
  349. package/src/runtime/background-job-runner.ts +68 -50
  350. package/src/runtime/channel-readiness-service.ts +0 -39
  351. package/src/runtime/http-server.ts +12 -4
  352. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  353. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  354. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  355. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  356. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  357. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  358. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  359. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  360. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  361. package/src/runtime/routes/approval-routes.ts +0 -12
  362. package/src/runtime/routes/avatar-routes.ts +17 -30
  363. package/src/runtime/routes/backup-routes.ts +2 -2
  364. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  365. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  366. package/src/runtime/routes/contact-routes.ts +20 -15
  367. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  368. package/src/runtime/routes/conversation-routes.ts +2 -2
  369. package/src/runtime/routes/credential-routes.ts +2 -1
  370. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  371. package/src/runtime/routes/inbound-message-handler.ts +47 -48
  372. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  373. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  374. package/src/runtime/routes/inbound-stages/edit-intercept.ts +20 -2
  375. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  376. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  377. package/src/runtime/routes/index.ts +2 -0
  378. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  379. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  380. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  381. package/src/runtime/routes/inference-send-routes.ts +2 -2
  382. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  383. package/src/runtime/routes/integrations/discord.ts +119 -0
  384. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  385. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  386. package/src/runtime/routes/migration-routes.ts +3 -1
  387. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  388. package/src/runtime/routes/secret-routes.ts +7 -0
  389. package/src/runtime/routes/stt-routes.ts +38 -6
  390. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  391. package/src/runtime/routes/watch-routes.ts +9 -5
  392. package/src/runtime/sync/resource-sync-events.ts +2 -0
  393. package/src/runtime/trust-verdict-consumer.ts +3 -3
  394. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  395. package/src/schedule/retry-policy.ts +8 -8
  396. package/src/schedule/run-script.ts +14 -23
  397. package/src/schedule/schedule-recovery.ts +0 -1
  398. package/src/schedule/scheduler.ts +89 -51
  399. package/src/schedule/worker.ts +57 -0
  400. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  401. package/src/security/ces-rpc-record-backend.ts +123 -0
  402. package/src/security/oauth2.ts +7 -0
  403. package/src/security/openai-device-auth.ts +364 -0
  404. package/src/security/secure-keys.ts +15 -0
  405. package/src/skills/available-skills.ts +10 -1
  406. package/src/skills/catalog-install.ts +19 -1
  407. package/src/skills/inline-command-runner.ts +6 -2
  408. package/src/skills/platform-compatibility.test.ts +49 -0
  409. package/src/skills/platform-compatibility.ts +65 -0
  410. package/src/skills/tool-manifest.ts +20 -0
  411. package/src/stt/__tests__/roles.test.ts +152 -0
  412. package/src/stt/daemon-batch-transcriber.ts +4 -1
  413. package/src/stt/roles.ts +180 -0
  414. package/src/stt/stt-stream-session.ts +3 -2
  415. package/src/stt/types.ts +33 -3
  416. package/src/subagent/manager.ts +12 -0
  417. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  418. package/src/telemetry/live-voice-funnel.ts +12 -2
  419. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  420. package/src/telemetry/tool-audit.ts +83 -30
  421. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  422. package/src/telemetry/turn-outcome.ts +75 -4
  423. package/src/tools/client-os.ts +115 -0
  424. package/src/tools/computer-use/definitions.ts +11 -7
  425. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  426. package/src/tools/credentials/broker-types.ts +0 -71
  427. package/src/tools/credentials/broker.ts +6 -201
  428. package/src/tools/credentials/metadata-store.ts +65 -16
  429. package/src/tools/credentials/store.ts +2 -0
  430. package/src/tools/executor.ts +2 -8
  431. package/src/tools/host-shell.test.ts +75 -0
  432. package/src/tools/host-shell.ts +24 -0
  433. package/src/tools/host-terminal/host-shell.ts +15 -44
  434. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  435. package/src/tools/permission-checker.ts +73 -21
  436. package/src/tools/policy-context.ts +1 -0
  437. package/src/tools/shared/filesystem/image-read.ts +1 -1
  438. package/src/tools/skills/find-similar-skills.ts +10 -2
  439. package/src/tools/skills/load.ts +41 -6
  440. package/src/tools/skills/sandbox-runner.ts +8 -16
  441. package/src/tools/skills/skill-tool-factory.ts +1 -0
  442. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  443. package/src/tools/terminal/safe-env.ts +39 -9
  444. package/src/tools/terminal/shell.ts +12 -31
  445. package/src/tools/tool-approval-handler.ts +64 -10
  446. package/src/tools/tool-types.ts +10 -0
  447. package/src/tools/types.ts +19 -3
  448. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  449. package/src/tools/workflows/manage-workflows.ts +2 -2
  450. package/src/tools/workflows/run-workflow.test.ts +2 -1
  451. package/src/util/__tests__/file-use.test.ts +61 -0
  452. package/src/util/__tests__/process-table.test.ts +66 -0
  453. package/src/util/__tests__/process-tree.test.ts +46 -0
  454. package/src/util/errors.ts +1 -0
  455. package/src/util/file-use.ts +91 -0
  456. package/src/util/host-process.test.ts +17 -0
  457. package/src/util/host-process.ts +75 -0
  458. package/src/util/image-conversion.ts +39 -7
  459. package/src/util/platform.ts +32 -13
  460. package/src/util/process-table.ts +249 -0
  461. package/src/util/process-tree.ts +85 -129
  462. package/src/util/provider-error-patterns.ts +20 -0
  463. package/src/util/spawn.ts +9 -2
  464. package/src/watch/__tests__/watch-retro.test.ts +48 -1
  465. package/src/watch/watch-retro.ts +46 -4
  466. package/src/workspace/git-service.ts +4 -14
  467. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  468. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  469. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  470. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  471. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  472. package/src/workspace/migrations/registry.ts +8 -0
  473. package/src/__tests__/permission-types.test.ts +0 -17
  474. package/src/channels/gateway-discord-admission.ts +0 -32
  475. package/src/persistence/fork-message-copy.ts +0 -239
  476. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  477. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  478. package/src/security/oauth2-device-code.ts +0 -312
@@ -0,0 +1,430 @@
1
+ /**
2
+ * Managed backup-profile dispatch against a mocked platform proxy.
3
+ *
4
+ * The primary test path includes `CallSiteRoutingProvider`, connection
5
+ * selection, the real adapter factory, retry escalation, and provider clients.
6
+ * Only the remote platform proxy is replaced with a local HTTP server.
7
+ */
8
+
9
+ import {
10
+ afterAll,
11
+ afterEach,
12
+ beforeAll,
13
+ beforeEach,
14
+ describe,
15
+ expect,
16
+ test,
17
+ } from "bun:test";
18
+
19
+ import { CallSiteRoutingProvider } from "../providers/call-site-routing.js";
20
+ import {
21
+ resetFallbackBreaker,
22
+ shouldSkipPrimary,
23
+ } from "../providers/fallback-breaker.js";
24
+ import { createAdapterFromConnection } from "../providers/inference/adapter-factory.js";
25
+ import type {
26
+ ProviderConnection,
27
+ ResolvedAuth,
28
+ } from "../providers/inference/auth.js";
29
+ import type {
30
+ Message,
31
+ Provider,
32
+ ProviderResponse,
33
+ } from "../providers/types.js";
34
+ import { getManagedUpstream } from "../providers/vellum-model-routing.js";
35
+ import { credentialKey } from "../security/credential-key.js";
36
+ import { setSecureKeyAsync } from "../security/secure-keys.js";
37
+ import { setConfig } from "./helpers/set-config.js";
38
+
39
+ interface UpstreamRequest {
40
+ path: string;
41
+ headers: Record<string, string>;
42
+ body: Record<string, unknown>;
43
+ }
44
+
45
+ const requests: UpstreamRequest[] = [];
46
+ let anthropicMode: "serve" | "retired" | "unavailable" = "serve";
47
+
48
+ const ANTHROPIC_PATH = "/v1/runtime-proxy/anthropic";
49
+ const OPENAI_PATH = "/v1/runtime-proxy/openai";
50
+
51
+ function anthropicCompletion(): Response {
52
+ const events = [
53
+ {
54
+ type: "message_start",
55
+ message: {
56
+ id: "msg_fallback_01",
57
+ type: "message",
58
+ role: "assistant",
59
+ content: [],
60
+ model: "claude-opus-5",
61
+ stop_reason: null,
62
+ stop_sequence: null,
63
+ usage: { input_tokens: 7, output_tokens: 1 },
64
+ },
65
+ },
66
+ {
67
+ type: "content_block_start",
68
+ index: 0,
69
+ content_block: { type: "text", text: "" },
70
+ },
71
+ {
72
+ type: "content_block_delta",
73
+ index: 0,
74
+ delta: { type: "text_delta", text: "served by the backup" },
75
+ },
76
+ { type: "content_block_stop", index: 0 },
77
+ {
78
+ type: "message_delta",
79
+ delta: { stop_reason: "end_turn", stop_sequence: null },
80
+ usage: { output_tokens: 4 },
81
+ },
82
+ { type: "message_stop" },
83
+ ];
84
+ const encoder = new TextEncoder();
85
+ const stream = new ReadableStream({
86
+ start(controller) {
87
+ for (const event of events) {
88
+ controller.enqueue(
89
+ encoder.encode(
90
+ `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`,
91
+ ),
92
+ );
93
+ }
94
+ controller.close();
95
+ },
96
+ });
97
+ return new Response(stream, {
98
+ headers: { "Content-Type": "text/event-stream" },
99
+ });
100
+ }
101
+
102
+ function upstreamUnavailable(): Response {
103
+ return new Response(
104
+ JSON.stringify({ error: { message: "upstream unavailable" } }),
105
+ {
106
+ status: 503,
107
+ headers: { "content-type": "application/json", "retry-after": "0" },
108
+ },
109
+ );
110
+ }
111
+
112
+ function modelRetired(): Response {
113
+ return new Response(
114
+ JSON.stringify({
115
+ type: "error",
116
+ error: { type: "not_found_error", message: "model: unknown model id" },
117
+ }),
118
+ { status: 404, headers: { "content-type": "application/json" } },
119
+ );
120
+ }
121
+
122
+ let server: ReturnType<typeof Bun.serve> | undefined;
123
+
124
+ const MESSAGES: Message[] = [
125
+ { role: "user", content: [{ type: "text", text: "hi" }] },
126
+ ];
127
+
128
+ const ASSISTANT_API_KEY = credentialKey("vellum", "assistant_api_key");
129
+ const BYOK_CREDENTIAL = credentialKey("anthropic", "api_key");
130
+
131
+ const vellumConnection = {
132
+ name: "vellum",
133
+ provider: "vellum",
134
+ auth: { type: "platform" },
135
+ label: "Vellum",
136
+ baseUrl: null,
137
+ models: null,
138
+ } as unknown as ProviderConnection;
139
+
140
+ const byokConnection = {
141
+ name: "anthropic-personal",
142
+ provider: "anthropic",
143
+ auth: { type: "api_key", credential: BYOK_CREDENTIAL },
144
+ label: "Anthropic (personal)",
145
+ baseUrl: null,
146
+ models: null,
147
+ } as unknown as ProviderConnection;
148
+
149
+ function managedAuth(proxyPath: string): ResolvedAuth {
150
+ if (server === undefined) {
151
+ throw new Error("test proxy is not running");
152
+ }
153
+ return {
154
+ kind: "header",
155
+ headers: { Authorization: "Bearer test-assistant-key" },
156
+ baseUrl: `http://127.0.0.1:${server.port}${proxyPath}`,
157
+ };
158
+ }
159
+
160
+ function requestsTo(proxyPath: string): UpstreamRequest[] {
161
+ return requests.filter((request) => request.path.startsWith(proxyPath));
162
+ }
163
+
164
+ function createManagedAdapter(upstream: string, model: string): Provider {
165
+ const proxyPath =
166
+ upstream === "openai"
167
+ ? OPENAI_PATH
168
+ : upstream === "anthropic"
169
+ ? ANTHROPIC_PATH
170
+ : null;
171
+ if (proxyPath === null) {
172
+ throw new Error(`unsupported test upstream: ${upstream}`);
173
+ }
174
+ const provider = createAdapterFromConnection(
175
+ vellumConnection,
176
+ managedAuth(proxyPath),
177
+ {
178
+ model,
179
+ provider: upstream,
180
+ streamTimeoutMs: 30_000,
181
+ },
182
+ );
183
+ if (provider === null) {
184
+ throw new Error(`failed to create ${upstream} test adapter`);
185
+ }
186
+ return provider;
187
+ }
188
+
189
+ /**
190
+ * Construct the production wrapper order that calls use. The connection
191
+ * resolver builds the managed adapter for the provider/model selected by the
192
+ * call-site router, so fallback eligibility sees the original call-site
193
+ * options while the primary transport matches the resolved route.
194
+ */
195
+ function createRoutedManagedProvider(): Provider {
196
+ const defaultProvider = createManagedAdapter("openai", "gpt-5.6-sol");
197
+ return new CallSiteRoutingProvider(
198
+ defaultProvider,
199
+ async (connectionName, expectedProvider, model) => {
200
+ if (connectionName !== "vellum" || model === undefined) {
201
+ return null;
202
+ }
203
+ const upstream =
204
+ expectedProvider === "vellum"
205
+ ? getManagedUpstream(model)
206
+ : expectedProvider;
207
+ return upstream === undefined || upstream === null
208
+ ? null
209
+ : createManagedAdapter(upstream, model);
210
+ },
211
+ defaultProvider.routeAttribution,
212
+ );
213
+ }
214
+
215
+ async function captureError(promise: Promise<unknown>): Promise<unknown> {
216
+ try {
217
+ await promise;
218
+ } catch (error) {
219
+ return error;
220
+ }
221
+ throw new Error("expected the promise to reject");
222
+ }
223
+
224
+ beforeAll(async () => {
225
+ server = Bun.serve({
226
+ hostname: "127.0.0.1",
227
+ port: 0,
228
+ async fetch(req) {
229
+ const url = new URL(req.url);
230
+ let body: Record<string, unknown> = {};
231
+ try {
232
+ body = (await req.json()) as Record<string, unknown>;
233
+ } catch {
234
+ // The request path is sufficient for non-JSON probes.
235
+ }
236
+ requests.push({
237
+ path: url.pathname,
238
+ headers: Object.fromEntries(req.headers.entries()),
239
+ body,
240
+ });
241
+ if (url.pathname.startsWith(ANTHROPIC_PATH)) {
242
+ if (anthropicMode === "retired") {
243
+ return modelRetired();
244
+ }
245
+ return anthropicMode === "unavailable"
246
+ ? upstreamUnavailable()
247
+ : anthropicCompletion();
248
+ }
249
+ return upstreamUnavailable();
250
+ },
251
+ });
252
+ setConfig("platform", { baseUrl: `http://127.0.0.1:${server.port}` });
253
+ await setSecureKeyAsync(ASSISTANT_API_KEY, "test-assistant-key");
254
+ await setSecureKeyAsync(BYOK_CREDENTIAL, "test-byok-key");
255
+ });
256
+
257
+ afterAll(() => {
258
+ server?.stop(true);
259
+ });
260
+
261
+ beforeEach(() => {
262
+ resetFallbackBreaker();
263
+ });
264
+
265
+ afterEach(() => {
266
+ requests.length = 0;
267
+ anthropicMode = "serve";
268
+ });
269
+
270
+ describe("managed fallback dispatch", () => {
271
+ test("a managed default outage is served through the routed backup profile", async () => {
272
+ setConfig("llm", { activeProfile: "quality-optimized" });
273
+ const provider = createRoutedManagedProvider();
274
+
275
+ const response: ProviderResponse = await provider.sendMessage(MESSAGES, {
276
+ config: { callSite: "mainAgent" },
277
+ });
278
+
279
+ expect(requestsTo(OPENAI_PATH).length).toBeGreaterThan(1);
280
+ const backup = requestsTo(ANTHROPIC_PATH);
281
+ expect(backup.length).toBe(1);
282
+ expect(backup[0].body.model).toBe("claude-opus-5");
283
+ expect(backup[0].body.max_tokens).toBe(32000);
284
+ expect(backup[0].body.thinking).toMatchObject({ type: "adaptive" });
285
+ expect(backup[0].headers["x-vellum-inference-profile"]).toBe(
286
+ "quality-optimized-backup",
287
+ );
288
+ expect(response.actualInferenceProfile).toBe("quality-optimized-backup");
289
+ expect(response.actualProvider).toBe("anthropic");
290
+ });
291
+
292
+ test.each([
293
+ ["model", "gpt-5.6-sol"],
294
+ ["provider", "openai"],
295
+ ] as const)(
296
+ "a persisted call-site %s pin never falls back",
297
+ async (field, value) => {
298
+ setConfig("llm", {
299
+ activeProfile: "quality-optimized",
300
+ callSites: { mainAgent: { [field]: value } },
301
+ });
302
+ const provider = createRoutedManagedProvider();
303
+
304
+ const error = await captureError(
305
+ provider.sendMessage(MESSAGES, { config: { callSite: "mainAgent" } }),
306
+ );
307
+
308
+ expect((error as { provider?: string }).provider).toBe("openai");
309
+ expect(requestsTo(OPENAI_PATH).length).toBeGreaterThan(1);
310
+ expect(requestsTo(ANTHROPIC_PATH)).toHaveLength(0);
311
+ expect(shouldSkipPrimary({ upstream: "openai" })).toBe(false);
312
+ },
313
+ );
314
+
315
+ test("a BYOK connection never installs automatic fallback routing", async () => {
316
+ setConfig("llm", {
317
+ profiles: {
318
+ "byok-primary": {
319
+ source: "user",
320
+ provider: "anthropic",
321
+ model: "claude-opus-5",
322
+ maxTokens: 1024,
323
+ },
324
+ },
325
+ activeProfile: "byok-primary",
326
+ });
327
+ anthropicMode = "retired";
328
+ const provider = createAdapterFromConnection(
329
+ byokConnection,
330
+ {
331
+ kind: "header",
332
+ headers: { Authorization: "Bearer test-byok-key" },
333
+ baseUrl: `http://127.0.0.1:${server?.port}${ANTHROPIC_PATH}`,
334
+ },
335
+ { model: "claude-opus-5", streamTimeoutMs: 30_000 },
336
+ );
337
+ expect(provider).not.toBeNull();
338
+
339
+ const error = await captureError(
340
+ provider!.sendMessage(MESSAGES, { config: { callSite: "mainAgent" } }),
341
+ );
342
+
343
+ expect((error as { statusCode?: number }).statusCode).toBe(404);
344
+ expect(new Set(requests.map((request) => request.body.model))).toEqual(
345
+ new Set(["claude-opus-5"]),
346
+ );
347
+ });
348
+
349
+ test("a custom managed profile has no automatic fallback contract", async () => {
350
+ setConfig("llm", {
351
+ profiles: {
352
+ custom: {
353
+ source: "user",
354
+ provider: "vellum",
355
+ model: "claude-opus-5",
356
+ maxTokens: 1024,
357
+ },
358
+ },
359
+ activeProfile: "custom",
360
+ });
361
+ anthropicMode = "retired";
362
+ const provider = createRoutedManagedProvider();
363
+
364
+ const error = await captureError(
365
+ provider.sendMessage(MESSAGES, { config: { callSite: "mainAgent" } }),
366
+ );
367
+
368
+ expect((error as { statusCode?: number }).statusCode).toBe(404);
369
+ expect(new Set(requests.map((request) => request.body.model))).toEqual(
370
+ new Set(["claude-opus-5"]),
371
+ );
372
+ });
373
+
374
+ test("a user shadow cannot change the code-owned backup route", async () => {
375
+ setConfig("llm", {
376
+ profiles: {
377
+ "quality-optimized-backup": {
378
+ source: "user",
379
+ provider: "missing-connection-entry",
380
+ model: "custom-model",
381
+ },
382
+ },
383
+ activeProfile: "quality-optimized",
384
+ });
385
+ const provider = createRoutedManagedProvider();
386
+
387
+ const response = await provider.sendMessage(MESSAGES, {
388
+ config: { callSite: "mainAgent" },
389
+ });
390
+
391
+ const backup = requestsTo(ANTHROPIC_PATH);
392
+ expect(backup).toHaveLength(1);
393
+ expect(backup[0].body.model).toBe("claude-opus-5");
394
+ expect(response.actualInferenceProfile).toBe("quality-optimized-backup");
395
+ });
396
+
397
+ test("a failed backup preserves the primary error and does not count as served", async () => {
398
+ setConfig("llm", { activeProfile: "quality-optimized" });
399
+ anthropicMode = "unavailable";
400
+ const provider = createRoutedManagedProvider();
401
+
402
+ const error = await captureError(
403
+ provider.sendMessage(MESSAGES, { config: { callSite: "mainAgent" } }),
404
+ );
405
+
406
+ expect((error as { provider?: string }).provider).toBe("openai");
407
+ expect(requestsTo(OPENAI_PATH).length).toBeGreaterThan(1);
408
+ expect(requestsTo(ANTHROPIC_PATH).length).toBeGreaterThan(0);
409
+ expect(shouldSkipPrimary({ upstream: "openai" })).toBe(false);
410
+ });
411
+
412
+ test("a served backup opens the breaker for the routed primary", async () => {
413
+ setConfig("llm", { activeProfile: "quality-optimized" });
414
+ const provider = createRoutedManagedProvider();
415
+
416
+ await provider.sendMessage(MESSAGES, {
417
+ config: { callSite: "mainAgent" },
418
+ });
419
+ expect(shouldSkipPrimary({ upstream: "openai" })).toBe(true);
420
+
421
+ requests.length = 0;
422
+ const response = await provider.sendMessage(MESSAGES, {
423
+ config: { callSite: "mainAgent" },
424
+ });
425
+
426
+ expect(requestsTo(OPENAI_PATH)).toHaveLength(0);
427
+ expect(requestsTo(ANTHROPIC_PATH)).toHaveLength(1);
428
+ expect(response.actualInferenceProfile).toBe("quality-optimized-backup");
429
+ });
430
+ });
@@ -286,6 +286,34 @@ describe("PUT /v1/config/llm/profiles/:name — managed profile guard", () => {
286
286
  expectNothingCommitted();
287
287
  });
288
288
 
289
+ test("PUT on a managed backup profile is rejected as code-owned", async () => {
290
+ // Backups are code-owned too, and they carry no on-disk entry, so the
291
+ // code-owned rejection has to win over the availability gate: reporting
292
+ // "not currently available" would be wrong for a profile the picker
293
+ // lists and the user can select.
294
+ seedRawConfig({ llm: { profiles: {} } });
295
+
296
+ for (const name of [
297
+ "balanced-backup",
298
+ "quality-optimized-backup",
299
+ "cost-optimized-backup",
300
+ "latency-optimized-backup",
301
+ ]) {
302
+ for (const body of [
303
+ { label: "Zippy" },
304
+ { status: "active" as const },
305
+ { status: null },
306
+ ]) {
307
+ await expect(
308
+ replaceRoute.handler({ pathParams: { name }, body }),
309
+ ).rejects.toThrow(
310
+ `Profile "${name}" is code-owned and cannot be edited.`,
311
+ );
312
+ }
313
+ }
314
+ expectNothingCommitted();
315
+ });
316
+
289
317
  test("PUT { status: null } on managed profile clears status (back to active-by-absence)", async () => {
290
318
  seedRawConfig({
291
319
  llm: {
@@ -26,7 +26,11 @@ mock.module("../calls/telephony-tts-capability.js", () => ({
26
26
  }));
27
27
 
28
28
  import { getConfig, invalidateConfigCache } from "../config/loader.js";
29
- import { maybeDefaultSpeechToManaged } from "../config/managed-speech-defaults.js";
29
+ import {
30
+ maybeDefaultSpeechToManaged,
31
+ resolveEffectiveSpeechProviders,
32
+ } from "../config/managed-speech-defaults.js";
33
+ import { sttCatalogKeyForRole } from "../stt/roles.js";
30
34
 
31
35
  const WORKSPACE_DIR = process.env.VELLUM_WORKSPACE_DIR!;
32
36
  const CONFIG_PATH = join(WORKSPACE_DIR, "config.json");
@@ -115,6 +119,112 @@ describe("maybeDefaultSpeechToManaged", () => {
115
119
  });
116
120
  });
117
121
 
122
+ test("a narrowing stand-in is written to the live-voice role, not the global", async () => {
123
+ // BYOK Flux with no key: the stand-in preserves turn detection, so it is
124
+ // vellum-flux, which streams and nothing else. Writing that globally is
125
+ // how batch transcription and telephony disappear for a managed user.
126
+ mockManagedSpeechAvailable = true;
127
+ writeConfig({
128
+ services: {
129
+ stt: {
130
+ provider: "deepgram",
131
+ providers: { deepgram: { model: "flux" } },
132
+ },
133
+ tts: { provider: "vellum" },
134
+ },
135
+ });
136
+
137
+ await maybeDefaultSpeechToManaged();
138
+
139
+ const stt = (readConfig().services as any).stt;
140
+ expect(stt.roles.liveVoice).toEqual({ provider: "vellum", model: "flux" });
141
+ // The other consumers land on the stand-in's base, which serves them all.
142
+ expect(stt.provider).toBe("vellum");
143
+ expect(stt.providers.vellum.model).toBe("nova-3");
144
+ });
145
+
146
+ test("the role write leaves batch and telephony on a provider that answers", async () => {
147
+ mockManagedSpeechAvailable = true;
148
+ writeConfig({
149
+ services: {
150
+ stt: {
151
+ provider: "deepgram",
152
+ providers: { deepgram: { model: "flux" } },
153
+ },
154
+ tts: { provider: "vellum" },
155
+ },
156
+ });
157
+
158
+ await maybeDefaultSpeechToManaged();
159
+
160
+ invalidateConfigCache();
161
+ const stt = getConfig().services.stt;
162
+ expect(sttCatalogKeyForRole(stt, "liveVoice")).toBe("vellum-flux");
163
+ expect(sttCatalogKeyForRole(stt, "batch")).toBe("vellum");
164
+ expect(sttCatalogKeyForRole(stt, "telephony")).toBe("vellum");
165
+ });
166
+
167
+ test("a language Flux has no model for keeps live voice on nova-3", async () => {
168
+ // Korean is outside Flux's ten-language roster, so the relay would reject
169
+ // the dial. Standing in with Flux hands the speaker a dead mic rather
170
+ // than a worse transcriber.
171
+ mockManagedSpeechAvailable = true;
172
+ writeConfig({
173
+ services: {
174
+ stt: {
175
+ provider: "deepgram",
176
+ language: "ko",
177
+ providers: { deepgram: { model: "flux" } },
178
+ },
179
+ tts: { provider: "vellum" },
180
+ },
181
+ });
182
+
183
+ await maybeDefaultSpeechToManaged();
184
+
185
+ const stt = (readConfig().services as any).stt;
186
+ expect(stt.provider).toBe("vellum");
187
+ expect(stt.providers.vellum.model).toBe("nova-3");
188
+ expect(stt.roles).toBeUndefined();
189
+ });
190
+
191
+ test("a language Flux does serve still reaches the flux stand-in", async () => {
192
+ mockManagedSpeechAvailable = true;
193
+ writeConfig({
194
+ services: {
195
+ stt: {
196
+ provider: "deepgram",
197
+ language: "hi",
198
+ providers: { deepgram: { model: "flux" } },
199
+ },
200
+ tts: { provider: "vellum" },
201
+ },
202
+ });
203
+
204
+ await maybeDefaultSpeechToManaged();
205
+
206
+ const stt = (readConfig().services as any).stt;
207
+ expect(stt.roles.liveVoice).toEqual({ provider: "vellum", model: "flux" });
208
+ });
209
+
210
+ test("a capability-preserving stand-in still writes the global provider", async () => {
211
+ // Plain deepgram has no turn detection, so the stand-in is plain vellum,
212
+ // which serves every role. That one belongs on the global, not a role.
213
+ mockManagedSpeechAvailable = true;
214
+ writeConfig({
215
+ services: {
216
+ stt: { provider: "deepgram" },
217
+ tts: { provider: "vellum" },
218
+ },
219
+ });
220
+
221
+ await maybeDefaultSpeechToManaged();
222
+
223
+ const stt = (readConfig().services as any).stt;
224
+ expect(stt.provider).toBe("vellum");
225
+ expect(stt.roles).toBeUndefined();
226
+ });
227
+
118
228
  test("never repoints an explicit BYOK provider with resolving credentials", async () => {
119
229
  mockManagedSpeechAvailable = true;
120
230
  mockSttKeyResolves = true;
@@ -133,3 +243,88 @@ describe("maybeDefaultSpeechToManaged", () => {
133
243
  expect((config.services as any).tts.provider).toBe("elevenlabs");
134
244
  });
135
245
  });
246
+
247
+ describe("managed live voice runs Flux", () => {
248
+ // Turn detection is the only reason to reach for that family, and a managed
249
+ // user cannot ask for it: the provider picker offers no family, so `vellum`
250
+ // is as specific as they can be.
251
+ test("live voice upgrades to the flux family", async () => {
252
+ mockManagedSpeechAvailable = true;
253
+ writeConfig({ services: { stt: { provider: "vellum" } } });
254
+
255
+ const { stt } = await resolveEffectiveSpeechProviders(undefined, {
256
+ role: "liveVoice",
257
+ });
258
+
259
+ expect(stt).toBe("vellum-flux");
260
+ });
261
+
262
+ test("no other consumer follows it there", async () => {
263
+ // The family streams and nothing else, so batch and telephony would lose
264
+ // their transcriber entirely.
265
+ mockManagedSpeechAvailable = true;
266
+ writeConfig({ services: { stt: { provider: "vellum" } } });
267
+
268
+ for (const role of ["batch", "telephony", "dictation", "watch"] as const) {
269
+ const { stt } = await resolveEffectiveSpeechProviders(undefined, {
270
+ role,
271
+ });
272
+ expect(stt).toBe("vellum");
273
+ }
274
+ const { stt: roleless } = await resolveEffectiveSpeechProviders();
275
+ expect(roleless).toBe("vellum");
276
+ });
277
+
278
+ test("a family the user named wins", async () => {
279
+ // `services.stt.providers.vellum.model` accepts nova-3, so quietly
280
+ // overriding it would be the silent substitution roles exist to prevent.
281
+ mockManagedSpeechAvailable = true;
282
+ writeConfig({
283
+ services: {
284
+ stt: { provider: "vellum", providers: { vellum: { model: "nova-3" } } },
285
+ },
286
+ });
287
+
288
+ const { stt } = await resolveEffectiveSpeechProviders(undefined, {
289
+ role: "liveVoice",
290
+ });
291
+
292
+ expect(stt).toBe("vellum");
293
+ });
294
+
295
+ test("a language the family cannot serve stays on nova-3", async () => {
296
+ mockManagedSpeechAvailable = true;
297
+ writeConfig({
298
+ services: { stt: { provider: "vellum", language: "ko" } },
299
+ });
300
+
301
+ const { stt } = await resolveEffectiveSpeechProviders(undefined, {
302
+ role: "liveVoice",
303
+ });
304
+
305
+ expect(stt).toBe("vellum");
306
+ });
307
+
308
+ test("nothing is upgraded while managed speech is unavailable", async () => {
309
+ mockManagedSpeechAvailable = false;
310
+ writeConfig({ services: { stt: { provider: "vellum" } } });
311
+
312
+ const { stt } = await resolveEffectiveSpeechProviders(undefined, {
313
+ role: "liveVoice",
314
+ });
315
+
316
+ expect(stt).toBe("vellum");
317
+ });
318
+
319
+ test("the upgrade writes no config", async () => {
320
+ // A resolution rule, not a substitution: nothing lands on disk for a user
321
+ // to discover, reset, or have to migrate.
322
+ mockManagedSpeechAvailable = true;
323
+ writeConfig({ services: { stt: { provider: "vellum" } } });
324
+ const before = JSON.stringify(readConfig());
325
+
326
+ await resolveEffectiveSpeechProviders(undefined, { role: "liveVoice" });
327
+
328
+ expect(JSON.stringify(readConfig())).toBe(before);
329
+ });
330
+ });