@vellumai/assistant 0.5.1 → 0.5.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 (338) hide show
  1. package/ARCHITECTURE.md +54 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop.test.ts +111 -0
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  8. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  9. package/src/__tests__/app-executors.test.ts +1 -291
  10. package/src/__tests__/app-git-history.test.ts +4 -4
  11. package/src/__tests__/app-routes-csp.test.ts +1 -0
  12. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  13. package/src/__tests__/attachments-store.test.ts +169 -21
  14. package/src/__tests__/attachments.test.ts +115 -1
  15. package/src/__tests__/btw-routes.test.ts +1 -0
  16. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  17. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  18. package/src/__tests__/checker.test.ts +54 -0
  19. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  21. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  22. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  23. package/src/__tests__/config-schema.test.ts +1 -1
  24. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  25. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  26. package/src/__tests__/conversation-attachments.test.ts +17 -19
  27. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  28. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  29. package/src/__tests__/conversation-error.test.ts +1 -1
  30. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  31. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  32. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  33. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  34. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  35. package/src/__tests__/conversation-queue.test.ts +36 -1
  36. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  37. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  38. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  39. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  40. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  41. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  42. package/src/__tests__/conversation-store.test.ts +24 -21
  43. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  44. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  45. package/src/__tests__/conversation-title-service.test.ts +137 -0
  46. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  47. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  48. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  50. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  52. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  53. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  54. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  55. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  56. package/src/__tests__/diagnostics-export.test.ts +70 -1
  57. package/src/__tests__/first-greeting.test.ts +80 -0
  58. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  59. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  60. package/src/__tests__/history-repair.test.ts +32 -10
  61. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  62. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  63. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  64. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  65. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  66. package/src/__tests__/media-generate-image.test.ts +47 -94
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  68. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  69. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  70. package/src/__tests__/migration-export-http.test.ts +3 -1
  71. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  72. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  73. package/src/__tests__/mime-builder.test.ts +3 -2
  74. package/src/__tests__/non-member-access-request.test.ts +12 -1
  75. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  76. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  77. package/src/__tests__/oauth-store.test.ts +115 -0
  78. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  79. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  80. package/src/__tests__/recording-handler.test.ts +17 -0
  81. package/src/__tests__/registry.test.ts +3 -8
  82. package/src/__tests__/relay-server.test.ts +1 -1
  83. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  84. package/src/__tests__/schema-transforms.test.ts +165 -5
  85. package/src/__tests__/server-history-render.test.ts +2 -2
  86. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  87. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  88. package/src/__tests__/starter-task-flow.test.ts +1 -0
  89. package/src/__tests__/suggestion-routes.test.ts +443 -0
  90. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  91. package/src/__tests__/swarm-recursion.test.ts +1 -0
  92. package/src/__tests__/swarm-tool.test.ts +1 -0
  93. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  94. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  95. package/src/__tests__/top-level-renderer.test.ts +22 -0
  96. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  97. package/src/__tests__/web-fetch.test.ts +6 -2
  98. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  99. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  100. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  101. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  102. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  103. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  104. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  105. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  106. package/src/agent/attachments.ts +27 -1
  107. package/src/agent/loop.ts +29 -1
  108. package/src/avatar/traits-png-sync.ts +80 -25
  109. package/src/bundler/app-bundler.ts +4 -4
  110. package/src/calls/call-domain.ts +1 -0
  111. package/src/calls/voice-session-bridge.ts +1 -0
  112. package/src/cli/commands/auth.ts +92 -0
  113. package/src/cli/commands/avatar.ts +7 -6
  114. package/src/cli/commands/config.ts +2 -0
  115. package/src/cli/commands/oauth/providers.ts +29 -0
  116. package/src/cli/program.ts +12 -0
  117. package/src/cli.ts +15 -48
  118. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  119. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  120. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  121. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  135. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  136. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  137. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  138. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  139. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  140. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  141. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  142. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  143. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  144. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  145. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  146. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  147. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  148. package/src/config/bundled-tool-registry.ts +2 -14
  149. package/src/config/feature-flag-registry.json +8 -0
  150. package/src/config/loader.ts +64 -0
  151. package/src/config/raw-config-utils.ts +30 -0
  152. package/src/config/schema-utils.ts +28 -7
  153. package/src/config/schema.ts +8 -0
  154. package/src/config/schemas/elevenlabs.ts +18 -0
  155. package/src/config/schemas/memory-lifecycle.ts +4 -2
  156. package/src/config/schemas/memory-storage.ts +1 -1
  157. package/src/config/schemas/services.ts +8 -6
  158. package/src/contacts/contact-store.ts +13 -6
  159. package/src/contacts/contacts-write.ts +0 -1
  160. package/src/context/window-manager.ts +13 -2
  161. package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
  162. package/src/daemon/conversation-agent-loop.ts +56 -19
  163. package/src/daemon/conversation-attachments.ts +18 -36
  164. package/src/daemon/conversation-error.ts +2 -1
  165. package/src/daemon/conversation-history.ts +18 -4
  166. package/src/daemon/conversation-lifecycle.ts +39 -15
  167. package/src/daemon/conversation-messaging.ts +70 -26
  168. package/src/daemon/conversation-process.ts +58 -34
  169. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  170. package/src/daemon/conversation-slash.ts +121 -256
  171. package/src/daemon/conversation-surfaces.ts +143 -20
  172. package/src/daemon/conversation-tool-setup.ts +0 -6
  173. package/src/daemon/conversation-workspace.ts +21 -1
  174. package/src/daemon/conversation.ts +51 -29
  175. package/src/daemon/first-greeting.ts +35 -0
  176. package/src/daemon/handlers/config-embeddings.ts +148 -0
  177. package/src/daemon/handlers/config-model.ts +71 -26
  178. package/src/daemon/handlers/conversations.ts +0 -23
  179. package/src/daemon/handlers/recording.ts +26 -21
  180. package/src/daemon/host-cu-proxy.ts +2 -2
  181. package/src/daemon/lifecycle.ts +106 -64
  182. package/src/daemon/message-protocol.ts +3 -0
  183. package/src/daemon/message-types/conversations.ts +19 -0
  184. package/src/daemon/message-types/messages.ts +1 -0
  185. package/src/daemon/message-types/shared.ts +2 -0
  186. package/src/daemon/message-types/surfaces.ts +2 -0
  187. package/src/daemon/message-types/upgrades.ts +23 -0
  188. package/src/daemon/server.ts +83 -12
  189. package/src/daemon/shutdown-handlers.ts +8 -5
  190. package/src/daemon/startup-error.ts +9 -0
  191. package/src/daemon/tool-side-effects.ts +11 -28
  192. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  193. package/src/instrument.ts +0 -4
  194. package/src/media/app-icon-generator.ts +2 -2
  195. package/src/memory/app-git-service.ts +28 -16
  196. package/src/memory/app-store.ts +230 -41
  197. package/src/memory/attachments-store.ts +558 -130
  198. package/src/memory/conversation-attention-store.ts +70 -0
  199. package/src/memory/conversation-crud.ts +442 -3
  200. package/src/memory/conversation-directories.ts +125 -0
  201. package/src/memory/conversation-disk-view.ts +390 -0
  202. package/src/memory/conversation-key-store.ts +17 -5
  203. package/src/memory/conversation-queries.ts +5 -1
  204. package/src/memory/conversation-title-service.ts +21 -49
  205. package/src/memory/db-init.ts +28 -0
  206. package/src/memory/embedding-backend.ts +42 -53
  207. package/src/memory/embedding-gemini.test.ts +4 -4
  208. package/src/memory/embedding-local.ts +1 -3
  209. package/src/memory/embedding-ollama.ts +1 -3
  210. package/src/memory/embedding-openai.ts +1 -3
  211. package/src/memory/indexer.ts +9 -7
  212. package/src/memory/items-extractor.ts +42 -13
  213. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  214. package/src/memory/job-handlers/embedding.test.ts +1 -4
  215. package/src/memory/llm-request-log-store.ts +100 -1
  216. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  217. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  218. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  219. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  220. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  221. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  222. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  223. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  224. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  225. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  226. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  227. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  228. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  229. package/src/memory/migrations/index.ts +7 -0
  230. package/src/memory/migrations/registry.ts +13 -0
  231. package/src/memory/retriever.test.ts +601 -2
  232. package/src/memory/retriever.ts +85 -9
  233. package/src/memory/schema/conversations.ts +6 -0
  234. package/src/memory/schema/infrastructure.ts +13 -7
  235. package/src/memory/schema/oauth.ts +6 -0
  236. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  237. package/src/notifications/copy-composer.ts +26 -0
  238. package/src/notifications/decision-engine.ts +14 -1
  239. package/src/notifications/emit-signal.ts +1 -1
  240. package/src/notifications/signal.ts +36 -0
  241. package/src/oauth/byo-connection.test.ts +1 -45
  242. package/src/oauth/byo-connection.ts +2 -8
  243. package/src/oauth/connect-orchestrator.ts +15 -11
  244. package/src/oauth/connection-resolver.test.ts +191 -0
  245. package/src/oauth/connection-resolver.ts +66 -38
  246. package/src/oauth/connection.ts +0 -1
  247. package/src/oauth/oauth-store.ts +97 -47
  248. package/src/oauth/platform-connection.test.ts +0 -1
  249. package/src/oauth/platform-connection.ts +11 -3
  250. package/src/oauth/seed-providers.ts +78 -3
  251. package/src/oauth/token-persistence.ts +16 -10
  252. package/src/permissions/checker.ts +71 -8
  253. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  254. package/src/providers/anthropic/client.ts +8 -1
  255. package/src/providers/failover.ts +4 -1
  256. package/src/providers/gemini/client.ts +50 -0
  257. package/src/providers/model-catalog.ts +92 -0
  258. package/src/providers/model-intents.ts +29 -20
  259. package/src/providers/openai/client.ts +49 -0
  260. package/src/providers/types.ts +2 -0
  261. package/src/runtime/access-request-helper.ts +16 -7
  262. package/src/runtime/auth/credential-service.ts +3 -1
  263. package/src/runtime/auth/route-policy.ts +14 -1
  264. package/src/runtime/btw-sidechain.ts +101 -0
  265. package/src/runtime/channel-reply-delivery.ts +17 -1
  266. package/src/runtime/http-router.ts +3 -1
  267. package/src/runtime/http-server.ts +196 -141
  268. package/src/runtime/http-types.ts +1 -0
  269. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  270. package/src/runtime/routes/access-request-decision.ts +41 -0
  271. package/src/runtime/routes/app-management-routes.ts +6 -3
  272. package/src/runtime/routes/app-routes.ts +7 -3
  273. package/src/runtime/routes/approval-routes.ts +1 -0
  274. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  275. package/src/runtime/routes/attachment-routes.ts +45 -15
  276. package/src/runtime/routes/btw-routes.ts +21 -61
  277. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  278. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  279. package/src/runtime/routes/conversation-routes.ts +222 -28
  280. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  281. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  282. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  283. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  284. package/src/runtime/routes/log-export-routes.ts +3 -0
  285. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  286. package/src/runtime/routes/memory-item-routes.ts +4 -0
  287. package/src/runtime/routes/migration-routes.ts +4 -1
  288. package/src/runtime/routes/oauth-apps.ts +291 -0
  289. package/src/runtime/routes/secret-routes.ts +28 -1
  290. package/src/runtime/routes/settings-routes.ts +14 -0
  291. package/src/runtime/routes/trace-event-routes.ts +4 -1
  292. package/src/schedule/schedule-store.ts +9 -21
  293. package/src/security/secure-keys.ts +21 -0
  294. package/src/signals/bash.ts +1 -1
  295. package/src/swarm/backend-claude-code.ts +3 -6
  296. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  297. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  298. package/src/tools/AGENTS.md +6 -10
  299. package/src/tools/apps/executors.ts +17 -232
  300. package/src/tools/claude-code/claude-code.ts +2 -3
  301. package/src/tools/credentials/vault.ts +7 -12
  302. package/src/tools/host-filesystem/read.ts +13 -10
  303. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  304. package/src/tools/schedule/list.ts +2 -7
  305. package/src/tools/schema-transforms.ts +5 -0
  306. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  307. package/src/tools/skills/execute.ts +1 -1
  308. package/src/tools/tool-manifest.ts +0 -6
  309. package/src/tools/ui-surface/definitions.ts +2 -2
  310. package/src/util/device-id.ts +28 -5
  311. package/src/util/platform.ts +6 -0
  312. package/src/util/pricing.ts +1 -0
  313. package/src/util/retry.ts +1 -3
  314. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  315. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  316. package/src/workspace/migrations/006-services-config.ts +5 -0
  317. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  318. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  319. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  320. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  321. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  322. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  323. package/src/workspace/migrations/registry.ts +10 -0
  324. package/src/workspace/top-level-renderer.ts +12 -0
  325. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  326. package/src/__tests__/asset-search-tool.test.ts +0 -536
  327. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  328. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  329. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  330. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  331. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  332. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  333. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  334. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  335. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  336. package/src/daemon/media-visibility-policy.ts +0 -59
  337. package/src/tools/assets/materialize.ts +0 -248
  338. package/src/tools/assets/search.ts +0 -400
