Skip to content

Commit 4c33421

Browse files
committed
feat(provider): add native Eden AI support
1 parent 7130406 commit 4c33421

5 files changed

Lines changed: 196 additions & 0 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
2+
3+
export async function EdenAIAuthPlugin(_input: PluginInput): Promise<Hooks> {
4+
return {
5+
auth: {
6+
provider: "edenai",
7+
methods: [
8+
{
9+
type: "api",
10+
label: "Eden AI API Key",
11+
},
12+
],
13+
},
14+
}
15+
}

packages/opencode/src/plugin/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { CopilotAuthPlugin } from "./github-copilot/copilot"
1111
import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth"
1212
import { PoeAuthPlugin } from "opencode-poe-auth"
1313
import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare"
14+
import { EdenAIAuthPlugin } from "./edenai"
1415
import { Effect, Layer, ServiceMap, Stream } from "effect"
1516
import { InstanceState } from "@/effect/instance-state"
1617
import { makeRuntime } from "@/effect/run-service"
@@ -54,6 +55,7 @@ export namespace Plugin {
5455
PoeAuthPlugin,
5556
CloudflareWorkersAuthPlugin,
5657
CloudflareAIGatewayAuthPlugin,
58+
EdenAIAuthPlugin,
5759
]
5860

5961
function isServerPlugin(value: unknown): value is PluginInstance {

packages/opencode/src/provider/models-snapshot.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56527,6 +56527,12 @@ export const snapshot = {
5652756527
},
5652856528
},
5652956529
},
56530+
edenai: {
56531+
id: "edenai",
56532+
env: ["EDENAI_API_KEY"],
56533+
name: "Eden AI",
56534+
models: {},
56535+
},
5653056536
cerebras: {
5653156537
id: "cerebras",
5653256538
env: ["CEREBRAS_API_KEY"],

packages/opencode/src/provider/provider.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,105 @@ export namespace Provider {
525525
},
526526
}
527527
}),
528+
edenai: Effect.fnUntraced(function* (input: Info) {
529+
const auth = yield* dep.auth(input.id)
530+
const apiKey = yield* Effect.sync(() => {
531+
if (auth?.type === "api") return auth.key
532+
return Env.get("EDENAI_API_KEY")
533+
})
534+
535+
if (!apiKey) {
536+
return { autoload: false }
537+
}
538+
539+
const options = {
540+
baseURL: "https://api.edenai.run/v3/llm",
541+
apiKey,
542+
}
543+
544+
return {
545+
autoload: true,
546+
options,
547+
async discoverModels(): Promise<Record<string, Model>> {
548+
log.info("edenai model discovery starting")
549+
try {
550+
const res = await fetch("https://api.edenai.run/v3/llm/models", {
551+
headers: { Authorization: `Bearer ${apiKey}` },
552+
signal: AbortSignal.timeout(10000),
553+
})
554+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
555+
const json = await res.json()
556+
const models: Record<string, Model> = {}
557+
558+
for (const m of json.data || []) {
559+
const caps = m.capabilities || {}
560+
if (!caps.supports_function_calling) continue
561+
562+
const p = m.pricing || {}
563+
const cost = {
564+
input: (p.input_cost_per_token || 0) * 1000000,
565+
output: (p.output_cost_per_token || 0) * 1000000,
566+
cache: {
567+
read: (p.cache_read_input_token_cost || 0) * 1000000,
568+
write: (p.cache_creation_input_token_cost || 0) * 1000000,
569+
},
570+
}
571+
572+
models[m.id] = {
573+
id: ModelID.make(m.id),
574+
providerID: ProviderID.make("edenai"),
575+
name: m.model_name || m.id,
576+
family: "",
577+
api: {
578+
id: m.id,
579+
url: "https://api.edenai.run/v3/llm",
580+
npm: "@ai-sdk/openai-compatible",
581+
},
582+
status: "active",
583+
headers: {},
584+
options: {},
585+
cost,
586+
limit: {
587+
context: m.context_length || 128000,
588+
output: 4096,
589+
},
590+
capabilities: {
591+
temperature: true,
592+
reasoning: !!caps.supports_reasoning,
593+
attachment: false,
594+
toolcall: true,
595+
input: {
596+
text: (caps.input_modalities || []).includes("text"),
597+
audio: (caps.input_modalities || []).includes("audio"),
598+
image: (caps.input_modalities || []).includes("image"),
599+
video: (caps.input_modalities || []).includes("video"),
600+
pdf: false,
601+
},
602+
output: {
603+
text: (caps.output_modalities || []).includes("text"),
604+
audio: (caps.output_modalities || []).includes("audio"),
605+
image: (caps.output_modalities || []).includes("image"),
606+
video: (caps.output_modalities || []).includes("video"),
607+
pdf: false,
608+
},
609+
interleaved: false,
610+
},
611+
release_date: m.created ? new Date(m.created * 1000).toISOString() : "",
612+
variants: {},
613+
}
614+
}
615+
616+
log.info("edenai model discovery complete", {
617+
count: Object.keys(models).length,
618+
})
619+
return models
620+
} catch (e) {
621+
log.warn("edenai model discovery failed", { error: e })
622+
return {}
623+
}
624+
},
625+
}
626+
}),
528627
zenmux: () =>
529628
Effect.succeed({
530629
autoload: false,
@@ -1041,6 +1140,17 @@ export namespace Provider {
10411140
const modelsDev = yield* Effect.promise(() => ModelsDev.get())
10421141
const database = mapValues(modelsDev, fromModelsDevProvider)
10431142

1143+
if (!database["edenai"]) {
1144+
database["edenai"] = {
1145+
id: ProviderID.make("edenai"),
1146+
name: "Eden AI",
1147+
source: "custom",
1148+
env: ["EDENAI_API_KEY"],
1149+
options: { baseURL: "https://api.edenai.run/v3/llm" },
1150+
models: {},
1151+
}
1152+
}
1153+
10441154
const providers: Record<ProviderID, Info> = {} as Record<ProviderID, Info>
10451155
const languages = new Map<string, LanguageModelV3>()
10461156
const modelLoaders: {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { test, expect, describe, afterEach, beforeEach } from "bun:test"
2+
import { ProviderID } from "../../src/provider/schema"
3+
import { tmpdir } from "../fixture/fixture"
4+
import { Instance } from "../../src/project/instance"
5+
import { Provider } from "../../src/provider/provider"
6+
7+
describe("Eden AI Provider", () => {
8+
const originalFetch = global.fetch
9+
10+
beforeEach(() => {
11+
// @ts-expect-error
12+
global.fetch = async (url, options) => {
13+
if (url === "https://api.edenai.run/v3/llm/models") {
14+
return new Response(JSON.stringify({
15+
data: [
16+
{
17+
id: "fake-provider/awesome-model",
18+
model_name: "Awesome Model",
19+
capabilities: { supports_function_calling: true },
20+
pricing: { input_cost_per_token: 0.00003, output_cost_per_token: 0.00006 }
21+
}
22+
]
23+
}))
24+
}
25+
return originalFetch(url, options)
26+
}
27+
})
28+
29+
afterEach(() => {
30+
global.fetch = originalFetch
31+
delete process.env.EDENAI_API_KEY
32+
})
33+
34+
test("loads automatically when EDENAI_API_KEY is present", async () => {
35+
process.env.EDENAI_API_KEY = "test_key"
36+
await using tmp = await tmpdir()
37+
await Instance.provide({
38+
directory: tmp.path,
39+
fn: async () => {
40+
const providers = await Provider.list()
41+
const edenai = providers[ProviderID.make("edenai")]
42+
expect(edenai).toBeDefined()
43+
expect(edenai.source).toBe("env")
44+
expect(edenai.key).toBe("test_key")
45+
expect(edenai.options.baseURL).toBe("https://api.edenai.run/v3/llm")
46+
expect(Object.keys(edenai.models).length).toBe(1)
47+
expect(edenai.models["fake-provider/awesome-model"].name).toBe("Awesome Model")
48+
},
49+
})
50+
})
51+
52+
test("does not load automatically without EDENAI_API_KEY", async () => {
53+
await using tmp = await tmpdir()
54+
await Instance.provide({
55+
directory: tmp.path,
56+
fn: async () => {
57+
const providers = await Provider.list()
58+
const edenai = providers[ProviderID.make("edenai")]
59+
expect(edenai).toBeUndefined()
60+
},
61+
})
62+
})
63+
})

0 commit comments

Comments
 (0)