@vellumai/assistant 0.11.3 → 0.11.4-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 +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +140 -38
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +2 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +55 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +82 -11
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +9 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +13 -3
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-intents.ts +26 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
- package/src/tts/reasoning-tag-filter.ts +110 -0
- package/src/workspace/byok-default-profile-ensure.ts +61 -18
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure protocol helpers for Deepgram's Flux conversational speech API
|
|
3
|
+
* (`wss://api.deepgram.com/v2/listen`).
|
|
4
|
+
*
|
|
5
|
+
* This module holds only pure functions: frame parsing and URL query
|
|
6
|
+
* construction. It owns no socket, no timers, and no I/O, so the risky part
|
|
7
|
+
* of the protocol (the wire shapes Deepgram controls) can be exercised
|
|
8
|
+
* exhaustively in tests. The streaming transcriber wires these into a
|
|
9
|
+
* WebSocket session.
|
|
10
|
+
*
|
|
11
|
+
* Flux differs from Deepgram's v1 live API in that its turn model, not the
|
|
12
|
+
* caller's endpointing heuristics, decides when a turn ends. Frames are
|
|
13
|
+
* mapped onto the daemon's {@link SttStreamServerEvent} contract so that a
|
|
14
|
+
* consumer which ignores the turn-detection events still commits transcripts
|
|
15
|
+
* from `partial` / `final` exactly as it does with any other provider.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
SttErrorCategory,
|
|
20
|
+
SttStreamServerErrorEvent,
|
|
21
|
+
SttStreamServerEvent,
|
|
22
|
+
} from "../../stt/types.js";
|
|
23
|
+
import { parseJsonSafe } from "../../util/json.js";
|
|
24
|
+
import { getLogger } from "../../util/logger.js";
|
|
25
|
+
import { isPlainObject } from "../../util/object.js";
|
|
26
|
+
|
|
27
|
+
const log = getLogger("deepgram-flux-frames");
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Wire types
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Frame discriminators Deepgram emits on `/v2/listen`.
|
|
35
|
+
*
|
|
36
|
+
* `EagerEndOfTurn` and `TurnResumed` appear only when
|
|
37
|
+
* `eager_eot_threshold` is set on the dialed URL. `Error` is a fatal frame:
|
|
38
|
+
* Deepgram terminates the session after sending it.
|
|
39
|
+
*/
|
|
40
|
+
export type FluxFrameKind =
|
|
41
|
+
| "Connected"
|
|
42
|
+
| "TurnInfo"
|
|
43
|
+
| "StartOfTurn"
|
|
44
|
+
| "Update"
|
|
45
|
+
| "EagerEndOfTurn"
|
|
46
|
+
| "TurnResumed"
|
|
47
|
+
| "EndOfTurn"
|
|
48
|
+
| "Error";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A turn-state frame.
|
|
52
|
+
*
|
|
53
|
+
* Deepgram nests the turn state under `event` on `TurnInfo` frames
|
|
54
|
+
* (`{ type: "TurnInfo", event: "EndOfTurn", ... }`) and also documents the
|
|
55
|
+
* states as frame types in their own right. Both shapes are accepted:
|
|
56
|
+
* `event` wins when present, otherwise `type` is the discriminator.
|
|
57
|
+
*/
|
|
58
|
+
export interface FluxTurnFrame {
|
|
59
|
+
type: FluxFrameKind;
|
|
60
|
+
/** Turn state carried by a `TurnInfo` frame. */
|
|
61
|
+
event?: FluxFrameKind;
|
|
62
|
+
request_id?: string;
|
|
63
|
+
sequence_id?: number;
|
|
64
|
+
/** Zero-based index of the turn within the session. */
|
|
65
|
+
turn_index?: number;
|
|
66
|
+
/** Start of the audio window this frame describes, in seconds. */
|
|
67
|
+
audio_window_start?: number;
|
|
68
|
+
/** End of the audio window this frame describes, in seconds. */
|
|
69
|
+
audio_window_end?: number;
|
|
70
|
+
/** Transcript for the turn so far. */
|
|
71
|
+
transcript?: string;
|
|
72
|
+
/** Flux's end-of-turn confidence in [0, 1]. */
|
|
73
|
+
end_of_turn_confidence?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Fatal error frame. Deepgram closes the session immediately after sending
|
|
78
|
+
* one, so it is the only diagnostic a caller ever gets for a provider-side
|
|
79
|
+
* failure.
|
|
80
|
+
*/
|
|
81
|
+
export interface FluxErrorFrame {
|
|
82
|
+
type: "Error";
|
|
83
|
+
sequence_id?: number;
|
|
84
|
+
/** Machine-readable identifier, e.g. `"INTERNAL_SERVER_ERROR"`. */
|
|
85
|
+
code?: string;
|
|
86
|
+
/** Prose explanation of the failure. */
|
|
87
|
+
description?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A payload that has been confirmed to be an object but whose frame kind is
|
|
92
|
+
* not yet known: the union of every field any documented frame can carry.
|
|
93
|
+
*/
|
|
94
|
+
type InboundFluxFrame = Partial<FluxTurnFrame> &
|
|
95
|
+
Partial<Omit<FluxErrorFrame, "type">>;
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Frame parsing
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Map one Flux frame onto zero or more daemon streaming events.
|
|
103
|
+
*
|
|
104
|
+
* Accepts either a JSON string (as delivered by the WebSocket) or an
|
|
105
|
+
* already-parsed value. Unknown frame kinds, malformed JSON, and non-object
|
|
106
|
+
* payloads yield an empty array and a debug log: Deepgram can add frame types
|
|
107
|
+
* without breaking the session, so this never throws and never rejects a
|
|
108
|
+
* stream over an unrecognized frame.
|
|
109
|
+
*
|
|
110
|
+
* Mapping:
|
|
111
|
+
* - `Connected` emits nothing (session bookkeeping).
|
|
112
|
+
* - `StartOfTurn` emits `turn-start`.
|
|
113
|
+
* - `Update` and bare `TurnInfo` emit `partial`: an interim transcript
|
|
114
|
+
* refresh, since the commit comes from `EndOfTurn`.
|
|
115
|
+
* - `EagerEndOfTurn` emits `eager-turn-end`, retracted by `TurnResumed` or
|
|
116
|
+
* confirmed by `EndOfTurn`.
|
|
117
|
+
* - `TurnResumed` emits `turn-resumed`.
|
|
118
|
+
* - `EndOfTurn` emits `final` followed by `turn-end`. The `final` comes first
|
|
119
|
+
* so a consumer that ignores turn events commits the transcript exactly as
|
|
120
|
+
* it does for a provider without turn detection.
|
|
121
|
+
* - `Error` emits `error`. This frame is fatal and is the only place the
|
|
122
|
+
* provider's own diagnostic appears, so it is surfaced rather than dropped.
|
|
123
|
+
*
|
|
124
|
+
* `turn-start` and `turn-end` carry the frame's `turn_index` when Deepgram
|
|
125
|
+
* sends one, which is what lets a consumer tell an end-of-turn for the turn
|
|
126
|
+
* still in progress from one Deepgram has already superseded.
|
|
127
|
+
*/
|
|
128
|
+
export function parseFluxFrame(raw: unknown): SttStreamServerEvent[] {
|
|
129
|
+
const frame = coerceFrame(raw);
|
|
130
|
+
if (!frame) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const kind = readFrameKind(frame);
|
|
135
|
+
const turnIndex = readNumber(frame.turn_index);
|
|
136
|
+
switch (kind) {
|
|
137
|
+
case "Connected":
|
|
138
|
+
return [];
|
|
139
|
+
case "StartOfTurn":
|
|
140
|
+
return [
|
|
141
|
+
{
|
|
142
|
+
type: "turn-start",
|
|
143
|
+
...(turnIndex !== undefined ? { turnIndex } : {}),
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
case "Update":
|
|
147
|
+
case "TurnInfo":
|
|
148
|
+
return [{ type: "partial", text: readTranscript(frame) }];
|
|
149
|
+
case "EagerEndOfTurn":
|
|
150
|
+
return [{ type: "eager-turn-end", text: readTranscript(frame) }];
|
|
151
|
+
case "TurnResumed":
|
|
152
|
+
return [{ type: "turn-resumed" }];
|
|
153
|
+
case "EndOfTurn": {
|
|
154
|
+
const text = readTranscript(frame);
|
|
155
|
+
const confidence = readNumber(frame.end_of_turn_confidence);
|
|
156
|
+
return [
|
|
157
|
+
{ type: "final", text },
|
|
158
|
+
{
|
|
159
|
+
type: "turn-end",
|
|
160
|
+
text,
|
|
161
|
+
...(confidence !== undefined ? { confidence } : {}),
|
|
162
|
+
...(turnIndex !== undefined ? { turnIndex } : {}),
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
case "Error":
|
|
167
|
+
return [readErrorEvent(frame)];
|
|
168
|
+
default:
|
|
169
|
+
log.debug({ kind }, "Ignoring unrecognized Deepgram Flux frame");
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Normalize an inbound payload into a frame object, or null when it is not
|
|
176
|
+
* one. JSON strings are parsed; anything that is not a plain object (null,
|
|
177
|
+
* arrays, primitives, unparseable text) is rejected.
|
|
178
|
+
*/
|
|
179
|
+
function coerceFrame(raw: unknown): InboundFluxFrame | null {
|
|
180
|
+
const value = typeof raw === "string" ? parseJsonSafe(raw) : raw;
|
|
181
|
+
if (!isPlainObject(value)) {
|
|
182
|
+
log.debug("Dropped a Deepgram Flux payload that is not a frame object");
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return value as InboundFluxFrame;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The turn state a frame describes: `event` when present, else `type`. */
|
|
189
|
+
function readFrameKind(frame: InboundFluxFrame): string | undefined {
|
|
190
|
+
if (typeof frame.event === "string") {
|
|
191
|
+
return frame.event;
|
|
192
|
+
}
|
|
193
|
+
return typeof frame.type === "string" ? frame.type : undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Trimmed transcript text, empty when the frame carries none. */
|
|
197
|
+
function readTranscript(frame: InboundFluxFrame): string {
|
|
198
|
+
return typeof frame.transcript === "string" ? frame.transcript.trim() : "";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Patterns that map a Flux error onto a normalized {@link SttErrorCategory},
|
|
203
|
+
* tried in order. Anything unmatched stays `provider-error`.
|
|
204
|
+
*
|
|
205
|
+
* Deepgram documents only `INTERNAL_SERVER_ERROR` for Flux itself, so these
|
|
206
|
+
* match the account-wide error vocabulary
|
|
207
|
+
* (https://developers.deepgram.com/docs/errors) and mirror how
|
|
208
|
+
* `DeepgramRealtimeTranscriber` separates auth and rate-limit failures from
|
|
209
|
+
* everything else.
|
|
210
|
+
*
|
|
211
|
+
* The timeout pattern deliberately requires a `_TIMEOUT` suffix at a word
|
|
212
|
+
* boundary so a configuration complaint about `eot_timeout_ms` is not
|
|
213
|
+
* mistaken for a transport timeout.
|
|
214
|
+
*/
|
|
215
|
+
const FLUX_ERROR_CATEGORY_PATTERNS: ReadonlyArray<
|
|
216
|
+
readonly [SttErrorCategory, RegExp]
|
|
217
|
+
> = [
|
|
218
|
+
[
|
|
219
|
+
"auth",
|
|
220
|
+
/INVALID_AUTH|INSUFFICIENT_PERMISSIONS|UNAUTHORIZED|FORBIDDEN|INVALID CREDENTIALS|API KEY/,
|
|
221
|
+
],
|
|
222
|
+
["rate-limit", /TOO_MANY_REQUESTS|RATE[_ ]LIMIT|QUOTA/],
|
|
223
|
+
["timeout", /^TIMEOUT|_TIMEOUT\b|TIMED[_ ]OUT/],
|
|
224
|
+
[
|
|
225
|
+
"invalid-audio",
|
|
226
|
+
/UNPROCESSABLE|CORRUPT|UNSUPPORTED|INVALID[_ ]AUDIO|BAD[_ ]AUDIO/,
|
|
227
|
+
],
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Turn a fatal `Error` frame into a stream `error` event, preserving the
|
|
232
|
+
* provider's own `code` and `description` so the diagnostic survives all the
|
|
233
|
+
* way to the caller. Deepgram closes the socket right after this frame, so
|
|
234
|
+
* dropping it would leave a bare close as the only signal.
|
|
235
|
+
*/
|
|
236
|
+
function readErrorEvent(frame: InboundFluxFrame): SttStreamServerErrorEvent {
|
|
237
|
+
const code = typeof frame.code === "string" ? frame.code.trim() : "";
|
|
238
|
+
const description =
|
|
239
|
+
typeof frame.description === "string" ? frame.description.trim() : "";
|
|
240
|
+
|
|
241
|
+
const haystack = `${code} ${description}`.toUpperCase();
|
|
242
|
+
const category =
|
|
243
|
+
FLUX_ERROR_CATEGORY_PATTERNS.find(([, pattern]) =>
|
|
244
|
+
pattern.test(haystack),
|
|
245
|
+
)?.[0] ?? "provider-error";
|
|
246
|
+
|
|
247
|
+
const detail = description || "no description provided";
|
|
248
|
+
const message = code
|
|
249
|
+
? `Deepgram Flux error (${code}): ${detail}`
|
|
250
|
+
: `Deepgram Flux error: ${detail}`;
|
|
251
|
+
|
|
252
|
+
log.warn({ code, category }, "Deepgram Flux sent a fatal error frame");
|
|
253
|
+
return { type: "error", category, message };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** A finite number, or undefined for anything else. */
|
|
257
|
+
function readNumber(value: unknown): number | undefined {
|
|
258
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
259
|
+
? value
|
|
260
|
+
: undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Query parameters
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Raw audio encodings Flux accepts. Containerized audio (WAV, Ogg) is
|
|
269
|
+
* self-describing and needs neither `encoding` nor `sample_rate`.
|
|
270
|
+
*/
|
|
271
|
+
export type FluxEncoding =
|
|
272
|
+
| "linear16"
|
|
273
|
+
| "linear32"
|
|
274
|
+
| "mulaw"
|
|
275
|
+
| "alaw"
|
|
276
|
+
| "opus"
|
|
277
|
+
| "ogg-opus";
|
|
278
|
+
|
|
279
|
+
/** Inclusive bounds Deepgram enforces on `eot_threshold`. */
|
|
280
|
+
const EOT_THRESHOLD_RANGE = { min: 0.5, max: 0.9 } as const;
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Deepgram's server-side default for `eot_threshold`, applied when the
|
|
284
|
+
* parameter is omitted. It is the ceiling `eager_eot_threshold` must respect
|
|
285
|
+
* in that case, since the validation runs against the threshold actually in
|
|
286
|
+
* force rather than the one we sent.
|
|
287
|
+
*/
|
|
288
|
+
const DEFAULT_EOT_THRESHOLD = 0.7;
|
|
289
|
+
|
|
290
|
+
/** Inclusive bounds Deepgram enforces on `eager_eot_threshold`. */
|
|
291
|
+
const EAGER_EOT_THRESHOLD_RANGE = { min: 0.3, max: 0.9 } as const;
|
|
292
|
+
|
|
293
|
+
/** Inclusive bounds Deepgram enforces on `eot_timeout_ms`. */
|
|
294
|
+
const EOT_TIMEOUT_MS_RANGE = { min: 500, max: 60_000 } as const;
|
|
295
|
+
|
|
296
|
+
export interface FluxQueryParamOptions {
|
|
297
|
+
/** Flux model to run, e.g. `"flux-general-en"`. Required by Deepgram. */
|
|
298
|
+
model: string;
|
|
299
|
+
/**
|
|
300
|
+
* Encoding of the raw audio being sent. Omit for containerized audio,
|
|
301
|
+
* which carries its own format header.
|
|
302
|
+
*/
|
|
303
|
+
encoding?: FluxEncoding;
|
|
304
|
+
/**
|
|
305
|
+
* Sample rate (Hz) of the raw audio. Required whenever {@link encoding} is
|
|
306
|
+
* set. Flux accepts 8000, 16000, 24000, 44100, and 48000.
|
|
307
|
+
*/
|
|
308
|
+
sampleRate?: number;
|
|
309
|
+
/** End-of-turn confidence Flux must reach before committing a turn. */
|
|
310
|
+
eotThreshold?: number;
|
|
311
|
+
/**
|
|
312
|
+
* Confidence at which Flux starts speculating a turn has ended. Leaving it
|
|
313
|
+
* undefined omits the parameter, which is what keeps Deepgram from emitting
|
|
314
|
+
* `EagerEndOfTurn` / `TurnResumed` frames at all.
|
|
315
|
+
*/
|
|
316
|
+
eagerEotThreshold?: number;
|
|
317
|
+
/** Silence (ms) after which Flux force-ends a turn. */
|
|
318
|
+
eotTimeoutMs?: number;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Build the query string for Flux's `/v2/listen` endpoint (no leading `?`).
|
|
323
|
+
*
|
|
324
|
+
* Every optional parameter is omitted when undefined rather than sent with a
|
|
325
|
+
* default, because Deepgram's own defaults are the intended behavior and
|
|
326
|
+
* omitting `eager_eot_threshold` is what disables eager turn-end speculation.
|
|
327
|
+
* Thresholds are clamped to the ranges Deepgram accepts so an out-of-range
|
|
328
|
+
* value degrades to the nearest legal one instead of failing the handshake.
|
|
329
|
+
*
|
|
330
|
+
* Deepgram additionally rejects a request whose `eager_eot_threshold` exceeds
|
|
331
|
+
* its `eot_threshold`
|
|
332
|
+
* (https://developers.deepgram.com/docs/flux/configuration#validation-rules),
|
|
333
|
+
* so after each threshold is clamped to its own range the eager value is
|
|
334
|
+
* clamped down again to the EOT threshold in force (the one being sent, or
|
|
335
|
+
* Deepgram's {@link DEFAULT_EOT_THRESHOLD} when none is). Clamping keeps a
|
|
336
|
+
* misconfigured pair from failing the whole session; the adjustment is logged
|
|
337
|
+
* at debug when it fires.
|
|
338
|
+
*/
|
|
339
|
+
export function buildFluxQueryParams(opts: FluxQueryParamOptions): string {
|
|
340
|
+
const params = new URLSearchParams();
|
|
341
|
+
params.set("model", opts.model);
|
|
342
|
+
|
|
343
|
+
if (opts.encoding !== undefined) {
|
|
344
|
+
params.set("encoding", opts.encoding);
|
|
345
|
+
}
|
|
346
|
+
const sampleRate = readNumber(opts.sampleRate);
|
|
347
|
+
if (sampleRate !== undefined) {
|
|
348
|
+
params.set("sample_rate", String(Math.round(sampleRate)));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const eotThreshold = clamp(opts.eotThreshold, EOT_THRESHOLD_RANGE);
|
|
352
|
+
if (eotThreshold !== undefined) {
|
|
353
|
+
params.set("eot_threshold", String(eotThreshold));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let eagerEotThreshold = clamp(
|
|
357
|
+
opts.eagerEotThreshold,
|
|
358
|
+
EAGER_EOT_THRESHOLD_RANGE,
|
|
359
|
+
);
|
|
360
|
+
const eagerCeiling = eotThreshold ?? DEFAULT_EOT_THRESHOLD;
|
|
361
|
+
if (eagerEotThreshold !== undefined && eagerEotThreshold > eagerCeiling) {
|
|
362
|
+
log.debug(
|
|
363
|
+
{
|
|
364
|
+
requested: opts.eagerEotThreshold,
|
|
365
|
+
clampedTo: eagerCeiling,
|
|
366
|
+
eotThreshold: eagerCeiling,
|
|
367
|
+
usingDefaultEotThreshold: eotThreshold === undefined,
|
|
368
|
+
},
|
|
369
|
+
"Clamped Deepgram Flux eager_eot_threshold down to eot_threshold",
|
|
370
|
+
);
|
|
371
|
+
eagerEotThreshold = eagerCeiling;
|
|
372
|
+
}
|
|
373
|
+
if (eagerEotThreshold !== undefined) {
|
|
374
|
+
params.set("eager_eot_threshold", String(eagerEotThreshold));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const eotTimeoutMs = clamp(opts.eotTimeoutMs, EOT_TIMEOUT_MS_RANGE);
|
|
378
|
+
if (eotTimeoutMs !== undefined) {
|
|
379
|
+
params.set("eot_timeout_ms", String(Math.round(eotTimeoutMs)));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return params.toString();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Clamp a finite number into an inclusive range; undefined passes through. */
|
|
386
|
+
function clamp(
|
|
387
|
+
value: number | undefined,
|
|
388
|
+
range: { min: number; max: number },
|
|
389
|
+
): number | undefined {
|
|
390
|
+
const numeric = readNumber(value);
|
|
391
|
+
if (numeric === undefined) {
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
return Math.min(Math.max(numeric, range.min), range.max);
|
|
395
|
+
}
|