@vellumai/assistant 0.11.6 → 0.11.7-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +31 -19
- package/Dockerfile +1 -0
- package/docs/architecture/security.md +3 -3
- package/docs/flux-turn-detection-spike.md +8 -7
- package/docs/runbook-trusted-contacts.md +14 -0
- package/docs/stt-provider-onboarding.md +8 -4
- package/knip.json +1 -0
- package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
- package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
- package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
- package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
- package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
- package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
- package/node_modules/@vellumai/environments/src/shell.ts +65 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
- package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
- package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/openapi.yaml +419 -55
- package/package.json +4 -1
- package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
- package/src/__tests__/approval-card-edit.test.ts +153 -0
- package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
- package/src/__tests__/available-skills.test.ts +54 -0
- package/src/__tests__/avatar-identity-sync.test.ts +5 -1
- package/src/__tests__/catalog-install-normalize.test.ts +9 -1
- package/src/__tests__/channel-policy.test.ts +1 -3
- package/src/__tests__/channel-readiness-discord.test.ts +9 -70
- package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
- package/src/__tests__/config-loader-backfill.test.ts +69 -0
- package/src/__tests__/config-schema.test.ts +2 -3
- package/src/__tests__/conversation-agent-loop.test.ts +238 -0
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +44 -0
- package/src/__tests__/conversation-evictor.test.ts +19 -0
- package/src/__tests__/conversation-fork-referential.test.ts +73 -63
- package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
- package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
- package/src/__tests__/conversation-slash.test.ts +9 -0
- package/src/__tests__/conversation-title-service.test.ts +97 -0
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
- package/src/__tests__/credential-broker-server-use.test.ts +0 -218
- package/src/__tests__/credential-broker.test.ts +43 -420
- package/src/__tests__/credential-routes.test.ts +17 -1
- package/src/__tests__/credential-security-invariants.test.ts +8 -9
- package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
- package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
- package/src/__tests__/discord-channel-config.test.ts +237 -0
- package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
- package/src/__tests__/fallback-breaker.test.ts +1335 -0
- package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
- package/src/__tests__/image-conversion.test.ts +38 -4
- package/src/__tests__/install-skill-routing.test.ts +25 -0
- package/src/__tests__/llm-resolver.test.ts +74 -0
- package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
- package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
- package/src/__tests__/managed-profile-guard.test.ts +28 -0
- package/src/__tests__/managed-speech-defaults.test.ts +196 -1
- package/src/__tests__/mcp-cli.test.ts +23 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
- package/src/__tests__/media-stream-stt-session.test.ts +2 -0
- package/src/__tests__/notification-decision-fallback.test.ts +106 -0
- package/src/__tests__/notification-discord-adapter.test.ts +162 -0
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
- package/src/__tests__/persist-unsendable-image.test.ts +3 -3
- package/src/__tests__/platform.test.ts +19 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
- package/src/__tests__/provider-usage-tracking.test.ts +56 -0
- package/src/__tests__/require-fresh-approval.test.ts +5 -2
- package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
- package/src/__tests__/schedule-retry.test.ts +183 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
- package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
- package/src/__tests__/skill-load-tool.test.ts +34 -0
- package/src/__tests__/skill-tool-factory.test.ts +6 -1
- package/src/__tests__/skill-tool-manifest.test.ts +20 -0
- package/src/__tests__/skills.test.ts +25 -0
- package/src/__tests__/subagent-manager-notify.test.ts +38 -0
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +146 -2
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
- package/src/__tests__/tool-audit.test.ts +111 -40
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
- package/src/__tests__/ui-shape-teaching.test.ts +1 -1
- package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
- package/src/__tests__/voice-config-update.test.ts +43 -0
- package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
- package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
- package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
- package/src/agent/loop.ts +31 -2
- package/src/approvals/guardian-card-withdrawal.ts +22 -0
- package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
- package/src/avatar/avatar-manifest.ts +48 -100
- package/src/avatar/avatar-store.ts +7 -7
- package/src/avatar/ensure-raster.ts +146 -0
- package/src/avatar/resvg-lazy.test.ts +0 -1
- package/src/avatar/traits-png-sync.ts +9 -10
- package/src/background-wake/wake-intent-hooks.test.ts +0 -1
- package/src/backup/__tests__/paths.test.ts +6 -2
- package/src/backup/paths.ts +6 -4
- package/src/bundler/app-compiler.ts +2 -2
- package/src/bundler/compiler-tools.test.ts +36 -0
- package/src/bundler/compiler-tools.ts +54 -18
- package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +20 -0
- package/src/calls/media-stream-stt-session.ts +2 -1
- package/src/calls/telephony-synthesis-language.ts +13 -5
- package/src/calls/voice-session-bridge.ts +6 -4
- package/src/channels/config.ts +23 -12
- package/src/cli/AGENTS.md +3 -1
- package/src/cli/commands/backup.help.ts +10 -13
- package/src/cli/commands/contacts.help.ts +16 -4
- package/src/cli/commands/contacts.ts +5 -1
- package/src/cli/commands/credentials.help.ts +1 -1
- package/src/cli/commands/inference-providers.ts +18 -53
- package/src/cli/commands/mcp.help.ts +6 -5
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/memory/memory-retrospective.ts +1 -0
- package/src/cli/commands/stt.help.ts +2 -2
- package/src/config/__tests__/backup-schema.test.ts +1 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
- package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
- package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
- package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
- package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
- package/src/config/bundled-skills/settings/TOOLS.json +11 -5
- package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
- package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
- package/src/config/default-profile-catalog.ts +195 -10
- package/src/config/default-profile-names.ts +41 -0
- package/src/config/feature-flag-registry.json +18 -2
- package/src/config/inference-profile-validation.ts +11 -2
- package/src/config/llm-resolver.ts +25 -0
- package/src/config/loader.ts +3 -0
- package/src/config/managed-speech-defaults.ts +160 -10
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
- package/src/config/schemas/__tests__/stt.test.ts +94 -0
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/channels.ts +32 -0
- package/src/config/schemas/live-voice.ts +13 -4
- package/src/config/schemas/llm.ts +259 -11
- package/src/config/schemas/mcp.ts +10 -2
- package/src/config/schemas/memory-retrospective.ts +1 -19
- package/src/config/schemas/services.ts +24 -0
- package/src/config/schemas/stt.ts +128 -5
- package/src/config/seed-inference-profiles.ts +32 -3
- package/src/config/skill-state.ts +5 -0
- package/src/config/skills.ts +21 -0
- package/src/contacts/notify-contacts-changed.ts +5 -2
- package/src/context/compactor.ts +74 -1
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
- package/src/daemon/config-watcher.ts +2 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
- package/src/daemon/conversation-agent-loop.ts +86 -10
- package/src/daemon/conversation-error.ts +25 -0
- package/src/daemon/conversation-evictor.ts +2 -4
- package/src/daemon/conversation-runtime-assembly.ts +19 -7
- package/src/daemon/conversation-skill-tools.ts +5 -1
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-store.ts +8 -1
- package/src/daemon/conversation-tool-setup.ts +37 -0
- package/src/daemon/conversation.ts +0 -4
- package/src/daemon/handlers/channel-config-result.ts +30 -0
- package/src/daemon/handlers/config-discord-channel.ts +301 -0
- package/src/daemon/handlers/config-ingress.ts +9 -0
- package/src/daemon/handlers/config-slack-channel.ts +13 -15
- package/src/daemon/handlers/config-telegram.ts +2 -5
- package/src/daemon/handlers/conversations.ts +0 -2
- package/src/daemon/handlers/skills.ts +25 -7
- package/src/daemon/lifecycle.ts +3 -1
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/tool-setup-types.ts +5 -3
- package/src/daemon/trust-context-types.test.ts +33 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
- package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
- package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
- package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
- package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
- package/src/live-voice/__tests__/protocol.test.ts +224 -0
- package/src/live-voice/live-voice-credential-preflight.ts +5 -1
- package/src/live-voice/live-voice-session-manager.ts +39 -2
- package/src/live-voice/live-voice-session.ts +213 -8
- package/src/live-voice/protocol.ts +187 -1
- package/src/messaging/providers/discord/api.ts +2 -1
- package/src/messaging/providers/discord/withdraw.ts +62 -0
- package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
- package/src/monitoring/__tests__/process-memory.test.ts +34 -1
- package/src/monitoring/db-integrity-sample.ts +1 -0
- package/src/monitoring/file-descriptors.ts +39 -6
- package/src/monitoring/process-memory.ts +37 -5
- package/src/monitoring/resource-sampler.ts +25 -8
- package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
- package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
- package/src/notifications/__tests__/connected-channels.test.ts +12 -0
- package/src/notifications/__tests__/copy-composer.test.ts +83 -0
- package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
- package/src/notifications/activity-failed-dedupe.ts +95 -0
- package/src/notifications/adapters/discord.ts +132 -0
- package/src/notifications/adapters/shared.ts +16 -0
- package/src/notifications/adapters/slack.ts +6 -1
- package/src/notifications/adapters/telegram.ts +5 -8
- package/src/notifications/approval-card-data.ts +94 -1
- package/src/notifications/background-failure-signal.ts +116 -0
- package/src/notifications/broadcaster.ts +3 -9
- package/src/notifications/copy-composer.ts +41 -8
- package/src/notifications/decision-engine.ts +55 -0
- package/src/notifications/destination-resolver.ts +45 -5
- package/src/notifications/emit-signal.ts +19 -5
- package/src/notifications/guardian-question-mode.ts +57 -0
- package/src/notifications/types.ts +2 -8
- package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
- package/src/oauth/seed-providers.ts +261 -0
- package/src/permissions/checker.test.ts +21 -0
- package/src/permissions/checker.ts +6 -0
- package/src/permissions/gateway-threshold-reader.test.ts +184 -0
- package/src/permissions/gateway-threshold-reader.ts +144 -10
- package/src/permissions/prompter.ts +0 -6
- package/src/permissions/types.ts +5 -4
- package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
- package/src/persistence/bulk-write-gate.ts +9 -10
- package/src/persistence/conversation-crud.ts +85 -133
- package/src/persistence/conversation-title-service.ts +43 -29
- package/src/persistence/embeddings/embedding-local.ts +14 -85
- package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
- package/src/persistence/lifecycle-events-store.test.ts +50 -0
- package/src/persistence/lifecycle-events-store.ts +75 -5
- package/src/platform/platform-patch-queue.test.ts +368 -0
- package/src/platform/platform-patch-queue.ts +190 -0
- package/src/platform/sync-avatar.test.ts +527 -0
- package/src/platform/sync-avatar.ts +209 -0
- package/src/platform/sync-identity.test.ts +75 -0
- package/src/platform/sync-identity.ts +17 -110
- package/src/plugin-api/model-profiles.test.ts +2 -2
- package/src/plugin-api/model-profiles.ts +7 -7
- package/src/plugin-api/transcription-session.ts +1 -1
- package/src/plugins/defaults/compaction/window-manager.ts +4 -0
- package/src/plugins/defaults/image-recovery/recover.ts +2 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
- package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
- package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
- package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
- package/src/plugins/defaults/memory/worker.ts +7 -0
- package/src/providers/__tests__/retry-callsite.test.ts +55 -0
- package/src/providers/connection-resolution.ts +2 -17
- package/src/providers/fallback-breaker.ts +389 -0
- package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
- package/src/providers/inference/adapter-factory.ts +231 -3
- package/src/providers/inference/auth.ts +1 -0
- package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
- package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
- package/src/providers/inference/resolve-auth.ts +1 -1
- package/src/providers/media-resolve.ts +2 -3
- package/src/providers/model-catalog.ts +75 -0
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
- package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
- package/src/providers/openai/api-error-normalization.ts +13 -0
- package/src/providers/openai/chat-completions-provider.ts +84 -0
- package/src/providers/opencode/client.test.ts +150 -0
- package/src/providers/opencode/client.ts +61 -0
- package/src/providers/provider-resolvability.ts +20 -0
- package/src/providers/retry.ts +956 -68
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
- package/src/providers/speech-to-text/provider-catalog.ts +191 -3
- package/src/providers/speech-to-text/resolve.ts +132 -24
- package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
- package/src/providers/types.ts +39 -0
- package/src/providers/usage-tracking.ts +12 -1
- package/src/runtime/AGENTS.md +1 -2
- package/src/runtime/__tests__/agent-wake.test.ts +105 -3
- package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
- package/src/runtime/agent-wake.ts +44 -5
- package/src/runtime/background-job-runner.ts +68 -50
- package/src/runtime/channel-readiness-service.ts +0 -39
- package/src/runtime/http-server.ts +12 -4
- package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
- package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
- package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
- package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
- package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
- package/src/runtime/routes/approval-routes.ts +0 -12
- package/src/runtime/routes/avatar-routes.ts +17 -30
- package/src/runtime/routes/backup-routes.ts +2 -2
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
- package/src/runtime/routes/contact-prompt-routes.ts +65 -6
- package/src/runtime/routes/contact-routes.ts +20 -15
- package/src/runtime/routes/conversation-query-routes.ts +98 -10
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +2 -1
- package/src/runtime/routes/guardian-approval-interception.ts +12 -9
- package/src/runtime/routes/inbound-message-handler.ts +21 -9
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
- package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
- package/src/runtime/routes/inference-profiles-routes.ts +12 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
- package/src/runtime/routes/inference-send-routes.ts +2 -2
- package/src/runtime/routes/ingress-status-routes.ts +2 -2
- package/src/runtime/routes/integrations/discord.ts +119 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
- package/src/runtime/routes/mcp-auth-routes.ts +12 -6
- package/src/runtime/routes/oauth-pending-flows.ts +136 -0
- package/src/runtime/routes/secret-routes.ts +7 -0
- package/src/runtime/routes/stt-routes.ts +38 -6
- package/src/runtime/routes/wake-conversation-routes.ts +8 -1
- package/src/runtime/routes/watch-routes.ts +9 -5
- package/src/runtime/sync/resource-sync-events.ts +2 -0
- package/src/runtime/trust-verdict-consumer.ts +3 -3
- package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
- package/src/schedule/retry-policy.ts +8 -8
- package/src/schedule/run-script.ts +14 -23
- package/src/schedule/schedule-recovery.ts +0 -1
- package/src/schedule/scheduler.ts +89 -51
- package/src/schedule/worker.ts +57 -0
- package/src/security/__tests__/openai-device-auth.test.ts +355 -0
- package/src/security/oauth2.ts +7 -0
- package/src/security/openai-device-auth.ts +364 -0
- package/src/skills/available-skills.ts +10 -1
- package/src/skills/catalog-install.ts +19 -1
- package/src/skills/inline-command-runner.ts +6 -2
- package/src/skills/platform-compatibility.test.ts +49 -0
- package/src/skills/platform-compatibility.ts +65 -0
- package/src/skills/tool-manifest.ts +20 -0
- package/src/stt/__tests__/roles.test.ts +152 -0
- package/src/stt/daemon-batch-transcriber.ts +4 -1
- package/src/stt/roles.ts +180 -0
- package/src/stt/stt-stream-session.ts +3 -2
- package/src/stt/types.ts +33 -3
- package/src/subagent/manager.ts +12 -0
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
- package/src/telemetry/live-voice-funnel.ts +12 -2
- package/src/telemetry/telemetry-wire.generated.ts +6 -0
- package/src/telemetry/tool-audit.ts +83 -30
- package/src/telemetry/tool-executed-events-store.test.ts +0 -1
- package/src/telemetry/turn-outcome.ts +75 -4
- package/src/tools/client-os.ts +115 -0
- package/src/tools/computer-use/definitions.ts +11 -7
- package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
- package/src/tools/credentials/broker-types.ts +0 -71
- package/src/tools/credentials/broker.ts +6 -201
- package/src/tools/executor.ts +2 -8
- package/src/tools/host-shell.test.ts +75 -0
- package/src/tools/host-shell.ts +24 -0
- package/src/tools/host-terminal/host-shell.ts +15 -44
- package/src/tools/mcp/mcp-tool-factory.ts +44 -9
- package/src/tools/permission-checker.ts +73 -21
- package/src/tools/policy-context.ts +1 -0
- package/src/tools/shared/filesystem/image-read.ts +1 -1
- package/src/tools/skills/find-similar-skills.ts +10 -2
- package/src/tools/skills/load.ts +41 -6
- package/src/tools/skills/sandbox-runner.ts +8 -16
- package/src/tools/skills/skill-tool-factory.ts +1 -0
- package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
- package/src/tools/terminal/safe-env.ts +39 -9
- package/src/tools/terminal/shell.ts +12 -31
- package/src/tools/tool-approval-handler.ts +64 -10
- package/src/tools/tool-types.ts +10 -0
- package/src/tools/types.ts +19 -3
- package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
- package/src/tools/workflows/manage-workflows.ts +2 -2
- package/src/tools/workflows/run-workflow.test.ts +2 -1
- package/src/util/__tests__/file-use.test.ts +61 -0
- package/src/util/__tests__/process-table.test.ts +66 -0
- package/src/util/__tests__/process-tree.test.ts +46 -0
- package/src/util/errors.ts +1 -0
- package/src/util/file-use.ts +91 -0
- package/src/util/host-process.test.ts +17 -0
- package/src/util/host-process.ts +75 -0
- package/src/util/image-conversion.ts +39 -7
- package/src/util/platform.ts +32 -13
- package/src/util/process-table.ts +249 -0
- package/src/util/process-tree.ts +85 -129
- package/src/util/provider-error-patterns.ts +20 -0
- package/src/util/spawn.ts +9 -2
- package/src/workspace/git-service.ts +4 -14
- package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
- package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
- package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
- package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/permission-types.test.ts +0 -17
- package/src/channels/gateway-discord-admission.ts +0 -32
- package/src/persistence/fork-message-copy.ts +0 -239
- package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
- package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
- package/src/security/oauth2-device-code.ts +0 -312
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed backup-profile dispatch against a mocked platform proxy.
|
|
3
|
+
*
|
|
4
|
+
* The primary test path includes `CallSiteRoutingProvider`, connection
|
|
5
|
+
* selection, the real adapter factory, retry escalation, and provider clients.
|
|
6
|
+
* Only the remote platform proxy is replaced with a local HTTP server.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
afterAll,
|
|
11
|
+
afterEach,
|
|
12
|
+
beforeAll,
|
|
13
|
+
beforeEach,
|
|
14
|
+
describe,
|
|
15
|
+
expect,
|
|
16
|
+
test,
|
|
17
|
+
} from "bun:test";
|
|
18
|
+
|
|
19
|
+
import { CallSiteRoutingProvider } from "../providers/call-site-routing.js";
|
|
20
|
+
import {
|
|
21
|
+
resetFallbackBreaker,
|
|
22
|
+
shouldSkipPrimary,
|
|
23
|
+
} from "../providers/fallback-breaker.js";
|
|
24
|
+
import { createAdapterFromConnection } from "../providers/inference/adapter-factory.js";
|
|
25
|
+
import type {
|
|
26
|
+
ProviderConnection,
|
|
27
|
+
ResolvedAuth,
|
|
28
|
+
} from "../providers/inference/auth.js";
|
|
29
|
+
import type {
|
|
30
|
+
Message,
|
|
31
|
+
Provider,
|
|
32
|
+
ProviderResponse,
|
|
33
|
+
} from "../providers/types.js";
|
|
34
|
+
import { getManagedUpstream } from "../providers/vellum-model-routing.js";
|
|
35
|
+
import { credentialKey } from "../security/credential-key.js";
|
|
36
|
+
import { setSecureKeyAsync } from "../security/secure-keys.js";
|
|
37
|
+
import { setConfig } from "./helpers/set-config.js";
|
|
38
|
+
|
|
39
|
+
interface UpstreamRequest {
|
|
40
|
+
path: string;
|
|
41
|
+
headers: Record<string, string>;
|
|
42
|
+
body: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const requests: UpstreamRequest[] = [];
|
|
46
|
+
let anthropicMode: "serve" | "retired" | "unavailable" = "serve";
|
|
47
|
+
|
|
48
|
+
const ANTHROPIC_PATH = "/v1/runtime-proxy/anthropic";
|
|
49
|
+
const OPENAI_PATH = "/v1/runtime-proxy/openai";
|
|
50
|
+
|
|
51
|
+
function anthropicCompletion(): Response {
|
|
52
|
+
const events = [
|
|
53
|
+
{
|
|
54
|
+
type: "message_start",
|
|
55
|
+
message: {
|
|
56
|
+
id: "msg_fallback_01",
|
|
57
|
+
type: "message",
|
|
58
|
+
role: "assistant",
|
|
59
|
+
content: [],
|
|
60
|
+
model: "claude-opus-5",
|
|
61
|
+
stop_reason: null,
|
|
62
|
+
stop_sequence: null,
|
|
63
|
+
usage: { input_tokens: 7, output_tokens: 1 },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: "content_block_start",
|
|
68
|
+
index: 0,
|
|
69
|
+
content_block: { type: "text", text: "" },
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: "content_block_delta",
|
|
73
|
+
index: 0,
|
|
74
|
+
delta: { type: "text_delta", text: "served by the backup" },
|
|
75
|
+
},
|
|
76
|
+
{ type: "content_block_stop", index: 0 },
|
|
77
|
+
{
|
|
78
|
+
type: "message_delta",
|
|
79
|
+
delta: { stop_reason: "end_turn", stop_sequence: null },
|
|
80
|
+
usage: { output_tokens: 4 },
|
|
81
|
+
},
|
|
82
|
+
{ type: "message_stop" },
|
|
83
|
+
];
|
|
84
|
+
const encoder = new TextEncoder();
|
|
85
|
+
const stream = new ReadableStream({
|
|
86
|
+
start(controller) {
|
|
87
|
+
for (const event of events) {
|
|
88
|
+
controller.enqueue(
|
|
89
|
+
encoder.encode(
|
|
90
|
+
`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`,
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
controller.close();
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
return new Response(stream, {
|
|
98
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function upstreamUnavailable(): Response {
|
|
103
|
+
return new Response(
|
|
104
|
+
JSON.stringify({ error: { message: "upstream unavailable" } }),
|
|
105
|
+
{
|
|
106
|
+
status: 503,
|
|
107
|
+
headers: { "content-type": "application/json", "retry-after": "0" },
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function modelRetired(): Response {
|
|
113
|
+
return new Response(
|
|
114
|
+
JSON.stringify({
|
|
115
|
+
type: "error",
|
|
116
|
+
error: { type: "not_found_error", message: "model: unknown model id" },
|
|
117
|
+
}),
|
|
118
|
+
{ status: 404, headers: { "content-type": "application/json" } },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let server: ReturnType<typeof Bun.serve> | undefined;
|
|
123
|
+
|
|
124
|
+
const MESSAGES: Message[] = [
|
|
125
|
+
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const ASSISTANT_API_KEY = credentialKey("vellum", "assistant_api_key");
|
|
129
|
+
const BYOK_CREDENTIAL = credentialKey("anthropic", "api_key");
|
|
130
|
+
|
|
131
|
+
const vellumConnection = {
|
|
132
|
+
name: "vellum",
|
|
133
|
+
provider: "vellum",
|
|
134
|
+
auth: { type: "platform" },
|
|
135
|
+
label: "Vellum",
|
|
136
|
+
baseUrl: null,
|
|
137
|
+
models: null,
|
|
138
|
+
} as unknown as ProviderConnection;
|
|
139
|
+
|
|
140
|
+
const byokConnection = {
|
|
141
|
+
name: "anthropic-personal",
|
|
142
|
+
provider: "anthropic",
|
|
143
|
+
auth: { type: "api_key", credential: BYOK_CREDENTIAL },
|
|
144
|
+
label: "Anthropic (personal)",
|
|
145
|
+
baseUrl: null,
|
|
146
|
+
models: null,
|
|
147
|
+
} as unknown as ProviderConnection;
|
|
148
|
+
|
|
149
|
+
function managedAuth(proxyPath: string): ResolvedAuth {
|
|
150
|
+
if (server === undefined) {
|
|
151
|
+
throw new Error("test proxy is not running");
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
kind: "header",
|
|
155
|
+
headers: { Authorization: "Bearer test-assistant-key" },
|
|
156
|
+
baseUrl: `http://127.0.0.1:${server.port}${proxyPath}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function requestsTo(proxyPath: string): UpstreamRequest[] {
|
|
161
|
+
return requests.filter((request) => request.path.startsWith(proxyPath));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createManagedAdapter(upstream: string, model: string): Provider {
|
|
165
|
+
const proxyPath =
|
|
166
|
+
upstream === "openai"
|
|
167
|
+
? OPENAI_PATH
|
|
168
|
+
: upstream === "anthropic"
|
|
169
|
+
? ANTHROPIC_PATH
|
|
170
|
+
: null;
|
|
171
|
+
if (proxyPath === null) {
|
|
172
|
+
throw new Error(`unsupported test upstream: ${upstream}`);
|
|
173
|
+
}
|
|
174
|
+
const provider = createAdapterFromConnection(
|
|
175
|
+
vellumConnection,
|
|
176
|
+
managedAuth(proxyPath),
|
|
177
|
+
{
|
|
178
|
+
model,
|
|
179
|
+
provider: upstream,
|
|
180
|
+
streamTimeoutMs: 30_000,
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
if (provider === null) {
|
|
184
|
+
throw new Error(`failed to create ${upstream} test adapter`);
|
|
185
|
+
}
|
|
186
|
+
return provider;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Construct the production wrapper order that calls use. The connection
|
|
191
|
+
* resolver builds the managed adapter for the provider/model selected by the
|
|
192
|
+
* call-site router, so fallback eligibility sees the original call-site
|
|
193
|
+
* options while the primary transport matches the resolved route.
|
|
194
|
+
*/
|
|
195
|
+
function createRoutedManagedProvider(): Provider {
|
|
196
|
+
const defaultProvider = createManagedAdapter("openai", "gpt-5.6-sol");
|
|
197
|
+
return new CallSiteRoutingProvider(
|
|
198
|
+
defaultProvider,
|
|
199
|
+
async (connectionName, expectedProvider, model) => {
|
|
200
|
+
if (connectionName !== "vellum" || model === undefined) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const upstream =
|
|
204
|
+
expectedProvider === "vellum"
|
|
205
|
+
? getManagedUpstream(model)
|
|
206
|
+
: expectedProvider;
|
|
207
|
+
return upstream === undefined || upstream === null
|
|
208
|
+
? null
|
|
209
|
+
: createManagedAdapter(upstream, model);
|
|
210
|
+
},
|
|
211
|
+
defaultProvider.routeAttribution,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function captureError(promise: Promise<unknown>): Promise<unknown> {
|
|
216
|
+
try {
|
|
217
|
+
await promise;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return error;
|
|
220
|
+
}
|
|
221
|
+
throw new Error("expected the promise to reject");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
beforeAll(async () => {
|
|
225
|
+
server = Bun.serve({
|
|
226
|
+
hostname: "127.0.0.1",
|
|
227
|
+
port: 0,
|
|
228
|
+
async fetch(req) {
|
|
229
|
+
const url = new URL(req.url);
|
|
230
|
+
let body: Record<string, unknown> = {};
|
|
231
|
+
try {
|
|
232
|
+
body = (await req.json()) as Record<string, unknown>;
|
|
233
|
+
} catch {
|
|
234
|
+
// The request path is sufficient for non-JSON probes.
|
|
235
|
+
}
|
|
236
|
+
requests.push({
|
|
237
|
+
path: url.pathname,
|
|
238
|
+
headers: Object.fromEntries(req.headers.entries()),
|
|
239
|
+
body,
|
|
240
|
+
});
|
|
241
|
+
if (url.pathname.startsWith(ANTHROPIC_PATH)) {
|
|
242
|
+
if (anthropicMode === "retired") {
|
|
243
|
+
return modelRetired();
|
|
244
|
+
}
|
|
245
|
+
return anthropicMode === "unavailable"
|
|
246
|
+
? upstreamUnavailable()
|
|
247
|
+
: anthropicCompletion();
|
|
248
|
+
}
|
|
249
|
+
return upstreamUnavailable();
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
setConfig("platform", { baseUrl: `http://127.0.0.1:${server.port}` });
|
|
253
|
+
await setSecureKeyAsync(ASSISTANT_API_KEY, "test-assistant-key");
|
|
254
|
+
await setSecureKeyAsync(BYOK_CREDENTIAL, "test-byok-key");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
afterAll(() => {
|
|
258
|
+
server?.stop(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
beforeEach(() => {
|
|
262
|
+
resetFallbackBreaker();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
afterEach(() => {
|
|
266
|
+
requests.length = 0;
|
|
267
|
+
anthropicMode = "serve";
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("managed fallback dispatch", () => {
|
|
271
|
+
test("a managed default outage is served through the routed backup profile", async () => {
|
|
272
|
+
setConfig("llm", { activeProfile: "quality-optimized" });
|
|
273
|
+
const provider = createRoutedManagedProvider();
|
|
274
|
+
|
|
275
|
+
const response: ProviderResponse = await provider.sendMessage(MESSAGES, {
|
|
276
|
+
config: { callSite: "mainAgent" },
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(requestsTo(OPENAI_PATH).length).toBeGreaterThan(1);
|
|
280
|
+
const backup = requestsTo(ANTHROPIC_PATH);
|
|
281
|
+
expect(backup.length).toBe(1);
|
|
282
|
+
expect(backup[0].body.model).toBe("claude-opus-5");
|
|
283
|
+
expect(backup[0].body.max_tokens).toBe(32000);
|
|
284
|
+
expect(backup[0].body.thinking).toMatchObject({ type: "adaptive" });
|
|
285
|
+
expect(backup[0].headers["x-vellum-inference-profile"]).toBe(
|
|
286
|
+
"quality-optimized-backup",
|
|
287
|
+
);
|
|
288
|
+
expect(response.actualInferenceProfile).toBe("quality-optimized-backup");
|
|
289
|
+
expect(response.actualProvider).toBe("anthropic");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test.each([
|
|
293
|
+
["model", "gpt-5.6-sol"],
|
|
294
|
+
["provider", "openai"],
|
|
295
|
+
] as const)(
|
|
296
|
+
"a persisted call-site %s pin never falls back",
|
|
297
|
+
async (field, value) => {
|
|
298
|
+
setConfig("llm", {
|
|
299
|
+
activeProfile: "quality-optimized",
|
|
300
|
+
callSites: { mainAgent: { [field]: value } },
|
|
301
|
+
});
|
|
302
|
+
const provider = createRoutedManagedProvider();
|
|
303
|
+
|
|
304
|
+
const error = await captureError(
|
|
305
|
+
provider.sendMessage(MESSAGES, { config: { callSite: "mainAgent" } }),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
expect((error as { provider?: string }).provider).toBe("openai");
|
|
309
|
+
expect(requestsTo(OPENAI_PATH).length).toBeGreaterThan(1);
|
|
310
|
+
expect(requestsTo(ANTHROPIC_PATH)).toHaveLength(0);
|
|
311
|
+
expect(shouldSkipPrimary({ upstream: "openai" })).toBe(false);
|
|
312
|
+
},
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
test("a BYOK connection never installs automatic fallback routing", async () => {
|
|
316
|
+
setConfig("llm", {
|
|
317
|
+
profiles: {
|
|
318
|
+
"byok-primary": {
|
|
319
|
+
source: "user",
|
|
320
|
+
provider: "anthropic",
|
|
321
|
+
model: "claude-opus-5",
|
|
322
|
+
maxTokens: 1024,
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
activeProfile: "byok-primary",
|
|
326
|
+
});
|
|
327
|
+
anthropicMode = "retired";
|
|
328
|
+
const provider = createAdapterFromConnection(
|
|
329
|
+
byokConnection,
|
|
330
|
+
{
|
|
331
|
+
kind: "header",
|
|
332
|
+
headers: { Authorization: "Bearer test-byok-key" },
|
|
333
|
+
baseUrl: `http://127.0.0.1:${server?.port}${ANTHROPIC_PATH}`,
|
|
334
|
+
},
|
|
335
|
+
{ model: "claude-opus-5", streamTimeoutMs: 30_000 },
|
|
336
|
+
);
|
|
337
|
+
expect(provider).not.toBeNull();
|
|
338
|
+
|
|
339
|
+
const error = await captureError(
|
|
340
|
+
provider!.sendMessage(MESSAGES, { config: { callSite: "mainAgent" } }),
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
expect((error as { statusCode?: number }).statusCode).toBe(404);
|
|
344
|
+
expect(new Set(requests.map((request) => request.body.model))).toEqual(
|
|
345
|
+
new Set(["claude-opus-5"]),
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("a custom managed profile has no automatic fallback contract", async () => {
|
|
350
|
+
setConfig("llm", {
|
|
351
|
+
profiles: {
|
|
352
|
+
custom: {
|
|
353
|
+
source: "user",
|
|
354
|
+
provider: "vellum",
|
|
355
|
+
model: "claude-opus-5",
|
|
356
|
+
maxTokens: 1024,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
activeProfile: "custom",
|
|
360
|
+
});
|
|
361
|
+
anthropicMode = "retired";
|
|
362
|
+
const provider = createRoutedManagedProvider();
|
|
363
|
+
|
|
364
|
+
const error = await captureError(
|
|
365
|
+
provider.sendMessage(MESSAGES, { config: { callSite: "mainAgent" } }),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
expect((error as { statusCode?: number }).statusCode).toBe(404);
|
|
369
|
+
expect(new Set(requests.map((request) => request.body.model))).toEqual(
|
|
370
|
+
new Set(["claude-opus-5"]),
|
|
371
|
+
);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("a user shadow cannot change the code-owned backup route", async () => {
|
|
375
|
+
setConfig("llm", {
|
|
376
|
+
profiles: {
|
|
377
|
+
"quality-optimized-backup": {
|
|
378
|
+
source: "user",
|
|
379
|
+
provider: "missing-connection-entry",
|
|
380
|
+
model: "custom-model",
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
activeProfile: "quality-optimized",
|
|
384
|
+
});
|
|
385
|
+
const provider = createRoutedManagedProvider();
|
|
386
|
+
|
|
387
|
+
const response = await provider.sendMessage(MESSAGES, {
|
|
388
|
+
config: { callSite: "mainAgent" },
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const backup = requestsTo(ANTHROPIC_PATH);
|
|
392
|
+
expect(backup).toHaveLength(1);
|
|
393
|
+
expect(backup[0].body.model).toBe("claude-opus-5");
|
|
394
|
+
expect(response.actualInferenceProfile).toBe("quality-optimized-backup");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("a failed backup preserves the primary error and does not count as served", async () => {
|
|
398
|
+
setConfig("llm", { activeProfile: "quality-optimized" });
|
|
399
|
+
anthropicMode = "unavailable";
|
|
400
|
+
const provider = createRoutedManagedProvider();
|
|
401
|
+
|
|
402
|
+
const error = await captureError(
|
|
403
|
+
provider.sendMessage(MESSAGES, { config: { callSite: "mainAgent" } }),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
expect((error as { provider?: string }).provider).toBe("openai");
|
|
407
|
+
expect(requestsTo(OPENAI_PATH).length).toBeGreaterThan(1);
|
|
408
|
+
expect(requestsTo(ANTHROPIC_PATH).length).toBeGreaterThan(0);
|
|
409
|
+
expect(shouldSkipPrimary({ upstream: "openai" })).toBe(false);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("a served backup opens the breaker for the routed primary", async () => {
|
|
413
|
+
setConfig("llm", { activeProfile: "quality-optimized" });
|
|
414
|
+
const provider = createRoutedManagedProvider();
|
|
415
|
+
|
|
416
|
+
await provider.sendMessage(MESSAGES, {
|
|
417
|
+
config: { callSite: "mainAgent" },
|
|
418
|
+
});
|
|
419
|
+
expect(shouldSkipPrimary({ upstream: "openai" })).toBe(true);
|
|
420
|
+
|
|
421
|
+
requests.length = 0;
|
|
422
|
+
const response = await provider.sendMessage(MESSAGES, {
|
|
423
|
+
config: { callSite: "mainAgent" },
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
expect(requestsTo(OPENAI_PATH)).toHaveLength(0);
|
|
427
|
+
expect(requestsTo(ANTHROPIC_PATH)).toHaveLength(1);
|
|
428
|
+
expect(response.actualInferenceProfile).toBe("quality-optimized-backup");
|
|
429
|
+
});
|
|
430
|
+
});
|
|
@@ -286,6 +286,34 @@ describe("PUT /v1/config/llm/profiles/:name — managed profile guard", () => {
|
|
|
286
286
|
expectNothingCommitted();
|
|
287
287
|
});
|
|
288
288
|
|
|
289
|
+
test("PUT on a managed backup profile is rejected as code-owned", async () => {
|
|
290
|
+
// Backups are code-owned too, and they carry no on-disk entry, so the
|
|
291
|
+
// code-owned rejection has to win over the availability gate: reporting
|
|
292
|
+
// "not currently available" would be wrong for a profile the picker
|
|
293
|
+
// lists and the user can select.
|
|
294
|
+
seedRawConfig({ llm: { profiles: {} } });
|
|
295
|
+
|
|
296
|
+
for (const name of [
|
|
297
|
+
"balanced-backup",
|
|
298
|
+
"quality-optimized-backup",
|
|
299
|
+
"cost-optimized-backup",
|
|
300
|
+
"latency-optimized-backup",
|
|
301
|
+
]) {
|
|
302
|
+
for (const body of [
|
|
303
|
+
{ label: "Zippy" },
|
|
304
|
+
{ status: "active" as const },
|
|
305
|
+
{ status: null },
|
|
306
|
+
]) {
|
|
307
|
+
await expect(
|
|
308
|
+
replaceRoute.handler({ pathParams: { name }, body }),
|
|
309
|
+
).rejects.toThrow(
|
|
310
|
+
`Profile "${name}" is code-owned and cannot be edited.`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
expectNothingCommitted();
|
|
315
|
+
});
|
|
316
|
+
|
|
289
317
|
test("PUT { status: null } on managed profile clears status (back to active-by-absence)", async () => {
|
|
290
318
|
seedRawConfig({
|
|
291
319
|
llm: {
|
|
@@ -26,7 +26,11 @@ mock.module("../calls/telephony-tts-capability.js", () => ({
|
|
|
26
26
|
}));
|
|
27
27
|
|
|
28
28
|
import { getConfig, invalidateConfigCache } from "../config/loader.js";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
maybeDefaultSpeechToManaged,
|
|
31
|
+
resolveEffectiveSpeechProviders,
|
|
32
|
+
} from "../config/managed-speech-defaults.js";
|
|
33
|
+
import { sttCatalogKeyForRole } from "../stt/roles.js";
|
|
30
34
|
|
|
31
35
|
const WORKSPACE_DIR = process.env.VELLUM_WORKSPACE_DIR!;
|
|
32
36
|
const CONFIG_PATH = join(WORKSPACE_DIR, "config.json");
|
|
@@ -115,6 +119,112 @@ describe("maybeDefaultSpeechToManaged", () => {
|
|
|
115
119
|
});
|
|
116
120
|
});
|
|
117
121
|
|
|
122
|
+
test("a narrowing stand-in is written to the live-voice role, not the global", async () => {
|
|
123
|
+
// BYOK Flux with no key: the stand-in preserves turn detection, so it is
|
|
124
|
+
// vellum-flux, which streams and nothing else. Writing that globally is
|
|
125
|
+
// how batch transcription and telephony disappear for a managed user.
|
|
126
|
+
mockManagedSpeechAvailable = true;
|
|
127
|
+
writeConfig({
|
|
128
|
+
services: {
|
|
129
|
+
stt: {
|
|
130
|
+
provider: "deepgram",
|
|
131
|
+
providers: { deepgram: { model: "flux" } },
|
|
132
|
+
},
|
|
133
|
+
tts: { provider: "vellum" },
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await maybeDefaultSpeechToManaged();
|
|
138
|
+
|
|
139
|
+
const stt = (readConfig().services as any).stt;
|
|
140
|
+
expect(stt.roles.liveVoice).toEqual({ provider: "vellum", model: "flux" });
|
|
141
|
+
// The other consumers land on the stand-in's base, which serves them all.
|
|
142
|
+
expect(stt.provider).toBe("vellum");
|
|
143
|
+
expect(stt.providers.vellum.model).toBe("nova-3");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("the role write leaves batch and telephony on a provider that answers", async () => {
|
|
147
|
+
mockManagedSpeechAvailable = true;
|
|
148
|
+
writeConfig({
|
|
149
|
+
services: {
|
|
150
|
+
stt: {
|
|
151
|
+
provider: "deepgram",
|
|
152
|
+
providers: { deepgram: { model: "flux" } },
|
|
153
|
+
},
|
|
154
|
+
tts: { provider: "vellum" },
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await maybeDefaultSpeechToManaged();
|
|
159
|
+
|
|
160
|
+
invalidateConfigCache();
|
|
161
|
+
const stt = getConfig().services.stt;
|
|
162
|
+
expect(sttCatalogKeyForRole(stt, "liveVoice")).toBe("vellum-flux");
|
|
163
|
+
expect(sttCatalogKeyForRole(stt, "batch")).toBe("vellum");
|
|
164
|
+
expect(sttCatalogKeyForRole(stt, "telephony")).toBe("vellum");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("a language Flux has no model for keeps live voice on nova-3", async () => {
|
|
168
|
+
// Korean is outside Flux's ten-language roster, so the relay would reject
|
|
169
|
+
// the dial. Standing in with Flux hands the speaker a dead mic rather
|
|
170
|
+
// than a worse transcriber.
|
|
171
|
+
mockManagedSpeechAvailable = true;
|
|
172
|
+
writeConfig({
|
|
173
|
+
services: {
|
|
174
|
+
stt: {
|
|
175
|
+
provider: "deepgram",
|
|
176
|
+
language: "ko",
|
|
177
|
+
providers: { deepgram: { model: "flux" } },
|
|
178
|
+
},
|
|
179
|
+
tts: { provider: "vellum" },
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await maybeDefaultSpeechToManaged();
|
|
184
|
+
|
|
185
|
+
const stt = (readConfig().services as any).stt;
|
|
186
|
+
expect(stt.provider).toBe("vellum");
|
|
187
|
+
expect(stt.providers.vellum.model).toBe("nova-3");
|
|
188
|
+
expect(stt.roles).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("a language Flux does serve still reaches the flux stand-in", async () => {
|
|
192
|
+
mockManagedSpeechAvailable = true;
|
|
193
|
+
writeConfig({
|
|
194
|
+
services: {
|
|
195
|
+
stt: {
|
|
196
|
+
provider: "deepgram",
|
|
197
|
+
language: "hi",
|
|
198
|
+
providers: { deepgram: { model: "flux" } },
|
|
199
|
+
},
|
|
200
|
+
tts: { provider: "vellum" },
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await maybeDefaultSpeechToManaged();
|
|
205
|
+
|
|
206
|
+
const stt = (readConfig().services as any).stt;
|
|
207
|
+
expect(stt.roles.liveVoice).toEqual({ provider: "vellum", model: "flux" });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("a capability-preserving stand-in still writes the global provider", async () => {
|
|
211
|
+
// Plain deepgram has no turn detection, so the stand-in is plain vellum,
|
|
212
|
+
// which serves every role. That one belongs on the global, not a role.
|
|
213
|
+
mockManagedSpeechAvailable = true;
|
|
214
|
+
writeConfig({
|
|
215
|
+
services: {
|
|
216
|
+
stt: { provider: "deepgram" },
|
|
217
|
+
tts: { provider: "vellum" },
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await maybeDefaultSpeechToManaged();
|
|
222
|
+
|
|
223
|
+
const stt = (readConfig().services as any).stt;
|
|
224
|
+
expect(stt.provider).toBe("vellum");
|
|
225
|
+
expect(stt.roles).toBeUndefined();
|
|
226
|
+
});
|
|
227
|
+
|
|
118
228
|
test("never repoints an explicit BYOK provider with resolving credentials", async () => {
|
|
119
229
|
mockManagedSpeechAvailable = true;
|
|
120
230
|
mockSttKeyResolves = true;
|
|
@@ -133,3 +243,88 @@ describe("maybeDefaultSpeechToManaged", () => {
|
|
|
133
243
|
expect((config.services as any).tts.provider).toBe("elevenlabs");
|
|
134
244
|
});
|
|
135
245
|
});
|
|
246
|
+
|
|
247
|
+
describe("managed live voice runs Flux", () => {
|
|
248
|
+
// Turn detection is the only reason to reach for that family, and a managed
|
|
249
|
+
// user cannot ask for it: the provider picker offers no family, so `vellum`
|
|
250
|
+
// is as specific as they can be.
|
|
251
|
+
test("live voice upgrades to the flux family", async () => {
|
|
252
|
+
mockManagedSpeechAvailable = true;
|
|
253
|
+
writeConfig({ services: { stt: { provider: "vellum" } } });
|
|
254
|
+
|
|
255
|
+
const { stt } = await resolveEffectiveSpeechProviders(undefined, {
|
|
256
|
+
role: "liveVoice",
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(stt).toBe("vellum-flux");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("no other consumer follows it there", async () => {
|
|
263
|
+
// The family streams and nothing else, so batch and telephony would lose
|
|
264
|
+
// their transcriber entirely.
|
|
265
|
+
mockManagedSpeechAvailable = true;
|
|
266
|
+
writeConfig({ services: { stt: { provider: "vellum" } } });
|
|
267
|
+
|
|
268
|
+
for (const role of ["batch", "telephony", "dictation", "watch"] as const) {
|
|
269
|
+
const { stt } = await resolveEffectiveSpeechProviders(undefined, {
|
|
270
|
+
role,
|
|
271
|
+
});
|
|
272
|
+
expect(stt).toBe("vellum");
|
|
273
|
+
}
|
|
274
|
+
const { stt: roleless } = await resolveEffectiveSpeechProviders();
|
|
275
|
+
expect(roleless).toBe("vellum");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("a family the user named wins", async () => {
|
|
279
|
+
// `services.stt.providers.vellum.model` accepts nova-3, so quietly
|
|
280
|
+
// overriding it would be the silent substitution roles exist to prevent.
|
|
281
|
+
mockManagedSpeechAvailable = true;
|
|
282
|
+
writeConfig({
|
|
283
|
+
services: {
|
|
284
|
+
stt: { provider: "vellum", providers: { vellum: { model: "nova-3" } } },
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const { stt } = await resolveEffectiveSpeechProviders(undefined, {
|
|
289
|
+
role: "liveVoice",
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(stt).toBe("vellum");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("a language the family cannot serve stays on nova-3", async () => {
|
|
296
|
+
mockManagedSpeechAvailable = true;
|
|
297
|
+
writeConfig({
|
|
298
|
+
services: { stt: { provider: "vellum", language: "ko" } },
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const { stt } = await resolveEffectiveSpeechProviders(undefined, {
|
|
302
|
+
role: "liveVoice",
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(stt).toBe("vellum");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("nothing is upgraded while managed speech is unavailable", async () => {
|
|
309
|
+
mockManagedSpeechAvailable = false;
|
|
310
|
+
writeConfig({ services: { stt: { provider: "vellum" } } });
|
|
311
|
+
|
|
312
|
+
const { stt } = await resolveEffectiveSpeechProviders(undefined, {
|
|
313
|
+
role: "liveVoice",
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(stt).toBe("vellum");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("the upgrade writes no config", async () => {
|
|
320
|
+
// A resolution rule, not a substitution: nothing lands on disk for a user
|
|
321
|
+
// to discover, reset, or have to migrate.
|
|
322
|
+
mockManagedSpeechAvailable = true;
|
|
323
|
+
writeConfig({ services: { stt: { provider: "vellum" } } });
|
|
324
|
+
const before = JSON.stringify(readConfig());
|
|
325
|
+
|
|
326
|
+
await resolveEffectiveSpeechProviders(undefined, { role: "liveVoice" });
|
|
327
|
+
|
|
328
|
+
expect(JSON.stringify(readConfig())).toBe(before);
|
|
329
|
+
});
|
|
330
|
+
});
|