@vellumai/assistant 0.4.31 → 0.4.32

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 (121) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
  4. package/src/__tests__/anthropic-provider.test.ts +86 -1
  5. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  6. package/src/__tests__/checker.test.ts +37 -98
  7. package/src/__tests__/commit-message-enrichment-service.test.ts +15 -0
  8. package/src/__tests__/config-schema.test.ts +6 -5
  9. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  10. package/src/__tests__/daemon-server-session-init.test.ts +1 -19
  11. package/src/__tests__/followup-tools.test.ts +0 -30
  12. package/src/__tests__/gemini-provider.test.ts +79 -1
  13. package/src/__tests__/ipc-snapshot.test.ts +0 -4
  14. package/src/__tests__/managed-proxy-context.test.ts +163 -0
  15. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  16. package/src/__tests__/memory-regressions.test.ts +6 -6
  17. package/src/__tests__/openai-provider.test.ts +82 -0
  18. package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
  19. package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
  20. package/src/__tests__/recurrence-types.test.ts +0 -15
  21. package/src/__tests__/schedule-tools.test.ts +28 -44
  22. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  23. package/src/__tests__/task-management-tools.test.ts +111 -0
  24. package/src/__tests__/twilio-config.test.ts +0 -3
  25. package/src/amazon/session.ts +30 -91
  26. package/src/calls/call-controller.ts +423 -571
  27. package/src/calls/finalize-call.ts +20 -0
  28. package/src/calls/relay-access-wait.ts +340 -0
  29. package/src/calls/relay-server.ts +267 -902
  30. package/src/calls/relay-setup-router.ts +307 -0
  31. package/src/calls/relay-verification.ts +280 -0
  32. package/src/calls/twilio-config.ts +1 -8
  33. package/src/calls/voice-control-protocol.ts +184 -0
  34. package/src/calls/voice-session-bridge.ts +1 -8
  35. package/src/config/agent-schema.ts +1 -1
  36. package/src/config/bundled-skills/followups/TOOLS.json +0 -4
  37. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  38. package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
  39. package/src/config/core-schema.ts +1 -1
  40. package/src/config/env.ts +0 -10
  41. package/src/config/feature-flag-registry.json +1 -1
  42. package/src/config/loader.ts +19 -0
  43. package/src/config/schema.ts +2 -2
  44. package/src/daemon/handlers/session-history.ts +398 -0
  45. package/src/daemon/handlers/session-user-message.ts +982 -0
  46. package/src/daemon/handlers/sessions.ts +9 -1338
  47. package/src/daemon/ipc-contract/sessions.ts +0 -6
  48. package/src/daemon/ipc-contract-inventory.json +0 -1
  49. package/src/daemon/lifecycle.ts +0 -29
  50. package/src/home-base/app-link-store.ts +0 -7
  51. package/src/memory/conversation-attention-store.ts +1 -1
  52. package/src/memory/conversation-store.ts +0 -51
  53. package/src/memory/db-init.ts +5 -1
  54. package/src/memory/job-handlers/conflict.ts +24 -0
  55. package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
  56. package/src/memory/migrations/134-contacts-notes-column.ts +50 -33
  57. package/src/memory/migrations/registry.ts +6 -0
  58. package/src/memory/recall-cache.ts +0 -5
  59. package/src/memory/schema/calls.ts +274 -0
  60. package/src/memory/schema/contacts.ts +125 -0
  61. package/src/memory/schema/conversations.ts +129 -0
  62. package/src/memory/schema/guardian.ts +172 -0
  63. package/src/memory/schema/index.ts +8 -0
  64. package/src/memory/schema/infrastructure.ts +205 -0
  65. package/src/memory/schema/memory-core.ts +196 -0
  66. package/src/memory/schema/notifications.ts +191 -0
  67. package/src/memory/schema/tasks.ts +78 -0
  68. package/src/memory/schema.ts +1 -1385
  69. package/src/memory/slack-thread-store.ts +0 -69
  70. package/src/notifications/decisions-store.ts +2 -105
  71. package/src/notifications/deliveries-store.ts +0 -11
  72. package/src/notifications/preferences-store.ts +1 -58
  73. package/src/permissions/checker.ts +6 -17
  74. package/src/providers/anthropic/client.ts +6 -2
  75. package/src/providers/gemini/client.ts +13 -2
  76. package/src/providers/managed-proxy/constants.ts +55 -0
  77. package/src/providers/managed-proxy/context.ts +77 -0
  78. package/src/providers/registry.ts +112 -0
  79. package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
  80. package/src/runtime/http-server.ts +83 -722
  81. package/src/runtime/http-types.ts +0 -16
  82. package/src/runtime/middleware/auth.ts +0 -12
  83. package/src/runtime/routes/app-routes.ts +33 -0
  84. package/src/runtime/routes/approval-routes.ts +32 -0
  85. package/src/runtime/routes/attachment-routes.ts +32 -0
  86. package/src/runtime/routes/brain-graph-routes.ts +27 -0
  87. package/src/runtime/routes/call-routes.ts +41 -0
  88. package/src/runtime/routes/channel-readiness-routes.ts +20 -0
  89. package/src/runtime/routes/channel-routes.ts +70 -0
  90. package/src/runtime/routes/contact-routes.ts +63 -0
  91. package/src/runtime/routes/conversation-attention-routes.ts +15 -0
  92. package/src/runtime/routes/conversation-routes.ts +190 -193
  93. package/src/runtime/routes/debug-routes.ts +15 -0
  94. package/src/runtime/routes/events-routes.ts +16 -0
  95. package/src/runtime/routes/global-search-routes.ts +15 -0
  96. package/src/runtime/routes/guardian-action-routes.ts +22 -0
  97. package/src/runtime/routes/guardian-bootstrap-routes.ts +20 -0
  98. package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
  99. package/src/runtime/routes/identity-routes.ts +20 -0
  100. package/src/runtime/routes/inbound-message-handler.ts +8 -0
  101. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +6 -6
  102. package/src/runtime/routes/integration-routes.ts +83 -0
  103. package/src/runtime/routes/invite-routes.ts +31 -0
  104. package/src/runtime/routes/migration-routes.ts +30 -0
  105. package/src/runtime/routes/pairing-routes.ts +18 -0
  106. package/src/runtime/routes/secret-routes.ts +20 -0
  107. package/src/runtime/routes/surface-action-routes.ts +26 -0
  108. package/src/runtime/routes/trust-rules-routes.ts +31 -0
  109. package/src/runtime/routes/twilio-routes.ts +79 -0
  110. package/src/schedule/recurrence-types.ts +1 -11
  111. package/src/tools/followups/followup_create.ts +9 -3
  112. package/src/tools/mcp/mcp-tool-factory.ts +0 -17
  113. package/src/tools/memory/definitions.ts +0 -6
  114. package/src/tools/network/script-proxy/session-manager.ts +38 -3
  115. package/src/tools/schedule/create.ts +1 -3
  116. package/src/tools/schedule/update.ts +9 -6
  117. package/src/twitter/session.ts +29 -77
  118. package/src/util/cookie-session.ts +114 -0
  119. package/src/__tests__/conversation-routes.test.ts +0 -99
  120. package/src/__tests__/task-tools.test.ts +0 -685
  121. package/src/contacts/startup-migration.ts +0 -21
