mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
* feat(providers): complete Jina AI via OmniRoute including Omni multimodal
Dashboard and env keys share one Jina credential pool, native v5 Omni
{text}/{image}/{content} docs pass through /v1/embeddings intact, and
classify/segment/search are proxied without a third unused Jina card.
* chore(changelog): name Jina complete-provider fragment for #10581
* feat(providers): make Gemini Embedding 2 multimodal work via OmniRoute
Route gemini-embedding-2 through embedContent/batchEmbedContents so N
OpenAI input items become N vectors, pass through native multimodal
parts, and use dashboard Gemini keys (GEMINI_API_KEY only as fallback).
* fix(providers): resolve rebase fallout for Jina/Gemini embeddings
- narrow the two new no-explicit-any violations introduced by this PR
(validateJinaFoundationProvider's params + catch, search.ts's
normalizeJinaSearchResponse data param)
- cast credentials to Record<string, unknown> at the two quota-preflight
call sites in src/sse/services/auth.ts so the new JinaEnvCredentials /
GeminiEnvCredentials union members type-check without loosening the
allRateLimited narrowing used elsewhere in the same function
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
311 lines
11 KiB
TypeScript
311 lines
11 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-gemini-embed2-"));
|
|
|
|
import {
|
|
GEMINI_ENV_CONNECTION_ID,
|
|
buildGeminiEnvCredentials,
|
|
isGeminiCredentialProvider,
|
|
readGeminiEnvApiKey,
|
|
} from "../../src/lib/providers/gemini.ts";
|
|
import { parseEmbeddingModel, getEmbeddingDimension } from "../../open-sse/config/embeddingRegistry.ts";
|
|
import { v1EmbeddingsSchema } from "../../src/shared/validation/schemas/apiV1.ts";
|
|
import { handleEmbedding } from "../../open-sse/handlers/embeddings.ts";
|
|
|
|
const ENV_KEYS = ["GEMINI_API_KEY", "GOOGLE_API_KEY"] as const;
|
|
const savedEnv = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]]));
|
|
|
|
function restoreEnv() {
|
|
for (const key of ENV_KEYS) {
|
|
if (savedEnv[key] === undefined) delete process.env[key];
|
|
else process.env[key] = savedEnv[key];
|
|
}
|
|
}
|
|
|
|
test.afterEach(restoreEnv);
|
|
|
|
const PNG_B64 =
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
|
const IMAGE_URL = "https://example.com/bike.png";
|
|
|
|
function batchEmbeddingResponse(count: number) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
embeddings: Array.from({ length: count }, (_, index) => ({
|
|
values: [0.1 * (index + 1), 0.2],
|
|
})),
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
function singleEmbeddingResponse() {
|
|
return new Response(JSON.stringify({ embedding: { values: [0.1, 0.2] } }), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
|
|
test("Gemini env helper prefers GEMINI_API_KEY over GOOGLE_API_KEY", () => {
|
|
delete process.env.GEMINI_API_KEY;
|
|
delete process.env.GOOGLE_API_KEY;
|
|
process.env.GOOGLE_API_KEY = "alias-key";
|
|
assert.equal(readGeminiEnvApiKey(), "alias-key");
|
|
process.env.GEMINI_API_KEY = "primary-key";
|
|
assert.equal(readGeminiEnvApiKey(), "primary-key");
|
|
});
|
|
|
|
test("Gemini env credentials are scoped to gemini and honor filters", () => {
|
|
process.env.GEMINI_API_KEY = "env-gemini-key";
|
|
assert.equal(isGeminiCredentialProvider("gemini"), true);
|
|
assert.equal(isGeminiCredentialProvider("google"), false);
|
|
assert.equal(isGeminiCredentialProvider("jina-ai"), false);
|
|
assert.equal(buildGeminiEnvCredentials("openai"), null);
|
|
|
|
const creds = buildGeminiEnvCredentials("gemini");
|
|
assert.ok(creds);
|
|
assert.equal(creds.apiKey, "env-gemini-key");
|
|
assert.equal(creds.connectionId, GEMINI_ENV_CONNECTION_ID);
|
|
assert.equal(buildGeminiEnvCredentials("gemini", { forcedConnectionId: "dashboard-row" }), null);
|
|
assert.ok(buildGeminiEnvCredentials("gemini", { forcedConnectionId: GEMINI_ENV_CONNECTION_ID }));
|
|
assert.equal(buildGeminiEnvCredentials("gemini", { allowedConnections: ["other-id"] }), null);
|
|
assert.equal(
|
|
buildGeminiEnvCredentials("gemini", { excludedConnectionIds: [GEMINI_ENV_CONNECTION_ID] }),
|
|
null
|
|
);
|
|
});
|
|
|
|
test("catalog id is gemini/gemini-embedding-2; google/ is an alias", () => {
|
|
const native = parseEmbeddingModel("gemini/gemini-embedding-2");
|
|
assert.equal(native.provider, "gemini");
|
|
assert.equal(native.model, "gemini-embedding-2");
|
|
assert.equal(getEmbeddingDimension("gemini/gemini-embedding-2"), 3072);
|
|
|
|
const aliased = parseEmbeddingModel("google/gemini-embedding-2");
|
|
assert.equal(aliased.provider, "gemini");
|
|
assert.equal(aliased.model, "gemini-embedding-2");
|
|
|
|
const preview = parseEmbeddingModel("google/gemini-embedding-2-preview");
|
|
assert.equal(preview.provider, "gemini");
|
|
assert.equal(preview.model, "gemini-embedding-2-preview");
|
|
|
|
// Custom provider_node prefix `google` plus embedding-001 must stay unaliased.
|
|
const custom = parseEmbeddingModel("google/gemini-embedding-001");
|
|
assert.equal(custom.provider, "google");
|
|
assert.equal(custom.model, "gemini-embedding-001");
|
|
});
|
|
|
|
test("schema accepts Gemini native text + inline_data mixed batches", () => {
|
|
const parsed = v1EmbeddingsSchema.safeParse({
|
|
model: "gemini/gemini-embedding-2",
|
|
task: "retrieval.query",
|
|
input: [
|
|
{ text: "a red bicycle" },
|
|
{ inline_data: { mime_type: "image/png", data: PNG_B64 } },
|
|
],
|
|
});
|
|
assert.equal(parsed.success, true);
|
|
if (parsed.success) {
|
|
assert.deepEqual(parsed.data.input, [
|
|
{ text: "a red bicycle" },
|
|
{ inline_data: { mime_type: "image/png", data: PNG_B64 } },
|
|
]);
|
|
}
|
|
});
|
|
|
|
test("schema accepts fused Gemini Content and rejects unsafe file URIs", () => {
|
|
assert.equal(
|
|
v1EmbeddingsSchema.safeParse({
|
|
model: "gemini/gemini-embedding-2",
|
|
input: {
|
|
parts: [{ text: "caption" }, { inline_data: { mime_type: "image/png", data: PNG_B64 } }],
|
|
},
|
|
}).success,
|
|
true
|
|
);
|
|
for (const file_uri of [
|
|
"http://example.com/bike.png",
|
|
"https://127.0.0.1/bike.png",
|
|
"https://169.254.169.254/latest/meta-data/",
|
|
"file:///etc/passwd",
|
|
]) {
|
|
const parsed = v1EmbeddingsSchema.safeParse({
|
|
model: "gemini/gemini-embedding-2",
|
|
input: [{ file_data: { mime_type: "image/png", file_uri } }],
|
|
});
|
|
assert.equal(parsed.success, false, `expected reject: ${file_uri}`);
|
|
}
|
|
});
|
|
|
|
test("handleEmbedding sends N Gemini Embedding 2 inputs as N batch requests", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const seen: Array<{ url: string; headers: Record<string, string>; body: Record<string, unknown> }> =
|
|
[];
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
const headers = (init.headers || {}) as Record<string, string>;
|
|
seen.push({
|
|
url: String(url),
|
|
headers,
|
|
body: JSON.parse(String(init.body || "{}")) as Record<string, unknown>,
|
|
});
|
|
return batchEmbeddingResponse(3);
|
|
};
|
|
|
|
try {
|
|
const result = await handleEmbedding({
|
|
body: {
|
|
model: "gemini/gemini-embedding-2",
|
|
input: ["alpha", "beta", "gamma"],
|
|
dimensions: 768,
|
|
},
|
|
credentials: { apiKey: "test-gemini-token", connectionId: "conn-gemini-embed" },
|
|
log: null,
|
|
});
|
|
assert.equal(result.success, true, result.error);
|
|
assert.equal(seen.length, 1);
|
|
assert.equal(
|
|
seen[0].url,
|
|
"https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-2:batchEmbedContents"
|
|
);
|
|
assert.equal(seen[0].headers["x-goog-api-key"], "test-gemini-token");
|
|
assert.equal(seen[0].headers.Authorization, undefined);
|
|
const requests = seen[0].body.requests as Array<{ content: { parts: unknown[] } }>;
|
|
assert.equal(requests.length, 3);
|
|
assert.deepEqual(
|
|
requests.map((request) => request.content.parts),
|
|
[[{ text: "alpha" }], [{ text: "beta" }], [{ text: "gamma" }]]
|
|
);
|
|
const data = (result.data as { data: Array<{ embedding: number[]; index: number }> }).data;
|
|
assert.equal(data.length, 3);
|
|
assert.deepEqual(
|
|
data.map((row) => row.index),
|
|
[0, 1, 2]
|
|
);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleEmbedding forwards Gemini native text+image parts and does not strip to string[]", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const seen: Array<{ url: string; body: Record<string, unknown> }> = [];
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
const target = String(url);
|
|
if (target === IMAGE_URL || target.includes("bike.png")) {
|
|
throw new Error("Gemini-native inline_data must not trigger a media fetch");
|
|
}
|
|
seen.push({
|
|
url: target,
|
|
body: JSON.parse(String(init.body || "{}")) as Record<string, unknown>,
|
|
});
|
|
return batchEmbeddingResponse(2);
|
|
};
|
|
|
|
try {
|
|
const result = await handleEmbedding({
|
|
body: {
|
|
model: "google/gemini-embedding-2",
|
|
input: [
|
|
{ text: "a red bicycle" },
|
|
{ inline_data: { mime_type: "image/png", data: PNG_B64 } },
|
|
],
|
|
},
|
|
credentials: { apiKey: "test-gemini-token" },
|
|
log: null,
|
|
});
|
|
assert.equal(result.success, true, result.error);
|
|
assert.equal(seen.length, 1);
|
|
assert.equal(
|
|
seen[0].url,
|
|
"https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-2:batchEmbedContents"
|
|
);
|
|
const requests = seen[0].body.requests as Array<{ content: { parts: unknown[] } }>;
|
|
assert.equal(requests.length, 2);
|
|
assert.deepEqual(requests[0].content.parts, [{ text: "a red bicycle" }]);
|
|
assert.deepEqual(requests[1].content.parts, [
|
|
{ inline_data: { mime_type: "image/png", data: PNG_B64 } },
|
|
]);
|
|
assert.equal(typeof seen[0].body.input, "undefined");
|
|
const data = (result.data as { data: unknown[] }).data;
|
|
assert.equal(data.length, 2);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleEmbedding fuses one Gemini Content with multiple parts into one vector", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let seenBody: Record<string, unknown> | null = null;
|
|
let seenUrl = "";
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
seenUrl = String(url);
|
|
seenBody = JSON.parse(String(init.body || "{}")) as Record<string, unknown>;
|
|
return singleEmbeddingResponse();
|
|
};
|
|
|
|
try {
|
|
const result = await handleEmbedding({
|
|
body: {
|
|
model: "gemini/gemini-embedding-2",
|
|
input: {
|
|
parts: [{ text: "caption" }, { inline_data: { mime_type: "image/png", data: PNG_B64 } }],
|
|
},
|
|
},
|
|
credentials: { apiKey: "test-gemini-token" },
|
|
log: null,
|
|
});
|
|
assert.equal(result.success, true, result.error);
|
|
assert.equal(
|
|
seenUrl,
|
|
"https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-2:embedContent"
|
|
);
|
|
assert.deepEqual(seenBody?.content, {
|
|
parts: [{ text: "caption" }, { inline_data: { mime_type: "image/png", data: PNG_B64 } }],
|
|
});
|
|
const data = (result.data as { data: unknown[] }).data;
|
|
assert.equal(data.length, 1);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleEmbedding keeps gemini-embedding-001 text batches on the OpenAI shim", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let seenUrl = "";
|
|
let seenBody: Record<string, unknown> | null = null;
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
seenUrl = String(url);
|
|
seenBody = JSON.parse(String(init.body || "{}")) as Record<string, unknown>;
|
|
return new Response(
|
|
JSON.stringify({
|
|
data: [
|
|
{ object: "embedding", embedding: [0.1], index: 0 },
|
|
{ object: "embedding", embedding: [0.2], index: 1 },
|
|
],
|
|
usage: { prompt_tokens: 2, total_tokens: 2 },
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleEmbedding({
|
|
body: {
|
|
model: "gemini/gemini-embedding-001",
|
|
input: ["alpha", "beta"],
|
|
},
|
|
credentials: { apiKey: "test-gemini-token" },
|
|
log: null,
|
|
});
|
|
assert.equal(result.success, true, result.error);
|
|
assert.equal(seenUrl, "https://generativelanguage.googleapis.com/v1beta/openai/embeddings");
|
|
assert.deepEqual(seenBody?.input, ["alpha", "beta"]);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|