@vellumai/assistant 0.10.6-staging.1 → 0.10.6

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 (58) hide show
  1. package/openapi.yaml +43 -6
  2. package/package.json +1 -1
  3. package/src/__tests__/attachment-stored-path-annotation.test.ts +151 -0
  4. package/src/__tests__/attachments-store.test.ts +37 -1
  5. package/src/__tests__/attachments.test.ts +53 -0
  6. package/src/__tests__/channel-reply-delivery.test.ts +214 -5
  7. package/src/__tests__/config-loader-backfill.test.ts +16 -36
  8. package/src/__tests__/image-source-path-reinject.test.ts +155 -14
  9. package/src/__tests__/managed-profile-guard.test.ts +2 -0
  10. package/src/__tests__/mtime-cache.test.ts +265 -80
  11. package/src/__tests__/plugin-config-data-migration.test.ts +2 -4
  12. package/src/__tests__/plugin-effective-enabled-set.test.ts +9 -6
  13. package/src/__tests__/workspace-migration-125-repoint-managed-connections-to-vellum.test.ts +140 -0
  14. package/src/agent/attachments.ts +55 -13
  15. package/src/cli/commands/platform/__tests__/credits.test.ts +111 -0
  16. package/src/cli/commands/platform/index.ts +86 -4
  17. package/src/cli/lib/__tests__/confirm-prompt.test.ts +9 -9
  18. package/src/cli/lib/diff-plugin.ts +7 -3
  19. package/src/cli/lib/inspect-plugin.ts +28 -10
  20. package/src/cli/lib/install-from-github.ts +69 -24
  21. package/src/cli/lib/ipc-params.ts +3 -3
  22. package/src/cli/lib/list-installed-plugins.ts +11 -5
  23. package/src/cli/lib/merge-plugin-tree.ts +15 -9
  24. package/src/cli/lib/plugin-fingerprint.ts +0 -6
  25. package/src/cli/lib/publish-plugin.ts +1 -1
  26. package/src/cli/lib/unknown-command.ts +3 -1
  27. package/src/cli/lib/upgrade-plugin.ts +2 -5
  28. package/src/config/__tests__/sync-gated-profiles.test.ts +2 -2
  29. package/src/config/seed-inference-profiles.ts +5 -4
  30. package/src/daemon/conversation-lifecycle.ts +37 -15
  31. package/src/daemon/conversation-messaging.ts +56 -28
  32. package/src/daemon/conversation.ts +2 -2
  33. package/src/heartbeat/__tests__/heartbeat-service.test.ts +6 -0
  34. package/src/hooks/__tests__/hook-live-reload.test.ts +110 -96
  35. package/src/hooks/hook-loader.ts +57 -144
  36. package/src/hooks/registry.ts +13 -9
  37. package/src/persistence/attachments-store.ts +2 -1
  38. package/src/persistence/conversation-crud.ts +24 -0
  39. package/src/plugin-api/types.ts +4 -1
  40. package/src/plugins/mtime-cache.ts +347 -33
  41. package/src/plugins/pipeline.ts +3 -2
  42. package/src/providers/__tests__/inference.test.ts +42 -31
  43. package/src/providers/inference/__tests__/connections-label.test.ts +9 -28
  44. package/src/providers/inference/auth.ts +1 -2
  45. package/src/providers/inference/backfill.ts +7 -5
  46. package/src/providers/inference/connections.ts +54 -30
  47. package/src/runtime/channel-reply-delivery.ts +83 -24
  48. package/src/runtime/channel-retry-sweep.ts +4 -0
  49. package/src/runtime/{slack-no-response.ts → no-response.ts} +26 -6
  50. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +43 -3
  51. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +61 -24
  52. package/src/runtime/routes/conversation-query-routes.ts +18 -1
  53. package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -1
  54. package/src/runtime/routes/inference-provider-connection-routes.ts +4 -3
  55. package/src/runtime/routes/platform-routes.ts +82 -1
  56. package/src/runtime/slack-reply-session.ts +1 -1
  57. package/src/workspace/migrations/125-repoint-managed-connections-to-vellum.ts +99 -0
  58. package/src/workspace/migrations/registry.ts +2 -0