@@ -101,10 +101,6 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
101
101
  type: "usage_request",
102
102
  sessionId: "sess-001",
103
103
  },
104
- sandbox_set: {
105
- type: "sandbox_set",
106
- enabled: true,
107
- },
108
104
  cu_session_create: {
109
105
  type: "cu_session_create",
110
106
  sessionId: "cu-sess-001",
@@ -0,0 +1,163 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // Mock logger to suppress output
4
+ mock.module("../util/logger.js", () => ({
5
+ getLogger: () =>
6
+ new Proxy({} as Record<string, unknown>, {
7
+ get: () => () => {},
8
+ }),
9
+ }));
10
+
11
+ // Mutable state for env and secure key stubs
12
+ let mockPlatformBaseUrl = "";
13
+ let mockAssistantApiKey: string | null = null;
14
+
15
+ mock.module("../config/env.js", () => ({
16
+ getPlatformBaseUrl: () => mockPlatformBaseUrl,
17
+ }));
18
+
19
+ mock.module("../security/secure-keys.js", () => ({
20
+ getSecureKey: (key: string) => {
21
+ if (key === "credential:vellum:assistant_api_key") {
22
+ return mockAssistantApiKey;
23
+ }
24
+ return null;
25
+ },
26
+ }));
27
+
28
+ import {
29
+ buildManagedBaseUrl,
30
+ hasManagedProxyPrereqs,
31
+ managedFallbackEnabledFor,
32
+ resolveManagedProxyContext,
33
+ } from "../providers/managed-proxy/context.js";
34
+
35
+ describe("resolveManagedProxyContext", () => {
36
+ beforeEach(() => {
37
+ mockPlatformBaseUrl = "";
38
+ mockAssistantApiKey = null;
39
+ });
40
+
41
+ test("returns disabled when platform URL is empty", () => {
42
+ mockPlatformBaseUrl = "";
43
+ mockAssistantApiKey = "sk-test-key";
44
+
45
+ const ctx = resolveManagedProxyContext();
46
+ expect(ctx.enabled).toBe(false);
47
+ expect(ctx.platformBaseUrl).toBe("");
48
+ });
49
+
50
+ test("returns disabled when assistant API key is missing", () => {
51
+ mockPlatformBaseUrl = "https://platform.example.com";
52
+ mockAssistantApiKey = null;
53
+
54
+ const ctx = resolveManagedProxyContext();
55
+ expect(ctx.enabled).toBe(false);
56
+ expect(ctx.assistantApiKey).toBe("");
57
+ });
58
+
59
+ test("returns disabled when both are missing", () => {
60
+ const ctx = resolveManagedProxyContext();
61
+ expect(ctx.enabled).toBe(false);
62
+ });
63
+
64
+ test("returns enabled when both platform URL and API key are present", () => {
65
+ mockPlatformBaseUrl = "https://platform.example.com/";
66
+ mockAssistantApiKey = "sk-test-key";
67
+
68
+ const ctx = resolveManagedProxyContext();
69
+ expect(ctx.enabled).toBe(true);
70
+ expect(ctx.platformBaseUrl).toBe("https://platform.example.com");
71
+ expect(ctx.assistantApiKey).toBe("sk-test-key");
72
+ });
73
+
74
+ test("strips trailing slashes from platform URL", () => {
75
+ mockPlatformBaseUrl = "https://platform.example.com///";
76
+ mockAssistantApiKey = "sk-test-key";
77
+
78
+ const ctx = resolveManagedProxyContext();
79
+ expect(ctx.platformBaseUrl).toBe("https://platform.example.com");
80
+ });
81
+ });
82
+
83
+ describe("hasManagedProxyPrereqs", () => {
84
+ beforeEach(() => {
85
+ mockPlatformBaseUrl = "";
86
+ mockAssistantApiKey = null;
87
+ });
88
+
89
+ test("returns false when prerequisites are missing", () => {
90
+ expect(hasManagedProxyPrereqs()).toBe(false);
91
+ });
92
+
93
+ test("returns true when prerequisites are satisfied", () => {
94
+ mockPlatformBaseUrl = "https://platform.example.com";
95
+ mockAssistantApiKey = "sk-test-key";
96
+ expect(hasManagedProxyPrereqs()).toBe(true);
97
+ });
98
+ });
99
+
100
+ describe("buildManagedBaseUrl", () => {
101
+ beforeEach(() => {
102
+ mockPlatformBaseUrl = "https://platform.example.com";
103
+ mockAssistantApiKey = "sk-test-key";
104
+ });
105
+
106
+ test("builds correct URL for managed providers", () => {
107
+ expect(buildManagedBaseUrl("openai")).toBe(
108
+ "https://platform.example.com/v1/runtime-proxy/openai",
109
+ );
110
+ expect(buildManagedBaseUrl("anthropic")).toBe(
111
+ "https://platform.example.com/v1/runtime-proxy/anthropic",
112
+ );
113
+ expect(buildManagedBaseUrl("gemini")).toBe(
114
+ "https://platform.example.com/v1/runtime-proxy/gemini",
115
+ );
116
+ expect(buildManagedBaseUrl("fireworks")).toBe(
117
+ "https://platform.example.com/v1/runtime-proxy/fireworks",
118
+ );
119
+ expect(buildManagedBaseUrl("openrouter")).toBe(
120
+ "https://platform.example.com/v1/runtime-proxy/openrouter",
121
+ );
122
+ });
123
+
124
+ test("returns undefined for non-managed provider (ollama)", () => {
125
+ expect(buildManagedBaseUrl("ollama")).toBeUndefined();
126
+ });
127
+
128
+ test("returns undefined for unknown provider", () => {
129
+ expect(buildManagedBaseUrl("unknown-provider")).toBeUndefined();
130
+ });
131
+
132
+ test("returns undefined when prerequisites are missing", () => {
133
+ mockPlatformBaseUrl = "";
134
+ mockAssistantApiKey = null;
135
+ expect(buildManagedBaseUrl("openai")).toBeUndefined();
136
+ });
137
+ });
138
+
139
+ describe("managedFallbackEnabledFor", () => {
140
+ beforeEach(() => {
141
+ mockPlatformBaseUrl = "https://platform.example.com";
142
+ mockAssistantApiKey = "sk-test-key";
143
+ });
144
+
145
+ test("returns true for managed providers with prerequisites", () => {
146
+ expect(managedFallbackEnabledFor("openai")).toBe(true);
147
+ expect(managedFallbackEnabledFor("anthropic")).toBe(true);
148
+ });
149
+
150
+ test("returns false for non-managed provider", () => {
151
+ expect(managedFallbackEnabledFor("ollama")).toBe(false);
152
+ });
153
+
154
+ test("returns false for unknown provider", () => {
155
+ expect(managedFallbackEnabledFor("unknown")).toBe(false);
156
+ });
157
+
158
+ test("returns false when prerequisites are missing", () => {
159
+ mockPlatformBaseUrl = "";
160
+ mockAssistantApiKey = null;
161
+ expect(managedFallbackEnabledFor("openai")).toBe(false);
162
+ });
163
+ });
@@ -478,7 +478,7 @@ describe("Memory lifecycle E2E regression", () => {
478
478
  confidence: 0.83,
479
479
  importance: 0.7,
480
480
  fingerprint: "fp-item-runtime-existing",
481
- verificationState: "assistant_inferred",
481
+ verificationState: "user_reported",
482
482
  scopeId: "default",
483
483
  firstSeenAt: now + 200,
484
484
  lastSeenAt: now + 200,
@@ -494,7 +494,7 @@ describe("Memory lifecycle E2E regression", () => {
494
494
  confidence: 0.81,
495
495
  importance: 0.7,
496
496
  fingerprint: "fp-item-runtime-candidate",
497
- verificationState: "assistant_inferred",
497
+ verificationState: "user_reported",
498
498
  scopeId: "default",
499
499
  firstSeenAt: now + 201,
500
500
  lastSeenAt: now + 201,
@@ -1201,7 +1201,7 @@ describe("Memory regressions", () => {
1201
1201
  status: "active",
1202
1202
  confidence: 0.8,
1203
1203
  fingerprint: "fp-conflict-existing",
1204
- verificationState: "assistant_inferred",
1204
+ verificationState: "user_reported",
1205
1205
  scopeId: "scope-conflicts",
1206
1206
  firstSeenAt: now - 10_000,
1207
1207
  lastSeenAt: now - 5_000,
@@ -1216,7 +1216,7 @@ describe("Memory regressions", () => {
1216
1216
  status: "pending_clarification",
1217
1217
  confidence: 0.8,
1218
1218
  fingerprint: "fp-conflict-candidate",
1219
- verificationState: "assistant_inferred",
1219
+ verificationState: "user_reported",
1220
1220
  scopeId: "scope-conflicts",
1221
1221
  firstSeenAt: now - 9_000,
1222
1222
  lastSeenAt: now - 4_000,
@@ -1312,7 +1312,7 @@ describe("Memory regressions", () => {
1312
1312
  status: "active",
1313
1313
  confidence: 0.8,
1314
1314
  fingerprint: "fp-conflict-existing-age",
1315
- verificationState: "assistant_inferred",
1315
+ verificationState: "user_reported",
1316
1316
  scopeId: "scope-conflicts-age",
1317
1317
  firstSeenAt: now - 10_000,
1318
1318
  lastSeenAt: now - 5_000,
@@ -1327,7 +1327,7 @@ describe("Memory regressions", () => {
1327
1327
  status: "pending_clarification",
1328
1328
  confidence: 0.8,
1329
1329
  fingerprint: "fp-conflict-candidate-age",
1330
- verificationState: "assistant_inferred",
1330
+ verificationState: "user_reported",
1331
1331
  scopeId: "scope-conflicts-age",
1332
1332
  firstSeenAt: now - 9_000,
1333
1333
  lastSeenAt: now - 4_000,
@@ -1418,7 +1418,7 @@ describe("Memory regressions", () => {
1418
1418
  status: "active",
1419
1419
  confidence: 0.8,
1420
1420
  fingerprint: "fp-conflict-existing-unrelated",
1421
- verificationState: "assistant_inferred",
1421
+ verificationState: "user_reported",
1422
1422
  scopeId: "scope-conflicts-unrelated",
1423
1423
  firstSeenAt: now - 10_000,
1424
1424
  lastSeenAt: now - 5_000,
@@ -1433,7 +1433,7 @@ describe("Memory regressions", () => {
1433
1433
  status: "pending_clarification",
1434
1434
  confidence: 0.8,
1435
1435
  fingerprint: "fp-conflict-candidate-unrelated",
1436
- verificationState: "assistant_inferred",
1436
+ verificationState: "user_reported",
1437
1437
  scopeId: "scope-conflicts-unrelated",
1438
1438
  firstSeenAt: now - 9_000,
1439
1439
  lastSeenAt: now - 4_000,
@@ -78,8 +78,10 @@ mock.module("openai", () => ({
78
78
  }));
79
79
 
80
80
  // Import after mocking
81
+ import { FireworksProvider } from "../providers/fireworks/client.js";
81
82
  import { OllamaProvider } from "../providers/ollama/client.js";
82
83
  import { OpenAIProvider } from "../providers/openai/client.js";
84
+ import { OpenRouterProvider } from "../providers/openrouter/client.js";
83
85
 
84
86
  // ---------------------------------------------------------------------------
85
87
  // Helpers
@@ -844,3 +846,83 @@ describe("OpenAIProvider", () => {
844
846
  });
845
847
  });
846
848
  });
849
+
850
+ // ---------------------------------------------------------------------------
851
+ // Managed proxy initialization
852
+ // ---------------------------------------------------------------------------
853
+
854
+ describe("managed proxy initialization", () => {
855
+ beforeEach(() => {
856
+ lastConstructorOptions = null;
857
+ });
858
+
859
+ test("OpenAIProvider initializes with managed proxy base URL", () => {
860
+ const managed = new OpenAIProvider("ast-key-123", "gpt-4o", {
861
+ baseURL: "https://platform.example.com/v1/runtime-proxy/openai",
862
+ });
863
+
864
+ expect(managed.name).toBe("openai");
865
+ expect(lastConstructorOptions).toEqual({
866
+ apiKey: "ast-key-123",
867
+ baseURL: "https://platform.example.com/v1/runtime-proxy/openai",
868
+ });
869
+ });
870
+
871
+ test("OpenAIProvider without baseURL calls provider directly", () => {
872
+ new OpenAIProvider("sk-user-key", "gpt-4o");
873
+
874
+ expect(lastConstructorOptions).toEqual({
875
+ apiKey: "sk-user-key",
876
+ baseURL: undefined,
877
+ });
878
+ });
879
+
880
+ test("FireworksProvider initializes with managed proxy base URL", () => {
881
+ const managed = new FireworksProvider(
882
+ "ast-key-123",
883
+ "accounts/fireworks/models/llama-v3p1-70b-instruct",
884
+ {
885
+ baseURL: "https://platform.example.com/v1/runtime-proxy/fireworks",
886
+ },
887
+ );
888
+
889
+ expect(managed.name).toBe("fireworks");
890
+ expect(lastConstructorOptions).toEqual({
891
+ apiKey: "ast-key-123",
892
+ baseURL: "https://platform.example.com/v1/runtime-proxy/fireworks",
893
+ });
894
+ });
895
+
896
+ test("FireworksProvider without managed baseURL uses default Fireworks URL", () => {
897
+ new FireworksProvider(
898
+ "fw-user-key",
899
+ "accounts/fireworks/models/llama-v3p1-70b-instruct",
900
+ );
901
+
902
+ expect(lastConstructorOptions).toEqual({
903
+ apiKey: "fw-user-key",
904
+ baseURL: "https://api.fireworks.ai/inference/v1",
905
+ });
906
+ });
907
+
908
+ test("OpenRouterProvider initializes with managed proxy base URL", () => {
909
+ const managed = new OpenRouterProvider("ast-key-123", "openai/gpt-4o", {
910
+ baseURL: "https://platform.example.com/v1/runtime-proxy/openrouter",
911
+ });
912
+
913
+ expect(managed.name).toBe("openrouter");
914
+ expect(lastConstructorOptions).toEqual({
915
+ apiKey: "ast-key-123",
916
+ baseURL: "https://platform.example.com/v1/runtime-proxy/openrouter",
917
+ });
918
+ });
919
+
920
+ test("OpenRouterProvider without managed baseURL uses default OpenRouter URL", () => {
921
+ new OpenRouterProvider("or-user-key", "openai/gpt-4o");
922
+
923
+ expect(lastConstructorOptions).toEqual({
924
+ apiKey: "or-user-key",
925
+ baseURL: "https://openrouter.ai/api/v1",
926
+ });
927
+ });
928
+ });
@@ -1,8 +1,35 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, mock, test } from "bun:test";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mock the underlying dependencies of managed-proxy/context.js rather than
5
+ // the context module itself. This avoids global mock bleed: other test files
6
+ // that import context.js will still get the real implementation with their
7
+ // own dependency mocks.
8
+ // ---------------------------------------------------------------------------
9
+ let mockPlatformBaseUrl = "";
10
+ let mockAssistantApiKey = "";
11
+
12
+ const actualEnv = await import("../config/env.js");
13
+ mock.module("../config/env.js", () => ({
14
+ ...actualEnv,
15
+ getPlatformBaseUrl: () => mockPlatformBaseUrl,
16
+ }));
17
+
18
+ const actualSecureKeys = await import("../security/secure-keys.js");
19
+ mock.module("../security/secure-keys.js", () => ({
20
+ ...actualSecureKeys,
21
+ getSecureKey: (key: string) => {
22
+ if (key === "credential:vellum:assistant_api_key") {
23
+ return mockAssistantApiKey || null;
24
+ }
25
+ return null;
26
+ },
27
+ }));
2
28
 
3
29
  import {
4
30
  getFailoverProvider,
5
31
  initializeProviders,
32
+ listProviders,
6
33
  resolveProviderSelection,
7
34
  } from "../providers/registry.js";
8
35
 
@@ -125,3 +152,109 @@ describe("getFailoverProvider (fail-open)", () => {
125
152
  expect(provider.name).not.toBe("failover");
126
153
  });
127
154
  });
155
+
156
+ // -------------------------------------------------------------------------
157
+ // Managed proxy fallback
158
+ // -------------------------------------------------------------------------
159
+
160
+ describe("managed proxy fallback", () => {
161
+ function enableManagedProxy() {
162
+ mockPlatformBaseUrl = "https://platform.example.com";
163
+ mockAssistantApiKey = "ast-key-123";
164
+ }
165
+
166
+ function disableManagedProxy() {
167
+ mockPlatformBaseUrl = "";
168
+ mockAssistantApiKey = "";
169
+ }
170
+
171
+ test("openai registered via managed fallback when no user key but proxy context is valid", () => {
172
+ enableManagedProxy();
173
+ try {
174
+ initializeProviders({
175
+ apiKeys: { anthropic: "test-key" },
176
+ provider: "anthropic",
177
+ model: "test-model",
178
+ });
179
+ const registered = listProviders();
180
+ expect(registered).toContain("openai");
181
+ expect(registered).toContain("fireworks");
182
+ expect(registered).toContain("openrouter");
183
+ } finally {
184
+ disableManagedProxy();
185
+ }
186
+ });
187
+
188
+ test("user key takes precedence over managed fallback", () => {
189
+ enableManagedProxy();
190
+ try {
191
+ initializeProviders({
192
+ apiKeys: { anthropic: "test-key", openai: "user-openai-key" },
193
+ provider: "anthropic",
194
+ model: "test-model",
195
+ });
196
+ // openai should be registered (via user key, not managed)
197
+ const registered = listProviders();
198
+ expect(registered).toContain("openai");
199
+ // fireworks/openrouter should also be registered via managed fallback
200
+ expect(registered).toContain("fireworks");
201
+ expect(registered).toContain("openrouter");
202
+ } finally {
203
+ disableManagedProxy();
204
+ }
205
+ });
206
+
207
+ test("managed fallback not activated when proxy context is disabled", () => {
208
+ disableManagedProxy();
209
+ initializeProviders({
210
+ apiKeys: { anthropic: "test-key" },
211
+ provider: "anthropic",
212
+ model: "test-model",
213
+ });
214
+ const registered = listProviders();
215
+ expect(registered).not.toContain("openai");
216
+ expect(registered).not.toContain("fireworks");
217
+ expect(registered).not.toContain("openrouter");
218
+ });
219
+
220
+ test("managed providers participate in failover selection", () => {
221
+ enableManagedProxy();
222
+ try {
223
+ initializeProviders({
224
+ apiKeys: { anthropic: "test-key" },
225
+ provider: "anthropic",
226
+ model: "test-model",
227
+ });
228
+ const selection = resolveProviderSelection("anthropic", [
229
+ "openai",
230
+ "fireworks",
231
+ ]);
232
+ expect(selection.availableProviders).toEqual([
233
+ "anthropic",
234
+ "openai",
235
+ "fireworks",
236
+ ]);
237
+ expect(selection.selectedPrimary).toBe("anthropic");
238
+ expect(selection.usedFallbackPrimary).toBe(false);
239
+ } finally {
240
+ disableManagedProxy();
241
+ }
242
+ });
243
+
244
+ test("managed provider selected as primary when configured primary unavailable", () => {
245
+ enableManagedProxy();
246
+ try {
247
+ // No anthropic key, no gemini key — only managed providers available
248
+ initializeProviders({
249
+ apiKeys: {},
250
+ provider: "openai",
251
+ model: "test-model",
252
+ });
253
+ const selection = resolveProviderSelection("openai", ["fireworks"]);
254
+ expect(selection.selectedPrimary).toBe("openai");
255
+ expect(selection.usedFallbackPrimary).toBe(false);
256
+ } finally {
257
+ disableManagedProxy();
258
+ }
259
+ });
260
+ });