Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/edenai-provider.md
Original file line number Diff line number Diff line change
@@ -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/<vendor>/<model>` 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.
1 change: 1 addition & 0 deletions examples/with-edenai/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
EDENAI_API_KEY=your_edenai_api_key
4 changes: 4 additions & 0 deletions examples/with-edenai/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist
.DS_Store
.voltagent/
73 changes: 73 additions & 0 deletions examples/with-edenai/README.md
Original file line number Diff line number Diff line change
@@ -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/<vendor>/<model>` 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/<vendor>/<model>`. 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
```
38 changes: 38 additions & 0 deletions examples/with-edenai/package.json
Original file line number Diff line number Diff line change
@@ -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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add --env-file=.env to the start script.

To ensure the example works consistently when users follow the README instruction to test the compiled output (pnpm build && pnpm start), the start script needs to load the .env file, just like the dev script does.

🐛 Proposed fix
-    "start": "node dist/index.js",
+    "start": "node --env-file=.env dist/index.js",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"start": "node dist/index.js",
"start": "node --env-file=.env dist/index.js",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/with-edenai/package.json` at line 34, Update the package.json start
script to invoke Node with --env-file=.env before dist/index.js, matching the
existing dev script’s environment loading behavior while preserving the
compiled-output startup command.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The start script doesn't load environment variables from .env, unlike the dev script which uses --env-file=.env. Since src/index.ts throws immediately if EDENAI_API_KEY is missing, running pnpm build && pnpm start (as the README suggests) will fail unless the user manually exports the variable. Adding --env-file=.env keeps behavior consistent with the dev script and the documented workflow.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/with-edenai/package.json, line 34:

<comment>The `start` script doesn't load environment variables from `.env`, unlike the `dev` script which uses `--env-file=.env`. Since `src/index.ts` throws immediately if `EDENAI_API_KEY` is missing, running `pnpm build && pnpm start` (as the README suggests) will fail unless the user manually exports the variable. Adding `--env-file=.env` keeps behavior consistent with the dev script and the documented workflow.</comment>

<file context>
@@ -0,0 +1,38 @@
+  "scripts": {
+    "build": "tsc",
+    "dev": "tsx watch --env-file=.env ./src",
+    "start": "node dist/index.js",
+    "volt": "volt"
+  },
</file context>

"volt": "volt"
},
"type": "module"
}
44 changes: 44 additions & 0 deletions examples/with-edenai/src/index.ts
Original file line number Diff line number Diff line change
@@ -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/<vendor>/<model>"
// 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,
});
})();
14 changes: 14 additions & 0 deletions examples/with-edenai/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
117 changes: 117 additions & 0 deletions packages/core/src/registries/model-provider-registry-edenai.spec.ts
Original file line number Diff line number Diff line change
@@ -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),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The safeStringify mock uses JSON.stringify directly. If the project's coding guidelines discourage JSON.stringify usage (preferring safeStringify from @voltagent/internal for safety), even test mocks should avoid it to stay consistent. Since no test here actually depends on the serialization output, a simpler stub like String(value) or a fixed string return would satisfy the mock contract without importing the discouraged primitive.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/registries/model-provider-registry-edenai.spec.ts, line 8:

<comment>The `safeStringify` mock uses `JSON.stringify` directly. If the project's coding guidelines discourage `JSON.stringify` usage (preferring `safeStringify` from `@voltagent/internal` for safety), even test mocks should avoid it to stay consistent. Since no test here actually depends on the serialization output, a simpler stub like `String(value)` or a fixed string return would satisfy the mock contract without importing the discouraged primitive.</comment>

<file context>
@@ -0,0 +1,117 @@
+
+// Mock @voltagent/internal to avoid build dependency
+vi.mock("@voltagent/internal", () => ({
+  safeStringify: (value: unknown) => JSON.stringify(value),
+}));
+
</file context>

}));
Comment on lines +7 to +9

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove JSON.stringify to comply with coding guidelines.

As per coding guidelines, JSON.stringify should never be used. Even within a test mock, this strict rule must be adhered to. You can use a simpler primitive like String or explicitly return a stubbed string representation instead of relying on JSON.stringify.

♻️ Proposed alternative
 // Mock `@voltagent/internal` to avoid build dependency
 vi.mock("`@voltagent/internal`", () => ({
-  safeStringify: (value: unknown) => JSON.stringify(value),
+  safeStringify: (value: unknown) => String(value),
 }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
vi.mock("@voltagent/internal", () => ({
safeStringify: (value: unknown) => JSON.stringify(value),
}));
vi.mock("`@voltagent/internal`", () => ({
safeStringify: (value: unknown) => String(value),
}));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/registries/model-provider-registry-edenai.spec.ts` around
lines 7 - 9, Update the safeStringify mock in the test setup to remove
JSON.stringify, using String or an explicit stubbed string representation while
preserving the mock’s string-returning behavior.

Source: Coding guidelines


// 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<string, unknown>).___voltagent_model_provider_registry = undefined;
process.env = { ...originalEnv };
});

afterEach(() => {
process.env = originalEnv;
(globalThis as Record<string, unknown>).___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<string, unknown>;
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 "<vendor>/<model>", 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<string, unknown>;
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<string, unknown>;
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/,
);
});
});
8 changes: 8 additions & 0 deletions packages/core/src/registries/model-provider-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.