@vellumai/assistant 0.11.6 → 0.11.7-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) 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/ces-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  21. package/node_modules/@vellumai/ces-client/src/http-credentials.ts +161 -0
  22. package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
  23. package/node_modules/@vellumai/environments/package.json +2 -1
  24. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  25. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  27. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  28. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  29. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  30. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  32. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  33. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  34. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  35. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  36. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  37. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  38. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  39. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  40. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  41. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  42. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  43. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  44. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  45. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  46. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  47. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  48. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  49. package/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  50. package/openapi.yaml +419 -55
  51. package/package.json +4 -1
  52. package/scripts/sync-llm-catalog.ts +6 -0
  53. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  54. package/src/__tests__/approval-card-edit.test.ts +153 -0
  55. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  56. package/src/__tests__/available-skills.test.ts +54 -0
  57. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  58. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  59. package/src/__tests__/channel-policy.test.ts +1 -3
  60. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  61. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  62. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  63. package/src/__tests__/config-schema.test.ts +2 -3
  64. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  65. package/src/__tests__/conversation-attachments.test.ts +0 -1
  66. package/src/__tests__/conversation-error.test.ts +44 -0
  67. package/src/__tests__/conversation-evictor.test.ts +19 -0
  68. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  69. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  70. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  71. package/src/__tests__/conversation-slash.test.ts +9 -0
  72. package/src/__tests__/conversation-title-service.test.ts +97 -0
  73. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  74. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  75. package/src/__tests__/credential-broker.test.ts +43 -420
  76. package/src/__tests__/credential-record-write-through.test.ts +78 -0
  77. package/src/__tests__/credential-routes.test.ts +17 -1
  78. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  79. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  80. package/src/__tests__/delete-propagation.test.ts +92 -2
  81. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  82. package/src/__tests__/discord-channel-config.test.ts +237 -0
  83. package/src/__tests__/edit-propagation.test.ts +42 -2
  84. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  85. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  86. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  87. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  88. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  89. package/src/__tests__/image-conversion.test.ts +38 -4
  90. package/src/__tests__/install-skill-routing.test.ts +25 -0
  91. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  92. package/src/__tests__/llm-resolver.test.ts +74 -0
  93. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  94. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  95. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  96. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  97. package/src/__tests__/mcp-cli.test.ts +23 -1
  98. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  99. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  100. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  101. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  102. package/src/__tests__/oauth-connect-orchestrator.test.ts +95 -0
  103. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  104. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  105. package/src/__tests__/platform.test.ts +19 -1
  106. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  107. package/src/__tests__/provider-catalog-visibility.test.ts +17 -0
  108. package/src/__tests__/provider-platform-proxy-integration.test.ts +8 -1
  109. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  110. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  111. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  112. package/src/__tests__/schedule-retry.test.ts +183 -4
  113. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  115. package/src/__tests__/skill-load-tool.test.ts +34 -0
  116. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  117. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  118. package/src/__tests__/skills.test.ts +25 -0
  119. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  120. package/src/__tests__/terminal-tools.test.ts +2 -0
  121. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  122. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  123. package/src/__tests__/tool-audit.test.ts +111 -40
  124. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  125. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  126. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  127. package/src/__tests__/voice-config-update.test.ts +43 -0
  128. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  129. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  130. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  131. package/src/agent/loop.ts +31 -2
  132. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  133. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  134. package/src/avatar/avatar-manifest.ts +48 -100
  135. package/src/avatar/avatar-store.ts +7 -7
  136. package/src/avatar/ensure-raster.ts +146 -0
  137. package/src/avatar/resvg-lazy.test.ts +0 -1
  138. package/src/avatar/traits-png-sync.ts +9 -10
  139. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  140. package/src/backup/__tests__/paths.test.ts +6 -2
  141. package/src/backup/paths.ts +6 -4
  142. package/src/bundler/app-compiler.ts +2 -2
  143. package/src/bundler/compiler-tools.test.ts +36 -0
  144. package/src/bundler/compiler-tools.ts +54 -18
  145. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  146. package/src/calls/__tests__/voice-session-bridge.test.ts +57 -0
  147. package/src/calls/media-stream-stt-session.ts +2 -1
  148. package/src/calls/telephony-synthesis-language.ts +13 -5
  149. package/src/calls/voice-session-bridge.ts +19 -4
  150. package/src/channels/config.ts +23 -12
  151. package/src/cli/AGENTS.md +3 -1
  152. package/src/cli/commands/backup.help.ts +10 -13
  153. package/src/cli/commands/contacts.help.ts +16 -4
  154. package/src/cli/commands/contacts.ts +5 -1
  155. package/src/cli/commands/credentials.help.ts +1 -1
  156. package/src/cli/commands/inference-providers.ts +18 -53
  157. package/src/cli/commands/mcp.help.ts +6 -5
  158. package/src/cli/commands/mcp.ts +1 -1
  159. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  160. package/src/cli/commands/stt.help.ts +2 -2
  161. package/src/config/__tests__/backup-schema.test.ts +1 -1
  162. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  163. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  164. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  165. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  166. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  167. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  168. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  169. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  170. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  171. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  172. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  173. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  174. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  175. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  176. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  177. package/src/config/default-profile-catalog.ts +195 -10
  178. package/src/config/default-profile-names.ts +41 -0
  179. package/src/config/feature-flag-registry.json +34 -2
  180. package/src/config/inference-profile-validation.ts +11 -2
  181. package/src/config/llm-resolver.ts +25 -0
  182. package/src/config/loader.ts +3 -0
  183. package/src/config/managed-speech-defaults.ts +160 -10
  184. package/src/config/schema.ts +2 -0
  185. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  186. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  187. package/src/config/schemas/backup.ts +1 -1
  188. package/src/config/schemas/channels.ts +32 -0
  189. package/src/config/schemas/live-voice.ts +13 -4
  190. package/src/config/schemas/llm.ts +263 -15
  191. package/src/config/schemas/mcp.ts +10 -2
  192. package/src/config/schemas/memory-retrospective.ts +1 -19
  193. package/src/config/schemas/services.ts +24 -0
  194. package/src/config/schemas/stt.ts +128 -5
  195. package/src/config/seed-inference-profiles.ts +32 -3
  196. package/src/config/skill-state.ts +5 -0
  197. package/src/config/skills.ts +21 -0
  198. package/src/contacts/notify-contacts-changed.ts +5 -2
  199. package/src/context/compactor.ts +74 -1
  200. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  201. package/src/daemon/config-watcher.ts +2 -1
  202. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  203. package/src/daemon/conversation-agent-loop.ts +86 -10
  204. package/src/daemon/conversation-error.ts +25 -0
  205. package/src/daemon/conversation-evictor.ts +2 -4
  206. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  207. package/src/daemon/conversation-skill-tools.ts +5 -1
  208. package/src/daemon/conversation-slash.ts +2 -2
  209. package/src/daemon/conversation-store.ts +8 -1
  210. package/src/daemon/conversation-tool-setup.ts +37 -0
  211. package/src/daemon/conversation.ts +0 -4
  212. package/src/daemon/handlers/channel-config-result.ts +30 -0
  213. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  214. package/src/daemon/handlers/config-ingress.ts +9 -0
  215. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  216. package/src/daemon/handlers/config-telegram.ts +2 -5
  217. package/src/daemon/handlers/conversations.ts +0 -2
  218. package/src/daemon/handlers/skills.ts +25 -7
  219. package/src/daemon/lifecycle.ts +3 -1
  220. package/src/daemon/message-types/messages.ts +0 -2
  221. package/src/daemon/tool-setup-types.ts +5 -3
  222. package/src/daemon/trust-context-types.test.ts +33 -0
  223. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  224. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  225. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  226. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  227. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  228. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +64 -0
  229. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  230. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  231. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  232. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  233. package/src/live-voice/live-voice-session-manager.ts +39 -2
  234. package/src/live-voice/live-voice-session.ts +224 -9
  235. package/src/live-voice/protocol.ts +187 -1
  236. package/src/messaging/providers/discord/api.ts +2 -1
  237. package/src/messaging/providers/discord/withdraw.ts +62 -0
  238. package/src/messaging/read-provider-metadata.ts +32 -0
  239. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  240. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  241. package/src/monitoring/db-integrity-sample.ts +1 -0
  242. package/src/monitoring/file-descriptors.ts +39 -6
  243. package/src/monitoring/process-memory.ts +37 -5
  244. package/src/monitoring/resource-sampler.ts +25 -8
  245. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  246. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  247. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  248. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  249. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  250. package/src/notifications/activity-failed-dedupe.ts +95 -0
  251. package/src/notifications/adapters/discord.ts +132 -0
  252. package/src/notifications/adapters/shared.ts +16 -0
  253. package/src/notifications/adapters/slack.ts +6 -1
  254. package/src/notifications/adapters/telegram.ts +5 -8
  255. package/src/notifications/approval-card-data.ts +94 -1
  256. package/src/notifications/background-failure-signal.ts +116 -0
  257. package/src/notifications/broadcaster.ts +3 -9
  258. package/src/notifications/copy-composer.ts +41 -8
  259. package/src/notifications/decision-engine.ts +55 -0
  260. package/src/notifications/destination-resolver.ts +45 -5
  261. package/src/notifications/emit-signal.ts +19 -5
  262. package/src/notifications/guardian-question-mode.ts +57 -0
  263. package/src/notifications/types.ts +2 -8
  264. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  265. package/src/oauth/connect-orchestrator.ts +16 -0
  266. package/src/oauth/seed-providers.ts +262 -0
  267. package/src/permissions/checker.test.ts +21 -0
  268. package/src/permissions/checker.ts +6 -0
  269. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  270. package/src/permissions/gateway-threshold-reader.ts +144 -10
  271. package/src/permissions/prompter.ts +0 -6
  272. package/src/permissions/types.ts +5 -4
  273. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  274. package/src/persistence/bulk-write-gate.ts +9 -10
  275. package/src/persistence/conversation-crud.ts +88 -134
  276. package/src/persistence/conversation-title-service.ts +43 -29
  277. package/src/persistence/embeddings/embedding-local.ts +14 -85
  278. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  279. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  280. package/src/persistence/lifecycle-events-store.ts +75 -5
  281. package/src/platform/platform-patch-queue.test.ts +368 -0
  282. package/src/platform/platform-patch-queue.ts +190 -0
  283. package/src/platform/sync-avatar.test.ts +527 -0
  284. package/src/platform/sync-avatar.ts +209 -0
  285. package/src/platform/sync-identity.test.ts +75 -0
  286. package/src/platform/sync-identity.ts +17 -110
  287. package/src/plugin-api/model-profiles.test.ts +2 -2
  288. package/src/plugin-api/model-profiles.ts +7 -7
  289. package/src/plugin-api/transcription-session.ts +1 -1
  290. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  291. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  292. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  293. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  294. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  295. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  296. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  297. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  298. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  299. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  300. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  301. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  302. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  303. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  304. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  305. package/src/plugins/defaults/memory/worker.ts +7 -0
  306. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  307. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  308. package/src/providers/__tests__/vellum-connection-routing.test.ts +21 -3
  309. package/src/providers/connection-resolution.ts +2 -17
  310. package/src/providers/fallback-breaker.ts +389 -0
  311. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  312. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  313. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  314. package/src/providers/inference/adapter-factory.ts +246 -3
  315. package/src/providers/inference/auth.ts +15 -14
  316. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  317. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  318. package/src/providers/inference/resolve-auth.ts +1 -1
  319. package/src/providers/media-resolve.ts +2 -3
  320. package/src/providers/model-catalog.ts +100 -0
  321. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  322. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  323. package/src/providers/openai/api-error-normalization.ts +13 -0
  324. package/src/providers/openai/chat-completions-provider.ts +84 -0
  325. package/src/providers/opencode/client.test.ts +150 -0
  326. package/src/providers/opencode/client.ts +61 -0
  327. package/src/providers/platform-proxy/constants.ts +5 -0
  328. package/src/providers/provider-resolvability.ts +20 -0
  329. package/src/providers/provider-secret-catalog.ts +3 -2
  330. package/src/providers/retry.ts +956 -68
  331. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  332. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  333. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  334. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  335. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  336. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  337. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  338. package/src/providers/speech-to-text/resolve.ts +132 -24
  339. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  340. package/src/providers/types.ts +39 -0
  341. package/src/providers/usage-tracking.ts +12 -1
  342. package/src/providers/vellum/client.ts +29 -0
  343. package/src/providers/vellum-model-routing.test.ts +2 -0
  344. package/src/providers/vellum-model-routing.ts +3 -3
  345. package/src/runtime/AGENTS.md +1 -2
  346. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  347. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  348. package/src/runtime/agent-wake.ts +44 -5
  349. package/src/runtime/background-job-runner.ts +68 -50
  350. package/src/runtime/channel-readiness-service.ts +0 -39
  351. package/src/runtime/http-server.ts +12 -4
  352. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  353. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  354. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  355. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  356. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  357. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  358. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  359. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  360. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  361. package/src/runtime/routes/approval-routes.ts +0 -12
  362. package/src/runtime/routes/avatar-routes.ts +17 -30
  363. package/src/runtime/routes/backup-routes.ts +2 -2
  364. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  365. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  366. package/src/runtime/routes/contact-routes.ts +20 -15
  367. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  368. package/src/runtime/routes/conversation-routes.ts +2 -2
  369. package/src/runtime/routes/credential-routes.ts +2 -1
  370. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  371. package/src/runtime/routes/inbound-message-handler.ts +47 -48
  372. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  373. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  374. package/src/runtime/routes/inbound-stages/edit-intercept.ts +20 -2
  375. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  376. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  377. package/src/runtime/routes/index.ts +2 -0
  378. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  379. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  380. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  381. package/src/runtime/routes/inference-send-routes.ts +2 -2
  382. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  383. package/src/runtime/routes/integrations/discord.ts +119 -0
  384. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  385. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  386. package/src/runtime/routes/migration-routes.ts +3 -1
  387. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  388. package/src/runtime/routes/secret-routes.ts +7 -0
  389. package/src/runtime/routes/stt-routes.ts +38 -6
  390. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  391. package/src/runtime/routes/watch-routes.ts +9 -5
  392. package/src/runtime/sync/resource-sync-events.ts +2 -0
  393. package/src/runtime/trust-verdict-consumer.ts +3 -3
  394. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  395. package/src/schedule/retry-policy.ts +8 -8
  396. package/src/schedule/run-script.ts +14 -23
  397. package/src/schedule/schedule-recovery.ts +0 -1
  398. package/src/schedule/scheduler.ts +89 -51
  399. package/src/schedule/worker.ts +57 -0
  400. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  401. package/src/security/ces-rpc-record-backend.ts +123 -0
  402. package/src/security/oauth2.ts +7 -0
  403. package/src/security/openai-device-auth.ts +364 -0
  404. package/src/security/secure-keys.ts +15 -0
  405. package/src/skills/available-skills.ts +10 -1
  406. package/src/skills/catalog-install.ts +19 -1
  407. package/src/skills/inline-command-runner.ts +6 -2
  408. package/src/skills/platform-compatibility.test.ts +49 -0
  409. package/src/skills/platform-compatibility.ts +65 -0
  410. package/src/skills/tool-manifest.ts +20 -0
  411. package/src/stt/__tests__/roles.test.ts +152 -0
  412. package/src/stt/daemon-batch-transcriber.ts +4 -1
  413. package/src/stt/roles.ts +180 -0
  414. package/src/stt/stt-stream-session.ts +3 -2
  415. package/src/stt/types.ts +33 -3
  416. package/src/subagent/manager.ts +12 -0
  417. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  418. package/src/telemetry/live-voice-funnel.ts +12 -2
  419. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  420. package/src/telemetry/tool-audit.ts +83 -30
  421. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  422. package/src/telemetry/turn-outcome.ts +75 -4
  423. package/src/tools/client-os.ts +115 -0
  424. package/src/tools/computer-use/definitions.ts +11 -7
  425. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  426. package/src/tools/credentials/broker-types.ts +0 -71
  427. package/src/tools/credentials/broker.ts +6 -201
  428. package/src/tools/credentials/metadata-store.ts +65 -16
  429. package/src/tools/credentials/store.ts +2 -0
  430. package/src/tools/executor.ts +2 -8
  431. package/src/tools/host-shell.test.ts +75 -0
  432. package/src/tools/host-shell.ts +24 -0
  433. package/src/tools/host-terminal/host-shell.ts +15 -44
  434. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  435. package/src/tools/permission-checker.ts +73 -21
  436. package/src/tools/policy-context.ts +1 -0
  437. package/src/tools/shared/filesystem/image-read.ts +1 -1
  438. package/src/tools/skills/find-similar-skills.ts +10 -2
  439. package/src/tools/skills/load.ts +41 -6
  440. package/src/tools/skills/sandbox-runner.ts +8 -16
  441. package/src/tools/skills/skill-tool-factory.ts +1 -0
  442. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  443. package/src/tools/terminal/safe-env.ts +39 -9
  444. package/src/tools/terminal/shell.ts +12 -31
  445. package/src/tools/tool-approval-handler.ts +64 -10
  446. package/src/tools/tool-types.ts +10 -0
  447. package/src/tools/types.ts +19 -3
  448. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  449. package/src/tools/workflows/manage-workflows.ts +2 -2
  450. package/src/tools/workflows/run-workflow.test.ts +2 -1
  451. package/src/util/__tests__/file-use.test.ts +61 -0
  452. package/src/util/__tests__/process-table.test.ts +66 -0
  453. package/src/util/__tests__/process-tree.test.ts +46 -0
  454. package/src/util/errors.ts +1 -0
  455. package/src/util/file-use.ts +91 -0
  456. package/src/util/host-process.test.ts +17 -0
  457. package/src/util/host-process.ts +75 -0
  458. package/src/util/image-conversion.ts +39 -7
  459. package/src/util/platform.ts +32 -13
  460. package/src/util/process-table.ts +249 -0
  461. package/src/util/process-tree.ts +85 -129
  462. package/src/util/provider-error-patterns.ts +20 -0
  463. package/src/util/spawn.ts +9 -2
  464. package/src/watch/__tests__/watch-retro.test.ts +48 -1
  465. package/src/watch/watch-retro.ts +46 -4
  466. package/src/workspace/git-service.ts +4 -14
  467. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  468. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  469. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  470. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  471. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  472. package/src/workspace/migrations/registry.ts +8 -0
  473. package/src/__tests__/permission-types.test.ts +0 -17
  474. package/src/channels/gateway-discord-admission.ts +0 -32
  475. package/src/persistence/fork-message-copy.ts +0 -239
  476. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  477. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  478. package/src/security/oauth2-device-code.ts +0 -312
