@vellumai/assistant 0.12.0 → 0.12.1-dev.202609141913.5e16607

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 (441) hide show
  1. package/AGENTS.md +5 -1
  2. package/ARCHITECTURE.md +11 -12
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/openapi.yaml +292 -50
  6. package/package.json +1 -1
  7. package/scripts/smoke-desktop-window-theme.ts +186 -0
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  9. package/src/__tests__/app-compiler.test.ts +6 -0
  10. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  12. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  13. package/src/__tests__/computer-use-tools.test.ts +17 -5
  14. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  15. package/src/__tests__/config-schema.test.ts +28 -29
  16. package/src/__tests__/config-watcher.test.ts +23 -8
  17. package/src/__tests__/connection-model-compat.test.ts +2 -2
  18. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  19. package/src/__tests__/conversation-pairing.test.ts +26 -0
  20. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  21. package/src/__tests__/conversation-queue.test.ts +48 -207
  22. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  23. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  24. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  25. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  26. package/src/__tests__/conversation-title-service.test.ts +14 -10
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  28. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  29. package/src/__tests__/credential-record-retire.test.ts +171 -1
  30. package/src/__tests__/credential-routes.test.ts +212 -3
  31. package/src/__tests__/credential-security-invariants.test.ts +4 -2
  32. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  33. package/src/__tests__/document-sync-tags.test.ts +25 -0
  34. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  35. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  36. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  37. package/src/__tests__/git-porcelain.test.ts +47 -0
  38. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  39. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  40. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  41. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  42. package/src/__tests__/host-file-proxy.test.ts +11 -23
  43. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  44. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  45. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  46. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  47. package/src/__tests__/mcp-health-check.test.ts +8 -11
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  49. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  50. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  51. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  52. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  53. package/src/__tests__/model-intents.test.ts +3 -0
  54. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  55. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  56. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  57. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  58. package/src/__tests__/plugin-import-boundary-guard.test.ts +13 -0
  59. package/src/__tests__/resolve-app.test.ts +135 -0
  60. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  61. package/src/__tests__/service-account-token.test.ts +247 -0
  62. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  63. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  64. package/src/__tests__/source-fingerprint.test.ts +184 -0
  65. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  66. package/src/__tests__/subagent-tools.test.ts +3 -2
  67. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  68. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  69. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  70. package/src/__tests__/workspace-git-service.test.ts +26 -0
  71. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  72. package/src/acp/__tests__/agent-process.test.ts +258 -4
  73. package/src/acp/__tests__/auth-required.test.ts +96 -0
  74. package/src/acp/__tests__/client-handler.test.ts +90 -2
  75. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  76. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  77. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  78. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  79. package/src/acp/__tests__/session-manager.test.ts +895 -4
  80. package/src/acp/agent-process.test.ts +4 -1
  81. package/src/acp/agent-process.ts +94 -9
  82. package/src/acp/auth-required.ts +50 -1
  83. package/src/acp/auto-install.test.ts +9 -7
  84. package/src/acp/client-handler.ts +16 -3
  85. package/src/acp/model-config.test.ts +166 -0
  86. package/src/acp/model-config.ts +138 -0
  87. package/src/acp/resolve-agent.test.ts +220 -6
  88. package/src/acp/resolve-agent.ts +88 -23
  89. package/src/acp/session-manager.test.ts +196 -3
  90. package/src/acp/session-manager.ts +441 -18
  91. package/src/acp/types.ts +50 -5
  92. package/src/api/events/acp-session-model-update.test.ts +88 -0
  93. package/src/api/events/acp-session-model-update.ts +46 -0
  94. package/src/api/events/app-files-changed.ts +4 -3
  95. package/src/api/events/notification-intent.ts +2 -0
  96. package/src/api/index.ts +6 -0
  97. package/src/apps/app-store.ts +1 -1
  98. package/src/apps/resolve-app.ts +81 -0
  99. package/src/apps/source-fingerprint.ts +266 -0
  100. package/src/bundler/app-compiler.ts +15 -0
  101. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  102. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  103. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  104. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  105. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  106. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  107. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  108. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  109. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  110. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  111. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  112. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  113. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  114. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  115. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  116. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  117. package/src/calls/barge-in-guard.ts +118 -0
  118. package/src/calls/call-constants.ts +24 -4
  119. package/src/calls/call-controller.ts +499 -199
  120. package/src/calls/call-transport.ts +16 -0
  121. package/src/calls/media-stream-output.ts +32 -0
  122. package/src/calls/media-stream-server.ts +82 -30
  123. package/src/calls/media-stream-stt-session.ts +15 -0
  124. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  125. package/src/calls/resolve-call-tts-provider.ts +33 -2
  126. package/src/calls/tts-call-strategy.ts +9 -5
  127. package/src/calls/types.ts +2 -0
  128. package/src/calls/voice-control-protocol.ts +53 -21
  129. package/src/calls/voice-leg-coordinator.ts +189 -0
  130. package/src/calls/voice-progress-cadence.ts +423 -0
  131. package/src/calls/voice-session-bridge.ts +59 -46
  132. package/src/calls/voice-triage-escalate.ts +175 -56
  133. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  134. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  135. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  136. package/src/cli/commands/apps.help.ts +67 -13
  137. package/src/cli/commands/apps.ts +183 -0
  138. package/src/cli/commands/bash.help.ts +8 -13
  139. package/src/cli/commands/bash.ts +7 -25
  140. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  141. package/src/cli/commands/channels/index.help.ts +70 -2
  142. package/src/cli/commands/channels/index.ts +16 -3
  143. package/src/cli/commands/channels/send.ts +128 -0
  144. package/src/cli/commands/credentials.help.ts +6 -2
  145. package/src/cli/commands/credentials.ts +2 -2
  146. package/src/cli/commands/mcp.help.ts +8 -8
  147. package/src/cli/commands/mcp.ts +1 -1
  148. package/src/cli/commands/roadmap.help.ts +5 -1
  149. package/src/cli/commands/roadmap.ts +1 -1
  150. package/src/cli/lib/bundled-marketplace.json +103 -90
  151. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  152. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  153. package/src/config/__tests__/platform-identity.test.ts +255 -0
  154. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  155. package/src/config/acp-agent-write.ts +43 -0
  156. package/src/config/acp-defaults.test.ts +13 -3
  157. package/src/config/acp-defaults.ts +23 -6
  158. package/src/config/acp-schema.test.ts +81 -0
  159. package/src/config/acp-schema.ts +30 -2
  160. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  161. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  162. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  163. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  164. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  165. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  166. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  167. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  168. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  169. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  170. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  171. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  172. package/src/config/default-profile-catalog.ts +1 -1
  173. package/src/config/env.ts +2 -2
  174. package/src/config/feature-flag-registry.json +24 -12
  175. package/src/config/loader.ts +1 -0
  176. package/src/config/memory-v3-gate.test.ts +31 -0
  177. package/src/config/memory-v3-gate.ts +20 -16
  178. package/src/config/platform-identity.ts +201 -0
  179. package/src/config/platform-rehydration.ts +11 -27
  180. package/src/config/schema.ts +2 -2
  181. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  182. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  183. package/src/config/schemas/calls.ts +1 -28
  184. package/src/config/schemas/live-voice.ts +1 -173
  185. package/src/config/schemas/mcp.ts +1 -1
  186. package/src/config/schemas/memory-retrospective.ts +10 -0
  187. package/src/config/schemas/services.ts +8 -0
  188. package/src/config/schemas/skills.ts +0 -14
  189. package/src/config/schemas/voice.ts +150 -0
  190. package/src/credential-execution/process-manager.test.ts +18 -19
  191. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  192. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  193. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  194. package/src/daemon/app-change-notify.ts +45 -0
  195. package/src/daemon/config-watcher.ts +13 -194
  196. package/src/daemon/conversation-process.ts +35 -64
  197. package/src/daemon/conversation-skill-tools.ts +13 -31
  198. package/src/daemon/conversation-store.ts +1 -1
  199. package/src/daemon/conversation-surfaces.ts +10 -8
  200. package/src/daemon/conversation-tool-setup.ts +37 -43
  201. package/src/daemon/conversation.ts +0 -3
  202. package/src/daemon/host-bash-proxy.ts +8 -12
  203. package/src/daemon/host-browser-proxy.ts +15 -1
  204. package/src/daemon/host-file-proxy.ts +8 -10
  205. package/src/daemon/host-transfer-proxy.ts +14 -18
  206. package/src/daemon/lifecycle.ts +0 -5
  207. package/src/daemon/mcp-reload-service.ts +5 -5
  208. package/src/daemon/message-types/sync.ts +1 -0
  209. package/src/daemon/orphan-reaper.ts +1 -1
  210. package/src/daemon/providers-setup.ts +1 -1
  211. package/src/daemon/shutdown-handlers.ts +1 -9
  212. package/src/daemon/skill-memory-refresh.ts +64 -0
  213. package/src/daemon/tool-side-effects.ts +7 -18
  214. package/src/desktop/__tests__/fake-desktop.ts +119 -0
  215. package/src/desktop/desktop-chrome-policy.test.ts +71 -0
  216. package/src/desktop/desktop-chrome-policy.ts +42 -0
  217. package/src/desktop/desktop-dependencies.test.ts +102 -0
  218. package/src/desktop/desktop-dependencies.ts +306 -0
  219. package/src/desktop/desktop-feature.test.ts +18 -0
  220. package/src/desktop/desktop-feature.ts +19 -0
  221. package/src/desktop/desktop-panel-config.test.ts +135 -0
  222. package/src/desktop/desktop-panel-config.ts +149 -0
  223. package/src/desktop/desktop-session-manager.test.ts +627 -0
  224. package/src/desktop/desktop-session-manager.ts +814 -0
  225. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  226. package/src/desktop/desktop-stream-bridge.ts +214 -0
  227. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  228. package/src/desktop/desktop-wallpaper.ts +113 -0
  229. package/src/desktop/desktop-window-theme.ts +170 -0
  230. package/src/home/__tests__/feed-writer.test.ts +22 -0
  231. package/src/home/feed-writer.ts +11 -2
  232. package/src/hooks/types.ts +33 -0
  233. package/src/i18n/AGENTS.md +22 -0
  234. package/src/i18n/__tests__/i18n.test.ts +108 -0
  235. package/src/i18n/index.ts +16 -0
  236. package/src/i18n/locales.ts +93 -0
  237. package/src/i18n/messages.ts +61 -0
  238. package/src/i18n/title.ts +25 -0
  239. package/src/inbound/platform-callback-registration.ts +4 -5
  240. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  241. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  242. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  243. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  244. package/src/live-voice/live-voice-session.ts +213 -693
  245. package/src/live-voice/protocol.ts +0 -2
  246. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  247. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  248. package/src/mcp/effective-config.ts +3 -3
  249. package/src/mcp/mcp-header-store.ts +29 -45
  250. package/src/mcp/spec-schema.ts +111 -0
  251. package/src/mcp/startup.ts +8 -4
  252. package/src/mcp/workspace-mcp-config.ts +188 -0
  253. package/src/messaging/provider.ts +10 -3
  254. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  255. package/src/messaging/providers/callback-routing.ts +18 -0
  256. package/src/messaging/providers/channel-transport.ts +56 -0
  257. package/src/messaging/providers/discord/transport.ts +17 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  259. package/src/messaging/providers/slack/adapter.ts +7 -44
  260. package/src/messaging/providers/slack/transport.ts +16 -0
  261. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  262. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  263. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  264. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  265. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  266. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  267. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  268. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  269. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  270. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  271. package/src/notifications/adapters/macos.ts +3 -0
  272. package/src/notifications/assistant-reply-producer.ts +8 -3
  273. package/src/notifications/conversation-pairing.ts +36 -16
  274. package/src/notifications/delivered-post-record.ts +11 -1
  275. package/src/notifications/guardian-feed-projection.ts +125 -5
  276. package/src/notifications/home-feed-side-effect.ts +55 -7
  277. package/src/notifications/schedule-result-producer.ts +66 -22
  278. package/src/oauth/__tests__/seed-providers-managed.test.ts +54 -0
  279. package/src/oauth/byo-connection.test.ts +45 -0
  280. package/src/oauth/byo-connection.ts +36 -6
  281. package/src/oauth/connection-resolver.test.ts +73 -0
  282. package/src/oauth/connection-resolver.ts +95 -2
  283. package/src/oauth/seed-providers.ts +165 -0
  284. package/src/outbound-proxy/certs.ts +3 -5
  285. package/src/permissions/checker.test.ts +11 -6
  286. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  287. package/src/persistence/conversation-crud.ts +71 -63
  288. package/src/persistence/conversation-lineage.ts +12 -4
  289. package/src/persistence/conversation-title-placeholders.ts +23 -0
  290. package/src/persistence/conversation-title-service.ts +9 -26
  291. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  292. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  293. package/src/persistence/message-cursor.ts +78 -0
  294. package/src/persistence/message-reads.ts +30 -0
  295. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  296. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  297. package/src/persistence/schema/memory-core.ts +5 -0
  298. package/src/platform/client.test.ts +4 -2
  299. package/src/platform/client.ts +2 -10
  300. package/src/plugin-api/constants.ts +2 -0
  301. package/src/plugin-api/index.ts +1 -0
  302. package/src/plugin-api/types.ts +2 -0
  303. package/src/plugins/defaults/index.ts +2 -0
  304. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  305. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  306. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  307. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  308. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  309. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  310. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  311. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  312. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  313. package/src/plugins/defaults/memory/graph/capability-seed.ts +4 -6
  314. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  315. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  316. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  317. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  318. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  319. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  320. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  321. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  322. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  323. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  324. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  325. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  326. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  327. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  328. package/src/plugins/defaults/memory/startup.ts +6 -1
  329. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  330. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  331. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +89 -0
  332. package/src/plugins/defaults/memory/worker.ts +6 -0
  333. package/src/plugins/mcp-servers.ts +15 -75
  334. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  335. package/src/providers/gemini/client.ts +9 -23
  336. package/src/providers/gemini-thought-signature.test.ts +63 -0
  337. package/src/providers/gemini-thought-signature.ts +44 -0
  338. package/src/providers/inference/auth.ts +2 -3
  339. package/src/providers/inference/connection-availability.ts +27 -8
  340. package/src/providers/inference/resolve-auth.ts +22 -4
  341. package/src/providers/inference/service-account-token.ts +248 -0
  342. package/src/providers/model-catalog.ts +34 -0
  343. package/src/providers/model-intents.ts +10 -5
  344. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  345. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  346. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  347. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  348. package/src/providers/openai/chat-completions-provider.ts +192 -78
  349. package/src/providers/openai/codex-models.ts +0 -4
  350. package/src/providers/openai/google-thought-signature.ts +320 -0
  351. package/src/runtime/AGENTS.md +2 -0
  352. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  353. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  354. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  355. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  356. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  357. package/src/runtime/auth/same-actor.ts +38 -3
  358. package/src/runtime/channel-send.ts +316 -0
  359. package/src/runtime/http-server.ts +160 -151
  360. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  361. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  362. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  363. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  364. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  365. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  366. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  367. package/src/runtime/routes/acp-routes.test.ts +15 -0
  368. package/src/runtime/routes/acp-routes.ts +72 -9
  369. package/src/runtime/routes/app-management-routes.ts +73 -0
  370. package/src/runtime/routes/app-routes.ts +1 -1
  371. package/src/runtime/routes/channel-send-routes.ts +85 -0
  372. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  373. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  374. package/src/runtime/routes/credential-routes.ts +19 -10
  375. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  376. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  377. package/src/runtime/routes/diagnostics-routes.ts +55 -3
  378. package/src/runtime/routes/index.ts +4 -2
  379. package/src/runtime/routes/log-export-routes.ts +2 -0
  380. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  381. package/src/runtime/routes/migration-routes.ts +6 -20
  382. package/src/runtime/routes/platform-routes.ts +18 -15
  383. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  384. package/src/runtime/services/conversation-serializer.ts +16 -3
  385. package/src/runtime/sync/resource-sync-events.ts +2 -1
  386. package/src/schedule/worker.ts +1 -1
  387. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  388. package/src/security/ces-http-record-backend.ts +120 -0
  389. package/src/security/ces-rpc-record-backend.ts +7 -0
  390. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  391. package/src/skills/workspace-skill-md-signature.ts +50 -0
  392. package/src/subagent/consult-context.ts +12 -25
  393. package/src/tools/acp/list-agents.test.ts +3 -1
  394. package/src/tools/acp/spawn.test.ts +146 -11
  395. package/src/tools/acp/spawn.ts +21 -3
  396. package/src/tools/acp/status.test.ts +94 -0
  397. package/src/tools/acp/status.ts +40 -3
  398. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  399. package/src/tools/capability-offer.test.ts +22 -2
  400. package/src/tools/capability-offer.ts +16 -1
  401. package/src/tools/computer-use/definitions.ts +4 -2
  402. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  403. package/src/tools/credentials/metadata-store.ts +100 -0
  404. package/src/tools/host-filesystem/edit.test.ts +2 -1
  405. package/src/tools/host-filesystem/edit.ts +2 -2
  406. package/src/tools/host-filesystem/read.test.ts +2 -1
  407. package/src/tools/host-filesystem/read.ts +2 -2
  408. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  409. package/src/tools/host-filesystem/transfer.ts +2 -2
  410. package/src/tools/host-filesystem/write.test.ts +2 -1
  411. package/src/tools/host-filesystem/write.ts +2 -2
  412. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  413. package/src/tools/host-terminal/host-shell.ts +4 -20
  414. package/src/tools/policy-context.ts +6 -7
  415. package/src/tools/subagent/spawn.ts +5 -8
  416. package/src/tools/terminal/__tests__/safe-env.test.ts +6 -6
  417. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  418. package/src/tools/terminal/safe-env.ts +23 -3
  419. package/src/tools/terminal/sanitized-bash.ts +105 -0
  420. package/src/tools/watch/watch-retro-report.ts +0 -8
  421. package/src/util/__tests__/language-subtag.test.ts +23 -1
  422. package/src/util/ca-bundle.ts +13 -0
  423. package/src/util/language-subtag.ts +16 -0
  424. package/src/util/platform.ts +8 -0
  425. package/src/workspace/git-porcelain.ts +66 -0
  426. package/src/workspace/git-service.ts +5 -54
  427. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  428. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  429. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  430. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  431. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  432. package/src/workspace/migrations/registry.ts +6 -0
  433. package/src/__tests__/app-source-watcher.test.ts +0 -218
  434. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  435. package/src/__tests__/speaker-identification.test.ts +0 -56
  436. package/src/calls/speaker-identification.ts +0 -233
  437. package/src/daemon/app-source-watcher.ts +0 -218
  438. package/src/messaging/providers/whatsapp/client.ts +0 -29
  439. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  440. package/src/util/fs-watcher-error.ts +0 -36
  441. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -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",
