@vellumai/assistant 0.11.8 → 0.11.9-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 (326) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/Dockerfile +8 -48
  3. package/docker-entrypoint.sh +5 -1
  4. package/docker-kata-apt-env.sh +3 -0
  5. package/docker-kata-apt-shims.sh +127 -0
  6. package/docker-kata-apt-wrapper.sh +45 -0
  7. package/docker-kata-chroot-exec.sh +35 -0
  8. package/docker-kata-pip.sh +8 -2
  9. package/docs/architecture/memory.md +8 -4
  10. package/docs/guardian-request-flow.md +18 -14
  11. package/docs/trusted-contact-access.md +1 -7
  12. package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
  13. package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  20. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  24. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
  25. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
  26. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
  27. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
  28. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
  29. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
  30. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
  31. package/node_modules/@vellumai/service-contracts/package.json +4 -1
  32. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  33. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  34. package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  35. package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  36. package/openapi.yaml +193 -4
  37. package/package.json +2 -2
  38. package/src/__tests__/access-request-card-view.test.ts +6 -5
  39. package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
  40. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
  41. package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
  42. package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
  43. package/src/__tests__/attachments-store.test.ts +27 -0
  44. package/src/__tests__/attachments.test.ts +43 -0
  45. package/src/__tests__/canned-reply-release.test.ts +48 -5
  46. package/src/__tests__/channel-delivery-store.test.ts +129 -18
  47. package/src/__tests__/channel-reply-delivery.test.ts +553 -93
  48. package/src/__tests__/channel-retry-sweep.test.ts +199 -0
  49. package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
  50. package/src/__tests__/conversation-error.test.ts +15 -0
  51. package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
  52. package/src/__tests__/conversation-pairing.test.ts +141 -1
  53. package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
  54. package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
  55. package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
  56. package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
  57. package/src/__tests__/conversation-sync-tags.test.ts +2 -37
  58. package/src/__tests__/credential-health-service.test.ts +98 -10
  59. package/src/__tests__/delete-propagation.test.ts +469 -0
  60. package/src/__tests__/dm-persistence.test.ts +16 -0
  61. package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
  62. package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
  63. package/src/__tests__/gemini-provider.test.ts +46 -0
  64. package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
  65. package/src/__tests__/guardian-gateway-sim.ts +0 -23
  66. package/src/__tests__/guardian-question-mode.test.ts +108 -0
  67. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
  68. package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
  69. package/src/__tests__/host-proxy-interface.test.ts +10 -0
  70. package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
  71. package/src/__tests__/injector-v3-suppression.test.ts +43 -9
  72. package/src/__tests__/list-messages-page-latest.test.ts +127 -0
  73. package/src/__tests__/list-messages-system-card.test.ts +103 -0
  74. package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
  75. package/src/__tests__/notification-decision-fallback.test.ts +199 -77
  76. package/src/__tests__/notification-decision-strategy.test.ts +198 -213
  77. package/src/__tests__/notification-discord-adapter.test.ts +25 -0
  78. package/src/__tests__/notification-slack-adapter.test.ts +133 -0
  79. package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
  80. package/src/__tests__/openai-provider.test.ts +5 -5
  81. package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
  82. package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
  83. package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
  84. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
  85. package/src/__tests__/pricing.test.ts +11 -0
  86. package/src/__tests__/process-message-display-content.test.ts +20 -9
  87. package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
  88. package/src/__tests__/provider-meta-persistence.test.ts +67 -0
  89. package/src/__tests__/reaction-persistence.test.ts +22 -198
  90. package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
  91. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
  92. package/src/__tests__/skill-load-tool.test.ts +27 -0
  93. package/src/__tests__/skills.test.ts +34 -1
  94. package/src/__tests__/strip-memory-injections.test.ts +3 -4
  95. package/src/__tests__/terminal-tools.test.ts +9 -0
  96. package/src/__tests__/thread-backfill.test.ts +5 -3
  97. package/src/__tests__/unified-turn-context-location.test.ts +76 -0
  98. package/src/__tests__/voice-session-bridge.test.ts +18 -0
  99. package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
  100. package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
  101. package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
  102. package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
  103. package/src/agent/attachments.ts +13 -2
  104. package/src/agent/loop.ts +3 -19
  105. package/src/api/README.md +9 -5
  106. package/src/api/index.ts +9 -8
  107. package/src/api/package.json +1 -0
  108. package/src/api/responses/conversation-message.ts +8 -0
  109. package/src/api/responses/home.ts +7 -18
  110. package/src/api/surfaces.ts +114 -7
  111. package/src/approvals/AGENTS.md +1 -1
  112. package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
  113. package/src/channels/__tests__/types.test.ts +22 -1
  114. package/src/channels/gateway-guardian-requests.ts +0 -22
  115. package/src/channels/types.ts +8 -6
  116. package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
  117. package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
  118. package/src/cli/commands/channels/index.help.ts +19 -6
  119. package/src/cli/commands/channels/index.ts +6 -2
  120. package/src/cli/commands/db/__tests__/status.test.ts +22 -0
  121. package/src/cli/commands/db/index.help.ts +1 -1
  122. package/src/cli/commands/db/status.ts +172 -1
  123. package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
  124. package/src/cli/commands/platform/connect.ts +14 -5
  125. package/src/cli/commands/plugins.help.ts +6 -5
  126. package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
  127. package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
  128. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
  129. package/src/cli/lib/bundled-marketplace.json +14 -0
  130. package/src/cli/lib/install-from-github.ts +9 -5
  131. package/src/cli/lib/install-from-platform.ts +12 -1
  132. package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
  133. package/src/config/assistant-initiated-threads-gate.ts +50 -0
  134. package/src/config/bundled-skills/acp/SKILL.md +10 -3
  135. package/src/config/bundled-skills/schedule/SKILL.md +25 -11
  136. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
  137. package/src/config/call-site-defaults.ts +4 -1
  138. package/src/config/feature-flag-registry.json +21 -4
  139. package/src/config/schemas/memory-v3.ts +4 -3
  140. package/src/context/strip-injections.ts +17 -56
  141. package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
  142. package/src/conversations/message-consolidation.ts +4 -3
  143. package/src/credential-health/credential-health-service.ts +130 -0
  144. package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
  145. package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
  146. package/src/daemon/conversation-error.ts +2 -0
  147. package/src/daemon/conversation-messaging.ts +195 -47
  148. package/src/daemon/conversation-process.ts +20 -5
  149. package/src/daemon/conversation-runtime-assembly.ts +48 -39
  150. package/src/daemon/conversation-store.ts +159 -11
  151. package/src/daemon/conversation-tool-setup.ts +9 -2
  152. package/src/daemon/conversation.ts +290 -19
  153. package/src/daemon/dictation-text-processing.ts +2 -7
  154. package/src/daemon/handlers/config-channels.ts +33 -3
  155. package/src/daemon/handlers/config-model.test.ts +1 -0
  156. package/src/daemon/handlers/shared.ts +9 -0
  157. package/src/daemon/port-oversized-content.test.ts +117 -0
  158. package/src/daemon/port-oversized-content.ts +110 -0
  159. package/src/daemon/process-message.ts +87 -16
  160. package/src/daemon/reaction-record.test.ts +23 -11
  161. package/src/daemon/reaction-record.ts +6 -9
  162. package/src/documents/document-store.ts +1 -5
  163. package/src/home/conversation-starter-validation.ts +1 -5
  164. package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
  165. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
  166. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
  167. package/src/live-voice/live-voice-photo.ts +544 -61
  168. package/src/live-voice/live-voice-session.ts +6 -2
  169. package/src/live-voice/protocol.ts +20 -1
  170. package/src/messaging/provider-message-metadata.ts +40 -1
  171. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
  172. package/src/messaging/providers/channel-transport.ts +28 -0
  173. package/src/messaging/providers/discord/send.ts +3 -2
  174. package/src/messaging/providers/slack/api.ts +11 -1
  175. package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
  176. package/src/messaging/providers/slack/message-metadata.ts +141 -1
  177. package/src/messaging/providers/slack/send.test.ts +58 -0
  178. package/src/messaging/providers/slack/send.ts +4 -4
  179. package/src/messaging/providers/slack/transport.ts +28 -2
  180. package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
  181. package/src/messaging/providers/telegram-bot/send.ts +93 -0
  182. package/src/messaging/providers/telegram-bot/transport.ts +71 -0
  183. package/src/messaging/reaction-envelopes.test.ts +73 -0
  184. package/src/messaging/reaction-envelopes.ts +33 -5
  185. package/src/messaging/read-provider-metadata.ts +4 -3
  186. package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
  187. package/src/monitoring/recovery/db.ts +35 -0
  188. package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
  189. package/src/monitoring/recovery/run-recovery.ts +6 -2
  190. package/src/monitoring/recovery/stale-processing.ts +3 -20
  191. package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
  192. package/src/notifications/AGENTS.md +2 -2
  193. package/src/notifications/README.md +2 -2
  194. package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
  195. package/src/notifications/__tests__/broadcaster.test.ts +35 -4
  196. package/src/notifications/__tests__/notification-utils.test.ts +31 -0
  197. package/src/notifications/access-request-copy.ts +126 -114
  198. package/src/notifications/adapters/discord.ts +9 -5
  199. package/src/notifications/adapters/shared.ts +17 -4
  200. package/src/notifications/adapters/slack.ts +14 -4
  201. package/src/notifications/adapters/telegram.ts +8 -4
  202. package/src/notifications/approval-card-data.ts +5 -2
  203. package/src/notifications/assistant-reply-producer.ts +7 -7
  204. package/src/notifications/broadcaster.ts +57 -25
  205. package/src/notifications/conversation-pairing.ts +68 -2
  206. package/src/notifications/copy-composer.ts +13 -32
  207. package/src/notifications/decision-engine.ts +56 -237
  208. package/src/notifications/guardian-delivery-recorder.ts +5 -9
  209. package/src/notifications/guardian-feed-projection.ts +0 -15
  210. package/src/notifications/guardian-question-mode.ts +147 -6
  211. package/src/notifications/notification-utils.ts +117 -1
  212. package/src/permissions/confirmation-guardian-request.ts +2 -2
  213. package/src/permissions/prompter.ts +1 -1
  214. package/src/persistence/conversation-crud.ts +93 -14
  215. package/src/persistence/conversation-queries.ts +144 -17
  216. package/src/persistence/conversation-types.ts +39 -5
  217. package/src/persistence/delivery-crud.ts +224 -28
  218. package/src/persistence/delivery-status.ts +21 -2
  219. package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
  220. package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
  221. package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
  222. package/src/persistence/schema/conversations.ts +71 -2
  223. package/src/persistence/schema-contract.test.ts +78 -0
  224. package/src/persistence/schema-contract.ts +96 -0
  225. package/src/persistence/steps.ts +4 -0
  226. package/src/platform/client.ts +55 -0
  227. package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
  228. package/src/plugins/defaults/injector-order.ts +2 -1
  229. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
  230. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
  231. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
  232. package/src/plugins/defaults/memory/injectors.ts +1 -1
  233. package/src/plugins/defaults/memory/memory-marker.ts +17 -7
  234. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
  235. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
  236. package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
  237. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
  238. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
  239. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
  240. package/src/plugins/defaults/memory/v3/injector.ts +14 -15
  241. package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
  242. package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
  243. package/src/plugins/defaults/memory/v3/types.ts +15 -4
  244. package/src/plugins/defaults/turn-context/injectors.ts +3 -0
  245. package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
  246. package/src/plugins/mcp-servers.ts +14 -2
  247. package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
  248. package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
  249. package/src/providers/anthropic/client.ts +21 -24
  250. package/src/providers/call-site-routing.ts +5 -4
  251. package/src/providers/connection-resolution.ts +29 -1
  252. package/src/providers/content-block-size.test.ts +82 -0
  253. package/src/providers/content-block-size.ts +99 -0
  254. package/src/providers/file-block-text.test.ts +64 -0
  255. package/src/providers/file-block-text.ts +28 -0
  256. package/src/providers/gemini/client.ts +15 -9
  257. package/src/providers/inference/auth.ts +6 -6
  258. package/src/providers/media-resolve.ts +14 -0
  259. package/src/providers/model-catalog.ts +47 -34
  260. package/src/providers/openai/chat-completions-provider.ts +9 -25
  261. package/src/providers/openai/responses-provider.ts +11 -18
  262. package/src/providers/registry.ts +10 -7
  263. package/src/providers/routing-identity.ts +2 -1
  264. package/src/providers/types.ts +1 -4
  265. package/src/providers/vellum-model-routing.ts +3 -2
  266. package/src/runtime/AGENTS.md +40 -15
  267. package/src/runtime/approval-message-composer.ts +1 -6
  268. package/src/runtime/assistant-event-hub.ts +2 -39
  269. package/src/runtime/channel-approval-types.ts +1 -5
  270. package/src/runtime/channel-reply-delivery.ts +182 -115
  271. package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
  272. package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
  273. package/src/runtime/channel-retry-sweep.ts +102 -1
  274. package/src/runtime/finalize-event-delivery.ts +4 -4
  275. package/src/runtime/guardian-action-message-composer.ts +1 -4
  276. package/src/runtime/guardian-reply-router.ts +4 -75
  277. package/src/runtime/http-router.ts +1 -5
  278. package/src/runtime/question-request-guardian-bridge.ts +2 -3
  279. package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
  280. package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
  281. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
  282. package/src/runtime/routes/acp-routes.ts +1 -1
  283. package/src/runtime/routes/canned-reply-release.ts +19 -9
  284. package/src/runtime/routes/channel-route-shared.ts +0 -39
  285. package/src/runtime/routes/channel-verification-routes.ts +4 -1
  286. package/src/runtime/routes/conversation-list-routes.ts +44 -3
  287. package/src/runtime/routes/conversation-management-routes.ts +2 -1
  288. package/src/runtime/routes/conversation-routes.ts +138 -59
  289. package/src/runtime/routes/diagnostics-routes.ts +111 -60
  290. package/src/runtime/routes/guardian-action-routes.ts +10 -14
  291. package/src/runtime/routes/guardian-approval-interception.ts +0 -5
  292. package/src/runtime/routes/inbound-message-handler.ts +170 -48
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
  294. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
  295. package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
  296. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
  297. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
  298. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
  299. package/src/runtime/routes/index.ts +2 -0
  300. package/src/runtime/routes/platform-routes.ts +99 -10
  301. package/src/runtime/routes/sight-frame-routes.ts +136 -0
  302. package/src/runtime/sync/resource-sync-events.ts +14 -37
  303. package/src/runtime/sync/sync-publisher.test.ts +5 -4
  304. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  305. package/src/tools/client-os.ts +11 -1
  306. package/src/tools/host-filesystem/edit.ts +2 -2
  307. package/src/tools/host-filesystem/read.ts +2 -2
  308. package/src/tools/host-filesystem/transfer.ts +2 -2
  309. package/src/tools/host-filesystem/write.ts +2 -2
  310. package/src/tools/host-terminal/host-shell.ts +2 -2
  311. package/src/tools/skills/load.ts +1 -1
  312. package/src/tools/terminal/safe-env.ts +4 -0
  313. package/src/tools/tool-input-schemas.ts +2 -0
  314. package/src/tools/tool-manifest.ts +2 -0
  315. package/src/tools/ui-surface/definitions.ts +4 -3
  316. package/src/tools/watch/watch-retro-report.ts +205 -0
  317. package/src/watch/__tests__/watch-retro.test.ts +268 -40
  318. package/src/watch/watch-retro.ts +190 -77
  319. package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
  320. package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
  321. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
  322. package/src/workspace/migrations/registry.ts +4 -0
  323. package/docker-kata-pip-chroot.sh +0 -22
  324. package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
  325. package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
  326. package/src/api/events/conversation-list-invalidated.ts +0 -38
