-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(core): add Eden AI as a built-in model provider #1386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| EDENAI_API_KEY=your_edenai_api_key |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| node_modules | ||
| dist | ||
| .DS_Store | ||
| .voltagent/ |
| 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 | ||
| ``` |
| 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", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The Prompt for AI agents |
||
| "volt": "volt" | ||
| }, | ||
| "type": "module" | ||
| } | ||
| 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, | ||
| }); | ||
| })(); |
| 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"] | ||
| } |
| 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), | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The Prompt for AI agents |
||||||||||||||
| })); | ||||||||||||||
|
Comment on lines
+7
to
+9
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Remove As per coding guidelines, ♻️ 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
Suggested change
🤖 Prompt for AI AgentsSource: 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/, | ||||||||||||||
| ); | ||||||||||||||
| }); | ||||||||||||||
| }); | ||||||||||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
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=.envto thestartscript.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.envfile, just like thedevscript does.🐛 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents