@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
218 lines (217 loc) • 9.2 kB
JavaScript
import { JinaModels } from "../constants/enums.js";
import { BaseProvider } from "../core/baseProvider.js";
import { isNeuroLink } from "../neurolink.js";
import { createProxyFetch } from "../proxy/proxyFetch.js";
import { AuthenticationError, InvalidModelError, ProviderError, RateLimitError, } from "../types/index.js";
import { logger } from "../utils/logger.js";
import { createJinaConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
const JINA_DEFAULT_BASE_URL = "https://api.jina.ai/v1";
const REQUEST_TIMEOUT_MS = 60_000;
const getJinaApiKey = () => validateApiKey(createJinaConfig());
const getDefaultJinaModel = () => getProviderModel("JINA_MODEL", JinaModels.JINA_EMBEDDINGS_V3);
/**
* Jina AI Provider — embeddings + reranking.
*
* Native API at api.jina.ai/v1. Chat / streaming / tool calling are not
* supported. Use `embed()` / `embedMany()` for embeddings, or call
* `rerank()` directly for retrieval reranking (Jina's strength).
*
* @see https://jina.ai/embeddings/
*/
export class JinaProvider extends BaseProvider {
apiKey;
baseURL;
proxyFetch;
constructor(modelName, sdk, _region, credentials) {
const validatedNeurolink = isNeuroLink(sdk) ? sdk : undefined;
super(modelName, "jina", validatedNeurolink);
const overrideKey = credentials?.apiKey?.trim();
this.apiKey =
overrideKey && overrideKey.length > 0 ? overrideKey : getJinaApiKey();
this.baseURL =
credentials?.baseURL ??
process.env.JINA_BASE_URL ??
JINA_DEFAULT_BASE_URL;
this.proxyFetch = createProxyFetch();
logger.debug("Jina Provider initialized (embeddings + reranking)", {
modelName: this.modelName,
baseURL: this.baseURL,
});
}
getProviderName() {
return this.providerName;
}
getDefaultModel() {
return getDefaultJinaModel();
}
supportsTools() {
return false;
}
getDefaultEmbeddingModel() {
return getDefaultJinaModel();
}
getAISDKModel() {
throw new Error("Jina AI is an embeddings + reranking provider; chat completions are not available. Use `embed()` / `embedMany()` / `rerank()`.");
}
async executeStream(_options, _analysisSchema) {
throw new Error("Jina AI is an embeddings + reranking provider; streaming chat is not available.");
}
formatProviderError(error) {
const message = error instanceof Error
? error.message
: typeof error === "string"
? error
: "Unknown error";
if (message.includes("401") ||
message.toLowerCase().includes("unauthorized")) {
return new AuthenticationError("Invalid Jina AI API key. Get one at https://jina.ai/?sui=apikey", "jina");
}
if (message.includes("429") ||
message.toLowerCase().includes("rate limit")) {
return new RateLimitError("Jina AI rate limit exceeded. Back off and retry.", "jina");
}
if (message.includes("404") ||
message.toLowerCase().includes("model_not_found")) {
return new InvalidModelError(`Jina AI model '${this.modelName}' not found. See https://jina.ai/embeddings/`, "jina");
}
return new ProviderError(`Jina AI error: ${message}`, "jina");
}
async embed(text, modelName) {
const vectors = await this.callEmbeddings([text], modelName);
if (!vectors[0]) {
throw new Error("Jina AI returned no embedding for the provided text");
}
return vectors[0];
}
async embedMany(texts, modelName) {
if (texts.length === 0) {
return [];
}
return this.callEmbeddings(texts, modelName);
}
/**
* Rerank a list of documents against a query.
*
* Returns the documents sorted by relevance (highest first), with
* score and original index preserved so callers can map back.
*
* Note: not exposed on `BaseProvider` — accessed by casting to
* `JinaProvider` or via the dedicated rerank route on the public API
* (`POST /api/agent/rerank` in the server module, when added).
*
* Per-call credentials can be supplied via `options.credentials?.jina`,
* overriding the instance-level credentials for this request only.
*/
async rerank(query, documents, options = {}) {
if (documents.length === 0) {
return [];
}
const model = options.model ?? JinaModels.JINA_RERANKER_V2_BASE_MULTILINGUAL;
// Resolve per-call credentials first, then fall back to instance-level.
const perCallCreds = options.credentials;
const effectiveApiKey = perCallCreds?.apiKey?.trim() || this.apiKey;
const effectiveBaseURL = perCallCreds?.baseURL || this.baseURL;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
let response;
try {
response = await this.proxyFetch(`${effectiveBaseURL}/rerank`, {
method: "POST",
headers: {
Authorization: `Bearer ${effectiveApiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model,
query,
documents,
top_n: options.topN ?? documents.length,
}),
signal: controller.signal,
});
}
catch (err) {
if (err instanceof Error && err.name === "AbortError") {
throw this.formatProviderError(new Error(`Jina rerank request timed out after ${REQUEST_TIMEOUT_MS / 1000}s`));
}
throw this.formatProviderError(err);
}
finally {
clearTimeout(timeoutId);
}
if (!response.ok) {
const text = await response.text();
throw this.formatProviderError(new Error(`Jina rerank failed: ${response.status} — ${text}`));
}
const data = (await response.json());
return (data.results ?? []).map((r) => ({
index: r.index,
score: r.relevance_score,
document: documents[r.index] ?? r.document?.text ?? "",
}));
}
async callEmbeddings(inputs, modelName, credentials) {
const model = modelName ?? this.modelName;
// Resolve per-call credentials first, then fall back to instance-level.
const effectiveApiKey = credentials?.apiKey?.trim() || this.apiKey;
const effectiveBaseURL = credentials?.baseURL || this.baseURL;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
let response;
try {
response = await this.proxyFetch(`${effectiveBaseURL}/embeddings`, {
method: "POST",
headers: {
Authorization: `Bearer ${effectiveApiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
input: inputs,
model,
}),
signal: controller.signal,
});
}
catch (err) {
if (err instanceof Error && err.name === "AbortError") {
throw this.formatProviderError(new Error(`Jina embeddings request timed out after ${REQUEST_TIMEOUT_MS / 1000}s`));
}
throw this.formatProviderError(err);
}
finally {
clearTimeout(timeoutId);
}
if (!response.ok) {
const text = await response.text();
throw this.formatProviderError(new Error(`Jina embeddings failed: ${response.status} — ${text}`));
}
const data = (await response.json());
if (!data.data || data.data.length === 0) {
throw new Error("Jina embeddings response missing data");
}
// Validate that the response covers all requested inputs.
if (data.data.length !== inputs.length) {
throw new Error(`Jina embeddings response count mismatch: expected ${inputs.length}, got ${data.data.length}`);
}
// Sort by index and verify sequential coverage (0 … n-1).
const sorted = data.data.slice().sort((a, b) => a.index - b.index);
for (let i = 0; i < sorted.length; i++) {
if (sorted[i].index !== i) {
throw new Error(`Jina embeddings response has unexpected index ordering: position ${i} has index ${sorted[i].index}`);
}
}
return sorted.map((d) => d.embedding);
}
async validateConfiguration() {
return typeof this.apiKey === "string" && this.apiKey.trim().length > 0;
}
getConfiguration() {
return {
provider: this.providerName,
model: this.modelName,
defaultModel: getDefaultJinaModel(),
baseURL: this.baseURL,
};
}
}
export default JinaProvider;