From dad65dd87e57bd137249768ff1f26f41dab73ba3 Mon Sep 17 00:00:00 2001 From: Nosenzor Date: Sat, 11 Jul 2026 02:18:55 +0200 Subject: [PATCH] feat(provider): add Eden AI as a first-class provider Eden AI (https://www.edenai.co) is an OpenAI-compatible aggregator gateway: one API key routes to 500+ models with smart routing and fallbacks. This wires it up like OpenRouter so it needs no manual opencode.json config. Because the provider catalog is sourced from models.dev (where Eden AI is not yet published), Eden AI ships as a built-in models.dev-shaped provider in packages/core/src/models-dev-builtin.ts. It is merged into the catalog via withBuiltinProviders() at every assembly site (v1 Provider service, v2 Catalog, the connect/list handlers and CLI), while ModelsDev.get() stays a faithful mirror of models.dev. A real models.dev entry, if one lands, always wins over the built-in. Details: - Curated default models: @edenai (Smart Router) plus popular provider/model ids (Claude, GPT-5, Gemini). Base URL https://api.edenai.run/v3, auth via EDENAI_API_KEY. - OpenRouter-style identity-header enrichment in both provider systems (legacy CUSTOMIZE map + v2 EdenAiPlugin). - packages/llm: edenai profile + provider (OpenAI-compatible chat with fallbacks/routerCandidates passthrough). - UI: icon, popular-providers entry, connect note (en). - Tests for the llm provider and the v2 plugin; updated models-dev and exports tests for the built-in. Co-Authored-By: Claude Opus 4.8 --- .../src/components/settings-v2/providers.tsx | 1 + packages/app/src/hooks/use-providers.ts | 1 + packages/app/src/i18n/en.ts | 1 + packages/core/src/models-dev-builtin.ts | 96 +++++++++++++++++++ packages/core/src/plugin/models-dev.ts | 5 +- packages/core/src/plugin/provider.ts | 2 + packages/core/src/plugin/provider/edenai.ts | 21 ++++ packages/core/test/plugin/models-dev.test.ts | 14 +++ .../core/test/plugin/provider-edenai.test.ts | 81 ++++++++++++++++ packages/llm/src/providers/edenai.ts | 94 ++++++++++++++++++ packages/llm/src/providers/index.ts | 1 + .../providers/openai-compatible-profile.ts | 1 + packages/llm/test/exports.test.ts | 4 + packages/llm/test/provider/edenai.test.ts | 66 +++++++++++++ .../opencode/src/cli/cmd/github.handler.ts | 3 +- packages/opencode/src/cli/cmd/providers.ts | 7 +- packages/opencode/src/provider/provider.ts | 13 ++- .../instance/httpapi/handlers/provider.ts | 3 +- packages/schema/src/provider.ts | 1 + .../ui/src/assets/icons/provider/edenai.svg | 4 + .../ui/src/components/provider-icons/types.ts | 1 + 21 files changed, 412 insertions(+), 8 deletions(-) create mode 100644 packages/core/src/models-dev-builtin.ts create mode 100644 packages/core/src/plugin/provider/edenai.ts create mode 100644 packages/core/test/plugin/provider-edenai.test.ts create mode 100644 packages/llm/src/providers/edenai.ts create mode 100644 packages/llm/test/provider/edenai.test.ts create mode 100644 packages/ui/src/assets/icons/provider/edenai.svg diff --git a/packages/app/src/components/settings-v2/providers.tsx b/packages/app/src/components/settings-v2/providers.tsx index f945fa33c643..dcff3444467d 100644 --- a/packages/app/src/components/settings-v2/providers.tsx +++ b/packages/app/src/components/settings-v2/providers.tsx @@ -24,6 +24,7 @@ const PROVIDER_NOTES = [ { match: (id: string) => id === "openai", key: "dialog.provider.openai.note" }, { match: (id: string) => id === "google", key: "dialog.provider.google.note" }, { match: (id: string) => id === "openrouter", key: "dialog.provider.openrouter.note" }, + { match: (id: string) => id === "edenai", key: "dialog.provider.edenai.note" }, { match: (id: string) => id === "vercel", key: "dialog.provider.vercel.note" }, ] as const diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts index 982c26ca9712..54a8759f8c5a 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -13,6 +13,7 @@ export const popularProviders = [ "openai", "google", "openrouter", + "edenai", "vercel", ] const popularProviderSet = new Set(popularProviders) diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 9d122447a8d0..c32058fcbe69 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -110,6 +110,7 @@ export const dict = { "dialog.provider.openai.note": "GPT models for fast, capable general AI tasks", "dialog.provider.google.note": "Gemini models for fast, structured responses", "dialog.provider.openrouter.note": "Access all supported models from one provider", + "dialog.provider.edenai.note": "Route to 500+ models with smart routing and fallbacks", "dialog.provider.vercel.note": "Unified access to AI models with smart routing", "dialog.model.select.title": "Select model", diff --git a/packages/core/src/models-dev-builtin.ts b/packages/core/src/models-dev-builtin.ts new file mode 100644 index 000000000000..7b3dcfc3bb76 --- /dev/null +++ b/packages/core/src/models-dev-builtin.ts @@ -0,0 +1,96 @@ +import type { Model, Provider } from "./models-dev" + +// Built-in providers that opencode ships as first-class citizens even though +// they are not (yet) published on models.dev. Each entry is shaped exactly like +// a models.dev provider record and is merged into `ModelsDev.get()`, so it flows +// into both the v1 (packages/opencode) and v2 (packages/core catalog) provider +// systems, `/connect` auth registration, and the model picker with no per-system +// wiring. A real models.dev entry, if one ever lands, wins over the built-in. + +// Eden AI (https://www.edenai.co) is an OpenAI-compatible aggregator gateway: +// one API key routes to 500+ models with smart routing and fallbacks. +// API reference: https://www.edenai.co/docs/v3/llms/chat-completions +// +// Costs and limits below are indicative list prices for the underlying models; +// Eden AI reports exact per-request cost at runtime. +const EDENAI_BASE_URL = "https://api.edenai.run/v3" + +function edenaiModel(id: string, name: string, overrides: Partial = {}): Model { + return { + id, + name, + release_date: "2025-01-01", + attachment: true, + reasoning: true, + temperature: true, + tool_call: true, + limit: { context: 200_000, output: 32_000 }, + modalities: { input: ["text", "image"], output: ["text"] }, + ...overrides, + } +} + +const edenai: Provider = { + id: "edenai", + name: "Eden AI", + npm: "@ai-sdk/openai-compatible", + api: EDENAI_BASE_URL, + env: ["EDENAI_API_KEY"], + models: { + // Smart Router — Eden AI picks the best model per request (quality, latency, + // cost, live provider health). Recommended default; needs no model config. + "@edenai": edenaiModel("@edenai", "Eden AI (Smart Router)", { + family: "edenai", + limit: { context: 200_000, output: 32_000 }, + modalities: { input: ["text", "image", "pdf"], output: ["text"] }, + }), + "anthropic/claude-sonnet-4-5": edenaiModel("anthropic/claude-sonnet-4-5", "Claude Sonnet 4.5", { + family: "claude", + release_date: "2025-09-29", + limit: { context: 200_000, output: 64_000 }, + modalities: { input: ["text", "image", "pdf"], output: ["text"] }, + cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, + }), + "anthropic/claude-opus-4-7": edenaiModel("anthropic/claude-opus-4-7", "Claude Opus 4.7", { + family: "claude", + release_date: "2025-11-24", + limit: { context: 200_000, output: 32_000 }, + modalities: { input: ["text", "image", "pdf"], output: ["text"] }, + cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, + }), + "anthropic/claude-haiku-4-5": edenaiModel("anthropic/claude-haiku-4-5", "Claude Haiku 4.5", { + family: "claude", + release_date: "2025-10-15", + limit: { context: 200_000, output: 64_000 }, + modalities: { input: ["text", "image", "pdf"], output: ["text"] }, + cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, + }), + "openai/gpt-5": edenaiModel("openai/gpt-5", "GPT-5", { + family: "gpt", + release_date: "2025-08-07", + temperature: false, + limit: { context: 400_000, output: 128_000 }, + modalities: { input: ["text", "image"], output: ["text"] }, + cost: { input: 1.25, output: 10 }, + }), + "google/gemini-2.5-pro": edenaiModel("google/gemini-2.5-pro", "Gemini 2.5 Pro", { + family: "gemini", + release_date: "2025-06-17", + limit: { context: 1_048_576, output: 65_536 }, + modalities: { input: ["text", "image", "pdf", "audio", "video"], output: ["text"] }, + cost: { input: 1.25, output: 10 }, + }), + }, +} + +export const BuiltinProviders: Record = { + edenai, +} + +// Merge the built-in providers into a models.dev catalog. Kept out of +// `ModelsDev.get()` (which stays a faithful mirror of models.dev) and applied +// wherever the provider catalog is assembled. A real models.dev entry, when +// present, always wins over the built-in of the same id. +export function withBuiltinProviders(data: Record): Record { + return { ...BuiltinProviders, ...data } +} diff --git a/packages/core/src/plugin/models-dev.ts b/packages/core/src/plugin/models-dev.ts index 075a6ed093c6..6c1219cd75b0 100644 --- a/packages/core/src/plugin/models-dev.ts +++ b/packages/core/src/plugin/models-dev.ts @@ -3,6 +3,7 @@ import type { ModelV2Info } from "@opencode-ai/sdk/v2/types" import { Effect, Stream } from "effect" import { EventV2 } from "../event" import { ModelsDev } from "../models-dev" +import { withBuiltinProviders } from "../models-dev-builtin" import { ProviderV2 } from "../provider" function released(date: string) { @@ -123,7 +124,7 @@ export const ModelsDevPlugin = define({ const events = yield* EventV2.Service yield* ctx.integration.transform( Effect.fn(function* (integrations) { - const data = yield* modelsDev.get() + const data = withBuiltinProviders(yield* modelsDev.get()) for (const item of Object.values(data)) { if (item.env.length === 0) continue const integrationID = item.id @@ -141,7 +142,7 @@ export const ModelsDevPlugin = define({ ) yield* ctx.catalog.transform( Effect.fn(function* (catalog) { - const data = yield* modelsDev.get() + const data = withBuiltinProviders(yield* modelsDev.get()) for (const item of Object.values(data)) { const providerID = ProviderV2.ID.make(item.id) catalog.provider.update(providerID, (provider) => { diff --git a/packages/core/src/plugin/provider.ts b/packages/core/src/plugin/provider.ts index 1749b474ed33..ac1a4e480054 100644 --- a/packages/core/src/plugin/provider.ts +++ b/packages/core/src/plugin/provider.ts @@ -8,6 +8,7 @@ import { CloudflareWorkersAIPlugin } from "./provider/cloudflare-workers-ai" import { CoherePlugin } from "./provider/cohere" import { DeepInfraPlugin } from "./provider/deepinfra" import { DynamicProviderPlugin } from "./provider/dynamic" +import { EdenAiPlugin } from "./provider/edenai" import { GatewayPlugin } from "./provider/gateway" import { GithubCopilotPlugin } from "./provider/github-copilot" import { GitLabPlugin } from "./provider/gitlab" @@ -44,6 +45,7 @@ export const ProviderPlugins: PluginInternal.Plugin { + provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" + provider.request.headers["X-Title"] = "opencode" + }) + } + }), + ) + }), +}) diff --git a/packages/core/test/plugin/models-dev.test.ts b/packages/core/test/plugin/models-dev.test.ts index 6c0f7e070296..d3430b1c952f 100644 --- a/packages/core/test/plugin/models-dev.test.ts +++ b/packages/core/test/plugin/models-dev.test.ts @@ -158,6 +158,20 @@ describe("ModelsDevPlugin", () => { ], connections: [], }), + // Eden AI ships as a built-in provider (see models-dev-builtin.ts), so it + // is always merged into the catalog and registers its env-based auth method. + new Integration.Info({ + id: Integration.ID.make("edenai"), + name: "Eden AI", + methods: [ + { type: "key" }, + { + type: "env", + names: ["EDENAI_API_KEY"], + }, + ], + connections: [], + }), ]) }).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))), (previous) => diff --git a/packages/core/test/plugin/provider-edenai.test.ts b/packages/core/test/plugin/provider-edenai.test.ts new file mode 100644 index 000000000000..28f06313f47f --- /dev/null +++ b/packages/core/test/plugin/provider-edenai.test.ts @@ -0,0 +1,81 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" +import { PluginV2 } from "@opencode-ai/core/plugin" +import { PluginHost } from "@opencode-ai/core/plugin/host" +import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" +import { EdenAiPlugin } from "@opencode-ai/core/plugin/provider/edenai" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { testEffect } from "../lib/effect" +import { PluginTestLayer } from "./fixture" + +const it = testEffect(PluginTestLayer) + +const addPlugin = Effect.fn(function* () { + const plugin = yield* PluginV2.Service + const host = yield* PluginHost.make(plugin) + yield* EdenAiPlugin.effect(host) +}) + +function required(value: T | undefined): T { + if (value === undefined) throw new Error("Expected value") + return value +} + +const edenaiApi = { + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://api.edenai.run/v3", +} as const + +describe("EdenAiPlugin", () => { + it.effect("is registered so opencode identity headers can be applied", () => + Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("edenai"))), + ) + + it.effect("applies the opencode identity headers to edenai", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + yield* catalog.transform((catalog) => { + catalog.provider.update(ProviderV2.ID.make("edenai"), (provider) => { + provider.api = { ...edenaiApi } + }) + }) + yield* addPlugin() + const result = required(yield* catalog.provider.get(ProviderV2.ID.make("edenai"))) + expect(result.request.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }) + }), + ) + + it.effect("merges opencode identity headers with existing headers", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + yield* catalog.transform((catalog) => { + catalog.provider.update(ProviderV2.ID.make("edenai"), (provider) => { + provider.api = { ...edenaiApi } + provider.request.headers.Existing = "value" + }) + }) + yield* addPlugin() + expect(required(yield* catalog.provider.get(ProviderV2.ID.make("edenai"))).request.headers).toEqual({ + Existing: "value", + "HTTP-Referer": "https://opencode.ai/", + "X-Title": "opencode", + }) + }), + ) + + it.effect("only touches the edenai openai-compatible endpoint", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + yield* catalog.transform((catalog) => { + // A different openai-compatible provider must be left untouched. + catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => { + provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" } + }) + }) + yield* addPlugin() + expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({}) + }), + ) +}) diff --git a/packages/llm/src/providers/edenai.ts b/packages/llm/src/providers/edenai.ts new file mode 100644 index 000000000000..fc6de8cec9a7 --- /dev/null +++ b/packages/llm/src/providers/edenai.ts @@ -0,0 +1,94 @@ +import { Effect, Schema } from "effect" +import { Route, type RouteDefaultsInput } from "../route/client" +import { Endpoint } from "../route/endpoint" +import { Framing } from "../route/framing" +import { Protocol } from "../route/protocol" +import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" +import { ProviderID, type ModelID, type ProviderOptions } from "../schema" +import * as OpenAICompatibleProfiles from "./openai-compatible-profile" +import * as OpenAIChat from "../protocols/openai-chat" +import { isRecord } from "../protocols/shared" + +export const profile = OpenAICompatibleProfiles.profiles.edenai +export const id = ProviderID.make(profile.provider) +const ADAPTER = "edenai" + +export interface EdenAIOptions { + readonly [key: string]: unknown + // Ordered list of `provider/model` fallbacks tried if the primary model fails. + readonly fallbacks?: ReadonlyArray + // Restrict the `@edenai` smart router to a subset of candidate models. + readonly routerCandidates?: ReadonlyArray +} + +export type EdenAIProviderOptionsInput = ProviderOptions & { + readonly edenai?: EdenAIOptions +} + +export type ModelOptions = Omit & + ProviderAuthOption<"optional"> & { + readonly baseURL?: string + readonly providerOptions?: EdenAIProviderOptionsInput + } + +const EdenAIBody = Schema.StructWithRest(Schema.Struct(OpenAIChat.bodyFields), [ + Schema.Record(Schema.String, Schema.Any), +]) +export type EdenAIBody = Schema.Schema.Type + +export const protocol = Protocol.make({ + id: "edenai-chat", + body: { + schema: EdenAIBody, + from: (request) => + OpenAIChat.protocol.body.from(request).pipe( + Effect.map( + (body) => + ({ + ...body, + ...bodyOptions(request.providerOptions?.edenai), + }) as EdenAIBody, + ), + ), + }, + stream: OpenAIChat.protocol.stream, +}) + +const bodyOptions = (input: unknown) => { + const edenai = isRecord(input) ? input : {} + return { + ...(Array.isArray(edenai.fallbacks) ? { fallbacks: edenai.fallbacks } : {}), + ...(Array.isArray(edenai.routerCandidates) ? { router_candidates: edenai.routerCandidates } : {}), + } +} + +export const route = Route.make({ + id: ADAPTER, + provider: profile.provider, + protocol, + endpoint: Endpoint.path("/chat/completions", { baseURL: profile.baseURL }), + framing: Framing.sse, +}) + +export const routes = [route] + +const configuredRoute = (input: ModelOptions) => { + const { apiKey: _, auth: _auth, baseURL, ...rest } = input + return route.with({ + ...rest, + endpoint: { baseURL: baseURL ?? profile.baseURL }, + auth: AuthOptions.bearer(input, "EDENAI_API_KEY"), + }) +} + +export const configure = (input: ModelOptions = {}) => { + const route = configuredRoute(input) + return { + id, + model: (modelID: string | ModelID) => route.model({ id: modelID }), + configure, + } +} + +export const provider = configure() +export const model = provider.model diff --git a/packages/llm/src/providers/index.ts b/packages/llm/src/providers/index.ts index 774274cf2d4a..1409a3ba6fd9 100644 --- a/packages/llm/src/providers/index.ts +++ b/packages/llm/src/providers/index.ts @@ -3,6 +3,7 @@ export * as AmazonBedrock from "./amazon-bedrock" export * as Azure from "./azure" export * as Cloudflare from "./cloudflare" export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare" +export * as EdenAI from "./edenai" export * as GitHubCopilot from "./github-copilot" export * as Google from "./google" export * as OpenAI from "./openai" diff --git a/packages/llm/src/providers/openai-compatible-profile.ts b/packages/llm/src/providers/openai-compatible-profile.ts index 30770c9671cd..ca731ac8406b 100644 --- a/packages/llm/src/providers/openai-compatible-profile.ts +++ b/packages/llm/src/providers/openai-compatible-profile.ts @@ -8,6 +8,7 @@ export const profiles = { cerebras: { provider: "cerebras", baseURL: "https://api.cerebras.ai/v1" }, deepinfra: { provider: "deepinfra", baseURL: "https://api.deepinfra.com/v1/openai" }, deepseek: { provider: "deepseek", baseURL: "https://api.deepseek.com/v1" }, + edenai: { provider: "edenai", baseURL: "https://api.edenai.run/v3" }, fireworks: { provider: "fireworks", baseURL: "https://api.fireworks.ai/inference/v1" }, groq: { provider: "groq", baseURL: "https://api.groq.com/openai/v1" }, openrouter: { provider: "openrouter", baseURL: "https://openrouter.ai/api/v1" }, diff --git a/packages/llm/test/exports.test.ts b/packages/llm/test/exports.test.ts index 4bed7e2e1357..6e287042fd47 100644 --- a/packages/llm/test/exports.test.ts +++ b/packages/llm/test/exports.test.ts @@ -5,6 +5,7 @@ import { Provider as ProviderSubpath } from "@opencode-ai/llm/provider" import { CloudflareAIGateway, CloudflareWorkersAI, + EdenAI, OpenAI, OpenAICompatible, OpenRouter, @@ -41,6 +42,9 @@ describe("public exports", () => { expect(CloudflareWorkersAI.configure({ accountId: "fixture", apiKey: "fixture" }).model).toBeFunction() expect(OpenRouter.model).toBeFunction() expect(OpenRouter.provider.model).toBe(OpenRouter.model) + expect(EdenAI.model).toBeFunction() + expect(EdenAI.provider.model).toBe(EdenAI.model) + expect(EdenAI.configure({ apiKey: "fixture" }).model("@edenai").route.id).toBe("edenai") expect(XAI.model).toBeFunction() expect(XAI.provider.model).toBe(XAI.model) expect(XAI.provider.responses).toBe(XAI.responses) diff --git a/packages/llm/test/provider/edenai.test.ts b/packages/llm/test/provider/edenai.test.ts new file mode 100644 index 000000000000..2a0339a07e98 --- /dev/null +++ b/packages/llm/test/provider/edenai.test.ts @@ -0,0 +1,66 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { LLM } from "../../src" +import { LLMClient } from "../../src/route" +import * as EdenAI from "../../src/providers/edenai" +import { it } from "../lib/effect" + +describe("EdenAI", () => { + it.effect("prepares EdenAI models through the OpenAI-compatible Chat route", () => + Effect.gen(function* () { + const model = EdenAI.configure({ apiKey: "test-key" }).model("openai/gpt-4o-mini") + + expect(model).toMatchObject({ + id: "openai/gpt-4o-mini", + provider: "edenai", + route: { id: "edenai" }, + }) + expect(model.route.endpoint.baseURL).toBe("https://api.edenai.run/v3") + + const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." })) + + expect(prepared.route).toBe("edenai") + expect(prepared.body).toMatchObject({ + model: "openai/gpt-4o-mini", + messages: [{ role: "user", content: "Say hello." }], + stream: true, + }) + }), + ) + + it.effect("targets the @edenai smart router model", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: EdenAI.configure({ apiKey: "test-key" }).model("@edenai"), + prompt: "Route this.", + }), + ) + expect(prepared.body).toMatchObject({ model: "@edenai" }) + }), + ) + + it.effect("applies EdenAI payload options from the model helper", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: EdenAI.configure({ + apiKey: "test-key", + providerOptions: { + edenai: { + fallbacks: ["openai/gpt-5", "anthropic/claude-sonnet-4-5"], + routerCandidates: ["anthropic/claude-haiku-4-5"], + }, + }, + }).model("@edenai"), + prompt: "Think briefly.", + }), + ) + + expect(prepared.body).toMatchObject({ + fallbacks: ["openai/gpt-5", "anthropic/claude-sonnet-4-5"], + router_candidates: ["anthropic/claude-haiku-4-5"], + }) + }), + ) +}) diff --git a/packages/opencode/src/cli/cmd/github.handler.ts b/packages/opencode/src/cli/cmd/github.handler.ts index d55c0bf3fbfa..cff53cf254c1 100644 --- a/packages/opencode/src/cli/cmd/github.handler.ts +++ b/packages/opencode/src/cli/cmd/github.handler.ts @@ -18,6 +18,7 @@ import type { } from "@octokit/webhooks-types" import { UI } from "../ui" import { ModelsDev } from "@opencode-ai/core/models-dev" +import { withBuiltinProviders } from "@opencode-ai/core/models-dev-builtin" import { InstanceRef } from "@/effect/instance-ref" import { SessionShare } from "@/share/session" import { Session } from "@/session/session" @@ -165,7 +166,7 @@ export const githubInstall = Effect.fn("Cli.github.install")(function* () { const app = await getAppInfo() await installGitHubApp() - const providers = await Effect.runPromise(modelsDev.get()).then((p) => { + const providers = await Effect.runPromise(modelsDev.get()).then(withBuiltinProviders).then((p) => { // TODO: add guide for copilot, for now just hide it delete p["github-copilot"] return p diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 3775123d83bd..6718b00b7177 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -5,6 +5,7 @@ import { CliError, effectCmd, fail } from "../effect-cmd" import { UI } from "../ui" import * as Prompt from "../effect/prompt" import { ModelsDev } from "@opencode-ai/core/models-dev" +import { withBuiltinProviders } from "@opencode-ai/core/models-dev-builtin" import { map, pipe, sortBy, values } from "remeda" import path from "path" @@ -261,7 +262,7 @@ export const ProvidersListCommand = effectCmd({ const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath yield* Prompt.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`) const results = Object.entries(yield* Effect.orDie(authSvc.all())) - const database = yield* modelsDev.get() + const database = withBuiltinProviders(yield* modelsDev.get()) for (const [providerID, result] of results) { const name = database[providerID]?.name || providerID @@ -361,7 +362,7 @@ export const ProvidersLoginCommand = effectCmd({ const disabled = new Set(config.disabled_providers ?? []) const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined - const allProviders = yield* modelsDev.get() + const allProviders = withBuiltinProviders(yield* modelsDev.get()) const providers: Record = {} for (const [key, value] of Object.entries(allProviders)) { if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) providers[key] = value @@ -509,7 +510,7 @@ export const ProvidersLogoutCommand = effectCmd({ yield* Prompt.log.error("No credentials found") return } - const database = yield* modelsDev.get() + const database = withBuiltinProviders(yield* modelsDev.get()) const options = credentials.map(([key, value]) => ({ label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")", value: key, diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c0a222d649bb..8ec41e2cb880 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -11,6 +11,7 @@ import { Plugin } from "../plugin" import { serviceUse } from "@opencode-ai/core/effect/service-use" import { type LanguageModelV3 } from "@ai-sdk/provider" import { ModelsDev } from "@opencode-ai/core/models-dev" +import { withBuiltinProviders } from "@opencode-ai/core/models-dev-builtin" import { Auth } from "../auth" import { Env } from "../env" import { InstallationVersion } from "@opencode-ai/core/installation/version" @@ -472,6 +473,16 @@ function custom(dep: CustomDep): Record { }, }, }), + edenai: () => + Effect.succeed({ + autoload: false, + options: { + headers: { + "HTTP-Referer": "https://opencode.ai/", + "X-Title": "opencode", + }, + }, + }), nvidia: (provider) => Effect.succeed({ autoload: provider.source === "config", @@ -1331,7 +1342,7 @@ const layer = Layer.effect( Effect.gen(function* () { const bridge = yield* EffectBridge.make() const cfg = yield* config.get() - const modelsDev = yield* modelsDevSvc.get() + const modelsDev = withBuiltinProviders(yield* modelsDevSvc.get()) const catalog = mapValues(modelsDev, fromModelsDevProvider) const database = mapValues(catalog, toPublicInfo) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts index e1377b6f75c5..4a49411407fa 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts @@ -1,6 +1,7 @@ import { ProviderAuth } from "@/provider/auth" import { Config } from "@/config/config" import { ModelsDev } from "@opencode-ai/core/models-dev" +import { withBuiltinProviders } from "@opencode-ai/core/models-dev-builtin" import { Provider } from "@/provider/provider" import { mapValues } from "remeda" @@ -39,7 +40,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider" const list = Effect.fn("ProviderHttpApi.list")(function* () { const config = yield* cfg.get() - const all = yield* ModelsDev.Service.use((s) => s.get()) + const all = withBuiltinProviders(yield* ModelsDev.Service.use((s) => s.get())) const disabled = new Set(config.disabled_providers ?? []) const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined const filtered: Record = {} diff --git a/packages/schema/src/provider.ts b/packages/schema/src/provider.ts index 51ff4b37913d..57b8392fce0c 100644 --- a/packages/schema/src/provider.ts +++ b/packages/schema/src/provider.ts @@ -17,6 +17,7 @@ export const ID = Schema.String.pipe( amazonBedrock: schema.make("amazon-bedrock"), azure: schema.make("azure"), openrouter: schema.make("openrouter"), + edenai: schema.make("edenai"), mistral: schema.make("mistral"), gitlab: schema.make("gitlab"), })), diff --git a/packages/ui/src/assets/icons/provider/edenai.svg b/packages/ui/src/assets/icons/provider/edenai.svg new file mode 100644 index 000000000000..30db85c5d8e5 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/edenai.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/ui/src/components/provider-icons/types.ts b/packages/ui/src/components/provider-icons/types.ts index 1c6f5fe6d2f0..4843eeb389f4 100644 --- a/packages/ui/src/components/provider-icons/types.ts +++ b/packages/ui/src/components/provider-icons/types.ts @@ -78,6 +78,7 @@ export const iconNames = [ "fireworks-ai", "fastrouter", "evroc", + "edenai", "digitalocean", "deepseek", "deepinfra",