mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-23 15:42:12 +03:00
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>
This commit is contained in:
@@ -35,7 +35,7 @@ const FETCH_PROVIDERS: FetchProviderDef[] = [
|
||||
},
|
||||
{
|
||||
id: "jina-reader",
|
||||
name: "Jina Reader",
|
||||
name: "Jina Reader (r.jina.ai)",
|
||||
costPerQuery: 0.0005,
|
||||
freeMonthlyQuota: 1000,
|
||||
fetchFormats: ["markdown", "text"],
|
||||
|
||||
79
src/app/api/v1/classify/route.ts
Normal file
79
src/app/api/v1/classify/route.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { handleJinaFoundationProxy } from "@omniroute/open-sse/handlers/jinaFoundation.ts";
|
||||
import {
|
||||
getProviderCredentialsWithQuotaPreflight,
|
||||
clearRecoveredProviderState,
|
||||
} from "@/sse/services/auth";
|
||||
import { withInjectionGuard } from "@/middleware/promptInjectionGuard";
|
||||
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
||||
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
|
||||
import { v1ClassifySchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import {
|
||||
isAllRateLimitedCredentials,
|
||||
rateLimitedProviderResponse,
|
||||
} from "@/app/api/v1/_shared/rateLimit";
|
||||
import { JINA_FOUNDATION_BASE_URL, JINA_FOUNDATION_PROVIDER_ID } from "@/lib/providers/jina";
|
||||
|
||||
/**
|
||||
* Handle CORS preflight
|
||||
*/
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/classify — Jina zero/few-shot classification.
|
||||
*
|
||||
* Proxies to https://api.jina.ai/v1/classify using jina-ai dashboard
|
||||
* credentials (or JINA_AI_API_KEY when no dashboard key exists).
|
||||
*/
|
||||
async function postHandler(request: Request) {
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
} catch {
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body");
|
||||
}
|
||||
|
||||
const validation = validateBody(v1ClassifySchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, validation.error.message);
|
||||
}
|
||||
const body = validation.data;
|
||||
const model = typeof body.model === "string" ? body.model : undefined;
|
||||
|
||||
const policy = await enforceApiKeyPolicy(request, model || "jina-ai/classify");
|
||||
if (policy.rejection) return policy.rejection;
|
||||
|
||||
const credentials = await getProviderCredentialsWithQuotaPreflight(JINA_FOUNDATION_PROVIDER_ID);
|
||||
if (!credentials) {
|
||||
return errorResponse(
|
||||
HTTP_STATUS.BAD_REQUEST,
|
||||
`No credentials for provider: ${JINA_FOUNDATION_PROVIDER_ID}`
|
||||
);
|
||||
}
|
||||
if (isAllRateLimitedCredentials(credentials)) {
|
||||
return rateLimitedProviderResponse(JINA_FOUNDATION_PROVIDER_ID, credentials);
|
||||
}
|
||||
|
||||
const response = await handleJinaFoundationProxy({
|
||||
path: "/v1/classify",
|
||||
upstreamUrl: `${JINA_FOUNDATION_BASE_URL}/v1/classify`,
|
||||
body,
|
||||
credentials,
|
||||
provider: JINA_FOUNDATION_PROVIDER_ID,
|
||||
model: model || null,
|
||||
});
|
||||
if (response?.ok) {
|
||||
await clearRecoveredProviderState(credentials);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
export const POST = withInjectionGuard(postHandler);
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import {
|
||||
getAllSearchProviders,
|
||||
getSearchProvider,
|
||||
resolveSearchProvider,
|
||||
selectProvider,
|
||||
supportsSearchType,
|
||||
SEARCH_PROVIDERS,
|
||||
@@ -129,7 +130,7 @@ async function postHandler(request: Request, context: unknown) {
|
||||
|
||||
// Resolve provider and credentials
|
||||
if (body.provider) {
|
||||
const explicitProvider = getSearchProvider(body.provider);
|
||||
const explicitProvider = resolveSearchProvider(body.provider);
|
||||
if (!explicitProvider) {
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, `Unknown search provider: ${body.provider}`);
|
||||
}
|
||||
|
||||
79
src/app/api/v1/segment/route.ts
Normal file
79
src/app/api/v1/segment/route.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { handleJinaFoundationProxy } from "@omniroute/open-sse/handlers/jinaFoundation.ts";
|
||||
import {
|
||||
getProviderCredentialsWithQuotaPreflight,
|
||||
clearRecoveredProviderState,
|
||||
} from "@/sse/services/auth";
|
||||
import { withInjectionGuard } from "@/middleware/promptInjectionGuard";
|
||||
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
||||
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
|
||||
import { v1SegmentSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import {
|
||||
isAllRateLimitedCredentials,
|
||||
rateLimitedProviderResponse,
|
||||
} from "@/app/api/v1/_shared/rateLimit";
|
||||
import { JINA_FOUNDATION_PROVIDER_ID, JINA_SEGMENT_BASE_URL } from "@/lib/providers/jina";
|
||||
|
||||
/**
|
||||
* Handle CORS preflight
|
||||
*/
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/segment — Jina segmenter (tokenize / chunk).
|
||||
*
|
||||
* Proxies to https://segment.jina.ai/ using the same jina-ai credentials as
|
||||
* embeddings and classify. Segment lives on a dedicated host; the UI card is
|
||||
* still Foundation API, not Reader.
|
||||
*/
|
||||
async function postHandler(request: Request) {
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
} catch {
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body");
|
||||
}
|
||||
|
||||
const validation = validateBody(v1SegmentSchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, validation.error.message);
|
||||
}
|
||||
const body = validation.data;
|
||||
|
||||
const policy = await enforceApiKeyPolicy(request, "jina-ai/segment");
|
||||
if (policy.rejection) return policy.rejection;
|
||||
|
||||
const credentials = await getProviderCredentialsWithQuotaPreflight(JINA_FOUNDATION_PROVIDER_ID);
|
||||
if (!credentials) {
|
||||
return errorResponse(
|
||||
HTTP_STATUS.BAD_REQUEST,
|
||||
`No credentials for provider: ${JINA_FOUNDATION_PROVIDER_ID}`
|
||||
);
|
||||
}
|
||||
if (isAllRateLimitedCredentials(credentials)) {
|
||||
return rateLimitedProviderResponse(JINA_FOUNDATION_PROVIDER_ID, credentials);
|
||||
}
|
||||
|
||||
const response = await handleJinaFoundationProxy({
|
||||
path: "/v1/segment",
|
||||
upstreamUrl: `${JINA_SEGMENT_BASE_URL}/`,
|
||||
body,
|
||||
credentials,
|
||||
provider: JINA_FOUNDATION_PROVIDER_ID,
|
||||
model: "segment",
|
||||
});
|
||||
if (response?.ok) {
|
||||
await clearRecoveredProviderState(credentials);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
export const POST = withInjectionGuard(postHandler);
|
||||
Reference in New Issue
Block a user