mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 03:12:36 +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>
88 lines
3.3 KiB
TypeScript
88 lines
3.3 KiB
TypeScript
/**
|
|
* Jina Search Foundation native embedding items (api.jina.ai EmbeddingsV5Request).
|
|
*
|
|
* Official 2026 input shapes (OpenAPI 2026.07.27):
|
|
* TextDoc { text }
|
|
* ImageDoc { image } URL or base64 / data URI
|
|
* AudioDoc { audio }
|
|
* VideoDoc { video }
|
|
* PDFDoc { pdf } single input only upstream; we still accept it in a list
|
|
* MergedContentGroup { content: [TextDoc|ImageDoc|AudioDoc|VideoDoc, ...] }
|
|
*
|
|
* These are not OmniRoute's canonical `{ type, source }` items. For jina-ai
|
|
* they must be forwarded intact — do not stringify, do not fetch image URLs
|
|
* into data URIs. Jina fetches public media itself.
|
|
*/
|
|
|
|
export const JINA_NATIVE_MEDIA_KEYS = ["text", "image", "audio", "video", "pdf"] as const;
|
|
export type JinaNativeMediaKey = (typeof JINA_NATIVE_MEDIA_KEYS)[number];
|
|
|
|
const NATIVE_KEY_TO_MODALITY: Record<JinaNativeMediaKey, "text" | "image" | "audio" | "video" | "document"> =
|
|
{
|
|
text: "text",
|
|
image: "image",
|
|
audio: "audio",
|
|
video: "video",
|
|
pdf: "document",
|
|
};
|
|
|
|
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
/** OmniRoute canonical structured item — leave those on the translator path. */
|
|
export function isCanonicalEmbeddingItem(value: unknown): boolean {
|
|
return isPlainObject(value) && "type" in value && typeof value.type === "string";
|
|
}
|
|
|
|
export function isJinaNativeDoc(value: unknown): boolean {
|
|
if (!isPlainObject(value) || isCanonicalEmbeddingItem(value)) return false;
|
|
if ("content" in value && Array.isArray(value.content)) return false;
|
|
const present = JINA_NATIVE_MEDIA_KEYS.filter((key) => key in value);
|
|
if (present.length !== 1) return false;
|
|
return typeof value[present[0]] === "string" && String(value[present[0]]).trim().length > 0;
|
|
}
|
|
|
|
export function isJinaMergedContentGroup(value: unknown): boolean {
|
|
if (!isPlainObject(value) || isCanonicalEmbeddingItem(value)) return false;
|
|
if (!Array.isArray(value.content) || value.content.length === 0) return false;
|
|
return value.content.every((item) => isJinaNativeDoc(item) && !("pdf" in (item as object)));
|
|
}
|
|
|
|
export function isJinaNativeEmbeddingItem(value: unknown): boolean {
|
|
return isJinaNativeDoc(value) || isJinaMergedContentGroup(value);
|
|
}
|
|
|
|
/**
|
|
* True when the request already uses Jina's documented multimodal contract
|
|
* (single doc, mixed string+doc batch, or fused content groups).
|
|
*/
|
|
export function isJinaNativeEmbeddingInput(input: unknown): boolean {
|
|
if (isJinaNativeEmbeddingItem(input)) return true;
|
|
if (!Array.isArray(input)) return false;
|
|
return input.some((item) => isJinaNativeEmbeddingItem(item));
|
|
}
|
|
|
|
export function collectJinaNativeModalities(
|
|
input: unknown
|
|
): Array<"text" | "image" | "audio" | "video" | "document"> {
|
|
const found = new Set<"text" | "image" | "audio" | "video" | "document">();
|
|
|
|
const visit = (value: unknown) => {
|
|
if (isJinaMergedContentGroup(value)) {
|
|
for (const item of (value as { content: unknown[] }).content) visit(item);
|
|
return;
|
|
}
|
|
if (!isJinaNativeDoc(value)) return;
|
|
const key = JINA_NATIVE_MEDIA_KEYS.find((mediaKey) => mediaKey in (value as object));
|
|
if (key) found.add(NATIVE_KEY_TO_MODALITY[key]);
|
|
};
|
|
|
|
if (Array.isArray(input)) {
|
|
for (const item of input) visit(item);
|
|
} else {
|
|
visit(input);
|
|
}
|
|
return [...found];
|
|
}
|