@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.
Files changed (247) hide show
  1. package/bun.lock +62 -349
  2. package/docs/architecture/integrations.md +1 -1
  3. package/docs/architecture/keychain-broker.md +94 -29
  4. package/docs/architecture/security.md +2 -2
  5. package/knip.json +7 -29
  6. package/package.json +2 -9
  7. package/src/__tests__/agent-loop.test.ts +1 -1
  8. package/src/__tests__/app-git-history.test.ts +0 -2
  9. package/src/__tests__/app-git-service.test.ts +1 -6
  10. package/src/__tests__/approval-cascade.test.ts +0 -1
  11. package/src/__tests__/avatar-e2e.test.ts +0 -1
  12. package/src/__tests__/browser-fill-credential.test.ts +1 -6
  13. package/src/__tests__/call-domain.test.ts +0 -1
  14. package/src/__tests__/call-routes-http.test.ts +0 -1
  15. package/src/__tests__/channel-guardian.test.ts +4 -4
  16. package/src/__tests__/channel-readiness-routes.test.ts +0 -1
  17. package/src/__tests__/channel-readiness-service.test.ts +0 -1
  18. package/src/__tests__/checker.test.ts +13 -11
  19. package/src/__tests__/claude-code-skill-regression.test.ts +0 -1
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +1 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +0 -3
  22. package/src/__tests__/config-schema.test.ts +3 -9
  23. package/src/__tests__/config-watcher.test.ts +11 -3
  24. package/src/__tests__/credential-broker-browser-fill.test.ts +27 -24
  25. package/src/__tests__/credential-broker-server-use.test.ts +60 -24
  26. package/src/__tests__/credential-security-e2e.test.ts +1 -6
  27. package/src/__tests__/credential-security-invariants.test.ts +13 -8
  28. package/src/__tests__/credential-vault-unit.test.ts +28 -12
  29. package/src/__tests__/credential-vault.test.ts +40 -28
  30. package/src/__tests__/credentials-cli.test.ts +1 -21
  31. package/src/__tests__/email-invite-adapter.test.ts +0 -1
  32. package/src/__tests__/fixtures/credential-security-fixtures.ts +3 -3
  33. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -79
  34. package/src/__tests__/gateway-only-enforcement.test.ts +1 -21
  35. package/src/__tests__/guardian-action-conversation-turn.test.ts +8 -8
  36. package/src/__tests__/guardian-action-late-reply.test.ts +13 -14
  37. package/src/__tests__/guardian-action-store.test.ts +0 -57
  38. package/src/__tests__/guardian-outbound-http.test.ts +1 -1
  39. package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -3
  40. package/src/__tests__/hooks-blocking.test.ts +1 -1
  41. package/src/__tests__/hooks-config.test.ts +5 -29
  42. package/src/__tests__/hooks-discovery.test.ts +1 -1
  43. package/src/__tests__/hooks-integration.test.ts +1 -1
  44. package/src/__tests__/hooks-manager.test.ts +1 -1
  45. package/src/__tests__/hooks-runner.test.ts +1 -23
  46. package/src/__tests__/hooks-settings.test.ts +1 -1
  47. package/src/__tests__/hooks-templates.test.ts +1 -1
  48. package/src/__tests__/integration-status.test.ts +0 -1
  49. package/src/__tests__/invite-routes-http.test.ts +0 -3
  50. package/src/__tests__/llm-usage-store.test.ts +50 -0
  51. package/src/__tests__/managed-proxy-context.test.ts +41 -41
  52. package/src/__tests__/media-generate-image.test.ts +2 -2
  53. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -6
  54. package/src/__tests__/memory-regressions.experimental.test.ts +4 -4
  55. package/src/__tests__/memory-regressions.test.ts +27 -27
  56. package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
  57. package/src/__tests__/memory-upsert-concurrency.test.ts +4 -4
  58. package/src/__tests__/notification-decision-fallback.test.ts +1 -1
  59. package/src/__tests__/oauth-cli.test.ts +1 -4
  60. package/src/__tests__/oauth-store.test.ts +1 -3
  61. package/src/__tests__/openai-provider.test.ts +7 -7
  62. package/src/__tests__/platform.test.ts +14 -4
  63. package/src/__tests__/pricing.test.ts +0 -223
  64. package/src/__tests__/provider-commit-message-generator.test.ts +1 -4
  65. package/src/__tests__/provider-fail-open-selection.test.ts +58 -54
  66. package/src/__tests__/provider-managed-proxy-integration.test.ts +63 -63
  67. package/src/__tests__/provider-registry-ollama.test.ts +3 -3
  68. package/src/__tests__/public-ingress-urls.test.ts +1 -1
  69. package/src/__tests__/registry.test.ts +3 -103
  70. package/src/__tests__/script-proxy-injection-runtime.test.ts +2 -7
  71. package/src/__tests__/secret-onetime-send.test.ts +1 -6
  72. package/src/__tests__/secret-routes-managed-proxy.test.ts +6 -13
  73. package/src/__tests__/secure-keys.test.ts +241 -229
  74. package/src/__tests__/session-abort-tool-results.test.ts +0 -1
  75. package/src/__tests__/session-confirmation-signals.test.ts +0 -1
  76. package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -7
  77. package/src/__tests__/session-pre-run-repair.test.ts +0 -1
  78. package/src/__tests__/session-provider-retry-repair.test.ts +0 -1
  79. package/src/__tests__/session-queue.test.ts +2 -4
  80. package/src/__tests__/session-slash-known.test.ts +0 -1
  81. package/src/__tests__/session-slash-queue.test.ts +0 -1
  82. package/src/__tests__/session-slash-unknown.test.ts +0 -1
  83. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  84. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  85. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  86. package/src/__tests__/slack-channel-config.test.ts +1 -7
  87. package/src/__tests__/swarm-recursion.test.ts +0 -1
  88. package/src/__tests__/swarm-session-integration.test.ts +0 -1
  89. package/src/__tests__/swarm-tool.test.ts +0 -1
  90. package/src/__tests__/task-compiler.test.ts +1 -1
  91. package/src/__tests__/test-support/browser-skill-harness.ts +0 -18
  92. package/src/__tests__/test-support/computer-use-skill-harness.ts +0 -23
  93. package/src/__tests__/tool-executor.test.ts +1 -1
  94. package/src/__tests__/trust-store.test.ts +3 -82
  95. package/src/__tests__/twilio-config.test.ts +0 -1
  96. package/src/__tests__/twilio-provider.test.ts +0 -5
  97. package/src/__tests__/twilio-routes.test.ts +0 -1
  98. package/src/__tests__/usage-cache-backfill-migration.test.ts +10 -10
  99. package/src/calls/guardian-question-copy.ts +1 -1
  100. package/src/cli/commands/doctor.ts +10 -34
  101. package/src/cli/commands/memory.ts +3 -5
  102. package/src/cli/commands/sessions.ts +1 -1
  103. package/src/cli/commands/usage.ts +359 -0
  104. package/src/cli/http-client.ts +22 -12
  105. package/src/cli/program.ts +2 -0
  106. package/src/cli/reference.ts +1 -0
  107. package/src/cli.ts +251 -181
  108. package/src/config/assistant-feature-flags.ts +0 -7
  109. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  110. package/src/config/bundled-skills/claude-code/SKILL.md +1 -1
  111. package/src/config/bundled-skills/claude-code/TOOLS.json +1 -1
  112. package/src/config/bundled-skills/gmail/SKILL.md +0 -1
  113. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
  114. package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
  115. package/src/config/bundled-skills/messaging/SKILL.md +0 -1
  116. package/src/config/bundled-skills/sequences/SKILL.md +0 -1
  117. package/src/config/env.ts +13 -0
  118. package/src/config/feature-flag-registry.json +9 -41
  119. package/src/config/schemas/security.ts +1 -2
  120. package/src/config/skills.ts +1 -1
  121. package/src/contacts/contact-store.ts +0 -50
  122. package/src/daemon/approved-devices-store.ts +0 -44
  123. package/src/daemon/classifier.ts +1 -1
  124. package/src/daemon/config-watcher.ts +12 -6
  125. package/src/daemon/handlers/config-model.ts +1 -1
  126. package/src/daemon/handlers/sessions.ts +4 -116
  127. package/src/daemon/handlers/skills.ts +1 -1
  128. package/src/daemon/lifecycle.ts +13 -15
  129. package/src/daemon/providers-setup.ts +1 -1
  130. package/src/daemon/server.ts +19 -3
  131. package/src/daemon/session-slash.ts +2 -2
  132. package/src/daemon/shutdown-handlers.ts +15 -0
  133. package/src/daemon/watch-handler.ts +2 -2
  134. package/src/email/guardrails.ts +1 -1
  135. package/src/email/service.ts +0 -5
  136. package/src/hooks/templates.ts +1 -1
  137. package/src/media/app-icon-generator.ts +2 -2
  138. package/src/media/avatar-router.ts +2 -2
  139. package/src/media/gemini-image-service.ts +5 -5
  140. package/src/memory/admin.ts +2 -2
  141. package/src/memory/app-git-service.ts +0 -7
  142. package/src/memory/conversation-crud.ts +1 -1
  143. package/src/memory/conversation-title-service.ts +2 -2
  144. package/src/memory/embedding-backend.ts +30 -26
  145. package/src/memory/external-conversation-store.ts +0 -30
  146. package/src/memory/guardian-action-store.ts +0 -31
  147. package/src/memory/guardian-approvals.ts +1 -56
  148. package/src/memory/indexer.ts +4 -3
  149. package/src/memory/items-extractor.ts +1 -1
  150. package/src/memory/job-handlers/backfill.ts +5 -2
  151. package/src/memory/job-handlers/index-maintenance.ts +2 -2
  152. package/src/memory/job-handlers/media-processing.ts +2 -2
  153. package/src/memory/job-handlers/summarization.ts +1 -1
  154. package/src/memory/job-utils.ts +1 -2
  155. package/src/memory/jobs-worker.ts +2 -2
  156. package/src/memory/llm-usage-store.ts +57 -11
  157. package/src/memory/media-store.ts +4 -535
  158. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +2 -2
  159. package/src/memory/migrations/110-channel-guardian.ts +0 -1
  160. package/src/memory/published-pages-store.ts +0 -83
  161. package/src/memory/qdrant-circuit-breaker.ts +0 -8
  162. package/src/memory/retriever.ts +1 -1
  163. package/src/memory/search/semantic.ts +1 -8
  164. package/src/memory/shared-app-links-store.ts +0 -15
  165. package/src/messaging/registry.ts +0 -5
  166. package/src/messaging/style-analyzer.ts +1 -1
  167. package/src/notifications/copy-composer.ts +5 -13
  168. package/src/notifications/decision-engine.ts +2 -2
  169. package/src/notifications/deliveries-store.ts +0 -39
  170. package/src/notifications/guardian-question-mode.ts +6 -10
  171. package/src/notifications/preference-extractor.ts +1 -1
  172. package/src/oauth/byo-connection.test.ts +29 -20
  173. package/src/oauth/provider-behaviors.ts +1 -1
  174. package/src/permissions/checker.ts +1 -1
  175. package/src/permissions/shell-identity.ts +0 -5
  176. package/src/permissions/trust-store.ts +0 -37
  177. package/src/prompts/system-prompt.ts +3 -3
  178. package/src/providers/managed-proxy/constants.ts +8 -10
  179. package/src/providers/managed-proxy/context.ts +14 -9
  180. package/src/providers/provider-send-message.ts +4 -52
  181. package/src/providers/registry.ts +16 -50
  182. package/src/runtime/actor-token-store.ts +0 -23
  183. package/src/runtime/http-router.ts +5 -1
  184. package/src/runtime/http-server.ts +101 -4
  185. package/src/runtime/invite-instruction-generator.ts +25 -51
  186. package/src/runtime/invite-service.ts +0 -20
  187. package/src/runtime/routes/attachment-routes.ts +1 -1
  188. package/src/runtime/routes/brain-graph-routes.ts +1 -1
  189. package/src/runtime/routes/call-routes.ts +1 -1
  190. package/src/runtime/routes/conversation-routes.ts +32 -11
  191. package/src/runtime/routes/debug-routes.ts +1 -1
  192. package/src/runtime/routes/diagnostics-routes.ts +2 -2
  193. package/src/runtime/routes/documents-routes.ts +3 -3
  194. package/src/runtime/routes/global-search-routes.ts +1 -1
  195. package/src/runtime/routes/guardian-bootstrap-routes.ts +0 -20
  196. package/src/runtime/routes/guardian-refresh-routes.ts +0 -20
  197. package/src/runtime/routes/secret-routes.ts +4 -4
  198. package/src/runtime/routes/trust-rules-routes.ts +1 -1
  199. package/src/security/credential-backend.ts +148 -0
  200. package/src/security/oauth2.ts +1 -1
  201. package/src/security/secret-allowlist.ts +1 -1
  202. package/src/security/secure-keys.ts +98 -160
  203. package/src/security/token-manager.ts +0 -7
  204. package/src/sequence/guardrails.ts +0 -4
  205. package/src/sequence/store.ts +1 -20
  206. package/src/sequence/types.ts +1 -36
  207. package/src/signals/cancel.ts +69 -0
  208. package/src/signals/conversation-undo.ts +127 -0
  209. package/src/signals/trust-rule.ts +174 -0
  210. package/src/skills/clawhub.ts +5 -5
  211. package/src/skills/managed-store.ts +4 -4
  212. package/src/telemetry/usage-telemetry-reporter.test.ts +366 -0
  213. package/src/telemetry/usage-telemetry-reporter.ts +181 -0
  214. package/src/tools/claude-code/claude-code.ts +2 -2
  215. package/src/tools/credentials/vault.ts +8 -4
  216. package/src/tools/memory/handlers.test.ts +24 -26
  217. package/src/tools/memory/handlers.ts +1 -13
  218. package/src/tools/registry.ts +5 -100
  219. package/src/tools/terminal/parser.ts +34 -4
  220. package/src/tools/tool-manifest.ts +0 -10
  221. package/src/usage/actors.ts +0 -12
  222. package/src/util/canonicalize-identity.ts +0 -9
  223. package/src/util/errors.ts +0 -3
  224. package/src/util/platform.ts +24 -7
  225. package/src/util/pricing.ts +0 -38
  226. package/src/watcher/constants.ts +0 -7
  227. package/src/watcher/providers/linear.ts +1 -1
  228. package/src/work-items/work-item-store.ts +4 -4
  229. package/src/workspace/commit-message-provider.ts +1 -1
  230. package/src/workspace/git-service.ts +44 -1
  231. package/src/workspace/provider-commit-message-generator.ts +1 -1
  232. package/src/__tests__/fixtures/proxy-fixtures.ts +0 -147
  233. package/src/browser-extension-relay/client.ts +0 -155
  234. package/src/contacts/index.ts +0 -18
  235. package/src/daemon/tls-certs.ts +0 -270
  236. package/src/errors.ts +0 -41
  237. package/src/events/index.ts +0 -18
  238. package/src/followups/index.ts +0 -10
  239. package/src/playbooks/index.ts +0 -10
  240. package/src/runtime/auth/index.ts +0 -44
  241. package/src/tasks/candidate-store.ts +0 -95
  242. package/src/tools/browser/api-map.ts +0 -313
  243. package/src/tools/browser/auto-navigate.ts +0 -469
  244. package/src/tools/browser/headless-browser.ts +0 -590
  245. package/src/tools/browser/recording-store.ts +0 -75
  246. package/src/tools/computer-use/registry.ts +0 -21
  247. 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
