@vellumai/assistant 0.5.1 → 0.5.3

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 (405) hide show
  1. package/ARCHITECTURE.md +163 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/docs/skills.md +100 -0
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop.test.ts +111 -0
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  8. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  9. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  10. package/src/__tests__/app-executors.test.ts +1 -291
  11. package/src/__tests__/app-git-history.test.ts +4 -4
  12. package/src/__tests__/app-routes-csp.test.ts +1 -0
  13. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  14. package/src/__tests__/attachments-store.test.ts +169 -21
  15. package/src/__tests__/attachments.test.ts +115 -1
  16. package/src/__tests__/btw-routes.test.ts +1 -0
  17. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  18. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  19. package/src/__tests__/checker.test.ts +54 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  22. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  23. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  24. package/src/__tests__/config-schema.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +156 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +297 -2
  27. package/src/__tests__/conversation-attachments.test.ts +17 -19
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  29. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  30. package/src/__tests__/conversation-error.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  32. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  33. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  34. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  35. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  36. package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
  37. package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
  38. package/src/__tests__/conversation-queue.test.ts +36 -1
  39. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  40. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  41. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  43. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  44. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  45. package/src/__tests__/conversation-store.test.ts +24 -21
  46. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  47. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  48. package/src/__tests__/conversation-title-service.test.ts +137 -0
  49. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  50. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  51. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  52. package/src/__tests__/conversation-wipe.test.ts +226 -0
  53. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  54. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  55. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  56. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  57. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  58. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  59. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  60. package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
  61. package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
  62. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
  63. package/src/__tests__/diagnostics-export.test.ts +70 -1
  64. package/src/__tests__/first-greeting.test.ts +80 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  66. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  67. package/src/__tests__/history-repair.test.ts +32 -10
  68. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  69. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  70. package/src/__tests__/inline-command-runner.test.ts +311 -0
  71. package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
  72. package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
  73. package/src/__tests__/list-messages-attachments.test.ts +96 -0
  74. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  75. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  76. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  77. package/src/__tests__/media-generate-image.test.ts +47 -94
  78. package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
  79. package/src/__tests__/memory-brief-time.test.ts +285 -0
  80. package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
  81. package/src/__tests__/memory-chunk-archive.test.ts +400 -0
  82. package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
  83. package/src/__tests__/memory-episode-archive.test.ts +370 -0
  84. package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
  85. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  86. package/src/__tests__/memory-observation-archive.test.ts +375 -0
  87. package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
  88. package/src/__tests__/memory-recall-quality.test.ts +7 -7
  89. package/src/__tests__/memory-reducer-store.test.ts +728 -0
  90. package/src/__tests__/memory-reducer-types.test.ts +699 -0
  91. package/src/__tests__/memory-reducer.test.ts +698 -0
  92. package/src/__tests__/memory-regressions.test.ts +6 -4
  93. package/src/__tests__/memory-simplified-config.test.ts +281 -0
  94. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  95. package/src/__tests__/migration-export-http.test.ts +3 -1
  96. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  97. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  98. package/src/__tests__/mime-builder.test.ts +3 -2
  99. package/src/__tests__/non-member-access-request.test.ts +12 -1
  100. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  101. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  102. package/src/__tests__/oauth-store.test.ts +115 -0
  103. package/src/__tests__/parse-identity-fields.test.ts +129 -0
  104. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  105. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  106. package/src/__tests__/recording-handler.test.ts +17 -0
  107. package/src/__tests__/registry.test.ts +3 -8
  108. package/src/__tests__/relay-server.test.ts +1 -1
  109. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  110. package/src/__tests__/schema-transforms.test.ts +165 -5
  111. package/src/__tests__/server-history-render.test.ts +2 -2
  112. package/src/__tests__/skill-load-inline-command.test.ts +598 -0
  113. package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
  114. package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
  115. package/src/__tests__/skills-transitive-hash.test.ts +333 -0
  116. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  117. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  118. package/src/__tests__/starter-task-flow.test.ts +1 -0
  119. package/src/__tests__/suggestion-routes.test.ts +443 -0
  120. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  121. package/src/__tests__/swarm-recursion.test.ts +1 -0
  122. package/src/__tests__/swarm-tool.test.ts +1 -0
  123. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  124. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  125. package/src/__tests__/top-level-renderer.test.ts +22 -0
  126. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  127. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
  128. package/src/__tests__/web-fetch.test.ts +6 -2
  129. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  130. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  131. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  132. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  133. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  134. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  135. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  136. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  137. package/src/agent/attachments.ts +27 -1
  138. package/src/agent/loop.ts +29 -1
  139. package/src/avatar/traits-png-sync.ts +80 -25
  140. package/src/bundler/app-bundler.ts +4 -4
  141. package/src/calls/call-domain.ts +1 -0
  142. package/src/calls/voice-session-bridge.ts +1 -0
  143. package/src/cli/commands/auth.ts +92 -0
  144. package/src/cli/commands/avatar.ts +7 -6
  145. package/src/cli/commands/config.ts +2 -0
  146. package/src/cli/commands/oauth/providers.ts +29 -0
  147. package/src/cli/program.ts +12 -0
  148. package/src/cli.ts +15 -48
  149. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  150. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  151. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  152. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  153. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  154. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  155. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  156. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  157. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  158. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  159. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  160. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  161. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  162. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  163. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  164. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  165. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  166. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  170. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  171. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  172. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  173. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  174. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  175. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  176. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  177. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  178. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  179. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  180. package/src/config/bundled-tool-registry.ts +2 -14
  181. package/src/config/feature-flag-registry.json +24 -0
  182. package/src/config/loader.ts +65 -0
  183. package/src/config/raw-config-utils.ts +58 -0
  184. package/src/config/schema-utils.ts +28 -7
  185. package/src/config/schema.ts +20 -0
  186. package/src/config/schemas/elevenlabs.ts +18 -0
  187. package/src/config/schemas/memory-lifecycle.ts +4 -2
  188. package/src/config/schemas/memory-simplified.ts +101 -0
  189. package/src/config/schemas/memory-storage.ts +1 -1
  190. package/src/config/schemas/memory.ts +4 -0
  191. package/src/config/schemas/services.ts +8 -6
  192. package/src/config/skills.ts +50 -4
  193. package/src/contacts/contact-store.ts +13 -6
  194. package/src/contacts/contacts-write.ts +0 -1
  195. package/src/context/window-manager.ts +13 -2
  196. package/src/daemon/conversation-agent-loop-handlers.ts +54 -8
  197. package/src/daemon/conversation-agent-loop.ts +127 -20
  198. package/src/daemon/conversation-attachments.ts +18 -36
  199. package/src/daemon/conversation-error.ts +2 -1
  200. package/src/daemon/conversation-history.ts +18 -4
  201. package/src/daemon/conversation-lifecycle.ts +50 -16
  202. package/src/daemon/conversation-messaging.ts +70 -26
  203. package/src/daemon/conversation-process.ts +58 -34
  204. package/src/daemon/conversation-runtime-assembly.ts +22 -38
  205. package/src/daemon/conversation-slash.ts +121 -256
  206. package/src/daemon/conversation-surfaces.ts +170 -24
  207. package/src/daemon/conversation-tool-setup.ts +0 -6
  208. package/src/daemon/conversation-workspace.ts +21 -1
  209. package/src/daemon/conversation.ts +69 -30
  210. package/src/daemon/first-greeting.ts +35 -0
  211. package/src/daemon/handlers/config-embeddings.ts +156 -0
  212. package/src/daemon/handlers/config-model.ts +62 -26
  213. package/src/daemon/handlers/conversations.ts +0 -23
  214. package/src/daemon/handlers/identity.ts +12 -1
  215. package/src/daemon/handlers/recording.ts +26 -21
  216. package/src/daemon/host-cu-proxy.ts +2 -2
  217. package/src/daemon/lifecycle.ts +115 -65
  218. package/src/daemon/message-protocol.ts +3 -0
  219. package/src/daemon/message-types/conversations.ts +18 -0
  220. package/src/daemon/message-types/messages.ts +1 -0
  221. package/src/daemon/message-types/shared.ts +2 -0
  222. package/src/daemon/message-types/surfaces.ts +2 -0
  223. package/src/daemon/message-types/upgrades.ts +23 -0
  224. package/src/daemon/server.ts +83 -12
  225. package/src/daemon/shutdown-handlers.ts +8 -5
  226. package/src/daemon/startup-error.ts +9 -0
  227. package/src/daemon/tool-side-effects.ts +11 -28
  228. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  229. package/src/followups/followup-store.ts +47 -1
  230. package/src/instrument.ts +0 -4
  231. package/src/media/app-icon-generator.ts +2 -2
  232. package/src/memory/app-git-service.ts +28 -16
  233. package/src/memory/app-store.ts +230 -41
  234. package/src/memory/archive-store.ts +400 -0
  235. package/src/memory/attachments-store.ts +558 -130
  236. package/src/memory/brief-formatting.ts +33 -0
  237. package/src/memory/brief-open-loops.ts +266 -0
  238. package/src/memory/brief-time.ts +161 -0
  239. package/src/memory/brief.ts +75 -0
  240. package/src/memory/conversation-attention-store.ts +70 -0
  241. package/src/memory/conversation-crud.ts +591 -8
  242. package/src/memory/conversation-directories.ts +125 -0
  243. package/src/memory/conversation-disk-view.ts +390 -0
  244. package/src/memory/conversation-key-store.ts +17 -5
  245. package/src/memory/conversation-queries.ts +5 -1
  246. package/src/memory/conversation-title-service.ts +21 -49
  247. package/src/memory/db-init.ts +40 -0
  248. package/src/memory/embedding-backend.ts +42 -53
  249. package/src/memory/embedding-gemini.test.ts +4 -4
  250. package/src/memory/embedding-local.ts +1 -3
  251. package/src/memory/embedding-ollama.ts +1 -3
  252. package/src/memory/embedding-openai.ts +1 -3
  253. package/src/memory/indexer.ts +114 -21
  254. package/src/memory/items-extractor.ts +42 -13
  255. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  256. package/src/memory/job-handlers/embedding.test.ts +2 -4
  257. package/src/memory/job-handlers/embedding.ts +83 -0
  258. package/src/memory/job-utils.ts +1 -1
  259. package/src/memory/jobs-store.ts +6 -0
  260. package/src/memory/jobs-worker.ts +12 -0
  261. package/src/memory/llm-request-log-store.ts +100 -1
  262. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  263. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  264. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  265. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  266. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  267. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  268. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  269. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  270. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  271. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  272. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  273. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  274. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  275. package/src/memory/migrations/185-memory-brief-state.ts +52 -0
  276. package/src/memory/migrations/186-memory-archive.ts +109 -0
  277. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
  278. package/src/memory/migrations/index.ts +10 -0
  279. package/src/memory/migrations/registry.ts +13 -0
  280. package/src/memory/qdrant-client.ts +23 -4
  281. package/src/memory/reducer-store.ts +271 -0
  282. package/src/memory/reducer-types.ts +99 -0
  283. package/src/memory/reducer.ts +453 -0
  284. package/src/memory/retriever.test.ts +601 -2
  285. package/src/memory/retriever.ts +85 -9
  286. package/src/memory/schema/conversations.ts +9 -0
  287. package/src/memory/schema/index.ts +2 -0
  288. package/src/memory/schema/infrastructure.ts +13 -7
  289. package/src/memory/schema/memory-archive.ts +121 -0
  290. package/src/memory/schema/memory-brief.ts +55 -0
  291. package/src/memory/schema/oauth.ts +6 -0
  292. package/src/memory/search/semantic.ts +17 -4
  293. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  294. package/src/notifications/copy-composer.ts +26 -0
  295. package/src/notifications/decision-engine.ts +14 -1
  296. package/src/notifications/emit-signal.ts +1 -1
  297. package/src/notifications/signal.ts +36 -0
  298. package/src/oauth/byo-connection.test.ts +1 -45
  299. package/src/oauth/byo-connection.ts +2 -8
  300. package/src/oauth/connect-orchestrator.ts +15 -11
  301. package/src/oauth/connection-resolver.test.ts +191 -0
  302. package/src/oauth/connection-resolver.ts +66 -38
  303. package/src/oauth/connection.ts +0 -1
  304. package/src/oauth/oauth-store.ts +99 -47
  305. package/src/oauth/platform-connection.test.ts +0 -1
  306. package/src/oauth/platform-connection.ts +11 -3
  307. package/src/oauth/seed-providers.ts +78 -3
  308. package/src/oauth/token-persistence.ts +16 -10
  309. package/src/permissions/checker.ts +160 -14
  310. package/src/permissions/defaults.ts +14 -0
  311. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  312. package/src/providers/anthropic/client.ts +8 -1
  313. package/src/providers/failover.ts +4 -1
  314. package/src/providers/gemini/client.ts +50 -0
  315. package/src/providers/model-catalog.ts +92 -0
  316. package/src/providers/model-intents.ts +29 -20
  317. package/src/providers/openai/client.ts +49 -0
  318. package/src/providers/types.ts +2 -0
  319. package/src/runtime/access-request-helper.ts +16 -7
  320. package/src/runtime/auth/credential-service.ts +3 -1
  321. package/src/runtime/auth/route-policy.ts +14 -1
  322. package/src/runtime/btw-sidechain.ts +101 -0
  323. package/src/runtime/channel-reply-delivery.ts +17 -1
  324. package/src/runtime/http-router.ts +3 -1
  325. package/src/runtime/http-server.ts +196 -141
  326. package/src/runtime/http-types.ts +1 -0
  327. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  328. package/src/runtime/routes/access-request-decision.ts +41 -0
  329. package/src/runtime/routes/app-management-routes.ts +6 -3
  330. package/src/runtime/routes/app-routes.ts +7 -3
  331. package/src/runtime/routes/approval-routes.ts +1 -0
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  333. package/src/runtime/routes/attachment-routes.ts +45 -15
  334. package/src/runtime/routes/btw-routes.ts +21 -61
  335. package/src/runtime/routes/conversation-management-routes.ts +74 -0
  336. package/src/runtime/routes/conversation-query-routes.ts +187 -10
  337. package/src/runtime/routes/conversation-routes.ts +269 -28
  338. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  339. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  340. package/src/runtime/routes/identity-routes.ts +2 -35
  341. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  342. package/src/runtime/routes/llm-context-normalization.ts +1212 -0
  343. package/src/runtime/routes/log-export-routes.ts +3 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  345. package/src/runtime/routes/memory-item-routes.ts +94 -5
  346. package/src/runtime/routes/migration-routes.ts +4 -1
  347. package/src/runtime/routes/oauth-apps.ts +291 -0
  348. package/src/runtime/routes/secret-routes.ts +30 -1
  349. package/src/runtime/routes/settings-routes.ts +14 -0
  350. package/src/runtime/routes/surface-action-routes.ts +68 -1
  351. package/src/runtime/routes/trace-event-routes.ts +4 -1
  352. package/src/schedule/schedule-store.ts +30 -21
  353. package/src/security/secure-keys.ts +21 -0
  354. package/src/signals/bash.ts +1 -1
  355. package/src/skills/inline-command-expansions.ts +204 -0
  356. package/src/skills/inline-command-render.ts +127 -0
  357. package/src/skills/inline-command-runner.ts +242 -0
  358. package/src/skills/transitive-version-hash.ts +88 -0
  359. package/src/swarm/backend-claude-code.ts +3 -6
  360. package/src/tasks/task-store.ts +43 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  362. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  363. package/src/tools/AGENTS.md +6 -10
  364. package/src/tools/apps/executors.ts +17 -232
  365. package/src/tools/claude-code/claude-code.ts +2 -3
  366. package/src/tools/credentials/vault.ts +7 -12
  367. package/src/tools/host-filesystem/read.ts +13 -10
  368. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  369. package/src/tools/permission-checker.ts +8 -1
  370. package/src/tools/schedule/list.ts +2 -7
  371. package/src/tools/schema-transforms.ts +5 -0
  372. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  373. package/src/tools/skills/execute.ts +1 -1
  374. package/src/tools/skills/load.ts +140 -6
  375. package/src/tools/tool-manifest.ts +0 -6
  376. package/src/tools/ui-surface/definitions.ts +2 -2
  377. package/src/util/device-id.ts +28 -5
  378. package/src/util/platform.ts +24 -0
  379. package/src/util/pricing.ts +1 -0
  380. package/src/util/retry.ts +1 -3
  381. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  382. package/src/workspace/migrations/006-services-config.ts +5 -0
  383. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  384. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  385. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  386. package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +24 -13
  387. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  388. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  389. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  390. package/src/workspace/migrations/registry.ts +11 -1
  391. package/src/workspace/top-level-renderer.ts +12 -0
  392. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  393. package/src/__tests__/asset-search-tool.test.ts +0 -536
  394. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  395. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  396. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  397. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  398. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  399. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  400. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  401. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  402. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  403. package/src/daemon/media-visibility-policy.ts +0 -59
  404. package/src/tools/assets/materialize.ts +0 -248
  405. package/src/tools/assets/search.ts +0 -400
