@vellumai/assistant 0.12.3-dev.202609221714.83d90c3 → 0.12.3-dev.202609222016.8d633dd
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 +8 -0
- package/package.json +1 -1
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/api/events/tool-result.ts +10 -0
- package/src/calls/voice-escalation-judge.ts +1 -1
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +60 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/plugin-catalog-cache.ts +17 -2
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/config/__tests__/default-profile-catalog.test.ts +28 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/call-site-defaults.ts +6 -0
- package/src/config/default-profile-catalog.ts +30 -1
- package/src/config/default-profile-names.ts +36 -2
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/llm.ts +21 -5
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
- package/src/daemon/auto-profile-router.ts +220 -0
- package/src/daemon/conversation-agent-loop.ts +77 -4
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/home/feed-types.ts +2 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
- package/src/notifications/conversation-pairing.ts +23 -8
- package/src/notifications/home-feed-mirror.ts +39 -0
- package/src/notifications/home-feed-side-effect.ts +46 -19
- package/src/persistence/jobs-store.ts +229 -0
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +561 -0
- package/src/plugins/defaults/memory/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/{calls/__tests__/typesafe-noul.test.ts → providers/jev/ask.test.ts} +4 -4
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +67 -34
- package/src/providers/model-catalog.ts +234 -0
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
- package/src/tools/network/web-fetch.ts +30 -9
package/openapi.yaml
CHANGED
|
@@ -21412,6 +21412,10 @@ paths:
|
|
|
21412
21412
|
type: string
|
|
21413
21413
|
mayRequireJavaScript:
|
|
21414
21414
|
type: boolean
|
|
21415
|
+
startIndexPastEnd:
|
|
21416
|
+
type: boolean
|
|
21417
|
+
providerWarning:
|
|
21418
|
+
type: string
|
|
21415
21419
|
required:
|
|
21416
21420
|
- url
|
|
21417
21421
|
- finalUrl
|
|
@@ -22026,6 +22030,10 @@ paths:
|
|
|
22026
22030
|
type: string
|
|
22027
22031
|
mayRequireJavaScript:
|
|
22028
22032
|
type: boolean
|
|
22033
|
+
startIndexPastEnd:
|
|
22034
|
+
type: boolean
|
|
22035
|
+
providerWarning:
|
|
22036
|
+
type: string
|
|
22029
22037
|
required:
|
|
22030
22038
|
- url
|
|
22031
22039
|
- finalUrl
|
package/package.json
CHANGED
|
@@ -996,6 +996,12 @@ describe("pairDeliveryWithConversation", () => {
|
|
|
996
996
|
expect(createConversationMock).not.toHaveBeenCalled();
|
|
997
997
|
expect(addMessageMock).toHaveBeenCalledTimes(1);
|
|
998
998
|
expect(messagesInvalidated).toContain("conv-foreground");
|
|
999
|
+
// The transcript is this notification's only home, so a chat marked Done
|
|
1000
|
+
// comes back for it.
|
|
1001
|
+
const options = addMessageMock.mock.calls[0]![3] as {
|
|
1002
|
+
skipResurface?: boolean;
|
|
1003
|
+
};
|
|
1004
|
+
expect(options.skipResurface).toBe(false);
|
|
999
1005
|
});
|
|
1000
1006
|
|
|
1001
1007
|
test("appended notification body skips indexing", async () => {
|
|
@@ -1017,8 +1023,12 @@ describe("pairDeliveryWithConversation", () => {
|
|
|
1017
1023
|
|
|
1018
1024
|
const options = addMessageMock.mock.calls[0]![3] as {
|
|
1019
1025
|
skipIndexing?: boolean;
|
|
1026
|
+
skipResurface?: boolean;
|
|
1020
1027
|
};
|
|
1021
1028
|
expect(options.skipIndexing).toBe(true);
|
|
1029
|
+
// The bell carries this one (async-background), so the append is
|
|
1030
|
+
// bookkeeping and never resurfaces a chat the user marked Done.
|
|
1031
|
+
expect(options.skipResurface).toBe(true);
|
|
1022
1032
|
});
|
|
1023
1033
|
|
|
1024
1034
|
test("passive vellum signal appends regardless of the producing conversation's source", async () => {
|
|
@@ -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
|
});
|
|
@@ -196,6 +196,9 @@ const BASELINE: Record<string, readonly string[]> = {
|
|
|
196
196
|
"../../../daemon/trust-context.js",
|
|
197
197
|
"../../../home/job-handlers/conversation-starters.js",
|
|
198
198
|
"../../../media/job-handlers/media-processing.js",
|
|
199
|
+
// The skill-update receipt job announces a burst through the
|
|
200
|
+
// notification pipeline's single entry point. No plugin-api equivalent.
|
|
201
|
+
"../../../notifications/emit-signal.js",
|
|
199
202
|
"../../../permissions/types.js",
|
|
200
203
|
"../../../persistence/auto-analysis-constants.js",
|
|
201
204
|
"../../../persistence/checkpoints.js",
|
|
@@ -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,
|
|
@@ -112,6 +112,16 @@ export const WebFetchMetadataSchema = z.object({
|
|
|
112
112
|
durationMs: z.number(),
|
|
113
113
|
errorMessage: z.string().optional(),
|
|
114
114
|
mayRequireJavaScript: z.boolean().optional(),
|
|
115
|
+
/**
|
|
116
|
+
* The requested `start_index` was past the end of the page, so nothing was
|
|
117
|
+
* read. Always set, true or false, by an assistant whose metadata carries
|
|
118
|
+
* every warning a reader acts on (this and `providerWarning` included); a
|
|
119
|
+
* record without it came from one that predates them, whose remaining
|
|
120
|
+
* warnings are only in the result text.
|
|
121
|
+
*/
|
|
122
|
+
startIndexPastEnd: z.boolean().optional(),
|
|
123
|
+
/** A hosted provider's own warning about the fetch, verbatim. */
|
|
124
|
+
providerWarning: z.string().optional(),
|
|
115
125
|
});
|
|
116
126
|
|
|
117
127
|
export type WebFetchMetadata = z.infer<typeof WebFetchMetadataSchema>;
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
* decision stands exactly as it would without the judge.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { askTypesafeNoul } from "../providers/jev/ask.js";
|
|
19
20
|
import type { Message, Provider } from "../providers/types.js";
|
|
20
21
|
import { safeStringSlice } from "../util/unicode.js";
|
|
21
|
-
import { askTypesafeNoul } from "./typesafe-noul.js";
|
|
22
22
|
|
|
23
23
|
export const VOICE_ESCALATION_JUDGE_CALL_SITE = "voiceEscalationJudge";
|
|
24
24
|
|
|
@@ -228,4 +228,64 @@ describe("mergePlatformCatalogWithBundledLocals", () => {
|
|
|
228
228
|
expect(merged.matches[1]?.source.kind).toBe("local");
|
|
229
229
|
expect(merged.ref).toBe("main");
|
|
230
230
|
});
|
|
231
|
+
test("keeps the bundled integration when a platform row without one shadows it", () => {
|
|
232
|
+
const integration = {
|
|
233
|
+
kind: "mcp",
|
|
234
|
+
displayName: "Fathom",
|
|
235
|
+
documentationUrl: "https://example.com/docs",
|
|
236
|
+
verifiedAt: "2026-09-10",
|
|
237
|
+
verification: "documentation-only",
|
|
238
|
+
setup: { mode: "oauth", instructions: "Sign in." },
|
|
239
|
+
logo: "fathom.png",
|
|
240
|
+
} as const;
|
|
241
|
+
const platformIntegration = { ...integration, displayName: "Platform" };
|
|
242
|
+
const platform: PluginCatalog = {
|
|
243
|
+
ref: "main",
|
|
244
|
+
matches: [
|
|
245
|
+
{
|
|
246
|
+
name: "fathom",
|
|
247
|
+
path: "github:provider/fathom@pin",
|
|
248
|
+
category: null,
|
|
249
|
+
source: {
|
|
250
|
+
kind: "github",
|
|
251
|
+
repo: "provider/fathom",
|
|
252
|
+
ref: "0".repeat(40),
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "linear",
|
|
257
|
+
path: "github:provider/linear@pin",
|
|
258
|
+
category: null,
|
|
259
|
+
integration: platformIntegration,
|
|
260
|
+
source: {
|
|
261
|
+
kind: "github",
|
|
262
|
+
repo: "provider/linear",
|
|
263
|
+
ref: "0".repeat(40),
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
};
|
|
268
|
+
const bundled: PluginCatalog = {
|
|
269
|
+
ref: "bundled",
|
|
270
|
+
matches: ["fathom", "linear"].map((name) => ({
|
|
271
|
+
name,
|
|
272
|
+
path: `local:plugins/mcp-catalog/${name}@1.0.0`,
|
|
273
|
+
category: null,
|
|
274
|
+
integration,
|
|
275
|
+
source: {
|
|
276
|
+
kind: "local",
|
|
277
|
+
path: `plugins/mcp-catalog/${name}`,
|
|
278
|
+
version: "1.0.0",
|
|
279
|
+
},
|
|
280
|
+
})),
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const merged = mergePlatformCatalogWithBundledLocals(platform, bundled);
|
|
284
|
+
|
|
285
|
+
const [fathom, linear] = merged.matches;
|
|
286
|
+
expect(fathom?.source.kind).toBe("github");
|
|
287
|
+
expect(fathom?.integration).toEqual(integration);
|
|
288
|
+
expect(linear?.source.kind).toBe("github");
|
|
289
|
+
expect(linear?.integration).toEqual(platformIntegration);
|
|
290
|
+
});
|
|
231
291
|
});
|
|
@@ -161,14 +161,78 @@ describe("fetchPluginCatalogFromPlatform", () => {
|
|
|
161
161
|
await expect(promise).rejects.toMatchObject({ status: 502 });
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
test(
|
|
164
|
+
test.each([
|
|
165
|
+
["a missing plugins array", {}],
|
|
166
|
+
["a non-array plugins field", { plugins: "nope" }],
|
|
167
|
+
["a non-object body", []],
|
|
168
|
+
])("throws on an invalid envelope (%s)", async (_label, body) => {
|
|
165
169
|
const promise = fetchPluginCatalogFromPlatform({
|
|
166
|
-
fetch: platformFetch(
|
|
170
|
+
fetch: platformFetch(body),
|
|
167
171
|
});
|
|
168
172
|
await expect(promise).rejects.toBeInstanceOf(PluginCatalogUnavailableError);
|
|
169
173
|
await expect(promise).rejects.toMatchObject({ status: 502 });
|
|
170
174
|
});
|
|
171
175
|
|
|
176
|
+
test("skips rows that fail the base row schema and keeps the rest", async () => {
|
|
177
|
+
const catalog = await fetchPluginCatalogFromPlatform({
|
|
178
|
+
fetch: platformFetch({
|
|
179
|
+
plugins: [
|
|
180
|
+
{ repo: "acme/nameless", ref: SHA_A },
|
|
181
|
+
{ name: 42, repo: "acme/numeric", ref: SHA_A },
|
|
182
|
+
"not-a-row",
|
|
183
|
+
{ name: "good", repo: "acme/good", ref: SHA_B },
|
|
184
|
+
],
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
expect(catalog.matches.map((m) => m.name)).toEqual(["good"]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("keeps a row whose integration is invalid, without the integration", async () => {
|
|
191
|
+
const catalog = await fetchPluginCatalogFromPlatform({
|
|
192
|
+
fetch: platformFetch({
|
|
193
|
+
plugins: [
|
|
194
|
+
{
|
|
195
|
+
name: "snake-case",
|
|
196
|
+
repo: "acme/snake-case",
|
|
197
|
+
ref: SHA_A,
|
|
198
|
+
description: "kept",
|
|
199
|
+
integration: {
|
|
200
|
+
kind: "mcp",
|
|
201
|
+
display_name: "Snake Case",
|
|
202
|
+
documentation_url: "https://example.com/docs",
|
|
203
|
+
verified_at: "2026-09-10",
|
|
204
|
+
verification: "documentation-only",
|
|
205
|
+
setup: { mode: "oauth", instructions: "Sign in." },
|
|
206
|
+
logo: "snake-case.png",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "valid",
|
|
211
|
+
repo: "acme/valid",
|
|
212
|
+
ref: SHA_B,
|
|
213
|
+
integration: {
|
|
214
|
+
kind: "mcp",
|
|
215
|
+
displayName: "Valid",
|
|
216
|
+
documentationUrl: "https://example.com/docs",
|
|
217
|
+
verifiedAt: "2026-09-10",
|
|
218
|
+
verification: "documentation-only",
|
|
219
|
+
setup: { mode: "manual", instructions: "Paste a key." },
|
|
220
|
+
logo: "valid.png",
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(catalog.matches.map((m) => m.name)).toEqual(["snake-case", "valid"]);
|
|
228
|
+
expect(catalog.matches[0]).toMatchObject({
|
|
229
|
+
name: "snake-case",
|
|
230
|
+
description: "kept",
|
|
231
|
+
});
|
|
232
|
+
expect(catalog.matches[0]?.integration).toBeUndefined();
|
|
233
|
+
expect(catalog.matches[1]?.integration?.displayName).toBe("Valid");
|
|
234
|
+
});
|
|
235
|
+
|
|
172
236
|
test("throws when fetch rejects (network error / abort)", async () => {
|
|
173
237
|
const promise = fetchPluginCatalogFromPlatform({
|
|
174
238
|
fetch: (async () => {
|
|
@@ -26,16 +26,31 @@ interface CacheEntry {
|
|
|
26
26
|
|
|
27
27
|
const cache = new Map<string, CacheEntry>();
|
|
28
28
|
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Add bundled local packages without overriding platform-authoritative rows.
|
|
31
|
+
* A platform row without an integration borrows the bundled row's
|
|
32
|
+
* integration of the same name, so an integration the platform omits or
|
|
33
|
+
* serves malformed never disappears from the catalog.
|
|
34
|
+
*/
|
|
30
35
|
export function mergePlatformCatalogWithBundledLocals(
|
|
31
36
|
platform: PluginCatalog,
|
|
32
37
|
bundledLocal: PluginCatalog = readBundledLocalPluginCatalog(),
|
|
33
38
|
): PluginCatalog {
|
|
39
|
+
const bundledByName = new Map(
|
|
40
|
+
bundledLocal.matches.map((match) => [match.name, match]),
|
|
41
|
+
);
|
|
42
|
+
const platformMatches = platform.matches.map((match) => {
|
|
43
|
+
const bundledIntegration = bundledByName.get(match.name)?.integration;
|
|
44
|
+
if (match.integration || !bundledIntegration) {
|
|
45
|
+
return match;
|
|
46
|
+
}
|
|
47
|
+
return { ...match, integration: bundledIntegration };
|
|
48
|
+
});
|
|
34
49
|
const seen = new Set(platform.matches.map((match) => match.name));
|
|
35
50
|
return {
|
|
36
51
|
ref: platform.ref,
|
|
37
52
|
matches: [
|
|
38
|
-
...
|
|
53
|
+
...platformMatches,
|
|
39
54
|
...bundledLocal.matches.filter((match) => !seen.has(match.name)),
|
|
40
55
|
].sort((a, b) => a.name.localeCompare(b.name)),
|
|
41
56
|
};
|
|
@@ -7,15 +7,17 @@
|
|
|
7
7
|
* catalog sourced from the platform is indistinguishable from one read off
|
|
8
8
|
* GitHub.
|
|
9
9
|
*
|
|
10
|
-
* Every failure mode (non-2xx, unreachable/aborted, malformed
|
|
11
|
-
* {@link PluginCatalogUnavailableError}
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Every failure mode (non-2xx, unreachable/aborted, malformed envelope)
|
|
11
|
+
* throws {@link PluginCatalogUnavailableError}, so a caller never mistakes an
|
|
12
|
+
* outage for "no plugins". Rows are validated one at a time: a row with an
|
|
13
|
+
* invalid `integration` is kept without it, and a row that fails the base
|
|
14
|
+
* row schema is skipped, so one bad row never blanks the whole catalog.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import { z } from "zod";
|
|
17
18
|
|
|
18
19
|
import { getPlatformBaseUrl } from "../../config/env.js";
|
|
20
|
+
import { getLogger } from "../../util/logger.js";
|
|
19
21
|
import {
|
|
20
22
|
type MarketplaceEntry,
|
|
21
23
|
mcpIntegrationSchema,
|
|
@@ -27,9 +29,12 @@ import {
|
|
|
27
29
|
type SearchPluginsDeps,
|
|
28
30
|
} from "./search-plugins.js";
|
|
29
31
|
|
|
32
|
+
const log = getLogger("plugin-catalog");
|
|
33
|
+
|
|
30
34
|
/**
|
|
31
35
|
* One flattened row from the platform catalog. Unknown keys (`id`,
|
|
32
|
-
* `display_name`) are accepted and dropped
|
|
36
|
+
* `display_name`) are accepted and dropped (zod strips them by default).
|
|
37
|
+
* `integration` is validated separately by {@link integrationSchema}.
|
|
33
38
|
*/
|
|
34
39
|
const platformPluginRowSchema = z.object({
|
|
35
40
|
name: z.string(),
|
|
@@ -41,11 +46,13 @@ const platformPluginRowSchema = z.object({
|
|
|
41
46
|
category: z.string().nullable().optional(),
|
|
42
47
|
homepage: z.string().nullable().optional(),
|
|
43
48
|
license: z.string().nullable().optional(),
|
|
44
|
-
integration:
|
|
49
|
+
integration: z.unknown(),
|
|
45
50
|
});
|
|
46
51
|
|
|
52
|
+
const integrationSchema = mcpIntegrationSchema.nullable().optional();
|
|
53
|
+
|
|
47
54
|
const platformCatalogSchema = z.object({
|
|
48
|
-
plugins: z.array(
|
|
55
|
+
plugins: z.array(z.unknown()),
|
|
49
56
|
});
|
|
50
57
|
|
|
51
58
|
/**
|
|
@@ -99,10 +106,22 @@ export async function fetchPluginCatalogFromPlatform(
|
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
const entries: MarketplaceEntry[] = [];
|
|
102
|
-
|
|
109
|
+
const skippedRows: string[] = [];
|
|
110
|
+
const droppedIntegrations: string[] = [];
|
|
111
|
+
for (const [index, rawRow] of parsed.data.plugins.entries()) {
|
|
112
|
+
const rowResult = platformPluginRowSchema.safeParse(rawRow);
|
|
113
|
+
if (!rowResult.success) {
|
|
114
|
+
skippedRows.push(describeInvalidRow(rawRow, index));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const row = rowResult.data;
|
|
103
118
|
if (!row.repo || !row.ref) {
|
|
104
119
|
continue;
|
|
105
120
|
}
|
|
121
|
+
const integrationResult = integrationSchema.safeParse(row.integration);
|
|
122
|
+
if (!integrationResult.success) {
|
|
123
|
+
droppedIntegrations.push(row.name);
|
|
124
|
+
}
|
|
106
125
|
entries.push({
|
|
107
126
|
name: row.name,
|
|
108
127
|
source: {
|
|
@@ -116,12 +135,32 @@ export async function fetchPluginCatalogFromPlatform(
|
|
|
116
135
|
category: row.category ?? undefined,
|
|
117
136
|
homepage: row.homepage ?? undefined,
|
|
118
137
|
license: row.license ?? undefined,
|
|
119
|
-
integration:
|
|
138
|
+
integration: integrationResult.success
|
|
139
|
+
? (integrationResult.data ?? undefined)
|
|
140
|
+
: undefined,
|
|
120
141
|
});
|
|
121
142
|
}
|
|
122
143
|
|
|
144
|
+
if (skippedRows.length > 0 || droppedIntegrations.length > 0) {
|
|
145
|
+
log.warn(
|
|
146
|
+
{ skippedRows, droppedIntegrations },
|
|
147
|
+
"platform plugin catalog contained invalid rows; skipped rows and dropped integrations",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
123
151
|
return {
|
|
124
152
|
ref: opts?.ref ?? "platform",
|
|
125
153
|
matches: projectMarketplaceEntries(entries),
|
|
126
154
|
};
|
|
127
155
|
}
|
|
156
|
+
|
|
157
|
+
/** Name of an invalid row for logging, or its position when it has none. */
|
|
158
|
+
function describeInvalidRow(row: unknown, index: number): string {
|
|
159
|
+
if (row && typeof row === "object" && "name" in row) {
|
|
160
|
+
const name = (row as { name: unknown }).name;
|
|
161
|
+
if (typeof name === "string" && name.length > 0) {
|
|
162
|
+
return name;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return `#${index}`;
|
|
166
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getEffectiveProfiles,
|
|
11
11
|
getEffectiveProfilesForProvider,
|
|
12
12
|
getUserSelectableProfilesForProvider,
|
|
13
|
+
MANAGED_PROFILE_TEMPLATES,
|
|
13
14
|
PROFILE_IMPLS,
|
|
14
15
|
resolveDefaultProfileForProvider,
|
|
15
16
|
} from "../default-profile-catalog.js";
|
|
@@ -79,6 +80,33 @@ describe("getEffectiveProfiles", () => {
|
|
|
79
80
|
).toBeUndefined();
|
|
80
81
|
});
|
|
81
82
|
|
|
83
|
+
test("the Auto profile is the managed Balanced body relabeled, managed-only, and flag-gated", () => {
|
|
84
|
+
const auto = CODE_DEFAULT_PROFILE_ENTRIES.auto;
|
|
85
|
+
const balanced = CODE_DEFAULT_PROFILE_ENTRIES.balanced;
|
|
86
|
+
expect(auto.label).toBe("Auto");
|
|
87
|
+
expect(auto.model).toBe(balanced.model);
|
|
88
|
+
expect(auto.provider).toBe(balanced.provider);
|
|
89
|
+
expect(auto.fallbackProfile).toBeUndefined();
|
|
90
|
+
// Not seeded unconditionally: the flag reconcile writes the stub.
|
|
91
|
+
expect(Object.keys(MANAGED_PROFILE_TEMPLATES)).not.toContain("auto");
|
|
92
|
+
expect(
|
|
93
|
+
resolveDefaultProfileForProvider(undefined, "auto", null),
|
|
94
|
+
).toBeUndefined();
|
|
95
|
+
const stub: Record<string, ProfileEntry> = { auto: { source: "managed" } };
|
|
96
|
+
expect(resolveDefaultProfileForProvider(stub, "auto", null)?.model).toBe(
|
|
97
|
+
balanced.model,
|
|
98
|
+
);
|
|
99
|
+
expect(
|
|
100
|
+
resolveDefaultProfileForProvider(stub, "auto", {
|
|
101
|
+
provider: "anthropic",
|
|
102
|
+
connectionName: "anthropic-personal",
|
|
103
|
+
}),
|
|
104
|
+
).toBeUndefined();
|
|
105
|
+
expect(
|
|
106
|
+
Object.keys(getConversationProfilesForProvider(stub, null)),
|
|
107
|
+
).toContain("auto");
|
|
108
|
+
});
|
|
109
|
+
|
|
82
110
|
test("the conversation view drops the managed Jev profile but keeps it selectable for call sites", () => {
|
|
83
111
|
expect(
|
|
84
112
|
Object.keys(getConversationProfilesForProvider(undefined, null)),
|
|
@@ -368,4 +368,75 @@ describe("reconcileFlagGatedProfiles", () => {
|
|
|
368
368
|
expect(reconcileFlagGatedProfiles()).toBe(false);
|
|
369
369
|
expect(readFileSync(CONFIG_PATH, "utf-8")).toBe(before);
|
|
370
370
|
});
|
|
371
|
+
|
|
372
|
+
test("auto-profile flag on materializes the managed auto profile at the head of the order", () => {
|
|
373
|
+
process.env.IS_PLATFORM = "true";
|
|
374
|
+
seedBalancedConfig();
|
|
375
|
+
setOverridesForTesting({ "auto-profile": true });
|
|
376
|
+
|
|
377
|
+
expect(reconcileFlagGatedProfiles()).toBe(true);
|
|
378
|
+
|
|
379
|
+
const raw = readConfig();
|
|
380
|
+
expect(raw.llm.profiles.auto).toEqual({ source: "managed" });
|
|
381
|
+
expect(raw.llm.profileOrder[0]).toBe("auto");
|
|
382
|
+
const effective = getEffectiveProfile(raw.llm.profiles, "auto")!;
|
|
383
|
+
expect(effective.label).toBe("Auto");
|
|
384
|
+
expect(effective.model).toBe(
|
|
385
|
+
getEffectiveProfile(raw.llm.profiles, "balanced")!.model,
|
|
386
|
+
);
|
|
387
|
+
expect(LLMSchema.safeParse(raw.llm).success).toBe(true);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("auto-profile flag on in BYOK mode writes a plain stub that the effective view hides", () => {
|
|
391
|
+
seedBalancedConfig();
|
|
392
|
+
setOverridesForTesting({ "auto-profile": true });
|
|
393
|
+
|
|
394
|
+
expect(reconcileFlagGatedProfiles()).toBe(true);
|
|
395
|
+
|
|
396
|
+
const raw = readConfig();
|
|
397
|
+
expect(raw.llm.profiles.auto).toEqual({ source: "managed" });
|
|
398
|
+
expect(getEffectiveProfile(raw.llm.profiles, "auto")).toBeDefined();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("auto-profile flag off removes the managed auto profile and repoints its references", () => {
|
|
402
|
+
process.env.IS_PLATFORM = "true";
|
|
403
|
+
seedBalancedConfig();
|
|
404
|
+
setOverridesForTesting({ "auto-profile": true });
|
|
405
|
+
reconcileFlagGatedProfiles();
|
|
406
|
+
|
|
407
|
+
const raw = readConfig();
|
|
408
|
+
raw.llm.activeProfile = "auto";
|
|
409
|
+
(raw.llm as Record<string, unknown>).callSites = {
|
|
410
|
+
subagentSpawn: { profile: "auto" },
|
|
411
|
+
};
|
|
412
|
+
writeConfig(raw);
|
|
413
|
+
invalidateConfigCache();
|
|
414
|
+
|
|
415
|
+
setOverridesForTesting({ "auto-profile": false });
|
|
416
|
+
expect(reconcileFlagGatedProfiles()).toBe(true);
|
|
417
|
+
|
|
418
|
+
const after = readConfig();
|
|
419
|
+
expect(after.llm.profiles.auto).toBeUndefined();
|
|
420
|
+
expect(after.llm.profileOrder.includes("auto")).toBe(false);
|
|
421
|
+
expect(after.llm.activeProfile).toBe("balanced");
|
|
422
|
+
expect((after.llm as Record<string, unknown>).callSites).toEqual({
|
|
423
|
+
subagentSpawn: {},
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("os-beta and auto-profile reconcile independently in one pass", () => {
|
|
428
|
+
process.env.IS_PLATFORM = "true";
|
|
429
|
+
seedBalancedConfig();
|
|
430
|
+
setOverridesForTesting({ "os-beta": true, "auto-profile": true });
|
|
431
|
+
expect(reconcileFlagGatedProfiles()).toBe(true);
|
|
432
|
+
let order = readConfig().llm.profileOrder;
|
|
433
|
+
expect(order[0]).toBe("auto");
|
|
434
|
+
expect(order.indexOf("os-beta")).toBe(order.indexOf("balanced") + 1);
|
|
435
|
+
|
|
436
|
+
setOverridesForTesting({ "os-beta": false, "auto-profile": true });
|
|
437
|
+
expect(reconcileFlagGatedProfiles()).toBe(true);
|
|
438
|
+
order = readConfig().llm.profileOrder;
|
|
439
|
+
expect(order[0]).toBe("auto");
|
|
440
|
+
expect(order.includes("os-beta")).toBe(false);
|
|
441
|
+
});
|
|
371
442
|
});
|
|
@@ -159,6 +159,12 @@ export const CALL_SITE_DEFAULTS: Record<LLMCallSite, CallSiteDefaultConfig> = {
|
|
|
159
159
|
voiceEscalationJudge: {
|
|
160
160
|
profile: JEV_MANAGED_PROFILE_KEY,
|
|
161
161
|
},
|
|
162
|
+
// Picks the default profile for a turn pinned to the Auto profile. On a
|
|
163
|
+
// BYOK install the managed-only Jev profile has no body, so the router is
|
|
164
|
+
// unavailable and the turn runs on Balanced.
|
|
165
|
+
autoProfileRouter: {
|
|
166
|
+
profile: JEV_MANAGED_PROFILE_KEY,
|
|
167
|
+
},
|
|
162
168
|
// The barge-in continuation judge remains off until a TypeSafe profile is
|
|
163
169
|
// pinned here (see live-voice/continuation-judge.ts).
|
|
164
170
|
voiceContinuationJudge: { profile: "cost-optimized" },
|
|
@@ -8,6 +8,7 @@ import { isCodexSubscriptionModel } from "../providers/openai/codex-models.js";
|
|
|
8
8
|
import type { ModelIntent } from "../providers/types.js";
|
|
9
9
|
import { getManagedUpstream } from "../providers/vellum-model-routing.js";
|
|
10
10
|
import {
|
|
11
|
+
AUTO_PROFILE_KEY,
|
|
11
12
|
BACKUP_PROFILE_KEYS,
|
|
12
13
|
type BackupProfileKey,
|
|
13
14
|
DEFAULT_PROFILE_KEYS,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
FALLBACK_PROFILE_BY_KEY,
|
|
18
19
|
isDefaultProfileKey,
|
|
19
20
|
isDefaultProfileProvider,
|
|
21
|
+
isFlagGatedProfileKey,
|
|
20
22
|
isManagedOnlyProfileKey,
|
|
21
23
|
JEV_MANAGED_PROFILE_KEY,
|
|
22
24
|
OS_BETA_PROFILE_KEY,
|
|
@@ -407,6 +409,27 @@ export const JEV_MANAGED_PROFILE_TEMPLATE: DefaultProfileTemplate = {
|
|
|
407
409
|
contextWindow: { maxInputTokens: 32_000 },
|
|
408
410
|
};
|
|
409
411
|
|
|
412
|
+
/**
|
|
413
|
+
* The Auto profile: the managed Balanced body under its own label, so that
|
|
414
|
+
* anything dispatching the name directly runs Balanced. The per-message
|
|
415
|
+
* choice among the defaults happens in the agent loop, not here (see
|
|
416
|
+
* `AUTO_PROFILE_KEY`). No `fallbackProfile`: the code-owned fallback mapping
|
|
417
|
+
* is keyed by default profile, and a direct dispatch of this name is already
|
|
418
|
+
* the fallback path. Flag-gated like `os-beta`: NOT in
|
|
419
|
+
* `MANAGED_PROFILE_TEMPLATES`, so the unconditional boot seed never creates
|
|
420
|
+
* it; the flag-gated profile reconcile materializes it while the
|
|
421
|
+
* `auto-profile` feature flag is on.
|
|
422
|
+
*/
|
|
423
|
+
export const AUTO_PROFILE_TEMPLATE: DefaultProfileTemplate = (() => {
|
|
424
|
+
const { fallbackProfile: _fallbackProfile, ...balanced } =
|
|
425
|
+
VELLUM_PROFILE_IMPLS.balanced;
|
|
426
|
+
return {
|
|
427
|
+
...balanced,
|
|
428
|
+
label: "Auto",
|
|
429
|
+
description: "Picks the profile that fits each message, in beta",
|
|
430
|
+
};
|
|
431
|
+
})();
|
|
432
|
+
|
|
410
433
|
/**
|
|
411
434
|
* Managed profiles, i.e. the `vellum` column keyed by profile name, plus the
|
|
412
435
|
* managed backup profiles and the managed Jev profile. Backups come after the
|
|
@@ -521,6 +544,7 @@ export const INVARIANT_PROFILE_NAMES = new Set<string>([
|
|
|
521
544
|
...DEFAULT_PROFILE_KEYS,
|
|
522
545
|
...BACKUP_PROFILE_KEYS,
|
|
523
546
|
JEV_MANAGED_PROFILE_KEY,
|
|
547
|
+
AUTO_PROFILE_KEY,
|
|
524
548
|
OS_BETA_PROFILE_KEY,
|
|
525
549
|
]);
|
|
526
550
|
|
|
@@ -535,6 +559,7 @@ export const MANAGED_PROFILE_NAMES = new Set<string>([
|
|
|
535
559
|
...DEFAULT_PROFILE_KEYS,
|
|
536
560
|
...BACKUP_PROFILE_KEYS,
|
|
537
561
|
JEV_MANAGED_PROFILE_KEY,
|
|
562
|
+
AUTO_PROFILE_KEY,
|
|
538
563
|
OS_BETA_PROFILE_KEY,
|
|
539
564
|
]);
|
|
540
565
|
|
|
@@ -666,6 +691,10 @@ function buildDefaultProfileEntries(): Record<string, ProfileEntry> {
|
|
|
666
691
|
JEV_MANAGED_PROFILE_TEMPLATE,
|
|
667
692
|
JEV_MANAGED_PROFILE_TEMPLATE.provider,
|
|
668
693
|
);
|
|
694
|
+
entries[AUTO_PROFILE_KEY] = materializeProfile(
|
|
695
|
+
AUTO_PROFILE_TEMPLATE,
|
|
696
|
+
AUTO_PROFILE_TEMPLATE.provider,
|
|
697
|
+
);
|
|
669
698
|
entries[OS_BETA_PROFILE_KEY] = materializeProfile(
|
|
670
699
|
OS_BETA_PROFILE_TEMPLATE,
|
|
671
700
|
OS_BETA_PROFILE_TEMPLATE.provider,
|
|
@@ -757,7 +786,7 @@ function resolveAgainstBody(
|
|
|
757
786
|
return { ...body };
|
|
758
787
|
}
|
|
759
788
|
if (workspace == null) {
|
|
760
|
-
return name
|
|
789
|
+
return isFlagGatedProfileKey(name) ? undefined : { ...body };
|
|
761
790
|
}
|
|
762
791
|
if (workspace.source !== "managed") {
|
|
763
792
|
return workspace;
|