From 18aae4d0127a33fd9c7d7e448c62278e0847c66c Mon Sep 17 00:00:00 2001 From: "Victor M. SMITH" <72023257+MVS-source@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:46:37 +0200 Subject: [PATCH] feat(core): add Eden AI as a built-in model provider Eden AI is an EU-based, OpenAI-compatible LLM gateway. It is registered as an EXTRA_PROVIDER_REGISTRY entry wired through the existing @ai-sdk/openai-compatible adapter, exactly like MiniMax, and reached with edenai// ids (e.g. edenai/openai/gpt-4o-mini). The provider reads EDENAI_API_KEY and defaults to https://api.edenai.run/v3, with EDENAI_BASE_URL supported for the EU endpoint. Adds a registry spec mirroring the MiniMax test and a with-edenai example. --- .changeset/edenai-provider.md | 5 + examples/with-edenai/.env.example | 1 + examples/with-edenai/.gitignore | 4 + examples/with-edenai/README.md | 73 +++++++++++ examples/with-edenai/package.json | 38 ++++++ examples/with-edenai/src/index.ts | 44 +++++++ examples/with-edenai/tsconfig.json | 14 +++ .../model-provider-registry-edenai.spec.ts | 117 ++++++++++++++++++ .../src/registries/model-provider-registry.ts | 8 ++ pnpm-lock.yaml | 31 +++++ 10 files changed, 335 insertions(+) create mode 100644 .changeset/edenai-provider.md create mode 100644 examples/with-edenai/.env.example create mode 100644 examples/with-edenai/.gitignore create mode 100644 examples/with-edenai/README.md create mode 100644 examples/with-edenai/package.json create mode 100644 examples/with-edenai/src/index.ts create mode 100644 examples/with-edenai/tsconfig.json create mode 100644 packages/core/src/registries/model-provider-registry-edenai.spec.ts diff --git a/.changeset/edenai-provider.md b/.changeset/edenai-provider.md new file mode 100644 index 000000000..620d9cf06 --- /dev/null +++ b/.changeset/edenai-provider.md @@ -0,0 +1,5 @@ +--- +"@voltagent/core": patch +--- + +Add Eden AI as a built-in model provider. Eden AI is an EU-based, OpenAI-compatible LLM gateway, so it is registered through the shared `@ai-sdk/openai-compatible` adapter and reached with `edenai//` model ids (e.g. `edenai/openai/gpt-4o-mini`). Reads `EDENAI_API_KEY` and defaults to `https://api.edenai.run/v3`, with `EDENAI_BASE_URL` supported for the EU endpoint. diff --git a/examples/with-edenai/.env.example b/examples/with-edenai/.env.example new file mode 100644 index 000000000..1695c8b9c --- /dev/null +++ b/examples/with-edenai/.env.example @@ -0,0 +1 @@ +EDENAI_API_KEY=your_edenai_api_key diff --git a/examples/with-edenai/.gitignore b/examples/with-edenai/.gitignore new file mode 100644 index 000000000..ad7241f15 --- /dev/null +++ b/examples/with-edenai/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.DS_Store +.voltagent/ diff --git a/examples/with-edenai/README.md b/examples/with-edenai/README.md new file mode 100644 index 000000000..c4e2f85b7 --- /dev/null +++ b/examples/with-edenai/README.md @@ -0,0 +1,73 @@ +# with-edenai + +A [VoltAgent](https://github.com/VoltAgent/voltagent) application using Eden AI, an EU-based OpenAI-compatible LLM gateway, through the built-in model registry. + +## Getting Started + +### Prerequisites + +- Node.js (v20 or newer) +- npm, yarn, or pnpm +- An Eden AI API key + +### Installation + +1. Clone this repository +2. Install dependencies +3. Copy `.env.example` to `.env` +4. Set `EDENAI_API_KEY` + +```bash +npm install +# or +yarn +# or +pnpm install +``` + +### Development + +Run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +``` + +The example starts a VoltAgent server on port `3141`. + +The dev script uses `tsx watch --env-file=.env ./src`, matching the example's `package.json` setup. + +## What This Example Shows + +- `Agent` configured with Eden AI via a built-in `edenai//` model string +- Model routing through Eden AI's OpenAI-compatible API (`https://api.edenai.run/v3`) +- Local memory storage with LibSQL +- Hono server integration through `@voltagent/server-hono` + +## Notes + +Eden AI model ids are vendor-prefixed, e.g. `openai/gpt-4o-mini`, `anthropic/claude-haiku-4-5` or `mistral/mistral-small-latest`, so the full model string is `edenai//`. Set `EDENAI_BASE_URL=https://api.eu.edenai.run/v3` for EU data residency. + +Use `pnpm build && pnpm start` for the compiled output, or `pnpm dev` during development. + +## Project Structure + +```text +. +├── src/ +│ └── index.ts +├── .env.example +├── package.json +├── tsconfig.json +└── README.md +``` + +## Try Example + +```bash +npm create voltagent-app@latest -- --example with-edenai +``` diff --git a/examples/with-edenai/package.json b/examples/with-edenai/package.json new file mode 100644 index 000000000..452ccecbd --- /dev/null +++ b/examples/with-edenai/package.json @@ -0,0 +1,38 @@ +{ + "name": "voltagent-example-with-edenai", + "author": "", + "dependencies": { + "@voltagent/cli": "^0.1.21", + "@voltagent/core": "^2.9.1", + "@voltagent/libsql": "^2.1.2", + "@voltagent/logger": "^2.0.2", + "@voltagent/server-hono": "^2.0.14", + "ai": "^6.0.0" + }, + "devDependencies": { + "@types/node": "^24.2.1", + "tsx": "^4.21.0", + "typescript": "^5.8.2" + }, + "keywords": [ + "agent", + "ai", + "edenai", + "eden-ai", + "voltagent" + ], + "license": "MIT", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/VoltAgent/voltagent.git", + "directory": "examples/with-edenai" + }, + "scripts": { + "build": "tsc", + "dev": "tsx watch --env-file=.env ./src", + "start": "node dist/index.js", + "volt": "volt" + }, + "type": "module" +} diff --git a/examples/with-edenai/src/index.ts b/examples/with-edenai/src/index.ts new file mode 100644 index 000000000..55f72fc62 --- /dev/null +++ b/examples/with-edenai/src/index.ts @@ -0,0 +1,44 @@ +import { Agent, Memory, VoltAgent } from "@voltagent/core"; +import { LibSQLMemoryAdapter } from "@voltagent/libsql"; +import { createPinoLogger } from "@voltagent/logger"; +import { honoServer } from "@voltagent/server-hono"; + +if (!process.env.EDENAI_API_KEY) { + throw new Error("EDENAI_API_KEY is required"); +} + +const logger = createPinoLogger({ + name: "with-edenai", + level: "info", +}); + +// Eden AI is registered as a built-in provider, so a plain "edenai//" +// model string is resolved through the model registry (reads EDENAI_API_KEY and +// defaults to https://api.edenai.run/v3). Set EDENAI_BASE_URL=https://api.eu.edenai.run/v3 +// for EU data residency. Eden AI model ids are vendor-prefixed, e.g. +// "openai/gpt-4o-mini", "anthropic/claude-haiku-4-5" or "mistral/mistral-small-latest". +const agent = new Agent({ + name: "Eden AI Assistant", + instructions: "You are a helpful assistant powered by Eden AI.", + model: "edenai/openai/gpt-4o-mini", + memory: new Memory({ + storage: new LibSQLMemoryAdapter(), + }), +}); + +new VoltAgent({ + agents: { + agent, + }, + logger, + server: honoServer(), +}); + +(async () => { + const result = await agent.generateText("Explain how observability helps with AI cost control."); + + logger.info("Eden AI example request completed", { + text: result.text, + usage: result.usage, + }); +})(); diff --git a/examples/with-edenai/tsconfig.json b/examples/with-edenai/tsconfig.json new file mode 100644 index 000000000..cee90c6f3 --- /dev/null +++ b/examples/with-edenai/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/core/src/registries/model-provider-registry-edenai.spec.ts b/packages/core/src/registries/model-provider-registry-edenai.spec.ts new file mode 100644 index 000000000..b73c86f96 --- /dev/null +++ b/packages/core/src/registries/model-provider-registry-edenai.spec.ts @@ -0,0 +1,117 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +// Track calls to createOpenAICompatible across module resets +let createOpenAICompatibleCalls: unknown[][] = []; + +// Mock @voltagent/internal to avoid build dependency +vi.mock("@voltagent/internal", () => ({ + safeStringify: (value: unknown) => JSON.stringify(value), +})); + +// Mock @ai-sdk/openai-compatible with a tracking wrapper +vi.mock("@ai-sdk/openai-compatible", () => ({ + createOpenAICompatible: (...args: unknown[]) => { + createOpenAICompatibleCalls.push(args); + const mockModel = { + modelId: "mock-model", + specificationVersion: "v1", + provider: "edenai", + }; + return { + languageModel: () => mockModel, + chatModel: () => mockModel, + }; + }, +})); + +describe("Eden AI provider registry", () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + createOpenAICompatibleCalls = []; + (globalThis as Record).___voltagent_model_provider_registry = undefined; + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + (globalThis as Record).___voltagent_model_provider_registry = undefined; + }); + + it("should list edenai as a registered provider", async () => { + const { ModelProviderRegistry } = await import("./model-provider-registry"); + const registry = ModelProviderRegistry.getInstance(); + const providers = registry.listProviders(); + expect(providers).toContain("edenai"); + }); + + it("should load edenai provider via @ai-sdk/openai-compatible", async () => { + process.env.EDENAI_API_KEY = "test-key-edenai"; + + const { ModelProviderRegistry } = await import("./model-provider-registry"); + const registry = ModelProviderRegistry.getInstance(); + const model = await registry.resolveLanguageModel("edenai/openai/gpt-4o-mini"); + + expect(model).toBeDefined(); + expect(createOpenAICompatibleCalls.length).toBeGreaterThan(0); + + const lastCall = createOpenAICompatibleCalls[createOpenAICompatibleCalls.length - 1]; + const config = lastCall[0] as Record; + expect(config.name).toBe("edenai"); + expect(config.baseURL).toBe("https://api.edenai.run/v3"); + expect(config.apiKey).toBe("test-key-edenai"); + }); + + it("should keep the vendor-prefixed model id after the provider segment", async () => { + process.env.EDENAI_API_KEY = "test-key"; + + const { ModelProviderRegistry } = await import("./model-provider-registry"); + const registry = ModelProviderRegistry.getInstance(); + + // Eden AI ids are "/", so the full router id has two slashes; + // only the first segment ("edenai") is the provider. + const modelIds = [ + "openai/gpt-4o-mini", + "anthropic/claude-haiku-4-5", + "mistral/mistral-small-latest", + ]; + + for (const modelId of modelIds) { + const model = await registry.resolveLanguageModel(`edenai/${modelId}`); + expect(model).toBeDefined(); + } + }); + + it("should support EDENAI_BASE_URL override", async () => { + process.env.EDENAI_API_KEY = "test-key"; + process.env.EDENAI_BASE_URL = "https://api.eu.edenai.run/v3"; + + const { ModelProviderRegistry } = await import("./model-provider-registry"); + const registry = ModelProviderRegistry.getInstance(); + await registry.resolveLanguageModel("edenai/openai/gpt-4o-mini"); + + const edenaiCall = createOpenAICompatibleCalls.find((call) => { + const config = call[0] as Record; + return config.name === "edenai"; + }); + expect(edenaiCall).toBeDefined(); + if (!edenaiCall) { + throw new Error("Expected edenai provider call to be recorded"); + } + const config = edenaiCall[0] as Record; + expect(config.baseURL).toBe("https://api.eu.edenai.run/v3"); + }); + + it("should throw if EDENAI_API_KEY is not set", async () => { + process.env = Object.fromEntries( + Object.entries(process.env).filter(([key]) => key !== "EDENAI_API_KEY"), + ); + + const { ModelProviderRegistry } = await import("./model-provider-registry"); + const registry = ModelProviderRegistry.getInstance(); + + await expect(registry.resolveLanguageModel("edenai/openai/gpt-4o-mini")).rejects.toThrow( + /EDENAI_API_KEY/, + ); + }); +}); diff --git a/packages/core/src/registries/model-provider-registry.ts b/packages/core/src/registries/model-provider-registry.ts index 5de3c0614..174f723a3 100644 --- a/packages/core/src/registries/model-provider-registry.ts +++ b/packages/core/src/registries/model-provider-registry.ts @@ -160,6 +160,14 @@ const EXTRA_PROVIDER_REGISTRY: ModelProviderRegistryEntry[] = [ env: ["MINIMAX_API_KEY"], doc: "https://platform.minimaxi.com/docs/api-reference/api-overview", }, + { + id: "edenai", + name: "Eden AI", + npm: "@ai-sdk/openai-compatible", + api: "https://api.edenai.run/v3", + env: ["EDENAI_API_KEY"], + doc: "https://www.edenai.co/docs", + }, ]; // EXTRA entries first so they take precedence over auto-generated entries diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8865a8b7..56b18a406 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1154,6 +1154,37 @@ importers: specifier: ^5.8.2 version: 5.9.2 + examples/with-edenai: + dependencies: + '@voltagent/cli': + specifier: ^0.1.21 + version: link:../../packages/cli + '@voltagent/core': + specifier: ^2.9.1 + version: link:../../packages/core + '@voltagent/libsql': + specifier: ^2.1.2 + version: link:../../packages/libsql + '@voltagent/logger': + specifier: ^2.0.2 + version: link:../../packages/logger + '@voltagent/server-hono': + specifier: ^2.0.14 + version: link:../../packages/server-hono + ai: + specifier: ^6.0.0 + version: 6.0.3(zod@4.3.5) + devDependencies: + '@types/node': + specifier: ^24.2.1 + version: 24.6.2 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.8.2 + version: 5.9.3 + examples/with-feedback: dependencies: '@voltagent/cli':