@vellumai/assistant 0.4.53 → 0.4.54
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/bun.lock +62 -349
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/keychain-broker.md +94 -29
- package/docs/architecture/security.md +2 -2
- package/knip.json +7 -29
- package/package.json +2 -9
- package/src/__tests__/agent-loop.test.ts +1 -1
- package/src/__tests__/app-git-history.test.ts +0 -2
- package/src/__tests__/app-git-service.test.ts +1 -6
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/avatar-e2e.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +1 -6
- package/src/__tests__/call-domain.test.ts +0 -1
- package/src/__tests__/call-routes-http.test.ts +0 -1
- package/src/__tests__/channel-guardian.test.ts +4 -4
- package/src/__tests__/channel-readiness-routes.test.ts +0 -1
- package/src/__tests__/channel-readiness-service.test.ts +0 -1
- package/src/__tests__/checker.test.ts +13 -11
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -1
- package/src/__tests__/claude-code-tool-profiles.test.ts +1 -2
- package/src/__tests__/config-loader-backfill.test.ts +0 -3
- package/src/__tests__/config-schema.test.ts +3 -9
- package/src/__tests__/config-watcher.test.ts +11 -3
- package/src/__tests__/credential-broker-browser-fill.test.ts +27 -24
- package/src/__tests__/credential-broker-server-use.test.ts +60 -24
- package/src/__tests__/credential-security-e2e.test.ts +1 -6
- package/src/__tests__/credential-security-invariants.test.ts +13 -8
- package/src/__tests__/credential-vault-unit.test.ts +28 -12
- package/src/__tests__/credential-vault.test.ts +40 -28
- package/src/__tests__/credentials-cli.test.ts +1 -21
- package/src/__tests__/email-invite-adapter.test.ts +0 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +3 -3
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -79
- package/src/__tests__/gateway-only-enforcement.test.ts +1 -21
- package/src/__tests__/guardian-action-conversation-turn.test.ts +8 -8
- package/src/__tests__/guardian-action-late-reply.test.ts +13 -14
- package/src/__tests__/guardian-action-store.test.ts +0 -57
- package/src/__tests__/guardian-outbound-http.test.ts +1 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -3
- package/src/__tests__/hooks-blocking.test.ts +1 -1
- package/src/__tests__/hooks-config.test.ts +5 -29
- package/src/__tests__/hooks-discovery.test.ts +1 -1
- package/src/__tests__/hooks-integration.test.ts +1 -1
- package/src/__tests__/hooks-manager.test.ts +1 -1
- package/src/__tests__/hooks-runner.test.ts +1 -23
- package/src/__tests__/hooks-settings.test.ts +1 -1
- package/src/__tests__/hooks-templates.test.ts +1 -1
- package/src/__tests__/integration-status.test.ts +0 -1
- package/src/__tests__/invite-routes-http.test.ts +0 -3
- package/src/__tests__/llm-usage-store.test.ts +50 -0
- package/src/__tests__/managed-proxy-context.test.ts +41 -41
- package/src/__tests__/media-generate-image.test.ts +2 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -6
- package/src/__tests__/memory-regressions.experimental.test.ts +4 -4
- package/src/__tests__/memory-regressions.test.ts +27 -27
- package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
- package/src/__tests__/memory-upsert-concurrency.test.ts +4 -4
- package/src/__tests__/notification-decision-fallback.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +1 -4
- package/src/__tests__/oauth-store.test.ts +1 -3
- package/src/__tests__/openai-provider.test.ts +7 -7
- package/src/__tests__/platform.test.ts +14 -4
- package/src/__tests__/pricing.test.ts +0 -223
- package/src/__tests__/provider-commit-message-generator.test.ts +1 -4
- package/src/__tests__/provider-fail-open-selection.test.ts +58 -54
- package/src/__tests__/provider-managed-proxy-integration.test.ts +63 -63
- package/src/__tests__/provider-registry-ollama.test.ts +3 -3
- package/src/__tests__/public-ingress-urls.test.ts +1 -1
- package/src/__tests__/registry.test.ts +3 -103
- package/src/__tests__/script-proxy-injection-runtime.test.ts +2 -7
- package/src/__tests__/secret-onetime-send.test.ts +1 -6
- package/src/__tests__/secret-routes-managed-proxy.test.ts +6 -13
- package/src/__tests__/secure-keys.test.ts +241 -229
- package/src/__tests__/session-abort-tool-results.test.ts +0 -1
- package/src/__tests__/session-confirmation-signals.test.ts +0 -1
- package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -7
- package/src/__tests__/session-pre-run-repair.test.ts +0 -1
- package/src/__tests__/session-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/session-queue.test.ts +2 -4
- package/src/__tests__/session-slash-known.test.ts +0 -1
- package/src/__tests__/session-slash-queue.test.ts +0 -1
- package/src/__tests__/session-slash-unknown.test.ts +0 -1
- package/src/__tests__/session-workspace-injection.test.ts +0 -1
- package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
- package/src/__tests__/slack-channel-config.test.ts +1 -7
- package/src/__tests__/swarm-recursion.test.ts +0 -1
- package/src/__tests__/swarm-session-integration.test.ts +0 -1
- package/src/__tests__/swarm-tool.test.ts +0 -1
- package/src/__tests__/task-compiler.test.ts +1 -1
- package/src/__tests__/test-support/browser-skill-harness.ts +0 -18
- package/src/__tests__/test-support/computer-use-skill-harness.ts +0 -23
- package/src/__tests__/tool-executor.test.ts +1 -1
- package/src/__tests__/trust-store.test.ts +3 -82
- package/src/__tests__/twilio-config.test.ts +0 -1
- package/src/__tests__/twilio-provider.test.ts +0 -5
- package/src/__tests__/twilio-routes.test.ts +0 -1
- package/src/__tests__/usage-cache-backfill-migration.test.ts +10 -10
- package/src/calls/guardian-question-copy.ts +1 -1
- package/src/cli/commands/doctor.ts +10 -34
- package/src/cli/commands/memory.ts +3 -5
- package/src/cli/commands/sessions.ts +1 -1
- package/src/cli/commands/usage.ts +359 -0
- package/src/cli/http-client.ts +22 -12
- package/src/cli/program.ts +2 -0
- package/src/cli/reference.ts +1 -0
- package/src/cli.ts +251 -181
- package/src/config/assistant-feature-flags.ts +0 -7
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/bundled-skills/claude-code/SKILL.md +1 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +1 -1
- package/src/config/bundled-skills/gmail/SKILL.md +0 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
- package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +0 -1
- package/src/config/bundled-skills/sequences/SKILL.md +0 -1
- package/src/config/env.ts +13 -0
- package/src/config/feature-flag-registry.json +9 -41
- package/src/config/schemas/security.ts +1 -2
- package/src/config/skills.ts +1 -1
- package/src/contacts/contact-store.ts +0 -50
- package/src/daemon/approved-devices-store.ts +0 -44
- package/src/daemon/classifier.ts +1 -1
- package/src/daemon/config-watcher.ts +12 -6
- package/src/daemon/handlers/config-model.ts +1 -1
- package/src/daemon/handlers/sessions.ts +4 -116
- package/src/daemon/handlers/skills.ts +1 -1
- package/src/daemon/lifecycle.ts +13 -15
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/server.ts +19 -3
- package/src/daemon/session-slash.ts +2 -2
- package/src/daemon/shutdown-handlers.ts +15 -0
- package/src/daemon/watch-handler.ts +2 -2
- package/src/email/guardrails.ts +1 -1
- package/src/email/service.ts +0 -5
- package/src/hooks/templates.ts +1 -1
- package/src/media/app-icon-generator.ts +2 -2
- package/src/media/avatar-router.ts +2 -2
- package/src/media/gemini-image-service.ts +5 -5
- package/src/memory/admin.ts +2 -2
- package/src/memory/app-git-service.ts +0 -7
- package/src/memory/conversation-crud.ts +1 -1
- package/src/memory/conversation-title-service.ts +2 -2
- package/src/memory/embedding-backend.ts +30 -26
- package/src/memory/external-conversation-store.ts +0 -30
- package/src/memory/guardian-action-store.ts +0 -31
- package/src/memory/guardian-approvals.ts +1 -56
- package/src/memory/indexer.ts +4 -3
- package/src/memory/items-extractor.ts +1 -1
- package/src/memory/job-handlers/backfill.ts +5 -2
- package/src/memory/job-handlers/index-maintenance.ts +2 -2
- package/src/memory/job-handlers/media-processing.ts +2 -2
- package/src/memory/job-handlers/summarization.ts +1 -1
- package/src/memory/job-utils.ts +1 -2
- package/src/memory/jobs-worker.ts +2 -2
- package/src/memory/llm-usage-store.ts +57 -11
- package/src/memory/media-store.ts +4 -535
- package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +2 -2
- package/src/memory/migrations/110-channel-guardian.ts +0 -1
- package/src/memory/published-pages-store.ts +0 -83
- package/src/memory/qdrant-circuit-breaker.ts +0 -8
- package/src/memory/retriever.ts +1 -1
- package/src/memory/search/semantic.ts +1 -8
- package/src/memory/shared-app-links-store.ts +0 -15
- package/src/messaging/registry.ts +0 -5
- package/src/messaging/style-analyzer.ts +1 -1
- package/src/notifications/copy-composer.ts +5 -13
- package/src/notifications/decision-engine.ts +2 -2
- package/src/notifications/deliveries-store.ts +0 -39
- package/src/notifications/guardian-question-mode.ts +6 -10
- package/src/notifications/preference-extractor.ts +1 -1
- package/src/oauth/byo-connection.test.ts +29 -20
- package/src/oauth/provider-behaviors.ts +1 -1
- package/src/permissions/checker.ts +1 -1
- package/src/permissions/shell-identity.ts +0 -5
- package/src/permissions/trust-store.ts +0 -37
- package/src/prompts/system-prompt.ts +3 -3
- package/src/providers/managed-proxy/constants.ts +8 -10
- package/src/providers/managed-proxy/context.ts +14 -9
- package/src/providers/provider-send-message.ts +4 -52
- package/src/providers/registry.ts +16 -50
- package/src/runtime/actor-token-store.ts +0 -23
- package/src/runtime/http-router.ts +5 -1
- package/src/runtime/http-server.ts +101 -4
- package/src/runtime/invite-instruction-generator.ts +25 -51
- package/src/runtime/invite-service.ts +0 -20
- package/src/runtime/routes/attachment-routes.ts +1 -1
- package/src/runtime/routes/brain-graph-routes.ts +1 -1
- package/src/runtime/routes/call-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +32 -11
- package/src/runtime/routes/debug-routes.ts +1 -1
- package/src/runtime/routes/diagnostics-routes.ts +2 -2
- package/src/runtime/routes/documents-routes.ts +3 -3
- package/src/runtime/routes/global-search-routes.ts +1 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +0 -20
- package/src/runtime/routes/guardian-refresh-routes.ts +0 -20
- package/src/runtime/routes/secret-routes.ts +4 -4
- package/src/runtime/routes/trust-rules-routes.ts +1 -1
- package/src/security/credential-backend.ts +148 -0
- package/src/security/oauth2.ts +1 -1
- package/src/security/secret-allowlist.ts +1 -1
- package/src/security/secure-keys.ts +98 -160
- package/src/security/token-manager.ts +0 -7
- package/src/sequence/guardrails.ts +0 -4
- package/src/sequence/store.ts +1 -20
- package/src/sequence/types.ts +1 -36
- package/src/signals/cancel.ts +69 -0
- package/src/signals/conversation-undo.ts +127 -0
- package/src/signals/trust-rule.ts +174 -0
- package/src/skills/clawhub.ts +5 -5
- package/src/skills/managed-store.ts +4 -4
- package/src/telemetry/usage-telemetry-reporter.test.ts +366 -0
- package/src/telemetry/usage-telemetry-reporter.ts +181 -0
- package/src/tools/claude-code/claude-code.ts +2 -2
- package/src/tools/credentials/vault.ts +8 -4
- package/src/tools/memory/handlers.test.ts +24 -26
- package/src/tools/memory/handlers.ts +1 -13
- package/src/tools/registry.ts +5 -100
- package/src/tools/terminal/parser.ts +34 -4
- package/src/tools/tool-manifest.ts +0 -10
- package/src/usage/actors.ts +0 -12
- package/src/util/canonicalize-identity.ts +0 -9
- package/src/util/errors.ts +0 -3
- package/src/util/platform.ts +24 -7
- package/src/util/pricing.ts +0 -38
- package/src/watcher/constants.ts +0 -7
- package/src/watcher/providers/linear.ts +1 -1
- package/src/work-items/work-item-store.ts +4 -4
- package/src/workspace/commit-message-provider.ts +1 -1
- package/src/workspace/git-service.ts +44 -1
- package/src/workspace/provider-commit-message-generator.ts +1 -1
- package/src/__tests__/fixtures/proxy-fixtures.ts +0 -147
- package/src/browser-extension-relay/client.ts +0 -155
- package/src/contacts/index.ts +0 -18
- package/src/daemon/tls-certs.ts +0 -270
- package/src/errors.ts +0 -41
- package/src/events/index.ts +0 -18
- package/src/followups/index.ts +0 -10
- package/src/playbooks/index.ts +0 -10
- package/src/runtime/auth/index.ts +0 -44
- package/src/tasks/candidate-store.ts +0 -95
- package/src/tools/browser/api-map.ts +0 -313
- package/src/tools/browser/auto-navigate.ts +0 -469
- package/src/tools/browser/headless-browser.ts +0 -590
- package/src/tools/browser/recording-store.ts +0 -75
- package/src/tools/computer-use/registry.ts +0 -21
- package/src/tools/tasks/index.ts +0 -27
|
@@ -21,7 +21,7 @@ mock.module("../config/env.js", () => ({
|
|
|
21
21
|
const actualSecureKeys = await import("../security/secure-keys.js");
|
|
22
22
|
mock.module("../security/secure-keys.js", () => ({
|
|
23
23
|
...actualSecureKeys,
|
|
24
|
-
|
|
24
|
+
getSecureKeyAsync: async (key: string) => {
|
|
25
25
|
if (key === credentialKey("vellum", "assistant_api_key")) {
|
|
26
26
|
return mockAssistantApiKey || null;
|
|
27
27
|
}
|
|
@@ -31,6 +31,7 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
31
31
|
|
|
32
32
|
import {
|
|
33
33
|
getFailoverProvider,
|
|
34
|
+
getProviderRoutingSource,
|
|
34
35
|
initializeProviders,
|
|
35
36
|
listProviders,
|
|
36
37
|
resolveProviderSelection,
|
|
@@ -44,50 +45,50 @@ import { ProviderNotConfiguredError } from "../util/errors.js";
|
|
|
44
45
|
*/
|
|
45
46
|
|
|
46
47
|
/** Initialize registry with anthropic + openai for most tests. */
|
|
47
|
-
function setupTwoProviders() {
|
|
48
|
+
async function setupTwoProviders() {
|
|
48
49
|
mockProviderKeys = { anthropic: "test-key", openai: "test-key" };
|
|
49
|
-
initializeProviders({
|
|
50
|
+
await initializeProviders({
|
|
50
51
|
provider: "anthropic",
|
|
51
52
|
model: "test-model",
|
|
52
53
|
});
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/** Initialize registry with no providers (empty keys, non-registerable primary). */
|
|
56
|
-
function setupNoProviders() {
|
|
57
|
+
async function setupNoProviders() {
|
|
57
58
|
mockProviderKeys = {};
|
|
58
|
-
initializeProviders({
|
|
59
|
+
await initializeProviders({
|
|
59
60
|
provider: "gemini",
|
|
60
61
|
model: "test-model",
|
|
61
62
|
});
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
describe("resolveProviderSelection", () => {
|
|
65
|
-
test("configured primary available → selected as primary", () => {
|
|
66
|
-
setupTwoProviders();
|
|
66
|
+
test("configured primary available → selected as primary", async () => {
|
|
67
|
+
await setupTwoProviders();
|
|
67
68
|
const result = resolveProviderSelection("anthropic", ["openai"]);
|
|
68
69
|
expect(result.selectedPrimary).toBe("anthropic");
|
|
69
70
|
expect(result.usedFallbackPrimary).toBe(false);
|
|
70
71
|
expect(result.availableProviders).toEqual(["anthropic", "openai"]);
|
|
71
72
|
});
|
|
72
73
|
|
|
73
|
-
test("configured primary unavailable + alternate available → alternate selected", () => {
|
|
74
|
-
setupTwoProviders();
|
|
74
|
+
test("configured primary unavailable + alternate available → alternate selected", async () => {
|
|
75
|
+
await setupTwoProviders();
|
|
75
76
|
const result = resolveProviderSelection("gemini", ["anthropic", "openai"]);
|
|
76
77
|
expect(result.selectedPrimary).toBe("anthropic");
|
|
77
78
|
expect(result.usedFallbackPrimary).toBe(true);
|
|
78
79
|
expect(result.availableProviders).toEqual(["anthropic", "openai"]);
|
|
79
80
|
});
|
|
80
81
|
|
|
81
|
-
test("configured primary unavailable + first alternate also unavailable → second alternate selected", () => {
|
|
82
|
-
setupTwoProviders();
|
|
82
|
+
test("configured primary unavailable + first alternate also unavailable → second alternate selected", async () => {
|
|
83
|
+
await setupTwoProviders();
|
|
83
84
|
const result = resolveProviderSelection("gemini", ["fireworks", "openai"]);
|
|
84
85
|
expect(result.selectedPrimary).toBe("openai");
|
|
85
86
|
expect(result.usedFallbackPrimary).toBe(true);
|
|
86
87
|
expect(result.availableProviders).toEqual(["openai"]);
|
|
87
88
|
});
|
|
88
89
|
|
|
89
|
-
test("deduplicates entries in providerOrder", () => {
|
|
90
|
-
setupTwoProviders();
|
|
90
|
+
test("deduplicates entries in providerOrder", async () => {
|
|
91
|
+
await setupTwoProviders();
|
|
91
92
|
const result = resolveProviderSelection("anthropic", [
|
|
92
93
|
"anthropic",
|
|
93
94
|
"openai",
|
|
@@ -96,8 +97,8 @@ describe("resolveProviderSelection", () => {
|
|
|
96
97
|
expect(result.availableProviders).toEqual(["anthropic", "openai"]);
|
|
97
98
|
});
|
|
98
99
|
|
|
99
|
-
test("unknown entries in providerOrder are filtered out", () => {
|
|
100
|
-
setupTwoProviders();
|
|
100
|
+
test("unknown entries in providerOrder are filtered out", async () => {
|
|
101
|
+
await setupTwoProviders();
|
|
101
102
|
const result = resolveProviderSelection("anthropic", [
|
|
102
103
|
"nonexistent",
|
|
103
104
|
"openai",
|
|
@@ -105,24 +106,24 @@ describe("resolveProviderSelection", () => {
|
|
|
105
106
|
expect(result.availableProviders).toEqual(["anthropic", "openai"]);
|
|
106
107
|
});
|
|
107
108
|
|
|
108
|
-
test("no available providers → null selectedPrimary", () => {
|
|
109
|
-
setupTwoProviders();
|
|
109
|
+
test("no available providers → null selectedPrimary", async () => {
|
|
110
|
+
await setupTwoProviders();
|
|
110
111
|
const result = resolveProviderSelection("gemini", ["fireworks", "ollama"]);
|
|
111
112
|
expect(result.selectedPrimary).toBeNull();
|
|
112
113
|
expect(result.usedFallbackPrimary).toBe(false);
|
|
113
114
|
expect(result.availableProviders).toEqual([]);
|
|
114
115
|
});
|
|
115
116
|
|
|
116
|
-
test("empty providerOrder with available primary → primary only", () => {
|
|
117
|
-
setupTwoProviders();
|
|
117
|
+
test("empty providerOrder with available primary → primary only", async () => {
|
|
118
|
+
await setupTwoProviders();
|
|
118
119
|
const result = resolveProviderSelection("anthropic", []);
|
|
119
120
|
expect(result.selectedPrimary).toBe("anthropic");
|
|
120
121
|
expect(result.usedFallbackPrimary).toBe(false);
|
|
121
122
|
expect(result.availableProviders).toEqual(["anthropic"]);
|
|
122
123
|
});
|
|
123
124
|
|
|
124
|
-
test("empty providerOrder with unavailable primary → null", () => {
|
|
125
|
-
setupTwoProviders();
|
|
125
|
+
test("empty providerOrder with unavailable primary → null", async () => {
|
|
126
|
+
await setupTwoProviders();
|
|
126
127
|
const result = resolveProviderSelection("gemini", []);
|
|
127
128
|
expect(result.selectedPrimary).toBeNull();
|
|
128
129
|
expect(result.availableProviders).toEqual([]);
|
|
@@ -130,20 +131,20 @@ describe("resolveProviderSelection", () => {
|
|
|
130
131
|
});
|
|
131
132
|
|
|
132
133
|
describe("getFailoverProvider (fail-open)", () => {
|
|
133
|
-
test("returns provider when primary is available", () => {
|
|
134
|
-
setupTwoProviders();
|
|
134
|
+
test("returns provider when primary is available", async () => {
|
|
135
|
+
await setupTwoProviders();
|
|
135
136
|
const provider = getFailoverProvider("anthropic", ["openai"]);
|
|
136
137
|
expect(provider).toBeDefined();
|
|
137
138
|
});
|
|
138
139
|
|
|
139
|
-
test("returns provider when primary is unavailable but alternate exists", () => {
|
|
140
|
-
setupTwoProviders();
|
|
140
|
+
test("returns provider when primary is unavailable but alternate exists", async () => {
|
|
141
|
+
await setupTwoProviders();
|
|
141
142
|
const provider = getFailoverProvider("gemini", ["anthropic", "openai"]);
|
|
142
143
|
expect(provider).toBeDefined();
|
|
143
144
|
});
|
|
144
145
|
|
|
145
|
-
test("throws ProviderNotConfiguredError when no providers are available", () => {
|
|
146
|
-
setupNoProviders();
|
|
146
|
+
test("throws ProviderNotConfiguredError when no providers are available", async () => {
|
|
147
|
+
await setupNoProviders();
|
|
147
148
|
expect(() => getFailoverProvider("gemini", ["fireworks"])).toThrow(
|
|
148
149
|
ProviderNotConfiguredError,
|
|
149
150
|
);
|
|
@@ -158,8 +159,8 @@ describe("getFailoverProvider (fail-open)", () => {
|
|
|
158
159
|
}
|
|
159
160
|
});
|
|
160
161
|
|
|
161
|
-
test("single available provider returns it directly (no failover wrapper)", () => {
|
|
162
|
-
setupTwoProviders();
|
|
162
|
+
test("single available provider returns it directly (no failover wrapper)", async () => {
|
|
163
|
+
await setupTwoProviders();
|
|
163
164
|
const provider = getFailoverProvider("gemini", ["anthropic"]);
|
|
164
165
|
// Should be a RetryProvider wrapping AnthropicProvider, not a FailoverProvider
|
|
165
166
|
expect(provider.name).not.toBe("failover");
|
|
@@ -181,72 +182,72 @@ describe("managed proxy fallback", () => {
|
|
|
181
182
|
mockAssistantApiKey = "";
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
test("
|
|
185
|
+
test("anthropic and gemini are registered via managed fallback when no user key but proxy context is valid", async () => {
|
|
185
186
|
enableManagedProxy();
|
|
186
187
|
try {
|
|
187
188
|
mockProviderKeys = { anthropic: "test-key" };
|
|
188
|
-
initializeProviders({
|
|
189
|
+
await initializeProviders({
|
|
189
190
|
provider: "anthropic",
|
|
190
191
|
model: "test-model",
|
|
191
192
|
});
|
|
192
193
|
const registered = listProviders();
|
|
193
|
-
expect(registered).
|
|
194
|
-
expect(registered).toContain("fireworks");
|
|
195
|
-
expect(registered).toContain("openrouter");
|
|
194
|
+
expect(registered).toEqual(["anthropic", "gemini"]);
|
|
196
195
|
} finally {
|
|
197
196
|
disableManagedProxy();
|
|
198
197
|
}
|
|
199
198
|
});
|
|
200
199
|
|
|
201
|
-
test("user key takes precedence
|
|
200
|
+
test("user key takes precedence and managed fallback only fills anthropic and gemini", async () => {
|
|
202
201
|
enableManagedProxy();
|
|
203
202
|
try {
|
|
204
203
|
mockProviderKeys = { anthropic: "test-key", openai: "user-openai-key" };
|
|
205
|
-
initializeProviders({
|
|
204
|
+
await initializeProviders({
|
|
206
205
|
provider: "anthropic",
|
|
207
206
|
model: "test-model",
|
|
208
207
|
});
|
|
209
|
-
// openai should be registered (via user key, not managed)
|
|
210
208
|
const registered = listProviders();
|
|
211
209
|
expect(registered).toContain("openai");
|
|
212
|
-
|
|
213
|
-
expect(registered).toContain("
|
|
214
|
-
expect(registered).toContain("
|
|
210
|
+
expect(registered).toContain("anthropic");
|
|
211
|
+
expect(registered).toContain("gemini");
|
|
212
|
+
expect(registered).not.toContain("fireworks");
|
|
213
|
+
expect(registered).not.toContain("openrouter");
|
|
215
214
|
} finally {
|
|
216
215
|
disableManagedProxy();
|
|
217
216
|
}
|
|
218
217
|
});
|
|
219
218
|
|
|
220
|
-
test("managed fallback not activated when proxy context is disabled", () => {
|
|
219
|
+
test("managed fallback not activated when proxy context is disabled", async () => {
|
|
221
220
|
disableManagedProxy();
|
|
222
221
|
mockProviderKeys = { anthropic: "test-key" };
|
|
223
|
-
initializeProviders({
|
|
222
|
+
await initializeProviders({
|
|
224
223
|
provider: "anthropic",
|
|
225
224
|
model: "test-model",
|
|
226
225
|
});
|
|
227
226
|
const registered = listProviders();
|
|
227
|
+
// Anthropic is registered via user-key, not managed proxy.
|
|
228
|
+
expect(registered).toContain("anthropic");
|
|
229
|
+
expect(getProviderRoutingSource("anthropic")).toBe("user-key");
|
|
230
|
+
// No other providers are registered — managed fallback is off.
|
|
231
|
+
expect(registered).not.toContain("gemini");
|
|
228
232
|
expect(registered).not.toContain("openai");
|
|
229
233
|
expect(registered).not.toContain("fireworks");
|
|
230
234
|
expect(registered).not.toContain("openrouter");
|
|
231
235
|
});
|
|
232
236
|
|
|
233
|
-
test("managed
|
|
237
|
+
test("managed anthropic and gemini participate in failover selection", async () => {
|
|
234
238
|
enableManagedProxy();
|
|
235
239
|
try {
|
|
236
240
|
mockProviderKeys = { anthropic: "test-key" };
|
|
237
|
-
initializeProviders({
|
|
241
|
+
await initializeProviders({
|
|
238
242
|
provider: "anthropic",
|
|
239
243
|
model: "test-model",
|
|
240
244
|
});
|
|
241
245
|
const selection = resolveProviderSelection("anthropic", [
|
|
242
246
|
"openai",
|
|
247
|
+
"gemini",
|
|
243
248
|
"fireworks",
|
|
244
249
|
]);
|
|
245
|
-
expect(selection.availableProviders).toEqual([
|
|
246
|
-
"anthropic",
|
|
247
|
-
"openai",
|
|
248
|
-
"fireworks",
|
|
249
|
-
]);
|
|
250
|
+
expect(selection.availableProviders).toEqual(["anthropic", "gemini"]);
|
|
250
251
|
expect(selection.selectedPrimary).toBe("anthropic");
|
|
251
252
|
expect(selection.usedFallbackPrimary).toBe(false);
|
|
252
253
|
} finally {
|
|
@@ -254,18 +255,21 @@ describe("managed proxy fallback", () => {
|
|
|
254
255
|
}
|
|
255
256
|
});
|
|
256
257
|
|
|
257
|
-
test("managed
|
|
258
|
+
test("managed gemini can be selected as fallback primary when configured primary is unavailable", async () => {
|
|
258
259
|
enableManagedProxy();
|
|
259
260
|
try {
|
|
260
261
|
// No anthropic key, no gemini key — only managed providers available
|
|
261
262
|
mockProviderKeys = {};
|
|
262
|
-
initializeProviders({
|
|
263
|
+
await initializeProviders({
|
|
263
264
|
provider: "openai",
|
|
264
265
|
model: "test-model",
|
|
265
266
|
});
|
|
266
|
-
const selection = resolveProviderSelection("openai", [
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
const selection = resolveProviderSelection("openai", [
|
|
268
|
+
"gemini",
|
|
269
|
+
"anthropic",
|
|
270
|
+
]);
|
|
271
|
+
expect(selection.selectedPrimary).toBe("gemini");
|
|
272
|
+
expect(selection.usedFallbackPrimary).toBe(true);
|
|
269
273
|
} finally {
|
|
270
274
|
disableManagedProxy();
|
|
271
275
|
}
|
|
@@ -46,7 +46,7 @@ mock.module("../config/env.js", () => ({
|
|
|
46
46
|
}));
|
|
47
47
|
|
|
48
48
|
mock.module("../security/secure-keys.js", () => ({
|
|
49
|
-
|
|
49
|
+
getSecureKeyAsync: async (key: string) => {
|
|
50
50
|
if (key === credentialKey("vellum", "assistant_api_key")) {
|
|
51
51
|
return mockAssistantApiKey;
|
|
52
52
|
}
|
|
@@ -68,13 +68,14 @@ import {
|
|
|
68
68
|
const PLATFORM_BASE = "https://platform.example.com";
|
|
69
69
|
const MANAGED_API_KEY = "ast-managed-key-123";
|
|
70
70
|
|
|
71
|
-
const
|
|
71
|
+
const DIRECT_OR_MANAGED_PROVIDER_KEYS: string[] = [
|
|
72
72
|
"openai",
|
|
73
73
|
"anthropic",
|
|
74
74
|
"gemini",
|
|
75
75
|
"fireworks",
|
|
76
76
|
"openrouter",
|
|
77
77
|
];
|
|
78
|
+
const MANAGED_FALLBACK_PROVIDERS: string[] = ["anthropic", "gemini"];
|
|
78
79
|
|
|
79
80
|
function enableManagedProxy() {
|
|
80
81
|
mockPlatformBaseUrl = PLATFORM_BASE;
|
|
@@ -108,12 +109,12 @@ beforeEach(() => {
|
|
|
108
109
|
|
|
109
110
|
describe("managed proxy integration — credential precedence", () => {
|
|
110
111
|
describe("user keys present → providers use direct connections (not proxy)", () => {
|
|
111
|
-
test.each(
|
|
112
|
+
test.each(DIRECT_OR_MANAGED_PROVIDER_KEYS)(
|
|
112
113
|
"%s routes via user-key when user key is provided regardless of managed context",
|
|
113
|
-
(provider: string) => {
|
|
114
|
+
async (provider: string) => {
|
|
114
115
|
enableManagedProxy();
|
|
115
116
|
setUserKeysFor(provider);
|
|
116
|
-
initializeProviders({
|
|
117
|
+
await initializeProviders({
|
|
117
118
|
provider,
|
|
118
119
|
model: "test-model",
|
|
119
120
|
});
|
|
@@ -122,29 +123,29 @@ describe("managed proxy integration — credential precedence", () => {
|
|
|
122
123
|
},
|
|
123
124
|
);
|
|
124
125
|
|
|
125
|
-
test("all five
|
|
126
|
+
test("all five configured providers route via user-key when user keys exist", async () => {
|
|
126
127
|
enableManagedProxy();
|
|
127
|
-
setUserKeysFor(...
|
|
128
|
-
initializeProviders({
|
|
128
|
+
setUserKeysFor(...DIRECT_OR_MANAGED_PROVIDER_KEYS);
|
|
129
|
+
await initializeProviders({
|
|
129
130
|
provider: "anthropic",
|
|
130
131
|
model: "test-model",
|
|
131
132
|
});
|
|
132
133
|
const registered = listProviders();
|
|
133
|
-
for (const p of
|
|
134
|
+
for (const p of DIRECT_OR_MANAGED_PROVIDER_KEYS) {
|
|
134
135
|
expect(registered).toContain(p);
|
|
135
136
|
expect(getProviderRoutingSource(p)).toBe("user-key");
|
|
136
137
|
}
|
|
137
138
|
});
|
|
138
139
|
|
|
139
|
-
test("user keys still route via user-key when managed context is disabled", () => {
|
|
140
|
+
test("user keys still route via user-key when managed context is disabled", async () => {
|
|
140
141
|
disableManagedProxy();
|
|
141
|
-
setUserKeysFor(...
|
|
142
|
-
initializeProviders({
|
|
142
|
+
setUserKeysFor(...DIRECT_OR_MANAGED_PROVIDER_KEYS);
|
|
143
|
+
await initializeProviders({
|
|
143
144
|
provider: "anthropic",
|
|
144
145
|
model: "test-model",
|
|
145
146
|
});
|
|
146
147
|
const registered = listProviders();
|
|
147
|
-
for (const p of
|
|
148
|
+
for (const p of DIRECT_OR_MANAGED_PROVIDER_KEYS) {
|
|
148
149
|
expect(registered).toContain(p);
|
|
149
150
|
expect(getProviderRoutingSource(p)).toBe("user-key");
|
|
150
151
|
}
|
|
@@ -152,14 +153,13 @@ describe("managed proxy integration — credential precedence", () => {
|
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
describe("user keys absent + managed context available → providers use managed proxy", () => {
|
|
155
|
-
test.each(
|
|
156
|
+
test.each(MANAGED_FALLBACK_PROVIDERS)(
|
|
156
157
|
"%s routes via managed-proxy when no user key",
|
|
157
|
-
(provider: string) => {
|
|
158
|
+
async (provider: string) => {
|
|
158
159
|
enableManagedProxy();
|
|
159
160
|
mockProviderKeys = {};
|
|
160
|
-
initializeProviders({
|
|
161
|
-
|
|
162
|
-
provider: provider === "openai" ? "openai" : "anthropic",
|
|
161
|
+
await initializeProviders({
|
|
162
|
+
provider: "anthropic",
|
|
163
163
|
model: "test-model",
|
|
164
164
|
});
|
|
165
165
|
expect(listProviders()).toContain(provider);
|
|
@@ -167,24 +167,25 @@ describe("managed proxy integration — credential precedence", () => {
|
|
|
167
167
|
},
|
|
168
168
|
);
|
|
169
169
|
|
|
170
|
-
test("
|
|
170
|
+
test("managed bootstrap registers anthropic and gemini only", async () => {
|
|
171
171
|
enableManagedProxy();
|
|
172
172
|
mockProviderKeys = {};
|
|
173
|
-
initializeProviders({
|
|
173
|
+
await initializeProviders({
|
|
174
174
|
provider: "anthropic",
|
|
175
175
|
model: "test-model",
|
|
176
176
|
});
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
expect(listProviders()).toEqual(["anthropic", "gemini"]);
|
|
178
|
+
expect(getProviderRoutingSource("anthropic")).toBe("managed-proxy");
|
|
179
|
+
expect(getProviderRoutingSource("gemini")).toBe("managed-proxy");
|
|
180
|
+
for (const p of ["openai", "fireworks", "openrouter"]) {
|
|
181
|
+
expect(getProviderRoutingSource(p)).toBeUndefined();
|
|
181
182
|
}
|
|
182
183
|
});
|
|
183
184
|
|
|
184
|
-
test("managed anthropic uses
|
|
185
|
+
test("managed anthropic uses anthropic proxy path", async () => {
|
|
185
186
|
enableManagedProxy();
|
|
186
187
|
mockProviderKeys = {};
|
|
187
|
-
initializeProviders({
|
|
188
|
+
await initializeProviders({
|
|
188
189
|
provider: "anthropic",
|
|
189
190
|
model: "claude-opus-4-6",
|
|
190
191
|
});
|
|
@@ -204,16 +205,14 @@ describe("managed proxy integration — credential precedence", () => {
|
|
|
204
205
|
expect(baseURL).toContain("/v1/runtime-proxy/anthropic");
|
|
205
206
|
});
|
|
206
207
|
|
|
207
|
-
test("managed gemini uses vertex proxy path
|
|
208
|
+
test("managed gemini uses vertex proxy path", async () => {
|
|
208
209
|
enableManagedProxy();
|
|
209
210
|
mockProviderKeys = {};
|
|
210
|
-
initializeProviders({
|
|
211
|
+
await initializeProviders({
|
|
211
212
|
provider: "anthropic",
|
|
212
213
|
model: "test-model",
|
|
213
214
|
});
|
|
214
215
|
|
|
215
|
-
// The GoogleGenAI constructor was captured by the mock — verify it
|
|
216
|
-
// received httpOptions.baseUrl pointing at the vertex proxy path.
|
|
217
216
|
expect(lastGeminiConstructorOpts).toBeDefined();
|
|
218
217
|
const httpOptions = lastGeminiConstructorOpts!.httpOptions as
|
|
219
218
|
| { baseUrl?: string }
|
|
@@ -225,12 +224,12 @@ describe("managed proxy integration — credential precedence", () => {
|
|
|
225
224
|
});
|
|
226
225
|
|
|
227
226
|
describe("neither user keys nor managed context → providers not initialized", () => {
|
|
228
|
-
test.each(
|
|
227
|
+
test.each(DIRECT_OR_MANAGED_PROVIDER_KEYS)(
|
|
229
228
|
"%s is NOT registered when no user key and no managed context",
|
|
230
|
-
(provider: string) => {
|
|
229
|
+
async (provider: string) => {
|
|
231
230
|
disableManagedProxy();
|
|
232
231
|
mockProviderKeys = {};
|
|
233
|
-
initializeProviders({
|
|
232
|
+
await initializeProviders({
|
|
234
233
|
provider: "anthropic",
|
|
235
234
|
model: "test-model",
|
|
236
235
|
});
|
|
@@ -239,10 +238,10 @@ describe("managed proxy integration — credential precedence", () => {
|
|
|
239
238
|
},
|
|
240
239
|
);
|
|
241
240
|
|
|
242
|
-
test("registry is empty when no keys and no managed context (non-ollama primary)", () => {
|
|
241
|
+
test("registry is empty when no keys and no managed context (non-ollama primary)", async () => {
|
|
243
242
|
disableManagedProxy();
|
|
244
243
|
mockProviderKeys = {};
|
|
245
|
-
initializeProviders({
|
|
244
|
+
await initializeProviders({
|
|
246
245
|
provider: "anthropic",
|
|
247
246
|
model: "test-model",
|
|
248
247
|
});
|
|
@@ -251,65 +250,71 @@ describe("managed proxy integration — credential precedence", () => {
|
|
|
251
250
|
});
|
|
252
251
|
|
|
253
252
|
describe("mixed: some user keys + managed fallback fills gaps", () => {
|
|
254
|
-
test("user key for anthropic routes direct
|
|
253
|
+
test("user key for anthropic routes direct and managed fallback only fills gemini", async () => {
|
|
255
254
|
enableManagedProxy();
|
|
256
255
|
setUserKeysFor("anthropic");
|
|
257
|
-
initializeProviders({
|
|
256
|
+
await initializeProviders({
|
|
258
257
|
provider: "anthropic",
|
|
259
258
|
model: "test-model",
|
|
260
259
|
});
|
|
261
260
|
const registered = listProviders();
|
|
262
261
|
expect(registered).toContain("anthropic");
|
|
263
262
|
expect(getProviderRoutingSource("anthropic")).toBe("user-key");
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
expect(registered).toContain("gemini");
|
|
264
|
+
expect(getProviderRoutingSource("gemini")).toBe("managed-proxy");
|
|
265
|
+
for (const p of ["openai", "fireworks", "openrouter"]) {
|
|
266
|
+
expect(registered).not.toContain(p);
|
|
267
|
+
expect(getProviderRoutingSource(p)).toBeUndefined();
|
|
267
268
|
}
|
|
268
269
|
});
|
|
269
270
|
|
|
270
|
-
test("user key for openai routes direct
|
|
271
|
+
test("user key for openai routes direct while anthropic and gemini still bootstrap via managed proxy", async () => {
|
|
271
272
|
enableManagedProxy();
|
|
272
273
|
setUserKeysFor("openai");
|
|
273
|
-
initializeProviders({
|
|
274
|
+
await initializeProviders({
|
|
274
275
|
provider: "openai",
|
|
275
276
|
model: "test-model",
|
|
276
277
|
});
|
|
277
278
|
const registered = listProviders();
|
|
278
279
|
expect(registered).toContain("openai");
|
|
279
280
|
expect(getProviderRoutingSource("openai")).toBe("user-key");
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
expect(registered).toContain("anthropic");
|
|
282
|
+
expect(getProviderRoutingSource("anthropic")).toBe("managed-proxy");
|
|
283
|
+
expect(registered).toContain("gemini");
|
|
284
|
+
expect(getProviderRoutingSource("gemini")).toBe("managed-proxy");
|
|
285
|
+
for (const p of ["fireworks", "openrouter"]) {
|
|
286
|
+
expect(registered).not.toContain(p);
|
|
287
|
+
expect(getProviderRoutingSource(p)).toBeUndefined();
|
|
283
288
|
}
|
|
284
289
|
});
|
|
285
290
|
});
|
|
286
291
|
});
|
|
287
292
|
|
|
288
293
|
describe("managed proxy integration — ollama exclusion", () => {
|
|
289
|
-
test("ollama is never registered via managed proxy fallback", () => {
|
|
294
|
+
test("ollama is never registered via managed proxy fallback", async () => {
|
|
290
295
|
enableManagedProxy();
|
|
291
296
|
mockProviderKeys = {};
|
|
292
|
-
initializeProviders({
|
|
297
|
+
await initializeProviders({
|
|
293
298
|
provider: "anthropic",
|
|
294
299
|
model: "test-model",
|
|
295
300
|
});
|
|
296
301
|
expect(listProviders()).not.toContain("ollama");
|
|
297
302
|
});
|
|
298
303
|
|
|
299
|
-
test("ollama registers only when explicitly configured as provider", () => {
|
|
304
|
+
test("ollama registers only when explicitly configured as provider", async () => {
|
|
300
305
|
enableManagedProxy();
|
|
301
306
|
mockProviderKeys = {};
|
|
302
|
-
initializeProviders({
|
|
307
|
+
await initializeProviders({
|
|
303
308
|
provider: "ollama",
|
|
304
309
|
model: "test-model",
|
|
305
310
|
});
|
|
306
311
|
expect(listProviders()).toContain("ollama");
|
|
307
312
|
});
|
|
308
313
|
|
|
309
|
-
test("ollama registers with explicit API key", () => {
|
|
314
|
+
test("ollama registers with explicit API key", async () => {
|
|
310
315
|
enableManagedProxy();
|
|
311
316
|
mockProviderKeys = { ollama: "ollama-key" };
|
|
312
|
-
initializeProviders({
|
|
317
|
+
await initializeProviders({
|
|
313
318
|
provider: "anthropic",
|
|
314
319
|
model: "test-model",
|
|
315
320
|
});
|
|
@@ -325,8 +330,8 @@ describe("managed proxy integration — ollama exclusion", () => {
|
|
|
325
330
|
});
|
|
326
331
|
|
|
327
332
|
describe("managed proxy integration — constants integrity", () => {
|
|
328
|
-
test("
|
|
329
|
-
for (const provider of
|
|
333
|
+
test("anthropic, gemini, and vertex have metadata with managed=true and a proxyPath", () => {
|
|
334
|
+
for (const provider of ["anthropic", "gemini", "vertex"]) {
|
|
330
335
|
const meta = MANAGED_PROVIDER_META[provider];
|
|
331
336
|
expect(meta).toBeDefined();
|
|
332
337
|
expect(meta.managed).toBe(true);
|
|
@@ -347,15 +352,10 @@ describe("managed proxy integration — constants integrity", () => {
|
|
|
347
352
|
);
|
|
348
353
|
});
|
|
349
354
|
|
|
350
|
-
test("
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
"/v1/runtime-proxy/fireworks",
|
|
356
|
-
);
|
|
357
|
-
expect(MANAGED_PROVIDER_META.openrouter.proxyPath).toBe(
|
|
358
|
-
"/v1/runtime-proxy/openrouter",
|
|
359
|
-
);
|
|
355
|
+
test("openai-compatible providers are not managed proxy capable", () => {
|
|
356
|
+
for (const provider of ["openai", "fireworks", "openrouter"]) {
|
|
357
|
+
expect(MANAGED_PROVIDER_META[provider].managed).toBe(false);
|
|
358
|
+
expect(MANAGED_PROVIDER_META[provider].proxyPath).toBeUndefined();
|
|
359
|
+
}
|
|
360
360
|
});
|
|
361
361
|
});
|
|
@@ -4,7 +4,7 @@ import { describe, expect, mock, test } from "bun:test";
|
|
|
4
4
|
const actualSecureKeys = await import("../security/secure-keys.js");
|
|
5
5
|
mock.module("../security/secure-keys.js", () => ({
|
|
6
6
|
...actualSecureKeys,
|
|
7
|
-
|
|
7
|
+
getSecureKeyAsync: async () => undefined,
|
|
8
8
|
}));
|
|
9
9
|
|
|
10
10
|
import {
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
} from "../providers/registry.js";
|
|
15
15
|
|
|
16
16
|
describe("provider registry (ollama)", () => {
|
|
17
|
-
test("registers ollama when selected provider has no API key", () => {
|
|
18
|
-
initializeProviders({
|
|
17
|
+
test("registers ollama when selected provider has no API key", async () => {
|
|
18
|
+
await initializeProviders({
|
|
19
19
|
provider: "ollama",
|
|
20
20
|
model: "claude-opus-4-6",
|
|
21
21
|
});
|
|
@@ -98,7 +98,7 @@ describe("getPublicBaseUrl", () => {
|
|
|
98
98
|
).toThrow(/Public ingress is disabled/);
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
test("returns URL when enabled is undefined
|
|
101
|
+
test("returns URL when enabled is undefined", () => {
|
|
102
102
|
const result = getPublicBaseUrl({
|
|
103
103
|
ingress: { enabled: undefined, publicBaseUrl: "https://example.com" },
|
|
104
104
|
});
|