@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import type { BatchTranscriber } from "../../../../stt/types.js";
|
|
4
|
+
import { SttError } from "../../../../stt/types.js";
|
|
4
5
|
import type { ToolContext } from "../../../../tools/types.js";
|
|
5
6
|
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
@@ -8,9 +9,15 @@ import type { ToolContext } from "../../../../tools/types.js";
|
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
|
|
10
11
|
let mockTranscriber: BatchTranscriber | null = null;
|
|
12
|
+
let mockResolveError: Error | null = null;
|
|
11
13
|
|
|
12
14
|
mock.module("../../../../providers/speech-to-text/resolve.js", () => ({
|
|
13
|
-
resolveBatchTranscriber: async () =>
|
|
15
|
+
resolveBatchTranscriber: async () => {
|
|
16
|
+
if (mockResolveError) {
|
|
17
|
+
throw mockResolveError;
|
|
18
|
+
}
|
|
19
|
+
return mockTranscriber;
|
|
20
|
+
},
|
|
14
21
|
}));
|
|
15
22
|
|
|
16
23
|
// Track calls to spawnWithTimeout so we can simulate ffmpeg/ffprobe results.
|
|
@@ -103,6 +110,7 @@ function makeMockTranscriber(
|
|
|
103
110
|
describe("transcribe_media tool", () => {
|
|
104
111
|
beforeEach(() => {
|
|
105
112
|
mockTranscriber = null;
|
|
113
|
+
mockResolveError = null;
|
|
106
114
|
spawnResults = {};
|
|
107
115
|
accessiblePaths = new Set();
|
|
108
116
|
mockFileContents = {};
|
|
@@ -119,6 +127,19 @@ describe("transcribe_media tool", () => {
|
|
|
119
127
|
"No speech-to-text provider is configured",
|
|
120
128
|
);
|
|
121
129
|
});
|
|
130
|
+
|
|
131
|
+
test("surfaces a resolver error verbatim rather than the not-configured copy", async () => {
|
|
132
|
+
const reason =
|
|
133
|
+
'Deepgram Flux is streaming-only. Batch transcription requires the deepgram provider: set services.stt.provider to "deepgram".';
|
|
134
|
+
mockResolveError = new SttError("provider-error", reason, {
|
|
135
|
+
userFacing: true,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const result = await run({ file_path: "/tmp/test.mp3" }, makeContext());
|
|
139
|
+
|
|
140
|
+
expect(result.isError).toBe(true);
|
|
141
|
+
expect(result.content).toBe(reason);
|
|
142
|
+
});
|
|
122
143
|
});
|
|
123
144
|
|
|
124
145
|
describe("input validation", () => {
|
|
@@ -234,8 +234,15 @@ export async function run(
|
|
|
234
234
|
};
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
// Resolve the configured STT provider
|
|
238
|
-
|
|
237
|
+
// Resolve the configured STT provider. A typed resolver error already names
|
|
238
|
+
// the mismatch (e.g. a streaming-only provider) and its fix, so it reaches
|
|
239
|
+
// the model verbatim rather than as the "nothing configured" copy below.
|
|
240
|
+
let transcriber: BatchTranscriber | null;
|
|
241
|
+
try {
|
|
242
|
+
transcriber = await resolveBatchTranscriber();
|
|
243
|
+
} catch (err) {
|
|
244
|
+
return { content: (err as Error).message, isError: true };
|
|
245
|
+
}
|
|
239
246
|
if (!transcriber) {
|
|
240
247
|
return {
|
|
241
248
|
content:
|
|
@@ -121,22 +121,21 @@ export const CALL_SITE_DEFAULTS: Record<LLMCallSite, CallSiteDefaultConfig> = {
|
|
|
121
121
|
effort: "low",
|
|
122
122
|
thinking: { enabled: false },
|
|
123
123
|
},
|
|
124
|
-
//
|
|
125
|
-
// upstream cannot fit any usable decision budget (~1s+ per forced tool call).
|
|
124
|
+
// Progress narration only helps when it arrives before the next real output.
|
|
126
125
|
// `latency-optimized` is the latency-class profile (see
|
|
127
126
|
// default-profile-catalog.ts): managed installs get the pinned latency model,
|
|
128
127
|
// BYOK installs resolve their own provider's latency model through the intent
|
|
129
128
|
// table rather than a model id they may hold no credential for. The profile
|
|
130
129
|
// is user-facing ("Speed"), so a user edit to it moves this call site too.
|
|
131
|
-
|
|
130
|
+
voiceProgressNarration: {
|
|
132
131
|
profile: "latency-optimized",
|
|
133
132
|
effort: "low",
|
|
134
133
|
thinking: { enabled: false },
|
|
135
134
|
},
|
|
136
135
|
// The front-door leg fronts EVERY unified live-voice turn and its leading
|
|
137
136
|
// tokens ARE the endpointing/triage verdict, so both TTFT variance and
|
|
138
|
-
// judgment quality gate the whole call.
|
|
139
|
-
//
|
|
137
|
+
// judgment quality gate the whole call. Live drives showed the
|
|
138
|
+
// cost-optimized upstream with multi-second
|
|
140
139
|
// cross-session TTFT tails and over-escalation of small talk under open-task
|
|
141
140
|
// context pressure.
|
|
142
141
|
voiceFrontDoor: {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
isModelInCatalog,
|
|
5
5
|
} from "../providers/model-catalog.js";
|
|
6
6
|
import { resolveModelIntent } from "../providers/model-intents.js";
|
|
7
|
+
import { isCodexSubscriptionModel } from "../providers/openai/codex-models.js";
|
|
7
8
|
import type { ModelIntent } from "../providers/types.js";
|
|
8
9
|
import { getManagedUpstream } from "../providers/vellum-model-routing.js";
|
|
9
10
|
import {
|
|
@@ -31,7 +32,8 @@ import {
|
|
|
31
32
|
* structured as an intent × provider matrix: each default profile is an
|
|
32
33
|
* intent, and each provider that can serve default profiles has a concrete
|
|
33
34
|
* implementation of that intent (model, token budget, effort, thinking).
|
|
34
|
-
* The `vellum` column is the platform-managed implementation
|
|
35
|
+
* The `vellum` column is the platform-managed implementation and the
|
|
36
|
+
* `chatgpt` column is the ChatGPT-subscription implementation; the other
|
|
35
37
|
* columns are the BYOK implementations resolved through `llm.defaultProvider`
|
|
36
38
|
* on off-platform installs.
|
|
37
39
|
*
|
|
@@ -147,6 +149,69 @@ const VELLUM_PROFILE_IMPLS: ProfileImpls = {
|
|
|
147
149
|
},
|
|
148
150
|
};
|
|
149
151
|
|
|
152
|
+
/**
|
|
153
|
+
* The `chatgpt` column: ChatGPT-subscription implementations, stamped
|
|
154
|
+
* `provider: "chatgpt"` so dispatch routes through the canonical
|
|
155
|
+
* `chatgpt-subscription` row via `resolveRoutingIdentity` with no pinned
|
|
156
|
+
* connection. Models are pinned (never intents): the intent tables are
|
|
157
|
+
* keyed by concrete dispatch providers, and the Codex endpoint serves only
|
|
158
|
+
* `CODEX_SUBSCRIPTION_MODEL_IDS`. Cost and Speed are identical
|
|
159
|
+
* implementations here: the subscription serves no tier cheaper or faster
|
|
160
|
+
* than luna, and both profiles advertise reasoning off.
|
|
161
|
+
*/
|
|
162
|
+
const CHATGPT_PROFILE_IMPLS: ProfileImpls = {
|
|
163
|
+
balanced: {
|
|
164
|
+
model: "gpt-5.6-luna",
|
|
165
|
+
provider: "chatgpt",
|
|
166
|
+
source: "managed",
|
|
167
|
+
label: "Balanced",
|
|
168
|
+
description: "Good balance of quality, cost, and speed",
|
|
169
|
+
// Matches the vellum column's Balanced (same model): the Codex path
|
|
170
|
+
// sends no max_output_tokens, so this only sizes internal budgeting.
|
|
171
|
+
maxTokens: 32000,
|
|
172
|
+
effort: "high",
|
|
173
|
+
thinking: { enabled: true, streamThinking: true },
|
|
174
|
+
contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
|
|
175
|
+
},
|
|
176
|
+
"quality-optimized": {
|
|
177
|
+
model: "gpt-5.6-sol",
|
|
178
|
+
provider: "chatgpt",
|
|
179
|
+
source: "managed",
|
|
180
|
+
label: "Quality",
|
|
181
|
+
description: "Best results with the most capable model",
|
|
182
|
+
maxTokens: 32000,
|
|
183
|
+
effort: "high",
|
|
184
|
+
thinking: { enabled: true, streamThinking: true },
|
|
185
|
+
contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
|
|
186
|
+
},
|
|
187
|
+
"cost-optimized": {
|
|
188
|
+
model: "gpt-5.6-luna",
|
|
189
|
+
provider: "chatgpt",
|
|
190
|
+
source: "managed",
|
|
191
|
+
label: "Cost",
|
|
192
|
+
description: "Cheapest responses, for high-volume work",
|
|
193
|
+
maxTokens: 8192,
|
|
194
|
+
effort: "none",
|
|
195
|
+
thinking: { enabled: false, streamThinking: false },
|
|
196
|
+
contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
|
|
197
|
+
},
|
|
198
|
+
"latency-optimized": {
|
|
199
|
+
model: "gpt-5.6-luna",
|
|
200
|
+
provider: "chatgpt",
|
|
201
|
+
source: "managed",
|
|
202
|
+
label: "Speed",
|
|
203
|
+
description: "Fastest responses, with reasoning turned off",
|
|
204
|
+
maxTokens: 8192,
|
|
205
|
+
// Explicit reasoning opt-out, matching the other columns: this profile
|
|
206
|
+
// advertises reasoning as off, and OpenAI-compat APIs default reasoning
|
|
207
|
+
// to "medium" when the field is omitted, so the opt-out has to be stated
|
|
208
|
+
// rather than implied.
|
|
209
|
+
effort: "none",
|
|
210
|
+
thinking: { enabled: false, streamThinking: false },
|
|
211
|
+
contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
150
215
|
/**
|
|
151
216
|
* The BYOK implementation of each default profile intent, shared by every
|
|
152
217
|
* non-vellum provider. The concrete model resolves per provider from the
|
|
@@ -219,7 +284,9 @@ export const PROFILE_IMPLS: Record<
|
|
|
219
284
|
provider,
|
|
220
285
|
provider === "vellum"
|
|
221
286
|
? VELLUM_PROFILE_IMPLS[key]
|
|
222
|
-
:
|
|
287
|
+
: provider === "chatgpt"
|
|
288
|
+
? CHATGPT_PROFILE_IMPLS[key]
|
|
289
|
+
: { ...BYOK_PROFILE_IMPLS[key], provider },
|
|
223
290
|
]),
|
|
224
291
|
) as Record<DefaultProfileProvider, DefaultProfileTemplate>,
|
|
225
292
|
]),
|
|
@@ -302,9 +369,9 @@ export const OS_BETA_PROFILE_TEMPLATE: DefaultProfileTemplate = {
|
|
|
302
369
|
/**
|
|
303
370
|
* Profiles whose body is code-owned outright: no workspace overlay, and no
|
|
304
371
|
* user-owned shadow. The shadow rule below lets a user replace a default they
|
|
305
|
-
* can select, but `latency-optimized`
|
|
306
|
-
* `
|
|
307
|
-
*
|
|
372
|
+
* can select, but `latency-optimized` serves `voiceFrontDoor` and
|
|
373
|
+
* `voiceProgressNarration`, where a model outside the latency envelope is
|
|
374
|
+
* audible dead air rather than a slow reply. A same-named
|
|
308
375
|
* workspace entry stays on disk and stays listed; it just never governs what
|
|
309
376
|
* this name resolves to.
|
|
310
377
|
*/
|
|
@@ -372,21 +439,23 @@ for (const key of DEFAULT_PROFILE_KEYS) {
|
|
|
372
439
|
`PROFILE_IMPLS[${key}][${provider}] must set exactly one of \`intent\` or \`model\`.`,
|
|
373
440
|
);
|
|
374
441
|
}
|
|
375
|
-
if (impl.provider
|
|
442
|
+
if (ROUTING_IDENTITY_PROVIDERS.has(impl.provider) && impl.model == null) {
|
|
376
443
|
throw new Error(
|
|
377
|
-
`PROFILE_IMPLS[${key}][${provider}] must pin a \`model\`:
|
|
378
|
-
`
|
|
444
|
+
`PROFILE_IMPLS[${key}][${provider}] must pin a \`model\`: routing ` +
|
|
445
|
+
`identities have no intent table, and the model selects the route.`,
|
|
379
446
|
);
|
|
380
447
|
}
|
|
381
448
|
if (impl.model != null) {
|
|
382
449
|
const routable =
|
|
383
450
|
impl.provider === "vellum"
|
|
384
451
|
? getManagedUpstream(impl.model) !== null
|
|
385
|
-
:
|
|
452
|
+
: impl.provider === "chatgpt"
|
|
453
|
+
? isCodexSubscriptionModel(impl.model)
|
|
454
|
+
: isModelInCatalog(impl.provider, impl.model);
|
|
386
455
|
if (!routable) {
|
|
387
456
|
throw new Error(
|
|
388
457
|
`PROFILE_IMPLS[${key}][${provider}] references model "${impl.model}" ` +
|
|
389
|
-
`which is not ${impl.provider === "vellum" ? "served by any managed upstream" : `in PROVIDER_CATALOG for provider "${impl.provider}"`}. ` +
|
|
458
|
+
`which is not ${impl.provider === "vellum" ? "served by any managed upstream" : impl.provider === "chatgpt" ? "in CODEX_SUBSCRIPTION_MODEL_IDS" : `in PROVIDER_CATALOG for provider "${impl.provider}"`}. ` +
|
|
390
459
|
`Update model-catalog.ts or default-profile-catalog.ts.`,
|
|
391
460
|
);
|
|
392
461
|
}
|
|
@@ -517,7 +586,9 @@ function resolveAgainstBody(
|
|
|
517
586
|
* Non-obvious rules:
|
|
518
587
|
*
|
|
519
588
|
* - The `vellum` column stamps `provider: "vellum"` with no connection —
|
|
520
|
-
* dispatch derives the upstream from the model per-request.
|
|
589
|
+
* dispatch derives the upstream from the model per-request. The `chatgpt`
|
|
590
|
+
* column likewise stamps its routing identity with no connection;
|
|
591
|
+
* dispatch resolves the canonical subscription row per-request.
|
|
521
592
|
* - A default provider without a named matrix column materializes from the
|
|
522
593
|
* shared `BYOK_PROFILE_IMPLS` templates, with `resolveModelIntent`
|
|
523
594
|
* falling back to the provider's catalog `defaultModel`.
|
|
@@ -38,7 +38,9 @@ export const OS_BETA_PROFILE_KEY = "os-beta";
|
|
|
38
38
|
/**
|
|
39
39
|
* The named columns of the intent × provider matrix. `vellum` is the
|
|
40
40
|
* platform-managed column (routed through the single `vellum` connection to
|
|
41
|
-
* an underlying provider per profile)
|
|
41
|
+
* an underlying provider per profile) and `chatgpt` is the
|
|
42
|
+
* ChatGPT-subscription column (routed through the `chatgpt-subscription`
|
|
43
|
+
* connection to the Codex endpoint); the rest are BYOK columns whose
|
|
42
44
|
* models resolve per provider via `resolveModelIntent`. The full set of
|
|
43
45
|
* providers that can back `llm.defaultProvider` is wider, see
|
|
44
46
|
* `DEFAULT_PROVIDER_CHOICES` in `schemas/llm.ts`.
|
|
@@ -53,6 +55,7 @@ export const DEFAULT_PROFILE_PROVIDERS = [
|
|
|
53
55
|
"gemini",
|
|
54
56
|
"fireworks",
|
|
55
57
|
"openrouter",
|
|
58
|
+
"chatgpt",
|
|
56
59
|
"vellum",
|
|
57
60
|
] as const;
|
|
58
61
|
export type DefaultProfileProvider = (typeof DEFAULT_PROFILE_PROVIDERS)[number];
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* conveniences (`getDefaultProvider()` without an argument,
|
|
7
7
|
* `setDefaultProvider`) live in `default-provider.ts`.
|
|
8
8
|
*/
|
|
9
|
+
import { CHATGPT_SUBSCRIPTION_CONNECTION_NAME } from "../providers/inference/auth.js";
|
|
9
10
|
import { VELLUM_MANAGED_CONNECTION_NAME } from "../providers/vellum-model-routing.js";
|
|
10
11
|
import type { DefaultProviderConfig } from "./schemas/llm.js";
|
|
11
12
|
import type { AssistantConfig } from "./types.js";
|
|
@@ -29,5 +30,8 @@ export function resolveDefaultConnectionName(
|
|
|
29
30
|
if (dp.provider === "vellum") {
|
|
30
31
|
return VELLUM_MANAGED_CONNECTION_NAME;
|
|
31
32
|
}
|
|
33
|
+
if (dp.provider === "chatgpt") {
|
|
34
|
+
return CHATGPT_SUBSCRIPTION_CONNECTION_NAME;
|
|
35
|
+
}
|
|
32
36
|
return `${dp.provider}-personal`;
|
|
33
37
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveEntryProviderKind } from "../providers/connection-resolution.js";
|
|
1
2
|
import { ROUTING_IDENTITY_PROVIDERS } from "../providers/inference/auth.js";
|
|
2
3
|
import {
|
|
3
4
|
getCatalogProviderForModel,
|
|
@@ -54,11 +55,18 @@ export function resolveEffectiveContextWindow({
|
|
|
54
55
|
forceOverrideProfile,
|
|
55
56
|
selectionSeed,
|
|
56
57
|
});
|
|
57
|
-
// Routing identities
|
|
58
|
-
// carries
|
|
58
|
+
// Routing identities dispatch built-in catalog models, so the model's
|
|
59
|
+
// catalog owner carries their limits. An entry-name provider resolves
|
|
60
|
+
// through its row's kind instead: a custom-endpoint kind has no catalog
|
|
61
|
+
// models, so its models keep the conservative default even when a model
|
|
62
|
+
// id collides with a built-in one (the custom endpoint's "gpt-5.5" is not
|
|
63
|
+
// OpenAI's). Labels with no row fall back to the model's catalog owner.
|
|
59
64
|
const catalogProviderId = ROUTING_IDENTITY_PROVIDERS.has(resolved.provider)
|
|
60
65
|
? getCatalogProviderForModel(resolved.model)
|
|
61
|
-
: resolved.provider
|
|
66
|
+
: PROVIDER_CATALOG.some((p) => p.id === resolved.provider)
|
|
67
|
+
? resolved.provider
|
|
68
|
+
: (resolveEntryProviderKind(resolved.provider, resolved.model) ??
|
|
69
|
+
getCatalogProviderForModel(resolved.model));
|
|
62
70
|
const catalogModel = PROVIDER_CATALOG.find(
|
|
63
71
|
(provider) => provider.id === catalogProviderId,
|
|
64
72
|
)?.models.find((model) => model.id === resolved.model);
|
|
@@ -76,6 +76,15 @@ import {
|
|
|
76
76
|
*/
|
|
77
77
|
export interface ResolveCallSiteOpts {
|
|
78
78
|
overrideProfile?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Whether a profile's provider value can actually dispatch: a known
|
|
81
|
+
* vendor, or a connection entry row. Selection is pure and DB-blind, so
|
|
82
|
+
* dispatch-side callers supply this (see `dispatchProviderResolvable` in
|
|
83
|
+
* `connection-resolution.ts`); a profile failing it is unusable and
|
|
84
|
+
* selection falls through to the next rung, keeping model and transport
|
|
85
|
+
* coherent. When absent, every provider is assumed resolvable.
|
|
86
|
+
*/
|
|
87
|
+
isResolvableProvider?: (provider: string) => boolean;
|
|
79
88
|
/**
|
|
80
89
|
* Float `overrideProfile` above the call-site layers for non-main-agent call
|
|
81
90
|
* sites. Retained for API compatibility; under single-winner selection the
|
|
@@ -112,7 +121,11 @@ export interface ResolveCallSiteOpts {
|
|
|
112
121
|
}) => void;
|
|
113
122
|
}
|
|
114
123
|
|
|
115
|
-
export type ResolutionFallbackReason =
|
|
124
|
+
export type ResolutionFallbackReason =
|
|
125
|
+
| "missing"
|
|
126
|
+
| "disabled"
|
|
127
|
+
| "incomplete"
|
|
128
|
+
| "unresolvable";
|
|
116
129
|
|
|
117
130
|
export interface ResolvedCallSiteConfig {
|
|
118
131
|
config: z.infer<typeof LLMConfigBase>;
|
|
@@ -313,6 +326,20 @@ function usableEntry(
|
|
|
313
326
|
report(name, "incomplete");
|
|
314
327
|
return undefined;
|
|
315
328
|
}
|
|
329
|
+
// A provider the caller cannot resolve (not a known vendor, not an entry
|
|
330
|
+
// row) makes the profile unusable, and selection falls through to the
|
|
331
|
+
// next rung the same way an incomplete profile does: the fallback keeps
|
|
332
|
+
// model and transport coherent, where a dispatch-time fallback would pair
|
|
333
|
+
// this profile's model with the default transport. Selection is DB-blind,
|
|
334
|
+
// so the predicate is supplied by dispatch-side callers; when absent,
|
|
335
|
+
// every provider is assumed resolvable.
|
|
336
|
+
if (
|
|
337
|
+
opts.isResolvableProvider != null &&
|
|
338
|
+
!opts.isResolvableProvider(entry.provider)
|
|
339
|
+
) {
|
|
340
|
+
report(name, "unresolvable");
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
316
343
|
return { name, entry };
|
|
317
344
|
}
|
|
318
345
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { getDb } from "../persistence/db-connection.js";
|
|
1
2
|
import { ROUTING_IDENTITY_PROVIDERS } from "../providers/inference/auth.js";
|
|
3
|
+
import { getConnection } from "../providers/inference/connections.js";
|
|
2
4
|
import {
|
|
3
5
|
getCatalogProviderForModel,
|
|
4
6
|
isModelInCatalog,
|
|
5
7
|
} from "../providers/model-catalog.js";
|
|
6
8
|
import {
|
|
9
|
+
getManagedUpstream,
|
|
7
10
|
MANAGED_ROUTABLE_PROVIDERS,
|
|
8
11
|
VELLUM_MANAGED_CONNECTION_NAME,
|
|
9
12
|
} from "../providers/vellum-model-routing.js";
|
|
@@ -97,35 +100,80 @@ export function completeCustomProfile(
|
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
// would
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
103
|
+
// Completion never stamps a `provider_connection`; an inherited binding
|
|
104
|
+
// would only re-introduce the collapsed field on disk. The default's
|
|
105
|
+
// explicit binding is instead inherited IN the provider value, the
|
|
106
|
+
// entries-model representation: the vellum binding becomes the routing
|
|
107
|
+
// identity (only when it can serve the model, or the read-path schema
|
|
108
|
+
// would strip the profile), and a same-vendor binding becomes the entry
|
|
109
|
+
// name, so a completed profile keeps signing with the credential the
|
|
110
|
+
// workspace default names rather than whatever auto-resolution finds
|
|
111
|
+
// first.
|
|
109
112
|
if (
|
|
110
113
|
completed.provider !== undefined &&
|
|
111
|
-
ROUTING_IDENTITY_PROVIDERS.has(completed.provider)
|
|
114
|
+
!ROUTING_IDENTITY_PROVIDERS.has(completed.provider) &&
|
|
115
|
+
profile.provider_connection === undefined &&
|
|
116
|
+
dflt.provider_connection !== undefined
|
|
112
117
|
) {
|
|
113
|
-
|
|
118
|
+
if (dflt.provider_connection === VELLUM_MANAGED_CONNECTION_NAME) {
|
|
119
|
+
// Only managed-servable pairs inherit the managed binding; anything
|
|
120
|
+
// else inherits nothing and lets dispatch auto-resolve by vendor,
|
|
121
|
+
// matching the pre-entries inheritance contract.
|
|
122
|
+
if (
|
|
123
|
+
MANAGED_ROUTABLE_PROVIDERS.has(completed.provider) &&
|
|
124
|
+
completed.model !== undefined &&
|
|
125
|
+
getManagedUpstream(completed.model) !== null
|
|
126
|
+
) {
|
|
127
|
+
completed.provider = "vellum";
|
|
128
|
+
}
|
|
129
|
+
} else if (completed.provider === dflt.provider) {
|
|
130
|
+
// Folding to the entry name is only safe against a verified row; a
|
|
131
|
+
// dangling or kind-disagreeing binding stays in the legacy field,
|
|
132
|
+
// where the collapse migration's recovery can judge it.
|
|
133
|
+
if (bindingRowKind(dflt.provider_connection) === completed.provider) {
|
|
134
|
+
completed.provider = dflt.provider_connection;
|
|
135
|
+
} else {
|
|
136
|
+
completed.provider_connection = dflt.provider_connection;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
114
139
|
}
|
|
140
|
+
return structuredClone(completed);
|
|
141
|
+
}
|
|
115
142
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
/**
|
|
144
|
+
* `{...raw, ...completed}` recursively: completed (schema-known) values win,
|
|
145
|
+
* raw keys the schema stripped survive at every depth. Used by boot
|
|
146
|
+
* materialization and the config write path so both preserve unknown keys
|
|
147
|
+
* the same way after `safeParse`.
|
|
148
|
+
*/
|
|
149
|
+
export function mergePreservingUnknownKeys(
|
|
150
|
+
raw: Record<string, unknown>,
|
|
151
|
+
completed: Record<string, unknown>,
|
|
152
|
+
): Record<string, unknown> {
|
|
153
|
+
const out: Record<string, unknown> = { ...raw, ...completed };
|
|
154
|
+
for (const [key, value] of Object.entries(completed)) {
|
|
155
|
+
const rawValue = raw[key];
|
|
156
|
+
if (isRecord(value) && isRecord(rawValue)) {
|
|
157
|
+
out[key] = mergePreservingUnknownKeys(rawValue, value);
|
|
158
|
+
}
|
|
126
159
|
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
127
162
|
|
|
128
|
-
|
|
163
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
164
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The provider kind stored on a connection row, or null when the row is
|
|
169
|
+
* missing or the DB is unavailable (both mean "unverifiable" to callers).
|
|
170
|
+
*/
|
|
171
|
+
function bindingRowKind(name: string): string | null {
|
|
172
|
+
try {
|
|
173
|
+
return getConnection(getDb(), name)?.provider ?? null;
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
129
177
|
}
|
|
130
178
|
|
|
131
179
|
type PlainObject = Record<string, unknown>;
|
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
LiveVoiceConfigSchema,
|
|
5
|
+
LiveVoiceFluxConfigSchema,
|
|
5
6
|
LiveVoiceFrontModelConfigSchema,
|
|
6
7
|
LiveVoiceVadConfigSchema,
|
|
7
8
|
VALID_LIVE_VOICE_MODES,
|
|
@@ -21,11 +22,18 @@ const FRONT_MODEL_DEFAULTS = {
|
|
|
21
22
|
endpointDecisionTimeoutMs: 1200,
|
|
22
23
|
endpointExtensionMs: 1500,
|
|
23
24
|
endpointMaxExtensions: 2,
|
|
24
|
-
ackFirstDeltaTimeoutMs: 2500,
|
|
25
|
-
ackGenerationTimeoutMs: 600,
|
|
26
25
|
progress: PROGRESS_DEFAULTS,
|
|
27
26
|
};
|
|
28
27
|
|
|
28
|
+
// `eagerEotThreshold` is deliberately absent: it has no default, and leaving it
|
|
29
|
+
// unset is what keeps Deepgram from emitting speculative turn events.
|
|
30
|
+
const FLUX_DEFAULTS = {
|
|
31
|
+
turnEnd: { enabled: false },
|
|
32
|
+
model: "flux-general-en",
|
|
33
|
+
eotThreshold: 0.7,
|
|
34
|
+
eotTimeoutMs: 5_000,
|
|
35
|
+
};
|
|
36
|
+
|
|
29
37
|
describe("LiveVoiceVadConfigSchema", () => {
|
|
30
38
|
test("empty object parses to defaults", () => {
|
|
31
39
|
const parsed = LiveVoiceVadConfigSchema.parse({});
|
|
@@ -124,8 +132,15 @@ describe("LiveVoiceFrontModelConfigSchema", () => {
|
|
|
124
132
|
expect(parsed.endpointMaxExtensions).toBe(0);
|
|
125
133
|
// Unspecified fields still get defaults
|
|
126
134
|
expect(parsed.endpointExtensionMs).toBe(1500);
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("strips retired generated-ack settings", () => {
|
|
138
|
+
const parsed = LiveVoiceFrontModelConfigSchema.parse({
|
|
139
|
+
ackFirstDeltaTimeoutMs: 2500,
|
|
140
|
+
ackGenerationTimeoutMs: 600,
|
|
141
|
+
});
|
|
142
|
+
expect(parsed).not.toHaveProperty("ackFirstDeltaTimeoutMs");
|
|
143
|
+
expect(parsed).not.toHaveProperty("ackGenerationTimeoutMs");
|
|
129
144
|
});
|
|
130
145
|
|
|
131
146
|
test("rejects non-positive endpointDecisionTimeoutMs", () => {
|
|
@@ -213,6 +228,87 @@ describe("LiveVoiceFrontModelConfigSchema", () => {
|
|
|
213
228
|
});
|
|
214
229
|
});
|
|
215
230
|
|
|
231
|
+
describe("LiveVoiceFluxConfigSchema", () => {
|
|
232
|
+
test("empty object parses to defaults, with turn-end off", () => {
|
|
233
|
+
expect(LiveVoiceFluxConfigSchema.parse({})).toEqual(FLUX_DEFAULTS);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("an unset eagerEotThreshold is absent, not zero", () => {
|
|
237
|
+
expect("eagerEotThreshold" in LiveVoiceFluxConfigSchema.parse({})).toBe(
|
|
238
|
+
false,
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("accepts overrides", () => {
|
|
243
|
+
const parsed = LiveVoiceFluxConfigSchema.parse({
|
|
244
|
+
turnEnd: { enabled: true },
|
|
245
|
+
model: "flux-general-multi",
|
|
246
|
+
eotThreshold: 0.85,
|
|
247
|
+
eagerEotThreshold: 0.45,
|
|
248
|
+
eotTimeoutMs: 12_000,
|
|
249
|
+
});
|
|
250
|
+
expect(parsed.turnEnd.enabled).toBe(true);
|
|
251
|
+
expect(parsed.model).toBe("flux-general-multi");
|
|
252
|
+
expect(parsed.eotThreshold).toBe(0.85);
|
|
253
|
+
expect(parsed.eagerEotThreshold).toBe(0.45);
|
|
254
|
+
expect(parsed.eotTimeoutMs).toBe(12_000);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("partial overrides merge with defaults", () => {
|
|
258
|
+
const parsed = LiveVoiceFluxConfigSchema.parse({ eotThreshold: 0.6 });
|
|
259
|
+
expect(parsed.eotThreshold).toBe(0.6);
|
|
260
|
+
expect(parsed.turnEnd.enabled).toBe(false);
|
|
261
|
+
expect(parsed.model).toBe("flux-general-en");
|
|
262
|
+
expect(parsed.eotTimeoutMs).toBe(5_000);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("rejects an eotThreshold outside 0.5..0.9", () => {
|
|
266
|
+
const above = LiveVoiceFluxConfigSchema.safeParse({ eotThreshold: 0.95 });
|
|
267
|
+
expect(above.success).toBe(false);
|
|
268
|
+
expect(above.error?.issues.map((i) => i.message)).toEqual([
|
|
269
|
+
"liveVoice.flux.eotThreshold must be <= 0.9",
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
const below = LiveVoiceFluxConfigSchema.safeParse({ eotThreshold: 0.4 });
|
|
273
|
+
expect(below.success).toBe(false);
|
|
274
|
+
expect(below.error?.issues.map((i) => i.message)).toEqual([
|
|
275
|
+
"liveVoice.flux.eotThreshold must be >= 0.5",
|
|
276
|
+
]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("rejects an eagerEotThreshold outside 0.3..0.9", () => {
|
|
280
|
+
expect(
|
|
281
|
+
LiveVoiceFluxConfigSchema.safeParse({ eagerEotThreshold: 0.2 }).success,
|
|
282
|
+
).toBe(false);
|
|
283
|
+
expect(
|
|
284
|
+
LiveVoiceFluxConfigSchema.safeParse({ eagerEotThreshold: 0.95 }).success,
|
|
285
|
+
).toBe(false);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("rejects an eotTimeoutMs outside 500..60000, and non-integers", () => {
|
|
289
|
+
expect(
|
|
290
|
+
LiveVoiceFluxConfigSchema.safeParse({ eotTimeoutMs: 499 }).success,
|
|
291
|
+
).toBe(false);
|
|
292
|
+
expect(
|
|
293
|
+
LiveVoiceFluxConfigSchema.safeParse({ eotTimeoutMs: 60_001 }).success,
|
|
294
|
+
).toBe(false);
|
|
295
|
+
expect(
|
|
296
|
+
LiveVoiceFluxConfigSchema.safeParse({ eotTimeoutMs: 1_500.5 }).success,
|
|
297
|
+
).toBe(false);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("rejects a non-boolean turnEnd.enabled", () => {
|
|
301
|
+
const result = LiveVoiceFluxConfigSchema.safeParse({
|
|
302
|
+
turnEnd: { enabled: "yes" },
|
|
303
|
+
});
|
|
304
|
+
expect(result.success).toBe(false);
|
|
305
|
+
const msgs = result.error?.issues.map((i) => i.message) ?? [];
|
|
306
|
+
expect(msgs.some((m) => m.includes("liveVoice.flux.turnEnd.enabled"))).toBe(
|
|
307
|
+
true,
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
216
312
|
describe("LiveVoiceConfigSchema", () => {
|
|
217
313
|
test("empty object parses to defaults", () => {
|
|
218
314
|
const parsed = LiveVoiceConfigSchema.parse({});
|
|
@@ -228,6 +324,9 @@ describe("LiveVoiceConfigSchema", () => {
|
|
|
228
324
|
echoDrainSlackMs: 300,
|
|
229
325
|
},
|
|
230
326
|
frontModel: FRONT_MODEL_DEFAULTS,
|
|
327
|
+
// Off by default: Flux turn detection is opt-in, so the front-door hold
|
|
328
|
+
// verdict keeps committing turns until it is enabled.
|
|
329
|
+
flux: FLUX_DEFAULTS,
|
|
231
330
|
maxSessionDurationSeconds: 1800,
|
|
232
331
|
// Off by default: voice turns carry only their transcript, no audio
|
|
233
332
|
// artifacts on the conversation messages (JARVIS-1283).
|
|
@@ -255,6 +354,7 @@ describe("LiveVoiceConfigSchema", () => {
|
|
|
255
354
|
mode: "ptt",
|
|
256
355
|
vad: { silenceThresholdMs: 900 },
|
|
257
356
|
frontModel: { endpointDecisionTimeoutMs: 300 },
|
|
357
|
+
flux: { turnEnd: { enabled: true } },
|
|
258
358
|
maxSessionDurationSeconds: 600,
|
|
259
359
|
});
|
|
260
360
|
expect(parsed.mode).toBe("ptt");
|
|
@@ -265,6 +365,9 @@ describe("LiveVoiceConfigSchema", () => {
|
|
|
265
365
|
// Partial frontModel overrides merge with defaults
|
|
266
366
|
expect(parsed.frontModel.endpointDecisionTimeoutMs).toBe(300);
|
|
267
367
|
expect(parsed.frontModel.endpointExtensionMs).toBe(1500);
|
|
368
|
+
// Partial flux overrides merge with defaults
|
|
369
|
+
expect(parsed.flux.turnEnd.enabled).toBe(true);
|
|
370
|
+
expect(parsed.flux.eotThreshold).toBe(0.7);
|
|
268
371
|
expect(parsed.maxSessionDurationSeconds).toBe(600);
|
|
269
372
|
});
|
|
270
373
|
|
|
@@ -289,11 +289,11 @@ const CATALOG_RECORD: CatalogRecord = {
|
|
|
289
289
|
"Captions images via a vision-capable profile for text-only model fallback.",
|
|
290
290
|
domain: "skills",
|
|
291
291
|
},
|
|
292
|
-
|
|
293
|
-
id: "
|
|
294
|
-
displayName: "Voice
|
|
292
|
+
voiceProgressNarration: {
|
|
293
|
+
id: "voiceProgressNarration",
|
|
294
|
+
displayName: "Voice Progress Narration",
|
|
295
295
|
description:
|
|
296
|
-
"
|
|
296
|
+
"Phrases short spoken progress updates during long-running live voice turns.",
|
|
297
297
|
domain: "agentLoop",
|
|
298
298
|
},
|
|
299
299
|
voiceFrontDoor: {
|