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

187 lines (186 loc) • 8.4 kB
import { CohereModels } from "../constants/enums.js"; import { AuthenticationError, InvalidModelError, ProviderError, RateLimitError, } from "../types/index.js"; import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../utils/errorClassifier.js"; import { logger } from "../utils/logger.js"; import { redactUrlCredentials } from "../utils/logSanitize.js"; import { createProxyFetch } from "../proxy/proxyFetch.js"; import { createCohereConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js"; import { createTimeoutController } from "../utils/timeout.js"; import { stripTrailingSlash } from "./openaiChatCompletionsClient.js"; import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js"; const COHERE_DEFAULT_BASE_URL = "https://api.cohere.com/compatibility/v1"; const getCohereApiKey = () => validateApiKey(createCohereConfig()); const getDefaultCohereModel = () => getProviderModel("COHERE_MODEL", CohereModels.COMMAND_R_PLUS); /** * Cohere Provider — direct HTTP, no AI SDK. * * Routes Command R / Command R+ chat completions through Cohere's * OpenAI-compatible endpoint at /compatibility/v1. All request/stream/ * tool-loop orchestration lives in `OpenAIChatCompletionsProvider`; this * class only declares configuration and provider-specific error mapping. * * Embed v3 is exposed via `embed()` / `embedMany()` backed by a native * POST to /v2/embed (the compatibility path is chat-only). * * @see https://docs.cohere.com/docs/compatibility-api * @see https://docs.cohere.com/reference/embed */ export class CohereProvider extends OpenAIChatCompletionsProvider { constructor(modelName, sdk, _region, credentials) { const overrideApiKey = credentials?.apiKey?.trim(); const apiKey = overrideApiKey && overrideApiKey.length > 0 ? overrideApiKey : getCohereApiKey(); const baseURL = credentials?.baseURL?.trim() || process.env.COHERE_BASE_URL?.trim() || COHERE_DEFAULT_BASE_URL; super("cohere", modelName, sdk, { baseURL, apiKey }); logger.debug("Cohere Provider initialized", { modelName: this.modelName, providerName: this.providerName, baseURL: redactUrlCredentials(this.config.baseURL), }); } getProviderName() { return "cohere"; } getDefaultModel() { return getDefaultCohereModel(); } getFallbackModelName() { return CohereModels.COMMAND_R; } getFallbackModels() { return [ CohereModels.COMMAND_A, CohereModels.COMMAND_A_REASONING, CohereModels.COMMAND_R_PLUS, CohereModels.COMMAND_R, CohereModels.COMMAND_R7B, ]; } formatProviderError(error) { const rules = [ // "invalid api token" is checked lowercase (case-sensitive substring, as // in the original) — deliberately NOT normalized to match the family's // more common "Invalid API key" capitalization, since Cohere's actual // wire message differs. { match: (ctx) => /invalid api token|Authentication|401|invalid_api_token/.test(ctx.message), errorClass: AuthenticationError, message: "Invalid Cohere API key. Check COHERE_API_KEY. Get one at https://dashboard.cohere.com/api-keys", }, { match: (ctx) => /rate limit|429/.test(ctx.message), errorClass: RateLimitError, message: "Cohere rate limit exceeded. Back off and retry.", }, { match: (ctx) => /model_not_found|404/.test(ctx.message), errorClass: InvalidModelError, message: () => `Cohere model '${this.modelName}' not found. Use command-r, command-r-plus, or command-r7b-12-2024.`, }, { match: (ctx) => /trial limit|trial_limit/.test(ctx.message), errorClass: ProviderError, message: "Cohere trial usage limit exceeded. Upgrade at https://dashboard.cohere.com/billing.", }, ...DEFAULT_ERROR_RULES, ]; return classifyProviderError(error, rules, this.providerName, this.modelName); } async validateConfiguration() { return (typeof this.config.apiKey === "string" && this.config.apiKey.trim().length > 0); } getConfiguration() { return { provider: this.providerName, model: this.modelName, defaultModel: getDefaultCohereModel(), baseURL: this.config.baseURL, }; } /** * Default embedding model for Cohere. */ getDefaultEmbeddingModel() { return CohereModels.EMBED_ENGLISH_V3; } /** * Generate an embedding for a single text via Cohere's native /v2/embed * endpoint. Returns the float[] embedding vector. * * The shared OpenAI-compatible /compatibility/v1 path is chat-only; embed * lives on the native API (POST /v2/embed). Documented at * https://docs.cohere.com/reference/embed. */ 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.embedMany([text], modelName); if (!vectors[0]) { throw new ProviderError("Cohere /v2/embed returned no embeddings.", "cohere"); } return vectors[0]; } /** * Batch embedding via Cohere's native /v2/embed endpoint. Cohere caps at * 96 inputs per request; larger batches are chunked. * * Partial failures are not surfaced: if any chunk request fails the whole * call rejects and already-embedded chunks are discarded — callers should * retry the full input. */ async embedMany(texts, modelName) { if (texts.length === 0) { return []; } const model = modelName ?? this.getDefaultEmbeddingModel(); // Strip the compatibility suffix to reach the native API root. const baseUrl = stripTrailingSlash(this.config.baseURL.replace(/\/compatibility\/v\d+\/?$/, "")); const url = `${baseUrl}/v2/embed`; const BATCH_SIZE = 96; const results = []; const fetchImpl = createProxyFetch(); for (let i = 0; i < texts.length; i += BATCH_SIZE) { const batch = texts.slice(i, i + BATCH_SIZE); const timeoutController = createTimeoutController(30_000, this.providerName, "generate"); try { const response = await fetchImpl(url, { method: "POST", headers: { Authorization: `Bearer ${this.config.apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ model, texts: batch, input_type: "search_document", embedding_types: ["float"], }), ...(timeoutController?.controller.signal ? { signal: timeoutController.controller.signal } : {}), }); if (!response.ok) { const body = await response.text().catch(() => ""); throw this.formatProviderError(new Error(`Cohere /v2/embed failed: ${response.status} — ${body.slice(0, 500)}`)); } const json = (await response.json()); const floatVecs = json.embeddings?.float ?? (Array.isArray(json.embeddings) ? json.embeddings : undefined); if (!floatVecs || floatVecs.length !== batch.length) { throw new ProviderError(`Cohere /v2/embed returned ${floatVecs?.length ?? 0} embeddings for ${batch.length} inputs.`, "cohere"); } results.push(...floatVecs); } finally { timeoutController?.cleanup(); } } return results; } }