@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.
- package/ARCHITECTURE.md +2 -2
- package/Dockerfile +8 -48
- package/docker-entrypoint.sh +5 -1
- package/docker-kata-apt-env.sh +3 -0
- package/docker-kata-apt-shims.sh +127 -0
- package/docker-kata-apt-wrapper.sh +45 -0
- package/docker-kata-chroot-exec.sh +35 -0
- package/docker-kata-pip.sh +8 -2
- package/docs/architecture/memory.md +8 -4
- package/docs/guardian-request-flow.md +18 -14
- package/docs/trusted-contact-access.md +1 -7
- package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
- package/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/openapi.yaml +193 -4
- package/package.json +2 -2
- package/src/__tests__/access-request-card-view.test.ts +6 -5
- package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
- package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
- package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
- package/src/__tests__/attachments-store.test.ts +27 -0
- package/src/__tests__/attachments.test.ts +43 -0
- package/src/__tests__/canned-reply-release.test.ts +48 -5
- package/src/__tests__/channel-delivery-store.test.ts +129 -18
- package/src/__tests__/channel-reply-delivery.test.ts +553 -93
- package/src/__tests__/channel-retry-sweep.test.ts +199 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
- package/src/__tests__/conversation-error.test.ts +15 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
- package/src/__tests__/conversation-pairing.test.ts +141 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
- package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
- package/src/__tests__/conversation-sync-tags.test.ts +2 -37
- package/src/__tests__/credential-health-service.test.ts +98 -10
- package/src/__tests__/delete-propagation.test.ts +469 -0
- package/src/__tests__/dm-persistence.test.ts +16 -0
- package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
- package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
- package/src/__tests__/gemini-provider.test.ts +46 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
- package/src/__tests__/guardian-gateway-sim.ts +0 -23
- package/src/__tests__/guardian-question-mode.test.ts +108 -0
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
- package/src/__tests__/host-proxy-interface.test.ts +10 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
- package/src/__tests__/injector-v3-suppression.test.ts +43 -9
- package/src/__tests__/list-messages-page-latest.test.ts +127 -0
- package/src/__tests__/list-messages-system-card.test.ts +103 -0
- package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
- package/src/__tests__/notification-decision-fallback.test.ts +199 -77
- package/src/__tests__/notification-decision-strategy.test.ts +198 -213
- package/src/__tests__/notification-discord-adapter.test.ts +25 -0
- package/src/__tests__/notification-slack-adapter.test.ts +133 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
- package/src/__tests__/openai-provider.test.ts +5 -5
- package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
- package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
- package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/process-message-display-content.test.ts +20 -9
- package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
- package/src/__tests__/provider-meta-persistence.test.ts +67 -0
- package/src/__tests__/reaction-persistence.test.ts +22 -198
- package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
- package/src/__tests__/skill-load-tool.test.ts +27 -0
- package/src/__tests__/skills.test.ts +34 -1
- package/src/__tests__/strip-memory-injections.test.ts +3 -4
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/__tests__/thread-backfill.test.ts +5 -3
- package/src/__tests__/unified-turn-context-location.test.ts +76 -0
- package/src/__tests__/voice-session-bridge.test.ts +18 -0
- package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
- package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
- package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
- package/src/agent/attachments.ts +13 -2
- package/src/agent/loop.ts +3 -19
- package/src/api/README.md +9 -5
- package/src/api/index.ts +9 -8
- package/src/api/package.json +1 -0
- package/src/api/responses/conversation-message.ts +8 -0
- package/src/api/responses/home.ts +7 -18
- package/src/api/surfaces.ts +114 -7
- package/src/approvals/AGENTS.md +1 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
- package/src/channels/__tests__/types.test.ts +22 -1
- package/src/channels/gateway-guardian-requests.ts +0 -22
- package/src/channels/types.ts +8 -6
- package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
- package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
- package/src/cli/commands/channels/index.help.ts +19 -6
- package/src/cli/commands/channels/index.ts +6 -2
- package/src/cli/commands/db/__tests__/status.test.ts +22 -0
- package/src/cli/commands/db/index.help.ts +1 -1
- package/src/cli/commands/db/status.ts +172 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
- package/src/cli/commands/platform/connect.ts +14 -5
- package/src/cli/commands/plugins.help.ts +6 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
- package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
- package/src/cli/lib/bundled-marketplace.json +14 -0
- package/src/cli/lib/install-from-github.ts +9 -5
- package/src/cli/lib/install-from-platform.ts +12 -1
- package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
- package/src/config/assistant-initiated-threads-gate.ts +50 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -3
- package/src/config/bundled-skills/schedule/SKILL.md +25 -11
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
- package/src/config/call-site-defaults.ts +4 -1
- package/src/config/feature-flag-registry.json +21 -4
- package/src/config/schemas/memory-v3.ts +4 -3
- package/src/context/strip-injections.ts +17 -56
- package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
- package/src/conversations/message-consolidation.ts +4 -3
- package/src/credential-health/credential-health-service.ts +130 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
- package/src/daemon/conversation-error.ts +2 -0
- package/src/daemon/conversation-messaging.ts +195 -47
- package/src/daemon/conversation-process.ts +20 -5
- package/src/daemon/conversation-runtime-assembly.ts +48 -39
- package/src/daemon/conversation-store.ts +159 -11
- package/src/daemon/conversation-tool-setup.ts +9 -2
- package/src/daemon/conversation.ts +290 -19
- package/src/daemon/dictation-text-processing.ts +2 -7
- package/src/daemon/handlers/config-channels.ts +33 -3
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/shared.ts +9 -0
- package/src/daemon/port-oversized-content.test.ts +117 -0
- package/src/daemon/port-oversized-content.ts +110 -0
- package/src/daemon/process-message.ts +87 -16
- package/src/daemon/reaction-record.test.ts +23 -11
- package/src/daemon/reaction-record.ts +6 -9
- package/src/documents/document-store.ts +1 -5
- package/src/home/conversation-starter-validation.ts +1 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
- package/src/live-voice/live-voice-photo.ts +544 -61
- package/src/live-voice/live-voice-session.ts +6 -2
- package/src/live-voice/protocol.ts +20 -1
- package/src/messaging/provider-message-metadata.ts +40 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
- package/src/messaging/providers/channel-transport.ts +28 -0
- package/src/messaging/providers/discord/send.ts +3 -2
- package/src/messaging/providers/slack/api.ts +11 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
- package/src/messaging/providers/slack/message-metadata.ts +141 -1
- package/src/messaging/providers/slack/send.test.ts +58 -0
- package/src/messaging/providers/slack/send.ts +4 -4
- package/src/messaging/providers/slack/transport.ts +28 -2
- package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
- package/src/messaging/providers/telegram-bot/send.ts +93 -0
- package/src/messaging/providers/telegram-bot/transport.ts +71 -0
- package/src/messaging/reaction-envelopes.test.ts +73 -0
- package/src/messaging/reaction-envelopes.ts +33 -5
- package/src/messaging/read-provider-metadata.ts +4 -3
- package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
- package/src/monitoring/recovery/db.ts +35 -0
- package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
- package/src/monitoring/recovery/run-recovery.ts +6 -2
- package/src/monitoring/recovery/stale-processing.ts +3 -20
- package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
- package/src/notifications/AGENTS.md +2 -2
- package/src/notifications/README.md +2 -2
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
- package/src/notifications/__tests__/broadcaster.test.ts +35 -4
- package/src/notifications/__tests__/notification-utils.test.ts +31 -0
- package/src/notifications/access-request-copy.ts +126 -114
- package/src/notifications/adapters/discord.ts +9 -5
- package/src/notifications/adapters/shared.ts +17 -4
- package/src/notifications/adapters/slack.ts +14 -4
- package/src/notifications/adapters/telegram.ts +8 -4
- package/src/notifications/approval-card-data.ts +5 -2
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/broadcaster.ts +57 -25
- package/src/notifications/conversation-pairing.ts +68 -2
- package/src/notifications/copy-composer.ts +13 -32
- package/src/notifications/decision-engine.ts +56 -237
- package/src/notifications/guardian-delivery-recorder.ts +5 -9
- package/src/notifications/guardian-feed-projection.ts +0 -15
- package/src/notifications/guardian-question-mode.ts +147 -6
- package/src/notifications/notification-utils.ts +117 -1
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/prompter.ts +1 -1
- package/src/persistence/conversation-crud.ts +93 -14
- package/src/persistence/conversation-queries.ts +144 -17
- package/src/persistence/conversation-types.ts +39 -5
- package/src/persistence/delivery-crud.ts +224 -28
- package/src/persistence/delivery-status.ts +21 -2
- package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
- package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
- package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
- package/src/persistence/schema/conversations.ts +71 -2
- package/src/persistence/schema-contract.test.ts +78 -0
- package/src/persistence/schema-contract.ts +96 -0
- package/src/persistence/steps.ts +4 -0
- package/src/platform/client.ts +55 -0
- package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
- package/src/plugins/defaults/injector-order.ts +2 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +1 -1
- package/src/plugins/defaults/memory/memory-marker.ts +17 -7
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
- package/src/plugins/defaults/memory/v3/injector.ts +14 -15
- package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
- package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
- package/src/plugins/defaults/memory/v3/types.ts +15 -4
- package/src/plugins/defaults/turn-context/injectors.ts +3 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
- package/src/plugins/mcp-servers.ts +14 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
- package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
- package/src/providers/anthropic/client.ts +21 -24
- package/src/providers/call-site-routing.ts +5 -4
- package/src/providers/connection-resolution.ts +29 -1
- package/src/providers/content-block-size.test.ts +82 -0
- package/src/providers/content-block-size.ts +99 -0
- package/src/providers/file-block-text.test.ts +64 -0
- package/src/providers/file-block-text.ts +28 -0
- package/src/providers/gemini/client.ts +15 -9
- package/src/providers/inference/auth.ts +6 -6
- package/src/providers/media-resolve.ts +14 -0
- package/src/providers/model-catalog.ts +47 -34
- package/src/providers/openai/chat-completions-provider.ts +9 -25
- package/src/providers/openai/responses-provider.ts +11 -18
- package/src/providers/registry.ts +10 -7
- package/src/providers/routing-identity.ts +2 -1
- package/src/providers/types.ts +1 -4
- package/src/providers/vellum-model-routing.ts +3 -2
- package/src/runtime/AGENTS.md +40 -15
- package/src/runtime/approval-message-composer.ts +1 -6
- package/src/runtime/assistant-event-hub.ts +2 -39
- package/src/runtime/channel-approval-types.ts +1 -5
- package/src/runtime/channel-reply-delivery.ts +182 -115
- package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
- package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
- package/src/runtime/channel-retry-sweep.ts +102 -1
- package/src/runtime/finalize-event-delivery.ts +4 -4
- package/src/runtime/guardian-action-message-composer.ts +1 -4
- package/src/runtime/guardian-reply-router.ts +4 -75
- package/src/runtime/http-router.ts +1 -5
- package/src/runtime/question-request-guardian-bridge.ts +2 -3
- package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
- package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
- package/src/runtime/routes/acp-routes.ts +1 -1
- package/src/runtime/routes/canned-reply-release.ts +19 -9
- package/src/runtime/routes/channel-route-shared.ts +0 -39
- package/src/runtime/routes/channel-verification-routes.ts +4 -1
- package/src/runtime/routes/conversation-list-routes.ts +44 -3
- package/src/runtime/routes/conversation-management-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +138 -59
- package/src/runtime/routes/diagnostics-routes.ts +111 -60
- package/src/runtime/routes/guardian-action-routes.ts +10 -14
- package/src/runtime/routes/guardian-approval-interception.ts +0 -5
- package/src/runtime/routes/inbound-message-handler.ts +170 -48
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/platform-routes.ts +99 -10
- package/src/runtime/routes/sight-frame-routes.ts +136 -0
- package/src/runtime/sync/resource-sync-events.ts +14 -37
- package/src/runtime/sync/sync-publisher.test.ts +5 -4
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/client-os.ts +11 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +2 -2
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/terminal/safe-env.ts +4 -0
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +4 -3
- package/src/tools/watch/watch-retro-report.ts +205 -0
- package/src/watch/__tests__/watch-retro.test.ts +268 -40
- package/src/watch/watch-retro.ts +190 -77
- package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
- package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +4 -0
- package/docker-kata-pip-chroot.sh +0 -22
- package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
- 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
|
-
|
|
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
|
-
|
|
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
|
-
| "
|
|
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
|
|
2153
|
-
: { type: "text", 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 (
|
|
2193
|
+
if (isDecodableTextMimeType(media_type)) {
|
|
2196
2194
|
// Decode base64 to UTF-8 text and send as PlainTextSource
|
|
2197
|
-
const decodedText =
|
|
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:
|
|
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
|
-
|
|
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
|
|
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: "
|
|
370
|
+
reason: "adapter_unavailable",
|
|
370
371
|
},
|
|
371
|
-
"Routed connection yielded no adapter
|
|
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
|
|
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
|
|
120
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|