@@ -2,12 +2,15 @@ import { createHash } from "node:crypto";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, resolve } from "node:path";
4
4
 
5
+ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
5
6
  import { getConfig } from "../config/loader.js";
6
- import { resolveSkillSelector } from "../config/skills.js";
7
+ import { loadSkillCatalog, resolveSkillSelector } from "../config/skills.js";
8
+ import { indexCatalogById } from "../skills/include-graph.js";
7
9
  import {
8
10
  isSkillSourcePath,
9
11
  normalizeFilePath,
10
12
  } from "../skills/path-classifier.js";
13
+ import { computeTransitiveSkillVersionHash } from "../skills/transitive-version-hash.js";
11
14
  import { computeSkillVersionHash } from "../skills/version-hash.js";
12
15
  import type { ManifestOverride } from "../tools/execution-target.js";
13
16
  import {
@@ -144,6 +147,7 @@ const LOW_RISK_PROGRAMS = new Set([
144
147
  "du",
145
148
  "df",
146
149
  "assistant",
150
+ "vellum",
147
151
  ]);
148
152
 
149
153
  // High-risk shell programs / patterns
@@ -197,6 +201,32 @@ const LOW_RISK_GIT_SUBCOMMANDS = new Set([
197
201
  "reflog",
198
202
  ]);
199
203
 
204
+ // Mutating assistant/vellum CLI subcommands that should be escalated to Medium
205
+ // risk. Most assistant/vellum subcommands are read-only and stay Low risk.
206
+ // This mirrors the git subcommand pattern — only known mutating operations
207
+ // get escalated.
208
+ const MEDIUM_RISK_CLI_SUBCOMMANDS = new Set([
209
+ "credentials",
210
+ "config",
211
+ "bash",
212
+ "trust",
213
+ "autonomy",
214
+ "contacts",
215
+ "mcp",
216
+ "keys",
217
+ "wake",
218
+ "sleep",
219
+ "hatch",
220
+ "retire",
221
+ "clean",
222
+ "setup",
223
+ "upgrade",
224
+ "recover",
225
+ "login",
226
+ "use",
227
+ "pair",
228
+ ]);
229
+
200
230
  // Commands that wrap another program — the real program appears as the first
201
231
  // non-flag argument. When one of these is the segment program we look through
202
232
  // its args to find the effective program (e.g. `env curl …` → curl).
@@ -219,15 +249,40 @@ const WRAPPER_PROGRAMS = new Set([
219
249
  // value of -u) as the wrapped program instead of `echo`.
220
250
  const ENV_VALUE_FLAGS = new Set(["-u", "--unset", "-C", "--chdir"]);
221
251
 
252
+ // `git` global flags that consume the next positional argument as their value.
253
+ // Without this, `git -C status commit` would incorrectly identify `status`
254
+ // (the directory path) as the subcommand instead of `commit`.
255
+ const GIT_VALUE_FLAGS = new Set([
256
+ "-C",
257
+ "-c",
258
+ "--git-dir",
259
+ "--work-tree",
260
+ "--namespace",
261
+ "--super-prefix",
262
+ "--config-env",
263
+ ]);
264
+
222
265
  /**
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`.
266
+ * Return the first non-flag argument from an argument list, optionally
267
+ * skipping value-taking flags. Flags are arguments that start with `-`.
268
+ * This is used to skip global options (e.g. `--verbose`, `-h`, `-C <path>`)
269
+ * when extracting the subcommand from CLIs like `git`, `vellum`, and
270
+ * `assistant`.
271
+ *
272
+ * When `valueFlags` is provided, any flag in that set causes the next
273
+ * argument to be skipped as well (it is the flag's value, not a positional).
227
274
  */
228
- function firstPositionalArg(args: string[]): string | undefined {
229
- for (const arg of args) {
230
- if (!arg.startsWith("-")) return arg;
275
+ function firstPositionalArg(
276
+ args: string[],
277
+ valueFlags?: Set<string>,
278
+ ): string | undefined {
279
+ for (let i = 0; i < args.length; i++) {
280
+ const arg = args[i];
281
+ if (arg.startsWith("-")) {
282
+ if (valueFlags?.has(arg)) i++; // skip the next arg (the flag's value)
283
+ continue;
284
+ }
285
+ return arg;
231
286
  }
232
287
  return undefined;
233
288
  }
@@ -300,6 +355,34 @@ function resolveSkillIdAndHash(
300
355
  }
301
356
  }
302
357
 
358
+ /**
359
+ * Check whether a skill (by id) has parsed inline command expansions.
360
+ * Returns false when the skill is not found in the catalog.
361
+ */
362
+ function hasInlineExpansions(skillId: string): boolean {
363
+ const catalog = loadSkillCatalog();
364
+ const skill = catalog.find((s) => s.id === skillId);
365
+ return (
366
+ skill?.inlineCommandExpansions != null &&
367
+ skill.inlineCommandExpansions.length > 0
368
+ );
369
+ }
370
+
371
+ /**
372
+ * Compute the transitive version hash for a skill, returning `undefined`
373
+ * when computation fails (missing includes, cycle, etc.). The permission
374
+ * layer falls back to the any-version candidate in that case.
375
+ */
376
+ function computeTransitiveHashSafe(skillId: string): string | undefined {
377
+ try {
378
+ const catalog = loadSkillCatalog();
379
+ const index = indexCatalogById(catalog);
380
+ return computeTransitiveSkillVersionHash(skillId, index);
381
+ } catch {
382
+ return undefined;
383
+ }
384
+ }
385
+
303
386
  function canonicalizeWebFetchUrl(parsed: URL): URL {
304
387
  parsed.hash = "";
305
388
  parsed.username = "";
@@ -381,13 +464,39 @@ async function buildCommandCandidates(
381
464
  targets.push("");
382
465
  } else {
383
466
  const resolved = resolveSkillIdAndHash(rawSelector);
384
- if (resolved && resolved.versionHash) {
385
- // Version-specific candidate lets rules pin to an exact skill version
386
- targets.push(`${resolved.id}@${resolved.versionHash}`);
467
+
468
+ // When the resolved skill contains inline command expansions and the
469
+ // feature flag is on, emit skill_load_dynamic: candidates so the
470
+ // higher-priority default ask rule catches them instead of falling
471
+ // through to the permissive skill_load:* allow rule.
472
+ const config = getConfig();
473
+ const inlineEnabled = isAssistantFeatureFlagEnabled(
474
+ "feature_flags.inline-skill-commands.enabled",
475
+ config,
476
+ );
477
+
478
+ if (resolved && inlineEnabled && hasInlineExpansions(resolved.id)) {
479
+ const transitiveHash = computeTransitiveHashSafe(resolved.id);
480
+ if (transitiveHash) {
481
+ targets.push(`skill_load_dynamic:${resolved.id}@${transitiveHash}`);
482
+ }
483
+ targets.push(`skill_load_dynamic:${resolved.id}`);
484
+ // Don't fall through to skill_load:* — dynamic skills use their own
485
+ // candidate namespace so the default ask rule applies.
486
+ } else {
487
+ if (resolved && resolved.versionHash) {
488
+ // Version-specific candidate lets rules pin to an exact skill version
489
+ targets.push(`${resolved.id}@${resolved.versionHash}`);
490
+ }
491
+ targets.push(rawSelector);
387
492
  }
388
- targets.push(rawSelector);
389
493
  }
390
- return [...new Set(targets)].map((target) => `${toolName}:${target}`);
494
+
495
+ // Dynamic candidates use skill_load_dynamic: prefix; normal ones use skill_load:
496
+ return [...new Set(targets)].map((target) => {
497
+ if (target.startsWith("skill_load_dynamic:")) return target;
498
+ return `${toolName}:${target}`;
499
+ });
391
500
  }
392
501
 
393
502
  if (
@@ -652,7 +761,7 @@ async function classifyRiskUncached(
652
761
  }
653
762
 
654
763
  if (prog === "git") {
655
- const subcommand = firstPositionalArg(seg.args);
764
+ const subcommand = firstPositionalArg(seg.args, GIT_VALUE_FLAGS);
656
765
  if (subcommand && LOW_RISK_GIT_SUBCOMMANDS.has(subcommand)) {
657
766
  // Stay at current risk
658
767
  continue;
@@ -662,6 +771,17 @@ async function classifyRiskUncached(
662
771
  continue;
663
772
  }
664
773
 
774
+ if (prog === "vellum" || prog === "assistant") {
775
+ const subcommand = firstPositionalArg(seg.args);
776
+ if (subcommand && MEDIUM_RISK_CLI_SUBCOMMANDS.has(subcommand)) {
777
+ // Known mutating subcommands are medium
778
+ maxRisk = RiskLevel.Medium;
779
+ continue;
780
+ }
781
+ // Read-only / unknown subcommands stay at current risk
782
+ continue;
783
+ }
784
+
665
785
  if (!LOW_RISK_PROGRAMS.has(prog)) {
666
786
  // Unknown program → medium
667
787
  if (maxRisk === RiskLevel.Low) {
@@ -1021,6 +1141,32 @@ function skillLoadAllowlistStrategy(
1021
1141
 
1022
1142
  if (rawSelector) {
1023
1143
  const resolved = resolveSkillIdAndHash(rawSelector);
1144
+
1145
+ // Check whether this is a dynamic (inline-command) skill load
1146
+ const config = getConfig();
1147
+ const inlineEnabled = isAssistantFeatureFlagEnabled(
1148
+ "feature_flags.inline-skill-commands.enabled",
1149
+ config,
1150
+ );
1151
+
1152
+ if (resolved && inlineEnabled && hasInlineExpansions(resolved.id)) {
1153
+ const transitiveHash = computeTransitiveHashSafe(resolved.id);
1154
+ const options: AllowlistOption[] = [];
1155
+ if (transitiveHash) {
1156
+ options.push({
1157
+ label: `${resolved.id}@${transitiveHash}`,
1158
+ description: "This exact version (pinned)",
1159
+ pattern: `skill_load_dynamic:${resolved.id}@${transitiveHash}`,
1160
+ });
1161
+ }
1162
+ options.push({
1163
+ label: resolved.id,
1164
+ description: "This skill (any version)",
1165
+ pattern: `skill_load_dynamic:${resolved.id}`,
1166
+ });
1167
+ return options;
1168
+ }
1169
+
1024
1170
  if (resolved && resolved.versionHash) {
1025
1171
  return [
1026
1172
  {
@@ -198,6 +198,19 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
198
198
  })),
199
199
  );
200
200
 
201
+ // Inline-command skill loads use a distinct candidate namespace
202
+ // (skill_load_dynamic:*) so they prompt by default instead of falling
203
+ // through to the permissive skill_load:* allow rule below. The higher
204
+ // priority ensures this rule wins when both could match.
205
+ const skillLoadDynamicRule: DefaultRuleTemplate = {
206
+ id: "default:ask-skill_load_dynamic-global",
207
+ tool: "skill_load",
208
+ pattern: "skill_load_dynamic:*",
209
+ scope: "everywhere",
210
+ decision: "ask",
211
+ priority: 200,
212
+ };
213
+
201
214
  const skillLoadRule: DefaultRuleTemplate = {
202
215
  id: "default:allow-skill_load-global",
203
216
  tool: "skill_load",
@@ -294,6 +307,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
294
307
  bootstrapDeleteRule,
295
308
  updatesDeleteRule,
296
309
  ...skillSourceMutationRules,
310
+ skillLoadDynamicRule,
297
311
  skillLoadRule,
298
312
  skillExecuteRule,
299
313
  browserNavigateRule,
@@ -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
  }
@@ -2,6 +2,7 @@ import OpenAI from "openai";
2
2
 
3
3
  import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../prompts/cache-boundary.js";
4
4
  import { ProviderError } from "../../util/errors.js";
5
+ import { getLogger } from "../../util/logger.js";
5
6
  import { extractRetryAfterMs } from "../../util/retry.js";
6
7
  import { escapeXmlAttr } from "../../util/xml.js";
7
8
  import { createStreamTimeout } from "../stream-timeout.js";
@@ -14,6 +15,54 @@ import type {
14
15
  ToolDefinition,
15
16
  } from "../types.js";
16
17
 
18
+ const log = getLogger("openai-client");
19
+
20
+ /** Validation-specific timeout (10s) so a stalled network doesn't block key submission. */
21
+ const VALIDATION_TIMEOUT_MS = 10_000;
22
+
23
+ /**
24
+ * Validate an OpenAI API key by making a lightweight GET /v1/models call.
25
+ * Returns `{ valid: true }` on success or `{ valid: false, reason: string }` on failure.
26
+ */
27
+ export async function validateOpenAIApiKey(
28
+ apiKey: string,
29
+ ): Promise<{ valid: true } | { valid: false; reason: string }> {
30
+ try {
31
+ const client = new OpenAI({
32
+ apiKey,
33
+ timeout: VALIDATION_TIMEOUT_MS,
34
+ maxRetries: 0,
35
+ });
36
+ await client.models.list();
37
+ return { valid: true };
38
+ } catch (error) {
39
+ if (error instanceof OpenAI.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: `OpenAI 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
+ "OpenAI 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 OpenAI key validation — allowing key storage",
61
+ );
62
+ return { valid: true };
63
+ }
64
+ }
65
+
17
66
  export interface OpenAICompatibleProviderOptions {
18
67
  baseURL?: string;
19
68
  providerName?: string;
@@ -93,6 +93,8 @@ export type ModelIntent =
93
93
  export interface ProviderResponse {
94
94
  content: ContentBlock[];
95
95
  model: string;
96
+ /** Provider that actually produced this response, which may differ from a wrapper provider name. */
97
+ actualProvider?: string;
96
98
  usage: {
97
99
  /** Total input tokens (input_tokens + cache_creation + cache_read). */
98
100
  inputTokens: number;