@@ -5,9 +5,11 @@ import { seedProviders } from "./oauth-store.js";
5
5
  *
6
6
  * These values are upserted into the `oauth_providers` SQLite table on
7
7
  * every startup. Only Vellum implementation fields (authUrl, tokenUrl,
8
- * tokenEndpointAuthMethod, extraParams, callbackTransport, pingUrl) are
9
- * overwritten on subsequent startups user-customizable
10
- * fields (defaultScopes, scopePolicy, userinfoUrl, baseUrl) are only
8
+ * tokenEndpointAuthMethod, userinfoUrl, extraParams, callbackTransport,
9
+ * pingUrl, managedServiceConfigKey) and display metadata (displayName,
10
+ * description, dashboardUrl, clientIdPlaceholder, requiresClientSecret)
11
+ * are overwritten on subsequent startups — user-customizable
12
+ * fields (defaultScopes, scopePolicy, baseUrl) are only
11
13
  * written on initial insert and preserved across restarts.
12
14
  *
13
15
  * Code-side behavioral fields (identityVerifier, injectionTemplates,
@@ -32,6 +34,12 @@ const PROVIDER_SEED_DATA: Record<
32
34
  };
33
35
  extraParams?: Record<string, string>;
34
36
  callbackTransport?: string;
37
+ managedServiceConfigKey?: string;
38
+ displayName: string;
39
+ description: string;
40
+ dashboardUrl: string | null;
41
+ clientIdPlaceholder: string | null;
42
+ requiresClientSecret?: boolean;
35
43
  }
