@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
|
@@ -126,6 +126,12 @@ export const VALID_CONNECTION_PROVIDERS: readonly string[] = [
|
|
|
126
126
|
// persisted `vellum` rows — the routing threaded in via `providerOverride`
|
|
127
127
|
// never runs on a row that fails to load.
|
|
128
128
|
VELLUM_MANAGED_PROVIDER,
|
|
129
|
+
// The ChatGPT-subscription row stores the "chatgpt" routing identity in its
|
|
130
|
+
// `provider` column for the same reason: the row IS the subscription route
|
|
131
|
+
// (auth modality = provider identity), and dispatch derives the openai
|
|
132
|
+
// upstream per-request. Allowlisted explicitly or the DB loaders would drop
|
|
133
|
+
// the persisted row.
|
|
134
|
+
"chatgpt",
|
|
129
135
|
];
|
|
130
136
|
|
|
131
137
|
export type ConnectionProvider = string;
|
|
@@ -23,6 +23,10 @@ import {
|
|
|
23
23
|
describeSubscriptionModelIncompatibility,
|
|
24
24
|
isConnectionCompatibleWithModel,
|
|
25
25
|
} from "../connection-model-compat.js";
|
|
26
|
+
import {
|
|
27
|
+
connectionProviderKind,
|
|
28
|
+
resolveEntryConnectionName,
|
|
29
|
+
} from "../connection-resolution.js";
|
|
26
30
|
import { PROVIDER_CATALOG } from "../model-catalog.js";
|
|
27
31
|
import { resolveManagedProxyContext } from "../platform-proxy/context.js";
|
|
28
32
|
import {
|
|
@@ -161,7 +165,12 @@ export async function computeConnectionAvailability(
|
|
|
161
165
|
if (resolvedConnectionName === VELLUM_MANAGED_CONNECTION_NAME) {
|
|
162
166
|
return vellumConnectionAvailability();
|
|
163
167
|
}
|
|
164
|
-
|
|
168
|
+
// The ChatGPT-subscription row stores the "chatgpt" identity while its
|
|
169
|
+
// upstream is openai; the credential switch below is the right check for
|
|
170
|
+
// it (the subscription token is its credential).
|
|
171
|
+
const isChatgptRow =
|
|
172
|
+
connection.provider === "chatgpt" && provider === "openai";
|
|
173
|
+
if (!isChatgptRow && connection.provider !== provider) {
|
|
165
174
|
return {
|
|
166
175
|
status: "provider_mismatch",
|
|
167
176
|
message: `Connection "${resolvedConnectionName}" is for provider "${connection.provider}", but the requested provider is "${provider}". Pick a connection for "${provider}" ${SETTINGS_HINT}.`,
|
|
@@ -326,9 +335,22 @@ export async function computeProfileAvailability(
|
|
|
326
335
|
}
|
|
327
336
|
}
|
|
328
337
|
|
|
338
|
+
// Precedence matches dispatch: an explicit provider_connection wins over
|
|
339
|
+
// an entry-name provider, and the vendor judged is the entry's
|
|
340
|
+
// dispatchable kind either way (the same translation dispatch uses).
|
|
341
|
+
const entryName = resolveEntryConnectionName(provider);
|
|
342
|
+
const entryKind =
|
|
343
|
+
entryName !== null
|
|
344
|
+
? (connectionProviderKind(entryName, model) ?? provider)
|
|
345
|
+
: provider;
|
|
346
|
+
|
|
329
347
|
const pinned = entry.provider_connection;
|
|
330
348
|
if (typeof pinned === "string") {
|
|
331
|
-
return computeConnectionAvailability(
|
|
349
|
+
return computeConnectionAvailability(entryKind, pinned);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (entryName !== null) {
|
|
353
|
+
return computeConnectionAvailability(entryKind, entryName);
|
|
332
354
|
}
|
|
333
355
|
|
|
334
356
|
// Mirror the dispatch-time auto-resolve (`resolveConfiguredProvider`): with
|
|
@@ -348,10 +348,12 @@ export function updateConnection(
|
|
|
348
348
|
connection: {
|
|
349
349
|
...existing,
|
|
350
350
|
auth,
|
|
351
|
+
provider: nextProvider,
|
|
351
352
|
label: input.label !== undefined ? input.label : existing.label,
|
|
352
353
|
baseUrl: nextBaseUrl,
|
|
353
354
|
models: nextModels,
|
|
354
355
|
updatedAt: now,
|
|
356
|
+
isManaged: isManagedRow({ name, provider: nextProvider, auth }),
|
|
355
357
|
},
|
|
356
358
|
};
|
|
357
359
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isModelInCatalog, PROVIDER_CATALOG } from "./model-catalog.js";
|
|
2
|
+
import { isCodexSubscriptionModel } from "./openai/codex-models.js";
|
|
2
3
|
import type { ModelIntent } from "./types.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -10,8 +11,10 @@ const PROVIDER_DEFAULT_MODELS: Record<string, string> = Object.fromEntries(
|
|
|
10
11
|
);
|
|
11
12
|
|
|
12
13
|
// `cost-optimized` is the cheapest model a provider serves, `latency-optimized`
|
|
13
|
-
// the fastest to first token. On anthropic, ollama and
|
|
14
|
-
// is both, so those columns repeat by construction rather than by
|
|
14
|
+
// the fastest to first token. On anthropic, ollama, fireworks and openai the
|
|
15
|
+
// same model is both, so those columns repeat by construction rather than by
|
|
16
|
+
// oversight (openai's balanced repeats it too: the default-profile templates
|
|
17
|
+
// split those tiers by reasoning effort, not model).
|
|
15
18
|
const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
|
|
16
19
|
anthropic: {
|
|
17
20
|
balanced: "claude-sonnet-4-6",
|
|
@@ -21,11 +24,11 @@ const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
|
|
|
21
24
|
"vision-optimized": "claude-opus-4-6",
|
|
22
25
|
},
|
|
23
26
|
openai: {
|
|
24
|
-
balanced: "gpt-5.
|
|
25
|
-
"cost-optimized": "gpt-5.
|
|
27
|
+
balanced: "gpt-5.6-luna",
|
|
28
|
+
"cost-optimized": "gpt-5.6-luna",
|
|
26
29
|
"latency-optimized": "gpt-5.6-luna",
|
|
27
|
-
"quality-optimized": "gpt-5.
|
|
28
|
-
"vision-optimized": "gpt-5.
|
|
30
|
+
"quality-optimized": "gpt-5.6-sol",
|
|
31
|
+
"vision-optimized": "gpt-5.6-terra",
|
|
29
32
|
},
|
|
30
33
|
gemini: {
|
|
31
34
|
balanced: "gemini-3-flash-preview",
|
|
@@ -89,6 +92,23 @@ for (const [provider, intents] of Object.entries(PROVIDER_MODEL_INTENTS)) {
|
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
// The openai column must additionally stay inside the ChatGPT Codex
|
|
96
|
+
// subscription set: the subscription default provider is stored as
|
|
97
|
+
// `llm.defaultProvider = { provider: "openai", connectionName:
|
|
98
|
+
// "chatgpt-subscription" }`, so the materialized default profiles resolve
|
|
99
|
+
// through this column while pinning the subscription connection, which
|
|
100
|
+
// bypasses the auto-resolution compat gate and hard-routes to the Codex
|
|
101
|
+
// endpoint, where a non-Codex model 400s on every request.
|
|
102
|
+
for (const [intent, modelId] of Object.entries(PROVIDER_MODEL_INTENTS.openai)) {
|
|
103
|
+
if (!isCodexSubscriptionModel(modelId)) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`PROVIDER_MODEL_INTENTS[openai][${intent}] references model "${modelId}" ` +
|
|
106
|
+
`which the ChatGPT subscription cannot serve. Pick a model from ` +
|
|
107
|
+
`CODEX_SUBSCRIPTION_MODEL_IDS in openai/codex-models.ts.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
export function isModelIntent(value: unknown): value is ModelIntent {
|
|
93
113
|
return typeof value === "string" && MODEL_INTENTS.has(value as ModelIntent);
|
|
94
114
|
}
|
|
@@ -13,9 +13,10 @@ export const CODEX_SUBSCRIPTION_MODEL_IDS: ReadonlySet<string> = new Set([
|
|
|
13
13
|
"gpt-5.6-terra",
|
|
14
14
|
"gpt-5.6-luna",
|
|
15
15
|
"gpt-5.5",
|
|
16
|
+
// OpenAI retires these two from ChatGPT sign-in on 2026-08-31; API-key
|
|
17
|
+
// auth is unaffected.
|
|
16
18
|
"gpt-5.4",
|
|
17
19
|
"gpt-5.4-mini",
|
|
18
|
-
"gpt-5.3-codex",
|
|
19
20
|
]);
|
|
20
21
|
|
|
21
22
|
/** True when `model` is allowlisted for Codex subscription routing. */
|
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
isConnectionCompatibleWithModel,
|
|
18
18
|
} from "./connection-model-compat.js";
|
|
19
19
|
import {
|
|
20
|
+
connectionProviderKind,
|
|
21
|
+
dispatchProviderResolvable,
|
|
22
|
+
expectedVendorProvider,
|
|
23
|
+
resolveEntryConnectionName,
|
|
20
24
|
resolveRoutingIdentity,
|
|
21
25
|
tryResolveProviderForConnectionName,
|
|
22
26
|
} from "./connection-resolution.js";
|
|
@@ -37,6 +41,13 @@ const log = getLogger("provider-send-message");
|
|
|
37
41
|
export interface ConfiguredProviderResult {
|
|
38
42
|
provider: Provider;
|
|
39
43
|
configuredProviderName: string;
|
|
44
|
+
/**
|
|
45
|
+
* True when the profile's provider was an entry label and dispatch routed
|
|
46
|
+
* through that row. The row's own credential already signed resolution, so
|
|
47
|
+
* vendor-keyed preflights (which check the generic per-vendor key slot)
|
|
48
|
+
* do not apply to this result.
|
|
49
|
+
*/
|
|
50
|
+
entryRouted: boolean;
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
export type ConfiguredProviderOptions = Pick<
|
|
@@ -140,7 +151,10 @@ export async function resolveConfiguredProvider(
|
|
|
140
151
|
}
|
|
141
152
|
}
|
|
142
153
|
|
|
143
|
-
const resolved = resolveCallSiteConfig(callSite, config.llm,
|
|
154
|
+
const resolved = resolveCallSiteConfig(callSite, config.llm, {
|
|
155
|
+
...opts,
|
|
156
|
+
isResolvableProvider: dispatchProviderResolvable,
|
|
157
|
+
});
|
|
144
158
|
const inferenceProvider = resolved.provider;
|
|
145
159
|
let connectionName = resolved.provider_connection;
|
|
146
160
|
|
|
@@ -158,6 +172,14 @@ export async function resolveConfiguredProvider(
|
|
|
158
172
|
resolved.model,
|
|
159
173
|
)?.connectionName;
|
|
160
174
|
}
|
|
175
|
+
// An entry-name provider IS the connection name: the label points at a
|
|
176
|
+
// row, and the row's own provider drives dispatch.
|
|
177
|
+
const entryRoute = connectionName
|
|
178
|
+
? null
|
|
179
|
+
: resolveEntryConnectionName(inferenceProvider);
|
|
180
|
+
if (entryRoute) {
|
|
181
|
+
connectionName = entryRoute;
|
|
182
|
+
}
|
|
161
183
|
if (!connectionName) {
|
|
162
184
|
if (inferenceProvider) {
|
|
163
185
|
try {
|
|
@@ -202,7 +224,7 @@ export async function resolveConfiguredProvider(
|
|
|
202
224
|
const connectionProvider = await tryResolveProviderForConnectionName(
|
|
203
225
|
connectionName,
|
|
204
226
|
config,
|
|
205
|
-
inferenceProvider,
|
|
227
|
+
expectedVendorProvider(inferenceProvider, resolved.model),
|
|
206
228
|
resolved.model,
|
|
207
229
|
);
|
|
208
230
|
if (!connectionProvider) {
|
|
@@ -234,7 +256,14 @@ export async function resolveConfiguredProvider(
|
|
|
234
256
|
opts.overrideProfile,
|
|
235
257
|
opts.forceOverrideProfile,
|
|
236
258
|
),
|
|
237
|
-
|
|
259
|
+
// The dispatched vendor kind, not the config label: an entry-name
|
|
260
|
+
// provider reports its row's kind so provider-keyed consumers
|
|
261
|
+
// (telemetry, pricing) never see a label that matches no catalog id.
|
|
262
|
+
configuredProviderName:
|
|
263
|
+
(entryRoute
|
|
264
|
+
? connectionProviderKind(entryRoute, resolved.model)
|
|
265
|
+
: undefined) ?? inferenceProvider,
|
|
266
|
+
entryRouted: entryRoute !== null,
|
|
238
267
|
};
|
|
239
268
|
}
|
|
240
269
|
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildFluxQueryParams,
|
|
5
|
+
parseFluxFrame,
|
|
6
|
+
} from "../deepgram-flux-frames.js";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Fixtures: one frame per documented Flux type
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const CONNECTED = {
|
|
13
|
+
type: "Connected",
|
|
14
|
+
request_id: "req-123",
|
|
15
|
+
sequence_id: 0,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const START_OF_TURN = {
|
|
19
|
+
type: "TurnInfo",
|
|
20
|
+
event: "StartOfTurn",
|
|
21
|
+
request_id: "req-123",
|
|
22
|
+
turn_index: 0,
|
|
23
|
+
audio_window_start: 0,
|
|
24
|
+
audio_window_end: 0.32,
|
|
25
|
+
transcript: "",
|
|
26
|
+
words: [],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const UPDATE = {
|
|
30
|
+
type: "TurnInfo",
|
|
31
|
+
event: "Update",
|
|
32
|
+
turn_index: 0,
|
|
33
|
+
transcript: "what is the",
|
|
34
|
+
words: [
|
|
35
|
+
{ word: "what", confidence: 0.99, start: 0.1, end: 0.28 },
|
|
36
|
+
{ word: "is", confidence: 0.98, start: 0.28, end: 0.4 },
|
|
37
|
+
{ word: "the", confidence: 0.97, start: 0.4, end: 0.55 },
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const EAGER_END_OF_TURN = {
|
|
42
|
+
type: "TurnInfo",
|
|
43
|
+
event: "EagerEndOfTurn",
|
|
44
|
+
turn_index: 0,
|
|
45
|
+
transcript: "what is the weather",
|
|
46
|
+
end_of_turn_confidence: 0.42,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const TURN_RESUMED = {
|
|
50
|
+
type: "TurnInfo",
|
|
51
|
+
event: "TurnResumed",
|
|
52
|
+
turn_index: 0,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const END_OF_TURN = {
|
|
56
|
+
type: "TurnInfo",
|
|
57
|
+
event: "EndOfTurn",
|
|
58
|
+
turn_index: 0,
|
|
59
|
+
transcript: "what is the weather today",
|
|
60
|
+
end_of_turn_confidence: 0.88,
|
|
61
|
+
words: [{ word: "today", confidence: 0.95, start: 1.1, end: 1.4 }],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const FATAL_ERROR = {
|
|
65
|
+
type: "Error",
|
|
66
|
+
sequence_id: 5,
|
|
67
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
68
|
+
description: "An internal server error occurred while processing the request",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// parseFluxFrame
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
describe("parseFluxFrame", () => {
|
|
76
|
+
test("Connected emits no events", () => {
|
|
77
|
+
expect(parseFluxFrame(CONNECTED)).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("StartOfTurn emits turn-start carrying the turn index", () => {
|
|
81
|
+
expect(parseFluxFrame(START_OF_TURN)).toEqual([
|
|
82
|
+
{ type: "turn-start", turnIndex: 0 },
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("Update emits a partial with the interim transcript", () => {
|
|
87
|
+
expect(parseFluxFrame(UPDATE)).toEqual([
|
|
88
|
+
{ type: "partial", text: "what is the" },
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("a bare TurnInfo frame emits a partial transcript refresh", () => {
|
|
93
|
+
expect(
|
|
94
|
+
parseFluxFrame({ type: "TurnInfo", transcript: "partial text" }),
|
|
95
|
+
).toEqual([{ type: "partial", text: "partial text" }]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("EagerEndOfTurn emits eager-turn-end with the speculated text", () => {
|
|
99
|
+
expect(parseFluxFrame(EAGER_END_OF_TURN)).toEqual([
|
|
100
|
+
{ type: "eager-turn-end", text: "what is the weather" },
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("TurnResumed emits turn-resumed", () => {
|
|
105
|
+
expect(parseFluxFrame(TURN_RESUMED)).toEqual([{ type: "turn-resumed" }]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("EndOfTurn emits final before turn-end", () => {
|
|
109
|
+
const events = parseFluxFrame(END_OF_TURN);
|
|
110
|
+
expect(events).toEqual([
|
|
111
|
+
{ type: "final", text: "what is the weather today" },
|
|
112
|
+
{
|
|
113
|
+
type: "turn-end",
|
|
114
|
+
text: "what is the weather today",
|
|
115
|
+
confidence: 0.88,
|
|
116
|
+
turnIndex: 0,
|
|
117
|
+
},
|
|
118
|
+
]);
|
|
119
|
+
// Consumers that ignore turn events must still see the commit first.
|
|
120
|
+
expect(events[0]?.type).toBe("final");
|
|
121
|
+
expect(events[1]?.type).toBe("turn-end");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("EndOfTurn omits confidence when Deepgram sends none", () => {
|
|
125
|
+
expect(parseFluxFrame({ type: "EndOfTurn", transcript: "done" })).toEqual([
|
|
126
|
+
{ type: "final", text: "done" },
|
|
127
|
+
{ type: "turn-end", text: "done" },
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("flat frames without a nested event field are accepted", () => {
|
|
132
|
+
expect(parseFluxFrame({ type: "StartOfTurn" })).toEqual([
|
|
133
|
+
{ type: "turn-start" },
|
|
134
|
+
]);
|
|
135
|
+
expect(parseFluxFrame({ type: "Update", transcript: "hi" })).toEqual([
|
|
136
|
+
{ type: "partial", text: "hi" },
|
|
137
|
+
]);
|
|
138
|
+
expect(parseFluxFrame({ type: "TurnResumed" })).toEqual([
|
|
139
|
+
{ type: "turn-resumed" },
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("JSON string payloads are parsed", () => {
|
|
144
|
+
expect(parseFluxFrame(JSON.stringify(END_OF_TURN))).toEqual([
|
|
145
|
+
{ type: "final", text: "what is the weather today" },
|
|
146
|
+
{
|
|
147
|
+
type: "turn-end",
|
|
148
|
+
text: "what is the weather today",
|
|
149
|
+
confidence: 0.88,
|
|
150
|
+
turnIndex: 0,
|
|
151
|
+
},
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("a later turn keeps its own index on both turn events", () => {
|
|
156
|
+
expect(
|
|
157
|
+
parseFluxFrame({ ...START_OF_TURN, turn_index: 3, transcript: "" }),
|
|
158
|
+
).toEqual([{ type: "turn-start", turnIndex: 3 }]);
|
|
159
|
+
expect(
|
|
160
|
+
parseFluxFrame({
|
|
161
|
+
type: "TurnInfo",
|
|
162
|
+
event: "EndOfTurn",
|
|
163
|
+
turn_index: 3,
|
|
164
|
+
transcript: "and then some",
|
|
165
|
+
}),
|
|
166
|
+
).toEqual([
|
|
167
|
+
{ type: "final", text: "and then some" },
|
|
168
|
+
{ type: "turn-end", text: "and then some", turnIndex: 3 },
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("turn events omit the index when Deepgram sends none", () => {
|
|
173
|
+
expect(parseFluxFrame({ type: "StartOfTurn" })).toEqual([
|
|
174
|
+
{ type: "turn-start" },
|
|
175
|
+
]);
|
|
176
|
+
expect(parseFluxFrame({ type: "EndOfTurn", transcript: "done" })).toEqual([
|
|
177
|
+
{ type: "final", text: "done" },
|
|
178
|
+
{ type: "turn-end", text: "done" },
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("transcripts are trimmed and missing ones become empty strings", () => {
|
|
183
|
+
expect(
|
|
184
|
+
parseFluxFrame({ type: "Update", transcript: " spaced " }),
|
|
185
|
+
).toEqual([{ type: "partial", text: "spaced" }]);
|
|
186
|
+
expect(parseFluxFrame({ type: "Update" })).toEqual([
|
|
187
|
+
{ type: "partial", text: "" },
|
|
188
|
+
]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("a fatal Error frame emits exactly one error event", () => {
|
|
192
|
+
const events = parseFluxFrame(FATAL_ERROR);
|
|
193
|
+
expect(events).toHaveLength(1);
|
|
194
|
+
expect(events[0]).toEqual({
|
|
195
|
+
type: "error",
|
|
196
|
+
category: "provider-error",
|
|
197
|
+
message:
|
|
198
|
+
"Deepgram Flux error (INTERNAL_SERVER_ERROR): An internal server error occurred while processing the request",
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("an Error frame preserves the provider description in the message", () => {
|
|
203
|
+
const [event] = parseFluxFrame(
|
|
204
|
+
JSON.stringify({
|
|
205
|
+
type: "Error",
|
|
206
|
+
code: "SOME_FUTURE_CODE",
|
|
207
|
+
description: "something specific went wrong upstream",
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
expect(event?.type).toBe("error");
|
|
211
|
+
expect((event as { message: string }).message).toContain(
|
|
212
|
+
"something specific went wrong upstream",
|
|
213
|
+
);
|
|
214
|
+
expect((event as { message: string }).message).toContain(
|
|
215
|
+
"SOME_FUTURE_CODE",
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("Error frames are categorized from the provider code", () => {
|
|
220
|
+
const categoryOf = (frame: Record<string, unknown>) => {
|
|
221
|
+
const [event] = parseFluxFrame({ type: "Error", ...frame });
|
|
222
|
+
return (event as { category?: string } | undefined)?.category;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
expect(categoryOf({ code: "INVALID_AUTH" })).toBe("auth");
|
|
226
|
+
expect(categoryOf({ code: "INSUFFICIENT_PERMISSIONS" })).toBe("auth");
|
|
227
|
+
expect(categoryOf({ code: "TOO_MANY_REQUESTS" })).toBe("rate-limit");
|
|
228
|
+
expect(categoryOf({ code: "REQUEST_TIMEOUT" })).toBe("timeout");
|
|
229
|
+
expect(categoryOf({ code: "ASR_UNPROCESSABLE_ENTITY" })).toBe(
|
|
230
|
+
"invalid-audio",
|
|
231
|
+
);
|
|
232
|
+
expect(categoryOf({ code: "INTERNAL_SERVER_ERROR" })).toBe(
|
|
233
|
+
"provider-error",
|
|
234
|
+
);
|
|
235
|
+
// Unrecognized and bare error frames still surface, as provider-error.
|
|
236
|
+
expect(categoryOf({ code: "SOMETHING_NEW" })).toBe("provider-error");
|
|
237
|
+
expect(categoryOf({})).toBe("provider-error");
|
|
238
|
+
// A complaint about eot_timeout_ms is a config error, not a transport
|
|
239
|
+
// timeout, so the timeout matcher must not claim it.
|
|
240
|
+
expect(
|
|
241
|
+
categoryOf({
|
|
242
|
+
code: "INVALID_EOT_TIMEOUT_MS",
|
|
243
|
+
description: "eot_timeout_ms is out of range",
|
|
244
|
+
}),
|
|
245
|
+
).toBe("provider-error");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("an Error frame with no description still yields a usable message", () => {
|
|
249
|
+
expect(parseFluxFrame({ type: "Error" })).toEqual([
|
|
250
|
+
{
|
|
251
|
+
type: "error",
|
|
252
|
+
category: "provider-error",
|
|
253
|
+
message: "Deepgram Flux error: no description provided",
|
|
254
|
+
},
|
|
255
|
+
]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("unknown frame types emit nothing", () => {
|
|
259
|
+
expect(parseFluxFrame({ type: "SomeFutureFrame", data: 1 })).toEqual([]);
|
|
260
|
+
expect(
|
|
261
|
+
parseFluxFrame({ type: "TurnInfo", event: "FutureTurnState" }),
|
|
262
|
+
).toEqual([]);
|
|
263
|
+
// Only the documented fatal `Error` frame becomes an error event: an
|
|
264
|
+
// unrecognized frame must never be reported as a stream failure, even
|
|
265
|
+
// when it carries error-shaped fields.
|
|
266
|
+
expect(
|
|
267
|
+
parseFluxFrame({ type: "ConfigureFailure", code: "INVALID_AUTH" }),
|
|
268
|
+
).toEqual([]);
|
|
269
|
+
expect(parseFluxFrame({ type: "error", description: "lowercase" })).toEqual(
|
|
270
|
+
[],
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("malformed and arbitrary input never throws and emits nothing", () => {
|
|
275
|
+
const garbage: unknown[] = [
|
|
276
|
+
undefined,
|
|
277
|
+
null,
|
|
278
|
+
0,
|
|
279
|
+
1,
|
|
280
|
+
Number.NaN,
|
|
281
|
+
true,
|
|
282
|
+
"",
|
|
283
|
+
"not json",
|
|
284
|
+
"{ broken json",
|
|
285
|
+
"[]",
|
|
286
|
+
"null",
|
|
287
|
+
"42",
|
|
288
|
+
[],
|
|
289
|
+
[{ type: "EndOfTurn" }],
|
|
290
|
+
{},
|
|
291
|
+
{ type: 7 },
|
|
292
|
+
{ type: null },
|
|
293
|
+
{ event: 7, type: 7 },
|
|
294
|
+
new Date(),
|
|
295
|
+
() => undefined,
|
|
296
|
+
Symbol("x"),
|
|
297
|
+
10n,
|
|
298
|
+
];
|
|
299
|
+
for (const value of garbage) {
|
|
300
|
+
expect(() => parseFluxFrame(value)).not.toThrow();
|
|
301
|
+
expect(parseFluxFrame(value)).toEqual([]);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// buildFluxQueryParams
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
describe("buildFluxQueryParams", () => {
|
|
311
|
+
test("builds the raw-audio parameter set", () => {
|
|
312
|
+
const query = buildFluxQueryParams({
|
|
313
|
+
model: "flux-general-en",
|
|
314
|
+
encoding: "linear16",
|
|
315
|
+
sampleRate: 16_000,
|
|
316
|
+
eotThreshold: 0.7,
|
|
317
|
+
eotTimeoutMs: 5_000,
|
|
318
|
+
});
|
|
319
|
+
const params = new URLSearchParams(query);
|
|
320
|
+
expect(params.get("model")).toBe("flux-general-en");
|
|
321
|
+
expect(params.get("encoding")).toBe("linear16");
|
|
322
|
+
expect(params.get("sample_rate")).toBe("16000");
|
|
323
|
+
expect(params.get("eot_threshold")).toBe("0.7");
|
|
324
|
+
expect(params.get("eot_timeout_ms")).toBe("5000");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("omits eager_eot_threshold when unset", () => {
|
|
328
|
+
const query = buildFluxQueryParams({
|
|
329
|
+
model: "flux-general-en",
|
|
330
|
+
eotThreshold: 0.7,
|
|
331
|
+
eotTimeoutMs: 5_000,
|
|
332
|
+
});
|
|
333
|
+
expect(query).not.toContain("eager_eot_threshold");
|
|
334
|
+
expect(new URLSearchParams(query).has("eager_eot_threshold")).toBe(false);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("passes eager_eot_threshold through when set", () => {
|
|
338
|
+
const params = new URLSearchParams(
|
|
339
|
+
buildFluxQueryParams({
|
|
340
|
+
model: "flux-general-en",
|
|
341
|
+
eagerEotThreshold: 0.5,
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
expect(params.get("eager_eot_threshold")).toBe("0.5");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("omits encoding and sample_rate for containerized audio", () => {
|
|
348
|
+
const params = new URLSearchParams(
|
|
349
|
+
buildFluxQueryParams({ model: "flux-general-en" }),
|
|
350
|
+
);
|
|
351
|
+
expect(params.has("encoding")).toBe(false);
|
|
352
|
+
expect(params.has("sample_rate")).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("clamps thresholds and the timeout to the ranges Deepgram accepts", () => {
|
|
356
|
+
const low = new URLSearchParams(
|
|
357
|
+
buildFluxQueryParams({
|
|
358
|
+
model: "flux-general-en",
|
|
359
|
+
eotThreshold: 0.1,
|
|
360
|
+
eagerEotThreshold: 0.05,
|
|
361
|
+
eotTimeoutMs: 10,
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
expect(low.get("eot_threshold")).toBe("0.5");
|
|
365
|
+
expect(low.get("eager_eot_threshold")).toBe("0.3");
|
|
366
|
+
expect(low.get("eot_timeout_ms")).toBe("500");
|
|
367
|
+
|
|
368
|
+
const high = new URLSearchParams(
|
|
369
|
+
buildFluxQueryParams({
|
|
370
|
+
model: "flux-general-en",
|
|
371
|
+
eotThreshold: 5,
|
|
372
|
+
eagerEotThreshold: 5,
|
|
373
|
+
eotTimeoutMs: 999_999,
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
376
|
+
expect(high.get("eot_threshold")).toBe("0.9");
|
|
377
|
+
expect(high.get("eager_eot_threshold")).toBe("0.9");
|
|
378
|
+
expect(high.get("eot_timeout_ms")).toBe("60000");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("clamps eager_eot_threshold down to eot_threshold", () => {
|
|
382
|
+
const params = new URLSearchParams(
|
|
383
|
+
buildFluxQueryParams({
|
|
384
|
+
model: "flux-general-en",
|
|
385
|
+
eotThreshold: 0.5,
|
|
386
|
+
eagerEotThreshold: 0.9,
|
|
387
|
+
}),
|
|
388
|
+
);
|
|
389
|
+
// Deepgram rejects the handshake when eager exceeds the final threshold.
|
|
390
|
+
expect(Number(params.get("eager_eot_threshold"))).toBeLessThanOrEqual(
|
|
391
|
+
Number(params.get("eot_threshold")),
|
|
392
|
+
);
|
|
393
|
+
expect(params.get("eager_eot_threshold")).toBe("0.5");
|
|
394
|
+
expect(params.get("eot_threshold")).toBe("0.5");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("clamps eager_eot_threshold against Deepgram's default when eot_threshold is unset", () => {
|
|
398
|
+
const params = new URLSearchParams(
|
|
399
|
+
buildFluxQueryParams({
|
|
400
|
+
model: "flux-general-en",
|
|
401
|
+
eagerEotThreshold: 0.9,
|
|
402
|
+
}),
|
|
403
|
+
);
|
|
404
|
+
// No eot_threshold is sent, so the server default of 0.7 is in force.
|
|
405
|
+
expect(params.has("eot_threshold")).toBe(false);
|
|
406
|
+
expect(params.get("eager_eot_threshold")).toBe("0.7");
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("leaves an already-valid eager/eot pair untouched", () => {
|
|
410
|
+
const params = new URLSearchParams(
|
|
411
|
+
buildFluxQueryParams({
|
|
412
|
+
model: "flux-general-en",
|
|
413
|
+
eotThreshold: 0.9,
|
|
414
|
+
eagerEotThreshold: 0.6,
|
|
415
|
+
}),
|
|
416
|
+
);
|
|
417
|
+
expect(params.get("eot_threshold")).toBe("0.9");
|
|
418
|
+
expect(params.get("eager_eot_threshold")).toBe("0.6");
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("the eager clamp never invents an eager_eot_threshold", () => {
|
|
422
|
+
// Omitting the parameter is what disables eager turn-end speculation, so
|
|
423
|
+
// the cross-threshold clamp must not fire when it is unset.
|
|
424
|
+
for (const eotThreshold of [undefined, 0.5, 0.9]) {
|
|
425
|
+
const query = buildFluxQueryParams({
|
|
426
|
+
model: "flux-general-en",
|
|
427
|
+
...(eotThreshold !== undefined ? { eotThreshold } : {}),
|
|
428
|
+
});
|
|
429
|
+
expect(query).not.toContain("eager_eot_threshold");
|
|
430
|
+
expect(new URLSearchParams(query).has("eager_eot_threshold")).toBe(false);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
});
|