@vellumai/assistant 0.12.4-staging.1 → 0.12.4-staging.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.
- package/openapi.yaml +3 -1
- package/package.json +1 -1
- package/src/__tests__/config-loader-backfill.test.ts +1 -0
- package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
- package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
- package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/__tests__/skill-load-tool.test.ts +42 -11
- package/src/__tests__/skill-tool-factory.test.ts +50 -2
- package/src/__tests__/validate-input.test.ts +73 -0
- package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
- package/src/api/requests/dictation.ts +2 -2
- package/src/calls/__tests__/typesafe-noul.test.ts +66 -0
- package/src/calls/typesafe-noul.ts +8 -2
- package/src/calls/voice-escalation-judge.ts +5 -4
- package/src/config/__tests__/default-profile-catalog.test.ts +33 -1
- package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
- package/src/config/call-site-defaults.ts +11 -11
- package/src/config/default-profile-catalog.ts +88 -18
- package/src/config/default-profile-names.ts +35 -0
- package/src/config/inference-profile-validation.ts +7 -6
- package/src/config/llm-resolver.ts +16 -0
- package/src/config/profile-text-generation.test.ts +9 -0
- package/src/config/schemas/call-site-catalog.ts +1 -1
- package/src/config/schemas/llm.ts +54 -9
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/dictation-text-processing.ts +4 -4
- package/src/plugin-api/model-profiles.ts +5 -3
- package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/providers/model-catalog.ts +257 -4
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
- package/src/runtime/routes/conversation-routes.ts +4 -2
- package/src/runtime/routes/diagnostics-routes.ts +66 -229
- package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
- package/src/skills/validate-input.ts +75 -11
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/format-input-schema.ts +15 -0
- package/src/tools/skills/load.ts +2 -26
- package/src/tools/skills/skill-tool-factory.ts +2 -1
- package/src/tools/workflows/manage-workflows.ts +4 -2
- package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
package/openapi.yaml
CHANGED
|
@@ -12060,7 +12060,9 @@ paths:
|
|
|
12060
12060
|
post:
|
|
12061
12061
|
operationId: dictation_post
|
|
12062
12062
|
summary: Process dictation
|
|
12063
|
-
description:
|
|
12063
|
+
description:
|
|
12064
|
+
Preserve dictated words with explicit dictionary and snippet replacements, classify actions, or edit
|
|
12065
|
+
selected text when instructed.
|
|
12064
12066
|
tags:
|
|
12065
12067
|
- diagnostics
|
|
12066
12068
|
requestBody:
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { validateInferenceProfileKey } from "../config/inference-profile-validation.js";
|
|
4
|
+
import { setConfig } from "./helpers/set-config.js";
|
|
5
|
+
|
|
6
|
+
describe("validateInferenceProfileKey and the managed Jev profile", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
setConfig("llm", { profiles: {} });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("a schedule or subagent turn cannot run on the verdict model", () => {
|
|
12
|
+
expect(validateInferenceProfileKey("balanced")).toBeNull();
|
|
13
|
+
expect(validateInferenceProfileKey("jev-managed")).toContain(
|
|
14
|
+
"is not defined in llm.profiles",
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -235,6 +235,10 @@ describe("LLM catalog parity: daemon vs client", () => {
|
|
|
235
235
|
|
|
236
236
|
test("jev-latest opts out of chat text generation", () => {
|
|
237
237
|
expect(catalogModelSupportsText("typesafe", "jev-latest")).toBe(false);
|
|
238
|
+
// A managed Jev profile is configured as `vellum`; the routing identity
|
|
239
|
+
// owns no catalog models, so the lookup must judge by the catalog owner.
|
|
240
|
+
expect(catalogModelSupportsText("vellum", "jev-latest")).toBe(false);
|
|
241
|
+
expect(catalogModelSupportsText("vellum", "claude-opus-5")).toBe(true);
|
|
238
242
|
expect(catalogModelSupportsText("anthropic", "claude-opus-4-8")).toBe(true);
|
|
239
243
|
expect(catalogModelSupportsText("openai-compatible", "local-model")).toBe(
|
|
240
244
|
true,
|
|
@@ -225,6 +225,49 @@ describe("composeCallSiteTweak", () => {
|
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
describe("shipped call-site tuning", () => {
|
|
228
|
+
test("the escalation judge uses managed Jev by default", () => {
|
|
229
|
+
const resolved = resolveCallSiteConfig(
|
|
230
|
+
"voiceEscalationJudge",
|
|
231
|
+
LLMSchema.parse({}),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
expect(resolved.provider).toBe("vellum");
|
|
235
|
+
expect(resolved.provider_connection).toBeUndefined();
|
|
236
|
+
expect(resolved.model).toBe("jev-latest");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("the escalation judge falls through on a BYOK default provider", () => {
|
|
240
|
+
const resolved = resolveCallSiteConfig(
|
|
241
|
+
"voiceEscalationJudge",
|
|
242
|
+
LLMSchema.parse(anthropicDp),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
expect(resolved.provider).toBe("anthropic");
|
|
246
|
+
expect(resolved.model).not.toBe("jev-latest");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("a BYOK Jev call-site override replaces the managed default", () => {
|
|
250
|
+
const llm = LLMSchema.parse({
|
|
251
|
+
...anthropicDp,
|
|
252
|
+
profiles: {
|
|
253
|
+
jev: {
|
|
254
|
+
...completeCustom,
|
|
255
|
+
provider: "typesafe",
|
|
256
|
+
provider_connection: "typesafe-personal",
|
|
257
|
+
model: "jev-latest",
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
callSites: {
|
|
261
|
+
voiceEscalationJudge: { profile: "jev" },
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
const resolved = resolveCallSiteConfig("voiceEscalationJudge", llm);
|
|
265
|
+
|
|
266
|
+
expect(resolved.provider).toBe("typesafe");
|
|
267
|
+
expect(resolved.provider_connection).toBe("typesafe-personal");
|
|
268
|
+
expect(resolved.model).toBe("jev-latest");
|
|
269
|
+
});
|
|
270
|
+
|
|
228
271
|
// A workspace entry that only repoints the profile must not cost the call
|
|
229
272
|
// site the tuning it ships with. `recall` carries a full set: a token
|
|
230
273
|
// budget, effort, sampling, thinking, and cache posture.
|
|
@@ -446,7 +446,7 @@ describe("OpenAIResponsesProvider explicit prompt caching (GPT-5.6+)", () => {
|
|
|
446
446
|
expect(JSON.stringify(messages)).not.toContain("prompt_cache_breakpoint");
|
|
447
447
|
});
|
|
448
448
|
|
|
449
|
-
test("catalog flags GPT-5.6 and GPT-6
|
|
449
|
+
test("catalog flags GPT-5.6 and GPT-6 direct-openai rows", () => {
|
|
450
450
|
const openai = PROVIDER_CATALOG.find((p) => p.id === "openai");
|
|
451
451
|
const flagged = (openai?.models ?? [])
|
|
452
452
|
.filter((m) => m.supportsPromptCacheBreakpoints)
|
|
@@ -457,6 +457,8 @@ describe("OpenAIResponsesProvider explicit prompt caching (GPT-5.6+)", () => {
|
|
|
457
457
|
"gpt-5.6-sol",
|
|
458
458
|
"gpt-5.6-terra",
|
|
459
459
|
"gpt-6-astra",
|
|
460
|
+
"gpt-6-luna",
|
|
461
|
+
"gpt-6-sol",
|
|
460
462
|
]);
|
|
461
463
|
});
|
|
462
464
|
});
|
|
@@ -40,6 +40,13 @@ describe("getModelProfiles", () => {
|
|
|
40
40
|
writeFixtureConfig({});
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
test("omits the managed Jev profile, whose model returns verdicts rather than text", () => {
|
|
44
|
+
writeFixtureConfig({});
|
|
45
|
+
const keys = getModelProfiles().map((p) => p.key);
|
|
46
|
+
expect(keys).toContain("balanced");
|
|
47
|
+
expect(keys).not.toContain("jev-managed");
|
|
48
|
+
});
|
|
49
|
+
|
|
43
50
|
test("orders profiles by profileOrder then the remaining keys alphabetically", () => {
|
|
44
51
|
// GIVEN a workspace whose profileOrder names some keys, with a duplicate
|
|
45
52
|
// ("balanced") and a key that resolves to no profile ("ghost").
|
|
@@ -135,6 +135,39 @@ describe("resolvePricing", () => {
|
|
|
135
135
|
expect(result.estimatedCostUsd).toBeCloseTo(0.5 + 0.625 + 0.05, 10);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
test("bills GPT-6 Sol at published short-context rates", () => {
|
|
139
|
+
const result = resolvePricingForUsage("openai", "gpt-6-sol", {
|
|
140
|
+
directInputTokens: 50_000,
|
|
141
|
+
outputTokens: 50_000,
|
|
142
|
+
cacheCreationInputTokens: 50_000,
|
|
143
|
+
cacheReadInputTokens: 50_000,
|
|
144
|
+
anthropicCacheCreation: null,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(result.pricingStatus).toBe("priced");
|
|
148
|
+
// 0.05M x $2 direct + 0.05M x $2.5 write + 0.05M x $0.2 read
|
|
149
|
+
// + 0.05M x $10 output.
|
|
150
|
+
expect(result.estimatedCostUsd).toBeCloseTo(0.1 + 0.125 + 0.01 + 0.5, 10);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("bills GPT-6 Luna at the long-context tier above 272k", () => {
|
|
154
|
+
const result = resolvePricingForUsage("openai", "gpt-6-luna", {
|
|
155
|
+
directInputTokens: 500_000,
|
|
156
|
+
outputTokens: 1_000_000,
|
|
157
|
+
cacheCreationInputTokens: 500_000,
|
|
158
|
+
cacheReadInputTokens: 1_000_000,
|
|
159
|
+
anthropicCacheCreation: null,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(result.pricingStatus).toBe("priced");
|
|
163
|
+
// Tier rates: 0.5M x $0.2 direct + 0.5M x $0.25 write + 1M x $0.02 read
|
|
164
|
+
// + 1M x $0.75 output.
|
|
165
|
+
expect(result.estimatedCostUsd).toBeCloseTo(
|
|
166
|
+
0.1 + 0.125 + 0.02 + 0.75,
|
|
167
|
+
10,
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
138
171
|
test("bills GPT-6 Astra at the long-context tier above 272k", () => {
|
|
139
172
|
const result = resolvePricingForUsage("openai", "gpt-6-astra", {
|
|
140
173
|
directInputTokens: 500_000,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, symlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
4
4
|
|
|
@@ -781,16 +781,48 @@ describe("skill_load tool", () => {
|
|
|
781
781
|
expect(result.content).toContain("Deploy the application to production");
|
|
782
782
|
expect(result.content).toContain("Rollback to previous version");
|
|
783
783
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
784
|
+
const schemas = [
|
|
785
|
+
...result.content.matchAll(/```json\n([\s\S]*?)\n```/g),
|
|
786
|
+
].map((match) => JSON.parse(match[1]));
|
|
787
|
+
expect(schemas[0].required).toEqual(["environment"]);
|
|
788
|
+
expect(schemas[0].properties.environment).toEqual({
|
|
789
|
+
type: "string",
|
|
790
|
+
description: "Target environment",
|
|
791
|
+
});
|
|
792
|
+
expect(schemas[0].properties.force.type).toBe("boolean");
|
|
793
|
+
expect(schemas[1].required).toEqual(["version"]);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
test("discloses the complete annotation mark schema before execution", async () => {
|
|
797
|
+
const manifest = JSON.parse(
|
|
798
|
+
readFileSync(
|
|
799
|
+
join(
|
|
800
|
+
import.meta.dir,
|
|
801
|
+
"../config/bundled-skills/screen-annotation/TOOLS.json",
|
|
802
|
+
),
|
|
803
|
+
"utf-8",
|
|
804
|
+
),
|
|
787
805
|
);
|
|
788
|
-
|
|
789
|
-
"-
|
|
806
|
+
writeSkill(
|
|
807
|
+
"annotation-schema",
|
|
808
|
+
"Annotation Schema",
|
|
809
|
+
"Point at a control",
|
|
810
|
+
"Name the control.",
|
|
790
811
|
);
|
|
791
|
-
|
|
792
|
-
|
|
812
|
+
writeToolsJson("annotation-schema", manifest.tools);
|
|
813
|
+
|
|
814
|
+
const result = await executeSkillLoad(
|
|
815
|
+
{ skill: "annotation-schema" },
|
|
816
|
+
"macos",
|
|
793
817
|
);
|
|
818
|
+
expect(result.isError).toBe(false);
|
|
819
|
+
const schemaMatch = result.content.match(/```json\n([\s\S]*?)\n```/);
|
|
820
|
+
expect(schemaMatch).not.toBeNull();
|
|
821
|
+
const schema = JSON.parse(schemaMatch![1]);
|
|
822
|
+
expect(schema).toEqual(manifest.tools[0].input_schema);
|
|
823
|
+
expect(schema.properties.marks.items.oneOf).toHaveLength(2);
|
|
824
|
+
expect(schema.properties.marks.items.properties.target.minLength).toBe(1);
|
|
825
|
+
expect(schema.properties.marks.items.properties.caption.maxLength).toBe(80);
|
|
794
826
|
});
|
|
795
827
|
|
|
796
828
|
test("skill without TOOLS.json does not include tool schemas section", async () => {
|
|
@@ -862,9 +894,8 @@ describe("skill_load tool", () => {
|
|
|
862
894
|
// The child skill's tool schemas should appear (#### level under ### Tools from …)
|
|
863
895
|
expect(result.content).toContain("#### child_action");
|
|
864
896
|
expect(result.content).toContain("A child tool action");
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
);
|
|
897
|
+
const schemaMatch = result.content.match(/```json\n([\s\S]*?)\n```/);
|
|
898
|
+
expect(JSON.parse(schemaMatch![1]).required).toEqual(["target"]);
|
|
868
899
|
expect(result.content).toContain(
|
|
869
900
|
"Use `skill_execute` to call these tools.",
|
|
870
901
|
);
|
|
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
6
6
|
|
|
7
|
-
import type
|
|
7
|
+
import { getBundledSkillsDir, type SkillToolEntry } from "../config/skills.js";
|
|
8
8
|
import { RiskLevel } from "../permissions/types.js";
|
|
9
9
|
import { computeSkillVersionHash } from "../skills/version-hash.js";
|
|
10
10
|
import { resolveSubagentRole } from "../subagent/role-resolution.js";
|
|
@@ -261,6 +261,54 @@ describe("createSkillTool — unknown parameter validation", () => {
|
|
|
261
261
|
expect(result.content).toContain('Unknown parameter "bar"');
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
+
test.each([
|
|
265
|
+
{ description: "Filters" },
|
|
266
|
+
{ marks: [{ description: "Filters" }] },
|
|
267
|
+
{ marks: [{}] },
|
|
268
|
+
{ marks: [{ target: 42 }] },
|
|
269
|
+
{ marks: [{ x: 0.1, y: 0.2 }] },
|
|
270
|
+
{ marks: [{ target: "Filters", x: 0.1, y: 0.2, width: 0.3, height: 0.4 }] },
|
|
271
|
+
])(
|
|
272
|
+
"an annotation validation error provides enough schema for one corrected retry: %j",
|
|
273
|
+
async (invalid) => {
|
|
274
|
+
const skillDir = join(getBundledSkillsDir(), "screen-annotation");
|
|
275
|
+
const manifest = JSON.parse(
|
|
276
|
+
readFileSync(join(skillDir, "TOOLS.json"), "utf-8"),
|
|
277
|
+
);
|
|
278
|
+
const entry = manifest.tools.find(
|
|
279
|
+
(tool: SkillToolEntry) => tool.name === "screen_point_at",
|
|
280
|
+
);
|
|
281
|
+
const tool = createSkillTool(
|
|
282
|
+
entry,
|
|
283
|
+
skillDir,
|
|
284
|
+
computeSkillVersionHash(skillDir),
|
|
285
|
+
BUNDLED,
|
|
286
|
+
);
|
|
287
|
+
const calls: unknown[] = [];
|
|
288
|
+
const context = makeContext({
|
|
289
|
+
proxyToolResolver: async (name, input) => {
|
|
290
|
+
calls.push({ name, input });
|
|
291
|
+
return { content: "Drew 1 mark", isError: false };
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const rejected = await tool.execute(invalid, context);
|
|
296
|
+
expect(rejected.isError).toBe(true);
|
|
297
|
+
expect(calls).toHaveLength(0);
|
|
298
|
+
expect(rejected.content).toContain("skill_load");
|
|
299
|
+
const schemaMatch = rejected.content.match(/```json\n([\s\S]*?)\n```/);
|
|
300
|
+
expect(schemaMatch).not.toBeNull();
|
|
301
|
+
expect(JSON.parse(schemaMatch![1])).toEqual(entry.input_schema);
|
|
302
|
+
|
|
303
|
+
const input = {
|
|
304
|
+
marks: [{ target: "Filters", caption: "Click Filters" }],
|
|
305
|
+
};
|
|
306
|
+
const corrected = await tool.execute(input, context);
|
|
307
|
+
expect(corrected.isError).toBe(false);
|
|
308
|
+
expect(calls).toEqual([{ name: "computer_use_point_at", input }]);
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
|
|
264
312
|
test("allows input with only known parameters", async () => {
|
|
265
313
|
const hash = computeSkillVersionHash(tempDir);
|
|
266
314
|
const tool = createSkillTool(
|
|
@@ -471,7 +519,7 @@ describe("createSkillTool — required/type/enum validation", () => {
|
|
|
471
519
|
|
|
472
520
|
expect(result.isError).toBe(true);
|
|
473
521
|
expect(result.content).toContain(
|
|
474
|
-
"auto_open must be a boolean
|
|
522
|
+
"auto_open must be a boolean: pass true or false as a JSON boolean, not a string",
|
|
475
523
|
);
|
|
476
524
|
});
|
|
477
525
|
|
|
@@ -6,6 +6,79 @@ import {
|
|
|
6
6
|
validateInputAgainstSchema,
|
|
7
7
|
} from "../skills/validate-input.js";
|
|
8
8
|
|
|
9
|
+
describe("nested input validation", () => {
|
|
10
|
+
const schema = {
|
|
11
|
+
properties: {
|
|
12
|
+
options: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: { label: { type: "string" } },
|
|
15
|
+
required: ["label"],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
test("reports nested required fields and types with their path", () => {
|
|
21
|
+
expect(
|
|
22
|
+
validateInputAgainstSchema("example", { options: {} }, schema),
|
|
23
|
+
).toEqual({
|
|
24
|
+
ok: false,
|
|
25
|
+
errors: ["options.label is required"],
|
|
26
|
+
});
|
|
27
|
+
expect(
|
|
28
|
+
validateInputAgainstSchema("example", { options: { label: 42 } }, schema),
|
|
29
|
+
).toEqual({
|
|
30
|
+
ok: false,
|
|
31
|
+
errors: ["options.label must be a string"],
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("preserves open nested objects and only rejects explicitly closed extra fields", () => {
|
|
36
|
+
const input = { options: { label: "Example", extra: true } };
|
|
37
|
+
expect(validateInputAgainstSchema("example", input, schema)).toEqual({
|
|
38
|
+
ok: true,
|
|
39
|
+
});
|
|
40
|
+
expect(
|
|
41
|
+
validateInputAgainstSchema("example", input, {
|
|
42
|
+
properties: {
|
|
43
|
+
options: {
|
|
44
|
+
...schema.properties.options,
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
).toEqual({
|
|
50
|
+
ok: false,
|
|
51
|
+
errors: ['Unknown parameter "options.extra". Supported: "label"'],
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("does not treat a richer oneOf as a presence-only union", () => {
|
|
56
|
+
expect(
|
|
57
|
+
validateInputAgainstSchema(
|
|
58
|
+
"example",
|
|
59
|
+
{ options: { label: "Example" } },
|
|
60
|
+
{
|
|
61
|
+
properties: {
|
|
62
|
+
options: {
|
|
63
|
+
...schema.properties.options,
|
|
64
|
+
oneOf: [
|
|
65
|
+
{
|
|
66
|
+
required: ["label"],
|
|
67
|
+
properties: { label: { const: "Example" } },
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
required: ["label"],
|
|
71
|
+
properties: { label: { const: "Other" } },
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
),
|
|
78
|
+
).toEqual({ ok: true });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
9
82
|
// ---------------------------------------------------------------------------
|
|
10
83
|
// required
|
|
11
84
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,197 @@
|
|
|
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 { WORKSPACE_MIGRATIONS } from "../workspace/migrations/registry.js";
|
|
14
|
+
import { assertNotLiveDb } from "./assert-not-live-db.js";
|
|
15
|
+
|
|
16
|
+
const migration = WORKSPACE_MIGRATIONS.find(
|
|
17
|
+
(entry) => entry.id === "159-rename-colliding-jev-profile-name",
|
|
18
|
+
)!;
|
|
19
|
+
|
|
20
|
+
let workspaceDir: string;
|
|
21
|
+
|
|
22
|
+
function writeConfig(data: Record<string, unknown>): void {
|
|
23
|
+
writeFileSync(
|
|
24
|
+
join(workspaceDir, "config.json"),
|
|
25
|
+
JSON.stringify(data, null, 2) + "\n",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readConfig(): Record<string, any> {
|
|
30
|
+
return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function seedProfilePinTables(rows: {
|
|
34
|
+
conversations: string[];
|
|
35
|
+
cronJobs: string[];
|
|
36
|
+
}): void {
|
|
37
|
+
mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
|
|
38
|
+
const db = new Database(join(workspaceDir, "data", "db", "assistant.db"));
|
|
39
|
+
db.run(
|
|
40
|
+
`CREATE TABLE conversations (id TEXT PRIMARY KEY, inference_profile TEXT)`,
|
|
41
|
+
);
|
|
42
|
+
db.run(
|
|
43
|
+
`CREATE TABLE cron_jobs (id TEXT PRIMARY KEY, inference_profile TEXT)`,
|
|
44
|
+
);
|
|
45
|
+
rows.conversations.forEach((profile, index) => {
|
|
46
|
+
db.query(
|
|
47
|
+
`INSERT INTO conversations (id, inference_profile) VALUES (?, ?)`,
|
|
48
|
+
).run(`conv-${index}`, profile);
|
|
49
|
+
});
|
|
50
|
+
rows.cronJobs.forEach((profile, index) => {
|
|
51
|
+
db.query(`INSERT INTO cron_jobs (id, inference_profile) VALUES (?, ?)`).run(
|
|
52
|
+
`job-${index}`,
|
|
53
|
+
profile,
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
db.close();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readPins(table: string): string[] {
|
|
60
|
+
const db = new Database(join(workspaceDir, "data", "db", "assistant.db"));
|
|
61
|
+
try {
|
|
62
|
+
return (
|
|
63
|
+
db
|
|
64
|
+
.query(`SELECT inference_profile AS p FROM ${table} ORDER BY id`)
|
|
65
|
+
.all() as Array<{ p: string }>
|
|
66
|
+
).map((row) => row.p);
|
|
67
|
+
} finally {
|
|
68
|
+
db.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
workspaceDir = join(
|
|
74
|
+
tmpdir(),
|
|
75
|
+
`vellum-migration-159-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
76
|
+
);
|
|
77
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
if (existsSync(workspaceDir)) {
|
|
82
|
+
assertNotLiveDb(workspaceDir);
|
|
83
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("159-rename-colliding-jev-profile-name migration", () => {
|
|
88
|
+
test("is registered under its id with checkpoint retry", () => {
|
|
89
|
+
expect(migration).toBeDefined();
|
|
90
|
+
expect(migration.retryFailedCheckpoint).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("renames a colliding user profile and rewrites every reference", async () => {
|
|
94
|
+
writeConfig({
|
|
95
|
+
llm: {
|
|
96
|
+
defaultProvider: { provider: "vellum" },
|
|
97
|
+
profiles: {
|
|
98
|
+
"jev-managed": {
|
|
99
|
+
source: "user",
|
|
100
|
+
provider: "anthropic",
|
|
101
|
+
model: "claude-opus-4-7",
|
|
102
|
+
label: "Mine",
|
|
103
|
+
},
|
|
104
|
+
scratch: { source: "user", fallbackProfile: "jev-managed" },
|
|
105
|
+
armA: { source: "user", speed: "fast" },
|
|
106
|
+
blend: {
|
|
107
|
+
source: "user",
|
|
108
|
+
mix: [
|
|
109
|
+
{ profile: "jev-managed", weight: 1 },
|
|
110
|
+
{ profile: "armA", weight: 1 },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
profileOrder: ["balanced", "jev-managed", "blend"],
|
|
115
|
+
activeProfile: "jev-managed",
|
|
116
|
+
advisorProfile: "jev-managed",
|
|
117
|
+
callSites: { recall: { profile: "jev-managed", maxTokens: 4096 } },
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
seedProfilePinTables({
|
|
121
|
+
conversations: ["jev-managed", "balanced"],
|
|
122
|
+
cronJobs: ["jev-managed"],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await migration.run(workspaceDir);
|
|
126
|
+
|
|
127
|
+
const llm = readConfig().llm;
|
|
128
|
+
expect(llm.profiles["jev-managed"]).toBeUndefined();
|
|
129
|
+
expect(llm.profiles["jev-managed-custom"]).toEqual({
|
|
130
|
+
source: "user",
|
|
131
|
+
provider: "anthropic",
|
|
132
|
+
model: "claude-opus-4-7",
|
|
133
|
+
label: "Mine",
|
|
134
|
+
});
|
|
135
|
+
expect(Object.keys(llm.profiles)).toEqual([
|
|
136
|
+
"jev-managed-custom",
|
|
137
|
+
"scratch",
|
|
138
|
+
"armA",
|
|
139
|
+
"blend",
|
|
140
|
+
]);
|
|
141
|
+
expect(llm.activeProfile).toBe("jev-managed-custom");
|
|
142
|
+
expect(llm.advisorProfile).toBe("jev-managed-custom");
|
|
143
|
+
expect(llm.profileOrder).toEqual([
|
|
144
|
+
"balanced",
|
|
145
|
+
"jev-managed-custom",
|
|
146
|
+
"blend",
|
|
147
|
+
]);
|
|
148
|
+
expect(llm.callSites.recall.profile).toBe("jev-managed-custom");
|
|
149
|
+
expect(llm.profiles.scratch.fallbackProfile).toBe("jev-managed-custom");
|
|
150
|
+
expect(llm.profiles.blend.mix[0].profile).toBe("jev-managed-custom");
|
|
151
|
+
expect(readPins("conversations")).toEqual([
|
|
152
|
+
"jev-managed-custom",
|
|
153
|
+
"balanced",
|
|
154
|
+
]);
|
|
155
|
+
expect(readPins("cron_jobs")).toEqual(["jev-managed-custom"]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("keeps suffixing past taken and reserved names", async () => {
|
|
159
|
+
writeConfig({
|
|
160
|
+
llm: {
|
|
161
|
+
profiles: {
|
|
162
|
+
"jev-managed": {
|
|
163
|
+
source: "user",
|
|
164
|
+
provider: "openai",
|
|
165
|
+
model: "gpt-5.5",
|
|
166
|
+
},
|
|
167
|
+
"jev-managed-custom": { source: "user", provider: "openai" },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await migration.run(workspaceDir);
|
|
173
|
+
|
|
174
|
+
const keys = Object.keys(readConfig().llm.profiles);
|
|
175
|
+
expect(keys).toEqual(["jev-managed-custom-2", "jev-managed-custom"]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("leaves a managed stub alone and is idempotent", async () => {
|
|
179
|
+
const original = {
|
|
180
|
+
llm: {
|
|
181
|
+
profiles: {
|
|
182
|
+
"jev-managed": { source: "managed", status: "disabled" },
|
|
183
|
+
balanced: { source: "managed" },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
writeConfig(original);
|
|
188
|
+
|
|
189
|
+
await migration.run(workspaceDir);
|
|
190
|
+
await migration.run(workspaceDir);
|
|
191
|
+
|
|
192
|
+
expect(readConfig()).toEqual(original);
|
|
193
|
+
expect(existsSync(join(workspaceDir, "data", "db", "assistant.db"))).toBe(
|
|
194
|
+
false,
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wire contract for the dictation REST endpoint (`POST /dictation`).
|
|
3
3
|
*
|
|
4
|
-
* The body web/macOS sends
|
|
5
|
-
*
|
|
4
|
+
* The body web/macOS sends to apply explicit dictation replacements, classify
|
|
5
|
+
* voice input, or edit selected text. `context` is its own
|
|
6
6
|
* named schema/type so consumers (web client) can import `DictationContext`
|
|
7
7
|
* directly instead of inferring it from the generated client.
|
|
8
8
|
*
|