@vellumai/assistant 0.8.2 → 0.8.3

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 (231) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -1
  3. package/docker-init-apt-root.sh +79 -6
  4. package/openapi.yaml +336 -21
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  7. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  8. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  9. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  10. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  11. package/src/__tests__/context-token-estimator.test.ts +30 -65
  12. package/src/__tests__/conversation-agent-loop.test.ts +57 -1
  13. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
  15. package/src/__tests__/date-context.test.ts +45 -0
  16. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  17. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  18. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  19. package/src/__tests__/heartbeat-service.test.ts +24 -164
  20. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  21. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  22. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  23. package/src/__tests__/injector-background-turn.test.ts +153 -0
  24. package/src/__tests__/injector-chain.test.ts +5 -0
  25. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  26. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  27. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  28. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  29. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  30. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  31. package/src/__tests__/llm-resolver.test.ts +255 -2
  32. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  33. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  34. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  35. package/src/__tests__/notification-deep-link.test.ts +15 -0
  36. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  37. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  38. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  39. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  40. package/src/__tests__/openai-provider.test.ts +218 -3
  41. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  42. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  43. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  44. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  45. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  46. package/src/__tests__/plugin-types.test.ts +2 -2
  47. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  48. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  49. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  50. package/src/__tests__/system-prompt.test.ts +6 -73
  51. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  52. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  53. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  54. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  55. package/src/a2a/__tests__/task-store.test.ts +246 -0
  56. package/src/a2a/agent-card.ts +58 -0
  57. package/src/a2a/feature-gate.ts +8 -0
  58. package/src/a2a/protocol-constants.ts +21 -0
  59. package/src/a2a/protocol-errors.ts +50 -0
  60. package/src/a2a/protocol-types.ts +162 -0
  61. package/src/a2a/task-store.ts +168 -0
  62. package/src/agent/loop.ts +167 -18
  63. package/src/channels/config.ts +9 -0
  64. package/src/channels/types.ts +14 -0
  65. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  66. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  67. package/src/cli/commands/notifications.ts +65 -35
  68. package/src/cli/commands/plugins.ts +67 -0
  69. package/src/cli/commands/schedules.ts +297 -5
  70. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  71. package/src/cli/lib/install-from-github.ts +8 -9
  72. package/src/cli/lib/search-plugins.ts +163 -0
  73. package/src/cli/program.ts +14 -0
  74. package/src/config/assistant-feature-flags.ts +24 -54
  75. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  76. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  77. package/src/config/call-site-defaults.ts +105 -0
  78. package/src/config/feature-flag-registry.json +21 -29
  79. package/src/config/llm-resolver.ts +52 -1
  80. package/src/config/schema.ts +2 -0
  81. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  82. package/src/config/schemas/channels.ts +9 -0
  83. package/src/config/schemas/conversations.ts +10 -0
  84. package/src/config/schemas/heartbeat.ts +14 -0
  85. package/src/config/schemas/llm.ts +1 -3
  86. package/src/config/schemas/memory-retrospective.ts +1 -1
  87. package/src/config/schemas/memory-v2.ts +4 -4
  88. package/src/config/schemas/memory.ts +3 -1
  89. package/src/config/seed-inference-profiles.ts +99 -29
  90. package/src/context/compactor.ts +72 -12
  91. package/src/context/token-estimator.ts +32 -34
  92. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  93. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  94. package/src/daemon/conversation-agent-loop.ts +29 -2
  95. package/src/daemon/conversation-runtime-assembly.ts +9 -0
  96. package/src/daemon/conversation.ts +0 -7
  97. package/src/daemon/date-context.ts +40 -0
  98. package/src/daemon/guardian-action-generators.ts +1 -125
  99. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  100. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  101. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  102. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  103. package/src/daemon/handlers/config-a2a.ts +289 -0
  104. package/src/daemon/handlers/conversations.ts +1 -0
  105. package/src/daemon/host-app-control-proxy.ts +69 -18
  106. package/src/daemon/host-proxy-preactivation.ts +85 -18
  107. package/src/daemon/lifecycle.ts +49 -61
  108. package/src/daemon/memory-v2-startup.ts +49 -13
  109. package/src/daemon/message-types/notifications.ts +21 -0
  110. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  111. package/src/daemon/pkb-reminder-builder.ts +4 -19
  112. package/src/daemon/process-message.ts +3 -0
  113. package/src/daemon/skill-memory-refresh.ts +5 -1
  114. package/src/daemon/wake-target-adapter.ts +2 -0
  115. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  116. package/src/export/transcript-formatter.ts +54 -20
  117. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
  118. package/src/heartbeat/heartbeat-service.ts +34 -191
  119. package/src/home/__tests__/feed-types.test.ts +40 -0
  120. package/src/home/feed-types.ts +14 -2
  121. package/src/ipc/cli-client.ts +147 -45
  122. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  123. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  124. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  125. package/src/memory/conversation-queries.ts +87 -1
  126. package/src/memory/conversation-title-service.ts +26 -4
  127. package/src/memory/db-init.ts +6 -0
  128. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  129. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  130. package/src/memory/graph/tools.ts +6 -37
  131. package/src/memory/invite-store.ts +53 -0
  132. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  133. package/src/memory/llm-request-log-store.ts +92 -1
  134. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  135. package/src/memory/memory-retrospective-job.ts +33 -6
  136. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  137. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  138. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  139. package/src/memory/migrations/index.ts +3 -0
  140. package/src/memory/migrations/registry.ts +8 -0
  141. package/src/memory/schema/a2a.ts +15 -0
  142. package/src/memory/schema/index.ts +1 -0
  143. package/src/memory/schema/inference.ts +2 -0
  144. package/src/memory/schema/infrastructure.ts +1 -0
  145. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  146. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  147. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  148. package/src/memory/v2/__tests__/injection.test.ts +190 -3
  149. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  150. package/src/memory/v2/activation-store.ts +14 -16
  151. package/src/memory/v2/cli-command-content.ts +19 -0
  152. package/src/memory/v2/cli-command-store.ts +304 -0
  153. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  154. package/src/memory/v2/injection.ts +49 -20
  155. package/src/memory/v2/page-index.ts +38 -13
  156. package/src/memory/v2/static-context.ts +4 -4
  157. package/src/memory/v2/types.ts +23 -0
  158. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  159. package/src/messaging/providers/a2a/deliver.ts +156 -0
  160. package/src/messaging/providers/gmail/client.ts +9 -2
  161. package/src/messaging/providers/index.ts +11 -2
  162. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  163. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  164. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  165. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  166. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  167. package/src/notifications/adapters/macos.ts +12 -2
  168. package/src/notifications/broadcaster.ts +29 -4
  169. package/src/notifications/copy-composer.ts +17 -64
  170. package/src/notifications/decision-engine.ts +111 -44
  171. package/src/notifications/deterministic-checks.ts +96 -0
  172. package/src/notifications/emit-signal.ts +1 -0
  173. package/src/notifications/home-feed-side-effect.ts +85 -6
  174. package/src/notifications/signal.ts +0 -4
  175. package/src/notifications/types.ts +8 -0
  176. package/src/oauth/platform-connection.test.ts +43 -3
  177. package/src/oauth/platform-connection.ts +13 -4
  178. package/src/plugins/defaults/injectors.ts +38 -19
  179. package/src/plugins/external-plugin-loader.ts +82 -10
  180. package/src/plugins/types.ts +16 -7
  181. package/src/prompts/__tests__/system-prompt.test.ts +6 -51
  182. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  183. package/src/prompts/system-prompt.ts +0 -8
  184. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  185. package/src/prompts/templates/system-sections.ts +0 -9
  186. package/src/providers/__tests__/inference.test.ts +2 -0
  187. package/src/providers/call-site-routing.ts +24 -6
  188. package/src/providers/connection-resolution.ts +63 -13
  189. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  190. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  191. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  192. package/src/providers/inference/adapter-factory.ts +9 -20
  193. package/src/providers/inference/auth.ts +12 -0
  194. package/src/providers/inference/backfill.ts +14 -1
  195. package/src/providers/inference/connections.ts +85 -5
  196. package/src/providers/inference/resolve-auth.ts +2 -0
  197. package/src/providers/model-catalog.ts +199 -244
  198. package/src/providers/model-intents.ts +3 -3
  199. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  200. package/src/providers/openai/chat-completions-provider.ts +159 -6
  201. package/src/providers/openrouter/client.ts +42 -4
  202. package/src/providers/platform-proxy/constants.ts +3 -4
  203. package/src/providers/provider-catalog-visibility.ts +3 -1
  204. package/src/providers/provider-send-message.ts +27 -12
  205. package/src/providers/registry.ts +30 -1
  206. package/src/runtime/agent-wake.ts +61 -1
  207. package/src/runtime/auth/route-policy.ts +13 -0
  208. package/src/runtime/http-server.ts +7 -16
  209. package/src/runtime/http-types.ts +0 -47
  210. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  211. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
  212. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  213. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  214. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  215. package/src/runtime/routes/consolidation-routes.ts +100 -0
  216. package/src/runtime/routes/conversation-query-routes.ts +70 -11
  217. package/src/runtime/routes/conversation-routes.ts +7 -0
  218. package/src/runtime/routes/index.ts +2 -0
  219. package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
  220. package/src/runtime/routes/integrations/a2a.ts +235 -0
  221. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  222. package/src/runtime/routes/subagents-routes.ts +41 -0
  223. package/src/subagent/manager.ts +2 -0
  224. package/src/tools/memory/register.ts +1 -9
  225. package/src/tools/registry.ts +2 -2
  226. package/src/tools/types.ts +37 -2
  227. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  228. package/src/workspace/migrations/registry.ts +2 -0
  229. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  230. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  231. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -0,0 +1,74 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { OpenAIChatCompletionsProvider } from "../../openai/chat-completions-provider.js";
