@vellumai/assistant 0.11.6 → 0.11.7-staging.1

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 (447) hide show
  1. package/ARCHITECTURE.md +31 -19
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +3 -3
  4. package/docs/flux-turn-detection-spike.md +8 -7
  5. package/docs/runbook-trusted-contacts.md +14 -0
  6. package/docs/stt-provider-onboarding.md +8 -4
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
  9. package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
  10. package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
  11. package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
  12. package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
  13. package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
  14. package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
  15. package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  19. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  20. package/node_modules/@vellumai/environments/package.json +2 -1
  21. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  22. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  30. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  32. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  33. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  34. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  35. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  36. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  37. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  38. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  39. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  40. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  41. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  42. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  43. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  44. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  45. package/openapi.yaml +419 -55
  46. package/package.json +4 -1
  47. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  48. package/src/__tests__/approval-card-edit.test.ts +153 -0
  49. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  50. package/src/__tests__/available-skills.test.ts +54 -0
  51. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  52. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  53. package/src/__tests__/channel-policy.test.ts +1 -3
  54. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  55. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  56. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  57. package/src/__tests__/config-schema.test.ts +2 -3
  58. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  59. package/src/__tests__/conversation-attachments.test.ts +0 -1
  60. package/src/__tests__/conversation-error.test.ts +44 -0
  61. package/src/__tests__/conversation-evictor.test.ts +19 -0
  62. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  63. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  64. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  65. package/src/__tests__/conversation-slash.test.ts +9 -0
  66. package/src/__tests__/conversation-title-service.test.ts +97 -0
  67. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  68. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  69. package/src/__tests__/credential-broker.test.ts +43 -420
  70. package/src/__tests__/credential-routes.test.ts +17 -1
  71. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  72. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  73. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  74. package/src/__tests__/discord-channel-config.test.ts +237 -0
  75. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  76. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  77. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  78. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  79. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  80. package/src/__tests__/image-conversion.test.ts +38 -4
  81. package/src/__tests__/install-skill-routing.test.ts +25 -0
  82. package/src/__tests__/llm-resolver.test.ts +74 -0
  83. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  84. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  85. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  86. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  87. package/src/__tests__/mcp-cli.test.ts +23 -1
  88. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  89. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  90. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  91. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  92. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  93. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  94. package/src/__tests__/platform.test.ts +19 -1
  95. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  96. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  97. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  98. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  99. package/src/__tests__/schedule-retry.test.ts +183 -4
  100. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  101. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  102. package/src/__tests__/skill-load-tool.test.ts +34 -0
  103. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  104. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  105. package/src/__tests__/skills.test.ts +25 -0
  106. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  107. package/src/__tests__/terminal-tools.test.ts +2 -0
  108. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  109. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  110. package/src/__tests__/tool-audit.test.ts +111 -40
  111. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  112. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  113. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  114. package/src/__tests__/voice-config-update.test.ts +43 -0
  115. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  116. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  117. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  118. package/src/agent/loop.ts +31 -2
  119. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  120. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  121. package/src/avatar/avatar-manifest.ts +48 -100
  122. package/src/avatar/avatar-store.ts +7 -7
  123. package/src/avatar/ensure-raster.ts +146 -0
  124. package/src/avatar/resvg-lazy.test.ts +0 -1
  125. package/src/avatar/traits-png-sync.ts +9 -10
  126. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  127. package/src/backup/__tests__/paths.test.ts +6 -2
  128. package/src/backup/paths.ts +6 -4
  129. package/src/bundler/app-compiler.ts +2 -2
  130. package/src/bundler/compiler-tools.test.ts +36 -0
  131. package/src/bundler/compiler-tools.ts +54 -18
  132. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  133. package/src/calls/__tests__/voice-session-bridge.test.ts +20 -0
  134. package/src/calls/media-stream-stt-session.ts +2 -1
  135. package/src/calls/telephony-synthesis-language.ts +13 -5
  136. package/src/calls/voice-session-bridge.ts +6 -4
  137. package/src/channels/config.ts +23 -12
  138. package/src/cli/AGENTS.md +3 -1
  139. package/src/cli/commands/backup.help.ts +10 -13
  140. package/src/cli/commands/contacts.help.ts +16 -4
  141. package/src/cli/commands/contacts.ts +5 -1
  142. package/src/cli/commands/credentials.help.ts +1 -1
  143. package/src/cli/commands/inference-providers.ts +18 -53
  144. package/src/cli/commands/mcp.help.ts +6 -5
  145. package/src/cli/commands/mcp.ts +1 -1
  146. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  147. package/src/cli/commands/stt.help.ts +2 -2
  148. package/src/config/__tests__/backup-schema.test.ts +1 -1
  149. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  150. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  151. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  152. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  153. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  154. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  155. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  156. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  157. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  158. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  159. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  160. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  161. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  162. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  163. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  164. package/src/config/default-profile-catalog.ts +195 -10
  165. package/src/config/default-profile-names.ts +41 -0
  166. package/src/config/feature-flag-registry.json +18 -2
  167. package/src/config/inference-profile-validation.ts +11 -2
  168. package/src/config/llm-resolver.ts +25 -0
  169. package/src/config/loader.ts +3 -0
  170. package/src/config/managed-speech-defaults.ts +160 -10
  171. package/src/config/schema.ts +2 -0
  172. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  173. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  174. package/src/config/schemas/backup.ts +1 -1
  175. package/src/config/schemas/channels.ts +32 -0
  176. package/src/config/schemas/live-voice.ts +13 -4
  177. package/src/config/schemas/llm.ts +259 -11
  178. package/src/config/schemas/mcp.ts +10 -2
  179. package/src/config/schemas/memory-retrospective.ts +1 -19
  180. package/src/config/schemas/services.ts +24 -0
  181. package/src/config/schemas/stt.ts +128 -5
  182. package/src/config/seed-inference-profiles.ts +32 -3
  183. package/src/config/skill-state.ts +5 -0
  184. package/src/config/skills.ts +21 -0
  185. package/src/contacts/notify-contacts-changed.ts +5 -2
  186. package/src/context/compactor.ts +74 -1
  187. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  188. package/src/daemon/config-watcher.ts +2 -1
  189. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  190. package/src/daemon/conversation-agent-loop.ts +86 -10
  191. package/src/daemon/conversation-error.ts +25 -0
  192. package/src/daemon/conversation-evictor.ts +2 -4
  193. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  194. package/src/daemon/conversation-skill-tools.ts +5 -1
  195. package/src/daemon/conversation-slash.ts +2 -2
  196. package/src/daemon/conversation-store.ts +8 -1
  197. package/src/daemon/conversation-tool-setup.ts +37 -0
  198. package/src/daemon/conversation.ts +0 -4
  199. package/src/daemon/handlers/channel-config-result.ts +30 -0
  200. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  201. package/src/daemon/handlers/config-ingress.ts +9 -0
  202. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  203. package/src/daemon/handlers/config-telegram.ts +2 -5
  204. package/src/daemon/handlers/conversations.ts +0 -2
  205. package/src/daemon/handlers/skills.ts +25 -7
  206. package/src/daemon/lifecycle.ts +3 -1
  207. package/src/daemon/message-types/messages.ts +0 -2
  208. package/src/daemon/tool-setup-types.ts +5 -3
  209. package/src/daemon/trust-context-types.test.ts +33 -0
  210. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  211. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  212. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  213. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  214. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  215. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  216. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  217. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  218. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  219. package/src/live-voice/live-voice-session-manager.ts +39 -2
  220. package/src/live-voice/live-voice-session.ts +213 -8
  221. package/src/live-voice/protocol.ts +187 -1
  222. package/src/messaging/providers/discord/api.ts +2 -1
  223. package/src/messaging/providers/discord/withdraw.ts +62 -0
  224. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  225. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  226. package/src/monitoring/db-integrity-sample.ts +1 -0
  227. package/src/monitoring/file-descriptors.ts +39 -6
  228. package/src/monitoring/process-memory.ts +37 -5
  229. package/src/monitoring/resource-sampler.ts +25 -8
  230. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  231. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  232. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  233. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  234. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  235. package/src/notifications/activity-failed-dedupe.ts +95 -0
  236. package/src/notifications/adapters/discord.ts +132 -0
  237. package/src/notifications/adapters/shared.ts +16 -0
  238. package/src/notifications/adapters/slack.ts +6 -1
  239. package/src/notifications/adapters/telegram.ts +5 -8
  240. package/src/notifications/approval-card-data.ts +94 -1
  241. package/src/notifications/background-failure-signal.ts +116 -0
  242. package/src/notifications/broadcaster.ts +3 -9
  243. package/src/notifications/copy-composer.ts +41 -8
  244. package/src/notifications/decision-engine.ts +55 -0
  245. package/src/notifications/destination-resolver.ts +45 -5
  246. package/src/notifications/emit-signal.ts +19 -5
  247. package/src/notifications/guardian-question-mode.ts +57 -0
  248. package/src/notifications/types.ts +2 -8
  249. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  250. package/src/oauth/seed-providers.ts +261 -0
  251. package/src/permissions/checker.test.ts +21 -0
  252. package/src/permissions/checker.ts +6 -0
  253. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  254. package/src/permissions/gateway-threshold-reader.ts +144 -10
  255. package/src/permissions/prompter.ts +0 -6
  256. package/src/permissions/types.ts +5 -4
  257. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  258. package/src/persistence/bulk-write-gate.ts +9 -10
  259. package/src/persistence/conversation-crud.ts +85 -133
  260. package/src/persistence/conversation-title-service.ts +43 -29
  261. package/src/persistence/embeddings/embedding-local.ts +14 -85
  262. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  263. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  264. package/src/persistence/lifecycle-events-store.ts +75 -5
  265. package/src/platform/platform-patch-queue.test.ts +368 -0
  266. package/src/platform/platform-patch-queue.ts +190 -0
  267. package/src/platform/sync-avatar.test.ts +527 -0
  268. package/src/platform/sync-avatar.ts +209 -0
  269. package/src/platform/sync-identity.test.ts +75 -0
  270. package/src/platform/sync-identity.ts +17 -110
  271. package/src/plugin-api/model-profiles.test.ts +2 -2
  272. package/src/plugin-api/model-profiles.ts +7 -7
  273. package/src/plugin-api/transcription-session.ts +1 -1
  274. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  275. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  276. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  277. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  278. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  279. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  280. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  281. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  282. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  283. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  284. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  285. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  286. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  287. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  288. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  289. package/src/plugins/defaults/memory/worker.ts +7 -0
  290. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  291. package/src/providers/connection-resolution.ts +2 -17
  292. package/src/providers/fallback-breaker.ts +389 -0
  293. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  294. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  295. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  296. package/src/providers/inference/adapter-factory.ts +231 -3
  297. package/src/providers/inference/auth.ts +1 -0
  298. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  299. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  300. package/src/providers/inference/resolve-auth.ts +1 -1
  301. package/src/providers/media-resolve.ts +2 -3
  302. package/src/providers/model-catalog.ts +75 -0
  303. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  304. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  305. package/src/providers/openai/api-error-normalization.ts +13 -0
  306. package/src/providers/openai/chat-completions-provider.ts +84 -0
  307. package/src/providers/opencode/client.test.ts +150 -0
  308. package/src/providers/opencode/client.ts +61 -0
  309. package/src/providers/provider-resolvability.ts +20 -0
  310. package/src/providers/retry.ts +956 -68
  311. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  312. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  313. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  314. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  315. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  316. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  317. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  318. package/src/providers/speech-to-text/resolve.ts +132 -24
  319. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  320. package/src/providers/types.ts +39 -0
  321. package/src/providers/usage-tracking.ts +12 -1
  322. package/src/runtime/AGENTS.md +1 -2
  323. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  324. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  325. package/src/runtime/agent-wake.ts +44 -5
  326. package/src/runtime/background-job-runner.ts +68 -50
  327. package/src/runtime/channel-readiness-service.ts +0 -39
  328. package/src/runtime/http-server.ts +12 -4
  329. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  330. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  331. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  332. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  333. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  334. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  335. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  336. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  337. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  338. package/src/runtime/routes/approval-routes.ts +0 -12
  339. package/src/runtime/routes/avatar-routes.ts +17 -30
  340. package/src/runtime/routes/backup-routes.ts +2 -2
  341. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  342. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  343. package/src/runtime/routes/contact-routes.ts +20 -15
  344. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  345. package/src/runtime/routes/conversation-routes.ts +2 -2
  346. package/src/runtime/routes/credential-routes.ts +2 -1
  347. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  348. package/src/runtime/routes/inbound-message-handler.ts +21 -9
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  350. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  351. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  352. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  353. package/src/runtime/routes/index.ts +2 -0
  354. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  355. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  356. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  357. package/src/runtime/routes/inference-send-routes.ts +2 -2
  358. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  359. package/src/runtime/routes/integrations/discord.ts +119 -0
  360. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  361. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  362. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  363. package/src/runtime/routes/secret-routes.ts +7 -0
  364. package/src/runtime/routes/stt-routes.ts +38 -6
  365. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  366. package/src/runtime/routes/watch-routes.ts +9 -5
  367. package/src/runtime/sync/resource-sync-events.ts +2 -0
  368. package/src/runtime/trust-verdict-consumer.ts +3 -3
  369. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  370. package/src/schedule/retry-policy.ts +8 -8
  371. package/src/schedule/run-script.ts +14 -23
  372. package/src/schedule/schedule-recovery.ts +0 -1
  373. package/src/schedule/scheduler.ts +89 -51
  374. package/src/schedule/worker.ts +57 -0
  375. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  376. package/src/security/oauth2.ts +7 -0
  377. package/src/security/openai-device-auth.ts +364 -0
  378. package/src/skills/available-skills.ts +10 -1
  379. package/src/skills/catalog-install.ts +19 -1
  380. package/src/skills/inline-command-runner.ts +6 -2
  381. package/src/skills/platform-compatibility.test.ts +49 -0
  382. package/src/skills/platform-compatibility.ts +65 -0
  383. package/src/skills/tool-manifest.ts +20 -0
  384. package/src/stt/__tests__/roles.test.ts +152 -0
  385. package/src/stt/daemon-batch-transcriber.ts +4 -1
  386. package/src/stt/roles.ts +180 -0
  387. package/src/stt/stt-stream-session.ts +3 -2
  388. package/src/stt/types.ts +33 -3
  389. package/src/subagent/manager.ts +12 -0
  390. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  391. package/src/telemetry/live-voice-funnel.ts +12 -2
  392. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  393. package/src/telemetry/tool-audit.ts +83 -30
  394. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  395. package/src/telemetry/turn-outcome.ts +75 -4
  396. package/src/tools/client-os.ts +115 -0
  397. package/src/tools/computer-use/definitions.ts +11 -7
  398. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  399. package/src/tools/credentials/broker-types.ts +0 -71
  400. package/src/tools/credentials/broker.ts +6 -201
  401. package/src/tools/executor.ts +2 -8
  402. package/src/tools/host-shell.test.ts +75 -0
  403. package/src/tools/host-shell.ts +24 -0
  404. package/src/tools/host-terminal/host-shell.ts +15 -44
  405. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  406. package/src/tools/permission-checker.ts +73 -21
  407. package/src/tools/policy-context.ts +1 -0
  408. package/src/tools/shared/filesystem/image-read.ts +1 -1
  409. package/src/tools/skills/find-similar-skills.ts +10 -2
  410. package/src/tools/skills/load.ts +41 -6
  411. package/src/tools/skills/sandbox-runner.ts +8 -16
  412. package/src/tools/skills/skill-tool-factory.ts +1 -0
  413. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  414. package/src/tools/terminal/safe-env.ts +39 -9
  415. package/src/tools/terminal/shell.ts +12 -31
  416. package/src/tools/tool-approval-handler.ts +64 -10
  417. package/src/tools/tool-types.ts +10 -0
  418. package/src/tools/types.ts +19 -3
  419. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  420. package/src/tools/workflows/manage-workflows.ts +2 -2
  421. package/src/tools/workflows/run-workflow.test.ts +2 -1
  422. package/src/util/__tests__/file-use.test.ts +61 -0
  423. package/src/util/__tests__/process-table.test.ts +66 -0
  424. package/src/util/__tests__/process-tree.test.ts +46 -0
  425. package/src/util/errors.ts +1 -0
  426. package/src/util/file-use.ts +91 -0
  427. package/src/util/host-process.test.ts +17 -0
  428. package/src/util/host-process.ts +75 -0
  429. package/src/util/image-conversion.ts +39 -7
  430. package/src/util/platform.ts +32 -13
  431. package/src/util/process-table.ts +249 -0
  432. package/src/util/process-tree.ts +85 -129
  433. package/src/util/provider-error-patterns.ts +20 -0
  434. package/src/util/spawn.ts +9 -2
  435. package/src/workspace/git-service.ts +4 -14
  436. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  437. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  438. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  439. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  440. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  441. package/src/workspace/migrations/registry.ts +8 -0
  442. package/src/__tests__/permission-types.test.ts +0 -17
  443. package/src/channels/gateway-discord-admission.ts +0 -32
  444. package/src/persistence/fork-message-copy.ts +0 -239
  445. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  446. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  447. package/src/security/oauth2-device-code.ts +0 -312
