@vellumai/assistant 0.11.1 → 0.11.2-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 (318) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +1 -1
  3. package/eslint.config.mjs +28 -8
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  12. package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  13. package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
  14. package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
  15. package/openapi.yaml +292 -5
  16. package/package.json +1 -1
  17. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  18. package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
  19. package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
  20. package/src/__tests__/compaction-events.test.ts +139 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +35 -3
  22. package/src/__tests__/config-schema.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +180 -7
  25. package/src/__tests__/conversation-error.test.ts +211 -4
  26. package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
  27. package/src/__tests__/conversation-queue.test.ts +126 -0
  28. package/src/__tests__/conversation-retry-route.test.ts +46 -7
  29. package/src/__tests__/conversation-slash.test.ts +4 -2
  30. package/src/__tests__/conversation-summarize-route.test.ts +7 -1
  31. package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
  32. package/src/__tests__/document-create-dedupe.test.ts +2 -1
  33. package/src/__tests__/document-find-replace.test.ts +2 -1
  34. package/src/__tests__/document-tool-security.test.ts +2 -1
  35. package/src/__tests__/document-update-default-surface.test.ts +2 -1
  36. package/src/__tests__/document-workspace-file.test.ts +467 -0
  37. package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
  38. package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
  39. package/src/__tests__/http-user-message-parity.test.ts +66 -0
  40. package/src/__tests__/list-messages-attachments.test.ts +87 -0
  41. package/src/__tests__/list-messages-provider-error.test.ts +143 -0
  42. package/src/__tests__/list-messages-system-card.test.ts +100 -0
  43. package/src/__tests__/llm-resolver.test.ts +19 -9
  44. package/src/__tests__/managed-profile-guard.test.ts +23 -0
  45. package/src/__tests__/notification-platform-adapter.test.ts +130 -2
  46. package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
  47. package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
  48. package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
  49. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  50. package/src/__tests__/provider-error-scenarios.test.ts +140 -0
  51. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  52. package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
  53. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
  54. package/src/__tests__/skills.test.ts +27 -0
  55. package/src/__tests__/slack-channels-routes.test.ts +0 -2
  56. package/src/__tests__/slack-share-routes.test.ts +0 -3
  57. package/src/__tests__/slack-users-routes.test.ts +0 -2
  58. package/src/__tests__/subagent-tools.test.ts +11 -0
  59. package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
  60. package/src/__tests__/tool-result-spool.test.ts +5 -4
  61. package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
  62. package/src/__tests__/turn-events-store.test.ts +26 -0
  63. package/src/__tests__/ui-visual-surface.test.ts +695 -0
  64. package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
  65. package/src/__tests__/visible-app-context.test.ts +189 -0
  66. package/src/__tests__/workspace-git-service.test.ts +173 -33
  67. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
  68. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  69. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
  70. package/src/agent/loop.ts +36 -7
  71. package/src/api/events/context-window-usage.ts +31 -0
  72. package/src/api/events/notification-intent.ts +8 -0
  73. package/src/api/events/ui-surface-pending.ts +35 -0
  74. package/src/api/index.ts +14 -0
  75. package/src/api/responses/conversation-message.ts +25 -4
  76. package/src/api/surfaces.ts +90 -1
  77. package/src/approvals/guardian-card-withdrawal.ts +66 -31
  78. package/src/approvals/guardian-decision-primitive.ts +4 -0
  79. package/src/calls/__tests__/call-setup-router.test.ts +156 -26
  80. package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
  81. package/src/calls/call-setup-router.ts +80 -36
  82. package/src/calls/voice-session-bridge.ts +173 -26
  83. package/src/cli/commands/inference.help.ts +3 -3
  84. package/src/cli/commands/notifications.help.ts +3 -2
  85. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
  86. package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
  87. package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
  88. package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
  89. package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
  90. package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
  91. package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
  92. package/src/cli/commands/platform/index.help.ts +92 -0
  93. package/src/cli/commands/platform/index.ts +7 -0
  94. package/src/cli/commands/platform/invoices.ts +132 -0
  95. package/src/cli/commands/usage.help.ts +1 -1
  96. package/src/cli/lib/list-installed-plugins.ts +2 -1
  97. package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
  98. package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
  99. package/src/config/assistant-feature-flags.ts +7 -2
  100. package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
  101. package/src/config/bundled-skills/subagent/SKILL.md +3 -1
  102. package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
  103. package/src/config/bundled-skills/visualize/SKILL.md +163 -0
  104. package/src/config/call-site-defaults.ts +3 -2
  105. package/src/config/default-profile-catalog.ts +75 -70
  106. package/src/config/default-profile-names.ts +13 -28
  107. package/src/config/env-registry.ts +1 -0
  108. package/src/config/feature-flag-registry.json +16 -0
  109. package/src/config/llm-resolver.ts +21 -5
  110. package/src/config/loader.ts +18 -11
  111. package/src/config/schemas/llm.ts +1 -9
  112. package/src/config/schemas/memory-retrospective.ts +9 -0
  113. package/src/config/schemas/monitoring.ts +28 -2
  114. package/src/config/schemas/workspace-git.ts +15 -0
  115. package/src/config/seed-inference-profiles.ts +19 -5
  116. package/src/context/post-turn-tool-result-truncation.ts +2 -2
  117. package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
  118. package/src/conversations/message-consolidation.ts +17 -15
  119. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
  120. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
  121. package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
  122. package/src/daemon/conversation-agent-loop.ts +107 -22
  123. package/src/daemon/conversation-error.ts +169 -48
  124. package/src/daemon/conversation-messaging.ts +87 -4
  125. package/src/daemon/conversation-process.ts +57 -45
  126. package/src/daemon/conversation-runtime-assembly.ts +57 -0
  127. package/src/daemon/conversation-store.ts +36 -1
  128. package/src/daemon/conversation-surfaces.ts +68 -6
  129. package/src/daemon/conversation-turn-finalize.ts +76 -18
  130. package/src/daemon/conversation.ts +122 -27
  131. package/src/daemon/lifecycle.ts +9 -0
  132. package/src/daemon/message-types/conversations.ts +8 -0
  133. package/src/daemon/message-types/surfaces.ts +3 -0
  134. package/src/documents/document-store.ts +247 -10
  135. package/src/home/__tests__/feed-types.test.ts +43 -8
  136. package/src/home/__tests__/feed-writer.test.ts +59 -0
  137. package/src/home/feed-types.ts +34 -7
  138. package/src/home/feed-writer.ts +15 -6
  139. package/src/live-voice/__tests__/activity-label.test.ts +95 -0
  140. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  141. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
  142. package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
  143. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
  144. package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
  145. package/src/live-voice/activity-label.ts +169 -0
  146. package/src/live-voice/live-activity-reporter.ts +35 -6
  147. package/src/live-voice/live-voice-session.ts +282 -41
  148. package/src/live-voice/protocol.ts +35 -0
  149. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
  150. package/src/messaging/providers/slack/adapter.ts +86 -19
  151. package/src/messaging/providers/slack/api.test.ts +85 -1
  152. package/src/messaging/providers/slack/api.ts +121 -288
  153. package/src/messaging/providers/slack/client.ts +20 -251
  154. package/src/messaging/providers/slack/send.test.ts +4 -9
  155. package/src/messaging/providers/slack/send.ts +1 -1
  156. package/src/messaging/providers/slack/types.ts +5 -0
  157. package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
  158. package/src/messaging/providers/slack/web-api-transport.ts +367 -0
  159. package/src/messaging/providers/slack/withdraw.ts +6 -14
  160. package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
  161. package/src/messaging/providers/telegram-bot/send.ts +23 -2
  162. package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
  163. package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
  164. package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
  165. package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
  166. package/src/monitoring/file-descriptors.ts +262 -0
  167. package/src/monitoring/process-memory.ts +4 -11
  168. package/src/monitoring/resource-sampler.ts +5 -0
  169. package/src/monitoring/worker.ts +12 -0
  170. package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
  171. package/src/notifications/__tests__/broadcaster.test.ts +347 -7
  172. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  173. package/src/notifications/__tests__/decision-engine.test.ts +349 -29
  174. package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
  175. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  176. package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
  177. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  178. package/src/notifications/adapters/macos.ts +3 -0
  179. package/src/notifications/adapters/platform.ts +90 -14
  180. package/src/notifications/adapters/telegram.ts +6 -4
  181. package/src/notifications/assistant-reply-producer.ts +219 -0
  182. package/src/notifications/broadcaster.ts +523 -317
  183. package/src/notifications/copy-composer.ts +27 -6
  184. package/src/notifications/decision-engine.ts +154 -77
  185. package/src/notifications/deterministic-checks.ts +6 -7
  186. package/src/notifications/edit-notification.ts +8 -4
  187. package/src/notifications/emit-signal.ts +49 -16
  188. package/src/notifications/guardian-delivery-recorder.ts +8 -2
  189. package/src/notifications/home-feed-side-effect.ts +43 -6
  190. package/src/notifications/notification-utils.ts +39 -4
  191. package/src/notifications/signal.ts +10 -0
  192. package/src/notifications/types.ts +17 -0
  193. package/src/persistence/bookmark-crud.ts +18 -9
  194. package/src/persistence/conversation-attention-store.ts +26 -0
  195. package/src/persistence/conversation-crud.ts +79 -32
  196. package/src/persistence/conversation-queries.ts +30 -15
  197. package/src/persistence/conversation-title-service.ts +5 -170
  198. package/src/persistence/conversation-types.ts +192 -1
  199. package/src/persistence/db-init.ts +14 -1
  200. package/src/persistence/db-maintenance.ts +5 -4
  201. package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
  202. package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
  203. package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
  204. package/src/persistence/embeddings/embedding-local.ts +2 -1
  205. package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
  206. package/src/persistence/embeddings/plugin-index.ts +84 -12
  207. package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
  208. package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
  209. package/src/persistence/planner-statistics.ts +14 -0
  210. package/src/persistence/schema/documents.ts +26 -11
  211. package/src/persistence/steps.ts +2 -0
  212. package/src/platform/client.test.ts +172 -2
  213. package/src/platform/client.ts +178 -55
  214. package/src/plugin-api/conversation-turn.ts +13 -0
  215. package/src/plugin-api/model-profiles.test.ts +6 -2
  216. package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
  217. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
  218. package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
  219. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
  220. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
  224. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
  225. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
  226. package/src/plugins/defaults/memory/host-utils.ts +5 -0
  227. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
  228. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
  229. package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
  230. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
  231. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
  232. package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
  233. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
  234. package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
  235. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
  236. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
  237. package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
  238. package/src/plugins/defaults/memory/v2/injection.ts +48 -3
  239. package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
  240. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
  241. package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
  242. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  243. package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
  244. package/src/plugins/types.ts +17 -0
  245. package/src/prompts/templates/system-sections.ts +2 -2
  246. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  247. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  248. package/src/providers/call-site-routing.ts +77 -14
  249. package/src/providers/connection-resolution.ts +42 -2
  250. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
  251. package/src/providers/inference/adapter-factory.ts +70 -12
  252. package/src/providers/model-catalog.ts +0 -11
  253. package/src/providers/model-intents.ts +11 -0
  254. package/src/providers/registry.ts +8 -0
  255. package/src/providers/retry.ts +85 -17
  256. package/src/providers/types.ts +8 -1
  257. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  258. package/src/runtime/agent-wake.ts +132 -9
  259. package/src/runtime/channel-approval-types.ts +25 -0
  260. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
  261. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
  262. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
  263. package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
  264. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
  265. package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
  266. package/src/runtime/routes/consolidation-routes.ts +1 -1
  267. package/src/runtime/routes/conversation-management-routes.ts +18 -6
  268. package/src/runtime/routes/conversation-query-routes.ts +14 -23
  269. package/src/runtime/routes/conversation-routes.ts +113 -8
  270. package/src/runtime/routes/credential-routes.ts +1 -5
  271. package/src/runtime/routes/documents-routes.ts +207 -12
  272. package/src/runtime/routes/identity-routes.ts +3 -93
  273. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
  274. package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
  275. package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
  276. package/src/runtime/routes/notification-routes.ts +4 -1
  277. package/src/runtime/routes/platform-routes.ts +238 -3
  278. package/src/runtime/routes/playground/guard.ts +1 -2
  279. package/src/runtime/routes/slack-channel-routes.ts +2 -4
  280. package/src/runtime/routes/workspace-commit-routes.ts +49 -2
  281. package/src/runtime/routes/workspace-routes.ts +11 -12
  282. package/src/runtime/routes/workspace-utils.ts +49 -2
  283. package/src/runtime/services/conversation-serializer.ts +2 -4
  284. package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
  285. package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
  286. package/src/subagent/__tests__/consult-context.test.ts +84 -0
  287. package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
  288. package/src/subagent/consult-context.ts +410 -0
  289. package/src/subagent/consult-prompt.ts +38 -5
  290. package/src/subagent/manager.ts +9 -4
  291. package/src/subagent/types.ts +8 -0
  292. package/src/telemetry/telemetry-event-sources.ts +7 -0
  293. package/src/telemetry/telemetry-wire-source.json +1 -1
  294. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  295. package/src/telemetry/turn-events-store.ts +30 -1
  296. package/src/telemetry/types.ts +24 -0
  297. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  298. package/src/tools/browser/pinned-tabs.ts +3 -1
  299. package/src/tools/subagent/spawn.ts +23 -1
  300. package/src/tools/terminal/safe-env.ts +1 -0
  301. package/src/tools/ui-surface/definitions.ts +39 -1
  302. package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
  303. package/src/tools/ui-surface/visual-validation.ts +787 -0
  304. package/src/tools/workflows/run-workflow.test.ts +1 -0
  305. package/src/util/__tests__/short-title.test.ts +229 -0
  306. package/src/util/__tests__/worker-compute.test.ts +65 -0
  307. package/src/util/cgroup-cpu.ts +93 -0
  308. package/src/util/errors.ts +27 -0
  309. package/src/util/process-tree.ts +19 -0
  310. package/src/util/short-title.ts +189 -0
  311. package/src/util/worker-compute.ts +83 -0
  312. package/src/workspace/byok-default-profile-ensure.ts +25 -15
  313. package/src/workspace/git-service.ts +79 -30
  314. package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
  315. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  316. package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
  317. package/src/workspace/migrations/registry.ts +6 -0
  318. package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