@@ -1374,6 +1374,32 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
1374
1374
  // usage never includes cached tokens. `supportsCaching` therefore stays
1375
1375
  // false; the `cacheReadPer1mTokens` rates below only apply if OpenRouter
1376
1376
  // starts reporting cached tokens in usage.
1377
+ {
1378
+ id: "x-ai/grok-4.6",
1379
+ displayName: "Grok 4.6",
1380
+ contextWindowTokens: 500000,
1381
+ // xAI publishes no completion cap; 30K is the tracker-reported
1382
+ // single-response limit used for sibling Grok 4.x catalog rows.
1383
+ maxOutputTokens: 30000,
1384
+ supportsThinking: true,
1385
+ supportsCaching: false,
1386
+ supportsVision: true,
1387
+ supportsToolUse: true,
1388
+ longContextPricingThresholdTokens: 200000,
1389
+ pricing: {
1390
+ inputPer1mTokens: 2,
1391
+ outputPer1mTokens: 6,
1392
+ cacheReadPer1mTokens: 0.5,
1393
+ tiers: [
1394
+ {
1395
+ inputTokenThreshold: 200000,
1396
+ inputPer1mTokens: 4,
1397
+ outputPer1mTokens: 12,
1398
+ cacheReadPer1mTokens: 1,
1399
+ },
1400
+ ],
1401
+ },
1402
+ },
1377
1403
  {
1378
1404
  id: "x-ai/grok-4.5",
1379
1405
  displayName: "Grok 4.5",
@@ -1697,6 +1723,36 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
1697
1723
  pricing: { inputPer1mTokens: 0.2, outputPer1mTokens: 1.1 },
1698
1724
  },