@@ -321,6 +321,26 @@ describe("routing identities", () => {
321
321
  expect(resolveProviderOpts[0]?.providerOverride).toBe("anthropic");
322
322
  });
323
323
 
324
+ test("a stored vellum GPU profile dispatches through the vellum adapter", async () => {
325
+ registerConnection(
326
+ { name: "vellum", provider: "vellum", auth: { type: "platform" } },
327
+ { name: "vellum", tag: "gpu-stub" },
328
+ );
329
+ setLlmConfig({
330
+ profiles: {
331
+ gpu: { provider: "vellum", model: "qwen/qwen3-8b" },
332
+ },
333
+ });
334
+
335
+ const result = await getConfiguredProvider("mainAgent", {
336
+ overrideProfile: "gpu",
337
+ });
338
+
339
+ expect(result).not.toBeNull();
340
+ expect(resolveProviderCalls[0]?.name).toBe("vellum");
341
+ expect(resolveProviderOpts[0]?.providerOverride).toBe("vellum");
342
+ });
343
+
324
344
  test("a stored chatgpt profile dispatches end-to-end through the real config loader", async () => {
325
345
  registerConnection(
326
346
  {
@@ -356,6 +376,10 @@ describe("routing identities", () => {
356
376
  expect(
357
377
  resolveRoutingIdentity("vellum", "accounts/fireworks/models/glm-5p2"),
358
378
  ).toEqual({ connectionName: "vellum", expectedProvider: "fireworks" });
379
+ expect(resolveRoutingIdentity("vellum", "qwen/qwen3-8b")).toEqual({
380
+ connectionName: "vellum",
381
+ expectedProvider: "vellum",
382
+ });
359
383
  });
360
384
 
361
385
  test("resolveRoutingIdentity throws loudly for an unroutable vellum model", () => {
@@ -423,6 +447,32 @@ describe("routing identities", () => {
423
447
  expect(resolveProviderOpts[0]?.providerOverride).toBe("anthropic");
424
448
  });
425
449
 
450
+ test("vellum identity + GPU model threads vellum as the upstream override", async () => {
451
+ fakeConnections.set("vellum", {
452
+ name: "vellum",
453
+ provider: "vellum",
454
+ auth: { type: "platform" },
455
+ });
456
+ fakeProviders.set("conn:vellum", { name: "vellum", tag: "gpu" });
457
+
458
+ const provider = await tryResolveProviderForConnectionName(
459
+ "ignored-stale-name",
460
+ { llm: {} } as never,
461
+ "vellum",
462
+ "qwen/qwen3-8b",
463
+ );
464
+
465
+ expect(provider).toEqual({
466
+ name: "vellum",
467
+ tag: "gpu",
468
+ routeAttribution: {
469
+ connectionName: "vellum",
470
+ isManagedRoute: true,
471
+ },
472
+ } as never);
473
+ expect(resolveProviderOpts[0]?.providerOverride).toBe("vellum");
474
+ });
475
+
426
476
  test("chatgpt identity resolves the subscription row by name with an openai override", async () => {
427
477
  fakeConnections.set("chatgpt-subscription", {
428
478
  name: "chatgpt-subscription",
@@ -14,7 +14,7 @@ import type { ProvidersConfig } from "../registry.js";
14
14
 
15
15
  const adapterCalls: Array<{
16
16
  connection: ProviderConnection;
17
- opts: { model: string; useNativeWebSearch?: boolean };
17
+ opts: { model: string; useNativeWebSearch?: boolean; provider?: string };
18
18
  }> = [];
19
19
 
20
20
  mock.module("../inference/resolve-auth.js", () => ({
@@ -32,7 +32,7 @@ mock.module("../inference/adapter-factory.js", () => ({
32
32
  createAdapterFromConnection: (
33
33
  connection: ProviderConnection,
34
34
  _resolvedAuth: unknown,
35
- opts: { model: string; useNativeWebSearch?: boolean },
35
+ opts: { model: string; useNativeWebSearch?: boolean; provider?: string },
36
36
  ) => {
37
37
  adapterCalls.push({ connection, opts });
38
38
  return {
@@ -83,6 +83,18 @@ const openRouterConnection: ProviderConnection = {
83
83
  isManaged: false,
84
84
  };
85
85
 
86
+ const vellumConnection: ProviderConnection = {
87
+ name: "vellum",
88
+ provider: "vellum",
89
+ auth: { type: "platform" },
90
+ label: "Vellum",
91
+ baseUrl: null,
92
+ models: null,
93
+ createdAt: 1,
94
+ updatedAt: 1,
95
+ isManaged: true,
96
+ };
97
+
86
98
  describe("resolveProviderFromConnection native web search selection", () => {
87
99
  beforeEach(() => {
88
100
  adapterCalls.length = 0;
@@ -160,6 +172,41 @@ describe("resolveProviderFromConnection connection cache TTL", () => {
160
172
  });
161
173
  });
162
174
 
175
+ describe("resolveProviderFromConnection vellum GPU upstream", () => {
176
+ beforeEach(() => {
177
+ adapterCalls.length = 0;
178
+ clearConnectionProviderCache();
179
+ });
180
+
181
+ test("a GPU model with providerOverride vellum builds an adapter", async () => {
182
+ const adapter = await resolveProviderFromConnection(
183
+ vellumConnection,
184
+ makeConfig(),
185
+ { model: "qwen/qwen3-8b", providerOverride: "vellum" },
186
+ );
187
+ expect(adapter).not.toBeNull();
188
+ expect(adapterCalls).toHaveLength(1);
189
+ expect(adapterCalls[0].opts).toMatchObject({
190
+ model: "qwen/qwen3-8b",
191
+ provider: "vellum",
192
+ });
193
+ });
194
+
195
+ test("providerOverride vellum is a factory id, not an unresolved identity", async () => {
196
+ const adapter = await resolveProviderFromConnection(
197
+ vellumConnection,
198
+ makeConfig(),
199
+ { model: "claude-opus-4-8", providerOverride: "vellum" },
200
+ );
201
+ expect(adapter).not.toBeNull();
202
+ expect(adapterCalls).toHaveLength(1);
203
+ expect(adapterCalls[0].opts).toMatchObject({
204
+ model: "claude-opus-4-8",
205
+ provider: "vellum",
206
+ });
207
+ });
208
+ });
209
+
163
210
  describe("isNativeWebSearchCapableProvider gateway anthropic routing", () => {
164
211
  test("vercel-ai-gateway anthropic/* models are capable", () => {
165
212
  expect(
@@ -10,6 +10,12 @@ import {
10
10
  } from "../../util/provider-error-patterns.js";
11
11
  import { extractRetryAfterMs } from "../../util/retry.js";
12
12
  import { stripOrphanedSurrogatesDeep } from "../../util/unicode.js";
13
+ import {
14
+ clampProviderString,
15
+ isDecodableTextMimeType,
16
+ keepFileAsWorkspaceRef,
17
+ } from "../content-block-size.js";
18
+ import { fileBlockToProviderText } from "../file-block-text.js";
13
19
  import { base64Source, resolveMediaReferences } from "../media-resolve.js";
14
20
  import {
15
21
  couldBePlaceholderSentinelPrefix,
@@ -106,8 +112,7 @@ function readAnthropicErrorType(
106
112
  error: InstanceType<typeof Anthropic.APIError>,
107
113
  ): string | undefined {
108
114
  const body = error.error as
109
- | { type?: string; error?: { type?: string; code?: string } }
110
- | undefined;
115
+ { type?: string; error?: { type?: string; code?: string } } | undefined;
111
116
  return body?.error?.type ?? body?.type;
112
117
  }
113
118
 
@@ -127,8 +132,7 @@ function readAnthropicMessage(
127
132
  error: InstanceType<typeof Anthropic.APIError>,
128
133
  ): string | undefined {
129
134
  const body = error.error as
130
- | { message?: string; error?: { message?: string } }
131
- | undefined;
135
+ { message?: string; error?: { message?: string } } | undefined;
132
136
  const inner = body?.error?.message ?? body?.message;
133
137
  return typeof inner === "string" && inner.length > 0 ? inner : undefined;
134
138
  }
@@ -283,15 +287,6 @@ const ANTHROPIC_SUPPORTED_IMAGE_TYPES = new Set([
283
287
  "image/webp",
284
288
  ]);
285
289
 
286
- function isTextBasedMimeType(mediaType: string): boolean {
287
- return (
288
- mediaType.startsWith("text/") ||
289
- mediaType === "application/json" ||
290
- mediaType === "application/xml" ||
291
- mediaType === "application/javascript"
292
- );
293
- }
294
-
295
290
  /** Anthropic requires tool_use IDs to match ^[a-zA-Z0-9_-]+$ */
296
291
  function sanitizeToolId(id: string): string {
297
292
  if (!id) {
@@ -968,8 +963,7 @@ export class AnthropicProvider implements Provider {
968
963
  const { tools, systemPrompt, config, onEvent, signal } = options ?? {};
969
964
  const cacheTtl: "5m" | "1h" =
970
965
  ((config as Record<string, unknown> | undefined)?.cacheTtl as
971
- | "5m"
972
- | "1h") ?? "1h";
966
+ "5m" | "1h") ?? "1h";
973
967
  // Opt-out for callers (e.g. the memory router) that send a single
974
968
  // user message per call with content that changes every time. The
975
969
  // turn-start cache breakpoint below is only useful when the same
@@ -2148,9 +2142,10 @@ export class AnthropicProvider implements Provider {
2148
2142
  const cacheControl = (
2149
2143
  block as { cache_control?: Anthropic.CacheControlEphemeral }
2150
2144
  ).cache_control;
2145
+ const text = clampProviderString(block.text);
2151
2146
  return cacheControl
2152
- ? { type: "text", text: block.text, cache_control: cacheControl }
2153
- : { type: "text", text: block.text };
2147
+ ? { type: "text", text, cache_control: cacheControl }
2148
+ : { type: "text", text };
2154
2149
  }
2155
2150
  case "thinking":
2156
2151
  if (!block.signature) {
@@ -2183,6 +2178,9 @@ export class AnthropicProvider implements Provider {
2183
2178
  },
2184
2179
  };
2185
2180
  case "file": {
2181
+ if (keepFileAsWorkspaceRef(block.source)) {
2182
+ return { type: "text", text: fileBlockToProviderText(block) };
2183
+ }
2186
2184
  const { media_type, data, filename } = base64Source(block.source);
2187
2185
  if (media_type === "application/pdf") {
2188
2186
  // Only valid base64 document source for Anthropic
@@ -2192,9 +2190,11 @@ export class AnthropicProvider implements Provider {
2192
2190
  ...(filename ? { title: filename } : {}),
2193
2191
  } as unknown as Anthropic.ContentBlockParam;
2194
2192
  }
2195
- if (isTextBasedMimeType(media_type)) {
2193
+ if (isDecodableTextMimeType(media_type)) {
2196
2194
  // Decode base64 to UTF-8 text and send as PlainTextSource
2197
- const decodedText = Buffer.from(data, "base64").toString("utf-8");
2195
+ const decodedText = clampProviderString(
2196
+ Buffer.from(data, "base64").toString("utf-8"),
2197
+ );
2198
2198
  return {
2199
2199
  type: "document",
2200
2200
  source: {
@@ -2205,14 +2205,11 @@ export class AnthropicProvider implements Provider {
2205
2205
  ...(filename ? { title: filename } : {}),
2206
2206
  } as unknown as Anthropic.ContentBlockParam;
2207
2207
  }
2208
- // Binary non-text file: use extracted_text if available, otherwise a placeholder
2208
+ // Binary non-text file: name the workspace file, never dump bytes.
2209
2209
  log.warn(
2210
2210
  `Binary file type not natively supported by Anthropic: ${media_type}; falling back to text`,
2211
2211
  );
2212
- const fallbackText = block.extracted_text?.trim()
2213
- ? block.extracted_text
2214
- : `[File: ${filename ?? "unknown"} (${media_type}) — binary file]`;
2215
- return { type: "text", text: fallbackText };
2212
+ return { type: "text", text: fileBlockToProviderText(block) };
2216
2213
  }
2217
2214
  case "tool_use":
2218
2215
  return {
@@ -358,17 +358,18 @@ export class CallSiteRoutingProvider implements Provider {
358
358
  }
359
359
  // Soft credential failure: the routed connection yielded no usable
360
360
  // adapter and dispatch is landing on the default transport, which may
361
- // be the platform-billed route — keep every such degradation
362
- // observable.
361
+ // be the platform-billed route. Keep every such degradation
362
+ // observable. The reason is "no adapter", not a proven credential miss:
363
+ // adapter construction can also fail for dispatch bugs.
363
364
  log.warn(
364
365
  {
365
366
  callSite,
366
367
  connectionName,
367
368
  provider: resolved.provider,
368
369
  model: resolved.model,
369
- reason: "credential_unavailable",
370
+ reason: "adapter_unavailable",
370
371
  },
371
- "Routed connection yielded no adapter — falling back to the default transport",
372
+ "Routed connection yielded no adapter: falling back to the default transport",
372
373
  );
373
374
  return this.defaultRoute(profileName);
374
375
  }
@@ -387,9 +387,23 @@ export async function tryResolveProviderForConnectionName(
387
387
  });
388
388
  return attachProviderRoute(provider, connection);
389
389
  } catch (err) {
390
+ if (err instanceof ConnectionResolutionError) {
391
+ throw err;
392
+ }
393
+ if (
394
+ err instanceof Error &&
395
+ /unresolved routing identity/.test(err.message)
396
+ ) {
397
+ throw new ConnectionResolutionError(
398
+ connectionName,
399
+ "adapter_unavailable",
400
+ err.message,
401
+ { cause: err, model },
402
+ );
403
+ }
390
404
  log.warn(
391
405
  { err, connectionName },
392
- "provider_connection auth resolution failed transiently — returning null",
406
+ "provider_connection auth resolution failed transiently: returning null",
393
407
  );
394
408
  return null;
395
409
  }
@@ -457,6 +471,20 @@ async function resolveThroughPlatform(
457
471
  { model, providerOverride: upstream },
458
472
  );
459
473
  } catch (err) {
474
+ if (err instanceof ConnectionResolutionError) {
475
+ throw err;
476
+ }
477
+ if (
478
+ err instanceof Error &&
479
+ /unresolved routing identity/.test(err.message)
480
+ ) {
481
+ throw new ConnectionResolutionError(
482
+ VELLUM_MANAGED_CONNECTION_NAME,
483
+ "adapter_unavailable",
484
+ err.message,
485
+ { cause: err, model },
486
+ );
487
+ }
460
488
  log.warn({ err }, "Platform fallback auth resolution failed transiently");
461
489
  return null;
462
490
  }
@@ -0,0 +1,82 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ clampProviderString,
5
+ exceedsProviderStringCap,
6
+ extractedTextForFileBlock,
7
+ isDecodableTextMimeType,
8
+ isVideoMimeType,
9
+ keepFileAsWorkspaceRef,
10
+ MAX_PROVIDER_STRING_BYTES,
11
+ PROVIDER_STRING_OMITTED_NOTE,
12
+ utf8ByteLength,
13
+ } from "./content-block-size.js";
14
+
15
+ describe("content-block-size", () => {
16
+ test("counts UTF-8 bytes, not JS string length", () => {
17
+ expect(utf8ByteLength("é")).toBe(2);
18
+ expect(exceedsProviderStringCap("é".repeat(4), 7)).toBe(true);
19
+ expect(exceedsProviderStringCap("é".repeat(3), 7)).toBe(false);
20
+ });
21
+
22
+ test("default cap is under OpenAI's 10 MiB per-part limit", () => {
23
+ expect(MAX_PROVIDER_STRING_BYTES).toBe(8_000_000);
24
+ expect(MAX_PROVIDER_STRING_BYTES).toBeLessThan(10_485_760);
25
+ });
26
+
27
+ test("treats video MIME types, including parameters, as video", () => {
28
+ expect(isVideoMimeType("video/mp4")).toBe(true);
29
+ expect(isVideoMimeType("VIDEO/WEBM; codecs=vp9")).toBe(true);
30
+ expect(isVideoMimeType("application/pdf")).toBe(false);
31
+ });
32
+
33
+ test("drops extracted_text for video and for over-cap extracts", () => {
34
+ expect(
35
+ extractedTextForFileBlock("video/mp4", "anything at all"),
36
+ ).toBeUndefined();
37
+ expect(extractedTextForFileBlock("application/pdf", "hello")).toBe("hello");
38
+ expect(
39
+ extractedTextForFileBlock("text/plain", "abcdef", 4),
40
+ ).toBeUndefined();
41
+ });
42
+
43
+ test("clampProviderString replaces an over-cap string", () => {
44
+ expect(clampProviderString("ok")).toBe("ok");
45
+ expect(clampProviderString("abcdef", 4)).toBe(PROVIDER_STRING_OMITTED_NOTE);
46
+ });
47
+
48
+ test("keepFileAsWorkspaceRef covers video and over-cap text refs", () => {
49
+ expect(isDecodableTextMimeType("text/plain")).toBe(true);
50
+ expect(isDecodableTextMimeType("application/json")).toBe(true);
51
+ expect(isDecodableTextMimeType("application/pdf")).toBe(false);
52
+
53
+ expect(
54
+ keepFileAsWorkspaceRef({
55
+ type: "workspace_ref",
56
+ media_type: "video/mp4",
57
+ sizeBytes: 12,
58
+ }),
59
+ ).toBe(true);
60
+ expect(
61
+ keepFileAsWorkspaceRef({
62
+ type: "workspace_ref",
63
+ media_type: "text/plain",
64
+ sizeBytes: MAX_PROVIDER_STRING_BYTES + 1,
65
+ }),
66
+ ).toBe(true);
67
+ expect(
68
+ keepFileAsWorkspaceRef({
69
+ type: "workspace_ref",
70
+ media_type: "text/plain",
71
+ sizeBytes: 20,
72
+ }),
73
+ ).toBe(false);
74
+ expect(
75
+ keepFileAsWorkspaceRef({
76
+ type: "workspace_ref",
77
+ media_type: "application/pdf",
78
+ sizeBytes: MAX_PROVIDER_STRING_BYTES + 1,
79
+ }),
80
+ ).toBe(false);
81
+ });
82
+ });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Provider per-string size cap and helpers for content that would be
3
+ * serialized as a single text part.
4
+ *
5
+ * OpenAI rejects a request when any `content[].text` / `input[].content[].text`
6
+ * exceeds 10_485_760 bytes (`string_above_max_length`). Stay under that so a
7
+ * single block cannot brick every later turn. Extra SQLite-only columns and
8
+ * attachment bytes are out of scope: this is the wire-string budget.
9
+ */
10
+
11
+ /** Bytes. Under OpenAI's 10 MiB per-part string limit (10_485_760). */
12
+ export const MAX_PROVIDER_STRING_BYTES = 8_000_000;
13
+
14
+ export function utf8ByteLength(text: string): number {
15
+ return Buffer.byteLength(text, "utf8");
16
+ }
17
+
18
+ export function exceedsProviderStringCap(
19
+ text: string,
20
+ maxBytes: number = MAX_PROVIDER_STRING_BYTES,
21
+ ): boolean {
22
+ return utf8ByteLength(text) > maxBytes;
23
+ }
24
+
25
+ export function isVideoMimeType(mimeType: string): boolean {
26
+ const normalised = mimeType.toLowerCase().trim().split(";")[0]?.trim() ?? "";
27
+ return normalised.startsWith("video/");
28
+ }
29
+
30
+ /**
31
+ * MIME types a serializer would decode into a provider text/document string.
32
+ * Binary types (PDF, images, audio) stay out of this set: they are not the
33
+ * OpenAI `string_above_max_length` failure mode.
34
+ */
35
+ export function isDecodableTextMimeType(mimeType: string): boolean {
36
+ const normalised = mimeType.toLowerCase().trim().split(";")[0]?.trim() ?? "";
37
+ return (
38
+ normalised.startsWith("text/") ||
39
+ normalised === "application/json" ||
40
+ normalised === "application/xml" ||
41
+ normalised === "application/javascript"
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Video and over-cap text files stay on disk. Serializers name the workspace
47
+ * file; they must not load bytes or emit a decoded dump.
48
+ */
49
+ export function keepFileAsWorkspaceRef(source: {
50
+ type: string;
51
+ media_type: string;
52
+ sizeBytes?: number;
53
+ }): boolean {
54
+ if (isVideoMimeType(source.media_type)) {
55
+ return true;
56
+ }
57
+ return (
58
+ source.type === "workspace_ref" &&
59
+ source.sizeBytes !== undefined &&
60
+ source.sizeBytes > MAX_PROVIDER_STRING_BYTES &&
61
+ isDecodableTextMimeType(source.media_type)
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Drop `extracted_text` that must not ride a file block into the provider
67
+ * prompt: video is a workspace file, not a transcript dump, and any extract
68
+ * over the cap belongs in the attachment store rather than the request.
69
+ */
70
+ export function extractedTextForFileBlock(
71
+ mimeType: string,
72
+ extractedText: string | undefined,
73
+ maxBytes: number = MAX_PROVIDER_STRING_BYTES,
74
+ ): string | undefined {
75
+ if (extractedText === undefined || extractedText.length === 0) {
76
+ return undefined;
77
+ }
78
+ if (isVideoMimeType(mimeType)) {
79
+ return undefined;
80
+ }
81
+ if (exceedsProviderStringCap(extractedText, maxBytes)) {
82
+ return undefined;
83
+ }
84
+ return extractedText;
85
+ }
86
+
87
+ /** Fallback copy when a serializer still sees a string over the cap. */
88
+ export const PROVIDER_STRING_OMITTED_NOTE =
89
+ "Content exceeded the provider size limit and was omitted from this prompt.";
90
+
91
+ export function clampProviderString(
92
+ text: string,
93
+ maxBytes: number = MAX_PROVIDER_STRING_BYTES,
94
+ ): string {
95
+ if (!exceedsProviderStringCap(text, maxBytes)) {
96
+ return text;
97
+ }
98
+ return PROVIDER_STRING_OMITTED_NOTE;
99
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { PROVIDER_STRING_OMITTED_NOTE } from "./content-block-size.js";
4
+ import { fileBlockToProviderText } from "./file-block-text.js";
5
+ import type { ContentBlock } from "./types.js";
6
+
7
+ function fileBlock(
8
+ mediaType: string,
9
+ extractedText?: string,
10
+ ): Extract<ContentBlock, { type: "file" }> {
11
+ return {
12
+ type: "file",
13
+ source: {
14
+ type: "workspace_ref",
15
+ media_type: mediaType,
16
+ attachmentId: "att-1",
17
+ sizeBytes: 12,
18
+ filename: "clip.mp4",
19
+ },
20
+ ...(extractedText !== undefined ? { extracted_text: extractedText } : {}),
21
+ };
22
+ }
23
+
24
+ describe("fileBlockToProviderText", () => {
25
+ test("names a video without dumping extracted_text", () => {
26
+ const text = fileBlockToProviderText(
27
+ fileBlock("video/mp4", "a".repeat(1000)),
28
+ );
29
+ expect(text).toContain("video/mp4");
30
+ expect(text).toContain("workspace file");
31
+ expect(text).not.toContain("aaa");
32
+ });
33
+
34
+ test("includes extracted_text for a non-video file under the cap", () => {
35
+ const text = fileBlockToProviderText({
36
+ type: "file",
37
+ source: {
38
+ type: "workspace_ref",
39
+ media_type: "application/pdf",
40
+ attachmentId: "att-2",
41
+ sizeBytes: 20,
42
+ filename: "notes.pdf",
43
+ },
44
+ extracted_text: "quarterly summary",
45
+ });
46
+ expect(text).toContain("quarterly summary");
47
+ });
48
+
49
+ test("omits an over-cap extracted_text dump", () => {
50
+ const text = fileBlockToProviderText({
51
+ type: "file",
52
+ source: {
53
+ type: "workspace_ref",
54
+ media_type: "text/plain",
55
+ attachmentId: "att-3",
56
+ sizeBytes: 20,
57
+ filename: "huge.txt",
58
+ },
59
+ extracted_text: "x".repeat(8_000_001),
60
+ });
61
+ expect(text).toBe(PROVIDER_STRING_OMITTED_NOTE);
62
+ expect(text.length).toBeLessThan(200);
63
+ });
64
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared file-block → provider text. OpenAI, Anthropic, and Gemini all fold
3
+ * non-native files into a text part. Video is never inlined: it stays a
4
+ * workspace file and the prompt only names it.
5
+ */
6
+
7
+ import { escapeXmlAttr } from "../util/xml.js";
8
+ import { clampProviderString, isVideoMimeType } from "./content-block-size.js";
9
+ import type { ContentBlock } from "./types.js";
10
+
11
+ const VIDEO_FILE_NOTE =
12
+ "Video is stored as a workspace file. It is not inlined in this prompt.";
13
+
14
+ export function fileBlockToProviderText(
15
+ block: Extract<ContentBlock, { type: "file" }>,
16
+ ): string {
17
+ const header = `<attached_file name="${escapeXmlAttr(
18
+ block.source.filename ?? "",
19
+ )}" type="${escapeXmlAttr(block.source.media_type)}" />`;
20
+ if (isVideoMimeType(block.source.media_type)) {
21
+ return `${header}\n${VIDEO_FILE_NOTE}`;
22
+ }
23
+ const extracted = block.extracted_text?.trim() ?? "";
24
+ if (extracted.length > 0) {
25
+ return clampProviderString(`${header}\n${extracted}`);
26
+ }
27
+ return `${header}\nNo extracted text available.`;
28
+ }
@@ -10,6 +10,11 @@ import { isAbortReason } from "../../util/abort-reasons.js";
10
10
  import { ProviderError, type ProviderErrorReason } from "../../util/errors.js";
11
11
  import { getLogger } from "../../util/logger.js";
12
12
  import { DAILY_LIMIT_PATTERNS } from "../../util/provider-error-patterns.js";
13
+ import {
14
+ clampProviderString,
15
+ keepFileAsWorkspaceRef,
16
+ } from "../content-block-size.js";
17
+ import { fileBlockToProviderText } from "../file-block-text.js";
13
18
  import { base64Source, resolveMediaReferences } from "../media-resolve.js";
14
19
  import { PROVIDER_CATALOG } from "../model-catalog.js";
15
20
  import { recordProviderRequestDiagnostics } from "../request-diagnostics.js";
@@ -116,8 +121,9 @@ function clampThinkingLevelToFloor(
116
121
  * dynamic medium-level thinking).
117
122
  *
118
123
  * - `enabled: false` maps to the model's floor (the most "off" state it
119
- * allows): `"minimal"` for most models, `"low"` for Pro and Gemini 3.7
120
- * Flash, which reject `"minimal"`.
124
+ * allows): `"minimal"` for most models, `"low"` for models whose catalog
125
+ * `thinkingFloor` is `"low"` (Pro and recent Flash IDs that reject
126
+ * `"minimal"`).
121
127
  * - An explicit `level` below the floor is raised to the floor.
122
128
  * - When no `level` is pinned, Pro models get the documented default (`"high"`)
123
129
  * because an absent level resolves to the unsupported `"minimal"` upstream;
@@ -429,8 +435,7 @@ export class GeminiProvider implements Provider {
429
435
  const maxTokens = configObj?.max_tokens as number | undefined;
430
436
  const modelOverride = configObj?.model as string | undefined;
431
437
  const usageAttributionHeaders = configObj?.usageAttributionHeaders as
432
- | Record<string, string>
433
- | undefined;
438
+ Record<string, string> | undefined;
434
439
  const activeModel = modelOverride ?? this.model;
435
440
  const thinkingConfig = geminiModelSupportsThinking(activeModel)
436
441
  ? buildThinkingConfig(
@@ -762,7 +767,7 @@ export class GeminiProvider implements Provider {
762
767
  for (const block of blocks) {
763
768
  switch (block.type) {
764
769
  case "text":
765
- parts.push({ text: block.text });
770
+ parts.push({ text: clampProviderString(block.text) });
766
771
  break;
767
772
  case "image": {
768
773
  const imageSrc = base64Source(block.source);
@@ -775,6 +780,10 @@ export class GeminiProvider implements Provider {
775
780
  break;
776
781
  }
777
782
  case "file": {
783
+ if (keepFileAsWorkspaceRef(block.source)) {
784
+ parts.push({ text: fileBlockToProviderText(block) });
785
+ break;
786
+ }
778
787
  const fileSrc = base64Source(block.source);
779
788
  if (this.supportsGeminiInlineFile(fileSrc.media_type)) {
780
789
  // Normalize audio MIME onto Gemini's spelling (e.g. audio/mpeg →
@@ -797,10 +806,7 @@ export class GeminiProvider implements Provider {
797
806
  });
798
807
  }
799
808
  } else {
800
- const fallback = block.extracted_text?.trim()
801
- ? `[Attached file: ${fileSrc.filename} (${fileSrc.media_type})]\n${block.extracted_text}`
802
- : `[Attached file: ${fileSrc.filename} (${fileSrc.media_type})]\nNo extracted text available.`;
803
- parts.push({ text: fallback });
809
+ parts.push({ text: fileBlockToProviderText(block) });
804
810
  }
805
811
  break;
806
812
  }