- getSecureKey: (key: string) => {
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("openai registered via managed fallback when no user key but proxy context is valid", () => {
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).toContain("openai");
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 over managed fallback", () => {
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
- // fireworks/openrouter should also be registered via managed fallback
213
- expect(registered).toContain("fireworks");
214
- expect(registered).toContain("openrouter");
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 providers participate in failover selection", () => {
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 provider selected as primary when configured primary unavailable", () => {
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", ["fireworks"]);
267
- expect(selection.selectedPrimary).toBe("openai");
268
- expect(selection.usedFallbackPrimary).toBe(false);
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
- getSecureKey: (key: string) => {
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 MANAGED_PROVIDERS: string[] = [
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(MANAGED_PROVIDERS)(
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 managed providers route via user-key with user keys", () => {
126
+ test("all five configured providers route via user-key when user keys exist", async () => {
126
127
  enableManagedProxy();
127
- setUserKeysFor(...MANAGED_PROVIDERS);
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 MANAGED_PROVIDERS) {
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(...MANAGED_PROVIDERS);
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 MANAGED_PROVIDERS) {
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(MANAGED_PROVIDERS)(
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
- // For ollama, provider selection does not trigger managed proxy
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("all five managed providers route via managed-proxy simultaneously", () => {
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
- const registered = listProviders();
178
- for (const p of MANAGED_PROVIDERS) {
179
- expect(registered).toContain(p);
180
- expect(getProviderRoutingSource(p)).toBe("managed-proxy");
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 vertex proxy path instead of anthropic proxy path", () => {
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 instead of gemini 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(MANAGED_PROVIDERS)(
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, managed fallback fills remaining four via proxy", () => {
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
- for (const p of ["openai", "gemini", "fireworks", "openrouter"]) {
265
- expect(registered).toContain(p);
266
- expect(getProviderRoutingSource(p)).toBe("managed-proxy");
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, managed fallback fills remaining four via proxy", () => {
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
- for (const p of ["anthropic", "gemini", "fireworks", "openrouter"]) {
281
- expect(registered).toContain(p);
282
- expect(getProviderRoutingSource(p)).toBe("managed-proxy");
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("all five managed providers have metadata with managed=true and a proxyPath", () => {
329
- for (const provider of MANAGED_PROVIDERS) {
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("other providers use their own proxy paths", () => {
351
- expect(MANAGED_PROVIDER_META.openai.proxyPath).toBe(
352
- "/v1/runtime-proxy/openai",
353
- );
354
- expect(MANAGED_PROVIDER_META.fireworks.proxyPath).toBe(
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
- getSecureKey: () => undefined,
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 (backward compat)", () => {
101
+ test("returns URL when enabled is undefined", () => {
102
102
  const result = getPublicBaseUrl({
103
103
  ingress: { enabled: undefined, publicBaseUrl: "https://example.com" },
104
104
  });