@vellumai/assistant 0.12.1 → 0.12.2-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (577) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +20 -21
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  6. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  7. package/openapi.yaml +360 -51
  8. package/package.json +3 -1
  9. package/scripts/smoke-desktop-window-theme.ts +186 -0
  10. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  11. package/src/__tests__/anthropic-provider.test.ts +0 -63
  12. package/src/__tests__/app-compiler.test.ts +6 -0
  13. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  14. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  15. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  16. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  17. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  18. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  19. package/src/__tests__/computer-use-tools.test.ts +19 -7
  20. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  21. package/src/__tests__/config-schema.test.ts +28 -29
  22. package/src/__tests__/config-watcher.test.ts +23 -8
  23. package/src/__tests__/connection-model-compat.test.ts +2 -2
  24. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-pairing.test.ts +26 -0
  26. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  27. package/src/__tests__/conversation-queue.test.ts +48 -207
  28. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
  30. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  31. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  32. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  35. package/src/__tests__/conversation-title-service.test.ts +14 -10
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  37. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  38. package/src/__tests__/credential-record-retire.test.ts +171 -1
  39. package/src/__tests__/credential-routes.test.ts +250 -3
  40. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  41. package/src/__tests__/cu-unified-flow.test.ts +14 -4
  42. package/src/__tests__/document-sync-tags.test.ts +25 -0
  43. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  44. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  45. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  46. package/src/__tests__/git-porcelain.test.ts +47 -0
  47. package/src/__tests__/heartbeat-service.test.ts +56 -40
  48. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  49. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  50. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  51. package/src/__tests__/host-cu-proxy.test.ts +596 -0
  52. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  53. package/src/__tests__/host-file-proxy.test.ts +11 -23
  54. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  55. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  56. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  57. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  58. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  59. package/src/__tests__/mcp-health-check.test.ts +8 -11
  60. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  61. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  62. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  63. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  64. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  65. package/src/__tests__/model-intents.test.ts +3 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  67. package/src/__tests__/oauth-commands-routes.test.ts +168 -0
  68. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  69. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  70. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  71. package/src/__tests__/openai-provider.test.ts +1 -0
  72. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  73. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  74. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  75. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  76. package/src/__tests__/qdrant-manager.test.ts +132 -0
  77. package/src/__tests__/resolve-app.test.ts +135 -0
  78. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  79. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  80. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  81. package/src/__tests__/service-account-token.test.ts +247 -0
  82. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  83. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +9 -4
  85. package/src/__tests__/source-fingerprint.test.ts +184 -0
  86. package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
  87. package/src/__tests__/subagent-tools.test.ts +3 -2
  88. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  89. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  90. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  91. package/src/__tests__/unicode.test.ts +13 -0
  92. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  93. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  94. package/src/__tests__/workspace-git-service.test.ts +26 -0
  95. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  97. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  98. package/src/acp/__tests__/agent-process.test.ts +258 -4
  99. package/src/acp/__tests__/auth-required.test.ts +96 -0
  100. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  101. package/src/acp/__tests__/client-handler.test.ts +90 -2
  102. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  103. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  104. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  105. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  106. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  107. package/src/acp/__tests__/session-manager.test.ts +895 -4
  108. package/src/acp/acp-claude-oauth.ts +328 -14
  109. package/src/acp/acp-credentials.ts +19 -0
  110. package/src/acp/agent-process.test.ts +4 -1
  111. package/src/acp/agent-process.ts +96 -10
  112. package/src/acp/auth-required.ts +50 -1
  113. package/src/acp/auto-install.test.ts +9 -7
  114. package/src/acp/claude-token-refresh.ts +150 -0
  115. package/src/acp/client-handler.ts +16 -3
  116. package/src/acp/model-config.test.ts +166 -0
  117. package/src/acp/model-config.ts +138 -0
  118. package/src/acp/prepare-agent-env.ts +21 -6
  119. package/src/acp/resolve-agent.test.ts +220 -6
  120. package/src/acp/resolve-agent.ts +88 -23
  121. package/src/acp/session-manager.test.ts +196 -3
  122. package/src/acp/session-manager.ts +441 -18
  123. package/src/acp/types.ts +50 -5
  124. package/src/agent/loop.ts +1 -0
  125. package/src/api/events/acp-session-model-update.test.ts +88 -0
  126. package/src/api/events/acp-session-model-update.ts +46 -0
  127. package/src/api/events/app-files-changed.ts +4 -3
  128. package/src/api/events/notification-intent.ts +2 -0
  129. package/src/api/events/user-message-echo.ts +7 -1
  130. package/src/api/index.ts +6 -0
  131. package/src/api/responses/conversation-message.ts +5 -0
  132. package/src/apps/app-store.ts +1 -1
  133. package/src/apps/resolve-app.ts +81 -0
  134. package/src/apps/source-fingerprint.ts +266 -0
  135. package/src/avatar/identity-avatar.ts +7 -2
  136. package/src/bundler/app-compiler.ts +15 -0
  137. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  138. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  139. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  140. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  141. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  142. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  143. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  144. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  145. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  146. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  147. package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
  148. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  149. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  150. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  151. package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
  152. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  153. package/src/calls/barge-in-guard.ts +118 -0
  154. package/src/calls/call-constants.ts +24 -4
  155. package/src/calls/call-controller.ts +499 -199
  156. package/src/calls/call-transport.ts +16 -0
  157. package/src/calls/media-stream-output.ts +32 -0
  158. package/src/calls/media-stream-server.ts +82 -30
  159. package/src/calls/media-stream-stt-session.ts +15 -0
  160. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  161. package/src/calls/resolve-call-tts-provider.ts +33 -2
  162. package/src/calls/tts-call-strategy.ts +9 -5
  163. package/src/calls/types.ts +2 -0
  164. package/src/calls/voice-control-protocol.ts +155 -21
  165. package/src/calls/voice-leg-coordinator.ts +189 -0
  166. package/src/calls/voice-progress-cadence.ts +423 -0
  167. package/src/calls/voice-session-bridge.ts +92 -72
  168. package/src/calls/voice-triage-escalate.ts +178 -59
  169. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  170. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  171. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  172. package/src/cli/commands/apps.help.ts +67 -13
  173. package/src/cli/commands/apps.ts +183 -0
  174. package/src/cli/commands/bash.help.ts +8 -13
  175. package/src/cli/commands/bash.ts +7 -25
  176. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  177. package/src/cli/commands/channels/index.help.ts +70 -2
  178. package/src/cli/commands/channels/index.ts +16 -3
  179. package/src/cli/commands/channels/send.ts +128 -0
  180. package/src/cli/commands/credentials.help.ts +6 -2
  181. package/src/cli/commands/credentials.ts +2 -2
  182. package/src/cli/commands/mcp.help.ts +8 -8
  183. package/src/cli/commands/mcp.ts +1 -1
  184. package/src/cli/commands/oauth/status.ts +5 -0
  185. package/src/cli/commands/roadmap.help.ts +5 -1
  186. package/src/cli/commands/roadmap.ts +1 -1
  187. package/src/cli/lib/bundled-marketplace.json +103 -90
  188. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  189. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  190. package/src/config/__tests__/platform-identity.test.ts +255 -0
  191. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  192. package/src/config/acp-agent-write.ts +43 -0
  193. package/src/config/acp-defaults.test.ts +13 -3
  194. package/src/config/acp-defaults.ts +23 -6
  195. package/src/config/acp-schema.test.ts +81 -0
  196. package/src/config/acp-schema.ts +30 -2
  197. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  198. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  199. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  200. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  201. package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
  202. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  203. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  204. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  205. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  206. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  207. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  208. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  209. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  210. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  211. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  212. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  213. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  214. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  215. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  216. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  217. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  218. package/src/config/default-profile-catalog.ts +1 -1
  219. package/src/config/env.ts +2 -2
  220. package/src/config/feature-flag-registry.json +33 -13
  221. package/src/config/loader.ts +2 -0
  222. package/src/config/memory-v3-gate.test.ts +31 -0
  223. package/src/config/memory-v3-gate.ts +20 -16
  224. package/src/config/platform-identity.ts +201 -0
  225. package/src/config/platform-rehydration.ts +11 -27
  226. package/src/config/schema.ts +2 -2
  227. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  228. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  229. package/src/config/schemas/calls.ts +1 -28
  230. package/src/config/schemas/live-voice.ts +1 -173
  231. package/src/config/schemas/mcp.ts +1 -1
  232. package/src/config/schemas/memory-retrospective.ts +10 -0
  233. package/src/config/schemas/services.ts +18 -0
  234. package/src/config/schemas/skills.ts +0 -14
  235. package/src/config/schemas/voice.ts +150 -0
  236. package/src/credential-execution/process-manager.test.ts +18 -19
  237. package/src/credential-health/credential-health-service.ts +11 -17
  238. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  239. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  240. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  241. package/src/daemon/app-change-notify.ts +45 -0
  242. package/src/daemon/config-watcher.ts +13 -194
  243. package/src/daemon/conversation-error.ts +3 -2
  244. package/src/daemon/conversation-media-retry.ts +2 -1
  245. package/src/daemon/conversation-process.ts +35 -64
  246. package/src/daemon/conversation-runtime-assembly.ts +20 -7
  247. package/src/daemon/conversation-skill-tools.ts +13 -31
  248. package/src/daemon/conversation-store.ts +1 -1
  249. package/src/daemon/conversation-surfaces.ts +21 -10
  250. package/src/daemon/conversation-tool-setup.ts +45 -59
  251. package/src/daemon/conversation.ts +0 -3
  252. package/src/daemon/dictation-profile-store.ts +2 -1
  253. package/src/daemon/handlers/shared.ts +2 -1
  254. package/src/daemon/handlers/skills.ts +6 -3
  255. package/src/daemon/host-bash-proxy.ts +8 -12
  256. package/src/daemon/host-browser-proxy.ts +15 -1
  257. package/src/daemon/host-cu-proxy.ts +224 -19
  258. package/src/daemon/host-file-proxy.ts +8 -10
  259. package/src/daemon/host-proxy-preactivation.ts +2 -1
  260. package/src/daemon/host-transfer-proxy.ts +14 -18
  261. package/src/daemon/lifecycle.ts +0 -5
  262. package/src/daemon/mcp-reload-service.ts +5 -5
  263. package/src/daemon/message-types/sync.ts +1 -0
  264. package/src/daemon/orphan-reaper.ts +1 -1
  265. package/src/daemon/providers-setup.ts +1 -1
  266. package/src/daemon/reaction-history-render.ts +2 -1
  267. package/src/daemon/shutdown-handlers.ts +1 -9
  268. package/src/daemon/skill-memory-refresh.ts +64 -0
  269. package/src/daemon/tool-side-effects.ts +7 -18
  270. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  271. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  272. package/src/desktop/desktop-chrome-policy.ts +47 -0
  273. package/src/desktop/desktop-chrome-session.ts +37 -0
  274. package/src/desktop/desktop-dependencies.test.ts +102 -0
  275. package/src/desktop/desktop-dependencies.ts +308 -0
  276. package/src/desktop/desktop-feature.test.ts +18 -0
  277. package/src/desktop/desktop-feature.ts +19 -0
  278. package/src/desktop/desktop-panel-config.test.ts +135 -0
  279. package/src/desktop/desktop-panel-config.ts +149 -0
  280. package/src/desktop/desktop-session-manager.test.ts +730 -0
  281. package/src/desktop/desktop-session-manager.ts +850 -0
  282. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  283. package/src/desktop/desktop-stream-bridge.ts +214 -0
  284. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  285. package/src/desktop/desktop-wallpaper.ts +113 -0
  286. package/src/desktop/desktop-window-drag.ts +177 -0
  287. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  288. package/src/desktop/desktop-window-manager-config.ts +238 -0
  289. package/src/desktop/desktop-window-theme.ts +170 -0
  290. package/src/heartbeat/heartbeat-service.ts +21 -3
  291. package/src/home/__tests__/feed-writer.test.ts +22 -0
  292. package/src/home/feed-writer.ts +11 -2
  293. package/src/hooks/types.ts +33 -0
  294. package/src/i18n/AGENTS.md +22 -0
  295. package/src/i18n/__tests__/i18n.test.ts +108 -0
  296. package/src/i18n/index.ts +16 -0
  297. package/src/i18n/locales.ts +93 -0
  298. package/src/i18n/messages.ts +61 -0
  299. package/src/i18n/title.ts +25 -0
  300. package/src/inbound/platform-callback-registration.ts +4 -5
  301. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
  302. package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
  303. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  304. package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
  305. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  306. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  307. package/src/live-voice/__tests__/protocol.test.ts +39 -0
  308. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  309. package/src/live-voice/__tests__/session-controls.test.ts +79 -0
  310. package/src/live-voice/live-voice-photo.ts +25 -6
  311. package/src/live-voice/live-voice-session.ts +297 -699
  312. package/src/live-voice/protocol.ts +60 -2
  313. package/src/live-voice/session-controls.ts +111 -0
  314. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  315. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  316. package/src/mcp/effective-config.ts +3 -3
  317. package/src/mcp/mcp-header-store.ts +29 -45
  318. package/src/mcp/spec-schema.ts +111 -0
  319. package/src/mcp/startup.ts +8 -4
  320. package/src/mcp/workspace-mcp-config.ts +188 -0
  321. package/src/messaging/provider.ts +10 -3
  322. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  323. package/src/messaging/providers/callback-routing.ts +18 -0
  324. package/src/messaging/providers/channel-transport.ts +56 -0
  325. package/src/messaging/providers/discord/transport.ts +17 -0
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  327. package/src/messaging/providers/slack/adapter.ts +7 -44
  328. package/src/messaging/providers/slack/transport.ts +16 -0
  329. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  330. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  331. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  332. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  333. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  334. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  335. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  336. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  337. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  338. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  339. package/src/notifications/adapters/macos.ts +3 -0
  340. package/src/notifications/assistant-reply-producer.ts +8 -3
  341. package/src/notifications/conversation-pairing.ts +36 -16
  342. package/src/notifications/delivered-post-record.ts +11 -1
  343. package/src/notifications/guardian-feed-projection.ts +125 -5
  344. package/src/notifications/home-feed-side-effect.ts +55 -7
  345. package/src/notifications/schedule-result-producer.ts +66 -22
  346. package/src/oauth/AGENTS.md +2 -0
  347. package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
  348. package/src/oauth/byo-connection.test.ts +105 -0
  349. package/src/oauth/byo-connection.ts +36 -6
  350. package/src/oauth/connection-resolver.test.ts +100 -0
  351. package/src/oauth/connection-resolver.ts +120 -3
  352. package/src/oauth/provider-serializer.ts +9 -0
  353. package/src/oauth/scope-utils.ts +24 -0
  354. package/src/oauth/seed-providers.ts +314 -27
  355. package/src/outbound-proxy/certs.ts +3 -5
  356. package/src/permissions/checker.test.ts +11 -6
  357. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  358. package/src/persistence/conversation-crud.ts +71 -63
  359. package/src/persistence/conversation-lineage.ts +12 -4
  360. package/src/persistence/conversation-queries.ts +2 -2
  361. package/src/persistence/conversation-title-placeholders.ts +23 -0
  362. package/src/persistence/conversation-title-service.ts +9 -26
  363. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  364. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  365. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  366. package/src/persistence/message-cursor.ts +78 -0
  367. package/src/persistence/message-reads.ts +30 -0
  368. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  369. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  370. package/src/persistence/schema/memory-core.ts +5 -0
  371. package/src/platform/client.test.ts +4 -2
  372. package/src/platform/client.ts +2 -10
  373. package/src/plugin-api/constants.ts +2 -0
  374. package/src/plugin-api/index.ts +1 -0
  375. package/src/plugin-api/types.ts +2 -0
  376. package/src/plugins/defaults/index.ts +2 -0
  377. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  378. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  379. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  380. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  381. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  382. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  383. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  384. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  385. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  386. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  387. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  388. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  389. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  390. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  391. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  392. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  393. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  394. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  395. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  396. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  397. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  398. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  399. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  400. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  401. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  402. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  403. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  404. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  405. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  406. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  407. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  408. package/src/plugins/defaults/memory/startup.ts +28 -2
  409. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  410. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  411. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  412. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  413. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  414. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  415. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  416. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  417. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  418. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  419. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  420. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  421. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  422. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  423. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  424. package/src/plugins/defaults/memory/worker.ts +6 -0
  425. package/src/plugins/mcp-servers.ts +15 -75
  426. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  427. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  428. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  429. package/src/providers/anthropic/client.ts +0 -47
  430. package/src/providers/gemini/client.ts +9 -23
  431. package/src/providers/gemini-thought-signature.test.ts +63 -0
  432. package/src/providers/gemini-thought-signature.ts +44 -0
  433. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  434. package/src/providers/inference/auth.ts +2 -3
  435. package/src/providers/inference/connection-availability.ts +27 -8
  436. package/src/providers/inference/endpoint-probe.ts +10 -1
  437. package/src/providers/inference/resolve-auth.ts +22 -4
  438. package/src/providers/inference/service-account-token.ts +248 -0
  439. package/src/providers/model-catalog.ts +34 -0
  440. package/src/providers/model-intents.ts +10 -5
  441. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  442. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  443. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  444. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  445. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  446. package/src/providers/openai/chat-completions-provider.ts +212 -79
  447. package/src/providers/openai/codex-models.ts +0 -4
  448. package/src/providers/openai/google-thought-signature.ts +320 -0
  449. package/src/providers/opencode/client.test.ts +73 -4
  450. package/src/providers/opencode/client.ts +60 -3
  451. package/src/providers/openrouter/client.ts +20 -0
  452. package/src/providers/outbound-request-sanitize.ts +96 -0
  453. package/src/providers/retry.ts +30 -19
  454. package/src/providers/unparseable-tool-args.ts +5 -1
  455. package/src/runtime/AGENTS.md +3 -1
  456. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  457. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  458. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  459. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  460. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  461. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  462. package/src/runtime/agent-wake.ts +2 -1
  463. package/src/runtime/assistant-event-hub.ts +21 -3
  464. package/src/runtime/auth/same-actor.ts +38 -3
  465. package/src/runtime/channel-send.ts +316 -0
  466. package/src/runtime/host-observe.ts +11 -1
  467. package/src/runtime/http-server.ts +160 -151
  468. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  469. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  470. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
  471. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  472. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  473. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  474. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  475. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  476. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  477. package/src/runtime/routes/acp-routes.test.ts +15 -0
  478. package/src/runtime/routes/acp-routes.ts +72 -9
  479. package/src/runtime/routes/app-management-routes.ts +90 -2
  480. package/src/runtime/routes/app-routes.ts +1 -1
  481. package/src/runtime/routes/channel-send-routes.ts +85 -0
  482. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  483. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  484. package/src/runtime/routes/conversation-routes.ts +16 -3
  485. package/src/runtime/routes/credential-routes.ts +20 -14
  486. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  487. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  488. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  489. package/src/runtime/routes/events-routes.ts +4 -0
  490. package/src/runtime/routes/host-cu-routes.ts +6 -0
  491. package/src/runtime/routes/index.ts +4 -2
  492. package/src/runtime/routes/log-export-routes.ts +2 -0
  493. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  494. package/src/runtime/routes/migration-routes.ts +6 -20
  495. package/src/runtime/routes/oauth-commands-routes.ts +50 -0
  496. package/src/runtime/routes/oauth-providers.ts +7 -0
  497. package/src/runtime/routes/platform-routes.ts +18 -15
  498. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  499. package/src/runtime/routes/workspace-routes.ts +46 -63
  500. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  501. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  502. package/src/runtime/routes/workspace-utils.ts +17 -0
  503. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  504. package/src/runtime/services/conversation-serializer.ts +16 -3
  505. package/src/runtime/sync/resource-sync-events.ts +2 -1
  506. package/src/schedule/run-script.ts +2 -1
  507. package/src/schedule/schedule-store.ts +9 -2
  508. package/src/schedule/worker.ts +1 -1
  509. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  510. package/src/security/ces-http-record-backend.ts +120 -0
  511. package/src/security/ces-rpc-record-backend.ts +7 -0
  512. package/src/security/untrusted-content.ts +3 -1
  513. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  514. package/src/skills/workspace-skill-md-signature.ts +50 -0
  515. package/src/subagent/consult-context.ts +12 -25
  516. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  517. package/src/tools/acp/list-agents.test.ts +3 -1
  518. package/src/tools/acp/spawn.test.ts +146 -11
  519. package/src/tools/acp/spawn.ts +21 -3
  520. package/src/tools/acp/status.test.ts +94 -0
  521. package/src/tools/acp/status.ts +40 -3
  522. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  523. package/src/tools/browser/browser-execution.ts +1 -1
  524. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  525. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  526. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  527. package/src/tools/browser/network-recorder.ts +2 -1
  528. package/src/tools/capability-offer.test.ts +22 -2
  529. package/src/tools/capability-offer.ts +16 -1
  530. package/src/tools/computer-use/definitions.ts +121 -2
  531. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  532. package/src/tools/credentials/metadata-store.ts +100 -0
  533. package/src/tools/filesystem/search.ts +2 -1
  534. package/src/tools/host-filesystem/edit.test.ts +2 -1
  535. package/src/tools/host-filesystem/edit.ts +2 -2
  536. package/src/tools/host-filesystem/read.test.ts +2 -1
  537. package/src/tools/host-filesystem/read.ts +2 -2
  538. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  539. package/src/tools/host-filesystem/transfer.ts +2 -2
  540. package/src/tools/host-filesystem/write.test.ts +2 -1
  541. package/src/tools/host-filesystem/write.ts +2 -2
  542. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  543. package/src/tools/host-terminal/host-shell.ts +4 -20
  544. package/src/tools/network/web-search.ts +6 -1
  545. package/src/tools/policy-context.ts +6 -7
  546. package/src/tools/skills/scaffold-managed.ts +61 -3
  547. package/src/tools/subagent/spawn.ts +5 -8
  548. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  549. package/src/tools/terminal/sanitized-bash.ts +105 -0
  550. package/src/tools/ui-surface/channel-variants.ts +10 -59
  551. package/src/tools/watch/watch-retro-report.ts +0 -8
  552. package/src/types/host-capabilities.ts +5 -3
  553. package/src/util/__tests__/language-subtag.test.ts +23 -1
  554. package/src/util/ca-bundle.ts +13 -0
  555. package/src/util/language-subtag.ts +16 -0
  556. package/src/util/platform.ts +8 -0
  557. package/src/util/unicode.ts +15 -1
  558. package/src/watch/watch-retro.ts +6 -8
  559. package/src/watch/watch-timeline.ts +2 -2
  560. package/src/workflows/run-manager.ts +2 -1
  561. package/src/workspace/git-porcelain.ts +66 -0
  562. package/src/workspace/git-service.ts +6 -55
  563. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  564. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  565. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  566. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  567. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  568. package/src/workspace/migrations/registry.ts +6 -0
  569. package/src/__tests__/app-source-watcher.test.ts +0 -218
  570. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  571. package/src/__tests__/speaker-identification.test.ts +0 -56
  572. package/src/calls/speaker-identification.ts +0 -233
  573. package/src/daemon/app-source-watcher.ts +0 -218
  574. package/src/messaging/providers/whatsapp/client.ts +0 -29
  575. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  576. package/src/util/fs-watcher-error.ts +0 -36
  577. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -6,7 +6,7 @@