4
+ import {
5
+ buildProviderAdapter,
6
+ createAdapterFromConnection,
7
+ } from "../adapter-factory.js";
8
+ import type { ProviderConnection, ResolvedAuth } from "../auth.js";
9
+
10
+ describe("openai-compatible adapter factory", () => {
11
+ test("buildProviderAdapter returns an OpenAIChatCompletionsProvider", () => {
12
+ const adapter = buildProviderAdapter("openai-compatible", {
13
+ apiKey: "test-key",
14
+ model: "my-local-model",
15
+ streamTimeoutMs: 60_000,
16
+ baseURL: "http://localhost:8080/v1",
17
+ useNativeWebSearch: false,
18
+ });
19
+ expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
20
+ });
21
+
22
+ test("createAdapterFromConnection wires baseURL from ResolvedAuth", () => {
23
+ const connection: ProviderConnection = {
24
+ name: "my-vllm",
25
+ provider: "openai-compatible",
26
+ auth: { type: "api_key", credential: "cred-vllm" },
27
+ status: "active",
28
+ label: "vLLM",
29
+ baseUrl: "http://localhost:8080/v1",
30
+ models: [{ id: "my-model" }],
31
+ createdAt: Date.now(),
32
+ updatedAt: Date.now(),
33
+ isManaged: false,
34
+ };
35
+
36
+ const resolvedAuth: ResolvedAuth = {
37
+ kind: "header",
38
+ headers: { Authorization: "Bearer sk-test" },
39
+ baseUrl: "http://localhost:8080/v1",
40
+ };
41
+
42
+ const adapter = createAdapterFromConnection(connection, resolvedAuth, {
43
+ model: "my-model",
44
+ streamTimeoutMs: 60_000,
45
+ });
46
+
47
+ expect(adapter).not.toBeNull();
48
+ });
49
+
50
+ test("createAdapterFromConnection rejects 'none' auth for openai-compatible", () => {
51
+ const connection: ProviderConnection = {
52
+ name: "my-vllm",
53
+ provider: "openai-compatible",
54
+ auth: { type: "none" },
55
+ status: "active",
56
+ label: null,
57
+ baseUrl: "http://localhost:8080/v1",
58
+ models: [{ id: "my-model" }],
59
+ createdAt: Date.now(),
60
+ updatedAt: Date.now(),
61
+ isManaged: false,
62
+ };
63
+
64
+ const resolvedAuth: ResolvedAuth = { kind: "none" };
65
+
66
+ const adapter = createAdapterFromConnection(connection, resolvedAuth, {
67
+ model: "my-model",
68
+ });
69
+
70
+ // openai-compatible is setupMode: "api-key", not keyless, so none auth
71
+ // should be rejected.
72
+ expect(adapter).toBeNull();
73
+ });
74
+ });
@@ -0,0 +1,175 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import { migrateCreateProviderConnections } from "../../../memory/migrations/243-provider-connections.js";
7
+ import { migrateProviderConnectionStatusLabel } from "../../../memory/migrations/244-provider-connection-status-label.js";
8
+ import { migrateProviderConnectionBaseUrlAndModels } from "../../../memory/migrations/250-provider-connection-base-url-and-models.js";
9
+ import * as schema from "../../../memory/schema.js";
10
+ import {
11
+ createConnection,
12
+ getConnection,
13
+ listConnections,
14
+ updateConnection,
15
+ } from "../connections.js";
16
+
17
+ function createTestDb() {
18
+ const sqlite = new Database(":memory:");
19
+ sqlite.exec("PRAGMA journal_mode=WAL");
20
+ return drizzle(sqlite, { schema });
21
+ }
22
+
23
+ function bootDb() {
24
+ const db = createTestDb();
25
+ migrateCreateProviderConnections(db);
26
+ migrateProviderConnectionStatusLabel(db);
27
+ migrateProviderConnectionBaseUrlAndModels(db);
28
+ return db;
29
+ }
30
+
31
+ describe("openai-compatible connection CRUD", () => {
32
+ test("create requires base_url", () => {
33
+ const db = bootDb();
34
+ const result = createConnection(db, {
35
+ name: "my-vllm",
36
+ provider: "openai-compatible",
37
+ auth: { type: "api_key", credential: "cred-vllm" },
38
+ models: [{ id: "my-model" }],
39
+ });
40
+ expect(result.ok).toBe(false);
41
+ if (!result.ok) {
42
+ expect(result.error.code).toBe("base_url_required");
43
+ }
44
+ });
45
+
46
+ test("create requires at least one model", () => {
47
+ const db = bootDb();
48
+ const result = createConnection(db, {
49
+ name: "my-vllm",
50
+ provider: "openai-compatible",
51
+ auth: { type: "api_key", credential: "cred-vllm" },
52
+ baseUrl: "http://localhost:8080/v1",
53
+ });
54
+ expect(result.ok).toBe(false);
55
+ if (!result.ok) {
56
+ expect(result.error.code).toBe("models_required");
57
+ }
58
+ });
59
+
60
+ test("create rejects empty models array", () => {
61
+ const db = bootDb();
62
+ const result = createConnection(db, {
63
+ name: "my-vllm",
64
+ provider: "openai-compatible",
65
+ auth: { type: "api_key", credential: "cred-vllm" },
66
+ baseUrl: "http://localhost:8080/v1",
67
+ models: [],
68
+ });
69
+ expect(result.ok).toBe(false);
70
+ if (!result.ok) {
71
+ expect(result.error.code).toBe("models_required");
72
+ }
73
+ });
74
+
75
+ test("create persists baseUrl and models; round-trips via getConnection", () => {
76
+ const db = bootDb();
77
+ const models = [
78
+ { id: "llama-3-70b" },
79
+ { id: "mistral-7b", displayName: "Mistral 7B" },
80
+ ];
81
+ const result = createConnection(db, {
82
+ name: "my-vllm",
83
+ provider: "openai-compatible",
84
+ auth: { type: "api_key", credential: "cred-vllm" },
85
+ baseUrl: "http://localhost:8080/v1",
86
+ models,
87
+ });
88
+ expect(result.ok).toBe(true);
89
+ if (result.ok) {
90
+ expect(result.connection.baseUrl).toBe("http://localhost:8080/v1");
91
+ expect(result.connection.models).toEqual(models);
92
+ }
93
+
94
+ const fetched = getConnection(db, "my-vllm");
95
+ expect(fetched).not.toBeNull();
96
+ expect(fetched!.baseUrl).toBe("http://localhost:8080/v1");
97
+ expect(fetched!.models).toEqual(models);
98
+ });
99
+
100
+ test("non-openai-compatible providers leave baseUrl/models null", () => {
101
+ const db = bootDb();
102
+ const result = createConnection(db, {
103
+ name: "my-anthropic",
104
+ provider: "anthropic",
105
+ auth: { type: "api_key", credential: "cred-anthropic" },
106
+ });
107
+ expect(result.ok).toBe(true);
108
+ if (result.ok) {
109
+ expect(result.connection.baseUrl).toBeNull();
110
+ expect(result.connection.models).toBeNull();
111
+ }
112
+ });
113
+
114
+ test("updateConnection can change models without re-supplying baseUrl", () => {
115
+ const db = bootDb();
116
+ createConnection(db, {
117
+ name: "my-vllm",
118
+ provider: "openai-compatible",
119
+ auth: { type: "api_key", credential: "cred-vllm" },
120
+ baseUrl: "http://localhost:8080/v1",
121
+ models: [{ id: "llama-3-70b" }],
122
+ });
123
+
124
+ const result = updateConnection(db, "my-vllm", {
125
+ auth: { type: "api_key", credential: "cred-vllm" },
126
+ models: [{ id: "llama-3-70b" }, { id: "mistral-7b" }],
127
+ });
128
+ expect(result.ok).toBe(true);
129
+ if (result.ok) {
130
+ expect(result.connection.baseUrl).toBe("http://localhost:8080/v1");
131
+ expect(result.connection.models).toEqual([
132
+ { id: "llama-3-70b" },
133
+ { id: "mistral-7b" },
134
+ ]);
135
+ }
136
+ });
137
+
138
+ test("updateConnection rejects clearing models on openai-compatible", () => {
139
+ const db = bootDb();
140
+ createConnection(db, {
141
+ name: "my-vllm",
142
+ provider: "openai-compatible",
143
+ auth: { type: "api_key", credential: "cred-vllm" },
144
+ baseUrl: "http://localhost:8080/v1",
145
+ models: [{ id: "llama-3-70b" }],
146
+ });
147
+
148
+ const result = updateConnection(db, "my-vllm", {
149
+ auth: { type: "api_key", credential: "cred-vllm" },
150
+ models: [],
151
+ });
152
+ expect(result.ok).toBe(false);
153
+ if (!result.ok) {
154
+ expect(result.error.code).toBe("models_required");
155
+ }
156
+ });
157
+
158
+ test("listConnections includes baseUrl and models", () => {
159
+ const db = bootDb();
160
+ createConnection(db, {
161
+ name: "my-vllm",
162
+ provider: "openai-compatible",
163
+ auth: { type: "api_key", credential: "cred-vllm" },
164
+ baseUrl: "http://localhost:8080/v1",
165
+ models: [{ id: "llama-3-70b" }],
166
+ });
167
+
168
+ const connections = listConnections(db, {
169
+ provider: "openai-compatible",
170
+ });
171
+ expect(connections.length).toBe(1);
172
+ expect(connections[0]!.baseUrl).toBe("http://localhost:8080/v1");
173
+ expect(connections[0]!.models).toEqual([{ id: "llama-3-70b" }]);
174
+ });
175
+ });
@@ -5,6 +5,7 @@ import { drizzle } from "drizzle-orm/bun-sqlite";
5
5
 
