@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231620.a15f321
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/Dockerfile +7 -0
- package/README.md +12 -12
- package/docker/mousepad/90-vellum.gschema.override +4 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
- package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
- package/openapi.yaml +97 -4
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +4 -2
- package/src/__tests__/call-routes-http.test.ts +4 -2
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/config-loader-backfill.test.ts +1 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-resurface-done.test.ts +289 -0
- package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
- package/src/__tests__/gemini-provider.test.ts +31 -0
- package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -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__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/__tests__/provider-error-scenarios.test.ts +46 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
- package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -2
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/skill-load-tool.test.ts +42 -11
- package/src/__tests__/skill-tool-factory.test.ts +50 -2
- package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
- 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/ask-question-tool.ts +50 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +7 -0
- package/src/api/requests/dictation.ts +2 -2
- package/src/api/responses/home.ts +27 -0
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +63 -1
- package/src/calls/voice-escalation-judge.ts +6 -5
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +22 -0
- package/src/cli/commands/__tests__/plugins.test.ts +29 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/commands/plugins.ts +16 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
- package/src/cli/lib/bundled-marketplace.json +4 -4
- package/src/cli/lib/inspect-plugin.ts +5 -0
- package/src/cli/lib/plugin-catalog-cache.ts +41 -4
- package/src/cli/lib/plugin-catalog-local.ts +9 -0
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
- package/src/cli/lib/plugin-details.ts +10 -7
- package/src/cli/lib/search-plugins.ts +2 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
- package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
- package/src/config/call-site-defaults.ts +17 -11
- package/src/config/default-profile-catalog.ts +118 -19
- package/src/config/default-profile-names.ts +69 -0
- package/src/config/feature-flag-registry.json +25 -9
- 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 +8 -1
- package/src/config/schemas/llm.ts +70 -9
- package/src/config/sidebar-done-gate.ts +27 -0
- 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/conversation-slash.ts +2 -2
- package/src/daemon/conversation-turn-finalize.ts +12 -0
- package/src/daemon/dictation-text-processing.ts +4 -4
- package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
- package/src/daemon/handlers/config-channels.ts +3 -1
- package/src/daemon/handlers/conversation-history.ts +11 -1
- package/src/daemon/handlers/conversations.ts +118 -10
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/desktop/desktop-browser-endpoint.ts +1 -0
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
- package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/protocol.test.ts +22 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/live-voice/live-voice-session.ts +7 -6
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/notifications/README.md +11 -3
- 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/oauth/AGENTS.md +7 -1
- package/src/permissions/question-prompter.ts +14 -4
- package/src/persistence/conversation-crud.ts +114 -3
- package/src/persistence/conversation-queries.ts +48 -3
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
- package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
- package/src/persistence/embeddings/embedding-backend.ts +23 -41
- package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
- package/src/persistence/embeddings/embedding-gemini.ts +3 -1
- package/src/persistence/embeddings/embedding-local.ts +72 -17
- package/src/persistence/embeddings/embedding-types.ts +0 -19
- package/src/persistence/jobs-store.ts +229 -0
- package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
- package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
- package/src/persistence/steps.ts +12 -0
- package/src/platform/sync-avatar.test.ts +29 -0
- package/src/plugin-api/model-profiles.ts +5 -3
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
- 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 +580 -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/memory-retrospective-skill-card.ts +5 -0
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- 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/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/providers/gemini/client.ts +17 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/jev/ask.test.ts +66 -0
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
- package/src/providers/malformed-tool-call.ts +55 -0
- package/src/providers/model-catalog.ts +257 -4
- package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +30 -0
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/retry.ts +68 -36
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/vellum-model-routing.test.ts +1 -0
- package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
- package/src/runtime/__tests__/question-resolution.test.ts +32 -0
- package/src/runtime/agent-wake.ts +9 -2
- package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
- package/src/runtime/auth/middleware.ts +15 -11
- package/src/runtime/middleware/auth.ts +7 -1
- package/src/runtime/pending-interactions.ts +8 -8
- package/src/runtime/question-resolution.ts +54 -8
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
- package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
- package/src/runtime/routes/conversation-list-routes.ts +16 -14
- package/src/runtime/routes/conversation-query-routes.ts +4 -0
- package/src/runtime/routes/conversation-routes.ts +4 -2
- package/src/runtime/routes/diagnostics-routes.ts +66 -229
- package/src/runtime/routes/global-search-routes.ts +22 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/plugins-routes.ts +5 -2
- package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
- package/src/runtime/sync/resource-sync-events.ts +11 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
- package/src/runtime/sync/worker-daemon-notify.ts +44 -0
- package/src/runtime/verification-outbound-actions.ts +2 -2
- package/src/skills/validate-input.ts +75 -11
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
- package/src/tools/ask-question/ask-question-tool.ts +12 -1
- 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/__tests__/web-search.test.ts +96 -0
- package/src/tools/network/web-fetch.ts +30 -9
- package/src/tools/network/web-search.ts +235 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/format-input-schema.ts +15 -0
- package/src/tools/skills/load.ts +15 -43
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/skills/skill-tool-factory.ts +2 -1
- package/src/tools/workflows/manage-workflows.ts +4 -2
- package/src/util/worker-pipe.test.ts +146 -0
- package/src/util/worker-pipe.ts +49 -0
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
|
@@ -27,9 +27,14 @@ import { LocalEmbeddingBackend } from "../embedding-local.js";
|
|
|
27
27
|
type Internals = any;
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* A
|
|
31
|
-
*
|
|
30
|
+
* A backend of its own for one test. Production gets the per-model instance
|
|
31
|
+
* from `forModel`; constructing directly keeps tests from sharing state.
|
|
32
32
|
*/
|
|
33
|
+
function newBackend(): Internals {
|
|
34
|
+
return new (LocalEmbeddingBackend as Internals)("test-model");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A stand-in for a Bun subprocess whose stdin `write` fails synchronously. */
|
|
33
38
|
function brokenPipeProc(pid = 4242) {
|
|
34
39
|
return {
|
|
35
40
|
pid,
|
|
@@ -49,6 +54,62 @@ function brokenPipeProc(pid = 4242) {
|
|
|
49
54
|
};
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
/**
|
|
58
|
+
* A stand-in for a Bun subprocess whose stdin write outruns the pipe buffer:
|
|
59
|
+
* `write` and `flush` hand back one pending promise, and killing the worker
|
|
60
|
+
* rejects it with EPIPE, the way Bun settles a pending write once the child
|
|
61
|
+
* is reaped.
|
|
62
|
+
*/
|
|
63
|
+
function pendingWriteProc(pid = 4244) {
|
|
64
|
+
const pending = Promise.withResolvers<number>();
|
|
65
|
+
return {
|
|
66
|
+
pid,
|
|
67
|
+
killed: false,
|
|
68
|
+
exited: Promise.resolve(0),
|
|
69
|
+
kill() {
|
|
70
|
+
this.killed = true;
|
|
71
|
+
pending.reject(
|
|
72
|
+
Object.assign(new Error("EPIPE: broken pipe, write"), {
|
|
73
|
+
code: "EPIPE",
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
stdin: {
|
|
78
|
+
write: () => pending.promise,
|
|
79
|
+
flush: () => pending.promise,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A subprocess whose stdout the test closes, to drive the reader loop that
|
|
86
|
+
* settles requests the pipe never reported a failure for.
|
|
87
|
+
*/
|
|
88
|
+
function stdoutProc(pid = 4245) {
|
|
89
|
+
let closeStdout: () => void = () => {};
|
|
90
|
+
const stdout = new ReadableStream<Uint8Array>({
|
|
91
|
+
start(controller) {
|
|
92
|
+
closeStdout = () => controller.close();
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
let resolveExit: (code: number) => void = () => {};
|
|
96
|
+
const exited = new Promise<number>((r) => {
|
|
97
|
+
resolveExit = r;
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
pid,
|
|
101
|
+
killed: false,
|
|
102
|
+
exited,
|
|
103
|
+
stdout,
|
|
104
|
+
closeStdout: () => closeStdout(),
|
|
105
|
+
kill() {
|
|
106
|
+
this.killed = true;
|
|
107
|
+
resolveExit(0);
|
|
108
|
+
},
|
|
109
|
+
stdin: { write: () => 0, flush: () => 0 },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
52
113
|
/** A subprocess that stays "alive" until killed, so exit can be observed. */
|
|
53
114
|
function liveProc(pid = 4243) {
|
|
54
115
|
let resolveExit: (code: number) => void = () => {};
|
|
@@ -196,7 +257,7 @@ describe("PID file ownership", () => {
|
|
|
196
257
|
*/
|
|
197
258
|
test("releasePidFile leaves an entry that names someone else's worker", () => {
|
|
198
259
|
writeFileSync(pidPath, "777");
|
|
199
|
-
const backend =
|
|
260
|
+
const backend = newBackend();
|
|
200
261
|
|
|
201
262
|
backend.releasePidFile(888);
|
|
202
263
|
|
|
@@ -206,7 +267,7 @@ describe("PID file ownership", () => {
|
|
|
206
267
|
|
|
207
268
|
test("releasePidFile drops the entry when it names our worker", () => {
|
|
208
269
|
writeFileSync(pidPath, "777");
|
|
209
|
-
const backend =
|
|
270
|
+
const backend = newBackend();
|
|
210
271
|
|
|
211
272
|
backend.releasePidFile(777);
|
|
212
273
|
|
|
@@ -219,7 +280,7 @@ describe("PID file ownership", () => {
|
|
|
219
280
|
*/
|
|
220
281
|
test("disposing an instance with no worker keeps another's PID entry", () => {
|
|
221
282
|
writeFileSync(pidPath, "777");
|
|
222
|
-
const backend =
|
|
283
|
+
const backend = newBackend();
|
|
223
284
|
backend.disposeRequested = true;
|
|
224
285
|
backend.workerProc = null;
|
|
225
286
|
|
|
@@ -232,12 +293,12 @@ describe("PID file ownership", () => {
|
|
|
232
293
|
|
|
233
294
|
describe("broken worker pipe", () => {
|
|
234
295
|
/**
|
|
235
|
-
* A
|
|
236
|
-
*
|
|
237
|
-
*
|
|
296
|
+
* A broken pipe comes back as an ordinary failed embed the backend chain can
|
|
297
|
+
* fall back from. An escaping rejection reaches the daemon's
|
|
298
|
+
* `unhandledRejection` handler, which shuts the process down.
|
|
238
299
|
*/
|
|
239
300
|
test("EPIPE on write resolves the request as an error", async () => {
|
|
240
|
-
const backend =
|
|
301
|
+
const backend = newBackend();
|
|
241
302
|
backend.workerProc = brokenPipeProc();
|
|
242
303
|
|
|
243
304
|
const response = await backend.sendRequest(["hello"]);
|
|
@@ -247,7 +308,7 @@ describe("broken worker pipe", () => {
|
|
|
247
308
|
});
|
|
248
309
|
|
|
249
310
|
test("EPIPE surfaces to embed() as a rejection, not a process crash", async () => {
|
|
250
|
-
const backend =
|
|
311
|
+
const backend = newBackend();
|
|
251
312
|
backend.workerProc = brokenPipeProc();
|
|
252
313
|
// Skip initialization: the worker is already (notionally) running.
|
|
253
314
|
backend.ensureInitialized = async () => {};
|
|
@@ -257,8 +318,49 @@ describe("broken worker pipe", () => {
|
|
|
257
318
|
);
|
|
258
319
|
});
|
|
259
320
|
|
|
321
|
+
/**
|
|
322
|
+
* A batch larger than the pipe buffer, sent to a worker too busy to drain
|
|
323
|
+
* stdin, leaves the write pending, and the pending write rejects after
|
|
324
|
+
* `sendRequest` has returned, once the worker dies. A synchronous guard
|
|
325
|
+
* never sees that rejection.
|
|
326
|
+
*/
|
|
327
|
+
test("a write still pending when the worker dies resolves the request as an error", async () => {
|
|
328
|
+
const backend = newBackend();
|
|
329
|
+
const proc = pendingWriteProc();
|
|
330
|
+
backend.workerProc = proc;
|
|
331
|
+
|
|
332
|
+
const request = backend.sendRequest(["hello"]);
|
|
333
|
+
proc.kill();
|
|
334
|
+
const response = await request;
|
|
335
|
+
|
|
336
|
+
expect(response.error).toContain("worker pipe write failed");
|
|
337
|
+
expect(backend.pendingRequests.size).toBe(0);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Bun does not always report a dead worker through the write: on Linux it
|
|
342
|
+
* occasionally resolves the pending write with a short count instead of
|
|
343
|
+
* rejecting it, and `writeWorkerLine` then stays silent. Nothing times a
|
|
344
|
+
* request out, so the stdout reader ending is the only thing that settles
|
|
345
|
+
* it.
|
|
346
|
+
*/
|
|
347
|
+
test("stdout ending settles a request the pipe never reported a failure for", async () => {
|
|
348
|
+
const backend = newBackend();
|
|
349
|
+
const proc = stdoutProc();
|
|
350
|
+
backend.workerProc = proc;
|
|
351
|
+
backend.startStdoutReader();
|
|
352
|
+
|
|
353
|
+
const inFlight = backend.sendRequest(["hello"]);
|
|
354
|
+
proc.closeStdout();
|
|
355
|
+
|
|
356
|
+
await expect(inFlight).resolves.toMatchObject({
|
|
357
|
+
error: expect.stringContaining("exited unexpectedly"),
|
|
358
|
+
});
|
|
359
|
+
expect(backend.pendingRequests.size).toBe(0);
|
|
360
|
+
});
|
|
361
|
+
|
|
260
362
|
test("a failed write does not leak the pending request", async () => {
|
|
261
|
-
const backend =
|
|
363
|
+
const backend = newBackend();
|
|
262
364
|
backend.workerProc = brokenPipeProc();
|
|
263
365
|
|
|
264
366
|
await backend.sendRequest(["hello"]);
|
|
@@ -274,7 +376,7 @@ describe("releasing ownership of a worker", () => {
|
|
|
274
376
|
* strands it: no handle, no PID entry, invisible to every later reclaim.
|
|
275
377
|
*/
|
|
276
378
|
test("stdout ending kills the child before clearing workerProc", async () => {
|
|
277
|
-
const backend =
|
|
379
|
+
const backend = newBackend();
|
|
278
380
|
const proc = liveProc();
|
|
279
381
|
backend.workerProc = proc;
|
|
280
382
|
|
|
@@ -285,7 +387,7 @@ describe("releasing ownership of a worker", () => {
|
|
|
285
387
|
});
|
|
286
388
|
|
|
287
389
|
test("shutdown reaps the worker and resolves only once it exits", async () => {
|
|
288
|
-
const backend =
|
|
390
|
+
const backend = newBackend();
|
|
289
391
|
const proc = liveProc(555);
|
|
290
392
|
backend.workerProc = proc;
|
|
291
393
|
writeFileSync(getEmbedWorkerPidPath(), "555");
|
|
@@ -298,7 +400,7 @@ describe("releasing ownership of a worker", () => {
|
|
|
298
400
|
});
|
|
299
401
|
|
|
300
402
|
test("shutdown settles in-flight requests instead of hanging them", async () => {
|
|
301
|
-
const backend =
|
|
403
|
+
const backend = newBackend();
|
|
302
404
|
backend.workerProc = liveProc(556);
|
|
303
405
|
|
|
304
406
|
const inFlight = backend.sendRequest(["hello"]);
|
|
@@ -315,7 +417,7 @@ describe("releasing ownership of a worker", () => {
|
|
|
315
417
|
* pending requests are only settled after the wait.
|
|
316
418
|
*/
|
|
317
419
|
test("a worker that ignores SIGTERM is escalated to SIGKILL, not waited on forever", async () => {
|
|
318
|
-
const backend =
|
|
420
|
+
const backend = newBackend();
|
|
319
421
|
backend.terminateGraceMs = 50;
|
|
320
422
|
const signals: (string | undefined)[] = [];
|
|
321
423
|
const wedged = {
|
|
@@ -354,7 +456,7 @@ describe("termination that cannot be confirmed", () => {
|
|
|
354
456
|
}
|
|
355
457
|
|
|
356
458
|
test("keeps the PID publication instead of unpublishing a possibly-live child", async () => {
|
|
357
|
-
const backend =
|
|
459
|
+
const backend = newBackend();
|
|
358
460
|
backend.terminateGraceMs = 20;
|
|
359
461
|
const proc = wedgedProc();
|
|
360
462
|
backend.workerProc = proc;
|
|
@@ -370,7 +472,7 @@ describe("termination that cannot be confirmed", () => {
|
|
|
370
472
|
});
|
|
371
473
|
|
|
372
474
|
test("refuses to start a replacement while the child may still be alive", async () => {
|
|
373
|
-
const backend =
|
|
475
|
+
const backend = newBackend();
|
|
374
476
|
// No workerPath recorded, so liveness falls back to a PID probe. Our own
|
|
375
477
|
// PID is certainly alive, standing in for a child that will not die.
|
|
376
478
|
backend.unconfirmedWorker = process.pid;
|
|
@@ -381,7 +483,7 @@ describe("termination that cannot be confirmed", () => {
|
|
|
381
483
|
});
|
|
382
484
|
|
|
383
485
|
test("surfaces the incomplete teardown through embed() rather than failing silently", async () => {
|
|
384
|
-
const backend =
|
|
486
|
+
const backend = newBackend();
|
|
385
487
|
backend.unconfirmedWorker = process.pid;
|
|
386
488
|
|
|
387
489
|
await expect(backend.embed(["hello"])).rejects.toThrow(
|
|
@@ -401,7 +503,7 @@ describe("termination that cannot be confirmed", () => {
|
|
|
401
503
|
});
|
|
402
504
|
await Bun.sleep(400);
|
|
403
505
|
|
|
404
|
-
const backend =
|
|
506
|
+
const backend = newBackend();
|
|
405
507
|
backend.workerPath = scriptPath;
|
|
406
508
|
backend.unconfirmedWorker = proc.pid;
|
|
407
509
|
writeFileSync(getEmbedWorkerPidPath(), String(proc.pid));
|
|
@@ -440,7 +542,7 @@ describe("reclaiming before spawning a replacement", () => {
|
|
|
440
542
|
await Bun.sleep(400);
|
|
441
543
|
writeFileSync(getEmbedWorkerPidPath(), String(proc.pid));
|
|
442
544
|
|
|
443
|
-
const backend =
|
|
545
|
+
const backend = newBackend();
|
|
444
546
|
await backend.reclaimOwnedWorkers(scriptPath);
|
|
445
547
|
|
|
446
548
|
// Confirmed gone by the time reclaim returns, not merely signalled.
|
|
@@ -464,7 +566,7 @@ describe("reclaiming before spawning a replacement", () => {
|
|
|
464
566
|
spawned.push(proc);
|
|
465
567
|
await Bun.sleep(400);
|
|
466
568
|
|
|
467
|
-
const backend =
|
|
569
|
+
const backend = newBackend();
|
|
468
570
|
backend.terminateGraceMs = 300;
|
|
469
571
|
await backend.reclaimOwnedWorkers(scriptPath);
|
|
470
572
|
|
|
@@ -497,7 +599,7 @@ setTimeout(() => {}, 60_000);\n`,
|
|
|
497
599
|
expect(workers.length).toBe(1);
|
|
498
600
|
expect(workers[0].ppid).toBe(parent.pid);
|
|
499
601
|
|
|
500
|
-
const backend =
|
|
602
|
+
const backend = newBackend();
|
|
501
603
|
backend.terminateGraceMs = 300;
|
|
502
604
|
await backend.reclaimOwnedWorkers(scriptPath);
|
|
503
605
|
|
|
@@ -521,7 +623,7 @@ describe("shutdown versus in-flight initialization", () => {
|
|
|
521
623
|
* lets the initializer spawn a worker afterwards, with nobody left to reap it.
|
|
522
624
|
*/
|
|
523
625
|
test("shutdown waits for an in-flight init and reaps the worker it creates", async () => {
|
|
524
|
-
const backend =
|
|
626
|
+
const backend = newBackend();
|
|
525
627
|
const proc = liveProc(600);
|
|
526
628
|
|
|
527
629
|
let finishInit: () => void = () => {};
|
|
@@ -564,7 +666,7 @@ describe("parent exit orderings", () => {
|
|
|
564
666
|
* process's and left it alone. So it kills its own child on the way out.
|
|
565
667
|
*/
|
|
566
668
|
test("terminateNow kills the worker and releases the entry without waiting", () => {
|
|
567
|
-
const backend =
|
|
669
|
+
const backend = newBackend();
|
|
568
670
|
const signals: string[] = [];
|
|
569
671
|
const proc = {
|
|
570
672
|
pid: 909,
|
|
@@ -590,7 +692,7 @@ describe("parent exit orderings", () => {
|
|
|
590
692
|
* process while the child was still being adopted.
|
|
591
693
|
*/
|
|
592
694
|
test("shutdown does not inherit the model-load wait when init never settles", async () => {
|
|
593
|
-
const backend =
|
|
695
|
+
const backend = newBackend();
|
|
594
696
|
backend.terminateGraceMs = 50;
|
|
595
697
|
// An initialization that never resolves, as during a cold model load.
|
|
596
698
|
backend.initInFlight = new Promise<void>(() => {});
|
|
@@ -602,7 +704,7 @@ describe("parent exit orderings", () => {
|
|
|
602
704
|
});
|
|
603
705
|
|
|
604
706
|
test("a backend already shutting down refuses to spawn a replacement", async () => {
|
|
605
|
-
const backend =
|
|
707
|
+
const backend = newBackend();
|
|
606
708
|
backend.disposeRequested = true;
|
|
607
709
|
|
|
608
710
|
await expect(backend.embed(["hello"])).rejects.toThrow(/shutting down/);
|
|
@@ -621,7 +723,7 @@ describe("transient failures stay transient", () => {
|
|
|
621
723
|
const PERMANENT_FAILURE_SENTINEL = "Local embedding backend unavailable";
|
|
622
724
|
|
|
623
725
|
test("an unconfirmed termination does not claim permanent unavailability", async () => {
|
|
624
|
-
const backend =
|
|
726
|
+
const backend = newBackend();
|
|
625
727
|
backend.unconfirmedWorker = process.pid;
|
|
626
728
|
|
|
627
729
|
const err = await backend.ensureInitialized().catch((e: Error) => e);
|
|
@@ -631,7 +733,7 @@ describe("transient failures stay transient", () => {
|
|
|
631
733
|
});
|
|
632
734
|
|
|
633
735
|
test("shutting down does not claim permanent unavailability", async () => {
|
|
634
|
-
const backend =
|
|
736
|
+
const backend = newBackend();
|
|
635
737
|
backend.disposeRequested = true;
|
|
636
738
|
|
|
637
739
|
const err = await backend.embed(["hello"]).catch((e: Error) => e);
|
|
@@ -652,7 +754,7 @@ describe("audit regressions", () => {
|
|
|
652
754
|
* embed queued behind it.
|
|
653
755
|
*/
|
|
654
756
|
test("a startup failure against a wedged worker does not hang", async () => {
|
|
655
|
-
const backend =
|
|
757
|
+
const backend = newBackend();
|
|
656
758
|
backend.terminateGraceMs = 30;
|
|
657
759
|
const signals: (string | undefined)[] = [];
|
|
658
760
|
backend.workerProc = {
|
|
@@ -680,7 +782,7 @@ describe("audit regressions", () => {
|
|
|
680
782
|
* startup for the full two-minute timeout.
|
|
681
783
|
*/
|
|
682
784
|
test("terminateNow clears the stdout buffer so a retry can parse ready", () => {
|
|
683
|
-
const backend =
|
|
785
|
+
const backend = newBackend();
|
|
684
786
|
backend.stdoutBuffer = '{"id":5,"vect';
|
|
685
787
|
backend.workerProc = null;
|
|
686
788
|
|
|
@@ -707,7 +809,7 @@ describe("audit regressions", () => {
|
|
|
707
809
|
spawned.push(proc);
|
|
708
810
|
await Bun.sleep(400);
|
|
709
811
|
|
|
710
|
-
const backend =
|
|
812
|
+
const backend = newBackend();
|
|
711
813
|
backend.workerPath = scriptPath;
|
|
712
814
|
backend.workerProc = null; // spawned, never adopted
|
|
713
815
|
|
|
@@ -722,7 +824,7 @@ describe("audit regressions", () => {
|
|
|
722
824
|
* contract, so it must leave the instance able to start a replacement.
|
|
723
825
|
*/
|
|
724
826
|
test("terminateNow leaves the instance reusable", () => {
|
|
725
|
-
const backend =
|
|
827
|
+
const backend = newBackend();
|
|
726
828
|
backend.workerProc = null;
|
|
727
829
|
backend.stdoutReaderActive = true;
|
|
728
830
|
|
|
@@ -766,3 +868,97 @@ test("orphan reclaim outlasts the embedding reap it must not truncate", () => {
|
|
|
766
868
|
EMBEDDING_SHUTDOWN_BUDGET_MS + 1_000,
|
|
767
869
|
);
|
|
768
870
|
});
|
|
871
|
+
|
|
872
|
+
describe("one backend per model", () => {
|
|
873
|
+
test("forModel hands out one instance per model", () => {
|
|
874
|
+
const first = LocalEmbeddingBackend.forModel("per-model-a");
|
|
875
|
+
|
|
876
|
+
expect(LocalEmbeddingBackend.forModel("per-model-a")).toBe(first);
|
|
877
|
+
expect(LocalEmbeddingBackend.forModel("per-model-b")).not.toBe(first);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* A backend-cache reset disposes the backend it evicts, and that disposal
|
|
882
|
+
* waits for in-flight embeds. The next embed needs the model again inside
|
|
883
|
+
* that window, and the embeds in flight keep their worker.
|
|
884
|
+
*/
|
|
885
|
+
test("needing the model again while a disposal is pending keeps the busy worker", () => {
|
|
886
|
+
const backend = LocalEmbeddingBackend.forModel(
|
|
887
|
+
"per-model-reopen",
|
|
888
|
+
) as Internals;
|
|
889
|
+
const proc = liveProc(7101);
|
|
890
|
+
backend.workerProc = proc;
|
|
891
|
+
backend.activeEmbeds = 1;
|
|
892
|
+
backend.dispose();
|
|
893
|
+
|
|
894
|
+
const again = LocalEmbeddingBackend.forModel(
|
|
895
|
+
"per-model-reopen",
|
|
896
|
+
) as Internals;
|
|
897
|
+
backend.activeEmbeds = 0;
|
|
898
|
+
backend.disposeIfIdle();
|
|
899
|
+
|
|
900
|
+
expect(again).toBe(backend);
|
|
901
|
+
expect(again.workerProc).toBe(proc);
|
|
902
|
+
expect(proc.killed).toBe(false);
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
test("a pending disposal nobody withdraws still releases the worker once idle", () => {
|
|
906
|
+
const backend = LocalEmbeddingBackend.forModel(
|
|
907
|
+
"per-model-dispose",
|
|
908
|
+
) as Internals;
|
|
909
|
+
const proc = liveProc(7102);
|
|
910
|
+
backend.workerProc = proc;
|
|
911
|
+
backend.activeEmbeds = 1;
|
|
912
|
+
backend.dispose();
|
|
913
|
+
|
|
914
|
+
backend.activeEmbeds = 0;
|
|
915
|
+
backend.disposeIfIdle();
|
|
916
|
+
|
|
917
|
+
expect(backend.workerProc).toBeNull();
|
|
918
|
+
expect(proc.killed).toBe(true);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
/** The process is exiting, so nothing may spawn a worker behind the reap. */
|
|
922
|
+
test("a shut-down backend stays shut down", async () => {
|
|
923
|
+
const backend = LocalEmbeddingBackend.forModel("per-model-shutdown");
|
|
924
|
+
await backend.shutdown();
|
|
925
|
+
|
|
926
|
+
const again = LocalEmbeddingBackend.forModel("per-model-shutdown");
|
|
927
|
+
|
|
928
|
+
expect(again).toBe(backend);
|
|
929
|
+
await expect(again.embed(["hello"])).rejects.toThrow(/shutting down/);
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* The backend cache forgets a backend whose disposal is pending, so process
|
|
934
|
+
* teardown reaches it through the class's own list.
|
|
935
|
+
*/
|
|
936
|
+
test("shutdownAll reaps a busy backend no cache still holds", async () => {
|
|
937
|
+
const backend = LocalEmbeddingBackend.forModel(
|
|
938
|
+
"per-model-evicted",
|
|
939
|
+
) as Internals;
|
|
940
|
+
const proc = liveProc(7103);
|
|
941
|
+
backend.workerProc = proc;
|
|
942
|
+
backend.activeEmbeds = 1;
|
|
943
|
+
backend.dispose();
|
|
944
|
+
|
|
945
|
+
await LocalEmbeddingBackend.shutdownAll();
|
|
946
|
+
|
|
947
|
+
expect(proc.killed).toBe(true);
|
|
948
|
+
expect(backend.workerProc).toBeNull();
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
test("terminateAllNow kills a busy backend no cache still holds", () => {
|
|
952
|
+
const backend = LocalEmbeddingBackend.forModel(
|
|
953
|
+
"per-model-evicted-now",
|
|
954
|
+
) as Internals;
|
|
955
|
+
const proc = liveProc(7104);
|
|
956
|
+
backend.workerProc = proc;
|
|
957
|
+
backend.activeEmbeds = 1;
|
|
958
|
+
backend.dispose();
|
|
959
|
+
|
|
960
|
+
LocalEmbeddingBackend.terminateAllNow();
|
|
961
|
+
|
|
962
|
+
expect(proc.killed).toBe(true);
|
|
963
|
+
});
|
|
964
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process teardown reaches every local embedding worker the process owns.
|
|
3
|
+
*
|
|
4
|
+
* Kept in its own file: `shutdownEmbeddingBackends` latches the module into its
|
|
5
|
+
* shut-down state for the rest of the process.
|
|
6
|
+
*/
|
|
7
|
+
import { expect, test } from "bun:test";
|
|
8
|
+
|
|
9
|
+
import { AssistantConfigSchema } from "../../config/schema.js";
|
|
10
|
+
import {
|
|
11
|
+
clearEmbeddingBackendCache,
|
|
12
|
+
selectEmbeddingBackend,
|
|
13
|
+
shutdownEmbeddingBackends,
|
|
14
|
+
} from "./embedding-backend.js";
|
|
15
|
+
|
|
16
|
+
/** Reach past `private`, which is compile-time only, so tests drive real state. */
|
|
17
|
+
type Internals = any;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A backend-cache reset forgets a backend whose disposal is waiting on
|
|
21
|
+
* in-flight embeds, while that backend still holds a live worker.
|
|
22
|
+
*/
|
|
23
|
+
test("shutdown reaps a busy local worker the cache has already forgotten", async () => {
|
|
24
|
+
const config = AssistantConfigSchema.parse({
|
|
25
|
+
memory: { embeddings: { provider: "local" } },
|
|
26
|
+
});
|
|
27
|
+
const { backend } = await selectEmbeddingBackend(config);
|
|
28
|
+
const delegate = await (backend as Internals).getDelegate();
|
|
29
|
+
let killed = false;
|
|
30
|
+
delegate.workerProc = {
|
|
31
|
+
pid: 7201,
|
|
32
|
+
exited: new Promise<number>(() => {}),
|
|
33
|
+
kill() {
|
|
34
|
+
killed = true;
|
|
35
|
+
},
|
|
36
|
+
stdin: { write: () => 0, flush: () => 0 },
|
|
37
|
+
};
|
|
38
|
+
delegate.terminateGraceMs = 10;
|
|
39
|
+
delegate.activeEmbeds = 1;
|
|
40
|
+
|
|
41
|
+
clearEmbeddingBackendCache();
|
|
42
|
+
expect(killed).toBe(false);
|
|
43
|
+
|
|
44
|
+
await shutdownEmbeddingBackends();
|
|
45
|
+
|
|
46
|
+
expect(killed).toBe(true);
|
|
47
|
+
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
recordBillingSuccess,
|
|
17
17
|
} from "./embedding-billing-breaker.js";
|
|
18
18
|
import { GeminiEmbeddingBackend } from "./embedding-gemini.js";
|
|
19
|
+
import type { LocalEmbeddingBackend } from "./embedding-local.js";
|
|
19
20
|
import { OllamaEmbeddingBackend } from "./embedding-ollama.js";
|
|
20
21
|
import {
|
|
21
22
|
OpenAIEmbeddingBackend,
|
|
@@ -78,6 +79,13 @@ export const EMBEDDING_SHUTDOWN_BUDGET_MS = 8_000;
|
|
|
78
79
|
* the failure is contained and other embedding backends can be used instead.
|
|
79
80
|
*/
|
|
80
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The local backend class, once {@link LazyLocalEmbeddingBackend} has loaded it.
|
|
84
|
+
* It lists every local backend this process owns, which process teardown needs
|
|
85
|
+
* and {@link backendCache} cannot supply.
|
|
86
|
+
*/
|
|
87
|
+
let localEmbeddingBackends: typeof LocalEmbeddingBackend | null = null;
|
|
88
|
+
|
|
81
89
|
class LazyLocalEmbeddingBackend implements EmbeddingBackend {
|
|
82
90
|
readonly provider = "local" as const;
|
|
83
91
|
readonly model: string;
|
|
@@ -114,21 +122,9 @@ class LazyLocalEmbeddingBackend implements EmbeddingBackend {
|
|
|
114
122
|
this.delegate?.dispose?.();
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
async sweepOwnedWorkers(): Promise<void> {
|
|
122
|
-
await this.delegate?.sweepOwnedWorkers?.();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async shutdown(): Promise<void> {
|
|
126
|
-
// A delegate under construction still ends up owning a worker, so settle
|
|
127
|
-
// the in-flight import before tearing down rather than skipping it.
|
|
128
|
-
if (!this.delegate && this.initPromise) {
|
|
129
|
-
await this.initPromise.catch(() => undefined);
|
|
130
|
-
}
|
|
131
|
-
await this.delegate?.shutdown?.();
|
|
125
|
+
/** Settles once a delegate under construction has been handed out or failed. */
|
|
126
|
+
async loaded(): Promise<void> {
|
|
127
|
+
await this.initPromise?.catch(() => undefined);
|
|
132
128
|
}
|
|
133
129
|
|
|
134
130
|
resetForRetry(): void {
|
|
@@ -146,7 +142,8 @@ class LazyLocalEmbeddingBackend implements EmbeddingBackend {
|
|
|
146
142
|
try {
|
|
147
143
|
const { LocalEmbeddingBackend } =
|
|
148
144
|
await import("./embedding-local.js");
|
|
149
|
-
|
|
145
|
+
localEmbeddingBackends = LocalEmbeddingBackend;
|
|
146
|
+
this.delegate = LocalEmbeddingBackend.forModel(this.model);
|
|
150
147
|
return this.delegate;
|
|
151
148
|
} catch (err) {
|
|
152
149
|
localBackendBroken = true;
|
|
@@ -270,7 +267,7 @@ export function clearEmbeddingBackendCache(): void {
|
|
|
270
267
|
}
|
|
271
268
|
|
|
272
269
|
/**
|
|
273
|
-
* Tear down every
|
|
270
|
+
* Tear down every local embedding worker this process owns and empty the caches.
|
|
274
271
|
*
|
|
275
272
|
* Called on daemon shutdown so process-owned embedding workers exit with their
|
|
276
273
|
* owner instead of being orphaned. `clearEmbeddingBackendCache()` is the
|
|
@@ -279,24 +276,18 @@ export function clearEmbeddingBackendCache(): void {
|
|
|
279
276
|
*/
|
|
280
277
|
export async function shutdownEmbeddingBackends(): Promise<void> {
|
|
281
278
|
embeddingBackendsShutDown = true;
|
|
282
|
-
const
|
|
279
|
+
const loading = [...backendCache.values()].filter(
|
|
280
|
+
(backend) => backend instanceof LazyLocalEmbeddingBackend,
|
|
281
|
+
);
|
|
283
282
|
backendCache.clear();
|
|
284
283
|
vectorCache.clear();
|
|
285
284
|
vectorCacheBytes = 0;
|
|
286
285
|
backendDimCache.clear();
|
|
287
286
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
await backend.sweepOwnedWorkers?.();
|
|
293
|
-
} catch (err) {
|
|
294
|
-
log.warn(
|
|
295
|
-
{ err, provider: backend.provider, model: backend.model },
|
|
296
|
-
"Failed to shut down embedding backend",
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
}),
|
|
287
|
+
// A delegate under construction still ends up owning a worker, so let it
|
|
288
|
+
// join the class's list before tearing that list down.
|
|
289
|
+
const teardown = Promise.all(loading.map((backend) => backend.loaded())).then(
|
|
290
|
+
() => localEmbeddingBackends?.shutdownAll(),
|
|
300
291
|
);
|
|
301
292
|
|
|
302
293
|
const timedOut = Symbol("timeout");
|
|
@@ -313,7 +304,7 @@ export async function shutdownEmbeddingBackends(): Promise<void> {
|
|
|
313
304
|
}
|
|
314
305
|
|
|
315
306
|
/**
|
|
316
|
-
* SIGKILL every
|
|
307
|
+
* SIGKILL every local embedding worker this process owns, synchronously.
|
|
317
308
|
*
|
|
318
309
|
* For a process that must exit immediately and cannot run the graceful
|
|
319
310
|
* teardown: the memory worker on PID-file eviction, where staying alive to reap
|
|
@@ -322,16 +313,7 @@ export async function shutdownEmbeddingBackends(): Promise<void> {
|
|
|
322
313
|
* sweep has passed, leaving two workers alive (JARVIS-1125).
|
|
323
314
|
*/
|
|
324
315
|
export function terminateEmbeddingWorkersNow(): void {
|
|
325
|
-
|
|
326
|
-
try {
|
|
327
|
-
backend.terminateNow?.();
|
|
328
|
-
} catch (err) {
|
|
329
|
-
log.warn(
|
|
330
|
-
{ err, provider: backend.provider, model: backend.model },
|
|
331
|
-
"Failed to terminate embedding worker",
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
316
|
+
localEmbeddingBackends?.terminateAllNow();
|
|
335
317
|
}
|
|
336
318
|
|
|
337
319
|
/** Reset the sticky local-backend failure flag without evicting live backends. */
|
|
@@ -439,6 +439,23 @@ describe("GeminiEmbeddingBackend: batched text inputs", () => {
|
|
|
439
439
|
expect(vectors.map((v) => v[0])).toEqual(texts(250).map(textIndex));
|
|
440
440
|
});
|
|
441
441
|
|
|
442
|
+
test("managed text inputs use single calls from the first request and preserve vector order", async () => {
|
|
443
|
+
const fetchMock = routedFetch();
|
|
444
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
445
|
+
const backend = new GeminiEmbeddingBackend("test-key", "test-model", {
|
|
446
|
+
managedBaseUrl: "https://proxy.example.com/v1/runtime-proxy/gemini",
|
|
447
|
+
interCallDelayMs: 0,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
expect(await backend.embed(texts(3))).toEqual([[0], [1], [2]]);
|
|
451
|
+
expect(await backend.embed(["t4", "t5"])).toEqual([[4], [5]]);
|
|
452
|
+
expect(calledUrls(fetchMock)).toEqual(
|
|
453
|
+
Array(5).fill(
|
|
454
|
+
"https://proxy.example.com/v1/runtime-proxy/gemini/v1beta/models/test-model:embedContent",
|
|
455
|
+
),
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
|
|
442
459
|
test("each batched request names the model with the models/ prefix and carries taskType and outputDimensionality", async () => {
|
|
443
460
|
const fetchMock = routedFetch();
|
|
444
461
|
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
@@ -71,7 +71,8 @@ export class GeminiEmbeddingBackend implements EmbeddingBackend {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* Embed `inputs` in order.
|
|
74
|
+
* Embed `inputs` in order. Managed requests use single `embedContent`
|
|
75
|
+
* calls because Vertex does not expose the batch route. Direct text runs use
|
|
75
76
|
* `batchEmbedContents`, {@link GEMINI_EMBED_BATCH_SIZE} texts per round
|
|
76
77
|
* trip, so a corpus re-embed costs one request per hundred sections rather
|
|
77
78
|
* than one per section; a lone text and every multimodal input take the
|
|
@@ -99,6 +100,7 @@ export class GeminiEmbeddingBackend implements EmbeddingBackend {
|
|
|
99
100
|
// Gather the run of text inputs starting here, up to one batch.
|
|
100
101
|
let end = i;
|
|
101
102
|
while (
|
|
103
|
+
!this.managed &&
|
|
102
104
|
!this.batchRouteUnavailable &&
|
|
103
105
|
end < normalized.length &&
|
|
104
106
|
end - i < GEMINI_EMBED_BATCH_SIZE &&
|