@vellumai/assistant 0.12.5-dev.202609281715.b73f696 → 0.12.5-dev.202609281822.fb70dee
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/config-get-vision-flag.test.ts +1 -1
- package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
- package/src/__tests__/conversation-pairing.test.ts +112 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +159 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -4
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
- package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
- package/src/notifications/README.md +41 -3
- package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
- package/src/notifications/__tests__/broadcaster.test.ts +299 -4
- package/src/notifications/__tests__/completion-policy.test.ts +37 -0
- package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
- package/src/notifications/assistant-initiated-thread.ts +101 -0
- package/src/notifications/broadcaster.ts +71 -12
- package/src/notifications/completion-policy.ts +7 -1
- package/src/notifications/conversation-pairing.ts +15 -70
- package/src/notifications/emit-signal.ts +34 -3
- package/src/plugin-api/vision-support.test.ts +9 -9
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
- package/src/providers/model-catalog.ts +3 -17
- package/src/util/pricing.ts +25 -0
- package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
- package/src/workspace/migrations/registry.ts +2 -0
package/package.json
CHANGED
|
@@ -171,7 +171,7 @@ describe("config-validation-reset notice sentinel", () => {
|
|
|
171
171
|
profiles: {
|
|
172
172
|
"glm-default": {
|
|
173
173
|
provider: "vellum",
|
|
174
|
-
model: "accounts/fireworks/models/glm-
|
|
174
|
+
model: "accounts/fireworks/models/glm-5p3",
|
|
175
175
|
},
|
|
176
176
|
},
|
|
177
177
|
activeProfile: "glm-default",
|
|
@@ -193,7 +193,7 @@ describe("config-validation-reset notice sentinel", () => {
|
|
|
193
193
|
expect(config.llm.activeProfile).toBe("glm-default");
|
|
194
194
|
expect(config.llm.callSites.mainAgent?.profile).toBe("glm-default");
|
|
195
195
|
expect(config.llm.profiles["glm-default"]?.model).toBe(
|
|
196
|
-
"accounts/fireworks/models/glm-
|
|
196
|
+
"accounts/fireworks/models/glm-5p3",
|
|
197
197
|
);
|
|
198
198
|
});
|
|
199
199
|
|
|
@@ -116,6 +116,7 @@ mock.module("../config/assistant-initiated-threads-gate.js", () => ({
|
|
|
116
116
|
isAssistantInitiatedThreadsEnabled: () => assistantInitiatedThreadsEnabled,
|
|
117
117
|
}));
|
|
118
118
|
|
|
119
|
+
import { resolveAssistantInitiatedThread } from "../notifications/assistant-initiated-thread.js";
|
|
119
120
|
import { pairDeliveryWithConversation } from "../notifications/conversation-pairing.js";
|
|
120
121
|
import type { NotificationSignal } from "../notifications/signal.js";
|
|
121
122
|
import type {
|
|
@@ -1478,6 +1479,117 @@ describe("pairDeliveryWithConversation", () => {
|
|
|
1478
1479
|
...overrides,
|
|
1479
1480
|
});
|
|
1480
1481
|
|
|
1482
|
+
test("a prepared resolution is reused even if the source changes before pairing", async () => {
|
|
1483
|
+
assistantInitiatedThreadsEnabled = true;
|
|
1484
|
+
const signal = share({
|
|
1485
|
+
conversationMetadata: { groupId: "group-123" },
|
|
1486
|
+
});
|
|
1487
|
+
const original = structuredClone(signal);
|
|
1488
|
+
const assistantInitiatedThread = resolveAssistantInitiatedThread(signal);
|
|
1489
|
+
getConversationMock.mockClear();
|
|
1490
|
+
mockExistingConversations[signal.sourceContextId] = {
|
|
1491
|
+
id: signal.sourceContextId,
|
|
1492
|
+
source: "user",
|
|
1493
|
+
title: null,
|
|
1494
|
+
conversationType: "standard",
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
const result = await pairDeliveryWithConversation(
|
|
1498
|
+
signal,
|
|
1499
|
+
"vellum",
|
|
1500
|
+
makeCopy(),
|
|
1501
|
+
{ assistantInitiatedThread },
|
|
1502
|
+
);
|
|
1503
|
+
|
|
1504
|
+
expect(result.createdNewConversation).toBe(true);
|
|
1505
|
+
expect(result.messageId).toBe("msg-001");
|
|
1506
|
+
expect(createConversationMock).toHaveBeenCalledWith(
|
|
1507
|
+
expect.objectContaining({
|
|
1508
|
+
source: "assistant_initiated",
|
|
1509
|
+
groupId: "group-123",
|
|
1510
|
+
}),
|
|
1511
|
+
);
|
|
1512
|
+
expect(getConversationMock).not.toHaveBeenCalled();
|
|
1513
|
+
expect(signal).toEqual(original);
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
test("an ineligible prepared resolution retains passive pairing when the flag changes", async () => {
|
|
1517
|
+
const signal = share();
|
|
1518
|
+
const assistantInitiatedThread = resolveAssistantInitiatedThread(signal);
|
|
1519
|
+
assistantInitiatedThreadsEnabled = true;
|
|
1520
|
+
mockExistingConversations[signal.sourceContextId] = {
|
|
1521
|
+
id: signal.sourceContextId,
|
|
1522
|
+
source: "heartbeat",
|
|
1523
|
+
title: null,
|
|
1524
|
+
conversationType: "background",
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
const result = await pairDeliveryWithConversation(
|
|
1528
|
+
signal,
|
|
1529
|
+
"vellum",
|
|
1530
|
+
makeCopy(),
|
|
1531
|
+
{ assistantInitiatedThread },
|
|
1532
|
+
);
|
|
1533
|
+
|
|
1534
|
+
expect(result.createdNewConversation).toBe(false);
|
|
1535
|
+
expect(result.conversationId).toBe(signal.sourceContextId);
|
|
1536
|
+
expect(createConversationMock).not.toHaveBeenCalled();
|
|
1537
|
+
expect(addMessageMock).toHaveBeenCalledTimes(1);
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
test.each(["slack", "telegram", "discord"] as const)(
|
|
1541
|
+
"%s retains the producer metadata despite a prepared vellum resolution",
|
|
1542
|
+
async (channel) => {
|
|
1543
|
+
assistantInitiatedThreadsEnabled = true;
|
|
1544
|
+
const signal = share({
|
|
1545
|
+
conversationMetadata: { groupId: "group-123" },
|
|
1546
|
+
});
|
|
1547
|
+
const original = structuredClone(signal);
|
|
1548
|
+
const assistantInitiatedThread =
|
|
1549
|
+
resolveAssistantInitiatedThread(signal);
|
|
1550
|
+
getConversationMock.mockClear();
|
|
1551
|
+
|
|
1552
|
+
const result = await pairDeliveryWithConversation(
|
|
1553
|
+
signal,
|
|
1554
|
+
channel,
|
|
1555
|
+
makeCopy(),
|
|
1556
|
+
{ assistantInitiatedThread },
|
|
1557
|
+
);
|
|
1558
|
+
|
|
1559
|
+
expect(result.createdNewConversation).toBe(true);
|
|
1560
|
+
expect(result.strategy).toBe("continue_existing_conversation");
|
|
1561
|
+
expect(createConversationMock).toHaveBeenCalledWith(
|
|
1562
|
+
expect.objectContaining({
|
|
1563
|
+
conversationType: "background",
|
|
1564
|
+
source: "notification",
|
|
1565
|
+
groupId: "group-123",
|
|
1566
|
+
}),
|
|
1567
|
+
);
|
|
1568
|
+
expect(addMessageMock).not.toHaveBeenCalled();
|
|
1569
|
+
expect(getConversationMock).not.toHaveBeenCalled();
|
|
1570
|
+
expect(signal).toEqual(original);
|
|
1571
|
+
},
|
|
1572
|
+
);
|
|
1573
|
+
|
|
1574
|
+
test("platform delivery does not pair a prepared vellum resolution", async () => {
|
|
1575
|
+
assistantInitiatedThreadsEnabled = true;
|
|
1576
|
+
const signal = share();
|
|
1577
|
+
const assistantInitiatedThread = resolveAssistantInitiatedThread(signal);
|
|
1578
|
+
|
|
1579
|
+
const result = await pairDeliveryWithConversation(
|
|
1580
|
+
signal,
|
|
1581
|
+
"platform",
|
|
1582
|
+
makeCopy(),
|
|
1583
|
+
{ assistantInitiatedThread },
|
|
1584
|
+
);
|
|
1585
|
+
|
|
1586
|
+
expect(result.strategy).toBe("push_only");
|
|
1587
|
+
expect(result.createdNewConversation).toBe(false);
|
|
1588
|
+
expect(result.conversationId).toBeNull();
|
|
1589
|
+
expect(createConversationMock).not.toHaveBeenCalled();
|
|
1590
|
+
expect(addMessageMock).not.toHaveBeenCalled();
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1481
1593
|
test("flag on: a share from a background run materializes a section thread", async () => {
|
|
1482
1594
|
assistantInitiatedThreadsEnabled = true;
|
|
1483
1595
|
mockExistingConversations["conv-heartbeat"] = {
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import { saveRawConfig } from "../config/loader.js";
|
|
4
|
+
import type { NotificationSignal } from "../notifications/signal.js";
|
|
4
5
|
import type { NotificationChannel } from "../notifications/types.js";
|
|
5
6
|
|
|
7
|
+
let assistantInitiatedCandidate = false;
|
|
8
|
+
const resolveAssistantInitiatedThreadMock = mock(
|
|
9
|
+
(signal: NotificationSignal) => ({
|
|
10
|
+
isCandidate: assistantInitiatedCandidate,
|
|
11
|
+
vellumSignal: assistantInitiatedCandidate
|
|
12
|
+
? {
|
|
13
|
+
...signal,
|
|
14
|
+
requiresConversation: true,
|
|
15
|
+
conversationMetadata: { source: "assistant_initiated" },
|
|
16
|
+
}
|
|
17
|
+
: signal,
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
mock.module("../notifications/assistant-initiated-thread.js", () => ({
|
|
21
|
+
resolveAssistantInitiatedThread: resolveAssistantInitiatedThreadMock,
|
|
22
|
+
}));
|
|
23
|
+
|
|
6
24
|
const evaluateSignalMock = mock();
|
|
7
25
|
const enforceRoutingIntentMock = mock();
|
|
8
26
|
const updateDecisionMock = mock();
|
|
@@ -113,6 +131,8 @@ import {
|
|
|
113
131
|
|
|
114
132
|
beforeEach(() => {
|
|
115
133
|
saveRawConfig({});
|
|
134
|
+
assistantInitiatedCandidate = false;
|
|
135
|
+
resolveAssistantInitiatedThreadMock.mockClear();
|
|
116
136
|
writeHomeFeedItemForSignalMock.mockClear();
|
|
117
137
|
evaluateSignalMock.mockReset();
|
|
118
138
|
enforceRoutingIntentMock.mockReset();
|
|
@@ -1020,3 +1040,142 @@ describe("classifyNotificationReceipt", () => {
|
|
|
1020
1040
|
).toBe("unknown");
|
|
1021
1041
|
});
|
|
1022
1042
|
});
|
|
1043
|
+
|
|
1044
|
+
describe("emitNotificationSignal From me push defaults", () => {
|
|
1045
|
+
const params = {
|
|
1046
|
+
sourceEventName: "assistant.share",
|
|
1047
|
+
sourceChannel: "assistant_tool" as const,
|
|
1048
|
+
sourceContextId: "conv-background",
|
|
1049
|
+
attentionHints: {
|
|
1050
|
+
urgency: "low" as const,
|
|
1051
|
+
requiresAction: false,
|
|
1052
|
+
isAsyncBackground: true,
|
|
1053
|
+
visibleInSourceNow: false,
|
|
1054
|
+
},
|
|
1055
|
+
};
|
|
1056
|
+
const decision = (selectedChannels: NotificationChannel[] = ["vellum"]) => ({
|
|
1057
|
+
shouldNotify: true,
|
|
1058
|
+
selectedChannels,
|
|
1059
|
+
reasoningSummary: "Assistant-authored share",
|
|
1060
|
+
renderedCopy: { vellum: { title: "An update", body: "A useful result." } },
|
|
1061
|
+
dedupeKey: "share-1",
|
|
1062
|
+
confidence: 1,
|
|
1063
|
+
fallbackUsed: false,
|
|
1064
|
+
persistedDecisionId: "decision-share",
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
beforeEach(() => {
|
|
1068
|
+
assistantInitiatedCandidate = true;
|
|
1069
|
+
evaluateSignalMock.mockResolvedValue(decision());
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
test("adds configured platform after routing and persists the final decision", async () => {
|
|
1073
|
+
const result = await emitNotificationSignal(params);
|
|
1074
|
+
expect(result.selectedChannels).toEqual(["vellum", "platform"]);
|
|
1075
|
+
expect(resolveAssistantInitiatedThreadMock).toHaveBeenCalledTimes(1);
|
|
1076
|
+
expect(updateDecisionMock).toHaveBeenCalledWith(
|
|
1077
|
+
"decision-share",
|
|
1078
|
+
expect.objectContaining({
|
|
1079
|
+
selectedChannels: ["vellum", "platform"],
|
|
1080
|
+
}),
|
|
1081
|
+
);
|
|
1082
|
+
expect(dispatchDecisionMock.mock.calls[0]?.[0]).toMatchObject({
|
|
1083
|
+
attentionHints: { urgency: "low" },
|
|
1084
|
+
});
|
|
1085
|
+
expect(dispatchDecisionMock.mock.calls[0]?.[3]).toMatchObject({
|
|
1086
|
+
assistantInitiatedThread: {
|
|
1087
|
+
platformAdded: true,
|
|
1088
|
+
resolution: {
|
|
1089
|
+
isCandidate: true,
|
|
1090
|
+
vellumSignal: {
|
|
1091
|
+
requiresConversation: true,
|
|
1092
|
+
conversationMetadata: { source: "assistant_initiated" },
|
|
1093
|
+
},
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
test.each([
|
|
1100
|
+
"quiet",
|
|
1101
|
+
"unconfigured",
|
|
1102
|
+
"noncandidate",
|
|
1103
|
+
"suppressed",
|
|
1104
|
+
"single_channel",
|
|
1105
|
+
"external_only",
|
|
1106
|
+
"vellum_allowlist",
|
|
1107
|
+
])("respects %s policy without adding platform", async (scenario) => {
|
|
1108
|
+
assistantInitiatedCandidate = scenario !== "noncandidate";
|
|
1109
|
+
isPlatformClientConfiguredMock.mockResolvedValue(
|
|
1110
|
+
scenario !== "unconfigured",
|
|
1111
|
+
);
|
|
1112
|
+
evaluateSignalMock.mockResolvedValue({
|
|
1113
|
+
...decision(scenario === "external_only" ? ["telegram"] : ["vellum"]),
|
|
1114
|
+
shouldNotify: scenario !== "suppressed",
|
|
1115
|
+
});
|
|
1116
|
+
const result = await emitNotificationSignal({
|
|
1117
|
+
...params,
|
|
1118
|
+
routingIntent:
|
|
1119
|
+
scenario === "single_channel" ? "single_channel" : undefined,
|
|
1120
|
+
contextPayload: {
|
|
1121
|
+
quiet: scenario === "quiet",
|
|
1122
|
+
...(scenario === "vellum_allowlist"
|
|
1123
|
+
? { channelAllowlist: ["vellum"] }
|
|
1124
|
+
: {}),
|
|
1125
|
+
},
|
|
1126
|
+
});
|
|
1127
|
+
expect(result.selectedChannels).not.toContain("platform");
|
|
1128
|
+
expect(dispatchDecisionMock.mock.calls[0]?.[3]).toMatchObject({
|
|
1129
|
+
assistantInitiatedThread: { platformAdded: false },
|
|
1130
|
+
});
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
test("keeps existing platform and external selections independent of this feature", async () => {
|
|
1134
|
+
evaluateSignalMock.mockResolvedValue(
|
|
1135
|
+
decision(["vellum", "platform", "telegram"]),
|
|
1136
|
+
);
|
|
1137
|
+
const result = await emitNotificationSignal({
|
|
1138
|
+
...params,
|
|
1139
|
+
contextPayload: { quiet: true },
|
|
1140
|
+
});
|
|
1141
|
+
expect(result.selectedChannels).toEqual(["vellum", "platform", "telegram"]);
|
|
1142
|
+
expect(isPlatformClientConfiguredMock).not.toHaveBeenCalled();
|
|
1143
|
+
expect(dispatchDecisionMock.mock.calls[0]?.[3]).toMatchObject({
|
|
1144
|
+
assistantInitiatedThread: { platformAdded: false },
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
test("probes an unbound platform once across urgency and From me policy", async () => {
|
|
1149
|
+
isPlatformClientConfiguredMock.mockResolvedValue(false);
|
|
1150
|
+
const result = await emitNotificationSignal({
|
|
1151
|
+
...params,
|
|
1152
|
+
attentionHints: { ...params.attentionHints, urgency: "high" },
|
|
1153
|
+
});
|
|
1154
|
+
expect(result.selectedChannels).toEqual(["vellum"]);
|
|
1155
|
+
expect(isPlatformClientConfiguredMock).toHaveBeenCalledTimes(1);
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
test("preserves preferred external selections while adding the platform default", async () => {
|
|
1159
|
+
evaluateSignalMock.mockResolvedValue(decision(["vellum", "telegram"]));
|
|
1160
|
+
const result = await emitNotificationSignal(params);
|
|
1161
|
+
expect(result.selectedChannels).toEqual(["vellum", "telegram", "platform"]);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
test.each(["deduplicated", "source_active"])(
|
|
1165
|
+
"does not resolve candidates for %s signals",
|
|
1166
|
+
async (scenario) => {
|
|
1167
|
+
if (scenario === "deduplicated") {
|
|
1168
|
+
createEventMock.mockReturnValue(null);
|
|
1169
|
+
}
|
|
1170
|
+
await emitNotificationSignal({
|
|
1171
|
+
...params,
|
|
1172
|
+
attentionHints: {
|
|
1173
|
+
...params.attentionHints,
|
|
1174
|
+
visibleInSourceNow: scenario === "source_active",
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
expect(resolveAssistantInitiatedThreadMock).not.toHaveBeenCalled();
|
|
1178
|
+
expect(dispatchDecisionMock).not.toHaveBeenCalled();
|
|
1179
|
+
},
|
|
1180
|
+
);
|
|
1181
|
+
});
|
|
@@ -390,9 +390,9 @@ describe("LLM catalog parity: daemon vs client", () => {
|
|
|
390
390
|
});
|
|
391
391
|
});
|
|
392
392
|
|
|
393
|
-
test("Fireworks catalog includes GLM 5.
|
|
393
|
+
test("Fireworks catalog includes GLM 5.3", () => {
|
|
394
394
|
expect(
|
|
395
|
-
isModelInCatalog("fireworks", "accounts/fireworks/models/glm-
|
|
395
|
+
isModelInCatalog("fireworks", "accounts/fireworks/models/glm-5p3"),
|
|
396
396
|
).toBe(true);
|
|
397
397
|
|
|
398
398
|
const fireworks = PROVIDER_CATALOG.find(
|
|
@@ -400,10 +400,10 @@ describe("LLM catalog parity: daemon vs client", () => {
|
|
|
400
400
|
);
|
|
401
401
|
expect(
|
|
402
402
|
fireworks?.models.find(
|
|
403
|
-
(model) => model.id === "accounts/fireworks/models/glm-
|
|
403
|
+
(model) => model.id === "accounts/fireworks/models/glm-5p3",
|
|
404
404
|
),
|
|
405
405
|
).toMatchObject({
|
|
406
|
-
displayName: "GLM 5.
|
|
406
|
+
displayName: "GLM 5.3",
|
|
407
407
|
contextWindowTokens: 1040000,
|
|
408
408
|
maxOutputTokens: 131072,
|
|
409
409
|
supportsToolUse: true,
|
|
@@ -10,6 +10,17 @@ import {
|
|
|
10
10
|
} from "../util/pricing.js";
|
|
11
11
|
|
|
12
12
|
describe("resolvePricing", () => {
|
|
13
|
+
test.each([
|
|
14
|
+
["accounts/fireworks/models/glm-5p2", 1.4 + 4.4],
|
|
15
|
+
["accounts/fireworks/models/kimi-k2p6", 0.95 + 4.0],
|
|
16
|
+
["accounts/fireworks/models/deepseek-v4-pro-0813", 1.32 + 3.96],
|
|
17
|
+
["accounts/fireworks/models/deepseek-v4-flash-0731", 0.14 + 0.28],
|
|
18
|
+
])("prices stored usage for retired Fireworks model %s", (model, cost) => {
|
|
19
|
+
const result = resolvePricing("fireworks", model, 1_000_000, 1_000_000);
|
|
20
|
+
expect(result.pricingStatus).toBe("priced");
|
|
21
|
+
expect(result.estimatedCostUsd).toBeCloseTo(cost, 10);
|
|
22
|
+
});
|
|
23
|
+
|
|
13
24
|
describe("Anthropic models", () => {
|
|
14
25
|
test("returns priced for claude-opus-4-6", () => {
|
|
15
26
|
const result = resolvePricing(
|
|
@@ -100,7 +100,7 @@ describe("133-collapse-provider-connections migration", () => {
|
|
|
100
100
|
source: "user",
|
|
101
101
|
provider: "fireworks",
|
|
102
102
|
provider_connection: "vellum",
|
|
103
|
-
model: "accounts/fireworks/models/
|
|
103
|
+
model: "accounts/fireworks/models/minimax-m3",
|
|
104
104
|
},
|
|
105
105
|
},
|
|
106
106
|
},
|
|
@@ -257,7 +257,7 @@ describe("133-collapse-provider-connections migration", () => {
|
|
|
257
257
|
encoded: {
|
|
258
258
|
source: "user",
|
|
259
259
|
provider: "vellum",
|
|
260
|
-
model: "fireworks/accounts/fireworks/models/
|
|
260
|
+
model: "fireworks/accounts/fireworks/models/minimax-m3",
|
|
261
261
|
},
|
|
262
262
|
},
|
|
263
263
|
},
|
|
@@ -272,7 +272,7 @@ describe("133-collapse-provider-connections migration", () => {
|
|
|
272
272
|
expect(llm.profiles.encoded).toEqual({
|
|
273
273
|
source: "user",
|
|
274
274
|
provider: "vellum",
|
|
275
|
-
model: "accounts/fireworks/models/
|
|
275
|
+
model: "accounts/fireworks/models/minimax-m3",
|
|
276
276
|
});
|
|
277
277
|
});
|
|
278
278
|
|
package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { Database } from "bun:sqlite";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import { repairRetiredFireworksGlm5p2ModelIdMigration } from "../workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.js";
|
|
14
|
+
import { WORKSPACE_MIGRATIONS } from "../workspace/migrations/registry.js";
|
|
15
|
+
import { assertNotLiveDb } from "./assert-not-live-db.js";
|
|
16
|
+
|
|
17
|
+
const STALE = "accounts/fireworks/models/glm-5p2";
|
|
18
|
+
const REPLACEMENT = "accounts/fireworks/models/glm-5p3";
|
|
19
|
+
|
|
20
|
+
let workspaceDir: string;
|
|
21
|
+
|
|
22
|
+
function freshWorkspace(): void {
|
|
23
|
+
workspaceDir = join(
|
|
24
|
+
tmpdir(),
|
|
25
|
+
`vellum-migration-161-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
26
|
+
);
|
|
27
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeConfig(data: Record<string, unknown>): void {
|
|
31
|
+
writeFileSync(
|
|
32
|
+
join(workspaceDir, "config.json"),
|
|
33
|
+
JSON.stringify(data, null, 2) + "\n",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readConfig(): Record<string, unknown> {
|
|
38
|
+
return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function seedRows(rows: Array<{ name: string; provider: string }>): void {
|
|
42
|
+
mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
|
|
43
|
+
const db = new Database(join(workspaceDir, "data", "db", "assistant.db"));
|
|
44
|
+
db.run(`CREATE TABLE IF NOT EXISTS provider_connections (
|
|
45
|
+
name TEXT PRIMARY KEY,
|
|
46
|
+
provider TEXT NOT NULL,
|
|
47
|
+
auth TEXT NOT NULL,
|
|
48
|
+
created_at INTEGER NOT NULL,
|
|
49
|
+
updated_at INTEGER NOT NULL
|
|
50
|
+
)`);
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
db.query(
|
|
53
|
+
`INSERT INTO provider_connections (name, provider, auth, created_at, updated_at) VALUES (?, ?, '{"type":"api_key"}', 1, 1)`,
|
|
54
|
+
).run(row.name, row.provider);
|
|
55
|
+
}
|
|
56
|
+
db.close();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
freshWorkspace();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
if (existsSync(workspaceDir)) {
|
|
65
|
+
assertNotLiveDb(workspaceDir);
|
|
66
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("161-repair-retired-fireworks-glm-5p2-model-id migration", () => {
|
|
71
|
+
test("has correct migration id and is registered", () => {
|
|
72
|
+
expect(repairRetiredFireworksGlm5p2ModelIdMigration.id).toBe(
|
|
73
|
+
"161-repair-retired-fireworks-glm-5p2-model-id",
|
|
74
|
+
);
|
|
75
|
+
expect(WORKSPACE_MIGRATIONS.map((m) => m.id)).toContain(
|
|
76
|
+
"161-repair-retired-fireworks-glm-5p2-model-id",
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("repairs the stale ID in default, call sites, and profiles", () => {
|
|
81
|
+
writeConfig({
|
|
82
|
+
llm: {
|
|
83
|
+
default: { provider: "fireworks", model: STALE },
|
|
84
|
+
callSites: {
|
|
85
|
+
recall: { model: STALE, maxTokens: 4096 },
|
|
86
|
+
heartbeat: { model: `${STALE}-tuned` },
|
|
87
|
+
malformed: STALE,
|
|
88
|
+
},
|
|
89
|
+
profiles: {
|
|
90
|
+
"cost-optimized": { provider: "vellum", model: STALE },
|
|
91
|
+
legacy: { model: STALE },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
repairRetiredFireworksGlm5p2ModelIdMigration.run(workspaceDir);
|
|
97
|
+
|
|
98
|
+
const llm = readConfig().llm as Record<string, any>;
|
|
99
|
+
expect(llm.default.model).toBe(REPLACEMENT);
|
|
100
|
+
expect(llm.callSites.recall.model).toBe(REPLACEMENT);
|
|
101
|
+
expect(llm.callSites.recall.maxTokens).toBe(4096);
|
|
102
|
+
// Non-exact matches and malformed leaves are untouched.
|
|
103
|
+
expect(llm.callSites.heartbeat.model).toBe(`${STALE}-tuned`);
|
|
104
|
+
expect(llm.callSites.malformed).toBe(STALE);
|
|
105
|
+
// Managed profiles stamped provider "vellum" carry Fireworks model IDs.
|
|
106
|
+
expect(llm.profiles["cost-optimized"].model).toBe(REPLACEMENT);
|
|
107
|
+
expect(llm.profiles.legacy.model).toBe(REPLACEMENT);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("repairs entry-bound fragments whose row kind is fireworks or vellum", () => {
|
|
111
|
+
seedRows([
|
|
112
|
+
{ name: "my-fireworks", provider: "fireworks" },
|
|
113
|
+
{ name: "managed-alt", provider: "vellum" },
|
|
114
|
+
]);
|
|
115
|
+
writeConfig({
|
|
116
|
+
llm: {
|
|
117
|
+
profiles: {
|
|
118
|
+
bound: { provider: "my-fireworks", model: STALE },
|
|
119
|
+
managed: { provider: "managed-alt", model: STALE },
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
repairRetiredFireworksGlm5p2ModelIdMigration.run(workspaceDir);
|
|
125
|
+
|
|
126
|
+
const llm = readConfig().llm as Record<string, any>;
|
|
127
|
+
expect(llm.profiles.bound.model).toBe(REPLACEMENT);
|
|
128
|
+
expect(llm.profiles.managed.model).toBe(REPLACEMENT);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("leaves fragments with an explicit other provider untouched", () => {
|
|
132
|
+
seedRows([{ name: "byo-compat", provider: "openai-compatible" }]);
|
|
133
|
+
writeConfig({
|
|
134
|
+
llm: {
|
|
135
|
+
default: { provider: "openai-compatible", model: STALE },
|
|
136
|
+
profiles: {
|
|
137
|
+
byo: { provider: "openai-compatible", model: STALE },
|
|
138
|
+
// Entry-bound profile whose row kind is not repairable.
|
|
139
|
+
compatBound: { provider: "byo-compat", model: STALE },
|
|
140
|
+
// Entry name with no row: dangling, so nothing proves it is a
|
|
141
|
+
// fireworks-kind route.
|
|
142
|
+
dangling: { provider: "ghost", model: STALE },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
repairRetiredFireworksGlm5p2ModelIdMigration.run(workspaceDir);
|
|
148
|
+
|
|
149
|
+
const llm = readConfig().llm as Record<string, any>;
|
|
150
|
+
expect(llm.default.model).toBe(STALE);
|
|
151
|
+
expect(llm.profiles.byo.model).toBe(STALE);
|
|
152
|
+
expect(llm.profiles.compatBound.model).toBe(STALE);
|
|
153
|
+
expect(llm.profiles.dangling.model).toBe(STALE);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("leaves entry-name providers untouched when no DB file exists", () => {
|
|
157
|
+
writeConfig({
|
|
158
|
+
llm: {
|
|
159
|
+
profiles: { bound: { provider: "my-fireworks", model: STALE } },
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
repairRetiredFireworksGlm5p2ModelIdMigration.run(workspaceDir);
|
|
164
|
+
|
|
165
|
+
const llm = readConfig().llm as Record<string, any>;
|
|
166
|
+
expect(llm.profiles.bound.model).toBe(STALE);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("throws when an entry-name provider needs rows and the DB is unreadable", () => {
|
|
170
|
+
// A DB file without the provider_connections table is unqueryable, so
|
|
171
|
+
// the run must fail (and retry later) instead of skipping the profile.
|
|
172
|
+
mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
|
|
173
|
+
writeFileSync(join(workspaceDir, "data", "db", "assistant.db"), "");
|
|
174
|
+
writeConfig({
|
|
175
|
+
llm: {
|
|
176
|
+
profiles: { bound: { provider: "my-fireworks", model: STALE } },
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(() =>
|
|
181
|
+
repairRetiredFireworksGlm5p2ModelIdMigration.run(workspaceDir),
|
|
182
|
+
).toThrow();
|
|
183
|
+
const llm = readConfig().llm as Record<string, any>;
|
|
184
|
+
expect(llm.profiles.bound.model).toBe(STALE);
|
|
185
|
+
|
|
186
|
+
// Vendor and absent providers never need the rows, so the same broken
|
|
187
|
+
// DB does not block their repair.
|
|
188
|
+
writeConfig({
|
|
189
|
+
llm: {
|
|
190
|
+
default: { provider: "fireworks", model: STALE },
|
|
191
|
+
profiles: { legacy: { model: STALE } },
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
repairRetiredFireworksGlm5p2ModelIdMigration.run(workspaceDir);
|
|
195
|
+
const repaired = readConfig().llm as Record<string, any>;
|
|
196
|
+
expect(repaired.default.model).toBe(REPLACEMENT);
|
|
197
|
+
expect(repaired.profiles.legacy.model).toBe(REPLACEMENT);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("is idempotent and a no-op without the stale ID", () => {
|
|
201
|
+
writeConfig({
|
|
202
|
+
llm: {
|
|
203
|
+
default: { provider: "fireworks", model: STALE },
|
|
204
|
+
profiles: { fine: { provider: "fireworks", model: REPLACEMENT } },
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
repairRetiredFireworksGlm5p2ModelIdMigration.run(workspaceDir);
|
|
209
|
+
const first = readConfig();
|
|
210
|
+
repairRetiredFireworksGlm5p2ModelIdMigration.run(workspaceDir);
|
|
211
|
+
expect(readConfig()).toEqual(first);
|
|
212
|
+
|
|
213
|
+
const llm = first.llm as Record<string, any>;
|
|
214
|
+
expect(llm.default.model).toBe(REPLACEMENT);
|
|
215
|
+
expect(llm.profiles.fine.model).toBe(REPLACEMENT);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("handles missing config, missing llm block, and invalid JSON", () => {
|
|
219
|
+
// No config.json at all.
|
|
220
|
+
expect(() =>
|
|
221
|
+
repairRetiredFireworksGlm5p2ModelIdMigration.run(workspaceDir),
|
|
222
|
+
).not.toThrow();
|
|
223
|
+
|
|
224
|
+
writeConfig({ theme: "dark" });
|
|
225
|
+
repairRetiredFireworksGlm5p2ModelIdMigration.run(workspaceDir);
|
|
226
|
+
expect(readConfig()).toEqual({ theme: "dark" });
|
|
227
|
+
|
|
228
|
+
writeFileSync(join(workspaceDir, "config.json"), "{not json");
|
|
229
|
+
expect(() =>
|
|
230
|
+
repairRetiredFireworksGlm5p2ModelIdMigration.run(workspaceDir),
|
|
231
|
+
).not.toThrow();
|
|
232
|
+
});
|
|
233
|
+
});
|