From 37b96ca70ae03c41817f725f138639f50b8e06d3 Mon Sep 17 00:00:00 2001 From: Elias TOURNEUX Date: Fri, 31 Oct 2025 14:10:29 -0400 Subject: [PATCH] feat(community): Add OVHcloud AI Endpoints provider --- .../models/chat/integration_ovhcloud.ts | 7 + .../models/embeddings/ovhcloud.ts | 15 ++ .../chat_models/tests/universal.int.test.ts | 11 ++ .../src/chat_models/universal.ts | 5 + libs/langchain-classic/src/hub/base.ts | 2 + libs/langchain-community/.gitignore | 8 + libs/langchain-community/package.json | 22 +++ .../src/chat_models/ovhcloud.ts | 158 ++++++++++++++++++ .../tests/chatovhcloud.int.test.ts | 103 ++++++++++++ .../src/embeddings/ovhcloud.ts | 53 ++++++ .../src/embeddings/tests/ovhcloud.int.test.ts | 19 +++ .../src/load/import_map.ts | 2 + .../src/load/import_type.ts | 1 + .../chat_models/tests/universal.int.test.ts | 11 ++ libs/langchain/src/chat_models/universal.ts | 6 + libs/langchain/src/hub/base.ts | 2 + 16 files changed, 425 insertions(+) create mode 100644 examples/src/langchain-classic/models/chat/integration_ovhcloud.ts create mode 100644 examples/src/langchain-classic/models/embeddings/ovhcloud.ts create mode 100644 libs/langchain-community/src/chat_models/ovhcloud.ts create mode 100644 libs/langchain-community/src/chat_models/tests/chatovhcloud.int.test.ts create mode 100644 libs/langchain-community/src/embeddings/ovhcloud.ts create mode 100644 libs/langchain-community/src/embeddings/tests/ovhcloud.int.test.ts diff --git a/examples/src/langchain-classic/models/chat/integration_ovhcloud.ts b/examples/src/langchain-classic/models/chat/integration_ovhcloud.ts new file mode 100644 index 000000000000..0c6814e362fb --- /dev/null +++ b/examples/src/langchain-classic/models/chat/integration_ovhcloud.ts @@ -0,0 +1,7 @@ +import { ChatOVHCloudAIEndpoints } from "@langchain/community/chat_models/ovhcloud"; + +const model = new ChatOVHCloudAIEndpoints({ + // In Node.js defaults to process.env.OVHCLOUD_AI_ENDPOINTS_API_KEY + apiKey: "your-api-key", + model: "your-model-name", +}); diff --git a/examples/src/langchain-classic/models/embeddings/ovhcloud.ts b/examples/src/langchain-classic/models/embeddings/ovhcloud.ts new file mode 100644 index 000000000000..e21017bd2719 --- /dev/null +++ b/examples/src/langchain-classic/models/embeddings/ovhcloud.ts @@ -0,0 +1,15 @@ +import { OVHcloudAIEndpointsEmbeddings } from "@langchain/community/embeddings/ovhcloud"; + +/* Embed queries */ +const ovhcloudEmbeddings = new OVHcloudAIEndpointsEmbeddings(); +const res = await ovhcloudEmbeddings.embedQuery("Hello world"); + +console.log(res); + +/* Embed documents */ +const documentRes = await ovhcloudEmbeddings.embedDocuments([ + "Hello world", + "Bye bye", +]); + +console.log(documentRes); diff --git a/libs/langchain-classic/src/chat_models/tests/universal.int.test.ts b/libs/langchain-classic/src/chat_models/tests/universal.int.test.ts index 33a8c79fead9..b728b1cdada4 100644 --- a/libs/langchain-classic/src/chat_models/tests/universal.int.test.ts +++ b/libs/langchain-classic/src/chat_models/tests/universal.int.test.ts @@ -577,6 +577,17 @@ describe("Works with all model providers", () => { expect(perplexityResult).toBeDefined(); expect(perplexityResult.content.length).toBeGreaterThan(0); }); + + it("Can invoke ovhcloud", async () => { + const ovhcloud = await initChatModel(undefined, { + modelProvider: "ovhcloud", + temperature: 0, + }); + + const ovhcloudResult = await ovhcloud.invoke("what's your name"); + expect(ovhcloudResult).toBeDefined(); + expect(ovhcloudResult.content.length).toBeGreaterThan(0); + }); }); test("Is compatible with agents", async () => { diff --git a/libs/langchain-classic/src/chat_models/universal.ts b/libs/langchain-classic/src/chat_models/universal.ts index 1b6a70e527b4..076dc42383cf 100644 --- a/libs/langchain-classic/src/chat_models/universal.ts +++ b/libs/langchain-classic/src/chat_models/universal.ts @@ -120,6 +120,11 @@ export const MODEL_PROVIDER_CONFIG = { className: "ChatPerplexity", hasCircularDependency: true, }, + ovhcloud: { + package: "@langchain/community/chat_models/ovhcloud", + className: "ChatOVHCloudAIEndpoints", + hasCircularDependency: true, + }, } as const; const SUPPORTED_PROVIDERS = Object.keys( diff --git a/libs/langchain-classic/src/hub/base.ts b/libs/langchain-classic/src/hub/base.ts index 2f41c3403004..d0bbf62d0fb9 100644 --- a/libs/langchain-classic/src/hub/base.ts +++ b/libs/langchain-classic/src/hub/base.ts @@ -121,6 +121,8 @@ export function generateModelImportMap( importMapKey = "chat_models__fireworks"; } else if (modelLcName === "ChatGroq") { importMapKey = "chat_models__groq"; + } else if (modelLcName === "ChatOVHCloudAIEndpoints") { + importMapKey = "chat_models__ovhcloud"; } else { throw new Error("Received unsupported model class when pulling prompt."); } diff --git a/libs/langchain-community/.gitignore b/libs/langchain-community/.gitignore index 1002fb9f04da..7f367e6cfb6b 100644 --- a/libs/langchain-community/.gitignore +++ b/libs/langchain-community/.gitignore @@ -206,6 +206,10 @@ embeddings/ollama.cjs embeddings/ollama.js embeddings/ollama.d.ts embeddings/ollama.d.cts +embeddings/ovhcloud.cjs +embeddings/ovhcloud.js +embeddings/ovhcloud.d.ts +embeddings/ovhcloud.d.cts embeddings/premai.cjs embeddings/premai.js embeddings/premai.d.ts @@ -598,6 +602,10 @@ chat_models/ollama.cjs chat_models/ollama.js chat_models/ollama.d.ts chat_models/ollama.d.cts +chat_models/ovhcloud.cjs +chat_models/ovhcloud.js +chat_models/ovhcloud.d.ts +chat_models/ovhcloud.d.cts chat_models/perplexity.cjs chat_models/perplexity.js chat_models/perplexity.d.ts diff --git a/libs/langchain-community/package.json b/libs/langchain-community/package.json index b67dc13f73ce..2d204451f2fe 100644 --- a/libs/langchain-community/package.json +++ b/libs/langchain-community/package.json @@ -1184,6 +1184,17 @@ "default": "./dist/embeddings/minimax.cjs" } }, + "./embeddings/ovhcloud": { + "input": "./src/embeddings/ovhcloud.ts", + "import": { + "types": "./dist/embeddings/ovhcloud.d.ts", + "default": "./dist/embeddings/ovhcloud.js" + }, + "require": { + "types": "./dist/embeddings/ovhcloud.d.cts", + "default": "./dist/embeddings/ovhcloud.cjs" + } + }, "./embeddings/premai": { "input": "./src/embeddings/premai.ts", "import": { @@ -2086,6 +2097,17 @@ "default": "./dist/chat_models/novita.cjs" } }, + "./chat_models/ovhcloud": { + "input": "./src/chat_models/ovhcloud.ts", + "import": { + "types": "./dist/chat_models/ovhcloud.d.ts", + "default": "./dist/chat_models/ovhcloud.js" + }, + "require": { + "types": "./dist/chat_models/ovhcloud.d.cts", + "default": "./dist/chat_models/ovhcloud.cjs" + } + }, "./chat_models/perplexity": { "input": "./src/chat_models/perplexity.ts", "import": { diff --git a/libs/langchain-community/src/chat_models/ovhcloud.ts b/libs/langchain-community/src/chat_models/ovhcloud.ts new file mode 100644 index 000000000000..0d22b8783d41 --- /dev/null +++ b/libs/langchain-community/src/chat_models/ovhcloud.ts @@ -0,0 +1,158 @@ +import type { + BaseChatModelParams, + LangSmithParams, +} from "@langchain/core/language_models/chat_models"; +import { + type OpenAIClient, + type ChatOpenAICallOptions, + type OpenAIChatInput, + type OpenAICoreRequestOptions, + ChatOpenAICompletions, +} from "@langchain/openai"; + +import { getEnvironmentVariable } from "@langchain/core/utils/env"; + +type OVHCloudUnsupportedArgs = + | "frequencyPenalty" + | "presencePenalty" + | "logitBias" + | "functions"; + +type OVHCloudUnsupportedCallOptions = "functions" | "function_call"; + +export type ChatOVHCloudAIEndpointsCallOptions = Partial< + Omit +>; + +export interface ChatOVHCloudAIEndpointsInput + extends Omit, + BaseChatModelParams { + /** + * The OVHcloud API key to use for requests. + * @default process.env.OVHCLOUD_AI_ENDPOINTS_API_KEY + */ + apiKey?: string; +} + +/** + * OVHcloud AI Endpoints chat model integration. + * + * OVHcloud AI Endpoints is compatible with the OpenAI API. + * Base URL: https://oai.endpoints.kepler.ai.cloud.ovh.net/v1 + * + * Setup: + * Install `@langchain/community` and set an environment variable named `OVHCLOUD_AI_ENDPOINTS_API_KEY`. + * If no API key is provided, the model can still be used but with a rate limit. + * + * ```bash + * npm install @langchain/community + * export OVHCLOUD_AI_ENDPOINTS_API_KEY="your-api-key" + * ``` + * + * ## Constructor args + * + * ## Runtime args + * + * Runtime args can be passed as the second argument to any of the base runnable methods `.invoke`, `.stream`, etc. + */ +export class ChatOVHCloudAIEndpoints extends ChatOpenAICompletions { + static lc_name() { + return "ChatOVHCloudAIEndpoints"; + } + + _llmType() { + return "ovhcloud"; + } + + get lc_secrets(): { [key: string]: string } | undefined { + return { + apiKey: "OVHCLOUD_AI_ENDPOINTS_API_KEY", + }; + } + + lc_serializable = true; + + constructor( + fields?: Partial< + Omit + > & + BaseChatModelParams & { + /** + * The OVHcloud AI Endpoints API key to use. + */ + apiKey?: string; + } + ) { + const apiKey = + fields?.apiKey || getEnvironmentVariable("OVHCLOUD_AI_ENDPOINTS_API_KEY"); + + if (!apiKey) { + console.warn( + "OVHcloud AI Endpoints API key not found. You can use the model but with a rate limit. " + + "Set the OVHCLOUD_AI_ENDPOINTS_API_KEY environment variable or provide the key via 'apiKey' for unlimited access." + ); + } + + super({ + ...fields, + apiKey: apiKey || "", + configuration: { + baseURL: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1", + }, + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getLsParams(options: any): LangSmithParams { + const params = super.getLsParams(options); + params.ls_provider = "ovhcloud"; + return params; + } + + toJSON() { + const result = super.toJSON(); + + if ( + "kwargs" in result && + typeof result.kwargs === "object" && + result.kwargs != null + ) { + delete result.kwargs.openai_api_key; + delete result.kwargs.configuration; + } + + return result; + } + + async completionWithRetry( + request: OpenAIClient.Chat.ChatCompletionCreateParamsStreaming, + options?: OpenAICoreRequestOptions + ): Promise>; + + async completionWithRetry( + request: OpenAIClient.Chat.ChatCompletionCreateParamsNonStreaming, + options?: OpenAICoreRequestOptions + ): Promise; + + async completionWithRetry( + request: + | OpenAIClient.Chat.ChatCompletionCreateParamsStreaming + | OpenAIClient.Chat.ChatCompletionCreateParamsNonStreaming, + options?: OpenAICoreRequestOptions + ): Promise< + | AsyncIterable + | OpenAIClient.Chat.Completions.ChatCompletion + > { + // Remove arguments not supported by OVHcloud AI Endpoints endpoint + delete request.frequency_penalty; + delete request.presence_penalty; + delete request.logit_bias; + delete request.functions; + + if (request.stream === true) { + return super.completionWithRetry(request, options); + } + + return super.completionWithRetry(request, options); + } +} diff --git a/libs/langchain-community/src/chat_models/tests/chatovhcloud.int.test.ts b/libs/langchain-community/src/chat_models/tests/chatovhcloud.int.test.ts new file mode 100644 index 000000000000..74f378ddba13 --- /dev/null +++ b/libs/langchain-community/src/chat_models/tests/chatovhcloud.int.test.ts @@ -0,0 +1,103 @@ +import { describe, test, expect } from "@jest/globals"; +import { HumanMessage, SystemMessage } from "@langchain/core/messages"; +import { z } from "zod/v3"; + +import { ChatOVHCloudAIEndpoints } from "../ovhcloud.js"; + +const model = "gpt-oss-120b"; + +describe("ChatOVHCloudAIEndpoints", () => { + test("should call ChatOVHCloudAIEndpoints", async () => { + const chat = new ChatOVHCloudAIEndpoints({ + model, + }); + const message = new HumanMessage("What is the capital of France?"); + const response = await chat.invoke([message], {}); + expect(response.content.length).toBeGreaterThan(10); + }); + + test("aggregated response using streaming", async () => { + const chat = new ChatOVHCloudAIEndpoints({ + model, + streaming: true, + }); + const message = new HumanMessage("What is the capital of France?"); + const response = await chat.invoke([message], {}); + expect(response.content.length).toBeGreaterThan(10); + }); + + test("use invoke", async () => { + const chat = new ChatOVHCloudAIEndpoints({ + model, + }); + const response = await chat.invoke("What is the capital of France?"); + expect(response.content.length).toBeGreaterThan(10); + }); + + test("should handle streaming", async () => { + const chat = new ChatOVHCloudAIEndpoints({ + streaming: true, + model, + }); + const message = new HumanMessage("What is the capital of France?"); + const stream = await chat.stream([message], {}); + const chunks = []; + for await (const chunk of stream) { + chunks.push(chunk); + } + expect(chunks.length).toBeGreaterThan(1); + expect(chunks.map((c) => c.content).join("")).toContain("Paris"); + }); + + test("should handle system messages", async () => { + const chat = new ChatOVHCloudAIEndpoints({ + model, + }); + const messages = [ + new SystemMessage("You are a geography expert."), + new HumanMessage("What is the capital of France?"), + ]; + const response = await chat.invoke(messages); + expect(response.content.length).toBeGreaterThan(10); + }); + + test("structured output", async () => { + const chat = new ChatOVHCloudAIEndpoints({ + model, + }).withStructuredOutput( + z.object({ + capital: z.string(), + country: z.string(), + }) + ); + const messages = [ + new SystemMessage("You are a geography expert."), + new HumanMessage("What is the capital of France? Return JSON."), + ]; + const response = await chat.invoke(messages); + expect(response.capital).toBe("Paris"); + expect(response.country).toBe("France"); + }); + + test("reasoning model with structured output", async () => { + const chat = new ChatOVHCloudAIEndpoints({ + model, + }).withStructuredOutput( + z.object({ + capital: z.string(), + country: z.string(), + }), + { + name: "reasoning_response", + } + ); + + const messages = [ + new SystemMessage("You are a geography expert."), + new HumanMessage("What is the capital of France? Return JSON."), + ]; + const response = await chat.invoke(messages); + expect(response.capital).toBe("Paris"); + expect(response.country).toBe("France"); + }); +}); diff --git a/libs/langchain-community/src/embeddings/ovhcloud.ts b/libs/langchain-community/src/embeddings/ovhcloud.ts new file mode 100644 index 000000000000..c109c80a941e --- /dev/null +++ b/libs/langchain-community/src/embeddings/ovhcloud.ts @@ -0,0 +1,53 @@ +import { + OpenAIEmbeddings, + type OpenAIEmbeddingsParams, +} from "@langchain/openai"; +import { getEnvironmentVariable } from "@langchain/core/utils/env"; + +export interface OVHcloudAIEndpointsEmbeddingsParams + extends Partial> { + /** + * The OVHcloud API key to use for requests. + * @default process.env.OVHCLOUD_AI_ENDPOINTS_API_KEY + */ + apiKey?: string; +} + +/** + * OVHcloud AI Endpoints embeddings integration. + * + * OVHcloud AI Endpoints is compatible with the OpenAI API. + * Base URL: https://oai.endpoints.kepler.ai.cloud.ovh.net/v1 + * + * Setup: + * Install `@langchain/community` and set an environment variable named `OVHCLOUD_AI_ENDPOINTS_API_KEY`. + * If no API key is provided, the model can still be used but with a rate limit. + * + * ```bash + * npm install @langchain/community + * export OVHCLOUD_AI_ENDPOINTS_API_KEY="your-api-key" + * ``` + * + */ +export class OVHcloudAIEndpointsEmbeddings extends OpenAIEmbeddings { + constructor(fields?: OVHcloudAIEndpointsEmbeddingsParams) { + const apiKey = + fields?.apiKey || getEnvironmentVariable("OVHCLOUD_AI_ENDPOINTS_API_KEY"); + + if (!apiKey) { + console.warn( + "OVHcloud AI Endpoints API key not found. You can use the model but with a rate limit. " + + "Set the OVHCLOUD_AI_ENDPOINTS_API_KEY environment variable or provide the key via 'apiKey' for unlimited access." + ); + } + + super({ + ...fields, + apiKey: apiKey || "", + model: fields?.model || "bge-multilingual-gemma2", + configuration: { + baseURL: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1", + }, + }); + } +} diff --git a/libs/langchain-community/src/embeddings/tests/ovhcloud.int.test.ts b/libs/langchain-community/src/embeddings/tests/ovhcloud.int.test.ts new file mode 100644 index 000000000000..b6a5017f23f4 --- /dev/null +++ b/libs/langchain-community/src/embeddings/tests/ovhcloud.int.test.ts @@ -0,0 +1,19 @@ +import { test, expect } from "@jest/globals"; +import { OVHcloudAIEndpointsEmbeddings } from "../ovhcloud.js"; + +test("Test OVHcloudAIEndpointsEmbeddings.embedQuery", async () => { + const embeddings = new OVHcloudAIEndpointsEmbeddings(); + const res = await embeddings.embedQuery("Hello world"); + expect(typeof res[0]).toBe("number"); + expect(res.length).toBe(3584); +}); + +test("Test OVHcloudAIEndpointsEmbeddings.embedDocuments", async () => { + const embeddings = new OVHcloudAIEndpointsEmbeddings(); + const res = await embeddings.embedDocuments(["Hello world", "Bye bye"]); + expect(res).toHaveLength(2); + expect(typeof res[0][0]).toBe("number"); + expect(typeof res[1][0]).toBe("number"); + expect(res[0].length).toBe(3584); + expect(res[1].length).toBe(3584); +}); diff --git a/libs/langchain-community/src/load/import_map.ts b/libs/langchain-community/src/load/import_map.ts index 1886f66c420d..fdbc8b31df82 100644 --- a/libs/langchain-community/src/load/import_map.ts +++ b/libs/langchain-community/src/load/import_map.ts @@ -45,6 +45,7 @@ export * as embeddings__ibm from "../embeddings/ibm.js"; export * as embeddings__jina from "../embeddings/jina.js"; export * as embeddings__llama_cpp from "../embeddings/llama_cpp.js"; export * as embeddings__minimax from "../embeddings/minimax.js"; +export * as embeddings__ovhcloud from "../embeddings/ovhcloud.js"; export * as embeddings__premai from "../embeddings/premai.js"; export * as embeddings__tensorflow from "../embeddings/tensorflow.js"; export * as embeddings__tencent_hunyuan from "../embeddings/tencent_hunyuan/index.js"; @@ -127,6 +128,7 @@ export * as chat_models__llama_cpp from "../chat_models/llama_cpp.js"; export * as chat_models__minimax from "../chat_models/minimax.js"; export * as chat_models__moonshot from "../chat_models/moonshot.js"; export * as chat_models__novita from "../chat_models/novita.js"; +export * as chat_models__ovhcloud from "../chat_models/ovhcloud.js"; export * as chat_models__perplexity from "../chat_models/perplexity.js"; export * as chat_models__portkey from "../chat_models/portkey.js"; export * as chat_models__premai from "../chat_models/premai.js"; diff --git a/libs/langchain-community/src/load/import_type.ts b/libs/langchain-community/src/load/import_type.ts index 1e333ca41014..295de6af25e7 100644 --- a/libs/langchain-community/src/load/import_type.ts +++ b/libs/langchain-community/src/load/import_type.ts @@ -32,6 +32,7 @@ export interface SecretMap { MINIMAX_GROUP_ID?: string; MOONSHOT_API_KEY?: string; NOVITA_API_KEY?: string; + OVHCLOUD_AI_ENDPOINTS_API_KEY?: string; PLANETSCALE_DATABASE_URL?: string; PLANETSCALE_HOST?: string; PLANETSCALE_PASSWORD?: string; diff --git a/libs/langchain/src/chat_models/tests/universal.int.test.ts b/libs/langchain/src/chat_models/tests/universal.int.test.ts index 37912e001f53..e8bf813c300d 100644 --- a/libs/langchain/src/chat_models/tests/universal.int.test.ts +++ b/libs/langchain/src/chat_models/tests/universal.int.test.ts @@ -576,6 +576,17 @@ describe("Works with all model providers", () => { expect(perplexityResult).toBeDefined(); expect(perplexityResult.content.length).toBeGreaterThan(0); }); + + it("Can invoke ovhcloud", async () => { + const ovhcloud = await initChatModel(undefined, { + modelProvider: "ovhcloud", + temperature: 0, + }); + + const ovhcloudResult = await ovhcloud.invoke("what's your name"); + expect(ovhcloudResult).toBeDefined(); + expect(ovhcloudResult.content.length).toBeGreaterThan(0); + }); }); /** diff --git a/libs/langchain/src/chat_models/universal.ts b/libs/langchain/src/chat_models/universal.ts index e7d04d510a92..08e26fac05f9 100644 --- a/libs/langchain/src/chat_models/universal.ts +++ b/libs/langchain/src/chat_models/universal.ts @@ -124,6 +124,11 @@ export const MODEL_PROVIDER_CONFIG = { className: "ChatPerplexity", hasCircularDependency: true, }, + ovhcloud: { + package: "@langchain/community/chat_models/ovhcloud", + className: "ChatOVHCloudAIEndpoints", + hasCircularDependency: true, + }, } as const; const SUPPORTED_PROVIDERS = Object.keys( @@ -656,6 +661,7 @@ export async function initChatModel< * - mistralai (@langchain/mistralai) * - groq (@langchain/groq) * - ollama (@langchain/ollama) + * - ovhcloud (@langchain/community/chat_models/ovhcloud) * - perplexity (@langchain/community/chat_models/perplexity) * - cerebras (@langchain/cerebras) * - deepseek (@langchain/deepseek) diff --git a/libs/langchain/src/hub/base.ts b/libs/langchain/src/hub/base.ts index 4dacb639b2b7..c57d9943e8c0 100644 --- a/libs/langchain/src/hub/base.ts +++ b/libs/langchain/src/hub/base.ts @@ -117,6 +117,8 @@ export function generateModelImportMap( importMapKey = "chat_models__fireworks"; } else if (modelLcName === "ChatGroq") { importMapKey = "chat_models__groq"; + } else if (modelLcName === "ChatOVHCloudAIEndpoints") { + importMapKey = "chat_models__ovhcloud"; } else { throw new Error("Received unsupported model class when pulling prompt."); }