@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
@@ -65,6 +65,10 @@ const fakeConnections = new Map<string, Connection>();
65
65
  mock.module("../inference/connections.js", () => ({
66
66
  getConnection: (_db: unknown, name: string) =>
67
67
  fakeConnections.get(name) ?? null,
68
+ listConnections: (_db: unknown, filter?: { provider?: string }) =>
69
+ Array.from(fakeConnections.values()).filter(
70
+ (c) => !filter?.provider || c.provider === filter.provider,
71
+ ),
68
72
  }));
69
73
 
70
74
  mock.module("../registry.js", () => ({
@@ -449,11 +453,43 @@ describe("routing identities", () => {
449
453
  },
450
454
  } as never);
451
455
  expect(resolveProviderCalls[0]?.name).toBe("chatgpt-subscription");
452
- // No override needed: the subscription row itself carries provider
453
- // "openai", so the adapter resolves from the row.
456
+ // A pre-migration row carries provider "openai": no override needed, the
457
+ // adapter resolves from the row.
454
458
  expect(resolveProviderCalls[0]?.provider).toBe("openai");
455
459
  });
456
460
 
461
+ test("a chatgpt-identity subscription row dispatches with an openai override", async () => {
462
+ // Migration 366 stamps the row with the "chatgpt" identity; the identity
463
+ // must never reach adapter construction, so dispatch threads the openai
464
+ // upstream as providerOverride, mirroring the vellum sentinel.
465
+ fakeConnections.set("chatgpt-subscription", {
466
+ name: "chatgpt-subscription",
467
+ provider: "chatgpt",
468
+ auth: {
469
+ type: "oauth_subscription",
470
+ credential: "credential/chatgpt/access_token",
471
+ },
472
+ });
473
+ fakeProviders.set("conn:chatgpt-subscription", {
474
+ name: "openai",
475
+ tag: "subscription",
476
+ });
477
+
478
+ const provider = await tryResolveProviderForConnectionName(
479
+ "ignored",
480
+ { llm: {} } as never,
481
+ "chatgpt",
482
+ "gpt-5.5",
483
+ );
484
+
485
+ expect(provider?.routeAttribution).toEqual({
486
+ connectionName: "chatgpt-subscription",
487
+ isManagedRoute: false,
488
+ });
489
+ expect(resolveProviderCalls[0]?.name).toBe("chatgpt-subscription");
490
+ expect(resolveProviderOpts[0]?.providerOverride).toBe("openai");
491
+ });
492
+
457
493
  // A second managed row is creatable under any non-reserved name: only the
458
494
  // canonical name is refused at the create route, and `provider: "vellum"`
459
495
  // derives platform auth. Managed-ness is the row, never its name.
@@ -492,3 +528,179 @@ describe("routing identities", () => {
492
528
  ).rejects.toMatchObject({ reason: "not_found" });
493
529
  });
494
530
  });
