@vellumai/assistant 0.8.10-dev.202606100540.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,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
|
|
309
|
-
if (
|
|
310
|
-
|
|
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:
|
|
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
|
|
679
|
-
|
|
680
|
-
|
|
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,
|