@@ -0,0 +1,198 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { ProviderError } from "../../../util/errors.js";
4
+ import { OpenAIChatCompletionsProvider } from "../chat-completions-provider.js";
5
+
6
+ type MockChunk = {
7
+ choices: Array<{
8
+ delta: { content?: string | null };
9
+ finish_reason?: string | null;
10
+ }>;
11
+ usage?: { prompt_tokens: number; completion_tokens: number };
12
+ };
13
+
14
+ function makeStream(chunks: MockChunk[]): AsyncIterable<MockChunk> {
15
+ return {
16
+ async *[Symbol.asyncIterator]() {
17
+ for (const c of chunks) {
18
+ yield c;
19
+ }
20
+ },
21
+ };
22
+ }
23
+
24
+ const OK_CHUNKS: MockChunk[] = [
25
+ {
26
+ choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
27
+ usage: { prompt_tokens: 1, completion_tokens: 1 },
28
+ },
29
+ ];
30
+
31
+ function stubProviderWithErrors(errors: unknown[]): {
32
+ provider: OpenAIChatCompletionsProvider;
33
+ requests: unknown[];
34
+ } {
35
+ const provider = new OpenAIChatCompletionsProvider("test-key", "test-model");
36
+ const requests: unknown[] = [];
37
+ const pending = [...errors];
38
+ (provider as unknown as { client: unknown }).client = {
39
+ chat: {
40
+ completions: {
41
+ create: async (params: unknown) => {
42
+ // Snapshot: the fallback mutates `params` between attempts.
43
+ requests.push(JSON.parse(JSON.stringify(params)));
44
+ const error = pending.shift();
45
+ if (error !== undefined) {
46
+ throw error;
47
+ }
48
+ return makeStream(OK_CHUNKS);
49
+ },
50
+ },
51
+ },
52
+ };
53
+ return { provider, requests };
54
+ }
55
+
56
+ // Together's server-side renderer error for MiniMax M3, verbatim.
57
+ const CHAT_TEMPLATE_400 =
58
+ "Failed to apply chat template: invalid operation: object is not callable (in chat:22)";
59
+
60
+ function rejection(message: string, status = 400): Error {
61
+ return Object.assign(new Error(message), { status });
62
+ }
63
+
64
+ const TINY_PNG =
65
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
66
+
67
+ describe("chat-template rejection flatten fallback", () => {
68
+ test("retries once with flattened string content when the chat template rejects a text-only content-parts array", async () => {
69
+ // GIVEN an endpoint whose chat template 400s on structured message content
70
+ const { provider, requests } = stubProviderWithErrors([
71
+ rejection(CHAT_TEMPLATE_400),
72
+ ]);
73
+
74
+ // WHEN a multi-block user message of purely textual parts is sent
75
+ const response = await provider.sendMessage([
76
+ {
77
+ role: "user",
78
+ content: [
79
+ { type: "text", text: "first paragraph" },
80
+ { type: "text", text: "second paragraph" },
81
+ ],
82
+ },
83
+ ]);
84
+
85
+ // THEN the first request carried a content-parts array
86
+ expect(requests).toHaveLength(2);
87
+ const first = requests[0] as {
88
+ messages: Array<{ content: unknown }>;
89
+ };
90
+ expect(Array.isArray(first.messages[0].content)).toBe(true);
91
+
92
+ // AND the retry flattened it to a plain string keeping all the text
93
+ const second = requests[1] as {
94
+ messages: Array<{ content: unknown }>;
95
+ };
96
+ const flattened = second.messages[0].content;
97
+ expect(flattened).toBe("first paragraph\n\nsecond paragraph");
98
+
99
+ // AND the retry succeeded
100
+ const text = response.content.find((b) => b.type === "text") as
101
+ | { type: "text"; text: string }
102
+ | undefined;
103
+ expect(text?.text).toBe("ok");
104
+ });
105
+
106
+ test("does not retry when a content-parts array carries media (never silently drop an image)", async () => {
107
+ // GIVEN an endpoint whose chat template 400s on structured message content
108
+ const { provider, requests } = stubProviderWithErrors([
109
+ rejection(CHAT_TEMPLATE_400),
110
+ ]);
111
+
112
+ // WHEN a multi-block user message with an image part is sent
113
+ const promise = provider.sendMessage([
114
+ {
115
+ role: "user",
116
+ content: [
117
+ { type: "text", text: "what is in this image?" },
118
+ {
119
+ type: "image",
120
+ source: {
121
+ type: "base64",
122
+ media_type: "image/png",
123
+ data: TINY_PNG,
124
+ },
125
+ },
126
+ ],
127
+ },
128
+ ]);
129
+
130
+ // THEN the error propagates with no retry, so downstream classification
131
+ // can surface the capability mismatch instead of answering without the image
132
+ await expect(promise).rejects.toBeInstanceOf(ProviderError);
133
+ expect(requests).toHaveLength(1);
134
+ });
135
+
136
+ test("does not retry when the messages are already plain strings", async () => {
137
+ // GIVEN a chat-template 400 on a request whose content is already a
138
+ // plain string (flattening would change nothing, so retrying is futile)
139
+ const { provider, requests } = stubProviderWithErrors([
140
+ rejection(CHAT_TEMPLATE_400),
141
+ ]);
142
+
143
+ // WHEN a single-text-block user message is sent
144
+ const promise = provider.sendMessage([
145
+ { role: "user", content: [{ type: "text", text: "hi" }] },
146
+ ]);
147
+
148
+ // THEN the error propagates as a ProviderError with no retry attempt
149
+ await expect(promise).rejects.toBeInstanceOf(ProviderError);
150
+ expect(requests).toHaveLength(1);
151
+ });
152
+
153
+ test("does not retry on 5xx even when the message mentions the chat template", async () => {
154
+ // GIVEN a 500 whose body happens to mention the chat template (a server
155
+ // fault, not a request-shape rejection)
156
+ const { provider, requests } = stubProviderWithErrors([
157
+ rejection(CHAT_TEMPLATE_400, 500),
158
+ ]);
159
+
160
+ // WHEN a multi-block user message is sent
161
+ const promise = provider.sendMessage([
162
+ {
163
+ role: "user",
164
+ content: [
165
+ { type: "text", text: "a" },
166
+ { type: "text", text: "b" },
167
+ ],
168
+ },
169
+ ]);
170
+
171
+ // THEN the error propagates with no retry attempt
172
+ await expect(promise).rejects.toBeInstanceOf(ProviderError);
173
+ expect(requests).toHaveLength(1);
174
+ });
175
+
176
+ test("surfaces the retry's error when the flattened request also fails", async () => {
177
+ // GIVEN an endpoint that rejects both the structured and the flattened shape
178
+ const { provider, requests } = stubProviderWithErrors([
179
+ rejection(CHAT_TEMPLATE_400),
180
+ rejection(CHAT_TEMPLATE_400),
181
+ ]);
182
+
183
+ // WHEN a multi-block user message is sent
184
+ const promise = provider.sendMessage([
185
+ {
186
+ role: "user",
187
+ content: [
188
+ { type: "text", text: "a" },
189
+ { type: "text", text: "b" },
190
+ ],
191
+ },
192
+ ]);
193
+
194
+ // THEN the failure propagates after exactly one retry
195
+ await expect(promise).rejects.toBeInstanceOf(ProviderError);
196
+ expect(requests).toHaveLength(2);
197
+ });
198
+ });
@@ -4,6 +4,7 @@ import type { ProviderErrorReason } from "../../util/errors.js";
4
4
  import {
5
5
  DAILY_LIMIT_PATTERNS,
6
6
  INSUFFICIENT_CREDITS_PATTERNS,
7
+ isChatTemplateFailureError,
7
8
  VISION_NOT_SUPPORTED_PATTERNS,
8
9
  } from "../../util/provider-error-patterns.js";
