@vellumai/assistant 0.11.9-staging.1 → 0.11.9-staging.2
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__/gemini-provider.test.ts +46 -0
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/cli/__tests__/catalog-search-help.test.ts +6 -0
- package/src/cli/commands/channels/index.help.ts +4 -5
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
- package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
- package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
- package/src/providers/gemini/client.ts +3 -2
- package/src/providers/model-catalog.ts +16 -0
package/package.json
CHANGED
|
@@ -467,6 +467,52 @@ describe("GeminiProvider", () => {
|
|
|
467
467
|
});
|
|
468
468
|
});
|
|
469
469
|
|
|
470
|
+
test("3.8 Flash: disabled maps to the LOW floor, not MINIMAL", async () => {
|
|
471
|
+
fakeChunks = [textChunk("OK"), finishChunk("STOP", 10, 2)];
|
|
472
|
+
|
|
473
|
+
const flash38Provider = new GeminiProvider(
|
|
474
|
+
"test-api-key",
|
|
475
|
+
"gemini-3.8-flash",
|
|
476
|
+
);
|
|
477
|
+
await flash38Provider.sendMessage(
|
|
478
|
+
[{ role: "user", content: [{ type: "text", text: "Hi" }] }],
|
|
479
|
+
{ config: { thinking: { type: "disabled" } } },
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const config = lastStreamParams!.config as Record<string, unknown>;
|
|
483
|
+
expect(config.thinkingConfig).toEqual({
|
|
484
|
+
thinkingLevel: "LOW",
|
|
485
|
+
includeThoughts: false,
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("3.8 Flash: an explicit minimal level is clamped up to LOW", async () => {
|
|
490
|
+
fakeChunks = [textChunk("OK"), finishChunk("STOP", 10, 2)];
|
|
491
|
+
|
|
492
|
+
const flash38Provider = new GeminiProvider(
|
|
493
|
+
"test-api-key",
|
|
494
|
+
"models/gemini-3.8-flash",
|
|
495
|
+
);
|
|
496
|
+
await flash38Provider.sendMessage(
|
|
497
|
+
[{ role: "user", content: [{ type: "text", text: "Hi" }] }],
|
|
498
|
+
{
|
|
499
|
+
config: {
|
|
500
|
+
thinking: {
|
|
501
|
+
type: "adaptive",
|
|
502
|
+
level: "minimal",
|
|
503
|
+
streamThinking: false,
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const config = lastStreamParams!.config as Record<string, unknown>;
|
|
510
|
+
expect(config.thinkingConfig).toEqual({
|
|
511
|
+
thinkingLevel: "LOW",
|
|
512
|
+
includeThoughts: false,
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
470
516
|
test("Pro: a supported explicit level passes through unchanged", async () => {
|
|
471
517
|
fakeChunks = [textChunk("OK"), finishChunk("STOP", 10, 2)];
|
|
472
518
|
|
|
@@ -224,6 +224,17 @@ describe("resolvePricing", () => {
|
|
|
224
224
|
);
|
|
225
225
|
});
|
|
226
226
|
|
|
227
|
+
test("returns priced for gemini-3.8-flash", () => {
|
|
228
|
+
const result = resolvePricing(
|
|
229
|
+
"gemini",
|
|
230
|
+
"gemini-3.8-flash",
|
|
231
|
+
1_000_000,
|
|
232
|
+
1_000_000,
|
|
233
|
+
);
|
|
234
|
+
expect(result.pricingStatus).toBe("priced");
|
|
235
|
+
expect(result.estimatedCostUsd).toBe(1.5 + 7.5);
|
|
236
|
+
});
|
|
237
|
+
|
|
227
238
|
test("returns priced for gemini-3-flash-preview", () => {
|
|
228
239
|
const result = resolvePricing(
|
|
229
240
|
"gemini",
|
|
@@ -58,5 +58,11 @@ describe("catalog search help for setup-intent retrieval", () => {
|
|
|
58
58
|
expect(list?.helpText).toBeDefined();
|
|
59
59
|
expect(indexed).toContain("assistant plugins search <name>");
|
|
60
60
|
expect(indexed).toContain("not listed");
|
|
61
|
+
expect(channelsHelp.description).toBe(
|
|
62
|
+
"Inspect and repair messaging channels",
|
|
63
|
+
);
|
|
64
|
+
expect(channelsHelp.description.toLowerCase()).not.toContain("slack");
|
|
65
|
+
expect(channelsHelp.description.toLowerCase()).not.toContain("telegram");
|
|
66
|
+
expect(channelsHelp.description.toLowerCase()).not.toContain("email");
|
|
61
67
|
});
|
|
62
68
|
});
|
|
@@ -19,12 +19,11 @@ export const CHANNELS_PLUGIN_SEARCH_HINT =
|
|
|
19
19
|
|
|
20
20
|
export const channelsHelp: CliCommandHelp = {
|
|
21
21
|
name: "channels",
|
|
22
|
-
description:
|
|
23
|
-
"Inspect and repair messaging channels (slack, telegram, email, etc.)",
|
|
22
|
+
description: "Inspect and repair messaging channels",
|
|
24
23
|
helpText: `
|
|
25
|
-
Channels are the messaging surfaces the assistant talks over
|
|
26
|
-
telegram, whatsapp, email, phone, vellum,
|
|
27
|
-
|
|
24
|
+
Channels are the messaging surfaces the assistant talks over. Built-in
|
|
25
|
+
readiness probes cover slack, telegram, whatsapp, email, phone, vellum,
|
|
26
|
+
platform, and a2a.
|
|
28
27
|
|
|
29
28
|
${CHANNELS_PLUGIN_SEARCH_HINT} Plugins can bundle additional channels
|
|
30
29
|
from other Vellum users.
|
|
@@ -65,6 +65,7 @@ describe("projectProviderForWire", () => {
|
|
|
65
65
|
const wire = projectProviderForWire(gemini!);
|
|
66
66
|
const modelIds = wire.models.map((model) => model.id);
|
|
67
67
|
const expectedGemini3ModelIds = [
|
|
68
|
+
"gemini-3.8-flash",
|
|
68
69
|
"gemini-3.7-flash",
|
|
69
70
|
"gemini-3.6-flash",
|
|
70
71
|
"gemini-3.5-flash",
|
|
@@ -119,3 +119,20 @@ describe("augmentMcpSetupDescription", () => {
|
|
|
119
119
|
expect(out.id).toBe("mcp-setup");
|
|
120
120
|
});
|
|
121
121
|
});
|
|
122
|
+
|
|
123
|
+
describe("SKILLS_INJECTION_CATALOG_HINT", () => {
|
|
124
|
+
test("points a missing product at plugin and skill search", async () => {
|
|
125
|
+
const { SKILLS_INJECTION_CATALOG_HINT } = await import(
|
|
126
|
+
"../skill-content.js"
|
|
127
|
+
);
|
|
128
|
+
expect(SKILLS_INJECTION_CATALOG_HINT).toContain(
|
|
129
|
+
"assistant plugins search <name>",
|
|
130
|
+
);
|
|
131
|
+
expect(SKILLS_INJECTION_CATALOG_HINT).toContain(
|
|
132
|
+
"assistant skills search <name>",
|
|
133
|
+
);
|
|
134
|
+
expect(SKILLS_INJECTION_CATALOG_HINT.toLowerCase()).toContain(
|
|
135
|
+
"currently in the workspace",
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -29,6 +29,13 @@ export const ALWAYS_CANDIDATE_CARD_CHARS = 900;
|
|
|
29
29
|
*/
|
|
30
30
|
export const DEFAULT_CARD_CHARS = 500;
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* One-shot line under injected skill cards. The cards list workspace skills
|
|
34
|
+
* only, so a missing product is not evidence it is unsupported.
|
|
35
|
+
*/
|
|
36
|
+
export const SKILLS_INJECTION_CATALOG_HINT =
|
|
37
|
+
"Injected skills are only ones currently in the workspace. Run `assistant plugins search <name>` and `assistant skills search <name>` before concluding a given integration or skill is unsupported";
|
|
38
|
+
|
|
32
39
|
/**
|
|
33
40
|
* Render the prose-style capability statement embedded into the unified
|
|
34
41
|
* `memory_v2_concept_pages` Qdrant collection (under the `skills/<id>` slug
|
|
@@ -36,6 +36,28 @@ describe("renderCardsBlockInner", () => {
|
|
|
36
36
|
test("empty card list renders the empty string (no header-only block)", () => {
|
|
37
37
|
expect(renderCardsBlockInner([])).toBe("");
|
|
38
38
|
});
|
|
39
|
+
|
|
40
|
+
test("skill cards get a one-shot catalog hint that is not a concept card", () => {
|
|
41
|
+
const inner = renderCardsBlockInner([
|
|
42
|
+
"# Skill: telegram-setup\nSet up Telegram.",
|
|
43
|
+
"# memory/concepts/page-a.md\nhead a",
|
|
44
|
+
]);
|
|
45
|
+
expect(inner).toContain(V3_CARDS_INJECTION_HEADER);
|
|
46
|
+
expect(inner).toContain("# Skills\n");
|
|
47
|
+
expect(inner).toContain("assistant plugins search <name>");
|
|
48
|
+
expect(inner).toContain("assistant skills search <name>");
|
|
49
|
+
expect(inner).toContain("currently in the workspace");
|
|
50
|
+
expect(inner.indexOf("# Skills")).toBeLessThan(inner.indexOf("# Skill:"));
|
|
51
|
+
expect(inner.startsWith(`${V3_CARDS_INJECTION_HEADER}\n\n# Skills\n`)).toBe(
|
|
52
|
+
true,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("concept-only cards omit the skill catalog hint", () => {
|
|
57
|
+
const inner = renderCardsBlockInner(["# memory/concepts/page-a.md\nhead a"]);
|
|
58
|
+
expect(inner).not.toContain("# Skills\n");
|
|
59
|
+
expect(inner).not.toContain("assistant plugins search");
|
|
60
|
+
});
|
|
39
61
|
});
|
|
40
62
|
|
|
41
63
|
describe("renderSpotlightInner", () => {
|
|
@@ -190,6 +190,18 @@ describe("parseCardSections / filterPrunedCardSections", () => {
|
|
|
190
190
|
expect(parsed.sections[1]!.text).toBe(card("page-b"));
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
test("skill catalog hint is a non-card piece and leaves the read-affordance preamble intact", () => {
|
|
194
|
+
const mixed = renderCardsBlockInner([
|
|
195
|
+
"# Skill: telegram-setup\nSet up Telegram.",
|
|
196
|
+
card("page-a"),
|
|
197
|
+
]);
|
|
198
|
+
const parsed = parseCardSections(mixed);
|
|
199
|
+
expect(parsed.preamble).toBe(V3_CARDS_INJECTION_HEADER);
|
|
200
|
+
expect(parsed.sections.map((s) => s.slug)).toEqual(["page-a"]);
|
|
201
|
+
expect(parsed.pieces.some((piece) => piece.kind === "other")).toBe(true);
|
|
202
|
+
expect(mixed).toContain("assistant plugins search <name>");
|
|
203
|
+
});
|
|
204
|
+
|
|
193
205
|
test("no pruned slug present → returns the SAME reference (no-op)", () => {
|
|
194
206
|
expect(filterPrunedCardSections(inner, new Set(["page-z"]))).toBe(inner);
|
|
195
207
|
expect(filterPrunedCardSections(inner, new Set())).toBe(inner);
|
|
@@ -228,8 +240,14 @@ describe("parseCardSections / filterPrunedCardSections", () => {
|
|
|
228
240
|
expect(parsed.sections.map((s) => s.slug)).toEqual(["page-a", "page-b"]);
|
|
229
241
|
// page-a's section stops AT the capability header — it must not absorb it.
|
|
230
242
|
expect(parsed.sections[0]!.text).toBe(card("page-a"));
|
|
231
|
-
expect(parsed.pieces.map((p) => p.kind)).toEqual([
|
|
232
|
-
|
|
243
|
+
expect(parsed.pieces.map((p) => p.kind)).toEqual([
|
|
244
|
+
"other",
|
|
245
|
+
"card",
|
|
246
|
+
"other",
|
|
247
|
+
"card",
|
|
248
|
+
]);
|
|
249
|
+
expect(parsed.pieces[0]!.text).toContain("assistant plugins search <name>");
|
|
250
|
+
expect(parsed.pieces[2]!.text).toBe(CAPABILITY_CHUNK);
|
|
233
251
|
});
|
|
234
252
|
|
|
235
253
|
test("pruning a concept card never swallows a trailing capability chunk", () => {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { wrapMemoryBlock } from "../memory-marker.js";
|
|
2
|
+
import { SKILLS_INJECTION_CATALOG_HINT } from "../substrate/skill-content.js";
|
|
2
3
|
import { Section, Slug } from "./types.js";
|
|
3
4
|
|
|
5
|
+
/** Own `# ` chunk so prune treats the catalog hint as a non-card piece. */
|
|
6
|
+
const SKILLS_CATALOG_HINT_CHUNK = `# Skills\n${SKILLS_INJECTION_CATALOG_HINT}`;
|
|
7
|
+
|
|
4
8
|
/**
|
|
5
9
|
* Leading instruction line of the frozen card block — byte-identical to v2's
|
|
6
10
|
* `INJECTION_HEADER` (`memory/v2/injection.ts`) so the read affordance the
|
|
@@ -23,7 +27,12 @@ export function renderCardsBlockInner(cards: string[]): string {
|
|
|
23
27
|
if (cards.length === 0) {
|
|
24
28
|
return "";
|
|
25
29
|
}
|
|
26
|
-
|
|
30
|
+
const parts = [V3_CARDS_INJECTION_HEADER];
|
|
31
|
+
if (cards.some((card) => card.startsWith("# Skill:"))) {
|
|
32
|
+
parts.push(SKILLS_CATALOG_HINT_CHUNK);
|
|
33
|
+
}
|
|
34
|
+
parts.push(...cards);
|
|
35
|
+
return parts.join("\n\n");
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
/**
|
|
@@ -121,8 +121,9 @@ function clampThinkingLevelToFloor(
|
|
|
121
121
|
* dynamic medium-level thinking).
|
|
122
122
|
*
|
|
123
123
|
* - `enabled: false` maps to the model's floor (the most "off" state it
|
|
124
|
-
* allows): `"minimal"` for most models, `"low"` for
|
|
125
|
-
*
|
|
124
|
+
* allows): `"minimal"` for most models, `"low"` for models whose catalog
|
|
125
|
+
* `thinkingFloor` is `"low"` (Pro and recent Flash IDs that reject
|
|
126
|
+
* `"minimal"`).
|
|
126
127
|
* - An explicit `level` below the floor is raised to the floor.
|
|
127
128
|
* - When no `level` is pinned, Pro models get the documented default (`"high"`)
|
|
128
129
|
* because an absent level resolves to the unsupported `"minimal"` upstream;
|
|
@@ -631,6 +631,22 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
|
|
|
631
631
|
linkLabel: "Open Google AI Studio",
|
|
632
632
|
},
|
|
633
633
|
models: [
|
|
634
|
+
{
|
|
635
|
+
id: "gemini-3.8-flash",
|
|
636
|
+
displayName: "Gemini 3.8 Flash",
|
|
637
|
+
contextWindowTokens: 1048576,
|
|
638
|
+
maxOutputTokens: 65536,
|
|
639
|
+
supportsThinking: true,
|
|
640
|
+
thinkingFloor: "low",
|
|
641
|
+
supportsCaching: true,
|
|
642
|
+
supportsVision: true,
|
|
643
|
+
supportsToolUse: true,
|
|
644
|
+
pricing: {
|
|
645
|
+
inputPer1mTokens: 1.5,
|
|
646
|
+
outputPer1mTokens: 7.5,
|
|
647
|
+
cacheReadPer1mTokens: 0.15,
|
|
648
|
+
},
|
|
649
|
+
},
|
|
634
650
|
{
|
|
635
651
|
id: "gemini-3.7-flash",
|
|
636
652
|
displayName: "Gemini 3.7 Flash",
|