531
+
532
+ // ---------------------------------------------------------------------------
533
+ // Entry-name providers — a profile provider naming a connection row directly.
534
+ // Write surfaces reject these until the entries model enables them; dispatch
535
+ // translates them so hand-edited configs (and the collapse migration later)
536
+ // route through the named row.
537
+ // ---------------------------------------------------------------------------
538
+
539
+ describe("entry-name providers", () => {
540
+ beforeEach(() => {
541
+ resolveProviderCalls.length = 0;
542
+ resolveProviderOpts.length = 0;
543
+ fakeConnections.clear();
544
+ fakeProviders.clear();
545
+ setConfig("llm", {});
546
+ });
547
+
548
+ test("a profile naming an entry dispatches through that row", async () => {
549
+ registerConnection(
550
+ {
551
+ name: "anthropic-work",
552
+ provider: "anthropic",
553
+ auth: {
554
+ type: "api_key",
555
+ credential: "credential/anthropic-work/api_key",
556
+ },
557
+ },
558
+ { name: "anthropic", tag: "work-key" },
559
+ );
560
+ setLlmConfig({
561
+ profiles: {
562
+ work: { provider: "anthropic-work", model: "claude-opus-4-8" },
563
+ },
564
+ });
565
+
566
+ const result = await getConfiguredProvider("mainAgent", {
567
+ overrideProfile: "work",
568
+ });
569
+
570
+ expect(result).not.toBeNull();
571
+ expect(resolveProviderCalls.length).toBe(1);
572
+ expect(resolveProviderCalls[0]?.name).toBe("anthropic-work");
573
+ // The label is not a vendor: the row's own provider drives dispatch,
574
+ // so no override is threaded.
575
+ expect(resolveProviderOpts[0]?.providerOverride).toBeUndefined();
576
+ });
577
+
578
+ test("a vellum-kind entry derives its upstream from the model", async () => {
579
+ registerConnection(
580
+ { name: "work-managed", provider: "vellum", auth: { type: "platform" } },
581
+ { name: "anthropic", tag: "managed-stub" },
582
+ );
583
+ setLlmConfig({
584
+ profiles: {
585
+ managed: { provider: "work-managed", model: "claude-opus-4-8" },
586
+ },
587
+ });
588
+
589
+ const result = await getConfiguredProvider("mainAgent", {
590
+ overrideProfile: "managed",
591
+ });
592
+
593
+ expect(result).not.toBeNull();
594
+ expect(resolveProviderCalls[0]?.name).toBe("work-managed");
595
+ expect(resolveProviderOpts[0]?.providerOverride).toBe("anthropic");
596
+ });
597
+
598
+ test("an entry label alongside an explicit provider_connection still dispatches", async () => {
599
+ // The step-3 collapse migration rewrites provider to an entry name; a
600
+ // stale provider_connection left beside it must not fail the vendor
601
+ // equality against a label that is not a vendor.
602
+ registerConnection(
603
+ {
604
+ name: "anthropic-work",
605
+ provider: "anthropic",
606
+ auth: {
607
+ type: "api_key",
608
+ credential: "credential/anthropic-work/api_key",
609
+ },
610
+ },
611
+ { name: "anthropic", tag: "work-key" },
612
+ );
613
+ setLlmConfig({
614
+ profiles: {
615
+ work: {
616
+ provider: "anthropic-work",
617
+ provider_connection: "anthropic-work",
618
+ model: "claude-opus-4-8",
619
+ },
620
+ },
621
+ });
622
+
623
+ const result = await getConfiguredProvider("mainAgent", {
624
+ overrideProfile: "work",
625
+ });
626
+
627
+ expect(result).not.toBeNull();
628
+ expect(resolveProviderCalls[0]?.name).toBe("anthropic-work");
629
+ expect(resolveProviderOpts[0]?.providerOverride).toBeUndefined();
630
+ });
631
+
632
+ test("a label with a conflicting provider_connection is held to the label's kind", async () => {
633
+ // The label's row kind threads as the expected vendor, so a stale or
634
+ // conflicting connection cannot silently dispatch a different vendor:
635
+ // the equality mismatch auto-recovers to a row matching the kind.
636
+ registerConnection(
637
+ {
638
+ name: "anthropic-work",
639
+ provider: "anthropic",
640
+ auth: {
641
+ type: "api_key",
642
+ credential: "credential/anthropic-work/api_key",
643
+ },
644
+ },
645
+ { name: "anthropic", tag: "work-key" },
646
+ );
647
+ registerConnection(
648
+ { name: "ollama-local", provider: "ollama", auth: { type: "none" } },
649
+ { name: "ollama", tag: "local" },
650
+ );
651
+ setLlmConfig({
652
+ profiles: {
653
+ conflicted: {
654
+ provider: "anthropic-work",
655
+ provider_connection: "ollama-local",
656
+ model: "claude-opus-4-8",
657
+ },
658
+ },
659
+ });
660
+
661
+ const result = await getConfiguredProvider("mainAgent", {
662
+ overrideProfile: "conflicted",
663
+ });
664
+
665
+ expect(result).not.toBeNull();
666
+ expect(resolveProviderCalls[0]?.name).toBe("anthropic-work");
667
+ });
668
+
669
+ test("a profile naming no row is healed away at selection", async () => {
670
+ // Selection treats an unresolvable provider like an incomplete profile:
671
+ // the broken winner loses and the next rung's model AND transport apply
672
+ // together, restoring the pre-open-schema self-healing. The anchor is
673
+ // the managed default, so the heal lands on the vellum route.
674
+ registerConnection(
675
+ { name: "vellum", provider: "vellum", auth: { type: "platform" } },
676
+ { name: "anthropic", tag: "managed-anchor" },
677
+ );
678
+ setLlmConfig({
679
+ profiles: {
680
+ ghost: { provider: "no-such-entry", model: "claude-opus-4-8" },
681
+ },
682
+ });
683
+
684
+ const result = await getConfiguredProvider("mainAgent", {
685
+ overrideProfile: "ghost",
686
+ });
687
+
688
+ expect(result).not.toBeNull();
689
+ expect(resolveProviderCalls[0]?.name).toBe("vellum");
690
+ });
691
+
692
+ test("healing hands off to the anchor, whose own failures stay loud", async () => {
693
+ // With the broken profile healed away, resolution rests on the managed
694
+ // anchor; its canonical row missing is a boot-seeding violation and
695
+ // throws explainably rather than masquerading as the ghost profile.
696
+ setLlmConfig({
697
+ profiles: {
698
+ ghost: { provider: "no-such-entry", model: "claude-opus-4-8" },
699
+ },
700
+ });
701
+
702
+ await expect(
703
+ getConfiguredProvider("mainAgent", { overrideProfile: "ghost" }),
704
+ ).rejects.toMatchObject({ reason: "not_found", connectionName: "vellum" });
705
+ });
706
+ });
@@ -220,6 +220,63 @@ describe("preflightResolvedConfig", () => {
220
220
  ).resolves.toBeUndefined();
221
221
  });