1699
1725
  // Z.ai
1726
+ {
1727
+ id: "z-ai/glm-5.3",
1728
+ displayName: "GLM-5.3",
1729
+ contextWindowTokens: 1048576,
1730
+ maxOutputTokens: 131072,
1731
+ supportsThinking: true,
1732
+ supportsCaching: true,
1733
+ supportsVision: false,
1734
+ supportsToolUse: true,
1735
+ pricing: {
1736
+ inputPer1mTokens: 1.4,
1737
+ outputPer1mTokens: 4.4,
1738
+ cacheReadPer1mTokens: 0.26,
1739
+ },
1740
+ },
1741
+ {
1742
+ id: "z-ai/glm-5.3-flash",
1743
+ displayName: "GLM-5.3 Flash",
1744
+ contextWindowTokens: 1310720,
1745
+ maxOutputTokens: 131072,
1746
+ supportsThinking: true,
1747
+ supportsCaching: true,
1748
+ supportsVision: true,
1749
+ supportsToolUse: true,
1750
+ pricing: {
1751
+ inputPer1mTokens: 0.075,
1752
+ outputPer1mTokens: 0.25,
1753
+ cacheReadPer1mTokens: 0.015,
1754
+ },
1755
+ },
1700
1756
  {
1701
1757
  id: "z-ai/glm-5.2",
1702
1758
  displayName: "GLM-5.2",
@@ -2038,6 +2094,25 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
2038
2094
  models: [],
2039
2095
  defaultModel: "",
2040
2096
  },
