@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
|
@@ -65,6 +65,10 @@ const fakeConnections = new Map<string, Connection>();
|
|
|
65
65
|
mock.module("../inference/connections.js", () => ({
|
|
66
66
|
getConnection: (_db: unknown, name: string) =>
|
|
67
67
|
fakeConnections.get(name) ?? null,
|
|
68
|
+
listConnections: (_db: unknown, filter?: { provider?: string }) =>
|
|
69
|
+
Array.from(fakeConnections.values()).filter(
|
|
70
|
+
(c) => !filter?.provider || c.provider === filter.provider,
|
|
71
|
+
),
|
|
68
72
|
}));
|
|
69
73
|
|
|
70
74
|
mock.module("../registry.js", () => ({
|
|
@@ -449,11 +453,43 @@ describe("routing identities", () => {
|
|
|
449
453
|
},
|
|
450
454
|
} as never);
|
|
451
455
|
expect(resolveProviderCalls[0]?.name).toBe("chatgpt-subscription");
|
|
452
|
-
//
|
|
453
|
-
//
|
|
456
|
+
// A pre-migration row carries provider "openai": no override needed, the
|
|
457
|
+
// adapter resolves from the row.
|
|
454
458
|
expect(resolveProviderCalls[0]?.provider).toBe("openai");
|
|
455
459
|
});
|
|
456
460
|
|
|
461
|
+
test("a chatgpt-identity subscription row dispatches with an openai override", async () => {
|
|
462
|
+
// Migration 366 stamps the row with the "chatgpt" identity; the identity
|
|
463
|
+
// must never reach adapter construction, so dispatch threads the openai
|
|
464
|
+
// upstream as providerOverride, mirroring the vellum sentinel.
|
|
465
|
+
fakeConnections.set("chatgpt-subscription", {
|
|
466
|
+
name: "chatgpt-subscription",
|
|
467
|
+
provider: "chatgpt",
|
|
468
|
+
auth: {
|
|
469
|
+
type: "oauth_subscription",
|
|
470
|
+
credential: "credential/chatgpt/access_token",
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
fakeProviders.set("conn:chatgpt-subscription", {
|
|
474
|
+
name: "openai",
|
|
475
|
+
tag: "subscription",
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const provider = await tryResolveProviderForConnectionName(
|
|
479
|
+
"ignored",
|
|
480
|
+
{ llm: {} } as never,
|
|
481
|
+
"chatgpt",
|
|
482
|
+
"gpt-5.5",
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
expect(provider?.routeAttribution).toEqual({
|
|
486
|
+
connectionName: "chatgpt-subscription",
|
|
487
|
+
isManagedRoute: false,
|
|
488
|
+
});
|
|
489
|
+
expect(resolveProviderCalls[0]?.name).toBe("chatgpt-subscription");
|
|
490
|
+
expect(resolveProviderOpts[0]?.providerOverride).toBe("openai");
|
|
491
|
+
});
|
|
492
|
+
|
|
457
493
|
// A second managed row is creatable under any non-reserved name: only the
|
|
458
494
|
// canonical name is refused at the create route, and `provider: "vellum"`
|
|
459
495
|
// derives platform auth. Managed-ness is the row, never its name.
|
|
@@ -492,3 +528,179 @@ describe("routing identities", () => {
|
|
|
492
528
|
).rejects.toMatchObject({ reason: "not_found" });
|
|
493
529
|
});
|
|
494
530
|
});
|
|
531
|
+
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
// Entry-name providers — a profile provider naming a connection row directly.
|
|
534
|
+
// Write surfaces reject these until the entries model enables them; dispatch
|
|
535
|
+
// translates them so hand-edited configs (and the collapse migration later)
|
|
536
|
+
// route through the named row.
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
|
|
539
|
+
describe("entry-name providers", () => {
|
|
540
|
+
beforeEach(() => {
|
|
541
|
+
resolveProviderCalls.length = 0;
|
|
542
|
+
resolveProviderOpts.length = 0;
|
|
543
|
+
fakeConnections.clear();
|
|
544
|
+
fakeProviders.clear();
|
|
545
|
+
setConfig("llm", {});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("a profile naming an entry dispatches through that row", async () => {
|
|
549
|
+
registerConnection(
|
|
550
|
+
{
|
|
551
|
+
name: "anthropic-work",
|
|
552
|
+
provider: "anthropic",
|
|
553
|
+
auth: {
|
|
554
|
+
type: "api_key",
|
|
555
|
+
credential: "credential/anthropic-work/api_key",
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
{ name: "anthropic", tag: "work-key" },
|
|
559
|
+
);
|
|
560
|
+
setLlmConfig({
|
|
561
|
+
profiles: {
|
|
562
|
+
work: { provider: "anthropic-work", model: "claude-opus-4-8" },
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const result = await getConfiguredProvider("mainAgent", {
|
|
567
|
+
overrideProfile: "work",
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
expect(result).not.toBeNull();
|
|
571
|
+
expect(resolveProviderCalls.length).toBe(1);
|
|
572
|
+
expect(resolveProviderCalls[0]?.name).toBe("anthropic-work");
|
|
573
|
+
// The label is not a vendor: the row's own provider drives dispatch,
|
|
574
|
+
// so no override is threaded.
|
|
575
|
+
expect(resolveProviderOpts[0]?.providerOverride).toBeUndefined();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("a vellum-kind entry derives its upstream from the model", async () => {
|
|
579
|
+
registerConnection(
|
|
580
|
+
{ name: "work-managed", provider: "vellum", auth: { type: "platform" } },
|
|
581
|
+
{ name: "anthropic", tag: "managed-stub" },
|
|
582
|
+
);
|
|
583
|
+
setLlmConfig({
|
|
584
|
+
profiles: {
|
|
585
|
+
managed: { provider: "work-managed", model: "claude-opus-4-8" },
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const result = await getConfiguredProvider("mainAgent", {
|
|
590
|
+
overrideProfile: "managed",
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
expect(result).not.toBeNull();
|
|
594
|
+
expect(resolveProviderCalls[0]?.name).toBe("work-managed");
|
|
595
|
+
expect(resolveProviderOpts[0]?.providerOverride).toBe("anthropic");
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("an entry label alongside an explicit provider_connection still dispatches", async () => {
|
|
599
|
+
// The step-3 collapse migration rewrites provider to an entry name; a
|
|
600
|
+
// stale provider_connection left beside it must not fail the vendor
|
|
601
|
+
// equality against a label that is not a vendor.
|
|
602
|
+
registerConnection(
|
|
603
|
+
{
|
|
604
|
+
name: "anthropic-work",
|
|
605
|
+
provider: "anthropic",
|
|
606
|
+
auth: {
|
|
607
|
+
type: "api_key",
|
|
608
|
+
credential: "credential/anthropic-work/api_key",
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
{ name: "anthropic", tag: "work-key" },
|
|
612
|
+
);
|
|
613
|
+
setLlmConfig({
|
|
614
|
+
profiles: {
|
|
615
|
+
work: {
|
|
616
|
+
provider: "anthropic-work",
|
|
617
|
+
provider_connection: "anthropic-work",
|
|
618
|
+
model: "claude-opus-4-8",
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const result = await getConfiguredProvider("mainAgent", {
|
|
624
|
+
overrideProfile: "work",
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
expect(result).not.toBeNull();
|
|
628
|
+
expect(resolveProviderCalls[0]?.name).toBe("anthropic-work");
|
|
629
|
+
expect(resolveProviderOpts[0]?.providerOverride).toBeUndefined();
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
test("a label with a conflicting provider_connection is held to the label's kind", async () => {
|
|
633
|
+
// The label's row kind threads as the expected vendor, so a stale or
|
|
634
|
+
// conflicting connection cannot silently dispatch a different vendor:
|
|
635
|
+
// the equality mismatch auto-recovers to a row matching the kind.
|
|
636
|
+
registerConnection(
|
|
637
|
+
{
|
|
638
|
+
name: "anthropic-work",
|
|
639
|
+
provider: "anthropic",
|
|
640
|
+
auth: {
|
|
641
|
+
type: "api_key",
|
|
642
|
+
credential: "credential/anthropic-work/api_key",
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
{ name: "anthropic", tag: "work-key" },
|
|
646
|
+
);
|
|
647
|
+
registerConnection(
|
|
648
|
+
{ name: "ollama-local", provider: "ollama", auth: { type: "none" } },
|
|
649
|
+
{ name: "ollama", tag: "local" },
|
|
650
|
+
);
|
|
651
|
+
setLlmConfig({
|
|
652
|
+
profiles: {
|
|
653
|
+
conflicted: {
|
|
654
|
+
provider: "anthropic-work",
|
|
655
|
+
provider_connection: "ollama-local",
|
|
656
|
+
model: "claude-opus-4-8",
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const result = await getConfiguredProvider("mainAgent", {
|
|
662
|
+
overrideProfile: "conflicted",
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
expect(result).not.toBeNull();
|
|
666
|
+
expect(resolveProviderCalls[0]?.name).toBe("anthropic-work");
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("a profile naming no row is healed away at selection", async () => {
|
|
670
|
+
// Selection treats an unresolvable provider like an incomplete profile:
|
|
671
|
+
// the broken winner loses and the next rung's model AND transport apply
|
|
672
|
+
// together, restoring the pre-open-schema self-healing. The anchor is
|
|
673
|
+
// the managed default, so the heal lands on the vellum route.
|
|
674
|
+
registerConnection(
|
|
675
|
+
{ name: "vellum", provider: "vellum", auth: { type: "platform" } },
|
|
676
|
+
{ name: "anthropic", tag: "managed-anchor" },
|
|
677
|
+
);
|
|
678
|
+
setLlmConfig({
|
|
679
|
+
profiles: {
|
|
680
|
+
ghost: { provider: "no-such-entry", model: "claude-opus-4-8" },
|
|
681
|
+
},
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
const result = await getConfiguredProvider("mainAgent", {
|
|
685
|
+
overrideProfile: "ghost",
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
expect(result).not.toBeNull();
|
|
689
|
+
expect(resolveProviderCalls[0]?.name).toBe("vellum");
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test("healing hands off to the anchor, whose own failures stay loud", async () => {
|
|
693
|
+
// With the broken profile healed away, resolution rests on the managed
|
|
694
|
+
// anchor; its canonical row missing is a boot-seeding violation and
|
|
695
|
+
// throws explainably rather than masquerading as the ghost profile.
|
|
696
|
+
setLlmConfig({
|
|
697
|
+
profiles: {
|
|
698
|
+
ghost: { provider: "no-such-entry", model: "claude-opus-4-8" },
|
|
699
|
+
},
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
await expect(
|
|
703
|
+
getConfiguredProvider("mainAgent", { overrideProfile: "ghost" }),
|
|
704
|
+
).rejects.toMatchObject({ reason: "not_found", connectionName: "vellum" });
|
|
705
|
+
});
|
|
706
|
+
});
|
|
@@ -220,6 +220,63 @@ describe("preflightResolvedConfig", () => {
|
|
|
220
220
|
).resolves.toBeUndefined();
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
+
test("an explicit connection outranks an entry-name provider, matching dispatch", async () => {
|
|
224
|
+
// A healthy entry row must not mask the row the request actually uses.
|
|
225
|
+
connectionsByName["anthropic-work"] = {
|
|
226
|
+
name: "anthropic-work",
|
|
227
|
+
provider: "anthropic",
|
|
228
|
+
auth: {
|
|
229
|
+
type: "api_key",
|
|
230
|
+
credential: "credential/anthropic-work/api_key",
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
secureKeys["credential/anthropic-work/api_key"] = "healthy";
|
|
234
|
+
|
|
235
|
+
const err = await preflightError(
|
|
236
|
+
resolved({
|
|
237
|
+
provider: "anthropic-work",
|
|
238
|
+
provider_connection: "deleted-row",
|
|
239
|
+
model: "claude-opus-4-8",
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
expect(err?.reason).toBe("not_found");
|
|
243
|
+
expect(err?.connectionName).toBe("deleted-row");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("a chatgpt-identity subscription row preflights the same way", async () => {
|
|
247
|
+
// Migration 366 stamps the row with provider "chatgpt"; preflight judges
|
|
248
|
+
// its subscription credential, not a provider equality with the openai
|
|
249
|
+
// upstream.
|
|
250
|
+
connectionsByName["chatgpt-subscription"] = {
|
|
251
|
+
name: "chatgpt-subscription",
|
|
252
|
+
provider: "chatgpt",
|
|
253
|
+
auth: {
|
|
254
|
+
type: "oauth_subscription",
|
|
255
|
+
credential: "credential/chatgpt/access_token",
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
const missing = await preflightError(
|
|
259
|
+
resolved({
|
|
260
|
+
provider: "chatgpt",
|
|
261
|
+
provider_connection: "",
|
|
262
|
+
model: "gpt-5.5",
|
|
263
|
+
}),
|
|
264
|
+
);
|
|
265
|
+
expect(missing?.reason).toBe("missing_credential");
|
|
266
|
+
|
|
267
|
+
secureKeys["credential/chatgpt/access_token"] = "tok";
|
|
268
|
+
await expect(
|
|
269
|
+
preflightResolvedConfig(
|
|
270
|
+
resolved({
|
|
271
|
+
provider: "chatgpt",
|
|
272
|
+
provider_connection: "",
|
|
273
|
+
model: "gpt-5.5",
|
|
274
|
+
}),
|
|
275
|
+
{},
|
|
276
|
+
),
|
|
277
|
+
).resolves.toBeUndefined();
|
|
278
|
+
});
|
|
279
|
+
|
|
223
280
|
test("a chatgpt identity with no subscription row throws not_found", async () => {
|
|
224
281
|
const err = await preflightError(
|
|
225
282
|
resolved({
|
|
@@ -337,7 +337,7 @@ describe("RetryProvider — callSite resolution", () => {
|
|
|
337
337
|
expect(config.max_tokens).toBe(expected.maxTokens as number);
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
-
test("live-voice
|
|
340
|
+
test("latency-sensitive live-voice call sites reach OpenAI with reasoning off", async () => {
|
|
341
341
|
// The `latency-optimized` pin's whole value is time-to-first-token, and on
|
|
342
342
|
// an OpenAI upstream that depends entirely on disabled thinking being
|
|
343
343
|
// encoded as `effort: "none"`. OpenAI-compatible APIs reason by default
|
|
@@ -351,7 +351,10 @@ describe("RetryProvider — callSite resolution", () => {
|
|
|
351
351
|
setLlmConfig({});
|
|
352
352
|
|
|
353
353
|
const expected = CODE_DEFAULT_PROFILE_ENTRIES["latency-optimized"];
|
|
354
|
-
for (const callSite of [
|
|
354
|
+
for (const callSite of [
|
|
355
|
+
"voiceFrontDoor",
|
|
356
|
+
"voiceProgressNarration",
|
|
357
|
+
] as const) {
|
|
355
358
|
let seen: SendMessageOptions | undefined;
|
|
356
359
|
const wrapped = new RetryProvider(
|
|
357
360
|
makeProvider("openai", (options) => {
|
|
@@ -37,8 +37,13 @@ import {
|
|
|
37
37
|
isConnectionCompatibleWithModel,
|
|
38
38
|
} from "./connection-model-compat.js";
|
|
39
39
|
import {
|
|
40
|
+
connectionProviderKind,
|
|
40
41
|
ConnectionResolutionError,
|
|
42
|
+
dispatchProviderResolvable,
|
|
43
|
+
expectedVendorProvider,
|
|
41
44
|
isManagedConnectionRoute,
|
|
45
|
+
resolveEntryConnectionName,
|
|
46
|
+
resolveEntryProviderKind,
|
|
42
47
|
resolveRoutingIdentity,
|
|
43
48
|
tryResolveProviderForConnectionName,
|
|
44
49
|
} from "./connection-resolution.js";
|
|
@@ -112,7 +117,7 @@ export class CallSiteRoutingProvider implements Provider {
|
|
|
112
117
|
*/
|
|
113
118
|
private readonly resolveByConnection: (
|
|
114
119
|
connectionName: string,
|
|
115
|
-
expectedProvider: string,
|
|
120
|
+
expectedProvider: string | undefined,
|
|
116
121
|
model: string | undefined,
|
|
117
122
|
) => Promise<Provider | null>,
|
|
118
123
|
private readonly defaultRouteAttribution?: ProviderRouteAttribution,
|
|
@@ -203,9 +208,17 @@ export class CallSiteRoutingProvider implements Provider {
|
|
|
203
208
|
forceOverrideProfile: options?.config?.forceOverrideProfile,
|
|
204
209
|
selectionSeed: options?.config?.selectionSeed,
|
|
205
210
|
});
|
|
211
|
+
// Capability follows the same row dispatch selects: an explicit
|
|
212
|
+
// provider_connection wins, then an entry-name label's row, then the
|
|
213
|
+
// resolved provider itself. Kept in this order so a label paired with a
|
|
214
|
+
// conflicting connection cannot enable a capability the dispatched
|
|
215
|
+
// transport lacks.
|
|
216
|
+
const routedKind = resolved.provider_connection
|
|
217
|
+
? connectionProviderKind(resolved.provider_connection, resolved.model)
|
|
218
|
+
: resolveEntryProviderKind(resolved.provider, resolved.model);
|
|
206
219
|
return shouldUseNativeWebSearch(
|
|
207
220
|
getConfig(),
|
|
208
|
-
resolved.provider,
|
|
221
|
+
routedKind ?? resolved.provider,
|
|
209
222
|
resolved.model,
|
|
210
223
|
);
|
|
211
224
|
}
|
|
@@ -256,6 +269,7 @@ export class CallSiteRoutingProvider implements Provider {
|
|
|
256
269
|
overrideProfile,
|
|
257
270
|
forceOverrideProfile,
|
|
258
271
|
selectionSeed,
|
|
272
|
+
isResolvableProvider: dispatchProviderResolvable,
|
|
259
273
|
},
|
|
260
274
|
);
|
|
261
275
|
|
|
@@ -273,6 +287,16 @@ export class CallSiteRoutingProvider implements Provider {
|
|
|
273
287
|
)?.connectionName;
|
|
274
288
|
}
|
|
275
289
|
|
|
290
|
+
// An entry-name provider IS the connection name: the label points at a
|
|
291
|
+
// row, and the row's own provider drives dispatch, so no expected
|
|
292
|
+
// provider is threaded (the label is not a vendor).
|
|
293
|
+
const entryRoute = connectionName
|
|
294
|
+
? null
|
|
295
|
+
: resolveEntryConnectionName(resolved.provider);
|
|
296
|
+
if (entryRoute) {
|
|
297
|
+
connectionName = entryRoute;
|
|
298
|
+
}
|
|
299
|
+
|
|
276
300
|
// When no connection is set, auto-resolve one for the resolved provider —
|
|
277
301
|
// including when the provider matches the default's name. The default
|
|
278
302
|
// transport may ride the managed (platform-billed) connection, so reusing
|
|
@@ -301,9 +325,12 @@ export class CallSiteRoutingProvider implements Provider {
|
|
|
301
325
|
}
|
|
302
326
|
|
|
303
327
|
if (connectionName) {
|
|
328
|
+
// The vendor guard covers both the entry route and a config carrying
|
|
329
|
+
// an entry label alongside an explicit provider_connection: a label
|
|
330
|
+
// that is not a vendor never threads into the row-equality check.
|
|
304
331
|
const connectionProvider = await this.resolveByConnection(
|
|
305
332
|
connectionName,
|
|
306
|
-
resolved.provider,
|
|
333
|
+
expectedVendorProvider(resolved.provider, resolved.model),
|
|
307
334
|
resolved.model,
|
|
308
335
|
);
|
|
309
336
|
if (connectionProvider) {
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
resolveCallSiteConfig,
|
|
33
33
|
selectWinningProfile,
|
|
34
34
|
} from "../config/llm-resolver.js";
|
|
35
|
+
import { unknownLlmProviderIssue } from "../config/schemas/llm.js";
|
|
35
36
|
import { getDb } from "../persistence/db-connection.js";
|
|
36
37
|
import { credentialKey } from "../security/credential-key.js";
|
|
37
38
|
import { ProviderNotConfiguredError } from "../util/errors.js";
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
describeSubscriptionModelIncompatibility,
|
|
41
42
|
isConnectionCompatibleWithModel,
|
|
42
43
|
} from "./connection-model-compat.js";
|
|
44
|
+
import { VALID_CONNECTION_PROVIDERS } from "./inference/auth.js";
|
|
43
45
|
import {
|
|
44
46
|
canonicalVellumConnection,
|
|
45
47
|
getConnection,
|
|
@@ -55,6 +57,7 @@ import {
|
|
|
55
57
|
} from "./routing-identity.js";
|
|
56
58
|
import type { Provider } from "./types.js";
|
|
57
59
|
import {
|
|
60
|
+
getManagedUpstream,
|
|
58
61
|
isVellumManagedConnection,
|
|
59
62
|
MANAGED_ROUTABLE_PROVIDERS,
|
|
60
63
|
VELLUM_MANAGED_CONNECTION_NAME,
|
|
@@ -64,6 +67,132 @@ export { ConnectionResolutionError, resolveRoutingIdentity };
|
|
|
64
67
|
|
|
65
68
|
const log = getLogger("providers/connection-resolution");
|
|
66
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a provider label that names a connection row (an entry) to that
|
|
72
|
+
* row's name. Returns null for catalog providers and routing identities
|
|
73
|
+
* (those translate through their own rules) and for labels naming no row.
|
|
74
|
+
*
|
|
75
|
+
* The write surfaces reject entry-name providers until the entries model
|
|
76
|
+
* enables them, so a config carrying one reaches dispatch only through a
|
|
77
|
+
* hand edit today and through the collapse migration later; translating
|
|
78
|
+
* here makes both route explainably instead of failing as an unknown
|
|
79
|
+
* provider.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveEntryConnectionName(
|
|
82
|
+
provider: string | undefined,
|
|
83
|
+
): string | null {
|
|
84
|
+
if (!provider || VALID_CONNECTION_PROVIDERS.includes(provider)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return getConnection(getDb(), provider) ? provider : null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Write-surface membership for a PROFILE provider value: the known vendor
|
|
96
|
+
* and identity set, plus existing connection entry rows. Fail-closed on DB
|
|
97
|
+
* unavailability (an unverifiable entry name is rejected; the write is
|
|
98
|
+
* retryable), unlike the selection-time predicate below, which is
|
|
99
|
+
* fail-open so a DB blip never heals away a valid profile. Call-site
|
|
100
|
+
* fragments keep vendor-only membership: overrides become model-only with
|
|
101
|
+
* the entries demolition, so entries must not leak into them meanwhile.
|
|
102
|
+
*/
|
|
103
|
+
export function writableProfileProviderIssue(provider: string): string | null {
|
|
104
|
+
const issue = unknownLlmProviderIssue(provider);
|
|
105
|
+
if (issue === null) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
if (getConnection(getDb(), provider) != null) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// Unverifiable: fall through to the rejection.
|
|
114
|
+
}
|
|
115
|
+
return `Invalid provider "${provider}". Use a known provider or the name of an existing connection.`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Selection-time predicate for `ResolveCallSiteOpts.isResolvableProvider`:
|
|
120
|
+
* a provider value dispatches when it is a known vendor/identity or names a
|
|
121
|
+
* connection entry row. Permissive on DB unavailability so a transient blip
|
|
122
|
+
* never heals away a valid entry profile; dispatch soft-fails on its own.
|
|
123
|
+
*/
|
|
124
|
+
export function dispatchProviderResolvable(provider: string): boolean {
|
|
125
|
+
if (VALID_CONNECTION_PROVIDERS.includes(provider)) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
return getConnection(getDb(), provider) != null;
|
|
130
|
+
} catch {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The vendor a resolved provider value expects its connection row to serve:
|
|
137
|
+
* catalog ids and routing identities pass through, and an entry-name label
|
|
138
|
+
* resolves to its row's dispatchable kind, so a config carrying both an
|
|
139
|
+
* entry label and a `provider_connection` is held to the label's kind by
|
|
140
|
+
* the row-equality check (a conflicting row mismatches explainably or
|
|
141
|
+
* auto-recovers to a matching one). A label naming no row yields undefined.
|
|
142
|
+
*/
|
|
143
|
+
export function expectedVendorProvider(
|
|
144
|
+
provider: string | undefined,
|
|
145
|
+
model: string | undefined,
|
|
146
|
+
): string | undefined {
|
|
147
|
+
if (!provider) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
if (VALID_CONNECTION_PROVIDERS.includes(provider)) {
|
|
151
|
+
return provider;
|
|
152
|
+
}
|
|
153
|
+
return resolveEntryProviderKind(provider, model) ?? undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The dispatchable provider kind behind a connection row, or null when the
|
|
158
|
+
* row is missing or its kind cannot be derived. Identity-kind rows derive
|
|
159
|
+
* their upstream the way the identities themselves do. Sync and
|
|
160
|
+
* best-effort so capability probes can share dispatch's translation
|
|
161
|
+
* without replaying its async resolution.
|
|
162
|
+
*/
|
|
163
|
+
export function connectionProviderKind(
|
|
164
|
+
connectionName: string,
|
|
165
|
+
model: string | undefined,
|
|
166
|
+
): string | null {
|
|
167
|
+
try {
|
|
168
|
+
const row = getConnection(getDb(), connectionName);
|
|
169
|
+
if (!row) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (isVellumManagedConnection(row)) {
|
|
173
|
+
return model ? getManagedUpstream(model) : null;
|
|
174
|
+
}
|
|
175
|
+
if (row.provider === "chatgpt") {
|
|
176
|
+
return "openai";
|
|
177
|
+
}
|
|
178
|
+
return row.provider;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The dispatchable provider kind behind an entry-name label, or null when
|
|
186
|
+
* the label is not an entry.
|
|
187
|
+
*/
|
|
188
|
+
export function resolveEntryProviderKind(
|
|
189
|
+
provider: string | undefined,
|
|
190
|
+
model: string | undefined,
|
|
191
|
+
): string | null {
|
|
192
|
+
const entryName = resolveEntryConnectionName(provider);
|
|
193
|
+
return entryName ? connectionProviderKind(entryName, model) : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
67
196
|
/**
|
|
68
197
|
* Resolve a Provider through a named `provider_connection`.
|
|
69
198
|
*
|
|
@@ -152,18 +281,36 @@ export async function tryResolveProviderForConnectionName(
|
|
|
152
281
|
// fall back to the default provider.
|
|
153
282
|
const isVellum = isVellumManagedConnection(connection);
|
|
154
283
|
if (isVellum && !expectedProvider) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
284
|
+
// An entry-name route carries no declared provider (the label is the
|
|
285
|
+
// row's name, not a vendor); a vellum-kind row derives its upstream
|
|
286
|
+
// from the model, same as the vellum identity itself.
|
|
287
|
+
expectedProvider = model
|
|
288
|
+
? (getManagedUpstream(model) ?? undefined)
|
|
289
|
+
: undefined;
|
|
290
|
+
if (!expectedProvider) {
|
|
291
|
+
throw new ConnectionResolutionError(
|
|
292
|
+
connectionName,
|
|
293
|
+
"provider_mismatch",
|
|
294
|
+
`provider_connection "${connectionName}" is the provider-agnostic Vellum-managed connection but the resolving profile declared no provider — set the profile's provider so the upstream can be selected`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
160
297
|
}
|
|
161
298
|
const isVellumRoute =
|
|
162
299
|
isVellum &&
|
|
163
300
|
!!expectedProvider &&
|
|
164
301
|
MANAGED_ROUTABLE_PROVIDERS.has(expectedProvider);
|
|
302
|
+
// The ChatGPT-subscription row stores the "chatgpt" routing identity in
|
|
303
|
+
// its provider column, so the equality never holds for its openai
|
|
304
|
+
// upstream. The identity dispatches with the upstream threaded as
|
|
305
|
+
// `providerOverride`, mirroring the vellum sentinel; the upstream is
|
|
306
|
+
// always openai (`resolveRoutingIdentity`). Any other declared provider
|
|
307
|
+
// on a chatgpt row is a genuine mismatch and falls through below.
|
|
308
|
+
const isChatgptRoute =
|
|
309
|
+
connection.provider === "chatgpt" &&
|
|
310
|
+
(expectedProvider === undefined || expectedProvider === "openai");
|
|
165
311
|
if (
|
|
166
312
|
!isVellumRoute &&
|
|
313
|
+
!isChatgptRoute &&
|
|
167
314
|
expectedProvider &&
|
|
168
315
|
connection.provider !== expectedProvider
|
|
169
316
|
) {
|
|
@@ -225,7 +372,11 @@ export async function tryResolveProviderForConnectionName(
|
|
|
225
372
|
try {
|
|
226
373
|
const provider = await resolveProviderFromConnection(connection, config, {
|
|
227
374
|
model,
|
|
228
|
-
providerOverride: isVellumRoute
|
|
375
|
+
providerOverride: isVellumRoute
|
|
376
|
+
? expectedProvider
|
|
377
|
+
: isChatgptRoute
|
|
378
|
+
? "openai"
|
|
379
|
+
: undefined,
|
|
229
380
|
});
|
|
230
381
|
return attachProviderRoute(provider, connection);
|
|
231
382
|
} catch (err) {
|
|
@@ -324,7 +475,9 @@ async function resolveThroughPlatform(
|
|
|
324
475
|
export async function resolveDefaultProvider(
|
|
325
476
|
config: ProvidersConfig,
|
|
326
477
|
): Promise<Provider | null> {
|
|
327
|
-
const resolved = resolveCallSiteConfig("mainAgent", config.llm
|
|
478
|
+
const resolved = resolveCallSiteConfig("mainAgent", config.llm, {
|
|
479
|
+
isResolvableProvider: dispatchProviderResolvable,
|
|
480
|
+
});
|
|
328
481
|
let connectionName = resolved.provider_connection;
|
|
329
482
|
// A routing-identity provider names its own connection row; the
|
|
330
483
|
// provider-keyed auto-resolve scan below cannot find it ("chatgpt" rows
|
|
@@ -335,6 +488,19 @@ export async function resolveDefaultProvider(
|
|
|
335
488
|
resolved.model,
|
|
336
489
|
)?.connectionName;
|
|
337
490
|
}
|
|
491
|
+
// An entry-name provider IS the connection name: the label points at a
|
|
492
|
+
// row, and the row's own provider drives dispatch.
|
|
493
|
+
const entryName = connectionName
|
|
494
|
+
? null
|
|
495
|
+
: resolveEntryConnectionName(resolved.provider);
|
|
496
|
+
if (entryName) {
|
|
497
|
+
return tryResolveProviderForConnectionName(
|
|
498
|
+
entryName,
|
|
499
|
+
config,
|
|
500
|
+
undefined,
|
|
501
|
+
resolved.model,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
338
504
|
if (!connectionName) {
|
|
339
505
|
// The merged config has no provider_connection — the profile likely set
|
|
340
506
|
// provider without a connection ("Any active" selection), and the merge
|
|
@@ -387,7 +553,7 @@ export async function resolveDefaultProvider(
|
|
|
387
553
|
return tryResolveProviderForConnectionName(
|
|
388
554
|
connectionName,
|
|
389
555
|
config,
|
|
390
|
-
resolved.provider,
|
|
556
|
+
expectedVendorProvider(resolved.provider, resolved.model),
|
|
391
557
|
resolved.model,
|
|
392
558
|
);
|
|
393
559
|
}
|
|
@@ -423,9 +589,21 @@ export async function preflightResolvedConfig(
|
|
|
423
589
|
// upstream; an unroutable vellum model throws here — it is statically
|
|
424
590
|
// detectable, exactly what preflight exists to surface.
|
|
425
591
|
const identity = resolveRoutingIdentity(resolved.provider, resolved.model);
|
|
426
|
-
|
|
592
|
+
// An entry-name provider IS the connection name, and the row's kind is
|
|
593
|
+
// what the checks below judge against (same translation dispatch uses).
|
|
594
|
+
// Precedence matches dispatch exactly: an explicit provider_connection
|
|
595
|
+
// wins over the entry name, so preflight judges the row the request
|
|
596
|
+
// actually uses rather than a healthy entry the request ignores.
|
|
597
|
+
const entryName = identity
|
|
598
|
+
? null
|
|
599
|
+
: resolveEntryConnectionName(resolved.provider);
|
|
600
|
+
const provider = identity
|
|
601
|
+
? identity.expectedProvider
|
|
602
|
+
: entryName
|
|
603
|
+
? (connectionProviderKind(entryName, resolved.model) ?? resolved.provider)
|
|
604
|
+
: resolved.provider;
|
|
427
605
|
const connectionName =
|
|
428
|
-
identity?.connectionName ?? resolved.provider_connection;
|
|
606
|
+
identity?.connectionName ?? resolved.provider_connection ?? entryName;
|
|
429
607
|
if (!connectionName) {
|
|
430
608
|
return;
|
|
431
609
|
}
|
|
@@ -512,7 +690,12 @@ export async function preflightResolvedConfig(
|
|
|
512
690
|
}
|
|
513
691
|
return;
|
|
514
692
|
}
|
|
515
|
-
|
|
693
|
+
// The ChatGPT-subscription row stores the "chatgpt" identity while its
|
|
694
|
+
// upstream is openai; the credential-presence switch below is the right
|
|
695
|
+
// preflight for it (the subscription token is its credential).
|
|
696
|
+
const isChatgptRow =
|
|
697
|
+
connection.provider === "chatgpt" && provider === "openai";
|
|
698
|
+
if (!isChatgptRow && connection.provider !== provider) {
|
|
516
699
|
throw new ConnectionResolutionError(
|
|
517
700
|
connectionName,
|
|
518
701
|
"provider_mismatch",
|