6
6
  * - platform → build managed proxy URL and fetch the platform API key
7
7
  * - none → pass through with no auth headers
8
8
  * - oauth_subscription → fetch OAuth token from vault (with auto-refresh) → inject as bearer header
9
- * - service_account → reject (v2 not yet shipped)
9
+ * - service_account → sign JWT and exchange for bearer token at token_uri (Google Vertex AI)
10
10
  */
11
11
 
12
12
  import { getSecureKeyAsync } from "../../security/secure-keys.js";
@@ -119,10 +119,28 @@ export async function resolveAuth(
119
119
  };
120
120
  }
121
121
 
122
- case "service_account":
122
+ case "service_account": {
123
+ const { getValidServiceAccountToken } =
124
+ await import("./service-account-token.js");
125
+ const result = await getValidServiceAccountToken(auth.credential);
126
+ if (!result.ok) {
127
+ if (result.reason === "exchange_failed") {
128
+ return { ok: false, error: { code: "platform_unavailable" } };
129
+ }
130
+ // not_found or invalid_config: the credential needs to be supplied or fixed.
131
+ return {
132
+ ok: false,
133
+ error: { code: "credential_not_found", credential: auth.credential },
134
+ };
135
+ }
123
136
  return {
124
- ok: false,
125
- error: { code: "not_implemented", authType: auth.type },
137
+ ok: true,
138
+ resolved: {
139
+ kind: "header",
140
+ headers: { Authorization: `Bearer ${result.token}` },
141
+ ...(safeBaseUrl ? { baseUrl: safeBaseUrl } : {}),
142
+ },
126
143
  };
144
+ }
127
145
  }