@@ -407,7 +407,14 @@ describe("routing identities", () => {
407
407
  "claude-opus-4-8",
408
408
  );
409
409
 
410
- expect(provider).toEqual({ name: "anthropic", tag: "managed" } as never);
410
+ expect(provider).toEqual({
411
+ name: "anthropic",
412
+ tag: "managed",
413
+ routeAttribution: {
414
+ connectionName: "vellum",
415
+ isManagedRoute: true,
416
+ },
417
+ } as never);
411
418
  expect(resolveProviderCalls[0]?.name).toBe("vellum");
412
419
  expect(resolveProviderOpts[0]?.providerOverride).toBe("anthropic");
413
420
  });
@@ -433,13 +440,47 @@ describe("routing identities", () => {
433
440
  "gpt-5.5",
434
441
  );
435
442
 
436
- expect(provider).toEqual({ name: "openai", tag: "subscription" } as never);
443
+ expect(provider).toEqual({
444
+ name: "openai",
445
+ tag: "subscription",
446
+ routeAttribution: {
447
+ connectionName: "chatgpt-subscription",
448
+ isManagedRoute: false,
449
+ },
450
+ } as never);
437
451
  expect(resolveProviderCalls[0]?.name).toBe("chatgpt-subscription");
438
452
  // No override needed: the subscription row itself carries provider
439
453
  // "openai", so the adapter resolves from the row.
440
454
  expect(resolveProviderCalls[0]?.provider).toBe("openai");
441
455
  });
