@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
177 lines (176 loc) • 7.99 kB
JavaScript
import { VoyageModels } 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 { withTimeout } from "../utils/errorHandling.js";
import { logger } from "../utils/logger.js";
import { createVoyageConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
const VOYAGE_DEFAULT_BASE_URL = "https://api.voyageai.com/v1";
const REQUEST_TIMEOUT_MS = 60_000;
const getVoyageApiKey = () => validateApiKey(createVoyageConfig());
const getDefaultVoyageModel = () => getProviderModel("VOYAGE_MODEL", VoyageModels.VOYAGE_3_5);
/**
* Voyage AI Provider — embedding-only.
*
* Top-tier RAG embedder. Native API at api.voyageai.com/v1/embeddings.
* Chat / streaming / tool calling are not supported — `executeStream` and
* `getAISDKModel` throw a friendly error so callers get an actionable
* message instead of a runtime crash deep in the streaming layer.
*
* @see https://docs.voyageai.com/docs/embeddings
*/
export class VoyageProvider extends BaseProvider {
apiKey;
baseURL;
proxyFetch;
constructor(modelName, sdk, _region, credentials) {
const validatedNeurolink = isNeuroLink(sdk) ? sdk : undefined;
super(modelName, "voyage", validatedNeurolink);
const overrideKey = credentials?.apiKey?.trim();
this.apiKey =
overrideKey && overrideKey.length > 0 ? overrideKey : getVoyageApiKey();
this.baseURL =
credentials?.baseURL ??
process.env.VOYAGE_BASE_URL ??
VOYAGE_DEFAULT_BASE_URL;
this.proxyFetch = createProxyFetch();
logger.debug("Voyage Provider initialized (embeddings only)", {
modelName: this.modelName,
baseURL: this.baseURL,
});
}
// ===== Required abstract overrides =====
getProviderName() {
return this.providerName;
}
getDefaultModel() {
return getDefaultVoyageModel();
}
supportsTools() {
return false;
}
getDefaultEmbeddingModel() {
return getDefaultVoyageModel();
}
/**
* Voyage is embedding-only — chat models do not exist on this endpoint.
* Caller surface stays consistent: returns an `AbortError`-shaped failure
* via `BaseProvider.handleProviderError`, not a TypeScript-level cast.
*/
getAISDKModel() {
throw new ProviderError("Voyage AI is an embedding-only provider; chat completions are not available. Use `embed()` or `embedMany()` instead, or pick a different provider for `generate()` / `stream()`.", "voyage");
}
async executeStream(_options, _analysisSchema) {
throw new ProviderError("Voyage AI is an embedding-only provider; streaming chat is not available. Use `embed()` / `embedMany()`, or pick another provider for `stream()`.", "voyage");
}
formatProviderError(error) {
const message = error instanceof Error
? error.message
: typeof error === "string"
? error
: "Unknown error";
if (message.includes("401") ||
message.toLowerCase().includes("unauthorized") ||
message.includes("invalid_api_key")) {
return new AuthenticationError("Invalid Voyage AI API key. Get one at https://dash.voyageai.com/api-keys", "voyage");
}
if (message.includes("429") ||
message.toLowerCase().includes("rate limit")) {
return new RateLimitError("Voyage AI rate limit exceeded. Back off and retry.", "voyage");
}
if (message.includes("404") ||
message.toLowerCase().includes("model_not_found")) {
return new InvalidModelError(`Voyage AI model '${this.modelName}' not found. Browse https://docs.voyageai.com/docs/embeddings`, "voyage");
}
return new ProviderError(`Voyage AI error: ${message}`, "voyage");
}
// ===== Embedding implementations =====
async embed(text, modelName) {
const vectors = await this.callEmbeddings([text], modelName);
if (!vectors[0]) {
throw new ProviderError("Voyage AI returned no embedding for the provided text", "voyage");
}
return vectors[0];
}
async embedMany(texts, modelName) {
if (texts.length === 0) {
return [];
}
// Voyage AI's /embeddings endpoint accepts up to 128 inputs per request.
// Split larger payloads into sequential batches to avoid API rejection.
const VOYAGE_MAX_BATCH_SIZE = 128;
const out = [];
for (let i = 0; i < texts.length; i += VOYAGE_MAX_BATCH_SIZE) {
const batch = texts.slice(i, i + VOYAGE_MAX_BATCH_SIZE);
const vectors = await this.callEmbeddings(batch, modelName);
out.push(...vectors);
}
return out;
}
/**
* POST /embeddings — Voyage accepts up to 128 inputs per request.
* Caller batches above that (see `embedMany`).
*/
async callEmbeddings(inputs, modelName) {
const model = modelName ?? this.modelName;
let response;
try {
response = await withTimeout(this.proxyFetch(`${this.baseURL}/embeddings`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
input: inputs,
model,
}),
}), REQUEST_TIMEOUT_MS, new ProviderError(`Voyage embeddings request timed out after ${REQUEST_TIMEOUT_MS / 1000}s`, "voyage"));
}
catch (err) {
// Re-throw typed provider errors produced by withTimeout (ProviderError
// subclasses: AuthenticationError, RateLimitError, InvalidModelError)
// so they are not double-wrapped by formatProviderError.
if (err instanceof ProviderError) {
throw err;
}
throw this.formatProviderError(err);
}
if (!response.ok) {
const text = await response.text();
throw this.formatProviderError(new Error(`Voyage embeddings failed: ${response.status} — ${text}`));
}
const data = (await response.json());
if (!data.data || data.data.length === 0) {
throw new ProviderError("Voyage embeddings response missing data", "voyage");
}
// Validate that the response covers all requested inputs.
// Voyage may return partial results in edge-case error scenarios.
if (data.data.length !== inputs.length) {
throw new ProviderError(`Voyage embeddings response count mismatch: expected ${inputs.length}, got ${data.data.length}`, "voyage");
}
// Sort by index (Voyage returns out-of-order under some conditions)
// and drop to a flat number[][] result.
const sorted = data.data.slice().sort((a, b) => a.index - b.index);
// Verify that index coverage is complete (0 … n-1).
for (let i = 0; i < sorted.length; i++) {
if (sorted[i].index !== i) {
throw new ProviderError(`Voyage embeddings response has unexpected index ordering: position ${i} has index ${sorted[i].index}`, "voyage");
}
}
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: getDefaultVoyageModel(),
baseURL: this.baseURL,
};
}
}
export default VoyageProvider;