2097
+ {
2098
+ id: "opencode",
2099
+ displayName: "OpenCode",
2100
+ subtitle:
2101
+ "OpenCode Zen and OpenCode Go. Models come from the endpoint, not a fixed catalog.",
2102
+ setupMode: "api-key",
2103
+ setupHint:
2104
+ "Enter your OpenCode API key. Leave the base URL empty for Zen, or set it to the Go endpoint.",
2105
+ envVar: "OPENCODE_API_KEY",
2106
+ credentialsGuide: {
2107
+ description:
2108
+ "Create an OpenCode API key, then choose Zen or Go as the endpoint.",
2109
+ url: "https://opencode.ai",
2110
+ linkLabel: "OpenCode",
2111
+ },
2112
+ apiKeyPlaceholder: "sk-...",
2113
+ models: [],
2114
+ defaultModel: "",
2115
+ },
2041
2116
  {
2042
2117
  id: "openai-compatible",
2043
2118
  displayName: "OpenAI-compatible",
@@ -2196,6 +2271,31 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
2196
2271
  apiKeyUrl: "https://poolside.ai",
2197
2272
  apiKeyPlaceholder: "Your Poolside API key",
2198
2273
  },
2274
+ {
2275
+ id: "vellum",
2276
+ displayName: "Vellum",
2277
+ subtitle:
2278
+ "Models served on Vellum GPU nodes through the managed connection.",
2279
+ setupMode: "api-key",
2280
+ setupHint:
2281
+ "Uses the assistant API key through the Vellum managed connection. These models cannot use a bring-your-own key.",
2282
+ featureFlag: "settings-developer-nav",
2283
+ models: [
2284
+ {
2285
+ id: "qwen/qwen3-8b",
2286
+ displayName: "Qwen3 8B",
2287
+ contextWindowTokens: 32768,
2288
+ maxOutputTokens: 32768,
2289
+ supportsThinking: false,
2290
+ supportsCaching: false,
2291
+ supportsVision: false,
2292
+ supportsToolUse: true,
2293
+ pricing: { inputPer1mTokens: 0.3, outputPer1mTokens: 0.3 },
2294
+ featureFlag: "settings-developer-nav",
2295
+ },
2296
+ ],
2297
+ defaultModel: "qwen/qwen3-8b",
2298
+ },
2199
2299
  ];
2200
2300
 
2201
2301
  export const PROVIDER_CATALOG: ProviderCatalogEntry[] =
@@ -387,6 +387,28 @@ describe("deriveReason", () => {
387
387
  ).toBe("vision_unsupported");
388
388
  });
389
389
 
390
+ test("chat-template failure prose on a 400 → request_shape_unsupported", () => {
391
+ // Together's server-side renderer error for MiniMax M3, verbatim.
392
+ expect(
393
+ deriveReason(
394
+ n({
395
+ message:
396
+ "Failed to apply chat template: invalid operation: object is not callable (in chat:22)",
397
+ }),
398
+ 400,
399
+ ),
400
+ ).toBe("request_shape_unsupported");
401
+ });
402
+
403
+ test("chat-template prose on a 5xx stays server-side, not request_shape_unsupported", () => {
404
+ expect(
405
+ deriveReason(
406
+ n({ message: "Failed to apply chat template: renderer crashed" }),
407
+ 500,
408
+ ),
409
+ ).not.toBe("request_shape_unsupported");
410
+ });
411
+
390
412
  test("402 → insufficient_credits", () => {
391
413
  expect(deriveReason(n(), 402)).toBe("insufficient_credits");
392
414
  });
@@ -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
+ });