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",