Files
OmniRoute/src/lib/providers/gemini.ts
Ravi Tharuma 3d0ffb49a4 feat(providers): complete Jina + Gemini Embedding 2 multimodal via OmniRoute (#10581)
* 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>
2026-08-18 10:52:43 -03:00

87 lines
2.6 KiB
TypeScript

/**
* Shared Gemini (Google AI Studio) integration helpers.
*
* Dashboard `gemini` connections stay preferred. GEMINI_API_KEY /
* GOOGLE_API_KEY are a headless fallback when no usable dashboard key
* exists — the same class of bug as unused JINA_AI_API_KEY. Do not treat
* the env as billed if a dashboard connection is selected (fill-first).
*/
export const GEMINI_PROVIDER_ID = "gemini";
/** Call-log / credential sentinel when the request used the process env key. */
export const GEMINI_ENV_CONNECTION_ID = "env:GEMINI_API_KEY";
export const GEMINI_ENV_API_KEY_NAMES = ["GEMINI_API_KEY", "GOOGLE_API_KEY"] as const;
export interface GeminiEnvCredentials {
apiKey: string;
accessToken: null;
connectionId: typeof GEMINI_ENV_CONNECTION_ID;
id: typeof GEMINI_ENV_CONNECTION_ID;
provider: string;
authType: "apikey";
defaultModel: null;
}
export function isGeminiCredentialProvider(providerId: string | null | undefined): boolean {
return providerId === GEMINI_PROVIDER_ID;
}
/**
* Read the first non-empty Gemini env key. Dashboard connections always win
* when getProviderCredentials finds one.
*/
export function readGeminiEnvApiKey(): string | null {
for (const name of GEMINI_ENV_API_KEY_NAMES) {
const value = process.env[name]?.trim();
if (value) return value;
}
return null;
}
/**
* Synthetic credentials for headless / Docker operators who inject
* GEMINI_API_KEY (or GOOGLE_API_KEY) instead of adding a dashboard connection.
*/
export function buildGeminiEnvCredentials(
providerId: string,
options: {
forcedConnectionId?: string | null;
allowedConnections?: string[] | null;
excludedConnectionIds?: Iterable<string> | null;
} = {}
): GeminiEnvCredentials | null {
if (!isGeminiCredentialProvider(providerId)) return null;
const forced =
typeof options.forcedConnectionId === "string" && options.forcedConnectionId.trim().length > 0
? options.forcedConnectionId.trim()
: null;
if (forced && forced !== GEMINI_ENV_CONNECTION_ID) return null;
const allowed = options.allowedConnections;
if (Array.isArray(allowed) && allowed.length > 0 && !allowed.includes(GEMINI_ENV_CONNECTION_ID)) {
return null;
}
if (options.excludedConnectionIds) {
for (const excluded of options.excludedConnectionIds) {
if (excluded === GEMINI_ENV_CONNECTION_ID) return null;
}
}
const apiKey = readGeminiEnvApiKey();
if (!apiKey) return null;
return {
apiKey,
accessToken: null,
connectionId: GEMINI_ENV_CONNECTION_ID,
id: GEMINI_ENV_CONNECTION_ID,
provider: providerId,
authType: "apikey",
defaultModel: null,
};
}