@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
|
@@ -3,15 +3,18 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
3
3
|
import type { AssistantConfig } from "../../config/types.js";
|
|
4
4
|
import {
|
|
5
5
|
clearEmbeddingBackendCache,
|
|
6
|
+
customEmbeddingSpaceIdentity,
|
|
6
7
|
embedWithBackend,
|
|
7
8
|
isEmbeddingDimensionAvailable,
|
|
8
9
|
resetLocalEmbeddingFailureState,
|
|
10
|
+
resolveBackendDimension,
|
|
9
11
|
selectEmbeddingBackend,
|
|
10
12
|
} from "./embedding-backend.js";
|
|
11
13
|
import {
|
|
12
14
|
_resetEmbeddingBillingBreaker,
|
|
13
15
|
recordBillingBlock,
|
|
14
16
|
} from "./embedding-billing-breaker.js";
|
|
17
|
+
import { OpenAIEmbeddingBackend } from "./embedding-openai.js";
|
|
15
18
|
|
|
16
19
|
const getProviderKeyAsyncMock = mock(
|
|
17
20
|
async (_provider: string): Promise<string | undefined> => undefined,
|
|
@@ -361,3 +364,162 @@ describe("managed-proxy Gemini fallback to direct key", () => {
|
|
|
361
364
|
expect(directUrl).toContain("key=direct-key");
|
|
362
365
|
});
|
|
363
366
|
});
|
|
367
|
+
|
|
368
|
+
function customConfig(overrides: {
|
|
369
|
+
baseUrl?: string;
|
|
370
|
+
customModel?: string;
|
|
371
|
+
customDimensions?: number;
|
|
372
|
+
}): AssistantConfig {
|
|
373
|
+
return {
|
|
374
|
+
memory: {
|
|
375
|
+
embeddings: {
|
|
376
|
+
provider: "custom",
|
|
377
|
+
customModel: overrides.customModel ?? "text-embedding-3-small",
|
|
378
|
+
baseUrl: overrides.baseUrl,
|
|
379
|
+
customDimensions: overrides.customDimensions,
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
} as unknown as AssistantConfig;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
describe("custom OpenAI-compatible embedding backend selection", () => {
|
|
386
|
+
afterEach(() => {
|
|
387
|
+
clearEmbeddingBackendCache();
|
|
388
|
+
getProviderKeyAsyncMock.mockReset();
|
|
389
|
+
getProviderKeyAsyncMock.mockImplementation(async () => undefined);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("selects a custom backend with a trimmed baseUrl", async () => {
|
|
393
|
+
getProviderKeyAsyncMock.mockImplementation(async (provider) =>
|
|
394
|
+
provider === "custom" ? "gw-key" : undefined,
|
|
395
|
+
);
|
|
396
|
+
const { backend, reason } = await selectEmbeddingBackend(
|
|
397
|
+
customConfig({
|
|
398
|
+
baseUrl: "http://127.0.0.1:4000/v1/",
|
|
399
|
+
customModel: "embed-mistral",
|
|
400
|
+
customDimensions: 1024,
|
|
401
|
+
}),
|
|
402
|
+
);
|
|
403
|
+
expect(reason).toBeNull();
|
|
404
|
+
expect(backend).toBeInstanceOf(OpenAIEmbeddingBackend);
|
|
405
|
+
expect(backend?.provider).toBe("custom");
|
|
406
|
+
expect(backend?.model).toBe("embed-mistral");
|
|
407
|
+
expect((backend as OpenAIEmbeddingBackend).baseURL).toBe(
|
|
408
|
+
"http://127.0.0.1:4000/v1",
|
|
409
|
+
);
|
|
410
|
+
expect((backend as OpenAIEmbeddingBackend).dimensions).toBe(1024);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("selects a custom backend without an API key", async () => {
|
|
414
|
+
const { backend, reason } = await selectEmbeddingBackend(
|
|
415
|
+
customConfig({ baseUrl: "http://127.0.0.1:1234/v1" }),
|
|
416
|
+
);
|
|
417
|
+
expect(reason).toBeNull();
|
|
418
|
+
expect(backend?.provider).toBe("custom");
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("is not configured when baseUrl is missing", async () => {
|
|
422
|
+
const { backend, reason } = await selectEmbeddingBackend(customConfig({}));
|
|
423
|
+
expect(backend).toBeNull();
|
|
424
|
+
expect(reason).toMatch(/baseUrl/);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("is not configured when baseUrl is not http(s)", async () => {
|
|
428
|
+
const { backend, reason } = await selectEmbeddingBackend(
|
|
429
|
+
customConfig({ baseUrl: "not-a-url" }),
|
|
430
|
+
);
|
|
431
|
+
expect(backend).toBeNull();
|
|
432
|
+
expect(reason).toMatch(/baseUrl/);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("is not selected by auto even when baseUrl is set", async () => {
|
|
436
|
+
const { backend } = await selectEmbeddingBackend({
|
|
437
|
+
memory: {
|
|
438
|
+
embeddings: {
|
|
439
|
+
provider: "auto",
|
|
440
|
+
localModel: "BAAI/bge-small-en-v1.5",
|
|
441
|
+
baseUrl: "http://127.0.0.1:4000/v1",
|
|
442
|
+
customModel: "embed-mistral",
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
} as unknown as AssistantConfig);
|
|
446
|
+
expect(backend?.provider).toBe("local");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("caches custom backends by baseUrl", async () => {
|
|
450
|
+
const first = await selectEmbeddingBackend(
|
|
451
|
+
customConfig({ baseUrl: "http://127.0.0.1:4000/v1" }),
|
|
452
|
+
);
|
|
453
|
+
const second = await selectEmbeddingBackend(
|
|
454
|
+
customConfig({ baseUrl: "http://127.0.0.1:4001/v1" }),
|
|
455
|
+
);
|
|
456
|
+
const firstAgain = await selectEmbeddingBackend(
|
|
457
|
+
customConfig({ baseUrl: "http://127.0.0.1:4000/v1" }),
|
|
458
|
+
);
|
|
459
|
+
expect(first.backend).not.toBe(second.backend);
|
|
460
|
+
expect(firstAgain.backend).toBe(first.backend);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("caches custom backends by requested dimensions", async () => {
|
|
464
|
+
const first = await selectEmbeddingBackend(
|
|
465
|
+
customConfig({
|
|
466
|
+
baseUrl: "http://127.0.0.1:4000/v1",
|
|
467
|
+
customDimensions: 1024,
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
const second = await selectEmbeddingBackend(
|
|
471
|
+
customConfig({
|
|
472
|
+
baseUrl: "http://127.0.0.1:4000/v1",
|
|
473
|
+
customDimensions: 1536,
|
|
474
|
+
}),
|
|
475
|
+
);
|
|
476
|
+
expect(first.backend).not.toBe(second.backend);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("dimension probes for custom backends are keyed by baseUrl", async () => {
|
|
480
|
+
const { backend: first } = await selectEmbeddingBackend(
|
|
481
|
+
customConfig({ baseUrl: "http://127.0.0.1:4000/v1" }),
|
|
482
|
+
);
|
|
483
|
+
const { backend: second } = await selectEmbeddingBackend(
|
|
484
|
+
customConfig({ baseUrl: "http://127.0.0.1:4001/v1" }),
|
|
485
|
+
);
|
|
486
|
+
if (!first || !second) {
|
|
487
|
+
throw new Error("expected custom backends");
|
|
488
|
+
}
|
|
489
|
+
(first as { embed: typeof first.embed }).embed = mock(async () => [
|
|
490
|
+
new Array(1536).fill(0),
|
|
491
|
+
]) as unknown as typeof first.embed;
|
|
492
|
+
(second as { embed: typeof second.embed }).embed = mock(async () => [
|
|
493
|
+
new Array(1024).fill(0),
|
|
494
|
+
]) as unknown as typeof second.embed;
|
|
495
|
+
|
|
496
|
+
expect(await resolveBackendDimension(first)).toBe(1536);
|
|
497
|
+
expect(await resolveBackendDimension(second)).toBe(1024);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("customEmbeddingSpaceIdentity includes model, url, and dimensions", () => {
|
|
501
|
+
expect(
|
|
502
|
+
customEmbeddingSpaceIdentity(
|
|
503
|
+
customConfig({
|
|
504
|
+
baseUrl: "http://127.0.0.1:4000/v1/",
|
|
505
|
+
customModel: "embed-mistral",
|
|
506
|
+
customDimensions: 1024,
|
|
507
|
+
}),
|
|
508
|
+
),
|
|
509
|
+
).toBe("custom\0embed-mistral\0url=http://127.0.0.1:4000/v1\0dim=1024");
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("customEmbeddingSpaceIdentity is null when the provider is not custom", () => {
|
|
513
|
+
expect(
|
|
514
|
+
customEmbeddingSpaceIdentity({
|
|
515
|
+
memory: {
|
|
516
|
+
embeddings: {
|
|
517
|
+
provider: "openai",
|
|
518
|
+
openaiModel: "text-embedding-3-small",
|
|
519
|
+
baseUrl: "http://127.0.0.1:4000/v1",
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
} as unknown as AssistantConfig),
|
|
523
|
+
).toBeNull();
|
|
524
|
+
});
|
|
525
|
+
});
|
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
} from "./embedding-billing-breaker.js";
|
|
18
18
|
import { GeminiEmbeddingBackend } from "./embedding-gemini.js";
|
|
19
19
|
import { OllamaEmbeddingBackend } from "./embedding-ollama.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
OpenAIEmbeddingBackend,
|
|
22
|
+
resolveOpenAICompatibleBaseUrl,
|
|
23
|
+
} from "./embedding-openai.js";
|
|
21
24
|
import { EmbeddingRuntimeManager } from "./embedding-runtime-manager.js";
|
|
22
25
|
import {
|
|
23
26
|
EMBEDDING_DIMENSION_PROBE_TEXT,
|
|
@@ -428,6 +431,70 @@ export function geminiCacheExtras(config: AssistantConfig): string[] {
|
|
|
428
431
|
return extras;
|
|
429
432
|
}
|
|
430
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Cache-key fragments for a custom OpenAI-compatible embeddings endpoint.
|
|
436
|
+
* Base URL and requested output dimensionality both change the vectors a
|
|
437
|
+
* given model returns, so they belong in backend, in-memory, and durable
|
|
438
|
+
* cache identity.
|
|
439
|
+
*/
|
|
440
|
+
export function customCacheExtras(config: AssistantConfig): string[] {
|
|
441
|
+
const extras: string[] = [];
|
|
442
|
+
const baseUrl = resolveOpenAICompatibleBaseUrl(
|
|
443
|
+
config.memory.embeddings.baseUrl,
|
|
444
|
+
);
|
|
445
|
+
if (baseUrl) {
|
|
446
|
+
extras.push(`url=${baseUrl}`);
|
|
447
|
+
}
|
|
448
|
+
if (config.memory.embeddings.customDimensions != null) {
|
|
449
|
+
extras.push(`dim=${config.memory.embeddings.customDimensions}`);
|
|
450
|
+
}
|
|
451
|
+
return extras;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Provider extras that belong in durable `memory_embeddings` identity.
|
|
456
|
+
* Gemini task type / dimensions and custom endpoint URL / dimensions both
|
|
457
|
+
* change the vector for identical text, so cache readers must fold them in.
|
|
458
|
+
*/
|
|
459
|
+
export function durableEmbeddingCacheExtras(
|
|
460
|
+
config: AssistantConfig,
|
|
461
|
+
provider: string | null,
|
|
462
|
+
): string[] {
|
|
463
|
+
if (provider === "gemini") {
|
|
464
|
+
return geminiCacheExtras(config);
|
|
465
|
+
}
|
|
466
|
+
if (provider === "custom") {
|
|
467
|
+
return customCacheExtras(config);
|
|
468
|
+
}
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Checkpoint key for the custom embedding space last used to populate
|
|
474
|
+
* Qdrant. Compared at reconcile time so a same-dimension endpoint change
|
|
475
|
+
* still enqueues a reembed instead of mixing vector spaces.
|
|
476
|
+
*/
|
|
477
|
+
export const CUSTOM_EMBEDDING_SPACE_CHECKPOINT =
|
|
478
|
+
"memory:custom_embedding_space";
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Stable identity of the configured custom embeddings endpoint. Null when
|
|
482
|
+
* the provider is not `custom`. Includes model, normalized base URL, and
|
|
483
|
+
* requested dimensions.
|
|
484
|
+
*/
|
|
485
|
+
export function customEmbeddingSpaceIdentity(
|
|
486
|
+
config: AssistantConfig,
|
|
487
|
+
): string | null {
|
|
488
|
+
if (config.memory.embeddings.provider !== "custom") {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
return [
|
|
492
|
+
"custom",
|
|
493
|
+
config.memory.embeddings.customModel,
|
|
494
|
+
...customCacheExtras(config),
|
|
495
|
+
].join("\0");
|
|
496
|
+
}
|
|
497
|
+
|
|
431
498
|
/** Build (or reuse) the direct-API Gemini backend for the given key. */
|
|
432
499
|
function getDirectGeminiBackend(
|
|
433
500
|
config: AssistantConfig,
|
|
@@ -449,6 +516,29 @@ function getDirectGeminiBackend(
|
|
|
449
516
|
);
|
|
450
517
|
}
|
|
451
518
|
|
|
519
|
+
/** Build (or reuse) a custom OpenAI-compatible embeddings backend. */
|
|
520
|
+
function getCustomEmbeddingBackend(
|
|
521
|
+
config: AssistantConfig,
|
|
522
|
+
baseUrl: string,
|
|
523
|
+
apiKey: string | undefined,
|
|
524
|
+
): EmbeddingBackend {
|
|
525
|
+
return getCachedOrCreate(
|
|
526
|
+
"custom",
|
|
527
|
+
config.memory.embeddings.customModel,
|
|
528
|
+
() =>
|
|
529
|
+
new OpenAIEmbeddingBackend(
|
|
530
|
+
apiKey ?? "",
|
|
531
|
+
config.memory.embeddings.customModel,
|
|
532
|
+
{
|
|
533
|
+
provider: "custom",
|
|
534
|
+
baseURL: baseUrl,
|
|
535
|
+
dimensions: config.memory.embeddings.customDimensions,
|
|
536
|
+
},
|
|
537
|
+
),
|
|
538
|
+
customCacheExtras(config),
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
452
542
|
/**
|
|
453
543
|
* Build (or reuse) the managed-proxy Gemini backend, or return undefined
|
|
454
544
|
* when the managed proxy prerequisites (platform URL + assistant API key)
|
|
@@ -517,6 +607,23 @@ export async function selectEmbeddingBackend(
|
|
|
517
607
|
reason: null,
|
|
518
608
|
};
|
|
519
609
|
}
|
|
610
|
+
if (requested === "custom") {
|
|
611
|
+
const baseUrl = resolveOpenAICompatibleBaseUrl(
|
|
612
|
+
config.memory.embeddings.baseUrl,
|
|
613
|
+
);
|
|
614
|
+
if (!baseUrl) {
|
|
615
|
+
return {
|
|
616
|
+
backend: null,
|
|
617
|
+
reason:
|
|
618
|
+
'Embedding backend "custom" requires memory.embeddings.baseUrl to be an http(s) URL',
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const customKey = (await getProviderKeyAsync("custom")) ?? undefined;
|
|
622
|
+
return {
|
|
623
|
+
backend: getCustomEmbeddingBackend(config, baseUrl, customKey),
|
|
624
|
+
reason: null,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
520
627
|
|
|
521
628
|
// When managed proxy prerequisites are satisfied, insert managed-proxy Gemini
|
|
522
629
|
// at the front of the auto chain so platform assistants use Vellum-managed
|
|
@@ -620,6 +727,9 @@ export async function selectEmbeddingBackend(
|
|
|
620
727
|
reason: null,
|
|
621
728
|
};
|
|
622
729
|
}
|
|
730
|
+
case "custom":
|
|
731
|
+
// Custom is explicit-only and is handled before the auto chain.
|
|
732
|
+
continue;
|
|
623
733
|
}
|
|
624
734
|
}
|
|
625
735
|
|
|
@@ -666,14 +776,30 @@ export async function getMemoryBackendStatus(config: AssistantConfig): Promise<{
|
|
|
666
776
|
}
|
|
667
777
|
|
|
668
778
|
/**
|
|
669
|
-
* Memoized output dimension per
|
|
670
|
-
* is fixed for the life of
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
779
|
+
* Memoized output dimension per backend identity. A backend's vector
|
|
780
|
+
* dimension is fixed for the life of that identity (provider, model, and for
|
|
781
|
+
* custom OpenAI-compatible endpoints the base URL and requested dimensions),
|
|
782
|
+
* so a single probe answers every subsequent
|
|
783
|
+
* {@link isEmbeddingDimensionAvailable} call without another backend
|
|
784
|
+
* round-trip. Cleared alongside the backend cache so a credential change or
|
|
785
|
+
* explicit reset re-probes the (possibly different) backend.
|
|
674
786
|
*/
|
|
675
787
|
const backendDimCache = new Map<string, number>();
|
|
676
788
|
|
|
789
|
+
function backendDimCacheKey(backend: EmbeddingBackend): string {
|
|
790
|
+
if (backend instanceof OpenAIEmbeddingBackend) {
|
|
791
|
+
const extras: string[] = [];
|
|
792
|
+
if (backend.baseURL) {
|
|
793
|
+
extras.push(`url=${backend.baseURL}`);
|
|
794
|
+
}
|
|
795
|
+
if (backend.dimensions != null) {
|
|
796
|
+
extras.push(`dim=${backend.dimensions}`);
|
|
797
|
+
}
|
|
798
|
+
return cacheKey(backend.provider, backend.model, extras);
|
|
799
|
+
}
|
|
800
|
+
return `${backend.provider}:${backend.model}`;
|
|
801
|
+
}
|
|
802
|
+
|
|
677
803
|
/**
|
|
678
804
|
* Whether the currently-reachable embedding backend can produce vectors of the
|
|
679
805
|
* committed Qdrant collection dimension (`config.memory.qdrant.vectorSize`).
|
|
@@ -692,7 +818,7 @@ const backendDimCache = new Map<string, number>();
|
|
|
692
818
|
*
|
|
693
819
|
* The selected backend's output dimension is not statically known from
|
|
694
820
|
* provider/model alone, so it is measured with a fixed-string probe and
|
|
695
|
-
* memoized per
|
|
821
|
+
* memoized per backend identity — steady-state calls resolve from
|
|
696
822
|
* {@link backendDimCache} without a backend round-trip.
|
|
697
823
|
*/
|
|
698
824
|
export async function isEmbeddingDimensionAvailable(
|
|
@@ -713,7 +839,7 @@ export async function isEmbeddingDimensionAvailable(
|
|
|
713
839
|
// assembler, so this preflight can never be more pessimistic than the real
|
|
714
840
|
// embed path. Dense recall stays available if ANY backend in the chain
|
|
715
841
|
// produces the committed dimension. `resolveBackendDimension` memoizes per
|
|
716
|
-
//
|
|
842
|
+
// backend identity, so each backend is probed at most once.
|
|
717
843
|
const expected = config.memory.qdrant.vectorSize;
|
|
718
844
|
for (const candidate of await assembleEmbeddingBackends(config, backend)) {
|
|
719
845
|
if ((await resolveBackendDimension(candidate)) === expected) {
|
|
@@ -736,7 +862,7 @@ export async function isEmbeddingDimensionAvailable(
|
|
|
736
862
|
export async function resolveBackendDimension(
|
|
737
863
|
backend: EmbeddingBackend,
|
|
738
864
|
): Promise<number | null> {
|
|
739
|
-
const key =
|
|
865
|
+
const key = backendDimCacheKey(backend);
|
|
740
866
|
const cached = backendDimCache.get(key);
|
|
741
867
|
if (cached != null) {
|
|
742
868
|
return cached;
|
|
@@ -849,7 +975,11 @@ export async function embedWithBackend(
|
|
|
849
975
|
|
|
850
976
|
// ── Compute provider-specific vector cache extras ───────────────
|
|
851
977
|
const vectorExtras =
|
|
852
|
-
primaryProvider === "gemini"
|
|
978
|
+
primaryProvider === "gemini"
|
|
979
|
+
? geminiCacheExtras(config)
|
|
980
|
+
: primaryProvider === "custom"
|
|
981
|
+
? customCacheExtras(config)
|
|
982
|
+
: undefined;
|
|
853
983
|
|
|
854
984
|
// ── In-memory cache check (primary provider only) ──────────────
|
|
855
985
|
const cached: (number[] | null)[] = inputs.map((input) => {
|
|
@@ -918,7 +1048,11 @@ export async function embedWithBackend(
|
|
|
918
1048
|
|
|
919
1049
|
// Populate cache with freshly embedded vectors
|
|
920
1050
|
const backendExtras =
|
|
921
|
-
backend.provider === "gemini"
|
|
1051
|
+
backend.provider === "gemini"
|
|
1052
|
+
? geminiCacheExtras(config)
|
|
1053
|
+
: backend.provider === "custom"
|
|
1054
|
+
? customCacheExtras(config)
|
|
1055
|
+
: undefined;
|
|
922
1056
|
for (let i = 0; i < inputsToEmbed.length; i++) {
|
|
923
1057
|
putInVectorCache(
|
|
924
1058
|
backend.provider,
|
|
@@ -1060,6 +1194,9 @@ async function selectFallbackBackends(
|
|
|
1060
1194
|
}
|
|
1061
1195
|
break;
|
|
1062
1196
|
}
|
|
1197
|
+
case "custom":
|
|
1198
|
+
case "local":
|
|
1199
|
+
break;
|
|
1063
1200
|
}
|
|
1064
1201
|
}
|
|
1065
1202
|
return backends;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
const createMock = mock(async (_body: unknown, _opts?: unknown) => ({
|
|
4
|
+
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
let lastCtorArgs: { apiKey?: string; baseURL?: string } | undefined;
|
|
8
|
+
|
|
9
|
+
mock.module("openai", () => ({
|
|
10
|
+
default: class OpenAI {
|
|
11
|
+
embeddings = { create: createMock };
|
|
12
|
+
constructor(opts: { apiKey: string; baseURL?: string }) {
|
|
13
|
+
lastCtorArgs = opts;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
OpenAIEmbeddingBackend,
|
|
20
|
+
resolveOpenAICompatibleBaseUrl,
|
|
21
|
+
} from "./embedding-openai.js";
|
|
22
|
+
|
|
23
|
+
describe("resolveOpenAICompatibleBaseUrl", () => {
|
|
24
|
+
test("trims and strips a trailing slash", () => {
|
|
25
|
+
expect(
|
|
26
|
+
resolveOpenAICompatibleBaseUrl(" https://gateway.example.com/v1/ "),
|
|
27
|
+
).toBe("https://gateway.example.com/v1");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("accepts http localhost URLs", () => {
|
|
31
|
+
expect(resolveOpenAICompatibleBaseUrl("http://127.0.0.1:4000/v1")).toBe(
|
|
32
|
+
"http://127.0.0.1:4000/v1",
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("rejects empty, non-http, and invalid values", () => {
|
|
37
|
+
expect(resolveOpenAICompatibleBaseUrl(undefined)).toBeNull();
|
|
38
|
+
expect(resolveOpenAICompatibleBaseUrl("")).toBeNull();
|
|
39
|
+
expect(resolveOpenAICompatibleBaseUrl(" ")).toBeNull();
|
|
40
|
+
expect(resolveOpenAICompatibleBaseUrl("not-a-url")).toBeNull();
|
|
41
|
+
expect(resolveOpenAICompatibleBaseUrl("ftp://gateway.example.com/v1")).toBe(
|
|
42
|
+
null,
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("OpenAIEmbeddingBackend", () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
createMock.mockClear();
|
|
50
|
+
lastCtorArgs = undefined;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
createMock.mockClear();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("constructs the official OpenAI client without a baseURL", () => {
|
|
58
|
+
const backend = new OpenAIEmbeddingBackend(
|
|
59
|
+
"sk-test",
|
|
60
|
+
"text-embedding-3-small",
|
|
61
|
+
);
|
|
62
|
+
expect(backend.provider).toBe("openai");
|
|
63
|
+
expect(lastCtorArgs).toEqual({ apiKey: "sk-test" });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("routes custom embeddings through the supplied baseURL", async () => {
|
|
67
|
+
const backend = new OpenAIEmbeddingBackend("gw-key", "embed-mistral", {
|
|
68
|
+
provider: "custom",
|
|
69
|
+
baseURL: "http://127.0.0.1:4000/v1",
|
|
70
|
+
dimensions: 1024,
|
|
71
|
+
});
|
|
72
|
+
expect(backend.provider).toBe("custom");
|
|
73
|
+
expect(backend.baseURL).toBe("http://127.0.0.1:4000/v1");
|
|
74
|
+
expect(lastCtorArgs).toEqual({
|
|
75
|
+
apiKey: "gw-key",
|
|
76
|
+
baseURL: "http://127.0.0.1:4000/v1",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const vectors = await backend.embed(["hello"]);
|
|
80
|
+
expect(vectors).toEqual([[0.1, 0.2, 0.3]]);
|
|
81
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
82
|
+
const [body] = createMock.mock.calls[0] as [
|
|
83
|
+
{
|
|
84
|
+
model: string;
|
|
85
|
+
input: string[];
|
|
86
|
+
encoding_format: string;
|
|
87
|
+
dimensions?: number;
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
expect(body.model).toBe("embed-mistral");
|
|
91
|
+
expect(body.input).toEqual(["hello"]);
|
|
92
|
+
expect(body.encoding_format).toBe("float");
|
|
93
|
+
expect(body.dimensions).toBe(1024);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("uses a placeholder API key when the custom endpoint is keyless", () => {
|
|
97
|
+
new OpenAIEmbeddingBackend("", "text-embedding-3-small", {
|
|
98
|
+
provider: "custom",
|
|
99
|
+
baseURL: "http://127.0.0.1:1234/v1",
|
|
100
|
+
});
|
|
101
|
+
expect(lastCtorArgs?.apiKey).toBe("not-needed");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("omits dimensions when they are not configured", async () => {
|
|
105
|
+
const backend = new OpenAIEmbeddingBackend(
|
|
106
|
+
"sk-test",
|
|
107
|
+
"text-embedding-3-small",
|
|
108
|
+
);
|
|
109
|
+
await backend.embed(["hello"]);
|
|
110
|
+
const [body] = createMock.mock.calls[0] as [{ dimensions?: number }];
|
|
111
|
+
expect(body.dimensions).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -3,18 +3,40 @@ import OpenAI from "openai";
|
|
|
3
3
|
import {
|
|
4
4
|
type EmbeddingBackend,
|
|
5
5
|
type EmbeddingInput,
|
|
6
|
+
type EmbeddingProviderName,
|
|
6
7
|
type EmbeddingRequestOptions,
|
|
7
8
|
normalizeEmbeddingInput,
|
|
8
9
|
} from "./embedding-types.js";
|
|
9
10
|
|
|
11
|
+
export type OpenAICompatibleEmbeddingProvider = Extract<
|
|
12
|
+
EmbeddingProviderName,
|
|
13
|
+
"openai" | "custom"
|
|
14
|
+
>;
|
|
15
|
+
|
|
16
|
+
export interface OpenAIEmbeddingOptions {
|
|
17
|
+
baseURL?: string;
|
|
18
|
+
dimensions?: number;
|
|
19
|
+
provider?: OpenAICompatibleEmbeddingProvider;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PLACEHOLDER_API_KEY = "not-needed";
|
|
23
|
+
|
|
10
24
|
export class OpenAIEmbeddingBackend implements EmbeddingBackend {
|
|
11
|
-
readonly provider
|
|
25
|
+
readonly provider: OpenAICompatibleEmbeddingProvider;
|
|
12
26
|
readonly model: string;
|
|
27
|
+
readonly baseURL?: string;
|
|
28
|
+
readonly dimensions?: number;
|
|
13
29
|
private readonly client: OpenAI;
|
|
14
30
|
|
|
15
|
-
constructor(apiKey: string, model: string) {
|
|
31
|
+
constructor(apiKey: string, model: string, options?: OpenAIEmbeddingOptions) {
|
|
16
32
|
this.model = model;
|
|
17
|
-
this.
|
|
33
|
+
this.provider = options?.provider ?? "openai";
|
|
34
|
+
this.baseURL = options?.baseURL;
|
|
35
|
+
this.dimensions = options?.dimensions;
|
|
36
|
+
this.client = new OpenAI({
|
|
37
|
+
apiKey: apiKey || PLACEHOLDER_API_KEY,
|
|
38
|
+
...(options?.baseURL ? { baseURL: options.baseURL } : {}),
|
|
39
|
+
});
|
|
18
40
|
}
|
|
19
41
|
|
|
20
42
|
async embed(
|
|
@@ -28,7 +50,9 @@ export class OpenAIEmbeddingBackend implements EmbeddingBackend {
|
|
|
28
50
|
const texts = inputs.map((i) => {
|
|
29
51
|
const n = normalizeEmbeddingInput(i);
|
|
30
52
|
if (n.type !== "text") {
|
|
31
|
-
throw new Error(
|
|
53
|
+
throw new Error(
|
|
54
|
+
`${this.provider === "custom" ? "Custom" : "OpenAI"} embedding backend only supports text inputs`,
|
|
55
|
+
);
|
|
32
56
|
}
|
|
33
57
|
return n.text;
|
|
34
58
|
});
|
|
@@ -38,6 +62,7 @@ export class OpenAIEmbeddingBackend implements EmbeddingBackend {
|
|
|
38
62
|
model: this.model,
|
|
39
63
|
input: texts,
|
|
40
64
|
encoding_format: "float",
|
|
65
|
+
...(this.dimensions != null ? { dimensions: this.dimensions } : {}),
|
|
41
66
|
},
|
|
42
67
|
{
|
|
43
68
|
signal: options?.signal,
|
|
@@ -46,3 +71,29 @@ export class OpenAIEmbeddingBackend implements EmbeddingBackend {
|
|
|
46
71
|
return response.data.map((item) => item.embedding);
|
|
47
72
|
}
|
|
48
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Trim and strip a trailing slash from a user-supplied OpenAI-compatible
|
|
77
|
+
* embeddings base URL. Returns null when the value is empty, not http(s),
|
|
78
|
+
* or not a valid absolute URL.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveOpenAICompatibleBaseUrl(
|
|
81
|
+
value: string | undefined,
|
|
82
|
+
): string | null {
|
|
83
|
+
if (value == null) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const trimmed = value.trim().replace(/\/+$/, "");
|
|
87
|
+
if (!trimmed) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const parsed = new URL(trimmed);
|
|
92
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return trimmed;
|
|
99
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
embeddingContentHashWithExtras,
|
|
4
5
|
embeddingInputContentHash,
|
|
5
6
|
type MultimodalEmbeddingInput,
|
|
6
7
|
normalizeEmbeddingInput,
|
|
@@ -114,3 +115,26 @@ describe("embeddingInputContentHash", () => {
|
|
|
114
115
|
expect(hash).toMatch(/^[0-9a-f]{64}$/);
|
|
115
116
|
});
|
|
116
117
|
});
|
|
118
|
+
|
|
119
|
+
describe("embeddingContentHashWithExtras", () => {
|
|
120
|
+
test("matches the bare content hash when extras are empty", () => {
|
|
121
|
+
expect(embeddingContentHashWithExtras("hello")).toBe(
|
|
122
|
+
embeddingInputContentHash("hello"),
|
|
123
|
+
);
|
|
124
|
+
expect(embeddingContentHashWithExtras("hello", [])).toBe(
|
|
125
|
+
embeddingInputContentHash("hello"),
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("changes when extras change for identical text", () => {
|
|
130
|
+
const text = "hello";
|
|
131
|
+
const a = embeddingContentHashWithExtras(text, [
|
|
132
|
+
"url=http://127.0.0.1:4000/v1",
|
|
133
|
+
]);
|
|
134
|
+
const b = embeddingContentHashWithExtras(text, [
|
|
135
|
+
"url=http://127.0.0.1:4001/v1",
|
|
136
|
+
]);
|
|
137
|
+
expect(a).not.toBe(b);
|
|
138
|
+
expect(a).not.toBe(embeddingInputContentHash(text));
|
|
139
|
+
});
|
|
140
|
+
});
|