6
6
  import { migrateCreateProviderConnections } from "../../../memory/migrations/243-provider-connections.js";
7
7
  import { migrateProviderConnectionStatusLabel } from "../../../memory/migrations/244-provider-connection-status-label.js";
8
+ import { migrateProviderConnectionBaseUrlAndModels } from "../../../memory/migrations/250-provider-connection-base-url-and-models.js";
8
9
  import * as schema from "../../../memory/schema.js";
9
10
  import {
10
11
  createConnection,
@@ -25,6 +26,7 @@ function bootDb() {
25
26
  const db = createTestDb();
26
27
  migrateCreateProviderConnections(db);
27
28
  migrateProviderConnectionStatusLabel(db);
29
+ migrateProviderConnectionBaseUrlAndModels(db);
28
30
  return db;
29
31
  }
30
32
 
@@ -199,6 +201,19 @@ describe("disableManagedConnectionsForByokHatch", () => {
199
201
  expect(getConnection(db, "gemini-managed")?.status).toBe("disabled");
200
202
  });
201
203
 
204
+ test("leaves an excluded managed connection active", () => {
205
+ const db = bootDb();
206
+ seedCanonicalConnections(db);
207
+
208
+ disableManagedConnectionsForByokHatch(db, {
209
+ excludeConnection: "anthropic-managed",
210
+ });
211
+
212
+ expect(getConnection(db, "anthropic-managed")?.status).toBe("active");
213
+ expect(getConnection(db, "openai-managed")?.status).toBe("disabled");
214
+ expect(getConnection(db, "gemini-managed")?.status).toBe("disabled");
215
+ });
216
+
202
217
  test("subsequent seedCanonicalConnections call does NOT re-flip a user-re-enabled connection", () => {
203
218
  // Models the post-hatch lifecycle: at hatch we disable; the user
204
219
  // later flips one back to active (e.g. after Vellum login). Every
@@ -84,33 +84,22 @@ const ADAPTER_FACTORIES: Record<string, AdapterFactory> = {
84
84
  apiKey: apiKey || undefined,
85
85
  streamTimeoutMs,
86
86
  }),
87
- fireworks: ({ apiKey, model, streamTimeoutMs }) =>
88
- new FireworksProvider(apiKey, model, { streamTimeoutMs }),
87
+ fireworks: ({ apiKey, model, streamTimeoutMs, baseURL }) =>
88
+ new FireworksProvider(apiKey, model, {
89
+ streamTimeoutMs,
90
+ ...(baseURL ? { baseURL } : {}),
91
+ }),
89
92
  openrouter: ({ apiKey, model, streamTimeoutMs, useNativeWebSearch }) =>
90
93
  new OpenRouterProvider(apiKey, model, {
91
94
  useNativeWebSearch,
92
95
  streamTimeoutMs,
93
96
  }),
94
- zai: ({ apiKey, model, streamTimeoutMs }) =>
95
- new OpenAIChatCompletionsProvider(apiKey, model, {
96
- providerName: "zai",
97
- providerLabel: "z.ai",
98
- baseURL: "https://api.z.ai/api/paas/v4/",
99
- streamTimeoutMs,
100
- }),
101
- deepseek: ({ apiKey, model, streamTimeoutMs }) =>
97
+ "openai-compatible": ({ apiKey, model, streamTimeoutMs, baseURL }) =>
102
98
  new OpenAIChatCompletionsProvider(apiKey, model, {
103
- providerName: "deepseek",
104
- providerLabel: "DeepSeek",
105
- baseURL: "https://api.deepseek.com",
106
- streamTimeoutMs,
107
- }),
108
- minimax: ({ apiKey, model, streamTimeoutMs }) =>
109
- new OpenAIChatCompletionsProvider(apiKey, model, {
110
- providerName: "minimax",
111
- providerLabel: "MiniMax",
112
- baseURL: "https://api.minimax.io/v1",
99
+ providerName: "openai-compatible",
100
+ providerLabel: "OpenAI-compatible",
113
101
  streamTimeoutMs,
102
+ ...(baseURL ? { baseURL } : {}),
114
103
  }),
115
104
  };
116
105
 
@@ -86,6 +86,16 @@ export const ConnectionProviderSchema = z.enum(
86
86
  export const ConnectionStatusSchema = z.enum(["active", "disabled"]);
87
87
  export type ConnectionStatus = z.infer<typeof ConnectionStatusSchema>;
88
88
 
89
+ // ---------------------------------------------------------------------------
90
+ // Per-connection model entries (openai-compatible)
91
+ // ---------------------------------------------------------------------------
92
+
93
+ export const ConnectionModelSchema = z.object({
94
+ id: z.string().min(1),
95
+ displayName: z.string().min(1).optional(),
96
+ });
97
+ export type ConnectionModel = z.infer<typeof ConnectionModelSchema>;
98
+
89
99
  // ---------------------------------------------------------------------------
90
100
  // Full connection shape used by CRUD layer
91
101
  // ---------------------------------------------------------------------------
@@ -96,6 +106,8 @@ export const ProviderConnectionSchema = z.object({
96
106
  auth: AuthSchema,
97
107
  status: ConnectionStatusSchema,
98
108
  label: z.string().min(1).nullable(),
109
+ baseUrl: z.string().url().nullable(),
110
+ models: z.array(ConnectionModelSchema).nullable(),
99
111
  createdAt: z.number().int(),
100
112
  updatedAt: z.number().int(),
101
113
  /**
@@ -21,7 +21,12 @@ import { loadRawConfig, saveRawConfig } from "../../config/loader.js";
21
21
  import type { DrizzleDb } from "../../memory/db-connection.js";
22
22
  import { credentialKey } from "../../security/credential-key.js";
23
23
  import { getLogger } from "../../util/logger.js";
24
- import { createConnection, getConnection, seedCanonicalConnections } from "./connections.js";
24
+ import {
25
+ createConnection,
26
+ getConnection,
27
+ PROVIDERS_REQUIRING_BASE_URL_AND_MODELS,
28
+ seedCanonicalConnections,
29
+ } from "./connections.js";
25
30
 
26
31
  const log = getLogger("provider-connections-backfill");
27
32
 
@@ -149,6 +154,14 @@ function ensureProviderConnection(
149
154
  const provider = entry.provider as string | undefined;
150
155
  if (!provider) return false;
151
156
 
157
+ if (PROVIDERS_REQUIRING_BASE_URL_AND_MODELS.has(provider)) {
158
+ log.warn(
159
+ { entry: entryLabel, provider },
160
+ "Skipping backfill for provider that requires per-connection base_url/models",
161
+ );
162
+ return false;
163
+ }
164
+
152
165
  let connectionName: string;
153
166
 
154
167
  if (globalMode === "managed" && MANAGED_PROVIDERS.has(provider)) {
@@ -1,4 +1,5 @@
1
1
  import { and, eq, isNull } from "drizzle-orm";
2
+ import { z } from "zod";
2
3
 
3
4
  import type { DrizzleDb } from "../../memory/db-connection.js";
4
5
  import { providerConnections } from "../../memory/schema/inference.js";
@@ -6,6 +7,8 @@ import { clearConnectionProviderCache } from "../registry.js";
6
7
  import {
7
8
  type Auth,
8
9
  AuthSchema,
10
+ type ConnectionModel,
11
+ ConnectionModelSchema,
9
12
  type ConnectionProvider,
10
13
  ConnectionProviderSchema,
11
14
  type ConnectionStatus,
@@ -14,6 +17,23 @@ import {
14
17
  VALID_CONNECTION_PROVIDERS,
15
18
  } from "./auth.js";
16
19
 
20
+ // ---------------------------------------------------------------------------
21
+ // Helpers
22
+ // ---------------------------------------------------------------------------
23
+
24
+ function parseModelsColumn(raw: string | null): ConnectionModel[] | null {
25
+ if (raw === null || raw === "") return null;
26
+ try {
27
+ const parsed = z.array(ConnectionModelSchema).safeParse(JSON.parse(raw));
28
+ return parsed.success ? parsed.data : null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ export const PROVIDERS_REQUIRING_BASE_URL_AND_MODELS: ReadonlySet<string> =
35
+ new Set(["openai-compatible"]);
36
+
17
37
  // ---------------------------------------------------------------------------
18
38
  // Read
19
39
  // ---------------------------------------------------------------------------
@@ -46,6 +66,8 @@ export function listConnections(
46
66
  provider: provider.data,
47
67
  status,
48
68
  label: row.label ?? null,
69
+ baseUrl: row.baseUrl ?? null,
70
+ models: parseModelsColumn(row.models),
49
71
  isManaged: MANAGED_CONNECTION_NAMES.has(row.name),
50
72
  },
51
73
  ];
@@ -77,6 +99,8 @@ export function getConnection(
77
99
  provider: provider.data,
78
100
  status,
79
101
  label: row.label ?? null,
102
+ baseUrl: row.baseUrl ?? null,
103
+ models: parseModelsColumn(row.models),
80
104
  isManaged: MANAGED_CONNECTION_NAMES.has(row.name),
81
105
  };
82
106
  }
@@ -91,22 +115,30 @@ export type CreateConnectionInput = {
91
115
  auth: Auth;
92
116
  status?: ConnectionStatus;
93
117
  label?: string | null;
118
+ baseUrl?: string | null;
119
+ models?: ConnectionModel[] | null;
94
120
  };
95
121
 
96
122
  export type UpdateConnectionInput = {
97
123
  auth: Auth;
98
124
  status?: ConnectionStatus;
99
125
  label?: string | null;
126
+ baseUrl?: string | null;
127
+ models?: ConnectionModel[] | null;
100
128
  };
101
129
 
102
130
  export type ConnectionCreateError =
103
131
  | { code: "already_exists" }
104
132
  | { code: "invalid_provider"; provider: string }
105
- | { code: "invalid_auth" };
133
+ | { code: "invalid_auth" }
134
+ | { code: "base_url_required" }
135
+ | { code: "models_required" };
106
136
 
107
137
  export type ConnectionUpdateError =
108
138
  | { code: "not_found" }
109
- | { code: "invalid_auth" };
139
+ | { code: "invalid_auth" }
140
+ | { code: "base_url_required" }
141
+ | { code: "models_required" };
110
142
 
111
143
  export type ConnectionDeleteError =
112
144
  | { code: "not_found" }
@@ -143,6 +175,15 @@ export function createConnection(
143
175
 
144
176
  const status = input.status ?? "active";
145
177
  const label = input.label ?? null;
178
+ const baseUrl = input.baseUrl ?? null;
179
+ const models = input.models ?? null;
180
+
181
+ if (PROVIDERS_REQUIRING_BASE_URL_AND_MODELS.has(provider)) {
182
+ if (!baseUrl) return { ok: false, error: { code: "base_url_required" } };
183
+ if (!models || models.length === 0) {
184
+ return { ok: false, error: { code: "models_required" } };
185
+ }
186
+ }
146
187
 
147
188
  const now = Date.now();
148
189
  db.insert(providerConnections)
@@ -152,6 +193,8 @@ export function createConnection(
152
193
  auth: JSON.stringify(authResult.data),
153
194
  status,
154
195
  label,
196
+ baseUrl,
197
+ models: models === null ? null : JSON.stringify(models),
155
198
  createdAt: now,
156
199
  updatedAt: now,
157
200
  })
@@ -169,6 +212,8 @@ export function createConnection(
169
212
  auth: authResult.data,
170
213
  status,
171
214
  label,
215
+ baseUrl,
216
+ models,
172
217
  createdAt: now,
173
218
  updatedAt: now,
174
219
  isManaged: MANAGED_CONNECTION_NAMES.has(input.name),
@@ -193,15 +238,34 @@ export function updateConnection(
193
238
  return { ok: false, error: { code: "invalid_auth" } };
194
239
  }
195
240
 
241
+ const nextBaseUrl =
242
+ input.baseUrl !== undefined ? input.baseUrl : existing.baseUrl;
243
+ const nextModels =
244
+ input.models !== undefined ? input.models : existing.models;
245
+
246
+ if (PROVIDERS_REQUIRING_BASE_URL_AND_MODELS.has(existing.provider)) {
247
+ if (!nextBaseUrl)
248
+ return { ok: false, error: { code: "base_url_required" } };
249
+ if (!nextModels || nextModels.length === 0) {
250
+ return { ok: false, error: { code: "models_required" } };
251
+ }
252
+ }
253
+
196
254
  const now = Date.now();
197
255
  const setClause: {
198
256
  auth: string;
199
257
  updatedAt: number;
200
258
  status?: string;
201
259
  label?: string | null;
260
+ baseUrl?: string | null;
261
+ models?: string | null;
202
262
  } = { auth: JSON.stringify(authResult.data), updatedAt: now };
203
263
  if (input.status !== undefined) setClause.status = input.status;
204
264
  if (input.label !== undefined) setClause.label = input.label;
265
+ if (input.baseUrl !== undefined) setClause.baseUrl = input.baseUrl;
266
+ if (input.models !== undefined)
267
+ setClause.models =
268
+ input.models === null ? null : JSON.stringify(input.models);
205
269
 
206
270
  db.update(providerConnections)
207
271
  .set(setClause)
@@ -218,6 +282,8 @@ export function updateConnection(
218
282
  auth: authResult.data,
219
283
  status: input.status !== undefined ? input.status : existing.status,
220
284
  label: input.label !== undefined ? input.label : existing.label,
285
+ baseUrl: nextBaseUrl,
286
+ models: nextModels,
221
287
  updatedAt: now,
222
288
  },
223
289
  };
@@ -294,6 +360,12 @@ const CANONICAL_CONNECTIONS: Array<{
294
360
  auth: { type: "platform" },
295
361
  label: "Google Gemini",
296
362
  },
363
+ {
364
+ name: "fireworks-managed",
365
+ provider: "fireworks",
366
+ auth: { type: "platform" },
367
+ label: "Fireworks",
368
+ },
297
369
  ];
298
370
 
299
371
  /**
@@ -330,7 +402,7 @@ export const MANAGED_CONNECTION_NAMES: ReadonlySet<string> = new Set(
330
402
  *
331
403
  * Status handling: the upsert never touches `status` so user customization
332
404
  * is preserved across reboots. New rows default to `status: "active"` via the
333
- * column default. Off-platform installs flip the three canonical rows to
405
+ * column default. Off-platform installs flip canonical managed rows to
334
406
  * `status: "disabled"` ONCE at hatch time via
335
407
  * `disableManagedConnectionsForByokHatch` (called from `seedInferenceProfiles`
336
408
  * when `isHatch && !isPlatform`); subsequent boots leave whatever the user
@@ -374,7 +446,7 @@ export function seedCanonicalConnections(db: DrizzleDb): void {
374
446
  }
375
447
 
376
448
  /**
377
- * Flip the three canonical managed connections to `status: "disabled"` at
449
+ * Flip canonical managed connections to `status: "disabled"` at
378
450
  * hatch time on BYOK (off-platform) installs.
379
451
  *
380
452
  * Why hatch-time only: managed connections need platform auth that a fresh
@@ -389,10 +461,18 @@ export function seedCanonicalConnections(db: DrizzleDb): void {
389
461
  *
390
462
  * Idempotent: a second hatch (workspace reset) re-disables the rows, which
391
463
  * is the right call — re-hatch means re-onboard.
464
+ *
465
+ * When onboarding explicitly selected a managed profile, callers may exclude
466
+ * that selected connection so the managed route remains usable for the first
467
+ * post-onboarding message.
392
468
  */
393
- export function disableManagedConnectionsForByokHatch(db: DrizzleDb): void {
469
+ export function disableManagedConnectionsForByokHatch(
470
+ db: DrizzleDb,
471
+ options: { excludeConnection?: string } = {},
472
+ ): void {
394
473
  const now = Date.now();
395
474
  for (const name of MANAGED_CONNECTION_NAMES) {
475
+ if (name === options.excludeConnection) continue;
396
476
  db.update(providerConnections)
397
477
  .set({ status: "disabled", updatedAt: now })
398
478
  .where(eq(providerConnections.name, name))
@@ -23,6 +23,7 @@ export type ResolveAuthError =
23
23
  export async function resolveAuth(
24
24
  auth: Auth,
25
25
  provider: string,
26
+ opts: { baseUrl?: string | null } = {},
26
27
  ): Promise<
27
28
  { ok: true; resolved: ResolvedAuth } | { ok: false; error: ResolveAuthError }
28
29
  > {
@@ -40,6 +41,7 @@ export async function resolveAuth(
40
41
  resolved: {
41
42
  kind: "header",
42
43
  headers: { Authorization: `Bearer ${value}` },
44
+ ...(opts.baseUrl ? { baseUrl: opts.baseUrl } : {}),
43
45
  },
44
46
  };
45
47
  }