9
10
 
@@ -81,6 +82,18 @@ export function deriveReason(
81
82
  return "vision_unsupported";
82
83
  }
83
84
 
85
+ // A 4xx from the endpoint's server-side chat-template renderer: the request
86
+ // shape (content-parts arrays, tool payloads) is what the template can't
87
+ // handle, not the credentials or the model.
88
+ if (
89
+ status !== undefined &&
90
+ status >= 400 &&
91
+ status < 500 &&
92
+ isChatTemplateFailureError(haystack)
93
+ ) {
94
+ return "request_shape_unsupported";
95
+ }
96
+
84
97
  // The managed proxy's daily-limit 402 shares the status with generic credit
85
98
  // exhaustion; match its specific body code first so it isn't swallowed.
86
99
  if (DAILY_LIMIT_PATTERNS.some((re) => re.test(haystack))) {
@@ -4,6 +4,7 @@ import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../prompts/cache-boundary.js";
4
4
  import { isAbortReason } from "../../util/abort-reasons.js";
5
5
  import { ProviderError, type ProviderErrorReason } from "../../util/errors.js";
6
6
  import { getLogger } from "../../util/logger.js";
7
+ import { isChatTemplateFailureError } from "../../util/provider-error-patterns.js";
7
8
  import { extractRetryAfterMs } from "../../util/retry.js";
8
9
  import { partialTagSuffix as sharedPartialTagSuffix } from "../../util/think-tag-stream.js";
9
10
  import { escapeXmlAttr } from "../../util/xml.js";
@@ -529,6 +530,74 @@ function isUnknownAssistantReasoningFieldRejection(
529
530
  );
530
531
  }