36
44
  > = {
37
45
  "integration:google": {
@@ -41,6 +49,10 @@ const PROVIDER_SEED_DATA: Record<
41
49
  userinfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
42
50
  pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
43
51
  baseUrl: "https://gmail.googleapis.com/gmail/v1/users/me",
52
+ displayName: "Google",
53
+ description: "Gmail, Calendar, and Contacts",
54
+ dashboardUrl: "https://console.cloud.google.com/apis/credentials",
55
+ clientIdPlaceholder: "123456789.apps.googleusercontent.com",
44
56
  defaultScopes: [
45
57
  "https://www.googleapis.com/auth/gmail.readonly",
46
58
  "https://www.googleapis.com/auth/gmail.modify",
@@ -60,6 +72,7 @@ const PROVIDER_SEED_DATA: Record<
60
72
  },
61
73
  extraParams: { access_type: "offline", prompt: "consent" },
62
74
  callbackTransport: "loopback",
75
+ managedServiceConfigKey: "google-oauth",
63
76
  },
64
77
 
65
78
  "integration:slack": {
@@ -68,6 +81,10 @@ const PROVIDER_SEED_DATA: Record<
68
81
  tokenUrl: "https://slack.com/api/oauth.v2.access",
69
82
  pingUrl: "https://slack.com/api/auth.test",
70
83
  baseUrl: "https://slack.com/api",
84
+ displayName: "Slack",
85
+ description: "Workspace messaging",
86
+ dashboardUrl: "https://api.slack.com/apps",
87
+ clientIdPlaceholder: null,
71
88
  defaultScopes: [
72
89
  "channels:read",
73
90
  "channels:history",
@@ -101,6 +118,10 @@ const PROVIDER_SEED_DATA: Record<
101
118
  tokenUrl: "https://api.notion.com/v1/oauth/token",
102
119
  pingUrl: "https://api.notion.com/v1/users/me",
103
120
  baseUrl: "https://api.notion.com",
121
+ displayName: "Notion",
122
+ description: "Pages and databases",
123
+ dashboardUrl: "https://www.notion.so/my-integrations",
124
+ clientIdPlaceholder: null,
104
125
  defaultScopes: [],
105
126
  scopePolicy: {
106
127
  allowAdditionalScopes: false,
@@ -118,6 +139,10 @@ const PROVIDER_SEED_DATA: Record<
118
139
  tokenUrl: "https://api.x.com/2/oauth2/token",
119
140
  pingUrl: "https://api.x.com/2/users/me",
120
141
  baseUrl: "https://api.x.com",
142
+ displayName: "Twitter",
143
+ description: "Posts and direct messages",
144
+ dashboardUrl: "https://developer.twitter.com/en/portal/dashboard",
145
+ clientIdPlaceholder: null,
121
146
  defaultScopes: [
122
147
  "tweet.read",
123
148
  "tweet.write",
@@ -139,6 +164,10 @@ const PROVIDER_SEED_DATA: Record<
139
164
  tokenUrl: "https://github.com/login/oauth/access_token",
140
165
  pingUrl: "https://api.github.com/user",
141
166
  baseUrl: "https://api.github.com",
167
+ displayName: "GitHub",
168
+ description: "Repositories and issues",
169
+ dashboardUrl: "https://github.com/settings/developers",
170
+ clientIdPlaceholder: null,
142
171
  defaultScopes: ["repo", "read:user", "notifications"],
143
172
  scopePolicy: {
144
173
  allowAdditionalScopes: true,
@@ -159,6 +188,10 @@ const PROVIDER_SEED_DATA: Record<
159
188
  tokenUrl: "https://api.linear.app/oauth/token",
160
189
  pingUrl: "https://api.linear.app/graphql",
161
190
  baseUrl: "https://api.linear.app",
191
+ displayName: "Linear",
192
+ description: "Issues and projects",
193
+ dashboardUrl: "https://linear.app/settings/api",
194
+ clientIdPlaceholder: null,
162
195
  defaultScopes: ["read", "write", "issues:create"],
163
196
  scopePolicy: {
164
197
  allowAdditionalScopes: false,
@@ -175,6 +208,10 @@ const PROVIDER_SEED_DATA: Record<
175
208
  tokenUrl: "https://accounts.spotify.com/api/token",
176
209
  pingUrl: "https://api.spotify.com/v1/me",
177
210
  baseUrl: "https://api.spotify.com/v1",
211
+ displayName: "Spotify",
212
+ description: "Music and playlists",
213
+ dashboardUrl: "https://developer.spotify.com/dashboard",
214
+ clientIdPlaceholder: null,
178
215
  defaultScopes: [
179
216
  "user-read-playback-state",
180
217
  "user-modify-playback-state",
@@ -201,6 +238,10 @@ const PROVIDER_SEED_DATA: Record<
201
238
  tokenUrl: "https://todoist.com/oauth/access_token",
202
239
  pingUrl: "https://api.todoist.com/rest/v2/projects",
203
240
  baseUrl: "https://api.todoist.com/rest/v2",
241
+ displayName: "Todoist",
242
+ description: "Tasks and projects",
243
+ dashboardUrl: "https://developer.todoist.com/appconsole.html",
244
+ clientIdPlaceholder: null,
204
245
  defaultScopes: ["data:read_write"],
205
246
  scopePolicy: {
206
247
  allowAdditionalScopes: false,
@@ -216,6 +257,10 @@ const PROVIDER_SEED_DATA: Record<
216
257
  tokenUrl: "https://discord.com/api/v10/oauth2/token",
217
258
  pingUrl: "https://discord.com/api/v10/users/@me",
218
259
  baseUrl: "https://discord.com/api/v10",
260
+ displayName: "Discord",
261
+ description: "Servers and messages",
262
+ dashboardUrl: "https://discord.com/developers/applications",
263
+ clientIdPlaceholder: null,
219
264
  defaultScopes: [
220
265
  "identify",
221
266
  "guilds",
@@ -236,6 +281,10 @@ const PROVIDER_SEED_DATA: Record<
236
281
  tokenUrl: "https://api.dropboxapi.com/oauth2/token",
237
282
  pingUrl: "https://api.dropboxapi.com/2/users/get_current_account",
238
283
  baseUrl: "https://api.dropboxapi.com/2",
284
+ displayName: "Dropbox",
285
+ description: "Files and folders",
286
+ dashboardUrl: "https://www.dropbox.com/developers/apps",
287
+ clientIdPlaceholder: null,
239
288
  defaultScopes: [
240
289
  "files.metadata.read",
241
290
  "files.content.read",
@@ -257,6 +306,10 @@ const PROVIDER_SEED_DATA: Record<
257
306
  tokenUrl: "https://app.asana.com/-/oauth_token",
258
307
  pingUrl: "https://app.asana.com/api/1.0/users/me",
259
308
  baseUrl: "https://app.asana.com/api/1.0",
309
+ displayName: "Asana",
310
+ description: "Tasks and projects",
311
+ dashboardUrl: "https://app.asana.com/0/my-apps",
312
+ clientIdPlaceholder: null,
260
313
  defaultScopes: ["default"],
261
314
  scopePolicy: {
262
315
  allowAdditionalScopes: false,
@@ -272,6 +325,10 @@ const PROVIDER_SEED_DATA: Record<
272
325
  tokenUrl: "https://airtable.com/oauth2/v1/token",
273
326
  pingUrl: "https://api.airtable.com/v0/meta/whoami",
274
327
  baseUrl: "https://api.airtable.com/v0",
328
+ displayName: "Airtable",
329
+ description: "Bases and records",
330
+ dashboardUrl: "https://airtable.com/create/tokens",
331
+ clientIdPlaceholder: null,
275
332
  defaultScopes: [
276
333
  "data.records:read",
277
334
  "data.records:write",
@@ -292,6 +349,10 @@ const PROVIDER_SEED_DATA: Record<
292
349
  tokenUrl: "https://api.hubapi.com/oauth/v1/token",
293
350
  pingUrl: "https://api.hubapi.com/crm/v3/objects/contacts?limit=1",
294
351
  baseUrl: "https://api.hubapi.com",
352
+ displayName: "HubSpot",
353
+ description: "CRM contacts and deals",
354
+ dashboardUrl: "https://developers.hubspot.com/",
355
+ clientIdPlaceholder: null,
295
356
  defaultScopes: [
296
357
  "crm.objects.contacts.read",
297
358
  "crm.objects.contacts.write",
@@ -316,6 +377,10 @@ const PROVIDER_SEED_DATA: Record<
316
377
  tokenUrl: "https://api.figma.com/v1/oauth/token",
317
378
  pingUrl: "https://api.figma.com/v1/me",
318
379
  baseUrl: "https://api.figma.com/v1",
380
+ displayName: "Figma",
381
+ description: "Design files and comments",
382
+ dashboardUrl: "https://www.figma.com/developers/apps",
383
+ clientIdPlaceholder: null,
319
384
  defaultScopes: ["files:read", "file_comments:write"],
320
385
  scopePolicy: {
321
386
  allowAdditionalScopes: false,
@@ -335,6 +400,11 @@ const PROVIDER_SEED_DATA: Record<
335
400
  tokenUrl: "urn:manual-token",
336
401
  pingUrl: "https://slack.com/api/auth.test",
337
402
  baseUrl: "https://slack.com/api",
403
+ displayName: "Slack Channel",
404
+ description: "Channel bot token",
405
+ dashboardUrl: null,
406
+ clientIdPlaceholder: null,
407
+ requiresClientSecret: false,
338
408
  defaultScopes: [],
339
409
  scopePolicy: {
340
410
  allowAdditionalScopes: false,
@@ -348,6 +418,11 @@ const PROVIDER_SEED_DATA: Record<
348
418
  authUrl: "urn:manual-token",
349
419
  tokenUrl: "urn:manual-token",
350
420
  baseUrl: "https://api.telegram.org",
421
+ displayName: "Telegram",
422
+ description: "Bot messaging",
423
+ dashboardUrl: null,
424
+ clientIdPlaceholder: null,
425
+ requiresClientSecret: false,
351
426
  defaultScopes: [],
352
427
  scopePolicy: {
353
428
  allowAdditionalScopes: false,
@@ -28,9 +28,8 @@ import {
28
28
  import { runPostConnectHook } from "../tools/credentials/post-connect-hooks.js";
29
29
  import {
30
30
  createConnection,
31
+ getActiveConnection,
31
32
  getApp,
32
- getConnectionByProvider,
33
- getConnectionByProviderAndAccount,
34
33
  listActiveConnectionsByProvider,
35
34
  updateConnection,
36
35
  upsertApp,
@@ -59,8 +58,12 @@ export interface StoreOAuth2TokensParams {
59
58
  clientId: string;
60
59
  clientSecret?: string;
61
60
  userinfoUrl?: string;
62
- /** Fallback account info from an identity verifier (e.g. @username, email). */
63
- identityAccountInfo?: string;
61
+ /**
62
+ * Best-effort account identifier parsed from the provider's identity
63
+ * endpoint (e.g. email, @username, display name). The format varies by
64
+ * provider and may be undefined if the API call fails.
65
+ */
66
+ parsedAccountIdentifier?: string;
64
67
  /** Pre-resolved oauth_app ID — skips the upsertApp() call if provided. */
65
68
  oauthAppId?: string;
66
69
  }
@@ -94,6 +97,10 @@ export async function storeOAuth2Tokens(
94
97
  ? Date.now() + tokens.expiresIn * 1000
95
98
  : null;
96
99
 
100
+ // Account identifier parsing is best-effort. The format varies by provider
101
+ // (email for Google, username for GitHub, display name for Spotify, etc.)
102
+ // and may be undefined if the userinfo/identity API call fails or the
103
+ // required scope wasn't granted.
97
104
  let accountInfo: string | undefined;
98
105
  if (userinfoUrl && grantedScopes.some((s) => s.includes("userinfo"))) {
99
106
  try {
@@ -109,7 +116,7 @@ export async function storeOAuth2Tokens(
109
116
  }
110
117
  }
111
118
 
112
- const resolvedAccountInfo = accountInfo ?? params.identityAccountInfo;
119
+ const resolvedAccountInfo = accountInfo ?? params.parsedAccountIdentifier;
113
120
 
114
121
  // -------------------------------------------------------------------
115
122
  // SQLite oauth_app + oauth_connection + new-format secure keys
@@ -144,12 +151,11 @@ export async function storeOAuth2Tokens(
144
151
  // lookup so that re-auth without userinfo still updates the right row.
145
152
  // However, treat provider-only matches as ambiguous when multiple active
146
153
  // connections exist to avoid overwriting the wrong account's tokens.
147
- let existingConn: ReturnType<typeof getConnectionByProvider>;
154
+ let existingConn: ReturnType<typeof getActiveConnection>;
148
155
  if (resolvedAccountInfo) {
149
- existingConn = getConnectionByProviderAndAccount(
150
- service,
151
- resolvedAccountInfo,
152
- );
156
+ existingConn = getActiveConnection(service, {
157
+ account: resolvedAccountInfo,
158
+ });
153
159
  } else {
154
160
  const activeConns = listActiveConnectionsByProvider(service);
155
161
  // Only reuse the provider-only match when it's unambiguous (single connection).
@@ -144,6 +144,7 @@ const LOW_RISK_PROGRAMS = new Set([
144
144
  "du",
145
145
  "df",
146
146
  "assistant",
147
+ "vellum",
147
148
  ]);
148
149
 
149
150
  // High-risk shell programs / patterns
@@ -197,6 +198,32 @@ const LOW_RISK_GIT_SUBCOMMANDS = new Set([
197
198
  "reflog",
198
199
  ]);
199
200
 
201
+ // Mutating assistant/vellum CLI subcommands that should be escalated to Medium
202
+ // risk. Most assistant/vellum subcommands are read-only and stay Low risk.
203
+ // This mirrors the git subcommand pattern — only known mutating operations
204
+ // get escalated.
205
+ const MEDIUM_RISK_CLI_SUBCOMMANDS = new Set([
206
+ "credentials",
207
+ "config",
208
+ "bash",
209
+ "trust",
210
+ "autonomy",
211
+ "contacts",
212
+ "mcp",
213
+ "keys",
214
+ "wake",
215
+ "sleep",
216
+ "hatch",
217
+ "retire",
218
+ "clean",
219
+ "setup",
220
+ "upgrade",
221
+ "recover",
222
+ "login",
223
+ "use",
224
+ "pair",
225
+ ]);
226
+
200
227
  // Commands that wrap another program — the real program appears as the first
201
228
  // non-flag argument. When one of these is the segment program we look through
202
229
  // its args to find the effective program (e.g. `env curl …` → curl).
@@ -219,15 +246,40 @@ const WRAPPER_PROGRAMS = new Set([
219
246
  // value of -u) as the wrapped program instead of `echo`.
220
247
  const ENV_VALUE_FLAGS = new Set(["-u", "--unset", "-C", "--chdir"]);
221
248
 
249
+ // `git` global flags that consume the next positional argument as their value.
250
+ // Without this, `git -C status commit` would incorrectly identify `status`
251
+ // (the directory path) as the subcommand instead of `commit`.
252
+ const GIT_VALUE_FLAGS = new Set([
253
+ "-C",
254
+ "-c",
255
+ "--git-dir",
256
+ "--work-tree",
257
+ "--namespace",
258
+ "--super-prefix",
259
+ "--config-env",
260
+ ]);
261
+
222
262
  /**
223
- * Return the first non-flag argument from an argument list.
224
- * Flags are arguments that start with `-`. This is used to skip global
225
- * options (e.g. `--verbose`, `-h`) when extracting the subcommand from
226
- * CLIs like `git`, `vellum`, and `assistant`.
263
+ * Return the first non-flag argument from an argument list, optionally
264
+ * skipping value-taking flags. Flags are arguments that start with `-`.
265
+ * This is used to skip global options (e.g. `--verbose`, `-h`, `-C <path>`)
266
+ * when extracting the subcommand from CLIs like `git`, `vellum`, and
267
+ * `assistant`.
268
+ *
269
+ * When `valueFlags` is provided, any flag in that set causes the next
270
+ * argument to be skipped as well (it is the flag's value, not a positional).
227
271
  */
228
- function firstPositionalArg(args: string[]): string | undefined {
229
- for (const arg of args) {
230
- if (!arg.startsWith("-")) return arg;
272
+ function firstPositionalArg(
273
+ args: string[],
274
+ valueFlags?: Set<string>,
275
+ ): string | undefined {
276
+ for (let i = 0; i < args.length; i++) {
277
+ const arg = args[i];
278
+ if (arg.startsWith("-")) {
279
+ if (valueFlags?.has(arg)) i++; // skip the next arg (the flag's value)
280
+ continue;
281
+ }
282
+ return arg;
231
283
  }
232
284
  return undefined;
233
285
  }
@@ -652,7 +704,7 @@ async function classifyRiskUncached(
652
704
  }
653
705
 
654
706
  if (prog === "git") {
655
- const subcommand = firstPositionalArg(seg.args);
707
+ const subcommand = firstPositionalArg(seg.args, GIT_VALUE_FLAGS);
656
708
  if (subcommand && LOW_RISK_GIT_SUBCOMMANDS.has(subcommand)) {
657
709
  // Stay at current risk
658
710
  continue;
@@ -662,6 +714,17 @@ async function classifyRiskUncached(
662
714
  continue;
663
715
  }
664
716
 
717
+ if (prog === "vellum" || prog === "assistant") {
718
+ const subcommand = firstPositionalArg(seg.args);
719
+ if (subcommand && MEDIUM_RISK_CLI_SUBCOMMANDS.has(subcommand)) {
720
+ // Known mutating subcommands are medium
721
+ maxRisk = RiskLevel.Medium;
722
+ continue;
723
+ }
724
+ // Read-only / unknown subcommands stay at current risk
725
+ continue;
726
+ }
727
+
665
728
  if (!LOW_RISK_PROGRAMS.has(prog)) {
666
729
  // Unknown program → medium
667
730
  if (maxRisk === RiskLevel.Low) {
@@ -208,6 +208,8 @@ Once you've completed Phase 1 and made reasonable progress through Phase 2, you'
208
208
 
209
209
  If you still haven't shown the two suggestions (Phase 2 step 4), do that before wrapping.
210
210
 
211
+ When you're confident onboarding is complete, delete `BOOTSTRAP.md` so it doesn't re-trigger on the next conversation.
212
+
211
213
  ---
212
214
 
213
215
  _Good luck out there. Make it count._
@@ -16,6 +16,9 @@ import type {
16
16
 
17
17
  const log = getLogger("anthropic-client");
18
18
 
19
+ /** Validation-specific timeout (10s) so a stalled network doesn't block key submission. */
20
+ const VALIDATION_TIMEOUT_MS = 10_000;
21
+
19
22
  /**
20
23
  * Validate an Anthropic API key by making a lightweight GET /v1/models call.
21
24
  * Returns `{ valid: true }` on success or `{ valid: false, reason: string }` on failure.
@@ -24,7 +27,11 @@ export async function validateAnthropicApiKey(
24
27
  apiKey: string,
25
28
  ): Promise<{ valid: true } | { valid: false; reason: string }> {
26
29
  try {
27
- const client = new Anthropic({ apiKey });
30
+ const client = new Anthropic({
31
+ apiKey,
32
+ timeout: VALIDATION_TIMEOUT_MS,
33
+ maxRetries: 0,
34
+ });
28
35
  await client.models.list({ limit: 1 });
29
36
  return { valid: true };
30
37
  } catch (error) {
@@ -133,7 +133,10 @@ export class FailoverProvider implements Provider {
133
133
  );
134
134
  health.unhealthySince = null;
135
135
  }
136
- return response;
136
+ return {
137
+ ...response,
138
+ actualProvider: response.actualProvider ?? provider.name,
139
+ };
137
140
  } catch (error) {
138
141
  lastError = error;
139
142
 
@@ -3,6 +3,7 @@ import { ApiError, GoogleGenAI } from "@google/genai";
3
3
 
4
4
  import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../prompts/cache-boundary.js";
5
5
  import { ProviderError } from "../../util/errors.js";
6
+ import { getLogger } from "../../util/logger.js";
6
7
  import { createStreamTimeout } from "../stream-timeout.js";
7
8
  import type {
8
9
  ContentBlock,
@@ -13,6 +14,55 @@ import type {
13
14
  ToolDefinition,
14
15
  } from "../types.js";
15
16
 
17
+ const log = getLogger("gemini-client");
18
+
19
+ /** Validation-specific timeout (10s) so a stalled network doesn't block key submission. */
20
+ const VALIDATION_TIMEOUT_MS = 10_000;
21
+
22
+ /**
23
+ * Validate a Gemini API key by making a lightweight models.list() call.
24
+ * Returns `{ valid: true }` on success or `{ valid: false, reason: string }` on failure.
25
+ */
26
+ export async function validateGeminiApiKey(
27
+ apiKey: string,
28
+ ): Promise<{ valid: true } | { valid: false; reason: string }> {
29
+ try {
30
+ const client = new GoogleGenAI({ apiKey });
31
+ await client.models.list({
32
+ config: {
33
+ pageSize: 1,
34
+ httpOptions: { timeout: VALIDATION_TIMEOUT_MS },
35
+ },
36
+ });
37
+ return { valid: true };
38
+ } catch (error) {
39
+ if (error instanceof ApiError) {
40
+ if (error.status === 401) {
41
+ return { valid: false, reason: "API key is invalid or expired." };
42
+ }
43
+ if (error.status === 403) {
44
+ return {
45
+ valid: false,
46
+ reason: `Gemini API error (${error.status}): ${error.message}`,
47
+ };
48
+ }
49
+ // Transient errors (429, 5xx, etc.) — validation is inconclusive,
50
+ // allow the key to be stored rather than blocking the user.
51
+ log.warn(
52
+ { status: error.status },
53
+ "Gemini API returned a transient error during key validation — allowing key storage",
54
+ );
55
+ return { valid: true };
56
+ }
57
+ // Network errors — validation is inconclusive, allow key storage.
58
+ log.warn(
59
+ { error: error instanceof Error ? error.message : String(error) },
60
+ "Network error during Gemini key validation — allowing key storage",
61
+ );
62
+ return { valid: true };
63
+ }
64
+ }
65
+
16
66
  export interface GeminiProviderOptions {
17
67
  streamTimeoutMs?: number;
18
68
  /** When set, routes requests through the managed proxy at this base URL. */
@@ -0,0 +1,92 @@
1
+ export interface CatalogModel {
2
+ id: string;
3
+ displayName: string;
4
+ }
5
+
6
+ export interface ProviderCatalogEntry {
7
+ id: string;
8
+ displayName: string;
9
+ models: CatalogModel[];
10
+ defaultModel: string;
11
+ apiKeyUrl?: string;
12
+ apiKeyPlaceholder?: string;
13
+ }
14
+
15
+ /** Single source of truth for all inference provider metadata and models. */
16
+ export const PROVIDER_CATALOG: ProviderCatalogEntry[] = [
17
+ {
18
+ id: "anthropic",
19
+ displayName: "Anthropic",
20
+ models: [
21
+ { id: "claude-opus-4-6", displayName: "Claude Opus 4.6" },
22
+ { id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" },
23
+ { id: "claude-haiku-4-5-20251001", displayName: "Claude Haiku 4.5" },
24
+ ],
25
+ defaultModel: "claude-opus-4-6",
26
+ apiKeyUrl: "https://console.anthropic.com/settings/keys",
27
+ apiKeyPlaceholder: "sk-ant-api03-...",
28
+ },
29
+ {
30
+ id: "openai",
31
+ displayName: "OpenAI",
32
+ models: [
33
+ { id: "gpt-5.4", displayName: "GPT-5.4" },
34
+ { id: "gpt-5.2", displayName: "GPT-5.2" },
35
+ { id: "gpt-5.4-mini", displayName: "GPT-5.4 Mini" },
36
+ { id: "gpt-5.4-nano", displayName: "GPT-5.4 Nano" },
37
+ ],
38
+ defaultModel: "gpt-5.4",
39
+ apiKeyUrl: "https://platform.openai.com/api-keys",
40
+ apiKeyPlaceholder: "sk-proj-...",
41
+ },
42
+ {
43
+ id: "gemini",
44
+ displayName: "Google Gemini",
45
+ models: [
46
+ { id: "gemini-3-flash", displayName: "Gemini 3 Flash" },
47
+ { id: "gemini-3-pro", displayName: "Gemini 3 Pro" },
48
+ ],
49
+ defaultModel: "gemini-3-flash",
50
+ apiKeyUrl: "https://aistudio.google.com/apikey",
51
+ apiKeyPlaceholder: "AIza...",
52
+ },
53
+ {
54
+ id: "ollama",
55
+ displayName: "Ollama",
56
+ models: [
57
+ { id: "llama3.2", displayName: "Llama 3.2" },
58
+ { id: "mistral", displayName: "Mistral" },
59
+ ],
60
+ defaultModel: "llama3.2",
61
+ },
62
+ {
63
+ id: "fireworks",
64
+ displayName: "Fireworks",
65
+ models: [
66
+ {
67
+ id: "accounts/fireworks/models/kimi-k2p5",
68
+ displayName: "Kimi K2.5",
69
+ },
70
+ ],
71
+ defaultModel: "accounts/fireworks/models/kimi-k2p5",
72
+ apiKeyUrl: "https://fireworks.ai/account/api-keys",
73
+ apiKeyPlaceholder: "fw_...",
74
+ },
75
+ {
76
+ id: "openrouter",
77
+ displayName: "OpenRouter",
78
+ models: [
79
+ { id: "x-ai/grok-4", displayName: "Grok 4" },
80
+ { id: "x-ai/grok-4.20-beta", displayName: "Grok 4.20 Beta" },
81
+ ],
82
+ defaultModel: "x-ai/grok-4",
83
+ apiKeyUrl: "https://openrouter.ai/keys",
84
+ apiKeyPlaceholder: "sk-or-v1-...",
85
+ },
86
+ ];
87
+
88
+ /** Check if a model ID is in the catalog for a given provider. */
89
+ export function isModelInCatalog(provider: string, modelId: string): boolean {
90
+ const entry = PROVIDER_CATALOG.find((p) => p.id === provider);
91
+ return entry?.models.some((m) => m.id === modelId) ?? false;
92
+ }
@@ -1,20 +1,16 @@
1
+ import { isModelInCatalog, PROVIDER_CATALOG } from "./model-catalog.js";
1
2
  import type { ModelIntent } from "./types.js";
2
3
 
3
- const PROVIDER_DEFAULT_MODELS = {
4
- anthropic: "claude-opus-4-6",
5
- openai: "gpt-5.2",
6
- gemini: "gemini-3-flash",
7
- ollama: "llama3.2",
8
- fireworks: "accounts/fireworks/models/kimi-k2p5",
9
- openrouter: "x-ai/grok-4",
10
- } as const;
11
-
12
- type KnownProviderName = keyof typeof PROVIDER_DEFAULT_MODELS;
4
+ /**
5
+ * Derived from PROVIDER_CATALOG — single source of truth for default models.
6
+ * Each provider's `defaultModel` in the catalog populates this map.
7
+ */
8
+ export const PROVIDER_DEFAULT_MODELS: Record<string, string> =
9
+ Object.fromEntries(
10
+ PROVIDER_CATALOG.map((entry) => [entry.id, entry.defaultModel]),
11
+ );
13
12
 
14
- const PROVIDER_MODEL_INTENTS: Record<
15
- KnownProviderName,
16
- Record<ModelIntent, string>
17
- > = {
13
+ const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
18
14
  anthropic: {
19
15
  "latency-optimized": "claude-haiku-4-5-20251001",
20
16
  "quality-optimized": "claude-opus-4-6",
@@ -47,29 +43,42 @@ const PROVIDER_MODEL_INTENTS: Record<
47
43
  },
48
44
  };
49
45
 
46
+ const FALLBACK_DEFAULT_MODEL = "claude-opus-4-6";
47
+
50
48
  const MODEL_INTENTS = new Set<ModelIntent>([
51
49
  "latency-optimized",
52
50
  "quality-optimized",
53
51
  "vision-optimized",
54
52
  ]);
55
53
 
54
+ // ── Consistency validation ───────────────────────────────────────────
55
+ // Eagerly verify that every model ID referenced by PROVIDER_MODEL_INTENTS
56
+ // exists in PROVIDER_CATALOG, catching drift at module-load time rather
57
+ // than at runtime when a user picks a model.
58
+ for (const [provider, intents] of Object.entries(PROVIDER_MODEL_INTENTS)) {
59
+ for (const [intent, modelId] of Object.entries(intents)) {
60
+ if (!isModelInCatalog(provider, modelId)) {
61
+ throw new Error(
62
+ `PROVIDER_MODEL_INTENTS[${provider}][${intent}] references model "${modelId}" ` +
63
+ `which is not in PROVIDER_CATALOG. Update model-catalog.ts or model-intents.ts.`,
64
+ );
65
+ }
66
+ }
67
+ }
68
+
56
69
  export function isModelIntent(value: unknown): value is ModelIntent {
57
70
  return typeof value === "string" && MODEL_INTENTS.has(value as ModelIntent);
58
71
  }
59
72
 
60
73
  export function getProviderDefaultModel(providerName: string): string {
61
- const knownProvider = providerName as KnownProviderName;
62
- return (
63
- PROVIDER_DEFAULT_MODELS[knownProvider] ?? PROVIDER_DEFAULT_MODELS.anthropic
64
- );
74
+ return PROVIDER_DEFAULT_MODELS[providerName] ?? FALLBACK_DEFAULT_MODEL;
65
75
  }
66
76
 
67
77
  export function resolveModelIntent(
68
78
  providerName: string,
69
79
  intent: ModelIntent,
70
80
  ): string {
71
- const knownProvider = providerName as KnownProviderName;
72
- const providerIntentModels = PROVIDER_MODEL_INTENTS[knownProvider];
81
+ const providerIntentModels = PROVIDER_MODEL_INTENTS[providerName];
73
82
  if (providerIntentModels) {
74
83
  return providerIntentModels[intent];
75
84
  }