@vellumai/assistant 0.8.10-dev.202606100742.99a7fab → 0.8.10-dev.202606100925.28c1cfb

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.8.10-dev.202606100742.99a7fab",
3
+ "version": "0.8.10-dev.202606100925.28c1cfb",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,12 +1,33 @@
1
- import { afterEach, describe, expect, mock, test } from "bun:test";
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import type { AssistantConfig } from "../config/types.js";
4
4
  import {
5
5
  clearEmbeddingBackendCache,
6
+ embedWithBackend,
6
7
  resetLocalEmbeddingFailureState,
7
8
  selectEmbeddingBackend,
8
9
  } from "./embedding-backend.js";
9
10
 
11
+ const getProviderKeyAsyncMock = mock(
12
+ async (_provider: string): Promise<string | undefined> => undefined,
13
+ );
14
+ mock.module("../security/secure-keys.js", () => ({
15
+ getProviderKeyAsync: getProviderKeyAsyncMock,
16
+ }));
17
+
18
+ const DISABLED_PROXY_CONTEXT = {
19
+ enabled: false,
20
+ platformBaseUrl: "",
21
+ assistantApiKey: "",
22
+ };
23
+ const resolveManagedProxyContextMock = mock(async () => DISABLED_PROXY_CONTEXT);
24
+ mock.module("../providers/platform-proxy/context.js", () => ({
25
+ resolveManagedProxyContext: resolveManagedProxyContextMock,
26
+ hasManagedProxyPrereqs: mock(async () => false),
27
+ buildManagedBaseUrl: mock(async () => undefined),
28
+ managedFallbackEnabledFor: mock(async () => false),
29
+ }));
30
+
10
31
  const LOCAL_CONFIG = {
11
32
  memory: {
12
33
  embeddings: {
@@ -73,3 +94,111 @@ describe("embedding backend cache invalidation", () => {
73
94
  expect(secondSelection.backend).toBe(firstSelection.backend);
74
95
  });
75
96
  });
97
+
98
+ const GEMINI_CONFIG = {
99
+ memory: {
100
+ embeddings: {
101
+ provider: "gemini",
102
+ geminiModel: "test-model",
103
+ },
104
+ qdrant: {
105
+ vectorSize: 3,
106
+ },
107
+ },
108
+ } as unknown as AssistantConfig;
109
+
110
+ const ENABLED_PROXY_CONTEXT = {
111
+ enabled: true,
112
+ platformBaseUrl: "https://proxy.example.com",
113
+ assistantApiKey: "stale-platform-key",
114
+ };
115
+
116
+ function jsonResponse(body: unknown, status: number): Response {
117
+ return new Response(JSON.stringify(body), {
118
+ status,
119
+ headers: { "Content-Type": "application/json" },
120
+ });
121
+ }
122
+
123
+ describe("managed-proxy Gemini fallback to direct key", () => {
124
+ const originalFetch = globalThis.fetch;
125
+ let fetchMock: ReturnType<typeof mock>;
126
+
127
+ beforeEach(() => {
128
+ clearEmbeddingBackendCache();
129
+ // Managed-proxy requests (proxy.example.com) are rejected like a platform
130
+ // proxy with a bad credential; direct Google API requests succeed.
131
+ fetchMock = mock(async (url: string | URL) => {
132
+ if (String(url).startsWith("https://proxy.example.com/")) {
133
+ return jsonResponse({ detail: "Invalid or revoked API key." }, 403);
134
+ }
135
+ return jsonResponse({ embedding: { values: [0.1, 0.2, 0.3] } }, 200);
136
+ });
137
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
138
+ });
139
+
140
+ afterEach(() => {
141
+ globalThis.fetch = originalFetch;
142
+ clearEmbeddingBackendCache();
143
+ getProviderKeyAsyncMock.mockReset();
144
+ getProviderKeyAsyncMock.mockImplementation(async () => undefined);
145
+ resolveManagedProxyContextMock.mockReset();
146
+ resolveManagedProxyContextMock.mockImplementation(
147
+ async () => DISABLED_PROXY_CONTEXT,
148
+ );
149
+ });
150
+
151
+ test("falls back to the direct key when the managed-proxy primary fails", async () => {
152
+ resolveManagedProxyContextMock.mockImplementation(
153
+ async () => ENABLED_PROXY_CONTEXT,
154
+ );
155
+ getProviderKeyAsyncMock.mockImplementation(async (provider) =>
156
+ provider === "gemini" ? "direct-key" : undefined,
157
+ );
158
+
159
+ const result = await embedWithBackend(GEMINI_CONFIG, ["hello world"]);
160
+
161
+ expect(result.provider).toBe("gemini");
162
+ expect(result.vectors).toEqual([[0.1, 0.2, 0.3]]);
163
+
164
+ expect(fetchMock).toHaveBeenCalledTimes(2);
165
+ const [managedUrl, managedInit] = fetchMock.mock.calls[0] as [
166
+ string,
167
+ RequestInit,
168
+ ];
169
+ expect(managedUrl).toBe(
170
+ "https://proxy.example.com/v1/runtime-proxy/gemini/v1beta/models/test-model:embedContent",
171
+ );
172
+ expect(
173
+ (managedInit.headers as Record<string, string>)["Authorization"],
174
+ ).toBe("Bearer stale-platform-key");
175
+ const [directUrl] = fetchMock.mock.calls[1] as [string, RequestInit];
176
+ expect(directUrl).toContain("generativelanguage.googleapis.com");
177
+ expect(directUrl).toContain("key=direct-key");
178
+ });
179
+
180
+ test("surfaces the managed-proxy error when no direct key exists", async () => {
181
+ resolveManagedProxyContextMock.mockImplementation(
182
+ async () => ENABLED_PROXY_CONTEXT,
183
+ );
184
+
185
+ await expect(
186
+ embedWithBackend(GEMINI_CONFIG, ["hello world"]),
187
+ ).rejects.toThrow(/403/);
188
+ expect(fetchMock).toHaveBeenCalledTimes(1);
189
+ });
190
+
191
+ test("uses the direct key as primary when the managed proxy is disabled", async () => {
192
+ getProviderKeyAsyncMock.mockImplementation(async (provider) =>
193
+ provider === "gemini" ? "direct-key" : undefined,
194
+ );
195
+
196
+ const result = await embedWithBackend(GEMINI_CONFIG, ["another input"]);
197
+
198
+ expect(result.vectors).toEqual([[0.1, 0.2, 0.3]]);
199
+ expect(fetchMock).toHaveBeenCalledTimes(1);
200
+ const [directUrl] = fetchMock.mock.calls[0] as [string, RequestInit];
201
+ expect(directUrl).toContain("generativelanguage.googleapis.com");
202
+ expect(directUrl).toContain("key=direct-key");
203
+ });
204
+ });
@@ -266,6 +266,56 @@ function geminiCacheExtras(config: AssistantConfig): string[] {
266
266
  return extras;
267
267
  }
268
268
 
269
+ /** Build (or reuse) the direct-API Gemini backend for the given key. */
270
+ function getDirectGeminiBackend(
271
+ config: AssistantConfig,
272
+ geminiKey: string,
273
+ ): EmbeddingBackend {
274
+ return getCachedOrCreate(
275
+ "gemini",
276
+ config.memory.embeddings.geminiModel,
277
+ () =>
278
+ new GeminiEmbeddingBackend(
279
+ geminiKey,
280
+ config.memory.embeddings.geminiModel,
281
+ {
282
+ taskType: config.memory.embeddings.geminiTaskType,
283
+ dimensions: config.memory.embeddings.geminiDimensions,
284
+ },
285
+ ),
286
+ geminiCacheExtras(config),
287
+ );
288
+ }
289
+
290
+ /**
291
+ * Build (or reuse) the managed-proxy Gemini backend, or return undefined
292
+ * when the managed proxy prerequisites (platform URL + assistant API key)
293
+ * are not satisfied.
294
+ */
295
+ async function tryGetManagedGeminiBackend(
296
+ config: AssistantConfig,
297
+ ): Promise<EmbeddingBackend | undefined> {
298
+ const proxyCtx = await resolveManagedProxyContext();
299
+ const meta = PLATFORM_PROVIDER_META["gemini"];
300
+ if (!proxyCtx.enabled || !meta?.managed || !meta.proxyPath) return undefined;
301
+ const managedBaseUrl = `${proxyCtx.platformBaseUrl}${meta.proxyPath}`;
302
+ return getCachedOrCreate(
303
+ "gemini",
304
+ config.memory.embeddings.geminiModel,
305
+ () =>
306
+ new GeminiEmbeddingBackend(
307
+ proxyCtx.assistantApiKey,
308
+ config.memory.embeddings.geminiModel,
309
+ {
310
+ taskType: config.memory.embeddings.geminiTaskType,
311
+ dimensions: config.memory.embeddings.geminiDimensions ?? 3072,
312
+ managedBaseUrl,
313
+ },
314
+ ),
315
+ [...geminiCacheExtras(config), "managed"],
316
+ );
317
+ }
318
+
269
319
  export interface EmbeddingBackendSelection {
270
320
  backend: EmbeddingBackend | null;
271
321
  reason: string | null;
@@ -305,34 +355,9 @@ export async function selectEmbeddingBackend(
305
355
  // at the front of the auto chain so platform assistants use Vellum-managed
306
356
  // Gemini embeddings.
307
357
  if (requested === "auto" || requested === "gemini") {
308
- const proxyCtx = await resolveManagedProxyContext();
309
- if (proxyCtx.enabled) {
310
- const meta = PLATFORM_PROVIDER_META["gemini"];
311
- if (meta?.managed && meta.proxyPath) {
312
- const managedBaseUrl = `${proxyCtx.platformBaseUrl}${meta.proxyPath}`;
313
- const managedModel = config.memory.embeddings.geminiModel;
314
- const managedDimensions =
315
- config.memory.embeddings.geminiDimensions ?? 3072;
316
- const extras = geminiCacheExtras(config);
317
- return {
318
- backend: getCachedOrCreate(
319
- "gemini",
320
- managedModel,
321
- () =>
322
- new GeminiEmbeddingBackend(
323
- proxyCtx.assistantApiKey,
324
- managedModel,
325
- {
326
- taskType: config.memory.embeddings.geminiTaskType,
327
- dimensions: managedDimensions,
328
- managedBaseUrl,
329
- },
330
- ),
331
- [...extras, "managed"],
332
- ),
333
- reason: null,
334
- };
335
- }
358
+ const managed = await tryGetManagedGeminiBackend(config);
359
+ if (managed) {
360
+ return { backend: managed, reason: null };
336
361
  }
337
362
  }
338
363
 
@@ -401,20 +426,7 @@ export async function selectEmbeddingBackend(
401
426
  continue;
402
427
  }
403
428
  return {
404
- backend: getCachedOrCreate(
405
- "gemini",
406
- config.memory.embeddings.geminiModel,
407
- () =>
408
- new GeminiEmbeddingBackend(
409
- geminiKey,
410
- config.memory.embeddings.geminiModel,
411
- {
412
- taskType: config.memory.embeddings.geminiTaskType,
413
- dimensions: config.memory.embeddings.geminiDimensions,
414
- },
415
- ),
416
- geminiCacheExtras(config),
417
- ),
429
+ backend: getDirectGeminiBackend(config, geminiKey),
418
430
  reason: null,
419
431
  };
420
432
  }
@@ -507,6 +519,21 @@ export async function embedWithBackend(
507
519
  ? await selectFallbackBackends(config, selection.backend.provider)
508
520
  : [];
509
521
 
522
+ // A managed-proxy Gemini primary can fail at the proxy (e.g. a stale or
523
+ // revoked platform credential) while a valid direct Gemini key sits in the
524
+ // credential store. The provider-level chain above never includes Gemini
525
+ // when Gemini IS the primary, so without this the direct key would never
526
+ // be tried and the provider would stay down for the whole process.
527
+ if (
528
+ selection.backend instanceof GeminiEmbeddingBackend &&
529
+ selection.backend.managed
530
+ ) {
531
+ const geminiKey = await getProviderKeyAsync("gemini");
532
+ if (geminiKey) {
533
+ fallbacks.push(getDirectGeminiBackend(config, geminiKey));
534
+ }
535
+ }
536
+
510
537
  // ── Compute provider-specific vector cache extras ───────────────
511
538
  const vectorExtras =
512
539
  primaryProvider === "gemini" ? geminiCacheExtras(config) : undefined;
@@ -657,49 +684,12 @@ async function selectFallbackBackends(
657
684
  case "gemini": {
658
685
  const geminiKey = await getProviderKeyAsync("gemini");
659
686
  if (geminiKey) {
660
- backends.push(
661
- getCachedOrCreate(
662
- "gemini",
663
- config.memory.embeddings.geminiModel,
664
- () =>
665
- new GeminiEmbeddingBackend(
666
- geminiKey,
667
- config.memory.embeddings.geminiModel,
668
- {
669
- taskType: config.memory.embeddings.geminiTaskType,
670
- dimensions: config.memory.embeddings.geminiDimensions,
671
- },
672
- ),
673
- geminiCacheExtras(config),
674
- ),
675
- );
687
+ backends.push(getDirectGeminiBackend(config, geminiKey));
676
688
  } else {
677
689
  // Try managed proxy Gemini as fallback when no direct key exists.
678
- const proxyCtx = await resolveManagedProxyContext();
679
- const meta = PLATFORM_PROVIDER_META["gemini"];
680
- if (proxyCtx.enabled && meta?.managed && meta.proxyPath) {
681
- const managedBaseUrl = `${proxyCtx.platformBaseUrl}${meta.proxyPath}`;
682
- const managedModel = config.memory.embeddings.geminiModel;
683
- const managedDimensions =
684
- config.memory.embeddings.geminiDimensions ?? 3072;
685
- const extras = geminiCacheExtras(config);
686
- backends.push(
687
- getCachedOrCreate(
688
- "gemini",
689
- managedModel,
690
- () =>
691
- new GeminiEmbeddingBackend(
692
- proxyCtx.assistantApiKey,
693
- managedModel,
694
- {
695
- taskType: config.memory.embeddings.geminiTaskType,
696
- dimensions: managedDimensions,
697
- managedBaseUrl,
698
- },
699
- ),
700
- [...extras, "managed"],
701
- ),
702
- );
690
+ const managed = await tryGetManagedGeminiBackend(config);
691
+ if (managed) {
692
+ backends.push(managed);
703
693
  } else {
704
694
  // Check managed cache variant first, then non-managed, so a warm
705
695
  // managed backend survives transient proxy-context blips.
@@ -36,6 +36,11 @@ export class GeminiEmbeddingBackend implements EmbeddingBackend {
36
36
  this.managedBaseUrl = options?.managedBaseUrl;
37
37
  }
38
38
 
39
+ /** True when requests route through the managed platform proxy. */
40
+ get managed(): boolean {
41
+ return Boolean(this.managedBaseUrl);
42
+ }
43
+
39
44
  async embed(
40
45
  inputs: EmbeddingInput[],
41
46
  options?: EmbeddingRequestOptions,