128
146
  }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Token acquisition for Google service-account credentials (Vertex AI).
3
+ *
4
+ * Reads the service-account JSON from the vault, signs a short-lived JWT
5
+ * with RS256, and exchanges it for a bearer access token at the token_uri
6
+ * embedded in the key file. Caches the resulting token and its expiry as a
7
+ * single JSON blob in the vault so the write is treated as one unit.
8
+ *
9
+ * A per-credential mutex prevents concurrent callers for the same credential
10
+ * from racing to refresh. Callers for different credentials are independent.
11
+ */
12
+
13
+ import { createSign } from "node:crypto";
14
+
15
+ import {
16
+ getSecureKeyAsync as _realGetKey,
17
+ setSecureKeyAsync as _realSetKey,
18
+ } from "../../security/secure-keys.js";
19
+ import { getLogger } from "../../util/logger.js";
20
+
21
+ // Mutable references — swapped in tests via _injectVaultAccessors so tests
22
+ // never need mock.module (which contaminates the entire bun worker process).
23
+ let _getKey: typeof _realGetKey = _realGetKey;
24
+ let _setKey: typeof _realSetKey = _realSetKey;
25
+
26
+ const log = getLogger("service-account-token");
27
+
28
+ /** Refresh 5 minutes before expiry to avoid using a nearly-expired token. */
29
+ const REFRESH_MARGIN_SECONDS = 300;
30
+
31
+ /** Google Cloud Platform scope required for Vertex AI inference. */
32
+ const GCP_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
33
+
34
+ export interface ServiceAccountKey {
35
+ client_email: string;
36
+ private_key: string;
37
+ token_uri: string;
38
+ }
39
+
40
+ export type ServiceAccountTokenResult =
41
+ | { ok: true; token: string }
42
+ | { ok: false; reason: "not_found" | "invalid_config" | "exchange_failed" };
43
+
44
+ /**
45
+ * Parse and validate a service-account JSON string.
46
+ * Returns the key object when valid, or null when the JSON is missing, invalid,
47
+ * or lacks required fields (client_email, private_key, token_uri).
48
+ */
49
+ export function parseServiceAccountKey(json: string): ServiceAccountKey | null {
50
+ let parsed: unknown;
51
+ try {
52
+ parsed = JSON.parse(json);
53
+ } catch {
54
+ return null;
55
+ }
56
+ if (!parsed || typeof parsed !== "object") {
57
+ return null;
58
+ }
59
+ const key = parsed as Record<string, unknown>;
60
+ if (
61
+ typeof key["client_email"] !== "string" ||
62
+ !key["client_email"] ||
63
+ typeof key["private_key"] !== "string" ||
64
+ !key["private_key"] ||
65
+ typeof key["token_uri"] !== "string" ||
66
+ !key["token_uri"]
67
+ ) {
68
+ return null;
69
+ }
70
+ // All required fields validated as non-empty strings above.
71
+ return parsed as ServiceAccountKey;
72
+ }
73
+
74
+ /** Per-credential in-flight mutex. Prevents concurrent exchange races per account. */
75
+ const exchangesInFlight = new Map<string, Promise<ServiceAccountTokenResult>>();
76
+
77
+ /**
78
+ * Return a valid bearer access token for a Google service account, fetching
79
+ * and caching one if the stored token is absent or about to expire.
80
+ *
81
+ * @param credential - Vault key under which the service-account JSON is stored
82
+ * (e.g. `"credential/my-vertex-ai"`). The derived token is cached as a JSON
83
+ * blob at `<credential>/token_cache`.
84
+ */
85
+ export async function getValidServiceAccountToken(
86
+ credential: string,
87
+ ): Promise<ServiceAccountTokenResult> {
88
+ const cacheKey = `${credential}/token_cache`;
89
+ const cached = await _getKey(cacheKey);
90
+ if (cached) {
91
+ try {
92
+ const blob = JSON.parse(cached) as {
93
+ access_token?: string;
94
+ expires_at?: number;
95
+ };
96
+ if (blob.access_token) {
97
+ if (!blob.expires_at) {
98
+ return { ok: true, token: blob.access_token };
99
+ }
100
+ const now = Date.now() / 1000;
101
+ if (now < blob.expires_at - REFRESH_MARGIN_SECONDS) {
102
+ return { ok: true, token: blob.access_token };
103
+ }
104
+ }
105
+ } catch {
106
+ // Corrupted cache entry — fall through to re-exchange.
107
+ }
108
+ }
109
+
110
+ const inFlight = exchangesInFlight.get(credential);
111
+ if (inFlight) {
112
+ return await inFlight;
113
+ }
114
+
115
+ const promise = doExchange(credential, cacheKey);
116
+ exchangesInFlight.set(credential, promise);
117
+ try {
118
+ return await promise;
119
+ } finally {
120
+ exchangesInFlight.delete(credential);
121
+ }
122
+ }
123
+
124
+ async function doExchange(
125
+ credential: string,
126
+ cacheKey: string,
127
+ ): Promise<ServiceAccountTokenResult> {
128
+ const keyJson = await _getKey(credential);
129
+ if (!keyJson) {
130
+ return { ok: false, reason: "not_found" };
131
+ }
132
+
133
+ const key = parseServiceAccountKey(keyJson);
134
+ if (!key) {
135
+ log.error(
136
+ { credential },
137
+ "Service account credential is not valid JSON or missing required fields",
138
+ );
139
+ return { ok: false, reason: "invalid_config" };
140
+ }
141
+
142
+ const jwt = buildJwt(key.client_email, key.private_key, key.token_uri);
143
+
144
+ let accessToken: string;
145
+ let expiresIn: number;
146
+ try {
147
+ const resp = await fetch(key.token_uri, {
148
+ method: "POST",
149
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
150
+ body: new URLSearchParams({
151
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
152
+ assertion: jwt,
153
+ }).toString(),
154
+ });
155
+
156
+ if (!resp.ok) {
157
+ const body = await resp.text();
158
+ log.error(
159
+ { status: resp.status, body, credential },
160
+ "Service account token exchange failed",
161
+ );
162
+ return { ok: false, reason: "exchange_failed" };
163
+ }
164
+
165
+ const data = (await resp.json()) as {
166
+ access_token?: string;
167
+ expires_in?: number;
168
+ };
169
+
170
+ if (!data.access_token) {
171
+ log.error({ credential }, "Token exchange response missing access_token");
172
+ return { ok: false, reason: "exchange_failed" };
173
+ }
174
+
175
+ accessToken = data.access_token;
176
+ expiresIn = data.expires_in ?? 3600;
177
+ } catch (err) {
178
+ log.error({ err, credential }, "Service account token exchange threw");
179
+ return { ok: false, reason: "exchange_failed" };
180
+ }
181
+
182
+ const newExpiresAt = Math.floor(Date.now() / 1000 + expiresIn);
183
+ const ok = await _setKey(
184
+ cacheKey,
185
+ JSON.stringify({ access_token: accessToken, expires_at: newExpiresAt }),
186
+ );
187
+ if (!ok) {
188
+ log.warn(
189
+ { credential },
190
+ "Failed to cache service account token — token still valid for this request",
191
+ );
192
+ } else {
193
+ log.info({ credential }, "Service account token exchanged and cached");
194
+ }
195
+ return { ok: true, token: accessToken };
196
+ }
197
+
198
+ function buildJwt(
199
+ clientEmail: string,
200
+ privateKey: string,
201
+ tokenUri: string,
202
+ ): string {
203
+ const now = Math.floor(Date.now() / 1000);
204
+ const header = Buffer.from(
205
+ JSON.stringify({ alg: "RS256", typ: "JWT" }),
206
+ ).toString("base64url");
207
+ const payload = Buffer.from(
208
+ JSON.stringify({
209
+ iss: clientEmail,
210
+ scope: GCP_SCOPE,
211
+ aud: tokenUri,
212
+ exp: now + 3600,
213
+ iat: now,
214
+ }),
215
+ ).toString("base64url");
216
+
217
+ const signingInput = `${header}.${payload}`;
218
+ const sign = createSign("SHA256");
219
+ sign.update(signingInput);
220
+ sign.end();
221
+ const signature = sign.sign(privateKey, "base64url");
222
+
223
+ return `${signingInput}.${signature}`;
224
+ }
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // Test helpers
228
+ // ---------------------------------------------------------------------------
229
+
230
+ /** @internal Test-only: reset all in-flight exchange mutexes. */
231
+ export function _resetServiceAccountMutex(): void {
232
+ exchangesInFlight.clear();
233
+ }
234
+
235
+ /** @internal Test-only: swap vault accessors so tests never need mock.module. */
236
+ export function _injectVaultAccessors(
237
+ get: typeof _realGetKey,
238
+ set: typeof _realSetKey,
239
+ ): void {
240
+ _getKey = get;
241
+ _setKey = set;
242
+ }
243
+
244
+ /** @internal Test-only: restore real vault accessors. */
245
+ export function _resetVaultAccessors(): void {
246
+ _getKey = _realGetKey;
247
+ _setKey = _realSetKey;
248
+ }
@@ -76,6 +76,13 @@ export interface CatalogModel {
76
76
  supportsAudioInput?: boolean;
77
77
  supportsToolUse?: boolean;
78
78
  supportsEffort?: boolean;
79
+ /**
80
+ * Whether this provider/model serving surface accepts a forced OpenAI
81
+ * chat-completions tool choice while thinking is enabled. Omit unless the
82
+ * combination is known incompatible. Daemon-only: not projected into the
83
+ * client catalog (see scripts/sync-llm-catalog.ts).
84
+ */
85
+ supportsForcedToolChoiceWithThinking?: boolean;
79
86
  pricing?: CatalogModelPricing;
80
87
  /**
81
88
  * Upper bound for `reasoning_effort` accepted by this model's upstream API.
@@ -856,6 +863,9 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
856
863
  cacheReadPer1mTokens: 0.03,
857
864
  },
858
865
  },
866
+ // Limited to grandfathered accounts: other API keys get HTTP 404 "no
867
+ // longer available to new users", so this model is user-selectable
868
+ // only and no intent column may resolve to it.
859
869
  {
860
870
  id: "gemini-2.5-flash-lite",
861
871
  displayName: "Gemini 2.5 Flash Lite",
@@ -1816,6 +1826,7 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
1816
1826
  supportsCaching: true,
1817
1827
  supportsVision: true,
1818
1828
  supportsToolUse: true,
1829
+ supportsForcedToolChoiceWithThinking: false,
1819
1830
  pricing: {
1820
1831
  inputPer1mTokens: 0.95,
1821
1832
  outputPer1mTokens: 4.0,
@@ -2628,6 +2639,29 @@ export function modelSupportedEfforts(
2628
2639
  );
2629
2640
  }
2630
2641
 
2642
+ /**
2643
+ * Whether a provider/model serving surface accepts a forced OpenAI
2644
+ * chat-completions tool choice while thinking is enabled. Unknown providers
2645
+ * and models fail open so custom routes retain their existing request shape
2646
+ * and can rely on the bounded provider-error retry if needed.
2647
+ */
2648
+ export function supportsForcedToolChoiceWithThinking(
2649
+ providerId: string,
2650
+ modelId: string,
2651
+ ): boolean {
2652
+ const provider = PROVIDER_CATALOG.find((entry) => entry.id === providerId);
2653
+ if (!provider) {
2654
+ return true;
2655
+ }
2656
+ const stripDateSuffix = (id: string): string => id.replace(/-\d{8}$/, "");
2657
+ const normalizedModelId = stripDateSuffix(modelId);
2658
+ return !provider.models.some(
2659
+ (model) =>
2660
+ model.supportsForcedToolChoiceWithThinking === false &&
2661
+ (model.id === modelId || stripDateSuffix(model.id) === normalizedModelId),
2662
+ );
2663
+ }
2664
+
2631
2665
  /**
2632
2666
  * Return the catalog provider that owns a model ID, if known. When multiple
2633
2667
  * providers list the same ID (e.g. OpenRouter and the Vercel AI Gateway share
@@ -11,10 +11,15 @@ const PROVIDER_DEFAULT_MODELS: Record<string, string> = Object.fromEntries(
11
11
  );
12
12
 
13
13
  // `cost-optimized` is the cheapest model a provider serves, `latency-optimized`
14
- // the fastest to first token. On anthropic, ollama, fireworks and openai the
15
- // same model is both, so those columns repeat by construction rather than by
16
- // oversight (openai's balanced repeats it too: the default-profile templates
17
- // split those tiers by reasoning effort, not model).
14
+ // the fastest to first token. On anthropic, ollama, fireworks, openai and
15
+ // gemini the same model is both, so those columns repeat by construction
16
+ // rather than by oversight (openai's balanced repeats it too: the
17
+ // default-profile templates split those tiers by reasoning effort, not model).
18
+ //
19
+ // "Serves" means serves to every API key: Google's `gemini-2.5-flash-lite`
20
+ // undercuts every 3.x Flash-Lite but is limited to grandfathered accounts
21
+ // (other keys get HTTP 404 "no longer available to new users"), so the
22
+ // gemini column skips it.
18
23
  const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
19
24
  anthropic: {
20
25
  balanced: "claude-sonnet-4-6",
@@ -32,7 +37,7 @@ const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
32
37
  },
33
38
  gemini: {
34
39
  balanced: "gemini-3-flash-preview",
35
- "cost-optimized": "gemini-2.5-flash-lite",
40
+ "cost-optimized": "gemini-3.1-flash-lite",
36
41
  "latency-optimized": "gemini-3.1-flash-lite",
37
42
  "quality-optimized": "gemini-3.1-pro-preview",
38
43
  "vision-optimized": "gemini-3-flash-preview",
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Request-shape coverage for the output-token-limit wire key.
3
+ *
4
+ * The shared OpenAI-compatible transport defaults to `max_completion_tokens`.
5
+ * OpenRouter's parameter router matches `max_tokens` on `require_parameters`
6
+ * routes, except GPT-5.1/5.2 Codex and Chat which advertise only
7
+ * `max_completion_tokens`.
8
+ */
9
+
10
+ import { describe, expect, test } from "bun:test";
11
+
12
+ import {
13
+ openRouterOutputTokenLimitField,
14
+ OpenRouterProvider,
15
+ } from "../../openrouter/client.js";
16
+ import { OpenAIChatCompletionsProvider } from "../chat-completions-provider.js";
17
+
18
+ type MockChunk = {
19
+ choices: Array<{
20
+ delta: { content?: string | null };
21
+ finish_reason?: string | null;
22
+ }>;
23
+ usage?: { prompt_tokens: number; completion_tokens: number };
24
+ };
25
+
26
+ const OK_CHUNKS: MockChunk[] = [
27
+ {
28
+ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
29
+ usage: { prompt_tokens: 1, completion_tokens: 1 },
30
+ },
31
+ ];
32
+
33
+ function makeStream(chunks: MockChunk[]): AsyncIterable<MockChunk> {
34
+ return {
35
+ async *[Symbol.asyncIterator]() {
36
+ for (const c of chunks) {
37
+ yield c;
38
+ }
39
+ },
40
+ };
41
+ }
42
+
43
+ function stubCreate(provider: OpenAIChatCompletionsProvider): {
44
+ requests: Record<string, unknown>[];
45
+ } {
46
+ const requests: Record<string, unknown>[] = [];
47
+ const inner = provider as unknown as {
48
+ client: {
49
+ chat: {
50
+ completions: {
51
+ create: (
52
+ params: Record<string, unknown>,
53
+ ) => Promise<AsyncIterable<MockChunk>>;
54
+ };
55
+ };
56
+ };
57
+ };
58
+ inner.client.chat.completions.create = async (params) => {
59
+ requests.push(params);
60
+ return makeStream(OK_CHUNKS);
61
+ };
62
+ return { requests };
63
+ }
64
+
65
+ describe("chat-completions output-token-limit wire key", () => {
66
+ test("default OpenAI path sends max_completion_tokens and omits max_tokens", async () => {
67
+ const provider = new OpenAIChatCompletionsProvider(
68
+ "test-key",
69
+ "test-model",
70
+ );
71
+ const { requests } = stubCreate(provider);
72
+
73
+ await provider.sendMessage(
74
+ [{ role: "user", content: [{ type: "text", text: "hi" }] }],
75
+ { config: { max_tokens: 64000 } },
76
+ );
77
+
78
+ expect(requests).toHaveLength(1);
79
+ expect(requests[0].max_completion_tokens).toBe(64000);
80
+ expect(requests[0]).not.toHaveProperty("max_tokens");
81
+ });
82
+
83
+ test("OpenRouter chat-completions path sends max_tokens and omits max_completion_tokens", async () => {
84
+ const provider = new OpenRouterProvider("or-key", "@preset/example");
85
+ const { requests } = stubCreate(provider);
86
+
87
+ await provider.sendMessage(
88
+ [{ role: "user", content: [{ type: "text", text: "hi" }] }],
89
+ { config: { max_tokens: 64000 } },
90
+ );
91
+
92
+ expect(requests).toHaveLength(1);
93
+ expect(requests[0].max_tokens).toBe(64000);
94
+ expect(requests[0]).not.toHaveProperty("max_completion_tokens");
95
+ });
96
+
97
+ test("OpenRouter GPT-5.2 Codex sends max_completion_tokens and omits max_tokens", async () => {
98
+ const provider = new OpenRouterProvider("or-key", "openai/gpt-5.2-codex");
99
+ const { requests } = stubCreate(provider);
100
+
101
+ await provider.sendMessage(
102
+ [{ role: "user", content: [{ type: "text", text: "hi" }] }],
103
+ { config: { max_tokens: 64000 } },
104
+ );
105
+
106
+ expect(requests).toHaveLength(1);
107
+ expect(requests[0].max_completion_tokens).toBe(64000);
108
+ expect(requests[0]).not.toHaveProperty("max_tokens");
109
+ });
110
+
111
+ test("OpenRouter per-call model override uses the overridden model's wire key", async () => {
112
+ const provider = new OpenRouterProvider("or-key", "x-ai/grok-4.20");
113
+ const { requests } = stubCreate(provider);
114
+
115
+ await provider.sendMessage(
116
+ [{ role: "user", content: [{ type: "text", text: "hi" }] }],
117
+ { config: { model: "openai/gpt-5.1-codex-mini", max_tokens: 64000 } },
118
+ );
119
+
120
+ expect(requests).toHaveLength(1);
121
+ expect(requests[0].max_completion_tokens).toBe(64000);
122
+ expect(requests[0]).not.toHaveProperty("max_tokens");
123
+ });
124
+ });
125
+
126
+ describe("openRouterOutputTokenLimitField", () => {
127
+ test.each([
128
+ ["@preset/example", "max_tokens"],
129
+ ["x-ai/grok-4.20", "max_tokens"],
130
+ ["openai/gpt-5.2", "max_tokens"],
131
+ ["openai/gpt-5.2-pro", "max_tokens"],
132
+ ["openai/gpt-5.3-codex", "max_tokens"],
133
+ ["openai/gpt-5.2-codex", "max_completion_tokens"],
134
+ ["openai/gpt-5.2-chat", "max_completion_tokens"],
135
+ ["openai/gpt-5.1-codex", "max_completion_tokens"],
136
+ ["openai/gpt-5.1-codex-max", "max_completion_tokens"],
137
+ ["openai/gpt-5.1-codex-mini", "max_completion_tokens"],
138
+ ] as const)("%s -> %s", (model, field) => {
139
+ expect(openRouterOutputTokenLimitField(model)).toBe(field);
140
+ });
141
+ });