@vellumai/assistant 0.11.10 → 0.11.11-staging.1
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/AGENTS.md +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/openapi.yaml +152 -55
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +60 -0
- package/src/__tests__/config-get-vision-flag.test.ts +36 -0
- package/src/__tests__/conversation-error.test.ts +38 -0
- package/src/__tests__/copy-composer-tc-templates.test.ts +2 -2
- package/src/__tests__/guardian-action-service-scope.test.ts +53 -0
- package/src/__tests__/headless-browser-mode.test.ts +63 -13
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +118 -27
- package/src/__tests__/host-app-control-proxy.test.ts +2 -2
- package/src/__tests__/host-app-control-routes.test.ts +41 -0
- package/src/__tests__/host-bash-routes.test.ts +30 -1
- package/src/__tests__/host-browser-routes.test.ts +34 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +29 -0
- package/src/__tests__/host-file-routes-targeted.test.ts +29 -0
- package/src/__tests__/host-proxy-base.test.ts +1 -0
- package/src/__tests__/host-transfer-routes-targeted.test.ts +86 -0
- package/src/__tests__/llm-schema.test.ts +32 -0
- package/src/__tests__/mcp-config-secret-boundary.test.ts +2 -0
- package/src/__tests__/memory-config-file-parity.test.ts +18 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +15 -0
- package/src/__tests__/messaging-send-tool.test.ts +159 -13
- package/src/__tests__/messaging-shared.test.ts +19 -0
- package/src/__tests__/notification-events-source-context-probe.test.ts +211 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +40 -6
- package/src/__tests__/outlook-messaging-provider.test.ts +9 -0
- package/src/__tests__/provider-catalog-visibility.test.ts +2 -2
- package/src/__tests__/schedule-routes.test.ts +30 -1
- package/src/__tests__/scheduler-result-notification.test.ts +209 -0
- package/src/__tests__/skills-install-extract.test.ts +26 -0
- package/src/__tests__/skills-install-staging.test.ts +11 -2
- package/src/api/constants/personality-sliders.test.ts +21 -0
- package/src/api/constants/personality-sliders.ts +64 -0
- package/src/api/index.ts +8 -0
- package/src/archive/ustar-size.test.ts +32 -0
- package/src/archive/ustar-size.ts +23 -0
- package/src/background-wake/next-wake.test.ts +13 -0
- package/src/background-wake/next-wake.ts +4 -2
- package/src/calls/voice-session-bridge.ts +26 -0
- package/src/cli/__tests__/catalog-search-help.test.ts +1 -0
- package/src/cli/commands/__tests__/plugins-search.test.ts +81 -0
- package/src/cli/commands/config.help.ts +3 -1
- package/src/cli/commands/memory/worker.ts +1 -1
- package/src/cli/commands/platform/__tests__/credits-format.test.ts +94 -5
- package/src/cli/commands/platform/__tests__/credits.test.ts +10 -0
- package/src/cli/commands/platform/credits-format.ts +57 -0
- package/src/cli/commands/platform/index.help.ts +19 -3
- package/src/cli/commands/plugins.help.ts +4 -1
- package/src/cli/commands/plugins.ts +2 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/cli/lib/install-from-platform.ts +8 -10
- package/src/config/bundled-skills/messaging/SKILL.md +2 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -4
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +85 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +9 -1
- package/src/config/bundled-skills/screen-annotation/SKILL.md +87 -20
- package/src/config/bundled-skills/screen-annotation/TOOLS.json +21 -5
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +67 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/input-modalities.test.ts +71 -0
- package/src/config/input-modalities.ts +95 -0
- package/src/config/schemas/heartbeat.ts +3 -3
- package/src/config/schemas/llm.ts +13 -3
- package/src/config/schemas/memory-storage.test.ts +39 -0
- package/src/config/schemas/memory-storage.ts +21 -0
- package/src/context/token-estimator.ts +2 -2
- package/src/daemon/__tests__/embedding-reconcile.test.ts +77 -0
- package/src/daemon/__tests__/lifecycle-embedding-reconcile.test.ts +3 -0
- package/src/daemon/conversation-error.ts +20 -7
- package/src/daemon/embedding-reconcile.ts +64 -1
- package/src/daemon/handlers/config-embeddings.ts +31 -0
- package/src/daemon/host-app-control-proxy.ts +2 -0
- package/src/daemon/host-bash-proxy.ts +1 -0
- package/src/daemon/host-cu-proxy.ts +6 -6
- package/src/daemon/host-file-proxy.ts +6 -6
- package/src/daemon/host-proxy-base.ts +10 -9
- package/src/daemon/host-transfer-proxy.ts +21 -24
- package/src/daemon/mcp-reload-service.ts +6 -0
- package/src/daemon/providers-setup.ts +5 -26
- package/src/heartbeat/heartbeat-service.ts +72 -13
- package/src/live-voice/__tests__/live-voice-photo.test.ts +72 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +81 -0
- package/src/live-voice/live-voice-photo.ts +68 -4
- package/src/live-voice/live-voice-session.ts +36 -2
- package/src/live-voice/protocol.ts +98 -1
- package/src/mcp/__tests__/reload-signal-emission.test.ts +54 -0
- package/src/mcp/__tests__/reload-signal.test.ts +57 -0
- package/src/mcp/__tests__/startup.test.ts +150 -0
- package/src/mcp/reload-signal.ts +50 -0
- package/src/mcp/startup.ts +101 -0
- package/src/messaging/providers/outlook/adapter.ts +2 -13
- package/src/messaging/providers/outlook/client.ts +13 -1
- package/src/messaging/providers/outlook/types.ts +3 -0
- package/src/notifications/__tests__/decision-engine.test.ts +79 -6
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +27 -0
- package/src/notifications/__tests__/schedule-result-producer.test.ts +397 -0
- package/src/notifications/copy-composer.ts +33 -11
- package/src/notifications/decision-engine.ts +48 -11
- package/src/notifications/events-store.ts +68 -2
- package/src/notifications/home-feed-side-effect.ts +15 -3
- package/src/notifications/schedule-result-producer.ts +275 -0
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/seed-providers.ts +3 -2
- package/src/persistence/conversation-crud.ts +38 -0
- package/src/persistence/embeddings/embedding-backend.test.ts +162 -0
- package/src/persistence/embeddings/embedding-backend.ts +148 -11
- package/src/persistence/embeddings/embedding-openai.test.ts +113 -0
- package/src/persistence/embeddings/embedding-openai.ts +55 -4
- package/src/persistence/embeddings/embedding-types.test.ts +24 -0
- package/src/persistence/embeddings/embedding-types.ts +25 -1
- package/src/persistence/job-utils.ts +6 -2
- package/src/plugin-api/index.ts +2 -1
- package/src/plugin-api/vision-support.test.ts +81 -0
- package/src/plugin-api/vision-support.ts +57 -10
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +8 -0
- package/src/plugins/defaults/memory/jobs/__tests__/embed-concept-page.test.ts +1 -0
- package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +11 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +20 -0
- package/src/plugins/defaults/memory/src/memory-worker-routes.ts +3 -1
- package/src/plugins/defaults/memory/substrate/__tests__/reembed-job.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/__tests__/backfill-jobs.test.ts +1 -0
- package/src/plugins/defaults/memory/v3/__tests__/section-dense-store.test.ts +82 -0
- package/src/plugins/defaults/memory/v3/section-dense-store.ts +9 -27
- package/src/providers/__tests__/provider-secret-catalog.test.ts +4 -0
- package/src/providers/__tests__/xml-tool-call-salvage.test.ts +172 -0
- package/src/providers/anthropic/client.ts +9 -4
- package/src/providers/inline-audio-support.test.ts +34 -0
- package/src/providers/inline-audio-support.ts +27 -0
- package/src/providers/model-catalog.ts +22 -2
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +50 -0
- package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +51 -0
- package/src/providers/openai/__tests__/chat-completions-provider-xml-salvage.test.ts +291 -0
- package/src/providers/openai/api-error-normalization.ts +18 -3
- package/src/providers/openai/chat-completions-provider.ts +113 -13
- package/src/providers/openai/responses-provider.ts +10 -8
- package/src/providers/provider-secret-catalog.ts +11 -1
- package/src/providers/vellum/client.ts +14 -1
- package/src/providers/vellum/personality-directions.test.ts +163 -0
- package/src/providers/vellum/personality-directions.ts +120 -0
- package/src/providers/xml-tool-call-salvage.ts +212 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +42 -4
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -6
- package/src/runtime/auth/__tests__/route-policy.test.ts +65 -0
- package/src/runtime/auth/same-actor.ts +29 -5
- package/src/runtime/background-job-runner.ts +38 -29
- package/src/runtime/guardian-action-service.ts +20 -1
- package/src/runtime/migrations/vbundle-validator.ts +3 -3
- package/src/runtime/pending-interactions.ts +4 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +24 -0
- package/src/runtime/routes/__tests__/platform-credits-routes.test.ts +99 -5
- package/src/runtime/routes/conversation-query-routes.ts +53 -11
- package/src/runtime/routes/document-comments-routes.ts +8 -2
- package/src/runtime/routes/guardian-action-routes.ts +7 -0
- package/src/runtime/routes/heartbeat-routes.ts +41 -53
- package/src/runtime/routes/host-app-control-routes.ts +10 -36
- package/src/runtime/routes/host-bash-routes.ts +11 -45
- package/src/runtime/routes/host-browser-routes.ts +24 -0
- package/src/runtime/routes/host-cu-routes.ts +11 -45
- package/src/runtime/routes/host-file-routes.ts +11 -44
- package/src/runtime/routes/host-proxy-result-binding.ts +57 -0
- package/src/runtime/routes/host-transfer-routes.ts +33 -25
- package/src/runtime/routes/index.ts +0 -2
- package/src/runtime/routes/integrations/a2a.ts +12 -3
- package/src/runtime/routes/integrations/vercel.ts +13 -3
- package/src/runtime/routes/platform-routes.ts +121 -24
- package/src/runtime/routes/schedule-routes.ts +15 -1
- package/src/schedule/__tests__/schedule-timezone.test.ts +21 -3
- package/src/schedule/__tests__/worker-mcp-tools.test.ts +88 -0
- package/src/schedule/schedule-timezone.ts +25 -0
- package/src/schedule/scheduler.ts +19 -0
- package/src/schedule/worker.ts +119 -2
- package/src/skills/catalog-install.ts +40 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +3 -0
- package/src/tools/browser/browser-execution.ts +117 -53
- package/src/tools/browser/browser-status-constants.ts +6 -6
- package/src/tools/capability-offer.test.ts +87 -0
- package/src/tools/capability-offer.ts +93 -0
- package/src/tools/terminal/__tests__/safe-env.test.ts +17 -0
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/util/provider-error-patterns.ts +34 -0
- package/src/watcher/__tests__/engine.test.ts +5 -2
- package/src/watcher/engine.ts +7 -4
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +0 -487
- package/src/runtime/routes/sight-frame-routes.ts +0 -136
|
@@ -70,12 +70,36 @@ export function embeddingInputContentHash(input: EmbeddingInput): string {
|
|
|
70
70
|
return hash.digest("hex");
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Durable cache identity for an embedding input. Folds provider extras
|
|
75
|
+
* (Gemini task type / dimensions, custom endpoint URL / dimensions) into the
|
|
76
|
+
* bare content hash so a change that alters the vector for identical text is
|
|
77
|
+
* a cache miss. With no extras this is the bare {@link embeddingInputContentHash}.
|
|
78
|
+
*/
|
|
79
|
+
export function embeddingContentHashWithExtras(
|
|
80
|
+
input: EmbeddingInput,
|
|
81
|
+
extras: string[] = [],
|
|
82
|
+
): string {
|
|
83
|
+
const base = embeddingInputContentHash(input);
|
|
84
|
+
if (extras.length === 0) {
|
|
85
|
+
return base;
|
|
86
|
+
}
|
|
87
|
+
return createHash("sha256")
|
|
88
|
+
.update(`${base}\0${extras.join("\0")}`)
|
|
89
|
+
.digest("hex");
|
|
90
|
+
}
|
|
91
|
+
|
|
73
92
|
// ---------------------------------------------------------------------------
|
|
74
93
|
// Backend interface types (extracted from embedding-backend.ts to break
|
|
75
94
|
// circular imports between the factory and provider implementations)
|
|
76
95
|
// ---------------------------------------------------------------------------
|
|
77
96
|
|
|
78
|
-
export type EmbeddingProviderName =
|
|
97
|
+
export type EmbeddingProviderName =
|
|
98
|
+
| "local"
|
|
99
|
+
| "openai"
|
|
100
|
+
| "gemini"
|
|
101
|
+
| "ollama"
|
|
102
|
+
| "custom";
|
|
79
103
|
|
|
80
104
|
export interface EmbeddingRequestOptions {
|
|
81
105
|
signal?: AbortSignal;
|
|
@@ -7,13 +7,14 @@ import { BackendUnavailableError } from "../util/errors.js";
|
|
|
7
7
|
import { getLogger } from "../util/logger.js";
|
|
8
8
|
import { getMemoryDb } from "./db-connection.js";
|
|
9
9
|
import {
|
|
10
|
+
durableEmbeddingCacheExtras,
|
|
10
11
|
embedWithBackend,
|
|
11
12
|
generateSparseEmbedding,
|
|
12
13
|
getMemoryBackendStatus,
|
|
13
14
|
} from "./embeddings/embedding-backend.js";
|
|
14
15
|
import type { EmbeddingInput } from "./embeddings/embedding-types.js";
|
|
15
16
|
import {
|
|
16
|
-
|
|
17
|
+
embeddingContentHashWithExtras,
|
|
17
18
|
normalizeEmbeddingInput,
|
|
18
19
|
} from "./embeddings/embedding-types.js";
|
|
19
20
|
import { withQdrantBreaker } from "./embeddings/qdrant-circuit-breaker.js";
|
|
@@ -186,7 +187,10 @@ export async function embedAndUpsert(
|
|
|
186
187
|
);
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
const contentHash =
|
|
190
|
+
const contentHash = embeddingContentHashWithExtras(
|
|
191
|
+
input,
|
|
192
|
+
durableEmbeddingCacheExtras(config, status.provider),
|
|
193
|
+
);
|
|
190
194
|
let provider = status.provider;
|
|
191
195
|
let model = status.model!;
|
|
192
196
|
let vector: number[];
|
package/src/plugin-api/index.ts
CHANGED
|
@@ -164,7 +164,8 @@ export { publishEvent } from "./publish-event.js";
|
|
|
164
164
|
// model id, a profile key, or a `ModelProfileInfo`; a bare string is resolved
|
|
165
165
|
// as a model id first and then as a profile key. Profile resolution merges over
|
|
166
166
|
// the workspace default and infers the provider for model-only profiles, then
|
|
167
|
-
// looks up the model catalog's `supportsVision` flag
|
|
167
|
+
// looks up the model catalog's `supportsVision` flag, with a profile
|
|
168
|
+
// `inputModalities.image` override winning when set (mix profiles are
|
|
168
169
|
// vision-capable if any arm is). Returns false when nothing resolves.
|
|
169
170
|
export { doesSupportVision } from "./vision-support.js";
|
|
170
171
|
// Resolve a stored credential to its plaintext value — the same value
|
|
@@ -17,6 +17,12 @@ interface MockProfileEntry {
|
|
|
17
17
|
model?: string;
|
|
18
18
|
status?: string;
|
|
19
19
|
mix?: Array<{ profile: string; weight: number }>;
|
|
20
|
+
inputModalities?: {
|
|
21
|
+
text?: { enabled?: boolean; supported?: boolean };
|
|
22
|
+
image?: { enabled?: boolean; supported?: boolean };
|
|
23
|
+
audio?: { enabled?: boolean; supported?: boolean };
|
|
24
|
+
video?: { enabled?: boolean; supported?: boolean };
|
|
25
|
+
};
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
let mockProfiles: Record<string, MockProfileEntry> = {};
|
|
@@ -103,6 +109,49 @@ describe("doesSupportVision", () => {
|
|
|
103
109
|
expect(doesSupportVision(profile("unknown-model"))).toBe(false);
|
|
104
110
|
});
|
|
105
111
|
|
|
112
|
+
test("an openai-compatible profile sends images when the override declares support", () => {
|
|
113
|
+
setMockConfig({
|
|
114
|
+
gateway: {
|
|
115
|
+
provider: "openai-compatible",
|
|
116
|
+
model: "qwen2.5-vl",
|
|
117
|
+
inputModalities: { image: { enabled: true, supported: true } },
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
expect(doesSupportVision(profile("gateway"))).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("an openai-compatible profile without an override still fail-closes", () => {
|
|
124
|
+
setMockConfig({
|
|
125
|
+
gateway: {
|
|
126
|
+
provider: "openai-compatible",
|
|
127
|
+
model: "qwen2.5-vl",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
expect(doesSupportVision(profile("gateway"))).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("enabling image without declaring support keeps the catalog value", () => {
|
|
134
|
+
setMockConfig({
|
|
135
|
+
gateway: {
|
|
136
|
+
provider: "openai-compatible",
|
|
137
|
+
model: "qwen2.5-vl",
|
|
138
|
+
inputModalities: { image: { enabled: true } },
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
expect(doesSupportVision(profile("gateway"))).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("a catalog vision model can be policy-disabled on the profile", () => {
|
|
145
|
+
setMockConfig({
|
|
146
|
+
"vision-profile": {
|
|
147
|
+
provider: "anthropic",
|
|
148
|
+
model: "claude-opus-4-6",
|
|
149
|
+
inputModalities: { image: { enabled: false } },
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
expect(doesSupportVision(profile("vision-profile"))).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
106
155
|
test("implies the provider from the catalog when profile only sets model", () => {
|
|
107
156
|
setMockConfig({ "model-only": { model: "claude-opus-4-6" } });
|
|
108
157
|
expect(doesSupportVision(profile("model-only"))).toBe(true);
|
|
@@ -144,6 +193,27 @@ describe("doesSupportVision", () => {
|
|
|
144
193
|
expect(doesSupportVision(profile("mix-profile"))).toBe(true);
|
|
145
194
|
});
|
|
146
195
|
|
|
196
|
+
test("mix profile returns true when an unknown arm declares image support", () => {
|
|
197
|
+
setMockConfig({
|
|
198
|
+
"mix-profile": {
|
|
199
|
+
mix: [
|
|
200
|
+
{ profile: "text-arm", weight: 0.5 },
|
|
201
|
+
{ profile: "gateway-arm", weight: 0.5 },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
"text-arm": {
|
|
205
|
+
provider: "fireworks",
|
|
206
|
+
model: "accounts/fireworks/models/glm-5p2",
|
|
207
|
+
},
|
|
208
|
+
"gateway-arm": {
|
|
209
|
+
provider: "openai-compatible",
|
|
210
|
+
model: "qwen2.5-vl",
|
|
211
|
+
inputModalities: { image: { enabled: true, supported: true } },
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
expect(doesSupportVision(profile("mix-profile"))).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
|
|
147
217
|
test("mix profile returns false when all arms are text-only", () => {
|
|
148
218
|
setMockConfig({
|
|
149
219
|
"mix-profile": {
|
|
@@ -315,4 +385,15 @@ describe("doesSupportVision with a bare string", () => {
|
|
|
315
385
|
setMockConfig({});
|
|
316
386
|
expect(doesSupportVision("some-unknown-string")).toBe(false);
|
|
317
387
|
});
|
|
388
|
+
|
|
389
|
+
test("honors a free-text image override when looked up by model id", () => {
|
|
390
|
+
setMockConfig({
|
|
391
|
+
gateway: {
|
|
392
|
+
provider: "openai-compatible",
|
|
393
|
+
model: "qwen2.5-vl",
|
|
394
|
+
inputModalities: { image: { enabled: true, supported: true } },
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
expect(doesSupportVision("qwen2.5-vl")).toBe(true);
|
|
398
|
+
});
|
|
318
399
|
});
|
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { resolveDefaultProfileForProvider } from "../config/default-profile-catalog.js";
|
|
25
|
+
import {
|
|
26
|
+
catalogEntryFor,
|
|
27
|
+
type InputModalities,
|
|
28
|
+
modalitiesOf,
|
|
29
|
+
resolveModalityOverride,
|
|
30
|
+
} from "../config/input-modalities.js";
|
|
25
31
|
import { getConfig } from "../config/loader.js";
|
|
26
32
|
import { resolveEntryProviderKind } from "../providers/connection-resolution.js";
|
|
27
33
|
import { ROUTING_IDENTITY_PROVIDERS } from "../providers/inference/auth.js";
|
|
@@ -42,14 +48,52 @@ export function doesSupportVision(
|
|
|
42
48
|
modelOrProfile: ModelProfileInfo | string,
|
|
43
49
|
): boolean {
|
|
44
50
|
if (typeof modelOrProfile === "string") {
|
|
45
|
-
// Concrete model id first, then
|
|
51
|
+
// Concrete catalog model id first, then a profile key, then any profile
|
|
52
|
+
// bound to that model id (so a free-text override is visible to callers
|
|
53
|
+
// that only have the provider-reported model, e.g. post-tool-use).
|
|
46
54
|
return (
|
|
47
|
-
modelVision(modelOrProfile) ??
|
|
55
|
+
modelVision(modelOrProfile) ??
|
|
56
|
+
profileVision(modelOrProfile) ??
|
|
57
|
+
modelVisionFromBoundProfiles(modelOrProfile) ??
|
|
58
|
+
false
|
|
48
59
|
);
|
|
49
60
|
}
|
|
50
61
|
return profileVision(modelOrProfile.key) ?? false;
|
|
51
62
|
}
|
|
52
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Vision from a profile whose `model` field matches `model`. Used when the
|
|
66
|
+
* catalog does not know the id (openai-compatible / free-text). Any bound
|
|
67
|
+
* profile that declares image support wins so tool-result images are not
|
|
68
|
+
* captioned for a model the user marked as vision-capable.
|
|
69
|
+
*/
|
|
70
|
+
function modelVisionFromBoundProfiles(model: string): boolean | undefined {
|
|
71
|
+
const profiles = getConfig().llm.profiles ?? {};
|
|
72
|
+
let sawUnknown = false;
|
|
73
|
+
let sawFalse = false;
|
|
74
|
+
for (const entry of Object.values(profiles)) {
|
|
75
|
+
if (entry?.model !== model || entry.mix != null) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const vision = resolveEntryVision(entry);
|
|
79
|
+
if (vision === true) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (vision === false) {
|
|
83
|
+
sawFalse = true;
|
|
84
|
+
} else {
|
|
85
|
+
sawUnknown = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (sawUnknown) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
if (sawFalse) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
53
97
|
/**
|
|
54
98
|
* Catalog vision flag for a concrete model id, or `undefined` when the catalog
|
|
55
99
|
* doesn't know the model. The same model id carries the same capability under
|
|
@@ -110,13 +154,16 @@ function profileVision(profileKey: string): boolean | undefined {
|
|
|
110
154
|
/**
|
|
111
155
|
* Resolve whether a concrete (non-mix) profile entry supports vision from its
|
|
112
156
|
* own `(provider, model)`, inferring the provider from the catalog when only
|
|
113
|
-
* the model is set.
|
|
114
|
-
*
|
|
115
|
-
*
|
|
157
|
+
* the model is set. A profile `inputModalities.image` override wins over the
|
|
158
|
+
* catalog. Returns `undefined` when the effective `(provider, model)` can't
|
|
159
|
+
* be determined or isn't in the catalog and no override is set — an entry
|
|
160
|
+
* that omits its model is not a usable resolution target, so it fails safe
|
|
161
|
+
* to "caption".
|
|
116
162
|
*/
|
|
117
163
|
function resolveEntryVision(entry: {
|
|
118
164
|
provider?: string;
|
|
119
165
|
model?: string;
|
|
166
|
+
inputModalities?: InputModalities | null;
|
|
120
167
|
}): boolean | undefined {
|
|
121
168
|
// Routing identities ("vellum"/"chatgpt") are not catalog providers; the
|
|
122
169
|
// model's catalog owner is the capability source for them.
|
|
@@ -142,12 +189,12 @@ function resolveEntryVision(entry: {
|
|
|
142
189
|
(typeof model === "string" ? getCatalogProviderForModel(model) : undefined);
|
|
143
190
|
|
|
144
191
|
if (typeof effectiveProvider !== "string" || typeof model !== "string") {
|
|
145
|
-
return undefined;
|
|
192
|
+
return resolveModalityOverride(modalitiesOf(entry)?.image, undefined);
|
|
146
193
|
}
|
|
147
194
|
|
|
148
|
-
const
|
|
149
|
-
|
|
195
|
+
const catalogModel = catalogEntryFor(effectiveProvider, model);
|
|
196
|
+
return resolveModalityOverride(
|
|
197
|
+
modalitiesOf(entry)?.image,
|
|
198
|
+
catalogModel?.supportsVision,
|
|
150
199
|
);
|
|
151
|
-
const catalogModel = catalogProvider?.models.find((m) => m.id === model);
|
|
152
|
-
return catalogModel?.supportsVision;
|
|
153
200
|
}
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
* Registered first in the `user-prompt-submit` chain, so it heads the chain
|
|
26
26
|
* ahead of history repair and title generation: those later hooks see the
|
|
27
27
|
* fully assembled, memory-injected history.
|
|
28
|
+
*
|
|
29
|
+
* Memory-retrospective wakes skip this hook. Those forks already carry copied
|
|
30
|
+
* injection metadata on historical messages. The remember-pass does not need a
|
|
31
|
+
* fresh selector or router call, and that call is what this hook awaits.
|
|
28
32
|
*/
|
|
29
33
|
|
|
30
34
|
import type {
|
|
@@ -252,6 +256,10 @@ const userPromptSubmitMemoryRetrieval: HookFunction<
|
|
|
252
256
|
// threaded in, mirroring how `applyRuntimeInjections` self-resolves its
|
|
253
257
|
// per-turn inputs.
|
|
254
258
|
const conversation = findConversationOrSubagent(ctx.conversationId);
|
|
259
|
+
if (conversation?.currentCallSite === "memoryRetrospective") {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
255
263
|
const config = getConfig();
|
|
256
264
|
const abortSignal = conversation?.abortController?.signal;
|
|
257
265
|
const isTrustedActor =
|
|
@@ -67,6 +67,7 @@ mock.module(
|
|
|
67
67
|
indices: [text.length % 100],
|
|
68
68
|
values: [1],
|
|
69
69
|
}),
|
|
70
|
+
durableEmbeddingCacheExtras: () => [],
|
|
70
71
|
// Other exports from the real module — stubbed so adjacent imports
|
|
71
72
|
// (e.g. via transitive `db.ts` → `indexer.ts`) don't crash on missing
|
|
72
73
|
// names when the mock replaces the module wholesale.
|
|
@@ -23,10 +23,11 @@ import { and, eq } from "drizzle-orm";
|
|
|
23
23
|
import type { AssistantConfig } from "../../../../config/types.js";
|
|
24
24
|
import { getDb } from "../../../../persistence/db-connection.js";
|
|
25
25
|
import {
|
|
26
|
+
durableEmbeddingCacheExtras,
|
|
26
27
|
generateSparseEmbedding,
|
|
27
28
|
getMemoryBackendStatus,
|
|
28
29
|
} from "../../../../persistence/embeddings/embedding-backend.js";
|
|
29
|
-
import {
|
|
30
|
+
import { embeddingContentHashWithExtras } from "../../../../persistence/embeddings/embedding-types.js";
|
|
30
31
|
import { withQdrantBreaker } from "../../../../persistence/embeddings/qdrant-circuit-breaker.js";
|
|
31
32
|
import {
|
|
32
33
|
asString,
|
|
@@ -131,7 +132,11 @@ export async function embedConceptPageJob(
|
|
|
131
132
|
// and (optional) summary share the same provider/model — but each gets
|
|
132
133
|
// its own cache row keyed by a distinct targetId so summary edits don't
|
|
133
134
|
// invalidate the body cache and vice versa.
|
|
134
|
-
const
|
|
135
|
+
const extras = durableEmbeddingCacheExtras(config, cacheProvider);
|
|
136
|
+
const bodyContentHash = embeddingContentHashWithExtras(
|
|
137
|
+
{ type: "text", text },
|
|
138
|
+
extras,
|
|
139
|
+
);
|
|
135
140
|
const bodyCache = mem
|
|
136
141
|
? readEmbeddingCache(mem, slug, cacheProvider, cacheModel, expectedDim)
|
|
137
142
|
: null;
|
|
@@ -145,7 +150,10 @@ export async function embedConceptPageJob(
|
|
|
145
150
|
const hasSummary = summaryText.length > 0;
|
|
146
151
|
const summaryCacheId = `${slug}#summary`;
|
|
147
152
|
const summaryContentHash = hasSummary
|
|
148
|
-
?
|
|
153
|
+
? embeddingContentHashWithExtras(
|
|
154
|
+
{ type: "text", text: summaryText },
|
|
155
|
+
extras,
|
|
156
|
+
)
|
|
149
157
|
: undefined;
|
|
150
158
|
const summaryCache =
|
|
151
159
|
hasSummary && mem
|
|
@@ -176,6 +176,26 @@ describe("memory_worker_status", () => {
|
|
|
176
176
|
).toBe(true);
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
+
test("accepts custom as a documented embedding provider on the wire", async () => {
|
|
180
|
+
backendStatus = {
|
|
181
|
+
enabled: true,
|
|
182
|
+
degraded: false,
|
|
183
|
+
provider: "custom",
|
|
184
|
+
model: "text-embedding-3-small",
|
|
185
|
+
reason: null,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const res = await handler("memory_worker_status")();
|
|
189
|
+
|
|
190
|
+
expect(res).toMatchObject({
|
|
191
|
+
embedding: { provider: "custom" },
|
|
192
|
+
});
|
|
193
|
+
expect(
|
|
194
|
+
embeddingStatusSchema.safeParse((res as { embedding: unknown }).embedding)
|
|
195
|
+
.success,
|
|
196
|
+
).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
179
199
|
test("surfaces a degraded embedding backend with a reason when none is configured", async () => {
|
|
180
200
|
backendStatus = {
|
|
181
201
|
enabled: true,
|
|
@@ -32,7 +32,9 @@ const log = getLogger("memory-worker-routes");
|
|
|
32
32
|
export const embeddingStatusSchema = z.object({
|
|
33
33
|
enabled: z.boolean(),
|
|
34
34
|
degraded: z.boolean(),
|
|
35
|
-
provider: z
|
|
35
|
+
provider: z
|
|
36
|
+
.enum(["local", "openai", "gemini", "ollama", "custom"])
|
|
37
|
+
.nullable(),
|
|
36
38
|
model: z.string().nullable(),
|
|
37
39
|
reason: z.string().nullable(),
|
|
38
40
|
});
|
|
@@ -33,6 +33,8 @@ const embedState = {
|
|
|
33
33
|
// the section cache treats it as a miss.
|
|
34
34
|
geminiTaskType: undefined as string | undefined,
|
|
35
35
|
geminiDimensions: undefined as number | undefined,
|
|
36
|
+
customBaseUrl: undefined as string | undefined,
|
|
37
|
+
customDimensions: undefined as number | undefined,
|
|
36
38
|
};
|
|
37
39
|
mock.module(
|
|
38
40
|
"../../../../../persistence/embeddings/embedding-backend.js",
|
|
@@ -64,6 +66,42 @@ mock.module(
|
|
|
64
66
|
}
|
|
65
67
|
return extras;
|
|
66
68
|
},
|
|
69
|
+
customCacheExtras: () => {
|
|
70
|
+
const extras: string[] = [];
|
|
71
|
+
if (embedState.customBaseUrl) {
|
|
72
|
+
extras.push(`url=${embedState.customBaseUrl}`);
|
|
73
|
+
}
|
|
74
|
+
if (embedState.customDimensions != null) {
|
|
75
|
+
extras.push(`dim=${embedState.customDimensions}`);
|
|
76
|
+
}
|
|
77
|
+
return extras;
|
|
78
|
+
},
|
|
79
|
+
durableEmbeddingCacheExtras: (
|
|
80
|
+
_config: unknown,
|
|
81
|
+
provider: string | null,
|
|
82
|
+
) => {
|
|
83
|
+
if (provider === "gemini") {
|
|
84
|
+
const extras: string[] = [];
|
|
85
|
+
if (embedState.geminiTaskType) {
|
|
86
|
+
extras.push(`task=${embedState.geminiTaskType}`);
|
|
87
|
+
}
|
|
88
|
+
if (embedState.geminiDimensions != null) {
|
|
89
|
+
extras.push(`dim=${embedState.geminiDimensions}`);
|
|
90
|
+
}
|
|
91
|
+
return extras;
|
|
92
|
+
}
|
|
93
|
+
if (provider === "custom") {
|
|
94
|
+
const extras: string[] = [];
|
|
95
|
+
if (embedState.customBaseUrl) {
|
|
96
|
+
extras.push(`url=${embedState.customBaseUrl}`);
|
|
97
|
+
}
|
|
98
|
+
if (embedState.customDimensions != null) {
|
|
99
|
+
extras.push(`dim=${embedState.customDimensions}`);
|
|
100
|
+
}
|
|
101
|
+
return extras;
|
|
102
|
+
}
|
|
103
|
+
return [];
|
|
104
|
+
},
|
|
67
105
|
}),
|
|
68
106
|
);
|
|
69
107
|
|
|
@@ -320,6 +358,8 @@ function resetState(): void {
|
|
|
320
358
|
embedState.embedModel = "test-model";
|
|
321
359
|
embedState.geminiTaskType = undefined;
|
|
322
360
|
embedState.geminiDimensions = undefined;
|
|
361
|
+
embedState.customBaseUrl = undefined;
|
|
362
|
+
embedState.customDimensions = undefined;
|
|
323
363
|
cacheState.store.clear();
|
|
324
364
|
cacheState.reads.length = 0;
|
|
325
365
|
checkpointState.deletes.length = 0;
|
|
@@ -767,6 +807,48 @@ describe("memory v3 section-dense-store — embedding cache", () => {
|
|
|
767
807
|
expect(embedState.calls).toHaveLength(1);
|
|
768
808
|
expect(state.upsertCalls).toHaveLength(2);
|
|
769
809
|
});
|
|
810
|
+
|
|
811
|
+
test("a custom endpoint change re-embeds unchanged text (cache miss)", async () => {
|
|
812
|
+
state.collectionExists = true;
|
|
813
|
+
embedState.statusProvider = "custom";
|
|
814
|
+
embedState.statusModel = "text-embedding-3-small";
|
|
815
|
+
embedState.embedProvider = "custom";
|
|
816
|
+
embedState.embedModel = "text-embedding-3-small";
|
|
817
|
+
embedState.customBaseUrl = "http://127.0.0.1:4000/v1";
|
|
818
|
+
|
|
819
|
+
await upsertSections(CONFIG, [
|
|
820
|
+
section("people/alice", 0, "alice lead text"),
|
|
821
|
+
]);
|
|
822
|
+
expect(embedState.calls).toHaveLength(1);
|
|
823
|
+
|
|
824
|
+
embedState.customBaseUrl = "http://127.0.0.1:4001/v1";
|
|
825
|
+
|
|
826
|
+
await upsertSections(CONFIG, [
|
|
827
|
+
section("people/alice", 0, "alice lead text"),
|
|
828
|
+
]);
|
|
829
|
+
|
|
830
|
+
expect(embedState.calls).toEqual([
|
|
831
|
+
["alice lead text"],
|
|
832
|
+
["alice lead text"],
|
|
833
|
+
]);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
test("an unchanged custom endpoint still serves from cache", async () => {
|
|
837
|
+
state.collectionExists = true;
|
|
838
|
+
embedState.statusProvider = "custom";
|
|
839
|
+
embedState.statusModel = "text-embedding-3-small";
|
|
840
|
+
embedState.embedProvider = "custom";
|
|
841
|
+
embedState.embedModel = "text-embedding-3-small";
|
|
842
|
+
embedState.customBaseUrl = "http://127.0.0.1:4000/v1";
|
|
843
|
+
|
|
844
|
+
const sections = [section("people/alice", 0, "alice lead text")];
|
|
845
|
+
|
|
846
|
+
await upsertSections(CONFIG, sections);
|
|
847
|
+
await upsertSections(CONFIG, sections);
|
|
848
|
+
|
|
849
|
+
expect(embedState.calls).toHaveLength(1);
|
|
850
|
+
expect(state.upsertCalls).toHaveLength(2);
|
|
851
|
+
});
|
|
770
852
|
});
|
|
771
853
|
|
|
772
854
|
describe("memory v3 section-dense-store — delete", () => {
|
|
@@ -19,8 +19,6 @@
|
|
|
19
19
|
// shared Qdrant client; the read side (`dense.ts`) reuses that client to query
|
|
20
20
|
// this collection.
|
|
21
21
|
|
|
22
|
-
import { createHash } from "node:crypto";
|
|
23
|
-
|
|
24
22
|
import { QdrantClient as QdrantRestClient } from "@qdrant/js-client-rest";
|
|
25
23
|
import { v5 as uuidv5 } from "uuid";
|
|
26
24
|
|
|
@@ -31,14 +29,14 @@ import {
|
|
|
31
29
|
setMemoryCheckpoint,
|
|
32
30
|
} from "../../../../persistence/checkpoints.js";
|
|
33
31
|
import {
|
|
34
|
-
|
|
32
|
+
durableEmbeddingCacheExtras,
|
|
35
33
|
getMemoryBackendStatus,
|
|
36
34
|
} from "../../../../persistence/embeddings/embedding-backend.js";
|
|
37
35
|
import {
|
|
38
36
|
readEmbeddingCache,
|
|
39
37
|
writeEmbeddingCache,
|
|
40
38
|
} from "../../../../persistence/embeddings/embedding-cache.js";
|
|
41
|
-
import {
|
|
39
|
+
import { embeddingContentHashWithExtras } from "../../../../persistence/embeddings/embedding-types.js";
|
|
42
40
|
import { embedWithBackend, resolveQdrantUrl } from "../embeddings.js";
|
|
43
41
|
import { getLogger } from "../logging.js";
|
|
44
42
|
import { memoryDbOrNull } from "../memory-db.js";
|
|
@@ -514,24 +512,6 @@ function sectionCacheId(article: string, ordinal: number): string {
|
|
|
514
512
|
return `${article}#${ordinal}`;
|
|
515
513
|
}
|
|
516
514
|
|
|
517
|
-
/**
|
|
518
|
-
* Content hash a section's cached vector is keyed by. Folds the provider's
|
|
519
|
-
* embedding-option extras (Gemini task type / output dimensions) into the base
|
|
520
|
-
* text hash, so changing an option that alters the vector for identical text is
|
|
521
|
-
* a cache miss that re-embeds. With no extras the bare text hash is returned
|
|
522
|
-
* unchanged, keeping existing rows valid for non-Gemini and default-Gemini
|
|
523
|
-
* configs.
|
|
524
|
-
*/
|
|
525
|
-
function sectionContentHash(text: string, extras: string[]): string {
|
|
526
|
-
const base = embeddingInputContentHash({ type: "text", text });
|
|
527
|
-
if (extras.length === 0) {
|
|
528
|
-
return base;
|
|
529
|
-
}
|
|
530
|
-
return createHash("sha256")
|
|
531
|
-
.update(`${base}\0${extras.join("\0")}`)
|
|
532
|
-
.digest("hex");
|
|
533
|
-
}
|
|
534
|
-
|
|
535
515
|
/**
|
|
536
516
|
* Embed each section's `text` and upsert one point per section, keyed by a
|
|
537
517
|
* deterministic `(article, ordinal)`-derived ID. Stable IDs mean re-upserting
|
|
@@ -641,11 +621,13 @@ async function embedSectionsCached(
|
|
|
641
621
|
const status = await getMemoryBackendStatus(config);
|
|
642
622
|
const mem = memoryDbOrNull("embedSectionsCached");
|
|
643
623
|
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
const extras = status.provider
|
|
648
|
-
const hashes = sections.map((s) =>
|
|
624
|
+
// Fold provider extras that change the vector for identical text (Gemini
|
|
625
|
+
// task type / dimensions, custom endpoint URL / dimensions) into the
|
|
626
|
+
// durable cache identity. Other backends keep the bare text hash.
|
|
627
|
+
const extras = durableEmbeddingCacheExtras(config, status.provider);
|
|
628
|
+
const hashes = sections.map((s) =>
|
|
629
|
+
embeddingContentHashWithExtras({ type: "text", text: s.text }, extras),
|
|
630
|
+
);
|
|
649
631
|
|
|
650
632
|
const result: Array<number[] | undefined> = new Array(sections.length);
|
|
651
633
|
const missIndices: number[] = [];
|
|
@@ -47,4 +47,8 @@ describe("API_KEY_PROVIDERS", () => {
|
|
|
47
47
|
expect(API_KEY_PROVIDERS).toContain("tavily");
|
|
48
48
|
expect(API_KEY_PROVIDERS).toContain("firecrawl");
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
test("includes custom for OpenAI-compatible memory embeddings", () => {
|
|
52
|
+
expect(API_KEY_PROVIDERS).toContain("custom");
|
|
53
|
+
});
|
|
50
54
|
});
|