442
456
 
457
+ // A second managed row is creatable under any non-reserved name: only the
458
+ // canonical name is refused at the create route, and `provider: "vellum"`
459
+ // derives platform auth. Managed-ness is the row, never its name.
460
+ test("a managed row under a non-canonical name carries managed route attribution", async () => {
461
+ fakeConnections.set("managed-openai", {
462
+ name: "managed-openai",
463
+ provider: "vellum",
464
+ auth: { type: "platform" },
465
+ });
466
+ fakeProviders.set("conn:managed-openai", {
467
+ name: "openai",
468
+ tag: "managed",
469
+ });
470
+
471
+ const provider = await tryResolveProviderForConnectionName(
472
+ "managed-openai",
473
+ { llm: {} } as never,
474
+ "openai",
475
+ "gpt-5.5",
476
+ );
477
+
478
+ expect(provider?.routeAttribution).toEqual({
479
+ connectionName: "managed-openai",
480
+ isManagedRoute: true,
481
+ });
482
+ });
483
+
443
484
  test("chatgpt identity with no subscription row throws not_found (never a silent fallback)", async () => {
444
485
  await expect(
445
486
  tryResolveProviderForConnectionName(
@@ -86,6 +86,10 @@ describe("vellum connection mismatch handling", () => {
86
86
  "accounts/fireworks/models/kimi-k2p5",
87
87
  );
88
88
  expect(provider).not.toBeNull();
89
+ expect(provider?.routeAttribution).toEqual({
90
+ connectionName: "vellum",
91
+ isManagedRoute: true,
92
+ });
89
93
  expect(resolveCalls).toHaveLength(1);
90
94
  expect(resolveCalls[0].connection.name).toBe("vellum");
91
95
  expect(resolveCalls[0].opts.providerOverride).toBe("fireworks");
@@ -129,6 +133,10 @@ describe("vellum connection mismatch handling", () => {
129
133
  "anthropic/claude-fable-5",
130
134
  );
131
135
  expect(provider).not.toBeNull();
136
+ expect(provider?.routeAttribution).toEqual({
137
+ connectionName: "openrouter-personal",
138
+ isManagedRoute: false,
139
+ });
132
140
  expect(resolveCalls).toHaveLength(1);
133
141
  expect(resolveCalls[0].connection.name).toBe("openrouter-personal");
134
142
  expect(resolveCalls[0].opts.providerOverride).toBeUndefined();
@@ -21,9 +21,16 @@
21
21
 
22
22
  import { AsyncLocalStorage } from "node:async_hooks";
23
23
 
24
- import { resolveCallSiteConfig } from "../config/llm-resolver.js";
24
+ import {
25
+ resolveCallSiteConfig,
26
+ resolveCallSiteConfigWithProfile,
27
+ } from "../config/llm-resolver.js";
25
28
  import { getConfig } from "../config/loader.js";
26
29
  import { getDb } from "../persistence/db-connection.js";
30
+ import {
31
+ ProviderError,
32
+ type ProviderRouteAttribution,
33
+ } from "../util/errors.js";
27
34
  import { getLogger } from "../util/logger.js";
28
35
  import {
29
36
  describeSubscriptionModelIncompatibility,
@@ -31,6 +38,7 @@ import {
31
38
  } from "./connection-model-compat.js";
32
39
  import {
33
40
  ConnectionResolutionError,
41
+ isManagedConnectionRoute,
34
42
  resolveRoutingIdentity,
35
43
  tryResolveProviderForConnectionName,
36
44
  } from "./connection-resolution.js";
@@ -46,6 +54,14 @@ import type {
46
54
  } from "./types.js";
47
55
 
48
56
  const log = getLogger("providers/call-site-routing");
57
+
58
+ interface SelectedProviderRoute {
59
+ provider: Provider;
60
+ connectionName?: string;
61
+ profileName?: string;
62
+ isManagedRoute?: boolean;
63
+ }
64
+
49
65
  export class CallSiteRoutingProvider implements Provider {
50
66
  public readonly tokenEstimationProvider?: string;
51
67
  // Forward native web-search capability so it survives the wrapper chain
@@ -99,6 +115,7 @@ export class CallSiteRoutingProvider implements Provider {
99
115
  expectedProvider: string,
100
116
  model: string | undefined,
101
117
  ) => Promise<Provider | null>,
118
+ private readonly defaultRouteAttribution?: ProviderRouteAttribution,
102
119
  ) {
103
120
  this.tokenEstimationProvider = defaultProvider.tokenEstimationProvider;
104
121
  this.supportsNativeWebSearch = defaultProvider.supportsNativeWebSearch;
@@ -112,11 +129,30 @@ export class CallSiteRoutingProvider implements Provider {
112
129
  messages: Message[],
113
130
  options?: SendMessageOptions,
114
131
  ): Promise<ProviderResponse> {
115
- const target = await this.selectProvider(options);
132
+ const selectedRoute = await this.selectProvider(options);
133
+ const target = selectedRoute.provider;
116
134
  const isRouted = target !== this.defaultProvider;
117
135
 
118
136
  const doSend = async (): Promise<ProviderResponse> => {
119
- const response = await target.sendMessage(messages, options);
137
+ let response: ProviderResponse;
138
+ try {
139
+ response = await target.sendMessage(messages, options);
140
+ } catch (error) {
141
+ if (error instanceof ProviderError) {
142
+ error.attachRouteAttribution({
143
+ ...(selectedRoute.connectionName
144
+ ? { connectionName: selectedRoute.connectionName }
145
+ : {}),
146
+ ...(selectedRoute.profileName
147
+ ? { profileName: selectedRoute.profileName }
148
+ : {}),
149
+ ...(selectedRoute.isManagedRoute !== undefined
150
+ ? { isManagedRoute: selectedRoute.isManagedRoute }
151
+ : {}),
152
+ });
153
+ }
154
+ throw error;
155
+ }
120
156
  // Also stamp actualProvider on the response so that handleUsage
121
157
  // (which reads event.actualProvider, not provider.name) attributes
122
158
  // the call to the right provider.
@@ -199,10 +235,10 @@ export class CallSiteRoutingProvider implements Provider {
199
235
  */
200
236
  private async selectProvider(
201
237
  options?: SendMessageOptions,
202
- ): Promise<Provider> {
238
+ ): Promise<SelectedProviderRoute> {
203
239
  const callSite = options?.config?.callSite;
204
240
  if (!callSite) {
205
- return this.defaultProvider;
241
+ return this.defaultRoute();
206
242
  }
207
243
 
208
244
  const overrideProfile = options?.config?.overrideProfile;
@@ -213,11 +249,15 @@ export class CallSiteRoutingProvider implements Provider {
213
249
  // request params.
214
250
  const forceOverrideProfile = options?.config?.forceOverrideProfile;
215
251
  const selectionSeed = options?.config?.selectionSeed;
216
- const resolved = resolveCallSiteConfig(callSite, getConfig().llm, {
217
- overrideProfile,
218
- forceOverrideProfile,
219
- selectionSeed,
220
- });
252
+ const { config: resolved, profileName } = resolveCallSiteConfigWithProfile(
253
+ callSite,
254
+ getConfig().llm,
255
+ {
256
+ overrideProfile,
257
+ forceOverrideProfile,
258
+ selectionSeed,
259
+ },
260
+ );
221
261
 
222
262
  let connectionName = resolved.provider_connection;
223
263
 
@@ -267,13 +307,27 @@ export class CallSiteRoutingProvider implements Provider {
267
307
  resolved.model,
268
308
  );
269
309
  if (connectionProvider) {
310
+ const actualRoute = {
311
+ connectionName:
312
+ connectionProvider.routeAttribution?.connectionName ??
313
+ connectionName,
314
+ isManagedRoute:
315
+ connectionProvider.routeAttribution?.isManagedRoute ??
316
+ isManagedConnectionRoute(connectionName),
317
+ };
270
318
  // The connection whose credential the call authenticates with is only
271
319
  // known here, and diagnostics for a failed request are unactionable
272
320
  // without it ("which key was this?"). Recorded once the adapter exists,
273
321
  // so a connection that fell back to the default transport is not
274
322
  // reported as the one that signed the request.
275
- recordProviderRequestDiagnostics({ connection_name: connectionName });
276
- return connectionProvider;
323
+ recordProviderRequestDiagnostics({
324
+ connection_name: actualRoute.connectionName,
325
+ });
326
+ return {
327
+ provider: connectionProvider,
328
+ ...actualRoute,
329
+ ...(profileName ? { profileName } : {}),
330
+ };
277
331
  }
278
332
  // Soft credential failure: the routed connection yielded no usable
279
333
  // adapter and dispatch is landing on the default transport, which may
@@ -289,11 +343,11 @@ export class CallSiteRoutingProvider implements Provider {
289
343
  },
290
344
  "Routed connection yielded no adapter — falling back to the default transport",
291
345
  );
292
- return this.defaultProvider;
346
+ return this.defaultRoute(profileName);
293
347
  }
294
348
 
295
349
  if (resolved.provider === this.defaultProvider.name) {
296
- return this.defaultProvider;
350
+ return this.defaultRoute(profileName);
297
351
  }
298
352
 
299
353
  if (autoResolveCandidates) {
@@ -317,6 +371,14 @@ export class CallSiteRoutingProvider implements Provider {
317
371
  `call-site "${callSite}" resolves to provider "${resolved.provider}" but no provider_connection is set — alternate-provider routing requires a connection`,
318
372
  );
319
373
  }
374
+
375
+ private defaultRoute(profileName?: string): SelectedProviderRoute {
376
+ return {
377
+ provider: this.defaultProvider,
378
+ ...this.defaultRouteAttribution,
379
+ ...(profileName ? { profileName } : {}),
380
+ };
381
+ }
320
382
  }
321
383
 
322
384
  /**
@@ -341,5 +403,6 @@ export function wrapWithCallSiteRouting(
341
403
  expectedProvider,
342
404
  model,
343
405
  ),
406
+ base.routeAttribution,
344
407
  );
345
408
  }
@@ -132,11 +132,12 @@ export async function tryResolveProviderForConnectionName(
132
132
  connectionName === VELLUM_MANAGED_CONNECTION_NAME &&
133
133
  !isVellumManagedConnection(connection)
134
134
  ) {
135
- return resolveThroughPlatform(
135
+ const provider = await resolveThroughPlatform(
136
136
  config,
137
137
  expectedProvider ?? declaredProvider,
138
138
  model,
139
139
  );
140
+ return attachProviderRoute(provider, canonicalVellumConnection());
140
141
  }
141
142
  // The provider-agnostic Vellum-managed connection carries only the `vellum`
142
143
  // sentinel, so the usual `connection.provider === expectedProvider` equality
@@ -221,10 +222,11 @@ export async function tryResolveProviderForConnectionName(
221
222
  // catch is specifically for in-flight failures that should not take
222
223
  // dispatch offline.
223
224
  try {
224
- return await resolveProviderFromConnection(connection, config, {
225
+ const provider = await resolveProviderFromConnection(connection, config, {
225
226
  model,
226
227
  providerOverride: isVellumRoute ? expectedProvider : undefined,
227
228
  });
229
+ return attachProviderRoute(provider, connection);
228
230
  } catch (err) {
229
231
  log.warn(
230
232
  { err, connectionName },
@@ -234,6 +236,44 @@ export async function tryResolveProviderForConnectionName(
234
236
  }
235
237
  }
236
238
 
239
+ function attachProviderRoute(
240
+ provider: Provider | null,
241
+ connection: { name: string; provider: string; auth: { type: string } },
242
+ ): Provider | null {
243
+ if (provider) {
244
+ provider.routeAttribution = {
245
+ connectionName: connection.name,
246
+ isManagedRoute: isVellumManagedConnection(connection),
247
+ };
248
+ }
249
+ return provider;
250
+ }
251
+
252
+ /**
253
+ * Whether a route through this connection name is Vellum-managed
254
+ * (platform-billed), for callers that hold only the name. Returns undefined
255
+ * when the row can't be read, so the caller can fall back rather than assert
256
+ * a BYOK route it never confirmed.
257
+ *
258
+ * The canonical name is always managed: a user-owned row claiming it is
259
+ * ignored and the route resolves through platform auth regardless (see
260
+ * `tryResolveProviderForConnectionName`).
261
+ */
262
+ export function isManagedConnectionRoute(
263
+ connectionName: string,
264
+ ): boolean | undefined {
265
+ if (connectionName === VELLUM_MANAGED_CONNECTION_NAME) {
266
+ return true;
267
+ }
268
+ let connection;
269
+ try {
270
+ connection = getConnection(getDb(), connectionName);
271
+ } catch {
272
+ return undefined;
273
+ }
274
+ return connection ? isVellumManagedConnection(connection) : undefined;
275
+ }
276
+
237
277
  /**
238
278
  * Resolve a managed route through platform auth without reading a connection
239
279
  * row. Used when the canonical `vellum` row is claimed by a user-owned
@@ -7,7 +7,33 @@ import {
7
7
  } from "../adapter-factory.js";
8
8
  import type { ProviderConnection, ResolvedAuth } from "../auth.js";
9
9
 
10
- describe("openai-compatible adapter factory", () => {
10
+ interface RetryOptions {
11
+ credentialSource?: string;
12
+ connectionName?: string;
13
+ refreshCredentialProvider?: () => Promise<unknown>;
14
+ }
15
+
16
+ /**
17
+ * Read the `RetryProvider` options the factory stamped. Walks the `inner`
18
+ * chain rather than assuming a fixed wrapper nesting, so reordering the
19
+ * wrappers fails a behavior assertion instead of this accessor.
20
+ */
21
+ function retryOptions(adapter: unknown): RetryOptions {
22
+ let node = adapter;
23
+ for (let depth = 0; node && depth < 8; depth++) {
24
+ const { options, inner } = node as {
25
+ options?: RetryOptions;
26
+ inner?: unknown;
27
+ };
28
+ if (options) {
29
+ return options;
30
+ }
31
+ node = inner;
32
+ }
33
+ throw new Error("no RetryProvider found in the adapter wrapper chain");
34
+ }
35
+
36
+ describe("adapter factory", () => {
11
37
  test("buildProviderAdapter returns an OpenAIChatCompletionsProvider", () => {
12
38
  const adapter = buildProviderAdapter("openai-compatible", {
13
39
  apiKey: "test-key",
@@ -71,6 +97,10 @@ describe("openai-compatible adapter factory", () => {
71
97
  });
72
98
 
73
99
  expect(adapter).not.toBeNull();
100
+ expect(retryOptions(adapter)).toMatchObject({
101
+ credentialSource: "no-auth",
102
+ connectionName: "my-vllm",
103
+ });
74
104
  });
75
105
 
76
106
  test("createAdapterFromConnection still rejects 'none' auth for keyed catalog providers", () => {
@@ -94,4 +124,100 @@ describe("openai-compatible adapter factory", () => {
94
124
 
95
125
  expect(adapter).toBeNull();
96
126
  });
127
+
128
+ test("attributes OAuth subscription credentials separately from API keys", () => {
129
+ const connection: ProviderConnection = {
130
+ name: "chatgpt-subscription",
131
+ provider: "openai",
132
+ auth: {
133
+ type: "oauth_subscription",
134
+ credential: "chatgpt-subscription-oauth",
135
+ },
136
+ label: "ChatGPT",
137
+ baseUrl: null,
138
+ models: null,
139
+ createdAt: Date.now(),
140
+ updatedAt: Date.now(),
141
+ isManaged: false,
142
+ };
143
+
144
+ const adapter = createAdapterFromConnection(
145
+ connection,
146
+ {
147
+ kind: "header",
148
+ headers: { Authorization: "Bearer test-token" },
149
+ },
150
+ { model: "gpt-5.4" },
151
+ );
152
+
153
+ expect(adapter).not.toBeNull();
154
+ expect(retryOptions(adapter)).toMatchObject({
155
+ credentialSource: "oauth-subscription",
156
+ connectionName: "chatgpt-subscription",
157
+ });
158
+ });
159
+
160
+ test("wires managed connections to reload rotated assistant credentials", () => {
161
+ const connection: ProviderConnection = {
162
+ name: "vellum",
163
+ provider: "vellum",
164
+ auth: { type: "platform" },
165
+ label: "Vellum",
166
+ baseUrl: null,
167
+ models: null,
168
+ createdAt: Date.now(),
169
+ updatedAt: Date.now(),
170
+ isManaged: true,
171
+ };
172
+
173
+ const adapter = createAdapterFromConnection(
174
+ connection,
175
+ {
176
+ kind: "header",
177
+ headers: { Authorization: "Bearer managed-key" },
178
+ baseUrl: "https://example.com/runtime-proxy/anthropic",
179
+ },
180
+ { model: "claude-opus-4-8", provider: "anthropic" },
181
+ );
182
+
183
+ expect(adapter).not.toBeNull();
184
+ const options = retryOptions(adapter);
185
+ expect(options).toMatchObject({
186
+ credentialSource: "vellum-managed",
187
+ connectionName: "vellum",
188
+ });
189
+ expect(typeof options.refreshCredentialProvider).toBe("function");
190
+ });
191
+
192
+ test("credential refresh declines when it cannot re-read a changed key", async () => {
193
+ const connection: ProviderConnection = {
194
+ name: "vellum",
195
+ provider: "vellum",
196
+ auth: { type: "platform" },
197
+ label: "Vellum",
198
+ baseUrl: null,
199
+ models: null,
200
+ createdAt: Date.now(),
201
+ updatedAt: Date.now(),
202
+ isManaged: true,
203
+ };
204
+
205
+ const adapter = createAdapterFromConnection(
206
+ connection,
207
+ {
208
+ kind: "header",
209
+ headers: { Authorization: "Bearer managed-key" },
210
+ baseUrl: "https://example.com/runtime-proxy/anthropic",
211
+ },
212
+ { model: "claude-opus-4-8", provider: "anthropic" },
213
+ );
214
+
215
+ // Guards the doubled-upstream-call regression: a refresh that cannot
216
+ // produce a credential different from the one that just failed must
217
+ // hand back nothing, so the retry loop surfaces the auth error instead
218
+ // of replaying the request against an identical key.
219
+ const refresh = retryOptions(adapter).refreshCredentialProvider;
220
+ expect(await refresh?.()).toBeNull();
221
+ expect(await refresh?.()).toBeNull();
222
+ });
97
223
  });
@@ -37,6 +37,7 @@ import { UsageTrackingProvider } from "../usage-tracking.js";
37
37
  import { VercelAIGatewayProvider } from "../vercel-ai-gateway/client.js";
38
38
  import type { ResolvedAuth } from "./auth.js";
39
39
  import type { ProviderConnection } from "./auth.js";
40
+ import { resolveAuth } from "./resolve-auth.js";
40
41
 
41
42
  /** Unified construction opts. Adapters ignore fields they don't consume. */
42
43
  export interface AdapterCreateOpts {
@@ -225,6 +226,74 @@ export function createAdapterFromConnection(
225
226
  */
226
227
  provider?: string;
227
228
  },
229
+ ): Provider | null {
230
+ const provider = opts.provider ?? connection.provider;
231
+ const adapter = buildConnectionAdapter(connection, resolvedAuth, opts);
232
+ if (!adapter) {
233
+ return null;
234
+ }
235
+
236
+ /**
237
+ * Re-read the managed credential after an auth rejection and rebuild the
238
+ * adapter around it, so a key rotated out-of-band is picked up without a
239
+ * restart. Returns `null` when the store hands back the same credential
240
+ * that just failed — otherwise a proxy 401 from any other cause (platform
241
+ * auth degraded, revoked account) would make every request pay a second
242
+ * doomed upstream call forever.
243
+ */
244
+ function makeCredentialRefresher(): () => Promise<Provider | null> {
245
+ let lastAuth = JSON.stringify(resolvedAuth);
246
+ return async (): Promise<Provider | null> => {
247
+ const refreshedAuth = await resolveAuth(connection.auth, provider, {
248
+ baseUrl: connection.baseUrl,
249
+ });
250
+ if (!refreshedAuth.ok) {
251
+ return null;
252
+ }
253
+ const nextAuth = JSON.stringify(refreshedAuth.resolved);
254
+ if (nextAuth === lastAuth) {
255
+ return null;
256
+ }
257
+ lastAuth = nextAuth;
258
+ return buildConnectionAdapter(connection, refreshedAuth.resolved, opts);
259
+ };
260
+ }
261
+
262
+ // Usage-attribution headers (`X-Vellum-*`) are only meaningful when the
263
+ // request is routed through the Vellum-managed proxy. They carry billing
264
+ // metadata for our own backend. Forwarding them to a third-party endpoint
265
+ // would leak internal Vellum metadata, so gate on the auth type:
266
+ // `platform` is the only auth that flows through our proxy.
267
+ const isManagedProxy = connection.auth.type === "platform";
268
+ return new UsageTrackingProvider(
269
+ new RetryProvider(adapter, {
270
+ forwardUsageAttributionHeaders: isManagedProxy,
271
+ credentialSource: isManagedProxy
272
+ ? "vellum-managed"
273
+ : connection.auth.type === "api_key"
274
+ ? "byok"
275
+ : connection.auth.type === "oauth_subscription"
276
+ ? "oauth-subscription"
277
+ : connection.auth.type === "none"
278
+ ? "no-auth"
279
+ : undefined,
280
+ connectionName: connection.name,
281
+ ...(isManagedProxy
282
+ ? { refreshCredentialProvider: makeCredentialRefresher() }
283
+ : {}),
284
+ }),
285
+ );
286
+ }
287
+
288
+ function buildConnectionAdapter(
289
+ connection: ProviderConnection,
290
+ resolvedAuth: ResolvedAuth,
291
+ opts: {
292
+ model: string;
293
+ streamTimeoutMs?: number;
294
+ useNativeWebSearch?: boolean;
295
+ provider?: string;
296
+ },
228
297
  ): Provider | null {
229
298
  const provider = opts.provider ?? connection.provider;
230
299
  const entry = PROVIDER_CATALOG.find((e) => e.id === provider);
@@ -264,16 +333,5 @@ export function createAdapterFromConnection(
264
333
  if (!adapter) {
265
334
  return null;
266
335
  }
267
-
268
- // Usage-attribution headers (`X-Vellum-*`) are only meaningful when the
269
- // request is routed through the Vellum-managed proxy — they carry billing
270
- // metadata for our own backend. Forwarding them to a third-party endpoint
271
- // would leak internal Vellum metadata, so gate on the auth type:
272
- // `platform` is the only auth that flows through our proxy.
273
- const isManagedProxy = connection.auth.type === "platform";
274
- return new UsageTrackingProvider(
275
- new RetryProvider(adapter, {
276
- forwardUsageAttributionHeaders: isManagedProxy,
277
- }),
278
- );
336
+ return adapter;
279
337
  }
@@ -926,17 +926,6 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
926
926
  supportsToolUse: true,
927
927
  pricing: { inputPer1mTokens: 0.3, outputPer1mTokens: 1.2 },
928
928
  },
929
- {
930
- id: "accounts/fireworks/models/minimax-m2p5",
931
- displayName: "MiniMax M2.5",
932
- contextWindowTokens: 196608,
933
- maxOutputTokens: 25000,
934
- supportsThinking: false,
935
- supportsCaching: false,
936
- supportsVision: false,
937
- supportsToolUse: true,
938
- pricing: { inputPer1mTokens: 0.3, outputPer1mTokens: 1.2 },
939
- },
940
929
  {
941
930
  id: "accounts/fireworks/models/deepseek-v4-pro",
942
931
  displayName: "DeepSeek V4 Pro",
@@ -9,45 +9,55 @@ const PROVIDER_DEFAULT_MODELS: Record<string, string> = Object.fromEntries(
9
9
  PROVIDER_CATALOG.map((entry) => [entry.id, entry.defaultModel]),
10
10
  );
11
11
 
12
+ // `cost-optimized` is the cheapest model a provider serves, `latency-optimized`
13
+ // the fastest to first token. On anthropic, ollama and fireworks the same model
14
+ // is both, so those columns repeat by construction rather than by oversight.
12
15
  const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
13
16
  anthropic: {
14
17
  balanced: "claude-sonnet-4-6",
18
+ "cost-optimized": "claude-haiku-4-5-20251001",
15
19
  "latency-optimized": "claude-haiku-4-5-20251001",
16
20
  "quality-optimized": "claude-fable-5",
17
21
  "vision-optimized": "claude-opus-4-6",
18
22
  },
19
23
  openai: {
20
24
  balanced: "gpt-5.4-mini",
25
+ "cost-optimized": "gpt-5.4-nano",
21
26
  "latency-optimized": "gpt-5.6-luna",
22
27
  "quality-optimized": "gpt-5.4",
23
28
  "vision-optimized": "gpt-5.4",
24
29
  },
25
30
  gemini: {
26
31
  balanced: "gemini-3-flash-preview",
32
+ "cost-optimized": "gemini-2.5-flash-lite",
27
33
  "latency-optimized": "gemini-3.1-flash-lite",
28
34
  "quality-optimized": "gemini-3.1-pro-preview",
29
35
  "vision-optimized": "gemini-3-flash-preview",
30
36
  },
31
37
  ollama: {
32
38
  balanced: "llama3.2",
39
+ "cost-optimized": "llama3.2",
33
40
  "latency-optimized": "llama3.2",
34
41
  "quality-optimized": "llama3.2",
35
42
  "vision-optimized": "llama3.2",
36
43
  },
37
44
  fireworks: {
38
45
  balanced: "accounts/fireworks/models/minimax-m3",
46
+ "cost-optimized": "accounts/fireworks/models/deepseek-v4-flash",
39
47
  "latency-optimized": "accounts/fireworks/models/deepseek-v4-flash",
40
48
  "quality-optimized": "accounts/fireworks/models/kimi-k2p6",
41
49
  "vision-optimized": "accounts/fireworks/models/kimi-k2p6",
42
50
  },
43
51
  openrouter: {
44
52
  balanced: "anthropic/claude-sonnet-4.6",
53
+ "cost-optimized": "deepseek/deepseek-v4-flash",
45
54
  "latency-optimized": "anthropic/claude-haiku-4.5",
46
55
  "quality-optimized": "anthropic/claude-fable-5",
47
56
  "vision-optimized": "anthropic/claude-opus-4.6",
48
57
  },
49
58
  "vercel-ai-gateway": {
50
59
  balanced: "anthropic/claude-sonnet-4.6",
60
+ "cost-optimized": "deepseek/deepseek-v4-flash",
51
61
  "latency-optimized": "anthropic/claude-haiku-4.5",
52
62
  "quality-optimized": "anthropic/claude-fable-5",
53
63
  "vision-optimized": "anthropic/claude-opus-4.6",
@@ -58,6 +68,7 @@ const FALLBACK_DEFAULT_MODEL = "claude-opus-4-8";
58
68
 
59
69
  const MODEL_INTENTS = new Set<ModelIntent>([
60
70
  "balanced",
71
+ "cost-optimized",
61
72
  "latency-optimized",
62
73
  "quality-optimized",
63
74
  "vision-optimized",
@@ -265,6 +265,14 @@ export async function initializeProviders(
265
265
  entry.id,
266
266
  new RetryProvider(adapter, {
267
267
  forwardUsageAttributionHeaders: source === "managed-proxy",
268
+ // A keyless provider booted without a key has no credential to blame,
269
+ // so don't let its rejections render as "update your personal key".
270
+ credentialSource:
271
+ source === "managed-proxy"
272
+ ? "vellum-managed"
273
+ : isKeyless && !apiKey
274
+ ? "no-auth"
275
+ : "byok",
268
276
  }),
269
277
  );
270
278
  routingSources.set(entry.id, source);