@@ -934,6 +934,55 @@ describe("thinking-mode tool_choice rejection fallback", () => {
934
934
  expect(text?.text).toBe("ok");
935
935
  });
936
936
 
937
+ test("retries once when Kimi rejects a specified tool_choice in thinking mode", async () => {
938
+ const { provider, requests } = stubProviderWithErrors(
939
+ [
940
+ rejection(
941
+ "tool_choice 'specified' is incompatible with thinking enabled",
942
+ ),
943
+ ],
944
+ OK_CHUNKS,
945
+ );
946
+
947
+ const response = await provider.sendMessage(
948
+ [{ role: "user", content: [{ type: "text", text: "hi" }] }],
949
+ {
950
+ tools: [
951
+ {
952
+ name: "select_pages",
953
+ description: "Pick relevant memory pages",
954
+ input_schema: { type: "object", properties: {} },
955
+ },
956
+ ],
957
+ config: {
958
+ tool_choice: { type: "tool", name: "select_pages" },
959
+ effort: "high",
960
+ },
961
+ },
962
+ );
963
+
964
+ expect(requests).toHaveLength(2);
965
+ const first = requests[0] as {
966
+ tool_choice?: { type: string; function: { name: string } };
967
+ reasoning_effort?: string;
968
+ };
969
+ const second = requests[1] as {
970
+ tool_choice?: unknown;
971
+ reasoning_effort?: string;
972
+ };
973
+ expect(first.tool_choice).toEqual({
974
+ type: "function",
975
+ function: { name: "select_pages" },
976
+ });
977
+ expect(first.reasoning_effort).toBe("high");
978
+ expect(second.tool_choice).toBeUndefined();
979
+ expect(second.reasoning_effort).toBe("high");
980
+ const text = response.content.find((b) => b.type === "text") as
981
+ | { type: "text"; text: string }
982
+ | undefined;
983
+ expect(text?.text).toBe("ok");
984
+ });
985
+
937
986
  test("retries once for an OpenRouter-wrapped thinking-mode tool_choice rejection", async () => {
938
987
  const wrapped = new OpenAI.APIError(
939
988
  400,
@@ -1141,33 +1190,80 @@ describe("unknown assistant reasoning field rejection fallback", () => {
1141
1190
  },
1142
1191
  ];
1143
1192
 
1144
- test("retries once without reasoning_content when a strict schema rejects it", async () => {
1193
+ test.each([
1194
+ [
1195
+ "a strict schema",
1196
+ "Additional properties are not allowed ('reasoning_content' was unexpected)",
1197
+ ],
1198
+ ["Groq", "property 'reasoning_content' is unsupported"],
1199
+ ])(
1200
+ "retries once without reasoning_content when %s rejects it",
1201
+ async (_upstream, message) => {
1202
+ const { provider, requests } = stubProviderWithErrors(
1203
+ [rejection(message)],
1204
+ OK_CHUNKS,
1205
+ { assistantReasoningField: "reasoning_content" },
1206
+ );
1207
+
1208
+ const response = await provider.sendMessage(thinkingHistory);
1209
+
1210
+ expect(requests).toHaveLength(2);
1211
+ const first = requests[0] as {
1212
+ messages: Array<{ reasoning_content?: string; content: string | null }>;
1213
+ };
1214
+ const second = requests[1] as {
1215
+ messages: Array<{ reasoning_content?: string; content: string | null }>;
1216
+ };
1217
+ expect(first.messages[0].reasoning_content).toBe("hidden chain");
1218
+ expect(second.messages[0].reasoning_content).toBeUndefined();
1219
+ expect(second.messages[0].content).toBe("answer");
1220
+ const text = response.content.find((b) => b.type === "text") as
1221
+ | { type: "text"; text: string }
1222
+ | undefined;
1223
+ expect(text?.text).toBe("ok");
1224
+ },
1225
+ );
1226
+
1227
+ test("preserves assistant tool calls when stripping an unsupported reasoning_content", async () => {
1145
1228
  const { provider, requests } = stubProviderWithErrors(
1146
- [
1147
- rejection(
1148
- "Additional properties are not allowed ('reasoning_content' was unexpected)",
1149
- ),
1150
- ],
1229
+ [rejection("property 'reasoning_content' is unsupported")],
1151
1230
  OK_CHUNKS,
1152
1231
  { assistantReasoningField: "reasoning_content" },
1153
1232
  );
1154
1233
 
1155
- const response = await provider.sendMessage(thinkingHistory);
1234
+ await provider.sendMessage([
1235
+ {
1236
+ role: "assistant",
1237
+ content: [
1238
+ { type: "thinking", thinking: "hidden chain", signature: "" },
1239
+ {
1240
+ type: "tool_use",
1241
+ id: "call_1",
1242
+ name: "lookup",
1243
+ input: { q: "x" },
1244
+ },
1245
+ ],
1246
+ },
1247
+ {
1248
+ role: "user",
1249
+ content: [
1250
+ { type: "tool_result", tool_use_id: "call_1", content: "found" },
1251
+ ],
1252
+ },
1253
+ ]);
1156
1254
 
1157
1255
  expect(requests).toHaveLength(2);
1158
- const first = requests[0] as {
1159
- messages: Array<{ reasoning_content?: string; content: string | null }>;
1160
- };
1161
1256
  const second = requests[1] as {
1162
- messages: Array<{ reasoning_content?: string; content: string | null }>;
1257
+ messages: Array<{
1258
+ role: string;
1259
+ reasoning_content?: string;
1260
+ tool_calls?: Array<{ id: string; function: { name: string } }>;
1261
+ }>;
1163
1262
  };
1164
- expect(first.messages[0].reasoning_content).toBe("hidden chain");
1165
- expect(second.messages[0].reasoning_content).toBeUndefined();
1166
- expect(second.messages[0].content).toBe("answer");
1167
- const text = response.content.find((b) => b.type === "text") as
1168
- | { type: "text"; text: string }
1169
- | undefined;
1170
- expect(text?.text).toBe("ok");
1263
+ const assistant = second.messages.find((m) => m.role === "assistant");
1264
+ expect(assistant?.reasoning_content).toBeUndefined();
1265
+ expect(assistant?.tool_calls?.[0]?.id).toBe("call_1");
1266
+ expect(assistant?.tool_calls?.[0]?.function.name).toBe("lookup");
1171
1267
  });
1172
1268
 
1173
1269
  test("does not strip reasoning_content on a must-be-passed-back error", async () => {
@@ -1189,6 +1285,38 @@ describe("unknown assistant reasoning field rejection fallback", () => {
1189
1285
  ).toBe("hidden chain");
1190
1286
  });
1191
1287
 
1288
+ test("does not strip reasoning_content on a 429 output-token limit", async () => {
1289
+ const { provider, requests } = stubProviderWithErrors(
1290
+ [
1291
+ rejection(
1292
+ "Rate limit reached for model `qwen/qwen3-32b` in organization `org_x` on tokens per minute (TPM): Limit 1000, Requested 4000.",
1293
+ 429,
1294
+ ),
1295
+ ],
1296
+ OK_CHUNKS,
1297
+ { assistantReasoningField: "reasoning_content" },
1298
+ );
1299
+
1300
+ await expect(provider.sendMessage(thinkingHistory)).rejects.toThrow();
1301
+ expect(requests).toHaveLength(1);
1302
+ });
1303
+
1304
+ test("does not strip reasoning_content on a 413 input-token limit", async () => {
1305
+ const { provider, requests } = stubProviderWithErrors(
1306
+ [
1307
+ rejection(
1308
+ "Request too large for model `qwen/qwen3-32b` in organization `org_x` on tokens per minute (TPM): Limit 7000, Requested 24936.",
1309
+ 413,
1310
+ ),
1311
+ ],
1312
+ OK_CHUNKS,
1313
+ { assistantReasoningField: "reasoning_content" },
1314
+ );
1315
+
1316
+ await expect(provider.sendMessage(thinkingHistory)).rejects.toThrow();
1317
+ expect(requests).toHaveLength(1);
1318
+ });
1319
+
1192
1320
  test("does not retry unknown-field 500s", async () => {
1193
1321
  const { provider, requests } = stubProviderWithErrors(
1194
1322
  [