UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

167 lines (166 loc) • 7.57 kB
import { VoyageModels } from "../constants/enums.js"; import { BaseProvider } from "../core/baseProvider.js"; import { isNeuroLink } from "../neurolink.js"; import { parseIndexedEmbeddingsResponse } from "./embeddingResponseParsing.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(input, modelName) { if (typeof input !== "string" && input.image) { throw new ProviderError(`${this.providerName} does not support image embeddings; provide text input`, this.providerName); } const text = typeof input === "string" ? input : (input.text ?? ""); 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()); // Sorts by index (Voyage returns out-of-order under some conditions) and // verifies complete 0..n-1 coverage (Voyage may return partial results // in edge-case error scenarios). return parseIndexedEmbeddingsResponse(data, inputs.length, "Voyage", (message) => new ProviderError(message, "voyage")); } 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, }; } }