531
532
 
533
+ function isTextualContentPart(part: { type: string }): boolean {
534
+ return part.type === "text" || part.type === "refusal";
535
+ }
536
+
537
+ function messagesCarryFlattenableContentPartsArrays(params: unknown): boolean {
538
+ const messages = paramsMessages(params);
539
+ if (!messages) {
540
+ return false;
541
+ }
542
+ const arrays = messages.filter((msg) => Array.isArray(msg.content));
543
+ if (arrays.length === 0) {
544
+ return false;
545
+ }
546
+ return arrays.every((msg) =>
547
+ (msg.content as Array<{ type: string }>).every(isTextualContentPart),
548
+ );
549
+ }
550
+
551
+ /**
552
+ * True when the endpoint's server-side chat-template renderer rejected the
553
+ * request and every content-parts array in the outbound messages is purely
554
+ * textual, so flattening to plain strings loses nothing. Some template
555
+ * engines behind OpenAI-compatible endpoints only render string message
556
+ * content (Together serving MiniMax M3 400s with `Failed to apply chat
557
+ * template: invalid operation: object is not callable`); one retry with
558
+ * flattened content lets the same endpoint succeed instead of surfacing the
559
+ * raw template error. Requests carrying media parts are never flattened:
560
+ * silently dropping an image or audio blob would let the model answer
561
+ * without it, so those rejections propagate to error classification.
562
+ */
563
+ function isChatTemplateRejection(error: unknown, params: unknown): boolean {
564
+ if (!isClientErrorStatus(error)) {
565
+ return false;
566
+ }
567
+ if (!isChatTemplateFailureError(openaiCompatErrorHaystack(error))) {
568
+ return false;
569
+ }
570
+ return messagesCarryFlattenableContentPartsArrays(params);
571
+ }
572
+
573
+ /**
574
+ * Rewrite every purely-textual content-parts array in `params.messages` into
575
+ * a plain string. Returns whether any message changed.
576
+ */
577
+ function flattenContentPartsToStrings(params: unknown): boolean {
578
+ const messages = paramsMessages(params);
579
+ if (!messages) {
580
+ return false;
581
+ }
582
+ let flattened = false;
583
+ for (const msg of messages) {
584
+ if (!Array.isArray(msg.content)) {
585
+ continue;
586
+ }
587
+ const parts = msg.content as Array<
588
+ { type: "text"; text: string } | { type: "refusal"; refusal: string }
589
+ >;
590
+ if (!parts.every(isTextualContentPart)) {
591
+ continue;
592
+ }
593
+ msg.content = parts
594
+ .map((part) => (part.type === "text" ? part.text : part.refusal))
595
+ .join("\n\n");
596
+ flattened = true;
597
+ }
598
+ return flattened;
599
+ }
600
+
532
601
  /**
533
602
  * Translate the neutral (Anthropic-shaped) `tool_choice` carried on the call
534
603
  * config into the OpenAI chat-completions wire format. Callers express
@@ -656,6 +725,9 @@ export class OpenAIChatCompletionsProvider implements Provider {
656
725
  const usageAttributionHeaders = configObj?.usageAttributionHeaders as
657
726
  | Record<string, string>
658
727
  | undefined;
728
+ const perRequestHeaders = configObj?.requestHeaders as
729
+ | Record<string, string>
730
+ | undefined;
659
731
 
660
732
  // Per-tool keys whose object schemas were rewritten to JSON strings for the
661
733
  // wire, to be decoded back on the response. Empty unless
@@ -844,6 +916,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
844
916
  const requestHeaders = {
845
917
  ...this.requestHeaders,
846
918
  ...(usageAttributionHeaders ?? {}),
919
+ ...(perRequestHeaders ?? {}),
847
920
  };
848
921
  const createStream = () =>
849
922
  this.client.chat.completions.create(params, {
@@ -901,6 +974,17 @@ export class OpenAIChatCompletionsProvider implements Provider {
901
974
  );
902
975
  stripAssistantReasoningFields(params);
903
976
  stream = await createStream();
977
+ } else if (isChatTemplateRejection(error, params)) {
978
+ log.warn(
979
+ {
980
+ provider: this.name,
981
+ model: modelOverride ?? this.model,
982
+ error: error instanceof Error ? error.message : String(error),
983
+ },
984
+ "Upstream chat template rejected structured message content; retrying with flattened plain-text content",
985
+ );
986
+ flattenContentPartsToStrings(params);
987
+ stream = await createStream();
904
988
  } else {
905
989
  throw error;
906
990
  }
@@ -0,0 +1,150 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ EMPTY_ASSISTANT_TURN_PLACEHOLDER,
5
+ OpenAIChatCompletionsProvider,
6
+ } from "../openai/chat-completions-provider.js";
7
+ import {
8
+ buildOpenCodeRequestHeaders,
9
+ OPENCODE_GO_BASE_URL,
10
+ OPENCODE_REQUEST_HEADER,
11
+ OPENCODE_SESSION_HEADER,
12
+ OPENCODE_ZEN_BASE_URL,
13
+ OpenCodeProvider,
14
+ resolveOpenCodeBaseURL,
15
+ } from "./client.js";
16
+
17
+ describe("resolveOpenCodeBaseURL", () => {
18
+ test("defaults to OpenCode Zen", () => {
19
+ expect(resolveOpenCodeBaseURL()).toBe(OPENCODE_ZEN_BASE_URL);
20
+ expect(resolveOpenCodeBaseURL("")).toBe(OPENCODE_ZEN_BASE_URL);
21
+ expect(resolveOpenCodeBaseURL(" ")).toBe(OPENCODE_ZEN_BASE_URL);
22
+ });
23
+
24
+ test("keeps a configured origin, including OpenCode Go", () => {
25
+ expect(resolveOpenCodeBaseURL(OPENCODE_GO_BASE_URL)).toBe(
26
+ OPENCODE_GO_BASE_URL,
27
+ );
28
+ expect(resolveOpenCodeBaseURL(" https://example.com/v1 ")).toBe(
29
+ "https://example.com/v1",
30
+ );
31
+ });
32
+ });
33
+
34
+ describe("buildOpenCodeRequestHeaders", () => {
35
+ test("omits headers when ids are missing", () => {
36
+ expect(buildOpenCodeRequestHeaders({})).toEqual({});
37
+ expect(buildOpenCodeRequestHeaders({ conversationId: " " })).toEqual({});
38
+ });
39
+
40
+ test("sets session and request headers without session_id", () => {
41
+ const headers = buildOpenCodeRequestHeaders({
42
+ conversationId: "conv-xyz",
43
+ requestId: "req-123",
44
+ });
45
+ expect(headers[OPENCODE_SESSION_HEADER]).toBe("conv-xyz");
46
+ expect(headers[OPENCODE_REQUEST_HEADER]).toBe("req-123");
47
+ expect(headers).not.toHaveProperty("session_id");
48
+ });
49
+
50
+ test("sets only the ids that are present", () => {
51
+ expect(
52
+ buildOpenCodeRequestHeaders({ conversationId: "conv-xyz" }),
53
+ ).toEqual({ [OPENCODE_SESSION_HEADER]: "conv-xyz" });
54
+ expect(buildOpenCodeRequestHeaders({ requestId: "req-123" })).toEqual({
55
+ [OPENCODE_REQUEST_HEADER]: "req-123",
56
+ });
57
+ });
58
+ });
59
+
60
+ describe("OpenCodeProvider", () => {
61
+ test("uses OpenAI chat-completions with reasoning-compatible knobs", () => {
62
+ const provider = new OpenCodeProvider("sk-test", "mimo-v2.5-free");
63
+ expect(provider).toBeInstanceOf(OpenAIChatCompletionsProvider);
64
+ expect(provider.name).toBe("opencode");
65
+ expect(
66
+ (provider as unknown as { assistantReasoningField?: string })
67
+ .assistantReasoningField,
68
+ ).toBe("reasoning_content");
69
+ expect(
70
+ (provider as unknown as { omitToolChoiceWhenReasoning: boolean })
71
+ .omitToolChoiceWhenReasoning,
72
+ ).toBe(true);
73
+ });
74
+
75
+ test("merges per-request OpenCode headers onto the transport", async () => {
76
+ const provider = new OpenCodeProvider("sk-test", "mimo-v2.5-free");
77
+ const seen: Array<{ params: unknown; options: unknown }> = [];
78
+ (provider as unknown as { client: unknown }).client = {
79
+ chat: {
80
+ completions: {
81
+ create: async (params: unknown, options: unknown) => {
82
+ seen.push({ params, options });
83
+ return {
84
+ async *[Symbol.asyncIterator]() {
85
+ yield {
86
+ choices: [
87
+ { delta: { content: "ok" }, finish_reason: "stop" },
88
+ ],
89
+ usage: { prompt_tokens: 2, completion_tokens: 1 },
90
+ };
91
+ },
92
+ };
93
+ },
94
+ },
95
+ },
96
+ };
97
+
98
+ await provider.sendMessage(
99
+ [{ role: "user", content: [{ type: "text", text: "question" }] }],
100
+ {
101
+ config: {
102
+ requestHeaders: {
103
+ [OPENCODE_SESSION_HEADER]: "conv-xyz",
104
+ [OPENCODE_REQUEST_HEADER]: "req-123",
105
+ },
106
+ },
107
+ },
108
+ );
109
+
110
+ const options = seen[0]!.options as { headers?: Record<string, string> };
111
+ expect(options.headers?.[OPENCODE_SESSION_HEADER]).toBe("conv-xyz");
112
+ expect(options.headers?.[OPENCODE_REQUEST_HEADER]).toBe("req-123");
113
+ expect(options.headers).not.toHaveProperty("session_id");
114
+ });
115
+
116
+ test("backfills placeholder content after an aborted empty assistant turn", async () => {
117
+ const provider = new OpenCodeProvider("sk-test", "mimo-v2.5-free");
118
+ const requests: unknown[] = [];
119
+ (provider as unknown as { client: unknown }).client = {
120
+ chat: {
121
+ completions: {
122
+ create: async (params: unknown) => {
123
+ requests.push(params);
124
+ return {
125
+ async *[Symbol.asyncIterator]() {
126
+ yield {
127
+ choices: [
128
+ { delta: { content: "ok" }, finish_reason: "stop" },
129
+ ],
130
+ usage: { prompt_tokens: 2, completion_tokens: 1 },
131
+ };
132
+ },
133
+ };
134
+ },
135
+ },
136
+ },
137
+ };
138
+
139
+ await provider.sendMessage([
140
+ { role: "user", content: [{ type: "text", text: "question" }] },
141
+ { role: "assistant", content: [] },
142
+ ]);
143
+
144
+ const params = requests[0] as {
145
+ messages: Array<{ role: string; content: string | null }>;
146
+ };
147
+ const assistantMsg = params.messages.find((m) => m.role === "assistant")!;
148
+ expect(assistantMsg.content).toBe(EMPTY_ASSISTANT_TURN_PLACEHOLDER);
149
+ });
150
+ });
@@ -0,0 +1,61 @@
1
+ import { OpenAIChatCompletionsProvider } from "../openai/chat-completions-provider.js";
2
+
3
+ export const OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1";
4
+ export const OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1";
5
+ export const OPENCODE_SESSION_HEADER = "x-opencode-session";
6
+ export const OPENCODE_REQUEST_HEADER = "x-opencode-request";
7
+
8
+ export interface OpenCodeProviderOptions {
9
+ baseURL?: string;
10
+ streamTimeoutMs?: number;
11
+ }
12
+
13
+ /**
14
+ * Resolve the OpenCode chat-completions origin. A stored connection URL
15
+ * wins; otherwise the request goes to OpenCode Zen.
16
+ */
17
+ export function resolveOpenCodeBaseURL(configuredBaseURL?: string): string {
18
+ const trimmed = configuredBaseURL?.trim();
19
+ if (trimmed) {
20
+ return trimmed;
21
+ }
22
+ return OPENCODE_ZEN_BASE_URL;
23
+ }
24
+
25
+ /**
26
+ * OpenCode-owned request headers for support lookup. Sends session and
27
+ * request ids only when they exist. Never sets `session_id` (zen/go
28
+ * returns 500 when that header is present).
29
+ */
30
+ export function buildOpenCodeRequestHeaders(opts: {
31
+ conversationId?: string;
32
+ requestId?: string;
33
+ }): Record<string, string> {
34
+ const headers: Record<string, string> = {};
35
+ const session = opts.conversationId?.trim();
36
+ if (session) {
37
+ headers[OPENCODE_SESSION_HEADER] = session;
38
+ }
39
+ const requestId = opts.requestId?.trim();
40
+ if (requestId) {
41
+ headers[OPENCODE_REQUEST_HEADER] = requestId;
42
+ }
43
+ return headers;
44
+ }
45
+
46
+ export class OpenCodeProvider extends OpenAIChatCompletionsProvider {
47
+ constructor(
48
+ apiKey: string,
49
+ model: string,
50
+ options: OpenCodeProviderOptions = {},
51
+ ) {
52
+ super(apiKey, model, {
53
+ baseURL: resolveOpenCodeBaseURL(options.baseURL),
54
+ providerName: "opencode",
55
+ providerLabel: "OpenCode",
56
+ streamTimeoutMs: options.streamTimeoutMs,
57
+ assistantReasoningField: "reasoning_content",
58
+ omitToolChoiceWhenReasoning: true,
59
+ });
60
+ }
61
+ }
@@ -0,0 +1,20 @@
1
+ import { getDb } from "../persistence/db-connection.js";
2
+ import { VALID_CONNECTION_PROVIDERS } from "./inference/auth.js";
3
+ import { getConnection } from "./inference/connections.js";
4
+
5
+ /**
6
+ * Selection-time predicate for `ResolveCallSiteOpts.isResolvableProvider`:
7
+ * a provider value dispatches when it is a known vendor/identity or names a
8
+ * connection entry row. Permissive on DB unavailability so a transient blip
9
+ * never heals away a valid entry profile; dispatch soft-fails on its own.
10
+ */
11
+ export function dispatchProviderResolvable(provider: string): boolean {
12
+ if (VALID_CONNECTION_PROVIDERS.includes(provider)) {
13
+ return true;
14
+ }
15
+ try {
16
+ return getConnection(getDb(), provider) != null;
17
+ } catch {
18
+ return true;
19
+ }
20
+ }