222
222
 
223
+ test("an explicit connection outranks an entry-name provider, matching dispatch", async () => {
224
+ // A healthy entry row must not mask the row the request actually uses.
225
+ connectionsByName["anthropic-work"] = {
226
+ name: "anthropic-work",
227
+ provider: "anthropic",
228
+ auth: {
229
+ type: "api_key",
230
+ credential: "credential/anthropic-work/api_key",
231
+ },
232
+ };
233
+ secureKeys["credential/anthropic-work/api_key"] = "healthy";
234
+
235
+ const err = await preflightError(
236
+ resolved({
237
+ provider: "anthropic-work",
238
+ provider_connection: "deleted-row",
239
+ model: "claude-opus-4-8",
240
+ }),
241
+ );
242
+ expect(err?.reason).toBe("not_found");
243
+ expect(err?.connectionName).toBe("deleted-row");
244
+ });
245
+
246
+ test("a chatgpt-identity subscription row preflights the same way", async () => {
247
+ // Migration 366 stamps the row with provider "chatgpt"; preflight judges
248
+ // its subscription credential, not a provider equality with the openai
249
+ // upstream.
250
+ connectionsByName["chatgpt-subscription"] = {
251
+ name: "chatgpt-subscription",
252
+ provider: "chatgpt",
253
+ auth: {
254
+ type: "oauth_subscription",
255
+ credential: "credential/chatgpt/access_token",
256
+ },
257
+ };
258
+ const missing = await preflightError(
259
+ resolved({
260
+ provider: "chatgpt",
261
+ provider_connection: "",
262
+ model: "gpt-5.5",
263
+ }),
264
+ );
265
+ expect(missing?.reason).toBe("missing_credential");
266
+
267
+ secureKeys["credential/chatgpt/access_token"] = "tok";
268
+ await expect(
269
+ preflightResolvedConfig(
270
+ resolved({
271
+ provider: "chatgpt",
272
+ provider_connection: "",
273
+ model: "gpt-5.5",
274
+ }),
275
+ {},
276
+ ),
277
+ ).resolves.toBeUndefined();
278
+ });
279
+
223
280
  test("a chatgpt identity with no subscription row throws not_found", async () => {
224
281
  const err = await preflightError(
225
282
  resolved({
@@ -337,7 +337,7 @@ describe("RetryProvider — callSite resolution", () => {
337
337
  expect(config.max_tokens).toBe(expected.maxTokens as number);
338
338
  });
339
339
 
340
- test("live-voice front-door call sites reach OpenAI with reasoning off", async () => {
340
+ test("latency-sensitive live-voice call sites reach OpenAI with reasoning off", async () => {
341
341
  // The `latency-optimized` pin's whole value is time-to-first-token, and on
342
342
  // an OpenAI upstream that depends entirely on disabled thinking being
343
343
  // encoded as `effort: "none"`. OpenAI-compatible APIs reason by default
@@ -351,7 +351,10 @@ describe("RetryProvider — callSite resolution", () => {
351
351
  setLlmConfig({});
352
352
 
353
353
  const expected = CODE_DEFAULT_PROFILE_ENTRIES["latency-optimized"];
354
- for (const callSite of ["voiceFrontDoor", "voiceFrontDecision"] as const) {
354
+ for (const callSite of [
355
+ "voiceFrontDoor",
356
+ "voiceProgressNarration",
357
+ ] as const) {
355
358
  let seen: SendMessageOptions | undefined;
356
359
  const wrapped = new RetryProvider(
357
360
  makeProvider("openai", (options) => {
@@ -37,8 +37,13 @@ import {
37
37
  isConnectionCompatibleWithModel,
38
38
  } from "./connection-model-compat.js";
39
39
  import {
40
+ connectionProviderKind,
40
41
  ConnectionResolutionError,
42
+ dispatchProviderResolvable,
43
+ expectedVendorProvider,
41
44
  isManagedConnectionRoute,
45
+ resolveEntryConnectionName,
46
+ resolveEntryProviderKind,
42
47
  resolveRoutingIdentity,
43
48
  tryResolveProviderForConnectionName,
44
49
  } from "./connection-resolution.js";
@@ -112,7 +117,7 @@ export class CallSiteRoutingProvider implements Provider {
112
117
  */
113
118
  private readonly resolveByConnection: (
114
119
  connectionName: string,
115
- expectedProvider: string,
120
+ expectedProvider: string | undefined,
116
121
  model: string | undefined,
117
122
  ) => Promise<Provider | null>,
118
123
  private readonly defaultRouteAttribution?: ProviderRouteAttribution,
@@ -203,9 +208,17 @@ export class CallSiteRoutingProvider implements Provider {
203
208
  forceOverrideProfile: options?.config?.forceOverrideProfile,
204
209
  selectionSeed: options?.config?.selectionSeed,
205
210
  });
211
+ // Capability follows the same row dispatch selects: an explicit
212
+ // provider_connection wins, then an entry-name label's row, then the
213
+ // resolved provider itself. Kept in this order so a label paired with a
214
+ // conflicting connection cannot enable a capability the dispatched
215
+ // transport lacks.
216
+ const routedKind = resolved.provider_connection
217
+ ? connectionProviderKind(resolved.provider_connection, resolved.model)
218
+ : resolveEntryProviderKind(resolved.provider, resolved.model);
206
219
  return shouldUseNativeWebSearch(
207
220
  getConfig(),
208
- resolved.provider,
221
+ routedKind ?? resolved.provider,
209
222
  resolved.model,
210
223
  );
211
224
  }
@@ -256,6 +269,7 @@ export class CallSiteRoutingProvider implements Provider {
256
269
  overrideProfile,
257
270
  forceOverrideProfile,
258
271
  selectionSeed,
272
+ isResolvableProvider: dispatchProviderResolvable,
259
273
  },
260
274
  );
261
275
 
@@ -273,6 +287,16 @@ export class CallSiteRoutingProvider implements Provider {
273
287
  )?.connectionName;
274
288
  }
275
289
 
290
+ // An entry-name provider IS the connection name: the label points at a
291
+ // row, and the row's own provider drives dispatch, so no expected
292
+ // provider is threaded (the label is not a vendor).
293
+ const entryRoute = connectionName
294
+ ? null
295
+ : resolveEntryConnectionName(resolved.provider);
296
+ if (entryRoute) {
297
+ connectionName = entryRoute;
298
+ }
299
+
276
300
  // When no connection is set, auto-resolve one for the resolved provider —
277
301
  // including when the provider matches the default's name. The default
278
302
  // transport may ride the managed (platform-billed) connection, so reusing
@@ -301,9 +325,12 @@ export class CallSiteRoutingProvider implements Provider {
301
325
  }
302
326
 
303
327
  if (connectionName) {
328
+ // The vendor guard covers both the entry route and a config carrying
329
+ // an entry label alongside an explicit provider_connection: a label
330
+ // that is not a vendor never threads into the row-equality check.
304
331
  const connectionProvider = await this.resolveByConnection(
305
332
  connectionName,
306
- resolved.provider,
333
+ expectedVendorProvider(resolved.provider, resolved.model),
307
334
  resolved.model,
308
335
  );
309
336
  if (connectionProvider) {
@@ -32,6 +32,7 @@ import {
32
32
  resolveCallSiteConfig,
33
33
  selectWinningProfile,
34
34
  } from "../config/llm-resolver.js";
35
+ import { unknownLlmProviderIssue } from "../config/schemas/llm.js";
35
36
  import { getDb } from "../persistence/db-connection.js";
36
37
  import { credentialKey } from "../security/credential-key.js";
37
38
  import { ProviderNotConfiguredError } from "../util/errors.js";
@@ -40,6 +41,7 @@ import {
40
41
  describeSubscriptionModelIncompatibility,
41
42
  isConnectionCompatibleWithModel,
42
43
  } from "./connection-model-compat.js";
44
+ import { VALID_CONNECTION_PROVIDERS } from "./inference/auth.js";
43
45
  import {
44
46
  canonicalVellumConnection,
45
47
  getConnection,
@@ -55,6 +57,7 @@ import {
55
57
  } from "./routing-identity.js";
56
58
  import type { Provider } from "./types.js";
57
59
  import {
60
+ getManagedUpstream,
58
61
  isVellumManagedConnection,
59
62
  MANAGED_ROUTABLE_PROVIDERS,
60
63
  VELLUM_MANAGED_CONNECTION_NAME,
@@ -64,6 +67,132 @@ export { ConnectionResolutionError, resolveRoutingIdentity };
64
67
 
65
68
  const log = getLogger("providers/connection-resolution");
66
69
 
70
+ /**
71
+ * Resolve a provider label that names a connection row (an entry) to that
72
+ * row's name. Returns null for catalog providers and routing identities
73
+ * (those translate through their own rules) and for labels naming no row.
74
+ *
75
+ * The write surfaces reject entry-name providers until the entries model
76
+ * enables them, so a config carrying one reaches dispatch only through a
77
+ * hand edit today and through the collapse migration later; translating
78
+ * here makes both route explainably instead of failing as an unknown
79
+ * provider.
80
+ */
81
+ export function resolveEntryConnectionName(
82
+ provider: string | undefined,
83
+ ): string | null {
84
+ if (!provider || VALID_CONNECTION_PROVIDERS.includes(provider)) {
85
+ return null;
86
+ }
87
+ try {
88
+ return getConnection(getDb(), provider) ? provider : null;
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Write-surface membership for a PROFILE provider value: the known vendor
96
+ * and identity set, plus existing connection entry rows. Fail-closed on DB
97
+ * unavailability (an unverifiable entry name is rejected; the write is
98
+ * retryable), unlike the selection-time predicate below, which is
99
+ * fail-open so a DB blip never heals away a valid profile. Call-site
100
+ * fragments keep vendor-only membership: overrides become model-only with
101
+ * the entries demolition, so entries must not leak into them meanwhile.
102
+ */
103
+ export function writableProfileProviderIssue(provider: string): string | null {
104
+ const issue = unknownLlmProviderIssue(provider);
105
+ if (issue === null) {
106
+ return null;
107
+ }
108
+ try {
109
+ if (getConnection(getDb(), provider) != null) {
110
+ return null;
111
+ }
112
+ } catch {
113
+ // Unverifiable: fall through to the rejection.
114
+ }
115
+ return `Invalid provider "${provider}". Use a known provider or the name of an existing connection.`;
116
+ }
117
+
118
+ /**
119
+ * Selection-time predicate for `ResolveCallSiteOpts.isResolvableProvider`:
120
+ * a provider value dispatches when it is a known vendor/identity or names a
121
+ * connection entry row. Permissive on DB unavailability so a transient blip
122
+ * never heals away a valid entry profile; dispatch soft-fails on its own.
123
+ */
124
+ export function dispatchProviderResolvable(provider: string): boolean {
125
+ if (VALID_CONNECTION_PROVIDERS.includes(provider)) {
126
+ return true;
127
+ }
128
+ try {
129
+ return getConnection(getDb(), provider) != null;
130
+ } catch {
131
+ return true;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * The vendor a resolved provider value expects its connection row to serve:
137
+ * catalog ids and routing identities pass through, and an entry-name label
138
+ * resolves to its row's dispatchable kind, so a config carrying both an
139
+ * entry label and a `provider_connection` is held to the label's kind by
140
+ * the row-equality check (a conflicting row mismatches explainably or
141
+ * auto-recovers to a matching one). A label naming no row yields undefined.
142
+ */
143
+ export function expectedVendorProvider(
144
+ provider: string | undefined,
145
+ model: string | undefined,
146
+ ): string | undefined {
147
+ if (!provider) {
148
+ return undefined;
149
+ }
150
+ if (VALID_CONNECTION_PROVIDERS.includes(provider)) {
151
+ return provider;
152
+ }
153
+ return resolveEntryProviderKind(provider, model) ?? undefined;
154
+ }
155
+
156
+ /**
157
+ * The dispatchable provider kind behind a connection row, or null when the
158
+ * row is missing or its kind cannot be derived. Identity-kind rows derive
159
+ * their upstream the way the identities themselves do. Sync and
160
+ * best-effort so capability probes can share dispatch's translation
161
+ * without replaying its async resolution.
162
+ */
163
+ export function connectionProviderKind(
164
+ connectionName: string,
165
+ model: string | undefined,
166
+ ): string | null {
167
+ try {
168
+ const row = getConnection(getDb(), connectionName);
169
+ if (!row) {
170
+ return null;
171
+ }
172
+ if (isVellumManagedConnection(row)) {
173
+ return model ? getManagedUpstream(model) : null;
174
+ }
175
+ if (row.provider === "chatgpt") {
176
+ return "openai";
177
+ }
178
+ return row.provider;
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * The dispatchable provider kind behind an entry-name label, or null when
186
+ * the label is not an entry.
187
+ */
188
+ export function resolveEntryProviderKind(
189
+ provider: string | undefined,
190
+ model: string | undefined,
191
+ ): string | null {
192
+ const entryName = resolveEntryConnectionName(provider);
193
+ return entryName ? connectionProviderKind(entryName, model) : null;
194
+ }
195
+
67
196
  /**
68
197
  * Resolve a Provider through a named `provider_connection`.
69
198
  *
@@ -152,18 +281,36 @@ export async function tryResolveProviderForConnectionName(
152
281
  // fall back to the default provider.
153
282
  const isVellum = isVellumManagedConnection(connection);
154
283
  if (isVellum && !expectedProvider) {
155
- throw new ConnectionResolutionError(
156
- connectionName,
157
- "provider_mismatch",
158
- `provider_connection "${connectionName}" is the provider-agnostic Vellum-managed connection but the resolving profile declared no provider — set the profile's provider so the upstream can be selected`,
159
- );
284
+ // An entry-name route carries no declared provider (the label is the
285
+ // row's name, not a vendor); a vellum-kind row derives its upstream
286
+ // from the model, same as the vellum identity itself.
287
+ expectedProvider = model
288
+ ? (getManagedUpstream(model) ?? undefined)
289
+ : undefined;
290
+ if (!expectedProvider) {
291
+ throw new ConnectionResolutionError(
292
+ connectionName,
293
+ "provider_mismatch",
294
+ `provider_connection "${connectionName}" is the provider-agnostic Vellum-managed connection but the resolving profile declared no provider — set the profile's provider so the upstream can be selected`,
295
+ );
296
+ }
160
297
  }
161
298
  const isVellumRoute =
162
299
  isVellum &&
163
300
  !!expectedProvider &&
164
301
  MANAGED_ROUTABLE_PROVIDERS.has(expectedProvider);
302
+ // The ChatGPT-subscription row stores the "chatgpt" routing identity in
303
+ // its provider column, so the equality never holds for its openai
304
+ // upstream. The identity dispatches with the upstream threaded as
305
+ // `providerOverride`, mirroring the vellum sentinel; the upstream is
306
+ // always openai (`resolveRoutingIdentity`). Any other declared provider
307
+ // on a chatgpt row is a genuine mismatch and falls through below.
308
+ const isChatgptRoute =
309
+ connection.provider === "chatgpt" &&
310
+ (expectedProvider === undefined || expectedProvider === "openai");
165
311
  if (
166
312
  !isVellumRoute &&
313
+ !isChatgptRoute &&
167
314
  expectedProvider &&
168
315
  connection.provider !== expectedProvider
169
316
  ) {
@@ -225,7 +372,11 @@ export async function tryResolveProviderForConnectionName(
225
372
  try {
226
373
  const provider = await resolveProviderFromConnection(connection, config, {
227
374
  model,
228
- providerOverride: isVellumRoute ? expectedProvider : undefined,
375
+ providerOverride: isVellumRoute
376
+ ? expectedProvider
377
+ : isChatgptRoute
378
+ ? "openai"
379
+ : undefined,
229
380
  });
230
381
  return attachProviderRoute(provider, connection);
231
382
  } catch (err) {
@@ -324,7 +475,9 @@ async function resolveThroughPlatform(
324
475
  export async function resolveDefaultProvider(
325
476
  config: ProvidersConfig,
326
477
  ): Promise<Provider | null> {
327
- const resolved = resolveCallSiteConfig("mainAgent", config.llm);
478
+ const resolved = resolveCallSiteConfig("mainAgent", config.llm, {
479
+ isResolvableProvider: dispatchProviderResolvable,
480
+ });
328
481
  let connectionName = resolved.provider_connection;
329
482
  // A routing-identity provider names its own connection row; the
330
483
  // provider-keyed auto-resolve scan below cannot find it ("chatgpt" rows
@@ -335,6 +488,19 @@ export async function resolveDefaultProvider(
335
488
  resolved.model,
336
489
  )?.connectionName;
337
490
  }
491
+ // An entry-name provider IS the connection name: the label points at a
492
+ // row, and the row's own provider drives dispatch.
493
+ const entryName = connectionName
494
+ ? null
495
+ : resolveEntryConnectionName(resolved.provider);
496
+ if (entryName) {
497
+ return tryResolveProviderForConnectionName(
498
+ entryName,
499
+ config,
500
+ undefined,
501
+ resolved.model,
502
+ );
503
+ }
338
504
  if (!connectionName) {
339
505
  // The merged config has no provider_connection — the profile likely set
340
506
  // provider without a connection ("Any active" selection), and the merge
@@ -387,7 +553,7 @@ export async function resolveDefaultProvider(
387
553
  return tryResolveProviderForConnectionName(
388
554
  connectionName,
389
555
  config,
390
- resolved.provider,
556
+ expectedVendorProvider(resolved.provider, resolved.model),
391
557
  resolved.model,
392
558
  );
393
559
  }
@@ -423,9 +589,21 @@ export async function preflightResolvedConfig(
423
589
  // upstream; an unroutable vellum model throws here — it is statically
424
590
  // detectable, exactly what preflight exists to surface.
425
591
  const identity = resolveRoutingIdentity(resolved.provider, resolved.model);
426
- const provider = identity?.expectedProvider ?? resolved.provider;
592
+ // An entry-name provider IS the connection name, and the row's kind is
593
+ // what the checks below judge against (same translation dispatch uses).
594
+ // Precedence matches dispatch exactly: an explicit provider_connection
595
+ // wins over the entry name, so preflight judges the row the request
596
+ // actually uses rather than a healthy entry the request ignores.
597
+ const entryName = identity
598
+ ? null
599
+ : resolveEntryConnectionName(resolved.provider);
600
+ const provider = identity
601
+ ? identity.expectedProvider
602
+ : entryName
603
+ ? (connectionProviderKind(entryName, resolved.model) ?? resolved.provider)
604
+ : resolved.provider;
427
605
  const connectionName =
428
- identity?.connectionName ?? resolved.provider_connection;
606
+ identity?.connectionName ?? resolved.provider_connection ?? entryName;
429
607
  if (!connectionName) {
430
608
  return;
431
609
  }
@@ -512,7 +690,12 @@ export async function preflightResolvedConfig(
512
690
  }
513
691
  return;
514
692
  }
515
- if (connection.provider !== provider) {
693
+ // The ChatGPT-subscription row stores the "chatgpt" identity while its
694
+ // upstream is openai; the credential-presence switch below is the right
695
+ // preflight for it (the subscription token is its credential).
696
+ const isChatgptRow =
697
+ connection.provider === "chatgpt" && provider === "openai";
698
+ if (!isChatgptRow && connection.provider !== provider) {
516
699
  throw new ConnectionResolutionError(
517
700
  connectionName,
518
701
  "provider_mismatch",