@@ -560,11 +560,7 @@ describe("PATCH with label", () => {
560
560
  // ── Managed-connection write protection ──────────────────────────────────────
561
561
 
562
562
  describe("Managed connection write protection", () => {
563
- const MANAGED_NAMES = [
564
- "anthropic-managed",
565
- "openai-managed",
566
- "gemini-managed",
567
- ] as const;
563
+ const MANAGED_NAMES = ["vellum"] as const;
568
564
 
569
565
  describe("DELETE", () => {
570
566
  for (const name of MANAGED_NAMES) {
@@ -590,21 +586,21 @@ describe("Managed connection write protection", () => {
590
586
  // Even though a profile references the managed connection, the error
591
587
  // should be the managed-protection 400, not the references-409.
592
588
  seedConnection({
593
- name: "anthropic-managed",
589
+ name: "vellum",
594
590
  provider: "anthropic",
595
591
  auth: { type: "platform" },
596
592
  });
597
593
  fakeConfig = {
598
594
  llm: {
599
595
  profiles: {
600
- balanced: { provider_connection: "anthropic-managed" },
596
+ balanced: { provider_connection: "vellum" },
601
597
  },
602
598
  },
603
599
  };
604
600
 
605
601
  const err = await call(
606
602
  findHandler("inference_provider_connections_delete"),
607
- { pathParams: { name: "anthropic-managed" } },
603
+ { pathParams: { name: "vellum" } },
608
604
  ).catch((e: unknown) => e);
609
605
 
610
606
  expect(err).toBeInstanceOf(BadRequestError);
@@ -656,7 +652,7 @@ describe("Managed connection write protection", () => {
656
652
 
657
653
  test("allows PATCH with auth still set to platform (no-op auth change)", async () => {
658
654
  seedConnection({
659
- name: "anthropic-managed",
655
+ name: "vellum",
660
656
  provider: "anthropic",
661
657
  auth: { type: "platform" },
662
658
  });
@@ -664,7 +660,7 @@ describe("Managed connection write protection", () => {
664
660
  const result = (await call(
665
661
  findHandler("inference_provider_connections_update"),
666
662
  {
667
- pathParams: { name: "anthropic-managed" },
663
+ pathParams: { name: "vellum" },
668
664
  body: {
669
665
  auth: { type: "platform" },
670
666
  label: "Vellum-managed Anthropic",
@@ -678,7 +674,7 @@ describe("Managed connection write protection", () => {
678
674
  describe("PATCH label (allowed)", () => {
679
675
  test("allows relabeling a managed connection", async () => {
680
676
  seedConnection({
681
- name: "openai-managed",
677
+ name: "vellum",
682
678
  provider: "openai",
683
679
  auth: { type: "platform" },
684
680
  });
@@ -686,7 +682,7 @@ describe("Managed connection write protection", () => {
686
682
  const result = (await call(
687
683
  findHandler("inference_provider_connections_update"),
688
684
  {
689
- pathParams: { name: "openai-managed" },
685
+ pathParams: { name: "vellum" },
690
686
  body: { auth: { type: "platform" }, label: "Custom Label" },
691
687
  },
692
688
  )) as { label: string | null };
@@ -698,11 +694,7 @@ describe("Managed connection write protection", () => {
698
694
  // ── isManaged response flag ───────────────────────────────────────────────────
699
695
 
700
696
  describe("isManaged flag on connection responses", () => {
701
- const MANAGED_NAMES = [
702
- "anthropic-managed",
703
- "openai-managed",
704
- "gemini-managed",
705
- ] as const;
697
+ const MANAGED_NAMES = ["vellum"] as const;
706
698
 
707
699
  describe("GET list", () => {
708
700
  test("returns isManaged: true for canonical names and false for user-created rows", async () => {
@@ -727,24 +719,69 @@ describe("isManaged flag on connection responses", () => {
727
719
  const byName = Object.fromEntries(
728
720
  result.connections.map((c) => [c.name, c.isManaged]),
729
721
  );
730
- expect(byName["anthropic-managed"]).toBe(true);
731
- expect(byName["openai-managed"]).toBe(true);
732
- expect(byName["gemini-managed"]).toBe(true);
722
+ expect(byName["vellum"]).toBe(true);
733
723
  expect(byName["my-custom-anthropic"]).toBe(false);
734
724
  });
725
+
726
+ test("hides orphaned legacy *-managed rows from the list", async () => {
727
+ // Existing installs (and fresh installs via migration 243) may still
728
+ // carry the pre-consolidation rows until a follow-up migration deletes
729
+ // them; they must not surface in the UI alongside `vellum`.
730
+ for (const name of [
731
+ "anthropic-managed",
732
+ "openai-managed",
733
+ "gemini-managed",
734
+ "fireworks-managed",
735
+ "together-managed",
736
+ ]) {
737
+ seedConnection({
738
+ name,
739
+ provider: name.replace("-managed", ""),
740
+ auth: { type: "platform" },
741
+ });
742
+ }
743
+ seedConnection({
744
+ name: "vellum",
745
+ provider: "vellum",
746
+ auth: { type: "platform" },
747
+ });
748
+ seedConnection({
749
+ name: "my-openai",
750
+ provider: "openai",
751
+ auth: { type: "api_key", credential: "ref/k" },
752
+ });
753
+
754
+ const result = (await call(
755
+ findHandler("inference_provider_connections_list"),
756
+ {},
757
+ )) as { connections: Array<{ name: string }> };
758
+ const names = result.connections.map((c) => c.name);
759
+
760
+ expect(names).toContain("vellum");
761
+ expect(names).toContain("my-openai");
762
+ for (const legacy of [
763
+ "anthropic-managed",
764
+ "openai-managed",
765
+ "gemini-managed",
766
+ "fireworks-managed",
767
+ "together-managed",
768
+ ]) {
769
+ expect(names).not.toContain(legacy);
770
+ }
771
+ });
735
772
  });
736
773
 
737
774
  describe("GET single", () => {
738
775
  test("returns isManaged: true for a managed name", async () => {
739
776
  seedConnection({
740
- name: "anthropic-managed",
777
+ name: "vellum",
741
778
  provider: "anthropic",
742
779
  auth: { type: "platform" },
743
780
  });
744
781
 
745
782
  const result = (await call(
746
783
  findHandler("inference_provider_connections_get"),
747
- { pathParams: { name: "anthropic-managed" } },
784
+ { pathParams: { name: "vellum" } },
748
785
  )) as { name: string; isManaged: boolean };
749
786
 
750
787
  expect(result.isManaged).toBe(true);
@@ -786,7 +823,7 @@ describe("isManaged flag on connection responses", () => {
786
823
  describe("PATCH update", () => {
787
824
  test("returns isManaged: true after relabeling a managed connection", async () => {
788
825
  seedConnection({
789
- name: "anthropic-managed",
826
+ name: "vellum",
790
827
  provider: "anthropic",
791
828
  auth: { type: "platform" },
792
829
  });
@@ -794,7 +831,7 @@ describe("isManaged flag on connection responses", () => {
794
831
  const result = (await call(
795
832
  findHandler("inference_provider_connections_update"),
796
833
  {
797
- pathParams: { name: "anthropic-managed" },
834
+ pathParams: { name: "vellum" },
798
835
  body: { auth: { type: "platform" }, label: "Vellum Anthropic" },
799
836
  },
800
837
  )) as { name: string; isManaged: boolean };
@@ -84,11 +84,15 @@ import { MEMORY_V2_CONSOLIDATION_SOURCE } from "../../plugins/defaults/memory/v2
84
84
  import { getMemoryV3SelectionForInspectorByMessageIds } from "../../plugins/defaults/memory/v3/selection-log-store.js";
85
85
  import {
86
86
  createConnection,
87
+ getConnection,
88
+ LEGACY_MANAGED_CONNECTION_NAMES,
87
89
  listConnections,
88
90
  PROVIDERS_REQUIRING_BASE_URL_AND_MODELS,
91
+ VELLUM_MANAGED_CONNECTION_NAME,
89
92
  } from "../../providers/inference/connections.js";
90
93
  import { PROVIDER_CATALOG } from "../../providers/model-catalog.js";
91
94
  import { initializeProviders } from "../../providers/registry.js";
95
+ import { MANAGED_ROUTABLE_PROVIDERS } from "../../providers/vellum-model-routing.js";
92
96
  import { credentialKey } from "../../security/credential-key.js";
93
97
  import { validateAllowlistFile } from "../../security/secret-allowlist.js";
94
98
  import {
@@ -1296,9 +1300,22 @@ async function handleReplaceInferenceProfile({
1296
1300
  if (!isManaged && fragment.provider && !fragment.provider_connection) {
1297
1301
  const provider = fragment.provider as string;
1298
1302
  const db = getDb();
1299
- const [active] = listConnections(db, { provider });
1303
+ // Exclude the orphaned legacy `*-managed` rows: they may still linger in
1304
+ // provider_connections on upgraded workspaces (hidden from the list route
1305
+ // until a follow-up migration deletes them). Auto-binding to one would keep
1306
+ // the profile stale and break it once those rows are removed.
1307
+ const [active] = listConnections(db, { provider }).filter(
1308
+ (c) => !LEGACY_MANAGED_CONNECTION_NAMES.has(c.name),
1309
+ );
1300
1310
  if (active) {
1301
1311
  fragment.provider_connection = active.name;
1312
+ } else if (
1313
+ MANAGED_ROUTABLE_PROVIDERS.has(provider) &&
1314
+ getConnection(db, VELLUM_MANAGED_CONNECTION_NAME)
1315
+ ) {
1316
+ // Managed-routable providers are served by the single Vellum-managed
1317
+ // connection; prefer it over lazily creating a personal connection.
1318
+ fragment.provider_connection = VELLUM_MANAGED_CONNECTION_NAME;
1302
1319
  } else if (!PROVIDERS_REQUIRING_BASE_URL_AND_MODELS.has(provider)) {
1303
1320
  const connectionName = `${provider}-personal`;
1304
1321
  const isKeyless = provider === "ollama";
@@ -46,7 +46,7 @@ import type {
46
46
  MessageProcessor,
47
47
  SlackInboundMessageMetadata,
48
48
  } from "../../http-types.js";
49
- import { hasDeliverableAssistantText } from "../../slack-no-response.js";
49
+ import { hasDeliverableAssistantText } from "../../no-response.js";
50
50
  import { createSlackReplySession } from "../../slack-reply-session.js";
51
51
  import type { TaskProgressData } from "../../slack-task-progress.js";
52
52
  import {
@@ -25,6 +25,7 @@ import {
25
25
  createConnection,
26
26
  deleteConnection,
27
27
  getConnection,
28
+ LEGACY_MANAGED_CONNECTION_NAMES,
28
29
  listConnections,
29
30
  MANAGED_CONNECTION_NAMES,
30
31
  PROVIDERS_REQUIRING_BASE_URL_AND_MODELS,
@@ -162,7 +163,7 @@ function handleListConnections({ queryParams = {} }: RouteHandlerArgs) {
162
163
  const connections = listConnections(
163
164
  getDb(),
164
165
  provider ? { provider } : undefined,
165
- );
166
+ ).filter((c) => !LEGACY_MANAGED_CONNECTION_NAMES.has(c.name));
166
167
  return { connections };
167
168
  }
168
169
 
@@ -461,7 +462,7 @@ export const ROUTES: RouteDefinition[] = [
461
462
  },
462
463
  summary: "Update a provider connection",
463
464
  description:
464
- "Update an existing connection. Cannot rename or change the provider. For managed connections (anthropic-managed, openai-managed, gemini-managed) the auth is locked to platform; label remains editable.",
465
+ "Update an existing connection. Cannot rename or change the provider. For the Vellum-managed connection (vellum) the auth is locked to platform; label remains editable.",
465
466
  tags: ["inference"],
466
467
  pathParams: [{ name: "name", description: "Connection name" }],
467
468
  requestBody: z.object({
@@ -490,7 +491,7 @@ export const ROUTES: RouteDefinition[] = [
490
491
  },
491
492
  summary: "Delete a provider connection",
492
493
  description:
493
- "Delete a provider connection. Fails with 400 for managed connections (anthropic-managed, openai-managed, gemini-managed) which are re-seeded on boot. Fails with 409 if any profile or call-site references the connection.",
494
+ "Delete a provider connection. Fails with 400 for the Vellum-managed connection (vellum) which is re-seeded on boot. Fails with 409 if any profile or call-site references the connection.",
494
495
  tags: ["inference"],
495
496
  pathParams: [{ name: "name", description: "Connection name" }],
496
497
  responseBody: z.object({ ok: z.literal(true) }),
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Platform route handlers for the shared HTTP/IPC route table.
3
3
  *
4
- * Serves five operations:
4
+ * Serves six operations:
5
5
  * - platform_status (GET platform/status): aggregates platform context,
6
6
  * credentials, assistant ID, webhook secret, and Velay tunnel status.
7
7
  * - platform_connect (POST platform/connect): checks existing credentials
@@ -12,6 +12,8 @@
12
12
  * registers a callback route with the platform gateway.
13
13
  * - platform_callback_routes_list (GET platform/callback-routes): lists
14
14
  * registered callback routes for this assistant.
15
+ * - platform_credits (GET platform/credits): fetches the org's remaining
16
+ * credit balance from the platform billing summary.
15
17
  */
16
18
 
17
19
  import { z } from "zod";
@@ -118,6 +120,16 @@ type CallbackRoutesListResponse = z.infer<
118
120
  typeof CallbackRoutesListResponseSchema
119
121
  >;
120
122
 
123
+ const PlatformCreditsResponseSchema = z.object({
124
+ remaining: z.number(),
125
+ settled: z.number(),
126
+ pending: z.number(),
127
+ unit: z.literal("USD"),
128
+ stale: z.boolean(),
129
+ as_of: z.string(),
130
+ });
131
+ type PlatformCreditsResponse = z.infer<typeof PlatformCreditsResponseSchema>;
132
+
121
133
  // ---------------------------------------------------------------------------
122
134
  // Handlers
123
135
  // ---------------------------------------------------------------------------
@@ -352,6 +364,60 @@ async function handleCallbackRoutesList(
352
364
  return { routes };
353
365
  }
354
366
 
367
+ async function handlePlatformCredits(
368
+ _args: RouteHandlerArgs,
369
+ ): Promise<PlatformCreditsResponse> {
370
+ const context = await resolvePlatformCallbackRegistrationContext();
371
+
372
+ if (!context.platformBaseUrl || !context.authHeader) {
373
+ throw new UnprocessableEntityError(
374
+ "Platform credentials not available — run 'assistant platform connect' or set VELLUM_PLATFORM_URL",
375
+ );
376
+ }
377
+
378
+ const url = `${context.platformBaseUrl}/v1/organizations/billing/summary/`;
379
+ let response: Response;
380
+ try {
381
+ response = await fetch(url, {
382
+ method: "GET",
383
+ headers: {
384
+ Authorization: context.authHeader,
385
+ Accept: "application/json",
386
+ },
387
+ signal: AbortSignal.timeout(10_000),
388
+ });
389
+ } catch (err) {
390
+ throw new InternalError(
391
+ `Failed to fetch credit balance: ${(err as Error).message}`,
392
+ );
393
+ }
394
+
395
+ if (!response.ok) {
396
+ const detail = await response.text().catch(() => "");
397
+ throw new InternalError(
398
+ `Failed to fetch credit balance (HTTP ${response.status}): ${detail}`,
399
+ );
400
+ }
401
+
402
+ const summary = (await response.json()) as {
403
+ settled_balance_usd: string;
404
+ pending_compute_usd: string;
405
+ effective_balance_usd: string;
406
+ is_degraded: boolean;
407
+ };
408
+
409
+ return {
410
+ remaining: Number(summary.effective_balance_usd),
411
+ settled: Number(summary.settled_balance_usd),
412
+ pending: Number(summary.pending_compute_usd),
413
+ unit: "USD",
414
+ stale: summary.is_degraded,
415
+ // as_of is response receipt time; add a server as_of field if the billing
416
+ // summary endpoint ever returns one.
417
+ as_of: new Date().toISOString(),
418
+ };
419
+ }
420
+
355
421
  // ---------------------------------------------------------------------------
356
422
  // Route definitions
357
423
  // ---------------------------------------------------------------------------
@@ -433,4 +499,19 @@ export const ROUTES: RouteDefinition[] = [
433
499
  handler: handleCallbackRoutesList,
434
500
  responseBody: CallbackRoutesListResponseSchema,
435
501
  },
502
+ {
503
+ operationId: "platform_credits",
504
+ endpoint: "platform/credits",
505
+ method: "GET",
506
+ policy: {
507
+ requiredScopes: ["settings.read"],
508
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
509
+ },
510
+ summary: "Get the organization's remaining credit balance",
511
+ description:
512
+ "Fetches the org's settled, pending, and effective (remaining) credit balance in USD from the platform billing summary.",
513
+ tags: ["platform"],
514
+ handler: handlePlatformCredits,
515
+ responseBody: PlatformCreditsResponseSchema,
516
+ },
436
517
  ];
@@ -18,7 +18,7 @@ import { deliverChannelReply } from "./gateway-client.js";
18
18
  import {
19
19
  hasDeliverableAssistantText,
20
20
  NO_RESPONSE_INLINE_RE,
21
- } from "./slack-no-response.js";
21
+ } from "./no-response.js";
22
22
  import type { TaskProgressData } from "./slack-task-progress.js";
23
23
  import {
24
24
  getTaskProgressDataFromSurfaceData,
@@ -0,0 +1,99 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type { WorkspaceMigration } from "./types.js";
5
+
6
+ // Repoint every `provider_connection` that referenced a per-provider
7
+ // `*-managed` connection onto the single provider-agnostic `vellum`
8
+ // connection.
9
+ //
10
+ // The five managed connections (anthropic/openai/gemini/fireworks/together)
11
+ // are collapsed into one `vellum` row and are no longer seeded (a follow-up
12
+ // migration deletes the orphaned rows), so any config still pointing at them
13
+ // would fail to resolve. Managed profiles also self-heal via the boot-time
14
+ // template reconcile, but `llm.default`, per-call-site overrides, and
15
+ // user-owned copies are rewritten here.
16
+ //
17
+ // Only the connection reference changes — each entry keeps its `provider` and
18
+ // `model`, which is how the `vellum` connection recovers the upstream at
19
+ // dispatch time. Names are hardcoded: this is a frozen historical snapshot.
20
+
21
+ const LEGACY_MANAGED_CONNECTIONS = new Set([
22
+ "anthropic-managed",
23
+ "openai-managed",
24
+ "gemini-managed",
25
+ "fireworks-managed",
26
+ "together-managed",
27
+ ]);
28
+
29
+ const VELLUM_CONNECTION = "vellum";
30
+
31
+ export const repointManagedConnectionsToVellumMigration: WorkspaceMigration = {
32
+ id: "125-repoint-managed-connections-to-vellum",
33
+ description:
34
+ "Repoint provider_connection from the per-provider *-managed connections to the single vellum connection",
35
+ run(workspaceDir: string): void {
36
+ const configPath = join(workspaceDir, "config.json");
37
+ if (!existsSync(configPath)) return;
38
+
39
+ let config: Record<string, unknown>;
40
+ try {
41
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
42
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
43
+ config = raw as Record<string, unknown>;
44
+ } catch {
45
+ return;
46
+ }
47
+
48
+ const llm = readObject(config.llm);
49
+ if (llm === null) return;
50
+
51
+ let changed = false;
52
+
53
+ // llm.default
54
+ if (repointEntry(readObject(llm.default))) changed = true;
55
+
56
+ // llm.profiles.* (managed and user-owned alike — a repointed reference is
57
+ // correct regardless of who owns the profile)
58
+ const profiles = readObject(llm.profiles);
59
+ if (profiles !== null) {
60
+ for (const key of Object.keys(profiles)) {
61
+ if (repointEntry(readObject(profiles[key]))) changed = true;
62
+ }
63
+ }
64
+
65
+ // llm.callSites.*
66
+ const callSites = readObject(llm.callSites);
67
+ if (callSites !== null) {
68
+ for (const key of Object.keys(callSites)) {
69
+ if (repointEntry(readObject(callSites[key]))) changed = true;
70
+ }
71
+ }
72
+
73
+ if (changed) {
74
+ config.llm = llm;
75
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
76
+ }
77
+ },
78
+ down(_workspaceDir: string): void {
79
+ // Forward-only.
80
+ },
81
+ };
82
+
83
+ /** Rewrite a single entry's provider_connection in place. Returns true if changed. */
84
+ function repointEntry(entry: Record<string, unknown> | null): boolean {
85
+ if (entry === null) return false;
86
+ const current = entry.provider_connection;
87
+ if (typeof current !== "string" || !LEGACY_MANAGED_CONNECTIONS.has(current)) {
88
+ return false;
89
+ }
90
+ entry.provider_connection = VELLUM_CONNECTION;
91
+ return true;
92
+ }
93
+
94
+ function readObject(value: unknown): Record<string, unknown> | null {
95
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
96
+ return null;
97
+ }
98
+ return value as Record<string, unknown>;
99
+ }
@@ -122,6 +122,7 @@ import { seedDefaultUserGuardrailsMigration } from "./121-seed-default-user-guar
122
122
  import { relocateDefaultUserBoundaryMigration } from "./122-relocate-default-user-boundary.js";
123
123
  import { swapQualityProfileToFableMigration } from "./123-swap-quality-profile-to-fable.js";
124
124
  import { correctDefaultUserBoundaryCommentsMigration } from "./124-correct-default-user-boundary-comments.js";
125
+ import { repointManagedConnectionsToVellumMigration } from "./125-repoint-managed-connections-to-vellum.js";
125
126
  import { migrateToWorkspaceVolumeMigration } from "./migrate-to-workspace-volume.js";
126
127
  import type { WorkspaceMigration } from "./types.js";
127
128
 
@@ -255,4 +256,5 @@ export const WORKSPACE_MIGRATIONS: WorkspaceMigration[] = [
255
256
  relocateDefaultUserBoundaryMigration,
256
257
  swapQualityProfileToFableMigration,
257
258
  correctDefaultUserBoundaryCommentsMigration,
259
+ repointManagedConnectionsToVellumMigration,
258
260
  ];